"""MCP connector — turns a remote MCP server into a Legate connector.

Its tools are dynamic (discovered from the server), so they are not part of the
static catalog: ``list_tools`` returns nothing. Instead the connectors API calls
``discover`` on install/test and caches the tool definitions on the connector row
(``config_json["mcp_tools"]``); the tool-resolution layer then turns each cached
definition into a registered tool whose handler calls the server via ``execute``.
"""

from __future__ import annotations

from legate.agent.tooling import ToolContext, ToolResult, ToolSpec
from legate.connectors.base import BaseConnector, ConnectionResult, ConnectorConfigError
from legate.mcp import client as mcp_client


class McpConnector(BaseConnector):
    type = "mcp"
    title = "MCP Server"
    config_schema = {
        "type": "object",
        "properties": {
            "url": {"type": "string", "description": "Streamable HTTP endpoint of the MCP server."}
        },
        "required": ["url"],
    }
    secret_schema = {
        "type": "object",
        "properties": {
            "token": {"type": "string", "description": "Optional bearer token for the server."}
        },
    }

    def validate_config(self, config: dict) -> None:
        super().validate_config(config)
        url = config.get("url", "")
        if not (url.startswith("http://") or url.startswith("https://")):
            raise ConnectorConfigError("url must be an absolute http(s) URL")

    def list_tools(self) -> list[ToolSpec]:
        # Tools are dynamic; discovered per server, not part of the static catalog.
        return []

    async def discover(self, config: dict, secrets: dict) -> list[dict]:
        """Fetch the server's tools, normalised for caching on the connector row."""
        raw = await mcp_client.list_tools(config.get("url", ""), secrets.get("token"))
        tools: list[dict] = []
        for tool in raw:
            name = tool.get("name")
            if not name:
                continue
            annotations = tool.get("annotations") or {}
            tools.append(
                {
                    "tool": name,
                    "description": tool.get("description", ""),
                    "parameters": tool.get("inputSchema") or {"type": "object", "properties": {}},
                    # Unknown side effects default to sensitive (route via approval);
                    # a server that marks a tool read-only opts out.
                    "sensitive": not bool(annotations.get("readOnlyHint")),
                }
            )
        return tools

    async def test_connection(self, config: dict, secrets: dict) -> ConnectionResult:
        try:
            tools = await mcp_client.list_tools(config.get("url", ""), secrets.get("token"))
        except mcp_client.MCPError as exc:
            return ConnectionResult(ok=False, message=f"connection failed: {exc}")
        return ConnectionResult(ok=True, message=f"connected · {len(tools)} tools")

    async def execute(
        self, tool: str, args: dict, config: dict, secrets: dict, ctx: ToolContext
    ) -> ToolResult:
        try:
            result = await mcp_client.call_tool(
                config.get("url", ""), secrets.get("token"), tool, args
            )
        except mcp_client.MCPError as exc:
            return ToolResult(ok=False, error=f"mcp call failed: {exc}")
        if result.get("isError"):
            return ToolResult(
                ok=False, error=mcp_client.content_text(result) or "tool reported an error"
            )
        return ToolResult(
            ok=True,
            data={
                "text": mcp_client.content_text(result),
                "content": result.get("content", []),
                "structured": result.get("structuredContent"),
            },
        )
