"""Hyperliquid API client — REST + WebSocket with rate limiting."""
from __future__ import annotations
import json
import time
import logging
from typing import Any, Optional
import requests

log = logging.getLogger(__name__)

BASE = "https://api.hyperliquid.xyz"
INFO_URL = f"{BASE}/info"


class RateLimiter:
    """Token bucket rate limiter — conservative for HL API."""
    def __init__(self, rate: float = 3.0, burst: int = 8):
        self.rate = rate
        self.burst = burst
        self.tokens = float(burst)
        self.last = time.monotonic()

    def acquire(self):
        now = time.monotonic()
        elapsed = now - self.last
        self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
        self.last = now
        if self.tokens < 1:
            wait = (1 - self.tokens) / self.rate
            time.sleep(wait)
            self.tokens = 0
        else:
            self.tokens -= 1


class HLClient:
    """Hyperliquid REST client with built-in rate limiting."""

    def __init__(self, timeout: float = 12.0):
        self.session = requests.Session()
        self.session.headers.update({"Content-Type": "application/json"})
        self.timeout = timeout
        self.limiter = RateLimiter()
        self._meta_cache: Optional[dict] = None
        self._meta_ts: float = 0

    def _post(self, payload: dict) -> Any:
        self.limiter.acquire()
        for attempt in range(3):
            try:
                r = self.session.post(INFO_URL, json=payload, timeout=self.timeout)
                if r.status_code == 429:
                    wait = min(2 ** attempt * 2, 30)
                    log.warning(f"429 rate limited, waiting {wait}s")
                    time.sleep(wait)
                    self.limiter.tokens = 0
                    continue
                r.raise_for_status()
                return r.json()
            except requests.exceptions.RequestException as e:
                if attempt == 2:
                    log.error(f"API request failed after 3 attempts: {e}")
                    raise
                time.sleep(2 ** attempt)
        return {}

    # === Market Data ===

    def all_mids(self) -> dict[str, float]:
        """All mid prices {coin: price}."""
        data = self._post({"type": "allMids"})
        return {k: float(v) for k, v in data.items()}

    def meta_and_contexts(self) -> list[Any]:
        """Universe metadata + asset contexts (funding, OI, volume)."""
        return self._post({"type": "metaAndAssetCtxs"})

    def meta(self) -> dict:
        if self._meta_cache and time.time() - self._meta_ts < 300:
            return self._meta_cache
        data = self._post({"type": "meta"})
        self._meta_cache = data
        self._meta_ts = time.time()
        return data

    def l2_book(self, coin: str) -> dict:
        return self._post({"type": "l2Book", "coin": coin})

    def candles(
        self,
        coin: str,
        interval: str = "5m",
        start_ms: Optional[int] = None,
        end_ms: Optional[int] = None,
    ) -> list[dict]:
        now = int(time.time() * 1000)
        payload = {
            "type": "candleSnapshot",
            "req": {
                "coin": coin,
                "interval": interval,
                "startTime": start_ms or (now - 86400_000),
                "endTime": end_ms or now,
            },
        }
        result = self._post(payload)
        return result if isinstance(result, list) else []

    def trades(self, coin: str) -> list[dict]:
        """Recent trades (last ~100)."""
        return self._post({"type": "recentTrades", "coin": coin})

    def funding_history(self, coin: str, start_ms: int, end_ms: int) -> list:
        result = self._post({
            "type": "fundingHistory",
            "coin": coin,
            "startTime": start_ms,
            "endTime": end_ms,
        })
        return result if isinstance(result, list) else []

    def predicted_fundings(self) -> list:
        """Predicted funding rates across Hyperliquid and external venues."""
        result = self._post({"type": "predictedFundings"})
        return result if isinstance(result, list) else []

    def perps_at_open_interest_cap(self) -> set[str]:
        """Perps currently at OI cap, where execution risk is higher."""
        result = self._post({"type": "perpsAtOpenInterestCap"})
        return {str(coin) for coin in result} if isinstance(result, list) else set()

    def perp_categories(self) -> dict[str, str]:
        """Builder-deployed perp categories, useful for filtering non-crypto markets."""
        result = self._post({"type": "perpCategories"})
        if not isinstance(result, list):
            return {}
        categories: dict[str, str] = {}
        for row in result:
            if isinstance(row, list) and len(row) >= 2:
                categories[str(row[0])] = str(row[1])
        return categories

    def perp_concise_annotations(self) -> dict[str, dict]:
        """Concise metadata for annotated perps."""
        result = self._post({"type": "perpConciseAnnotations"})
        if not isinstance(result, list):
            return {}
        annotations: dict[str, dict] = {}
        for row in result:
            if isinstance(row, list) and len(row) >= 2 and isinstance(row[1], dict):
                annotations[str(row[0])] = row[1]
        return annotations

    # === Account ===

    def user_state(self, user: str) -> dict:
        return self._post({"type": "clearinghouseState", "user": user})

    def user_fills(self, user: str, start_ms: int, end_ms: int) -> list:
        result = self._post({
            "type": "userFillsByTime",
            "user": user,
            "startTime": start_ms,
            "endTime": end_ms,
            "aggregateByTime": True,
        })
        return result if isinstance(result, list) else []

    def open_orders(self, user: str) -> list:
        result = self._post({"type": "openOrders", "user": user})
        return result if isinstance(result, list) else []

    def frontend_open_orders(self, user: str) -> list:
        """Open orders with richer frontend trigger metadata for TP/SL hydration."""
        result = self._post({"type": "frontendOpenOrders", "user": user})
        return result if isinstance(result, list) else []

    # === Helpers ===

    def asset_context(self, coin: str) -> Optional[dict]:
        """Get funding, OI, volume for a specific coin."""
        data = self.meta_and_contexts()
        if not data or len(data) < 2:
            return None
        universe = data[0]["universe"]
        contexts = data[1]
        for i, asset in enumerate(universe):
            if asset["name"] == coin and i < len(contexts):
                ctx = contexts[i]
                return {
                    "funding": float(ctx.get("funding", 0)),
                    "open_interest": float(ctx.get("openInterest", 0)),
                    "day_volume": float(ctx.get("dayNtlVlm", 0)),
                    "mark_price": float(ctx.get("markPx", 0)),
                    "oracle_price": float(ctx.get("oraclePx", 0)),
                    "premium_bps": (
                        (float(ctx.get("markPx", 0)) - float(ctx.get("oraclePx", 0)))
                        / float(ctx.get("oraclePx", 1)) * 10000
                    ),
                }
        return None

    def asset_meta(self, coin: str) -> Optional[dict]:
        """Get static exchange metadata for one perp asset."""
        data = self.meta()
        for asset in data.get("universe", []):
            if asset.get("name") == coin:
                return asset
        return None

    def sz_decimals(self, coin: str) -> int:
        """Get size decimals for order rounding."""
        asset = self.asset_meta(coin)
        if asset is not None:
            return int(asset.get("szDecimals", 2))
        return 2

    def max_leverage(self, coin: str, default: int = 10) -> int:
        """Get exchange max leverage for a coin from Hyperliquid metadata."""
        asset = self.asset_meta(coin)
        if asset is not None:
            return int(asset.get("maxLeverage", default))
        return int(default)
