#!/bin/sh
# Actualizador de PBI. Lo dispara (como ROOT) la unidad pbi-update.path cuando el
# servicio web deja una solicitud en /var/lib/pbi. Tres tipos de solicitud:
#
#   .update-request    linea 1 = URL del .deb oficial   linea 2 = SHA-256 esperado
#   .install-request   linea 1 = ruta del .deb subido   linea 2 = SHA-256 esperado
#   .rollback-request  (vacio) -> reinstala la version anterior archivada
#
# El servicio web NUNCA escala privilegios: solo escribe la solicitud.
#
# Robustez: el gestor de paquetes (dpkg/apt) suele estar ocupado por los timers
# apt-daily / unattended-upgrades. En ese caso `dpkg -i` falla al no poder tomar
# el lock; por eso se ESPERA a que se libere y se REINTENTA, en vez de abortar en
# silencio (que era la causa de "reinicia pero sigue en la version anterior").
# El resultado se escribe en .update-status para que el panel pueda mostrarlo.
#
# Volver atras: cada instalacion archiva el .deb instalado en rollback/current.deb
# y desplaza el anterior a rollback/previous.deb. Volver atras = reinstalar ese
# previous.deb, sin red y sin depender del servidor de actualizaciones. Los DATOS
# NO se tocan: volver a la version anterior no debe perder lo configurado despues.
# Aun asi, antes de cada instalacion se guarda una foto de los .json por si hiciera
# falta rescatarla a mano.
set -u

# La raiz de datos es fija en produccion; la variable solo existe para poder
# probar este script en un sandbox (la unidad systemd no la define).
DATA=${PBI_UPDATE_ROOT:-/var/lib/pbi}
REQ="$DATA/.update-request"
REQ_LOCAL="$DATA/.install-request"
REQ_ROLLBACK="$DATA/.rollback-request"
LOG="$DATA/.update.log"
STATUS="$DATA/.update-status"
DEB="$DATA/.pending-update.deb"
UPLOAD="$DATA/.uploaded.deb"
ROLLDIR="$DATA/rollback"

# Estado legible por el panel (usuario pbi): "estado|fase|extra|fecha-utc".
#   estado: running | ok | error
#   fase:   download | verify | wait | install | rollback | done  (o el mensaje si error)
#   extra:  en download, bytes descargados hasta ahora (para la barra de progreso)
set_status() {
  printf '%s|%s|%s|%s\n' "$1" "$2" "${3:-}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$STATUS" 2>/dev/null || true
  chown pbi:pbi "$STATUS" 2>/dev/null || true
  chmod 640 "$STATUS" 2>/dev/null || true
}
fail() { echo "$2"; set_status error "$2" ""; exit "$1"; }

dpkg_busy() {
  fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 && return 0
  fuser /var/lib/dpkg/lock >/dev/null 2>&1 && return 0
  return 1
}

# Solo se instala un paquete que DIGA SER PBI. El panel deja la solicitud sin
# privilegios, asi que esta comprobacion es la que impide que un .deb cualquiera
# acabe instalandose como root desde la interfaz.
assert_es_pbi() {
  p=$(dpkg-deb -f "$1" Package 2>/dev/null || true)
  [ "$p" = "pbi" ] || fail 8 "el paquete no es PBI (Package=$p)"
}

# Foto de la configuracion antes de tocar nada. Pesa unos kilobytes y algun dia
# salvara a alguien; NO se restaura sola al volver atras.
guardar_datos() {
  mkdir -p "$ROLLDIR" 2>/dev/null || true
  ts=$(date -u +%Y%m%d-%H%M%S)
  tar czf "$ROLLDIR/datos-$ts.tgz" -C "$DATA" --exclude='*.deb' --ignore-failed-read \
    $(cd "$DATA" && ls *.json 2>/dev/null) 2>/dev/null || true
  # Solo se conservan las dos ultimas.
  ls -1t "$ROLLDIR"/datos-*.tgz 2>/dev/null | tail -n +3 | while read -r v; do rm -f "$v"; done
}

# Archiva el .deb recien instalado y desplaza el anterior al hueco de "version previa".
archivar() {
  mkdir -p "$ROLLDIR" 2>/dev/null || true
  ver=$(dpkg-deb -f "$1" Version 2>/dev/null || echo desconocida)
  if [ -f "$ROLLDIR/current.deb" ]; then
    mv -f "$ROLLDIR/current.deb" "$ROLLDIR/previous.deb" 2>/dev/null || true
    mv -f "$ROLLDIR/current.version" "$ROLLDIR/previous.version" 2>/dev/null || true
  fi
  cp -f "$1" "$ROLLDIR/current.deb" 2>/dev/null || true
  printf '%s\n' "$ver" > "$ROLLDIR/current.version" 2>/dev/null || true
  chown -R pbi:pbi "$ROLLDIR" 2>/dev/null || true
  chmod 750 "$ROLLDIR" 2>/dev/null || true
}

esperar_dpkg() {
  set_status running wait
  w=0
  while dpkg_busy; do
    w=$((w + 1))
    if [ "$w" -ge 60 ]; then fail 6 "el gestor de paquetes sigue ocupado (5 min)"; fi
    echo "dpkg/apt ocupado; espero 5s (intento $w)..."
    sleep 5
  done
}

instalar() {
  set_status running install
  n=0
  until dpkg -i "$1"; do
    n=$((n + 1))
    if [ "$n" -ge 12 ]; then fail 7 "dpkg fallo tras $n intentos"; fi
    echo "dpkg fallo; reintento $n en 10s..."
    set_status running wait
    sleep 10
    set_status running install
  done
}

# ---------------------------------------------------------------- volver atras
if [ -f "$REQ_ROLLBACK" ]; then
  rm -f "$REQ_ROLLBACK"
  {
    echo "== $(date) == volver a la version anterior"
    set_status running rollback
    [ -f "$ROLLDIR/previous.deb" ] || fail 9 "no hay version anterior archivada"
    assert_es_pbi "$ROLLDIR/previous.deb"
    guardar_datos
    esperar_dpkg
    instalar "$ROLLDIR/previous.deb"
    # Se intercambian los huecos: lo que acabamos de instalar pasa a ser el actual
    # y lo que habia queda como "anterior", para poder ir y volver.
    mv -f "$ROLLDIR/current.deb" "$ROLLDIR/.tmp.deb" 2>/dev/null || true
    mv -f "$ROLLDIR/current.version" "$ROLLDIR/.tmp.version" 2>/dev/null || true
    mv -f "$ROLLDIR/previous.deb" "$ROLLDIR/current.deb" 2>/dev/null || true
    mv -f "$ROLLDIR/previous.version" "$ROLLDIR/current.version" 2>/dev/null || true
    mv -f "$ROLLDIR/.tmp.deb" "$ROLLDIR/previous.deb" 2>/dev/null || true
    mv -f "$ROLLDIR/.tmp.version" "$ROLLDIR/previous.version" 2>/dev/null || true
    chown -R pbi:pbi "$ROLLDIR" 2>/dev/null || true
    echo "Vuelta a la version anterior completada."
    set_status ok done
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------- instalar un .deb ya presente
if [ -f "$REQ_LOCAL" ]; then
  RUTA=$(sed -n '1p' "$REQ_LOCAL" 2>/dev/null || true)
  SHA=$(sed -n '2p' "$REQ_LOCAL" 2>/dev/null || true)
  rm -f "$REQ_LOCAL"
  {
    echo "== $(date) == instalar paquete subido"
    # Ruta FIJA: la del fichero que deja el panel. Nada de rutas arbitrarias.
    [ "$RUTA" = "$UPLOAD" ] || fail 2 "ruta no permitida"
    [ -f "$UPLOAD" ] || fail 2 "no hay paquete subido"
    printf '%s' "$SHA" | grep -Eq '^[a-fA-F0-9]{64}$' || fail 2 "checksum con formato no valido"
    set_status running verify
    echo "${SHA}  ${UPLOAD}" | sha256sum -c - >/dev/null 2>&1 || { rm -f "$UPLOAD"; fail 4 "el checksum NO coincide"; }
    assert_es_pbi "$UPLOAD"
    guardar_datos
    esperar_dpkg
    instalar "$UPLOAD"
    archivar "$UPLOAD"
    rm -f "$UPLOAD"
    echo "Instalacion completada."
    set_status ok done
  } >>"$LOG" 2>&1
  exit 0
fi

# ------------------------------------------------------- actualizacion en linea
[ -f "$REQ" ] || exit 0
URL=$(sed -n '1p' "$REQ" 2>/dev/null || true)
SHA=$(sed -n '2p' "$REQ" 2>/dev/null || true)
rm -f "$REQ"   # consumir la solicitud (evita reprocesos)

{
  echo "== $(date) =="
  set_status running download 0

  # Solo el servidor oficial de distribucion, HTTPS y fichero .deb (misma allowlist que
  # el panel). Se rechaza '..' para evitar path traversal.
  printf '%s' "$URL" | grep -Eq '^https://software\.k0braintheworld\.com/[A-Za-z0-9._/+-]+\.deb$' \
    || fail 2 "URL no permitida"
  case "$URL" in *..*) fail 2 "URL no permitida" ;; esac
  printf '%s' "$SHA" | grep -Eq '^[a-fA-F0-9]{64}$' || fail 2 "checksum con formato no valido"

  # Descarga EN SEGUNDO PLANO para poder ir reportando los bytes recibidos (barra
  # de progreso en el panel). El tamaño total lo conoce el panel (asset de GitHub).
  rm -f "$DEB"
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --max-time 300 "$URL" -o "$DEB" & DL=$!
  elif command -v wget >/dev/null 2>&1; then
    wget -q -T 300 -O "$DEB" "$URL" & DL=$!
  else
    fail 5 "ni curl ni wget disponibles"
  fi
  while kill -0 "$DL" 2>/dev/null; do
    set_status running download "$(stat -c %s "$DEB" 2>/dev/null || echo 0)"
    sleep 1
  done
  wait "$DL" || fail 3 "descarga fallida"
  set_status running download "$(stat -c %s "$DEB" 2>/dev/null || echo 0)"

  set_status running verify
  if ! echo "${SHA}  ${DEB}" | sha256sum -c - >/dev/null 2>&1; then
    rm -f "$DEB"; fail 4 "el checksum NO coincide"
  fi
  echo "Checksum OK."
  assert_es_pbi "$DEB"

  guardar_datos
  esperar_dpkg
  instalar "$DEB"
  archivar "$DEB"

  rm -f "$DEB"
  echo "Instalacion completada."
  set_status ok done
} >>"$LOG" 2>&1
