#!/bin/bash
# ==============================================================================================
# ============================= Rsync Core Script ==============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Core rsync engine for the two-server ecosystem. Called per share or per
# appdata profile by orchestrators (daily_sync_maintenance, weekly_sync_maintenance,
# critical_sync_maintenance) and directly for manual or scheduled dirty syncs.
#
# Profile is inferred from the directory basename (lowercased). Override with
# --profile=name for explicit selection. If no profile matches, global defaults
# from master.conf apply and no containers are stopped.
#
# After each sync, logs transfer data to bandwidth_monitor.sh for the weekly
# bandwidth report. Silent on success — only failures produce visible output.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Profiles define per-share behavior:
#   PROFILE_CRITICAL_CONTAINER_NAMES  — containers stopped on both servers before sync
#   PROFILE_DELAYED_CONTAINERS        — containers with a delay before restart after sync
#   PROFILE_CONTAINER_DELAY           — seconds before delayed containers start
#   PROFILE_RSYNC_OPTS                — rsync flags (does not inherit DEFAULT_RSYNC_OPTS)
#   PROFILE_BW_LIMIT                  — bandwidth limit in KB/s
#   PROFILE_RETRY_COUNT               — retry attempts on failure
#   PROFILE_SLEEP                     — seconds between retry attempts
#   PROFILE_EXCLUDE_DIRS              — paths excluded from transfer
#   PROFILE_REMOTE_RESTART_CONTAINERS — containers restarted on remote after dirty sync
#                                       Was running → restart. Was stopped → leave stopped.
#
# Two-tier rsync enable/disable:
#   Tier 1: RSYNC_ENABLED=false → all rsync stops immediately (checked by this script)
#   Tier 2: per-orchestrator flag (DAILY_RSYNC_ENABLED etc.) → checked by caller
#
# Bandwidth logging: after each sync, logs profile/duration/status/bytes to
# bandwidth_monitor.sh --log-transfer. Bytes captured from rsync --stats via awk
# using version-stable field names.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Deletion Is Opt-In, Never Inherited
#   DEFAULT_RSYNC_OPTS deliberately omits --delete. Media shares only ever gain files
#   here; the arr cleanup scripts own deletion and are the only things that understand
#   whether a file is genuinely orphaned. --delete appears in exactly two places: a
#   profile that sets it explicitly, and the push pass of --merge-run.
#
# Profiles Replace, Not Extend
#   PROFILE_RSYNC_OPTS does not inherit DEFAULT_RSYNC_OPTS. A profile states its full
#   flag set, so reading one profile tells you exactly what will run — no tracing
#   through a base list to discover an inherited --delete.
#
# Local Is Authoritative
#   In --merge-run the remote may contribute content the local lacks, but never a
#   competing version. Pass 1 pulls only with --ignore-existing, so the push in pass 2
#   can safely be authoritative. Reversing that order would let the remote overwrite
#   local files before the delete pass.
#
# Pre-flight Before Payload
#   Connectivity, version parity, remote rootfs, remote share, remote disks and drive
#   temperature are all checked before a byte moves. A transfer aborted halfway is more
#   expensive to reason about than one that never started.
#
# Bounded, Resumable Transfers
#   No single attempt may exceed RSYNC_MAX_RUNTIME_HOURS. This is safe only because
#   --partial is in the default opts: a terminated transfer resumes rather than
#   restarting, so bounding it costs nothing and prevents one huge or stuck transfer
#   from holding its lock indefinitely and starving every other profile of a turn.
#
# Silent on Success
#   Only failures produce visible output. A quiet run is a successful one.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   rsync over SSH as root and container stop/start both require it.
#
# Docker Presence Check
#   Verified before any profile container operations.
#
# Source Path Depth Guard
#   The source must be an absolute path at least three levels deep. It is pushed to
#   root@remote at the same absolute path and --merge-run adds --delete, so a truncated
#   argument is a remote-side hazard: /mnt/user would sync every share at once.
#
# Source Existence Check
#   The local directory must exist. Without this, --merge-run's pull pass would create
#   a mistyped directory, populate it from the remote, then push it back with --delete.
#
# Global Rsync Gate
#   check_rsync_enabled() — RSYNC_ENABLED=false exits cleanly before any operation.
#
# Partnership Blocklist
#   Refuses to sync if REMOTE_SERVER_NAME appears in the partnership blocklist.
#   Written at offboard — prevents stale access after a partnership ends.
#
# Version Parity
#   check_os_version_parity — refuses sync if servers on incompatible unRAID versions.
#
# Remote Health Pre-flights
#   check_connectivity() — Tailscale IP reachable before any SSH
#   check_remote_rootfs() — aborts if remote rootfs exceeds ROOTFS_WARN
#   check_remote_share() — aborts if target directory missing or empty on remote
#   check_remote_disks() — verifies all backing disks online on remote
#
# Drive Temperature Check
#   check_local_disk_temps() — runs before any transfer. Exit 1 = skip this profile,
#   exit 2 = abort all remaining syncs (CRITICAL temperature).
#
# Remote Docker Daemon Check
#   check_remote_docker_daemon — verified before any container stop/start operations.
#   If daemon unresponsive: container operations skipped, rsync proceeds without stopping.
#
# Per-Profile Concurrency Lock
#   acquire_rsync_lock() — per-profile lock prevents parallel runs of the same profile.
#   Global concurrent limit prevents too many simultaneous rsync processes.
#
# Runtime Ceiling
#   RSYNC_MAX_RUNTIME_HOURS terminates a single attempt that overruns, releasing the
#   per-profile lock for the next scheduled run. --partial in the default opts means the
#   paused transfer resumes rather than restarting from scratch.
#
# Merge-Run Ordering
#   Pass 1 pulls remote-unique content with --ignore-existing before pass 2 pushes with
#   --delete. Content the remote had and the local did not is preserved locally before
#   anything is deleted remotely.
#
# Merge-Run Delete Interlock
#   --delete is applied only when pass 1 completed. Its entire justification is that the
#   local is now the authoritative superset, and a capped or failed pull means it is not:
#   the remote still holds content the local never received. On an incomplete pull the
#   push proceeds without --delete and notifies, so local content still propagates while
#   nothing remote-unique is destroyed. The delete happens on a later run whose pull
#   succeeded.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   RSYNC_ENABLED
#       Global on/off toggle for all rsync operations. (default: true)
#
#   DEFAULT_RSYNC_OPTS
#       Base rsync flags for unproiled shares. Note: --delete is intentionally absent —
#       media shares spread files only, arr cleanup scripts own deletions. Profile-specific
#       opts set --delete explicitly where needed.
#
#   BW_LIMIT
#       Default bandwidth cap in KB/s when no PROFILE_BW_LIMIT is set. (default: 0 = unlimited)
#
#   RETRY_COUNT
#       Default retry attempts on rsync failure. (default: 3)
#
#   SLEEP
#       Default seconds between retry attempts. (shipped default: 300)
#
#   RSYNC_MAX_RUNTIME_HOURS
#       Max hours a single transfer attempt may run before it's terminated and paused
#       for the next scheduled run. Protects the per-profile lock from being held
#       indefinitely by one huge/stuck transfer, starving other profiles of a turn.
#       Safe because DEFAULT_RSYNC_OPTS includes --partial — a paused transfer resumes
#       from where it left off, not from scratch. (code fallback 23; shipped conf sets 19)
#
#   ROOTFS_WARN
#       Abort threshold for remote rootfs percentage full. (default: 75)
#
#   PROFILES["profile_KEY"]
#       Profile definitions — one entry per PROFILE_* key per profile name.
#       See OPERATIONAL MODEL above for all supported keys.
#
#   BANDWIDTH_LOG / BANDWIDTH_WARN_GB
#       Shared with bandwidth_monitor.sh — set once, used by both.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# rsync.sh /path/to/share
#     Sync the given path to the remote. Profile inferred from directory basename.
#
# rsync.sh /path/to/share --profile=name
#     Sync with explicit profile override — bypasses basename inference.
#
# rsync.sh /path/to/share --dry-run
#     Run all pre-flight checks and show what rsync would transfer. No transfer,
#     no container stops.
#
# rsync.sh /path/to/share --status
#     Show resolved profile, remote identity, and configuration. Then exit.
#
# rsync.sh /path/to/share --log
#     Verbose output throughout — every decision logged.
#
# rsync.sh /path/to/share --seed
#     Skip the empty-remote-share guard. Use for first-time seeding of a new share.
#
# rsync.sh /path/to/share --merge-run
#     Bidirectional merge: pull remote-unique content to local first (--ignore-existing),
#     then push local → remote with --delete so the remote matches local exactly.
#     Local is always authoritative — remote loses divergent file versions but keeps
#     any content the local did not have (pulled in pass 1 before the delete push).
#     Also auto-triggered when pre-scan detects ≥75% directory overlap with remote.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../load_config.sh"

# ── Separate positional directory arg from flags ───────────────────────────────────────────────
DIRECTORY=""
PROFILE_OVERRIDE=""
SEED=false
MERGE_RUN=false
RAW_ARGS=()

for ARG in "$@"; do
    case "$ARG" in
        --profile=*) PROFILE_OVERRIDE="${ARG#--profile=}" ;;
        --seed)      SEED=true ;;
        --merge-run) MERGE_RUN=true ;;
        --*|*=*)     RAW_ARGS+=("$ARG") ;;
        *)           [[ -z "$DIRECTORY" ]] && DIRECTORY="$ARG" || RAW_ARGS+=("$ARG") ;;
    esac
done

parse_args "${RAW_ARGS[@]}"

[[ -z "$DIRECTORY" ]] && {
    error "No directory specified"
    error "Usage: rsync.sh <dir> [--dry-run] [--log] [--profile=name]"
    exit 1
}

# ── Source path guards ────────────────────────────────────────────────────────────────────────
# This script pushes to root@remote at the same absolute path, and --merge-run adds --delete on
# the push pass. A truncated or mistyped source is therefore a remote-side data hazard, not just
# a local no-op: /mnt/user would sync every share at once, and / would target the filesystem
# root. Every real job path (see HOST*_*_SYNC_SHARES) is at least three levels deep.
_rsync_depth="${DIRECTORY//[^\/]/}"
if [[ "$DIRECTORY" != /* || "${#_rsync_depth}" -lt 3 ]]; then
    error "Refusing unsafe source path: '$DIRECTORY' — expected an absolute path at least 3 levels deep"
    exit 1
fi
unset _rsync_depth

# Without this, --merge-run's pull pass would create a mistyped local directory, fill it with
# remote content, then push it back with --delete.
if [[ ! -d "$DIRECTORY" ]]; then
    error "Source directory does not exist locally: $DIRECTORY"
    exit 1
fi

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

if ! command -v docker &>/dev/null; then
    error "Docker command not found"
    exit 1
fi


detect_hosts
require_partnership

# Tier 1 global gate — Tier 2 (per-orchestrator) checked by caller
if ! check_rsync_enabled; then
    warn "RSYNC_ENABLED=false — exiting cleanly"
    exit 0
fi

# Blocklist gate — refuse to sync with a partner blocked after offboard
BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR}/partnership_blocklist.db}"
if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then
    error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist"
    error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard"
    exit 1
fi

resolve_remote_ip

# ── Profile inference ─────────────────────────────────────────────────────────────────────────
if [[ -n "$PROFILE_OVERRIDE" ]]; then
    PROFILE_NAME="$PROFILE_OVERRIDE"
    log "Profile override: $PROFILE_NAME"
else
    PROFILE_NAME=$(basename "$DIRECTORY" | tr '[:upper:]' '[:lower:]')
    log "Profile inferred: $PROFILE_NAME"
fi

# Acquire per-profile lock and check global concurrent limit
acquire_rsync_lock "$PROFILE_NAME"

# Tee all output to a live log file for the Varaverk UI
VV_LIVE_LOG="/tmp/unraid_locks/rsync_${PROFILE_NAME}.log"
VV_LAST_LOG="/tmp/unraid_locks/rsync_${PROFILE_NAME}.last.log"
: > "$VV_LIVE_LOG"
exec 1> >(tee -a "$VV_LIVE_LOG") 2>&1

# ── Load profile settings ─────────────────────────────────────────────────────────────────────
# NOTE: DEFAULT_RSYNC_OPTS and PROFILE_RSYNC_OPTS are bash arrays defined in master.conf —
# any "$BW_LIMIT" inside them is expanded once, when master.conf is sourced, before this
# override runs. So this recomputed $BW_LIMIT only affects the log line below and --status;
# it does NOT change the actual rsync --bwlimit unless a profile also bakes its own
# --bwlimit="${PROFILE_BW_LIMIT[name]}" directly into that profile's PROFILE_RSYNC_OPTS entry.
BW_LIMIT=${PROFILE_BW_LIMIT[$PROFILE_NAME]:-$BW_LIMIT}
RETRY_COUNT=${PROFILE_RETRY_COUNT[$PROFILE_NAME]:-$RETRY_COUNT}
SLEEP=${PROFILE_SLEEP[$PROFILE_NAME]:-$SLEEP}
CONTAINER_DELAY=${PROFILE_CONTAINER_DELAY[$PROFILE_NAME]:-$CONTAINER_DELAY}
RSYNC_MAX_RUNTIME_HOURS=${RSYNC_MAX_RUNTIME_HOURS:-23}
RSYNC_MAX_RUNTIME_SECONDS=$(( RSYNC_MAX_RUNTIME_HOURS * 3600 ))

read -r -a CRITICAL_CONTAINER_NAMES       <<< "${PROFILE_CRITICAL_CONTAINER_NAMES[$PROFILE_NAME]:-}"
read -r -a DELAYED_CONTAINERS             <<< "${PROFILE_DELAYED_CONTAINERS[$PROFILE_NAME]:-}"
read -r -a EXCLUDE_DIRS                   <<< "${PROFILE_EXCLUDE_DIRS[$PROFILE_NAME]:-}"
read -r -a REMOTE_RESTART_CONTAINERS      <<< "${PROFILE_REMOTE_RESTART_CONTAINERS[$PROFILE_NAME]:-}"

# Local containers use same names as remote (mirrored naming scheme)
LOCAL_CRITICAL_CONTAINER_NAMES=("${CRITICAL_CONTAINER_NAMES[@]}")

log "$ICON_GEAR Config: profile=${PROFILE_NAME} bw-limit=${BW_LIMIT}KB/s retry=${RETRY_COUNT} sleep=${SLEEP}s container-delay=${CONTAINER_DELAY}s"
log "$ICON_GEAR Containers: critical=${CRITICAL_CONTAINER_NAMES[*]:-none} delayed=${DELAYED_CONTAINERS[*]:-none} remote-restart=${REMOTE_RESTART_CONTAINERS[*]:-none}"

[[ "$SHOW_STATUS" == true ]] && show_status && exit 0
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ==============================================================================================
# ━━━ Pre-flight Checks ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SHIELD Pre-flight ━━━"

# Disk temp — before touching remote or moving data
# Exit 1 = skip this profile | Exit 2 = abort all remaining profiles
check_local_disk_temps
TEMP_RESULT=$?
if [[ "$TEMP_RESULT" -eq 2 ]]; then
    error "Drive temps CRITICAL — aborting all remaining syncs"
    exit 2
elif [[ "$TEMP_RESULT" -eq 1 ]]; then
    warn "Drive temps high — skipping profile [$PROFILE_NAME]"
    exit 1
else
    log "Drive temps OK — $TEMP_CHECK_RESULT"
fi

# Version parity — refuse if servers on incompatible unRAID versions
check_os_version_parity || exit 1

check_connectivity
check_remote_rootfs
[[ "$SEED" == false ]] && check_remote_share "$DIRECTORY"
check_remote_disks "$DIRECTORY"

# ── Merge-run pre-scan ────────────────────────────────────────────────────────────────────────
# Auto-promote to merge mode when ≥75% of remote's top-level entries exist locally.
# Skipped when: --merge-run is already set, --seed is active, a named profile is resolved,
# or RSYNC_MERGE_ENABLED=false in master.conf.
if [[ "$MERGE_RUN" == false && "$SEED" == false \
    && "${RSYNC_MERGE_ENABLED:-true}" != "false" \
    && -z "${PROFILE_RSYNC_OPTS[$PROFILE_NAME]+x}" ]]; then
    _remote_top=$(ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
        root@"$REMOTE_SERVER" "ls -1A '$DIRECTORY' 2>/dev/null | sort" 2>/dev/null)
    _remote_count=$(echo "$_remote_top" | grep -c .)
    _remote_count=${_remote_count:-0}

    if [[ "$_remote_count" -gt 0 ]]; then
        _local_top=$(ls -1A "$DIRECTORY" 2>/dev/null | sort)
        _overlap=$(comm -12 \
            <(echo "$_local_top") \
            <(echo "$_remote_top") | grep -c .)
        _overlap=${_overlap:-0}
        _overlap_pct=$(( _overlap * 100 / _remote_count ))

        if [[ "$_overlap_pct" -ge 75 ]]; then
            info "Overlap ${_overlap_pct}% (${_overlap}/${_remote_count} entries) — auto-promoting to merge mode"
            MERGE_RUN=true
        else
            log "Overlap ${_overlap_pct}% (${_overlap}/${_remote_count} entries) — normal sync"
        fi
    fi
    unset _remote_top _remote_count _local_top _overlap _overlap_pct
fi

# Remote Docker daemon — check before attempting container operations
if [[ ${#CRITICAL_CONTAINER_NAMES[@]} -gt 0 ]] || [[ ${#REMOTE_RESTART_CONTAINERS[@]} -gt 0 ]]; then
    check_remote_docker_daemon || {
        warn "Remote Docker daemon unresponsive — skipping container operations"
        warn "Proceeding with rsync only — containers will not be stopped or restarted"
        CRITICAL_CONTAINER_NAMES=()
        LOCAL_CRITICAL_CONTAINER_NAMES=()
        REMOTE_RESTART_CONTAINERS=()
    }
fi

# ==============================================================================================
# ━━━ Stop Containers ━━━
# ==============================================================================================
if [[ ${#CRITICAL_CONTAINER_NAMES[@]} -gt 0 || ${#LOCAL_CRITICAL_CONTAINER_NAMES[@]} -gt 0 ]]; then
    echo ""
    echo "━━━ $ICON_STOP $ICON_CONTAINERS Stop Containers ━━━"
    if [[ "$DRY_RUN" == true ]]; then
        for c in "${LOCAL_CRITICAL_CONTAINER_NAMES[@]:-}"; do
            [[ -n "$c" ]] && warn "DRY RUN — would stop local: $c"
        done
        for c in "${CRITICAL_CONTAINER_NAMES[@]:-}"; do
            [[ -n "$c" ]] && warn "DRY RUN — would stop remote: $c"
        done
    else
        # Local first — flush local databases before pushing
        stop_local_containers
        # Remote next — prevent writes while receiving
        stop_containers
    fi
fi

# ==============================================================================================
# ━━━ Transfer ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Transfer ━━━"
echo "$ICON_RUN  Source:   $DIRECTORY"
echo "$ICON_NET  Remote:   $REMOTE_SERVER:$DIRECTORY"
echo "$ICON_GEAR Profile:  $PROFILE_NAME"
echo "$ICON_HOST Identity: $MY_ID → $REMOTE_ID"
echo ""

get_rsync_opts

# Append profile excludes
for ex in "${EXCLUDE_DIRS[@]:-}"; do
    [[ -n "$ex" ]] && RSYNC_OPTS+=(--exclude="$ex")
done

# Add --stats to capture bytes transferred for bandwidth logging
RSYNC_OPTS+=(--stats)

[[ "$DRY_RUN" == true ]] && RSYNC_OPTS+=("--dry-run")

# ── Merge pass 1: pull remote-unique content to local (--ignore-existing) ────────────────────
# Runs before the push so anything the remote has that we don't is preserved locally.
# After this pass, local is the superset — the delete push in pass 2 is then safe.
if [[ "$MERGE_RUN" == true ]]; then
    echo "$ICON_SYNC  Merge pass 1 — pulling ${REMOTE_SERVER_NAME}-unique content to local (capped at ${RSYNC_MAX_RUNTIME_HOURS}h)..."
    _pull_opts=(-av --ignore-existing --stats)
    [[ "$DRY_RUN" == true ]] && _pull_opts+=(--dry-run)
    _pull_output=$(timeout "${RSYNC_MAX_RUNTIME_SECONDS}s" rsync "${_pull_opts[@]}" \
        -e "ssh -i $SSH_KEY -T -o Compression=no -o IPQoS=throughput" \
        "root@${REMOTE_SERVER}:${DIRECTORY}/" "${DIRECTORY}/" 2>&1)
    _pull_exit=$?
    _pull_bytes=$(echo "$_pull_output" | \
        awk '/Total transferred file size:/{gsub(/,/,"",$NF); gsub(/[^0-9]/,"",$NF); print $NF+0}')
    _pull_complete=false
    if [[ "$_pull_exit" -eq 0 ]]; then
        echo "$ICON_DONE Merge pass 1 complete — ${_pull_bytes:-0} bytes pulled"
        _pull_complete=true
    elif [[ "$_pull_exit" -eq 124 ]]; then
        # Same cap as the main push — never let one direction of the merge run unbounded.
        # Files already completed before the timeout stay pulled; next scheduled run
        # picks up whatever's still remote-unique (no --partial here, so an in-flight
        # file at the moment of the kill is discarded, not corrupted).
        warn "Merge pass 1 exceeded ${RSYNC_MAX_RUNTIME_HOURS}h cap — pull incomplete"
    else
        warn "Merge pass 1 failed (exit $_pull_exit)"
        echo "$_pull_output" | grep -iE "error|rsync:|permission denied" | while read -r _line; do
            warn "  $_line"
        done
    fi

    # Pass 2: push with --delete — but ONLY if pass 1 actually finished.
    # --delete is justified solely by "local is now the authoritative superset", and that
    # premise holds only on a complete pull. If pass 1 was capped or failed, the remote still
    # holds content the local never received; deleting it here would destroy it permanently
    # and make the "resume the pull next run" promise impossible to keep. Push without
    # --delete instead — local content still propagates, nothing remote-unique is lost, and
    # the delete happens on a later run whose pull completed.
    if [[ "$_pull_complete" == true ]]; then
        RSYNC_OPTS+=(--delete)
    else
        warn "Skipping --delete this run — pass 1 did not complete, remote may hold content the local lacks"
        warn "Local content still pushes; the delete pass runs once a full pull succeeds"
        notify "Merge run on $(hostname) pushed without --delete — pass 1 incomplete for $PROFILE_NAME" \
            "Rsync" "warning"
    fi
    unset _pull_opts _pull_output _pull_exit _pull_bytes _pull_complete
fi

START=$(date +%s)
RSYNC_SUCCESS=false
RSYNC_TIMED_OUT=false
BYTES_TRANSFERRED=0
ATTEMPT=0

for (( ATTEMPT=1; ATTEMPT<=RETRY_COUNT; ATTEMPT++ )); do
    log "$ICON_RETRY Attempt $ATTEMPT of $RETRY_COUNT..."
    echo "$ICON_SYNC  Rsync running — this may take a while (capped at ${RSYNC_MAX_RUNTIME_HOURS}h)..."

    RSYNC_OUTPUT=$(timeout "${RSYNC_MAX_RUNTIME_SECONDS}s" rsync "${RSYNC_OPTS[@]}" \
        -e "ssh -i $SSH_KEY -T -o Compression=no -o IPQoS=throughput" \
        "$DIRECTORY" "root@${REMOTE_SERVER}:$(dirname "$DIRECTORY")/" 2>&1)

    RSYNC_EXIT=$?

    if [[ "$RSYNC_EXIT" -eq 0 ]]; then
        # Parse bytes transferred from --stats output
        BYTES_TRANSFERRED=$(echo "$RSYNC_OUTPUT" | \
            awk '/Total transferred file size:/{gsub(/,/,"",$NF); gsub(/[^0-9]/,"",$NF); print $NF+0}')
        BYTES_TRANSFERRED="${BYTES_TRANSFERRED:-0}"

        echo "$ICON_DONE Rsync complete — $BYTES_TRANSFERRED bytes transferred"
        RSYNC_SUCCESS=true
        break
    elif [[ "$RSYNC_EXIT" -eq 124 ]]; then
        # Hit the runtime cap, not a failure — --partial means next run resumes from here.
        # No retry: retrying an already-huge transfer 2 more times just wastes the window.
        warn "$ICON_RETRY Rsync exceeded ${RSYNC_MAX_RUNTIME_HOURS}h cap — pausing, will resume next scheduled run"
        RSYNC_TIMED_OUT=true
        break
    else
        warn "$ICON_RETRY Rsync failed (attempt $ATTEMPT/$RETRY_COUNT)"
        log "Exit code: $RSYNC_EXIT"
        if [[ "$ATTEMPT" -lt "$RETRY_COUNT" ]]; then
            log "Retrying in ${SLEEP}s..."
            sleep "$SLEEP"
        fi
    fi
done

# ==============================================================================================
# ━━━ Start Containers ━━━
# ==============================================================================================
if [[ ${#CRITICAL_CONTAINER_NAMES[@]} -gt 0 || ${#LOCAL_CRITICAL_CONTAINER_NAMES[@]} -gt 0 ]]; then
    echo ""
    echo "━━━ $ICON_START $ICON_CONTAINERS Start Containers ━━━"
    if [[ "$DRY_RUN" == true ]]; then
        for c in "${CRITICAL_CONTAINER_NAMES[@]:-}"; do
            [[ -n "$c" ]] && warn "DRY RUN — would start remote: $c"
        done
        for c in "${LOCAL_CRITICAL_CONTAINER_NAMES[@]:-}"; do
            [[ -n "$c" ]] && warn "DRY RUN — would start local: $c"
        done
    else
        # Remote first — can be coming up while local restarts
        start_containers
        # Local next
        start_local_containers
    fi
fi

# ==============================================================================================
# ━━━ Remote Restart (dirty sync profiles) ━━━
# ==============================================================================================
# For dirty sync profiles (critical-fallback) — restart containers on remote
# that were running before sync so they pick up config changes from the dirty sync window.
# Was running → restart. Was stopped → leave stopped.
if [[ ${#REMOTE_RESTART_CONTAINERS[@]} -gt 0 ]]; then
    echo ""
    echo "━━━ $ICON_START $ICON_CONTAINERS Remote Restart (post dirty sync) ━━━"
    log "Restarting configured containers on $REMOTE_SERVER_NAME..."

    for container in "${REMOTE_RESTART_CONTAINERS[@]}"; do
        [[ -z "$container" ]] && continue

        # Check if container was running before sync (still tracked via RUNNING_CONTAINERS)
        WAS_RUNNING=false
        for prev in "${RUNNING_CONTAINERS[@]:-}"; do
            [[ "$prev" == "$container" ]] && WAS_RUNNING=true && break
        done

        if [[ "$WAS_RUNNING" == false ]]; then
            # Not in stop list — check current remote state
            REMOTE_STATUS=$(timeout 15 ssh -i "$SSH_KEY" \
                -o ConnectTimeout=10 root@"$REMOTE_SERVER" \
                "docker inspect -f '{{.State.Running}}' $container 2>/dev/null" 2>/dev/null)
            [[ "$REMOTE_STATUS" != "true" ]] && \
                log "$container not running on $REMOTE_SERVER_NAME — skipping remote restart" && \
                continue
        fi

        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would restart $container on $REMOTE_SERVER_NAME"
            continue
        fi

        timeout 15 ssh -i "$SSH_KEY" -o ConnectTimeout=10 root@"$REMOTE_SERVER" \
            "docker restart $container" >/dev/null 2>&1 && \
            echo "$ICON_STARTED $container restarted on $REMOTE_SERVER_NAME ✅" || \
            warn "Failed to restart $container on $REMOTE_SERVER_NAME"
    done
fi

END=$(date +%s)
DURATION=$(( END - START ))

# ==============================================================================================
# ━━━ Bandwidth Logging ━━━
# ==============================================================================================
# Logs to bandwidth_monitor.sh — new format includes bytes transferred and warn flag.
# Only logs on actual runs (not dry-run) and only when bandwidth_monitor.sh exists.
BANDWIDTH_MONITOR="$SCRIPT_DIR/../Monitors/bandwidth_monitor.sh"

if [[ "$DRY_RUN" == false ]] && [[ -f "$BANDWIDTH_MONITOR" ]]; then
    STATUS="success"
    [[ "$RSYNC_SUCCESS" == false ]] && STATUS="failed"
    [[ "$RSYNC_TIMED_OUT" == true ]] && STATUS="timeout"
    bash "$BANDWIDTH_MONITOR" --log-transfer \
        "$PROFILE_NAME" "$DURATION" "$STATUS" "$BYTES_TRANSFERRED"
    log "$ICON_BANDWIDTH Transfer logged to bandwidth monitor ($BYTES_TRANSFERRED bytes)"
fi

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY RSYNC SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_RUN     Directory: $DIRECTORY"
echo "$ICON_GEAR    Profile:   $PROFILE_NAME"
echo "$ICON_TIME    Duration:  $(format_duration $DURATION)"
[[ "$BYTES_TRANSFERRED" -gt 0 ]] && \
    echo "$ICON_BANDWIDTH Transferred: $(awk "BEGIN {printf \"%.2fGB\", $BYTES_TRANSFERRED / 1073741824}")"

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no changes made"
elif [[ "$RSYNC_SUCCESS" == true ]]; then
    echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
elif [[ "$RSYNC_TIMED_OUT" == true ]]; then
    echo "$ICON_DONE Status: PAUSED — exceeded ${RSYNC_MAX_RUNTIME_HOURS}h cap, resumes next scheduled run"
else
    echo "$ICON_ERROR Status: FAILED after $RETRY_COUNT attempts"
    notify "Rsync FAILED — $DIRECTORY ($PROFILE_NAME) after $RETRY_COUNT attempts on $(hostname)" \
        "Rsync" "warning"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━"

# Flush tee and preserve log for UI — close both ends of the pipe so tee gets EOF
exec 1>&- 2>&-; wait
cp "$VV_LIVE_LOG" "$VV_LAST_LOG" 2>/dev/null
rm -f "$VV_LIVE_LOG"

[[ "$RSYNC_SUCCESS" == false ]] && [[ "$RSYNC_TIMED_OUT" == false ]] && [[ "$DRY_RUN" == false ]] && exit 1
exit 0