//! [`OpenProject`] and [`ListProjects`] (ARCHITECTURE §6).

use std::sync::Arc;

use domain::ports::{FileSystem, ProjectStore, RemotePath};
use domain::{AgentManifest, Project, ProjectId};

use crate::error::AppError;

use super::meta::{from_json_bytes, join_root, ProjectMeta, AGENTS_FILE, IDEAI_DIR, PROJECT_FILE};

/// Input for [`OpenProject::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenProjectInput {
    /// Id of the project to open (as known by the registry).
    pub project_id: ProjectId,
}

/// Output of [`OpenProject::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenProjectOutput {
    /// The opened project (registry source of truth for id/name/root/remote).
    pub project: Project,
    /// The project-local meta read from `.ideai/project.json`, if present and
    /// readable. Read tolerantly: a missing/corrupt file does not fail the open.
    pub meta: Option<ProjectMeta>,
    /// The agent manifest read from `.ideai/agents.json`, if present. Tolerant:
    /// absent in a brand-new project.
    pub manifest: Option<AgentManifest>,
}

/// Loads a project, its `.ideai/project.json` meta and (tolerantly) its manifest.
pub struct OpenProject {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
}

impl OpenProject {
    /// Builds the use case from its injected ports.
    #[must_use]
    pub fn new(store: Arc<dyn ProjectStore>, fs: Arc<dyn FileSystem>) -> Self {
        Self { store, fs }
    }

    /// Executes the open.
    ///
    /// # Errors
    /// - [`AppError::NotFound`] if the registry has no such project,
    /// - [`AppError::Store`] on registry I/O failure.
    ///
    /// Reading the `.ideai/` files is **tolerant**: their absence or a parse
    /// failure yields `None` rather than an error, so a project whose `.ideai/`
    /// was deleted can still be opened.
    pub async fn execute(&self, input: OpenProjectInput) -> Result<OpenProjectOutput, AppError> {
        let project = self.store.load_project(input.project_id).await?;

        let meta = self
            .read_optional_json::<ProjectMeta>(&project, PROJECT_FILE)
            .await;
        let manifest = self
            .read_optional_json::<AgentManifest>(&project, AGENTS_FILE)
            .await;

        Ok(OpenProjectOutput {
            project,
            meta,
            manifest,
        })
    }

    /// Reads and parses a JSON file inside the project's `.ideai/`, tolerating
    /// any failure (missing file, I/O error, parse error) by returning `None`.
    async fn read_optional_json<T: for<'de> serde::Deserialize<'de>>(
        &self,
        project: &Project,
        file: &str,
    ) -> Option<T> {
        let path = RemotePath::new(join_root(&project.root, &format!("{IDEAI_DIR}/{file}")));
        match self.fs.read(&path).await {
            Ok(bytes) => from_json_bytes::<T>(&bytes).ok(),
            Err(_) => None,
        }
    }
}

/// Output of [`ListProjects::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListProjectsOutput {
    /// All projects known to the registry.
    pub projects: Vec<Project>,
}

/// Lists the projects known to the registry.
pub struct ListProjects {
    store: Arc<dyn ProjectStore>,
}

impl ListProjects {
    /// Builds the use case from its injected port.
    #[must_use]
    pub fn new(store: Arc<dyn ProjectStore>) -> Self {
        Self { store }
    }

    /// Executes the listing.
    ///
    /// # Errors
    /// [`AppError::Store`] on registry I/O failure.
    pub async fn execute(&self) -> Result<ListProjectsOutput, AppError> {
        let projects = self.store.list_projects().await?;
        Ok(ListProjectsOutput { projects })
    }
}
