import { execFileSync } from "node:child_process";
import { getCodeIndex } from "./index-tools.js";
import { collectImportEdges } from "../utils/import-graph.js";
// ---------------------------------------------------------------------------
// fan_in_fan_out
// ---------------------------------------------------------------------------
export async function fanInFanOut(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index) {
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    }
    const topN = options?.top_n ?? 20;
    const focusPath = options?.path;
    const minFanIn = options?.min_fan_in ?? 0;
    const minFanOut = options?.min_fan_out ?? 0;
    // Collect ALL edges — we cannot pre-filter via collectImportEdges because
    // that only reads imports FROM files in the filter, which loses inbound
    // edges from outside the focus area. Post-filter after collection instead.
    const edges = await collectImportEdges(index);
    // Build directed fan-in and fan-out maps
    const fanInMap = new Map(); // file → set of importers
    const fanOutMap = new Map(); // file → set of imports
    for (const edge of edges) {
        // Keep edges where at least one side is in the focus path
        if (focusPath && !edge.from.startsWith(focusPath) && !edge.to.startsWith(focusPath)) {
            continue;
        }
        // Fan-in: who imports this file
        if (!fanInMap.has(edge.to))
            fanInMap.set(edge.to, new Set());
        fanInMap.get(edge.to).add(edge.from);
        // Fan-out: what does this file import
        if (!fanOutMap.has(edge.from))
            fanOutMap.set(edge.from, new Set());
        fanOutMap.get(edge.from).add(edge.to);
    }
    // Collect all files
    const allFiles = new Set();
    for (const f of fanInMap.keys())
        allFiles.add(f);
    for (const f of fanOutMap.keys())
        allFiles.add(f);
    // Build sorted lists. min_fan_in/min_fan_out filter BEFORE top_n cap so users
    // can request "all files with fan_in > 50" by setting min_fan_in=50, top_n=Infinity.
    const fanInEntries = [...fanInMap.entries()]
        .map(([file, importers]) => ({
        file,
        count: importers.size,
        connections: [...importers].sort(),
    }))
        .filter((m) => m.count >= minFanIn)
        .sort((a, b) => b.count - a.count)
        .slice(0, topN);
    const fanOutEntries = [...fanOutMap.entries()]
        .map(([file, imports]) => ({
        file,
        count: imports.size,
        connections: [...imports].sort(),
    }))
        .filter((m) => m.count >= minFanOut)
        .sort((a, b) => b.count - a.count)
        .slice(0, topN);
    // Hub files: both fan-in AND fan-out above 75th percentile
    const inCounts = [...fanInMap.values()].map((s) => s.size).sort((a, b) => a - b);
    const outCounts = [...fanOutMap.values()].map((s) => s.size).sort((a, b) => a - b);
    const inP75 = inCounts.length > 0 ? inCounts[Math.floor(inCounts.length * 0.75)] : 0;
    const outP75 = outCounts.length > 0 ? outCounts[Math.floor(outCounts.length * 0.75)] : 0;
    const hubFiles = [];
    for (const file of allFiles) {
        const inCount = fanInMap.get(file)?.size ?? 0;
        const outCount = fanOutMap.get(file)?.size ?? 0;
        if (inCount > inP75 && outCount > outP75) {
            hubFiles.push({
                file,
                count: inCount + outCount,
                connections: [`in=${inCount}`, `out=${outCount}`],
            });
        }
    }
    hubFiles.sort((a, b) => b.count - a.count);
    // Coupling score: fewer hubs = better
    const totalFiles = allFiles.size;
    const score = totalFiles > 0
        ? Math.max(0, Math.min(100, Math.round(100 - (hubFiles.length / totalFiles) * 100)))
        : 100;
    return {
        fan_in_top: fanInEntries,
        fan_out_top: fanOutEntries,
        hub_files: hubFiles.slice(0, topN),
        coupling_score: score,
        total_files: totalFiles,
        total_edges: edges.length,
    };
}
// ---------------------------------------------------------------------------
// computeCoChangePairs — shared function (extracted from review-diff-tools.ts)
// ---------------------------------------------------------------------------
const DEFAULT_SINCE_DAYS = 180;
const DEFAULT_MIN_SUPPORT = 3;
const DEFAULT_MAX_FILES_PER_COMMIT = 50;
export function computeCoChangePairs(repoRoot, options) {
    const sinceDays = options?.since_days ?? DEFAULT_SINCE_DAYS;
    const minSupport = options?.min_support ?? DEFAULT_MIN_SUPPORT;
    const maxFilesPerCommit = options?.max_files_per_commit ?? DEFAULT_MAX_FILES_PER_COMMIT;
    // maxBuffer: Node default is 1MB which overflows on large repos (>6 mo, 2000+ commits).
    // Increase to 256MB to cover any reasonable single-repo git log output.
    // timeout: 30s (was 15s) — large repos need more time to walk history.
    let raw;
    try {
        raw = execFileSync("git", [
            "log",
            "--name-only",
            "--no-merges",
            "--diff-filter=AMRC",
            `--since=${sinceDays} days ago`,
            "--pretty=format:COMMIT %H",
        ], {
            cwd: repoRoot,
            encoding: "utf-8",
            timeout: 30000,
            maxBuffer: 256 * 1024 * 1024, // 256MB
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.includes("ENOBUFS") || msg.includes("maxBuffer")) {
            throw new Error(`git log output too large for co_change_analysis (>256MB). ` +
                `Try reducing since_days (current: ${sinceDays}) to limit history scope.`);
        }
        if (msg.includes("ETIMEDOUT") || msg.includes("timeout")) {
            throw new Error(`git log timed out after 30s. Try reducing since_days (current: ${sinceDays}).`);
        }
        throw new Error(`co_change_analysis: git log failed — ${msg}`);
    }
    // Parse commits robustly using a COMMIT sentinel line. Commits with no
    // qualifying files (e.g. D-only commits dropped by --diff-filter=AMRC)
    // produce a COMMIT line with no following file list, which the old
    // i += 2 block-pairing approach mis-parsed.
    const lines = raw.split("\n");
    const fileCommitCounts = new Map();
    const pairCounts = new Map();
    let totalCommits = 0;
    let currentFiles = [];
    const flushCommit = () => {
        if (currentFiles.length === 0 || currentFiles.length > maxFilesPerCommit) {
            currentFiles = [];
            return;
        }
        totalCommits++;
        for (const file of currentFiles) {
            fileCommitCounts.set(file, (fileCommitCounts.get(file) ?? 0) + 1);
        }
        for (let a = 0; a < currentFiles.length; a++) {
            for (let b = a + 1; b < currentFiles.length; b++) {
                const pair = [currentFiles[a], currentFiles[b]].sort().join("\0");
                pairCounts.set(pair, (pairCounts.get(pair) ?? 0) + 1);
            }
        }
        currentFiles = [];
    };
    for (const line of lines) {
        if (line.startsWith("COMMIT ")) {
            flushCommit();
            continue;
        }
        const trimmed = line.trim();
        if (trimmed.length > 0)
            currentFiles.push(trimmed);
    }
    flushCommit(); // final commit
    // Build CoChangePair[] for all pairs above min_support
    const pairs = [];
    for (const [pair, coCount] of pairCounts) {
        if (coCount < minSupport)
            continue;
        const [fileA, fileB] = pair.split("\0");
        const countA = fileCommitCounts.get(fileA) ?? 0;
        const countB = fileCommitCounts.get(fileB) ?? 0;
        const jaccard = coCount / (countA + countB - coCount);
        pairs.push({
            file_a: fileA,
            file_b: fileB,
            co_commits: coCount,
            jaccard,
            support_a: countA,
            support_b: countB,
        });
    }
    return { pairs, total_commits: totalCommits };
}
// ---------------------------------------------------------------------------
// co_change_analysis
// ---------------------------------------------------------------------------
export async function coChangeAnalysis(repo, options) {
    const index = await getCodeIndex(repo);
    if (!index) {
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    }
    const minJaccard = options?.min_jaccard ?? 0.3;
    const focusPath = options?.path;
    const topN = options?.top_n ?? 30;
    const sinceDays = options?.since_days ?? DEFAULT_SINCE_DAYS;
    const coChangeOpts = { since_days: sinceDays };
    if (options?.min_support != null)
        coChangeOpts.min_support = options.min_support;
    const { pairs: allPairs, total_commits } = computeCoChangePairs(index.root, coChangeOpts);
    // Filter by jaccard threshold and focus path
    let filtered = allPairs.filter((p) => p.jaccard >= minJaccard);
    if (focusPath) {
        filtered = filtered.filter((p) => p.file_a.startsWith(focusPath) || p.file_b.startsWith(focusPath));
    }
    // Sort by jaccard descending
    filtered.sort((a, b) => b.jaccard - a.jaccard);
    const topPairs = filtered.slice(0, topN);
    // Cluster detection: connected components where jaccard > 0.7
    const clusters = findClusters(filtered.filter((p) => p.jaccard > 0.7));
    return {
        pairs: topPairs,
        clusters,
        total_commits_analyzed: total_commits,
        period: `${sinceDays} days`,
    };
}
// ---------------------------------------------------------------------------
// Cluster detection via union-find
// ---------------------------------------------------------------------------
function findClusters(pairs) {
    const parent = new Map();
    function find(x) {
        if (!parent.has(x))
            parent.set(x, x);
        let root = parent.get(x);
        while (root !== parent.get(root)) {
            root = parent.get(root);
        }
        parent.set(x, root); // path compression
        return root;
    }
    function union(a, b) {
        const ra = find(a);
        const rb = find(b);
        if (ra !== rb)
            parent.set(ra, rb);
    }
    for (const p of pairs) {
        union(p.file_a, p.file_b);
    }
    // Group by root
    const groups = new Map();
    for (const file of parent.keys()) {
        const root = find(file);
        if (!groups.has(root))
            groups.set(root, []);
        groups.get(root).push(file);
    }
    // Return only clusters with 2+ files, sorted by size descending
    return [...groups.values()]
        .filter((g) => g.length >= 2)
        .sort((a, b) => b.length - a.length)
        .map((g) => g.sort());
}
//# sourceMappingURL=coupling-tools.js.map