//! [`FsTemplateStore`] — file implementation of the [`TemplateStore`] port
//! (ARCHITECTURE §5, §9.2).
//!
//! Templates live in the **global IDE store** (machine-local app-data dir, *not*
//! inside any project): the Markdown content travels as a diffable `.md`, with a
//! small JSON index carrying the metadata (version, hash) needed to list and
//! version templates without parsing every `.md`:
//!
//! ```text
//! <app_data_dir>/templates/
//! ├── index.json   # { version, templates: [{ id, name, version, contentHash, defaultProfileId }] }
//! └── md/
//!     └── <id>.md   # a template's Markdown content
//! ```
//!
//! `contentHash` is a stable digest of the `.md` content, recorded so a future
//! spike can detect **out-of-app edits** (ARCHITECTURE §13.9); it is not part of
//! the [`AgentTemplate`] domain entity. Like the other stores, all I/O goes
//! through the [`FileSystem`] port, so the adapter is Tauri-agnostic.

use std::hash::{Hash, Hasher};
use std::sync::Arc;

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

use domain::ids::{ProfileId, TemplateId};
use domain::markdown::MarkdownDoc;
use domain::ports::{FileSystem, RemotePath, StoreError, TemplateStore};
use domain::template::{AgentTemplate, TemplateVersion};

/// Directory (under app-data) holding the templates store.
const TEMPLATES_DIR: &str = "templates";

/// Index file name inside the templates dir.
const INDEX_FILE: &str = "index.json";

/// Current schema version of the index file.
const INDEX_VERSION: u32 = 1;

/// One metadata row in `index.json` (the `.md` content lives separately).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IndexEntry {
    id: TemplateId,
    name: String,
    version: TemplateVersion,
    content_hash: String,
    default_profile_id: ProfileId,
}

/// On-disk shape of `templates/index.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct IndexDoc {
    version: u32,
    templates: Vec<IndexEntry>,
}

impl Default for IndexDoc {
    fn default() -> Self {
        Self {
            version: INDEX_VERSION,
            templates: Vec::new(),
        }
    }
}

/// A stable, dependency-free digest of Markdown content for out-of-app edit
/// detection. `DefaultHasher::new()` uses fixed keys, so this is deterministic
/// across runs and platforms (unlike a `RandomState`-seeded hasher).
fn content_hash(md: &MarkdownDoc) -> String {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    md.as_str().hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

/// File-backed [`TemplateStore`], composing a [`FileSystem`] port.
#[derive(Clone)]
pub struct FsTemplateStore {
    fs: Arc<dyn FileSystem>,
    app_data_dir: String,
}

impl FsTemplateStore {
    /// Builds the store from an injected [`FileSystem`] and the app-data dir
    /// (resolved by the composition root). Directories are created 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(),
        }
    }

    /// `<app>/templates`.
    fn dir(&self) -> String {
        let base = self.app_data_dir.trim_end_matches(['/', '\\']);
        format!("{base}/{TEMPLATES_DIR}")
    }

    /// `<app>/templates/index.json`.
    fn index_path(&self) -> RemotePath {
        RemotePath::new(format!("{}/{INDEX_FILE}", self.dir()))
    }

    /// `<app>/templates/md/<id>.md`.
    fn md_path(&self, id: TemplateId) -> RemotePath {
        RemotePath::new(format!("{}/md/{id}.md", self.dir()))
    }

    /// Reads the index, returning an empty default if absent.
    async fn read_index(&self) -> Result<IndexDoc, StoreError> {
        match self.fs.read(&self.index_path()).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            Err(domain::ports::FsError::NotFound(_)) => Ok(IndexDoc::default()),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    /// Writes the index, ensuring `templates/` exists.
    async fn write_index(&self, doc: &IndexDoc) -> Result<(), StoreError> {
        self.fs
            .create_dir_all(&RemotePath::new(self.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.index_path(), &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    /// Reconstructs the [`AgentTemplate`] for an index entry by reading its `.md`.
    async fn load(&self, entry: &IndexEntry) -> Result<AgentTemplate, StoreError> {
        let bytes = self
            .fs
            .read(&self.md_path(entry.id))
            .await
            .map_err(|e| match e {
                domain::ports::FsError::NotFound(_) => StoreError::NotFound,
                other => StoreError::Io(other.to_string()),
            })?;
        let content =
            String::from_utf8(bytes).map_err(|e| StoreError::Serialization(e.to_string()))?;
        // The domain entity carries the authoritative version/name from the index;
        // we reconstruct it directly (no public mutator needed) since every field
        // is known and already validated when it was first saved.
        Ok(AgentTemplate {
            id: entry.id,
            name: entry.name.clone(),
            content_md: MarkdownDoc::new(content),
            version: entry.version,
            default_profile_id: entry.default_profile_id,
        })
    }
}

#[async_trait]
impl TemplateStore for FsTemplateStore {
    async fn list(&self) -> Result<Vec<AgentTemplate>, StoreError> {
        let index = self.read_index().await?;
        let mut out = Vec::with_capacity(index.templates.len());
        for entry in &index.templates {
            out.push(self.load(entry).await?);
        }
        Ok(out)
    }

    async fn get(&self, id: TemplateId) -> Result<AgentTemplate, StoreError> {
        let index = self.read_index().await?;
        let entry = index
            .templates
            .iter()
            .find(|e| e.id == id)
            .ok_or(StoreError::NotFound)?;
        self.load(entry).await
    }

    async fn save(&self, template: &AgentTemplate) -> Result<(), StoreError> {
        // (1) Write the Markdown content.
        self.fs
            .create_dir_all(&RemotePath::new(format!("{}/md", self.dir())))
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;
        self.fs
            .write(
                &self.md_path(template.id),
                template.content_md.as_str().as_bytes(),
            )
            .await
            .map_err(|e| StoreError::Io(e.to_string()))?;

        // (2) Upsert the index metadata.
        let mut index = self.read_index().await?;
        let row = IndexEntry {
            id: template.id,
            name: template.name.clone(),
            version: template.version,
            content_hash: content_hash(&template.content_md),
            default_profile_id: template.default_profile_id,
        };
        if let Some(slot) = index.templates.iter_mut().find(|e| e.id == template.id) {
            *slot = row;
        } else {
            index.templates.push(row);
        }
        self.write_index(&index).await
    }

    async fn delete(&self, id: TemplateId) -> Result<(), StoreError> {
        let mut index = self.read_index().await?;
        let before = index.templates.len();
        index.templates.retain(|e| e.id != id);
        if index.templates.len() == before {
            return Err(StoreError::NotFound);
        }
        // The orphaned `md/<id>.md` is left on disk (the FileSystem port exposes no
        // delete); the index no longer references it, so it is effectively gone.
        self.write_index(&index).await
    }
}
