package com.correx.core.kernel.retry

import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.orchestration.OrchestrationState
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId

/** Outcome of [RetryCoordinator.decide] for a single gate's failure. */
sealed interface RetryDecision {
    data object Retry : RetryDecision
    data object Exhausted : RetryDecision
}

interface RetryCoordinator {
    /**
     * Legacy shared-budget entry point, kept for the non-gate transition-failure paths
     * (Stay/NoMatch in DefaultSessionOrchestrator.step) that predate per-gate budgeting.
     */
    suspend fun shouldRetry(
        sessionId: SessionId,
        stageId: StageId,
        currentAttempt: Int,
        policy: RetryPolicy,
        failureReason: String,
    ): Boolean

    /**
     * Per-gate, progress-aware retry decision (design 2026-07-06-per-gate-retry-budgets.md §5).
     * [failureReason] is fingerprinted internally (see [FailureFingerprint]) and charges [gate]'s
     * budget only when the fingerprint is unchanged from the last attempt recorded in [state] for
     * that gate (no progress); a changed fingerprint is free. On a charging decision that would
     * exceed the gate's budget, returns [RetryDecision.Exhausted] without emitting an event — the
     * caller (hybrid exhaustion, T6) decides what happens next. Otherwise emits
     * [com.correx.core.events.events.RetryAttemptedEvent] (with the fingerprint and the full
     * [failureReason] both recorded), applies backoff, and returns [RetryDecision.Retry].
     */
    suspend fun decide(
        sessionId: SessionId,
        stageId: StageId,
        gate: String,
        failureReason: String,
        state: OrchestrationState,
        policy: RetryPolicy,
    ): RetryDecision
}
