"""
Strategy Adapter for Backtesting
=================================
Adapts ML strategies for backtesting framework.

Provides a unified interface for all 5 trading strategies:
- PVA (Price Variation Attention)
- MRD (Momentum Regime Detection)
- VBP (Volatility Breakout Predictor)
- MSA (Market Structure Analysis)
- MTS (Multi-Timeframe Synthesis)

Each adapter converts strategy-specific outputs to a common Signal format
suitable for backtesting.

Author: ML-Specialist (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-25
"""

import numpy as np
import pandas as pd
from abc import ABC, abstractmethod
from typing import Dict, List, Optional, Tuple, Any, Union
from dataclasses import dataclass, field
from pathlib import Path
from enum import Enum
from loguru import logger

try:
    import torch
    TORCH_AVAILABLE = True
except ImportError:
    TORCH_AVAILABLE = False


class SignalDirection(Enum):
    """Trading signal direction."""
    LONG = 1
    SHORT = -1
    NEUTRAL = 0


@dataclass
class Prediction:
    """
    Unified prediction output from any strategy.

    Contains the raw prediction values before signal generation.
    """
    direction: float  # -1 to 1 (bearish to bullish)
    magnitude: float  # Expected return magnitude (absolute)
    confidence: float  # 0 to 1 confidence score
    raw_output: Dict[str, Any] = field(default_factory=dict)
    strategy_name: str = ""
    timestamp: Optional[pd.Timestamp] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'direction': float(self.direction),
            'magnitude': float(self.magnitude),
            'confidence': float(self.confidence),
            'strategy_name': self.strategy_name,
            'timestamp': str(self.timestamp) if self.timestamp else None,
            'raw_output': self.raw_output
        }


@dataclass
class Signal:
    """
    Trading signal generated from a prediction.

    Contains entry parameters including stop loss and take profit levels.
    """
    direction: SignalDirection
    confidence: float  # 0 to 1
    entry_price: float
    stop_loss: float
    take_profit: float
    strategy_name: str = ""
    timestamp: Optional[pd.Timestamp] = None
    risk_reward_ratio: float = 2.0
    position_size_factor: float = 1.0  # For position sizing based on confidence
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'direction': self.direction.name,
            'direction_value': self.direction.value,
            'confidence': float(self.confidence),
            'entry_price': float(self.entry_price),
            'stop_loss': float(self.stop_loss),
            'take_profit': float(self.take_profit),
            'strategy_name': self.strategy_name,
            'timestamp': str(self.timestamp) if self.timestamp else None,
            'risk_reward_ratio': float(self.risk_reward_ratio),
            'position_size_factor': float(self.position_size_factor),
            'metadata': self.metadata
        }

    @property
    def is_valid(self) -> bool:
        """Check if signal is valid for trading."""
        if self.direction == SignalDirection.NEUTRAL:
            return False
        if self.confidence < 0.5:
            return False
        if self.stop_loss <= 0 or self.take_profit <= 0:
            return False
        return True

    @property
    def risk_distance(self) -> float:
        """Get distance to stop loss."""
        if self.direction == SignalDirection.LONG:
            return abs(self.entry_price - self.stop_loss)
        else:
            return abs(self.stop_loss - self.entry_price)

    @property
    def reward_distance(self) -> float:
        """Get distance to take profit."""
        if self.direction == SignalDirection.LONG:
            return abs(self.take_profit - self.entry_price)
        else:
            return abs(self.entry_price - self.take_profit)


class BaseStrategy(ABC):
    """
    Abstract base class for all trading strategies.

    Defines the interface that all strategy adapters must implement.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        """
        Initialize base strategy.

        Args:
            model_path: Path to saved model weights
            device: Device for inference ('cpu' or 'cuda')
        """
        self.model_path = model_path
        self.device = device
        self._is_loaded = False
        self.model = None
        self.strategy_name = self.__class__.__name__

    @abstractmethod
    def load_model(self) -> bool:
        """
        Load the model from disk.

        Returns:
            True if model loaded successfully
        """
        pass

    @abstractmethod
    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """
        Generate prediction from input features.

        Args:
            features: Input features for the model

        Returns:
            Prediction object with direction, magnitude, confidence
        """
        pass

    def get_signal(
        self,
        prediction: Prediction,
        current_price: float,
        atr: float,
        threshold: float = 0.55,
        risk_reward: float = 2.0
    ) -> Optional[Signal]:
        """
        Convert prediction to trading signal.

        Args:
            prediction: Model prediction
            current_price: Current market price
            atr: Average True Range for SL/TP calculation
            threshold: Minimum confidence threshold
            risk_reward: Risk-reward ratio for TP

        Returns:
            Signal object if conditions met, None otherwise
        """
        if prediction.confidence < threshold:
            return None

        if abs(prediction.direction) < 0.1:
            return None

        direction = SignalDirection.LONG if prediction.direction > 0 else SignalDirection.SHORT

        sl_multiplier = 1.5
        tp_multiplier = sl_multiplier * risk_reward

        if direction == SignalDirection.LONG:
            stop_loss = current_price - (atr * sl_multiplier)
            take_profit = current_price + (atr * tp_multiplier)
        else:
            stop_loss = current_price + (atr * sl_multiplier)
            take_profit = current_price - (atr * tp_multiplier)

        position_size_factor = min(prediction.confidence, 1.0)
        if prediction.confidence > 0.8:
            position_size_factor = 1.0
        elif prediction.confidence > 0.7:
            position_size_factor = 0.75
        elif prediction.confidence > 0.6:
            position_size_factor = 0.5
        else:
            position_size_factor = 0.25

        return Signal(
            direction=direction,
            confidence=prediction.confidence,
            entry_price=current_price,
            stop_loss=stop_loss,
            take_profit=take_profit,
            strategy_name=prediction.strategy_name,
            timestamp=prediction.timestamp,
            risk_reward_ratio=risk_reward,
            position_size_factor=position_size_factor,
            metadata={
                'direction_raw': prediction.direction,
                'magnitude': prediction.magnitude,
                'atr': atr
            }
        )

    @property
    def is_loaded(self) -> bool:
        """Check if model is loaded."""
        return self._is_loaded


class PVAAdapter(BaseStrategy):
    """
    Adapter for PVA (Price Variation Attention) model.

    PVA uses transformer attention for sequence representation
    combined with XGBoost for prediction.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        super().__init__(model_path, device)
        self.strategy_name = "PVA"

    def load_model(self) -> bool:
        """Load PVA model."""
        if self.model_path is None:
            logger.warning("PVA: No model path provided, using mock model")
            self._is_loaded = True
            return True

        try:
            from ..models.strategies.pva.model import PVAModel
            self.model = PVAModel.load(self.model_path, device=self.device)
            self._is_loaded = True
            logger.info(f"PVA model loaded from {self.model_path}")
            return True
        except Exception as e:
            logger.error(f"Failed to load PVA model: {e}")
            return False

    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """Generate PVA prediction."""
        if isinstance(features, pd.DataFrame):
            features = features.values

        if self.model is not None and hasattr(self.model, 'predict'):
            try:
                pva_pred = self.model.predict(features)
                return Prediction(
                    direction=pva_pred.direction,
                    magnitude=pva_pred.magnitude,
                    confidence=pva_pred.confidence,
                    raw_output={'raw_prediction': pva_pred.raw_prediction},
                    strategy_name=self.strategy_name
                )
            except Exception as e:
                logger.error(f"PVA prediction error: {e}")

        direction = np.tanh(np.mean(features[-1] if features.ndim > 1 else features) * 0.1)
        return Prediction(
            direction=float(direction),
            magnitude=abs(float(direction)) * 0.01,
            confidence=0.5 + abs(float(direction)) * 0.3,
            raw_output={},
            strategy_name=self.strategy_name
        )


class MRDAdapter(BaseStrategy):
    """
    Adapter for MRD (Momentum Regime Detection) model.

    MRD uses HMM for regime detection combined with LSTM
    for sequence modeling and XGBoost for final prediction.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        super().__init__(model_path, device)
        self.strategy_name = "MRD"

    def load_model(self) -> bool:
        """Load MRD model."""
        if self.model_path is None:
            logger.warning("MRD: No model path provided, using mock model")
            self._is_loaded = True
            return True

        try:
            from ..models.strategies.mrd.model import MRDModel
            self.model = MRDModel()
            self.model.load(self.model_path)
            self._is_loaded = True
            logger.info(f"MRD model loaded from {self.model_path}")
            return True
        except Exception as e:
            logger.error(f"Failed to load MRD model: {e}")
            return False

    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """Generate MRD prediction."""
        if isinstance(features, np.ndarray):
            df = pd.DataFrame(features)
            if features.shape[1] >= 5:
                df.columns = ['open', 'high', 'low', 'close', 'volume'] + \
                            [f'feature_{i}' for i in range(features.shape[1] - 5)]
        else:
            df = features

        if self.model is not None and hasattr(self.model, 'predict'):
            try:
                mrd_pred = self.model.predict(df)
                regime_direction = {0: -0.8, 1: 0.0, 2: 0.8}.get(mrd_pred.regime, 0.0)
                direction = regime_direction * mrd_pred.continuation_prob
                return Prediction(
                    direction=direction,
                    magnitude=abs(direction) * 0.01,
                    confidence=mrd_pred.regime_confidence,
                    raw_output={
                        'regime': mrd_pred.regime,
                        'regime_name': mrd_pred.regime_name,
                        'duration': mrd_pred.duration,
                        'continuation_prob': mrd_pred.continuation_prob
                    },
                    strategy_name=self.strategy_name
                )
            except Exception as e:
                logger.error(f"MRD prediction error: {e}")

        if isinstance(df, pd.DataFrame) and 'close' in df.columns:
            returns = df['close'].pct_change().iloc[-20:].mean() if len(df) > 20 else 0
            direction = np.tanh(returns * 100)
        else:
            direction = 0.0

        return Prediction(
            direction=float(direction),
            magnitude=abs(float(direction)) * 0.01,
            confidence=0.5 + abs(float(direction)) * 0.25,
            raw_output={'regime': 1, 'regime_name': 'range'},
            strategy_name=self.strategy_name
        )


class VBPAdapter(BaseStrategy):
    """
    Adapter for VBP (Volatility Breakout Predictor) model.

    VBP uses CNN encoder for temporal patterns combined with
    XGBoost for breakout classification and magnitude prediction.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        super().__init__(model_path, device)
        self.strategy_name = "VBP"

    def load_model(self) -> bool:
        """Load VBP model."""
        if self.model_path is None:
            logger.warning("VBP: No model path provided, using mock model")
            self._is_loaded = True
            return True

        try:
            from ..models.strategies.vbp.model import VBPModel
            self.model = VBPModel()
            self.model.load(self.model_path)
            self._is_loaded = True
            logger.info(f"VBP model loaded from {self.model_path}")
            return True
        except Exception as e:
            logger.error(f"Failed to load VBP model: {e}")
            return False

    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """Generate VBP prediction."""
        if isinstance(features, np.ndarray):
            df = pd.DataFrame(features)
            if features.shape[1] >= 5:
                df.columns = ['open', 'high', 'low', 'close', 'volume'] + \
                            [f'feature_{i}' for i in range(features.shape[1] - 5)]
        else:
            df = features

        if self.model is not None and hasattr(self.model, 'predict_single'):
            try:
                vbp_pred = self.model.predict_single(df)
                direction = vbp_pred.direction * vbp_pred.breakout_probability
                return Prediction(
                    direction=direction,
                    magnitude=vbp_pred.magnitude,
                    confidence=vbp_pred.confidence,
                    raw_output={
                        'breakout_probability': vbp_pred.breakout_probability,
                        'breakout_class': vbp_pred.breakout_class,
                        'is_breakout': vbp_pred.is_breakout,
                        'is_high_confidence': vbp_pred.is_high_confidence
                    },
                    strategy_name=self.strategy_name
                )
            except Exception as e:
                logger.error(f"VBP prediction error: {e}")

        if isinstance(df, pd.DataFrame) and 'high' in df.columns and 'low' in df.columns:
            atr = (df['high'] - df['low']).iloc[-14:].mean() if len(df) > 14 else 0.01
            recent_range = df['high'].iloc[-5:].max() - df['low'].iloc[-5:].min() if len(df) > 5 else atr
            breakout_prob = min(recent_range / (atr * 2), 1.0) if atr > 0 else 0.5
            direction = 0.0
        else:
            breakout_prob = 0.3
            direction = 0.0

        return Prediction(
            direction=float(direction),
            magnitude=breakout_prob * 0.02,
            confidence=breakout_prob,
            raw_output={'breakout_probability': breakout_prob, 'is_breakout': False},
            strategy_name=self.strategy_name
        )


class MSAAdapter(BaseStrategy):
    """
    Adapter for MSA (Market Structure Analysis) model.

    MSA uses XGBoost for BOS direction, POI reaction, and
    structure continuation predictions.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        super().__init__(model_path, device)
        self.strategy_name = "MSA"

    def load_model(self) -> bool:
        """Load MSA model."""
        if self.model_path is None:
            logger.warning("MSA: No model path provided, using mock model")
            self._is_loaded = True
            return True

        try:
            from ..models.strategies.msa.model import MSAModel
            self.model = MSAModel()
            self.model.load(self.model_path)
            self._is_loaded = True
            logger.info(f"MSA model loaded from {self.model_path}")
            return True
        except Exception as e:
            logger.error(f"Failed to load MSA model: {e}")
            return False

    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """Generate MSA prediction."""
        if isinstance(features, pd.DataFrame):
            features_arr = features.values
        else:
            features_arr = features

        if self.model is not None and hasattr(self.model, 'predict_single'):
            try:
                msa_pred = self.model.predict_single(pd.DataFrame(features_arr))
                direction_map = {'bullish': 0.8, 'bearish': -0.8, 'neutral': 0.0}
                direction = direction_map.get(msa_pred.next_bos_direction, 0.0)
                direction = direction * msa_pred.bos_confidence
                return Prediction(
                    direction=direction,
                    magnitude=abs(direction) * 0.015,
                    confidence=msa_pred.bos_confidence,
                    raw_output={
                        'next_bos_direction': msa_pred.next_bos_direction,
                        'poi_reaction_prob': msa_pred.poi_reaction_prob,
                        'structure_continuation_prob': msa_pred.structure_continuation_prob,
                        'trading_bias': msa_pred.trading_bias
                    },
                    strategy_name=self.strategy_name
                )
            except Exception as e:
                logger.error(f"MSA prediction error: {e}")

        direction = 0.0
        return Prediction(
            direction=float(direction),
            magnitude=0.01,
            confidence=0.5,
            raw_output={'next_bos_direction': 'neutral'},
            strategy_name=self.strategy_name
        )


class MTSAdapter(BaseStrategy):
    """
    Adapter for MTS (Multi-Timeframe Synthesis) model.

    MTS uses hierarchical attention to combine multiple timeframes
    and produces unified direction, confidence, and optimal entry TF.
    """

    def __init__(self, model_path: Optional[str] = None, device: str = "cpu"):
        super().__init__(model_path, device)
        self.strategy_name = "MTS"

    def load_model(self) -> bool:
        """Load MTS model."""
        if self.model_path is None:
            logger.warning("MTS: No model path provided, using mock model")
            self._is_loaded = True
            return True

        try:
            from ..models.strategies.mts.model import MTSModel
            self.model = MTSModel()
            self.model.load(self.model_path)
            self._is_loaded = True
            logger.info(f"MTS model loaded from {self.model_path}")
            return True
        except Exception as e:
            logger.error(f"Failed to load MTS model: {e}")
            return False

    def predict(self, features: Union[np.ndarray, pd.DataFrame]) -> Prediction:
        """Generate MTS prediction."""
        if isinstance(features, np.ndarray):
            df = pd.DataFrame(features)
            if features.shape[1] >= 5:
                df.columns = ['open', 'high', 'low', 'close', 'volume'] + \
                            [f'feature_{i}' for i in range(features.shape[1] - 5)]
        else:
            df = features

        if self.model is not None and hasattr(self.model, 'predict'):
            try:
                mts_pred = self.model.predict(df)
                return Prediction(
                    direction=mts_pred.unified_direction,
                    magnitude=0.01,
                    confidence=mts_pred.confidence,
                    raw_output={
                        'direction_class': mts_pred.direction_class,
                        'confidence_by_alignment': mts_pred.confidence_by_alignment,
                        'optimal_entry_tf': mts_pred.optimal_entry_tf,
                        'tf_contributions': mts_pred.tf_contributions,
                        'recommended_action': mts_pred.recommended_action
                    },
                    strategy_name=self.strategy_name
                )
            except Exception as e:
                logger.error(f"MTS prediction error: {e}")

        if isinstance(df, pd.DataFrame) and 'close' in df.columns:
            sma_short = df['close'].iloc[-10:].mean() if len(df) > 10 else df['close'].iloc[-1]
            sma_long = df['close'].iloc[-50:].mean() if len(df) > 50 else sma_short
            direction = np.tanh((sma_short - sma_long) / sma_long * 100) if sma_long != 0 else 0
        else:
            direction = 0.0

        return Prediction(
            direction=float(direction),
            magnitude=abs(float(direction)) * 0.01,
            confidence=0.5 + abs(float(direction)) * 0.3,
            raw_output={'direction_class': 'neutral', 'optimal_entry_tf': '15m'},
            strategy_name=self.strategy_name
        )


class StrategyAdapter:
    """
    Factory class to load and manage strategy adapters.

    Provides a unified interface to load any of the 5 strategies
    and generate predictions or signals.
    """

    STRATEGY_MAP = {
        'pva': PVAAdapter,
        'mrd': MRDAdapter,
        'vbp': VBPAdapter,
        'msa': MSAAdapter,
        'mts': MTSAdapter
    }

    def __init__(self, models_dir: Optional[str] = None, device: str = "cpu"):
        """
        Initialize strategy adapter factory.

        Args:
            models_dir: Base directory for model files
            device: Device for inference
        """
        self.models_dir = Path(models_dir) if models_dir else None
        self.device = device
        self._strategies: Dict[str, BaseStrategy] = {}

    def load_strategy(
        self,
        strategy_name: str,
        symbol: Optional[str] = None
    ) -> BaseStrategy:
        """
        Load a specific strategy.

        Args:
            strategy_name: Name of strategy ('pva', 'mrd', 'vbp', 'msa', 'mts')
            symbol: Optional symbol for symbol-specific models

        Returns:
            Loaded strategy adapter
        """
        strategy_name = strategy_name.lower()

        if strategy_name not in self.STRATEGY_MAP:
            raise ValueError(f"Unknown strategy: {strategy_name}. "
                           f"Available: {list(self.STRATEGY_MAP.keys())}")

        cache_key = f"{strategy_name}_{symbol}" if symbol else strategy_name

        if cache_key in self._strategies and self._strategies[cache_key].is_loaded:
            return self._strategies[cache_key]

        model_path = None
        if self.models_dir:
            if symbol:
                model_path = self.models_dir / strategy_name / symbol
            else:
                model_path = self.models_dir / strategy_name

            if model_path.exists():
                model_path = str(model_path)
            else:
                model_path = None

        strategy_class = self.STRATEGY_MAP[strategy_name]
        strategy = strategy_class(model_path=model_path, device=self.device)
        strategy.load_model()

        self._strategies[cache_key] = strategy

        logger.info(f"Loaded strategy: {strategy_name}" +
                   (f" for {symbol}" if symbol else ""))

        return strategy

    def load_all_strategies(
        self,
        symbol: Optional[str] = None
    ) -> Dict[str, BaseStrategy]:
        """
        Load all available strategies.

        Args:
            symbol: Optional symbol for symbol-specific models

        Returns:
            Dictionary of loaded strategies
        """
        strategies = {}
        for name in self.STRATEGY_MAP.keys():
            try:
                strategies[name] = self.load_strategy(name, symbol)
            except Exception as e:
                logger.error(f"Failed to load {name}: {e}")

        return strategies

    def get_loaded_strategies(self) -> List[str]:
        """Get names of currently loaded strategies."""
        return [k.split('_')[0] for k, v in self._strategies.items() if v.is_loaded]

    def unload_strategy(self, strategy_name: str, symbol: Optional[str] = None):
        """Unload a strategy from memory."""
        cache_key = f"{strategy_name}_{symbol}" if symbol else strategy_name
        if cache_key in self._strategies:
            del self._strategies[cache_key]
            logger.info(f"Unloaded strategy: {cache_key}")


if __name__ == "__main__":
    print("Testing Strategy Adapter...")
    print("=" * 60)

    np.random.seed(42)
    n_samples = 100

    df = pd.DataFrame({
        'open': np.random.randn(n_samples).cumsum() + 100,
        'high': np.random.randn(n_samples).cumsum() + 101,
        'low': np.random.randn(n_samples).cumsum() + 99,
        'close': np.random.randn(n_samples).cumsum() + 100,
        'volume': np.random.randint(1000, 10000, n_samples)
    })
    df['high'] = df[['open', 'high', 'close']].max(axis=1)
    df['low'] = df[['open', 'low', 'close']].min(axis=1)

    adapter = StrategyAdapter(device='cpu')

    strategies = adapter.load_all_strategies()
    print(f"\nLoaded strategies: {list(strategies.keys())}")

    print("\n--- Strategy Predictions ---")
    for name, strategy in strategies.items():
        prediction = strategy.predict(df)
        print(f"{name.upper()}: direction={prediction.direction:.3f}, "
              f"magnitude={prediction.magnitude:.4f}, "
              f"confidence={prediction.confidence:.3f}")

    print("\n--- Signal Generation ---")
    current_price = df['close'].iloc[-1]
    atr = (df['high'] - df['low']).iloc[-14:].mean()

    for name, strategy in strategies.items():
        prediction = strategy.predict(df)
        signal = strategy.get_signal(prediction, current_price, atr, threshold=0.3)

        if signal and signal.is_valid:
            print(f"{name.upper()}: {signal.direction.name} @ {signal.entry_price:.2f}, "
                  f"SL={signal.stop_loss:.2f}, TP={signal.take_profit:.2f}, "
                  f"confidence={signal.confidence:.3f}")
        else:
            print(f"{name.upper()}: No valid signal")

    print("\n" + "=" * 60)
    print("Strategy Adapter tests complete!")
