//! [`RotateConversationLog`] — rotation **best-effort** du log d'une conversation (lot LS6).
//!
//! Déclenchée **hors chemin chaud** (à la reprise/ouverture d'un fil, jamais par un
//! `append`), elle borne le segment actif en archivant sa tête froide. L'invariant pivot
//! (INV-LS6) est garanti par le **plancher** : `keep_from = up_to` du handoff courant ⇒
//! aucun tour `≥ up_to` n'est jamais archivé, donc `read(since = up_to)` (fold incrémental)
//! et la reprise restent corrects.
//!
//! Best-effort strict : pas de handoff (ou `up_to` nil) ⇒ skip ; toute erreur store est
//! typée et **avalée par l'appelant** (le câblage app-tauri), jamais propagée en échec de
//! reprise.

use std::sync::Arc;

use domain::project::ProjectPath;
use domain::{rotation_plan, ConversationId, RotationDecision, RotationThresholds};

use crate::conversation::ConversationArchiveProvider;
use crate::error::AppError;
use crate::HandoffProvider;

/// Entrée de [`RotateConversationLog::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RotateConversationLogInput {
    /// Le project root dont dériver l'archive et le handoff.
    pub project_root: ProjectPath,
    /// La conversation (paire) à roter.
    pub conversation: ConversationId,
}

/// Use case de rotation du log (lot LS6) — consomme un [`ConversationArchive`] et le
/// [`HandoffStore`] (pour lire `floor = up_to`), tous deux résolus par root.
///
/// [`ConversationArchive`]: domain::ConversationArchive
/// [`HandoffStore`]: domain::HandoffStore
pub struct RotateConversationLog {
    archives: Arc<dyn ConversationArchiveProvider>,
    handoffs: Arc<dyn HandoffProvider>,
    thresholds: RotationThresholds,
}

impl RotateConversationLog {
    /// Construit le use case à partir des providers par root (seuils par défaut).
    #[must_use]
    pub fn new(
        archives: Arc<dyn ConversationArchiveProvider>,
        handoffs: Arc<dyn HandoffProvider>,
    ) -> Self {
        Self {
            archives,
            handoffs,
            thresholds: RotationThresholds::defaults(),
        }
    }

    /// Applique la politique de rotation à `conversation`, si nécessaire.
    ///
    /// Étapes : résoudre l'archive + le handoff store du root ; lire le plancher
    /// `floor = up_to` (pas de handoff ⇒ `None` ⇒ skip) ; lire les `stats` bon marché du
    /// segment actif ; décider via [`rotation_plan`] ; archiver
    /// (`rotate(keep_from = up_to)`) seulement si la décision est `Archive`.
    ///
    /// # Errors
    /// [`AppError::Store`] si la lecture du handoff/stats ou la rotation échoue. L'appelant
    /// (câblage) **avale** cette erreur : la rotation ne doit jamais casser une reprise.
    pub async fn execute(&self, input: RotateConversationLogInput) -> Result<(), AppError> {
        // Sans provider d'archive/handoff câblé pour ce root ⇒ rien à faire (best-effort).
        let Some(archive) = self.archives.conversation_archive_for(&input.project_root) else {
            return Ok(());
        };
        let Some(handoff_store) = self.handoffs.handoff_store_for(&input.project_root) else {
            return Ok(());
        };

        // Plancher = `up_to` du handoff courant (pas de handoff ⇒ None ⇒ skip plus bas).
        let floor = handoff_store
            .load(input.conversation)
            .await?
            .map(|handoff| handoff.up_to);

        let stats = archive.stats(input.conversation).await?;
        match rotation_plan(
            stats.active_turns,
            stats.active_bytes,
            floor,
            self.thresholds,
        ) {
            RotationDecision::Skip => Ok(()),
            RotationDecision::Archive { keep_from } => archive
                .rotate(input.conversation, keep_from)
                .await
                .map_err(AppError::from),
        }
    }
}
