# src/data/processor.py
"""
Módulo para limpiar, validar y procesar datos de mercado
"""
import pandas as pd
import pandas_ta as ta
import numpy as np
from typing import Optional
from ..utils.logger import log

class DataProcessor:
    """
    Clase para procesar y limpiar datos de mercado
    """
    
    @staticmethod
    def validate_ohlcv(df: pd.DataFrame) -> bool:
        """
        Valida que un DataFrame tenga estructura OHLCV válida
        
        Args:
            df: DataFrame a validar
            
        Returns:
            True si es válido, False si no
        """
        required_columns = ['open', 'high', 'low', 'close', 'volume']
        
        # Verificar columnas requeridas
        if not all(col in df.columns for col in required_columns):
            log.error(f"Faltan columnas requeridas. Presentes: {df.columns.tolist()}")
            return False
        
        # Verificar que el índice sea datetime
        if not isinstance(df.index, pd.DatetimeIndex):
            log.error("El índice debe ser DatetimeIndex")
            return False
        
        # Verificar valores numéricos
        for col in required_columns:
            if not pd.api.types.is_numeric_dtype(df[col]):
                log.error(f"Columna {col} no es numérica")
                return False
        
        # Verificar relaciones lógicas: high >= low, etc
        invalid_rows = (df['high'] < df['low']) | (df['high'] < df['open']) | (df['high'] < df['close'])
        if invalid_rows.any():
            log.warning(f"Encontradas {invalid_rows.sum()} filas con valores ilógicos")
            return False
        
        log.debug("Validación OHLCV exitosa")
        return True
    
    @staticmethod
    def clean_data(df: pd.DataFrame, remove_duplicates: bool = True) -> pd.DataFrame:
        """
        Limpia datos: duplicados, NaN, outliers
        
        Args:
            df: DataFrame a limpiar
            remove_duplicates: Si eliminar duplicados
            
        Returns:
            DataFrame limpio
        """
        log.info(f"Limpiando datos. Filas iniciales: {len(df)}")
        df_clean = df.copy()
        
        # Eliminar duplicados en el índice
        if remove_duplicates:
            before = len(df_clean)
            df_clean = df_clean[~df_clean.index.duplicated(keep='first')]
            removed = before - len(df_clean)
            if removed > 0:
                log.info(f"Eliminados {removed} duplicados")
        
        # Ordenar por índice
        df_clean.sort_index(inplace=True)
        
        # Manejar valores faltantes
        nan_count = df_clean.isnull().sum().sum()
        if nan_count > 0:
            log.warning(f"Encontrados {nan_count} valores NaN")
            
            # Forward fill para datos de serie temporal
            df_clean.fillna(method='ffill', inplace=True)
            
            # Si aún quedan NaN al inicio, usar backward fill
            df_clean.fillna(method='bfill', inplace=True)
        
        # Detectar y manejar outliers extremos (más de 100x el precio medio)
        for col in ['open', 'high', 'low', 'close']:
            if col in df_clean.columns:
                median = df_clean[col].median()
                outliers = (df_clean[col] > median * 100) | (df_clean[col] < median / 100)
                outlier_count = outliers.sum()
                
                if outlier_count > 0:
                    log.warning(f"Detectados {outlier_count} outliers en {col}")
                    # Reemplazar outliers con el valor anterior válido
                    df_clean.loc[outliers, col] = np.nan
                    df_clean[col].fillna(method='ffill', inplace=True)
        
        # Verificar volumen negativo
        if 'volume' in df_clean.columns:
            negative_vol = df_clean['volume'] < 0
            if negative_vol.any():
                log.warning(f"Encontrados {negative_vol.sum()} volúmenes negativos")
                df_clean.loc[negative_vol, 'volume'] = 0
        
        log.success(f"Limpieza completa. Filas finales: {len(df_clean)}")
        return df_clean
    
    @staticmethod
    def resample_timeframe(
        df: pd.DataFrame, 
        target_timeframe: str,
        original_timeframe: Optional[str] = None
    ) -> pd.DataFrame:
        """
        Resamplea datos a un timeframe diferente
        
        Args:
            df: DataFrame con datos OHLCV
            target_timeframe: Timeframe destino ('5m', '15m', '1h', '4h', '1d')
            original_timeframe: Timeframe original (opcional, para validación)
            
        Returns:
            DataFrame resampled
        """
        log.info(f"Resampling a {target_timeframe}")
        
        # Diccionario de conversión de timeframe a pandas offset
        timeframe_map = {
            '1m': '1T',
            '5m': '5T',
            '15m': '15T',
            '30m': '30T',
            '1h': '1H',
            '4h': '4H',
            '1d': '1D',
            '1w': '1W'
        }
        
        if target_timeframe not in timeframe_map:
            raise ValueError(f"Timeframe {target_timeframe} no soportado")
        
        offset = timeframe_map[target_timeframe]
        
        # Resamplear usando reglas OHLC estándar
        resampled = df.resample(offset).agg({
            'open': 'first',
            'high': 'max',
            'low': 'min',
            'close': 'last',
            'volume': 'sum'
        })
        
        # Eliminar filas con NaN (periodos sin datos)
        resampled.dropna(inplace=True)
        
        # Mantener metadata si existe
        if 'symbol' in df.columns:
            resampled['symbol'] = df['symbol'].iloc[0]
        resampled['timeframe'] = target_timeframe
        
        log.success(f"Resampling completo: {len(df)} -> {len(resampled)} velas")
        return resampled
    
    @staticmethod
    def calculate_returns(df: pd.DataFrame, period: int = 1) -> pd.DataFrame:
        """
        Calcula retornos simples y logarítmicos
        
        Args:
            df: DataFrame con datos OHLCV
            period: Periodo para calcular retornos
            
        Returns:
            DataFrame con columnas de retornos añadidas
        """
        df_returns = df.copy()
        
        # Retornos simples
        df_returns['returns'] = df_returns['close'].pct_change(period)
        
        # Retornos logarítmicos (mejores para análisis estadístico)
        df_returns['log_returns'] = np.log(df_returns['close'] / df_returns['close'].shift(period))
        
        log.debug(f"Retornos calculados para periodo {period}")
        return df_returns
    
    @staticmethod
    def detect_gaps(df: pd.DataFrame, timeframe: str) -> pd.DataFrame:
        """
        Detecta gaps en los datos (periodos faltantes)
        
        Args:
            df: DataFrame con índice temporal
            timeframe: Timeframe esperado
            
        Returns:
            DataFrame con información de gaps
        """
        timeframe_delta = {
            '1m': pd.Timedelta(minutes=1),
            '5m': pd.Timedelta(minutes=5),
            '15m': pd.Timedelta(minutes=15),
            '1h': pd.Timedelta(hours=1),
            '4h': pd.Timedelta(hours=4),
            '1d': pd.Timedelta(days=1)
        }
        
        if timeframe not in timeframe_delta:
            log.warning(f"Timeframe {timeframe} no reconocido para detección de gaps")
            return pd.DataFrame()
        
        expected_delta = timeframe_delta[timeframe]
        time_diffs = df.index.to_series().diff()
        
        # Gaps son diferencias mayores al timeframe esperado
        gaps = time_diffs[time_diffs > expected_delta * 1.5]
        
        if len(gaps) > 0:
            log.warning(f"Detectados {len(gaps)} gaps en los datos")
            gap_info = pd.DataFrame({
                'gap_start': gaps.index,
                'gap_duration': gaps.values
            })
            return gap_info
        
        log.debug("No se detectaron gaps en los datos")
        return pd.DataFrame()
    
    @staticmethod
    def normalize_data(df: pd.DataFrame, method: str = 'minmax') -> pd.DataFrame:
        """
        Normaliza datos numéricos
        
        Args:
            df: DataFrame a normalizar
            method: Método de normalización ('minmax', 'zscore')
            
        Returns:
            DataFrame normalizado
        """
        df_norm = df.copy()
        numeric_cols = df_norm.select_dtypes(include=[np.number]).columns
        
        if method == 'minmax':
            # Min-Max normalization [0, 1]
            for col in numeric_cols:
                min_val = df_norm[col].min()
                max_val = df_norm[col].max()
                if max_val > min_val:
                    df_norm[col] = (df_norm[col] - min_val) / (max_val - min_val)
        
        elif method == 'zscore':
            # Z-score normalization (mean=0, std=1)
            for col in numeric_cols:
                mean = df_norm[col].mean()
                std = df_norm[col].std()
                if std > 0:
                    df_norm[col] = (df_norm[col] - mean) / std
        
        else:
            raise ValueError(f"Método {method} no soportado")
        
        log.debug(f"Datos normalizados usando {method}")
        return df_norm
