"""OKX 公共 WebSocket：永续 + 期权 books5 / mark-price。只读。"""

from __future__ import annotations

import asyncio
import json
import logging
from typing import Any
from urllib.parse import urlparse

import websockets
from websockets.asyncio.client import ClientConnection

from ..book_cache import BookCache
from ..types import BookLevel
from .parse import safe_float

logger = logging.getLogger(__name__)


class OkxPublicWs:
    def __init__(
        self,
        url: str,
        cache: BookCache,
        *,
        proxy: str | None = None,
        ping_interval: float = 20.0,
    ) -> None:
        self.url = url
        self.cache = cache
        self.proxy = (proxy or "").strip() or None
        self.ping_interval = ping_interval
        self._inst_ids: list[str] = []
        self._task: asyncio.Task[None] | None = None
        self._stop = asyncio.Event()
        self._subscribed: set[str] = set()

    def set_instruments(self, inst_ids: list[str]) -> None:
        self._inst_ids = [i for i in inst_ids if i]

    async def start(self) -> None:
        if self._task and not self._task.done():
            return
        self._stop.clear()
        self._task = asyncio.create_task(self._run_forever(), name="okx-public-ws")

    async def stop(self) -> None:
        self._stop.set()
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None
        self.cache.set_connected(False)

    async def resubscribe(self, inst_ids: list[str]) -> None:
        self.set_instruments(inst_ids)
        self._stop.set()
        await asyncio.sleep(0)
        self._stop.clear()
        if self._task and not self._task.done():
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
        self._task = asyncio.create_task(self._run_forever(), name="okx-public-ws")

    async def _open_connection(self) -> ClientConnection:
        if not self.proxy:
            return await websockets.connect(
                self.url,
                ping_interval=None,
                max_size=2**22,
                open_timeout=20,
            )

        from python_socks.async_.asyncio import Proxy

        parsed = urlparse(self.url)
        host = parsed.hostname or "ws.okx.com"
        port = parsed.port or (443 if parsed.scheme == "wss" else 80)
        sock = await Proxy.from_url(self.proxy).connect(dest_host=host, dest_port=port)
        return await websockets.connect(
            self.url,
            sock=sock,
            server_hostname=host,
            ping_interval=None,
            max_size=2**22,
            open_timeout=20,
        )

    async def _run_forever(self) -> None:
        backoff = 1.0
        while not self._stop.is_set():
            try:
                async with await self._open_connection() as ws:
                    self.cache.set_connected(True)
                    backoff = 1.0
                    await self._subscribe(ws)
                    waiter = asyncio.create_task(self._stop.wait())
                    reader = asyncio.create_task(self._read_loop(ws))
                    pinger = asyncio.create_task(self._ping_loop(ws))
                    done, pending = await asyncio.wait(
                        {waiter, reader, pinger},
                        return_when=asyncio.FIRST_COMPLETED,
                    )
                    for t in pending:
                        t.cancel()
                    for t in done:
                        exc = t.exception()
                        if exc and not isinstance(exc, asyncio.CancelledError):
                            raise exc
            except asyncio.CancelledError:
                raise
            except Exception as e:
                logger.warning("OKX WS disconnected: %s", e)
                self.cache.set_connected(False)
                try:
                    await asyncio.wait_for(self._stop.wait(), timeout=backoff)
                    break
                except asyncio.TimeoutError:
                    backoff = min(backoff * 2, 30.0)

        self.cache.set_connected(False)

    async def _subscribe(self, ws: ClientConnection) -> None:
        args: list[dict[str, str]] = []
        for inst in self._inst_ids:
            args.append({"channel": "books5", "instId": inst})
            args.append({"channel": "mark-price", "instId": inst})
        if not args:
            return
        payload = {"op": "subscribe", "args": args}
        await ws.send(json.dumps(payload))
        self._subscribed = {a["instId"] for a in args}
        logger.info("OKX WS subscribed: %s", sorted(self._subscribed))

    async def _ping_loop(self, ws: ClientConnection) -> None:
        while True:
            await asyncio.sleep(self.ping_interval)
            await ws.send("ping")

    async def _read_loop(self, ws: ClientConnection) -> None:
        try:
            async for raw in ws:
                if raw == "pong":
                    continue
                if isinstance(raw, bytes):
                    raw = raw.decode("utf-8", errors="ignore")
                if raw == "ping":
                    await ws.send("pong")
                    continue
                try:
                    msg = json.loads(raw)
                except json.JSONDecodeError:
                    continue
                self._handle_message(msg)
        except websockets.exceptions.ConnectionClosed:
            return

    def _handle_message(self, msg: dict[str, Any]) -> None:
        if msg.get("event") in ("subscribe", "error", "channel-conn-count"):
            if msg.get("event") == "error":
                logger.error("OKX WS error: %s", msg)
            return
        arg = msg.get("arg") or {}
        channel = str(arg.get("channel") or "")
        inst_id = str(arg.get("instId") or "")
        data = msg.get("data") or []
        if not inst_id or not data:
            return
        row = data[0] if isinstance(data[0], dict) else None
        if row is None:
            return

        if channel == "books5":
            ts = safe_float(row.get("ts"))
            self.cache.upsert_book(
                inst_id,
                bids=_levels(row.get("bids") or []),
                asks=_levels(row.get("asks") or []),
                ts_ms=int(ts) if ts is not None else None,
            )
        elif channel == "mark-price":
            ts = safe_float(row.get("ts"))
            self.cache.set_mark_px(
                inst_id,
                safe_float(row.get("markPx")),
                ts_ms=int(ts) if ts is not None else None,
            )


def _levels(raw: list[Any]) -> list[BookLevel]:
    out: list[BookLevel] = []
    for item in raw:
        if not isinstance(item, (list, tuple)) or len(item) < 2:
            continue
        px = safe_float(item[0])
        sz = safe_float(item[1])
        if px is None or sz is None or px <= 0 or sz <= 0:
            continue
        out.append(BookLevel(px=px, sz=sz))
    return out
