/**
 * Execution flow tracing — forward BFS from entry points through callees to leaves.
 *
 * Answers "what happens when a user hits POST /login?" by tracing from
 * framework entry points (routes, commands, events) through their call chains.
 */
import { openReadonlyOrFail } from '../db/index.js';
import { CORE_SYMBOL_KINDS, findMatchingNodes } from '../domain/queries.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { toSymbolRef } from '../shared/normalize.js';
import { paginateResult } from '../shared/paginate.js';
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
export function entryPointType(name) {
    for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
        if (name.startsWith(prefix)) {
            return prefix.slice(0, -1); // 'route:', 'event:', 'command:' → 'route', 'event', 'command'
        }
    }
    return null;
}
export function listEntryPointsData(dbPath, opts = {}) {
    const db = openReadonlyOrFail(dbPath);
    try {
        const noTests = opts.noTests || false;
        // Find all framework-prefixed nodes
        const prefixConditions = FRAMEWORK_ENTRY_PREFIXES.map(() => 'n.name LIKE ?').join(' OR ');
        const prefixParams = FRAMEWORK_ENTRY_PREFIXES.map((p) => `${p}%`);
        let rows = db
            .prepare(`SELECT n.name, n.kind, n.file, n.line, n.role
         FROM nodes n
         WHERE (
           (${prefixConditions})
           OR n.role = 'entry'
         )
           AND n.kind NOT IN ('file', 'directory')
         ORDER BY n.name`)
            .all(...prefixParams);
        if (noTests)
            rows = rows.filter((r) => !isTestFile(r.file));
        const entries = rows.map((r) => ({
            name: r.name,
            kind: r.kind,
            file: r.file,
            line: r.line,
            role: r.role,
            type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
        }));
        const byType = {};
        for (const e of entries) {
            const t = e.type || 'other';
            if (!byType[t])
                byType[t] = [];
            byType[t].push(e);
        }
        const base = { entries, byType, count: entries.length };
        return paginateResult(base, 'entries', { limit: opts.limit, offset: opts.offset });
    }
    finally {
        db.close();
    }
}
/** Resolve the entry node by direct match or framework-prefix matching. */
function resolveEntryNode(db, name, flowOpts) {
    // Phase 1: Direct LIKE match on full name
    let matchNode = findMatchingNodes(db, name, flowOpts)[0] ?? null;
    // Phase 2: Prefix-stripped matching — try adding framework prefixes
    if (!matchNode) {
        for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
            matchNode = findMatchingNodes(db, `${prefix}${name}`, flowOpts)[0] ?? null;
            if (matchNode)
                break;
        }
    }
    return matchNode;
}
/** Process one callee row, recording cycle hits or expanding frontier. */
function processFlowCallee(c, fid, depth, noTests, frame) {
    if (noTests && isTestFile(c.file))
        return;
    if (frame.visited.has(c.id)) {
        const fromNode = frame.idToNode.get(fid);
        if (fromNode) {
            frame.cycles.push({ from: fromNode.name, to: c.name, depth });
        }
        return;
    }
    frame.visited.add(c.id);
    frame.nextFrontier.push(c.id);
    const nodeInfo = toSymbolRef(c);
    frame.levelNodes.push(nodeInfo);
    frame.nodeDepths.set(c.id, depth);
    frame.idToNode.set(c.id, nodeInfo);
}
/** Forward BFS through callees, collecting steps, cycles, and node depth info. */
function bfsCallees(db, entryId, entryInfo, maxDepth, noTests) {
    const visited = new Set([entryId]);
    let frontier = [entryId];
    const steps = [];
    const cycles = [];
    const nodeDepths = new Map();
    const idToNode = new Map();
    idToNode.set(entryId, entryInfo);
    let truncated = false;
    const calleesStmt = db.prepare(`SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
     FROM edges e JOIN nodes n ON e.target_id = n.id
     WHERE e.source_id = ? AND e.kind = 'calls'`);
    for (let d = 1; d <= maxDepth; d++) {
        const frame = {
            visited,
            cycles,
            nodeDepths,
            idToNode,
            nextFrontier: [],
            levelNodes: [],
        };
        for (const fid of frontier) {
            for (const c of calleesStmt.all(fid)) {
                processFlowCallee(c, fid, d, noTests, frame);
            }
        }
        if (frame.levelNodes.length > 0) {
            steps.push({ depth: d, nodes: frame.levelNodes });
        }
        frontier = frame.nextFrontier;
        if (frontier.length === 0)
            break;
        if (d === maxDepth && frontier.length > 0)
            truncated = true;
    }
    return { visited, steps, cycles, nodeDepths, idToNode, truncated };
}
/** Identify leaf nodes — visited nodes with no outgoing 'calls' edges. */
function findLeafNodes(db, nodeDepths, idToNode) {
    const leaves = [];
    const outgoingStmt = db.prepare(`SELECT DISTINCT n.id
     FROM edges e JOIN nodes n ON e.target_id = n.id
     WHERE e.source_id = ? AND e.kind = 'calls'`);
    for (const [id, depth] of nodeDepths) {
        const outgoing = outgoingStmt.all(id);
        if (outgoing.length === 0) {
            const node = idToNode.get(id);
            if (node)
                leaves.push({ ...node, depth });
        }
    }
    return leaves;
}
export function flowData(name, dbPath, opts = {}) {
    const db = openReadonlyOrFail(dbPath);
    try {
        const maxDepth = opts.depth || 10;
        const noTests = opts.noTests || false;
        const flowOpts = {
            ...opts,
            kinds: opts.kind ? [opts.kind] : CORE_SYMBOL_KINDS,
        };
        const matchNode = resolveEntryNode(db, name, flowOpts);
        if (!matchNode) {
            return {
                entry: null,
                depth: maxDepth,
                steps: [],
                leaves: [],
                cycles: [],
                totalReached: 0,
                truncated: false,
            };
        }
        const epType = entryPointType(matchNode.name);
        const entry = {
            name: matchNode.name,
            kind: matchNode.kind,
            file: matchNode.file,
            line: matchNode.line,
            type: epType || 'exported',
            role: matchNode.role,
        };
        const bfs = bfsCallees(db, matchNode.id, entry, maxDepth, noTests);
        const leaves = findLeafNodes(db, bfs.nodeDepths, bfs.idToNode);
        const base = {
            entry,
            depth: maxDepth,
            steps: bfs.steps,
            leaves,
            cycles: bfs.cycles,
            totalReached: bfs.visited.size - 1,
            truncated: bfs.truncated,
        };
        return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
    }
    finally {
        db.close();
    }
}
//# sourceMappingURL=flow.js.map