//! [`FsHandoffStore`] — l'adapter `tokio::fs` du port [`HandoffStore`]
//! (cadrage « persistance conversationnelle », lot P3).
//!
//! Le **point de reprise** d'une conversation (ARCHITECTURE §19.2/§19.3) est un
//! [`Handoff`] : un résumé cumulatif Markdown borné par un curseur [`TurnId`]. On en
//! garde **un seul** par conversation (le dernier), dans un fichier lisible à l'œil :
//!
//! ```text
//! <project_root>/.ideai/conversations/
//! └── <conversationId>/
//!     ├── log.jsonl     # le log append-only (lot P2)
//!     └── handoff.md    # le dernier point de reprise (ce module)
//! ```
//!
//! ## Format `handoff.md`
//!
//! Un **front-matter** YAML délimité par `---`, suivi du corps `summary_md` tel quel :
//!
//! ```text
//! ---
//! upTo: 00000000-0000-0000-0000-00000000002a
//! objective: livrer le lot P3
//! ---
//! # Résumé
//! …le summary_md, octet pour octet…
//! ```
//!
//! Le front-matter porte le curseur `upTo` (toujours) et `objective` (seulement s'il
//! est `Some`). Le corps après le second `---\n` est le `summary_md` **exact** : le
//! round-trip (`save` puis `load`) redonne le même [`Handoff`]. `objective` étant
//! sérialisé sur une seule ligne, un objectif est interdit de retour-chariot ici ; en
//! pratique c'est une phrase courte (un titre de but), jamais du multi-ligne.
//!
//! ## Robustesse
//!
//! - **Écriture atomique** : on écrit dans `handoff.md.tmp` puis on `rename` — jamais
//!   de fichier à moitié écrit visible.
//! - **Fichier absent** ⇒ `Ok(None)` (jamais une erreur) : une conversation sans
//!   reprise est l'état normal au premier tour.
//! - **Fichier présent mais illisible** (front-matter absent/incomplet, `upTo`
//!   manquant ou non-UUID) ⇒ [`StoreError::Serialization`]. Contrairement au log
//!   JSONL (où une ligne corrompue est sautée), un handoff corrompu est une vraie
//!   erreur : il n'y a qu'un enregistrement, on ne peut pas « sauter ».

use std::path::PathBuf;

use async_trait::async_trait;

use domain::conversation::ConversationId;
use domain::conversation_log::{Handoff, HandoffStore, TurnId};
use domain::ports::StoreError;
use domain::project::ProjectPath;

use super::{CONVERSATIONS_DIR, IDEAI_DIR};

/// Nom du fichier de handoff, par conversation.
const HANDOFF_FILE: &str = "handoff.md";

/// Nom du fichier temporaire d'écriture atomique (renommé sur `handoff.md`).
const HANDOFF_TMP_FILE: &str = "handoff.md.tmp";

/// Délimiteur de front-matter (en tête et fin de l'entête).
const FRONT_MATTER_FENCE: &str = "---";

/// Adapter `tokio::fs` du store de handoff, un `handoff.md` par conversation.
///
/// Même convention de construction que [`super::FsConversationLog`] : le **project
/// root** est fourni au constructeur, la base `<root>/.ideai/conversations` en dérive,
/// et chaque conversation a son sous-dossier `<conversationId>/`.
pub struct FsHandoffStore {
    /// Racine `<project_root>/.ideai/conversations`.
    base: PathBuf,
}

impl FsHandoffStore {
    /// Construit l'adapter à partir du **project root**.
    ///
    /// La base `<root>/.ideai/conversations` en est dérivée ; le dossier de
    /// conversation est créé paresseusement au premier `save`.
    #[must_use]
    pub fn new(root: &ProjectPath) -> Self {
        let base = PathBuf::from(root.as_str())
            .join(IDEAI_DIR)
            .join(CONVERSATIONS_DIR);
        Self { base }
    }

    /// `<base>/<conversationId>` — le dossier d'une conversation.
    fn conversation_dir(&self, conversation: ConversationId) -> PathBuf {
        self.base.join(conversation.to_string())
    }

    /// `<base>/<conversationId>/handoff.md` — le fichier de handoff d'une conversation.
    fn handoff_path(&self, conversation: ConversationId) -> PathBuf {
        self.conversation_dir(conversation).join(HANDOFF_FILE)
    }

    /// `<base>/<conversationId>/handoff.md.tmp` — le fichier temporaire d'écriture.
    fn handoff_tmp_path(&self, conversation: ConversationId) -> PathBuf {
        self.conversation_dir(conversation).join(HANDOFF_TMP_FILE)
    }
}

/// Sérialise un [`Handoff`] au format `handoff.md` (front-matter + corps exact).
fn serialize(handoff: &Handoff) -> String {
    let mut out = String::new();
    out.push_str(FRONT_MATTER_FENCE);
    out.push('\n');
    out.push_str(&format!("upTo: {}\n", handoff.up_to));
    if let Some(objective) = &handoff.objective {
        out.push_str(&format!("objective: {objective}\n"));
    }
    out.push_str(FRONT_MATTER_FENCE);
    out.push('\n');
    // Corps : le summary_md tel quel, octet pour octet.
    out.push_str(&handoff.summary_md);
    out
}

/// Parse le contenu d'un `handoff.md` en [`Handoff`].
///
/// Format attendu : `---\n<clé: valeur>*\n---\n<summary_md>`. Le corps après le second
/// `---\n` est rendu **exact**. Toute déviation (front-matter absent/incomplet, `upTo`
/// manquant ou non-UUID) ⇒ `Err(StoreError::Serialization)`.
fn deserialize(content: &str) -> Result<Handoff, StoreError> {
    let bad = |msg: &str| StoreError::Serialization(format!("handoff.md: {msg}"));

    // Entête : `---\n` exactement en tête.
    let after_open = content
        .strip_prefix(FRONT_MATTER_FENCE)
        .and_then(|rest| rest.strip_prefix('\n'))
        .ok_or_else(|| bad("front-matter ouvrant `---` absent"))?;

    // Fin de l'entête : la première ligne `---\n` (ou `---` en toute fin).
    // On sépare les lignes du front-matter du corps.
    let mut up_to: Option<TurnId> = None;
    let mut objective: Option<String> = None;

    // Trouver le `---` de fermeture, ligne par ligne.
    let mut rest = after_open;
    loop {
        // Découpe la prochaine ligne (jusqu'au `\n` inclus si présent).
        let (line, tail) = match rest.find('\n') {
            Some(idx) => (&rest[..idx], &rest[idx + 1..]),
            None => (rest, ""),
        };

        if line == FRONT_MATTER_FENCE {
            // Fermeture trouvée : `tail` est le corps exact.
            let up_to = up_to.ok_or_else(|| bad("clé `upTo` absente du front-matter"))?;
            return Ok(Handoff {
                summary_md: tail.to_string(),
                up_to,
                objective,
            });
        }

        // Une ligne de clé: valeur dans le front-matter.
        let (key, value) = line
            .split_once(':')
            .ok_or_else(|| bad("ligne de front-matter sans `:`"))?;
        let value = value.trim();
        match key.trim() {
            "upTo" => {
                let uuid = uuid::Uuid::parse_str(value)
                    .map_err(|_| bad("`upTo` n'est pas un UUID valide"))?;
                up_to = Some(TurnId::from_uuid(uuid));
            }
            "objective" => objective = Some(value.to_string()),
            // Clé inconnue : tolérée (extensible), ignorée.
            _ => {}
        }

        if tail.is_empty() {
            // Plus de lignes et toujours pas de `---` fermant.
            return Err(bad("front-matter fermant `---` absent"));
        }
        rest = tail;
    }
}

#[async_trait]
impl HandoffStore for FsHandoffStore {
    async fn load(&self, conversation: ConversationId) -> Result<Option<Handoff>, StoreError> {
        let content = match tokio::fs::read_to_string(self.handoff_path(conversation)).await {
            Ok(content) => content,
            // Absent ⇒ pas de reprise (jamais une erreur).
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(e) => return Err(StoreError::Io(e.to_string())),
        };
        deserialize(&content).map(Some)
    }

    async fn save(&self, conversation: ConversationId, handoff: Handoff) -> Result<(), StoreError> {
        let body = serialize(&handoff);

        let dir = self.conversation_dir(conversation);
        tokio::fs::create_dir_all(&dir)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;

        // Écriture atomique : écrire le tmp puis `rename` sur la cible. Un lecteur ne
        // voit jamais de fichier à moitié écrit (le rename est atomique sur le FS).
        let tmp = self.handoff_tmp_path(conversation);
        tokio::fs::write(&tmp, body.as_bytes())
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        tokio::fs::rename(&tmp, self.handoff_path(conversation))
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        Ok(())
    }
}
