import { createHash } from 'node:crypto';
import { candidateFromNode, readCodeSource, selectByQuery } from './source.js';
import { computeEntityHashes } from './entity/hash.js';
import { SymbolKeyQuerySchema, } from './types.js';
/**
 * Batch body-hash primitive behind the `code.symbol.bodyHash` feature key.
 *
 * Resolution reuses the `readCodeSource` candidate matcher (`selectByQuery`),
 * narrowed by `containerName` when the query provides one. Each query is
 * independent: a bad query yields one `error`/`not_found` entry in the result
 * array (positionally aligned with `queries`), never a thrown batch.
 *
 * Frozen hash contract: `bodyHash` is the full 64-char lowercase hex sha256
 * over the UTF-8 bytes of the legacy `source` string `readCodeSource` returns
 * for the resolved node (the `lines.slice(line - 1, endLine)` join). No
 * truncation. Algorithm and normalization are part of the public contract;
 * changing either is a breaking change requiring a new feature key.
 */
export async function hashCodeSymbols(index, queries, _options = {}) {
    const results = [];
    for (const query of queries) {
        results.push(await hashOneSymbol(index, query, _options));
    }
    return results;
}
export function computeBodyHash(source) {
    return createHash('sha256').update(source, 'utf8').digest('hex');
}
async function hashOneSymbol(index, rawQuery, options) {
    const parsed = SymbolKeyQuerySchema.safeParse(rawQuery);
    if (!parsed.success) {
        return { status: 'error', message: `Invalid symbol key query: ${parsed.error.message}` };
    }
    const query = parsed.data;
    try {
        const selected = selectByQuery(index, { name: query.name, file: query.relativePath, type: query.kind });
        if (typeof selected === 'string') {
            return { status: 'error', query, message: selected };
        }
        const candidates = query.containerName != null ? selected.filter(node => node.containerName === query.containerName) : selected;
        if (candidates.length === 0) {
            const result = {
                status: 'not_found',
                query,
                candidates: [],
                message: 'No matching source node was found.',
            };
            if (query.expected) {
                result.verdict = 'gone';
                if (options.resolveGone) {
                    result.lineage = await options.resolveGone(query, query.expected.bodyHash);
                    if (result.lineage.status === 'unavailable') {
                        result.message = 'No matching source node was found; gone (lineage unavailable).';
                    }
                }
            }
            return result;
        }
        if (candidates.length > 1) {
            // Overloads/redeclarations where all key fields match stay explicit —
            // never a silent first pick.
            return {
                status: 'ambiguous',
                query,
                candidates: candidates.map(candidateFromNode),
                message: `Found ${candidates.length} matching source nodes.`,
            };
        }
        const node = candidates[0];
        const source = await readCodeSource(index, { nodeId: node.id });
        if (source.status !== 'resolved') {
            const message = 'message' in source ? source.message : `Could not read source for node "${node.id}".`;
            return { status: 'error', query, message };
        }
        const symbol = hashedSymbolFromSource(query, node, source.source, source.exactSource, source);
        const result = { status: 'resolved', query, symbol };
        if (query.expected) {
            result.verdict = compareSymbolHashTuples(symbol.hashes, query.expected, {
                normalizedEqualVerdict: options.normalizedEqualVerdict ?? true,
            });
        }
        return result;
    }
    catch (error) {
        return { status: 'error', query, message: error.message };
    }
}
function hashedSymbolFromSource(query, node, source, exactSource, span) {
    const legacySpanExact = span.endLine > span.line;
    const exactSpanHash = exactSource === undefined ? undefined : computeBodyHash(exactSource);
    const exactBytesAvailable = exactSource !== undefined;
    // `spanExact` predates byte coordinates and historically meant that a
    // multi-line body tier was available. Preserve that public boolean while
    // allowing proved one-line spans to opt in. Consumers that need byte-exact
    // evidence use `exactSpanHash`/`span.exact`, not this compatibility flag.
    const spanExact = legacySpanExact || exactBytesAvailable;
    const hashes = verificationHashes(legacySpanExact ? source : (exactSource ?? source), span.relativePath ?? node.relativePath ?? query.relativePath, spanExact, computeBodyHash(source), exactSpanHash);
    return {
        key: {
            relativePath: span.relativePath ?? node.relativePath ?? query.relativePath,
            kind: node.type,
            name: node.name,
            containerName: node.containerName,
        },
        nodeId: node.id,
        line: span.line,
        endLine: span.endLine,
        span: node.span,
        signature: node.signature,
        bodyHash: hashes.bodyHash,
        exactSpanHash,
        hashVersion: hashes.hashVersion,
        hashAlgo: hashes.algo,
        normalizedHash: hashes.normalizedHash,
        structuralHash: hashes.structuralHash,
        hashes,
        // Compatibility includes legacy multi-line body spans; exactSpanHash is
        // the narrower signal for parser-proved byte-exact declarations.
        spanExact,
    };
}
function verificationHashes(source, relativePath, spanExact, bodyHash, exactSpanHash) {
    const hashes = computeEntityHashes({
        source,
        relativePath,
        spanExact,
    });
    if (!spanExact) {
        // A declaration-only regex fallback has no body tier. Its frozen
        // declaration-line bodyHash remains useful, but normalized/structural
        // verdicts would pretend that a declaration is the whole symbol.
        return {
            hashVersion: hashes.hashVersion,
            algo: 'none',
            bodyHash,
        };
    }
    return {
        hashVersion: hashes.hashVersion,
        algo: hashes.algo,
        bodyHash,
        normalizedHash: hashes.normalizedHash,
        structuralHash: hashes.structuralHash,
        exactSpanHash,
    };
}
/**
 * Mechanical comparison of one stored tuple with the current tiers.
 *
 * The exact-span shortcut runs first because raw sha256 byte equality is
 * version-independent proof. Every other tier is produced by the normalizing
 * lexer, so a `hashVersion` or `algo` mismatch refuses (`unknown-tier`) rather
 * than misreporting a version bump or an engine change as drift. A partial
 * tuple can still prove body equality (`fresh`) but never classify drift —
 * cosmetic-vs-semantic needs the normalized tier on both sides.
 */
export function compareSymbolHashTuples(current, expected, options = {}) {
    const normalizedEqualVerdict = options.normalizedEqualVerdict ?? true;
    const exactComparable = current.exactSpanHash !== undefined && expected.exactSpanHash !== undefined;
    if (exactComparable && current.exactSpanHash === expected.exactSpanHash)
        return 'fresh';
    if (expected.hashVersion !== undefined && expected.hashVersion !== current.hashVersion)
        return 'unknown-tier';
    if (expected.algo !== undefined && expected.algo !== current.algo)
        return 'unknown-tier';
    if (current.algo !== 'lexer' || !current.normalizedHash || !current.structuralHash)
        return 'unknown-tier';
    if (expected.normalizedHash === undefined || expected.structuralHash === undefined) {
        // Partial tuple: body-tier byte equality is provable on its own (same
        // version, same algo, same bytes ⇒ same derived tiers), inequality is not
        // classifiable without the stored normalized tier.
        if (!exactComparable && current.bodyHash === expected.bodyHash)
            return 'fresh';
        return 'unknown-tier';
    }
    if (!exactComparable &&
        current.bodyHash === expected.bodyHash &&
        current.normalizedHash === expected.normalizedHash &&
        current.structuralHash === expected.structuralHash) {
        return 'fresh';
    }
    if ((exactComparable ? current.exactSpanHash !== expected.exactSpanHash : current.bodyHash !== expected.bodyHash) &&
        current.normalizedHash === expected.normalizedHash) {
        return normalizedEqualVerdict ? 'normalized-equal' : 'cosmetic-drift';
    }
    // This label records only a changed normalized token stream. It does not
    // claim that observable behavior changed.
    if (current.normalizedHash !== expected.normalizedHash)
        return 'semantic-drift';
    // A body/normalized pair cannot remain equal while only the structural tier
    // changes under one hash version. Treat a malformed or incomparable tuple as
    // unavailable evidence, never as semantic drift.
    return 'unknown-tier';
}
//# sourceMappingURL=hash.js.map