import { writeFile } from 'node:fs/promises';
import { Option } from 'commander';
import { z } from 'zod';
import { writeStream } from '../utils.js';
import { getGitChangedFiles, getGitChurnSummary, getGitCochange, getGitCommitHistory, getGitDiffPatch, getGitDiffSummary, getGitFileHistory, } from './api.js';
import { formatChangedFiles, formatChurnSummary, formatCochange, formatCommitHistory, formatDiffPatch, formatDiffSummary, formatFileHistory, formatGitJson, } from './output.js';
import { captureGitRepository } from './capture.js';
import { GitRangeModeSchema } from './types.js';
const GitCommandOptionsSchema = z
    .object({
    path: z.string().optional(),
    base: z.string().optional(),
    head: z.string().optional(),
    staged: z.boolean().optional(),
    rangeMode: GitRangeModeSchema.optional(),
    format: z.string().optional(),
    output: z.string().optional(),
    limit: z.string().optional(),
    since: z.string().optional(),
    patch: z.boolean().optional(),
    mapTokens: z.string().optional(),
    tokenizer: z.string().optional(),
    sort: z.string().optional(),
    follow: z.boolean().optional(),
    lineStart: z.string().optional(),
    lineEnd: z.string().optional(),
    target: z.string().optional(),
    maxCommitFiles: z.string().optional(),
    scope: z.string().optional(),
    ref: z.string().optional(),
    maxCommits: z.string().optional(),
    maxChanges: z.string().optional(),
    maxTreeEntries: z.string().optional(),
    batchSize: z.string().optional(),
    maxRecordBytes: z.string().optional(),
    timeoutMs: z.string().optional(),
    requireComplete: z.boolean().optional(),
    pretty: z.boolean().optional(),
})
    .passthrough();
function getOptions(command) {
    return GitCommandOptionsSchema.parse(command.optsWithGlobals());
}
async function emit(output, options) {
    const text = output.endsWith('\n') ? output : `${output}\n`;
    if (options.output) {
        await writeFile(options.output, text);
        return;
    }
    await writeStream(process.stdout, text);
}
function addSharedGitOptions(command) {
    command
        .option('--path <repo-root>', 'repository root', process.cwd())
        .option('--base <ref>', 'base ref for the diff range')
        .option('--head <ref>', 'head ref for the diff range')
        .option('--staged', 'inspect staged changes')
        .addOption(new Option('--range-mode <mode>', 'compare an explicit --base/--head by merge base or exactly')
        .choices(['merge-base', 'exact'])
        .default('merge-base'))
        .addOption(new Option('--format <type>', 'output format').choices(['tabular', 'json']).default('tabular'))
        .option('-o, --output <file>', 'write output to file');
}
function commonApiOptions(options) {
    return {
        repoRoot: options.path,
        base: options.base,
        head: options.head,
        staged: options.staged,
        rangeMode: options.rangeMode,
    };
}
function parsePositiveIntOption(value, fallback, label) {
    if (value == null || value === '')
        return fallback;
    const parsed = Number.parseInt(value, 10);
    if (!Number.isFinite(parsed) || parsed <= 0) {
        throw new Error(`Invalid ${label}: expected a positive integer.`);
    }
    return parsed;
}
// The root scan defines a global `--sort` option that is inherited by every subcommand and
// wins option parsing, so `git churn` cannot rely on commander to validate its own keys. We
// resolve the value here instead, defaulting to `churn` for anything that is not a churn key
// (including the scan's default of `files`).
function resolveChurnSort(value) {
    switch (value) {
        case 'commits':
        case 'added':
        case 'deleted':
        case 'net':
        case 'churn':
            return value;
        default:
            return 'churn';
    }
}
function historyScopeOption() {
    return new Option('--scope <scope>', "aggregate over the resolved diff range or full HEAD history (pair 'history' with --since)")
        .choices(['range', 'history'])
        .default('range');
}
function resolveHistoryScope(options) {
    if (options.scope !== 'history')
        return 'range';
    if (options.base != null || options.head != null || options.staged) {
        throw new Error('--scope history conflicts with --base/--head/--staged: history aggregates over full HEAD history (bound it with --since).');
    }
    return 'history';
}
export function registerGitCommands(program) {
    const git = program.command('git').description('Inspect git changes, history, and churn');
    const capture = git
        .command('capture')
        .description('Stream versioned physical Git repository evidence as NDJSON')
        .requiredOption('--path <repo-root>', 'repository root')
        .requiredOption('--ref <ref>', 'Git ref to capture')
        .option('--max-commits <n>', 'maximum reachable commits to capture')
        .option('--max-changes <n>', 'maximum physical changes to capture')
        .option('--max-tree-entries <n>', 'maximum pinned-tree entries to capture')
        .option('--batch-size <n>', 'commits processed by each bounded git show invocation')
        .option('--max-record-bytes <n>', 'maximum bytes accepted in one Git output record')
        .option('--timeout-ms <n>', 'timeout for each Git subprocess')
        .option('--require-complete', 'exit nonzero when the footer reports incomplete coverage');
    capture.action(async (_options, command) => {
        try {
            const options = getOptions(command);
            const maxCommits = options.maxCommits ? parsePositiveIntOption(options.maxCommits, 0, 'max commits') : undefined;
            let complete = false;
            for await (const event of captureGitRepository({
                repoPath: options.path ?? process.cwd(),
                ref: options.ref ?? 'HEAD',
                maxCommits,
                maxChanges: options.maxChanges ? parsePositiveIntOption(options.maxChanges, 0, 'max changes') : undefined,
                maxTreeEntries: options.maxTreeEntries
                    ? parsePositiveIntOption(options.maxTreeEntries, 0, 'max tree entries')
                    : undefined,
                batchSize: options.batchSize ? parsePositiveIntOption(options.batchSize, 0, 'batch size') : undefined,
                maxRecordBytes: options.maxRecordBytes
                    ? parsePositiveIntOption(options.maxRecordBytes, 0, 'max record bytes')
                    : undefined,
                timeoutMs: options.timeoutMs ? parsePositiveIntOption(options.timeoutMs, 0, 'timeout') : undefined,
            })) {
                await writeStream(process.stdout, `${JSON.stringify(event)}\n`);
                if (event.type === 'footer')
                    complete = event.complete;
            }
            if (options.requireComplete && !complete)
                process.exitCode = 1;
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            await writeStream(process.stderr, `Error: ${message}\n`);
            process.exitCode = 1;
        }
    });
    const changed = git.command('changed').description('List changed files for the resolved diff range');
    addSharedGitOptions(changed);
    changed.action(async (_options, command) => {
        const options = getOptions(command);
        const files = await getGitChangedFiles(commonApiOptions(options));
        const output = options.format === 'json' ? formatGitJson({ files }, options.pretty) : formatChangedFiles(files);
        await emit(output, options);
    });
    const diff = git.command('diff').description('Summarize or render the resolved diff');
    addSharedGitOptions(diff);
    diff.option('--patch', 'emit the raw patch instead of a summary');
    diff.option('--map-tokens <n>', 'token budget for --patch');
    diff.addOption(new Option('--tokenizer <name>', 'token counter for --map-tokens').choices([
        'heuristic',
        'o200k_base',
        'cl100k_base',
    ]));
    diff.action(async (_options, command) => {
        const options = getOptions(command);
        if (options.patch) {
            const result = await getGitDiffPatch({
                ...commonApiOptions(options),
                tokenBudget: options.mapTokens ? parsePositiveIntOption(options.mapTokens, 0, 'map tokens') : undefined,
                tokenizer: options.tokenizer,
            });
            const output = options.format === 'json' ? formatGitJson(result, options.pretty) : formatDiffPatch(result);
            await emit(output, options);
            return;
        }
        const summary = await getGitDiffSummary(commonApiOptions(options));
        const output = options.format === 'json' ? formatGitJson(summary, options.pretty) : formatDiffSummary(summary);
        await emit(output, options);
    });
    const history = git
        .command('history')
        .argument('[file]', 'optional file path to filter history')
        .description('Show commit history for a range or file');
    addSharedGitOptions(history);
    history.option('--limit <n>', 'maximum commits to return', '50');
    history.option('--since <date>', 'only include commits after this date');
    history.option('--follow', 'follow a single file across renames');
    history.option('--line-start <n>', 'start line for git log -L file history');
    history.option('--line-end <n>', 'end line for git log -L file history');
    history.addOption(historyScopeOption());
    history.action(async (file, _options, command) => {
        const options = getOptions(command);
        const limit = parsePositiveIntOption(options.limit, 50, 'limit');
        if (file) {
            const hasLineRange = options.lineStart != null || options.lineEnd != null;
            const result = await getGitFileHistory({
                ...commonApiOptions(options),
                path: file,
                follow: options.follow,
                since: options.since,
                limit,
                lineRange: hasLineRange
                    ? {
                        start: parsePositiveIntOption(options.lineStart, 1, 'line start'),
                        end: parsePositiveIntOption(options.lineEnd, parsePositiveIntOption(options.lineStart, 1, 'line end'), 'line end'),
                    }
                    : undefined,
            });
            const output = options.format === 'json' ? formatGitJson(result, options.pretty) : formatFileHistory(result);
            await emit(output, options);
            return;
        }
        const result = await getGitCommitHistory({
            ...commonApiOptions(options),
            since: options.since,
            limit,
            scope: resolveHistoryScope(options),
        });
        const output = options.format === 'json' ? formatGitJson(result, options.pretty) : formatCommitHistory(result);
        await emit(output, options);
    });
    const churn = git.command('churn').description('Rank changed files by churn');
    addSharedGitOptions(churn);
    churn.option('--limit <n>', 'maximum files to return');
    churn.option('--since <date>', 'only include commits after this date');
    // Declared without .choices() because the inherited global `--sort` (from the default scan)
    // wins parsing; resolveChurnSort validates the value in the action instead.
    churn.option('--sort <key>', 'sort by: churn, commits, added, deleted, or net (default: churn)');
    churn.addOption(historyScopeOption());
    churn.action(async (_options, command) => {
        const options = getOptions(command);
        const result = await getGitChurnSummary({
            ...commonApiOptions(options),
            since: options.since,
            limit: options.limit ? parsePositiveIntOption(options.limit, 0, 'limit') : undefined,
            sort: resolveChurnSort(options.sort),
            scope: resolveHistoryScope(options),
        });
        const output = options.format === 'json' ? formatGitJson(result, options.pretty) : formatChurnSummary(result);
        await emit(output, options);
    });
    const cochange = git.command('cochange').description('Rank files that historically change together');
    addSharedGitOptions(cochange);
    cochange.option('--limit <n>', 'maximum pairs to return');
    cochange.option('--since <date>', 'only include commits after this date');
    cochange.option('--target <path>', 'only include pairs involving this path');
    cochange.option('--max-commit-files <n>', 'skip commits touching more than this many files', '50');
    cochange.addOption(historyScopeOption());
    cochange.action(async (_options, command) => {
        const options = getOptions(command);
        const result = await getGitCochange({
            ...commonApiOptions(options),
            since: options.since,
            limit: options.limit ? parsePositiveIntOption(options.limit, 0, 'limit') : undefined,
            target: options.target,
            maxCommitFiles: parsePositiveIntOption(options.maxCommitFiles, 50, 'max commit files'),
            scope: resolveHistoryScope(options),
        });
        const output = options.format === 'json' ? formatGitJson(result, options.pretty) : formatCochange(result);
        await emit(output, options);
    });
}
//# sourceMappingURL=command.js.map