#!/usr/bin/env python3
"""¿Sigue funcionando la máquina? Se pregunta después de cada actualización.

Por qué existe: filmos se actualiza por `apt` desde un repositorio propio que se
publica varias veces al día. Una actualización que rompe algo no avisa —dpkg
termina contento y el fallo aparece tres horas después, en mitad de un montaje—,
y para entonces nadie recuerda qué se instaló.

Esto mira lo que tiene que estar en pie y, si algo no lo está, **dice con qué
orden exacta se deshace la última actualización**. Una comprobación que detecta
un problema y no dice qué hacer con él solo sirve para preocupar.

No hace falta root: todo lo que mira se lee sin permisos.
"""

from __future__ import annotations

import re
import shutil
import subprocess
import sys
from pathlib import Path

INSTANTANEAS = Path("/.snapshots")

#: Lo que tiene que estar vivo. Si una unidad no está instalada no se echa en
#: falta: una estación sin escritorio no tiene botón, y no por eso está rota.
DEL_SISTEMA = {
    "filmos-hermes.service": "HermesOS",
}
DE_LA_SESION = {
    "filmos-vault.service": "el vault",
    "filmos-hermesd.service": "el puente del agente",
}

VERDE = "\033[32m"
ROJO = "\033[31m"
AMARILLO = "\033[33m"
APAGADO = "\033[2m"
FIN = "\033[0m"


def color(texto: str, tono: str) -> str:
    return f"{tono}{texto}{FIN}" if sys.stdout.isatty() else texto


def correr(*orden: str) -> tuple[int, str]:
    try:
        hecho = subprocess.run(  # noqa: S603 - órdenes fijas, sin shell
            list(orden), capture_output=True, text=True, timeout=30, check=False
        )
    except (OSError, subprocess.SubprocessError):
        return 127, ""
    return hecho.returncode, hecho.stdout


class Informe:
    def __init__(self) -> None:
        self.problemas: list[str] = []
        self.lineas: list[str] = []

    def bien(self, texto: str) -> None:
        self.lineas.append(f"  {color('·', APAGADO)} {texto}")

    def mal(self, texto: str, detalle: str = "") -> None:
        self.problemas.append(texto)
        self.lineas.append(f"  {color('✗', ROJO)} {texto}")
        for fila in detalle.strip().splitlines()[:6]:
            self.lineas.append(f"      {color(fila.strip(), APAGADO)}")

    def aviso(self, texto: str) -> None:
        self.lineas.append(f"  {color('!', AMARILLO)} {texto}")


def mirar_dpkg(informe: Informe) -> None:
    """Paquetes a medias. Pasó de verdad: doce desempaquetados sin configurar."""

    codigo, salida = correr("dpkg", "--audit")
    if codigo == 127:
        return
    if salida.strip():
        informe.mal("hay paquetes a medio instalar (`sudo dpkg --configure -a`)", salida)
    else:
        informe.bien("ningún paquete a medias")


def mirar_unidades(informe: Informe) -> None:
    _, salida = correr("systemctl", "--failed", "--no-legend", "--plain")
    rotas = [l for l in salida.splitlines() if l.strip()]
    if rotas:
        informe.mal(f"{len(rotas)} unidad(es) del sistema en fallo", salida)
    else:
        informe.bien("ninguna unidad del sistema en fallo")

    codigo, salida = correr("systemctl", "--user", "--failed", "--no-legend", "--plain")
    if codigo not in (0, 1):
        return
    rotas = [l for l in salida.splitlines() if l.strip()]
    if rotas:
        informe.mal(f"{len(rotas)} unidad(es) de tu sesión en fallo", salida)
    else:
        informe.bien("ninguna unidad de tu sesión en fallo")


def mirar_servicios(informe: Informe) -> None:
    for unidad, quien in DEL_SISTEMA.items():
        _, salida = correr("systemctl", "is-active", unidad)
        estado = salida.strip()
        if estado == "active":
            informe.bien(f"{quien} en marcha")
        elif estado in ("inactive", "failed"):
            informe.mal(f"{quien} no está en marcha ({unidad}: {estado})")

    for unidad, quien in DE_LA_SESION.items():
        _, salida = correr("systemctl", "--user", "is-active", unidad)
        estado = salida.strip()
        if estado == "active":
            informe.bien(f"{quien} en marcha")
        elif estado in ("inactive", "failed"):
            informe.mal(f"{quien} no está en marcha ({unidad}: {estado})")


def mirar_arranque(informe: Informe) -> None:
    """Que la máquina pueda volver a arrancar. Es lo único irrecuperable."""

    boot = Path("/boot")
    if not boot.is_dir():
        return
    nucleos = sorted(boot.glob("vmlinuz-*"))
    if not nucleos:
        informe.mal("no hay ningún núcleo en /boot: esta máquina no volvería a arrancar")
        return
    sueltos = [
        n.name
        for n in nucleos
        if not (boot / n.name.replace("vmlinuz-", "initrd.img-", 1)).exists()
    ]
    if sueltos:
        informe.mal(
            f"{len(sueltos)} núcleo(s) sin su initrd (`sudo update-initramfs -u -k all`)",
            "\n".join(sueltos),
        )
    else:
        informe.bien(f"{len(nucleos)} núcleo(s) en /boot, cada uno con su initrd")


def mirar_espacio(informe: Informe) -> None:
    for punto in ("/", "/boot"):
        ruta = Path(punto)
        if not ruta.is_dir():
            continue
        uso = shutil.disk_usage(punto)
        libre = uso.free / uso.total * 100 if uso.total else 100
        gigas = uso.free / 1024**3
        if libre < 3:
            informe.mal(f"{punto} casi lleno: quedan {gigas:.1f} GB ({libre:.0f} %)")
        elif libre < 10:
            informe.aviso(f"{punto} apretado: quedan {gigas:.1f} GB ({libre:.0f} %)")
        else:
            informe.bien(f"{punto} con {gigas:.1f} GB libres")


def ultima_de_apt(donde: Path = INSTANTANEAS) -> tuple[str, str] | None:
    """La instantánea de antes de la última actualización, si la hay."""

    if not donde.is_dir():
        return None
    mejor: tuple[int, str] | None = None
    try:
        carpetas = list(donde.iterdir())
    except OSError:
        # `/.snapshots` es de root. Esto corre como la persona que usa la
        # máquina, así que lo normal es NO poder leerlo: se dice la orden sin el
        # número en vez de reventar justo cuando algo ya está roto.
        return None
    for carpeta in carpetas:
        if not carpeta.name.isdigit():
            continue
        info = carpeta / "info.xml"
        if not info.is_file():
            continue
        try:
            texto = info.read_text(encoding="utf-8", errors="replace")
        except OSError:
            continue
        if "<type>pre</type>" not in texto:
            continue
        numero = int(carpeta.name)
        if mejor is None or numero > mejor[0]:
            fecha = re.search(r"<date>(.*?)</date>", texto)
            mejor = (numero, fecha.group(1) if fecha else "")
    return (str(mejor[0]), mejor[1]) if mejor else None


def main(argumentos: list[str]) -> int:
    # En modo callado solo se habla cuando hay algo que decir: un mensaje
    # después de cada `apt` que siempre dice «todo bien» se deja de leer, y
    # entonces tampoco se lee el día que dice otra cosa.
    callado = "--callado" in argumentos

    informe = Informe()
    mirar_dpkg(informe)
    mirar_unidades(informe)
    mirar_servicios(informe)
    mirar_arranque(informe)
    mirar_espacio(informe)

    if not informe.problemas:
        if not callado:
            for linea in informe.lineas:
                print(linea)
            print()
            print(color("Todo en pie.", VERDE))
        return 0

    print()
    for linea in informe.lineas:
        print(linea)
    print()
    print(color(f"{len(informe.problemas)} cosa(s) rotas.", ROJO))

    print()
    print("Si esto lo ha roto la última actualización, se deshace con:")
    anterior = ultima_de_apt()
    if anterior:
        numero, fecha = anterior
        cuando = f"   {color(f'(de antes de {fecha})', APAGADO)}" if fecha else ""
        print(color(f"    sudo filmos-volver-atras {numero}", AMARILLO) + cuando)
    else:
        # Sin poder leer las instantáneas -lo normal sin root- se manda a la
        # orden, que las lista. Peor sería callarse.
        print(color("    sudo filmos-volver-atras", AMARILLO) + "   (lista a dónde volver)")
    print("y reiniciar. La raíz de ahora se guarda, no se pierde.")
    return 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
