#!/usr/bin/env python3
"""
🚀 Embeddings para todo — Demo completa
========================================
Episodio 817: Un solo script que recorre todos los casos de uso.
Búsqueda semántica, clasificación, deduplicación, recomendación y ChromaDB.

Requisitos:
  - Ollama corriendo en localhost:11434
  - Modelo nomic-embed-text descargado: ollama pull nomic-embed-text
  - Python 3.10+

Uso:
  python3 setup_completo.py                          # Ejecuta todo
  python3 setup_completo.py --fast                   # Solo pasos principales, sin ChromaDB
  python3 setup_completo.py --list                   # Lista los pasos disponibles
  python3 setup_completo.py --step 3                 # Ejecuta solo el paso 3
  python3 setup_completo.py --step 2,4,6             # Ejecuta pasos específicos
  python3 setup_completo.py --interactive            # Paso a paso con confirmación
"""

import os, sys, json, math, glob, time, shutil, subprocess, textwrap, argparse

# ─── Configuración ────────────────────────────────────────────────────────
MODEL = "bge-m3"
OLLAMA_URL = "http://localhost:11434"
DIR_DOCS = "./docs_prueba"  # Documentos de prueba para búsqueda semántica
DIR_CHROMA = "./chroma_store"  # Donde ChromaDB guarda los datos

# Colores para la terminal
VERDE = "\033[92m"
AZUL = "\033[94m"
AMAR = "\033[93m"
ROJO = "\033[91m"
FIN = "\033[0m"
NEGR = "\033[1m"


def ok(msg):
    print(f"  {VERDE}✅{FIN} {msg}")


def info(msg):
    print(f"  {AZUL}ℹ️{FIN}  {msg}")


def warn(msg):
    print(f"  {AMAR}⚠️{FIN}  {msg}")


def error(msg):
    print(f"  {ROJO}❌{FIN} {msg}")


def titulo(t):
    print(f"\n{NEGR}{'=' * 60}{FIN}\n{NEGR}  {t}{FIN}\n{'=' * 60}")


# ─── Helpers ──────────────────────────────────────────────────────────────


def check_ollama():
    """Verifica que Ollama está corriendo y el modelo está disponible."""
    try:
        r = subprocess.run(
            ["curl", "-s", f"{OLLAMA_URL}/api/tags"],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if r.returncode != 0:
            error("Ollama no responde. ¿Está corriendo?")
            info("Ejecuta: ollama serve")
            return False
        modelos = json.loads(r.stdout)
        disponible = any(MODEL in m["name"] for m in modelos.get("models", []))
        if not disponible:
            warn(f"Modelo {MODEL} no encontrado. Descargando...")
            subprocess.run(["ollama", "pull", MODEL], check=True)
            ok(f"Modelo {MODEL} descargado")
        return True
    except Exception as e:
        error(f"Error conectando con Ollama: {e}")
        return False


def emb(texto):
    """Genera un embedding usando Ollama."""
    payload = json.dumps({"model": MODEL, "prompt": texto})
    r = subprocess.run(
        ["curl", "-s", f"{OLLAMA_URL}/api/embeddings", "-d", payload],
        capture_output=True,
        text=True,
    )
    return json.loads(r.stdout)["embedding"]


def cos_sim(a, b):
    """Similitud coseno entre dos vectores."""
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb) if na and nb else 0


# ─── 1. Crear documentos de prueba ────────────────────────────────────────


def crear_documentos():
    titulo("1️⃣  CREAR DOCUMENTOS DE PRUEBA")

    if os.path.exists(DIR_DOCS):
        shutil.rmtree(DIR_DOCS)
    os.makedirs(DIR_DOCS)

    documentos = {
        "facturas.txt": """Factura hosting enero 2026: 12.99€
Factura dominio atareao.es 2026: 18.50€
Pago servidor VPS febrero: 24.99€
Total gastos infraestructura Q1: 156.47€
Próximo pago hosting: 1 de marzo""",
        "apuntes-linux.txt": """Cómo montar un servidor web con Nginx en Ubuntu 24.04
Configurar firewall con nftables: tabla inet filter con policy drop
Instalar Docker: sudo apt install docker.io docker-compose
Los logs del sistema están en /var/log/syslog
systemctl enable --now para activar servicios al arranque""",
        "recetas.txt": """Paella valenciana: arroz bomba, pollo, conejo, judía verde, garrofón
Tortilla de patatas: 4 huevos, 2 patatas grandes, cebolla, aceite de oliva
Pan casero: 500g harina de fuerza, 300ml agua, 10g sal, 5g levadura
La clave de un buen arroz es el sofrito y el caldo caliente""",
        "compras.txt": """Lista de la compra: leche, huevos, pan, arroz, tomates, lechuga
Comprar regalo cumpleaños: libro de Rust
Recoger paquete en Correos: número de seguimiento 123456789
Repostar coche: gasolina 95, 40€""",
        "ideas-proyectos.txt": """App web para gestionar gastos compartidos con amigos
Script que monitoriza el precio de la luz y avisa cuando baja
Bot de Telegram que resuma noticias de IA cada mañana
Generador de menú semanal con recetas de un archivo de cocina""",
    }

    for nombre, contenido in documentos.items():
        with open(os.path.join(DIR_DOCS, nombre), "w") as f:
            f.write(contenido.strip())
        ok(f"Documento creado: {nombre}")

    info("5 documentos de prueba listos en ./docs_prueba/")
    return list(documentos.keys())


# ─── 2. Búsqueda semántica ────────────────────────────────────────────────


def busqueda_semantica():
    titulo("2️⃣  BÚSQUEDA SEMÁNTICA — buscar por significado")

    # Indexar documentos
    info("Generando embeddings de los documentos de prueba...")
    archivos = sorted(glob.glob(f"{DIR_DOCS}/*.txt"))
    index = []

    for path in archivos:
        with open(path) as f:
            texto = f.read()
        nombre = os.path.basename(path)
        vec = emb(texto)
        index.append((nombre, vec, texto))
        print(f"    {nombre}: {len(vec)} dimensiones")

    ok(f"{len(index)} documentos indexados")

    # Consultas de prueba
    consultas = [
        "¿Cuánto dinero gastamos en servidores?",
        "¿Qué hay para cenar esta noche?",
        "Alguna idea para un proyecto con IA",
    ]

    for consulta in consultas:
        print(f'\n  {NEGR}🔍 Consulta:{FIN} "{consulta}"')
        vec_q = emb(consulta)

        resultados = sorted(
            [
                (nombre, cos_sim(vec_q, vec), texto[:100])
                for nombre, vec, texto in index
            ],
            key=lambda x: -x[1],
        )

        for nombre, sim, preview in resultados[:3]:
            barra = "█" * int(sim * 20) + "░" * (20 - int(sim * 20))
            print(f"    {barra} {sim:.4f}  {nombre}")

        mejor = resultados[0]
        print(f"  → Mejor resultado: {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")
        if mejor[1] > 0.7:
            ok("Coincidencia semántica alta")
        elif mejor[1] > 0.5:
            info("Coincidencia semántica media")
        else:
            warn("Coincidencia baja — prueba con otra consulta")


# ─── 3. Clasificación ─────────────────────────────────────────────────────


def clasificacion():
    titulo("3️⃣  CLASIFICACIÓN — sin entrenar modelos")

    categorias = {
        "urgente": [
            "se ha caído el servidor de producción",
            "necesito esto para mañana sin falta",
            "error crítico en la base de datos",
        ],
        "normal": [
            "te envío el documento actualizado",
            "confirma si has recibido el email",
        ],
        "spam": [
            "gana dinero rápido desde casa",
            "has ganado un premio exclusivo",
        ],
        "cocina": [
            "receta de paella valenciana",
            "cómo hacer pan casero",
        ],
    }

    # Pre-calcular embedding de cada categoría (promedio de ejemplos)
    info("Calculando firmas de categorías...")
    firmas = {}
    for cat, ejemplos in categorias.items():
        vecs = [emb(e) for e in ejemplos]
        firmas[cat] = [sum(vals) / len(vals) for vals in zip(*vecs)]
        ok(f"Categoría '{cat}' lista ({len(ejemplos)} ejemplos)")

    # Textos a clasificar
    textos_prueba = [
        "el servidor web no responde desde las 3 de la mañana",
        "te adjunto la factura de este mes para tu revisión",
        "FELICIDADES HAS GANADO UN VIAJE AL CARIBE",
        "cómo hacer una tortilla de patatas con cebolla caramelizada",
    ]

    for texto in textos_prueba:
        vec = emb(texto)
        resultados = sorted(
            [(cat, cos_sim(vec, firma)) for cat, firma in firmas.items()],
            key=lambda x: -x[1],
        )
        mejor = resultados[0]
        barra = "█" * int(mejor[1] * 20) + "░" * (20 - int(mejor[1] * 20))
        print(f'\n  📝 "{texto[:60]}..."')
        print(f"  → {barra} {NEGR}{mejor[0]}{FIN} ({mejor[1]:.1%})")


# ─── 4. Deduplicación ─────────────────────────────────────────────────────


def deduplicacion():
    titulo("4️⃣  DEDUPLICACIÓN — encontrar contenido repetido")

    items = [
        {"id": "doc1", "texto": "Cómo instalar Ollama en Ubuntu 24.04 paso a paso"},
        {"id": "doc2", "texto": "Guía completa para instalar Ollama en Ubuntu 24.04"},
        {"id": "doc3", "texto": "Receta de paella valenciana tradicional con marisco"},
        {"id": "doc4", "texto": "Cómo configurar nftables como firewall en Linux"},
        {"id": "doc5", "texto": "Instalación de Ollama en Ubuntu 24.04 - tutorial"},
    ]

    info("Generando embeddings...")
    embebidos = [(item, emb(item["texto"])) for item in items]

    print(f"\n  {'Item':<10} {'Similitud con':<20} {'Score':<8}")
    print(f"  {'-' * 40}")
    for i, (item_a, emb_a) in enumerate(embebidos):
        for j, (item_b, emb_b) in enumerate(embebidos[i + 1 :], i + 1):
            sim = cos_sim(emb_a, emb_b)
            status = (
                "🔴 DUPLICADO"
                if sim > 0.90
                else "🟡 PARECIDO"
                if sim > 0.75
                else "🟢 DISTINTO"
            )
            print(f"  {item_a['id']:<10} {item_b['id']:<20} {sim:.4f}  {status}")

    # Detectar duplicados
    duplicados = [
        (item_a, item_b, sim)
        for (item_a, emb_a), (item_b, emb_b) in [
            ((items[i], embebidos[i][1]), (items[j], embebidos[j][1]))
            for i in range(len(items))
            for j in range(i + 1, len(items))
        ]
        if (sim := cos_sim(emb_a, emb_b)) > 0.85
    ]

    if duplicados:
        print(
            f"\n  {NEGR}🔴 Se detectaron {len(duplicados)} grupos de duplicados:{FIN}"
        )
        for a, b, sim in duplicados:
            print(f'    • "{a["texto"][:50]}..."')
            print(f'      "{b["texto"][:50]}..."')
            print(f"      → Similitud: {sim:.1%}")
    else:
        info("No se detectaron duplicados significativos")


# ─── 5. Recomendación ─────────────────────────────────────────────────────


def recomendacion():
    titulo("5️⃣  RECOMENDACIÓN — más como esto")

    articulos = [
        "Linux es el mejor sistema operativo para servidores",
        "Cómo instalar Docker en Ubuntu 24.04",
        "Introducción a Rust: seguridad de memoria sin recolector de basura",
        "PostgreSQL con pgvector para búsqueda semántica",
        "Ollama: modelos de lenguaje locales en tu servidor",
        "Configurar nftables como firewall en Linux",
        "Rust vs Go: comparativa para backend en 2026",
        "Embeddings: coordenadas en un mapa de significados",
    ]

    info("Generando embeddings de {len(articulos)} artículos...")
    index = [(tit, emb(tit)) for tit in articulos]

    # Elegir un artículo de referencia
    ref = "Ollama: modelos de lenguaje locales en tu servidor"
    print(f'\n  📖 Artículo de referencia: "{ref}"')
    print(f"\n  {NEGR}📚 Recomendaciones:{FIN}")

    vec_ref = emb(ref)
    recomendados = sorted(
        [(tit, cos_sim(vec_ref, vec)) for tit, vec in index if tit != ref],
        key=lambda x: -x[1],
    )

    for i, (tit, sim) in enumerate(recomendados[:4], 1):
        barra = "█" * int(sim * 20) + "░" * (20 - int(sim * 20))
        print(f"  {i}. {barra} {sim:.4f}  {tit}")


# ─── 6. ChromaDB ──────────────────────────────────────────────────────────


def chroma_demo():
    titulo("6️⃣  CHROMADB — base de datos vectorial")

    try:
        import chromadb
        from chromadb.utils import embedding_functions
    except ImportError:
        warn("chromadb no instalado. Ejecuta: pip install chromadb")
        return

    # Limpiar y crear
    if os.path.exists(DIR_CHROMA):
        shutil.rmtree(DIR_CHROMA)

    info("Creando base de datos vectorial con ChromaDB...")
    client = chromadb.PersistentClient(path=DIR_CHROMA)
    collection = client.get_or_create_collection(
        name="demo_817",
        embedding_function=embedding_functions.OllamaEmbeddingFunction(
            url=f"{OLLAMA_URL}/api/embeddings", model_name=MODEL
        ),
    )

    # Documentos de prueba
    docs = [
        "Linux es el mejor sistema operativo para servidores por su estabilidad",
        "Para instalar Docker en Ubuntu ejecuta sudo apt install docker.io",
        "La paella valenciana lleva arroz, pollo, conejo, judía y garrofón",
        "Ollama permite ejecutar modelos de lenguaje localmente en tu hardware",
        "Rust es un lenguaje de programación de sistemas seguro y rápido",
        "PostgreSQL con pgvector permite búsqueda semántica sobre documentos",
        "Los embeddings convierten texto en vectores numéricos",
        "El jamón ibérico de bellota se diferencia por la dieta del cerdo",
    ]
    metadatos = [
        {"tema": "linux"},
        {"tema": "linux"},
        {"tema": "cocina"},
        {"tema": "ia"},
        {"tema": "programacion"},
        {"tema": "ia"},
        {"tema": "ia"},
        {"tema": "cocina"},
    ]
    ids = [f"doc_{i}" for i in range(len(docs))]

    collection.add(documents=docs, metadatas=metadatos, ids=ids)
    ok(f"{len(docs)} documentos indexados en ChromaDB")

    # Búsqueda
    consultas = [
        "¿Qué framework usar para backend?",
        "Dime algo de cocina",
    ]

    for consulta in consultas:
        print(f'\n  {NEGR}🔍 Consulta:{FIN} "{consulta}"')
        results = collection.query(query_texts=[consulta], n_results=3)

        for doc, meta, dist in zip(
            results["documents"][0], results["metadatas"][0], results["distances"][0]
        ):
            conf = 1 - dist
            barra = "█" * int(conf * 20) + "░" * (20 - int(conf * 20))
            print(f"    {barra} {conf:.1%}  [{meta['tema']}] {doc}")

        # Filtro por metadatos
        print(f"     🔽 Con filtro [tema=linux]:")
        filtrados = collection.query(
            query_texts=[consulta], n_results=2, where={"tema": "linux"}
        )
        for doc in filtrados["documents"][0]:
            print(f"       🐧 {doc}")

    # Persistencia
    if os.path.exists(DIR_CHROMA):
        ok(f"Datos persistentes en {DIR_CHROMA}/")
        info("Cierra y abre Python — los datos siguen ahí")

    # Limpiar
    shutil.rmtree(DIR_CHROMA)
    info("ChromaDB limpiada (demo temporal)")


# ─── MAIN ──────────────────────────────────────────────────────────────────

PASOS = {
    1: ("Crear documentos de prueba", crear_documentos, True),
    2: ("Búsqueda semántica", busqueda_semantica, True),
    3: ("Clasificación", clasificacion, True),
    4: ("Deduplicación", deduplicacion, True),
    5: ("Recomendación", recomendacion, True),
    6: ("ChromaDB", chroma_demo, False),
}


def listar_pasos():
    print(f"\n{'=' * 60}")
    print(f"  PASOS DISPONIBLES")
    print(f"{'=' * 60}")
    for n in sorted(PASOS):
        nombre, _, esencial = PASOS[n]
        req = " (requiere chromadb)" if not esencial else ""
        print(f"  {n}. {nombre}{req}")
    print(f"\n  --fast  omite el paso 6 (ChromaDB)")
    print(f"  --interactive  pide confirmación entre pasos\n")


def ejecutar_paso(n, limpiar_al_final=False):
    if n not in PASOS:
        warn(f"Paso {n} no válido. Usa --list para ver los pasos.")
        return
    nombre, fn, _ = PASOS[n]
    info(f"Ejecutando paso {n}: {nombre}...")
    fn()
    if limpiar_al_final and os.path.exists(DIR_DOCS):
        shutil.rmtree(DIR_DOCS)


def main():
    parser = argparse.ArgumentParser(
        description="Episodio 817 — Embeddings para todo. Demo completa.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent("""\
            Ejemplos:
              python3 setup_completo.py                          # todo
              python3 setup_completo.py --fast                   # sin ChromaDB
              python3 setup_completo.py --list                   # listar pasos
              python3 setup_completo.py --step 3                 # solo paso 3
              python3 setup_completo.py --step 1,3,5             # pasos 1,3,5
              python3 setup_completo.py --interactive            # paso a paso
        """),
    )
    parser.add_argument("--fast", action="store_true", help="Omite ChromaDB (paso 6)")
    parser.add_argument(
        "--list", "-l", action="store_true", help="Lista los pasos disponibles"
    )
    parser.add_argument(
        "--step",
        "-s",
        type=str,
        help="Ejecuta paso(s) específicos: 3, 1,3,5, 1-4",
    )
    parser.add_argument(
        "--interactive",
        "-i",
        action="store_true",
        help="Ejecuta paso a paso con confirmación",
    )
    args = parser.parse_args()

    if args.list:
        listar_pasos()
        return

    print(f"""
{NEGR}🚀 EPISODIO 817 — Embeddings para todo{FIN}
{NEGR}   Demo completa: búsqueda, clasificación, deduplicación, recomendación, ChromaDB{FIN}

{NEGR}Requisitos:{FIN}
  • Ollama en {OLLAMA_URL}  →  {VERDE}ollama serve{FIN}
  • Modelo: {MODEL}         →  {VERDE}ollama pull {MODEL}{FIN}
  • Python 3.10+
  • pip install chromadb    (opcional, para el bloque 6)
""")

    if not check_ollama():
        sys.exit(1)

    # Resolver qué pasos ejecutar
    if args.step:
        pasos = set()
        for parte in args.step.split(","):
            parte = parte.strip()
            if "-" in parte:
                a, b = parte.split("-", 1)
                pasos.update(range(int(a), int(b) + 1))
            else:
                pasos.add(int(parte))
        pasos = sorted(pasos)
        info(f"Ejecutando pasos: {', '.join(str(p) for p in pasos)}")
        for p in pasos:
            ejecutar_paso(p)
        if os.path.exists(DIR_DOCS):
            shutil.rmtree(DIR_DOCS)
    elif args.interactive:
        info(
            "Modo interactivo: presiona Enter para avanzar, 's' para saltar, 'q' para salir"
        )
        for n in sorted(PASOS):
            nombre, fn, esencial = PASOS[n]
            if args.fast and n == 6:
                info(f"Paso {n} omitido (--fast)")
                continue
            r = (
                input(f"\n{NEGR}¿Ejecutar paso {n}: {nombre}?{FIN} [Enter/s/q] ")
                .strip()
                .lower()
            )
            if r == "q":
                info("Interrumpido por el usuario")
                break
            if r == "s":
                info(f"Paso {n} saltado")
                continue
            fn()
        if os.path.exists(DIR_DOCS):
            shutil.rmtree(DIR_DOCS)
    else:
        crear_documentos()
        busqueda_semantica()
        clasificacion()
        deduplicacion()
        recomendacion()

        if not args.fast:
            chroma_demo()
        else:
            info("Modo rápido: omitiendo ChromaDB")
            info("Ejecuta sin --fast para incluir ChromaDB")

        if os.path.exists(DIR_DOCS):
            shutil.rmtree(DIR_DOCS)

    print(f"\n{NEGR}{'=' * 60}{FIN}")
    print(f"\n{VERDE}🎉 Demo completada{FIN}")
    print(f"  • 6 casos de uso ejecutados")
    print(f"  • 1 modelo de embeddings ({MODEL})")
    print(f"  • 0 modelos entrenados, 0 GPUs, 0€")
    print(
        f"\n  {AZUL}📖 Escaleta completa en: episodio-817-escaleta-embeddings.md{FIN}"
    )
    print(f"  {AZUL}🔗 https://github.com/aejimmi/fail2ban-rs{FIN}")


if __name__ == "__main__":
    main()
