//! Agent entity, its origin and the project agent manifest.

use serde::{Deserialize, Serialize};

use crate::error::DomainError;
use crate::ids::{AgentId, ProfileId, TemplateId};
use crate::skill::SkillRef;
use crate::template::TemplateVersion;

/// Origin of an agent: created from scratch, or derived from a template.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum AgentOrigin {
    /// Created from scratch; no template link.
    Scratch,
    /// Derived from a template, tracking the last synced version.
    #[serde(rename_all = "camelCase")]
    FromTemplate {
        /// Source template.
        template_id: TemplateId,
        /// Template version recorded at the last successful sync.
        synced_template_version: TemplateVersion,
    },
}

impl AgentOrigin {
    /// Returns the source template id, if any.
    #[must_use]
    pub fn template_id(&self) -> Option<TemplateId> {
        match self {
            Self::Scratch => None,
            Self::FromTemplate { template_id, .. } => Some(*template_id),
        }
    }

    /// Returns `true` if this origin is a template.
    #[must_use]
    pub fn is_from_template(&self) -> bool {
        matches!(self, Self::FromTemplate { .. })
    }
}

/// A project-scoped agent.
///
/// Invariants enforced here:
/// - `name` non-empty,
/// - `context_path` is a relative, safe path (the `.md` lives under `.ideai/`),
/// - `synchronized == true` ⇒ `origin == FromTemplate { .. }`.
///
/// Note: "`context` must exist at activation" and "`profile_id` must reference a
/// known profile" are *runtime/cross-aggregate* invariants checked by the
/// application layer (they require I/O or the profile registry), not by this
/// pure constructor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Agent {
    /// Stable identifier.
    pub id: AgentId,
    /// Display name.
    pub name: String,
    /// Relative path of the agent's `.md` within `.ideai/` (e.g. `agents/foo.md`).
    pub context_path: String,
    /// Runtime profile reference.
    pub profile_id: ProfileId,
    /// Origin of the agent.
    pub origin: AgentOrigin,
    /// Whether the agent tracks its template (only valid for template origins).
    pub synchronized: bool,
    /// Skills assigned to this agent, injected into its convention file at
    /// activation (ARCHITECTURE §14.2). Empty by default.
    #[serde(default)]
    pub skills: Vec<SkillRef>,
}

impl Agent {
    /// Builds a validated agent.
    ///
    /// # Errors
    /// - [`DomainError::EmptyField`] if `name` is empty,
    /// - [`DomainError::PathNotRelativeSafe`] if `context_path` is absolute or
    ///   contains `..`,
    /// - [`DomainError::SyncRequiresTemplate`] if `synchronized` is `true` while
    ///   `origin` is [`AgentOrigin::Scratch`].
    pub fn new(
        id: AgentId,
        name: impl Into<String>,
        context_path: impl Into<String>,
        profile_id: ProfileId,
        origin: AgentOrigin,
        synchronized: bool,
    ) -> Result<Self, DomainError> {
        let name = name.into();
        let context_path = context_path.into();
        crate::validation::non_empty(&name, "agent.name")?;
        crate::validation::relative_safe(&context_path)?;
        if synchronized && !origin.is_from_template() {
            return Err(DomainError::SyncRequiresTemplate);
        }
        Ok(Self {
            id,
            name,
            context_path,
            profile_id,
            origin,
            synchronized,
            skills: Vec::new(),
        })
    }

    /// Change le profil runtime de l'agent. Le contexte `.md`, l'origine template
    /// et la synchronisation sont **inchangés** (décision verrouillée : on garde le
    /// `.md`/mémoire, on ne touche qu'au moteur). Pur, infaillible (un `ProfileId`
    /// est déjà un VO validé).
    #[must_use]
    pub fn with_profile(mut self, profile_id: ProfileId) -> Self {
        self.profile_id = profile_id;
        self
    }

    /// Returns a copy of this agent carrying the given assigned skills,
    /// deduplicated by `skill_id` (keeping first occurrence).
    #[must_use]
    pub fn with_skills(mut self, skills: Vec<SkillRef>) -> Self {
        self.skills = Vec::new();
        for skill in skills {
            self.assign_skill(skill);
        }
        self
    }

    /// Assigns a skill to this agent. Idempotent: re-assigning the same
    /// `skill_id` is a no-op (returns `false`); a new assignment returns `true`.
    pub fn assign_skill(&mut self, skill: SkillRef) -> bool {
        if self.skills.iter().any(|s| s.skill_id == skill.skill_id) {
            return false;
        }
        self.skills.push(skill);
        true
    }

    /// Removes a skill assignment by id. Returns `true` if a skill was removed.
    pub fn unassign_skill(&mut self, skill_id: crate::ids::SkillId) -> bool {
        let before = self.skills.len();
        self.skills.retain(|s| s.skill_id != skill_id);
        self.skills.len() != before
    }
}

/// One entry in the project agent manifest (`.ideai/agents.json`).
///
/// This is the **persisted form of an [`Agent`]** (ARCHITECTURE §9.1): the
/// manifest is the source of truth for a project's agents, so each entry carries
/// everything needed to reconstruct the agent — its `name` and `profile_id`
/// included (without them the IDE could not list agents or resolve the profile to
/// launch). The template link is kept flat (`template_id` +
/// `synced_template_version`) for a compact on-disk shape; [`to_agent`] folds it
/// back into an [`AgentOrigin`].
///
/// Invariants:
/// - `name` non-empty,
/// - `md_path` relative and safe,
/// - `synchronized == true` ⇒ `template_id.is_some() && synced_template_version.is_some()`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ManifestEntry {
    /// The agent this entry describes.
    pub agent_id: AgentId,
    /// Display name of the agent.
    pub name: String,
    /// Relative path of the agent's `.md`.
    pub md_path: String,
    /// Runtime profile reference.
    pub profile_id: ProfileId,
    /// Source template, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub template_id: Option<TemplateId>,
    /// Whether the agent tracks its template.
    pub synchronized: bool,
    /// Template version recorded at the last sync.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub synced_template_version: Option<TemplateVersion>,
    /// Skills assigned to this agent (ARCHITECTURE §14.2). Defaults to empty for
    /// backward-compatible deserialisation of pre-L12 manifests.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub skills: Vec<SkillRef>,
}

impl ManifestEntry {
    /// Builds a validated manifest entry.
    ///
    /// # Errors
    /// - [`DomainError::EmptyField`] if `name` is empty,
    /// - [`DomainError::PathNotRelativeSafe`] if `md_path` is absolute or has `..`,
    /// - [`DomainError::InconsistentManifest`] if `synchronized` is `true` while
    ///   `template_id` or `synced_template_version` is missing.
    pub fn new(
        agent_id: AgentId,
        name: impl Into<String>,
        md_path: impl Into<String>,
        profile_id: ProfileId,
        template_id: Option<TemplateId>,
        synchronized: bool,
        synced_template_version: Option<TemplateVersion>,
    ) -> Result<Self, DomainError> {
        let name = name.into();
        let md_path = md_path.into();
        crate::validation::non_empty(&name, "manifestEntry.name")?;
        crate::validation::relative_safe(&md_path)?;
        if synchronized && (template_id.is_none() || synced_template_version.is_none()) {
            return Err(DomainError::InconsistentManifest {
                reason: "synchronized entry requires templateId and syncedTemplateVersion"
                    .to_string(),
            });
        }
        Ok(Self {
            agent_id,
            name,
            md_path,
            profile_id,
            template_id,
            synchronized,
            synced_template_version,
            skills: Vec::new(),
        })
    }

    /// Projects an [`Agent`] into its manifest entry (flattening the origin).
    ///
    /// Infallible: an [`Agent`] is already validated, and every entry invariant
    /// is implied by the agent's invariants.
    #[must_use]
    pub fn from_agent(agent: &Agent) -> Self {
        let (template_id, synced_template_version) = match &agent.origin {
            AgentOrigin::Scratch => (None, None),
            AgentOrigin::FromTemplate {
                template_id,
                synced_template_version,
            } => (Some(*template_id), Some(*synced_template_version)),
        };
        Self {
            agent_id: agent.id,
            name: agent.name.clone(),
            md_path: agent.context_path.clone(),
            profile_id: agent.profile_id,
            template_id,
            synchronized: agent.synchronized,
            synced_template_version,
            skills: agent.skills.clone(),
        }
    }

    /// Reconstructs the validated [`Agent`] this entry persists, folding the flat
    /// template link back into an [`AgentOrigin`].
    ///
    /// # Errors
    /// Returns a [`DomainError`] if the persisted fields violate an [`Agent`]
    /// invariant (e.g. a synchronized entry whose origin is not a template).
    pub fn to_agent(&self) -> Result<Agent, DomainError> {
        let origin = match (self.template_id, self.synced_template_version) {
            (Some(template_id), Some(synced_template_version)) => AgentOrigin::FromTemplate {
                template_id,
                synced_template_version,
            },
            _ => AgentOrigin::Scratch,
        };
        Ok(Agent::new(
            self.agent_id,
            self.name.clone(),
            self.md_path.clone(),
            self.profile_id,
            origin,
            self.synchronized,
        )?
        .with_skills(self.skills.clone()))
    }
}

/// In-memory image of `.ideai/agents.json`.
///
/// Invariants enforced here:
/// - `md_path` values are unique across entries;
/// - `orchestrator == Some(id)` ⇒ `id` is present in `entries` (referential).
///
/// **Ordering invariant**: `entries` are kept in **creation order**, so
/// `entries.first()` is the **oldest** agent. There is no per-entry timestamp; the
/// vector position *is* the chronology, which is why the default orchestrator (when
/// none is designated) is `entries.first()`.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentManifest {
    /// Schema version of the manifest file.
    pub version: u32,
    /// Entries (one per project agent), in **creation order** (oldest first).
    #[serde(rename = "agents")]
    pub entries: Vec<ManifestEntry>,
    /// Explicitly designated orchestrator agent, if any.
    ///
    /// We persist only the **deviation** from the default: `None` (the common case)
    /// means « the oldest agent orchestrates » (see [`effective_orchestrator`]);
    /// `Some(id)` is an explicit designation. Skipped on serialisation when `None`,
    /// so pre-feature manifests round-trip unchanged (free backward compatibility).
    ///
    /// [`effective_orchestrator`]: AgentManifest::effective_orchestrator
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub orchestrator: Option<AgentId>,
}

impl AgentManifest {
    /// Builds a validated manifest with **no** explicit orchestrator (the default:
    /// the oldest agent orchestrates — see [`effective_orchestrator`]).
    ///
    /// [`effective_orchestrator`]: AgentManifest::effective_orchestrator
    ///
    /// # Errors
    /// Returns [`DomainError::InconsistentManifest`] if two entries share the
    /// same `md_path`.
    pub fn new(version: u32, entries: Vec<ManifestEntry>) -> Result<Self, DomainError> {
        Self::with_orchestrator(version, entries, None)
    }

    /// Builds a validated manifest carrying an explicit `orchestrator` designation.
    ///
    /// # Errors
    /// - [`DomainError::InconsistentManifest`] if two entries share the same
    ///   `md_path`;
    /// - [`DomainError::InconsistentManifest`] if `orchestrator == Some(id)` while
    ///   `id` is **not** present in `entries` (referential integrity).
    pub fn with_orchestrator(
        version: u32,
        entries: Vec<ManifestEntry>,
        orchestrator: Option<AgentId>,
    ) -> Result<Self, DomainError> {
        let mut seen = std::collections::HashSet::new();
        for entry in &entries {
            if !seen.insert(entry.md_path.as_str()) {
                return Err(DomainError::InconsistentManifest {
                    reason: format!("duplicate md_path `{}`", entry.md_path),
                });
            }
        }
        if let Some(id) = orchestrator {
            if !entries.iter().any(|e| e.agent_id == id) {
                return Err(DomainError::InconsistentManifest {
                    reason: format!("designated orchestrator `{id}` is not a known agent"),
                });
            }
        }
        Ok(Self {
            version,
            entries,
            orchestrator,
        })
    }

    /// The **effective** orchestrator agent: the explicit designation if any,
    /// otherwise the **oldest** agent (`entries.first()`, by the creation-order
    /// invariant). `None` only when the manifest has no entries at all.
    #[must_use]
    pub fn effective_orchestrator(&self) -> Option<AgentId> {
        self.orchestrator
            .or_else(|| self.entries.first().map(|e| e.agent_id))
    }

    /// Folds the [`effective_orchestrator`](Self::effective_orchestrator) into the
    /// [`OrchestratorDesignation`] value object consumed by the FileGuard policy.
    #[must_use]
    pub fn orchestrator_designation(&self) -> crate::fileguard::OrchestratorDesignation {
        match self.effective_orchestrator() {
            Some(id) => crate::fileguard::OrchestratorDesignation::of(id),
            None => crate::fileguard::OrchestratorDesignation::none(),
        }
    }

    /// Designates `id` as the project's orchestrator (radio semantics: overwrites any
    /// previous designation).
    ///
    /// # Errors
    /// [`DomainError::InconsistentManifest`] if `id` is not a known agent of this
    /// manifest.
    pub fn designate(&mut self, id: AgentId) -> Result<(), DomainError> {
        if !self.entries.iter().any(|e| e.agent_id == id) {
            return Err(DomainError::InconsistentManifest {
                reason: format!("cannot designate unknown agent `{id}` as orchestrator"),
            });
        }
        self.orchestrator = Some(id);
        Ok(())
    }

    /// Reacts to the removal of an agent: if it was the **explicitly designated**
    /// orchestrator, clears the designation so the manifest falls back to the default
    /// (lazy succession — the next-oldest agent becomes effective). A no-op when the
    /// removed agent was not the designated one.
    ///
    /// Pure bookkeeping: it does **not** remove the entry itself (the caller owns
    /// `entries`); it only keeps the `orchestrator` deviation consistent.
    pub fn on_agent_deleted(&mut self, removed: AgentId) {
        if self.orchestrator == Some(removed) {
            self.orchestrator = None;
        }
    }
}

#[cfg(test)]
mod orchestrator_tests {
    use super::*;
    use crate::conversation::ConversationParty;
    use crate::fileguard::{may_write_directly, GuardedResource, OrchestratorDesignation};

    fn agent_id(n: u128) -> AgentId {
        AgentId::from_uuid(uuid::Uuid::from_u128(n))
    }

    /// Builds a manifest entry for `id`, with a `md_path` derived from `n` so entries
    /// stay unique.
    fn entry(n: u128) -> ManifestEntry {
        ManifestEntry::new(
            agent_id(n),
            format!("agent-{n}"),
            format!("agents/agent-{n}.md"),
            ProfileId::from_uuid(uuid::Uuid::from_u128(1000 + n)),
            None,
            false,
            None,
        )
        .unwrap()
    }

    #[test]
    fn default_orchestrator_is_oldest_agent_when_none_designated() {
        // entries are in creation order: first is the oldest → the effective default.
        let m = AgentManifest::new(1, vec![entry(1), entry(2), entry(3)]).unwrap();
        assert_eq!(m.orchestrator, None);
        assert_eq!(m.effective_orchestrator(), Some(agent_id(1)));
        assert_eq!(
            m.orchestrator_designation(),
            OrchestratorDesignation::of(agent_id(1))
        );
    }

    #[test]
    fn empty_manifest_has_no_effective_orchestrator() {
        let m = AgentManifest::new(1, vec![]).unwrap();
        assert_eq!(m.effective_orchestrator(), None);
        assert_eq!(
            m.orchestrator_designation(),
            OrchestratorDesignation::none()
        );
    }

    #[test]
    fn explicit_designation_overrides_oldest() {
        let mut m = AgentManifest::new(1, vec![entry(1), entry(2)]).unwrap();
        m.designate(agent_id(2)).unwrap();
        assert_eq!(m.orchestrator, Some(agent_id(2)));
        assert_eq!(m.effective_orchestrator(), Some(agent_id(2)));
    }

    #[test]
    fn designate_is_radio_and_rejects_unknown_agent() {
        let mut m = AgentManifest::new(1, vec![entry(1), entry(2)]).unwrap();
        m.designate(agent_id(1)).unwrap();
        // Radio: a second designation overwrites the first (never accumulates).
        m.designate(agent_id(2)).unwrap();
        assert_eq!(m.orchestrator, Some(agent_id(2)));
        // Referential: designating an agent absent from `entries` is rejected.
        let err = m.designate(agent_id(99)).unwrap_err();
        assert!(matches!(err, DomainError::InconsistentManifest { .. }));
        // …and the previous valid designation is untouched.
        assert_eq!(m.orchestrator, Some(agent_id(2)));
    }

    #[test]
    fn lazy_succession_falls_back_to_oldest_when_designated_is_deleted() {
        let mut m = AgentManifest::new(1, vec![entry(1), entry(2), entry(3)]).unwrap();
        m.designate(agent_id(3)).unwrap();
        // The designated agent is removed → designation cleared, default takes over.
        m.entries.retain(|e| e.agent_id != agent_id(3));
        m.on_agent_deleted(agent_id(3));
        assert_eq!(m.orchestrator, None);
        // Falls back to the (now) oldest remaining agent.
        assert_eq!(m.effective_orchestrator(), Some(agent_id(1)));
    }

    #[test]
    fn on_agent_deleted_is_noop_for_non_designated_agent() {
        let mut m = AgentManifest::new(1, vec![entry(1), entry(2)]).unwrap();
        m.designate(agent_id(2)).unwrap();
        m.on_agent_deleted(agent_id(1));
        // Deleting a non-designated agent leaves the designation intact.
        assert_eq!(m.orchestrator, Some(agent_id(2)));
    }

    #[test]
    fn constructor_enforces_referential_integrity_of_orchestrator() {
        // Some(id) with id present in entries → ok.
        assert!(
            AgentManifest::with_orchestrator(1, vec![entry(1), entry(2)], Some(agent_id(2)),)
                .is_ok()
        );
        // Some(id) with id absent → rejected.
        let err =
            AgentManifest::with_orchestrator(1, vec![entry(1)], Some(agent_id(42))).unwrap_err();
        assert!(matches!(err, DomainError::InconsistentManifest { .. }));
        // None is always valid (even with an empty manifest).
        assert!(AgentManifest::with_orchestrator(1, vec![], None).is_ok());
    }

    #[test]
    fn designated_agent_may_write_project_context_via_manifest_designation() {
        let mut m = AgentManifest::new(1, vec![entry(1), entry(2)]).unwrap();
        m.designate(agent_id(1)).unwrap();
        let d = m.orchestrator_designation();
        // The designated agent writes the project context directly…
        assert!(may_write_directly(
            ConversationParty::agent(agent_id(1)),
            &GuardedResource::ProjectContext,
            &d,
        ));
        // …another agent is refused (single-writer preserved)…
        assert!(!may_write_directly(
            ConversationParty::agent(agent_id(2)),
            &GuardedResource::ProjectContext,
            &d,
        ));
        // …and the human is always allowed.
        assert!(may_write_directly(
            ConversationParty::User,
            &GuardedResource::ProjectContext,
            &d,
        ));
    }
}
