"""MCP Client - 调用外部 MCP 工具服务器

U16: 重写为 langchain-mcp-adapters 包装层。保留 MCPClient / MCPTool API 向后兼容。
旧的 Transport 注入路径保留（发出 DeprecationWarning），供现有调用方过渡。

传输类型由 URL scheme 自动检测（transport=None 时）：
- ``stdio://command arg1 arg2`` → stdio 传输
- ``http://`` / ``https://``     → streamable_http 传输
- ``sse://...``                  → sse 传输（自动转为 http://）
"""

from __future__ import annotations

import json
import logging
import warnings
from typing import TYPE_CHECKING
from urllib.parse import urlparse

from agentkit.mcp.transport import HTTPTransport, SSETransport, StdioTransport, Transport
from agentkit.tools.base import Tool

if TYPE_CHECKING:
    from langchain_mcp_adapters.client import MultiServerMCPClient

logger = logging.getLogger(__name__)


def _import_langchain_client() -> type["MultiServerMCPClient"]:
    """延迟导入 langchain-mcp-adapters 的 MultiServerMCPClient。

    未安装时抛出带提示信息的 ImportError，便于调用方回退到 Transport 路径。
    """
    try:
        from langchain_mcp_adapters.client import MultiServerMCPClient as _Client
    except ImportError as e:
        raise ImportError(
            "langchain-mcp-adapters 未安装，无法使用 langchain 传输路径。"
            "请执行 pip install 'fischer-agentkit[mcp]' 或 pip install langchain-mcp-adapters。"
            "如需使用旧的 Transport 路径，请通过 MCPClient.from_transport(transport) 创建客户端。"
        ) from e
    return _Client


class MCPClient:
    """MCP Client - 连接外部 MCP Server 并调用工具

    U16: 内部使用 langchain-mcp-adapters 的 MultiServerMCPClient 管理连接。
    保留旧 Transport 注入路径以向后兼容（发出 DeprecationWarning）。

    支持两种模式：
    1. URL scheme 自动检测（推荐）：transport=None，根据 server_url 的 scheme 选择传输
    2. Transport 注入（旧路径，弃用）：传入 Transport 实例，走原有 JSON-RPC 路径
    """

    def __init__(
        self,
        server_url: str,
        timeout: int = 30,
        transport: Transport | None = None,
    ):
        self._server_url = server_url.rstrip("/") if server_url else ""
        self._timeout = timeout
        self._tools_cache: list[dict] | None = None
        self._transport = transport
        # U10 — 懒构造并缓存的 langchain client，避免每次 list_tools/call_tool
        # 都新建 MultiServerMCPClient（stdio 传输下会反复 spawn 子进程）。
        self._lc_client: "MultiServerMCPClient | None" = None

        if transport is not None:
            # 旧 Transport 路径 — 发出 DeprecationWarning，但保持原有行为
            warnings.warn(
                "通过 Transport 实例创建 MCPClient 已弃用，请使用 URL scheme 自动检测"
                "（如 MCPClient('http://...') 或 MCPClient('stdio://...')）。"
                "详见 U16 迁移指南。将在下个版本移除。",
                DeprecationWarning,
                stacklevel=2,
            )
            self._langchain_config: dict[str, object] | None = None
        else:
            # 新 langchain 路径 — 解析 URL scheme 构建连接配置
            self._langchain_config = self._build_langchain_config(self._server_url, timeout)

    @staticmethod
    def _build_langchain_config(server_url: str, timeout: float) -> dict[str, object]:
        """根据 URL scheme 构建 langchain-mcp-adapters 连接配置。

        Args:
            server_url: 服务器 URL，支持 stdio:// http:// https:// sse://
            timeout: 超时秒数（仅 http/sse 传输使用）

        Returns:
            langchain-mcp-adapters 连接配置 dict

        Raises:
            ValueError: URL 为空或 scheme 不支持
        """
        if not server_url:
            raise ValueError("server_url 不能为空（transport=None 时需要有效 URL）")

        # stdio 特殊处理：stdio://command arg1 arg2 → command + args
        # 不用 urlparse，因为命令行参数可能含特殊字符
        if server_url.startswith("stdio://"):
            raw = server_url[len("stdio://") :]
            parts = raw.split()
            if not parts:
                raise ValueError(f"无效的 stdio URL: {server_url!r}")
            return {
                "transport": "stdio",
                "command": parts[0],
                "args": parts[1:],
            }

        parsed = urlparse(server_url)
        scheme = parsed.scheme.lower()

        if scheme in ("http", "https"):
            return {
                "transport": "streamable_http",
                "url": server_url,
                "timeout": timeout,
            }

        if scheme == "sse":
            # sse://host/path → http://host/path（sse scheme 不携带 TLS 信息）
            converted = "http://" + server_url[len("sse://") :]
            return {
                "transport": "sse",
                "url": converted,
                "timeout": timeout,
            }

        raise ValueError(
            f"不支持的 URL scheme: {scheme!r}。支持 stdio://, http://, https://, sse://"
        )

    @classmethod
    def from_transport(cls, transport: Transport) -> "MCPClient":
        """从 Transport 实例创建 MCPClient（旧路径，向后兼容）

        发出 DeprecationWarning。建议迁移到 URL scheme 自动检测。
        """
        if isinstance(transport, HTTPTransport):
            server_url = transport.endpoint_url
        elif isinstance(transport, SSETransport):
            server_url = transport.endpoint_url
        elif isinstance(transport, StdioTransport):
            server_url = f"stdio://{transport._command}"
        else:
            server_url = ""
        return cls(server_url=server_url, transport=transport)

    async def _get_lc_client(self) -> "MultiServerMCPClient":
        """懒构造并缓存 langchain ``MultiServerMCPClient`` 实例。

        首次调用时创建，后续返回缓存，避免每次 list_tools/call_tool 都新建
        client（stdio 传输下会 spawn 新子进程，造成连接/进程泄漏）。
        """
        if self._lc_client is None:
            client_cls = _import_langchain_client()
            self._lc_client = client_cls({"server": self._langchain_config})
        return self._lc_client

    async def aclose(self) -> None:
        """关闭缓存的 langchain client（如果它提供 ``aclose`` 方法）。"""
        if self._lc_client is not None:
            aclose = getattr(self._lc_client, "aclose", None)
            if aclose is not None:
                await aclose()
            self._lc_client = None

    async def list_tools(self) -> list[dict]:
        """列出远程 MCP Server 上的工具

        Returns:
            工具列表，每项为 ``{"name":..., "description":..., "inputSchema":...}``

        Raises:
            ImportError: langchain-mcp-adapters 未安装（仅 transport=None 路径）
        """
        if self._transport is not None:
            # 旧 Transport 路径
            if not self._transport.is_connected:
                await self._transport.connect()
            result = await self._transport.send_request("tools/list")
            tools = result.get("tools", []) if isinstance(result, dict) else []
            self._tools_cache = tools
            return self._tools_cache

        # 新 langchain 路径 — 复用缓存的 client
        client = await self._get_lc_client()
        lc_tools = await client.get_tools()
        tools = [
            {
                "name": t.name,
                "description": t.description or "",
                "inputSchema": self._extract_schema(t),
            }
            for t in lc_tools
        ]
        self._tools_cache = tools
        return tools

    @staticmethod
    def _extract_schema(tool: object) -> dict[str, object]:
        """从 LangChain Tool 提取 inputSchema（JSON Schema 格式）。

        LangChain 工具通常有 args_schema（pydantic model），回退到 tool.args dict。
        """
        args_schema = getattr(tool, "args_schema", None)
        if args_schema is not None and hasattr(args_schema, "model_json_schema"):
            return args_schema.model_json_schema()
        # ponytail: tool.args 在 langchain BaseTool 上是 dict，够用作回退
        args = getattr(tool, "args", None)
        if isinstance(args, dict):
            return args
        return {"type": "object", "properties": {}}

    async def call_tool(self, tool_name: str, arguments: dict) -> dict:
        """调用远程 MCP 工具

        Args:
            tool_name: 工具名称
            arguments: 工具参数

        Returns:
            MCP 响应格式的 dict：``{"content": [{"type":"text","text":...}]}``

        Raises:
            KeyError: 工具不存在（仅 transport=None 路径）
            ImportError: langchain-mcp-adapters 未安装
        """
        if self._transport is not None:
            # 旧 Transport 路径
            if not self._transport.is_connected:
                await self._transport.connect()
            return await self._transport.send_request(
                "tools/call",
                params={"name": tool_name, "arguments": arguments},
            )

        # 新 langchain 路径 — 复用缓存的 client
        client = await self._get_lc_client()
        lc_tools = await client.get_tools()
        for tool in lc_tools:
            if tool.name == tool_name:
                result = await tool.ainvoke(arguments)
                # 包装为 MCP 响应格式，保持 call_tool 返回形状与旧路径一致
                return {
                    "content": [
                        {
                            "type": "text",
                            "text": json.dumps(result, ensure_ascii=False, default=str),
                        }
                    ]
                }
        raise KeyError(f"MCP 工具不存在: {tool_name!r}")

    def as_tool(self, tool_name: str, description: str = "") -> "MCPTool":
        """将远程 MCP 工具包装为本地 Tool 对象"""
        return MCPTool(
            name=tool_name,
            description=description,
            client=self,
        )


class MCPTool(Tool):
    """MCP 工具 - 通过 MCP Client 调用远程工具"""

    def __init__(
        self,
        name: str,
        description: str,
        client: MCPClient,
        input_schema: dict[str, object] | None = None,
        output_schema: dict[str, object] | None = None,
        version: str = "1.0.0",
        tags: list[str] | None = None,
    ):
        super().__init__(
            name=name,
            description=description,
            input_schema=input_schema,
            output_schema=output_schema,
            version=version,
            tags=tags or ["mcp"],
        )
        self._client = client

    async def execute(self, **kwargs) -> dict:
        result = await self._client.call_tool(self.name, kwargs)
        # 解析 MCP 响应格式
        if "content" in result:
            for item in result["content"]:
                if item.get("type") == "text":
                    try:
                        return json.loads(item["text"])
                    except json.JSONDecodeError:
                        return {"result": item["text"]}
        return result
