//! [`ProjectMeta`] — the on-disk shape of `.ideai/project.json` (ARCHITECTURE §9.1).
//!
//! This is the *project-local* metadata that travels with the code (it lives
//! inside the project root, is versionable, and is independent of the machine).
//! The known-projects **registry** is a separate, machine-local concern owned by
//! the [`domain::ports::ProjectStore`] adapter (ARCHITECTURE §9.2).

use serde::{Deserialize, Serialize};

use domain::{Project, ProjectId, ProjectPath, RemoteRef};

use crate::error::AppError;

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

/// The project-meta file name inside `.ideai/`.
pub(crate) const PROJECT_FILE: &str = "project.json";

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

/// Current schema version of `project.json`.
pub(crate) const PROJECT_META_VERSION: u32 = 1;

/// Serialised contents of `.ideai/project.json`.
///
/// Carries the project's identity and the metadata needed to reopen it: its
/// stable id, display name, the default agent profile, the remote reference and
/// the creation timestamp. The `root` itself is *not* stored here — the file
/// already lives at `<root>/.ideai/project.json`, so the root is implied by the
/// file's location (and authoritatively held by the registry).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectMeta {
    /// Schema version of this file.
    pub version: u32,
    /// Stable project id (matches the registry entry).
    pub id: ProjectId,
    /// Display name.
    pub name: String,
    /// Default agent profile id, if one has been chosen (`null` until first-run
    /// / profile selection in later lots).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_profile_id: Option<String>,
    /// Where the project physically lives.
    pub remote: RemoteRef,
    /// Creation timestamp, epoch milliseconds.
    pub created_at: i64,
}

impl ProjectMeta {
    /// Builds the meta image from a [`Project`].
    #[must_use]
    pub fn from_project(project: &Project, default_profile_id: Option<String>) -> Self {
        Self {
            version: PROJECT_META_VERSION,
            id: project.id,
            name: project.name.clone(),
            default_profile_id,
            remote: project.remote.clone(),
            created_at: project.created_at,
        }
    }

    /// Reconstructs a validated [`Project`] from this meta and its (registry-known)
    /// root.
    ///
    /// # Errors
    /// Returns [`AppError::Invalid`] if the stored fields violate a domain
    /// invariant (e.g. empty name).
    pub fn into_project(self, root: ProjectPath) -> Result<Project, AppError> {
        Project::new(self.id, self.name, root, self.remote, self.created_at)
            .map_err(|e| AppError::Invalid(e.to_string()))
    }
}

/// Serialises a value to pretty JSON bytes, mapping failures to [`AppError`].
pub(crate) fn to_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, AppError> {
    serde_json::to_vec_pretty(value)
        .map(|mut v| {
            v.push(b'\n');
            v
        })
        .map_err(|e| AppError::Store(format!("serialize failed: {e}")))
}

/// Deserialises JSON bytes, mapping failures to [`AppError`].
pub(crate) fn from_json_bytes<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, AppError> {
    serde_json::from_slice(bytes).map_err(|e| AppError::Store(format!("deserialize failed: {e}")))
}

/// Joins a project root with a relative path segment using a POSIX-style
/// separator. Paths inside `.ideai/` are always written with `/`, which is valid
/// on every platform we target (Windows `tokio::fs` accepts `/`).
pub(crate) fn join_root(root: &ProjectPath, rel: &str) -> String {
    let base = root.as_str().trim_end_matches(['/', '\\']);
    format!("{base}/{rel}")
}
