from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
import os
from typing import Any, Protocol

import requests

from src.hyperliquid.rounding import HyperliquidAssetMeta

MAINNET_INFO_URL = "https://api.hyperliquid.xyz/info"
TESTNET_INFO_URL = "https://api.hyperliquid-testnet.xyz/info"


def build_hyperliquid_info_url(env: str | None = None) -> str:
    resolved = (env or os.getenv("CTB_HL_ENV") or "mainnet").lower()
    if resolved == "testnet":
        return TESTNET_INFO_URL
    if resolved == "mainnet":
        return MAINNET_INFO_URL
    raise ValueError("CTB_HL_ENV must be mainnet or testnet")


class ReadOnlyTransport(Protocol):
    def post(self, url: str, payload: dict[str, Any], timeout: int = 10) -> Any: ...


class RequestsInfoTransport:
    def post(self, url: str, payload: dict[str, Any], timeout: int = 10) -> Any:
        response = requests.post(url, headers={"Content-Type": "application/json"}, json=payload, timeout=timeout)
        response.raise_for_status()
        return response.json()


@dataclass(frozen=True)
class HyperliquidMarketMeta:
    assets: dict[str, HyperliquidAssetMeta]

    @classmethod
    def from_meta(cls, payload: dict[str, Any]) -> "HyperliquidMarketMeta":
        universe = payload["universe"]
        return cls({row["name"]: HyperliquidAssetMeta(row["name"], idx, int(row["szDecimals"])) for idx, row in enumerate(universe)})

    @classmethod
    def from_meta_and_asset_ctxs(cls, payload: Any) -> "HyperliquidMarketMeta":
        universe = payload[0]["universe"] if isinstance(payload, list) else payload["universe"]
        return cls({row["name"]: HyperliquidAssetMeta(row["name"], idx, int(row["szDecimals"])) for idx, row in enumerate(universe)})


@dataclass(frozen=True)
class L2BookSummary:
    best_bid: Decimal | None
    best_ask: Decimal | None
    spread_pct: Decimal | None
    bid_depth_notional_5: Decimal = Decimal("0")
    ask_depth_notional_5: Decimal = Decimal("0")
    buy_impact_1k_pct: Decimal | None = None
    sell_impact_1k_pct: Decimal | None = None
    buy_impact_5k_pct: Decimal | None = None
    sell_impact_5k_pct: Decimal | None = None


def _level_notional(level: dict[str, Any]) -> Decimal:
    return Decimal(str(level.get("px", "0"))) * Decimal(str(level.get("sz", "0")))


def _impact_pct(levels: list[dict[str, Any]], *, target_notional: Decimal, mid: Decimal, side: str) -> Decimal | None:
    if target_notional <= 0 or mid <= 0:
        return None
    remaining = target_notional
    total_base = Decimal("0")
    total_quote = Decimal("0")
    for level in levels:
        px = Decimal(str(level.get("px", "0")))
        sz = Decimal(str(level.get("sz", "0")))
        if px <= 0 or sz <= 0:
            continue
        level_quote = px * sz
        take_quote = min(remaining, level_quote)
        take_base = take_quote / px
        total_base += take_base
        total_quote += take_quote
        remaining -= take_quote
        if remaining <= 0:
            break
    if remaining > 0 or total_base <= 0:
        return None
    avg_px = total_quote / total_base
    if side == "buy":
        return max(Decimal("0"), (avg_px - mid) / mid * Decimal("100"))
    return max(Decimal("0"), (mid - avg_px) / mid * Decimal("100"))


def summarize_l2_book(book: dict[str, Any]) -> L2BookSummary:
    levels = book.get("levels") or [[], []]
    bids = levels[0] if len(levels) > 0 else []
    asks = levels[1] if len(levels) > 1 else []
    best_bid = Decimal(str(bids[0]["px"])) if bids else None
    best_ask = Decimal(str(asks[0]["px"])) if asks else None
    spread_pct = None
    mid = None
    if best_bid is not None and best_ask is not None and best_bid > 0:
        mid = (best_bid + best_ask) / Decimal("2")
        spread_pct = (best_ask - best_bid) / mid * Decimal("100") if mid > 0 else None
    bid_depth = sum((_level_notional(level) for level in bids[:5]), Decimal("0"))
    ask_depth = sum((_level_notional(level) for level in asks[:5]), Decimal("0"))
    buy_1k = sell_1k = buy_5k = sell_5k = None
    if mid is not None:
        buy_1k = _impact_pct(asks, target_notional=Decimal("1000"), mid=mid, side="buy")
        sell_1k = _impact_pct(bids, target_notional=Decimal("1000"), mid=mid, side="sell")
        buy_5k = _impact_pct(asks, target_notional=Decimal("5000"), mid=mid, side="buy")
        sell_5k = _impact_pct(bids, target_notional=Decimal("5000"), mid=mid, side="sell")
    return L2BookSummary(
        best_bid=best_bid,
        best_ask=best_ask,
        spread_pct=spread_pct,
        bid_depth_notional_5=bid_depth,
        ask_depth_notional_5=ask_depth,
        buy_impact_1k_pct=buy_1k,
        sell_impact_1k_pct=sell_1k,
        buy_impact_5k_pct=buy_5k,
        sell_impact_5k_pct=sell_5k,
    )


class HyperliquidMarketData:
    """Read-only Hyperliquid info endpoint adapter. No exchange/order imports here."""

    def __init__(self, env: str | None = None, transport: ReadOnlyTransport | None = None) -> None:
        self.env = (env or os.getenv("CTB_HL_ENV") or "mainnet").lower()
        self.info_url = build_hyperliquid_info_url(self.env)
        self.transport = transport or RequestsInfoTransport()
        self._meta_cache: HyperliquidMarketMeta | None = None

    def _post(self, payload: dict[str, Any], timeout: int = 10) -> Any:
        return self.transport.post(self.info_url, payload, timeout=timeout)

    def get_meta(self) -> dict[str, Any]:
        return self._post({"type": "meta"})

    def get_meta_and_asset_ctxs(self) -> Any:
        return self._post({"type": "metaAndAssetCtxs"})

    def get_all_mids(self) -> dict[str, Decimal]:
        data = self._post({"type": "allMids"})
        return {coin: Decimal(str(price)) for coin, price in data.items()}

    def get_candles(self, coin: str, interval: str, start_ms: int, end_ms: int) -> list[dict[str, Any]]:
        return self._post({"type": "candleSnapshot", "req": {"coin": coin.upper(), "interval": interval, "startTime": start_ms, "endTime": end_ms}})

    def get_l2_book(self, coin: str) -> dict[str, Any]:
        return self._post({"type": "l2Book", "coin": coin.upper()})

    def get_asset_universe(self) -> dict[str, HyperliquidAssetMeta]:
        if self._meta_cache is None:
            self._meta_cache = HyperliquidMarketMeta.from_meta(self.get_meta())
        return self._meta_cache.assets

    def get_symbol_meta(self, coin: str) -> HyperliquidAssetMeta:
        assets = self.get_asset_universe()
        coin = coin.upper()
        if coin not in assets:
            raise KeyError(f"Unknown Hyperliquid coin: {coin}")
        return assets[coin]

    def get_sz_decimals(self, coin: str) -> int:
        return self.get_symbol_meta(coin).sz_decimals

    # Compatibility aliases from previous scaffold.
    def meta(self) -> HyperliquidMarketMeta:
        return HyperliquidMarketMeta.from_meta(self.get_meta())

    def all_mids(self) -> dict[str, Decimal]:
        return self.get_all_mids()

    def candle_snapshot(self, request: dict[str, Any]) -> list[dict[str, Any]]:
        return self._post({"type": "candleSnapshot", "req": request})
