//! [`IdeaiContextStore`] — file implementation of the [`AgentContextStore`] port
//! (ARCHITECTURE §5, §9.1).
//!
//! Persists, **inside the project root**, the agent contexts and the manifest
//! that together make a project's agents travel with the code:
//!
//! ```text
//! <project_root>/
//! └── .ideai/
//!     ├── agents.json          # AgentManifest  { version, agents: [ManifestEntry, ...] }
//!     └── agents/
//!         ├── backend-dev.md   # an agent's context (md_path = "agents/backend-dev.md")
//!         └── ...
//! ```
//!
//! All I/O goes through the [`FileSystem`] port (here the `RemoteHost`'s
//! filesystem — [`crate::LocalFileSystem`] locally, an SFTP/WSL one later), so the
//! store is **location-neutral**: the very same adapter serves a local project or
//! a project hosted over SSH/WSL (Liskov, ARCHITECTURE §1.2).
//!
//! `md_path` values are interpreted **relative to `.ideai/`** (so a manifest
//! `md_path` of `agents/backend-dev.md` maps to
//! `<root>/.ideai/agents/backend-dev.md`), matching the documented schema.

use std::sync::Arc;

use async_trait::async_trait;

use domain::agent::AgentManifest;
use domain::ids::AgentId;
use domain::markdown::MarkdownDoc;
use domain::ports::{AgentContextStore, FileSystem, FsError, RemotePath, StoreError};
use domain::project::{Project, ProjectPath};

/// The `.ideai/` directory name inside a project root.
const IDEAI_DIR: &str = ".ideai";

/// The agent-manifest file name inside `.ideai/`.
const AGENTS_FILE: &str = "agents.json";

/// Current schema version written into a freshly-created manifest.
const MANIFEST_VERSION: u32 = 1;

/// File-backed [`AgentContextStore`], composing a [`FileSystem`] port.
///
/// Cheap to clone (everything is behind `Arc`); the composition root constructs
/// one per resolved `RemoteHost` and shares it across the agent use cases.
#[derive(Clone)]
pub struct IdeaiContextStore {
    fs: Arc<dyn FileSystem>,
}

impl IdeaiContextStore {
    /// Builds the store from an injected [`FileSystem`] (the project's
    /// `RemoteHost` filesystem).
    #[must_use]
    pub fn new(fs: Arc<dyn FileSystem>) -> Self {
        Self { fs }
    }

    /// Joins a project root with a relative segment using a POSIX separator
    /// (valid on every target; `tokio::fs` accepts `/` on Windows too).
    fn join(root: &ProjectPath, rel: &str) -> String {
        let base = root.as_str().trim_end_matches(['/', '\\']);
        format!("{base}/{rel}")
    }

    /// Absolute path of the manifest file for a project.
    fn manifest_path(project: &Project) -> RemotePath {
        RemotePath::new(Self::join(
            &project.root,
            &format!("{IDEAI_DIR}/{AGENTS_FILE}"),
        ))
    }

    /// Absolute path of an agent context `.md` from its (`.ideai/`-relative)
    /// `md_path`.
    fn context_path(project: &Project, md_path: &str) -> RemotePath {
        RemotePath::new(Self::join(&project.root, &format!("{IDEAI_DIR}/{md_path}")))
    }

    /// Resolves the `md_path` of an agent from the manifest, or
    /// [`StoreError::NotFound`] if the agent is unknown to this project.
    async fn md_path_of(&self, project: &Project, agent: &AgentId) -> Result<String, StoreError> {
        let manifest = self.load_manifest(project).await?;
        manifest
            .entries
            .into_iter()
            .find(|e| &e.agent_id == agent)
            .map(|e| e.md_path)
            .ok_or(StoreError::NotFound)
    }

    /// Ensures the directory holding `path` exists (creates `.ideai/` and any
    /// nested `agents/` parent before a write).
    async fn ensure_parent(&self, path: &RemotePath) -> Result<(), StoreError> {
        let raw = path.as_str();
        let dir = match raw.rfind(['/', '\\']) {
            Some(idx) => &raw[..idx],
            None => return Ok(()),
        };
        self.fs
            .create_dir_all(&RemotePath::new(dir.to_owned()))
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }
}

#[async_trait]
impl AgentContextStore for IdeaiContextStore {
    async fn read_context(
        &self,
        project: &Project,
        agent: &AgentId,
    ) -> Result<MarkdownDoc, StoreError> {
        let md_path = self.md_path_of(project, agent).await?;
        let path = Self::context_path(project, &md_path);
        match self.fs.read(&path).await {
            Ok(bytes) => {
                let content = String::from_utf8(bytes)
                    .map_err(|e| StoreError::Serialization(e.to_string()))?;
                Ok(MarkdownDoc::new(content))
            }
            Err(FsError::NotFound(_)) => Err(StoreError::NotFound),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    async fn write_context(
        &self,
        project: &Project,
        agent: &AgentId,
        md: &MarkdownDoc,
    ) -> Result<(), StoreError> {
        let md_path = self.md_path_of(project, agent).await?;
        let path = Self::context_path(project, &md_path);
        self.ensure_parent(&path).await?;
        self.fs
            .write(&path, md.as_str().as_bytes())
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }

    async fn load_manifest(&self, project: &Project) -> Result<AgentManifest, StoreError> {
        let path = Self::manifest_path(project);
        match self.fs.read(&path).await {
            Ok(bytes) => {
                serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
            }
            // No manifest yet: a project simply has no agents — return the empty
            // default rather than erroring (mirrors the project-store policy).
            Err(FsError::NotFound(_)) => Ok(AgentManifest {
                version: MANIFEST_VERSION,
                entries: Vec::new(),
                orchestrator: None,
            }),
            Err(e) => Err(StoreError::Io(e.to_string())),
        }
    }

    async fn save_manifest(
        &self,
        project: &Project,
        manifest: &AgentManifest,
    ) -> Result<(), StoreError> {
        let path = Self::manifest_path(project);
        self.ensure_parent(&path).await?;
        let mut bytes = serde_json::to_vec_pretty(manifest)
            .map_err(|e| StoreError::Serialization(e.to_string()))?;
        bytes.push(b'\n');
        self.fs
            .write(&path, &bytes)
            .await
            .map_err(|e| StoreError::Io(e.to_string()))
    }
}
