#!/bin/bash
# ==============================================================================================
# ================================= ARR Sync ===================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Full-mesh arr library sync across all nodes — Lidarr, Sonarr, and Radarr.
# Every node syncs with every other, union model, no hierarchy. Run before
# rsync in the weekly sync window: once arrs agree on what to track, rsync
# spreads the actual files.
#
# The LOCAL side of each sync (_local_library()) is cache-first (2026-07-17) — comes from
# the shared tracked-data cache via arr_get_tracked_data(), fresh (kept warm every 30min by
# arr_cache_prefill.sh), live fetch as fallback. The REMOTE side (_remote_library()) is
# unaffected — that cache is per-host by design, so a remote node's library is always
# fetched live here.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Full mesh: every node syncs with every other — no primary, no hierarchy.
# Union model: if any node tracks an item, all nodes get it (unless blocklisted).
# Convergence: any node can add content; after one full cycle all nodes agree.
# Upgrade-aware: a file upgrade on one node → arr tracks new path → rsync spreads
#   it → arr_cleanup removes old path on all nodes (arr no longer tracks it).
#
# Node discovery: reads HOST* vars from master.conf. Add HOST3= and it joins the
# sync automatically — no script changes needed for a new node.
#
# Graceful skip: arr not configured locally → skip cleanly. Arr not reachable on
# a remote → skip that node for that arr type, continue with others.
#
# What gets synced — library items keyed on stable external IDs:
#   Lidarr  — MusicBrainz artist ID (foreignArtistId)
#   Sonarr  — TVDB series ID (tvdbId)
#   Radarr  — TMDB movie ID (tmdbId)
# When adding to a remote, that node's own quality profile, metadata profile,
# and root folder path are used — settings are never copied from source.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Remote API Access — Cache-First, SSH Fallback
#   If conf_sync.sh has populated /tmp/.cache/vv/d/ and
#   load_config.sh has sourced it, HOST*_<ARR>_API_KEY vars are available
#   in the environment. Remote functions use them to call the arr API
#   directly over Tailscale (no SSH, no remote shell). If the cached key
#   is absent (first boot, cache not yet populated) the functions fall back
#   to SSHing in and reading the key from config.xml on the remote node.
#
# Blocklist TSV
#   ARR_SYNC_BLOCKLIST in DATA_DIR tombstones IDs that must never be re-added
#   anywhere. Read from ALL nodes via SSH at run start — immediate effect with
#   no rsync delay.
#
#   --blocklist-add does three things atomically:
#     1. Writes the TSV tombstone entry (prevents future re-adds by arr_sync)
#     2. Deletes the item from the local arr API (deleteFiles=false)
#     3. SSHes each remote node and deletes from their arr API (deleteFiles=false)
#   Files become orphans on all nodes — arr_cleanup removes them on next run.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
#   acquire_lock         — prevents two sync instances running simultaneously
#   ARR_SYNC_ENABLED     — global gate, exits cleanly when false
#   SSH connect timeout  — ARR_SYNC_CONNECT_TIMEOUT — does not hang on unreachable node
#   API call timeout     — ARR_SYNC_API_TIMEOUT — does not hang on slow arr
#   Graceful skip        — unreachable node/arr → skip and continue, never abort
#   Blocklist gate       — item in blocklist → never added to any node
#   Silent by default    — only additions produce output, clean runs stay silent
#
# ==============================================================================================
# STATE FILES
# ==============================================================================================
#
#   ARR_SYNC_BLOCKLIST — TSV file in DATA_DIR (default: DATA_DIR/arr_sync_blocklist.tsv)
#       Columns: arr_type, id, reason, date_added
#       Read from all nodes via SSH at the start of each run.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — local Lidarr (aliased by detect_hosts)
#   HOST*_SONARR_URL / HOST*_SONARR_API_KEY — local Sonarr
#   HOST*_RADARR_URL / HOST*_RADARR_API_KEY — local Radarr
#
# master.conf
#
#   ARR_SYNC_ENABLED         — global on/off toggle (default: true)
#   ARR_SYNC_BLOCKLIST       — path to TSV blocklist file
#   ARR_SYNC_CONNECT_TIMEOUT — SSH connect timeout in seconds (default: 10)
#   ARR_SYNC_API_TIMEOUT     — curl API call timeout in seconds (default: 60)
#   DOCKER_APPDATA_BASE      — base path for arr appdata dirs (default: /mnt/user/appdata)
#   ARR_SYNC_LIDARR_PORT     — Lidarr port on all nodes (default: 8686)
#   ARR_SYNC_SONARR_PORT     — Sonarr port on all nodes (default: 8989)
#   ARR_SYNC_RADARR_PORT     — Radarr port on all nodes (default: 7878)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
#   arr_sync.sh                                            — sync all arr types, all nodes
#   arr_sync.sh --dry-run                                  — preview only, no changes
#   arr_sync.sh --log                                      — verbose output
#   arr_sync.sh --status                                   — show config and exit
#
# Blocklist management:
#   arr_sync.sh --blocklist-add lidarr <mbid> "reason"    — remove from all arrs + tombstone
#   arr_sync.sh --blocklist-add sonarr <tvdbId> "reason"  — remove from all arrs + tombstone
#   arr_sync.sh --blocklist-add radarr <tmdbId> "reason"  — remove from all arrs + tombstone
#   arr_sync.sh --blocklist-remove lidarr <id>            — un-tombstone (does NOT re-add)
#   arr_sync.sh --blocklist-list                          — show all blocklisted IDs
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"

# ── Blocklist flag pre-processing ─────────────────────────────────────────────────────────────
# Handle before parse_args — these are action flags, not standard options
BLOCKLIST_ACTION=""
BLOCKLIST_ARR=""
BLOCKLIST_ID=""
BLOCKLIST_REASON=""
FORCE_MODE=false
FILTERED_ARGS=()
_skip_next=false
for _arg in "$@"; do
    if [[ "$_skip_next" == true ]]; then _skip_next=false; continue; fi
    case "$_arg" in
        --blocklist-add)    BLOCKLIST_ACTION="add" ;;
        --blocklist-remove) BLOCKLIST_ACTION="remove" ;;
        --blocklist-list)   BLOCKLIST_ACTION="list" ;;
        --force)            FORCE_MODE=true ;;
        *) FILTERED_ARGS+=("$_arg") ;;
    esac
done
unset _arg _skip_next

parse_args "${FILTERED_ARGS[@]}"

# Consume blocklist positional args from remaining FILTERED_ARGS
# --blocklist-add lidarr <id> "reason"
# --blocklist-remove lidarr <id>
if [[ -n "$BLOCKLIST_ACTION" ]] && [[ "$BLOCKLIST_ACTION" != "list" ]]; then
    for _a in "${FILTERED_ARGS[@]}"; do
        [[ "$_a" == --* ]] && continue
        if [[ -z "$BLOCKLIST_ARR" ]];    then BLOCKLIST_ARR="$_a"
        elif [[ -z "$BLOCKLIST_ID" ]];   then BLOCKLIST_ID="$_a"
        elif [[ -z "$BLOCKLIST_REASON" ]]; then BLOCKLIST_REASON="$_a"
        fi
    done
    unset _a
fi

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

acquire_lock

for _tool in curl jq; do
    if ! command -v "$_tool" >/dev/null 2>&1; then
        error "$_tool not found — required for arr API calls"
        exit 1
    fi
done
unset _tool

detect_hosts
require_partnership

# ── Runtime config with defaults ──────────────────────────────────────────────────────────────
ARR_SYNC_ENABLED="${ARR_SYNC_ENABLED:-true}"
ARR_SYNC_BLOCKLIST="${ARR_SYNC_BLOCKLIST:-${DATA_DIR}/arr_sync_blocklist.tsv}"
ARR_SYNC_CONNECT_TIMEOUT="${ARR_SYNC_CONNECT_TIMEOUT:-10}"
ARR_SYNC_API_TIMEOUT="${ARR_SYNC_API_TIMEOUT:-60}"

ARR_SYNC_LIDARR_PORT="${ARR_SYNC_LIDARR_PORT:-8686}"
ARR_SYNC_SONARR_PORT="${ARR_SYNC_SONARR_PORT:-8989}"
ARR_SYNC_RADARR_PORT="${ARR_SYNC_RADARR_PORT:-7878}"

if [[ "$ARR_SYNC_ENABLED" != "true" ]]; then
    log "ARR_SYNC_ENABLED=false — exiting"
    exit 0
fi

log "$ICON_GEAR Config: api-timeout=${ARR_SYNC_API_TIMEOUT}s connect-timeout=${ARR_SYNC_CONNECT_TIMEOUT}s blocklist=${ARR_SYNC_BLOCKLIST}"
log "$ICON_GEAR Ports:  lidarr=${ARR_SYNC_LIDARR_PORT} sonarr=${ARR_SYNC_SONARR_PORT} radarr=${ARR_SYNC_RADARR_PORT}"

# ── Arr type definitions ───────────────────────────────────────────────────────────────────────
# Each arr type maps to its port, API version, endpoint, stable ID field, and display name field
declare -A _PORT=([lidarr]="$ARR_SYNC_LIDARR_PORT" [sonarr]="$ARR_SYNC_SONARR_PORT" [radarr]="$ARR_SYNC_RADARR_PORT")
declare -A _VER=( [lidarr]="v1"     [sonarr]="v3"       [radarr]="v3")
declare -A _EP=(  [lidarr]="artist" [sonarr]="series"   [radarr]="movie")
declare -A _ID=(  [lidarr]="foreignArtistId" [sonarr]="tvdbId" [radarr]="tmdbId")
declare -A _NAME=([lidarr]="artistName"      [sonarr]="title"  [radarr]="title")
# ID type: "string" for MusicBrainz UUID, "int" for TVDB/TMDB numeric IDs
declare -A _ID_TYPE=([lidarr]="string" [sonarr]="int" [radarr]="int")
ARR_TYPES=(lidarr sonarr radarr)

# ── Remote node discovery ──────────────────────────────────────────────────────────────────────
discover_remote_nodes

if [[ "${#REMOTE_NODES[@]}" -eq 0 ]]; then
    warn "No remote nodes defined in master.conf — nothing to sync"
    exit 0
fi

echo "  Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "  Remote nodes: ${REMOTE_NODES[*]}"
echo "  Blocklist: $ARR_SYNC_BLOCKLIST"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"

# ==============================================================================================
# ── BLOCKLIST ─────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

declare -A BLOCKLIST_MAP  # key: "arr_type:stable_id" → display name

_load_blocklist() {
    BLOCKLIST_MAP=()
    local count=0

    _parse_blocklist_lines() {
        while IFS=$'\t' read -r arr_type stable_id display_name rest; do
            [[ -z "$arr_type" ]] || [[ "$arr_type" == \#* ]] && continue
            BLOCKLIST_MAP["${arr_type}:${stable_id}"]="$display_name"
            (( count++ ))
        done
    }

    # Local blocklist
    [[ -f "$ARR_SYNC_BLOCKLIST" ]] && _parse_blocklist_lines < "$ARR_SYNC_BLOCKLIST"

    # Remote blocklists — read via SSH so tombstones are effective immediately
    for _node_id in "${REMOTE_NODES[@]}"; do
        local _node_ip
        _node_ip=$(_resolve_node_ip "$_node_id") || continue
        local _remote_lines
        _remote_lines=$(ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
            root@"$_node_ip" "cat '$ARR_SYNC_BLOCKLIST' 2>/dev/null" 2>/dev/null) || continue
        _parse_blocklist_lines <<< "$_remote_lines"
    done
    unset _node_id _node_ip _remote_lines

    log "Loaded blocklist: $count entries from $(( ${#REMOTE_NODES[@]} + 1 )) nodes"
    unset -f _parse_blocklist_lines
}

_is_blocklisted() {
    [[ -n "${BLOCKLIST_MAP["${1}:${2}"]:-}" ]]
}

_blocklist_add() {
    local arr_type="$1" stable_id="$2" display_name="$3" reason="${4:-manually excluded}"
    mkdir -p "$(dirname "$ARR_SYNC_BLOCKLIST")"
    if ! grep -qP "^${arr_type}\t${stable_id}\t" "$ARR_SYNC_BLOCKLIST" 2>/dev/null; then
        printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
            "$arr_type" "$stable_id" "$display_name" \
            "$MY_ID" "$(date -Iseconds)" "$reason" \
            >> "$ARR_SYNC_BLOCKLIST"
        BLOCKLIST_MAP["${arr_type}:${stable_id}"]="$display_name"
        log "Blocklisted: [$arr_type] $display_name ($stable_id)"
    else
        log "Already blocklisted: [$arr_type] $stable_id"
    fi
}

_blocklist_remove() {
    local arr_type="$1" stable_id="$2"
    if [[ -f "$ARR_SYNC_BLOCKLIST" ]]; then
        local tmp
        tmp=$(mktemp)
        grep -vP "^${arr_type}\t${stable_id}\t" "$ARR_SYNC_BLOCKLIST" > "$tmp" && \
            mv "$tmp" "$ARR_SYNC_BLOCKLIST" || rm -f "$tmp"
        unset "BLOCKLIST_MAP[${arr_type}:${stable_id}]"
        log "Removed from blocklist: [$arr_type] $stable_id"
    fi
}

# Look up item in local arr by stable_id — returns "internal_id\tdisplay_name" or empty
_lookup_local_item() {
    local url="$1" api_key="$2" api_ver="$3" endpoint="$4"
    local id_field="$5" id_type="$6" name_field="$7" stable_id="$8"
    local raw select_expr
    raw=$(curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
        -H "X-Api-Key: $api_key" \
        "${url}/api/${api_ver}/${endpoint}" 2>/dev/null)
    [[ -z "$raw" ]] && return 1
    if [[ "$id_type" == "string" ]]; then
        select_expr=".[] | select(.${id_field} == \"${stable_id}\")"
    else
        select_expr=".[] | select(.${id_field} == ${stable_id})"
    fi
    echo "$raw" | jq -r "${select_expr} | [(.id | tostring), .${name_field}] | @tsv" 2>/dev/null | head -1
}

# Delete item from local arr by internal integer id — returns HTTP status code
_delete_local_item() {
    local url="$1" api_key="$2" api_ver="$3" endpoint="$4" internal_id="$5"
    curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
        -H "X-Api-Key: $api_key" \
        "${url}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
}

# Delete item from remote arr by stable_id.
# Outputs: HTTP code on success | "not_found" if item absent | empty on failure.
# deleteFiles=false — files become orphans for arr_cleanup to handle with its safety checks.
# Args: node_id port api_ver arr_type endpoint id_field id_type stable_id
_delete_remote_item() {
    local node_id="$1" port="$2" api_ver="$3" arr_type="$4" endpoint="$5"
    local id_field="$6" id_type="$7" stable_id="$8"
    local node_name="${!node_id}"
    local node_ip
    node_ip=$(resolve_tailscale_ip "$node_name") || return 1

    local select_expr
    if [[ "$id_type" == "string" ]]; then
        select_expr=".[] | select(.${id_field} == \"${stable_id}\") | .id"
    else
        select_expr=".[] | select(.${id_field} == ${stable_id}) | .id"
    fi

    local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
    if [[ -n "$cached_key" ]]; then
        local library internal_id
        library=$(curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
            -H "X-Api-Key: $cached_key" \
            "http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null)
        [[ -z "$library" ]] && return 1
        internal_id=$(echo "$library" | jq -r "${select_expr}" 2>/dev/null | head -1)
        [[ -z "$internal_id" ]] && echo "not_found" && return 0
        curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
            -H "X-Api-Key: $cached_key" \
            "http://${node_ip}:${port}/api/${api_ver}/${endpoint}/${internal_id}?deleteFiles=false" 2>/dev/null
        return
    fi
    local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
    ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
        root@"$node_ip" bash <<REMOTE 2>/dev/null
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
[[ -z "\$KEY" ]] && exit 1
LIBRARY=\$(curl -sf --max-time ${ARR_SYNC_API_TIMEOUT} \
    -H "X-Api-Key: \$KEY" \
    "http://localhost:${port}/api/${api_ver}/${endpoint}" 2>/dev/null)
[[ -z "\$LIBRARY" ]] && exit 1
INTERNAL_ID=\$(echo "\$LIBRARY" | jq -r '${select_expr}' 2>/dev/null | head -1)
[[ -z "\$INTERNAL_ID" ]] && echo "not_found" && exit 0
curl -sf -o /dev/null -w '%{http_code}' -X DELETE \
    -H "X-Api-Key: \$KEY" \
    "http://localhost:${port}/api/${api_ver}/${endpoint}/\${INTERNAL_ID}?deleteFiles=false"
REMOTE
}

# ── Blocklist management mode ──────────────────────────────────────────────────────────────────
if [[ -n "$BLOCKLIST_ACTION" ]]; then
    case "$BLOCKLIST_ACTION" in
        list)
            echo ""
            echo "━━━━━ ARR SYNC BLOCKLIST ━━━━━"
            if [[ ! -f "$ARR_SYNC_BLOCKLIST" ]] || [[ ! -s "$ARR_SYNC_BLOCKLIST" ]]; then
                echo "  (empty)"
            else
                echo ""
                printf '%-8s  %-40s  %-30s  %s\n' "Arr" "Stable ID" "Name" "Reason"
                printf '%-8s  %-40s  %-30s  %s\n' "---" "---------" "----" "------"
                while IFS=$'\t' read -r arr_type stable_id display_name tombstoned_by ts reason; do
                    [[ -z "$arr_type" ]] || [[ "$arr_type" == \#* ]] && continue
                    printf '%-8s  %-40s  %-30s  %s\n' "$arr_type" "$stable_id" "$display_name" "$reason"
                done < "$ARR_SYNC_BLOCKLIST"
            fi
            echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
            exit 0
            ;;
        add)
            if [[ -z "$BLOCKLIST_ARR" ]] || [[ -z "$BLOCKLIST_ID" ]]; then
                error "Usage: arr_sync.sh --blocklist-add <arr_type> <stable_id> [reason]"
                error "  arr_type: lidarr | sonarr | radarr"
                exit 1
            fi
            if [[ -z "${_PORT[$BLOCKLIST_ARR]:-}" ]]; then
                error "Unknown arr type: $BLOCKLIST_ARR — use lidarr, sonarr, or radarr"
                exit 1
            fi

            _bl_port="${_PORT[$BLOCKLIST_ARR]}"
            _bl_ver="${_VER[$BLOCKLIST_ARR]}"
            _bl_ep="${_EP[$BLOCKLIST_ARR]}"
            _bl_id_field="${_ID[$BLOCKLIST_ARR]}"
            _bl_id_type="${_ID_TYPE[$BLOCKLIST_ARR]}"
            _bl_name_field="${_NAME[$BLOCKLIST_ARR]}"
            _bl_url="" _bl_key=""
            case "$BLOCKLIST_ARR" in
                lidarr) _bl_url="${LIDARR_URL:-}"; _bl_key="${LIDARR_API_KEY:-}" ;;
                sonarr) _bl_url="${SONARR_URL:-}"; _bl_key="${SONARR_API_KEY:-}" ;;
                radarr) _bl_url="${RADARR_URL:-}"; _bl_key="${RADARR_API_KEY:-}" ;;
            esac

            # Look up display name and internal id from local arr
            _bl_display_name="$BLOCKLIST_ID"
            _bl_internal_id=""
            if [[ -n "$_bl_url" ]] && [[ -n "$_bl_key" ]]; then
                _bl_lookup=$(_lookup_local_item "$_bl_url" "$_bl_key" "$_bl_ver" "$_bl_ep" \
                                               "$_bl_id_field" "$_bl_id_type" "$_bl_name_field" "$BLOCKLIST_ID")
                if [[ -n "$_bl_lookup" ]]; then
                    IFS=$'\t' read -r _bl_internal_id _bl_display_name <<< "$_bl_lookup"
                fi
            fi

            _blocklist_add "$BLOCKLIST_ARR" "$BLOCKLIST_ID" "$_bl_display_name" "${BLOCKLIST_REASON:-manually excluded}"
            echo "  Blocklisted [$BLOCKLIST_ARR] $_bl_display_name ($BLOCKLIST_ID)"

            # Remove from local arr (deleteFiles=false — arr_cleanup handles file removal)
            if [[ -n "$_bl_internal_id" ]]; then
                _bl_http=$(_delete_local_item "$_bl_url" "$_bl_key" "$_bl_ver" "$_bl_ep" "$_bl_internal_id")
                if [[ "$_bl_http" == "200" ]]; then
                    log "Removed from local ${BLOCKLIST_ARR^}: $_bl_display_name"
                else
                    warn "Failed to remove from local ${BLOCKLIST_ARR^} (HTTP ${_bl_http:-no response}) — remove manually via UI"
                fi
            else
                log "Not found in local ${BLOCKLIST_ARR^} — already removed or not tracked locally"
            fi

            # Remove from all remote arrs
            for _bl_node_id in "${REMOTE_NODES[@]}"; do
                _bl_node_name="${!_bl_node_id}"
                _bl_result=$(_delete_remote_item "$_bl_node_id" "$_bl_port" "$_bl_ver" \
                                                 "$BLOCKLIST_ARR" "$_bl_ep" "$_bl_id_field" "$_bl_id_type" "$BLOCKLIST_ID")
                case "$_bl_result" in
                    200)       log "Removed from $_bl_node_name ${BLOCKLIST_ARR^}: $_bl_display_name" ;;
                    not_found) log "Not found on $_bl_node_name ${BLOCKLIST_ARR^} — already removed or not tracked" ;;
                    *)         warn "Failed to remove from $_bl_node_name ${BLOCKLIST_ARR^} (${_bl_result:-SSH error}) — remove manually via UI" ;;
                esac
            done

            echo ""
            echo "  Files are now orphans on all nodes — arr_cleanup.sh will remove them on next run"
            exit 0
            ;;
        remove)
            if [[ -z "$BLOCKLIST_ARR" ]] || [[ -z "$BLOCKLIST_ID" ]]; then
                error "Usage: arr_sync.sh --blocklist-remove <arr_type> <stable_id>"
                exit 1
            fi
            _blocklist_remove "$BLOCKLIST_ARR" "$BLOCKLIST_ID"
            echo "  Removed [$BLOCKLIST_ARR] $BLOCKLIST_ID from blocklist"
            exit 0
            ;;
    esac
fi

# ==============================================================================================
# ── STATUS ────────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY ARR SYNC STATUS ━━━━━"
    echo "$ICON_HOST  Identity:       $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_HOST  Remote nodes:   ${REMOTE_NODES[*]}"
    echo "$ICON_GEAR  Appdata base:   $DOCKER_APPDATA_BASE"
    echo "$ICON_GEAR  Blocklist:      $ARR_SYNC_BLOCKLIST"
    echo "$ICON_GEAR  Connect timeout: ${ARR_SYNC_CONNECT_TIMEOUT}s"
    echo "$ICON_GEAR  API timeout:    ${ARR_SYNC_API_TIMEOUT}s"
    echo ""
    echo "  Arr    Port  URL"
    for _arr in "${ARR_TYPES[@]}"; do
        _url=""
        case "$_arr" in
            lidarr) _url="${LIDARR_URL:-not configured}" ;;
            sonarr) _url="${SONARR_URL:-not configured}" ;;
            radarr) _url="${RADARR_URL:-not configured}" ;;
        esac
        printf '  %-8s %-6s %s\n' "$_arr" "${_PORT[$_arr]}" "$_url"
    done
    unset _arr _url
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ── REMOTE HELPERS ────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

_resolve_node_ip() {
    local node_id="$1"
    resolve_tailscale_ip "${!node_id}"
}

# Check if arr is reachable on remote node
# Args: node_id node_ip port api_ver arr_type
_remote_arr_up() {
    local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5"
    local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
    if [[ -n "$cached_key" ]]; then
        curl -sf --max-time 5 \
            -H "X-Api-Key: $cached_key" \
            "http://${node_ip}:${port}/api/${api_ver}/system/status" >/dev/null 2>/dev/null
        return
    fi
    local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
    ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
        root@"$node_ip" \
        "KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
         [[ -z \"\$KEY\" ]] && exit 1
         curl -sf --max-time 5 -H \"X-Api-Key: \$KEY\" \
             'http://localhost:${port}/api/${api_ver}/system/status' >/dev/null" 2>/dev/null
}

# Fetch full library from remote arr — returns raw JSON array
# Args: node_id node_ip port api_ver arr_type endpoint
_remote_library() {
    local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5" endpoint="$6"
    local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
    if [[ -n "$cached_key" ]]; then
        curl -sf --max-time "$ARR_SYNC_API_TIMEOUT" \
            -H "X-Api-Key: $cached_key" \
            "http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null
        return
    fi
    local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
    ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
        root@"$node_ip" \
        "KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
         [[ -z \"\$KEY\" ]] && exit 1
         curl -sf --max-time ${ARR_SYNC_API_TIMEOUT} \
             -H \"X-Api-Key: \$KEY\" \
             'http://localhost:${port}/api/${api_ver}/${endpoint}'" 2>/dev/null
}

# Fetch remote arr defaults: qualityProfileId, rootFolderPath, metadataProfileId (Lidarr)
# Args: node_id node_ip port api_ver arr_type
_remote_defaults() {
    local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5"
    local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
    if [[ -n "$cached_key" ]]; then
        local base_url="http://${node_ip}:${port}/api/${api_ver}"
        local qp rf mp
        qp=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/qualityprofile"   2>/dev/null | jq '.[0].id // 1')
        rf=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/rootfolder"       2>/dev/null | jq -r '.[0].path // ""')
        [[ -z "$qp" ]] && return 1
        if [[ "$arr_type" == "lidarr" ]]; then
            mp=$(curl -sf -H "X-Api-Key: $cached_key" "${base_url}/metadataprofile" 2>/dev/null | \
                 jq 'map(select(.name == "Standard")) | .[0].id // .[0].id // 1')
            jq -n --argjson qp "$qp" --arg rf "$rf" --argjson mp "$mp" \
                '{qualityProfileId: $qp, rootFolderPath: $rf, metadataProfileId: $mp}'
        else
            jq -n --argjson qp "$qp" --arg rf "$rf" \
                '{qualityProfileId: $qp, rootFolderPath: $rf}'
        fi
        return
    fi
    local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
    local meta_field=""
    [[ "$arr_type" == "lidarr" ]] && \
        meta_field=', metadataProfileId: ($mp | map(select(.name == "Standard")) | .[0].id // .[0].id // 1)'
    ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
        root@"$node_ip" bash <<REMOTE 2>/dev/null
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
[[ -z "\$KEY" ]] && exit 1
QP=\$(curl -sf -H "X-Api-Key: \$KEY" "http://localhost:${port}/api/${api_ver}/qualityprofile")
RF=\$(curl -sf -H "X-Api-Key: \$KEY" "http://localhost:${port}/api/${api_ver}/rootfolder")
MP=\$(curl -sf -H "X-Api-Key: \$KEY" "http://localhost:${port}/api/${api_ver}/metadataprofile" 2>/dev/null || echo '[]')
jq -n --argjson qp "\$QP" --argjson rf "\$RF" --argjson mp "\$MP" \
    '{qualityProfileId: (\$qp | .[0].id // 1), rootFolderPath: (\$rf | .[0].path // "")${meta_field}}'
REMOTE
}

# Add item to remote arr — payload is base64-encoded to avoid quoting issues
# Args: node_id node_ip port api_ver arr_type endpoint encoded_payload
_remote_add() {
    local node_id="$1" node_ip="$2" port="$3" api_ver="$4" arr_type="$5" endpoint="$6"
    local encoded="$7"
    local _kvar="${node_id}_${arr_type^^}_API_KEY"; local cached_key="${!_kvar:-}"
    if [[ -n "$cached_key" ]]; then
        local body
        body=$(printf '%s' "$encoded" | base64 -d)
        curl -sf -o /dev/null -w '%{http_code}' -X POST \
            -H "X-Api-Key: $cached_key" \
            -H "Content-Type: application/json" \
            -d "$body" \
            "http://${node_ip}:${port}/api/${api_ver}/${endpoint}" 2>/dev/null
        return
    fi
    local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
    ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
        root@"$node_ip" bash <<REMOTE 2>/dev/null
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
[[ -z "\$KEY" ]] && exit 1
BODY=\$(printf '%s' '${encoded}' | base64 -d)
curl -sf -o /dev/null -w '%{http_code}' -X POST \
    -H "X-Api-Key: \$KEY" \
    -H 'Content-Type: application/json' \
    -d "\$BODY" \
    "http://localhost:${port}/api/${api_ver}/${endpoint}"
REMOTE
}

# Trigger a full library rescan on a remote arr — used after merge-run to force the arr
# to accept what is now on disk as ground truth rather than chasing stale file versions.
# Args: arr_type node_id node_ip port api_ver
_trigger_rescan() {
    local arr_type="$1" node_id="$2" node_ip="$3" port="$4" api_ver="$5"
    local command_name
    case "$arr_type" in
        sonarr) command_name="RefreshSeries" ;;
        radarr) command_name="RefreshMovie" ;;
        lidarr) command_name="RefreshArtist" ;;
        *) return 0 ;;
    esac

    local encoded
    encoded=$(printf '{"name":"%s"}' "$command_name" | base64 -w0)
    local _kvar="${node_id}_${arr_type^^}_API_KEY"
    local cached_key="${!_kvar:-}"

    local http_code
    if [[ -n "$cached_key" ]]; then
        local body
        body=$(printf '%s' "$encoded" | base64 -d)
        http_code=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
            -H "X-Api-Key: $cached_key" \
            -H "Content-Type: application/json" \
            -d "$body" \
            "http://${node_ip}:${port}/api/${api_ver}/command" 2>/dev/null)
    else
        local config_xml="${DOCKER_APPDATA_BASE}/${arr_type^}/config.xml"
        http_code=$(ssh -i "$SSH_KEY" -o ConnectTimeout="$ARR_SYNC_CONNECT_TIMEOUT" \
            root@"$node_ip" bash <<REMOTE 2>/dev/null
KEY=\$(grep -oP '(?<=<ApiKey>)[^<]+' '${config_xml}' 2>/dev/null)
[[ -z "\$KEY" ]] && exit 1
BODY=\$(printf '%s' '${encoded}' | base64 -d)
curl -sf -o /dev/null -w '%{http_code}' -X POST \
    -H "X-Api-Key: \$KEY" \
    -H 'Content-Type: application/json' \
    -d "\$BODY" \
    "http://localhost:${port}/api/${api_ver}/command"
REMOTE
)
    fi

    if [[ "$http_code" == "201" || "$http_code" == "200" ]]; then
        log "${arr_type^}: triggered ${command_name} on ${node_id}"
    else
        warn "${arr_type^}: failed to trigger ${command_name} on ${node_id} (HTTP ${http_code:-timeout})"
    fi
}

# ==============================================================================================
# ── LOCAL HELPERS ─────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

_local_library() {
    local url="$1" api_key="$2" api_ver="$3" endpoint="$4"
    # endpoint doubles as arr_type here — artist/series/movie match lidarr/sonarr/radarr's
    # ARR_LIBRARY_ENDPOINT values exactly, so the same lookup works either direction.
    local arr_type
    case "$endpoint" in
        artist) arr_type="lidarr" ;;
        series) arr_type="sonarr" ;;
        movie)  arr_type="radarr" ;;
    esac
    # Cache-first — arr_get_tracked_data() serves the shared cache when it's fresh (kept
    # current every 30min by arr_cache_prefill.sh in CRITICAL_MAINTENANCE_SCRIPTS), falls back
    # to a live fetch when it's stale, and waits out an active rescan before either. This is
    # the LOCAL host's library only — the remote side of this sync isn't cached, since the
    # shared cache is per-host by design.
    arr_get_tracked_data "$arr_type" "$url" "$api_key" "$api_ver"
}

_local_defaults() {
    local url="$1" api_key="$2" api_ver="$3" arr_type="$4"
    local qp rf mp meta_field=""
    qp=$(curl -sf -H "X-Api-Key: $api_key" "${url}/api/${api_ver}/qualityprofile" | jq '.[0].id // 1')
    rf=$(curl -sf -H "X-Api-Key: $api_key" "${url}/api/${api_ver}/rootfolder"     | jq -r '.[0].path // ""')
    if [[ "$arr_type" == "lidarr" ]]; then
        mp=$(curl -sf -H "X-Api-Key: $api_key" "${url}/api/${api_ver}/metadataprofile" | \
             jq 'map(select(.name == "Standard")) | .[0].id // .[0].id // 1')
        jq -n --argjson qp "$qp" --arg rf "$rf" --argjson mp "$mp" \
            '{qualityProfileId: $qp, rootFolderPath: $rf, metadataProfileId: $mp}'
    else
        jq -n --argjson qp "$qp" --arg rf "$rf" \
            '{qualityProfileId: $qp, rootFolderPath: $rf}'
    fi
}

_local_add() {
    local url="$1" api_key="$2" api_ver="$3" endpoint="$4" payload="$5"
    curl -sf -o /dev/null -w '%{http_code}' -X POST \
        -H "X-Api-Key: $api_key" \
        -H "Content-Type: application/json" \
        -d "$payload" \
        "${url}/api/${api_ver}/${endpoint}" 2>/dev/null
}

# ==============================================================================================
# ── PAYLOAD BUILDER ───────────────────────────────────────────────────────────────────────────
# ==============================================================================================
# Builds the minimal POST body to add an item to an arr.
# Uses the TARGET node's own defaults — never copies source node settings.

_build_payload() {
    local arr_type="$1" stable_id="$2" display_name="$3" monitored="$4" defaults_json="$5"
    case "$arr_type" in
        lidarr)
            jq -n \
                --arg    id  "$stable_id" \
                --arg    nm  "$display_name" \
                --argjson mn "$monitored" \
                --argjson df "$defaults_json" \
                '{
                    foreignArtistId:  $id,
                    artistName:       $nm,
                    monitored:        $mn,
                    qualityProfileId:  $df.qualityProfileId,
                    metadataProfileId: ($df.metadataProfileId // 1),
                    rootFolderPath:    $df.rootFolderPath,
                    addOptions: {monitor: "all", searchForMissingAlbums: false}
                }'
            ;;
        sonarr)
            jq -n \
                --argjson id  "$stable_id" \
                --arg     nm  "$display_name" \
                --argjson mn  "$monitored" \
                --argjson df  "$defaults_json" \
                '{
                    tvdbId:           $id,
                    title:            $nm,
                    monitored:        $mn,
                    qualityProfileId:  $df.qualityProfileId,
                    rootFolderPath:    $df.rootFolderPath,
                    seasons:           [],
                    addOptions: {searchForMissingEpisodes: false, monitor: "all"}
                }'
            ;;
        radarr)
            jq -n \
                --argjson id  "$stable_id" \
                --arg     nm  "$display_name" \
                --argjson mn  "$monitored" \
                --argjson df  "$defaults_json" \
                '{
                    tmdbId:           $id,
                    title:            $nm,
                    monitored:        $mn,
                    qualityProfileId:  $df.qualityProfileId,
                    rootFolderPath:    $df.rootFolderPath,
                    addOptions: {searchForMovie: false}
                }'
            ;;
    esac
}

# ==============================================================================================
# ── MONITORED ENFORCEMENT — re-monitor any unmonitored items via bulk editor ──────────────────
# ==============================================================================================
# All library members must be monitored. Unmonitored items won't be searched and eventually
# lose their files when cleanup runs after the series is removed from the arr.

_enforce_monitored() {
    local arr_type="$1" url="$2" api_key="$3" api_ver="$4" local_json="$5"

    local bulk_endpoint ids_key
    case "$arr_type" in
        sonarr) bulk_endpoint="series/editor"; ids_key="seriesIds"  ;;
        radarr) bulk_endpoint="movie/editor";  ids_key="movieIds"   ;;
        lidarr) bulk_endpoint="artist/editor"; ids_key="artistIds"  ;;
        *)      return 0 ;;
    esac

    local ids_json count
    ids_json=$(echo "$local_json" | jq '[.[] | select(.monitored == false) | .id]' 2>/dev/null)
    count=$(echo "$ids_json" | jq 'length' 2>/dev/null || echo 0)
    [[ "$count" -eq 0 ]] && return 0

    warn "${arr_type^}: $count unmonitored items — re-monitoring"

    if [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN: would re-monitor $count ${arr_type^} items"
        return 0
    fi

    local body http_code
    body=$(jq -n --arg k "$ids_key" --argjson ids "$ids_json" '{($k): $ids, monitored: true}')
    http_code=$(curl -sf -o /dev/null -w '%{http_code}' -X PUT \
        -H "X-Api-Key: $api_key" \
        -H "Content-Type: application/json" \
        -d "$body" \
        "${url}/api/${api_ver}/${bulk_endpoint}" 2>/dev/null)

    if [[ "$http_code" == "200" || "$http_code" == "202" ]]; then
        echo "${arr_type^}: re-monitored $count items ✅"
    else
        warn "${arr_type^}: bulk re-monitor failed (HTTP $http_code)"
    fi
}

# ==============================================================================================
# ── CORE SYNC — one arr type across all remote nodes ──────────────────────────────────────────
# ==============================================================================================

_sync_arr() {
    local arr_type="$1"
    local port="${_PORT[$arr_type]}"
    local api_ver="${_VER[$arr_type]}"
    local endpoint="${_EP[$arr_type]}"
    local id_field="${_ID[$arr_type]}"
    local name_field="${_NAME[$arr_type]}"
    local id_type="${_ID_TYPE[$arr_type]}"

    # Resolve local credentials
    local local_url local_key
    case "$arr_type" in
        lidarr) local_url="${LIDARR_URL:-}"; local_key="${LIDARR_API_KEY:-}" ;;
        sonarr) local_url="${SONARR_URL:-}"; local_key="${SONARR_API_KEY:-}" ;;
        radarr) local_url="${RADARR_URL:-}"; local_key="${RADARR_API_KEY:-}" ;;
    esac

    if [[ -z "$local_url" ]] || [[ -z "$local_key" ]]; then
        log "${arr_type^}: not configured on $MY_ID — skipping"
        return 0
    fi

    echo ""
    echo "━━━ ${arr_type^} ━━━"

    # ── Fetch local library ────────────────────────────────────────────────────────────────────
    local local_json
    local_json=$(_local_library "$local_url" "$local_key" "$api_ver" "$endpoint")
    if [[ -z "$local_json" ]] || ! echo "$local_json" | jq -e '.' >/dev/null 2>&1; then
        warn "${arr_type^}: could not fetch local library — skipping"
        return 0
    fi

    # Build local ID map: stable_id → "display_name|monitored"
    declare -A local_ids
    local local_count=0
    local _jq_id
    [[ "$id_type" == "string" ]] && _jq_id=".${id_field}" || _jq_id="(.${id_field} | tostring)"
    while IFS=$'\t' read -r stable_id display_name monitored; do
        [[ -z "$stable_id" ]] && continue
        local_ids["$stable_id"]="${display_name}|${monitored}"
        (( local_count++ ))
    done < <(echo "$local_json" | jq -r \
        ".[] | [${_jq_id}, .${name_field}, (.monitored | tostring)] | @tsv" 2>/dev/null)
    unset _jq_id

    log "${arr_type^}: $local_count items in local library"

    # ── Enforce monitored — fix any unmonitored items before sync ─────────────────────────────
    _enforce_monitored "$arr_type" "$local_url" "$local_key" "$api_ver" "$local_json"

    local total_added_local=0 total_added_remote=0 total_skipped=0

    # ── Sync with each remote node ─────────────────────────────────────────────────────────────
    for node_id in "${REMOTE_NODES[@]}"; do
        local node_name="${!node_id}"
        log "${arr_type^}: syncing with $node_name..."

        local node_ip
        node_ip=$(_resolve_node_ip "$node_id") || {
            warn "${arr_type^}: cannot resolve Tailscale IP for $node_name — skipping"
            continue
        }

        if ! _remote_arr_up "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type"; then
            log "${arr_type^}: not reachable on $node_name — skipping"
            continue
        fi

        local remote_json
        remote_json=$(_remote_library "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type" "$endpoint")
        if [[ -z "$remote_json" ]] || ! echo "$remote_json" | jq -e '.' >/dev/null 2>&1; then
            warn "${arr_type^}: could not fetch library from $node_name — skipping"
            continue
        fi

        # Build remote ID map
        declare -A remote_ids
        local remote_count=0
        local _jq_id
        [[ "$id_type" == "string" ]] && _jq_id=".${id_field}" || _jq_id="(.${id_field} | tostring)"
        while IFS=$'\t' read -r stable_id display_name monitored; do
            [[ -z "$stable_id" ]] && continue
            remote_ids["$stable_id"]="${display_name}|${monitored}"
            (( remote_count++ ))
        done < <(echo "$remote_json" | jq -r \
            ".[] | [${_jq_id}, .${name_field}, (.monitored | tostring)] | @tsv" 2>/dev/null)
        unset _jq_id

        log "${arr_type^}: $remote_count items on $node_name"

        # ── Remote → Local: items on remote not in local ───────────────────────────────────────
        # Skipped in force mode — local arr is authoritative; remote tracking does not propagate back.
        if [[ "$FORCE_MODE" == false ]]; then
            local to_add_local=()
            for stable_id in "${!remote_ids[@]}"; do
                [[ -n "${local_ids[$stable_id]:-}" ]] && continue
                if _is_blocklisted "$arr_type" "$stable_id"; then
                    log "BLOCKLISTED [$arr_type] $stable_id — skipping"
                    (( total_skipped++ ))
                    continue
                fi
                to_add_local+=("$stable_id")
            done

            if [[ "${#to_add_local[@]}" -gt 0 ]]; then
                local local_defs
                local_defs=$(_local_defaults "$local_url" "$local_key" "$api_ver" "$arr_type")
                if [[ -z "$local_defs" ]]; then
                    warn "${arr_type^}: could not fetch local defaults — skipping adds from $node_name"
                else
                    for stable_id in "${to_add_local[@]}"; do
                        IFS='|' read -r display_name _ <<< "${remote_ids[$stable_id]}"
                        local payload
                        payload=$(_build_payload "$arr_type" "$stable_id" "$display_name" \
                                                "true" "$local_defs")
                        if [[ "$DRY_RUN" == true ]]; then
                            log "DRY RUN: would add to local ${arr_type^}: $display_name ($stable_id)"
                            (( total_added_local++ ))
                        else
                            local http_code
                            http_code=$(_local_add "$local_url" "$local_key" "$api_ver" \
                                                   "$endpoint" "$payload")
                            if [[ "$http_code" == "201" ]] || [[ "$http_code" == "200" ]]; then
                                log "Added to local ${arr_type^}: $display_name"
                                local_ids["$stable_id"]="${display_name}|true"
                                (( total_added_local++ ))
                            else
                                warn "Failed to add to local ${arr_type^}: $display_name (HTTP $http_code)"
                            fi
                        fi
                    done
                fi
            fi
        else
            log "${arr_type^}: force mode — skipping remote→local (local is authoritative)"
        fi

        # ── Local → Remote: items on local not on remote ───────────────────────────────────────
        local to_add_remote=()
        for stable_id in "${!local_ids[@]}"; do
            [[ -n "${remote_ids[$stable_id]:-}" ]] && continue
            if _is_blocklisted "$arr_type" "$stable_id"; then
                (( total_skipped++ ))
                continue
            fi
            to_add_remote+=("$stable_id")
        done

        if [[ "${#to_add_remote[@]}" -gt 0 ]]; then
            local remote_defs
            remote_defs=$(_remote_defaults "$node_id" "$node_ip" "$port" "$api_ver" "$arr_type")
            if [[ -z "$remote_defs" ]]; then
                warn "${arr_type^}: could not fetch defaults from $node_name — skipping remote adds"
            else
                for stable_id in "${to_add_remote[@]}"; do
                    IFS='|' read -r display_name _ <<< "${local_ids[$stable_id]}"
                    local payload
                    payload=$(_build_payload "$arr_type" "$stable_id" "$display_name" \
                                            "true" "$remote_defs")
                    if [[ "$DRY_RUN" == true ]]; then
                        log "DRY RUN: would add to $node_name ${arr_type^}: $display_name ($stable_id)"
                        (( total_added_remote++ ))
                    else
                        local encoded http_code
                        encoded=$(printf '%s' "$payload" | base64 -w0)
                        http_code=$(_remote_add "$node_id" "$node_ip" "$port" "$api_ver" \
                                               "$arr_type" "$endpoint" "$encoded")
                        if [[ "$http_code" == "201" ]] || [[ "$http_code" == "200" ]]; then
                            log "Added to $node_name ${arr_type^}: $display_name"
                            (( total_added_remote++ ))
                        else
                            warn "Failed to add to $node_name ${arr_type^}: $display_name (HTTP $http_code)"
                        fi
                    fi
                done
            fi
        fi

        # ── Force mode: trigger library rescan on remote ──────────────────────────────────────────
        # After files are settled by merge-run and tracking pushed by force arr-sync, the remote
        # arr rescans its library so it accepts the authoritative file versions as ground truth.
        if [[ "$FORCE_MODE" == true ]]; then
            if [[ "$DRY_RUN" == true ]]; then
                log "DRY RUN: would trigger ${arr_type^} rescan on $node_name"
            else
                _trigger_rescan "$arr_type" "$node_id" "$node_ip" "$port" "$api_ver"
            fi
        fi

        local _n_local=${#to_add_local[@]}
        local _n_remote=${#to_add_remote[@]}
        log "  $node_name: +${_n_local} local | +${_n_remote} remote | $total_skipped blocklisted"

        unset remote_ids to_add_local to_add_remote _n_local _n_remote
        declare -A remote_ids
    done

    echo "  Total added to local: $total_added_local | to remotes: $total_added_remote | blocklisted: $total_skipped"
    unset local_ids
    declare -A local_ids
}

# ==============================================================================================
# ━━━ Main ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Loading Blocklist ━━━"
_load_blocklist

START=$(date +%s)

for arr_type in "${ARR_TYPES[@]}"; do
    _sync_arr "$arr_type"
done

END=$(date +%s)

echo ""
echo "━━━━━ $ICON_SUMMARY ARR SYNC SUMMARY ━━━━━"
echo "$ICON_HOST  Node:       $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_SYNC  Peers:      ${REMOTE_NODES[*]}"
echo "$ICON_TIME  Duration:   $(format_duration $(( END - START )))"
[[ "$DRY_RUN"   == true ]] && warn "DRY RUN — no changes were made"
[[ "$FORCE_MODE" == true ]] && echo "  Mode: force (local authoritative — remote→local skipped, rescan triggered)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

exit 0
