"""Intelligent autopilot orchestrator with ML-based strategy selection."""

import asyncio
import json
from decimal import Decimal
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime, timedelta
import pandas as pd

from src.core.logger import get_logger
from src.core.database import get_database, OrderSide, OrderType
from src.core.config import get_config
from src.core.redis import get_redis_client
from .market_analyzer import MarketConditions, get_market_analyzer
from .strategy_selector import get_strategy_selector
from .performance_tracker import get_performance_tracker
from src.strategies.base import get_strategy_registry, BaseStrategy, StrategySignal, SignalType
from src.trading.engine import get_trading_engine
from src.risk.manager import get_risk_manager
from src.data.collector import get_data_collector

logger = get_logger(__name__)

# Strategies that require user configuration (excluded from default autopilot)
# These are only loaded if the user has configured them in the Strategies page
STRATEGIES_REQUIRING_CONFIG = {
    'pairs_trading',      # Requires second_symbol
    'grid',               # Requires grid_spacing, num_levels  
    'dca',                # Requires amount, interval
    'market_making',      # Requires spread_percent, max_inventory
}


class IntelligentAutopilot:
    """Intelligent autopilot with ML-based strategy selection and auto-execution."""
    
    def __init__(
        self,
        symbol: str,
        exchange_id: int = 1,
        timeframe: str = "1h",
        interval: float = 60.0,
        paper_trading: bool = True
    ):
        """Initialize intelligent autopilot.
        
        Args:
            symbol: Trading symbol (e.g., "BTC/USD")
            exchange_id: Exchange ID
            timeframe: Analysis timeframe
            interval: Analysis cycle interval in seconds
            paper_trading: Paper trading mode
        """
        self.symbol = symbol
        self.exchange_id = exchange_id
        self.timeframe = timeframe
        self.interval = interval
        self.paper_trading = paper_trading
        
        self.logger = get_logger(__name__)
        self.config = get_config()
        self.db = get_database()
        self.redis = get_redis_client()
        
        # Core components
        self.market_analyzer = get_market_analyzer()
        self.strategy_selector = get_strategy_selector()
        self.performance_tracker = get_performance_tracker()
        self.strategy_registry = get_strategy_registry()
        self.trading_engine = get_trading_engine()
        self.risk_manager = get_risk_manager()
        self.data_collector = get_data_collector()
        
        # Configuration
        self.min_confidence_threshold = self.config.get(
            "autopilot.intelligent.min_confidence_threshold",
            0.75
        )
        self.max_trades_per_day = self.config.get(
            "autopilot.intelligent.max_trades_per_day",
            10
        )
        self.min_profit_target = self.config.get(
            "autopilot.intelligent.min_profit_target",
            0.02  # 2% minimum expected profit
        )
        self.enable_auto_execution = self.config.get(
            "autopilot.intelligent.enable_auto_execution",
            True
        )
        
        # State
        self._local_running = False  # Local flag for the loop
        self._last_analysis: Optional[Dict[str, Any]] = None
        self._selected_strategy: Optional[str] = None
        self._strategy_instances: Dict[str, BaseStrategy] = {}
        self._ohlcv_data: Optional[pd.DataFrame] = None
        
        # Redis Keys
        safe_symbol = symbol.replace("/", "-")
        self.key_running = f"autopilot:running:{safe_symbol}:{timeframe}"
        self.key_trades = f"autopilot:trades:{safe_symbol}"
        
        # Initialize strategy instances
        self._initialize_strategies()
        
    @property
    def _trades_today_key(self) -> str:
        """Get Redis key for today's trade count."""
        today = datetime.utcnow().strftime("%Y-%m-%d")
        return f"{self.key_trades}:{today}"
    
    async def _fetch_market_data(self):
        """Fetch OHLCV data from src.database."""
        from sqlalchemy import select
        try:
            async with self.db.get_session() as session:
                from src.core.database import MarketData, Exchange
                
                # Get exchange name
                stmt = select(Exchange).filter_by(id=self.exchange_id).limit(1)
                result = await session.execute(stmt)
                exchange = result.scalar_one_or_none()
                exchange_name = exchange.name if exchange else "coinbase"
                
                # Fetch recent OHLCV data (last 200 candles for analysis)
                ohlcv_stmt = select(MarketData).filter_by(
                    exchange=exchange_name,
                    symbol=self.symbol,
                    timeframe=self.timeframe
                ).order_by(MarketData.timestamp.desc()).limit(200)
                
                ohlcv_result = await session.execute(ohlcv_stmt)
                market_data = ohlcv_result.scalars().all()
                
                if market_data:
                    # Convert to DataFrame
                    data = {
                        'timestamp': [md.timestamp for md in reversed(market_data)],
                        'open': [float(md.open) for md in reversed(market_data)],
                        'high': [float(md.high) for md in reversed(market_data)],
                        'low': [float(md.low) for md in reversed(market_data)],
                        'close': [float(md.close) for md in reversed(market_data)],
                        'volume': [float(md.volume) for md in reversed(market_data)],
                    }
                    self._ohlcv_data = pd.DataFrame(data)
                    self.logger.info(f"Loaded {len(self._ohlcv_data)} candles from src.database")
                else:
                    self.logger.warning("No market data found in database")
                    
        except Exception as e:
            self.logger.error(f"Error fetching market data: {e}")
    
    def _initialize_strategies(self):
        """Initialize strategy instances for strategies that work without configuration."""
        available_strategies = self.strategy_registry.list_available()
        
        for strategy_name in available_strategies:
            # Skip strategies that require user configuration
            if strategy_name.lower() in STRATEGIES_REQUIRING_CONFIG:
                self.logger.debug(f"Skipping {strategy_name} (requires configuration)")
                continue
                
            try:
                # Create instance with default parameters
                strategy_class = self.strategy_registry._strategies.get(strategy_name.lower())
                if strategy_class:
                    instance = strategy_class(
                        name=strategy_name,
                        parameters={},
                        timeframes=[self.timeframe]
                    )
                    instance.enabled = True
                    self._strategy_instances[strategy_name] = instance
                    self.logger.debug(f"Initialized strategy: {strategy_name}")
            except Exception as e:
                self.logger.warning(f"Failed to initialize strategy {strategy_name}: {e}")
    
    async def _load_user_configured_strategies(self):
        """Load user-configured strategies from database.
        
        This enables strategies that require configuration (like pairs_trading)
        when the user has set them up in the Strategies page.
        Also overrides default parameters if user has configured them.
        """
        from sqlalchemy import select
        from src.core.database import Strategy
        
        try:
            async with self.db.get_session() as session:
                # Get all enabled strategies from database
                stmt = select(Strategy).where(Strategy.enabled == True)
                result = await session.execute(stmt)
                user_strategies = result.scalars().all()
                
                for db_strategy in user_strategies:
                    strategy_type = db_strategy.strategy_type
                    if not strategy_type:
                        continue
                    
                    strategy_type_lower = strategy_type.lower()
                    
                    # Check if this strategy type is registered
                    strategy_class = self.strategy_registry._strategies.get(strategy_type_lower)
                    if not strategy_class:
                        self.logger.debug(f"Strategy type {strategy_type} not registered")
                        continue
                    
                    try:
                        # Create instance with user's parameters
                        instance = strategy_class(
                            name=db_strategy.name,
                            parameters=db_strategy.parameters or {},
                            timeframes=db_strategy.timeframes or [self.timeframe]
                        )
                        instance.enabled = True
                        
                        # Use strategy_type as key (overwrites default if exists)
                        self._strategy_instances[strategy_type_lower] = instance
                        
                        self.logger.info(
                            f"Loaded user-configured strategy: {db_strategy.name} "
                            f"(type: {strategy_type}, params: {db_strategy.parameters})"
                        )
                    except Exception as e:
                        self.logger.warning(
                            f"Failed to load user strategy {db_strategy.name}: {e}"
                        )
                        
        except Exception as e:
            self.logger.error(f"Error loading user-configured strategies: {e}")
    
    def update_market_data(self, ohlcv_data: pd.DataFrame):
        """Update OHLCV market data.
        
        Args:
            ohlcv_data: DataFrame with columns: open, high, low, close, volume
        """
        self._ohlcv_data = ohlcv_data.copy()
        self.logger.debug(f"Updated market data: {len(ohlcv_data)} candles")
    
    async def start(self):
        """Start the intelligent autopilot loop."""
        redis_client = self.redis.get_client()
        
        # Check if already running (distributed lock style)
        is_running = await redis_client.get(self.key_running)
        if is_running:
            self.logger.warning(f"Autopilot for {self.symbol} is already running elsewhere")
            return

        # Set running state in Redis
        await redis_client.set(self.key_running, "1")
        self._local_running = True
        
        self.logger.info(
            f"Intelligent autopilot started for {self.symbol} "
            f"(timeframe: {self.timeframe}, interval: {self.interval}s)"
        )
        
        # Initial data fetch
        await self._fetch_market_data()
        
        # Try to load or train model
        try:
            # Check if this task should be offloaded to Celery in future
            await self.strategy_selector.train_model(force_retrain=False)
        except Exception as e:
            self.logger.warning(f"Model training failed, will use fallback: {e}")
        
        # Load user-configured strategies from database
        await self._load_user_configured_strategies()
        
        self.logger.info(
            f"Autopilot has {len(self._strategy_instances)} strategies available: "
            f"{list(self._strategy_instances.keys())}"
        )
        
        try:
            while self._local_running:
                # Distributed check: verify key still exists
                if not await redis_client.exists(self.key_running):
                    self.logger.info("Remote stop signal received")
                    break
                
                try:
                    await self._analysis_cycle()
                except Exception as e:
                    self.logger.error(f"Analysis cycle error: {e}")
                
                await asyncio.sleep(self.interval)
        finally:
            self._local_running = False
            # Ensure key is deleted (cleanup)
            await redis_client.delete(self.key_running)
            self.logger.info("Intelligent autopilot stopped")
    
    async def _analysis_cycle(self):
        """Perform one analysis and execution cycle."""
        # Check daily trade limit from Redis
        trades_today = await self._get_trades_today()
        
        if trades_today >= self.max_trades_per_day:
            self.logger.debug(f"Daily trade limit reached: {trades_today}/{self.max_trades_per_day}")
            return
        
        # Get market data (refresh if needed)
        if self._ohlcv_data is None or len(self._ohlcv_data) < 50:
            await self._fetch_market_data()
            if self._ohlcv_data is None or len(self._ohlcv_data) < 50:
                self.logger.warning("Insufficient market data for analysis")
                return
        
        # Analyze market conditions
        market_conditions = self.market_analyzer.analyze_current_conditions(
            symbol=self.symbol,
            timeframe=self.timeframe,
            ohlcv_data=self._ohlcv_data
        )
        
        # Select best strategy using ML
        selection_result = self.strategy_selector.select_best_strategy(
            market_conditions,
            min_confidence=self.min_confidence_threshold
        )
        
        if selection_result is None:
            self.logger.debug("No strategy selected (confidence too low)")
            self._last_analysis = {
                'market_conditions': market_conditions.to_dict(),
                'selected_strategy': None,
                'confidence': 0.0
            }
            return
        
        selected_strategy_name, confidence, all_predictions = selection_result
        self._selected_strategy = selected_strategy_name
        
        # Get strategy instance
        strategy_instance = self._strategy_instances.get(selected_strategy_name)
        if not strategy_instance:
            self.logger.error(f"Strategy instance not found: {selected_strategy_name}")
            return
        
        # Generate signal from selected strategy
        current_price = Decimal(str(self._ohlcv_data['close'].iloc[-1]))
        signal = await self._generate_strategy_signal(
            strategy_instance,
            current_price
        )
        
        if signal is None or signal.signal_type == SignalType.HOLD:
            self.logger.debug(f"No actionable signal from {selected_strategy_name}")
            self._last_analysis = {
                'market_conditions': market_conditions.to_dict(),
                'selected_strategy': selected_strategy_name,
                'confidence': confidence,
                'signal': None
            }
            return
        
        # Evaluate opportunity
        if not self._evaluate_opportunity(signal, confidence, market_conditions):
            self.logger.debug("Opportunity does not meet execution criteria")
            return
        
        # Execute trade
        if self.enable_auto_execution:
            await self._execute_opportunity(strategy_instance, signal, market_conditions)
        else:
            self.logger.info(f"Auto-execution disabled. Signal: {signal.signal_type.value}")
        
        # Store analysis result
        self._last_analysis = {
            'market_conditions': market_conditions.to_dict(),
            'selected_strategy': selected_strategy_name,
            'confidence': confidence,
            'signal': {
                'type': signal.signal_type.value,
                'strength': signal.strength,
                'price': float(signal.price) if signal.price else None
            }
        }
    
    async def _generate_strategy_signal(
        self,
        strategy: BaseStrategy,
        current_price: Decimal
    ) -> Optional[StrategySignal]:
        """Generate signal from strategy."""
        try:
            # Prepare market data for strategy
            latest_candle = self._ohlcv_data.iloc[-1]
            data = {
                'open': latest_candle['open'],
                'high': latest_candle['high'],
                'low': latest_candle['low'],
                'volume': latest_candle['volume']
            }
            
            # Generate signal
            signal = await strategy.on_tick(
                symbol=self.symbol,
                price=current_price,
                timeframe=self.timeframe,
                data=data
            )
            
            if signal:
                # Process signal
                signal = strategy.on_signal(signal)
            
            return signal
            
        except Exception as e:
            self.logger.error(f"Error generating strategy signal: {e}")
            return None
    
    def _evaluate_opportunity(
        self,
        signal: StrategySignal,
        confidence: float,
        market_conditions: MarketConditions
    ) -> bool:
        """Evaluate if opportunity meets execution criteria."""
        # Check confidence threshold
        if confidence < self.min_confidence_threshold:
            return False
        
        # Check signal strength
        if signal.strength < 0.5:
            return False
        
        return True
    
    async def _can_execute_order(
        self,
        side: OrderSide,
        quantity: Decimal,
        price: Decimal
    ) -> Tuple[bool, str]:
        """Pre-flight check if order can be executed."""
        # 1. Check minimum order value ($1 USD)
        order_value = quantity * price
        if order_value < Decimal('1.0'):
            return False, f"Order value ${order_value:.2f} below minimum $1.00"
        
        # 2. For BUY: check sufficient balance (include fee buffer)
        if side == OrderSide.BUY:
            if self.paper_trading:
                balance = self.trading_engine.paper_trading.get_balance()
            else:
                balance = Decimal('0')  # Live trading balance check handled elsewhere
            
            # Add 1% fee buffer
            fee_estimate = order_value * Decimal('0.01')
            total_required = order_value + fee_estimate
            
            if balance < total_required:
                return False, f"Insufficient funds: ${balance:.2f} < ${total_required:.2f}"
        
        # 3. For SELL: check position exists with sufficient quantity
        if side == OrderSide.SELL:
            if self.paper_trading:
                positions = self.trading_engine.paper_trading.get_positions()
                position = next((p for p in positions if p.symbol == self.symbol), None)
                
                if not position:
                    return False, f"No position to sell for {self.symbol}"
                
                if position.quantity < quantity:
                    return False, f"Insufficient position: {position.quantity} < {quantity}"
        
        return True, "OK"
    
    def _determine_order_type_and_price(
        self,
        side: OrderSide,
        signal_strength: float,
        current_price: Decimal,
        is_stop_loss: bool = False
    ) -> Tuple[OrderType, Optional[Decimal]]:
        """Determine optimal order type and price for execution."""
        # Strong signals (>80% strength) or stop-loss use MARKET for speed
        if signal_strength > 0.8 or is_stop_loss:
            reason = "stop-loss" if is_stop_loss else f"high strength ({signal_strength:.2f})"
            self.logger.debug(f"Using MARKET order ({reason})")
            return OrderType.MARKET, None
        
        # Normal signals use LIMIT for better price
        # BUY: bid slightly below market (0.1% discount)
        # SELL (take-profit): ask slightly above market (0.1% premium)
        if side == OrderSide.BUY:
            limit_price = current_price * Decimal('0.999')  # 0.1% below
        else:
            limit_price = current_price * Decimal('1.001')  # 0.1% above
        
        # Round to reasonable precision (2 decimal places for USD pairs)
        limit_price = limit_price.quantize(Decimal('0.01'))
        
        return OrderType.LIMIT, limit_price
    
    async def _execute_opportunity(
        self,
        strategy: BaseStrategy,
        signal: StrategySignal,
        market_conditions: MarketConditions
    ):
        """Execute trade opportunity."""
        try:
            current_price = signal.price or Decimal(str(self._ohlcv_data['close'].iloc[-1]))
            
            # Calculate position size
            balance = self.trading_engine.paper_trading.get_balance() if self.paper_trading else Decimal(0)
            quantity = strategy.calculate_position_size(signal, balance, current_price)
            
            if quantity <= 0:
                self.logger.warning("Invalid position size calculated")
                return
            
            # Determine order side
            if signal.signal_type == SignalType.BUY:
                side = OrderSide.BUY
            elif signal.signal_type == SignalType.SELL:
                side = OrderSide.SELL
            else:
                self.logger.warning(f"Unsupported signal type: {signal.signal_type}")
                return
            
            # Pre-flight validation - skip order if it would fail
            can_execute, reason = await self._can_execute_order(side, quantity, current_price)
            if not can_execute:
                self.logger.info(f"Skipping order ({strategy.name}): {reason}")
                return
            
            # Determine optimal order type
            is_stop_loss = False
            if side == OrderSide.SELL and self.paper_trading:
                positions = self.trading_engine.paper_trading.get_positions()
                position = next((p for p in positions if p.symbol == self.symbol), None)
                if position:
                    is_stop_loss = current_price < position.entry_price
            
            order_type, limit_price = self._determine_order_type_and_price(
                side=side,
                signal_strength=signal.strength,
                current_price=current_price,
                is_stop_loss=is_stop_loss
            )
            
            # Execute order with smart order type
            order = await self.trading_engine.execute_order(
                exchange_id=self.exchange_id,
                strategy_id=None,  # Intelligent autopilot doesn't use strategy_id
                symbol=self.symbol,
                side=side,
                order_type=order_type,
                quantity=quantity,
                price=limit_price,  # None for MARKET, price for LIMIT
                paper_trading=self.paper_trading
            )
            
            order_type_str = "LIMIT" if order_type == OrderType.LIMIT else "MARKET"
            if order:
                self.logger.info(
                    f"Executed {side.value} {order_type_str} order: {quantity} {self.symbol} "
                    f"at {limit_price or current_price} (strategy: {strategy.name})"
                )
                
                # Increment Redis trade counter
                await self._increment_trades_today()
                
                # Record trade for ML training (async, don't wait)
                asyncio.create_task(self._record_trade_for_learning(
                    strategy.name,
                    market_conditions,
                    order
                ))
            else:
                self.logger.warning("Order execution failed")
                
        except Exception as e:
            self.logger.error(f"Error executing opportunity: {e}")
    
    async def _record_trade_for_learning(
        self,
        strategy_name: str,
        market_conditions: MarketConditions,
        order: Any
    ):
        """Record trade for ML learning (called after trade completes)."""
        try:
            # Wait a bit for order to complete
            await asyncio.sleep(5)
            
            trade_result = {
                'return_pct': 0.0,
                'sharpe_ratio': 0.0,
                'win_rate': 0.0,
                'max_drawdown': 0.0,
                'trade_count': 1
            }
            
            await self.performance_tracker.record_trade(
                strategy_name=strategy_name,
                market_conditions=market_conditions,
                trade_result=trade_result
            )
            
        except Exception as e:
            self.logger.error(f"Error recording trade for learning: {e}")
            
    async def _get_trades_today(self) -> int:
        """Get number of trades executed today from Redis."""
        try:
            redis_client = self.redis.get_client()
            count = await redis_client.get(self._trades_today_key)
            return int(count) if count else 0
        except Exception as e:
            self.logger.error(f"Error getting trade count from Redis: {e}")
            return 0
            
    async def _increment_trades_today(self):
        """Increment daily trade counter in Redis."""
        try:
            redis_client = self.redis.get_client()
            key = self._trades_today_key
            
            # Increment
            await redis_client.incr(key)
            
            # Set expiry to 24 hours (if new key)
            if await redis_client.ttl(key) == -1:
                await redis_client.expire(key, 86400)
                
        except Exception as e:
            self.logger.error(f"Error incrementing trade count in Redis: {e}")
    
    def stop(self):
        """Stop the intelligent autopilot (signals distributed stop)."""
        # Synchronous method to trigger stop
        # Since we need to delete Redis key async, we create a task
        asyncio.create_task(self._stop_async())
        
    async def _stop_async(self):
        """Async stop implementation."""
        self._local_running = False
        redis_client = self.redis.get_client()
        await redis_client.delete(self.key_running)
        self.logger.info("Intelligent autopilot stopping... (signal sent)")
    
    @property
    def is_running(self) -> bool:
        """Check if autopilot is running (Local check)."""
        return self._local_running
    
    def get_status(self) -> Dict[str, Any]:
        """Get current autopilot status (Synchronous wrapper)."""
        return {
            'symbol': self.symbol,
            'timeframe': self.timeframe,
            'running': self._local_running,
            'selected_strategy': self._selected_strategy,
            'trades_today': 0, # Fetch async if needed via get_distributed_status
            'max_trades_per_day': self.max_trades_per_day,
            'min_confidence_threshold': self.min_confidence_threshold,
            'enable_auto_execution': self.enable_auto_execution,
            'last_analysis': self._last_analysis,
            'model_info': self.strategy_selector.get_model_info()
        }
        
    async def get_distributed_status(self) -> Dict[str, Any]:
        """Get full distributed status (Async)."""
        try:
            redis_client = self.redis.get_client()
            is_running_distributed = await redis_client.exists(self.key_running)
            trades_today = await self._get_trades_today()
            
            status = self.get_status()
            status['running'] = bool(is_running_distributed)
            status['trades_today'] = trades_today
            return status
        except Exception:
            # Fallback to local status if Redis fails
            return self.get_status()


# Global instances (factory cache)
_intelligent_autopilots: Dict[str, IntelligentAutopilot] = {}


def get_intelligent_autopilot(
    symbol: str,
    exchange_id: int = 1,
    timeframe: str = "1h",
    **kwargs
) -> IntelligentAutopilot:
    """Get or create intelligent autopilot instance."""
    key = f"{symbol}:{exchange_id}:{timeframe}"
    if key not in _intelligent_autopilots:
        _intelligent_autopilots[key] = IntelligentAutopilot(
            symbol=symbol,
            exchange_id=exchange_id,
            timeframe=timeframe,
            **kwargs
        )
        logger.info(f"Created new IntelligentAutopilot instance for {key}")
    
    return _intelligent_autopilots[key]


def stop_all_autopilots():
    """Stop all running IntelligentAutoPilot instances."""
    for key, autopilot in _intelligent_autopilots.items():
        if autopilot.is_running:
            autopilot.stop()
            logger.info(f"Stopped IntelligentAutopilot for {key}")
