"""MCP stdio ↔ HTTP bridge for the SOAGILA daemon.

Runs as the ``python -m soagila.daemon_bridge`` subprocess that the VS
Code extension spawns **when the daemon is available**. It speaks the
real MCP protocol over stdio to Claude Code, but forwards every tool
call to the shared daemon over HTTP. The bridge itself holds no SQLite
connections, so two VS Code windows that both spawn the bridge cannot
collide — all writes serialise inside the daemon.

Fallback logic: if the bridge cannot reach the daemon at startup, it
falls back to the in-process ``mcp_server`` implementation so the user
doesn't lose functionality on a cold install.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
import sys
from pathlib import Path

import urllib.request
import urllib.error

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool

# Import tool registry for tool definitions only — we don't call them
# locally in daemon mode.
from soagila.tools import (
    annihilate,
    backup,
    config_tool,
    crossagent,
    enterprise,
    inherit,
    inspect_tool,
    prime,
    recall,
    receipt,
    remember,
    restore,
    snapshot,
    status,
)

logger = logging.getLogger("soagila.daemon_bridge")

TOOLS = {
    "soagila_remember": remember,
    "soagila_recall": recall,
    "soagila_receipt": receipt,
    "soagila_status": status,
    "soagila_annihilate": annihilate,
    "soagila_prime": prime,
    "soagila_inspect": inspect_tool,
    "soagila_config": config_tool,
    "soagila_backup": backup,
    "soagila_enterprise": enterprise,
    "soagila_crossagent": crossagent,
    "soagila_inherit": inherit,
    "soagila_snapshot": snapshot,
    "soagila_restore": restore,
}


def _soagila_home() -> Path:
    return Path.home() / ".soagila"


def _read_port() -> int | None:
    path = _soagila_home() / "daemon_port"
    if not path.exists():
        return None
    try:
        return int(path.read_text(encoding="utf-8").strip())
    except (OSError, ValueError):
        return None


def _read_token() -> str | None:
    path = _soagila_home() / "daemon_token"
    if not path.exists():
        return None
    try:
        return path.read_text(encoding="utf-8").strip() or None
    except OSError:
        return None


def _daemon_base_url() -> str | None:
    port = _read_port()
    if port is None:
        return None
    return f"http://127.0.0.1:{port}"


def _http_request(
    method: str,
    path: str,
    body: dict | None = None,
    extra_headers: dict[str, str] | None = None,
) -> dict | None:
    """Blocking HTTP call to the daemon. Returns parsed JSON or None."""
    base = _daemon_base_url()
    token = _read_token()
    if not base or not token:
        return None

    data = json.dumps(body).encode() if body is not None else None
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}",
        "X-Client-Type": "vscode",
    }
    project_cwd = os.environ.get("SOAGILA_PROJECT_CWD") or os.getcwd()
    headers["X-Workspace-Path"] = project_cwd
    try:
        import hashlib
        normalized = os.path.normcase(os.path.abspath(project_cwd))
        headers["X-Project-Hash"] = hashlib.sha256(normalized.encode()).hexdigest()[:16]
    except Exception:
        pass
    if extra_headers:
        headers.update(extra_headers)

    req = urllib.request.Request(
        f"{base}{path}",
        data=data,
        headers=headers,
        method=method,
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        try:
            return json.loads(exc.read().decode("utf-8"))
        except Exception:
            return {"success": False, "error": f"HTTP {exc.code}"}
    except urllib.error.URLError as exc:
        logger.debug("Daemon unreachable: %s", exc)
        return None


def _daemon_alive() -> bool:
    """Ping /health to check if the daemon is reachable."""
    base = _daemon_base_url()
    if not base:
        return False
    try:
        with urllib.request.urlopen(f"{base}/health", timeout=3) as resp:
            return resp.status == 200
    except Exception:
        return False


async def _forward_tool_call(
    tool_name: str,
    arguments: dict,
) -> list[TextContent]:
    """Forward a tool call to the daemon and wrap the JSON in TextContent.

    Returns TextContent carrying the JSON payload — identical format to
    what the in-process tool modules would return, so Claude Code sees
    no difference.
    """
    # Map tool_name to the matching HTTP route
    http_routes: dict[str, tuple[str, str]] = {
        "soagila_remember":    ("POST", "/tools/remember"),
        "soagila_recall":      ("POST", "/tools/recall"),
        "soagila_receipt":     ("GET",  "/tools/receipt"),
        "soagila_status":      ("GET",  "/tools/status"),
        "soagila_annihilate":  ("POST", "/tools/annihilate"),
        "soagila_prime":       ("POST", "/tools/prime"),
        "soagila_inspect":     ("POST", "/tools/inspect"),
        "soagila_config":      ("POST", "/tools/config"),
        "soagila_backup":      ("POST", "/tools/backup"),
        "soagila_enterprise":  ("POST", "/tools/enterprise"),
        "soagila_crossagent":  ("POST", "/tools/crossagent"),
        "soagila_inherit":     ("POST", "/tools/inherit"),
        "soagila_snapshot":    ("POST", "/tools/snapshot"),
        "soagila_restore":     ("POST", "/tools/restore"),
    }

    route = http_routes.get(tool_name)
    if route is None:
        return [TextContent(type="text", text=json.dumps({
            "error": f"Unknown tool: {tool_name}"
        }))]

    method, path = route
    # GET requests with query params
    if method == "GET" and tool_name == "soagila_receipt" and arguments.get("receipt_id"):
        path = f"{path}?id={arguments['receipt_id']}"
        body = None
    else:
        body = arguments if method != "GET" else None

    # Run the blocking HTTP call in a thread so we don't block the event loop
    loop = asyncio.get_running_loop()
    envelope = await loop.run_in_executor(
        None, lambda: _http_request(method, path, body)
    )

    if envelope is None:
        return [TextContent(type="text", text=json.dumps({
            "error": "Daemon unreachable",
            "hint": "SOAGILA Daemon is not running. Tools will fall back to local mode.",
        }))]

    # Daemon wraps the result in {success, data}; we unwrap data
    if envelope.get("success"):
        inner = envelope.get("data")
        if isinstance(inner, (dict, list)):
            text = json.dumps(inner, indent=2)
        else:
            text = str(inner)
    else:
        text = json.dumps({
            "error": envelope.get("error", "Unknown daemon error"),
            "code": envelope.get("code"),
        })

    return [TextContent(type="text", text=text)]


def create_bridge_server() -> Server:
    """Build the MCP Server that proxies all tool calls to the daemon."""
    server = Server("soagila-daemon-bridge")

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [mod.get_definition() for mod in TOOLS.values()]

    @server.call_tool()
    async def handle_call(name: str, arguments: dict) -> list[TextContent]:
        return await _forward_tool_call(name, arguments)

    return server


async def _run_daemon_mode() -> int:
    """MCP stdio loop that proxies everything to the daemon."""
    logger.info("Running in DAEMON mode — proxying tool calls to HTTP daemon")
    server = create_bridge_server()
    async with stdio_server() as (read, write):
        await server.run(
            read,
            write,
            initialization_options=server.create_initialization_options(),
        )
    return 0


async def _run_fallback_local() -> int:
    """If the daemon isn't available, fall back to the legacy MCP server."""
    logger.warning("Daemon not reachable — falling back to local MCP mode")
    from soagila.mcp_server import main as run_local
    await run_local()
    return 0


async def _amain() -> int:
    logging.basicConfig(
        level=os.environ.get("SOAGILA_BRIDGE_LOG", "INFO"),
        format="[SOAGILA-BRIDGE] %(asctime)s %(levelname)s %(message)s",
        stream=sys.stderr,
    )

    if _daemon_alive():
        return await _run_daemon_mode()
    return await _run_fallback_local()


def main() -> int:
    try:
        return asyncio.run(_amain())
    except KeyboardInterrupt:
        return 0


if __name__ == "__main__":
    sys.exit(main())
