import argparse
import json
import os
import time
import sys
import uuid
from typing import Any
from urllib import error, request
from urllib.parse import urlparse

import paho.mqtt.client as mqtt

DEFAULT_BASE_URL = "http://localhost:8008"
USER_AGENT = "missioncontrol-mcp/0.1.0"
DEFAULT_HTTP_TIMEOUT_SEC = 20.0
DEFAULT_HTTP_RETRIES = 2
DEFAULT_HTTP_RETRY_BACKOFF_MS = 250
DEFAULT_STARTUP_PREFLIGHT = "health"
DEFAULT_HEALTHCHECK_PATH = "/"
DEFAULT_FAIL_OPEN_ON_LIST = False

PREFLIGHT_MODES = {"none", "health", "tools"}


class McpProtocolError(Exception):
    pass


class MissionControlHttpError(RuntimeError):
    def __init__(self, category: str, message: str):
        super().__init__(message)
        self.category = category


def log(message: str) -> None:
    sys.stderr.write(f"[missioncontrol-mcp] {message}\n")
    sys.stderr.flush()


def parse_bool_env(name: str, default: bool) -> bool:
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def parse_int_env(name: str, default: int) -> int:
    raw = os.getenv(name)
    if raw is None:
        return default
    try:
        return int(raw)
    except ValueError:
        return default


def parse_float_env(name: str, default: float) -> float:
    raw = os.getenv(name)
    if raw is None:
        return default
    try:
        value = float(raw)
    except ValueError:
        return default
    if value <= 0:
        return default
    return value


def base_urls() -> list[str]:
    configured = os.getenv("MISSIONCONTROL_BASE_URLS", "").strip()
    if configured:
        items = [x.strip().rstrip("/") for x in configured.split(",") if x.strip()]
        if items:
            return items
    return [os.getenv("MISSIONCONTROL_BASE_URL", DEFAULT_BASE_URL).strip().rstrip("/")]


def build_api_url(base_url: str, path: str) -> str:
    if not path.startswith("/"):
        path = "/" + path
    return f"{base_url}{path}"


def classify_http_error(exc: Exception) -> str:
    if isinstance(exc, error.HTTPError):
        if exc.code in {401, 403}:
            return "auth_error"
        if 500 <= exc.code <= 599:
            return "http_5xx"
        return "http_4xx"
    if isinstance(exc, TimeoutError):
        return "timeout"
    if isinstance(exc, error.URLError):
        reason = str(exc.reason).lower()
        if "name or service not known" in reason or "temporary failure in name resolution" in reason:
            return "dns_error"
        if "timed out" in reason:
            return "timeout"
        if "ssl" in reason or "tls" in reason or "certificate" in reason:
            return "tls_error"
        return "network_error"
    return "unknown_error"


def _is_retryable(exc: Exception) -> bool:
    if isinstance(exc, error.HTTPError):
        return 500 <= exc.code <= 599
    if isinstance(exc, (error.URLError, TimeoutError)):
        return True
    return False


class MissionControlHttpClient:
    def __init__(self) -> None:
        self._base_urls = base_urls()
        self._timeout_sec = parse_float_env("MISSIONCONTROL_HTTP_TIMEOUT_SEC", DEFAULT_HTTP_TIMEOUT_SEC)
        self._retries = max(0, parse_int_env("MISSIONCONTROL_HTTP_RETRIES", DEFAULT_HTTP_RETRIES))
        self._retry_backoff_ms = max(0, parse_int_env("MISSIONCONTROL_HTTP_RETRY_BACKOFF_MS", DEFAULT_HTTP_RETRY_BACKOFF_MS))
        self._token = os.getenv("MISSIONCONTROL_TOKEN")
        self._last_success_base_url: str | None = None

    @property
    def preferred_base_url(self) -> str:
        if self._last_success_base_url:
            return self._last_success_base_url
        return self._base_urls[0] if self._base_urls else DEFAULT_BASE_URL

    @property
    def base_url_candidates(self) -> list[str]:
        if self._last_success_base_url and self._last_success_base_url in self._base_urls:
            remaining = [u for u in self._base_urls if u != self._last_success_base_url]
            return [self._last_success_base_url] + remaining
        return list(self._base_urls)

    def _request_once(self, *, base_url: str, method: str, path: str, payload: dict[str, Any] | None) -> Any:
        url = build_api_url(base_url, path)
        data = None if payload is None else json.dumps(payload).encode("utf-8")
        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "User-Agent": USER_AGENT,
        }
        if self._token:
            headers["Authorization"] = f"Bearer {self._token}"
        req = request.Request(url=url, data=data, headers=headers, method=method)
        with request.urlopen(req, timeout=self._timeout_sec) as resp:
            body = resp.read().decode("utf-8")
            if not body:
                return {}
            return json.loads(body)

    def http_json(self, method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
        request_id = str(uuid.uuid4())[:8]
        attempts = self._retries + 1
        last_error: Exception | None = None
        last_error_category = "unknown_error"

        for attempt in range(1, attempts + 1):
            for base_url in self.base_url_candidates:
                try:
                    result = self._request_once(
                        base_url=base_url,
                        method=method,
                        path=path,
                        payload=payload,
                    )
                    self._last_success_base_url = base_url
                    return result
                except Exception as exc:  # pragma: no cover - error classification is tested indirectly
                    last_error = exc
                    last_error_category = classify_http_error(exc)
                    if isinstance(exc, error.HTTPError):
                        detail = exc.read().decode("utf-8", errors="replace")
                        message = f"MissionControl API HTTP {exc.code}: {detail}"
                    else:
                        message = f"MissionControl API unreachable: {exc}"
                    log(
                        "request_id="
                        f"{request_id} category={last_error_category} method={method} path={path} "
                        f"base_url={base_url} attempt={attempt}/{attempts} error={message}"
                    )
                    if not _is_retryable(exc):
                        raise MissionControlHttpError(last_error_category, message) from exc

            if attempt < attempts:
                sleep_sec = (self._retry_backoff_ms / 1000.0) * attempt
                time.sleep(sleep_sec)

        if isinstance(last_error, error.HTTPError):
            detail = last_error.read().decode("utf-8", errors="replace")
            message = f"MissionControl API HTTP {last_error.code}: {detail}"
        elif last_error is not None:
            message = f"MissionControl API unreachable: {last_error}"
        else:
            message = "MissionControl API unreachable: unknown error"
        raise MissionControlHttpError(last_error_category, message) from last_error


_HTTP_CLIENT_SINGLETON: MissionControlHttpClient | None = None


def http_json(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
    """
    Backward-compatible helper for explorer/admin CLIs.
    Uses a lazily initialized shared client.
    """
    global _HTTP_CLIENT_SINGLETON
    if _HTTP_CLIENT_SINGLETON is None:
        _HTTP_CLIENT_SINGLETON = MissionControlHttpClient()
    return _HTTP_CLIENT_SINGLETON.http_json(method, path, payload)


def read_message() -> dict[str, Any] | None:
    content_length = None
    while True:
        line = sys.stdin.buffer.readline()
        if not line:
            return None
        if line in (b"\r\n", b"\n"):
            break
        key, sep, value = line.decode("utf-8").partition(":")
        if sep != ":":
            raise McpProtocolError(f"Invalid header line: {line!r}")
        if key.lower().strip() == "content-length":
            content_length = int(value.strip())

    if content_length is None:
        raise McpProtocolError("Missing Content-Length header")

    body = sys.stdin.buffer.read(content_length)
    if len(body) != content_length:
        raise McpProtocolError("Unexpected EOF while reading message body")

    return json.loads(body.decode("utf-8"))


def write_message(payload: dict[str, Any]) -> None:
    body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
    header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
    sys.stdout.buffer.write(header)
    sys.stdout.buffer.write(body)
    sys.stdout.buffer.flush()


def rpc_result(msg_id: Any, result: dict[str, Any]) -> dict[str, Any]:
    return {"jsonrpc": "2.0", "id": msg_id, "result": result}


def rpc_error(msg_id: Any, code: int, message: str) -> dict[str, Any]:
    return {
        "jsonrpc": "2.0",
        "id": msg_id,
        "error": {"code": code, "message": message},
    }


def _preflight_mode() -> str:
    raw = os.getenv("MISSIONCONTROL_STARTUP_PREFLIGHT", DEFAULT_STARTUP_PREFLIGHT).strip().lower()
    if raw in PREFLIGHT_MODES:
        return raw
    return DEFAULT_STARTUP_PREFLIGHT


def _healthcheck_path() -> str:
    path = os.getenv("MISSIONCONTROL_HEALTHCHECK_PATH", DEFAULT_HEALTHCHECK_PATH).strip()
    if not path:
        return DEFAULT_HEALTHCHECK_PATH
    if not path.startswith("/"):
        path = "/" + path
    return path


def _preflight(http: MissionControlHttpClient, mode: str) -> None:
    if mode == "none":
        return
    if mode == "health":
        http.http_json("GET", _healthcheck_path())
        return
    if mode == "tools":
        http.http_json("GET", "/mcp/tools")
        return


def handle_tools_list(msg_id: Any, http: MissionControlHttpClient, fail_open_on_list: bool) -> dict[str, Any]:
    try:
        tools = http.http_json("GET", "/mcp/tools")
    except MissionControlHttpError as exc:
        if fail_open_on_list:
            warning = {
                "warning": str(exc),
                "category": exc.category,
                "base_url": http.preferred_base_url,
            }
            return rpc_result(msg_id, {"tools": [], "_missioncontrol_warning": warning})
        raise
    mapped = []
    for tool in tools:
        mapped.append(
            {
                "name": tool.get("name"),
                "description": tool.get("description", ""),
                "inputSchema": tool.get("input_schema", {"type": "object", "properties": {}}),
            }
        )
    return rpc_result(msg_id, {"tools": mapped})


def handle_tools_call(msg_id: Any, params: dict[str, Any], http: MissionControlHttpClient) -> dict[str, Any]:
    tool_name = params.get("name")
    tool_args = params.get("arguments") or {}
    if not tool_name:
        return rpc_error(msg_id, -32602, "Missing tool name")

    api_result = http.http_json("POST", "/mcp/call", {"tool": tool_name, "args": tool_args})
    ok = bool(api_result.get("ok"))
    if not ok:
        err = api_result.get("error") or "Tool call failed"
        return rpc_result(
            msg_id,
            {
                "content": [{"type": "text", "text": str(err)}],
                "isError": True,
            },
        )

    return rpc_result(
        msg_id,
        {
            "content": [{"type": "text", "text": json.dumps(api_result.get("result", {}))}],
            "isError": False,
        },
    )


def handle_request(
    message: dict[str, Any],
    *,
    http: MissionControlHttpClient,
    preflight_state: dict[str, bool],
    fail_open_on_list: bool,
) -> dict[str, Any] | None:
    method = message.get("method")
    msg_id = message.get("id")
    params = message.get("params") or {}

    if method == "notifications/initialized":
        return None

    if method == "initialize":
        preflight_mode = _preflight_mode()
        if not preflight_state["done"]:
            try:
                _preflight(http, preflight_mode)
            except Exception as exc:
                log(f"preflight_failed mode={preflight_mode} base_url={http.preferred_base_url} error={exc}")
            finally:
                preflight_state["done"] = True
        return rpc_result(
            msg_id,
            {
                "protocolVersion": "2024-11-05",
                "capabilities": {
                    "tools": {
                        "listChanged": False,
                    }
                },
                "serverInfo": {
                    "name": "missioncontrol-mcp",
                    "version": "0.1.0",
                },
            },
        )

    if method == "ping":
        return rpc_result(msg_id, {})

    if method == "tools/list":
        return handle_tools_list(msg_id, http, fail_open_on_list)

    if method == "tools/call":
        return handle_tools_call(msg_id, params, http)

    if msg_id is None:
        return None

    return rpc_error(msg_id, -32601, f"Method not found: {method}")


def start_mqtt_listener() -> mqtt.Client | None:
    host = os.getenv("MQTT_HOST")
    agent_id = os.getenv("MISSIONCONTROL_AGENT_ID")
    if not host or not agent_id:
        return None

    port_raw = os.getenv("MQTT_PORT", "1883")
    try:
        port = int(port_raw)
    except ValueError:
        port = 1883

    client = mqtt.Client(client_id=os.getenv("MQTT_CLIENT_ID", f"missioncontrol-mcp-{agent_id}"))
    username = os.getenv("MQTT_USERNAME")
    password = os.getenv("MQTT_PASSWORD")
    if username:
        client.username_pw_set(username, password)

    def on_connect(client, userdata, flags, rc):
        if rc != 0:
            log(f"mqtt connect failed: rc={rc}")
            return
        topic = f"agents/{agent_id}/inbox"
        client.subscribe(topic, qos=1)
        log(f"mqtt subscribed: {topic}")

    def on_message(client, userdata, msg):
        try:
            payload = msg.payload.decode("utf-8", errors="replace")
            log(f"mqtt message on {msg.topic}: {payload}")
        except Exception as exc:
            log(f"mqtt message error: {exc}")

    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(host, port, keepalive=60)
    client.loop_start()
    return client


def run() -> None:
    http = MissionControlHttpClient()
    fail_open_on_list = parse_bool_env("MISSIONCONTROL_FAIL_OPEN_ON_LIST", DEFAULT_FAIL_OPEN_ON_LIST)
    preflight_state = {"done": False}
    log(
        f"starting bridge for {http.preferred_base_url} "
        f"(fallbacks={','.join(http.base_url_candidates)})"
    )
    mqtt_client = start_mqtt_listener()
    while True:
        try:
            message = read_message()
            if message is None:
                break
            response = handle_request(
                message,
                http=http,
                preflight_state=preflight_state,
                fail_open_on_list=fail_open_on_list,
            )
            if response is not None:
                write_message(response)
        except McpProtocolError as exc:
            log(f"protocol error: {exc}")
            break
        except Exception as exc:  # pragma: no cover
            msg_id = None
            if isinstance(locals().get("message"), dict):
                msg_id = locals()["message"].get("id")
            if msg_id is not None:
                write_message(rpc_error(msg_id, -32000, str(exc)))
            else:
                log(f"fatal error: {exc}")
                break
    if mqtt_client is not None:
        mqtt_client.loop_stop()
        mqtt_client.disconnect()


def doctor() -> int:
    http = MissionControlHttpClient()
    findings: dict[str, Any] = {
        "base_url_candidates": http.base_url_candidates,
        "preferred_base_url": http.preferred_base_url,
        "token_present": bool(os.getenv("MISSIONCONTROL_TOKEN")),
        "http_timeout_sec": parse_float_env("MISSIONCONTROL_HTTP_TIMEOUT_SEC", DEFAULT_HTTP_TIMEOUT_SEC),
        "http_retries": parse_int_env("MISSIONCONTROL_HTTP_RETRIES", DEFAULT_HTTP_RETRIES),
        "http_retry_backoff_ms": parse_int_env("MISSIONCONTROL_HTTP_RETRY_BACKOFF_MS", DEFAULT_HTTP_RETRY_BACKOFF_MS),
        "startup_preflight": _preflight_mode(),
        "checks": {},
    }

    for base_url in http.base_url_candidates:
        parsed = urlparse(base_url)
        host = parsed.hostname
        if not host:
            findings["checks"][base_url] = {"ok": False, "error": "invalid_url", "error_category": "config_error"}
            continue
        try:
            http._request_once(base_url=base_url, method="GET", path=_healthcheck_path(), payload=None)
            health_ok = True
            health_error = None
            health_category = None
        except Exception as exc:
            health_ok = False
            health_category = classify_http_error(exc)
            health_error = str(exc)

        try:
            tools = http._request_once(base_url=base_url, method="GET", path="/mcp/tools", payload=None)
            tools_ok = True
            tools_count = len(tools) if isinstance(tools, list) else 0
            tools_error = None
            tools_category = None
        except Exception as exc:
            tools_ok = False
            tools_count = 0
            tools_category = classify_http_error(exc)
            tools_error = str(exc)

        findings["checks"][base_url] = {
            "ok": health_ok and tools_ok,
            "health_ok": health_ok,
            "health_error": health_error,
            "health_error_category": health_category,
            "tools_ok": tools_ok,
            "tools_count": tools_count,
            "tools_error": tools_error,
            "tools_error_category": tools_category,
        }

    print(json.dumps(findings, indent=2, sort_keys=True))
    return 0


def main() -> None:
    parser = argparse.ArgumentParser(description="MissionControl MCP bridge")
    parser.add_argument(
        "command",
        nargs="?",
        choices=["serve", "doctor"],
        default="serve",
        help="serve MCP over stdio (default) or run doctor checks",
    )
    args = parser.parse_args()
    if args.command == "doctor":
        raise SystemExit(doctor())
    run()


if __name__ == "__main__":
    main()
