#!/usr/bin/env bash
#
# samba-dc-tls.sh - CA und TLS-Zertifikate fuer Samba AD DCs generisch neu bauen
#
# Auslegung: mehrere DCs teilen sich EINE CA. Die CA liegt auf DC1, die
# weiteren DCs bekommen ein Bundle (cert/key/ca) oder lassen einen CSR
# signieren. Alle Aenderungen werden gesichert; schlaegt der Health-Check
# nach dem Restart fehl, wird automatisch zurueckgerollt.
#
# Getestet mit Samba 4.17+ / OpenSSL 1.1.1+ / 3.x auf Debian und RHEL.
#
# Lizenz: MIT
#
set -euo pipefail
umask 077

VERSION="1.1"
PROG="$(basename "$0")"

# ---------------------------------------------------------------------------
# Defaults - per Umgebungsvariable oder Option ueberschreibbar
# ---------------------------------------------------------------------------
CA_DIR="${CA_DIR:-/var/lib/samba/private/tls-ca}"
CA_DAYS="${CA_DAYS:-3650}"          # CA-Laufzeit (10 Jahre)
CERT_DAYS="${CERT_DAYS:-825}"       # DC-Zertifikat; 825 = Apple/Chrome-Limit
KEY_BITS="${KEY_BITS:-4096}"        # RSA; breiteste Client-Kompatibilitaet
CA_CN="${CA_CN:-}"                  # Default: "<REALM> Samba AD CA"
ORG="${ORG:-}"                      # Default: Realm
EXTRA_DNS="${EXTRA_DNS:-}"          # zusaetzliche DNS-SANs, komma-/leerzeichensepariert
EXTRA_IP="${EXTRA_IP:-}"            # zusaetzliche IP-SANs
BACKUP_KEEP="${BACKUP_KEEP:-10}"    # wie viele Backup-Generationen behalten

DO_RESTART=1
DO_ROLLBACK=1
DRY_RUN=0
FORCE=0
ASSUME_YES=0

# ---------------------------------------------------------------------------
# Ausgabe-Helfer
# ---------------------------------------------------------------------------
if [[ -t 1 ]]; then
  C_R=$'\e[31m'; C_G=$'\e[32m'; C_Y=$'\e[33m'; C_B=$'\e[34m'; C_0=$'\e[0m'
else
  C_R=""; C_G=""; C_Y=""; C_B=""; C_0=""
fi
log()  { printf '%s[*]%s %s\n' "$C_B" "$C_0" "$*"; }
ok()   { printf '%s[+]%s %s\n' "$C_G" "$C_0" "$*"; }
warn() { printf '%s[!]%s %s\n' "$C_Y" "$C_0" "$*" >&2; }
die()  { printf '%s[x]%s %s\n' "$C_R" "$C_0" "$*" >&2; exit 1; }
run()  { if (( DRY_RUN )); then printf '    would run: %s\n' "$*"; else "$@"; fi; }

confirm() {
  (( ASSUME_YES )) && return 0
  local a; read -r -p "    $1 [j/N] " a || true
  [[ "$a" =~ ^([jJ]|[yY])$ ]]
}

need_root() { [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausfuehren."; }

need_bin() {
  local m=()
  for b in "$@"; do command -v "$b" >/dev/null 2>&1 || m+=("$b"); done
  ((${#m[@]})) && die "Fehlende Programme: ${m[*]}"
  return 0
}

# ---------------------------------------------------------------------------
# Samba-Umgebung einlesen
# ---------------------------------------------------------------------------
tp() { testparm -s --parameter-name="$1" 2>/dev/null | tail -n1 | tr -d '\r'; }

abspath_in_private() {
  local p="$1"
  [[ "$p" == /* ]] && { printf '%s' "$p"; return; }
  printf '%s/%s' "$PRIVATE_DIR" "$p"
}

detect_env() {
  need_bin testparm openssl

  SERVER_ROLE="$(tp 'server role')"
  [[ "$SERVER_ROLE" == *"domain controller"* ]] || \
    warn "server role = '${SERVER_ROLE:-?}' - das sieht nicht nach einem AD DC aus."

  REALM="$(tp realm)"
  [[ -n "$REALM" ]] || die "Kein 'realm' in der smb.conf gefunden."
  REALM_LC="${REALM,,}"
  NETBIOS="$(tp 'netbios name')"
  PRIVATE_DIR="$(tp 'private dir')"
  PRIVATE_DIR="${PRIVATE_DIR:-/var/lib/samba/private}"

  TLS_ENABLED="$(tp 'tls enabled')"
  TLS_CERT="$(abspath_in_private "$(tp 'tls certfile')")"
  TLS_KEY="$(abspath_in_private "$(tp 'tls keyfile')")"
  TLS_CA="$(abspath_in_private "$(tp 'tls cafile')")"
  TLS_DIR="$(dirname "$TLS_CERT")"

  # FQDN ermitteln - hostname -f kann bei kaputtem DNS luegen
  local short; short="$(hostname -s 2>/dev/null || true)"
  FQDN="$(hostname -f 2>/dev/null || true)"
  if [[ -z "$FQDN" || "$FQDN" != *.* || "${FQDN,,}" != *"$REALM_LC" ]]; then
    FQDN="${short,,}.${REALM_LC}"
  fi
  FQDN="${FQDN,,}"
  SHORTNAME="${short,,}"

  ORG="${ORG:-$REALM}"
  CA_CN="${CA_CN:-$REALM Samba AD CA}"

  CA_KEY="$CA_DIR/ca-key.pem"
  CA_CERT="$CA_DIR/ca-cert.pem"
  CA_SRL="$CA_DIR/ca.srl"
  STATE_FILE="$TLS_DIR/.samba-dc-tls.state"
}

# NTDS-DSA-GUID fuer den <guid>._msdcs.<realm> SAN
detect_dsa_guid() {
  local samdb="$PRIVATE_DIR/sam.ldb" dsdn guid
  DSA_GUID=""
  command -v ldbsearch >/dev/null 2>&1 || return 0
  [[ -r "$samdb" ]] || return 0
  dsdn="$(ldbsearch -H "$samdb" -s base -b '' dsServiceName 2>/dev/null \
          | sed -n 's/^dsServiceName: //p' | head -n1)" || true
  [[ -n "$dsdn" ]] || return 0
  guid="$(ldbsearch -H "$samdb" -s base -b "$dsdn" objectGUID 2>/dev/null \
          | sed -n 's/^objectGUID: //p' | head -n1)" || true
  [[ "$guid" =~ ^[0-9a-fA-F-]{36}$ ]] && DSA_GUID="${guid,,}"
  return 0
}

print_env() {
  cat <<EOF
    Realm .......... $REALM
    NetBIOS ........ ${NETBIOS:-?}
    FQDN ........... $FQDN
    private dir .... $PRIVATE_DIR
    tls enabled .... ${TLS_ENABLED:-?}
    tls certfile ... $TLS_CERT
    tls keyfile .... $TLS_KEY
    tls cafile ..... $TLS_CA
    CA-Verzeichnis . $CA_DIR
EOF
  [[ -n "${DSA_GUID:-}" ]] && printf '    DSA-GUID ....... %s\n' "$DSA_GUID"
  if [[ "${TLS_ENABLED,,}" == "no" ]]; then
    warn "'tls enabled = no' - Samba nutzt die Zertifikate dann nicht."
    warn "In der [global]-Sektion setzen: tls enabled = yes"
  fi
}

# ---------------------------------------------------------------------------
# OpenSSL-Konfiguration erzeugen
# ---------------------------------------------------------------------------
# $1 = CN/FQDN, $2 = Zielpfad der Config, $3 = optionale DSA-GUID
write_cert_conf() {
  local cn="$1" out="$2" guid="${3:-}"
  local -a dns=() ips=()
  local n

  dns+=("$cn")
  [[ "$cn" == *.* ]] && dns+=("${cn%%.*}")
  dns+=("$REALM_LC")
  [[ -n "$guid" ]] && dns+=("${guid}._msdcs.${REALM_LC}")
  for n in ${EXTRA_DNS//,/ }; do [[ -n "$n" ]] && dns+=("${n,,}"); done
  for n in ${EXTRA_IP//,/ }; do [[ -n "$n" ]] && ips+=("$n"); done

  # Duplikate entfernen, Reihenfolge erhalten
  local -a udns=(); local -A seen=()
  for n in "${dns[@]}"; do [[ -n "${seen[$n]:-}" ]] || { udns+=("$n"); seen[$n]=1; }; done

  {
    cat <<EOF
[req]
default_md         = sha256
prompt             = no
distinguished_name = dn
req_extensions     = v3_req

[dn]
O  = $ORG
CN = $cn

[v3_req]
basicConstraints       = critical,CA:FALSE
keyUsage               = critical,digitalSignature,keyEncipherment
extendedKeyUsage       = serverAuth,clientAuth
subjectAltName         = @alt

[v3_sign]
basicConstraints       = critical,CA:FALSE
keyUsage               = critical,digitalSignature,keyEncipherment
extendedKeyUsage       = serverAuth,clientAuth
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
subjectAltName         = @alt

[alt]
EOF
    local i=1
    for n in "${udns[@]}"; do printf 'DNS.%d = %s\n' "$i" "$n"; ((i++)); done
    i=1
    for n in "${ips[@]}"; do printf 'IP.%d = %s\n' "$i" "$n"; ((i++)); done
  } >"$out"

  log "SANs: ${udns[*]} ${ips[*]:-}"
}

write_ca_conf() {
  cat >"$1" <<EOF
[req]
default_md         = sha256
prompt             = no
distinguished_name = dn
x509_extensions    = v3_ca

[dn]
O  = $ORG
CN = $CA_CN

[v3_ca]
basicConstraints     = critical,CA:TRUE,pathlen:0
keyUsage             = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
EOF
}

# ---------------------------------------------------------------------------
# CA
# ---------------------------------------------------------------------------
cmd_init_ca() {
  need_root
  if [[ -f "$CA_KEY" && -f "$CA_CERT" ]] && (( ! FORCE )); then
    log "CA existiert bereits: $CA_CERT"
    openssl x509 -in "$CA_CERT" -noout -subject -enddate | sed 's/^/    /'
    return 0
  fi
  (( FORCE )) && [[ -f "$CA_CERT" ]] && warn "--force: bestehende CA wird ersetzt!"

  log "Erzeuge CA in $CA_DIR"
  run install -d -m 0700 -o root -g root "$CA_DIR"
  (( DRY_RUN )) && return 0

  local conf; conf="$(mktemp)"; write_ca_conf "$conf"
  openssl req -x509 -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \
    -days "$CA_DAYS" -keyout "$CA_KEY.new" -out "$CA_CERT.new" -config "$conf"
  rm -f "$conf"
  mv "$CA_KEY.new" "$CA_KEY"; chmod 0600 "$CA_KEY"
  mv "$CA_CERT.new" "$CA_CERT"; chmod 0644 "$CA_CERT"
  openssl rand -hex 16 >"$CA_SRL"
  ok "CA erstellt (gueltig $CA_DAYS Tage)."
  openssl x509 -in "$CA_CERT" -noout -subject -enddate | sed 's/^/    /'
}

require_ca() {
  [[ -f "$CA_KEY" && -f "$CA_CERT" ]] || \
    die "Keine CA unter $CA_DIR. Erst '$PROG init-ca' ausfuehren (auf dem CA-DC)."
}

# ---------------------------------------------------------------------------
# Zertifikat erzeugen / signieren
# ---------------------------------------------------------------------------
# $1 = FQDN, $2 = Ausgabeverzeichnis, $3 = optionale DSA-GUID
make_cert() {
  local cn="$1" dir="$2" guid="${3:-}"
  local conf="$dir/openssl.cnf"
  install -d -m 0700 "$dir"
  write_cert_conf "$cn" "$conf" "$guid"

  openssl req -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \
    -keyout "$dir/key.pem" -out "$dir/csr.pem" -config "$conf"
  chmod 0600 "$dir/key.pem"

  [[ -s "$CA_SRL" ]] || openssl rand -hex 16 >"$CA_SRL"
  openssl x509 -req -in "$dir/csr.pem" -sha256 -days "$CERT_DAYS" \
    -CA "$CA_CERT" -CAkey "$CA_KEY" -CAserial "$CA_SRL" \
    -extfile "$conf" -extensions v3_sign -out "$dir/cert.pem" 2>/dev/null
  chmod 0644 "$dir/cert.pem"
  cp -f "$CA_CERT" "$dir/ca.pem"; chmod 0644 "$dir/ca.pem"
  rm -f "$dir/csr.pem"
}

# ---------------------------------------------------------------------------
# Pruefungen
# ---------------------------------------------------------------------------
check_pair() {
  local cert="$1" key="$2" ca="$3" fqdn="${4:-}"
  local a b

  a="$(openssl x509 -in "$cert" -noout -pubkey | openssl pkey -pubin -outform DER 2>/dev/null | openssl dgst -sha256 -r | cut -d' ' -f1)"
  b="$(openssl pkey -in "$key" -pubout -outform DER 2>/dev/null | openssl dgst -sha256 -r | cut -d' ' -f1)"
  [[ -n "$a" && "$a" == "$b" ]] || die "Key und Zertifikat passen nicht zusammen: $cert / $key"

  openssl verify -CAfile "$ca" "$cert" >/dev/null 2>&1 \
    || die "Zertifikat wird von der CA nicht verifiziert: $cert"

  openssl x509 -in "$cert" -noout -ext extendedKeyUsage 2>/dev/null | grep -q 'TLS Web Server Authentication' \
    || die "Zertifikat hat kein serverAuth-EKU."

  if [[ -n "$fqdn" ]]; then
    openssl x509 -in "$cert" -noout -ext subjectAltName 2>/dev/null | grep -qi "DNS:$fqdn" \
      || die "SAN enthaelt den FQDN '$fqdn' nicht."
  fi
  ok "Zertifikat, Key und CA sind konsistent."
}

health_check() {
  local host="${1:-$FQDN}" rc=0
  log "Health-Check gegen ldaps://$host:636"
  if ! timeout 15 openssl s_client -connect "127.0.0.1:636" -servername "$host" \
        -CAfile "$TLS_CA" -verify_return_error </dev/null >/dev/null 2>&1; then
    warn "LDAPS-Handshake auf 127.0.0.1:636 fehlgeschlagen."
    rc=1
  else
    ok "LDAPS-Handshake ok (Kette gegen eigene CA verifiziert)."
  fi

  if command -v ldbsearch >/dev/null 2>&1; then
    if ldbsearch -H "ldaps://$host" -k yes -s base -b '' defaultNamingContext >/dev/null 2>&1 \
       || ldbsearch -H "ldap://$host" -k yes -s base -b '' defaultNamingContext >/dev/null 2>&1; then
      ok "LDAP-Abfrage gegen $host beantwortet."
    else
      warn "LDAP-Abfrage gegen $host fehlgeschlagen (evtl. kein Kerberos-Ticket - nicht zwingend ein Fehler)."
    fi
  fi

  if command -v samba-tool >/dev/null 2>&1; then
    if samba-tool drs showrepl >/tmp/.drs.$$ 2>&1; then
      if grep -qiE 'consecutive failure|WERR_|failed' /tmp/.drs.$$; then
        warn "drs showrepl meldet Fehler:"; sed 's/^/    /' /tmp/.drs.$$ | head -n 30
        rc=1
      else
        ok "Replikation (drs showrepl) ohne Fehler."
      fi
    else
      warn "samba-tool drs showrepl fehlgeschlagen."; rc=1
    fi
    rm -f /tmp/.drs.$$
  fi
  return $rc
}

# ---------------------------------------------------------------------------
# Dienststeuerung
# ---------------------------------------------------------------------------
detect_unit() {
  local u
  for u in samba-ad-dc samba sernet-samba-ad; do
    if systemctl cat "$u.service" >/dev/null 2>&1; then
      if systemctl is-enabled "$u.service" >/dev/null 2>&1 || systemctl is-active "$u.service" >/dev/null 2>&1; then
        printf '%s' "$u.service"; return 0
      fi
    fi
  done
  return 1
}

restart_samba() {
  local unit
  if ! unit="$(detect_unit)"; then
    warn "Keine aktive Samba-AD-Unit gefunden - bitte manuell neu starten."
    return 0
  fi
  log "Starte $unit neu ..."
  run systemctl restart "$unit"
  (( DRY_RUN )) && return 0
  sleep 4
  systemctl is-active --quiet "$unit" || die "$unit ist nach dem Restart nicht aktiv!"
  ok "$unit laeuft."
}

# ---------------------------------------------------------------------------
# Installation mit Backup / Rollback
# ---------------------------------------------------------------------------
backup_current() {
  local ts bdir
  ts="$(date +%Y%m%d-%H%M%S)"
  bdir="$TLS_DIR/backup-$ts"
  install -d -m 0700 "$bdir"
  local f
  for f in "$TLS_CERT" "$TLS_KEY" "$TLS_CA"; do
    [[ -f "$f" ]] && cp -a "$f" "$bdir/"
  done
  printf '%s\n' "$bdir" >"$STATE_FILE"; chmod 0600 "$STATE_FILE"
  ok "Backup: $bdir"

  # alte Generationen aufraeumen
  local -a old
  mapfile -t old < <(ls -1d "$TLS_DIR"/backup-* 2>/dev/null | sort)
  if (( ${#old[@]} > BACKUP_KEEP )); then
    local del=$(( ${#old[@]} - BACKUP_KEEP )) i
    for ((i=0; i<del; i++)); do rm -rf "${old[$i]}"; done
  fi
  printf '%s' "$bdir"
}

install_bundle() {
  local src="$1"   # Verzeichnis mit cert.pem, key.pem, ca.pem
  check_pair "$src/cert.pem" "$src/key.pem" "$src/ca.pem" "$FQDN"

  log "Aktuelle Zertifikate:"
  if [[ -f "$TLS_CERT" ]]; then
    openssl x509 -in "$TLS_CERT" -noout -subject -enddate 2>/dev/null | sed 's/^/    alt: /' || true
  else
    printf '    alt: (keine vorhanden)\n'
  fi
  openssl x509 -in "$src/cert.pem" -noout -subject -enddate | sed 's/^/    neu: /'

  if ! confirm "Zertifikate installieren und Samba neu starten?"; then
    die "Abgebrochen."
  fi

  local bdir; bdir="$(backup_current)"

  install -d -m 0700 -o root -g root "$TLS_DIR"
  install -m 0600 -o root -g root "$src/key.pem"  "$TLS_KEY.new"
  install -m 0644 -o root -g root "$src/cert.pem" "$TLS_CERT.new"
  install -m 0644 -o root -g root "$src/ca.pem"   "$TLS_CA.new"
  mv -f "$TLS_KEY.new"  "$TLS_KEY"
  mv -f "$TLS_CERT.new" "$TLS_CERT"
  mv -f "$TLS_CA.new"   "$TLS_CA"
  command -v restorecon >/dev/null 2>&1 && restorecon -R "$TLS_DIR" >/dev/null 2>&1 || true
  ok "Dateien installiert."

  restart_samba

  if ! health_check "$FQDN"; then
    if (( DO_ROLLBACK )); then
      warn "Health-Check fehlgeschlagen - rolle zurueck auf $bdir"
      restore_backup "$bdir"
      restart_samba
      health_check "$FQDN" || warn "Auch nach Rollback nicht sauber - bitte manuell pruefen!"
      die "Rollback durchgefuehrt. Zertifikate wurden NICHT geaendert."
    fi
    warn "Health-Check fehlgeschlagen (Rollback deaktiviert)."
    return 1
  fi
  ok "Fertig. Neues Zertifikat ist aktiv."
}

restore_backup() {
  local bdir="$1"
  [[ -d "$bdir" ]] || die "Backup-Verzeichnis nicht gefunden: $bdir"
  [[ -f "$bdir/$(basename "$TLS_KEY")" ]]  && install -m 0600 "$bdir/$(basename "$TLS_KEY")"  "$TLS_KEY"
  [[ -f "$bdir/$(basename "$TLS_CERT")" ]] && install -m 0644 "$bdir/$(basename "$TLS_CERT")" "$TLS_CERT"
  [[ -f "$bdir/$(basename "$TLS_CA")" ]]   && install -m 0644 "$bdir/$(basename "$TLS_CA")"   "$TLS_CA"
  ok "Backup zurueckgespielt: $bdir"
}

# ---------------------------------------------------------------------------
# Kommandos
# ---------------------------------------------------------------------------
cmd_renew() {
  need_root; require_ca; detect_dsa_guid
  local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
  log "Erzeuge Zertifikat fuer $FQDN"
  (( DRY_RUN )) && { log "(dry-run) kein Schluesselmaterial erzeugt"; return 0; }
  make_cert "$FQDN" "$tmp" "${DSA_GUID:-}"
  install_bundle "$tmp"
}

cmd_issue_for() {
  need_root; require_ca
  local host="${1,,}"; shift || true
  local guid="${1:-}"
  [[ -n "$host" ]] || die "Verwendung: $PROG issue-for <fqdn.des.dc> [dsa-guid]"
  [[ "$host" == *.* ]] || die "Bitte den vollstaendigen FQDN angeben."

  local out="${OUT_DIR:-/root/samba-tls-$host}"
  log "Erzeuge Bundle fuer $host in $out"
  (( DRY_RUN )) && return 0
  rm -rf "$out"
  make_cert "$host" "$out" "$guid"
  check_pair "$out/cert.pem" "$out/key.pem" "$out/ca.pem" "$host"

  local tgz="/root/samba-tls-$host.tar.gz"
  tar -czf "$tgz" -C "$out" cert.pem key.pem ca.pem
  chmod 0600 "$tgz"
  ok "Bundle: $tgz"
  cat <<EOF

    Uebertragen und einspielen:
      scp $tgz root@$host:/root/
      ssh root@$host '$PROG install /root/$(basename "$tgz")'
EOF
}

cmd_request() {
  need_root; detect_dsa_guid
  local out="${OUT_DIR:-/root/samba-tls-request}"
  install -d -m 0700 "$out"
  local conf="$out/openssl.cnf"
  write_cert_conf "$FQDN" "$conf" "${DSA_GUID:-}"
  (( DRY_RUN )) && return 0
  openssl req -new -nodes -newkey "rsa:$KEY_BITS" -sha256 \
    -keyout "$out/key.pem" -out "$out/csr.pem" -config "$conf"
  chmod 0600 "$out/key.pem"
  ok "CSR: $out/csr.pem   (Key bleibt hier: $out/key.pem)"
  cat <<EOF

    Auf dem CA-DC signieren lassen:
      scp $out/csr.pem root@<ca-dc>:/tmp/
      ssh root@<ca-dc> '$PROG sign /tmp/csr.pem $FQDN'
    Danach cert.pem + ca.pem zurueckkopieren und:
      $PROG install-files $out/cert.pem $out/key.pem $out/ca.pem
EOF
}

cmd_sign() {
  need_root; require_ca
  local csr="$1" cn="${2:-}"
  [[ -f "$csr" ]] || die "CSR nicht gefunden: $csr"
  [[ -n "$cn" ]] || cn="$(openssl req -in "$csr" -noout -subject | sed -n 's/.*CN *= *\([^,/]*\).*/\1/p')"
  [[ -n "$cn" ]] || die "CN konnte nicht ermittelt werden - bitte als 2. Argument angeben."
  local out="${OUT_DIR:-/root/samba-tls-signed-$cn}"
  install -d -m 0700 "$out"
  local conf="$out/openssl.cnf"
  write_cert_conf "${cn,,}" "$conf" "${DSA_GUID:-}"
  (( DRY_RUN )) && return 0
  [[ -s "$CA_SRL" ]] || openssl rand -hex 16 >"$CA_SRL"
  openssl x509 -req -in "$csr" -sha256 -days "$CERT_DAYS" \
    -CA "$CA_CERT" -CAkey "$CA_KEY" -CAserial "$CA_SRL" \
    -extfile "$conf" -extensions v3_sign -out "$out/cert.pem" 2>/dev/null
  cp -f "$CA_CERT" "$out/ca.pem"
  chmod 0644 "$out/cert.pem" "$out/ca.pem"
  ok "Signiert: $out/cert.pem (+ ca.pem)"
}

cmd_install() {
  need_root
  local arg="$1"
  local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
  if [[ -d "$arg" ]]; then
    cp -f "$arg/cert.pem" "$arg/key.pem" "$arg/ca.pem" "$tmp/"
  elif [[ -f "$arg" ]]; then
    tar -xzf "$arg" -C "$tmp"
  else
    die "Bundle nicht gefunden: $arg"
  fi
  chmod 0600 "$tmp/key.pem"
  install_bundle "$tmp"
}

cmd_install_files() {
  need_root
  local c="$1" k="$2" a="$3"
  local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN
  install -m 0644 "$c" "$tmp/cert.pem"
  install -m 0600 "$k" "$tmp/key.pem"
  install -m 0644 "$a" "$tmp/ca.pem"
  install_bundle "$tmp"
}

cmd_status() {
  print_env
  echo
  local f
  for f in "$CA_CERT:CA" "$TLS_CERT:DC-Zertifikat"; do
    local p="${f%%:*}" n="${f##*:}"
    if [[ -f "$p" ]]; then
      printf '  %s (%s)\n' "$n" "$p"
      openssl x509 -in "$p" -noout -subject -issuer -startdate -enddate 2>/dev/null | sed 's/^/    /'
      openssl x509 -in "$p" -noout -ext subjectAltName 2>/dev/null | sed -n '2p' | sed 's/^ */    SAN: /'
      if ! openssl x509 -in "$p" -noout -checkend $((30*86400)) >/dev/null 2>&1; then
        warn "  laeuft in weniger als 30 Tagen ab!"
      fi
      echo
    else
      warn "$n fehlt: $p"
    fi
  done
  [[ -f "$STATE_FILE" ]] && printf '  letztes Backup: %s\n' "$(cat "$STATE_FILE")"
  return 0
}

cmd_verify() {
  need_root
  [[ -f "$TLS_CERT" && -f "$TLS_KEY" && -f "$TLS_CA" ]] || die "Zertifikatsdateien unvollstaendig."
  check_pair "$TLS_CERT" "$TLS_KEY" "$TLS_CA" "$FQDN"
  health_check "$FQDN"
}

cmd_rollback() {
  need_root
  local bdir="${1:-}"
  if [[ -z "$bdir" ]]; then
    [[ -f "$STATE_FILE" ]] || die "Kein Backup-Zeiger gefunden. Pfad bitte angeben."
    bdir="$(cat "$STATE_FILE")"
  fi
  confirm "Auf $bdir zurueckrollen?" || die "Abgebrochen."
  restore_backup "$bdir"
  restart_samba
  health_check "$FQDN" || warn "Health-Check nach Rollback nicht sauber."
}

cmd_export_ca() {
  require_ca
  local out="${1:-/root/$(printf '%s' "$REALM_LC" | tr '.' '-')-ca.crt}"
  cp -f "$CA_CERT" "$out"; chmod 0644 "$out"
  ok "CA-Zertifikat exportiert: $out"
  cat <<EOF

    Verteilung an Clients:
      Windows : per GPO nach 'Vertrauenswuerdige Stammzertifizierungsstellen'
      Debian  : cp $out /usr/local/share/ca-certificates/ && update-ca-certificates
      RHEL    : cp $out /etc/pki/ca-trust/source/anchors/ && update-ca-trust
    Nicht vergessen: sssd/realmd, Keycloak, Nextcloud, Zammad, Backup-Tools,
    Drucker, NAS - alles was per LDAPS/StartTLS an die DCs geht.
EOF
}

usage() {
  cat <<EOF
$PROG $VERSION - CA und TLS-Zertifikate fuer Samba AD DCs neu bauen

Verwendung: $PROG [Optionen] <Kommando> [Argumente]

Kommandos:
  status                       Umgebung und aktuelle Zertifikate anzeigen
  init-ca                      Neue CA anlegen (einmalig, auf DC1)
  renew                        Zertifikat fuer DIESEN DC neu bauen + installieren
  issue-for <fqdn> [dsa-guid]  Bundle fuer einen anderen DC erzeugen (auf dem CA-DC)
  install <bundle.tar.gz|dir>  Bundle auf diesem DC installieren
  request                      Key + CSR lokal erzeugen (CA-Key bleibt auf DC1)
  sign <csr> [cn]              CSR auf dem CA-DC signieren
  install-files <cert> <key> <ca>
  verify                       Konsistenz- und Health-Check
  rollback [backup-dir]        Letzten Stand zurueckspielen
  export-ca [datei]            CA-Zertifikat zur Client-Verteilung exportieren

Optionen:
  --ca-dir DIR        CA-Verzeichnis (Default: $CA_DIR)
  --days N            Laufzeit DC-Zertifikat (Default: $CERT_DAYS)
  --ca-days N         Laufzeit CA (Default: $CA_DAYS)
  --bits N            RSA-Schluessellaenge (Default: $KEY_BITS)
  --dns a,b,c         zusaetzliche DNS-SANs (z.B. Alias-Namen, ldap.firma.de)
  --ip 10.0.0.1,...   zusaetzliche IP-SANs
  --out DIR           Ausgabeverzeichnis fuer issue-for/request/sign
  --no-restart        Samba nicht neu starten
  --no-rollback       kein automatischer Rollback bei fehlgeschlagenem Check
  --yes               keine Rueckfragen
  --force             bestehende CA ueberschreiben
  --dry-run           nur anzeigen, nichts aendern
  -h, --help          diese Hilfe

Ablauf bei zwei DCs (empfohlen):
  DC1:  $PROG init-ca
        $PROG renew
        $PROG issue-for dc2.${REALM_LC:-example.local}
        scp /root/samba-tls-dc2.${REALM_LC:-example.local}.tar.gz root@dc2:/root/
  DC2:  $PROG install /root/samba-tls-dc2.${REALM_LC:-example.local}.tar.gz
  DC1:  $PROG export-ca      # CA an alle Clients verteilen

Immer nur EINEN DC nach dem anderen anfassen und dazwischen
'samba-tool drs showrepl' auf beiden Seiten pruefen.
EOF
}

# ---------------------------------------------------------------------------
# Argumente
# ---------------------------------------------------------------------------
OUT_DIR="${OUT_DIR:-}"
ARGS=()
while (( $# )); do
  case "$1" in
    --ca-dir)      CA_DIR="$2"; shift 2 ;;
    --days)        CERT_DAYS="$2"; shift 2 ;;
    --ca-days)     CA_DAYS="$2"; shift 2 ;;
    --bits)        KEY_BITS="$2"; shift 2 ;;
    --dns)         EXTRA_DNS="$2"; shift 2 ;;
    --ip)          EXTRA_IP="$2"; shift 2 ;;
    --out)         OUT_DIR="$2"; shift 2 ;;
    --ca-cn)       CA_CN="$2"; shift 2 ;;
    --org)         ORG="$2"; shift 2 ;;
    --no-restart)  DO_RESTART=0; shift ;;
    --no-rollback) DO_ROLLBACK=0; shift ;;
    --yes|-y)      ASSUME_YES=1; shift ;;
    --force)       FORCE=1; shift ;;
    --dry-run)     DRY_RUN=1; shift ;;
    -h|--help)     usage; exit 0 ;;
    --) shift; ARGS+=("$@"); break ;;
    -*) die "Unbekannte Option: $1" ;;
    *)  ARGS+=("$1"); shift ;;
  esac
done

if (( ! DO_RESTART )); then
  restart_samba() {
    warn "Restart uebersprungen (--no-restart) - das Zertifikat wird erst nach einem Neustart aktiv."
  }
fi

CMD="${ARGS[0]:-status}"
detect_env

case "$CMD" in
  status)        cmd_status ;;
  init-ca)       cmd_init_ca ;;
  renew)         cmd_init_ca; cmd_renew ;;
  issue-for)     cmd_issue_for "${ARGS[1]:-}" "${ARGS[2]:-}" ;;
  install)       cmd_install "${ARGS[1]:?Bundle angeben}" ;;
  install-files) cmd_install_files "${ARGS[1]:?cert}" "${ARGS[2]:?key}" "${ARGS[3]:?ca}" ;;
  request)       cmd_request ;;
  sign)          cmd_sign "${ARGS[1]:?CSR angeben}" "${ARGS[2]:-}" ;;
  verify)        cmd_verify ;;
  rollback)      cmd_rollback "${ARGS[1]:-}" ;;
  export-ca)     cmd_export_ca "${ARGS[1]:-}" ;;
  help)          usage ;;
  *)             usage; die "Unbekanntes Kommando: $CMD" ;;
esac
