"""CCXT-based pricing provider with multi-exchange support and WebSocket capabilities."""

import ccxt
import threading
import time
from decimal import Decimal
from typing import Dict, List, Optional, Any, Callable
from datetime import datetime
from .base_provider import BasePricingProvider
from src.core.logger import get_logger

logger = get_logger(__name__)


class CCXTProvider(BasePricingProvider):
    """CCXT-based pricing provider with multi-exchange fallback support.
    
    This provider uses CCXT to connect to multiple exchanges (Kraken, Coinbase, Binance)
    as primary data sources. It supports WebSocket where available and falls back to
    polling if WebSocket is not available.
    """
    
    def __init__(self, exchange_name: Optional[str] = None):
        """Initialize CCXT provider.
        
        Args:
            exchange_name: Specific exchange to use ('kraken', 'coinbase', 'binance'),
                          or None to try multiple exchanges
        """
        super().__init__()
        self.exchange_name = exchange_name
        self.exchange = None
        self._selected_exchange_id = None
        self._polling_threads: Dict[str, threading.Thread] = {}
        self._stop_polling: Dict[str, bool] = {}
        
        # Exchange priority order (try first to last)
        if exchange_name:
            self._exchange_options = [(exchange_name.lower(), exchange_name.capitalize())]
        else:
            self._exchange_options = [
                ('kraken', 'Kraken'),
                ('coinbase', 'Coinbase'),
                ('binance', 'Binance'),
            ]
    
    @property
    def name(self) -> str:
        """Provider name."""
        if self._selected_exchange_id:
            return f"CCXT-{self._selected_exchange_id.capitalize()}"
        return "CCXT Provider"
    
    @property
    def supports_websocket(self) -> bool:
        """Check if current exchange supports WebSocket."""
        if not self.exchange:
            return False
        
        # Check if exchange has WebSocket support
        # Most modern CCXT exchanges support WebSocket, but we check explicitly
        exchange_id = getattr(self.exchange, 'id', '').lower()
        
        # Known WebSocket-capable exchanges
        ws_capable = ['kraken', 'coinbase', 'binance', 'binanceus', 'okx']
        
        return exchange_id in ws_capable
    
    def connect(self) -> bool:
        """Connect to an exchange via CCXT.
        
        Tries multiple exchanges in order until one succeeds.
        
        Returns:
            True if connection successful
        """
        for exchange_id, exchange_display_name in self._exchange_options:
            try:
                # Get exchange class from CCXT
                exchange_class = getattr(ccxt, exchange_id, None)
                if not exchange_class:
                    logger.warning(f"Exchange {exchange_id} not found in CCXT")
                    continue
                
                # Create exchange instance
                self.exchange = exchange_class({
                    'enableRateLimit': True,
                    'options': {
                        'defaultType': 'spot',
                    }
                })
                
                # Load markets to test connection
                if not hasattr(self.exchange, 'markets') or not self.exchange.markets:
                    try:
                        self.exchange.load_markets()
                    except Exception as e:
                        logger.warning(f"Failed to load markets for {exchange_id}: {e}")
                        continue
                
                # Test connection with a common symbol
                test_symbols = ['BTC/USDT', 'BTC/USD', 'BTC/EUR']
                ticker_result = None
                
                for test_symbol in test_symbols:
                    try:
                        ticker_result = self.exchange.fetch_ticker(test_symbol)
                        if ticker_result:
                            break
                    except Exception:
                        continue
                
                if not ticker_result:
                    logger.warning(f"Could not fetch ticker from {exchange_id}")
                    continue
                
                # Success!
                self._selected_exchange_id = exchange_id
                self._connected = True
                logger.info(f"Connected to {exchange_display_name} via CCXT")
                return True
                
            except Exception as e:
                logger.warning(f"Failed to connect to {exchange_id}: {e}")
                continue
        
        # All exchanges failed
        logger.error("Failed to connect to any CCXT exchange")
        self._connected = False
        self.exchange = None
        return False
    
    def disconnect(self):
        """Disconnect from exchange."""
        # Stop all polling threads
        for symbol in list(self._stop_polling.keys()):
            self._stop_polling[symbol] = True
        
        # Wait a bit for threads to stop
        for symbol, thread in self._polling_threads.items():
            if thread.is_alive():
                thread.join(timeout=2.0)
        
        self._polling_threads.clear()
        self._stop_polling.clear()
        self._subscribers.clear()
        self._connected = False
        self.exchange = None
        self._selected_exchange_id = None
        logger.info(f"Disconnected from {self.name}")
    
    def get_ticker(self, symbol: str) -> Dict[str, Any]:
        """Get current ticker data."""
        if not self._connected or not self.exchange:
            logger.error("Provider not connected")
            return {}
        
        try:
            normalized_symbol = self.normalize_symbol(symbol)
            ticker = self.exchange.fetch_ticker(normalized_symbol)
            
            return {
                'symbol': symbol,
                'bid': Decimal(str(ticker.get('bid', 0) or 0)),
                'ask': Decimal(str(ticker.get('ask', 0) or 0)),
                'last': Decimal(str(ticker.get('last', 0) or ticker.get('close', 0) or 0)),
                'high': Decimal(str(ticker.get('high', 0) or 0)),
                'low': Decimal(str(ticker.get('low', 0) or 0)),
                'volume': Decimal(str(ticker.get('quoteVolume', ticker.get('volume', 0)) or 0)),
                'timestamp': ticker.get('timestamp', int(time.time() * 1000)),
            }
        except Exception as e:
            logger.error(f"Failed to get ticker for {symbol}: {e}")
            return {}
    
    def get_ohlcv(
        self,
        symbol: str,
        timeframe: str = '1h',
        since: Optional[datetime] = None,
        limit: int = 100
    ) -> List[List]:
        """Get OHLCV candlestick data."""
        if not self._connected or not self.exchange:
            logger.error("Provider not connected")
            return []
        
        try:
            since_timestamp = int(since.timestamp() * 1000) if since else None
            normalized_symbol = self.normalize_symbol(symbol)
            
            # Most exchanges support up to 1000 candles per request
            max_limit = min(limit, 1000)
            
            ohlcv = self.exchange.fetch_ohlcv(
                normalized_symbol,
                timeframe,
                since_timestamp,
                max_limit
            )
            
            logger.debug(f"Fetched {len(ohlcv)} candles for {symbol} ({timeframe})")
            return ohlcv
            
        except Exception as e:
            logger.error(f"Failed to get OHLCV for {symbol}: {e}")
            return []
    
    def subscribe_ticker(self, symbol: str, callback: Callable) -> bool:
        """Subscribe to ticker updates.
        
        Uses WebSocket if available, otherwise falls back to polling.
        """
        if not self._connected or not self.exchange:
            logger.error("Provider not connected")
            return False
        
        key = f"ticker_{symbol}"
        
        # Add callback to subscribers
        if key not in self._subscribers:
            self._subscribers[key] = []
        if callback not in self._subscribers[key]:
            self._subscribers[key].append(callback)
        
        # Try WebSocket first if supported
        if self.supports_websocket:
            try:
                # CCXT WebSocket support varies by exchange
                # For now, use polling as fallback since WebSocket implementation
                # in CCXT requires exchange-specific handling
                # TODO: Implement native WebSocket when CCXT adds better support
                pass
            except Exception as e:
                logger.warning(f"WebSocket subscription failed, falling back to polling: {e}")
        
        # Use polling as primary method (more reliable across exchanges)
        if key not in self._polling_threads or not self._polling_threads[key].is_alive():
            self._stop_polling[key] = False
            
            def poll_ticker():
                """Poll ticker every 2 seconds."""
                while not self._stop_polling.get(key, False):
                    try:
                        ticker_data = self.get_ticker(symbol)
                        if ticker_data and 'last' in ticker_data:
                            # Call all callbacks for this symbol
                            for cb in self._subscribers.get(key, []):
                                try:
                                    cb({
                                        'symbol': symbol,
                                        'price': ticker_data['last'],
                                        'bid': ticker_data.get('bid', 0),
                                        'ask': ticker_data.get('ask', 0),
                                        'volume': ticker_data.get('volume', 0),
                                        'timestamp': ticker_data.get('timestamp'),
                                    })
                                except Exception as e:
                                    logger.error(f"Callback error for {symbol}: {e}")
                        
                        time.sleep(2)  # Poll every 2 seconds
                    except Exception as e:
                        logger.error(f"Ticker polling error for {symbol}: {e}")
                        time.sleep(5)  # Wait longer on error
            
            thread = threading.Thread(target=poll_ticker, daemon=True)
            thread.start()
            self._polling_threads[key] = thread
            logger.info(f"Subscribed to ticker updates for {symbol} (polling mode)")
            return True
        
        return True
    
    def unsubscribe_ticker(self, symbol: str, callback: Optional[Callable] = None):
        """Unsubscribe from ticker updates."""
        key = f"ticker_{symbol}"
        
        # Stop polling thread
        if key in self._stop_polling:
            self._stop_polling[key] = True
        
        # Remove from subscribers
        super().unsubscribe_ticker(symbol, callback)
        
        # Clean up thread reference
        if key in self._polling_threads:
            del self._polling_threads[key]
        
        logger.info(f"Unsubscribed from ticker updates for {symbol}")
    
    def normalize_symbol(self, symbol: str) -> str:
        """Normalize symbol for the selected exchange."""
        if not self.exchange:
            return symbol.replace('-', '/').upper()
        
        # Basic normalization
        normalized = symbol.replace('-', '/').upper()
        
        # Try to use exchange's markets to find correct symbol
        try:
            if hasattr(self.exchange, 'markets') and self.exchange.markets:
                # Check if symbol exists
                if normalized in self.exchange.markets:
                    return normalized
                
                # Try alternative formats
                if '/USD' in normalized:
                    alt_symbol = normalized.replace('/USD', '/USDT')
                    if alt_symbol in self.exchange.markets:
                        return alt_symbol
                
                # For Kraken: try XBT instead of BTC
                if normalized.startswith('BTC/'):
                    alt_symbol = normalized.replace('BTC/', 'XBT/')
                    if alt_symbol in self.exchange.markets:
                        return alt_symbol
        except Exception:
            pass
        
        # Fallback: return normalized (let CCXT handle errors)
        return normalized
    
    def get_supported_symbols(self) -> List[str]:
        """Get list of supported trading symbols."""
        if not self._connected or not self.exchange:
            return []
        
        try:
            markets = self.exchange.load_markets()
            return list(markets.keys())
        except Exception as e:
            logger.error(f"Failed to get supported symbols: {e}")
            return []
