/**
 * Dataflow analysis — define/use chains and data movement edges.
 *
 * Adds three edge types to track how data moves through functions:
 *   - flows_to:  parameter/variable flows into another function as an argument
 *   - returns:   a call's return value is captured and used in the caller
 *   - mutates:   a parameter-derived value is mutated (e.g. arr.push())
 *
 * Opt-in via `build --dataflow`. Supports all languages with DATAFLOW_RULES.
 */
import fs from 'node:fs';
import path from 'node:path';
import { DATAFLOW_RULES } from '../ast-analysis/rules/index.js';
import { makeDataflowRules as _makeDataflowRules, buildExtensionSet, buildExtToLangMap, } from '../ast-analysis/shared.js';
import { walkWithVisitors } from '../ast-analysis/visitor.js';
import { createDataflowVisitor } from '../ast-analysis/visitors/dataflow-visitor.js';
import { hasDataflowTable, openReadonlyOrFail, openReadonlyWithNative } from '../db/index.js';
import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js';
import { debug, info } from '../infrastructure/logger.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { paginateResult } from '../shared/paginate.js';
import { findNodes } from './shared/find-nodes.js';
// Re-export for backward compatibility
export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES };
export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
export function extractDataflow(tree, _filePath, _definitions, langId = 'javascript') {
    const rules = DATAFLOW_RULES.get(langId);
    if (!rules)
        return { parameters: [], returns: [], assignments: [], argFlows: [], mutations: [] };
    const visitor = createDataflowVisitor(rules);
    const results = walkWithVisitors(tree.rootNode, [visitor], langId, {
        functionNodeTypes: rules.functionNodes,
        getFunctionName: () => null, // dataflow visitor handles its own name extraction
    });
    return results.dataflow;
}
async function initDataflowParsers(fileSymbols) {
    let needsFallback = false;
    for (const [relPath, symbols] of fileSymbols) {
        if (!symbols._tree && !symbols.dataflow) {
            const ext = path.extname(relPath).toLowerCase();
            if (DATAFLOW_EXTENSIONS.has(ext)) {
                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 getDataflowForFile(symbols, relPath, rootDir, extToLang, parsers, getParserFn) {
    if (symbols.dataflow)
        return symbols.dataflow;
    let tree = symbols._tree;
    let langId = symbols._langId;
    if (!tree) {
        if (!getParserFn)
            return null;
        const ext = path.extname(relPath).toLowerCase();
        langId = extToLang.get(ext);
        if (!langId || !DATAFLOW_RULES.has(langId))
            return null;
        const absPath = path.join(rootDir, relPath);
        let code;
        try {
            code = fs.readFileSync(absPath, 'utf-8');
        }
        catch (e) {
            debug(`dataflow: cannot read ${relPath}: ${e.message}`);
            return null;
        }
        const parser = getParserFn(parsers, absPath);
        if (!parser)
            return null;
        try {
            tree = parser.parse(code);
        }
        catch (e) {
            debug(`dataflow: parse failed for ${relPath}: ${e.message}`);
            return null;
        }
    }
    if (!langId) {
        const ext = path.extname(relPath).toLowerCase();
        langId = extToLang.get(ext);
        if (!langId)
            return null;
    }
    if (!DATAFLOW_RULES.has(langId))
        return null;
    return extractDataflow(tree, relPath, symbols.definitions, langId);
}
function insertDataflowEdges(insert, data, resolveNode) {
    let edgeCount = 0;
    for (const flow of data.argFlows) {
        const sourceNode = resolveNode(flow.callerFunc);
        const targetNode = resolveNode(flow.calleeName);
        if (sourceNode && targetNode) {
            insert.run(sourceNode.id, targetNode.id, 'flows_to', flow.argIndex, flow.expression, flow.line, flow.confidence);
            edgeCount++;
        }
    }
    for (const assignment of data.assignments) {
        const producerNode = resolveNode(assignment.sourceCallName);
        const consumerNode = resolveNode(assignment.callerFunc);
        if (producerNode && consumerNode) {
            insert.run(producerNode.id, consumerNode.id, 'returns', null, assignment.expression, assignment.line, 1.0);
            edgeCount++;
        }
    }
    for (const mut of data.mutations) {
        const mutatorNode = resolveNode(mut.funcName);
        if (mutatorNode && mut.binding?.type === 'param') {
            insert.run(mutatorNode.id, mutatorNode.id, 'mutates', null, mut.mutatingExpr, mut.line, 1.0);
            edgeCount++;
        }
    }
    return edgeCount;
}
// ── buildDataflowEdges ──────────────────────────────────────────────────────
function prepareNodeResolvers(db) {
    return {
        getNodeByNameAndFile: db.prepare(`SELECT id, name, kind, file, line FROM nodes
       WHERE name = ? AND file = ? AND kind IN ('function', 'method')`),
        getNodeByName: db.prepare(`SELECT id, name, kind, file, line FROM nodes
       WHERE name = ? AND kind IN ('function', 'method')
       ORDER BY file, line LIMIT 10`),
    };
}
function makeNodeResolver(stmts, relPath) {
    return (funcName) => {
        const local = stmts.getNodeByNameAndFile.all(funcName, relPath);
        if (local.length > 0)
            return local[0];
        const global = stmts.getNodeByName.all(funcName);
        return global.length > 0 ? global[0] : null;
    };
}
function collectNativeEdges(data, resolveNode, edges) {
    for (const flow of data.argFlows) {
        const sourceNode = resolveNode(flow.callerFunc);
        const targetNode = resolveNode(flow.calleeName);
        if (sourceNode && targetNode) {
            edges.push({
                sourceId: sourceNode.id,
                targetId: targetNode.id,
                kind: 'flows_to',
                paramIndex: flow.argIndex,
                expression: flow.expression,
                line: flow.line,
                confidence: flow.confidence,
            });
        }
    }
    for (const assignment of data.assignments) {
        const producerNode = resolveNode(assignment.sourceCallName);
        const consumerNode = resolveNode(assignment.callerFunc);
        if (producerNode && consumerNode) {
            edges.push({
                sourceId: producerNode.id,
                targetId: consumerNode.id,
                kind: 'returns',
                paramIndex: undefined,
                expression: assignment.expression,
                line: assignment.line,
                confidence: 1.0,
            });
        }
    }
    for (const mut of data.mutations) {
        const mutatorNode = resolveNode(mut.funcName);
        if (mutatorNode && mut.binding?.type === 'param') {
            edges.push({
                sourceId: mutatorNode.id,
                targetId: mutatorNode.id,
                kind: 'mutates',
                paramIndex: undefined,
                expression: mut.mutatingExpr,
                line: mut.line,
                confidence: 1.0,
            });
        }
    }
}
export async function buildDataflowEdges(db, fileSymbols, rootDir, engineOpts) {
    const extToLang = buildExtToLangMap();
    // ── Native bulk-insert fast path ──────────────────────────────────────
    const nativeDb = engineOpts?.nativeDb;
    if (nativeDb?.bulkInsertDataflow) {
        let needsJsFallback = false;
        const nativeEdges = [];
        const stmts = prepareNodeResolvers(db);
        for (const [relPath, symbols] of fileSymbols) {
            const ext = path.extname(relPath).toLowerCase();
            if (!DATAFLOW_EXTENSIONS.has(ext))
                continue;
            if (!symbols.dataflow) {
                needsJsFallback = true;
                break;
            }
            collectNativeEdges(symbols.dataflow, makeNodeResolver(stmts, relPath), nativeEdges);
        }
        if (!needsJsFallback) {
            if (nativeEdges.length > 0) {
                let inserted;
                try {
                    engineOpts?.suspendJsDb?.();
                    inserted = nativeDb.bulkInsertDataflow(nativeEdges);
                }
                finally {
                    engineOpts?.resumeJsDb?.();
                }
                info(`Dataflow (native bulk): ${inserted} edges inserted`);
            }
            return;
        }
        debug('Dataflow: some files lack pre-computed data — falling back to JS');
    }
    // ── JS fallback path ─────────────────────────────────────────────────
    const { parsers, getParserFn } = await initDataflowParsers(fileSymbols);
    const insert = db.prepare(`INSERT INTO dataflow (source_id, target_id, kind, param_index, expression, line, confidence)
     VALUES (?, ?, ?, ?, ?, ?, ?)`);
    const stmts = prepareNodeResolvers(db);
    let totalEdges = 0;
    const tx = db.transaction(() => {
        for (const [relPath, symbols] of fileSymbols) {
            const ext = path.extname(relPath).toLowerCase();
            if (!DATAFLOW_EXTENSIONS.has(ext))
                continue;
            const data = getDataflowForFile(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
            if (!data)
                continue;
            totalEdges += insertDataflowEdges(insert, data, makeNodeResolver(stmts, relPath));
        }
    });
    tx();
    info(`Dataflow: ${totalEdges} edges inserted`);
}
function prepareDataflowStmts(db) {
    return {
        flowsToOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
     FROM dataflow d JOIN nodes n ON d.target_id = n.id
     WHERE d.source_id = ? AND d.kind = 'flows_to'`),
        flowsToIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
     FROM dataflow d JOIN nodes n ON d.source_id = n.id
     WHERE d.target_id = ? AND d.kind = 'flows_to'`),
        returnsOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
     FROM dataflow d JOIN nodes n ON d.target_id = n.id
     WHERE d.source_id = ? AND d.kind = 'returns'`),
        returnsIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
     FROM dataflow d JOIN nodes n ON d.source_id = n.id
     WHERE d.target_id = ? AND d.kind = 'returns'`),
        mutatesOut: db.prepare(`SELECT d.*, n.name AS target_name, n.kind AS target_kind, n.file AS target_file, n.line AS target_line
     FROM dataflow d JOIN nodes n ON d.target_id = n.id
     WHERE d.source_id = ? AND d.kind = 'mutates'`),
        mutatesIn: db.prepare(`SELECT d.*, n.name AS source_name, n.kind AS source_kind, n.file AS source_file, n.line AS source_line
     FROM dataflow d JOIN nodes n ON d.source_id = n.id
     WHERE d.target_id = ? AND d.kind = 'mutates'`),
    };
}
/**
 * Build a unified dataflow result from pre-mapped edge data.
 * Shared between the SQL and native code paths.
 */
function buildDataflowResult(sym, edges, noTests) {
    if (noTests) {
        const filter = (arr) => arr.filter((r) => !isTestFile(r.file));
        return {
            ...sym,
            flowsTo: filter(edges.flowsTo),
            flowsFrom: filter(edges.flowsFrom),
            returns: edges.returnConsumers.filter((r) => !isTestFile(r.file)),
            returnedBy: edges.returnedBy.filter((r) => !isTestFile(r.file)),
            mutates: edges.mutatesTargets,
            mutatedBy: edges.mutatedBy,
        };
    }
    return {
        ...sym,
        flowsTo: edges.flowsTo,
        flowsFrom: edges.flowsFrom,
        returns: edges.returnConsumers,
        returnedBy: edges.returnedBy,
        mutates: edges.mutatesTargets,
        mutatedBy: edges.mutatedBy,
    };
}
function buildNodeDataflowResult(node, stmts, db, hc, noTests) {
    const sym = normalizeSymbol(node, db, hc);
    const edges = {
        flowsTo: stmts.flowsToOut.all(node.id).map((r) => ({
            target: r.target_name,
            kind: r.target_kind,
            file: r.target_file,
            line: r.line,
            paramIndex: r.param_index,
            expression: r.expression,
            confidence: r.confidence,
        })),
        flowsFrom: stmts.flowsToIn.all(node.id).map((r) => ({
            source: r.source_name,
            kind: r.source_kind,
            file: r.source_file,
            line: r.line,
            paramIndex: r.param_index,
            expression: r.expression,
            confidence: r.confidence,
        })),
        returnConsumers: stmts.returnsOut.all(node.id).map((r) => ({
            consumer: r.target_name,
            kind: r.target_kind,
            file: r.target_file,
            line: r.line,
            expression: r.expression,
        })),
        returnedBy: stmts.returnsIn.all(node.id).map((r) => ({
            producer: r.source_name,
            kind: r.source_kind,
            file: r.source_file,
            line: r.line,
            expression: r.expression,
        })),
        mutatesTargets: stmts.mutatesOut.all(node.id).map((r) => ({
            target: r.target_name,
            expression: r.expression,
            line: r.line,
        })),
        mutatedBy: stmts.mutatesIn.all(node.id).map((r) => ({
            source: r.source_name,
            expression: r.expression,
            line: r.line,
        })),
    };
    return buildDataflowResult(sym, edges, noTests);
}
function buildNativeDataflowResult(node, nativeDb, db, hc, noTests) {
    const sym = normalizeSymbol(node, db, hc);
    const d = nativeDb.getDataflowEdges(node.id);
    const edges = {
        flowsTo: d.flowsToOut.map((r) => ({
            target: r.name,
            kind: r.kind,
            file: r.file,
            line: r.line,
            paramIndex: r.paramIndex,
            expression: r.expression,
            confidence: r.confidence,
        })),
        flowsFrom: d.flowsToIn.map((r) => ({
            source: r.name,
            kind: r.kind,
            file: r.file,
            line: r.line,
            paramIndex: r.paramIndex,
            expression: r.expression,
            confidence: r.confidence,
        })),
        returnConsumers: d.returnsOut.map((r) => ({
            consumer: r.name,
            kind: r.kind,
            file: r.file,
            line: r.line,
            expression: r.expression,
        })),
        returnedBy: d.returnsIn.map((r) => ({
            producer: r.name,
            kind: r.kind,
            file: r.file,
            line: r.line,
            expression: r.expression,
        })),
        mutatesTargets: d.mutatesOut.map((r) => ({
            target: r.name,
            expression: r.expression,
            line: r.line,
        })),
        mutatedBy: d.mutatesIn.map((r) => ({
            source: r.name,
            expression: r.expression,
            line: r.line,
        })),
    };
    return buildDataflowResult(sym, edges, noTests);
}
export function dataflowData(name, customDbPath, opts = {}) {
    const { db, nativeDb, close } = openReadonlyWithNative(customDbPath);
    try {
        const noTests = opts.noTests || false;
        if (!hasDataflowTable(db)) {
            return {
                name,
                results: [],
                warning: 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
            };
        }
        const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }, ALL_SYMBOL_KINDS);
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        // ── Native fast path: 6 queries per node → 1 napi call per node ──
        if (nativeDb?.getDataflowEdges) {
            const hc = new Map();
            const results = nodes.map((node) => buildNativeDataflowResult(node, nativeDb, db, hc, noTests));
            const base = { name, results };
            return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
        }
        // ── JS fallback ───────────────────────────────────────────────────
        const stmts = prepareDataflowStmts(db);
        const hc = new Map();
        const results = nodes.map((node) => buildNodeDataflowResult(node, stmts, db, hc, noTests));
        const base = { name, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    }
    finally {
        close();
    }
}
/**
 * Process a single neighbor in the dataflow BFS. Returns true once the target
 * has been reached so the caller can stop expanding.
 */
function processDataflowNeighbor(n, currentId, targetId, noTests, state) {
    if (noTests && isTestFile(n.file))
        return false;
    const entry = {
        parentId: currentId,
        edgeKind: n.edge_kind,
        expression: n.expression,
    };
    if (n.id === targetId) {
        if (!state.found) {
            state.found = true;
            state.parent.set(n.id, entry);
        }
        return true;
    }
    if (state.visited.has(n.id))
        return false;
    state.visited.add(n.id);
    state.parent.set(n.id, entry);
    state.nextQueue.push(n.id);
    return false;
}
/** BFS through dataflow edges to find a path from source to target. */
function bfsDataflowPath(db, sourceId, targetId, maxDepth, noTests) {
    const neighborStmt = db.prepare(`SELECT n.id, n.name, n.kind, n.file, n.line, d.kind AS edge_kind, d.expression
     FROM dataflow d JOIN nodes n ON d.target_id = n.id
     WHERE d.source_id = ? AND d.kind IN ('flows_to', 'returns')`);
    const state = {
        visited: new Set([sourceId]),
        parent: new Map(),
        nextQueue: [],
        found: false,
    };
    let queue = [sourceId];
    for (let depth = 1; depth <= maxDepth; depth++) {
        state.nextQueue = [];
        for (const currentId of queue) {
            const neighbors = neighborStmt.all(currentId);
            for (const n of neighbors) {
                processDataflowNeighbor(n, currentId, targetId, noTests, state);
            }
        }
        if (state.found)
            break;
        queue = state.nextQueue;
        if (queue.length === 0)
            break;
    }
    return state.found ? state.parent : null;
}
/** Reconstruct a path from BFS parent map. */
function reconstructDataflowPath(db, parent, sourceId, targetId) {
    const nodeById = db.prepare('SELECT * FROM nodes WHERE id = ?');
    const hc = new Map();
    const pathItems = [];
    let cur = targetId;
    while (cur !== undefined) {
        const nodeRow = nodeById.get(cur);
        const parentInfo = parent.get(cur);
        pathItems.unshift({
            ...normalizeSymbol(nodeRow, db, hc),
            edgeKind: parentInfo?.edgeKind ?? null,
            expression: parentInfo?.expression ?? null,
        });
        cur = parentInfo?.parentId;
        if (cur === sourceId) {
            const srcRow = nodeById.get(cur);
            pathItems.unshift({
                ...normalizeSymbol(srcRow, db, hc),
                edgeKind: null,
                expression: null,
            });
            break;
        }
    }
    return pathItems;
}
export function dataflowPathData(from, to, customDbPath, opts = {}) {
    const db = openReadonlyOrFail(customDbPath);
    try {
        const noTests = opts.noTests || false;
        const maxDepth = opts.maxDepth || 10;
        if (!hasDataflowTable(db)) {
            return {
                from,
                to,
                found: false,
                warning: 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
            };
        }
        const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind }, ALL_SYMBOL_KINDS);
        if (fromNodes.length === 0) {
            return { from, to, found: false, error: `No symbol matching "${from}"` };
        }
        const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind }, ALL_SYMBOL_KINDS);
        if (toNodes.length === 0) {
            return { from, to, found: false, error: `No symbol matching "${to}"` };
        }
        const sourceNode = fromNodes[0];
        const targetNode = toNodes[0];
        if (sourceNode.id === targetNode.id) {
            const hc = new Map();
            const sym = normalizeSymbol(sourceNode, db, hc);
            return { from, to, found: true, hops: 0, path: [{ ...sym, edgeKind: null }] };
        }
        const parent = bfsDataflowPath(db, sourceNode.id, targetNode.id, maxDepth, noTests);
        if (!parent) {
            return { from, to, found: false };
        }
        const pathItems = reconstructDataflowPath(db, parent, sourceNode.id, targetNode.id);
        return { from, to, found: true, hops: pathItems.length - 1, path: pathItems };
    }
    finally {
        db.close();
    }
}
/** BFS forward through return-value consumers to build impact levels. */
function bfsReturnConsumers(node, consumersStmt, db, hc, maxDepth, noTests) {
    const visited = new Set([node.id]);
    const levels = {};
    let frontier = [node.id];
    for (let d = 1; d <= maxDepth; d++) {
        const nextFrontier = [];
        for (const fid of frontier) {
            const consumers = consumersStmt.all(fid);
            for (const c of consumers) {
                if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
                    visited.add(c.id);
                    nextFrontier.push(c.id);
                    if (!levels[d])
                        levels[d] = [];
                    levels[d].push(normalizeSymbol(c, db, hc));
                }
            }
        }
        frontier = nextFrontier;
        if (frontier.length === 0)
            break;
    }
    return { levels, totalAffected: visited.size - 1 };
}
export function dataflowImpactData(name, customDbPath, opts = {}) {
    const db = openReadonlyOrFail(customDbPath);
    try {
        const maxDepth = opts.depth || 5;
        const noTests = opts.noTests || false;
        if (!hasDataflowTable(db)) {
            return {
                name,
                results: [],
                warning: 'No dataflow data found. Rebuild with `codegraph build` (dataflow is now included by default).',
            };
        }
        const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind }, ALL_SYMBOL_KINDS);
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        const consumersStmt = db.prepare(`SELECT DISTINCT n.*
     FROM dataflow d JOIN nodes n ON d.target_id = n.id
     WHERE d.source_id = ? AND d.kind = 'returns'`);
        const hc = new Map();
        const results = nodes.map((node) => {
            const sym = normalizeSymbol(node, db, hc);
            const { levels, totalAffected } = bfsReturnConsumers(node, consumersStmt, db, hc, maxDepth, noTests);
            return { ...sym, levels, totalAffected };
        });
        const base = { name, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    }
    finally {
        db.close();
    }
}
//# sourceMappingURL=dataflow.js.map