#!/bin/bash
# Vérifie l'intégrité d'une sauvegarde osx-backup, sans rien y écrire.
#
#   ./verifier.sh [chemin-de-la-sauvegarde]
#
# Le chemin par défaut est le dossier qui contient ce script.
# Définir OSX_BACKUP_PASSPHRASE active en plus le déchiffrement réel de
# l'archive des secrets et le comptage de ses fichiers.
#
# Code de sortie : 0 si tout est valide, 1 si au moins une anomalie bloquante.

set -u
D="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
ok=0; alerte=0; erreur=0

titre()  { printf '\n\033[1m%s\033[0m\n' "$1"; printf '%.0s─' $(seq 1 78); echo; }
bon()    { printf '  \033[32m✓\033[0m %s\n' "$1"; ok=$((ok+1)); }
attn()   { printf '  \033[33m!\033[0m %s\n' "$1"; alerte=$((alerte+1)); }
mal()    { printf '  \033[31m✗\033[0m %s\n' "$1"; erreur=$((erreur+1)); }
saut()   { printf '  \033[90m-\033[0m %s\n' "$1"; }

[ -d "$D" ] || { echo "Sauvegarde introuvable : $D"; exit 1; }
echo "Sauvegarde : $D"

# ─────────────────────────────────────────────────────────────── manifeste ───
titre "Manifeste"
if [ -f "$D/manifest.json" ]; then
  python3 - "$D/manifest.json" <<'PY'
import json,sys
m=json.load(open(sys.argv[1]))
print(f"  date du dernier run : {m['created_at']}")
print(f"  machine d'origine   : {m['hostname']}  ({m['home']})")
ko=[k for k,v in m['modules'].items() if v['status']!='ok']
for n,v in sorted(m['modules'].items()):
    d=v.get('created_at','')
    print(f"    {n:10} {v['status']:8} {v.get('items',0):>7} él.  {d}")
sys.exit(1 if ko else 0)
PY
  [ $? -eq 0 ] && bon "tous les modules du manifeste sont en statut ok" \
               || mal "au moins un module n'est pas en statut ok"
else
  mal "manifest.json absent : impossible de savoir ce que contient la sauvegarde"
fi

# ───────────────────────────────────────────────────────────────── secrets ───
titre "Archive chiffrée des secrets"
S="$D/secrets.tar.age"
if [ ! -f "$S" ]; then
  attn "secrets.tar.age absent : clés SSH, GPG, jetons et .env non sauvegardés"
else
  taille=$(wc -c < "$S" | tr -d ' ')
  if [ "$(head -c 21 "$S")" = "age-encryption.org/v1" ]; then
    bon "en-tête age v1 valide ($(numfmt --to=iec $taille 2>/dev/null || echo "$taille octets"))"
  else
    mal "en-tête age invalide : fichier corrompu ou tronqué"
  fi
  head -c 400 "$S" | grep -q '^-> scrypt ' \
    && bon "chiffrement par passphrase (scrypt)" \
    || mal "aucun destinataire scrypt : l'archive n'est pas déchiffrable par passphrase"
  head -c 400 "$S" | grep -q '^--- ' \
    && bon "signature d'en-tête présente" \
    || mal "signature d'en-tête absente"

  if [ -n "${OSX_BACKUP_PASSPHRASE:-}" ]; then
    if command -v uv >/dev/null && [ -f "$D/osx-backup/pyproject.toml" ]; then
      n=$(uv run --project "$D/osx-backup" python - "$S" <<'PY' 2>/dev/null
import io,os,sys,tarfile,pyrage
data=pyrage.passphrase.decrypt(open(sys.argv[1],'rb').read(), os.environ['OSX_BACKUP_PASSPHRASE'])
with tarfile.open(fileobj=io.BytesIO(data)) as t:
    print(sum(1 for m in t if m.isfile() or m.issym()))
PY
)
      if [ -n "$n" ]; then
        bon "déchiffrement réussi, $n fichiers lisibles dans l'archive"
        att=$(python3 -c "import json;print(json.load(open('$D/manifest.json'))['modules'].get('secrets',{}).get('items',0))" 2>/dev/null)
        [ "$n" = "$att" ] && bon "compte identique au manifeste ($att)" \
                          || attn "compte différent du manifeste : $n vs $att"
      else
        mal "déchiffrement impossible : passphrase erronée ou archive corrompue"
      fi
    else
      saut "uv ou copie de l'outil absente : test de déchiffrement ignoré"
    fi
  else
    saut "OSX_BACKUP_PASSPHRASE non définie : test de déchiffrement ignoré"
  fi
fi

# ─────────────────────────────────────────────────────── volumes docker ───
titre "Volumes Docker"
V="$D/docker/volumes"
if [ ! -d "$V" ]; then
  saut "aucun volume Docker dans cette sauvegarde"
else
  n_ok=0; n_vide=0
  for f in "$V"/*.tar.gz; do
    [ -e "$f" ] || continue
    nom=$(basename "$f" .tar.gz)
    if ! gzip -t "$f" 2>/dev/null; then mal "$nom : flux gzip corrompu"; continue; fi
    liste=$(tar -tzf "$f" 2>/dev/null) || { mal "$nom : structure tar illisible"; continue; }
    nb=$(printf '%s\n' "$liste" | grep -c .)
    if [ "$nb" -le 1 ]; then n_vide=$((n_vide+1))
    else
      n_ok=$((n_ok+1))
      case "$liste" in *PG_VERSION*)
        printf '%s\n' "$liste" | grep -q '^\./base/' \
          || attn "$nom : PG_VERSION présent mais base/ vide" ;;
      esac
    fi
  done
  [ "$n_ok" -gt 0 ] && bon "$n_ok archives de volumes valides"
  [ "$n_vide" -gt 0 ] && attn "$n_vide archive(s) vide(s) — normal si le volume l'était"
  [ -f "$D/docker/inventory.json" ] && python3 - "$D/docker/inventory.json" <<'PY'
import json,sys
i=json.load(open(sys.argv[1]))
print(f"  inventaire : {len(i.get('containers',[]))} containers, "
      f"{len(i.get('volumes',[]))} volumes, {len(i.get('images',[]))} images")
PY
fi

# ────────────────────────────────────────────────────────── dumps SQL ───
titre "Dumps logiques des bases"
for rep in "$D/db-dumps" "$D/postgres"; do
  [ -d "$rep" ] || continue
  echo "  [$(basename "$rep")]"
  for f in "$rep"/*.dump; do
    [ -e "$f" ] || continue
    nom=$(basename "$f")
    if command -v pg_restore >/dev/null; then
      o=$(pg_restore -l "$f" 2>/dev/null | grep -c '^[0-9;]')
      t=$(pg_restore -l "$f" 2>/dev/null | grep -c 'TABLE DATA')
      [ "$o" -gt 0 ] && bon "$nom : $o objets, $t tables avec données" \
                     || mal "$nom : illisible par pg_restore"
    else
      head -c 5 "$f" | grep -q 'PGDMP' && bon "$nom : en-tête PGDMP valide" \
                                       || mal "$nom : en-tête invalide"
    fi
  done
  for f in "$rep"/*globals.sql; do
    [ -e "$f" ] || continue
    nom=$(basename "$f")
    if [ -s "$f" ] && grep -q 'PostgreSQL database cluster dump' "$f"; then
      bon "$nom : $(grep -c '^CREATE ROLE' "$f") rôle(s)"
    else
      mal "$nom : fichier de rôles vide ou invalide"
    fi
  done
  for f in "$rep"/*.sql.gz; do
    [ -e "$f" ] || continue
    nom=$(basename "$f")
    if gzip -t "$f" 2>/dev/null; then
      bon "$nom : $(gzip -dc "$f" | grep -c '^CREATE TABLE') tables, $(gzip -dc "$f" | grep -c '^CREATE DATABASE') bases"
    else
      mal "$nom : flux gzip corrompu"
    fi
  done
done

# ──────────────────────────────────────────────────────────── miroirs ───
titre "Miroirs de fichiers"
for m in github documents; do
  if [ -d "$D/$m" ]; then
    bon "$m : $(ls "$D/$m" | wc -l | tr -d ' ') entrées, $(du -sh "$D/$m" 2>/dev/null | cut -f1)"
    [ "$m" = github ] && {
      g=$(find "$D/github" -maxdepth 2 -name .git 2>/dev/null | wc -l | tr -d ' ')
      bon "github : $g dépôts git conservés"
      for f in $(find "$D/github" -maxdepth 3 -name '.env' -o -maxdepth 3 -name '.env.*' 2>/dev/null | head -1); do
        attn "un fichier .env traîne dans le miroir : $f"
      done
    }
  else
    saut "$m absent de cette sauvegarde"
  fi
done

# ───────────────────────────────────────────────── configs et outils ───
titre "Configurations et gestionnaires de paquets"
[ -f "$D/brew/Brewfile" ] && {
  bon "Brewfile : $(grep -c '^' "$D/brew/Brewfile") lignes"
  grep -q '^brew "rsync"' "$D/brew/Brewfile" \
    && bon "rsync GNU présent dans le Brewfile (prérequis de la restauration)" \
    || attn "rsync absent du Brewfile : github et documents seront indisponibles au restore"
}
[ -f "$D/brew/services.json" ] && bon "services brew : $(python3 -c "import json;print(len(json.load(open('$D/brew/services.json'))))" 2>/dev/null) entrées"
[ -f "$D/mise/config.toml" ]   && bon "mise : config.toml présent"
[ -f "$D/uv/tools.json" ]      && bon "uv : $(python3 -c "import json;print(len(json.load(open('$D/uv/tools.json'))))" 2>/dev/null) outils"
[ -f "$D/apps/applications.md" ] && bon "apps : $(( $(grep -c '^| ' "$D/apps/applications.md") - 1 )) applications listées"

for m in dotfiles claude editors; do
  e="$D/$m/_entries.json"
  if [ -f "$e" ]; then
    manquants=$(python3 - "$e" "$D/$m" <<'PY'
import json,sys,os
base=sys.argv[2]
print(sum(1 for r in json.load(open(sys.argv[1]))
          if not os.path.lexists(os.path.join(base,r))))
PY
)
    [ "$manquants" = "0" ] \
      && bon "$m : $(python3 -c "import json;print(len(json.load(open('$e'))))") entrées, toutes présentes sur le disque" \
      || mal "$m : $manquants entrée(s) listée(s) mais absente(s) du disque"
  else
    saut "$m absent de cette sauvegarde"
  fi
done

# ──────────────────────────────────────────────────── autonomie du disque ───
titre "Autonomie de la sauvegarde"
[ -f "$D/README.md" ] && bon "README de réinstallation présent" || attn "README.md absent"
[ -f "$D/osx-backup/pyproject.toml" ] \
  && bon "copie de l'outil présente : restauration possible sans réseau ni dépôt" \
  || attn "copie de l'outil absente : il faudra récupérer osx-backup autrement"
[ -d "$D/logs" ] && bon "journaux : $(ls "$D/logs" | wc -l | tr -d ' ') fichier(s)"

# ───────────────────────────────────────────────────────────────── bilan ───
titre "Bilan"
echo "  contrôles réussis : $ok"
echo "  avertissements    : $alerte"
echo "  anomalies         : $erreur"
echo
if [ "$erreur" -gt 0 ]; then
  printf '\033[31m  Sauvegarde incomplète ou endommagée.\033[0m\n'; exit 1
elif [ "$alerte" -gt 0 ]; then
  printf '\033[33m  Sauvegarde exploitable, avec des réserves listées ci-dessus.\033[0m\n'; exit 0
else
  printf '\033[32m  Sauvegarde complète et vérifiée.\033[0m\n'; exit 0
fi
