#!/bin/bash
# ==============================================================================================
# ================================= Docker Daily Restart =======================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Restarts configured containers every night at 1am as proactive maintenance.
#
# Called by daily_sync_maintenance.sh via DAILY_MAINTENANCE_SCRIPTS. Runs inside
# the daily maintenance window — any service downtime is absorbed by a window
# that is already happening. Also drives docker_update.sh in normal mode: the
# same DAILY_RESTART_CONTAINERS list is used for both restarts and image pulls,
# so there is no second list to maintain.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Proactive Maintenance
#   Daily restarts target containers known to degrade over time without
#   crossing a clear failure threshold — connection table growth, scheduler
#   state accumulation, session cache bloat. The watchdog cannot detect this
#   class of degradation. Scheduled restarts clear it before it becomes visible.
#
# State Respect
#   Running containers are restarted. Stopped containers are left stopped — they
#   were intentionally halted and this script has no authority to override that
#   decision. This rule is consistent across the entire ecosystem.
#
# Dependency-Safe Ordering
#   Restarts follow the same dependency ordering used by docker_watchdog.sh.
#   Services that other containers depend on restart first. A dependent is never
#   restarted while its dependency is still coming up.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Dependency Ordering
#   Containers restart in dependency-safe order using HOST*_WATCHDOG_DEPENDENCIES.
#   CONTAINER_DELAY seconds between dependency restart and dependent restart gives
#   the dependency time to fully initialise before dependents try to connect.
#
# Restart Verification
#   After each restart, container state is checked after a settle period. A
#   container that starts and immediately crashes is marked failed and a
#   notification is sent — the script does not silently pass a restart that
#   did not stick.
#
# Timeout Protection
#   All docker commands wrapped in a 30 second timeout. A hung Docker daemon
#   cannot cause this script to hang indefinitely. Timed-out commands retry
#   per RETRY_COUNT before marking as failed.
#
# Lock Acquisition
#   acquire_lock() prevents concurrent execution if a previous run is still active.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
#   HOST*_DAILY_RESTART_CONTAINERS
#       Containers restarted nightly. Also used by docker_update.sh normal mode
#       for image pulls — add a container once, it gets both. Aliased by
#       detect_hosts() → DAILY_RESTART_CONTAINERS
#
#   HOST*_WATCHDOG_DEPENDENCIES
#       Dependency ordering shared with docker_watchdog.sh. Aliased by
#       detect_hosts() → WATCHDOG_DEPENDENCIES
#
# master.conf
#
#   RETRY_COUNT
#       Retry attempts before giving up on a container
#
#   SLEEP
#       Seconds between retry attempts
#
#   CONTAINER_DELAY
#       Seconds to wait after restarting a dependency before starting its dependents
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_daily_restart.sh
#     Restart all containers in DAILY_RESTART_CONTAINERS
#
# docker_daily_restart.sh --dry-run
#     Preview which containers would be restarted and which would be skipped
#
# docker_daily_restart.sh --status
#     Show configured restart list, container states, and dependency ordering
#
# docker_daily_restart.sh --log
#     Verbose per-container execution 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
acquire_lock

if ! command -v docker &>/dev/null; then
    error "Docker command not found — check PATH or Docker installation"
    notify "Docker daily restart failed — Docker not found on $(hostname)" "Docker Daily Restart" "warning"
    exit 1
fi

# detect_hosts() sets MY_ID and aliases HOST*_DAILY_RESTART_CONTAINERS → DAILY_RESTART_CONTAINERS
detect_hosts

if [[ ${#DAILY_RESTART_CONTAINERS[@]} -eq 0 ]]; then
    warn "DAILY_RESTART_CONTAINERS is empty for $MY_ID — nothing to restart"
    warn "Check HOST${MY_ID#HOST}_DAILY_RESTART_CONTAINERS in host*.conf"
    exit 0
fi

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
    echo "$ICON_HOST       Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
    echo "$ICON_RETRY      Retries:   $RETRY_COUNT"
    echo "$ICON_TIME       Sleep:     ${SLEEP}s between retries"
    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 containers will be restarted"

# ==============================================================================================
# ── FUNCTIONS ─────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================

# Wraps docker commands with a 30 second timeout.
# Prevents a hung Docker daemon from causing the script to hang indefinitely.
# Usage: docker_cmd docker restart ContainerName
DOCKER_TIMEOUT=30
docker_cmd() {
    timeout "$DOCKER_TIMEOUT" "$@"
    local exit_code=$?
    if [[ "$exit_code" -eq 124 ]]; then
        error "Docker command timed out after ${DOCKER_TIMEOUT}s: $*"
        return 1
    fi
    return "$exit_code"
}

# Verifies a container is still running after restart.
# Gives the container a short settle period before checking.
# Returns 0 if running, 1 if crashed or stopped.
RESTART_VERIFY_WAIT=5   # seconds to wait before checking state post-restart
verify_running() {
    local container="$1"
    sleep "$RESTART_VERIFY_WAIT"
    local state
    state=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)
    if [[ "$state" != "true" ]]; then
        error "$container failed to stay running after restart — may have crashed"
        return 1
    fi
    return 0
}

# Builds a dependency-safe restart order from DAILY_RESTART_CONTAINERS.
# Containers that are dependencies of others restart first.
# Returns ordered list in ORDERED_RESTART array.
build_restart_order() {
    ORDERED_RESTART=()
    local remaining=("${DAILY_RESTART_CONTAINERS[@]}")
    local placed=()

    # First pass — add dependency containers that appear in our list
    for container in "${remaining[@]}"; do
        [[ -z "$container" ]] && continue
        local is_dependency=false
        # Check if this container is a dependency of any other in our list
        for dep_string in "${WATCHDOG_DEPENDENCIES[@]}"; do
            if [[ "$dep_string" == *"$container"* ]]; then
                is_dependency=true
                break
            fi
        done
        # Also check associative array format
        for dependent in "${!WATCHDOG_DEPENDENCIES[@]}"; do
            if [[ "${WATCHDOG_DEPENDENCIES[$dependent]}" == *"$container"* ]]; then
                is_dependency=true
                break
            fi
        done
        if [[ "$is_dependency" == true ]]; then
            # Check not already placed
            local already=false
            for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
            if [[ "$already" == false ]]; then
                ORDERED_RESTART+=("$container")
                placed+=("$container")
            fi
        fi
    done

    # Second pass — add remaining containers (dependents and independents)
    for container in "${remaining[@]}"; do
        [[ -z "$container" ]] && continue
        local already=false
        for p in "${placed[@]}"; do [[ "$p" == "$container" ]] && already=true && break; done
        if [[ "$already" == false ]]; then
            ORDERED_RESTART+=("$container")
            placed+=("$container")
        fi
    done

    log "Restart order: ${ORDERED_RESTART[*]}"
}

# Checks if a container is a dependent of the previously restarted container.
# If so, waits CONTAINER_DELAY before restarting to allow dependency to settle.
# Usage: check_dependency_delay "$container" "$last_restarted"
check_dependency_delay() {
    local container="$1"
    local last="$2"
    [[ -z "$last" ]] && return

    local deps="${WATCHDOG_DEPENDENCIES[$container]:-}"
    if [[ -n "$deps" ]] && [[ "$deps" == *"$last"* ]]; then
        echo "  Waiting ${CONTAINER_DELAY}s — $container depends on $last..."
        sleep "$CONTAINER_DELAY"
    fi
}

# Retries a docker command up to RETRY_COUNT times with SLEEP seconds between attempts.
# Uses docker_cmd wrapper for timeout protection on each attempt.
# Usage: retry_docker docker restart ContainerName
retry_docker() {
    local attempt=1

    while [[ "$attempt" -le "$RETRY_COUNT" ]]; do
        log "$ICON_RETRY Attempt $attempt of $RETRY_COUNT: $*"

        if docker_cmd "$@"; then
            log "Succeeded on attempt $attempt"
            return 0
        else
            warn "Attempt $attempt failed"
            (( attempt++ ))
            [[ "$attempt" -le "$RETRY_COUNT" ]] && sleep "$SLEEP"
        fi
    done

    error "Command failed after $RETRY_COUNT attempts: $*"
    return 1
}

# ==============================================================================================
# ━━━ Daily Restart ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_CONTAINERS Daily Restart — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME) — ${#DAILY_RESTART_CONTAINERS[@]} container(s)"
log "$ICON_CONTAINERS Containers: ${DAILY_RESTART_CONTAINERS[*]}"
log "$ICON_RETRY      Retries:    $RETRY_COUNT"

START=$(date +%s)
FAILED=()
RESTARTED=()
SKIPPED=()

# Build dependency-safe restart order
build_restart_order
log "$ICON_GEAR Restart order: ${ORDERED_RESTART[*]}"

LAST_RESTARTED=""

for container in "${ORDERED_RESTART[@]}"; do
    [[ -z "$container" ]] && continue
    log "━━━ $ICON_CONTAINERS $container ━━━"

    if ! timeout "$DOCKER_TIMEOUT" docker inspect "$container" &>/dev/null; then
        warn "$container does not exist — skipping"
        continue
    fi

    STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null)

    case "$STATUS" in
        true)
            log "$ICON_RUNNING $container is running — restarting..."

            # Wait if this container depends on the last one restarted
            check_dependency_delay "$container" "$LAST_RESTARTED"

            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would restart $container"
                RESTARTED+=("$container")
            else
                if retry_docker docker restart "$container"; then
                    # Verify container stayed running after restart
                    if verify_running "$container"; then
                        log "$ICON_STARTED $container restarted and running ✅"
                        RESTARTED+=("$container")
                        LAST_RESTARTED="$container"
                    else
                        error "$container restarted but crashed immediately"
                        notify "$container crashed after restart on $(hostname)" "Docker Daily Restart" "warning"
                        FAILED+=("$container")
                    fi
                else
                    error "Failed to restart $container after $RETRY_COUNT attempts"
                    notify "$container failed to restart on $(hostname)" "Docker Daily Restart" "warning"
                    FAILED+=("$container")
                fi
            fi
            ;;
        false)
            log "$ICON_NOT_RUNNING $container is stopped — skipping"
            SKIPPED+=("$container")
            ;;
        *)
            error "Unknown status for $container: $STATUS"
            FAILED+=("$container")
            ;;
    esac
done

END=$(date +%s)

# ==============================================================================================
# ━━━ Prune Old Images ━━━
# ==============================================================================================
# Restarts above swap containers onto new images — old images are now dangling. Prune immediately.
echo ""
echo "━━━ $ICON_SYNC Pruning Dangling Images — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would prune dangling images"
    PRUNED_SUMMARY="(dry run)"
else
    PRUNED_OUTPUT=$(docker image prune -f 2>&1)
    [[ "$ENABLE_LOGGING" == "true" ]] && echo "$PRUNED_OUTPUT" | sed 's/^/  /'
    PRUNED_SUMMARY=$(echo "$PRUNED_OUTPUT" | grep -E "^Total reclaimed" || echo "nothing reclaimed")
fi

# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo "━━━━━ $ICON_SUMMARY DAILY RESTART SUMMARY ━━━━━"
echo "$ICON_HOST     Identity:  $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME     Duration:  $(format_duration $((END - START)))"
echo "$ICON_CONTAINERS Scope:   ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped, ${#FAILED[@]} failed"
[[ ${#RESTARTED[@]} -gt 0 ]] && log "$ICON_STARTED  Restarted: ${RESTARTED[*]}"
[[ ${#SKIPPED[@]} -gt 0 ]]   && log "$ICON_NOT_RUNNING Skipped: ${SKIPPED[*]}"
[[ ${#FAILED[@]} -gt 0 ]]    && echo "$ICON_ERROR  Failed:    ${FAILED[*]}"
echo "$ICON_SYNC     Pruned:    ${PRUNED_SUMMARY:-none}"

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — no changes made"
elif [[ ${#FAILED[@]} -eq 0 ]]; then
    echo "$ICON_DONE     Status:    ALL DONE ✅"
    notify "Daily restart complete — ${#RESTARTED[@]} restarted, ${#SKIPPED[@]} skipped on $(hostname)" "Docker Daily Restart" "normal"
else
    echo "$ICON_ERROR    Status:    ${#FAILED[@]} container(s) failed"
    notify "Daily restart completed with errors on $(hostname) — failed: ${FAILED[*]}" "Docker Daily Restart" "warning"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

[[ ${#FAILED[@]} -gt 0 ]] && exit 1
exit 0