// Planu — engine/actuals/git-analyzer.ts (SPEC-061)
// Derives actuals from git log history for a given spec.
import { execSync } from 'node:child_process';
const SPEC_ID_PATTERN = /^SPEC-\d+$/;
const MAX_SESSION_GAP_MS = 2 * 60 * 60 * 1000; // 2 hours
const REVIEW_HOURS_RATIO = 0.15;
const MINIMUM_DEV_HOURS = 1;
/**
 * Parses git log output in the format produced by:
 *   git log --format="%H|%aI|%s" --numstat
 */
export function parseGitLogOutput(output) {
    const commits = [];
    const lines = output.split('\n');
    let i = 0;
    while (i < lines.length) {
        const result = parseNextCommit(lines, i);
        if (result === null) {
            i++;
            continue;
        }
        const { commit, nextIndex } = result;
        if (commit) {
            commits.push(commit);
        }
        i = nextIndex;
    }
    return commits;
}
function parseNextCommit(lines, startIndex) {
    const headerLine = lines[startIndex];
    if (!headerLine?.trim()) {
        return null;
    }
    const headerParts = headerLine.split('|');
    if (headerParts.length < 3) {
        return null;
    }
    /* v8 ignore start */
    const hash = headerParts[0] ?? '';
    const timestamp = headerParts[1] ?? '';
    /* v8 ignore stop */
    const message = headerParts.slice(2).join('|');
    const { insertions, deletions, nextIndex } = parseNumstat(lines, startIndex + 1);
    const commit = hash.length === 40 ? { hash, timestamp, message, insertions, deletions } : null;
    return { commit, nextIndex };
}
function parseNumstat(lines, startIndex) {
    let i = startIndex;
    let insertions = 0;
    let deletions = 0;
    // Skip optional blank line separator after header
    if (lines[i]?.trim() === '') {
        i++;
    }
    // Parse numstat lines (insertions<TAB>deletions<TAB>filename)
    while (i < lines.length) {
        const statLine = lines[i];
        if (!statLine?.trim()) {
            i++;
            break;
        }
        // Next header line detection: 40-char hex hash followed by |
        if (/^[0-9a-f]{40}\|/.test(statLine)) {
            break;
        }
        const statParts = statLine.split('\t');
        /* v8 ignore start */
        const ins = parseInt(statParts[0] ?? '0', 10);
        const del = parseInt(statParts[1] ?? '0', 10);
        /* v8 ignore stop */
        if (!isNaN(ins)) {
            insertions += ins;
        }
        if (!isNaN(del)) {
            deletions += del;
        }
        i++;
    }
    return { insertions, deletions, nextIndex: i };
}
/**
 * Calculates estimated dev hours from commits.
 * Sums intervals between consecutive commits, capping each at 2h
 * (gaps > 2h are treated as overnight pauses and ignored).
 * Minimum 1h for single-commit specs.
 */
export function calculateDevHours(commits) {
    if (commits.length === 0) {
        return 0;
    }
    if (commits.length === 1) {
        return MINIMUM_DEV_HOURS;
    }
    const sorted = [...commits].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
    let totalMs = 0;
    for (let i = 1; i < sorted.length; i++) {
        const prev = sorted[i - 1];
        const curr = sorted[i];
        /* v8 ignore start */
        if (!prev || !curr) {
            continue;
        }
        /* v8 ignore stop */
        const gap = new Date(curr.timestamp).getTime() - new Date(prev.timestamp).getTime();
        if (gap > 0 && gap <= MAX_SESSION_GAP_MS) {
            totalMs += gap;
        }
    }
    const hours = totalMs / (1000 * 60 * 60);
    return Math.max(MINIMUM_DEV_HOURS, Math.round(hours * 10) / 10);
}
/**
 * Sums insertions + deletions across all commits.
 */
export function calculateLinesChanged(commits) {
    return commits.reduce((acc, c) => acc + c.insertions + c.deletions, 0);
}
/**
 * Derives actuals for a spec by running git log filtered by specId in commit messages.
 * Returns null if specId is invalid or no commits are found.
 */
export function deriveActualsFromGit(projectPath, specId) {
    if (!SPEC_ID_PATTERN.test(specId)) {
        return Promise.resolve(null);
    }
    const gitOutput = runGitLog(projectPath, specId);
    if (!gitOutput) {
        return Promise.resolve(null);
    }
    const commits = parseGitLogOutput(gitOutput);
    if (commits.length === 0) {
        return Promise.resolve(null);
    }
    const sorted = [...commits].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
    const firstCommit = sorted[0];
    const lastCommit = sorted[sorted.length - 1];
    /* v8 ignore start */
    if (!firstCommit || !lastCommit) {
        return Promise.resolve(null);
    }
    /* v8 ignore stop */
    const devHours = calculateDevHours(commits);
    const linesChanged = calculateLinesChanged(commits);
    return Promise.resolve({
        devHours,
        reviewHours: Math.round(devHours * REVIEW_HOURS_RATIO * 10) / 10,
        commitCount: commits.length,
        linesChanged,
        firstCommitDate: firstCommit.timestamp,
        lastCommitDate: lastCommit.timestamp,
        completedAt: lastCommit.timestamp,
        source: 'git-log',
    });
}
function runGitLog(projectPath, specId) {
    try {
        const result = execSync(`git log --format="%H|%aI|%s" --numstat --all --grep="${specId}"`, {
            cwd: projectPath,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return result.trim() || null;
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=git-analyzer.js.map