#!/bin/sh
# ─────────────────────────────────────────────────────────────────────────────
#  The-Y CRM — Sicherung (DB + Dateien).  Läuft im Backup-Sidecar (postgres:16-alpine, busybox-sh).
#  Sichert die Datenbank (pg_dump) und die Datei-Ablage (MinIO-Volume) nach /backups,
#  optional zusätzlich an ein Offsite-Ziel (rclone), räumt alte Sicherungen auf und pingt
#  eine Überwachungs-URL (healthchecks.io o. ä.) bei Erfolg/Fehler.
#
#  1.1.50 Sicherheits-Hotfix (Review F03): bricht bei JEDEM Fehler ab (set -eu -o pipefail). Vorher zählte bei „pg_dump | gzip“
#  nur gzip — ein leerer/abgeschnittener Dump galt als Erfolg, und die Aufbewahrung löschte danach die guten Stände.
#  Jetzt: Dump prüfen (gunzip -t, Abschlusszeile von pg_dump, Mindestgröße BACKUP_MIN_BYTES), erst dann umbenennen und Erfolg
#  melden; alte Sicherungen nur nach einer gültigen neuen löschen, die neuesten BACKUP_KEEP_MIN (Standard 3) bleiben immer.
# ─────────────────────────────────────────────────────────────────────────────
set -eu -o pipefail
DIR="${BACKUP_DIR:-/backups}"; mkdir -p "$DIR"
TS=$(date -u +%Y%m%d-%H%M%S)
DB_PART="$DIR/db-$TS.sql.gz.part"
FILES_PART="$DIR/files-$TS.tar.gz.part"
FAILED=""
log(){ echo "[backup $(date -u +%H:%M:%S)] $*"; }
ping_ok(){   [ -n "${BACKUP_PING_URL:-}" ] && wget -q -T 10 -O /dev/null "${BACKUP_PING_URL}"        2>/dev/null || true; }
ping_fail(){ [ -n "${BACKUP_PING_URL:-}" ] && wget -q -T 10 -O /dev/null "${BACKUP_PING_URL%/}/fail" 2>/dev/null || true; }
fail(){ FAILED=1; log "FEHLER: $*"; ping_fail; exit 1; }
# Jeder Abbruch — auch ein unerwarteter über set -e — räumt halbe Dateien weg und meldet den Fehler (nie „OK“).
on_exit(){ rc=$?; rm -f "$DB_PART" "$FILES_PART"; if [ "$rc" -ne 0 ] && [ -z "$FAILED" ]; then log "FEHLER: Abbruch (Code $rc)"; ping_fail; fi; exit "$rc"; }
trap on_exit EXIT

log "Sicherung $TS startet"

# 1) Datenbank — pg_dump → gzip in eine .part-Datei (halbe Dateien zählen nie als Backup). pipefail: scheitert pg_dump, scheitert
#    die ganze Zeile. --clean --if-exists → das Backup ist in eine bestehende DB zurückspielbar (droppt Objekte zuerst).
#    1.1.52 (F14): MIT Rechten (kein --no-privileges mehr) — restore.sh stellt so auch die Rechte der App-Rolle crm_app genau wieder her.
if ! PGPASSWORD="${POSTGRES_PASSWORD:-}" pg_dump -h "${PGHOST:-postgres}" -U "${POSTGRES_USER:-crm}" -d "${POSTGRES_DB:-crm}" \
  --no-owner --clean --if-exists 2>>"$DIR/backup.log" | gzip > "$DB_PART"; then
  fail "pg_dump (Details: $DIR/backup.log)"
fi
# 1b) Prüfen, BEVOR der Dump als Sicherung gilt: gzip intakt, pg_dump hat zu Ende geschrieben, nicht verdächtig klein.
gunzip -t "$DB_PART" 2>/dev/null || fail "Dump beschädigt (gunzip -t)"
TAIL=$(gunzip -c "$DB_PART" | tail -n 12) || fail "Dump nicht lesbar"
case "$TAIL" in
  *"PostgreSQL database dump complete"*) ;;
  *) fail "Dump unvollständig (Abschlusszeile von pg_dump fehlt)" ;;
esac
SIZE=$(wc -c < "$DB_PART" | tr -d ' ')
[ "$SIZE" -ge "${BACKUP_MIN_BYTES:-4096}" ] || fail "Dump zu klein ($SIZE Bytes < ${BACKUP_MIN_BYTES:-4096}) — falsche/leere Datenbank?"
mv "$DB_PART" "$DIR/db-$TS.sql.gz"
log "DB gesichert und geprüft ($(du -h "$DIR/db-$TS.sql.gz" | cut -f1))"

# 2) Dateien — MinIO-Datenvolume (read-only gemountet unter /minio-data).
if [ -d /minio-data ]; then
  if tar czf "$FILES_PART" -C /minio-data . 2>/dev/null && gunzip -t "$FILES_PART" 2>/dev/null; then
    mv "$FILES_PART" "$DIR/files-$TS.tar.gz"
    log "Dateien gesichert ($(du -h "$DIR/files-$TS.tar.gz" | cut -f1))"
  else
    rm -f "$FILES_PART"; log "WARN Datei-Sicherung übersprungen"
  fi
fi

# 2b) Schlüssel-Sicherung (optional, ab 1.1.53): schluessel.enc — APP_ENCRYPTION_KEY, mit einer Passphrase verschlüsselt (angelegt auf
#     dem Server mit schluessel.sh --sichern). Bleibt neben den Sicherungen liegen und geht mit jeder Sicherung ans Offsite-Ziel.
#     Nur eine echte, verschlüsselte Schlüssel-Sicherung wird kopiert (nie ein Schlüssel im Klartext). Ohne die Datei: wie bisher.
KF="$DIR/schluessel.enc"; KF_OK=""
if [ -f "$KF" ]; then
  if grep -qx 'THEY-SCHLUESSEL-V1' "$KF" && grep -q '^daten=' "$KF" && ! grep -q '^wert=' "$KF"; then
    KF_OK=1; log "Schlüssel-Sicherung (verschlüsselt) liegt bei: schluessel.enc"
  else
    log "WARN schluessel.enc ist keine verschlüsselte Schlüssel-Sicherung (schluessel.sh --sichern) — wird nicht mitkopiert"
  fi
fi

# 3) Offsite (optional) — Kopie an ein rclone-Remote (S3, SFTP, Storage-Box, …).
if [ -n "${BACKUP_REMOTE:-}" ] && command -v rclone >/dev/null 2>&1; then
  OK=1
  rclone copy "$DIR/db-$TS.sql.gz" "$BACKUP_REMOTE" 2>>"$DIR/backup.log" || OK=0
  if [ -f "$DIR/files-$TS.tar.gz" ]; then rclone copy "$DIR/files-$TS.tar.gz" "$BACKUP_REMOTE" 2>>"$DIR/backup.log" || OK=0; fi
  if [ -n "$KF_OK" ]; then rclone copy "$KF" "$BACKUP_REMOTE" 2>>"$DIR/backup.log" || OK=0; fi
  if [ "$OK" = 1 ]; then log "Offsite-Kopie nach $BACKUP_REMOTE ok"; else log "WARN Offsite-Kopie fehlgeschlagen"; ping_fail; fi
fi

# 4) Aufbewahrung — erst JETZT (die neue Sicherung ist geprüft). Älter als BACKUP_KEEP_DAYS Tage → weg, aber die neuesten
#    BACKUP_KEEP_MIN Stände bleiben immer (auch wenn sie älter sind, z. B. nach Wochen ohne erfolgreiche Sicherung).
KEEP="${BACKUP_KEEP_DAYS:-14}"
KEEP_MIN="${BACKUP_KEEP_MIN:-3}"
prune(){ # $1 = Präfix (db|files), $2 = Endung — Zeitstempel im Namen → alphabetisch = vom ältesten zum neuesten
  set -- "$DIR"/"$1"-*."$2"
  [ -e "$1" ] || return 0
  cut=$(( $# - KEEP_MIN ))
  i=0
  for f in "$@"; do
    i=$((i + 1))
    if [ "$i" -gt "$cut" ]; then break; fi
    if [ -n "$(find "$f" -maxdepth 0 -mtime +"$KEEP" 2>/dev/null)" ]; then rm -f "$f"; log "alte Sicherung entfernt: ${f##*/}"; fi
  done
}
prune db sql.gz
prune files tar.gz

# 5) Erfolg melden (nur hier — nach geprüftem Dump).
ping_ok
log "Sicherung $TS fertig — $(ls -1 "$DIR"/db-*.sql.gz | wc -l) DB-Stände vorhanden"
