import { spawn } from "node:child_process";
import { readFileSync, existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import chalk from "chalk";
import { DEFAULT_INGEST_URL } from "@kenkaiiii/gg-pixel";
import { PIXEL_FIX_SYSTEM_PROMPT } from "./pixel-fix-agent.js";
import { tryResolveStack } from "./source-maps.js";
export async function fixError(errorId, opts = {}) {
    const ingestUrl = (opts.ingestUrl ?? DEFAULT_INGEST_URL).replace(/\/+$/, "");
    const fetchFn = opts.fetchFn ?? fetch;
    const home = opts.homeDir ?? homedir();
    const owner = await resolveErrorOwner(fetchFn, ingestUrl, errorId, home);
    const { error, project, secret } = owner;
    const branch = `fix/pixel-${error.id}`;
    await patchError(fetchFn, ingestUrl, error.id, { status: "in_progress", branch }, secret);
    const exitCode = await runAgent({
        cwd: project.path,
        prompt: buildAgentPrompt(error, branch, project.path),
        systemPrompt: PIXEL_FIX_SYSTEM_PROMPT,
        spawnFn: opts.spawnFn ?? spawn,
        ggcoderBin: opts.ggcoderBin ?? "ggcoder",
        inheritStdio: opts.inheritStdio ?? true,
        maxTurns: opts.maxTurns ?? 60,
    });
    const observed = await observeOutcome(project.path, branch, opts.spawnFn ?? spawn);
    let outcome;
    let reason;
    if (exitCode !== 0) {
        outcome = "failed";
        reason = `Agent exited with code ${exitCode}`;
    }
    else if (!observed.branchExists) {
        outcome = "failed";
        reason = `Branch ${branch} was not created`;
    }
    else if (!observed.hasCommits) {
        outcome = "failed";
        reason = `Branch ${branch} has no new commits`;
    }
    else {
        outcome = "awaiting_review";
        reason = `Branch ${branch} created with ${observed.commitCount} commit(s) — ready for review`;
    }
    await patchError(fetchFn, ingestUrl, error.id, { status: outcome, branch }, secret);
    return { errorId: error.id, projectName: project.name, branch, outcome, reason };
}
export async function runQueue(opts = {}) {
    const ingestUrl = (opts.ingestUrl ?? DEFAULT_INGEST_URL).replace(/\/+$/, "");
    const fetchFn = opts.fetchFn ?? fetch;
    const home = opts.homeDir ?? homedir();
    const log = opts.onProgress ?? ((msg) => console.log(msg));
    const projectsPath = join(home, ".gg", "projects.json");
    if (!existsSync(projectsPath)) {
        log(chalk.dim("No projects registered. Run `ggcoder pixel install` first."));
        return { fixed: 0, failed: 0, total: 0 };
    }
    const projects = JSON.parse(readFileSync(projectsPath, "utf8"));
    const queue = [];
    for (const [projectId, project] of Object.entries(projects)) {
        if (!project.secret)
            continue; // legacy entry: cannot list, must re-install
        try {
            const res = await fetchFn(`${ingestUrl}/api/projects/${projectId}/errors?status=open`, {
                headers: { authorization: `Bearer ${project.secret}` },
            });
            if (!res.ok)
                continue;
            const body = (await res.json());
            for (const e of body.errors) {
                queue.push({ projectName: project.name, errorId: e.id });
            }
        }
        catch {
            // skip unreachable project
        }
    }
    if (queue.length === 0) {
        log(chalk.hex("#4ade80")("No open errors. Queue is clean."));
        return { fixed: 0, failed: 0, total: 0 };
    }
    log(chalk.bold(`Fixing ${queue.length} ${queue.length === 1 ? "error" : "errors"}...`));
    log("");
    let fixed = 0;
    let failed = 0;
    for (const item of queue) {
        log(chalk.hex("#a78bfa").bold(`▸ ${item.projectName}`) + chalk.dim(`  ${item.errorId}`));
        try {
            const result = await fixError(item.errorId, opts);
            if (result.outcome === "awaiting_review") {
                log(chalk.hex("#4ade80")(`  ✓ ${result.reason}`));
                fixed++;
            }
            else {
                log(chalk.hex("#ef4444")(`  ✗ ${result.reason}`));
                failed++;
            }
        }
        catch (err) {
            log(chalk.hex("#ef4444")(`  ✗ ${err instanceof Error ? err.message : String(err)}`));
            failed++;
        }
        log("");
    }
    return { fixed, failed, total: queue.length };
}
export async function preparePixelFix(errorId, opts = {}) {
    const ingestUrl = (opts.ingestUrl ?? DEFAULT_INGEST_URL).replace(/\/+$/, "");
    const fetchFn = opts.fetchFn ?? fetch;
    const home = opts.homeDir ?? homedir();
    const owner = await resolveErrorOwner(fetchFn, ingestUrl, errorId, home);
    const { error, project, secret } = owner;
    const branch = `fix/pixel-${error.id}`;
    await patchError(fetchFn, ingestUrl, error.id, { status: "in_progress", branch }, secret);
    return {
        errorId: error.id,
        projectId: error.project_id,
        projectName: project.name,
        projectPath: project.path,
        branch,
        prompt: buildAgentPrompt(error, branch, project.path),
    };
}
export async function finalizePixelFix(prep, opts = {}) {
    const ingestUrl = (opts.ingestUrl ?? DEFAULT_INGEST_URL).replace(/\/+$/, "");
    const fetchFn = opts.fetchFn ?? fetch;
    const home = opts.homeDir ?? homedir();
    const observed = await observeOutcome(prep.projectPath, prep.branch, opts.spawnFn ?? spawn);
    let outcome;
    let reason;
    if (opts.agentExitedCleanly === false) {
        outcome = "failed";
        reason = "Agent did not finish cleanly";
    }
    else if (!observed.branchExists) {
        outcome = "failed";
        reason = `Branch ${prep.branch} was not created`;
    }
    else if (!observed.hasCommits) {
        outcome = "failed";
        reason = `Branch ${prep.branch} has no new commits`;
    }
    else {
        outcome = "awaiting_review";
        reason = `Branch ${prep.branch} created with ${observed.commitCount} commit(s) — ready for review`;
    }
    const secret = lookupProjectSecret(home, prep.projectId);
    await patchError(fetchFn, ingestUrl, prep.errorId, { status: outcome, branch: prep.branch }, secret);
    return { outcome, reason };
}
export function buildAgentPrompt(error, branch, projectDir) {
    let stack = parseStackJson(error.stack);
    // For browser stacks (URLs in the file field), try to resolve via local
    // source maps before showing the agent. Resolved frames point at the
    // user's TS/JS source — much more actionable than `app.min.js:1:48201`.
    if (projectDir && stack.length > 0 && looksLikeMinifiedBrowserStack(stack)) {
        stack = tryResolveStack(stack, projectDir);
    }
    const ctx = parseCodeContext(error.code_context);
    const lines = [];
    lines.push("An error has been identified in this project. Investigate and fix it.");
    lines.push("");
    lines.push(`Error: ${error.type ?? "Error"} — ${error.message ?? "(no message)"}`);
    const topInApp = stack.find((f) => f.in_app) ?? stack[0];
    if (topInApp)
        lines.push(`Location: ${topInApp.file}:${topInApp.line}`);
    if (ctx && ctx.lines.length > 0) {
        lines.push("");
        lines.push("Code at site:");
        const startLine = ctx.error_line - Math.floor((ctx.lines.length - 1) / 2);
        ctx.lines.forEach((line, i) => {
            const lineNum = startLine + i;
            const marker = lineNum === ctx.error_line ? ">" : " ";
            lines.push(`  ${marker}${String(lineNum).padStart(4)} | ${line}`);
        });
    }
    if (stack.length > 0) {
        lines.push("");
        lines.push("Stack:");
        for (const f of stack.slice(0, 10)) {
            const lib = f.in_app ? "" : "  (lib)";
            lines.push(`  ${f.fn || "<anon>"}  ${f.file}:${f.line}${lib}`);
        }
    }
    if (error.runtime)
        lines.push(`\nRuntime: ${error.runtime}`);
    lines.push(`Occurrences: ${error.occurrences}`);
    if (error.recurrence_count > 0) {
        lines.push("");
        lines.push(`Recurrence: this is the ${ordinal(error.recurrence_count)} time this fingerprint has appeared after a previous fix was merged. The earlier fix did not hold — investigate why.`);
    }
    lines.push("");
    lines.push("When done:");
    lines.push(`  1. Create branch: ${branch}`);
    lines.push("  2. Commit the fix on that branch");
    lines.push("  3. Run the project's quality checks (only commit if they pass)");
    lines.push("");
    lines.push("Do not merge. Do not push. Do not switch back to main.");
    return lines.join("\n");
}
/**
 * Walks ~/.gg/projects.json trying each project's bearer secret against
 * GET /api/errors/:id until one returns 200. Wrong secrets get 403/404 from
 * the server, so this scan exits as soon as the rightful owner is found.
 *
 * Bounded by the number of registered projects on this machine (typically
 * single digits) so cost is negligible per fix.
 */
async function resolveErrorOwner(fetchFn, ingestUrl, errorId, home) {
    const projectsPath = join(home, ".gg", "projects.json");
    if (!existsSync(projectsPath)) {
        throw new Error(`No projects mapping at ${projectsPath} — run \`ggcoder pixel install\` in the project first.`);
    }
    const projects = JSON.parse(readFileSync(projectsPath, "utf8"));
    const ownersWithSecret = Object.entries(projects).filter(([, p]) => Boolean(p.secret));
    if (ownersWithSecret.length === 0) {
        throw new Error("No managed projects on this machine — run `ggcoder pixel install` in the project to refresh management access.");
    }
    for (const [, project] of ownersWithSecret) {
        const res = await fetchFn(`${ingestUrl}/api/errors/${errorId}`, {
            headers: { authorization: `Bearer ${project.secret}` },
        });
        if (res.ok) {
            const error = (await res.json());
            return { error, project, secret: project.secret };
        }
        // 401/403/404 → not this project; keep scanning.
    }
    throw new Error(`Error ${errorId} was not found in any registered project. Ensure the project is installed (\`ggcoder pixel install\`).`);
}
function lookupProjectSecret(home, projectId) {
    const projectsPath = join(home, ".gg", "projects.json");
    if (!existsSync(projectsPath)) {
        throw new Error(`No projects mapping at ${projectsPath} — run \`ggcoder pixel install\` in the project first.`);
    }
    const projects = JSON.parse(readFileSync(projectsPath, "utf8"));
    const project = projects[projectId];
    if (!project) {
        throw new Error(`No local mapping for project ${projectId} in ${projectsPath}. Run \`ggcoder pixel install\` in the project's directory.`);
    }
    if (!project.secret) {
        throw new Error(`Project ${projectId} is missing its bearer secret in ${projectsPath} — re-run \`ggcoder pixel install\` to refresh.`);
    }
    return project.secret;
}
async function patchError(fetchFn, ingestUrl, errorId, body, secret) {
    const res = await fetchFn(`${ingestUrl}/api/errors/${errorId}`, {
        method: "PATCH",
        headers: {
            "content-type": "application/json",
            authorization: `Bearer ${secret}`,
        },
        body: JSON.stringify(body),
    });
    if (!res.ok)
        throw new Error(`PATCH /api/errors/${errorId} failed: ${res.status}`);
}
async function runAgent(opts) {
    return new Promise((resolve, reject) => {
        const args = [
            "--json",
            "--max-turns",
            String(opts.maxTurns),
            "--system-prompt",
            opts.systemPrompt,
            opts.prompt,
        ];
        const child = opts.spawnFn(opts.ggcoderBin, args, {
            cwd: opts.cwd,
            stdio: opts.inheritStdio ? "inherit" : ["ignore", "pipe", "pipe"],
        });
        child.on("error", reject);
        child.on("exit", (code) => resolve(code ?? 1));
    });
}
async function observeOutcome(cwd, branch, spawnFn) {
    const exists = await runGit(cwd, ["show-ref", "--verify", `refs/heads/${branch}`], spawnFn);
    if (exists.code !== 0)
        return { branchExists: false, hasCommits: false, commitCount: 0 };
    let baseBranch = null;
    for (const candidate of ["main", "master"]) {
        const r = await runGit(cwd, ["show-ref", "--verify", `refs/heads/${candidate}`], spawnFn);
        if (r.code === 0) {
            baseBranch = candidate;
            break;
        }
    }
    if (!baseBranch)
        return { branchExists: true, hasCommits: false, commitCount: 0 };
    const ahead = await runGit(cwd, ["rev-list", "--count", `${baseBranch}..${branch}`], spawnFn);
    const count = ahead.code === 0 ? parseInt(ahead.stdout.trim(), 10) || 0 : 0;
    return { branchExists: true, hasCommits: count > 0, commitCount: count };
}
function runGit(cwd, args, spawnFn) {
    return new Promise((resolve) => {
        const child = spawnFn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (b) => (stdout += b.toString()));
        child.stderr?.on("data", (b) => (stderr += b.toString()));
        child.on("error", () => resolve({ code: 1, stdout, stderr }));
        child.on("exit", (code) => resolve({ code: code ?? 1, stdout, stderr }));
    });
}
function looksLikeMinifiedBrowserStack(stack) {
    // Heuristic: any frame whose file is an http(s) URL = browser stack.
    // We try map resolution for these. Native Node paths don't need it.
    return stack.some((f) => /^https?:\/\//.test(f.file));
}
function parseStackJson(raw) {
    if (!raw)
        return [];
    try {
        const parsed = JSON.parse(raw);
        if (Array.isArray(parsed))
            return parsed;
    }
    catch {
        // ignore
    }
    return [];
}
function parseCodeContext(raw) {
    if (!raw)
        return null;
    try {
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
function ordinal(n) {
    const s = ["th", "st", "nd", "rd"];
    const v = n % 100;
    return `${n}${s[(v - 20) % 10] ?? s[v] ?? s[0]}`;
}
//# sourceMappingURL=pixel-fix.js.map