/**
 * Generic Git forge abstraction — GitHub, GitLab, Gitea, Azure DevOps, Bitbucket.
 *
 * Each forge implements the same interface so PR creation, auth checks,
 * and status queries work uniformly regardless of hosting provider.
 */
import { exec as execCb } from "node:child_process";
import { writeFile, unlink } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { parseGitRemote } from "./git.js";
function exec(cmd, opts) {
    return new Promise((resolve, reject) => {
        execCb(cmd, { encoding: "utf-8", windowsHide: true, ...opts }, (error, stdout, stderr) => {
            if (error) {
                const enriched = error;
                enriched.stdout = stdout;
                enriched.stderr = stderr;
                reject(enriched);
                return;
            }
            resolve({ stdout, stderr });
        });
    });
}
// ── Helpers ──────────────────────────────────────────────────────────
function cleanGhEnv() {
    const { GH_REPO, GH_HOST, GH_TOKEN, GITHUB_TOKEN, ...rest } = process.env;
    return rest;
}
async function cliExec(cmd, cwd, timeout = 30_000, env) {
    const { stdout } = await exec(cmd, { cwd, timeout, env });
    return stdout.trim();
}
async function cliAvailable(cmd, cwd) {
    try {
        await exec(`${cmd} --version`, { cwd, timeout: 5_000 });
        return true;
    }
    catch {
        return false;
    }
}
function apiHeaders(token, extra) {
    return {
        Authorization: `Bearer ${token}`,
        "User-Agent": "jait-gateway",
        ...extra,
    };
}
function buildGhRepoFlag(remote) {
    return remote.owner ? ` --repo "${remote.owner}/${remote.repo}"` : "";
}
// ══════════════════════════════════════════════════════════════════════
//  GitHub Forge
// ══════════════════════════════════════════════════════════════════════
export class GitHubForge {
    provider = "github";
    displayName = "GitHub";
    async checkCliAvailable(cwd) {
        return cliAvailable("gh", cwd);
    }
    async checkAuth(cwd) {
        try {
            const out = await cliExec("gh auth status", cwd, 10_000, cleanGhEnv());
            const userMatch = out.match(/Logged in to .+ as (\S+)/);
            return { authenticated: true, username: userMatch?.[1] };
        }
        catch (err) {
            return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
        }
    }
    async loginWithToken(token, cwd) {
        try {
            const { spawn } = await import("node:child_process");
            await new Promise((resolve, reject) => {
                const child = spawn("gh", ["auth", "login", "--with-token"], {
                    cwd, stdio: "pipe", shell: true, env: cleanGhEnv(), windowsHide: true,
                });
                child.stdin.write(token);
                child.stdin.end();
                child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`gh auth failed (code ${code})`)));
                child.on("error", reject);
            });
            const username = await cliExec('gh api user --jq ".login"', cwd, 10_000, cleanGhEnv()).catch(() => "");
            return { authenticated: true, username: username || undefined };
        }
        catch (err) {
            return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
        }
    }
    async resolveToken(_cwd) {
        const quick = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_PAT ?? null;
        if (quick)
            return quick;
        // Try git credential manager
        try {
            const { spawn } = await import("node:child_process");
            return await new Promise((resolve) => {
                const proc = spawn("git", ["credential", "fill"], { timeout: 5_000, windowsHide: true });
                let out = "";
                proc.stdout.on("data", (d) => { out += d.toString(); });
                proc.on("close", () => {
                    const match = out.match(/^password=(.+)$/m);
                    resolve(match?.[1]?.trim() ?? null);
                });
                proc.on("error", () => resolve(null));
                proc.stdin.write("protocol=https\nhost=github.com\n\n");
                proc.stdin.end();
            });
        }
        catch {
            return null;
        }
    }
    async getDefaultBranch(cwd, _remote) {
        try {
            const json = await cliExec(`gh repo view${buildGhRepoFlag(_remote)} --json defaultBranchRef`, cwd, 15_000, cleanGhEnv());
            const parsed = JSON.parse(json);
            const ref = parsed.defaultBranchRef;
            if (ref?.name)
                return String(ref.name);
        }
        catch { /* not available */ }
        return null;
    }
    async findExistingPr(cwd, remote, headBranch, token) {
        // Try gh CLI first
        if (await this.checkCliAvailable(cwd)) {
            try {
                const raw = await cliExec(`gh pr view --head "${headBranch}"${buildGhRepoFlag(remote)} --json number,url,title,state,baseRefName,headRefName`, cwd, 15_000, cleanGhEnv());
                const parsed = JSON.parse(raw);
                if (parsed.number) {
                    return {
                        status: "opened_existing",
                        url: String(parsed.url ?? ""),
                        number: Number(parsed.number),
                        baseBranch: String(parsed.baseRefName ?? ""),
                        headBranch: String(parsed.headRefName ?? headBranch),
                        title: String(parsed.title ?? ""),
                        state: String(parsed.state ?? "OPEN").toLowerCase() === "open" ? "open" : "closed",
                    };
                }
            }
            catch { /* no existing PR */ }
        }
        // Try API
        const effectiveToken = token ?? await this.resolveToken(cwd);
        if (effectiveToken && remote.owner) {
            return this.findPrViaApi(remote, effectiveToken, headBranch);
        }
        return null;
    }
    async createPr(cwd, remote, input, token) {
        // Try gh CLI first
        if (await this.checkCliAvailable(cwd)) {
            return this.createPrViaCli(cwd, remote, input);
        }
        // Fall back to API
        const effectiveToken = token ?? await this.resolveToken(cwd);
        if (effectiveToken && remote.owner) {
            return this.createPrViaApi(remote, effectiveToken, input);
        }
        return { status: "not_found" };
    }
    async getPrChecks(cwd, _headBranch) {
        if (!await this.checkCliAvailable(cwd))
            return [];
        try {
            const raw = await cliExec("gh pr checks --json name,state,conclusion,startedAt,completedAt,detailsUrl", cwd, 15_000, cleanGhEnv());
            return JSON.parse(raw);
        }
        catch {
            return [];
        }
    }
    buildCreatePrUrl(remote, headBranch, baseBranch) {
        return `${remote.normalizedUrl}/compare/${encodeURIComponent(baseBranch ?? "main")}...${encodeURIComponent(headBranch)}?expand=1`;
    }
    async prDiffStats(cwd, remote, headBranch, token) {
        // Try gh CLI first — `gh pr view` returns additions/deletions/changedFiles
        if (await this.checkCliAvailable(cwd)) {
            try {
                const raw = await cliExec(`gh pr view --head "${headBranch}"${buildGhRepoFlag(remote)} --json additions,deletions,changedFiles`, cwd, 15_000, cleanGhEnv());
                const parsed = JSON.parse(raw);
                if (parsed.changedFiles != null) {
                    return {
                        files: Number(parsed.changedFiles ?? 0),
                        insertions: Number(parsed.additions ?? 0),
                        deletions: Number(parsed.deletions ?? 0),
                        hasChanges: Number(parsed.changedFiles ?? 0) > 0,
                    };
                }
            }
            catch { /* no PR or gh error — fall through to API */ }
        }
        // Fall back to API
        const effectiveToken = token ?? await this.resolveToken(cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        return this.prDiffStatsViaApi(remote, effectiveToken, headBranch);
    }
    async prFileDiffs(cwd, remote, headBranch, token) {
        // Try gh CLI first — `gh pr diff` returns the unified diff
        if (await this.checkCliAvailable(cwd)) {
            try {
                // Get PR number first so we can use the files API
                const raw = await cliExec(`gh pr view --head "${headBranch}"${buildGhRepoFlag(remote)} --json number`, cwd, 15_000, cleanGhEnv());
                const parsed = JSON.parse(raw);
                const prNumber = Number(parsed.number ?? 0);
                if (prNumber > 0) {
                    // Use API for structured per-file diffs (we need original + modified content)
                    const effectiveToken = token ?? await this.resolveToken(cwd);
                    if (effectiveToken) {
                        return this.prFileDiffsViaApi(remote, effectiveToken, prNumber, cwd);
                    }
                }
            }
            catch { /* no PR — fall through */ }
        }
        // Fall back to API if we have a token
        const effectiveToken = token ?? await this.resolveToken(cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        // Find PR number via API
        const pr = await this.findPrViaApi(remote, effectiveToken, headBranch);
        if (!pr?.number)
            return null;
        return this.prFileDiffsViaApi(remote, effectiveToken, pr.number, cwd);
    }
    async prDiffStatsViaApi(remote, token, headBranch) {
        const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
        const headers = apiHeaders(token, { Accept: "application/vnd.github+json" });
        const headParam = `${remote.owner}:${headBranch}`;
        try {
            const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls?head=${encodeURIComponent(headParam)}&state=all`, { headers });
            if (!res.ok)
                return null;
            const list = await res.json();
            const pr = (list.find((p) => p?.state === "open") ?? list[0]);
            if (!pr)
                return null;
            return {
                files: Number(pr.changed_files ?? 0),
                insertions: Number(pr.additions ?? 0),
                deletions: Number(pr.deletions ?? 0),
                hasChanges: Number(pr.changed_files ?? 0) > 0,
            };
        }
        catch {
            return null;
        }
    }
    async prFileDiffsViaApi(remote, token, prNumber, cwd) {
        const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
        const headers = apiHeaders(token, { Accept: "application/vnd.github+json" });
        const entries = [];
        // Paginate through all PR files
        let page = 1;
        while (true) {
            const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls/${prNumber}/files?per_page=100&page=${page}`, { headers });
            if (!res.ok)
                break;
            const files = await res.json();
            if (files.length === 0)
                break;
            for (const file of files) {
                const filePath = String(file.filename ?? "");
                const ghStatus = String(file.status ?? "modified");
                let status = "M";
                if (ghStatus === "added")
                    status = "A";
                else if (ghStatus === "removed")
                    status = "D";
                else if (ghStatus === "renamed")
                    status = "R";
                // Get file content at both refs using git locally
                let original = "";
                let modified = "";
                if (status !== "A") {
                    try {
                        original = await cliExec(`gh api repos/${remote.owner}/${remote.repo}/contents/${encodeURIComponent(filePath)}?ref=${encodeURIComponent(String(file.contents_url ?? "").match(/ref=([^&]+)/)?.[1] ?? "HEAD")} --jq .content`, cwd, 15_000, cleanGhEnv()).catch(() => "");
                        // content is base64 encoded
                        if (original)
                            original = Buffer.from(original, "base64").toString("utf-8");
                    }
                    catch { /* new file */ }
                }
                // For modified content, use the patch to reconstruct or get from the branch
                if (status !== "D") {
                    try {
                        modified = String(file.patch ?? "");
                    }
                    catch { /* deleted */ }
                }
                entries.push({ path: filePath, original, modified, status });
            }
            if (files.length < 100)
                break;
            page++;
        }
        return entries;
    }
    async createPrViaCli(cwd, remote, input) {
        const bodyFile = join(tmpdir(), `jait-pr-body-${Date.now()}.md`);
        await writeFile(bodyFile, input.body, "utf-8");
        try {
            const baseFlag = input.baseBranch ? ` --base "${input.baseBranch}"` : "";
            const prUrl = await cliExec(`gh pr create --head "${input.headBranch}"${buildGhRepoFlag(remote)} --title "${input.title.replace(/"/g, '\\"')}" --body-file "${bodyFile}"${baseFlag}`, cwd, 60_000, cleanGhEnv());
            let prNumber = 0;
            let baseBranch = input.baseBranch;
            let headBranch = input.headBranch;
            let title = input.title;
            try {
                const details = await cliExec(`gh pr view "${prUrl.trim()}" --json number,title,baseRefName,headRefName`, cwd, 15_000, cleanGhEnv());
                const p = JSON.parse(details);
                prNumber = Number(p.number ?? 0);
                baseBranch = String(p.baseRefName ?? baseBranch);
                headBranch = String(p.headRefName ?? headBranch);
                title = String(p.title ?? title);
            }
            catch { /* best effort */ }
            return { status: "created", url: prUrl.trim(), number: prNumber, baseBranch, headBranch, title };
        }
        finally {
            await unlink(bodyFile).catch(() => { });
        }
    }
    async createPrViaApi(remote, token, input) {
        const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
        const headers = { ...apiHeaders(token), Accept: "application/vnd.github+json", "Content-Type": "application/json" };
        const headParam = `${remote.owner}:${input.headBranch}`;
        const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls`, {
            method: "POST",
            headers,
            body: JSON.stringify({ title: input.title, head: headParam, base: input.baseBranch, body: input.body }),
        });
        if (!res.ok) {
            const text = await res.text().catch(() => "");
            throw new Error(`GitHub API PR create failed (${res.status}): ${text.slice(0, 400)}`);
        }
        const json = await res.json();
        return {
            status: "created",
            url: String(json.html_url ?? ""),
            number: Number(json.number ?? 0),
            baseBranch: String(json.base?.ref ?? input.baseBranch),
            headBranch: String(json.head?.ref ?? input.headBranch),
            title: String(json.title ?? input.title),
        };
    }
    async findPrViaApi(remote, token, headBranch) {
        const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
        const headers = apiHeaders(token, { Accept: "application/vnd.github+json" });
        const headParam = `${remote.owner}:${headBranch}`;
        try {
            const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls?head=${encodeURIComponent(headParam)}&state=all`, { headers });
            if (!res.ok)
                return null;
            const list = await res.json();
            const pr = (list.find((p) => p?.state === "open") ?? list[0]);
            if (!pr?.html_url)
                return null;
            const merged = pr.merged_at != null;
            return {
                status: "opened_existing",
                url: String(pr.html_url),
                number: Number(pr.number ?? 0),
                baseBranch: String(pr.base?.ref ?? ""),
                headBranch: String(pr.head?.ref ?? headBranch),
                title: String(pr.title ?? ""),
                state: merged ? "merged" : String(pr.state ?? "open") === "closed" ? "closed" : "open",
            };
        }
        catch {
            return null;
        }
    }
}
// ══════════════════════════════════════════════════════════════════════
//  GitLab Forge
// ══════════════════════════════════════════════════════════════════════
export class GitLabForge {
    provider = "gitlab";
    displayName = "GitLab";
    async checkCliAvailable(cwd) {
        return cliAvailable("glab", cwd);
    }
    async checkAuth(cwd) {
        try {
            const out = await cliExec("glab auth status", cwd, 10_000);
            const userMatch = out.match(/Logged in .+ as (\S+)/i);
            return { authenticated: true, username: userMatch?.[1] };
        }
        catch {
            const token = await this.resolveToken(cwd);
            return token ? { authenticated: true } : { authenticated: false, error: "glab CLI not authenticated and no GITLAB_TOKEN set" };
        }
    }
    async loginWithToken(token, cwd) {
        try {
            await cliExec(`glab auth login --token "${token}"`, cwd, 15_000);
            return { authenticated: true };
        }
        catch (err) {
            return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
        }
    }
    async resolveToken(_cwd) {
        return process.env.GITLAB_TOKEN ?? process.env.GITLAB_PAT ?? null;
    }
    async getDefaultBranch(_cwd, remote) {
        const token = await this.resolveToken(_cwd);
        if (!token)
            return null;
        const apiBase = `https://${remote.host}/api/v4`;
        const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
        try {
            const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}`, {
                headers: apiHeaders(token),
            });
            if (!res.ok)
                return null;
            const json = await res.json();
            return String(json.default_branch ?? "main");
        }
        catch {
            return null;
        }
    }
    async findExistingPr(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken)
            return null;
        const apiBase = `https://${remote.host}/api/v4`;
        const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
        try {
            const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests?source_branch=${encodeURIComponent(headBranch)}&state=opened`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const mr = list[0];
            if (!mr)
                return null;
            return {
                status: "opened_existing",
                url: String(mr.web_url ?? ""),
                number: Number(mr.iid ?? 0),
                baseBranch: String(mr.target_branch ?? ""),
                headBranch: String(mr.source_branch ?? headBranch),
                title: String(mr.title ?? ""),
                state: "open",
            };
        }
        catch {
            return null;
        }
    }
    async createPr(_cwd, remote, input, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken)
            return { status: "not_found" };
        const apiBase = `https://${remote.host}/api/v4`;
        const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
        const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests`, {
            method: "POST",
            headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
            body: JSON.stringify({
                title: input.title,
                source_branch: input.headBranch,
                target_branch: input.baseBranch,
                description: input.body,
            }),
        });
        if (!res.ok) {
            const text = await res.text().catch(() => "");
            throw new Error(`GitLab API MR create failed (${res.status}): ${text.slice(0, 400)}`);
        }
        const json = await res.json();
        return {
            status: "created",
            url: String(json.web_url ?? ""),
            number: Number(json.iid ?? 0),
            baseBranch: String(json.target_branch ?? input.baseBranch),
            headBranch: String(json.source_branch ?? input.headBranch),
            title: String(json.title ?? input.title),
        };
    }
    async getPrChecks(_cwd, _headBranch) {
        return [];
    }
    buildCreatePrUrl(remote, headBranch, _baseBranch) {
        return `${remote.normalizedUrl}/-/merge_requests/new?merge_request[source_branch]=${encodeURIComponent(headBranch)}`;
    }
    async prDiffStats(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken)
            return null;
        const apiBase = `https://${remote.host}/api/v4`;
        const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
        try {
            // Find MR by source branch
            const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests?source_branch=${encodeURIComponent(headBranch)}&state=opened`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const mr = list[0];
            if (!mr)
                return null;
            const mrIid = Number(mr.iid ?? 0);
            // Get changes to count files and compute stats
            const changesRes = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests/${mrIid}/changes`, { headers: apiHeaders(effectiveToken) });
            if (!changesRes.ok)
                return null;
            const changesData = await changesRes.json();
            const changes = changesData.changes ?? [];
            let insertions = 0;
            let deletions = 0;
            for (const change of changes) {
                const diff = change.diff ?? "";
                for (const line of diff.split("\n")) {
                    if (line.startsWith("+") && !line.startsWith("+++"))
                        insertions++;
                    else if (line.startsWith("-") && !line.startsWith("---"))
                        deletions++;
                }
            }
            return { files: changes.length, insertions, deletions, hasChanges: changes.length > 0 };
        }
        catch {
            return null;
        }
    }
    async prFileDiffs(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken)
            return null;
        const apiBase = `https://${remote.host}/api/v4`;
        const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
        try {
            const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests?source_branch=${encodeURIComponent(headBranch)}&state=opened`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const mr = list[0];
            if (!mr)
                return null;
            const mrIid = Number(mr.iid ?? 0);
            const changesRes = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests/${mrIid}/changes`, { headers: apiHeaders(effectiveToken) });
            if (!changesRes.ok)
                return null;
            const changesData = await changesRes.json();
            const entries = [];
            for (const change of changesData.changes ?? []) {
                const filePath = change.new_path ?? change.old_path ?? "";
                let status = "M";
                if (change.new_file)
                    status = "A";
                else if (change.deleted_file)
                    status = "D";
                else if (change.renamed_file)
                    status = "R";
                // For GitLab, we return the patch as modified content —
                // full file content retrieval requires additional API calls.
                entries.push({ path: filePath, original: "", modified: change.diff ?? "", status });
            }
            return entries;
        }
        catch {
            return null;
        }
    }
}
// ══════════════════════════════════════════════════════════════════════
//  Gitea Forge (also covers Forgejo)
// ══════════════════════════════════════════════════════════════════════
export class GiteaForge {
    provider = "gitea";
    displayName = "Gitea";
    async checkCliAvailable(cwd) {
        return cliAvailable("tea", cwd);
    }
    async checkAuth(_cwd) {
        const token = await this.resolveToken(_cwd);
        return token
            ? { authenticated: true }
            : { authenticated: false, error: "No GITEA_TOKEN set. Configure it in Settings → API." };
    }
    async loginWithToken(_token, _cwd) {
        return { authenticated: true };
    }
    async resolveToken(_cwd) {
        return process.env.GITEA_TOKEN ?? process.env.GITEA_PAT ?? null;
    }
    async getDefaultBranch(_cwd, remote) {
        const token = await this.resolveToken(_cwd);
        if (!token || !remote.owner)
            return null;
        try {
            const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}`, {
                headers: apiHeaders(token),
            });
            if (!res.ok)
                return null;
            const json = await res.json();
            return String(json.default_branch ?? "main");
        }
        catch {
            return null;
        }
    }
    async findExistingPr(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls?state=open&head=${encodeURIComponent(`${remote.owner}:${headBranch}`)}`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const pr = list[0];
            if (!pr)
                return null;
            return {
                status: "opened_existing",
                url: String(pr.html_url ?? ""),
                number: Number(pr.number ?? 0),
                baseBranch: String(pr.base?.label ?? ""),
                headBranch: String(pr.head?.label ?? headBranch),
                title: String(pr.title ?? ""),
                state: "open",
            };
        }
        catch {
            return null;
        }
    }
    async createPr(_cwd, remote, input, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return { status: "not_found" };
        const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls`, {
            method: "POST",
            headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
            body: JSON.stringify({
                title: input.title,
                head: input.headBranch,
                base: input.baseBranch,
                body: input.body,
            }),
        });
        if (!res.ok) {
            const text = await res.text().catch(() => "");
            throw new Error(`Gitea API PR create failed (${res.status}): ${text.slice(0, 400)}`);
        }
        const json = await res.json();
        return {
            status: "created",
            url: String(json.html_url ?? ""),
            number: Number(json.number ?? 0),
            baseBranch: String(json.base?.label ?? input.baseBranch),
            headBranch: String(json.head?.label ?? input.headBranch),
            title: String(json.title ?? input.title),
        };
    }
    async getPrChecks(_cwd, _headBranch) {
        return [];
    }
    buildCreatePrUrl(remote, headBranch, baseBranch) {
        return `${remote.normalizedUrl}/compare/${encodeURIComponent(baseBranch ?? "main")}...${encodeURIComponent(headBranch)}`;
    }
    async prDiffStats(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            // Find PR by head branch
            const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls?state=open&head=${encodeURIComponent(`${remote.owner}:${headBranch}`)}`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const pr = list[0];
            if (!pr)
                return null;
            return {
                files: Number(pr.changed_files ?? 0),
                insertions: Number(pr.additions ?? 0),
                deletions: Number(pr.deletions ?? 0),
                hasChanges: Number(pr.changed_files ?? 0) > 0,
            };
        }
        catch {
            return null;
        }
    }
    async prFileDiffs(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls?state=open&head=${encodeURIComponent(`${remote.owner}:${headBranch}`)}`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const list = await res.json();
            const pr = list[0];
            if (!pr)
                return null;
            const prNumber = Number(pr.number ?? 0);
            // Gitea /pulls/:index/files returns per-file diffs
            const filesRes = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls/${prNumber}/files`, { headers: apiHeaders(effectiveToken) });
            if (!filesRes.ok)
                return null;
            const files = await filesRes.json();
            return files.map((f) => {
                let status = "M";
                if (f.status === "added")
                    status = "A";
                else if (f.status === "removed" || f.status === "deleted")
                    status = "D";
                else if (f.status === "renamed")
                    status = "R";
                return { path: f.filename ?? "", original: "", modified: f.patch ?? "", status };
            });
        }
        catch {
            return null;
        }
    }
}
// ══════════════════════════════════════════════════════════════════════
//  Azure DevOps Forge
// ══════════════════════════════════════════════════════════════════════
export class AzureDevOpsForge {
    provider = "azure-devops";
    displayName = "Azure DevOps";
    async checkCliAvailable(cwd) {
        return cliAvailable("az", cwd);
    }
    async checkAuth(cwd) {
        try {
            const out = await cliExec("az account show --query user.name -o tsv", cwd, 10_000);
            return { authenticated: true, username: out || undefined };
        }
        catch {
            const token = await this.resolveToken(cwd);
            return token
                ? { authenticated: true }
                : { authenticated: false, error: "Azure CLI not authenticated and no AZURE_DEVOPS_PAT set" };
        }
    }
    async loginWithToken(_token, _cwd) {
        return { authenticated: true };
    }
    async resolveToken(_cwd) {
        return process.env.AZURE_DEVOPS_PAT ?? process.env.AZURE_DEVOPS_TOKEN ?? null;
    }
    async getDefaultBranch(_cwd, _remote) {
        return null;
    }
    async findExistingPr(cwd, remote, headBranch, _token) {
        if (!await this.checkCliAvailable(cwd))
            return null;
        const orgUrl = this.buildOrgUrl(remote);
        try {
            const raw = await cliExec(`az repos pr list --organization "${orgUrl}" --project "${remote.project}" --repository "${remote.repo}" --source-branch "${headBranch}" --status active --output json`, cwd, 30_000);
            const list = JSON.parse(raw);
            const first = list[0];
            if (!first)
                return null;
            const prId = Number(first.pullRequestId ?? 0);
            return {
                status: "opened_existing",
                url: this.buildPrUrl(remote, prId),
                number: prId,
                baseBranch: String(first.targetRefName ?? "").replace(/^refs\/heads\//, ""),
                headBranch: String(first.sourceRefName ?? headBranch).replace(/^refs\/heads\//, ""),
                title: String(first.title ?? ""),
                state: "open",
            };
        }
        catch {
            return null;
        }
    }
    async createPr(cwd, remote, input, _token) {
        if (!await this.checkCliAvailable(cwd))
            return { status: "not_found" };
        const orgUrl = this.buildOrgUrl(remote);
        const bodyFile = join(tmpdir(), `jait-az-pr-body-${Date.now()}.md`);
        await writeFile(bodyFile, input.body, "utf-8");
        try {
            const raw = await cliExec(`az repos pr create --organization "${orgUrl}" --project "${remote.project}" --repository "${remote.repo}" --source-branch "${input.headBranch}" --target-branch "${input.baseBranch}" --title "${input.title.replace(/"/g, '\\"')}" --description @"${bodyFile}" --output json`, cwd, 60_000);
            const created = JSON.parse(raw);
            const prId = Number(created.pullRequestId ?? 0);
            return {
                status: "created",
                url: this.buildPrUrl(remote, prId),
                number: prId,
                baseBranch: String(created.targetRefName ?? input.baseBranch).replace(/^refs\/heads\//, ""),
                headBranch: String(created.sourceRefName ?? input.headBranch).replace(/^refs\/heads\//, ""),
                title: String(created.title ?? input.title),
            };
        }
        finally {
            await unlink(bodyFile).catch(() => { });
        }
    }
    async getPrChecks(_cwd, _headBranch) {
        return [];
    }
    buildCreatePrUrl(remote, headBranch, baseBranch) {
        const base = this.buildRepoUrl(remote);
        return `${base}/pullrequestcreate?sourceRef=${encodeURIComponent(`refs/heads/${headBranch}`)}${baseBranch ? `&targetRef=${encodeURIComponent(`refs/heads/${baseBranch}`)}` : ""}`;
    }
    async prDiffStats(cwd, remote, headBranch, _token) {
        if (!await this.checkCliAvailable(cwd))
            return null;
        const orgUrl = this.buildOrgUrl(remote);
        try {
            // Find active PR by source branch
            const raw = await cliExec(`az repos pr list --organization "${orgUrl}" --project "${remote.project}" --repository "${remote.repo}" --source-branch "${headBranch}" --status active --output json`, cwd, 30_000);
            const list = JSON.parse(raw);
            const first = list[0];
            if (!first)
                return null;
            // az repos CLI doesn't have a direct diff-stats command;
            // return null and let the caller use git merge-base fallback.
            return null;
        }
        catch {
            return null;
        }
    }
    async prFileDiffs(_cwd, _remote, _headBranch, _token) {
        // Azure DevOps CLI doesn't provide structured file diffs easily;
        // fall back to git merge-base in the caller.
        return null;
    }
    buildOrgUrl(remote) {
        return remote.host === "dev.azure.com"
            ? `https://dev.azure.com/${remote.organization}`
            : `https://${remote.host}`;
    }
    buildRepoUrl(remote) {
        return remote.host === "dev.azure.com"
            ? `https://dev.azure.com/${remote.organization}/${remote.project}/_git/${remote.repo}`
            : `https://${remote.host}/${remote.project}/_git/${remote.repo}`;
    }
    buildPrUrl(remote, prId) {
        return `${this.buildRepoUrl(remote)}/pullrequest/${prId}`;
    }
}
// ══════════════════════════════════════════════════════════════════════
//  Bitbucket Forge
// ══════════════════════════════════════════════════════════════════════
export class BitbucketForge {
    provider = "bitbucket";
    displayName = "Bitbucket";
    async checkCliAvailable(_cwd) {
        return false;
    }
    async checkAuth(_cwd) {
        const token = await this.resolveToken(_cwd);
        return token
            ? { authenticated: true }
            : { authenticated: false, error: "No BITBUCKET_TOKEN set. Configure it in Settings → API." };
    }
    async loginWithToken(_token, _cwd) {
        return { authenticated: true };
    }
    async resolveToken(_cwd) {
        return process.env.BITBUCKET_TOKEN ?? process.env.BITBUCKET_APP_PASSWORD ?? null;
    }
    async getDefaultBranch(_cwd, remote) {
        const token = await this.resolveToken(_cwd);
        if (!token || !remote.owner)
            return null;
        try {
            const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}`, {
                headers: apiHeaders(token),
            });
            if (!res.ok)
                return null;
            const json = await res.json();
            return json.mainbranch?.name ?? null;
        }
        catch {
            return null;
        }
    }
    async findExistingPr(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests?q=source.branch.name="${encodeURIComponent(headBranch)}"&state=OPEN`, { headers: apiHeaders(effectiveToken) });
            if (!res.ok)
                return null;
            const data = await res.json();
            const pr = data.values?.[0];
            if (!pr)
                return null;
            return {
                status: "opened_existing",
                url: String(pr.links?.html?.href ?? ""),
                number: Number(pr.id ?? 0),
                baseBranch: String(pr.destination?.branch?.name ?? ""),
                headBranch: String(pr.source?.branch?.name ?? headBranch),
                title: String(pr.title ?? ""),
                state: "open",
            };
        }
        catch {
            return null;
        }
    }
    async createPr(_cwd, remote, input, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return { status: "not_found" };
        const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests`, {
            method: "POST",
            headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
            body: JSON.stringify({
                title: input.title,
                source: { branch: { name: input.headBranch } },
                destination: { branch: { name: input.baseBranch } },
                description: input.body,
            }),
        });
        if (!res.ok) {
            const text = await res.text().catch(() => "");
            throw new Error(`Bitbucket API PR create failed (${res.status}): ${text.slice(0, 400)}`);
        }
        const json = await res.json();
        return {
            status: "created",
            url: String(json.links?.html?.href ?? ""),
            number: Number(json.id ?? 0),
            baseBranch: String(json.destination?.branch?.name ?? input.baseBranch),
            headBranch: String(json.source?.branch?.name ?? input.headBranch),
            title: String(json.title ?? input.title),
        };
    }
    async getPrChecks(_cwd, _headBranch) {
        return [];
    }
    buildCreatePrUrl(remote, headBranch, _baseBranch) {
        return `${remote.normalizedUrl}/pull-requests/new?source=${encodeURIComponent(headBranch)}`;
    }
    async prDiffStats(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            // Use diffstat endpoint for PR
            const prRes = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests?q=source.branch.name="${encodeURIComponent(headBranch)}"&state=OPEN`, { headers: apiHeaders(effectiveToken) });
            if (!prRes.ok)
                return null;
            const prData = await prRes.json();
            const pr = prData.values?.[0];
            if (!pr)
                return null;
            const prId = Number(pr.id ?? 0);
            const diffstatRes = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests/${prId}/diffstat`, { headers: apiHeaders(effectiveToken) });
            if (!diffstatRes.ok)
                return null;
            const diffstat = await diffstatRes.json();
            const values = diffstat.values ?? [];
            let insertions = 0;
            let deletions = 0;
            for (const v of values) {
                insertions += Number(v.lines_added ?? 0);
                deletions += Number(v.lines_removed ?? 0);
            }
            return { files: values.length, insertions, deletions, hasChanges: values.length > 0 };
        }
        catch {
            return null;
        }
    }
    async prFileDiffs(_cwd, remote, headBranch, token) {
        const effectiveToken = token ?? await this.resolveToken(_cwd);
        if (!effectiveToken || !remote.owner)
            return null;
        try {
            const prRes = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests?q=source.branch.name="${encodeURIComponent(headBranch)}"&state=OPEN`, { headers: apiHeaders(effectiveToken) });
            if (!prRes.ok)
                return null;
            const prData = await prRes.json();
            const pr = prData.values?.[0];
            if (!pr)
                return null;
            const prId = Number(pr.id ?? 0);
            const diffstatRes = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests/${prId}/diffstat`, { headers: apiHeaders(effectiveToken) });
            if (!diffstatRes.ok)
                return null;
            const diffstat = await diffstatRes.json();
            return (diffstat.values ?? []).map((v) => {
                const filePath = v.new?.path ?? v.old?.path ?? "";
                let status = "M";
                if (v.status === "added")
                    status = "A";
                else if (v.status === "removed")
                    status = "D";
                else if (v.status === "renamed")
                    status = "R";
                return { path: filePath, original: "", modified: "", status };
            });
        }
        catch {
            return null;
        }
    }
}
// ══════════════════════════════════════════════════════════════════════
//  Forge Factory
// ══════════════════════════════════════════════════════════════════════
const FORGE_MAP = {
    github: () => new GitHubForge(),
    gitlab: () => new GitLabForge(),
    gitea: () => new GiteaForge(),
    "azure-devops": () => new AzureDevOpsForge(),
    bitbucket: () => new BitbucketForge(),
};
let _forgeCache = new Map();
export function getForge(provider) {
    if (provider === "unknown" || provider === "none")
        return null;
    const cached = _forgeCache.get(provider);
    if (cached)
        return cached;
    const factory = FORGE_MAP[provider];
    if (!factory)
        return null;
    const forge = factory();
    _forgeCache.set(provider, forge);
    return forge;
}
export function getForgeForRemote(remoteUrl) {
    if (!remoteUrl)
        return null;
    const parsed = parseGitRemote(remoteUrl);
    if (!parsed)
        return null;
    return getForge(parsed.provider);
}
//# sourceMappingURL=git-forge.js.map