//! [`FsProfileStore`] — JSON file implementation of the [`ProfileStore`] port.
//!
//! Persists the configured [`AgentProfile`]s in the global IDE store
//! (ARCHITECTURE §9.2):
//!
//! ```text
//! <app_data_dir>/
//! └── profiles.json     # { version, profiles: [AgentProfile, ...] }
//! ```
//!
//! Each profile item is exactly the declarative profile of CONTEXT §9
//! (`id, name, command, args, contextInjection{strategy,…}, detect, cwd`). The
//! existence of `profiles.json` is what marks the first run as *done* (see
//! [`ProfileStore::is_configured`]).
//!
//! Like [`super::FsProjectStore`], the store is Tauri-agnostic: the app-data
//! directory is resolved by the composition root and injected as a plain path,
//! and all I/O goes through the [`FileSystem`] port.

use std::sync::Arc;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use domain::ids::ProfileId;
use domain::ports::{EmbedderProfileStore, FileSystem, ProfileStore, RemotePath, StoreError};
use domain::profile::{AgentProfile, EmbedderProfile};

/// File name of the profiles store inside the app-data dir.
const PROFILES_FILE: &str = "profiles.json";

/// Current schema version of the profiles file.
const PROFILES_VERSION: u32 = 1;

/// On-disk shape of `profiles.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProfilesDoc {
    /// Schema version.
    version: u32,
    /// All configured profiles.
    profiles: Vec<AgentProfile>,
}

impl Default for ProfilesDoc {
    fn default() -> Self {
        Self {
            version: PROFILES_VERSION,
            profiles: Vec::new(),
        }
    }
}

/// JSON-file implementation of the [`ProfileStore`] port.
///
/// Cheap to clone (everything is behind `Arc`); built once at the composition
/// root and shared across use cases.
#[derive(Clone)]
pub struct FsProfileStore {
    fs: Arc<dyn FileSystem>,
    app_data_dir: String,
}

impl FsProfileStore {
    /// Builds the store from an injected [`FileSystem`] and the app-data
    /// directory path (resolved by the composition root). The directory is
    /// created lazily on first write.
    #[must_use]
    pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
        Self {
            fs,
            app_data_dir: app_data_dir.into(),
        }
    }

    /// Joins the app-data dir with the profiles file name (POSIX separator, valid
    /// on every target — `tokio::fs` accepts `/` on Windows too).
    fn path(&self) -> RemotePath {
        let base = self.app_data_dir.trim_end_matches(['/', '\\']);
        RemotePath::new(format!("{base}/{PROFILES_FILE}"))
    }

    /// Reads and parses the doc, returning an empty default if the file is absent.
    async fn read_doc(&self) -> Result<ProfilesDoc, StoreError> {
        match self.fs.read(&self.path()).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            Err(domain::ports::FsError::NotFound(_)) => Ok(ProfilesDoc::default()),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    /// Writes the doc, ensuring the app-data dir exists first.
    async fn write_doc(&self, doc: &ProfilesDoc) -> Result<(), StoreError> {
        let dir = RemotePath::new(self.app_data_dir.trim_end_matches(['/', '\\']).to_owned());
        self.fs
            .create_dir_all(&dir)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        let bytes =
            serde_json::to_vec_pretty(doc).map_err(|e| StoreError::Serialization(e.to_string()))?;
        self.fs
            .write(&self.path(), &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }
}

#[async_trait]
impl ProfileStore for FsProfileStore {
    async fn list(&self) -> Result<Vec<AgentProfile>, StoreError> {
        Ok(self.read_doc().await?.profiles)
    }

    async fn save(&self, profile: &AgentProfile) -> Result<(), StoreError> {
        let mut doc = self.read_doc().await?;
        if let Some(slot) = doc.profiles.iter_mut().find(|p| p.id == profile.id) {
            *slot = profile.clone();
        } else {
            doc.profiles.push(profile.clone());
        }
        self.write_doc(&doc).await
    }

    async fn delete(&self, id: ProfileId) -> Result<(), StoreError> {
        let mut doc = self.read_doc().await?;
        let before = doc.profiles.len();
        doc.profiles.retain(|p| p.id != id);
        if doc.profiles.len() == before {
            return Err(StoreError::NotFound);
        }
        self.write_doc(&doc).await
    }

    async fn is_configured(&self) -> Result<bool, StoreError> {
        self.fs
            .exists(&self.path())
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    async fn mark_configured(&self) -> Result<(), StoreError> {
        // Write the current doc back (an empty default when nothing exists),
        // which materialises `profiles.json` and records first-run completion.
        let doc = self.read_doc().await?;
        self.write_doc(&doc).await
    }
}

// ---------------------------------------------------------------------------
// FsEmbedderProfileStore — declarative embedder profiles (`embedder.json`, LOT C).
// ---------------------------------------------------------------------------

/// File name of the embedder-profiles store inside the app-data dir.
const EMBEDDER_FILE: &str = "embedder.json";

/// Current schema version of the embedder-profiles file.
const EMBEDDER_VERSION: u32 = 1;

/// On-disk shape of `embedder.json` (LOT C, §14.5.3), mirroring [`ProfilesDoc`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EmbedderDoc {
    /// Schema version.
    version: u32,
    /// All configured embedder profiles.
    profiles: Vec<EmbedderProfile>,
}

impl Default for EmbedderDoc {
    fn default() -> Self {
        Self {
            version: EMBEDDER_VERSION,
            profiles: Vec::new(),
        }
    }
}

/// JSON-file store for declarative [`EmbedderProfile`]s (LOT C, étage 2 vectoriel),
/// calqué on [`FsProfileStore`]: `embedder.json` in the global IDE app-data dir,
/// mirroring `profiles.json`.
///
/// Implements the domain [`EmbedderProfileStore`] port (parallel to [`ProfileStore`]).
/// The inherent `list`/`save`/`delete` methods are kept so the composition root can
/// load the configured profile *before* type-erasing to `Arc<dyn EmbedderProfileStore>`
/// (e.g. inside a one-shot blocking runtime in `state.rs`); the trait impl simply
/// delegates to them.
///
/// Cheap to clone (everything behind `Arc`).
#[derive(Clone)]
pub struct FsEmbedderProfileStore {
    fs: Arc<dyn FileSystem>,
    app_data_dir: String,
}

impl FsEmbedderProfileStore {
    /// Builds the store from an injected [`FileSystem`] and the app-data dir.
    #[must_use]
    pub fn new(fs: Arc<dyn FileSystem>, app_data_dir: impl Into<String>) -> Self {
        Self {
            fs,
            app_data_dir: app_data_dir.into(),
        }
    }

    /// `<app-data-dir>/embedder.json`.
    fn path(&self) -> RemotePath {
        let base = self.app_data_dir.trim_end_matches(['/', '\\']);
        RemotePath::new(format!("{base}/{EMBEDDER_FILE}"))
    }

    async fn read_doc(&self) -> Result<EmbedderDoc, StoreError> {
        match self.fs.read(&self.path()).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            Err(domain::ports::FsError::NotFound(_)) => Ok(EmbedderDoc::default()),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    async fn write_doc(&self, doc: &EmbedderDoc) -> Result<(), StoreError> {
        let dir = RemotePath::new(self.app_data_dir.trim_end_matches(['/', '\\']).to_owned());
        self.fs
            .create_dir_all(&dir)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        let bytes =
            serde_json::to_vec_pretty(doc).map_err(|e| StoreError::Serialization(e.to_string()))?;
        self.fs
            .write(&self.path(), &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    /// Lists all configured embedder profiles (empty when `embedder.json` is
    /// absent — the default `none` posture).
    ///
    /// # Errors
    /// [`StoreError`] on an I/O or deserialisation failure.
    pub async fn list(&self) -> Result<Vec<EmbedderProfile>, StoreError> {
        Ok(self.read_doc().await?.profiles)
    }

    /// Saves (creates or replaces by id) an embedder profile.
    ///
    /// # Errors
    /// [`StoreError`] on failure.
    pub async fn save(&self, profile: &EmbedderProfile) -> Result<(), StoreError> {
        let mut doc = self.read_doc().await?;
        if let Some(slot) = doc.profiles.iter_mut().find(|p| p.id == profile.id) {
            *slot = profile.clone();
        } else {
            doc.profiles.push(profile.clone());
        }
        self.write_doc(&doc).await
    }

    /// Deletes an embedder profile by id.
    ///
    /// # Errors
    /// [`StoreError::NotFound`] if no profile carries that id.
    pub async fn delete(&self, id: &str) -> Result<(), StoreError> {
        let mut doc = self.read_doc().await?;
        let before = doc.profiles.len();
        doc.profiles.retain(|p| p.id != id);
        if doc.profiles.len() == before {
            return Err(StoreError::NotFound);
        }
        self.write_doc(&doc).await
    }
}

#[async_trait]
impl EmbedderProfileStore for FsEmbedderProfileStore {
    async fn list(&self) -> Result<Vec<EmbedderProfile>, StoreError> {
        FsEmbedderProfileStore::list(self).await
    }

    async fn save(&self, profile: &EmbedderProfile) -> Result<(), StoreError> {
        FsEmbedderProfileStore::save(self, profile).await
    }

    async fn delete(&self, id: &str) -> Result<(), StoreError> {
        FsEmbedderProfileStore::delete(self, id).await
    }
}
