//! Tool configuration container for managing a set of [`ToolDirective`]s.
//!
//! A [`ToolConfiguration`] holds a flat list of directives and provides
//! query helpers used by the ACP intercept layer, the UI, and session
//! persistence (via metadata helpers).

use super::directive::{ToolDirective, ToolHandler};
use serde::{Deserialize, Serialize};

/// Metadata key used for storing tool configuration in session metadata.
const METADATA_KEY: &str = "tool_configuration";

/// A collection of tool directives that together define the tool routing
/// policy for a session or connector.
///
/// # Examples
///
/// ```
/// use dirigent_core::tools::{ToolConfiguration, ToolDirective, ToolHandler};
///
/// let mut config = ToolConfiguration::new();
/// config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
///
/// assert!(config.should_intercept("shell_exec"));
/// assert!(!config.should_intercept("read_file"));
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct ToolConfiguration {
    /// The ordered list of tool directives.
    #[serde(default)]
    pub directives: Vec<ToolDirective>,
}

impl ToolConfiguration {
    /// Create an empty configuration with no directives.
    pub fn new() -> Self {
        Self::default()
    }

    /// Look up the directive for a given tool by its ID.
    pub fn get(&self, tool_id: &str) -> Option<&ToolDirective> {
        self.directives.iter().find(|d| d.tool_id == tool_id)
    }

    /// Look up the directive for a given tool by its ID (mutable).
    pub fn get_mut(&mut self, tool_id: &str) -> Option<&mut ToolDirective> {
        self.directives.iter_mut().find(|d| d.tool_id == tool_id)
    }

    /// Insert or replace a directive. If a directive for the same `tool_id`
    /// already exists, it is replaced in place; otherwise the new directive
    /// is appended.
    pub fn set(&mut self, directive: ToolDirective) {
        if let Some(existing) = self
            .directives
            .iter_mut()
            .find(|d| d.tool_id == directive.tool_id)
        {
            *existing = directive;
        } else {
            self.directives.push(directive);
        }
    }

    /// Returns `true` if the tool with the given ID has a checked directive
    /// with a non-Agent handler, meaning the call should be intercepted.
    ///
    /// Returns `false` for unknown tools (no directive) or passthrough
    /// directives.
    pub fn should_intercept(&self, tool_id: &str) -> bool {
        self.get(tool_id)
            .is_some_and(|d| d.checked && d.handler != ToolHandler::Agent)
    }

    /// Returns the active handler for a tool, if it has a checked directive.
    ///
    /// Returns `None` for unknown tools or passthrough directives.
    pub fn active_handler(&self, tool_id: &str) -> Option<&ToolHandler> {
        self.get(tool_id)
            .filter(|d| d.checked)
            .map(|d| &d.handler)
    }

    /// Returns the tool IDs of all directives that are checked (intercepted).
    pub fn checked_tools(&self) -> Vec<&str> {
        self.directives
            .iter()
            .filter(|d| d.checked)
            .map(|d| d.tool_id.as_str())
            .collect()
    }

    // -- Metadata helpers for session persistence --

    /// Deserialize a `ToolConfiguration` from the `"tool_configuration"` key
    /// in a session metadata JSON value.
    ///
    /// Returns `None` if the key is absent or the value cannot be deserialized.
    pub fn from_metadata(metadata: &serde_json::Value) -> Option<Self> {
        metadata
            .get(METADATA_KEY)
            .and_then(|v| serde_json::from_value(v.clone()).ok())
    }

    /// Serialize this configuration into the `"tool_configuration"` key of
    /// a session metadata JSON object.
    ///
    /// If `metadata` is not already an object, this is a no-op.
    pub fn to_metadata(&self, metadata: &mut serde_json::Value) {
        if let Some(obj) = metadata.as_object_mut() {
            if let Ok(value) = serde_json::to_value(self) {
                obj.insert(METADATA_KEY.to_string(), value);
            }
        }
    }
}

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

    #[test]
    fn empty_config_no_interception() {
        let config = ToolConfiguration::new();
        assert!(!config.should_intercept("any_tool"));
        assert!(config.active_handler("any_tool").is_none());
        assert!(config.checked_tools().is_empty());
    }

    #[test]
    fn passthrough_directive() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::passthrough("read_file"));

        assert!(!config.should_intercept("read_file"));
        // active_handler returns None for passthrough (not checked)
        assert!(config.active_handler("read_file").is_none());
        assert!(config.checked_tools().is_empty());
    }

    #[test]
    fn checked_directive_deny() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));

        assert!(config.should_intercept("shell_exec"));
        assert_eq!(config.active_handler("shell_exec"), Some(&ToolHandler::Deny));
        assert_eq!(config.checked_tools(), vec!["shell_exec"]);
    }

    #[test]
    fn checked_directive_hide() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::checked("dangerous_tool", ToolHandler::Hide));

        assert!(config.should_intercept("dangerous_tool"));
        assert_eq!(
            config.active_handler("dangerous_tool"),
            Some(&ToolHandler::Hide)
        );
    }

    #[test]
    fn checked_directive_agent_not_intercepted() {
        // A checked directive with Agent handler should NOT be intercepted
        // (Agent means passthrough even when checked).
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::checked("file_write", ToolHandler::Agent));

        assert!(!config.should_intercept("file_write"));
        assert_eq!(
            config.active_handler("file_write"),
            Some(&ToolHandler::Agent)
        );
        assert_eq!(config.checked_tools(), vec!["file_write"]);
    }

    #[test]
    fn set_replaces_existing() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
        assert!(config.should_intercept("shell_exec"));

        // Replace with passthrough
        config.set(ToolDirective::passthrough("shell_exec"));
        assert!(!config.should_intercept("shell_exec"));

        // Should still be exactly one directive
        assert_eq!(config.directives.len(), 1);
    }

    #[test]
    fn checked_tools_list() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::passthrough("read_file"));
        config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
        config.set(ToolDirective::checked("file_write", ToolHandler::Editor));
        config.set(ToolDirective::passthrough("search"));

        let checked = config.checked_tools();
        assert_eq!(checked.len(), 2);
        assert!(checked.contains(&"shell_exec"));
        assert!(checked.contains(&"file_write"));
    }

    #[test]
    fn get_and_get_mut() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::passthrough("read_file"));

        assert!(config.get("read_file").is_some());
        assert!(config.get("nonexistent").is_none());

        // Mutate via get_mut
        if let Some(d) = config.get_mut("read_file") {
            d.checked = true;
            d.handler = ToolHandler::Dirigent;
        }
        assert!(config.should_intercept("read_file"));
    }

    #[test]
    fn serde_roundtrip() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::passthrough("read_file"));
        config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
        config.set(ToolDirective::checked(
            "custom_tool",
            ToolHandler::Plugin { name: "my_plugin".to_string() },
        ));

        let json = serde_json::to_string_pretty(&config).expect("serialize");
        let deserialized: ToolConfiguration =
            serde_json::from_str(&json).expect("deserialize");
        assert_eq!(config, deserialized);
    }

    #[test]
    fn serde_roundtrip_plugin_variant() {
        let handler = ToolHandler::Plugin { name: "security_scanner".to_string() };
        let json = serde_json::to_string(&handler).expect("serialize");

        // Verify the tagged representation
        assert!(json.contains("Plugin"));
        assert!(json.contains("security_scanner"));

        let deserialized: ToolHandler = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(handler, deserialized);
    }

    #[test]
    fn from_metadata_roundtrip() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::checked("shell_exec", ToolHandler::Deny));
        config.set(ToolDirective::checked(
            "plugin_tool",
            ToolHandler::Plugin { name: "my_plugin".to_string() },
        ));

        // Write to metadata
        let mut metadata = serde_json::json!({ "other_key": "other_value" });
        config.to_metadata(&mut metadata);

        // Read back from metadata
        let restored = ToolConfiguration::from_metadata(&metadata)
            .expect("should deserialize from metadata");
        assert_eq!(config, restored);

        // Original metadata keys are preserved
        assert_eq!(metadata["other_key"], "other_value");
    }

    #[test]
    fn from_metadata_missing_key() {
        let metadata = serde_json::json!({ "something_else": 42 });
        assert!(ToolConfiguration::from_metadata(&metadata).is_none());
    }

    #[test]
    fn to_metadata_noop_on_non_object() {
        let mut config = ToolConfiguration::new();
        config.set(ToolDirective::passthrough("tool"));

        // Attempting to write to a non-object value should be a no-op
        let mut metadata = serde_json::json!("not an object");
        config.to_metadata(&mut metadata);
        assert_eq!(metadata, serde_json::json!("not an object"));
    }
}
