//! [`FsProjectStore`] — JSON file implementation of the [`ProjectStore`] port.
//!
//! Persistence layout (under the injected app-data directory, ARCHITECTURE §9.2):
//!
//! ```text
//! <app_data_dir>/
//! ├── projects.json     # the known-projects registry  { version, projects: [Project, ...] }
//! └── workspace.json    # the persisted Workspace (windows/tabs/layouts)
//! ```
//!
//! The store does **not** know about Tauri: the app-data directory is resolved
//! by the composition root and handed in as a plain path (Dependency Inversion).
//! All I/O goes through the [`FileSystem`] port (here [`LocalFileSystem`]) so the
//! store stays decoupled from `tokio::fs` directly and is reusable as-is.

use std::sync::Arc;

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

use domain::ids::ProjectId;
use domain::layout::Workspace;
use domain::ports::{FileSystem, ProjectStore, RemotePath, StoreError};
use domain::project::Project;

/// File name of the known-projects registry inside the app-data dir.
const REGISTRY_FILE: &str = "projects.json";

/// File name of the persisted workspace inside the app-data dir.
const WORKSPACE_FILE: &str = "workspace.json";

/// Current schema version of the registry file.
const REGISTRY_VERSION: u32 = 1;

/// On-disk shape of the registry file.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Registry {
    /// Schema version.
    version: u32,
    /// All known projects.
    projects: Vec<Project>,
}

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

impl FsProjectStore {
    /// Builds the store from an injected [`FileSystem`] and the app-data
    /// directory path (resolved by the composition root, e.g. via the Tauri path
    /// API). 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 a file name using a POSIX separator (valid on
    /// every target — `tokio::fs` accepts `/` on Windows too).
    fn path(&self, file: &str) -> RemotePath {
        let base = self.app_data_dir.trim_end_matches(['/', '\\']);
        RemotePath::new(format!("{base}/{file}"))
    }

    /// Reads and parses the registry, returning an empty one if the file does
    /// not exist yet.
    async fn read_registry(&self) -> Result<Registry, StoreError> {
        let path = self.path(REGISTRY_FILE);
        match self.fs.read(&path).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            Err(domain::ports::FsError::NotFound(_)) => Ok(Registry {
                version: REGISTRY_VERSION,
                projects: Vec::new(),
            }),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    /// Writes the registry, ensuring the app-data dir exists first.
    async fn write_registry(&self, registry: &Registry) -> Result<(), StoreError> {
        self.ensure_dir().await?;
        let bytes = serde_json::to_vec_pretty(registry)
            .map_err(|e| StoreError::Serialization(e.to_string()))?;
        self.fs
            .write(&self.path(REGISTRY_FILE), &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    /// Creates the app-data directory and all missing parents.
    async fn ensure_dir(&self) -> 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()))
    }
}

#[async_trait]
impl ProjectStore for FsProjectStore {
    async fn list_projects(&self) -> Result<Vec<Project>, StoreError> {
        Ok(self.read_registry().await?.projects)
    }

    async fn load_project(&self, id: ProjectId) -> Result<Project, StoreError> {
        self.read_registry()
            .await?
            .projects
            .into_iter()
            .find(|p| p.id == id)
            .ok_or(StoreError::NotFound)
    }

    async fn save_project(&self, project: &Project) -> Result<(), StoreError> {
        let mut registry = self.read_registry().await?;
        if let Some(slot) = registry.projects.iter_mut().find(|p| p.id == project.id) {
            *slot = project.clone();
        } else {
            registry.projects.push(project.clone());
        }
        self.write_registry(&registry).await
    }

    async fn save_workspace(&self, workspace: &Workspace) -> Result<(), StoreError> {
        self.ensure_dir().await?;
        let bytes = serde_json::to_vec_pretty(workspace)
            .map_err(|e| StoreError::Serialization(e.to_string()))?;
        self.fs
            .write(&self.path(WORKSPACE_FILE), &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    async fn load_workspace(&self) -> Result<Workspace, StoreError> {
        let path = self.path(WORKSPACE_FILE);
        match self.fs.read(&path).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            // No workspace persisted yet: return the empty default.
            Err(domain::ports::FsError::NotFound(_)) => Ok(Workspace::default()),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }
}
