/**
 * PR Generator
 *
 * Orchestrates the creation of autofix pull requests.
 * Uses gh CLI for GitHub operations (works locally and in CI).
 *
 * @module autofix/pr-generator
 */
import spawn from "cross-spawn";
import { applyFix, previewFix, FIX_PATTERNS } from "../certification/autofix.js";
import { git, isGitRepo, getCurrentBranch, getDefaultBranch, createBranch, checkoutBranch, stageFiles, pushBranch, getRemoteUrl, parseGitHubRemote, generateBranchName, ensureCleanWorkingTree, restoreOriginalState, } from "./branch-manager.js";
import { generateCommitMessage, generateCommitBody, createCommit, generatePRTitle, generatePRBody, } from "./commit-generator.js";
import { logger } from "../logger.js";
import { loadConstitution, isPathAllowed, isPatternApproved, } from "./constitution.js";
/**
 * Check if gh CLI is available
 */
export async function isGhCliAvailable() {
    return new Promise((resolve) => {
        const child = spawn("gh", ["--version"], { timeout: 5000 });
        child.on("close", (code) => {
            resolve(code === 0);
        });
        child.on("error", () => {
            resolve(false);
        });
    });
}
/**
 * Check if authenticated with gh CLI
 */
export async function isGhAuthenticated() {
    return new Promise((resolve) => {
        const child = spawn("gh", ["auth", "status"], { timeout: 10000 });
        child.on("close", (code) => {
            resolve(code === 0);
        });
        child.on("error", () => {
            resolve(false);
        });
    });
}
/**
 * Create a PR using gh CLI
 */
async function createPRWithGh(cwd, title, body, baseBranch, headBranch, labels = []) {
    return new Promise((resolve) => {
        const args = [
            "pr",
            "create",
            "--title",
            title,
            "--body",
            body,
            "--base",
            baseBranch,
            "--head",
            headBranch,
        ];
        if (labels.length > 0) {
            args.push("--label", labels.join(","));
        }
        logger.info("gh.create_pr", { title, baseBranch, headBranch });
        const child = spawn("gh", args, {
            cwd,
            timeout: 60000,
        });
        let stdout = "";
        let stderr = "";
        child.stdout?.on("data", (data) => {
            stdout += data.toString();
        });
        child.stderr?.on("data", (data) => {
            stderr += data.toString();
        });
        child.on("close", (code) => {
            if (code === 0) {
                // stdout contains the PR URL
                const prUrl = stdout.trim();
                // Extract PR number from URL
                const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
                const prNumber = prNumberMatch ? parseInt(prNumberMatch[1], 10) : 0;
                logger.info("gh.pr_created", { prNumber, prUrl });
                resolve({ prNumber, prUrl });
            }
            else {
                logger.error("gh.pr_create_failed", { stderr, exitCode: code });
                resolve({ error: stderr || "Failed to create PR" });
            }
        });
        child.on("error", (error) => {
            logger.error("gh.spawn_error", { error: String(error) });
            resolve({ error: String(error) });
        });
    });
}
/**
 * Group findings into PR groups based on config
 */
export function groupFindings(findings, groupBy, branchPrefix) {
    const groups = [];
    switch (groupBy) {
        case "severity": {
            const severityOrder = ["critical", "high", "medium", "low", "info"];
            for (const severity of severityOrder) {
                const matching = findings.filter((f) => f.severity === severity);
                if (matching.length > 0) {
                    groups.push({
                        groupId: `severity-${severity}`,
                        branchName: generateBranchName(branchPrefix, severity),
                        findings: matching,
                        severity,
                    });
                }
            }
            break;
        }
        case "file": {
            const byFile = new Map();
            for (const finding of findings) {
                if (finding.file) {
                    const existing = byFile.get(finding.file) || [];
                    existing.push(finding);
                    byFile.set(finding.file, existing);
                }
            }
            for (const [file, fileFindings] of byFile) {
                const shortFile = file.split("/").pop() || file;
                groups.push({
                    groupId: `file-${file}`,
                    branchName: generateBranchName(branchPrefix, shortFile),
                    findings: fileFindings,
                    file,
                });
            }
            break;
        }
        case "pattern": {
            const byPattern = new Map();
            for (const finding of findings) {
                const patternId = finding.category || "other";
                const existing = byPattern.get(patternId) || [];
                existing.push(finding);
                byPattern.set(patternId, existing);
            }
            for (const [patternId, patternFindings] of byPattern) {
                groups.push({
                    groupId: `pattern-${patternId}`,
                    branchName: generateBranchName(branchPrefix, patternId),
                    findings: patternFindings,
                    patternId,
                });
            }
            break;
        }
        case "single": {
            for (let i = 0; i < findings.length; i++) {
                const finding = findings[i];
                groups.push({
                    groupId: `single-${finding.id}`,
                    branchName: generateBranchName(branchPrefix, finding.id),
                    findings: [finding],
                    severity: finding.severity,
                });
            }
            break;
        }
    }
    return groups;
}
/**
 * Create a single autofix PR for a group of findings
 */
export async function createAutofixPR(config, group, constitution) {
    const { projectPath, baseBranch, dryRun, signCommits, includeUnsafe, remoteName } = config;
    // Store original state
    const originalBranch = await getCurrentBranch(projectPath);
    if (!originalBranch) {
        return {
            branch: group.branchName,
            fixesApplied: [],
            filesModified: [],
            dryRun,
            error: "Could not determine current branch",
        };
    }
    // Ensure clean working tree
    const { clean, stashed } = await ensureCleanWorkingTree(projectPath);
    if (!clean) {
        return {
            branch: group.branchName,
            fixesApplied: [],
            filesModified: [],
            dryRun,
            error: "Working tree has uncommitted changes. Please commit or stash them first.",
        };
    }
    try {
        // Create and checkout the new branch
        if (!dryRun) {
            const branchResult = await createBranch(projectPath, group.branchName, baseBranch);
            if (!branchResult.success) {
                await restoreOriginalState(projectPath, originalBranch, stashed);
                return {
                    branch: group.branchName,
                    fixesApplied: [],
                    filesModified: [],
                    dryRun,
                    error: `Failed to create branch: ${branchResult.stderr}`,
                };
            }
        }
        // Apply fixes
        const fixResults = [];
        const filesModified = new Set();
        for (const finding of group.findings) {
            // Preview first
            const preview = await previewFix(projectPath, finding);
            if (!preview.canAutoFix) {
                continue;
            }
            if (preview.risk === "high" && !includeUnsafe) {
                logger.info("autofix.skipped_unsafe", { findingId: finding.id });
                continue;
            }
            // Apply the fix
            const result = await applyFix(projectPath, finding, dryRun);
            fixResults.push(result);
            if (result.applied || result.diff) {
                filesModified.add(result.file);
            }
        }
        if (fixResults.length === 0) {
            if (!dryRun) {
                await restoreOriginalState(projectPath, originalBranch, stashed);
            }
            return {
                branch: group.branchName,
                fixesApplied: [],
                filesModified: [],
                dryRun,
                error: "No fixes could be applied",
                severity: group.severity,
            };
        }
        if (dryRun) {
            return {
                branch: group.branchName,
                fixesApplied: fixResults,
                filesModified: Array.from(filesModified),
                dryRun: true,
                severity: group.severity,
            };
        }
        // Stage the modified files
        const stageResult = await stageFiles(projectPath, Array.from(filesModified));
        if (!stageResult.success) {
            await restoreOriginalState(projectPath, originalBranch, stashed);
            return {
                branch: group.branchName,
                fixesApplied: fixResults,
                filesModified: Array.from(filesModified),
                dryRun,
                error: `Failed to stage files: ${stageResult.stderr}`,
                severity: group.severity,
            };
        }
        // Create commit
        const commitMessage = generateCommitMessage(fixResults, group.severity);
        const commitBody = generateCommitBody(fixResults, group.severity);
        const commitResult = await createCommit(projectPath, commitMessage, commitBody);
        if ("error" in commitResult) {
            await restoreOriginalState(projectPath, originalBranch, stashed);
            return {
                branch: group.branchName,
                fixesApplied: fixResults,
                filesModified: Array.from(filesModified),
                dryRun,
                error: commitResult.error,
                severity: group.severity,
            };
        }
        // Push branch
        const pushResult = await pushBranch(projectPath, group.branchName, remoteName);
        if (!pushResult.success) {
            await restoreOriginalState(projectPath, originalBranch, stashed);
            return {
                branch: group.branchName,
                fixesApplied: fixResults,
                filesModified: Array.from(filesModified),
                commitSha: commitResult.sha,
                dryRun,
                error: `Failed to push branch: ${pushResult.stderr}`,
                severity: group.severity,
            };
        }
        // Create PR with constitution labels
        const prLabels = constitution?.prRules.requiredLabels || ["autofix", "security"];
        const prTitle = generatePRTitle(fixResults, group.severity, config.titleTemplate);
        const prBody = generatePRBody(fixResults, group.severity, {
            includeBeforeAfter: true,
        });
        const prResult = await createPRWithGh(projectPath, prTitle, prBody, baseBranch, group.branchName, prLabels);
        // Restore original branch
        await checkoutBranch(projectPath, originalBranch);
        if (stashed) {
            await git(["stash", "pop"], { cwd: projectPath });
        }
        if ("error" in prResult) {
            return {
                branch: group.branchName,
                fixesApplied: fixResults,
                filesModified: Array.from(filesModified),
                commitSha: commitResult.sha,
                dryRun,
                error: prResult.error,
                severity: group.severity,
            };
        }
        return {
            prNumber: prResult.prNumber,
            prUrl: prResult.prUrl,
            branch: group.branchName,
            fixesApplied: fixResults,
            filesModified: Array.from(filesModified),
            commitSha: commitResult.sha,
            dryRun,
            severity: group.severity,
        };
    }
    catch (error) {
        // Always try to restore original state on error
        try {
            await restoreOriginalState(projectPath, originalBranch, stashed);
        }
        catch {
            // Ignore restoration errors
        }
        return {
            branch: group.branchName,
            fixesApplied: [],
            filesModified: [],
            dryRun,
            error: String(error),
            severity: group.severity,
        };
    }
}
/**
 * Find the fix pattern for a finding
 */
function findPatternForFinding(finding) {
    // Try matching by category first
    if (finding.category) {
        const categoryStr = typeof finding.category === "string" ? finding.category : String(finding.category);
        const pattern = FIX_PATTERNS.find((p) => p.patternId === categoryStr || p.patternId.includes(categoryStr));
        if (pattern)
            return pattern;
    }
    // Try matching by scanner source
    if (finding.scanner_source) {
        const pattern = FIX_PATTERNS.find((p) => p.patternId.includes(finding.scanner_source || ""));
        if (pattern)
            return pattern;
    }
    return undefined;
}
/**
 * Filter findings based on constitution rules
 */
function filterFindingsByConstitution(findings, constitution) {
    const allowed = [];
    const blocked = [];
    for (const finding of findings) {
        // Check path restrictions
        if (finding.file) {
            const pathCheck = isPathAllowed(constitution, finding.file);
            if (!pathCheck.allowed) {
                blocked.push({ finding, reason: pathCheck.reason || "Path not allowed" });
                continue;
            }
        }
        // Check pattern approval
        const pattern = findPatternForFinding(finding);
        if (pattern) {
            const severity = (finding.severity || "medium");
            const approval = isPatternApproved(constitution, pattern, {
                filePath: finding.file || "",
                severity,
            });
            if (!approval.approved) {
                blocked.push({
                    finding,
                    reason: approval.reason || "Pattern not approved",
                });
                continue;
            }
        }
        allowed.push(finding);
    }
    return { allowed, blocked };
}
/**
 * Create autofix PRs for all findings
 */
export async function createAutofixPRs(config, constitution) {
    // Load constitution if not provided
    const effectiveConstitution = constitution || await loadConstitution(config.projectPath);
    const fullConfig = {
        branchPrefix: "vaspera/autofix",
        baseBranch: "main",
        // Use constitution's groupBy if specified
        groupBy: effectiveConstitution.prRules.groupBy,
        // Use constitution's dryRunDefault
        dryRun: effectiveConstitution.safety.dryRunDefault,
        signCommits: false,
        // Use aggressive risk tolerance to include unsafe
        includeUnsafe: effectiveConstitution.riskTolerance === "aggressive",
        remoteName: "origin",
        ...config,
    };
    const { projectPath, findings, groupBy, dryRun } = fullConfig;
    // Filter findings based on constitution
    const { allowed: allowedFindings, blocked } = filterFindingsByConstitution(findings, effectiveConstitution);
    logger.info("constitution.filtered_findings", {
        total: findings.length,
        allowed: allowedFindings.length,
        blocked: blocked.length,
    });
    // Add blocked findings to unfixable list
    const unfixable = blocked.map(({ finding, reason }) => ({ findingId: finding.id, reason }));
    // Check if any findings remain
    if (allowedFindings.length === 0) {
        return {
            totalFindings: findings.length,
            prsCreated: [],
            unfixable,
            success: unfixable.length === 0,
            summary: { bySeverity: {}, byFile: {}, byPattern: {} },
        };
    }
    // Validate prerequisites
    if (!(await isGitRepo(projectPath))) {
        return {
            totalFindings: findings.length,
            prsCreated: [],
            unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "Not a git repository" }))],
            success: false,
            summary: { bySeverity: {}, byFile: {}, byPattern: {} },
        };
    }
    if (!dryRun) {
        const ghAvailable = await isGhCliAvailable();
        if (!ghAvailable) {
            return {
                totalFindings: findings.length,
                prsCreated: [],
                unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "gh CLI not available" }))],
                success: false,
                summary: { bySeverity: {}, byFile: {}, byPattern: {} },
            };
        }
        const ghAuthenticated = await isGhAuthenticated();
        if (!ghAuthenticated) {
            return {
                totalFindings: findings.length,
                prsCreated: [],
                unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "gh CLI not authenticated" }))],
                success: false,
                summary: { bySeverity: {}, byFile: {}, byPattern: {} },
            };
        }
        const remoteUrl = await getRemoteUrl(projectPath, fullConfig.remoteName);
        if (!remoteUrl || !parseGitHubRemote(remoteUrl)) {
            return {
                totalFindings: findings.length,
                prsCreated: [],
                unfixable: [...unfixable, ...allowedFindings.map((f) => ({ findingId: f.id, reason: "Not a GitHub repository" }))],
                success: false,
                summary: { bySeverity: {}, byFile: {}, byPattern: {} },
            };
        }
    }
    // Get default branch if not specified
    if (fullConfig.baseBranch === "main") {
        fullConfig.baseBranch = await getDefaultBranch(projectPath);
    }
    // Check max files limit from constitution
    const maxFiles = effectiveConstitution.safety.maxFilesPerRun;
    const uniqueFiles = new Set(allowedFindings.map((f) => f.file).filter(Boolean));
    if (uniqueFiles.size > maxFiles) {
        logger.warn("constitution.max_files_exceeded", {
            files: uniqueFiles.size,
            maxFiles,
        });
        // Continue but log warning - could also return error here
    }
    // Check max PRs limit from constitution
    const maxPRs = effectiveConstitution.prRules.maxPRsPerRun;
    // Group findings
    let groups = groupFindings(allowedFindings, groupBy, fullConfig.branchPrefix);
    // Limit to maxPRsPerRun
    if (groups.length > maxPRs) {
        logger.warn("constitution.max_prs_exceeded", {
            groups: groups.length,
            maxPRs,
        });
        groups = groups.slice(0, maxPRs);
    }
    logger.info("autofix.creating_prs", {
        totalFindings: findings.length,
        allowedFindings: allowedFindings.length,
        groups: groups.length,
        groupBy,
        dryRun,
    });
    // Create PRs for each group, passing constitution for labels
    const prsCreated = [];
    for (const group of groups) {
        const result = await createAutofixPR(fullConfig, group, effectiveConstitution);
        prsCreated.push(result);
        if (result.error) {
            for (const finding of group.findings) {
                unfixable.push({ findingId: finding.id, reason: result.error });
            }
        }
    }
    // Build summary
    const summary = {
        bySeverity: {},
        byFile: {},
        byPattern: {},
    };
    for (const pr of prsCreated) {
        if (pr.severity) {
            summary.bySeverity[pr.severity] = (summary.bySeverity[pr.severity] || 0) + pr.fixesApplied.length;
        }
        for (const file of pr.filesModified) {
            summary.byFile[file] = (summary.byFile[file] || 0) + 1;
        }
    }
    const allSuccessful = prsCreated.every((pr) => !pr.error);
    logger.info("autofix.batch_complete", {
        prsCreated: prsCreated.length,
        successfulPrs: prsCreated.filter((pr) => !pr.error).length,
        totalFixes: prsCreated.reduce((sum, pr) => sum + pr.fixesApplied.length, 0),
        unfixable: unfixable.length,
    });
    return {
        totalFindings: findings.length,
        prsCreated,
        unfixable,
        success: allSuccessful,
        summary,
    };
}
/**
 * Preview autofix PRs without creating them
 */
export async function previewAutofixPRs(projectPath, findings, options) {
    const groupBy = options?.groupBy ?? "severity";
    const includeUnsafe = options?.includeUnsafe ?? false;
    const groups = groupFindings(findings, groupBy, "vaspera/autofix");
    const result = [];
    for (const group of groups) {
        const changes = [];
        let fixable = 0;
        let unfixable = 0;
        for (const finding of group.findings) {
            const preview = await previewFix(projectPath, finding);
            const canFix = preview.canAutoFix && (includeUnsafe || preview.risk !== "high");
            changes.push({
                file: finding.file || "unknown",
                findingId: finding.id,
                canAutoFix: canFix,
                risk: preview.risk,
            });
            if (canFix) {
                fixable++;
            }
            else {
                unfixable++;
            }
        }
        result.push({
            groupId: group.groupId,
            branchName: group.branchName,
            findings: group.findings.length,
            severity: group.severity,
            fixable,
            unfixable,
            estimatedChanges: changes,
        });
    }
    return { groups: result };
}
//# sourceMappingURL=pr-generator.js.map