/**
 * ForensicsCollector — subscribes to the RuntimeEventBus and automatically
 * generates FailureReport objects when tasks or turns reach terminal failure
 * states.
 *
 * Integration points:
 * - 'tasks' domain: TASK_FAILED, TASK_CANCELLED
 * - 'turn' domain: TURN_ERROR, TURN_CANCEL
 * - 'tools' domain: tool failures (permission denial, execution failure)
 * - Health cascade events from the ops/session domain
 *
 * Reports are pushed to the ForensicsRegistry and a FORENSICS_REPORT_CREATED
 * event is emitted so the panel can refresh.
 */
import { randomUUID } from 'node:crypto';
import { summarizeError } from '../../utils/error-display.js';
import { logger } from '../../utils/logger.js';
import { classifyFailure, summariseFailure } from './classifier.js';
import { emitForensicsReportCreated } from '../emitters/forensics.js';
/** Maximum orphaned trackers retained before evicting oldest entries. */
const MAX_TRACKER_SIZE = 500;
// ---------------------------------------------------------------------------
// ForensicsCollector
// ---------------------------------------------------------------------------
export class ForensicsCollector {
    _bus;
    _registry;
    _unsubs = [];
    /** Active turn trackers keyed by turnId. */
    _turns = new Map();
    /** Active task trackers keyed by taskId. */
    _tasks = new Map();
    /** Correlation map from tool call id to owning turn id. */
    _callToTurn = new Map();
    constructor(bus, registry) {
        this._bus = bus;
        this._registry = registry;
        this._start();
    }
    _start() {
        // Subscribe to turn domain
        this._unsubs.push(this._bus.onDomain('turn', (env) => this._collect('turn', env, () => {
            this._handleTurnEnvelope(env);
        })));
        // Subscribe to tasks domain
        this._unsubs.push(this._bus.onDomain('tasks', (env) => this._collect('tasks', env, () => {
            this._handleTaskEnvelope(env);
        })));
        // Subscribe to tools domain for permission/failure context
        this._unsubs.push(this._bus.onDomain('tools', (env) => this._collect('tools', env, () => {
            this._handleToolEnvelope(env);
        })));
        this._unsubs.push(this._bus.onDomain('permissions', (env) => this._collect('permissions', env, () => {
            this._handlePermissionEnvelope(env);
        })));
        // Subscribe to session domain for cascade/compaction events
        this._unsubs.push(this._bus.onDomain('compaction', (env) => this._collect('compaction', env, () => {
            this._handleCompactionEnvelope(env);
        })));
    }
    _collect(domain, env, handle) {
        try {
            handle();
        }
        catch (error) {
            logger.warn('Forensics collector event collection failed', {
                domain,
                type: env.type,
                sessionId: env.sessionId,
                traceId: env.traceId,
                turnId: env.turnId,
                taskId: env.taskId,
                error: summarizeError(error),
            });
        }
    }
    // ── Turn handling ──────────────────────────────────────────────────────────
    _handleTurnEnvelope(env) {
        const payload = env.payload;
        const turnId = payload.turnId;
        if (!turnId)
            return;
        switch (payload.type) {
            case 'TURN_SUBMITTED': {
                // Evict oldest orphaned tracker if at cap
                if (this._turns.size >= MAX_TRACKER_SIZE) {
                    const firstKey = this._turns.keys().next().value;
                    if (firstKey !== undefined) {
                        const evicted = this._turns.get(firstKey);
                        this._turns.delete(firstKey);
                        logger.warn('Forensics collector evicted active turn tracker', {
                            turnId: firstKey,
                            sessionId: evicted?.sessionId,
                            traceId: evicted?.traceId,
                            maxTrackerSize: MAX_TRACKER_SIZE,
                        });
                    }
                }
                this._turns.set(turnId, {
                    turnId,
                    sessionId: env.sessionId ?? '',
                    traceId: env.traceId ?? '',
                    startedAt: env.ts,
                    phaseTimings: [],
                    phaseLedger: [],
                    causalChain: [],
                    cascadeEvents: [],
                    permissionEvidence: [],
                    budgetBreaches: [],
                    hasToolFailure: false,
                    hasPermissionDenial: false,
                    hasCascadeEvents: false,
                    hasCompactionError: false,
                    currentPhase: 'SUBMITTED',
                    lastPhaseStart: env.ts,
                    currentPhaseEnterEventType: payload.type,
                    _causalSeq: 0,
                    _phaseSeq: 0,
                });
                break;
            }
            case 'PREFLIGHT_OK': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'PREFLIGHT', env.ts, payload.type);
                break;
            }
            case 'PREFLIGHT_FAIL': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._closePhase(t, 'turn', env.ts, 'failed', payload.type, payload.reason);
                this._addCausal(t, env.ts, `Preflight failed: ${payload.reason ?? 'unknown reason'}`, payload.type, true);
                this._finalise_turn(t, payload.stopReason ?? payload.reason, payload.reason ?? 'Preflight failed', false);
                this._turns.delete(turnId);
                break;
            }
            case 'STREAM_START': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'STREAM', env.ts, payload.type);
                break;
            }
            case 'STREAM_END': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'STREAM_COMPLETE', env.ts, payload.type);
                break;
            }
            case 'TOOL_BATCH_READY': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'TOOL_BATCH', env.ts, payload.type);
                break;
            }
            case 'TOOLS_DONE': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'POST_TOOL_BATCH', env.ts, payload.type);
                break;
            }
            case 'POST_HOOKS_DONE': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._transitionPhase(t, 'turn', 'POST_HOOKS', env.ts, payload.type);
                break;
            }
            case 'TURN_COMPLETED': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._closePhase(t, 'turn', env.ts, 'succeeded', payload.type);
                this._turns.delete(turnId);
                break;
            }
            case 'TURN_ERROR': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._closePhase(t, 'turn', env.ts, 'failed', payload.type, payload.error);
                this._addCausal(t, env.ts, `Turn error: ${payload.error ?? 'unknown error'}`, payload.type, true);
                this._finalise_turn(t, payload.stopReason, payload.error, false);
                this._turns.delete(turnId);
                break;
            }
            case 'TURN_CANCEL': {
                const t = this._turns.get(turnId);
                if (!t)
                    break;
                this._closePhase(t, 'turn', env.ts, 'cancelled', payload.type, payload.reason);
                this._addCausal(t, env.ts, `Turn cancelled: ${payload.reason ?? 'no reason given'}`, payload.type, false);
                this._finalise_turn(t, payload.stopReason ?? payload.reason, payload.reason, true);
                this._turns.delete(turnId);
                break;
            }
        }
    }
    // ── Task handling ──────────────────────────────────────────────────────────
    _handleTaskEnvelope(env) {
        const payload = env.payload;
        const taskId = payload.taskId;
        if (!taskId)
            return;
        switch (payload.type) {
            case 'TASK_CREATED': {
                // Evict oldest orphaned tracker if at cap
                if (this._tasks.size >= MAX_TRACKER_SIZE) {
                    const firstKey = this._tasks.keys().next().value;
                    if (firstKey !== undefined) {
                        const evicted = this._tasks.get(firstKey);
                        this._tasks.delete(firstKey);
                        logger.warn('Forensics collector evicted active task tracker', {
                            taskId: firstKey,
                            sessionId: evicted?.sessionId,
                            traceId: evicted?.traceId,
                            maxTrackerSize: MAX_TRACKER_SIZE,
                        });
                    }
                }
                this._tasks.set(taskId, {
                    taskId,
                    sessionId: env.sessionId ?? '',
                    traceId: env.traceId ?? '',
                    startedAt: env.ts,
                    phaseTimings: [],
                    phaseLedger: [],
                    causalChain: [],
                    cascadeEvents: [],
                    permissionEvidence: [],
                    budgetBreaches: [],
                    agentId: payload.agentId,
                    hasToolFailure: false,
                    hasPermissionDenial: false,
                    hasCascadeEvents: false,
                    hasCompactionError: false,
                    currentPhase: 'CREATED',
                    lastPhaseStart: env.ts,
                    currentPhaseEnterEventType: payload.type,
                    _causalSeq: 0,
                    _phaseSeq: 0,
                });
                break;
            }
            case 'TASK_STARTED': {
                const task = this._tasks.get(taskId);
                if (task && payload.agentId) {
                    task.agentId = payload.agentId;
                }
                if (task) {
                    this._transitionPhase(task, 'task', 'RUNNING', env.ts, payload.type);
                }
                break;
            }
            case 'TASK_FAILED': {
                const task = this._tasks.get(taskId);
                if (!task)
                    break;
                this._closePhase(task, 'task', env.ts, 'failed', payload.type, payload.error);
                this._addCausal(task, env.ts, `Task failed: ${payload.error ?? 'unknown error'}`, payload.type, true);
                this._finalise_task(task, payload.error, false);
                this._tasks.delete(taskId);
                break;
            }
            case 'TASK_CANCELLED': {
                const task = this._tasks.get(taskId);
                if (!task)
                    break;
                this._closePhase(task, 'task', env.ts, 'cancelled', payload.type, payload.reason);
                this._addCausal(task, env.ts, `Task cancelled: ${payload.reason ?? 'no reason given'}`, payload.type, false);
                this._finalise_task(task, payload.reason, true);
                this._tasks.delete(taskId);
                break;
            }
            case 'TASK_COMPLETED': {
                const task = this._tasks.get(taskId);
                if (task) {
                    this._closePhase(task, 'task', env.ts, 'succeeded', payload.type);
                }
                this._tasks.delete(taskId);
                break;
            }
        }
    }
    // ── Tool handling ──────────────────────────────────────────────────────────
    _handleToolEnvelope(env) {
        const payload = env.payload;
        if (payload.callId && payload.turnId) {
            this._callToTurn.set(payload.callId, payload.turnId);
        }
        switch (payload.type) {
            case 'TOOL_FAILED': {
                if (!payload.turnId)
                    break;
                const t = this._turns.get(payload.turnId);
                if (!t)
                    break;
                t.hasToolFailure = true;
                this._addCausal(t, env.ts, `Tool "${payload.tool ?? 'unknown'}" failed: ${payload.error ?? 'unknown error'}`, payload.type, false, payload.tool ? { tool: payload.tool } : undefined);
                if (payload.callId)
                    this._callToTurn.delete(payload.callId);
                break;
            }
            case 'BUDGET_EXCEEDED_MS':
            case 'BUDGET_EXCEEDED_TOKENS':
            case 'BUDGET_EXCEEDED_COST': {
                if (!payload.turnId || !payload.callId)
                    break;
                const t = this._turns.get(payload.turnId);
                if (!t)
                    break;
                t.budgetBreaches.push({
                    callId: payload.callId,
                    tool: payload.tool ?? 'unknown',
                    eventType: payload.type,
                    phase: payload.phase ?? 'unknown',
                    ts: env.ts,
                    meta: {
                        ...(payload.limitMs !== undefined ? { limitMs: payload.limitMs } : {}),
                        ...(payload.elapsedMs !== undefined ? { elapsedMs: payload.elapsedMs } : {}),
                        ...(payload.limitTokens !== undefined ? { limitTokens: payload.limitTokens } : {}),
                        ...(payload.usedTokens !== undefined ? { usedTokens: payload.usedTokens } : {}),
                        ...(payload.limitCostUsd !== undefined ? { limitCostUsd: payload.limitCostUsd } : {}),
                        ...(payload.usedCostUsd !== undefined ? { usedCostUsd: payload.usedCostUsd } : {}),
                    },
                });
                this._addCausal(t, env.ts, `Tool "${payload.tool ?? 'unknown'}" exceeded runtime budget (${payload.type}) during ${payload.phase ?? 'unknown phase'}`, payload.type, false, { tool: payload.tool ?? 'unknown', phase: payload.phase ?? 'unknown' });
                break;
            }
            case 'TOOL_PERMISSION_CHECKED': {
                // If approved is explicitly false, record permission denial
                if (payload.approved === false && payload.turnId) {
                    const t = this._turns.get(payload.turnId);
                    if (!t)
                        break;
                    t.hasPermissionDenial = true;
                    this._addCausal(t, env.ts, `Permission denied for tool "${payload.tool ?? 'unknown'}"`, payload.type, false, payload.tool ? { tool: payload.tool } : undefined);
                }
                break;
            }
            case 'TOOL_SUCCEEDED':
            case 'TOOL_CANCELLED': {
                if (payload.callId)
                    this._callToTurn.delete(payload.callId);
                break;
            }
        }
    }
    _handlePermissionEnvelope(env) {
        const payload = env.payload;
        const callId = payload.callId;
        if (!callId)
            return;
        const turnId = this._callToTurn.get(callId);
        if (!turnId)
            return;
        const tracker = this._turns.get(turnId);
        if (!tracker)
            return;
        if (payload.type === 'PERMISSION_REQUESTED') {
            tracker.permissionEvidence.push({
                callId,
                tool: payload.tool ?? 'unknown',
                requestedAt: env.ts,
                classification: payload.classification,
                riskLevel: payload.riskLevel,
                summary: payload.summary,
            });
            return;
        }
        if (payload.type === 'DECISION_EMITTED') {
            const existing = tracker.permissionEvidence.find((entry) => entry.callId === callId);
            if (existing) {
                const idx = tracker.permissionEvidence.indexOf(existing);
                tracker.permissionEvidence[idx] = {
                    ...existing,
                    tool: payload.tool ?? existing.tool,
                    decidedAt: env.ts,
                    durationMs: existing.requestedAt !== undefined ? env.ts - existing.requestedAt : undefined,
                    approved: payload.approved,
                    source: payload.source,
                    reasonCode: payload.reasonCode,
                    classification: payload.classification ?? existing.classification,
                    riskLevel: payload.riskLevel ?? existing.riskLevel,
                    summary: payload.summary ?? existing.summary,
                };
            }
            else {
                tracker.permissionEvidence.push({
                    callId,
                    tool: payload.tool ?? 'unknown',
                    decidedAt: env.ts,
                    approved: payload.approved,
                    source: payload.source,
                    reasonCode: payload.reasonCode,
                    classification: payload.classification,
                    riskLevel: payload.riskLevel,
                    summary: payload.summary,
                });
            }
        }
    }
    // ── Compaction handling ────────────────────────────────────────────────────
    _handleCompactionEnvelope(env) {
        const payload = env.payload;
        if (payload.type !== 'COMPACTION_FAILED')
            return;
        // Mark all active turns as having a compaction error
        for (const t of this._turns.values()) {
            if (t.sessionId === env.sessionId) {
                t.hasCompactionError = true;
                this._addCausal(t, env.ts, `Compaction failed: ${payload.error ?? 'unknown error'}`, payload.type, false);
            }
        }
    }
    // ── Report finalisation ───────────────────────────────────────────────────
    _finalise_turn(tracker, stopReason, errorMessage, wasCancelled) {
        const classification = classifyFailure({
            stopReason,
            errorMessage,
            wasCancelled,
            hasCascadeEvents: tracker.hasCascadeEvents,
            hasToolFailure: tracker.hasToolFailure,
            hasPermissionDenial: tracker.hasPermissionDenial,
            hasCompactionError: tracker.hasCompactionError,
        });
        const summary = summariseFailure(classification, errorMessage, stopReason);
        const jumpLinks = [
            { label: 'Open tool inspector', kind: 'panel', target: 'tools' },
            { label: 'Open health dashboard', kind: 'panel', target: 'provider-health' },
        ];
        const reportId = this._shortId(tracker.traceId);
        const report = {
            id: reportId,
            traceId: tracker.traceId,
            sessionId: tracker.sessionId,
            generatedAt: Date.now(),
            classification,
            summary,
            stopReason,
            errorMessage,
            turnId: tracker.turnId,
            phaseTimings: tracker.phaseTimings,
            phaseLedger: tracker.phaseLedger,
            causalChain: tracker.causalChain,
            cascadeEvents: tracker.cascadeEvents,
            permissionEvidence: tracker.permissionEvidence,
            budgetBreaches: tracker.budgetBreaches,
            jumpLinks,
        };
        this._publishReport(report, { turnId: tracker.turnId });
    }
    _finalise_task(tracker, errorMessage, wasCancelled) {
        const classification = classifyFailure({
            errorMessage,
            wasCancelled,
            hasCascadeEvents: tracker.hasCascadeEvents,
            hasToolFailure: tracker.hasToolFailure,
            hasPermissionDenial: tracker.hasPermissionDenial,
            hasCompactionError: tracker.hasCompactionError,
        });
        const summary = summariseFailure(classification, errorMessage);
        const jumpLinks = [
            { label: 'View in ops-control', kind: 'panel', target: 'ops-control' },
            { label: 'Open health dashboard', kind: 'panel', target: 'provider-health' },
        ];
        const reportId = this._shortId(tracker.traceId);
        const report = {
            id: reportId,
            traceId: tracker.traceId,
            sessionId: tracker.sessionId,
            generatedAt: Date.now(),
            classification,
            summary,
            errorMessage,
            taskId: tracker.taskId,
            agentId: tracker.agentId,
            phaseTimings: tracker.phaseTimings,
            phaseLedger: tracker.phaseLedger,
            causalChain: tracker.causalChain,
            cascadeEvents: tracker.cascadeEvents,
            permissionEvidence: tracker.permissionEvidence,
            budgetBreaches: tracker.budgetBreaches,
            jumpLinks,
        };
        this._publishReport(report, { taskId: tracker.taskId });
    }
    // ── Helpers ────────────────────────────────────────────────────────────────
    _transitionPhase(t, domain, nextPhase, ts, eventType) {
        this._closePhase(t, domain, ts, 'succeeded', eventType);
        t.currentPhase = nextPhase;
        t.lastPhaseStart = ts;
        t.currentPhaseEnterEventType = eventType;
    }
    _closePhase(t, domain, ts, outcome, eventType, error) {
        if (!t.currentPhase || t.lastPhaseStart === undefined)
            return;
        t.phaseTimings.push({
            phase: t.currentPhase,
            startedAt: t.lastPhaseStart,
            endedAt: ts,
            durationMs: ts - t.lastPhaseStart,
            success: outcome === 'succeeded',
            error,
        });
        t.phaseLedger.push({
            seq: ++t._phaseSeq,
            domain,
            phase: t.currentPhase,
            enterEventType: t.currentPhaseEnterEventType ?? 'unknown',
            enteredAt: t.lastPhaseStart,
            exitEventType: eventType,
            exitedAt: ts,
            durationMs: ts - t.lastPhaseStart,
            outcome,
            error,
        });
    }
    _addCausal(t, ts, description, sourceEventType, isRootCause, context) {
        t.causalChain.push({
            seq: ++t._causalSeq,
            ts,
            description,
            sourceEventType,
            isRootCause,
            context,
        });
    }
    _publishReport(report, context) {
        try {
            this._registry.push(report);
        }
        catch (error) {
            logger.warn('Forensics collector failed to store report', {
                reportId: report.id,
                classification: report.classification,
                sessionId: report.sessionId,
                traceId: report.traceId,
                ...context,
                error: summarizeError(error),
            });
            return;
        }
        try {
            emitForensicsReportCreated(this._bus, { sessionId: report.sessionId, traceId: report.traceId, source: 'ForensicsCollector' }, {
                reportId: report.id,
                classification: report.classification,
                errorMessage: report.errorMessage,
                ...context,
            });
        }
        catch (error) {
            logger.warn('Forensics collector failed to emit report event', {
                reportId: report.id,
                classification: report.classification,
                sessionId: report.sessionId,
                traceId: report.traceId,
                ...context,
                error: summarizeError(error),
            });
        }
    }
    /** Derive a short human-readable report ID from a trace ID. */
    _shortId(traceId) {
        // Use the first 8 hex chars of the trace ID, fall back to a fresh UUID prefix
        const clean = traceId.replace(/-/g, '');
        return clean.length >= 8 ? clean.slice(0, 8) : randomUUID().replace(/-/g, '').slice(0, 8);
    }
    /** Dispose all event bus subscriptions. */
    dispose() {
        for (const unsub of this._unsubs) {
            try {
                unsub();
            }
            catch (error) {
                logger.warn('Forensics collector unsubscribe failed', { error: summarizeError(error) });
            }
        }
        this._unsubs.length = 0;
        this._turns.clear();
        this._tasks.clear();
        this._callToTurn.clear();
    }
}
