#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
source "$SCRIPT_DIR/libenv.sh"

log() { printf '[limristem-mail-restore] %s\n' "$*"; }

usage() {
  cat <<'EOF'
Uso:
  restore.sh --source /var/backups/limristem-mail/<backup-dir> [--target-root /] [--restore-files] [--restore-db]
             [--i-trust-this-archive]

Note:
  - Il backup viene verificato contro SHA256SUMS prima di qualunque estrazione. Il restore gira
    come root e scompatta sulla root di destinazione: un archivio manomesso compromette l'host.
    --i-trust-this-archive salta la verifica solo quando il manifest è assente; usalo unicamente
    su un archivio di cui conosci con certezza la provenienza.
  - Il restore filesystem di backup incrementali richiede l'applicazione della catena completa (full + incrementali successivi).
  - I backup MariaDB fisici vengono solo estratti/stage-ati: la copy-back finale richiede un intervento controllato.
  - Il restore DB richiede privilegi DDL (DROP/CREATE): usa MariaDB root via socket locale o LIMRISTEM_MAIL_DB_ROOT_PASS.
EOF
}

require_root() {
  if [[ $EUID -ne 0 ]]; then
    log "Questo script va eseguito come root."
    exit 1
  fi
}

load_env() {
  limristem_mail_load_env_file "$LIMRISTEM_MAIL_CONFIG_DIR/limristem-mail.env"
}

mysql_client_cmd() {
  if command -v mariadb >/dev/null 2>&1; then
    printf 'mariadb\n'
  else
    printf 'mysql\n'
  fi
}

# Elevated DB client for logical restore (needs DROP/CREATE). Prefer local root socket.
mysql_restore_cmd() {
  local mysql_cmd
  mysql_cmd=$(mysql_client_cmd)
  local db_name=${LIMRISTEM_MAIL_DB_NAME:-limristem-mail}

  # 1) Local unix socket as root without password (default Debian/MariaDB root)
  if "$mysql_cmd" -uroot --protocol=socket "$db_name" -e "SELECT 1" >/dev/null 2>&1; then
    printf '%s\n' "$mysql_cmd -uroot --protocol=socket"
    return 0
  fi
  # 2) Explicit root password
  if [[ -n "${LIMRISTEM_MAIL_DB_ROOT_PASS:-}" ]]; then
    if MYSQL_PWD="$LIMRISTEM_MAIL_DB_ROOT_PASS" "$mysql_cmd" -h"${LIMRISTEM_MAIL_DB_HOST:-127.0.0.1}" -P"${LIMRISTEM_MAIL_DB_PORT:-3306}" -uroot "$db_name" -e "SELECT 1" >/dev/null 2>&1; then
      printf 'MYSQL_PWD=%q %s -h%q -P%q -uroot\n' \
        "$LIMRISTEM_MAIL_DB_ROOT_PASS" "$mysql_cmd" \
        "${LIMRISTEM_MAIL_DB_HOST:-127.0.0.1}" \
        "${LIMRISTEM_MAIL_DB_PORT:-3306}"
      # Can't easily return env+cmd with quoting for eval - use function instead
      return 2
    fi
  fi
  return 1
}

run_mysql_restore() {
  local mysql_cmd db_name host port
  mysql_cmd=$(mysql_client_cmd)
  db_name=${LIMRISTEM_MAIL_DB_NAME:-limristem-mail}
  host=${LIMRISTEM_MAIL_DB_HOST:-127.0.0.1}
  port=${LIMRISTEM_MAIL_DB_PORT:-3306}

  if "$mysql_cmd" -uroot --protocol=socket "$db_name" -e "SELECT 1" >/dev/null 2>&1; then
    log "Restore DB con MariaDB root (unix socket)"
    if [[ "$1" == "--gzip" ]]; then
      gunzip -c "$2" | "$mysql_cmd" -uroot --protocol=socket "$db_name"
    else
      "$mysql_cmd" -uroot --protocol=socket "$db_name" < "$2"
    fi
    return 0
  fi

  if [[ -n "${LIMRISTEM_MAIL_DB_ROOT_PASS:-}" ]]; then
    log "Restore DB con MariaDB root (password da LIMRISTEM_MAIL_DB_ROOT_PASS)"
    if [[ "$1" == "--gzip" ]]; then
      gunzip -c "$2" | MYSQL_PWD="$LIMRISTEM_MAIL_DB_ROOT_PASS" "$mysql_cmd" -h"$host" -P"$port" -uroot "$db_name"
    else
      MYSQL_PWD="$LIMRISTEM_MAIL_DB_ROOT_PASS" "$mysql_cmd" -h"$host" -P"$port" -uroot "$db_name" < "$2"
    fi
    return 0
  fi

  log "ERRORE: restore logico richiede privilegi DDL (DROP/CREATE)."
  log "Configura accesso root locale (socket) oppure limristem_mail_env LIMRISTEM_MAIL_DB_ROOT_PASS."
  log "L'utente applicativo (${LIMRISTEM_MAIL_DB_USER:-app}) ha solo SELECT/INSERT/UPDATE/DELETE e non può ripristinare lo schema."
  return 1
}

SOURCE_DIR=''
TARGET_ROOT='/'
RESTORE_FILES=false
RESTORE_DB=false
SKIP_INTEGRITY_CHECK=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --source)
      SOURCE_DIR=$2
      shift 2
      ;;
    --target-root)
      TARGET_ROOT=$2
      shift 2
      ;;
    --restore-files)
      RESTORE_FILES=true
      shift
      ;;
    --restore-db)
      RESTORE_DB=true
      shift
      ;;
    --i-trust-this-archive)
      SKIP_INTEGRITY_CHECK=true
      shift
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      log "Argomento non riconosciuto: $1"
      usage
      exit 1
      ;;
  esac
done

# backup.sh writes a SHA256SUMS manifest into every backup directory. Restoring
# runs as root and unpacks straight onto the target root, so a tampered archive is
# a full host compromise: verify the manifest before touching anything. Backups are
# copied off-host over rclone (FTP/FTPS/SFTP/S3), so the archive is not inherently
# trusted just because it sits in the backup directory.
backup_signing_pub_path() {
  printf '%s/backup-signing.pub' "$(limristem_mail_resolve_managed_config_dir)"
}

# Members the restore refuses to write, even from a correctly signed archive.
#
# The backup archives $BASE_DIR wholesale, which means it carries CODE: bin/manage-*.sh
# are the sudoers targets and .venv holds the interpreter the root timers execute.
# Restoring those from an archive means trusting the archive with root. They are
# reproducible from the signed release package instead, so the recovery order is:
# install the package first, then restore data and config on top of it.
RESTORE_CODE_EXCLUDES=(
  'opt/limristem-mail/bin/*'
  'opt/limristem-mail/.venv/*'
  'opt/limristem-mail/api/*'
  'opt/limristem-mail/scripts/*'
  'opt/limristem-mail/limristem-mail'
)

# Only these prefixes may be written. A signed archive from this install will never
# contain anything else; refusing the rest keeps a surprising member (or a future
# change to what gets archived) from landing somewhere unexpected as root.
RESTORE_ALLOWED_PREFIXES=(
  'var/mail/'
  'var/lib/limristem-mail/'
  'opt/limristem-mail/'
  'etc/postfix/'
  'etc/dovecot/'
  'etc/rspamd/'
  'etc/nginx/'
  'etc/limristem-mail.d/'
  'etc/default/postsrsd'
  'etc/limristem-mail.env'
  'etc/limristem-mail-backup.env'
  'etc/limristem-mail-rclone.conf'
)

build_restore_filter_args() {
  RESTORE_TAR_ARGS=()
  local pattern
  for pattern in "${RESTORE_CODE_EXCLUDES[@]}"; do
    RESTORE_TAR_ARGS+=("--exclude=$pattern" "--exclude=./$pattern")
  done
}

# Refuse the whole archive if it carries a member outside the allowlist, rather
# than silently skipping it: an unexpected member means the archive is not what we
# think it is, and that is a reason to stop and look, not to continue.
assert_archive_members_are_expected() {
  local fs_archive=$1
  local unexpected
  unexpected=$(tar --list --file="$fs_archive" 2>/dev/null | awk -v prefixes="$(printf '%s\n' "${RESTORE_ALLOWED_PREFIXES[@]}")" '
    {
      path = $0
      sub(/^\.\//, "", path)
      if (path == "" || path == "./") next
      allowed = 0
      n = split(prefixes, p, "\n")
      for (i = 1; i <= n; i++) {
        if (p[i] == "") continue
        # Under an allowed prefix, or an ancestor directory of one: tar lists the
        # intermediate dirs ("opt/", "etc/") and refusing those would reject every
        # legitimate archive.
        if (index(path, p[i]) == 1 || index(p[i], path) == 1) { allowed = 1; break }
      }
      if (!allowed) print path
    }
  ' | head -n 20)
  if [[ -n "$unexpected" ]]; then
    log "L'archivio contiene percorsi fuori dall'ambito di un backup limristem-mail:"
    printf '  %s\n' $unexpected | head -n 20
    log "Interrompo: un backup legittimo di questa installazione non li conterrebbe."
    exit 1
  fi
}

verify_backup_manifest() {
  local manifest="$SOURCE_DIR/SHA256SUMS"
  if [[ ! -f "$manifest" ]]; then
    if [[ "$SKIP_INTEGRITY_CHECK" == true ]]; then
      log "ATTENZIONE: SHA256SUMS assente e --i-trust-this-archive attivo: proseguo senza verifica."
      return 0
    fi
    log "SHA256SUMS non trovato in $SOURCE_DIR: rifiuto di ripristinare un backup non verificabile."
    log "Se il backup è certamente integro, ripeti con --i-trust-this-archive."
    exit 1
  fi
  log "Verifico l'integrità del backup (SHA256SUMS)..."
  if ! ( cd "$SOURCE_DIR" && sha256sum --quiet -c SHA256SUMS ); then
    log "Verifica integrità FALLITA: il backup è incompleto o manomesso. Interrompo."
    exit 1
  fi
  log "Integrità del backup verificata."
  verify_backup_signature "$manifest"
}

# SHA256SUMS proves the archive matches the manifest — but both live in the same
# directory, so whoever can rewrite one can rewrite the other. Only the Ed25519
# signature proves the manifest came from THIS installation: the private half never
# left the server, so a hostile backup store cannot produce a valid one.
verify_backup_signature() {
  local manifest=$1
  local signature="${manifest}.sig"
  local pub
  pub=$(backup_signing_pub_path)

  if [[ ! -f "$signature" ]]; then
    if [[ "$SKIP_INTEGRITY_CHECK" == true ]]; then
      log "ATTENZIONE: firma assente e --i-trust-this-archive attivo: proseguo senza autenticazione."
      return 0
    fi
    log "Firma SHA256SUMS.sig assente: questo backup non è autenticabile."
    log "Un backup manomesso supererebbe comunque il controllo SHA256SUMS, perché il manifest"
    log "si trova nella stessa directory dell'archivio. Ripeti con --i-trust-this-archive solo"
    log "se conosci con certezza la provenienza di questo archivio."
    exit 1
  fi

  if [[ ! -s "$pub" ]]; then
    if [[ "$SKIP_INTEGRITY_CHECK" == true ]]; then
      log "ATTENZIONE: chiave pubblica assente e --i-trust-this-archive attivo: firma non verificata."
      return 0
    fi
    log "Chiave pubblica di firma non trovata: $pub"
    log "In un ripristino su macchina nuova, recuperala dalla copia che hai messo al sicuro"
    log "(l'installer la mostra e la salva in ~/limristem-mail-backup-signing.pub) e copiala lì,"
    log "oppure ripeti con --i-trust-this-archive per proseguire senza verificarla."
    exit 1
  fi

  log "Verifico la firma del manifest..."
  if ! openssl pkeyutl -verify -pubin -inkey "$pub" -rawin \
      -sigfile "$signature" -in "$manifest" >/dev/null 2>&1; then
    log "FIRMA NON VALIDA: questo backup non è stato prodotto da questa installazione,"
    log "oppure è stato modificato dopo la creazione. Interrompo."
    exit 1
  fi
  log "Firma del manifest verificata."
}

main() {
  require_root
  load_env

  if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
    log "Specifica una directory backup valida con --source."
    exit 1
  fi

  verify_backup_manifest

  if [[ "$RESTORE_FILES" != true && "$RESTORE_DB" != true ]]; then
    log "Seleziona almeno una tra --restore-files e --restore-db."
    exit 1
  fi

  if [[ "$RESTORE_FILES" == true ]]; then
    local fs_archive
    fs_archive=$(find "$SOURCE_DIR" -maxdepth 1 -type f -name 'filesystem.*.tar*' | sort | head -n 1)
    if [[ -z "$fs_archive" ]]; then
      log "Archivio filesystem non trovato in $SOURCE_DIR"
      exit 1
    fi
    assert_archive_members_are_expected "$fs_archive"
    build_restore_filter_args
    mkdir -p "$TARGET_ROOT"
    log "Ripristino filesystem da ${fs_archive} verso ${TARGET_ROOT}"
    log "Il codice dell'applicazione NON viene ripristinato: reinstalla il pacchetto firmato."
    # --no-same-owner / --no-same-permissions: for root these default to ON, so a
    # crafted member would otherwise be restored with its archived uid/gid and mode
    # bits — including setuid. Ownership is re-established by the installer and the
    # manage-* helpers, never by trusting what the archive claims.
    tar --extract --auto-compress --listed-incremental=/dev/null \
      --no-same-owner --no-same-permissions --no-overwrite-dir \
      "${RESTORE_TAR_ARGS[@]}" \
      --file="$fs_archive" --directory="$TARGET_ROOT"
    # Belt and braces: nothing in a limristem-mail backup legitimately carries the
    # setuid/setgid bit, so strip any that survived.
    find "$TARGET_ROOT/opt/limristem-mail" "$TARGET_ROOT/etc/limristem-mail.d" \
      -xdev \( -perm -4000 -o -perm -2000 \) -type f -exec chmod a-s {} + 2>/dev/null || true
  fi

  if [[ "$RESTORE_DB" == true ]]; then
    local logical_dump physical_dir
    logical_dump=$(find "$SOURCE_DIR" -maxdepth 1 -type f -name 'mariadb.logical.sql*' | sort | head -n 1)
    physical_dir=$(find "$SOURCE_DIR" -maxdepth 1 -type d -name 'mariadb.physical' | sort | head -n 1)

    if [[ -n "$logical_dump" ]]; then
      log "Ripristino dump logico ${logical_dump}"
      if [[ "$logical_dump" == *.gz ]]; then
        run_mysql_restore --gzip "$logical_dump"
      else
        run_mysql_restore --file "$logical_dump"
      fi
    elif [[ -n "$physical_dir" ]]; then
      log "Backup MariaDB fisico rilevato in ${physical_dir}"
      log "Esegui manualmente 'mariabackup --prepare --target-dir=${physical_dir}' e la successiva copy-back in una finestra controllata."
    else
      log "Nessun backup database trovato in $SOURCE_DIR"
      exit 1
    fi
  fi

  log "Restore completato."
}

main "$@"
