import { ENTITY_HASH_VERSION } from './hash.js';
import { compareCodeUnits } from '../../utils.js';
import { resolveCommit } from './git.js';
import { mapEntityGitError } from './git-error.js';
function push(map, key, value) {
    const bucket = map.get(key);
    if (bucket)
        bucket.push(value);
    else
        map.set(key, [value]);
}
/**
 * One side of an event, back into an entity record.
 *
 * The hashes are marked `algo: 'none'` deliberately. A recalled record was not
 * hashed from source, it was remembered, and `hashesComparable` refuses `'none'`
 * — so a reconstructed record can never be mistaken for evidence by the matcher
 * even if one is fed to it by accident.
 */
function recordFrom(event, side) {
    const relativePath = side === 'previous' ? event.previousPath : event.currentPath;
    const name = side === 'previous' ? event.previousName : event.currentName;
    const qualifiedName = side === 'previous' ? event.previousQualifiedName : event.currentQualifiedName;
    const kind = side === 'previous' ? event.previousKind : event.currentKind;
    const startLine = side === 'previous' ? event.previousStartLine : event.currentStartLine;
    const endLine = side === 'previous' ? event.previousEndLine : event.currentEndLine;
    if (!relativePath || !name || !qualifiedName || !kind || !startLine || !endLine)
        return undefined;
    const containerName = side === 'previous' ? event.previousContainerName : event.currentContainerName;
    const signature = side === 'previous' ? event.previousSignature : event.currentSignature;
    const bodyHash = (side === 'previous' ? event.previousBodyHash : event.currentBodyHash) ?? '';
    const exactBodyHash = side === 'previous' ? event.previousExactBodyHash : event.currentExactBodyHash;
    const sourceSpan = side === 'previous' ? event.previousSourceSpan : event.currentSourceSpan;
    const syntaxEvidence = side === 'previous' ? event.previousSyntaxEvidence : event.currentSyntaxEvidence;
    const signatureEvidence = side === 'previous' ? event.previousSignatureEvidence : event.currentSignatureEvidence;
    const ordinal = (side === 'previous' ? event.previousOrdinal : event.currentOrdinal) ?? 0;
    const isExported = (side === 'previous' ? event.previousIsExported : event.currentIsExported) ?? false;
    const record = {
        name,
        qualifiedName,
        kind,
        relativePath,
        startLine,
        endLine,
        isExported,
        ordinal,
        // Rewritten below, once every entity in the file is known: declaration order
        // is a fact about a file, and no single event can report it.
        declarationIndex: 0,
        // This is the frozen line-span tier. V2 byte exactness lives separately in
        // `sourceSpan` and must not rewrite persisted legacy hash semantics.
        spanExact: endLine > startLine,
        hashes: { hashVersion: ENTITY_HASH_VERSION, algo: 'none', bodyHash },
    };
    if (exactBodyHash)
        record.hashes.exactBodyHash = exactBodyHash;
    if (sourceSpan)
        record.sourceSpan = sourceSpan;
    if (syntaxEvidence)
        record.syntaxEvidence = syntaxEvidence;
    if (signatureEvidence)
        record.signatureEvidence = signatureEvidence;
    if (containerName)
        record.containerName = containerName;
    if (signature)
        record.signature = signature;
    return record;
}
/**
 * Which entities were alive at a commit, and what each looked like.
 *
 * Three cases per lineage, and they are genuinely different questions:
 *
 * - It has an event at or before the commit — its state is that event's current
 *   side, unless the event was its death.
 * - Its first event comes later but describes a predecessor — it existed then,
 *   in the shape that event's previous side records.
 * - It has no event at all — it was seeded at the window boundary and never
 *   changed, so its head record is its state at every commit in the window.
 */
function replayTo(entityIndex, commitOrder) {
    const alive = new Map();
    for (const [lineageId, head] of entityIndex.state.byLineageId) {
        // Cloned because `assignDeclarationIndexes` rewrites `declarationIndex` on
        // the projected records; the live head must not absorb historical order.
        if (!entityIndex.eventsByLineage.has(lineageId))
            alive.set(lineageId, { ...head.record });
    }
    for (const [lineageId, events] of entityIndex.eventsByLineage) {
        let last;
        for (const event of events) {
            if (event.commitOrder > commitOrder)
                break;
            last = event;
        }
        if (!last) {
            // Everything this lineage did happened after the commit asked about. If
            // its first act had a predecessor, it was already there in that shape.
            const record = recordFrom(events[0], 'previous');
            if (record)
                alive.set(lineageId, record);
            continue;
        }
        if (last.changeType === 'deleted') {
            alive.delete(lineageId);
            continue;
        }
        const record = recordFrom(last, 'current');
        if (record)
            alive.set(lineageId, record);
    }
    return alive;
}
/** Declaration order within a file, recovered from position. */
function assignDeclarationIndexes(byPath) {
    for (const entities of byPath.values()) {
        entities.sort((a, b) => a.record.startLine - b.record.startLine || compareCodeUnits(a.record.name, b.record.name));
        for (const [index, entity] of entities.entries())
            entity.record.declarationIndex = index;
    }
}
export async function resolveEntitiesAtRef(options) {
    let commit;
    try {
        commit = await resolveCommit(options.ref, { repoRoot: options.repoRoot, signal: options.signal });
    }
    catch (error) {
        const mapped = mapEntityGitError(error);
        return { status: 'error', ...mapped };
    }
    if (!commit) {
        return { status: 'error', code: 'invalid-ref', message: `Could not resolve "${options.ref}" to a commit.` };
    }
    return projectEntitiesAtCommit({
        ref: options.ref,
        commit,
        entityIndex: options.entityIndex,
        recognizer: options.recognizer,
    });
}
/**
 * Project historical entity state at an already-resolved commit.
 *
 * Kept separate from ref resolution so a compound operation can resolve a
 * mutable ref once, pin the SHA, then reuse the event-log replay without a
 * second Git lookup. The replay itself remains private to this module.
 */
export function projectEntitiesAtCommit(options) {
    const commit = options.commit;
    const record = options.entityIndex.commitBySha.get(commit);
    if (!record) {
        // Outside the indexed window is a different answer from "no such ref", and
        // conflating them would send a caller looking for a typo that is not there.
        return {
            status: 'error',
            code: 'invalid-ref',
            message: `Commit ${commit.slice(0, 12)} is outside the indexed history window, so OCC has no record of ` +
                `what existed there. Raise maxCommits or index from an older head.`,
        };
    }
    const alive = replayTo(options.entityIndex, record.order);
    const index = {
        all: [],
        byNodeId: new Map(),
        byLineageId: new Map(),
        byName: new Map(),
        byQualifiedName: new Map(),
        byPath: new Map(),
    };
    for (const [lineageId, entityRecord] of alive) {
        const fileSignal = options.recognizer.classifyFile({ relativePath: entityRecord.relativePath });
        const symbolSignal = options.recognizer.classifySymbol({
            name: entityRecord.name,
            kind: entityRecord.kind,
            containerName: entityRecord.containerName,
        });
        const testSignal = options.recognizer.classifyEntity(fileSignal, symbolSignal);
        // No `nodeId`: node ids belong to an index of one particular working tree,
        // and there is no graph for a past revision. Anything that needs one says so
        // rather than traversing an unrelated tree's edges.
        const ref = {
            lineageId,
            name: entityRecord.name,
            qualifiedName: entityRecord.qualifiedName,
            kind: entityRecord.kind,
            relativePath: entityRecord.relativePath,
            startLine: entityRecord.startLine,
            endLine: entityRecord.endLine,
            isExported: entityRecord.isExported,
            isTest: testSignal.isTest,
        };
        if (entityRecord.containerName)
            ref.containerName = entityRecord.containerName;
        if (entityRecord.signature)
            ref.signature = entityRecord.signature;
        if (testSignal.isTest)
            ref.testReasons = testSignal.reasons;
        const entity = { record: entityRecord, ref };
        index.all.push(entity);
        index.byLineageId.set(lineageId, entity);
        push(index.byName, entityRecord.name, entity);
        push(index.byQualifiedName, entityRecord.qualifiedName, entity);
        push(index.byPath, entityRecord.relativePath, entity);
    }
    assignDeclarationIndexes(index.byPath);
    return { status: 'resolved', at: { ref: options.ref, commit, commitOrder: record.order, index } };
}
//# sourceMappingURL=at-ref.js.map