#!/usr/bin/env bash
#
# node_exporter_push.sh - holt den kompletten /metrics-Body vom lokalen
#   node_exporter und PUSHT ihn an einen Prometheus Pushgateway.
#
#   node_exporter (inkl. textfile collector)  ->  dieses Skript  ->  Pushgateway
#                                                                       ^
#                                                     Prometheus scrapet hier
#
# Es wird NICHTS selbst gesammelt. Custom-Metriken einfach wie gehabt als
# .prom in den textfile_collector legen, sie werden mitgepusht.
#
# Abhaengigkeiten: bash, curl, awk  (kein jq, kein python)
#
set -uo pipefail

VERSION="1.1.0"
SCRIPT_NAME="$(basename "$0")"

ENV_FILE="${ENV_FILE:-/etc/node-exporter-push/push.env}"
DRY_RUN=0
DO_DELETE=0

usage() {
    cat <<EOF
${SCRIPT_NAME} v${VERSION}

  --env FILE     Konfigurationsdatei (default: ${ENV_FILE})
  --dry-run      Payload auf stdout ausgeben, nicht pushen
  --delete       Metrik-Gruppe im Pushgateway loeschen (Host-Decommission)
  --help

Exit-Codes: 0 = ok | 1 = Scrape fehlgeschlagen (kein Push!) | 2 = Push-Fehler
            78 = Config-Fehler
EOF
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --env)     ENV_FILE="$2"; shift 2 ;;
        --dry-run) DRY_RUN=1; shift ;;
        --delete)  DO_DELETE=1; shift ;;
        --help|-h) usage; exit 0 ;;
        *) echo "Unbekannte Option: $1" >&2; usage >&2; exit 78 ;;
    esac
done

# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
if [[ ! -r "$ENV_FILE" ]]; then
    echo "FATAL: Config ${ENV_FILE} nicht lesbar" >&2
    exit 78
fi
# Die Datei wird per 'source' gelesen -> Werte mit Leerzeichen, Klammern oder
# Pipes MUESSEN gequotet sein. Parse-Fehler sind hier fatal, sonst laeuft das
# Skript mit halb geladener Config weiter (z.B. ohne customer-Label).
if ! bash -n "$ENV_FILE" 2>/dev/null; then
    echo "FATAL: ${ENV_FILE} ist syntaktisch ungueltig. Werte quoten! Details:" >&2
    bash -n "$ENV_FILE" >&2
    exit 78
fi
# shellcheck disable=SC1090
set -a
if ! source "$ENV_FILE"; then
    echo "FATAL: ${ENV_FILE} konnte nicht geladen werden" >&2
    exit 78
fi
set +a

: "${PUSHGATEWAY_URL:?PUSHGATEWAY_URL fehlt in ${ENV_FILE}}"

NODE_EXPORTER_URL="${NODE_EXPORTER_URL:-http://127.0.0.1:9100/metrics}"
PUSH_JOB="${PUSH_JOB:-node}"
PUSH_INSTANCE="${PUSH_INSTANCE:-$(hostname -f 2>/dev/null || hostname)}"
PUSH_CUSTOMER="${PUSH_CUSTOMER:-}"
PUSH_ROLE="${PUSH_ROLE:-}"
HTTP_TIMEOUT="${HTTP_TIMEOUT:-20}"
CURL_INSECURE="${CURL_INSECURE:-false}"
MIN_SAMPLES="${MIN_SAMPLES:-50}"
REQUIRE_FAMILY="${REQUIRE_FAMILY:-node_exporter_build_info}"
INCLUDE_REGEX="${INCLUDE_REGEX:-}"
EXCLUDE_REGEX="${EXCLUDE_REGEX:-}"
STRIP_RESERVED_LABELS="${STRIP_RESERVED_LABELS:-true}"
ADD_RELAY_METRICS="${ADD_RELAY_METRICS:-true}"

RAW="$(mktemp -t nepush-raw.XXXXXX)"
BUF="$(mktemp -t nepush-buf.XXXXXX)"
trap 'rm -f "$RAW" "$BUF" 2>/dev/null' EXIT

log() { echo "[$(date -Is)] $*" >&2; }

CURL_BASE=(--silent --show-error --max-time "$HTTP_TIMEOUT")
[[ "$CURL_INSECURE" == "true" ]] && CURL_BASE+=(--insecure)

PG_AUTH=()
[[ -n "${PUSHGATEWAY_USER:-}" ]] && PG_AUTH=(-u "${PUSHGATEWAY_USER}:${PUSHGATEWAY_PASS:-}")

# ---------------------------------------------------------------------------
# Grouping Key. Werte mit "/" oder leere Werte muessen base64url-kodiert
# werden (Pushgateway >= 1.0: /<label>@base64/<wert>).
# ---------------------------------------------------------------------------
urlenc() {
    local s="$1" out="" i c
    for (( i=0; i<${#s}; i++ )); do
        c="${s:i:1}"
        case "$c" in
            [a-zA-Z0-9.~_-]) out+="$c" ;;
            *) out+="$(printf '%%%02X' "'$c")" ;;
        esac
    done
    printf '%s' "$out"
}

group_segment() {
    local label="$1" value="$2" b64
    if [[ -z "$value" || "$value" == *"/"* ]]; then
        b64="$(printf '%s' "$value" | base64 | tr -d '\n' | tr '+/' '-_')"
        [[ -z "$b64" ]] && b64="="
        printf '/%s@base64/%s' "$label" "$b64"
    else
        printf '/%s/%s' "$label" "$(urlenc "$value")"
    fi
}

build_push_url() {
    local url="${PUSHGATEWAY_URL%/}/metrics"
    url+="$(group_segment job "$PUSH_JOB")"
    url+="$(group_segment instance "$PUSH_INSTANCE")"
    [[ -n "$PUSH_CUSTOMER" ]] && url+="$(group_segment customer "$PUSH_CUSTOMER")"
    [[ -n "$PUSH_ROLE" ]]     && url+="$(group_segment role "$PUSH_ROLE")"
    printf '%s' "$url"
}

PUSH_URL="$(build_push_url)"

# ---------------------------------------------------------------------------
# --delete
# ---------------------------------------------------------------------------
if [[ "$DO_DELETE" == "1" ]]; then
    code="$(curl "${CURL_BASE[@]}" "${PG_AUTH[@]}" -X DELETE -o /dev/null -w '%{http_code}' "$PUSH_URL")"
    if [[ "$code" =~ ^2 ]]; then log "Gruppe geloescht: ${PUSH_URL}"; exit 0; fi
    log "FEHLER beim Loeschen (HTTP ${code}): ${PUSH_URL}"; exit 2
fi

# ---------------------------------------------------------------------------
# 1) node_exporter scrapen
# ---------------------------------------------------------------------------
NE_AUTH=()
[[ -n "${NE_USER:-}" ]] && NE_AUTH=(-u "${NE_USER}:${NE_PASS:-}")

T0="$(date +%s.%N)"
SCRAPE_CODE="$(curl "${CURL_BASE[@]}" "${NE_AUTH[@]}" --compressed \
    -H 'Accept: text/plain;version=0.0.4;q=1,*/*;q=0.1' \
    -o "$RAW" -w '%{http_code}' "$NODE_EXPORTER_URL" 2>/dev/null)"
T1="$(date +%s.%N)"
SCRAPE_SECONDS="$(awk -v a="$T0" -v b="$T1" 'BEGIN{printf "%.4f", b-a}')"

if [[ ! "$SCRAPE_CODE" =~ ^2 ]]; then
    log "FEHLER: Scrape von ${NODE_EXPORTER_URL} fehlgeschlagen (HTTP ${SCRAPE_CODE}) - es wird NICHT gepusht"
    exit 1
fi

# ---------------------------------------------------------------------------
# 2) Payload aufbereiten
#    - Kommentare ausser HELP/TYPE entfernen
#    - client-side Timestamps entfernen (Pushgateway lehnt sie mit 400 ab)
#    - optional job=/instance= Labels entfernen (kollidieren mit Grouping Key)
#    - optional Familien-Filter
# ---------------------------------------------------------------------------
awk -v inc="$INCLUDE_REGEX" -v exc="$EXCLUDE_REGEX" -v strip="$STRIP_RESERVED_LABELS" '
function keep(fam) {
    if (inc != "" && fam !~ inc) return 0
    if (exc != "" && fam ~  exc) return 0
    return 1
}
{
    line = $0
    if (line ~ /^[[:space:]]*$/) next

    if (line ~ /^#[[:space:]]*(HELP|TYPE)[[:space:]]/) {
        split(line, h, /[[:space:]]+/)
        if (!keep(h[3])) next
        print line
        next
    }
    if (line ~ /^#/) next          # UNIT / sonstige Kommentare

    # Familienname = alles bis zum ersten "{" oder Whitespace
    fam = line
    sub(/[{[:space:]].*$/, "", fam)
    if (!keep(fam)) next

    # Timestamp am Zeilenende abschneiden: "<value> <ts>" -> "<value>"
    if (match(line, /[[:space:]]+-?[0-9]+$/)) {
        head = substr(line, 1, RSTART-1)
        if (head ~ /[[:space:]](-?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?|[-+]?Inf|NaN)$/)
            line = head
    }

    # reservierte Labels entfernen
    if (strip == "true" && line ~ /(\{|,)(job|instance)=/) {
        gsub(/(job|instance)="[^"]*",[[:space:]]*/, "", line)
        gsub(/,[[:space:]]*(job|instance)="[^"]*"/,  "", line)
        gsub(/\{(job|instance)="[^"]*"\}/,          "", line)
    }

    print line
}
' "$RAW" > "$BUF"

SAMPLES="$(grep -cve '^#' -e '^$' "$BUF" || true)"
PAYLOAD_BYTES="$(wc -c <"$BUF" | tr -d ' ')"

# ---------------------------------------------------------------------------
# 3) Sanity-Checks: nie eine abgeschnittene Payload pushen. Ein PUT ersetzt
#    die ganze Gruppe - ein halber Body wuerde stumm halbe Dashboards leeren.
# ---------------------------------------------------------------------------
if (( SAMPLES < MIN_SAMPLES )); then
    log "FEHLER: nur ${SAMPLES} Samples (< ${MIN_SAMPLES}) - Payload verdaechtig, kein Push"
    exit 1
fi
if [[ -n "$REQUIRE_FAMILY" ]] && ! grep -q "^${REQUIRE_FAMILY}" "$BUF"; then
    log "FEHLER: Pflicht-Metrik ${REQUIRE_FAMILY} fehlt - Payload unvollstaendig, kein Push"
    exit 1
fi

# ---------------------------------------------------------------------------
# 4) Relay-Selbstmetriken anhaengen
# ---------------------------------------------------------------------------
if [[ "$ADD_RELAY_METRICS" == "true" ]]; then
    cat >>"$BUF" <<EOF
# HELP pushrelay_scrape_duration_seconds Dauer des lokalen node_exporter-Scrapes
# TYPE pushrelay_scrape_duration_seconds gauge
pushrelay_scrape_duration_seconds ${SCRAPE_SECONDS}
# HELP pushrelay_samples Anzahl gepushter Samples
# TYPE pushrelay_samples gauge
pushrelay_samples ${SAMPLES}
# HELP pushrelay_payload_bytes Groesse der vom node_exporter geholten Payload (ohne Relay-Metriken)
# TYPE pushrelay_payload_bytes gauge
pushrelay_payload_bytes ${PAYLOAD_BYTES}
# HELP pushrelay_last_run_timestamp_seconds Unix-Zeit des letzten Relay-Laufs
# TYPE pushrelay_last_run_timestamp_seconds gauge
pushrelay_last_run_timestamp_seconds $(date +%s)
# HELP pushrelay_info Version des Relay-Skripts
# TYPE pushrelay_info gauge
pushrelay_info{version="${VERSION}"} 1
EOF
fi

# ---------------------------------------------------------------------------
# 5) Push  (PUT = ersetzt die komplette Gruppe -> keine verwaisten Metriken)
# ---------------------------------------------------------------------------
if [[ "$DRY_RUN" == "1" ]]; then
    cat "$BUF"
    log "dry-run: ${SAMPLES} Samples, ${PAYLOAD_BYTES} Bytes, Ziel waere ${PUSH_URL}"
    exit 0
fi

HTTP_CODE="$(curl "${CURL_BASE[@]}" "${PG_AUTH[@]}" \
    --retry 3 --retry-delay 2 --retry-connrefused \
    -X PUT --data-binary "@${BUF}" \
    -H 'Content-Type: text/plain; version=0.0.4' \
    -o /dev/null -w '%{http_code}' "$PUSH_URL")"

if [[ "$HTTP_CODE" =~ ^2 ]]; then
    log "OK: ${SAMPLES} Samples / ${PAYLOAD_BYTES} B -> ${PUSH_URL} (HTTP ${HTTP_CODE}, scrape ${SCRAPE_SECONDS}s)"
    exit 0
fi

log "FEHLER: Push fehlgeschlagen (HTTP ${HTTP_CODE}) -> ${PUSH_URL}"
[[ "$HTTP_CODE" == "400" ]] && log "HINWEIS: 400 = Payload vom Pushgateway abgelehnt. Pruefe: 'curl -s ${NODE_EXPORTER_URL} | promtool check metrics'"
exit 2
