# src/backtest/optimizer.py
"""
Optimizador de parámetros para estrategias
"""
import os
import pandas as pd
from typing import Dict, List, Any, Type, Optional
from itertools import product
from ..utils.logger import log
from .engine import Engine
from .strategy import Strategy
from src.risk.sizing.base import PositionSizer
from src.risk.stops.base import StopLoss

class ParameterOptimizer:
    """
    Optimiza parámetros de estrategias mediante grid search
    """
    
    def __init__(self, 
                 strategy_class: Type[Strategy],
                 data: pd.DataFrame,
                 initial_capital: float = 10000,
                 commission: float = 0.001,
                 slippage: float = 0.0005,
                 position_size: float = 0.95,
                 position_sizer: Optional[PositionSizer] = None,
                 stop_loss: Optional[StopLoss] = None,
                 max_combinations: Optional[int] = None):
        """
        Args:
            strategy_class: Clase de estrategia (no instancia)
            data: Datos para backtest
            initial_capital: Capital inicial
            commission: Comisión por trade
            slippage: Slippage simulado
            position_size: Fracción del capital por trade
        """
        self.strategy_class = strategy_class
        self.data = data
        self.initial_capital = initial_capital
        self.commission = commission
        self.slippage = slippage
        self.position_size = position_size
        self.position_sizer = position_sizer
        self.stop_loss = stop_loss
        self.max_combinations = max_combinations
        
        self.results: List[Dict] = []
    
    def optimize(self, param_grid: Dict[str, List[Any]]) -> pd.DataFrame:
        """
        Optimiza parámetros mediante grid search
        
        Args:
            param_grid: Diccionario con parámetros a probar
                Ejemplo: {
                    'fast_period': [5, 10, 15, 20],
                    'slow_period': [30, 50, 100],
                    'ma_type': ['sma', 'ema']
                }
        
        Returns:
            DataFrame con resultados (sin ordenar)
        """
        # Limpiar resultados previos
        self.results = []
        
        # Generar todas las combinaciones posibles
        param_names = list(param_grid.keys())
        param_values = list(param_grid.values())
        combinations = list(product(*param_values))

        if self.max_combinations is not None:
            combinations = combinations[: int(self.max_combinations)]
        
        total_tests = len(combinations)
        log.info(f"🔧 Iniciando optimización: {total_tests} combinaciones")
        
        # Probar cada combinación
        successful = 0
        failed = 0
        
        for i, values in enumerate(combinations, 1):
            params = dict(zip(param_names, values))
            
            # Mostrar progreso cada 10 tests o en el primero
            if i % 10 == 0 or i == 1 or i == total_tests:
                log.info(f"   [{i}/{total_tests}] Probando: {params}")
            
            try:
                # Crear estrategia con estos parámetros
                strategy = self.strategy_class(**params)
                
                # Ejecutar backtest
                engine = Engine(
                    strategy=strategy,
                    initial_capital=self.initial_capital,
                    commission=self.commission,
                    slippage=self.slippage,
                    position_size=self.position_size,
                    position_sizer=self.position_sizer,
                    stop_loss=self.stop_loss,
                )
                
                results = engine.run(self.data)
                
                # Guardar resultados
                result_entry = {
                    **params,  # Parámetros probados
                    'total_return_pct': results['total_return_pct'],
                    'sharpe_ratio': results['sharpe_ratio'],
                    'max_drawdown_pct': results['max_drawdown_pct'],
                    'total_trades': results['total_trades'],
                    'win_rate_pct': results['win_rate_pct'],
                    'profit_factor': results['profit_factor'],
                    'final_equity': results['final_equity'],
                }
                
                self.results.append(result_entry)
                successful += 1
                
            except Exception as e:
                log.error(f"❌ Error con {params}: {e}")
                failed += 1
                continue
        
        # Convertir a DataFrame
        if not self.results:
            log.error("❌ No se obtuvieron resultados válidos")
            return pd.DataFrame()
        
        df_results = pd.DataFrame(self.results)
        
        log.success(f"✅ Optimización completa: {successful} exitosos, {failed} fallidos")
        
        # Mostrar top 5 por Sharpe Ratio
        self._print_top_results(df_results, param_names, metric='sharpe_ratio', top_n=5)
        
        return df_results
    
    def _print_top_results(self, df: pd.DataFrame, param_names: List[str], 
                          metric: str = 'sharpe_ratio', top_n: int = 5):
        """
        Muestra los mejores resultados según una métrica
        """
        # Ordenar por la métrica (descendente para la mayoría)
        ascending = True if metric in ['max_drawdown_pct'] else False
        df_sorted = df.sort_values(metric, ascending=ascending)
        
        log.info(f"\n🏆 TOP {top_n} POR {metric.upper()}:")
        
        for i, (idx, row) in enumerate(df_sorted.head(top_n).iterrows(), 1):
            param_str = ", ".join([f"{k}={row[k]}" for k in param_names])
            log.info(f"   #{i}: {param_str}")
            log.info(f"        → {metric}: {row[metric]:.2f}, "
                    f"Return: {row['total_return_pct']:.2f}%, "
                    f"Trades: {int(row['total_trades'])}, "
                    f"Win Rate: {row['win_rate_pct']:.1f}%")
    
    def get_best_params(self, metric: str = 'sharpe_ratio') -> Dict:
        """
        Obtiene los mejores parámetros según una métrica
        
        Args:
            metric: Métrica a optimizar 
                ('sharpe_ratio', 'total_return_pct', 'profit_factor', etc)
        
        Returns:
            Diccionario con los mejores parámetros
        """
        if not self.results:
            raise ValueError("No hay resultados. Ejecuta optimize() primero.")
        
        df_results = pd.DataFrame(self.results)
        
        # Para drawdown queremos el MENOR (menos negativo = más cercano a 0)
        if metric in ['max_drawdown_pct']:
            best_idx = df_results[metric].idxmax()
        else:
            # Para otras métricas, queremos el MAYOR
            best_idx = df_results[metric].idxmax()
        
        best_result = df_results.loc[best_idx]
        
        # Extraer solo parámetros (no métricas)
        metric_cols = ['total_return_pct', 'sharpe_ratio', 'max_drawdown_pct', 
                       'total_trades', 'win_rate_pct', 'profit_factor', 'final_equity']
        param_names = [col for col in df_results.columns if col not in metric_cols]
        
        best_params = {param: best_result[param] for param in param_names}
        
        log.info(f"\n🎯 MEJORES PARÁMETROS SEGÚN {metric.upper()}:")
        log.info(f"   Parámetros: {best_params}")
        log.info(f"   {metric}: {best_result[metric]:.2f}")
        log.info(f"   Retorno: {best_result['total_return_pct']:.2f}%")
        log.info(f"   Sharpe: {best_result['sharpe_ratio']:.2f}")
        log.info(f"   Max DD: {best_result['max_drawdown_pct']:.2f}%")
        log.info(f"   Trades: {int(best_result['total_trades'])}")
        
        return best_params
    
    def get_top_n_params(self, metric: str = 'sharpe_ratio', n: int = 5) -> List[Dict]:
        """
        Obtiene los N mejores conjuntos de parámetros
        
        Args:
            metric: Métrica a optimizar
            n: Número de mejores configuraciones
        
        Returns:
            Lista de diccionarios con parámetros
        """
        if not self.results:
            raise ValueError("No hay resultados. Ejecuta optimize() primero.")
        
        df_results = pd.DataFrame(self.results)
        
        # Ordenar
        ascending = True if metric in ['max_drawdown_pct'] else False
        df_sorted = df_results.sort_values(metric, ascending=ascending)
        
        # Extraer parámetros
        metric_cols = ['total_return_pct', 'sharpe_ratio', 'max_drawdown_pct', 
                       'total_trades', 'win_rate_pct', 'profit_factor', 'final_equity']
        param_names = [col for col in df_results.columns if col not in metric_cols]
        
        top_params = []
        for idx, row in df_sorted.head(n).iterrows():
            params = {param: row[param] for param in param_names}
            top_params.append(params)
        
        return top_params
    
    def save_results(self, filename: str = 'backtest_results/optimizer/optimization_results.csv'):
        """
        Guarda resultados en CSV
        
        Args:
            filename: Nombre del archivo (puede incluir ruta)
        """
        if not self.results:
            log.warning("⚠️  No hay resultados para guardar")
            return
        
        # Crear directorio si no existe
        os.makedirs(os.path.dirname(filename), exist_ok=True)
        
        df = pd.DataFrame(self.results)
        
        # Ordenar por Sharpe Ratio antes de guardar
        df = df.sort_values('sharpe_ratio', ascending=False)
        
        df.to_csv(filename, index=False)
        log.success(f"💾 Resultados guardados en: {filename}")
        log.info(f"   Total filas: {len(df)}")
    
    def compare_metrics(self, metrics: List[str] = None):
        """
        Compara mejores parámetros según diferentes métricas
        
        Args:
            metrics: Lista de métricas a comparar
        """
        if not self.results:
            raise ValueError("No hay resultados. Ejecuta optimize() primero.")
        
        if metrics is None:
            metrics = ['sharpe_ratio', 'total_return_pct', 'profit_factor', 'max_drawdown_pct']
        
        log.info("\n📊 COMPARACIÓN DE MÉTRICAS:")
        log.info("="*70)
        
        for metric in metrics:
            try:
                best_params = self.get_best_params(metric)
                log.info("")  # Línea en blanco para separar
            except Exception as e:
                log.error(f"Error con métrica {metric}: {e}")