import pandas as pd
import sys
from dataclasses import dataclass
from .api_config import API
from concurrent.futures import ThreadPoolExecutor, as_completed

@dataclass
class Datamart(API):
    # === VALORES CUOTA ===
    def _print_progress(self, current: int, total: int) -> None:
        if total <= 0:
            return

        width = 30
        progress = current / total
        completed = int(width * progress)
        bar = f"{'█' * completed}{'-' * (width - completed)}"
        pct = int(progress * 100)
        sys.stdout.write(f"\rDescargando bloques: [{bar}] {pct}% ({current}/{total})")
        sys.stdout.flush()

    def _sort_by_fecha(self, data: pd.DataFrame) -> pd.DataFrame:
        if data is None or data.empty:
            return pd.DataFrame()
        if 'FECHA' in data.columns:
            data = data.sort_values(by='FECHA', ascending=True).reset_index(drop=True)
        return data

    def _build_valores_cuota_url(self, start: str, end: str, ccy: str, categ: str = None, lst_run: list = None) -> str:
        if categ and lst_run:
            raise ValueError("No se pueden ingresar ambos parámetros 'categ' y 'lst_run' al mismo tiempo. Por favor, ingrese solo uno de ellos.")

        if categ:
            return f'{self.API_URL}/FM_Valores_Cuota_Categoria/{start}_{end}_{ccy}_{categ}'

        if lst_run:
            id_run = '%2C%20'.join([str(run) for run in lst_run])
            return f'{self.API_URL}/FM_Valores_Cuota_Lista_Run/{start}_{end}_{ccy}_{id_run}'

        return f'{self.API_URL}/FM_Valores_Cuota/{start}_{end}_{ccy}'

    def _split_date_ranges(self, start: str, end: str, chunk_days: int) -> list:
        start_dt = pd.to_datetime(start)
        end_dt = pd.to_datetime(end)

        if end_dt < start_dt:
            raise ValueError("La fecha 'end' debe ser mayor o igual a 'start'.")

        ranges = []
        current_start = start_dt

        while current_start <= end_dt:
            current_end = min(current_start + pd.Timedelta(days=chunk_days - 1), end_dt)
            ranges.append((current_start.strftime('%Y-%m-%d'), current_end.strftime('%Y-%m-%d')))
            current_start = current_end + pd.Timedelta(days=1)

        return ranges

    def get_fm_valores_cuota(self, start:str, end:str, ccy:str='clp', categ:str=None, lst_run:list=None, chunk_days:int=90, show_progress:bool=True) -> pd.DataFrame:
        """
        Retorna el método GET FM_Valores_Cuota de la API-INVESTMENT-RISK.
        """
        if chunk_days is None or chunk_days <= 0:
            api_url = self._build_valores_cuota_url(start=start, end=end, ccy=ccy, categ=categ, lst_run=lst_run)
            data = self.engine(url=api_url)
            return self._sort_by_fecha(data)

        partial_results = []
        date_ranges = self._split_date_ranges(start=start, end=end, chunk_days=chunk_days)
        total_chunks = len(date_ranges)

        for idx, (chunk_start, chunk_end) in enumerate(date_ranges, start=1):
            if show_progress:
                self._print_progress(current=idx - 1, total=total_chunks)

            api_url = self._build_valores_cuota_url(start=chunk_start, end=chunk_end, ccy=ccy, categ=categ, lst_run=lst_run)
            data_chunk = self.engine(url=api_url)

            if data_chunk is not None and not data_chunk.empty:
                partial_results.append(data_chunk)

            if show_progress:
                self._print_progress(current=idx, total=total_chunks)

        if show_progress and total_chunks > 0:
            sys.stdout.write("\n")

        if not partial_results:
            return pd.DataFrame()

        data = pd.concat(partial_results, ignore_index=True)
        data = data.drop_duplicates().reset_index(drop=True)
        return self._sort_by_fecha(data)
    
    def get_fm_valores_cuota_parallel(self, start:str, end:str, ccy:str='clp', categ:str=None, lst_run:list=None, chunk_days:int=180, max_workers:int=5, show_progress:bool=True) -> pd.DataFrame:
        """
        Retorna el método GET FM_Valores_Cuota de la API-INVESTMENT-RISK con paralelización (mucho más rápido).
        
        Args:
            start (str): Fecha inicio
            end (str): Fecha fin
            ccy (str): Código de moneda
            categ (str): Categoría opcional
            lst_run (list): Lista de RUNs opcional
            chunk_days (int): Días por chunk (default 180 para menos requests)
            max_workers (int): Cantidad de requests paralelos (default 5)
            show_progress (bool): Mostrar progreso
            
        Returns:
            pd.DataFrame: Datos consolidados y ordenados
        """
        date_ranges = self._split_date_ranges(start=start, end=end, chunk_days=chunk_days)
        total_chunks = len(date_ranges)
        
        partial_results = []
        completed_count = [0]  # usar lista para evitar problema de nonlocal en threading
        
        def fetch_chunk(chunk_start, chunk_end, idx):
            api_url = self._build_valores_cuota_url(start=chunk_start, end=chunk_end, ccy=ccy, categ=categ, lst_run=lst_run)
            try:
                data_chunk = self.engine(url=api_url)
                completed_count[0] += 1
                if show_progress:
                    self._print_progress(current=completed_count[0], total=total_chunks)
                return data_chunk if (data_chunk is not None and not data_chunk.empty) else None
            except Exception as e:
                completed_count[0] += 1
                print(f"\nError en chunk {idx}: {e}")
                return None
        
        # Ejecutar requests en paralelo
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(fetch_chunk, chunk_start, chunk_end, idx): (chunk_start, chunk_end)
                for idx, (chunk_start, chunk_end) in enumerate(date_ranges)
            }
            
            for future in as_completed(futures):
                result = future.result()
                if result is not None:
                    partial_results.append(result)
        
        if show_progress and total_chunks > 0:
            sys.stdout.write("\n")
        
        if not partial_results:
            return pd.DataFrame()
        
        data = pd.concat(partial_results, ignore_index=True)
        data = data.drop_duplicates().reset_index(drop=True)
        return self._sort_by_fecha(data)
    
    def get_fm_info(self, ignorar_cambio_nombre: bool = False, fecha_corte:str=None, debug:bool=False) -> pd.DataFrame:
        """
        Retorna el método GET FM_Detalle de la API-INVESTMENT-RISK.
        """
        api_url = f'{self.API_URL}/Fondos_Mutuos/{str(ignorar_cambio_nombre).lower()}'
        if fecha_corte is not None:
            api_url += f'?fecha_corte={fecha_corte}'
        if debug:
            print(f"API URL: {api_url}")
        data = self.engine(url=api_url)
        if data is None or data.empty:
            return pd.DataFrame()
        data['DAYS'] = (pd.to_datetime(data['FECHA_MAX']) - pd.to_datetime(data['FECHA_MIN'])).dt.days
        data['YEARS'] = data['DAYS'] / 365
        data['VIGENTE'] = data.apply(lambda x: 'Vigente' if x['FECHA_MAX'] == data['FECHA_MAX'].max() else 'No Vigente', axis=1)
        return data

@dataclass
class Risk(API):
    # === METRICAS EXPOST ===
    def _print_progress(self, current: int, total: int) -> None:
        if total <= 0:
            return

        width = 30
        progress = current / total
        completed = int(width * progress)
        bar = f"{'█' * completed}{'-' * (width - completed)}"
        pct = int(progress * 100)
        sys.stdout.write(f"\rDescargando bloques: [{bar}] {pct}% ({current}/{total})")
        sys.stdout.flush()

    def _sort_by_fecha(self, data: pd.DataFrame) -> pd.DataFrame:
        if data is None or data.empty:
            return pd.DataFrame()
        if 'FECHA' in data.columns:
            data = data.sort_values(by='FECHA', ascending=True).reset_index(drop=True)
        return data

    def _split_date_ranges(self, start: str, end: str, chunk_days: int) -> list:
        start_dt = pd.to_datetime(start)
        end_dt = pd.to_datetime(end)

        if end_dt < start_dt:
            raise ValueError("La fecha 'end' debe ser mayor o igual a 'start'.")

        ranges = []
        current_start = start_dt

        while current_start <= end_dt:
            current_end = min(current_start + pd.Timedelta(days=chunk_days - 1), end_dt)
            ranges.append((current_start.strftime('%Y-%m-%d'), current_end.strftime('%Y-%m-%d')))
            current_start = current_end + pd.Timedelta(days=1)

        return ranges

    def _build_metricas_expost_url(self, start: str, end: str, run: str = None, categoria: str = None, metrica: str = None) -> str:
        if run and categoria:
            raise ValueError("No se pueden ingresar ambos parámetros 'run' y 'categoria' al mismo tiempo. Por favor, ingrese solo uno de ellos.")

        base_url = f'{self.API_URL}/Metricas_Expost/{start}_{end}'

        if run:
            base_url += f'?run={run}'
            if metrica:
                base_url += f'&metrica={metrica}'
            return base_url

        if categoria:
            base_url += f'?categoria={categoria}'
            if metrica:
                base_url += f'&metrica={metrica}'
            return base_url

        if metrica:
            base_url += f'?metrica={metrica}'

        return base_url

    def get_fm_metricas_expost_parallel(self, start: str, end: str, run: str = None, categoria: str = None, metrica: str = None, chunk_days: int = 180, max_workers: int = 5, show_progress: bool = True) -> pd.DataFrame:
        """
        Retorna el método GET Metricas_Expost de la API-INVESTMENT-RISK con paralelización (mucho más rápido).
        
        Args:
            start (str): Fecha inicio
            end (str): Fecha fin
            run (str): RUN opcional
            categoria (str): Categoría opcional
            metrica (str): Métrica opcional
            chunk_days (int): Días por chunk (default 180 para menos requests)
            max_workers (int): Cantidad de requests paralelos (default 5)
            show_progress (bool): Mostrar progreso
            
        Returns:
            pd.DataFrame: Datos consolidados y ordenados
        """
        date_ranges = self._split_date_ranges(start=start, end=end, chunk_days=chunk_days)
        total_chunks = len(date_ranges)
        
        partial_results = []
        completed_count = [0]  # usar lista para evitar problema de nonlocal en threading
        
        def fetch_chunk(chunk_start, chunk_end, idx):
            api_url = self._build_metricas_expost_url(start=chunk_start, end=chunk_end, run=run, categoria=categoria, metrica=metrica)
            try:
                data_chunk = self.engine(url=api_url)
                completed_count[0] += 1
                if show_progress:
                    self._print_progress(current=completed_count[0], total=total_chunks)
                return data_chunk if (data_chunk is not None and not data_chunk.empty) else None
            except Exception as e:
                completed_count[0] += 1
                print(f"\nError en chunk {idx}: {e}")
                return None
        
        # Ejecutar requests en paralelo
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(fetch_chunk, chunk_start, chunk_end, idx): (chunk_start, chunk_end)
                for idx, (chunk_start, chunk_end) in enumerate(date_ranges)
            }
            
            for future in as_completed(futures):
                result = future.result()
                if result is not None:
                    partial_results.append(result)
        
        if show_progress and total_chunks > 0:
            sys.stdout.write("\n")
        
        if not partial_results:
            return pd.DataFrame()
        
        data = pd.concat(partial_results, ignore_index=True)
        data = data.drop_duplicates().reset_index(drop=True)
        return self._sort_by_fecha(data)
    
    # === BENCHMARKS ===
    def get_benchmarks(self,bmk:str, year:str, debug:bool=False) -> pd.DataFrame:
        """
        Retorna el método GET FM_Detalle de la API-INVESTMENT-RISK.
        """
        api_url = f'{self.API_URL}/Benchmarks/{bmk}_{year}'
        if debug:
            print(f"API URL: {api_url}")
        data = self.engine(url=api_url)
        if data is None or data.empty:
            return pd.DataFrame()

        return data