import { MATCH_CONFIDENCE, SEMANTIC_CHANGE_TYPES } from './types.js';
import { normalizeRelativePath } from './resolve.js';
import { walkBackKeyFor } from './walkback.js';
/**
 * Entities the index never reached, and what became of them.
 *
 * The bound is worth reporting whether or not the walk-back rescued them: a
 * caller deciding whether to raise its bounds needs to know the index was
 * starved, and a caller reading attribution needs to know it did not come from
 * the index. Saying "these carry no attribution" once they do would be the same
 * class of error this whole area is about.
 */
function truncationDiagnostic(context, normalized, starved, recovered) {
    const total = context.live.byPath.get(normalized)?.length ?? starved;
    const detail = recovered > 0
        ? `${recovered} of them were attributed by searching git directly instead`
        : 'so they carry no attribution';
    return {
        code: 'entity-budget-exhausted',
        message: `${starved} entities in ${normalized} were never reached by the index because it hit a bound, ` +
            `${detail}. Raise the bounds or read session.status() for the reasons.`,
        relativePath: normalized,
        total,
        kept: total - starved + recovered,
    };
}
/**
 * The newest event that changed what the entity does.
 *
 * Reformatting a file and moving a declaration both produce events, and both
 * would otherwise become the answer to "who last changed this". Skipping them
 * is the whole difference between entity blame and line blame.
 */
function lastSemanticChange(chain, atCommitOrder) {
    for (let index = chain.length - 1; index >= 0; index--) {
        if (atCommitOrder !== undefined && chain[index].commitOrder > atCommitOrder)
            continue;
        if (SEMANTIC_CHANGE_TYPES.has(chain[index].changeType))
            return chain[index];
    }
    return undefined;
}
/**
 * Per-entity blame for one file.
 *
 * Every entry states its provenance. When attribution is unavailable — the file
 * is untracked, the entity is uncommitted, history is shallow, or the entity
 * predates the indexed window — the commit fields are simply absent, and the
 * state says which of those it was. Inventing a plausible commit here would be
 * worse than admitting the gap.
 */
export function entityBlame(relativePath, context) {
    const normalized = normalizeRelativePath(relativePath);
    const diagnostics = [];
    const entities = context.live.byPath.get(normalized) ?? [];
    const isUntracked = context.untracked.has(normalized);
    const atRef = context.atCommitOrder !== undefined;
    const entries = entities.map(entity => {
        const chain = context.entityIndex.eventsByLineage.get(entity.ref.lineageId) ?? [];
        const head = context.entityIndex.state.byLineageId.get(entity.ref.lineageId);
        const event = lastSemanticChange(chain, context.atCommitOrder);
        let state;
        // At a past revision every entity in view is by definition committed; the
        // working tree is not what was asked about, so its states cannot apply.
        if (atRef)
            state = event ? 'committed' : context.shallow ? 'shallow-history' : 'no-history';
        else if (isUntracked)
            state = 'untracked';
        else if (entity.ref.provisional)
            state = 'uncommitted';
        // An indeterminate entity is one the index never reached. Its file is
        // tracked and git may well hold its whole history, so the honest answer is
        // that OCC has none — never that the working tree is dirty.
        else if (entity.ref.indeterminate || !event)
            state = context.shallow ? 'shallow-history' : 'no-history';
        else if (head && head.record.hashes.bodyHash !== entity.record.hashes.bodyHash)
            state = 'working-tree-modified';
        else
            state = 'committed';
        const entry = { entity: entity.ref, state };
        if (event && state !== 'untracked' && state !== 'uncommitted') {
            entry.commit = event.commit;
            entry.author = event.authorName;
            entry.authorEmail = event.authorEmail;
            entry.date = event.authoredAt;
            entry.subject = event.subject;
            entry.changeType = event.changeType;
            entry.matchReason = event.matchReason;
            entry.confidence = event.confidence ?? MATCH_CONFIDENCE[event.matchReason];
            return entry;
        }
        // Nothing in the window. The history usually still exists — it is simply
        // older than OCC indexed — so ask git before reporting an absence.
        if (context.walkBack && (state === 'no-history' || state === 'shallow-history')) {
            const found = context.walkBack.byEntity.get(walkBackKeyFor(entity.record))?.[0];
            if (found) {
                // Attribution came from git rather than from the window, which says
                // nothing about whether the working tree still matches what is
                // committed — so answer that with the same comparison the in-window
                // branch makes above, against the lineage head.
                //
                // Not against `found`: walk-back attribution carries commit metadata,
                // not a content snapshot that can be compared with the working tree.
                // The lineage head is the only directly comparable committed record.
                // With no head there is no evidence either way, and `committed` is the
                // honest default. At a ref there is no working tree in view, so the
                // question does not arise.
                entry.state =
                    !atRef && head && head.record.hashes.bodyHash !== entity.record.hashes.bodyHash
                        ? 'working-tree-modified'
                        : 'committed';
                entry.commit = found.commit;
                entry.author = found.authorName;
                entry.authorEmail = found.authorEmail;
                entry.date = found.authoredAt;
                entry.subject = found.subject;
                entry.changeType = found.changeType;
                entry.matchReason = found.matchReason;
                entry.confidence = MATCH_CONFIDENCE[found.matchReason];
            }
            else if (context.walkBack.exhausted && state === 'no-history') {
                // We looked and ran out of budget, which is a different fact from
                // "there is nothing to find".
                entry.state = 'outside-window';
            }
        }
        return entry;
    });
    if (context.shallow) {
        diagnostics.push({
            code: 'shallow-history',
            message: 'History is shallow, so entities older than the graft point cannot be attributed.',
            relativePath: normalized,
        });
    }
    const starved = entries.filter(entry => entry.entity.indeterminate);
    const unreached = starved.filter(entry => entry.commit === undefined).length;
    if (starved.length > 0) {
        diagnostics.push(truncationDiagnostic(context, normalized, starved.length, starved.length - unreached));
    }
    const outsideWindow = entries.filter(entry => entry.state === 'outside-window').length;
    if (outsideWindow > 0) {
        diagnostics.push({
            code: 'commit-window-truncated',
            message: `${outsideWindow} entities in ${normalized} last changed further back than the ` +
                `${context.walkBack?.commitsScanned ?? 0} commits searched; their history exists but was not reached.`,
            relativePath: normalized,
            total: entries.length,
            kept: entries.length - outsideWindow,
        });
    }
    const unattributed = entries.filter(entry => entry.state === 'no-history').length - unreached;
    if (unattributed > 0) {
        diagnostics.push({
            code: 'evidence-unavailable',
            message: `${unattributed} entities in ${normalized} predate the indexed history window and carry no attribution.`,
            relativePath: normalized,
            total: entries.length,
            kept: entries.length - unattributed,
        });
    }
    return { relativePath: normalized, entries, diagnostics };
}
//# sourceMappingURL=blame.js.map