#!/usr/bin/env bash
# Ajoute uniquement le dépôt APT StackX pour Debian 12 / Proxmox VE 8 amd64.
# N'installe aucun paquet, ne lance pas apt-get update et ne redémarre pas.
set -euo pipefail
umask 077
export LC_ALL=C
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
unset APT_CONFIG GNUPGHOME GPG_AGENT_INFO

ORIGIN='https://mirrors.scalarx.net/stackx-px/ve8'
SUITE='bookworm-pve8-security'
KEY_URL="${ORIGIN}/stackx-px-ve8.asc"
INRELEASE_URL="${ORIGIN}/dists/${SUITE}/InRelease"
KEYRING='/usr/share/keyrings/stackx-px-ve8.asc'
SOURCE_LIST='/etc/apt/sources.list.d/stackx-px-ve8.list'
FINGERPRINT='AF97ED98AACE4CE129A03B6E2C9736B593BABF7F'
APT_HELPER='/usr/lib/apt/apt-helper'

die() { printf 'Erreur : %s\n' "$*" >&2; exit 1; }
usage() {
  printf 'Usage : sudo bash %s\n' "${0##*/}"
  printf 'Configure le dépôt signé StackX pour VE 8 ; aucun paquet ni index APT installé.\n'
}

if [[ "${1:-}" == '--help' && "$#" -eq 1 ]]; then usage; exit 0; fi
test "$#" -eq 0 || { usage >&2; exit 2; }
test "$(id -u)" -eq 0 || die 'exécuter avec les droits root'
test "$(dpkg --print-architecture)" = amd64 || die 'architecture amd64 requise'
test -f /etc/os-release || die '/etc/os-release absent'
os_id="$(awk -F= '$1 == "ID" {gsub(/"/, "", $2); print $2; exit}' /etc/os-release)"
os_version="$(awk -F= '$1 == "VERSION_ID" {gsub(/"/, "", $2); print $2; exit}' /etc/os-release)"
[[ "$os_id" == debian && "$os_version" == 12 ]] || die 'Debian 12 Bookworm requis'
pve_version="$(dpkg-query -W -f='${Version}' pve-manager 2>/dev/null)" || die 'pve-manager absent'
[[ "$pve_version" == 8.* ]] || die "pve-manager 8 requis (trouvé : $pve_version)"

for command_name in awk cat chmod cmp date dpkg dpkg-query flock gpg gpgv grep id install ln mkdir mktemp mv rm stat; do
  command -v "$command_name" >/dev/null 2>&1 || die "commande absente : $command_name"
done
[[ -d /run && ! -L /run ]] || die 'répertoire /run invalide'
lock_file='/run/stackx-px-ve8-repo.lock'
if test -e "$lock_file" || test -L "$lock_file"; then
  [[ -f "$lock_file" && ! -L "$lock_file" ]] || die 'verrou existant non régulier'
  [[ "$(stat -c %u:%a "$lock_file")" == '0:600' ]] ||
    die 'propriétaire ou permissions du verrou inattendus'
fi
exec 9>"$lock_file"
flock -n 9 || die 'une autre installation du dépôt est en cours'
test -x "$APT_HELPER" || die 'apt-helper absent'
id -u _apt >/dev/null 2>&1 || die 'utilisateur _apt absent'
test -x /usr/lib/apt/methods/https || die 'méthode HTTPS APT absente'
test -s /etc/ssl/certs/ca-certificates.crt || die 'certificats CA absents'
[[ -d /usr/share/keyrings && ! -L /usr/share/keyrings ]] || die 'répertoire keyrings invalide'
[[ -d /etc/apt/sources.list.d && ! -L /etc/apt/sources.list.d ]] || die 'répertoire sources.list.d invalide'
for target in "$KEYRING" "$SOURCE_LIST"; do
  test ! -L "$target" || die "destination liée refusée : $target"
  if test -e "$target"; then
    test -f "$target" || die "destination non régulière : $target"
  fi
done

work="$(mktemp -d /var/tmp/stackx-px-ve8-repo.XXXXXXXX)"
created_key=0
created_source=0
temp_key=''
temp_source=''
cleanup() {
  status=$?
  if test "$status" -ne 0; then
    if test "$created_source" -eq 1 && test -f "$SOURCE_LIST" &&
       cmp -s -- "$work/source.list" "$SOURCE_LIST"; then
      rm -f -- "$SOURCE_LIST"
    fi
    if test "$created_key" -eq 1 && test -f "$KEYRING" &&
       cmp -s -- "$work/key.asc" "$KEYRING"; then
      rm -f -- "$KEYRING"
    fi
  fi
  if [[ -n "$temp_key" ]]; then rm -f -- "$temp_key"; fi
  if [[ -n "$temp_source" ]]; then rm -f -- "$temp_source"; fi
  rm -rf -- "$work"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
mkdir -m 0700 "$work/gnupg"
# apt-helper télécharge sous _apt. Seul ce sous-répertoire lui est accessible ;
# les copies contrôlées et le trousseau GPG restent exclusivement root.
install -d -m 0700 -o _apt -g root -- "$work/download"
chmod 0711 "$work"
export GNUPGHOME="$work/gnupg"

# apt-helper utilise la pile HTTPS et les certificats de l'APT local.
# La clé et l'index sont contrôlés avant toute écriture sous /etc ou /usr.
download_and_seal() {
  local url="$1" name="$2"
  local stderr_file="$work/$name.apt-stderr"
  if ! "$APT_HELPER" -o APT::Sandbox::User=_apt \
    -o Acquire::https::Verify-Peer=true \
    -o Acquire::https::Verify-Host=true \
    -o Acquire::https::AllowRedirect=false \
    -o Acquire::http::AllowRedirect=false \
    download-file "$url" "$work/download/$name" >/dev/null 2>"$stderr_file"; then
    cat -- "$stderr_file" >&2
    die "téléchargement impossible : $name"
  fi
  if grep -Fq 'Download is performed unsandboxed as root' "$stderr_file"; then
    die "téléchargement sans isolation _apt refusé : $name"
  fi
  if test -s "$stderr_file"; then cat -- "$stderr_file" >&2; fi
  # Le nom est d'abord déplacé hors du répertoire modifiable par _apt ;
  # la copie root est ensuite figée avant toute vérification cryptographique.
  mv -T -- "$work/download/$name" "$work/$name.downloaded" ||
    die "déplacement du téléchargement impossible : $name"
  [[ -f "$work/$name.downloaded" && ! -L "$work/$name.downloaded" ]] ||
    die "téléchargement non régulier : $name"
  install -m 0600 -o root -g root -- "$work/$name.downloaded" "$work/$name" ||
    die "copie root impossible : $name"
  rm -f -- "$work/$name.downloaded"
}
download_and_seal "$KEY_URL" key.asc
[[ -f "$work/key.asc" && ! -L "$work/key.asc" ]] || die 'clé non régulière'
key_bytes="$(stat -c %s "$work/key.asc")"
(( key_bytes >= 100 && key_bytes <= 65536 )) || die 'taille de clé inattendue'
if grep -q -- 'PRIVATE KEY' "$work/key.asc"; then die 'matériel secret refusé'; fi
key_listing="$(gpg --batch --no-options --with-colons --show-keys "$work/key.asc" 2>/dev/null)" ||
  die 'clé publique illisible'
pub_count="$(awk -F: '$1 == "pub" {n++} END {print n+0}' <<< "$key_listing")"
primary_fingerprint="$(awk -F: '$1 == "fpr" {print $10; exit}' <<< "$key_listing")"
pub_status="$(awk -F: '$1 == "pub" {print $2; exit}' <<< "$key_listing")"
pub_expires="$(awk -F: '$1 == "pub" {print $7; exit}' <<< "$key_listing")"
[[ "$pub_count" -eq 1 && "$primary_fingerprint" == "$FINGERPRINT" ]] ||
  die 'empreinte primaire de clé différente de la clé StackX historique'
[[ "$pub_status" != *r* && "$pub_status" != *e* ]] || die 'clé révoquée ou expirée'
if [[ -n "$pub_expires" ]]; then
  [[ "$pub_expires" =~ ^[0-9]+$ ]] || die 'expiration de clé illisible'
  (( pub_expires > $(date -u +%s) )) || die 'clé expirée'
fi
gpg --batch --yes --dearmor --output "$work/keyring.gpg" "$work/key.asc" 2>/dev/null ||
  die 'conversion de la clé publique impossible'

download_and_seal "$INRELEASE_URL" InRelease
chmod 0700 "$work"
rm -rf -- "$work/download"
[[ -f "$work/InRelease" && ! -L "$work/InRelease" ]] || die 'InRelease non régulier'
inrelease_bytes="$(stat -c %s "$work/InRelease")"
(( inrelease_bytes >= 100 && inrelease_bytes <= 1048576 )) || die 'taille InRelease inattendue'
gpgv --keyring "$work/keyring.gpg" --output "$work/Release" \
  "$work/InRelease" >/dev/null 2>&1 || die 'signature InRelease refusée'

release_field() {
  awk -v key="$1" '$0 ~ "^" key ": " {n++; value=substr($0, length(key)+3)}
    END {if (n == 1) print value; else exit 1}' "$work/Release"
}
test "$(release_field Origin)" = StackX || die 'Origin inattendu'
test "$(release_field Suite)" = "$SUITE" || die 'Suite inattendue'
test "$(release_field Codename)" = "$SUITE" || die 'Codename inattendu'
test "$(release_field Architectures)" = amd64 || die 'architecture Release inattendue'
test "$(release_field Components)" = main || die 'composant Release inattendu'
test "$(release_field NotAutomatic)" = yes || die 'NotAutomatic absent'
release_date="$(date -u -d "$(release_field Date)" +%s)" || die 'Date Release illisible'
valid_until="$(date -u -d "$(release_field Valid-Until)" +%s)" ||
  die 'Valid-Until illisible'
now="$(date -u +%s)"
(( release_date <= now + 600 && valid_until > now && valid_until > release_date )) ||
  die 'index expiré ou horloge incohérente'

printf '# Dépôt signé StackX pour Proxmox VE 8 sur Debian 12 amd64.\n' > "$work/source.list"
printf 'deb [arch=amd64 signed-by=%s] %s %s main\n' \
  "$KEYRING" "$ORIGIN" "$SUITE" >> "$work/source.list"

# Une deuxième déclaration de la même URL avec une autre clé casserait APT.
for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list \
                   /etc/apt/sources.list.d/*.sources; do
  test -f "$source_file" || continue
  test "$source_file" != "$SOURCE_LIST" || continue
  if awk -v origin="$ORIGIN" '$0 !~ /^[[:space:]]*#/ && index($0, origin) {found=1}
       END {exit !found}' "$source_file"; then
    die "URL déjà présente dans une autre source APT : $source_file"
  fi
done

# Prévalider les deux destinations avant toute écriture.
source_exists=0
if test -e "$SOURCE_LIST"; then
  cmp -s -- "$work/source.list" "$SOURCE_LIST" || die "source existante différente : $SOURCE_LIST"
  [[ "$(stat -c %u:%a "$SOURCE_LIST")" == '0:644' ]] ||
    die "permissions de source existante inattendues : $SOURCE_LIST"
  source_exists=1
fi
key_exists=0
refresh_key=0
if test -e "$KEYRING"; then
  [[ "$(stat -c %u:%a "$KEYRING")" == '0:644' ]] ||
    die "permissions de clé existante inattendues : $KEYRING"
  key_exists=1
  if ! cmp -s -- "$work/key.asc" "$KEYRING"; then
    # Un nouvel export peut changer les UID sans changer la clé de signature.
    # N'accepter ce rafraîchissement que pour la même clé primaire vérifiée.
    old_listing="$(gpg --batch --no-options --with-colons --show-keys "$KEYRING" 2>/dev/null)" ||
      die 'clé existante illisible'
    old_pub_count="$(awk -F: '$1 == "pub" {n++} END {print n+0}' <<< "$old_listing")"
    old_fingerprint="$(awk -F: '$1 == "fpr" {print $10; exit}' <<< "$old_listing")"
    old_pub_status="$(awk -F: '$1 == "pub" {print $2; exit}' <<< "$old_listing")"
    old_pub_expires="$(awk -F: '$1 == "pub" {print $7; exit}' <<< "$old_listing")"
    [[ "$old_pub_count" -eq 1 && "$old_fingerprint" == "$FINGERPRINT" ]] ||
      die 'empreinte de clé existante différente'
    [[ "$old_pub_status" != *r* && "$old_pub_status" != *e* ]] ||
      die 'clé existante révoquée ou expirée : rafraîchissement automatique refusé'
    if [[ -n "$old_pub_expires" ]]; then
      [[ "$old_pub_expires" =~ ^[0-9]+$ ]] || die 'expiration de clé existante illisible'
      (( old_pub_expires > now )) || die 'clé existante expirée'
    fi
    refresh_key=1
  fi
fi

# Première installation : la clé précède la source. Rafraîchissement :
# la nouvelle clé remplace l'ancienne seulement après contrôle de la source.
if test "$key_exists" -eq 0; then
  temp_key="$(mktemp /usr/share/keyrings/.stackx-px-ve8.XXXXXXXX)"
  install -m 0644 -o root -g root -- "$work/key.asc" "$temp_key"
  ln -- "$temp_key" "$KEYRING" || die 'création de la clé impossible'
  created_key=1
  rm -f -- "$temp_key"
fi
if test "$source_exists" -eq 0; then
  temp_source="$(mktemp /etc/apt/sources.list.d/.stackx-px-ve8.XXXXXXXX)"
  install -m 0644 -o root -g root -- "$work/source.list" "$temp_source"
  ln -- "$temp_source" "$SOURCE_LIST" || die 'création de la source impossible'
  created_source=1
  rm -f -- "$temp_source"
fi
if test "$refresh_key" -eq 1; then
  temp_key="$(mktemp /usr/share/keyrings/.stackx-px-ve8.XXXXXXXX)"
  install -m 0644 -o root -g root -- "$work/key.asc" "$temp_key"
  mv -T -- "$temp_key" "$KEYRING" || die 'rafraîchissement de la clé impossible'
  temp_key=''
fi

printf 'Dépôt configuré : %s (%s main)\n' "$ORIGIN" "$SUITE"
printf 'Clé vérifiée : %s\n' "$FINGERPRINT"
printf 'Aucun paquet installé, aucun index APT rafraîchi, aucun redémarrage.\n'
printf 'Étape suivante : apt-get update, puis simulation des sept versions exactes sur ce nœud.\n'
