//! `Registry` — central dispatch table mapping tool names to their implementations.
//!
//! Build-time workflow:
//! 1. Construct `Registry::new()`.
//! 2. Call `register(Arc::new(MyTool))` once per tool — duplicates fail.
//! 3. Wrap in `Arc<Registry>` and share across async handlers.
//!
//! Dispatch-time workflow:
//! - Surfaces call `invoke(name, params, ctx)` to execute.
//! - `list()` / `get()` / `schema_of()` power `tools/list` and schema endpoints.
//!
//! RBAC is NOT enforced here — callers (HTTP middleware) gate on
//! `UserContext::is_operator`/`is_admin` vs `ToolCategory`. Keeping the Registry
//! dumb lets unauthenticated surfaces (local CLI, MCP stdio) reuse it verbatim.

use crate::protocol::ToolError;
use crate::providers::hcloud::HcloudClient;
use crate::sources::{NclCache, OrchestratorClient, VaultSource};
use crate::tool::{Tool, ToolMetadata};
use crate::tools::{
    agents::{AgentList, AgentRun, AgentStatus},
    cluster::{ClusterCreate, ClusterDescribePod, ClusterList, ClusterPods, ClusterShow},
    component::{ComponentList, ComponentShow},
    dag::{DagExport, DagShow, DagValidate},
    dashboard::{DashboardAudit, DashboardServices, DashboardStatistics, DashboardWorkflows},
    extension::{ExtensionCapabilities, ExtensionList, ExtensionSearch, ExtensionShow},
    guidance::{
        GuidanceConfigValidate, GuidanceDocFind, GuidanceNextAction, GuidanceSystemStatus,
        GuidanceTroubleshoot,
    },
    infra::{InfraDetect, InfraReconcile, InfraStatus, InfraValidate},
    installer::{InstallerSettingsDefaults, InstallerSettingsGet, InstallerSettingsValidate},
    monitoring::{MonitoringHealth, MonitoringMetrics, MonitoringStatistics},
    ontology::{OntologyGraph, OntologyNode, OntologySearch},
    orchestrator::{
        OrchestratorJobCancel, OrchestratorJobShow, OrchestratorJobs, OrchestratorRunWorkflow,
    },
    server::{ServerCreate, ServerDelete, ServerList, ServerShow},
    taskserv::TaskservDeploy,
    vault::{VaultGet, VaultList, VaultSealStatus, VaultUnseal},
    volume::{VolumeList, VolumeShow},
    workspace::{
        WorkspaceActive, WorkspaceDag, WorkspaceList, WorkspaceRegister, WorkspaceShow,
        WorkspaceValidate,
    },
};
use std::collections::HashMap;
use std::sync::Arc;

#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
    #[error("tool '{0}' already registered")]
    Duplicate(&'static str),
}

pub struct Registry {
    tools: HashMap<&'static str, Arc<dyn Tool>>,
    meta: HashMap<&'static str, ToolMetadata>,
}

impl Registry {
    pub fn new() -> Self {
        Self { tools: HashMap::new(), meta: HashMap::new() }
    }

    /// Register a tool. Fails if `name()` collides with a previously registered one.
    ///
    /// `schema()` is invoked exactly once here and cached in an `Arc<Value>` so that
    /// `tools/list` and schema endpoints pay zero serialization cost per request.
    pub fn register(&mut self, tool: Arc<dyn Tool>) -> Result<(), RegistryError> {
        let name = tool.name();
        if self.tools.contains_key(name) {
            return Err(RegistryError::Duplicate(name));
        }
        let meta = ToolMetadata {
            name,
            description: tool.description(),
            category: tool.category(),
            schema: Arc::new(tool.schema()),
        };
        self.tools.insert(name, tool);
        self.meta.insert(name, meta);
        Ok(())
    }

    pub fn len(&self) -> usize {
        self.tools.len()
    }

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

    pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> {
        self.tools.get(name).cloned()
    }

    pub fn metadata(&self, name: &str) -> Option<&ToolMetadata> {
        self.meta.get(name)
    }

    pub fn schema_of(&self, name: &str) -> Option<Arc<serde_json::Value>> {
        self.meta.get(name).map(|m| Arc::clone(&m.schema))
    }

    /// Return all tool metadata sorted alphabetically by name.
    ///
    /// Stable ordering matters for deterministic `tools/list` output and for
    /// the G3 contract test (CLI↔HTTP↔MCP diff).
    pub fn list(&self) -> Vec<&ToolMetadata> {
        let mut items: Vec<&ToolMetadata> = self.meta.values().collect();
        items.sort_by_key(|m| m.name);
        items
    }

    /// Dispatch. Does not perform authorization — callers handle that.
    pub async fn invoke(
        &self,
        name: &str,
        params: serde_json::Value,
        ctx: &crate::tool::Context,
    ) -> Result<serde_json::Value, ToolError> {
        let tool = self
            .get(name)
            .ok_or_else(|| ToolError::not_found(format!("tool '{name}'")))?;
        tool.invoke(params, ctx).await
    }
}

impl Default for Registry {
    fn default() -> Self {
        Self::new()
    }
}

impl Registry {
    /// Construct a `Registry` pre-loaded with every tool implementation.
    ///
    /// `vault` is optional — surfaces that start without a configured KMS backend
    /// (local dev, CI) pass `None`; vault tools are skipped in that case.
    pub fn with_all_tools(
        cache: Arc<NclCache>,
        orch: Arc<OrchestratorClient>,
        hcloud: Arc<HcloudClient>,
        vault: Option<Arc<VaultSource>>,
    ) -> Result<Self, RegistryError> {
        let mut reg = Registry::new();

        // agents (3)
        reg.register(Arc::new(AgentList::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(AgentStatus::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(AgentRun::new()))?;

        // cluster (5)
        reg.register(Arc::new(ClusterList::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ClusterShow::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ClusterCreate::new()))?;
        reg.register(Arc::new(ClusterPods::new()))?;
        reg.register(Arc::new(ClusterDescribePod::new()))?;

        // component (2)
        reg.register(Arc::new(ComponentList::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ComponentShow::new(Arc::clone(&cache))))?;

        // dag (3)
        reg.register(Arc::new(DagShow::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(DagValidate::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(DagExport::new(Arc::clone(&cache))))?;

        // dashboard (4)
        reg.register(Arc::new(DashboardServices::new()))?;
        reg.register(Arc::new(DashboardWorkflows::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(DashboardAudit::new()))?;
        reg.register(Arc::new(DashboardStatistics::new(Arc::clone(&orch))))?;

        // extension (4)
        reg.register(Arc::new(ExtensionList::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ExtensionShow::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ExtensionSearch::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(ExtensionCapabilities::new(Arc::clone(&cache))))?;

        // guidance (5)
        reg.register(Arc::new(GuidanceSystemStatus::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(GuidanceDocFind::new()))?;
        reg.register(Arc::new(GuidanceNextAction::new(Arc::clone(&orch))))?;
        let mut troubleshoot = GuidanceTroubleshoot::new(Arc::clone(&orch));
        if let Some(v) = &vault {
            troubleshoot = troubleshoot.with_vault(Arc::clone(v));
        }
        reg.register(Arc::new(troubleshoot))?;
        reg.register(Arc::new(GuidanceConfigValidate::new(Arc::clone(&cache))))?;

        // infra (4)
        reg.register(Arc::new(InfraStatus::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(InfraDetect::new()))?;
        reg.register(Arc::new(InfraValidate::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(InfraReconcile::new()))?;

        // installer (3)
        reg.register(Arc::new(InstallerSettingsGet::new()))?;
        reg.register(Arc::new(InstallerSettingsValidate::new()))?;
        reg.register(Arc::new(InstallerSettingsDefaults::new()))?;

        // monitoring (3)
        reg.register(Arc::new(MonitoringMetrics::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(MonitoringHealth::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(MonitoringStatistics::new()))?;

        // ontology (3)
        reg.register(Arc::new(OntologySearch::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(OntologyNode::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(OntologyGraph::new(Arc::clone(&cache))))?;

        // orchestrator (4)
        reg.register(Arc::new(OrchestratorJobs::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(OrchestratorJobShow::new(Arc::clone(&orch))))?;
        reg.register(Arc::new(OrchestratorJobCancel::new()))?;
        reg.register(Arc::new(OrchestratorRunWorkflow::new()))?;

        // server (4)
        reg.register(Arc::new(ServerList::new(Arc::clone(&hcloud))))?;
        reg.register(Arc::new(ServerShow::new(Arc::clone(&hcloud))))?;
        reg.register(Arc::new(ServerCreate::new()))?;
        reg.register(Arc::new(ServerDelete::new(Arc::clone(&hcloud))))?;

        // volume (2)
        reg.register(Arc::new(VolumeList::new(Arc::clone(&hcloud))))?;
        reg.register(Arc::new(VolumeShow::new(Arc::clone(&hcloud))))?;

        // taskserv (1)
        reg.register(Arc::new(TaskservDeploy::new()))?;

        // vault (4) — only when backend is available
        if let Some(v) = vault {
            reg.register(Arc::new(VaultSealStatus::new(Arc::clone(&v))))?;
            reg.register(Arc::new(VaultList::new(Arc::clone(&v))))?;
            reg.register(Arc::new(VaultGet::new(Arc::clone(&v))))?;
            reg.register(Arc::new(VaultUnseal::new(Arc::clone(&v))))?;
        }

        // workspace (6)
        reg.register(Arc::new(WorkspaceList))?;
        reg.register(Arc::new(WorkspaceShow::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(WorkspaceActive))?;
        reg.register(Arc::new(WorkspaceRegister))?;
        reg.register(Arc::new(WorkspaceDag::new(Arc::clone(&cache))))?;
        reg.register(Arc::new(WorkspaceValidate::new(Arc::clone(&cache))))?;

        Ok(reg)
    }
}

impl std::fmt::Debug for Registry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Registry")
            .field("tool_count", &self.tools.len())
            .field("tools", &self.meta.keys().collect::<Vec<_>>())
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::{Environment, ToolCategory};
    use crate::tool::test_support::{EchoTool, FailingTool};
    use crate::tool::Context;
    use serde_json::json;

    fn ctx() -> Context {
        Context::new(Arc::new(Environment::default()))
    }

    #[test]
    fn register_and_list() {
        let mut reg = Registry::new();
        reg.register(Arc::new(EchoTool)).unwrap();
        reg.register(Arc::new(FailingTool)).unwrap();
        let list = reg.list();
        assert_eq!(list.len(), 2);
        assert_eq!(list[0].name, "echo");
        assert_eq!(list[1].name, "failing");
        assert_eq!(list[1].category, ToolCategory::Mutation);
    }

    #[test]
    fn duplicate_registration_fails() {
        let mut reg = Registry::new();
        reg.register(Arc::new(EchoTool)).unwrap();
        let err = reg.register(Arc::new(EchoTool)).unwrap_err();
        assert!(matches!(err, RegistryError::Duplicate("echo")));
    }

    #[test]
    fn schema_of_present_and_absent() {
        let mut reg = Registry::new();
        reg.register(Arc::new(EchoTool)).unwrap();
        let s = reg.schema_of("echo").unwrap();
        assert_eq!(s["type"], "object");
        assert!(reg.schema_of("nonexistent").is_none());
    }

    #[tokio::test]
    async fn invoke_unknown_returns_not_found() {
        let reg = Registry::new();
        let err = reg.invoke("missing", json!({}), &ctx()).await.unwrap_err();
        assert!(matches!(err, ToolError::NotFound(_)));
    }

    #[tokio::test]
    async fn invoke_echo_returns_params() {
        let mut reg = Registry::new();
        reg.register(Arc::new(EchoTool)).unwrap();
        let input = json!({"x": 1, "y": "z"});
        let out = reg.invoke("echo", input.clone(), &ctx()).await.unwrap();
        assert_eq!(out, input);
    }

    #[tokio::test]
    async fn invoke_failing_bubbles_error() {
        let mut reg = Registry::new();
        reg.register(Arc::new(FailingTool)).unwrap();
        let err = reg.invoke("failing", json!({}), &ctx()).await.unwrap_err();
        assert!(matches!(err, ToolError::NotFound(_)));
    }

    #[tokio::test]
    async fn shared_registry_across_tasks() {
        let mut reg = Registry::new();
        reg.register(Arc::new(EchoTool)).unwrap();
        let reg = Arc::new(reg);
        let handles: Vec<_> = (0..8)
            .map(|i| {
                let reg = Arc::clone(&reg);
                tokio::spawn(async move {
                    let out = reg
                        .invoke("echo", json!({"i": i}), &ctx())
                        .await
                        .unwrap();
                    assert_eq!(out["i"], i);
                })
            })
            .collect();
        for h in handles {
            h.await.unwrap();
        }
    }
}

// ── with_all_tools integration tests ─────────────────────────────────────────

#[cfg(test)]
mod integration {
    use super::*;
    use crate::protocol::ToolCategory;
    use crate::sources::NclCache;

    fn make_registry() -> Registry {
        let cache = Arc::new(NclCache::new());
        let orch = Arc::new(OrchestratorClient::new("http://localhost:19999"));
        let hcloud = Arc::new(HcloudClient::new());
        Registry::with_all_tools(cache, orch, hcloud, None).expect("with_all_tools failed")
    }

    #[test]
    fn with_all_tools_no_vault_registers_56_tools() {
        let reg = make_registry();
        // 60 total - 4 vault = 56 when vault = None
        assert_eq!(reg.len(), 56, "expected 56 tools (no vault); got {}", reg.len());
    }

    #[test]
    fn all_tool_names_follow_domain_action_pattern() {
        let reg = make_registry();
        for meta in reg.list() {
            assert!(
                meta.name.contains('_'),
                "tool '{}' does not follow <domain>_<action> naming",
                meta.name
            );
            assert!(
                meta.name.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
                "tool '{}' contains non-lowercase or non-underscore characters",
                meta.name
            );
        }
    }

    #[test]
    fn list_is_alphabetically_sorted() {
        let reg = make_registry();
        let names: Vec<&str> = reg.list().iter().map(|m| m.name).collect();
        let mut sorted = names.clone();
        sorted.sort_unstable();
        assert_eq!(names, sorted, "registry list is not alphabetically sorted");
    }

    #[test]
    fn all_tools_have_object_schema() {
        let reg = make_registry();
        for meta in reg.list() {
            let schema = reg.schema_of(meta.name).unwrap();
            assert_eq!(
                schema["type"], "object",
                "tool '{}' schema is not type:object",
                meta.name
            );
        }
    }

    #[test]
    fn no_duplicate_names_in_list() {
        let reg = make_registry();
        let names: Vec<&str> = reg.list().iter().map(|m| m.name).collect();
        let unique: std::collections::HashSet<&str> = names.iter().cloned().collect();
        assert_eq!(names.len(), unique.len(), "duplicate tool names detected");
    }

    #[test]
    fn read_tools_do_not_require_operator() {
        let reg = make_registry();
        for meta in reg.list() {
            if meta.category == ToolCategory::Read {
                assert!(
                    !meta.category.requires_operator(),
                    "Read tool '{}' incorrectly requires_operator",
                    meta.name
                );
            }
        }
    }

    #[test]
    fn destructive_tools_require_admin() {
        let reg = make_registry();
        for meta in reg.list() {
            if meta.category == ToolCategory::Destructive {
                assert!(
                    meta.category.requires_admin(),
                    "Destructive tool '{}' does not require_admin",
                    meta.name
                );
            }
        }
    }
}
