/**
 * GitHub API client for the Issues subsystem.
 *
 * Uses native fetch against the GitHub REST API. Token resolution
 * follows the priority order defined in gh_auth.ts.
 *
 * Supports:
 *   - Public issues (Issues API)
 *   - Issue comments (as messages)
 */
import { resolveGitHubToken } from "./gh_auth.js";
import { loadIssuesConfig } from "./config.js";
const GITHUB_API = "https://api.github.com";
/** Applied to every GitHub issue created through Neotoma issue tooling (MCP, CLI, API). */
export const NEOTOMA_ISSUE_TOOLING_GITHUB_LABEL = "neotoma";
/**
 * Ensures tooling-submitted issues carry the canonical GitHub label plus any caller labels,
 * deduplicated in stable order (canonical first).
 */
export function mergeNeotomaToolingIssueLabels(userLabels) {
    const seen = new Set();
    const out = [];
    for (const raw of [NEOTOMA_ISSUE_TOOLING_GITHUB_LABEL, ...(userLabels ?? [])]) {
        const normalized = raw.trim();
        if (!normalized || seen.has(normalized))
            continue;
        seen.add(normalized);
        out.push(normalized);
    }
    return out;
}
async function resolveOptions(opts) {
    const token = opts?.token ?? (await resolveGitHubToken());
    if (!token) {
        throw new Error("No GitHub token available. Run `gh auth login` or set NEOTOMA_ISSUES_GITHUB_TOKEN.");
    }
    const cfg = await loadIssuesConfig();
    const fullRepo = opts?.repo ?? cfg.repo;
    const [owner, repo] = fullRepo.split("/");
    if (!owner || !repo) {
        throw new Error(`Invalid repo format: "${fullRepo}". Expected "owner/repo".`);
    }
    return { token, owner, repo };
}
class GitHubApiError extends Error {
    status;
    statusText;
    body;
    constructor(status, statusText, body) {
        super(`GitHub API ${status} ${statusText}: ${body}`);
        this.status = status;
        this.statusText = statusText;
        this.body = body;
    }
}
async function githubFetch(path, token, options) {
    const url = `${GITHUB_API}${path}`;
    const res = await fetch(url, {
        ...options,
        headers: {
            Authorization: `Bearer ${token}`,
            Accept: "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "Content-Type": "application/json",
            ...(options?.headers ?? {}),
        },
    });
    if (!res.ok) {
        const body = await res.text();
        throw new GitHubApiError(res.status, res.statusText, body);
    }
    return res.json();
}
/**
 * Create a new GitHub issue (for public mirror only).
 *
 * When GitHub returns 422 (Unprocessable Entity) for unknown labels, the
 * unknown labels are dropped and the request is retried with only the labels
 * that exist on the repo. A warning is logged for each dropped label. This
 * prevents a stale or unknown label name from silently aborting the mirror.
 */
export async function createIssue(params, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    const labels = mergeNeotomaToolingIssueLabels(params.labels);
    try {
        return await githubFetch(`/repos/${owner}/${repo}/issues`, token, {
            method: "POST",
            body: JSON.stringify({ title: params.title, body: params.body, labels }),
        });
    }
    catch (err) {
        if (!(err instanceof GitHubApiError) || err.status !== 422)
            throw err;
        // 422 often means one or more labels do not exist on the repo.
        // Fetch existing repo labels and retry with only the intersection.
        let existingLabels;
        try {
            existingLabels = await listRepoLabelNames({ token, repo: `${owner}/${repo}` });
        }
        catch {
            // Cannot determine which labels are valid — re-throw original error.
            throw err;
        }
        const existingSet = new Set(existingLabels.map((l) => l.toLowerCase()));
        const validLabels = labels.filter((l) => existingSet.has(l.toLowerCase()));
        const droppedLabels = labels.filter((l) => !existingSet.has(l.toLowerCase()));
        if (droppedLabels.length === 0) {
            // 422 was not label-related — re-throw.
            throw err;
        }
        // Emit structured warning into stderr so operators can add missing labels.
        process.stderr.write(`[neotoma] GitHub issue mirror: dropping unknown label(s) [${droppedLabels.join(", ")}] ` +
            `from repo ${owner}/${repo} — add them in GitHub Settings → Labels to include them.\n`);
        return githubFetch(`/repos/${owner}/${repo}/issues`, token, {
            method: "POST",
            body: JSON.stringify({ title: params.title, body: params.body, labels: validLabels }),
        });
    }
}
/**
 * Add a comment (message) to an existing GitHub issue.
 */
export async function addIssueComment(issueNumber, body, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    return githubFetch(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, token, { method: "POST", body: JSON.stringify({ body }) });
}
/**
 * Get a single issue by number.
 */
export async function getIssue(issueNumber, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    return githubFetch(`/repos/${owner}/${repo}/issues/${issueNumber}`, token);
}
/**
 * List issues from the configured GitHub repo.
 */
export async function listIssues(params, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    const query = new URLSearchParams();
    query.set("state", params?.state ?? "open");
    if (params?.labels?.length)
        query.set("labels", params.labels.join(","));
    if (params?.since)
        query.set("since", params.since);
    query.set("per_page", String(params?.per_page ?? 100));
    query.set("page", String(params?.page ?? 1));
    query.set("direction", "desc");
    query.set("sort", "updated");
    return githubFetch(`/repos/${owner}/${repo}/issues?${query.toString()}`, token);
}
/**
 * List comments on a specific issue.
 */
export async function listIssueComments(issueNumber, params, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    const query = new URLSearchParams();
    if (params?.since)
        query.set("since", params.since);
    query.set("per_page", String(params?.per_page ?? 100));
    query.set("page", String(params?.page ?? 1));
    return githubFetch(`/repos/${owner}/${repo}/issues/${issueNumber}/comments?${query.toString()}`, token);
}
/**
 * Close an issue.
 */
export async function closeIssue(issueNumber, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    return githubFetch(`/repos/${owner}/${repo}/issues/${issueNumber}`, token, {
        method: "PATCH",
        body: JSON.stringify({ state: "closed" }),
    });
}
/**
 * Add labels to an issue.
 */
export async function addIssueLabels(issueNumber, labels, opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    return githubFetch(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, token, { method: "POST", body: JSON.stringify({ labels }) });
}
/**
 * List all label names defined on a repo (used to validate labels before issue creation).
 */
export async function listRepoLabelNames(opts) {
    const { token, owner, repo } = await resolveOptions(opts);
    const results = await githubFetch(`/repos/${owner}/${repo}/labels?per_page=100`, token);
    return results.map((r) => r.name);
}
//# sourceMappingURL=github_client.js.map