"""
Strategy Ensemble
Combines signals from multiple ML models and strategies for robust trading decisions

Models integrated:
- AMDDetector: Market phase detection (Accumulation/Manipulation/Distribution)
- ICTSMCDetector: Smart Money Concepts (Order Blocks, FVG, Liquidity)
- RangePredictor: Price range predictions
- TPSLClassifier: Take Profit / Stop Loss probability

Ensemble methods:
- Weighted voting based on model confidence and market conditions
- Confluence detection (multiple signals agreeing)
- Risk-adjusted position sizing
"""

import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Any, Tuple
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from loguru import logger

from .amd_detector import AMDDetector, AMDPhase
from .ict_smc_detector import ICTSMCDetector, ICTAnalysis, MarketBias
from .range_predictor import RangePredictor
from .tp_sl_classifier import TPSLClassifier


class SignalStrength(str, Enum):
    """Signal strength levels"""
    STRONG = "strong"
    MODERATE = "moderate"
    WEAK = "weak"
    NEUTRAL = "neutral"


class TradeAction(str, Enum):
    """Trading actions"""
    STRONG_BUY = "strong_buy"
    BUY = "buy"
    HOLD = "hold"
    SELL = "sell"
    STRONG_SELL = "strong_sell"


@dataclass
class ModelSignal:
    """Individual model signal"""
    model_name: str
    action: str  # 'buy', 'sell', 'hold'
    confidence: float  # 0-1
    weight: float  # Model weight in ensemble
    details: Dict[str, Any] = field(default_factory=dict)


@dataclass
class EnsembleSignal:
    """Combined ensemble trading signal"""
    timestamp: datetime
    symbol: str
    timeframe: str

    # Primary signal
    action: TradeAction
    confidence: float  # 0-1 overall confidence
    strength: SignalStrength

    # Direction scores (-1 to 1)
    bullish_score: float
    bearish_score: float
    net_score: float  # bullish - bearish

    # Entry/Exit levels
    entry_price: Optional[float] = None
    stop_loss: Optional[float] = None
    take_profit_1: Optional[float] = None
    take_profit_2: Optional[float] = None
    take_profit_3: Optional[float] = None
    risk_reward: Optional[float] = None

    # Position sizing
    suggested_risk_percent: float = 1.0
    position_size_multiplier: float = 1.0

    # Model contributions
    model_signals: List[ModelSignal] = field(default_factory=list)
    confluence_count: int = 0

    # Analysis details
    market_phase: str = "unknown"
    market_bias: str = "neutral"
    key_levels: Dict[str, float] = field(default_factory=dict)
    signals: List[str] = field(default_factory=list)

    # Quality metrics
    setup_score: float = 0  # 0-100

    def to_dict(self) -> Dict[str, Any]:
        return {
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'symbol': self.symbol,
            'timeframe': self.timeframe,
            'action': self.action.value,
            'confidence': round(self.confidence, 3),
            'strength': self.strength.value,
            'scores': {
                'bullish': round(self.bullish_score, 3),
                'bearish': round(self.bearish_score, 3),
                'net': round(self.net_score, 3)
            },
            'levels': {
                'entry': self.entry_price,
                'stop_loss': self.stop_loss,
                'take_profit_1': self.take_profit_1,
                'take_profit_2': self.take_profit_2,
                'take_profit_3': self.take_profit_3,
                'risk_reward': self.risk_reward
            },
            'position': {
                'risk_percent': self.suggested_risk_percent,
                'size_multiplier': self.position_size_multiplier
            },
            'model_signals': [
                {
                    'model': s.model_name,
                    'action': s.action,
                    'confidence': round(s.confidence, 3),
                    'weight': s.weight
                }
                for s in self.model_signals
            ],
            'confluence_count': self.confluence_count,
            'market_phase': self.market_phase,
            'market_bias': self.market_bias,
            'key_levels': self.key_levels,
            'signals': self.signals,
            'setup_score': self.setup_score
        }


class StrategyEnsemble:
    """
    Ensemble of trading strategies and ML models

    Combines multiple analysis methods to generate high-confidence trading signals.
    Uses weighted voting and confluence detection for robust decision making.
    """

    def __init__(
        self,
        # Model weights (should sum to 1.0)
        amd_weight: float = 0.25,
        ict_weight: float = 0.35,
        range_weight: float = 0.20,
        tpsl_weight: float = 0.20,
        # Thresholds
        min_confidence: float = 0.6,
        min_confluence: int = 2,
        strong_signal_threshold: float = 0.75,
        # Risk parameters
        base_risk_percent: float = 1.0,
        max_risk_percent: float = 2.0,
        min_risk_reward: float = 1.5
    ):
        # Normalize weights
        total_weight = amd_weight + ict_weight + range_weight + tpsl_weight
        self.weights = {
            'amd': amd_weight / total_weight,
            'ict': ict_weight / total_weight,
            'range': range_weight / total_weight,
            'tpsl': tpsl_weight / total_weight
        }

        # Thresholds
        self.min_confidence = min_confidence
        self.min_confluence = min_confluence
        self.strong_signal_threshold = strong_signal_threshold

        # Risk parameters
        self.base_risk_percent = base_risk_percent
        self.max_risk_percent = max_risk_percent
        self.min_risk_reward = min_risk_reward

        # Initialize models
        self.amd_detector = AMDDetector(lookback_periods=100)
        self.ict_detector = ICTSMCDetector(
            swing_lookback=10,
            ob_min_size=0.001,
            fvg_min_size=0.0005
        )
        self.range_predictor = None  # Lazy load
        self.tpsl_classifier = None  # Lazy load

        logger.info(
            f"StrategyEnsemble initialized with weights: "
            f"AMD={self.weights['amd']:.2f}, ICT={self.weights['ict']:.2f}, "
            f"Range={self.weights['range']:.2f}, TPSL={self.weights['tpsl']:.2f}"
        )

    def analyze(
        self,
        df: pd.DataFrame,
        symbol: str = "UNKNOWN",
        timeframe: str = "1H",
        current_price: Optional[float] = None
    ) -> EnsembleSignal:
        """
        Perform ensemble analysis combining all models

        Args:
            df: OHLCV DataFrame
            symbol: Trading symbol
            timeframe: Analysis timeframe
            current_price: Current market price (uses last close if not provided)

        Returns:
            EnsembleSignal with combined analysis
        """
        if len(df) < 100:
            return self._empty_signal(symbol, timeframe)

        current_price = current_price or df['close'].iloc[-1]
        model_signals = []

        # 1. AMD Analysis
        amd_signal = self._get_amd_signal(df)
        if amd_signal:
            model_signals.append(amd_signal)

        # 2. ICT/SMC Analysis
        ict_signal = self._get_ict_signal(df, symbol, timeframe)
        if ict_signal:
            model_signals.append(ict_signal)

        # 3. Range Prediction (if model available)
        range_signal = self._get_range_signal(df, current_price)
        if range_signal:
            model_signals.append(range_signal)

        # 4. TP/SL Probability (if model available)
        tpsl_signal = self._get_tpsl_signal(df, current_price)
        if tpsl_signal:
            model_signals.append(tpsl_signal)

        # Calculate ensemble scores
        bullish_score, bearish_score = self._calculate_direction_scores(model_signals)
        net_score = bullish_score - bearish_score

        # Determine action and confidence
        action, confidence, strength = self._determine_action(
            bullish_score, bearish_score, net_score, model_signals
        )

        # Get best entry/exit levels from models
        entry, sl, tp1, tp2, tp3, rr = self._get_best_levels(
            model_signals, action, current_price
        )

        # Calculate position sizing
        risk_percent, size_multiplier = self._calculate_position_sizing(
            confidence, len([s for s in model_signals if self._is_aligned(s, action)]),
            rr
        )

        # Collect all signals
        all_signals = self._collect_signals(model_signals)

        # Get market context
        market_phase = self._get_market_phase(model_signals)
        market_bias = self._get_market_bias(model_signals)

        # Get key levels
        key_levels = self._get_key_levels(model_signals, current_price)

        # Calculate setup score
        setup_score = self._calculate_setup_score(
            confidence, len(model_signals), rr, bullish_score, bearish_score
        )

        # Count confluence
        confluence = sum(1 for s in model_signals if self._is_aligned(s, action))

        return EnsembleSignal(
            timestamp=datetime.now(),
            symbol=symbol,
            timeframe=timeframe,
            action=action,
            confidence=confidence,
            strength=strength,
            bullish_score=bullish_score,
            bearish_score=bearish_score,
            net_score=net_score,
            entry_price=entry,
            stop_loss=sl,
            take_profit_1=tp1,
            take_profit_2=tp2,
            take_profit_3=tp3,
            risk_reward=rr,
            suggested_risk_percent=risk_percent,
            position_size_multiplier=size_multiplier,
            model_signals=model_signals,
            confluence_count=confluence,
            market_phase=market_phase,
            market_bias=market_bias,
            key_levels=key_levels,
            signals=all_signals,
            setup_score=setup_score
        )

    def _get_amd_signal(self, df: pd.DataFrame) -> Optional[ModelSignal]:
        """Get signal from AMD Detector"""
        try:
            phase = self.amd_detector.detect_phase(df)
            bias = self.amd_detector.get_trading_bias(phase)

            if phase.phase == 'accumulation' and phase.confidence > 0.5:
                action = 'buy'
                confidence = phase.confidence * 0.9  # Slight discount for accumulation
            elif phase.phase == 'distribution' and phase.confidence > 0.5:
                action = 'sell'
                confidence = phase.confidence * 0.9
            elif phase.phase == 'manipulation':
                action = 'hold'
                confidence = phase.confidence * 0.7  # High uncertainty in manipulation
            else:
                action = 'hold'
                confidence = 0.5

            return ModelSignal(
                model_name='AMD',
                action=action,
                confidence=confidence,
                weight=self.weights['amd'],
                details={
                    'phase': phase.phase,
                    'strength': phase.strength,
                    'signals': phase.signals,
                    'direction': bias['direction'],
                    'strategies': bias['strategies']
                }
            )

        except Exception as e:
            logger.warning(f"AMD analysis failed: {e}")
            return None

    def _get_ict_signal(
        self,
        df: pd.DataFrame,
        symbol: str,
        timeframe: str
    ) -> Optional[ModelSignal]:
        """Get signal from ICT/SMC Detector"""
        try:
            analysis = self.ict_detector.analyze(df, symbol, timeframe)
            recommendation = self.ict_detector.get_trade_recommendation(analysis)

            action = recommendation['action'].lower()
            if action in ['strong_buy', 'buy']:
                action = 'buy'
            elif action in ['strong_sell', 'sell']:
                action = 'sell'
            else:
                action = 'hold'

            confidence = analysis.bias_confidence if action != 'hold' else 0.5

            return ModelSignal(
                model_name='ICT',
                action=action,
                confidence=confidence,
                weight=self.weights['ict'],
                details={
                    'market_bias': analysis.market_bias.value,
                    'trend': analysis.current_trend,
                    'score': analysis.score,
                    'signals': analysis.signals,
                    'entry_zone': analysis.entry_zone,
                    'stop_loss': analysis.stop_loss,
                    'take_profit_1': analysis.take_profit_1,
                    'take_profit_2': analysis.take_profit_2,
                    'risk_reward': analysis.risk_reward,
                    'order_blocks': len(analysis.order_blocks),
                    'fvgs': len(analysis.fair_value_gaps)
                }
            )

        except Exception as e:
            logger.warning(f"ICT analysis failed: {e}")
            return None

    def _get_range_signal(
        self,
        df: pd.DataFrame,
        current_price: float
    ) -> Optional[ModelSignal]:
        """Get signal from Range Predictor"""
        try:
            if self.range_predictor is None:
                # Try to initialize
                try:
                    self.range_predictor = RangePredictor()
                except Exception:
                    return None

            # Get prediction
            prediction = self.range_predictor.predict(df)

            if prediction is None:
                return None

            # Determine action based on predicted range
            pred_high = prediction.predicted_high
            pred_low = prediction.predicted_low
            pred_mid = (pred_high + pred_low) / 2

            # If price is below predicted midpoint, expect upside
            if current_price < pred_mid:
                potential_up = (pred_high - current_price) / current_price
                potential_down = (current_price - pred_low) / current_price

                if potential_up > potential_down * 1.5:
                    action = 'buy'
                    confidence = min(0.8, 0.5 + potential_up * 2)
                else:
                    action = 'hold'
                    confidence = 0.5
            else:
                potential_down = (current_price - pred_low) / current_price
                potential_up = (pred_high - current_price) / current_price

                if potential_down > potential_up * 1.5:
                    action = 'sell'
                    confidence = min(0.8, 0.5 + potential_down * 2)
                else:
                    action = 'hold'
                    confidence = 0.5

            return ModelSignal(
                model_name='Range',
                action=action,
                confidence=confidence,
                weight=self.weights['range'],
                details={
                    'predicted_high': pred_high,
                    'predicted_low': pred_low,
                    'predicted_range': pred_high - pred_low,
                    'current_position': 'below_mid' if current_price < pred_mid else 'above_mid'
                }
            )

        except Exception as e:
            logger.debug(f"Range prediction not available: {e}")
            return None

    def _get_tpsl_signal(
        self,
        df: pd.DataFrame,
        current_price: float
    ) -> Optional[ModelSignal]:
        """Get signal from TP/SL Classifier"""
        try:
            if self.tpsl_classifier is None:
                try:
                    self.tpsl_classifier = TPSLClassifier()
                except Exception:
                    return None

            # Get classification
            result = self.tpsl_classifier.predict(df, current_price)

            if result is None:
                return None

            # Higher TP probability = bullish
            tp_prob = result.tp_probability
            sl_prob = result.sl_probability

            if tp_prob > sl_prob * 1.3:
                action = 'buy'
                confidence = tp_prob
            elif sl_prob > tp_prob * 1.3:
                action = 'sell'
                confidence = sl_prob
            else:
                action = 'hold'
                confidence = 0.5

            return ModelSignal(
                model_name='TPSL',
                action=action,
                confidence=confidence,
                weight=self.weights['tpsl'],
                details={
                    'tp_probability': tp_prob,
                    'sl_probability': sl_prob,
                    'expected_rr': result.expected_rr if hasattr(result, 'expected_rr') else None
                }
            )

        except Exception as e:
            logger.debug(f"TPSL classification not available: {e}")
            return None

    def _calculate_direction_scores(
        self,
        signals: List[ModelSignal]
    ) -> Tuple[float, float]:
        """Calculate weighted bullish and bearish scores"""
        bullish_score = 0.0
        bearish_score = 0.0
        total_weight = 0.0

        for signal in signals:
            weight = signal.weight * signal.confidence
            total_weight += signal.weight

            if signal.action == 'buy':
                bullish_score += weight
            elif signal.action == 'sell':
                bearish_score += weight
            # 'hold' contributes to neither

        # Normalize by total weight
        if total_weight > 0:
            bullish_score /= total_weight
            bearish_score /= total_weight

        return bullish_score, bearish_score

    def _determine_action(
        self,
        bullish_score: float,
        bearish_score: float,
        net_score: float,
        signals: List[ModelSignal]
    ) -> Tuple[TradeAction, float, SignalStrength]:
        """Determine final action, confidence, and strength"""

        # Count aligned signals
        buy_count = sum(1 for s in signals if s.action == 'buy')
        sell_count = sum(1 for s in signals if s.action == 'sell')

        # Calculate confidence
        confidence = max(bullish_score, bearish_score)

        # Determine action
        if net_score > 0.3 and bullish_score >= self.min_confidence:
            if bullish_score >= self.strong_signal_threshold and buy_count >= self.min_confluence:
                action = TradeAction.STRONG_BUY
                strength = SignalStrength.STRONG
            elif buy_count >= self.min_confluence:
                action = TradeAction.BUY
                strength = SignalStrength.MODERATE
            else:
                action = TradeAction.BUY
                strength = SignalStrength.WEAK

        elif net_score < -0.3 and bearish_score >= self.min_confidence:
            if bearish_score >= self.strong_signal_threshold and sell_count >= self.min_confluence:
                action = TradeAction.STRONG_SELL
                strength = SignalStrength.STRONG
            elif sell_count >= self.min_confluence:
                action = TradeAction.SELL
                strength = SignalStrength.MODERATE
            else:
                action = TradeAction.SELL
                strength = SignalStrength.WEAK

        else:
            action = TradeAction.HOLD
            strength = SignalStrength.NEUTRAL
            confidence = 1 - max(bullish_score, bearish_score)  # Confidence in holding

        return action, confidence, strength

    def _is_aligned(self, signal: ModelSignal, action: TradeAction) -> bool:
        """Check if a signal is aligned with the action"""
        if action in [TradeAction.STRONG_BUY, TradeAction.BUY]:
            return signal.action == 'buy'
        elif action in [TradeAction.STRONG_SELL, TradeAction.SELL]:
            return signal.action == 'sell'
        return signal.action == 'hold'

    def _get_best_levels(
        self,
        signals: List[ModelSignal],
        action: TradeAction,
        current_price: float
    ) -> Tuple[Optional[float], Optional[float], Optional[float], Optional[float], Optional[float], Optional[float]]:
        """Get best entry/exit levels from model signals"""

        # Prioritize ICT levels as they're most specific
        for signal in signals:
            if signal.model_name == 'ICT' and signal.details.get('entry_zone'):
                entry_zone = signal.details['entry_zone']
                entry = (entry_zone[0] + entry_zone[1]) / 2 if entry_zone else current_price
                sl = signal.details.get('stop_loss')
                tp1 = signal.details.get('take_profit_1')
                tp2 = signal.details.get('take_profit_2')
                rr = signal.details.get('risk_reward')

                if entry and sl and tp1:
                    return entry, sl, tp1, tp2, None, rr

        # Fallback: Calculate from Range predictions
        for signal in signals:
            if signal.model_name == 'Range':
                pred_high = signal.details.get('predicted_high')
                pred_low = signal.details.get('predicted_low')

                if pred_high and pred_low:
                    if action in [TradeAction.STRONG_BUY, TradeAction.BUY]:
                        entry = current_price
                        sl = pred_low * 0.995  # Slightly below predicted low
                        tp1 = pred_high * 0.98  # Just below predicted high
                        risk = entry - sl
                        rr = (tp1 - entry) / risk if risk > 0 else 0
                        return entry, sl, tp1, None, None, round(rr, 2)

                    elif action in [TradeAction.STRONG_SELL, TradeAction.SELL]:
                        entry = current_price
                        sl = pred_high * 1.005  # Slightly above predicted high
                        tp1 = pred_low * 1.02  # Just above predicted low
                        risk = sl - entry
                        rr = (entry - tp1) / risk if risk > 0 else 0
                        return entry, sl, tp1, None, None, round(rr, 2)

        # Default: Use ATR-based levels
        return current_price, None, None, None, None, None

    def _calculate_position_sizing(
        self,
        confidence: float,
        confluence: int,
        risk_reward: Optional[float]
    ) -> Tuple[float, float]:
        """Calculate suggested position sizing"""

        # Base risk
        risk = self.base_risk_percent

        # Adjust by confidence
        if confidence >= 0.8:
            risk *= 1.5
        elif confidence >= 0.7:
            risk *= 1.25
        elif confidence < 0.6:
            risk *= 0.75

        # Adjust by confluence
        if confluence >= 3:
            risk *= 1.25
        elif confluence >= 2:
            risk *= 1.0
        else:
            risk *= 0.75

        # Adjust by risk/reward
        if risk_reward:
            if risk_reward >= 3:
                risk *= 1.25
            elif risk_reward >= 2:
                risk *= 1.0
            elif risk_reward < 1.5:
                risk *= 0.5  # Reduce for poor R:R

        # Cap at max risk
        risk = min(risk, self.max_risk_percent)

        # Calculate size multiplier
        multiplier = risk / self.base_risk_percent

        return round(risk, 2), round(multiplier, 2)

    def _collect_signals(self, model_signals: List[ModelSignal]) -> List[str]:
        """Collect all signals from models"""
        all_signals = []

        for signal in model_signals:
            # Add model action
            all_signals.append(f"{signal.model_name}_{signal.action.upper()}")

            # Add specific signals from details
            if 'signals' in signal.details:
                all_signals.extend(signal.details['signals'])

            if 'phase' in signal.details:
                all_signals.append(f"AMD_PHASE_{signal.details['phase'].upper()}")

        return list(set(all_signals))  # Remove duplicates

    def _get_market_phase(self, signals: List[ModelSignal]) -> str:
        """Get market phase from AMD signal"""
        for signal in signals:
            if signal.model_name == 'AMD' and 'phase' in signal.details:
                return signal.details['phase']
        return 'unknown'

    def _get_market_bias(self, signals: List[ModelSignal]) -> str:
        """Get market bias from ICT signal"""
        for signal in signals:
            if signal.model_name == 'ICT' and 'market_bias' in signal.details:
                return signal.details['market_bias']
        return 'neutral'

    def _get_key_levels(
        self,
        signals: List[ModelSignal],
        current_price: float
    ) -> Dict[str, float]:
        """Compile key levels from all models"""
        levels = {'current': current_price}

        for signal in signals:
            if signal.model_name == 'ICT':
                if signal.details.get('stop_loss'):
                    levels['ict_sl'] = signal.details['stop_loss']
                if signal.details.get('take_profit_1'):
                    levels['ict_tp1'] = signal.details['take_profit_1']
                if signal.details.get('take_profit_2'):
                    levels['ict_tp2'] = signal.details['take_profit_2']

            elif signal.model_name == 'Range':
                if signal.details.get('predicted_high'):
                    levels['range_high'] = signal.details['predicted_high']
                if signal.details.get('predicted_low'):
                    levels['range_low'] = signal.details['predicted_low']

        return levels

    def _calculate_setup_score(
        self,
        confidence: float,
        num_signals: int,
        risk_reward: Optional[float],
        bullish_score: float,
        bearish_score: float
    ) -> float:
        """Calculate overall setup quality score (0-100)"""
        score = 0

        # Confidence contribution (0-40)
        score += confidence * 40

        # Model agreement contribution (0-20)
        score += min(20, num_signals * 5)

        # Directional clarity (0-20)
        directional_clarity = abs(bullish_score - bearish_score)
        score += directional_clarity * 20

        # Risk/Reward contribution (0-20)
        if risk_reward:
            if risk_reward >= 3:
                score += 20
            elif risk_reward >= 2:
                score += 15
            elif risk_reward >= 1.5:
                score += 10
            elif risk_reward >= 1:
                score += 5

        return min(100, round(score, 1))

    def _empty_signal(self, symbol: str, timeframe: str) -> EnsembleSignal:
        """Return empty signal when analysis cannot be performed"""
        return EnsembleSignal(
            timestamp=datetime.now(),
            symbol=symbol,
            timeframe=timeframe,
            action=TradeAction.HOLD,
            confidence=0,
            strength=SignalStrength.NEUTRAL,
            bullish_score=0,
            bearish_score=0,
            net_score=0
        )

    def get_quick_signal(
        self,
        df: pd.DataFrame,
        symbol: str = "UNKNOWN"
    ) -> Dict[str, Any]:
        """
        Get a quick trading signal for immediate use

        Returns:
            Simple dictionary with action, confidence, and key levels
        """
        signal = self.analyze(df, symbol)

        return {
            'symbol': symbol,
            'action': signal.action.value,
            'confidence': signal.confidence,
            'strength': signal.strength.value,
            'entry': signal.entry_price,
            'stop_loss': signal.stop_loss,
            'take_profit': signal.take_profit_1,
            'risk_reward': signal.risk_reward,
            'risk_percent': signal.suggested_risk_percent,
            'score': signal.setup_score,
            'signals': signal.signals[:5],  # Top 5 signals
            'confluence': signal.confluence_count,
            'timestamp': signal.timestamp.isoformat()
        }
