/** Default maximum number of failure reports retained in memory. */
export const DEFAULT_REGISTRY_LIMIT = 100;
export class ForensicsRegistry {
    _reports = [];
    _byId = new Map();
    _limit;
    _subscribers = new Set();
    constructor(limit = DEFAULT_REGISTRY_LIMIT) {
        this._limit = limit;
    }
    /**
     * Push a new failure report into the registry.
     * If the buffer is full, the oldest entry is evicted.
     */
    push(report) {
        // Evict oldest if at capacity
        if (this._reports.length >= this._limit) {
            const evicted = this._reports.shift();
            if (evicted)
                this._byId.delete(evicted.id);
        }
        this._reports.push(report);
        this._byId.set(report.id, report);
        this._notify();
    }
    /**
     * Return the most recently generated report, or undefined if none exist.
     */
    latest() {
        return this._reports.at(-1);
    }
    /**
     * Return a report by its short ID, or undefined if not found.
     */
    getById(id) {
        return this._byId.get(id);
    }
    /**
     * Return all reports, newest first.
     */
    getAll() {
        return [...this._reports].reverse();
    }
    /**
     * Return the count of retained reports.
     */
    count() {
        return this._reports.length;
    }
    /**
     * Serialize a report to a pretty-printed JSON string.
     * Returns undefined if the report ID is not found.
     */
    exportAsJson(id) {
        const report = this._byId.get(id);
        if (!report)
            return undefined;
        return JSON.stringify(report, null, 2);
    }
    /**
     * Build a richer incident bundle for export, including derived evidence and
     * optional replay state when available.
     */
    buildBundle(id, options = {}) {
        const report = this._byId.get(id);
        if (!report)
            return undefined;
        const rootCause = report.causalChain.find((entry) => entry.isRootCause)?.description;
        const terminalPhase = report.phaseLedger.at(-1);
        return {
            schemaVersion: 'v1',
            exportedAt: Date.now(),
            report,
            evidence: {
                rootCause,
                terminalPhase: terminalPhase?.phase,
                terminalOutcome: terminalPhase?.outcome,
                phaseCount: report.phaseLedger.length,
                causalCount: report.causalChain.length,
                cascadeCount: report.cascadeEvents.length,
                permissionDecisionCount: report.permissionEvidence.length,
                deniedPermissionCount: report.permissionEvidence.filter((entry) => entry.approved === false).length,
                budgetBreachCount: report.budgetBreaches.length,
                slowPhases: report.phaseTimings
                    .filter((phase) => (phase.durationMs ?? 0) >= 1_000)
                    .map((phase) => phase.phase),
                jumpLinkCount: report.jumpLinks.length,
                relatedIds: {
                    turnId: report.turnId,
                    taskId: report.taskId,
                    agentId: report.agentId,
                },
            },
            replay: this._buildReplayEvidence(report, options.replaySnapshot),
        };
    }
    /**
     * Serialize an incident bundle to pretty-printed JSON.
     * Returns undefined if the report ID is not found.
     */
    exportBundleAsJson(id, options = {}) {
        const bundle = this.buildBundle(id, options);
        if (!bundle)
            return undefined;
        return JSON.stringify(bundle, null, 2);
    }
    /**
     * Subscribe to registry changes. Returns an unsubscribe function.
     */
    subscribe(callback) {
        this._subscribers.add(callback);
        return () => { this._subscribers.delete(callback); };
    }
    _notify() {
        for (const cb of this._subscribers) {
            try {
                cb();
            }
            catch { /* non-fatal — subscriber errors must not affect the registry */ }
        }
    }
    _buildReplayEvidence(report, snapshot) {
        if (!snapshot) {
            return {
                status: 'unavailable',
                mismatchCount: 0,
                mismatches: [],
                relatedMismatches: [],
                mismatchBreakdown: { byKind: {}, byFailureMode: {}, byOwnerDomain: {} },
                turnSummaries: [],
            };
        }
        if (snapshot.status === 'idle' || !snapshot.runId) {
            return {
                status: 'not_loaded',
                mismatchCount: 0,
                mismatches: [],
                relatedMismatches: [],
                mismatchBreakdown: { byKind: {}, byFailureMode: {}, byOwnerDomain: {} },
                turnSummaries: [],
            };
        }
        const mismatches = snapshot.mismatches.map((mismatch) => ({
            rev: mismatch.rev,
            kind: mismatch.kind,
            description: mismatch.description,
            eventName: mismatch.eventName,
            ownerDomain: mismatch.ownerDomain,
            failureMode: mismatch.failureMode,
            relatedTurnId: mismatch.relatedTurnId,
        }));
        const turnSummaries = snapshot.turnSummaries.map((summary) => ({
            turnId: summary.turnId,
            outcome: summary.outcome,
            terminalEvent: summary.terminalEvent,
            startedRev: summary.startedRev,
            terminalRev: summary.terminalRev,
            stopReason: summary.stopReason,
            message: summary.message,
        }));
        const relatedMismatches = report.turnId
            ? mismatches.filter((mismatch) => mismatch.relatedTurnId === report.turnId)
            : [];
        return {
            status: 'available',
            runId: snapshot.runId,
            currentRev: snapshot.currentRev,
            totalRevisions: snapshot.totalRevisions,
            mismatchCount: mismatches.length,
            mismatches,
            relatedMismatches,
            mismatchBreakdown: {
                byKind: this._countBy(mismatches, (mismatch) => mismatch.kind),
                byFailureMode: this._countBy(mismatches, (mismatch) => mismatch.failureMode ?? 'unknown'),
                byOwnerDomain: this._countBy(mismatches, (mismatch) => mismatch.ownerDomain ?? 'unknown'),
            },
            turnSummaries,
            matchingTurnSummary: report.turnId
                ? turnSummaries.find((summary) => summary.turnId === report.turnId)
                : undefined,
        };
    }
    _countBy(items, key) {
        const counts = {};
        for (const item of items) {
            const label = key(item);
            counts[label] = (counts[label] ?? 0) + 1;
        }
        return counts;
    }
}
