#!/bin/sh
# initMAX zero-touch post-install for gaugemax 2.0.8. POSIX sh.
set +e
ID="gaugemax"; VERSION="2.0.8"; SLUG="gaugemax"; LEGACY_IDS=""; STAGING="/usr/share/initmax/zabbix-modules/gaugemax"
BACKUP_FOREIGN_BRANDING="0"
COMPANIONS=""
log(){ echo "initMAX: $*" >&2; }
rule(){ log "============================================================"; }

# 1. active frontend root (defines.inc.php marker)
FE_ROOT=""
for r in "${ZABBIX_FRONTEND_DIR:-}" /usr/share/zabbix/ui /usr/share/zabbix /usr/share/zabbix/php /srv/www/htdocs/zabbix; do
  [ -n "$r" ] && [ -f "$r/include/defines.inc.php" ] && { FE_ROOT="$r"; break; }
done
if [ -z "$FE_ROOT" ]; then
  log ""; rule
  log "[WARN] $ID $VERSION was staged, but no Zabbix frontend was found"
  log "[ACTION] Deploy it after the frontend is installed: /usr/libexec/initmax-widget-deploy-$SLUG"
  log "[INFO] Docs: https://www.initmax.com/wiki/$SLUG/"
  rule; exit 0
fi
MODULES="$FE_ROOT/modules"; DEST="$MODULES/$ID"
ZBXVER=$(grep -oE "'ZABBIX_VERSION'[[:space:]]*,[[:space:]]*'[0-9.]+'" "$FE_ROOT/include/defines.inc.php" 2>/dev/null | grep -oE "[0-9]+\.[0-9]+" | head -1)

# 2. pick module tree for this Zabbix (v1 = 6.0/6.2, v2 = 6.4+) + relative_path
case "$ZBXVER" in
  6.0|6.2) SRC="$STAGING/legacy"; REL="$ID" ;;
  *)       SRC="$STAGING/modern"; REL="modules/$ID" ;;
esac
# Never replace a working tree with an incompatible fallback. The build
# refuses to package a declared 6.0/6.2 target without legacy/, but this
# runtime guard also protects a damaged or manually altered installation.
[ -d "$SRC" ] || { log "[WARN] Compatible module tree missing: $SRC - current deployment was left untouched"; exit 0; }

# deploy the chosen tree to the real path (atomic, no symlink)
if [ "$SRC" != "$DEST" ]; then
  mkdir -p "$MODULES"; rm -rf "$DEST.imxnew"; cp -a "$SRC" "$DEST.imxnew"; rm -rf "$DEST"; mv "$DEST.imxnew" "$DEST"
fi

# 3. ownership + SELinux
OWNER=$(stat -c '%U:%G' "$FE_ROOT/include/defines.inc.php" 2>/dev/null)
[ -n "$OWNER" ] && chown -R "$OWNER" "$DEST" 2>/dev/null
command -v restorecon >/dev/null 2>&1 && restorecon -RF "$DEST" >/dev/null 2>&1

# 3a. Frontend branding hand-off. A module that manages frontend branding
# (ships services/Branding.php) writes <frontend>/local/conf/brand.conf.php
# at request time, as the web server. The frontend's files are typically
# root-owned and read-only to PHP, so without this the feature can only
# report 'not writable'. Package install is the one moment root is at the
# keyboard - prepare the directory here, owned by the RUNTIME user
# (php-fpm/apache worker), which is NOT the owner of defines.inc.php on
# most installs. Fail-open: no detectable web user just logs the manual
# command. Additionally, when the product opts in through features.yaml,
# an administrator's own brand.conf.php is never overwritten: it is moved
# atomically beside the original under a unique, discoverable backup name.
if [ "$BACKUP_FOREIGN_BRANDING" = "1" ] || [ -f "$STAGING/modern/services/Branding.php" ] || [ -f "$STAGING/legacy/services/Branding.php" ]; then
  WEBUSER=""
  VHOST=$(grep -rslE "(Alias|DocumentRoot|root)[^#]*$FE_ROOT" /etc/apache2 /etc/httpd /etc/nginx 2>/dev/null | head -1)
  if [ -n "$VHOST" ]; then
    SOCK=$(grep -oE '(proxy:unix:|fastcgi_pass[[:space:]]+unix:)[^|";]+' "$VHOST" 2>/dev/null | sed 's/.*unix://; s/[|";].*//' | head -1)
    if [ -n "$SOCK" ]; then
      POOL=$(grep -rslF -- "$SOCK" /etc/php*/fpm/pool.d/*.conf /etc/php/*/fpm/pool.d/*.conf /etc/php-fpm.d/*.conf 2>/dev/null | head -1)
      [ -n "$POOL" ] && WEBUSER=$(grep -E '^[[:space:]]*user[[:space:]]*=' "$POOL" | head -1 | sed 's/.*=[[:space:]]*//; s/[[:space:]].*//; s/;.*//')
    fi
  fi
  [ -z "$WEBUSER" ] && WEBUSER=$(ps -eo user=,comm= 2>/dev/null | grep -E "php-fpm|httpd|apache2|nginx" | grep -v "^root" | head -1 | cut -d" " -f1)
  if [ -z "$WEBUSER" ]; then
    for u in www-data apache nginx wwwrun; do id "$u" >/dev/null 2>&1 && { WEBUSER="$u"; break; }; done
  fi
  LCONF="$FE_ROOT/local/conf"; BRAND_FILE="$LCONF/brand.conf.php"
  BRAND_STORE="/var/lib/initmax/zabbix-branding"
  mkdir -p "$LCONF/images" "$BRAND_STORE" 2>/dev/null
  if [ "$BACKUP_FOREIGN_BRANDING" = "1" ] && [ -f "$BRAND_FILE" ] && [ ! -L "$BRAND_FILE" ] \
      && ! grep -qF "// uxmax-branding:" "$BRAND_FILE" 2>/dev/null; then
    BRAND_BACKUP="$BRAND_FILE.uxmax.bak"; N=0
    while [ -e "$BRAND_BACKUP" ] || [ -L "$BRAND_BACKUP" ]; do N=$((N + 1)); BRAND_BACKUP="$BRAND_FILE.uxmax.bak.$N"; done
    if mv "$BRAND_FILE" "$BRAND_BACKUP"; then
      log "[BACKUP] Existing branding preserved: $BRAND_FILE -> $BRAND_BACKUP"
    else
      log "[WARN] Existing branding could not be backed up: $BRAND_FILE"
    fi
  fi
  if [ -f "$BRAND_FILE" ] && [ ! -L "$BRAND_FILE" ] && grep -qF "// uxmax-branding:" "$BRAND_FILE" 2>/dev/null; then
    mv "$BRAND_FILE" "$BRAND_STORE/brand.conf.php" \
      && log "[INFO] Branding moved   : $BRAND_FILE -> $BRAND_STORE/brand.conf.php"
  fi
  if [ ! -e "$BRAND_FILE" ] || [ -L "$BRAND_FILE" ]; then
    ln -sfn "$BRAND_STORE/brand.conf.php" "$BRAND_FILE"
  fi
  BRAND_OWNER="$WEBUSER"; [ -z "$BRAND_OWNER" ] && BRAND_OWNER="${OWNER%%:*}"
  BRAND_WRITERS=""
  for u in "$WEBUSER" www-data apache nginx wwwrun http; do
    [ -n "$u" ] || continue
    id "$u" >/dev/null 2>&1 || continue
    case " $BRAND_WRITERS " in *" $u "*) ;; *) BRAND_WRITERS="$BRAND_WRITERS $u" ;; esac
  done
  if [ -n "$BRAND_OWNER" ]; then
    chown "$BRAND_OWNER" "$LCONF" "$LCONF/images" "$BRAND_STORE" 2>/dev/null
    chmod u+rwx "$LCONF" "$LCONF/images" "$BRAND_STORE" 2>/dev/null
    [ -f "$BRAND_STORE/brand.conf.php" ] && chown "$BRAND_OWNER" "$BRAND_STORE/brand.conf.php" 2>/dev/null
  fi
  if command -v setfacl >/dev/null 2>&1; then
    for u in $BRAND_WRITERS; do
      setfacl -m u:"$u":rwx "$BRAND_STORE" "$LCONF" "$LCONF/images" 2>/dev/null
      [ -f "$BRAND_STORE/brand.conf.php" ] && setfacl -m u:"$u":rw "$BRAND_STORE/brand.conf.php" 2>/dev/null
    done
  fi
  command -v restorecon >/dev/null 2>&1 && restorecon -RF "$FE_ROOT/local" >/dev/null 2>&1
  if [ "$(id -u)" = "0" ] && [ -n "$BRAND_WRITERS" ]; then
    BRAND_OK=""
    for u in $BRAND_WRITERS; do
      if su -s /bin/sh "$u" -c "touch \"$BRAND_STORE/.imx-write-test\" && rm -f \"$BRAND_STORE/.imx-write-test\"" 2>/dev/null; then
        BRAND_OK="$BRAND_OK $u"
      fi
    done
    if [ -n "$BRAND_OK" ]; then
      log "[OK] Branding path   : prepared $LCONF -> $BRAND_STORE (writable by:$BRAND_OK)"
    else
      log "[WARN] $BRAND_STORE is not writable by any web user ($BRAND_WRITERS) - ACL, mount or SELinux?"
      log "[ACTION] Grant write once, e.g.: setfacl -m u:<web-user>:rwx $BRAND_STORE $LCONF $LCONF/images"
    fi
  elif [ -n "$BRAND_OWNER" ] || [ "$(id -u)" != "0" ]; then
    log "[OK] Branding path   : prepared $LCONF"
  else
    log "[ACTION] Branding needs $LCONF writable by the web server user:"
    log "         mkdir -p $LCONF/images && chown <web-user> $LCONF $LCONF/images"
  fi
fi

# 3b. Retire STALE COPIES of every module id THIS package ships.
# The id list is derived from the PAYLOAD itself - the staged manifests
# (modern, legacy and every companion) plus the declared migrate_from
# ids - so every product self-cleans with nothing to declare per repo.
# Before the packages existed the docs told customers to git-clone the
# widget into modules/, which yields a DIFFERENTLY NAMED directory (e.g.
# modules/Zabbix-UI-Widgets-<name>/) holding the SAME manifest id. The
# package deploys to modules/<id>/, so that clone survives, Zabbix lists
# the widget TWICE after a directory scan - or refuses both copies with
# `Identical namespace ...`. Match strictly on the manifest id and skip
# every directory the PACKAGE owns ($DEST and each companion $CDEST);
# package payloads live outside $MODULES, so nothing else is exempt.
manifest_id(){ grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' "$1" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/'; }
OWN_IDS=""
add_own_id(){ [ -n "$1" ] || return 0; for o in $OWN_IDS; do [ "$o" = "$1" ] && return 0; done; OWN_IDS="$OWN_IDS $1"; }
add_own_id "$ID"
for m in "$STAGING"/modern/manifest.json "$STAGING"/legacy/manifest.json "$STAGING"/companions/*/manifest.json; do
  [ -f "$m" ] && add_own_id "$(manifest_id "$m")"
done
for lid in $LEGACY_IDS; do add_own_id "$lid"; done
# Directories the package owns and must never delete.
OWN_DIRS="$DEST"
for spec in $COMPANIONS; do OWN_DIRS="$OWN_DIRS $MODULES/${spec%%:*}"; done
for oid in $OWN_IDS; do
  for d in "$MODULES"/*/; do
    d="${d%/}"
    [ -d "$d" ] || continue
    for own in $OWN_DIRS; do [ "$d" = "$own" ] && continue 2; done
    DM=""
    for mf in "$d/manifest.json" "$d/manifest.json.tpl"; do [ -f "$mf" ] && { DM="$mf"; break; }; done
    [ -n "$DM" ] || continue
    grep -q "\"id\"[[:space:]]*:[[:space:]]*\"$oid\"" "$DM" 2>/dev/null || continue
    rm -rf "$d" && log "[INFO] Removed superseded module at $d (stale copy of $oid)"
  done
done


# 5. Register and enable every newly installed initMAX module. Reuse the
# migration-safe behaviour previously limited to migrate_from widgets:
# preserve an explicit admin-disabled status and all existing config.
ENABLE_RESULT="manual"
CONF=""
for c in "$FE_ROOT/conf/zabbix.conf.php" /etc/zabbix/web/zabbix.conf.php "$FE_ROOT/../conf/zabbix.conf.php"; do [ -f "$c" ] && { CONF="$c"; break; }; done
if [ -n "$CONF" ]; then
  val() { grep -oE "DB\\['$1'\\][[:space:]]*=[[:space:]]*'[^']*'" "$CONF" 2>/dev/null | sed "s/.*'\\([^']*\\)'.*/\\1/" | head -1; }
  DBTYPE=$(val TYPE); DBHOST=$(val SERVER); DBPORT=$(val PORT); DBNAME=$(val DATABASE); DBUSER=$(val USER); DBPASS=$(val PASSWORD)
  [ -z "$DBHOST" ] && DBHOST="localhost"
  pg() { export PGPASSWORD="$DBPASS"; PA=""; [ -n "$DBPORT" ] && [ "$DBPORT" != "0" ] && PA="-p $DBPORT"; psql -h "$DBHOST" $PA -U "$DBUSER" -d "$DBNAME" -tAqc "$1" 2>/dev/null; }
  my() { mysql -h "$DBHOST" ${DBPORT:+-P $DBPORT} -u "$DBUSER" -p"$DBPASS" -N -s "$DBNAME" -e "$1" 2>/dev/null; }
  sync_module_ids() {  # $1 = pg|my
    HAVE=$($1 "SELECT COUNT(*) FROM ids WHERE table_name='module' AND field_name='moduleid'")
    case "$HAVE" in
      ''|*[!0-9]*) return 0 ;;
    esac
    if [ "$HAVE" = "0" ]; then
      $1 "INSERT INTO ids (table_name,field_name,nextid) SELECT 'module','moduleid',COALESCE(MAX(moduleid),0) FROM module" >/dev/null
    else
      $1 "UPDATE ids SET nextid=(SELECT COALESCE(MAX(moduleid),0) FROM module) WHERE table_name='module' AND field_name='moduleid' AND nextid < (SELECT COALESCE(MAX(moduleid),0) FROM module)" >/dev/null
    fi
  }
  purge_stale_rows() {  # $1 = pg|my
    for oid in $OWN_IDS; do
      TARGET=""
      [ "$oid" = "$ID" ] && TARGET="$REL"
      for spec in $COMPANIONS; do [ "$oid" = "${spec%%:*}" ] && TARGET="modules/$oid"; done
      if [ -n "$TARGET" ] && [ ! -d "$FE_ROOT/$TARGET" ] && [ ! -d "$MODULES/$TARGET" ]; then TARGET=""; fi
      $1 "SELECT relative_path FROM module WHERE id='$oid'" | while read -r rp; do
        [ -n "$rp" ] || continue
        [ -d "$FE_ROOT/$rp" ] && continue
        [ -d "$MODULES/$rp" ] && continue
        if [ -n "$TARGET" ]; then
          CLAIMED=$($1 "SELECT COUNT(*) FROM module WHERE id='$oid' AND relative_path='$TARGET'")
          if [ "$CLAIMED" = "0" ]; then
            $1 "UPDATE module SET relative_path='$TARGET' WHERE id='$oid' AND relative_path='$rp'" >/dev/null \
              && log "[INFO] Migrated module row (settings kept): $oid: $rp -> $TARGET"
            continue
          fi
          $1 "UPDATE module SET config=(SELECT c FROM (SELECT config c FROM module WHERE id='$oid' AND relative_path='$rp') x) WHERE id='$oid' AND relative_path='$TARGET' AND (config='' OR config='[]' OR config='{}') AND (SELECT c FROM (SELECT config c FROM module WHERE id='$oid' AND relative_path='$rp') x) NOT IN ('','[]','{}')" >/dev/null \
            && log "[INFO] Carried settings from the old $oid row onto $TARGET"
        fi
        $1 "DELETE FROM module WHERE id='$oid' AND relative_path='$rp'" >/dev/null \
          && log "[INFO] Removed stale module row: $oid -> $rp"
      done
    done
  }
  ensure_module_row() {  # $1 = pg|my  [$2 = id] [$3 = relative_path]
    MID="${2:-$ID}"; MREL="${3:-$REL}"
    ST=$($1 "SELECT status FROM module WHERE id='$MID'"); QRC=$?
    [ "$QRC" -ne 0 ] && { ENABLE_RESULT="manual"; return; }
    if [ "$ST" = "0" ]; then
      $1 "UPDATE module SET relative_path='$MREL' WHERE id='$MID'" >/dev/null && ENABLE_RESULT="admin-disabled"
      return
    fi
    if [ -n "$ST" ]; then
      $1 "UPDATE module SET relative_path='$MREL' WHERE id='$MID'" >/dev/null && ENABLE_RESULT="already-enabled"
    elif [ "$1" = "pg" ]; then
      M=$(pg "SELECT COALESCE(MAX(moduleid),0)+1 FROM module"); [ -z "$M" ] && M=1
      pg "INSERT INTO module (moduleid,id,relative_path,status,config) VALUES ($M,'$MID','$MREL',1,'[]')" >/dev/null
    else
      my "INSERT INTO module (moduleid,id,relative_path,status,config) VALUES ((SELECT COALESCE(MAX(moduleid),0)+1 FROM (SELECT moduleid FROM module) m),'$MID','$MREL',1,'[]')" >/dev/null
    fi
    if [ -z "$ST" ]; then
      ST=$($1 "SELECT status FROM module WHERE id='$MID'")
      [ "$ST" = "1" ] && ENABLE_RESULT="auto-enabled" || ENABLE_RESULT="manual"
    fi
    case "$ENABLE_RESULT" in
      auto-enabled|already-enabled) for lid in $LEGACY_IDS; do $1 "DELETE FROM module WHERE id='$lid'" >/dev/null; done ;;
    esac
    sync_module_ids "$1"
  }
  case "$DBTYPE" in
    POSTGRESQL) command -v psql  >/dev/null 2>&1 && { purge_stale_rows pg; ensure_module_row pg; } ;;
    MYSQL)      command -v mysql >/dev/null 2>&1 && { purge_stale_rows my; ensure_module_row my; } ;;
  esac
fi


# 5b. Companion modules of the same product.
# ENABLE_RESULT describes the PRIMARY module in the summary below, and
# ensure_module_row writes it - so it is saved across this loop.
PRIMARY_ENABLE_RESULT="$ENABLE_RESULT"
# A companion is deployed ONLY on the Zabbix versions its own manifest
# claims. On any other version its directory is REMOVED rather than
# left behind: a widget that can never receive anything is worse than
# an absent one, because the customer can still put it on a dashboard.
for spec in $COMPANIONS; do
  CID="${spec%%:*}"; CVERS="${spec#*:}"
  CDEST="$MODULES/$CID"
  case ",$CVERS," in
    *",$ZBXVER,"*) ;;
    *) [ -d "$CDEST" ] && rm -rf "$CDEST" && log "[INFO] $CID needs Zabbix ${CVERS} - removed from this ${ZBXVER} frontend"; continue ;;
  esac
  CSRC="$STAGING/companions/$CID"
  [ -d "$CSRC" ] || { log "[WARN] companion tree missing: $CSRC"; continue; }
  rm -rf "$CDEST.imxnew"; cp -a "$CSRC" "$CDEST.imxnew"; rm -rf "$CDEST"; mv "$CDEST.imxnew" "$CDEST"
  [ -n "$OWNER" ] && chown -R "$OWNER" "$CDEST" 2>/dev/null
  command -v restorecon >/dev/null 2>&1 && restorecon -RF "$CDEST" >/dev/null 2>&1
  if [ -n "$CONF" ]; then
    case "$DBTYPE" in
      POSTGRESQL) command -v psql  >/dev/null 2>&1 && ensure_module_row pg "$CID" "modules/$CID" ;;
      MYSQL)      command -v mysql >/dev/null 2>&1 && ensure_module_row my "$CID" "modules/$CID" ;;
    esac
  fi
  log " [OK] Companion       : $CID deployed to $CDEST"
done
ENABLE_RESULT="$PRIMARY_ENABLE_RESULT"
# Final sweep: a companion gated out above had its directory removed,
# so its row is now stale too. Same generic rule, same owned-ids guard.
if [ -n "$CONF" ]; then
  case "$DBTYPE" in
    POSTGRESQL) command -v psql  >/dev/null 2>&1 && purge_stale_rows pg ;;
    MYSQL)      command -v mysql >/dev/null 2>&1 && purge_stale_rows my ;;
  esac
fi

# RPM has no nFPM file-trigger field. Its package therefore ships a
# systemd.path watcher which invokes this selector when a Zabbix frontend
# replaces defines.inc.php. Debian uses its native dpkg pathname trigger.
WATCH="initmax-widget-deploy-$SLUG.path"; WATCH_RESULT="automatic"
if [ -f "/usr/lib/systemd/system/$WATCH" ]; then
  WATCH_RESULT="helper"
  if command -v systemctl >/dev/null 2>&1; then
    systemctl daemon-reload >/dev/null 2>&1 || true
    systemctl enable --now "$WATCH" >/dev/null 2>&1 && WATCH_RESULT="automatic"
  fi
fi

# 6. One compact, stable result block for apt, dnf and manual review.
log ""; rule
log " $ID $VERSION - INSTALLATION COMPLETE"
rule
log " [OK] Zabbix frontend : ${ZBXVER:-unknown} ($FE_ROOT)"
log " [OK] Module deployed : $DEST"
case "$ENABLE_RESULT" in
  auto-enabled)    log " [OK] Module status   : enabled automatically" ;;
  already-enabled) log " [OK] Module status   : already enabled; settings preserved" ;;
  admin-disabled)  log " [INFO] Module status : disabled by administrator - preserved" ;;
  *)               log " [ACTION] Module could not be enabled automatically"; log " [ACTION] Open Administration > General > Modules > Scan directory" ;;
esac
if [ "$WATCH_RESULT" = "automatic" ]; then
  log " [OK] Upgrade recovery: automatic"
else
  log " [INFO] Upgrade recovery: use the helper after a Zabbix upgrade"
fi
log ""
log " Docs     : https://www.initmax.com/wiki/$SLUG/"
log " Recovery : /usr/libexec/initmax-widget-deploy-$SLUG"
rule
exit 0
