//! [`CreateProject`] — create a project from a project root (ARCHITECTURE §6).
//!
//! Responsibilities (and *only* these):
//! 1. validate the root (absolute path — enforced by [`ProjectPath`]) and name,
//! 2. enforce the cross-aggregate uniqueness invariant `(remote, root)` — this
//!    lives here, not in the domain, because it requires knowledge of *all*
//!    projects (a repository concern, ARCHITECTURE §3.2),
//! 3. create the `.ideai/` directory and write `project.json`,
//! 4. register the project via the [`ProjectStore`],
//! 5. publish [`DomainEvent::ProjectCreated`].

use std::sync::Arc;

use domain::ports::{Clock, EventBus, FileSystem, IdGenerator, ProjectStore, RemotePath};
use domain::{DomainEvent, Project, ProjectId, ProjectPath, RemoteRef};

use crate::error::AppError;

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

/// Input for [`CreateProject::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateProjectInput {
    /// Display name of the project.
    pub name: String,
    /// Absolute project root (validated into a [`ProjectPath`]).
    pub root: String,
    /// Where the project lives. Defaults to [`RemoteRef::Local`] when `None`.
    pub remote: Option<RemoteRef>,
    /// Default agent profile id, if already chosen.
    pub default_profile_id: Option<String>,
}

/// Output of [`CreateProject::execute`]: the freshly-created project.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateProjectOutput {
    /// The created project.
    pub project: Project,
}

/// Creates a project: inits `.ideai/`, writes `project.json`, registers it.
pub struct CreateProject {
    store: Arc<dyn ProjectStore>,
    fs: Arc<dyn FileSystem>,
    ids: Arc<dyn IdGenerator>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes project creation.
    ///
    /// # Errors
    /// - [`AppError::Invalid`] if the name is empty or the root is not absolute,
    /// - [`AppError::Invalid`] if a project already exists for the same
    ///   `(remote, root)` (uniqueness invariant),
    /// - [`AppError::FileSystem`] / [`AppError::Store`] on I/O failures.
    pub async fn execute(
        &self,
        input: CreateProjectInput,
    ) -> Result<CreateProjectOutput, AppError> {
        let root = ProjectPath::new(input.root).map_err(|e| AppError::Invalid(e.to_string()))?;
        let remote = input.remote.unwrap_or(RemoteRef::Local);

        // (1+2) Uniqueness invariant on (remote, root) — repository-level.
        let existing = self.store.list_projects().await?;
        if existing
            .iter()
            .any(|p| p.remote == remote && p.root == root)
        {
            return Err(AppError::Invalid(format!(
                "a project already exists at {} for this remote",
                root.as_str()
            )));
        }

        // Build the validated aggregate (enforces non-empty name).
        let id = ProjectId::from_uuid(self.ids.new_uuid());
        let created_at = self.clock.now_millis();
        let project = Project::new(id, input.name, root.clone(), remote, created_at)
            .map_err(|e| AppError::Invalid(e.to_string()))?;

        // (3) Materialise `.ideai/` and write `project.json`.
        let ideai_dir = join_root(&root, IDEAI_DIR);
        self.fs.create_dir_all(&RemotePath::new(ideai_dir)).await?;

        let meta = ProjectMeta::from_project(&project, input.default_profile_id);
        let meta_path = join_root(&root, &format!("{IDEAI_DIR}/{PROJECT_FILE}"));
        self.fs
            .write(&RemotePath::new(meta_path), &to_json_bytes(&meta)?)
            .await?;

        // (4) Register in the known-projects registry.
        self.store.save_project(&project).await?;

        // (5) Announce.
        self.events
            .publish(DomainEvent::ProjectCreated { project_id: id });

        Ok(CreateProjectOutput { project })
    }
}
