from __future__ import annotations

import asyncio
import json
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Set, Tuple, Any

import websockets

from app.streaming.live_store import live_store


@dataclass(frozen=True)
class StreamKey:
    symbol: str
    timeframe: str  # only '1m' supported in hub


class BinanceStreamHub:
    """
    Maintains a single upstream websocket per (symbol,timeframe) and fans out
    kline events to multiple subscribers via asyncio.Queues.
    """

    def __init__(self, base_ws: str | None = None) -> None:
        self.base_ws = (base_ws or os.getenv("BINANCE_WS_URL", "wss://stream.binance.com:9443/ws")).rstrip("/")
        self._subs: Dict[StreamKey, Set[asyncio.Queue]] = {}
        self._tasks: Dict[StreamKey, asyncio.Task] = {}
        self._lock = asyncio.Lock()

    def get_status(self) -> list[dict]:
        """Return status snapshot of active streams."""
        out: list[dict] = []
        for key, subs in self._subs.items():
            hist = live_store.get_history(key.symbol, key.timeframe)
            last_ts = hist[-1]["time"] if hist else None
            last_iso = None
            if isinstance(last_ts, (int, float)):
                try:
                    last_iso = datetime.utcfromtimestamp(int(last_ts)).isoformat() + "Z"
                except Exception:
                    last_iso = None
            out.append({
                "symbol": key.symbol,
                "timeframe": key.timeframe,
                "subscribers": len(subs),
                "last_event_time": last_iso,
            })
        return out

    async def subscribe(self, symbol: str, timeframe: str = "1m") -> Tuple[asyncio.Queue, Any]:
        """Subscribe to a stream. Returns (queue, unsubscribe_cb)."""
        if timeframe != "1m":
            raise ValueError("BinanceStreamHub currently supports timeframe '1m' only")
        key = StreamKey(symbol=symbol.upper().replace("/", ""), timeframe=timeframe)
        q: asyncio.Queue = asyncio.Queue(maxsize=1000)
        async with self._lock:
            subs = self._subs.get(key)
            if not subs:
                subs = set()
                self._subs[key] = subs
            subs.add(q)
            if key not in self._tasks:
                self._tasks[key] = asyncio.create_task(self._run_stream(key))

        async def _unsubscribe() -> None:
            async with self._lock:
                s = self._subs.get(key)
                if s and q in s:
                    s.remove(q)
                # Close queue to unblock listeners
                try:
                    q.put_nowait(None)
                except Exception:
                    pass
                if s is not None and len(s) == 0:
                    # cancel task and cleanup
                    t = self._tasks.pop(key, None)
                    if t:
                        t.cancel()
                    self._subs.pop(key, None)
        return q, _unsubscribe

    async def _run_stream(self, key: StreamKey) -> None:
        symbol = key.symbol
        stream = f"{symbol.lower()}@kline_{key.timeframe}"
        url = self.base_ws.replace("/ws", "/stream") + f"?streams={stream}"
        # Reconnect loop
        while True:
            try:
                async with websockets.connect(url, ping_interval=20, ping_timeout=20) as ws:
                    async for message in ws:
                        try:
                            data = json.loads(message)
                            k = (data.get("data") or {}).get("k") or {}
                            if not k:
                                continue
                            # Normalize event
                            evt = {
                                "symbol": symbol,
                                "timeframe": key.timeframe,
                                "open_time": datetime.fromtimestamp(k["t"] / 1000.0).isoformat(),
                                "close_time": datetime.fromtimestamp(k["T"] / 1000.0).isoformat(),
                                "open": float(k["o"]),
                                "high": float(k["h"]),
                                "low": float(k["l"]),
                                "close": float(k["c"]),
                                "volume": float(k.get("v", 0.0)),
                                "is_closed": bool(k.get("x", False)),
                                "source": "binance",
                            }
                            # Update live store (1m bar)
                            try:
                                tsec = int(k["T"] // 1000)
                                live_store.ingest_bar(symbol=symbol, timeframe=key.timeframe, bar={
                                    "time": tsec, "open": evt["open"], "high": evt["high"], "low": evt["low"], "close": evt["close"], "volume": evt["volume"],
                                })
                            except Exception:
                                pass
                            # Fan-out to subscribers
                            subs = self._subs.get(key) or set()
                            for q in list(subs):
                                try:
                                    if q.full():
                                        q.get_nowait()
                                    q.put_nowait(evt)
                                except Exception:
                                    # Drop failed subscriber
                                    try:
                                        subs.remove(q)
                                    except Exception:
                                        pass
                        except Exception:
                            continue
            except Exception:
                await asyncio.sleep(1.5)


# Singleton hub instance
hub = BinanceStreamHub()
