"""
V16 — Feeds: Binance (precio spot) y CLOB (orderbook Polymarket).

CLOBFeed mantiene un libro local completo (price→size) para derivar
best_bid/best_ask correctamente, filtrando cancelaciones (size=0).
"""
import json, time, websocket


class BinanceFeed:
    def __init__(self, ws_url: str):
        self.ws_url  = ws_url
        self.last_px = 0.0
        self._ws     = None
        self._stop   = False

    def run(self):
        def on_msg(ws, msg):
            try:
                self.last_px = float(json.loads(msg)["p"])
            except Exception:
                pass
        backoff = 2
        while not self._stop:
            try:
                self._ws = websocket.WebSocketApp(self.ws_url, on_message=on_msg)
                self._ws.run_forever(ping_interval=20, ping_timeout=10)
                backoff = 2
            except Exception as e:
                print(f"[binance] ws error: {e}")
            if not self._stop:
                time.sleep(backoff)
                backoff = min(backoff * 2, 60)

    def stop(self):
        self._stop = True
        try:
            self._ws.close()
        except Exception:
            pass


class CLOBFeed:
    """Tracks best bid/ask for a single token via CLOB websocket.

    Mantiene un libro local (dict price→size) para manejar correctamente:
    - Snapshot inicial (event_type=book): reconstruye el libro entero
    - Updates incrementales (event_type=price_change): aplica deltas,
      eliminando niveles con size=0 (cancelaciones)
    """

    def __init__(self, token_id: str, ws_url: str):
        self.token_id = token_id
        self.ws_url   = ws_url
        self.best_bid = 0.0
        self.best_ask = 0.0
        self._bids    = {}  # price (float) -> size (float)
        self._asks    = {}
        self._ws      = None
        self._stop    = False

    def _update_best(self):
        self.best_bid = max(self._bids.keys()) if self._bids else 0.0
        self.best_ask = min(self._asks.keys()) if self._asks else 0.0

    def _on_open(self, ws):
        ws.send(json.dumps({"type": "market", "assets_ids": [self.token_id]}))

    def _on_msg(self, ws, msg):
        try:
            it    = json.loads(msg)
            items = it if isinstance(it, list) else [it]
            for i in items:
                event = i.get("event_type", "")

                if event == "book" or ("bids" in i and "asks" in i):
                    # Snapshot completo: reconstruir libro filtrando size=0
                    self._bids = {
                        float(x["price"]): float(x["size"])
                        for x in i.get("bids", [])
                        if float(x.get("size", 0)) > 0
                    }
                    self._asks = {
                        float(x["price"]): float(x["size"])
                        for x in i.get("asks", [])
                        if float(x.get("size", 0)) > 0
                    }
                    self._update_best()

                elif event == "price_change":
                    # Update incremental: aplicar deltas nivel por nivel
                    for ch in i.get("changes", []):
                        price = float(ch["price"])
                        size  = float(ch.get("size", 0))
                        side  = ch.get("side", "").upper()
                        if side in ("SELL", "ASK"):
                            if size > 0:
                                self._asks[price] = size
                            else:
                                self._asks.pop(price, None)  # cancelación
                        elif side in ("BUY", "BID"):
                            if size > 0:
                                self._bids[price] = size
                            else:
                                self._bids.pop(price, None)
                    self._update_best()

        except Exception:
            pass

    def run(self):
        backoff = 2
        while not self._stop:
            try:
                self._ws = websocket.WebSocketApp(
                    self.ws_url, on_open=self._on_open, on_message=self._on_msg)
                self._ws.run_forever(ping_interval=20, ping_timeout=10)
                backoff = 2
            except Exception as e:
                print(f"[clob:{self.token_id[:8]}] reconnect: {e}")
            if not self._stop:
                # Reset libro al reconectar para forzar snapshot fresco
                self._bids.clear()
                self._asks.clear()
                self.best_bid = 0.0
                self.best_ask = 0.0
                time.sleep(backoff)
                backoff = min(backoff * 2, 60)

    def stop(self):
        self._stop = True
        try:
            self._ws.close()
        except Exception:
            pass
