"""Small local adapter from the hardware Hub to CoCo's existing HTTP bridge."""

from __future__ import annotations

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path


class CocoAdapterError(RuntimeError):
    pass


@dataclass
class VoiceResult:
    agent_id: str
    transcript: str
    response: str
    audio_mp3: bytes
    audio_stats: dict | None = None
    timings_ms: dict[str, int] | None = None


ProgressCallback = Callable[[str, dict], None]


class CocoAdapter:
    def __init__(self, project_root: Path, bridge_url: str) -> None:
        self.project_root = project_root
        self.bridge_url = bridge_url.rstrip("/")

    def list_agents(self) -> list[dict]:
        data = self._json_request("GET", "/api/agents")
        agents = data.get("agents")
        return agents if isinstance(agents, list) else []

    def process_audio(
        self,
        audio_wav: bytes,
        agent_id: str,
        system_overlay: str = "",
        progress: ProgressCallback | None = None,
        synthesize_audio: bool = True,
    ) -> VoiceResult:
        started_at = time.perf_counter()
        timings: dict[str, int] = {}

        def elapsed_ms(begin: float) -> int:
            return max(0, int((time.perf_counter() - begin) * 1000))

        def emit(step: str, payload: dict | None = None) -> None:
            if progress is None:
                return
            try:
                progress(step, payload or {})
            except Exception:
                # Progress reporting must never break the voice request.
                pass

        resolve_started_at = time.perf_counter()
        agent_id = self.resolve_agent(agent_id)
        timings["resolve_ms"] = elapsed_ms(resolve_started_at)
        agent_path = f"/api/agents/{urllib.parse.quote(agent_id, safe='')}"

        emit("stt_start")
        stt_started_at = time.perf_counter()
        stt = self._request(
            "POST",
            f"{agent_path}/stt?ext=wav",
            audio_wav,
            "audio/wav",
        )
        stt_payload = self._decode_json(stt)
        timings["stt_ms"] = elapsed_ms(stt_started_at)
        transcript = str(stt_payload.get("text") or "").strip()
        audio_stats = stt_payload.get("audio") if isinstance(stt_payload.get("audio"), dict) else None
        emit(
            "transcript",
            {
                "text": transcript,
                "audio": audio_stats,
                "timings_ms": dict(timings),
            },
        )
        if not transcript:
            timings["total_ms"] = elapsed_ms(started_at)
            return VoiceResult(agent_id, "", "", b"", audio_stats, timings)

        chat_body = json.dumps(
            {
                "message": transcript,
                "save": True,
                "system_overlay": system_overlay,
            },
            ensure_ascii=False,
        ).encode("utf-8")
        chat_started_at = time.perf_counter()
        chat = self._decode_json(
            self._request("POST", f"{agent_path}/chat", chat_body, "application/json")
        )
        timings["chat_ms"] = elapsed_ms(chat_started_at)
        response = str(chat.get("response") or "").strip()
        if not response:
            raise CocoAdapterError("CoCo 未返回回复")
        emit(
            "reply",
            {
                "text": response,
                "timings_ms": dict(timings),
            },
        )

        if not synthesize_audio:
            timings["total_ms"] = elapsed_ms(started_at)
            return VoiceResult(agent_id, transcript, response, b"", audio_stats, timings)

        tts_body = json.dumps({"text": response}, ensure_ascii=False).encode("utf-8")
        tts_started_at = time.perf_counter()
        audio_mp3 = self._request(
            "POST",
            f"{agent_path}/tts",
            tts_body,
            "application/json",
        )
        timings["tts_ms"] = elapsed_ms(tts_started_at)
        timings["total_ms"] = elapsed_ms(started_at)
        emit(
            "audio_ready",
            {
                "bytes": len(audio_mp3),
                "timings_ms": dict(timings),
            },
        )
        return VoiceResult(agent_id, transcript, response, audio_mp3, audio_stats, timings)

    def open_tts_stream(self, agent_id: str, text: str):
        agent_path = f"/api/agents/{urllib.parse.quote(str(agent_id), safe='')}"
        body = json.dumps({"text": text}, ensure_ascii=False).encode("utf-8")
        request = urllib.request.Request(
            f"{self.bridge_url}{agent_path}/tts/stream",
            data=body,
            headers={
                "X-CoCo-Bridge-Token": self._bridge_token(),
                "Content-Type": "application/json",
            },
            method="POST",
        )
        try:
            return urllib.request.urlopen(request, timeout=90)
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            raise CocoAdapterError(f"CoCo Bridge 返回 HTTP {exc.code}: {detail[:240]}") from exc
        except urllib.error.URLError as exc:
            raise CocoAdapterError(
                f"无法连接 CoCo Bridge（{self.bridge_url}）：{exc.reason}"
            ) from exc

    def resolve_agent(self, requested_id: str) -> str:
        requested_id = str(requested_id or "").strip()
        agents = self.list_agents()
        ids = {
            str(agent.get("id") or "").strip()
            for agent in agents
            if isinstance(agent, dict)
        }
        if requested_id in ids:
            return requested_id
        if len(ids) == 1:
            return next(iter(ids))
        raise CocoAdapterError(
            "硬件设置中的智能体 ID 无效。请在 CoCo 设置中选择一个可用智能体。"
        )

    def _json_request(self, method: str, path: str) -> dict:
        return self._decode_json(self._request(method, path, None, None))

    def _request(
        self,
        method: str,
        path: str,
        body: bytes | None,
        content_type: str | None,
    ) -> bytes:
        token = self._bridge_token()
        url = f"{self.bridge_url}{path}"
        headers = {"X-CoCo-Bridge-Token": token}
        if content_type:
            headers["Content-Type"] = content_type
        request = urllib.request.Request(url, data=body, headers=headers, method=method)
        try:
            with urllib.request.urlopen(request, timeout=90) as response:
                return response.read()
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            raise CocoAdapterError(f"CoCo Bridge 返回 HTTP {exc.code}: {detail[:240]}") from exc
        except urllib.error.URLError as exc:
            raise CocoAdapterError(
                f"无法连接 CoCo Bridge（{self.bridge_url}）：{exc.reason}"
            ) from exc

    def _bridge_token(self) -> str:
        token = os.environ.get("coco_bridge_token", "").strip()
        if token:
            return token

        token_paths = []
        coco_home = os.environ.get("coco_home", "").strip()
        if coco_home:
            token_paths.append(Path(coco_home).expanduser() / "bridge-token")
        token_paths.extend(
            [
                self.project_root / ".coco" / "bridge-token",
                self.project_root / "main" / ".coco" / "bridge-token",
            ]
        )

        for token_path in token_paths:
            try:
                token = token_path.read_text(encoding="utf-8").strip()
            except OSError:
                continue
            if token:
                return token

        raise CocoAdapterError("未找到 CoCo Bridge token。请先启动一次 main\\cli.py serve。")

    @staticmethod
    def _decode_json(payload: bytes) -> dict:
        try:
            data = json.loads(payload.decode("utf-8"))
        except (UnicodeDecodeError, json.JSONDecodeError) as exc:
            raise CocoAdapterError("CoCo Bridge 返回了无效 JSON") from exc
        if not isinstance(data, dict):
            raise CocoAdapterError("CoCo Bridge 返回了无效响应")
        if data.get("ok") is False:
            raise CocoAdapterError(str(data.get("error") or "CoCo Bridge 请求失败"))
        return data
