#!/usr/bin/env python3
"""Por donde va la migracion, en la terminal.

    ./estado.py           las barras
    ./estado.py -v        y que falta exactamente en cada hito
    ./estado.py --rapido  sin ejecutar nada, solo mirar ficheros

El porcentaje NO se escribe a mano: sale de comprobaciones de verdad —que un
fichero exista, que una funcion este, que una prueba pase—. Un contador que se
actualiza a mano miente a la semana, y ademas es justo lo que uno quiere que no
mienta: si dice 60 % es porque tres de cinco comprobaciones pasan ahora mismo.

Las que cuestan tiempo (arrancar whisper, hablar con ollama) van marcadas como
lentas y se saltan con --rapido.
"""
import argparse
import os
import subprocess
import sys

AQUI = os.path.dirname(os.path.abspath(__file__))
RAIZ = os.path.dirname(AQUI)
VENV = os.path.join(AQUI, "venv", "bin", "python")


# ------------------------------------------------------------- comprobantes

def hay(*trozos) -> bool:
    return os.path.exists(os.path.join(AQUI, *trozos))


def dentro(fichero, texto) -> bool:
    try:
        with open(os.path.join(AQUI, fichero), encoding="utf-8") as f:
            return texto in f.read()
    except OSError:
        return False


def corre(orden, espera=90) -> bool:
    try:
        r = subprocess.run(orden, cwd=AQUI, capture_output=True, timeout=espera)
        return r.returncode == 0
    except (OSError, subprocess.TimeoutExpired):
        return False


def modulo(nombre) -> bool:
    return corre([VENV if os.path.exists(VENV) else sys.executable,
                  "-c", f"import {nombre}"], espera=30)


def sin_comentarios(fichero, texto) -> bool:
    """Si el texto aparece en una linea que NO es un comentario."""
    try:
        with open(os.path.join(AQUI, fichero), encoding="utf-8") as f:
            for l in f:
                limpia = l.strip()
                if limpia and not limpia.startswith(("#", "//", "*")) and texto in limpia:
                    return True
    except OSError:
        pass
    return False


def catalogo_tiene(n) -> bool:
    try:
        import json
        with open(os.path.join(AQUI, "datos", "acciones.json"), encoding="utf-8") as f:
            return len(json.load(f)["acciones"]) >= n
    except Exception:
        return False


# ---------------------------------------------------------------- los hitos
#
# Cada comprobacion es (que se comprueba, como, si es lenta). El orden dentro
# de cada hito va de lo mas basico a lo mas fino, que es como se construyo.

HITOS = [
    ("la boca", "hablar por los altavoces", [
        ("el modulo esta", lambda: hay("boca", "voz.py"), False),
        ("hay tres voces o mas", lambda: dentro("boca/voz.py", '"claude"'), False),
        ("cache de frases por sha1", lambda: dentro("boca/voz.py", "_clave"), False),
        ("sabe si esta sonando", lambda: dentro("boca/voz.py", "def hablando"), False),
        ("habla de verdad", lambda: corre(
            [VENV, "jarvis.py", "--di", "prueba"], 60), True),
    ]),

    ("el oido", "escuchar y transcribir", [
        ("captura con pw-record", lambda: dentro("oido/captura.py", "pw-record"), False),
        ("detector de voz Silero", lambda: modulo("pysilero_vad"), False),
        ("cliente de whisper-server", lambda: hay("oido", "whisper.py"), False),
        ("filtro de alucinaciones", lambda: dentro("oido/whisper.py", "def es_ruido"), False),
        ("guarda contra el eco", lambda: dentro("oido/captura.py", "boca.hablando"), False),
        ("whisper arranca y transcribe", lambda: corre(
            [VENV, "-c",
             "import sys;sys.path.insert(0,'.');from oido.whisper import Oido;"
             "o=Oido();import sys as s;s.exit(0 if o.arranca() else 1)"], 90), True),
    ]),

    ("las manos", "ejecutar las 150 acciones", [
        ("el emparejador copiado", lambda: hay("manos", "acciones.py"), False),
        ("las tareas en paralelo", lambda: hay("manos", "tareas.py"), False),
        ("el catalogo entero", lambda: catalogo_tiene(150), False),
        ("una sola puerta para ejecutar", lambda: dentro("manos/__init__.py", "class Manos"), False),
        ("compara tambien por sonido", lambda: hay("manos", "fonetica.py"), False),
        ("banco fijo para medir", lambda: hay("datos", "banco.json"), False),
        ("las pruebas en verde", lambda: corre(
            [VENV, "pruebas/prueba_manos.py"], 120), True),
    ]),

    ("el cerebro", "pensar lo que el catalogo no cubre", [
        ("cliente de ollama", lambda: hay("cerebro", "ollama.py"), False),
        ("dos herramientas, no una", lambda: dentro("cerebro/ollama.py", "comando_de_shell"), False),
        ("no puede inventarse datos", lambda: dentro("cerebro/ollama.py", "REGLA QUE NO SE ROMPE"), False),
        ("tope de vueltas", lambda: dentro("cerebro/ollama.py", "VUELTAS"), False),
        ("cadena catalogo-primero", lambda: dentro("jarvis.py", "def atiende"), False),
        ("ollama contesta", lambda: corre(
            ["curl", "-sf", "-m", "10", "http://127.0.0.1:11434/api/tags"], 20), True),
    ]),

    ("la palabra", "que le llamen JARVIS", [
        ("el centinela existe", lambda: hay("oido", "despierta.py"), False),
        ("se llama JARVIS", lambda: dentro("oido/despierta.py", 'NOMBRE = "jarvis"'), False),
        ("aguanta como lo oye Whisper", lambda: dentro("oido/despierta.py", "PARECIDAS"), False),
        ("la orden va en la misma frase", lambda: dentro("oido/despierta.py",
                                                         "def parte_el_nombre"), False),
        ("conectado al bucle de voz", lambda: dentro("jarvis.py", "centinela.filtra"), False),
        # El que de verdad importaba: sin el prompt del nombre, Whisper nunca
        # escribe JARVIS y el centinela no despierta jamas.
        ("prompt del nombre para Whisper", lambda: dentro("oido/whisper.py",
                                                          "PROMPT_NOMBRE"), False),
        ("boton para dormirlo y despertarlo", lambda: dentro("cara/panel.py",
                                                             'dato == "sueno"'), False),
        ("prueba de voz de punta a punta", lambda: hay("pruebas", "prueba_voz.py"), False),
        ("y la palabra va APAGADA por defecto", lambda: dentro("jarvis.py",
                                                               "activo=a.palabra"), False),
    ]),

    ("la cara", "el panel", [
        ("el HUD copiado", lambda: hay("cara", "hud", "index.html"), False),
        ("servidor http y websocket", lambda: hay("cara", "servidor.py"), False),
        # El WebSocket vive en puente.js, no en hud.js: hud.js no se toca a
        # proposito y el puente suplanta window.webkit por debajo.
        ("puente por WebSocket", lambda: dentro("cara/hud/puente.js", "WebSocket"), False),
        ("y hud.js sigue sin tocarse", lambda: dentro("cara/hud/hud.js",
                                                      "messageHandlers"), False),
        ("el panel se conecta al nucleo", lambda: dentro("cara/servidor.py", "async"), False),
        # Ojo con las comprobaciones flojas: esta buscaba la palabra "tarea" en
        # servidor.py y pasaba porque salia en un COMENTARIO, con las tarjetas
        # sin conectar. Una barra que miente es peor que no tener barra.
        ("tarjetas de tareas en marcha", lambda: dentro("cara/panel.py",
                                                        "GestorTareas"), False),
        ("la consola lleva la conversacion", lambda: dentro("cara/hud/hud.js",
                                                            "aConsolaDicho"), False),
    ]),

    ("el dialogo", "que pregunte en vez de recitar", [
        ("las preguntas salen del catalogo", lambda: hay("manos", "preguntar.py"), False),
        ("pulsar pregunta lo que falta", lambda: dentro("cara/panel.py", "_pregunta("), False),
        ("la respuesta hablada la recoge", lambda: dentro("jarvis.py", "panel.responde"), False),
        ("lo destructivo confirma", lambda: dentro("cara/panel.py", "accion.confirmar"), False),
        ("y el panel enseña que espera", lambda: dentro("cara/hud/hud.js", "preguntando"), False),
        ("la prueba pasa", lambda: corre([VENV, "pruebas/prueba_dialogo.py"], 120), True),
    ]),

    ("el candado", "que no obedezca a cualquiera", [
        ("el candado en el HUD", lambda: dentro("cara/hud/index.html", "candado"), False),
        ("lo atiende el nucleo", lambda: dentro("cara/panel.py", "_intenta_desbloquear"), False),
        ("valida con la contrasena del sistema", lambda: dentro("cara/panel.py",
                                                                "GestorRoot"), False),
        ("con el candado puesto no ejecuta", lambda: dentro("cara/panel.py",
                                                            "no hay ordenes"), False),
        ("y la VOZ tampoco obedece", lambda: dentro("jarvis.py", "(bloqueado)"), False),
        ("la prueba pasa", lambda: corre([VENV, "pruebas/prueba_candado.py"], 120), True),
    ]),

    ("el saber", "conocer COFRE entero", [
        ("indexador de COFRE", lambda: hay("saber", "indexa.py"), False),
        ("busqueda por significado", lambda: hay("saber", "busca.py"), False),
        ("indice construido", lambda: hay("datos", "saber.jsonl"), False),
        ("el cerebro lo consulta antes de responder", lambda: dentro(
            "cerebro/ollama.py", "def _saber"), False),
        ("filtra el codigo de terceros", lambda: dentro("saber/indexa.py", "IGNORA"), False),
        ("indexa la documentacion de Linux", lambda: hay("saber", "sistema.py"), False),
        ("y los servidores SSH", lambda: dentro("saber/sistema.py", "def servidores"), False),
        ("el cerebro puede ejecutar como root", lambda: dentro("manos/__init__.py",
                                                              "necesita_root"), False),
        ("root ligado al candado", lambda: dentro("cara/panel.py", "root.recuerda"), False),
    ]),

    ("el pipeline", "aprender del uso", [
        ("diario de lo que oye y hace", lambda: hay("manos", "diario.py"), False),
        ("apunta la VIA de cada frase", lambda: dentro("jarvis.py", 'anota(texto, "ruido")'), False),
        ("y tambien los aciertos", lambda: dentro("jarvis.py", '"catalogo", accion.id'), False),
        ("herramienta para revisarlo", lambda: hay("mejora.py"), False),
        ("propone, NO commitea solo", lambda: dentro("mejora.py", "Esto NO cambia nada"), False),
        ("mide contra el banco fijo", lambda: hay("datos", "banco.json"), False),
    ]),

    ("paridad", "usarlo a diario", [
        ("icono naranja", lambda: hay("jarvis-naranja.svg"), False),
        ("icono instalado", lambda: os.path.exists(os.path.expanduser(
            "~/.local/share/icons/jarvis-nucleo.png")), False),
        ("lanzador propio", lambda: hay("arranca.sh"), False),
        ("ventana propia, no pestaña", lambda: hay("ventana.sh"), False),
        # Se miran las lineas de CODIGO, no el fichero entero: el comentario que
        # explica por que ya no se usa xdg-open contiene la palabra, y una
        # comprobacion ingenua se cree que sigue ahi. Es la tercera vez que la
        # barra casi miente por buscar texto suelto en vez de codigo.
        ("una sola forma de abrirla", lambda: not sin_comentarios(
            "arranca.sh", "xdg-open"), False),
        ("la barra de tareas la reconoce", lambda: "StartupWMClass" in open(
            os.path.expanduser("~/.local/share/applications/jarvis-nucleo.desktop")).read(), False),
        ("entrada en el escritorio", lambda: os.path.exists(os.path.expanduser(
            "~/.local/share/applications/jarvis-nucleo.desktop")), False),
        ("Newelle congelado y aparte", lambda: corre(
            ["git", "-C", RAIZ, "rev-parse", "jarvis-newelle-final"], 15), False),
    ]),
]


# ------------------------------------------------------------------ pintarlo

ANCHO = 34


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


def barra(pct, hecho, total):
    lleno = round(ANCHO * pct / 100)
    # verde entero, amarillo a medias, gris sin empezar
    c = "32" if pct == 100 else ("33" if pct > 0 else "90")
    return color("█" * lleno, c) + color("·" * (ANCHO - lleno), "90")


def main() -> int:
    p = argparse.ArgumentParser()
    p.add_argument("-v", "--detalle", action="store_true")
    p.add_argument("--rapido", action="store_true",
                   help="sin ejecutar nada, solo mirar ficheros")
    a = p.parse_args()

    if not os.path.exists(VENV):
        print(color("  aviso: no hay venv; algunas comprobaciones no se pueden hacer\n", "33"))

    print()
    print(color("  JARVIS · migracion a nucleo propio", "1"))
    print()

    hechas = totales = 0
    for i, (nombre, para_que, comprobaciones) in enumerate(HITOS, 1):
        activas = [c for c in comprobaciones if not (a.rapido and c[2])]
        pasan = []
        for texto, prueba, lenta in activas:
            try:
                ok = bool(prueba())
            except Exception:
                ok = False
            pasan.append((texto, ok, lenta))
        n = sum(1 for _, ok, _ in pasan if ok)
        pct = 100 * n / len(pasan) if pasan else 0
        hechas += n
        totales += len(pasan)

        marca = color("✓", "32") if pct == 100 else (" " if pct == 0 else color("·", "33"))
        print(f"  {marca} {i}. {nombre:12s} {barra(pct, n, len(pasan))} "
              f"{pct:3.0f}%  {n}/{len(pasan)}   {color(para_que, '90')}")

        if a.detalle:
            for texto, ok, lenta in pasan:
                s = color("  ok ", "32") if ok else color("  --", "31")
                print(f"        {s} {texto}" + (color("  (lenta)", "90") if lenta else ""))
            print()

    print()
    total_pct = 100 * hechas / totales if totales else 0
    print(f"    {'TOTAL':14s} {barra(total_pct, hechas, totales)} "
          f"{total_pct:3.0f}%  {hechas}/{totales}")
    print()
    if a.rapido:
        print(color("    (rapido: sin las comprobaciones que ejecutan cosas)\n", "90"))
    return 0


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