#!/bin/bash
#
# ResticRestore.sh - Restore Proxmox VMs + LXC from restic backup
# ---------------------------------------------------------------------------
echo "Restic Restore V 4.4.8"


set -euo pipefail

# Set Default Values
RESTIC_BASEFOLDER="/media/SmartStore"

# Same config as ResticBackup.sh (optional - the script also runs without it)
config=/usr/local/bin/config.cfg
if [[ -r "$config" ]]; then
    # shellcheck source=/dev/null
    source "$config"
fi

# Derive repo and password path from RESTIC_BASEFOLDER
# (both can be overridden explicitly in the config)
RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-${RESTIC_BASEFOLDER}/ResticBackup}"
RESTIC_PASSWORDFILE="${RESTIC_PASSWORDFILE:-${RESTIC_BASEFOLDER}/ResticBackup.pw}"

FOUND_SNAP_ID=""
# On a restore via --snap <config-id>: requires every disk to carry the same
# config-id tag, so that exactly the disks belonging to that config snapshot
# are restored (not merely the snapshot closest in time).
CONFIG_ID_FILTER=""

log() { echo "[$(date '+%H:%M:%S')] [$1] $2"; }
info() { log "INFO" "$*"; }
warn() { log "WARN" "$*"; }
error() { log "ERROR" "$*" >&2; }

restic() {
    command restic --repo "$RESTIC_REPOSITORY" --password-file "$RESTIC_PASSWORDFILE" "$@"
}

# Bytes -> menschenlesbar (z.B. 1536000000 -> "1.43GiB").
# Leere/ungueltige Eingabe -> "-"
human_size() {
    local bytes="${1:-}"
    if [[ -z "$bytes" || ! "$bytes" =~ ^[0-9]+$ ]]; then
        echo "-"
        return
    fi
    awk -v b="$bytes" 'BEGIN{
        split("B KiB MiB GiB TiB PiB", u, " ");
        i=1; s=b;
        while (s>=1024 && i<6) { s/=1024; i++ }
        if (i==1) printf "%d%s\n", s, u[i];
        else printf "%.2f%s\n", s, u[i];
    }'
}

# Formatted snapshot overview: one line per snapshot with
# SNAP ID, Backup Time, TYPE, MACHINE, NAME, DISK, SIZE, PARENT, CONFIG.
# Filters case-insensitively by the global FILTER_* variables and SRC_ID.
# With --size (SHOW_SIZE=true) a cumulative restore size is shown:
#   FULL disk / LXC-FS / LOCAL : plain disk size
#   DIFF-Disk                  : Diff + zugehoerige Parent-Full-Disk
#   CONFIG (full run)          : sum of all full disks of that run
#   CONFIG (diff run)          : sum of all diff disks + their parents
print_snapshot_table() {
    local json
    json=$(restic snapshots --json 2>/dev/null) || json=""
    if [[ -z "$json" || "$json" == "null" || "$json" == "[]" ]]; then
        return 1
    fi

    # Typ-Filter normalisieren (Aliase zulassen)
    local ftype="${FILTER_TYPE,,}"
    case "$ftype" in
        differential) ftype="diff" ;;
        lxc)          ftype="lxc-fs" ;;
    esac

    local rows
    rows=$(echo "$json" | jq -r \
        --arg ftype "$ftype" \
        --arg fmachine "${SRC_ID,,}" \
        --arg fname "${FILTER_NAME,,}" \
        --arg fconfig "${FILTER_CONFIG,,}" '
        sort_by(.time)[] |
        (if ((.tags // []) | index("raw")) then "RAW"
         elif ((.tags // []) | index("full")) then "FULL"
         elif ((.tags // []) | index("differential")) then "DIFF"
         elif ((.tags // []) | index("config")) then "CONFIG"
         elif ((.tags // []) | index("lxc")) then "LXC-FS"
         elif ((.tags // []) | index("local")) then "LOCAL"
         else "?" end) as $typ |
        ((([.tags[]? | select(test("^(vm|ct)-[0-9]+$"))] | first) // "-")) as $machine |
        ((([.tags[]? | select(startswith("name="))] | first) // ""
          | sub("^name="; "")
          | if . == "" then "-" else . end)) as $name |
        ((([.tags[]? | select(test("^(vm|ct)-[0-9]+-.+$"))] | first) // ""
          | sub("^(vm|ct)-[0-9]+-"; "")) as $dk |
         (([.tags[]? | select(startswith("dir-"))] | first) // ""
          | sub("^dir-"; "")) as $dir |
         (if $dk != "" then $dk elif $dir != "" then $dir else "-" end)) as $disk |
        ((([.tags[]? | select(startswith("parent-id="))] | first) // ""
          | sub("^parent-id="; "")
          | if . == "" then "-" else . end)) as $parent |
        (if $typ == "CONFIG" then (.short_id // "-")
         else (([.tags[]? | select(startswith("config-id="))] | first) // ""
               | sub("^config-id="; "")
               | if . == "" then "-" else . end) end) as $cfg |
        select($ftype == ""    or ($typ | ascii_downcase) == $ftype) |
        select($fmachine == "" or ($machine | ascii_downcase) == $fmachine
                               or ($machine | sub("^(vm|ct)-"; "")) == $fmachine) |
        select($fname == ""    or ($name | ascii_downcase) == $fname) |
        select($fconfig == ""  or ($cfg | ascii_downcase) == $fconfig) |
        [ (.short_id // ""),
          ((.time // "")[0:16] | sub("T"; " ")),
          $typ, $machine, $name, $disk, $parent, $cfg
        ] | @tsv' 2>/dev/null) || rows=""
    [[ -z "$rows" ]] && return 1

    # The SIZE column is only computed when requested via --size. The size is
    # determined per snapshot via 'restic stats --mode restore-size' (one call
    # per snapshot) - with very many snapshots --list can get noticeably
    # dadurch spuerbar laenger dauern, daher standardmaessig deaktiviert.
    if [[ "${SHOW_SIZE:-false}" == "true" ]]; then
        info "Calculating snapshot sizes ..." >&2

        # ---- Pass 1: determine the raw size per snapshot and remember the
        # metadata (TYPE, config-id, parent-id) for the aggregation below.
        declare -A _RAWSIZE=()   # short_id -> bytes (plain restore size)
        declare -A _TYPE=()      # short_id -> TYPE
        declare -A _CFG=()       # short_id -> config-id (backup run)
        declare -A _PARENT=()    # short_id -> parent-id (DIFF disks only)
        local _line _sid _typ _parent _cfg _bytes
        while IFS=$'\t' read -r _sid _time _typ _machine _name _disk _parent _cfg; do
            [[ -z "$_sid" ]] && continue
            _bytes=$(get_snap_restore_size "$_sid") || _bytes=""
            [[ "$_bytes" =~ ^[0-9]+$ ]] || _bytes=0
            _RAWSIZE["$_sid"]="$_bytes"
            _TYPE["$_sid"]="$_typ"
            _CFG["$_sid"]="$_cfg"
            _PARENT["$_sid"]="$_parent"
        done <<< "$rows"

        # ---- Pass 2: compute the cumulative size per line ----
        # Regeln:
        #  FULL disk / LXC-FS / LOCAL : plain disk size
        #  DIFF-Disk                  : Diff + Parent-Full-Disk (parent-id)
        #  CONFIG (full run)          : sum of all FULL disks of that run
        #  CONFIG (diff run)          : sum of all DIFF disks of that run
        #                               + deren Parent-Full-Disks
        # Whether a CONFIG run is full or diff is derived from the TYPEs of the
        # disk snapshots carrying the same config-id.
        local rows_with_size=""
        local _cum _psize
        while IFS=$'\t' read -r _sid _time _typ _machine _name _disk _parent _cfg; do
            [[ -z "$_sid" ]] && continue
            _cum=0
            case "$_typ" in
                DIFF)
                    _cum=${_RAWSIZE["$_sid"]:-0}
                    if [[ -n "$_parent" && "$_parent" != "-" ]]; then
                        _psize=${_RAWSIZE["$_parent"]:-0}
                        _cum=$(( _cum + _psize ))
                    fi
                    ;;
                CONFIG)
                    # Add up all snapshots belonging to the run (same
                    # aufaddieren:
                    #  FULL   : plain disk size
                    #  DIFF   : disk size + parent full disk
                    #  LXC-FS : plain filesystem size (no parent concept)
                    local _k
                    for _k in "${!_CFG[@]}"; do
                        [[ "${_CFG[$_k]}" == "$_cfg" ]] || continue
                        case "${_TYPE[$_k]}" in
                            FULL|RAW)
                                _cum=$(( _cum + ${_RAWSIZE[$_k]:-0} ))
                                ;;
                            DIFF)
                                _cum=$(( _cum + ${_RAWSIZE[$_k]:-0} ))
                                local _pp="${_PARENT[$_k]}"
                                if [[ -n "$_pp" && "$_pp" != "-" ]]; then
                                    _cum=$(( _cum + ${_RAWSIZE[$_pp]:-0} ))
                                fi
                                ;;
                            LXC-FS)
                                _cum=$(( _cum + ${_RAWSIZE[$_k]:-0} ))
                                ;;
                        esac
                    done
                    ;;
                *)
                    _cum=${_RAWSIZE["$_sid"]:-0}
                    ;;
            esac
            rows_with_size+="${_sid}"$'\t'"${_time}"$'\t'"${_typ}"$'\t'"${_machine}"$'\t'"${_name}"$'\t'"${_disk}"$'\t'"${_parent}"$'\t'"${_cfg}"$'\t'"$(human_size "$_cum")"$'\n'
        done <<< "$rows"

        {
            printf 'SNAP ID\tBackup Time\tTYPE\tMACHINE\tNAME\tDISK\tPARENT\tCONFIG\tSIZE\n'
            printf '%s' "$rows_with_size"
        } | awk -F'\t' '{printf "%-10s  %-16s  %-7s  %-9s  %-24s  %-10s  %10s  %-10s  %s\n", $1, $2, $3, $4, $5, $6, $9, $7, $8}'
    else
        {
            printf 'SNAP ID\tBackup Time\tTYPE\tMACHINE\tNAME\tDISK\tPARENT\tCONFIG\n'
            echo "$rows"
        } | awk -F'\t' '{printf "%-10s  %-16s  %-7s  %-9s  %-24s  %-10s  %-10s  %s\n", $1, $2, $3, $4, $5, $6, $7, $8}'
    fi
    return 0
}

show_usage() {
    cat << 'EOF'
Usage: ResticRestore.sh <command> [options]

List:
  --list                        List all restic snapshots
  --list --machine <ID>         Only snapshots of this VM/CT
  --list --type <TYPE>          Only this type: raw, full, diff, config,
                                lxc-fs, local
  --list --name <NAME>          Only machines with this name
  --list --config <SNAP-ID>     Only snapshots belonging to this backup run
                                (all filters are case-insensitive, combinable)
  --list --size                 Additionally show each snapshot's restore
                                size (slower: one restic stats call per
                                snapshot)
  --list-disks <VMID>           Show disk backup status for a VM

Restore (requires --target AND one of --machine / --snap):
  --machine <ID> --target <DST>   Restore the LATEST backup run of machine <ID>
  --snap <SNAP-ID> --target <DST> Restore exactly this backup run (config
                                  snapshot ID from --list; the machine is
                                  derived from the snapshot, --machine is
                                  not required)

LOCAL / dir snapshots (browse or pull files, no VM/CT involved):
  --snap <SNAP-ID>                        If the snapshot is a LOCAL/dir backup,
                                          it is mounted via FUSE into a fresh
                                          temp dir in the foreground. Open a
                                          SECOND console to copy files from
                                          <tmp>/ids/<SNAP-ID>/. Ctrl+C unmounts
                                          and removes the temp dir. Needs FUSE.
  --snap <SNAP-ID> --target <DIR>         LOCAL/dir snapshot: extract it straight
                                          into <DIR> via 'restic restore' (no
                                          FUSE, no temp dir, no blocking).

Options:
  --target <ID>                 Destination VMID/CTID - MANDATORY for restore,
                                must not exist yet
  --date <YYYY-MM-DD>           With --machine: use latest backup run on or
                                before this date instead of the newest
  --dry-run                     Show what would be restored without doing it
  --help                        Show this help

Safety:
  - A restore never runs without an explicit --target; this prevents
    accidental restores when you actually meant to filter --list.
  - The target ID must NOT exist. Existing machines are never
    overwritten - delete the machine manually first or choose a
    different --target ID.

Backup run selection:
  - Disks/filesystems are matched to the chosen config snapshot via the
    config-id tag, so exactly the disks belonging to that run are
    restored. Differentials automatically pull in their parent full.

RAW image backups (TYPE=RAW in --list):
  - Volumes that are not ZFS-backed (LVM, LVM-thin, raw files) or hosts
    running with BACKUP_MODE=raw are stored as full raw block images.
  - There are no differentials for RAW backups; every RAW snapshot is
    self-contained and restores in a single step.
  - On restore the target volume is allocated automatically via
    'pvesm alloc' (size taken from the raw-size tag) and written with dd.
EOF
}

find_snap_by_tag() {
    FOUND_SNAP_ID=""
    local before_cutoff=""
    local tags=()

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --before) before_cutoff="$2"; shift 2 ;;
            *) tags+=("$1"); shift ;;
        esac
    done

    local tag_csv
    tag_csv=$(IFS=,; echo "${tags[*]}")

    local snapshots_json
    snapshots_json=$(restic snapshots --tag "$tag_csv" --json 2>/dev/null) || return 1
    [[ -z "$snapshots_json" || "$snapshots_json" == "[]" ]] && return 1

    if [[ -n "$before_cutoff" ]]; then
        local cutoff="$before_cutoff"
        [[ "$cutoff" != *T* ]] && cutoff="${cutoff}T23:59:59"
        FOUND_SNAP_ID=$(echo "$snapshots_json" | jq -r \
            --arg before "$cutoff" \
            'sort_by(.time) | map(select(.time <= $before)) | last | .short_id // empty' 2>/dev/null) || true
    else
        FOUND_SNAP_ID=$(echo "$snapshots_json" | jq -r \
            'sort_by(.time) | last | .short_id // empty' 2>/dev/null) || true
    fi
    [[ -n "$FOUND_SNAP_ID" && "$FOUND_SNAP_ID" != "null" ]]
}

get_snap_info() {
    local snap_id="$1"
    restic snapshots --json "$snap_id" 2>/dev/null \
        | jq -r '.[0] | [.time, ([.tags[] | select(startswith("parent-id=")) | sub("^parent-id=";"")][0] // ""),
                     ([.tags[] | select(startswith("parent-snap=")) | sub("^parent-snap=";"")][0] // ""),
                     ([.tags[] | select(startswith("base-snap=")) | sub("^base-snap=";"")][0] // "")] | @tsv' 2>/dev/null
}

snap_exists() {
    restic snapshots --json "$1" 2>/dev/null | jq -e 'length > 0' >/dev/null 2>&1
}

snap_has_file() {
    local snap_id="$1" filename="$2"
    restic ls "$snap_id" 2>/dev/null | awk -v f="$filename" '$0 ~ "(^|/)"f"$" {found=1} END{exit !found}'
}

get_zfs_pool_path() {
    local storage="$1"
    awk -v s="$storage" '
        /^zfspool:/ { name=$2 }
        $1=="pool" && name==s { print $2 }
    ' /etc/pve/storage.cfg
}

# Progress display: with a known size pv shows percent + ETA,
# without a size only throughput; without pv the data is passed through (cat)
pipe_helper() {
    local label="$1" size="${2:-}"
    if command -v pv &>/dev/null; then
        if [[ -n "$size" && "$size" =~ ^[0-9]+$ && "$size" -gt 0 ]]; then
            pv -N "$label" -s "$size"
        else
            pv -N "$label"
        fi
    else
        cat
    fi
}

# Size of a file inside a restic snapshot (bytes, empty if unknown)
get_snap_file_size() {
    local snap_id="$1" filename="$2"
    restic ls --json "$snap_id" 2>/dev/null \
        | jq -r --arg n "$filename" 'select(.type? == "file" and .name? == $n) | .size' 2>/dev/null \
        | head -1
}

# Total size of a snapshot on restore (bytes, empty if unknown)
get_snap_restore_size() {
    local snap_id="$1"
    restic stats --mode restore-size --json "$snap_id" 2>/dev/null \
        | jq -r '.total_size // empty' 2>/dev/null
}

# Recorded root path of a snapshot (paths[0])
get_snap_root_path() {
    local snap_id="$1"
    restic snapshots --json "$snap_id" 2>/dev/null \
        | jq -r '.[0].paths[0] // empty' 2>/dev/null
}

# Read the value of a tag with the given prefix from a snapshot
# (z.B. snap_tag_value <id> "raw-size=")
snap_tag_value() {
    local snap_id="$1" prefix="$2"
    restic snapshots --json "$snap_id" 2>/dev/null \
        | jq -r --arg p "$prefix" '.[0].tags[]? | select(startswith($p)) | sub("^" + $p; "")' 2>/dev/null \
        | head -1
}

# ================== RAW Image Restore ==================
#
# RAW backups are always complete (no diff). Restore:
#   1) Create the target volume if it does not exist (pvesm alloc, size taken
#      from the raw-size tag)
#   2) restic dump <snap> /<datei>.raw | dd of=<volpath>
#
# This works for ZFS zvols, LVM/LVM-thin LVs and raw files on directory
# storages alike.
restore_raw_volume() {
    local snap="$1" dst_volume="$2" dst_id="$3" label="$4" dump_fn="$5"
    local size volpath storage volname

    size=$(snap_tag_value "$snap" "raw-size=") || size=""
    if [[ ! "$size" =~ ^[0-9]+$ ]]; then
        size=$(get_snap_file_size "$snap" "$dump_fn") || size=""
    fi
    if [[ ! "$size" =~ ^[0-9]+$ ]]; then
        error "  ${label}: cannot determine size of RAW image (${dump_fn})"
        return 1
    fi

    if ! snap_has_file "$snap" "$dump_fn"; then
        error "  ${label}: RAW snapshot ${snap} does not contain ${dump_fn}"
        return 1
    fi

    storage="${dst_volume%%:*}"
    volname="${dst_volume##*:}"
    volname="${volname##*/}"

    if $DRY_RUN; then
        info "  ${label}: [DRY-RUN] RAW (${snap}, $(human_size "$size"))"
        info "    pvesm alloc ${storage} ${dst_id} ${volname} $(( (size + 1023) / 1024 ))"
        info "    restic dump ${snap} /${dump_fn} | dd of=<${dst_volume}>"
        return 0
    fi

    volpath=$(pvesm path "$dst_volume" 2>/dev/null || true)
    if [[ -z "$volpath" || ! -e "$volpath" ]]; then
        local kb=$(( (size + 1023) / 1024 ))
        if ! pvesm alloc "$storage" "$dst_id" "$volname" "$kb" &>/dev/null; then
            error "  ${label}: 'pvesm alloc ${storage} ${dst_id} ${volname} ${kb}' failed"
            return 1
        fi
        volpath=$(pvesm path "$dst_volume" 2>/dev/null || true)
    fi
    if [[ -z "$volpath" || ! -e "$volpath" ]]; then
        error "  ${label}: target volume ${dst_volume} not available"
        return 1
    fi

    info "  ${label}: RAW restore ${snap} ($(human_size "$size")) -> ${volpath}"
    if ! restic dump "$snap" "/${dump_fn}" \
            | pipe_helper "${label}-raw" "$size" \
            | dd of="$volpath" bs=512k conv=notrunc,sparse status=none; then
        error "  ${label}: RAW restore failed"
        return 1
    fi
    info "  OK: ${label} (raw)"
    return 0
}

# ================== VM Disk Restore ==================

restore_vm_disk() {
    local src_id="$1" disk_key="$2" dst_volume="$3"
    local before_date=""
    shift 3
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --before) before_date="$2"; shift 2 ;;
            *) shift ;;
        esac
    done

    local disk_tag="vm-${src_id}-${disk_key}"

    # --- RAW image? Then take the dedicated path, without ZFS ---
    local raw_find_extra=() raw_group_extra=()
    [[ -n "$before_date" ]] && raw_find_extra=(--before "$before_date")
    [[ -n "$CONFIG_ID_FILTER" ]] && raw_group_extra=("config-id=${CONFIG_ID_FILTER}")
    if find_snap_by_tag "$disk_tag" "raw" "${raw_group_extra[@]}" "${raw_find_extra[@]}"; then
        restore_raw_volume "$FOUND_SNAP_ID" "$dst_volume" "$DST_ID" "$disk_key" "vm-${src_id}-${disk_key}.raw"
        return $?
    fi

    local volpath dataset
    volpath=$(pvesm path "$dst_volume" 2>/dev/null || true)
    if [[ -n "$volpath" && "$volpath" == /dev/zvol/* ]]; then
        dataset="${volpath#/dev/zvol/}"
    else
        local volname="${dst_volume##*:}"
        local storage_name="${dst_volume%%:*}"
        local pool_path
        pool_path=$(get_zfs_pool_path "$storage_name")
        if [[ -z "$pool_path" ]]; then
            error "  ${disk_key}: cannot find ZFS pool for storage '${storage_name}'"
            return 1
        fi
        dataset="${pool_path}/${volname}"
    fi

    local find_extra=()
    [[ -n "$before_date" ]] && find_extra=(--before "$before_date")

    # When restoring via --snap <config-id>: use strictly the disks belonging
    # to that config snapshot. The differential tag gets config-id directly,
    # the full fallback (for the diff -> full chain) does NOT, because a diff
    # may point at an older full from a different run.
    local disk_group_extra=()
    [[ -n "$CONFIG_ID_FILTER" ]] && disk_group_extra=("config-id=${CONFIG_ID_FILTER}")

    local dump_fn="vm-${src_id}-${disk_key}.zfs"

    # ---- Phase 1: build and validate the restore plan (destroy nothing yet) ----
    local full_snap="" diff_snap=""

    if find_snap_by_tag "$disk_tag" "differential" "${disk_group_extra[@]}" "${find_extra[@]}"; then
        diff_snap="$FOUND_SNAP_ID"
        local _info diff_time parent_id parent_snap
        _info=$(get_snap_info "$diff_snap") || true
        IFS=$'\t' read -r diff_time parent_id parent_snap _ <<< "$_info"

        if ! snap_has_file "$diff_snap" "$dump_fn"; then
            warn "  ${disk_key}: differential ${diff_snap} does not contain ${dump_fn}, skipping"
            diff_snap=""
        fi

        if [[ -n "$diff_snap" ]]; then
            if [[ -n "$parent_id" ]] && snap_exists "$parent_id" && snap_has_file "$parent_id" "$dump_fn"; then
                full_snap="$parent_id"
            else
                [[ -n "$parent_id" ]] && warn "  ${disk_key}: parent-id ${parent_id} missing or without ${dump_fn}"
                if find_snap_by_tag "$disk_tag" "full" "${find_extra[@]}" \
                        && snap_has_file "$FOUND_SNAP_ID" "$dump_fn"; then
                    full_snap="$FOUND_SNAP_ID"
                    warn "  ${disk_key}: falling back to latest full ${full_snap} as base"
                fi
            fi
            if [[ -n "$full_snap" && "$full_snap" == "$diff_snap" ]]; then
                warn "  ${disk_key}: differential and full are same snapshot, treating as full-only"
                diff_snap=""
            fi
            if [[ -z "$full_snap" ]]; then
                warn "  ${disk_key}: no usable full base for differential ${diff_snap}, trying full-only"
                diff_snap=""
            fi
        fi
    fi

    if [[ -z "$full_snap" ]]; then
        # For "pure" full runs the config-id filter may apply - the full then
        # sits in the same backup run as the config snapshot.
        if find_snap_by_tag "$disk_tag" "full" "${disk_group_extra[@]}" "${find_extra[@]}"; then
            full_snap="$FOUND_SNAP_ID"
            if ! snap_has_file "$full_snap" "$dump_fn"; then
                warn "  ${disk_key}: full ${full_snap} does not contain ${dump_fn}"
                warn "  ${disk_key}: available files:"
                restic ls "$full_snap" 2>/dev/null | head -10
                error "  ${disk_key}: cannot find ${dump_fn} in any snapshot"
                return 1
            fi
        fi
    fi

    if [[ -z "$full_snap" ]]; then
        error "  ${disk_key}: no backup found for ${disk_tag}"
        return 1
    fi

    if $DRY_RUN; then
        local dr_full_size dr_diff_size
        dr_full_size=$(get_snap_file_size "$full_snap" "$dump_fn") || true
        if [[ -n "$diff_snap" ]]; then
            dr_diff_size=$(get_snap_file_size "$diff_snap" "$dump_fn") || true
            info "  ${disk_key}: [DRY-RUN] Full (${full_snap}, $(human_size "$dr_full_size")) + Differential (${diff_snap}, $(human_size "$dr_diff_size"))"
            info "    restic dump ${full_snap} /${dump_fn} | zfs recv -F ${dataset}"
            info "    restic dump ${diff_snap} /${dump_fn} | zfs recv -F ${dataset}"
        else
            info "  ${disk_key}: [DRY-RUN] Full only (${full_snap}, $(human_size "$dr_full_size"))"
            info "    restic dump ${full_snap} /${dump_fn} | zfs recv -F ${dataset}"
        fi
        return 0
    fi

    # ---- Phase 2: the plan holds - only now remove the target dataset ----
    zfs destroy -r "$dataset" 2>/dev/null || true

    local full_size
    full_size=$(get_snap_file_size "$full_snap" "$dump_fn") || true
    if [[ -n "$diff_snap" ]]; then
        local diff_size_pre
        diff_size_pre=$(get_snap_file_size "$diff_snap" "$dump_fn") || true
        info "  ${disk_key}: Full (${full_snap}, $(human_size "$full_size")) + Differential (${diff_snap}, $(human_size "$diff_size_pre"))"
    else
        info "  ${disk_key}: Full (${full_snap}, $(human_size "$full_size"))"
    fi
    restic dump "$full_snap" "/${dump_fn}" \
        | pipe_helper "${disk_key}-full" "$full_size" \
        | zfs recv -F "$dataset" || {
        error "  ${disk_key}: full restore failed"
        return 1
    }

    if [[ -n "$diff_snap" ]]; then
        local diff_size
        diff_size=$(get_snap_file_size "$diff_snap" "$dump_fn") || true
        restic dump "$diff_snap" "/${dump_fn}" \
            | pipe_helper "${disk_key}-diff" "$diff_size" \
            | zfs recv -F "$dataset" || {
            error "  ${disk_key}: differential apply failed"
            return 1
        }
        info "  OK: ${disk_key} (full + differential)"
    else
        info "  OK: ${disk_key} (full only)"
    fi
    return 0
}

# ================== LXC Filesystem Restore ==================

restore_lxc_fs() {
    local src_id="$1" key="$2" dst_volume="$3"
    local before_date=""
    shift 3
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --before) before_date="$2"; shift 2 ;;
            *) shift ;;
        esac
    done

    local find_extra=()
    [[ -n "$before_date" ]] && find_extra=(--before "$before_date")

    local fs_group_extra=()
    [[ -n "$CONFIG_ID_FILTER" ]] && fs_group_extra=("config-id=${CONFIG_ID_FILTER}")

    # --- RAW image (LVM/LVM-thin/raw file)? Then do a block restore ---
    if find_snap_by_tag "ct-${src_id}-${key}" "raw" "${fs_group_extra[@]}" "${find_extra[@]}"; then
        restore_raw_volume "$FOUND_SNAP_ID" "$dst_volume" "$DST_ID" "$key" "ct-${src_id}-${key}.raw"
        return $?
    fi

    if ! find_snap_by_tag "ct-${src_id}" "lxc" "${fs_group_extra[@]}" "${find_extra[@]}"; then
        error "  ${key}: no filesystem snapshot found for ct-${src_id}"
        return 1
    fi
    local fs_snap="$FOUND_SNAP_ID"

    local volpath
    volpath=$(pvesm path "$dst_volume" 2>/dev/null || true)
    if [[ -z "$volpath" || ! -d "$volpath" ]]; then
        local volname="${dst_volume##*:}"
        local storage_name="${dst_volume%%:*}"
        local pool_path
        pool_path=$(get_zfs_pool_path "$storage_name")
        if [[ -n "$pool_path" ]]; then
            zfs create "${pool_path}/${volname}" 2>/dev/null || true
            volpath=$(pvesm path "$dst_volume" 2>/dev/null || true)
        fi
        if [[ -z "$volpath" || ! -d "$volpath" ]]; then
            # Do not assume the pool is mounted at /<pool> - ask ZFS for the
            # real mountpoint. Only fall back to the naive path if ZFS has
            # nothing usable (unset, none, legacy).
            local mp=""
            mp=$(zfs get -H -o value mountpoint "${pool_path}/${volname}" 2>/dev/null) || mp=""
            case "$mp" in
                ""|"-"|none|legacy) volpath="/${pool_path}/${volname}" ;;
                *)                  volpath="$mp" ;;
            esac
            mkdir -p "$volpath" 2>/dev/null || true
        fi
    fi

    # Inside the snapshot the content sits at the snapshot ROOT, not under the
    # original host mountpoint from paths[0].
    # The backup ran as "cd <snap-mount> && restic backup ." -> content = "/".
    # restic dump <snap> <path> expects a path INSIDE the snapshot tree; the
    # absolute host path (e.g. /tmp/restic-snap-ct1007-rootfs) does NOT exist
    # there and leads to "path not found in snapshot".
    # So the correct internal dump path is determined robustly.
    local dump_path
    dump_path=$(determine_lxc_dump_path "$fs_snap") || {
        error "  ${key}: cannot determine dump path inside snapshot ${fs_snap}"
        return 1
    }

    if $DRY_RUN; then
        local dr_fs_size
        dr_fs_size=$(get_snap_restore_size "$fs_snap") || true
        info "  ${key}: [DRY-RUN] restic restore ${fs_snap} ($(human_size "$dr_fs_size")) --target /tmp/... -> tar-copy -> ${volpath}"
        return 0
    fi

    local fs_size
    fs_size=$(get_snap_restore_size "$fs_snap") || true
    info "  ${key}: restoring ${fs_snap} (${dump_path}, $(human_size "$fs_size")) -> ${volpath}"

    # --- Primary path: stage through /tmp ---
    # A direct stream (restic dump | tar -x -C <zvol-mount>) reliably fails
    # here, because the freshly created target dataset cannot be written to
    # directly. The detour via restic restore into a /tmp directory and a
    # subsequent tar copy does work.
    # --numeric-owner is mandatory: unprivileged CTs have shifted UIDs.
    local staged_ok=true
    local stage_dir="/tmp/restic-lxc-restore-${src_id}-$$"
    rm -rf "$stage_dir"
    mkdir -p "$stage_dir"

    if ! restic restore "$fs_snap" --target "$stage_dir"; then
        error "  ${key}: staging restore to ${stage_dir} failed"
        rm -rf "$stage_dir"
        staged_ok=false
    fi

    if $staged_ok; then
        # Determine the source root in the staging folder: the content is
        # either directly in stage_dir or under the recorded host path.
        local src_root="$stage_dir"
        local rec_root
        rec_root=$(get_snap_root_path "$fs_snap") || true
        if [[ -n "$rec_root" && -d "${stage_dir}${rec_root}" ]]; then
            src_root="${stage_dir}${rec_root}"
        elif [[ ! -e "${stage_dir}/etc" && ! -e "${stage_dir}/usr" && ! -e "${stage_dir}/bin" ]]; then
            # Content may be one level deeper (a single subdirectory)
            local only
            only=$(find "$stage_dir" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -1) || true
            [[ -n "$only" ]] && src_root="$only"
        fi

        # Copy staging -> target with tar, preserving numeric permissions/owner
        if ! tar -c --numeric-owner -p -C "$src_root" . \
                | pipe_helper "${key}" "$fs_size" \
                | tar -x -p --numeric-owner -f - -C "$volpath"; then
            error "  ${key}: copy from staging ${src_root} to ${volpath} failed"
            rm -rf "$stage_dir"
            return 1
        fi
        rm -rf "$stage_dir"
    else
        # --- Fallback: direkter Stream ---
        # If staging fails (e.g. /tmp is full), stream directly.
        warn "  ${key}: staging failed, falling back to direct stream"
        if ! restic dump "$fs_snap" "$dump_path" \
                | pipe_helper "${key}" "$fs_size" \
                | tar -x -p --numeric-owner -f - -C "$volpath"; then
            error "  ${key}: direct stream restore also failed"
            return 1
        fi
    fi

    # Fix ownership of the dataset root: 'zfs create' creates it as root:root,
    # but the container root (UID 100000 for unprivileged CTs) needs it for
    # itself - otherwise the start fails with
    # 'mount_autodev: Failed to create /dev: Permission denied'.
    local ref_entry=""
    local _cand
    for _cand in etc usr bin var; do
        [[ -e "${volpath}/${_cand}" ]] && { ref_entry="${volpath}/${_cand}"; break; }
    done
    [[ -z "$ref_entry" ]] && ref_entry=$(find "$volpath" -mindepth 1 -maxdepth 1 2>/dev/null | head -1) || true
    if [[ -n "$ref_entry" ]]; then
        local _owner
        _owner=$(stat -c '%u:%g' "$ref_entry" 2>/dev/null) || _owner=""
        if [[ -n "$_owner" ]]; then
            chown "$_owner" "$volpath" 2>/dev/null || warn "  ${key}: chown ${_owner} ${volpath} failed"
            chmod 755 "$volpath" 2>/dev/null || true
            info "  ${key}: rootfs owner set to ${_owner}"
        fi
    fi

    info "  OK: ${key} -> ${volpath}"
    return 0
}

# Determine the correct dump path INSIDE the snapshot.
# restic dump expects a path in the snapshot file tree. Depending on how the
# backup ran that is either "/" (content directly at the root) or the recorded
# paths[0]. Check via 'restic ls' which path actually holds entries.
# tatsaechlich Eintraege enthaelt.
determine_lxc_dump_path() {
    local snap_id="$1"
    local rec_root
    rec_root=$(get_snap_root_path "$snap_id") || rec_root=""

    # 1) Does the recorded path really exist as a directory in the snapshot?
    if [[ -n "$rec_root" && "$rec_root" != "/" ]]; then
        if restic ls "$snap_id" "$rec_root" 2>/dev/null | grep -q .; then
            echo "$rec_root"
            return 0
        fi
    fi

    # 2) Default case: the content sits at the root "/"
    #    (the backup ran as "cd <mount> && restic backup .")
    if restic ls "$snap_id" "/" 2>/dev/null | grep -qE '/(etc|usr|bin|var|dev|lib)(/|$)'; then
        echo "/"
        return 0
    fi

    # 3) Last fallback: accept the root if there are any entries at all
    if restic ls "$snap_id" 2>/dev/null | grep -q .; then
        echo "/"
        return 0
    fi

    return 1
}

# ================= Config Restore ==================

restore_config() {
    local src_id="$1" dst_id="$2" prefix="$3" snap_id="$4"
    local config_filename="${prefix}-${src_id}.conf"
    local restore_dir="/tmp/restic-restore-cfg-$$"
    mkdir -p "$restore_dir"

    if $DRY_RUN; then
        info "  [DRY-RUN] restic restore ${snap_id} --include ${config_filename}"
        rm -rf "$restore_dir"
        return 0
    fi

    restic restore "$snap_id" --include "$config_filename" --target "$restore_dir" || {
        error "  Config restore failed"
        rm -rf "$restore_dir"
        return 1
    }

    local src_file="${restore_dir}/${config_filename}"
    if [[ ! -f "$src_file" ]]; then
        src_file=$(find "$restore_dir" -name "$config_filename" -type f 2>/dev/null | head -1)
    fi
    if [[ ! -f "$src_file" ]]; then
        error "  Config file ${config_filename} not found in snapshot"
        rm -rf "$restore_dir"
        return 1
    fi

    local dst_dir dst_file
    if [[ "$prefix" == "vm" ]]; then
        dst_dir="/etc/pve/qemu-server"
    else
        dst_dir="/etc/pve/lxc"
    fi
    dst_file="${dst_dir}/${dst_id}.conf"

    python3 -c "
import json, sys
data = json.load(open(sys.argv[1]))
src, dst, out, pfx = sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
lines = []

# Fields managed by Proxmox / dependent on the run that must NEVER end up in a
# geschriebene Config gehoeren:
#  - digest : checksum of the config state -> PVE reports 'unknown setting'
#             and recomputes it itself anyway
#  - parent / snaptime / <snapshotname> as a section : snapshot references
#    that would point at snapshots which do not exist after the restore
#  - lxc.* is kept (valid container options)
SKIP_KEYS = {'digest', 'parent', 'snaptime'}

def rewrite(v):
    if pfx == 'vm':
        return v.replace(f'vm-{src}-', f'vm-{dst}-')
    # LXC: ZFS/dir-Storages nutzen subvol-<id>-, LVM/LVM-thin nutzen vm-<id>-
    return v.replace(f'subvol-{src}-', f'subvol-{dst}-') \
            .replace(f'vm-{src}-', f'vm-{dst}-')

# PVE config convention for multi-line values (e.g. description):
# the first line is a normal 'key: <first line>', every continuation line
# starts with exactly one space. That way continuation lines are NOT
# Config-Keys fehlinterpretiert.
def emit(key, value):
    value = rewrite(str(value))
    parts = value.split('\n')
    if len(parts) <= 1:
        if value:
            lines.append(f'{key}: {value}')
        else:
            lines.append(f'{key}:')
    else:
        first = parts[0]
        lines.append(f'{key}: {first}' if first else f'{key}:')
        for cont in parts[1:]:
            # fuehrendes Leerzeichen = Fortsetzungszeile
            lines.append(f' {cont}')

for key, value in sorted(data.items()):
    if key in SKIP_KEYS:
        continue
    # Sections for named snapshots appear as nested dicts/lists with their own
    # key -> skip them, we only want the active config, not the snapshot
    # history.
    if isinstance(value, dict):
        # a whole snapshot section, do not take it over
        continue
    if isinstance(value, list) and value and all(
        isinstance(item, list) and len(item) == 2 for item in value
    ):
        for subkey, subvalue in value:
            if subkey in SKIP_KEYS:
                continue
            emit(subkey, subvalue)
        continue
    if isinstance(value, list):
        value = json.dumps(value)
    emit(key, value)

open(out, 'w').write('\n'.join(lines) + '\n')
" "$src_file" "$src_id" "$dst_id" "$dst_file" "$prefix" || {
        error "  Config conversion failed"
        rm -rf "$restore_dir"
        return 1
    }

    info "  Config: ${dst_file}"
    rm -rf "$restore_dir"
    return 0
}

# ================== List Disks ==================

show_disk_status() {
    local vmid="$1"
    local vm_config
    vm_config=$(qm config "$vmid" 2>/dev/null) || {
        error "Cannot read config for VM ${vmid}"
        return 1
    }
    local vm_name
    vm_name=$(echo "$vm_config" | awk -F': ' '/^name:/{print $2}')
    [[ -z "$vm_name" ]] && vm_name="(unnamed)"
    info "Disk backup status for VM ${vmid} (${vm_name}):"
    info ""

    local disk_regex='^(ide[0-3]|sata[0-5]|scsi[0-9]+|virtio[0-9]+|efidisk[0-9]+|tpmstate[0-9]+):'

    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        local key="${line%%:*}"
        local disk_tag="vm-${vmid}-${key}"
        printf "%-12s " "${key}:"

        if find_snap_by_tag "$disk_tag" "raw"; then
            local raw_snap="$FOUND_SNAP_ID"
            local _rinfo raw_time
            _rinfo=$(get_snap_info "$raw_snap") || true
            IFS=$'\t' read -r raw_time _ _ _ <<< "$_rinfo"
            printf "RAW (%s)\n" "$raw_snap"
            printf "%-12s   size: %s\n" "" "$(human_size "$(snap_tag_value "$raw_snap" "raw-size=")")"
            printf "%-12s   time: %s\n" "" "$raw_time"
        elif find_snap_by_tag "$disk_tag" "differential"; then
            local diff_snap="$FOUND_SNAP_ID"
            local _info diff_time parent_id parent_snap
            _info=$(get_snap_info "$diff_snap") || true
            IFS=$'\t' read -r diff_time parent_id parent_snap _ <<< "$_info"
            if [[ -n "$parent_id" ]] && snap_exists "$parent_id"; then
                printf "DIFF (%s) + FULL (%s)\n" "$diff_snap" "$parent_id"
                printf "%-12s   parent-snap: %s\n" "" "$parent_snap"
                printf "%-12s   diff time:   %s\n" "" "$diff_time"
            else
                printf "DIFF (%s) [parent forgotten!]\n" "$diff_snap"
            fi
        elif find_snap_by_tag "$disk_tag" "full"; then
            local full_snap="$FOUND_SNAP_ID"
            local _info full_time _ base_snap
            _info=$(get_snap_info "$full_snap") || true
            IFS=$'\t' read -r full_time _ _ base_snap <<< "$_info"
            printf "FULL (%s)\n" "$full_snap"
            printf "%-12s   base-snap: %s\n" "" "$base_snap"
            printf "%-12s   time:      %s\n" "" "$full_time"
        else
            printf "NO BACKUP\n"
        fi
    done < <(echo "$vm_config" | awk -v re="$disk_regex" '$0 ~ re {print}')

    printf "%-12s " "config:"
    if find_snap_by_tag "vm-${vmid}" "config"; then
        printf "CONFIG (%s)\n" "$FOUND_SNAP_ID"
    else
        printf "NO BACKUP\n"
    fi
}

# ================== LOCAL snapshot mount / extract ==================

# Handles a LOCAL/dir snapshot (called automatically when --snap points at
# one). Two modes of operation:
#   1) With a target directory ($2): extract exactly this snapshot there via
#      'restic restore' (no FUSE, no temp folder needed).
#   2) Without a target: mount the snapshot via FUSE into a fresh temp
#      directory and block in the foreground - for copying out individual
#      files from a second console. Ctrl+C unmounts cleanly and removes the
#      temp directory (trap).
mount_snapshot() {
    local snap_id="$1"
    local dest="${2:-}"

    if ! snap_exists "$snap_id"; then
        error "Snapshot ${snap_id} not found in repository"
        return 1
    fi

    # ---- Mode 1: extract straight into a target directory ----
    if [[ -n "$dest" ]]; then
        if [[ -e "$dest" && ! -d "$dest" ]]; then
            error "Target ${dest} exists and is not a directory"
            return 1
        fi
        mkdir -p "$dest" 2>/dev/null || {
            error "Could not create target directory ${dest}"
            return 1
        }
        # Warn if the target is not empty (restic overwrites/merges).
        if [[ -n "$(ls -A "$dest" 2>/dev/null)" ]]; then
            warn "Target directory ${dest} is not empty - existing files"
            warn "may be overwritten."
        fi

        local _rsize
        _rsize=$(get_snap_restore_size "$snap_id") || true
        info "Extracting snapshot ${snap_id} ($(human_size "$_rsize")) to ${dest} ..."

        if $DRY_RUN; then
            info "  [DRY-RUN] restic restore ${snap_id} --target ${dest}"
            return 0
        fi

        if ! restic restore "$snap_id" --target "$dest"; then
            error "restic restore of ${snap_id} to ${dest} failed"
            return 1
        fi
        info "Done: snapshot ${snap_id} is now available at ${dest}"
        return 0
    fi

    # ---- Mode 2: FUSE mount in the foreground ----
    # 'restic mount' needs FUSE. Report clearly and early if it is missing.
    if ! command -v fusermount &>/dev/null && ! command -v fusermount3 &>/dev/null; then
        warn "fusermount not found - 'restic mount' requires FUSE."
        warn "Install if needed: apt-get install -y fuse3"
    fi

    local mnt
    mnt=$(mktemp -d "/tmp/restic-mount-${snap_id}.XXXXXX") || {
        error "Could not create temporary mount directory"
        return 1
    }

    # Cleanup: unmount (if still mounted) and delete the temp folder.
    # Runs on Ctrl+C (INT), TERM and on a regular EXIT.
    local _cleaned=false
    _cleanup_mount() {
        $_cleaned && return 0
        _cleaned=true
        echo
        info "Ending mount, unmounting ..."
        if mountpoint -q "$mnt"; then
            fusermount -u "$mnt" 2>/dev/null \
                || fusermount3 -u "$mnt" 2>/dev/null \
                || umount "$mnt" 2>/dev/null \
                || warn "Unmounting ${mnt} failed - do it manually if needed: umount ${mnt}"
        fi
        rmdir "$mnt" 2>/dev/null || rm -rf "$mnt" 2>/dev/null || true
        info "Temporary directory removed."
    }
    trap '_cleanup_mount' INT TERM EXIT

    info "Mounting snapshot ${snap_id} at:"
    info "    ${mnt}"
    info ""
    info "The snapshot content is located at:"
    info "    ${mnt}/ids/${snap_id}/"
    info "(all snapshots are also available under ${mnt}/snapshots/ and ${mnt}/hosts/)"
    info ""
    info "Open a SECOND console to copy files, e.g.:"
    info "    cp -a ${mnt}/ids/${snap_id}/. /target/path/"
    info ""
    info ">>> Mount is running in the foreground - press Ctrl+C to stop. <<<"
    info ""

    # Blocks until Ctrl+C; the trap takes over afterwards.
    # Only the chosen snapshot is of interest (--tag is not unique here, so it
    # is reached through the short-id in the ids/ tree).
    local _rc=0
    restic mount "$mnt" || _rc=$?
    # 130 = terminated by SIGINT (Ctrl+C) -> the normal case, not an error.
    if [[ "$_rc" -ne 0 && "$_rc" -ne 130 ]]; then
        error "restic mount failed (exit code ${_rc})"
        _cleanup_mount
        trap - INT TERM EXIT
        return 1
    fi
    # Nach normalem Ende (falls restic selbst zurueckkehrt) ebenfalls aufraeumen.
    _cleanup_mount
    trap - INT TERM EXIT
    return 0
}

# ================== Main ==================

ACTION=""
SRC_ID=""
SRC_TYPE=""      # vm | ct - determined automatically
DST_ID=""
SNAPSHOT_ID=""
DATE_FILTER=""
DRY_RUN=false
SHOW_SIZE=false
FILTER_TYPE=""
FILTER_NAME=""
FILTER_CONFIG=""

# --opt=value Schreibweise in --opt value normalisieren
_args=()
for _a in "$@"; do
    case "$_a" in
        --*=*) _args+=("${_a%%=*}" "${_a#*=}") ;;
        *)     _args+=("$_a") ;;
    esac
done
set -- ${_args[@]+"${_args[@]}"}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --list)       ACTION="list"; shift ;;
        --list-disks) ACTION="list-disks"; [[ $# -lt 2 ]] && { error "--list-disks requires a VMID"; exit 1; }; SRC_ID="$2"; shift 2 ;;
        --machine)    [[ $# -lt 2 ]] && { error "--machine requires an ID"; exit 1; }; SRC_ID="$2"; shift 2 ;;
        --target)     [[ $# -lt 2 ]] && { error "--target requires an ID"; exit 1; }; DST_ID="$2"; shift 2 ;;
        --snap)       [[ $# -lt 2 ]] && { error "--snap requires a snapshot ID"; exit 1; }; SNAPSHOT_ID="$2"; shift 2 ;;
        --date)       [[ $# -lt 2 ]] && { error "--date requires a date (YYYY-MM-DD)"; exit 1; }; DATE_FILTER="$2"; shift 2 ;;
        --type)       [[ $# -lt 2 ]] && { error "--type requires a value"; exit 1; }; FILTER_TYPE="$2"; shift 2 ;;
        --name)       [[ $# -lt 2 ]] && { error "--name requires a value"; exit 1; }; FILTER_NAME="$2"; shift 2 ;;
        --config)     [[ $# -lt 2 ]] && { error "--config requires a snapshot ID"; exit 1; }; FILTER_CONFIG="$2"; shift 2 ;;
        --dry-run)    DRY_RUN=true; shift ;;
        --size)       SHOW_SIZE=true; shift ;;
        --help|-h)    show_usage; exit 0 ;;
        *) error "Unknown option: $1"; show_usage; exit 1 ;;
    esac
done

# Without --list / --list-disks it is a restore request as soon as a
# selection (--machine or --snap) was given
if [[ -z "$ACTION" ]]; then
    if [[ -n "$SNAPSHOT_ID" || -n "$SRC_ID" ]]; then
        ACTION="restore"
    else
        show_usage
        exit 1
    fi
fi

for cmd in restic qm pct pvesm jq mountpoint; do
    command -v "$cmd" &>/dev/null || { error "Missing: ${cmd}"; exit 1; }
done
# zfs is only needed for ZFS restores - optional on pure LVM/dir hosts
command -v zfs &>/dev/null || warn "zfs not found - only RAW and file-based restores are possible"

# --- Make sure the repository is reachable ---
# If opening the repo fails: try all UUIDs from the config until a medium with
# a readable repo is found (same as ResticBackup.sh). An already mounted but
# unusable medium is unmounted for that.

# Mounts all UUIDs one after another; only accepts media whose repo can
# actually be opened (for a restore a fresh medium without a repo is
# useless)
try_mount_repo() {
    local _uuid
    for _uuid in ${UUIDS[@]+"${UUIDS[@]}"}; do
        mount "UUID=${_uuid}" "${RESTIC_BASEFOLDER}" ${MOUNTOPT[@]+"${MOUNTOPT[@]}"} 2>/dev/null || true
        mountpoint -q "${RESTIC_BASEFOLDER}" || continue
        if restic cat config &>/dev/null; then
            info "Backup medium mounted (UUID ${_uuid})"
            return 0
        fi
        warn "UUID ${_uuid}: no readable restic repo on this medium - trying the next one"
        umount "${RESTIC_BASEFOLDER}" 2>/dev/null || true
    done
    return 1
}

if ! restic cat config &>/dev/null; then
    info "Repository ${RESTIC_REPOSITORY} not reachable - trying to mount backup media"
    mkdir -p "${RESTIC_BASEFOLDER}" 2>/dev/null || true
    if mountpoint -q "${RESTIC_BASEFOLDER}"; then
        warn "The existing mount holds no readable repo - unmounting and trying all UUIDs"
        if ! umount "${RESTIC_BASEFOLDER}"; then
            error "Unmounting ${RESTIC_BASEFOLDER} failed (in use?)"
            exit 1
        fi
    fi
    if ! try_mount_repo; then
        error "Repository could not be opened on any of the configured media."
        error "Possible causes:"
        error "  - backup medium not attached, or UUIDs in ${config} wrong/empty"
        error "  - password file missing: ${RESTIC_PASSWORDFILE}"
        error "    (created by ResticBackup.sh on its first run)"
        exit 1
    fi
fi

# --- Clear a leftover repository lock ---
# A run that was killed leaves a lock behind and every later restic call fails
# with "repository is already locked". 'restic unlock' removes exactly the
# locks that are no longer being refreshed.
# Deliberately WITHOUT --remove-all here: unlike ResticBackup.sh this script
# holds no flock, so a backup could legitimately be running in parallel and
# --remove-all would rip its lock away mid-run.
clear_stale_repo_lock() {
    restic list locks 2>/dev/null | grep -q . || return 0
    warn "Lock found in the repository (previous run aborted?) - removing stale locks"
    restic unlock &>/dev/null || true
    if restic list locks 2>/dev/null | grep -q .; then
        warn "A lock is still held - another restic run may be active."
        warn "If you are sure nothing is running: restic unlock --remove-all"
    else
        info "Stale lock removed"
    fi
}
clear_stale_repo_lock

# --- List snapshots ---
if [[ "$ACTION" == "list" ]]; then
    info "Snapshots in ${RESTIC_REPOSITORY}:"
    print_snapshot_table || warn "No snapshots found (matching the given filters)"
    exit 0
fi

if [[ "$ACTION" == "list-disks" ]]; then
    show_disk_status "$SRC_ID"
    exit 0
fi

if [[ -z "$ACTION" ]]; then
    show_usage
    exit 1
fi

# --- pv is only needed for the actual data transfer of a restore ---
# Installing it in the script header would run apt-get on every --help and
# --list call as well.
if ! command -v pv &>/dev/null; then
    echo "pv not found. Starting installation..."
    apt-get update && apt-get install -y pv \
        || echo "WARNING: installing pv failed, restore runs without a progress display"
fi

# --- Special case: --snap points at a LOCAL/dir snapshot ---
# A LOCAL snapshot belongs to no VM/CT config. With --snap on such a snapshot
# the intent is unambiguous: provide the directory content. With --target
# <DIR> exactly this snapshot is extracted there, without --target it is
# mounted via FUSE into a foreground temp directory.
if [[ -n "$SNAPSHOT_ID" ]]; then
    _snap_json=$(restic snapshots --json "$SNAPSHOT_ID" 2>/dev/null) || _snap_json=""
    if [[ -z "$_snap_json" || "$_snap_json" == "[]" || "$_snap_json" == "null" ]]; then
        error "Snapshot ${SNAPSHOT_ID} not found in repository"
        exit 1
    fi
    _snap_tags=$(echo "$_snap_json" | jq -r '.[0].tags[]?' 2>/dev/null) || _snap_tags=""
    if grep -qx "local" <<< "$_snap_tags" && ! grep -qx "config" <<< "$_snap_tags"; then
        # normalize to the short_id for clean messages
        _local_sid=$(echo "$_snap_json" | jq -r '.[0].short_id // empty' 2>/dev/null) || _local_sid=""
        [[ -z "$_local_sid" ]] && _local_sid="$SNAPSHOT_ID"
        info "Snapshot ${_local_sid} is a LOCAL/dir backup (no VM/CT config)."
        if [[ -n "$DST_ID" ]]; then
            info "Extracting it directly into the target directory."
        else
            info "No --target given -> mounting it in the foreground."
        fi
        # With --target <DIR>: extract. Without: FUSE mount in the foreground.
        mount_snapshot "$_local_sid" "$DST_ID"
        exit $?
    fi
fi

# --- Restore-Vorbereitung ---

# --target is mandatory: it prevents accidental restores when the intention
# was only to filter with --machine/--snap
if [[ -z "$DST_ID" ]]; then
    error "A --target <ID> is mandatory for a restore."
    error "Use --list to show available backups."
    exit 1
fi

# Safety rule: existing machines are NEVER overwritten.
# There is deliberately no override option - delete the machine manually
# (qm destroy / pct destroy) or choose a different --target ID.
machine_exists() {
    qm config "$1" &>/dev/null || pct config "$1" &>/dev/null \
        || [[ -f "/etc/pve/qemu-server/$1.conf" || -f "/etc/pve/lxc/$1.conf" ]]
}
if machine_exists "$DST_ID"; then
    if $DRY_RUN; then
        warn "Target ID ${DST_ID} already exists - a real restore would ABORT here"
    else
        error "Target ID ${DST_ID} already exists - restore aborted."
        error "Existing machines are never overwritten. Options:"
        error "  1) delete the machine manually (qm destroy ${DST_ID} / pct destroy ${DST_ID})"
        error "  2) choose a different target ID: --target <free ID>"
        exit 1
    fi
fi

FIND_ARGS=()
if [[ -n "$DATE_FILTER" ]]; then
    info "Point-in-time restore: ${DATE_FILTER}"
    FIND_ARGS=(--before "$DATE_FILTER")
fi

# Resolve config snapshot + machine
CFG_SNAP=""
if [[ -n "$SNAPSHOT_ID" ]]; then
    [[ -n "$DATE_FILTER" ]] && warn "--date is ignored, --snap already selects the backup run unambiguously"
    FIND_ARGS=()
    _snap_json=$(restic snapshots --json "$SNAPSHOT_ID" 2>/dev/null) || _snap_json=""
    if [[ -z "$_snap_json" || "$_snap_json" == "[]" || "$_snap_json" == "null" ]]; then
        error "Snapshot ${SNAPSHOT_ID} not found in repository"
        exit 1
    fi
    _snap_tags=$(echo "$_snap_json" | jq -r '.[0].tags[]?' 2>/dev/null) || _snap_tags=""
    if ! grep -qx "config" <<< "$_snap_tags"; then
        error "Snapshot ${SNAPSHOT_ID} is not a config snapshot (TYPE=CONFIG in --list)."
        error "Pass the SNAP ID of a CONFIG row to --snap - the matching"
        error "disks are found automatically via their config-id tag."
        exit 1
    fi
    _machine_tag=$(grep -E '^(vm|ct)-[0-9]+$' <<< "$_snap_tags" | head -1) || _machine_tag=""
    if [[ -z "$_machine_tag" ]]; then
        error "Snapshot ${SNAPSHOT_ID} carries no vm-<id>/ct-<id> tag"
        exit 1
    fi
    _snap_src="${_machine_tag#*-}"
    SRC_TYPE="${_machine_tag%%-*}"
    if [[ -n "$SRC_ID" && "$SRC_ID" != "$_snap_src" ]]; then
        error "--machine ${SRC_ID} contradicts --snap ${SNAPSHOT_ID} (belongs to ${_machine_tag})"
        exit 1
    fi
    SRC_ID="$_snap_src"
    # Normalize to the short_id - the config-id tag of the disks contains the
    # short_id, not the long ID
    CFG_SNAP=$(echo "$_snap_json" | jq -r '.[0].short_id // empty' 2>/dev/null) || CFG_SNAP=""
    [[ -z "$CFG_SNAP" ]] && CFG_SNAP="$SNAPSHOT_ID"
else
    # Only --machine: determine the type automatically (vm or ct)
    if find_snap_by_tag "vm-${SRC_ID}" "config" "${FIND_ARGS[@]}"; then
        SRC_TYPE="vm"
        CFG_SNAP="$FOUND_SNAP_ID"
    elif find_snap_by_tag "ct-${SRC_ID}" "config" "${FIND_ARGS[@]}"; then
        SRC_TYPE="ct"
        CFG_SNAP="$FOUND_SNAP_ID"
    else
        if [[ -n "$DATE_FILTER" ]]; then
            error "No config snapshot found for machine ${SRC_ID} on or before ${DATE_FILTER}"
        else
            error "No config snapshot found for machine ${SRC_ID}"
        fi
        exit 1
    fi
fi

info "Machine: ${SRC_TYPE}-${SRC_ID} -> Target: ${DST_ID}"
info "Config snapshot: ${CFG_SNAP}"

# Map disks/filesystems strictly to the chosen backup run
CONFIG_ID_FILTER="$CFG_SNAP"
info "Restoring backup run config-id=${CONFIG_ID_FILTER}"

# =================== VM Restore ===================
if [[ "$SRC_TYPE" == "vm" ]]; then
    info "=== Restoring VM ${SRC_ID} -> ${DST_ID} ==="
    restore_config "$SRC_ID" "$DST_ID" "vm" "$CFG_SNAP" || exit 1

    DISK_REGEX='^(ide[0-3]|sata[0-5]|scsi[0-9]+|virtio[0-9]+|efidisk[0-9]+|tpmstate[0-9]+):'
    CONFIG_FILE="/etc/pve/qemu-server/${DST_ID}.conf"
    info "Restoring disks ..."
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        key="${line%%:*}"
        value="${line#*:}"
        value="${value#"${value%%[![:space:]]*}"}"
        volume="${value%%,*}"
        volume="${volume#file=}"
        [[ -z "$volume" || "$volume" == "none" ]] && continue
        dst_volume=$(echo "$volume" | sed "s/vm-${SRC_ID}-/vm-${DST_ID}-/g")
        restore_vm_disk "$SRC_ID" "$key" "$dst_volume" "${FIND_ARGS[@]}" || true
    done < <(awk -v re="$DISK_REGEX" '$0 ~ re {print}' "$CONFIG_FILE" 2>/dev/null)

    info "=== VM ${DST_ID} restore complete ==="
    if ! $DRY_RUN; then
        info "Start with: qm start ${DST_ID}"
    fi
fi

# =================== LXC Restore ===================
if [[ "$SRC_TYPE" == "ct" ]]; then
    info "=== Restoring LXC ${SRC_ID} -> ${DST_ID} ==="
    restore_config "$SRC_ID" "$DST_ID" "ct" "$CFG_SNAP" || exit 1

    CONFIG_FILE="/etc/pve/lxc/${DST_ID}.conf"
    info "Restoring filesystems ..."
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        key="${line%%:*}"
        value="${line#*:}"
        value="${value#"${value%%[![:space:]]*}"}"
        volume="${value%%,*}"
        volume="${volume#file=}"
        [[ -z "$volume" || "$volume" == "none" ]] && continue
        dst_volume=$(echo "$volume" \
            | sed -e "s/subvol-${SRC_ID}-/subvol-${DST_ID}-/g" \
                  -e "s/vm-${SRC_ID}-/vm-${DST_ID}-/g")
        restore_lxc_fs "$SRC_ID" "$key" "$dst_volume" "${FIND_ARGS[@]}" || true
    done < <(awk '/^rootfs:/{print}' "$CONFIG_FILE" 2>/dev/null)

    info "=== LXC ${DST_ID} restore complete ==="
    if ! $DRY_RUN; then
        info "Start with: pct start ${DST_ID}"
    fi
fi

