import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { getDatabase } from '../db/better-sqlite3.js';
import { buildGraph } from '../domain/graph/builder.js';
import { kindIcon } from '../domain/queries.js';
import { debug } from '../infrastructure/logger.js';
import { getNative, isNativeAvailable } from '../infrastructure/native.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { toErrorMessage } from '../shared/errors.js';
import { toSymbolRef } from '../shared/normalize.js';
// ─── Git Helpers ────────────────────────────────────────────────────────
function validateGitRef(repoRoot, ref) {
    try {
        const sha = execFileSync('git', ['rev-parse', '--verify', ref], {
            cwd: repoRoot,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        }).trim();
        return sha;
    }
    catch (e) {
        debug(`validateGitRef failed for "${ref}": ${toErrorMessage(e)}`);
        return null;
    }
}
function getChangedFilesBetweenRefs(repoRoot, base, target) {
    const output = execFileSync('git', ['diff', '--name-only', `${base}..${target}`], {
        cwd: repoRoot,
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
    }).trim();
    if (!output)
        return [];
    return output.split('\n').filter(Boolean);
}
function createWorktree(repoRoot, ref, dir) {
    execFileSync('git', ['worktree', 'add', '--detach', dir, ref], {
        cwd: repoRoot,
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
    });
}
function removeWorktree(repoRoot, dir) {
    try {
        execFileSync('git', ['worktree', 'remove', '--force', dir], {
            cwd: repoRoot,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
    }
    catch (e) {
        debug(`removeWorktree: git worktree remove failed for ${dir}: ${toErrorMessage(e)}`);
        try {
            fs.rmSync(dir, { recursive: true, force: true });
        }
        catch (rmErr) {
            debug(`removeWorktree: rmSync fallback failed for ${dir}: ${toErrorMessage(rmErr)}`);
        }
        try {
            execFileSync('git', ['worktree', 'prune'], {
                cwd: repoRoot,
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
            });
        }
        catch (pruneErr) {
            debug(`removeWorktree: git worktree prune failed: ${toErrorMessage(pruneErr)}`);
        }
    }
}
function makeSymbolKey(kind, file, name) {
    return `${kind}::${file}::${name}`;
}
function loadSymbolsFromDb(dbPath, changedFiles, noTests) {
    const Database = getDatabase();
    const db = new Database(dbPath, { readonly: true });
    // Try opening a NativeDatabase for batched fan metrics
    let nativeDb;
    if (isNativeAvailable()) {
        try {
            const native = getNative();
            nativeDb = native.NativeDatabase.openReadonly(dbPath);
        }
        catch (e) {
            debug(`loadSymbolsFromDb: native path failed: ${toErrorMessage(e)}`);
        }
    }
    try {
        const symbols = new Map();
        if (changedFiles.length === 0) {
            return symbols;
        }
        const placeholders = changedFiles.map(() => '?').join(', ');
        const rows = db
            .prepare(`SELECT n.id, n.name, n.kind, n.file, n.line, n.end_line
         FROM nodes n
         WHERE n.file IN (${placeholders})
           AND n.kind NOT IN ('file', 'directory')
         ORDER BY n.file, n.line`)
            .all(...changedFiles);
        // Filter first, then batch fan metrics for all surviving rows
        const filtered = noTests ? rows.filter((r) => !isTestFile(r.file)) : rows;
        // ── Native fast path: batch all fan-in/fan-out in one napi call ──
        if (nativeDb?.batchFanMetrics && filtered.length > 0) {
            const nodeIds = filtered.map((r) => r.id);
            const metrics = nativeDb.batchFanMetrics(nodeIds);
            const metricsMap = new Map(metrics.map((m) => [m.nodeId, m]));
            for (const row of filtered) {
                const lineCount = row.end_line ? row.end_line - row.line + 1 : 0;
                const m = metricsMap.get(row.id);
                const key = makeSymbolKey(row.kind, row.file, row.name);
                symbols.set(key, {
                    id: row.id,
                    name: row.name,
                    kind: row.kind,
                    file: row.file,
                    line: row.line,
                    lineCount,
                    fanIn: m?.fanIn ?? 0,
                    fanOut: m?.fanOut ?? 0,
                });
            }
            return symbols;
        }
        // ── JS fallback ───────────────────────────────────────────────────
        const fanInStmt = db.prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE target_id = ? AND kind = 'calls'`);
        const fanOutStmt = db.prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE source_id = ? AND kind = 'calls'`);
        for (const row of filtered) {
            const lineCount = row.end_line ? row.end_line - row.line + 1 : 0;
            const fanIn = fanInStmt.get(row.id).cnt;
            const fanOut = fanOutStmt.get(row.id).cnt;
            const key = makeSymbolKey(row.kind, row.file, row.name);
            symbols.set(key, {
                id: row.id,
                name: row.name,
                kind: row.kind,
                file: row.file,
                line: row.line,
                lineCount,
                fanIn,
                fanOut,
            });
        }
        return symbols;
    }
    finally {
        db.close();
        if (nativeDb) {
            try {
                nativeDb.close();
            }
            catch (e) {
                debug(`loadSymbolsFromDb: nativeDb close failed: ${toErrorMessage(e)}`);
            }
        }
    }
}
// ─── Caller BFS ─────────────────────────────────────────────────────────
function loadCallersFromDb(dbPath, nodeIds, maxDepth, noTests) {
    if (nodeIds.length === 0)
        return [];
    const Database = getDatabase();
    const db = new Database(dbPath, { readonly: true });
    try {
        const allCallers = new Set();
        for (const startId of nodeIds) {
            const visited = new Set([startId]);
            let frontier = [startId];
            for (let d = 1; d <= maxDepth; d++) {
                const nextFrontier = [];
                for (const fid of frontier) {
                    const callers = db
                        .prepare(`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line
               FROM edges e JOIN nodes n ON e.source_id = n.id
               WHERE e.target_id = ? AND e.kind = 'calls'`)
                        .all(fid);
                    for (const c of callers) {
                        if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
                            visited.add(c.id);
                            nextFrontier.push(c.id);
                            allCallers.add(JSON.stringify(toSymbolRef(c)));
                        }
                    }
                }
                frontier = nextFrontier;
                if (frontier.length === 0)
                    break;
            }
        }
        return [...allCallers].map((s) => JSON.parse(s));
    }
    finally {
        db.close();
    }
}
// ─── Symbol Comparison ──────────────────────────────────────────────────
function compareSymbols(baseSymbols, targetSymbols) {
    const added = [];
    const removed = [];
    const changed = [];
    for (const [key, sym] of targetSymbols) {
        if (!baseSymbols.has(key)) {
            added.push(sym);
        }
    }
    for (const [key, sym] of baseSymbols) {
        if (!targetSymbols.has(key)) {
            removed.push(sym);
        }
    }
    for (const [key, baseSym] of baseSymbols) {
        const targetSym = targetSymbols.get(key);
        if (!targetSym)
            continue;
        const lineCountDelta = targetSym.lineCount - baseSym.lineCount;
        const fanInDelta = targetSym.fanIn - baseSym.fanIn;
        const fanOutDelta = targetSym.fanOut - baseSym.fanOut;
        if (lineCountDelta !== 0 || fanInDelta !== 0 || fanOutDelta !== 0) {
            changed.push({
                name: baseSym.name,
                kind: baseSym.kind,
                file: baseSym.file,
                base: {
                    line: baseSym.line,
                    lineCount: baseSym.lineCount,
                    fanIn: baseSym.fanIn,
                    fanOut: baseSym.fanOut,
                },
                target: {
                    line: targetSym.line,
                    lineCount: targetSym.lineCount,
                    fanIn: targetSym.fanIn,
                    fanOut: targetSym.fanOut,
                },
                changes: {
                    lineCount: lineCountDelta,
                    fanIn: fanInDelta,
                    fanOut: fanOutDelta,
                },
            });
        }
    }
    return { added, removed, changed };
}
function attachImpactToSymbols(symbols, dbPath, _baseSymbols, maxDepth, noTests) {
    for (const sym of symbols) {
        const symCallers = loadCallersFromDb(dbPath, sym.id ? [sym.id] : [], maxDepth, noTests);
        sym.impact = symCallers;
    }
}
function attachImpactToChanged(changed, dbPath, baseSymbols, maxDepth, noTests) {
    for (const sym of changed) {
        const baseSym = baseSymbols.get(makeSymbolKey(sym.kind, sym.file, sym.name));
        const symCallers = loadCallersFromDb(dbPath, baseSym?.id ? [baseSym.id] : [], maxDepth, noTests);
        sym.impact = symCallers;
    }
}
export async function branchCompareData(baseRef, targetRef, opts = {}) {
    const repoRoot = opts.repoRoot || process.cwd();
    const maxDepth = opts.depth || 3;
    const noTests = opts.noTests || false;
    const engine = (opts.engine || 'wasm');
    try {
        execFileSync('git', ['rev-parse', '--git-dir'], {
            cwd: repoRoot,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        });
    }
    catch (e) {
        debug(`branchCompareData: git check failed: ${toErrorMessage(e)}`);
        return { error: 'Not a git repository' };
    }
    const baseSha = validateGitRef(repoRoot, baseRef);
    if (!baseSha)
        return { error: `Invalid git ref: "${baseRef}"` };
    const targetSha = validateGitRef(repoRoot, targetRef);
    if (!targetSha)
        return { error: `Invalid git ref: "${targetRef}"` };
    const changedFiles = getChangedFilesBetweenRefs(repoRoot, baseSha, targetSha);
    if (changedFiles.length === 0) {
        return {
            baseRef,
            targetRef,
            baseSha,
            targetSha,
            changedFiles: [],
            added: [],
            removed: [],
            changed: [],
            summary: {
                added: 0,
                removed: 0,
                changed: 0,
                totalImpacted: 0,
                filesAffected: 0,
            },
        };
    }
    const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-bc-'));
    const baseDir = path.join(tmpBase, 'base');
    const targetDir = path.join(tmpBase, 'target');
    try {
        createWorktree(repoRoot, baseSha, baseDir);
        createWorktree(repoRoot, targetSha, targetDir);
        await buildGraph(baseDir, { engine, skipRegistry: true });
        await buildGraph(targetDir, { engine, skipRegistry: true });
        const baseDbPath = path.join(baseDir, '.codegraph', 'graph.db');
        const targetDbPath = path.join(targetDir, '.codegraph', 'graph.db');
        const normalizedFiles = changedFiles.map((f) => f.replace(/\\/g, '/'));
        const baseSymbols = loadSymbolsFromDb(baseDbPath, normalizedFiles, noTests);
        const targetSymbols = loadSymbolsFromDb(targetDbPath, normalizedFiles, noTests);
        const { added, removed, changed } = compareSymbols(baseSymbols, targetSymbols);
        const removedIds = removed.map((s) => s.id).filter(Boolean);
        const changedIds = changed
            .map((s) => {
            const baseSym = baseSymbols.get(makeSymbolKey(s.kind, s.file, s.name));
            return baseSym?.id;
        })
            .filter((id) => Boolean(id));
        const removedImpact = loadCallersFromDb(baseDbPath, removedIds, maxDepth, noTests);
        const changedImpact = loadCallersFromDb(baseDbPath, changedIds, maxDepth, noTests);
        attachImpactToSymbols(removed, baseDbPath, baseSymbols, maxDepth, noTests);
        attachImpactToChanged(changed, baseDbPath, baseSymbols, maxDepth, noTests);
        const allImpacted = new Set();
        for (const c of removedImpact)
            allImpacted.add(`${c.file}:${c.name}`);
        for (const c of changedImpact)
            allImpacted.add(`${c.file}:${c.name}`);
        const impactedFiles = new Set();
        for (const key of allImpacted)
            impactedFiles.add(key.split(':')[0]);
        const cleanAdded = added.map(({ id: _id, ...rest }) => rest);
        const cleanRemoved = removed.map(({ id: _id, ...rest }) => {
            const result = rest;
            if (rest.impact) {
                result.impact = rest.impact;
            }
            return result;
        });
        return {
            baseRef,
            targetRef,
            baseSha,
            targetSha,
            changedFiles: normalizedFiles,
            added: cleanAdded,
            removed: cleanRemoved,
            changed,
            summary: {
                added: added.length,
                removed: removed.length,
                changed: changed.length,
                totalImpacted: allImpacted.size,
                filesAffected: impactedFiles.size,
            },
        };
    }
    catch (err) {
        return { error: toErrorMessage(err) };
    }
    finally {
        removeWorktree(repoRoot, baseDir);
        removeWorktree(repoRoot, targetDir);
        try {
            fs.rmSync(tmpBase, { recursive: true, force: true });
        }
        catch (cleanupErr) {
            debug(`branchCompareData: temp cleanup failed: ${toErrorMessage(cleanupErr)}`);
        }
    }
}
function mermaidNodeId(state, key) {
    if (!state.map.has(key)) {
        state.map.set(key, `n${state.counter++}`);
    }
    return state.map.get(key);
}
function addMermaidSubgraph(lines, state, prefix, label, symbols, fillColor, strokeColor) {
    if (symbols.length === 0)
        return;
    lines.push(`    subgraph sg_${prefix}["${label}"]`);
    for (const sym of symbols) {
        const key = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
        const nid = mermaidNodeId(state, key);
        lines.push(`        ${nid}["[${kindIcon(sym.kind)}] ${sym.name}"]`);
    }
    lines.push('    end');
    lines.push(`    style sg_${prefix} fill:${fillColor},stroke:${strokeColor}`);
}
function collectImpactedCallers(impactSources) {
    const allImpacted = new Map();
    for (const sym of impactSources) {
        if (!sym.impact)
            continue;
        for (const c of sym.impact) {
            const key = `impact::${c.kind}::${c.file}::${c.name}`;
            if (!allImpacted.has(key))
                allImpacted.set(key, c);
        }
    }
    return allImpacted;
}
export function branchCompareMermaid(data) {
    if (data.error)
        return data.error;
    if ((data.added?.length ?? 0) === 0 &&
        (data.removed?.length ?? 0) === 0 &&
        (data.changed?.length ?? 0) === 0) {
        return 'flowchart TB\n    none["No structural differences detected"]';
    }
    const lines = ['flowchart TB'];
    const state = { counter: 0, map: new Map() };
    addMermaidSubgraph(lines, state, 'added', 'Added', data.added || [], '#e8f5e9', '#4caf50');
    addMermaidSubgraph(lines, state, 'removed', 'Removed', data.removed || [], '#ffebee', '#f44336');
    addMermaidSubgraph(lines, state, 'changed', 'Changed', data.changed || [], '#fff3e0', '#ff9800');
    const impactSources = [...(data.removed || []), ...(data.changed || [])];
    const allImpacted = collectImpactedCallers(impactSources);
    if (allImpacted.size > 0) {
        lines.push('    subgraph sg_impact["Impacted Callers"]');
        for (const [key, c] of allImpacted) {
            const nid = mermaidNodeId(state, key);
            lines.push(`        ${nid}["[${kindIcon(c.kind)}] ${c.name}"]`);
        }
        lines.push('    end');
        lines.push('    style sg_impact fill:#f3e5f5,stroke:#9c27b0');
    }
    for (const sym of impactSources) {
        if (!sym.impact)
            continue;
        const prefix = (data.removed || []).includes(sym) ? 'removed' : 'changed';
        const symKey = `${prefix}::${sym.kind}::${sym.file}::${sym.name}`;
        for (const c of sym.impact) {
            const callerKey = `impact::${c.kind}::${c.file}::${c.name}`;
            if (state.map.has(symKey) && state.map.has(callerKey)) {
                lines.push(`    ${state.map.get(symKey)} -.-> ${state.map.get(callerKey)}`);
            }
        }
    }
    return lines.join('\n');
}
//# sourceMappingURL=branch-compare.js.map