import path from 'node:path';
import { InvalidInputError } from '../errors.js';
import { estimateTokenCount, sliceByTokens } from '../tokens.js';
import { abortIfNeeded, compareCodeUnits, createYielder } from '../utils.js';
import { normalizePath } from './languages.js';
import { acquireTreeSitterQuery, collectTreeSitterErrorNodes, openTreeSitterParseCache, parseTreeSitterContent, } from './tree-sitter.js';
import { Utf8SourceIndex } from './source-span.js';
const STRUCTURAL_LANGUAGES = [
    { name: 'javascript', grammar: 'javascript', extensions: ['js', 'jsx', 'mjs', 'cjs', 'es6'] },
    { name: 'typescript', grammar: 'typescript', extensions: ['ts', 'mts', 'cts'] },
    { name: 'tsx', grammar: 'tsx', extensions: ['tsx'] },
    { name: 'python', grammar: 'python', extensions: ['py'] },
    { name: 'go', grammar: 'go', extensions: ['go'] },
    { name: 'rust', grammar: 'rust', extensions: ['rs'] },
    { name: 'java', grammar: 'java', extensions: ['java'] },
    { name: 'ruby', grammar: 'ruby', extensions: ['rb'] },
    { name: 'php', grammar: 'php', extensions: ['php', 'phtml'] },
    { name: 'c', grammar: 'c', extensions: ['c', 'h'] },
    { name: 'cpp', grammar: 'cpp', extensions: ['cpp', 'cc', 'cxx', 'hpp', 'hh', 'hxx'] },
    { name: 'csharp', grammar: 'c-sharp', extensions: ['cs'] },
    { name: 'kotlin', grammar: 'kotlin', extensions: ['kt', 'kts'] },
    { name: 'scala', grammar: 'scala', extensions: ['scala', 'sc', 'sbt'] },
    { name: 'swift', grammar: 'swift', extensions: ['swift'] },
];
const LANGUAGE_ALIASES = {
    js: 'javascript',
    jsx: 'javascript',
    ts: 'typescript',
    'c-sharp': 'csharp',
    cs: 'csharp',
    'c++': 'cpp',
};
export function structuralLanguages() {
    return STRUCTURAL_LANGUAGES;
}
export function resolveStructuralLanguage(language) {
    const normalized = language.trim().toLowerCase();
    const name = LANGUAGE_ALIASES[normalized] ?? normalized;
    return STRUCTURAL_LANGUAGES.find(candidate => candidate.name === name) ?? null;
}
export function inferStructuralLanguage(filePath) {
    const extension = path.extname(filePath).toLowerCase().replace(/^\./, '');
    return STRUCTURAL_LANGUAGES.find(candidate => candidate.extensions.includes(extension)) ?? null;
}
const DEFAULT_QUERY_LIMIT = 50;
const DEFAULT_MAP_TOKENS = 4_096;
const QUERY_MATCH_LIMIT = 65_536;
const MAX_CAPTURE_SCAN = 100_000;
const DEFAULT_MAX_QUERY_BYTES = 65_536;
const DEFAULT_MAX_SOURCE_BYTES = 2_097_152;
const DEFAULT_MAX_TREE_NODES = 250_000;
const DEFAULT_MAX_DIAGNOSTICS = 100;
const DEFAULT_MAX_JOIN_CANDIDATES = 20;
const DEFAULT_MAX_JOIN_NODES = 10_000;
const DEFAULT_MAX_RENDER_CHARACTERS = 1_048_576;
const MAX_CONFIGURABLE_SOURCE_BYTES = 16_777_216;
const MAX_CONFIGURABLE_TREE_NODES = 1_000_000;
const MAX_CONFIGURABLE_RENDER_CHARACTERS = 4_194_304;
function positiveInteger(value, fallback, label, maximum) {
    const resolved = value ?? fallback;
    if (!Number.isInteger(resolved) || resolved <= 0) {
        throw new InvalidInputError(`${label} must be a positive integer.`);
    }
    if (maximum !== undefined && resolved > maximum) {
        throw new InvalidInputError(`${label} must not exceed ${maximum}.`);
    }
    return resolved;
}
const joinNodesByIndex = new WeakMap();
const joinSearchByNodes = new WeakMap();
function compareNodeSpecificity(left, right) {
    const leftStart = left.node.line ?? 1;
    const rightStart = right.node.line ?? 1;
    const leftLineSpan = (left.node.endLine ?? leftStart) - leftStart;
    const rightLineSpan = (right.node.endLine ?? rightStart) - rightStart;
    if (leftLineSpan !== rightLineSpan)
        return leftLineSpan - rightLineSpan;
    const leftExact = left.exactStartByte !== undefined && left.exactEndByte !== undefined;
    const rightExact = right.exactStartByte !== undefined && right.exactEndByte !== undefined;
    if (leftExact !== rightExact)
        return leftExact ? -1 : 1;
    if (leftExact && rightExact) {
        const leftByteSpan = (left.exactEndByte ?? 0) - (left.exactStartByte ?? 0);
        const rightByteSpan = (right.exactEndByte ?? 0) - (right.exactStartByte ?? 0);
        if (leftByteSpan !== rightByteSpan)
            return leftByteSpan - rightByteSpan;
    }
    else if (leftLineSpan === 0) {
        const leftColumnSpan = (left.node.endColumn ?? 1) - (left.node.startColumn ?? 1);
        const rightColumnSpan = (right.node.endColumn ?? 1) - (right.node.startColumn ?? 1);
        if (leftColumnSpan !== rightColumnSpan)
            return leftColumnSpan - rightColumnSpan;
    }
    return 0;
}
function compareIndexedNodes(left, right) {
    return compareNodeSpecificity(left, right) || compareCodeUnits(left.node.id, right.node.id);
}
function containsCapture(indexed, span) {
    if (indexed.exactStartByte !== undefined && indexed.exactEndByte !== undefined) {
        return indexed.exactStartByte <= span.startByte && indexed.exactEndByte >= span.endByte;
    }
    const node = indexed.node;
    if (node.line === undefined)
        return false;
    const nodeStartLine = node.line;
    const nodeEndLine = node.endLine ?? nodeStartLine;
    if (nodeStartLine > span.startLine || nodeEndLine < span.endLine)
        return false;
    if (nodeStartLine === span.startLine && node.startColumn !== undefined) {
        // Both compiler-index columns and span columns count UTF-16 code units,
        // and both are 1-based here.
        if (node.startColumn > span.startColumn)
            return false;
    }
    if (nodeEndLine === span.endLine && node.endColumn !== undefined) {
        if (node.endColumn < span.endColumn)
            return false;
    }
    return true;
}
function candidateFromNode(node) {
    return { nodeId: node.id, name: node.name, type: node.type, line: node.line, endLine: node.endLine };
}
export function joinSpanToNode(nodes, span, maxJoinNodes, maxCandidates) {
    let search = joinSearchByNodes.get(nodes);
    if (!search) {
        search = buildJoinSearch(nodes);
        joinSearchByNodes.set(nodes, search);
    }
    const exactUpper = upperBoundExactStart(search.exactByStartByte, span.startByte);
    const exactLower = lowerBoundPrefixEnd(search.exactPrefixMaxEndByte, span.endByte, exactUpper);
    const containing = [];
    for (let index = exactLower; index < exactUpper; index += 1) {
        const node = search.exactByStartByte[index];
        if (node && containsCapture(node, span))
            containing.push(node);
    }
    // Inexact legacy nodes cannot participate in the byte window. They remain
    // in a small, always-scanned bucket so an optimization never weakens the
    // line/column fallback.
    for (const node of search.fallbackBySpecificity) {
        if (containsCapture(node, span))
            containing.push(node);
    }
    containing.sort(compareIndexedNodes);
    const inspected = Math.min(maxJoinNodes, containing.length);
    let best;
    const ties = [];
    for (let index = 0; index < inspected; index += 1) {
        const node = containing[index];
        if (!node)
            continue;
        if (!best) {
            best = node;
            ties.push(node);
            continue;
        }
        const comparison = compareNodeSpecificity(node, best);
        if (comparison > 0)
            break;
        if (comparison === 0)
            ties.push(node);
    }
    const next = containing[inspected];
    const searchTruncated = inspected < containing.length && (!best || (next !== undefined && compareNodeSpecificity(next, best) <= 0));
    if (!best)
        return { ambiguous: false, searchTruncated };
    const basis = best.exactStartByte !== undefined && best.exactEndByte !== undefined
        ? 'exact-span'
        : 'line-span';
    if (ties.length > 1 || searchTruncated) {
        return {
            ambiguous: true,
            basis,
            candidates: ties.slice(0, maxCandidates).map(candidate => candidateFromNode(candidate.node)),
            searchTruncated,
        };
    }
    return { node: best.node, ambiguous: false, basis, searchTruncated: false };
}
function buildJoinSearch(nodes) {
    const exactByStartByte = nodes
        .filter(node => node.exactStartByte !== undefined && node.exactEndByte !== undefined)
        .sort((left, right) => (left.exactStartByte ?? 0) - (right.exactStartByte ?? 0) ||
        (right.exactEndByte ?? 0) - (left.exactEndByte ?? 0) ||
        compareIndexedNodes(left, right));
    const exactPrefixMaxEndByte = [];
    let maximum = -1;
    for (const node of exactByStartByte) {
        maximum = Math.max(maximum, node.exactEndByte ?? -1);
        exactPrefixMaxEndByte.push(maximum);
    }
    return {
        exactByStartByte,
        exactPrefixMaxEndByte,
        fallbackBySpecificity: nodes
            .filter(node => node.exactStartByte === undefined || node.exactEndByte === undefined)
            .sort(compareIndexedNodes),
    };
}
function upperBoundExactStart(nodes, startByte) {
    let low = 0;
    let high = nodes.length;
    while (low < high) {
        const middle = (low + high) >> 1;
        if ((nodes[middle]?.exactStartByte ?? Number.POSITIVE_INFINITY) <= startByte)
            low = middle + 1;
        else
            high = middle;
    }
    return low;
}
function lowerBoundPrefixEnd(prefixEnds, endByte, upper) {
    let low = 0;
    let high = upper;
    while (low < high) {
        const middle = (low + high) >> 1;
        if ((prefixEnds[middle] ?? -1) >= endByte)
            high = middle;
        else
            low = middle + 1;
    }
    return low;
}
/**
 * Bounded single-line rendering of a matched region. Avoids materializing the
 * full text of a capture: one match can span a multi-megabyte file, and a
 * broad query can return tens of thousands of them. Offsets are UTF-16 —
 * exactly what String#slice expects — never UTF-8 bytes.
 */
export function compactSourceText(source, startIndex, endIndex) {
    const start = Math.max(0, startIndex);
    const end = Math.min(endIndex, start + 1_024, source.length);
    const prefix = source.slice(start, end);
    const compact = prefix.replace(/\s+/g, ' ').trim();
    const clipped = compact.length <= 240 ? compact : `${compact.slice(0, 237)}...`;
    return endIndex > end && !clipped.endsWith('...') ? `${clipped.slice(0, 237)}...` : clipped;
}
export function matchesLanguagePath(relativePath, language) {
    const extension = path.extname(relativePath).toLowerCase().replace(/^\./, '');
    return language.extensions.includes(extension);
}
export function filesForLanguage(index, language) {
    return index.files.filter(file => matchesLanguagePath(file.relativePath, language));
}
export function normalizeScopePrefixes(files) {
    if (!files || files.length === 0)
        return undefined;
    const prefixes = files.map(file => normalizePath(file.trim()).replace(/\/+$/, ''));
    for (const prefix of prefixes) {
        if (prefix.length === 0 ||
            prefix === '.' ||
            prefix === '..' ||
            prefix.startsWith('../') ||
            path.isAbsolute(prefix)) {
            throw new InvalidInputError(`Structural query file scope "${prefix}" must be a repo-relative path.`);
        }
    }
    return prefixes;
}
export function matchesScope(relativePath, prefixes) {
    if (!prefixes)
        return true;
    const normalized = normalizePath(relativePath);
    return prefixes.some(prefix => normalized === prefix || normalized.startsWith(`${prefix}/`));
}
/**
 * Sorted per-path join candidates for capture-to-node containment joins.
 * Exact spans come from the node itself or, as a fallback, from the parsed
 * file's symbol table keyed by nodeId.
 */
export function indexJoinNodesByPath(index) {
    const cached = joinNodesByIndex.get(index);
    if (cached)
        return cached;
    const symbolSpanByNodeId = new Map();
    for (const file of index.files) {
        for (const symbol of file.symbols) {
            if (symbol.nodeId && symbol.span)
                symbolSpanByNodeId.set(symbol.nodeId, symbol.span);
        }
    }
    const nodesByPath = new Map();
    for (const node of index.nodes) {
        if (!node.relativePath || node.type === 'file' || node.type === 'module')
            continue;
        const normalized = normalizePath(node.relativePath);
        const span = node.span ?? symbolSpanByNodeId.get(node.id);
        const indexed = {
            node,
            exactStartByte: span?.exact ? span.startByte : undefined,
            exactEndByte: span?.exact ? span.endByte : undefined,
        };
        const existing = nodesByPath.get(normalized);
        if (existing)
            existing.push(indexed);
        else
            nodesByPath.set(normalized, [indexed]);
    }
    for (const nodes of nodesByPath.values()) {
        nodes.sort(compareIndexedNodes);
        joinSearchByNodes.set(nodes, buildJoinSearch(nodes));
    }
    joinNodesByIndex.set(index, nodesByPath);
    return nodesByPath;
}
function validateContainingLines(range) {
    if (!range)
        return undefined;
    if (!Number.isInteger(range.startLine) ||
        range.startLine <= 0 ||
        !Number.isInteger(range.endLine) ||
        range.endLine < range.startLine) {
        throw new InvalidInputError('Structural query containingLines must satisfy 1 <= startLine <= endLine.');
    }
    return range;
}
function diagnosticCollector(maxDiagnostics) {
    const collector = {
        values: [],
        omitted: 0,
        add(message) {
            if (collector.values.length < maxDiagnostics)
                collector.values.push(message);
            else
                collector.omitted += 1;
        },
    };
    return collector;
}
async function inspectTree(root, maxNodes, maxDiagnostics, signal, yielder) {
    abortIfNeeded(signal);
    await yielder.tick();
    const nodesVisited = root.descendantCount;
    if (nodesVisited > maxNodes) {
        return { nodesVisited: maxNodes, nodeLimitHit: true, syntaxIssues: [], syntaxIssueCount: 0 };
    }
    const syntaxIssues = collectTreeSitterErrorNodes(root, {
        signal,
        maxNodes,
        maxDiagnostics,
    }).map(issue => ({ type: issue.type, line: issue.line, column: issue.column }));
    // Error discovery is deliberately bounded to the reported diagnostic set.
    // `hasError` remains the complete file-level signal; the numeric count is a
    // bounded diagnostic count rather than a full-CST issue census.
    return {
        nodesVisited,
        nodeLimitHit: false,
        syntaxIssues,
        syntaxIssueCount: syntaxIssues.length,
    };
}
export async function queryTreeSitter(index, options) {
    const language = resolveStructuralLanguage(options.language);
    if (!language) {
        throw new InvalidInputError(`Unsupported structural query language "${options.language}". Expected one of: ${STRUCTURAL_LANGUAGES.map(item => item.name).join(', ')}`);
    }
    const queryText = options.query;
    if (queryText.trim().length === 0)
        throw new InvalidInputError('Structural query must not be empty.');
    const limit = positiveInteger(options.limit, DEFAULT_QUERY_LIMIT, 'Structural query limit');
    const mapTokens = positiveInteger(options.mapTokens, DEFAULT_MAP_TOKENS, 'Structural query mapTokens');
    const maxQueryBytes = positiveInteger(options.maxQueryBytes, DEFAULT_MAX_QUERY_BYTES, 'Structural query maxQueryBytes');
    const maxSourceBytes = positiveInteger(options.maxSourceBytes, DEFAULT_MAX_SOURCE_BYTES, 'Structural query maxSourceBytes', MAX_CONFIGURABLE_SOURCE_BYTES);
    const maxNodes = positiveInteger(options.maxNodesPerFile, DEFAULT_MAX_TREE_NODES, 'Structural query maxNodesPerFile', MAX_CONFIGURABLE_TREE_NODES);
    const maxDiagnostics = positiveInteger(options.maxDiagnostics, DEFAULT_MAX_DIAGNOSTICS, 'Structural query maxDiagnostics');
    const maxJoinCandidates = positiveInteger(options.maxJoinCandidates, DEFAULT_MAX_JOIN_CANDIDATES, 'Structural query maxJoinCandidates');
    const maxJoinNodes = positiveInteger(options.maxJoinNodes, DEFAULT_MAX_JOIN_NODES, 'Structural query maxJoinNodes');
    const scopePrefixes = normalizeScopePrefixes(options.files);
    const containingLines = validateContainingLines(options.containingLines);
    if (Buffer.byteLength(queryText, 'utf8') > maxQueryBytes) {
        throw new InvalidInputError(`Structural query exceeds the ${maxQueryBytes}-byte query limit.`);
    }
    if (index.contentMode !== 'full') {
        throw new InvalidInputError('Structural queries require a full-content code index.');
    }
    abortIfNeeded(options.signal);
    // Every validation above intentionally precedes this allocation. Once a
    // query handle exists, the outer finally below is its single owner.
    let queryLease;
    try {
        queryLease = await acquireTreeSitterQuery(language.grammar, queryText);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new InvalidInputError(`Tree-sitter rejected the structural query: ${message}`, { cause: error });
    }
    if (!queryLease)
        throw new Error(`The vendored ${language.name} tree-sitter grammar is unavailable.`);
    const query = queryLease.query;
    const treeCache = openTreeSitterParseCache(index);
    try {
        const files = filesForLanguage(index, language).filter(file => matchesScope(file.relativePath, scopePrefixes));
        const eligibleSkipped = index.skippedFiles.filter(file => matchesLanguagePath(file.relativePath, language) && matchesScope(file.relativePath, scopePrefixes)).length;
        const nodesByPath = indexJoinNodesByPath(index);
        const countTokens = options.countTokens ?? estimateTokenCount;
        const hits = [];
        const diagnostic = diagnosticCollector(maxDiagnostics);
        const yielder = createYielder(options.cooperative, options.signal);
        let totalCaptures = 0;
        let tokensUsed = 0;
        let filesParsed = 0;
        let runtimeFilesSkipped = 0;
        let filesQueried = 0;
        let filesVisited = 0;
        let scanBoundHit = false;
        let budgetEarlyExitHit = false;
        let matchLimitHit = false;
        let unjoinedCaptures = 0;
        let ambiguousSpanJoins = 0;
        let syntaxErrorFiles = 0;
        let parserCoveragePartialFiles = 0;
        let nodeLimitFiles = 0;
        let joinSearchTruncated = 0;
        if (index.truncated) {
            diagnostic.add(`The code index was truncated by ${index.truncated.reason}; ${index.truncated.droppedFiles} of ` +
                `${index.truncated.totalFiles} discovered file(s) were not indexed, so absence claims are incomplete.`);
        }
        if (eligibleSkipped > 0) {
            diagnostic.add(`${eligibleSkipped} matching file(s) were skipped while the code index was built.`);
        }
        for (const file of files) {
            if (scanBoundHit || budgetEarlyExitHit)
                break;
            filesVisited += 1;
            abortIfNeeded(options.signal);
            await yielder.tick();
            if (file.content === undefined) {
                runtimeFilesSkipped += 1;
                diagnostic.add(`${file.relativePath} has no retained source content and was not queried.`);
                continue;
            }
            const sourceBytes = Buffer.byteLength(file.content, 'utf8');
            if (sourceBytes > maxSourceBytes) {
                runtimeFilesSkipped += 1;
                diagnostic.add(`${file.relativePath} exceeds the ${maxSourceBytes}-byte structural source limit.`);
                continue;
            }
            const treeLease = await treeCache.acquire(language.grammar, file.content, options.signal);
            if (!treeLease) {
                runtimeFilesSkipped += 1;
                diagnostic.add(`${file.relativePath} could not be parsed with the ${language.grammar} grammar.`);
                continue;
            }
            const tree = treeLease.tree;
            try {
                const inspection = await inspectTree(tree.rootNode, maxNodes, maxDiagnostics, options.signal, yielder);
                if (inspection.nodeLimitHit) {
                    nodeLimitFiles += 1;
                    runtimeFilesSkipped += 1;
                    diagnostic.add(`${file.relativePath} exceeds the ${maxNodes}-node structural traversal limit.`);
                    continue;
                }
                filesParsed += 1;
                const provenanceRecovered = file.provenance?.syntax.status === 'recovered';
                if (tree.rootNode.hasError || inspection.syntaxIssues.length > 0 || provenanceRecovered) {
                    syntaxErrorFiles += 1;
                    if (inspection.syntaxIssues.length === 0 && provenanceRecovered) {
                        const parserDiagnostics = file.provenance?.syntax.diagnostics ?? [];
                        if (parserDiagnostics.length === 0) {
                            diagnostic.add(`${file.relativePath} was recovered by its index parser; captures may be incomplete.`);
                        }
                        for (const issue of parserDiagnostics) {
                            const location = issue.line ? `:${issue.line}${issue.column ? `:${issue.column}` : ''}` : '';
                            diagnostic.add(`${file.relativePath}${location} ${issue.message}`);
                        }
                    }
                    else if (inspection.syntaxIssues.length === 0) {
                        diagnostic.add(`${file.relativePath} contains syntax recovery outside the bounded diagnostic scan.`);
                    }
                    else {
                        for (const issue of inspection.syntaxIssues) {
                            diagnostic.add(`${file.relativePath}:${issue.line}:${issue.column} contains a ${issue.type === 'ERROR' ? 'syntax error' : `missing ${issue.type} node`}; captures may be recovered.`);
                        }
                    }
                    diagnostic.omitted += Math.max(0, inspection.syntaxIssueCount - inspection.syntaxIssues.length);
                }
                const parserCoverageIncomplete = file.provenance !== undefined &&
                    (file.provenance.capabilities.definitions !== 'full' || file.provenance.capabilities.exactSpans !== 'full');
                if (parserCoverageIncomplete) {
                    parserCoveragePartialFiles += 1;
                    diagnostic.add(`${file.relativePath} has partial index-parser definition/span coverage; capture joins may be incomplete.`);
                }
                const captureOptions = { matchLimit: QUERY_MATCH_LIMIT };
                if (containingLines) {
                    // Containment is 0-based point ordering: start at column 0 of the
                    // first allowed line, end at column 0 of the line after the last.
                    captureOptions.startContainingPosition = { row: containingLines.startLine - 1, column: 0 };
                    captureOptions.endContainingPosition = { row: containingLines.endLine, column: 0 };
                }
                if (options.signal) {
                    // The declaration types this callback as void-returning, but the
                    // query contract cancels when it returns true (see parse above).
                    captureOptions.progressCallback = () => options.signal?.aborted === true;
                }
                const captures = query.captures(tree.rootNode, captureOptions);
                abortIfNeeded(options.signal);
                filesQueried += 1;
                if (query.didExceedMatchLimit()) {
                    matchLimitHit = true;
                    diagnostic.add(`Query match limit reached in ${file.relativePath}; capture count is a lower bound.`);
                }
                const indexedNodes = nodesByPath.get(normalizePath(file.relativePath)) ?? [];
                const sourceIndex = new Utf8SourceIndex(file.content);
                for (const capture of captures) {
                    if ((totalCaptures & 255) === 0) {
                        abortIfNeeded(options.signal);
                        await yielder.tick();
                    }
                    if (containingLines && capture.node.endPosition.row + 1 > containingLines.endLine) {
                        // The cursor's end point is column 0 of the line after the range,
                        // so a capture whose span ends with that line's newline slips
                        // through containment; the echoed scope promises captures end on
                        // `endLine` at the latest, so drop the boundary artifact here.
                        continue;
                    }
                    totalCaptures += 1;
                    if (totalCaptures > MAX_CAPTURE_SCAN) {
                        scanBoundHit = true;
                        diagnostic.add(`Capture scan stopped at ${MAX_CAPTURE_SCAN}; totalCaptures and omitted are lower bounds.`);
                        break;
                    }
                    // web-tree-sitter parses JavaScript strings as UTF-16 input, so Node
                    // indexes are UTF-16 offsets. Canonical index spans are UTF-8 bytes;
                    // convert exactly once at this boundary before comparing them.
                    const captureSpan = {
                        startByte: sourceIndex.byteOffset(capture.node.startIndex),
                        endByte: sourceIndex.byteOffset(capture.node.endIndex),
                        startLine: capture.node.startPosition.row + 1,
                        startColumn: capture.node.startPosition.column + 1,
                        endLine: capture.node.endPosition.row + 1,
                        endColumn: capture.node.endPosition.column + 1,
                    };
                    const joined = joinSpanToNode(indexedNodes, captureSpan, maxJoinNodes, maxJoinCandidates);
                    if (joined.searchTruncated)
                        joinSearchTruncated += 1;
                    if (!joined.node && !joined.ambiguous)
                        unjoinedCaptures += 1;
                    if (joined.ambiguous)
                        ambiguousSpanJoins += 1;
                    if (hits.length >= limit)
                        continue;
                    const hit = {
                        capture: capture.name,
                        nodeType: capture.node.type,
                        text: compactSourceText(file.content, capture.node.startIndex, capture.node.endIndex),
                        relativePath: normalizePath(file.relativePath),
                        startLine: capture.node.startPosition.row + 1,
                        startColumn: capture.node.startPosition.column + 1,
                        endLine: capture.node.endPosition.row + 1,
                        endColumn: capture.node.endPosition.column + 1,
                        nodeId: joined.node?.id,
                        symbol: joined.node?.name,
                        joinStatus: joined.node ? 'resolved' : joined.ambiguous ? 'ambiguous' : 'unresolved',
                        joinBasis: joined.basis,
                        nodeCandidates: joined.ambiguous ? joined.candidates : undefined,
                    };
                    const tokenCost = countTokens(JSON.stringify(hit));
                    if (!Number.isFinite(tokenCost) || tokenCost < 0) {
                        throw new Error('Structural query token counter returned an invalid value.');
                    }
                    if (tokensUsed + tokenCost > mapTokens) {
                        if (options.budgetEarlyExit) {
                            budgetEarlyExitHit = true;
                            diagnostic.add('Budget early exit stopped at the first capture that did not fit; later smaller captures were not considered, so totalCaptures and omitted are lower bounds.');
                            break;
                        }
                        continue;
                    }
                    hits.push(hit);
                    tokensUsed += tokenCost;
                }
            }
            finally {
                treeLease.release();
            }
        }
        const unvisitedFiles = scanBoundHit || budgetEarlyExitHit ? Math.max(0, files.length - filesVisited) : 0;
        const filesSkipped = eligibleSkipped + runtimeFilesSkipped + unvisitedFiles;
        const omitted = Math.max(0, totalCaptures - hits.length);
        const incomplete = scanBoundHit ||
            budgetEarlyExitHit ||
            matchLimitHit ||
            filesSkipped > 0 ||
            syntaxErrorFiles > 0 ||
            parserCoveragePartialFiles > 0 ||
            nodeLimitFiles > 0 ||
            joinSearchTruncated > 0 ||
            diagnostic.omitted > 0 ||
            index.truncated !== undefined;
        const truncated = omitted > 0 || scanBoundHit || budgetEarlyExitHit || matchLimitHit || nodeLimitFiles > 0 || unvisitedFiles > 0;
        return {
            status: 'resolved',
            language: language.name,
            grammar: language.grammar,
            query: queryText,
            ...(scopePrefixes || containingLines
                ? {
                    scope: {
                        ...(scopePrefixes ? { files: scopePrefixes } : {}),
                        ...(containingLines ? { containingLines } : {}),
                    },
                }
                : {}),
            hits,
            totalCaptures,
            omitted,
            truncated,
            filesParsed,
            filesSkipped,
            mapTokens,
            tokensUsed,
            tokenizer: options.tokenizer ?? 'heuristic',
            diagnostics: diagnostic.values,
            honesty: {
                freshness: 'unverified',
                stalenessReason: 'CodebaseIndex does not carry a filesystem freshness marker.',
                coverage: incomplete ? 'partial' : 'full',
                truncated,
                omitted,
                filesSkipped,
                unjoinedCaptures,
                ambiguousSpanJoins,
                eligibleFiles: files.length + eligibleSkipped,
                filesQueried,
                syntaxErrorFiles,
                parserCoveragePartialFiles,
                nodeLimitFiles,
                joinSearchTruncated,
                diagnosticsOmitted: diagnostic.omitted,
                matchLimitHit,
                captureLimitHit: scanBoundHit,
                ...(budgetEarlyExitHit ? { budgetEarlyExitHit: true } : {}),
            },
        };
    }
    finally {
        treeCache.close();
        queryLease.release();
    }
}
async function renderParseTree(root, options) {
    const stack = [{ kind: 'node', node: root, leadingSpace: false }];
    const chunks = [];
    const syntaxIssues = [];
    let characters = 0;
    let nodesVisited = 0;
    let nodeLimitHit = false;
    let renderLimitHit = false;
    let syntaxIssueCount = 0;
    const append = (value) => {
        if (characters + value.length > options.maxCharacters)
            return false;
        chunks.push(value);
        characters += value.length;
        return true;
    };
    while (stack.length > 0) {
        const event = stack.pop();
        if (!event)
            break;
        if (event.kind === 'close') {
            if (!append(')')) {
                renderLimitHit = true;
                break;
            }
            continue;
        }
        if ((nodesVisited & 255) === 0) {
            abortIfNeeded(options.signal);
            await options.yielder.tick();
        }
        if (nodesVisited >= options.maxNodes) {
            nodeLimitHit = true;
            break;
        }
        const node = event.node;
        nodesVisited += 1;
        if (node.type === 'ERROR' || node.isMissing) {
            syntaxIssueCount += 1;
            if (syntaxIssues.length < options.maxDiagnostics) {
                syntaxIssues.push({
                    type: node.type,
                    line: node.startPosition.row + 1,
                    column: node.startPosition.column + 1,
                });
            }
        }
        const prefix = `${event.leadingSpace ? ' ' : ''}${event.fieldName ? `${event.fieldName}: ` : ''}(${node.type}`;
        if (!append(prefix)) {
            renderLimitHit = true;
            break;
        }
        if (node.namedChildCount === 0) {
            if (!append(')')) {
                renderLimitHit = true;
                break;
            }
            continue;
        }
        stack.push({ kind: 'close' });
        for (let index = node.namedChildCount - 1; index >= 0; index--) {
            const child = node.namedChild(index);
            if (!child)
                continue;
            stack.push({
                kind: 'node',
                node: child,
                fieldName: node.fieldNameForNamedChild(index) ?? undefined,
                leadingSpace: true,
            });
        }
    }
    abortIfNeeded(options.signal);
    return {
        text: chunks.join(''),
        nodesVisited,
        nodeLimitHit,
        renderLimitHit,
        syntaxIssues,
        syntaxIssueCount,
    };
}
/**
 * Render a bounded tree-sitter concrete syntax tree (parse tree).
 *
 * The historical `Ast` name is retained as a compatibility alias for the CLI
 * and facade even though tree-sitter exposes a CST rather than a semantic AST.
 */
export async function parseTreeSitterAst(options) {
    const language = resolveStructuralLanguage(options.language);
    if (!language) {
        throw new InvalidInputError(`Unsupported parse-tree language "${options.language}". Expected one of: ${STRUCTURAL_LANGUAGES.map(item => item.name).join(', ')}`);
    }
    const mapTokens = positiveInteger(options.mapTokens, DEFAULT_MAP_TOKENS, 'Parse-tree mapTokens');
    const maxSourceBytes = positiveInteger(options.maxSourceBytes, DEFAULT_MAX_SOURCE_BYTES, 'Parse-tree maxSourceBytes', MAX_CONFIGURABLE_SOURCE_BYTES);
    const maxNodes = positiveInteger(options.maxNodes, DEFAULT_MAX_TREE_NODES, 'Parse-tree maxNodes', MAX_CONFIGURABLE_TREE_NODES);
    const maxDiagnostics = positiveInteger(options.maxDiagnostics, DEFAULT_MAX_DIAGNOSTICS, 'Parse-tree maxDiagnostics');
    const maxRenderCharacters = positiveInteger(options.maxRenderCharacters, DEFAULT_MAX_RENDER_CHARACTERS, 'Parse-tree maxRenderCharacters', MAX_CONFIGURABLE_RENDER_CHARACTERS);
    const sourceBytes = Buffer.byteLength(options.source, 'utf8');
    if (sourceBytes > maxSourceBytes) {
        throw new InvalidInputError(`Parse-tree source exceeds the ${maxSourceBytes}-byte source limit.`);
    }
    abortIfNeeded(options.signal);
    const countTokens = options.countTokens ?? estimateTokenCount;
    const sliceTokens = options.sliceTokens ?? sliceByTokens;
    const yielder = createYielder(options.cooperative, options.signal);
    // Validate all resource settings before allocating the WASM tree.
    const tree = await parseTreeSitterContent(language.grammar, options.source, options.signal);
    if (!tree)
        throw new Error(`The vendored ${language.name} tree-sitter grammar is unavailable.`);
    try {
        const rendered = await renderParseTree(tree.rootNode, {
            maxNodes,
            maxCharacters: maxRenderCharacters,
            maxDiagnostics,
            signal: options.signal,
            yielder,
        });
        const diagnostics = rendered.syntaxIssues.map(issue => issue.type === 'ERROR'
            ? `Syntax error at ${issue.line}:${issue.column}; the parse tree contains recovery.`
            : `Missing ${issue.type} node at ${issue.line}:${issue.column}; the parse tree contains recovery.`);
        let diagnosticsOmitted = Math.max(0, rendered.syntaxIssueCount - rendered.syntaxIssues.length);
        const addDiagnostic = (message) => {
            if (diagnostics.length < maxDiagnostics)
                diagnostics.push(message);
            else
                diagnosticsOmitted += 1;
        };
        if (tree.rootNode.hasError && diagnostics.length === 0) {
            addDiagnostic('The parse tree contains syntax recovery outside the bounded diagnostic traversal.');
        }
        if (rendered.nodeLimitHit) {
            addDiagnostic(`Parse-tree traversal stopped at the ${maxNodes}-node limit.`);
        }
        if (rendered.renderLimitHit) {
            addDiagnostic(`Parse-tree rendering stopped at the ${maxRenderCharacters}-character limit.`);
        }
        const fullTokens = countTokens(rendered.text);
        if (!Number.isFinite(fullTokens) || fullTokens < 0) {
            throw new Error('Parse-tree token counter returned an invalid value.');
        }
        const tokenLimitHit = fullTokens > mapTokens;
        const bounded = tokenLimitHit ? sliceTokens(rendered.text, 0, mapTokens) : rendered.text;
        const tokensUsed = countTokens(bounded);
        if (!Number.isFinite(tokensUsed) || tokensUsed < 0) {
            throw new Error('Parse-tree token counter returned an invalid value.');
        }
        const truncated = tokenLimitHit || rendered.nodeLimitHit || rendered.renderLimitHit;
        const syntaxErrors = Math.max(rendered.syntaxIssueCount, tree.rootNode.hasError ? 1 : 0);
        return {
            status: 'resolved',
            language: language.name,
            grammar: language.grammar,
            source: {
                kind: options.sourceKind ?? 'snippet',
                relativePath: options.relativePath ? normalizePath(options.relativePath) : undefined,
            },
            tree: bounded,
            truncated,
            mapTokens,
            tokensUsed,
            tokenizer: options.tokenizer ?? 'heuristic',
            diagnostics,
            honesty: {
                freshness: 'source-input',
                coverage: syntaxErrors > 0 || truncated ? 'partial' : 'full',
                truncated,
                ambiguityCount: 0,
                nodesVisited: rendered.nodesVisited,
                nodeLimitHit: rendered.nodeLimitHit,
                renderLimitHit: rendered.renderLimitHit,
                tokenLimitHit,
                syntaxErrors,
                diagnosticsOmitted,
            },
        };
    }
    finally {
        tree.delete();
    }
}
//# sourceMappingURL=structural.js.map