#!/usr/bin/env bash
# qemu-desktop-image.sh — imagen de disco EFI COSMIC para QEMU (virtio-gpu + GL por software), que
# auto-lanza cosmic-session. Hermano de scripts/gnome/qemu-desktop-image.sh y scripts/kde/…: comparte
# la base metal, la inyección de mesa-llvmpipe/LLVM/musl y el truco del console-getty.
#
#   work/metal-rootfs      base: arje-zero PID1 + busybox + kernel(EFI-stub) + firmware
#   work/cosmic-rootfs     cierre de COSMIC (lo arma scripts/cosmic/hydrate-cosmic.sh)
#   + store/*-mesa-llvmpipe   GBM/EGL de software (el cierre trae mesa con iris_dri, que es Intel real)
#   + store/*-llvm18          libLLVM que llvmpipe JIT-linkea
#   → work/cosmic-qemu-rootfs (hardlinks) → work/hammer-cosmic-qemu.img
#
# Uso:  scripts/cosmic/qemu-desktop-image.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"

BASE="${BASE:-work/metal-rootfs}"
COSMIC="${COSMIC:-work/cosmic-rootfs}"
MESA="${MESA:-$(ls -dt store/*-mesa-llvmpipe 2>/dev/null | head -1)}"
LLVM="${LLVM:-$(ls -dt store/*-llvm18 2>/dev/null | head -1)}"
MERGED="${MERGED:-work/cosmic-qemu-rootfs}"
IMG="${IMG:-work/hammer-cosmic-qemu.img}"

[ -d "$BASE" ]   || { echo "falta $BASE" >&2; exit 1; }
[ -d "$COSMIC" ] || { echo "falta $COSMIC — corré scripts/cosmic/hydrate-cosmic.sh" >&2; exit 1; }
[ -n "$MESA" ] && [ -d "$MESA" ] || { echo "falta mesa-llvmpipe en store/" >&2; exit 1; }

echo "==> fundiendo rootfs por hardlinks (base metal + cierre COSMIC)"
rm -rf "$MERGED"
cp -al "$BASE" "$MERGED"
cp -al --remove-destination "$COSMIC"/. "$MERGED"/

. "$ROOT/scripts/lib/pid1-desde-store.sh"
inyectar_pid1 "$MERGED" "$BASE"

echo "==> parcheando mesa software desde $(basename "$MESA")"
# El cierre trae mesa con SÓLO iris_dri.so (Intel real). En QEMU no hay GPU: hace falta kms_swrast.
mkdir -p "$MERGED"/usr/lib/dri
cp -a --remove-destination "$MESA"/usr/lib/dri/. "$MERGED"/usr/lib/dri/ 2>/dev/null || true
for so in libglapi libgbm libEGL libGLESv2 libGL libgallium; do
  cp -a --remove-destination "$MESA"/usr/lib/$so* "$MERGED"/usr/lib/ 2>/dev/null || true
done
ls "$MERGED"/usr/lib/dri/*swrast* >/dev/null 2>&1 && echo "   ✓ kms_swrast instalado" || { echo "   ✗ swrast no entró" >&2; exit 1; }

echo "==> inyectando libLLVM.18 (llvmpipe lo JIT-linkea)"
[ -n "$LLVM" ] && [ -d "$LLVM" ] || { echo "   ✗ falta llvm18 en store/ (set LLVM=)" >&2; exit 1; }
cp -a --remove-destination "$LLVM"/usr/lib/libLLVM.so.18.1 "$MERGED"/usr/lib/
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so.18
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so

echo "==> inyectando el runtime C++ de Alpine (deuda CONOCIDA, no accidente)"
# libLLVM y mesa-llvmpipe se construyen con gcc ⇒ NEEDean libstdc++/libgcc_s. Nada de COSMIC los pide
# —el compositor cierra con ocho NEEDED y ninguno es de gcc—: esto es deuda de la cadena de mesa.
for lib in libgcc_s.so.1 libstdc++.so.6; do
  src=".dev-fs/alpine/usr/lib/$lib"
  if [ -r "$src" ]; then cp -a --remove-destination "$src"* "$MERGED"/usr/lib/ 2>/dev/null || true
  else echo "   ⚠ sin $lib ($src)"; fi
done

echo "==> inyectando musl — y los DOS sonames"
# La base metal es 100% estática (arje-zero+busybox). En musl el loader ES libc (mismo fichero). Van
# dos nombres porque conviven dos toolchains: zig-cc emite NEEDED `libc.so` y el gcc/clang de Alpine
# emite `libc.musl-x86_64.so.1`. Es la MISMA libc; sin el segundo symlink lo de gcc no carga.
MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}"
[ -r "$MUSL" ] || { echo "no encuentro musl libc ($MUSL) — set MUSL=" >&2; exit 1; }
mkdir -p "$MERGED/usr/lib/musl/lib" "$MERGED/lib"
cp -a --remove-destination "$MUSL" "$MERGED/usr/lib/musl/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/lib/ld-musl-x86_64.so.1"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.musl-x86_64.so.1"
echo "   ✓ libc.so + libc.musl-x86_64.so.1"

echo "==> inyectando arje-logind-compat (el login1 del fractal) + su política"
# cosmic-comp pregunta por su sesión igual que mutter y kwin. El daemon no entra por el cierre porque
# nadie lo declara como dep de build. Se elige por `hammer hash` —la receta de HOY—, no por el más
# reciente del store. Su política D-Bus viaja DENTRO del artefacto desde 2026-07-30.
if ALC="${ALC:-store/$(./target/release/hammer --store store hash recipes/arje-logind-compat.toml 2>/dev/null | tail -1 | sed 's/^b3://')-arje-logind-compat}"; [ -d "$ALC" ]; then
  install -Dm755 "$ALC"/usr/bin/arje-logind-compat "$MERGED"/usr/bin/arje-logind-compat
  install -d "$MERGED/usr/share/dbus-1/system.d"
  cp -a --remove-destination "$ALC"/usr/share/dbus-1/system.d/. "$MERGED"/usr/share/dbus-1/system.d/ 2>/dev/null || true
  echo "   ✓ $(basename "$ALC" | cut -c1-12)"
else
  echo "   ✗ falta arje-logind-compat en store/ — cosmic-comp no hallará sesión" >&2; exit 1
fi
# Comprobación, que es lo que reemplaza a escribir la política acá: mover algo a un artefacto sólo es
# una mejora si se NOTA cuando falta.
[ -f "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf" ] \
  && echo "   ✓ org.freedesktop.login1.conf (del artefacto)" \
  || { echo "   ✗ falta org.freedesktop.login1.conf — re-sellá arje-logind-compat" >&2; exit 1; }

echo "==> grupo video (seatd lo exige por NOMBRE)"
# `seatd -g video` sale con «Could not find group by name 'video'» y no arranca. No es cosmético:
# sin seatd, libseat cae al backend `builtin`, que anda sólo porque acá todo corre como root — o sea
# que el escritorio funcionaría por un accidente de privilegios y no por el camino real. La base metal
# es tan mínima que no trae ni los grupos estándar.
if ! grep -q '^video:' "$MERGED/etc/group" 2>/dev/null; then
  tmp="$MERGED/etc/group.new"
  { cat "$MERGED/etc/group"; echo "video:x:44:"; } > "$tmp"
  chmod 644 "$tmp"; mv "$tmp" "$MERGED/etc/group"
  echo "   ✓ video:x:44"
fi

echo "==> usuario messagebus (el system bus hace setuid a él)"
for f in passwd:messagebus:x:81:81:messagebus:/dev/null:/sbin/nologin group:messagebus:x:81:; do
  file="${f%%:*}"; line="${f#*:}"
  if ! grep -q '^messagebus:' "$MERGED/etc/$file" 2>/dev/null; then
    tmp="$MERGED/etc/$file.new"
    { cat "$MERGED/etc/$file"; echo "$line"; } > "$tmp"
    chmod 644 "$tmp"; mv "$tmp" "$MERGED/etc/$file"
  fi
done
for h in usr/libexec/dbus-daemon-launch-helper usr/lib/dbus-1.0/dbus-daemon-launch-helper; do
  if [ -f "$MERGED/$h" ]; then
    chown 0:81 "$MERGED/$h" 2>/dev/null || true
    chmod 4750 "$MERGED/$h"
    echo "   ✓ setuid en /$h"
  fi
done

# COSMIC_MODE se hornea como fichero: el getty no hereda ambiente. `bare` (default) lanza el
# compositor solo y los clientes a mano; `session` usa cosmic-session, que EXIGE
# cosmic-settings-daemon o panickea. Ver el runbook.
printf 'COSMIC_MODE=%s\n' "${COSMIC_MODE:-bare}" > "$MERGED/etc/cosmic-mode"
echo "==> modo de arranque: ${COSMIC_MODE:-bare}"

echo "==> instalando /usr/bin/cosmic-start"
install -Dm755 scripts/cosmic/cosmic-start.sh "$MERGED/usr/bin/cosmic-start"

echo "==> auto-lanzar COSMIC: console-getty ejecuta cosmic-start (no /bin/sh)"
python3 - "$MERGED/ente/seed.card.json" <<'PY'
import json, sys, os
p = sys.argv[1]
c = json.load(open(p))
for g in c.get("genesis", []):
    if g.get("label") == "console-getty":
        argv = g["payload"]["Native"]["argv"]
        argv[:] = [("/usr/bin/cosmic-start" if a == "/bin/sh" else a) for a in argv]
        print("  console-getty argv →", argv)
tmp = p + ".new"
with open(tmp, "w") as f:
    json.dump(c, f, indent=2)
os.chmod(tmp, 0o644)
os.replace(tmp, p)
PY

# /var/run → /run: arje-logind-compat busca el bus de sistema en la ruta LEGACY y dbus-daemon escucha
# en /run. Esta base metal es tan mínima que ni siquiera tiene /var/run; sin el enlace el daemon se va
# a modo idle y nunca registra login1.
ln -sfn /run "$MERGED/var/run"

mkdir -p "$MERGED/root/.config" "$MERGED/root/.cache" "$MERGED/run/user/0" "$MERGED/tmp"
chmod 1777 "$MERGED/tmp"

# ── CATÁLOGO APPSTREAM, o por qué cosmic-store salía VACÍO ─────────────────────────────────────
# El backend `pkgar` de la tienda NO lee `/usr/share/metainfo/*.metainfo.xml` —los ficheros que cada
# receta instala, uno por aplicación—. Lee el **CATÁLOGO**: los ficheros de
# `{/usr/share,/var/lib,/var/cache}/{swcatalog,app-info}/{xml,yaml}/`, que en una distro normal
# genera `appstreamcli compose` agregando todos los metainfo en un solo documento
# `<components version=… origin=…>`. Sin ese fichero la tienda abre perfecta y no lista NADA
# —«No results for …»— porque su catálogo está vacío, no porque le falte nada del sistema.
#
# hammer no tiene appstreamcli (sería AppStream + glib, o sea la torre de C que esta campaña evita),
# pero el formato es XML plano y la agregación es exactamente eso: envolver los `<component>` de cada
# metainfo en una raíz. Doce líneas de python valen la librería entera.
echo "==> catálogo AppStream para cosmic-store (agrega los metainfo del rootfs)"
#
# ⚠ Y hay que SACAR el bloque `<provides>`. Los metainfo que genera `xdgen` —los de cosmic-edit,
# files, settings, store y term— lo escriben con los envoltorios `<mimetypes>`/`<binaries>`, y el
# parser de la tienda (el crate `appstream`) espera los hijos PLANOS del esquema de catálogo
# (`<mediatype>`, `<binary>`). Con el envoltorio muere con
#     failed to parse … in "/usr/share/swcatalog/xml/hammer.xml": The tag provide doesn't have a value
# y —esto es lo caro— **descarta el componente entero, callado para el usuario**: la tienda abría
# perfecta y listaba sólo 2 de 7, justamente los dos que NO tienen `<provides>` (App Library y
# Launcher). Se descubrió corriendo `RUST_LOG=info cosmic-store` desde cosmic-term DENTRO de la VM;
# desde el lanzador la salida del proceso no llega al log de la sesión.
python3 - "$MERGED" <<'PY'
import glob, os, re, sys
merged = sys.argv[1]
comps = []
for p in sorted(glob.glob(os.path.join(merged, "usr/share/metainfo/*.xml"))):
    x = open(p, encoding="utf-8").read()
    # Quedarse con el elemento raíz <component …>…</component>, sin cabecera XML ni comentarios.
    m = re.search(r"<component[\s>].*</component>", x, re.S)
    if m:
        comps.append(re.sub(r"\s*<provides>.*?</provides>", "", m.group(0), flags=re.S))
out = os.path.join(merged, "usr/share/swcatalog/xml")
os.makedirs(out, exist_ok=True)
with open(os.path.join(out, "hammer.xml"), "w", encoding="utf-8") as f:
    f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
    f.write('<components version="0.14" origin="hammer" architecture="x86_64">\n')
    f.write("\n".join(comps))
    f.write("\n</components>\n")
print("   %d componentes → /usr/share/swcatalog/xml/hammer.xml" % len(comps))
PY

echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)"
need_mb=$(du -sm "$MERGED" | cut -f1)
ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}"
echo "==> ROOT_SIZE=${ROOT_SIZE} MiB"

export ROOTFS="$MERGED" IMG ROOT_SIZE
export ESP_MB="${ESP_MB:-128}" STORE_SIZE="${STORE_SIZE:-512}" STATE_SIZE="${STATE_SIZE:-768}"
exec ./scripts/install-image-efi.sh
