"""
Wbudowany serwer MCP do zarządzania backendami gateway.

Odpowiedzialność:
- Uruchamia lokalny serwer MCP na podanym porcie
- Eksponuje narzędzia management__* (add/remove/list/refresh/enable/disable*)
- Traktowany przez gateway jako zwykły backend — brak specjalnej logiki
"""

from __future__ import annotations

import contextlib
import json
import logging
from collections.abc import AsyncIterator
from typing import TYPE_CHECKING

import mcp.types as types
import uvicorn
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from starlette.applications import Starlette
from starlette.routing import Mount

if TYPE_CHECKING:
    from .orchestrator import Orchestrator
    from .registry import Registry

logger = logging.getLogger(__name__)

TOOL_DEFINITIONS = [
    types.Tool(
        name="add_mcp_server",
        description=(
            "Rejestruje nowy serwer MCP jako backend gateway i ładuje z niego "
            "narzędzia oraz zasoby (jeśli backend je deklaruje). Narzędzia backendu "
            "są wystawione pod nazwami z prefiksem '{id}__{nazwa}' (np. po dodaniu "
            "id='files' narzędzie 'read' będzie widoczne jako 'files__read') — pod "
            "tymi nazwami się je później woła. URI zasobów są wystawiane bez prefiksu; "
            "kolizja URI z innym backendem skutkuje błędem rejestracji. Domyślnie wpis "
            "trafia do config.json i wraca po restarcie gateway; ustaw transient=true "
            "jeśli backend ma być tylko na czas tej sesji. Zwraca komunikat tekstowy "
            "z liczbą zarejestrowanych narzędzi."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "description": (
                        "Unikalny identyfikator backendu, użyty jako prefix nazw "
                        "narzędzi (np. 'files' → narzędzia 'files__*'). Konwencja: snake_case."
                    ),
                },
                "url": {
                    "type": "string",
                    "description": (
                        "URL endpointu MCP backendu, transport streamable-HTTP "
                        "(np. 'http://localhost:8124/mcp/' lub 'https://host/mcp/endpoint')."
                    ),
                },
                "transient": {
                    "type": "boolean",
                    "description": (
                        "Jeśli true, wpis NIE jest zapisywany do config.json — backend "
                        "znika po restarcie gateway. Domyślnie false (persistowany)."
                    ),
                },
            },
            "required": ["id", "url"],
        },
    ),
    types.Tool(
        name="remove_mcp_server",
        description=(
            "Trwale usuwa backend z gateway: zamyka połączenie, czyści jego narzędzia "
            "i zasoby, oraz usuwa wpis z config.json. Operacja nieodwracalna — żeby "
            "tymczasowo wyłączyć backend zachowując konfigurację, użyj 'disable_mcp_server'."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "description": "Identyfikator backendu (z 'list_mcp_servers') do trwałego usunięcia.",
                },
            },
            "required": ["id"],
        },
    ),
    types.Tool(
        name="list_mcp_servers",
        description=(
            "Zwraca JSON z listą wszystkich zarejestrowanych backendów: każdy wpis ma "
            "pola 'id', 'url' i 'enabled' (bool — czy backend jest aktywny). Backend "
            "wyłączony przez 'disable_mcp_server' nadal jest na liście z enabled=false. "
            "Bez argumentów. Gdy lista jest pusta zwraca tekst 'Brak zarejestrowanych backendów.'"
        ),
        inputSchema={
            "type": "object",
            "properties": {},
        },
    ),
    types.Tool(
        name="refresh_mcp_server",
        description=(
            "Ponownie pobiera z backendu pełną listę narzędzi i zasobów oraz aktualizuje "
            "rejestr gateway. Stosuj gdy backend dodał/zmienił/usunął narzędzia lub zasoby "
            "i chcesz to zsynchronizować bez ręcznego remove+add. Zwraca komunikat z aktualną "
            "listą nazw narzędzi."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "description": "Identyfikator backendu do odświeżenia (z 'list_mcp_servers').",
                },
            },
            "required": ["id"],
        },
    ),
    types.Tool(
        name="disable_mcp_server",
        description=(
            "Tymczasowo wyłącza backend: zamyka połączenie, usuwa jego narzędzia i zasoby "
            "z gateway, ale wpis pozostaje w config.json z enabled=false. Można później "
            "przywrócić przez 'enable_mcp_server' (bez ponownego podawania URL). "
            "Stosuj żeby ograniczyć kontekst dostępnych narzędzi bez utraty konfiguracji "
            "— w przeciwieństwie do 'remove_mcp_server' jest odwracalne."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "description": "Identyfikator backendu (z 'list_mcp_servers') do wyłączenia.",
                },
            },
            "required": ["id"],
        },
    ),
    types.Tool(
        name="enable_mcp_server",
        description=(
            "Włącza backend wcześniej wyłączony przez 'disable_*': nawiązuje połączenie i "
            "rejestruje narzędzia oraz zasoby. Wymaga, żeby backend był już zarejestrowany "
            "(po add_mcp_server lub z config.json). Zwraca komunikat z liczbą narzędzi."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "id": {
                    "type": "string",
                    "description": "Identyfikator wcześniej wyłączonego backendu do włączenia.",
                },
            },
            "required": ["id"],
        },
    ),
    types.Tool(
        name="disable_all_mcp_servers",
        description=(
            "Wyłącza WSZYSTKIE zarejestrowane backendy oprócz wbudowanego 'management'. "
            "Wpisy pozostają w config.json (enabled=false), więc każdy z nich można pojedynczo "
            "wrócić przez 'enable_mcp_server'. Stosuj żeby drastycznie zmniejszyć liczbę "
            "widocznych narzędzi (np. dla skupienia kontekstu) — zostaje tylko management. "
            "Bez argumentów. Zwraca listę ID wyłączonych backendów."
        ),
        inputSchema={
            "type": "object",
            "properties": {},
        },
    ),
    types.Tool(
        name="disable_mcp_servers",
        description=(
            "Wyłącza wybrane backendy (lista ID) — wariant 'disable_mcp_server' dla wielu "
            "naraz. Każdy ID jest przetwarzany niezależnie: błąd jednego nie przerywa pozostałych, "
            "a w odpowiedzi dostajesz raport (które wyłączono, które wywaliły błąd). "
            "Stosuj gdy chcesz jednym wywołaniem zostawić aktywne tylko niektóre backendy."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "ids": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Lista identyfikatorów backendów do wyłączenia (np. ['files', 'video']).",
                },
            },
            "required": ["ids"],
        },
    ),
]


class ManagementServer:
    def __init__(self, orchestrator: Orchestrator, registry: Registry, port: int = 8999):
        self._orchestrator = orchestrator
        self._registry = registry
        self._port = port
        self._server = Server("mcp-management")
        self._session_manager = StreamableHTTPSessionManager(
            app=self._server,
            json_response=False,
            stateless=True,
        )
        self._register_handlers()

    def _register_handlers(self) -> None:
        @self._server.list_tools()
        async def list_tools() -> list[types.Tool]:
            return TOOL_DEFINITIONS

        @self._server.call_tool()
        async def call_tool(name: str, arguments: dict | None) -> types.CallToolResult:
            args = arguments or {}
            try:
                result = await self._dispatch(name, args)
                return types.CallToolResult(
                    content=[types.TextContent(type="text", text=result)],
                    isError=False,
                )
            except Exception as e:
                return types.CallToolResult(
                    content=[types.TextContent(type="text", text=f"Błąd: {e}")],
                    isError=True,
                )

    async def _dispatch(self, name: str, args: dict) -> str:
        if name == "add_mcp_server":
            backend_id = args["id"]
            url = args["url"]
            transient = bool(args.get("transient", False))
            await self._orchestrator.add_backend(backend_id, url, transient=transient)
            tools = [m.public_name for m in self._registry.list_tools() if m.backend.id == backend_id]
            return f"Dodano backend '{backend_id}' ({url}). Zarejestrowano {len(tools)} narzędzi: {', '.join(tools)}"

        if name == "remove_mcp_server":
            backend_id = args["id"]
            await self._orchestrator.remove_backend(backend_id)
            return f"Usunięto backend '{backend_id}'."

        if name == "list_mcp_servers":
            backends = list(self._registry._backends.values())
            if not backends:
                return "Brak zarejestrowanych backendów."
            return json.dumps(
                [{"id": b.id, "url": b.url, "enabled": b.enabled} for b in backends],
                ensure_ascii=False,
                indent=2,
            )

        if name == "refresh_mcp_server":
            backend_id = args["id"]
            await self._orchestrator.refresh_backend(backend_id)
            tools = [m.public_name for m in self._registry.list_tools() if m.backend.id == backend_id]
            return f"Odświeżono backend '{backend_id}'. Aktualne narzędzia ({len(tools)}): {', '.join(tools)}"

        if name == "disable_mcp_server":
            backend_id = args["id"]
            await self._orchestrator.disable_backend(backend_id)
            return f"Wyłączono backend '{backend_id}'."

        if name == "enable_mcp_server":
            backend_id = args["id"]
            await self._orchestrator.enable_backend(backend_id)
            tools = [m.public_name for m in self._registry.list_tools() if m.backend.id == backend_id]
            return f"Włączono backend '{backend_id}'. Zarejestrowano {len(tools)} narzędzi: {', '.join(tools)}"

        if name == "disable_all_mcp_servers":
            disabled = await self._orchestrator.disable_all_backends()
            if not disabled:
                return "Brak aktywnych backendów do wyłączenia."
            return f"Wyłączono {len(disabled)} backendów: {', '.join(disabled)}"

        if name == "disable_mcp_servers":
            ids = args["ids"]
            disabled, errors = [], []
            for backend_id in ids:
                try:
                    await self._orchestrator.disable_backend(backend_id)
                    disabled.append(backend_id)
                except Exception as e:
                    errors.append(f"{backend_id}: {e}")
            parts = []
            if disabled:
                parts.append(f"Wyłączono {len(disabled)}: {', '.join(disabled)}")
            if errors:
                parts.append(f"Błędy — {'; '.join(errors)}")
            return ". ".join(parts) if parts else "Nic nie wyłączono."

        raise ValueError(f"Nieznane narzędzie: {name}")

    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)],
        )

    async def start(self) -> None:
        """Startuje serwer w tle jako asyncio task."""
        import asyncio
        app = self.create_app()
        config = uvicorn.Config(app, host="127.0.0.1", port=self._port, log_level="warning")
        server = uvicorn.Server(config)
        asyncio.get_event_loop().create_task(server.serve())
        for _ in range(50):
            if server.started:
                break
            await asyncio.sleep(0.1)
        logger.info(f"Management server nasłuchuje na http://127.0.0.1:{self._port}/mcp/")

    @property
    def url(self) -> str:
        return f"http://127.0.0.1:{self._port}/mcp"
