#!/usr/bin/env python3
"""Diagnostico del RAG: comprueba la cadena entera y dice que falla y como arreglarlo.

    python -m saber.diagnostico            comprobaciones rapidas (segundos)
    python -m saber.diagnostico --eval     ademas mide hit@k con el eval_set
    python -m saber.diagnostico --verboso  ensena el detalle de cada resultado

Pensado para cuando "el RAG no responde bien": en vez de adivinar, corres esto y
te marca en verde/rojo cada eslabon —dependencias, indice, embedder, la busqueda,
y opcionalmente ollama— con una linea de que hacer si falla. Sale con codigo != 0
si hay algun fallo duro, para poder meterlo en CI.
"""
import argparse
import os
import sys
import traceback

AQUI = os.path.dirname(os.path.abspath(__file__))
NUCLEO = os.path.dirname(AQUI)
if NUCLEO not in sys.path:
    sys.path.insert(0, NUCLEO)

_ok = _fallo = _aviso = 0


def _color(txt, c):
    return f"\033[{c}m{txt}\033[0m" if sys.stdout.isatty() else txt


def marca(estado, nombre, detalle="", arregla=""):
    global _ok, _fallo, _aviso
    etq = {"ok": _color("OK   ", "32"), "fallo": _color("FALLO", "31"),
           "aviso": _color("AVISO", "33")}[estado]
    print(f"  {etq} {nombre}")
    if detalle:
        print(f"        {detalle}")
    if arregla and estado != "ok":
        print(f"        {_color('→ ' + arregla, '90')}")
    _ok += estado == "ok"
    _fallo += estado == "fallo"
    _aviso += estado == "aviso"


# ── Consultas de humo: valen tanto en el indice publico como en el personal ──
# (consulta, [palabras que un buen resultado deberia contener, cualquiera vale])
HUMO = [
    ("como cambiar los permisos de un fichero", ["chmod"]),
    ("como saco una shell reversa", ["/dev/tcp", "nc ", "bash -i", "reversa"]),
    ("escanear puertos de una maquina", ["nmap"]),
    ("alternativa self-hosted a google analytics", ["analytic", "plausible",
                                                    "matomo", "goatcounter", "umami"]),
]


def check_deps():
    try:
        import numpy  # noqa: F401
        import model2vec  # noqa: F401
        marca("ok", "dependencias (numpy, model2vec) importan")
        return True
    except ImportError as e:
        marca("fallo", "dependencias", f"falta: {e.name}",
              "instala con: pip install -r requirements.txt (o activa el venv)")
        return False


def check_indice():
    from saber import busca
    fuente = busca._fuente()
    if not fuente:
        marca("fallo", "indice presente", "no hay saber.jsonl ni saber-publico.jsonl",
              "corre nucleo/saber/indexa.py, o comprueba que el repo trae "
              "nucleo/datos/saber-publico.jsonl")
        return None
    cual = "personal" if fuente.endswith("saber.jsonl") else "publico (del repo)"
    kb = os.path.getsize(fuente) // 1024
    marca("ok", "indice presente", f"{cual}: {os.path.basename(fuente)} ({kb} KB)")
    return busca


def check_carga(busca):
    try:
        if not busca._carga():
            marca("fallo", "carga del indice", "el indice esta vacio o ilegible",
                  "reconstruyelo con nucleo/saber/indexa.py")
            return False
    except Exception as e:
        marca("fallo", "carga del indice", f"{type(e).__name__}: {e}",
              "el jsonl puede estar corrupto; reconstruye con indexa.py")
        if os.environ.get("JARVIS_DEBUG"):
            traceback.print_exc()
        return False
    n = len(busca._entradas)
    tipos = {}
    for e in busca._entradas:
        tipos[e.get("tipo", "?")] = tipos.get(e.get("tipo", "?"), 0) + 1
    resumen = ", ".join(f"{k}:{v}" for k, v in sorted(tipos.items(),
                                                      key=lambda x: -x[1])[:6])
    marca("ok", "carga del indice", f"{n} entradas ({resumen})")
    # avisos si falta contenido que deberia venir con el repo
    if tipos.get("metodologia", 0) == 0:
        marca("aviso", "pack de conocimiento", "0 fragmentos de metodologia",
              "deberia venir en nucleo/saber/conocimiento/; revisa el clon")
    if tipos.get("catalogo", 0) == 0:
        marca("aviso", "catalogo (awesome lists)", "0 fragmentos de catalogo",
              "clona listas en ~/COFRE/CODERS/ y reindexa, o usa el indice publico")
    return True


def check_busqueda(busca, verboso):
    todo_ok = True
    for consulta, esperadas in HUMO:
        try:
            res = busca.busca(consulta, cuantos=5)
        except Exception as e:
            marca("fallo", f"busqueda: '{consulta}'", f"{type(e).__name__}: {e}",
                  "falla el embedder o la busqueda; revisa model2vec")
            todo_ok = False
            continue
        blob = " ".join(r.get("texto", "").lower() for r in res)
        acierto = any(p.lower() in blob for p in esperadas)
        top = (res[0].get("herramienta") or res[0].get("perfil") or "?") if res else "(nada)"
        if acierto:
            marca("ok", f"busqueda: '{consulta}'", f"top: {top}" if verboso else "")
        else:
            marca("aviso", f"busqueda: '{consulta}'",
                  f"top: {top} — no contiene {esperadas}",
                  "recuperacion floja para esta consulta; puede faltar contenido")
    return todo_ok


def check_ollama():
    import json
    import urllib.request
    endpoint = os.environ.get("JARVIS_OLLAMA", "http://127.0.0.1:11434")
    try:
        with urllib.request.urlopen(endpoint + "/api/tags", timeout=5) as r:
            modelos = [m["name"] for m in json.load(r).get("models", [])]
    except Exception as e:
        marca("aviso", "ollama (para el cerebro)", f"no responde en {endpoint}: {e}",
              "el RAG recupera sin ollama, pero el asistente necesita: "
              "arranca ollama y descarga el modelo (ollama pull qwen3.5:4b)")
        return
    if not modelos:
        marca("aviso", "ollama (para el cerebro)", "responde pero sin modelos",
              "descarga uno: ollama pull qwen3.5:4b")
        return
    marca("ok", "ollama (para el cerebro)", f"{len(modelos)} modelos: {', '.join(modelos[:4])}")


def check_eval(busca):
    """Mide hit@k con el conjunto de evaluacion, si esta."""
    import json
    ruta = os.path.join(AQUI, "enriquece", "eval_set.jsonl")
    if not os.path.exists(ruta):
        marca("aviso", "eval hit@k", "no hay enriquece/eval_set.jsonl")
        return
    casos = [json.loads(l) for l in open(ruta, encoding="utf-8") if l.strip()]
    h1 = h5 = 0
    for c in casos:
        res = busca.busca(c["pregunta"], cuantos=5)
        textos = [r.get("texto", "").lower() for r in res]
        esperado = [p.lower() for p in c.get("espera", c.get("contiene", []))]
        # misma semantica que evalua.py: basta que UNA palabra esperada aparezca
        acierta = lambda t: any(p and p in t for p in esperado)
        if res and acierta(textos[0]):
            h1 += 1
        if any(acierta(t) for t in textos):
            h5 += 1
    n = len(casos) or 1
    est = "ok" if 100 * h1 // n >= 50 else "aviso"
    marca(est, "eval hit@k",
          f"hit@1 {h1}/{n} ({100*h1//n}%)   hit@5 {h5}/{n} ({100*h5//n}%)",
          "recuperacion baja: reconstruye el indice o revisa el eval_set")


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--eval", action="store_true", help="ademas mide hit@k")
    p.add_argument("--verboso", action="store_true", help="ensena el top de cada busqueda")
    a = p.parse_args()

    print("\n  Diagnostico del RAG de JARVIS\n  " + "─" * 40)
    if not check_deps():
        print(f"\n  {_color('Para el resto hacen falta las dependencias.', '31')}\n")
        return 1
    busca = check_indice()
    if busca and check_carga(busca):
        check_busqueda(busca, a.verboso)
        if a.eval:
            check_eval(busca)
    check_ollama()

    print("  " + "─" * 40)
    resumen = f"  {_ok} OK"
    if _aviso:
        resumen += f"   {_aviso} avisos"
    if _fallo:
        resumen += f"   {_color(str(_fallo) + ' fallos', '31')}"
    print(resumen + "\n")
    return 1 if _fallo else 0


if __name__ == "__main__":
    sys.exit(main())
