/**
 * Review command - executes the PR reviewer cron script
 */
import { CLAUDE_MODEL_IDS, PROVIDER_COMMANDS, analyzeFeedbackOutcome, buildProjectFeedbackPromptBlock, buildSessionOutcomeInput, createSpinner, createTable, dim, executeScriptWithOutput, fetchPrDetailsByNumber, fetchReviewedPrDetails, getRepositories, getScriptPath, header, info, isFeedbackPromptEnabled, loadConfig, parseScriptResult, resolveJobProvider, sendNotifications, error as uiError, } from '@night-watch/core';
import { buildBaseEnvVars, formatProviderDisplay, maybeApplyCronSchedulingDelay, } from './shared/env-builder.js';
import { getFeedbackAnalysisOptions, isFeedbackEnabled } from './shared/feedback.js';
import { buildTelemetryBaseProperties, fireTelemetryEvent, trackCommandCompleted, trackCommandStarted, trackJobCompletedOrFailed, trackJobStarted, } from './shared/telemetry.js';
import { execFileSync } from 'child_process';
import * as path from 'path';
/**
 * Review notifications should not fire for script-level skip/no-op outcomes.
 */
export function shouldSendReviewNotification(scriptStatus) {
    if (!scriptStatus) {
        return true;
    }
    if (scriptStatus === 'queued') {
        return false;
    }
    return !scriptStatus.startsWith('skip_');
}
/**
 * Review completion notifications are only valid for successful reviewer runs.
 * Guard against both non-zero exits and mismatched legacy status markers.
 */
export function shouldSendReviewCompletionNotification(exitCode, scriptStatus) {
    if (exitCode !== 0) {
        return false;
    }
    if (scriptStatus === 'failure' || scriptStatus === 'timeout') {
        return false;
    }
    return shouldSendReviewNotification(scriptStatus);
}
/**
 * Parse comma-separated PR numbers like "#12,#34" into numeric IDs.
 */
export function parseAutoMergedPrNumbers(raw) {
    if (!raw || raw.trim().length === 0) {
        return [];
    }
    return raw
        .split(',')
        .map((token) => parseInt(token.trim().replace(/^#/, ''), 10))
        .filter((value) => !Number.isNaN(value));
}
/**
 * Parse comma-separated PR numbers like "#12,#34" into numeric IDs.
 * Deduplicates while preserving order.
 */
export function parseReviewedPrNumbers(raw) {
    if (!raw || raw.trim().length === 0) {
        return [];
    }
    const seen = new Set();
    return raw
        .split(',')
        .map((token) => parseInt(token.trim().replace(/^#/, ''), 10))
        .filter((value) => !Number.isNaN(value))
        .filter((value) => {
        if (seen.has(value)) {
            return false;
        }
        seen.add(value);
        return true;
    });
}
/**
 * Build per-PR review notification targets from the script result payload.
 * Legacy no_changes_needed is only trustworthy when exactly one PR was reviewed.
 */
export function buildReviewNotificationTargets(reviewedPrNumbers, noChangesPrNumbers, legacyNoChangesNeeded = false) {
    const uniqueReviewedPrNumbers = Array.from(new Set(reviewedPrNumbers));
    const noChangesSet = new Set(noChangesPrNumbers);
    if (legacyNoChangesNeeded && uniqueReviewedPrNumbers.length === 1) {
        noChangesSet.add(uniqueReviewedPrNumbers[0]);
    }
    return uniqueReviewedPrNumbers.map((prNumber) => ({
        prNumber,
        noChangesNeeded: noChangesSet.has(prNumber),
    }));
}
export function applyProjectFeedbackPromptEnv(envVars, projectDir, jobType, markApplied = true) {
    delete envVars.NW_PROJECT_FEEDBACK_PROMPT;
    const config = loadConfig(projectDir);
    if (!isFeedbackPromptEnabled() || config.feedback?.enabled === false) {
        return;
    }
    try {
        const { promptBlock } = buildProjectFeedbackPromptBlock(getRepositories().sessionOutcomes, projectDir, jobType, { markApplied, maxActiveAugmentations: config.feedback?.maxActiveAugmentations });
        if (promptBlock.length > 0) {
            envVars.NW_PROJECT_FEEDBACK_PROMPT = promptBlock;
        }
    }
    catch {
        // Feedback prompt context must never block the primary reviewer path.
    }
}
/**
 * Parse retry attempts from script result data.
 * Returns the number of attempts (defaults to 1 if not present or invalid).
 */
export function parseRetryAttempts(raw) {
    if (!raw) {
        return 1;
    }
    const parsed = parseInt(raw, 10);
    return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
}
/**
 * Parse final review score from script result data.
 * Returns undefined when missing or invalid.
 */
export function parseFinalReviewScore(raw) {
    if (!raw) {
        return undefined;
    }
    const parsed = parseInt(raw, 10);
    if (Number.isNaN(parsed)) {
        return undefined;
    }
    return parsed;
}
/**
 * Build environment variables map from config and CLI options for reviewer
 */
export function buildEnvVars(config, options) {
    // Start with base env vars shared by all job types
    const env = buildBaseEnvVars(config, 'reviewer', options.dryRun);
    // Runtime for reviewer (uses NW_REVIEWER_* variables)
    env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
    env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
    env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
    env.NW_REVIEWER_MAX_PRS_PER_RUN = String(config.reviewerMaxPrsPerRun);
    env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
    env.NW_BRANCH_PATTERNS = config.branchPatterns.join(',');
    env.NW_PRD_DIR = config.prdDir;
    env.NW_PR_RESOLVER_READY_LABEL = config.prResolver.readyLabel;
    env.NW_CLAUDE_MODEL_ID =
        CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? 'sonnet'];
    return env;
}
/**
 * Apply CLI flag overrides to the config for reviewer
 */
export function applyCliOverrides(config, options) {
    const overridden = { ...config };
    if (options.timeout) {
        const timeout = parseInt(options.timeout, 10);
        if (!isNaN(timeout)) {
            overridden.reviewerMaxRuntime = timeout;
        }
    }
    if (options.provider) {
        // Use _cliProviderOverride to ensure CLI flag takes precedence over jobProviders
        overridden._cliProviderOverride = options.provider;
    }
    return overridden;
}
/**
 * Whether a GitHub check entry should be treated as failing/action-required.
 */
export function isFailingCheck(check) {
    const bucket = (check.bucket ?? '').toLowerCase();
    const state = (check.state ?? '').toLowerCase();
    const conclusion = (check.conclusion ?? '').toLowerCase();
    return (bucket === 'fail' ||
        bucket === 'cancel' ||
        state === 'failure' ||
        state === 'error' ||
        state === 'cancelled' ||
        conclusion === 'failure' ||
        conclusion === 'error' ||
        conclusion === 'cancelled' ||
        conclusion === 'timed_out' ||
        conclusion === 'action_required' ||
        conclusion === 'startup_failure' ||
        conclusion === 'stale');
}
/**
 * Get a human-readable list of failing checks for a PR.
 */
export function getPrFailingChecks(prNumber) {
    try {
        const result = execFileSync('gh', ['pr', 'checks', String(prNumber), '--json', 'name,bucket,state,conclusion'], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        const checks = JSON.parse(result.trim() || '[]');
        const failing = checks
            .filter((check) => isFailingCheck(check))
            .map((check) => `${check.name ?? 'unknown'} [state=${check.state ?? 'unknown'}, conclusion=${check.conclusion ?? 'unknown'}]`);
        if (failing.length > 0) {
            return failing;
        }
    }
    catch {
        // Fall through to text-mode fallback.
    }
    try {
        const result = execFileSync('gh', ['pr', 'checks', String(prNumber)], {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return result
            .split('\n')
            .map((line) => line.trim())
            .filter((line) => line.length > 0)
            .filter((line) => /fail|error|cancel|timed[_ -]?out|action_required|startup_failure|stale/i.test(line));
    }
    catch {
        return [];
    }
}
/**
 * Get open PRs that need work (matching branch patterns)
 */
function getOpenPrsNeedingWork(branchPatterns) {
    try {
        // Build args array for safe shell execution
        const args = ['pr', 'list', '--state', 'open', '--json', 'number,title,headRefName'];
        for (const pattern of branchPatterns) {
            args.push('--head', pattern);
        }
        // Get open PRs as JSON using execFileSync for safe argument handling
        const result = execFileSync('gh', args, {
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        const prs = JSON.parse(result.trim() || '[]');
        return prs.map((pr) => ({
            number: pr.number,
            title: pr.title,
            branch: pr.headRefName,
        }));
    }
    catch {
        // gh CLI not available or not authenticated
        return [];
    }
}
/**
 * Register the review command with the program
 */
export function reviewCommand(program) {
    program
        .command('review')
        .description('Run PR reviewer now')
        .option('--dry-run', 'Show what would be executed without running')
        .option('--timeout <seconds>', 'Override max runtime in seconds for reviewer')
        .option('--provider <string>', 'AI provider to use (claude or codex)')
        .action(async (options) => {
        // Get the project directory (current working directory)
        const projectDir = process.cwd();
        // Load config from file and environment
        let config = loadConfig(projectDir);
        // Apply CLI flag overrides
        config = applyCliOverrides(config, options);
        const commandStartedAt = Date.now();
        await trackCommandStarted('review', config);
        if (!config.reviewerEnabled && !options.dryRun) {
            info('Reviewer is disabled in config; skipping review.');
            await trackCommandCompleted('review', commandStartedAt, 0, config);
            process.exit(0);
        }
        // Build environment variables
        const envVars = buildEnvVars(config, options);
        applyProjectFeedbackPromptEnv(envVars, projectDir, 'reviewer', !options.dryRun);
        // Get the script path
        const scriptPath = getScriptPath('night-watch-pr-reviewer-cron.sh');
        if (options.dryRun) {
            header('Dry Run: PR Reviewer');
            // Resolve reviewer-specific provider
            const reviewerProvider = resolveJobProvider(config, 'reviewer');
            // Configuration section with table
            header('Configuration');
            const configTable = createTable({ head: ['Setting', 'Value'] });
            configTable.push(['Provider', reviewerProvider]);
            configTable.push(['Provider CLI', PROVIDER_COMMANDS[reviewerProvider]]);
            configTable.push([
                'Max Runtime',
                `${config.reviewerMaxRuntime}s (${Math.floor(config.reviewerMaxRuntime / 60)}min)`,
            ]);
            configTable.push(['Min Review Score', `${config.minReviewScore}/100`]);
            configTable.push(['Branch Patterns', config.branchPatterns.join(', ')]);
            configTable.push(['Max Retry Attempts', String(config.reviewerMaxRetries)]);
            configTable.push(['Retry Delay', `${config.reviewerRetryDelay}s`]);
            configTable.push([
                'Max PRs Per Run',
                config.reviewerMaxPrsPerRun === 0 ? 'Unlimited' : String(config.reviewerMaxPrsPerRun),
            ]);
            console.log(configTable.toString());
            // Check for open PRs needing work
            header('Open PRs Needing Work');
            const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
            if (openPrs.length === 0) {
                dim('  (no open PRs matching branch patterns)');
            }
            else {
                for (const pr of openPrs) {
                    info(`#${pr.number}: ${pr.title}`);
                    dim(`         Branch: ${pr.branch}`);
                }
            }
            // Provider invocation command
            header('Provider Invocation');
            if (reviewerProvider === 'claude') {
                dim('  claude -p "/night-watch-pr-reviewer" --dangerously-skip-permissions');
            }
            else {
                dim('  codex exec --yolo "/night-watch-pr-reviewer"');
            }
            // Environment variables
            header('Environment Variables');
            for (const [key, value] of Object.entries(envVars)) {
                dim(`  ${key}=${value}`);
            }
            // Full command that would be executed
            header('Command');
            dim(`  bash ${scriptPath} ${projectDir}`);
            console.log();
            await trackCommandCompleted('review', commandStartedAt, 0, config);
            process.exit(0);
        }
        // Preflight visibility: show currently failing checks before the fixer runs.
        const preflightOpenPrs = getOpenPrsNeedingWork(config.branchPatterns);
        const preflightFailures = preflightOpenPrs
            .map((pr) => ({
            prNumber: pr.number,
            title: pr.title,
            failingChecks: getPrFailingChecks(pr.number),
        }))
            .filter((entry) => entry.failingChecks.length > 0);
        if (preflightFailures.length > 0) {
            header('Preflight Failing Checks');
            for (const entry of preflightFailures) {
                info(`#${entry.prNumber}: ${entry.title}`);
                for (const check of entry.failingChecks) {
                    dim(`  ${check}`);
                }
            }
        }
        // Execute the script with spinner
        const spinner = createSpinner('Running PR reviewer...');
        spinner.start();
        try {
            const startedAt = Date.now();
            const provider = envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'reviewer');
            await trackJobStarted('reviewer', provider, config);
            await maybeApplyCronSchedulingDelay(config, 'reviewer', projectDir);
            const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
            const finishedAt = Date.now();
            const scriptResult = parseScriptResult(`${stdout}\n${stderr}`);
            await trackJobCompletedOrFailed('reviewer', provider, startedAt, exitCode, config, scriptResult?.status);
            if (exitCode === 0) {
                if (scriptResult?.status === 'queued') {
                    spinner.succeed('PR reviewer queued — another job is currently running');
                }
                else if (scriptResult?.status?.startsWith('skip_')) {
                    spinner.succeed('PR reviewer completed (no PRs needed review)');
                }
                else {
                    spinner.succeed('PR reviewer completed successfully');
                }
            }
            else {
                spinner.fail(`PR reviewer exited with code ${exitCode}`);
            }
            // Send notifications (fire-and-forget, failures do not affect exit code)
            if (!options.dryRun) {
                try {
                    const repository = getRepositories().sessionOutcomes;
                    const storedOutcome = repository.insertOutcome(buildSessionOutcomeInput({
                        exitCode,
                        finishedAt,
                        jobType: 'reviewer',
                        metadata: {
                            providerCommand: envVars.NW_PROVIDER_CMD,
                            providerLabel: envVars.NW_PROVIDER_LABEL,
                        },
                        minReviewScore: config.minReviewScore,
                        projectPath: projectDir,
                        providerKey: envVars.NW_PROVIDER_KEY ?? resolveJobProvider(config, 'reviewer'),
                        scriptResult,
                        startedAt,
                        stderr,
                        stdout,
                    }));
                    if (isFeedbackEnabled(config)) {
                        analyzeFeedbackOutcome(repository, storedOutcome, getFeedbackAnalysisOptions(config));
                    }
                }
                catch {
                    // Outcome persistence must not change command exit behavior.
                }
                const shouldNotifyCompletion = shouldSendReviewCompletionNotification(exitCode, scriptResult?.status);
                if (!shouldNotifyCompletion) {
                    info('Skipping review completion notification (review did not complete successfully)');
                }
                else {
                    fireTelemetryEvent('review_completed', {
                        ...buildTelemetryBaseProperties(config),
                        jobType: 'reviewer',
                        provider,
                        success: true,
                    });
                }
                // Enrich with PR details (graceful — null if gh fails)
                let fallbackPrDetails = null;
                if (shouldNotifyCompletion) {
                    const reviewedPrNumbers = parseReviewedPrNumbers(scriptResult?.data.prs);
                    const firstReviewedPrNumber = reviewedPrNumbers[0];
                    if (firstReviewedPrNumber !== undefined) {
                        fallbackPrDetails = fetchPrDetailsByNumber(firstReviewedPrNumber, projectDir);
                    }
                    if (!fallbackPrDetails) {
                        fallbackPrDetails = fetchReviewedPrDetails(config.branchPatterns, projectDir);
                    }
                }
                if (shouldNotifyCompletion) {
                    // Extract retry attempts from script result
                    const attempts = parseRetryAttempts(scriptResult?.data.attempts);
                    const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
                    const legacyNoChangesNeeded = scriptResult?.data.no_changes_needed === '1';
                    const reviewedPrNumbers = parseReviewedPrNumbers(scriptResult?.data.prs);
                    const noChangesPrNumbers = parseReviewedPrNumbers(scriptResult?.data.no_changes_prs);
                    const fallbackPrNumber = fallbackPrDetails?.number;
                    let notificationPrNumbers = reviewedPrNumbers;
                    if (notificationPrNumbers.length === 0 && fallbackPrNumber !== undefined) {
                        notificationPrNumbers = [fallbackPrNumber];
                    }
                    const notificationTargets = buildReviewNotificationTargets(notificationPrNumbers, noChangesPrNumbers, legacyNoChangesNeeded);
                    if (notificationTargets.length === 0) {
                        const reviewEvent = legacyNoChangesNeeded
                            ? 'review_ready_for_human'
                            : 'review_completed';
                        await sendNotifications(config, {
                            event: reviewEvent,
                            projectName: path.basename(projectDir),
                            exitCode,
                            provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
                            prUrl: fallbackPrDetails?.url,
                            prTitle: fallbackPrDetails?.title,
                            prBody: fallbackPrDetails?.body,
                            prNumber: fallbackPrDetails?.number,
                            filesChanged: fallbackPrDetails?.changedFiles,
                            additions: fallbackPrDetails?.additions,
                            deletions: fallbackPrDetails?.deletions,
                            attempts,
                            finalScore,
                        });
                    }
                    else {
                        for (const target of notificationTargets) {
                            const prDetails = fallbackPrDetails?.number === target.prNumber
                                ? fallbackPrDetails
                                : fetchPrDetailsByNumber(target.prNumber, projectDir);
                            const reviewEvent = target.noChangesNeeded
                                ? 'review_ready_for_human'
                                : 'review_completed';
                            await sendNotifications(config, {
                                event: reviewEvent,
                                projectName: path.basename(projectDir),
                                exitCode,
                                provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
                                prUrl: prDetails?.url,
                                prTitle: prDetails?.title,
                                prBody: prDetails?.body,
                                prNumber: prDetails?.number ?? target.prNumber,
                                filesChanged: prDetails?.changedFiles,
                                additions: prDetails?.additions,
                                deletions: prDetails?.deletions,
                                attempts,
                                finalScore,
                            });
                        }
                    }
                }
                const autoMergedPrNumbers = parseAutoMergedPrNumbers(scriptResult?.data.auto_merged);
                if (autoMergedPrNumbers.length > 0) {
                    fireTelemetryEvent('auto_merge_completed', {
                        ...buildTelemetryBaseProperties(config),
                        jobType: 'reviewer',
                        provider,
                        success: true,
                    });
                    const autoMergedPrNumber = autoMergedPrNumbers[0];
                    const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
                    const _mergeCtx = {
                        event: 'pr_auto_merged',
                        projectName: path.basename(projectDir),
                        exitCode,
                        provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
                        prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
                        prUrl: autoMergedPrDetails?.url,
                        prTitle: autoMergedPrDetails?.title,
                        prBody: autoMergedPrDetails?.body,
                        filesChanged: autoMergedPrDetails?.changedFiles,
                        additions: autoMergedPrDetails?.additions,
                        deletions: autoMergedPrDetails?.deletions,
                    };
                    await sendNotifications(config, _mergeCtx);
                }
            }
            await trackCommandCompleted('review', commandStartedAt, exitCode, config);
            process.exit(exitCode);
        }
        catch (err) {
            spinner.fail('Failed to execute review command');
            uiError(`${err instanceof Error ? err.message : String(err)}`);
            await trackCommandCompleted('review', commandStartedAt, 1, config, {
                errorCategory: err instanceof Error ? err.message : String(err),
            });
            process.exit(1);
        }
    });
}
//# sourceMappingURL=review.js.map