import path from 'node:path';
import { compareCodeUnits } from '../utils.js';
import { normalizePath } from './languages.js';
import { summarizeObservedCapabilities } from './parser-evidence.js';
import { rankNodesCached } from './rank.js';
import { searchSymbols } from './search.js';
function resolveFileFilter(repoRoot, file) {
    if (!file)
        return undefined;
    return path.isAbsolute(file) ? path.normalize(file) : path.resolve(repoRoot, file);
}
function requireFullContent(index, operation) {
    if (index.contentMode !== 'full') {
        throw new Error(`${operation} requires contentMode "full". Current contentMode is "${index.contentMode}".`);
    }
}
function matchesFile(node, filePath) {
    return !filePath || path.normalize(node.path) === path.normalize(filePath);
}
function compareNodes(a, b) {
    if (a.path !== b.path)
        return compareCodeUnits(a.path, b.path);
    return (a.line ?? 0) - (b.line ?? 0);
}
export function createPayload(index, query, results, meta) {
    return {
        repo: index.repoRoot,
        contentMode: index.contentMode,
        ...(meta ? { meta } : {}),
        query,
        results,
        stats: {
            filesIndexed: index.files.length,
            nodes: index.nodes.length,
            edges: index.edges.length,
            skippedFiles: index.skippedFiles.length,
        },
        capabilities: index.capabilities,
        observedCapabilities: summarizeObservedCapabilities(index),
    };
}
export function findByName(index, name, type, file, limit = 20) {
    const fileFilter = resolveFileFilter(index.repoRoot, file);
    return index.nodes
        .filter(node => node.name === name)
        .filter(node => !type || node.type === type)
        .filter(node => matchesFile(node, fileFilter))
        .sort(compareNodes)
        .slice(0, limit)
        .map(node => ({ node }));
}
export function findByPattern(index, pattern, type, limit = 50) {
    const lowered = pattern.toLowerCase();
    return index.nodes
        .filter(node => node.name.toLowerCase().includes(lowered))
        .filter(node => !type || node.type === type)
        .sort(compareNodes)
        .slice(0, limit)
        .map(node => ({ node }));
}
export function findByType(index, type, limit = 50) {
    return index.nodes
        .filter(node => node.type === type)
        .sort(compareNodes)
        .slice(0, limit)
        .map(node => ({ node }));
}
export function findContent(index, text, limit = 50) {
    requireFullContent(index, 'findContent');
    const lowered = text.toLowerCase();
    const matches = [];
    for (const file of index.files) {
        const lines = file.lines ?? [];
        for (let indexLine = 0; indexLine < lines.length; indexLine++) {
            const line = lines[indexLine];
            if (!line.toLowerCase().includes(lowered))
                continue;
            matches.push({
                path: file.path,
                relativePath: normalizePath(path.relative(index.repoRoot, file.path)),
                language: file.language,
                line: indexLine + 1,
                snippet: line.trim(),
            });
            if (matches.length >= limit)
                return matches;
        }
    }
    return matches;
}
const EMPTY_EDGES = Object.freeze([]);
function edgeKey(type, nodeId) {
    return `${type}\u0000${nodeId}`;
}
// Memoized per index object. Indexes are replaced (never mutated) on
// refresh/incremental update, so a cached graph can never go stale: a new
// index object simply misses the WeakMap and builds its own adjacency, and
// the old graph is collected with the old index. Turns every per-node edge
// lookup from an O(edges) scan into a map hit, which is what keeps repeated
// per-symbol queries (module summaries, class trees) linear instead of
// O(symbols × edges).
const queryGraphCache = new WeakMap();
function queryGraph(index) {
    const cached = queryGraphCache.get(index);
    if (cached)
        return cached;
    const byId = new Map(index.nodes.map(node => [node.id, node]));
    const legacyAliases = new Map();
    for (const node of index.nodes) {
        if (!node.legacyNodeId)
            continue;
        const aliases = legacyAliases.get(node.legacyNodeId) ?? [];
        aliases.push(node);
        legacyAliases.set(node.legacyNodeId, aliases);
    }
    for (const [legacyId, aliases] of legacyAliases) {
        if (aliases.length === 1)
            byId.set(legacyId, aliases[0]);
    }
    const outgoing = new Map();
    const incoming = new Map();
    for (const edge of index.edges) {
        const outKey = edgeKey(edge.type, edge.from);
        const out = outgoing.get(outKey);
        if (out)
            out.push(edge);
        else
            outgoing.set(outKey, [edge]);
        if (edge.to !== undefined) {
            const inKey = edgeKey(edge.type, edge.to);
            const incoming_ = incoming.get(inKey);
            if (incoming_)
                incoming_.push(edge);
            else
                incoming.set(inKey, [edge]);
        }
    }
    const graph = { byId, outgoing, incoming };
    queryGraphCache.set(index, graph);
    return graph;
}
export function nodeById(index) {
    return queryGraph(index).byId;
}
// Callers iterate or spread the returned arrays; they are shared with the
// memoized graph and must not be mutated.
export function outgoingEdges(index, type, nodeId) {
    const graph = queryGraph(index);
    const canonicalId = graph.byId.get(nodeId)?.id ?? nodeId;
    return graph.outgoing.get(edgeKey(type, canonicalId)) ?? EMPTY_EDGES;
}
export function incomingEdges(index, type, nodeId) {
    const graph = queryGraph(index);
    const canonicalId = graph.byId.get(nodeId)?.id ?? nodeId;
    return graph.incoming.get(edgeKey(type, canonicalId)) ?? EMPTY_EDGES;
}
function resolveFunctionNodes(index, name, file) {
    const fileFilter = resolveFileFilter(index.repoRoot, file);
    return index.nodes
        .filter(node => node.type === 'function' && node.name === name)
        .filter(node => matchesFile(node, fileFilter))
        .sort(compareNodes);
}
function resolveCallRelationshipNodes(index, name, file) {
    const fileFilter = resolveFileFilter(index.repoRoot, file);
    return index.nodes
        .filter(node => node.name === name &&
        (node.type === 'function' ||
            (node.type === 'class' && node.language === 'vue' && node.signature === 'Vue component')))
        .filter(node => matchesFile(node, fileFilter))
        .sort(compareNodes);
}
const CLASS_LIKE_TYPES = new Set(['class', 'interface', 'enum']);
function resolveClassNodes(index, name, file) {
    const fileFilter = resolveFileFilter(index.repoRoot, file);
    const matches = index.nodes
        .filter(node => CLASS_LIKE_TYPES.has(node.type) && node.name === name)
        .filter(node => matchesFile(node, fileFilter))
        .sort(compareNodes);
    if (fileFilter)
        return matches;
    const classes = matches.filter(node => node.type === 'class');
    if (classes.length > 0)
        return classes;
    const interfaces = matches.filter(node => node.type === 'interface');
    if (interfaces.length > 0)
        return interfaces;
    return matches;
}
export function analyzeCalls(index, functionName, file) {
    const byId = nodeById(index);
    const matches = [];
    for (const node of resolveCallRelationshipNodes(index, functionName, file)) {
        for (const type of ['calls', 'uses']) {
            for (const edge of outgoingEdges(index, type, node.id)) {
                matches.push({ from: node, edge, to: edge.to ? byId.get(edge.to) : undefined });
            }
        }
    }
    return matches.sort((a, b) => (a.edge.line ?? 0) - (b.edge.line ?? 0));
}
export function analyzeCallers(index, functionName, file) {
    const byId = nodeById(index);
    const matches = [];
    for (const node of resolveCallRelationshipNodes(index, functionName, file)) {
        for (const type of ['calls', 'uses']) {
            for (const edge of incomingEdges(index, type, node.id)) {
                const from = byId.get(edge.from);
                if (from)
                    matches.push({ from, edge, to: node });
            }
        }
    }
    return matches.sort((a, b) => a.from.path !== b.from.path ? compareCodeUnits(a.from.path, b.from.path) : (a.from.line ?? 0) - (b.from.line ?? 0));
}
// The number of acyclic call paths is exponential in `depth` on high-fan-out
// graphs, so the breadth-first search is bounded on total node expansions,
// queued paths, and collected results. The queue is FIFO, so paths are visited
// in non-decreasing length: the shortest chains are still discovered first, and
// hitting a bound returns the partial results found so far rather than hanging.
const MAX_CHAIN_EXPANSIONS = 20_000;
const MAX_CHAIN_QUEUE = 20_000;
const MAX_CHAINS_COLLECTED = 200;
const MAX_BLOCKED_COLLECTED = 200;
function searchCallChain(index, starts, targetIds, depth) {
    const byId = nodeById(index);
    const chains = [];
    const blocked = new Map();
    let expansions = 0;
    for (const start of starts) {
        if (expansions >= MAX_CHAIN_EXPANSIONS || chains.length >= MAX_CHAINS_COLLECTED)
            break;
        const queue = [
            { node: start, pathNodes: [start], pathEdges: [] },
        ];
        while (queue.length > 0) {
            if (expansions >= MAX_CHAIN_EXPANSIONS || chains.length >= MAX_CHAINS_COLLECTED)
                break;
            const current = queue.shift();
            if (!current)
                continue;
            expansions++;
            if (current.pathEdges.length >= depth)
                continue;
            for (const edge of [
                ...outgoingEdges(index, 'calls', current.node.id),
                ...outgoingEdges(index, 'uses', current.node.id),
            ]) {
                if (edge.status === 'ambiguous') {
                    if (blocked.size >= MAX_BLOCKED_COLLECTED)
                        continue;
                    const key = [current.pathNodes.map(node => node.id).join('>'), edge.id].join('|');
                    if (!blocked.has(key)) {
                        blocked.set(key, {
                            nodes: [...current.pathNodes],
                            edges: [...current.pathEdges],
                            status: 'blocked_ambiguous',
                            blockedAt: current.node,
                            blockedBy: edge,
                        });
                    }
                    continue;
                }
                if (!edge.to)
                    continue;
                const next = byId.get(edge.to);
                if (!next || current.pathNodes.some(node => node.id === next.id))
                    continue;
                const nextNodes = [...current.pathNodes, next];
                const nextEdges = [...current.pathEdges, edge];
                if (targetIds.has(next.id)) {
                    chains.push({ nodes: nextNodes, edges: nextEdges, status: 'resolved' });
                    if (chains.length >= MAX_CHAINS_COLLECTED)
                        break;
                    continue;
                }
                if (queue.length < MAX_CHAIN_QUEUE) {
                    queue.push({ node: next, pathNodes: nextNodes, pathEdges: nextEdges });
                }
            }
        }
    }
    return {
        chains: chains.sort((a, b) => a.edges.length - b.edges.length),
        blocked: [...blocked.values()].sort((a, b) => a.nodes.length - b.nodes.length),
    };
}
function markDirection(chains, direction) {
    return chains.map(chain => ({ ...chain, direction }));
}
export function analyzeCallChain(index, fromName, toName, depth = 5, fromFile, toFile) {
    const fromNodes = resolveCallRelationshipNodes(index, fromName, fromFile);
    const toNodes = resolveCallRelationshipNodes(index, toName, toFile);
    const fromIds = new Set(fromNodes.map(node => node.id));
    const toIds = new Set(toNodes.map(node => node.id));
    const forward = searchCallChain(index, fromNodes, toIds, depth);
    if (forward.chains.length > 0) {
        return [...markDirection(forward.chains, 'forward'), ...markDirection(forward.blocked, 'forward')].slice(0, 20);
    }
    const reverse = searchCallChain(index, toNodes, fromIds, depth);
    if (reverse.chains.length > 0) {
        return markDirection(reverse.chains, 'reverse').slice(0, 20);
    }
    return markDirection(forward.blocked, 'forward').slice(0, 20);
}
export function analyzeDeps(index, target) {
    const byId = nodeById(index);
    const normalizedTarget = normalizePath(target.replace(/\\/g, '/'));
    const localFile = index.nodes.find(node => node.type === 'file' &&
        (node.path === target ||
            node.relativePath === normalizedTarget ||
            node.name === target ||
            node.moduleName === target));
    const moduleCandidates = index.nodes.filter(node => node.type === 'module' && (node.name === target || node.path === target || node.relativePath === normalizedTarget));
    const targetPaths = new Set();
    if (localFile)
        targetPaths.add(localFile.path);
    for (const node of moduleCandidates)
        targetPaths.add(node.path);
    const isDirectory = !localFile && targetPaths.size === 0;
    const dirPrefix = isDirectory
        ? normalizedTarget.endsWith('/')
            ? normalizedTarget
            : `${normalizedTarget}/`
        : undefined;
    if (isDirectory) {
        for (const node of index.nodes) {
            if (node.type === 'file' && node.relativePath?.startsWith(dirPrefix)) {
                targetPaths.add(node.path);
            }
        }
    }
    const importers = [];
    const localImports = [];
    const externalImports = [];
    const unresolvedImports = [];
    for (const edge of index.edges.filter(edge => edge.type === 'imports')) {
        const from = byId.get(edge.from);
        const to = edge.to ? byId.get(edge.to) : undefined;
        const fromInside = from != null && targetPaths.has(from.path);
        const toInside = to != null && targetPaths.has(to.path);
        if (isDirectory) {
            if (!fromInside && toInside && from) {
                importers.push({ from, edge, to });
            }
            if (fromInside) {
                const match = { from: from, edge, to };
                if (edge.importKind === 'external') {
                    externalImports.push(match);
                }
                else if (edge.importKind === 'local' && !toInside) {
                    localImports.push(match);
                }
                else if (edge.status === 'unresolved') {
                    unresolvedImports.push(match);
                }
            }
        }
        else {
            if (to && (to.name === target || targetPaths.has(to.path))) {
                if (from)
                    importers.push({ from, edge, to });
            }
            if (from && localFile && from.path === localFile.path) {
                const match = { from, edge, to };
                if (edge.importKind === 'local') {
                    localImports.push(match);
                }
                else if (edge.importKind === 'external') {
                    externalImports.push(match);
                }
                else {
                    unresolvedImports.push(match);
                }
            }
        }
    }
    return {
        target,
        targetFile: localFile,
        importers,
        localImports,
        externalImports,
        unresolvedImports,
    };
}
export function analyzeTree(index, className, file) {
    const byId = nodeById(index);
    const target = resolveClassNodes(index, className, file)[0];
    if (!target)
        return null;
    const parents = [
        ...outgoingEdges(index, 'inherits', target.id),
        ...outgoingEdges(index, 'implements', target.id),
    ].map(edge => ({ from: target, edge, to: edge.to ? byId.get(edge.to) : undefined }));
    const children = [];
    for (const edge of [
        ...incomingEdges(index, 'inherits', target.id),
        ...incomingEdges(index, 'implements', target.id),
    ]) {
        const from = byId.get(edge.from);
        if (from)
            children.push({ from, edge, to: target });
    }
    const methods = index.nodes
        .filter(node => node.type === 'function' && node.containerId === target.id)
        .sort(compareNodes);
    return { target, parents, children, methods };
}
/**
 * Directory-module for a repo-relative path: everything above the file name.
 * Shared by module-coupling analysis and seam enumeration (src/code/seams.ts)
 * so their module grouping cannot diverge. Root-level files map to ''.
 */
export function moduleForRelativePath(relativePath) {
    return relativePath.split('/').slice(0, -1).join('/');
}
export function analyzeModuleCoupling(index, modulePath) {
    const normalizedModule = normalizePath(modulePath.replace(/\\/g, '/'));
    const dirPrefix = normalizedModule.endsWith('/') ? normalizedModule : `${normalizedModule}/`;
    const includeAll = normalizedModule === '.' || normalizedModule === '';
    const byId = nodeById(index);
    // Find all files belonging to this module
    const moduleFilePaths = new Set();
    for (const node of index.nodes) {
        if (node.type === 'file' &&
            (includeAll ||
                node.relativePath === normalizedModule ||
                node.moduleName === normalizedModule ||
                node.relativePath?.startsWith(dirPrefix))) {
            moduleFilePaths.add(node.path);
        }
    }
    // Count public symbols (exported)
    let publicSymbolCount = 0;
    for (const file of index.files) {
        if (!moduleFilePaths.has(file.path))
            continue;
        for (const sym of file.symbols) {
            if (sym.exported)
                publicSymbolCount++;
        }
    }
    // Afferent coupling: external modules that import from this module
    const importerModules = new Set();
    const importerDetails = [];
    // Efferent coupling: external modules this module imports from
    const dependencyModules = new Set();
    const dependencyDetails = [];
    for (const edge of index.edges) {
        if (edge.type !== 'imports')
            continue;
        const fromFile = byId.get(edge.from);
        const toFile = edge.to ? byId.get(edge.to) : undefined;
        if (!fromFile)
            continue;
        const fromInside = moduleFilePaths.has(fromFile.path);
        const toInside = toFile ? moduleFilePaths.has(toFile.path) : false;
        if (!fromInside && toInside) {
            // External file imports from our module
            const fromModule = moduleForRelativePath(fromFile.relativePath ?? '');
            if (!importerModules.has(fromModule)) {
                importerModules.add(fromModule);
                importerDetails.push(fromModule || (fromFile.relativePath ?? fromFile.name));
            }
        }
        if (fromInside && !toInside && toFile && edge.importKind !== 'external') {
            // Our module imports from external internal module
            const toModule = moduleForRelativePath(toFile.relativePath ?? '');
            if (!dependencyModules.has(toModule)) {
                dependencyModules.add(toModule);
                dependencyDetails.push(toModule || (toFile.relativePath ?? toFile.name));
            }
        }
    }
    const ca = importerModules.size;
    const ce = dependencyModules.size;
    const instability = ca + ce > 0 ? ce / (ca + ce) : 0;
    // Key classes
    const classNodes = index.nodes.filter(n => n.type === 'class' && moduleFilePaths.has(n.path));
    const keyClasses = classNodes.map(classNode => {
        const tree = analyzeTree(index, classNode.name, classNode.relativePath);
        return {
            name: classNode.name,
            parents: tree?.parents.map(p => p.to?.name ?? p.edge.targetName ?? '') ?? [],
            childCount: tree?.children.length ?? 0,
            methodCount: tree?.methods.length ?? 0,
        };
    });
    return {
        module: modulePath,
        publicSymbolCount,
        afferentCoupling: ca,
        efferentCoupling: ce,
        instability: Math.round(instability * 1000) / 1000,
        topImporters: importerDetails.slice(0, 10),
        topDependencies: dependencyDetails.slice(0, 10),
        keyClasses: keyClasses.sort((a, b) => b.methodCount + b.childCount - (a.methodCount + a.childCount)).slice(0, 10),
    };
}
function moduleFilePaths(index, modulePath) {
    const normalizedModule = normalizePath(modulePath.replace(/\\/g, '/'));
    const dirPrefix = normalizedModule.endsWith('/') ? normalizedModule : `${normalizedModule}/`;
    const includeAll = normalizedModule === '.' || normalizedModule === '';
    const paths = new Set();
    for (const node of index.nodes) {
        if (node.type !== 'file')
            continue;
        if (includeAll ||
            node.relativePath === normalizedModule ||
            node.moduleName === normalizedModule ||
            node.relativePath?.startsWith(dirPrefix)) {
            paths.add(node.path);
        }
    }
    return paths;
}
function exportedSymbolsForModule(index, modulePaths) {
    const symbolKeys = new Set();
    for (const file of index.files) {
        if (!modulePaths.has(file.path))
            continue;
        for (const symbol of file.symbols) {
            if (!symbol.exported)
                continue;
            symbolKeys.add([symbol.type, file.path, symbol.name, symbol.line, symbol.containerName ?? ''].join('|'));
        }
    }
    return index.nodes
        .filter(node => symbolKeys.has([node.type, node.path, node.name, node.line, node.containerName ?? ''].join('|')))
        .sort(compareNodes);
}
export function summarizeModule(index, modulePath, options = {}) {
    const maxClasses = options.maxClasses ?? 10;
    const maxFunctions = options.maxFunctions ?? 20;
    const maxEdges = options.maxEdges ?? 40;
    const coupling = analyzeModuleCoupling(index, modulePath);
    const byId = nodeById(index);
    const paths = moduleFilePaths(index, modulePath);
    const exportedNodes = exportedSymbolsForModule(index, paths);
    const keyFunctions = exportedNodes
        .filter(node => node.type === 'function')
        .map(node => ({
        name: node.name,
        relativePath: node.relativePath,
        line: node.line,
        signature: node.signature,
        exported: true,
        outgoingCalls: outgoingEdges(index, 'calls', node.id).length,
        incomingCalls: incomingEdges(index, 'calls', node.id).length,
    }))
        .sort((left, right) => right.outgoingCalls + right.incomingCalls - (left.outgoingCalls + left.incomingCalls))
        .slice(0, maxFunctions);
    const importEdges = index.edges
        .filter(edge => edge.type === 'imports')
        .filter(edge => {
        const from = byId.get(edge.from);
        return from ? paths.has(from.path) : false;
    })
        .slice(0, maxEdges)
        .map(edge => {
        const from = byId.get(edge.from);
        const to = edge.to ? byId.get(edge.to) : undefined;
        return {
            from: from?.relativePath ?? edge.fromPath,
            to: to?.relativePath ?? edge.toPath,
            specifier: edge.specifier,
            status: edge.status,
            resolutionBasis: edge.resolutionBasis,
            candidates: edge.candidates,
        };
    });
    const exportedApi = exportedNodes.slice(0, maxFunctions + maxClasses).map(node => ({
        name: node.name,
        type: node.type,
        relativePath: node.relativePath,
        line: node.line,
        signature: node.signature,
    }));
    return {
        module: modulePath,
        coupling,
        keyClasses: coupling.keyClasses.slice(0, maxClasses),
        keyFunctions,
        importEdges,
        exportedApi,
    };
}
function mermaidId(value) {
    const normalized = value.replace(/[^a-zA-Z0-9_]/g, '_').replace(/^_+/, '');
    return normalized ? `n_${normalized}` : 'n_node';
}
function mermaidLabel(value) {
    return value.replace(/"/g, '\\"');
}
function relativeNodeLabel(node) {
    const suffix = node.line ? `:${node.line}` : '';
    return node.relativePath ? `${node.name}\\n${node.relativePath}${suffix}` : node.name;
}
function emptyMermaid(kind) {
    return [
        kind === 'class-hierarchy' ? 'classDiagram' : 'graph LR',
        kind === 'class-hierarchy' ? '  class Empty' : '  empty["No edges found"]',
    ].join('\n');
}
export function toMermaid(index, kind, target, options = {}) {
    const byId = nodeById(index);
    const maxEdges = options.maxEdges ?? 40;
    const maxNodes = options.maxNodes ?? 30;
    if (kind === 'import-graph') {
        const paths = moduleFilePaths(index, target);
        const lines = ['graph LR'];
        const edges = index.edges
            .filter(edge => edge.type === 'imports')
            .filter(edge => {
            const from = byId.get(edge.from);
            const to = edge.to ? byId.get(edge.to) : undefined;
            return !!from && paths.has(from.path) && !!to;
        })
            .slice(0, maxEdges);
        if (edges.length === 0)
            return emptyMermaid(kind);
        for (const edge of edges) {
            const from = byId.get(edge.from);
            const to = edge.to ? byId.get(edge.to) : undefined;
            if (!from || !to)
                continue;
            lines.push(`  ${mermaidId(from.id)}["${mermaidLabel(from.relativePath ?? from.name)}"] --> ${mermaidId(to.id)}["${mermaidLabel(to.relativePath ?? to.name)}"]`);
        }
        return lines.join('\n');
    }
    if (kind === 'class-hierarchy') {
        const lines = ['classDiagram'];
        const paths = moduleFilePaths(index, target);
        const classNodes = paths.size > 0
            ? index.nodes.filter(node => node.type === 'class' && paths.has(node.path)).slice(0, maxNodes)
            : resolveClassNodes(index, target).slice(0, maxNodes);
        if (classNodes.length === 0)
            return emptyMermaid(kind);
        const emitted = new Set();
        for (const classNode of classNodes) {
            lines.push(`  class ${mermaidId(classNode.id)}["${mermaidLabel(classNode.name)}"]`);
            emitted.add(classNode.id);
            const tree = analyzeTree(index, classNode.name, classNode.relativePath);
            for (const parent of tree?.parents ?? []) {
                const parentName = parent.to?.name ?? parent.edge.targetName;
                if (!parentName)
                    continue;
                const parentId = mermaidId(parent.to?.id ?? parentName);
                if (!emitted.has(parent.to?.id ?? parentName)) {
                    lines.push(`  class ${parentId}["${mermaidLabel(parentName)}"]`);
                    emitted.add(parent.to?.id ?? parentName);
                }
                lines.push(`  ${parentId} <|-- ${mermaidId(classNode.id)}`);
            }
            for (const method of tree?.methods.slice(0, 12) ?? []) {
                lines.push(`  ${mermaidId(classNode.id)} : ${mermaidLabel(method.name)}()`);
            }
        }
        return lines.join('\n');
    }
    const targetFunctions = resolveCallRelationshipNodes(index, target).slice(0, maxNodes);
    const paths = moduleFilePaths(index, target);
    const seeds = targetFunctions.length > 0
        ? targetFunctions
        : index.nodes
            .filter(node => paths.has(node.path) &&
            (node.type === 'function' ||
                (node.type === 'class' && node.language === 'vue' && node.signature === 'Vue component')))
            .slice(0, maxNodes);
    const lines = ['graph LR'];
    let emittedEdges = 0;
    for (const seed of seeds) {
        for (const edge of [...outgoingEdges(index, 'calls', seed.id), ...outgoingEdges(index, 'uses', seed.id)]) {
            if (emittedEdges >= maxEdges)
                break;
            const targetNode = edge.to ? byId.get(edge.to) : undefined;
            if (!targetNode)
                continue;
            lines.push(`  ${mermaidId(seed.id)}["${mermaidLabel(relativeNodeLabel(seed))}"] --> ${mermaidId(targetNode.id)}["${mermaidLabel(relativeNodeLabel(targetNode))}"]`);
            emittedEdges++;
        }
    }
    return emittedEdges > 0 ? lines.join('\n') : emptyMermaid(kind);
}
/** Fused search: runs name, pattern, and content search, merges via Reciprocal Rank Fusion */
export function fusedSearch(index, query, options = {}) {
    const { weights = {}, limit = 50 } = options;
    const nameWeight = weights.name ?? 3;
    const patternWeight = weights.pattern ?? 2;
    const contentWeight = weights.content ?? 1;
    const symbolsWeight = weights.symbols ?? 0;
    const RRF_K = 60;
    if (contentWeight > 0) {
        requireFullContent(index, 'fusedSearch');
    }
    // Run all three searches
    const nameResults = findByName(index, query, undefined, undefined, 100);
    const patternResults = findByPattern(index, query, undefined, 200);
    const contentResults = contentWeight > 0 ? findContent(index, query, 200) : [];
    const filesByPath = new Map(index.files.map(file => [file.path, file]));
    function compactExcerpt(value, maxChars = 600) {
        const excerpt = value
            ?.split('\n')
            .map(line => line.trimEnd())
            .filter(line => line.trim().length > 0)
            .join('\n')
            .trim();
        return excerpt ? excerpt.slice(0, maxChars) : undefined;
    }
    function getNodeExcerpt(node) {
        const file = filesByPath.get(node.path);
        if (file?.lines && node.line) {
            const startLine = Math.max(0, node.line - 3);
            const endLine = Math.min(file.lines.length, node.line + 18);
            const excerpt = compactExcerpt(file.lines.slice(startLine, endLine).join('\n'));
            if (excerpt)
                return excerpt;
        }
        const fileExcerpt = compactExcerpt(file?.excerpt?.content);
        if (fileExcerpt)
            return fileExcerpt;
        return node.signature ?? node.name;
    }
    // Build fused scores by node identity
    const scores = new Map();
    function ensureEntry(key, node) {
        const existing = scores.get(key);
        if (existing)
            return existing;
        const entry = {
            nodeId: key,
            path: node.path,
            relativePath: node.relativePath,
            name: node.name,
            type: node.type ?? 'function',
            line: node.line,
            endLine: node.endLine,
            excerpt: node.excerpt,
            signature: node.signature,
            containerName: node.containerName,
            language: node.language,
            score: 0,
            sources: [],
        };
        scores.set(key, entry);
        return entry;
    }
    // Name results — weight by rank
    for (let i = 0; i < nameResults.length; i++) {
        const r = nameResults[i];
        const key = r.node.id;
        const entry = ensureEntry(key, {
            ...r.node,
            excerpt: getNodeExcerpt(r.node),
        });
        entry.score += nameWeight / (RRF_K + i + 1);
        entry.sources.push({ method: 'name', rank: i + 1 });
    }
    // Pattern results — weight by rank
    for (let i = 0; i < patternResults.length; i++) {
        const r = patternResults[i];
        const key = r.node.id;
        const entry = ensureEntry(key, {
            ...r.node,
            excerpt: getNodeExcerpt(r.node),
        });
        entry.score += patternWeight / (RRF_K + i + 1);
        entry.sources.push({ method: 'pattern', rank: i + 1 });
    }
    // Symbols arm (opt-in): the camelCase-aware subsequence scorer, merged by
    // node id. Symbols without graph linkage are skipped rather than duplicated
    // under a synthetic key — they stay reachable via the name/pattern arms.
    if (symbolsWeight > 0) {
        const nodesById = new Map(index.nodes.map(node => [node.id, node]));
        const symbolResults = searchSymbols(index, query, { limit: 200 });
        for (let i = 0; i < symbolResults.length; i++) {
            const r = symbolResults[i];
            if (!r.nodeId)
                continue;
            const node = nodesById.get(r.nodeId);
            if (!node)
                continue;
            const entry = ensureEntry(r.nodeId, { ...node, excerpt: getNodeExcerpt(node) });
            entry.score += symbolsWeight / (RRF_K + i + 1);
            entry.sources.push({ method: 'symbols', rank: i + 1 });
            entry.symbolMatch ??= { field: r.field, matchedText: r.matchedText, positions: r.positions, span: r.span };
        }
    }
    // Content results — use file:line as key since these aren't nodes
    for (let i = 0; i < contentResults.length; i++) {
        const r = contentResults[i];
        const key = `content:${r.path}:${r.line}`;
        const entry = ensureEntry(key, {
            path: r.path,
            relativePath: r.relativePath,
            name: r.snippet.slice(0, 60),
            type: 'variable',
            line: r.line,
            excerpt: r.snippet,
            language: r.language,
        });
        entry.score += contentWeight / (RRF_K + i + 1);
        entry.sources.push({ method: 'content', rank: i + 1 });
    }
    // PageRank blend (opt-in): a bounded multiplier on the fused score, exactly
    // the hotspots formula. Node hits use their own normalized rank; synthetic
    // content rows use their file's, so the content channel is not
    // systematically deflated. Rows the ranking does not cover keep factor 1
    // and carry no pageRank field.
    if (options.pageRank === true) {
        const ranking = rankNodesCached(index);
        const nodeScores = new Map(ranking.nodes.map(node => [node.nodeId, node.normalizedScore]));
        const fileScores = new Map(ranking.fileScores.map(file => [file.path, file.normalizedScore]));
        for (const entry of scores.values()) {
            const normalized = nodeScores.get(entry.nodeId) ?? fileScores.get(entry.path);
            if (normalized === undefined)
                continue;
            entry.pageRank = normalized;
            entry.score *= 1 + normalized / 100;
        }
    }
    return [...scores.values()].sort((a, b) => b.score - a.score).slice(0, limit);
}
/** Select the most architecturally significant portion of a file */
export function extractKeySection(file, maxChars) {
    if (file.excerpt && (file.content === undefined || !file.lines)) {
        return {
            content: file.excerpt.content.slice(0, maxChars),
            startLine: file.excerpt.startLine,
            endLine: file.excerpt.endLine,
            reason: file.excerpt.reason,
        };
    }
    const lines = file.lines;
    const totalContent = file.content;
    if (!lines || totalContent === undefined) {
        throw new Error(`extractKeySection requires excerpt or full content for ${file.relativePath}`);
    }
    if (totalContent.length <= maxChars) {
        return { content: totalContent, startLine: 1, endLine: lines.length, reason: 'full file' };
    }
    // Prioritize: exported class definitions > exported functions > public API surface > file start
    const exportedSymbols = file.symbols.filter(s => s.exported);
    const exportedClasses = exportedSymbols.filter(s => s.type === 'class');
    const exportedFunctions = exportedSymbols.filter(s => s.type === 'function' && !s.containerName);
    // Try exported class first (including its methods)
    if (exportedClasses.length > 0) {
        const cls = exportedClasses[0];
        const section = extractSymbolRegion(lines, cls.line - 1, maxChars);
        if (section)
            return { ...section, reason: `exported class ${cls.name}` };
    }
    // Try exported functions
    if (exportedFunctions.length > 0) {
        const startLine = exportedFunctions[0].line - 1;
        const section = extractSymbolRegion(lines, startLine, maxChars);
        if (section)
            return { ...section, reason: `exported functions starting at ${exportedFunctions[0].name}` };
    }
    // Try all exported symbols
    if (exportedSymbols.length > 0) {
        const startLine = exportedSymbols[0].line - 1;
        const section = extractSymbolRegion(lines, startLine, maxChars);
        if (section)
            return { ...section, reason: 'public API surface' };
    }
    // Fallback: first maxChars of file
    return extractFromStart(lines, maxChars);
}
function extractSymbolRegion(lines, startLineIdx, maxChars) {
    // Include a few lines before for context (imports/JSDoc)
    const contextStart = Math.max(0, startLineIdx - 3);
    let chars = 0;
    let endIdx = contextStart;
    for (let i = contextStart; i < lines.length; i++) {
        chars += lines[i].length + 1;
        endIdx = i;
        if (chars >= maxChars)
            break;
    }
    const content = lines.slice(contextStart, endIdx + 1).join('\n');
    return { content, startLine: contextStart + 1, endLine: endIdx + 1 };
}
function extractFromStart(lines, maxChars) {
    let chars = 0;
    let endIdx = 0;
    for (let i = 0; i < lines.length; i++) {
        chars += lines[i].length + 1;
        endIdx = i;
        if (chars >= maxChars)
            break;
    }
    return {
        content: lines.slice(0, endIdx + 1).join('\n'),
        startLine: 1,
        endLine: endIdx + 1,
        reason: 'file start',
    };
}
//# sourceMappingURL=query.js.map