//! Durable continuation state for a suspended run (ADR-045).
//!
//! Pure data depending only on `ids` + `llm`, so both the `error` layer
//! (`Error::Suspended` carries a `RunCheckpoint`) and the `runtime` layer (which
//! builds and replays it) can name these types without a module cycle. The
//! persist + resume behaviour lives in [`crate::runtime::resume_from_checkpoint`].

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use crate::ids::{RunId, ThreadId};
use crate::llm::{Message, ToolCall};

/// Bucket under which suspended-run checkpoints persist in `KvStore`.
pub const CHECKPOINT_BUCKET: &str = "klieo.run-checkpoints";

/// Minimal state required to resume a suspended run. Excludes the `Agent`
/// (its `Arc<dyn _>` ports are not serializable) — only the conversation and
/// the tool calls awaiting approval are captured.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RunCheckpoint {
    /// Stable across resume; doubles as the `KvStore` key for this checkpoint.
    pub run_id: RunId,
    /// 1-indexed step the resume re-enters the loop at.
    pub step_index: u32,
    /// Thread whose short-term history is cleared and restored from `messages`.
    pub thread_id: ThreadId,
    /// Short-term history captured at suspend time, oldest first.
    pub messages: Vec<Message>,
    /// Tool calls held back for approval; `Some` only when the paused step's
    /// finish reason was `ToolCalls`.
    pub pending_tool_calls: Option<Vec<ToolCall>>,
    /// Latched `true` by the first resume before it dispatches the pending
    /// calls, so a later (retried, cross-process) resume can tell it is not the
    /// first attempt. A retry that finds this set refuses to re-dispatch a
    /// non-idempotent pending call rather than risk a duplicate side effect
    /// (ADR-045 fail-closed). `#[serde(default)]` keeps pre-existing persisted
    /// checkpoints (no field) readable — they default to `false`, the
    /// first-attempt state.
    #[serde(default)]
    pub resume_attempted: bool,
    /// Suspend timestamp, for TTL/GC of abandoned checkpoints.
    pub created_at: DateTime<Utc>,
}

impl RunCheckpoint {
    /// Test-only fixture.
    #[cfg(test)]
    pub(crate) fn for_test() -> Self {
        Self {
            run_id: RunId::new(),
            step_index: 1,
            thread_id: ThreadId::new("t-test"),
            messages: vec![],
            pending_tool_calls: None,
            resume_attempted: false,
            created_at: Utc::now(),
        }
    }
}

/// Operator decision delivered to `resume_from_checkpoint`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ApprovalDecision {
    /// The human reviewer approved the paused step.
    Approved,
    /// The human reviewer refused the paused step.
    Rejected {
        /// Reason surfaced to the model in the injected tool message.
        reason: String,
    },
}
