"""最小限の MCP クライアント（Streamable HTTP）。

SDK を使わずに書いているのは、ツール呼び出しごとに traceparent ヘッダーを確実に付けるため。
エージェントのスパンと、サーバー側（agent-tools → Camel → 基幹）のスパンが一本のトレースになる。
"""

import json
from dataclasses import dataclass, field

import httpx
from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode

from .telemetry import trace_headers

PROTOCOL_VERSION = "2025-06-18"
tracer = trace.get_tracer("asc_agent.mcp")


class McpHttpError(Exception):
    """HTTP レベルで拒否された（401/403/429 など）。ゲートウェイや認可の判断。"""

    def __init__(self, status: int, body: str):
        super().__init__(f"HTTP {status}: {body[:200]}")
        self.status = status


@dataclass
class ToolResult:
    is_error: bool
    text: str
    data: list = field(default_factory=list)


class McpClient:
    def __init__(self, url: str, token: str, name: str):
        self.url = url
        self.name = name
        self._token = token
        self._session: str | None = None
        self._id = 0
        self._http = httpx.Client(timeout=60)

    def _headers(self) -> dict[str, str]:
        h = {
            "Authorization": f"Bearer {self._token}",
            "Content-Type": "application/json",
            "Accept": "application/json, text/event-stream",
            "MCP-Protocol-Version": PROTOCOL_VERSION,
        }
        if self._session:
            h["Mcp-Session-Id"] = self._session
        h.update(trace_headers())
        return h

    def _rpc(self, method: str, params: dict | None = None, notify: bool = False) -> dict | None:
        msg: dict = {"jsonrpc": "2.0", "method": method}
        if params is not None:
            msg["params"] = params
        if not notify:
            self._id += 1
            msg["id"] = self._id
        r = self._http.post(self.url, headers=self._headers(), content=json.dumps(msg))
        if r.status_code >= 400:
            raise McpHttpError(r.status_code, r.text)
        if "mcp-session-id" in r.headers:
            self._session = r.headers["mcp-session-id"]
        if notify:
            return None
        body = r.text
        if r.headers.get("content-type", "").startswith("text/event-stream"):
            # SSE の場合は data 行から応答を取り出す
            for line in body.splitlines():
                if line.startswith("data:"):
                    payload = json.loads(line[5:].strip())
                    if payload.get("id") == msg["id"]:
                        return payload
            raise RuntimeError("no response in SSE stream")
        return json.loads(body)

    def initialize(self) -> None:
        resp = self._rpc("initialize", {
            "protocolVersion": PROTOCOL_VERSION,
            "capabilities": {},
            "clientInfo": {"name": "agent-concierge", "version": "0.1"},
        })
        if "error" in resp:
            raise RuntimeError(resp["error"])
        self._rpc("notifications/initialized", notify=True)

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

    def call_tool(self, name: str, arguments: dict) -> ToolResult:
        with tracer.start_as_current_span(f"execute_tool {name}", kind=SpanKind.CLIENT) as span:
            span.set_attribute("gen_ai.operation.name", "execute_tool")
            span.set_attribute("gen_ai.tool.name", name)
            span.set_attribute("mcp.server", self.name)
            resp = self._rpc("tools/call", {"name": name, "arguments": arguments})
            if "error" in resp:
                span.set_status(Status(StatusCode.ERROR, str(resp["error"])))
                return ToolResult(True, json.dumps(resp["error"], ensure_ascii=False))
            result = resp["result"]
            texts = [c.get("text", "") for c in result.get("content", []) if c.get("type") == "text"]
            data = []
            for t in texts:
                try:
                    data.append(json.loads(t))
                except ValueError:
                    pass
            is_error = bool(result.get("isError"))
            if is_error:
                span.set_status(Status(StatusCode.ERROR, texts[0] if texts else "tool error"))
            return ToolResult(is_error, "\n".join(texts), data)

    def close(self) -> None:
        self._http.close()
