#!/usr/bin/env bash
#
# CheckMK local check: PHP-Versionen
#
# cmk-cache-interval: 3600
#
#   ohne Parameter : erzeugt <<<local:sep(0)>>> Output
#   --detect       : exit 0 = Plugin wird auf dieser Maschine benoetigt
#   --list         : Debug - zeigt CLI, installierte Versionen und FPM-Pools
#
# Prueft getrennt:
#   * die Standard-PHP-Version von root (CLI) - die benutzen Cronjobs,
#     Deployment-Skripte und Werkzeuge wie wp-cli oder occ, wenn ihnen keine
#     Version vorgegeben wird
#   * jeden laufenden FPM-Pool - das ist die Version, die tatsaechlich
#     Besucher-Requests verarbeitet
#
# Bewusst getrennt von den Anwendungs-Plugins: mehrere Sites teilen sich oft
# einen Pool, und Anwendungen ohne eigenes Plugin (TYPO3, Shopware, eigene
# Software) waeren sonst gar nicht erfasst.
#
# Benoetigt: bash, php; optional curl (EOL-Daten), systemctl
# Netz:      endoflife.date - ohne Zugriff wird auf den Cache zurueckgegriffen
#
set -uo pipefail

########################### Konfiguration ##################################
PHP_WARN_DAYS="${PHP_WARN_DAYS:-90}"

# Pools, die zwar konfiguriert sind, deren FPM-Dienst aber nicht laeuft:
# 1 = melden (Standard), 0 = ignorieren.
CHECK_STOPPED_POOLS="${CHECK_STOPPED_POOLS:-1}"

# Eine Version gilt als STILLGELEGT, wenn der Dienst weder laeuft noch fuer
# den Autostart aktiviert ist UND keine Pool-Konfiguration mehr existiert
# (Verzeichnis geloescht/verschoben oder leer). Dann verarbeitet diese Version
# nichts und kann auch nicht versehentlich wieder anspringen - ein EOL-Alarm
# waere sinnlos, selbst wenn das Paket noch installiert ist.
# Auf 0 setzen, wenn stillgelegte EOL-Versionen trotzdem gemeldet werden
# sollen (etwa um die Deinstallation nachzuhalten).
IGNORE_RETIRED="${IGNORE_RETIRED:-1}"

PHP_EOL_API_V1="${PHP_EOL_API_V1:-https://endoflife.date/api/v1/products/php/releases}"
PHP_EOL_API_LEGACY="${PHP_EOL_API_LEGACY:-https://endoflife.date/api/php}"
PHP_EOL_HTTP_TIMEOUT="${PHP_EOL_HTTP_TIMEOUT:-6}"

# Geteilter Cache: "<major.minor>\t<eol-datum>\t<abruf-epoch>"
PHP_EOL_CACHE="${PHP_EOL_CACHE:-/var/lib/check_mk_agent/php_eol.cache}"
PHP_EOL_CACHE_TTL="${PHP_EOL_CACHE_TTL:-604800}"   # 7 Tage

declare -A PHP_EOL_MEMO=()

########################### Helfer #########################################

have() { command -v "$1" >/dev/null 2>&1; }

emit() {
  local state="$1" name="$2" perf="${3:--}" text="$4"
  name="${name//\"/}"; name="${name//$'\t'/ }"
  text="${text//$'\n'/ }"; text="${text//$'\t'/ }"
  printf '%s "%s" %s %s\n' "$state" "$name" "$perf" "$text"
}

########################### detect mode ####################################

if [[ "${1:-}" == "--detect" ]]; then
  have php && exit 0
  compgen -G "/etc/php/*/fpm" >/dev/null 2>&1 && exit 0
  exit 1
fi

########################### PHP EOL ########################################

eol_from_json() {
  grep -oE '"(eolFrom|eol)"[[:space:]]*:[[:space:]]*"[0-9]{4}-[0-9]{2}-[0-9]{2}"' \
    | head -n1 | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}'
}

php_eol_cache_get() {  # $1 = mm -> "datum timestamp"
  [[ -r "$PHP_EOL_CACHE" ]] || return 1
  awk -v v="$1" '
    $1 == v && $2 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/ && $3 ~ /^[0-9]+$/ { d=$2; t=$3 }
    END { if (d == "") exit 1; print d, t }
  ' "$PHP_EOL_CACHE" 2>/dev/null
}

php_eol_cache_put() {  # $1 = mm, $2 = Datum
  local tmp
  mkdir -p "$(dirname "$PHP_EOL_CACHE")" 2>/dev/null || return 0
  printf '%s\t%s\t%s\n' "$1" "$2" "$(date +%s)" >> "$PHP_EOL_CACHE" 2>/dev/null || return 0
  if (( $(wc -l < "$PHP_EOL_CACHE" 2>/dev/null || echo 0) > 50 )); then
    tmp="$(mktemp "${PHP_EOL_CACHE}.XXXXXX" 2>/dev/null)" || return 0
    awk '{ e[$1] = $0 } END { for (k in e) print e[k] }' "$PHP_EOL_CACHE" > "$tmp" 2>/dev/null \
      && mv -f "$tmp" "$PHP_EOL_CACHE" 2>/dev/null
    rm -f "$tmp" 2>/dev/null
  fi
  return 0
}

php_eol_fetch() {  # $1 = mm
  local mm="$1" json d=""
  have curl || return 0
  json="$(curl -fsS --max-time "$PHP_EOL_HTTP_TIMEOUT" \
            -H 'Accept: application/json' "$PHP_EOL_API_V1/$mm" 2>/dev/null)"
  d="$(printf '%s' "$json" | eol_from_json)"
  if [[ -z "$d" ]]; then
    json="$(curl -fsS --max-time "$PHP_EOL_HTTP_TIMEOUT" \
              "$PHP_EOL_API_LEGACY/$mm.json" 2>/dev/null)"
    d="$(printf '%s' "$json" | eol_from_json)"
  fi
  [[ -n "$d" ]] && printf '%s\n' "$d"
  return 0
}

# Memo -> Cache (frisch) -> API -> Cache (abgelaufen)
php_eol_date() {  # $1 = major.minor
  local mm="$1" memo entry cached_date cached_ts age d=""
  [[ -n "$mm" ]] || return 0
  memo="${PHP_EOL_MEMO[$mm]:-}"
  if [[ -n "$memo" ]]; then
    [[ "$memo" == "-" ]] || printf '%s\n' "$memo"
    return 0
  fi
  if entry="$(php_eol_cache_get "$mm")"; then
    cached_date="${entry%% *}"; cached_ts="${entry##* }"
    age=$(( $(date +%s) - cached_ts ))
    if (( age < PHP_EOL_CACHE_TTL )); then
      PHP_EOL_MEMO[$mm]="$cached_date"; printf '%s\n' "$cached_date"; return 0
    fi
  fi
  d="$(php_eol_fetch "$mm")"
  if [[ -n "$d" ]]; then
    php_eol_cache_put "$mm" "$d"
  elif [[ -n "${cached_date:-}" ]]; then
    d="$cached_date"
  fi
  PHP_EOL_MEMO[$mm]="${d:--}"
  [[ -n "$d" ]] && printf '%s\n' "$d"
  return 0
}

# Setzt EOL_STATE / EOL_DAYS / EOL_TEXT
php_eol_check() {  # $1 = Version
  local ver="$1" mm eol eol_ts days
  EOL_STATE=3; EOL_DAYS=""; EOL_TEXT="Version nicht ermittelbar"
  [[ -n "$ver" ]] || return 0
  mm="$(printf '%s' "$ver" | cut -d. -f1,2)"
  eol="$(php_eol_date "$mm")"
  if [[ -z "$eol" ]]; then
    EOL_TEXT="EOL-Datum fuer PHP $mm nicht abrufbar (endoflife.date erreichbar?)"
    return 0
  fi
  eol_ts="$(date -d "$eol" +%s 2>/dev/null)" \
    || { EOL_TEXT="EOL-Datum '$eol' nicht lesbar"; return 0; }
  days=$(( (eol_ts - $(date +%s)) / 86400 ))
  EOL_DAYS="$days"
  if (( days < 0 )); then
    EOL_STATE=2; EOL_TEXT="seit $eol End-of-Life (${days#-} Tage)"
  elif (( days < PHP_WARN_DAYS )); then
    EOL_STATE=1; EOL_TEXT="erreicht am $eol EOL, nur noch $days Tage"
  else
    EOL_STATE=0; EOL_TEXT="Support bis $eol (noch $days Tage)"
  fi
}

########################### Inventar #######################################

# Standard-PHP von root: das benutzt alles, was ohne explizite Version
# aufgerufen wird - Cronjobs, Deployment-Skripte, composer, wp-cli, occ.
cli_version() {
  have php || return 1
  php -r 'echo PHP_VERSION;' 2>/dev/null | tr -d '[:space:]'
}

cli_path() { command -v php 2>/dev/null; }

# Alle installierten Versionen (Debian-Layout und phpX.Y im PATH)
installed_versions() {
  local d v
  for d in /etc/php/*/; do
    [[ -d "$d" ]] || continue
    v="${d%/}"; v="${v##*/}"
    [[ "$v" =~ ^[0-9]+\.[0-9]+$ ]] && printf '%s\n' "$v"
  done
  compgen -c 'php' 2>/dev/null | grep -oE '^php[0-9]+\.[0-9]+$' | sed 's/^php//'
  return 0
} 

# Alle PHP-Versionen mit FPM-Installation.
# Ausgabe: "version|pool|aktiv|autostart"   pool leer = keine Konfiguration
#
# Bewusst nicht nur ueber pool.d: eine Version, deren Konfiguration entfernt
# wurde, deren Dienst aber noch aktiviert ist, soll sichtbar bleiben - sie
# kann beim naechsten Start wieder hochkommen.
fpm_pools() {
  local d base v f pool active enabled
  local -A seen=()
  for d in /etc/php/*/ /etc/php*/; do
    [[ -d "$d" ]] || continue
    v="${d%/}"; v="${v##*/}"
    [[ "$v" =~ ^[0-9]+\.[0-9]+$ ]] || continue
    [[ -n "${seen[$v]:-}" ]] && continue
    seen[$v]=1

    active="$(systemctl is-active  "php$v-fpm.service" 2>/dev/null)"
    enabled="$(systemctl is-enabled "php$v-fpm.service" 2>/dev/null)"

    # Kein fpm-Verzeichnis: die Version hat gar keine FPM-Installation
    # (oder sie wurde verschoben) - nur melden, wenn der Dienst noch da ist.
    if [[ ! -d "${d}fpm" ]]; then
      [[ -n "$active$enabled" ]] && printf '%s||%s|%s\n' "$v" "${active:-unbekannt}" "${enabled:-unbekannt}"
      continue
    fi

    local found=0
    for f in "${d}fpm/pool.d"/*.conf; do
      [[ -f "$f" ]] || continue
      found=1
      pool="${f##*/}"; pool="${pool%.conf}"
      printf '%s|%s|%s|%s\n' "$v" "$pool" "${active:-unbekannt}" "${enabled:-unbekannt}"
    done
    (( found )) || printf '%s||%s|%s\n' "$v" "${active:-unbekannt}" "${enabled:-unbekannt}"
  done
  return 0
}

########################### debug ##########################################

if [[ "${1:-}" == "--list" ]]; then
  printf 'CLI (root default): %s  (%s)\n' "$(cli_version)" "$(cli_path)"
  printf 'Installiert:        %s\n' "$(installed_versions | sort -Vu | paste -sd' ' -)"
  echo 'FPM-Pools:'
  fpm_pools | sort | while IFS='|' read -r v p a e; do
    printf '  php%-5s %-24s aktiv: %-10s autostart: %s\n' "$v" "${p:-<keine Pools>}" "$a" "$e"
  done
  exit 0
fi

########################### Checks #########################################

check_cli() {
  local ver path
  ver="$(cli_version)"
  path="$(cli_path)"
  if [[ -z "$ver" ]]; then
    emit 3 "PHP CLI" "-" "UNKNOWN - kein php im PATH von root"
    return 0
  fi
  php_eol_check "$ver"
  local perf="-"
  [[ -n "$EOL_DAYS" ]] && perf="php_eol_days=${EOL_DAYS};${PHP_WARN_DAYS};0;"
  local txt=(OK WARN CRIT UNKNOWN)
  emit "$EOL_STATE" "PHP CLI" "$perf" \
    "${txt[$EOL_STATE]} - Standard-PHP von root ist $ver ($path), $EOL_TEXT - benutzt von Cronjobs, Deployment-Skripten und CLI-Werkzeugen ohne explizite Version"
}

check_pools() {
  local v pool active enabled
  local -A byver=() actbyver=() enbyver=()
  while IFS='|' read -r v pool active enabled; do
    [[ -n "$v" ]] || continue
    [[ -n "$pool" ]] && byver[$v]="${byver[$v]:-} $pool"
    [[ -n "${byver[$v]:-}" ]] || byver[$v]=""
    actbyver[$v]="$active"; enbyver[$v]="$enabled"
  done < <(fpm_pools)

  if (( ${#byver[@]} == 0 )); then
    emit 0 "PHP FPM Pools" "fpm_versions=0" "OK - keine PHP-FPM-Installation vorhanden"
    return 0
  fi

  local n=0 retired=0
  local -a eol_versions=() retired_versions=()
  for v in $(printf '%s\n' "${!byver[@]}" | sort -V); do
    active="${actbyver[$v]}"; enabled="${enbyver[$v]}"
    local pools="${byver[$v]# }" cnt=0
    [[ -n "$pools" ]] && cnt="$(wc -w <<< "$pools")"

    # --- Stillgelegt: aus, nicht aktiviert, keine Konfiguration ---
    local is_off=0 is_disabled=0
    [[ "$active"  != "active"  ]] && is_off=1
    case "$enabled" in enabled|enabled-runtime|static|indirect) ;; *) is_disabled=1 ;; esac
    if (( is_off && is_disabled )) && (( cnt == 0 )); then
      retired=$(( retired + 1 )); retired_versions+=("$v")
      if [[ "$IGNORE_RETIRED" == "1" ]]; then
        continue
      fi
    fi

    n=$(( n + 1 ))
    php_eol_check "$v"
    local state="$EOL_STATE" note=""
    local txt=(OK WARN CRIT UNKNOWN)

    if (( is_off )); then
      if [[ "$CHECK_STOPPED_POOLS" == "1" ]]; then
        note=" - php$v-fpm ist $active/$enabled"
        (( cnt > 0 )) && note="$note, die Pools verarbeiten derzeit nichts" \
                      || note="$note, keine Pool-Konfiguration"
        # Nicht laufend heisst: kein akutes Risiko. Aber solange der Dienst
        # aktiviert ist oder Pools existieren, kann er wieder anspringen.
        (( state == 2 )) && state=1
      else
        continue
      fi
    fi

    (( state >= 2 )) && eol_versions+=("$v")
    local perf="-"
    [[ -n "$EOL_DAYS" ]] && perf="php_eol_days=${EOL_DAYS};${PHP_WARN_DAYS};0;"
    emit "$state" "PHP FPM $v" "$perf pools=$cnt" \
      "${txt[$state]} - PHP $v, $EOL_TEXT, $cnt Pool(s)$( [[ -n "$pools" ]] && { [[ $cnt -le 8 ]] && echo ": $pools" || echo ": $(cut -d' ' -f1-8 <<< "$pools") …"; })$note"
  done

  local rnote=""
  (( retired > 0 )) && rnote=", $retired stillgelegt (aus, nicht aktiviert, ohne Konfiguration): ${retired_versions[*]}"
  emit "$( (( ${#eol_versions[@]} > 0 )) && echo 2 || echo 0 )" "PHP FPM Pools" \
    "fpm_versions=$n fpm_eol_versions=${#eol_versions[@]};;1;0 fpm_retired=$retired" \
    "$( (( ${#eol_versions[@]} > 0 )) \
        && echo "CRIT - $n aktive PHP-Version(en), davon End-of-Life: ${eol_versions[*]}$rnote" \
        || echo "OK - $n aktive PHP-Version(en), keine davon End-of-Life$rnote" )"
}

########################### Ausgabe sammeln ################################

collect() {
  echo "<<<local:sep(0)>>>"
  check_cli
  check_pools
  return 0
}

collect
exit 0