#!/usr/bin/env bash
#
# CheckMK local check: Samba Active Directory Domain Controller
#
# cmk-cache-interval: 3600
#
#   ohne Parameter : erzeugt <<<local:sep(0)>>> Output
#   --detect       : exit 0 = Plugin wird auf dieser Maschine benoetigt
#
# Benoetigt: bash, samba-tool, testparm; optional ldapsearch, openssl,
#            chronyc/ntpq/timedatectl, host/dig
#
# Achtung: kein "set -e" - viele Aufrufe duerfen fehlschlagen, ohne dass das
# Plugin abbricht. Fehler werden pro Service in einen Status uebersetzt.
set -uo pipefail

########################### Konfiguration ##################################
MAX_PARALLEL="${MAX_PARALLEL:-$(nproc 2>/dev/null || echo 4)}"

SAMBA_UNIT="${SAMBA_UNIT:-samba-ad-dc}"
SAMBA_PRIVATE="${SAMBA_PRIVATE:-/var/lib/samba/private}"

# dbcheck laeuft ueber die gesamte Datenbank und dauert bei grossen Domains
# Minuten. Mit dem Stundenintervall des Plugin-Managers ist das vertretbar -
# wenn nicht, hier abschalten und in ein eigenes Plugin mit Tagesintervall
# auslagern.
SAMBA_DBCHECK="${SAMBA_DBCHECK:-1}"
DBCHECK_TIMEOUT="${DBCHECK_TIMEOUT:-600}"
SYSVOLCHECK_TIMEOUT="${SYSVOLCHECK_TIMEOUT:-300}"
CMD_TIMEOUT="${CMD_TIMEOUT:-60}"

# Kerberos bricht ab 300s Zeitversatz zusammen (Standard-Skew). CRIT bei 240s
# laesst eine Minute Vorlauf, um zu reagieren, bevor die Anmeldungen scheitern.
TIME_WARN_SEC="${TIME_WARN_SEC:-120}"
TIME_CRIT_SEC="${TIME_CRIT_SEC:-240}"

# Replikation: einzelne Fehlversuche sind Normalbetrieb (RPC-Abbruch, kurzer
# Netzaussetzer - typisch WERR_NETNAME_DELETED). Der Zaehler bleibt stehen,
# bis der naechste Zyklus durchlaeuft, und alarmiert daher lange nachdem sich
# das Problem selbst erledigt hat.
#
# Bewertet wird deshalb AUSSCHLIESSLICH das Alter der letzten erfolgreichen
# Replikation. Solange die Replikation durchlaeuft, ist der Zustand OK - egal
# wie viele Fehlversuche der Zaehler zwischendurch mitgenommen hat. Der
# intra-site KCC-Zyklus liegt bei 15 Minuten; 2h ohne Erfolg heisst also, dass
# mehrere Zyklen in Folge nicht durchgekommen sind.
REPL_WARN_AGE_HOURS="${REPL_WARN_AGE_HOURS:-2}"
REPL_CRIT_AGE_HOURS="${REPL_CRIT_AGE_HOURS:-6}"

CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
CERT_CRIT_DAYS="${CERT_CRIT_DAYS:-7}"

# Verzeichnisse mit "samba-tool domain backup"-Archiven (Leerzeichen-getrennt)
# "samba-tool domain backup online|offline" - im zmd-ad liegen beide Varianten
# unter /backup. Alle Verzeichnisse werden geprueft, gewertet wird das global
# juengste Archiv (ein frisches Online-Backup soll nicht von einem alten
# Offline-Backup verdeckt werden).
BACKUP_DIRS="${BACKUP_DIRS:-/backup/online /backup/offline /var/univention-backup/samba /var/backups/samba /var/lib/samba/backups /backup/samba}"

# Archiv-Endungen. Bewusst breit: "samba-tool domain backup" schreibt tar.bz2,
# Univention und eigene Skripte nutzen teils andere Kompression.
BACKUP_PATTERNS="${BACKUP_PATTERNS:-*.tar.bz2 *.tar.gz *.tar.xz *.tar.zst *.tgz *.tar}"
# Cronjob, der die Backups anlegt
BACKUP_CRON="${BACKUP_CRON:-/etc/cron.d/smb-backup}"
BACKUP_WARN_HOURS="${BACKUP_WARN_HOURS:-48}"
BACKUP_CRIT_HOURS="${BACKUP_CRIT_HOURS:-168}"

# Ports, die ein AD DC bedienen muss
DC_PORTS="${DC_PORTS:-53 88 135 139 389 445 464 636 3268 3269}"

# Externer Name fuer den Forwarder-Test; leer = Test ueberspringen
DNS_FORWARD_TEST="${DNS_FORWARD_TEST:-}"

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

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

emit() {  # <state> <servicename> <perfdata|-> <text>
  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"
}

st() { timeout "$CMD_TIMEOUT" samba-tool "$@" 2>&1; }

tp() {  # Wert aus smb.conf, ohne die Datei selbst zu parsen
  testparm -s --parameter-name="$1" 2>/dev/null | tail -n1 | tr -d '[:space:]'
}

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

# testparm meldet die BERECHNETE Rolle auf stderr ("Server role: ROLE_...").
# --parameter-name='server role' liefert dagegen nur den KONFIGURIERTEN Wert -
# und der steht bei einem Member ueblicherweise gar nicht in der smb.conf,
# dann kommt der Default "AUTO" zurueck. Die Rolle wird dort aus "security"
# abgeleitet, nicht aus dem Parameter.
server_role() {
  local r
  r="$(timeout "$CMD_TIMEOUT" testparm -s 2>&1 >/dev/null \
       | sed -nE 's/^Server role:[[:space:]]*(.*)$/\1/p' | head -n1)"
  [[ -n "$r" ]] || r="$(tp 'server role')"
  printf '%s\n' "$r"
}

# Univention Corporate Server bringt seine eigene Backup-Verwaltung mit
# (Listener/Join-Skripte statt eines Cronjobs in /etc/cron.d). Dort nach
# smb-backup zu suchen erzeugt einen Daueralarm fuer etwas, das es auf diesem
# System per Design nicht gibt.
is_univention() {
  [[ -f /etc/univention/base.conf ]] && return 0
  [[ -d /var/univention-backup ]] && return 0
  have ucr && return 0
  return 1
}

is_ad_dc() {
  have samba-tool || return 1
  [[ -f "$SAMBA_PRIVATE/sam.ldb" ]] || return 1
  local role; role="$(server_role)"
  case "${role^^}" in
    *ROLE_ACTIVE_DIRECTORY_DC*|*"ACTIVE DIRECTORY DOMAIN CONTROLLER"*) return 0 ;;
  esac
  return 1
}

if [[ "${1:-}" == "--detect" ]]; then
  is_ad_dc && exit 0
  exit 1
fi

########################### Job-Pool #######################################

SEM_FD=""

pool_init() {
  local fifo i
  (( MAX_PARALLEL > 1 )) || return 1
  fifo="$(mktemp -u)" || return 1
  mkfifo "$fifo" 2>/dev/null || return 1
  exec {SEM_FD}<>"$fifo" || { rm -f "$fifo"; SEM_FD=""; return 1; }
  rm -f "$fifo"
  for (( i = 0; i < MAX_PARALLEL; i++ )); do printf '.' >&"$SEM_FD"; done
  return 0
}

pool_run() {
  local out="$1"; shift
  if [[ -z "$SEM_FD" ]]; then "$@" > "$out" 2>/dev/null; return 0; fi
  local tok
  IFS= read -r -n1 -u "$SEM_FD" tok
  { "$@" > "$out" 2>/dev/null; printf '.' >&"$SEM_FD"; } &
}

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

check_service() {
  local active enabled ver
  active="$(systemctl is-active "$SAMBA_UNIT.service" 2>/dev/null)"
  enabled="$(systemctl is-enabled "$SAMBA_UNIT.service" 2>/dev/null)"
  ver="$(samba --version 2>/dev/null | head -n1)"

  # Auf einem AD DC startet der samba-Daemon smbd/nmbd/winbindd selbst.
  # Geprueft wird is-active, NICHT is-enabled: die Debian-Units bleiben auf
  # "enabled" stehen und brechen auf einem DC ueber ihre Condition sofort ab -
  # ein Konflikt besteht erst, wenn sie tatsaechlich zusaetzlich laufen.
  local -a conflict=()
  local u
  for u in smbd nmbd winbind; do
    [[ "$(systemctl is-active "$u.service" 2>/dev/null)" == "active" ]] && conflict+=("$u")
  done

  if [[ "$active" != "active" ]]; then
    emit 2 "Samba DC Service" "-" "CRIT - $SAMBA_UNIT ist $active (${ver:-Version unbekannt})"
  elif (( ${#conflict[@]} > 0 )); then
    emit 1 "Samba DC Service" "-" \
      "WARN - $SAMBA_UNIT laeuft, aber zusaetzlich laufen: ${conflict[*]} - auf einem DC startet samba diese Dienste selbst"
  elif [[ "$enabled" != "enabled" ]]; then
    emit 1 "Samba DC Service" "-" "WARN - $SAMBA_UNIT laeuft, ist aber nicht fuer den Autostart aktiviert ($enabled)"
  else
    emit 0 "Samba DC Service" "-" "OK - $SAMBA_UNIT aktiv und aktiviert (${ver:-?})"
  fi
}

check_config() {
  local out errs
  out="$(timeout "$CMD_TIMEOUT" testparm -s 2>&1 >/dev/null)"
  errs="$(grep -icE '^(ERROR|Error|Unknown parameter|Global parameter .* found in service)' <<< "$out")"
  if (( errs > 0 )); then
    emit 1 "Samba DC Config" "config_errors=$errs;1;;0" \
      "WARN - testparm meldet $errs Problem(e): $(grep -iE '^(ERROR|Unknown parameter)' <<< "$out" | head -n3 | paste -sd'; ' -)"
  else
    emit 0 "Samba DC Config" "config_errors=0;1;;0" "OK - smb.conf ohne Beanstandung"
  fi
}

# Wie viele DCs kennt die Domain? Primaer aus der lokalen Datenbank (kein
# Netz, keine Credentials), sonst ueber die DNS-SRV-Records - das ist zugleich
# die Sicht, die Clients auf die Domain haben.
dc_count() {
  local n="" realm
  if have ldbsearch; then
    n="$(timeout "$CMD_TIMEOUT" ldbsearch -H "$SAMBA_PRIVATE/sam.ldb" --cross-ncs \
          '(objectClass=nTDSDSA)' dn 2>/dev/null | grep -c '^dn:')"
  fi
  if [[ -z "$n" || "$n" == "0" ]]; then
    realm="$(tp realm)"
    if have dig; then
      n="$(timeout 10 dig +short +time=3 "@127.0.0.1" SRV "_ldap._tcp.dc._msdcs.$realm" 2>/dev/null | grep -c .)"
    elif have host; then
      n="$(timeout 10 host -t SRV "_ldap._tcp.dc._msdcs.$realm" 127.0.0.1 2>/dev/null | grep -c 'SRV record')"
    fi
  fi
  printf '%s\n' "${n:-0}"
}

# Der wichtigste Check auf einem Multi-DC-Verbund: ein DC mit stehender
# Replikation beantwortet LDAP, DNS und Kerberos einwandfrei und liefert
# trotzdem veraltete Daten aus.
check_replication() {
  local out dcs total max part_bad oldest_age codes line ts age now
  local -a fails=()
  dcs="$(dc_count)"
  local raw
  raw="$(st drs showrepl)"

  if [[ -z "$raw" ]] || grep -qiE 'ERROR|Failed to bind|NT_STATUS' <<< "${raw%%$'\n'*}"; then
    emit 3 "Samba DC Replication" "-" "UNKNOWN - drs showrepl lieferte keine verwertbare Ausgabe: ${raw%%$'\n'*}"
    return 0
  fi

  # NUR die eingehenden Nachbarn auswerten. Bei OUTBOUND NEIGHBORS steht
  # regulaer "Last success @ NTTIME(0)", weil die ausgehende Replikation vom
  # Partner angestossen wird und dieser DC dafuer keinen Erfolgszeitstempel
  # fuehrt - das als "noch nie erfolgreich" zu werten, erzeugt auf jedem
  # gesunden Multi-DC-Verbund einen Daueralarm. Eingehend ist ohnehin das,
  # was den lokalen Datenbestand aktuell haelt.
  out="$(awk '/====[[:space:]]*INBOUND NEIGHBORS/ { f=1; next }
              /^[[:space:]]*====/ { f=0 }
              f' <<< "$raw")"
  [[ -n "${out//[[:space:]]/}" ]] || out="$raw"

  mapfile -t fails < <(grep -oE '[0-9]+ consecutive failure' <<< "$out" | grep -oE '^[0-9]+')
  total="${#fails[@]}"
  max=0; part_bad=0
  local f
  for f in ${fails[@]+"${fails[@]}"}; do
    (( f > 0 )) && part_bad=$(( part_bad + 1 ))
    (( f > max )) && max="$f"
  done

  # Alter des aeltesten "Last success" ueber alle Verbindungen. NTTIME(0)
  # bedeutet "noch nie erfolgreich" und wiegt am schwersten.
  now="$(date +%s)"; oldest_age=0
  local never=0
  while IFS= read -r line; do
    line="${line#*Last success @ }"
    if [[ "$line" == NTTIME\(0\)* ]]; then
      # "noch nie erfolgreich" zaehlt nur, wenn auch Fehlversuche anliegen.
      # Eine frisch angelegte Verbindung hat noch keinen Zeitstempel, ohne
      # dass etwas kaputt waere.
      (( part_bad > 0 )) && never=1
      continue
    fi
    ts="$(date -d "$line" +%s 2>/dev/null)" || continue
    age=$(( (now - ts) / 3600 ))
    (( age > oldest_age )) && oldest_age="$age"
  done < <(grep -E 'Last success @' <<< "$out")

  codes="$(grep -oE 'result [0-9]+ \([A-Z_]+\)' <<< "$out" | sort -u | paste -sd', ' -)"
  local perf="domain_dcs=$dcs repl_partners=$total repl_failing=$part_bad repl_max_failures=$max repl_last_success_age=$oldest_age;$REPL_WARN_AGE_HOURS;$REPL_CRIT_AGE_HOURS;0"

  if (( total == 0 )); then
    if (( dcs > 1 )); then
      emit 2 "Samba DC Replication" "domain_dcs=$dcs repl_partners=0 repl_failing=1" \
        "CRIT - Domain kennt $dcs DCs, dieser hat aber keine Replikationsverbindungen (KCC-Topologie verloren, verwaister Join?)"
    else
      emit 0 "Samba DC Replication" "domain_dcs=$dcs repl_partners=0 repl_failing=0" \
        "OK - Einzel-DC, keine Replikation erforderlich"
    fi
    return 0
  fi

  # Hinweis auf zwischenzeitliche Aussetzer - nur Information, kein Alarm.
  local hint=""
  (( part_bad > 0 )) && hint=" (zwischenzeitlich $max Fehlversuch(e) auf $part_bad von $total Verbindungen: $codes - hat sich selbst erholt)"

  if (( never )); then
    emit 2 "Samba DC Replication" "$perf" \
      "CRIT - mindestens eine Replikationsverbindung war noch NIE erfolgreich: $codes"
  elif (( oldest_age >= REPL_CRIT_AGE_HOURS )); then
    emit 2 "Samba DC Replication" "$perf" \
      "CRIT - seit ${oldest_age}h keine erfolgreiche Replikation (Grenze ${REPL_CRIT_AGE_HOURS}h), $part_bad von $total Verbindungen betroffen: $codes"
  elif (( oldest_age >= REPL_WARN_AGE_HOURS )); then
    emit 1 "Samba DC Replication" "$perf" \
      "WARN - letzte erfolgreiche Replikation vor ${oldest_age}h, mehrere Zyklen ohne Erfolg - CRIT ab ${REPL_CRIT_AGE_HOURS}h: $codes"
  else
    emit 0 "Samba DC Replication" "$perf" \
      "OK - Replikation laeuft, letzter Erfolg vor ${oldest_age}h ueber $total eingehende Verbindungen ($dcs DCs)$hint"
  fi
}

check_fsmo() {
  local out n
  out="$(st fsmo show)"
  if [[ -z "$out" ]] || ! grep -q 'owner:' <<< "$out"; then
    emit 3 "Samba DC FSMO" "-" "UNKNOWN - fsmo show lieferte keine Rollen"
    return 0
  fi
  n="$(grep -c 'owner:' <<< "$out")"
  emit 0 "Samba DC FSMO" "fsmo_roles=$n" \
    "OK - $n Rollen: $(sed -nE 's/^([A-Za-z ]+owner): CN=NTDS Settings,CN=([^,]+),.*/\1=\2/p' <<< "$out" | paste -sd', ' - | cut -c1-300)"
}

check_ldap() {
  local base p
  if have ldapsearch; then
    # Anonymer RootDSE-Read ist auf einem AD DC erlaubt und testet die
    # komplette Kette bis zur Datenbank, nicht nur den offenen Port.
    base="$(timeout "$CMD_TIMEOUT" ldapsearch -x -LLL -o ldif-wrap=no \
             -H ldap://127.0.0.1 -b "" -s base defaultNamingContext 2>/dev/null \
             | sed -nE 's/^defaultNamingContext:[[:space:]]*(.*)$/\1/p' | head -n1)"
    if [[ -n "$base" ]]; then
      emit 0 "Samba DC LDAP" "-" "OK - RootDSE beantwortet, defaultNamingContext=$base"
    else
      emit 2 "Samba DC LDAP" "-" "CRIT - LDAP auf 127.0.0.1:389 antwortet nicht oder liefert keinen defaultNamingContext"
    fi
    return 0
  fi
  # Ohne ldap-utils bleibt nur der Portcheck.
  if timeout 5 bash -c ": < /dev/tcp/127.0.0.1/389" 2>/dev/null; then
    emit 1 "Samba DC LDAP" "-" "WARN - Port 389 offen, aber ldapsearch fehlt (apt install ldap-utils) - keine echte Abfrage moeglich"
  else
    emit 2 "Samba DC LDAP" "-" "CRIT - Port 389 nicht erreichbar"
  fi
}

check_ports() {
  local p open=0 total=0
  local -a closed=()
  for p in $DC_PORTS; do
    total=$(( total + 1 ))
    if timeout 3 bash -c ": < /dev/tcp/127.0.0.1/$p" 2>/dev/null; then
      open=$(( open + 1 ))
    else
      closed+=("$p")
    fi
  done
  if (( ${#closed[@]} > 0 )); then
    emit 2 "Samba DC Ports" "ports_open=$open;;;0;$total ports_closed=${#closed[@]};;1;0" \
      "CRIT - $open/$total Ports offen, geschlossen: ${closed[*]} (88=Kerberos, 464=kpasswd, 3268/3269=Global Catalog)"
  else
    emit 0 "Samba DC Ports" "ports_open=$open;;;0;$total ports_closed=0;;1;0" \
      "OK - alle $total DC-Ports erreichbar"
  fi
}

check_dns() {
  local realm host_fqdn srv a fwd_state="" res
  realm="$(tp realm)"
  host_fqdn="$(hostname -f 2>/dev/null)"
  if [[ -z "$realm" ]]; then
    emit 3 "Samba DC DNS" "-" "UNKNOWN - realm nicht aus smb.conf ermittelbar"
    return 0
  fi
  if ! have host && ! have dig; then
    emit 3 "Samba DC DNS" "-" "UNKNOWN - weder host noch dig vorhanden (apt install dnsutils)"
    return 0
  fi

  q() {  # $1 = typ, $2 = name
    if have dig; then timeout 10 dig +short +time=3 "@127.0.0.1" "$1" "$2" 2>/dev/null
    else timeout 10 host -t "$1" "$2" 127.0.0.1 2>/dev/null | grep -v 'not found'; fi
  }

  srv="$(q SRV "_ldap._tcp.$realm")"
  a="$(q A "$host_fqdn")"

  local -a missing=()
  [[ -n "$srv" ]] || missing+=("SRV _ldap._tcp.$realm")
  [[ -n "$a"   ]] || missing+=("A $host_fqdn")

  if [[ -n "$DNS_FORWARD_TEST" ]]; then
    res="$(q A "$DNS_FORWARD_TEST")"
    [[ -n "$res" ]] || fwd_state=", Forwarder liefert nichts fuer $DNS_FORWARD_TEST"
  fi

  if (( ${#missing[@]} > 0 )); then
    emit 2 "Samba DC DNS" "-" "CRIT - lokale DNS-Aufloesung unvollstaendig, fehlt: ${missing[*]}$fwd_state"
  elif [[ -n "$fwd_state" ]]; then
    emit 1 "Samba DC DNS" "-" "WARN - eigene Records ok$fwd_state"
  else
    emit 0 "Samba DC DNS" "-" "OK - SRV _ldap._tcp.$realm und A $host_fqdn werden lokal aufgeloest"
  fi
}

# Kerberos vertraegt maximal 300s Versatz. Ein DC, dessen Uhr weglaeuft,
# sperrt sich selbst und alle Clients aus - deshalb eigener Service.
#
# Im LXC-Container gilt eine andere Logik: die Systemuhr kommt vom Kernel des
# Hosts, der Container hat kein CAP_SYS_TIME und DARF sie nicht stellen. Der
# Zeitdienst ist dort kein Uhrensteller, sondern Zeitserver fuer die Domain -
# er muss die Upstream-Zeit uebernehmen und aufs Netz geben. Entsprechend
# wird geprueft, ob er laeuft, upstream-synchron ist, auf 123/udp lauscht und
# die Uhr eben NICHT zu stellen versucht.

is_container() {
  local v
  v="$(systemd-detect-virt -c 2>/dev/null)"
  [[ -n "$v" && "$v" != "none" ]] || return 1
  printf '%s\n' "$v"
}

# chronyd -x = "no clock control". Im Container ist genau das die korrekte
# Betriebsart; ohne -x scheitert jeder Stellversuch und landet im Log.
chrony_sets_clock() {
  have pgrep || return 1
  pgrep -a chronyd 2>/dev/null | grep -qE ' -x( |$)' && return 1
  return 0
}

# Lauscht ueberhaupt jemand auf 123/udp? Ohne das bekommen die Domaenenclients
# keine Zeit, auch wenn der Dienst laeuft und sauber synchronisiert ist.
ntp_serving() {
  if have ss; then
    ss -lun 2>/dev/null | grep -qE ':123([[:space:]]|$)' && return 0
  elif have netstat; then
    netstat -lun 2>/dev/null | grep -qE ':123[[:space:]]' && return 0
  else
    return 0   # nicht pruefbar - nicht als Fehler werten
  fi
  return 1
}

time_state_chrony() {  # -> "offset|leap|stratum|refid"
  have chronyc || return 1
  local t off
  t="$(timeout 10 chronyc tracking 2>/dev/null)" || return 1
  off="$(sed -nE 's/^System time[[:space:]]*:[[:space:]]*([0-9.]+) seconds.*/\1/p' <<< "$t" | head -n1)"
  [[ -n "$off" ]] || return 1
  printf '%s|%s|%s|%s\n' "$off" \
    "$(sed -nE 's/^Leap status[[:space:]]*:[[:space:]]*(.*)$/\1/p'   <<< "$t" | head -n1)" \
    "$(sed -nE 's/^Stratum[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p'   <<< "$t" | head -n1)" \
    "$(sed -nE 's/^Reference ID[[:space:]]*:[[:space:]]*([^ ]+).*/\1/p' <<< "$t" | head -n1)"
}

time_state_ntpq() {  # -> "offset|leap|stratum|refid"
  have ntpq || return 1
  local rv off stratum refid
  rv="$(timeout 10 ntpq -c 'rv 0' 2>/dev/null)"
  off="$(grep -oE 'offset=-?[0-9.]+' <<< "$rv"  | head -n1 | cut -d= -f2)"
  stratum="$(grep -oE 'stratum=[0-9]+' <<< "$rv" | head -n1 | cut -d= -f2)"
  refid="$(grep -oE 'refid=[^,[:space:]]+' <<< "$rv" | head -n1 | cut -d= -f2)"
  [[ -n "$off" ]] || off="$(timeout 10 ntpq -pn 2>/dev/null | awk '/^[*o]/ { print $9; exit }')"
  [[ -n "$off" ]] || return 1
  printf '%s|%s|%s|%s\n' \
    "$(awk -v v="$off" 'BEGIN { if (v < 0) v = -v; printf "%.6f", v / 1000 }')" \
    "" "${stratum:-}" "${refid:-}"
}

time_state_timedatectl() {
  have timedatectl || return 1
  [[ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" == "yes" ]] || return 1
  printf '0|synchron||\n'
}

active_time_services() {
  local u id
  local -A seen=()
  for u in chrony chronyd ntpsec ntp ntpd openntpd systemd-timesyncd; do
    [[ "$(systemctl is-active "$u.service" 2>/dev/null)" == "active" ]] || continue
    # chrony.service und chronyd.service sind auf Debian dieselbe Unit
    # (Alias). Ohne Aufloesung auf die kanonische Id meldet der Check einen
    # Dienst als zwei und warnt vor einem Konflikt, den es nicht gibt.
    id="$(systemctl show -p Id --value "$u.service" 2>/dev/null)"
    id="${id:-$u.service}"
    [[ -n "${seen[$id]:-}" ]] && continue
    seen[$id]=1
    printf '%s\n' "${id%.service}"
  done
  return 0
}

check_time() {
  local off="" src="" res="" leap="" stratum="" refid="" ct=""
  local -a running=() notes=()
  mapfile -t running < <(active_time_services)
  ct="$(is_container)" || ct=""

  if res="$(time_state_chrony)";        then src="chrony"
  elif res="$(time_state_ntpq)";        then src="ntpq"
  elif res="$(time_state_timedatectl)"; then src="timedatectl"
  else res=""; fi

  if [[ -n "$res" ]]; then
    IFS='|' read -r off leap stratum refid <<< "$res"
  fi

  if [[ -z "$off" ]]; then
    if (( ${#running[@]} == 0 )); then
      emit 2 "Samba DC Time" "-" \
        "CRIT - kein Zeitdienst aktiv${ct:+ (Container $ct)} - die Domaenenclients bekommen keine Zeit vom DC"
    else
      emit 3 "Samba DC Time" "-" \
        "UNKNOWN - Zeitversatz ueber keine Quelle ermittelbar (aktiv: ${running[*]}) - chronyc/ntpq antworten nicht"
    fi
    return 0
  fi

  local abs int state=0 txt="OK"
  abs="${off#-}"
  int="${abs%%.*}"; int="${int:-0}"
  if   (( int >= TIME_CRIT_SEC )); then state=2; txt="CRIT"
  elif (( int >= TIME_WARN_SEC )); then state=1; txt="WARN"
  fi

  esc() { (( state < $1 )) && { state="$1"; txt="$2"; }; return 0; }

  # Upstream-Synchronitaet: ein Zeitserver, der selbst nicht synchron ist,
  # verteilt falsche Zeit an die ganze Domain.
  if [[ -n "$stratum" ]] && (( stratum >= 16 )); then
    notes+=("nicht upstream-synchron (Stratum $stratum)"); esc 1 WARN
  elif [[ -n "$leap" && "$leap" != "Normal" && "$leap" != "synchron" ]]; then
    notes+=("Leap-Status $leap"); esc 1 WARN
  fi

  if (( ${#running[@]} > 1 )); then
    notes+=("mehrere Zeitdienste aktiv (${running[*]}) - die streiten um die Systemuhr"); esc 1 WARN
  fi

  if [[ -n "$ct" ]]; then
    # Im Container ist "stellt die Uhr nicht" der Sollzustand, kein Fehler.
    if [[ "$src" == "chrony" ]] && chrony_sets_clock; then
      notes+=("chronyd laeuft ohne -x und versucht die Systemuhr zu stellen - im Container $ct nicht erlaubt, jeder Versuch scheitert"); esc 1 WARN
    fi
    if ! ntp_serving; then
      notes+=("lauscht nicht auf 123/udp - der DC gibt dem Netz keine Zeit"); esc 1 WARN
    fi
  fi

  local prefix=""
  [[ -n "$ct" ]] && prefix="Container $ct, Systemuhr kommt vom Host-Kernel - "

  local hint=""
  if (( state == 2 )) && [[ -n "$ct" ]]; then
    hint=" - Korrektur gehoert auf den LXC-Host, nicht in den Container"
  fi

  emit "$state" "Samba DC Time" "time_offset=$abs;$TIME_WARN_SEC;$TIME_CRIT_SEC;0" \
    "$txt - ${prefix}Versatz ${abs}s via $src${stratum:+, Stratum $stratum}${refid:+ ($refid)}$(
      (( ${#notes[@]} )) && printf ' - %s' "$(IFS='; '; printf '%s' "${notes[*]}")")$hint"
}

check_sysvol_acl() {
  local out rc
  out="$(timeout "$SYSVOLCHECK_TIMEOUT" samba-tool ntacl sysvolcheck 2>&1)"; rc=$?
  if (( rc == 124 )); then
    emit 3 "Samba DC Sysvol ACL" "-" "UNKNOWN - sysvolcheck nach ${SYSVOLCHECK_TIMEOUT}s abgebrochen"
  elif (( rc == 0 )) && [[ -z "${out//[[:space:]]/}" ]]; then
    emit 0 "Samba DC Sysvol ACL" "sysvol_acl_errors=0;;1;0" "OK - sysvol-ACLs stimmen mit der Datenbank ueberein"
  else
    emit 2 "Samba DC Sysvol ACL" "sysvol_acl_errors=1;;1;0" \
      "CRIT - sysvolcheck meldet Abweichungen (Reparatur: samba-tool ntacl sysvolreset): ${out%%$'\n'*}"
  fi
}

check_dbcheck() {
  local out rc errors
  if [[ "$SAMBA_DBCHECK" != "1" ]]; then
    emit 0 "Samba DC Database" "-" "OK - dbcheck deaktiviert (SAMBA_DBCHECK=0)"
    return 0
  fi
  out="$(timeout "$DBCHECK_TIMEOUT" samba-tool dbcheck --cross-ncs 2>&1)"; rc=$?
  if (( rc == 124 )); then
    emit 3 "Samba DC Database" "-" \
      "UNKNOWN - dbcheck nach ${DBCHECK_TIMEOUT}s abgebrochen - fuer grosse Domains in ein eigenes Plugin mit Tagesintervall auslagern"
    return 0
  fi
  errors="$(sed -nE 's/.*Checked [0-9]+ objects \(([0-9]+) errors\).*/\1/p' <<< "$out" | tail -n1)"
  errors="${errors:-}"
  if [[ -z "$errors" ]]; then
    emit 3 "Samba DC Database" "-" "UNKNOWN - dbcheck-Ausgabe nicht auswertbar: ${out##*$'\n'}"
  elif (( errors > 0 )); then
    emit 2 "Samba DC Database" "db_errors=$errors;;1;0" \
      "CRIT - dbcheck meldet $errors Fehler (Reparatur nur bewusst mit --fix --yes)"
  else
    emit 0 "Samba DC Database" "db_errors=0;;1;0" "OK - dbcheck ohne Fehler"
  fi
}

check_certs() {
  local f end end_ts days state txt found=0
  # Nur der Glob - cert.pem zusaetzlich explizit aufzufuehren erzeugte den
  # Service zweimal, und doppelte Servicenamen kollidieren in CheckMK.
  for f in "$SAMBA_PRIVATE"/tls/*.pem; do
    [[ -f "$f" ]] || continue
    case "$(basename "$f")" in *key*.pem) continue ;; esac
    have openssl || break
    end="$(openssl x509 -enddate -noout -in "$f" 2>/dev/null | cut -d= -f2)"
    [[ -n "$end" ]] || continue
    end_ts="$(date -d "$end" +%s 2>/dev/null)" || continue
    found=$(( found + 1 ))
    days=$(( (end_ts - $(date +%s)) / 86400 ))
    if   (( days < CERT_CRIT_DAYS )); then state=2; txt="CRIT"
    elif (( days < CERT_WARN_DAYS )); then state=1; txt="WARN"
    else                                   state=0; txt="OK"
    fi
    emit "$state" "Samba DC Cert $(basename "$f")" \
      "days_left=$days;$CERT_WARN_DAYS;$CERT_CRIT_DAYS;0" \
      "$txt - laeuft in $days Tagen ab ($end) - betrifft LDAPS/636 und StartTLS"
  done
  (( found > 0 )) || emit 3 "Samba DC Cert" "-" \
    "UNKNOWN - kein Zertifikat unter $SAMBA_PRIVATE/tls/ gefunden (openssl installiert?)"
}

check_backup() {
  local d newest="" ts age_h state txt file="" dirs=0
  local -a cand=()
  for d in $BACKUP_DIRS; do
    [[ -d "$d" ]] || continue
    dirs=$(( dirs + 1 ))
    # Suchausdruck aus BACKUP_PATTERNS bauen: -name A -o -name B -o ...
    local -a fargs=(); local pat
    for pat in $BACKUP_PATTERNS; do
      (( ${#fargs[@]} )) && fargs+=(-o)
      fargs+=(-name "$pat")
    done
    mapfile -t -O "${#cand[@]}" cand < <(
      find "$d" -maxdepth 2 -type f \( "${fargs[@]}" \) -printf '%T@ %p\n' 2>/dev/null)
  done
  if (( ${#cand[@]} > 0 )); then
    newest="$(printf '%s\n' "${cand[@]}" | sort -n | tail -n1)"
    ts="${newest%% *}"; ts="${ts%%.*}"; file="${newest#* }"
  fi

  if [[ -z "$file" ]]; then
    emit 2 "Samba DC Backup" "-" \
      "CRIT - kein Backup-Archiv in: $BACKUP_DIRS (samba-tool domain backup offline --targetdir=...)"
    return 0
  fi
  age_h=$(( ( $(date +%s) - ts ) / 3600 ))
  state=0; txt="OK"
  if   (( age_h >= BACKUP_CRIT_HOURS )); then state=2; txt="CRIT"
  elif (( age_h >= BACKUP_WARN_HOURS )); then state=1; txt="WARN"
  fi
  # Online- und Offline-Backups liegen teils in getrennten Verzeichnissen und
  # tragen unterschiedliche Namen - der Typ wird deshalb aus Pfad und Dateiname
  # abgeleitet und mit ausgegeben, statt ihn zu erraten.
  local kind="" base; base="$(basename "$file")"
  case "${file,,}" in
    *offline*) kind="offline" ;;
    *online*)  kind="online" ;;
  esac
  [[ -z "$kind" && "$file" == /var/univention-backup/* ]] && kind="Univention"

  # Zweitjuengstes Archiv aus einem ANDEREN Verzeichnis: zeigt, ob beide
  # Backup-Arten laufen oder nur eine.
  local others=""
  others="$(printf '%s\n' "${cand[@]}" | sort -rn \
            | awk -v d="$(dirname "$file")" '{ p=$2; sub(/\/[^\/]*$/, "", p); if (p != d) { print p; exit } }')"

  emit "$state" "Samba DC Backup" \
    "backup_age=$age_h;$BACKUP_WARN_HOURS;$BACKUP_CRIT_HOURS;0 backup_files=${#cand[@]}" \
    "$txt - juengstes Backup vor ${age_h}h: $base${kind:+ ($kind)}, ${#cand[@]} Archiv(e) insgesamt${others:+, weitere auch in $others}"
}


# Das Archivalter allein reicht nicht: ein frisches Backup von gestern sieht
# gut aus, auch wenn der Cronjob heute geloescht wurde. Der Job ist die
# Ursache, das Archivalter nur die Wirkung - deshalb getrennte Services.
check_backup_job() {
  local f="$BACKUP_CRON" owner perm mode jobs sched
  if is_univention; then
    emit 0 "Samba DC Backup Job" "backup_cron=1;;1;0;1" \
      "OK - Univention-System: Backups laufen ueber die UCS-eigene Verwaltung, kein $f erforderlich"
    return 0
  fi
  if [[ ! -f "$f" ]]; then
    emit 2 "Samba DC Backup Job" "backup_cron=0;;1;0;1" \
      "CRIT - $f fehlt - kein automatisches Backup eingerichtet"
    return 0
  fi

  owner="$(stat -c '%U' "$f" 2>/dev/null)"
  perm="$(stat -c '%a' "$f" 2>/dev/null)"
  mode=$(( 8#${perm:-0} ))

  # cron ueberspringt Dateien in /etc/cron.d stillschweigend, wenn sie nicht
  # root gehoeren oder fuer Gruppe/Andere schreibbar sind. Die Datei liegt
  # dann da und sieht richtig aus, der Job laeuft aber nie.
  if [[ "$owner" != "root" ]] || (( (mode & 0022) != 0 )); then
    emit 2 "Samba DC Backup Job" "backup_cron=0;;1;0;1" \
      "CRIT - $f wird von cron ignoriert: Eigentuemer $owner, Rechte $perm (root und nicht gruppen-/weltschreibbar erforderlich)"
    return 0
  fi

  # Aktive Zeilen sind weder Kommentar noch Env-Zuweisung (PATH=..., SHELL=...)
  jobs="$(grep -vE '^[[:space:]]*(#|$)' "$f" 2>/dev/null \
          | grep -vE '^[A-Za-z_][A-Za-z0-9_]*=' | grep -c .)"
  jobs="${jobs:-0}"

  if (( jobs == 0 )); then
    emit 1 "Samba DC Backup Job" "backup_cron=0;;1;0;1" \
      "WARN - $f vorhanden, aber keine aktive Zeile (alles auskommentiert?)"
    return 0
  fi

  sched="$(grep -vE '^[[:space:]]*(#|$)' "$f" 2>/dev/null \
           | grep -vE '^[A-Za-z_][A-Za-z0-9_]*=' | head -n1 | awk '{ print $1, $2, $3, $4, $5 }')"
  emit 0 "Samba DC Backup Job" "backup_cron=1;;1;0;1" \
    "OK - $f aktiv, $jobs Eintrag/Eintraege, Zeitplan: ${sched:-?}"
}

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

collect() {
  echo "<<<local:sep(0)>>>"

  if ! is_ad_dc; then
    emit 3 "Samba DC" "-" "UNKNOWN - kein Samba AD DC (server role passt nicht oder samba-tool fehlt)"
    return 0
  fi

  local realm nb
  realm="$(tp realm)"; nb="$(tp 'netbios name')"
  emit 0 "Samba DC Status" "-" "OK - Realm ${realm:-?}, NetBIOS ${nb:-?}, $(samba --version 2>/dev/null | head -n1)"

  local tmpdir
  tmpdir="$(mktemp -d)" || return 0
  trap 'rm -rf "$tmpdir"' RETURN

  pool_init || SEM_FD=""

  pool_run "$tmpdir/01.out" check_service
  pool_run "$tmpdir/02.out" check_config
  pool_run "$tmpdir/03.out" check_replication
  pool_run "$tmpdir/04.out" check_fsmo
  pool_run "$tmpdir/05.out" check_ldap
  pool_run "$tmpdir/06.out" check_ports
  pool_run "$tmpdir/07.out" check_dns
  pool_run "$tmpdir/08.out" check_time
  pool_run "$tmpdir/09.out" check_sysvol_acl
  pool_run "$tmpdir/10.out" check_dbcheck
  pool_run "$tmpdir/11.out" check_certs
  pool_run "$tmpdir/12.out" check_backup
  pool_run "$tmpdir/13.out" check_backup_job
  wait

  local n
  for n in "$tmpdir"/*.out; do
    [[ -s "$n" ]] && cat "$n"
  done
  return 0
}

########################### Main ###########################################

collect

exit 0