from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from statistics import fmean
from typing import Any

from src.hyperliquid.api_health import ApiHealthMonitor
from src.hyperliquid.market_data import HyperliquidMarketData, summarize_l2_book
from src.market.context import CoinMarketContext, DEFAULT_CONTEXT_UNIVERSE, MarketContextSnapshot


def _d(value: Any, default: str = "0") -> Decimal:
    try:
        return Decimal(str(value))
    except Exception:
        return Decimal(default)


def _sma(values: list[Decimal], n: int) -> Decimal:
    sample = values[-n:]
    return sum(sample) / Decimal(len(sample)) if sample else Decimal("0")


def _rsi(closes: list[Decimal], period: int = 14) -> Decimal:
    if len(closes) <= period:
        return Decimal("50")
    gains: list[Decimal] = []
    losses: list[Decimal] = []
    for prev, curr in zip(closes[-period-1:-1], closes[-period:]):
        diff = curr - prev
        gains.append(max(diff, Decimal("0")))
        losses.append(abs(min(diff, Decimal("0"))))
    avg_gain = sum(gains) / Decimal(period)
    avg_loss = sum(losses) / Decimal(period)
    if avg_loss == 0:
        return Decimal("100")
    rs = avg_gain / avg_loss
    return Decimal("100") - (Decimal("100") / (Decimal("1") + rs))


def _asset_contexts_by_coin(payload: Any) -> dict[str, dict[str, Any]]:
    if not isinstance(payload, list) or len(payload) < 2:
        return {}
    meta, asset_ctxs = payload[0], payload[1]
    universe = meta.get("universe") or [] if isinstance(meta, dict) else []
    if not isinstance(asset_ctxs, list):
        return {}
    result: dict[str, dict[str, Any]] = {}
    for idx, row in enumerate(asset_ctxs):
        if idx >= len(universe) or not isinstance(row, dict):
            continue
        asset = universe[idx]
        name = str(asset.get("name", "")).upper() if isinstance(asset, dict) else ""
        if name:
            result[name] = row
    return result


def _atr(candles: list[dict[str, Any]], period: int = 14) -> Decimal:
    trs: list[Decimal] = []
    for row in candles[-period:]:
        trs.append(_d(row.get("h")) - _d(row.get("l")))
    return sum(trs) / Decimal(len(trs)) if trs else Decimal("0")


@dataclass(frozen=True)
class MarketContextCollector:
    market_data: HyperliquidMarketData
    api_health: ApiHealthMonitor

    def collect(self, *, coins: tuple[str, ...] = DEFAULT_CONTEXT_UNIVERSE, interval: str = "5m", lookback_ms: int = 4 * 60 * 60 * 1000) -> MarketContextSnapshot:
        now = datetime.now(timezone.utc)
        now_ms = int(now.timestamp() * 1000)
        mids = self.market_data.get_all_mids()
        try:
            asset_contexts = _asset_contexts_by_coin(self.market_data.get_meta_and_asset_ctxs())
        except Exception as exc:
            self.api_health.record_error(exc)
            asset_contexts = {}
        ctxs: list[CoinMarketContext] = []
        for coin in coins:
            coin = coin.upper()
            stale = False
            l2_available = False
            spread_pct: Decimal | None = None
            bid_depth_notional_5 = Decimal("0")
            ask_depth_notional_5 = 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
            try:
                candles = self.market_data.get_candles(coin, interval, now_ms - lookback_ms, now_ms)
                closes = [_d(row.get("c")) for row in candles if row.get("c") is not None]
                volume = sum((_d(row.get("v")) for row in candles), Decimal("0"))
            except Exception as exc:
                self.api_health.record_error(exc)
                self.api_health.record_stale_context()
                candles = []
                closes = []
                volume = Decimal("0")
                stale = True
            try:
                summary = summarize_l2_book(self.market_data.get_l2_book(coin))
                spread_pct = summary.spread_pct
                l2_available = summary.best_bid is not None and summary.best_ask is not None
                bid_depth_notional_5 = summary.bid_depth_notional_5
                ask_depth_notional_5 = summary.ask_depth_notional_5
                buy_impact_1k_pct = summary.buy_impact_1k_pct
                sell_impact_1k_pct = summary.sell_impact_1k_pct
                buy_impact_5k_pct = summary.buy_impact_5k_pct
                sell_impact_5k_pct = summary.sell_impact_5k_pct
            except Exception as exc:
                self.api_health.record_error(exc)
            mid = mids.get(coin) or (closes[-1] if closes else Decimal("0"))
            asset_ctx = asset_contexts.get(coin, {})
            reliability = Decimal("1.0")
            if stale or not closes:
                reliability -= Decimal("0.5")
            if not l2_available:
                reliability -= Decimal("0.25")
            ctxs.append(CoinMarketContext(
                coin=coin,
                rsi=_rsi(closes),
                sma_fast=_sma(closes, 12),
                sma_slow=_sma(closes, 26),
                atr=_atr(candles),
                funding=_d(asset_ctx.get("funding")),
                volume=volume,
                mid=mid,
                spread_pct=spread_pct,
                timestamp=now,
                reliability_score=max(Decimal("0"), reliability),
                stale_data=stale,
                l2_available=l2_available,
                open_interest=_d(asset_ctx.get("openInterest")),
                premium=_d(asset_ctx.get("premium")),
                oracle_px=_d(asset_ctx.get("oraclePx")),
                mark_px=_d(asset_ctx.get("markPx")),
                prev_day_px=_d(asset_ctx.get("prevDayPx")),
                day_ntl_vlm=_d(asset_ctx.get("dayNtlVlm")),
                bid_depth_notional_5=bid_depth_notional_5,
                ask_depth_notional_5=ask_depth_notional_5,
                buy_impact_1k_pct=buy_impact_1k_pct,
                sell_impact_1k_pct=sell_impact_1k_pct,
                buy_impact_5k_pct=buy_impact_5k_pct,
                sell_impact_5k_pct=sell_impact_5k_pct,
            ))
        return MarketContextSnapshot.from_coin_contexts(ctxs)
