"""Linear Canal — Lightweight HTTP MCP server (daemon mode).

Wraps StdioMCPServer's tool logic in a minimal asyncio HTTP server.
Zero extra dependencies — uses only Python stdlib + lancedb + fastembed.

Transport: JSON-RPC 2.0 over HTTP POST (MCP Streamable-HTTP subset)

Endpoints:
    POST /mcp          — JSON-RPC tool call or batch
    GET  /mcp/health   — returns {"status":"ok","version":"1.0"}
    GET  /             — redirect to /mcp/health
"""
from __future__ import annotations

import asyncio
import json
import logging
import os
import sys
from pathlib import Path
from typing import Any, Optional

logger = logging.getLogger(__name__)

_CORS_HEADERS = [
    ("Access-Control-Allow-Origin", "*"),
    ("Access-Control-Allow-Methods", "POST, GET, OPTIONS"),
    ("Access-Control-Allow-Headers", "Content-Type, Authorization"),
]

# ── Minimal async HTTP server (no uvicorn/fastapi needed) ────────────────────

class _HTTPRequest:
    def __init__(self, method: str, path: str, headers: dict[str, str], body: bytes):
        self.method  = method
        self.path    = path
        self.headers = headers
        self.body    = body


class _HTTPResponse:
    def __init__(self, status: int, body: str, content_type: str = "application/json"):
        self.status       = status
        self.body_bytes   = body.encode("utf-8")
        self.content_type = content_type


class MCPHttpServer:
    """Async HTTP server wrapping StdioMCPServer tool logic."""

    def __init__(
        self,
        port: int = 3141,
        host: str = "127.0.0.1",
        lancedb_path: Optional[str] = None,
        ctx_dir: Optional[str] = None,
        project_id: str = "default",
    ) -> None:
        self.port       = port
        self.host       = host

        _default_lancedb = str(Path.home() / ".linear-canal" / "data" / "lancedb_global_store")
        self._lancedb_path = lancedb_path or os.environ.get("LANCEDB_PATH", _default_lancedb)
        self._project_id   = project_id
        self._ctx_dir      = ctx_dir or os.environ.get(
            "CTX_DIR", f".linear_canal_data/{project_id}/ctx"
        )

        self._mcp: Any = None   # lazy-loaded StdioMCPServer

    def _get_mcp(self) -> Any:
        """Lazy-load the MCP server (loads embedding model on first search)."""
        if self._mcp is None:
            try:
                from api.mcp.stdio import StdioMCPServer  # type: ignore[import-not-found]
                self._mcp = StdioMCPServer(
                    project_id=self._project_id,
                    lancedb_path=self._lancedb_path,
                    ctx_dir=self._ctx_dir,
                )
            except ImportError as exc:
                raise RuntimeError(f"MCP server not available: {exc}") from exc
        return self._mcp

    async def _dispatch_jsonrpc(self, payload: dict) -> dict:
        """Route a single JSON-RPC request to the correct MCP tool handler."""
        rpc_id  = payload.get("id")
        method  = payload.get("method", "")
        params  = payload.get("params", {})

        def _ok(result: Any) -> dict:
            return {"jsonrpc": "2.0", "id": rpc_id, "result": result}

        def _err(code: int, message: str) -> dict:
            return {"jsonrpc": "2.0", "id": rpc_id, "error": {"code": code, "message": message}}

        try:
            mcp = self._get_mcp()
        except RuntimeError as exc:
            return _err(-32603, str(exc))

        try:
            if method == "initialize":
                return _ok({
                    "protocolVersion": "2024-11-05",
                    "capabilities": {"tools": {}},
                    "serverInfo": {"name": "linear-canal", "version": "1.0"},
                })

            if method == "tools/list":
                tools = mcp._list_tools() if hasattr(mcp, "_list_tools") else []
                return _ok({"tools": tools})

            if method == "tools/call":
                tool_name = params.get("name", "")
                arguments = params.get("arguments", {})
                if tool_name == "lc_bootstrap":
                    result = await mcp._lc_bootstrap()
                elif tool_name == "lc_search":
                    result = await mcp._lc_search(arguments)
                elif tool_name == "lc_impact":
                    result = await mcp._lc_impact(arguments)
                elif tool_name == "lc_context":
                    result = await mcp._lc_context(arguments)
                else:
                    return _err(-32601, f"Unknown tool: {tool_name}")

                # Wrap in MCP content format
                content_text = result.get("content", "") if isinstance(result, dict) else str(result)
                return _ok({
                    "content": [{"type": "text", "text": content_text}],
                    "isError": result.get("isError", False) if isinstance(result, dict) else False,
                })

            if method == "ping":
                return _ok({})

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

        except Exception as exc:
            logger.exception("Tool error for method %s", method)
            return _err(-32603, f"Internal error: {exc}")

    async def _handle(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
        """Handle one HTTP connection."""
        try:
            req = await self._parse_request(reader)
            if req is None:
                writer.close()
                return

            resp = await self._route(req)
            await self._send_response(writer, resp)
        except (ConnectionResetError, BrokenPipeError):
            pass
        except Exception:
            logger.exception("Error handling request")
        finally:
            try:
                writer.close()
                await writer.wait_closed()
            except Exception:
                pass

    async def _parse_request(self, reader: asyncio.StreamReader) -> Optional[_HTTPRequest]:
        """Parse a minimal HTTP/1.1 request."""
        try:
            head = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=10)
        except (asyncio.TimeoutError, asyncio.IncompleteReadError):
            return None

        lines  = head.split(b"\r\n")
        if not lines:
            return None

        parts = lines[0].decode("utf-8", errors="replace").split(" ")
        if len(parts) < 2:
            return None
        method = parts[0].upper()
        path   = parts[1]

        headers: dict[str, str] = {}
        for line in lines[1:]:
            if b":" in line:
                k, _, v = line.partition(b":")
                headers[k.decode().strip().lower()] = v.decode().strip()

        body = b""
        content_length = int(headers.get("content-length", 0) or 0)
        if content_length > 0:
            try:
                body = await asyncio.wait_for(reader.read(content_length), timeout=30)
            except asyncio.TimeoutError:
                return None

        return _HTTPRequest(method, path, headers, body)

    async def _route(self, req: _HTTPRequest) -> _HTTPResponse:
        """Route request to handler."""
        # OPTIONS (CORS preflight)
        if req.method == "OPTIONS":
            return _HTTPResponse(204, "")

        path = req.path.split("?")[0].rstrip("/")

        if path in ("", "/mcp/health"):
            body = json.dumps({"status": "ok", "version": "1.0", "port": self.port})
            return _HTTPResponse(200, body)

        if path == "/mcp":
            if req.method != "POST":
                return _HTTPResponse(405, json.dumps({"error": "Method Not Allowed"}))
            try:
                payload = json.loads(req.body.decode("utf-8")) if req.body else {}
            except json.JSONDecodeError as exc:
                return _HTTPResponse(400, json.dumps({"jsonrpc":"2.0","id":None,"error":{"code":-32700,"message":f"Parse error: {exc}"}}))

            if isinstance(payload, list):
                results = [await self._dispatch_jsonrpc(p) for p in payload]
                return _HTTPResponse(200, json.dumps(results))

            result = await self._dispatch_jsonrpc(payload)
            return _HTTPResponse(200, json.dumps(result))

        return _HTTPResponse(404, json.dumps({"error": "Not found"}))

    @staticmethod
    async def _send_response(writer: asyncio.StreamWriter, resp: _HTTPResponse) -> None:
        status_text = {200: "OK", 204: "No Content", 400: "Bad Request",
                       404: "Not Found", 405: "Method Not Allowed"}.get(resp.status, "OK")
        header_lines = [
            f"HTTP/1.1 {resp.status} {status_text}",
            f"Content-Type: {resp.content_type}; charset=utf-8",
            f"Content-Length: {len(resp.body_bytes)}",
        ]
        for k, v in _CORS_HEADERS:
            header_lines.append(f"{k}: {v}")
        header_lines += ["Connection: close", "", ""]
        writer.write("\r\n".join(header_lines).encode("utf-8"))
        if resp.body_bytes:
            writer.write(resp.body_bytes)
        await writer.drain()

    async def run(self) -> None:
        """Start the server and run forever."""
        server = await asyncio.start_server(self._handle, self.host, self.port)
        logger.info("Linear Canal MCP daemon listening on http://%s:%d/mcp", self.host, self.port)
        async with server:
            await server.serve_forever()


def run_server(port: int, lancedb_path: Optional[str], ctx_dir: Optional[str], project_id: str) -> None:
    """Entrypoint called by the daemon subprocess."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )
    server = MCPHttpServer(
        port=port,
        lancedb_path=lancedb_path,
        ctx_dir=ctx_dir,
        project_id=project_id,
    )
    try:
        asyncio.run(server.run())
    except KeyboardInterrupt:
        pass
