"""
Leonex — Agente de Datos
================================
Inspirado en López de Prado: "Basura entra, basura sale"

Qué hace:
  1. Descarga datos de equities (yfinance), forex y crypto (ccxt)
  2. Limpia y valida los datos (detecta gaps, outliers, datos corruptos)
  3. Calcula retornos logarítmicos y métricas estadísticas básicas
  4. Guarda todo en SQLite local (tu base de datos del sistema)
  5. Genera un JSON para el dashboard

Cómo ejecutar:
  python agents/agente_datos.py

Autor: Leonex — Fase 2
"""

import sqlite3
import json
import logging
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path

import numpy as np
import pandas as pd
import yfinance as yf

# ── Configuración de rutas ──────────────────────────────────────────────────
BASE_DIR   = Path(__file__).resolve().parent   # raíz del proyecto (el script vive aquí)
DATA_DIR   = BASE_DIR / "data"
LOGS_DIR   = BASE_DIR / "logs"
DB_PATH    = DATA_DIR / "Leonex.sqlite"        # misma DB que agents/agente_datos.py
JSON_PATH  = BASE_DIR / "dashboard" / "datos_mercado.json"

DATA_DIR.mkdir(exist_ok=True)
LOGS_DIR.mkdir(exist_ok=True)
(BASE_DIR / "dashboard").mkdir(exist_ok=True)

# ── Logging ─────────────────────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(LOGS_DIR / "agente_datos.log", encoding="utf-8"),
        logging.StreamHandler(sys.stdout)
    ]
)
log = logging.getLogger("Agentedatos")

# ══════════════════════════════════════════════════════════════════════════════
# UNIVERSO DE ACTIVOS
# Empezamos pequeño — expandimos cuando el sistema esté validado
# ══════════════════════════════════════════════════════════════════════════════

EQUITIES = {
    # ETFs de mercado amplio (base de la cartera)
    "SPY":  "S&P 500 ETF",
    "QQQ":  "Nasdaq 100 ETF",
    "IWM":  "Russell 2000 ETF",
    "EFA":  "MSCI EAFE (Europa+Asia)",
    "EEM":  "Mercados Emergentes",
    # Acciones individuales — momentum histórico alto
    "AAPL": "Apple",
    "MSFT": "Microsoft",
    "NVDA": "NVIDIA",
    "GOOGL":"Alphabet",
    "AMZN": "Amazon",
    "META": "Meta",
    "TSLA": "Tesla",
    # Sectores
    "XLF":  "Financiero",
    "XLE":  "Energía",
    "XLV":  "Salud",
}

FOREX = {
    # Majors — mayor liquidez, menor spread
    "EURUSD=X": "EUR/USD",
    "GBPUSD=X": "GBP/USD",
    "USDJPY=X": "USD/JPY",
    "AUDUSD=X": "AUD/USD",
    "USDCHF=X": "USD/CHF",
    "USDCAD=X": "USD/CAD",
    # Minors interesantes para swing
    "EURGBP=X": "EUR/GBP",
    "EURJPY=X": "EUR/JPY",
    "GBPJPY=X": "GBP/JPY",
}

CRYPTO = {
    # Solo top por liquidez — evita coins manipulables
    "BTC-USD": "Bitcoin",
    "ETH-USD": "Ethereum",
    "SOL-USD": "Solana",
    "BNB-USD": "Binance Coin",
    "ADA-USD": "Cardano",
}

# Fondos indexados españoles — proxies via ETFs equivalentes
FONDOS_PROXY = {
    "IWDA.AS": "iShares MSCI World (proxy MyInvestor)",
    "EIMI.AS": "iShares MSCI EM (proxy emergentes)",
    "EXSA.AS": "iShares STOXX Europe 600",
    "VWCE.DE": "Vanguard FTSE All-World",
}

# ══════════════════════════════════════════════════════════════════════════════
# BASE DE DATOS SQLite
# ══════════════════════════════════════════════════════════════════════════════

def init_db():
    """Crea las tablas si no existen."""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()

    # Tabla principal de precios
    c.execute("""
        CREATE TABLE IF NOT EXISTS precios (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            ticker      TEXT NOT NULL,
            mercado     TEXT NOT NULL,
            fecha       TEXT NOT NULL,
            open        REAL,
            high        REAL,
            low         REAL,
            close       REAL,
            volume      REAL,
            ret_log     REAL,
            UNIQUE(ticker, fecha)
        )
    """)

    # Tabla de métricas estadísticas por activo
    c.execute("""
        CREATE TABLE IF NOT EXISTS estadisticas (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            ticker          TEXT NOT NULL,
            fecha_calculo   TEXT NOT NULL,
            media_ret       REAL,
            vol_diaria      REAL,
            vol_anual       REAL,
            skewness        REAL,
            kurtosis        REAL,
            sharpe_hist     REAL,
            max_drawdown    REAL,
            n_observaciones INTEGER,
            UNIQUE(ticker, fecha_calculo)
        )
    """)

    # Tabla de log del agente
    c.execute("""
        CREATE TABLE IF NOT EXISTS log_agente (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            timestamp   TEXT NOT NULL,
            agente      TEXT NOT NULL,
            evento      TEXT NOT NULL,
            detalle     TEXT
        )
    """)

    conn.commit()
    conn.close()
    log.info(f"Base de datos inicializada: {DB_PATH}")


def guardar_precios(df: pd.DataFrame, ticker: str, mercado: str):
    """Guarda un DataFrame de precios en SQLite. Ignora duplicados."""
    if df.empty:
        return 0

    conn = sqlite3.connect(DB_PATH)
    registros = 0

    for fecha, row in df.iterrows():
        try:
            conn.execute("""
                INSERT OR IGNORE INTO precios
                (ticker, mercado, fecha, open, high, low, close, volume, ret_log)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                ticker, mercado,
                str(fecha.date()),
                float(row.get("Open",  np.nan)),
                float(row.get("High",  np.nan)),
                float(row.get("Low",   np.nan)),
                float(row.get("Close", np.nan)),
                float(row.get("Volume", 0)),
                float(row.get("ret_log", np.nan))
            ))
            registros += 1
        except Exception as e:
            log.warning(f"Error guardando {ticker} {fecha}: {e}")

    conn.commit()
    conn.close()
    return registros


def guardar_estadisticas(ticker: str, stats: dict):
    """Guarda métricas estadísticas de un activo."""
    conn = sqlite3.connect(DB_PATH)
    hoy = str(datetime.now().date())
    try:
        conn.execute("""
            INSERT OR REPLACE INTO estadisticas
            (ticker, fecha_calculo, media_ret, vol_diaria, vol_anual,
             skewness, kurtosis, sharpe_hist, max_drawdown, n_observaciones)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            ticker, hoy,
            stats["media_ret"], stats["vol_diaria"], stats["vol_anual"],
            stats["skewness"],  stats["kurtosis"],
            stats["sharpe_hist"], stats["max_drawdown"], stats["n_obs"]
        ))
        conn.commit()
    except Exception as e:
        log.warning(f"Error guardando estadísticas {ticker}: {e}")
    finally:
        conn.close()


def log_evento(agente: str, evento: str, detalle: str = ""):
    """Registra un evento en el log de la base de datos."""
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        INSERT INTO log_agente (timestamp, agente, evento, detalle)
        VALUES (?, ?, ?, ?)
    """, (datetime.now().isoformat(), agente, evento, detalle))
    conn.commit()
    conn.close()


# ══════════════════════════════════════════════════════════════════════════════
# DESCARGA Y LIMPIEZA DE DATOS
# ══════════════════════════════════════════════════════════════════════════════

def limpiar_df(df: pd.DataFrame, ticker: str) -> pd.DataFrame:
    """
    Limpieza estándar de datos de precios.
    Detecta y elimina: NaN, precios negativos, gaps extremos (outliers).
    """
    if df.empty:
        return df

    # Aplanar MultiIndex si yfinance lo devuelve
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)

    df = df.copy()

    # Eliminar filas completamente vacías
    df.dropna(how="all", inplace=True)

    # Eliminar precios negativos o cero (datos corruptos)
    if "Close" in df.columns:
        antes = len(df)
        df = df[df["Close"] > 0]
        eliminados = antes - len(df)
        if eliminados > 0:
            log.warning(f"{ticker}: {eliminados} filas con precio ≤ 0 eliminadas")

    # Calcular retornos logarítmicos
    if "Close" in df.columns and len(df) > 1:
        df["ret_log"] = np.log(df["Close"] / df["Close"].shift(1))

        # Detectar outliers: retornos > 5 sigma (probablemente splits/errores)
        mu  = df["ret_log"].mean()
        sig = df["ret_log"].std()
        umbral = 5 * sig
        outliers = (df["ret_log"].abs() > umbral).sum()
        if outliers > 0:
            log.warning(f"{ticker}: {outliers} retornos extremos (>5σ) detectados — revisar")

    return df


def calcular_estadisticas(df: pd.DataFrame) -> dict:
    """
    Calcula las métricas estadísticas clave de Fase 1.
    Media, volatilidad, skew, kurtosis, Sharpe histórico, max drawdown.
    """
    if df.empty or "ret_log" not in df.columns:
        return {}

    rets = df["ret_log"].dropna()
    if len(rets) < 30:
        return {}

    # Curva de capital para drawdown
    capital = (1 + rets).cumprod()
    rolling_max = capital.cummax()
    drawdown = (capital - rolling_max) / rolling_max
    max_dd = drawdown.min()

    # Sharpe histórico (asumiendo rf = 0 para simplificar)
    sharpe = (rets.mean() / rets.std()) * np.sqrt(252) if rets.std() > 0 else 0

    return {
        "media_ret":    round(rets.mean(), 6),
        "vol_diaria":   round(rets.std(), 6),
        "vol_anual":    round(rets.std() * np.sqrt(252), 4),
        "skewness":     round(rets.skew(), 4),
        "kurtosis":     round(rets.kurtosis(), 4),  # exceso de kurtosis
        "sharpe_hist":  round(sharpe, 4),
        "max_drawdown": round(max_dd, 4),
        "n_obs":        len(rets)
    }


def descargar_yfinance(tickers: dict, mercado: str, periodo: str = "2y") -> dict:
    """
    Descarga datos de Yahoo Finance para un grupo de tickers.
    Devuelve dict {ticker: DataFrame limpio}
    """
    resultados = {}
    total = len(tickers)

    log.info(f"Descargando {total} activos de {mercado}...")

    for i, (ticker, nombre) in enumerate(tickers.items(), 1):
        try:
            df = yf.download(
                ticker,
                period=periodo,
                auto_adjust=True,
                progress=False,
                timeout=10
            )

            if df.empty:
                log.warning(f"[{i}/{total}] {ticker} ({nombre}): sin datos")
                continue

            df = limpiar_df(df, ticker)
            stats = calcular_estadisticas(df)

            registros = guardar_precios(df, ticker, mercado)
            if stats:
                guardar_estadisticas(ticker, stats)

            resultados[ticker] = {
                "nombre":    nombre,
                "mercado":   mercado,
                "registros": registros,
                "stats":     stats,
                "ultimo_precio": float(df["Close"].iloc[-1]) if not df.empty else None,
                "ultima_fecha":  str(df.index[-1].date()) if not df.empty else None
            }

            log.info(
                f"[{i}/{total}] {ticker:12s} | {registros:4d} registros | "
                f"Vol anual: {stats.get('vol_anual', 0)*100:.1f}% | "
                f"Sharpe: {stats.get('sharpe_hist', 0):.2f}"
            )

        except Exception as e:
            log.error(f"[{i}/{total}] {ticker}: ERROR — {e}")

    return resultados


# ══════════════════════════════════════════════════════════════════════════════
# GENERADOR DE JSON PARA EL DASHBOARD
# ══════════════════════════════════════════════════════════════════════════════

def leer_estadisticas_db() -> list:
    """Lee las últimas estadísticas de cada activo desde SQLite."""
    conn = sqlite3.connect(DB_PATH)
    query = """
        SELECT e.ticker, e.media_ret, e.vol_anual, e.skewness,
               e.kurtosis, e.sharpe_hist, e.max_drawdown, e.n_observaciones,
               p.mercado, p.close as ultimo_precio, p.fecha as ultima_fecha
        FROM estadisticas e
        JOIN (
            SELECT ticker, MAX(fecha_calculo) as max_fecha
            FROM estadisticas GROUP BY ticker
        ) latest ON e.ticker = latest.ticker
                 AND e.fecha_calculo = latest.max_fecha
        JOIN (
            SELECT ticker, close, fecha, mercado
            FROM precios p1
            WHERE fecha = (SELECT MAX(fecha) FROM precios p2 WHERE p2.ticker = p1.ticker)
        ) p ON e.ticker = p.ticker
        ORDER BY e.sharpe_hist DESC
    """
    try:
        df = pd.read_sql_query(query, conn)
        conn.close()
        return df.to_dict(orient="records")
    except Exception as e:
        log.warning(f"Error leyendo estadísticas: {e}")
        conn.close()
        return []


def leer_ultimos_precios(ticker: str, n: int = 90) -> list:
    """Lee los últimos N precios de un ticker para gráficos."""
    conn = sqlite3.connect(DB_PATH)
    query = f"""
        SELECT fecha, close, ret_log
        FROM precios
        WHERE ticker = ?
        ORDER BY fecha DESC
        LIMIT {n}
    """
    try:
        df = pd.read_sql_query(query, conn, params=[ticker])
        conn.close()
        return df.sort_values("fecha").to_dict(orient="records")
    except:
        conn.close()
        return []


def generar_json_dashboard(resultados_descarga: dict):
    """
    Genera el JSON que lee el dashboard HTML.
    Incluye: resumen de activos, estadísticas, últimas señales, estado del agente.
    """
    estadisticas = leer_estadisticas_db()

    # Resumen por mercado
    mercados = {}
    for item in estadisticas:
        m = item.get("mercado", "unknown")
        if m not in mercados:
            mercados[m] = {"n_activos": 0, "sharpe_medio": [], "vol_media": []}
        mercados[m]["n_activos"] += 1
        mercados[m]["sharpe_medio"].append(item.get("sharpe_hist", 0))
        mercados[m]["vol_media"].append(item.get("vol_anual", 0))

    for m in mercados:
        s = mercados[m]["sharpe_medio"]
        v = mercados[m]["vol_media"]
        mercados[m]["sharpe_medio"] = round(np.mean(s), 3) if s else 0
        mercados[m]["vol_media"]    = round(np.mean(v) * 100, 1) if v else 0

    # Top activos por Sharpe histórico
    top_sharpe = sorted(
        estadisticas,
        key=lambda x: x.get("sharpe_hist", 0),
        reverse=True
    )[:10]

    # Activos con fat tails pronunciadas (kurtosis > 3)
    fat_tails = [
        x for x in estadisticas
        if x.get("kurtosis", 0) > 3
    ]

    datos_json = {
        "meta": {
            "generado":        datetime.now().isoformat(),
            "version":         "1.0",
            "total_activos":   len(estadisticas),
            "agente":          "AgentesDatos",
            "estado":          "operativo"
        },
        "resumen": {
            "total_activos":   len(estadisticas),
            "por_mercado":     mercados,
            "ultima_actualizacion": datetime.now().strftime("%d/%m/%Y %H:%M")
        },
        "top_por_sharpe":  top_sharpe[:5],
        "fat_tails_alerta": [
            {
                "ticker":   x["ticker"],
                "kurtosis": round(x.get("kurtosis", 0), 2),
                "mercado":  x.get("mercado", "")
            }
            for x in fat_tails[:5]
        ],
        "todos_activos": estadisticas,
    }

    with open(JSON_PATH, "w", encoding="utf-8") as f:
        json.dump(datos_json, f, ensure_ascii=False, indent=2)

    log.info(f"JSON del dashboard generado: {JSON_PATH}")
    return datos_json


# ══════════════════════════════════════════════════════════════════════════════
# REPORTE FINAL EN CONSOLA
# ══════════════════════════════════════════════════════════════════════════════

def imprimir_resumen(datos_json: dict):
    """Imprime un resumen legible en consola."""
    print("\n" + "="*65)
    print("  Leonex — AGENTE DE DATOS — RESUMEN")
    print("="*65)

    resumen = datos_json.get("resumen", {})
    print(f"\n  Total activos en base de datos: {resumen.get('total_activos', 0)}")
    print(f"  Última actualización: {resumen.get('ultima_actualizacion', '')}")

    por_mercado = resumen.get("por_mercado", {})
    if por_mercado:
        print("\n  Por mercado:")
        for m, info in por_mercado.items():
            print(
                f"    {m:12s} | {info['n_activos']:3d} activos | "
                f"Sharpe medio: {info['sharpe_medio']:5.2f} | "
                f"Vol media: {info['vol_media']:5.1f}%"
            )

    top = datos_json.get("top_por_sharpe", [])
    if top:
        print("\n  Top 5 por Sharpe histórico:")
        for i, a in enumerate(top[:5], 1):
            fat = "⚠ FAT TAIL" if a.get("kurtosis", 0) > 3 else ""
            print(
                f"    {i}. {a['ticker']:10s} | "
                f"Sharpe: {a.get('sharpe_hist',0):5.2f} | "
                f"Vol: {a.get('vol_anual',0)*100:5.1f}% | "
                f"Skew: {a.get('skewness',0):6.3f} {fat}"
            )

    alertas = datos_json.get("fat_tails_alerta", [])
    if alertas:
        print(f"\n  Alertas fat tails (kurtosis > 3) — stops mas amplios:")
        for a in alertas:
            print(f"    {a['ticker']:10s} | Kurtosis: {a['kurtosis']:.2f} | {a['mercado']}")

    print("\n" + "="*65)
    print("  SIGUIENTE PASO: Abre el dashboard y conecta el JSON")
    print(f"  JSON generado en: {JSON_PATH}")
    print("="*65 + "\n")


# ══════════════════════════════════════════════════════════════════════════════
# MAIN — PUNTO DE ENTRADA
# ══════════════════════════════════════════════════════════════════════════════

def main():
    inicio = datetime.now()
    log.info("="*50)
    log.info("Leonex — Agente de Datos iniciado")
    log.info("="*50)

    # 1. Inicializar base de datos
    init_db()
    log_evento("AgentesDatos", "INICIO", f"Python {sys.version.split()[0]}")

    # 2. Descargar todos los mercados
    todos_resultados = {}

    log.info("\n--- EQUITIES ---")
    r_eq = descargar_yfinance(EQUITIES, "equity", periodo="2y")
    todos_resultados.update(r_eq)

    log.info("\n--- FOREX ---")
    r_fx = descargar_yfinance(FOREX, "forex", periodo="2y")
    todos_resultados.update(r_fx)

    log.info("\n--- CRYPTO ---")
    r_cr = descargar_yfinance(CRYPTO, "crypto", periodo="2y")
    todos_resultados.update(r_cr)

    log.info("\n--- FONDOS (proxies ETF) ---")
    r_fd = descargar_yfinance(FONDOS_PROXY, "fondos", periodo="2y")
    todos_resultados.update(r_fd)

    # 3. Generar JSON para el dashboard
    log.info("\nGenerando JSON para el dashboard...")
    datos_json = generar_json_dashboard(todos_resultados)

    # 4. Resumen final
    imprimir_resumen(datos_json)

    duracion = (datetime.now() - inicio).seconds
    log_evento("AgentesDatos", "FIN", f"Duración: {duracion}s | Activos: {len(todos_resultados)}")
    log.info(f"Agente de Datos completado en {duracion} segundos")


if __name__ == "__main__":
    main()
