import os from 'node:os';
import path from 'node:path';
import { stat } from 'node:fs/promises';
import { z } from 'zod';
import { discoverCodeFiles } from './discover.js';
import { getLanguageSpec, getModuleName, normalizePath } from './languages.js';
import { parseCodeFile } from './parsers.js';
import { extractKeySection } from './query.js';
import { ContentModeSchema, } from './types.js';
import { abortIfNeeded, createYielder, isCooperative, mapWithConcurrency, } from '../utils.js';
export const BudgetExceededBehaviorSchema = z.enum(['throw', 'truncate']);
export const BuildCodebaseOptionsSchema = z.object({
    repoRoot: z.string(),
    excludeDir: z.array(z.string()).optional(),
    noGitignore: z.boolean().optional(),
    ignorePatterns: z.array(z.string()).optional(),
    overlayIgnorePatterns: z.array(z.string()).optional(),
    contentMode: z.enum(['none', 'excerpt', 'full']).optional(),
    maxFileSizeBytes: z.number().optional(),
    maxLines: z.number().optional(),
    skipMinified: z.boolean().optional(),
    maxFiles: z.number().int().positive().optional(),
    maxBytes: z.number().int().positive().optional(),
    onBudgetExceeded: BudgetExceededBehaviorSchema.optional(),
    parseConcurrency: z.number().int().positive().optional(),
});
/**
 * Default fan-out for parseCodeFiles. parseCodeFile is async I/O (stat + read)
 * followed by a synchronous CPU parse; a small worker pool hides the I/O
 * latency while bounding the memory held by in-flight file contents.
 * worker_threads are explicitly out of scope: each worker would pay the
 * typescript module load cost (dwarfing small incremental updates) and
 * full-mode payloads would pay structured-clone overhead;
 * buildCodebaseIndexIsolated already provides process-level isolation.
 */
export const DEFAULT_PARSE_CONCURRENCY = Math.min(8, Math.max(2, 
// os.availableParallelism is missing on Node 18.0-18.13; fall back to cpus().
typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length));
export const ParseCodeFilesOptionsSchema = z.object({
    repoRoot: z.string(),
    contentMode: ContentModeSchema.optional(),
    maxFileSizeBytes: z.number().optional(),
    maxLines: z.number().optional(),
    skipMinified: z.boolean().optional(),
    concurrency: z.number().int().positive().optional(),
});
export class CodeIndexBudgetExceededError extends Error {
    name = 'CodeIndexBudgetExceededError';
    code = 'OCC_CODE_INDEX_BUDGET_EXCEEDED';
    budget;
    constructor(budget) {
        const reasons = [
            budget.maxFiles != null && budget.totalFiles > budget.maxFiles
                ? `files ${budget.totalFiles}/${budget.maxFiles}`
                : undefined,
            budget.maxBytes != null && budget.totalBytes > budget.maxBytes
                ? `bytes ${budget.totalBytes}/${budget.maxBytes}`
                : undefined,
        ]
            .filter(Boolean)
            .join(', ');
        super(`Code index budget exceeded (${reasons}).`);
        this.budget = budget;
    }
}
function makeId(prefix, ...parts) {
    return [prefix, ...parts.filter((part) => part != null)].join(':');
}
function mapPush(map, key, value) {
    const existing = map.get(key);
    if (existing)
        existing.push(value);
    else
        map.set(key, [value]);
}
function mergeCapabilities(files) {
    const capabilities = {};
    for (const file of files) {
        const spec = getLanguageSpec(file.path);
        if (!spec)
            continue;
        capabilities[spec.name] = spec.capabilities;
    }
    return capabilities;
}
function applyContentMode(file, contentMode) {
    if (contentMode === 'full') {
        return file;
    }
    if (contentMode === 'excerpt') {
        const excerpt = extractKeySection(file, 4_000);
        return {
            ...file,
            content: undefined,
            lines: undefined,
            excerpt,
        };
    }
    return {
        ...file,
        content: undefined,
        lines: undefined,
        excerpt: undefined,
    };
}
async function withFileSizes(filePaths, signal) {
    const statted = await mapWithConcurrency(filePaths, 32, async (filePath) => {
        try {
            const fileStat = await stat(filePath);
            return fileStat.isFile() ? { path: filePath, size: fileStat.size, mtimeMs: fileStat.mtimeMs } : null;
        }
        catch {
            // Files can disappear between discovery and stat; they are dropped from
            // the parse list here (parseCodeFiles likewise drops files that vanish
            // before parsing).
            return null;
        }
    }, signal);
    return statted.filter((file) => file !== null);
}
/**
 * Filesystem failures parseCodeFiles absorbs per file instead of rejecting
 * the whole batch: the file vanished between listing and parse (ENOENT,
 * ENOTDIR) or became unreadable (EACCES, EPERM).
 */
const RECOVERABLE_PARSE_FS_CODES = new Set(['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM']);
function isRecoverableParseFsError(error) {
    return (error instanceof Error &&
        'code' in error &&
        typeof error.code === 'string' &&
        RECOVERABLE_PARSE_FS_CODES.has(error.code));
}
function enforceBuildBudget(discovered, options) {
    const totalFiles = discovered.length;
    const totalBytes = discovered.reduce((sum, file) => sum + file.size, 0);
    const overFiles = options.maxFiles != null && totalFiles > options.maxFiles;
    const overBytes = options.maxBytes != null && totalBytes > options.maxBytes;
    if (!overFiles && !overBytes) {
        return { files: discovered.map(file => file.path) };
    }
    if ((options.onBudgetExceeded ?? 'throw') === 'throw') {
        throw new CodeIndexBudgetExceededError({
            maxFiles: options.maxFiles,
            maxBytes: options.maxBytes,
            totalFiles,
            totalBytes,
        });
    }
    const retained = [];
    let retainedBytes = 0;
    for (const file of discovered) {
        if (options.maxFiles != null && retained.length >= options.maxFiles)
            continue;
        if (options.maxBytes != null && retainedBytes + file.size > options.maxBytes)
            continue;
        retained.push(file);
        retainedBytes += file.size;
    }
    const reason = overFiles && overBytes ? 'maxFilesAndBytes' : overFiles ? 'maxFiles' : 'maxBytes';
    return {
        files: retained.map(file => file.path),
        truncated: {
            reason,
            keptFiles: retained.length,
            droppedFiles: totalFiles - retained.length,
            totalFiles,
            totalBytes,
            maxFiles: options.maxFiles,
            maxBytes: options.maxBytes,
        },
    };
}
function toCandidates(nodes) {
    if (!nodes || nodes.length === 0)
        return undefined;
    return nodes.map(node => ({
        name: node.name,
        relativePath: node.relativePath,
        line: node.line,
    }));
}
const GLOBAL_BUILTIN_CALLS = new Set([
    'Array',
    'Boolean',
    'Date',
    'Error',
    'JSON',
    'Map',
    'Number',
    'Object',
    'Promise',
    'Reflect',
    'RegExp',
    'Set',
    'String',
    'Symbol',
    'console',
    'decodeURIComponent',
    'encodeURIComponent',
    'fetch',
    'isFinite',
    'isNaN',
    'parseFloat',
    'parseInt',
    'require',
    'setInterval',
    'setTimeout',
]);
const MEMBER_BUILTIN_CALLS = new Set([
    'add',
    'append',
    'appendChild',
    'assign',
    'at',
    'catch',
    'charAt',
    'clear',
    'concat',
    'delete',
    'entries',
    'every',
    'filter',
    'find',
    'findIndex',
    'flat',
    'forEach',
    'get',
    'has',
    'includes',
    'indexOf',
    'join',
    'keys',
    'log',
    'map',
    'match',
    'pop',
    'push',
    'reduce',
    'replace',
    'resolve',
    'reverse',
    'set',
    'shift',
    'slice',
    'some',
    'sort',
    'split',
    'startsWith',
    'substring',
    'then',
    'toISOString',
    'toLowerCase',
    'toString',
    'toUpperCase',
    'trim',
    'values',
]);
function isBuiltinNoiseCall(call, bindings, localSymbols) {
    if (!call.qualifier) {
        return localSymbols.length === 0 && !bindings.has(call.calleeName) && GLOBAL_BUILTIN_CALLS.has(call.calleeName);
    }
    if (call.qualifier === 'this' ||
        call.qualifier === 'self' ||
        call.qualifier === 'cls' ||
        call.qualifier === 'super') {
        return false;
    }
    return !bindings.has(call.qualifier) && MEMBER_BUILTIN_CALLS.has(call.calleeName);
}
/**
 * Parse `files` with bounded concurrency. Both result arrays are returned in
 * input order (per-slot results are compacted after the pool drains).
 * Content-mode reduction happens at parse time, while content is still in
 * memory — excerpt extraction needs the full source text.
 */
export async function parseCodeFiles(files, options, onProgress, signal, yielder) {
    const repoRoot = options.repoRoot;
    const contentMode = options.contentMode ?? 'excerpt';
    const concurrency = options.concurrency ?? DEFAULT_PARSE_CONCURRENCY;
    // Shared monotonic counter — completion order, not slot order.
    let done = 0;
    const slots = await mapWithConcurrency(files, concurrency, async (filePath) => {
        let slot;
        try {
            const parsed = await parseCodeFile(filePath, { repoRoot }, {
                maxFileSizeBytes: options.maxFileSizeBytes,
                maxLines: options.maxLines,
                skipMinified: options.skipMinified,
            });
            if (parsed?.kind === 'parsed') {
                slot = { kind: 'parsed', file: applyContentMode(parsed.file, contentMode) };
            }
            else if (parsed?.kind === 'skipped') {
                slot = {
                    kind: 'skipped',
                    skipped: {
                        path: filePath,
                        relativePath: normalizePath(path.relative(repoRoot, filePath)),
                        reason: parsed.reason,
                    },
                };
            }
            else {
                slot = { kind: 'none' };
            }
        }
        catch (error) {
            // A file deleted (or made unreadable) between listing and parse must
            // not reject the batch: drop its slot — the file simply vanishes from
            // the inputs and the next manifest diff reconciles it.
            if (!isRecoverableParseFsError(error))
                throw error;
            slot = { kind: 'none' };
        }
        done += 1;
        onProgress?.({ phase: 'parse', total: files.length, completed: done, detail: filePath });
        if (yielder)
            await yielder.tick();
        return slot;
    }, signal);
    const parsedFiles = [];
    const skippedFiles = [];
    for (const slot of slots) {
        if (slot.kind === 'parsed')
            parsedFiles.push(slot.file);
        else if (slot.kind === 'skipped')
            skippedFiles.push(slot.skipped);
    }
    return { parsedFiles, skippedFiles };
}
export async function buildCodebaseIndex(options, onProgress, signal) {
    const { index } = await buildCodebaseIndexWithStats(options, onProgress, signal);
    return index;
}
/**
 * Like {@link buildCodebaseIndex}, but also returns the pre-parse stat
 * snapshot of the budgeted file set so callers persisting a freshness
 * manifest don't have to re-stat every file after the build.
 */
export async function buildCodebaseIndexWithStats(options, onProgress, signal) {
    const repoRoot = path.resolve(options.repoRoot);
    const contentMode = options.contentMode ?? 'excerpt';
    const cooperative = isCooperative(options.cooperative);
    const yielder = cooperative ? createYielder(options.cooperative, signal) : undefined;
    abortIfNeeded(signal);
    const discovered = await discoverCodeFiles(repoRoot, {
        excludeDir: options.excludeDir,
        noGitignore: options.noGitignore,
        ignorePatterns: options.ignorePatterns,
        overlayIgnorePatterns: options.overlayIgnorePatterns,
        signal,
    });
    abortIfNeeded(signal);
    const discoveredWithSizes = await withFileSizes(discovered, signal);
    const budgeted = enforceBuildBudget(discoveredWithSizes, options);
    onProgress?.({
        phase: 'discover',
        total: discovered.length,
        completed: discovered.length,
        detail: 'File discovery complete',
    });
    const { parsedFiles, skippedFiles } = await parseCodeFiles(budgeted.files, {
        repoRoot,
        contentMode,
        maxFileSizeBytes: options.maxFileSizeBytes,
        maxLines: options.maxLines,
        skipMinified: options.skipMinified,
        concurrency: options.parseConcurrency,
    }, onProgress, signal, yielder);
    abortIfNeeded(signal);
    const input = {
        repoRoot,
        contentMode,
        parsedFiles,
        skippedFiles,
        truncated: budgeted.truncated,
    };
    const index = yielder
        ? await assembleCodebaseIndexCooperative(input, yielder, onProgress, signal)
        : assembleCodebaseIndex(input, onProgress, signal);
    const budgetedPaths = new Set(budgeted.files);
    const fileStats = discoveredWithSizes
        .filter(file => budgetedPaths.has(file.path))
        .map(file => ({
        relativePath: normalizePath(path.relative(repoRoot, file.path)),
        size: file.size,
        mtimeMs: file.mtimeMs,
    }));
    return { index, fileStats };
}
/**
 * Assemble the node/edge graph from already-parsed files. Synchronous —
 * assembly has no awaits — so it can be reused verbatim by incremental
 * updates that splice fresh parses into a cached file set.
 */
function* assembleSteps(input, onProgress, signal) {
    const { repoRoot, contentMode, parsedFiles, skippedFiles } = input;
    const nodes = [];
    const nodeById = new Map();
    const edges = [];
    const functionsByName = new Map();
    const classNodesByName = new Map();
    const interfaceNodesByName = new Map();
    const filesByPath = new Map();
    const modulesByName = new Map();
    const topLevelSymbolsByFile = new Map();
    const allPaths = new Set(parsedFiles.map(file => file.path));
    const moduleNodeByPath = new Map();
    const symbolByIdentity = new Map();
    const classByFileAndName = new Map();
    const methodNodesByClassId = new Map();
    const parentNamesByClassId = new Map();
    const classKey = (filePath, className) => `${filePath}::${className}`;
    const pushNode = (node) => {
        nodes.push(node);
        nodeById.set(node.id, node);
    };
    for (const file of parsedFiles) {
        abortIfNeeded(signal);
        const fileNode = {
            id: makeId('file', file.path),
            type: 'file',
            name: path.basename(file.path),
            path: file.path,
            relativePath: file.relativePath,
            line: 1,
            language: file.language,
            moduleName: file.moduleName,
        };
        const moduleNode = {
            id: makeId('module', file.path),
            type: 'module',
            name: file.moduleName,
            path: file.path,
            relativePath: file.relativePath,
            line: 1,
            language: file.language,
            moduleName: file.moduleName,
        };
        pushNode(fileNode);
        pushNode(moduleNode);
        filesByPath.set(file.path, fileNode);
        moduleNodeByPath.set(file.path, moduleNode);
        mapPush(modulesByName, file.moduleName, moduleNode);
        mapPush(modulesByName, path.basename(file.moduleName), moduleNode);
        edges.push({
            id: makeId('contains', fileNode.id, moduleNode.id),
            type: 'contains',
            from: fileNode.id,
            to: moduleNode.id,
            fromPath: file.path,
            toPath: file.path,
            status: 'resolved',
            targetName: moduleNode.name,
        });
        const symbolIndex = new Map();
        for (const symbol of file.symbols) {
            // Containers can be classes or interface-like types (interfaces, traits,
            // protocols). Rebuild the container id exactly as the container node's own
            // id is constructed below so member `contains` edges resolve to a real node.
            const container = symbol.containerName
                ? file.symbols.find(candidate => (candidate.type === 'class' || candidate.type === 'interface') && candidate.name === symbol.containerName)
                : undefined;
            const containerId = container
                ? makeId(container.type, file.path, container.name, container.line, container.containerName)
                : moduleNode.id;
            const node = {
                id: makeId(symbol.type, file.path, symbol.name, symbol.line, symbol.containerName),
                type: symbol.type,
                name: symbol.name,
                path: file.path,
                relativePath: file.relativePath,
                line: symbol.line,
                endLine: symbol.endLine,
                startColumn: symbol.startColumn,
                endColumn: symbol.endColumn,
                language: file.language,
                moduleName: file.moduleName,
                containerId,
                containerName: symbol.containerName,
                signature: symbol.signature,
            };
            pushNode(node);
            // Stamp the graph linkage onto the file symbol so index consumers can
            // jump from files[].symbols[] to nodes without a scan. Node ids are
            // deterministic, so re-assembly over cached parses stamps identically.
            symbol.nodeId = node.id;
            symbolByIdentity.set(makeId(symbol.type, file.path, symbol.name, symbol.line, symbol.containerName), node);
            mapPush(symbolIndex, symbol.name, node);
            edges.push({
                id: makeId('contains', containerId, node.id),
                type: 'contains',
                from: containerId,
                to: node.id,
                fromPath: file.path,
                toPath: file.path,
                line: symbol.line,
                status: 'resolved',
                targetName: node.name,
            });
            if (node.type === 'function') {
                mapPush(functionsByName, node.name, node);
                if (node.containerName) {
                    const parentClassNode = classByFileAndName.get(classKey(file.path, node.containerName));
                    if (parentClassNode) {
                        mapPush(methodNodesByClassId, parentClassNode.id, node);
                    }
                }
            }
            else if (node.type === 'class') {
                mapPush(classNodesByName, node.name, node);
                classByFileAndName.set(classKey(file.path, node.name), node);
            }
            else if (node.type === 'interface') {
                mapPush(interfaceNodesByName, node.name, node);
            }
        }
        topLevelSymbolsByFile.set(file.path, symbolIndex);
        yield;
    }
    const reExportsByFile = new Map();
    for (const file of parsedFiles) {
        abortIfNeeded(signal);
        for (const fileImport of file.imports) {
            if (!fileImport.isReExport || !fileImport.resolvedPath || !allPaths.has(fileImport.resolvedPath))
                continue;
            let fileMap = reExportsByFile.get(file.path);
            if (!fileMap) {
                fileMap = new Map();
                reExportsByFile.set(file.path, fileMap);
            }
            for (const binding of fileImport.bindings) {
                if (binding.isNamespace) {
                    const sourceSymbols = topLevelSymbolsByFile.get(fileImport.resolvedPath);
                    if (sourceSymbols) {
                        for (const name of sourceSymbols.keys()) {
                            fileMap.set(name, { path: fileImport.resolvedPath, sourceName: name });
                        }
                    }
                }
                else {
                    // For aliased re-exports (`export { foo as baz }`) the source module
                    // exports the original name, so resolution must look it up by
                    // importedName, not the outward alias.
                    fileMap.set(binding.localName, {
                        path: fileImport.resolvedPath,
                        sourceName: binding.importedName ?? binding.localName,
                    });
                }
            }
        }
        yield;
    }
    for (let fi = 0; fi < parsedFiles.length; fi++) {
        abortIfNeeded(signal);
        const file = parsedFiles[fi];
        const fileNode = filesByPath.get(file.path);
        if (!fileNode)
            continue;
        onProgress?.({ phase: 'index', total: parsedFiles.length, completed: fi + 1, detail: file.relativePath });
        const bindings = new Map();
        for (const fileImport of file.imports) {
            const resolvedPath = fileImport.resolvedPath && allPaths.has(fileImport.resolvedPath) ? fileImport.resolvedPath : undefined;
            const importKind = resolvedPath ? 'local' : fileImport.kind === 'external' ? 'external' : 'unresolved';
            let targetNode = resolvedPath ? moduleNodeByPath.get(resolvedPath) : undefined;
            if (!targetNode && importKind === 'external') {
                targetNode = {
                    id: makeId('module-external', fileImport.specifier),
                    type: 'module',
                    name: fileImport.specifier,
                    path: fileImport.specifier,
                    relativePath: fileImport.specifier,
                    line: fileImport.line,
                    language: 'external',
                    moduleName: fileImport.specifier,
                    external: true,
                };
                const existingNode = nodeById.get(targetNode.id);
                if (!existingNode) {
                    pushNode(targetNode);
                    mapPush(modulesByName, targetNode.name, targetNode);
                }
                else {
                    targetNode = existingNode;
                }
            }
            edges.push({
                id: makeId('imports', fileNode.id, targetNode?.id, fileImport.line, fileImport.specifier),
                type: 'imports',
                from: fileNode.id,
                to: targetNode?.id,
                fromPath: file.path,
                toPath: resolvedPath ?? targetNode?.path,
                line: fileImport.line,
                status: importKind === 'unresolved' ? 'unresolved' : 'resolved',
                targetName: targetNode?.name ?? fileImport.specifier,
                specifier: fileImport.specifier,
                importKind,
            });
            for (const binding of fileImport.bindings) {
                bindings.set(binding.localName, {
                    importedName: binding.importedName,
                    modulePath: resolvedPath,
                    namespace: binding.isNamespace,
                    specifier: fileImport.specifier,
                });
            }
        }
        for (const inheritance of file.inheritances) {
            const classNodeType = file.symbols.find(s => s.name === inheritance.className && s.line === inheritance.classLine)?.type ?? 'class';
            const classNodeId = makeId(classNodeType, file.path, inheritance.className, inheritance.classLine);
            const classNode = symbolByIdentity.get(classNodeId);
            if (!classNode)
                continue;
            const edgeType = inheritance.kind === 'implements' ? 'implements' : 'inherits';
            if (edgeType === 'inherits') {
                mapPush(parentNamesByClassId, classNode.id, inheritance.baseName);
            }
            const candidatePool = edgeType === 'implements'
                ? interfaceNodesByName
                : classNode.type === 'interface'
                    ? interfaceNodesByName
                    : classNodesByName;
            const localCandidates = candidatePool.get(inheritance.baseName) ?? [];
            const exactLocal = localCandidates.filter(candidate => candidate.path === file.path);
            const candidates = exactLocal.length > 0 ? exactLocal : localCandidates;
            const resolved = candidates.length === 1 ? candidates[0] : undefined;
            edges.push({
                id: makeId(edgeType, classNode.id, inheritance.baseName, inheritance.line),
                type: edgeType,
                from: classNode.id,
                to: resolved?.id,
                fromPath: file.path,
                toPath: resolved?.path,
                line: inheritance.line,
                status: resolved ? 'resolved' : candidates.length > 1 ? 'ambiguous' : 'unresolved',
                targetName: inheritance.baseName,
            });
        }
        const visitedClassMethods = new Map();
        const resolveClassMethods = (classNode, methodName, includeParents) => {
            if (!classNode)
                return [];
            const visitKey = `${classNode.id}:${methodName}:${includeParents}`;
            const cached = visitedClassMethods.get(visitKey);
            if (cached)
                return cached;
            const ownMethods = (methodNodesByClassId.get(classNode.id) ?? []).filter(node => node.name === methodName);
            if (!includeParents || ownMethods.length > 0) {
                visitedClassMethods.set(visitKey, ownMethods);
                return ownMethods;
            }
            const parents = parentNamesByClassId.get(classNode.id) ?? [];
            const inherited = [];
            for (const parentName of parents) {
                const directParent = classByFileAndName.get(classKey(classNode.path, parentName));
                const parentCandidates = directParent ? [directParent] : (classNodesByName.get(parentName) ?? []);
                for (const parentNode of parentCandidates) {
                    inherited.push(...resolveClassMethods(parentNode, methodName, true));
                }
            }
            const unique = inherited.filter((node, index, array) => array.findIndex(candidate => candidate.id === node.id) === index);
            visitedClassMethods.set(visitKey, unique);
            return unique;
        };
        for (const call of file.calls) {
            const callerId = makeId('function', file.path, call.callerName, call.callerLine, call.callerContainerName);
            const callerNode = symbolByIdentity.get(callerId);
            if (!callerNode)
                continue;
            const localSymbols = topLevelSymbolsByFile.get(file.path)?.get(call.calleeName) ?? [];
            const namespaceBinding = call.qualifier ? bindings.get(call.qualifier) : undefined;
            const directBinding = bindings.get(call.calleeName);
            if (isBuiltinNoiseCall(call, bindings, localSymbols))
                continue;
            const callerClassNode = call.callerContainerName
                ? classByFileAndName.get(classKey(file.path, call.callerContainerName))
                : undefined;
            let candidates = localSymbols;
            if (call.qualifier === 'this' || call.qualifier === 'self' || call.qualifier === 'cls') {
                const classCandidates = resolveClassMethods(callerClassNode, call.calleeName, true);
                if (classCandidates.length > 0)
                    candidates = classCandidates;
            }
            else if (call.qualifier === 'super') {
                const parents = callerClassNode ? (parentNamesByClassId.get(callerClassNode.id) ?? []) : [];
                const parentCandidates = [];
                for (const parentName of parents) {
                    const directParent = callerClassNode
                        ? classByFileAndName.get(classKey(callerClassNode.path, parentName))
                        : undefined;
                    const baseCandidates = directParent ? [directParent] : (classNodesByName.get(parentName) ?? []);
                    for (const parentNode of baseCandidates) {
                        parentCandidates.push(...resolveClassMethods(parentNode, call.calleeName, true));
                    }
                }
                if (parentCandidates.length > 0)
                    candidates = parentCandidates;
            }
            else if (namespaceBinding?.modulePath) {
                let importedFileSymbols = topLevelSymbolsByFile.get(namespaceBinding.modulePath)?.get(call.calleeName) ?? [];
                if (importedFileSymbols.length === 0) {
                    const reExport = reExportsByFile.get(namespaceBinding.modulePath)?.get(call.calleeName);
                    if (reExport) {
                        importedFileSymbols = topLevelSymbolsByFile.get(reExport.path)?.get(reExport.sourceName) ?? [];
                    }
                }
                if (importedFileSymbols.length > 0)
                    candidates = importedFileSymbols;
            }
            else if (directBinding?.modulePath) {
                const symbolName = directBinding.importedName ?? call.calleeName;
                let importedFileSymbols = topLevelSymbolsByFile.get(directBinding.modulePath)?.get(symbolName) ?? [];
                if (importedFileSymbols.length === 0) {
                    const reExport = reExportsByFile.get(directBinding.modulePath)?.get(symbolName);
                    if (reExport) {
                        importedFileSymbols = topLevelSymbolsByFile.get(reExport.path)?.get(reExport.sourceName) ?? [];
                    }
                }
                if (importedFileSymbols.length > 0)
                    candidates = importedFileSymbols;
            }
            else if (candidates.length === 0) {
                candidates = functionsByName.get(call.calleeName) ?? [];
            }
            const exactCandidates = directBinding?.modulePath
                ? candidates.filter(candidate => candidate.path === directBinding.modulePath)
                : namespaceBinding?.modulePath
                    ? candidates.filter(candidate => candidate.path === namespaceBinding.modulePath)
                    : candidates;
            const resolvedCandidates = exactCandidates.length > 0 ? exactCandidates : candidates;
            const resolved = resolvedCandidates.length === 1 ? resolvedCandidates[0] : undefined;
            edges.push({
                id: makeId('calls', callerNode.id, call.calleeName, call.line),
                type: 'calls',
                from: callerNode.id,
                to: resolved?.id,
                fromPath: file.path,
                toPath: resolved?.path,
                line: call.line,
                status: resolved ? 'resolved' : resolvedCandidates.length > 1 ? 'ambiguous' : 'unresolved',
                targetName: call.calleeName,
                detail: call.qualifier,
                candidates: resolved ? undefined : resolvedCandidates.length > 1 ? toCandidates(resolvedCandidates) : undefined,
            });
        }
        yield;
    }
    return {
        repoRoot,
        contentMode,
        files: parsedFiles.map(file => ({
            ...file,
            relativePath: normalizePath(path.relative(repoRoot, file.path)),
            moduleName: getModuleName(repoRoot, file.path),
        })),
        nodes,
        edges,
        capabilities: Object.fromEntries(Object.entries(mergeCapabilities(parsedFiles)).map(([language, capabilities]) => [
            language,
            { ...capabilities, content: contentMode === 'full' },
        ])),
        skippedFiles,
        truncated: input.truncated,
    };
}
export function assembleCodebaseIndex(input, onProgress, signal) {
    const steps = assembleSteps(input, onProgress, signal);
    let result = steps.next();
    while (!result.done) {
        result = steps.next();
    }
    return result.value;
}
export async function assembleCodebaseIndexCooperative(input, yielder, onProgress, signal) {
    const steps = assembleSteps(input, onProgress, signal);
    let result = steps.next();
    while (!result.done) {
        await yielder.tick();
        result = steps.next();
    }
    return result.value;
}
//# sourceMappingURL=build.js.map