import { findFileNodes } from '../../db/index.js';
import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
import { normalizeSymbol, toSymbolRef } from '../../shared/normalize.js';
import { paginateResult } from '../../shared/paginate.js';
import { withReadonlyDb, withRepo } from './query-helpers.js';
import { findMatchingNodes } from './symbol-lookup.js';
const _nodeByIdStmtCache = new WeakMap();
export function fileDepsData(file, customDbPath, opts = {}) {
    return withRepo(customDbPath, (repo) => {
        const noTests = opts.noTests || false;
        const fileNodes = repo.findFileNodes(`%${file}%`);
        if (fileNodes.length === 0) {
            return { file, results: [] };
        }
        const results = fileNodes.map((fn) => {
            let importsTo = repo.findImportTargets(fn.id);
            if (noTests)
                importsTo = importsTo.filter((i) => !isTestFile(i.file));
            let importedBy = repo.findImportSources(fn.id);
            if (noTests)
                importedBy = importedBy.filter((i) => !isTestFile(i.file));
            const defs = repo.findNodesByFile(fn.file);
            return {
                file: fn.file,
                imports: importsTo.map((i) => ({ file: i.file, typeOnly: i.edge_kind === 'imports-type' })),
                importedBy: importedBy.map((i) => ({ file: i.file })),
                definitions: defs.map((d) => ({ name: d.name, kind: d.kind, line: d.line })),
            };
        });
        const base = { file, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    });
}
/** Compute the next BFS frontier from a batched upstream-callers lookup. */
function buildNextCallerFrontier(unvisited, batchCallers, visited, noTests) {
    const nextFrontier = [];
    const nextFrontierIds = new Set();
    for (const f of unvisited) {
        const upstream = batchCallers.get(f.id) || [];
        for (const u of upstream) {
            if (noTests && isTestFile(u.file))
                continue;
            if (visited.has(u.id) || nextFrontierIds.has(u.id))
                continue;
            nextFrontierIds.add(u.id);
            nextFrontier.push(u);
        }
    }
    return nextFrontier;
}
function buildTransitiveCallers(repo, callers, nodeId, depth, noTests) {
    const transitiveCallers = {};
    if (depth <= 1)
        return transitiveCallers;
    const visited = new Set([nodeId]);
    let frontier = callers;
    for (let d = 2; d <= depth; d++) {
        // Collect unvisited frontier IDs for a single batched query per depth
        const unvisited = frontier.filter((f) => !visited.has(f.id));
        for (const f of unvisited)
            visited.add(f.id);
        if (unvisited.length === 0)
            break;
        const batchCallers = repo.findCallersBatch(unvisited.map((f) => f.id));
        const nextFrontier = buildNextCallerFrontier(unvisited, batchCallers, visited, noTests);
        if (nextFrontier.length > 0) {
            transitiveCallers[d] = nextFrontier.map((n) => ({
                name: n.name,
                kind: n.kind,
                file: n.file,
                line: n.line,
            }));
        }
        frontier = nextFrontier;
    }
    return transitiveCallers;
}
function collectCallersWithHierarchy(repo, node, noTests) {
    let callers = repo.findCallers(node.id);
    if (node.kind === 'method' && node.name.includes('.')) {
        const methodName = node.name.split('.').pop();
        const relatedMethods = resolveMethodViaHierarchy(repo, methodName);
        for (const rm of relatedMethods) {
            if (rm.id === node.id)
                continue;
            const extraCallers = repo.findCallers(rm.id);
            callers.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
        }
    }
    if (noTests)
        callers = callers.filter((c) => !isTestFile(c.file));
    return callers;
}
function buildNodeDepsResult(repo, node, hc, depth, noTests) {
    const callees = repo.findCallees(node.id);
    const filteredCallees = noTests ? callees.filter((c) => !isTestFile(c.file)) : callees;
    const callers = collectCallersWithHierarchy(repo, node, noTests);
    const transitiveCallers = buildTransitiveCallers(repo, callers, node.id, depth, noTests);
    return {
        ...normalizeSymbol(node, repo, hc),
        callees: filteredCallees.map(toSymbolRef),
        // Not using toSymbolRef — callers include the extra viaHierarchy field
        callers: callers.map((c) => ({
            name: c.name,
            kind: c.kind,
            file: c.file,
            line: c.line,
            viaHierarchy: c.viaHierarchy || undefined,
        })),
        transitiveCallers,
    };
}
export function fnDepsData(name, customDbPath, opts = {}) {
    return withRepo(customDbPath, (repo) => {
        // Try native composite path — single NAPI call for the entire query.
        const nativeResult = repo.fnDeps(name, {
            depth: opts.depth,
            noTests: opts.noTests,
            file: opts.file,
            kind: opts.kind,
        });
        if (nativeResult) {
            const base = { name: nativeResult.name, results: nativeResult.results };
            return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
        }
        // Fallback: JS-orchestrated path (used when native engine is unavailable)
        const depth = opts.depth || 3;
        const noTests = opts.noTests || false;
        const hc = new Map();
        const nodes = findMatchingNodes(repo, name, { noTests, file: opts.file, kind: opts.kind });
        if (nodes.length === 0) {
            return { name, results: [] };
        }
        const results = nodes.map((node) => buildNodeDepsResult(repo, node, hc, depth, noTests));
        const base = { name, results };
        return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
    });
}
/**
 * Resolve from/to symbol names to node records.
 * Returns { sourceNode, targetNode, fromCandidates, toCandidates } on success,
 * or { earlyResult } when a caller-facing error/not-found response should be returned immediately.
 */
function resolveEndpoints(db, from, to, opts) {
    const { noTests = false } = opts;
    const fromNodes = findMatchingNodes(db, from, {
        noTests,
        file: opts.fromFile,
        kind: opts.kind,
    });
    if (fromNodes.length === 0) {
        return {
            earlyResult: {
                from,
                to,
                found: false,
                error: `No symbol matching "${from}"`,
                fromCandidates: [],
                toCandidates: [],
            },
        };
    }
    const toNodes = findMatchingNodes(db, to, {
        noTests,
        file: opts.toFile,
        kind: opts.kind,
    });
    if (toNodes.length === 0) {
        return {
            earlyResult: {
                from,
                to,
                found: false,
                error: `No symbol matching "${to}"`,
                fromCandidates: fromNodes.slice(0, 5).map(toSymbolRef),
                toCandidates: [],
            },
        };
    }
    const fromCandidates = fromNodes.slice(0, 5).map(toSymbolRef);
    const toCandidates = toNodes.slice(0, 5).map(toSymbolRef);
    return {
        sourceNode: fromNodes[0],
        targetNode: toNodes[0],
        fromCandidates,
        toCandidates,
    };
}
/** Build the SQL statement that yields neighbors of a node id in the requested direction. */
function buildNeighborStmt(db, edgeKinds, reverse) {
    const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
    // Forward: source_id -> target_id (A calls... calls B)
    // Reverse: target_id -> source_id (B is called by... called by A)
    const neighborQuery = reverse
        ? `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
       FROM edges e JOIN nodes n ON e.source_id = n.id
       WHERE e.target_id = ? AND e.kind IN (${kindPlaceholders})`
        : `SELECT n.id, n.name, n.kind, n.file, n.line, e.kind AS edge_kind
       FROM edges e JOIN nodes n ON e.target_id = n.id
       WHERE e.source_id = ? AND e.kind IN (${kindPlaceholders})`;
    return db.prepare(neighborQuery);
}
/** Process a single neighbor row during BFS; returns true once the target has been reached. */
function visitNeighbor(n, currentId, depth, targetId, state, nextQueue, noTests) {
    if (noTests && isTestFile(n.file))
        return;
    if (n.id === targetId) {
        if (!state.found) {
            state.found = true;
            state.foundDepth = depth;
            state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
        }
        state.alternateCount++;
        return;
    }
    if (state.visited.has(n.id))
        return;
    state.visited.add(n.id);
    state.parent.set(n.id, { parentId: currentId, edgeKind: n.edge_kind });
    nextQueue.push(n.id);
}
/**
 * BFS from sourceId toward targetId.
 * Returns { found, parent, alternateCount, foundDepth }.
 * `parent` maps nodeId -> { parentId, edgeKind }.
 */
function bfsShortestPath(db, sourceId, targetId, edgeKinds, reverse, maxDepth, noTests) {
    const neighborStmt = buildNeighborStmt(db, edgeKinds, reverse);
    const state = {
        visited: new Set([sourceId]),
        parent: new Map(),
        found: false,
        foundDepth: -1,
        alternateCount: 0,
    };
    let queue = [sourceId];
    for (let depth = 1; depth <= maxDepth; depth++) {
        const nextQueue = [];
        for (const currentId of queue) {
            const neighbors = neighborStmt.all(currentId, ...edgeKinds);
            for (const n of neighbors) {
                visitNeighbor(n, currentId, depth, targetId, state, nextQueue, noTests);
            }
        }
        if (state.found)
            break;
        queue = nextQueue;
        if (queue.length === 0)
            break;
    }
    return {
        found: state.found,
        parent: state.parent,
        alternateCount: state.alternateCount,
        foundDepth: state.foundDepth,
    };
}
/**
 * Walk the parent map from targetId back to sourceId and return an ordered
 * array of node IDs source -> target.
 */
function reconstructPath(db, pathIds, parent) {
    const nodeCache = new Map();
    const nodeByIdStmt = cachedStmt(_nodeByIdStmtCache, db, 'SELECT name, kind, file, line FROM nodes WHERE id = ?');
    const getNode = (id) => {
        if (nodeCache.has(id))
            return nodeCache.get(id);
        const row = nodeByIdStmt.get(id);
        nodeCache.set(id, row);
        return row;
    };
    return pathIds.map((id, idx) => {
        const node = getNode(id);
        const edgeKind = idx === 0 ? null : parent.get(id).edgeKind;
        return { name: node.name, kind: node.kind, file: node.file, line: node.line, edgeKind };
    });
}
export function pathData(from, to, customDbPath, opts = {}) {
    return withReadonlyDb(customDbPath, (db) => {
        const noTests = opts.noTests || false;
        const maxDepth = opts.maxDepth || 10;
        const edgeKinds = opts.edgeKinds || ['calls'];
        const reverse = opts.reverse || false;
        const resolved = resolveEndpoints(db, from, to, {
            noTests,
            fromFile: opts.fromFile,
            toFile: opts.toFile,
            kind: opts.kind,
        });
        if ('earlyResult' in resolved)
            return resolved.earlyResult;
        const { sourceNode, targetNode, fromCandidates, toCandidates } = resolved;
        // Self-path
        if (sourceNode.id === targetNode.id) {
            return {
                from,
                to,
                fromCandidates,
                toCandidates,
                found: true,
                hops: 0,
                path: [
                    {
                        name: sourceNode.name,
                        kind: sourceNode.kind,
                        file: sourceNode.file,
                        line: sourceNode.line,
                        edgeKind: null,
                    },
                ],
                alternateCount: 0,
                edgeKinds,
                reverse,
                maxDepth,
            };
        }
        const { found, parent, alternateCount: rawAlternateCount, foundDepth, } = bfsShortestPath(db, sourceNode.id, targetNode.id, edgeKinds, reverse, maxDepth, noTests);
        if (!found) {
            return {
                from,
                to,
                fromCandidates,
                toCandidates,
                found: false,
                hops: null,
                path: [],
                alternateCount: 0,
                edgeKinds,
                reverse,
                maxDepth,
            };
        }
        // rawAlternateCount includes the one we kept; subtract 1 for "alternates"
        const alternateCount = Math.max(0, rawAlternateCount - 1);
        // Reconstruct path from target back to source
        const pathIds = [targetNode.id];
        let cur = targetNode.id;
        while (cur !== sourceNode.id) {
            const p = parent.get(cur);
            pathIds.push(p.parentId);
            cur = p.parentId;
        }
        pathIds.reverse();
        const resultPath = reconstructPath(db, pathIds, parent);
        return {
            from,
            to,
            fromCandidates,
            toCandidates,
            found: true,
            hops: foundDepth,
            path: resultPath,
            alternateCount,
            edgeKinds,
            reverse,
            maxDepth,
        };
    });
}
/** Process a neighbor file during file-level BFS; updates state in place. */
function visitFileNeighbor(neighborFile, currentFile, targetFile, state, nextQueue, noTests) {
    if (noTests && isTestFile(neighborFile))
        return;
    if (neighborFile === targetFile) {
        if (!state.found) {
            state.found = true;
            state.parentMap.set(neighborFile, currentFile);
        }
        state.alternateCount++;
        return;
    }
    if (state.visited.has(neighborFile))
        return;
    state.visited.add(neighborFile);
    state.parentMap.set(neighborFile, currentFile);
    nextQueue.push(neighborFile);
}
/** Reconstruct file path from target back to source using parent links. */
function reconstructFilePath(parentMap, sourceFile, targetFile) {
    const filePath = [targetFile];
    let cur = targetFile;
    while (cur !== sourceFile) {
        cur = parentMap.get(cur);
        filePath.push(cur);
    }
    filePath.reverse();
    return filePath;
}
/** BFS over file adjacency graph to find shortest path. */
function bfsFilePath(neighborStmt, sourceFile, targetFile, edgeKinds, maxDepth, noTests) {
    const state = {
        visited: new Set([sourceFile]),
        parentMap: new Map(),
        found: false,
        alternateCount: 0,
    };
    let queue = [sourceFile];
    for (let depth = 1; depth <= maxDepth; depth++) {
        const nextQueue = [];
        for (const currentFile of queue) {
            const neighbors = neighborStmt.all(currentFile, ...edgeKinds);
            for (const n of neighbors) {
                visitFileNeighbor(n.neighbor_file, currentFile, targetFile, state, nextQueue, noTests);
            }
        }
        if (state.found)
            break;
        queue = nextQueue;
        if (queue.length === 0)
            break;
    }
    if (!state.found)
        return { found: false, path: [], alternateCount: 0 };
    return {
        found: true,
        path: reconstructFilePath(state.parentMap, sourceFile, targetFile),
        alternateCount: Math.max(0, state.alternateCount - 1),
    };
}
/**
 * BFS at the file level: find shortest import/edge path between two files.
 * Adjacency: file A → file B if any symbol in A has an edge to any symbol in B.
 */
export function filePathData(from, to, customDbPath, opts = {}) {
    return withReadonlyDb(customDbPath, (db) => {
        const noTests = opts.noTests || false;
        const maxDepth = opts.maxDepth || 10;
        const edgeKinds = opts.edgeKinds || ['imports', 'imports-type'];
        const reverse = opts.reverse || false;
        // Resolve from/to as file paths (LIKE match)
        const fromFiles = findFileNodes(db, `%${from}%`);
        if (fromFiles.length === 0) {
            return {
                from,
                to,
                found: false,
                error: `No file matching "${from}"`,
                path: [],
                fromCandidates: [],
                toCandidates: [],
            };
        }
        const toFiles = findFileNodes(db, `%${to}%`);
        if (toFiles.length === 0) {
            return {
                from,
                to,
                found: false,
                error: `No file matching "${to}"`,
                path: [],
                fromCandidates: fromFiles.slice(0, 5).map((f) => f.file),
                toCandidates: [],
            };
        }
        const sourceFile = fromFiles[0].file;
        const targetFile = toFiles[0].file;
        const fromCandidates = fromFiles.slice(0, 5).map((f) => f.file);
        const toCandidates = toFiles.slice(0, 5).map((f) => f.file);
        if (sourceFile === targetFile) {
            return {
                from,
                to,
                fromCandidates,
                toCandidates,
                found: true,
                hops: 0,
                path: [sourceFile],
                alternateCount: 0,
                edgeKinds,
                reverse,
                maxDepth,
            };
        }
        // Build neighbor query: find all distinct files adjacent to a given file via edges
        const kindPlaceholders = edgeKinds.map(() => '?').join(', ');
        const neighborQuery = reverse
            ? `SELECT DISTINCT n_src.file AS neighbor_file
         FROM nodes n_tgt
         JOIN edges e ON e.target_id = n_tgt.id
         JOIN nodes n_src ON e.source_id = n_src.id
         WHERE n_tgt.file = ? AND e.kind IN (${kindPlaceholders}) AND n_src.file != n_tgt.file`
            : `SELECT DISTINCT n_tgt.file AS neighbor_file
         FROM nodes n_src
         JOIN edges e ON e.source_id = n_src.id
         JOIN nodes n_tgt ON e.target_id = n_tgt.id
         WHERE n_src.file = ? AND e.kind IN (${kindPlaceholders}) AND n_tgt.file != n_src.file`;
        const neighborStmt = db.prepare(neighborQuery);
        // BFS to find shortest file path
        const bfsResult = bfsFilePath(neighborStmt, sourceFile, targetFile, edgeKinds, maxDepth, noTests);
        if (!bfsResult.found) {
            return {
                from,
                to,
                fromCandidates,
                toCandidates,
                found: false,
                hops: null,
                path: [],
                alternateCount: 0,
                edgeKinds,
                reverse,
                maxDepth,
            };
        }
        return {
            from,
            to,
            fromCandidates,
            toCandidates,
            found: true,
            hops: bfsResult.path.length - 1,
            path: bfsResult.path,
            alternateCount: bfsResult.alternateCount,
            edgeKinds,
            reverse,
            maxDepth,
        };
    });
}
//# sourceMappingURL=dependencies.js.map