"""
Jedyny serwer MCP widoczny dla Claude'a.

Odpowiedzialność:
- Implementuje protokół MCP (tools/list, tools/call)
- Przy tools/list: zwraca zagregowaną listę wszystkich narzędzi ze wszystkich backendów (z Registry)
- Przy tools/call: rozwiązuje nazwę narzędzia przez Registry, deleguje wywołanie do Executora
- Tłumaczy wyniki z formatu backendowego na format MCP odpowiedzi
- Punkt wejścia dla Claude'a — nie zawiera logiki biznesowej, tylko orkiestruje przepływ
"""

import contextlib
import logging
import weakref
from collections.abc import AsyncIterator

import base64

import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.session import ServerSession
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from pydantic import AnyUrl
from starlette.applications import Starlette
from starlette.routing import Mount

from .executor import Executor
from .management import BuiltinTool
from .registry import Registry

logger = logging.getLogger(__name__)


class Gateway:
    def __init__(
        self,
        registry: Registry,
        executor: Executor,
        name: str = "mcp-gateway",
        builtin_tools: list[BuiltinTool] | None = None,
    ):
        self._registry = registry
        self._executor = executor
        self._builtin_tools: dict[str, BuiltinTool] = {t.name: t for t in (builtin_tools or [])}
        self._server = Server(name)
        self._session_manager = StreamableHTTPSessionManager(
            app=self._server,
            json_response=False,
            stateless=False,
        )
        self._sessions: weakref.WeakSet[ServerSession] = weakref.WeakSet()
        self._register_handlers()

    def _register_handlers(self) -> None:
        @self._server.list_tools()
        async def list_tools() -> list[types.Tool]:
            try:
                self._sessions.add(self._server.request_context.session)
            except LookupError:
                pass
            return [
                types.Tool(
                    name=m.public_name,
                    description=m.definition.description,
                    inputSchema=m.definition.input_schema,
                )
                for m in self._registry.list_tools()
            ]

        @self._server.call_tool()
        async def call_tool(name: str, arguments: dict | None) -> types.CallToolResult:
            try:
                mapping = self._registry.resolve(name)
            except KeyError:
                return types.CallToolResult(
                    content=[types.TextContent(type="text", text=f"Unknown tool: {name}")],
                    isError=True,
                )
            try:
                return await self._executor.call_tool(mapping, arguments or {})
            except Exception as e:
                logger.error("Tool call failed for %s: %s", name, e)
                return types.CallToolResult(
                    content=[types.TextContent(type="text", text=f"Błąd wywołania narzędzia: {e}")],
                    isError=True,
                )

        @self._server.list_resources()
        async def list_resources() -> list[types.Resource]:
            try:
                self._sessions.add(self._server.request_context.session)
            except LookupError:
                pass
            return [
                types.Resource(
                    uri=AnyUrl(m.public_uri),
                    name=m.descriptor.name,
                    description=m.descriptor.description,
                    mimeType=m.descriptor.mime_type,
                )
                for m in self._registry.list_resources()
            ]

        @self._server.list_resource_templates()
        async def list_resource_templates() -> list[types.ResourceTemplate]:
            try:
                self._sessions.add(self._server.request_context.session)
            except LookupError:
                pass
            return [
                types.ResourceTemplate(
                    uriTemplate=m.public_template,
                    name=m.descriptor.name,
                    description=m.descriptor.description,
                    mimeType=m.descriptor.mime_type,
                )
                for m in self._registry.list_resource_templates()
            ]

        @self._server.read_resource()
        async def read_resource(uri: AnyUrl) -> list[ReadResourceContents]:
            backend, original_uri = self._registry.resolve_resource_or_template(str(uri))
            result = await self._executor.read_resource_uri(backend, original_uri)
            contents: list[ReadResourceContents] = []
            for c in result.contents:
                if isinstance(c, types.TextResourceContents):
                    contents.append(ReadResourceContents(content=c.text, mime_type=c.mimeType))
                elif isinstance(c, types.BlobResourceContents):
                    contents.append(
                        ReadResourceContents(
                            content=base64.b64decode(c.blob),
                            mime_type=c.mimeType,
                        )
                    )
                else:
                    raise TypeError(
                        f"Unsupported resource content type from backend {backend.id}: "
                        f"{type(c).__name__}"
                    )
            return contents

        @self._server.subscribe_resource()
        async def subscribe_resource(uri: AnyUrl) -> None:
            backend, original_uri = self._registry.resolve_resource_or_template(str(uri))
            await self._executor.subscribe_resource(backend, original_uri)

        @self._server.unsubscribe_resource()
        async def unsubscribe_resource(uri: AnyUrl) -> None:
            backend, original_uri = self._registry.resolve_resource_or_template(str(uri))
            await self._executor.unsubscribe_resource(backend, original_uri)

    async def notify_tool_list_changed(self) -> None:
        for session in list(self._sessions):
            try:
                await session.send_tool_list_changed()
            except Exception:
                logger.debug("Failed to notify session, likely disconnected")

    async def notify_resource_list_changed(self) -> None:
        for session in list(self._sessions):
            try:
                await session.send_resource_list_changed()
            except Exception:
                logger.debug("Failed to notify session, likely disconnected")

    async def notify_resource_updated(self, uri: str) -> None:
        for session in list(self._sessions):
            try:
                await session.send_resource_updated(AnyUrl(uri))
            except Exception:
                logger.debug("Failed to notify session, likely disconnected")

    def create_app(self) -> Starlette:
        @contextlib.asynccontextmanager
        async def lifespan(app: Starlette) -> AsyncIterator[None]:
            async with self._session_manager.run():
                yield

        return Starlette(
            lifespan=lifespan,
            routes=[
                Mount("/mcp", app=self._session_manager.handle_request),
            ],
        )
