# src/data/fetcher.py
"""
Módulo para obtener datos de exchanges usando CCXT
"""
import ccxt
import pandas as pd
from datetime import datetime, timedelta
from typing import List, Optional, Dict, Callable
import time
from ..utils.logger import log


class DataFetcher:
    """
    Clase para obtener datos históricos y en tiempo real de exchanges
    """

    def __init__(self, exchange_name: str, api_key: str = None, api_secret: str = None):
        self.exchange_name = exchange_name

        try:
            exchange_class = getattr(ccxt, exchange_name)

            config = {
                "enableRateLimit": True,
                "options": {
                    "defaultType": "spot",
                },
            }

            if api_key and api_secret:
                config["apiKey"] = api_key
                config["secret"] = api_secret
                log.info(f"Conectado al exchange: {exchange_name} (con API keys)")
            else:
                log.info(
                    f"Conectado al exchange: {exchange_name} (modo público - sin API keys)"
                )

            self.exchange = exchange_class(config)

        except Exception as e:
            log.error(f"Error conectando a {exchange_name}: {e}")
            raise

    # ------------------------------------------------------------------

    def fetch_ohlcv(
        self,
        symbol: str,
        timeframe: str = "1h",
        since: Optional[datetime] = None,
        limit: int = 500,
    ) -> pd.DataFrame:
        try:
            since_ms = int(since.timestamp() * 1000) if since else None

            log.info(f"Obteniendo datos OHLCV: {symbol} {timeframe}")

            ohlcv = self.exchange.fetch_ohlcv(
                symbol,
                timeframe=timeframe,
                since=since_ms,
                limit=limit,
            )

            df = pd.DataFrame(
                ohlcv,
                columns=["timestamp", "open", "high", "low", "close", "volume"],
            )

            df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
            df.set_index("timestamp", inplace=True)

            df["symbol"] = symbol
            df["timeframe"] = timeframe

            log.success(f"Obtenidos {len(df)} registros de {symbol}")
            return df

        except Exception as e:
            log.error(f"Error obteniendo OHLCV para {symbol}: {e}")
            raise

    # ------------------------------------------------------------------

    def fetch_historical(
        self,
        symbol: str,
        timeframe: str = "1h",
        since: Optional[datetime] = None,
        until: Optional[datetime] = None,
        days: Optional[int] = None,
        max_retries: int = 3,
        *,
        progress_cb: Optional[Callable[[int, pd.DataFrame], None]] = None,
        should_cancel: Optional[Callable[[], bool]] = None,
    ) -> pd.DataFrame:
        """
        Obtiene datos históricos completos (puede requerir múltiples llamadas)

        progress_cb(iteration, df):
            callback por bloque descargado

        should_cancel():
            devuelve True si la descarga debe abortarse
        """

        all_data = []

        if since is None:
            if days is None:
                raise ValueError("Debes proporcionar 'since' o 'days'")
            since = datetime.utcnow() - timedelta(days=days)

        if until is None:
            until = datetime.utcnow()

        log.info(f"Iniciando descarga histórica: {symbol} desde {since.date()}")

        iteration = 0

        while True:
            if should_cancel and should_cancel():
                log.warning("Descarga cancelada desde fetch_historical")
                break

            iteration += 1
            log.debug(f"Iteración {iteration}: Obteniendo datos desde {since}")

            retry_count = 0
            success = False

            while retry_count < max_retries and not success:
                try:
                    df = self.fetch_ohlcv(symbol, timeframe, since, limit=1000)

                    if df.empty:
                        log.warning(f"No hay más datos disponibles para {symbol}")
                        success = True
                        break

                    all_data.append(df)

                    # Callback de progreso
                    if progress_cb:
                        try:
                            progress_cb(iteration, df)
                        except Exception as exc:
                            log.warning(f"Error en progress_cb: {exc}")

                    last_timestamp = df.index[-1]
                    timeframe_seconds = self.exchange.parse_timeframe(timeframe)
                    since = last_timestamp + pd.Timedelta(
                        seconds=timeframe_seconds
                    )

                    if since >= until:
                        success = True
                        break

                    success = True
                    time.sleep(self.exchange.rateLimit / 1000)

                except Exception as e:
                    retry_count += 1
                    log.warning(f"Intento {retry_count}/{max_retries} falló: {e}")
                    time.sleep(5 * retry_count)

            if not success:
                log.error(f"Falló después de {max_retries} intentos")
                break

            if since >= until or df.empty:
                break

        if not all_data:
            log.error("No se pudo obtener ningún dato histórico")
            return pd.DataFrame()

        final_df = pd.concat(all_data).drop_duplicates()
        final_df.sort_index(inplace=True)

        log.success(f"Descarga completa: {len(final_df)} velas de {symbol}")
        return final_df

    # ------------------------------------------------------------------

    def fetch_ticker(self, symbol: str) -> Dict:
        try:
            ticker = self.exchange.fetch_ticker(symbol)
            log.debug(f"Ticker de {symbol}: {ticker['last']}")
            return ticker
        except Exception as e:
            log.error(f"Error obteniendo ticker de {symbol}: {e}")
            raise

    def fetch_order_book(self, symbol: str, limit: int = 20) -> Dict:
        try:
            return self.exchange.fetch_order_book(symbol, limit)
        except Exception as e:
            log.error(f"Error obteniendo order book de {symbol}: {e}")
            raise

    def get_available_symbols(self) -> List[str]:
        try:
            markets = self.exchange.load_markets()
            symbols = list(markets.keys())
            log.info(f"Símbolos disponibles: {len(symbols)}")
            return symbols
        except Exception as e:
            log.error(f"Error obteniendo símbolos: {e}")
            raise
