package com.correx.core.events.events

import com.correx.core.events.execution.RetryPolicy
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@SerialName("WorkflowStarted")
data class WorkflowStartedEvent(
    val sessionId: SessionId,
    val workflowId: String,
    val startStageId: StageId,
    val retryPolicy: RetryPolicy? = null,
) : EventPayload

@Serializable
@SerialName("WorkflowCompleted")
data class WorkflowCompletedEvent(
    val sessionId: SessionId,
    val terminalStageId: StageId,
    val totalStages: Int,
    // The workflow that completed. Lets the session reducer tell a session-terminal completion from
    // the freestyle planning workflow, which completes mid-session before handing off to execution.
    // Defaulted for backward-compatible replay of events stored before this field existed.
    val workflowId: String = "",
) : EventPayload

@Serializable
@SerialName("WorkflowFailed")
data class WorkflowFailedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val reason: String,
    val retryExhausted: Boolean,
) : EventPayload

/**
 * Observation recorded only after the optional server Git transport has pushed a terminal run
 * branch. This keeps the reviewable branch reference replayable without re-running Git.
 */
@Serializable
@SerialName("RunBranchPushed")
data class RunBranchPushedEvent(
    val sessionId: SessionId,
    val branch: String,
    val baseSha: String,
    val headSha: String,
) : EventPayload

/**
 * Records that the operator approved access to a specific path OUTSIDE the workspace root
 * (a `file_read`/`list_dir` target). The intent plane raises PROMPT_USER for any out-of-workspace
 * read; on approval this event is recorded so (a) subsequent reads of the same path this session
 * skip the prompt, and (b) the tool jail is widened to admit it. [path] is the lexically-resolved
 * absolute path — the recorded fact replay reads instead of re-prompting (invariants #8/#9).
 * Scope is the session: nothing here persists across sessions. Privileged locations are BLOCKED
 * upstream and never reach this event.
 */
@Serializable
@SerialName("OutsidePathAccessGranted")
data class OutsidePathAccessGrantedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val path: String,
) : EventPayload

/**
 * Records that the operator approved widening the write scope/manifest to admit [path], after
 * the same path was rejected `escalate_scope_after_n` times in a row (small models frequently
 * fail to comply with the WRITE_SCOPE/PATH_OUTSIDE_MANIFEST remediation and thrash instead —
 * see #301). Folded the same way [OutsidePathAccessGrantedEvent] widens out-of-workspace reads:
 * subsequent writes to this path this session are admitted without re-prompting.
 */
@Serializable
@SerialName("WriteScopeGranted")
data class WriteScopeGrantedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val path: String,
) : EventPayload

@Serializable
@SerialName("OrchestrationPaused")
data class OrchestrationPausedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val reason: String,   // e.g. "APPROVAL_PENDING", "USER_REQUESTED"
) : EventPayload

@Serializable
@SerialName("OrchestrationResumed")
data class OrchestrationResumedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
) : EventPayload

@Serializable
@SerialName("RetryAttempted")
data class RetryAttemptedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val attemptNumber: Int,
    val maxAttempts: Int,
    val failureReason: String,
    // Per-gate retry budgets (design 2026-07-06-per-gate-retry-budgets.md): budget key this attempt
    // is charged against. Defaulted to the legacy shared "stage" bucket for schema back-compat.
    val gate: String = "stage",
    // Stable hash of the failure's normalised reason, used to detect progress across attempts.
    val fingerprint: String = "",
    // Whether this attempt consumed the gate's budget (false when the fingerprint changed from the
    // previous attempt — i.e. the run is moving, so the retry is free).
    val charged: Boolean = true,
) : EventPayload

/**
 * Outcome of the hybrid-exhaustion salvage judgement for the semantic review gate.
 * CONTINUE resets the gate budget and retries the reviewed stage in place; FAIL is terminal;
 * RECOVER hands the failure to the recovery stage (same destination as the deterministic-gate
 * agency guard) — safe under invariant #7 because recovery output re-enters verification and so
 * cannot corrupt state, it only chooses which bounded remediation to spend budget on.
 */
@Serializable
enum class SalvageDecision { CONTINUE, FAIL, RECOVER }

/**
 * A verification gate failed on a stage that lacks the capability to change the failure condition
 * (the retry-agency invariant: recovery/ticket model). Rather than retry the same write-less stage
 * in place — which is futile by construction (live repro: freestyle `final_verification` compiled
 * with `allowedTools=[shell]` re-ran the identical broken `npm run build` until the budget drained,
 * never able to touch a file) — the orchestrator opens a failure ticket and routes to a recovery
 * stage that DOES hold the capability. [category] and [requiredCapability] are derived
 * deterministically from [gate] (no LLM), so this drives routing without violating invariant #7.
 * [evidence] is the gate's captured failure output; it is recorded here at ticket-open time and
 * replay reads it back (invariant #9) — the gate command is never re-run on replay.
 */
@Serializable
@SerialName("FailureTicketOpened")
data class FailureTicketOpenedEvent(
    val sessionId: SessionId,
    // The stage that failed the gate but lacks the capability to fix it (the write-less verifier).
    val stageId: StageId,
    val gate: String,
    // Deterministic from [gate]: "implementation" | "planning" | "environment".
    val category: String,
    // The capability the failing stage lacks (e.g. "file_write"); the recovery stage must hold it.
    val requiredCapability: String,
    // The recovery stage the ticket is routed to.
    val routeTo: StageId,
    // The gate's captured failure output (command/exit/stderr/findings) that recovery must resolve.
    val evidence: String,
    // Count of NO-PROGRESS routes charged against this stage's recovery budget so far. A route whose
    // failure fingerprint changed from the previous route (recovery made progress — a different error
    // now) is FREE and leaves this flat; only a route that reproduces the same failure charges it. So
    // a multi-cause failure repaired one layer at a time is not penalised for progressing. Folded
    // directly by the reducer (idempotent under replay). Defaulted for backward-compatible replay of
    // pre-progress-aware tickets.
    val routeAttempt: Int,
    // Fingerprint of [evidence] (whitespace/digit-normalised, see FailureFingerprint) at route time.
    // The next route compares against this to decide progress vs. no-progress. Empty on old events.
    val fingerprint: String = "",
    // Tier-2 escalation marker. False = tier-1, the ticket routed to the file's own author to patch its
    // own layer. True = the author loop exhausted its budget, so the ticket escalated to the arbiter
    // (the recovery stage recast as intent-holder): the failure is a cross-file CONTRACT dispute no
    // single owner can settle, so the arbiter is given the initial intent as authority and told to
    // reconcile ALL the named files in one pass. Defaulted for backward-compatible replay.
    val escalated: Boolean = false,
) : EventPayload

/**
 * Recorded when a gate exhausts its retry budget and (per design decision 3) the salvage judge is
 * consulted — currently only the "review" gate. The judgement is nondeterministic (LLM-backed, or a
 * deterministic fallback when no judge is wired), so invariant #9 requires it be recorded here;
 * replay reads this event back and never re-invokes the judge.
 */
@Serializable
@SerialName("RetrySalvageDecided")
data class RetrySalvageDecidedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val gate: String,
    val decision: SalvageDecision,
    val rationale: String,
) : EventPayload

/**
 * A structured, untrusted recovery proposal produced by the one-shot post-failure diagnostic
 * (design task #294). The diagnostic inference is tool-free and reads only recorded facts; this is
 * its proposal. [expectedFingerprint] is the failure fingerprint the proposed [recoveryAction] is
 * predicted to change the run to — the kernel routes only when it is materially different from the
 * current terminal fingerprint (i.e. a genuinely new path, not the same dead end). [noRecovery]
 * lets the model explicitly decline; [confidence] is thresholded by the kernel. LLM-proposed and
 * therefore untrusted (invariant #7): validated deterministically before it can affect routing.
 */
@Serializable
data class RecoveryProposal(
    val diagnosis: String,
    val citedEvidence: String,
    val recoveryAction: String,
    val expectedFingerprint: String,
    val confidence: Double,
    val noRecovery: Boolean = false,
)

/**
 * Records the one-shot post-failure diagnostic (design task #294): when a run is about to become
 * terminal, exactly one tool-free diagnostic inference runs per terminal-failure [fingerprint]. The
 * nondeterministic [proposal] (LLM-backed, null when none/unparseable), the kernel's deterministic
 * validation [decision], and whether it [routed] into the existing recovery stage are all recorded
 * here so replay reproduces the decision without re-invoking the diagnoser (invariants #7/#9). The
 * per-fingerprint dedupe that bounds this to one attempt keys off this event.
 */
@Serializable
@SerialName("PostFailureDiagnosed")
data class PostFailureDiagnosedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    val gate: String,
    val fingerprint: String,
    val proposal: RecoveryProposal?,
    // ROUTE | TERMINAL_NO_PROPOSAL | TERMINAL_NO_RECOVERY | TERMINAL_LOW_CONFIDENCE |
    // TERMINAL_NOT_MATERIAL | TERMINAL_NO_ROUTE
    val decision: String,
    val routed: Boolean,
) : EventPayload

/**
 * A stage repeatedly blocked on a missing build prerequisite (see repeatedBuildCriticalReferenceBlock,
 * design 2026-07-15 §#170) AND the stage both holds file_write and declares the path in-scope, so the
 * orchestrator grants it ONE dedicated bootstrap turn to create/repair that prerequisite — separately
 * budgeted, NOT charged against the normal stage retry counter. This marker records the grant: it caps
 * the bootstrap at one attempt per stage (a second block escalates to recovery) and scopes the
 * REFERENCE_EXISTS block window so historical blocks from before the grant don't re-trip the gate on
 * the fresh turn. Recorded (invariant #9) so replay reproduces the same bounded bootstrap.
 */
@Serializable
@SerialName("BuildPrerequisiteBootstrapAttempted")
data class BuildPrerequisiteBootstrapAttemptedEvent(
    val sessionId: SessionId,
    val stageId: StageId,
    // The build-critical workspace-relative path the stage is granted a turn to create/repair.
    val path: String,
    // The gate evidence (repeatedBuildCriticalReferenceBlock reason) that triggered the grant.
    val evidence: String,
) : EventPayload
