import Parser from 'tree-sitter';
import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
import { getProvider } from './languages/index.js';
import { generateId } from '../../lib/utils.js';
import { getLanguageFromFilename, SupportedLanguages } from '../../_shared/index.js';
import { extractVueScript, isVueSetupTopLevel } from './vue-sfc-extractor.js';
import { yieldToEventLoop } from './utils/event-loop.js';
import { isVerboseIngestionEnabled } from './utils/verbose.js';
import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, getLabelFromCaptures, CLASS_CONTAINER_TYPES, } from './utils/ast-helpers.js';
import { detectFrameworkFromAST } from './framework-detection.js';
import { buildTypeEnv } from './type-env.js';
import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, } from './utils/method-props.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
// ============================================================================
// Worker-based parallel parsing
// ============================================================================
const processParsingWithWorkers = async (graph, files, symbolTable, astCache, workerPool, onFileProgress) => {
    // Filter to parseable files only
    const parseableFiles = [];
    for (const file of files) {
        const lang = getLanguageFromFilename(file.path);
        if (lang)
            parseableFiles.push({ path: file.path, content: file.content });
    }
    if (parseableFiles.length === 0)
        return {
            imports: [],
            calls: [],
            assignments: [],
            heritage: [],
            routes: [],
            fetchCalls: [],
            decoratorRoutes: [],
            toolDefs: [],
            ormQueries: [],
            constructorBindings: [],
            fileScopeBindings: [],
            parsedFiles: [],
        };
    const total = files.length;
    // Dispatch to worker pool — pool handles splitting into chunks and sub-batching
    const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
        onFileProgress?.(Math.min(filesProcessed, total), total, 'Parsing...');
    });
    // Merge results from all workers into graph and symbol table
    const allImports = [];
    const allCalls = [];
    const allAssignments = [];
    const allHeritage = [];
    const allRoutes = [];
    const allFetchCalls = [];
    const allDecoratorRoutes = [];
    const allToolDefs = [];
    const allORMQueries = [];
    const allConstructorBindings = [];
    const fileScopeBindingsByFile = [];
    const allParsedFiles = [];
    for (const result of chunkResults) {
        for (const node of result.nodes) {
            graph.addNode({
                id: node.id,
                label: node.label,
                properties: node.properties,
            });
        }
        for (const rel of result.relationships) {
            graph.addRelationship(rel);
        }
        for (const sym of result.symbols) {
            symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
                parameterCount: sym.parameterCount,
                requiredParameterCount: sym.requiredParameterCount,
                parameterTypes: sym.parameterTypes,
                returnType: sym.returnType,
                declaredType: sym.declaredType,
                ownerId: sym.ownerId,
                qualifiedName: sym.qualifiedName,
            });
        }
        for (const item of result.imports)
            allImports.push(item);
        for (const item of result.calls)
            allCalls.push(item);
        for (const item of result.assignments)
            allAssignments.push(item);
        for (const item of result.heritage)
            allHeritage.push(item);
        for (const item of result.routes)
            allRoutes.push(item);
        for (const item of result.fetchCalls)
            allFetchCalls.push(item);
        for (const item of result.decoratorRoutes)
            allDecoratorRoutes.push(item);
        for (const item of result.toolDefs)
            allToolDefs.push(item);
        if (result.ormQueries)
            for (const item of result.ormQueries)
                allORMQueries.push(item);
        for (const item of result.constructorBindings)
            allConstructorBindings.push(item);
        if (result.fileScopeBindings)
            for (const item of result.fileScopeBindings)
                fileScopeBindingsByFile.push(item);
        // RFC #909 Ring 2: aggregate per-file scope artifacts. Tolerant of
        // workers that don't emit the field yet (older worker builds or
        // partial rollouts), since the additive contract means undefined =
        // "this worker produced no ParsedFiles for this chunk".
        if (result.parsedFiles)
            for (const item of result.parsedFiles)
                allParsedFiles.push(item);
    }
    // Merge and log skipped languages from workers
    const skippedLanguages = new Map();
    for (const result of chunkResults) {
        for (const [lang, count] of Object.entries(result.skippedLanguages)) {
            skippedLanguages.set(lang, (skippedLanguages.get(lang) || 0) + count);
        }
    }
    if (skippedLanguages.size > 0) {
        const summary = Array.from(skippedLanguages.entries())
            .map(([lang, count]) => `${lang}: ${count}`)
            .join(', ');
        console.warn(`  Skipped unsupported languages: ${summary}`);
    }
    // Final progress
    onFileProgress?.(total, total, 'done');
    return {
        imports: allImports,
        calls: allCalls,
        assignments: allAssignments,
        heritage: allHeritage,
        routes: allRoutes,
        fetchCalls: allFetchCalls,
        decoratorRoutes: allDecoratorRoutes,
        toolDefs: allToolDefs,
        ormQueries: allORMQueries,
        constructorBindings: allConstructorBindings,
        fileScopeBindings: fileScopeBindingsByFile,
        parsedFiles: allParsedFiles,
    };
};
// ============================================================================
// Sequential fallback (original implementation)
// ============================================================================
// Inline caches to avoid repeated parent-walks per node (same pattern as parse-worker.ts).
// Keyed by tree-sitter node reference — cleared at the start of each file.
const classInfoCache = new Map();
const exportCache = new Map();
const cachedFindEnclosingClassInfo = (node, filePath, resolveEnclosingOwner) => {
    const cached = classInfoCache.get(node);
    if (cached !== undefined)
        return cached;
    const result = findEnclosingClassInfo(node, filePath, resolveEnclosingOwner);
    classInfoCache.set(node, result);
    return result;
};
const cachedExportCheck = (checker, node, name) => {
    const cached = exportCache.get(node);
    if (cached !== undefined)
        return cached;
    const result = checker(node, name);
    exportCache.set(node, result);
    return result;
};
// FieldExtractor cache for sequential path — same pattern as parse-worker.ts
const seqFieldInfoCache = new Map();
// MethodExtractor cache for sequential path — avoids re-traversing the same class
// body once per method. Keyed on classNode.id (tree-sitter node identity number).
const seqMethodExtractCache = new Map();
// Derived method map + collision groups cache — avoids rebuilding per method.
const seqMethodMapCache = new Map();
/** Provider-aware enclosing container lookup.
 *  Walks up from `node` until a CLASS_CONTAINER_TYPES node is found.
 *  When `resolveEnclosingOwner` is provided, delegates language-specific
 *  container remapping (e.g., Ruby singleton_class → enclosing class).
 *  Without the hook, returns the first matching container directly (raw lookup). */
function seqFindEnclosingOwnerNode(node, resolveEnclosingOwner) {
    let current = node.parent;
    while (current) {
        if (CLASS_CONTAINER_TYPES.has(current.type)) {
            if (resolveEnclosingOwner) {
                const resolved = resolveEnclosingOwner(current);
                if (resolved === null) {
                    // Provider says skip this container — keep walking up.
                    current = current.parent;
                    continue;
                }
                return resolved;
            }
            return current;
        }
        current = current.parent;
    }
    return null;
}
/** Minimal no-op SymbolTable stub for sequential extractor contexts. The real
 *  SymbolTable is not fully populated yet at this stage, so use the stub for safety.
 *  Implements the full {@link SymbolTableReader} surface so future extractor additions
 *  don't silently fall off an `as unknown as` cast. */
const NOOP_SYMBOL_TABLE_SEQ = {
    lookupExact: () => undefined,
    lookupExactFull: () => undefined,
    lookupExactAll: () => [],
    lookupCallableByName: () => [],
    getFiles: () => [][Symbol.iterator](),
    getStats: () => ({ fileCount: 0 }),
};
function seqGetFieldInfo(classNode, provider, context) {
    if (!provider.fieldExtractor)
        return undefined;
    const cacheKey = classNode.startIndex;
    let cached = seqFieldInfoCache.get(cacheKey);
    if (cached)
        return cached;
    const extracted = provider.fieldExtractor.extract(classNode, context);
    if (!extracted?.fields?.length)
        return undefined;
    cached = new Map();
    for (const field of extracted.fields)
        cached.set(field.name, field);
    seqFieldInfoCache.set(cacheKey, cached);
    return cached;
}
const processParsingSequential = async (graph, files, symbolTable, astCache, scopeTreeCache, onFileProgress) => {
    const parser = await loadParser();
    const total = files.length;
    const logSkipped = isVerboseIngestionEnabled();
    const skippedByLang = logSkipped ? new Map() : null;
    for (let i = 0; i < files.length; i++) {
        const file = files[i];
        // Reset memoization before each new file (node refs are per-tree)
        classInfoCache.clear();
        exportCache.clear();
        seqFieldInfoCache.clear();
        seqMethodExtractCache.clear();
        seqMethodMapCache.clear();
        onFileProgress?.(i + 1, total, file.path);
        if (i % 20 === 0)
            await yieldToEventLoop();
        const language = getLanguageFromFilename(file.path);
        if (!language)
            continue;
        if (!isLanguageAvailable(language)) {
            if (skippedByLang) {
                skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
            }
            continue;
        }
        // Skip files larger than the max tree-sitter buffer (32 MB)
        if (file.content.length > TREE_SITTER_MAX_BUFFER)
            continue;
        // Vue SFC preprocessing: extract <script> block content
        let parseContent = file.content;
        let lineOffset = 0;
        let isVueSetup = false;
        if (language === SupportedLanguages.Vue) {
            const extracted = extractVueScript(file.content);
            if (!extracted)
                continue; // skip .vue files with no script block
            parseContent = extracted.scriptContent;
            lineOffset = extracted.lineOffset;
            isVueSetup = extracted.isSetup;
        }
        try {
            await loadLanguage(language, file.path);
        }
        catch {
            continue; // parser unavailable — safety net
        }
        let tree;
        try {
            tree = parser.parse(parseContent, undefined, {
                bufferSize: getTreeSitterBufferSize(parseContent.length),
            });
        }
        catch (_parseError) {
            console.warn(`Skipping unparseable file: ${file.path}`);
            continue;
        }
        astCache.set(file.path, tree);
        const provider = getProvider(language);
        // Mirror into the cross-phase cache only when the language has a
        // scope-resolution consumer — otherwise we retain Trees no one
        // reads. parse-impl clears `astCache` between chunks;
        // `scopeTreeCache` survives until scope-resolution disposes it.
        if (provider.emitScopeCaptures !== undefined) {
            scopeTreeCache?.set(file.path, tree);
        }
        const queryString = provider.treeSitterQueries;
        if (!queryString) {
            continue;
        }
        let query;
        let matches;
        try {
            const language = parser.getLanguage();
            query = new Parser.Query(language, queryString);
            matches = query.matches(tree.rootNode);
        }
        catch (queryError) {
            console.warn(`Query error for ${file.path}:`, queryError);
            continue;
        }
        // Build per-file type environment for FieldExtractor context (lightweight — skipped if no fieldExtractor).
        //
        // Note: this TypeEnv is intentionally NOT flushed into the BindingAccumulator.
        // The accumulator feed happens later in `call-processor.ts` via its own
        // `typeEnv.flush(accumulator)` call. Flushing here would double-count
        // file-scope bindings and break the single-use invariant of `flush()`.
        // See the BindingAccumulator class JSDoc for the full accumulator
        // lifecycle and flush-site ownership rules.
        const typeEnv = provider.fieldExtractor
            ? buildTypeEnv(tree, language, {
                enclosingFunctionFinder: provider.enclosingFunctionFinder,
                extractFunctionName: provider.methodExtractor?.extractFunctionName,
            })
            : null;
        matches.forEach((match) => {
            const captureMap = {};
            match.captures.forEach((c) => {
                captureMap[c.name] = c.node;
            });
            const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap);
            const definitionNode = getDefinitionNodeFromCaptures(captureMap);
            const defaultNodeLabel = getLabelFromCaptures(captureMap, provider);
            if (!defaultNodeLabel)
                return;
            const nameNode = captureMap['name'];
            const extractedClassSymbol = definitionNode && provider.classExtractor?.isTypeDeclaration(definitionNode)
                ? provider.classExtractor.extract(definitionNode, {
                    name: nameNode?.text,
                    type: defaultNodeLabel,
                })
                : null;
            const nodeLabel = extractedClassSymbol?.type ?? defaultNodeLabel;
            // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
            if (!nameNode && nodeLabel !== 'Constructor' && !extractedClassSymbol)
                return;
            const nodeName = extractedClassSymbol?.name ?? (nameNode ? nameNode.text : 'init');
            const startLine = definitionNodeForRange
                ? definitionNodeForRange.startPosition.row + lineOffset
                : nameNode
                    ? nameNode.startPosition.row + lineOffset
                    : lineOffset;
            // Compute enclosing class BEFORE node ID — needed to qualify method IDs
            const needsOwner = nodeLabel === 'Method' ||
                nodeLabel === 'Constructor' ||
                nodeLabel === 'Property' ||
                nodeLabel === 'Function';
            const enclosingClassInfo = needsOwner
                ? cachedFindEnclosingClassInfo(nameNode || definitionNodeForRange, file.path, provider.resolveEnclosingOwner)
                : null;
            const enclosingClassId = enclosingClassInfo?.classId ?? null;
            // Qualify method/property IDs with enclosing class name to avoid collisions
            // e.g. "Method:animal.dart:Animal.speak" vs "Method:animal.dart:Dog.speak"
            const qualifiedName = enclosingClassInfo
                ? `${enclosingClassInfo.className}.${nodeName}`
                : nodeName;
            // Extract method metadata for Function/Method/Constructor nodes BEFORE generating
            // the node ID — parameterCount is needed to disambiguate overloaded methods.
            // Use the per-language MethodExtractor for method metadata (isAbstract, isStatic,
            // visibility, annotations, parameterCount, parameterTypes, returnType, etc.).
            const isMethodLike = nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor';
            let methodProps = {};
            let arityForId; // raw param count for ID, even for variadic
            let seqDefMethodInfo;
            let seqDefMethods;
            let seqClassNodeId;
            if (isMethodLike && definitionNode) {
                let enriched = false;
                if (provider.methodExtractor) {
                    // Try class-based extraction (method inside a class/struct/trait body).
                    // Raw lookup (no resolveEnclosingOwner) so the method extractor sees
                    // the actual container node (e.g. singleton_class) for static detection.
                    const methodOwnerNode = seqFindEnclosingOwnerNode(definitionNode);
                    if (methodOwnerNode) {
                        // Cache extract() results per class node to avoid re-traversing the
                        // same class body for every method it contains (O(N) -> O(1) per hit).
                        let result = seqMethodExtractCache.get(methodOwnerNode.id);
                        if (result === undefined) {
                            result =
                                provider.methodExtractor.extract(methodOwnerNode, {
                                    filePath: file.path,
                                    language,
                                }) ?? null;
                            seqMethodExtractCache.set(methodOwnerNode.id, result);
                        }
                        if (result?.methods?.length) {
                            const defLine = definitionNode.startPosition.row + 1;
                            const info = result.methods.find((m) => m.name === nodeName && m.line === defLine);
                            if (info) {
                                enriched = true;
                                arityForId = arityForIdFromInfo(info);
                                methodProps = buildMethodProps(info);
                                seqDefMethodInfo = info;
                                seqDefMethods = result.methods;
                                seqClassNodeId = methodOwnerNode.id;
                            }
                        }
                    }
                    // For top-level methods (e.g. Go method_declaration), try extractFromNode
                    if (!enriched && provider.methodExtractor.extractFromNode) {
                        const info = provider.methodExtractor.extractFromNode(definitionNode, {
                            filePath: file.path,
                            language,
                        });
                        if (info) {
                            enriched = true;
                            arityForId = arityForIdFromInfo(info);
                            methodProps = buildMethodProps(info);
                        }
                    }
                }
            }
            // Append #<paramCount> to Method/Constructor IDs to disambiguate overloads.
            // Functions are not suffixed — they don't overload by name in the same scope.
            // When same-arity collisions exist, append ~type1,type2 for further disambiguation.
            const needsAritySuffix = nodeLabel === 'Method' || nodeLabel === 'Constructor';
            let arityTag = needsAritySuffix && arityForId !== undefined ? `#${arityForId}` : '';
            if (arityTag && seqDefMethods && seqDefMethodInfo && seqClassNodeId !== undefined) {
                // Use cached method map + collision groups (built once per class, not per method)
                let cached = seqMethodMapCache.get(seqClassNodeId);
                if (!cached) {
                    const tempMap = new Map();
                    for (const m of seqDefMethods)
                        tempMap.set(`${m.name}:${m.line}`, m);
                    cached = { map: tempMap, groups: buildCollisionGroups(tempMap) };
                    seqMethodMapCache.set(seqClassNodeId, cached);
                }
                arityTag += typeTagForId(cached.map, nodeName, arityForId, seqDefMethodInfo, language, cached.groups);
                arityTag += constTagForId(cached.map, nodeName, arityForId, seqDefMethodInfo, cached.groups);
            }
            const nodeId = generateId(nodeLabel, `${file.path}:${qualifiedName}${arityTag}`);
            const classNodeForSymbol = definitionNodeForRange || definitionNode || nameNode;
            const qualifiedTypeName = extractedClassSymbol?.qualifiedName ??
                (classNodeForSymbol && provider.classExtractor?.isTypeDeclaration(classNodeForSymbol)
                    ? (provider.classExtractor.extractQualifiedName(classNodeForSymbol, nodeName) ?? nodeName)
                    : undefined);
            const frameworkHint = definitionNode
                ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
                : null;
            const node = {
                id: nodeId,
                label: nodeLabel,
                properties: {
                    name: nodeName,
                    filePath: file.path,
                    startLine: definitionNodeForRange
                        ? definitionNodeForRange.startPosition.row + lineOffset
                        : startLine,
                    endLine: definitionNodeForRange
                        ? definitionNodeForRange.endPosition.row + lineOffset
                        : startLine,
                    language: language,
                    isExported: language === SupportedLanguages.Vue && isVueSetup
                        ? isVueSetupTopLevel(nameNode || definitionNodeForRange)
                        : cachedExportCheck(provider.exportChecker, nameNode || definitionNodeForRange, nodeName),
                    ...(qualifiedTypeName !== undefined ? { qualifiedName: qualifiedTypeName } : {}),
                    ...(frameworkHint
                        ? {
                            astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
                            astFrameworkReason: frameworkHint.reason,
                        }
                        : {}),
                    ...methodProps,
                },
            };
            graph.addNode(node);
            // enclosingClassId already computed above (before nodeId generation)
            // Extract declared type and field metadata for Property nodes
            let declaredType;
            let seqVisibility;
            let seqIsStatic;
            let seqIsReadonly;
            if (nodeLabel === 'Property' && definitionNode) {
                // FieldExtractor is the single source of truth when available
                if (provider.fieldExtractor && typeEnv) {
                    const classNode = seqFindEnclosingOwnerNode(definitionNode, provider.resolveEnclosingOwner);
                    if (classNode) {
                        const fieldMap = seqGetFieldInfo(classNode, provider, {
                            typeEnv,
                            symbolTable: NOOP_SYMBOL_TABLE_SEQ,
                            filePath: file.path,
                            language,
                        });
                        const info = fieldMap?.get(nodeName);
                        if (info) {
                            declaredType = info.type ?? undefined;
                            seqVisibility = info.visibility;
                            seqIsStatic = info.isStatic;
                            seqIsReadonly = info.isReadonly;
                        }
                    }
                }
                // All 15 tree-sitter languages register a FieldExtractor — no fallback needed.
            }
            // Apply field metadata to the graph node retroactively
            if (seqVisibility !== undefined)
                node.properties.visibility = seqVisibility;
            if (seqIsStatic !== undefined)
                node.properties.isStatic = seqIsStatic;
            if (seqIsReadonly !== undefined)
                node.properties.isReadonly = seqIsReadonly;
            if (declaredType !== undefined)
                node.properties.declaredType = declaredType;
            symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
                parameterCount: methodProps.parameterCount,
                requiredParameterCount: methodProps.requiredParameterCount,
                parameterTypes: methodProps.parameterTypes,
                returnType: methodProps.returnType,
                declaredType,
                ownerId: enclosingClassId ?? undefined,
                qualifiedName: qualifiedTypeName,
            });
            const fileId = generateId('File', file.path);
            const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
            const relationship = {
                id: relId,
                sourceId: fileId,
                targetId: nodeId,
                type: 'DEFINES',
                confidence: 1.0,
                reason: '',
            };
            graph.addRelationship(relationship);
            // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
            if (enclosingClassId) {
                const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
                graph.addRelationship({
                    id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
                    sourceId: enclosingClassId,
                    targetId: nodeId,
                    type: memberEdgeType,
                    confidence: 1.0,
                    reason: '',
                });
            }
        });
    }
    if (skippedByLang && skippedByLang.size > 0) {
        for (const [lang, count] of skippedByLang.entries()) {
            console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in parsing processing — ${lang} parser not available.`);
        }
    }
};
// ============================================================================
// Public API
// ============================================================================
export const processParsing = async (graph, files, symbolTable, astCache, 
/**
 * Persistent tree cache (separate from `astCache`, which the caller
 * clears between chunks). Sequential parses additionally write the
 * Tree here so cross-phase consumers (scope-resolution) can read it.
 * Worker-mode parses skip — Trees can't cross MessageChannels.
 * Pass `undefined` if no consumer needs cross-phase access.
 */
scopeTreeCache, onFileProgress, workerPool) => {
    if (workerPool) {
        if (scopeTreeCache !== undefined && process.env.PROF_SCOPE_RESOLUTION === '1') {
            // Trees can't cross MessageChannels, so worker-parsed files land
            // in scope-resolution with an empty cache and get re-parsed.
            // Surfacing this in PROF mode prevents silent perf cliffs when
            // a repo crosses the worker-pool threshold.
            console.warn(`[scope-resolution prof] worker pool engaged for ${files.length} files — cross-phase tree cache will be empty; scope-resolution re-parses.`);
        }
        try {
            return await processParsingWithWorkers(graph, files, symbolTable, astCache, workerPool, onFileProgress);
        }
        catch (err) {
            console.warn('Worker pool parsing failed for this chunk, falling back to sequential:', err instanceof Error ? err.message : err);
            try {
                await workerPool.terminate();
            }
            catch (terminateErr) {
                console.warn('Worker pool termination after parsing failure failed:', terminateErr instanceof Error ? terminateErr.message : terminateErr);
            }
        }
    }
    // Fallback: sequential parsing (no pre-extracted data)
    await processParsingSequential(graph, files, symbolTable, astCache, scopeTreeCache, onFileProgress);
    return null;
};
