#!/bin/bash
# ==============================================================================================
# ================================= Network Watchdog ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Services-layer connectivity — checks that the outside world can actually reach
# what it needs to reach. Silent when everything is reachable. Only fires when
# something in the connectivity chain has broken.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Check 1 — Internet Connectivity
#   curl external endpoint → fail = alert + skip all remaining checks.
#   Internet down means DDNS and NPM checks would false-positive — gating prevents noise.
#
# Check 2 — DDNS (Cloudflare)
#   Public IP via ifconfig.me vs DNS record via dig @1.1.1.1.
#   Match → pass (silent). Mismatch → restart DDNS container + notify.
#   Container restart triggers an immediate Cloudflare record update.
#
# Check 3 — Tailscale
#   tailscale status → Running → pass (silent). Not running → notify.
#   Notify only — no restart attempt. Tailscale state issues warrant human review.
#
# Check 4 — NPM Proxy (external check)
#   curl external URL → 2-strike system before restarting NginxProxyManager.
#   Strike 1: warn + notify. Strike 2: restart NPM + notify + clear strikes.
#   Strikes auto-clear when the external URL becomes reachable again.
#   External check — verifies the full stack (DNS → NPM → backend), not just NPM running.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Gate on Root Cause
#   The internet check runs first and short-circuits everything after it. DDNS
#   and NPM checks both depend on outbound connectivity — running them during an
#   outage produces three alarms for one fault and can trigger container restarts
#   that fix nothing.
#
# Verify the Path, Not the Process
#   NPM is checked by fetching an external URL rather than asking whether the
#   container is running. A running container behind broken DNS or a broken
#   upstream still serves nothing. Checking the whole path is the only result
#   that means anything to a user.
#
# Restart Only What Restarting Fixes
#   DDNS and NPM are restarted because a restart forces a record update or
#   reloads proxy config — the restart is the remedy. Tailscale is notify-only:
#   its failures are auth, key expiry or ACL problems that a restart cannot
#   resolve and may obscure.
#
# Strike Before Restarting NPM
#   NPM sits in front of every externally reachable service, so restarting it is
#   itself disruptive. A single failed fetch can be a transient upstream blip;
#   two consecutive failures justify the interruption.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Required
#   docker restart requires root.
#
# Single Instance Lock
#   acquire_lock prevents concurrent runs.
#
# Host Detection Before Config Resolution
#   detect_hosts() runs before the HOST*_NETWORK_WATCHDOG_* names are built. MY_ID
#   is not exported, so an orchestrated run starts with it empty — resolving these
#   any earlier silently produces empty DDNS and NPM config and skips both checks.
#
# Unresolved Config Warning
#   Warns when neither DDNS nor NPM config resolves for this host. "Not configured"
#   is a legitimate state, but it looks identical to a broken lookup, so it is
#   stated out loud rather than passed over in silence.
#
# NETWORK_WATCHDOG_ENABLED Toggle
#   Exits cleanly when disabled, without removing it from the orchestrator list.
#
# Internet Gates All Checks
#   If internet is down, DDNS and NPM checks are skipped — no cascade of false positives.
#
# Strike Before Acting on NPM
#   Single curl failure could be transient DNS hiccup or CDN blip.
#   Two consecutive failures confirms NPM is the problem.
#
# Notify-Only for Tailscale
#   Tailscale is never restarted. Its failures are auth, key expiry or ACL issues
#   that a restart cannot fix and would only obscure.
#
# Timeout Protection
#   Every connectivity probe carries an explicit timeout, so a black-holed route
#   cannot stall the every-minute watchdog chain.
#
# Dry Run Support
#   --dry-run performs all checks and reports restarts without issuing them.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
#   NETWORK_WATCHDOG_ENABLED          toggle entire watchdog (default: true)
#   NETWORK_WATCHDOG_INTERNET_URL     endpoint for internet reachability check
#   NETWORK_WATCHDOG_INTERNET_TIMEOUT curl timeout in seconds for internet check
#   NETWORK_WATCHDOG_CHECK_TAILSCALE  toggle tailscale check (default: true)
#   NETWORK_WATCHDOG_NPM_TIMEOUT      curl timeout for NPM external check
#   NETWORK_WATCHDOG_NPM_STRIKE_LIMIT consecutive failures before NPM restart
#   NETWORK_WATCHDOG_NPM_STATE_FILE   strike count persistence (/tmp — resets on reboot)
#
# host*.conf (host-specific)
#
#   HOST*_NETWORK_WATCHDOG_DDNS_DOMAIN     domain to resolve and compare to public IP
#   HOST*_NETWORK_WATCHDOG_DDNS_CONTAINER  container to restart on DDNS mismatch
#   HOST*_NETWORK_WATCHDOG_NPM_URL         external URL to test full proxy stack
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# network_watchdog.sh
#     Run all connectivity checks. Silent when healthy.
#
# network_watchdog.sh --dry-run
#     Run all checks without restarting any containers.
#
# network_watchdog.sh --status
#     Show configuration, current public IP, DNS record, NPM strike state.
#
# network_watchdog.sh --log
#     Verbose output — show each check result even when passing.
#
# ==============================================================================================

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

detect_hosts

# ==============================================================================================
# ━━━ Resolve host-specific config ━━━
# ==============================================================================================
# MUST run after detect_hosts() — MY_ID is set there and is not exported, so orchestrated
# runs start with it empty. Building these names any earlier yields "_NETWORK_WATCHDOG_*",
# which is always unset, and the DDNS and NPM checks silently skip as "not configured".
_ddns_domain_var="${MY_ID}_NETWORK_WATCHDOG_DDNS_DOMAIN"
_ddns_container_var="${MY_ID}_NETWORK_WATCHDOG_DDNS_CONTAINER"
_npm_url_var="${MY_ID}_NETWORK_WATCHDOG_NPM_URL"

DDNS_DOMAIN="${!_ddns_domain_var:-}"
DDNS_CONTAINER="${!_ddns_container_var:-}"
NPM_URL="${!_npm_url_var:-}"

# A check that is configured but resolves empty means the lookup broke, not that the
# operator opted out. Say so — silent skipping is what hid this for so long.
if [[ -z "$DDNS_DOMAIN" && -z "$NPM_URL" ]]; then
    warn "No DDNS or NPM config resolved for ${MY_ID:-unknown host}"
    warn "Expected ${MY_ID}_NETWORK_WATCHDOG_DDNS_DOMAIN / ${MY_ID}_NETWORK_WATCHDOG_NPM_URL in host*.conf"
fi

[[ "${NETWORK_WATCHDOG_ENABLED:-true}" != "true" ]] && echo "Network watchdog disabled" && exit 0
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no containers will be restarted"

log "$ICON_GEAR Config: internet=${NETWORK_WATCHDOG_INTERNET_URL:-https://1.1.1.1} timeout=${NETWORK_WATCHDOG_INTERNET_TIMEOUT:-5}s tailscale=${NETWORK_WATCHDOG_CHECK_TAILSCALE:-true} npm-strikes=${NETWORK_WATCHDOG_NPM_STRIKE_LIMIT:-2}"
log "$ICON_NET   DDNS: ${DDNS_DOMAIN:-not configured} → ${DDNS_CONTAINER:-no container}  NPM: ${NPM_URL:-not configured}"

touch "${NETWORK_WATCHDOG_NPM_STATE_FILE}" 2>/dev/null

# ━━━ Strike helpers — wrap common.sh's wd_state_get()/wd_state_set() ━━━
get_strikes() { wd_state_get "$1" "$2"; }
set_strikes() { wd_state_set "$1" "$2" "$3"; }

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY NETWORK WATCHDOG STATUS ━━━━━"
    echo "$ICON_HOST  Identity:       $MY_ID ($LOCAL_SERVER_NAME)"
    echo "$ICON_GEAR  Internet URL:   ${NETWORK_WATCHDOG_INTERNET_URL:-https://1.1.1.1}"
    echo "$ICON_GEAR  DDNS domain:    ${DDNS_DOMAIN:-not configured}"
    echo "$ICON_GEAR  DDNS container: ${DDNS_CONTAINER:-not configured}"
    echo "$ICON_GEAR  Tailscale:      ${NETWORK_WATCHDOG_CHECK_TAILSCALE:-true}"
    echo "$ICON_GEAR  NPM URL:        ${NPM_URL:-not configured}"
    echo "$ICON_GEAR  NPM strikes:    $(get_strikes "npm" "${NETWORK_WATCHDOG_NPM_STATE_FILE}") / ${NETWORK_WATCHDOG_NPM_STRIKE_LIMIT:-2}"
    echo ""
    echo "── Current State ──"

    if curl -sf --max-time "${NETWORK_WATCHDOG_INTERNET_TIMEOUT:-5}" \
        "${NETWORK_WATCHDOG_INTERNET_URL:-https://1.1.1.1}" >/dev/null 2>&1; then
        echo "  $ICON_SUCCESS Internet:   reachable"
    else
        echo "  $ICON_ERROR  Internet:   NOT reachable"
    fi

    if [[ -n "$DDNS_DOMAIN" ]]; then
        _pub=$(curl -sf --max-time 5 https://ifconfig.me 2>/dev/null | tr -d '[:space:]')
        _dns=$(dig +short "$DDNS_DOMAIN" @1.1.1.1 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)
        echo "  $ICON_GEAR   Public IP:  ${_pub:-unknown}"
        echo "  $ICON_GEAR   DNS record: ${_dns:-unknown}"
        [[ "$_pub" == "$_dns" ]] && \
            echo "  $ICON_SUCCESS DDNS:       in sync" || \
            echo "  $ICON_ERROR  DDNS:       MISMATCH — public=$_pub dns=$_dns"
    else
        echo "  $ICON_GEAR   DDNS:       not configured for $MY_ID"
    fi

    if [[ "${NETWORK_WATCHDOG_CHECK_TAILSCALE:-true}" == "true" ]] && command -v tailscale >/dev/null 2>&1; then
        if tailscale status --json 2>/dev/null | grep -qE '"BackendState":\s*"Running"'; then
            echo "  $ICON_SUCCESS Tailscale:  running"
        else
            echo "  $ICON_ERROR  Tailscale:  NOT running"
        fi
    else
        echo "  $ICON_GEAR   Tailscale:  check disabled or not installed"
    fi

    if [[ -n "$NPM_URL" ]]; then
        if curl -sf --max-time "${NETWORK_WATCHDOG_NPM_TIMEOUT:-10}" "$NPM_URL" >/dev/null 2>&1; then
            echo "  $ICON_SUCCESS NPM proxy:  reachable ($NPM_URL)"
        else
            echo "  $ICON_ERROR  NPM proxy:  NOT reachable ($NPM_URL)"
        fi
    else
        echo "  $ICON_GEAR   NPM proxy:  not configured for $MY_ID"
    fi

    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Check 1 — Internet Connectivity ━━━
# ==============================================================================================
echo "━━━ $ICON_NET Network Watchdog — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

ISSUES=0

if ! curl -sf --max-time "${NETWORK_WATCHDOG_INTERNET_TIMEOUT:-5}" \
    "${NETWORK_WATCHDOG_INTERNET_URL:-https://1.1.1.1}" >/dev/null 2>&1; then
    warn "$ICON_ERROR Internet not reachable — skipping DDNS, Tailscale, and NPM checks"
    notify "Network watchdog: internet not reachable on $(hostname) ($MY_ID)" \
        "Network Watchdog" "warning"
    exit 1
fi

log "$ICON_SUCCESS Internet reachable"

# ==============================================================================================
# ━━━ Check 2 — DDNS ━━━
# ==============================================================================================
if [[ -n "$DDNS_DOMAIN" ]] && [[ -n "$DDNS_CONTAINER" ]]; then
    PUBLIC_IP=$(curl -sf --max-time 5 https://ifconfig.me 2>/dev/null | tr -d '[:space:]')
    DNS_IP=$(dig +short "$DDNS_DOMAIN" @1.1.1.1 2>/dev/null | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)

    if [[ -z "$PUBLIC_IP" ]]; then
        warn "Could not determine public IP — skipping DDNS check"
    elif [[ -z "$DNS_IP" ]]; then
        warn "Could not resolve $DDNS_DOMAIN — skipping DDNS check"
    elif [[ "$PUBLIC_IP" == "$DNS_IP" ]]; then
        log "$ICON_SUCCESS DDNS in sync — $DDNS_DOMAIN → $DNS_IP"
    else
        warn "$ICON_ERROR DDNS mismatch — public=$PUBLIC_IP dns=$DNS_IP"
        (( ISSUES++ ))
        if [[ "$DRY_RUN" == true ]]; then
            warn "DRY RUN — would restart $DDNS_CONTAINER"
        else
            warn "Restarting $DDNS_CONTAINER to trigger Cloudflare update..."
            if docker restart "$DDNS_CONTAINER" >/dev/null 2>&1; then
                warn "$DDNS_CONTAINER restarted ✅"
                notify "DDNS mismatch on $(hostname) ($MY_ID) — $DDNS_DOMAIN was $DNS_IP, public is $PUBLIC_IP — $DDNS_CONTAINER restarted" \
                    "Network Watchdog" "warning"
            else
                warn "$DDNS_CONTAINER restart failed"
                notify "DDNS mismatch on $(hostname) ($MY_ID) — $DDNS_CONTAINER restart failed — manual intervention needed" \
                    "Network Watchdog" "warning"
            fi
        fi
    fi
else
    log "DDNS check not configured for $MY_ID — skipping"
fi

# ==============================================================================================
# ━━━ Check 3 — Tailscale ━━━
# ==============================================================================================
if [[ "${NETWORK_WATCHDOG_CHECK_TAILSCALE:-true}" == "true" ]]; then
    if ! command -v tailscale >/dev/null 2>&1; then
        log "Tailscale not installed — skipping"
    elif tailscale status --json 2>/dev/null | grep -qE '"BackendState":\s*"Running"'; then
        log "$ICON_SUCCESS Tailscale running"
    else
        warn "$ICON_ERROR Tailscale not in Running state"
        notify "Tailscale not running on $(hostname) ($MY_ID) — manual check needed" \
            "Network Watchdog" "warning"
    fi
fi

# ==============================================================================================
# ━━━ Check 4 — NPM Proxy (external) ━━━
# ==============================================================================================
if [[ -n "$NPM_URL" ]]; then
    NPM_STRIKES=$(get_strikes "npm" "${NETWORK_WATCHDOG_NPM_STATE_FILE}")
    NPM_STRIKE_LIMIT="${NETWORK_WATCHDOG_NPM_STRIKE_LIMIT:-2}"

    if curl -sf --max-time "${NETWORK_WATCHDOG_NPM_TIMEOUT:-10}" "$NPM_URL" >/dev/null 2>&1; then
        log "$ICON_SUCCESS NPM proxy reachable — $NPM_URL"
        if [[ "$NPM_STRIKES" -gt 0 ]]; then
            echo "NPM strikes cleared (was $NPM_STRIKES)"
            set_strikes "npm" 0 "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
        fi
    else
        NPM_STRIKES=$(( NPM_STRIKES + 1 ))
        set_strikes "npm" "$NPM_STRIKES" "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
        (( ISSUES++ ))

        if [[ "$NPM_STRIKES" -lt "$NPM_STRIKE_LIMIT" ]]; then
            warn "$ICON_ERROR NPM proxy not reachable — $NPM_URL (strike $NPM_STRIKES/$NPM_STRIKE_LIMIT)"
            notify "NPM proxy not reachable on $(hostname) ($MY_ID) — $NPM_URL (strike $NPM_STRIKES/$NPM_STRIKE_LIMIT)" \
                "Network Watchdog" "warning"
        else
            warn "$ICON_ERROR NPM proxy strike limit reached ($NPM_STRIKES) — restarting NginxProxyManager"
            if [[ "$DRY_RUN" == true ]]; then
                warn "DRY RUN — would restart NginxProxyManager"
            else
                if docker restart NginxProxyManager >/dev/null 2>&1; then
                    warn "NginxProxyManager restarted ✅"
                    set_strikes "npm" 0 "${NETWORK_WATCHDOG_NPM_STATE_FILE}"
                    notify "NPM proxy restarted on $(hostname) ($MY_ID) — $NPM_URL was unreachable for $NPM_STRIKES cycles" \
                        "Network Watchdog" "warning"
                else
                    warn "NginxProxyManager restart failed — manual intervention needed"
                    notify "NPM proxy restart FAILED on $(hostname) ($MY_ID) — manual intervention needed" \
                        "Network Watchdog" "warning"
                fi
            fi
        fi
    fi
else
    log "NPM check not configured for $MY_ID — skipping"
fi

# ==============================================================================================
# ━━━ Exit ━━━
# ==============================================================================================
if [[ "$ISSUES" -gt 0 ]]; then
    exit 1
else
    echo "Network healthy ✅ ($(date '+%H:%M:%S'))"
    exit 0
fi
