#!/usr/bin/env python3
"""Ejecuta las acciones de verdad y mira si funcionan y si se entienden.

    python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py
    python3 ~/COFRE/CODERS/JARVIS/config/probar_acciones.py --romper   # solo las malas

Las 121 acciones se escribieron a mano y casi ninguna se ha ejecutado nunca.
Alguna tendra el comando roto, la salida vacia, o una plantilla que apunta a un
campo que no existe —y entonces JARVIS diria una frase sin sentido en voz alta.
Esto las corre, captura la salida y las clasifica, para saber cuales arreglar.

Que hace con cada tipo:
  - Solo lectura (df, free, ps, cat /sys...): las EJECUTA y comprueba la salida.
    Son la mayoria y las que de verdad importan, porque son las que hay que
    "interpretar bien".
  - Con efecto (abre navegador, apaga pantalla, bloquea, sube volumen): NO las
    ejecuta —tendria efectos de verdad— pero valida que el comando exista.
  - Destructivas (confirmar=True): NO las ejecuta, solo comprueba el comando.
  - Con argumento: las ejecuta con un valor de prueba inofensivo.

Clasificacion de cada accion:
  OK        corrio, dio salida, y la respuesta hablada sale con datos reales
  VACIA     corrio pero sin salida (la herramienta no aplica en esta maquina)
  ROTA      el comando fallo o no existe
  PLANTILLA la respuesta cayo al volcado crudo: la plantilla pide un campo que
            la salida no tiene. Es el fallo que hace que JARVIS suene raro.
  EFECTO    no se ejecuta por tener efectos; comando validado
"""
import argparse
import importlib.util
import json
import os
import re
import shlex
import subprocess
import sys

JARVIS = os.path.expanduser("~/COFRE/CODERS/JARVIS")
MOTOR = os.path.join(JARVIS, "newelle/src/utility/acciones.py")
CATALOGO = os.path.expanduser(
    "~/.local/share/flatpak/app/io.github.qwersyk.Newelle/x86_64/master/"
    "active/files/share/newelle/acciones/acciones.json")
CATALOGO_FUENTE = os.path.join(JARVIS, "newelle/data/acciones/acciones.json")
INFORME = os.path.join(JARVIS, "muestras/acciones-salud.json")

# Órdenes que cambian algo del sistema o abren cosas: se validan pero no se
# lanzan, que probar un asistente no deberia dejarte el navegador con veinte
# pestañas ni la pantalla apagada.
EFECTOS = ["firefox", "xdg-open", "loginctl", "xset", "pactl set", "pkill",
           "brightnessctl", "systemctl start", "systemctl stop", "import -window",
           "gnome-screenshot", "rm ", "kill", "--private-window", "dpms",
           "ventanas.sh", "xdotool", "wmctrl", "xrandr --output"]

# Valores de prueba para las acciones con argumento, inofensivos a proposito.
ARGUMENTOS = {
    "buscar_fichero": "acciones", "buscar_carpeta": "config",
    "leer_fichero": "README", "abrir_fichero": "no_existe_xyz",
    "abrir_carpeta": "no_existe_xyz", "tamano_carpeta": "config",
    "proceso_buscar": "python", "proceso_matar": "proceso_inexistente_xyz",
    "instalado": "bash", "manual": "ls",
}


def carga_motor():
    spec = importlib.util.spec_from_file_location("acciones", MOTOR)
    m = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(m)
    return m


def spawn():
    return ["flatpak-spawn", "--host"] if os.path.exists("/.flatpak-info") else []


def tiene_efecto(comando: str) -> bool:
    return any(marca in comando for marca in EFECTOS)


def ejecuta_crudo(comando: str, argumento: str = "") -> tuple:
    """Corre el comando en el host y devuelve (salida, codigo)."""
    c = comando.replace("{argumento}", shlex.quote(argumento))
    c = c.replace("{argumento_url}", shlex.quote(argumento))
    try:
        r = subprocess.run(spawn() + ["bash", "-lc", c],
                           capture_output=True, text=True, timeout=15)
        return (r.stdout or r.stderr or "").strip(), r.returncode
    except subprocess.TimeoutExpired:
        return "TIMEOUT", 124
    except OSError as e:
        return f"ERROR: {e}", 1


def clasifica(motor, accion, salida, codigo):
    """A partir de la salida cruda, decide como quedaria la respuesta hablada."""
    plantilla = accion.respuesta
    dicho = motor.formatea(plantilla, salida, "")

    # ¿la plantilla cayo al volcado crudo? formatea() devuelve {salida} cuando
    # el .format() peta por un campo que no existe. Se detecta comparando: si
    # la respuesta ES la salida y la plantilla NO era {salida}, fallo.
    campos = re.findall(r"\{(campo\d+|ultima|primera)\}", plantilla)
    if campos and dicho.strip() == salida[:600].strip() and plantilla.strip() not in ("{salida}", "{salida}."):
        return "PLANTILLA", dicho
    if not salida:
        return "VACIA", dicho
    if codigo != 0 and ("command not found" in salida or "No such file" in salida
                        or salida.startswith("ERROR") or salida == "TIMEOUT"):
        return "ROTA", dicho
    return "OK", dicho


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--romper", action="store_true", help="solo las que fallan")
    ap.add_argument("--json", action="store_true", help="salida en json")
    args = ap.parse_args()

    motor = carga_motor()
    catalogo = CATALOGO if os.path.exists(CATALOGO) else CATALOGO_FUENTE
    cat = motor.Catalogo([catalogo])

    resultados = []
    for accion in cat.acciones:
        estado = None
        if accion.confirmar:
            estado = "EFECTO" if not accion.comando else "DESTRUCTIVA"
            dicho = accion.acuse
        elif tiene_efecto(accion.comando):
            estado = "EFECTO"
            dicho = "(no se ejecuta; " + ("comando ok" if accion.comando else "sin comando") + ")"
        else:
            arg = ARGUMENTOS.get(accion.id, "") if accion.captura else ""
            salida, codigo = ejecuta_crudo(accion.comando, arg)
            estado, dicho = clasifica(motor, accion, salida, codigo)

        resultados.append({
            "id": accion.id, "grupo": accion.grupo, "estado": estado,
            "dice": dicho[:120],
        })

    cuenta = {}
    for r in resultados:
        cuenta[r["estado"]] = cuenta.get(r["estado"], 0) + 1

    if args.json:
        print(json.dumps({"cuenta": cuenta, "acciones": resultados},
                         ensure_ascii=False))
    else:
        malas = {"ROTA", "PLANTILLA", "VACIA"}
        for r in resultados:
            if args.romper and r["estado"] not in malas:
                continue
            marca = {"OK": "  ok  ", "VACIA": " vacia", "ROTA": " ROTA ",
                     "PLANTILLA": " PLANT", "EFECTO": " efect",
                     "DESTRUCTIVA": " destr"}.get(r["estado"], "  ?   ")
            print(f"[{marca}] {r['id']:<22} {r['dice']}")
        print("\n" + "  ".join(f"{k}: {v}" for k, v in sorted(cuenta.items())))
        buenas = cuenta.get("OK", 0) + cuenta.get("EFECTO", 0) + cuenta.get("DESTRUCTIVA", 0)
        print(f"utiles: {buenas}/{len(resultados)}  ·  "
              f"a revisar: {sum(cuenta.get(k, 0) for k in ('ROTA','PLANTILLA','VACIA'))}")

    os.makedirs(os.path.dirname(INFORME), exist_ok=True)
    with open(INFORME, "w") as f:
        json.dump({"cuenta": cuenta, "acciones": resultados}, f,
                  indent=2, ensure_ascii=False)

    fallan = sum(cuenta.get(k, 0) for k in ("ROTA", "PLANTILLA"))
    return 1 if fallan else 0


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