//! [`ReadConversationPage`] — lecture **humaine** paginée d'une conversation (lot LS6).
//!
//! Lecture archive-aware (segments d'archive + actif) exposée comme un **DTO humain
//! riche** : contrairement au handoff (résumé borné) ou au read-model work-state
//! (aperçus tronqués), la page humaine porte le **texte complet, non borné** de chaque
//! tour — c'est la vue « transcript » destinée à l'utilisateur (le React arrive en LS7).
//!
//! Hors chemin chaud : composé du seul port [`ConversationArchive`] (via un provider par
//! root), jamais d'`append`/de rotation ici.

use std::sync::Arc;

use domain::project::ProjectPath;
use domain::{AgentId, ConversationArchive, ConversationId, PageCursor, TurnId, TurnRole};

use crate::error::AppError;

/// Fournit le [`ConversationArchive`] **lié au project root** courant (lot LS6),
/// calqué sur [`crate::HandoffProvider`]/[`crate::RecordTurnProvider`].
///
/// Les use cases d'archivage/pagination ([`crate::RotateConversationLog`],
/// [`ReadConversationPage`]) sont **uniques** pour tous les projets, alors que les logs
/// sont **par project root** (`<root>/.ideai/conversations/`) et l'adapter `Fs*` fixe sa
/// racine à la construction. Ce port matérialise un archive ciblant le **bon** dossier à
/// chaque appel. `None` ⇒ pas d'archivage/pagination (best-effort absent). Implémenté
/// dans `app-tauri` (seul détenteur des adapters `Fs*`).
pub trait ConversationArchiveProvider: Send + Sync {
    /// Construit le [`ConversationArchive`] dont la persistance cible `root`, ou `None`.
    fn conversation_archive_for(&self, root: &ProjectPath) -> Option<Arc<dyn ConversationArchive>>;
}

/// Origine d'un tour, pour la vue humaine paginée (miroir de [`domain::input::InputSource`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TurnSource {
    /// L'opérateur humain.
    Human,
    /// Un autre agent (délégation via `idea_ask_agent`).
    Agent {
        /// L'agent à l'origine du tour.
        agent_id: AgentId,
    },
}

impl From<domain::input::InputSource> for TurnSource {
    fn from(source: domain::input::InputSource) -> Self {
        match source {
            domain::input::InputSource::Human => Self::Human,
            domain::input::InputSource::Agent { agent_id } => Self::Agent { agent_id },
        }
    }
}

/// Un tour projeté pour la vue humaine (lot LS6) — **texte complet, non borné**.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnView {
    /// Identifiant stable du tour (sert aussi d'ancre de pagination).
    pub id: TurnId,
    /// Horodatage (epoch millisecondes).
    pub at_ms: u64,
    /// Nature du tour (invite, réponse, activité outillée).
    pub role: TurnRole,
    /// Origine (humain ou agent délégant).
    pub source: TurnSource,
    /// Le **texte intégral** du tour (jamais tronqué — c'est la vue transcript).
    pub text: String,
    /// Longueur (en caractères) du texte, pour l'UI.
    pub text_len: usize,
}

impl From<domain::ConversationTurn> for TurnView {
    fn from(turn: domain::ConversationTurn) -> Self {
        let text_len = turn.text.chars().count();
        Self {
            id: turn.id,
            at_ms: turn.at_ms,
            role: turn.role,
            source: turn.source.into(),
            text: turn.text,
            text_len,
        }
    }
}

/// Une page de tours pour la vue humaine (lot LS6).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnPage {
    /// Les tours de la page, **du plus ancien au plus récent**.
    pub turns: Vec<TurnView>,
    /// `true` s'il existe d'autres tours au-delà de la page dans le sens de progression.
    pub has_more: bool,
    /// L'id du **dernier** tour de la page (ancre pour la requête suivante) ; `None` si
    /// la page est vide.
    pub next_anchor: Option<TurnId>,
}

/// Entrée de [`ReadConversationPage::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadConversationPageInput {
    /// Le project root dont dériver l'archive.
    pub project_root: ProjectPath,
    /// La conversation (paire) à lire.
    pub conversation: ConversationId,
    /// Le curseur (ancre + sens) de pagination.
    pub cursor: PageCursor,
    /// La taille de page demandée (`0` ⇒ défaut ; clampée par le domaine).
    pub limit: usize,
}

/// Use case de lecture humaine paginée (lot LS6) — consomme le seul
/// [`ConversationArchive`] (résolu par root via le provider).
pub struct ReadConversationPage {
    archives: Arc<dyn ConversationArchiveProvider>,
}

impl ReadConversationPage {
    /// Construit le use case à partir du provider d'archive par root.
    #[must_use]
    pub fn new(archives: Arc<dyn ConversationArchiveProvider>) -> Self {
        Self { archives }
    }

    /// Lit une page de `conversation`, mappée vers le DTO humain riche.
    ///
    /// Provider d'archive absent pour ce root ⇒ page **vide** (best-effort, jamais une
    /// erreur dure). Le texte de chaque tour est renvoyé **intégralement**.
    ///
    /// # Errors
    /// [`AppError::Store`] si la lecture du log échoue.
    pub async fn execute(&self, input: ReadConversationPageInput) -> Result<TurnPage, AppError> {
        let Some(archive) = self.archives.conversation_archive_for(&input.project_root) else {
            return Ok(TurnPage {
                turns: Vec::new(),
                has_more: false,
                next_anchor: None,
            });
        };
        let slice = archive
            .page(input.conversation, input.cursor, input.limit)
            .await?;
        let next_anchor = slice.turns.last().map(|t| t.id);
        let turns = slice.turns.into_iter().map(TurnView::from).collect();
        Ok(TurnPage {
            turns,
            has_more: slice.has_more,
            next_anchor,
        })
    }
}
