//! Public data model for the user-facing agent registry.

use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AgentRegistrySource {
    Default,
    Custom,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, JsonSchema)]
pub struct AgentSubagentPolicy {
    /// Agent ids this agent may delegate to. Empty means no subagent calls.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub allowlist: Vec<String>,
}

impl AgentSubagentPolicy {
    pub fn from_allowlist(allowlist: Vec<String>) -> Self {
        Self { allowlist }
    }

    pub fn is_empty(&self) -> bool {
        self.allowlist.is_empty()
    }
}

impl<'de> Deserialize<'de> for AgentSubagentPolicy {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Wire {
            Section { allowlist: Vec<String> },
            LegacyList(Vec<String>),
        }

        match Wire::deserialize(deserializer)? {
            Wire::Section { allowlist } => Ok(Self { allowlist }),
            Wire::LegacyList(allowlist) => Ok(Self { allowlist }),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AgentRegistryEntry {
    /// Stable id used by routing and UI selection.
    pub id: String,
    /// User-facing name.
    pub name: String,
    /// Short explanation of when this agent should be used.
    pub description: String,
    /// Default agents ship with OpenHuman; custom agents are user-authored.
    pub source: AgentRegistrySource,
    /// Whether this agent is available for selection/delegation.
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// Optional model or route hint for this agent.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    /// Optional custom instructions. Default agents usually keep this empty
    /// because their runtime prompts are generated by the harness.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub system_prompt: Option<String>,
    /// Tool names this agent may use. `"*"` means all currently visible tools.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_allowlist: Vec<String>,
    /// Tool names that must be hidden even if they match the allowlist.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_denylist: Vec<String>,
    /// Subagent delegation policy. Only ids in `allowlist` may be spawned.
    #[serde(default, skip_serializing_if = "AgentSubagentPolicy::is_empty")]
    pub subagents: AgentSubagentPolicy,
    /// Search/filter tags for UI grouping.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
    /// Free-form UI/runtime metadata. Keep execution-critical fields typed.
    #[serde(default, skip_serializing_if = "Value::is_null")]
    pub metadata: Value,
}

impl AgentRegistryEntry {
    pub fn validate(&self) -> Result<(), String> {
        if self.id.trim().is_empty() {
            return Err("id is required".to_string());
        }
        if self
            .id
            .chars()
            .any(|ch| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'))
        {
            return Err("id may only contain ASCII letters, numbers, '_' and '-'".to_string());
        }
        if self.name.trim().is_empty() {
            return Err("name is required".to_string());
        }
        if self.description.trim().is_empty() {
            return Err("description is required".to_string());
        }
        for child in &self.subagents.allowlist {
            if child.trim().is_empty() {
                return Err("subagents.allowlist entries must not be blank".to_string());
            }
            if child
                .chars()
                .any(|ch| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '-'))
            {
                return Err(
                    "subagents.allowlist entries may only contain ASCII letters, numbers, '_' and '-'"
                        .to_string(),
                );
            }
        }
        Ok(())
    }
}

/// One available agent tool, surfaced to the agent editor so users can pick
/// `tool_allowlist` entries from a searchable catalog with descriptions.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AgentToolInfo {
    /// Tool name — the exact identifier used in `tool_allowlist`.
    pub name: String,
    /// Human-readable description of what the tool does.
    pub description: String,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AgentRegistryConfig {
    /// User-authored agents plus persisted overrides for shipped default agents.
    #[serde(default)]
    pub entries: Vec<AgentRegistryEntry>,
}

#[derive(Debug, Clone, Default, Deserialize)]
pub struct AgentRegistryPatch {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub description: Option<String>,
    #[serde(default)]
    pub enabled: Option<bool>,
    #[serde(default)]
    pub model: Option<String>,
    #[serde(default)]
    pub system_prompt: Option<String>,
    #[serde(default)]
    pub tool_allowlist: Option<Vec<String>>,
    #[serde(default)]
    pub tool_denylist: Option<Vec<String>>,
    #[serde(default)]
    pub subagents: Option<AgentSubagentPolicy>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
    #[serde(default)]
    pub metadata: Option<Value>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn subagent_policy_accepts_legacy_array() {
        let entry: AgentRegistryEntry = serde_json::from_value(json!({
            "id": "planner",
            "name": "Planner",
            "description": "Plans work.",
            "source": "custom",
            "enabled": true,
            "subagents": ["researcher", "critic"]
        }))
        .expect("legacy subagents array should parse");

        assert_eq!(
            entry.subagents.allowlist,
            vec!["researcher".to_string(), "critic".to_string()]
        );
    }

    #[test]
    fn subagent_policy_serializes_as_section() {
        let entry = AgentRegistryEntry {
            id: "planner".to_string(),
            name: "Planner".to_string(),
            description: "Plans work.".to_string(),
            source: AgentRegistrySource::Custom,
            enabled: true,
            model: None,
            system_prompt: None,
            tool_allowlist: Vec::new(),
            tool_denylist: Vec::new(),
            subagents: AgentSubagentPolicy::from_allowlist(vec!["researcher".to_string()]),
            tags: Vec::new(),
            metadata: Value::Null,
        };

        let value = serde_json::to_value(entry).expect("serialize entry");
        assert_eq!(
            value.get("subagents"),
            Some(&json!({ "allowlist": ["researcher"] }))
        );
    }
}
