//! MCP Server configuration structures
//!
//! This module provides configuration compatible with Claude Desktop/Cursor format:
//! - Uses `mcpServers` object (HashMap) instead of array
//! - Uses `isActive` instead of `enabled`
//! - Uses `baseUrl` instead of `url` for HTTP
//! - Uses `streamableHttp` type (compatible with Claude Desktop)
//! - Supports `${ENV_VAR}` placeholder syntax for headers

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// MCP integration configuration
///
/// Compatible with Claude Desktop format using `mcpServers` object.
/// Example config:
/// ```json
/// {
///   "mcpServers": {
///     "WebSearch": {
///       "type": "streamableHttp",
///       "baseUrl": "https://api.example.com/mcp",
///       "headers": { "Authorization": "Bearer ${API_KEY}" },
///       "isActive": true,
///       "name": "WebSearch"
///     }
///   }
/// }
/// ```
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct McpConfig {
    /// MCP servers as a map (Claude Desktop compatible format)
    /// The key is used as the server identifier
    #[serde(default, rename = "mcpServers")]
    pub mcp_servers: HashMap<String, McpServerConfig>,
}

/// Configuration for a single MCP server
///
/// Supports both stdio and HTTP (streamableHttp) transports.
/// Configuration is flattened (no nested transport object).
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct McpServerConfig {
    /// Server name (optional, defaults to the key in mcpServers)
    #[serde(default)]
    pub name: Option<String>,

    /// Transport type: "stdio" or "streamableHttp" (or "http")
    #[serde(rename = "type")]
    pub transport_type: String,

    /// Whether this server is active (Claude Desktop compatible)
    #[serde(default = "default_is_active", alias = "enabled", alias = "isActive")]
    pub is_active: bool,

    // Stdio transport fields
    /// Command to execute for stdio transport (e.g., "npx", "cargo")
    #[serde(default)]
    pub command: Option<String>,
    /// Arguments for stdio transport
    #[serde(default)]
    pub args: Option<Vec<String>>,
    /// Environment variables for stdio transport
    #[serde(default)]
    pub env: Option<HashMap<String, String>>,

    // HTTP transport fields
    /// Base URL for HTTP transport (Claude Desktop compatible naming)
    #[serde(default, alias = "url", alias = "baseUrl")]
    pub base_url: Option<String>,
    /// Headers for HTTP transport (supports ${ENV_VAR} placeholders)
    #[serde(default)]
    pub headers: Option<HashMap<String, String>>,

    /// Optional description for the server
    #[serde(default)]
    pub description: Option<String>,
}

fn default_is_active() -> bool {
    true
}

impl McpServerConfig {
    /// Get the effective server name (uses key if name not specified)
    pub fn effective_name(&self, key: &str) -> String {
        self.name.clone().unwrap_or_else(|| key.to_string())
    }

    /// Parse transport type to internal enum
    pub fn transport(&self) -> Result<McpTransportConfig, String> {
        match self.transport_type.as_str() {
            "stdio" => {
                let command = self.command.clone().unwrap_or_default();
                if command.is_empty() {
                    return Err("stdio transport requires 'command' field".to_string());
                }
                Ok(McpTransportConfig::Stdio {
                    command,
                    args: self.args.clone().unwrap_or_default(),
                    env: self.env.clone().unwrap_or_default(),
                })
            }
            "http" | "streamableHttp" => {
                let url = self.base_url.clone().unwrap_or_default();
                if url.is_empty() {
                    return Err("HTTP transport requires 'baseUrl' field".to_string());
                }
                Ok(McpTransportConfig::Http {
                    url,
                    headers: self.headers.clone().unwrap_or_default(),
                })
            }
            other => Err(format!("unknown transport type: {}", other)),
        }
    }

    /// Create a stdio server config
    pub fn stdio(name: impl Into<String>, command: impl Into<String>, args: Vec<String>) -> Self {
        Self {
            name: Some(name.into()),
            transport_type: "stdio".to_string(),
            is_active: true,
            command: Some(command.into()),
            args: Some(args),
            env: Some(HashMap::new()),
            base_url: None,
            headers: None,
            description: None,
        }
    }

    /// Create an HTTP server config
    pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
        Self {
            name: Some(name.into()),
            transport_type: "streamableHttp".to_string(),
            is_active: true,
            command: None,
            args: None,
            env: None,
            base_url: Some(url.into()),
            headers: Some(HashMap::new()),
            description: None,
        }
    }
}

/// Transport configuration for connecting to MCP servers
#[derive(Debug, Clone)]
pub enum McpTransportConfig {
    /// Stdio transport: spawn a child process
    Stdio {
        command: String,
        args: Vec<String>,
        env: HashMap<String, String>,
    },
    /// HTTP transport: connect to a remote server (Streamable HTTP)
    Http {
        url: String,
        headers: HashMap<String, String>,
    },
}

impl McpConfig {
    /// Get active servers as a list
    pub fn active_servers(&self) -> Vec<(String, McpServerConfig)> {
        self.mcp_servers
            .iter()
            .filter(|(_, config)| config.is_active)
            .map(|(key, config)| (key.clone(), config.clone()))
            .collect()
    }

    /// Check if there are any active servers
    pub fn has_active_servers(&self) -> bool {
        self.mcp_servers.iter().any(|(_, config)| config.is_active)
    }

    /// Get enabled servers with resolved transport
    pub fn enabled_servers(&self) -> Vec<&McpServerConfig> {
        self.mcp_servers
            .iter()
            .filter(|(_, config)| config.is_active)
            .map(|(_, config)| config)
            .collect()
    }
}

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

    #[test]
    fn test_stdio_config_creation() {
        let config = McpServerConfig::stdio(
            "filesystem",
            "npx",
            vec!["-y".to_string(), "@modelcontextprotocol/server-filesystem".to_string(), "/tmp".to_string()],
        );

        assert_eq!(config.name, Some("filesystem".to_string()));
        assert!(config.is_active);
        assert_eq!(config.transport_type, "stdio");

        let transport = config.transport().unwrap();
        assert!(matches!(transport, McpTransportConfig::Stdio { .. }));
    }

    #[test]
    fn test_http_config_creation() {
        let config = McpServerConfig::http("custom", "http://localhost:8000/mcp");

        assert_eq!(config.name, Some("custom".to_string()));
        assert!(config.is_active);
        assert_eq!(config.transport_type, "streamableHttp");

        let transport = config.transport().unwrap();
        assert!(matches!(transport, McpTransportConfig::Http { .. }));
    }

    #[test]
    fn test_claude_desktop_format_deserialization() {
        // Claude Desktop/Cursor compatible format
        let json = r#"{
            "mcpServers": {
                "filesystem": {
                    "type": "stdio",
                    "command": "npx",
                    "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"],
                    "isActive": true
                },
                "WebSearch": {
                    "type": "streamableHttp",
                    "baseUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp",
                    "headers": {
                        "Authorization": "Bearer ${DASHSCOPE_API_KEY}"
                    },
                    "isActive": true,
                    "name": "AliyunBailianMCP_WebSearch"
                },
                "disabled-server": {
                    "type": "stdio",
                    "command": "npx",
                    "args": ["-y", "some-server"],
                    "isActive": false
                }
            }
        }"#;

        let config: McpConfig = serde_json::from_str(json).unwrap();
        assert_eq!(config.mcp_servers.len(), 3);
        assert_eq!(config.active_servers().len(), 2);

        // Check filesystem server
        let fs = config.mcp_servers.get("filesystem").unwrap();
        assert_eq!(fs.transport_type, "stdio");
        assert!(fs.is_active);
        let transport = fs.transport().unwrap();
        match transport {
            McpTransportConfig::Stdio { command, args, .. } => {
                assert_eq!(command, "npx");
                assert_eq!(args, vec![
                    "-y",
                    "@modelcontextprotocol/server-filesystem",
                    "/home/user"
                ]);
            }
            _ => panic!("Expected stdio transport"),
        }

        // Check WebSearch server (streamableHttp)
        let websearch = config.mcp_servers.get("WebSearch").unwrap();
        assert_eq!(websearch.transport_type, "streamableHttp");
        assert_eq!(websearch.name, Some("AliyunBailianMCP_WebSearch".to_string()));
        assert!(websearch.is_active);
        let transport = websearch.transport().unwrap();
        match transport {
            McpTransportConfig::Http { url, headers } => {
                assert_eq!(url, "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/mcp");
                assert_eq!(
                    headers.get("Authorization"),
                    Some(&"Bearer ${DASHSCOPE_API_KEY}".to_string())
                );
            }
            _ => panic!("Expected HTTP transport"),
        }

        // Check disabled server
        let disabled = config.mcp_servers.get("disabled-server").unwrap();
        assert!(!disabled.is_active);
    }

    #[test]
    fn test_effective_name_uses_key_when_name_missing() {
        let config = McpServerConfig {
            name: None,
            transport_type: "stdio".to_string(),
            is_active: true,
            command: Some("npx".to_string()),
            args: Some(vec!["-y".to_string(), "server".to_string()]),
            env: None,
            base_url: None,
            headers: None,
            description: None,
        };

        assert_eq!(config.effective_name("my-key"), "my-key");
    }

    #[test]
    fn test_effective_name_uses_name_when_specified() {
        let config = McpServerConfig {
            name: Some("MyServer".to_string()),
            transport_type: "stdio".to_string(),
            is_active: true,
            command: Some("npx".to_string()),
            args: Some(vec!["-y".to_string(), "server".to_string()]),
            env: None,
            base_url: None,
            headers: None,
            description: None,
        };

        assert_eq!(config.effective_name("my-key"), "MyServer");
    }

    #[test]
    fn test_transport_validation() {
        // Missing command for stdio
        let config = McpServerConfig {
            name: Some("test".to_string()),
            transport_type: "stdio".to_string(),
            is_active: true,
            command: None,
            args: None,
            env: None,
            base_url: None,
            headers: None,
            description: None,
        };
        assert!(config.transport().is_err());

        // Missing baseUrl for HTTP
        let config = McpServerConfig {
            name: Some("test".to_string()),
            transport_type: "streamableHttp".to_string(),
            is_active: true,
            command: None,
            args: None,
            env: None,
            base_url: None,
            headers: None,
            description: None,
        };
        assert!(config.transport().is_err());

        // Unknown transport type
        let config = McpServerConfig {
            name: Some("test".to_string()),
            transport_type: "unknown".to_string(),
            is_active: true,
            command: Some("cmd".to_string()),
            args: None,
            env: None,
            base_url: None,
            headers: None,
            description: None,
        };
        assert!(config.transport().is_err());
    }

    #[test]
    fn test_http_type_alias() {
        // Both "http" and "streamableHttp" should work
        let json_http = r#"{"mcpServers": {"test": {"type": "http", "baseUrl": "http://localhost"}}}"#;
        let json_streamable = r#"{"mcpServers": {"test": {"type": "streamableHttp", "baseUrl": "http://localhost"}}}"#;

        let config_http: McpConfig = serde_json::from_str(json_http).unwrap();
        let config_streamable: McpConfig = serde_json::from_str(json_streamable).unwrap();

        let transport_http = config_http.mcp_servers.get("test").unwrap().transport().unwrap();
        let transport_streamable = config_streamable.mcp_servers.get("test").unwrap().transport().unwrap();

        assert!(matches!(transport_http, McpTransportConfig::Http { .. }));
        assert!(matches!(transport_streamable, McpTransportConfig::Http { .. }));
    }
}