//! # Tool registry — the FOB boundary for brain-proposed tool calls
//!
//! Per spec §6 + ADR-0007 ("rutster mediates both the provider call-control
//! API and the brain tap, so the brain never holds the wire"). The brain
//! proposes (via function_call events); the FOB disposes (via
//! function_call_output).
//!
//! `hangup` is the only wired tool in slice-3 (spec §6.3); other tool names
//! reply `not_implemented` so the model is free to retry or give up.

use async_trait::async_trait;
use rutster_call_model::ChannelId;
use serde_json::{Value, json};
use tracing::warn;

use crate::session_map::AppState;

/// A registry-dispatchable tool. The async-trait pattern is needed because
/// the registry holds `Vec<Box<dyn Tool>>` (stable Rust doesn't support
/// async fns in trait *objects* without `async-trait` as of Rust 1.85).
#[async_trait]
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    /// JSON-schema descriptor the registry sends to the brain on tools.update.
    fn schema(&self) -> Value;
    /// Execute the tool. The args Value is the raw JSON from the function_call
    /// event (the brain's translator extracts `arguments` from OpenAI's
    /// event and the registry hands it here verbatim).
    async fn call(&self, args: Value) -> ToolResult;
}

#[derive(Debug, Clone)]
pub enum ToolResult {
    Ok(Value),
    Error(String),
    NotImplemented,
}

impl ToolResult {
    /// Serialize to the (status, result) pair the binary's poll task will
    /// pass to `encode_function_call_output`.
    pub fn to_status_result(&self) -> (String, Value) {
        match self {
            ToolResult::Ok(v) => ("ok".to_string(), v.clone()),
            ToolResult::Error(msg) => ("error".to_string(), json!({ "error": msg })),
            ToolResult::NotImplemented => ("not_implemented".to_string(), Value::Null),
        }
    }
}

pub struct ToolRegistry {
    tools: Vec<Box<dyn Tool>>,
}

impl ToolRegistry {
    pub fn new() -> Self {
        Self { tools: Vec::new() }
    }
    pub fn register(&mut self, tool: Box<dyn Tool>) {
        self.tools.push(tool);
    }
    /// Dispatch by tool name. Returns `ToolResult::NotImplemented` if no
    /// tool with the given name is registered.
    pub async fn dispatch(&self, name: &str, args: Value) -> ToolResult {
        for tool in &self.tools {
            if tool.name() == name {
                return tool.call(args).await;
            }
        }
        warn!(tool = %name, "brain proposed unknown tool; returning not_implemented");
        ToolResult::NotImplemented
    }
    /// Serialize the catalog (used on startup + tools.update emission).
    pub fn catalog(&self) -> Vec<Value> {
        self.tools.iter().map(|t| t.schema()).collect()
    }
}

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

/// The `hangup` tool. Holds `AppState` (cloned — it's `Arc`-cheap) + the
/// `ChannelId` of the call this tool operates on. `call()` ->
/// `AppState::close(channel_id).await` -> returns `Ok({"channel_state": "Closing"})`.
pub struct HangupTool {
    app_state: AppState,
    channel_id: ChannelId,
}

impl HangupTool {
    pub fn new(app_state: AppState, channel_id: ChannelId) -> Self {
        Self {
            app_state,
            channel_id,
        }
    }
}

#[async_trait]
impl Tool for HangupTool {
    fn name(&self) -> &str {
        "hangup"
    }
    fn schema(&self) -> Value {
        json!({
            "name": "hangup",
            "description": "Hang up the current call.",
            "parameters": {
                "type": "object",
                "properties": {}
            }
        })
    }
    async fn call(&self, _args: Value) -> ToolResult {
        self.app_state.close(self.channel_id).await;
        ToolResult::Ok(json!({ "channel_state": "Closing" }))
    }
}

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

    /// A no-op tool whose `call` returns a fixed value — for testing the
    /// registry's dispatch + catalog shape without needing AppState.
    struct EchoTool {
        tool_name: String,
    }
    #[async_trait]
    impl Tool for EchoTool {
        fn name(&self) -> &str {
            &self.tool_name
        }
        fn schema(&self) -> Value {
            json!({ "name": self.tool_name, "description": "test tool" })
        }
        async fn call(&self, args: Value) -> ToolResult {
            ToolResult::Ok(args)
        }
    }

    #[tokio::test]
    async fn dispatch_known_tool_returns_ok() {
        let mut reg = ToolRegistry::new();
        reg.register(Box::new(EchoTool {
            tool_name: "echo".to_string(),
        }));
        let r = reg.dispatch("echo", json!({"x": 1})).await;
        let (status, result) = r.to_status_result();
        assert_eq!(status, "ok");
        assert_eq!(result, json!({"x": 1}));
    }

    #[tokio::test]
    async fn dispatch_unknown_tool_returns_not_implemented() {
        let mut reg = ToolRegistry::new();
        reg.register(Box::new(EchoTool {
            tool_name: "echo".to_string(),
        }));
        let r = reg.dispatch("not_registered", json!({})).await;
        let (status, _) = r.to_status_result();
        assert_eq!(status, "not_implemented");
    }

    #[tokio::test]
    async fn catalog_lists_all_registered_tools() {
        let mut reg = ToolRegistry::new();
        reg.register(Box::new(EchoTool {
            tool_name: "a".to_string(),
        }));
        reg.register(Box::new(EchoTool {
            tool_name: "b".to_string(),
        }));
        let cat = reg.catalog();
        assert_eq!(cat.len(), 2);
        assert_eq!(cat[0]["name"], "a");
        assert_eq!(cat[1]["name"], "b");
    }

    #[tokio::test]
    async fn hangup_tool_schema_shape() {
        // Don't construct a full HangupTool (needs AppState) — just verify
        // the schema shape via the impl on a standalone.
        let app_state = AppState::default();
        let h = HangupTool::new(app_state, ChannelId(uuid::Uuid::nil()));
        let s = h.schema();
        assert_eq!(s["name"], "hangup");
        assert!(s["description"].is_string());
        assert_eq!(s["parameters"]["type"], "object");
    }

    #[tokio::test]
    async fn hangup_tool_call_fires_app_state_close() {
        // AppState::close on a fake session_id that doesn't exist just
        // logs + no-ops (the `if let Some((_id, session_arc)) = ...` arm).
        // The tool returns Ok with channel_state: "Closing" regardless —
        // the dispatch boundary gives the brain a deterministic reply.
        let app_state = AppState::default();
        let h = HangupTool::new(app_state, ChannelId(uuid::Uuid::new_v4()));
        let r = h.call(json!({})).await;
        let (status, result) = r.to_status_result();
        assert_eq!(status, "ok");
        assert_eq!(result["channel_state"], "Closing");
    }
}
