"""Dashboard WebSocket integration for real-time market data.

This module integrates WebSocket market data feeds with the dashboard,
providing real-time price updates, order books, and trade streams.
"""

from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Set

from loguru import logger

from openclaw.data.websocket_feed import (
    BinanceWebSocketFeed,
    KlineData,
    OrderBook,
    Trade,
    WebSocketFeedManager,
)
from openclaw.exchange.models import Ticker


@dataclass
class MarketDataState:
    """Market data state for a symbol."""

    symbol: str
    exchange_id: str
    last_price: float = 0.0
    price_change_24h: float = 0.0
    price_change_pct_24h: float = 0.0
    high_24h: float = 0.0
    low_24h: float = 0.0
    volume_24h: float = 0.0
    bid: float = 0.0
    ask: float = 0.0
    orderbook: Optional[OrderBook] = None
    recent_trades: List[Trade] = field(default_factory=list)
    klines: Dict[str, List[KlineData]] = field(default_factory=dict)
    last_update: datetime = field(default_factory=datetime.now)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for WebSocket transmission."""
        return {
            "symbol": self.symbol,
            "exchange_id": self.exchange_id,
            "last_price": self.last_price,
            "price_change_24h": self.price_change_24h,
            "price_change_pct_24h": self.price_change_pct_24h,
            "high_24h": self.high_24h,
            "low_24h": self.low_24h,
            "volume_24h": self.volume_24h,
            "bid": self.bid,
            "ask": self.ask,
            "spread": self.ask - self.bid if self.bid and self.ask else 0.0,
            "orderbook": self._orderbook_to_dict() if self.orderbook else None,
            "recent_trades": self._trades_to_dict(),
            "last_update": self.last_update.isoformat(),
        }

    def _orderbook_to_dict(self) -> Dict[str, Any]:
        """Convert orderbook to dictionary."""
        if not self.orderbook:
            return {}
        return {
            "bids": [[level.price, level.amount] for level in self.orderbook.bids[:10]],
            "asks": [[level.price, level.amount] for level in self.orderbook.asks[:10]],
            "best_bid": self.orderbook.best_bid.price if self.orderbook.best_bid else 0.0,
            "best_ask": self.orderbook.best_ask.price if self.orderbook.best_ask else 0.0,
            "spread": self.orderbook.spread,
            "mid_price": self.orderbook.mid_price,
        }

    def _trades_to_dict(self) -> List[Dict[str, Any]]:
        """Convert recent trades to dictionary."""
        return [
            {
                "trade_id": t.trade_id,
                "price": t.price,
                "amount": t.amount,
                "side": t.side,
                "timestamp": t.timestamp.isoformat(),
            }
            for t in self.recent_trades[-20:]  # Last 20 trades
        ]


class DashboardMarketDataManager:
    """Manages real-time market data for the dashboard.

    Provides unified access to market data from multiple exchanges,
    with automatic updates to connected WebSocket clients.
    """

    def __init__(self) -> None:
        """Initialize the market data manager."""
        self._feed_manager = WebSocketFeedManager()
        self._market_data: Dict[str, MarketDataState] = {}  # symbol -> state
        self._subscribed_symbols: Set[str] = set()
        self._update_callbacks: List[callable] = []
        self._lock = asyncio.Lock()
        self._running = False
        self._broadcast_task: Optional[asyncio.Task] = None

    @property
    def feed_manager(self) -> WebSocketFeedManager:
        """Get the underlying feed manager."""
        return self._feed_manager

    def add_exchange(self, exchange_id: str, feed: BinanceWebSocketFeed) -> None:
        """Add an exchange feed to the manager.

        Args:
            exchange_id: Exchange identifier
            feed: WebSocket feed instance
        """
        self._feed_manager.add_feed(feed)

        # Register handlers for this exchange
        self._feed_manager.add_handler(exchange_id, "ticker", self._on_ticker)
        self._feed_manager.add_handler(exchange_id, "orderbook", self._on_orderbook)
        self._feed_manager.add_handler(exchange_id, "trade", self._on_trade)
        self._feed_manager.add_handler(exchange_id, "kline", self._on_kline)

        logger.info(f"Added exchange feed: {exchange_id}")

    def remove_exchange(self, exchange_id: str) -> None:
        """Remove an exchange feed."""
        self._feed_manager.remove_feed(exchange_id)
        logger.info(f"Removed exchange feed: {exchange_id}")

    async def start(self) -> None:
        """Start the market data manager."""
        if self._running:
            return

        self._running = True
        await self._feed_manager.connect_all()

        # Start background broadcast task
        self._broadcast_task = asyncio.create_task(self._broadcast_loop())

        logger.info("Market data manager started")

    async def stop(self) -> None:
        """Stop the market data manager."""
        if not self._running:
            return

        self._running = False

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

        await self._feed_manager.disconnect_all()
        logger.info("Market data manager stopped")

    async def subscribe_symbol(
        self, symbol: str, exchange_id: str = "binance", intervals: List[str] = None
    ) -> None:
        """Subscribe to market data for a symbol.

        Args:
            symbol: Trading pair symbol (e.g., "BTC/USDT")
            exchange_id: Exchange identifier
            intervals: List of kline intervals to subscribe
        """
        if intervals is None:
            intervals = ["1m", "5m", "15m", "1h"]

        async with self._lock:
            self._subscribed_symbols.add(symbol)

            # Initialize market data state
            key = f"{exchange_id}:{symbol}"
            if key not in self._market_data:
                self._market_data[key] = MarketDataState(
                    symbol=symbol,
                    exchange_id=exchange_id,
                )

        # Subscribe to feeds
        try:
            # Subscribe to ticker
            await self._feed_manager.subscribe_orderbook(exchange_id, symbol, depth=20)

            # Subscribe to trades
            await self._feed_manager.subscribe_trades(exchange_id, symbol)

            # Subscribe to klines
            for interval in intervals:
                await self._feed_manager.subscribe_kline(exchange_id, symbol, interval)

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

        except Exception as e:
            logger.error(f"Failed to subscribe to {symbol}: {e}")
            raise

    async def unsubscribe_symbol(self, symbol: str, exchange_id: str = "binance") -> None:
        """Unsubscribe from market data for a symbol."""
        async with self._lock:
            self._subscribed_symbols.discard(symbol)
            key = f"{exchange_id}:{symbol}"
            self._market_data.pop(key, None)

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

    async def _on_ticker(self, exchange_id: str, symbol: str, ticker: Ticker) -> None:
        """Handle ticker update."""
        key = f"{exchange_id}:{symbol}"

        async with self._lock:
            if key not in self._market_data:
                self._market_data[key] = MarketDataState(
                    symbol=symbol,
                    exchange_id=exchange_id,
                )

            state = self._market_data[key]
            state.last_price = ticker.last
            state.bid = ticker.bid
            state.ask = ticker.ask
            state.high_24h = ticker.high or state.high_24h
            state.low_24h = ticker.low or state.low_24h
            state.volume_24h = ticker.volume or state.volume_24h
            state.last_update = datetime.now()

    async def _on_orderbook(
        self, exchange_id: str, symbol: str, orderbook: OrderBook
    ) -> None:
        """Handle order book update."""
        key = f"{exchange_id}:{symbol}"

        async with self._lock:
            if key not in self._market_data:
                self._market_data[key] = MarketDataState(
                    symbol=symbol,
                    exchange_id=exchange_id,
                )

            state = self._market_data[key]
            state.orderbook = orderbook
            if orderbook.best_bid:
                state.bid = orderbook.best_bid.price
            if orderbook.best_ask:
                state.ask = orderbook.best_ask.price
            state.last_update = datetime.now()

    async def _on_trade(self, exchange_id: str, symbol: str, trade: Trade) -> None:
        """Handle trade update."""
        key = f"{exchange_id}:{symbol}"

        async with self._lock:
            if key not in self._market_data:
                self._market_data[key] = MarketDataState(
                    symbol=symbol,
                    exchange_id=exchange_id,
                )

            state = self._market_data[key]
            state.last_price = trade.price
            state.recent_trades.append(trade)

            # Keep only last 100 trades
            if len(state.recent_trades) > 100:
                state.recent_trades = state.recent_trades[-100:]

            state.last_update = datetime.now()

    async def _on_kline(
        self, exchange_id: str, symbol: str, kline: KlineData
    ) -> None:
        """Handle kline update."""
        key = f"{exchange_id}:{symbol}"

        async with self._lock:
            if key not in self._market_data:
                self._market_data[key] = MarketDataState(
                    symbol=symbol,
                    exchange_id=exchange_id,
                )

            state = self._market_data[key]
            interval = kline.interval

            if interval not in state.klines:
                state.klines[interval] = []

            # Update or append kline
            klines = state.klines[interval]
            updated = False

            for i, existing in enumerate(klines):
                if existing.timestamp == kline.timestamp:
                    klines[i] = kline
                    updated = True
                    break

            if not updated:
                klines.append(kline)

            # Keep only last 500 klines per interval
            if len(klines) > 500:
                state.klines[interval] = klines[-500:]

            state.last_update = datetime.now()

    async def _broadcast_loop(self) -> None:
        """Background loop to broadcast updates to clients."""
        while self._running:
            try:
                await asyncio.sleep(1)  # Broadcast every second

                # Get current market data snapshot
                data = await self.get_market_data_snapshot()

                # Notify callbacks
                for callback in self._update_callbacks:
                    try:
                        await callback(data)
                    except Exception as e:
                        logger.error(f"Error in broadcast callback: {e}")

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in broadcast loop: {e}")

    async def get_market_data_snapshot(self) -> Dict[str, Any]:
        """Get current market data snapshot for all subscribed symbols."""
        async with self._lock:
            return {
                key: state.to_dict()
                for key, state in self._market_data.items()
            }

    def get_symbol_data(
        self, symbol: str, exchange_id: str = "binance"
    ) -> Optional[MarketDataState]:
        """Get market data for a specific symbol."""
        key = f"{exchange_id}:{symbol}"
        return self._market_data.get(key)

    def add_update_callback(self, callback: callable) -> None:
        """Add a callback for market data updates.

        Args:
            callback: Async function that receives market data updates
        """
        self._update_callbacks.append(callback)

    def remove_update_callback(self, callback: callable) -> None:
        """Remove an update callback."""
        if callback in self._update_callbacks:
            self._update_callbacks.remove(callback)

    def list_subscribed_symbols(self) -> List[str]:
        """List all subscribed symbols."""
        return list(self._subscribed_symbols)


# Global market data manager instance
_market_data_manager: Optional[DashboardMarketDataManager] = None


def get_market_data_manager() -> DashboardMarketDataManager:
    """Get or create the global market data manager."""
    global _market_data_manager
    if _market_data_manager is None:
        _market_data_manager = DashboardMarketDataManager()
    return _market_data_manager


async def setup_default_market_data() -> DashboardMarketDataManager:
    """Set up default market data feeds with common symbols.

    Returns:
        Configured DashboardMarketDataManager instance
    """
    manager = get_market_data_manager()

    # Add Binance spot feed
    binance_spot = BinanceWebSocketFeed(futures=False)
    manager.add_exchange("binance", binance_spot)

    # Add Binance futures feed
    binance_futures = BinanceWebSocketFeed(futures=True)
    manager.add_exchange("binance_futures", binance_futures)

    # Start the manager
    await manager.start()

    # Subscribe to common symbols
    default_symbols = [
        "BTC/USDT",
        "ETH/USDT",
        "BNB/USDT",
        "SOL/USDT",
        "XRP/USDT",
    ]

    for symbol in default_symbols:
        try:
            await manager.subscribe_symbol(symbol, exchange_id="binance")
        except Exception as e:
            logger.warning(f"Failed to subscribe to {symbol}: {e}")

    return manager
