"""
Hyperliquid data provider — historical candles, orderbook snapshots, and WebSocket streams.

Fetches OHLCV candles from Hyperliquid info API (candleSnapshot) and
provides them as pandas DataFrames (for VectorBT) and NT Bar objects
(for NautilusTrader backtesting).

WebSocket support: real-time orderbook, trades, mark prices via Hyperliquid WS.
"""
from __future__ import annotations

import asyncio
import json
import logging
import time
from datetime import datetime, timezone
from typing import AsyncIterator, Callable

import numpy as np
import pandas as pd
import requests

from nautilus_trader.model.data import Bar, BarSpecification, BarType
from nautilus_trader.model.enums import BarAggregation, PriceType
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.model.objects import Price, Quantity

logger = logging.getLogger(__name__)

TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
MAINNET_API = "https://api.hyperliquid.xyz/info"

WS_TESTNET = "wss://api.hyperliquid-testnet.xyz/ws"
WS_MAINNET = "wss://api.hyperliquid.xyz/ws"

INTERVAL_MAP: dict[str, str] = {
    "1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m",
    "1h": "1h", "4h": "4h", "8h": "8h", "1d": "1d",
    "1w": "1w",
}

INTERVAL_TO_SECONDS: dict[str, int] = {
    "1m": 60, "5m": 300, "15m": 900, "30m": 1800,
    "1h": 3600, "4h": 14400, "8h": 28800, "1d": 86400,
    "1w": 604800,
}


class HyperliquidDataProvider:
    """Fetches and manages Hyperliquid market data."""

    def __init__(self, testnet: bool = True):
        self._api_url = TESTNET_API if testnet else MAINNET_API
        self._ws_url = WS_TESTNET if testnet else WS_MAINNET
        self._testnet = testnet

    # ── Historical candles ──────────────────────────────────────

    def fetch_candles(
        self,
        coin: str,
        interval: str = "1h",
        start_ms: int | None = None,
        end_ms: int | None = None,
        limit: int = 5000,
    ) -> pd.DataFrame:
        """Fetch OHLCV candles from Hyperliquid info API.

        Returns DataFrame with columns: open, high, low, close, volume, timestamp.
        Timestamp is UTC datetime index.
        """
        hl_interval = INTERVAL_MAP.get(interval, interval)
        now = int(time.time() * 1000)
        payload = {
            "type": "candleSnapshot",
            "req": {
                "coin": coin.upper(),
                "interval": hl_interval,
                "startTime": start_ms or (now - limit * INTERVAL_TO_SECONDS.get(interval, 3600) * 1000),
                "endTime": end_ms or now,
            },
        }
        resp = requests.post(self._api_url, json=payload, timeout=30)
        resp.raise_for_status()
        candles = resp.json()

        if not candles:
            return pd.DataFrame(columns=["open", "high", "low", "close", "volume", "timestamp"])

        rows = []
        for c in candles:
            rows.append({
                "open": float(c["o"]),
                "high": float(c["h"]),
                "low": float(c["l"]),
                "close": float(c["c"]),
                "volume": float(c["v"]),
                "timestamp": datetime.fromtimestamp(c["t"] / 1000, tz=timezone.utc),
            })
        df = pd.DataFrame(rows)
        df.set_index("timestamp", inplace=True)
        df.sort_index(inplace=True)
        return df

    def fetch_multi_candles(
        self,
        coins: list[str],
        interval: str = "1h",
        limit: int = 5000,
    ) -> dict[str, pd.DataFrame]:
        """Fetch candles for multiple coins in parallel."""
        results = {}
        for coin in coins:
            try:
                results[coin] = self.fetch_candles(coin, interval=interval, limit=limit)
            except Exception as e:
                logger.warning("Failed to fetch %s candles: %s", coin, e)
        return results

    def to_nt_bars(
        self,
        df: pd.DataFrame,
        instrument_id: InstrumentId,
        step: int = 1,
        bar_aggregation: BarAggregation = BarAggregation.MINUTE,
        price_type: PriceType = PriceType.LAST,
    ) -> list[Bar]:
        """Convert a pandas DataFrame of candles to NautilusTrader Bar objects."""
        spec = BarSpecification(step, bar_aggregation, price_type)
        bar_type = BarType(instrument_id, spec)
        bars = []

        for idx, row in df.iterrows():
            ts_event = int(idx.timestamp() * 1e9)
            ts_init = ts_event
            bar = Bar(
                bar_type=bar_type,
                open=Price(row["open"], instrument_id.venue.precision or 2),
                high=Price(row["high"], instrument_id.venue.precision or 2),
                low=Price(row["low"], instrument_id.venue.precision or 2),
                close=Price(row["close"], instrument_id.venue.precision or 2),
                volume=Quantity(row["volume"], 0),
                ts_event=ts_event,
                ts_init=ts_init,
            )
            bars.append(bar)

        return bars

    # ── Orderbook snapshots ─────────────────────────────────────

    def fetch_orderbook(self, coin: str) -> dict:
        """Get current L2 orderbook snapshot."""
        resp = requests.post(self._api_url, json={"type": "l2Book", "coin": coin.upper()}, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        bids = [[float(l["px"]), float(l["sz"])] for l in data["levels"][0]]
        asks = [[float(l["px"]), float(l["sz"])] for l in data["levels"][1]]
        return {
            "bids": bids,
            "asks": asks,
            "timestamp": time.time(),
        }

    def fetch_orderbook_df(self, coin: str) -> tuple[pd.DataFrame, pd.DataFrame]:
        """Get orderbook as bid/ask DataFrames."""
        ob = self.fetch_orderbook(coin)
        bids_df = pd.DataFrame(ob["bids"], columns=["price", "size"])
        asks_df = pd.DataFrame(ob["asks"], columns=["price", "size"])
        return bids_df, asks_df

    # ── Mark prices ─────────────────────────────────────────────

    def fetch_mark_prices(self) -> dict[str, float]:
        """Get current mark prices for all assets."""
        resp = requests.post(self._api_url, json={"type": "metaAndAssetCtxs"}, timeout=10)
        resp.raise_for_status()
        data = resp.json()
        if not isinstance(data, list) or len(data) < 2:
            return {}
        universe = data[0].get("universe", [])
        ctxs = data[1]
        prices = {}
        for i, u in enumerate(universe):
            if i < len(ctxs):
                prices[u["name"]] = float(ctxs[i].get("markPx", 0))
        return prices

    # ── WebSocket streaming ─────────────────────────────────────

    async def stream_orderbook(self, coin: str) -> AsyncIterator[dict]:
        """Stream L2 orderbook updates via Hyperliquid WebSocket."""
        try:
            import websockets
        except ImportError:
            logger.error("websockets not installed; pip install websockets")
            return

        subscribe_msg = json.dumps({"method": "subscribe", "subscription": {"type": "l2Book", "coin": coin.upper()}})
        while True:
            try:
                async with websockets.connect(self._ws_url) as ws:
                    await ws.send(subscribe_msg)
                    async for msg in ws:
                        yield json.loads(msg)
            except Exception as e:
                logger.warning("WebSocket error: %s (reconnecting)", e)
                await asyncio.sleep(1)

    async def stream_prices(self, coins: list[str]) -> AsyncIterator[dict[str, float]]:
        """Stream mark prices via polling fallback (1s interval).

        Hyperliquid WebSocket doesn't have a simple 'mark prices' stream,
        so we poll the REST API with async sleep.
        """
        while True:
            try:
                prices = self.fetch_mark_prices()
                yield {c: prices.get(c, 0) for c in coins}
            except Exception as e:
                logger.warning("Price poll error: %s", e)
            await asyncio.sleep(1)
