/**
 * Cross-file binding propagation — extracted from pipeline.ts.
 *
 * Seeds downstream files with resolved type bindings from upstream exports.
 * Files are processed in topological import order so upstream bindings
 * are available when downstream files are re-resolved.
 *
 * @module
 */
import { processCalls, buildImportedReturnTypes, buildImportedRawReturnTypes, } from '../call-processor.js';
import { createASTCache } from '../ast-cache.js';
import { getLanguageFromFilename, } from '../../../_shared/index.js';
import { readFileContents } from '../filesystem-walker.js';
import { isLanguageAvailable } from '../../tree-sitter/parser-loader.js';
import { isRegistryPrimary } from '../registry-primary-flag.js';
import { topologicalLevelSort } from '../utils/graph-sort.js';
import { isDev } from '../utils/env.js';
import { logger } from '../../logger.js';
/** Max AST trees to keep in LRU cache for cross-file binding propagation. */
const AST_CACHE_CAP = 50;
/** Minimum percentage of files that must benefit from cross-file seeding. */
const CROSS_FILE_SKIP_THRESHOLD = 0.03;
/** Hard cap on files re-processed during cross-file propagation. */
const MAX_CROSS_FILE_REPROCESS = 2000;
/**
 * Cross-file binding propagation.
 * Returns the number of files re-processed.
 */
export async function runCrossFileBindingPropagation(graph, ctx, parseExportedTypeMap, allPathSet, totalFiles, repoPath, pipelineStart, onProgress) {
    if (parseExportedTypeMap.size === 0 || ctx.namedImportMap.size === 0)
        return 0;
    // Build a local mutable working copy. Per-file re-resolution below mutates
    // this map (each `processCalls` writes that file's exports back into it so
    // later iterations in the same level/loop can resolve transitive bindings).
    // Owning a local copy here keeps `ParseOutput.exportedTypeMap` truly
    // read-only at the phase boundary — no cast, no shared-mutable handoff.
    const exportedTypeMap = new Map();
    for (const [fp, exports] of parseExportedTypeMap) {
        exportedTypeMap.set(fp, new Map(exports));
    }
    const { levels, cycleCount } = topologicalLevelSort(ctx.importMap);
    if (isDev && cycleCount > 0) {
        logger.info(`🔄 ${cycleCount} files in import cycles (processed last in undefined order)`);
    }
    let filesWithGaps = 0;
    const gapThreshold = Math.max(1, Math.ceil(totalFiles * CROSS_FILE_SKIP_THRESHOLD));
    outer: for (const level of levels) {
        for (const filePath of level) {
            const imports = ctx.namedImportMap.get(filePath);
            if (!imports)
                continue;
            for (const [, binding] of imports) {
                const upstream = exportedTypeMap.get(binding.sourcePath);
                if (upstream?.has(binding.exportedName)) {
                    filesWithGaps++;
                    break;
                }
                const def = ctx.model.symbols.lookupExactFull(binding.sourcePath, binding.exportedName);
                if (def?.returnType) {
                    filesWithGaps++;
                    break;
                }
            }
            if (filesWithGaps >= gapThreshold)
                break outer;
        }
    }
    const gapRatio = totalFiles > 0 ? filesWithGaps / totalFiles : 0;
    if (gapRatio < CROSS_FILE_SKIP_THRESHOLD && filesWithGaps < gapThreshold) {
        if (isDev) {
            logger.info(`⏭️ Cross-file re-resolution skipped (${filesWithGaps}/${totalFiles} files, ${(gapRatio * 100).toFixed(1)}% < ${CROSS_FILE_SKIP_THRESHOLD * 100}% threshold)`);
        }
        return 0;
    }
    // Intentionally reports `phase: 'parsing'` rather than a separate
    // 'crossFile' phase: cross-file re-resolution is logically a continuation of
    // the parsing/resolution work and is bucketed under "parsing" in any
    // telemetry that groups events by phase name. Kept consistent with the
    // upstream `parse` phase's progress events so the UI shows one continuous
    // progress segment instead of a phase flicker. If a future change splits
    // this out into its own phase, also rename `parse-impl.ts` per-chunk
    // progress events accordingly.
    onProgress({
        phase: 'parsing',
        percent: 82,
        message: `Cross-file type propagation (${filesWithGaps}+ files)...`,
        stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
    });
    let crossFileResolved = 0;
    const crossFileStart = Date.now();
    const astCache = createASTCache(AST_CACHE_CAP);
    // Compiled query objects keyed by language name. Shared across all processCalls
    // invocations in this phase so the same tree-sitter query string is only
    // compiled once per language instead of once per file (O(1) vs O(N)).
    const compiledQueryCache = new Map();
    // Snapshot total topological candidates for progress math.  We walk the
    // levels once more here (fast — no I/O) so we can report meaningful
    // percentages rather than a frozen display.
    let totalCandidates = 0;
    for (const level of levels) {
        for (const filePath of level) {
            if (totalCandidates >= MAX_CROSS_FILE_REPROCESS)
                break;
            const imports = ctx.namedImportMap.get(filePath);
            if (!imports)
                continue;
            if (!allPathSet.has(filePath))
                continue;
            const lang = getLanguageFromFilename(filePath);
            if (!lang || !isLanguageAvailable(lang))
                continue;
            // Registry-primary languages have their call resolution handled by the
            // scope-resolution pipeline — processCalls skips them immediately. Skip
            // here too so we avoid the I/O cost (readFileContents) and map-building
            // overhead for files that would be no-ops anyway.
            if (isRegistryPrimary(lang))
                continue;
            totalCandidates++;
        }
        if (totalCandidates >= MAX_CROSS_FILE_REPROCESS)
            break;
    }
    const cappedTotal = Math.min(totalCandidates, MAX_CROSS_FILE_REPROCESS);
    /** Emit a progress event every PROGRESS_INTERVAL files so the UI stays alive. */
    const PROGRESS_INTERVAL = 25;
    for (const level of levels) {
        const levelCandidates = [];
        for (const filePath of level) {
            if (crossFileResolved + levelCandidates.length >= MAX_CROSS_FILE_REPROCESS)
                break;
            const imports = ctx.namedImportMap.get(filePath);
            if (!imports)
                continue;
            const seeded = new Map();
            for (const [localName, binding] of imports) {
                const upstream = exportedTypeMap.get(binding.sourcePath);
                if (upstream) {
                    const type = upstream.get(binding.exportedName);
                    if (type)
                        seeded.set(localName, type);
                }
            }
            const importedReturns = buildImportedReturnTypes(filePath, ctx.namedImportMap, ctx.model.symbols);
            const importedRawReturns = buildImportedRawReturnTypes(filePath, ctx.namedImportMap, ctx.model.symbols);
            if (seeded.size === 0 && importedReturns.size === 0)
                continue;
            if (!allPathSet.has(filePath))
                continue;
            const lang = getLanguageFromFilename(filePath);
            if (!lang || !isLanguageAvailable(lang))
                continue;
            // Registry-primary languages have their call resolution handled by the
            // scope-resolution pipeline — processCalls skips them immediately. Skip
            // here to avoid readFileContents I/O and map-building for no-op files.
            if (isRegistryPrimary(lang))
                continue;
            levelCandidates.push({ filePath, seeded, importedReturns, importedRawReturns });
        }
        if (levelCandidates.length === 0)
            continue;
        const levelPaths = levelCandidates.map((c) => c.filePath);
        const contentMap = await readFileContents(repoPath, levelPaths);
        for (const { filePath, seeded, importedReturns, importedRawReturns } of levelCandidates) {
            const content = contentMap.get(filePath);
            if (!content)
                continue;
            const reFile = [{ path: filePath, content }];
            const bindings = new Map();
            if (seeded.size > 0)
                bindings.set(filePath, seeded);
            const importedReturnTypesMap = new Map();
            if (importedReturns.size > 0) {
                importedReturnTypesMap.set(filePath, importedReturns);
            }
            const importedRawReturnTypesMap = new Map();
            if (importedRawReturns.size > 0) {
                importedRawReturnTypesMap.set(filePath, importedRawReturns);
            }
            await processCalls(graph, reFile, astCache, ctx, undefined, exportedTypeMap, bindings.size > 0 ? bindings : undefined, importedReturnTypesMap.size > 0 ? importedReturnTypesMap : undefined, importedRawReturnTypesMap.size > 0 ? importedRawReturnTypesMap : undefined, undefined, undefined, compiledQueryCache);
            crossFileResolved++;
            // Emit progress every PROGRESS_INTERVAL files so the UI shows real
            // movement instead of a frozen display (cross-file can take minutes
            // on large repos with many cross-file imports).
            if (crossFileResolved % PROGRESS_INTERVAL === 0 || crossFileResolved === cappedTotal) {
                const pct = cappedTotal > 0 ? Math.round((crossFileResolved / cappedTotal) * 8) : 0;
                onProgress({
                    phase: 'parsing',
                    percent: 82 + pct,
                    message: `Cross-file type propagation (${crossFileResolved}/${cappedTotal} files)...`,
                    stats: { filesProcessed: crossFileResolved, totalFiles, nodesCreated: graph.nodeCount },
                });
            }
        }
        if (crossFileResolved >= MAX_CROSS_FILE_REPROCESS) {
            if (isDev)
                logger.info(`⚠️ Cross-file re-resolution capped at ${MAX_CROSS_FILE_REPROCESS} files`);
            break;
        }
    }
    astCache.clear();
    if (isDev) {
        const elapsed = Date.now() - crossFileStart;
        const totalElapsed = Date.now() - pipelineStart;
        const reResolutionPct = totalElapsed > 0 ? ((elapsed / totalElapsed) * 100).toFixed(1) : '0';
        logger.info(`🔗 Cross-file re-resolution: ${crossFileResolved} candidates re-processed` +
            ` in ${elapsed}ms (${reResolutionPct}% of total ingestion time so far)`);
    }
    return crossFileResolved;
}
