"""Ejecutar ordenes como root de forma controlada.

El problema que resuelve. Newelle convierte `sudo` en `sudo -S` (leer la
contraseña de la entrada estandar), pero en el panel de voz no hay una entrada
donde teclearla, asi que se queda colgado esperando una contraseña que no
llega. Y las acciones marcadas para confirmar no tenian dialogo: solo decian
"confirme antes" y no ejecutaban nada.

Como se resuelve, y las decisiones de seguridad detras:

- La contraseña NUNCA se escribe en disco. Vive en memoria de este proceso solo
  si el usuario activa el permiso de sesion, y se pasa a `sudo -S` por stdin.
- El permiso de sesion CADUCA: a los 15 minutos de la ultima orden se olvida
  sola, y tambien al cerrar el panel. No es un "root para siempre".
- CADA orden con root muestra su comando exacto en un dialogo antes de correr.
  El permiso de sesion solo ahorra re-teclear la contraseña, nunca salta el ver
  y aprobar lo que se va a ejecutar. La voz no puede autoaprobar root.

Alternativa descartada: apoyarse solo en el timestamp de sudo (`sudo -v`). Es
mas limpio en teoria —lo gestiona el sistema— pero a traves de flatpak-spawn el
tty cambia entre llamadas y el cacheo no es fiable. Mantener la contraseña en
memoria del proceso, con caducidad y sin tocar disco, funciona siempre y el
usuario opta a ello de forma explicita.
"""
import re
import subprocess
import time

# A los 15 minutos sin usarla, la contraseña en memoria se olvida. Igual que el
# valor por defecto de sudo, para no sorprender.
CADUCIDAD = 15 * 60

# Detecta una invocacion real de sudo: al principio del comando o justo tras un
# separador (;, |, &, &&, ||, salto de linea), no la palabra "sudo" dicha como
# argumento ("echo sudo no cuenta"). Errar aqui solo pediria una contraseña de
# mas, no es peligroso, pero molesta.
_SUDO = re.compile(r"(?:^|[;&|\n]|&&|\|\|)\s*sudo(?:\s|$)")


def necesita_root(comando: str) -> bool:
    """True si el comando lanza algo con sudo."""
    return bool(comando and _SUDO.search(comando))


class GestorRoot:
    """Guarda —o no— la contraseña de sesion, y ejecuta con ella.

    Una instancia por panel. Al cerrar el panel se llama a olvida().
    """

    def __init__(self, prefijo_host=None):
        # prefijo para salir del sandbox; se pasa como argumento para poder
        # probar esto sin flatpak
        self._prefijo = list(prefijo_host or [])
        self._password = None
        self._ultimo_uso = 0.0

    # -------------------------------------------------------------- sesion

    def sesion_activa(self) -> bool:
        """Hay contraseña recordada y aun no ha caducado."""
        if self._password is None:
            return False
        if time.monotonic() - self._ultimo_uso > CADUCIDAD:
            self.olvida()
            return False
        return True

    def olvida(self):
        """Borra la contraseña de memoria. Se llama al cerrar el panel y al
        caducar. Se sobreescribe antes de soltarla, por si acaso."""
        if self._password is not None:
            self._password = "\x00" * len(self._password)
        self._password = None
        self._ultimo_uso = 0.0

    def recuerda(self, password: str):
        self._password = password
        self._ultimo_uso = time.monotonic()

    # ------------------------------------------------------------ ejecutar

    def valida(self, password: str) -> bool:
        """Comprueba la contraseña con `sudo -S -v`, sin ejecutar nada mas.

        -k borra cualquier timestamp previo para que la comprobacion sea de
        verdad; -v solo actualiza credenciales. Si la contraseña es correcta
        devuelve True; nunca lanza ningun comando del usuario.
        """
        try:
            r = subprocess.run(
                self._prefijo + ["sudo", "-S", "-k", "-p", "", "-v"],
                input=password + "\n", capture_output=True, text=True,
                timeout=10)
            return r.returncode == 0
        except (OSError, subprocess.SubprocessError):
            return False

    def ejecuta(self, comando: str, password: str = None, timeout: int = 30):
        """Ejecuta el comando dando la contraseña a los sudo que lleve.

        password: si se pasa, se usa esa vez; si no, la de la sesion. La
        contraseña se pasa por stdin a `sudo -S`. No se registra en ningun
        sitio.

        Devuelve (salida, codigo). codigo -1 = no habia contraseña disponible.
        """
        clave = password if password is not None else self._password
        if clave is None:
            return "No hay contraseña disponible para root.", -1

        # que todos los sudo del comando lean de stdin sin imprimir el prompt
        preparado = re.sub(r"(^|[;&|]|\s)sudo(\s)",
                           r"\1sudo -S -p '' \2", comando)
        try:
            r = subprocess.run(
                self._prefijo + ["bash", "-lc", preparado],
                input=clave + "\n", capture_output=True, text=True,
                timeout=timeout)
            salida = (r.stdout or r.stderr or "").strip()
            # sudo escribe su prompt/errores en stderr aunque -p sea vacio;
            # se limpia lo que es ruido de sudo, no del comando
            salida = "\n".join(
                l for l in salida.splitlines()
                if l.strip() and not l.startswith(("[sudo]", "Sorry, try again")))
            self._ultimo_uso = time.monotonic()
            return salida, r.returncode
        except subprocess.TimeoutExpired:
            return "La orden con root ha tardado demasiado.", 124
        except OSError as e:
            return f"No se pudo ejecutar: {e}", 1
        finally:
            # no dejar la copia local viva mas de lo necesario
            clave = "\x00" * len(clave)
