#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

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

log() {
  local msg="[limristem-mail-backup] $*"
  printf '%s\n' "$msg"
  printf '[%s] %s\n' "$(date -Iseconds)" "$msg" >> /var/log/limristem-mail-backup.log
}

fail_with_error() {
  local message=$1
  trap - ERR
  log "ERROR: ${message}"
  exit 1
}

handle_error() {
  local exit_code=$?
  local line_no=$1
  printf '[limristem-mail-backup] ERROR: backup failed unexpectedly on line %s.\n' "$line_no" >&2
  exit "$exit_code"
}

trap 'handle_error $LINENO' ERR

require_writable_directory() {
  local dir_path=$1
  local label=$2
  if limristem_mail_path_is_writable "$dir_path/.limristem-mail-backup-write-test"; then
    return 0
  fi
  fail_with_error "${label} is not writable in the current environment: ${dir_path}"
}

STATE_DIR=$(limristem_mail_resolve_managed_config_dir)
SCHEDULES_FILE=$STATE_DIR/backup-schedules.json
STORAGES_FILE=$STATE_DIR/backup-storages.json

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

load_env() {
  limristem_mail_load_env_file "$(limristem_mail_resolve_main_env_file)"
  limristem_mail_load_env_file "$(limristem_mail_resolve_backup_env_file)"
  local rclone_config
  rclone_config=$(limristem_mail_resolve_rclone_config_file)
  if [[ -f "$rclone_config" ]]; then
    export RCLONE_CONFIG="$rclone_config"
  fi
}

load_schedule_overrides() {
  local schedule_id=$1
  if [[ ! -f "$SCHEDULES_FILE" ]]; then
    return 0
  fi
  local py="${LIMRISTEM_MAIL_BASE_DIR:-/opt/limristem-mail}/.venv/bin/python"
  if [[ ! -x "$py" ]]; then
    py="${LIMRISTEM_MAIL_BASE_DIR:-/opt/limristem-mail}/venv/bin/python"
  fi
  if [[ ! -x "$py" ]]; then
    py=python3
  fi
  "$py" - "$SCHEDULES_FILE" "$STORAGES_FILE" "$schedule_id" <<'PY'
import json
import os
import pathlib
import re
import shlex
import sys

def normalize_choice(value, default="", mode="raw"):
    candidate = str(value if value is not None else default).strip()
    if len(candidate) >= 2 and candidate[0] == candidate[-1] and candidate[0] in {'"', "'"}:
        candidate = candidate[1:-1].strip()
    if not candidate:
        candidate = default
    if mode == "lower":
        return candidate.lower()
    if mode == "title":
        return candidate.title()
    return candidate

def decrypt_secret(value):
    """Decrypt Fernet-at-rest backup credentials; accept legacy plaintext."""
    if not value:
        return ""
    base = os.environ.get("LIMRISTEM_MAIL_BASE_DIR", "/opt/limristem-mail")
    for candidate in (os.path.join(base, "bin"), base):
        if candidate not in sys.path:
            sys.path.insert(0, candidate)
    try:
        from api.crypto import decrypt_secret_flexible
        return decrypt_secret_flexible(str(value)) or ""
    except Exception as exc:
        raise SystemExit("Unable to decrypt backup storage credentials") from exc

schedules = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") or "[]")
storages = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding="utf-8") or "[]") if pathlib.Path(sys.argv[2]).exists() else []
schedule_id = sys.argv[3]
schedule = next((item for item in schedules if item.get("id") == schedule_id), None)
if schedule is None and schedule_id:
    raise SystemExit(f"Unknown backup schedule: {schedule_id}")
if schedule is None:
    schedule = next((item for item in schedules if item.get("enabled", "yes") == "yes"), schedules[0] if schedules else None)
if schedule is None:
    raise SystemExit(0)
storage = next((item for item in storages if item.get("id") == schedule.get("storage_id")), {})
if schedule.get("storage_id") and not storage:
    raise SystemExit("Backup schedule references a missing storage")
if not re.fullmatch(r"[a-z0-9][a-z0-9_.-]{0,63}", str(schedule.get("id", ""))):
    raise SystemExit("Invalid backup schedule ID")
backup_type = normalize_choice(schedule.get("backup_type", "auto"), "auto", mode="lower")
full_weekday = normalize_choice(schedule.get("full_weekday", "Sun"), "Sun", mode="title")
compression = normalize_choice(schedule.get("compression", "gzip"), "gzip", mode="lower")
include_redis = normalize_choice(schedule.get("include_redis", "yes"), "yes", mode="lower")
include_database = normalize_choice(schedule.get("include_database", "yes"), "yes", mode="lower")
exports = {
    "LIMRISTEM_MAIL_BACKUP_SCHEDULE_ID": schedule.get("id", ""),
    "LIMRISTEM_MAIL_BACKUP_TYPE": backup_type,
    "LIMRISTEM_MAIL_BACKUP_FULL_WEEKDAY": full_weekday,
    "LIMRISTEM_MAIL_BACKUP_COMPRESSION": compression,
    "LIMRISTEM_MAIL_BACKUP_INCLUDE_REDIS": include_redis,
    "LIMRISTEM_MAIL_BACKUP_DB_MODE": "logical" if include_database == "yes" else "none",
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_TYPE": storage.get("type", "local"),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_REMOTE_NAME": storage.get("remote_name", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PATH": storage.get("path", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_HOST": storage.get("host", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PORT": storage.get("port", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_USER": storage.get("user", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PASSWORD": decrypt_secret(storage.get("password", "")),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_BUCKET": storage.get("bucket", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_REGION": storage.get("region", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ENDPOINT": storage.get("endpoint", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ACCESS_KEY_ID": storage.get("access_key_id", ""),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_SECRET_ACCESS_KEY": decrypt_secret(storage.get("secret_access_key", "")),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ENCRYPT": storage.get("encrypt", "no"),
    "LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PUBLIC_KEY_PATH": storage.get("public_key_path", ""),
}
for key, value in exports.items():
    if any(character in str(value) for character in ("\x00", "\r", "\n")):
        raise SystemExit(f"Invalid control character in {key}")
    print(f"{key}={shlex.quote(str(value))}")
PY
}

obscure_rclone_password() {
  local secret=${1-}
  if [[ -z "$secret" ]]; then
    printf '\n'
    return 0
  fi
  if command -v rclone >/dev/null 2>&1; then
    # Over stdin, never argv: /proc/<pid>/cmdline is world-readable.
    printf '%s\n' "$secret" | rclone obscure -
  else
    printf '%s\n' "$secret"
  fi
}

configure_selected_storage() {
  local storage_type=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_TYPE:-local}
  local remote_name=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_REMOTE_NAME:-limristem-mail-backup}
  local remote_path=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PATH:-}
  local host=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_HOST:-}
  local port=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PORT:-}
  local user=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_USER:-}
  local password=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PASSWORD:-}
  local bucket=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_BUCKET:-}
  local region=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_REGION:-}
  local endpoint=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ENDPOINT:-}
  local access_key=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ACCESS_KEY_ID:-}
  local secret_key=${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_SECRET_ACCESS_KEY:-}
  local field_value
  for field_value in "$remote_name" "$host" "$port" "$user" "$password" "$bucket" "$region" "$endpoint" "$access_key" "$secret_key"; do
    [[ "$field_value" != *$'\n'* && "$field_value" != *$'\r'* ]] || fail_with_error "Caratteri di controllo nella configurazione storage."
  done
  [[ "$remote_name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$ ]] || fail_with_error "Nome remoto storage non valido."
  mkdir -p "$(dirname "$RCLONE_CONFIG")"

  case "$storage_type" in
    local|'')
      export LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS=
      rm -f "$RCLONE_CONFIG"
      return 0
      ;;
    s3)
      export LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS="${remote_name}:${bucket}"
      if [[ -n "$remote_path" ]]; then
        LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS="${LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS}/$(printf '%s' "$remote_path" | sed 's#^/*##')"
        export LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS
      fi
      local s3_provider="AWS"
      if [[ -n "$endpoint" && "$endpoint" != *"amazonaws.com"* ]]; then
        s3_provider="Other"
      fi
      cat > "$RCLONE_CONFIG" <<EOF
[$remote_name]
type = s3
provider = $s3_provider
env_auth = false
access_key_id = $access_key
secret_access_key = $secret_key
EOF
      [[ -n "$region" ]] && printf 'region = %s\n' "$region" >> "$RCLONE_CONFIG"
      [[ -n "$endpoint" ]] && printf 'endpoint = %s\n' "$endpoint" >> "$RCLONE_CONFIG"
      ;;
    sftp)
      LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS="${remote_name}:$( [[ -n "$remote_path" ]] && printf '%s' "$remote_path" || printf '/' )"
      export LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS
      cat > "$RCLONE_CONFIG" <<EOF
[$remote_name]
type = sftp
host = $host
user = $user
known_hosts_file = /root/.ssh/known_hosts
EOF
      [[ -n "$port" ]] && printf 'port = %s\n' "$port" >> "$RCLONE_CONFIG"
      [[ -n "$password" ]] && printf 'pass = %s\n' "$(obscure_rclone_password "$password")" >> "$RCLONE_CONFIG"
      ;;
    ftp|ftps)
      LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS="${remote_name}:$( [[ -n "$remote_path" ]] && printf '%s' "$remote_path" || printf '/' )"
      export LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS
      cat > "$RCLONE_CONFIG" <<EOF
[$remote_name]
type = ftp
host = $host
user = $user
EOF
      [[ -n "$port" ]] && printf 'port = %s\n' "$port" >> "$RCLONE_CONFIG"
      [[ -n "$password" ]] && printf 'pass = %s\n' "$(obscure_rclone_password "$password")" >> "$RCLONE_CONFIG"
      [[ "$storage_type" == "ftps" ]] && printf 'tls = true\n' >> "$RCLONE_CONFIG"
      ;;
  esac
  chmod 600 "$RCLONE_CONFIG" 2>/dev/null || true
}

bool_is_true() {
  case "${1:-}" in
    1|true|TRUE|yes|YES|on|ON) return 0 ;;
    *) return 1 ;;
  esac
}

choose_backup_type() {
  local state_dir=${1:-/var/lib/limristem-mail/backup-state}
  local backup_local_dir=${2:-/var/backups/limristem-mail}
  case "${LIMRISTEM_MAIL_BACKUP_TYPE:-auto}" in
    full|incremental) CHOSEN_BACKUP_TYPE=${LIMRISTEM_MAIL_BACKUP_TYPE} ;;
    auto)
      # Se non esiste alcuno snapshot snar, oppure se nella cartella di destinazione
      # non è presente alcun backup FULL precedente, forziamo il backup iniziale a FULL.
      local has_full=no
      if [[ -d "$backup_local_dir" ]]; then
        if compgen -G "${backup_local_dir}/*-full" >/dev/null; then
          has_full=yes
        fi
      fi
      if [[ ! -f "${state_dir}/filesystem.snar" || "$has_full" == 'no' ]]; then
        CHOSEN_BACKUP_TYPE=full
      elif [[ "$(date +%a)" == "${LIMRISTEM_MAIL_BACKUP_FULL_WEEKDAY:-Sun}" ]]; then
        CHOSEN_BACKUP_TYPE=full
      else
        CHOSEN_BACKUP_TYPE=incremental
      fi
      ;;
    *)
      fail_with_error "LIMRISTEM_MAIL_BACKUP_TYPE non valido: ${LIMRISTEM_MAIL_BACKUP_TYPE:-}"
      ;;
  esac
  if [[ "$CHOSEN_BACKUP_TYPE" == incremental && ! -f "${state_dir}/filesystem.snar" ]]; then
    CHOSEN_BACKUP_TYPE=full
  fi
}

compression_extension() {
  case "${LIMRISTEM_MAIL_BACKUP_COMPRESSION:-gzip}" in
    gzip) COMPRESSION_EXTENSION=.gz ;;
    none) COMPRESSION_EXTENSION= ;;
    *)
      fail_with_error "LIMRISTEM_MAIL_BACKUP_COMPRESSION non valido: ${LIMRISTEM_MAIL_BACKUP_COMPRESSION:-}"
      ;;
  esac
}

tar_create() {
  local archive=$1
  shift
  case "${LIMRISTEM_MAIL_BACKUP_COMPRESSION:-gzip}" in
    gzip) tar --create --gzip --file="$archive" "$@" ;;
    none) tar --create --file="$archive" "$@" ;;
  esac
}

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

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

perform_logical_backup() {
  local target_file=$1
  local dump_cmd
  dump_cmd=$(mysql_dump_cmd)
  log "Eseguo dump logico MariaDB"
  MYSQL_PWD="${LIMRISTEM_MAIL_DB_PASS}" "$dump_cmd" \
    --single-transaction \
    --routines \
    --triggers \
    --skip-events \
    -h "${LIMRISTEM_MAIL_DB_HOST}" \
    -P "${LIMRISTEM_MAIL_DB_PORT}" \
    -u"${LIMRISTEM_MAIL_DB_USER}" \
    "${LIMRISTEM_MAIL_DB_NAME}" > "$target_file"
}

perform_physical_backup() {
  local mode=$1
  local target_dir=$2
  local state_dir=$3
  local basedir=''
  local args=(--backup "--target-dir=${target_dir}" --user=root)

  if [[ "$mode" == 'physical-incremental' ]]; then
    if [[ -f "${state_dir}/last_physical_dir" ]]; then
      basedir=$(<"${state_dir}/last_physical_dir")
    fi
    if [[ -z "$basedir" || ! -d "$basedir" ]]; then
      log "Nessun physical backup precedente disponibile: eseguo full."
      mode='physical-full'
    else
      args+=("--incremental-basedir=${basedir}")
    fi
  fi

  log "Eseguo backup MariaDB ${mode}"
  MYSQL_PWD="${LIMRISTEM_MAIL_BACKUP_DB_ROOT_PASSWORD:-}" mariabackup "${args[@]}"
  printf '%s\n' "$target_dir" > "${state_dir}/last_physical_dir"
}

perform_redis_backup() {
  local target_file=$1
  if ! bool_is_true "${LIMRISTEM_MAIL_BACKUP_INCLUDE_REDIS:-yes}"; then
    return 0
  fi
  if ! command -v redis-cli >/dev/null 2>&1; then
    log "redis-cli non disponibile: salto backup Redis."
    return 0
  fi
  log "Eseguo snapshot Redis"
  REDISCLI_AUTH="${LIMRISTEM_MAIL_REDIS_PASSWORD:-}" \
    redis-cli -h "${LIMRISTEM_MAIL_REDIS_HOST}" -p "${LIMRISTEM_MAIL_REDIS_PORT}" --rdb "$target_file" >/dev/null
}

write_manifest() {
  local backup_dir=$1
  (
    cd "$backup_dir"
    # The shell creates the redirect target before find runs, so a plain
    #   find . -type f ... > SHA256SUMS
    # hashes SHA256SUMS while it is still being written and records the digest of
    # an empty file. "sha256sum -c" then reported a failure on every verification,
    # which is indistinguishable from a genuinely corrupt backup. Build the list in
    # a staging file that find skips, and exclude the manifest from its own digests.
    find . -type f ! -name 'SHA256SUMS*' -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS.partial
    mv SHA256SUMS.partial SHA256SUMS
  )
}

# Ed25519 keypair used to sign each backup manifest.
#
# The private half never leaves the server (0600 root, in the managed config dir);
# the public half is what the operator escrows, and restore.sh verifies against it.
# That asymmetry is the point: SHA256SUMS alone lives in the same directory as the
# archive, so anyone who can rewrite a backup can rewrite the manifest too. A
# signature they cannot forge is what makes the manifest mean something.
backup_signing_key_path() {
  printf '%s/backup-signing.key' "$(limristem_mail_resolve_managed_config_dir)"
}

backup_signing_pub_path() {
  printf '%s/backup-signing.pub' "$(limristem_mail_resolve_managed_config_dir)"
}

ensure_backup_signing_key() {
  local key pub
  key=$(backup_signing_key_path)
  pub=$(backup_signing_pub_path)
  if [[ -s "$key" && -s "$pub" ]]; then
    return 0
  fi
  command -v openssl >/dev/null 2>&1 || return 1
  limristem_mail_prepare_managed_dir "$(dirname "$key")"
  if [[ ! -s "$key" ]]; then
    local tmp_key
    tmp_key=$(mktemp)
    if ! openssl genpkey -algorithm ed25519 -out "$tmp_key" >/dev/null 2>&1; then
      rm -f "$tmp_key"
      return 1
    fi
    install -m 0600 "$tmp_key" "$key"
    rm -f "$tmp_key"
  fi
  # 0644: this half is public by design and the operator has to be able to read it.
  openssl pkey -in "$key" -pubout -out "$pub" >/dev/null 2>&1 || return 1
  chmod 0644 "$pub" 2>/dev/null || true
  log "Generata chiave di firma dei backup: $pub"
  return 0
}

sign_manifest() {
  local backup_dir=$1
  local key
  key=$(backup_signing_key_path)
  if ! ensure_backup_signing_key || [[ ! -s "$key" ]]; then
    fail_with_error "Chiave di firma non disponibile: backup non pubblicato."
  fi
  if openssl pkeyutl -sign -inkey "$key" -rawin \
      -in "${backup_dir}/SHA256SUMS" -out "${backup_dir}/SHA256SUMS.sig" 2>/dev/null; then
    chmod 0644 "${backup_dir}/SHA256SUMS.sig" 2>/dev/null || true
  else
    rm -f "${backup_dir}/SHA256SUMS.sig"
    fail_with_error "Firma del manifest non riuscita: backup non pubblicato."
  fi
}

copy_to_remote_targets() {
  local backup_dir=$1
  local payload_dir=$backup_dir
  local backup_name
  backup_name=$(basename "$backup_dir")

  if [[ -z "${LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS:-}" ]]; then
    return 0
  fi
  if ! command -v rclone >/dev/null 2>&1; then
    fail_with_error "rclone non disponibile ma LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS è valorizzato."
  fi

  if [[ "${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_ENCRYPT:-no}" == "yes" ]]; then
    [[ -s "${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PUBLIC_KEY_PATH:-}" ]] || \
      fail_with_error "Cifratura richiesta ma chiave pubblica assente: backup non pubblicato."
    local bundle_file export_dir key_material
    export_dir="${backup_dir}/.encrypted-export"
    bundle_file="${export_dir}/${backup_name}.tar.gz"
    mkdir -p "$export_dir"
    tar --create --gzip --preserve-permissions --exclude="$backup_name/.encrypted-export" \
      --file="$bundle_file" -C "$(dirname "$backup_dir")" "$backup_name"
    key_material=$(openssl rand -hex 32)
    openssl enc -aes-256-cbc -pbkdf2 -salt -in "$bundle_file" -out "${bundle_file}.enc" -pass file:<(printf '%s' "$key_material")
    # Encrypt-then-MAC. AES-CBC on its own is malleable: it hides the plaintext but
    # proves nothing about it, so whoever can rewrite the ciphertext on the remote
    # store can induce controlled changes in what you decrypt. The MAC is taken over
    # the ciphertext with a key derived separately from the same secret, so a
    # tampered bundle is detected *before* anyone decrypts it.
    openssl dgst -sha256 -mac HMAC \
      -macopt "hexkey:$(printf 'mac|%s' "$key_material" | openssl dgst -sha256 -r | cut -d' ' -f1)" \
      -out "${bundle_file}.enc.hmac" "${bundle_file}.enc"
    # OAEP rather than the PKCS#1 v1.5 default: v1.5 is the padding Bleichenbacher
    # attacks target, and there is no reason to ship it in new code.
    openssl pkeyutl -encrypt -pubin -inkey "${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PUBLIC_KEY_PATH}" \
      -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \
      -in <(printf '%s' "$key_material") -out "${bundle_file}.key.enc"
    # How to reverse this, so the bundle is recoverable without reading the source.
    cat > "${export_dir}/README.decrypt.txt" <<'DECRYPT'
Per decifrare e verificare questo bundle servono la chiave privata RSA scaricata
al momento della configurazione dello storage e i tre file qui presenti.

  1) Recupera il materiale di chiave:
     openssl pkeyutl -decrypt -inkey <privata.pem> \
       -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \
       -in <bundle>.tar.gz.key.enc -out keymat.txt

  2) VERIFICA IL MAC PRIMA DI DECIFRARE (una modifica al ciphertext e' rilevabile
     solo qui: AES-CBC da solo non autentica nulla):
     MACKEY=$(printf 'mac|%s' "$(cat keymat.txt)" | openssl dgst -sha256 -r | cut -d' ' -f1)
     openssl dgst -sha256 -mac HMAC -macopt "hexkey:$MACKEY" <bundle>.tar.gz.enc
     Confronta l'output con <bundle>.tar.gz.enc.hmac. Se differisce, FERMATI.

  3) Solo se il MAC coincide:
     openssl enc -d -aes-256-cbc -pbkdf2 -in <bundle>.tar.gz.enc \
       -out <bundle>.tar.gz -pass file:keymat.txt

  4) Dentro l'archivio, SHA256SUMS.sig autentica il contenuto rispetto a questa
     installazione: verificalo con la chiave pubblica di firma dei backup.
DECRYPT
    rm -f "$bundle_file"
    payload_dir=$export_dir
  fi

  for target in ${LIMRISTEM_MAIL_BACKUP_REMOTE_TARGETS}; do
    log "Copio backup verso ${target}"
    rclone copy "$payload_dir" "${target%/}/${backup_name}"
  done
}

apply_retention() {
  local local_dir=$1
  local retention_days=${LIMRISTEM_MAIL_BACKUP_RETENTION_DAYS:-14}
  log "Applico retention locale > ${retention_days} giorni"
  python3 "$SCRIPT_DIR/backup-safety.py" retention "$local_dir" "$retention_days"
}

main() {
  require_root
  load_env
  # Scheduled and manual jobs share the rclone profile and must not race snapshots.
  local lock_dir=/var/lib/limristem-mail/backup-state
  mkdir -p "$lock_dir"
  exec 9>"$lock_dir/backup.lock"
  flock -n 9 || fail_with_error "Un altro backup è già in esecuzione."
  local requested_schedule_id=
  local cli_backup_type=
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --schedule-id)
        requested_schedule_id=${2:-}
        shift 2
        ;;
      --type)
        case "${2:-}" in
          full|incremental|auto) cli_backup_type=$2 ;;
          *) fail_with_error "Opzione --type non valida: ${2:-} (usa full, incremental o auto)" ;;
        esac
        shift 2
        ;;
      *)
        shift
        ;;
    esac
  done
  local rclone_config
  rclone_config=$(limristem_mail_resolve_rclone_config_file)
  export RCLONE_CONFIG="$rclone_config"
  if [[ -n "$requested_schedule_id" || -f "$SCHEDULES_FILE" ]]; then
    local schedule_overrides
    schedule_overrides=$(load_schedule_overrides "$requested_schedule_id")
    eval "$schedule_overrides"
    configure_selected_storage
  fi
  if [[ -n "$cli_backup_type" ]]; then
    LIMRISTEM_MAIL_BACKUP_TYPE=$cli_backup_type
  fi

  local backup_type db_mode timestamp backup_root state_dir extension fs_archive db_dump redis_file meta_file
  local backup_local_dir=${LIMRISTEM_MAIL_BACKUP_LOCAL_DIR:-/var/backups/limristem-mail}
  local snapshot_id=${LIMRISTEM_MAIL_BACKUP_SCHEDULE_ID:-manual}
  [[ "$snapshot_id" =~ ^[a-z0-9][a-z0-9_.-]{0,63}$ ]] || fail_with_error "ID pianificazione non valido."
  state_dir="$lock_dir/$snapshot_id"
  if [[ "${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_TYPE:-}" == local && -n "${LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PATH:-}" ]]; then
    backup_local_dir=$LIMRISTEM_MAIL_BACKUP_SELECTED_STORAGE_PATH
  fi
  python3 "$SCRIPT_DIR/backup-safety.py" local-path "$backup_local_dir"
  mkdir -p "$backup_local_dir" "$state_dir"
  choose_backup_type "$state_dir" "$backup_local_dir"
  backup_type=$CHOSEN_BACKUP_TYPE
  db_mode=${LIMRISTEM_MAIL_BACKUP_DB_MODE:-logical}
  timestamp=$(date -u +%Y%m%dT%H%M%SZ)
  backup_root="${backup_local_dir}/${timestamp}-${backup_type}"
  compression_extension
  extension=$COMPRESSION_EXTENSION

  require_writable_directory "$backup_local_dir" "Backup destination"
  require_writable_directory "$state_dir" "Backup state directory"

  mkdir "$backup_root"

  meta_file="${backup_root}/metadata.env"
  {
    printf 'LIMRISTEM_MAIL_BACKUP_TIMESTAMP=%s\n' "$timestamp"
    printf 'LIMRISTEM_MAIL_BACKUP_TYPE=%s\n' "$backup_type"
    printf 'LIMRISTEM_MAIL_BACKUP_DB_MODE=%s\n' "$db_mode"
    printf 'LIMRISTEM_MAIL_BACKUP_HOSTNAME=%s\n' "${LIMRISTEM_MAIL_HOSTNAME:-unknown}"
    printf 'LIMRISTEM_MAIL_BACKUP_SCHEDULE_ID=%s\n' "${LIMRISTEM_MAIL_BACKUP_SCHEDULE_ID:-manual}"
  } > "$meta_file"

  fs_archive="${backup_root}/filesystem.${backup_type}.tar${extension}"
  db_dump="${backup_root}/mariadb.logical.sql"
  redis_file="${backup_root}/redis.rdb"

  local -a backup_paths=()
  local -a candidate_paths=(
    "${LIMRISTEM_MAIL_MAIL_HOME:-/var/mail/vhosts}"
    "${LIMRISTEM_MAIL_DKIM_KEYS_DIR:-/var/lib/limristem-mail/dkim}"
    "${LIMRISTEM_MAIL_BASE_DIR:-}"
    /etc/postfix
    /etc/dovecot
    /etc/rspamd
    /etc/nginx
    /etc/default/postsrsd
    /etc/limristem-mail.env
    /etc/limristem-mail-backup.env
    /etc/limristem-mail-rclone.conf
    /etc/limristem-mail.d
  )
  local candidate
  for candidate in "${candidate_paths[@]}"; do
    if [[ -n "$candidate" && -e "$candidate" ]]; then
      backup_paths+=("${candidate#/}")
    fi
  done

  local pending_snapshot="${state_dir}/filesystem.pending.snar"
  rm -f "$pending_snapshot"
  if [[ "$backup_type" != 'full' && -f "${state_dir}/filesystem.snar" ]]; then
    cp "${state_dir}/filesystem.snar" "$pending_snapshot"
  fi

  log "Archivio filesystem (${backup_type})"
  tar_create "$fs_archive" \
    --directory=/ \
    --listed-incremental="$pending_snapshot" \
    --exclude='backup-signing.key' \
    --exclude='backup-decryption.key' \
    --exclude='backup-storage-private' \
    --exclude="${backup_local_dir#/}" \
    --numeric-owner \
    "${backup_paths[@]}"

  case "$db_mode" in
    logical)
      perform_logical_backup "$db_dump"
      if [[ "${LIMRISTEM_MAIL_BACKUP_COMPRESSION:-gzip}" == 'gzip' ]]; then
        gzip -f "$db_dump"
      fi
      ;;
    physical-full|physical-incremental)
      if ! command -v mariabackup >/dev/null 2>&1; then
        fail_with_error "mariabackup non disponibile per ${db_mode}"
      fi
      perform_physical_backup "$db_mode" "${backup_root}/mariadb.physical" "$state_dir"
      ;;
    none)
      log "MariaDB backup disabled for this schedule."
      ;;
    *)
      fail_with_error "LIMRISTEM_MAIL_BACKUP_DB_MODE non valido: ${db_mode}"
      ;;
  esac

  perform_redis_backup "$redis_file"
  write_manifest "$backup_root"
  # Sign AFTER the manifest exists and BEFORE anything leaves the host.
  sign_manifest "$backup_root"
  copy_to_remote_targets "$backup_root"
  mv "$pending_snapshot" "${state_dir}/filesystem.snar"
  apply_retention "$backup_local_dir"

  log "Backup completato: ${backup_root}"
}

main "$@"
