"""Internal MCP server that lets Navi push UI components to the webclient.

The server exposes a single tool, ``render_component``.  When Navi calls it,
the payload is forwarded over the active WebSocket for the session as a
``ui_component`` event.  The webclient receives the event and renders the
named component with the supplied data.

This server is started by ``navi.main:lifespan`` on a dedicated port
(``NAVI_UI_MCP_PORT``, default 8001) before the main container is built so
that :class:`navi.mcp.McpManager` can connect to it during startup like any
other MCP server.
"""

from __future__ import annotations

import asyncio
import logging
from typing import Any

import structlog
from mcp.server import FastMCP

logger = logging.getLogger(__name__)

# Shared holder: the lifespan task sets the orchestrator once the main
# FastAPI container is ready.  The tool waits (with a timeout) so that early
# MCP health-check pings or tool calls do not crash before startup completes.
_orchestrator: Any | None = None
_orchestrator_ready = asyncio.Event()

_ORCHESTRATOR_TIMEOUT = 10.0

# Component schema documentation embedded into server instructions.
# This is separate from the webclient implementation; both must agree on the
# same contract, but the server only validates the envelope.
SERVER_INSTRUCTIONS = """\
Internal Navi UI server. Use render_component to show structured UI in the webclient.

Tool: render_component(component_name, payload, session_id)
- component_name: only "card_grid" is supported.
- payload: JSON object. See schema below.
- session_id: DO NOT pass manually. It is injected automatically by the agent.

## card_grid payload schema

{
  "title": "Optional section heading",
  "cards": [
    {
      "id": "required-string-id",
      "title": "Required card title",
      "subtitle": "Optional secondary line",
      "image": "https://optional-direct-image-url",
      "meta": [
        {"label": "Label", "value": "Value"}
      ],
      "description": "Optional short one-line summary",
      "details": [
        {"label": "Detail label", "value": "Detail value"}
      ],
      "actions": [
        {"label": "Button label", "url": "https://..."}
      ]
    }
  ]
}

Hard rules:
1. component_name must be "card_grid".
2. payload.cards is required and must be a non-empty list.
3. Every card must have id (string) and title (string).
4. Limit to 4 cards in one call. If there are more, say there are more and ask if the user wants them.
5. image must be a direct URL, never a local path.
6. Put the full information that should appear after a card click into details.
7. After calling render_component, still provide a short text summary and offer next steps.
"""

mcp = FastMCP(
    "navi_ui",
    host="127.0.0.1",
    port=8001,
    streamable_http_path="/mcp",
    instructions=SERVER_INSTRUCTIONS,
)


def _is_valid_card_grid(payload: dict[str, Any]) -> tuple[bool, str]:
    """Lightweight structural validation for the card_grid component.

    Returns (ok, error_message).  Does not validate URL reachability.
    """
    cards = payload.get("cards")
    if not isinstance(cards, list):
        return False, "card_grid payload must contain a 'cards' list"
    if len(cards) == 0:
        return False, "card_grid 'cards' list must not be empty"
    for idx, card in enumerate(cards):
        if not isinstance(card, dict):
            return False, f"card at index {idx} must be an object"
        if not isinstance(card.get("id"), str) or not card["id"]:
            return False, f"card at index {idx} must have a non-empty string 'id'"
        if not isinstance(card.get("title"), str) or not card["title"]:
            return False, f"card at index {idx} must have a non-empty string 'title'"
    return True, ""


@mcp.tool()
async def render_component(
    component_name: str,
    payload: dict[str, Any],
    session_id: str | None = None,
) -> str:
    """Render a UI component in the webclient for the given session.

    Args:
        component_name: Identifier of the registered UI component (e.g. "table").
        payload: JSON-serializable data object consumed by that component.
        session_id: Navi session id. Injected automatically by the agent context.

    Returns:
        A short confirmation string, or an error message if forwarding failed.
    """
    if not component_name or not isinstance(component_name, str):
        return "Error: component_name must be a non-empty string"
    if not isinstance(payload, dict):
        return "Error: payload must be a JSON object (dict)"
    if not session_id:
        return "Error: session_id is required (it is normally injected by the agent)"

    if component_name == "card_grid":
        ok, error = _is_valid_card_grid(payload)
        if not ok:
            return f"Error: {error}"

    try:
        await asyncio.wait_for(
            _orchestrator_ready.wait(),
            timeout=_ORCHESTRATOR_TIMEOUT,
        )
    except asyncio.TimeoutError:
        return "Error: UI server is not ready (orchestrator unavailable)"

    orchestrator = _orchestrator
    if orchestrator is None:
        return "Error: UI server orchestrator reference is missing"

    try:
        await orchestrator._notify_session(
            session_id,
            {
                "type": "ui_component",
                "component": component_name,
                "payload": payload,
            },
        )
    except Exception as exc:
        logger.warning("render_component failed for session %s: %s", session_id, exc)
        return f"Error: failed to send component to session: {exc}"

    return f"Component {component_name!r} rendered for session {session_id}"


def set_orchestrator(orchestrator: Any) -> None:
    """Wire the active orchestrator so the tool can reach WebSocket clients."""
    global _orchestrator
    _orchestrator = orchestrator
    _orchestrator_ready.set()
    logger.info("navi_ui orchestrator wired")


def clear_orchestrator() -> None:
    """Clear the orchestrator reference, e.g. during graceful shutdown."""
    global _orchestrator
    _orchestrator = None
    _orchestrator_ready.clear()


async def start_ui_server(host: str, port: int) -> None:
    """Start the StreamableHTTP MCP server and run forever.

    This coroutine is intended to be scheduled as a background task from the
    main application lifespan.  It only returns when the server is stopped.
    """
    log = structlog.get_logger()
    log.info("navi_ui_mcp_server_starting", host=host, port=port)
    # FastMCP copies constructor args into its settings object, so update them
    # explicitly to honour runtime configuration.
    mcp.settings.host = host
    mcp.settings.port = port
    await mcp.run_streamable_http_async()
