/**
 * commands/audit-branch-current.mjs — pre-PR rebase gate envelope for apt:ship §0.7.
 *
 * Pure read-only branch-drift audit. Given a base ref (default `main`), reports
 * how many commits HEAD is ahead/behind `origin/<base>`, captures the current
 * `origin/<base>` tip SHA (the lease target for any subsequent
 * `--force-with-lease=refs/heads/<branch>:<sha>` push), and probes whether a
 * trial merge against the remote tip would conflict.
 *
 * Single network call: `git fetch origin <base>`. Fetch failure is recovered
 * as `audit_warning: 'fetch-failed'` — the envelope still returns, just with
 * stale counts. No `git rebase`, no `git push`, no `git reset` — the SKILL.md
 * prose owns history rewrites.
 *
 * Envelope:
 *
 *   {
 *     status:             'ok',
 *     command:            'audit-branch-current',
 *     projectDir:         string,
 *     base:               string,
 *     ahead:              number,           // commits HEAD has over origin/<base>
 *     behind:             number,           // commits origin/<base> has over HEAD
 *     remote_tip_sha:     string | null,    // origin/<base> after fetch
 *     mergeable:          boolean | null,   // null when probe unavailable
 *     conflicts_preview?: string[],         // populated only when mergeable=false
 *     audit_warning?:     string            // 'fetch-failed' | 'git-version-too-old' | ...
 *   }
 *
 * Spec cross-ref: .aperant/tasks/apt-ship-pre-pr-rebase-gate-audit-branch_21-05-26/spec.md
 *
 * @internal
 */
import { execFileSync } from 'node:child_process';
import { resolve } from 'node:path';
import { err, ok } from '../util/result.mjs';
/**
 * Try-execute a git command, returning {ok, stdout, stderr, code}. Never throws.
 *
 * @param {string} cwd
 * @param {string[]} args
 */
function gitTry(cwd, args) {
    try {
        const stdout = execFileSync('git', args, {
            cwd,
            encoding: 'utf-8',
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        return { ok: true, stdout, stderr: '', code: 0 };
    }
    catch (e) {
        // execFileSync throws an error whose `stdout`, `stderr`, and `status`
        // properties carry the captured output and exit code.
        return {
            ok: false,
            stdout: typeof e?.stdout === 'string' ? e.stdout : (e?.stdout?.toString() ?? ''),
            stderr: typeof e?.stderr === 'string' ? e.stderr : (e?.stderr?.toString() ?? ''),
            code: typeof e?.status === 'number' ? e.status : 1,
        };
    }
}
/**
 * Parse `--base <ref>` (or `--base=<ref>`) from extraArgs. Defaults to 'main'.
 *
 * @param {string[]} extraArgs
 * @returns {string}
 */
function parseBaseFlag(extraArgs) {
    for (let i = 0; i < extraArgs.length; i++) {
        const a = extraArgs[i];
        if (a === '--base' && i + 1 < extraArgs.length)
            return extraArgs[i + 1];
        if (a.startsWith('--base='))
            return a.slice('--base='.length);
    }
    return 'main';
}
/**
 * Parse `git merge-tree --write-tree --no-messages HEAD origin/<base>` output
 * for conflict file paths. Modern (git ≥ 2.38) merge-tree emits, on conflict:
 *
 *   <tree-OID>
 *   <conflict report lines, each "<mode> <stage> <oid>\t<path>" for stage > 0>
 *   (NUL separator)
 *   <textual conflict messages>
 *
 * Older variants emit a `+<<<<<<< ... +======= ... +>>>>>>>` diff per file.
 * For correctness we use the stable form: extract file paths from the conflict
 * report. The report includes the SAME path multiple times (once per non-zero
 * stage), so we de-dupe.
 *
 * @param {string} stdout
 * @returns {string[]}
 */
function parseConflictPaths(stdout) {
    const paths = new Set();
    for (const rawLine of stdout.split('\n')) {
        const line = rawLine.replace(/\0/g, '');
        if (!line.trim())
            continue;
        // Match "<mode> <oid> <stage>\t<path>" — git merge-tree --write-tree
        // emits conflict report lines as "<mode> <oid> <stage>\t<path>".
        // Stage 0 means no conflict, so we only surface stage ≥ 1.
        const m = line.match(/^(\d{6})\s+([0-9a-f]{4,40})\s+(\d)\t(.+)$/);
        if (m) {
            const stage = Number.parseInt(m[3], 10);
            if (stage >= 1)
                paths.add(m[4]);
            continue;
        }
        // Fallback (older git): the textual report block prefixes paths with
        // "CONFLICT (content): Merge conflict in <path>".
        const c = line.match(/Merge conflict in (.+)$/);
        if (c)
            paths.add(c[1].trim());
    }
    return Array.from(paths).sort();
}
/**
 * Detect "git too old" from merge-tree stderr. Pre-2.38 git rejects
 * `--write-tree` outright; older still rejects merge-tree's modern arg
 * shape entirely. We treat any non-zero-with-no-output as "version too old"
 * — callers are expected to fall back gracefully.
 *
 * @param {string} stderr
 * @returns {boolean}
 */
function isGitVersionTooOld(stderr) {
    return /unknown option|did you mean|usage: git merge-tree/i.test(stderr);
}
/**
 * @param {string} projectDir
 * @param {string[]} extraArgs
 */
export function auditBranchCurrent(projectDir, extraArgs = []) {
    if (!projectDir) {
        return err('Usage: apt-tools audit branch-current <project-dir> [--base <ref>]');
    }
    const targetDir = resolve(projectDir);
    const base = parseBaseFlag(extraArgs);
    /** @type {string | undefined} */
    let auditWarning;
    /** @type {string[]} */
    const warnings = [];
    // 1. Fetch origin <base>. Tolerate failure (offline, no remote, etc.).
    const fetch = gitTry(targetDir, ['fetch', 'origin', base]);
    if (!fetch.ok) {
        auditWarning = 'fetch-failed';
        warnings.push(fetch.stderr.trim().split('\n').slice(0, 3).join(' | '));
    }
    // 2. Resolve origin/<base> tip.
    let remoteTipSha = null;
    const tip = gitTry(targetDir, ['rev-parse', `origin/${base}`]);
    if (tip.ok) {
        remoteTipSha = tip.stdout.trim();
    }
    else {
        // No origin/<base> ref at all — emit an envelope with null counts so the
        // caller can recognise the gap. The SKILL.md prose treats null counts as
        // "audit unavailable, STOP with a clear message" rather than proceeding.
        return ok({
            status: 'ok',
            command: 'audit-branch-current',
            projectDir: targetDir,
            base,
            ahead: null,
            behind: null,
            remote_tip_sha: null,
            mergeable: null,
            audit_warning: auditWarning ?? 'no-remote-base-ref',
            warnings,
        });
    }
    // 3. Counts.
    let ahead = 0;
    let behind = 0;
    const aheadOut = gitTry(targetDir, ['rev-list', '--count', `origin/${base}..HEAD`]);
    const behindOut = gitTry(targetDir, ['rev-list', '--count', `HEAD..origin/${base}`]);
    if (aheadOut.ok)
        ahead = Number.parseInt(aheadOut.stdout.trim(), 10) || 0;
    if (behindOut.ok)
        behind = Number.parseInt(behindOut.stdout.trim(), 10) || 0;
    // 4. Mergeability.
    //    - When behind === 0, the branch is current or strictly ahead — no
    //      remote changes to integrate, mergeable = true by definition.
    //    - When behind > 0 AND ahead === 0, the branch is strictly fast-forward
    //      behind — a clean fast-forward rebase is always mergeable.
    //    - When diverged (behind > 0 AND ahead > 0), probe via git merge-tree.
    let mergeable;
    /** @type {string[] | undefined} */
    let conflictsPreview;
    if (behind === 0) {
        mergeable = true;
    }
    else if (behind > 0 && ahead === 0) {
        mergeable = true;
    }
    else {
        // Diverged. Run modern merge-tree trial.
        const probe = gitTry(targetDir, [
            'merge-tree',
            '--write-tree',
            '--no-messages',
            'HEAD',
            `origin/${base}`,
        ]);
        if (probe.ok) {
            mergeable = true;
        }
        else if (probe.code === 1) {
            // Documented merge-tree exit-code semantics: 0 clean, 1 conflict,
            // other = invocation error.
            mergeable = false;
            conflictsPreview = parseConflictPaths(probe.stdout);
        }
        else if (isGitVersionTooOld(probe.stderr)) {
            mergeable = null;
            auditWarning = auditWarning ?? 'git-version-too-old';
            warnings.push(`git merge-tree --write-tree unsupported: ${probe.stderr.trim().slice(0, 200)}`);
        }
        else {
            // Unknown probe failure — treat as unknown rather than asserting
            // clean.
            mergeable = null;
            auditWarning = auditWarning ?? 'merge-tree-probe-failed';
            warnings.push(`git merge-tree probe failed (code ${probe.code}): ${probe.stderr.trim().slice(0, 200)}`);
        }
    }
    const envelope = {
        status: 'ok',
        command: 'audit-branch-current',
        projectDir: targetDir,
        base,
        ahead,
        behind,
        remote_tip_sha: remoteTipSha,
        mergeable,
    };
    if (conflictsPreview && conflictsPreview.length > 0) {
        envelope.conflicts_preview = conflictsPreview;
    }
    if (auditWarning)
        envelope.audit_warning = auditWarning;
    if (warnings.length > 0)
        envelope.warnings = warnings;
    return ok(envelope);
}
// Test-only export for unit-level parser coverage (not part of the public CLI
// contract).
export { parseConflictPaths as __parseConflictPathsForTests };
//# sourceMappingURL=audit-branch-current.mjs.map