#!/usr/bin/env python3
"""Ejecuta TODAS las acciones del catalogo y dice cuales funcionan de verdad.

Por que hace falta: hasta hoy los comandos libres del modelo corrian dentro del
sandbox, donde no hay ni el sistema ni los discos. Se arreglo (virtualization a
false), pero las 134 acciones del catalogo nunca se habian ejecutado todas
seguidas contra la maquina real: la suite prueba que las FRASES caen en la
accion correcta, no que el comando de cada accion haga algo util.

Que se ejecuta y que no:

  - Las destructivas (`confirmar: true`) y las que piden root NO se ejecutan.
    En uso normal pasan por un dialogo que aprueba una persona; aqui no hay
    persona, y aprobarlas solas seria justo lo que el diseño evita.
  - Las que abren ventanas (firefox, xdg-open) se ejecutan solo con --abrir,
    porque llenan la pantalla. Sin esa opcion se comprueba que el programa
    existe, que es lo que puede fallar.
  - El resto se ejecuta entero y se mira que conteste algo con sentido.

    python repasar_acciones.py [--abrir] [--grupo sistema] [--json salida.json]
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOGO = os.path.join(RAIZ, "newelle", "data", "acciones", "acciones.json")

# Argumentos de mentira para las acciones que capturan uno. Se eligen inocuos:
# nada que borre, mueva o mande nada fuera.
ARGUMENTOS = {
    "buscar_fichero": "README.md",
    "buscar_carpeta": "config",
    "grep_recursivo": "JARVIS",
    "leer_fichero": "README.md",   # un NOMBRE, que es lo que se dice por voz
    "abrir_carpeta": RAIZ,
    "tamano_carpeta": os.path.join(RAIZ, "config"),
    "buscar_youtube": "musica",
    "buscar_wikipedia": "madrid",
    "buscar_github": "newelle",
    "traducir": "hola",
    "buscar_web": "que hora es",
    "matar_proceso": "proceso_que_no_existe_jarvis",
    "ping": "127.0.0.1",
}
POR_DEFECTO = "prueba"

# Lo que abre o MUEVE ventanas. ventanas.sh reordena el escritorio del usuario:
# ejecutarlo en un repaso automatico le cambia las ventanas de sitio sin avisar.
VENTANA = re.compile(r"\b(firefox|xdg-open|gnome-terminal|nautilus|vlc|mpv|eog|"
                     r"gedit|gnome-text-editor|totem|ventanas\.sh|xdotool|wmctrl)\b")


def carga():
    with open(CATALOGO, encoding="utf-8") as f:
        d = json.load(f)
    return d.get("acciones", d if isinstance(d, list) else [])


def prefijo_host():
    """Las acciones llevan host:true y en uso salen del sandbox. Aqui ya
    estamos fuera, asi que no hace falta prefijo."""
    return []


def ejecuta(accion, argumento, tope=25):
    import shlex
    import urllib.parse

    comando = accion.get("comando") or ""
    comando = comando.replace("{argumento}", shlex.quote(argumento))
    comando = comando.replace("{argumento_url}",
                              shlex.quote(urllib.parse.quote_plus(argumento)))
    t0 = time.monotonic()
    try:
        r = subprocess.run(["bash", "-lc", comando], capture_output=True,
                           text=True, errors="replace", timeout=tope)
        salida = (r.stdout or r.stderr or "").strip()
        return r.returncode, salida, time.monotonic() - t0
    except subprocess.TimeoutExpired:
        return -1, f"(no termino en {tope}s)", time.monotonic() - t0
    except OSError as e:
        return -2, f"(no se pudo ejecutar: {e})", time.monotonic() - t0


def programa_existe(comando):
    """El primer ejecutable de la orden, para las que abren o mueven ventanas.

    Algunos son ordenes del PATH (firefox) y otros son scripts nuestros con
    ruta completa (config/ventanas.sh): `command -v` solo sirve para los
    primeros, y con los segundos hay que mirar el fichero.
    """
    m = VENTANA.search(comando)
    if not m:
        return None
    nombre = m.group(1)
    if nombre.endswith(".sh"):
        # sacar la ruta entera tal como aparece en el comando
        for trozo in comando.split():
            if trozo.endswith(nombre):
                ruta = os.path.expandvars(os.path.expanduser(trozo))
                return os.access(ruta, os.X_OK)
        return False
    return subprocess.run(["bash", "-lc", f"command -v {nombre}"],
                          capture_output=True).returncode == 0


def main():
    p = argparse.ArgumentParser()
    p.add_argument("--abrir", action="store_true",
                   help="ejecutar tambien las que abren ventanas")
    p.add_argument("--grupo")
    p.add_argument("--json")
    p.add_argument("--tope", type=int, default=25)
    args = p.parse_args()

    acciones = carga()
    if args.grupo:
        acciones = [a for a in acciones if a.get("grupo") == args.grupo]

    resultados = []
    print(f"  repasando {len(acciones)} acciones"
          f"{' del grupo ' + args.grupo if args.grupo else ''}\n", flush=True)

    for a in acciones:
        ident = a.get("id", "?")
        comando = a.get("comando") or ""
        arg = ARGUMENTOS.get(ident, POR_DEFECTO if a.get("captura") else "")

        if not comando:
            estado, detalle = "solo habla", a.get("acuse", "")[:50]
        elif a.get("confirmar") or "sudo" in comando:
            # no se aprueban solas: en uso las aprueba una persona
            estado, detalle = "saltada", "destructiva o con root"
        elif VENTANA.search(comando) and not args.abrir:
            hay = programa_existe(comando)
            estado = "programa ok" if hay else "FALTA PROGRAMA"
            detalle = VENTANA.search(comando).group(1)
        else:
            codigo, salida, tardo = ejecuta(a, arg, args.tope)
            una_linea = " ".join(salida.split())[:70]
            if codigo == 0:
                estado = "ok" if salida else "ok (sin salida)"
            else:
                estado = f"FALLO ({codigo})"
            detalle = f"{tardo:4.1f}s  {una_linea}"

        resultados.append({"id": ident, "grupo": a.get("grupo"),
                           "estado": estado, "detalle": detalle})
        marca = "!" if "FALLO" in estado or "FALTA" in estado else " "
        print(f" {marca} {ident:26s} {estado:16s} {detalle}", flush=True)

    print()
    from collections import Counter
    c = Counter(r["estado"].split(" (")[0].split(" ")[0] for r in resultados)
    for k, v in c.most_common():
        print(f"    {v:3d}  {k}")
    malas = [r for r in resultados if "FALLO" in r["estado"] or "FALTA" in r["estado"]]
    print(f"\n  {len(resultados) - len(malas)} de {len(resultados)} responden bien")
    if malas:
        print("  fallan: " + ", ".join(r["id"] for r in malas))

    if args.json:
        with open(args.json, "w", encoding="utf-8") as f:
            json.dump(resultados, f, ensure_ascii=False, indent=2)
    return 1 if malas else 0


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