"""urllib BinanceFundingClient — Binance USDⓈ-M futures public API (no key, no capital). Faithful to
foxhunt scripts/surfer/crypto_funding_paper.py's scan() (lines 48-92): the hedgeable, liquid,
crypto-native universe + trailing-30d-mean funding (tf30) + today's booking rate (last24).

  * crypto_native()  -> symbols with underlyingType == "COIN" from /fapi/v1/exchangeInfo
  * spot_symbols()   -> symbols with a Binance SPOT market (api.binance.com/api/v3/ticker/price)
  * universe()       -> {sym: quoteVolume} for crypto-native, spot-hedgeable USDT perps with vol > LIQ_USD
  * scan(prev)       -> (liq, tf30, last24) over union of the universe and prev positions

Retries with backoff; mirrors the original `get()` helper and the yahoo_daily `_get` style."""
from __future__ import annotations

import json
import time
import urllib.request
from typing import Any

from fxhnt.domain.strategies.funding_carry import LIQ_USD

_FAPI = "https://fapi.binance.com"
INTERVALS_PER_DAY = 3  # Binance funding settles every 8h (matches the original constant)


class BinanceFundingClient:
    def __init__(self, liq_usd: float = LIQ_USD) -> None:
        self._liq_usd = liq_usd

    def _get(self, url: str, tries: int = 4) -> Any:
        for a in range(tries):
            try:
                req = urllib.request.Request(url, headers={"User-Agent": "curl/8"})
                return json.loads(urllib.request.urlopen(req, timeout=30).read())
            except Exception:  # noqa: BLE001 — transient HTTP; retry then re-raise
                if a == tries - 1:
                    raise
                time.sleep(2 * (a + 1))
        raise RuntimeError("unreachable")

    def crypto_native(self) -> set[str]:
        """Symbols whose underlyingType is COIN (original crypto_native(), line 48-50)."""
        info = self._get(f"{_FAPI}/fapi/v1/exchangeInfo")
        return {s["symbol"] for s in info["symbols"] if s.get("underlyingType") == "COIN"}

    def spot_symbols(self) -> set[str]:
        """Symbols with a Binance SPOT market — required to build the delta-neutral hedge
        (original spot_symbols(), line 53-57). Perp-only coins can't be cash-and-carry harvested."""
        return {x["symbol"] for x in self._get("https://api.binance.com/api/v3/ticker/price")}

    def universe(self) -> dict[str, float]:
        """Liquid, crypto-native, HEDGEABLE (spot+perp) USDT perps (original universe(), line 60-68)."""
        native = self.crypto_native()
        spot = self.spot_symbols()
        t = self._get(f"{_FAPI}/fapi/v1/ticker/24hr")
        return {
            x["symbol"]: float(x["quoteVolume"])
            for x in t
            if x["symbol"].endswith("USDT")
            and x["symbol"] in native
            and x["symbol"] in spot
            and float(x["quoteVolume"]) > self._liq_usd
        }

    def _funding_hist(self, sym: str, limit: int = 90) -> list[float]:
        """Funding-rate history (original funding_hist(), line 71-73)."""
        h = self._get(f"{_FAPI}/fapi/v1/fundingRate?symbol={sym}&limit={limit}")
        return [float(x["fundingRate"]) for x in h]

    def scan(self, prev: set[str]) -> tuple[dict[str, float], dict[str, float], dict[str, float]]:
        """(liq, tf30, last24) for the union of the universe and prev positions (original scan(),
        line 76-92)."""
        liq = self.universe()
        need = set(liq) | set(prev)
        tf30: dict[str, float] = {}
        last24: dict[str, float] = {}
        for i, sym in enumerate(sorted(need)):
            try:
                h = self._funding_hist(sym)
            except Exception:  # noqa: BLE001 — skip transient per-symbol failures
                continue
            if len(h) < 30:
                continue
            tf30[sym] = (sum(h) / len(h)) * INTERVALS_PER_DAY
            last24[sym] = sum(h[-INTERVALS_PER_DAY:])
            if i % 50 == 49:
                time.sleep(0.5)
        return liq, tf30, last24
