"""MCP bridge server -- expose Gateway tools to ACP agents.

Wraps a Gateway instance as an MCP stdio server so that ACP agents
(Claude Code, Codex, etc.) can use Otto's memory, skills, and custom
tools via MCP server configs passed during ``session/new``.

Architecture:
    The ACP agent spawns this module as a subprocess (stdio MCP server).
    Since the subprocess can't access the main process's in-memory Gateway,
    a lightweight Unix socket IPC server bridges the gap:

    Main process (Otto daemon)          Subprocess (this module)
    ┌──────────────────────┐            ┌────────────────────┐
    │  Gateway (real tools) │            │  MCP stdio server   │
    │         │             │            │         │           │
    │  _ToolIPCServer ◄────── Unix sock ──► _ToolIPCClient    │
    └──────────────────────┘            └────────────────────┘

    The main process starts a _ToolIPCServer that exposes ``list_tools``
    and ``call_tool`` over a Unix socket.  The subprocess connects as a
    _ToolIPCClient and proxies MCP requests through it.
"""

from __future__ import annotations

import asyncio
import json
import os
import sys
import uuid
from typing import Any

import mcp.server
import mcp.types as mcp_types
from mcp.server.stdio import stdio_server

from otto.agent import Tool
from otto.gateway import Gateway
from otto.log import get_logger

_log = get_logger("otto.acp.mcp_bridge")

_SOCKET_ENV_VAR = "OTTO_MCP_SOCKET"

# Tools that ACP agents already have (or should not access remotely).
_EXCLUDED_TOOLS: frozenset[str] = frozenset(
    {
        "read_file",
        "write_file",
        "edit_file",
        "bash",
        "view_image",
        "send_file",
        "browse",
    }
)


def _select_tools(gateway: Gateway) -> list[Tool]:
    """Return the subset of Gateway tools suitable for ACP agents.

    Includes memory tools, skill tools, web tools, and any custom
    bot-specific tools.  Excludes filesystem and shell tools that the
    agent already has natively.
    """
    return [t for t in gateway.tools() if t.name not in _EXCLUDED_TOOLS]


def _bridge_tool_name(name: str) -> str:
    """Prefix a tool name with ``otto_`` to avoid conflicts with agent builtins."""
    return f"otto_{name}"


def _gateway_tool_name(bridge_name: str) -> str:
    """Strip the ``otto_`` prefix to recover the original Gateway tool name."""
    if bridge_name.startswith("otto_"):
        return bridge_name[5:]
    return bridge_name


def _tool_to_mcp(tool: Tool) -> mcp_types.Tool:
    """Convert an Otto Tool into an MCP Tool definition."""
    return mcp_types.Tool(
        name=_bridge_tool_name(tool.name),
        description=tool.description,
        inputSchema=tool.parameters,
    )


# ---------------------------------------------------------------------------
# Unix socket IPC — server (main process) and client (subprocess)
# ---------------------------------------------------------------------------


class _ToolIPCServer:
    """Unix socket server exposing Gateway tools to MCP bridge subprocesses."""

    def __init__(self, gateway: Gateway, socket_path: str) -> None:
        self._gateway = gateway
        self._socket_path = socket_path
        self._tools = _select_tools(gateway)
        self._tool_map: dict[str, Tool] = {t.name: t for t in self._tools}
        self._server: asyncio.AbstractServer | None = None

    async def start(self) -> None:
        try:
            os.unlink(self._socket_path)
        except FileNotFoundError:
            pass
        self._server = await asyncio.start_unix_server(
            self._handle_connection, path=self._socket_path
        )
        _log.info("tool ipc server started", socket=self._socket_path, tools=len(self._tools))

    async def stop(self) -> None:
        if self._server is not None:
            self._server.close()
            await self._server.wait_closed()
            self._server = None
        try:
            os.unlink(self._socket_path)
        except FileNotFoundError:
            pass

    async def _handle_connection(
        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        _log.info("tool ipc client connected")
        try:
            while True:
                line = await reader.readline()
                if not line:
                    break
                request = json.loads(line.decode())
                method = request.get("method")
                if method == "list_tools":
                    response = self._handle_list()
                elif method == "call_tool":
                    response = await self._handle_call(
                        request.get("name", ""), request.get("arguments", {})
                    )
                else:
                    response = {"error": f"Unknown IPC method: {method}"}
                writer.write(json.dumps(response).encode() + b"\n")
                await writer.drain()
        except (ConnectionResetError, asyncio.IncompleteReadError):
            pass
        finally:
            writer.close()
            await writer.wait_closed()

    def _handle_list(self) -> dict:
        return {
            "tools": [
                {"name": t.name, "description": t.description, "parameters": t.parameters}
                for t in self._tools
            ]
        }

    async def _handle_call(self, name: str, arguments: dict) -> dict:
        tool = self._tool_map.get(name)
        if tool is None:
            return {"error": f"Unknown tool: {name}"}
        try:
            result = await tool.execute(**arguments)
            text = result if isinstance(result, str) else json.dumps(result)
            return {"result": text}
        except Exception as exc:
            _log.warning("tool ipc call failed", name=name, error=str(exc))
            return {"error": f"Tool error: {exc}"}


class _ToolIPCClient:
    """Connects to the main process's tool IPC server from a subprocess."""

    def __init__(self, socket_path: str) -> None:
        self._socket_path = socket_path
        self._reader: asyncio.StreamReader | None = None
        self._writer: asyncio.StreamWriter | None = None

    async def connect(self) -> None:
        self._reader, self._writer = await asyncio.open_unix_connection(self._socket_path)

    async def close(self) -> None:
        if self._writer is not None:
            self._writer.close()
            await self._writer.wait_closed()

    async def list_tools(self) -> list[dict]:
        resp = await self._request({"method": "list_tools"})
        return resp.get("tools", [])

    async def call_tool(self, name: str, arguments: dict) -> str:
        resp = await self._request({"method": "call_tool", "name": name, "arguments": arguments})
        if "error" in resp:
            raise RuntimeError(resp["error"])
        return resp.get("result", "")

    async def _request(self, data: dict) -> dict:
        assert self._reader is not None and self._writer is not None
        self._writer.write(json.dumps(data).encode() + b"\n")
        await self._writer.drain()
        line = await self._reader.readline()
        if not line:
            raise ConnectionError("IPC server closed connection")
        return json.loads(line.decode())


# ---------------------------------------------------------------------------
# OttoMCPBridge — in-process + IPC mode
# ---------------------------------------------------------------------------


class OttoMCPBridge:
    """Expose selected Gateway tools as an MCP stdio server.

    Usage::

        bridge = OttoMCPBridge(gateway)
        await bridge.start()           # start IPC server for subprocesses
        cfg = bridge.get_server_config()  # pass to ACP session/new
        ...
        await bridge.stop()            # cleanup on shutdown
    """

    def __init__(self, gateway: Gateway) -> None:
        self._gateway = gateway
        self._tools = _select_tools(gateway)
        self._tool_map: dict[str, Tool] = {_bridge_tool_name(t.name): t for t in self._tools}
        self._server = mcp.server.Server("otto-tools")
        self._register_handlers()

        self._socket_path = f"/tmp/otto-mcp-{uuid.uuid4().hex[:12]}.sock"
        self._ipc_server: _ToolIPCServer | None = None

    @property
    def tool_names(self) -> list[str]:
        """Return the list of MCP tool names exposed by this bridge."""
        return sorted(self._tool_map.keys())

    async def start(self) -> None:
        """Start the IPC server so MCP bridge subprocesses can reach Gateway tools."""
        if self._ipc_server is not None:
            return
        self._ipc_server = _ToolIPCServer(self._gateway, self._socket_path)
        await self._ipc_server.start()

    async def stop(self) -> None:
        """Stop the IPC server and clean up the socket file."""
        if self._ipc_server is not None:
            await self._ipc_server.stop()
            self._ipc_server = None

    def get_server_config(self) -> dict[str, Any]:
        """Return an MCP server config dict for passing to ACP ``session/new``.

        The returned dict describes a stdio-based MCP server that ACP agents
        can connect to by spawning this module as a subprocess.  The socket
        path is passed via the ``OTTO_MCP_SOCKET`` env var so the subprocess
        can connect back to the main process for tool execution.
        """
        return {
            "name": "otto-tools",
            "command": sys.executable,
            "args": ["-m", "otto.acp.mcp_bridge"],
            "env": [{"name": _SOCKET_ENV_VAR, "value": self._socket_path}],
        }

    def _register_handlers(self) -> None:
        """Wire up the list_tools and call_tool MCP handlers."""

        @self._server.list_tools()
        async def list_tools() -> list[mcp_types.Tool]:
            return [_tool_to_mcp(t) for t in self._tools]

        @self._server.call_tool()
        async def call_tool(
            name: str, arguments: dict[str, Any] | None = None
        ) -> list[mcp_types.TextContent]:
            tool = self._tool_map.get(name)
            if tool is None:
                return [mcp_types.TextContent(type="text", text=f"Unknown tool: {name}")]

            kwargs = arguments or {}
            try:
                result = await tool.execute(**kwargs)
            except Exception as exc:
                return [mcp_types.TextContent(type="text", text=f"Tool error: {exc}")]

            text = result if isinstance(result, str) else json.dumps(result)
            return [mcp_types.TextContent(type="text", text=text)]

    async def run(self) -> None:
        """Run the MCP stdio server (blocks until the client disconnects)."""
        options = self._server.create_initialization_options()
        async with stdio_server() as (read_stream, write_stream):
            await self._server.run(read_stream, write_stream, options)


# ---------------------------------------------------------------------------
# Subprocess entry point
# ---------------------------------------------------------------------------


async def _run_ipc_bridge(socket_path: str) -> None:
    """Run as MCP stdio server, proxying tool calls to the main process via IPC."""
    client = _ToolIPCClient(socket_path)
    await client.connect()

    tool_defs = await client.list_tools()
    _log.info("mcp bridge connected via ipc", socket=socket_path, tools=len(tool_defs))

    server = mcp.server.Server("otto-tools")

    @server.list_tools()
    async def list_tools() -> list[mcp_types.Tool]:
        return [
            mcp_types.Tool(
                name=_bridge_tool_name(t["name"]),
                description=t["description"],
                inputSchema=t["parameters"],
            )
            for t in tool_defs
        ]

    @server.call_tool()
    async def call_tool(
        name: str, arguments: dict[str, Any] | None = None
    ) -> list[mcp_types.TextContent]:
        gw_name = _gateway_tool_name(name)
        try:
            result = await client.call_tool(gw_name, arguments or {})
            return [mcp_types.TextContent(type="text", text=result)]
        except Exception as exc:
            return [mcp_types.TextContent(type="text", text=f"Tool error: {exc}")]

    options = server.create_initialization_options()
    try:
        async with stdio_server() as (read_stream, write_stream):
            await server.run(read_stream, write_stream, options)
    finally:
        await client.close()


def _run_standalone() -> None:
    """Entry point for ``python -m otto.acp.mcp_bridge``.

    When ``OTTO_MCP_SOCKET`` is set, connects to the main process via IPC.
    Otherwise falls back to an empty Gateway (for protocol testing only).
    """
    socket_path = os.environ.get(_SOCKET_ENV_VAR)
    if socket_path:
        asyncio.run(_run_ipc_bridge(socket_path))
    else:
        from otto.gateway import Policy

        gateway = Gateway(Policy(blocked_paths=[], max_calls_per_session=0, allowed_servers=None))
        bridge = OttoMCPBridge(gateway)
        asyncio.run(bridge.run())


if __name__ == "__main__":
    _run_standalone()
