import { createHash } from 'node:crypto';
import path from 'node:path';
import { compareCodeUnits } from '../../utils.js';
import { collectTreeSitterTagCaptures, parseTreeSitterContent } from '../tree-sitter.js';
import { computeExactBodyHash } from './hash.js';
import { ENTITY_CST_PROOF, ENTITY_SIGNATURE_PROOF, WHOLE_DIFF_GRAMMAR_MANIFEST_DIGEST, grammarManifestEntry, grammarManifestEntryForPath, verifyGrammarManifestEntry, } from './grammar-manifest.js';
const NAME_NODE_TYPES = new Set([
    'identifier',
    'field_identifier',
    'type_identifier',
    'constant',
    'property_identifier',
    'simple_identifier',
    'operator_name',
]);
const DEFINITION_CAPTURE_PREFIX = 'definition.';
const ROOT_NODE_TYPES = new Set(['program', 'module', 'source_file', 'translation_unit']);
function children(node) {
    const result = [];
    for (let index = 0; index < node.childCount; index++) {
        const child = node.child(index);
        if (child)
            result.push({ node: child, fieldName: node.fieldNameForChild(index) });
    }
    return result;
}
function findFirstName(node) {
    const stack = [node];
    while (stack.length > 0) {
        const candidate = stack.pop();
        if (!candidate)
            continue;
        if (NAME_NODE_TYPES.has(candidate.type))
            return candidate;
        const nested = children(candidate);
        for (let index = nested.length - 1; index >= 0; index--)
            stack.push(nested[index].node);
    }
    return null;
}
function declarationNameNode(node) {
    const direct = node.childForFieldName('name');
    if (direct)
        return direct;
    const declarator = node.childForFieldName('declarator');
    if (declarator) {
        const found = findFirstName(declarator);
        if (found)
            return found;
    }
    const type = node.childForFieldName('type');
    if (type) {
        const found = findFirstName(type);
        if (found)
            return found;
    }
    if (node.type === 'export_statement' || node.type === 'decorated_definition')
        return findFirstName(node);
    return null;
}
function nodeKey(node) {
    return `${node.type}:${node.startIndex}:${node.endIndex}`;
}
function isCandidate(node, symbol, tagged) {
    if (ROOT_NODE_TYPES.has(node.type) || NAME_NODE_TYPES.has(node.type) || node.childCount === 0)
        return false;
    if (node.startPosition.row + 1 !== symbol.line)
        return false;
    if (node.endPosition.row + 1 !== (symbol.endLine ?? symbol.line))
        return false;
    const name = declarationNameNode(node);
    if (name?.text !== symbol.name)
        return false;
    return tagged.has(nodeKey(node)) || name !== null;
}
function selectCandidate(nodes, symbol, tagged, used) {
    const candidates = nodes.filter(node => !used.has(nodeKey(node)) && isCandidate(node, symbol, tagged));
    const exportRank = (node) => (symbol.exported && node.type === 'export_statement' ? 0 : 1);
    candidates.sort((left, right) => {
        const exportOrder = exportRank(left) - exportRank(right);
        if (exportOrder !== 0)
            return exportOrder;
        const leftSize = left.endIndex - left.startIndex;
        const rightSize = right.endIndex - right.startIndex;
        return leftSize - rightSize || left.startIndex - right.startIndex || compareCodeUnits(left.type, right.type);
    });
    const selected = candidates[0];
    const runnerUp = candidates[1];
    if (!selected || !runnerUp)
        return selected;
    const sameRank = exportRank(selected) === exportRank(runnerUp) &&
        selected.startIndex === runnerUp.startIndex &&
        selected.endIndex === runnerUp.endIndex;
    return sameRank ? undefined : selected;
}
function emit(hash, marker, values) {
    const fields = [marker, ...values];
    for (const value of fields) {
        const bytes = Buffer.from(value, 'utf8');
        hash.update(Buffer.from(`${bytes.byteLength}:`, 'ascii'));
        hash.update(bytes);
    }
}
function digestNode(root, options) {
    const hash = createHash('sha256');
    emit(hash, 'proof', [options.proof, options.grammar, options.grammarDigest, WHOLE_DIFF_GRAMMAR_MANIFEST_DIGEST]);
    const stack = [{ kind: 'node', node: root, fieldName: null, close: false }];
    while (stack.length > 0) {
        const frame = stack.pop();
        if (!frame)
            continue;
        if (frame.kind === 'omitted') {
            emit(hash, 'omitted', [frame.fieldName ?? '<no-field>', frame.nodeType]);
            continue;
        }
        const node = frame.node;
        if (node.isMissing || node.isError || node.hasError)
            return undefined;
        if (options.elideComments && options.comments.has(node.type))
            continue;
        if (frame.close) {
            emit(hash, 'close', [node.type]);
            continue;
        }
        const isDeclarationName = options.signature &&
            options.declarationName !== undefined &&
            options.declarationName !== null &&
            node.startIndex === options.declarationName.startIndex &&
            node.endIndex === options.declarationName.endIndex;
        emit(hash, 'open', [frame.fieldName ?? '<no-field>', node.type]);
        if (isDeclarationName) {
            emit(hash, 'name', ['<declaration-name>']);
            emit(hash, 'close', [node.type]);
            continue;
        }
        const nested = children(node);
        if (nested.length === 0) {
            emit(hash, 'leaf', [node.text]);
            emit(hash, 'close', [node.type]);
            continue;
        }
        stack.push({ ...frame, close: true });
        for (let index = nested.length - 1; index >= 0; index--) {
            const child = nested[index];
            const omitBody = options.signature && child.fieldName === 'body';
            const omitValue = options.signature && options.omitValue === true && child.fieldName === 'value';
            if (omitBody || omitValue) {
                stack.push({ kind: 'omitted', fieldName: child.fieldName, nodeType: child.node.type });
                continue;
            }
            stack.push({ kind: 'node', node: child.node, fieldName: child.fieldName, close: false });
        }
    }
    return hash.digest('hex');
}
function collectNodes(root) {
    const result = [];
    const stack = [root];
    while (stack.length > 0) {
        const node = stack.pop();
        if (!node)
            continue;
        result.push(node);
        const nested = children(node);
        for (let index = nested.length - 1; index >= 0; index--)
            stack.push(nested[index].node);
    }
    return result;
}
function splitLinesWithStarts(value) {
    const lines = [];
    const starts = [];
    let start = 0;
    for (let index = 0; index < value.length; index++) {
        const code = value.charCodeAt(index);
        if (code !== 0x0a && code !== 0x0d)
            continue;
        starts.push(start);
        lines.push(value.slice(start, index));
        if (code === 0x0d && value.charCodeAt(index + 1) === 0x0a)
            index++;
        start = index + 1;
    }
    starts.push(start);
    lines.push(value.slice(start));
    return { lines, starts };
}
async function prepareVueEvidenceSource(content) {
    let compiler;
    try {
        compiler = await import('@vue/compiler-sfc');
    }
    catch {
        return undefined;
    }
    const parsed = compiler.parse(content);
    if (parsed.errors.length > 0)
        return undefined;
    const blocks = [parsed.descriptor.script, parsed.descriptor.scriptSetup].filter((block) => block !== null && block !== undefined && block.content.length > 0);
    if (blocks.length === 0)
        return undefined;
    const grammar = blocks.some(block => block.lang === 'tsx')
        ? 'tsx'
        : blocks.some(block => block.lang === 'ts')
            ? 'typescript'
            : 'javascript';
    const manifest = grammarManifestEntry(grammar);
    if (!manifest)
        return undefined;
    const virtualLines = content.split(/\r?\n/u).map(() => '');
    const locations = [];
    for (const block of blocks) {
        const split = splitLinesWithStarts(block.content);
        const startRow = Math.max(0, block.loc.start.line - 1);
        for (const [offset, line] of split.lines.entries())
            virtualLines[startRow + offset] = line;
        locations.push({
            startRow,
            originalOffset: block.loc.start.offset,
            content: block.content,
            lineStarts: split.starts,
            lines: split.lines,
        });
    }
    const originalIndex = (row, column) => {
        for (const location of locations) {
            const lineIndex = row - location.startRow;
            if (lineIndex < 0 || lineIndex >= location.lines.length)
                continue;
            const line = location.lines[lineIndex];
            const lineStart = location.lineStarts[lineIndex];
            if (line === undefined || lineStart === undefined || column > line.length)
                continue;
            return location.originalOffset + lineStart + column;
        }
        return undefined;
    };
    return {
        source: virtualLines.join('\n'),
        manifest,
        originalRange: node => {
            const startUtf16 = originalIndex(node.startPosition.row, node.startPosition.column);
            const endUtf16 = originalIndex(node.endPosition.row, node.endPosition.column);
            return startUtf16 === undefined || endUtf16 === undefined ? undefined : { startUtf16, endUtf16 };
        },
    };
}
async function prepareEvidenceSource(content, relativePath) {
    if (path.posix.extname(relativePath.replaceAll('\\', '/')).toLowerCase() === '.vue') {
        return prepareVueEvidenceSource(content);
    }
    const manifest = grammarManifestEntryForPath(relativePath);
    if (!manifest)
        return undefined;
    return {
        source: content,
        manifest,
        originalRange: node => ({ startUtf16: node.startIndex, endUtf16: node.endIndex }),
    };
}
/**
 * web-tree-sitter exposes indexes/columns in JavaScript UTF-16 code units when
 * parsing a string. Persisted spans are explicitly UTF-8 byte coordinates, so
 * convert once per file instead of silently relabeling those indexes.
 */
function utf8Coordinates(source) {
    const bytesAtUtf16Index = new Uint32Array(source.length + 1);
    const lineStartUtf16Indexes = [0];
    let utf16Index = 0;
    let byteOffset = 0;
    for (const character of source) {
        const width = character.length;
        for (let offset = 1; offset < width; offset++) {
            bytesAtUtf16Index[utf16Index + offset] = byteOffset;
        }
        utf16Index += width;
        byteOffset += Buffer.byteLength(character, 'utf8');
        bytesAtUtf16Index[utf16Index] = byteOffset;
        if (character === '\n')
            lineStartUtf16Indexes.push(utf16Index);
    }
    return { bytesAtUtf16Index, lineStartUtf16Indexes };
}
/**
 * Join parser declarations to manifest-pinned CST nodes without changing the
 * shared parser contract. Missing or ambiguous joins simply provide no V2
 * evidence; callers then classify changes as unknown.
 */
export async function extractEntityEvidenceV2(file, relativePath, options = {}) {
    const result = new Map();
    const content = file.content;
    if (!content)
        return result;
    const prepared = await prepareEvidenceSource(content, relativePath);
    if (!prepared || !(await verifyGrammarManifestEntry(prepared.manifest)))
        return result;
    const { manifest } = prepared;
    const tree = await parseTreeSitterContent(manifest.grammar, prepared.source);
    if (!tree)
        return result;
    try {
        if (tree.rootNode.hasError)
            return result;
        const captures = await collectTreeSitterTagCaptures(manifest.grammar, tree.rootNode);
        const tagged = new Set((captures ?? [])
            .filter(capture => capture.name.startsWith(DEFINITION_CAPTURE_PREFIX))
            .map(capture => nodeKey(capture.node)));
        const nodes = collectNodes(tree.rootNode);
        const coordinates = utf8Coordinates(content);
        const contentBytes = Buffer.from(content, 'utf8');
        const used = new Set();
        const comments = new Set(manifest.commentNodeTypes);
        const ordered = [...file.symbols].sort((left, right) => left.line - right.line ||
            (left.endLine ?? left.line) - (right.endLine ?? right.line) ||
            compareCodeUnits(left.name, right.name));
        for (const symbol of ordered) {
            const node = selectCandidate(nodes, symbol, tagged, used);
            if (!node)
                continue;
            const originalRange = prepared.originalRange(node);
            if (!originalRange)
                continue;
            const startByte = coordinates.bytesAtUtf16Index[originalRange.startUtf16];
            const endByte = coordinates.bytesAtUtf16Index[originalRange.endUtf16];
            const startLineIndex = coordinates.lineStartUtf16Indexes[node.startPosition.row];
            const endLineIndex = coordinates.lineStartUtf16Indexes[node.endPosition.row];
            if (startLineIndex === undefined || endLineIndex === undefined || endByte < startByte)
                continue;
            const startLineByte = coordinates.bytesAtUtf16Index[startLineIndex];
            const endLineByte = coordinates.bytesAtUtf16Index[endLineIndex];
            const exactSource = contentBytes.subarray(startByte, endByte).toString('utf8');
            if (exactSource !== node.text)
                continue;
            const declarationName = declarationNameNode(node);
            const common = {
                grammar: manifest.grammar,
                grammarDigest: manifest.wasmSha256,
                comments,
            };
            const primaryHash = digestNode(node, {
                ...common,
                proof: ENTITY_CST_PROOF,
                elideComments: false,
                signature: false,
            });
            const commentsElidedHash = digestNode(node, {
                ...common,
                proof: ENTITY_CST_PROOF,
                elideComments: true,
                signature: false,
            });
            const signatureHash = digestNode(node, {
                ...common,
                proof: ENTITY_SIGNATURE_PROOF,
                elideComments: true,
                signature: true,
                declarationName,
                omitValue: symbol.type === 'variable',
            });
            if (!primaryHash || !commentsElidedHash || !signatureHash)
                continue;
            used.add(nodeKey(node));
            result.set(symbol, {
                sourceSpan: {
                    version: 2,
                    encoding: 'utf8',
                    startLine: node.startPosition.row + 1,
                    startColumn: startByte - startLineByte + 1 + (node.startPosition.row === 0 ? (options.leadingUtf8Bytes ?? 0) : 0),
                    endLine: node.endPosition.row + 1,
                    endColumn: endByte - endLineByte + 1 + (node.endPosition.row === 0 ? (options.leadingUtf8Bytes ?? 0) : 0),
                    startByte: startByte + (options.leadingUtf8Bytes ?? 0),
                    endByte: endByte + (options.leadingUtf8Bytes ?? 0),
                    exact: true,
                },
                exactSource,
                exactBodyHash: computeExactBodyHash(exactSource),
                syntaxEvidence: {
                    version: 2,
                    proof: ENTITY_CST_PROOF,
                    grammar: manifest.grammar,
                    grammarDigest: manifest.wasmSha256,
                    manifestDigest: WHOLE_DIFF_GRAMMAR_MANIFEST_DIGEST,
                    primaryHash,
                    commentsElidedHash,
                    complete: true,
                },
                signatureEvidence: {
                    version: 2,
                    proof: ENTITY_SIGNATURE_PROOF,
                    grammar: manifest.grammar,
                    grammarDigest: manifest.wasmSha256,
                    manifestDigest: WHOLE_DIFF_GRAMMAR_MANIFEST_DIGEST,
                    hash: signatureHash,
                    complete: true,
                },
            });
        }
        return result;
    }
    finally {
        tree.delete();
    }
}
export function exactSourceForRecord(content, record) {
    const span = record.sourceSpan;
    if (!span)
        return undefined;
    const bytes = Buffer.from(content, 'utf8');
    if (span.endByte > bytes.byteLength)
        return undefined;
    return bytes.subarray(span.startByte, span.endByte).toString('utf8');
}
export function grammarForEntityPath(relativePath) {
    return grammarManifestEntryForPath(path.posix.normalize(relativePath))?.grammar;
}
//# sourceMappingURL=syntax-evidence.js.map