/**
 * manager.ts
 *
 * CompactionManager — orchestrates the full compaction lifecycle state machine.
 *
 * Responsibilities:
 * - Gate all compaction behind the `session-compaction` feature flag
 * - Drive state transitions (idle → checking_threshold → strategy → boundary_commit → done/failed)
 * - Select and execute the appropriate compaction strategy
 * - Create boundary commits with lineage tracking
 * - Emit CompactionEvents at each transition
 * - Expose the resume repair pipeline
 */
import { logger } from '../../utils/logger.js';
import { applyTransition, selectStrategy, strategyToState, } from './lifecycle.js';
import { runMicrocompact, runCollapse, runAutocompact, runReactive, createBoundaryCommit, validateBoundaryCommit, } from './strategies/index.js';
import { runResumeRepair } from './resume-repair.js';
import { emitCompactionCheck, emitCompactionDone, emitCompactionFailed, emitCompactionBoundaryCommit, emitCompactionMicrocompact, emitCompactionCollapse, emitCompactionAutocompact, emitCompactionReactive, emitCompactionQualityScore, emitCompactionStrategySwitch, } from '../emitters/compaction.js';
import { computeQualityScore, escalateStrategy, LOW_QUALITY_THRESHOLD } from './quality-score.js';
import { summarizeError } from '../../utils/error-display.js';
// ---------------------------------------------------------------------------
// Manager
// ---------------------------------------------------------------------------
/**
 * CompactionManager — manages the full lifecycle of session context compaction.
 *
 * All compaction is gated behind the `session-compaction` feature flag.
 * When the flag is disabled, `compact()` is a no-op and returns the original
 * messages unchanged.
 *
 * Usage:
 * ```ts
 * const manager = createCompactionManager({ sessionId, bus, flags, contextWindow });
 * const result = await manager.compact({ messages, tokenCount: 45000, trigger: 'auto' });
 * ```
 */
export class CompactionManager {
    _sessionId;
    _bus;
    _flags;
    _contextWindow;
    _thresholdFraction;
    /** Current state machine state. */
    _state = 'idle';
    /** Most recent boundary commit (null until first successful compaction). */
    _lastCommit = null;
    /** Emitter context used for all event emissions. */
    _ctx;
    constructor(opts) {
        this._sessionId = opts.sessionId;
        this._bus = opts.bus;
        this._flags = opts.flags;
        this._contextWindow = opts.contextWindow;
        this._thresholdFraction = opts.thresholdFraction ?? 0.75;
        this._ctx = {
            sessionId: opts.sessionId,
            source: 'compaction-manager',
            traceId: crypto.randomUUID(),
        };
    }
    // ── Public API ─────────────────────────────────────────────────────────────
    /** Returns the current lifecycle state. */
    get state() {
        return this._state;
    }
    /** Returns the most recent boundary commit, or null. */
    get lastCommit() {
        return this._lastCommit;
    }
    /**
     * Options for a compaction run.
     */
    /**
     * Runs the compaction lifecycle for the given messages and trigger.
     *
     * If the `session-compaction` feature flag is disabled, returns the
     * original messages unchanged with no events emitted.
     *
     * @param opts - Run options.
     * @returns Lifecycle result, or null if gated/skipped.
     */
    async compact(opts) {
        // ── Feature flag gate ────────────────────────────────────────────────────
        if (!this._flags.isEnabled('session-compaction')) {
            logger.debug('[CompactionManager] session-compaction flag disabled; skipping', {
                sessionId: this._sessionId,
            });
            return null;
        }
        const runStart = Date.now();
        const { messages, tokenCount, trigger, isPromptTooLong } = opts;
        const threshold = Math.floor(this._contextWindow * this._thresholdFraction);
        // ── Transition: idle → checking_threshold ────────────────────────────────
        this._transition('checking_threshold');
        emitCompactionCheck(this._bus, this._ctx, {
            sessionId: this._sessionId,
            tokenCount,
            threshold,
        });
        // ── Check threshold ──────────────────────────────────────────────────────
        if (trigger === 'auto' && !isPromptTooLong && tokenCount < threshold) {
            // Below threshold — go straight to done without compacting
            this._transition('done');
            this._transition('idle');
            logger.debug('[CompactionManager] below threshold; no compaction needed', {
                sessionId: this._sessionId,
                tokenCount,
                threshold,
            });
            return null;
        }
        // ── Select strategy ──────────────────────────────────────────────────────
        let strategy = selectStrategy({
            trigger,
            currentTokens: tokenCount,
            contextWindow: this._contextWindow,
            isPromptTooLong,
        });
        const strategyState = strategyToState(strategy);
        // ── Transition: checking_threshold → <strategy state> ────────────────────
        this._transition(strategyState);
        // ── Execute strategy ─────────────────────────────────────────────────────
        let strategyOutput;
        let qualityScore = null;
        let strategySwitchReason = null;
        const strategyInput = {
            sessionId: this._sessionId,
            messages,
            tokensBefore: tokenCount,
            contextWindow: this._contextWindow,
            strategy,
            meta: isPromptTooLong !== undefined ? { isPromptTooLong } : undefined,
        };
        try {
            strategyOutput = await this._runStrategy(strategy, strategyInput);
        }
        catch (err) {
            const error = summarizeError(err);
            this._transition('failed');
            emitCompactionFailed(this._bus, this._ctx, {
                sessionId: this._sessionId,
                strategy,
                error,
            });
            this._transition('idle');
            logger.error('[CompactionManager] strategy execution failed', {
                sessionId: this._sessionId,
                strategy,
                error,
            });
            return null;
        }
        // ── Score quality and auto-switch if low ─────────────────────────────────
        qualityScore = computeQualityScore(strategyInput, strategyOutput);
        emitCompactionQualityScore(this._bus, this._ctx, {
            sessionId: this._sessionId,
            strategy,
            score: qualityScore.score,
            grade: qualityScore.grade,
            compressionRatio: qualityScore.compressionRatio,
            retentionScore: qualityScore.retentionScore,
            isLowQuality: qualityScore.isLowQuality,
            description: qualityScore.description,
        });
        if (qualityScore.isLowQuality) {
            const escalated = escalateStrategy(strategy);
            if (escalated !== strategy) {
                const reason = `Quality score ${qualityScore.score.toFixed(2)} below threshold ${LOW_QUALITY_THRESHOLD}; escalating from ${strategy} to ${escalated}`;
                strategySwitchReason = reason;
                logger.warn('[CompactionManager] low quality score — switching strategy', {
                    sessionId: this._sessionId,
                    fromStrategy: strategy,
                    toStrategy: escalated,
                    score: qualityScore.score,
                    grade: qualityScore.grade,
                });
                emitCompactionStrategySwitch(this._bus, this._ctx, {
                    sessionId: this._sessionId,
                    fromStrategy: strategy,
                    toStrategy: escalated,
                    reason,
                    score: qualityScore.score,
                });
                // Force state to the escalated strategy state, bypassing normal transition
                // validation. This is intentional: quality-correction reruns are not modelled
                // in the standard state machine, and forcing the state allows _runStrategy to
                // emit the correct strategy event while keeping lifecycle state consistent.
                this._state = strategyToState(escalated);
                const escalatedInput = { ...strategyInput, strategy: escalated };
                try {
                    const escalatedOutput = await this._runStrategy(escalated, escalatedInput);
                    // Re-score the escalated result
                    qualityScore = computeQualityScore(escalatedInput, escalatedOutput);
                    strategyOutput = escalatedOutput;
                    strategy = escalated;
                }
                catch (err) {
                    const error = summarizeError(err);
                    logger.warn('[CompactionManager] escalated strategy also failed; using original output', {
                        sessionId: this._sessionId,
                        strategy: escalated,
                        error,
                    });
                    // Fall back to the original output — restore to the original strategy state
                    this._state = strategyToState(strategy);
                }
            }
            else {
                // Already at ceiling strategy (collapse or reactive) — log but continue
                logger.debug('[CompactionManager] low quality at ceiling strategy; no escalation possible', {
                    sessionId: this._sessionId,
                    strategy,
                    score: qualityScore.score,
                });
            }
        }
        // ── Transition: <strategy> → boundary_commit ─────────────────────────────
        this._transition('boundary_commit');
        // ── Create boundary commit ───────────────────────────────────────────────
        const commit = createBoundaryCommit({
            sessionId: this._sessionId,
            strategyOutput,
            parent: this._lastCommit,
            tokensBefore: tokenCount,
        });
        const commitErrors = validateBoundaryCommit(commit);
        if (commitErrors.length > 0) {
            const error = commitErrors.join('; ');
            this._transition('failed');
            emitCompactionFailed(this._bus, this._ctx, {
                sessionId: this._sessionId,
                strategy,
                error,
            });
            this._transition('idle');
            logger.error('[CompactionManager] boundary commit validation failed', {
                sessionId: this._sessionId,
                errors: commitErrors,
            });
            return null;
        }
        emitCompactionBoundaryCommit(this._bus, this._ctx, {
            sessionId: this._sessionId,
            checkpointId: commit.checkpointId,
        });
        this._lastCommit = commit;
        // ── Transition: boundary_commit → done ───────────────────────────────────
        this._transition('done');
        const durationMs = Date.now() - runStart;
        emitCompactionDone(this._bus, this._ctx, {
            sessionId: this._sessionId,
            strategy,
            tokensBefore: tokenCount,
            tokensAfter: strategyOutput.tokensAfter,
            durationMs,
        });
        // ── Transition: done → idle ───────────────────────────────────────────────
        this._transition('idle');
        return {
            sessionId: this._sessionId,
            strategy,
            tokensBefore: tokenCount,
            tokensAfter: strategyOutput.tokensAfter,
            durationMs,
            commit,
            messages: strategyOutput.messages,
            warnings: strategyOutput.warnings,
            qualityScore,
            strategySwitchReason,
        };
    }
    /**
     * Runs the session resume repair pipeline on the last boundary commit.
     *
     * If no commit exists, returns a result with the original messages and
     * a warning action.
     *
     * @param overrideCommit - Optional commit to repair (defaults to lastCommit).
     * @returns ResumeRepairResult.
     */
    repair(overrideCommit) {
        const commit = overrideCommit ?? this._lastCommit;
        if (!commit) {
            return {
                sessionId: this._sessionId,
                repaired: false,
                actions: [
                    {
                        kind: 'no_commit',
                        description: 'No boundary commit available; nothing to repair.',
                        severity: 'info',
                    },
                ],
                messages: [],
                safeToResume: false,
                failReason: 'No boundary commit available for repair.',
            };
        }
        return runResumeRepair({ commit });
    }
    // ── Private helpers ────────────────────────────────────────────────────────
    /**
     * Executes the selected strategy and emits the corresponding domain event.
     */
    async _runStrategy(strategy, input) {
        switch (strategy) {
            case 'microcompact': {
                const output = runMicrocompact(input);
                emitCompactionMicrocompact(this._bus, this._ctx, {
                    sessionId: this._sessionId,
                    turnCount: input.messages.length,
                    tokensBefore: input.tokensBefore,
                    tokensAfter: output.tokensAfter,
                });
                return output;
            }
            case 'collapse': {
                const output = runCollapse(input);
                emitCompactionCollapse(this._bus, this._ctx, {
                    sessionId: this._sessionId,
                    messageCount: input.messages.length,
                    tokensBefore: input.tokensBefore,
                    tokensAfter: output.tokensAfter,
                });
                return output;
            }
            case 'autocompact': {
                const output = runAutocompact(input);
                emitCompactionAutocompact(this._bus, this._ctx, {
                    sessionId: this._sessionId,
                    strategy: 'autocompact',
                    tokensBefore: input.tokensBefore,
                    tokensAfter: output.tokensAfter,
                });
                return output;
            }
            case 'reactive': {
                const output = runReactive(input);
                emitCompactionReactive(this._bus, this._ctx, {
                    sessionId: this._sessionId,
                    tokenCount: input.tokensBefore,
                    limit: this._contextWindow,
                });
                return output;
            }
            default: {
                const _exhaustive = strategy;
                throw new Error(`Unknown compaction strategy: ${_exhaustive}`);
            }
        }
    }
    /**
     * Applies a state transition, throwing if invalid.
     * Invalid transitions are logged and treated as internal errors.
     */
    _transition(target) {
        const result = applyTransition(this._state, target);
        if (!result.ok) {
            logger.error('[CompactionManager] invalid state transition', {
                from: this._state,
                to: target,
                reason: result.reason,
                sessionId: this._sessionId,
            });
            // Force to failed to avoid being stuck in an inconsistent state
            this._state = 'failed';
            return;
        }
        this._state = result.state;
    }
}
