import { execFile } from "node:child_process";
import { readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { createGitProcessEnv } from "../core/git-process-env.js";
const execFileAsync = promisify(execFile);
const GIT_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
const WORKSPACE_CHANGES_CACHE_MAX_ENTRIES = 128;
const workspaceChangesCacheByRepoRoot = new Map();
function mapNameStatus(code) {
    const kind = code.charAt(0);
    if (kind === "M")
        return "modified";
    if (kind === "A")
        return "added";
    if (kind === "D")
        return "deleted";
    if (kind === "R")
        return "renamed";
    if (kind === "C")
        return "copied";
    return "unknown";
}
function toLineCount(text) {
    if (!text) {
        return 0;
    }
    return text.split("\n").length;
}
function parseTrackedChanges(output) {
    const entries = [];
    const lines = output
        .split("\n")
        .map((line) => line.trim())
        .filter(Boolean);
    for (const line of lines) {
        const parts = line.split("\t");
        const statusCode = parts[0];
        const status = mapNameStatus(statusCode);
        if ((status === "renamed" || status === "copied") && parts.length >= 3) {
            const previousPath = parts[1];
            const path = parts[2];
            if (path) {
                entries.push({
                    path,
                    previousPath: previousPath || undefined,
                    status,
                });
            }
            continue;
        }
        const path = parts[1];
        if (path) {
            entries.push({
                path,
                status,
            });
        }
    }
    return entries;
}
async function runGit(args, cwd) {
    try {
        const { stdout } = await execFileAsync("git", args, {
            cwd,
            encoding: "utf8",
            maxBuffer: GIT_MAX_BUFFER_BYTES,
            env: createGitProcessEnv(),
        });
        return String(stdout);
    }
    catch (error) {
        const message = typeof error === "object" && error !== null && "stderr" in error
            ? String(error.stderr ?? "").trim()
            : "";
        if (message) {
            throw new Error(message);
        }
        throw error;
    }
}
async function buildFileFingerprints(repoRoot, paths) {
    if (paths.length === 0) {
        return [];
    }
    const uniqueSortedPaths = Array.from(new Set(paths)).sort((left, right) => left.localeCompare(right));
    const entries = await Promise.all(uniqueSortedPaths.map(async (path) => {
        const absolutePath = join(repoRoot, path);
        try {
            const fileStat = await stat(absolutePath);
            return {
                path,
                size: fileStat.size,
                mtimeMs: fileStat.mtimeMs,
                ctimeMs: fileStat.ctimeMs,
            };
        }
        catch {
            return {
                path,
                size: null,
                mtimeMs: null,
                ctimeMs: null,
            };
        }
    }));
    return entries;
}
function buildWorkspaceChangesStateKey(input) {
    const fingerprintsToken = input.fingerprints
        .map((entry) => `${entry.path}\t${entry.size ?? "null"}\t${entry.mtimeMs ?? "null"}\t${entry.ctimeMs ?? "null"}`)
        .join("\n");
    return [
        input.repoRoot,
        input.headCommit ?? "no-head",
        input.trackedChangesOutput,
        input.untrackedOutput,
        fingerprintsToken,
    ].join("\n--\n");
}
function pruneWorkspaceChangesCache() {
    if (workspaceChangesCacheByRepoRoot.size <= WORKSPACE_CHANGES_CACHE_MAX_ENTRIES) {
        return;
    }
    const entries = Array.from(workspaceChangesCacheByRepoRoot.entries()).sort((left, right) => left[1].lastAccessedAt - right[1].lastAccessedAt);
    const removeCount = entries.length - WORKSPACE_CHANGES_CACHE_MAX_ENTRIES;
    for (let index = 0; index < removeCount; index += 1) {
        const candidate = entries[index];
        if (!candidate) {
            break;
        }
        workspaceChangesCacheByRepoRoot.delete(candidate[0]);
    }
}
async function readHeadFile(repoRoot, path) {
    try {
        return await runGit(["show", `HEAD:${path}`], repoRoot);
    }
    catch {
        return null;
    }
}
async function readWorkingTreeFile(repoRoot, path) {
    try {
        return await readFile(join(repoRoot, path), "utf8");
    }
    catch {
        return null;
    }
}
function fallbackStats(oldText, newText) {
    if (oldText == null && newText == null) {
        return { additions: 0, deletions: 0 };
    }
    if (oldText == null) {
        return { additions: toLineCount(newText ?? ""), deletions: 0 };
    }
    if (newText == null) {
        return { additions: 0, deletions: toLineCount(oldText) };
    }
    const oldLines = toLineCount(oldText);
    const newLines = toLineCount(newText);
    return {
        additions: Math.max(newLines - oldLines, 0),
        deletions: Math.max(oldLines - newLines, 0),
    };
}
async function readDiffStat(repoRoot, path) {
    try {
        const output = await runGit(["diff", "--numstat", "HEAD", "--", path], repoRoot);
        const firstLine = output
            .split("\n")
            .map((line) => line.trim())
            .find(Boolean);
        if (!firstLine) {
            return null;
        }
        const [addedRaw, deletedRaw] = firstLine.split("\t");
        const additions = Number.parseInt(addedRaw ?? "", 10);
        const deletions = Number.parseInt(deletedRaw ?? "", 10);
        return {
            additions: Number.isFinite(additions) ? additions : 0,
            deletions: Number.isFinite(deletions) ? deletions : 0,
        };
    }
    catch {
        return null;
    }
}
async function buildFileChange(repoRoot, entry) {
    const basePath = entry.previousPath ?? entry.path;
    const oldText = entry.status === "added" || entry.status === "untracked" ? null : await readHeadFile(repoRoot, basePath);
    const newText = entry.status === "deleted" ? null : await readWorkingTreeFile(repoRoot, entry.path);
    const stats = entry.status === "untracked"
        ? { additions: toLineCount(newText ?? ""), deletions: 0 }
        : ((await readDiffStat(repoRoot, entry.path)) ?? fallbackStats(oldText, newText));
    return {
        path: entry.path,
        previousPath: entry.previousPath,
        status: entry.status,
        additions: stats.additions,
        deletions: stats.deletions,
        oldText,
        newText,
    };
}
export async function getWorkspaceChanges(cwd) {
    const repoRoot = (await runGit(["rev-parse", "--show-toplevel"], cwd)).trim();
    if (!repoRoot) {
        throw new Error("Could not resolve git repository root.");
    }
    const [trackedChangesOutput, untrackedOutput, headCommitOutput] = await Promise.all([
        runGit(["diff", "--name-status", "HEAD", "--"], repoRoot),
        runGit(["ls-files", "--others", "--exclude-standard"], repoRoot),
        runGit(["rev-parse", "--verify", "HEAD"], repoRoot).catch(() => ""),
    ]);
    const trackedChanges = parseTrackedChanges(trackedChangesOutput);
    const untrackedPaths = untrackedOutput
        .split("\n")
        .map((line) => line.trim())
        .filter(Boolean);
    const trackedPaths = new Set(trackedChanges.map((entry) => entry.path));
    const allChanges = [
        ...trackedChanges,
        ...untrackedPaths
            .filter((path) => !trackedPaths.has(path))
            .map((path) => ({
            path,
            status: "untracked",
        })),
    ];
    const fingerprintPaths = allChanges.flatMap((entry) => [entry.path, entry.previousPath].filter(Boolean));
    const fingerprints = await buildFileFingerprints(repoRoot, fingerprintPaths);
    const stateKey = buildWorkspaceChangesStateKey({
        repoRoot,
        headCommit: headCommitOutput.trim() || null,
        trackedChangesOutput,
        untrackedOutput,
        fingerprints,
    });
    const existing = workspaceChangesCacheByRepoRoot.get(repoRoot);
    if (existing && existing.stateKey === stateKey) {
        existing.lastAccessedAt = Date.now();
        return existing.response;
    }
    const files = await Promise.all(allChanges.map((entry) => buildFileChange(repoRoot, entry)));
    files.sort((left, right) => left.path.localeCompare(right.path));
    const response = {
        repoRoot,
        generatedAt: Date.now(),
        files,
    };
    workspaceChangesCacheByRepoRoot.set(repoRoot, {
        stateKey,
        response,
        lastAccessedAt: Date.now(),
    });
    pruneWorkspaceChangesCache();
    return response;
}
//# sourceMappingURL=get-workspace-changes.js.map