"""The MCP JSON-RPC loop, transport-agnostic — stdio and TCP serve the same handler.

One handler, two wires. The protocol is newline-delimited JSON-RPC 2.0 either way; what
differs is only where the lines come from and go to, so that difference is all the
transports are allowed to contain. Anything the surface refuses, it refuses identically
on every wire — a second behaviour per transport is how two disclosure surfaces happen
by accident.
"""

from __future__ import annotations

import json
from typing import Callable, Iterable

from .server import EdgeMCPServer

PROTOCOL = "2025-06-18"


def make_rpc_handler(edge, counterparty_hint: list[dict] | None = None) -> Callable[[dict], dict]:
    """One edge's JSON-RPC method handler. Raises KeyError for unsupported methods —
    the loop turns that into a JSON-RPC error, never into a dead connection."""
    mcp = EdgeMCPServer(edge)
    tools = mcp.list_tools()
    if counterparty_hint is not None:
        # a convenience the demo needs and a real deployment must not have: the counterparty's
        # records are the OTHER party's to hand over, never something an edge holds for them.
        tools = tools + [{
            "name": "demo_counterparty_records",
            "description": "DEMO ONLY — the counterparty's records, so edge_review can be "
                           "called without a second edge. In real use the other party hands "
                           "these over; an edge never holds them.",
            "inputSchema": {"type": "object", "properties": {}},
        }]

    def handle(msg: dict) -> dict:
        method = msg.get("method")
        if method == "initialize":
            asked = (msg.get("params") or {}).get("protocolVersion")
            return {"protocolVersion": asked or PROTOCOL,
                    "capabilities": {"tools": {}},
                    "serverInfo": {"name": "neoedge", "version": "0.2.0"}}
        if method == "ping":
            return {}
        if method == "tools/list":
            return {"tools": tools}
        if method == "tools/call":
            params = msg.get("params") or {}
            name = params.get("name")
            args = params.get("arguments") or {}
            if name == "demo_counterparty_records" and counterparty_hint is not None:
                out = {"isError": False, "content": counterparty_hint}
            else:
                out = mcp.call(name, args)
            return {"content": [{"type": "text",
                                 "text": json.dumps(out["content"], indent=2, default=str)}],
                    "isError": out["isError"]}
        raise KeyError(f"method not supported: {method}")

    return handle


def serve_lines(lines: Iterable[str], write_line: Callable[[str], None],
                handle: Callable[[dict], dict]) -> int:
    """Run the JSON-RPC loop over any pair of line streams. Notifications are
    acknowledged by silence (per spec); malformed lines are skipped; a handler error
    becomes a JSON-RPC error object, so one bad call never costs the session."""
    for line in lines:
        line = line.strip()
        if not line:
            continue
        try:
            msg = json.loads(line)
        except json.JSONDecodeError:
            continue
        if "id" not in msg:
            continue
        try:
            result = {"jsonrpc": "2.0", "id": msg["id"], "result": handle(msg)}
        except Exception as error:  # noqa: BLE001
            result = {"jsonrpc": "2.0", "id": msg["id"],
                      "error": {"code": -32601, "message": f"{type(error).__name__}: {error}"}}
        write_line(json.dumps(result))
    return 0
