#!/bin/bash
# ==============================================================================================
# ================================= Downloaders Reset ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Maintenance reset for all download clients on this server. Clears accumulated
# state that download clients generate but never clean up themselves — stuck
# searches, dead transfers, failed imports, stale queue entries, completed history.
#
# Called every 30 minutes by critical_sync_maintenance.sh via
# CRITICAL_MAINTENANCE_SCRIPTS. Can also be run manually for ad hoc cleanup.
# If a downloader is not configured for this host, that section skips cleanly.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# slskd
#   Stuck searches    — clears Completed/Errored searches left by Soularr crashes
#                       prevents 409 Conflict on next Soularr startup
#   Dead transfers    — removes completed/errored/aborted transfer records per user
#                       prevents Soularr 404 loop when polling a user whose transfer is gone
#                       NEVER removes InProgress or Queued transfers
#   Failed imports    — purges albums Soularr downloaded but Lidarr rejected
#                       Soularr moves these to failed_imports/ and never cleans them up
#
# SABnzbd
#   Completed history — removes completed download records older than DOWNLOADER_RETENTION_DAYS
#   Failed history    — removes failed download records older than DOWNLOADER_RETENTION_DAYS
#   Stalled queue     — removes Paused or Stuck queue items no longer progressing
#                       active downloading items are never touched
#
# qBittorrent
#   Age failsafe      — removes torrents older than QBIT_FAILSAFE_MIN_DAYS
#                       deleteFiles=false — removes from qBit, leaves files for arrs to manage
#                       optional ratio requirement via QBIT_FAILSAFE_MIN_RATIO
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Never Interrupt Active Downloads
#   Each downloader section checks for active state before any removal. slskd
#   skips users with InProgress or Queued transfers. SABnzbd only removes items
#   past the retention threshold. qBittorrent applies minimum age and optional
#   ratio requirements. In-progress work is never touched.
#
# Graceful Skip on Unavailability
#   If a downloader's URL is empty or the service is unreachable, that section
#   skips cleanly with a log message. The script never exits fatally on a single
#   unreachable downloader — the others still run.
#
# Host-Aware Configuration
#   detect_hosts() aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
#   to their unprefixed names. Downloaders not configured for this host are absent
#   from the aliased vars and skip automatically.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   Failed-import purging deletes directories owned by container users.
#
# Dependency Check
#   Verifies curl and jq exist before any API work. jq backs the slskd connection
#   probe — without it the probe returns false forever, the script burns its full
#   60 second reconnect wait, then skips every slskd section as "disconnected".
#   A missing dependency is reported as itself rather than as a phantom outage.
#
# Active Transfer Protection
#   slskd: skips users with InProgress or Queued transfers before any removal.
#   SABnzbd: age threshold enforced before deletion.
#   qBittorrent: minimum age plus optional ratio gate before failsafe removal.
#
# Reachability Check
#   Each section validates its downloader URL before API calls. Missing or
#   unreachable downloaders skip without affecting other sections.
#
# No Downloaders Guard
#   Exits cleanly when none of SLSKD_URL, SABNZBD_URL or QBIT_URL are set for
#   this host — nothing configured is not an error.
#
# Timeout Protection
#   Every curl carries --max-time. An unresponsive downloader cannot stall the
#   30 minute maintenance cycle or overlap the next run.
#
# Deletion Scope Limit
#   qBittorrent removals pass deleteFiles=false — the torrent record is dropped
#   but files on disk are left for the arrs to manage. This script never deletes
#   media.
#
# Host Detection
#   detect_hosts() identifies which server is running the script and aliases
#   all HOST*_SLSKD_*, HOST*_SABNZBD_*, and HOST*_QBIT_* vars to the correct
#   host's values. Downloaders not configured on this host skip automatically.
#
# Lock Acquisition
#   acquire_lock "wait" — waits for previous run to finish since this runs every
#   30 minutes and prior execution may still be completing.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_SLSKD_URL / HOST*_SLSKD_API_KEY / HOST*_SLSKD_FAILED_IMPORTS_DIR
#       slskd connection and failed imports path. Aliased by detect_hosts()
#
#   HOST*_SABNZBD_URL / HOST*_SABNZBD_API_KEY
#       SABnzbd connection details. Aliased by detect_hosts()
#
#   HOST*_QBIT_URL / HOST*_QBIT_USERNAME / HOST*_QBIT_PASSWORD
#       qBittorrent connection details. Aliased by detect_hosts()
#
# master.conf
#
#   DOWNLOADER_RETENTION_DAYS
#       Days before SABnzbd history entries (completed or failed) are removed
#
#   QBIT_FAILSAFE_MIN_DAYS
#       Minimum torrent age in days before failsafe removal is considered
#
#   QBIT_FAILSAFE_MIN_RATIO
#       Minimum seeding ratio required alongside age gate (0 = age only)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# downloaders_reset.sh
#     Run maintenance reset for all configured download clients
#
# downloaders_reset.sh --dry-run
#     Preview what would be removed without making any changes
#
# downloaders_reset.sh --status
#     Show configured downloaders, current queue depths, and retention settings
#
# downloaders_reset.sh --log
#     Verbose per-client per-item output
#
# ==============================================================================================

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

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

parse_args "$@"

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

# Lock first — wait mode since this runs every 30min and previous may still be finishing
acquire_lock "wait"

# jq backs the slskd connection probe. Missing, the probe never returns true and slskd
# looks permanently disconnected — a 60s wait followed by silently skipped sections.
for _dep in curl jq; do
    if ! command -v "$_dep" &>/dev/null; then
        error "$_dep not found — required for downloader API calls"
        notify "Downloaders reset failed on $(hostname) — $_dep not installed" "Downloaders Reset" "warning"
        exit 1
    fi
done
unset _dep

# detect_hosts() sets MY_ID and aliases all HOST*_SLSKD_*, HOST*_SABNZBD_*, HOST*_QBIT_* vars
detect_hosts

START_TIME=$(date +%s)
CUTOFF=$(( $(date +%s) - (DOWNLOADER_RETENTION_DAYS * 86400) ))
TOTAL_PASS=0
TOTAL_FAIL=0

log "$ICON_GEAR Config: retention=${DOWNLOADER_RETENTION_DAYS}d qbit-age=${QBIT_FAILSAFE_MIN_DAYS}d qbit-ratio=${QBIT_FAILSAFE_MIN_RATIO}"

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST    Identity:    $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_GEAR    slskd:       ${SLSKD_URL:-not configured}"
    echo "$ICON_GEAR    SABnzbd:     ${SABNZBD_URL:-not configured}"
    echo "$ICON_GEAR    qBittorrent: ${QBIT_URL:-not configured}"
    echo "$ICON_TIME    Retention:   ${DOWNLOADER_RETENTION_DAYS} days"
    echo "$ICON_GEAR    qBit age:    ${QBIT_FAILSAFE_MIN_DAYS} days"
    echo "$ICON_GEAR    qBit ratio:  ${QBIT_FAILSAFE_MIN_RATIO} (0=age only)"
    echo "$ICON_NOTIFY  Notify:      unRAID=${NOTIFY_UNRAID:-false} Discord=$([[ -n "${MY_DISCORD_WEBHOOK:-}" ]] && echo enabled || echo disabled)"
    echo "$ICON_GEAR    Dry Run:     $DRY_RUN"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

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

# Log which downloaders are active on this host
if [[ -z "$SLSKD_URL" ]] && [[ -z "$SABNZBD_URL" ]] && [[ -z "$QBIT_URL" ]]; then
    warn "No downloaders configured for $MY_ID — nothing to reset"
    exit 0
fi
[[ -n "$SLSKD_URL" ]]   && log "slskd active on $MY_ID"
[[ -n "$SABNZBD_URL" ]] && log "SABnzbd active on $MY_ID"
[[ -n "$QBIT_URL" ]]    && log "qBittorrent active on $MY_ID"

# ==============================================================================================
# ━━━ slskd — Connection Check ━━━
# ==============================================================================================
# slskd's internal watchdog doesn't always recover from disconnection. Check before
# running API-dependent sections; attempt reconnect if down.

SLSKD_CONNECTED=false

if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]]; then
    echo ""
    echo "━━━ $ICON_SYNC slskd — Connection Check ━━━"

    _slskd_is_connected() {
        local state
        state=$(curl -sf --max-time 10 \
            -H "X-Api-Key: $SLSKD_API_KEY" \
            "$SLSKD_URL/api/v0/application" 2>/dev/null | \
            jq -r '.server.isConnected // false' 2>/dev/null)
        [[ "$state" == "true" ]]
    }

    if _slskd_is_connected; then
        echo "$ICON_DONE [OK]    slskd connected to Soulseek ✅"
        SLSKD_CONNECTED=true
    else
        warn "slskd disconnected — triggering reconnect"
        curl -sf --max-time 10 -X PUT \
            -H "X-Api-Key: $SLSKD_API_KEY" \
            -H "Content-Type: application/json" \
            "$SLSKD_URL/api/v0/server" \
            -d '{"address":"server.slsknet.org","port":2242}' \
            >/dev/null 2>&1

        _ELAPSED=0
        while [[ "$_ELAPSED" -lt 60 ]]; do
            sleep 10
            _ELAPSED=$(( _ELAPSED + 10 ))
            if _slskd_is_connected; then
                echo "slskd reconnected after ${_ELAPSED}s ✅"
                SLSKD_CONNECTED=true
                break
            fi
            log "  waiting... (${_ELAPSED}s / 60s)"
        done

        [[ "$SLSKD_CONNECTED" != true ]] && \
            warn "slskd still disconnected after 60s — skipping API-dependent sections"
    fi
fi

# ==============================================================================================
# ━━━ slskd — Stuck Searches ━━━
# ==============================================================================================
# Clears searches in Completed/Errored state left by Soularr crashes.
# Prevents 409 Conflict error on next Soularr startup when it tries to
# create a search with the same ID that already exists in a terminal state.

if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
    echo ""
    echo "━━━ 🔍 slskd — Stuck Searches ━━━"

    SEARCHES=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/searches" \
        -H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)

    if [[ -z "$SEARCHES" ]]; then
        warn "slskd not reachable — skipping searches"
    else
        IDS=$(echo "$SEARCHES" | tr '{' '\n' | \
            grep '"isComplete":true' | grep '"searchText":' | \
            grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')
        COUNT=$(echo "$IDS" | grep -c . 2>/dev/null || echo 0)
        COUNT="${COUNT//[^0-9]/}"; COUNT="${COUNT:-0}"

        if [[ "$COUNT" -eq 0 ]]; then
            success "No stuck searches found ✅"
        else
            log "Found $COUNT stuck search(es)"
            SUCCESS=0; FAIL=0
            while IFS= read -r ID; do
                [[ -z "$ID" ]] && continue
                if [[ "$DRY_RUN" == true ]]; then
                    warn "DRY RUN — would delete search: $ID"
                    ((SUCCESS++))
                    continue
                fi
                RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
                    "$SLSKD_URL/api/v0/searches/$ID" \
                    -H "X-Api-Key: $SLSKD_API_KEY")
                if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
                    log "$ICON_TRASH Cleared search: $ID"
                    ((SUCCESS++))
                else
                    error "Failed: $ID (HTTP $RESULT)"
                    ((FAIL++))
                fi
            done <<< "$IDS"
            success "Searches: $SUCCESS cleared, $FAIL failed"
            (( TOTAL_FAIL += FAIL ))
            (( TOTAL_PASS += SUCCESS ))
        fi
    fi
fi

# ==============================================================================================
# ━━━ slskd — Dead Transfer Records ━━━
# ==============================================================================================
# Removes completed/errored/aborted transfer records per user.
# Prevents Soularr 404 loop when polling a user whose transfer no longer exists.
# Safety: NEVER removes transfers that are InProgress or Queued — active downloads protected.

if [[ -n "$SLSKD_URL" ]] && [[ -n "$SLSKD_API_KEY" ]] && [[ "$SLSKD_CONNECTED" == true ]]; then
    echo ""
    echo "━━━ 🔍 slskd — Dead Transfer Records ━━━"

    TRANSFERS=$(curl -sf --max-time 10 -X GET "$SLSKD_URL/api/v0/transfers/downloads" \
        -H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)

    if [[ -z "$TRANSFERS" ]]; then
        warn "slskd not reachable — skipping transfers"
    else
        USERNAMES=$(echo "$TRANSFERS" | grep -o '"username":"[^"]*"' | \
            sed 's/"username":"//;s/"//' | sort -u)

        if [[ -z "$USERNAMES" ]]; then
            success "No transfer records found ✅"
        else
            USER_COUNT=$(echo "$USERNAMES" | grep -c . 2>/dev/null || echo 0)
            log "Found $USER_COUNT user(s) with transfer records"
            SUCCESS=0; SKIPPED=0; FAIL=0
            while IFS= read -r USER; do
                [[ -z "$USER" ]] && continue

                USER_DATA=$(curl -sf --max-time 10 \
                    "$SLSKD_URL/api/v0/transfers/downloads/$USER" \
                    -H "X-Api-Key: $SLSKD_API_KEY" 2>/dev/null)

                # Skip users with any active or queued transfers — never interrupt downloads
                ACTIVE=$(echo "$USER_DATA" | grep -c '"state":"InProgress"\|"state":"Queued"')
                if [[ "${ACTIVE:-0}" -gt 0 ]]; then
                    log "$ICON_SKIP Skipping $USER — has active/queued transfer(s)"
                    ((SKIPPED++))
                    continue
                fi

                # Extract IDs of terminal-state file transfers
                # Split at { so each file object lands on its own line, then grep for state
                FILE_IDS=$(echo "$USER_DATA" | tr '{' '\n' | \
                    grep '"state":"Completed"\|"state":"Errored"\|"state":"Aborted"\|"state":"Cancelled"' | \
                    grep -o '"id":"[^"]*"' | sed 's/"id":"//;s/"//')

                if [[ -z "$FILE_IDS" ]]; then
                    log "$ICON_SKIP Skipping $USER — no terminal-state transfers"
                    ((SKIPPED++))
                    continue
                fi

                if [[ "$DRY_RUN" == true ]]; then
                    F_COUNT=$(echo "$FILE_IDS" | grep -c .)
                    warn "DRY RUN — would clear $F_COUNT transfer(s) for: $USER"
                    ((SUCCESS++))
                    continue
                fi

                F_SUCCESS=0; F_FAIL=0
                while IFS= read -r FILE_ID; do
                    [[ -z "$FILE_ID" ]] && continue
                    RESULT=$(curl -sf --max-time 10 -o /dev/null -w "%{http_code}" -X DELETE \
                        "$SLSKD_URL/api/v0/transfers/downloads/$USER/$FILE_ID" \
                        -H "X-Api-Key: $SLSKD_API_KEY")
                    if [[ "$RESULT" == "200" || "$RESULT" == "204" ]]; then
                        ((F_SUCCESS++))
                    else
                        ((F_FAIL++))
                    fi
                done <<< "$FILE_IDS"

                log "$ICON_TRASH Cleared $F_SUCCESS transfer(s) for: $USER ($F_FAIL failed)"
                ((SUCCESS += F_SUCCESS))
                ((FAIL += F_FAIL))
            done <<< "$USERNAMES"
            success "Transfers: $SUCCESS cleared, $SKIPPED skipped (active/empty), $FAIL failed"
            (( TOTAL_FAIL += FAIL ))
            (( TOTAL_PASS += SUCCESS ))
        fi
    fi
fi

# ==============================================================================================
# ━━━ slskd — Purge Expired Failed Imports ━━━
# ==============================================================================================
# Removes albums Soularr downloaded but Lidarr rejected.
# Soularr moves rejected albums to failed_imports/ and never cleans them up.
# Purges directories older than DOWNLOADER_RETENTION_DAYS to prevent unbounded growth.

if [[ -n "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
    echo ""
    echo "━━━ 🔍 slskd — Failed Imports (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"

    if [[ ! -d "$SLSKD_FAILED_IMPORTS_DIR" ]]; then
        warn "Directory not found: $SLSKD_FAILED_IMPORTS_DIR — skipping"
    else
        OLD_IMPORTS=$(find "$SLSKD_FAILED_IMPORTS_DIR" \
            -mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}")
        IMPORT_COUNT=$(echo "$OLD_IMPORTS" | grep -c . 2>/dev/null || echo 0)
        IMPORT_COUNT="${IMPORT_COUNT//[^0-9]/}"; IMPORT_COUNT="${IMPORT_COUNT:-0}"

        if [[ "$IMPORT_COUNT" -eq 0 ]]; then
            success "No expired failed imports found ✅"
        else
            log "Found $IMPORT_COUNT expired failed import(s)"
            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would delete:"
                echo "$OLD_IMPORTS"
            else
                find "$SLSKD_FAILED_IMPORTS_DIR" \
                    -mindepth 1 -maxdepth 1 -mtime +"${DOWNLOADER_RETENTION_DAYS}" \
                    -exec rm -rf {} \;
                success "$ICON_TRASH Purged $IMPORT_COUNT expired failed import(s)"
                (( TOTAL_PASS += IMPORT_COUNT ))
            fi
        fi
    fi
fi

# ==============================================================================================
# ━━━ SABnzbd — Clear Completed History ━━━
# ==============================================================================================
# Removes completed download history older than DOWNLOADER_RETENTION_DAYS.
# Keeps recent history for reference — only purges what's past the retention window.

if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
    echo ""
    echo "━━━ 🔍 SABnzbd — Completed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"

    HISTORY=$(curl -sf --max-time 15 \
        "$SABNZBD_URL/api?mode=history&output=json&limit=1000&apikey=$SABNZBD_API_KEY" 2>/dev/null)

    if [[ -z "$HISTORY" ]]; then
        warn "SABnzbd not reachable — skipping completed history"
    else
        COMPLETED_IDS=$(echo "$HISTORY" | grep -o '"nzo_id":"[^"]*"' | \
            sed 's/"nzo_id":"//;s/"//')

        if [[ -z "$COMPLETED_IDS" ]]; then
            success "No completed history found ✅"
        else
            HIST_TOTAL=$(echo "$COMPLETED_IDS" | grep -c . 2>/dev/null || echo 0)
            log "Found $HIST_TOTAL completed history entries"
            DELETED=0; SKIPPED=0
            while IFS= read -r NZO_ID; do
                [[ -z "$NZO_ID" ]] && continue
                JOB_TIME=$(echo "$HISTORY" | grep -A5 "$NZO_ID" | \
                    grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
                [[ -z "$JOB_TIME" ]] && continue
                [[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
                if [[ "$DRY_RUN" == true ]]; then
                    warn "DRY RUN — would delete completed job: $NZO_ID"
                    ((DELETED++))
                else
                    curl -sf --max-time 10 \
                        "$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
                        >/dev/null
                    log "$ICON_TRASH Deleted: $NZO_ID"
                    ((DELETED++))
                fi
            done <<< "$COMPLETED_IDS"
            success "Completed: $DELETED deleted, $SKIPPED within retention"
            (( TOTAL_PASS += DELETED ))
        fi
    fi
fi

# ==============================================================================================
# ━━━ SABnzbd — Clear Failed History ━━━
# ==============================================================================================
# Removes failed download history older than DOWNLOADER_RETENTION_DAYS.
# Failed history is kept briefly for diagnosis but purged after the retention window.

if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
    echo ""
    echo "━━━ 🔍 SABnzbd — Failed History (older than ${DOWNLOADER_RETENTION_DAYS} days) ━━━"

    FAILED_HIST=$(curl -sf --max-time 15 \
        "$SABNZBD_URL/api?mode=history&output=json&limit=1000&failed_only=1&apikey=$SABNZBD_API_KEY" 2>/dev/null)

    if [[ -z "$FAILED_HIST" ]]; then
        warn "SABnzbd not reachable — skipping failed history"
    else
        FAILED_IDS=$(echo "$FAILED_HIST" | grep -o '"nzo_id":"[^"]*"' | \
            sed 's/"nzo_id":"//;s/"//')

        if [[ -z "$FAILED_IDS" ]]; then
            success "No failed history found ✅"
        else
            FAILED_TOTAL=$(echo "$FAILED_IDS" | grep -c . 2>/dev/null || echo 0)
            log "Found $FAILED_TOTAL failed history entries"
            DELETED=0; SKIPPED=0
            while IFS= read -r NZO_ID; do
                [[ -z "$NZO_ID" ]] && continue
                JOB_TIME=$(echo "$FAILED_HIST" | grep -A5 "$NZO_ID" | \
                    grep -o '"completed":[0-9]*' | grep -o '[0-9]*' | head -1)
                [[ -z "$JOB_TIME" ]] && continue
                [[ "$JOB_TIME" -gt "$CUTOFF" ]] && ((SKIPPED++)) && continue
                if [[ "$DRY_RUN" == true ]]; then
                    warn "DRY RUN — would delete failed job: $NZO_ID"
                    ((DELETED++))
                else
                    curl -sf --max-time 10 \
                        "$SABNZBD_URL/api?mode=history&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
                        >/dev/null
                    log "$ICON_TRASH Deleted: $NZO_ID"
                    ((DELETED++))
                fi
            done <<< "$FAILED_IDS"
            success "Failed: $DELETED deleted, $SKIPPED within retention"
            (( TOTAL_PASS += DELETED ))
        fi
    fi
fi

# ==============================================================================================
# ━━━ SABnzbd — Remove Stalled Queue Items ━━━
# ==============================================================================================
# Removes queue items in Paused or Stuck state that are no longer progressing.
# Active downloading items (Downloading, Grabbing) are never touched.
# Paused items may be intentional pauses — but in an automated environment
# a Paused item sitting in the queue indefinitely is effectively stalled.

if [[ -n "$SABNZBD_URL" ]] && [[ -n "$SABNZBD_API_KEY" ]]; then
    echo ""
    echo "━━━ 🔍 SABnzbd — Stalled Queue Items ━━━"

    QUEUE=$(curl -sf --max-time 10 \
        "$SABNZBD_URL/api?mode=queue&output=json&apikey=$SABNZBD_API_KEY" 2>/dev/null)

    if [[ -z "$QUEUE" ]]; then
        warn "SABnzbd not reachable — skipping queue"
    else
        STALLED_IDS=$(echo "$QUEUE" | grep -o '"nzo_id":"[^"]*"' | \
            sed 's/"nzo_id":"//;s/"//')

        if [[ -z "$STALLED_IDS" ]]; then
            success "No stalled queue items found ✅"
        else
            QUEUE_TOTAL=$(echo "$STALLED_IDS" | grep -c . 2>/dev/null || echo 0)
            log "Found $QUEUE_TOTAL queue item(s) — checking status"
            DELETED=0; SKIPPED=0
            while IFS= read -r NZO_ID; do
                [[ -z "$NZO_ID" ]] && continue
                STATUS=$(echo "$QUEUE" | grep -A10 "$NZO_ID" | \
                    grep -o '"status":"[^"]*"' | sed 's/"status":"//;s/"//')
                # Only remove Paused or Stuck items — Downloading/Grabbing are active
                if [[ "$STATUS" != "Paused" ]] && [[ "$STATUS" != "Stuck" ]]; then
                    ((SKIPPED++))
                    continue
                fi
                if [[ "$DRY_RUN" == true ]]; then
                    warn "DRY RUN — would remove stalled item: $NZO_ID ($STATUS)"
                    ((DELETED++))
                else
                    curl -sf --max-time 10 \
                        "$SABNZBD_URL/api?mode=queue&name=delete&value=$NZO_ID&apikey=$SABNZBD_API_KEY" \
                        >/dev/null
                    log "$ICON_TRASH Removed stalled ($STATUS): $NZO_ID"
                    ((DELETED++))
                fi
            done <<< "$STALLED_IDS"
            success "Queue: $DELETED removed, $SKIPPED active (skipped)"
            (( TOTAL_PASS += DELETED ))
        fi
    fi
fi

# ==============================================================================================
# ━━━ qBittorrent — Age Failsafe Cleanup ━━━
# ==============================================================================================
# Last-chance cleanup for torrents that have been sitting in qBit past their useful life.
# deleteFiles=false — removes the torrent record from qBit but leaves files on disk.
# Radarr/Sonarr manage actual files independently — this only cleans up the qBit entry.
#
# Safety checks before deletion:
#   Age must exceed QBIT_FAILSAFE_MIN_DAYS
#   Ratio must meet QBIT_FAILSAFE_MIN_RATIO (0 = age only, no ratio requirement)

if [[ -n "$QBIT_URL" ]] && [[ -n "$QBIT_USERNAME" ]]; then
    echo ""
    echo "━━━ 🔍 qBittorrent — Failsafe (older than ${QBIT_FAILSAFE_MIN_DAYS} days) ━━━"
    [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]] && \
        log "Ratio requirement: >= ${QBIT_FAILSAFE_MIN_RATIO}"

    QBIT_COOKIE=$(curl -sf --max-time 10 -c - \
        "$QBIT_URL/api/v2/auth/login" \
        --data "username=$QBIT_USERNAME&password=$QBIT_PASSWORD" 2>/dev/null | \
        grep SID | awk '{print "SID="$NF}')

    if [[ -z "$QBIT_COOKIE" ]]; then
        error "Failed to authenticate with qBittorrent — check QBIT_USERNAME/PASSWORD"
        notify "qBittorrent auth failed on $(hostname) — check credentials in host*.conf" "Downloaders Reset" "warning"
        ((TOTAL_FAIL++))
    else
        TORRENTS=$(curl -sf --max-time 15 \
            "$QBIT_URL/api/v2/torrents/info" \
            -H "Cookie: $QBIT_COOKIE" 2>/dev/null)

        NOW=$(date +%s)
        TORRENT_TOTAL=$(echo "$TORRENTS" | tr '}' '\n' | grep -c '"hash"' 2>/dev/null || echo 0)
        log "Found $TORRENT_TOTAL torrent(s) — applying age/ratio filter"
        DELETED=0; SKIPPED=0

        while read -r TORRENT; do
            [[ -z "$TORRENT" ]] && continue
            HASH=$(echo "$TORRENT" | grep -o '"hash":"[^"]*"' | sed 's/"hash":"//;s/"//')
            NAME=$(echo "$TORRENT" | grep -o '"name":"[^"]*"' | sed 's/"name":"//;s/"//')
            ADDED=$(echo "$TORRENT" | grep -o '"added_on":[0-9]*' | grep -o '[0-9]*')
            RATIO=$(echo "$TORRENT" | grep -o '"ratio":[0-9.]*' | grep -o '[0-9.]*')
            [[ -z "$HASH" || -z "$ADDED" ]] && continue

            AGE_DAYS=$(( (NOW - ADDED) / 86400 ))

            # Age check — must be old enough
            [[ "$AGE_DAYS" -lt "$QBIT_FAILSAFE_MIN_DAYS" ]] && ((SKIPPED++)) && continue

            # Ratio check — if configured. awk handles the fractional comparison;
            # bash's [[ -lt ]] only does integers and would treat e.g. 1.4 and 1.5
            # as equal once truncated. Missing/empty ratio defaults to 0 (protected).
            if [[ "$QBIT_FAILSAFE_MIN_RATIO" != "0" ]]; then
                if (( $(awk "BEGIN {print (${RATIO:-0} < $QBIT_FAILSAFE_MIN_RATIO) ? 1 : 0}") )); then
                    ((SKIPPED++)) && continue
                fi
            fi

            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would delete: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
                ((DELETED++))
            else
                curl -sf --max-time 10 -X POST \
                    "$QBIT_URL/api/v2/torrents/delete" \
                    -H "Cookie: $QBIT_COOKIE" \
                    --data "hashes=$HASH&deleteFiles=false" >/dev/null
                log "$ICON_TRASH Deleted: $NAME (${AGE_DAYS}d old, ratio: $RATIO)"
                ((DELETED++))
            fi
        done < <(echo "$TORRENTS" | tr '}' '\n')

        success "qBittorrent: $DELETED deleted, $SKIPPED skipped (under threshold)"
        (( TOTAL_PASS += DELETED ))
    fi
fi

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY DOWNLOADERS RESET SUMMARY ━━━━━"
echo "$ICON_HOST    Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME    Duration:  $(format_duration $(( $(date +%s) - START_TIME )))"
echo "$ICON_SUCCESS Actions:   $TOTAL_PASS"
echo "$ICON_ERROR   Failures:  $TOTAL_FAIL"
echo ""

if [[ "$DRY_RUN" == true ]]; then
    echo "$ICON_WARN  Status: DRY RUN — no changes made"
elif [[ "$TOTAL_FAIL" -gt 0 ]]; then
    echo "$ICON_ERROR Status: $TOTAL_FAIL failure(s) — check logs"
    notify "Downloaders reset completed with failures on $(hostname)" "Downloaders Reset" "warning"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    exit 1
else
    echo "$ICON_DONE  Status: $ICON_SUCCESS DONE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0