"""Lo que JARVIS puede HACER, y el unico sitio por donde pasa.

`Manos` es la puerta: el catalogo entra por aqui, el cerebro entra por aqui y
el panel entrara por aqui. Tener un solo punto importa porque es donde se
decide que se ejecuta y que no, y esa decision no puede estar repartida.

Las cuatro piezas de debajo vienen tal cual de la version sobre Newelle
—`acciones.py`, `tareas.py`, `sudo_root.py`, `seleccion.py`— y se comprobo al
copiarlas que 374 frases dan exactamente el mismo resultado en los dos sitios.
"""
import os
import shlex
import subprocess

from .acciones import Catalogo, ejecuta, formatea

AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CATALOGO = os.path.join(AQUI, "datos", "acciones.json")
PROPIO = os.path.expanduser("~/.config/jarvis/acciones.json")

# Lo que no se ejecuta por mucho que lo pida el modelo. No es una lista de
# seguridad completa —eso es imposible— sino un tope contra el accidente: el
# modelo es servicial y a veces se inventa una limpieza que nadie pidio.
PROHIBIDO = (
    "rm -rf /", "mkfs", "dd if=", ":(){", "shutdown", "reboot", "halt",
    "> /dev/sd", "chmod -R 777 /", "chown -R", "userdel", "passwd",
)

TOPE_SALIDA = 4000


class Manos:
    def __init__(self, catalogo=None, al_ejecutar=None):
        self.catalogo = catalogo or Catalogo([PROPIO, CATALOGO])
        self.al_ejecutar = al_ejecutar
        # Root para la sesion: la primera vez que una accion necesita sudo se
        # pide la contraseña, y queda aqui hasta que caduque (15 min). No se
        # pide al arrancar: solo cuando de verdad hace falta.
        from manos.sudo_root import GestorRoot
        self.root = GestorRoot([])
        # El panel conecta aqui como pedir la contraseña de root cuando haga
        # falta (muestra el dialogo). Sin panel (modo terminal) se queda en None.
        self.al_pedir_root = None

    # ------------------------------------------------------------- catalogo

    def busca(self, texto: str):
        return self.catalogo.busca(texto)

    def haz(self, accion, argumento="") -> tuple:
        """Ejecuta una accion ya emparejada. Devuelve (lo que se dice, crudo)."""
        dicho, salida = ejecuta(accion, [], argumento)
        if salida:
            dicho = formatea(accion.respuesta, salida, argumento)
        return dicho, salida

    def por_orden(self, orden: str, al_ejecutar=None) -> str:
        """Para el cerebro: empareja contra el catalogo y ejecuta.

        Devuelve texto para el modelo, no para el usuario: si no encaja, se le
        dice claramente para que pruebe con el shell en vez de insistir.
        """
        if not orden.strip():
            return "no me has dicho ninguna orden"
        # El modelo a veces cuela un comando aqui de todas formas. Se detecta y
        # se le devuelve el motivo, que es mas util que un "no encontrado".
        if any(c in orden for c in "|><;&$") or orden.split()[0] in ("df", "ls", "ps", "cat"):
            return ("eso es un comando de shell, no una orden del catalogo. "
                    "Usa la herramienta comando_de_shell.")
        accion, argumento = self.catalogo.busca(orden)
        if accion is None:
            return (f"'{orden}' no esta en el catalogo. Si hace falta, usa "
                    "comando_de_shell.")
        if al_ejecutar or self.al_ejecutar:
            (al_ejecutar or self.al_ejecutar)(accion.id, argumento)
        dicho, salida = self.haz(accion, argumento)
        return (salida or dicho)[:TOPE_SALIDA]

    # ---------------------------------------------------------------- shell

    def por_shell(self, comando: str, al_ejecutar=None) -> str:
        if not comando.strip():
            return "no me has dicho ningun comando"
        bajo = comando.lower()
        for veneno in PROHIBIDO:
            if veneno in bajo:
                return (f"no ejecuto eso: contiene '{veneno}'. Si de verdad hace "
                        "falta, que lo pida el usuario expresamente.")
        if al_ejecutar or self.al_ejecutar:
            (al_ejecutar or self.al_ejecutar)("shell", comando)

        from manos.sudo_root import necesita_root
        # Los comandos con sudo van por GestorRoot, que da la contraseña de la
        # sesion por stdin. Sin ella, `bash -c "sudo ..."` se colgaria pidiendo
        # una contraseña que nadie teclea. Y si el candado no se ha abierto, no
        # hay contraseña: el root queda cerrado hasta que el usuario desbloquee.
        if necesita_root(comando):
            if not self.root.sesion_activa():
                # No hay root todavia: se pide la contraseña AHORA (una vez), no
                # al arrancar. El panel muestra el dialogo; el usuario la teclea
                # y repite la orden, que ya correra con la sesion abierta.
                if self.al_pedir_root:
                    self.al_pedir_root(comando)
                    return ("Esto necesita permiso de administrador, señor. "
                            "Introduzca su contraseña y repitamelo.")
                return ("eso necesita permiso de administrador y no hay panel "
                        "para pedir la contraseña.")
            try:
                r = self.root.ejecuta(comando, timeout=60)
            except Exception as e:
                return f"no se pudo ejecutar como root: {e}"
        else:
            try:
                r = subprocess.run(["bash", "-c", comando], capture_output=True,
                                   text=True, timeout=60)
            except subprocess.TimeoutExpired:
                return "el comando tardo mas de un minuto y lo he cortado"
            except OSError as e:
                return f"no se pudo ejecutar: {e}"

        salida = (r.stdout or r.stderr or "").strip()
        if not salida:
            return "hecho, sin salida" if r.returncode == 0 else f"fallo (codigo {r.returncode})"
        return salida[:TOPE_SALIDA]
