//! Tool definitions for the Anthropic Messages API
//!
//! This module contains types for defining tools that Claude can use,
//! including tool schemas and tool choice configuration.
//!
//! # Example
//!
//! ```
//! use mixtape_anthropic_sdk::{Tool, ToolInputSchema, ToolChoice};
//!
//! let tool = Tool {
//!     name: "get_weather".to_string(),
//!     description: Some("Get the current weather for a location".to_string()),
//!     input_schema: ToolInputSchema::new(),
//!     cache_control: None,
//!     tool_type: None,
//! };
//! ```

use crate::messages::CacheControl;
use serde::Serialize;
use serde_json::Value;

/// Tool definition
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Tool {
    /// Name of the tool
    pub name: String,

    /// Description of what the tool does
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// JSON schema for the tool's input parameters
    pub input_schema: ToolInputSchema,

    /// Cache control for this tool definition
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cache_control: Option<CacheControl>,

    /// Tool type (defaults to "custom" if not specified)
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub tool_type: Option<String>,
}

impl Tool {
    /// Create a new tool with the given name, description, and input schema
    ///
    /// # Example
    ///
    /// ```
    /// use mixtape_anthropic_sdk::{Tool, ToolInputSchema};
    ///
    /// let tool = Tool::new(
    ///     "get_weather",
    ///     "Get the current weather for a location",
    ///     ToolInputSchema::new(),
    /// );
    /// ```
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        input_schema: ToolInputSchema,
    ) -> Self {
        Self {
            name: name.into(),
            description: Some(description.into()),
            input_schema,
            cache_control: None,
            tool_type: None,
        }
    }

    /// Create a new tool with cache control
    pub fn with_cache_control(mut self, cache_control: CacheControl) -> Self {
        self.cache_control = Some(cache_control);
        self
    }
}

/// JSON schema for tool input
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolInputSchema {
    /// Always "object" for tool inputs
    #[serde(rename = "type")]
    pub schema_type: String,

    /// Property definitions
    #[serde(skip_serializing_if = "Option::is_none")]
    pub properties: Option<serde_json::Map<String, Value>>,

    /// Required property names
    #[serde(skip_serializing_if = "Option::is_none")]
    pub required: Option<Vec<String>>,

    /// Additional schema properties (additionalProperties, etc.)
    #[serde(flatten)]
    pub additional: serde_json::Map<String, Value>,
}

impl ToolInputSchema {
    /// Create a new empty object schema
    pub fn new() -> Self {
        Self {
            schema_type: "object".to_string(),
            properties: None,
            required: None,
            additional: serde_json::Map::new(),
        }
    }

    /// Create a schema with properties
    pub fn with_properties(
        properties: serde_json::Map<String, Value>,
        required: Vec<String>,
    ) -> Self {
        Self {
            schema_type: "object".to_string(),
            properties: Some(properties),
            required: if required.is_empty() {
                None
            } else {
                Some(required)
            },
            additional: serde_json::Map::new(),
        }
    }

    /// Create a ToolInputSchema from a schemars RootSchema
    ///
    /// This converts a schema generated by `schemars::schema_for!()` into
    /// the format expected by the Anthropic API. Requires the `schemars` feature.
    ///
    /// # Example
    ///
    /// ```no_run
    /// // Enable with: cargo add mixtape-anthropic-sdk --features schemars
    /// # #[cfg(feature = "schemars")]
    /// # fn example() {
    /// use schemars::schema_for;
    /// use mixtape_anthropic_sdk::ToolInputSchema;
    ///
    /// #[derive(schemars::JsonSchema)]
    /// struct WeatherParams {
    ///     location: String,
    ///     unit: Option<String>,
    /// }
    ///
    /// let schema = schema_for!(WeatherParams);
    /// let tool_schema = ToolInputSchema::from_root_schema(&schema);
    /// # }
    /// ```
    #[cfg(feature = "schemars")]
    pub fn from_root_schema(schema: &schemars::schema::RootSchema) -> Self {
        // Convert the root schema to a Value, then extract what we need
        let schema_value =
            serde_json::to_value(schema).unwrap_or(Value::Object(Default::default()));

        let mut additional = serde_json::Map::new();

        // Extract properties
        let properties = schema_value
            .get("properties")
            .and_then(|v| v.as_object())
            .cloned();

        // Extract required
        let required = schema_value
            .get("required")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            });

        // Copy over definitions if present (for nested types)
        if let Some(defs) = schema_value.get("definitions") {
            additional.insert("definitions".to_string(), defs.clone());
        }
        if let Some(defs) = schema_value.get("$defs") {
            additional.insert("$defs".to_string(), defs.clone());
        }

        // Copy additionalProperties if present
        if let Some(ap) = schema_value.get("additionalProperties") {
            additional.insert("additionalProperties".to_string(), ap.clone());
        }

        Self {
            schema_type: "object".to_string(),
            properties,
            required,
            additional,
        }
    }
}

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

/// How the model should use tools
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ToolChoice {
    /// Model decides whether to use tools
    Auto {
        #[serde(skip_serializing_if = "Option::is_none")]
        disable_parallel_tool_use: Option<bool>,
    },
    /// Model must use at least one tool
    Any {
        #[serde(skip_serializing_if = "Option::is_none")]
        disable_parallel_tool_use: Option<bool>,
    },
    /// Model must use the specified tool
    Tool {
        name: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        disable_parallel_tool_use: Option<bool>,
    },
    /// Model cannot use tools
    None,
}

impl ToolChoice {
    /// Create auto tool choice
    pub fn auto() -> Self {
        Self::Auto {
            disable_parallel_tool_use: None,
        }
    }

    /// Create any tool choice
    pub fn any() -> Self {
        Self::Any {
            disable_parallel_tool_use: None,
        }
    }

    /// Create tool choice for a specific tool
    pub fn tool(name: impl Into<String>) -> Self {
        Self::Tool {
            name: name.into(),
            disable_parallel_tool_use: None,
        }
    }

    /// Create none tool choice
    pub fn none() -> Self {
        Self::None
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_tool_input_schema_new() {
        let schema = ToolInputSchema::new();
        assert_eq!(schema.schema_type, "object");
        assert!(schema.properties.is_none());
        assert!(schema.required.is_none());
    }

    #[test]
    fn test_tool_input_schema_with_properties() {
        let mut props = serde_json::Map::new();
        props.insert(
            "location".to_string(),
            serde_json::json!({"type": "string"}),
        );

        let schema = ToolInputSchema::with_properties(props, vec!["location".to_string()]);
        assert!(schema.properties.is_some());
        assert_eq!(schema.required, Some(vec!["location".to_string()]));
    }

    #[test]
    fn test_tool_choice_serialization() {
        let auto = ToolChoice::auto();
        let json = serde_json::to_string(&auto).unwrap();
        assert!(json.contains("\"type\":\"auto\""));

        let tool = ToolChoice::tool("get_weather");
        let json = serde_json::to_string(&tool).unwrap();
        assert!(json.contains("\"type\":\"tool\""));
        assert!(json.contains("\"name\":\"get_weather\""));
    }

    #[test]
    fn test_tool_serialization() {
        let tool = Tool {
            name: "get_weather".to_string(),
            description: Some("Get weather".to_string()),
            input_schema: ToolInputSchema::new(),
            cache_control: None,
            tool_type: None,
        };
        let json = serde_json::to_string(&tool).unwrap();
        assert!(json.contains("\"name\":\"get_weather\""));
        assert!(json.contains("\"description\":\"Get weather\""));
    }

    #[test]
    fn test_tool_new() {
        let tool = Tool::new(
            "get_weather",
            "Get the current weather",
            ToolInputSchema::new(),
        );

        assert_eq!(tool.name, "get_weather");
        assert_eq!(
            tool.description,
            Some("Get the current weather".to_string())
        );
        assert!(tool.cache_control.is_none());
        assert!(tool.tool_type.is_none());
    }

    #[test]
    fn test_tool_new_string_conversions() {
        // Test that Into<String> works with various input types
        let str_name: &str = "tool_from_str";
        let string_desc: String = "Description from String".to_string();

        let tool = Tool::new(str_name, string_desc, ToolInputSchema::new());

        assert_eq!(tool.name, "tool_from_str");
        assert_eq!(
            tool.description,
            Some("Description from String".to_string())
        );
    }

    #[test]
    fn test_tool_with_cache_control() {
        let cache_ctrl = CacheControl::ephemeral();
        let tool = Tool::new("my_tool", "A tool", ToolInputSchema::new())
            .with_cache_control(cache_ctrl.clone());

        assert_eq!(tool.name, "my_tool");
        assert!(tool.cache_control.is_some());
        assert_eq!(tool.cache_control.unwrap().cache_type, "ephemeral");
    }

    #[test]
    fn test_tool_with_cache_control_chaining() {
        // Test that with_cache_control returns Self for method chaining
        let tool = Tool::new("tool", "desc", ToolInputSchema::new())
            .with_cache_control(CacheControl::ephemeral_5m());

        assert!(tool.cache_control.is_some());
    }

    #[test]
    fn test_tool_new_serialization() {
        // Test that Tool::new creates properly serializable objects
        let tool = Tool::new(
            "calculator",
            "Performs calculations",
            ToolInputSchema::new(),
        );

        let json = serde_json::to_string(&tool).unwrap();
        assert!(json.contains("\"name\":\"calculator\""));
        assert!(json.contains("\"description\":\"Performs calculations\""));
        assert!(json.contains("\"input_schema\""));

        // Optional tool-level fields should not be present
        assert!(!json.contains("\"cache_control\""));

        // Note: input_schema.type will be present (it's required for JSON schema)
        // but tool.type (the tool_type field) should not be present
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(
            parsed.get("type").is_none(),
            "tool_type should not be serialized when None"
        );
        assert!(
            parsed["input_schema"].get("type").is_some(),
            "input_schema.type should be present"
        );
    }

    #[test]
    fn test_tool_with_cache_control_serialization() {
        let tool = Tool::new("tool", "desc", ToolInputSchema::new())
            .with_cache_control(CacheControl::ephemeral());

        let json = serde_json::to_string(&tool).unwrap();
        assert!(json.contains("\"cache_control\""));
        assert!(json.contains("\"type\":\"ephemeral\""));
    }

    #[cfg(feature = "schemars")]
    #[test]
    fn test_from_root_schema() {
        use schemars::{schema_for, JsonSchema};

        #[derive(JsonSchema)]
        #[allow(dead_code)] // Fields used by schemars for schema generation
        struct TestParams {
            location: String,
            unit: Option<String>,
        }

        let root_schema = schema_for!(TestParams);
        let tool_schema = ToolInputSchema::from_root_schema(&root_schema);

        assert_eq!(tool_schema.schema_type, "object");
        assert!(tool_schema.properties.is_some());

        let props = tool_schema.properties.as_ref().unwrap();
        assert!(props.contains_key("location"));
        assert!(props.contains_key("unit"));

        // location should be required, unit should not be
        assert!(tool_schema.required.is_some());
        let required = tool_schema.required.as_ref().unwrap();
        assert!(required.contains(&"location".to_string()));
        assert!(!required.contains(&"unit".to_string()));
    }
}
