import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { findDbPath, openReadonlyOrFail } from '../../db/index.js';
import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { evaluateBoundaries } from '../../features/boundaries.js';
import { coChangeForFiles } from '../../features/cochange.js';
import { ownersForFiles } from '../../features/owners.js';
import { loadConfig } from '../../infrastructure/config.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { paginateResult } from '../../shared/paginate.js';
import { bfsTransitiveCallers } from './fn-impact.js';
const _defsStmtCache = new WeakMap();
// --- diffImpactData helpers ---
/**
 * Walk up from repoRoot until a .git directory is found.
 * Returns true if a git root exists, false otherwise.
 */
function findGitRoot(repoRoot) {
    let checkDir = repoRoot;
    while (checkDir) {
        if (fs.existsSync(path.join(checkDir, '.git'))) {
            return true;
        }
        const parent = path.dirname(checkDir);
        if (parent === checkDir)
            break;
        checkDir = parent;
    }
    return false;
}
/**
 * Execute git diff and return the raw output string.
 * Returns `{ output: string }` on success or `{ error: string }` on failure.
 */
function runGitDiff(repoRoot, opts) {
    try {
        const args = opts.staged
            ? ['diff', '--cached', '--unified=0', '--no-color']
            : ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
        const output = execFileSync('git', args, {
            cwd: repoRoot,
            encoding: 'utf-8',
            maxBuffer: 10 * 1024 * 1024,
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        return { output };
    }
    catch (e) {
        return { error: `Failed to run git diff: ${e.message}` };
    }
}
/**
 * Parse raw git diff output into a changedRanges map and newFiles set.
 */
function parseGitDiff(diffOutput) {
    const changedRanges = new Map();
    const newFiles = new Set();
    let currentFile = null;
    let prevIsDevNull = false;
    for (const line of diffOutput.split('\n')) {
        if (line.startsWith('--- /dev/null')) {
            prevIsDevNull = true;
            continue;
        }
        if (line.startsWith('--- ')) {
            prevIsDevNull = false;
            continue;
        }
        const fileMatch = line.match(/^\+\+\+ b\/(.+)/);
        if (fileMatch) {
            currentFile = fileMatch[1];
            if (!changedRanges.has(currentFile))
                changedRanges.set(currentFile, []);
            if (prevIsDevNull)
                newFiles.add(currentFile);
            prevIsDevNull = false;
            continue;
        }
        const hunkMatch = line.match(/^@@ .+ \+(\d+)(?:,(\d+))? @@/);
        if (hunkMatch && currentFile) {
            const start = parseInt(hunkMatch[1], 10);
            const count = parseInt(hunkMatch[2] || '1', 10);
            changedRanges.get(currentFile).push({ start, end: start + count - 1 });
        }
    }
    return { changedRanges, newFiles };
}
/**
 * Find all function/method/class nodes whose line ranges overlap any changed range.
 */
function findAffectedFunctions(db, changedRanges, noTests) {
    const affectedFunctions = [];
    const defsStmt = cachedStmt(_defsStmtCache, db, `SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`);
    for (const [file, ranges] of changedRanges) {
        if (noTests && isTestFile(file))
            continue;
        const defs = defsStmt.all(file);
        for (let i = 0; i < defs.length; i++) {
            const def = defs[i];
            const endLine = def.end_line || (defs[i + 1] ? defs[i + 1].line - 1 : 999999);
            for (const range of ranges) {
                if (range.start <= endLine && range.end >= def.line) {
                    affectedFunctions.push(def);
                    break;
                }
            }
        }
    }
    return affectedFunctions;
}
/**
 * Run BFS per affected function, collecting per-function results and the full affected set.
 */
function buildFunctionImpactResults(db, affectedFunctions, noTests, maxDepth, includeImplementors = true) {
    const allAffected = new Set();
    const functionResults = affectedFunctions.map((fn) => {
        const edges = [];
        const idToKey = new Map();
        idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
        const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
            noTests,
            maxDepth,
            includeImplementors,
            onVisit(c, parentId) {
                allAffected.add(`${c.file}:${c.name}`);
                const callerKey = `${c.file}::${c.name}:${c.line}`;
                idToKey.set(c.id, callerKey);
                edges.push({ from: idToKey.get(parentId), to: callerKey });
            },
        });
        return {
            name: fn.name,
            kind: fn.kind,
            file: fn.file,
            line: fn.line,
            transitiveCallers: totalDependents,
            levels,
            edges,
        };
    });
    return { functionResults, allAffected };
}
/**
 * Look up historically co-changed files for the set of changed files.
 * Returns an empty array if the co_changes table is unavailable.
 */
function lookupCoChanges(db, changedRanges, affectedFiles, noTests) {
    try {
        db.prepare('SELECT 1 FROM co_changes LIMIT 1').get();
        const changedFilesList = [...changedRanges.keys()];
        const coResults = coChangeForFiles(changedFilesList, db, {
            minJaccard: 0.3,
            limit: 20,
            noTests,
        });
        return coResults.filter((r) => !affectedFiles.has(r.file));
    }
    catch (e) {
        debug(`co_changes lookup skipped: ${e.message}`);
        return [];
    }
}
/**
 * Look up CODEOWNERS for changed and affected files.
 * Returns null if no owners are found or lookup fails.
 */
function lookupOwnership(changedRanges, affectedFiles, repoRoot) {
    try {
        const allFilePaths = [...new Set([...changedRanges.keys(), ...affectedFiles])];
        const ownerResult = ownersForFiles(allFilePaths, repoRoot);
        if (ownerResult.affectedOwners.length > 0) {
            return {
                owners: Object.fromEntries(ownerResult.owners),
                affectedOwners: ownerResult.affectedOwners,
                suggestedReviewers: ownerResult.suggestedReviewers,
            };
        }
        return null;
    }
    catch (e) {
        debug(`CODEOWNERS lookup skipped: ${e.message}`);
        return null;
    }
}
/**
 * Check manifesto boundary violations scoped to the changed files.
 * Returns `{ boundaryViolations, boundaryViolationCount }`.
 */
function checkBoundaryViolations(db, changedRanges, noTests, opts, repoRoot) {
    try {
        const cfg = opts.config || loadConfig(repoRoot);
        const boundaryConfig = cfg.manifesto?.boundaries;
        if (boundaryConfig) {
            const result = evaluateBoundaries(db, boundaryConfig, {
                scopeFiles: [...changedRanges.keys()],
                noTests,
            });
            return {
                boundaryViolations: result.violations,
                boundaryViolationCount: result.violationCount,
            };
        }
    }
    catch (e) {
        debug(`boundary check skipped: ${e.message}`);
    }
    return { boundaryViolations: [], boundaryViolationCount: 0 };
}
// --- diffImpactData ---
/**
 * Compute diff-impact analysis between two git refs (or staged changes).
 * Uses execFileSync (via runGitDiff) to avoid shell injection.
 */
export function diffImpactData(customDbPath, opts = {}) {
    const db = openReadonlyOrFail(customDbPath);
    try {
        const noTests = opts.noTests || false;
        const config = opts.config || loadConfig();
        const maxDepth = opts.depth || config.analysis?.impactDepth || 3;
        const dbPath = findDbPath(customDbPath);
        const repoRoot = path.resolve(path.dirname(dbPath), '..');
        if (!findGitRoot(repoRoot)) {
            return { error: `Not a git repository: ${repoRoot}` };
        }
        const gitResult = runGitDiff(repoRoot, opts);
        if ('error' in gitResult)
            return { error: gitResult.error };
        if (!gitResult.output.trim()) {
            return {
                changedFiles: 0,
                newFiles: [],
                affectedFunctions: [],
                affectedFiles: [],
                summary: null,
            };
        }
        const { changedRanges, newFiles } = parseGitDiff(gitResult.output);
        if (changedRanges.size === 0) {
            return {
                changedFiles: 0,
                newFiles: [],
                affectedFunctions: [],
                affectedFiles: [],
                summary: null,
            };
        }
        const affectedFunctions = findAffectedFunctions(db, changedRanges, noTests);
        // Short-circuit: when no function-level changes detected, skip expensive
        // lookups (BFS, co-change, ownership).  Boundary checks are preserved
        // because they are file-scoped and can surface real violations even when
        // no function bodies were touched (e.g. import or type-alias changes).
        if (affectedFunctions.length === 0) {
            const { boundaryViolations, boundaryViolationCount } = checkBoundaryViolations(db, changedRanges, noTests, { ...opts, config }, repoRoot);
            const base = {
                changedFiles: changedRanges.size,
                newFiles: [...newFiles],
                affectedFunctions: [],
                affectedFiles: [],
                historicallyCoupled: [],
                ownership: null,
                boundaryViolations,
                boundaryViolationCount,
                summary: null,
            };
            return paginateResult(base, 'affectedFunctions', { limit: opts.limit, offset: opts.offset });
        }
        const includeImplementors = opts.includeImplementors !== false;
        const { functionResults, allAffected } = buildFunctionImpactResults(db, affectedFunctions, noTests, maxDepth, includeImplementors);
        const affectedFiles = new Set();
        for (const key of allAffected)
            affectedFiles.add(key.split(':')[0]);
        const historicallyCoupled = lookupCoChanges(db, changedRanges, affectedFiles, noTests);
        const ownership = lookupOwnership(changedRanges, affectedFiles, repoRoot);
        const { boundaryViolations, boundaryViolationCount } = checkBoundaryViolations(db, changedRanges, noTests, { ...opts, config }, repoRoot);
        const base = {
            changedFiles: changedRanges.size,
            newFiles: [...newFiles],
            affectedFunctions: functionResults,
            affectedFiles: [...affectedFiles],
            historicallyCoupled,
            ownership,
            boundaryViolations,
            boundaryViolationCount,
            summary: {
                functionsChanged: affectedFunctions.length,
                callersAffected: allAffected.size,
                filesAffected: affectedFiles.size,
                historicallyCoupledCount: historicallyCoupled.length,
                ownersAffected: ownership ? ownership.affectedOwners.length : 0,
                boundaryViolationCount,
            },
        };
        return paginateResult(base, 'affectedFunctions', { limit: opts.limit, offset: opts.offset });
    }
    finally {
        db.close();
    }
}
//# sourceMappingURL=diff-impact.js.map