import { execFile } from 'node:child_process';
import { existsSync, statSync } from 'node:fs';
import { isAbsolute, relative, resolve } from 'node:path';
import { ReviewDiffParser } from '../../../../core/review/index.js';
const MAX_PATCH_LENGTH = 120_000;
export class ControlPlaneWorkspaceDiffController {
    static async readChanges(workspaceRoot) {
        const git = await ControlPlaneWorkspaceDiffController.ensureGitWorkspace(workspaceRoot);
        if (!git.ok) {
            return {
                vcs: 'none',
                clean: true,
                files: [],
                error: git.error,
            };
        }
        const status = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
        if (status.exitCode !== 0) {
            return {
                vcs: 'git',
                clean: true,
                files: [],
                error: ControlPlaneWorkspaceDiffController.normalizeError(status.stderr) ?? 'Unable to read git status.',
            };
        }
        const entries = ControlPlaneWorkspaceDiffController.parsePorcelainStatus(status.stdout);
        const stats = await ControlPlaneWorkspaceDiffController.readDiffStats(workspaceRoot);
        const files = entries.filter((entry) => !ControlPlaneWorkspaceDiffController.isHeddleStatePath(entry.path)).map((entry) => {
            const stat = stats.get(entry.path);
            return {
                path: entry.path,
                oldPath: entry.oldPath,
                status: ControlPlaneWorkspaceDiffController.statusFromPorcelain(entry),
                indexStatus: ControlPlaneWorkspaceDiffController.normalizeStatusCode(entry.indexStatus),
                workingTreeStatus: ControlPlaneWorkspaceDiffController.normalizeStatusCode(entry.workingTreeStatus),
                additions: stat?.additions,
                deletions: stat?.deletions,
                binary: stat?.binary,
            };
        });
        return {
            vcs: 'git',
            clean: files.length === 0,
            files,
        };
    }
    static async readFileDiff(workspaceRoot, path) {
        const git = await ControlPlaneWorkspaceDiffController.ensureGitWorkspace(workspaceRoot);
        const scopedPath = ControlPlaneWorkspaceDiffController.resolveWorkspacePath(workspaceRoot, path);
        if (!scopedPath.ok) {
            return {
                vcs: git.ok ? 'git' : 'none',
                path,
                error: scopedPath.error,
            };
        }
        if (ControlPlaneWorkspaceDiffController.isHeddleStatePath(scopedPath.path)) {
            return {
                vcs: git.ok ? 'git' : 'none',
                path: scopedPath.path,
                error: 'Heddle runtime state is not included in workspace review.',
            };
        }
        if (!git.ok) {
            return {
                vcs: 'none',
                path: scopedPath.path,
                error: git.error,
            };
        }
        const unstaged = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, ['diff', '--patch', '--', scopedPath.path]);
        const staged = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, ['diff', '--cached', '--patch', '--', scopedPath.path]);
        const patches = [unstaged.stdout, staged.stdout].filter((patch) => patch.trim().length > 0);
        if (patches.length === 0 && ControlPlaneWorkspaceDiffController.isReadableUntrackedFile(workspaceRoot, scopedPath.path)) {
            const absolutePath = resolve(workspaceRoot, scopedPath.path);
            const untracked = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, ['diff', '--no-index', '--', '/dev/null', absolutePath]);
            if (untracked.stdout.trim()) {
                patches.push(ControlPlaneWorkspaceDiffController.rewriteNoIndexPatchPaths(untracked.stdout, scopedPath.path));
            }
        }
        const patch = patches.join('\n').trim();
        const truncated = patch ? ControlPlaneWorkspaceDiffController.truncatePatch(patch) : undefined;
        const parsedFiles = patch ? ReviewDiffParser.parseUnifiedDiffFiles(patch) : [];
        const parsedFile = parsedFiles.find((file) => file.path === scopedPath.path || file.oldPath === scopedPath.path);
        return {
            vcs: 'git',
            path: scopedPath.path,
            patch: truncated?.patch,
            diff: truncated?.truncated ? undefined : parsedFile,
            truncated: truncated?.truncated,
            binary: parsedFile?.binary ?? /Binary files .* differ/.test(patch),
        };
    }
    static isHeddleStatePath(path) {
        return path === '.heddle' || path.startsWith('.heddle/');
    }
    static async ensureGitWorkspace(workspaceRoot) {
        const result = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, ['rev-parse', '--is-inside-work-tree']);
        if (result.exitCode !== 0 || result.stdout.trim() !== 'true') {
            return {
                ok: false,
                error: 'Not a git workspace.',
            };
        }
        return { ok: true };
    }
    static async readDiffStats(workspaceRoot) {
        const stats = new Map();
        for (const args of [
            ['diff', '--numstat', '--'],
            ['diff', '--cached', '--numstat', '--'],
        ]) {
            const result = await ControlPlaneWorkspaceDiffController.runGit(workspaceRoot, args);
            if (result.exitCode !== 0) {
                continue;
            }
            for (const line of result.stdout.split('\n')) {
                const [additionsRaw, deletionsRaw, path] = line.split('\t');
                if (!additionsRaw || !deletionsRaw || !path) {
                    continue;
                }
                const existing = stats.get(path) ?? {};
                if (additionsRaw === '-' || deletionsRaw === '-') {
                    stats.set(path, { ...existing, binary: true });
                    continue;
                }
                stats.set(path, {
                    additions: (existing.additions ?? 0) + Number(additionsRaw),
                    deletions: (existing.deletions ?? 0) + Number(deletionsRaw),
                    binary: existing.binary,
                });
            }
        }
        return stats;
    }
    static parsePorcelainStatus(stdout) {
        const parts = stdout.split('\0').filter(Boolean);
        const entries = [];
        for (let index = 0; index < parts.length; index += 1) {
            const raw = parts[index] ?? '';
            if (raw.length < 4) {
                continue;
            }
            const indexStatus = raw[0] ?? ' ';
            const workingTreeStatus = raw[1] ?? ' ';
            const path = raw.slice(3);
            if (!path) {
                continue;
            }
            if ((indexStatus === 'R' || indexStatus === 'C') && parts[index + 1]) {
                entries.push({
                    path,
                    oldPath: parts[index + 1],
                    indexStatus,
                    workingTreeStatus,
                });
                index += 1;
                continue;
            }
            entries.push({
                path,
                indexStatus,
                workingTreeStatus,
            });
        }
        return entries;
    }
    static statusFromPorcelain(entry) {
        if (entry.indexStatus === '?' && entry.workingTreeStatus === '?') {
            return 'untracked';
        }
        if (entry.indexStatus === 'R') {
            return 'renamed';
        }
        if (entry.indexStatus === 'C') {
            return 'copied';
        }
        if (entry.indexStatus === 'D' || entry.workingTreeStatus === 'D') {
            return 'deleted';
        }
        if (entry.indexStatus === 'A' || entry.workingTreeStatus === 'A') {
            return 'added';
        }
        if (entry.indexStatus === 'M' || entry.workingTreeStatus === 'M') {
            return 'modified';
        }
        return 'unknown';
    }
    static normalizeStatusCode(status) {
        return status.trim() ? status : undefined;
    }
    static resolveWorkspacePath(workspaceRoot, path) {
        const absolutePath = resolve(workspaceRoot, path);
        const relativePath = relative(workspaceRoot, absolutePath);
        if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath)) {
            return {
                ok: false,
                error: 'Path is outside the active workspace.',
            };
        }
        return {
            ok: true,
            path: relativePath,
        };
    }
    static isReadableUntrackedFile(workspaceRoot, path) {
        const absolutePath = resolve(workspaceRoot, path);
        if (!existsSync(absolutePath)) {
            return false;
        }
        try {
            return statSync(absolutePath).isFile();
        }
        catch {
            return false;
        }
    }
    static rewriteNoIndexPatchPaths(patch, path) {
        return patch
            .split('\n')
            .map((line) => {
            if (line.startsWith('+++ ')) {
                return `+++ b/${path}`;
            }
            return line;
        })
            .join('\n');
    }
    static truncatePatch(patch) {
        if (patch.length <= MAX_PATCH_LENGTH) {
            return { patch, truncated: false };
        }
        return {
            patch: `${patch.slice(0, MAX_PATCH_LENGTH - 1)}…`,
            truncated: true,
        };
    }
    static normalizeError(stderr) {
        const trimmed = stderr.trim();
        return trimmed || undefined;
    }
    static runGit(cwd, args) {
        return new Promise((resolveResult) => {
            execFile('git', args, {
                cwd,
                maxBuffer: 10 * 1024 * 1024,
            }, (error, stdout, stderr) => {
                const code = typeof error?.code === 'number'
                    ? error.code
                    : error ? 1 : 0;
                resolveResult({
                    exitCode: code,
                    stdout: stdout.toString(),
                    stderr: stderr.toString(),
                });
            });
        });
    }
}
//# sourceMappingURL=workspace-diff.js.map