//! MCP transport implementations using rs-utcp.

use crate::Result;
use async_trait::async_trait;
use rs_utcp::providers::mcp::McpProvider;
use rs_utcp::transports::mcp::McpTransport as RsUtcpMcpTransport;
use rs_utcp::transports::ClientTransport;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use thulp_core::{
    Error, GetPromptResult, Prompt, PromptListResult, PromptMessage, Resource, ResourceContents,
    ResourceListResult, ResourceTemplate, ResourceTemplateListResult, ToolCall, ToolDefinition,
    ToolResult, Transport as CoreTransport,
};

/// Wrapper around rs-utcp's MCP transport
pub struct McpTransport {
    /// The underlying rs-utcp transport
    inner: RsUtcpMcpTransport,
    /// The MCP provider (concrete type for URL access)
    provider: Arc<McpProvider>,
    /// Connection status
    connected: bool,
    /// HTTP client for direct JSON-RPC calls (resources/prompts)
    http_client: reqwest::Client,
    /// Request ID counter for JSON-RPC
    request_id: AtomicU64,
}

impl McpTransport {
    /// Create a new MCP transport for HTTP connection
    pub fn new_http(name: String, url: String) -> Self {
        let provider = Arc::new(McpProvider::new(name, url, None));
        let inner = RsUtcpMcpTransport::new();

        Self {
            inner,
            provider,
            connected: false,
            http_client: reqwest::Client::new(),
            request_id: AtomicU64::new(1000),
        }
    }

    /// Create a new MCP transport for STDIO connection
    pub fn new_stdio(name: String, command: String, args: Option<Vec<String>>) -> Self {
        let provider = Arc::new(McpProvider::new_stdio(name, command, args, None));
        let inner = RsUtcpMcpTransport::new();

        Self {
            inner,
            provider,
            connected: false,
            http_client: reqwest::Client::new(),
            request_id: AtomicU64::new(1000),
        }
    }

    /// Create a new MCP transport for STDIO connection with environment variables
    pub fn new_stdio_with_env(
        name: String,
        command: String,
        args: Option<Vec<String>>,
        env: HashMap<String, String>,
    ) -> Self {
        let provider = Arc::new(McpProvider::new_stdio(name, command, args, Some(env)));
        let inner = RsUtcpMcpTransport::new();

        Self {
            inner,
            provider,
            connected: false,
            http_client: reqwest::Client::new(),
            request_id: AtomicU64::new(1000),
        }
    }

    /// Create a new MCP transport with default configuration
    pub fn new() -> Self {
        Self::new_http("default".to_string(), "http://localhost:8080".to_string())
    }

    fn next_request_id(&self) -> u64 {
        self.request_id.fetch_add(1, Ordering::Relaxed)
    }

    async fn raw_jsonrpc(&self, method: &str, params: Value) -> Result<Value> {
        let url = self
            .provider
            .url
            .as_deref()
            .ok_or_else(|| Error::ExecutionFailed("resources require HTTP transport".into()))?;

        let request = serde_json::json!({
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": self.next_request_id(),
        });

        let resp = self
            .http_client
            .post(url)
            .json(&request)
            .send()
            .await
            .map_err(|e| Error::ExecutionFailed(format!("HTTP request failed: {e}")))?;

        if !resp.status().is_success() {
            return Err(Error::ExecutionFailed(format!(
                "MCP server returned {}",
                resp.status()
            )));
        }

        let body: Value = resp
            .json()
            .await
            .map_err(|e| Error::ExecutionFailed(format!("invalid JSON response: {e}")))?;

        if let Some(err) = body.get("error") {
            return Err(Error::ExecutionFailed(format!("JSON-RPC error: {err}")));
        }

        Ok(body.get("result").cloned().unwrap_or(Value::Null))
    }

    /// List resources from the MCP server.
    pub async fn list_resources(&self) -> Result<ResourceListResult> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        let result = self.raw_jsonrpc("resources/list", serde_json::json!({})).await?;
        let resources: Vec<Resource> = serde_json::from_value(
            result.get("resources").cloned().unwrap_or(Value::Array(vec![])),
        )
        .map_err(|e| Error::ExecutionFailed(format!("failed to parse resources: {e}")))?;
        let next_cursor = result
            .get("nextCursor")
            .and_then(|v| v.as_str())
            .map(String::from);
        Ok(ResourceListResult {
            resources,
            next_cursor,
        })
    }

    /// Read a resource by URI from the MCP server.
    pub async fn read_resource(&self, uri: &str) -> Result<ResourceContents> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        let result = self
            .raw_jsonrpc("resources/read", serde_json::json!({ "uri": uri }))
            .await?;
        let contents_arr = result
            .get("contents")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();
        if let Some(first) = contents_arr.first() {
            let text = first.get("text").and_then(|v| v.as_str()).map(String::from);
            let blob = first.get("blob").and_then(|v| v.as_str()).map(String::from);
            let mime_type = first
                .get("mimeType")
                .and_then(|v| v.as_str())
                .map(String::from);
            Ok(ResourceContents {
                uri: first
                    .get("uri")
                    .and_then(|v| v.as_str())
                    .unwrap_or(uri)
                    .to_string(),
                text,
                blob,
                mime_type,
            })
        } else {
            Ok(ResourceContents::text(uri, ""))
        }
    }

    /// List resource templates from the MCP server.
    pub async fn list_resource_templates(&self) -> Result<ResourceTemplateListResult> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        let result = self
            .raw_jsonrpc("resources/templates/list", serde_json::json!({}))
            .await?;
        let templates: Vec<ResourceTemplate> = serde_json::from_value(
            result
                .get("resourceTemplates")
                .cloned()
                .unwrap_or(Value::Array(vec![])),
        )
        .map_err(|e| Error::ExecutionFailed(format!("failed to parse templates: {e}")))?;
        let next_cursor = result
            .get("nextCursor")
            .and_then(|v| v.as_str())
            .map(String::from);
        Ok(ResourceTemplateListResult {
            resource_templates: templates,
            next_cursor,
        })
    }

    /// Subscribe to resource changes.
    pub async fn subscribe_resource(&self, uri: &str) -> Result<()> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        self.raw_jsonrpc("resources/subscribe", serde_json::json!({ "uri": uri }))
            .await?;
        Ok(())
    }

    /// Unsubscribe from resource changes.
    pub async fn unsubscribe_resource(&self, uri: &str) -> Result<()> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        self.raw_jsonrpc("resources/unsubscribe", serde_json::json!({ "uri": uri }))
            .await?;
        Ok(())
    }

    /// List prompts from the MCP server.
    pub async fn list_prompts(&self) -> Result<PromptListResult> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        let result = self.raw_jsonrpc("prompts/list", serde_json::json!({})).await?;
        let prompts: Vec<Prompt> = serde_json::from_value(
            result.get("prompts").cloned().unwrap_or(Value::Array(vec![])),
        )
        .map_err(|e| Error::ExecutionFailed(format!("failed to parse prompts: {e}")))?;
        let next_cursor = result
            .get("nextCursor")
            .and_then(|v| v.as_str())
            .map(String::from);
        Ok(PromptListResult {
            prompts,
            next_cursor,
        })
    }

    /// Get a rendered prompt by name with arguments.
    pub async fn get_prompt(
        &self,
        name: &str,
        arguments: HashMap<String, String>,
    ) -> Result<GetPromptResult> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".into()));
        }
        let params = serde_json::json!({
            "name": name,
            "arguments": arguments,
        });
        let result = self.raw_jsonrpc("prompts/get", params).await?;
        let description = result
            .get("description")
            .and_then(|v| v.as_str())
            .map(String::from);
        let messages: Vec<PromptMessage> = serde_json::from_value(
            result.get("messages").cloned().unwrap_or(Value::Array(vec![])),
        )
        .map_err(|e| Error::ExecutionFailed(format!("failed to parse prompt messages: {e}")))?;
        Ok(GetPromptResult {
            description,
            messages,
        })
    }
}

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

#[async_trait]
impl CoreTransport for McpTransport {
    async fn connect(&mut self) -> Result<()> {
        // Register the provider with the transport
        let _tools = self
            .inner
            .register_tool_provider(&*self.provider)
            .await
            .map_err(|e| Error::ExecutionFailed(format!("Failed to register provider: {}", e)))?;

        self.connected = true;
        Ok(())
    }

    async fn disconnect(&mut self) -> Result<()> {
        self.inner
            .deregister_tool_provider(&*self.provider)
            .await
            .map_err(|e| Error::ExecutionFailed(format!("Failed to deregister provider: {}", e)))?;
        self.connected = false;
        Ok(())
    }

    fn is_connected(&self) -> bool {
        self.connected
    }

    async fn list_tools(&self) -> Result<Vec<ToolDefinition>> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".to_string()));
        }

        // Use rs-utcp's register_tool_provider which calls tools/list internally
        let tools = self
            .inner
            .register_tool_provider(&*self.provider)
            .await
            .map_err(|e| Error::ExecutionFailed(format!("Failed to list tools: {}", e)))?;

        // Convert rs-utcp Tool to ToolDefinition
        let mut definitions = Vec::new();
        for tool in tools {
            // Convert the ToolInputOutputSchema to JSON for parsing
            let inputs_json = serde_json::to_value(&tool.inputs).map_err(|e| {
                Error::ExecutionFailed(format!("Failed to serialize inputs: {}", e))
            })?;

            // Extract parameters from tool.inputs
            let parameters =
                ToolDefinition::parse_mcp_input_schema(&inputs_json).unwrap_or_default();

            definitions.push(ToolDefinition {
                name: tool.name,
                description: tool.description,
                parameters,
            });
        }

        Ok(definitions)
    }

    async fn call(&self, call: &ToolCall) -> Result<ToolResult> {
        if !self.connected {
            return Err(Error::ExecutionFailed("not connected".to_string()));
        }

        // Convert arguments to the format expected by rs-utcp
        let args: HashMap<String, Value> = match &call.arguments {
            Value::Object(map) => map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
            _ => HashMap::new(),
        };

        // Call the tool through the transport
        let result = self
            .inner
            .call_tool(&call.tool, args, &*self.provider)
            .await
            .map_err(|e| Error::ExecutionFailed(format!("Tool call failed: {}", e)))?;

        Ok(ToolResult::success(result))
    }
}

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

    #[test]
    fn transport_new_http() {
        let transport =
            McpTransport::new_http("test".to_string(), "http://localhost:8080".to_string());
        assert!(!transport.is_connected());
    }

    #[test]
    fn transport_new_stdio() {
        let transport = McpTransport::new_stdio(
            "test".to_string(),
            "test-cmd".to_string(),
            Some(vec!["--arg1".to_string()]),
        );
        assert!(!transport.is_connected());
    }

    #[test]
    fn transport_new_default() {
        let transport = McpTransport::new();
        assert!(!transport.is_connected());
    }

    #[test]
    fn transport_default_impl() {
        let transport1 = McpTransport::default();
        let transport2 = McpTransport::new();
        assert!(!transport1.is_connected());
        assert!(!transport2.is_connected());
    }

    #[tokio::test]
    async fn transport_connect_disconnect() {
        let transport =
            McpTransport::new_http("test".to_string(), "http://localhost:9999".to_string());

        // Start with disconnected
        assert!(!transport.is_connected());

        // Note: We can't actually connect in tests without a real MCP server
        // This test verifies the structure and state management
        assert_eq!(transport.is_connected(), false);
    }

    #[test]
    fn test_argument_conversion() {
        // Test argument conversion for tool calls
        let call = ToolCall::builder("test_tool")
            .arg_str("string_param", "value")
            .arg_int("int_param", 42)
            .arg_bool("bool_param", true)
            .build();

        // Verify arguments are in expected format
        assert!(call.arguments.is_object());
        assert_eq!(call.arguments["string_param"], "value");
        assert_eq!(call.arguments["int_param"], 42);
        assert_eq!(call.arguments["bool_param"], true);
    }

    #[test]
    fn test_argument_conversion_nested() {
        // Test with nested object arguments
        let call = ToolCall::builder("test_tool")
            .arg("nested", json!({"key": "value", "number": 123}))
            .build();

        assert!(call.arguments.is_object());
        assert_eq!(call.arguments["nested"]["key"], "value");
        assert_eq!(call.arguments["nested"]["number"], 123);
    }

    #[test]
    fn test_argument_conversion_array() {
        // Test with array arguments
        let call = ToolCall::builder("test_tool")
            .arg("items", json!([1, 2, 3, 4, 5]))
            .build();

        assert!(call.arguments.is_object());
        assert_eq!(call.arguments["items"], json!([1, 2, 3, 4, 5]));
    }

    // Edge case tests
    #[tokio::test]
    async fn test_list_tools_when_disconnected() {
        let transport = McpTransport::new();

        // Should fail when not connected
        let result = transport.list_tools().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_call_when_disconnected() {
        let transport = McpTransport::new();
        let call = ToolCall::new("test_tool");

        // Should fail when not connected
        let result = transport.call(&call).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[test]
    fn test_empty_arguments() {
        let call = ToolCall::new("test_tool");
        assert!(call.arguments.is_object());
        assert_eq!(call.arguments.as_object().unwrap().len(), 0);
    }

    #[test]
    fn test_special_characters_in_tool_name() {
        let call = ToolCall::new("test-tool_v2.0");
        assert_eq!(call.tool, "test-tool_v2.0");
    }

    #[test]
    fn test_unicode_in_arguments() {
        let call = ToolCall::builder("test_tool")
            .arg_str("message", "Hello 世界 🌍")
            .build();

        assert_eq!(call.arguments["message"], "Hello 世界 🌍");
    }

    #[test]
    fn test_large_argument_values() {
        // Test with large string
        let large_string = "x".repeat(10000);
        let call = ToolCall::builder("test_tool")
            .arg_str("data", &large_string)
            .build();

        assert_eq!(call.arguments["data"].as_str().unwrap().len(), 10000);
    }

    #[test]
    fn test_null_arguments() {
        let call = ToolCall::builder("test_tool")
            .arg("null_param", json!(null))
            .build();

        assert!(call.arguments["null_param"].is_null());
    }

    #[test]
    fn test_mixed_type_arguments() {
        let call = ToolCall::builder("test_tool")
            .arg_str("string", "value")
            .arg_int("int", 42)
            .arg_bool("bool", true)
            .arg("null", json!(null))
            .arg("array", json!([1, 2, 3]))
            .arg("object", json!({"key": "value"}))
            .build();

        assert_eq!(call.arguments["string"], "value");
        assert_eq!(call.arguments["int"], 42);
        assert_eq!(call.arguments["bool"], true);
        assert!(call.arguments["null"].is_null());
        assert!(call.arguments["array"].is_array());
        assert!(call.arguments["object"].is_object());
    }

    #[test]
    fn test_stdio_transport_creation() {
        let transport = McpTransport::new_stdio(
            "echo-server".to_string(),
            "npx".to_string(),
            Some(vec![
                "-y".to_string(),
                "@modelcontextprotocol/server-echo".to_string(),
            ]),
        );

        assert!(!transport.is_connected());
    }

    #[test]
    fn test_http_transport_with_https() {
        let transport = McpTransport::new_http(
            "secure".to_string(),
            "https://api.example.com/mcp".to_string(),
        );

        assert!(!transport.is_connected());
    }

    #[test]
    fn test_transport_creation_with_empty_name() {
        let transport = McpTransport::new_http("".to_string(), "http://localhost:8080".to_string());
        assert!(!transport.is_connected());
    }

    #[test]
    fn test_deeply_nested_arguments() {
        let nested = json!({
            "level1": {
                "level2": {
                    "level3": {
                        "level4": {
                            "level5": "deep"
                        }
                    }
                }
            }
        });

        let call = ToolCall::builder("test_tool")
            .arg("nested", nested.clone())
            .build();

        assert_eq!(call.arguments["nested"], nested);
        assert_eq!(
            call.arguments["nested"]["level1"]["level2"]["level3"]["level4"]["level5"],
            "deep"
        );
    }

    #[test]
    fn test_argument_with_numbers() {
        let call = ToolCall::builder("test_tool")
            .arg_int("positive", 42)
            .arg_int("negative", -42)
            .arg_int("zero", 0)
            .arg("float", json!(3.14159))
            .arg("scientific", json!(1.5e10))
            .build();

        assert_eq!(call.arguments["positive"], 42);
        assert_eq!(call.arguments["negative"], -42);
        assert_eq!(call.arguments["zero"], 0);
        assert_eq!(call.arguments["float"], 3.14159);
        assert_eq!(call.arguments["scientific"], 1.5e10);
    }

    #[test]
    fn test_argument_with_special_json_values() {
        let call = ToolCall::builder("test_tool")
            .arg("empty_string", json!(""))
            .arg("empty_array", json!([]))
            .arg("empty_object", json!({}))
            .arg("boolean_true", json!(true))
            .arg("boolean_false", json!(false))
            .build();

        assert_eq!(call.arguments["empty_string"], "");
        assert_eq!(call.arguments["empty_array"], json!([]));
        assert_eq!(call.arguments["empty_object"], json!({}));
        assert_eq!(call.arguments["boolean_true"], true);
        assert_eq!(call.arguments["boolean_false"], false);
    }

    #[tokio::test]
    async fn test_list_resources_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.list_resources().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_read_resource_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.read_resource("file:///test.txt").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_list_resource_templates_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.list_resource_templates().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_subscribe_resource_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.subscribe_resource("file:///test.txt").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_unsubscribe_resource_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.unsubscribe_resource("file:///test.txt").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_stdio_transport_rejects_resources() {
        let mut transport = McpTransport::new_stdio(
            "test".to_string(),
            "echo".to_string(),
            None,
        );
        transport.connected = true;
        let result = transport.list_resources().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("HTTP transport"));
    }

    #[tokio::test]
    async fn test_list_prompts_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.list_prompts().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_get_prompt_when_disconnected() {
        let transport = McpTransport::new();
        let result = transport.get_prompt("test", HashMap::new()).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("not connected"));
    }

    #[tokio::test]
    async fn test_stdio_transport_rejects_prompts() {
        let mut transport = McpTransport::new_stdio(
            "test".to_string(),
            "echo".to_string(),
            None,
        );
        transport.connected = true;
        let result = transport.list_prompts().await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("HTTP transport"));
    }

    #[test]
    fn transport_new_stdio_with_env() {
        let mut env = HashMap::new();
        env.insert("API_KEY".to_string(), "secret123".to_string());
        env.insert("DEBUG".to_string(), "1".to_string());

        let transport = McpTransport::new_stdio_with_env(
            "test-server".to_string(),
            "node".to_string(),
            Some(vec!["server.js".to_string()]),
            env,
        );

        assert!(!transport.is_connected());
        // Verify env vars are stored on the provider
        let env_vars = transport.provider.env_vars.as_ref().unwrap();
        assert_eq!(env_vars.get("API_KEY").unwrap(), "secret123");
        assert_eq!(env_vars.get("DEBUG").unwrap(), "1");
        assert_eq!(env_vars.len(), 2);
    }

    #[test]
    fn transport_new_stdio_with_empty_env() {
        let env = HashMap::new();
        let transport = McpTransport::new_stdio_with_env(
            "test".to_string(),
            "echo".to_string(),
            None,
            env,
        );

        assert!(!transport.is_connected());
        let env_vars = transport.provider.env_vars.as_ref().unwrap();
        assert!(env_vars.is_empty());
    }

    #[test]
    fn transport_new_stdio_has_no_env() {
        let transport = McpTransport::new_stdio(
            "test".to_string(),
            "echo".to_string(),
            None,
        );

        // Regular stdio constructor should have no env vars
        assert!(transport.provider.env_vars.is_none());
    }

    #[test]
    fn test_request_id_increments() {
        let transport = McpTransport::new();
        let id1 = transport.next_request_id();
        let id2 = transport.next_request_id();
        assert_eq!(id2, id1 + 1);
    }
}
