//! [`RecordTurn`] — the end-of-turn checkpoint use case (ARCHITECTURE §19, lot P6a).

use std::sync::Arc;

use domain::{
    bound_handoff_summary, ConversationId, ConversationLog, ConversationTurn, HandoffStore,
    HandoffSummarizer, HANDOFF_SUMMARY_MAX_CHARS,
};

use crate::error::AppError;

/// Records one conversation turn at a checkpoint: appends it to the canonical
/// log, then advances the incremental handoff (ARCHITECTURE §19, décision D19-5).
///
/// **Single Responsibility**: materialise *one* end-of-turn checkpoint. It is the
/// only place that couples the append-only log (source of truth, [`ConversationLog`])
/// with the cumulative resume summary ([`HandoffStore`]) via the incremental
/// [`HandoffSummarizer`]. It consumes **exactly** those three ports (Interface
/// Segregation), injected as `Arc<dyn …>` at the composition root.
///
/// ## Incremental by contract (no full re-read)
///
/// Each call folds **only the new turn** into the previous handoff
/// (`fold(prev, &[turn])`), never re-reading the whole thread — exactly the seam
/// [`HandoffSummarizer`] was designed for (§19, lot P4). The resulting handoff's
/// [`Handoff::up_to`](domain::Handoff::up_to) is the recorded turn's id (guaranteed
/// by the summarizer, P4).
///
/// ## Best-effort fold, honest `Result`
///
/// The fold itself cannot fail (it returns no `Result`, P4 — a future LLM
/// summarizer falls back rather than erroring), so the only failures are the
/// store operations (`append` / `load` / `save`), which propagate as [`AppError`].
/// This use case stays **honest** and surfaces them; the live wiring (P6b) decides
/// whether to swallow a handoff failure rather than block the turn.
///
/// ## No debounce here (deferred)
///
/// One `append` + one `save` per recorded turn. The debounce optimisation (coalesce
/// several rapid turns into a single handoff recompute) is intentionally **deferred**
/// — it belongs to the live wiring (P6b), not to this isolated logic.
pub struct RecordTurn {
    log: Arc<dyn ConversationLog>,
    handoffs: Arc<dyn HandoffStore>,
    summarizer: Arc<dyn HandoffSummarizer>,
}

impl RecordTurn {
    /// Builds the use case from its three injected ports.
    #[must_use]
    pub fn new(
        log: Arc<dyn ConversationLog>,
        handoffs: Arc<dyn HandoffStore>,
        summarizer: Arc<dyn HandoffSummarizer>,
    ) -> Self {
        Self {
            log,
            handoffs,
            summarizer,
        }
    }

    /// Records `turn` in `conversation` at an end-of-turn checkpoint.
    ///
    /// Steps (ordering is contractual):
    /// 1. **append** the turn to the canonical log (source of truth) ;
    /// 2. **load** the previous handoff (`None` on a first checkpoint) ;
    /// 3. **fold** `prev` with the single new turn (incremental, best-effort) ;
    /// 3b. **bound** the folded handoff's summary to [`HANDOFF_SUMMARY_MAX_CHARS`] — a
    ///     **universal cap independent of the summarizer** (heuristic *or* a future LLM):
    ///     idempotent, never rejecting, so it never blocks the checkpoint (LS5) ;
    /// 4. **save** the advanced handoff.
    ///
    /// The append happens **first** so the durable source of truth is never behind
    /// the derived handoff. The turn is cloned for the append because the fold (step
    /// 3) borrows it as the incremental slice.
    ///
    /// # Errors
    /// [`AppError::Store`] (or [`AppError::NotFound`] for a missing store item) when
    /// the canonical log append, the handoff load, or the handoff save fails — every
    /// [`domain::ports::StoreError`] is mapped through the existing `From` impl. The
    /// fold never fails (P4).
    pub async fn record(
        &self,
        conversation: ConversationId,
        turn: ConversationTurn,
    ) -> Result<(), AppError> {
        // 1. Append to the canonical append-only log (source of truth, D19-1a).
        self.log.append(conversation, turn.clone()).await?;

        // 2. Load the previous resume point (absence is never an error, P3).
        let prev = self.handoffs.load(conversation).await?;

        // 3. Fold the previous handoff with *only* the new turn (incremental, P4):
        //    no full re-read of the log; best-effort, so no `Result` to propagate.
        let handoff = self
            .summarizer
            .fold(prev, std::slice::from_ref(&turn))
            .await;

        // 3b. **Universal bound** (LS5): clamp the resume summary to
        //     `HANDOFF_SUMMARY_MAX_CHARS`, *independently of the summarizer*. Whatever
        //     produced the handoff — the heuristic or a future LLM summarizer — the
        //     persisted (and later injected) summary is size-bounded. Idempotent and
        //     never rejecting, so it can never block the checkpoint.
        let handoff = bound_handoff_summary(handoff, HANDOFF_SUMMARY_MAX_CHARS);

        // 4. Persist the advanced handoff (overwrites the previous resume point).
        self.handoffs.save(conversation, handoff).await?;

        Ok(())
    }
}
