"""LinkskyGateway WebSocket 客户端（同步阻塞 + 自动重连）。

基于 :mod:`websocket-client`：
* ``connect()`` 启动一个 daemon 接收线程，调用 ``ws.run_forever``；
* 心跳由 websocket-client 内置（ping_interval=10s, ping_timeout=5s）；
* 断开后自动 ``schedule_reconnect`` —— 在内部 ``Timer`` 上延迟 3 秒重连；
* ``send_text`` **同步** 发送，并把所有错误日志化。
"""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
import secrets
import ssl
import threading
import time
from typing import TYPE_CHECKING, Any, Dict, Optional
from urllib.parse import urlparse

import websocket

from ..mgr.agent_thread_mgr import AgentThreadMgr
from ..mgr.callback_type_mgr import AgentSdkCallbackTypeMgr
from .message_router import MessageRouter

if TYPE_CHECKING:
    from ..agent_sdk import AgentSdk


_log = logging.getLogger("linksoul_agentsdk.client")

_DEFAULT_PING_INTERVAL = 10
_DEFAULT_PING_TIMEOUT = 5
_RECONNECT_DELAY = 3.0


class LinkskyClient:
    """与 LinkskyGateway 的同步 WebSocket 客户端。"""

    def __init__(
        self,
        agent_sdk: "AgentSdk",
        *,
        max_reconnect_attempts: int = -1,
        reconnect_delay: float = _RECONNECT_DELAY,
    ) -> None:
        self._agent_sdk = agent_sdk
        self._max_reconnect_attempts = max_reconnect_attempts
        self._reconnect_delay = reconnect_delay
        self._current_attempt = 0

        self._ws: Optional[websocket.WebSocketApp] = None
        self._thread: Optional[threading.Thread] = None
        self._send_lock = threading.Lock()
        self._reconnect_timer: Optional[threading.Timer] = None
        self._stopped = threading.Event()

        self._router = MessageRouter(agent_sdk, self)

    # ----- 连接生命周期 --------------------------------------------------------

    def connect(self) -> None:
        if self._stopped.is_set():
            return
        url = self._agent_sdk.url
        headers = self._build_headers(url)

        self._ws = websocket.WebSocketApp(
            url,
            header=headers,
            on_open=self._on_open,
            on_message=self._on_message,
            on_error=self._on_error,
            on_close=self._on_close,
        )

        sslopt = self._build_sslopt(url)
        self._thread = threading.Thread(
            target=self._run_forever,
            kwargs={"sslopt": sslopt},
            name=f"linksky-recv-{self._agent_sdk.app_id}",
            daemon=True,
        )
        self._thread.start()

    def _run_forever(self, sslopt: Optional[Dict[str, Any]] = None) -> None:
        try:
            assert self._ws is not None
            self._ws.run_forever(
                ping_interval=_DEFAULT_PING_INTERVAL,
                ping_timeout=_DEFAULT_PING_TIMEOUT,
                sslopt=sslopt,
            )
        except Exception:  # noqa: BLE001
            _log.exception("ws run_forever crashed")
        finally:
            if not self._stopped.is_set():
                self._schedule_reconnect()

    def stop(self) -> None:
        self._stopped.set()
        timer = self._reconnect_timer
        if timer is not None:
            timer.cancel()
        try:
            if self._ws is not None:
                self._ws.close()
        except Exception:  # noqa: BLE001
            _log.exception("ws close failed")

    def is_closed(self) -> bool:
        return self._stopped.is_set() or self._ws is None

    # ----- 收发 ----------------------------------------------------------------

    def send_text(
        self,
        agent_id: str,
        event_id: str,
        text: str,
        log_text: str,
    ) -> None:
        ws = self._ws
        if ws is None or ws.sock is None or not ws.sock.connected:
            _log.error(
                "agent_sdk to gw %s %s dropped : %s", agent_id, event_id, log_text,
            )
            return
        with self._send_lock:
            try:
                ws.send(text)
                _log.info(
                    "agent_sdk to gw %s %s success: %s", agent_id, event_id, log_text,
                )
            except Exception as exc:  # noqa: BLE001
                _log.error(
                    "agent_sdk to gw %s %s failed: %s exc=%s",
                    agent_id, event_id, log_text, exc,
                )

    # ----- 事件回调 ------------------------------------------------------------

    def _on_open(self, ws: websocket.WebSocketApp) -> None:
        self._current_attempt = 0
        _log.info("agent_sdk connect to %s success", self._agent_sdk.url)
        try:
            self._agent_sdk.on_auth_state(0, "auth success")
        except Exception:  # noqa: BLE001
            _log.exception("on_auth_state cb threw")

    def _on_message(self, ws: websocket.WebSocketApp, text: str) -> None:
        # 把消息派发到 per-agent worker thread，避免阻塞接收线程
        try:
            msg = json.loads(text)
        except json.JSONDecodeError:
            _log.warning("from gw invalid json: %s", text[:200])
            return
        agent_id = msg.get("agentId")
        if not agent_id:
            # 没有 agent_id 的消息（例如鉴权失败响应）就在接收线程同步处理
            self._router.dispatch(msg)
            return
        worker = AgentThreadMgr.get_thread_name(agent_id) or AgentThreadMgr.borrow_idle_thread()
        if worker is None:
            _log.error("no worker thread available for agent_id=%s", agent_id)
            return
        AgentThreadMgr.bind(agent_id, worker)
        AgentThreadMgr.submit(worker, lambda m=msg: self._router.dispatch(m))

    def _on_error(self, ws: websocket.WebSocketApp, error: BaseException) -> None:
        _log.error("agent_sdk with %s error: %s", self._agent_sdk.url, error)

    def _on_close(self, ws: websocket.WebSocketApp, code, reason) -> None:
        _log.warning(
            "agent_sdk disconnect from %s code=%s reason=%s",
            self._agent_sdk.url, code, reason,
        )

    # ----- 重连 ---------------------------------------------------------------

    def _schedule_reconnect(self) -> None:
        if self._stopped.is_set():
            return
        if (
            self._max_reconnect_attempts != -1
            and self._current_attempt >= self._max_reconnect_attempts
        ):
            _log.warning(
                "agent_sdk %s reached max reconnect attempts (%d), stop",
                self._agent_sdk.url, self._max_reconnect_attempts,
            )
            self.stop()
            return
        self._current_attempt += 1
        _log.info(
            "agent_sdk %s reconnect #%d in %.1fs",
            self._agent_sdk.url, self._current_attempt, self._reconnect_delay,
        )
        timer = threading.Timer(self._reconnect_delay, self.connect)
        timer.daemon = True
        self._reconnect_timer = timer
        timer.start()

    # ----- 内部工具 -----------------------------------------------------------

    def _build_headers(self, url: str) -> list[str]:
        """构造 WebSocket 握手时的 HTTP headers，与 Java SDK 对齐。

        - HMAC-SHA256 签名头：X-App-Id / X-App-Key / X-Timestamp / X-Nonce / X-Signature
        - 已注册的回调类型集合：X-Callback-Types（JSON 数组字符串）

        签名串：``"GET\\n<path>\\n<timestamp>\\n<nonce>"``，密钥为 ``app_secret``，
        小写 hex 输出。
        """
        headers: list[str] = []
        app_id = self._agent_sdk.app_id
        app_key = self._agent_sdk.app_key
        app_secret = self._agent_sdk.app_secret
        if app_id and app_key and app_secret:
            parsed = urlparse(url)
            path = parsed.path or "/"
            timestamp = str(int(time.time() * 1000))
            nonce = secrets.token_hex(16)
            payload = f"GET\n{path}\n{timestamp}\n{nonce}"
            signature = hmac.new(
                app_secret.encode("utf-8"),
                payload.encode("utf-8"),
                hashlib.sha256,
            ).hexdigest()
            headers.extend([
                f"X-App-Id: {app_id}",
                f"X-App-Key: {app_key}",
                f"X-Timestamp: {timestamp}",
                f"X-Nonce: {nonce}",
                f"X-Signature: {signature}",
            ])
        callback_types = AgentSdkCallbackTypeMgr.get(self._agent_sdk.app_id)
        callback_type_values = sorted(ct.value for ct in (callback_types or set()))
        headers.append(
            f"X-Callback-Types: {json.dumps(callback_type_values, ensure_ascii=False)}"
        )
        return headers

    @staticmethod
    def _build_sslopt(url: str) -> Optional[Dict[str, Any]]:
        parsed = urlparse(url)
        if parsed.scheme.lower() != "wss":
            return None
        # 与 Java 版的 InsecureTrustManagerFactory 行为一致：不校验服务端证书。
        # 生产环境如需校验，请自行替换为受信任的 CA bundle。
        return {"cert_reqs": ssl.CERT_NONE}
