/**
 * Diff-aware context module — prioritizes recently-changed files when building
 * context for the AI agent. Uses git diff/status to discover changes, then
 * provides ranking utilities for context-window budget allocation.
 *
 * Standalone module: only imports node:child_process and node:path.
 */
import { execFileSync, execFile } from "node:child_process";
import { resolve } from "node:path";
import { NerfError, NERF_PARSE_ERROR } from "./errors.js";
// ─── Helpers ────────────────────────────────────────────────────────────────
const EXEC_OPTS = {
    encoding: "utf-8",
    timeout: 5000,
    stdio: ["pipe", "pipe", "pipe"],
};
/** Synchronous git — used where sequential control flow is required (fallback paths). */
function gitSync(args, cwd) {
    return execFileSync("git", args, { ...EXEC_OPTS, cwd }).trim();
}
/** Async git — returns stdout on success, empty string on any error. */
function gitAsync(args, cwd) {
    return new Promise((resolve) => {
        execFile("git", args, { encoding: "utf-8", timeout: 5000, cwd }, (err, stdout) => resolve(err ? "" : stdout.trim()));
    });
}
function splitLines(output) {
    return output
        .split("\n")
        .map((l) => l.trim())
        .filter(Boolean);
}
// ─── Public API ─────────────────────────────────────────────────────────────
/**
 * Returns recently-changed file paths, deduplicated and sorted by recency
 * (most recent first). Combines committed diffs, staged changes, and
 * unstaged/untracked files from `git status`.
 *
 * The three git queries (log, diff --cached, status) run concurrently.
 */
export async function getRecentlyChangedFiles(cwd, options) {
    const maxFiles = options?.maxFiles ?? 20;
    const sinceDays = options?.sinceDays ?? 7;
    const dir = resolve(cwd);
    try {
        const days = Math.max(1, Math.floor(Number(sinceDays)));
        if (!Number.isFinite(days))
            throw new NerfError(NERF_PARSE_ERROR, "invalid sinceDays");
        // Run all three queries concurrently — they read independent git state
        const [logRaw, stagedRaw, statusRaw] = await Promise.all([
            // 1. Committed changes since N days ago (fallback to HEAD~10 on error)
            gitAsync(["log", "--diff-filter=ACMR", "--name-only", "--pretty=format:", `--since=${days} days ago`], dir)
                .then((out) => out || gitAsync(["diff", "--name-only", "HEAD~10"], dir)),
            // 2. Staged changes
            gitAsync(["diff", "--cached", "--name-only"], dir),
            // 3. Unstaged / untracked
            gitAsync(["status", "--porcelain"], dir),
        ]);
        const committedFiles = splitLines(logRaw);
        const stagedFiles = splitLines(stagedRaw);
        const statusFiles = splitLines(statusRaw).map((line) => line.slice(3)); // strip XY + space
        // Deduplicate, preserving insertion order (most-recent-first: status → staged → committed)
        const seen = new Set();
        const result = [];
        for (const file of [...statusFiles, ...stagedFiles, ...committedFiles]) {
            if (!seen.has(file)) {
                seen.add(file);
                result.push(file);
            }
        }
        return result.slice(0, maxFiles);
    }
    catch {
        // Non-git directory or git unavailable — graceful fallback
        return [];
    }
}
/**
 * Returns richer change metadata: change type and approximate line counts.
 *
 * The three git queries (diff --cached --numstat, diff --numstat, status --porcelain)
 * run concurrently — they read independent git state.
 */
export async function getChangedFilesWithContext(cwd) {
    const dir = resolve(cwd);
    try {
        // Run all three queries concurrently
        const [cachedNumstat, unstagedNumstat, statusRaw] = await Promise.all([
            gitAsync(["diff", "--cached", "--numstat"], dir),
            gitAsync(["diff", "--numstat"], dir),
            gitAsync(["status", "--porcelain"], dir),
        ]);
        const result = [];
        const seen = new Set();
        const add = (info) => {
            if (!seen.has(info.path)) {
                seen.add(info.path);
                result.push(info);
            }
        };
        // 1. Staged files with numstat
        for (const line of splitLines(cachedNumstat)) {
            const [added, deleted, filePath] = line.split("\t");
            if (!filePath)
                continue;
            const lines = added === "-" || deleted === "-"
                ? undefined
                : Number(added) + Number(deleted);
            add({ path: filePath, changeType: "staged", linesChanged: lines });
        }
        // 2. Unstaged modifications with numstat
        for (const line of splitLines(unstagedNumstat)) {
            const [added, deleted, filePath] = line.split("\t");
            if (!filePath)
                continue;
            const lines = added === "-" || deleted === "-"
                ? undefined
                : Number(added) + Number(deleted);
            add({ path: filePath, changeType: "modified", linesChanged: lines });
        }
        // 3. Untracked / deleted from porcelain status
        for (const line of splitLines(statusRaw)) {
            const code = line.slice(0, 2);
            const filePath = line.slice(3);
            if (seen.has(filePath))
                continue;
            if (code.includes("?")) {
                add({ path: filePath, changeType: "added" });
            }
            else if (code.includes("D")) {
                add({ path: filePath, changeType: "deleted" });
            }
            else if (code.includes("A")) {
                add({ path: filePath, changeType: "added" });
            }
            else {
                add({ path: filePath, changeType: "modified" });
            }
        }
        return result;
    }
    catch {
        return [];
    }
}
/**
 * Ranks a file list so that recently-changed files appear first.
 * Files present in `changedFiles` are placed at the front (in their
 * original changedFiles order), followed by unchanged files in their
 * original order.
 */
export function rankFilesByRelevance(files, changedFiles) {
    const changedSet = new Set(changedFiles);
    const changed = [];
    const rest = [];
    for (const f of files) {
        if (changedSet.has(f)) {
            changed.push(f);
        }
        else {
            rest.push(f);
        }
    }
    // Maintain changedFiles priority order within the changed group
    const changedOrder = new Map(changedFiles.map((f, i) => [f, i]));
    changed.sort((a, b) => (changedOrder.get(a) ?? 0) - (changedOrder.get(b) ?? 0));
    return [...changed, ...rest];
}
