import path from 'node:path';
import { openReadonlyOrFail, openReadonlyWithNative, testFilterSQL } from '../../db/index.js';
import { loadConfig } from '../../infrastructure/config.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js';
import { findCycles } from '../graph/cycles.js';
import { LANGUAGE_REGISTRY } from '../parser.js';
export const FALSE_POSITIVE_NAMES = new Set([
    'run',
    'get',
    'set',
    'init',
    'start',
    'handle',
    'main',
    'new',
    'create',
    'update',
    'delete',
    'process',
    'execute',
    'call',
    'apply',
    'setup',
    'render',
    'build',
    'load',
    'save',
    'find',
    'make',
    'open',
    'close',
    'reset',
    'send',
    'read',
    'write',
]);
export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
// ---------------------------------------------------------------------------
// Section helpers
// ---------------------------------------------------------------------------
function countNodesByKind(db, noTests) {
    const testFilter = testFilterSQL('file', noTests);
    const nodeRows = db
        .prepare(`SELECT kind, COUNT(*) as c FROM nodes WHERE 1=1 ${testFilter} GROUP BY kind`)
        .all();
    const byKind = {};
    let total = 0;
    for (const r of nodeRows) {
        byKind[r.kind] = r.c;
        total += r.c;
    }
    return { total, byKind };
}
function countEdgesByKind(db, noTests) {
    let edgeRows;
    if (noTests) {
        // Join edges with source node to filter out test files in SQL
        const srcFilter = testFilterSQL('ns.file', true);
        const tgtFilter = testFilterSQL('nt.file', true);
        edgeRows = db
            .prepare(`
        SELECT e.kind, COUNT(*) as c FROM edges e
        JOIN nodes ns ON e.source_id = ns.id
        JOIN nodes nt ON e.target_id = nt.id
        WHERE 1=1 ${srcFilter} ${tgtFilter}
        GROUP BY e.kind
      `)
            .all();
    }
    else {
        edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
    }
    const byKind = {};
    let total = 0;
    for (const r of edgeRows) {
        byKind[r.kind] = r.c;
        total += r.c;
    }
    return { total, byKind };
}
function countFilesByLanguage(db, noTests) {
    const extToLang = new Map();
    for (const entry of LANGUAGE_REGISTRY) {
        for (const ext of entry.extensions) {
            extToLang.set(ext, entry.id);
        }
    }
    let fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
    if (noTests)
        fileNodes = fileNodes.filter((n) => !isTestFile(n.file));
    const byLanguage = {};
    for (const row of fileNodes) {
        const ext = path.extname(row.file).toLowerCase();
        const lang = extToLang.get(ext) || 'other';
        byLanguage[lang] = (byLanguage[lang] || 0) + 1;
    }
    return { total: fileNodes.length, languages: Object.keys(byLanguage).length, byLanguage };
}
function findHotspots(db, noTests, limit) {
    const testFilter = testFilterSQL('n.file', noTests);
    const hotspotRows = db
        .prepare(`
      SELECT n.file,
        COALESCE(fi.cnt, 0) as fan_in,
        COALESCE(fo.cnt, 0) as fan_out
      FROM nodes n
      LEFT JOIN (
        SELECT target_id, COUNT(*) AS cnt FROM edges
        WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
        GROUP BY target_id
      ) fi ON fi.target_id = n.id
      LEFT JOIN (
        SELECT source_id, COUNT(*) AS cnt FROM edges
        WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
        GROUP BY source_id
      ) fo ON fo.source_id = n.id
      WHERE n.kind = 'file' ${testFilter}
      ORDER BY COALESCE(fi.cnt, 0) + COALESCE(fo.cnt, 0) DESC
      LIMIT ?
    `)
        .all(limit);
    return hotspotRows.map((r) => ({
        file: r.file,
        fanIn: r.fan_in,
        fanOut: r.fan_out,
    }));
}
function getEmbeddingsInfo(db) {
    try {
        const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
        if (count && count.c > 0) {
            const meta = {};
            const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
            for (const r of metaRows)
                meta[r.key] = r.value;
            return {
                count: count.c,
                model: meta.model || null,
                dim: meta.dim ? parseInt(meta.dim, 10) : null,
                builtAt: meta.built_at || null,
            };
        }
    }
    catch (e) {
        debug(`embeddings lookup skipped: ${e.message}`);
    }
    return null;
}
function computeQualityMetrics(db, testFilter, fpThreshold = FALSE_POSITIVE_CALLER_THRESHOLD) {
    const qualityTestFilter = testFilter.replace(/n\.file/g, 'file');
    const totalCallable = db
        .prepare(`SELECT COUNT(*) as c FROM nodes WHERE kind IN ('function', 'method') ${qualityTestFilter}`)
        .get().c;
    const callableWithCallers = db
        .prepare(`
      SELECT COUNT(DISTINCT e.target_id) as c FROM edges e
      JOIN nodes n ON e.target_id = n.id
      WHERE e.kind = 'calls' AND n.kind IN ('function', 'method') ${testFilter}
    `)
        .get().c;
    const callerCoverage = totalCallable > 0 ? callableWithCallers / totalCallable : 0;
    const totalCallEdges = db.prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls'").get().c;
    const highConfCallEdges = db
        .prepare("SELECT COUNT(*) as c FROM edges WHERE kind = 'calls' AND confidence >= 0.7")
        .get().c;
    const callConfidence = totalCallEdges > 0 ? highConfCallEdges / totalCallEdges : 0;
    const fpRows = db
        .prepare(`
      SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
      FROM nodes n
      LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
      WHERE n.kind IN ('function', 'method')
      GROUP BY n.id
      HAVING caller_count > ?
      ORDER BY caller_count DESC
    `)
        .all(fpThreshold);
    const falsePositiveWarnings = fpRows
        .filter((r) => FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name))
        .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
    let fpEdgeCount = 0;
    for (const fp of falsePositiveWarnings)
        fpEdgeCount += fp.callerCount;
    const falsePositiveRatio = totalCallEdges > 0 ? fpEdgeCount / totalCallEdges : 0;
    const score = Math.round(callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20);
    return {
        score,
        callerCoverage: {
            ratio: callerCoverage,
            covered: callableWithCallers,
            total: totalCallable,
        },
        callConfidence: {
            ratio: callConfidence,
            highConf: highConfCallEdges,
            total: totalCallEdges,
        },
        falsePositiveWarnings,
    };
}
function countRoles(db, noTests) {
    const testFilter = testFilterSQL('file', noTests);
    const roleRows = db
        .prepare(`SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL ${testFilter} GROUP BY role`)
        .all();
    const roles = {};
    let deadTotal = 0;
    for (const r of roleRows) {
        roles[r.role] = r.c;
        if (r.role.startsWith(DEAD_ROLE_PREFIX))
            deadTotal += r.c;
    }
    if (deadTotal > 0)
        roles.dead = deadTotal;
    return roles;
}
function getComplexitySummary(db, testFilter) {
    try {
        const cRows = db
            .prepare(`SELECT fc.cognitive, fc.cyclomatic, fc.max_nesting, fc.maintainability_index
         FROM function_complexity fc JOIN nodes n ON fc.node_id = n.id
         WHERE n.kind IN ('function','method') ${testFilter}`)
            .all();
        if (cRows.length > 0) {
            const miValues = cRows.map((r) => r.maintainability_index || 0);
            return {
                analyzed: cRows.length,
                avgCognitive: +(cRows.reduce((s, r) => s + r.cognitive, 0) / cRows.length).toFixed(1),
                avgCyclomatic: +(cRows.reduce((s, r) => s + r.cyclomatic, 0) / cRows.length).toFixed(1),
                maxCognitive: Math.max(...cRows.map((r) => r.cognitive)),
                maxCyclomatic: Math.max(...cRows.map((r) => r.cyclomatic)),
                avgMI: +(miValues.reduce((s, v) => s + v, 0) / miValues.length).toFixed(1),
                minMI: +Math.min(...miValues).toFixed(1),
            };
        }
    }
    catch (e) {
        debug(`complexity summary skipped: ${e.message}`);
    }
    return null;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function moduleMapData(customDbPath, limit = 20, opts = {}) {
    const db = openReadonlyOrFail(customDbPath);
    try {
        const noTests = opts.noTests || false;
        const testFilter = testFilterSQL('n.file', noTests);
        const nodes = db
            .prepare(`
      SELECT n.file,
        COALESCE(fo.cnt, 0) as out_edges,
        COALESCE(fi.cnt, 0) as in_edges
      FROM nodes n
      LEFT JOIN (
        SELECT source_id, COUNT(*) AS cnt FROM edges
        WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
        GROUP BY source_id
      ) fo ON fo.source_id = n.id
      LEFT JOIN (
        SELECT target_id, COUNT(*) AS cnt FROM edges
        WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
        GROUP BY target_id
      ) fi ON fi.target_id = n.id
      WHERE n.kind = 'file'
        ${testFilter}
      ORDER BY COALESCE(fi.cnt, 0) DESC
      LIMIT ?
    `)
            .all(limit);
        const topNodes = nodes.map((n) => ({
            file: n.file,
            dir: path.dirname(n.file) || '.',
            inEdges: n.in_edges,
            outEdges: n.out_edges,
            coupling: n.in_edges + n.out_edges,
        }));
        const totalNodes = db.prepare('SELECT COUNT(*) as c FROM nodes').get()?.c ?? 0;
        const totalEdges = db.prepare('SELECT COUNT(*) as c FROM edges').get()?.c ?? 0;
        const totalFiles = db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'file'").get()?.c ?? 0;
        return { limit, topNodes, stats: { totalFiles, totalNodes, totalEdges } };
    }
    finally {
        db.close();
    }
}
export function statsData(customDbPath, opts = {}) {
    const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
    try {
        const noTests = opts.noTests || false;
        const config = opts.config || loadConfig();
        // These always need JS (non-SQL logic)
        const files = countFilesByLanguage(db, noTests);
        const fileCycles = findCycles(db, { fileLevel: true, noTests });
        const fnCycles = findCycles(db, { fileLevel: false, noTests });
        // ── Native fast path: batch all SQL aggregations in one napi call ──
        if (nativeDb?.getGraphStats) {
            const s = nativeDb.getGraphStats(noTests);
            const nodesByKind = {};
            for (const k of s.nodesByKind)
                nodesByKind[k.kind] = k.count;
            const edgesByKind = {};
            for (const k of s.edgesByKind)
                edgesByKind[k.kind] = k.count;
            const roles = {};
            let deadTotal = 0;
            for (const r of s.roleCounts) {
                roles[r.role] = r.count;
                if (r.role.startsWith(DEAD_ROLE_PREFIX))
                    deadTotal += r.count;
            }
            if (deadTotal > 0)
                roles.dead = deadTotal;
            const callerCoverage = s.quality.callableTotal > 0 ? s.quality.callableWithCallers / s.quality.callableTotal : 0;
            const callConfidence = s.quality.callEdges > 0 ? s.quality.highConfCallEdges / s.quality.callEdges : 0;
            // False-positive analysis still uses JS (needs FALSE_POSITIVE_NAMES set)
            const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
            const fpRows = db
                .prepare(`
          SELECT n.name, n.file, n.line, COUNT(e.source_id) as caller_count
          FROM nodes n
          LEFT JOIN edges e ON n.id = e.target_id AND e.kind = 'calls'
          WHERE n.kind IN ('function', 'method')
          GROUP BY n.id
          HAVING caller_count > ?
          ORDER BY caller_count DESC
        `)
                .all(fpThreshold);
            const falsePositiveWarnings = fpRows
                .filter((r) => FALSE_POSITIVE_NAMES.has(r.name.includes('.') ? r.name.split('.').pop() : r.name))
                .map((r) => ({ name: r.name, file: r.file, line: r.line, callerCount: r.caller_count }));
            let fpEdgeCount = 0;
            for (const fp of falsePositiveWarnings)
                fpEdgeCount += fp.callerCount;
            const falsePositiveRatio = s.quality.callEdges > 0 ? fpEdgeCount / s.quality.callEdges : 0;
            const score = Math.round(callerCoverage * 40 + callConfidence * 40 + (1 - falsePositiveRatio) * 20);
            return {
                nodes: { total: s.totalNodes, byKind: nodesByKind },
                edges: { total: s.totalEdges, byKind: edgesByKind },
                files,
                cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
                hotspots: s.hotspots.map((h) => ({ file: h.file, fanIn: h.fanIn, fanOut: h.fanOut })),
                embeddings: s.embeddings
                    ? {
                        count: s.embeddings.count,
                        model: s.embeddings.model,
                        dim: s.embeddings.dim,
                        builtAt: s.embeddings.builtAt,
                    }
                    : null,
                quality: {
                    score,
                    callerCoverage: {
                        ratio: callerCoverage,
                        covered: s.quality.callableWithCallers,
                        total: s.quality.callableTotal,
                    },
                    callConfidence: {
                        ratio: callConfidence,
                        highConf: s.quality.highConfCallEdges,
                        total: s.quality.callEdges,
                    },
                    falsePositiveWarnings,
                },
                roles,
                complexity: s.complexity
                    ? {
                        analyzed: s.complexity.analyzed,
                        avgCognitive: s.complexity.avgCognitive,
                        avgCyclomatic: s.complexity.avgCyclomatic,
                        maxCognitive: s.complexity.maxCognitive,
                        maxCyclomatic: s.complexity.maxCyclomatic,
                        avgMI: s.complexity.avgMi,
                        minMI: s.complexity.minMi,
                    }
                    : null,
            };
        }
        // ── JS fallback ───────────────────────────────────────────────────
        const testFilter = testFilterSQL('n.file', noTests);
        const { total: totalNodes, byKind: nodesByKind } = countNodesByKind(db, noTests);
        const { total: totalEdges, byKind: edgesByKind } = countEdgesByKind(db, noTests);
        const hotspots = findHotspots(db, noTests, 5);
        const embeddings = getEmbeddingsInfo(db);
        const fpThreshold = config.analysis?.falsePositiveCallers ?? FALSE_POSITIVE_CALLER_THRESHOLD;
        const quality = computeQualityMetrics(db, testFilter, fpThreshold);
        const roles = countRoles(db, noTests);
        const complexity = getComplexitySummary(db, testFilter);
        return {
            nodes: { total: totalNodes, byKind: nodesByKind },
            edges: { total: totalEdges, byKind: edgesByKind },
            files,
            cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
            hotspots,
            embeddings,
            quality,
            roles,
            complexity,
        };
    }
    finally {
        close();
    }
}
//# sourceMappingURL=module-map.js.map