#!/usr/bin/env bash
# PrestaShop Classic interactive installer for Debian 13.
# v1.1.0: safely replace failed/installed managed shops with verified backup.
# Preserves separate TLS vhosts and root-only settings.
# Downloads official Classic automation packages, not GitHub source snapshots.
set -Eeuo pipefail
umask 077

APP='PrestaShop Debian 13 TUI v1.1.0'
PHPV='8.4'
PHP_BIN="/usr/bin/php${PHPV}"
LOG=''
WORK=''
CONFIG_DIR='/var/lib/prestashop-tui'
CONFIG_FILE="$CONFIG_DIR/config.json"
BACKUP_BASE='/var/backups/prestashop-tui'
LAST_BACKUP=''
BACKUP_COMPLETE=0
LAST_ERROR=''


# Defaults: edit everything from the menus before deployment.
RELEASE='latest'
INSTALL_MODE='fresh'
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: 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).
Fresh mode refuses existing installations. Replace mode can reset a failed or
fully installed shop made with this installer, after an automatic root-only
backup and an explicit confirmation. Only local MariaDB is supported for reset.
Replacement is a DESTRUCTIVE NEW INSTALL, not an in-place PrestaShop upgrade.
Existing separately configured HTTPS virtual hosts and certificates are preserved.
Selections, including passwords, are auto-saved to /var/lib/prestashop-tui/config.json
with root-only permissions, so rerunning restores the previous selections.
Backups: /var/backups/prestashop-tui/<slug>/<timestamp>/
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='' backup_info=''
  trap - ERR
  snippet=$(tail -n 16 "$LOG" 2>/dev/null || true)
  if [[ -n "$LAST_BACKUP" ]]; then
    if [[ "$BACKUP_COMPLETE" == 1 ]]; then
      backup_info="\n\nVerified backup: $LAST_BACKUP\nSee README for restoration."
    else
      backup_info="\n\nIncomplete backup directory: $LAST_BACKUP\nOld data should still be intact if reset had not started."
    fi
  fi
  if command -v whiptail >/dev/null; then
    whiptail --title 'Installation failed' --scrolltext --msgbox \
      "Stopped on line $line (exit $code).\n\n${LAST_ERROR:+Error: $LAST_ERROR\n\n}Log: $LOG$backup_info\n\nRecent output:\n$snippet" 27 100 || true
  else
    printf 'Installer failed on line %s (exit %s).\n%s\nLog: %s\nBackup: %s\n%s\n' \
      "$line" "$code" "$LAST_ERROR" "$LOG" "${LAST_BACKUP:-none}" "$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() {
  LAST_ERROR="$*"
  printf '%s\n' "ERROR: $LAST_ERROR" >>"$LOG"
  on_error 1 "${BASH_LINENO[0]:-unknown}"
}

# 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 INSTALL_MODE 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' \
      mode "Installation mode ($INSTALL_MODE)" \
      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.' ;;
      mode) ui_choice INSTALL_MODE 'Fresh refuses existing data. Replace makes a verified backup and wipes the selected shop, DB, and generated HTTP/PHP configuration. All shop products, users, orders and changes are reset.' \
        fresh 'New installation; never overwrite' \
        replace 'Reinstall/reset failed or installed shop (BACKUP + ERASE)'
        if [[ "$INSTALL_MODE" == replace && "$TLS" == none ]] &&
           [[ -f "/etc/apache2/sites-available/ps-$SLUG-ssl.conf" || -f "/etc/apache2/sites-available/ps-$SLUG-le-ssl.conf" ]]; then
          if ui_yesno 'Existing HTTPS virtual host detected. Preserve it and enable SSL inside PrestaShop during reinstall?'; then
            TLS=existing
            save_config
          fi
        fi
        ;;
      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' existing 'Preserve existing HTTPS vhost/cert, and enable shop SSL' ;;
      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 'Database name. In replace mode, this LOCAL database is backed up, dropped and recreated.' ;;
      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() {
  [[ "$INSTALL_MODE" == fresh || "$INSTALL_MODE" == replace ]] || { ui_info 'Invalid installation mode.'; return 1; }
  [[ "$TLS" == none || "$TLS" == letsencrypt || "$TLS" == existing ]] || { ui_info 'Invalid TLS mode.'; return 1; }
  if [[ "$INSTALL_MODE" == replace && "$DB_MODE" != local ]]; then
    ui_info 'Replace mode currently supports LOCAL MariaDB only. It will not clear remote databases.'
    return 1
  fi
  [[ "$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 mode_warning
  if [[ "$INSTALL_MODE" == replace ]]; then
    mode_warning='REINSTALL/RESET: old shop files, products, orders and users WILL BE ERASED after backup. This is NOT an in-place upgrade.'
  else
    mode_warning='FRESH INSTALL: refuses existing shop files and database.'
  fi
  summary="PrestaShop Classic:  $RELEASE\nDeployment mode:    $INSTALL_MODE\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.\n\n$mode_warning\n\nExisting separate HTTPS vhost and certificate are not removed."
  whiptail --title 'Review configuration (no changes made yet)' --scrolltext --msgbox "$summary" 29 95
}

sql_quote() {
  local val=${1//\\/\\\\}
  val=${val//\'/\'\'}
  printf "'%s'" "$val"
}
# This installer owns only precisely named /var/www/prestashop-<slug>,
# /etc/apache2/sites-available/ps-<slug>.conf, and its named FPM pool.
# Standalone *-ssl.conf is kept because the class shop may use Cloudflare Tunnel.
managed_paths() {
  SITE_PARENT="/var/www/prestashop-$SLUG"
  SITE_ROOT="$SITE_PARENT/public"
  HTTP_CONF="/etc/apache2/sites-available/ps-$SLUG.conf"
  SSL_CONF="/etc/apache2/sites-available/ps-$SLUG-ssl.conf"
  LE_SSL_CONF="/etc/apache2/sites-available/ps-$SLUG-le-ssl.conf"
  POOL_CONF="/etc/php/$PHPV/fpm/pool.d/ps-$SLUG.conf"
  FPM_SOCKET="/run/php/php${PHPV}-fpm-ps-$SLUG.sock"
}

check_existing() {
  managed_paths
  local path
  for path in "$SITE_PARENT" "$SITE_ROOT" "$HTTP_CONF" "$SSL_CONF" "$LE_SSL_CONF" "$POOL_CONF"; do
    [[ ! -L "$path" ]] || fail "Symlinked managed path refused: $path"
  done
  [[ ! -e "$SITE_PARENT" || -d "$SITE_PARENT" ]] || fail "Site directory path is not a directory: $SITE_PARENT"
  [[ ! -e "$HTTP_CONF" || -f "$HTTP_CONF" ]] || fail "Unexpected Apache config type: $HTTP_CONF"
  [[ ! -e "$SSL_CONF" || -f "$SSL_CONF" ]] || fail "Unexpected SSL config type: $SSL_CONF"
  [[ ! -e "$LE_SSL_CONF" || -f "$LE_SSL_CONF" ]] || fail "Unexpected Certbot SSL config type: $LE_SSL_CONF"
  [[ ! -e "$POOL_CONF" || -f "$POOL_CONF" ]] || fail "Unexpected PHP-FPM pool config type: $POOL_CONF"
  if systemctl is-active --quiet nginx; then fail 'Nginx is active. This installer manages Apache only.'; 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 in use by another service.'; fi
  fi
  local avail required_kib=1048576 existing_kib=0
  avail=$(df -Pk /var | awk 'NR==2 {print $4}')
  if [[ -d "$SITE_PARENT" ]]; then
    existing_kib=$(du -sk -- "$SITE_PARENT" | awk '{print $1}')
    if [[ "$INSTALL_MODE" == replace && "$existing_kib" -gt 0 ]]; then
      required_kib=$((existing_kib + 524288))
      ((required_kib < 1048576)) && required_kib=1048576
    fi
  fi
  [[ "$avail" =~ ^[0-9]+$ && "$avail" -ge "$required_kib" ]] || \
    fail "Not enough space under /var for download and backup (need at least ${required_kib} KiB free)."

  if [[ "$INSTALL_MODE" == fresh ]]; then
    [[ ! -e "$HTTP_CONF" ]] || fail "Existing Apache vhost: $HTTP_CONF. Choose Replace mode to reinstall."
    [[ ! -e "$SSL_CONF" && ! -e "$LE_SSL_CONF" ]] || fail "Existing HTTPS vhost. Choose Replace mode to reuse it."
    [[ ! -e "$POOL_CONF" ]] || fail "Existing PHP-FPM pool: $POOL_CONF. Choose Replace mode."
    if [[ -d "$SITE_PARENT" && -n "$(find "$SITE_PARENT" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
      fail "Site directory $SITE_PARENT is not empty. Choose Replace mode."
    fi
  else
    # Validate legacy v1.0.x resources before permitting a destructive reset.
    # An identical name alone must not authorize wiping an unrelated website.
    if [[ -f "$HTTP_CONF" ]]; then
      grep -Fq "DocumentRoot $SITE_ROOT" "$HTTP_CONF" || fail "Existing HTTP vhost is not owned by this shop: $HTTP_CONF"
      grep -Fq "$FPM_SOCKET" "$HTTP_CONF" || fail "Existing HTTP vhost uses an unexpected PHP-FPM socket."
    fi
    local existing_tls_conf
    for existing_tls_conf in "$SSL_CONF" "$LE_SSL_CONF"; do
      if [[ -f "$existing_tls_conf" ]]; then
        grep -Fq "DocumentRoot $SITE_ROOT" "$existing_tls_conf" || fail "Existing SSL vhost uses an unexpected site directory: $existing_tls_conf"
        grep -Fq "$FPM_SOCKET" "$existing_tls_conf" || fail "Existing SSL vhost uses an unexpected PHP-FPM socket: $existing_tls_conf"
      fi
    done
    if [[ -f "$POOL_CONF" ]]; then
      grep -Fq "[ps-$SLUG]" "$POOL_CONF" || fail "Existing PHP-FPM pool has unexpected name."
      grep -Fq "listen = $FPM_SOCKET" "$POOL_CONF" || fail "Existing PHP-FPM pool has unexpected socket."
    fi
    if [[ -d "$SITE_PARENT" && -n "$(find "$SITE_PARENT" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then
      if [[ -f "$SITE_PARENT/.prestashop-tui-managed" ]]; then
        grep -Fxq "$SLUG" "$SITE_PARENT/.prestashop-tui-managed" || fail 'Managed marker does not match this shop slug.'
      elif [[ -f "$HTTP_CONF" || -f "$SSL_CONF" || -f "$LE_SSL_CONF" || -f "$POOL_CONF" ]]; then
        : # Recover a partial v1.0.2 install with exactly matching generated config.
      elif [[ -f "$SITE_ROOT/index.php" && -d "$SITE_ROOT/classes" && -f "$SITE_ROOT/config/defines.inc.php" ]]; then
        : # Recover a legacy install whose Apache/PHP config was already deleted.
      else
        fail "Unrecognized existing files in $SITE_PARENT; refusing to erase unrelated content."
      fi
    fi
  fi
  if [[ "$TLS" == letsencrypt && ( -f "$SSL_CONF" || -f "$LE_SSL_CONF" ) ]]; then
    fail 'Existing standalone SSL site detected. Choose TLS=existing to preserve it (or remove it manually before using Certbot).'
  fi
  if [[ "$TLS" == existing ]]; then
    [[ -f "$SSL_CONF" || -f "$LE_SSL_CONF" ]] || fail "TLS=existing requires an existing SSL Apache vhost."
    [[ -L "/etc/apache2/sites-enabled/ps-$SLUG-ssl.conf" || -L "/etc/apache2/sites-enabled/ps-$SLUG-le-ssl.conf" ]] || \
      fail "TLS=existing requires an ENABLED SSL Apache vhost for this shop."
  fi
}

local_db_exists() {
  local query="SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='${DB_NAME}';" count
  count=$(mariadb --protocol=socket -N -B -e "$query" 2>>"$LOG") || fail 'Could not query MariaDB databases.'
  [[ "$count" == 0 || "$count" == 1 ]] || fail 'Unexpected database existence query result.'
  [[ "$count" == 1 ]]
}

local_db_user_exists() {
  local query="SELECT COUNT(*) FROM mysql.user WHERE User='${DB_USER}' AND Host='localhost';" count
  count=$(mariadb --protocol=socket -N -B -e "$query" 2>>"$LOG") || fail 'Could not query MariaDB users.'
  [[ "$count" == 0 || "$count" == 1 ]] || fail 'Unexpected database account query result.'
  [[ "$count" == 1 ]]
}

check_local_user_scope() {
  local grants grant
  local sql_user
  sql_user=$(sql_quote "$DB_USER")
  grants=$(mariadb --protocol=socket -N -B -e "SHOW GRANTS FOR ${sql_user}@'localhost';" 2>>"$LOG") || \
    fail "Could not inspect permissions of existing database user $DB_USER."
  while IFS= read -r grant; do
    case "$grant" in
      "GRANT USAGE ON *.* TO "*|"GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO "*) ;;
      *) fail "Existing DB user $DB_USER has extra privileges. Refusing to alter its password; choose a different user." ;;
    esac
  done <<< "$grants"
}

# Create a complete backup BEFORE touching any existing DB, web files or config.
# Archive and DB dump are root-readable only. A failed backup aborts the reinstall.
backup_existing() {
  [[ "$INSTALL_MODE" == replace ]] || return 0
  notice 'Backing up the existing shop before its reset...'
  managed_paths
  local dir="$BACKUP_BASE/$SLUG" stamp
  stamp="$(date -u +%Y%m%dT%H%M%SZ)-$$"
  [[ ! -L "$BACKUP_BASE" && ! -L "$dir" ]] || fail 'Refusing symlinked backup directory.'
  install -d -m 0700 "$BACKUP_BASE" "$dir"
  LAST_BACKUP="$dir/$stamp"
  install -d -m 0700 "$LAST_BACKUP"
  printf 'Shop: %s\nDatabase: %s\nCreated UTC: %s\nMode: full reset/reinstall\n' \
    "$SLUG" "$DB_NAME" "$(date -u +%FT%TZ)" > "$LAST_BACKUP/README.txt"
  if [[ -d "$SITE_PARENT" ]]; then
    tar -C /var/www -czf "$LAST_BACKUP/webroot.tar.gz" "prestashop-$SLUG" >>"$LOG" 2>&1 \
      || fail 'Webroot backup failed; existing shop was not modified.'
    tar -tzf "$LAST_BACKUP/webroot.tar.gz" >/dev/null 2>>"$LOG" \
      || fail 'Webroot backup verification failed; nothing was erased.'
  fi
  local file name
  for file in "$HTTP_CONF" "$SSL_CONF" "$LE_SSL_CONF" "$POOL_CONF" "$CONFIG_FILE"; do
    case "$file" in
      "$HTTP_CONF") name='apache-http.conf' ;;
      "$SSL_CONF") name='apache-ssl.conf' ;;
      "$LE_SSL_CONF") name='apache-certbot-ssl.conf' ;;
      "$POOL_CONF") name='php-fpm-pool.conf' ;;
      "$CONFIG_FILE") name='installer-config.json' ;;
    esac
    if [[ -f "$file" ]]; then
      cp -- "$file" "$LAST_BACKUP/$name" \
        || fail "Failed to back up $file; nothing was erased."
      chmod 600 "$LAST_BACKUP/$name"
    fi
  done
  if local_db_exists; then
    # Use socket authentication; never send root password or SQL commands to logs.
    mariadb-dump --protocol=socket --single-transaction --routines --events --triggers \
      --databases "$DB_NAME" --result-file="$LAST_BACKUP/database.sql" >>"$LOG" 2>&1 \
      || fail 'Database dump failed; existing shop was not modified.'
    [[ -s "$LAST_BACKUP/database.sql" ]] || fail 'Database dump is empty; refusing to erase.'
    gzip "$LAST_BACKUP/database.sql" || fail 'Database compression failed; nothing was erased.'
    gzip -t "$LAST_BACKUP/database.sql.gz" || fail 'Database archive is corrupt; nothing was erased.'
  fi
  chmod -R go-rwx "$LAST_BACKUP"
  BACKUP_COMPLETE=1
  printf 'Complete reinstall backup: %s\n' "$LAST_BACKUP" >> "$LOG"
}

reset_existing() {
  [[ "$INSTALL_MODE" == replace ]] || return 0
  [[ "$BACKUP_COMPLETE" == 1 && -n "$LAST_BACKUP" && -f "$LAST_BACKUP/README.txt" ]] || fail 'No completed reinstall backup; refusing to erase.'
  notice "Removing the old shop after backup ($LAST_BACKUP)..."
  managed_paths
  if local_db_exists; then
    run mariadb --protocol=socket -e "DROP DATABASE \`$DB_NAME\`;" || fail 'Could not drop the selected local database.'
  fi
  if [[ -d "$SITE_PARENT" ]]; then
    rm -rf -- "$SITE_PARENT" || fail 'Could not remove old webroot.'
  fi
  # Leave the standalone SSL vhost/cert intact: Cloudflare Tunnel may depend on it.
  rm -f -- "$HTTP_CONF" "$POOL_CONF"
  printf 'Old shop reset; standalone HTTPS vhost retained if present.\n' >>"$LOG"
}

# Inspect an existing local DB account BEFORE any destructive action.
# In a fresh installation, fail early when a previous attempt left a database.
preflight_database_scope() {
  [[ "$DB_MODE" == local ]] || return 0
  if [[ "$INSTALL_MODE" == replace ]]; then
    if local_db_user_exists; then check_local_user_scope; fi
  else
    if local_db_exists; then fail "Local database $DB_NAME already exists. Choose Replace mode."; fi
    if local_db_user_exists; then fail "Local DB user $DB_USER already exists. Choose Replace mode if it belongs to the old installation."; fi
  fi
}

confirm_replace() {
  local answer required="REINSTALL $SLUG $DB_NAME"
  answer=$(whiptail --title 'DESTRUCTIVE REINSTALL — typed confirmation' \
    --inputbox "This will permanently ERASE and RECREATE the selected local database ($DB_NAME), the shop files at /var/www/prestashop-$SLUG, and their generated HTTP/PHP configs.\n\nA full backup is made first in $BACKUP_BASE. Existing SSL vhosts/certs will be preserved.\n\nThis is NOT an update that preserves customers/orders/products.\n\nType exactly: $required" \
    19 98 '' 3>&1 1>&2 2>&3) || return 1
  [[ "$answer" == "$required" ]] || { ui_info 'Confirmation did not match. Nothing was erased.'; return 1; }
  return 0
}

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 local_db_exists; then
      fail "Database $DB_NAME already exists. Choose Replace mode to back it up, drop it and reinstall."
    fi
    local sql_pass sql_user
    sql_pass=$(sql_quote "$DB_PASS")
    sql_user=$(sql_quote "$DB_USER")
    if local_db_user_exists; then
      if [[ "$INSTALL_MODE" != replace ]]; then
        fail "Local DB account $DB_USER already exists. Choose Replace mode if it belongs to the previous shop."
      fi
      check_local_user_scope
      # The old managed account may contain a password from a previous install.
      # Rotate it to the user's saved TUI password, but ONLY after scope validation.
      mariadb --protocol=socket <<SQL >>"$LOG" 2>&1
ALTER USER $sql_user@'localhost' IDENTIFIED BY $sql_pass;
CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO $sql_user@'localhost';
SQL
    else
      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
    fi
  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"
  printf '%s\n' "$SLUG" > "/var/www/prestashop-$SLUG/.prestashop-tui-managed"
  chmod 600 "/var/www/prestashop-$SLUG/.prestashop-tui-managed"
  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 || "$TLS" == existing ]] && 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 || "$TLS" == existing ]]; 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 || "$TLS" == existing ]] && 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\n${LAST_BACKUP:+PREVIOUS SHOP BACKUP: $LAST_BACKUP\n\n}SHOP: $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"
  [[ -z "$LAST_BACKUP" ]] || printf 'Previous shop backup (root-only): %s\n' "$LAST_BACKUP"
}

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 autosave to $CONFIG_FILE (includes passwords).\nInstall mode: $INSTALL_MODE. Replace backs up and ERASES the shop, DB, and HTTP/PHP config; separate HTTPS is kept." \
      24 98 15 \
      1 'Version, REINSTALL mode, hostname, 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 [[ "$INSTALL_MODE" == replace ]]; then
          confirm_replace || continue
        else
          ui_yesno 'Install PrestaShop without overwriting any existing shop files or database?\n\nProceed?' || continue
        fi
        # Everything that can be prepared is done before the destructive step.
        check_existing
        resolve_release
        install_packages
        check_existing
        preflight_database_scope
        download_release
        if [[ "$INSTALL_MODE" == replace ]]; then
          backup_existing
          reset_existing
        fi
        create_database
        configure_services
        configure_https
        install_prestashop
        check_http
        finish
        return 0
        ;;
      8) exit 0 ;;
    esac
  done
}
main
