import pandas as pd
from dataclasses import dataclass, field
from .client import Datamart
from scipy.stats import norm

api_datamart = Datamart()

@dataclass
class TimeSeries():
    '''
    Descarga series de tiempo de precios y retornos de fondos mutuos.
    '''
    df_data : pd.DataFrame = None
    list_runs : list = None
    ignore_name_change : bool = False
    time_series_info : pd.DataFrame = None
    time_series_matrix : dict = field(default_factory=dict)
    start_date : str = None
    end_date : str = None
    ccy : str = 'clp'
    categ : str = None
    drop_weekends : bool = True
    rolling_window : int = None
    
    def __post_init__(self) -> None:
        '''
        Método que se ejecuta automáticamente al instanciar la clase
        '''
        # Se obtiene la información de los runs disponibles
        self.time_series_info = self.runs_info()
        
        # Definir fechas
        if self.start_date is None:
            self.start_date = self.time_series_info['FECHA_MIN'].min()
        if self.end_date is None:
            self.end_date = self.time_series_info['FECHA_MAX'].max()
        
        # Decarga de valores cuota
        df_data: pd.DataFrame = self.df_data
        df_data['FECHA'] = pd.to_datetime(df_data['FECHA'], errors='coerce')
        df_data = df_data[df_data['FECHA'].notna()].reset_index(drop=True)
        if self.categ:
            df_data = df_data[df_data['CATEGORIA']==self.categ].reset_index(drop=True)
        if self.drop_weekends:
            df_data = df_data[~df_data['FECHA'].dt.dayofweek.isin([5,6])].reset_index(drop=True)
        
        # ==== Retornos diarios ====
        pivot_dtd = pd.pivot_table(
            df_data,
            index=['FECHA','MONEDA_RETORNO'], 
            columns='RUN', 
            aggfunc='max',
            values='DTD').reset_index()
        
        # ==== Retornos semanales (viernes contra viernes) ====
        df_data_wtd = df_data.copy()
        # Filtrar solo viernes (dayofweek = 4)
        df_data_wtd = df_data_wtd[df_data_wtd['FECHA'].dt.dayofweek == 4].reset_index(drop=True)
        # Calcular retorno semanal
        df_data_wtd['WEEK'] = df_data_wtd.groupby('RUN')['VALORCUOTA'].pct_change()
        
        pivot_wtd = pd.pivot_table(
            df_data_wtd,
            index=['FECHA','MONEDA_RETORNO'], 
            columns='RUN', 
            aggfunc='max',
            values='WEEK').reset_index()
        
        # ==== Retornos mensuales (último día del mes) ====
        df_data_mtd = df_data.copy()
        # Identificar último día del mes
        df_data_mtd['is_month_end'] = df_data_mtd['FECHA'].dt.is_month_end
        df_data_mtd = df_data_mtd[df_data_mtd['is_month_end']].reset_index(drop=True)
        # Calcular retorno mensual
        df_data_mtd['MONTH'] = df_data_mtd.groupby('RUN')['VALORCUOTA'].pct_change()
        
        pivot_mtd = pd.pivot_table(
            df_data_mtd,
            index=['FECHA','MONEDA_RETORNO'], 
            columns='RUN', 
            aggfunc='max',
            values='MTD').reset_index()
        
        # === Retornos Rolling ===
        if self.rolling_window is not None:
            df_data_rolling = df_data.copy()
            df_data_rolling = df_data_rolling.sort_values(by=['RUN', 'FECHA']).reset_index(drop=True)
            df_data_rolling['ROLLING'] = df_data_rolling.groupby('RUN')['VALORCUOTA'].pct_change(periods=self.rolling_window)
            
            pivot_rolling = pd.pivot_table(
                df_data_rolling,
                index=['FECHA','MONEDA_RETORNO'], 
                columns='RUN', 
                aggfunc='max',
                values='ROLLING').reset_index()
            
            self.time_series_matrix['rolling'] = pivot_rolling
        
        self.time_series_matrix['valores_cuota'] = df_data
        self.time_series_matrix['day'] = pivot_dtd
        self.time_series_matrix['week'] = pivot_wtd
        self.time_series_matrix['month'] = pivot_mtd
        
    def runs_info(self) -> pd.DataFrame:
        '''
        Retorna un dataframe con la información de los runs disponibles en la API-INVESTMENT-RISK.
        '''
        
        if self.ignore_name_change == True:
            df_info = api_datamart.get_fm_info(ignorar_cambio_nombre=True, fecha_corte=self.end_date)

        elif self.ignore_name_change == False:
            df_info = api_datamart.get_fm_info(ignorar_cambio_nombre=False,fecha_corte=self.end_date)
            
        df = df_info[df_info['RUN'].isin(self.list_runs)].reset_index(drop=True)
        df = df[df['VIGENTE']=='Vigente'].reset_index(drop=True)
        df = df.sort_values(by='DAYS', ascending=True).reset_index(drop=True)
        return df

@dataclass
class Expost():
    '''
    Cálculo de métricas de riesgo ex-post para fondos mutuos.
    '''
    
    def volatility(self, df_returns: pd.DataFrame, ewma_factor: float = None) -> float:
        '''
        Cálculo de la volatilidad para una serie de retornos.
        
        Parámetros:
        df_returns: DataFrame con los retornos de los fondos mutuos.
        ewma_factor: Factor de suavizamiento para el cálculo de la volatilidad con EWMA (por defecto None, sin suavizamiento).
        
        Retorna:
        Serie con la volatilidad calculada para cada fondo mutuo.
        '''
        sigma = df_returns.std()
        if ewma_factor is not None:
            ret = df_returns.iloc[-1, 0]
            sigma = (ewma_factor * sigma**2) + ((1 - ewma_factor) * ret**2)
        return sigma
    
    def VaR(self, df_returns: pd.DataFrame, sigma: float, confidence_level: float = 0.95, mu_0:bool=False) -> float:
        '''
        Cálculo del Value at Risk (VaR) para una serie de retornos.
        
        Parámetros:
        df_returns: DataFrame con los retornos de los fondos mutuos.
        confidence_level: Nivel de confianza para el cálculo del VaR (por defecto 0.95).
        
        Retorna:
        Serie con el VaR calculado para cada fondo mutuo.
        '''
        z_score = norm.ppf(1 - confidence_level)
        if mu_0 == True:
            mu = 0
        elif mu_0 == False:
            mu = df_returns.mean()
        var = z_score * sigma + mu
        return var
        
    
    def max_drawdown(self, df_returns: pd.DataFrame) -> float:
        '''
        Cálculo del Max Drawdown para una serie de retornos.
        '''
        cumulative_returns = (1 + df_returns).cumprod()
        rolling_max = cumulative_returns.cummax()
        drawdown = (cumulative_returns - rolling_max) / rolling_max
        max_drawdown = drawdown.min()
        return max_drawdown
    
    
    def tracking_error(self, df_returns: pd.DataFrame, benchmark_returns: pd.DataFrame) -> float:
        '''
        Cálculo del Tracking Error para una serie de retornos en comparación con un benchmark.
        '''


    
    def beta(self, df_returns: pd.DataFrame, benchmark_returns: pd.DataFrame) -> float:
        '''
        Cálculo del Beta para una serie de retornos en comparación con un benchmark.
        '''
        cov = df_returns.cov(benchmark_returns)
        var_benchmark = benchmark_returns.var()
        beta = cov / var_benchmark
        return beta
