"""WebSocket real-time market data feed for OpenClaw Trading.

This module provides WebSocket-based real-time market data feeds from
multiple cryptocurrency exchanges, supporting klines (candlesticks),
order book updates, and trade streams.
"""

from __future__ import annotations

import asyncio
import json
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Callable, Coroutine, Dict, List, Optional, Set
from urllib.parse import urljoin

from loguru import logger

from openclaw.data.interface import Interval
from openclaw.exchange.models import Ticker

# Type alias for message handlers
MessageHandler = Callable[[str, Any], Coroutine[Any, Any, None]]


class WebSocketError(Exception):
    """Base exception for WebSocket feed errors."""

    pass


class ConnectionError(WebSocketError):
    """Raised when WebSocket connection fails."""

    pass


class SubscriptionError(WebSocketError):
    """Raised when subscription fails."""

    pass


@dataclass(frozen=True)
class KlineData:
    """Kline (candlestick) data structure.

    Attributes:
        symbol: Trading pair symbol (e.g., 'BTC/USDT')
        interval: Time interval
        timestamp: Kline start time
        open: Opening price
        high: Highest price
        low: Lowest price
        close: Closing price
        volume: Trading volume
        quote_volume: Quote asset volume
        trades: Number of trades
        is_closed: Whether this kline is closed
    """

    symbol: str
    interval: str
    timestamp: datetime
    open: float
    high: float
    low: float
    close: float
    volume: float
    quote_volume: float
    trades: int
    is_closed: bool


@dataclass(frozen=True)
class OrderBookLevel:
    """Order book price level.

    Attributes:
        price: Price level
        amount: Amount at this price
    """

    price: float
    amount: float


@dataclass
class OrderBook:
    """Order book data structure.

    Attributes:
        symbol: Trading pair symbol
        bids: List of bid levels (price, amount), sorted by price descending
        asks: List of ask levels (price, amount), sorted by price ascending
        timestamp: Last update timestamp
        last_update_id: Last update ID for synchronization
    """

    symbol: str
    bids: List[OrderBookLevel] = field(default_factory=list)
    asks: List[OrderBookLevel] = field(default_factory=list)
    timestamp: datetime = field(default_factory=datetime.now)
    last_update_id: int = 0

    @property
    def best_bid(self) -> Optional[OrderBookLevel]:
        """Get the best (highest) bid."""
        return self.bids[0] if self.bids else None

    @property
    def best_ask(self) -> Optional[OrderBookLevel]:
        """Get the best (lowest) ask."""
        return self.asks[0] if self.asks else None

    @property
    def spread(self) -> float:
        """Calculate bid-ask spread."""
        if self.best_bid and self.best_ask:
            return self.best_ask.price - self.best_bid.price
        return 0.0

    @property
    def mid_price(self) -> float:
        """Calculate mid price."""
        if self.best_bid and self.best_ask:
            return (self.best_bid.price + self.best_ask.price) / 2
        return 0.0

    def update(self, bids: List[List[float]], asks: List[List[float]], update_id: int) -> None:
        """Update order book with new levels."""
        self.timestamp = datetime.now()
        self.last_update_id = update_id

        # Update bids
        for price, amount in bids:
            self._update_level(self.bids, price, amount, reverse=True)

        # Update asks
        for price, amount in asks:
            self._update_level(self.asks, price, amount, reverse=False)

    def _update_level(
        self, levels: List[OrderBookLevel], price: float, amount: float, reverse: bool
    ) -> None:
        """Update a single price level."""
        # Remove zero amounts
        if amount == 0:
            levels[:] = [l for l in levels if l.price != price]
            return

        # Update existing level or insert new one
        for i, level in enumerate(levels):
            if level.price == price:
                levels[i] = OrderBookLevel(price=price, amount=amount)
                return
            # Insert in sorted order
            if (reverse and price > level.price) or (not reverse and price < level.price):
                levels.insert(i, OrderBookLevel(price=price, amount=amount))
                return

        # Append if not found
        levels.append(OrderBookLevel(price=price, amount=amount))


@dataclass(frozen=True)
class Trade:
    """Trade data structure.

    Attributes:
        symbol: Trading pair symbol
        trade_id: Unique trade ID
        price: Trade price
        amount: Trade amount
        side: Trade side (buy/sell)
        timestamp: Trade timestamp
        is_buyer_maker: Whether buyer is maker
    """

    symbol: str
    trade_id: str
    price: float
    amount: float
    side: str  # "buy" or "sell"
    timestamp: datetime
    is_buyer_maker: bool


class WebSocketFeed(ABC):
    """Abstract base class for WebSocket market data feeds.

    Provides a common interface for real-time market data from various exchanges.
    """

    def __init__(
        self,
        exchange_id: str,
        ws_url: str,
        reconnect_interval: float = 5.0,
        max_reconnect_attempts: int = 10,
        ping_interval: float = 20.0,
        ping_timeout: float = 10.0,
    ) -> None:
        """Initialize WebSocket feed.

        Args:
            exchange_id: Exchange identifier
            ws_url: WebSocket endpoint URL
            reconnect_interval: Seconds between reconnection attempts
            max_reconnect_attempts: Maximum reconnection attempts (0 = unlimited)
            ping_interval: Seconds between ping messages
            ping_timeout: Seconds to wait for pong response
        """
        self.exchange_id = exchange_id
        self.ws_url = ws_url
        self.reconnect_interval = reconnect_interval
        self.max_reconnect_attempts = max_reconnect_attempts
        self.ping_interval = ping_interval
        self.ping_timeout = ping_timeout

        self._ws: Any = None
        self._connected = False
        self._reconnect_count = 0
        self._stop_event = asyncio.Event()
        self._listen_task: Optional[asyncio.Task] = None
        self._ping_task: Optional[asyncio.Task] = None

        # Subscriptions
        self._subscribed_symbols: Set[str] = set()
        self._subscribed_klines: Dict[str, Set[str]] = {}  # symbol -> intervals
        self._subscribed_orderbooks: Set[str] = set()
        self._subscribed_trades: Set[str] = set()

        # Data caches
        self._orderbooks: Dict[str, OrderBook] = {}
        self._tickers: Dict[str, Ticker] = {}

        # Message handlers
        self._handlers: Dict[str, List[MessageHandler]] = {
            "kline": [],
            "orderbook": [],
            "trade": [],
            "ticker": [],
        }

        self._lock = asyncio.Lock()

    @property
    def is_connected(self) -> bool:
        """Check if WebSocket is connected."""
        return self._connected

    def add_handler(self, channel: str, handler: MessageHandler) -> None:
        """Add a message handler for a specific channel.

        Args:
            channel: Channel name (kline, orderbook, trade, ticker)
            handler: Async callback function
        """
        if channel in self._handlers:
            self._handlers[channel].append(handler)

    def remove_handler(self, channel: str, handler: MessageHandler) -> None:
        """Remove a message handler."""
        if channel in self._handlers and handler in self._handlers[channel]:
            self._handlers[channel].remove(handler)

    async def connect(self) -> None:
        """Connect to WebSocket endpoint with automatic reconnection."""
        self._stop_event.clear()
        await self._connect_with_retry()

    async def _connect_with_retry(self) -> None:
        """Connect with retry logic."""
        while not self._stop_event.is_set():
            try:
                await self._connect()
                self._connected = True
                self._reconnect_count = 0
                logger.info(f"Connected to {self.exchange_id} WebSocket")

                # Start background tasks
                self._listen_task = asyncio.create_task(self._listen())
                self._ping_task = asyncio.create_task(self._ping_loop())

                # Resubscribe to previous subscriptions
                await self._resubscribe()

                # Wait for disconnect
                await self._listen_task

            except Exception as e:
                self._connected = False
                self._reconnect_count += 1

                if self.max_reconnect_attempts > 0 and self._reconnect_count > self.max_reconnect_attempts:
                    logger.error(f"Max reconnection attempts ({self.max_reconnect_attempts}) reached")
                    raise ConnectionError(f"Failed to connect after {self.max_reconnect_attempts} attempts")

                logger.warning(
                    f"WebSocket connection failed (attempt {self._reconnect_count}): {e}. "
                    f"Retrying in {self.reconnect_interval}s..."
                )

                try:
                    await asyncio.wait_for(
                        self._stop_event.wait(),
                        timeout=self.reconnect_interval
                    )
                except asyncio.TimeoutError:
                    continue

    @abstractmethod
    async def _connect(self) -> None:
        """Establish WebSocket connection. Implement in subclass."""
        pass

    @abstractmethod
    async def _listen(self) -> None:
        """Listen for incoming messages. Implement in subclass."""
        pass

    @abstractmethod
    async def _ping_loop(self) -> None:
        """Send periodic ping messages. Implement in subclass."""
        pass

    async def disconnect(self) -> None:
        """Disconnect from WebSocket."""
        logger.info(f"Disconnecting from {self.exchange_id} WebSocket...")
        self._stop_event.set()
        self._connected = False

        if self._listen_task:
            self._listen_task.cancel()
            try:
                await self._listen_task
            except asyncio.CancelledError:
                pass

        if self._ping_task:
            self._ping_task.cancel()
            try:
                await self._ping_task
            except asyncio.CancelledError:
                pass

        await self._disconnect()
        logger.info(f"Disconnected from {self.exchange_id} WebSocket")

    @abstractmethod
    async def _disconnect(self) -> None:
        """Close WebSocket connection. Implement in subclass."""
        pass

    async def _resubscribe(self) -> None:
        """Resubscribe to all previous subscriptions after reconnection."""
        logger.info("Resubscribing to previous subscriptions...")

        # Resubscribe klines
        for symbol, intervals in self._subscribed_klines.items():
            for interval in intervals:
                try:
                    await self.subscribe_kline(symbol, interval)
                except Exception as e:
                    logger.error(f"Failed to resubscribe kline {symbol} {interval}: {e}")

        # Resubscribe orderbooks
        for symbol in self._subscribed_orderbooks:
            try:
                await self.subscribe_orderbook(symbol)
            except Exception as e:
                logger.error(f"Failed to resubscribe orderbook {symbol}: {e}")

        # Resubscribe trades
        for symbol in self._subscribed_trades:
            try:
                await self.subscribe_trades(symbol)
            except Exception as e:
                logger.error(f"Failed to resubscribe trades {symbol}: {e}")

    @abstractmethod
    async def subscribe_kline(self, symbol: str, interval: str | Interval) -> None:
        """Subscribe to kline (candlestick) updates.

        Args:
            symbol: Trading pair symbol
            interval: Time interval (e.g., '1m', '5m', '1h')
        """
        pass

    @abstractmethod
    async def unsubscribe_kline(self, symbol: str, interval: str | Interval) -> None:
        """Unsubscribe from kline updates."""
        pass

    @abstractmethod
    async def subscribe_orderbook(self, symbol: str, depth: int = 20) -> None:
        """Subscribe to order book updates.

        Args:
            symbol: Trading pair symbol
            depth: Order book depth
        """
        pass

    @abstractmethod
    async def unsubscribe_orderbook(self, symbol: str) -> None:
        """Unsubscribe from order book updates."""
        pass

    @abstractmethod
    async def subscribe_trades(self, symbol: str) -> None:
        """Subscribe to trade stream.

        Args:
            symbol: Trading pair symbol
        """
        pass

    @abstractmethod
    async def unsubscribe_trades(self, symbol: str) -> None:
        """Unsubscribe from trade stream."""
        pass

    async def _notify_handlers(self, channel: str, symbol: str, data: Any) -> None:
        """Notify all handlers for a channel."""
        if channel in self._handlers:
            for handler in self._handlers[channel]:
                try:
                    await handler(symbol, data)
                except Exception as e:
                    logger.error(f"Error in {channel} handler: {e}")

    def get_orderbook(self, symbol: str) -> Optional[OrderBook]:
        """Get current order book for a symbol."""
        return self._orderbooks.get(symbol)

    def get_ticker(self, symbol: str) -> Optional[Ticker]:
        """Get current ticker for a symbol."""
        return self._tickers.get(symbol)


class BinanceWebSocketFeed(WebSocketFeed):
    """Binance WebSocket feed implementation."""

    # Binance WebSocket endpoints
    SPOT_WS_URL = "wss://stream.binance.com:9443/ws"
    FUTURES_WS_URL = "wss://fstream.binance.com/ws"
    SPOT_STREAM_URL = "wss://stream.binance.com:9443/stream"
    FUTURES_STREAM_URL = "wss://fstream.binance.com/stream"

    # Interval mapping
    INTERVAL_MAP = {
        Interval.MINUTE_1: "1m",
        Interval.MINUTE_5: "5m",
        Interval.MINUTE_15: "15m",
        Interval.MINUTE_30: "30m",
        Interval.HOUR_1: "1h",
        Interval.HOUR_4: "4h",
        Interval.DAY_1: "1d",
        Interval.WEEK_1: "1w",
        Interval.MONTH_1: "1M",
    }

    def __init__(
        self,
        futures: bool = False,
        reconnect_interval: float = 5.0,
        max_reconnect_attempts: int = 10,
    ) -> None:
        """Initialize Binance WebSocket feed.

        Args:
            futures: Use futures market instead of spot
            reconnect_interval: Seconds between reconnection attempts
            max_reconnect_attempts: Maximum reconnection attempts
        """
        self.futures = futures
        ws_url = self.FUTURES_WS_URL if futures else self.SPOT_WS_URL
        exchange_id = "binance_futures" if futures else "binance"

        super().__init__(
            exchange_id=exchange_id,
            ws_url=ws_url,
            reconnect_interval=reconnect_interval,
            max_reconnect_attempts=max_reconnect_attempts,
        )

        self._combined_streams: Set[str] = set()
        self._stream_id = 1

    def _normalize_symbol(self, symbol: str) -> str:
        """Normalize symbol to Binance format (remove /)."""
        return symbol.replace("/", "").lower()

    def _denormalize_symbol(self, symbol: str) -> str:
        """Convert Binance symbol back to standard format."""
        # Try to identify quote currency
        for quote in ["usdt", "busd", "btc", "eth", "bnb", "usd"]:
            if symbol.endswith(quote):
                base = symbol[:-len(quote)]
                return f"{base.upper()}/{quote.upper()}"
        return symbol.upper()

    def _get_interval_str(self, interval: str | Interval) -> str:
        """Convert interval to Binance format."""
        if isinstance(interval, Interval):
            return self.INTERVAL_MAP.get(interval, "1h")
        return interval

    async def _connect(self) -> None:
        """Connect to Binance WebSocket."""
        try:
            import websockets
        except ImportError:
            raise ImportError("websockets package is required. Install with: pip install websockets")

        self._ws = await websockets.connect(
            self.ws_url,
            ping_interval=None,  # We handle ping ourselves
            ping_timeout=None,
        )

    async def _disconnect(self) -> None:
        """Close Binance WebSocket connection."""
        if self._ws:
            await self._ws.close()
            self._ws = None

    async def _listen(self) -> None:
        """Listen for Binance WebSocket messages."""
        try:
            import websockets
        except ImportError:
            raise ImportError("websockets package is required")

        while not self._stop_event.is_set():
            try:
                message = await self._ws.recv()
                await self._handle_message(message)
            except websockets.exceptions.ConnectionClosed:
                logger.warning("WebSocket connection closed")
                break
            except Exception as e:
                logger.error(f"Error handling message: {e}")

    async def _ping_loop(self) -> None:
        """Send periodic ping messages."""
        while not self._stop_event.is_set():
            try:
                await asyncio.wait_for(
                    self._stop_event.wait(),
                    timeout=self.ping_interval
                )
            except asyncio.TimeoutError:
                if self._ws and self._connected:
                    try:
                        await self._ws.send(json.dumps({"method": "LIST_SUBSCRIPTIONS", "id": self._stream_id}))
                        self._stream_id += 1
                    except Exception as e:
                        logger.error(f"Ping failed: {e}")

    async def _handle_message(self, message: str | bytes) -> None:
        """Handle incoming WebSocket message."""
        try:
            if isinstance(message, bytes):
                message = message.decode("utf-8")

            data = json.loads(message)

            # Handle combined stream response
            if "stream" in data:
                stream = data["stream"]
                payload = data["data"]

                if "@kline" in stream:
                    await self._handle_kline(payload)
                elif "@depth" in stream:
                    await self._handle_orderbook(payload)
                elif "@trade" in stream:
                    await self._handle_trade(payload)
                elif "@ticker" in stream:
                    await self._handle_ticker(payload)

            # Handle single stream response
            elif "e" in data:
                event_type = data["e"]

                if event_type == "kline":
                    await self._handle_kline(data)
                elif event_type == "depthUpdate":
                    await self._handle_orderbook(data)
                elif event_type == "trade":
                    await self._handle_trade(data)
                elif event_type == "24hrTicker":
                    await self._handle_ticker(data)

        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse message: {e}")
        except Exception as e:
            logger.error(f"Error processing message: {e}")

    async def _handle_kline(self, data: Dict[str, Any]) -> None:
        """Handle kline message."""
        try:
            k = data["k"]
            symbol = self._denormalize_symbol(k["s"])

            kline = KlineData(
                symbol=symbol,
                interval=k["i"],
                timestamp=datetime.fromtimestamp(k["t"] / 1000),
                open=float(k["o"]),
                high=float(k["h"]),
                low=float(k["l"]),
                close=float(k["c"]),
                volume=float(k["v"]),
                quote_volume=float(k["q"]),
                trades=int(k["n"]),
                is_closed=k["x"],
            )

            await self._notify_handlers("kline", symbol, kline)

        except Exception as e:
            logger.error(f"Error handling kline: {e}")

    async def _handle_orderbook(self, data: Dict[str, Any]) -> None:
        """Handle order book update."""
        try:
            symbol = self._denormalize_symbol(data.get("s", ""))

            if symbol not in self._orderbooks:
                self._orderbooks[symbol] = OrderBook(symbol=symbol)

            orderbook = self._orderbooks[symbol]

            # Parse bids and asks
            bids = [[float(p), float(a)] for p, a in data.get("b", [])]
            asks = [[float(p), float(a)] for p, a in data.get("a", [])]

            update_id = data.get("u", 0)
            orderbook.update(bids, asks, update_id)

            await self._notify_handlers("orderbook", symbol, orderbook)

        except Exception as e:
            logger.error(f"Error handling orderbook: {e}")

    async def _handle_trade(self, data: Dict[str, Any]) -> None:
        """Handle trade message."""
        try:
            symbol = self._denormalize_symbol(data.get("s", ""))

            trade = Trade(
                symbol=symbol,
                trade_id=str(data.get("t", "")),
                price=float(data.get("p", 0)),
                amount=float(data.get("q", 0)),
                side="buy" if data.get("m", False) else "sell",
                timestamp=datetime.fromtimestamp(data.get("T", 0) / 1000),
                is_buyer_maker=data.get("m", False),
            )

            await self._notify_handlers("trade", symbol, trade)

        except Exception as e:
            logger.error(f"Error handling trade: {e}")

    async def _handle_ticker(self, data: Dict[str, Any]) -> None:
        """Handle ticker message."""
        try:
            symbol = self._denormalize_symbol(data.get("s", ""))

            ticker = Ticker(
                symbol=symbol,
                bid=float(data.get("b", 0)),
                ask=float(data.get("a", 0)),
                last=float(data.get("c", 0)),
                high=float(data.get("h", 0)) if "h" in data else None,
                low=float(data.get("l", 0)) if "l" in data else None,
                volume=float(data.get("v", 0)) if "v" in data else None,
                timestamp=datetime.now(),
            )

            self._tickers[symbol] = ticker
            await self._notify_handlers("ticker", symbol, ticker)

        except Exception as e:
            logger.error(f"Error handling ticker: {e}")

    async def subscribe_kline(self, symbol: str, interval: str | Interval) -> None:
        """Subscribe to kline updates."""
        if not self._connected:
            raise ConnectionError("WebSocket not connected")

        norm_symbol = self._normalize_symbol(symbol)
        interval_str = self._get_interval_str(interval)
        stream = f"{norm_symbol}@kline_{interval_str}"

        # Track subscription
        if symbol not in self._subscribed_klines:
            self._subscribed_klines[symbol] = set()
        self._subscribed_klines[symbol].add(interval_str)

        # Subscribe via combined stream if needed
        if stream not in self._combined_streams:
            self._combined_streams.add(stream)
            await self._subscribe_stream(stream)

        logger.info(f"Subscribed to kline {symbol} {interval_str}")

    async def unsubscribe_kline(self, symbol: str, interval: str | Interval) -> None:
        """Unsubscribe from kline updates."""
        norm_symbol = self._normalize_symbol(symbol)
        interval_str = self._get_interval_str(interval)
        stream = f"{norm_symbol}@kline_{interval_str}"

        if symbol in self._subscribed_klines:
            self._subscribed_klines[symbol].discard(interval_str)

        self._combined_streams.discard(stream)
        await self._unsubscribe_stream(stream)

        logger.info(f"Unsubscribed from kline {symbol} {interval_str}")

    async def subscribe_orderbook(self, symbol: str, depth: int = 20) -> None:
        """Subscribe to order book updates."""
        if not self._connected:
            raise ConnectionError("WebSocket not connected")

        norm_symbol = self._normalize_symbol(symbol)
        stream = f"{norm_symbol}@depth@{depth}ms"

        self._subscribed_orderbooks.add(symbol)
        self._combined_streams.add(stream)
        await self._subscribe_stream(stream)

        # Initialize order book snapshot
        await self._fetch_orderbook_snapshot(symbol)

        logger.info(f"Subscribed to orderbook {symbol}")

    async def unsubscribe_orderbook(self, symbol: str) -> None:
        """Unsubscribe from order book updates."""
        norm_symbol = self._normalize_symbol(symbol)

        # Find and remove all depth streams for this symbol
        streams_to_remove = [s for s in self._combined_streams if s.startswith(f"{norm_symbol}@depth")]
        for stream in streams_to_remove:
            self._combined_streams.discard(stream)
            await self._unsubscribe_stream(stream)

        self._subscribed_orderbooks.discard(symbol)

        logger.info(f"Unsubscribed from orderbook {symbol}")

    async def subscribe_trades(self, symbol: str) -> None:
        """Subscribe to trade stream."""
        if not self._connected:
            raise ConnectionError("WebSocket not connected")

        norm_symbol = self._normalize_symbol(symbol)
        stream = f"{norm_symbol}@trade"

        self._subscribed_trades.add(symbol)
        self._combined_streams.add(stream)
        await self._subscribe_stream(stream)

        logger.info(f"Subscribed to trades {symbol}")

    async def unsubscribe_trades(self, symbol: str) -> None:
        """Unsubscribe from trade stream."""
        norm_symbol = self._normalize_symbol(symbol)
        stream = f"{norm_symbol}@trade"

        self._subscribed_trades.discard(symbol)
        self._combined_streams.discard(stream)
        await self._unsubscribe_stream(stream)

        logger.info(f"Unsubscribed from trades {symbol}")

    async def _subscribe_stream(self, stream: str) -> None:
        """Subscribe to a stream."""
        if self._ws:
            msg = {
                "method": "SUBSCRIBE",
                "params": [stream],
                "id": self._stream_id,
            }
            await self._ws.send(json.dumps(msg))
            self._stream_id += 1

    async def _unsubscribe_stream(self, stream: str) -> None:
        """Unsubscribe from a stream."""
        if self._ws:
            msg = {
                "method": "UNSUBSCRIBE",
                "params": [stream],
                "id": self._stream_id,
            }
            await self._ws.send(json.dumps(msg))
            self._stream_id += 1

    async def _fetch_orderbook_snapshot(self, symbol: str) -> None:
        """Fetch initial order book snapshot."""
        try:
            import aiohttp
        except ImportError:
            logger.warning("aiohttp not installed, skipping orderbook snapshot")
            return

        try:
            norm_symbol = self._normalize_symbol(symbol)
            base_url = (
                "https://fapi.binance.com/fapi/v1"
                if self.futures
                else "https://api.binance.com/api/v3"
            )

            async with aiohttp.ClientSession() as session:
                async with session.get(
                    f"{base_url}/depth",
                    params={"symbol": norm_symbol.upper(), "limit": 100},
                ) as response:
                    if response.status == 200:
                        data = await response.json()

                        if symbol not in self._orderbooks:
                            self._orderbooks[symbol] = OrderBook(symbol=symbol)

                        orderbook = self._orderbooks[symbol]

                        # Parse snapshot
                        bids = [[float(p), float(a)] for p, a in data.get("bids", [])]
                        asks = [[float(p), float(a)] for p, a in data.get("asks", [])]

                        orderbook.bids = [OrderBookLevel(price=p, amount=a) for p, a in bids]
                        orderbook.asks = [OrderBookLevel(price=p, amount=a) for p, a in asks]
                        orderbook.bids.sort(key=lambda x: x.price, reverse=True)
                        orderbook.asks.sort(key=lambda x: x.price)
                        orderbook.last_update_id = data.get("lastUpdateId", 0)

        except Exception as e:
            logger.error(f"Failed to fetch orderbook snapshot for {symbol}: {e}")


class WebSocketFeedManager:
    """Manager for multiple WebSocket feeds from different exchanges.

    Provides a unified interface for subscribing to market data from
    multiple exchanges simultaneously.
    """

    def __init__(self) -> None:
        """Initialize the feed manager."""
        self._feeds: Dict[str, WebSocketFeed] = {}
        self._handlers: Dict[str, Dict[str, List[MessageHandler]]] = {}
        self._lock = asyncio.Lock()

    def add_feed(self, feed: WebSocketFeed) -> None:
        """Add a WebSocket feed to the manager.

        Args:
            feed: WebSocket feed instance
        """
        self._feeds[feed.exchange_id] = feed
        self._handlers[feed.exchange_id] = {
            "kline": [],
            "orderbook": [],
            "trade": [],
            "ticker": [],
        }

        # Register proxy handlers
        for channel in ["kline", "orderbook", "trade", "ticker"]:
            feed.add_handler(channel, self._create_proxy_handler(feed.exchange_id, channel))

    def remove_feed(self, exchange_id: str) -> None:
        """Remove a WebSocket feed."""
        if exchange_id in self._feeds:
            del self._feeds[exchange_id]
            del self._handlers[exchange_id]

    def get_feed(self, exchange_id: str) -> Optional[WebSocketFeed]:
        """Get a feed by exchange ID."""
        return self._feeds.get(exchange_id)

    def list_feeds(self) -> List[str]:
        """List all connected exchange IDs."""
        return list(self._feeds.keys())

    def add_handler(self, exchange_id: str, channel: str, handler: MessageHandler) -> None:
        """Add a message handler for a specific exchange and channel.

        Args:
            exchange_id: Exchange identifier
            channel: Channel name (kline, orderbook, trade, ticker)
            handler: Async callback function
        """
        if exchange_id in self._handlers and channel in self._handlers[exchange_id]:
            self._handlers[exchange_id][channel].append(handler)

    def remove_handler(self, exchange_id: str, channel: str, handler: MessageHandler) -> None:
        """Remove a message handler."""
        if exchange_id in self._handlers and channel in self._handlers[exchange_id]:
            if handler in self._handlers[exchange_id][channel]:
                self._handlers[exchange_id][channel].remove(handler)

    def _create_proxy_handler(self, exchange_id: str, channel: str) -> MessageHandler:
        """Create a proxy handler that routes to registered handlers."""

        async def proxy_handler(symbol: str, data: Any) -> None:
            """Proxy handler that calls registered handlers."""
            handlers = self._handlers.get(exchange_id, {}).get(channel, [])
            for handler in handlers:
                try:
                    await handler(exchange_id, symbol, data)
                except Exception as e:
                    logger.error(f"Error in {exchange_id}/{channel} handler: {e}")

        return proxy_handler

    async def connect_all(self) -> None:
        """Connect all feeds."""
        tasks = [feed.connect() for feed in self._feeds.values()]
        await asyncio.gather(*tasks, return_exceptions=True)

    async def disconnect_all(self) -> None:
        """Disconnect all feeds."""
        tasks = [feed.disconnect() for feed in self._feeds.values()]
        await asyncio.gather(*tasks, return_exceptions=True)

    async def subscribe_kline(
        self, exchange_id: str, symbol: str, interval: str | Interval
    ) -> None:
        """Subscribe to kline updates on a specific exchange."""
        feed = self._feeds.get(exchange_id)
        if feed:
            await feed.subscribe_kline(symbol, interval)

    async def subscribe_kline_all(self, symbol: str, interval: str | Interval) -> None:
        """Subscribe to kline updates on all exchanges."""
        tasks = [
            feed.subscribe_kline(symbol, interval)
            for feed in self._feeds.values()
        ]
        await asyncio.gather(*tasks, return_exceptions=True)

    async def subscribe_orderbook(self, exchange_id: str, symbol: str, depth: int = 20) -> None:
        """Subscribe to order book updates on a specific exchange."""
        feed = self._feeds.get(exchange_id)
        if feed:
            await feed.subscribe_orderbook(symbol, depth)

    async def subscribe_orderbook_all(self, symbol: str, depth: int = 20) -> None:
        """Subscribe to order book updates on all exchanges."""
        tasks = [
            feed.subscribe_orderbook(symbol, depth)
            for feed in self._feeds.values()
        ]
        await asyncio.gather(*tasks, return_exceptions=True)

    async def subscribe_trades(self, exchange_id: str, symbol: str) -> None:
        """Subscribe to trade stream on a specific exchange."""
        feed = self._feeds.get(exchange_id)
        if feed:
            await feed.subscribe_trades(symbol)

    async def subscribe_trades_all(self, symbol: str) -> None:
        """Subscribe to trade stream on all exchanges."""
        tasks = [feed.subscribe_trades(symbol) for feed in self._feeds.values()]
        await asyncio.gather(*tasks, return_exceptions=True)

    def get_orderbook(self, exchange_id: str, symbol: str) -> Optional[OrderBook]:
        """Get order book for a specific exchange and symbol."""
        feed = self._feeds.get(exchange_id)
        return feed.get_orderbook(symbol) if feed else None

    def get_ticker(self, exchange_id: str, symbol: str) -> Optional[Ticker]:
        """Get ticker for a specific exchange and symbol."""
        feed = self._feeds.get(exchange_id)
        return feed.get_ticker(symbol) if feed else None


# Global feed manager instance
_feed_manager: Optional[WebSocketFeedManager] = None


def get_feed_manager() -> WebSocketFeedManager:
    """Get or create the global feed manager instance."""
    global _feed_manager
    if _feed_manager is None:
        _feed_manager = WebSocketFeedManager()
    return _feed_manager


def create_binance_feed(futures: bool = False) -> BinanceWebSocketFeed:
    """Create a Binance WebSocket feed.

    Args:
        futures: Use futures market instead of spot

    Returns:
        BinanceWebSocketFeed instance
    """
    return BinanceWebSocketFeed(futures=futures)
