#!/bin/bash
# ==========================================================
# ProxMenux - Host Config Backup/Restore - Shared Library
# ==========================================================
# Author      : MacRimi
# Copyright   : (c) 2024 MacRimi
# License     : MIT
# Version     : 1.0
# Last Updated: 08/04/2026
# ==========================================================
# Do not execute directly — source from backup_host.sh

# Library guard
[[ "${BASH_SOURCE[0]}" == "$0" ]] && {
    echo "This file is a library. Source it, do not run it directly." >&2; exit 1
}

HB_STATE_DIR="/usr/local/share/proxmenux"
HB_BORG_VERSION="1.2.8"
HB_BORG_LINUX64_SHA256="cfa50fb704a93d3a4fa258120966345fddb394f960dca7c47fcb774d0172f40b"
HB_BORG_LINUX64_URL="https://github.com/borgbackup/borg/releases/download/${HB_BORG_VERSION}/borg-linux64"

# Translation wrapper — safe fallback if translate not yet loaded
hb_translate() {
    declare -f translate >/dev/null 2>&1 && translate "$1" || echo "$1"
}

# ==========================================================
# UI SIZE CONSTANTS
# ==========================================================
HB_UI_MENU_H=22
HB_UI_MENU_W=84
HB_UI_MENU_LIST=10
HB_UI_INPUT_H=10
HB_UI_INPUT_W=72
HB_UI_PASS_H=10
HB_UI_PASS_W=72
HB_UI_YESNO_H=10
HB_UI_YESNO_W=78

# ==========================================================
# DEFAULT PROFILE PATHS
# ==========================================================
hb_default_profile_paths() {
    local paths=(
        "/etc/pve"
        "/etc/network"
        "/etc/hosts"
        "/etc/hostname"
        "/etc/ssh"
        "/etc/systemd/system"
        "/etc/modules"
        "/etc/modules-load.d"
        "/etc/modprobe.d"
        "/etc/udev/rules.d"
        "/etc/default/grub"
        "/etc/fstab"
        "/etc/kernel"
        "/etc/apt"
        "/etc/vzdump.conf"
        "/etc/postfix"
        "/etc/resolv.conf"
        "/etc/timezone"
        "/etc/iscsi"
        "/etc/multipath"
        "/usr/local/bin"
        "/usr/local/share/proxmenux"
        "/root"
        "/etc/cron.d"
        "/etc/cron.daily"
        "/etc/cron.hourly"
        "/etc/cron.weekly"
        "/etc/cron.monthly"
        "/etc/cron.allow"
        "/etc/cron.deny"
        "/var/spool/cron/crontabs"
        "/var/lib/pve-cluster"
    )
    if [[ -d /etc/zfs ]] || command -v zpool >/dev/null 2>&1; then
        paths+=("/etc/zfs")
    fi
    printf '%s\n' "${paths[@]}"
}

# ==========================================================
# PATH CLASSIFICATION  (restore safety)
# Returns: dangerous | reboot | hot
# ==========================================================
hb_classify_path() {
    local rel="$1"   # without leading /
    case "$rel" in
        etc/pve|etc/pve/*|\
        var/lib/pve-cluster|var/lib/pve-cluster/*|\
        etc/network|etc/network/*)
            echo "dangerous" ;;
        etc/modules|etc/modules/*|\
        etc/modules-load.d|etc/modules-load.d/*|\
        etc/modprobe.d|etc/modprobe.d/*|\
        etc/udev/rules.d|etc/udev/rules.d/*|\
        etc/default/grub|\
        etc/fstab|\
        etc/kernel|etc/kernel/*|\
        etc/iscsi|etc/iscsi/*|\
        etc/multipath|etc/multipath/*|\
        etc/zfs|etc/zfs/*)
            echo "reboot" ;;
        *)
            echo "hot" ;;
    esac
}

hb_path_warning() {
    local rel="$1"
    case "$rel" in
        etc/pve|etc/pve/*)
            hb_translate "/etc/pve is managed by pmxcfs (cluster filesystem). Applying this on a running node can corrupt cluster state. Use 'Export to file' and apply it manually during a maintenance window." ;;
        var/lib/pve-cluster|var/lib/pve-cluster/*)
            hb_translate "/var/lib/pve-cluster is live cluster data. Never restore this while the node is running. Use 'Export to file' for manual recovery only." ;;
        etc/network|etc/network/*)
            hb_translate "/etc/network controls active interfaces. Applying may immediately change or drop network connectivity, including active SSH sessions." ;;
    esac
}

# ==========================================================
# PROFILE PATH SELECTION
# ==========================================================
hb_select_profile_paths() {
    local mode="$1"
    local __out_var="$2"
    local -n __out_ref="$__out_var"

    mapfile -t __defaults < <(hb_default_profile_paths)

    if [[ "$mode" == "default" ]]; then
        __out_ref=("${__defaults[@]}")
        return 0
    fi

    local options=() idx=1 path
    for path in "${__defaults[@]}"; do
        options+=("$idx" "$path" "off")
        ((idx++))
    done

    local selected
    selected=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Custom backup profile")" \
        --separate-output --checklist \
        "$(hb_translate "Select paths to include:")" \
        26 86 18 "${options[@]}" 3>&1 1>&2 2>&3) || return 1

    __out_ref=()
    local choice
    while read -r choice; do
        [[ -z "$choice" ]] && continue
        __out_ref+=("${__defaults[$((choice-1))]}")
    done <<< "$selected"

    if [[ ${#__out_ref[@]} -eq 0 ]]; then
        dialog --backtitle "ProxMenux" --title "$(hb_translate "Error")" \
            --msgbox "$(hb_translate "No paths selected. Select at least one path.")" 8 60
        return 1
    fi
}

# ==========================================================
# STAGING OPERATIONS
# ==========================================================
hb_prepare_staging() {
    local staging_root="$1"; shift
    local paths=("$@")

    rm -rf "$staging_root"
    mkdir -p "$staging_root/rootfs" "$staging_root/metadata"

    local selected_file="$staging_root/metadata/selected_paths.txt"
    local missing_file="$staging_root/metadata/missing_paths.txt"
    : > "$selected_file"
    : > "$missing_file"

    local p rel target
    for p in "${paths[@]}"; do
        rel="${p#/}"
        echo "$rel" >> "$selected_file"
        [[ -e "$p" ]] || { echo "$p" >> "$missing_file"; continue; }
        target="$staging_root/rootfs/$rel"
        if [[ -d "$p" ]]; then
            mkdir -p "$target"
            local -a rsync_opts=(
                -aAXH --numeric-ids
                --exclude "images/"
                --exclude "dump/"
                --exclude "tmp/"
                --exclude "*.log"
            )

            # /root is included by default for easier recovery, but avoid volatile/sensitive noise.
            if [[ "$rel" == "root" || "$rel" == "root/"* ]]; then
                rsync_opts+=(
                    --exclude ".bash_history"
                    --exclude ".cache/"
                    --exclude "tmp/"
                    --exclude ".local/share/Trash/"
                )
            fi

            # Runtime pending-restore data belongs in /var/lib/proxmenux, never in app code tree.
            if [[ "$rel" == "usr/local/share/proxmenux" || "$rel" == "usr/local/share/proxmenux/"* ]]; then
                rsync_opts+=(
                    --exclude "restore-pending/"
                )
            fi

            rsync "${rsync_opts[@]}" "$p/" "$target/" 2>/dev/null || true
        else
            mkdir -p "$(dirname "$target")"
            cp -a "$p" "$target" 2>/dev/null || true
        fi
    done

    # Metadata snapshot
    local meta="$staging_root/metadata"
    {
        echo "generated_at=$(date -Iseconds)"
        echo "hostname=$(hostname)"
        echo "kernel=$(uname -r)"
    } > "$meta/run_info.env"
    command -v pveversion >/dev/null 2>&1 && pveversion -v > "$meta/pveversion.txt" 2>&1 || true
    command -v lsblk    >/dev/null 2>&1 && lsblk -f     > "$meta/lsblk.txt"      2>&1 || true
    command -v qm       >/dev/null 2>&1 && qm list       > "$meta/qm-list.txt"    2>&1 || true
    command -v pct      >/dev/null 2>&1 && pct list      > "$meta/pct-list.txt"   2>&1 || true
    command -v zpool    >/dev/null 2>&1 && zpool status  > "$meta/zpool.txt"      2>&1 || true

    # Manifest + checksums
    (
        cd "$staging_root/rootfs" || return 1
        find . -mindepth 1 -print | sort > "$meta/manifest.txt"
        find . -type f -print0 | sort -z | xargs -0 sha256sum 2>/dev/null \
            > "$meta/checksums.sha256" || true
    )
}

hb_load_restore_paths() {
    local restore_root="$1"
    local __out_var="$2"
    local -n __out="$__out_var"

    __out=()
    local selected="$restore_root/metadata/selected_paths.txt"
    if [[ -f "$selected" ]]; then
        while IFS= read -r line; do
            [[ -n "$line" ]] && __out+=("$line")
        done < "$selected"
    fi
    # Fallback: scan rootfs
    if [[ ${#__out[@]} -eq 0 ]]; then
        local p
        while IFS= read -r p; do
            [[ -n "$p" && -e "$restore_root/rootfs/${p#/}" ]] && __out+=("${p#/}")
        done < <(hb_default_profile_paths)
    fi
}

# ==========================================================
# PBS CONFIG — auto-detect from storage.cfg + manual
# ==========================================================
hb_collect_pbs_configs() {
    HB_PBS_NAMES=()
    HB_PBS_REPOS=()
    HB_PBS_SECRETS=()
    HB_PBS_SOURCES=()

    if [[ -f /etc/pve/storage.cfg ]]; then
        local current="" server="" datastore="" username="" pw_file pw_val
        while IFS= read -r line; do
            line="${line%%#*}"
            line="${line#"${line%%[![:space:]]*}"}"
            line="${line%"${line##*[![:space:]]}"}"
            [[ -z "$line" ]] && continue
            if [[ $line =~ ^pbs:[[:space:]]*(.+)$ ]]; then
                if [[ -n "$current" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
                    pw_file="/etc/pve/priv/storage/${current}.pw"
                    pw_val="$([[ -f "$pw_file" ]] && cat "$pw_file" || echo "")"
                    HB_PBS_NAMES+=("$current")
                    HB_PBS_REPOS+=("${username}@${server}:${datastore}")
                    HB_PBS_SECRETS+=("$pw_val")
                    HB_PBS_SOURCES+=("proxmox")
                fi
                current="${BASH_REMATCH[1]}"; server="" datastore="" username=""
            elif [[ -n "$current" ]]; then
                [[ $line =~ ^[[:space:]]+server[[:space:]]+(.+)$    ]] && server="${BASH_REMATCH[1]}"
                [[ $line =~ ^[[:space:]]+datastore[[:space:]]+(.+)$ ]] && datastore="${BASH_REMATCH[1]}"
                [[ $line =~ ^[[:space:]]+username[[:space:]]+(.+)$  ]] && username="${BASH_REMATCH[1]}"
                if [[ $line =~ ^[a-zA-Z]+:[[:space:]] &&
                      -n "$server" && -n "$datastore" && -n "$username" ]]; then
                    pw_file="/etc/pve/priv/storage/${current}.pw"
                    pw_val="$([[ -f "$pw_file" ]] && cat "$pw_file" || echo "")"
                    HB_PBS_NAMES+=("$current")
                    HB_PBS_REPOS+=("${username}@${server}:${datastore}")
                    HB_PBS_SECRETS+=("$pw_val")
                    HB_PBS_SOURCES+=("proxmox")
                    current="" server="" datastore="" username=""
                fi
            fi
        done < /etc/pve/storage.cfg
        # Last stanza
        if [[ -n "$current" && -n "$server" && -n "$datastore" && -n "$username" ]]; then
            pw_file="/etc/pve/priv/storage/${current}.pw"
            pw_val="$([[ -f "$pw_file" ]] && cat "$pw_file" || echo "")"
            HB_PBS_NAMES+=("$current")
            HB_PBS_REPOS+=("${username}@${server}:${datastore}")
            HB_PBS_SECRETS+=("$pw_val")
            HB_PBS_SOURCES+=("proxmox")
        fi
    fi

    # Manual configs
    local manual_cfg="$HB_STATE_DIR/pbs-manual-configs.txt"
    if [[ -f "$manual_cfg" ]]; then
        local line name repo sf
        while IFS= read -r line; do
            line="${line%%#*}"
            line="${line#"${line%%[![:space:]]*}"}"
            line="${line%"${line##*[![:space:]]}"}"
            [[ -z "$line" ]] && continue
            name="${line%%|*}"; repo="${line##*|}"
            sf="$HB_STATE_DIR/pbs-pass-${name}.txt"
            HB_PBS_NAMES+=("$name"); HB_PBS_REPOS+=("$repo")
            HB_PBS_SECRETS+=("$([[ -f "$sf" ]] && cat "$sf" || echo "")")
            HB_PBS_SOURCES+=("manual")
        done < "$manual_cfg"
    fi
}

hb_configure_pbs_manual() {
    local name user host datastore repo secret

    name=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Add PBS")" \
        --inputbox "$(hb_translate "Configuration name:")" \
        "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "PBS-$(date +%m%d)" 3>&1 1>&2 2>&3) || return 1
    [[ -z "$name" ]] && return 1

    user=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Add PBS")" \
        --inputbox "$(hb_translate "Username (e.g. root@pam or user@pbs!token):")" \
        "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "root@pam" 3>&1 1>&2 2>&3) || return 1

    host=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Add PBS")" \
        --inputbox "$(hb_translate "PBS host or IP address:")" \
        "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "" 3>&1 1>&2 2>&3) || return 1
    [[ -z "$host" ]] && return 1

    datastore=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Add PBS")" \
        --inputbox "$(hb_translate "Datastore name:")" \
        "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "" 3>&1 1>&2 2>&3) || return 1
    [[ -z "$datastore" ]] && return 1

    secret=$(dialog --backtitle "ProxMenux" --title "$(hb_translate "Add PBS")" \
        --insecure --passwordbox "$(hb_translate "Password or API token secret:")" \
        "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 1

    repo="${user}@${host}:${datastore}"
    mkdir -p "$HB_STATE_DIR"
    local cfg_line="${name}|${repo}"
    local manual_cfg="$HB_STATE_DIR/pbs-manual-configs.txt"
    touch "$manual_cfg"
    grep -Fxq "$cfg_line" "$manual_cfg" || echo "$cfg_line" >> "$manual_cfg"
    printf '%s' "$secret" > "$HB_STATE_DIR/pbs-pass-${name}.txt"
    chmod 600 "$HB_STATE_DIR/pbs-pass-${name}.txt"

    HB_PBS_NAME="$name"; HB_PBS_REPOSITORY="$repo"; HB_PBS_SECRET="$secret"
}

hb_select_pbs_repository() {
    hb_collect_pbs_configs

    local menu=() i=1 idx
    for idx in "${!HB_PBS_NAMES[@]}"; do
        local src="${HB_PBS_SOURCES[$idx]}"
        local label="${HB_PBS_NAMES[$idx]}  —  ${HB_PBS_REPOS[$idx]}  [$src]"
        [[ -z "${HB_PBS_SECRETS[$idx]}" ]] && label+="  ⚠ $(hb_translate "no password")"
        menu+=("$i" "$label"); ((i++))
    done
    menu+=("$i" "$(hb_translate "+ Add new PBS manually")")

    local choice
    choice=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Select PBS repository")" \
        --menu "\n$(hb_translate "Available PBS repositories:")" \
        "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" "${menu[@]}" 3>&1 1>&2 2>&3) || return 1

    if [[ "$choice" == "$i" ]]; then
        hb_configure_pbs_manual || return 1
    else
        local sel=$((choice-1))
        HB_PBS_NAME="${HB_PBS_NAMES[$sel]}"
        export HB_PBS_REPOSITORY="${HB_PBS_REPOS[$sel]}"
        HB_PBS_SECRET="${HB_PBS_SECRETS[$sel]}"
        if [[ -z "$HB_PBS_SECRET" ]]; then
            HB_PBS_SECRET=$(dialog --backtitle "ProxMenux" --title "PBS" \
                --insecure --passwordbox \
                "$(hb_translate "Password for:") $HB_PBS_NAME" \
                "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 1
            mkdir -p "$HB_STATE_DIR"
            printf '%s' "$HB_PBS_SECRET" > "$HB_STATE_DIR/pbs-pass-${HB_PBS_NAME}.txt"
            chmod 600 "$HB_STATE_DIR/pbs-pass-${HB_PBS_NAME}.txt"
        fi
    fi
}

hb_ask_pbs_encryption() {
    local key_file="$HB_STATE_DIR/pbs-key.conf"
    local enc_pass_file="$HB_STATE_DIR/pbs-encryption-pass.txt"
    export HB_PBS_KEYFILE_OPT=""
    export HB_PBS_ENC_PASS=""

    dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
        --yesno "$(hb_translate "Encrypt this backup with a keyfile?")" \
        "$HB_UI_YESNO_H" "$HB_UI_YESNO_W" || return 0

    if [[ -f "$key_file" ]]; then
        export HB_PBS_KEYFILE_OPT="--keyfile $key_file"
        if [[ -f "$enc_pass_file" ]]; then
            HB_PBS_ENC_PASS="$(<"$enc_pass_file")"
            export HB_PBS_ENC_PASS
        fi
        msg_ok "$(hb_translate "Using existing encryption key:") $key_file"
        return 0
    fi

    # No key — offer to create one
    dialog --backtitle "ProxMenux" --title "$(hb_translate "Encryption")" \
        --yesno "$(hb_translate "No encryption key found. Create one now?")" \
        "$HB_UI_YESNO_H" "$HB_UI_YESNO_W" || return 0

    local pass1 pass2
    while true; do
        pass1=$(dialog --backtitle "ProxMenux" --insecure --passwordbox \
            "$(hb_translate "Encryption passphrase (separate from PBS password):")" \
            "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 0
        pass2=$(dialog --backtitle "ProxMenux" --insecure --passwordbox \
            "$(hb_translate "Confirm encryption passphrase:")" \
            "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 0
        [[ "$pass1" == "$pass2" ]] && break
        dialog --backtitle "ProxMenux" \
            --msgbox "$(hb_translate "Passphrases do not match. Try again.")" 8 50
    done

    msg_info "$(hb_translate "Creating PBS encryption key...")"
    if PBS_ENCRYPTION_PASSWORD="$pass1" \
        proxmox-backup-client key create "$key_file" >/dev/null 2>&1; then
        printf '%s' "$pass1" > "$enc_pass_file"
        chmod 600 "$enc_pass_file"
        msg_ok "$(hb_translate "Encryption key created:") $key_file"
        HB_PBS_KEYFILE_OPT="--keyfile $key_file"
        HB_PBS_ENC_PASS="$pass1"
        local key_warn_msg
        key_warn_msg="$(hb_translate "IMPORTANT: Back up this key file. Without it the backup cannot be restored.")"$'\n\n'"$(hb_translate "Key:") $key_file"
        dialog --backtitle "ProxMenux" --msgbox \
            "$key_warn_msg" \
            10 74
    else
        msg_error "$(hb_translate "Failed to create encryption key. Backup will proceed without encryption.")"
    fi
}

# ==========================================================
# BORG
# ==========================================================
hb_ensure_borg() {
    command -v borg >/dev/null 2>&1 && { echo "borg"; return 0; }
    local appimage="$HB_STATE_DIR/borg"
    local tmp_file
    [[ -x "$appimage" ]] && { echo "$appimage"; return 0; }
    command -v sha256sum >/dev/null 2>&1 || {
        msg_error "$(hb_translate "sha256sum not found. Cannot verify Borg binary.")"
        return 1
    }
    msg_info "$(hb_translate "Borg not found. Downloading borg") ${HB_BORG_VERSION}..."
    mkdir -p "$HB_STATE_DIR"
    tmp_file=$(mktemp "$HB_STATE_DIR/.borg-download.XXXXXX") || return 1
    if wget -qO "$tmp_file" "$HB_BORG_LINUX64_URL"; then
        if echo "${HB_BORG_LINUX64_SHA256}  $tmp_file" | sha256sum -c - >/dev/null 2>&1; then
            mv -f "$tmp_file" "$appimage"
        else
            rm -f "$tmp_file"
            msg_error "$(hb_translate "Borg binary checksum verification failed.")"
            return 1
        fi
        chmod +x "$appimage"
        msg_ok "$(hb_translate "Borg ready.")"
        echo "$appimage"; return 0
    fi
    rm -f "$tmp_file"
    msg_error "$(hb_translate "Failed to download Borg.")"
    return 1
}

hb_borg_init_if_needed() {
    local borg_bin="$1" repo="$2" encrypt_mode="$3"
    "$borg_bin" list "$repo" >/dev/null 2>&1 && return 0
    if "$borg_bin" help repo-create >/dev/null 2>&1; then
        "$borg_bin" repo-create -e "$encrypt_mode" "$repo"
    else
        "$borg_bin" init --encryption="$encrypt_mode" "$repo"
    fi
}

hb_prepare_borg_passphrase() {
    local pass_file="$HB_STATE_DIR/borg-pass.txt"
    BORG_ENCRYPT_MODE="none"
    unset BORG_PASSPHRASE

    if [[ -f "$pass_file" ]]; then
        export BORG_PASSPHRASE
        BORG_PASSPHRASE="$(<"$pass_file")"
        BORG_ENCRYPT_MODE="repokey"
        return 0
    fi

    dialog --backtitle "ProxMenux" --title "$(hb_translate "Borg encryption")" \
        --yesno "$(hb_translate "Encrypt this Borg repository?")" \
        "$HB_UI_YESNO_H" "$HB_UI_YESNO_W" || return 0

    local pass1 pass2
    while true; do
        pass1=$(dialog --backtitle "ProxMenux" --insecure --passwordbox \
            "$(hb_translate "Borg passphrase:")" \
            "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 1
        pass2=$(dialog --backtitle "ProxMenux" --insecure --passwordbox \
            "$(hb_translate "Confirm Borg passphrase:")" \
            "$HB_UI_PASS_H" "$HB_UI_PASS_W" "" 3>&1 1>&2 2>&3) || return 1
        [[ "$pass1" == "$pass2" ]] && break
        dialog --backtitle "ProxMenux" \
            --msgbox "$(hb_translate "Passphrases do not match.")" 8 50
    done

    mkdir -p "$HB_STATE_DIR"
    printf '%s' "$pass1" > "$pass_file"
    chmod 600 "$pass_file"
    export BORG_PASSPHRASE="$pass1"
    export BORG_ENCRYPT_MODE="repokey"
}

hb_select_borg_repo() {
    local _borg_repo_var="$1"
    local -n _borg_repo_ref="$_borg_repo_var"
    local type

    type=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Borg repository location")" \
        --menu "\n$(hb_translate "Select repository destination:")" \
        "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
        "local"  "$(hb_translate 'Local directory')" \
        "usb"    "$(hb_translate 'Mounted external disk')" \
        "remote" "$(hb_translate 'Remote server via SSH')" \
        3>&1 1>&2 2>&3) || return 1

    unset BORG_RSH
    case "$type" in
        local)
            _borg_repo_ref=$(dialog --backtitle "ProxMenux" \
                --inputbox "$(hb_translate "Borg repository path:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "/backup/borgbackup" \
                3>&1 1>&2 2>&3) || return 1
            mkdir -p "$_borg_repo_ref" 2>/dev/null || true
            ;;
        usb)
            local mnt
            mnt=$(hb_prompt_mounted_path "/mnt/backup") || return 1
            _borg_repo_ref="$mnt/borgbackup"
            mkdir -p "$_borg_repo_ref" 2>/dev/null || true
            ;;
        remote)
            local user host rpath ssh_key
            user=$(dialog --backtitle "ProxMenux" --inputbox "$(hb_translate "SSH user:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "root" 3>&1 1>&2 2>&3) || return 1
            host=$(dialog --backtitle "ProxMenux" --inputbox "$(hb_translate "SSH host or IP:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "" 3>&1 1>&2 2>&3) || return 1
            rpath=$(dialog --backtitle "ProxMenux" \
                --inputbox "$(hb_translate "Remote repository path:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "/backup/borgbackup" \
                3>&1 1>&2 2>&3) || return 1
            if dialog --backtitle "ProxMenux" \
                --yesno "$(hb_translate "Use a custom SSH key?")" \
                "$HB_UI_YESNO_H" "$HB_UI_YESNO_W"; then
                ssh_key=$(dialog --backtitle "ProxMenux" \
                    --fselect "$HOME/.ssh/" 12 70 3>&1 1>&2 2>&3) || return 1
                export BORG_RSH="ssh -i $ssh_key -o StrictHostKeyChecking=accept-new"
            fi
            _borg_repo_ref="ssh://$user@$host/$rpath"
            ;;
    esac
}

# ==========================================================
# COMMON PROMPTS
# ==========================================================
hb_trim_dialog_value() {
    local value="$1"
    value="${value//$'\r'/}"
    value="${value//$'\n'/}"
    value="${value#"${value%%[![:space:]]*}"}"
    value="${value%"${value##*[![:space:]]}"}"
    printf '%s' "$value"
}

hb_prompt_mounted_path() {
    local default_path="${1:-/mnt/backup}"
    local out

    out=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Mounted disk path")" \
        --inputbox "$(hb_translate "Path where the external disk is mounted:")" \
        "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "$default_path" 3>&1 1>&2 2>&3) || return 1

    out=$(hb_trim_dialog_value "$out")
    [[ -n "$out" && -d "$out" ]] || { msg_error "$(hb_translate "Path does not exist.")"; return 1; }
    if ! mountpoint -q "$out" 2>/dev/null; then
        dialog --backtitle "ProxMenux" --title "$(hb_translate "Warning")" \
            --yesno "$(hb_translate "This path is not a registered mount point. Use it anyway?")" \
            "$HB_UI_YESNO_H" "$HB_UI_YESNO_W" || return 1
    fi
    echo "$out"
}

hb_prompt_dest_dir() {
    local selection out

    selection=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Select destination")" \
        --menu "\n$(hb_translate "Choose where to save the backup:")" \
        "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
        "vzdump" "$(hb_translate '/var/lib/vz/dump   (Proxmox default vzdump path)')" \
        "backup" "$(hb_translate '/backup')" \
        "local"  "$(hb_translate 'Custom local directory')" \
        "usb"    "$(hb_translate 'Mounted external disk')" \
        3>&1 1>&2 2>&3) || return 1

    case "$selection" in
        vzdump) out="/var/lib/vz/dump" ;;
        backup) out="/backup" ;;
        local)
            out=$(dialog --backtitle "ProxMenux" \
                --inputbox "$(hb_translate "Enter directory path:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "/backup" 3>&1 1>&2 2>&3) || return 1
            ;;
        usb) out=$(hb_prompt_mounted_path "/mnt/backup") || return 1 ;;
    esac

    out=$(hb_trim_dialog_value "$out")
    [[ -n "$out" ]] || return 1
    mkdir -p "$out" || { msg_error "$(hb_translate "Cannot create:") $out"; return 1; }
    echo "$out"
}

hb_prompt_restore_source_dir() {
    local choice out

    choice=$(dialog --backtitle "ProxMenux" \
        --title "$(hb_translate "Restore source location")" \
        --menu "\n$(hb_translate "Where are the backup archives stored?")" \
        "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" \
        "vzdump" "$(hb_translate '/var/lib/vz/dump   (Proxmox default)')" \
        "backup" "$(hb_translate '/backup')" \
        "usb"    "$(hb_translate 'Mounted external disk')" \
        "custom" "$(hb_translate 'Custom path')" \
        3>&1 1>&2 2>&3) || return 1

    case "$choice" in
        vzdump) out="/var/lib/vz/dump" ;;
        backup) out="/backup" ;;
        usb)    out=$(hb_prompt_mounted_path "/mnt/backup") || return 1 ;;
        custom)
            out=$(dialog --backtitle "ProxMenux" \
                --inputbox "$(hb_translate "Enter path:")" \
                "$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "/backup" 3>&1 1>&2 2>&3) || return 1
            ;;
    esac

    out=$(hb_trim_dialog_value "$out")
    [[ -n "$out" && -d "$out" ]] || {
        msg_error "$(hb_translate "Directory does not exist.")"
        return 1
    }
    echo "$out"
}

hb_prompt_local_archive() {
    local base_dir="$1"
    local title="${2:-$(hb_translate "Select backup archive")}"
    local -a rows=() files=() menu=()

    # Single find pass using -printf: no per-file stat subprocesses.
    # maxdepth 6 catches nested backup layouts commonly used in /var/lib/vz/dump.
    mapfile -t rows < <(
        find "$base_dir" -maxdepth 6 -type f \
            \( -name '*.tar.zst' -o -name '*.tar.gz' -o -name '*.tar' \) \
            -printf '%T@|%s|%p\n' 2>/dev/null \
        | sort -t'|' -k1,1nr \
        | head -200
    )

    if [[ ${#rows[@]} -eq 0 ]]; then
        local no_backups_msg
        no_backups_msg="$(hb_translate "No backup archives were found in:") $base_dir"$'\n\n'"$(hb_translate "Select another source path and try again.")"
        dialog --backtitle "ProxMenux" \
            --title "$(hb_translate "No backups found")" \
            --msgbox "$no_backups_msg" \
            10 78 || true
        return 1
    fi

    local i=1 row epoch size path date_str size_str label
    for row in "${rows[@]}"; do
        epoch="${row%%|*}"; row="${row#*|}"
        size="${row%%|*}";  path="${row#*|}"
        epoch="${epoch%%.*}"   # drop sub-second fraction from %T@
        date_str=$(date -d "@$epoch" '+%Y-%m-%d %H:%M' 2>/dev/null || echo "-")
        size_str=$(numfmt --to=iec-i --suffix=B "$size" 2>/dev/null || echo "${size}B")
        label="${path#$base_dir/}    $date_str    $size_str"
        files+=("$path"); menu+=("$i" "$label"); ((i++))
    done

    local choice
    choice=$(dialog --backtitle "ProxMenux" --title "$title" \
        --menu "\n$(hb_translate "Detected backups — newest first:")" \
        "$HB_UI_MENU_H" "$HB_UI_MENU_W" "$HB_UI_MENU_LIST" "${menu[@]}" 3>&1 1>&2 2>&3) || return 1

    echo "${files[$((choice-1))]}"
}

# ==========================================================
# UTILITIES
# ==========================================================
hb_human_elapsed() {
    local secs="$1"
    if   (( secs < 60 ));   then printf '%ds' "$secs"
    elif (( secs < 3600 )); then printf '%dm %ds' "$((secs/60))" "$((secs%60))"
    else                         printf '%dh %dm' "$((secs/3600))" "$(( (secs%3600)/60 ))"
    fi
}

hb_file_size() {
    local path="$1"
    if [[ -f "$path" ]]; then
        numfmt --to=iec-i --suffix=B "$(stat -c %s "$path" 2>/dev/null || echo 0)" 2>/dev/null \
            || du -sh "$path" 2>/dev/null | awk '{print $1}'
    elif [[ -d "$path" ]]; then
        du -sh "$path" 2>/dev/null | awk '{print $1}'
    else
        echo "-"
    fi
}

hb_show_log() {
    local logfile="$1" title="${2:-$(hb_translate "Operation log")}"
    [[ -f "$logfile" && -s "$logfile" ]] || return 0
    dialog --backtitle "ProxMenux" --exit-label "OK" \
        --title "$title" --textbox "$logfile" 26 110 || true
}

hb_require_cmd() {
    local cmd="$1" pkg="${2:-$1}"
    command -v "$cmd" >/dev/null 2>&1 && return 0
    if command -v apt-get >/dev/null 2>&1; then
        msg_warn "$(hb_translate "Installing dependency:") $pkg"
        apt-get update -qq >/dev/null 2>&1 && apt-get install -y "$pkg" >/dev/null 2>&1
    fi
    command -v "$cmd" >/dev/null 2>&1
}
