import { execFile } from 'node:child_process';
import { join } from 'node:path';
async function git(cwd, args, options = {}) {
    const timeout = options.timeoutMs ?? 20_000;
    const acceptedExitCodes = options.acceptedExitCodes ?? [0];
    return new Promise((resolve, reject) => {
        execFile('git', args, {
            cwd,
            timeout,
            maxBuffer: 20 * 1024 * 1024,
            encoding: 'buffer'
        }, (error, stdout, stderr) => {
            const code = error && typeof error.code === 'number'
                ? (error.code)
                : 0;
            if (error && !acceptedExitCodes.includes(code)) {
                const message = stderr?.toString('utf8') || error.message;
                reject(new Error(message.trim() || `git ${args.join(' ')} failed`));
                return;
            }
            resolve(stdout.toString('utf8'));
        });
    });
}
function parseStatusCode(code) {
    switch (code) {
        case 'A':
            return 'added';
        case 'M':
            return 'modified';
        case 'D':
            return 'deleted';
        case 'R':
            return 'renamed';
        case '?':
            return 'untracked';
        default:
            return 'modified';
    }
}
function pushChange(staged, unstaged, path, indexStatus, worktreeStatus, originalPath) {
    if (indexStatus !== '.') {
        staged.push({
            path,
            originalPath,
            status: parseStatusCode(indexStatus),
            staged: true
        });
    }
    if (worktreeStatus !== '.') {
        unstaged.push({
            path,
            originalPath,
            status: parseStatusCode(worktreeStatus),
            staged: false
        });
    }
}
function parseNumstatCount(value) {
    if (!value || value === '-')
        return 0;
    const parsed = Number.parseInt(value, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function sumNumstatOutput(raw) {
    let insertions = 0;
    let deletions = 0;
    for (const record of raw.split('\0')) {
        if (!record.includes('\t'))
            continue;
        const [insertionsRaw, deletionsRaw] = record.split('\t');
        insertions += parseNumstatCount(insertionsRaw);
        deletions += parseNumstatCount(deletionsRaw);
    }
    return { insertions, deletions };
}
async function diffNumstat(cwd, args) {
    const raw = await git(cwd, args, { acceptedExitCodes: [0, 1] });
    return sumNumstatOutput(raw);
}
async function untrackedNumstat(cwd, paths) {
    let insertions = 0;
    let deletions = 0;
    for (const path of paths) {
        const raw = await git(cwd, ['diff', '--no-index', '--numstat', '-z', '--', '/dev/null', path], {
            acceptedExitCodes: [0, 1]
        });
        const counts = sumNumstatOutput(raw);
        insertions += counts.insertions;
        deletions += counts.deletions;
    }
    return { insertions, deletions };
}
export function parsePorcelainV2Z(raw) {
    const records = raw.split('\0').filter(Boolean);
    let branch = '';
    let upstream = null;
    let ahead = 0;
    let behind = 0;
    const staged = [];
    const unstaged = [];
    for (let i = 0; i < records.length; i += 1) {
        const record = records[i];
        if (record.startsWith('# branch.head ')) {
            branch = record.slice('# branch.head '.length);
        }
        else if (record.startsWith('# branch.upstream ')) {
            upstream = record.slice('# branch.upstream '.length);
        }
        else if (record.startsWith('# branch.ab ')) {
            const match = record.match(/^\# branch\.ab \+(\d+) -(\d+)/);
            if (match) {
                ahead = Number.parseInt(match[1], 10);
                behind = Number.parseInt(match[2], 10);
            }
        }
        else if (record.startsWith('1 ')) {
            const parts = record.split(' ');
            const xy = parts[1] ?? '..';
            const path = parts.slice(8).join(' ');
            pushChange(staged, unstaged, path, xy[0] ?? '.', xy[1] ?? '.');
        }
        else if (record.startsWith('2 ')) {
            const parts = record.split(' ');
            const xy = parts[1] ?? '..';
            const path = parts.slice(9).join(' ');
            const originalPath = records[i + 1]?.startsWith('#') ? undefined : records[i + 1];
            if (originalPath)
                i += 1;
            pushChange(staged, unstaged, path, xy[0] ?? '.', xy[1] ?? '.', originalPath);
        }
        else if (record.startsWith('? ')) {
            unstaged.push({ path: record.slice(2), status: 'untracked', staged: false });
        }
    }
    return {
        branch,
        upstream,
        ahead,
        behind,
        staged,
        unstaged,
        clean: staged.length === 0 && unstaged.length === 0
    };
}
export class GitService {
    async isRepo(cwd) {
        try {
            await git(cwd, ['rev-parse', '--is-inside-work-tree']);
            return true;
        }
        catch {
            return false;
        }
    }
    async status(cwd) {
        const raw = await git(cwd, ['status', '--porcelain=v2', '-z', '--branch', '-uall']);
        const status = parsePorcelainV2Z(raw);
        if (status.clean) {
            return { ...status, insertions: 0, deletions: 0 };
        }
        const [stagedCounts, unstagedCounts] = await Promise.all([
            diffNumstat(cwd, ['diff', '--cached', '--numstat', '-z', '--find-renames', '--']),
            diffNumstat(cwd, ['diff', '--numstat', '-z', '--find-renames', '--'])
        ]);
        const untrackedPaths = status.unstaged
            .filter((file) => file.status === 'untracked')
            .map((file) => file.path);
        const untrackedCounts = untrackedPaths.length > 0 ? await untrackedNumstat(cwd, untrackedPaths) : null;
        return {
            ...status,
            insertions: stagedCounts.insertions +
                unstagedCounts.insertions +
                (untrackedCounts?.insertions ?? 0),
            deletions: stagedCounts.deletions +
                unstagedCounts.deletions +
                (untrackedCounts?.deletions ?? 0)
        };
    }
    async diff(cwd, filePath, staged) {
        if (staged) {
            return git(cwd, ['diff', '--cached', '--no-color', '--', filePath], {
                acceptedExitCodes: [0, 1]
            });
        }
        const trackedDiff = await git(cwd, ['diff', '--no-color', '--', filePath], {
            acceptedExitCodes: [0, 1]
        });
        if (trackedDiff.trim())
            return trackedDiff;
        const absolutePath = join(cwd, filePath);
        return git(cwd, ['diff', '--no-color', '--no-index', '--', '/dev/null', absolutePath], {
            acceptedExitCodes: [0, 1]
        });
    }
    async stagedDiff(cwd) {
        return git(cwd, ['diff', '--cached', '--no-color', '--'], { acceptedExitCodes: [0, 1] });
    }
    async stage(cwd, files) {
        if (files.length === 0)
            return;
        await git(cwd, ['add', '--', ...files]);
    }
    async unstage(cwd, files) {
        if (files.length === 0)
            return;
        await git(cwd, ['restore', '--staged', '--', ...files]);
    }
    async commit(cwd, message) {
        await git(cwd, ['commit', '-m', message], { timeoutMs: 60_000 });
        return (await git(cwd, ['rev-parse', '--short', 'HEAD'])).trim() || 'unknown';
    }
    async log(cwd, limit = 50) {
        try {
            const format = '%H%n%h%n%s%n%an%n%aI%n%D%x00';
            const raw = await git(cwd, ['log', `--format=${format}`, '-n', String(limit), '--no-merges']);
            const entries = [];
            for (const record of raw.split('\x00')) {
                const lines = record.trimStart().split('\n');
                const hash = lines[0]?.trim();
                if (!hash)
                    continue;
                entries.push({
                    hash,
                    shortHash: lines[1]?.trim() ?? '',
                    message: lines[2]?.trim() ?? '',
                    author: lines[3]?.trim() ?? '',
                    date: lines[4]?.trim() ?? '',
                    refs: lines[5]?.trim() ?? ''
                });
            }
            return entries;
        }
        catch {
            return [];
        }
    }
    async branches(cwd) {
        try {
            const raw = await git(cwd, [
                'branch',
                '-a',
                '--format=%(refname:short)\t%(HEAD)\t%(if)%(upstream:remotename)%(then)remote%(else)local%(end)'
            ]);
            return raw
                .trim()
                .split('\n')
                .filter((line) => line.length > 0)
                .map((line) => {
                const [name, head, type] = line.split('\t');
                return {
                    name,
                    current: head === '*',
                    remote: type === 'remote' || name.startsWith('remotes/')
                };
            });
        }
        catch {
            return [];
        }
    }
    async checkout(cwd, branch) {
        await git(cwd, ['checkout', branch]);
    }
    async createBranch(cwd, branch) {
        await git(cwd, ['checkout', '-b', branch]);
    }
    async fetch(cwd) {
        return git(cwd, ['fetch', '--prune'], { timeoutMs: 120_000 });
    }
    async pull(cwd) {
        return git(cwd, ['pull', '--ff-only'], { timeoutMs: 120_000 });
    }
    async push(cwd) {
        return git(cwd, ['push'], { timeoutMs: 120_000 });
    }
}
//# sourceMappingURL=git-service.js.map