#!/usr/bin/env bash
#
# apache-provision.sh — Provision an Apache virtual host (optionally with PHP
# and WordPress) on a Linux server, using mpm-itk so each vhost runs as its
# own dedicated Unix user.
#
# Cross-distro: detects the running OS and optimizes for it instead of assuming
# Debian/Ubuntu. Supported families:
#   - debian  (Debian, Ubuntu, Linux Mint, Pop!_OS, ...)          -> apt-get
#   - rhel    (RHEL, CentOS, Rocky, AlmaLinux, Fedora, Amazon)    -> dnf / yum
#   - arch    (Arch, Manjaro, EndeavourOS, ...)                   -> pacman
#   - suse    (openSUSE, SLES)                                    -> zypper
#   - alpine  (Alpine)                                           -> apk
#
# Why mpm-itk (not PHP-FPM):
#   mpm-itk lets each <VirtualHost> run its Apache children as a dedicated user
#   via `AssignUserId`. Combined with mod_php, that means the process serving a
#   site — and executing its PHP — *is* the site's own user. No per-site FPM
#   pools or sockets, and no shared www-data reading everyone's files.
#   NOTE: itk + mod_php is cleanest on Debian/Ubuntu (and works on RHEL via
#   EPEL's httpd-itk). On Arch/SUSE/Alpine the itk MPM and/or mod_php may not be
#   packaged; the script installs what it can and warns you where a manual step
#   is needed.
#
# Features:
#   - Root privilege check
#   - OS/distro detection with per-family package/service/config layout
#   - Auto-install of Apache, mpm-itk, and (only if you choose PHP) mod_php,
#     plus every dependency the site needs (openssl, curl, tar, DB client,
#     certbot, php-mysql, ...) — installed only when missing
#   - Domain name validation
#   - Dedicated Linux system user per site (the itk AssignUserId)
#   - Custom (or default) document root, secure ownership + permissions
#   - Automatic Apache vhost generation (sites-enabled OR conf.d)
#   - Optional WordPress install (download, DB, wp-config, admin creds)
#   - Optional Let's Encrypt SSL via certbot --apache (with auto-renewal check)
#   - Random password generation + saved credential file
#   - Config testing before reload; error handling + logging
#   - Interactive menu + manage mode for existing vhosts
#
# Usage:
#   sudo ./apache-provision.sh
#   sudo ./apache-provision.sh --domain example.com --root /var/www/example.com \
#        --php --wordpress --non-interactive
#
set -Eeuo pipefail

# ----------------------------------------------------------------------------
# Globals / defaults
# ----------------------------------------------------------------------------
SCRIPT_NAME="$(basename "$0")"
LOG_FILE="/var/log/apache-provision.log"

DOMAIN=""
ROOT_DIR=""
SITE_USER=""
SITE_GROUP=""
INSTALL_PHP=""
INSTALL_WP=""
INSTALL_SSL=""
SSL_EMAIL=""
ENABLE_LISTING="n"
NON_INTERACTIVE="false"
MANAGE_MODE="false"
FORCE="false"
BACKUP_DIR=""
EXISTING_DOCROOT="false"
VHOST_EXISTED="false"
MENU_CHOICE=""

# --- Filled in by detect_os() ------------------------------------------------
OS_ID=""; OS_LIKE=""; OS_FAMILY=""; OS_PRETTY=""
PKG_MGR=""
WEB_USER=""; WEB_GROUP=""          # Apache's default runtime user/group
INIT_SYSTEM="systemd"              # systemd | openrc
NOLOGIN_SHELL="/usr/sbin/nologin"

# Apache layout
APACHE_SVC="apache2"               # apache2 | httpd
APACHE_CTL="apachectl"             # apache2ctl | apachectl | httpd
APACHE_CONF_MODE="sites"           # sites | confd
APACHE_SITES_AVAILABLE="/etc/apache2/sites-available"
APACHE_SITES_ENABLED="/etc/apache2/sites-enabled"
APACHE_CONFD="/etc/httpd/conf.d"
APACHE_LOG_DIR="/var/log/apache2"
USE_A2="false"                     # Debian a2ensite/a2enmod helpers available

# Filled in by install_php()
PHP_VERSION=""
PHP_MODE="mod_php"                 # mod_php | fpm  (fpm = RHEL fallback when
                                   # mod_php isn't shipped, e.g. RHEL/Rocky 8+)
PHP_FPM_SOCK=""
PHP_FPM_SERVICE=""
PHP_FPM_POOL_DIR=""

# ----------------------------------------------------------------------------
# Logging helpers
# ----------------------------------------------------------------------------
_ts()   { date '+%Y-%m-%d %H:%M:%S'; }
log()   { printf '[%s] %s\n' "$(_ts)" "$*" | tee -a "$LOG_FILE"; }
info()  { log "INFO:  $*"; }
warn()  { log "WARN:  $*" >&2; }
error() { log "ERROR: $*" >&2; }
die()   { error "$*"; exit 1; }
trap 'error "Failed at line $LINENO (exit $?). See $LOG_FILE for details."' ERR

# ----------------------------------------------------------------------------
# Argument parsing
# ----------------------------------------------------------------------------
usage() {
  cat <<EOF
$SCRIPT_NAME — provision an Apache site with mpm-itk (cross-distro).

Options:
  -d, --domain DOMAIN     Domain name (e.g. example.com)
  -r, --root DIR          Document root (default: /var/www/DOMAIN/public_html)
  -u, --user USER         System user to own + run the site (default: from domain)
      --php               Install/enable PHP (mod_php) for the site
      --wordpress         Install WordPress (implies --php)
      --ssl               Obtain a Let's Encrypt certificate via certbot --apache
      --email EMAIL       Email for Let's Encrypt (registration + expiry notices)
      --listing           Enable directory autoindex (Options +Indexes)
      --manage            Manage an EXISTING vhost
      --force             Allow destructive actions without prompting
      --non-interactive   Do not prompt; require all needed flags
  -h, --help              Show this help

Manage mode (--manage):
    sudo $SCRIPT_NAME --manage
    sudo $SCRIPT_NAME --manage --domain example.com --wordpress
EOF
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -d|--domain)      DOMAIN="${2:-}"; shift 2 ;;
      -r|--root)        ROOT_DIR="${2:-}"; shift 2 ;;
      -u|--user)        SITE_USER="${2:-}"; shift 2 ;;
      --php)            INSTALL_PHP="y"; shift ;;
      --wordpress)      INSTALL_WP="y"; INSTALL_PHP="y"; shift ;;
      --ssl)            INSTALL_SSL="y"; shift ;;
      --email)          SSL_EMAIL="${2:-}"; shift 2 ;;
      --listing)        ENABLE_LISTING="y"; shift ;;
      --manage)         MANAGE_MODE="true"; shift ;;
      --force)          FORCE="true"; shift ;;
      --non-interactive) NON_INTERACTIVE="true"; shift ;;
      -h|--help)        usage; exit 0 ;;
      *) die "Unknown argument: $1 (see --help)" ;;
    esac
  done
}

# ----------------------------------------------------------------------------
# OS / distro detection
# ----------------------------------------------------------------------------
detect_os() {
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    OS_ID="${ID:-}"; OS_LIKE="${ID_LIKE:-}"
    OS_PRETTY="${PRETTY_NAME:-${NAME:-$OS_ID}}"
  else
    OS_ID="unknown"; OS_PRETTY="unknown"
  fi

  local probe=" $OS_ID $OS_LIKE "
  case "$probe" in
    *" debian "*|*" ubuntu "*)                               OS_FAMILY="debian" ;;
    *" rhel "*|*" fedora "*|*" centos "*|*" rocky "*|*" almalinux "*|*" amzn "*) OS_FAMILY="rhel" ;;
    *" arch "*|*" archlinux "*)                              OS_FAMILY="arch" ;;
    *" suse "*|*" opensuse "*|*" sles "*)                    OS_FAMILY="suse" ;;
    *" alpine "*)                                            OS_FAMILY="alpine" ;;
    *)
      if   command -v apt-get >/dev/null 2>&1; then OS_FAMILY="debian"
      elif command -v dnf     >/dev/null 2>&1; then OS_FAMILY="rhel"
      elif command -v yum     >/dev/null 2>&1; then OS_FAMILY="rhel"
      elif command -v pacman  >/dev/null 2>&1; then OS_FAMILY="arch"
      elif command -v zypper  >/dev/null 2>&1; then OS_FAMILY="suse"
      elif command -v apk     >/dev/null 2>&1; then OS_FAMILY="alpine"
      else die "Unsupported/undetected OS. Need apt/dnf/yum/pacman/zypper/apk."
      fi
      ;;
  esac

  case "$OS_FAMILY" in
    debian)
      PKG_MGR="apt-get"
      WEB_USER="www-data"; WEB_GROUP="www-data"
      APACHE_SVC="apache2"; APACHE_CTL="apache2ctl"; USE_A2="true"
      APACHE_CONF_MODE="sites"
      APACHE_SITES_AVAILABLE="/etc/apache2/sites-available"
      APACHE_SITES_ENABLED="/etc/apache2/sites-enabled"
      APACHE_LOG_DIR="/var/log/apache2"
      NOLOGIN_SHELL="/usr/sbin/nologin"
      ;;
    rhel)
      if command -v dnf >/dev/null 2>&1; then PKG_MGR="dnf"; else PKG_MGR="yum"; fi
      WEB_USER="apache"; WEB_GROUP="apache"
      APACHE_SVC="httpd"; APACHE_CTL="apachectl"
      APACHE_CONF_MODE="confd"; APACHE_CONFD="/etc/httpd/conf.d"
      APACHE_LOG_DIR="/var/log/httpd"
      NOLOGIN_SHELL="/sbin/nologin"
      ;;
    arch)
      PKG_MGR="pacman"
      WEB_USER="http"; WEB_GROUP="http"
      APACHE_SVC="httpd"; APACHE_CTL="apachectl"
      APACHE_CONF_MODE="confd"; APACHE_CONFD="/etc/httpd/conf/vhosts"
      APACHE_LOG_DIR="/var/log/httpd"
      NOLOGIN_SHELL="/usr/bin/nologin"
      ;;
    suse)
      PKG_MGR="zypper"
      WEB_USER="wwwrun"; WEB_GROUP="www"
      APACHE_SVC="apache2"; APACHE_CTL="apache2ctl"
      APACHE_CONF_MODE="confd"; APACHE_CONFD="/etc/apache2/vhosts.d"
      APACHE_LOG_DIR="/var/log/apache2"
      NOLOGIN_SHELL="/sbin/nologin"
      ;;
    alpine)
      PKG_MGR="apk"
      WEB_USER="apache"; WEB_GROUP="apache"
      APACHE_SVC="apache2"; APACHE_CTL="httpd"
      APACHE_CONF_MODE="confd"; APACHE_CONFD="/etc/apache2/conf.d"
      APACHE_LOG_DIR="/var/log/apache2"
      NOLOGIN_SHELL="/sbin/nologin"
      INIT_SYSTEM="openrc"
      ;;
  esac

  if [[ "$INIT_SYSTEM" != "openrc" ]]; then
    if command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]; then
      INIT_SYSTEM="systemd"
    elif command -v rc-service >/dev/null 2>&1; then
      INIT_SYSTEM="openrc"
    fi
  fi

  if [[ ! -x "$NOLOGIN_SHELL" ]]; then
    if   [[ -x /usr/sbin/nologin ]]; then NOLOGIN_SHELL="/usr/sbin/nologin"
    elif [[ -x /sbin/nologin     ]]; then NOLOGIN_SHELL="/sbin/nologin"
    elif [[ -x /usr/bin/nologin  ]]; then NOLOGIN_SHELL="/usr/bin/nologin"
    else NOLOGIN_SHELL="/bin/false"; fi
  fi

  info "Detected OS: ${OS_PRETTY} (family=${OS_FAMILY}, pkg=${PKG_MGR}, apache=${APACHE_SVC}, init=${INIT_SYSTEM}, layout=${APACHE_CONF_MODE})."
}

# ----------------------------------------------------------------------------
# Package-manager abstraction
# ----------------------------------------------------------------------------
PKG_UPDATED="false"
pkg_update() {
  [[ "$PKG_UPDATED" == "true" ]] && return 0
  info "Refreshing package metadata (${PKG_MGR})..."
  case "$PKG_MGR" in
    apt-get) apt-get update -y               >>"$LOG_FILE" 2>&1 ;;
    dnf)     dnf -y makecache                 >>"$LOG_FILE" 2>&1 || true ;;
    yum)     yum -y makecache                 >>"$LOG_FILE" 2>&1 || true ;;
    pacman)  pacman -Sy --noconfirm           >>"$LOG_FILE" 2>&1 ;;
    zypper)  zypper --non-interactive refresh >>"$LOG_FILE" 2>&1 ;;
    apk)     apk update                       >>"$LOG_FILE" 2>&1 ;;
  esac
  PKG_UPDATED="true"
}

pkg_install() {
  [[ $# -gt 0 ]] || return 0
  pkg_update
  info "Installing package(s): $*"
  case "$PKG_MGR" in
    apt-get) DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" >>"$LOG_FILE" 2>&1 ;;
    dnf)     dnf install -y "$@"                                    >>"$LOG_FILE" 2>&1 ;;
    yum)     yum install -y "$@"                                    >>"$LOG_FILE" 2>&1 ;;
    pacman)  pacman -S --noconfirm --needed "$@"                    >>"$LOG_FILE" 2>&1 ;;
    zypper)  zypper --non-interactive install -y "$@"             >>"$LOG_FILE" 2>&1 ;;
    apk)     apk add "$@"                                           >>"$LOG_FILE" 2>&1 ;;
  esac
}

pkg_is_installed() {
  local p="$1"
  case "$PKG_MGR" in
    apt-get) dpkg -s "$p"     >/dev/null 2>&1 ;;
    dnf|yum) rpm -q "$p"      >/dev/null 2>&1 ;;
    pacman)  pacman -Qq "$p"  >/dev/null 2>&1 ;;
    zypper)  rpm -q "$p"      >/dev/null 2>&1 ;;
    apk)     apk info -e "$p" >/dev/null 2>&1 ;;
    *)       return 1 ;;
  esac
}

ensure_pkg() {
  # ensure_pkg <pkg> [check-binary]
  local pkg="$1" bin="${2:-}"
  if [[ -n "$bin" ]] && command -v "$bin" >/dev/null 2>&1; then return 0; fi
  if pkg_is_installed "$pkg"; then return 0; fi
  pkg_install "$pkg"
}

ensure_dep() {
  case "$1" in
    apache)
      case "$OS_FAMILY" in
        debian) ensure_pkg apache2 apache2 ;;
        rhel)   ensure_pkg httpd httpd ;;
        arch)   ensure_pkg apache httpd ;;
        suse)   ensure_pkg apache2 ;;
        alpine) ensure_pkg apache2 httpd ;;
      esac ;;
    apache-itk)
      # The mpm-itk MPM module. Package availability varies a lot.
      case "$OS_FAMILY" in
        debian) ensure_pkg libapache2-mpm-itk ;;
        rhel)
          if [[ "$OS_ID" != "fedora" ]]; then ensure_pkg epel-release || true; fi
          ensure_pkg httpd-itk || warn "httpd-itk not found (EPEL?); itk MPM may be unavailable." ;;
        suse)   ensure_pkg apache2-mpm-itk || warn "apache2-mpm-itk not found; itk MPM may be unavailable." ;;
        arch)   warn "mpm-itk is not in Arch's official repos (AUR only). Install 'apache-mpm-itk' from the AUR manually." ;;
        alpine) warn "mpm-itk is not packaged for Alpine; AssignUserId will be inert. Consider php-fpm instead." ;;
      esac ;;
    mod-php)
      case "$OS_FAMILY" in
        debian) ensure_pkg libapache2-mod-php ;;
        rhel)   ensure_pkg php php ;;  # provides mod_php on rhel7; rhel8+ may prefer fpm
        arch)   ensure_pkg php-apache ;;
        suse)   ensure_pkg apache2-mod_php8 || ensure_pkg apache2-mod_php7 || warn "No apache2-mod_php package found." ;;
        alpine) ensure_pkg php-apache2 || ensure_pkg php83-apache2 || warn "No php-apache2 package found." ;;
      esac ;;
    php-mysql)
      case "$OS_FAMILY" in
        debian) ensure_pkg php-mysql ;;
        rhel)   ensure_pkg php-mysqlnd ;;
        arch)   : ;;  # bundled with arch 'php'
        suse)   ensure_pkg php-mysql || ensure_pkg php8-mysql ;;
        alpine) ensure_pkg php-pdo_mysql || ensure_pkg php83-pdo_mysql || true
                ensure_pkg php-mysqli    || ensure_pkg php83-mysqli    || true ;;
      esac ;;
    php-extras)
      case "$OS_FAMILY" in
        debian) pkg_install php-gd php-curl php-xml php-mbstring php-zip php-intl php-opcache 2>/dev/null || true ;;
        rhel)   pkg_install php-gd php-xml php-mbstring php-json php-opcache 2>/dev/null || true ;;
        arch)   : ;;
        suse)   pkg_install php-gd php-curl php-mbstring php-zip php-opcache 2>/dev/null || true ;;
        alpine) pkg_install php-gd php-curl php-xml php-mbstring php-zip php-opcache php-session php-fileinfo php-dom 2>/dev/null || true ;;
      esac ;;
    openssl)  ensure_pkg openssl openssl ;;
    curl)     ensure_pkg curl curl ;;
    tar)      ensure_pkg tar tar ;;
    ca-certs) ensure_pkg ca-certificates ;;
    useradd)
      if ! command -v useradd >/dev/null 2>&1; then
        case "$OS_FAMILY" in
          alpine) ensure_pkg shadow useradd ;;
          *)      ensure_pkg shadow-utils useradd ;;
        esac
      fi ;;
    mysql-client)
      if command -v mysql >/dev/null 2>&1 || command -v mariadb >/dev/null 2>&1; then return 0; fi
      case "$OS_FAMILY" in
        debian) ensure_pkg default-mysql-client mysql || ensure_pkg mariadb-client mysql ;;
        rhel)   ensure_pkg mariadb mysql || ensure_pkg mysql mysql ;;
        arch)   ensure_pkg mariadb-clients mysql ;;
        suse)   ensure_pkg mariadb-client mysql || ensure_pkg mariadb-tools mysql ;;
        alpine) ensure_pkg mariadb-client mysql ;;
      esac ;;
    db-server)
      case "$OS_FAMILY" in
        debian) ensure_pkg mariadb-server mariadbd || ensure_pkg default-mysql-server ;;
        rhel)   ensure_pkg mariadb-server ;;
        arch)   ensure_pkg mariadb ;;
        suse)   ensure_pkg mariadb ;;
        alpine) ensure_pkg mariadb ;;
      esac ;;
    certbot)
      case "$OS_FAMILY" in
        debian) ensure_pkg certbot certbot; ensure_pkg python3-certbot-apache ;;
        rhel)
          if [[ "$OS_ID" != "fedora" ]]; then ensure_pkg epel-release || true; fi
          ensure_pkg certbot certbot; ensure_pkg python3-certbot-apache || true ;;
        arch)   ensure_pkg certbot certbot; ensure_pkg certbot-apache ;;
        suse)   ensure_pkg certbot certbot; ensure_pkg python3-certbot-apache || true ;;
        alpine) ensure_pkg certbot certbot; ensure_pkg certbot-apache || true ;;
      esac ;;
    *) warn "ensure_dep: unknown dependency '$1'" ;;
  esac
}

# ----------------------------------------------------------------------------
# Service abstraction (systemd / openrc)
# ----------------------------------------------------------------------------
svc_restart() {
  if [[ "$INIT_SYSTEM" == "systemd" ]]; then systemctl restart "$1"
  else rc-service "$1" restart; fi
}
svc_reload() {
  if [[ "$INIT_SYSTEM" == "systemd" ]]; then
    systemctl reload "$1" 2>/dev/null || systemctl restart "$1"
  else
    rc-service "$1" reload 2>/dev/null || rc-service "$1" restart
  fi
}
svc_enable_now() {
  if [[ "$INIT_SYSTEM" == "systemd" ]]; then
    systemctl enable --now "$1" >>"$LOG_FILE" 2>&1 || systemctl restart "$1" || true
  else
    rc-update add "$1" default >>"$LOG_FILE" 2>&1 || true
    rc-service "$1" start        >>"$LOG_FILE" 2>&1 || rc-service "$1" restart || true
  fi
}

# Make sure a MariaDB/MySQL *server* is installed and running before we try to
# create the WordPress database. The scripts otherwise only install the client,
# which is why a fresh box fails with "Can't connect to local MySQL server
# through socket". Runs as root, so it authenticates via the unix socket.
ensure_database_server() {
  if ! command -v mysqld >/dev/null 2>&1 && ! command -v mariadbd >/dev/null 2>&1 \
     && ! pkg_is_installed mariadb-server && ! pkg_is_installed default-mysql-server \
     && ! pkg_is_installed mysql-server; then
    info "No database server found; installing MariaDB server..."
    ensure_dep db-server
  fi

  # Some distros (notably Arch) don't initialize the data dir automatically.
  if [[ ! -d /var/lib/mysql/mysql ]] && command -v mariadb-install-db >/dev/null 2>&1; then
    info "Initializing MariaDB data directory..."
    mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql >>"$LOG_FILE" 2>&1 || true
  fi

  # Start + enable whichever service name this distro uses.
  local svc
  for svc in mariadb mysqld mysql; do
    if [[ "$INIT_SYSTEM" == "systemd" ]]; then
      if systemctl list-unit-files 2>/dev/null | grep -q "^${svc}\.service"; then
        svc_enable_now "$svc"; break
      fi
    else
      if [[ -x "/etc/init.d/${svc}" ]]; then svc_enable_now "$svc"; break; fi
    fi
  done

  # Wait (bounded) for the server socket to accept connections.
  local i
  for i in $(seq 1 30); do
    if mysqladmin ping >/dev/null 2>&1 || mysql -e 'SELECT 1' >/dev/null 2>&1 \
       || mariadb -e 'SELECT 1' >/dev/null 2>&1; then
      info "Database server is up."
      return 0
    fi
    sleep 1
  done
  die "Database server did not become ready; check its service status/logs."
}

# ----------------------------------------------------------------------------
# Apache control helpers
# ----------------------------------------------------------------------------
apache_configtest() {
  if   command -v apache2ctl >/dev/null 2>&1; then apache2ctl configtest
  elif command -v apachectl  >/dev/null 2>&1; then apachectl configtest
  else "$APACHE_SVC" -t 2>/dev/null || httpd -t; fi
}

# Enable the modules itk + PHP need. Debian uses a2enmod; other distros load
# modules via their package's drop-in config (best effort + warnings).
enable_apache_modules() {
  if [[ "$USE_A2" == "true" ]]; then
    # itk is a prefork variant; mod_php requires prefork (not event/worker).
    a2dismod mpm_event   >>"$LOG_FILE" 2>&1 || true
    a2dismod mpm_worker  >>"$LOG_FILE" 2>&1 || true
    a2enmod  mpm_prefork >>"$LOG_FILE" 2>&1 || true
    a2enmod  mpm_itk     >>"$LOG_FILE" 2>&1 || warn "Could not enable mpm_itk (is libapache2-mpm-itk installed?)."
    a2enmod  rewrite     >>"$LOG_FILE" 2>&1 || true
    a2enmod  ssl         >>"$LOG_FILE" 2>&1 || true
    if [[ "$INSTALL_PHP" == "y" ]]; then
      if [[ "$PHP_MODE" == "fpm" ]]; then
        a2enmod proxy proxy_fcgi setenvif >>"$LOG_FILE" 2>&1 || \
          warn "Could not enable proxy_fcgi; PHP-FPM handoff may not work."
      elif [[ -n "$PHP_VERSION" ]]; then
        a2enmod "php${PHP_VERSION}" >>"$LOG_FILE" 2>&1 || a2enmod php >>"$LOG_FILE" 2>&1 || \
          warn "Could not enable the mod_php module (a2enmod php${PHP_VERSION})."
      fi
    fi
  else
    # Non-Debian: the itk/php packages ship LoadModule drop-ins. Just remind.
    info "On ${OS_FAMILY}, ensure the MPM is set to itk (prefork-based) and mod_php is loaded."
    warn "If PHP does not execute, verify the itk MPM is the active MPM and mod_php is loaded, then restart ${APACHE_SVC}."
  fi
}

# ----------------------------------------------------------------------------
# Preconditions
# ----------------------------------------------------------------------------
require_root() { [[ "$(id -u)" -eq 0 ]] || die "This script must be run as root (use sudo)."; }

check_dependencies() {
  ensure_dep useradd
  ensure_dep openssl
  ensure_dep ca-certs
  ensure_dep apache
  ensure_dep apache-itk    # the whole point: per-vhost AssignUserId

  command -v "$APACHE_SVC" >/dev/null 2>&1 || command -v httpd >/dev/null 2>&1 || command -v apache2 >/dev/null 2>&1 \
    || die "Apache is required but could not be installed."
  command -v openssl >/dev/null 2>&1 || die "openssl is required but could not be installed."
  command -v useradd >/dev/null 2>&1 || die "useradd is required but could not be installed."

  if [[ "$APACHE_CONF_MODE" == "sites" ]]; then
    mkdir -p "$APACHE_SITES_AVAILABLE" "$APACHE_SITES_ENABLED"
  else
    mkdir -p "$APACHE_CONFD"
    ensure_confd_included
  fi
  mkdir -p "$APACHE_LOG_DIR"

  svc_enable_now "$APACHE_SVC"
}

# On distros whose main httpd.conf does not already Include our vhost dir
# (notably Arch), add the Include so our conf files are actually read.
ensure_confd_included() {
  local main=""
  for main in /etc/httpd/conf/httpd.conf /etc/apache2/httpd.conf /etc/apache2/apache2.conf; do
    [[ -f "$main" ]] || continue
    if ! grep -q "IncludeOptional ${APACHE_CONFD}/\*.conf" "$main" 2>/dev/null \
       && ! grep -q "Include ${APACHE_CONFD}/" "$main" 2>/dev/null; then
      # SUSE/Debian already include their vhost dirs; only add where missing.
      if [[ "$OS_FAMILY" == "arch" ]]; then
        printf '\n# Added by %s\nIncludeOptional %s/*.conf\n' "$SCRIPT_NAME" "$APACHE_CONFD" >> "$main"
        info "Added IncludeOptional ${APACHE_CONFD}/*.conf to $main"
      fi
    fi
    break
  done
}

# ----------------------------------------------------------------------------
# Prompt helpers
# ----------------------------------------------------------------------------
prompt() {
  local __var="$1" __msg="$2" __default="${3:-}" __reply=""
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    [[ -n "${!__var:-}" ]] && return 0
    [[ -n "$__default" ]] && { printf -v "$__var" '%s' "$__default"; return 0; }
    die "Missing required value for '$__var' in non-interactive mode."
  fi
  [[ -n "${!__var:-}" ]] && return 0
  if [[ -n "$__default" ]]; then
    read -r -p "$__msg [$__default]: " __reply
    __reply="${__reply:-$__default}"
  else
    read -r -p "$__msg: " __reply
  fi
  printf -v "$__var" '%s' "$__reply"
}

prompt_yn() {
  local __var="$1" __msg="$2" __reply=""
  [[ -n "${!__var:-}" ]] && return 0
  if [[ "$NON_INTERACTIVE" == "true" ]]; then printf -v "$__var" '%s' "n"; return 0; fi
  read -r -p "$__msg (y/n): " __reply
  case "$__reply" in
    [yY]|[yY][eE][sS]) printf -v "$__var" '%s' "y" ;;
    *)                 printf -v "$__var" '%s' "n" ;;
  esac
}

# ----------------------------------------------------------------------------
# Validation
# ----------------------------------------------------------------------------
validate_domain() {
  local d="$1"
  [[ "$d" =~ ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$ ]] \
    || die "Invalid domain name: '$d'"
}

# ----------------------------------------------------------------------------
# Site user (the itk AssignUserId identity)
# ----------------------------------------------------------------------------
derive_user() {
  local base="${DOMAIN%%.*}"
  base="$(echo "$base" | tr -cd 'a-z0-9')"
  echo "web_${base}" | cut -c1-32
}

create_site_user() {
  if id "$SITE_USER" >/dev/null 2>&1; then
    local eh es
    eh="$(getent passwd "$SITE_USER" | cut -d: -f6)"
    es="$(getent passwd "$SITE_USER" | cut -d: -f7)"
    info "User '$SITE_USER' already exists; reusing it unchanged (home: ${eh:-?}, shell: ${es:-?})."
  else
    info "Creating new system user '$SITE_USER'..."
    useradd --system --home-dir "$ROOT_DIR" --no-create-home \
            --shell "$NOLOGIN_SHELL" "$SITE_USER"
  fi
  # Resolve the user's primary group for AssignUserId + ownership.
  SITE_GROUP="$(id -gn "$SITE_USER" 2>/dev/null || echo "$SITE_USER")"
}

# ----------------------------------------------------------------------------
# Document root, ownership, permissions
# ----------------------------------------------------------------------------
setup_docroot() {
  if [[ -d "$ROOT_DIR" ]] && [[ -n "$(ls -A "$ROOT_DIR" 2>/dev/null)" ]]; then
    EXISTING_DOCROOT="true"
    info "Document root $ROOT_DIR already exists and is not empty; preserving existing content, owners and permissions."
    # Under itk the worker runs AS the site user, so it needs read access to the
    # existing files. Grant it additively via ACLs without changing ownership.
    if command -v setfacl >/dev/null 2>&1; then
      setfacl -R  -m "u:${SITE_USER}:rX" "$ROOT_DIR" 2>/dev/null || \
        warn "Could not apply ACLs; ensure ${SITE_USER} can read ${ROOT_DIR}."
      setfacl -R -d -m "u:${SITE_USER}:rX" "$ROOT_DIR" 2>/dev/null || true
      local p="$ROOT_DIR"
      while [[ "$p" != "/" && -n "$p" ]]; do
        setfacl -m "u:${SITE_USER}:rX" "$p" 2>/dev/null || true
        p="$(dirname "$p")"
      done
    else
      warn "setfacl unavailable; if pages 403/500, chown ${ROOT_DIR} to ${SITE_USER} or install 'acl'."
    fi
    return 0
  fi

  EXISTING_DOCROOT="false"
  info "Creating new document root at $ROOT_DIR"
  mkdir -p "$ROOT_DIR"
  # itk runs children as SITE_USER, so the site user simply owns everything.
  chown -R "$SITE_USER:$SITE_GROUP" "$ROOT_DIR"
  find "$ROOT_DIR" -type d -exec chmod 2750 {} +
  find "$ROOT_DIR" -type f -exec chmod 640  {} +
  info "Ownership and permissions applied (owner ${SITE_USER}:${SITE_GROUP})."
}

# ----------------------------------------------------------------------------
# PHP (mod_php) — only installed when the user chose PHP
# ----------------------------------------------------------------------------
detect_php_version() {
  if command -v php >/dev/null 2>&1; then
    php -v | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1
  else
    echo ""
  fi
}

# True if an Apache mod_php module (.so) is actually present on the system.
apache_mod_php_available() {
  ls /usr/lib64/httpd/modules/libphp*.so >/dev/null 2>&1 && return 0
  ls /usr/lib/apache2/modules/libphp*.so  >/dev/null 2>&1 && return 0
  ls /usr/lib*/apache2/modules/libphp*.so >/dev/null 2>&1 && return 0
  return 1
}

# RHEL 8+ (Rocky/Alma/RHEL) drop mod_php in favor of php-fpm. When mod_php is
# unavailable we set up a dedicated FPM pool running as the site user and have
# Apache hand .php off to it over a unix socket via proxy_fcgi. PHP then still
# runs as the site's own user, matching the itk model for static content.
setup_php_fpm() {
  info "Setting up PHP-FPM fallback (mod_php unavailable)..."
  ensure_pkg php-fpm php-fpm || pkg_install php-fpm

  PHP_FPM_POOL_DIR="/etc/php-fpm.d"
  PHP_FPM_SERVICE="php-fpm"
  mkdir -p "$PHP_FPM_POOL_DIR" /run/php-fpm
  PHP_FPM_SOCK="/run/php-fpm/${DOMAIN}.sock"

  local pool_file="${PHP_FPM_POOL_DIR}/${DOMAIN}.conf"
  info "Creating dedicated PHP-FPM pool: $pool_file (user ${SITE_USER})"
  cat > "$pool_file" <<EOF
; Dedicated PHP-FPM pool for ${DOMAIN} (Apache proxy_fcgi fallback)
[${DOMAIN}]
user = ${SITE_USER}
group = ${SITE_GROUP}
listen = ${PHP_FPM_SOCK}
listen.owner = ${WEB_USER}
listen.group = ${WEB_GROUP}
listen.mode = 0660

pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 4
pm.max_requests = 500

php_admin_value[error_log] = /var/log/php/${DOMAIN}.error.log
php_admin_flag[log_errors] = on
chdir = /
EOF
  mkdir -p /var/log/php
  svc_enable_now "$PHP_FPM_SERVICE"
  svc_restart "$PHP_FPM_SERVICE" 2>/dev/null || true
  PHP_MODE="fpm"
  info "PHP-FPM pool active; Apache will proxy .php to ${PHP_FPM_SOCK}."
}

install_php() {
  local ver; ver="$(detect_php_version)"
  if [[ -z "$ver" ]]; then
    info "PHP not found; installing mod_php for Apache..."
    ensure_dep mod-php
    ver="$(detect_php_version)"
  else
    info "PHP $ver already present; ensuring the Apache mod_php module is installed..."
    ensure_dep mod-php
  fi
  [[ -n "$ver" ]] || die "Failed to detect/install PHP."
  PHP_VERSION="$ver"

  # RHEL fallback: if mod_php isn't shipped/available, use PHP-FPM instead.
  if [[ "$OS_FAMILY" == "rhel" ]] && ! apache_mod_php_available; then
    warn "mod_php is not available on this ${OS_PRETTY}; falling back to PHP-FPM (proxy_fcgi)."
    setup_php_fpm
    info "Using PHP $PHP_VERSION (php-fpm fallback, pool runs as ${SITE_USER})."
  else
    PHP_MODE="mod_php"
    info "Using PHP $PHP_VERSION (mod_php, executed per-vhost as the itk user)."
  fi
}

# ----------------------------------------------------------------------------
# Apache vhost path helpers (abstract sites-enabled vs conf.d)
# ----------------------------------------------------------------------------
vhost_src() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    echo "${APACHE_CONFD}/${DOMAIN}.conf"
  else
    echo "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf"
  fi
}

vhost_find() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    local p
    for p in "${APACHE_CONFD}/${DOMAIN}.conf" "${APACHE_CONFD}/${DOMAIN}.conf.disabled"; do
      [[ -f "$p" ]] && { echo "$p"; return 0; }
    done
  else
    [[ -f "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf" ]] && echo "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf"
  fi
  # Always succeed: "no match" is a valid result, not an error. Returning
  # non-zero here would trip `set -e` on `x="$(vhost_find)"` assignments.
  return 0
}

vhost_exists() { [[ -n "$(vhost_find)" ]]; }

vhost_is_enabled() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    [[ -f "${APACHE_CONFD}/${DOMAIN}.conf" ]]
  else
    [[ -L "${APACHE_SITES_ENABLED}/${DOMAIN}.conf" ]]
  fi
}

vhost_enable() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    [[ -f "${APACHE_CONFD}/${DOMAIN}.conf.disabled" ]] && \
      mv -f "${APACHE_CONFD}/${DOMAIN}.conf.disabled" "${APACHE_CONFD}/${DOMAIN}.conf"
  elif [[ "$USE_A2" == "true" ]]; then
    a2ensite "${DOMAIN}.conf" >>"$LOG_FILE" 2>&1 || \
      ln -sf "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf" "${APACHE_SITES_ENABLED}/${DOMAIN}.conf"
  else
    ln -sf "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf" "${APACHE_SITES_ENABLED}/${DOMAIN}.conf"
  fi
}

vhost_disable() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    [[ -f "${APACHE_CONFD}/${DOMAIN}.conf" ]] && \
      mv -f "${APACHE_CONFD}/${DOMAIN}.conf" "${APACHE_CONFD}/${DOMAIN}.conf.disabled"
  elif [[ "$USE_A2" == "true" ]]; then
    a2dissite "${DOMAIN}.conf" >>"$LOG_FILE" 2>&1 || rm -f "${APACHE_SITES_ENABLED}/${DOMAIN}.conf"
  else
    rm -f "${APACHE_SITES_ENABLED}/${DOMAIN}.conf"
  fi
}

# ----------------------------------------------------------------------------
# Apache vhost
# ----------------------------------------------------------------------------
write_apache_conf() {
  local conf; conf="$(vhost_src)"
  local existing; existing="$(vhost_find)"

  if [[ -n "$existing" ]]; then
    VHOST_EXISTED="true"
    info "Apache vhost already exists ($existing); reusing it unchanged."
    vhost_enable
    return 0
  fi

  info "Writing Apache vhost: $conf"

  local options="-Indexes +FollowSymLinks"
  [[ "$ENABLE_LISTING" == "y" ]] && options="+Indexes +FollowSymLinks"

  # PHP-FPM fallback: route .php to the site's FPM pool over its unix socket.
  # (Only set when install_php chose the fpm path; empty for mod_php.)
  local php_fpm_block=""
  if [[ "$PHP_MODE" == "fpm" && -n "$PHP_FPM_SOCK" ]]; then
    php_fpm_block=$(cat <<EOF

    <FilesMatch \.php\$>
        SetHandler "proxy:unix:${PHP_FPM_SOCK}|fcgi://localhost/"
    </FilesMatch>
EOF
)
  fi

  # WordPress hardening (only meaningful once WP is present; harmless otherwise).
  local wp_block=""
  if [[ "$INSTALL_WP" == "y" ]]; then
    wp_block=$(cat <<'EOF'

    <Files "wp-config.php">
        Require all denied
    </Files>
    <Files "xmlrpc.php">
        Require all denied
    </Files>
EOF
)
  fi

  cat > "$conf" <<EOF
<VirtualHost *:80>
    ServerAdmin webmaster@${DOMAIN}
    ServerName ${DOMAIN}
    ServerAlias www.${DOMAIN}
    DocumentRoot ${ROOT_DIR}

    # mpm-itk: run this vhost's processes (and its PHP) as the site's own user.
    <IfModule mpm_itk_module>
        AssignUserId ${SITE_USER} ${SITE_GROUP}
    </IfModule>

    <Directory ${ROOT_DIR}>
        Options ${options}
        AllowOverride All
        Require all granted
    </Directory>
${php_fpm_block}
    # Hide dotfiles (except ACME challenges).
    <DirectoryMatch "/\.(?!well-known)">
        Require all denied
    </DirectoryMatch>
${wp_block}
    ErrorLog ${APACHE_LOG_DIR}/${DOMAIN}.error.log
    CustomLog ${APACHE_LOG_DIR}/${DOMAIN}.access.log combined
</VirtualHost>
EOF

  vhost_enable
  info "Site enabled."
}

# ----------------------------------------------------------------------------
# WordPress — deps installed here, only if chosen and only if missing
# ----------------------------------------------------------------------------
rand_pw() { openssl rand -base64 24 | tr -d '/+=' | cut -c1-24; }

install_wordpress() {
  # WordPress-only dependencies (never in install_php); installed only if absent.
  ensure_dep php-mysql
  ensure_dep php-extras
  ensure_dep curl
  ensure_dep tar
  ensure_dep mysql-client
  # A running DB server is required to create the WordPress database.
  ensure_database_server
  # Pick up the freshly added PHP MySQL driver: reload whichever runtime runs PHP.
  if [[ "$PHP_MODE" == "fpm" && -n "$PHP_FPM_SERVICE" ]]; then
    svc_restart "$PHP_FPM_SERVICE" 2>/dev/null || true
  else
    svc_reload "$APACHE_SVC" 2>/dev/null || true
  fi

  command -v curl >/dev/null 2>&1 || die "curl is required for WordPress download."
  command -v mysql >/dev/null 2>&1 || command -v mariadb >/dev/null 2>&1 || \
    die "MySQL/MariaDB client not found; install a DB server first."
  local MYSQL_CLI="mysql"; command -v mysql >/dev/null 2>&1 || MYSQL_CLI="mariadb"

  if [[ -f "$ROOT_DIR/wp-load.php" ]] || [[ -f "$ROOT_DIR/wp-config.php" ]]; then
    warn "WordPress already present in $ROOT_DIR; leaving it untouched."
    return 0
  fi
  [[ "$EXISTING_DOCROOT" == "true" ]] && \
    info "Adding WordPress into existing doc root (existing files preserved)."

  local db_name db_user db_pass wp_admin wp_admin_pass creds_file
  db_name="wp_$(echo "${SITE_USER}" | tr -cd 'a-z0-9' | cut -c1-24)"
  db_user="$db_name"; db_pass="$(rand_pw)"
  wp_admin="admin"; wp_admin_pass="$(rand_pw)"

  info "Downloading WordPress..."
  local tmp; tmp="$(mktemp -d)"
  curl -fsSL https://wordpress.org/latest.tar.gz -o "$tmp/wp.tar.gz"
  tar -xzf "$tmp/wp.tar.gz" -C "$tmp"
  local wp_items=(); mapfile -t wp_items < <(cd "$tmp/wordpress" && ls -A)
  cp -a -n "$tmp/wordpress/." "$ROOT_DIR/"
  rm -rf "$tmp"

  info "Creating database '$db_name' and user '$db_user'..."
  "$MYSQL_CLI" <<SQL
CREATE DATABASE IF NOT EXISTS \`${db_name}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${db_user}'@'localhost' IDENTIFIED BY '${db_pass}';
GRANT ALL PRIVILEGES ON \`${db_name}\`.* TO '${db_user}'@'localhost';
FLUSH PRIVILEGES;
SQL

  info "Generating wp-config.php..."
  local salts; salts="$(curl -fsSL https://api.wordpress.org/secret-key/1.1/salt/ || true)"
  local wp_config="$ROOT_DIR/wp-config.php"
  cp "$ROOT_DIR/wp-config-sample.php" "$wp_config"
  sed -i \
    -e "s/database_name_here/${db_name}/" \
    -e "s/username_here/${db_user}/" \
    -e "s/password_here/${db_pass}/" \
    "$wp_config"
  if [[ -n "$salts" ]]; then
    sed -i "/AUTH_KEY/d;/SECURE_AUTH_KEY/d;/LOGGED_IN_KEY/d;/NONCE_KEY/d;/AUTH_SALT/d;/SECURE_AUTH_SALT/d;/LOGGED_IN_SALT/d;/NONCE_SALT/d" "$wp_config"
    printf '%s\n' "$salts" >> "$wp_config"
  fi

  # Ownership/permissions: whole tree on a fresh root, only WP items on existing.
  local -a targets
  if [[ "$EXISTING_DOCROOT" == "true" ]]; then
    targets=(); local item
    for item in "${wp_items[@]}"; do
      [[ -e "$ROOT_DIR/$item" ]] && targets+=("$ROOT_DIR/$item")
    done
  else
    targets=("$ROOT_DIR")
  fi
  if [[ ${#targets[@]} -gt 0 ]]; then
    chown -R "$SITE_USER:$SITE_GROUP" "${targets[@]}"
    find "${targets[@]}" -type d -exec chmod 2750 {} +
    find "${targets[@]}" -type f -exec chmod 640  {} +
  fi
  chown "$SITE_USER:$SITE_GROUP" "$wp_config"
  chmod 600 "$wp_config"

  creds_file="/root/${DOMAIN}.credentials.txt"
  cat > "$creds_file" <<EOF
WordPress / site credentials for ${DOMAIN}
Generated: $(_ts)

Document root : ${ROOT_DIR}
System user   : ${SITE_USER} (itk AssignUserId)

Database name : ${db_name}
Database user : ${db_user}
Database pass : ${db_pass}

WP admin user : ${wp_admin}
WP admin pass : ${wp_admin_pass}
  (Set this on first login via the WordPress installer at http://${DOMAIN}/)
EOF
  chmod 600 "$creds_file"
  info "Credentials saved to $creds_file (root-only)."

  if [[ "$VHOST_EXISTED" == "true" ]]; then
    warn "Existing vhost was left unchanged. Ensure it has 'AllowOverride All' and"
    warn "the mpm_itk AssignUserId block so WordPress permalinks + PHP work."
  fi
}

# ----------------------------------------------------------------------------
# Manage an existing vhost
# ----------------------------------------------------------------------------
list_vhosts() {
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    { ls -1 "$APACHE_CONFD"/*.conf "$APACHE_CONFD"/*.conf.disabled 2>/dev/null || true; } \
      | xargs -r -n1 basename \
      | sed -e 's/\.conf\.disabled$//' -e 's/\.conf$//' \
      | sort -u | grep -vxE 'default|ssl|welcome' || true
  else
    ls -1 "$APACHE_SITES_AVAILABLE" 2>/dev/null \
      | sed -e 's/\.conf$//' | grep -vxE '000-default|default-ssl|default' || true
  fi
}

select_vhost() {
  local sites=(); mapfile -t sites < <(list_vhosts)
  [[ ${#sites[@]} -gt 0 ]] || die "No vhosts found to manage."
  if [[ -n "$DOMAIN" ]]; then
    [[ -n "$(vhost_find)" ]] || die "No vhost named '$DOMAIN' found."
    return 0
  fi
  if [[ "$NON_INTERACTIVE" == "true" ]]; then die "--manage --non-interactive requires --domain."; fi
  echo "Existing vhosts:"
  local i=1 s
  for s in "${sites[@]}"; do printf '  %d) %s\n' "$i" "$s"; ((i++)); done
  local choice=""
  read -r -p "Select a vhost by number, or type a domain: " choice
  if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#sites[@]} )); then
    DOMAIN="${sites[$((choice-1))]}"
  else
    DOMAIN="$choice"
  fi
  [[ -n "$DOMAIN" && -n "$(vhost_find)" ]] || die "Invalid selection: '$choice'."
}

vhost_docroot() {
  awk 'tolower($1)=="documentroot"{gsub(/"/,"",$2); print $2; exit}' "$1"
}

vhost_user() {
  awk 'tolower($1)=="assignuserid"{print $2; exit}' "$1"
}

backup_docroot() {
  local default dest
  default="/var/backups/${DOMAIN}-$(date +%Y%m%d-%H%M%S)"
  dest=""
  prompt dest "Backup destination directory" "$default"
  [[ -n "$dest" ]] || die "No backup destination given."
  mkdir -p "$dest"
  info "Backing up ${ROOT_DIR} -> ${dest} ..."
  [[ -n "$(ls -A "$ROOT_DIR" 2>/dev/null)" ]] && cp -a "$ROOT_DIR/." "$dest/"
  BACKUP_DIR="$dest"
  info "Backup complete: ${BACKUP_DIR}"
}

wipe_docroot() {
  info "Clearing existing contents of ${ROOT_DIR} ..."
  find "$ROOT_DIR" -mindepth 1 -delete
}

site_enabled() { vhost_is_enabled; }
site_has_ssl() { local c; c="$(vhost_find)"; [[ -n "$c" ]] && grep -Eqi 'SSLEngine|SSLCertificateFile|:443' "$c" 2>/dev/null; }

action_show_info() {
  local conf; conf="$(vhost_find)"
  echo
  echo "  Site info: ${DOMAIN}"
  echo "  --------------------------------------------"
  echo "  Config     : ${conf}"
  echo "  Doc root   : ${ROOT_DIR}"
  echo "  Site user  : ${SITE_USER} (itk)"
  echo "  Enabled    : $(site_enabled && echo yes || echo no)"
  echo "  SSL/TLS    : $(site_has_ssl && echo yes || echo no)"
  if [[ -d "$ROOT_DIR" ]]; then
    echo "  Disk usage : $(du -sh "$ROOT_DIR" 2>/dev/null | cut -f1)"
    echo "  Files      : $(find "$ROOT_DIR" -type f 2>/dev/null | wc -l)"
    [[ -f "$ROOT_DIR/wp-load.php" ]] && echo "  WordPress  : detected"
  fi
  echo
}

action_install_wordpress() {
  INSTALL_PHP="y"; INSTALL_WP="y"
  warn "WARNING: installing WordPress here will REPLACE the entire contents of:"
  warn "         ${ROOT_DIR}"
  local do_backup=""
  prompt_yn do_backup "Back up the current contents somewhere else first?"
  [[ "$do_backup" == "y" ]] && backup_docroot
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    [[ "$FORCE" == "true" ]] || die "Refusing to replace ${ROOT_DIR} non-interactively without --force."
  else
    local confirm=""
    prompt_yn confirm "Proceed and REPLACE ${ROOT_DIR} with a fresh WordPress install?"
    [[ "$confirm" == "y" ]] || { warn "Aborted; nothing changed."; return 0; }
  fi
  create_site_user
  install_php
  enable_apache_modules
  wipe_docroot
  EXISTING_DOCROOT="false"
  install_wordpress
  reload_apache
  info "WordPress installed for ${DOMAIN}. Creds: /root/${DOMAIN}.credentials.txt"
  [[ -n "$BACKUP_DIR" ]] && info "Previous contents backed up to: ${BACKUP_DIR}"
}

action_enable_ssl() { INSTALL_SSL="y"; install_ssl; }
action_backup_only() { backup_docroot; info "Backup finished: ${BACKUP_DIR}"; }

action_toggle_listing() {
  local conf; conf="$(vhost_find)"
  [[ -n "$conf" ]] || { warn "No config file found for ${DOMAIN}."; return 0; }
  if grep -q '+Indexes' "$conf"; then
    sed -i 's/+Indexes/-Indexes/' "$conf"; info "Directory listing turned OFF."
  elif grep -q -- '-Indexes' "$conf"; then
    sed -i 's/-Indexes/+Indexes/' "$conf"; info "Directory listing turned ON."
  else
    warn "No 'Indexes' option found in $conf; not changing anything."; return 0
  fi
  reload_apache
}

action_repair_perms() {
  create_site_user
  [[ -d "$ROOT_DIR" ]] || die "Doc root $ROOT_DIR does not exist."
  info "Repairing ownership/permissions on ${ROOT_DIR} (owner ${SITE_USER}:${SITE_GROUP})..."
  chown -R "$SITE_USER:$SITE_GROUP" "$ROOT_DIR"
  find "$ROOT_DIR" -type d -exec chmod 2750 {} +
  find "$ROOT_DIR" -type f -exec chmod 640  {} +
  [[ -f "$ROOT_DIR/wp-config.php" ]] && chmod 600 "$ROOT_DIR/wp-config.php"
  info "Permissions repaired."
}

action_disable_site() {
  if ! site_enabled; then warn "Site is already disabled."; return 0; fi
  vhost_disable; reload_apache; info "Site ${DOMAIN} disabled."
}
action_enable_site() {
  if site_enabled; then warn "Site is already enabled."; return 0; fi
  vhost_enable; reload_apache; info "Site ${DOMAIN} enabled."
}
action_view_error_log() {
  local elog="${APACHE_LOG_DIR}/${DOMAIN}.error.log"
  if [[ -f "$elog" ]]; then
    echo "  --- last 30 lines of ${elog} ---"; tail -n 30 "$elog"; echo "  --------------------------------"
  else
    warn "No error log found at $elog yet."
  fi
}

action_delete_site() {
  warn "WARNING: this will DELETE the vhost for ${DOMAIN}."
  local del_root="" do_backup="" confirm=""
  prompt_yn del_root "Also delete the document root (${ROOT_DIR})?"
  if [[ "$del_root" == "y" ]]; then
    prompt_yn do_backup "Back up the document root before deleting it?"
    [[ "$do_backup" == "y" ]] && backup_docroot
  fi
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    [[ "$FORCE" == "true" ]] || die "Refusing to delete non-interactively without --force."
  else
    prompt_yn confirm "Type y to permanently remove the ${DOMAIN} vhost"
    [[ "$confirm" == "y" ]] || { warn "Aborted; nothing deleted."; return 0; }
  fi
  [[ "$USE_A2" == "true" ]] && a2dissite "${DOMAIN}.conf" >>"$LOG_FILE" 2>&1 || true
  if [[ "$APACHE_CONF_MODE" == "confd" ]]; then
    rm -f "${APACHE_CONFD}/${DOMAIN}.conf" "${APACHE_CONFD}/${DOMAIN}.conf.disabled"
  else
    rm -f "${APACHE_SITES_ENABLED}/${DOMAIN}.conf" "${APACHE_SITES_AVAILABLE}/${DOMAIN}.conf"
  fi
  if [[ "$del_root" == "y" && -d "$ROOT_DIR" ]]; then
    rm -rf "$ROOT_DIR"; info "Deleted document root ${ROOT_DIR}."
  fi
  svc_reload "$APACHE_SVC" 2>/dev/null || true
  info "Vhost ${DOMAIN} deleted."
  [[ -n "$BACKUP_DIR" ]] && info "Backup kept at: ${BACKUP_DIR}"
  DOMAIN=""
}

manage_menu() {
  local choice=""
  while true; do
    echo
    echo "=================================="
    echo " Manage: ${DOMAIN}"
    echo "   root: ${ROOT_DIR}"
    echo "   $(site_enabled && echo enabled || echo disabled) | SSL: $(site_has_ssl && echo on || echo off)"
    echo "=================================="
    echo "  1) Show site info"
    echo "  2) Install WordPress (replaces doc root)"
    echo "  3) Enable/obtain SSL (certbot)"
    echo "  4) Back up the document root"
    echo "  5) Toggle directory listing"
    echo "  6) Repair permissions"
    echo "  7) $(site_enabled && echo 'Disable' || echo 'Enable') this site"
    echo "  8) View recent error log"
    echo "  9) Delete this vhost"
    echo "  b) Back to main menu"
    echo "  q) Quit"
    echo "----------------------------------"
    read -r -p "Choose an option: " choice
    case "$choice" in
      1) action_show_info ;;
      2) action_install_wordpress ;;
      3) action_enable_ssl ;;
      4) action_backup_only ;;
      5) action_toggle_listing ;;
      6) action_repair_perms ;;
      7) if site_enabled; then action_disable_site; else action_enable_site; fi ;;
      8) action_view_error_log ;;
      9) action_delete_site; [[ -z "$DOMAIN" ]] && return 0 ;;
      b|B) return 0 ;;
      q|Q) info "Bye."; exit 0 ;;
      *) warn "Invalid choice: '$choice'." ;;
    esac
  done
}

manage_existing_vhost() {
  info "=== Manage existing vhost ==="
  select_vhost
  validate_domain "$DOMAIN"
  local conf; conf="$(vhost_find)"
  VHOST_EXISTED="true"
  ROOT_DIR="$(vhost_docroot "$conf")"
  [[ -n "$ROOT_DIR" ]] || die "Could not read DocumentRoot from $conf."
  if [[ -z "$SITE_USER" ]]; then
    SITE_USER="$(vhost_user "$conf")"; [[ -n "$SITE_USER" ]] || SITE_USER="$(derive_user)"
  fi
  SITE_GROUP="$(id -gn "$SITE_USER" 2>/dev/null || echo "$SITE_USER")"
  info "Managing '${DOMAIN}'  (doc root: ${ROOT_DIR}, user: ${SITE_USER})"
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
    [[ "$INSTALL_WP" == "y" ]] && action_install_wordpress
    return 0
  fi
  manage_menu
}

# ----------------------------------------------------------------------------
# SSL / Let's Encrypt (certbot --apache)
# ----------------------------------------------------------------------------
ensure_certbot() {
  if ! command -v certbot >/dev/null 2>&1; then
    info "certbot not found; installing certbot + apache plugin..."
    ensure_dep certbot
  fi
  command -v certbot >/dev/null 2>&1 || die "Failed to install certbot."
}

install_ssl() {
  ensure_certbot
  if [[ -z "$SSL_EMAIL" ]]; then
    if [[ "$NON_INTERACTIVE" == "true" ]]; then
      die "SSL requested but no --email provided (required in non-interactive mode)."
    fi
    prompt SSL_EMAIL "Email for Let's Encrypt notices"
  fi
  info "Requesting Let's Encrypt certificate for ${DOMAIN} (and www)..."
  if certbot --apache --non-interactive --agree-tos --email "$SSL_EMAIL" --redirect \
       -d "$DOMAIN" -d "www.$DOMAIN" >>"$LOG_FILE" 2>&1; then
    info "TLS certificate installed; HTTP now redirects to HTTPS."
    if certbot renew --dry-run >>"$LOG_FILE" 2>&1; then
      info "Auto-renewal dry run succeeded."
    else
      warn "Auto-renewal dry run failed; check 'certbot renew --dry-run' manually."
    fi
  else
    warn "certbot failed (DNS not pointed here yet? port 80/443 blocked?)."
    warn "Site is live over HTTP. Re-run later: certbot --apache -d ${DOMAIN} -d www.${DOMAIN}"
  fi
}

# ----------------------------------------------------------------------------
# Finalize
# ----------------------------------------------------------------------------
reload_apache() {
  info "Testing Apache configuration..."
  if apache_configtest >>"$LOG_FILE" 2>&1; then
    svc_reload "$APACHE_SVC"
    info "Apache reloaded successfully."
  else
    error "Apache config test FAILED. Rolling back site enablement."
    vhost_disable
    die "See $LOG_FILE for the config test output."
  fi
}

# ----------------------------------------------------------------------------
# Menus
# ----------------------------------------------------------------------------
main_menu() {
  echo
  echo "=================================="
  echo "       Apache Site Manager"
  echo "     (${OS_PRETTY}, mpm-itk)"
  echo "=================================="
  echo "  1) Create a new site"
  echo "  2) Manage an existing site"
  echo "  q) Quit"
  echo "----------------------------------"
  local choice=""
  read -r -p "Choose an option [1/2/q]: " choice
  case "$choice" in
    1) MENU_CHOICE="create" ;;
    2) MENU_CHOICE="manage" ;;
    q|Q|"") MENU_CHOICE="quit" ;;
    *) warn "Invalid choice: '$choice' (pick 1, 2 or q)."; MENU_CHOICE="again" ;;
  esac
}

reset_state() {
  DOMAIN=""; ROOT_DIR=""; SITE_USER=""; SITE_GROUP=""
  INSTALL_PHP=""; INSTALL_WP=""; INSTALL_SSL=""; SSL_EMAIL=""
  ENABLE_LISTING="n"; MANAGE_MODE="false"
  EXISTING_DOCROOT="false"; VHOST_EXISTED="false"; BACKUP_DIR=""
  PHP_VERSION=""; PHP_MODE="mod_php"; PHP_FPM_SOCK=""; PHP_FPM_SERVICE=""; PHP_FPM_POOL_DIR=""
}

# ----------------------------------------------------------------------------
# Create flow
# ----------------------------------------------------------------------------
create_new_site() {
  prompt DOMAIN "Enter the domain name (e.g. example.com)"
  validate_domain "$DOMAIN"
  prompt ROOT_DIR "Document root" "/var/www/${DOMAIN}/public_html"
  [[ -z "$SITE_USER" ]] && SITE_USER="$(derive_user)"

  prompt_yn INSTALL_PHP "Install/enable PHP (mod_php) for this site?"
  prompt_yn INSTALL_WP  "Install WordPress on this domain?"
  prompt_yn INSTALL_SSL "Obtain a Let's Encrypt SSL certificate (certbot)?"
  [[ "$INSTALL_WP" == "y" ]] && INSTALL_PHP="y"

  create_site_user
  setup_docroot

  # PHP (mod_php) is only installed when the user chose PHP.
  [[ "$INSTALL_PHP" == "y" ]] && install_php

  # Enable itk + (if chosen) mod_php modules, then write and enable the vhost.
  enable_apache_modules
  write_apache_conf

  [[ "$INSTALL_WP" == "y" ]] && install_wordpress

  reload_apache

  [[ "$INSTALL_SSL" == "y" ]] && install_ssl

  info "=== Done. Site ${DOMAIN} is provisioned. ==="
  echo
  echo "Summary:"
  echo "  OS        : ${OS_PRETTY} (${OS_FAMILY})"
  echo "  Server    : Apache (${APACHE_SVC}) + mpm-itk"
  echo "  Domain    : ${DOMAIN}"
  echo "  Doc root  : ${ROOT_DIR}$([[ "$EXISTING_DOCROOT" == "true" ]] && echo ' (existing, preserved)')"
  echo "  Runs as   : ${SITE_USER}:${SITE_GROUP} (AssignUserId)"
  echo "  PHP       : ${INSTALL_PHP:-n} ${PHP_VERSION:+($PHP_VERSION, ${PHP_MODE})}"
  echo "  WordPress : ${INSTALL_WP:-n}"
  echo "  SSL/TLS   : ${INSTALL_SSL:-n}"
  local scheme="http"; [[ "$INSTALL_SSL" == "y" ]] && scheme="https"
  echo "  URL       : ${scheme}://${DOMAIN}/"
  [[ "$INSTALL_WP" == "y" ]] && echo "  Creds     : /root/${DOMAIN}.credentials.txt"
}

# ----------------------------------------------------------------------------
main() {
  local argc=$#
  parse_args "$@"
  require_root
  touch "$LOG_FILE"; chmod 640 "$LOG_FILE"
  info "=== Starting ($SCRIPT_NAME) ==="

  detect_os
  check_dependencies

  if [[ "$argc" -eq 0 ]]; then
    while true; do
      reset_state
      main_menu
      case "$MENU_CHOICE" in
        create) create_new_site ;;
        manage) manage_existing_vhost ;;
        quit)   info "Bye."; return 0 ;;
        again)  : ;;
      esac
    done
  fi

  if [[ "$MANAGE_MODE" == "true" ]]; then
    manage_existing_vhost
    info "=== Done (manage mode). ==="
    return 0
  fi
  create_new_site
}

main "$@"
