#!/bin/bash
# alpha-backup — snapshots of the whole stack OR a chosen target. Root-owned, NOPASSWD.
# Snapshots land in /var/backups/alpha (640 root:www-data so the panel can list/download).
#   create [version]                full snapshot: panel + sites + DBs + mail + config
#   prune <days>                    delete snapshots older than <days> (+ their .meta)
#   delete <file>                   remove one snapshot (validated name, + its .meta)
#   snap file <abs-path> [label]    snapshot a file/folder under /var/www
#   snap site <sub> [label]         snapshot a site docroot (+ its WordPress DB)
#   snap db <mysql|pg> <db> [label] snapshot one database
#   snap vm <name> [label]          snapshot a QEMU VM (disk image + config)
#   snap lxd <name> [label]         snapshot an LXD instance (lxc export)
#   snap vscode [label]             snapshot the VS Code Web editor: settings/keybindings
#                                   (main + instances), extension list, service + invite config
#   restore <file>                  restore a targeted snapshot to its recorded target
#                                   (a "pre-restore" snapshot of the current state is made first)
set -euo pipefail
DIR=/var/backups/alpha
install -d -m 750 -o root -g www-data "$DIR"

sanitize_label(){ echo "${1:-}" | tr -cd 'A-Za-z0-9 ._-' | cut -c1-60; }
meta_json(){ printf '{"scope":"%s","target":"%s","label":"%s","created":%s,"engine":"%s"}' "$1" "$2" "$3" "$(date +%s)" "${4:-}"; }

# pack_snap workdir scope target label [engine]  — payload already inside workdir
pack_snap(){
  local work="$1" scope="$2" target="$3" label="$4" engine="${5:-}"
  local ts out base; ts=$(date +%Y%m%d-%H%M%S)
  out="$DIR/alpha-snap-$ts-$scope.tar.gz"; base="${out%.tar.gz}"
  meta_json "$scope" "$target" "$label" "$engine" > "$work/SNAPMETA"
  tar czf "$out" -C "$work" . 2>/dev/null
  cp "$work/SNAPMETA" "$base.meta"
  chown root:www-data "$out" "$base.meta"; chmod 640 "$out" "$base.meta"
  rm -rf "$work"
  echo "OK: $(basename "$out") $(du -h "$out" | cut -f1)"
}

case "${1:-}" in
  create)
    ver="${2:-}"; sfx=""
    if [[ "$ver" =~ ^v[0-9A-Za-z._-]+$ ]]; then sfx="-$ver"; fi
    ts=$(date +%Y%m%d-%H%M%S)
    work=$(mktemp -d)
    tar czf "$work/alpha-panel.tar.gz" --exclude="alpha/phpmyadmin" --exclude="alpha/.git" -C /var/www alpha 2>/dev/null || true
    [ -d /var/www/sites ] && tar czf "$work/sites.tar.gz" -C /var/www sites 2>/dev/null || true
    if command -v mysqldump >/dev/null && systemctl is-active --quiet mariadb 2>/dev/null; then
      mysqldump --all-databases --single-transaction --quick --events 2>/dev/null | gzip > "$work/mariadb.sql.gz" || true
    fi
    if command -v pg_dumpall >/dev/null && systemctl is-active --quiet postgresql 2>/dev/null; then
      sudo -u postgres pg_dumpall 2>/dev/null | gzip > "$work/postgres.sql.gz" || true
    fi
    [ -d /var/mail/vhosts ] && tar czf "$work/mail.tar.gz" -C /var/mail vhosts 2>/dev/null || true
    tar czf "$work/config.tar.gz" -C / etc/nginx etc/letsencrypt var/www/alpha-private 2>/dev/null || true
    if [ -n "$sfx" ]; then echo "$ver" > "$work/VERSION"; fi
    out="$DIR/alpha-backup-$ts$sfx.tar.gz"
    tar czf "$out" -C "$work" . 2>/dev/null
    rm -rf "$work"
    chown root:www-data "$out"; chmod 640 "$out"
    echo "OK: $(basename "$out") $(du -h "$out" | cut -f1)"
    ;;

  snap)
    kind="${2:-}"
    case "$kind" in
      file)
        p="${3:-}"; label="$(sanitize_label "${4:-}")"
        case "$p" in /var/www/*) ;; *) echo "BAD_PATH"; exit 2 ;; esac
        case "$p" in *..*) echo "BAD_PATH"; exit 2 ;; esac
        real=$(realpath -m "$p")
        case "$real" in /var/www/*) ;; *) echo "BAD_PATH"; exit 2 ;; esac
        case "$real" in /var/www/alpha-private*) echo "PROTECTED"; exit 2 ;; esac
        [ -e "$real" ] || { echo "NO_TARGET"; exit 2; }
        rel="${real#/var/www/}"
        work=$(mktemp -d)
        tar czf "$work/data.tar.gz" -C /var/www "$rel" 2>/dev/null
        pack_snap "$work" file "$rel" "$label"
        ;;
      site)
        s="${3:-}"; label="$(sanitize_label "${4:-}")"
        [[ "$s" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || { echo "BAD_SITE"; exit 2; }
        d="/var/www/sites/$s"; [ -d "$d" ] || { echo "NO_TARGET"; exit 2; }
        work=$(mktemp -d)
        tar czf "$work/data.tar.gz" -C /var/www/sites "$s" 2>/dev/null
        if [ -f "$d/wp-config.php" ] && command -v mysqldump >/dev/null; then
          dbn=$(grep -oP "define\(\s*'DB_NAME'\s*,\s*'\K[^']+" "$d/wp-config.php" 2>/dev/null | head -1 || true)
          dbu=$(grep -oP "define\(\s*'DB_USER'\s*,\s*'\K[^']+" "$d/wp-config.php" 2>/dev/null | head -1 || true)
          dbp=$(grep -oP "define\(\s*'DB_PASSWORD'\s*,\s*'\K[^']*" "$d/wp-config.php" 2>/dev/null | head -1 || true)
          if [ -n "${dbn:-}" ]; then mysqldump --single-transaction --quick -u"${dbu:-root}" -p"${dbp:-}" "$dbn" 2>/dev/null | gzip > "$work/db.sql.gz" || true; fi
        fi
        pack_snap "$work" site "$s" "$label"
        ;;
      db)
        eng="${3:-}"; db="${4:-}"; label="$(sanitize_label "${5:-}")"
        [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || { echo "BAD_DB"; exit 2; }
        work=$(mktemp -d)
        case "$eng" in
          mysql) command -v mysqldump >/dev/null && mysqldump --single-transaction --quick --events "$db" 2>/dev/null | gzip > "$work/db.sql.gz" || { echo "DUMP_FAIL"; rm -rf "$work"; exit 2; } ;;
          pg)    command -v pg_dump   >/dev/null && sudo -u postgres pg_dump "$db" 2>/dev/null | gzip > "$work/db.sql.gz" || { echo "DUMP_FAIL"; rm -rf "$work"; exit 2; } ;;
          *)     echo "BAD_ENGINE"; rm -rf "$work"; exit 2 ;;
        esac
        [ -s "$work/db.sql.gz" ] || { echo "DUMP_FAIL"; rm -rf "$work"; exit 2; }
        pack_snap "$work" db "$eng/$db" "$label" "$eng"
        ;;
      vm)
        n="${3:-}"; label="$(sanitize_label "${4:-}")"
        [[ "$n" =~ ^[a-z0-9][a-z0-9-]{0,30}$ ]] || { echo "BAD_VM"; exit 2; }
        qd="/var/lib/alpha-qemu"
        [ -f "$qd/$n.qcow2" ] && [ -f "$qd/$n.json" ] || { echo "NO_TARGET"; exit 2; }
        work=$(mktemp -d)
        cp --reflink=auto "$qd/$n.qcow2" "$work/disk.qcow2" 2>/dev/null || cp "$qd/$n.qcow2" "$work/disk.qcow2"
        cp "$qd/$n.json" "$work/vm.json"
        pack_snap "$work" vm "$n" "$label" qemu
        ;;
      lxd)
        n="${3:-}"; label="$(sanitize_label "${4:-}")"
        [[ "$n" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] || { echo "BAD_LXD"; exit 2; }
        /snap/bin/lxc info "$n" >/dev/null 2>&1 || { echo "NO_TARGET"; exit 2; }
        work=$(mktemp -d)
        /snap/bin/lxc export "$n" "$work/instance.tar.gz" --instance-only >/dev/null 2>&1 || { echo "EXPORT_FAIL"; rm -rf "$work"; exit 2; }
        pack_snap "$work" lxd "$n" "$label" lxd
        ;;
      vscode)
        label="$(sanitize_label "${3:-}")"
        VB="/var/www/Addons/VS Code Web"
        [ -d "$VB/UserData" ] || { echo "NO_TARGET"; exit 2; }
        work=$(mktemp -d)
        # the small, precious part of UserData: settings, keybindings, snippets
        tar czf "$work/userdata.tar.gz" -C "$VB/UserData" User 2>/dev/null || true
        # per-instance user settings
        iu=$(cd "$VB" && ls -d Instances/*/UserData/User 2>/dev/null || true)
        [ -n "$iu" ] && tar czf "$work/instances-user.tar.gz" -C "$VB" $iu 2>/dev/null || true
        # exact extension set (folder names carry publisher.name-version)
        ls "$VB/Extensions" 2>/dev/null | grep -v '^\.' > "$work/extensions.txt" || true
        # editor service + instance + invite config from the credential store
        cf=$(cd /var/www/alpha-private && ls code-server.yaml vscode-host.conf vscode-invites.json 2>/dev/null || true)
        cd_dir=""; [ -d /var/www/alpha-private/code-server-inst ] && cd_dir="code-server-inst"
        [ -n "$cf$cd_dir" ] && tar czf "$work/config.tar.gz" -C /var/www/alpha-private $cf $cd_dir 2>/dev/null || true
        pack_snap "$work" vscode editor "$label"
        ;;
      *) echo "BAD_SNAP"; exit 2 ;;
    esac
    ;;

  restore)
    f="${2:-}"
    [[ "$f" =~ ^alpha-snap-[0-9]{8}-[0-9]{6}-(file|site|db|vm|lxd|vscode)\.tar\.gz$ ]] || { echo "BAD_FILE"; exit 2; }
    src="$DIR/$f"; [ -f "$src" ] || { echo "NO_FILE"; exit 2; }
    tmp=$(mktemp -d)
    tar xzf "$src" -C "$tmp" 2>/dev/null
    scope=$(grep -oP '"scope":"\K[^"]+' "$tmp/SNAPMETA" 2>/dev/null || true)
    target=$(grep -oP '"target":"\K[^"]+' "$tmp/SNAPMETA" 2>/dev/null || true)
    case "$scope" in
      file)
        case "$target" in *..*) echo "BAD_TARGET"; rm -rf "$tmp"; exit 2 ;; esac
        [ -f "$tmp/data.tar.gz" ] || { echo "NO_DATA"; rm -rf "$tmp"; exit 2; }
        full="/var/www/$target"
        [ -e "$full" ] && "$0" snap file "$full" "pre-restore" >/dev/null 2>&1 || true
        tar xzf "$tmp/data.tar.gz" -C /var/www 2>/dev/null
        rm -rf "$tmp"; echo "OK: restored /var/www/$target"
        ;;
      site)
        [[ "$target" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$ ]] || { echo "BAD_TARGET"; rm -rf "$tmp"; exit 2; }
        [ -f "$tmp/data.tar.gz" ] || { echo "NO_DATA"; rm -rf "$tmp"; exit 2; }
        full="/var/www/sites/$target"
        [ -d "$full" ] && "$0" snap site "$target" "pre-restore" >/dev/null 2>&1 || true
        tar xzf "$tmp/data.tar.gz" -C /var/www/sites 2>/dev/null
        chown -R www-data:www-data "$full" 2>/dev/null || true
        rm -rf "$tmp"; echo "OK: restored site $target (docroot only; DB not auto-restored)"
        ;;
      db)
        eng="${target%%/*}"; db="${target#*/}"
        [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || { echo "BAD_TARGET"; rm -rf "$tmp"; exit 2; }
        [ -f "$tmp/db.sql.gz" ] || { echo "NO_DUMP"; rm -rf "$tmp"; exit 2; }
        case "$eng" in
          mysql) gunzip -c "$tmp/db.sql.gz" | mysql "$db" 2>/dev/null && { rm -rf "$tmp"; echo "OK: restored MySQL db $db"; } || { echo "IMPORT_FAIL"; rm -rf "$tmp"; exit 2; } ;;
          pg)    gunzip -c "$tmp/db.sql.gz" | sudo -u postgres psql -q "$db" 2>/dev/null && { rm -rf "$tmp"; echo "OK: restored Postgres db $db"; } || { echo "IMPORT_FAIL"; rm -rf "$tmp"; exit 2; } ;;
          *)     echo "BAD_ENGINE"; rm -rf "$tmp"; exit 2 ;;
        esac
        ;;
      vm)
        [[ "$target" =~ ^[a-z0-9][a-z0-9-]{0,30}$ ]] || { echo "BAD_TARGET"; rm -rf "$tmp"; exit 2; }
        [ -f "$tmp/disk.qcow2" ] && [ -f "$tmp/vm.json" ] || { echo "NO_DATA"; rm -rf "$tmp"; exit 2; }
        qd="/var/lib/alpha-qemu"
        if [ -f "$qd/$target.pid" ] && kill -0 "$(cat "$qd/$target.pid")" 2>/dev/null; then echo "VM_RUNNING"; rm -rf "$tmp"; exit 2; fi
        [ -f "$qd/$target.qcow2" ] && "$0" snap vm "$target" "pre-restore" >/dev/null 2>&1 || true
        cp "$tmp/disk.qcow2" "$qd/$target.qcow2"; cp "$tmp/vm.json" "$qd/$target.json"
        chown root:root "$qd/$target.qcow2" "$qd/$target.json" 2>/dev/null || true
        rm -rf "$tmp"; echo "OK: restored VM $target (start it from Virtualization)"
        ;;
      lxd)
        [[ "$target" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] || { echo "BAD_TARGET"; rm -rf "$tmp"; exit 2; }
        [ -f "$tmp/instance.tar.gz" ] || { echo "NO_DATA"; rm -rf "$tmp"; exit 2; }
        if /snap/bin/lxc info "$target" >/dev/null 2>&1; then
          "$0" snap lxd "$target" "pre-restore" >/dev/null 2>&1 || true
          /snap/bin/lxc delete --force "$target" >/dev/null 2>&1 || { echo "IMPORT_FAIL"; rm -rf "$tmp"; exit 2; }
        fi
        /snap/bin/lxc import "$tmp/instance.tar.gz" >/dev/null 2>&1 || { echo "IMPORT_FAIL"; rm -rf "$tmp"; exit 2; }
        rm -rf "$tmp"; echo "OK: restored instance $target (start it from Virtualization)"
        ;;
      vscode)
        VB="/var/www/Addons/VS Code Web"
        { [ -f "$tmp/userdata.tar.gz" ] || [ -f "$tmp/config.tar.gz" ]; } || { echo "NO_DATA"; rm -rf "$tmp"; exit 2; }
        "$0" snap vscode "pre-restore" >/dev/null 2>&1 || true
        [ -f "$tmp/userdata.tar.gz" ] && tar xzf "$tmp/userdata.tar.gz" -C "$VB/UserData" 2>/dev/null || true
        [ -f "$tmp/instances-user.tar.gz" ] && tar xzf "$tmp/instances-user.tar.gz" -C "$VB" 2>/dev/null || true
        [ -f "$tmp/config.tar.gz" ] && tar xzf "$tmp/config.tar.gz" -C /var/www/alpha-private 2>/dev/null || true
        chown -R www-data:www-data "$VB" 2>/dev/null || true
        chown www-data:www-data /var/www/alpha-private/code-server.yaml /var/www/alpha-private/vscode-host.conf /var/www/alpha-private/vscode-invites.json 2>/dev/null || true
        [ -d /var/www/alpha-private/code-server-inst ] && chown -R www-data:www-data /var/www/alpha-private/code-server-inst 2>/dev/null || true
        systemctl try-restart code-server-alpha >/dev/null 2>&1 || true
        for u in /etc/systemd/system/code-server-alpha-*.service; do [ -f "$u" ] && systemctl try-restart "$(basename "$u" .service)" >/dev/null 2>&1; done
        rm -rf "$tmp"; echo "OK: restored editor settings & config — editors restarted (extension list kept as extensions.txt inside the snapshot)"
        ;;
      *) echo "BAD_META"; rm -rf "$tmp"; exit 2 ;;
    esac
    ;;

  prune)
    days="${2:-14}"; [[ "$days" =~ ^[0-9]+$ ]] || { echo "BAD_DAYS"; exit 2; }
    find "$DIR" -maxdepth 1 \( -name "alpha-backup-*.tar.gz" -o -name "alpha-snap-*.tar.gz" -o -name "alpha-snap-*.meta" \) -mtime +"$days" -delete 2>/dev/null || true
    echo "OK"
    ;;

  delete)
    f="${2:-}"
    [[ "$f" =~ ^(alpha-backup-[0-9]{8}-[0-9]{6}(-v[0-9A-Za-z._-]+)?|alpha-snap-[0-9]{8}-[0-9]{6}-(file|site|db|vm|lxd|vscode))\.tar\.gz$ ]] || { echo "BAD_FILE"; exit 2; }
    rm -f "$DIR/$f" "$DIR/${f%.tar.gz}.meta" && echo "OK"
    ;;

  *) echo "BAD_ACTION"; exit 1 ;;
esac
