"""MCP over TCP — the wire between the core container and the agent containers.

Same newline JSON-RPC as stdio, over a socket: what compose calls the internal network
is, on bare metal, just a host and a port — so the identical wire is provable with two
processes and no Docker at all, which is how the tests prove it.

**Never publish this port.** In the compose topology it listens on an INTERNAL network
only; the agent containers are its whole audience. The UI is the human's port; this one
is the agents'. Publishing it would hand the read surface to the LAN — the withheld
discipline still holds even then (the wire never carried figures), but the audience
decision belongs to the owner, not to a default.
"""

from __future__ import annotations

import json
import socket
import socketserver
import threading

from .rpc import make_rpc_handler, serve_lines


class EdgeTCPServer:
    """Serve one edge's MCP surface on host:port. One thread per connection; every
    connection gets the same handler, so every client sees the same refusals."""

    def __init__(self, edge, host: str = "127.0.0.1", port: int = 0,
                 counterparty_hint: list[dict] | None = None):
        handle = make_rpc_handler(edge, counterparty_hint)

        class Handler(socketserver.StreamRequestHandler):
            def handle(self) -> None:
                reader = (raw.decode("utf-8", errors="replace") for raw in self.rfile)

                def write_line(text: str) -> None:
                    self.wfile.write((text + "\n").encode("utf-8"))
                    self.wfile.flush()

                serve_lines(reader, write_line, handle)

        class Server(socketserver.ThreadingTCPServer):
            allow_reuse_address = True
            daemon_threads = True

        self._server = Server((host, port), Handler)
        self.host, self.port = self._server.server_address[:2]
        self._thread: threading.Thread | None = None

    def start(self) -> "EdgeTCPServer":
        self._thread = threading.Thread(target=self._server.serve_forever,
                                        name="neoedge-mcp-tcp", daemon=True)
        self._thread.start()
        return self

    def serve_forever(self) -> None:
        self._server.serve_forever()

    def shutdown(self) -> None:
        self._server.shutdown()
        self._server.server_close()


class MCPClient:
    """A minimal MCP client for the agent side: initialize once, then call tools.

    `call()` returns `(content, is_error)` with the content already parsed back out of
    the MCP text envelope — the runner and `RemoteEdge` want values, not envelopes.
    """

    def __init__(self, host: str, port: int, timeout_s: float = 10.0):
        self._sock = socket.create_connection((host, port), timeout=timeout_s)
        self._reader = self._sock.makefile("r", encoding="utf-8")
        self._writer = self._sock.makefile("w", encoding="utf-8")
        self._next_id = 0
        self.server_info = self._rpc("initialize",
                                     {"protocolVersion": "2025-06-18"}).get("serverInfo", {})

    def _rpc(self, method: str, params: dict | None = None) -> dict:
        self._next_id += 1
        request = {"jsonrpc": "2.0", "id": self._next_id, "method": method}
        if params is not None:
            request["params"] = params
        self._writer.write(json.dumps(request) + "\n")
        self._writer.flush()
        line = self._reader.readline()
        if not line:
            raise ConnectionError("the core closed the MCP connection")
        response = json.loads(line)
        if "error" in response:
            raise RuntimeError(response["error"].get("message", "rpc error"))
        return response["result"]

    def list_tools(self) -> list[dict]:
        return self._rpc("tools/list")["tools"]

    def call(self, name: str, arguments: dict | None = None) -> tuple[object, bool]:
        result = self._rpc("tools/call", {"name": name, "arguments": arguments or {}})
        content = json.loads(result["content"][0]["text"])
        return content, bool(result.get("isError"))

    def close(self) -> None:
        try:
            self._sock.close()
        except OSError:
            pass

    def __enter__(self) -> "MCPClient":
        return self

    def __exit__(self, *exc) -> None:
        self.close()
