#!/usr/bin/env bash
# PrestaShop Classic interactive installer for a fresh Debian 13 host.
# v1.0.2: fix invalid file test and persist interactive settings securely.
# Downloads official Classic automation packages, not GitHub source snapshots.
set -Eeuo pipefail
umask 077

APP='PrestaShop Debian 13 Installer'
PHPV='8.4'
PHP_BIN="/usr/bin/php${PHPV}"
LOG=''
WORK=''
CONFIG_DIR='/var/lib/prestashop-tui'
CONFIG_FILE="$CONFIG_DIR/config.json"

# Defaults: edit everything from the menus before deployment.
RELEASE='latest'
DOMAIN='shop.example.test'
SLUG='class-shop'
BASE_URI='/'
TLS='none'
DISABLE_DEFAULT='yes'
SHOP_NAME='My Class Shop'
LANGUAGE='fr'
ALL_LANGUAGES='0'
COUNTRY='fr'
TIMEZONE='Europe/Paris'
THEME=''
FIXTURES='1'
MODULES=''
REWRITE='1'
ADMIN_FIRST='Admin'
ADMIN_LAST='Student'
ADMIN_EMAIL='admin@example.test'
ADMIN_PASS=''
DB_MODE='local'
DB_HOST='localhost'
DB_NAME='prestashop_class'
DB_USER='ps_class'
DB_PASS=''
DB_PREFIX='ps_'
DB_ENGINE='InnoDB'
PHP_MEMORY=512
PHP_UPLOAD=64
PHP_TIME=300
PHP_OPCACHE='1'
PHP_WORKERS=4
PHP_INPUT_VARS=10000

show_help() {
  cat <<'HELP'
Interactive PrestaShop Classic installation on Debian 13 (Trixie).
Usage: sudo bash prestashop-debian13-tui.sh
Requirements: fresh Debian 13, root, outbound HTTPS and APT access,
80/tcp reachable for Let's Encrypt (if chosen), enough disk space.
All choices are presented before anything is installed (apart from TUI tools).
This program never upgrades or deletes existing PrestaShop installations.
Selections, including passwords, are auto-saved to /var/lib/prestashop-tui/config.json
with root-only permissions, so rerunning restores the previous selections.
HELP
}
[[ ${1:-} == '--help' || ${1:-} == '-h' ]] && { show_help; exit 0; }
[[ $# -eq 0 ]] || { show_help >&2; exit 2; }
[[ $(id -u) -eq 0 ]] || { echo 'Run as root: sudo bash ...' >&2; exit 1; }
[[ -t 0 && -t 1 && -t 2 ]] || { echo 'An interactive terminal (TTY) is required.' >&2; exit 1; }
# shellcheck disable=SC1091
source /etc/os-release
[[ "$ID" == debian && "$VERSION_ID" == 13 ]] || {
  echo 'This version is intentionally restricted to Debian 13.' >&2; exit 1;
}
command -v apt-get >/dev/null || { echo 'apt-get not found.' >&2; exit 1; }
exec 9>/run/lock/prestashop-debian13-tui.lock
flock -n 9 || { echo 'Another installer session is already running.' >&2; exit 1; }
LOG="/var/log/prestashop-tui-$(date +%Y%m%d-%H%M%S)-$$.log"
touch "$LOG"
chmod 600 "$LOG"

cleanup() { [[ -z "$WORK" || ! -d "$WORK" ]] || rm -rf -- "$WORK"; }
trap cleanup EXIT
on_error() {
  local code=$1 line=$2 snippet=''
  trap - ERR
  snippet=$(tail -n 16 "$LOG" 2>/dev/null || true)
  if command -v whiptail >/dev/null; then
    whiptail --title 'Installation failed' --msgbox \
      "Stopped on line $line (exit $code).\n\nCheck: $LOG\n\nRecent output:\n$snippet\n\nExisting files are not automatically removed." 23 95 || true
  else
    printf 'Installer failed on line %s (exit %s). Log: %s\n%s\n' "$line" "$code" "$LOG" "$snippet" >&2
  fi
  exit "$code"
}
trap 'on_error "$?" "$LINENO"' ERR
run() { "$@" >>"$LOG" 2>&1; }
notice() {
  if command -v whiptail >/dev/null; then
    whiptail --title "$APP" --infobox "$1\n\nLog: $LOG" 9 85
  else
    printf '%s\n' "$1"
  fi
}
fail() { printf '%s\n' "ERROR: $*" >>"$LOG"; false; }

# Bootstrap only the TUI prerequisites; the full stack is installed after review.
if ! command -v whiptail >/dev/null || ! command -v curl >/dev/null || \
   ! command -v jq >/dev/null || ! command -v openssl >/dev/null || \
   ! command -v unzip >/dev/null || [[ ! -f /etc/ssl/certs/ca-certificates.crt ]]; then
  echo 'Installing basic interactive-installer tools (details in log)...'
  run env DEBIAN_FRONTEND=noninteractive apt-get update
  run env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    whiptail curl jq openssl unzip ca-certificates
fi
ADMIN_PASS=$(openssl rand -hex 20)
DB_PASS=$(openssl rand -hex 24)

# Persist only the explicitly listed configuration fields. JSON is data, never sourced
# as shell code. The enclosing directory is private to root, as are its files.
CONFIG_KEYS=(
  RELEASE DOMAIN SLUG BASE_URI TLS DISABLE_DEFAULT SHOP_NAME LANGUAGE
  ALL_LANGUAGES COUNTRY TIMEZONE THEME FIXTURES MODULES REWRITE
  ADMIN_FIRST ADMIN_LAST ADMIN_EMAIL ADMIN_PASS DB_MODE DB_HOST
  DB_NAME DB_USER DB_PASS DB_PREFIX DB_ENGINE PHP_MEMORY PHP_UPLOAD
  PHP_TIME PHP_OPCACHE PHP_WORKERS PHP_INPUT_VARS
)
ensure_config_dir() {
  [[ ! -L "$CONFIG_DIR" ]] || fail "Refusing symlinked config directory: $CONFIG_DIR"
  install -d -o root -g root -m 0700 "$CONFIG_DIR"
  [[ "$(stat -c %u "$CONFIG_DIR")" == 0 ]] || fail "Config directory is not root-owned: $CONFIG_DIR"
  chmod 700 "$CONFIG_DIR"
}
save_config() {
  ensure_config_dir
  [[ ! -L "$CONFIG_FILE" ]] || fail "Refusing symlinked config file: $CONFIG_FILE"
  local temp key filter='{'
  local -a arguments=()
  temp=$(mktemp "$CONFIG_DIR/.config.XXXXXXXX")
  for key in "${CONFIG_KEYS[@]}"; do
    arguments+=(--arg "$key" "${!key}")
    filter+="\"$key\":\$$key,"
  done
  filter="${filter%,}}"
  if ! jq -n "${arguments[@]}" "$filter" >"$temp"; then
    rm -f -- "$temp"
    fail 'Unable to write saved configuration.'
  fi
  chmod 600 "$temp"
  mv -f -- "$temp" "$CONFIG_FILE"
}
load_config() {
  [[ -e "$CONFIG_FILE" || -L "$CONFIG_FILE" ]] || return 0
  ensure_config_dir
  [[ ! -L "$CONFIG_FILE" && -f "$CONFIG_FILE" ]] || fail "Unsafe config path: $CONFIG_FILE"
  [[ "$(stat -c %u "$CONFIG_FILE")" == 0 && "$(stat -c %a "$CONFIG_FILE")" == 600 ]] || \
    fail "Saved config must be owned by root and chmod 600: $CONFIG_FILE"
  jq -e 'type == "object"' "$CONFIG_FILE" >/dev/null || fail "Invalid saved config JSON: $CONFIG_FILE"
  local key value
  for key in "${CONFIG_KEYS[@]}"; do
    if value=$(jq -er --arg name "$key" 'if (.[$name] | type) == "string" then .[$name] else empty end' "$CONFIG_FILE"); then
      printf -v "$key" '%s' "$value"
    fi
  done
  printf 'Loaded saved configuration from %s\n' "$CONFIG_FILE" >>"$LOG"
}
load_config

ui_menu() { whiptail --title "$APP" --cancel-button Back --menu "$1" 24 94 15 "${@:2}" 3>&1 1>&2 2>&3; }
ui_input() {
  local destination=$1 prompt=$2 current=${!1} answer
  answer=$(whiptail --title "$APP" --inputbox "$prompt" 11 87 "$current" 3>&1 1>&2 2>&3) || return 0
  printf -v "$destination" '%s' "$answer"
  save_config
}
ui_password() {
  local destination=$1 prompt=$2 answer
  answer=$(whiptail --title "$APP" --passwordbox "$prompt\n\nLeave empty to retain the current password." 12 87 3>&1 1>&2 2>&3) || return 0
  if [[ -n "$answer" ]]; then
    printf -v "$destination" '%s' "$answer"
    save_config
  fi
}
ui_yesno() { whiptail --title "$APP" --yesno "$1" 12 87; }
ui_info() { whiptail --title "$APP" --msgbox "$1" 20 92; }
ui_choice() {
  local destination=$1 question=$2 selection
  shift 2
  selection=$(whiptail --title "$APP" --cancel-button Back --menu "$question" 21 90 12 "$@" 3>&1 1>&2 2>&3) || return 0
  printf -v "$destination" '%s' "$selection"
  save_config
}

version_menu() {
  while true; do
    local choice
    choice=$(ui_menu "Version and deployment (current release selection: $RELEASE)" \
      release 'latest stable via official release API, or pin a tag' \
      domain "Host name or IP ($DOMAIN)" \
      slug "Dedicated site identifier ($SLUG)" \
      base "Shop URL base path ($BASE_URI)" \
      tls "HTTPS option ($TLS)" \
      default "Disable Apache default site ($DISABLE_DEFAULT)") || break
    case "$choice" in
      release) ui_input RELEASE 'Use latest, or a Classic release tag such as 9.2.0-6.0.' ;;
      domain) ui_input DOMAIN 'Domain (no scheme, port, or path), or the server IP.\nFor an internal class site you can use e.g. shop.test in /etc/hosts.' ;;
      slug) ui_input SLUG 'Unique slug for folder, Apache vhost, and PHP-FPM pool (letters, numbers, hyphens).' ;;
      base) ui_input BASE_URI 'Base URI: / or e.g. /class/ (must begin and end with /).' ;;
      tls) ui_choice TLS "HTTPS provisioning. Let's Encrypt requires a publicly resolvable domain and TCP port 80." \
        none 'HTTP only (good for LAN/class labs)' letsencrypt 'Certbot / public DNS / auto redirect' ;;
      default) ui_choice DISABLE_DEFAULT 'The default Apache page may interfere with access by IP.' yes 'Disable only 000-default.conf' no 'Leave default site enabled' ;;
    esac
  done
}
shop_menu() {
  while true; do
    local choice
    choice=$(ui_menu "Shop identity and installation defaults" \
      name "Shop name ($SHOP_NAME)" \
      language "Primary language ($LANGUAGE)" \
      country "Country ISO-3166 ($COUNTRY)" \
      timezone "PHP/PrestaShop timezone ($TIMEZONE)" \
      languages "Install all languages ($ALL_LANGUAGES)" \
      theme "Theme (${THEME:-release default})" \
      fixtures "Demo catalog (1=yes, 0=no): $FIXTURES" \
      modules "Bundled module selection (${MODULES:-all default})" \
      rewrite "Friendly URLs / Apache rewrite ($REWRITE)") || break
    case "$choice" in
      name) ui_input SHOP_NAME 'Public-facing shop name.' ;;
      language) ui_input LANGUAGE 'Two-letter shop language ISO code, e.g. fr, en, es.' ;;
      country) ui_input COUNTRY 'Two-letter country ISO code, e.g. fr, be, gb.' ;;
      timezone) ui_input TIMEZONE 'Timezone identifier, e.g. Europe/Paris.' ;;
      languages) ui_choice ALL_LANGUAGES 'Install all available language packs?' 0 'No' 1 'Yes' ;;
      theme) ui_choice THEME 'Choose a theme bundled with your selected release.' '' 'Version default (recommended)' hummingbird 'Hummingbird' classic 'Classic' ;;
      fixtures) ui_choice FIXTURES 'Install sample products/categories for the classroom?' 1 'Yes, demo data' 0 'No, empty catalog' ;;
      modules) ui_input MODULES 'Comma-separated module directory names, or EMPTY to use all bundled defaults.\nRestricting modules can remove important shop functions.' ;;
      rewrite) ui_choice REWRITE 'Enable Apache URL rewriting (recommended)?' 1 'Yes' 0 'No' ;;
    esac
  done
}
admin_menu() {
  while true; do
    local choice
    choice=$(ui_menu 'Administrator account (password is never displayed in review)' \
      first "First name ($ADMIN_FIRST)" last "Last name ($ADMIN_LAST)" \
      email "Login e-mail ($ADMIN_EMAIL)" pass 'Change password (generated by default)') || break
    case "$choice" in
      first) ui_input ADMIN_FIRST 'Back-office first name.' ;;
      last) ui_input ADMIN_LAST 'Back-office last name.' ;;
      email) ui_input ADMIN_EMAIL 'Back-office email (used to log in).' ;;
      pass) ui_password ADMIN_PASS 'Enter a strong administrator password (at least 12 characters).' ;;
    esac
  done
}
db_menu() {
  while true; do
    local choice
    choice=$(ui_menu "Database: $DB_MODE / $DB_HOST" \
      mode "Mode ($DB_MODE)" host "Server / optional port ($DB_HOST)" \
      name "Database name ($DB_NAME)" user "Database user ($DB_USER)" \
      pass 'Set database password (hidden)' \
      prefix "Table prefix ($DB_PREFIX)" engine "Storage engine ($DB_ENGINE)") || break
    case "$choice" in
      mode)
        ui_choice DB_MODE 'Local: create isolated DB/user via MariaDB root UNIX socket.\nRemote: use a pre-created database and account.' \
          local 'Install and initialize MariaDB' remote 'Use existing MySQL / MariaDB service'
        [[ "$DB_MODE" == remote && "$DB_HOST" == localhost ]] && DB_HOST='db.example.test:3306'
        [[ "$DB_MODE" == local ]] && DB_HOST='localhost'
        save_config
        ;;
      host)
        if [[ "$DB_MODE" == remote ]]; then
          ui_input DB_HOST 'Remote server hostname or IPv4 with optional :port. The DB must already exist.'
        else
          ui_info 'Local database mode always uses localhost and the local UNIX socket.'
        fi
        ;;
      name) ui_input DB_NAME 'New database name (must not already exist in local mode).' ;;
      user) ui_input DB_USER 'SQL user (local mode creates this account if unused).' ;;
      pass) ui_password DB_PASS 'SQL user password.' ;;
      prefix) ui_input DB_PREFIX 'Table prefix (recommended unique, letters/numbers/underscores, ends in underscore).' ;;
      engine) ui_choice DB_ENGINE 'Choose MySQL table storage engine.' InnoDB 'InnoDB (recommended)' MyISAM 'Legacy MyISAM (discouraged)' ;;
    esac
  done
}
php_menu() {
  while true; do
    local choice
    choice=$(ui_menu 'PHP 8.4 isolated PHP-FPM pool settings' \
      memory "memory_limit (${PHP_MEMORY}M)" \
      upload "upload_max_filesize (${PHP_UPLOAD}M; post_max_size adds 8M)" \
      execution "max_execution_time (${PHP_TIME}s)" \
      workers "FPM max children ($PHP_WORKERS)" \
      inputvars "max_input_vars ($PHP_INPUT_VARS)" \
      opcache "OPcache enabled ($PHP_OPCACHE)") || break
    case "$choice" in
      memory) ui_input PHP_MEMORY 'Maximum memory per PHP request in MiB (min 512).' ;;
      upload) ui_input PHP_UPLOAD 'Maximum upload size in MiB (2-1024).' ;;
      execution) ui_input PHP_TIME 'Max execution time in seconds (30-3600).' ;;
      workers) ui_input PHP_WORKERS 'FPM max children (1-20); more workers need more RAM.' ;;
      inputvars) ui_input PHP_INPUT_VARS 'PHP max_input_vars (1000-50000; recommended 10000).' ;;
      opcache) ui_choice PHP_OPCACHE 'PHP opcode caching?' 1 'Enabled' 0 'Disabled' ;;
    esac
  done
}

validate() {
  [[ "$RELEASE" == latest || "$RELEASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+$ ]] || { ui_info 'Invalid release tag.'; return 1; }
  [[ "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*$ ]] || { ui_info 'Invalid domain or IP: no ports, schemes, or paths.'; return 1; }
  [[ "$SLUG" =~ ^[a-z][a-z0-9-]{1,29}$ ]] || { ui_info 'Slug must be 2-30 characters: lowercase a-z, numbers, hyphens (start with a letter).'; return 1; }
  [[ "$BASE_URI" == '/' || "$BASE_URI" =~ ^/([a-zA-Z0-9_-]+/)+$ ]] || { ui_info 'Base URI must look like / or /class/ and not contain spaces or ../'; return 1; }
  [[ -n "$SHOP_NAME" && -n "$ADMIN_FIRST" && -n "$ADMIN_LAST" ]] || { ui_info 'Shop name and administrator names are mandatory.'; return 1; }
  [[ "$ADMIN_EMAIL" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]] || { ui_info 'Invalid administrator email.'; return 1; }
  [[ ${#ADMIN_PASS} -ge 12 && "$ADMIN_PASS" != *$'\n'* && "$ADMIN_PASS" != *$'\r'* ]] || { ui_info 'Administrator password must have at least 12 characters, with no newlines.'; return 1; }
  [[ ${#DB_PASS} -ge 12 && "$DB_PASS" != *$'\n'* && "$DB_PASS" != *$'\r'* ]] || { ui_info 'Database password must have at least 12 characters, with no newlines.'; return 1; }
  [[ "$LANGUAGE" =~ ^[a-z]{2}$ && "$COUNTRY" =~ ^[a-z]{2}$ ]] || { ui_info 'Language and country must be two lowercase letters.'; return 1; }
  [[ -f "/usr/share/zoneinfo/$TIMEZONE" && "$TIMEZONE" != *'..'* ]] || { ui_info 'Invalid timezone (check /usr/share/zoneinfo/).'; return 1; }
  [[ "$DB_NAME" =~ ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ && "$DB_USER" =~ ^[a-zA-Z][a-zA-Z0-9_]{0,31}$ ]] || { ui_info 'DB name/user may only contain letters, numbers, underscore, starting with a letter.'; return 1; }
  [[ "$DB_PREFIX" =~ ^[a-zA-Z][a-zA-Z0-9_]{0,19}_$ ]] || { ui_info 'Table prefix must use alphanumerics/underscore and end in underscore.'; return 1; }
  [[ "$DB_MODE" == local || "$DB_HOST" =~ ^[a-zA-Z0-9._-]+(:[0-9]{1,5})?$ ]] || { ui_info 'Invalid remote DB hostname/IPv4 or port.'; return 1; }
  [[ "$MODULES" =~ ^[a-zA-Z0-9_,]*$ ]] || { ui_info 'Modules must be comma-separated directory names, with no spaces.'; return 1; }
  [[ "$PHP_MEMORY" =~ ^[1-9][0-9]*$ && "$PHP_MEMORY" -ge 512 && "$PHP_MEMORY" -le 4096 ]] || { ui_info 'Memory limit must be 512-4096 MiB.'; return 1; }
  [[ "$PHP_UPLOAD" =~ ^[1-9][0-9]*$ && "$PHP_UPLOAD" -ge 2 && "$PHP_UPLOAD" -le 1024 ]] || { ui_info 'Upload limit must be 2-1024 MiB.'; return 1; }
  [[ "$PHP_TIME" =~ ^[1-9][0-9]*$ && "$PHP_TIME" -ge 30 && "$PHP_TIME" -le 3600 ]] || { ui_info 'Execution time must be 30-3600 seconds.'; return 1; }
  [[ "$PHP_WORKERS" =~ ^[1-9][0-9]*$ && "$PHP_WORKERS" -ge 1 && "$PHP_WORKERS" -le 20 ]] || { ui_info 'FPM workers must be 1-20.'; return 1; }
  [[ "$PHP_INPUT_VARS" =~ ^[1-9][0-9]*$ && "$PHP_INPUT_VARS" -ge 1000 && "$PHP_INPUT_VARS" -le 50000 ]] || { ui_info 'PHP max_input_vars must be 1000-50000.'; return 1; }
  [[ "$TLS" != letsencrypt || ( "$DOMAIN" == *.* && "$DOMAIN" != *.test && "$DOMAIN" != *.local && "$DOMAIN" != *.internal && ! "$DOMAIN" =~ ^[0-9.]+$ ) ]] || {
    ui_info "Let's Encrypt needs a publicly resolvable DNS hostname (not an IP or local/test domain)."; return 1;
  }
  return 0
}

review() {
  local summary
  summary="PrestaShop Classic:  $RELEASE\nSite:               $DOMAIN$BASE_URI\nTarget directory:   /var/www/prestashop-$SLUG/public$BASE_URI\nApache vhost:       ps-$SLUG.conf\nTLS:                $TLS\nDisable default:    $DISABLE_DEFAULT\n\nShop name:          $SHOP_NAME\nLocale/country:     $LANGUAGE / $COUNTRY\nTimezone:           $TIMEZONE\nAll languages:      $ALL_LANGUAGES\nTheme:              ${THEME:-release default}\nDemo products:      $FIXTURES\nModules:            ${MODULES:-all bundled}\nRewrite:            $REWRITE\n\nAdmin:              $ADMIN_FIRST $ADMIN_LAST ($ADMIN_EMAIL)\nDB:                 $DB_MODE ($DB_HOST / $DB_NAME)\nDB user:            $DB_USER\nPrefix/engine:      $DB_PREFIX / $DB_ENGINE\nPHP:                8.4 / ${PHP_MEMORY}M RAM / ${PHP_UPLOAD}M upload / ${PHP_TIME}s\nOPcache:            $PHP_OPCACHE\nFPM max children:    $PHP_WORKERS\nmax_input_vars:     $PHP_INPUT_VARS\n\nPasswords:          hidden; shown after successful install."
  whiptail --title 'Review configuration (no changes made yet)' --scrolltext --msgbox "$summary" 29 95
}

sql_quote() {
  local val=${1//\\/\\\\}
  val=${val//\'/\'\'}
  printf "'%s'" "$val"
}
check_existing() {
  local root="/var/www/prestashop-$SLUG/public"
  local target="$root${BASE_URI%/}"
  local conf="/etc/apache2/sites-available/ps-$SLUG.conf"
  [[ ! -e "$conf" ]] || fail "Existing Apache vhost: $conf; refusing to overwrite."
  [[ ! -e "/etc/php/$PHPV/fpm/pool.d/ps-$SLUG.conf" ]] || fail 'This PHP-FPM pool already exists.'
  [[ ! -e "$target/index.php" ]] || fail "Existing web application: $target."
  if [[ -d "$root" ]] && [[ -n "$(find "$root" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
    fail "Site root $root isn't empty; refusing to overwrite."
  fi
  if systemctl is-active --quiet nginx; then fail 'Nginx is active. This installer manages Apache only.'; fi
  # Never silently take over a server that already serves unrelated Apache vhosts.
  if [[ -d /etc/apache2/sites-enabled ]]; then
    local file
    for file in /etc/apache2/sites-enabled/*.conf; do
      [[ -e "$file" ]] || continue
      [[ "$(basename "$file")" == 000-default.conf ]] || fail "Other Apache vhost enabled: $file; use a fresh VM/CT."
    done
  fi
  if command -v ss >/dev/null && ! systemctl is-active --quiet apache2; then
    if ss -H -ltn '( sport = :80 )' | grep -q .; then fail 'TCP port 80 is already in use by another service.'; fi
  fi
  local avail
  avail=$(df -Pk /var | awk 'NR==2 {print $4}')
  [[ "$avail" =~ ^[0-9]+$ && "$avail" -ge 1048576 ]] || fail 'At least 1 GiB of free space required in /var/www.'
}

resolve_release() {
  if [[ "$RELEASE" == latest ]]; then
    notice 'Looking up the latest official PrestaShop Classic release...'
    local payload
    payload=$(curl -fsSL --retry 3 --connect-timeout 12 --max-time 45 \
      -H 'Accept: application/vnd.github+json' \
      'https://api.github.com/repos/PrestaShopCorp/prestashop-classic/releases/latest') || \
      fail 'Could not query the official GitHub release API. Pin a release tag in the menu and retry.'
    RELEASE=$(jq -er '.tag_name' <<<"$payload")
  fi
  [[ "$RELEASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+-[0-9]+\.[0-9]+$ ]] || fail "Unsupported official release tag: $RELEASE"
  # Installer package handling and PHP 8.4 are known for PrestaShop 9.
  [[ "$RELEASE" == 9.* ]] || fail "Release $RELEASE is outside the verified PrestaShop 9 series; update this script first."
  printf 'Release resolved: %s\n' "$RELEASE" >>"$LOG"
}

install_packages() {
  notice 'Installing Apache, PHP 8.4-FPM/extensions, and database clients...'
  run env DEBIAN_FRONTEND=noninteractive apt-get update
  local packages=(apache2 "php${PHPV}-fpm" "php${PHPV}-cli"
    "php${PHPV}-common" "php${PHPV}-mysql" "php${PHPV}-curl" "php${PHPV}-gd"
    "php${PHPV}-intl" "php${PHPV}-mbstring" "php${PHPV}-xml"
    "php${PHPV}-zip" "php${PHPV}-bcmath" "php${PHPV}-soap"
    "php${PHPV}-opcache" mariadb-client tzdata)
  [[ "$DB_MODE" == local ]] && packages+=(mariadb-server)
  [[ "$TLS" == letsencrypt ]] && packages+=(certbot python3-certbot-apache)
  run env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
  [[ -x "$PHP_BIN" ]] || fail "PHP binary $PHP_BIN is missing."
  local ext
  for ext in curl dom fileinfo gd iconv intl mbstring openssl pdo_mysql simplexml zip; do
    if ! "$PHP_BIN" -m | grep -qiE "^${ext}$"; then
      fail "PHP extension $ext is not enabled for PHP $PHPV (CLI)."
    fi
  done
  run systemctl enable --now "php${PHPV}-fpm"
  if [[ "$DB_MODE" == local ]]; then run systemctl enable --now mariadb; fi
}

download_release() {
  notice "Downloading the official Classic $RELEASE package..."
  WORK=$(mktemp -d /var/tmp/prestashop-tui.XXXXXXXX)
  local url="https://assets.prestashop3.com/dst/edition/corporate/$RELEASE/prestashop_edition_classic_version_$RELEASE.zip?source=class-tui"
  run curl --fail --location --retry 3 --connect-timeout 15 --max-time 1800 \
    --output "$WORK/classic.zip" "$url"
  run unzip -tq "$WORK/classic.zip"
  mkdir -p "$WORK/wrapper" "$WORK/site"
  run unzip -q "$WORK/classic.zip" -d "$WORK/wrapper"
  [[ -s "$WORK/wrapper/prestashop.zip" ]] || fail 'Downloaded archive is not an official Classic wrapper (missing prestashop.zip).'
  run unzip -tq "$WORK/wrapper/prestashop.zip"
  run unzip -q "$WORK/wrapper/prestashop.zip" -d "$WORK/site"
  [[ -f "$WORK/site/index.php" ]] || fail 'Expected PrestaShop files missing (index.php).'
  [[ -f "$WORK/site/install/index_cli.php" || -f "$WORK/site/install-dev/index_cli.php" ]] || \
    fail 'PrestaShop CLI installer missing in downloaded archive.'
  if [[ -n "$THEME" && ! -d "$WORK/site/themes/$THEME" ]]; then
    fail "Requested theme $THEME not found in release $RELEASE."
  fi
  if [[ -n "$MODULES" ]]; then
    local -a selected
    IFS=, read -ra selected <<<"$MODULES"
    local module
    for module in "${selected[@]}"; do
      [[ -d "$WORK/site/modules/$module" ]] || fail "Requested module $module not found in release."
    done
  fi
}

create_database() {
  notice "Configuring $DB_MODE database..."
  if [[ "$DB_MODE" == local ]]; then
    run mariadb --protocol=socket -e 'SELECT VERSION();'
    if [[ -n "$(mariadb --protocol=socket -N -B -e "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${DB_NAME}'" 2>>"$LOG")" ]]; then
      fail "Database $DB_NAME already exists; refusing to reuse or clear it."
    fi
    if [[ -n "$(mariadb --protocol=socket -N -B -e "SELECT User FROM mysql.user WHERE User='${DB_USER}' AND Host='localhost'" 2>>"$LOG")" ]]; then
      fail "Local DB user $DB_USER already exists; choose a different username."
    fi
    local sql_pass sql_user
    sql_pass=$(sql_quote "$DB_PASS")
    sql_user=$(sql_quote "$DB_USER")
    # Never log SQL commands containing credentials.
    mariadb --protocol=socket <<SQL >>"$LOG" 2>&1
CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER $sql_user@'localhost' IDENTIFIED BY $sql_pass;
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO $sql_user@'localhost';
SQL
  else
    local db_hostname=$DB_HOST db_port=3306
    if [[ "$DB_HOST" == *:* ]]; then
      db_hostname=${DB_HOST%:*}; db_port=${DB_HOST##*:}
    fi
    MYSQL_PWD="$DB_PASS" mariadb --connect-timeout=8 \
      --host="$db_hostname" --port="$db_port" --user="$DB_USER" --database="$DB_NAME" \
      -Nse 'SELECT 1;' >>"$LOG" 2>&1 || fail 'Remote DB login failed or database does not exist.'
    local tables
    tables=$(MYSQL_PWD="$DB_PASS" mariadb --connect-timeout=8 \
      --host="$db_hostname" --port="$db_port" --user="$DB_USER" --database="$DB_NAME" \
      -Nse 'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE();' 2>>"$LOG")
    [[ "$tables" == 0 ]] || fail 'Remote DB is not empty. Refusing to install into an existing database.'
  fi
}

configure_services() {
  notice 'Creating isolated PHP-FPM pool and Apache virtual host...'
  local root="/var/www/prestashop-$SLUG/public"
  local dest="$root${BASE_URI%/}" socket="/run/php/php${PHPV}-fpm-ps-$SLUG.sock"
  mkdir -p "$dest"
  chmod 755 "/var/www/prestashop-$SLUG"
  run cp -a "$WORK/site/." "$dest/"
  run chown -R www-data:www-data "$root"
  # Secure baseline, allowing PrestaShop to manage themes, modules, caches and uploads.
  run find "$root" -type d -exec chmod 750 '{}' +
  run find "$root" -type f -exec chmod 640 '{}' +
  cat >"/etc/php/$PHPV/fpm/pool.d/ps-$SLUG.conf" <<FPM
[ps-$SLUG]
user = www-data
group = www-data
listen = $socket
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = $PHP_WORKERS
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = $PHP_WORKERS
php_admin_value[memory_limit] = ${PHP_MEMORY}M
php_admin_value[upload_max_filesize] = ${PHP_UPLOAD}M
php_admin_value[post_max_size] = $((PHP_UPLOAD + 8))M
php_admin_value[max_execution_time] = $PHP_TIME
php_admin_value[max_input_vars] = $PHP_INPUT_VARS
php_admin_value[date.timezone] = $TIMEZONE
php_admin_flag[allow_url_fopen] = on
php_admin_flag[opcache.enable] = ${PHP_OPCACHE}
FPM
  local redirect=''
  [[ "$BASE_URI" == '/' ]] || redirect="RedirectMatch ^/\$ ${BASE_URI}"
  cat >"/etc/apache2/sites-available/ps-$SLUG.conf" <<APACHE
<VirtualHost *:80>
    ServerName $DOMAIN
    DocumentRoot $root
    DirectoryIndex index.php
    ErrorLog \${APACHE_LOG_DIR}/prestashop-$SLUG-error.log
    CustomLog \${APACHE_LOG_DIR}/prestashop-$SLUG-access.log combined

    <Directory "$root">
        Require all granted
        AllowOverride All
        Options FollowSymLinks
    </Directory>
    <FilesMatch "\\.php$">
        SetHandler "proxy:unix:$socket|fcgi://localhost/"
    </FilesMatch>
    $redirect
</VirtualHost>
APACHE
  run a2enmod rewrite proxy_fcgi setenvif headers
  if [[ "$DISABLE_DEFAULT" == yes ]]; then run a2dissite 000-default.conf; fi
  run a2ensite "ps-$SLUG.conf"
  run "php-fpm${PHPV}" -t
  run apache2ctl configtest
  run systemctl restart "php${PHPV}-fpm"
  run systemctl enable --now apache2
  run systemctl reload apache2
}

configure_https() {
  [[ "$TLS" == letsencrypt ]] || return 0
  notice "Requesting a public Let's Encrypt certificate via Apache..."
  run certbot --apache --non-interactive --agree-tos --redirect \
    --email "$ADMIN_EMAIL" -d "$DOMAIN" \
    || fail 'Certificate request failed. Check public DNS, reachability on port 80, and Certbot log.'
  run apache2ctl configtest
  run systemctl reload apache2
}

install_prestashop() {
  notice 'Installing PrestaShop through its official CLI installer...'
  local root="/var/www/prestashop-$SLUG/public"
  local dest="$root${BASE_URI%/}"
  local installer="$dest/install/index_cli.php"
  [[ -f "$installer" ]] || installer="$dest/install-dev/index_cli.php"
  local -a args=(
    "--domain=$DOMAIN" "--base_uri=$BASE_URI" "--language=$LANGUAGE"
    "--all_languages=$ALL_LANGUAGES" "--timezone=$TIMEZONE"
    "--db_server=$DB_HOST" "--db_name=$DB_NAME" "--db_user=$DB_USER"
    "--db_password=$DB_PASS" '--db_clear=0' '--db_create=0'
    "--prefix=$DB_PREFIX" "--engine=$DB_ENGINE"
    "--name=$SHOP_NAME" "--country=$COUNTRY"
    "--firstname=$ADMIN_FIRST" "--lastname=$ADMIN_LAST"
    "--email=$ADMIN_EMAIL" "--password=$ADMIN_PASS"
    "--ssl=$([[ "$TLS" == letsencrypt ]] && printf 1 || printf 0)"
    "--rewrite=$REWRITE" "--fixtures=$FIXTURES" '--license=0'
  )
  [[ -z "$THEME" ]] || args+=("--theme=$THEME")
  [[ -z "$MODULES" ]] || args+=("--modules=$MODULES")
  # PrestaShop's official CLI requires password arguments. Do not log the command.
  # Run as the PHP site user, never as root. Passwords are transiently visible in argv.
  runuser -u www-data -- "$PHP_BIN" -d "memory_limit=${PHP_MEMORY}M" \
    -d "max_execution_time=${PHP_TIME}" \
    "$installer" "${args[@]}" >>"$LOG" 2>&1
  # PrestaShop's CLI sometimes returns 0 even if warnings occurred: check DB/config too.
  [[ -s "$dest/app/config/parameters.php" || -s "$dest/config/settings.inc.php" ]] || \
    fail 'Installation did not produce the expected application configuration file.'
  if [[ -d "$dest/install" ]]; then rm -rf -- "$dest/install"; fi
  if [[ -d "$dest/install-dev" ]]; then rm -rf -- "$dest/install-dev"; fi
  if [[ -d "$dest/admin" ]]; then
    mv -- "$dest/admin" "$dest/admin$(openssl rand -hex 8)"
  fi
  local admin_dir
  admin_dir=$(find "$dest" -maxdepth 1 -type d -name 'admin*' -printf '%f\n' | head -n 1)
  [[ -n "$admin_dir" ]] || fail 'Back-office directory not found after installation.'
  printf '%s' "$admin_dir" >"$WORK/admin_dir"
  run chown -R www-data:www-data "$dest"
  run apache2ctl configtest
  run systemctl reload apache2
}

check_http() {
  local url="http://$DOMAIN$BASE_URI" scheme='http'
  if [[ "$TLS" == letsencrypt ]]; then
    url="https://$DOMAIN$BASE_URI"; scheme='https'
  fi
  # Force localhost only for health checks; don't require LAN/public DNS at runtime.
  local code=''
  code=$(curl -k -sS -L --connect-timeout 5 --max-time 20 \
    --resolve "$DOMAIN:80:127.0.0.1" --resolve "$DOMAIN:443:127.0.0.1" \
    -o /dev/null -w '%{http_code}' "$url" 2>>"$LOG") || code='curl_failed'
  printf 'Local storefront HTTP check (%s): %s\n' "$scheme" "$code" >>"$LOG"
  if [[ "$code" != 200 ]]; then
    ui_info "The installation completed, but the local storefront HTTP check returned: $code\n\nCheck Apache/PHP/PrestaShop logs after finishing.\nInstaller log: $LOG"
  fi
}

finish() {
  local scheme=http
  [[ "$TLS" == letsencrypt ]] && scheme=https
  local admin_dir
  admin_dir=$(cat "$WORK/admin_dir")
  local url="$scheme://$DOMAIN$BASE_URI"
  local back_office="${url}${admin_dir}/"
  local summary="Installation completed.\n\nSHOP: $url\nADMIN: $back_office\nEMAIL: $ADMIN_EMAIL\nADMIN PASSWORD: $ADMIN_PASS\nDB NAME: $DB_NAME\nDB USER: $DB_USER\nDB PASSWORD: $DB_PASS\n\nLOG: $LOG\nSAVED CONFIG: $CONFIG_FILE\n\nSaved config includes credentials and is readable only by root. Remove it when no longer needed."
  whiptail --title 'SUCCESS — save credentials now' --scrolltext --msgbox "$summary" 25 98
  printf '\nPrestaShop %s successfully installed.\n' "$RELEASE"
  printf 'Shop URL:  %s\nAdmin URL: %s\nLogin:     %s\n' "$url" "$back_office" "$ADMIN_EMAIL"
  printf 'Install log: %s\nSaved config (root-only, includes passwords): %s\n' "$LOG" "$CONFIG_FILE"
}

main() {
  # Store generated passwords on the first launch, too. Subsequent runs load
  # the previous values instead of silently generating fresh credentials.
  save_config
  while true; do
    local choice
    choice=$(whiptail --title "$APP" --cancel-button Quit --menu \
      "Choices are saved automatically to $CONFIG_FILE.\nIncludes passwords (root-only). A dedicated fresh VM/LXC is recommended." \
      24 98 15 \
      1 'Version, hostname, site path, HTTPS' \
      2 'Shop identity, language, themes and demo data' \
      3 'Administrator credentials' \
      4 'Database settings and local/remote mode' \
      5 'PHP limits and OPcache' \
      6 'Review current configuration' \
      7 'VALIDATE AND INSTALL' \
      8 'Exit without installing (settings stay saved)' \
      3>&1 1>&2 2>&3) || exit 0
    case "$choice" in
      1) version_menu ;; 2) shop_menu ;; 3) admin_menu ;;
      4) db_menu ;; 5) php_menu ;; 6) review ;;
      7)
        validate || continue
        review
        if ui_yesno 'This will install system packages, create a dedicated database/user (local mode), configure Apache and PHP-FPM, and install PrestaShop.\n\nExisting shop data will NOT be overwritten.\n\nProceed?'; then
          check_existing
          resolve_release
          install_packages
          check_existing
          download_release
          create_database
          configure_services
          configure_https
          install_prestashop
          check_http
          finish
          return 0
        fi
        ;;
      8) exit 0 ;;
    esac
  done
}
main
