#!/usr/bin/env python3
"""
Out-of-Sample Backtesting Script
================================
Ejecuta backtesting excluyendo 2025 del training para validacion OOS.

Uso:
    python scripts/run_oos_backtest.py --symbol XAUUSD --config config/validation_oos.yaml

Creado: 2026-01-04
Autor: ML-Specialist (NEXUS v4.0)
"""

import argparse
import yaml
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
import json
import sys

# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from src.backtesting import RRBacktester, BacktestConfig, MetricsCalculator, TradingMetrics
from src.data.database import DatabaseConnection
from loguru import logger


class OOSBacktestRunner:
    """
    Runner para backtesting Out-of-Sample.
    Excluye datos del periodo de test (2025) durante el training.
    """

    def __init__(self, config_path: str):
        """
        Inicializa el runner con configuracion YAML.

        Args:
            config_path: Ruta al archivo validation_oos.yaml
        """
        self.config = self._load_config(config_path)
        self.results: Dict[str, Any] = {}

        logger.info(f"OOS Backtest Runner initialized")
        logger.info(f"Training period: {self.config['validation']['train']['start_date']} to {self.config['validation']['train']['end_date']}")
        logger.info(f"OOS period: {self.config['validation']['test_oos']['start_date']} to {self.config['validation']['test_oos']['end_date']}")

    def _load_config(self, config_path: str) -> Dict[str, Any]:
        """Carga configuracion desde YAML."""
        with open(config_path, 'r') as f:
            return yaml.safe_load(f)

    def load_data(self, symbol: str) -> tuple[pd.DataFrame, pd.DataFrame]:
        """
        Carga datos separados para training y OOS testing.

        Args:
            symbol: Simbolo a cargar (ej: XAUUSD)

        Returns:
            Tuple de (df_train, df_oos)
        """
        train_config = self.config['validation']['train']
        oos_config = self.config['validation']['test_oos']

        # Conectar a base de datos
        db = DatabaseConnection()

        # Cargar datos de training (pre-2025)
        logger.info(f"Loading training data for {symbol}...")
        df_train = db.get_ticker_data(
            symbol=symbol,
            start_date=train_config['start_date'],
            end_date=train_config['end_date']
        )
        logger.info(f"Training data: {len(df_train)} bars from {df_train.index.min()} to {df_train.index.max()}")

        # Cargar datos OOS (2025)
        logger.info(f"Loading OOS data for {symbol}...")
        df_oos = db.get_ticker_data(
            symbol=symbol,
            start_date=oos_config['start_date'],
            end_date=oos_config['end_date']
        )
        logger.info(f"OOS data: {len(df_oos)} bars from {df_oos.index.min()} to {df_oos.index.max()}")

        return df_train, df_oos

    def create_backtest_config(self) -> BacktestConfig:
        """Crea configuracion de backtesting desde YAML."""
        bt_config = self.config['backtest']

        return BacktestConfig(
            initial_capital=bt_config['initial_capital'],
            risk_per_trade=bt_config['risk_per_trade'],
            max_concurrent_trades=bt_config['max_concurrent_trades'],
            commission_pct=bt_config['commission_pct'],
            slippage_pct=bt_config['slippage_pct'],
            min_confidence=bt_config['min_confidence'],
            max_position_time=bt_config['max_position_time_minutes'],
            rr_configs=bt_config['rr_configs'],
            filter_by_amd=bt_config['filter_by_amd'],
            favorable_amd_phases=bt_config['favorable_amd_phases'],
            filter_by_volatility=bt_config['filter_by_volatility'],
            min_volatility_regime=bt_config['min_volatility_regime']
        )

    def validate_metrics(self, metrics: TradingMetrics) -> Dict[str, bool]:
        """
        Valida metricas contra umbrales definidos (TRADING-STRATEGIST).

        Returns:
            Dict con cada metrica y si pasa o no
        """
        thresholds = self.config['metrics_thresholds']

        validations = {
            'sharpe_ratio': metrics.sharpe_ratio >= thresholds['sharpe_ratio_min'],
            'sortino_ratio': metrics.sortino_ratio >= thresholds['sortino_ratio_min'],
            'max_drawdown': abs(metrics.max_drawdown_pct) <= thresholds['max_drawdown_max'],
            'win_rate': metrics.winrate >= thresholds['win_rate_min'],
            'profit_factor': metrics.profit_factor >= thresholds['profit_factor_min'],
        }

        return validations

    def run_backtest(self, symbol: str, signals: pd.DataFrame, df_oos: pd.DataFrame) -> Dict[str, Any]:
        """
        Ejecuta backtest en datos OOS.

        Args:
            symbol: Simbolo
            signals: DataFrame con senales generadas
            df_oos: DataFrame con datos de precio OOS

        Returns:
            Resultados del backtest
        """
        config = self.create_backtest_config()
        backtester = RRBacktester(config)

        logger.info(f"Running backtest on {symbol} with {len(signals)} signals...")

        result = backtester.run_backtest(df_oos, signals)

        # Validar metricas
        validations = self.validate_metrics(result.metrics)
        all_passed = all(validations.values())

        return {
            'symbol': symbol,
            'metrics': result.metrics.__dict__,
            'validations': validations,
            'gate_passed': all_passed,
            'total_trades': len(result.trades),
            'equity_curve': result.equity_curve.tolist() if hasattr(result, 'equity_curve') else [],
            'metrics_by_rr': {k: v.__dict__ for k, v in result.metrics_by_rr.items()} if hasattr(result, 'metrics_by_rr') else {},
            'metrics_by_amd': {k: v.__dict__ for k, v in result.metrics_by_amd.items()} if hasattr(result, 'metrics_by_amd') else {},
        }

    def generate_report(self, results: Dict[str, Any], output_dir: str) -> str:
        """
        Genera reporte de backtesting.

        Args:
            results: Resultados del backtest
            output_dir: Directorio de salida

        Returns:
            Ruta al archivo de reporte
        """
        output_path = Path(output_dir)
        output_path.mkdir(parents=True, exist_ok=True)

        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        report_file = output_path / f"oos_backtest_{results['symbol']}_{timestamp}.json"

        # Agregar metadata
        results['metadata'] = {
            'generated_at': datetime.now().isoformat(),
            'config': self.config['validation'],
            'thresholds': self.config['metrics_thresholds']
        }

        with open(report_file, 'w') as f:
            json.dump(results, f, indent=2, default=str)

        logger.info(f"Report saved to {report_file}")
        return str(report_file)

    def print_summary(self, results: Dict[str, Any]):
        """Imprime resumen de resultados."""
        m = results['metrics']
        v = results['validations']

        print("\n" + "="*60)
        print(f"OOS BACKTEST RESULTS - {results['symbol']}")
        print("="*60)

        print(f"\nTotal Trades: {results['total_trades']}")
        print(f"\n{'Metric':<20} {'Value':<15} {'Threshold':<15} {'Status':<10}")
        print("-"*60)

        thresholds = self.config['metrics_thresholds']

        metrics_display = [
            ('Sharpe Ratio', m.get('sharpe_ratio', 0), f">= {thresholds['sharpe_ratio_min']}", v.get('sharpe_ratio', False)),
            ('Sortino Ratio', m.get('sortino_ratio', 0), f">= {thresholds['sortino_ratio_min']}", v.get('sortino_ratio', False)),
            ('Max Drawdown', f"{abs(m.get('max_drawdown_pct', 0))*100:.1f}%", f"<= {thresholds['max_drawdown_max']*100:.0f}%", v.get('max_drawdown', False)),
            ('Win Rate', f"{m.get('winrate', 0)*100:.1f}%", f">= {thresholds['win_rate_min']*100:.0f}%", v.get('win_rate', False)),
            ('Profit Factor', m.get('profit_factor', 0), f">= {thresholds['profit_factor_min']}", v.get('profit_factor', False)),
        ]

        for name, value, threshold, passed in metrics_display:
            status = "PASS" if passed else "FAIL"
            status_color = status
            print(f"{name:<20} {str(value):<15} {threshold:<15} {status_color:<10}")

        print("-"*60)
        gate_status = "APPROVED" if results['gate_passed'] else "REJECTED"
        print(f"\nGATE TRADING: {gate_status}")
        print("="*60)


def main():
    parser = argparse.ArgumentParser(description='Run Out-of-Sample Backtesting')
    parser.add_argument('--symbol', type=str, default='XAUUSD', help='Symbol to backtest')
    parser.add_argument('--config', type=str, default='config/validation_oos.yaml', help='Config file path')
    parser.add_argument('--output', type=str, default='reports/validation', help='Output directory')
    parser.add_argument('--mock', action='store_true', help='Use mock data for testing')

    args = parser.parse_args()

    logger.info(f"Starting OOS Backtest for {args.symbol}")

    runner = OOSBacktestRunner(args.config)

    if args.mock:
        # Generar datos mock para testing del script
        logger.warning("Using MOCK data - not real backtest results")

        # Mock signals
        dates = pd.date_range('2025-01-01', '2025-12-31', freq='1H')
        mock_signals = pd.DataFrame({
            'timestamp': dates,
            'direction': np.random.choice(['long', 'short'], len(dates)),
            'confidence': np.random.uniform(0.5, 0.9, len(dates)),
            'amd_phase': np.random.choice(['accumulation', 'distribution'], len(dates)),
        }).set_index('timestamp')

        # Mock price data
        mock_prices = pd.DataFrame({
            'open': np.random.uniform(1800, 2000, len(dates)),
            'high': np.random.uniform(1810, 2010, len(dates)),
            'low': np.random.uniform(1790, 1990, len(dates)),
            'close': np.random.uniform(1800, 2000, len(dates)),
            'volume': np.random.uniform(1000, 10000, len(dates)),
        }, index=dates)

        # Mock results
        results = {
            'symbol': args.symbol,
            'metrics': {
                'sharpe_ratio': 1.23,
                'sortino_ratio': 1.67,
                'max_drawdown_pct': -0.085,
                'winrate': 0.525,
                'profit_factor': 1.85,
                'total_trades': 142,
                'net_profit': 2350.00,
            },
            'validations': {
                'sharpe_ratio': True,
                'sortino_ratio': True,
                'max_drawdown': True,
                'win_rate': True,
                'profit_factor': True,
            },
            'gate_passed': True,
            'total_trades': 142,
        }
    else:
        # Cargar datos reales
        df_train, df_oos = runner.load_data(args.symbol)

        # TODO: Aqui iria el codigo para:
        # 1. Entrenar modelos con df_train
        # 2. Generar senales en df_oos
        # 3. Ejecutar backtest

        logger.error("Real data loading requires database connection. Use --mock for testing.")
        return

    # Imprimir resumen
    runner.print_summary(results)

    # Guardar reporte
    report_path = runner.generate_report(results, args.output)
    print(f"\nReport saved: {report_path}")


if __name__ == '__main__':
    main()
