#!/bin/sh
# initMAX zero-touch post-install for navigatormax 2.0.1. POSIX sh.
set +e
DOCS_URL=https://www.initmax.com/wiki/navigatormax/
ID="navigatormax"; VERSION="2.0.1"; SLUG="navigatormax"; LEGACY_IDS=""; STAGING="/usr/share/initmax/zabbix-modules/navigatormax"
BACKUP_FOREIGN_BRANDING="0"
RENAMED_IDS=""
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: $DOCS_URL"
  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; }

# A manual install must keep working if database migration cannot be completed.
manifest_id(){ grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' "$1" 2>/dev/null | head -1 | sed 's/.*"\([^"]*\)"$/\1/'; }
TAKEOVER_WORK=""; TAKEOVER_DONE=0; TAKEOVER_RECOVERY=0
takeover_deferred() {
  if [ "$TAKEOVER_RECOVERY" = 1 ]; then
    log "[DEFERRED] Previous takeover still needs recovery: $1; backups retained in $TAKEOVER_WORK"
    log "[ACTION] Restore database access, then run /usr/libexec/initmax-widget-deploy-$SLUG"
    exit 0
  fi
  log "[DEFERRED] $ID $VERSION is staged; the existing installation was kept: $1"
  log "[ACTION] Resolve the database/filesystem issue, then run /usr/libexec/initmax-widget-deploy-$SLUG"
  [ -z "$TAKEOVER_WORK" ] || rm -rf "$TAKEOVER_WORK"
  exit 0
}
TAKEOVER_IDS="$ID"
for spec in $COMPANIONS; do TAKEOVER_IDS="$TAKEOVER_IDS ${spec%%:*}"; done
for pending in "$STAGING"/.takeover.*/; do
  [ -d "$pending" ] || continue
  if [ -n "$TAKEOVER_WORK" ] || [ ! -f "$pending/deployed" ]; then
    log "[DEFERRED] Existing takeover backups need manual review under $STAGING; no installation files were changed"
    log "[ACTION] Inspect the retained .takeover directories before retrying /usr/libexec/initmax-widget-deploy-$SLUG"
    exit 0
  fi
  TAKEOVER_WORK="${pending%/}"; TAKEOVER_RECOVERY=1
  if [ "$(cat "$TAKEOVER_WORK/context-root" 2>/dev/null)" != "$FE_ROOT" ] ||
      [ "$(cat "$TAKEOVER_WORK/context-zabbix" 2>/dev/null)" != "$ZBXVER" ] ||
      [ "$(cat "$TAKEOVER_WORK/context-version" 2>/dev/null)" != "$VERSION" ]; then
    log "[DEFERRED] Retained takeover belongs to another frontend or package version: $TAKEOVER_WORK"
    log "[ACTION] Review the saved frontend/version context and backups before retrying /usr/libexec/initmax-widget-deploy-$SLUG"
    exit 0
  fi
  TAKEOVER_IDS=$(cat "$TAKEOVER_WORK/ids")
done
if [ "$TAKEOVER_RECOVERY" = 0 ]; then
for tid in $TAKEOVER_IDS; do
  for old in "$MODULES"/*/; do
    old="${old%/}"
    [ -d "$old" ] && [ "$old" != "$MODULES/$tid" ] || continue
    mf="$old/manifest.json"; [ -f "$mf" ] || mf="$old/manifest.json.tpl"
    [ "$(manifest_id "$mf")" = "$tid" ] || continue
    [ -n "$TAKEOVER_WORK" ] || TAKEOVER_WORK=$(mktemp -d "$STAGING/.takeover.XXXXXX")
    [ -n "$TAKEOVER_WORK" ] || takeover_deferred "cannot prepare a takeover backup"
    case "$old" in *'
'*|*'	'*) takeover_deferred "legacy directory contains an unsupported line separator" ;; esac
    printf '%s\n' "$old" >> "$TAKEOVER_WORK/old" || takeover_deferred "cannot record legacy directories"
  done
done
fi
if [ -n "$TAKEOVER_WORK" ]; then
  TCONF=""
  for c in "$FE_ROOT/conf/zabbix.conf.php" /etc/zabbix/web/zabbix.conf.php "$FE_ROOT/../conf/zabbix.conf.php"; do
    [ -f "$c" ] && { TCONF="$c"; break; }
  done
  [ -n "$TCONF" ] || takeover_deferred "Zabbix database configuration is unavailable"
  tval() { grep -oE "DB\['$1'\][[:space:]]*=[[:space:]]*'[^']*'" "$TCONF" 2>/dev/null | sed "s/.*'\([^']*\)'.*/\1/" | head -1; }
  TTYPE=$(tval TYPE); THOST=$(tval SERVER); TPORT=$(tval PORT); TNAME=$(tval DATABASE); TUSER=$(tval USER); TPASS=$(tval PASSWORD)
  [ -n "$THOST" ] || THOST=localhost
  case "$TTYPE" in
    POSTGRESQL) command -v psql >/dev/null 2>&1 || takeover_deferred "PostgreSQL client psql is unavailable" ;;
    MYSQL) command -v mysql >/dev/null 2>&1 || takeover_deferred "MySQL client mysql is unavailable" ;;
    *) takeover_deferred "database type is unsupported" ;;
  esac
  # Explicit string modes make a directory containing quotes/backslashes literal.
  tsql() {
    if [ "$TTYPE" = POSTGRESQL ]; then
      export PGPASSWORD="$TPASS"
      if [ -n "$TPORT" ] && [ "$TPORT" != 0 ]; then
        psql -X -v ON_ERROR_STOP=1 -h "$THOST" -p "$TPORT" -U "$TUSER" -d "$TNAME" -tAqc "SET standard_conforming_strings=on; $1" 2>/dev/null
      else
        psql -X -v ON_ERROR_STOP=1 -h "$THOST" -U "$TUSER" -d "$TNAME" -tAqc "SET standard_conforming_strings=on; $1" 2>/dev/null
      fi
    else
      mysql -h "$THOST" ${TPORT:+-P $TPORT} -u "$TUSER" --password="$TPASS" --raw -N -s "$TNAME" -e "SET SESSION sql_mode=CONCAT(@@SESSION.sql_mode,',NO_BACKSLASH_ESCAPES'); $1" 2>/dev/null
    fi
  }
  tquote() { printf '%s' "$1" | sed "s/'/''/g"; }
  TAKEOVER_SQL="BEGIN;"
  if [ "$TAKEOVER_RECOVERY" = 0 ]; then
  mkdir -p "$TAKEOVER_WORK/new" "$TAKEOVER_WORK/backup" "$TAKEOVER_WORK/retired" "$TAKEOVER_WORK/state" || takeover_deferred "cannot prepare candidate directories"
  printf '%s\n' "$TAKEOVER_IDS" > "$TAKEOVER_WORK/ids" || takeover_deferred "cannot record takeover identities"
  printf '%s\n' "$FE_ROOT" > "$TAKEOVER_WORK/context-root" &&
  printf '%s\n' "$ZBXVER" > "$TAKEOVER_WORK/context-zabbix" &&
  printf '%s\n' "$VERSION" > "$TAKEOVER_WORK/context-version" || takeover_deferred "cannot record takeover context"
  for tid in $TAKEOVER_IDS; do
    target="modules/$tid"; payload="$STAGING/companions/$tid"; supported=0
    if [ "$tid" = "$ID" ]; then target="$REL"; payload="$SRC"; supported=1
    else
      for spec in $COMPANIONS; do
        [ "$tid" = "${spec%%:*}" ] || continue
        case ",${spec#*:}," in *",$ZBXVER,"*) supported=1 ;; esac
      done
    fi
    count=$(tsql "SELECT COUNT(*) FROM module WHERE id='$(tquote "$tid")'")
    [ "$?" = 0 ] || takeover_deferred "cannot read existing module registration"
    case "$count" in 0|1) ;; *) takeover_deferred "multiple or unreadable registrations for $tid require manual review" ;; esac
    previous=""
    if [ "$count" = 1 ]; then
      previous=$(tsql "SELECT relative_path FROM module WHERE id='$(tquote "$tid")'")
      [ "$?" = 0 ] && [ -n "$previous" ] || takeover_deferred "cannot read the existing path for $tid"
      case "$previous" in *'
'*|*'	'*) takeover_deferred "registration path contains an unsupported line separator" ;; esac
      if [ "$supported" = 1 ]; then
        TAKEOVER_SQL="$TAKEOVER_SQL
UPDATE module SET relative_path='$(tquote "$target")' WHERE id='$(tquote "$tid")' AND relative_path='$(tquote "$previous")';"
      else
        TAKEOVER_SQL="$TAKEOVER_SQL
DELETE FROM module WHERE id='$(tquote "$tid")' AND relative_path='$(tquote "$previous")';"
      fi
    fi
    expected_count="$count"; expected_path="$target"
    [ "$supported" = 1 ] || { expected_count=0; expected_path=""; }
    printf '%s\n' "$count" > "$TAKEOVER_WORK/state/$tid.old-count" &&
    printf '%s\n' "$previous" > "$TAKEOVER_WORK/state/$tid.old-path" &&
    printf '%s\n' "$expected_count" > "$TAKEOVER_WORK/state/$tid.new-count" &&
    printf '%s\n' "$expected_path" > "$TAKEOVER_WORK/state/$tid.new-path" || takeover_deferred "cannot record expected registration state"
    [ ! -e "$MODULES/$tid" ] && [ ! -L "$MODULES/$tid" ] || {
      [ "$(manifest_id "$MODULES/$tid/manifest.json")" = "$tid" ] || takeover_deferred "canonical path $tid belongs to another module"
    }
    if [ "$supported" = 1 ]; then
      [ -d "$payload" ] && [ "$(manifest_id "$payload/manifest.json")" = "$tid" ] || takeover_deferred "compatible payload missing for $tid"
      cp -a "$payload" "$TAKEOVER_WORK/new/$tid" || takeover_deferred "cannot copy the compatible payload for $tid"
    fi
  done
  TAKEOVER_SQL="$TAKEOVER_SQL
COMMIT;"
  fi
  rollback_takeover() {
    failed=0
    if [ -f "$TAKEOVER_WORK/retired-map" ]; then
      while IFS='	' read -r number old; do
        if [ -e "$TAKEOVER_WORK/retired/$number" ] || [ -L "$TAKEOVER_WORK/retired/$number" ]; then
          mv "$TAKEOVER_WORK/retired/$number" "$old" || failed=1
        fi
      done < "$TAKEOVER_WORK/retired-map"
    fi
    if [ -f "$TAKEOVER_WORK/active" ]; then
      while IFS='	' read -r tid had_original; do
        if [ -e "$TAKEOVER_WORK/backup/$tid" ] || [ -L "$TAKEOVER_WORK/backup/$tid" ]; then
          rm -rf "$MODULES/$tid" || failed=1
          mv "$TAKEOVER_WORK/backup/$tid" "$MODULES/$tid" || failed=1
        elif [ "$had_original" = 0 ]; then
          rm -rf "$MODULES/$tid" || failed=1
        fi
      done < "$TAKEOVER_WORK/active"
    fi
    [ "$failed" = 0 ] || { log "[DEFERRED] Filesystem recovery needs attention; backups retained in $TAKEOVER_WORK"; exit 0; }
  }
  takeover_abort() { rollback_takeover; takeover_deferred "$1"; }
  takeover_state() {
    TAKEOVER_STATE=unknown; old_match=1; new_match=1
    for tid in $TAKEOVER_IDS; do
      actual_count=$(tsql "SELECT COUNT(*) FROM module WHERE id='$(tquote "$tid")'")
      [ "$?" = 0 ] || return
      case "$actual_count" in 0|1) ;; *) return ;; esac
      actual_path=""
      if [ "$actual_count" = 1 ]; then
        actual_path=$(tsql "SELECT relative_path FROM module WHERE id='$(tquote "$tid")'")
        [ "$?" = 0 ] || return
      fi
      [ "$actual_count" = "$(cat "$TAKEOVER_WORK/state/$tid.old-count")" ] &&
        { [ "$actual_count" = 0 ] || [ "$actual_path" = "$(cat "$TAKEOVER_WORK/state/$tid.old-path")" ]; } || old_match=0
      [ "$actual_count" = "$(cat "$TAKEOVER_WORK/state/$tid.new-count")" ] &&
        { [ "$actual_count" = 0 ] || [ "$actual_path" = "$(cat "$TAKEOVER_WORK/state/$tid.new-path")" ]; } || new_match=0
    done
    if [ "$new_match" = 1 ]; then TAKEOVER_STATE=new
    elif [ "$old_match" = 1 ]; then TAKEOVER_STATE=old; fi
  }
  takeover_uncertain() {
    log "[DEFERRED] Database commit outcome is unknown; candidate and original backups are retained in $TAKEOVER_WORK"
    log "[ACTION] Restore database access, then run /usr/libexec/initmax-widget-deploy-$SLUG to reconcile the saved registration paths"
    exit 0
  }
  if [ "$TAKEOVER_RECOVERY" = 1 ]; then
    takeover_state
    case "$TAKEOVER_STATE" in
      new) TAKEOVER_DONE=1 ;;
      old) rollback_takeover; rm -rf "$TAKEOVER_WORK"; exec /bin/sh "$0" "$@" ;;
      *) takeover_uncertain ;;
    esac
  else
  for tid in $TAKEOVER_IDS; do
    had_original=0
    if [ -e "$MODULES/$tid" ] || [ -L "$MODULES/$tid" ]; then
      had_original=1
    fi
    # Record intent before moving anything. Rollback tolerates a failed move,
    # but must never lose the only backup when a journal write fails.
    printf '%s\t%s\n' "$tid" "$had_original" >> "$TAKEOVER_WORK/active" || takeover_abort "cannot record canonical rollback state"
    if [ "$had_original" = 1 ]; then
      mv "$MODULES/$tid" "$TAKEOVER_WORK/backup/$tid" || takeover_abort "cannot back up canonical path $tid"
    fi
    if [ -d "$TAKEOVER_WORK/new/$tid" ]; then
      mv "$TAKEOVER_WORK/new/$tid" "$MODULES/$tid" || takeover_abort "cannot activate candidate $tid"
    fi
  done
  number=0
  while IFS= read -r old; do
    number=$((number + 1))
    printf '%s\t%s\n' "$number" "$old" >> "$TAKEOVER_WORK/retired-map" || takeover_abort "cannot record legacy rollback state"
    mv "$old" "$TAKEOVER_WORK/retired/$number" || takeover_abort "cannot retain the old module directory"
  done < "$TAKEOVER_WORK/old"
  # One connection and one transaction: a later failed statement must roll back
  # earlier path updates. A successful exit is the commit acknowledgement; never
  # roll files back merely because a separate post-commit read is unavailable.
  printf '%s\n' deployed > "$TAKEOVER_WORK/deployed" || takeover_abort "cannot record completed filesystem deployment"
  if ! tsql "$TAKEOVER_SQL" >/dev/null; then
    # A lost acknowledgement can report failure after COMMIT actually succeeded.
    # Only a known old registration permits filesystem rollback. Unknown state
    # keeps both recoverable versions, outside duplicate module scan paths.
    takeover_state
    case "$TAKEOVER_STATE" in
      new) log "[INFO] Database commit verified after a lost client acknowledgement" ;;
      old) takeover_abort "database migration failed; old registration and files restored" ;;
      *) takeover_uncertain ;;
    esac
  fi
  fi
  TAKEOVER_DONE=1
  rm -rf "$TAKEOVER_WORK"; TAKEOVER_WORK=""
  log "[OK] Manual installation migrated; module identities, status and settings preserved"
fi

# deploy the chosen tree to the real path (atomic, no symlink)
if [ "$SRC" != "$DEST" ] && [ "$TAKEOVER_DONE" != 1 ]; 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
# A former id of this module (module-id rename): its directory is the
# same product under the old name and is retired the same way; its row
# is NOT - carry_renamed_rows hands its config over first.
for rid in $RENAMED_IDS; do add_own_id "$rid"; done
# Native Zabbix packages move the frontend between the root and /ui.
# Retire only canonical copies of OUR ids in an abandoned known root.
# A second frontend with its own version marker is a live installation
# and must be preserved. Never follow a redirected root/modules path.
for oldroot in /usr/share/zabbix/ui /usr/share/zabbix /usr/share/zabbix/php /srv/www/htdocs/zabbix; do
  [ "$oldroot" = "$FE_ROOT" ] && continue
  [ -f "$oldroot/include/defines.inc.php" ] && continue
  [ -L "$oldroot" ] && continue
  [ -L "$oldroot/modules" ] && continue
  for oid in $OWN_IDS; do
    d="$oldroot/modules/$oid"
    [ -d "$d" ] && [ ! -L "$d" ] || continue
    [ "$(manifest_id "$d/manifest.json")" = "$oid" ] || continue
    rm -rf "$d" && log "[INFO] Removed orphaned module at $d (frontend moved)"
  done
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" --password="$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
      case " $RENAMED_IDS " in *" $oid "*) continue ;; esac
      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
          if ! $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; then
            log "[WARN] Could not carry settings from $oid; the old registration was kept"; continue
          fi
        fi
        $1 "DELETE FROM module WHERE id='$oid' AND relative_path='$rp'" >/dev/null \
          && log "[INFO] Removed stale module row: $oid -> $rp"
      done
    done
  }
  carry_renamed_rows() {  # $1 = pg|my
    for oid in $RENAMED_IDS; do
      OLD=$($1 "SELECT moduleid FROM module WHERE id='$oid' ORDER BY CASE WHEN config IN ('','[]','{}') THEN 1 ELSE 0 END, status DESC, moduleid LIMIT 1")
      case "$OLD" in ''|*[!0-9]*) continue ;; esac
      HAVE_NEW=$($1 "SELECT COUNT(*) FROM module WHERE id='$ID'")
      case "$HAVE_NEW" in ''|*[!0-9]*) continue ;; esac
      if [ "$HAVE_NEW" = "0" ]; then
        M=$($1 "SELECT COALESCE(MAX(moduleid),0)+1 FROM module")
        case "$M" in ''|*[!0-9]*) continue ;; esac
        $1 "INSERT INTO module (moduleid,id,relative_path,status,config) SELECT $M,'$ID','$REL',status,config FROM module WHERE moduleid=$OLD" >/dev/null \
          && log "[INFO] Module id renamed: $oid -> $ID (settings and status carried)"
        sync_module_ids "$1"
      else
        $1 "UPDATE module SET config=(SELECT c FROM (SELECT config c FROM module WHERE moduleid=$OLD) x) WHERE id='$ID' AND config IN ('','[]','{}') AND (SELECT c FROM (SELECT config c FROM module WHERE moduleid=$OLD) x) NOT IN ('','[]','{}')" >/dev/null
      fi
      LOSS=$($1 "SELECT COUNT(*) FROM module WHERE id='$oid' AND config NOT IN ('','[]','{}') AND config NOT IN (SELECT c FROM (SELECT config c FROM module WHERE id='$ID') x)")
      case "$LOSS" in ''|*[!0-9]*) continue ;; esac
      if [ "$LOSS" = "0" ]; then
        $1 "DELETE FROM module WHERE id='$oid'" >/dev/null && log "[INFO] Removed former module row: $oid (settings now on $ID)"
      else
        $1 "UPDATE module SET status=0 WHERE id='$oid'" >/dev/null
        log "[WARN] $oid and $ID both hold settings - $oid kept DISABLED for review in Administration > General > Modules"
      fi
    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 && { carry_renamed_rows pg; purge_stale_rows pg; ensure_module_row pg; } ;;
    MYSQL)      command -v mysql >/dev/null 2>&1 && { carry_renamed_rows my; 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; }
  if [ "$TAKEOVER_DONE" != 1 ]; then
    rm -rf "$CDEST.imxnew"; cp -a "$CSRC" "$CDEST.imxnew"; rm -rf "$CDEST"; mv "$CDEST.imxnew" "$CDEST"
  fi
  [ -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     : $DOCS_URL"
log " Recovery : /usr/libexec/initmax-widget-deploy-$SLUG"
rule
exit 0
