#!/usr/bin/env bash
# APOLO: instalacija na VPS (AlmaLinux / Rocky Linux / RHEL 9 i 10).
#
#   curl -fsSL https://update.apolopanel.com/apolo-install.sh | bash
#
# Skripta preuzme apolo.zip iz istog foldera iz kog je preuzeta i ona (podrazumevano
# https://update.apolopanel.com/), proveri sha256 (apolo.zip.sha256), instalira web stek
# (Nginx, PHP 8.4 i 8.3 iz Remi repozitorijuma, MariaDB, Redis, certbot, WP-CLI) i pokrene
# APOLO: apolo-agent (root, privilegovane operacije) i apolo-api (korisnik apolo, panel na
# https://<ip servera>:8443). Pri prvoj instalaciji pravi administratora "admin" i JEDNOM
# ispiše njegovu lozinku.
#
# Opcije (uz "| bash -s -- OPCIJE" ili "bash apolo-install.sh OPCIJE"):
#   --port N         port panela (podrazumevano 8443)
#   --email ADRESA   e-pošta prvog administratora (podrazumevano root@<hostname>)
#   --zip PUTANJA    instaliraj iz lokalnog apolo.zip umesto preuzimanja
#   --uninstall      ukloni APOLO (servisi, program, /etc/apolo); stanje u /var/lib/apolo ostaje
#   --purge          uz --uninstall: obriši i stanje panela i APOLO podešavanja sajtova
#   --upgrade        samo nadogradnja: odbija da radi ako APOLO još nije instaliran
#   --base-url URL   preuzmi paket iz drugog foldera (i ažuriraj panel odatle)
#   --force-os       ne proveravaj operativni sistem (samo za testiranje)
#
# Ponovno pokretanje radi nadogradnju (administrator i stanje se ne diraju); ako nova
# verzija ne proradi, vraća se prethodna. Web stek, sajtovi (/home) i baze se nikad ne brišu.
# Panel se ažurira i sam (Podešavanja → APOLO ažuriranja): apolo-agent preuzme paket sa adrese
# APOLO_UPDATE_URL iz /etc/apolo/apolo.env (upisuje je ovaj instaler: folder iz kog je preuzet,
# ako je administrator nije već postavio) i pokrene instaler iz paketa sa --upgrade.
#
# Za testiranje: APOLO_INSTALL_DRY=1 ispisuje dnf/systemctl/semanage/firewall… komande umesto
# da ih pokrene, a fajlove piše u APOLO_INSTALL_ROOT (privremeni folder ako nije zadat). Uz to
# APOLO_INSTALL_TEST_RESTART=KOMANDA se pokreće umesto restarta apolo-agent/apolo-api, a
# APOLO_INSTALL_TEST_HEALTH=URL (/healthz) se zaista proverava, sa vraćanjem prethodne verzije.
set -euo pipefail

INSTALLER_VERSION=8

# The whole script is read before anything runs (main at the bottom), so "curl | bash" can't
# execute a half-downloaded file, and commands read </dev/null so they never eat the script.

BASE_URL="${APOLO_URL:-https://update.apolopanel.com}"
DRY="${APOLO_INSTALL_DRY:-0}"
R="${APOLO_INSTALL_ROOT:-}" # filesystem root prefix (dry runs); empty on a real server

PORT=""
EMAIL=""
ZIP=""
UNINSTALL=0
PURGE=0
FORCE_OS=0
UPGRADE=0

die() { echo "Greška: $*" >&2; exit 1; }
say() { echo "==> $*"; }
warn() { echo "Upozorenje: $*" >&2; }
# progress PERCENT STEP: a machine-readable line for the panel's updater (APOLO_PROGRESS=1).
progress() { [ "${APOLO_PROGRESS:-0}" = 1 ] && printf '::apolo-progress %s %s\n' "$1" "$2"; return 0; }

usage() {
  cat <<'HELP'
APOLO instaler

  curl -fsSL https://update.apolopanel.com/apolo-install.sh | bash
  curl -fsSL https://update.apolopanel.com/apolo-install.sh | bash -s -- --port 8443 --email ja@primer.rs

Opcije:
  --port N         port panela (podrazumevano 8443)
  --email ADRESA   e-pošta prvog administratora (podrazumevano root@<hostname>)
  --zip PUTANJA    instaliraj iz lokalnog apolo.zip umesto preuzimanja
  --uninstall      ukloni APOLO (servisi, program, /etc/apolo); /var/lib/apolo ostaje
  --purge          uz --uninstall: obriši i /var/lib/apolo i APOLO podešavanja sajtova
  --upgrade        samo nadogradnja (odbija prvu instalaciju); ovako ga pokreće panel
  --base-url URL   preuzmi paket iz drugog foldera (i ažuriraj panel odatle)
  --force-os       ne proveravaj operativni sistem (samo za testiranje)
HELP
}

parse_args() {
  while [ $# -gt 0 ]; do
    case "$1" in
      --port) PORT="${2:-}"; shift 2 ;;
      --port=*) PORT="${1#*=}"; shift ;;
      --email) EMAIL="${2:-}"; shift 2 ;;
      --email=*) EMAIL="${1#*=}"; shift ;;
      --zip) ZIP="${2:-}"; shift 2 ;;
      --zip=*) ZIP="${1#*=}"; shift ;;
      --base-url) BASE_URL="${2:-}"; shift 2 ;; # testing: download from another folder
      --base-url=*) BASE_URL="${1#*=}"; shift ;;
      --uninstall) UNINSTALL=1; shift ;;
      --purge) PURGE=1; shift ;;
      --upgrade) UPGRADE=1; shift ;;
      --force-os) FORCE_OS=1; shift ;;
      -h|--help) usage; exit 0 ;;
      *) die "nepoznata opcija: $1 (pogledaj --help)" ;;
    esac
  done
  BASE_URL="${BASE_URL%/}"
  case "$BASE_URL" in https://?*|http://?*) ;; *) die "--base-url mora biti adresa foldera, npr. https://update.apolopanel.com" ;; esac
  case "$BASE_URL" in *[[:space:]\'\"\;\`\$\|\\\<\>\&]*) die "neispravna adresa: $BASE_URL" ;; esac
  if [ -n "$PORT" ]; then
    case "$PORT" in ''|*[!0-9]*) die "port mora biti broj" ;; esac
    [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "port van opsega (1-65535)"
    case "$PORT" in 80|443) die "portovi 80 i 443 su za sajtove; izaberi drugi port za panel" ;; esac
  fi
  if [ -n "$EMAIL" ]; then
    case "$EMAIL" in *@*.*) ;; *) die "neispravna e-pošta: $EMAIL" ;; esac
    case "$EMAIL" in *[[:space:]\'\"\;\`\$]*) die "neispravna e-pošta: $EMAIL" ;; esac
  fi
  [ "$PURGE" = 1 ] && [ "$UNINSTALL" != 1 ] && die "--purge ide samo uz --uninstall"
  return 0
}

# ---------------------------------------------------------------- helpers

# run CMD…: a command that changes the system (printed in a dry run).
run() {
  if [ "$DRY" = 1 ]; then
    printf '    [dry] %s\n' "$*"
    return 0
  fi
  "$@" </dev/null
}

# quiet CMD…: like run, output only on failure.
quiet() {
  if [ "$DRY" = 1 ]; then
    printf '    [dry] %s\n' "$*"
    return 0
  fi
  local out
  if ! out="$("$@" </dev/null 2>&1)"; then
    printf '%s\n' "$out" | tail -n 20 >&2
    return 1
  fi
}

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

# own USER:GROUP MODE PATH…: ownership and mode (ownership printed in a dry run: the user may not exist).
own() {
  local who="$1" mode="$2"
  shift 2
  if [ "$DRY" = 1 ]; then
    printf '    [dry] chown %s %s; chmod %s\n' "$who" "$*" "$mode"
    chmod "$mode" "$@"
  else
    chown "$who" "$@"
    chmod "$mode" "$@"
  fi
}

# mkdirs USER:GROUP MODE DIR…
mkdirs() {
  local who="$1" mode="$2"
  shift 2
  local d
  for d in "$@"; do
    mkdir -p "$R$d"
    own "$who" "$mode" "$R$d"
  done
}

# install_file SRC DEST MODE: copy only when different; returns 0 when it changed.
install_file() {
  local src="$1" dst="$R$2" mode="$3"
  mkdir -p "$(dirname "$dst")"
  if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
    chmod "$mode" "$dst"
    return 1
  fi
  cp "$src" "$dst.apolo-new"
  chmod "$mode" "$dst.apolo-new"
  mv -f "$dst.apolo-new" "$dst"
  return 0
}

svc() { run systemctl "$@"; }
FIRST=0

firewall() { # firewall add|remove PORT
  local action="$1" p="$2" what
  [ "$action" = add ] && what="otvoren" || what="zatvoren"
  if [ "$DRY" = 1 ]; then
    run firewall-cmd -q --permanent "--${action}-port=${p}/tcp"
    return 0
  fi
  if has firewall-cmd && firewall-cmd --state >/dev/null 2>&1 </dev/null; then
    firewall-cmd -q --permanent "--${action}-port=${p}/tcp" </dev/null 2>/dev/null || true
    FW_RELOAD=1
    say "firewalld: port $p $what"
  elif has ufw && ufw status 2>/dev/null </dev/null | grep -q "Status: active"; then
    if [ "$action" = add ]; then ufw allow "$p/tcp" >/dev/null </dev/null; else ufw delete allow "$p/tcp" >/dev/null </dev/null || true; fi
    say "ufw: port $p $what"
  fi
}
FW_RELOAD=0
firewall_reload() {
  if [ "$DRY" = 1 ]; then run firewall-cmd -q --reload; return 0; fi
  [ "$FW_RELOAD" = 1 ] && firewall-cmd -q --reload </dev/null 2>/dev/null || true
  FW_RELOAD=0
}

env_get() { # env_get KEY: value from /etc/apolo/apolo.env ("" when unset or no file)
  [ -f "$R/etc/apolo/apolo.env" ] || return 0
  sed -n "s/^$1=//p" "$R/etc/apolo/apolo.env" | tail -n 1
}

env_set() { # env_set KEY VALUE
  local f="$R/etc/apolo/apolo.env"
  if grep -q "^$1=" "$f" 2>/dev/null; then
    sed -i "s|^$1=.*|$1=$2|" "$f"
  else
    echo "$1=$2" >> "$f"
  fi
}

port_of_listen() { # ":8443" / "0.0.0.0:8443" → 8443
  local l="${1:-}"
  echo "${l##*:}"
}

# ---------------------------------------------------------------- operating system

OS_ID=""
OS_NAME=""
EL=""

detect_os() {
  local id="" like="" ver="" name=""
  if [ -r /etc/os-release ]; then
    # shellcheck disable=SC1091
    id="$(. /etc/os-release && echo "${ID:-}")"
    like="$(. /etc/os-release && echo "${ID_LIKE:-}")"
    ver="$(. /etc/os-release && echo "${VERSION_ID:-}")"
    name="$(. /etc/os-release && echo "${PRETTY_NAME:-$ID}")"
  fi
  OS_ID="$id"
  OS_NAME="${name:-nepoznat sistem}"
  EL="${ver%%.*}"
  case "$id" in
    almalinux|rocky|rhel) ;;
    *)
      if [ "$FORCE_OS" = 1 ]; then
        EL="${APOLO_FORCE_EL:-9}"
        warn "$OS_NAME nije podržan; --force-os: nastavljam kao da je EL$EL (samo za testiranje)"
        return 0
      fi
      case " $like " in
        *" rhel "*|*" centos "*|*" fedora "*)
          die "$OS_NAME nije podržan. APOLO radi na AlmaLinux, Rocky Linux i RHEL 9 i 10." ;;
      esac
      die "$OS_NAME nije podržan. APOLO radi na AlmaLinux, Rocky Linux i RHEL 9 i 10 (instaliraj server sa jednim od njih)." ;;
  esac
  case "$EL" in
    9|10) ;;
    *)
      [ "$FORCE_OS" = 1 ] || die "$OS_NAME nije podržan: potrebna je verzija 9 ili 10."
      EL="${APOLO_FORCE_EL:-9}"
      warn "--force-os: nastavljam kao EL$EL" ;;
  esac
}

# cpu_v3: 0 when the CPU runs x86-64-v3 code (glibc's ld.so knows best; /proc/cpuinfo otherwise).
cpu_v3() {
  if [ -n "${APOLO_TEST_CPU_V3:-}" ]; then [ "$APOLO_TEST_CPU_V3" = 1 ]; return; fi # testing
  local ld out
  for ld in /lib64/ld-linux-x86-64.so.2 /usr/lib64/ld-linux-x86-64.so.2; do
    [ -x "$ld" ] || continue
    out="$("$ld" --help 2>/dev/null </dev/null || true)"
    case "$out" in
      *"x86-64-v3 (supported"*) return 0 ;;
      *"x86-64-v3"*) return 1 ;;
    esac
  done
  local flags f
  flags=" $(grep -m 1 '^flags' /proc/cpuinfo 2>/dev/null | cut -d: -f2) "
  for f in avx avx2 bmi1 bmi2 f16c fma abm movbe xsave; do
    case "$flags" in *" $f "*) ;; *) return 1 ;; esac
  done
  return 0
}

# check_cpu: RHEL 10 / Rocky 10 / AlmaLinux 10 are built for x86-64-v3; AlmaLinux also has an
# x86_64_v2 build for older CPUs, but Remi (PHP) and EPEL packages may not exist or work there.
check_cpu() {
  [ "$EL" = 10 ] || return 0
  cpu_v3 && return 0
  local arch="${APOLO_TEST_RPM_ARCH:-}"
  if [ -z "$arch" ] && has rpm; then
    arch="$(rpm -q --qf '%{ARCH}\n' glibc 2>/dev/null </dev/null | head -n 1 || true)"
  fi
  if [ "$arch" = x86_64_v2 ]; then
    warn "procesor ne podržava x86-64-v3, a ovo je AlmaLinux 10 za x86_64_v2. Nije sigurno da Remi (PHP) i EPEL imaju pakete za tu varijantu; ako dnf ne nađe PHP ili PHP javi \"Illegal instruction\", koristi VPS čiji procesor podržava x86-64-v3 (KVM/Proxmox: tip procesora \"host\" ili \"x86-64-v3\") ili AlmaLinux 9."
    return 0
  fi
  if [ "$FORCE_OS" = 1 ]; then
    warn "procesor ne podržava x86-64-v3, a EL10 ga zahteva; --force-os: nastavljam (samo za testiranje)"
    return 0
  fi
  die "procesor ne podržava x86-64-v3, a $OS_NAME je napravljen za x86-64-v3 (programi iz paketa neće raditi). Izaberi VPS sa novijim tipom procesora (KVM/Proxmox: tip procesora \"host\" ili \"x86-64-v3\"), AlmaLinux 10 za x86_64_v2 ili verziju 9."
}

# check_sshd: APOLO's SSH/SFTP rules for panel accounts are drop-ins in /etc/ssh/sshd_config.d
# (00-apolo.conf, 01-apolo-users.conf). sshd only reads them through an Include in sshd_config,
# and they only win when that Include comes before any Match block (EL9/EL10 default: line 1).
check_sshd() {
  local f="$R/etc/ssh/sshd_config" line kw seen_match=0
  [ -f "$f" ] || return 0
  while IFS= read -r line || [ -n "$line" ]; do
    line="${line#"${line%%[![:space:]]*}"}"
    case "$line" in ''|'#'*) continue ;; esac
    kw="${line%%[[:space:]=]*}"
    case "$kw" in
      [Mm][Aa][Tt][Cc][Hh]) seen_match=1 ;;
      [Ii][Nn][Cc][Ll][Uu][Dd][Ee])
        case "$line" in
          *sshd_config.d/*)
            [ "$seen_match" = 1 ] && warn "/etc/ssh/sshd_config uključuje sshd_config.d tek posle Match bloka: pravila tog bloka imaju prednost nad SSH/SFTP pravilima naloga iz panela. Premesti red \"Include /etc/ssh/sshd_config.d/*.conf\" na vrh fajla i pokreni: sshd -t && systemctl reload sshd"
            return 0 ;;
        esac ;;
    esac
  done < "$f"
  warn "/etc/ssh/sshd_config ne uključuje /etc/ssh/sshd_config.d: SSH/SFTP pravila naloga iz panela neće važiti. Dodaj na vrh fajla red \"Include /etc/ssh/sshd_config.d/*.conf\" i pokreni: sshd -t && systemctl reload sshd"
}

# ---------------------------------------------------------------- uninstall

uninstall() {
  say "Uklanjam APOLO"
  local old_port sites=0
  old_port="$(port_of_listen "$(env_get APOLO_LISTEN)")"
  svc disable --now apolo-api.service apolo-agent.service >/dev/null 2>&1 || true
  # v2 (Node.js) service, if it is still around
  [ -f "$R/etc/systemd/system/apolo.service" ] && { svc disable --now apolo.service >/dev/null 2>&1 || true; rm -f "$R/etc/systemd/system/apolo.service"; }
  rm -f "$R/etc/systemd/system/apolo-api.service" "$R/etc/systemd/system/apolo-agent.service"
  rm -rf "$R/etc/systemd/system/apolo-api.service.d"
  rm -f "$R/usr/local/bin/apolo"
  rm -rf "$R/var/lib/apolo-update/stage" # a downloaded update that wasn't installed
  if [ "$PURGE" = 1 ]; then
    rm -rf "$R/opt/apolo"
  else
    # Node.js verzije instalirane iz panela (/opt/apolo/node) ostaju: aplikacije sajtova ih koriste.
    find "$R/opt/apolo" -mindepth 1 -maxdepth 1 ! -name node -exec rm -rf {} + 2>/dev/null || true
    rmdir "$R/opt/apolo" 2>/dev/null || true
  fi
  [ -n "$old_port" ] && firewall remove "$old_port"
  firewall_reload

  if [ -d "$R/etc/apolo/php" ] || [ -d "$R/etc/apolo/node" ]; then
    sites="$( { find "$R/etc/apolo/php" "$R/etc/apolo/node" -mindepth 1 -maxdepth 1 2>/dev/null || true; } | wc -l | tr -d ' ')"
  fi
  if [ "$PURGE" = 1 ]; then
    # Per-site PHP-FPM / Node.js units and the Nginx configs APOLO wrote. Files in /home,
    # databases and the installed packages stay.
    local u
    if [ "$DRY" != 1 ]; then
      for u in $(systemctl list-units --all --plain --no-legend 'apolo-php@*' 'apolo-node@*' 2>/dev/null </dev/null | awk '{print $1}'); do
        systemctl disable --now "$u" >/dev/null 2>&1 </dev/null || true
      done
    else
      run systemctl disable --now 'apolo-php@*' 'apolo-node@*'
    fi
    # Network rules of Settings → Security hardening (nftables table inet apolo).
    if [ "$DRY" = 1 ]; then
      run systemctl disable --now apolo-nft.service
    else
      systemctl disable --now apolo-nft.service >/dev/null 2>&1 </dev/null || true
      ! has nft || nft delete table inet apolo >/dev/null 2>&1 </dev/null || true
    fi
    rm -rf "$R/etc/systemd/system/apolo-php@.service" "$R/etc/systemd/system/apolo-node@.service" "$R/etc/systemd/system/apolo-nft.service" \
      "$R"/etc/systemd/system/apolo-php@*.service.d "$R"/etc/systemd/system/apolo-node@*.service.d
    rm -f "$R"/etc/nginx/conf.d/apolo-*.conf "$R/etc/nginx/conf.d/00-apolo.conf"
    if [ "$DRY" = 1 ] || { has nginx && nginx -t >/dev/null 2>&1 </dev/null; }; then
      svc reload nginx >/dev/null 2>&1 || true
    fi
    rm -rf "$R/etc/apolo" "$R/var/lib/apolo" "$R/var/log/apolo" "$R/var/cache/apolo" "$R/usr/share/apolo" "$R/var/lib/apolo-tools" "$R/var/lib/apolo-update"
    # Webmail (its program went with /usr/share/apolo) and the spam filter's rules. Dovecot keeps
    # running: a missing per-domain Sieve file is skipped, and the webmail master user is emptied
    # (not removed) because Dovecot's config still names the file.
    rm -rf "$R/var/lib/apolo-webmail" "$R/var/lib/apolo-mail"
    rm -f "$R/etc/logrotate.d/apolo" "$R"/etc/logrotate.d/apolo-*
    if [ -f "$R/etc/dovecot/apolo-master-users" ]; then : > "$R/etc/dovecot/apolo-master-users"; fi
    if [ "$DRY" = 1 ]; then run userdel apolo; elif id apolo >/dev/null 2>&1; then userdel apolo </dev/null || true; fi
    if [ "$DRY" = 1 ]; then run userdel apolo-tools; elif id apolo-tools >/dev/null 2>&1; then userdel apolo-tools </dev/null || true; fi
    if [ "$DRY" = 1 ]; then run userdel apolo-webmail; elif id apolo-webmail >/dev/null 2>&1; then userdel apolo-webmail </dev/null || true; fi
    svc daemon-reload
    say "APOLO je uklonjen, sa stanjem panela i podešavanjima sajtova"
    echo "    Sajtovi u /home, baze podataka i instalirani paketi (Nginx, PHP, MariaDB…) su ostali."
  else
    if [ "$sites" -gt 0 ] 2>/dev/null; then
      # Keep what the websites need to keep running (PHP-FPM / Node.js configs and units).
      find "$R/etc/apolo" -mindepth 1 -maxdepth 1 ! -name php ! -name node -exec rm -rf {} + 2>/dev/null || true
      svc daemon-reload
      say "APOLO je uklonjen"
      echo "    Sajtovi i dalje rade: njihova PHP-FPM / Node.js podešavanja su ostala u /etc/apolo/php i /etc/apolo/node."
      if [ -d "$R/opt/apolo/node" ]; then echo "    Node.js verzije instalirane iz panela su ostale u /opt/apolo/node."; fi
    else
      rm -rf "$R/etc/apolo"
      svc daemon-reload
      say "APOLO je uklonjen"
    fi
    echo "    Stanje panela je sačuvano u /var/lib/apolo (ponovna instalacija ga koristi)."
    echo "    Potpuno uklanjanje: bash apolo-install.sh --uninstall --purge"
  fi
}

# ---------------------------------------------------------------- packages

DNF="dnf"
dnf_install() { # dnf_install PKG…: all must install
  quiet "$DNF" install -y -q "$@" || die "dnf nije instalirao: $*"
}
dnf_install_optional() { # dnf_install_optional PKG…: what's unavailable is skipped
  quiet "$DNF" install -y -q --setopt=strict=0 --skip-broken "$@" || warn "dnf: neki paketi nisu instalirani: $*"
}
rpm_has() { [ "$DRY" != 1 ] && rpm -q "$1" >/dev/null 2>&1 </dev/null; }

php_packages() { # php_packages 84
  local v="$1" p="php$1-php"
  echo "$p-fpm $p-cli $p-common $p-opcache $p-mysqlnd $p-pdo $p-mbstring $p-intl $p-gd $p-xml $p-soap $p-bcmath $p-gmp $p-sodium $p-process $p-pgsql $p-pecl-zip $p-pecl-redis6 $p-pecl-apcu $p-pecl-imagick-im7"
}

REDIS_SVC=redis

install_stack() {
  say "Instaliram web stek (dnf): EPEL, Remi, Nginx, PHP 8.4 i 8.3, MariaDB, Redis, certbot (može da potraje nekoliko minuta)"
  [ "$DRY" = 1 ] || has dnf || die "nema dnf"
  [ "$DRY" = 1 ] || has rpm || die "nema rpm"

  # EPEL (+ CRB, needed by some EPEL packages)
  if ! rpm_has epel-release; then
    if [ "$OS_ID" = rhel ]; then
      run subscription-manager repos --enable "codeready-builder-for-rhel-$EL-$(uname -m)-rpms" >/dev/null 2>&1 || warn "CodeReady Builder repozitorijum nije uključen"
      dnf_install "https://dl.fedoraproject.org/pub/epel/epel-release-latest-$EL.noarch.rpm"
    else
      dnf_install dnf-plugins-core epel-release
      quiet "$DNF" config-manager --set-enabled crb || warn "CRB repozitorijum nije uključen"
    fi
  fi
  # Remi (PHP versions side by side, /opt/remi/phpXX)
  rpm_has remi-release || dnf_install "https://rpms.remirepo.net/enterprise/remi-release-$EL.rpm"

  # Newer module streams on EL9 (EL10 has no modules), only before the first install.
  if [ "$EL" = 9 ]; then
    rpm_has nginx || quiet "$DNF" module enable -y nginx:1.24 || true
    rpm_has mariadb-server || quiet "$DNF" module enable -y mariadb:10.11 || true
  fi

  # (curl: EL9 ships curl-minimal, which conflicts with the curl package; it's already there)
  dnf_install nginx mariadb-server cronie tar unzip rsync openssl policycoreutils-python-utils
  # dnf-plugins-core: `dnf needs-restarting` for Settings → System updates (also on servers that
  # had EPEL before APOLO, where the EPEL step above didn't install it)
  dnf_install_optional certbot quota acl git nftables dnf-plugins-core
  [ "$DRY" = 1 ] || has certbot || warn "certbot nije instaliran: Let's Encrypt sertifikati neće raditi dok ga ne instaliraš (dnf install certbot)"
  # Redis: EL10 ships Valkey (Redis-compatible) instead.
  if [ "$EL" -ge 10 ] 2>/dev/null; then
    dnf_install valkey
    dnf_install_optional valkey-compat-redis
    REDIS_SVC=valkey
  else
    dnf_install redis
    REDIS_SVC=redis
  fi

  # PHP 8.4 and 8.3 (FPM + CLI). The distribution's php-fpm services stay off:
  # APOLO runs one FPM master per website (apolo-php@<site>).
  # shellcheck disable=SC2046
  dnf_install php84-php-fpm php84-php-cli php83-php-fpm php83-php-cli
  # shellcheck disable=SC2046
  dnf_install_optional $(php_packages 84) $(php_packages 83) php84-syspaths
}

install_wpcli() {
  local url="https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar"
  if [ -x "$R/usr/local/bin/wp" ] && [ "${APOLO_WPCLI_UPDATE:-0}" != 1 ]; then
    say "WP-CLI je već instaliran"
    return 0
  fi
  say "Instaliram WP-CLI"
  if [ "$DRY" = 1 ]; then
    run curl -fsSL "$url" -o /usr/local/bin/wp
    return 0
  fi
  curl -fsSL --retry 3 "$url" -o "$TMP/wp-cli.phar" </dev/null || { warn "WP-CLI nije preuzet ($url)"; return 0; }
  if curl -fsSL --retry 3 "$url.sha512" -o "$TMP/wp-cli.phar.sha512" </dev/null; then
    [ "$(sha512sum "$TMP/wp-cli.phar" | awk '{print $1}')" = "$(tr -d ' \n' < "$TMP/wp-cli.phar.sha512")" ] || { warn "WP-CLI: sha512 se ne poklapa, nije instaliran"; return 0; }
  fi
  case "$(head -c 64 "$TMP/wp-cli.phar" | tr -d '\0')" in *php*) ;; *) warn "WP-CLI: preuzeti fajl nije phar"; return 0 ;; esac
  install -m 0755 "$TMP/wp-cli.phar" "$R/usr/local/bin/wp"
}

install_composer() {
  local url="https://getcomposer.org/download/latest-stable/composer.phar"
  if [ -x "$R/usr/local/bin/composer" ] && [ "${APOLO_COMPOSER_UPDATE:-0}" != 1 ]; then
    say "Composer je već instaliran"
    return 0
  fi
  say "Instaliram Composer"
  if [ "$DRY" = 1 ]; then
    run curl -fsSL "$url" -o /usr/local/bin/composer
    return 0
  fi
  curl -fsSL --retry 3 "$url" -o "$TMP/composer.phar" </dev/null || { warn "Composer nije preuzet ($url)"; return 0; }
  if curl -fsSL --retry 3 "$url.sha256sum" -o "$TMP/composer.phar.sha256sum" </dev/null; then
    [ "$(sha256sum "$TMP/composer.phar" | awk '{print $1}')" = "$(awk '{print $1}' "$TMP/composer.phar.sha256sum")" ] || { warn "Composer: sha256 se ne poklapa, nije instaliran"; return 0; }
  fi
  case "$(head -c 64 "$TMP/composer.phar" | tr -d '\0')" in *php*) ;; *) warn "Composer: preuzeti fajl nije phar"; return 0 ;; esac
  install -m 0755 "$TMP/composer.phar" "$R/usr/local/bin/composer"
}

secure_mariadb() {
  say "MariaDB: uklanjam anonimne korisnike i test bazu"
  local sql="DELETE FROM mysql.global_priv WHERE User=''; DELETE FROM mysql.global_priv WHERE User='root' AND Host NOT IN ('localhost','127.0.0.1','::1'); DROP DATABASE IF EXISTS test; DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'; FLUSH PRIVILEGES;"
  if [ "$DRY" = 1 ]; then
    run mysql -e "$sql"
    return 0
  fi
  local i
  for i in $(seq 1 20); do mysqladmin ping >/dev/null 2>&1 </dev/null && break; sleep 1; done
  mysql -e "$sql" </dev/null 2>/dev/null || warn "MariaDB nije obezbeđena (root nema pristup preko unix socket-a?)"
}

# ---------------------------------------------------------------- SELinux

selinux() {
  local mode="Disabled"
  if [ "$DRY" = 1 ]; then
    mode="${APOLO_DRY_SELINUX:-Enforcing}"
  elif has getenforce; then
    mode="$(getenforce 2>/dev/null </dev/null || echo Disabled)"
  fi
  [ "$mode" = Enforcing ] || { say "SELinux: $mode, bez podešavanja"; return 0; }
  say "SELinux: dozvole za Nginx i PHP-FPM (sajtovi u /home, PHP-FPM socket-i, ACME, mail() iz PHP-a)"
  # php-fpm runs as httpd_t: httpd_can_sendmail lets PHP's mail() hand mail to Postfix.
  quiet setsebool -P httpd_can_network_connect=1 httpd_read_user_content=1 httpd_enable_homedirs=1 httpd_can_sendmail=1 || warn "setsebool nije uspeo"
  fctx httpd_sys_rw_content_t '/home/[^/]+/[^/.][^/]*\.[^/]+(/.*)?' # website folders /home/<user>/<domain>
  fctx ssh_home_t '/home/[^/]+/\.ssh(/.*)?'                          # keep SSH keys readable for sshd
  fctx httpd_var_run_t '/run/apolo-php(/.*)?'
  fctx httpd_log_t '/var/log/apolo(/.*)?'      # site logs (Nginx, Apache, PHP-FPM)
  fctx httpd_cache_t '/var/cache/apolo(/.*)?'  # Nginx page cache
  fctx httpd_sys_content_t '/var/lib/apolo/acme(/.*)?'
  fctx httpd_sys_content_t '/usr/share/apolo/pages(/.*)?'
  fctx httpd_sys_content_t '/usr/share/apolo/tools(/.*)?'     # phpMyAdmin / Adminer (downloaded on first use)
  fctx httpd_sys_rw_content_t '/var/lib/apolo-tools(/.*)?'
  # Webmail (Roundcube, installed from Services): its tree read-only for PHP-FPM (httpd_t), its own
  # folder writable (sessions, SQLite database, sign-in links). The PHP-FPM socket is in /run/apolo-php.
  fctx httpd_sys_content_t '/usr/share/apolo/webmail(/.*)?'
  fctx httpd_sys_rw_content_t '/var/lib/apolo-webmail(/.*)?'
  # The spam filter's Sieve rules per domain, read by Dovecot (LMTP) and compiled by apolo-agent.
  fctx dovecot_var_lib_t '/var/lib/apolo-mail(/.*)?'
  # Nginx serves the database tools on 127.0.0.1:8091 for the panel's /tools/ proxy.
  quiet semanage port -a -t http_port_t -p tcp 8091 || quiet semanage port -m -t http_port_t -p tcp 8091 || true
  # Postfix hands incoming mail to Rspamd's milter on 127.0.0.1:11332 (like OpenDKIM on 8891);
  # Dovecot's ManageSieve (webmail's filters) listens on 127.0.0.1:4190, sieve_port_t in the policy.
  quiet semanage port -a -t milter_port_t -p tcp 11332 || quiet semanage port -m -t milter_port_t -p tcp 11332 || true
  quiet semanage port -a -t sieve_port_t -p tcp 4190 || true
  local d dirs="/var/lib/apolo/acme /usr/share/apolo /run/apolo-php /var/log/apolo /var/cache/apolo /var/lib/apolo-webmail /var/lib/apolo-mail"
  [ "$FIRST" = 1 ] && dirs="/home $dirs" # existing websites; new ones are labeled by apolo-agent
  for d in $dirs; do
    if [ "$DRY" = 1 ] || [ -e "$d" ]; then quiet restorecon -R "$d" || true; fi
  done
}

fctx() { # fctx TYPE REGEX: add or update a local file-context rule
  if [ "$DRY" = 1 ]; then run semanage fcontext -a -t "$1" "$2"; return 0; fi
  semanage fcontext -a -t "$1" "$2" </dev/null >/dev/null 2>&1 || semanage fcontext -m -t "$1" "$2" </dev/null >/dev/null 2>&1 || warn "semanage fcontext $2 nije uspeo"
}

# ---------------------------------------------------------------- built-in units (used when the zip has none)

write_default_units() { # write_default_units DIR
  mkdir -p "$1"
  [ -f "$1/apolo-agent.service" ] || cat > "$1/apolo-agent.service" <<'UNIT'
[Unit]
Description=APOLO agent (privileged operations for the panel)
After=network-online.target
Wants=network-online.target
Before=apolo-api.service

[Service]
Type=simple
EnvironmentFile=-/etc/apolo/apolo.env
ExecStart=/usr/local/bin/apolo agent
RuntimeDirectory=apolo
RuntimeDirectoryMode=0750
Restart=always
RestartSec=2
TimeoutStopSec=60
KillMode=mixed
UMask=0022
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
UNIT
  [ -f "$1/apolo-api.service" ] || cat > "$1/apolo-api.service" <<'UNIT'
[Unit]
Description=APOLO panel (web UI and API)
After=network-online.target apolo-agent.service
Wants=network-online.target apolo-agent.service

[Service]
Type=simple
User=apolo
Group=apolo
EnvironmentFile=-/etc/apolo/apolo.env
ExecStart=/usr/local/bin/apolo api
Restart=always
RestartSec=2
TimeoutStopSec=30
UMask=0027
LimitNOFILE=65536
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/apolo /etc/apolo/tls
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
AmbientCapabilities=

[Install]
WantedBy=multi-user.target
UNIT
  return 0
}

# ---------------------------------------------------------------- install

TMP=""
cleanup() { [ -n "$TMP" ] && rm -rf "$TMP"; return 0; }

download() {
  if [ -n "$ZIP" ]; then
    [ -f "$ZIP" ] || die "ne postoji: $ZIP"
    cp "$ZIP" "$TMP/apolo.zip"
    [ -f "$ZIP.sha256" ] && cp "$ZIP.sha256" "$TMP/apolo.zip.sha256"
  else
    say "Preuzimam $BASE_URL/apolo.zip"
    curl -fSL --retry 3 --progress-bar "$BASE_URL/apolo.zip" -o "$TMP/apolo.zip" </dev/null || die "preuzimanje nije uspelo"
    curl -fsSL --retry 3 "$BASE_URL/apolo.zip.sha256" -o "$TMP/apolo.zip.sha256" </dev/null || die "nema $BASE_URL/apolo.zip.sha256"
  fi
  if [ -f "$TMP/apolo.zip.sha256" ]; then
    local want got
    want="$(awk '{print $1; exit}' "$TMP/apolo.zip.sha256")"
    got="$(sha256sum "$TMP/apolo.zip" | awk '{print $1}')"
    [ -n "$want" ] && [ "$want" = "$got" ] || die "sha256 se ne poklapa, apolo.zip je oštećen ili zamenjen"
    say "sha256 u redu"
  else
    warn "nema apolo.zip.sha256, paket nije proveren"
  fi
  unzip -q "$TMP/apolo.zip" -d "$TMP/x" </dev/null || die "apolo.zip se ne raspakuje"
  [ -f "$TMP/x/apolo/VERSION" ] && [ -f "$TMP/x/apolo/apolo" ] || die "apolo.zip nema očekivani sadržaj (apolo/apolo, apolo/VERSION); da li je to paket za v3?"
  chmod 755 "$TMP/x/apolo/apolo"
  VERSION="$(tr -d ' \n\r' < "$TMP/x/apolo/VERSION")"
  case "$VERSION" in ''|*[!0-9A-Za-z.+_-]*) die "neispravna verzija u paketu: $VERSION" ;; esac
  local v
  v="$("$TMP/x/apolo/apolo" version </dev/null 2>/dev/null || true)"
  [ -n "$v" ] || die "program iz paketa ne radi na ovom serveru ($(uname -m))"
  say "Paket: $v"
}

install_files() {
  local a="$TMP/x/apolo/assets"
  SAME_RELEASE=0

  # user and folders
  if [ "$DRY" = 1 ]; then
    run useradd --system --home-dir /var/lib/apolo --no-create-home --shell /sbin/nologin apolo
  elif ! id apolo >/dev/null 2>&1; then
    useradd --system --home-dir /var/lib/apolo --no-create-home --shell "$(command -v nologin || echo /sbin/nologin)" --comment "APOLO panel" apolo </dev/null
  else
    usermod -d /var/lib/apolo apolo </dev/null 2>/dev/null || true # v2 used /opt/apolo
  fi
  mkdirs root:apolo 0750 /etc/apolo
  mkdirs apolo:apolo 0750 /etc/apolo/tls
  # 0711: Nginx must reach /var/lib/apolo/acme (Let's Encrypt HTTP-01); nothing can be listed.
  mkdirs apolo:apolo 0711 /var/lib/apolo
  mkdirs root:root 0755 /var/lib/apolo/acme
  # Nginx workers (user nginx) must enter the page cache (/var/cache/apolo/nginx/<site>) and
  # reopen the site logs after logrotate: logs root:nginx 0750 (other accounts can't read
  # them), cache root 0755. (Up to 0.4.0 both were root:apolo 0750: the page cache broke.)
  if [ "$DRY" = 1 ] || getent group nginx >/dev/null 2>&1 </dev/null; then
    mkdirs root:nginx 0750 /var/log/apolo
  else
    mkdirs root:root 0755 /var/log/apolo
  fi
  mkdirs root:root 0755 /var/cache/apolo
  mkdirs root:root 0755 /opt/apolo /opt/apolo/releases /usr/share/apolo /usr/share/apolo/pages /usr/local/bin

  # binary: /opt/apolo/releases/<version>/apolo, /opt/apolo/current → it, /usr/local/bin/apolo → current
  PREV="$(readlink "$R/opt/apolo/current" 2>/dev/null || true)"
  REL="/opt/apolo/releases/$VERSION"
  if [ -e "$R$REL" ]; then
    if [ "$R$REL" = "$R$PREV" ] && cmp -s "$TMP/x/apolo/apolo" "$R$REL/apolo"; then
      SAME_RELEASE=1
    else
      REL="$REL-$(date +%Y%m%d-%H%M%S)"
    fi
  fi
  if [ "${SAME_RELEASE:-0}" != 1 ]; then
    mkdir -p "$R$REL"
    install -m 0755 "$TMP/x/apolo/apolo" "$R$REL/apolo"
    cp "$TMP/x/apolo/VERSION" "$R$REL/VERSION"
    [ -d "$a" ] && cp -r "$a" "$R$REL/assets"
    own root:root 0755 "$R$REL"
  fi
  ln -sfn "$REL" "$R/opt/apolo/current"
  ln -sfn /opt/apolo/current/apolo "$R/usr/local/bin/apolo"

  # systemd units
  [ -d "$a/systemd" ] || mkdir -p "$a/systemd"
  write_default_units "$a/systemd"
  UNITS_CHANGED=0
  # the previous unit files, for a rollback (restore_units)
  UNITS_BAK="$TMP/units-prev"
  mkdir -p "$UNITS_BAK"
  local u
  for u in apolo-agent.service apolo-api.service apolo-php@.service apolo-node@.service; do
    [ -f "$a/systemd/$u" ] || continue
    if [ -f "$R/etc/systemd/system/$u" ]; then cp -p "$R/etc/systemd/system/$u" "$UNITS_BAK/$u"; else : > "$UNITS_BAK/$u.new"; fi
    if install_file "$a/systemd/$u" "/etc/systemd/system/$u" 0644; then UNITS_CHANGED=1; fi
  done
  # a port below 1024 needs CAP_NET_BIND_SERVICE
  local dropin="$R/etc/systemd/system/apolo-api.service.d/10-port.conf"
  if [ "$PANEL_PORT" -lt 1024 ]; then
    mkdir -p "$(dirname "$dropin")"
    printf '# Written by the APOLO installer: the panel port %s is below 1024.\n[Service]\nCapabilityBoundingSet=CAP_NET_BIND_SERVICE\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n' "$PANEL_PORT" > "$dropin"
  else
    rm -f "$dropin"
  fi

  # Nginx: global settings + default server; "suspended"/"stopped" pages
  if [ -f "$a/nginx/00-apolo.conf" ]; then
    local conf="$TMP/00-apolo.conf"
    if [ -e /proc/net/if_inet6 ] && [ "$DRY" != 1 ]; then
      cp "$a/nginx/00-apolo.conf" "$conf"
    else
      grep -v 'apolo:ipv6' "$a/nginx/00-apolo.conf" > "$conf"
    fi
    local had=0
    [ -f "$R/etc/nginx/conf.d/00-apolo.conf" ] && { cp "$R/etc/nginx/conf.d/00-apolo.conf" "$TMP/00-apolo.conf.bak"; had=1; }
    if install_file "$conf" /etc/nginx/conf.d/00-apolo.conf 0644 && [ "$DRY" != 1 ]; then
      if ! nginx -t >/dev/null 2>&1 </dev/null; then
        nginx -t </dev/null 2>&1 | tail -n 5 >&2 || true
        if [ "$had" = 1 ]; then cp "$TMP/00-apolo.conf.bak" "$R/etc/nginx/conf.d/00-apolo.conf"; else rm -f "$R/etc/nginx/conf.d/00-apolo.conf"; fi
        warn "Nginx ne prihvata 00-apolo.conf; ostavljena je prethodna konfiguracija (apolo-agent će je napisati)"
      fi
    fi
  fi
  if [ -d "$a/pages" ]; then
    local f
    for f in "$a"/pages/*; do [ -f "$f" ] && install_file "$f" "/usr/share/apolo/pages/$(basename "$f")" 0644 || true; done
  fi

  # settings
  local envf="$R/etc/apolo/apolo.env"
  if [ ! -f "$envf" ] || ! grep -q '^APOLO_LISTEN=' "$envf"; then
    local old_tls_cert="" old_tls_key=""
    if [ -f "$envf" ]; then # v2 → v3: keep the port and the certificate
      old_tls_cert="$(env_get APOLO_TLS_CERT)"
      old_tls_key="$(env_get APOLO_TLS_KEY)"
      mv "$envf" "$envf.v2"
    fi
    cat > "$envf" <<ENV
# APOLO settings. After a change: systemctl restart apolo-agent apolo-api
APOLO_LISTEN=:$PANEL_PORT
APOLO_TLS_CERT=${old_tls_cert:-/etc/apolo/tls/panel.crt}
APOLO_TLS_KEY=${old_tls_key:-/etc/apolo/tls/panel.key}
APOLO_STATE=/var/lib/apolo/state.json
APOLO_AGENT_SOCK=/run/apolo/agent.sock
APOLO_API_USER=apolo
ENV
  else
    env_set APOLO_LISTEN ":$PANEL_PORT"
  fi
  # Where the panel looks for updates: the folder this installer came from (default
  # https://update.apolopanel.com/ or --base-url), unless the administrator set another one.
  # The old default (bedem.xd.rs/apolo, until 0.8.0) moves to the new one; a mirror the
  # administrator set stays.
  case "$(env_get APOLO_UPDATE_URL)" in
    https://bedem.xd.rs/apolo | https://bedem.xd.rs/apolo/) env_set APOLO_UPDATE_URL "https://update.apolopanel.com/" ;;
  esac
  [ -n "$(env_get APOLO_UPDATE_URL)" ] || env_set APOLO_UPDATE_URL "$BASE_URL/"
  own root:apolo 0640 "$envf"
  # the v2 certificate (if kept) must be readable by apolo-api
  local c k
  c="$(env_get APOLO_TLS_CERT)"
  k="$(env_get APOLO_TLS_KEY)"
  [ -f "$R$c" ] && own apolo:apolo 0644 "$R$c"
  [ -f "$R$k" ] && own apolo:apolo 0600 "$R$k"
  return 0
}

health() { # health PORT VERSION: 0 when https://127.0.0.1:PORT/healthz answers with VERSION
  local i out url="https://127.0.0.1:$1/healthz" tries=40
  if [ "$DRY" = 1 ] && [ -n "${APOLO_INSTALL_TEST_HEALTH:-}" ]; then # testing
    url="$APOLO_INSTALL_TEST_HEALTH"
    tries="${APOLO_INSTALL_TEST_HEALTH_TRIES:-40}"
  fi
  for i in $(seq 1 "$tries"); do
    out="$(curl -fsk --max-time 3 "$url" </dev/null 2>/dev/null || true)"
    case "$out" in *"\"version\":\"$2\""*) return 0 ;; esac
    sleep 1
  done
  return 1
}

restart_panel() { # restart apolo-agent and apolo-api (the health check tells whether it worked)
  if [ "$DRY" = 1 ]; then
    if [ -n "${APOLO_INSTALL_TEST_RESTART:-}" ]; then
      "$APOLO_INSTALL_TEST_RESTART" restart </dev/null || true
    else
      run systemctl restart apolo-agent.service apolo-api.service
    fi
    return 0
  fi
  # a unit that crash-looped (e.g. on a broken release before a rollback) may have hit the start
  # limit; "restart" is refused until it is reset
  quiet systemctl reset-failed apolo-agent.service apolo-api.service || true
  quiet systemctl restart apolo-agent.service || true
  quiet systemctl restart apolo-api.service || true
}

restore_units() { # rollback: the unit files of the previous release
  [ -n "${UNITS_BAK:-}" ] && [ -d "$UNITS_BAK" ] || return 0
  local f u
  for f in "$UNITS_BAK"/*; do
    [ -e "$f" ] || continue
    u="$(basename "$f")"
    case "$u" in
      *.new) rm -f "$R/etc/systemd/system/${u%.new}" ;;
      *) cp -p "$f" "$R/etc/systemd/system/$u" ;;
    esac
  done
  svc daemon-reload
}

main() {
  parse_args "$@"
  say "APOLO instaler v$INSTALLER_VERSION"
  if [ "$DRY" = 1 ]; then
    [ -n "$R" ] || R="$(mktemp -d /tmp/apolo-dry.XXXXXX)"
    say "Probni rad (APOLO_INSTALL_DRY=1): sistemske komande se samo ispisuju, fajlovi idu u $R"
  else
    [ "$(id -u)" = 0 ] || die "pokreni kao root"
    has systemctl || die "potreban je systemd"
  fi
  case "$(uname -m)" in x86_64) ;; *) die "paket je za x86_64, a ovaj server je $(uname -m)" ;; esac

  if [ "$UNINSTALL" = 1 ]; then
    uninstall
    exit 0
  fi

  detect_os
  say "Sistem: $OS_NAME (EL$EL)"
  check_cpu
  TMP="$(mktemp -d)"
  trap cleanup EXIT

  # tools for the download, before the rest of the stack
  if [ "$DRY" != 1 ]; then
    local missing=""
    has curl || missing="$missing curl"
    has unzip || missing="$missing unzip"
    has sha256sum || missing="$missing coreutils"
    [ -z "$missing" ] || dnf_install $missing
  fi
  download
  progress 30 verified

  # panel port: --port, else the configured one (v3 APOLO_LISTEN, v2 APOLO_PORT), else 8443
  local old_port
  old_port="$(port_of_listen "$(env_get APOLO_LISTEN)")"
  [ -n "$old_port" ] || old_port="$(env_get APOLO_PORT)"
  PANEL_PORT="${PORT:-${old_port:-8443}}"
  FIRST=0
  [ -s "$R/var/lib/apolo/state.json" ] || FIRST=1
  if [ "$UPGRADE" = 1 ] && [ "$FIRST" = 1 ]; then
    die "--upgrade: APOLO nije instaliran na ovom serveru (nema /var/lib/apolo/state.json); za prvu instalaciju pokreni instaler bez --upgrade"
  fi

  progress 40 packages
  install_stack
  install_wpcli
  install_composer

  # v2 (Node.js panel, apolo.service) is replaced by apolo-agent + apolo-api
  if [ -f "$R/etc/systemd/system/apolo.service" ]; then
    say "Uklanjam stari apolo.service (v2)"
    svc disable --now apolo.service >/dev/null 2>&1 || true
    rm -f "$R/etc/systemd/system/apolo.service"
  fi

  progress 60 files
  install_files
  selinux

  # firewall: websites and the panel
  firewall add 80
  firewall add 443
  [ -n "$old_port" ] && [ "$old_port" != "$PANEL_PORT" ] && firewall remove "$old_port"
  firewall add "$PANEL_PORT"
  firewall_reload

  say "Pokrećem servise"
  svc daemon-reload
  quiet systemctl enable --now nginx.service mariadb.service "$REDIS_SVC.service" crond.service || die "servisi web steka se ne pokreću (systemctl status nginx mariadb $REDIS_SVC)"
  [ "$FIRST" = 1 ] && secure_mariadb
  check_sshd

  # first install: the administrator (state file written as root, readable by apolo-api)
  local admin_user="" admin_pass=""
  if [ "$FIRST" = 1 ]; then
    local mail="${EMAIL:-root@$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo localhost)}"
    say "Pravim administratora admin ($mail)"
    if [ "$DRY" = 1 ]; then
      run /usr/local/bin/apolo admin create --username admin --email "$mail"
      admin_user=admin
      admin_pass="(probni rad: lozinka se pravi na pravom serveru)"
    else
      local out
      out="$(/usr/local/bin/apolo admin create --username admin --email "$mail" </dev/null)" || die "administrator nije napravljen"
      admin_user="$(printf '%s\n' "$out" | sed -n 's/^username: //p')"
      admin_pass="$(printf '%s\n' "$out" | sed -n 's/^password: //p')"
      [ -n "$admin_pass" ] || die "administrator nije napravljen (nema lozinke u izlazu)"
    fi
  fi

  quiet systemctl enable apolo-agent.service apolo-api.service || die "systemctl enable apolo-agent apolo-api nije uspeo"
  progress 80 restart
  restart_panel

  # health check (the new version must answer), rollback to the previous release on failure
  say "Čekam da se APOLO $VERSION pokrene"
  progress 88 health
  if [ "$DRY" = 1 ] && [ -z "${APOLO_INSTALL_TEST_HEALTH:-}" ]; then
    run curl -fsk "https://127.0.0.1:$PANEL_PORT/healthz"
  elif ! health "$PANEL_PORT" "$VERSION"; then
    [ "$DRY" = 1 ] || journalctl -u apolo-agent -u apolo-api -n 30 --no-pager </dev/null 2>/dev/null || true
    if [ -n "$PREV" ] && [ "$PREV" != "$REL" ] && [ -x "$R$PREV/apolo" ]; then
      progress 92 rollback
      local prev_version
      prev_version="$(tr -d ' \n\r' < "$R$PREV/VERSION" 2>/dev/null || true)"
      say "APOLO $VERSION se ne javlja; vraćam prethodnu verziju (${prev_version:-$(basename "$PREV")})"
      ln -sfn "$PREV" "$R/opt/apolo/current"
      restore_units
      restart_panel
      rm -rf "$R$REL"
      if [ -n "$prev_version" ] && ! health "$PANEL_PORT" "$prev_version"; then
        die "nova verzija se ne pokreće, a ne javlja se ni vraćena prethodna ($prev_version). Log: journalctl -u apolo-api -u apolo-agent"
      fi
      die "nova verzija se ne pokreće; vraćena je prethodna (${prev_version:-$(basename "$PREV")}). Log: journalctl -u apolo-api -u apolo-agent"
    fi
    die "APOLO se ne pokreće (log iznad: journalctl -u apolo-api -u apolo-agent)"
  fi

  # keep the 3 newest releases (never the current one)
  local cur d
  cur="$(readlink -f "$R/opt/apolo/current" 2>/dev/null || true)"
  # shellcheck disable=SC2012
  { ls -1dt "$R"/opt/apolo/releases/*/ 2>/dev/null || true; } | tail -n +4 | while read -r d; do
    [ "$(readlink -f "$d")" = "$cur" ] || rm -rf "$d"
  done

  # where to open the panel
  local ip host
  ip="$( { ip -4 route get 1.1.1.1 2>/dev/null </dev/null | sed -n 's/.* src \([0-9.]*\).*/\1/p' | head -n 1; } || true)"
  [ -n "$ip" ] || ip="$( { hostname -I 2>/dev/null </dev/null | awk '{print $1}'; } || true)"
  host="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo apolo)"

  echo
  say "APOLO $VERSION radi"
  echo
  echo "    Panel:  https://${ip:-$host}:$PANEL_PORT"
  if [ -n "$admin_pass" ]; then
    echo
    echo "    ┌──────────────────────────────────────────────────────────────"
    echo "    │  Korisničko ime:  $admin_user"
    echo "    │  Lozinka:         $admin_pass"
    echo "    │"
    echo "    │  Lozinka se prikazuje samo sada; nije sačuvana nigde u čitljivom obliku."
    echo "    │  Sačuvaj je i promeni u panelu (Podešavanja → Lozinka)."
    echo "    │  Nova lozinka kasnije: apolo admin reset-password --username admin"
    echo "    └──────────────────────────────────────────────────────────────"
  else
    echo "    Nadogradnja: administrator i podešavanja panela nisu menjani."
  fi
  echo
  echo "    Sertifikat panela je samopotpisan: pregledač će jednom pitati da ga prihvatiš."
  echo "    Portovi: 80 i 443 (sajtovi), $PANEL_PORT (panel)."
  echo "    Log: journalctl -u apolo-api -u apolo-agent -f    Podešavanja: /etc/apolo/apolo.env"
  echo "    Ažuriranja: Podešavanja → APOLO ažuriranja (izvor: $(env_get APOLO_UPDATE_URL))"
  progress 100 done
}

main "$@"
