//! [`FsLiveStateStore`] — l'adapter `tokio::fs` du port [`LiveStateStore`]
//! (programme live-state, lot LS2).
//!
//! Le **live-state** est la projection volatile « qui fait quoi en ce moment » :
//! une entrée [`LiveEntry`](domain::live_state::LiveEntry) par agent, sémantique
//! *keyed last-writer-wins* (jamais un journal). On en garde **un seul** fichier
//! par projet, à la racine du `.ideai/` :
//!
//! ```text
//! <project_root>/.ideai/live-state.json
//! ```
//!
//! Le format est le JSON camelCase de [`LiveState`](domain::live_state::LiveState)
//! (dérivé serde du domaine), lisible à l'œil.
//!
//! ## Robustesse
//!
//! - **Écriture atomique** : on écrit dans `live-state.json.tmp` puis on `rename`
//!   — un lecteur ne voit jamais de fichier à moitié écrit (le `rename` est
//!   atomique sur le FS). Même pattern que [`FsHandoffStore`](crate::FsHandoffStore).
//! - **Fichier absent** ⇒ [`LiveState`] vide (jamais une erreur) : c'est l'état
//!   normal d'un projet qui n'a encore rien publié.
//! - **Fichier présent mais illisible** (JSON corrompu/partiel) ⇒
//!   [`StoreError::Serialization`]. Choix **aligné** sur les autres stores FS
//!   (`FsHandoffStore`, l'`index.json` de `FsSkillStore`) : on ne panique jamais,
//!   mais on ne masque pas non plus une corruption — l'écriture atomique rend ce
//!   cas très improbable, et le fichier étant runtime/non-versionné, le supprimer
//!   suffit à repartir d'un état vide.
//!
//! Le project root est fourni au constructeur (le port [`LiveStateStore`] n'a pas
//! de paramètre `root` par appel, contrairement à `SkillStore`/`MemoryStore`) ;
//! une instance sert donc **un** projet.

use std::path::PathBuf;

use async_trait::async_trait;

use domain::live_state::{LiveEntry, LiveState};
use domain::ports::{LiveStateStore, StoreError};
use domain::project::ProjectPath;

/// Le dossier `.ideai/` dans une racine de projet.
const IDEAI_DIR: &str = ".ideai";

/// Nom du fichier de live-state, par projet.
const LIVE_STATE_FILE: &str = "live-state.json";

/// Nom du fichier temporaire d'écriture atomique (renommé sur `live-state.json`).
const LIVE_STATE_TMP_FILE: &str = "live-state.json.tmp";

/// Adapter `tokio::fs` du store de live-state, un `live-state.json` par projet.
///
/// Même convention de construction que [`FsHandoffStore`](crate::FsHandoffStore) :
/// le **project root** est fourni au constructeur, la base `<root>/.ideai` en
/// dérive, et le fichier est créé paresseusement à la première écriture.
pub struct FsLiveStateStore {
    /// Racine `<project_root>/.ideai`.
    dir: PathBuf,
}

impl FsLiveStateStore {
    /// Construit l'adapter à partir du **project root**.
    #[must_use]
    pub fn new(root: &ProjectPath) -> Self {
        let dir = PathBuf::from(root.as_str()).join(IDEAI_DIR);
        Self { dir }
    }

    /// `<root>/.ideai/live-state.json` — le fichier cible.
    fn path(&self) -> PathBuf {
        self.dir.join(LIVE_STATE_FILE)
    }

    /// `<root>/.ideai/live-state.json.tmp` — le tmp d'écriture atomique.
    fn tmp_path(&self) -> PathBuf {
        self.dir.join(LIVE_STATE_TMP_FILE)
    }

    /// Lit l'état courant : fichier absent ⇒ état vide ; JSON corrompu ⇒ erreur.
    async fn read_state(&self) -> Result<LiveState, StoreError> {
        match tokio::fs::read(self.path()).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            // Absent ⇒ état vide (jamais une erreur).
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(LiveState::default()),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    /// Réécrit l'état **atomiquement** : tmp puis `rename` sur la cible.
    async fn write_state(&self, state: &LiveState) -> Result<(), StoreError> {
        tokio::fs::create_dir_all(&self.dir)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;

        let bytes = serde_json::to_vec_pretty(state)
            .map_err(|e| StoreError::Serialization(e.to_string()))?;

        let tmp = self.tmp_path();
        tokio::fs::write(&tmp, &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        tokio::fs::rename(&tmp, self.path())
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        Ok(())
    }
}

#[async_trait]
impl LiveStateStore for FsLiveStateStore {
    async fn load(&self) -> Result<LiveState, StoreError> {
        self.read_state().await
    }

    async fn upsert(&self, entry: LiveEntry) -> Result<(), StoreError> {
        let mut state = self.read_state().await?;
        state.upsert(entry);
        self.write_state(&state).await
    }

    async fn prune(&self, now_ms: u64, ttl_ms: u64, max_n: usize) -> Result<(), StoreError> {
        let mut state = self.read_state().await?;
        state.prune(now_ms, ttl_ms, max_n);
        self.write_state(&state).await
    }
}
