/**
 * Minimal GitHub API client for the D085 hybrid local GitHub-write path.
 *
 * Only the three methods D085 needs: submit evidence, draft proof, open draft PR.
 * Token is acquired from the broker per-operation, used transiently (only in the
 * Authorization header), and released (byte-wiped) immediately after the API call
 * completes — even on error paths. The token is never logged, never stored in a
 * projection, and never returned to the browser.
 *
 * The client is intentionally narrow: it does NOT support clone, force-push,
 * repository deletion, or any other GitHub operation.
 */
import { createHash } from "node:crypto";
const GITHUB_API_BASE = "https://api.github.com";
export class LocalGitHubForgeClient {
    #broker;
    #forgeRef;
    #fetch;
    #baseUrl;
    constructor(options) {
        this.#broker = options.broker;
        this.#forgeRef = options.forgeRef;
        this.#fetch = options.fetchImpl ?? fetch;
        this.#baseUrl = options.baseUrl ?? GITHUB_API_BASE;
    }
    // ---------------------------------------------------------------------------
    // Public API — the three D085 methods
    // ---------------------------------------------------------------------------
    /**
     * Submit run evidence as a commit on the preparation branch.
     *
     * GitHub API sequence:
     * 1. GET /repos/{owner}/{repo}/git/ref/heads/{branch} → parent SHA
     * 2. POST /repos/{owner}/{repo}/git/blobs (per file) → blob SHAs
     * 3. POST /repos/{owner}/{repo}/git/trees → tree SHA
     * 4. POST /repos/{owner}/{repo}/git/commits → commit SHA
     * 5. PATCH /repos/{owner}/{repo}/git/refs/heads/{branch} → updated ref
     */
    async submitEvidence(input) {
        return this.#withToken(async (token) => {
            const [owner, repo] = splitRepo(input.repository);
            // 1. Get the current branch ref (parent commit).
            const ref = await this.#ghGet(token, `/repos/${owner}/${repo}/git/ref/heads/${encodeURIComponent(input.branch)}`);
            if (!ref) {
                return failure("forge-branch-not-found", `Branch ${input.branch} not found.`);
            }
            const parentSha = ref.object.sha;
            // 2. Create blobs for each file.
            const blobSHAs = [];
            for (const file of input.files) {
                const blob = await this.#ghPost(token, `/repos/${owner}/${repo}/git/blobs`, {
                    content: file.contentBase64,
                    encoding: "base64",
                });
                if (!blob) {
                    return failure("forge-validation-failed", `Failed to create blob for ${file.path}.`);
                }
                blobSHAs.push({ path: file.path, sha: blob.sha, mode: "100644" });
            }
            // 3. Create a tree.
            const tree = await this.#ghPost(token, `/repos/${owner}/${repo}/git/trees`, {
                base_tree: parentSha,
                tree: blobSHAs.map(({ path: p, sha, mode }) => ({
                    path: p,
                    mode,
                    type: "blob",
                    sha,
                })),
            });
            if (!tree) {
                return failure("forge-validation-failed", "Failed to create tree.");
            }
            // 4. Create a commit.
            const commit = await this.#ghPost(token, `/repos/${owner}/${repo}/git/commits`, {
                message: input.message,
                tree: tree.sha,
                parents: [parentSha],
            });
            if (!commit) {
                return failure("forge-validation-failed", "Failed to create commit.");
            }
            // 5. Update the branch ref.
            const updated = await this.#ghPatch(token, `/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(input.branch)}`, { sha: commit.sha, force: false });
            if (!updated) {
                return failure("forge-conflict", "Failed to update branch ref — branch may have changed.");
            }
            return {
                status: "committed",
                commitSha: commit.sha,
                branch: input.branch,
            };
        });
    }
    /**
     * Draft a proof from submitted evidence.
     *
     * Creates a proof-draft record as a commit on the branch. The commit
     * message encodes the proof metadata; the tree is unchanged (empty
     * commit on top of the evidence commit). This makes the proof draft
     * visible in the Git history without modifying file contents.
     */
    async draftProof(input) {
        return this.#withToken(async (token) => {
            const [owner, repo] = splitRepo(input.repository);
            const proofDraftId = proofDraftIdFor(input);
            // Get the current branch ref so we know the parent commit.
            const ref = await this.#ghGet(token, `/repos/${owner}/${repo}/git/ref/heads/${encodeURIComponent(input.branch)}`);
            if (!ref) {
                return failure("forge-branch-not-found", `Branch ${input.branch} not found.`);
            }
            // Create an empty-tree commit (same tree as parent) with proof metadata.
            const parentCommit = await this.#ghGet(token, `/repos/${owner}/${repo}/git/commits/${ref.object.sha}`);
            if (!parentCommit) {
                return failure("forge-validation-failed", "Failed to read parent commit.");
            }
            const message = [
                `PatchTrail proof draft`,
                ``,
                `Proof ID: ${proofDraftId}`,
                `Summary: ${input.proofSummary}`,
                `Changed files: ${input.changedFileCount}`,
                `Validations: ${input.validationCount}`,
            ].join("\n");
            const proofCommit = await this.#ghPost(token, `/repos/${owner}/${repo}/git/commits`, {
                message,
                tree: parentCommit.tree.sha,
                parents: [ref.object.sha],
            });
            if (!proofCommit) {
                return failure("forge-validation-failed", "Failed to create proof commit.");
            }
            // Update the branch ref to point to the proof commit.
            const updated = await this.#ghPatch(token, `/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(input.branch)}`, { sha: proofCommit.sha, force: false });
            if (!updated) {
                return failure("forge-conflict", "Failed to update branch ref for proof commit.");
            }
            return {
                status: "proof-drafted",
                proofCommitSha: proofCommit.sha,
                proofDraftId,
            };
        });
    }
    /**
     * Open a draft pull request.
     */
    async openDraftPullRequest(input) {
        return this.#withToken(async (token) => {
            const [owner, repo] = splitRepo(input.repository);
            const pr = await this.#ghPost(token, `/repos/${owner}/${repo}/pulls`, {
                title: input.title,
                body: input.body,
                head: input.headBranch,
                base: input.baseBranch,
                draft: true,
            });
            if (!pr) {
                return failure("forge-validation-failed", "Failed to create pull request.");
            }
            return {
                status: "draft-pr-opened",
                pullRequestNumber: pr.number,
                pullRequestUrl: pr.html_url,
            };
        });
    }
    // ---------------------------------------------------------------------------
    // Token lifecycle
    // ---------------------------------------------------------------------------
    /**
     * Acquires the forge token from the broker, invokes `fn` with the decoded
     * token string, and releases (byte-wipes) the token when done — even if
     * `fn` throws. The token string is dropped at the end of this call and is
     * never retained by the client.
     */
    async #withToken(fn) {
        let credential;
        try {
            credential = await this.#broker.acquire(this.#forgeRef);
        }
        catch {
            return failure("forge-token-missing", "No local forge token is connected.");
        }
        try {
            const token = new TextDecoder().decode(credential.secret);
            return await fn(token);
        }
        catch (error) {
            return failure("forge-unknown-error", error instanceof Error
                ? error.message
                : "An unknown forge error occurred.");
        }
        finally {
            this.#broker.release(this.#forgeRef);
        }
    }
    // ---------------------------------------------------------------------------
    // GitHub API helpers
    // ---------------------------------------------------------------------------
    async #ghGet(token, path) {
        const response = await this.#ghRequest(token, "GET", path);
        if (response.status === 404)
            return null;
        if (!response.ok)
            return null;
        return (await response.json());
    }
    async #ghPost(token, path, body) {
        const response = await this.#ghRequest(token, "POST", path, body);
        if (response.status === 404)
            return null;
        if (!response.ok)
            return null;
        return (await response.json());
    }
    async #ghPatch(token, path, body) {
        const response = await this.#ghRequest(token, "PATCH", path, body);
        if (response.status === 404)
            return null;
        if (!response.ok)
            return null;
        return (await response.json());
    }
    async #ghRequest(token, method, path, body) {
        const headers = {
            authorization: `Bearer ${token}`,
            accept: "application/vnd.github+json",
            "user-agent": "PatchTrail (local-first forge; D085)",
        };
        if (body !== undefined) {
            headers["content-type"] = "application/json; charset=utf-8";
        }
        return this.#fetch(`${this.#baseUrl}${path}`, {
            method,
            headers,
            ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
        });
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function splitRepo(repository) {
    const parts = repository.split("/");
    if (parts.length !== 2 || !parts[0] || !parts[1]) {
        throw new Error("Repository must be in owner/repo format.");
    }
    return [parts[0], parts[1]];
}
function proofDraftIdFor(input) {
    return createHash("sha256")
        .update(`patchtrail-proof-draft:v1:${input.repository}:${input.branch}:${input.evidenceCommitSha}`)
        .digest("base64url")
        .slice(0, 32);
}
function failure(reasonCode, message, statusCode) {
    return {
        status: "failed",
        reasonCode,
        message,
        ...(statusCode !== undefined ? { statusCode } : {}),
    };
}
