#!/usr/bin/env bash
# lib-heartbeat.sh — shared library for factory liveness signals.
#
# Sourced by bend-emit-pool.sh, bend-dispatcher.sh, bend-supervisor.sh,
# bend-autoscaler.sh, bend-supervisor-dlq-runner.sh. Pure POSIX-ish bash;
# no external daemons; portable to busybox-bash + flock + setsid + nohup.
#
# Env vars consumed: none. See $LUMBDA_FACTORY_DIR/CONTRACT.md for full
# factory-wide knob list.
#
# Contract:
#   - heartbeat_touch <state_dir> <service_name>
#       Writes "<pid> <unix_ts>" to <state_dir>/<service_name>.heartbeat.
#       Called at top of every loop iteration; mtime + content authoritative.
#       Returns 0 on success, 1 on write failure.
#
#   - heartbeat_is_alive <state_dir> <service_name> <stale_threshold_sec>
#       Returns 0 if BOTH:
#         (a) heartbeat file mtime within <stale_threshold_sec> of now
#         (b) pid recorded in heartbeat file responds to kill -0
#       Returns 1 otherwise (missing file, stale mtime, dead pid).
#       (a) gates daemon liveness; (b) gates against post-mortem stale files.
#
#   - heartbeat_exit_cause <state_dir> <service_name> <reason>
#       Appends "<utc_iso8601> <pid> <reason>" to
#       <state_dir>/<service_name>.exit-cause, removes <service_name>.heartbeat.
#       Caller invokes from every exit path (STOP marker, signal trap, drain
#       timeout, error abort). Operators read .exit-cause to triage post-mortem.
#
# File formats:
#   <service>.heartbeat   — single line: "<pid> <unix_ts>" (whitespace-sep)
#   <service>.exit-cause  — append-only log, one line per exit:
#                           "<utc_iso8601> <pid> <reason>"
#
# Backwards compatibility: legacy per-worker dispatcher.heartbeat-<wid>
# files used freeform text content + only mtime for liveness. heartbeat_is_alive
# falls back to mtime-only when a heartbeat file does not parse as
# "<pid> <unix_ts>".
#
# Caller responsibilities:
#   - mkdir -p "$state_dir" BEFORE calling any function (library does NOT
#     auto-create; refusing to silently swallow path typos).
#   - Wrap heartbeat_touch in an EXIT trap that calls heartbeat_exit_cause
#     so an unexpected crash still leaves a forensic trail.

# heartbeat_touch <state_dir> <service_name>
heartbeat_touch() {
    local state_dir="$1"
    local service_name="$2"
    local hb_file="${state_dir}/${service_name}.heartbeat"
    printf '%s %s\n' "$$" "$(date +%s)" > "$hb_file" 2>/dev/null
}

# heartbeat_is_alive <state_dir> <service_name> <stale_threshold_sec>
# Returns 0 alive, 1 dead/stale.
heartbeat_is_alive() {
    local state_dir="$1"
    local service_name="$2"
    local threshold="$3"
    local hb_file="${state_dir}/${service_name}.heartbeat"

    [ -f "$hb_file" ] || return 1

    # mtime gate
    local now mtime age
    now=$(date +%s)
    mtime=$(stat -c %Y "$hb_file" 2>/dev/null) || return 1
    age=$(( now - mtime ))
    [ "$age" -le "$threshold" ] || return 1

    # pid gate: parse first token. Tolerate freeform legacy content by
    # treating non-numeric first token as "mtime-only mode" — return 0 if
    # mtime gate already passed.
    local first_token
    first_token=$(awk 'NR==1 {print $1}' "$hb_file" 2>/dev/null)
    if [ -z "$first_token" ]; then
        return 0
    fi
    case "$first_token" in
        ''|*[!0-9]*)
            # non-numeric (e.g., ISO timestamp from legacy writer) → mtime-only
            return 0
            ;;
    esac

    # Numeric pid: must respond to kill -0. Cross-host caveat: this library
    # runs local to whichever process reads our heartbeat. heartbeat_is_alive
    # called against a remote host's state dir over SSH must run on that
    # host (kill -0 lives in local pid space only). Supervisor + status
    # callers SSH per-host before calling, so this holds.
    if kill -0 "$first_token" 2>/dev/null; then
        return 0
    fi
    return 1
}

# heartbeat_exit_cause <state_dir> <service_name> <reason>
heartbeat_exit_cause() {
    local state_dir="$1"
    local service_name="$2"
    local reason="$3"
    local ec_file="${state_dir}/${service_name}.exit-cause"
    local hb_file="${state_dir}/${service_name}.heartbeat"
    local ts
    ts=$(date -u +%FT%TZ)
    printf '%s %s %s\n' "$ts" "$$" "$reason" >> "$ec_file" 2>/dev/null
    rm -f "$hb_file" 2>/dev/null
}
