#!/usr/bin/env bash
# app-health — measure local health and send a heartbeat event to CSC.
#
# Usage: app-health [options]
#   (default)        send a heartbeat if HEARTBEAT_INTERVAL elapsed since the last one
#   --send           send now (ignore the interval throttle)
#   --check          print local status only, no send; exit 0=up 1=degraded 2=down
#   --config <path>  use a specific config file
#   --show-config    print resolved configuration and exit
#   -h, --help       this help
#
# Status: `systemctl is-active SERVICE_NAME` always; plus an HTTP probe when
# HEALTH_URL is set (any 2xx/3xx = serving). unit down -> down; unit up but probe
# failing -> degraded; otherwise -> up.
#
# Designed to run from app-health.timer as the app user (no root needed). The
# timer ticks every 10 minutes; HEARTBEAT_INTERVAL throttles actual sends, so the
# interval is configured in one place. Without a CSC account the script logs and
# exits 0, so the timer can be installed before the account is.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# help = the header comment block above, printed until the first non-# line. The
# generic command names (app-health, …) are rewritten to THIS script's own name,
# so a renamed <product>-health — or a custom CLI sourcing the libs — prints its
# own name with no install-time edit. pfx='app' (a no-op) when not renamed.
usage() {
    local cmd pfx; cmd="$(basename -- "$0")"; pfx="${cmd%-health}"
    sed -n '2,${/^#/!q;s/^# \{0,1\}//;s/^#$//;p}' "$0" \
        | sed -E "s/app-(deploy|backup|health|update)/$pfx-\1/g"
}

MODE=tick   # tick (throttled send) | send | check | showconfig
while [ $# -gt 0 ]; do
    case "$1" in
        --send)         MODE=send ;;
        --check)        MODE=check ;;
        --show-config)  MODE=showconfig ;;
        --config)       CONFIG_FILE="$2"; shift ;;
        --config=*)     CONFIG_FILE="${1#*=}" ;;
        -h|--help)      usage; exit 0 ;;
        *)              echo "Unknown option: $1" >&2; exit 2 ;;
    esac
    shift
done

. "$SCRIPT_DIR/common.sh"

[ "$MODE" = showconfig ] && { de_show_config; exit 0; }

HEALTH_STATE_FILE="$APP_HOME/.$PRODUCT-health.last"

# HEARTBEAT_INTERVAL ("90s" | "10m" | "1h" | plain seconds) -> seconds (stdout)
interval_secs() {
    local v="$1" n unit=1
    case "$v" in
        *s) n="${v%s}" ;;
        *m) n="${v%m}"; unit=60 ;;
        *h) n="${v%h}"; unit=3600 ;;
        *)  n="$v" ;;
    esac
    case "$n" in ''|*[!0-9]*) de_die "invalid HEARTBEAT_INTERVAL: '$v' (use e.g. 10m, 1h)" ;; esac
    printf '%s' "$((n * unit))"
}

# Measure local health. Sets globals: SVC (up|down), HTTP (up|down|''),
# STATUS (up|degraded|down), VERSION (may be ''), STARTED_AT (ISO 8601 or '').
# $1 = service unit to probe (default SERVICE_NAME).
measure() {
    local unit="${1:-$SERVICE_NAME}"
    if systemctl is-active --quiet "$unit" 2>/dev/null; then SVC=up; else SVC=down; fi

    HTTP=""
    if [ -n "$HEALTH_URL" ] && [ "$SVC" = up ]; then
        if de_health_probe "$HEALTH_URL"; then HTTP=up; else HTTP=down; fi
    fi

    if [ "$SVC" = down ]; then STATUS=down
    elif [ "$HTTP" = down ]; then STATUS=degraded
    else STATUS=up; fi

    VERSION=""
    if [ -L "$APP_HOME/current" ]; then
        VERSION="$(basename "$(readlink -f "$APP_HOME/current" 2>/dev/null || true)")"
        [ "$VERSION" = current ] && VERSION=""   # dangling link resolves to itself
    fi

    STARTED_AT=""
    if [ "$SVC" = up ]; then
        local raw
        raw="$(systemctl show -p ActiveEnterTimestamp --value "$unit" 2>/dev/null || true)"
        if [ -n "$raw" ] && [ "$raw" != "n/a" ]; then
            STARTED_AT="$(date -u -d "$raw" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
        fi
    fi
}

# Throttle (tick mode): send when no state, or when the interval (minus a 90s
# fudge for timer drift) elapsed. Reads INTERVAL_SECS (validated by the caller).
should_send() {
    local last now min
    [ -r "$HEALTH_STATE_FILE" ] || return 0
    last="$(tr -cd '0-9' < "$HEALTH_STATE_FILE")"
    [ -n "$last" ] || return 0
    now="$(date +%s)"
    min="$((INTERVAL_SECS - 90))"
    [ "$min" -lt 0 ] && min=0
    if [ "$((now - last))" -ge "$min" ]; then
        return 0
    fi
    de_log "Heartbeat throttled — last sent $(( (now - last) / 60 ))m ago, interval $HEARTBEAT_INTERVAL; nothing sent"
    return 1
}

record_sent() {
    if printf '%s\n' "$(date +%s)" > "$HEALTH_STATE_FILE" 2>/dev/null; then
        # invoked via the deploy CLI as root: keep the file writable for the app user
        [ "$(id -u)" -eq 0 ] && chown "$APP_USER" "$HEALTH_STATE_FILE" 2>/dev/null || true
    else
        de_warn "cannot write $HEALTH_STATE_FILE; interval throttling disabled (every tick sends)"
    fi
}

measure

if [ "$MODE" = check ]; then
    # stdout = data (status report); exit code mirrors the status
    printf 'status=%s\n' "$STATUS"
    printf 'service=%s\n' "$SVC"
    [ -n "$HTTP" ]       && printf 'http=%s\n' "$HTTP"
    [ -n "$VERSION" ]    && printf 'version=%s\n' "$VERSION"
    [ -n "$STARTED_AT" ] && printf 'startedAt=%s\n' "$STARTED_AT"
    case "$STATUS" in up) exit 0 ;; degraded) exit 1 ;; *) exit 2 ;; esac
fi

if ! csc_enabled; then
    de_log "CSC account not configured; heartbeat skipped (status: $STATUS)"
    exit 0
fi
# Validate the interval here at top level — set -e then makes a bad value fatal.
INTERVAL_SECS="$(interval_secs "$HEARTBEAT_INTERVAL")"
if [ "$MODE" = tick ] && ! should_send; then
    exit 0   # within the interval; should_send already logged the outcome
fi

checks="{\"service\":\"$SVC\""
[ -n "$HTTP" ] && checks="$checks,\"http\":\"$HTTP\""
checks="$checks}"
# Optionally scrape selected app metrics from a local Prometheus/actuator endpoint and
# ride them in the heartbeat (CSC stores + alerts on them). Best-effort: empty on any
# problem, so metrics never block a heartbeat. The selection map is CSC_METRICS_MAP, or — when
# unset — derived from the app's shipped profile at CSC_METRICS_PROFILE (needs jq).
METRICS=""
METRICS_MAP="${CSC_METRICS_MAP:-}"
if [ -z "$METRICS_MAP" ] && [ -n "${CSC_METRICS_PROFILE:-}" ]; then
    METRICS_MAP="$(csc_metrics_map_from_profile "$CSC_METRICS_PROFILE" || true)"
fi
if [ -n "${CSC_METRICS_URL:-}" ] && [ -n "$METRICS_MAP" ]; then
    METRICS="$(csc_scrape_metrics "$CSC_METRICS_URL" "$METRICS_MAP" || true)"
fi
# On a failed send the state is NOT updated, so the next 10-min tick retries
# naturally. Exit 0 either way — heartbeats are best-effort and the timer unit
# must not go red over a transient CSC outage.
if csc_report_heartbeat "$STATUS" "$VERSION" "$STARTED_AT" "$checks" "$METRICS"; then
    record_sent
fi
exit 0
