import * as path from "path";
import { spawnSync } from "child_process";

/**
 * Parses the output of `git diff --unified=0` and returns a Map from absolute
 * file path to a Set of 0-based line indices that were added or modified.
 *
 * Only added/modified lines are collected — pure deletions have no line in the
 * new file and cannot be matched against diagnostics.
 *
 * @param {string} diffOutput  Raw stdout from `git diff --unified=0`
 * @param {string} repoRoot    Absolute path to the git repository root
 * @returns {Map<string, Set<number>>}
 */
function parseGitDiff(diffOutput, repoRoot) {
    const result = new Map();
    let currentSet = null;

    for (const line of diffOutput.split("\n")) {
        // "+++ b/path/to/file" — identifies the new-file path for subsequent hunks
        if (line.startsWith("+++ b/")) {
            const relPath = line.slice(6).trimEnd(); // strip "+++ b/" and any trailing \r (CRLF git output)
            const absPath = path.join(repoRoot, relPath);
            if (!result.has(absPath)) {
                result.set(absPath, new Set());
            }
            currentSet = result.get(absPath);
            continue;
        }

        // "@@ -old_start[,old_count] +new_start[,new_count] @@"
        if (line.startsWith("@@") && currentSet !== null) {
            const match = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
            if (match) {
                const newStart = parseInt(match[1], 10);
                const newCount = match[2] !== undefined ? parseInt(match[2], 10) : 1;
                // newCount === 0 means a pure deletion — no lines added to new file
                for (let i = 0; i < newCount; i++) {
                    currentSet.add(newStart + i - 1); // convert from 1-based to 0-based
                }
            }
        }
    }

    return result;
}

/**
 * Runs `git diff --unified=0 <baseBranch>...HEAD` from within `workDir` and
 * returns a Map from absolute file path to changed 0-based line indices.
 *
 * Throws an Error if git is unavailable, `workDir` is not inside a git repo,
 * or the base branch ref cannot be resolved.
 *
 * @param {string} workDir    Any directory within the git repository
 * @param {string} baseBranch Git ref to diff against (e.g. "main", "origin/main")
 * @returns {Map<string, Set<number>>}
 */
function buildChangedLinesMap(workDir, baseBranch) {
    const GIT_TIMEOUT_MS = 30_000;

    const rootResult = spawnSync("git", ["rev-parse", "--show-toplevel"], {
        cwd: workDir,
        encoding: "utf8",
        timeout: GIT_TIMEOUT_MS
    });
    if (rootResult.signal === "SIGTERM" || rootResult.status === null) {
        throw new Error("--diff-only: git rev-parse timed out");
    }
    if (rootResult.status !== 0) {
        const msg = (rootResult.stderr || "").trim() || "git rev-parse failed";
        throw new Error(`--diff-only requires a git repository: ${msg}`);
    }
    const repoRoot = rootResult.stdout.trim();

    const diffResult = spawnSync(
        "git",
        ["diff", "--unified=0", `${baseBranch}...HEAD`],
        { cwd: workDir, encoding: "utf8", timeout: GIT_TIMEOUT_MS }
    );
    if (diffResult.signal === "SIGTERM" || diffResult.status === null) {
        throw new Error("--diff-only: git diff timed out");
    }
    if (diffResult.status !== 0) {
        const msg = (diffResult.stderr || "").trim() || "git diff failed";
        throw new Error(`--diff-only: ${msg}`);
    }

    return parseGitDiff(diffResult.stdout, repoRoot);
}

export { parseGitDiff, buildChangedLinesMap };
