"""Ensemble/consensus strategy.

Combines signals from multiple strategies with voting mechanism.
Only executes when multiple strategies agree, improving signal quality.
"""

from decimal import Decimal
from typing import Optional, Dict, Any, List
import pandas as pd
from src.strategies.base import BaseStrategy, StrategySignal, SignalType, get_strategy_registry
from src.core.logger import get_logger
from src.autopilot.performance_tracker import get_performance_tracker

logger = get_logger(__name__)


class ConsensusStrategy(BaseStrategy):
    """Ensemble strategy that combines signals from multiple strategies.
    
    This strategy aggregates signals from multiple registered strategies
    and only generates signals when a minimum consensus is reached.
    Signals are weighted by strategy performance metrics.
    """
    
    def __init__(self, name: str, parameters: Optional[Dict[str, Any]] = None, timeframes: Optional[list] = None):
        """Initialize consensus strategy.
        
        Parameters:
            strategy_names: List of strategy names to include (None = all)
            min_consensus: Minimum number of strategies that must agree (default 2)
            use_weights: Weight signals by strategy performance (default True)
            min_weight: Minimum weight for a strategy to participate (default 0.3)
            exclude_strategies: List of strategy names to exclude
        """
        super().__init__(name, parameters, timeframes)
        
        self.strategy_names = self.parameters.get('strategy_names', None)
        self.min_consensus = self.parameters.get('min_consensus', 2)
        self.use_weights = self.parameters.get('use_weights', True)
        self.min_weight = self.parameters.get('min_weight', 0.3)
        self.exclude_strategies = self.parameters.get('exclude_strategies', [])
        
        self.registry = get_strategy_registry()
        self.performance_tracker = get_performance_tracker()
        self._strategy_instances: Dict[str, BaseStrategy] = {}
        self._strategy_weights: Dict[str, float] = {}
        
        # Initialize strategy instances
        self._initialize_strategies()
        
        # Calculate weights if using weighted voting
        if self.use_weights:
            self._calculate_weights()
    
    def _initialize_strategies(self):
        """Initialize instances of strategies to monitor."""
        if self.strategy_names is None:
            available = self.registry.list_available()
            # Aggressive filtering to prevent recursion
            self.strategy_names = [
                s for s in available 
                if s not in self.exclude_strategies 
                and "consensus" not in s.lower()
                and s.lower() != self.name.lower()
            ]
            self.logger.info(f"ConsensusStrategy({self.name}) automatically selected strategies: {self.strategy_names}")
        else:
            self.logger.info(f"ConsensusStrategy({self.name}) manually selected strategies: {self.strategy_names}")
        
        for strategy_name in self.strategy_names:
            try:
                strategy_class = self.registry._strategies.get(strategy_name.lower())
                if strategy_class:
                    instance = strategy_class(
                        name=f"{strategy_name}_consensus",
                        parameters={},
                        timeframes=self.timeframes
                    )
                    instance.enabled = True
                    self._strategy_instances[strategy_name] = instance
                    self.logger.debug(f"Initialized strategy for consensus: {strategy_name}")
            except Exception as e:
                self.logger.warning(f"Failed to initialize strategy {strategy_name} for consensus: {e}")
    
    async def _calculate_weights(self):
        """Calculate weights for strategies based on performance."""
        for strategy_name in self._strategy_instances.keys():
            try:
                metrics = await self.performance_tracker.calculate_metrics(strategy_name, period_days=30)
                
                # Weight based on win rate and Sharpe ratio
                win_rate = metrics.get('win_rate', 0.5)
                sharpe_ratio = max(0.0, metrics.get('sharpe_ratio', 0.0))
                
                # Normalize to 0-1 range
                weight = (win_rate * 0.6) + (min(sharpe_ratio / 2.0, 1.0) * 0.4)
                
                # Apply minimum weight threshold
                if weight < self.min_weight:
                    weight = self.min_weight
                
                self._strategy_weights[strategy_name] = weight
                self.logger.debug(f"Strategy {strategy_name} weight: {weight:.2f}")
            except Exception as e:
                # Default weight if metrics unavailable
                self._strategy_weights[strategy_name] = 0.5
                self.logger.warning(f"Could not calculate weight for {strategy_name}: {e}")
    
    async def _collect_signals(
        self,
        symbol: str,
        price: Decimal,
        timeframe: str,
        data: Dict[str, Any]
    ) -> List[tuple[str, StrategySignal, float]]:
        """Collect signals from all monitored strategies.
        
        Args:
            symbol: Trading symbol
            price: Current price
            timeframe: Timeframe
            data: Market data
            
        Returns:
            List of (strategy_name, signal, weight) tuples
        """
        signals: List[tuple[str, StrategySignal, float]] = []
        
        for strategy_name, strategy_instance in self._strategy_instances.items():
            try:
                signal = await strategy_instance.on_tick(symbol, price, timeframe, data)
                if signal and signal.signal_type != SignalType.HOLD:
                    # Process signal through strategy's on_signal
                    signal = strategy_instance.on_signal(signal)
                    if signal:
                        weight = self._strategy_weights.get(strategy_name, 0.5) if self.use_weights else 1.0
                        signals.append((strategy_name, signal, weight))
            except Exception as e:
                import traceback
                self.logger.warning(f"Error getting signal from {strategy_name}: {e}\n{traceback.format_exc()}")
        
        return signals
    
    def _aggregate_signals(
        self,
        signals: List[tuple[str, StrategySignal, float]]
    ) -> Optional[StrategySignal]:
        """Aggregate signals and determine consensus.
        
        Args:
            signals: List of (strategy_name, signal, weight) tuples
            
        Returns:
            Consensus signal or None
        """
        if not signals:
            return None
        
        # Group signals by type
        buy_signals: List[tuple[str, StrategySignal, float]] = []
        sell_signals: List[tuple[str, StrategySignal, float]] = []
        
        for strategy_name, signal, weight in signals:
            if signal.signal_type == SignalType.BUY:
                buy_signals.append((strategy_name, signal, weight))
            elif signal.signal_type == SignalType.SELL:
                sell_signals.append((strategy_name, signal, weight))
        
        # Calculate weighted consensus scores
        buy_score = sum(weight * signal.strength for _, signal, weight in buy_signals)
        sell_score = sum(weight * signal.strength for _, signal, weight in sell_signals)
        
        buy_count = len(buy_signals)
        sell_count = len(sell_signals)
        
        # Determine final signal
        final_signal_type = None
        consensus_count = 0
        consensus_score = 0.0
        participating_strategies = []
        
        if buy_count >= self.min_consensus and buy_score > sell_score:
            final_signal_type = SignalType.BUY
            consensus_count = buy_count
            consensus_score = buy_score
            participating_strategies = [name for name, _, _ in buy_signals]
        elif sell_count >= self.min_consensus and sell_score > buy_score:
            final_signal_type = SignalType.SELL
            consensus_count = sell_count
            consensus_score = sell_score
            participating_strategies = [name for name, _, _ in sell_signals]
        
        if final_signal_type is None:
            return None
        
        # Calculate final signal strength (normalized)
        max_possible_score = len(self._strategy_instances) * 1.0  # Max weight * max strength
        strength = min(1.0, consensus_score / max_possible_score) if max_possible_score > 0 else 0.5
        
        # Use price from strongest signal
        strongest_signal = max(
            buy_signals if final_signal_type == SignalType.BUY else sell_signals,
            key=lambda x: x[2] * x[1].strength  # weight * strength
        )[1]
        
        return StrategySignal(
            signal_type=final_signal_type,
            symbol=strongest_signal.symbol,
            strength=strength,
            price=strongest_signal.price,
            metadata={
                'consensus_count': consensus_count,
                'consensus_score': consensus_score,
                'participating_strategies': participating_strategies,
                'buy_count': buy_count,
                'sell_count': sell_count,
                'buy_score': buy_score,
                'sell_score': sell_score,
                'strategy': 'consensus'
            }
        )
    
    async def on_tick(self, symbol: str, price: Decimal, timeframe: str, data: Dict[str, Any]) -> Optional[StrategySignal]:
        """Generate consensus signal from multiple strategies."""
        # Update weights periodically (every 100 ticks)
        if not hasattr(self, '_tick_count'):
            self._tick_count = 0
        self._tick_count += 1
        
        if self.use_weights and self._tick_count % 100 == 0:
            await self._calculate_weights()
        
        # Collect signals from all strategies
        signals = await self._collect_signals(symbol, price, timeframe, data)
        
        # Aggregate signals
        consensus_signal = self._aggregate_signals(signals)
        
        return consensus_signal
    
    def on_signal(self, signal: StrategySignal) -> Optional[StrategySignal]:
        """Process signal."""
        return signal if self.should_execute(signal) else None

