import { listCodeSeams, seamKeyFor } from '../seams.js';
import { moduleForRelativePath } from '../query.js';
import { compareCodeUnits } from '../../utils.js';
import { entityImpact } from './impact.js';
import { DEFAULT_ENTITY_BOUNDS } from './types.js';
const MAX_SKIPPED_CHANGE_DETAILS = 100;
function changeRef(change) {
    return change.current ?? change.previous;
}
function recordSkippedChange(skipped, change, reason) {
    skipped.count++;
    if (skipped.rows.length >= MAX_SKIPPED_CHANGE_DETAILS)
        return;
    const ref = changeRef(change);
    if (!ref)
        return;
    skipped.rows.push({
        qualifiedName: ref.qualifiedName ?? ref.name,
        relativePath: ref.relativePath,
        reason,
    });
}
function sameCurrentEntity(candidate, ref) {
    return (candidate.ref.relativePath === ref.relativePath &&
        candidate.ref.kind === ref.kind &&
        (candidate.ref.qualifiedName ?? candidate.ref.name) === (ref.qualifiedName ?? ref.name) &&
        candidate.ref.startLine === ref.startLine &&
        candidate.ref.endLine === ref.endLine);
}
/**
 * A diff can describe arbitrary refs while the session graph describes the
 * working tree. Exact identity is therefore required before traversing: a
 * shared lineage id alone is not permission to answer a historical question
 * with today's call graph.
 */
function resolveCurrentEntity(ref, live) {
    const byLineage = live.byLineageId.get(ref.lineageId);
    if (byLineage && sameCurrentEntity(byLineage, ref))
        return byLineage;
    return live.byPath.get(ref.relativePath)?.find(candidate => sameCurrentEntity(candidate, ref));
}
function changedLogicEntities(changes, live, diagnostics, skipped) {
    const entities = [];
    const seen = new Set();
    for (const change of changes) {
        // `structuralChange` is the public tri-state projection of the flags: null
        // means logic evidence was unavailable, while false is a proved non-logic
        // change and therefore needs no verification obligation.
        if (change.structuralChange === null) {
            const ref = changeRef(change);
            recordSkippedChange(skipped, change, 'logic-classification-unknown');
            diagnostics.push({
                code: 'evidence-unavailable',
                message: `Logic classification for "${ref?.qualifiedName ?? ref?.name ?? 'unknown entity'}" was unavailable; its verification obligations were skipped.`,
                ...(ref ? { relativePath: ref.relativePath } : {}),
            });
            continue;
        }
        if (change.structuralChange !== true)
            continue;
        if (!change.current) {
            const ref = changeRef(change);
            recordSkippedChange(skipped, change, 'current-graph-join-missing');
            diagnostics.push({
                code: 'evidence-unavailable',
                message: `Logic-changed entity "${ref?.qualifiedName ?? ref?.name ?? 'unknown entity'}" has no current graph identity; its test and seam obligations are absent.`,
                ...(ref ? { relativePath: ref.relativePath } : {}),
            });
            continue;
        }
        const entity = resolveCurrentEntity(change.current, live);
        if (!entity) {
            recordSkippedChange(skipped, change, 'current-graph-join-missing');
            diagnostics.push({
                code: 'evidence-unavailable',
                message: `Could not join changed entity "${change.current.qualifiedName ?? change.current.name}" ` +
                    'to the current code graph; its test and seam obligations are absent.',
                relativePath: change.current.relativePath,
            });
            continue;
        }
        const key = entity.ref.nodeId ?? entity.ref.lineageId;
        if (seen.has(key))
            continue;
        seen.add(key);
        entities.push({ change, entity });
    }
    return entities;
}
/**
 * Existing static seams directly touched by a logic-changed entity.
 *
 * Call edges are entity-addressable. Imports are file-addressable, so an
 * import seam is included when both files participate in the reached reverse
 * subgraph.
 * Intersecting with `listCodeSeams` keeps this projection on the canonical
 * module grouping and excludes keys that are not real current seams.
 */
function touchedSeamKeys(index, entities) {
    const nodeIds = new Set(entities.flatMap(entity => (entity.ref.nodeId ? [entity.ref.nodeId] : [])));
    const relativePaths = new Set(entities.map(entity => entity.ref.relativePath));
    const relativeByPath = new Map(index.files.map(file => [file.path, file.relativePath]));
    const touched = new Set();
    for (const edge of index.edges) {
        if (edge.type !== 'calls' && edge.type !== 'imports')
            continue;
        const fromRelative = relativeByPath.get(edge.fromPath);
        const toRelative = edge.toPath ? relativeByPath.get(edge.toPath) : undefined;
        if (!fromRelative || !toRelative)
            continue;
        const incident = edge.type === 'calls'
            ? nodeIds.has(edge.from) && Boolean(edge.to && nodeIds.has(edge.to))
            : relativePaths.has(fromRelative) && relativePaths.has(toRelative);
        if (!incident)
            continue;
        const fromModule = moduleForRelativePath(fromRelative);
        const toModule = moduleForRelativePath(toRelative);
        if (fromModule === toModule)
            continue;
        touched.add(seamKeyFor(fromModule, toModule));
    }
    const current = new Set(listCodeSeams(index).map(seam => seam.seamKey));
    return [...touched].filter(seamKey => current.has(seamKey)).sort();
}
/**
 * Compose semantic diff, reverse impact traversal, and static seams into
 * verification obligations. No history prior or risk scalar participates.
 */
export function computeEntityObligations(diff, context) {
    const diagnostics = [];
    const split = {
        logic: diff.changes.filter(change => change.flags.logicChanged === true).length,
        cosmetic: diff.changes.filter(change => change.flags.cosmeticOnly === true).length,
    };
    const skipped = { rows: [], count: 0 };
    const entities = changedLogicEntities(diff.changes, context.live, diagnostics, skipped);
    const maxEntities = (context.bounds ?? DEFAULT_ENTITY_BOUNDS).maxLiveEntities;
    const gaps = [];
    const seamEntities = new Map(entities.map(({ entity }) => [entity.ref.nodeId ?? entity.ref.lineageId, entity]));
    let impactsComplete = true;
    for (const { change, entity } of entities) {
        const impact = entityImpact(entity, { scope: 'dependents', depth: 0, maxEntities }, {
            index: context.index,
            live: context.live,
            formatFiles: context.formatFiles,
            bounds: context.bounds,
            indexTruncated: context.indexTruncated,
        });
        for (const entry of [...impact.dependents, ...impact.transitive, ...impact.tests]) {
            const reached = entry.entity.nodeId
                ? context.live.byNodeId.get(entry.entity.nodeId)
                : context.live.byLineageId.get(entry.entity.lineageId);
            if (reached)
                seamEntities.set(reached.ref.nodeId ?? reached.ref.lineageId, reached);
        }
        if (impact.coverage !== 'full') {
            impactsComplete = false;
            recordSkippedChange(skipped, change, 'impact-partial');
            diagnostics.push({
                code: 'evidence-unavailable',
                message: `Reverse test traversal for "${entity.record.qualifiedName}" was partial; ` +
                    (impact.tests.length > 0
                        ? 'reached tests are retained, but coverage is incomplete.'
                        : 'no test gap is claimed.'),
                relativePath: entity.record.relativePath,
            });
            continue;
        }
        if (impact.tests.length > 0)
            continue;
        gaps.push({
            entityId: entity.ref.lineageId,
            qualifiedName: entity.record.qualifiedName,
            nearestTestedAncestor: null,
        });
    }
    gaps.sort((a, b) => compareCodeUnits(a.qualifiedName, b.qualifiedName) || compareCodeUnits(a.entityId, b.entityId));
    return {
        obligations: {
            gaps,
            seamCrossings: touchedSeamKeys(context.index, [...seamEntities.values()]),
            split,
            coverage: skipped.count === 0 && impactsComplete ? 'complete' : 'partial',
            evidenceGaps: {
                skippedChanges: skipped.rows,
                count: skipped.count,
            },
        },
        diagnostics,
    };
}
//# sourceMappingURL=obligations.js.map