"""币安交易所适配器：USDT 永续 + 欧洲期权公共行情（SIM 只读）。"""

from __future__ import annotations

import logging
import time
from typing import Any, Sequence

from ...config import Settings, get_settings
from ..book_cache import BookCache
from ..types import BookLevel, MarketSnapshot, OptionPair, Quote
from .parse import safe_float
from .rest import BinanceRestClient
from .ws import BinancePublicWs

logger = logging.getLogger(__name__)

# snapshot 轮询勿每 1.5s 打 REST；缺盘口时最少间隔再补
_REST_FILL_MIN_INTERVAL_SEC = 5.0


class BinanceExchange:
    name = "binance"

    def __init__(self, settings: Settings | None = None) -> None:
        self.settings = settings or get_settings()
        self.cache = BookCache()
        proxy = (
            self.settings.binance_http_proxy
            or self.settings.okx_http_proxy
            or None
        )
        self.rest = BinanceRestClient(
            fapi_base=self.settings.binance_fapi_base,
            eapi_base=self.settings.binance_eapi_base,
            proxy=proxy,
        )
        self.ws = BinancePublicWs(
            futures_ws_base=self.settings.binance_futures_ws,
            options_ws_base=self.settings.binance_options_ws,
            cache=self.cache,
            proxy=proxy,
        )
        self._started = False
        self._ct_cache: dict[str, float] = {}
        self._rest_fill_at: dict[str, float] = {}

    async def start(self) -> None:
        if self._started:
            return
        self._started = True
        await self.ws.start()
        logger.info("Binance exchange started")

    async def stop(self) -> None:
        self._started = False
        await self.ws.stop()
        self.rest.close()
        logger.info("Binance exchange stopped")

    def list_option_contracts(self, family: str) -> list[dict[str, Any]]:
        contracts = self.rest.list_option_contracts(family)
        for c in contracts:
            if c.get("ct_mult"):
                self._ct_cache[str(c["inst_id"])] = float(c["ct_mult"])
        return contracts

    def fetch_index(self, index_id: str) -> float | None:
        return self.rest.fetch_index(index_id)

    def fetch_mark(self, inst_id: str) -> float | None:
        return self.rest.fetch_mark(inst_id)

    def fetch_book(
        self, inst_id: str, depth: int = 5
    ) -> tuple[list[BookLevel], list[BookLevel], int | None]:
        return self.rest.fetch_books(inst_id, sz=depth)

    def get_ct_mult(self, option_inst_id: str, family: str, default: float) -> float:
        if option_inst_id in self._ct_cache:
            return self._ct_cache[option_inst_id]
        # 币安 ETH 期权 unit 常见为 1
        return float(default if default > 0 else 1.0)

    def set_pair(self, pair: OptionPair | None) -> None:
        self.cache.set_pair(pair)

    def warm_and_subscribe(self, inst_ids: Sequence[str]) -> None:
        ids = [i for i in inst_ids if i]
        for inst in ids:
            try:
                bids, asks, ts = self.rest.fetch_books(inst, sz=10)
                if bids or asks:
                    self.cache.upsert_book(inst, bids=bids, asks=asks, ts_ms=ts)
                else:
                    logger.warning("binance warm book empty: %s", inst)
            except Exception as e:
                logger.warning("binance warm book %s failed: %s", inst, e)
            # 期权 mark 易触发 eapi 限流；有盘口即可，永续再拉 mark
            from .parse import is_option_symbol

            if not is_option_symbol(inst):
                try:
                    mp = self.rest.fetch_mark(inst)
                    if mp:
                        self.cache.set_mark_px(inst, mp)
                except Exception:
                    pass
        # 指数（失败则跳过，勿连环打 eapi）
        try:
            idx = self.rest.fetch_index(self.settings.index_inst_id)
            if idx:
                self.cache.set_index_px(idx)
        except Exception as e:
            logger.debug("binance index failed: %s", e)
        keep = set(ids)
        self.cache.drop_except(keep)
        self.ws.set_instruments(ids)

    async def resubscribe(self, inst_ids: Sequence[str]) -> None:
        await self.ws.resubscribe([i for i in inst_ids if i])

    def quote(self, inst_id: str) -> Quote | None:
        return self.cache.get(inst_id)

    def _refresh_quote_if_stale(self, inst_id: str) -> None:
        """仅在买卖一都缺失时补 REST，且有最短间隔，避免 429。"""
        if not inst_id:
            return
        q = self.cache.get(inst_id)
        if q is not None and q.bid is not None and q.ask is not None:
            return
        now = time.monotonic()
        last = self._rest_fill_at.get(inst_id, 0.0)
        if now - last < _REST_FILL_MIN_INTERVAL_SEC:
            return
        self._rest_fill_at[inst_id] = now
        try:
            bids, asks, ts = self.rest.fetch_books(inst_id, sz=10)
            if bids or asks:
                self.cache.upsert_book(inst_id, bids=bids, asks=asks, ts_ms=ts)
        except Exception as e:
            logger.debug("binance refresh quote %s: %s", inst_id, e)

    def snapshot(self, perp_inst_id: str) -> MarketSnapshot:
        # 优先吃 WS 缓存；仅缺盘口时低频 REST 补齐
        self._refresh_quote_if_stale(perp_inst_id)
        pair = None
        try:
            with self.cache._lock:  # noqa: SLF001
                pair = self.cache._pair
        except Exception:
            pair = None
        if pair is not None:
            self._refresh_quote_if_stale(pair.call_inst_id)
            self._refresh_quote_if_stale(pair.put_inst_id)
        return self.cache.snapshot(perp_inst_id)

    def snapshot_dict(self, perp_inst_id: str) -> dict[str, Any]:
        return self.snapshot(perp_inst_id).to_dict()

    def set_index_px(self, px: float | None) -> None:
        self.cache.set_index_px(px)

    def set_mark_px(self, inst_id: str, mark_px: float | None) -> None:
        self.cache.set_mark_px(inst_id, mark_px)
