import type { GitHost } from './git-host';
import { uint8ArrayToBase64, base64ToUint8Array, BLOB_THRESHOLD_BYTES } from './git-host';

/// GitHub Contents API implementation.
///
/// Endpoints:
///   GET    https://api.github.com/repos/{repoPath}/contents/{path}
///   PUT    https://api.github.com/repos/{repoPath}/contents/{path}   (create/update)
///   DELETE https://api.github.com/repos/{repoPath}/contents/{path}
///
/// Auth: `Bearer {apiToken}` header.
/// Content is base64-encoded. Updates and deletes require the current file SHA.

export class GitHubHost implements GitHost {
  private baseUrl: string;
  private gitApiBase: string;
  private commitsUrl: string;
  private branch: string = 'main';
  private headers: Record<string, string>;
  lastSyncAt: number | null = null;
  ahead = 0;
  behind = 0;

  constructor(repoPath: string, apiToken: string) {
    this.baseUrl = `https://api.github.com/repos/${repoPath}/contents`;
    this.gitApiBase = `https://api.github.com/repos/${repoPath}/git`;
    this.commitsUrl = `https://api.github.com/repos/${repoPath}/commits`;
    this.headers = {
      'Authorization': `Bearer ${apiToken}`,
      'Content-Type': 'application/json',
      'Accept': 'application/vnd.github.v3+json',
      'X-GitHub-Api-Version': '2022-11-28',
    };
  }

  async readFile(path: string): Promise<Uint8Array> {
    const resp = await fetch(`${this.baseUrl}/${path}`, {
      headers: this.headers,
    });
    if (!resp.ok) {
      throw new Error(`GitHub readFile ${path}: ${resp.status} ${resp.statusText}`);
    }
    const json = await resp.json();
    const clean = (json.content as string).replace(/\n/g, '');
    return base64ToUint8Array(clean);
  }

  async writeFile(path: string, content: Uint8Array, message: string): Promise<void> {
    const b64 = uint8ArrayToBase64(content);

    // Try to get the current SHA for an update; if 404 it's a create.
    let sha: string | null = null;
    try {
      const existing = await fetch(`${this.baseUrl}/${path}`, {
        headers: this.headers,
      });
      if (existing.ok) {
        const json = await existing.json();
        sha = json.sha as string;
      }
    } catch {
      // File does not exist — will create.
    }

    const body: Record<string, unknown> = { content: b64, message };
    if (sha) {
      body.sha = sha;
    }

    const resp = await fetch(`${this.baseUrl}/${path}`, {
      method: 'PUT',
      headers: this.headers,
      body: JSON.stringify(body),
    });

    if (!resp.ok) {
      const text = await resp.text();
      throw new Error(`GitHub writeFile ${path}: ${resp.status} ${text}`);
    }
  }

  async writeFileCreateOnly(path: string, content: Uint8Array, message: string): Promise<void> {
    const existing = await fetch(`${this.baseUrl}/${path}`, { headers: this.headers });
    if (existing.ok) {
      throw new Error(`writeFileCreateOnly: ${path} already exists`);
    }
    const b64 = uint8ArrayToBase64(content);
    const resp = await fetch(`${this.baseUrl}/${path}`, {
      method: 'PUT',
      headers: this.headers,
      body: JSON.stringify({ content: b64, message }),  // no sha → create-only
    });
    if (!resp.ok) {
      const text = await resp.text();
      throw new Error(`GitHub writeFileCreateOnly ${path}: ${resp.status} ${text}`);
    }
  }

  async deleteFile(path: string, message: string): Promise<void> {
    const existing = await fetch(`${this.baseUrl}/${path}`, {
      headers: this.headers,
    });
    if (!existing.ok) {
      throw new Error(`GitHub deleteFile ${path}: file not found (${existing.status})`);
    }
    const json = await existing.json();
    const sha = json.sha as string;

    const resp = await fetch(`${this.baseUrl}/${path}`, {
      method: 'DELETE',
      headers: this.headers,
      body: JSON.stringify({ message, sha }),
    });

    if (!resp.ok) {
      const text = await resp.text();
      throw new Error(`GitHub deleteFile ${path}: ${resp.status} ${text}`);
    }
  }

  async listDir(path: string): Promise<string[]> {
    const resp = await fetch(`${this.baseUrl}/${path}`, {
      headers: this.headers,
    });
    if (!resp.ok) {
      if (resp.status === 404) return [];
      throw new Error(`GitHub listDir ${path}: ${resp.status} ${resp.statusText}`);
    }
    const json = await resp.json();
    if (!Array.isArray(json)) return [];
    return json.map((item: { name: string }) => item.name);
  }

  async lastCommit(path: string): Promise<{ sha: string; author: string; date: string } | null> {
    try {
      const url = `${this.commitsUrl}?path=${encodeURIComponent(path)}&per_page=1`;
      const resp = await fetch(url, { headers: this.headers });
      if (!resp.ok) return null;
      const json = await resp.json();
      if (!Array.isArray(json) || json.length === 0) return null;
      const c = json[0];
      return {
        sha: String(c.sha).slice(0, 7),
        author: c.commit?.author?.name ?? 'unknown',
        date: c.commit?.author?.date ?? '',
      };
    } catch {
      return null;
    }
  }

  async putBlob(path: string, content: Uint8Array, message: string): Promise<string> {
    if (content.length <= BLOB_THRESHOLD_BYTES) {
      // Contents API path — same as writeFile
      await this.writeFile(path, content, message);
      return path;
    }

    // Git Data API fallback for large blobs.
    // 1. Create the blob.
    const blobResp = await fetch(`${this.gitApiBase}/blobs`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({
        content: uint8ArrayToBase64(content),
        encoding: 'base64',
      }),
    });
    if (!blobResp.ok) {
      const text = await blobResp.text();
      throw new Error(`GitHub putBlob create-blob ${path}: ${blobResp.status} ${text}`);
    }
    const { sha: blobSha } = await blobResp.json() as { sha: string };

    // 2. Get current ref (branch tip commit SHA).
    const refResp = await fetch(`${this.gitApiBase}/refs/heads/${this.branch}`, { headers: this.headers });
    if (!refResp.ok) {
      const text = await refResp.text();
      throw new Error(`GitHub putBlob get-ref ${this.branch}: ${refResp.status} ${text}`);
    }
    const { object: { sha: commitSha } } = await refResp.json() as { object: { sha: string } };

    // 3. Get current commit's tree SHA.
    const commitResp = await fetch(`${this.gitApiBase}/commits/${commitSha}`, { headers: this.headers });
    if (!commitResp.ok) {
      const text = await commitResp.text();
      throw new Error(`GitHub putBlob get-commit ${commitSha}: ${commitResp.status} ${text}`);
    }
    const { tree: { sha: baseTreeSha } } = await commitResp.json() as { tree: { sha: string } };

    // 4. Create new tree adding the blob at `path` on top of base tree.
    const treeResp = await fetch(`${this.gitApiBase}/trees`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({
        base_tree: baseTreeSha,
        tree: [{ path, mode: '100644', type: 'blob', sha: blobSha }],
      }),
    });
    if (!treeResp.ok) {
      const text = await treeResp.text();
      throw new Error(`GitHub putBlob create-tree: ${treeResp.status} ${text}`);
    }
    const { sha: newTreeSha } = await treeResp.json() as { sha: string };

    // 5. Create new commit pointing at the new tree.
    const newCommitResp = await fetch(`${this.gitApiBase}/commits`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify({
        message,
        tree: newTreeSha,
        parents: [commitSha],
      }),
    });
    if (!newCommitResp.ok) {
      const text = await newCommitResp.text();
      throw new Error(`GitHub putBlob create-commit: ${newCommitResp.status} ${text}`);
    }
    const { sha: newCommitSha } = await newCommitResp.json() as { sha: string };

    // 6. Fast-forward branch to the new commit.
    const updateRefResp = await fetch(`${this.gitApiBase}/refs/heads/${this.branch}`, {
      method: 'PATCH',
      headers: this.headers,
      body: JSON.stringify({ sha: newCommitSha, force: false }),
    });
    if (!updateRefResp.ok) {
      const text = await updateRefResp.text();
      throw new Error(`GitHub putBlob update-ref: ${updateRefResp.status} ${text}`);
    }

    return path;
  }

  async getBlob(path: string): Promise<Uint8Array> {
    return this.readFile(path);
  }

  async deleteBlob(path: string, message: string): Promise<void> {
    return this.deleteFile(path, message);
  }
}
