"""
R:R Backtester - Phase 2
Backtester focused on Risk:Reward based trading with TP/SL simulation
"""

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, timedelta
from pathlib import Path
import json
from loguru import logger

from .metrics import TradingMetrics, TradeRecord, MetricsCalculator


@dataclass
class BacktestConfig:
    """Configuration for backtesting"""
    initial_capital: float = 10000.0
    risk_per_trade: float = 0.02          # 2% risk per trade
    max_concurrent_trades: int = 1
    commission_pct: float = 0.0
    slippage_pct: float = 0.0005
    min_confidence: float = 0.55          # Minimum probability to enter
    max_position_time: int = 60           # Maximum minutes to hold

    # R:R configurations to test
    rr_configs: List[Dict] = field(default_factory=lambda: [
        {'name': 'rr_2_1', 'sl': 5.0, 'tp': 10.0},
        {'name': 'rr_3_1', 'sl': 5.0, 'tp': 15.0}
    ])

    # Filters
    filter_by_amd: bool = True
    favorable_amd_phases: List[str] = field(default_factory=lambda: ['accumulation', 'distribution'])
    filter_by_volatility: bool = True
    min_volatility_regime: str = 'medium'


@dataclass
class BacktestResult:
    """Complete backtest results"""
    config: BacktestConfig
    trades: List[TradeRecord]
    metrics: TradingMetrics
    equity_curve: np.ndarray
    drawdown_curve: np.ndarray

    # Segmented results
    metrics_by_horizon: Dict[str, TradingMetrics] = field(default_factory=dict)
    metrics_by_rr: Dict[str, TradingMetrics] = field(default_factory=dict)
    metrics_by_amd: Dict[str, TradingMetrics] = field(default_factory=dict)
    metrics_by_volatility: Dict[str, TradingMetrics] = field(default_factory=dict)

    # Summary statistics
    total_bars: int = 0
    signals_generated: int = 0
    signals_filtered: int = 0
    signals_traded: int = 0

    def to_dict(self) -> Dict:
        """Convert to dictionary"""
        return {
            'metrics': self.metrics.to_dict(),
            'total_bars': self.total_bars,
            'signals_generated': self.signals_generated,
            'signals_traded': self.signals_traded,
            'trade_count': len(self.trades),
            'equity_curve_final': float(self.equity_curve[-1]) if len(self.equity_curve) > 0 else 0,
            'max_drawdown': self.metrics.max_drawdown,
            'metrics_by_horizon': {k: v.to_dict() for k, v in self.metrics_by_horizon.items()},
            'metrics_by_rr': {k: v.to_dict() for k, v in self.metrics_by_rr.items()}
        }

    def save_report(self, filepath: str):
        """Save detailed report to JSON"""
        report = {
            'summary': self.to_dict(),
            'trades': [t.to_dict() for t in self.trades],
            'equity_curve': self.equity_curve.tolist(),
            'drawdown_curve': self.drawdown_curve.tolist()
        }
        with open(filepath, 'w') as f:
            json.dump(report, f, indent=2, default=str)
        logger.info(f"Saved backtest report to {filepath}")


class RRBacktester:
    """
    Backtester for R:R-based trading strategies

    Simulates trades based on predicted TP/SL probabilities
    and evaluates performance using trading metrics.
    """

    def __init__(self, config: BacktestConfig = None):
        """
        Initialize backtester

        Args:
            config: Backtest configuration
        """
        self.config = config or BacktestConfig()
        self.metrics_calculator = MetricsCalculator()

        # State variables
        self.trades = []
        self.open_positions = []
        self.equity = self.config.initial_capital
        self.equity_history = []
        self.trade_id_counter = 0

        logger.info(f"Initialized RRBacktester with ${self.config.initial_capital:,.0f} capital")

    def run_backtest(
        self,
        price_data: pd.DataFrame,
        signals: pd.DataFrame,
        rr_config: Dict = None
    ) -> BacktestResult:
        """
        Run backtest on price data with signals

        Args:
            price_data: DataFrame with OHLCV data (indexed by datetime)
            signals: DataFrame with signal data including:
                     - prob_tp_first: Probability of TP hitting first
                     - direction: 'long' or 'short'
                     - horizon: Prediction horizon
                     - amd_phase: (optional) AMD phase
                     - volatility_regime: (optional) Volatility level
            rr_config: Specific R:R config to use, or None to use from signals

        Returns:
            BacktestResult object
        """
        logger.info(f"Starting backtest on {len(price_data)} bars")

        # Reset state
        self._reset_state()

        # Validate data
        if 'prob_tp_first' not in signals.columns:
            raise ValueError("signals must contain 'prob_tp_first' column")

        # Align indices
        common_idx = price_data.index.intersection(signals.index)
        price_data = price_data.loc[common_idx]
        signals = signals.loc[common_idx]

        total_bars = len(price_data)
        signals_generated = 0
        signals_filtered = 0
        signals_traded = 0

        # Iterate through each bar
        for i in range(len(price_data) - 1):
            current_time = price_data.index[i]
            current_price = price_data.iloc[i]

            # Update open positions
            self._update_positions(price_data, i)

            # Check for signal at this bar
            if current_time in signals.index:
                signal = signals.loc[current_time]

                # Check if we have a valid signal
                if pd.notna(signal.get('prob_tp_first')):
                    signals_generated += 1

                    # Apply filters
                    if self._should_trade(signal):
                        # Check if we can open new position
                        if len(self.open_positions) < self.config.max_concurrent_trades:
                            # Open trade
                            trade = self._open_trade(
                                signal=signal,
                                price_data=price_data,
                                bar_idx=i,
                                rr_config=rr_config
                            )
                            if trade:
                                signals_traded += 1
                    else:
                        signals_filtered += 1

            # Record equity
            self.equity_history.append(self.equity)

        # Close any remaining positions
        self._close_all_positions(price_data, len(price_data) - 1)

        # Calculate metrics
        metrics = self.metrics_calculator.calculate_metrics(
            self.trades,
            self.config.initial_capital
        )

        # Calculate equity and drawdown curves
        equity_curve = np.array(self.equity_history)
        drawdown_curve = self._calculate_drawdown_curve(equity_curve)

        # Segmented metrics
        segments = self.metrics_calculator.segment_metrics(
            self.trades,
            self.config.initial_capital
        )

        result = BacktestResult(
            config=self.config,
            trades=self.trades,
            metrics=metrics,
            equity_curve=equity_curve,
            drawdown_curve=drawdown_curve,
            metrics_by_horizon=segments.get('by_horizon', {}),
            metrics_by_rr=segments.get('by_rr_config', {}),
            metrics_by_amd=segments.get('by_amd_phase', {}),
            metrics_by_volatility=segments.get('by_volatility', {}),
            total_bars=total_bars,
            signals_generated=signals_generated,
            signals_filtered=signals_filtered,
            signals_traded=signals_traded
        )

        logger.info(f"Backtest complete: {len(self.trades)} trades, "
                   f"Net P&L: ${metrics.net_profit:,.2f}, "
                   f"Win Rate: {metrics.winrate:.2%}")

        return result

    def simulate_trade(
        self,
        entry_price: float,
        sl_distance: float,
        tp_distance: float,
        direction: str,
        price_data: pd.DataFrame,
        entry_bar_idx: int,
        max_bars: int = None
    ) -> Tuple[str, float, int]:
        """
        Simulate a single trade and determine outcome

        Args:
            entry_price: Entry price
            sl_distance: Stop loss distance in price units
            tp_distance: Take profit distance in price units
            direction: 'long' or 'short'
            price_data: OHLCV data
            entry_bar_idx: Bar index of entry
            max_bars: Maximum bars to hold (timeout)

        Returns:
            Tuple of (result, exit_price, bars_held)
            result is 'tp', 'sl', or 'timeout'
        """
        if max_bars is None:
            max_bars = self.config.max_position_time // 5  # Assume 5m bars

        if direction == 'long':
            sl_price = entry_price - sl_distance
            tp_price = entry_price + tp_distance
        else:
            sl_price = entry_price + sl_distance
            tp_price = entry_price - tp_distance

        # Iterate through subsequent bars
        for i in range(1, min(max_bars + 1, len(price_data) - entry_bar_idx)):
            bar_idx = entry_bar_idx + i
            bar = price_data.iloc[bar_idx]

            high = bar['high']
            low = bar['low']

            if direction == 'long':
                # Check SL first (conservative)
                if low <= sl_price:
                    return 'sl', sl_price, i
                # Check TP
                if high >= tp_price:
                    return 'tp', tp_price, i
            else:  # short
                # Check SL first
                if high >= sl_price:
                    return 'sl', sl_price, i
                # Check TP
                if low <= tp_price:
                    return 'tp', tp_price, i

        # Timeout - exit at current price
        exit_bar = price_data.iloc[min(entry_bar_idx + max_bars, len(price_data) - 1)]
        return 'timeout', exit_bar['close'], max_bars

    def _reset_state(self):
        """Reset backtester state"""
        self.trades = []
        self.open_positions = []
        self.equity = self.config.initial_capital
        self.equity_history = [self.config.initial_capital]
        self.trade_id_counter = 0

    def _should_trade(self, signal: pd.Series) -> bool:
        """Check if signal passes filters"""
        # Confidence filter
        prob = signal.get('prob_tp_first', 0)
        if prob < self.config.min_confidence:
            return False

        # AMD filter
        if self.config.filter_by_amd:
            amd_phase = signal.get('amd_phase')
            if amd_phase and amd_phase not in self.config.favorable_amd_phases:
                return False

        # Volatility filter
        if self.config.filter_by_volatility:
            vol_regime = signal.get('volatility_regime')
            if vol_regime == 'low' and self.config.min_volatility_regime != 'low':
                return False

        return True

    def _open_trade(
        self,
        signal: pd.Series,
        price_data: pd.DataFrame,
        bar_idx: int,
        rr_config: Dict = None
    ) -> Optional[TradeRecord]:
        """Open a new trade"""
        entry_bar = price_data.iloc[bar_idx]
        entry_time = price_data.index[bar_idx]
        entry_price = entry_bar['close']

        # Apply slippage
        slippage = entry_price * self.config.slippage_pct
        direction = signal.get('direction', 'long')

        if direction == 'long':
            entry_price += slippage
        else:
            entry_price -= slippage

        # Get R:R config
        if rr_config is None:
            rr_name = signal.get('rr_config', 'rr_2_1')
            rr_config = next(
                (r for r in self.config.rr_configs if r['name'] == rr_name),
                self.config.rr_configs[0]
            )

        sl_distance = rr_config['sl']
        tp_distance = rr_config['tp']

        # Calculate position size based on risk
        risk_amount = self.equity * self.config.risk_per_trade
        position_size = risk_amount / sl_distance

        # Simulate the trade
        result, exit_price, bars_held = self.simulate_trade(
            entry_price=entry_price,
            sl_distance=sl_distance,
            tp_distance=tp_distance,
            direction=direction,
            price_data=price_data,
            entry_bar_idx=bar_idx
        )

        # Calculate P&L
        if direction == 'long':
            pnl = (exit_price - entry_price) * position_size
        else:
            pnl = (entry_price - exit_price) * position_size

        # Apply commission
        commission = abs(pnl) * self.config.commission_pct
        pnl -= commission

        # Calculate R multiple
        pnl_r = pnl / risk_amount

        # Exit time
        exit_bar_idx = min(bar_idx + bars_held, len(price_data) - 1)
        exit_time = price_data.index[exit_bar_idx]

        # Create trade record
        self.trade_id_counter += 1
        trade = TradeRecord(
            id=self.trade_id_counter,
            entry_time=entry_time,
            exit_time=exit_time,
            direction=direction,
            entry_price=entry_price,
            exit_price=exit_price,
            sl_price=entry_price - sl_distance if direction == 'long' else entry_price + sl_distance,
            tp_price=entry_price + tp_distance if direction == 'long' else entry_price - tp_distance,
            sl_distance=sl_distance,
            tp_distance=tp_distance,
            rr_config=rr_config['name'],
            result=result,
            pnl=pnl,
            pnl_pct=pnl / self.equity * 100,
            pnl_r=pnl_r,
            duration_minutes=bars_held * 5,  # Assume 5m bars
            horizon=signal.get('horizon', '15m'),
            amd_phase=signal.get('amd_phase'),
            volatility_regime=signal.get('volatility_regime'),
            confidence=signal.get('confidence', 0),
            prob_tp_first=signal.get('prob_tp_first', 0)
        )

        # Update equity
        self.equity += pnl

        # Add to trades
        self.trades.append(trade)

        return trade

    def _update_positions(self, price_data: pd.DataFrame, bar_idx: int):
        """Update open positions (not used in simplified version)"""
        pass

    def _close_all_positions(self, price_data: pd.DataFrame, bar_idx: int):
        """Close all open positions (not used in simplified version)"""
        pass

    def _calculate_drawdown_curve(self, equity_curve: np.ndarray) -> np.ndarray:
        """Calculate drawdown at each point"""
        running_max = np.maximum.accumulate(equity_curve)
        drawdown = (running_max - equity_curve) / running_max
        return drawdown

    def run_walk_forward_backtest(
        self,
        price_data: pd.DataFrame,
        signals: pd.DataFrame,
        n_splits: int = 5,
        train_pct: float = 0.7
    ) -> List[BacktestResult]:
        """
        Run walk-forward backtest

        Args:
            price_data: Full price data
            signals: Full signals data
            n_splits: Number of walk-forward splits
            train_pct: Percentage of each window for training

        Returns:
            List of BacktestResult for each test period
        """
        results = []
        total_len = len(price_data)
        window_size = total_len // n_splits

        for i in range(n_splits):
            start_idx = i * window_size
            end_idx = min((i + 2) * window_size, total_len)

            # Split into train/test
            train_end = start_idx + int(window_size * train_pct)
            test_start = train_end
            test_end = end_idx

            # Use test period for backtest
            test_prices = price_data.iloc[test_start:test_end]
            test_signals = signals.iloc[test_start:test_end]

            logger.info(f"Walk-forward split {i+1}/{n_splits}: "
                       f"Test {test_start}-{test_end} ({len(test_prices)} bars)")

            # Run backtest on test period
            result = self.run_backtest(test_prices, test_signals)
            results.append(result)

        return results


def create_sample_signals(price_data: pd.DataFrame) -> pd.DataFrame:
    """Create sample signals for testing"""
    import numpy as np

    n = len(price_data)
    signals = pd.DataFrame(index=price_data.index)

    # Generate random signals (for testing only)
    np.random.seed(42)

    # Only generate signals for ~20% of bars
    signal_mask = np.random.rand(n) < 0.2

    signals['prob_tp_first'] = np.where(signal_mask, np.random.uniform(0.4, 0.7, n), np.nan)
    signals['direction'] = 'long'
    signals['horizon'] = np.random.choice(['15m', '1h'], n)
    signals['rr_config'] = np.random.choice(['rr_2_1', 'rr_3_1'], n)
    signals['amd_phase'] = np.random.choice(
        ['accumulation', 'manipulation', 'distribution', 'neutral'], n
    )
    signals['volatility_regime'] = np.random.choice(['low', 'medium', 'high'], n)
    signals['confidence'] = np.random.uniform(0.4, 0.8, n)

    return signals


if __name__ == "__main__":
    # Test backtester
    import numpy as np

    # Create sample price data
    np.random.seed(42)
    n_bars = 1000

    dates = pd.date_range(start='2024-01-01', periods=n_bars, freq='5min')
    base_price = 2000

    # Generate realistic price movements
    returns = np.random.randn(n_bars) * 0.001
    prices = base_price * np.cumprod(1 + returns)

    price_data = pd.DataFrame({
        'open': prices,
        'high': prices * (1 + abs(np.random.randn(n_bars) * 0.001)),
        'low': prices * (1 - abs(np.random.randn(n_bars) * 0.001)),
        'close': prices * (1 + np.random.randn(n_bars) * 0.0005),
        'volume': np.random.randint(1000, 10000, n_bars)
    }, index=dates)

    # Ensure OHLC consistency
    price_data['high'] = price_data[['open', 'high', 'close']].max(axis=1)
    price_data['low'] = price_data[['open', 'low', 'close']].min(axis=1)

    # Create sample signals
    signals = create_sample_signals(price_data)

    # Run backtest
    config = BacktestConfig(
        initial_capital=10000,
        risk_per_trade=0.02,
        min_confidence=0.55,
        filter_by_amd=True,
        favorable_amd_phases=['accumulation', 'distribution']
    )

    backtester = RRBacktester(config)
    result = backtester.run_backtest(price_data, signals)

    # Print results
    print("\n=== BACKTEST RESULTS ===")
    result.metrics.print_summary()

    print(f"\nTotal Bars: {result.total_bars}")
    print(f"Signals Generated: {result.signals_generated}")
    print(f"Signals Filtered: {result.signals_filtered}")
    print(f"Signals Traded: {result.signals_traded}")

    print("\n=== Metrics by R:R ===")
    for rr, m in result.metrics_by_rr.items():
        print(f"{rr}: WR={m.winrate:.2%}, PF={m.profit_factor:.2f}, N={m.total_trades}")

    print("\n=== Metrics by AMD Phase ===")
    for phase, m in result.metrics_by_amd.items():
        print(f"{phase}: WR={m.winrate:.2%}, PF={m.profit_factor:.2f}, N={m.total_trades}")
