"""Public Data Exchange Adapter - Free market data without API keys.

Uses CCXT in public mode to fetch market data from Binance (or other exchanges)
without requiring API keys. Perfect for testing, backtesting, and paper trading.
"""

import ccxt.async_support as ccxt
from decimal import Decimal
from typing import Dict, List, Optional, Any, Callable
from datetime import datetime
from .base import BaseExchangeAdapter, Order
from src.core.logger import get_logger

logger = get_logger(__name__)


class PublicDataAdapter(BaseExchangeAdapter):
    """Public market data adapter using CCXT in public mode (no API keys needed).
    
    This adapter uses Binance's public API to fetch:
    - Historical OHLCV data
    - Real-time ticker data
    - Order book data
    - Trade data
    
    No API keys required - perfect for testing and paper trading.
    """
    
    @property
    def name(self) -> str:
        """Exchange name."""
        return self._selected_exchange_name or "Public Data"
    
    def __init__(
        self, 
        api_key: str = "", 
        api_secret: str = "", 
        sandbox: bool = False, 
        read_only: bool = True
    ):
        """Initialize Public Data adapter.
        
        Args:
            api_key: Ignored (public data doesn't need API keys)
            api_secret: Ignored (public data doesn't need API keys)
            sandbox: Ignored (Binance public API is always live data)
            read_only: Always True (this adapter is read-only by design)
        """
        super().__init__("", "", False, True)  # Always read-only, no keys needed
        
        # List of exchanges to try as fallbacks (in order of preference)
        # These exchanges have good public API access without geographic restrictions
        self._exchange_options = [
            ('kraken', 'Kraken Public'),
            ('coinbase', 'Coinbase Public'),
            ('binance', 'Binance Public'),  # Try Binance last since it has geo restrictions
        ]
        
        self.exchange = None
        self._selected_exchange_name = None
        
        # WebSocket connections for real-time data
        self._ws_callbacks = {}
        self._polling_timer = None
    
    async def connect(self) -> bool:
        """Connect to exchange (test public API access).
        
        Tries multiple exchanges as fallbacks if one is blocked.
        
        Returns:
            True if connection successful (public API is always available)
        """
        # Try each exchange until one works
        for exchange_id, exchange_display_name in self._exchange_options:
            try:
                # Create exchange instance
                exchange_class = getattr(ccxt, exchange_id, None)
                if not exchange_class:
                    continue
                
                self.exchange = exchange_class({
                    'enableRateLimit': True,
                    'options': {
                        'defaultType': 'spot',
                    }
                })
                
                # Load markets if not already loaded
                if not hasattr(self.exchange, 'markets') or not self.exchange.markets:
                    try:
                        await self.exchange.load_markets()
                    except Exception as e:
                        raise
                
                # Test connection with a common symbol
                # Different exchanges use different symbol formats
                test_symbols = ['BTC/USDT', 'BTC/USD', 'BTC/EUR']
                ticker_result = None
                
                for test_symbol in test_symbols:
                    try:
                        ticker_result = await self.exchange.fetch_ticker(test_symbol)
                        break
                    except Exception as e:
                        continue
                
                if not ticker_result:
                    raise Exception("Could not fetch ticker from any test symbol")
                
                # Success! Use this exchange
                self._selected_exchange_name = exchange_display_name
                self._connected = True
                logger.info(f"Connected to {self.name} (public data, no API keys needed)")
                
                return True
                
            except Exception as e:
                logger.warning(f"Failed to connect to {exchange_id}: {e}, trying next exchange...")
                continue
        
        # All exchanges failed
        logger.error("Failed to connect to any public exchange")
        self._connected = False
        return False
    
    async def disconnect(self):
        """Disconnect from exchange."""
        self._ws_callbacks.clear()
        if self._polling_timer:
            self._polling_timer.cancel()
            self._polling_timer = None
        
        if self.exchange:
            await self.exchange.close()
            
        self._connected = False
        logger.info(f"Disconnected from {self.name}")
    
    async def get_balance(self, currency: Optional[str] = None) -> Dict[str, Decimal]:
        """Get account balance.
        
        Note: Public data adapter cannot access account balances.
        Returns empty dict. Use paper trading balance instead.
        """
        logger.warning("Public data adapter cannot access account balances")
        return {}
    
    async def get_ticker(self, symbol: str) -> Dict[str, Any]:
        """Get current ticker (public endpoint)."""
        try:
            ticker = await self.exchange.fetch_ticker(self.normalize_symbol(symbol))
            return {
                'symbol': symbol,
                'bid': Decimal(str(ticker.get('bid', 0))),
                'ask': Decimal(str(ticker.get('ask', 0))),
                'last': Decimal(str(ticker.get('last', 0))),
                'high': Decimal(str(ticker.get('high', 0))),
                'low': Decimal(str(ticker.get('low', 0))),
                'volume': Decimal(str(ticker.get('quoteVolume', 0))),
                'timestamp': ticker.get('timestamp'),
            }
        except Exception as e:
            logger.error(f"Failed to get ticker for {symbol}: {e}")
            return {}
    
    async def get_orderbook(self, symbol: str, limit: int = 20) -> Dict[str, List]:
        """Get order book (public endpoint)."""
        try:
            orderbook = await self.exchange.fetch_order_book(self.normalize_symbol(symbol), limit)
            return {
                'bids': [[Decimal(str(b[0])), Decimal(str(b[1]))] for b in orderbook['bids']],
                'asks': [[Decimal(str(a[0])), Decimal(str(a[1]))] for a in orderbook['asks']],
            }
        except Exception as e:
            logger.error(f"Failed to get orderbook for {symbol}: {e}")
            return {'bids': [], 'asks': []}
    
    async def place_order(self, order: Order) -> Dict[str, Any]:
        """Place an order.
        
        Note: Public data adapter cannot place orders.
        Returns error message. Use paper trading instead.
        """
        logger.warning("Public data adapter cannot place orders - use paper trading")
        return {'error': 'Public data adapter is read-only. Use paper trading for order execution.'}
    
    async def cancel_order(self, order_id: str, symbol: str) -> bool:
        """Cancel an order.
        
        Note: Public data adapter cannot cancel orders.
        """
        logger.warning("Public data adapter cannot cancel orders")
        return False
    
    async def get_order_status(self, order_id: str, symbol: str) -> Dict[str, Any]:
        """Get order status.
        
        Note: Public data adapter cannot access order status.
        """
        logger.warning("Public data adapter cannot access order status")
        return {}
    
    async def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
        """Get open orders.
        
        Note: Public data adapter cannot access orders.
        """
        logger.warning("Public data adapter cannot access orders")
        return []
    
    async def get_positions(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
        """Get open positions.
        
        Note: Public data adapter cannot access positions.
        """
        logger.warning("Public data adapter cannot access positions")
        return []
    
    async def get_ohlcv(
        self,
        symbol: str,
        timeframe: str = '1h',
        since: Optional[datetime] = None,
        limit: int = 100
    ) -> List[List]:
        """Get OHLCV data (public endpoint).
        
        This is the main method for fetching historical data.
        Most exchanges support up to 1000 candles per request.
        """
        try:
            since_timestamp = int(since.timestamp() * 1000) if since else None
            
            # Most exchanges support up to 1000 candles per request
            # If limit > 1000, we'll need to make multiple requests
            max_limit = min(limit, 1000)
            
            normalized_symbol = self.normalize_symbol(symbol)
            
            ohlcv = await self.exchange.fetch_ohlcv(
                normalized_symbol,
                timeframe,
                since_timestamp,
                max_limit
            )
            
            logger.info(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):
        """Subscribe to ticker updates (polling-based for public data).
        
        Since we don't have WebSocket auth, we poll the ticker endpoint.
        """
        try:
            import threading
            import time
            
            normalized_symbol = self.normalize_symbol(symbol)
            self._ws_callbacks[f'ticker_{symbol}'] = callback
            
            def poll_ticker():
                """Poll ticker every 2 seconds."""
                while f'ticker_{symbol}' in self._ws_callbacks:
                    try:
                        ticker = self.get_ticker(symbol)
                        if ticker and callback:
                            callback({
                                'price': ticker.get('last', 0),
                                'bid': ticker.get('bid', 0),
                                'ask': ticker.get('ask', 0),
                                'volume': ticker.get('volume', 0),
                                'timestamp': ticker.get('timestamp'),
                            })
                        time.sleep(2)  # Poll every 2 seconds
                    except Exception as e:
                        logger.error(f"Ticker polling error: {e}")
                        time.sleep(5)  # Wait longer on error
            
            # Start polling thread
            thread = threading.Thread(target=poll_ticker, daemon=True)
            thread.start()
            
            logger.info(f"Subscribed to ticker updates for {symbol} (polling mode)")
        except Exception as e:
            logger.error(f"Failed to subscribe to ticker: {e}")
            self._ws_callbacks[f'ticker_{symbol}'] = callback
    
    def subscribe_orderbook(self, symbol: str, callback: Callable):
        """Subscribe to order book updates (polling-based)."""
        try:
            import threading
            import time
            
            normalized_symbol = self.normalize_symbol(symbol)
            self._ws_callbacks[f'orderbook_{symbol}'] = callback
            
            def poll_orderbook():
                """Poll order book every 1 second."""
                while f'orderbook_{symbol}' in self._ws_callbacks:
                    try:
                        orderbook = self.get_orderbook(symbol, limit=20)
                        if orderbook and callback:
                            callback(orderbook)
                        time.sleep(1)  # Poll every second
                    except Exception as e:
                        logger.error(f"Orderbook polling error: {e}")
                        time.sleep(5)
            
            thread = threading.Thread(target=poll_orderbook, daemon=True)
            thread.start()
            
            logger.info(f"Subscribed to orderbook updates for {symbol} (polling mode)")
        except Exception as e:
            logger.error(f"Failed to subscribe to orderbook: {e}")
            self._ws_callbacks[f'orderbook_{symbol}'] = callback
    
    def subscribe_trades(self, symbol: str, callback: Callable):
        """Subscribe to trade updates (polling-based)."""
        try:
            import threading
            import time
            
            normalized_symbol = self.normalize_symbol(symbol)
            self._ws_callbacks[f'trades_{symbol}'] = callback
            
            def poll_trades():
                """Poll recent trades every 1 second."""
                while f'trades_{symbol}' in self._ws_callbacks:
                    try:
                        trades = self.exchange.fetch_trades(normalized_symbol, limit=10)
                        if trades and callback:
                            for trade in trades[-5:]:  # Last 5 trades
                                callback({
                                    'price': trade.get('price', 0),
                                    'amount': trade.get('amount', 0),
                                    'side': trade.get('side', 'buy'),
                                    'timestamp': trade.get('timestamp'),
                                })
                        time.sleep(1)
                    except Exception as e:
                        logger.error(f"Trades polling error: {e}")
                        time.sleep(5)
            
            thread = threading.Thread(target=poll_trades, daemon=True)
            thread.start()
            
            logger.info(f"Subscribed to trades updates for {symbol} (polling mode)")
        except Exception as e:
            logger.error(f"Failed to subscribe to trades: {e}")
            self._ws_callbacks[f'trades_{symbol}'] = callback
    
    def normalize_symbol(self, symbol: str) -> str:
        """Normalize symbol for the selected exchange.
        
        Different exchanges use different symbol formats:
        - Binance/Kraken: BTC/USDT, BTC/USD
        - Some exchanges: BTC-USDT, BTC-USD
        - Kraken sometimes uses: XBT/USD instead of BTC/USD
        """
        if not self.exchange:
            return symbol.replace('-', '/').upper()
        
        # Basic normalization: convert dashes to slashes, uppercase
        normalized = symbol.replace('-', '/').upper()
        
        # Try to use exchange's built-in symbol normalization
        try:
            if hasattr(self.exchange, 'markets') and self.exchange.markets:
                # Check if normalized symbol exists in markets
                if normalized in self.exchange.markets:
                    return normalized
                
                # Try alternative formats
                # For USD pairs, try USDT (common on many exchanges)
                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
                
                # Try to find similar symbol (fuzzy match)
                base = normalized.split('/')[0] if '/' in normalized else normalized
                quote = normalized.split('/')[1] if '/' in normalized else 'USD'
                
                # Search for matching symbols
                for market_symbol in self.exchange.markets.keys():
                    if market_symbol.startswith(base + '/') or market_symbol.endswith('/' + quote):
                        return market_symbol
        except Exception as e:
            pass
        
        # Fallback: return normalized symbol (let CCXT handle errors)
        return normalized
    
    def get_fee_structure(self) -> Dict[str, float]:
        """Get fee structure for the selected exchange."""
        # Default fees (approximate spot trading fees)
        if not self.exchange or not hasattr(self.exchange, 'id'):
            return {'maker': 0.001, 'taker': 0.001}
        
        exchange_id = self.exchange.id if hasattr(self.exchange, 'id') else None
        
        # Exchange-specific fees (approximate)
        fee_map = {
            'binance': {'maker': 0.001, 'taker': 0.001},  # 0.1%
            'kraken': {'maker': 0.0016, 'taker': 0.0026},  # 0.16% / 0.26%
            'coinbase': {'maker': 0.004, 'taker': 0.006},  # 0.4% / 0.6%
        }
        
        return fee_map.get(exchange_id, {'maker': 0.001, 'taker': 0.001})
    
    def get_available_symbols(self) -> List[str]:
        """Get list of available trading symbols.
        
        Returns:
            List of available symbol pairs (e.g., ['BTC/USDT', 'ETH/USDT', ...])
        """
        try:
            markets = self.exchange.load_markets()
            return list(markets.keys())
        except Exception as e:
            logger.error(f"Failed to get available symbols: {e}")
            return []
