# @File: backend/services/uptimerobot/mcp.py
# @Desc: UptimeRobot Model Context Protocol (MCP) integration helpers.
#
# The MCP integration runs at mcp.uptimerobot.com and is consumed by LLM
# clients (Claude Desktop, Cursor, OpenAI agents, …). Our backend does not
# execute MCP calls itself — instead we surface:
#   * the endpoint URL and auth headers the client needs,
#   * a ready-to-copy JSON config block for ``mcp-remote`` style bridges,
#   * a lightweight connectivity probe so the admin UI can show a
#     "MCP reachable" badge without storing another credential.
#
# Reference: https://help.uptimerobot.com/en/articles/12928342-uptimerobot-mcp-integration-guide
from __future__ import annotations

import json
import logging
from typing import Optional

import httpx

from services.uptimerobot.client import get_master_api_key, mask_api_key

logger = logging.getLogger(__name__)

# Public endpoint documented by UptimeRobot. We expose this as a constant so
# it can be swapped via env without touching callers if UptimeRobot ever
# publishes a regional endpoint.
MCP_ENDPOINT_URL = "https://mcp.uptimerobot.com/mcp"

# Headers the MCP bridge must send. ``Authorization`` is filled per-tenant
# from the master API key; ``X-MCP-Content-Format`` is required by the
# UptimeRobot MCP server.
_STATIC_HEADERS: dict[str, str] = {"X-MCP-Content-Format": "true"}

# Curated capability list mirrored from UptimeRobot's public docs so the
# admin UI can describe what the MCP channel unlocks without a live call.
MCP_CAPABILITIES: list[dict[str, str]] = [
    {
        "id": "monitor_management",
        "title": "Monitor management",
        "description": (
            "List monitors, inspect monitor details, fetch stats and response "
            "times, pause or resume monitors, update configuration, and create "
            "new HTTP / KEYWORD / PING / PORT / DNS / HEARTBEAT monitors."
        ),
    },
    {
        "id": "incident_management",
        "title": "Incident management",
        "description": (
            "List incidents by time range, monitor id, or status and retrieve "
            "timelines, error codes, request logs, and checker locations."
        ),
    },
    {
        "id": "integration_management",
        "title": "Integration management",
        "description": (
            "List and inspect alert integrations such as email, SMS, voice, "
            "mobile app, and webhook contacts."
        ),
    },
]

# Short natural-language prompts the admin UI can show as copy-paste samples.
MCP_SAMPLE_PROMPTS: list[str] = [
    "Show me all monitors that are currently down.",
    "What is the 30-day availability of my checkout monitor?",
    "List incidents that happened in the last 24 hours.",
    "Pause monitor 801150533 for maintenance.",
]


def _build_mcp_config(api_key: Optional[str]) -> dict:
    """Return the ``mcpServers`` config block expected by Claude Desktop /
    Cursor / any ``mcp-remote`` based LLM client.

    When no API key is configured yet we still return the skeleton so the
    admin UI can render a grey-state preview (the secret is rendered as an
    obvious placeholder the admin must replace before pasting).
    """
    bearer_placeholder = api_key.strip() if api_key else "<your-uptimerobot-api-key>"
    return {
        "mcpServers": {
            "uptimerobot": {
                "command": "npx",
                "args": [
                    "-y",
                    "mcp-remote",
                    MCP_ENDPOINT_URL,
                    "--header",
                    f"Authorization: Bearer {bearer_placeholder}",
                    "--header",
                    "X-MCP-Content-Format: true",
                ],
            }
        }
    }


async def get_mcp_config_payload() -> dict:
    """Build the read-side payload consumed by the admin page.

    Returns a dict with:
      * ``endpoint`` / ``auth_scheme`` / ``static_headers`` — raw facts,
      * ``api_key_present`` + ``api_key_masked`` + ``api_key_source`` —
        enough to tell the admin whether the MCP bridge will actually work,
      * ``config_json`` — a pre-rendered ``claude_desktop_config.json``
        snippet ready for copy/paste,
      * ``capabilities`` / ``sample_prompts`` — static copy for the UI.

    The raw API key is NEVER returned. Only the masked form is exposed.
    """
    api_key, source = await get_master_api_key()
    masked = mask_api_key(api_key) if api_key else None
    config_block = _build_mcp_config(api_key)

    return {
        "endpoint": MCP_ENDPOINT_URL,
        "auth_scheme": "Bearer",
        "static_headers": dict(_STATIC_HEADERS),
        "api_key_present": bool(api_key),
        "api_key_masked": masked,
        "api_key_source": source,
        "config_json": json.dumps(config_block, indent=2),
        "capabilities": list(MCP_CAPABILITIES),
        "sample_prompts": list(MCP_SAMPLE_PROMPTS),
        "doc_url": (
            "https://help.uptimerobot.com/en/articles/"
            "12928342-uptimerobot-mcp-integration-guide"
        ),
    }


async def probe_mcp_endpoint(timeout_seconds: float = 6.0) -> dict:
    """Cheap connectivity probe used by the admin "Test connection" button.

    We purposely keep this lightweight: we issue a HEAD (fallback to GET)
    against ``MCP_ENDPOINT_URL`` with the current master API key and report
    whether the remote end responded. This is *not* a full MCP handshake —
    the actual JSON-RPC negotiation happens inside the LLM client's bridge.
    A 2xx/3xx/4xx status with a response still proves the network path
    works; only timeouts / DNS errors / 5xx count as a failure.
    """
    api_key, source = await get_master_api_key()
    if not api_key:
        return {
            "ok": False,
            "error_class": "NoApiKey",
            "error_message": (
                "No UptimeRobot API key is configured. Save the master "
                "API key first under API Settings."
            ),
            "api_key_source": source,
            "http_status": None,
        }

    headers = {
        "Authorization": f"Bearer {api_key}",
        **_STATIC_HEADERS,
        "Accept": "application/json, text/event-stream",
    }

    try:
        async with httpx.AsyncClient(timeout=timeout_seconds, trust_env=True) as client:
            try:
                resp = await client.head(MCP_ENDPOINT_URL, headers=headers)
            except httpx.HTTPError:
                resp = await client.get(MCP_ENDPOINT_URL, headers=headers)
    except httpx.TimeoutException as exc:
        return {
            "ok": False,
            "error_class": "Timeout",
            "error_message": f"MCP endpoint did not respond within {timeout_seconds:.0f}s.",
            "http_status": None,
            "api_key_source": source,
            "detail": str(exc),
        }
    except httpx.HTTPError as exc:
        logger.warning("UptimeRobot MCP probe failed: %s", exc)
        return {
            "ok": False,
            "error_class": "HttpError",
            "error_message": f"Network error contacting MCP endpoint: {exc}",
            "http_status": None,
            "api_key_source": source,
        }

    # Any structured HTTP response from the MCP host means the route is
    # reachable. We still flag >= 500 as a failure because that means the
    # upstream is down.
    if resp.status_code >= 500:
        return {
            "ok": False,
            "error_class": f"HTTP{resp.status_code}",
            "error_message": (
                f"MCP endpoint returned HTTP {resp.status_code}. "
                "UptimeRobot's MCP service may be unavailable."
            ),
            "http_status": resp.status_code,
            "api_key_source": source,
        }

    return {
        "ok": True,
        "http_status": resp.status_code,
        "api_key_source": source,
        "endpoint": MCP_ENDPOINT_URL,
    }