import fs from 'node:fs';
import path from 'node:path';
import { CFG_RULES } from '../ast-analysis/rules/index.js';
import { makeCfgRules as _makeCfgRules, buildExtensionSet, buildExtToLangMap, } from '../ast-analysis/shared.js';
import { walkWithVisitors } from '../ast-analysis/visitor.js';
import { createCfgVisitor } from '../ast-analysis/visitors/cfg-visitor.js';
import { deleteCfgForNode, getCfgBlocks, getCfgEdges, getFunctionNodeId, hasCfgTables, openReadonlyOrFail, } from '../db/index.js';
import { debug, info } from '../infrastructure/logger.js';
import { paginateResult } from '../shared/paginate.js';
import { findNodes } from './shared/find-nodes.js';
export { _makeCfgRules as makeCfgRules, CFG_RULES };
const CFG_EXTENSIONS = buildExtensionSet(CFG_RULES);
export function buildFunctionCFG(functionNode, langId) {
    const rules = CFG_RULES.get(langId);
    if (!rules)
        return { blocks: [], edges: [], cyclomatic: 0 };
    const visitor = createCfgVisitor(rules);
    const walkerOpts = {
        functionNodeTypes: new Set(rules.functionNodes),
        nestingNodeTypes: new Set(),
        getFunctionName: (node) => {
            const nameNode = node.childForFieldName?.('name');
            return nameNode ? nameNode.text : null;
        },
    };
    const results = walkWithVisitors(functionNode, [visitor], langId, walkerOpts);
    const cfgResults = (results.cfg || []);
    if (cfgResults.length === 0)
        return { blocks: [], edges: [], cyclomatic: 0 };
    const r = cfgResults.find((result) => result.funcNode === functionNode);
    if (!r)
        return { blocks: [], edges: [], cyclomatic: 0 };
    return { blocks: r.blocks, edges: r.edges, cyclomatic: r.cyclomatic };
}
/**
 * Check whether all function/method definitions in a single file already
 * have native CFG data (blocks populated by the Rust extractor).
 * cfg === null means no body (expected); cfg with empty blocks means not computed.
 */
function hasNativeCfgForFile(symbols) {
    return symbols.definitions
        .filter((d) => (d.kind === 'function' || d.kind === 'method') &&
        d.line > 0 &&
        d.endLine != null &&
        d.endLine > d.line &&
        !d.name.includes('.'))
        .every((d) => d.cfg === null || (d.cfg?.blocks?.length ?? 0) > 0);
}
async function initCfgParsers(fileSymbols) {
    let needsFallback = false;
    for (const [relPath, symbols] of fileSymbols) {
        if (!symbols._tree) {
            const ext = path.extname(relPath).toLowerCase();
            if (CFG_EXTENSIONS.has(ext)) {
                if (!hasNativeCfgForFile(symbols)) {
                    needsFallback = true;
                    break;
                }
            }
        }
    }
    let parsers = null;
    let getParserFn = null;
    if (needsFallback) {
        const { createParsers } = await import('../domain/parser.js');
        parsers = await createParsers();
        const mod = await import('../domain/parser.js');
        getParserFn = mod.getParser;
    }
    return { parsers, getParserFn };
}
function getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn) {
    const ext = path.extname(relPath).toLowerCase();
    let tree = symbols._tree;
    let langId = symbols._langId;
    if (!tree && !hasNativeCfgForFile(symbols)) {
        if (!getParserFn)
            return null;
        langId = extToLang.get(ext);
        if (!langId || !CFG_RULES.has(langId))
            return null;
        const absPath = path.join(rootDir, relPath);
        let code;
        try {
            code = fs.readFileSync(absPath, 'utf-8');
        }
        catch (e) {
            debug(`cfg: cannot read ${relPath}: ${e.message}`);
            return null;
        }
        const parser = getParserFn(parsers, absPath);
        if (!parser)
            return null;
        try {
            tree = parser.parse(code);
        }
        catch (e) {
            debug(`cfg: parse failed for ${relPath}: ${e.message}`);
            return null;
        }
    }
    if (!langId) {
        langId = extToLang.get(ext);
        if (!langId)
            return null;
    }
    return { tree: tree ?? null, langId };
}
function buildVisitorCfgMap(tree, cfgRules, symbols, langId) {
    const needsVisitor = tree &&
        symbols.definitions.some((d) => (d.kind === 'function' || d.kind === 'method') &&
            d.line &&
            d.cfg !== null &&
            !d.cfg?.blocks?.length);
    if (!needsVisitor)
        return null;
    const visitor = createCfgVisitor(cfgRules);
    const typedRules = cfgRules;
    const walkerOpts = {
        functionNodeTypes: new Set(typedRules.functionNodes),
        nestingNodeTypes: new Set(),
        getFunctionName: (node) => {
            const nameNode = node.childForFieldName?.('name');
            return nameNode ? nameNode.text : null;
        },
    };
    const walkResults = walkWithVisitors(tree?.rootNode, [visitor], langId, walkerOpts);
    const cfgResults = (walkResults.cfg || []);
    const visitorCfgByLine = new Map();
    for (const r of cfgResults) {
        if (r.funcNode) {
            const line = r.funcNode.startPosition.row + 1;
            if (!visitorCfgByLine.has(line))
                visitorCfgByLine.set(line, []);
            visitorCfgByLine.get(line)?.push(r);
        }
    }
    return visitorCfgByLine;
}
function persistCfg(cfg, nodeId, insertBlock, insertEdge) {
    const blockDbIds = new Map();
    for (const block of cfg.blocks) {
        const result = insertBlock.run(nodeId, block.index, block.type, block.startLine, block.endLine, block.label);
        blockDbIds.set(block.index, result.lastInsertRowid);
    }
    for (const edge of cfg.edges) {
        const sourceDbId = blockDbIds.get(edge.sourceIndex);
        const targetDbId = blockDbIds.get(edge.targetIndex);
        if (sourceDbId && targetDbId) {
            insertEdge.run(nodeId, sourceDbId, targetDbId, edge.kind);
        }
    }
}
// ─── Build-Time: Compute CFG for Changed Files ─────────────────────────
/**
 * Check if all function/method definitions across all files already have
 * native CFG data (blocks array populated by the Rust extractor).
 * When true, the WASM parser and JS CFG visitor can be fully bypassed.
 */
function allCfgNative(fileSymbols) {
    let hasCfgFile = false;
    for (const [relPath, symbols] of fileSymbols) {
        if (symbols._tree)
            continue; // already parsed via WASM; will use _tree in slow path
        const ext = path.extname(relPath).toLowerCase();
        if (!CFG_EXTENSIONS.has(ext))
            continue;
        hasCfgFile = true;
        if (!hasNativeCfgForFile(symbols))
            return false;
    }
    // Return false when no CFG files found (empty map, all _tree, or all non-CFG
    // extensions) to avoid vacuously triggering the fast path.
    return hasCfgFile;
}
/** Persist native CFG data for a single file (fast path — no tree/visitor needed). */
function persistNativeFileCfg(db, symbols, relPath, insertBlock, insertEdge) {
    let count = 0;
    for (const def of symbols.definitions) {
        if (def.kind !== 'function' && def.kind !== 'method')
            continue;
        if (!def.line)
            continue;
        const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
        if (!nodeId)
            continue;
        deleteCfgForNode(db, nodeId);
        if (!def.cfg?.blocks?.length)
            continue;
        persistCfg(def.cfg, nodeId, insertBlock, insertEdge);
        count++;
    }
    return count;
}
/** Resolve CFG for a definition from native data or visitor results. */
function resolveCfgForDef(def, visitorCfgByLine) {
    if (def.cfg?.blocks?.length) {
        return def.cfg;
    }
    if (!visitorCfgByLine)
        return null;
    const candidates = visitorCfgByLine.get(def.line);
    if (!candidates)
        return null;
    const r = candidates.length === 1
        ? candidates[0]
        : (candidates.find((c) => {
            const n = c.funcNode.childForFieldName?.('name');
            return n && n.text === def.name;
        }) ?? candidates[0]);
    return r ? { blocks: r.blocks, edges: r.edges } : null;
}
/** Persist CFG data for a single file using visitor/native hybrid path. */
function persistVisitorFileCfg(db, symbols, relPath, rootDir, extToLang, parsers, getParserFn, insertBlock, insertEdge) {
    const treeLang = getTreeAndLang(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
    if (!treeLang)
        return 0;
    const { tree, langId } = treeLang;
    const cfgRules = CFG_RULES.get(langId);
    if (!cfgRules)
        return 0;
    const visitorCfgByLine = buildVisitorCfgMap(tree, cfgRules, symbols, langId);
    let count = 0;
    for (const def of symbols.definitions) {
        if (def.kind !== 'function' && def.kind !== 'method')
            continue;
        if (!def.line)
            continue;
        const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
        if (!nodeId)
            continue;
        const cfg = resolveCfgForDef(def, visitorCfgByLine);
        deleteCfgForNode(db, nodeId);
        if (!cfg || cfg.blocks.length === 0)
            continue;
        persistCfg(cfg, nodeId, insertBlock, insertEdge);
        count++;
    }
    return count;
}
export async function buildCFGData(db, fileSymbols, rootDir, engineOpts) {
    // Fast path: when all function/method defs already have native CFG data,
    // skip WASM parser init, tree parsing, and JS visitor entirely — just persist.
    const allNative = allCfgNative(fileSymbols);
    // ── Native bulk-insert fast path ──────────────────────────────────────
    // The Rust bulkInsertCfg handles delete-before-insert atomically on a
    // single rusqlite connection, so there is no dual-connection WAL conflict.
    const nativeDb = engineOpts?.nativeDb;
    if (allNative && nativeDb?.bulkInsertCfg) {
        const entries = [];
        for (const [relPath, symbols] of fileSymbols) {
            const ext = path.extname(relPath).toLowerCase();
            if (!CFG_EXTENSIONS.has(ext))
                continue;
            for (const def of symbols.definitions) {
                if (def.kind !== 'function' && def.kind !== 'method')
                    continue;
                if (!def.line)
                    continue;
                const nodeId = getFunctionNodeId(db, def.name, relPath, def.line);
                if (!nodeId)
                    continue;
                const cfg = def.cfg;
                if (!cfg?.blocks?.length)
                    continue;
                entries.push({
                    nodeId,
                    blocks: cfg.blocks.map((b) => ({
                        index: b.index,
                        blockType: b.type,
                        startLine: b.startLine ?? undefined,
                        endLine: b.endLine ?? undefined,
                        label: b.label ?? undefined,
                    })),
                    edges: (cfg.edges || []).map((e) => ({
                        sourceIndex: e.sourceIndex,
                        targetIndex: e.targetIndex,
                        kind: e.kind,
                    })),
                });
            }
        }
        if (entries.length > 0) {
            let inserted = 0;
            try {
                engineOpts?.suspendJsDb?.();
                inserted = nativeDb.bulkInsertCfg(entries);
            }
            finally {
                engineOpts?.resumeJsDb?.();
            }
            info(`CFG (native bulk): ${inserted} functions analyzed`);
        }
        return;
    }
    const extToLang = buildExtToLangMap();
    let parsers = null;
    let getParserFn = null;
    if (!allNative) {
        ({ parsers, getParserFn } = await initCfgParsers(fileSymbols));
    }
    const insertBlock = db.prepare(`INSERT INTO cfg_blocks (function_node_id, block_index, block_type, start_line, end_line, label)
     VALUES (?, ?, ?, ?, ?, ?)`);
    const insertEdge = db.prepare(`INSERT INTO cfg_edges (function_node_id, source_block_id, target_block_id, kind)
     VALUES (?, ?, ?, ?)`);
    let analyzed = 0;
    const tx = db.transaction(() => {
        for (const [relPath, symbols] of fileSymbols) {
            const ext = path.extname(relPath).toLowerCase();
            if (!CFG_EXTENSIONS.has(ext))
                continue;
            if (allNative && !symbols._tree) {
                analyzed += persistNativeFileCfg(db, symbols, relPath, insertBlock, insertEdge);
                continue;
            }
            analyzed += persistVisitorFileCfg(db, symbols, relPath, rootDir, extToLang, parsers, getParserFn, insertBlock, insertEdge);
        }
    });
    tx();
    if (analyzed > 0) {
        info(`CFG: ${analyzed} functions analyzed`);
    }
}
// ─── Query-Time Functions ───────────────────────────────────────────────
const CFG_DEFAULT_KINDS = ['function', 'method'];
export function cfgData(name, customDbPath, opts = {}) {
    const db = openReadonlyOrFail(customDbPath);
    try {
        const noTests = opts.noTests || false;
        if (!hasCfgTables(db)) {
            return {
                name,
                results: [],
                warning: 'No CFG data found. Rebuild with `codegraph build` (CFG is now included by default).',
            };
        }
        const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }, CFG_DEFAULT_KINDS);
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        const results = nodes.map((node) => {
            const cfgBlocks = getCfgBlocks(db, node.id);
            const cfgEdges = getCfgEdges(db, node.id);
            return {
                name: node.name,
                kind: node.kind,
                file: node.file,
                line: node.line,
                blocks: cfgBlocks.map((b) => ({
                    index: b.block_index,
                    type: b.block_type,
                    startLine: b.start_line,
                    endLine: b.end_line,
                    label: b.label,
                })),
                edges: cfgEdges.map((e) => ({
                    source: e.source_index,
                    sourceType: e.source_type,
                    target: e.target_index,
                    targetType: e.target_type,
                    kind: e.kind,
                })),
                summary: {
                    blockCount: cfgBlocks.length,
                    edgeCount: cfgEdges.length,
                },
            };
        });
        return paginateResult({ name, results }, 'results', opts);
    }
    finally {
        db.close();
    }
}
// ─── Export Formats ─────────────────────────────────────────────────────
export function cfgToDOT(cfgResult) {
    const lines = [];
    for (const r of cfgResult.results) {
        lines.push(`digraph "${r.name}" {`);
        lines.push('  rankdir=TB;');
        lines.push('  node [shape=box, fontname="monospace", fontsize=10];');
        for (const block of r.blocks) {
            const label = blockLabel(block);
            const shape = block.type === 'entry' || block.type === 'exit' ? 'ellipse' : 'box';
            const style = block.type === 'condition' || block.type === 'loop_header'
                ? ', style=filled, fillcolor="#ffffcc"'
                : '';
            lines.push(`  B${block.index} [label="${label}", shape=${shape}${style}];`);
        }
        for (const edge of r.edges) {
            const style = edgeStyle(edge.kind);
            lines.push(`  B${edge.source} -> B${edge.target} [label="${edge.kind}"${style}];`);
        }
        lines.push('}');
    }
    return lines.join('\n');
}
export function cfgToMermaid(cfgResult) {
    const lines = [];
    for (const r of cfgResult.results) {
        lines.push(`graph TD`);
        lines.push(`  subgraph "${r.name}"`);
        for (const block of r.blocks) {
            const label = blockLabel(block);
            if (block.type === 'entry' || block.type === 'exit') {
                lines.push(`    B${block.index}(["${label}"])`);
            }
            else if (block.type === 'condition' || block.type === 'loop_header') {
                lines.push(`    B${block.index}{"${label}"}`);
            }
            else {
                lines.push(`    B${block.index}["${label}"]`);
            }
        }
        for (const edge of r.edges) {
            const label = edge.kind;
            lines.push(`    B${edge.source} -->|${label}| B${edge.target}`);
        }
        lines.push('  end');
    }
    return lines.join('\n');
}
function blockLabel(block) {
    const loc = block.startLine && block.endLine
        ? ` L${block.startLine}${block.endLine !== block.startLine ? `-${block.endLine}` : ''}`
        : '';
    const label = block.label ? ` (${block.label})` : '';
    return `${block.type}${label}${loc}`;
}
function edgeStyle(kind) {
    if (kind === 'exception')
        return ', color=red, fontcolor=red';
    if (kind === 'branch_true')
        return ', color=green, fontcolor=green';
    if (kind === 'branch_false')
        return ', color=red, fontcolor=red';
    if (kind === 'loop_back')
        return ', style=dashed, color=blue';
    if (kind === 'loop_exit')
        return ', color=orange';
    if (kind === 'return')
        return ', color=purple';
    if (kind === 'break')
        return ', color=orange, style=dashed';
    if (kind === 'continue')
        return ', color=blue, style=dashed';
    return '';
}
//# sourceMappingURL=cfg.js.map