"""sync_chromium_profiles_to_rofi — sincroniza perfiles de Chromium con el lanzador (rofi / menus .desktop).

Lee los perfiles de Chromium desde el `Local State` de un user-data-dir y, por
cada perfil de usuario valido, genera:

1. un avatar PNG con las iniciales del perfil (icono distinto por perfil), o
   reutiliza la foto Gaia del perfil si esta disponible,
2. un archivo `.desktop` que lanza Chromium en ese perfil con ese icono.

Ademas limpia los `.desktop` de perfiles que ya no existen (identificados por el
marcador `X-RofiChromiumProfile=true`) y refresca la base de datos de aplicaciones
con `update-desktop-database` (best-effort).

El `Exec` NO pasa `--user-data-dir`: el wrapper del sistema (`/etc/chromium.d/cdp`)
inyecta `--user-data-dir=$HOME/.config/chromium-cdp` en cada invocacion de chromium.
Por eso basta `chromium --profile-directory="<dir>"`.
"""

from __future__ import annotations

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

# Resolver el paquete `infra` del registry tanto al ejecutar via `fn run`
# (PYTHONPATH=python/functions) como al ejecutar el archivo directo como script.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from infra.generate_initials_avatar import generate_initials_avatar  # noqa: E402

# Marcador que identifica los .desktop generados por ESTA funcion. Gobierna la
# limpieza de obsoletos sin tocar .desktop ajenos.
MARKER = "X-RofiChromiumProfile=true"


def _slugify(key: str) -> str:
    """Convierte la clave del dir del perfil en un slug seguro para nombres de archivo.

    "Profile 1" -> "profile-1", "Default" -> "default", "osint_01" -> "osint-01".
    """
    s = key.strip().lower()
    s = re.sub(r"[\s_]+", "-", s)
    s = re.sub(r"[^a-z0-9-]", "", s)
    s = re.sub(r"-+", "-", s).strip("-")
    return s


def _desktop_body(name: str, dir_key: str, icon_path: str, chromium_cmd: str) -> str:
    """Construye el cuerpo del archivo .desktop para un perfil.

    El em dash en `Name` es intencional. `dir_key` se encierra entre comillas
    dobles en el Exec porque puede contener espacios ("Profile 1").
    """
    return (
        "[Desktop Entry]\n"
        "Version=1.0\n"
        "Type=Application\n"
        f"Name=Chromium — {name}\n"
        "GenericName=Web Browser\n"
        f"Comment=Chromium en el perfil {name}\n"
        f'Exec={chromium_cmd} --profile-directory="{dir_key}" %U\n'
        f"Icon={icon_path}\n"
        "Terminal=false\n"
        "Categories=Network;WebBrowser;\n"
        "StartupWMClass=chromium\n"
        f"{MARKER}\n"
    )


def sync_chromium_profiles_to_rofi(
    local_state_path: str = "",
    apps_dir: str = "",
    icons_dir: str = "",
    chromium_cmd: str = "chromium",
) -> dict:
    """Sincroniza los perfiles de Chromium con entradas .desktop + iconos para rofi.

    Lee `profile.info_cache` del `Local State` y, por cada perfil de usuario valido
    (excluye "System Profile" y perfiles con nombre vacio), genera un avatar PNG y
    un archivo `.desktop` que lanza Chromium en ese perfil. Limpia los `.desktop`
    obsoletos generados previamente por esta funcion y refresca la base de datos de
    aplicaciones. Es idempotente: ejecutarla N veces deja el mismo estado.

    Args:
        local_state_path: Ruta al archivo `Local State` de Chromium. Si viene vacio
            usa `~/.config/chromium-cdp/Local State`.
        apps_dir: Directorio de salida de los `.desktop`. Si viene vacio usa
            `~/.local/share/applications`.
        icons_dir: Directorio de salida de los iconos PNG. Si viene vacio usa
            `~/.local/share/icons/chromium-profiles`.
        chromium_cmd: Comando base para lanzar Chromium en el Exec del `.desktop`.
            Default "chromium" (el wrapper del sistema inyecta `--user-data-dir`).

    Returns:
        dict con:
            user_data_dir: ruta absoluta del user-data-dir derivado del Local State.
            created: lista de paths de los .desktop escritos/actualizados este run.
            removed: lista de paths de los .desktop obsoletos borrados.
            profiles: lista de {dir, name, desktop, icon} por perfil procesado.
            warnings: lista de mensajes de perfiles saltados por datos raros.
            update_desktop_database: dict {attempted, ok, detail} con el resultado
                best-effort de `update-desktop-database`.

    Raises:
        FileNotFoundError: si el `Local State` no existe.
        ValueError: si el JSON no tiene `profile.info_cache`.
    """
    if not local_state_path:
        local_state_path = os.path.expanduser("~/.config/chromium-cdp/Local State")
    if not apps_dir:
        apps_dir = os.path.expanduser("~/.local/share/applications")
    if not icons_dir:
        icons_dir = os.path.expanduser("~/.local/share/icons/chromium-profiles")

    ls_path = Path(local_state_path)
    if not ls_path.exists():
        raise FileNotFoundError(
            f"Local State no encontrado en {ls_path}. "
            "Verifica la ruta del user-data-dir de Chromium."
        )

    user_data_dir = ls_path.parent

    try:
        state = json.loads(ls_path.read_text(encoding="utf-8"))
    except Exception as exc:
        raise ValueError(f"No se pudo parsear el JSON de {ls_path}: {exc}") from exc

    info_cache = state.get("profile", {}).get("info_cache")
    if not isinstance(info_cache, dict):
        raise ValueError(
            f"{ls_path} no contiene profile.info_cache (dict). "
            "El archivo no parece un Local State de Chromium valido."
        )

    apps_path = Path(apps_dir)
    icons_path = Path(icons_dir)
    icons_path.mkdir(parents=True, exist_ok=True)
    apps_path.mkdir(parents=True, exist_ok=True)

    created: list[str] = []
    profiles: list[dict] = []
    warnings: list[str] = []
    # slugs vigentes -> sirve para la limpieza de obsoletos
    valid_slugs: set[str] = set()

    for dir_key, meta in info_cache.items():
        try:
            if dir_key == "System Profile":
                continue
            if not isinstance(meta, dict):
                warnings.append(f"perfil {dir_key!r}: metadata no es dict, saltado")
                continue

            name = (meta.get("name") or "").strip()
            if not name:
                # name vacio -> no es un perfil de usuario util
                continue

            slug = _slugify(dir_key)
            if not slug:
                warnings.append(f"perfil {dir_key!r}: slug vacio tras sanitizar, saltado")
                continue
            valid_slugs.add(slug)

            icon_path = icons_path / f"{slug}.png"

            # Preferir la foto Gaia del perfil si existe; si no, avatar de iniciales.
            gaia = (meta.get("gaia_picture_file_name") or "").strip()
            gaia_src = user_data_dir / dir_key / gaia if gaia else None
            if gaia_src is not None and gaia_src.is_file():
                try:
                    shutil.copyfile(str(gaia_src), str(icon_path))
                except Exception as exc:
                    warnings.append(
                        f"perfil {dir_key!r}: no se pudo copiar foto Gaia ({exc}), "
                        "usando avatar de iniciales"
                    )
                    generate_initials_avatar(name, str(icon_path))
            else:
                generate_initials_avatar(name, str(icon_path))

            desktop_path = apps_path / f"chromium-{slug}.desktop"
            desktop_path.write_text(
                _desktop_body(name, dir_key, str(icon_path), chromium_cmd),
                encoding="utf-8",
            )

            created.append(str(desktop_path))
            profiles.append(
                {
                    "dir": dir_key,
                    "name": name,
                    "desktop": str(desktop_path),
                    "icon": str(icon_path),
                }
            )
        except Exception as exc:
            warnings.append(f"perfil {dir_key!r}: error inesperado, saltado ({exc})")
            continue

    # --- Limpieza de obsoletos: .desktop generados por esta funcion cuyo slug ya
    #     no corresponde a ningun perfil vigente. ---
    removed: list[str] = []
    for desktop_file in apps_path.glob("chromium-*.desktop"):
        try:
            content = desktop_file.read_text(encoding="utf-8")
        except Exception:
            continue
        if MARKER not in content:
            # No es nuestro -> no tocar.
            continue
        slug = desktop_file.stem[len("chromium-"):]
        if slug in valid_slugs:
            continue
        # Obsoleto: borrar .desktop + su icono asociado.
        try:
            desktop_file.unlink()
            removed.append(str(desktop_file))
        except Exception as exc:
            warnings.append(f"no se pudo borrar obsoleto {desktop_file}: {exc}")
            continue
        old_icon = icons_path / f"{slug}.png"
        if old_icon.exists():
            try:
                old_icon.unlink()
            except Exception as exc:
                warnings.append(f"no se pudo borrar icono obsoleto {old_icon}: {exc}")

    # --- Refrescar base de datos de aplicaciones (best-effort). ---
    udb = {"attempted": False, "ok": False, "detail": ""}
    udb_bin = shutil.which("update-desktop-database")
    if udb_bin:
        udb["attempted"] = True
        try:
            proc = subprocess.run(
                [udb_bin, str(apps_path)],
                capture_output=True,
                text=True,
                timeout=30,
            )
            udb["ok"] = proc.returncode == 0
            udb["detail"] = (proc.stderr or proc.stdout or "").strip()
        except Exception as exc:
            udb["detail"] = f"error ejecutando update-desktop-database: {exc}"
    else:
        udb["detail"] = "update-desktop-database no encontrado en PATH"

    return {
        "user_data_dir": str(user_data_dir),
        "created": created,
        "removed": removed,
        "profiles": profiles,
        "warnings": warnings,
        "update_desktop_database": udb,
    }


if __name__ == "__main__":
    result = sync_chromium_profiles_to_rofi()
    print(json.dumps(result, indent=2, default=str, ensure_ascii=False))
