"""
Signal Generator - Phase 2
Generates complete trading signals for LLM integration
"""

import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any, Union
from datetime import datetime
from pathlib import Path
import json
from loguru import logger

from .range_predictor import RangePredictor, RangePrediction
from .tp_sl_classifier import TPSLClassifier, TPSLPrediction

# Import feature flags para control de nuevas funcionalidades
from ..config.feature_flags import FeatureFlags


class DirectionalFilters:
    """
    Filtros direccionales basados en backtests exitosos.

    Validación:
    - SHORT: Requiere 2+ confirmaciones técnicas
    - LONG: Requiere 3+ confirmaciones (más estricto por histórico)

    Indicadores usados:
    - RSI: Momentum
    - SAR: Tendencia
    - CMF: Flujo de dinero
    - MFI: Índice de flujo monetario
    """

    @staticmethod
    def is_short_valid(df: pd.DataFrame, symbol: str) -> Tuple[bool, int, List[str]]:
        """
        Validar señal SHORT con indicadores técnicos.

        Args:
            df: DataFrame con columnas de indicadores (rsi, sar, close, cmf, mfi)
            symbol: Símbolo de trading

        Returns:
            (is_valid, confirmation_count, reasons)
        """
        confirmations = 0
        reasons = []

        if df is None or len(df) == 0:
            return False, 0, ["Empty DataFrame"]

        last = df.iloc[-1]

        # RSI > 55 (sobreextensión alcista)
        if 'rsi' in df.columns and pd.notna(last.get('rsi', None)):
            if last['rsi'] > 55:
                confirmations += 1
                reasons.append(f"RSI={last['rsi']:.1f}>55")

        # SAR above price (tendencia bajista)
        if 'sar' in df.columns and 'close' in df.columns:
            sar_val = last.get('sar', None)
            close_val = last.get('close', None)
            if pd.notna(sar_val) and pd.notna(close_val):
                if sar_val > close_val:
                    confirmations += 1
                    reasons.append("SAR_above_price")

        # CMF < 0 (flujo vendedor)
        if 'cmf' in df.columns and pd.notna(last.get('cmf', None)):
            if last['cmf'] < 0:
                confirmations += 1
                reasons.append(f"CMF={last['cmf']:.3f}<0")

        # MFI > 55 (distribución)
        if 'mfi' in df.columns and pd.notna(last.get('mfi', None)):
            if last['mfi'] > 55:
                confirmations += 1
                reasons.append(f"MFI={last['mfi']:.1f}>55")

        return confirmations >= 2, confirmations, reasons

    @staticmethod
    def is_long_valid(df: pd.DataFrame, symbol: str) -> Tuple[bool, int, List[str]]:
        """
        Validar señal LONG (requiere 3+ confirmaciones, más estricto).

        Args:
            df: DataFrame con columnas de indicadores
            symbol: Símbolo de trading

        Returns:
            (is_valid, confirmation_count, reasons)
        """
        confirmations = 0
        reasons = []

        if df is None or len(df) == 0:
            return False, 0, ["Empty DataFrame"]

        last = df.iloc[-1]

        # RSI < 35 (sobreventa)
        if 'rsi' in df.columns and pd.notna(last.get('rsi', None)):
            if last['rsi'] < 35:
                confirmations += 1
                reasons.append(f"RSI={last['rsi']:.1f}<35")

        # SAR below price (tendencia alcista)
        if 'sar' in df.columns and 'close' in df.columns:
            sar_val = last.get('sar', None)
            close_val = last.get('close', None)
            if pd.notna(sar_val) and pd.notna(close_val):
                if sar_val < close_val:
                    confirmations += 1
                    reasons.append("SAR_below_price")

        # CMF > 0.1 (flujo comprador fuerte)
        if 'cmf' in df.columns and pd.notna(last.get('cmf', None)):
            if last['cmf'] > 0.1:
                confirmations += 1
                reasons.append(f"CMF={last['cmf']:.3f}>0.1")

        # MFI < 35 (acumulación)
        if 'mfi' in df.columns and pd.notna(last.get('mfi', None)):
            if last['mfi'] < 35:
                confirmations += 1
                reasons.append(f"MFI={last['mfi']:.1f}<35")

        return confirmations >= 3, confirmations, reasons


@dataclass
class TradingSignal:
    """Complete trading signal for LLM consumption"""
    # Identification
    symbol: str
    timeframe_base: str
    horizon_minutes: int
    timestamp: datetime

    # Signal
    direction: str              # "long", "short", "none"
    entry_price: float
    stop_loss: float
    take_profit: float
    expected_rr: float

    # Probabilities
    prob_tp_first: float
    confidence_score: float

    # Context
    phase_amd: str
    volatility_regime: str

    # Predictions
    range_prediction: Dict[str, float]

    # Metadata
    model_metadata: Dict[str, Any]

    def to_dict(self) -> Dict:
        """Convert to dictionary"""
        return {
            'symbol': self.symbol,
            'timeframe_base': self.timeframe_base,
            'horizon_minutes': self.horizon_minutes,
            'timestamp': self.timestamp.isoformat() if self.timestamp else None,
            'direction': self.direction,
            'entry_price': self.entry_price,
            'stop_loss': self.stop_loss,
            'take_profit': self.take_profit,
            'expected_rr': self.expected_rr,
            'prob_tp_first': self.prob_tp_first,
            'confidence_score': self.confidence_score,
            'phase_amd': self.phase_amd,
            'volatility_regime': self.volatility_regime,
            'range_prediction': self.range_prediction,
            'model_metadata': self.model_metadata
        }

    def to_json(self) -> str:
        """Convert to JSON string"""
        return json.dumps(self.to_dict(), indent=2, default=str)

    @classmethod
    def from_dict(cls, data: Dict) -> 'TradingSignal':
        """Create from dictionary"""
        if isinstance(data.get('timestamp'), str):
            data['timestamp'] = datetime.fromisoformat(data['timestamp'])
        return cls(**data)


class SignalGenerator:
    """
    Generates trading signals by combining:
    - Range predictions (ΔHigh/ΔLow)
    - TP/SL classification
    - AMD phase detection
    - Volatility regime
    """

    def __init__(
        self,
        range_predictor: RangePredictor = None,
        tp_sl_classifier: TPSLClassifier = None,
        config: Dict[str, Any] = None
    ):
        """
        Initialize signal generator

        Args:
            range_predictor: Trained RangePredictor
            tp_sl_classifier: Trained TPSLClassifier
            config: Configuration dictionary
        """
        self.range_predictor = range_predictor
        self.tp_sl_classifier = tp_sl_classifier
        self.config = config or self._default_config()

        # Model metadata
        self.model_metadata = {
            'version': self.config.get('version', 'phase2_v1.0'),
            'training_window': self.config.get('training_window', 'unknown'),
            'eval_mape_delta_high': None,
            'eval_mape_delta_low': None,
            'eval_accuracy_tp_sl': None,
            'eval_roc_auc': None
        }

        logger.info("Initialized SignalGenerator")

    def _default_config(self) -> Dict:
        """Default configuration"""
        return {
            'version': 'phase2_v1.0',
            'training_window': '2020-2024',
            'horizons': {
                '15m': {'minutes': 15, 'bars': 3},
                '1h': {'minutes': 60, 'bars': 12}
            },
            'rr_configs': {
                'rr_2_1': {'sl': 5.0, 'tp': 10.0, 'rr': 2.0},
                'rr_3_1': {'sl': 5.0, 'tp': 15.0, 'rr': 3.0}
            },
            'filters': {
                'min_prob_tp_first': 0.55,
                'min_confidence': 0.50,
                'min_expected_rr': 1.5,
                'check_amd_phase': True,
                'check_volatility': True,
                'favorable_amd_phases': ['accumulation', 'distribution'],
                'min_volatility': 'medium'
            },
            'default_symbol': 'XAUUSD',
            'default_timeframe': '5m'
        }

    def set_model_metadata(
        self,
        version: str = None,
        training_window: str = None,
        mape_high: float = None,
        mape_low: float = None,
        accuracy_tp_sl: float = None,
        roc_auc: float = None
    ):
        """Set model metadata"""
        if version:
            self.model_metadata['version'] = version
        if training_window:
            self.model_metadata['training_window'] = training_window
        if mape_high is not None:
            self.model_metadata['eval_mape_delta_high'] = mape_high
        if mape_low is not None:
            self.model_metadata['eval_mape_delta_low'] = mape_low
        if accuracy_tp_sl is not None:
            self.model_metadata['eval_accuracy_tp_sl'] = accuracy_tp_sl
        if roc_auc is not None:
            self.model_metadata['eval_roc_auc'] = roc_auc

    def generate_signal(
        self,
        features: Union[pd.DataFrame, np.ndarray],
        current_price: float,
        symbol: str = None,
        timestamp: datetime = None,
        horizon: str = '15m',
        rr_config: str = 'rr_2_1',
        amd_phase: str = None,
        volatility_regime: str = None,
        direction: str = 'long',
        df: pd.DataFrame = None  # DataFrame con indicadores para filtros direccionales
    ) -> Optional[TradingSignal]:
        """
        Generate a complete trading signal

        Args:
            features: Feature vector for prediction
            current_price: Current market price
            symbol: Trading symbol
            timestamp: Signal timestamp
            horizon: Prediction horizon ('15m' or '1h')
            rr_config: R:R configuration name
            amd_phase: Current AMD phase (or None to skip filter)
            volatility_regime: Current volatility regime (or None to skip filter)
            df: Optional DataFrame with indicators (rsi, sar, cmf, mfi) for directional filters
            direction: Trade direction ('long' or 'short')

        Returns:
            TradingSignal if passes filters, None otherwise
        """
        symbol = symbol or self.config.get('default_symbol', 'XAUUSD')
        timestamp = timestamp or datetime.now()

        # Get R:R configuration
        rr = self.config['rr_configs'].get(rr_config, {'sl': 5.0, 'tp': 10.0, 'rr': 2.0})
        sl_distance = rr['sl']
        tp_distance = rr['tp']
        expected_rr = rr['rr']

        # Get range predictions
        range_pred = None
        if self.range_predictor is not None:
            preds = self.range_predictor.predict(features)
            # Find prediction for this horizon
            for pred in preds:
                if pred.horizon == horizon:
                    range_pred = pred
                    break

        # Get TP/SL probability
        prob_tp_first = 0.5
        if self.tp_sl_classifier is not None:
            proba = self.tp_sl_classifier.predict_proba(
                features, horizon=horizon, rr_config=rr_config
            )
            prob_tp_first = float(proba[0]) if len(proba) > 0 else 0.5

        # Calculate confidence
        confidence = self._calculate_confidence(
            prob_tp_first=prob_tp_first,
            range_pred=range_pred,
            amd_phase=amd_phase,
            volatility_regime=volatility_regime
        )

        # Calculate prices
        if direction == 'long':
            sl_price = current_price - sl_distance
            tp_price = current_price + tp_distance
        else:
            sl_price = current_price + sl_distance
            tp_price = current_price - tp_distance

        # Determine direction based on probability
        if prob_tp_first >= self.config['filters']['min_prob_tp_first']:
            final_direction = direction
        elif prob_tp_first < (1 - self.config['filters']['min_prob_tp_first']):
            final_direction = 'short' if direction == 'long' else 'long'
        else:
            final_direction = 'none'

        # Apply directional filters if DataFrame with indicators is provided
        if FeatureFlags.USE_DIRECTIONAL_FILTERS and df is not None and len(df) > 0:
            if final_direction == 'short':
                is_valid, conf_count, reasons = DirectionalFilters.is_short_valid(df, symbol)
                if not is_valid:
                    logger.debug(f"SHORT signal filtered for {symbol}: only {conf_count} confirmations")
                    return None
                # Boost de confianza por confirmaciones adicionales
                confidence_boost = 1 + (0.05 * min(conf_count, 4))
                confidence = min(confidence * confidence_boost, 1.0)
                logger.debug(f"SHORT validated: {conf_count} confirmations - {reasons}")

            elif final_direction == 'long':
                is_valid, conf_count, reasons = DirectionalFilters.is_long_valid(df, symbol)
                if not is_valid:
                    logger.debug(f"LONG signal filtered for {symbol}: only {conf_count} confirmations (need 3+)")
                    return None
                confidence_boost = 1 + (0.05 * min(conf_count, 4))
                confidence = min(confidence * confidence_boost, 1.0)
                logger.debug(f"LONG validated: {conf_count} confirmations - {reasons}")

        # Create signal
        signal = TradingSignal(
            symbol=symbol,
            timeframe_base=self.config.get('default_timeframe', '5m'),
            horizon_minutes=self.config['horizons'].get(horizon, {}).get('minutes', 15),
            timestamp=timestamp,
            direction=final_direction,
            entry_price=current_price,
            stop_loss=sl_price,
            take_profit=tp_price,
            expected_rr=expected_rr,
            prob_tp_first=prob_tp_first,
            confidence_score=confidence,
            phase_amd=amd_phase or 'neutral',
            volatility_regime=volatility_regime or 'medium',
            range_prediction={
                'delta_high': range_pred.delta_high if range_pred else 0.0,
                'delta_low': range_pred.delta_low if range_pred else 0.0,
                'delta_high_bin': range_pred.delta_high_bin if range_pred else None,
                'delta_low_bin': range_pred.delta_low_bin if range_pred else None
            },
            model_metadata=self.model_metadata.copy()
        )

        # Apply filters
        if self.filter_signal(signal):
            return signal
        else:
            return None

    def generate_signals_batch(
        self,
        features: Union[pd.DataFrame, np.ndarray],
        prices: np.ndarray,
        timestamps: List[datetime],
        symbol: str = None,
        horizon: str = '15m',
        rr_config: str = 'rr_2_1',
        amd_phases: List[str] = None,
        volatility_regimes: List[str] = None,
        direction: str = 'long'
    ) -> List[Optional[TradingSignal]]:
        """
        Generate signals for a batch of samples

        Args:
            features: Feature matrix (n_samples x n_features)
            prices: Current prices for each sample
            timestamps: Timestamps for each sample
            symbol: Trading symbol
            horizon: Prediction horizon
            rr_config: R:R configuration
            amd_phases: AMD phases for each sample
            volatility_regimes: Volatility regimes for each sample
            direction: Default trade direction

        Returns:
            List of TradingSignal (or None for filtered signals)
        """
        n_samples = len(prices)
        signals = []

        # Get batch predictions if models available
        range_preds = None
        if self.range_predictor is not None:
            range_preds = self.range_predictor.predict(features)

        tp_sl_probs = None
        if self.tp_sl_classifier is not None:
            tp_sl_probs = self.tp_sl_classifier.predict_proba(
                features, horizon=horizon, rr_config=rr_config
            )

        for i in range(n_samples):
            amd_phase = amd_phases[i] if amd_phases else None
            vol_regime = volatility_regimes[i] if volatility_regimes else None

            # Get individual feature row
            if isinstance(features, pd.DataFrame):
                feat_row = features.iloc[[i]]
            else:
                feat_row = features[i:i+1]

            signal = self.generate_signal(
                features=feat_row,
                current_price=prices[i],
                symbol=symbol,
                timestamp=timestamps[i],
                horizon=horizon,
                rr_config=rr_config,
                amd_phase=amd_phase,
                volatility_regime=vol_regime,
                direction=direction
            )
            signals.append(signal)

        # Log statistics
        valid_signals = [s for s in signals if s is not None]
        logger.info(f"Generated {len(valid_signals)}/{n_samples} signals "
                   f"(filtered: {n_samples - len(valid_signals)})")

        return signals

    def filter_signal(self, signal: TradingSignal) -> bool:
        """
        Apply filters to determine if signal should be used

        Args:
            signal: Trading signal to filter

        Returns:
            True if signal passes all filters
        """
        filters = self.config.get('filters', {})

        # Probability filter
        if signal.prob_tp_first < filters.get('min_prob_tp_first', 0.55):
            if signal.prob_tp_first > (1 - filters.get('min_prob_tp_first', 0.55)):
                # Not confident in either direction
                return False

        # Confidence filter
        if signal.confidence_score < filters.get('min_confidence', 0.50):
            return False

        # R:R filter
        if signal.expected_rr < filters.get('min_expected_rr', 1.5):
            return False

        # AMD phase filter
        if filters.get('check_amd_phase', True):
            favorable_phases = filters.get('favorable_amd_phases', ['accumulation', 'distribution'])
            if signal.phase_amd not in favorable_phases and signal.phase_amd != 'neutral':
                return False

        # Volatility filter
        if filters.get('check_volatility', True):
            min_vol = filters.get('min_volatility', 'medium')
            vol_order = {'low': 0, 'medium': 1, 'high': 2}
            if vol_order.get(signal.volatility_regime, 1) < vol_order.get(min_vol, 1):
                return False

        # Direction filter - no signal if direction is 'none'
        if signal.direction == 'none':
            return False

        return True

    def _calculate_confidence(
        self,
        prob_tp_first: float,
        range_pred: Optional[RangePrediction],
        amd_phase: str,
        volatility_regime: str
    ) -> float:
        """
        Calculate overall confidence score

        Args:
            prob_tp_first: TP probability
            range_pred: Range prediction
            amd_phase: AMD phase
            volatility_regime: Volatility regime

        Returns:
            Confidence score (0-1)
        """
        # Base confidence from probability
        prob_confidence = abs(prob_tp_first - 0.5) * 2  # 0 at 0.5, 1 at 0 or 1

        # Range prediction confidence
        range_confidence = 0.5
        if range_pred is not None:
            range_confidence = (range_pred.confidence_high + range_pred.confidence_low) / 2

        # AMD phase bonus
        amd_bonus = 0.0
        favorable_phases = self.config.get('filters', {}).get(
            'favorable_amd_phases', ['accumulation', 'distribution']
        )
        if amd_phase in favorable_phases:
            amd_bonus = 0.1
        elif amd_phase == 'manipulation':
            amd_bonus = -0.1

        # Volatility adjustment
        vol_adjustment = 0.0
        if volatility_regime == 'high':
            vol_adjustment = 0.05  # Slight bonus for high volatility
        elif volatility_regime == 'low':
            vol_adjustment = -0.1  # Penalty for low volatility

        # Combined confidence
        confidence = (
            prob_confidence * 0.5 +
            range_confidence * 0.3 +
            0.5 * 0.2  # Base confidence
        ) + amd_bonus + vol_adjustment

        # Clamp to [0, 1]
        return max(0.0, min(1.0, confidence))

    def save(self, path: str):
        """Save signal generator configuration"""
        path = Path(path)
        path.mkdir(parents=True, exist_ok=True)

        config_data = {
            'config': self.config,
            'model_metadata': self.model_metadata
        }

        with open(path / 'signal_generator_config.json', 'w') as f:
            json.dump(config_data, f, indent=2)

        logger.info(f"Saved SignalGenerator config to {path}")

    def load(self, path: str):
        """Load signal generator configuration"""
        path = Path(path)

        with open(path / 'signal_generator_config.json', 'r') as f:
            config_data = json.load(f)

        self.config = config_data['config']
        self.model_metadata = config_data['model_metadata']

        logger.info(f"Loaded SignalGenerator config from {path}")


if __name__ == "__main__":
    # Test signal generator
    import numpy as np
    from datetime import datetime

    # Create mock signal generator (without trained models)
    generator = SignalGenerator()

    # Generate sample signal
    features = np.random.randn(1, 20)
    current_price = 2000.0

    signal = generator.generate_signal(
        features=features,
        current_price=current_price,
        symbol='XAUUSD',
        timestamp=datetime.now(),
        horizon='15m',
        rr_config='rr_2_1',
        amd_phase='accumulation',
        volatility_regime='high',
        direction='long'
    )

    if signal:
        print("\n=== Generated Signal ===")
        print(signal.to_json())
    else:
        print("Signal was filtered out")

    # Test batch generation
    print("\n=== Batch Generation Test ===")
    features_batch = np.random.randn(10, 20)
    prices = np.random.uniform(1990, 2010, 10)
    timestamps = [datetime.now() for _ in range(10)]
    amd_phases = np.random.choice(['accumulation', 'manipulation', 'distribution', 'neutral'], 10)
    vol_regimes = np.random.choice(['low', 'medium', 'high'], 10)

    signals = generator.generate_signals_batch(
        features=features_batch,
        prices=prices,
        timestamps=timestamps,
        symbol='XAUUSD',
        horizon='1h',
        rr_config='rr_2_1',
        amd_phases=amd_phases.tolist(),
        volatility_regimes=vol_regimes.tolist()
    )

    valid_count = sum(1 for s in signals if s is not None)
    print(f"Generated {valid_count}/{len(signals)} valid signals")
