#!/bin/bash
# php-set-ini-v2.sh - setzt php.ini-Direktiven (optimierte Version)
# Standard: DRY-RUN. Aenderungen nur mit --apply.
set -o pipefail

usage() {
  cat <<USAGE
Aufruf: $0 [-n|--dry-run] [-a|--apply] [-s|--sapi SAPI]... [-h|--help]
  -n, --dry-run   nur anzeigen, was passieren wuerde (Standard)
  -a, --apply     Aenderungen schreiben und nur betroffene Dienste graceful neu laden
  -s, --sapi X    nur diese SAPI bearbeiten (fpm, cli, apache2, cgi) - mehrfach moeglich
USAGE
}

DRYRUN=1
SAPIS=()
while [ $# -gt 0 ]; do
  case "$1" in
    -a|--apply)   DRYRUN=0 ;;
    -n|--dry-run) DRYRUN=1 ;;
    -s|--sapi)    [ $# -ge 2 ] || { echo "Fehlender Wert fuer $1"; exit 1; }; SAPIS+=("$2"); shift ;;
    -h|--help)    usage; exit 0 ;;
    *) echo "Unbekannte Option: $1"; echo; usage; exit 1 ;;
  esac
  shift
done

# ---- Hier die gewuenschten Direktiven eintragen: "schluessel=wert" ----
SETTINGS=(
  "session.upload_progress.enabled=Off"
)
# -----------------------------------------------------------------------

log() { echo "[$(date '+%F %T')] $*"; }

# Farben nur bei Ausgabe auf ein Terminal (in Logs/Pipes ohne Steuerzeichen)
if [ -t 1 ]; then C_RED=$'\e[1;31m'; C_YEL=$'\e[1;33m'; C_GRN=$'\e[1;32m'; C_OFF=$'\e[0m'
else C_RED=""; C_YEL=""; C_GRN=""; C_OFF=""; fi
ERRORS=0; ERROR_LIST=(); NOT_RELOADED=(); RELOADED=()
# Fehler melden: Zeile beginnt mit ERROR, wird gezaehlt und am Ende zusammengefasst
err() { log "${C_RED}ERROR: $*${C_OFF}"; ERRORS=$((ERRORS+1)); ERROR_LIST+=("$*"); }
run() { if [ "$DRYRUN" -eq 1 ]; then echo "    DRY-RUN: $*"; else "$@"; fi; }

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

exec 9>/run/php-set-ini.lock
flock -n 9 || { echo "Skript laeuft bereits - Abbruch."; exit 1; }

if [ "$DRYRUN" -eq 1 ]; then
  log "MODUS: DRY-RUN - es wird NICHTS geaendert. Zum Ausfuehren: $0 --apply"
else
  log "MODUS: APPLY - Dateien werden geaendert, betroffene Dienste graceful neu geladen."
fi

HAS_SYSTEMD=0
[ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1 && HAS_SYSTEMD=1

svc_active() {
  if [ "$HAS_SYSTEMD" -eq 1 ]; then systemctl is-active --quiet "$1"
  else service "$1" status >/dev/null 2>&1; fi
}
svc_reload() {
  if [ "$HAS_SYSTEMD" -eq 1 ]; then run systemctl reload "$1"
  else run service "$1" reload; fi
}

sapi_selected() {
  [ "${#SAPIS[@]}" -eq 0 ] && return 0
  local s
  for s in "${SAPIS[@]}"; do [ "$s" = "$1" ] && return 0; done
  return 1
}

# Setzt SAPI, VER, FPM_SVC, FPM_BIN, CLI_BIN fuer eine php.ini
ini_info() {
  local dir; dir=$(dirname "$1")
  SAPI=$(basename "$dir")
  case "$1" in
    /etc/php5/*) VER=5; FPM_SVC=php5-fpm; FPM_BIN=php5-fpm; CLI_BIN=php5 ;;
    /etc/php/*)  VER=$(basename "$(dirname "$dir")")
                 FPM_SVC="php${VER}-fpm"; FPM_BIN="php-fpm${VER}"; CLI_BIN="php${VER}" ;;
  esac
}

# Erzeugt in $2 die neue Fassung von $1. Schluessel werden WOERTLICH verglichen (keine Regex).
# Regeln: 1. aktive Zeile setzen, weitere aktive Duplikate auskommentieren;
#         keine aktive -> erste auskommentierte ersetzen; gar keine -> anhaengen.
build_new() {
  local src="$1" dst="$2" s
  cp "$src" "$dst" || return 1
  for s in "${SETTINGS[@]}"; do
    K="${s%%=*}" V="${s#*=}" awk '
      function trim(x){ sub(/^[ \t]+/,"",x); sub(/[ \t]+$/,"",x); return x }
      function norm(x){ x=tolower(x); gsub(/"/,"",x); if (x~/^(0|off|false|no|none)?$/) return "0"; if (x~/^(1|on|true|yes)$/) return "1"; return "?" x }
      function same(a,b){ return a==b || (norm(a)~/^[01]$/ && norm(a)==norm(b)) }
      function parse(line,   t,p){
        t=line; sub(/^[ \t]+/,"",t); C=0
        if (substr(t,1,1)==";") { C=1; t=substr(t,2) }
        p=index(t,"="); if (p==0) return 0
        VP=trim(substr(t,p+1))
        return trim(substr(t,1,p-1))==ENVIRON["K"]
      }
      NR==FNR { if (parse($0) && !C) act++; next }
      {
        if (parse($0)) {
          if (!C) {
            if (!done) { done=1; if (same(VP,ENVIRON["V"])) print; else print ENVIRON["K"] " = " ENVIRON["V"] }
            else print "; " $0 "   ; Duplikat deaktiviert durch php-set-ini-v2.sh"
            next
          }
          if (act==0 && !done) { done=1; print ENVIRON["K"] " = " ENVIRON["V"]; next }
        }
        print
      }
      END { if (!done) printf "\n; gesetzt durch php-set-ini-v2.sh\n%s = %s\n", ENVIRON["K"], ENVIRON["V"] }
    ' "$dst" "$dst" > "$dst.new" && mv "$dst.new" "$dst" || return 1
  done
}

# Warnt vor Stellen, die die php.ini uebersteuern
check_overrides() {
  local ini="$1" dir s k f
  dir=$(dirname "$ini")
  for s in "${SETTINGS[@]}"; do
    k="${s%%=*}"
    for f in "$dir"/conf.d/*.ini; do
      [ -f "$f" ] || continue
      if K="$k" awk '{ t=$0; sub(/^[ \t]+/,"",t); p=index(t,"=")
             if (p && substr(t,1,1)!=";") { x=substr(t,1,p-1); gsub(/[ \t]/,"",x); if (x==ENVIRON["K"]) { f=1; exit } } }
             END { exit !f }' "$f"; then
        log "    WARNUNG: $k wird ueberschrieben in $f"
      fi
    done
    if [ "$SAPI" = "fpm" ]; then
      for f in "$dir"/pool.d/*.conf; do
        [ -f "$f" ] || continue
        if grep -v '^[[:space:]]*;' "$f" | grep -qF "_value[$k]"; then
          log "    WARNUNG: $k wird im FPM-Pool ueberschrieben: $f"
        fi
      done
    fi
    if [ "$SAPI" = "apache2" ] && [ -d /etc/apache2 ]; then
      f=$(grep -rlsF "$k" /etc/apache2 | xargs -r grep -lsE "php_(admin_)?(value|flag)")
      [ -n "$f" ] && log "    WARNUNG: moeglicher Override von $k in Apache-Konfig: $(echo $f)"
    fi
  done
}

# Normalisiert Werte fuer den Vergleich (Boolean nach Bedeutung)
norm_val() {
  local x="${1,,}"
  x="${x//\"/}"
  x="${x#"${x%%[![:space:]]*}"}"; x="${x%"${x##*[![:space:]]}"}"
  case "$x" in
    ""|0|off|false|no|none|"no value") echo 0 ;;
    1|on|true|yes) echo 1 ;;
    *) echo "?$x" ;;
  esac
}

# Prueft nach dem Reload per php-fpmX.Y -i, ob die Sollwerte wirksam sind.
# Sicht des FPM-Binaries (php.ini + conf.d), OHNE Pool-Overrides (php_admin_value).
fpm_verify() {
  local bin="$1" svc="$2" info s k want got rc=0
  if ! command -v "$bin" >/dev/null 2>&1; then
    log "    Pruefung: $bin nicht gefunden - uebersprungen"; return 0
  fi
  info=$("$bin" -i 2>/dev/null | sed -e 's#</td><td[^>]*># => #g' -e 's/<[^>]*>//g' -e 's/&quot;/"/g')
  if [ -z "$info" ]; then
    err "$svc: $bin -i lieferte keine Ausgabe - Pruefung nicht moeglich"; return 1
  fi
  for s in "${SETTINGS[@]}"; do
    k="${s%%=*}"; want="${s#*=}"
    got=$(printf '%s\n' "$info" | K="$k" awk -F' => ' '{ x=$1; gsub(/^[ \t]+|[ \t]+$/,"",x) }
          x==ENVIRON["K"] { v=$2; gsub(/^[ \t]+|[ \t]+$/,"",v); print v; f=1; exit } END { if (!f) print "__FEHLT__" }')
    if [ "$got" = "__FEHLT__" ]; then
      err "$svc: $k in '$bin -i' nicht gefunden"; rc=1
    elif [ "$got" = "$want" ] || { [ "$(norm_val "$got")" = "$(norm_val "$want")" ] && [[ "$(norm_val "$want")" =~ ^[01]$ ]]; }; then
      log "    Pruefung OK ($svc, $bin -i): $k = '$got'"
    else
      err "$svc: Pruefung per $bin -i: $k = '$got', erwartet '$want'"; rc=1
    fi
  done
  return $rc
}

# ---------- 0. Momentaufnahme: welche Dienste laufen BEIM START ----------
# Nur diese werden spaeter neu geladen; gestoppte Dienste werden nie gestartet.
declare -A WAS_ACTIVE=()
SNAP_UNITS=()
for b in /usr/sbin/php-fpm* /usr/sbin/php5-fpm; do
  [ -x "$b" ] || continue
  n=$(basename "$b")
  case "$n" in
    php5-fpm)  SNAP_UNITS+=("php5-fpm") ;;
    php-fpm*)  SNAP_UNITS+=("php${n#php-fpm}-fpm") ;;
  esac
done
SNAP_UNITS+=("apache2")
RUNNING_LIST=""
for u in "${SNAP_UNITS[@]}"; do
  if svc_active "$u"; then WAS_ACTIVE[$u]=1; RUNNING_LIST="$RUNNING_LIST $u"; fi
done
log "Laufende Dienste beim Start:${RUNNING_LIST:- keine}"


# ---------- 1. php.ini-Dateien finden ----------
mapfile -t INIS < <(find /etc/php5 /etc/php -maxdepth 3 -name php.ini -type f 2>/dev/null | sort)
if [ "${#INIS[@]}" -eq 0 ]; then
  log "Keine php.ini unter /etc/php5 oder /etc/php gefunden - Abbruch."
  exit 1
fi

TMPD=$(mktemp -d) || exit 1
trap 'rm -rf "$TMPD"' EXIT

declare -A RELOAD_FPM=() FPM_BIN_OF=() FPM_INI_OF=() FPM_BAK_OF=()
RELOAD_APACHE=0
CHANGED=0
ERRORS=0

# ---------- 2./3. pruefen, Backup, setzen ----------
for INI in "${INIS[@]}"; do
  ini_info "$INI"
  sapi_selected "$SAPI" || continue
  log "Pruefe: $INI (SAPI=$SAPI, PHP $VER)"
  check_overrides "$INI"

  NEW="$TMPD/$(echo "$INI" | tr '/' '_')"
  if ! build_new "$INI" "$NEW"; then
    err "$INI: neue Fassung konnte nicht erzeugt werden - uebersprungen"
    continue
  fi
  if cmp -s "$INI" "$NEW"; then
    log "    bereits korrekt - keine Aenderung"
    continue
  fi

  log "    Aenderungen:"
  diff -u "$INI" "$NEW" | sed -n '3,$p' | sed 's/^/        /'
  CHANGED=$((CHANGED+1))

  N=1; while [ -e "${INI}-version-${N}" ]; do N=$((N+1)); done
  BAK="${INI}-version-${N}"

  if [ "$DRYRUN" -eq 1 ]; then
    echo "    DRY-RUN: Backup -> $BAK, dann neue Fassung schreiben"
  else
    if ! cp -a "$INI" "$BAK"; then
      err "$INI: Backup fehlgeschlagen - Datei NICHT geaendert"
      continue
    fi
    cat "$NEW" > "$INI"      # Rechte/Eigentuemer/Inode bleiben erhalten
    log "    geschrieben, Backup: $BAK"

    if command -v "$CLI_BIN" >/dev/null 2>&1; then
      OUT=$("$CLI_BIN" -c "$INI" -r '' 2>&1)
      if echo "$OUT" | grep -qiE 'syntax error|parse error'; then
        err "$INI: Parse-Test fehlgeschlagen -> Rollback aus $BAK"
        echo "$OUT" | sed 's/^/        /'
        cat "$BAK" > "$INI"
        continue
      fi
      for S in "${SETTINGS[@]}"; do
        k="${S%%=*}"
        v=$(K="$k" "$CLI_BIN" -c "$INI" -r 'echo ini_get(getenv("K"));' 2>/dev/null)
        log "    wirksam (Sicht $CLI_BIN): $k = '$v'"
      done
    fi
  fi

  case "$SAPI" in
    fpm)     RELOAD_FPM[$FPM_SVC]=1; FPM_BIN_OF[$FPM_SVC]="$FPM_BIN"
             FPM_INI_OF[$FPM_SVC]="$INI"; FPM_BAK_OF[$FPM_SVC]="$BAK" ;;
    apache2) RELOAD_APACHE=1 ;;
    *)       log "    SAPI $SAPI: kein Dienst-Reload noetig" ;;
  esac
done

# ---------- 4. nur betroffene Dienste graceful neu laden ----------
if [ "$CHANGED" -eq 0 ]; then
  log "Keine Aenderungen - es wird kein Dienst neu geladen."
else
  log "----- Betroffene Dienste -----"
  for SVC in "${!RELOAD_FPM[@]}"; do
    if [ -z "${WAS_ACTIVE[$SVC]}" ]; then
      log "  ${C_YEL}NICHT NEU GELADEN: $SVC (lief beim Start nicht - neue php.ini wirkt erst beim naechsten Start)${C_OFF}"; NOT_RELOADED+=("$SVC (lief beim Start nicht)"); continue
    fi
    if ! svc_active "$SVC"; then
      err "$SVC NICHT NEU GELADEN: Dienst wurde waehrend des Laufs gestoppt (kein Start)"; NOT_RELOADED+=("$SVC (inzwischen gestoppt)"); continue
    fi
    BIN="${FPM_BIN_OF[$SVC]}"
    if [ "$DRYRUN" -eq 1 ]; then
      echo "    DRY-RUN: $BIN -t && reload $SVC && Pruefung per $BIN -i"; continue
    fi
    if command -v "$BIN" >/dev/null 2>&1; then
      if ! OUT=$("$BIN" -t 2>&1); then
        err "$SVC NICHT NEU GELADEN: $BIN -t fehlgeschlagen -> Rollback ${FPM_INI_OF[$SVC]}"; NOT_RELOADED+=("$SVC (Konfigtest fehlgeschlagen)")
        echo "$OUT" | sed 's/^/        /'
        cat "${FPM_BAK_OF[$SVC]}" > "${FPM_INI_OF[$SVC]}"
        continue
      fi
    fi
    if svc_reload "$SVC"; then log "  ${C_GRN}neu geladen (graceful): $SVC${C_OFF}"; RELOADED+=("$SVC"); sleep 1; svc_active "$SVC" || err "$SVC laeuft nach dem Reload NICHT mehr - pruefen: systemctl status $SVC"; fpm_verify "$BIN" "$SVC"
    else err "$SVC NICHT NEU GELADEN: Reload fehlgeschlagen"; NOT_RELOADED+=("$SVC (Reload fehlgeschlagen)"); fi
  done

  if [ "$RELOAD_APACHE" -eq 1 ] && command -v apache2ctl >/dev/null 2>&1; then
    MODS=$(apache2ctl -M 2>/dev/null)
    if echo "$MODS" | grep -qiE 'php[0-9_]*_module'; then
      if [ -z "${WAS_ACTIVE[apache2]}" ] || ! svc_active apache2; then
        log "  ${C_YEL}NICHT NEU GELADEN: apache2 (lief beim Start nicht oder gestoppt)${C_OFF}"; NOT_RELOADED+=("apache2 (lief nicht)")
      elif [ "$DRYRUN" -eq 1 ]; then
        echo "    DRY-RUN: apache2ctl configtest && reload apache2"
      elif apache2ctl configtest >/dev/null 2>&1; then
        if svc_reload apache2; then log "  ${C_GRN}neu geladen (graceful): apache2${C_OFF}"; RELOADED+=("apache2")
        else err "apache2 NICHT NEU GELADEN: Reload fehlgeschlagen"; NOT_RELOADED+=("apache2 (Reload fehlgeschlagen)"); fi
      else
        err "apache2 NICHT NEU GELADEN: apache2ctl configtest fehlgeschlagen"; NOT_RELOADED+=("apache2 (configtest fehlgeschlagen)")
      fi
    else
      log "  apache2: kein mod_php geladen - kein Reload noetig"
    fi
  fi
fi

if [ "$DRYRUN" -eq 1 ]; then
  log "Fertig (DRY-RUN - nichts geaendert). Dateien mit Aenderungsbedarf: $CHANGED. Zum Ausfuehren: $0 --apply"
else
  log "Fertig. (APPLY, geaenderte Dateien=$CHANGED, Fehler=$ERRORS)"
fi

# ---------- 5. Zusammenfassung ----------
echo
echo "==================== ZUSAMMENFASSUNG ===================="
[ "${#RELOADED[@]}" -gt 0 ] && echo "${C_GRN}Neu geladen: ${RELOADED[*]}${C_OFF}"
if [ "${#NOT_RELOADED[@]}" -gt 0 ]; then
  echo "${C_YEL}NICHT NEU GELADEN (trotz geaenderter php.ini):${C_OFF}"
  for x in "${NOT_RELOADED[@]}"; do echo "${C_YEL}  - $x${C_OFF}"; done
fi
if [ "$ERRORS" -gt 0 ]; then
  echo "${C_RED}ERROR: $ERRORS Fehler aufgetreten:${C_OFF}"
  for x in "${ERROR_LIST[@]}"; do echo "${C_RED}  ERROR: $x${C_OFF}"; done
  echo "${C_RED}ERGEBNIS: ERROR${C_OFF}"
else
  echo "${C_GRN}ERGEBNIS: OK - keine Fehler${C_OFF}"
fi
echo "========================================================="
[ "$ERRORS" -eq 0 ]
