import { readFile, stat } from 'node:fs/promises';
import path from 'node:path';
import { getLanguageSpec, getModuleName, isAssetSpecifier, isLocalSpecifier, normalizePath, resolveLocalImport, resolvePythonImport, resolveTsconfigImport, } from './languages.js';
import { collectTreeSitterTagCaptures, parseTreeSitterContent } from './tree-sitter.js';
/**
 * Signals that a `.vue` single-file component could not be parsed because its
 * SFC structure (template/script/style blocks) is malformed. Carries a
 * human-readable reason so the file can be reported as skipped rather than
 * aborting the surrounding batch.
 */
class VueSfcParseError extends Error {
    reason;
    constructor(reason) {
        super(reason);
        this.name = 'VueSfcParseError';
        this.reason = reason;
    }
}
const DEFAULT_MAX_FILE_SIZE_BYTES = 2_097_152;
const DEFAULT_MAX_LINES = 25_000;
const MINIFIED_FILE_MIN_BYTES = 4_096;
const PYTHON_STDLIB_MODULES = new Set([
    'abc',
    'argparse',
    'asyncio',
    'base64',
    'collections',
    'contextlib',
    'csv',
    'dataclasses',
    'datetime',
    'functools',
    'glob',
    'hashlib',
    'heapq',
    'inspect',
    'itertools',
    'json',
    'logging',
    'math',
    'os',
    'pathlib',
    'queue',
    'random',
    're',
    'shutil',
    'statistics',
    'string',
    'subprocess',
    'sys',
    'tempfile',
    'threading',
    'time',
    'typing',
    'unittest',
    'uuid',
]);
let tsModulePromise;
let vueCompilerPromise;
async function loadTypeScript() {
    if (!tsModulePromise) {
        tsModulePromise = import('typescript')
            .then(module => ('default' in module ? module.default : module))
            .catch((error) => {
            tsModulePromise = undefined;
            const detail = error instanceof Error ? ` ${error.message}` : '';
            throw new Error(`Failed to load the bundled "typescript" dependency. Reinstall "@cesarandreslopez/occ" and try again.${detail}`);
        });
    }
    return tsModulePromise;
}
async function loadVueCompiler() {
    if (!vueCompilerPromise) {
        vueCompilerPromise = import('@vue/compiler-sfc').catch((error) => {
            vueCompilerPromise = undefined;
            const detail = error instanceof Error ? ` ${error.message}` : '';
            throw new Error(`Failed to load the bundled "@vue/compiler-sfc" dependency. Reinstall "@cesarandreslopez/occ" and try again.${detail}`);
        });
    }
    return vueCompilerPromise;
}
function lineOf(sourceFile, node) {
    return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
}
function endLineOf(sourceFile, node) {
    return sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
}
function propertyName(ts, node, sourceFile) {
    if (!node)
        return null;
    if (ts.isIdentifier(node))
        return node.text;
    if (ts.isStringLiteral(node) || ts.isNumericLiteral(node))
        return node.text;
    return node.getText(sourceFile);
}
function hasExportModifier(ts, node) {
    if (!ts.canHaveModifiers(node))
        return false;
    return !!ts.getModifiers(node)?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword);
}
function hasModifierKind(ts, node, kind) {
    if (!ts.canHaveModifiers(node))
        return false;
    return !!ts.getModifiers(node)?.some(modifier => modifier.kind === kind);
}
function getVisibility(ts, node) {
    if (hasModifierKind(ts, node, ts.SyntaxKind.PrivateKeyword))
        return 'private';
    if (hasModifierKind(ts, node, ts.SyntaxKind.ProtectedKeyword))
        return 'protected';
    if (hasModifierKind(ts, node, ts.SyntaxKind.PublicKeyword))
        return 'public';
    return undefined;
}
function getLeadingJsDoc(sourceFile, node) {
    const fullText = sourceFile.getFullText();
    const nodeStart = node.getFullStart();
    const leadingText = fullText.slice(Math.max(0, nodeStart), node.getStart(sourceFile));
    const jsdocMatch = leadingText.match(/\/\*\*([\s\S]*?)\*\//);
    if (!jsdocMatch)
        return undefined;
    return (jsdocMatch[0]
        .replace(/^\/\*\*\s*/, '')
        .replace(/\s*\*\/$/, '')
        .split('\n')
        .map(line => line.replace(/^\s*\*\s?/, ''))
        .join('\n')
        .trim() || undefined);
}
function buildFullSignature(sourceFile, node) {
    const params = node.parameters.map(p => p.getText(sourceFile)).join(', ');
    const returnType = node.type ? node.type.getText(sourceFile) : undefined;
    return returnType ? `(${params}) => ${returnType}` : `(${params})`;
}
function getReturnTypeText(sourceFile, node) {
    return node.type ? node.type.getText(sourceFile) : undefined;
}
function symbolIdentityKey(symbol) {
    return [symbol.type, symbol.name, symbol.line, symbol.containerName ?? ''].join('\u0000');
}
// Dedup keys are tracked per target array so insertion stays O(1); the array
// itself preserves discovery order.
const symbolIdentityKeys = new WeakMap();
function pushUniqueSymbol(symbols, next) {
    let seen = symbolIdentityKeys.get(symbols);
    if (!seen) {
        seen = new Set(symbols.map(symbolIdentityKey));
        symbolIdentityKeys.set(symbols, seen);
    }
    const key = symbolIdentityKey(next);
    if (!seen.has(key)) {
        seen.add(key);
        symbols.push(next);
    }
}
export function resolveImportPath(language, context, filePath, specifier) {
    if (language === 'python') {
        return resolvePythonImport(context.repoRoot, filePath, specifier);
    }
    if (isLocalSpecifier(specifier)) {
        return resolveLocalImport(context.repoRoot, filePath, specifier);
    }
    if (language === 'javascript' || language === 'typescript' || language === 'vue') {
        return resolveTsconfigImport(context.repoRoot, specifier);
    }
    return undefined;
}
function typescriptImportKind(specifier, resolvedPath) {
    if (resolvedPath)
        return 'local';
    if (isLocalSpecifier(specifier) && !isAssetSpecifier(specifier))
        return 'local';
    return 'external';
}
function pythonImportKind(specifier, resolvedPath) {
    if (resolvedPath)
        return 'local';
    if (specifier.startsWith('.'))
        return 'unresolved';
    const root = specifier.split('.')[0] ?? specifier;
    return PYTHON_STDLIB_MODULES.has(root) ? 'external' : 'unresolved';
}
async function parseTypescriptFile(filePath, content, context) {
    const ts = await loadTypeScript();
    const relativePath = normalizePath(path.relative(context.repoRoot, filePath));
    const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
    const symbols = [];
    const imports = [];
    const calls = [];
    const inheritances = [];
    const stack = [];
    const pushFunction = (name, node, opts) => {
        const currentClass = [...stack].reverse().find(entry => entry.type === 'class');
        pushUniqueSymbol(symbols, {
            type: 'function',
            name,
            line: lineOf(sourceFile, node),
            endLine: endLineOf(sourceFile, node),
            containerName: currentClass?.name,
            signature: opts.signature,
            exported: opts.exported,
            fullSignature: opts.fullSignature,
            returnType: opts.returnType,
            jsdoc: opts.jsdoc,
            isAsync: opts.isAsync || undefined,
            isStatic: opts.isStatic || undefined,
            visibility: opts.visibility,
        });
    };
    const visit = (node) => {
        if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
            const specifier = node.moduleSpecifier.text;
            const bindings = [];
            if (node.importClause?.name) {
                bindings.push({ localName: node.importClause.name.text, importedName: 'default', isDefault: true });
            }
            if (node.importClause?.namedBindings) {
                if (ts.isNamespaceImport(node.importClause.namedBindings)) {
                    bindings.push({ localName: node.importClause.namedBindings.name.text, importedName: '*', isNamespace: true });
                }
                else {
                    for (const element of node.importClause.namedBindings.elements) {
                        bindings.push({
                            localName: element.name.text,
                            importedName: element.propertyName?.text ?? element.name.text,
                        });
                    }
                }
            }
            const resolvedPath = resolveImportPath(getLanguageSpec(filePath)?.name ?? 'javascript', context, filePath, specifier);
            imports.push({
                specifier,
                line: lineOf(sourceFile, node),
                kind: typescriptImportKind(specifier, resolvedPath),
                bindings,
                resolvedPath,
            });
        }
        else if (ts.isFunctionDeclaration(node) && node.name?.text) {
            const name = node.name.text;
            pushFunction(name, node, {
                signature: node.parameters.map(parameter => parameter.name.getText(sourceFile)).join(', '),
                exported: hasExportModifier(ts, node),
                fullSignature: buildFullSignature(sourceFile, node),
                returnType: getReturnTypeText(sourceFile, node),
                jsdoc: getLeadingJsDoc(sourceFile, node),
                isAsync: hasModifierKind(ts, node, ts.SyntaxKind.AsyncKeyword) || undefined,
            });
            stack.push({ type: 'function', name, line: lineOf(sourceFile, node) });
            ts.forEachChild(node, visit);
            stack.pop();
            return;
        }
        else if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
            ts.isVariableDeclaration(node.parent) &&
            ts.isIdentifier(node.parent.name)) {
            const name = node.parent.name.text;
            const variableStatement = node.parent.parent?.parent;
            const declarationNode = variableStatement && ts.isVariableStatement(variableStatement) ? variableStatement : undefined;
            pushFunction(name, node, {
                signature: node.parameters.map(parameter => parameter.name.getText(sourceFile)).join(', '),
                exported: !!variableStatement && hasExportModifier(ts, variableStatement),
                fullSignature: buildFullSignature(sourceFile, node),
                returnType: getReturnTypeText(sourceFile, node),
                jsdoc: declarationNode ? getLeadingJsDoc(sourceFile, declarationNode) : undefined,
                isAsync: hasModifierKind(ts, node, ts.SyntaxKind.AsyncKeyword) || undefined,
            });
            stack.push({ type: 'function', name, line: lineOf(sourceFile, node) });
            ts.forEachChild(node, visit);
            stack.pop();
            return;
        }
        else if (ts.isMethodDeclaration(node)) {
            const name = propertyName(ts, node.name, sourceFile);
            if (name) {
                pushFunction(name, node, {
                    signature: node.parameters.map(parameter => parameter.name.getText(sourceFile)).join(', '),
                    fullSignature: buildFullSignature(sourceFile, node),
                    returnType: getReturnTypeText(sourceFile, node),
                    jsdoc: getLeadingJsDoc(sourceFile, node),
                    isAsync: hasModifierKind(ts, node, ts.SyntaxKind.AsyncKeyword) || undefined,
                    isStatic: hasModifierKind(ts, node, ts.SyntaxKind.StaticKeyword) || undefined,
                    visibility: getVisibility(ts, node),
                });
                stack.push({ type: 'function', name, line: lineOf(sourceFile, node) });
                ts.forEachChild(node, visit);
                stack.pop();
                return;
            }
        }
        else if (ts.isClassDeclaration(node) && node.name?.text) {
            const name = node.name.text;
            pushUniqueSymbol(symbols, {
                type: 'class',
                name,
                line: lineOf(sourceFile, node),
                endLine: endLineOf(sourceFile, node),
                exported: hasExportModifier(ts, node),
                jsdoc: getLeadingJsDoc(sourceFile, node),
            });
            if (node.heritageClauses) {
                for (const clause of node.heritageClauses) {
                    for (const typeNode of clause.types) {
                        const expressionText = typeNode.expression.getText(sourceFile);
                        inheritances.push({
                            className: name,
                            classLine: lineOf(sourceFile, node),
                            baseName: expressionText.split('.').at(-1) ?? expressionText,
                            line: lineOf(sourceFile, typeNode),
                            kind: clause.token === ts.SyntaxKind.ImplementsKeyword ? 'implements' : 'extends',
                        });
                    }
                }
            }
            stack.push({ type: 'class', name, line: lineOf(sourceFile, node) });
            ts.forEachChild(node, visit);
            stack.pop();
            return;
        }
        else if (ts.isInterfaceDeclaration(node)) {
            const name = node.name.text;
            pushUniqueSymbol(symbols, {
                type: 'interface',
                name,
                line: lineOf(sourceFile, node),
                endLine: endLineOf(sourceFile, node),
                exported: hasExportModifier(ts, node),
                jsdoc: getLeadingJsDoc(sourceFile, node),
            });
            if (node.heritageClauses) {
                for (const clause of node.heritageClauses) {
                    for (const typeNode of clause.types) {
                        const expressionText = typeNode.expression.getText(sourceFile);
                        inheritances.push({
                            className: name,
                            classLine: lineOf(sourceFile, node),
                            baseName: expressionText.split('.').at(-1) ?? expressionText,
                            line: lineOf(sourceFile, typeNode),
                            kind: 'extends',
                        });
                    }
                }
            }
            ts.forEachChild(node, visit);
            return;
        }
        else if (ts.isTypeAliasDeclaration(node)) {
            pushUniqueSymbol(symbols, {
                type: 'type-alias',
                name: node.name.text,
                line: lineOf(sourceFile, node),
                endLine: endLineOf(sourceFile, node),
                exported: hasExportModifier(ts, node),
                jsdoc: getLeadingJsDoc(sourceFile, node),
            });
            return;
        }
        else if (ts.isEnumDeclaration(node)) {
            pushUniqueSymbol(symbols, {
                type: 'enum',
                name: node.name.text,
                line: lineOf(sourceFile, node),
                endLine: endLineOf(sourceFile, node),
                exported: hasExportModifier(ts, node),
                jsdoc: getLeadingJsDoc(sourceFile, node),
            });
            return;
        }
        else if (ts.isVariableStatement(node)) {
            const exported = hasExportModifier(ts, node);
            for (const declaration of node.declarationList.declarations) {
                const initializer = declaration.initializer;
                const isFunctionInitializer = !!initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer));
                if (ts.isIdentifier(declaration.name) && !isFunctionInitializer) {
                    pushUniqueSymbol(symbols, {
                        type: 'variable',
                        name: declaration.name.text,
                        line: lineOf(sourceFile, declaration),
                        endLine: endLineOf(sourceFile, declaration),
                        exported,
                    });
                }
            }
        }
        else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
            const specifier = node.moduleSpecifier.text;
            const bindings = [];
            if (node.exportClause && ts.isNamedExports(node.exportClause)) {
                for (const element of node.exportClause.elements) {
                    bindings.push({
                        localName: element.name.text,
                        importedName: element.propertyName?.text ?? element.name.text,
                    });
                }
            }
            else if (!node.exportClause) {
                bindings.push({ localName: '*', importedName: '*', isNamespace: true });
            }
            const resolvedPath = resolveImportPath(getLanguageSpec(filePath)?.name ?? 'javascript', context, filePath, specifier);
            imports.push({
                specifier,
                line: lineOf(sourceFile, node),
                kind: typescriptImportKind(specifier, resolvedPath),
                bindings,
                resolvedPath,
                isReExport: true,
            });
        }
        else if (ts.isCallExpression(node)) {
            const currentFunction = [...stack].reverse().find(entry => entry.type === 'function');
            if (currentFunction) {
                let calleeName = node.expression.getText(sourceFile);
                let qualifier;
                if (ts.isIdentifier(node.expression)) {
                    calleeName = node.expression.text;
                }
                else if (ts.isPropertyAccessExpression(node.expression)) {
                    calleeName = node.expression.name.text;
                    qualifier = node.expression.expression.getText(sourceFile);
                }
                calls.push({
                    callerName: currentFunction.name,
                    callerLine: currentFunction.line,
                    callerContainerName: [...stack].reverse().find(entry => entry.type === 'class')?.name,
                    calleeName,
                    qualifier,
                    line: lineOf(sourceFile, node),
                });
            }
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return {
        path: filePath,
        relativePath,
        moduleName: getModuleName(context.repoRoot, filePath),
        language: getLanguageSpec(filePath)?.name ?? 'javascript',
        content,
        lines: content.split(/\r?\n/),
        symbols,
        imports,
        calls,
        inheritances,
    };
}
function toPascalCase(value) {
    const words = value
        .replace(/\.[^.]+$/, '')
        .split(/[^A-Za-z0-9]+/)
        .filter(Boolean);
    const result = words.map(word => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`).join('');
    return result || 'VueComponent';
}
function inferVueComponentName(filePath, scriptContent) {
    // Only trust a `name:` field when it sits inside a component-options object
    // (defineOptions/defineComponent/export default), scoped to the script block —
    // a bare `name:` anywhere else is usually unrelated data.
    const explicit = scriptContent.match(/\bdefineOptions\s*\(\s*\{[\s\S]*?\bname\s*:\s*['"]([^'"]+)['"]/) ??
        scriptContent.match(/\bdefineComponent\s*\(\s*\{[\s\S]*?\bname\s*:\s*['"]([^'"]+)['"]/) ??
        scriptContent.match(/\bexport\s+default\s*\{[\s\S]*?\bname\s*:\s*['"]([^'"]+)['"]/);
    return explicit?.[1] ?? toPascalCase(path.basename(filePath));
}
function composeVueVirtualScript(content, blocks) {
    const virtualLines = content.split(/\r?\n/).map(() => '');
    for (const block of blocks) {
        if (!block?.content)
            continue;
        const startLine = Math.max(0, block.loc.start.line - 1);
        const blockLines = block.content.split(/\r?\n/);
        for (let offset = 0; offset < blockLines.length; offset++) {
            virtualLines[startLine + offset] = blockLines[offset];
        }
    }
    return virtualLines.join('\n');
}
async function parseVueFile(filePath, content, context) {
    const compiler = await loadVueCompiler();
    const { descriptor, errors } = compiler.parse(content, { filename: filePath });
    if (errors.length > 0) {
        const firstError = errors[0];
        const message = firstError instanceof Error ? firstError.message : String(firstError);
        throw new VueSfcParseError(`Unable to parse Vue SFC: ${message}`);
    }
    const relativePath = normalizePath(path.relative(context.repoRoot, filePath));
    const scriptContent = composeVueVirtualScript(content, [descriptor.script, descriptor.scriptSetup]);
    const parsed = scriptContent.trim()
        ? await parseTypescriptFile(filePath, scriptContent, context)
        : {
            path: filePath,
            relativePath,
            moduleName: getModuleName(context.repoRoot, filePath),
            language: 'vue',
            content,
            lines: content.split(/\r?\n/),
            symbols: [],
            imports: [],
            calls: [],
            inheritances: [],
        };
    parsed.language = 'vue';
    parsed.content = content;
    parsed.lines = content.split(/\r?\n/);
    const componentName = inferVueComponentName(filePath, scriptContent);
    const componentLine = descriptor.script?.loc.start.line ?? descriptor.scriptSetup?.loc.start.line ?? 1;
    pushUniqueSymbol(parsed.symbols, {
        type: 'class',
        name: componentName,
        line: componentLine,
        endLine: parsed.lines?.length ?? content.split(/\r?\n/).length,
        exported: true,
        signature: 'Vue component',
    });
    return parsed;
}
// Parses a single Go import spec (the `"path"` / `alias "path"` fragment that
// follows `import` on a line or stands alone inside a grouped `import ( ... )`
// block). The alias may be an identifier, `_` (blank), or `.` (dot import).
function parseGoImportSpec(spec) {
    const match = spec.match(/^(?:([A-Za-z0-9_.]+)\s+)?["`]([^"`]+)["`]/);
    if (!match)
        return undefined;
    const specifier = match[2];
    return {
        specifier,
        line: 0,
        kind: 'external',
        bindings: [{ localName: match[1] || specifier.split('/').at(-1) || specifier, importedName: specifier }],
    };
}
function parseImportsFromLine(line, language, context, filePath) {
    const imports = [];
    if (language === 'python') {
        const direct = line.match(/^import\s+(.+)$/);
        if (direct) {
            for (const part of direct[1].split(',')) {
                const trimmed = part.trim();
                const [name, alias] = trimmed.split(/\s+as\s+/);
                const resolvedPath = resolvePythonImport(context.repoRoot, filePath, name.trim());
                imports.push({
                    specifier: name.trim(),
                    line: 0,
                    kind: pythonImportKind(name.trim(), resolvedPath),
                    bindings: [{ localName: (alias || name).trim(), importedName: name.trim() }],
                    resolvedPath,
                });
            }
        }
        const from = line.match(/^from\s+([A-Za-z0-9_./]+)\s+import\s+(.+)$/);
        if (from) {
            const specifier = from[1].trim();
            const resolvedPath = resolvePythonImport(context.repoRoot, filePath, specifier);
            const names = from[2]
                .split(',')
                .map(part => part.trim())
                .filter(Boolean);
            imports.push({
                specifier,
                line: 0,
                kind: pythonImportKind(specifier, resolvedPath),
                bindings: names.map(name => {
                    const [importedName, localName] = name.split(/\s+as\s+/).map(part => part.trim());
                    return { localName: localName || importedName, importedName };
                }),
                resolvedPath,
            });
        }
        return imports;
    }
    if (language === 'go') {
        const single = line.match(/^import\s+(.+)$/);
        if (single) {
            const parsed = parseGoImportSpec(single[1].trim());
            if (parsed)
                imports.push(parsed);
        }
        return imports;
    }
    if (language === 'rust') {
        const match = line.match(/^use\s+(.+);$/);
        if (match) {
            const specifier = match[1].trim();
            imports.push({
                specifier,
                line: 0,
                kind: 'external',
                bindings: [{ localName: specifier.split('::').at(-1) || specifier, importedName: specifier }],
            });
        }
        return imports;
    }
    // Strip an optional leading `#` so C/C++ `#include <...>` / `#include "..."`
    // directives are recognized alongside plain import/require forms.
    const generic = line.replace(/^#\s*/, '').match(/^(?:import|include|require)\s+["<]?([^">;]+)[">]?/);
    if (generic) {
        const specifier = generic[1].trim();
        imports.push({
            specifier,
            line: 0,
            kind: isLocalSpecifier(specifier) && !isAssetSpecifier(specifier) ? 'local' : 'external',
            bindings: [],
        });
    }
    return imports;
}
/**
 * Extracts every import across `lines`, stamping each with its 1-based line
 * number. Handles Go's grouped `import ( ... )` blocks, whose inner specs are
 * bare quoted paths that no single-line pattern matches.
 */
function collectImportsFromLines(lines, language, context, filePath) {
    const imports = [];
    let inGoImportBlock = false;
    for (let index = 0; index < lines.length; index++) {
        const trimmed = lines[index].trim();
        const lineNumber = index + 1;
        if (language === 'go') {
            if (inGoImportBlock) {
                if (trimmed.startsWith(')')) {
                    inGoImportBlock = false;
                    continue;
                }
                const grouped = parseGoImportSpec(trimmed);
                if (grouped)
                    imports.push({ ...grouped, line: lineNumber });
                continue;
            }
            if (/^import\s*\(\s*$/.test(trimmed)) {
                inGoImportBlock = true;
                continue;
            }
        }
        for (const parsedImport of parseImportsFromLine(trimmed, language, context, filePath)) {
            imports.push({
                ...parsedImport,
                line: lineNumber,
                resolvedPath: parsedImport.resolvedPath ?? resolveImportPath(language, context, filePath, parsedImport.specifier),
            });
        }
    }
    return imports;
}
function parseRegexFile(filePath, content, context) {
    const language = getLanguageSpec(filePath)?.name ?? 'generic';
    const relativePath = normalizePath(path.relative(context.repoRoot, filePath));
    const lines = content.split(/\r?\n/);
    const symbols = [];
    const imports = [];
    const calls = [];
    const inheritances = [];
    const classStack = [];
    const functionStack = [];
    const currentFunction = (indent) => {
        while (functionStack.length > 0 && indent <= functionStack[functionStack.length - 1].indent) {
            functionStack.pop();
        }
        return functionStack[functionStack.length - 1];
    };
    const currentClass = (indent) => {
        while (classStack.length > 0 && indent <= classStack[classStack.length - 1].indent) {
            classStack.pop();
        }
        return classStack[classStack.length - 1];
    };
    // Imports are collected up front so grouped/multi-line forms (e.g. Go's
    // `import ( ... )`) and directive lines skipped as comments below are still
    // captured.
    imports.push(...collectImportsFromLines(lines, language, context, filePath));
    for (let index = 0; index < lines.length; index++) {
        const line = lines[index];
        const lineNumber = index + 1;
        const trimmed = line.trim();
        const indent = line.match(/^\s*/)?.[0].length ?? 0;
        if (trimmed.length === 0 || trimmed.startsWith('#') || trimmed.startsWith('//'))
            continue;
        if (language === 'python') {
            const classMatch = trimmed.match(/^class\s+([A-Za-z_][A-Za-z0-9_]*)(?:\(([^)]+)\))?:/);
            if (classMatch) {
                const name = classMatch[1];
                symbols.push({ type: 'class', name, line: lineNumber, endLine: lineNumber });
                classStack.push({ indent, name });
                if (classMatch[2]) {
                    for (const baseName of classMatch[2]
                        .split(',')
                        .map(value => value.trim())
                        .filter(Boolean)) {
                        inheritances.push({ className: name, classLine: lineNumber, baseName, line: lineNumber });
                    }
                }
                continue;
            }
            const functionMatch = trimmed.match(/^def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\):/);
            if (functionMatch) {
                const container = currentClass(indent);
                symbols.push({
                    type: 'function',
                    name: functionMatch[1],
                    line: lineNumber,
                    endLine: lineNumber,
                    containerName: container?.name,
                    signature: functionMatch[2],
                });
                functionStack.push({ indent, name: functionMatch[1], containerName: container?.name, line: lineNumber });
                continue;
            }
        }
        else if (language === 'go') {
            const typeMatch = trimmed.match(/^type\s+([A-Za-z_][A-Za-z0-9_]*)\s+(?:struct|interface)/);
            if (typeMatch) {
                symbols.push({ type: 'class', name: typeMatch[1], line: lineNumber, endLine: lineNumber });
                continue;
            }
            const methodMatch = trimmed.match(/^func\s+\(\s*[^)]+\)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/);
            if (methodMatch) {
                symbols.push({
                    type: 'function',
                    name: methodMatch[1],
                    line: lineNumber,
                    endLine: lineNumber,
                    signature: methodMatch[2],
                });
                functionStack.push({ indent, name: methodMatch[1], line: lineNumber });
                continue;
            }
            const functionMatch = trimmed.match(/^func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/);
            if (functionMatch) {
                symbols.push({
                    type: 'function',
                    name: functionMatch[1],
                    line: lineNumber,
                    endLine: lineNumber,
                    signature: functionMatch[2],
                });
                functionStack.push({ indent, name: functionMatch[1], line: lineNumber });
                continue;
            }
        }
        else if (language === 'rust') {
            const classMatch = trimmed.match(/^(?:struct|trait|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/);
            if (classMatch) {
                symbols.push({ type: 'class', name: classMatch[1], line: lineNumber, endLine: lineNumber });
                continue;
            }
            const implMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+([A-Za-z_][A-Za-z0-9_]*)/);
            if (implMatch) {
                classStack.push({ indent, name: implMatch[1] });
                continue;
            }
            const functionMatch = trimmed.match(/^fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/);
            if (functionMatch) {
                const container = currentClass(indent);
                symbols.push({
                    type: 'function',
                    name: functionMatch[1],
                    line: lineNumber,
                    endLine: lineNumber,
                    containerName: container?.name,
                    signature: functionMatch[2],
                });
                functionStack.push({ indent, name: functionMatch[1], containerName: container?.name, line: lineNumber });
                continue;
            }
        }
        else {
            const classMatch = trimmed.match(/^(?:class|interface)\s+([A-Za-z_][A-Za-z0-9_]*)/);
            if (classMatch) {
                symbols.push({ type: 'class', name: classMatch[1], line: lineNumber, endLine: lineNumber });
                continue;
            }
            const functionMatch = trimmed.match(/^(?:def|function)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)/);
            if (functionMatch) {
                symbols.push({
                    type: 'function',
                    name: functionMatch[1],
                    line: lineNumber,
                    endLine: lineNumber,
                    signature: functionMatch[2],
                });
                functionStack.push({ indent, name: functionMatch[1], line: lineNumber });
                continue;
            }
        }
        const activeFunction = currentFunction(indent);
        if (!activeFunction)
            continue;
        const callPattern = /\b([A-Za-z_][A-Za-z0-9_]*)(?:\.([A-Za-z_][A-Za-z0-9_]*))?\s*\(/g;
        let match;
        while ((match = callPattern.exec(trimmed)) !== null) {
            const qualifier = match[2] ? match[1] : undefined;
            const calleeName = match[2] || match[1];
            if (calleeName === 'if' ||
                calleeName === 'for' ||
                calleeName === 'while' ||
                calleeName === 'switch' ||
                calleeName === 'return') {
                continue;
            }
            calls.push({
                callerName: activeFunction.name,
                callerLine: activeFunction.line,
                callerContainerName: activeFunction.containerName,
                calleeName,
                qualifier,
                line: lineNumber,
            });
        }
    }
    return {
        path: filePath,
        relativePath,
        moduleName: getModuleName(context.repoRoot, filePath),
        language,
        content,
        lines,
        symbols,
        imports,
        calls,
        inheritances,
    };
}
function nodeLine(node) {
    return node.startPosition.row + 1;
}
function nodeEndLine(node) {
    return node.endPosition.row + 1;
}
function nodeStartColumn(node) {
    return node.startPosition.column + 1;
}
function nodeEndColumn(node) {
    return node.endPosition.column + 1;
}
function nodeKey(node) {
    return `${node.type}:${node.startIndex}:${node.endIndex}`;
}
const NAME_NODE_TYPES = new Set([
    'identifier',
    'field_identifier',
    'type_identifier',
    'constant',
    'property_identifier',
    'simple_identifier',
]);
const PARAMETER_NODE_TYPES = new Set([
    'parameters',
    'parameter_list',
    'formal_parameters',
    'function_value_parameters',
    'argument_list',
]);
function namedChildren(node) {
    const children = [];
    for (let index = 0; index < node.namedChildCount; index++) {
        const child = node.namedChild(index);
        if (child)
            children.push(child);
    }
    return children;
}
function findFirstDescendant(node, predicate) {
    if (predicate(node))
        return node;
    for (const child of namedChildren(node)) {
        const found = findFirstDescendant(child, predicate);
        if (found)
            return found;
    }
    return null;
}
function findLastDescendant(node, predicate) {
    let found = predicate(node) ? node : null;
    for (const child of namedChildren(node)) {
        const childFound = findLastDescendant(child, predicate);
        if (childFound)
            found = childFound;
    }
    return found;
}
function nameNodeFromDefinition(node) {
    const fieldName = node.childForFieldName('name');
    if (fieldName)
        return fieldName;
    const declarator = node.childForFieldName('declarator');
    if (declarator) {
        const declaratorName = findFirstDescendant(declarator, candidate => NAME_NODE_TYPES.has(candidate.type));
        if (declaratorName)
            return declaratorName;
    }
    const type = node.childForFieldName('type');
    if (type) {
        const typeName = findFirstDescendant(type, candidate => NAME_NODE_TYPES.has(candidate.type));
        if (typeName)
            return typeName;
    }
    return findFirstDescendant(node, candidate => NAME_NODE_TYPES.has(candidate.type));
}
function definitionName(node) {
    return nameNodeFromDefinition(node)?.text;
}
function parametersText(node) {
    const direct = node.childForFieldName('parameters');
    if (direct)
        return direct.text;
    return findFirstDescendant(node, candidate => PARAMETER_NODE_TYPES.has(candidate.type))?.text;
}
function expressionName(node) {
    if (!node)
        return {};
    if (NAME_NODE_TYPES.has(node.type))
        return { calleeName: node.text };
    const attribute = node.childForFieldName('attribute') ?? node.childForFieldName('field') ?? node.childForFieldName('name');
    if (attribute) {
        const object = node.childForFieldName('object');
        const qualifier = object && NAME_NODE_TYPES.has(object.type) ? object.text : undefined;
        return { calleeName: attribute.text, qualifier };
    }
    const scopedName = findLastDescendant(node, candidate => NAME_NODE_TYPES.has(candidate.type));
    return { calleeName: scopedName?.text };
}
function callFromNode(node) {
    return expressionName(node.childForFieldName('function') ?? node.childForFieldName('name') ?? node.namedChild(0));
}
function collectTypeNames(node) {
    const names = new Set();
    const visit = (candidate) => {
        if (NAME_NODE_TYPES.has(candidate.type)) {
            names.add(candidate.text);
            return;
        }
        for (const child of namedChildren(candidate))
            visit(child);
    };
    visit(node);
    return [...names];
}
function inheritanceNames(node, className) {
    const fields = ['superclasses', 'superclass', 'interfaces', 'base_clause', 'supertype', 'supertype_list'];
    const names = new Set();
    for (const field of fields) {
        const value = node.childForFieldName(field);
        if (!value)
            continue;
        for (const name of collectTypeNames(value)) {
            if (name !== className)
                names.add(name);
        }
    }
    return [...names];
}
async function treeSitterTagIndex(grammar, root) {
    const captures = await collectTreeSitterTagCaptures(grammar, root);
    const index = {
        functions: new Set(),
        classes: new Set(),
        calls: new Set(),
    };
    for (const capture of captures ?? []) {
        if (capture.name === 'definition.function' || capture.name === 'definition.method') {
            index.functions.add(nodeKey(capture.node));
        }
        else if (capture.name === 'definition.class' ||
            capture.name === 'definition.interface' ||
            capture.name === 'definition.type' ||
            capture.name === 'definition.enum') {
            index.classes.add(nodeKey(capture.node));
        }
        else if (capture.name === 'reference.call') {
            index.calls.add(nodeKey(capture.node));
        }
    }
    return index;
}
async function parseTreeSitterFile(filePath, content, context, descriptor) {
    const tree = await parseTreeSitterContent(descriptor.grammar, content);
    if (!tree)
        return null;
    const language = getLanguageSpec(filePath)?.name ?? 'generic';
    const relativePath = normalizePath(path.relative(context.repoRoot, filePath));
    const lines = content.split(/\r?\n/);
    const symbols = [];
    const imports = [];
    const calls = [];
    const inheritances = [];
    const functionNodeTypes = new Set(descriptor.functionNodeTypes);
    const classNodeTypes = new Set(descriptor.classNodeTypes);
    const callNodeTypes = new Set(descriptor.callNodeTypes);
    const tagIndex = await treeSitterTagIndex(descriptor.grammar, tree.rootNode);
    imports.push(...collectImportsFromLines(lines, language, context, filePath));
    const visit = (node, containers, activeFunction) => {
        let nextContainers = containers;
        let nextFunction = activeFunction;
        const key = nodeKey(node);
        const pendingCall = activeFunction && (callNodeTypes.has(node.type) || tagIndex.calls.has(key)) ? callFromNode(node) : undefined;
        if (classNodeTypes.has(node.type) || tagIndex.classes.has(key)) {
            const name = definitionName(node);
            if (name) {
                const line = nodeLine(node);
                symbols.push({
                    type: node.type.includes('interface') || node.type.includes('trait') || node.type.includes('protocol')
                        ? 'interface'
                        : 'class',
                    name,
                    line,
                    endLine: nodeEndLine(node),
                    startColumn: nodeStartColumn(node),
                    endColumn: nodeEndColumn(node),
                    exported: true,
                });
                nextContainers = [...containers, { name, line }];
                for (const baseName of inheritanceNames(node, name)) {
                    inheritances.push({ className: name, classLine: line, baseName, line });
                }
            }
        }
        if (functionNodeTypes.has(node.type) || tagIndex.functions.has(key)) {
            const name = definitionName(node);
            if (name) {
                const line = nodeLine(node);
                const container = nextContainers.at(-1);
                symbols.push({
                    type: 'function',
                    name,
                    line,
                    endLine: nodeEndLine(node),
                    startColumn: nodeStartColumn(node),
                    endColumn: nodeEndColumn(node),
                    containerName: container?.name,
                    signature: parametersText(node),
                    exported: true,
                });
                nextFunction = { name, line, containerName: container?.name };
            }
        }
        for (const child of namedChildren(node))
            visit(child, nextContainers, nextFunction);
        if (activeFunction && pendingCall?.calleeName) {
            calls.push({
                callerName: activeFunction.name,
                callerLine: activeFunction.line,
                callerContainerName: activeFunction.containerName,
                calleeName: pendingCall.calleeName,
                qualifier: pendingCall.qualifier,
                line: nodeLine(node),
            });
        }
    };
    // The returned ParsedFile holds only plain data (symbols/imports/calls copy node
    // positions/text out during the traversal above and in tagIndex), so the WASM tree
    // handle can be freed here — no TreeSitterNode reference escapes this function.
    try {
        visit(tree.rootNode, []);
        return {
            path: filePath,
            relativePath,
            moduleName: getModuleName(context.repoRoot, filePath),
            language,
            content,
            lines,
            symbols,
            imports,
            calls,
            inheritances,
        };
    }
    finally {
        tree.delete();
    }
}
export async function parseCodeFile(filePath, context, guardrails = {}) {
    const spec = getLanguageSpec(filePath);
    if (!spec)
        return null;
    const fileStat = await stat(filePath);
    const maxFileSizeBytes = guardrails.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
    if (fileStat.size > maxFileSizeBytes) {
        return {
            kind: 'skipped',
            reason: `Exceeds max file size of ${maxFileSizeBytes} bytes`,
        };
    }
    const content = await readFile(filePath, 'utf8');
    const lines = content.split(/\r?\n/);
    const maxLines = guardrails.maxLines ?? DEFAULT_MAX_LINES;
    if (lines.length > maxLines) {
        return {
            kind: 'skipped',
            reason: `Exceeds max line count of ${maxLines}`,
        };
    }
    const skipMinified = guardrails.skipMinified ?? true;
    if (skipMinified && fileStat.size >= MINIFIED_FILE_MIN_BYTES && isLikelyMinified(lines)) {
        return {
            kind: 'skipped',
            reason: 'Likely minified source',
        };
    }
    switch (spec.parser) {
        case 'typescript':
            return { kind: 'parsed', file: await parseTypescriptFile(filePath, content, context) };
        case 'vue':
            try {
                return { kind: 'parsed', file: await parseVueFile(filePath, content, context) };
            }
            catch (error) {
                // A structurally broken SFC is reported as a skipped file rather than
                // aborting the surrounding batch, matching the other parsers' tolerance
                // of malformed input.
                if (error instanceof VueSfcParseError) {
                    return { kind: 'skipped', reason: error.reason };
                }
                throw error;
            }
        case 'python':
        case 'go':
        case 'rust':
        case 'generic':
            if (spec.treeSitter) {
                const treeSitterFile = await parseTreeSitterFile(filePath, content, context, spec.treeSitter);
                if (treeSitterFile)
                    return { kind: 'parsed', file: treeSitterFile };
            }
            return { kind: 'parsed', file: parseRegexFile(filePath, content, context) };
        default:
            return null;
    }
}
function isLikelyMinified(lines) {
    const nonEmptyLines = lines.filter(line => line.trim().length > 0);
    if (nonEmptyLines.length === 0)
        return false;
    let totalLength = 0;
    let longLineCount = 0;
    let maxLineLength = 0;
    for (const line of nonEmptyLines) {
        totalLength += line.length;
        if (line.length > 120)
            longLineCount++;
        if (line.length > maxLineLength)
            maxLineLength = line.length;
    }
    const averageLength = totalLength / nonEmptyLines.length;
    return ((nonEmptyLines.length <= 20 && maxLineLength >= 2_000) ||
        (averageLength >= 300 && longLineCount / nonEmptyLines.length >= 0.9));
}
//# sourceMappingURL=parsers.js.map