#!/bin/sh
#
# scan_babuk_encrypted.sh
# ESXi ash helper for Babuk recovery (verbose, multi-round).
#
# ONLY these extensions are scanned/backed up/renamed after PRE:
#   .vmdk  .vmem  .vswp  .vmsn  (+ .babyk forms)
# .log is intentionally EXCLUDED from scan rounds (noisy / low value).
# PRE-pass still runs d_esxi.out once on roots so leftover *.log.babyk
# can be decrypted by the decryptor itself in that first sweep.
#
# Usage:
#   sh /tmp/scan_babuk_encrypted.sh
#   sh /tmp/scan_babuk_encrypted.sh /path
#   sh /tmp/scan_babuk_encrypted.sh scan [/path]
#   sh /tmp/scan_babuk_encrypted.sh dec  [/path]
#
# Workflow (MAX_ROUNDS=3):
#   PRE) run d_esxi.out on initial roots (default: whole /vmfs/volumes)
#   Round 1: full scan under initial roots (VM disk suffixes only)
#   Round 2+: only re-scan/decrypt parent dirs that still had encrypted hits
#   Each round:
#     A) scan  B) unique bak  C) rename .babyk
#     D) stash enc-bak + decrypt parents  E) rescan then narrow roots
#



set -u

# Default empty; resolved after args (script-dir first, then /tmp)
DECRYPTOR="${DECRYPTOR:-}"
BACKUP_DIRNAME="${BACKUP_DIRNAME:-enc-bak}"
ROOT_DEFAULT="/vmfs/volumes"
MODE="auto"
MAX_ROUNDS="${MAX_ROUNDS:-3}"
SCAN_ROOTS=""

LOG_FILE="/tmp/babuk_scan.log"
COUNT_FILE="/tmp/babuk_last_count.txt"
BAKPATH_FILE="/tmp/babuk_last_bak_path.txt"
BABYK_FILE="/tmp/babuk_last_babyk_path.txt"

ts() {
    date 2>/dev/null || echo unknown-time
}

log() {
    _msg="$1"
    echo "$_msg"
    echo "$(ts) $_msg" >> "$LOG_FILE" 2>/dev/null
}

usage() {
    cat <<'EOF'
Usage:
  sh scan_babuk_encrypted.sh [scan|dec|auto] [path ...]

  auto|dec   3-round workflow (default)
  scan       one-pass encrypted list only

Only these suffixes are scanned (logs excluded):
  .vmdk .vmem .vswp .vmsn  (+ .babyk forms)
PRE-pass still runs decryptor once on roots (can touch logs there).

Decryptor resolution order:
  1) $DECRYPTOR if set and exists
  2) d_esxi.out next to this script
  3) /tmp/d_esxi.out
  (missing -> exit 1; found -> chmod +x)

Env:
  DECRYPTOR=/path/to/d_esxi.out
  MAX_ROUNDS=3
  BACKUP_DIRNAME=enc-bak
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
        -h|--help) usage; exit 0 ;;
        scan|SCAN|--scan) MODE="scan"; shift ;;
        dec|DEC|--dec|auto|AUTO|--auto) MODE="auto"; shift ;;
        -*)
            echo "Unknown option: $1" >&2
            usage >&2
            exit 2
            ;;
        *)
            if [ -z "$SCAN_ROOTS" ]; then
                SCAN_ROOTS="$1"
            else
                SCAN_ROOTS="$SCAN_ROOTS $1"
            fi
            shift
            ;;
    esac
done

if [ -z "$SCAN_ROOTS" ]; then
    SCAN_ROOTS="$ROOT_DEFAULT"
fi

# Absolute directory of this script (works with: sh /path/scan_babuk_encrypted.sh)
SCRIPT_DIR=$(dirname -- "$0")
SCRIPT_DIR=$(cd -- "$SCRIPT_DIR" 2>/dev/null && pwd)
if [ -z "$SCRIPT_DIR" ]; then
    SCRIPT_DIR=$(dirname -- "$0")
fi

resolve_decryptor() {
    # Prefer explicit env if it points to an existing file
    if [ -n "${DECRYPTOR}" ] && [ -f "${DECRYPTOR}" ]; then
        echo "${DECRYPTOR}"
        return 0
    fi
    if [ -n "${DECRYPTOR}" ] && [ ! -f "${DECRYPTOR}" ]; then
        echo "WARN: DECRYPTOR set but not found: ${DECRYPTOR}" >&2
    fi

    if [ -f "$SCRIPT_DIR/d_esxi.out" ]; then
        echo "$SCRIPT_DIR/d_esxi.out"
        return 0
    fi
    if [ -f "/tmp/d_esxi.out" ]; then
        echo "/tmp/d_esxi.out"
        return 0
    fi
    return 1
}

_dec=$(resolve_decryptor) || {
    echo "ERROR: d_esxi.out not found." >&2
    echo "  looked: \$DECRYPTOR, $SCRIPT_DIR/d_esxi.out, /tmp/d_esxi.out" >&2
    echo "  place d_esxi.out next to this script or in /tmp, then re-run." >&2
    exit 1
}
DECRYPTOR="$_dec"

# Ensure executable bit before any run
chmod +x "$DECRYPTOR" 2>/dev/null || true
if [ ! -x "$DECRYPTOR" ]; then
    # some FS ignore +x; still try later, but warn
    echo "WARN: chmod +x failed or not executable: $DECRYPTOR" >&2
fi

: > "$LOG_FILE"

log "============================================================"
log "Babuk multi-round recovery script"
log "Time:       $(ts)"
log "Mode:       $MODE"
log "Rounds:     $MAX_ROUNDS"
log "Script dir: $SCRIPT_DIR"
log "Decryptor:  $DECRYPTOR"
log "Backup dir: $BACKUP_DIRNAME (stashed while decryptor runs)"
log "Scan roots: $SCAN_ROOTS"
log "Scan suffixes: .vmdk .vmem .vswp .vmsn (+ .babyk)  [NO .log]"
log "Note: .log not scanned after PRE; PRE d_esxi.out may still decrypt logs"
log "Log file:   $LOG_FILE"
log "============================================================"

head_hex() {
    dd if="$1" bs=1 count="$2" 2>/dev/null | hexdump -v -e '1/1 "%02x"'
}

byte_hex() {
    dd if="$1" bs=1 skip="$2" count=1 2>/dev/null | hexdump -v -e '1/1 "%02x"'
}

slice_hex() {
    dd if="$1" bs=1 skip="$2" count="$3" 2>/dev/null | hexdump -v -e '1/1 "%02x"'
}

file_size() {
    if stat -c%s "$1" >/dev/null 2>&1; then
        stat -c%s "$1"
    else
        ls -l "$1" 2>/dev/null | awk '{print $5}'
    fi
}

md5_head8() {
    _f="$1"
    _sz=$(file_size "$_f")
    case "$_sz" in ''|*[!0-9]*) echo "00000000"; return 0 ;; esac
    if [ "$_sz" -le 1048576 ]; then
        md5sum "$_f" 2>/dev/null | awk '{print substr($1,1,8)}'
    else
        dd if="$_f" bs=1M count=1 2>/dev/null | md5sum | awk '{print substr($1,1,8)}'
    fi
}

# Scan/backup whitelist. .log intentionally omitted (user: only PRE bulk decrypt).
# return 0 = is target
is_babuk_target_name() {
    _b=$(basename "$1")
    case "$_b" in
        *.vmdk|*.VMDK) return 0 ;;
        *.vmdk.babyk|*.VMDK.babyk|*.vmdk.BABYK|*.VMDK.BABYK) return 0 ;;
        *.vmem|*.VMEM) return 0 ;;
        *.vmem.babyk|*.VMEM.babyk|*.vmem.BABYK|*.VMEM.BABYK) return 0 ;;
        *.vswp|*.VSWP) return 0 ;;
        *.vswp.babyk|*.VSWP.babyk|*.vswp.BABYK|*.VSWP.BABYK) return 0 ;;
        *.vmsn|*.VMSN) return 0 ;;
        *.vmsn.babyk|*.VMSN.babyk|*.vmsn.BABYK|*.VMSN.BABYK) return 0 ;;
    esac
    return 1
}


is_vmfs_system_file() {
    _b=$(basename "$1")
    case "$_b" in
        .fbb.sf|.fdc.sf|.jbc.sf|.pb2.sf|.pbc.sf|.sbc.sf|.vh.sf|.sdd.sf) return 0 ;;
        .*.sf) return 0 ;;
    esac
    case "$1" in
        */.sdd.sf|*/.sdd.sf/*) return 0 ;;
    esac
    return 1
}

is_skip_noise_file() {
    _b=$(basename "$1")
    case "$_b" in
        *.dumpfile|*.DUMPFILE) return 0 ;;
    esac
    case "$1" in
        */vmkdump/*|*/vmkdump) return 0 ;;
        */BOOTBANK1/*|*/BOOTBANK2/*) return 0 ;;
        */.vSphere-HA/*) return 0 ;;
    esac
    return 1
}

# 0 = encrypted/suspect, 1 = plaintext
is_encrypted() {
    _f="$1"
    _base=$(basename "$_f")

    case "$_base" in
        *.babyk|*.BABYK) return 0 ;;
    esac

    _h32=$(head_hex "$_f" 32)
    [ -n "$_h32" ] || return 0

    case "$_h32" in
        4b444d56*) return 1 ;; # KDMV sparse vmdk
        434f5744*) return 1 ;; # COWD
        23204469736b*|23204449534b*|2320*) return 1 ;; # # Disk descriptor / #
        00000000000000000000000000000000*) return 1 ;;
    esac

    # text log / mostly printable
    _nums=$(dd if="$_f" bs=1 count=32 2>/dev/null | hexdump -v -e '1/1 "%d "')
    _ok=0
    _n=0
    for _b in $_nums; do
        _n=$((_n + 1))
        if [ "$_b" -eq 9 ] || [ "$_b" -eq 10 ] || [ "$_b" -eq 13 ] || \
           { [ "$_b" -ge 32 ] && [ "$_b" -le 126 ]; }; then
            _ok=$((_ok + 1))
        fi
    done
    if [ "$_n" -gt 0 ] && [ "$_ok" -ge $((_n * 9 / 10)) ]; then
        return 1
    fi

    # MBR / GPT / ext4 on flat vmdk
    _b510=$(byte_hex "$_f" 510)
    _b511=$(byte_hex "$_f" 511)
    if [ "$_b510" = "55" ] && [ "$_b511" = "aa" ]; then
        return 1
    fi
    _gpt=$(slice_hex "$_f" 512 8)
    if [ "$_gpt" = "4546492050415254" ]; then
        return 1
    fi
    _m1=$(slice_hex "$_f" 1049656 2)
    if [ "$_m1" = "53ef" ]; then
        return 1
    fi
    _m2=$(slice_hex "$_f" 2098232 2)
    if [ "$_m2" = "53ef" ]; then
        return 1
    fi

    return 0
}

# return 0 = skip
should_skip_path() {
    _f="$1"

    if ! is_babuk_target_name "$_f"; then
        return 0
    fi

    case "$_f" in
        */d_esxi.out|*/scan_babuk_encrypted.sh) return 0 ;;
        */"$BACKUP_DIRNAME"/*|*/"$BACKUP_DIRNAME") return 0 ;;
        */.sdd.sf/*|*/.sdd.sf) return 0 ;;
        */.babuk-enc-bak-stash*|*/.babuk-enc-bak-stash*/*) return 0 ;;
    esac

    if is_vmfs_system_file "$_f"; then
        return 0
    fi
    if is_skip_noise_file "$_f"; then
        return 0
    fi
    return 1
}

stash_enc_bak() {
    _dir="$1"
    _stash_out="$2"
    : > "$_stash_out"
    _bak="$_dir/$BACKUP_DIRNAME"
    if [ ! -d "$_bak" ]; then
        log "[STASH] no $BACKUP_DIRNAME under $_dir"
        return 0
    fi
    _parent=$(dirname "$_dir")
    _base=$(basename "$_dir")
    _stash="$_parent/.babuk-enc-bak-stash-${_base}"
    _n=1
    while [ -e "$_stash" ]; do
        _stash="$_parent/.babuk-enc-bak-stash-${_base}.$_n"
        _n=$((_n + 1))
    done
    log "[STASH] move $_bak -> $_stash"
    if mv "$_bak" "$_stash"; then
        echo "$_stash" > "$_stash_out"
        log "[STASH] OK"
        return 0
    fi
    log "[STASH] FAIL"
    return 1
}

restore_enc_bak() {
    _dir="$1"
    _stash_file="$2"
    _bak="$_dir/$BACKUP_DIRNAME"
    if [ ! -s "$_stash_file" ]; then
        return 0
    fi
    _stash=$(cat "$_stash_file")
    if [ ! -d "$_stash" ]; then
        log "[STASH-RESTORE] missing: $_stash"
        return 1
    fi
    if [ -e "$_bak" ]; then
        log "[STASH-RESTORE] $_bak exists; leave stash at $_stash"
        return 1
    fi
    log "[STASH-RESTORE] move $_stash -> $_bak"
    if mv "$_stash" "$_bak"; then
        log "[STASH-RESTORE] OK"
        return 0
    fi
    log "[STASH-RESTORE] FAIL"
    return 1
}

run_decryptor_on_dir() {
    _dir="$1"
    if [ ! -d "$_dir" ]; then
        log "[DEC-SKIP] not a directory: $_dir"
        return 1
    fi
    if [ ! -f "$DECRYPTOR" ]; then
        log "[DEC-ERROR] decryptor missing: $DECRYPTOR"
        return 1
    fi
    chmod +x "$DECRYPTOR" 2>/dev/null || true

    _stash_file="/tmp/babuk_stash_$(echo "$_dir" | md5sum | awk '{print $1}').txt"
    stash_enc_bak "$_dir" "$_stash_file"

    log "[DEC] >>> START decryptor"
    log "[DEC] cmd: $DECRYPTOR $_dir/"
    log "[DEC] dir: $_dir/"
    _rc=0
    if "$DECRYPTOR" "$_dir/"; then
        log "[DEC] <<< OK: $_dir/"
    else
        log "[DEC] <<< FAIL: $_dir/"
        _rc=1
    fi
    restore_enc_bak "$_dir" "$_stash_file"
    return $_rc
}


run_decryptor_on_roots() {
    log "[DEC] === decrypt pass on scan roots ==="
    for _root in $SCAN_ROOTS; do
        if [ -d "$_root" ]; then
            log "[DEC] root: $_root"
            run_decryptor_on_dir "$_root"
        else
            log "[DEC-SKIP] missing root: $_root"
        fi
    done
}

run_decryptor_on_dir_list() {
    _list="$1"
    if [ ! -s "$_list" ]; then
        log "[DEC] no parent dirs queued"
        return 0
    fi
    log "[DEC] === decrypt queued parents ==="
    _i=0
    while IFS= read -r _dir; do
        [ -n "$_dir" ] || continue
        _i=$((_i + 1))
        log "[DEC] queued #${_i}: $_dir"
        run_decryptor_on_dir "$_dir"
    done < "$_list"
}

backup_unique() {
    _src="$1"
    _round="$2"
    _parent=$(dirname "$_src")
    _base=$(basename "$_src")
    _bakdir="$_parent/$BACKUP_DIRNAME"
    _sz=$(file_size "$_src")
    case "$_sz" in ''|*[!0-9]*) _sz=0 ;; esac

    log "[BAK] >>> START backup"
    log "[BAK] source: $_src"
    log "[BAK] fingerprint..."
    _fp=$(md5_head8 "$_src")
    [ -n "$_fp" ] || _fp="unknown"
    _dst="$_bakdir/${_base}.r${_round}.s${_sz}.h${_fp}"
    log "[BAK] round=${_round} size=${_sz} fp=${_fp}"
    log "[BAK] dest: $_dst"

    if [ ! -d "$_bakdir" ]; then
        log "[BAK] mkdir $_bakdir"
        mkdir -p "$_bakdir" || { log "[BAK] <<< FAIL mkdir"; return 1; }
    fi

    if [ -f "$_dst" ]; then
        log "[BAK] <<< SKIP same unique name exists"
        echo "$_dst" > "$BAKPATH_FILE"
        return 0
    fi

    log "[BAK] copying..."
    if cp -p "$_src" "$_dst" 2>/tmp/babuk_cp_err.txt; then
        log "[BAK] <<< OK -> $_dst"
        echo "$_dst" > "$BAKPATH_FILE"
        return 0
    fi
    _err=$(cat /tmp/babuk_cp_err.txt 2>/dev/null)
    log "[BAK] <<< FAIL: $_err"
    return 1
}

ensure_babyk() {
    _src="$1"
    case "$_src" in
        *.babyk|*.BABYK)
            log "[REN] already .babyk: $_src"
            echo "$_src" > "$BABYK_FILE"
            return 0
            ;;
    esac
    _dst="${_src}.babyk"
    log "[REN] >>> mv to .babyk"
    log "[REN] from: $_src"
    log "[REN] to:   $_dst"
    if [ -e "$_dst" ]; then
        log "[REN] <<< FAIL target exists"
        return 1
    fi
    if [ ! -w "$_src" ]; then
        log "[REN] <<< FAIL not writable"
        return 1
    fi
    if mv "$_src" "$_dst" 2>/tmp/babuk_mv_err.txt; then
        log "[REN] <<< OK $_dst"
        echo "$_dst" > "$BABYK_FILE"
        return 0
    fi
    _err=$(cat /tmp/babuk_mv_err.txt 2>/dev/null)
    log "[REN] <<< FAIL: $_err"
    return 1
}

remember_dir() {
    _d="$1"
    _list="$2"
    if ! grep -F "$_d" "$_list" >/dev/null 2>&1; then
        echo "$_d" >> "$_list"
        log "[QUEUE] add parent: $_d"
    else
        log "[QUEUE] already queued: $_d"
    fi
}

scan_encrypted() {
    _outlist="$1"
    _round_label="$2"
    : > "$_outlist"
    _find_out="/tmp/babuk_find_paths.txt"

    # ACTIVE_ROOTS: full tree on round1 / user path; later narrowed to hot parents
    if [ -z "${ACTIVE_ROOTS:-}" ]; then
        ACTIVE_ROOTS="$SCAN_ROOTS"
    fi

    log "[SCAN] === START (${_round_label}) ==="
    log "[SCAN] active roots: $ACTIVE_ROOTS"
    log "[SCAN] only: .vmdk .vmem .vswp .vmsn (+ .babyk)  [NO .log]"


    for _root in $ACTIVE_ROOTS; do


        find "$_root" \
            \( -type d -name "$BACKUP_DIRNAME" -prune \) -o \
            \( -type d -name '.sdd.sf' -prune \) -o \
            \( -type d -name 'vmkdump' -prune \) -o \
            \( -type d -name 'BOOTBANK1' -prune \) -o \
            \( -type d -name 'BOOTBANK2' -prune \) -o \
            \( -type d -name '.babuk-enc-bak-stash-*' -prune \) -o \
            -type f \( \
                -name '*.vmdk' -o -name '*.VMDK' -o \
                -name '*.vmdk.babyk' -o -name '*.VMDK.babyk' -o -name '*.vmdk.BABYK' -o -name '*.VMDK.BABYK' -o \
                -name '*.vmem' -o -name '*.VMEM' -o \
                -name '*.vmem.babyk' -o -name '*.VMEM.babyk' -o -name '*.vmem.BABYK' -o -name '*.VMEM.BABYK' -o \
                -name '*.vswp' -o -name '*.VSWP' -o \
                -name '*.vswp.babyk' -o -name '*.VSWP.babyk' -o -name '*.vswp.BABYK' -o -name '*.VSWP.BABYK' -o \
                -name '*.vmsn' -o -name '*.VMSN' -o \
                -name '*.vmsn.babyk' -o -name '*.VMSN.babyk' -o -name '*.vmsn.BABYK' -o -name '*.VMSN.BABYK' \
            \) -print 2>/dev/null > "$_find_out"

        _total=$(wc -l < "$_find_out" 2>/dev/null)
        _total=$(echo $_total)
        [ -n "$_total" ] || _total=0
        log "[SCAN] suffix candidates: ${_total}"


        while IFS= read -r _path; do
            [ -n "$_path" ] || continue

            if should_skip_path "$_path"; then
                continue
            fi
            if [ ! -f "$_path" ] || [ ! -r "$_path" ]; then
                continue
            fi
            _sz=$(file_size "$_path")
            case "$_sz" in ''|*[!0-9]*) continue ;; esac
            if [ "$_sz" -lt 1 ]; then
                continue
            fi

            log "[SCAN] check: $_path (size=${_sz})"
            if is_encrypted "$_path"; then
                log "[SCAN] HIT encrypted: $_path"
                echo "$_path" >> "$_outlist"
            else
                log "[SCAN] ok plaintext: $_path"
            fi
        done < "$_find_out"
    done

    _count=$(wc -l < "$_outlist" 2>/dev/null)
    _count=$(echo $_count)
    [ -n "$_count" ] || _count=0
    echo "$_count" > "$COUNT_FILE"
    log "[SCAN] === DONE (${_round_label}): encrypted=${_count} ==="
    if [ "$_count" -gt 0 ]; then
        log "[SCAN] list:"
        while IFS= read -r _line; do
            log "[SCAN]   * $_line"
        done < "$_outlist"
    fi
}

process_backup_and_queue() {
    _inlist="$1"
    _round="$2"
    _dirlist="$3"
    : > "$_dirlist"
    _n=0
    _ok=0
    _fail=0

    log "[WORK] === START backup+rename round=${_round} ==="
    if [ ! -s "$_inlist" ]; then
        log "[WORK] nothing to process"
        return 0
    fi

    while IFS= read -r _f; do
        [ -n "$_f" ] || continue
        _n=$((_n + 1))
        log "[WORK] -------- item #${_n} --------"
        log "[WORK] file: $_f"

        if [ ! -f "$_f" ]; then
            log "[WORK] SKIP disappeared"
            continue
        fi
        if [ ! -w "$_f" ]; then
            log "[WORK] SKIP not writable (busy?): $_f"
            _fail=$((_fail + 1))
            continue
        fi
        if is_skip_noise_file "$_f"; then
            log "[WORK] SKIP noise: $_f"
            continue
        fi

        if ! backup_unique "$_f" "$_round"; then
            log "[WORK] FAIL backup: $_f"
            _fail=$((_fail + 1))
            continue
        fi
        _bakpath=$(cat "$BAKPATH_FILE" 2>/dev/null)
        log "[WORK] backup: $_bakpath"

        if ! ensure_babyk "$_f"; then
            log "[WORK] FAIL rename: $_f"
            _fail=$((_fail + 1))
            continue
        fi
        _babyk=$(cat "$BABYK_FILE" 2>/dev/null)
        log "[WORK] babyk: $_babyk"
        remember_dir "$(dirname "$_babyk")" "$_dirlist"
        log "[WORK] OK: $_babyk"
        _ok=$((_ok + 1))
    done < "$_inlist"

    log "[WORK] === DONE ok=${_ok} fail=${_fail} total=${_n} ==="
}

# Build space-separated ACTIVE_ROOTS from parent dirs of paths in list file.
# Writes unique dirs to /tmp/babuk_active_roots.txt as well.
narrow_active_roots_from_list() {
    _inlist="$1"
    _out="/tmp/babuk_active_roots.txt"
    : > "$_out"

    if [ ! -s "$_inlist" ]; then
        ACTIVE_ROOTS=""
        log "[NARROW] no remaining hits -> active roots cleared"
        return 0
    fi

    while IFS= read -r _f; do
        [ -n "$_f" ] || continue
        _d=$(dirname "$_f")
        if ! grep -F "$_d" "$_out" >/dev/null 2>&1; then
            echo "$_d" >> "$_out"
            log "[NARROW] keep hot dir: $_d"
        fi
    done < "$_inlist"

    ACTIVE_ROOTS=""
    while IFS= read -r _d; do
        [ -n "$_d" ] || continue
        if [ -z "$ACTIVE_ROOTS" ]; then
            ACTIVE_ROOTS="$_d"
        else
            ACTIVE_ROOTS="$ACTIVE_ROOTS $_d"
        fi
    done < "$_out"

    log "[NARROW] next-round active roots: $ACTIVE_ROOTS"
}

# ---------------- main ----------------

if [ ! -f "$DECRYPTOR" ]; then
    log "[ERROR] decryptor disappeared: $DECRYPTOR"
    exit 1
fi
chmod +x "$DECRYPTOR" 2>/dev/null || true


# Remember user/default roots; first pass is broad, later passes narrow.
ORIGINAL_ROOTS="$SCAN_ROOTS"
ACTIVE_ROOTS="$SCAN_ROOTS"

if [ "$MODE" = "scan" ]; then
    ACTIVE_ROOTS="$ORIGINAL_ROOTS"
    scan_encrypted "/tmp/babuk_scan_only.txt" "scan-only"
    _c=$(cat "$COUNT_FILE" 2>/dev/null)
    log "[DONE] scan-only encrypted=${_c}"
    exit 0
fi

log "[PLAN] x${MAX_ROUNDS} rounds on Babuk suffixes only"
log "[PLAN] Round1 roots (broad): $ORIGINAL_ROOTS"
log "[PLAN] Round2+ roots: only parent dirs still containing encrypted hits"
log "[PLAN] PRE decrypt broad roots -> scan -> bak -> rename -> decrypt parents -> rescan -> narrow"

log ""
log "########## PRE-PASS: decryptor on BROAD roots ##########"
log "[PRE] broad roots: $ORIGINAL_ROOTS"
# run_decryptor_on_roots uses SCAN_ROOTS
SCAN_ROOTS="$ORIGINAL_ROOTS"
run_decryptor_on_roots

_round=1
while [ "$_round" -le "$MAX_ROUNDS" ]; do
    log ""
    log "########## ROUND ${_round}/${MAX_ROUNDS} START ##########"
    if [ "$_round" -eq 1 ]; then
        ACTIVE_ROOTS="$ORIGINAL_ROOTS"
        log "[ROUND ${_round}] scope=BROAD roots: $ACTIVE_ROOTS"
    else
        log "[ROUND ${_round}] scope=NARROW roots: $ACTIVE_ROOTS"
        if [ -z "$ACTIVE_ROOTS" ]; then
            log "[ROUND ${_round}] no active roots -> stop"
            break
        fi
    fi

    _list="/tmp/babuk_round_${_round}_list.txt"
    _dirs="/tmp/babuk_round_${_round}_dirs.txt"

    log "[ROUND ${_round}] step A: SCAN"
    scan_encrypted "$_list" "round-${_round}"
    _cnt=$(cat "$COUNT_FILE" 2>/dev/null)
    [ -n "$_cnt" ] || _cnt=0

    if [ "$_cnt" -eq 0 ]; then
        log "[ROUND ${_round}] no encrypted targets left -> stop"
        log "########## ROUND ${_round}/${MAX_ROUNDS} END (clean) ##########"
        break
    fi

    log "[ROUND ${_round}] step B/C: BACKUP + RENAME"
    process_backup_and_queue "$_list" "$_round" "$_dirs"

    log "[ROUND ${_round}] step D: DECRYPT parents (enc-bak stashed)"
    run_decryptor_on_dir_list "$_dirs"

    log "[ROUND ${_round}] step E: POST rescan (same active roots)"
    _post="/tmp/babuk_round_${_round}_post.txt"
    scan_encrypted "$_post" "round-${_round}-post"
    _pcnt=$(cat "$COUNT_FILE" 2>/dev/null)
    [ -n "$_pcnt" ] || _pcnt=0
    log "[ROUND ${_round}] still encrypted: ${_pcnt}"

    # Prepare narrower roots for next round from remaining hits
    if [ "$_round" -lt "$MAX_ROUNDS" ]; then
        log "[ROUND ${_round}] step F: NARROW roots for next round from remaining hits"
        narrow_active_roots_from_list "$_post"
    fi

    log "########## ROUND ${_round}/${MAX_ROUNDS} END ##########"
    _round=$((_round + 1))
done

log ""
log "########## FINAL SCAN (BROAD original roots) ##########"
ACTIVE_ROOTS="$ORIGINAL_ROOTS"
scan_encrypted "/tmp/babuk_final_list.txt" "final"
_fc=$(cat "$COUNT_FILE" 2>/dev/null)
[ -n "$_fc" ] || _fc=0
log "[FINAL] encrypted count=${_fc}"

log "============================================================"
log "ALL DONE at $(ts)"
log "Log: $LOG_FILE"
log "============================================================"

exit 0
