#!/bin/bash
# ==============================================================================================
# ============================ Watchdog Orchestrator ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Runs WATCHDOG_ORCHESTRATOR_SCRIPTS in order each cron cycle. Replaces the
# continuous loops previously embedded in individual watchdog scripts — those
# are now single-pass; this orchestrator provides the cadence.
# Schedule: */15 * * * * (every 15 minutes)
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Order driven by WATCHDOG_ORCHESTRATOR_SCRIPTS in master.conf.
# Default: resource_watchdog → docker_watchdog → system_watchdog →
#          unraid_api_key_renew → stability_watchdog
#
# ARRAY CHECK
#   Exits immediately if /mnt/user is not mounted as shfs. Watchdogs check
#   Docker containers and storage — meaningless without the array. Prevents
#   false positives and unnecessary reboots when array is stopped or stopping.
#
# STARTUP GRACE
#   No action until system uptime >= WATCHDOG_STARTUP_GRACE seconds. Prevents
#   false positives from containers still starting at array launch. Each
#   sub-script enforces this independently — orchestrator exits early to avoid
#   log noise.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Pressure Before Healing
#   Resource Watchdog runs first — it frees RAM and CPU before any container
#   restart is attempted. Containers restarted into a resource-pressured system
#   just fail again. Docker Watchdog restarts with pressure already reduced.
#   System Watchdog checks component health after containers are healed.
#   Stability Watchdog reboots only when all prior layers could not resolve the
#   issue. API key renew is check-first and silent when valid.
#
# Never Queue
#   acquire_lock exits immediately if a prior cycle is still running. Prevents
#   pile-up when a cycle runs long (daemon restart attempt = 30s, etc.).
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   Every watchdog launched from here requires root. Failing once at the top gives one
#   clear error instead of the same permission failure repeated per child.
#
# Lock Acquisition
#   acquire_lock in strict mode — if the previous cycle is still running, this one exits
#   rather than queuing. At a 15-minute cadence a waiting lock would pile up cycles
#   behind a slow watchdog and eventually run them all at once.
#
# Host Detection
#   detect_hosts() sets MY_ID for logs and notifications.
#
# Empty Job List Guard
#   Exits with an error and a notification if WATCHDOG_ORCHESTRATOR_SCRIPTS is empty.
#   Without it the cycle reports "0/0 passed" and exits 0 every cycle — indistinguishable
#   from a healthy run, while nothing at all is being monitored.
#
# Array Check
#   Exits early if /mnt/user is not shfs-mounted. Watchdogs that inspect shares would
#   otherwise read an unmounted array as missing data and act on it.
#
# Startup Grace
#   WATCHDOG_STARTUP_GRACE is respected before any checks run, so containers still
#   initialising after boot are not judged as unhealthy.
#
# Non-Fatal Steps
#   run_orch_child() records a failing or missing watchdog and continues. One broken
#   watchdog never suppresses the rest of the chain.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   WATCHDOG_ORCHESTRATOR_SCRIPTS         — watchdogs to run, in order
#   WATCHDOG_STARTUP_GRACE                — seconds after boot before checks activate
#   WATCHDOG_ORCHESTRATOR_HEARTBEAT       — periodic heartbeat log toggle
#   WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS — heartbeat interval in hours
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# watchdog_orchestrator.sh
#     Normal run (called by cron every 15 minutes).
#
# watchdog_orchestrator.sh --dry-run
#     Pass --dry-run to all sub-scripts.
#
# watchdog_orchestrator.sh --status
#     Show script paths and current grace state.
#
# watchdog_orchestrator.sh --log
#     Verbose output from all sub-scripts.
#
# ==============================================================================================

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

source "$ECOSYSTEM_ROOT/load_config.sh"

parse_args "$@"

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

# Skip immediately if another cycle is still running — no pile-up
acquire_lock

detect_hosts

# An unconfigured job list would run nothing and still report "0/0 passed" — indistinguishable
# from a healthy run. Fail loudly instead of silently doing no work.
if [[ ${#WATCHDOG_ORCHESTRATOR_SCRIPTS[@]} -eq 0 ]]; then
    error "WATCHDOG_ORCHESTRATOR_SCRIPTS is empty — no watchdogs will run"
    error "Check WATCHDOG_ORCHESTRATOR_SCRIPTS in master.conf"
    notify "watchdogs skipped on $(hostname) ($MY_ID) — WATCHDOG_ORCHESTRATOR_SCRIPTS is empty" \
        "$(basename "$0" .sh)" "warning"
    exit 1
fi

log "$ICON_GEAR Config: grace=${WATCHDOG_STARTUP_GRACE}s heartbeat=${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true}/${WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS:-1}hr scripts=${#WATCHDOG_ORCHESTRATOR_SCRIPTS[@]}"
log "$ICON_WATCHDOG Order: $(for s in "${WATCHDOG_ORCHESTRATOR_SCRIPTS[@]}"; do printf '%s ' "${s##*/}"; done)"

[[ "$DRY_RUN" == true ]] && warn "DRY RUN — passing --dry-run to all sub-scripts"

# Derive a display name from a script path: "resource_watchdog.sh" → "Resource Watchdog"
_watchdog_display_name() {
    local path="$1"
    local base="${path##*/}"
    base="${base%.sh}"
    base="${base//_/ }"
    echo "$base" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2); print}'
}

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY WATCHDOG ORCHESTRATOR STATUS ━━━━━"
    echo "$ICON_HOST  Identity: $MY_ID ($LOCAL_SERVER_NAME)"
    echo ""

    UPTIME_S=$(awk '{print int($1)}' /proc/uptime)
    if [[ "$UPTIME_S" -lt "$WATCHDOG_STARTUP_GRACE" ]]; then
        warn "Within startup grace — $(format_duration $UPTIME_S) / $(format_duration $WATCHDOG_STARTUP_GRACE)"
    else
        echo "Past startup grace — $(format_duration $UPTIME_S) uptime"
    fi

    echo ""
    echo "── Sub-scripts ──"
    for entry in "${WATCHDOG_ORCHESTRATOR_SCRIPTS[@]}"; do
        local_path="$ECOSYSTEM_ROOT/$entry"
        label="$(_watchdog_display_name "$entry")"
        if [[ -f "$local_path" ]]; then
            [[ -x "$local_path" ]] && icon="$ICON_DONE" || icon="$ICON_WARN"
            echo "  $icon $label — ${local_path##*/}"
        else
            echo "  $ICON_ERROR $label — NOT FOUND: $local_path"
        fi
    done

    echo ""
    echo "  Schedule:   */15 * * * * (every 15 minutes)"
    echo "  Heartbeat:  ${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true} / every ${WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS:-1}hr"
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Array Check ━━━
# ==============================================================================================
if ! platform_storage_healthy; then
    echo "Array not started — skipping watchdog cycle"
    exit 0
fi
log "$ICON_DISK Array: $(platform_storage_path) mounted ✅"

# ==============================================================================================
# ━━━ Startup Grace ━━━
# ==============================================================================================
UPTIME_SECONDS=$(awk '{print int($1)}' /proc/uptime)
if [[ "$UPTIME_SECONDS" -lt "$WATCHDOG_STARTUP_GRACE" ]]; then
    echo "Startup grace — $(format_duration $UPTIME_SECONDS) / $(format_duration $WATCHDOG_STARTUP_GRACE) — skipping cycle"
    exit 0
fi
log "Startup grace: past — uptime $(format_duration $UPTIME_SECONDS)"

# ==============================================================================================
# ━━━ Run Watchdog Cycle ━━━
# ==============================================================================================
CYCLE_START=$(date +%s)
JOB_PASS=()
JOB_FAIL=()

for _entry in "${WATCHDOG_ORCHESTRATOR_SCRIPTS[@]}"; do
    [[ -z "$_entry" ]] && continue
    run_orch_child "$_entry"
done

CYCLE_END=$(date +%s)
DURATION=$(( CYCLE_END - CYCLE_START ))

# ==============================================================================================
# ━━━ Heartbeat ━━━
# ==============================================================================================
if [[ "${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true}" == true ]]; then
    HB_SECONDS=$(( ${WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS:-1} * 3600 ))
    HB_COUNT_FILE="${STATE_DIR:-/tmp}/watchdog_orch_hb.count"
    HB_COUNT=$(cat "$HB_COUNT_FILE" 2>/dev/null || echo 0)
    HB_COUNT=$(( HB_COUNT + 1 ))
    echo "$HB_COUNT" > "$HB_COUNT_FILE"
    # Each cron run = ~60s — use count × 60 as uptime approximation
    HB_ELAPSED=$(( HB_COUNT * 60 ))
    if [[ "$HB_SECONDS" -gt 0 ]] && (( HB_ELAPSED % HB_SECONDS < 60 )) && [[ "$HB_COUNT" -gt 1 ]]; then
        HB_HR=$(( HB_ELAPSED / 3600 ))
        log "♥ watchdog_orchestrator alive — $MY_ID — ~${HB_HR}hr ($(date '+%H:%M:%S'))"
    fi
fi

# ==============================================================================================
# ━━━ Summary — minimal one-liner by default, full breakdown on failure or --log ━━━
# ==============================================================================================
if [[ "${#JOB_FAIL[@]}" -gt 0 || "$ENABLE_LOGGING" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY WATCHDOG CYCLE — $MY_ID — $(date '+%H:%M:%S') ━━━━━"
    for p in "${JOB_PASS[@]}"; do log "  $ICON_DONE  $p"; done
    for f in "${JOB_FAIL[@]}"; do error "  $ICON_ERROR $f"; done
    echo "$ICON_TIME  Duration: $(format_duration $DURATION)"
    echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
    echo "$ICON_DONE Watchdog cycle — ${#JOB_PASS[@]}/${#WATCHDOG_ORCHESTRATOR_SCRIPTS[@]} passed ($(format_duration $DURATION))"
fi

if [[ "${#JOB_FAIL[@]}" -gt 0 ]]; then
    notify "Watchdog cycle failure on $(hostname) ($MY_ID) — ${JOB_FAIL[*]}" \
        "Watchdog Orchestrator" "warning"
    exit 1
fi

exit 0
