"""
Preprocesamiento de datos financieros para modelos de ML
"""

import numpy as np
import pandas as pd
from typing import Tuple, List
from sklearn.preprocessing import StandardScaler, MinMaxScaler


class FinancialPreprocessor:
    """Preprocesa series temporales financieras para modelos de ML"""
    
    def __init__(self, scaler_type: str = 'standard'):
        """
        Args:
            scaler_type: 'standard' para StandardScaler o 'minmax' para MinMaxScaler
        """
        if scaler_type == 'standard':
            self.scaler = StandardScaler()
        elif scaler_type == 'minmax':
            self.scaler = MinMaxScaler()
        else:
            raise ValueError(f"Scaler no reconocido: {scaler_type}")
        
        self.scaler_type = scaler_type
        self.is_fitted = False
    
    def create_sequences(
        self, 
        data: pd.DataFrame, 
        lookback: int = 60, 
        horizon: int = 1
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Crea secuencias de ventanas deslizantes para entrenamiento
        
        Args:
            data: DataFrame con series temporales
            lookback: Número de pasos históricos (ventana de entrada)
            horizon: Número de pasos a predecir
            
        Returns:
            Tupla (X, y) donde:
                X shape: (n_samples, lookback, n_features)
                y shape: (n_samples, horizon, n_features)
        """
        X, y = [], []
        
        data_values = data.values
        
        for i in range(len(data_values) - lookback - horizon + 1):
            X.append(data_values[i:i+lookback])
            y.append(data_values[i+lookback:i+lookback+horizon])
        
        X = np.array(X)
        y = np.array(y)
        
        # Si horizon = 1, aplanar la dimensión
        if horizon == 1:
            y = y.squeeze(1)
        
        return X, y
    
    def normalize(self, data: pd.DataFrame, fit: bool = True) -> pd.DataFrame:
        """
        Normaliza los datos
        
        Args:
            data: DataFrame a normalizar
            fit: Si True, ajusta el scaler; si False, usa scaler pre-ajustado
            
        Returns:
            DataFrame normalizado
        """
        if fit:
            scaled_values = self.scaler.fit_transform(data.values)
            self.is_fitted = True
        else:
            if not self.is_fitted:
                raise ValueError("Scaler no ha sido ajustado. Usa fit=True primero")
            scaled_values = self.scaler.transform(data.values)
        
        return pd.DataFrame(
            scaled_values, 
            index=data.index, 
            columns=data.columns
        )
    
    def inverse_normalize(self, data: np.ndarray) -> np.ndarray:
        """
        Desnormaliza los datos
        
        Args:
            data: Array normalizado
            
        Returns:
            Array en escala original
        """
        if not self.is_fitted:
            raise ValueError("Scaler no ha sido ajustado")
        
        return self.scaler.inverse_transform(data)
    
    def add_technical_indicators(self, prices: pd.DataFrame) -> pd.DataFrame:
        """
        Agrega indicadores técnicos como features adicionales
        
        Args:
            prices: DataFrame con precios históricos
            
        Returns:
            DataFrame con indicadores técnicos adicionales
        """
        df = prices.copy()
        
        for col in prices.columns:
            # Moving Averages
            df[f'{col}_MA5'] = prices[col].rolling(window=5).mean()
            df[f'{col}_MA20'] = prices[col].rolling(window=20).mean()
            
            # Relative Strength Index (RSI)
            delta = prices[col].diff()
            gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
            loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
            rs = gain / loss
            df[f'{col}_RSI'] = 100 - (100 / (1 + rs))
            
            # Bollinger Bands
            ma20 = prices[col].rolling(window=20).mean()
            std20 = prices[col].rolling(window=20).std()
            df[f'{col}_BB_upper'] = ma20 + (2 * std20)
            df[f'{col}_BB_lower'] = ma20 - (2 * std20)
            
            # Momentum
            df[f'{col}_momentum'] = prices[col].diff(10)
        
        # Drop NaN values generados por rolling windows
        df = df.dropna()
        
        return df
    
    def prepare_for_training(
        self,
        data: pd.DataFrame,
        lookback: int = 60,
        horizon: int = 30,
        normalize: bool = True,
        test_split: float = 0.2
    ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """
        Pipeline completo de preparación de datos
        
        Args:
            data: DataFrame con datos financieros
            lookback: Ventana de entrada
            horizon: Horizonte de predicción
            normalize: Si True, normaliza los datos
            test_split: Proporción de datos para test
            
        Returns:
            (X_train, y_train, X_test, y_test)
        """
        # Normalizar si se solicita
        if normalize:
            data = self.normalize(data, fit=True)
        
        # Crear secuencias
        X, y = self.create_sequences(data, lookback, horizon)
        
        # Split temporal
        split_idx = int(len(X) * (1 - test_split))
        X_train, X_test = X[:split_idx], X[split_idx:]
        y_train, y_test = y[:split_idx], y[split_idx:]
        
        print(f"✓ Datos preparados:")
        print(f"  Train: {X_train.shape[0]} muestras")
        print(f"  Test: {X_test.shape[0]} muestras")
        print(f"  Input shape: {X_train.shape[1:]}")
        print(f"  Output shape: {y_train.shape[1:]}")
        
        return X_train, y_train, X_test, y_test


if __name__ == '__main__':
    # Ejemplo de uso
    from data_loader import load_sample_data
    
    print("=" * 60)
    print("PREPROCESSING DEMO")
    print("=" * 60)
    
    # Cargar datos
    loader = load_sample_data()
    
    # Crear preprocessor
    preprocessor = FinancialPreprocessor(scaler_type='standard')
    
    # Preparar datos para entrenamiento
    X_train, y_train, X_test, y_test = preprocessor.prepare_for_training(
        data=loader.returns,
        lookback=60,
        horizon=30,
        normalize=True,
        test_split=0.2
    )
    
    print(f"\n✓ Preprocessor funcionando correctamente!")
