export function buildGithubReviewPreview(draft, cards, changeset = draft.changeset) {
    const target = githubTargetFromChangeset(changeset);
    const warnings = [];
    if (!target) {
        warnings.push({ code: "github_target_missing", severity: "error", message: "This walkthrough is not linked to a GitHub pull request." });
    }
    const lineIndex = indexDiffLines(cards);
    const cardById = new Map(cards.map(card => [card.id, card]));
    const cardLevelComments = [];
    const rows = [];
    for (const comment of draft.comments) {
        const trimmedBody = comment.body.trim();
        if (!comment.diffBlockId && !comment.lineId) {
            if (trimmedBody)
                cardLevelComments.push({ ...comment, body: trimmedBody });
            continue;
        }
        const card = cardById.get(comment.cardId);
        const mapped = mapLineComment(comment, lineIndex, card);
        rows.push(mapped);
        if (!mapped.valid) {
            warnings.push({ code: "github_comment_unmappable", severity: "warning", message: mapped.reason ?? "Comment cannot be mapped to a GitHub review line.", commentId: comment.id });
        }
    }
    const validCommentCount = rows.filter(row => row.valid).length;
    const unmappableCommentCount = rows.length - validCommentCount;
    if (unmappableCommentCount > 0) {
        warnings.push({
            code: "github_unmappable_comments",
            severity: "warning",
            message: `${unmappableCommentCount} line comment${unmappableCommentCount === 1 ? "" : "s"} cannot be submitted to GitHub and will stay in the draft body/preview.`,
        });
    }
    return {
        provider: "github",
        target,
        body: buildReviewBody(draft, cards, cardLevelComments, rows),
        rows,
        validCommentCount,
        unmappableCommentCount,
        warnings,
        generatedAt: new Date().toISOString(),
    };
}
export async function submitGithubReview(preview, confirmation, options = {}) {
    if (confirmation.confirm !== true) {
        return { ok: false, status: 400, submitted: false, message: "Explicit confirm=true is required before submitting a GitHub review." };
    }
    if (!preview.target) {
        return { ok: false, status: 400, submitted: false, message: "No GitHub pull request target is available for this review preview.", warnings: preview.warnings };
    }
    const token = cleanString(confirmation.token) ?? cleanString(options.token) ?? cleanString(process.env.GITHUB_TOKEN) ?? cleanString(process.env.GH_TOKEN);
    if (!token) {
        return { ok: false, status: 401, submitted: false, message: "Set GITHUB_TOKEN or GH_TOKEN before submitting a GitHub review.", warnings: preview.warnings };
    }
    const payload = createGithubReviewPayload(preview, confirmation.event ?? "COMMENT");
    const apiBaseUrl = cleanString(options.apiBaseUrl) ?? cleanString(process.env.BOBBIT_GITHUB_API_BASE_URL) ?? "https://api.github.com";
    const url = `${apiBaseUrl}/repos/${encodeURIComponent(preview.target.owner)}/${encodeURIComponent(preview.target.repo)}/pulls/${preview.target.prNumber}/reviews`;
    const response = await (options.fetch ?? fetch)(url, {
        method: "POST",
        headers: {
            Accept: "application/vnd.github+json",
            "Content-Type": "application/json",
            "User-Agent": "bobbit-pr-walkthrough",
            "X-GitHub-Api-Version": "2022-11-28",
            Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(20_000),
    });
    if (!response.ok) {
        const text = await response.text().catch(() => response.statusText);
        return { ok: false, status: response.status, submitted: false, message: `GitHub review submission failed (${response.status}): ${text || response.statusText}`, warnings: preview.warnings };
    }
    const json = await response.json().catch(() => ({}));
    return {
        ok: true,
        status: response.status,
        submitted: true,
        message: "GitHub review submitted.",
        reviewUrl: reviewUrlFromResponse(json),
        response: json,
        warnings: preview.warnings,
    };
}
export function createGithubReviewPayload(preview, event = "COMMENT") {
    const comments = preview.rows
        .filter(row => row.valid && row.path && row.side && Number.isInteger(row.line) && (row.line ?? 0) > 0)
        .map(row => ({ path: row.path, side: row.side, line: row.line, body: row.body }));
    return {
        body: preview.body,
        event,
        ...(preview.target?.headSha ? { commit_id: preview.target.headSha } : {}),
        comments,
    };
}
function mapLineComment(comment, lineIndex, card) {
    const base = {
        id: `row:${comment.id}`,
        commentId: comment.id,
        cardId: comment.cardId,
        cardTitle: card?.title,
        diffBlockId: comment.diffBlockId,
        lineId: comment.lineId,
        body: comment.body.trim(),
    };
    if (!base.body)
        return { ...base, valid: false, reason: "Comment body is empty." };
    if (!comment.diffBlockId || !comment.lineId)
        return { ...base, valid: false, reason: "Line comments need both a diff block and line anchor." };
    const indexed = lineIndex.get(lineKey(comment.cardId, comment.diffBlockId, comment.lineId));
    if (!indexed)
        return { ...base, valid: false, reason: "Diff line anchor was not found in the current walkthrough cards." };
    if (isUnreviewableBlock(indexed.block))
        return { ...base, path: indexed.block.filePath, valid: false, reason: "This file has no GitHub-reviewable text diff." };
    const sideAndLine = githubSideAndLine(indexed.line);
    if (!sideAndLine)
        return { ...base, path: indexed.block.filePath, valid: false, reason: "Diff line has no old or new line number for GitHub." };
    return {
        ...base,
        path: indexed.block.filePath,
        side: sideAndLine.side,
        line: sideAndLine.line,
        valid: true,
    };
}
function githubSideAndLine(line) {
    if (line.side !== "old" && typeof line.newLine === "number" && line.newLine > 0)
        return { side: "RIGHT", line: line.newLine };
    if (typeof line.oldLine === "number" && line.oldLine > 0)
        return { side: "LEFT", line: line.oldLine };
    return undefined;
}
function indexDiffLines(cards) {
    const index = new Map();
    for (const card of cards) {
        for (const block of card.diffBlocks) {
            for (const hunk of block.hunks) {
                for (const line of hunk.lines) {
                    index.set(lineKey(card.id, block.id, line.id), { card, block, line });
                }
            }
        }
    }
    return index;
}
function lineKey(cardId, diffBlockId, lineId) {
    return `${cardId}\u0000${diffBlockId}\u0000${lineId}`;
}
function buildReviewBody(draft, cards, cardLevelComments, rows) {
    const cardById = new Map(cards.map(card => [card.id, card]));
    const liked = Object.values(draft.decisions).filter(decision => decision.value === "liked").length;
    const disliked = Object.values(draft.decisions).filter(decision => decision.value === "disliked").length;
    const lines = ["Bobbit PR walkthrough draft", ""];
    if (draft.changeset.title)
        lines.push(`Changeset: ${draft.changeset.title}`);
    if (draft.changeset.externalUrl || draft.changeset.prUrl)
        lines.push(`Source: ${draft.changeset.externalUrl ?? draft.changeset.prUrl}`);
    lines.push(`Reviewed cards: ${draft.completedCardIds.length}`);
    lines.push(`Decisions: ${liked} liked, ${disliked} disliked`);
    lines.push(`GitHub line comments ready: ${rows.filter(row => row.valid).length}`);
    const unmappableRows = rows.filter(row => !row.valid);
    if (unmappableRows.length > 0) {
        lines.push("", "Unmappable line comments:");
        for (const row of unmappableRows)
            lines.push(`- ${row.cardTitle ?? row.cardId}: ${row.body}${row.reason ? ` (${row.reason})` : ""}`);
    }
    if (cardLevelComments.length > 0) {
        lines.push("", "Card-level comments:");
        for (const comment of cardLevelComments) {
            const title = cardById.get(comment.cardId)?.title ?? comment.cardId;
            lines.push(`- ${title}: ${comment.body}`);
        }
    }
    return lines.join("\n").trim();
}
function githubTargetFromChangeset(changeset) {
    const url = cleanString(changeset.prUrl) ?? cleanString(changeset.externalUrl);
    const fromUrl = url ? parseGithubPullUrl(url) : undefined;
    const repoParts = splitRepository(cleanString(changeset.repository));
    const owner = cleanString(changeset.owner) ?? fromUrl?.owner ?? repoParts?.owner;
    const repo = cleanString(changeset.repo) ?? fromUrl?.repo ?? repoParts?.repo;
    const prNumber = normalizePrNumber(changeset.prNumber) ?? fromUrl?.number;
    if (!owner || !repo || !prNumber)
        return undefined;
    return { provider: "github", owner, repo, prNumber, prUrl: url, headSha: cleanString(changeset.headSha) };
}
function parseGithubPullUrl(value) {
    try {
        const url = new URL(value);
        const match = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\/|$)/i.exec(url.pathname);
        if (!match)
            return undefined;
        return { owner: decodeURIComponent(match[1]), repo: decodeURIComponent(match[2]).replace(/\.git$/i, ""), number: Number(match[3]) };
    }
    catch {
        return undefined;
    }
}
function splitRepository(repository) {
    if (!repository)
        return undefined;
    const [owner, repo] = repository.split("/");
    return owner && repo ? { owner, repo } : undefined;
}
function normalizePrNumber(value) {
    const raw = typeof value === "number" ? String(Math.trunc(value)) : cleanString(value);
    if (!raw)
        return undefined;
    const normalized = raw.replace(/^#/, "").trim();
    return /^\d+$/.test(normalized) ? Number(normalized) : undefined;
}
function isUnreviewableBlock(block) {
    return block.isBinary === true || block.isTruncated === true || /^(binary|truncated)$/i.test(block.status ?? "") || block.hunks.length === 0;
}
function reviewUrlFromResponse(value) {
    if (!value || typeof value !== "object")
        return undefined;
    const htmlUrl = value.html_url;
    return typeof htmlUrl === "string" ? htmlUrl : undefined;
}
function cleanString(value) {
    return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
//# sourceMappingURL=export-mapper.js.map