#!/usr/bin/env bash
#
# ONOXSOFT Panel Master Installer (Phase 1)
#
# Desteklenen OS aileleri:
#   RHEL family : AlmaLinux 9, Rocky Linux 9, RHEL 9, CentOS Stream 9, Oracle Linux 9
#   Debian      : Debian 11 (Bullseye), Debian 12 (Bookworm)
#   Ubuntu      : 22.04 LTS (Jammy), 24.04 LTS (Noble)
#
# Required: fresh server, root access, public IP (Let's Encrypt için)
#
# Usage:
#   curl -fsSL https://onox.com.tr/install.sh | bash
#   curl -fsSL https://onox.com.tr/install.sh | bash -s -- --hostname panel.example.com --admin-email admin@example.com
#   bash install.sh --hostname panel.example.com --admin-email admin@example.com --debug
#
# Exit codes:
#   0 = success
#   1 = argument / preflight error
#   2 = package install error
#   3 = configuration error
#   5 = critical / unexpected error

set -euo pipefail

# ---------------------------------------------------------------------------
# Version & global paths
# ---------------------------------------------------------------------------
readonly SCRIPT_VERSION="0.2.0"
readonly ONOX_HOME=/opt/onoxsoft
readonly ONOX_REPO=https://github.com/onoxsoft/panel.git
# Release tarball — get.onox.com.tr üzerinden host edilir, tek-komut kurulum için.
# Versiyonlu: paneltr-v0.18.0.tar.gz; "latest" alias: paneltr.tar.gz
readonly ONOX_RELEASE_URL="${ONOX_RELEASE_URL:-https://get.onox.com.tr/paneltr.tar.gz}"
readonly ONOX_RELEASE_SHA256_URL="${ONOX_RELEASE_SHA256_URL:-https://get.onox.com.tr/paneltr.tar.gz.sha256}"
readonly ONOX_BIN=/usr/local/onoxsoft/bin
readonly ONOX_ETC=/etc/onoxsoft
readonly ONOX_ACME=/usr/local/onoxsoft/acme.sh
readonly LOG=/var/log/onox-install.log

# Args
PANEL_HOSTNAME="${ONOX_HOSTNAME:-}"
ADMIN_EMAIL="${ONOX_EMAIL:-}"
ADMIN_TEMP_PASS=""                              # step_create_admin_user üretir, step_print_summary gösterir
SKIP_PREFLIGHT=0
DRY_RUN=0
DEBUG=0

# Stack seçenekleri
WEB_SERVER="${ONOX_WEB_SERVER:-apache}"      # apache | nginx | openlitespeed | litespeed
ENABLE_ANTIVIRUS="${ONOX_ENABLE_ANTIVIRUS:-1}"
ENABLE_MAIL_SERVER="${ONOX_ENABLE_MAIL:-1}"      # Postfix + Dovecot kur (1) veya atla (0)
ENABLE_SPAM_FILTER="${ONOX_ENABLE_SPAM:-1}"      # Rspamd kur + Postfix milter bağla
ENABLE_WEBMAIL="${ONOX_ENABLE_WEBMAIL:-1}"       # Webmail kur (Roundcube veya SnappyMail)
WEBMAIL_DRIVER="${ONOX_WEBMAIL:-roundcube}"      # roundcube | snappymail | none

# OS abstraction — set by step_detect_os()
OS_FAMILY=""           # rhel | debian
DISTRO=""              # almalinux, rocky, centos, rhel, oracle, debian, ubuntu
DISTRO_VER=""          # 9, 11, 12, 22.04, 24.04
PKG_MGR=""             # dnf | apt-get
HAS_SELINUX=0          # 1 on RHEL family, 0 on Debian (uses AppArmor — handled separately)

# Web server
WEB_PKG=""             # httpd | apache2
WEB_SVC=""             # httpd | apache2
WEB_USER=""            # apache | www-data
WEB_GROUP=""           # apache | www-data
WEB_CONF_DIR=""        # /etc/httpd/conf.d | /etc/apache2/sites-available
WEB_LOG_DIR=""         # /var/log/httpd | /var/log/apache2

# ── ONOXSOFT Panel dedicated ports (cPanel/WHM pattern: WHM=2087, cPanel=2083)
# Panel UI (admin + customer + reseller) yalnız bu portlardan açılır.
# Müşteri site'leri 80/443 üzerinde kalır.
PANEL_HTTP_PORT="${PANEL_HTTP_PORT:-665}"     # HTTP → HTTPS redirect
PANEL_SSL_PORT="${PANEL_SSL_PORT:-666}"       # ONOXSOFT panel SSL (varsayılan)
INSTALL_MODE=""                                # "ip-only" | "" (fqdn). IP modunda LE atlanır, self-signed kullanılır.

# PHP-FPM (target Remi 82 / sury 8.2)
PHP_VERSIONS=()        # available versions array — set by detect
PHP_DEFAULT_VER=""     # "82" (Remi style) | "8.2" (Debian style)
PHP_FPM_SVC=""         # php82-php-fpm | php8.2-fpm
PHP_FPM_POOL_DIR=""
PHP_FPM_SOCK_DIR=""
PHP_CLI_PATH=""

# MariaDB
MARIADB_PKG=""
MARIADB_SVC=""

# Redis
REDIS_PKG=""
REDIS_SVC=""

# PowerDNS
PDNS_PKG=""
PDNS_BACKEND_PKG=""
PDNS_CONF_DIR=""

# Firewall
FIREWALL_TOOL=""       # firewalld | ufw

# ---------------------------------------------------------------------------
# Color + Unicode UI helpers (disabled when piped / no TTY)
# ---------------------------------------------------------------------------
if [[ -t 1 ]]; then
  # ANSI-C quoting ($'...') ile gerçek escape character (0x1B) saklanır.
  # Böylece hem `echo -e` hem `cat <<EOF` doğru render eder.
  RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'
  BLUE=$'\033[0;34m'; MAGENTA=$'\033[0;35m'; CYAN=$'\033[0;36m'
  WHITE=$'\033[1;37m'; GREY=$'\033[0;90m'
  BG_BLUE=$'\033[44m'; BG_CYAN=$'\033[46m'
  BOLD=$'\033[1m'; DIM=$'\033[2m'; NC=$'\033[0m'
else
  RED=''; GREEN=''; YELLOW=''; BLUE=''; MAGENTA=''; CYAN=''
  WHITE=''; GREY=''; BG_BLUE=''; BG_CYAN=''; BOLD=''; DIM=''; NC=''
fi

# Box drawing characters (Unicode)
readonly UI_TL='╭' UI_TR='╮' UI_BL='╰' UI_BR='╯' UI_H='─' UI_V='│'
readonly UI_DOT='◉' UI_OK='✓' UI_FAIL='✗' UI_ARROW='▶' UI_STAR='★'

info()    { echo -e "${BLUE}${UI_ARROW}${NC} $*" | tee -a "$LOG"; }
warn()    { echo -e "${YELLOW}${BOLD}⚠${NC}  $*" | tee -a "$LOG"; }
fatal()   { echo -e "${RED}${BOLD}${UI_FAIL}${NC} $*" | tee -a "$LOG" >&2; }
success() { echo -e "${GREEN}${BOLD}${UI_OK}${NC} $*" | tee -a "$LOG"; }

# Box-drawing step header — "01/17 Preflight" stilinde
step() {
  local raw="$*"
  local num="${raw%%  *}"     # "1/17" kısmı
  local title="${raw#*  }"     # başlık kısmı
  local pad
  pad=$(printf '%*s' $((52 - ${#title})) '')
  echo "" | tee -a "$LOG"
  echo -e "${CYAN}${UI_TL}${UI_H}${UI_H}${UI_H} ${BOLD}${UI_DOT} ${num}${NC}${CYAN} ${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_TR}${NC}" | tee -a "$LOG"
  echo -e "${CYAN}${UI_V}${NC}  ${WHITE}${BOLD}${title}${NC}${pad}${CYAN}${UI_V}${NC}" | tee -a "$LOG"
  echo -e "${CYAN}${UI_BL}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_BR}${NC}" | tee -a "$LOG"
}

# ASCII Banner — kurulum başında bir kez
show_banner() {
  [[ ! -t 1 ]] && return 0
  clear 2>/dev/null || true
  cat <<BANNER

${CYAN}${BOLD}     ██████╗ ███╗   ██╗ ██████╗ ██╗  ██╗${MAGENTA}███████╗ ██████╗ ███████╗████████╗${NC}
${CYAN}${BOLD}    ██╔═══██╗████╗  ██║██╔═══██╗╚██╗██╔╝${MAGENTA}██╔════╝██╔═══██╗██╔════╝╚══██╔══╝${NC}
${CYAN}${BOLD}    ██║   ██║██╔██╗ ██║██║   ██║ ╚███╔╝ ${MAGENTA}███████╗██║   ██║█████╗     ██║   ${NC}
${CYAN}${BOLD}    ██║   ██║██║╚██╗██║██║   ██║ ██╔██╗ ${MAGENTA}╚════██║██║   ██║██╔══╝     ██║   ${NC}
${CYAN}${BOLD}    ╚██████╔╝██║ ╚████║╚██████╔╝██╔╝ ██╗${MAGENTA}███████║╚██████╔╝██║        ██║   ${NC}
${CYAN}${BOLD}     ╚═════╝ ╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝${MAGENTA}╚══════╝ ╚═════╝ ╚═╝        ╚═╝   ${NC}

${WHITE}${BOLD}              Türkiye'nin yerli hosting kontrol paneli${NC}
${GREY}                       Installer v${SCRIPT_VERSION}${NC}

${GREY}    ╭──────────────────────────────────────────────────────────╮${NC}
${GREY}    ${UI_V}${NC}  ${UI_STAR} 17 adım  ${UI_STAR} ~10 dk  ${UI_STAR} cPanel/WHM/Plesk alternatifi  ${GREY}${UI_V}${NC}
${GREY}    ╰──────────────────────────────────────────────────────────╯${NC}

BANNER
  sleep 1
}

# Whiptail wrapper — yoksa pkg install + fallback
HAS_WHIPTAIL=0
ui_init() {
  if command -v whiptail &>/dev/null; then
    HAS_WHIPTAIL=1
    return 0
  fi
  # TTY varsa ve internet erişimi varsa whiptail kurmaya dene (sessiz)
  if [[ -t 0 ]]; then
    if command -v dnf &>/dev/null; then
      dnf install -y newt &>/dev/null && HAS_WHIPTAIL=1
    elif command -v apt-get &>/dev/null; then
      DEBIAN_FRONTEND=noninteractive apt-get install -y whiptail &>/dev/null && HAS_WHIPTAIL=1
    fi
  fi
}

# ui_input "Soru" "default" "BAŞLIK" -> stdout: cevap
ui_input() {
  local question="$1" default="${2:-}" title="${3:-ONOXSOFT Installer}"
  if [[ $HAS_WHIPTAIL -eq 1 && -t 0 ]]; then
    whiptail --title "$title" --inputbox "$question" 10 70 "$default" 3>&1 1>&2 2>&3
  else
    local answer=""
    if [[ -n "$default" ]]; then
      read -rp "$question [$default]: " answer
      echo "${answer:-$default}"
    else
      read -rp "$question: " answer
      echo "$answer"
    fi
  fi
}

# ui_yesno "Soru" "BAŞLIK" -> exit 0 (yes) / 1 (no)
ui_yesno() {
  local question="$1" title="${2:-ONOXSOFT Installer}"
  if [[ $HAS_WHIPTAIL -eq 1 && -t 0 ]]; then
    whiptail --title "$title" --yesno "$question" 10 70
  else
    local answer=""
    read -rp "$question [E/h]: " answer
    [[ "$answer" =~ ^[Hh] ]] && return 1
    return 0
  fi
}

# ui_msg "Mesaj" "BAŞLIK"
ui_msg() {
  local msg="$1" title="${2:-ONOXSOFT Installer}"
  if [[ $HAS_WHIPTAIL -eq 1 && -t 0 ]]; then
    whiptail --title "$title" --msgbox "$msg" 12 76
  else
    echo -e "\n${CYAN}${BOLD}══ ${title} ══${NC}\n${msg}\n"
  fi
}

# ui_menu "Soru" "BAŞLIK" "tag1" "label1" "tag2" "label2" ...
ui_menu() {
  local question="$1" title="$2"; shift 2
  if [[ $HAS_WHIPTAIL -eq 1 && -t 0 ]]; then
    whiptail --title "$title" --menu "$question" 15 70 5 "$@" 3>&1 1>&2 2>&3
  else
    # Plain text fallback
    echo "$question"
    local i=1
    local -a tags=()
    while [[ $# -gt 0 ]]; do
      tags+=("$1")
      echo "  $i) $1 - $2"
      shift 2
      ((i++))
    done
    local choice
    read -rp "Seçim [1]: " choice
    choice="${choice:-1}"
    echo "${tags[$((choice-1))]}"
  fi
}

die() {
  fatal "$1"
  echo "Tam log: $LOG" >&2
  exit "${2:-1}"
}

# Idempotent + dry-run-safe komut sarmalayıcı
run() {
  [[ "$DEBUG" -eq 1 ]] && echo -e "${YELLOW}[CMD]${NC} $*"
  if [[ "$DRY_RUN" -eq 1 ]]; then
    echo "[DRY-RUN] $*" >> "$LOG"
    return 0
  fi
  echo "[CMD] $*" >> "$LOG"
  "$@" >> "$LOG" 2>&1 || die "Komut basarisiz: $*"
}

# Hatada die ETMEYEN run varyantı — || true / || warn ile kullanılır.
# (run XXX || true kalıbı tuzaklı: run zaten die ediyorsa || çalışmıyor.)
try_run() {
  [[ "$DEBUG" -eq 1 ]] && echo -e "${YELLOW}[CMD?]${NC} $*"
  if [[ "$DRY_RUN" -eq 1 ]]; then
    echo "[DRY-RUN] $*" >> "$LOG"
    return 0
  fi
  echo "[CMD?] $*" >> "$LOG"
  "$@" >> "$LOG" 2>&1
}

# ---------------------------------------------------------------------------
# Usage / arg parse
# ---------------------------------------------------------------------------
usage() {
  cat <<EOF
ONOXSOFT Panel Installer v${SCRIPT_VERSION}

Desteklenen OS:
  RHEL family : AlmaLinux 9 / Rocky 9 / RHEL 9 / CentOS Stream 9 / Oracle Linux 9
  Debian      : 11 (Bullseye), 12 (Bookworm)
  Ubuntu      : 22.04 LTS, 24.04 LTS

Kullanim:
  sudo bash install.sh [SECENEKLER]

Secenekler:
  --hostname  FQDN          Panel hostname (panel.example.com)
  --admin-email EMAIL       Admin e-posta adresi
  --web-server NAME         Web server: apache (default), nginx, openlitespeed, litespeed
  --http-port N             Panel HTTP portu (default: 665, redirect → SSL)
  --ssl-port  N             Panel SSL  portu (default: 666 — cPanel 2087 pattern)
  --ip-only                 IP-only kurulum (FQDN yok, self-signed SSL, hostname sonra)
  --no-antivirus            ClamAV kurulumunu atla (varsayilan kurar)
  --no-mail-server          Postfix + Dovecot kurulumunu atla
  --no-spam-filter          Rspamd kurulumunu atla (varsayilan kurar)
  --no-webmail              Webmail kurulumunu atla
  --webmail NAME            Webmail driver: roundcube (default), snappymail, none
  --skip-preflight          Preflight kontrollerini atla (sadece test icin)
  --dry-run                 Komutlari yazdir, calistirma
  --debug                   Verbose + xtrace
  -h, --help                Bu yardim

NOT: Panel portu — 80/443 musteri site'lerine ait kalir, panel UI yalniz
     PANEL_SSL_PORT (default 666) uzerinden eriilir. Erisim: https://HOSTNAME:666

Ortam degiskenleri (--arg yerine kullanilabilir):
  ONOX_HOSTNAME
  ONOX_EMAIL
EOF
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --hostname)         PANEL_HOSTNAME="$2"; shift 2 ;;
      --admin-email)      ADMIN_EMAIL="$2";    shift 2 ;;
      --web-server)       WEB_SERVER="$2";     shift 2 ;;
      --http-port)        PANEL_HTTP_PORT="$2"; shift 2 ;;
      --ssl-port)         PANEL_SSL_PORT="$2";  shift 2 ;;
      --ip-only)          INSTALL_MODE="ip-only"; shift ;;
      --no-antivirus)     ENABLE_ANTIVIRUS=0;  shift ;;
      --no-mail-server)   ENABLE_MAIL_SERVER=0; shift ;;
      --no-spam-filter)   ENABLE_SPAM_FILTER=0; shift ;;
      --no-webmail)       ENABLE_WEBMAIL=0;    shift ;;
      --webmail)          WEBMAIL_DRIVER="$2"; shift 2 ;;
      --skip-preflight)   SKIP_PREFLIGHT=1;    shift ;;
      --dry-run)          DRY_RUN=1;           shift ;;
      --debug)            DEBUG=1; set -x;     shift ;;
      -h|--help)          usage; exit 0 ;;
      *) die "Bilinmeyen arguman: $1  (--help ile yardim gorun)" ;;
    esac
  done

  # Web server validation
  case "$WEB_SERVER" in
    apache|nginx|openlitespeed|litespeed) ;;
    *) die "Gecersiz --web-server: $WEB_SERVER (apache|nginx|openlitespeed|litespeed)" ;;
  esac
}

# ---------------------------------------------------------------------------
# OS Detection — /etc/os-release tabanlı
# Çıktı: OS_FAMILY, DISTRO, DISTRO_VER, ve OS-bağımlı tüm değişkenler
# ---------------------------------------------------------------------------
detect_os() {
  [[ -f /etc/os-release ]] || die "/etc/os-release bulunamadi — destek olmayan OS"

  # shellcheck source=/dev/null
  source /etc/os-release

  DISTRO="${ID:-unknown}"
  DISTRO_VER="${VERSION_ID:-unknown}"

  # Major version (9.4 → 9, 22.04 → 22.04 olduğu gibi)
  local major
  major=$(echo "$DISTRO_VER" | cut -d. -f1)

  case "$DISTRO" in
    almalinux|rocky|centos|rhel|ol|oracle)
      # ol = Oracle Linux
      [[ "$major" == "9" ]] || die "RHEL ailesi yalnızca v9 destekleniyor — tespit: ${DISTRO} ${DISTRO_VER}"
      OS_FAMILY="rhel"
      PKG_MGR="dnf"
      HAS_SELINUX=1

      WEB_PKG="httpd"
      WEB_SVC="httpd"
      WEB_USER="apache"
      WEB_GROUP="apache"
      WEB_CONF_DIR="/etc/httpd/conf.d"
      WEB_LOG_DIR="/var/log/httpd"

      PHP_DEFAULT_VER="82"
      PHP_VERSIONS=("74" "81" "82" "83")
      PHP_FPM_SVC="php82-php-fpm"
      PHP_FPM_POOL_DIR="/etc/opt/remi/php82/php-fpm.d"
      PHP_FPM_SOCK_DIR="/var/opt/remi/php82/run/php-fpm"
      PHP_CLI_PATH="/usr/bin/php82"

      MARIADB_PKG="mariadb-server"
      MARIADB_SVC="mariadb"

      REDIS_PKG="redis"
      REDIS_SVC="redis"

      PDNS_PKG="pdns"
      PDNS_BACKEND_PKG="pdns-backend-mysql"
      PDNS_CONF_DIR="/etc/pdns"

      FIREWALL_TOOL="firewalld"
      ;;

    debian)
      [[ "$major" =~ ^(11|12)$ ]] || die "Debian yalnızca v11/12 destekleniyor — tespit: ${DISTRO_VER}"
      _set_debian_family_vars
      ;;

    ubuntu)
      [[ "$DISTRO_VER" =~ ^(22\.04|24\.04)$ ]] || die "Ubuntu yalnızca 22.04/24.04 LTS destekleniyor — tespit: ${DISTRO_VER}"
      _set_debian_family_vars
      ;;

    *)
      die "Desteklenmeyen OS: ${DISTRO} ${DISTRO_VER}. Desteklenenler: AlmaLinux/Rocky/RHEL/CentOS Stream/Oracle 9, Debian 11/12, Ubuntu 22.04/24.04"
      ;;
  esac

  info "OS algılandı: ${DISTRO} ${DISTRO_VER} (family: ${OS_FAMILY})"
}

# Debian/Ubuntu için ortak değişkenler — apt + www-data + sury PPA
_set_debian_family_vars() {
  OS_FAMILY="debian"
  PKG_MGR="apt-get"
  HAS_SELINUX=0

  WEB_PKG="apache2"
  WEB_SVC="apache2"
  WEB_USER="www-data"
  WEB_GROUP="www-data"
  WEB_CONF_DIR="/etc/apache2/sites-available"
  WEB_LOG_DIR="/var/log/apache2"

  PHP_DEFAULT_VER="8.2"
  PHP_VERSIONS=("7.4" "8.1" "8.2" "8.3")
  PHP_FPM_SVC="php8.2-fpm"
  PHP_FPM_POOL_DIR="/etc/php/8.2/fpm/pool.d"
  PHP_FPM_SOCK_DIR="/run/php"
  PHP_CLI_PATH="/usr/bin/php8.2"

  MARIADB_PKG="mariadb-server"
  MARIADB_SVC="mariadb"

  REDIS_PKG="redis-server"
  REDIS_SVC="redis-server"

  PDNS_PKG="pdns-server"
  PDNS_BACKEND_PKG="pdns-backend-mysql"
  PDNS_CONF_DIR="/etc/powerdns"

  FIREWALL_TOOL="ufw"
}

# Paket kurulum sarmalayıcı — OS ailesine göre dnf veya apt-get çağırır
pkg_install() {
  if [[ "$OS_FAMILY" == "rhel" ]]; then
    run dnf install -y "$@"
  else
    DEBIAN_FRONTEND=noninteractive run apt-get install -y --no-install-recommends "$@"
  fi
}

pkg_update() {
  if [[ "$OS_FAMILY" == "rhel" ]]; then
    run dnf update -y
  else
    DEBIAN_FRONTEND=noninteractive run apt-get update
  fi
}

# ---------------------------------------------------------------------------
# STEP 1 — Preflight: root, resources, network, OS
# ---------------------------------------------------------------------------
step_preflight() {
  step "1/17  Preflight"

  [[ "$(id -u)" -eq 0 ]] || die "Bu script root olarak çalıştırılmalıdır (sudo bash install.sh)."

  detect_os

  # SELinux state (yalnızca RHEL'de)
  if [[ "$HAS_SELINUX" -eq 1 ]]; then
    local sel_mode
    sel_mode=$(getenforce 2>/dev/null || echo "Disabled")
    if [[ "$sel_mode" == "Disabled" ]]; then
      warn "SELinux Disabled. Production'da Enforcing önerilir."
    else
      info "SELinux: ${sel_mode}"
    fi
  else
    info "AppArmor profili kullanılıyor (Debian/Ubuntu) — gerekli istisnalar adım 6'da eklenir"
  fi

  # RAM ≥ 1.5 GB
  local ram_mb
  ram_mb=$(( $(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024 ))
  [[ "$ram_mb" -ge 1500 ]] || die "Minimum 1.5 GB RAM gerekli; mevcut: ${ram_mb} MB"
  info "RAM: ${ram_mb} MB — OK"

  # Disk ≥ 10 GB free on /
  local disk_gb
  disk_gb=$(( $(df / --output=avail | tail -1) / 1024 / 1024 ))
  [[ "$disk_gb" -ge 10 ]] || die "/ üzerinde 10 GB boş disk gerekli; mevcut: ${disk_gb} GB"
  info "Disk: ${disk_gb} GB boş — OK"

  # Public IP (best-effort) — IPv4 force
  local pub_ip
  pub_ip=$(curl -4 -fsS --max-time 8 https://ipv4.icanhazip.com 2>/dev/null || true)
  [[ -n "$pub_ip" ]] && info "Genel IP: ${pub_ip}" || warn "Genel IP tespit edilemedi."

  success "Preflight geçti"
}

# ---------------------------------------------------------------------------
# STEP 2 — Prompt / validate hostname + email
# ---------------------------------------------------------------------------
step_prompt_admin() {
  step "2/17  Admin bilgileri"

  # Public IP'yi her zaman tespit et — IP-only modda hostname için, FQDN modda info için
  local pub_ip=""
  # IPv4 force et — IPv6 dönerse hostname validasyonu patlar (sadece IPv4 + FQDN kabul ediliyor)
  pub_ip=$(curl -4 -fsS --max-time 5 https://ipv4.icanhazip.com 2>/dev/null \
    || curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null \
    || curl -4 -fsS --max-time 5 https://ifconfig.io 2>/dev/null \
    || ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1 \
    || hostname -I 2>/dev/null | awk '{print $1}' \
    || echo "")
  # Son güvenlik: IPv6 yakalanmışsa boşalt (regex'le filtrele)
  [[ "$pub_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || pub_ip=""

  # IP-only mod açıkça --ip-only ile verildiyse: doğrudan public IP'yi hostname yap
  if [[ "$INSTALL_MODE" == "ip-only" ]] && [[ -z "$PANEL_HOSTNAME" ]]; then
    if [[ -n "$pub_ip" ]]; then
      PANEL_HOSTNAME="$pub_ip"
      info "IP-only mod: hostname = ${PANEL_HOSTNAME}"
    else
      die "IP-only mod istendi ama public IP tespit edilemedi"
    fi
  fi

  # Hostname hâlâ boşsa interaktif sor (TTY varsa whiptail kullan)
  if [[ -z "$PANEL_HOSTNAME" ]]; then
    if [[ -t 0 ]]; then
      local default_host="${pub_ip:-panel.example.com}"
      PANEL_HOSTNAME=$(ui_input \
        "Panel hostname (FQDN veya boş = IP modu).\n\nÖrnek: panel.şirketim.com\nIP modu seçersen sonradan Settings'ten değiştirebilirsin." \
        "$default_host" "ONOXSOFT - Hostname")

      # Whiptail iptal edilirse veya boş bırakılırsa IP moduna düş
      if [[ -z "$PANEL_HOSTNAME" || "$PANEL_HOSTNAME" == "$pub_ip" ]]; then
        if [[ -n "$pub_ip" ]]; then
          PANEL_HOSTNAME="$pub_ip"
          INSTALL_MODE="ip-only"
        fi
      fi
    fi

    if [[ -z "$PANEL_HOSTNAME" ]]; then
      die "PANEL_HOSTNAME boş ve public IP tespit edilemedi — --hostname veya ONOX_HOSTNAME ile manuel sağla"
    fi
  fi

  if [[ "$INSTALL_MODE" == "ip-only" ]]; then
    warn "IP-only mod: https://${PANEL_HOSTNAME}:${PANEL_SSL_PORT}"
    warn "Sertifika self-signed; FQDN ayarlandığında otomatik LE alınacak."
  fi

  if [[ -z "$ADMIN_EMAIL" ]]; then
    if [[ -t 0 ]]; then
      ADMIN_EMAIL=$(ui_input \
        "Admin e-posta adresi.\n\nLet's Encrypt sertifikası, güvenlik uyarıları ve panel admin hesabı için kullanılacak." \
        "" "ONOXSOFT - Admin Email")
    else
      die "ADMIN_EMAIL boş — --admin-email bayrağıyla veya ONOX_EMAIL env ile sağlayın"
    fi
  fi

  # Hostname iki tip kabul eder: FQDN veya IPv4. IPv6 placeholder olarak şimdilik dışarda.
  local is_fqdn=0 is_ip=0
  [[ "$PANEL_HOSTNAME" =~ ^[a-z0-9.-]+\.[a-z]{2,}$ ]] && is_fqdn=1
  [[ "$PANEL_HOSTNAME" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && is_ip=1
  if [[ $is_fqdn -eq 0 && $is_ip -eq 0 ]]; then
    die "Geçersiz hostname/IP: ${PANEL_HOSTNAME}"
  fi
  if [[ $is_ip -eq 1 ]]; then
    INSTALL_MODE="ip-only"
  fi

  [[ "$ADMIN_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]] \
    || die "Geçersiz email: ${ADMIN_EMAIL}"

  info "Hostname: ${PANEL_HOSTNAME} ${INSTALL_MODE:+(${INSTALL_MODE})}"
  info "Admin: ${ADMIN_EMAIL}"

  success "Bilgiler alındı"
}

# ---------------------------------------------------------------------------
# STEP 3 — Repository kurulumu
# RHEL: EPEL + Remi + PowerDNS
# Debian/Ubuntu: deb.sury.org (PHP multi-version) + PowerDNS
# ---------------------------------------------------------------------------
step_install_repos() {
  step "3/17  Repository kurulumu"

  if [[ "$OS_FAMILY" == "rhel" ]]; then
    info "EPEL 9"
    pkg_install epel-release

    info "Remi PHP repo"
    pkg_install "https://rpms.remirepo.net/enterprise/remi-release-9.rpm"

    info "PowerDNS authoritative 4.9 repo"
    # 2025'te repo dosya adı el-pdns-49.repo → el-auth-49.repo olarak değişti.
    # Fallback: doğrudan repo dosyasını manuel oluştur (URL ölü olsa bile çalışır).
    if ! curl -fsSL https://repo.powerdns.com/repo-files/el-auth-49.repo \
         -o /etc/yum.repos.d/pdns.repo 2>/dev/null; then
      warn "PowerDNS resmi repo URL'i erişilemedi — fallback olarak repo dosyası manuel oluşturuluyor"
      cat > /etc/yum.repos.d/pdns.repo <<'PDNSREPO'
[powerdns-auth-49]
name=PowerDNS Authoritative Server 4.9 - $basearch
baseurl=https://repo.powerdns.com/repo-files/$basearch/auth-49/
enabled=1
gpgcheck=1
gpgkey=https://repo.powerdns.com/CBC8B383-pub.asc
       https://repo.powerdns.com/FD380FBB-pub.asc
PDNSREPO
    fi
  else
    info "apt-get update (mevcut repo'ları senkronize et)"
    pkg_update

    info "Önkoşul paketler (gnupg, ca-certificates, lsb-release)"
    pkg_install ca-certificates apt-transport-https lsb-release gnupg2 curl wget

    info "deb.sury.org PHP repo (multi-version)"
    if [[ "$DISTRO" == "ubuntu" ]]; then
      # Ubuntu — PPA via curl + gpg
      run curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/trusted.gpg.d/sury-php.gpg
      run sh -c "echo 'deb https://ppa.launchpadcontent.net/ondrej/php/ubuntu $(lsb_release -sc) main' > /etc/apt/sources.list.d/ondrej-php.list"
    else
      # Debian
      run curl -fsSL https://packages.sury.org/php/apt.gpg -o /etc/apt/trusted.gpg.d/sury-php.gpg
      run sh -c "echo 'deb https://packages.sury.org/php/ $(lsb_release -sc) main' > /etc/apt/sources.list.d/sury-php.list"
    fi

    info "PowerDNS 4.9 repo (Sury / PowerDNS resmi)"
    if [[ "$DISTRO" == "ubuntu" ]]; then
      run curl -fsSL https://repo.powerdns.com/CBC8B383-pub.asc -o /etc/apt/trusted.gpg.d/powerdns.asc
      run sh -c "echo 'deb [arch=amd64] https://repo.powerdns.com/ubuntu $(lsb_release -sc)-auth-49 main' > /etc/apt/sources.list.d/pdns.list"
    else
      run curl -fsSL https://repo.powerdns.com/CBC8B383-pub.asc -o /etc/apt/trusted.gpg.d/powerdns.asc
      run sh -c "echo 'deb [arch=amd64] https://repo.powerdns.com/debian $(lsb_release -sc)-auth-49 main' > /etc/apt/sources.list.d/pdns.list"
    fi

    # Sury / PowerDNS pin (öncelik)
    cat > /etc/apt/preferences.d/pdns <<EOF
Package: pdns-*
Pin: origin repo.powerdns.com
Pin-Priority: 600
EOF
  fi

  info "Paket listesi güncelleniyor"
  pkg_update

  success "Repository kurulumu tamamlandı"
}

# ---------------------------------------------------------------------------
# STEP 4 — Paket kurulumu
# ---------------------------------------------------------------------------
step_install_packages() {
  step "4/17  Paket kurulumu"

  if [[ "$OS_FAMILY" == "rhel" ]]; then
    _install_packages_rhel
  else
    _install_packages_debian
  fi

  # Composer — her iki OS'ta aynı yöntem
  if command -v composer &>/dev/null; then
    # --version subshell'i Packagist'e takılırsa hang yapar — timeout ile koru
    local composer_ver
    composer_ver=$(timeout 10 composer --version 2>/dev/null | head -1 || echo "version-check timeout")
    info "Composer mevcut (${composer_ver})"
    # self-update non-critical: mevcut composer panel deploy için yeterli.
    info "  self-update deneniyor (30sn timeout)"
    timeout 30 composer self-update --no-interaction >> "$LOG" 2>&1 \
      && success "  composer güncellendi" \
      || warn "  composer self-update başarısız/timeout (kritik değil, mevcut composer kullanılır)"
  else
    info "Composer kuruluyor"
    run curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php
    run php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
    rm -f /tmp/composer-setup.php
  fi

  # Default php CLI link
  try_run ln -sfn "${PHP_CLI_PATH}" /usr/local/bin/php 2>/dev/null || true

  success "Paket kurulumu tamamlandı"
}

_install_packages_rhel() {
  info "Base paketler (RHEL)"
  pkg_install \
    httpd mod_ssl mod_security mod_security_crs \
    "${MARIADB_PKG}" \
    "${REDIS_PKG}" \
    fail2ban-server fail2ban-firewalld \
    firewalld nftables \
    quota xfsprogs \
    policycoreutils-python-utils setools-console \
    openssh-server \
    cronie chrony \
    git curl wget tar \
    jq inotify-tools htop lsof net-tools bind-utils \
    whois sqlite \
    certbot python3-certbot-apache
  # grepcidr — EPEL'den (firewall IP lookup + GeoIP scan için)
  pkg_install --enablerepo=epel grepcidr || warn "grepcidr kurulamadı (EPEL erişimi gerek) — IP araçları sınırlı çalışır"

  local PHP_EXTS="mysqlnd opcache gd curl mbstring zip xml intl bcmath json process"
  for ver in "${PHP_VERSIONS[@]}"; do
    info "PHP ${ver} (Remi)"
    local pkgs="php${ver}-php-fpm php${ver}-php-cli"
    for ext in $PHP_EXTS; do
      pkgs="${pkgs} php${ver}-php-${ext}"
    done
    [[ "$ver" != "74" ]] && pkgs="${pkgs} php${ver}-php-imagick"
    # shellcheck disable=SC2086
    pkg_install ${pkgs}
  done

  info "PowerDNS + gmysql"
  pkg_install "${PDNS_PKG}" "${PDNS_BACKEND_PKG}"

  info "pure-ftpd (EPEL)"
  pkg_install pure-ftpd

  info "phpMyAdmin (EPEL) — DB yönetimi için web arayüz"
  pkg_install --enablerepo=epel phpMyAdmin || warn "phpMyAdmin kurulamadı (EPEL erişimi gerek)"
  _configure_phpmyadmin_rhel

  # Mail Stack — Postfix + Dovecot + Rspamd + Roundcube
  if [[ "$ENABLE_MAIL_SERVER" -eq 1 ]]; then
    _install_mail_stack_rhel
  else
    info "Mail server kurulumu atlandı (--no-mail-server)"
  fi

  info "Node.js 20"
  # AlmaLinux 9 default stream nodejs:18 — :20 geçişi prompt'a takılabilir.
  # 1. yöntem: AppStream module reset + enable (timeout ile)
  # 2. yöntem: NodeSource resmi repo (daha güvenilir)
  if ! command -v node &>/dev/null || [[ "$(node -v 2>/dev/null | sed 's/v//;s/\..*//')" -lt 20 ]]; then
    info "  AppStream nodejs:20 module deneniyor (60sn timeout)"
    if timeout 60 dnf module reset -y nodejs >> "$LOG" 2>&1 \
       && timeout 60 dnf module enable -y --assumeyes nodejs:20 >> "$LOG" 2>&1 \
       && timeout 180 dnf install -y --allowerasing nodejs npm >> "$LOG" 2>&1; then
      success "  Node.js AppStream'den kuruldu: $(node -v 2>/dev/null)"
    else
      warn "  AppStream module başarısız — NodeSource fallback"
      # NodeSource resmi setup script'i — RHEL/CentOS desteği var
      if curl -4 -fsSL --retry 3 --connect-timeout 10 --max-time 30 \
           -o /tmp/nodesource_setup.sh https://rpm.nodesource.com/setup_20.x 2>>"$LOG"; then
        bash /tmp/nodesource_setup.sh >> "$LOG" 2>&1 || warn "NodeSource setup script başarısız"
        rm -f /tmp/nodesource_setup.sh
        timeout 180 dnf install -y --allowerasing nodejs >> "$LOG" 2>&1 \
          && success "  Node.js NodeSource'tan kuruldu: $(node -v 2>/dev/null)" \
          || die "Node.js kurulumu başarısız (hem AppStream hem NodeSource)"
      else
        die "NodeSource setup script indirilemedi — Node.js kurulumu mümkün değil"
      fi
    fi
  else
    info "  Node.js zaten kurulu: $(node -v)"
  fi
}

# ─── Mail Stack Installer (RHEL) ───────────────────────────────────────────
# Postfix MTA + Dovecot IMAP/POP3 + (opsiyonel) Rspamd + (opsiyonel) Webmail
_install_mail_stack_rhel() {
  info "Mail Stack — Postfix + Dovecot"
  pkg_install \
    postfix \
    dovecot \
    dovecot-pigeonhole \
    cyrus-sasl cyrus-sasl-plain

  if [[ "$ENABLE_SPAM_FILTER" -eq 1 ]]; then
    info "Rspamd Spam Filter (opsiyonel — fail durumunda atlanır)"
    # Rspamd resmi repo (RHEL'de EPEL'de yok, doğrudan upstream)
    # IPv4 force + retry + lokal indir then import — rspamd.com bazen IPv6 cevap vermiyor.
    if ! rpm -q rspamd &>/dev/null; then
      local rspamd_ok=1
      local tmp_key=/tmp/rspamd-gpg.key

      info "  GPG key indiriliyor (IPv4 + retry)"
      if curl -4 -fsSL --retry 3 --connect-timeout 10 --max-time 30 \
           -o "$tmp_key" https://rspamd.com/rpm-stable/gpg.key 2>>"$LOG"; then
        try_run rpm --import "$tmp_key" \
          || { warn "Rspamd GPG key import edilemedi"; rspamd_ok=0; }
        rm -f "$tmp_key"
      else
        warn "Rspamd GPG key indirilemedi (rspamd.com erişimi yok)"
        rspamd_ok=0
      fi

      if [[ $rspamd_ok -eq 1 ]]; then
        info "  Repo dosyası indiriliyor"
        if curl -4 -fsSL --retry 3 --connect-timeout 10 --max-time 30 \
             -o /etc/yum.repos.d/rspamd.repo \
             https://rspamd.com/rpm-stable/centos-9/rspamd.repo 2>>"$LOG"; then
          :
        else
          warn "Rspamd repo dosyası indirilemedi"
          rspamd_ok=0
        fi
      fi

      if [[ $rspamd_ok -eq 1 ]]; then
        info "  rspamd paketi kuruluyor"
        if dnf install -y rspamd >> "$LOG" 2>&1; then
          success "Rspamd kuruldu"
        else
          warn "Rspamd paketi kurulamadı — spam filter atlandı"
          ENABLE_SPAM_FILTER=0
          rm -f /etc/yum.repos.d/rspamd.repo
        fi
      else
        warn "Spam filter atlandı — Postfix + Dovecot yine kurulu olacak"
        warn "İstersen sonra elle kurabilirsin: dnf install rspamd (repo: rspamd.com)"
        ENABLE_SPAM_FILTER=0
      fi
    fi
  fi

  if [[ "$ENABLE_WEBMAIL" -eq 1 ]]; then
    case "$WEBMAIL_DRIVER" in
      roundcube) _install_roundcube_rhel ;;
      snappymail) _install_snappymail ;;
      none) info "Webmail driver=none, kurulum atlandı" ;;
      *) warn "Bilinmeyen webmail driver: $WEBMAIL_DRIVER — atlandı" ;;
    esac
  fi
}

_install_roundcube_rhel() {
  info "Roundcube (webmail) — EPEL"

  # Ana paket — bu kuruluyorsa devam, değilse webmail tamamen atla
  if ! dnf install -y --enablerepo=epel roundcubemail >> "$LOG" 2>&1; then
    warn "roundcubemail paketi EPEL'de bulunamadı — webmail atlandı (kritik değil)"
    warn "Sonra elle kurabilirsin: dnf install --enablerepo=epel roundcubemail"
    ENABLE_WEBMAIL=0
    return 0
  fi
  success "  roundcubemail kuruldu"

  # Opsiyonel destek paketleri — birer birer dene, yokları atla
  info "  Opsiyonel PEAR paketleri (yoksa atlanır)"
  for pkg in php-pear-Net-IDNA2 php-pear-Net-LDAP3 php-pear-Net-SMTP php-pear-Net-Sieve; do
    dnf install -y --enablerepo=epel "$pkg" >> "$LOG" 2>&1 \
      && info "    ✓ $pkg" \
      || info "    - $pkg (EPEL'de yok, atlandı)"
  done

  # Roundcube DB hazırlığı — MariaDB step 9'da yapılandırılır, burada henüz hazır olmayabilir.
  # 3 senaryo: (1) root_cnf var → kullan (2) passwordless → kullan (3) hiçbiri → step 9 sonrası elle
  if ! command -v mysql &>/dev/null; then
    warn "Roundcube DB: mysql binary yok — DB step 9 sonrası elle yapılabilir"
    return 0
  fi
  if ! systemctl is-active --quiet mariadb 2>/dev/null; then
    info "Roundcube DB: MariaDB henüz aktif değil — step 9 sonrası elle yapılabilir"
    return 0
  fi

  local mysql_cmd=""
  if [[ -f /root/.onox-mysql-root.cnf ]] && mysql --defaults-file=/root/.onox-mysql-root.cnf -e "SELECT 1" &>/dev/null; then
    mysql_cmd="mysql --defaults-file=/root/.onox-mysql-root.cnf"
    info "Roundcube DB: root_cnf ile bağlanılıyor"
  elif mysql -e "SELECT 1" &>/dev/null; then
    mysql_cmd="mysql"
    info "Roundcube DB: passwordless root ile bağlanılıyor"
  else
    warn "Roundcube DB: root erişimi yok — DB step 9 sonrası elle yapılabilir"
    warn "  Komut: mysql --defaults-file=/root/.onox-mysql-root.cnf < /usr/share/roundcubemail/SQL/mysql.initial.sql"
    return 0
  fi

  info "Roundcube veritabanı oluşturuluyor"
  local rcube_pass
  rcube_pass="$(openssl rand -base64 24 | tr -d '=+/' | head -c 24)"
  $mysql_cmd -e "CREATE DATABASE IF NOT EXISTS roundcubemail CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" >> "$LOG" 2>&1 \
    || { warn "Roundcube DB oluşturulamadı (kritik değil)"; return 0; }
  $mysql_cmd -e "CREATE USER IF NOT EXISTS 'roundcube'@'localhost' IDENTIFIED BY '${rcube_pass}';" >> "$LOG" 2>&1
  $mysql_cmd -e "GRANT ALL ON roundcubemail.* TO 'roundcube'@'localhost';" >> "$LOG" 2>&1
  $mysql_cmd -e "FLUSH PRIVILEGES;" >> "$LOG" 2>&1
  # Schema import
  if [[ -f /usr/share/roundcubemail/SQL/mysql.initial.sql ]]; then
    $mysql_cmd roundcubemail < /usr/share/roundcubemail/SQL/mysql.initial.sql >> "$LOG" 2>&1 \
      || warn "Roundcube schema import başarısız"
  fi
  # Credentials kaydet
  mkdir -p /etc/onoxsoft
  cat > /etc/onoxsoft/roundcube.env <<EOF
ROUNDCUBE_DB_NAME=roundcubemail
ROUNDCUBE_DB_USER=roundcube
ROUNDCUBE_DB_PASS=${rcube_pass}
EOF
  chmod 600 /etc/onoxsoft/roundcube.env
  success "Roundcube DB hazır"
}

_install_snappymail() {
  info "SnappyMail (webmail) — manuel kurulum"
  local SNAPPY_DIR=/usr/share/snappymail
  local SNAPPY_VER="2.36.4"  # Latest stable @ 2026-05

  run mkdir -p "$SNAPPY_DIR"
  run curl -fsSL "https://github.com/the-djmaze/snappymail/releases/download/v${SNAPPY_VER}/snappymail-${SNAPPY_VER}.tar.gz" -o /tmp/snappymail.tar.gz
  run tar -xzf /tmp/snappymail.tar.gz -C "$SNAPPY_DIR"
  run chown -R "${WEB_USER}:${WEB_GROUP}" "$SNAPPY_DIR"
  run chmod 755 "$SNAPPY_DIR"
  rm -f /tmp/snappymail.tar.gz
}

# ─── phpMyAdmin Apache config + auth_type=cookie (RHEL) ────────────────────
_configure_phpmyadmin_rhel() {
  # EPEL phpMyAdmin paketi /etc/httpd/conf.d/phpMyAdmin.conf yazar ama default
  # erişim 127.0.0.1 ile sınırlı; panel SSO için Require all granted gerek.
  local PMA_CONF="/etc/httpd/conf.d/phpMyAdmin.conf"
  if [[ -f "${PMA_CONF}" ]]; then
    info "phpMyAdmin Apache config — erişim açılıyor (Require all granted)"
    # mevcut Require ip 127.0.0.1 satırlarını Require all granted ile değiştir
    run sed -i 's|Require ip 127.0.0.1|Require all granted|g' "${PMA_CONF}"
    run sed -i 's|Require ip ::1|# Require ip ::1|g' "${PMA_CONF}"
    # Apache henüz başlatılmamış olabilir (step 13'te start ediliyor). Sadece çalışıyorsa reload.
    if systemctl is-active --quiet httpd; then
      systemctl reload httpd >> "$LOG" 2>&1 \
        || warn "httpd reload başarısız (kritik değil, vhost step'te restart edilir)"
    else
      info "Apache henüz başlatılmadı — config step 13'te aktif olacak"
    fi
  else
    warn "phpMyAdmin Apache config bulunamadı: ${PMA_CONF}"
  fi

  # blowfish_secret — config.inc.php'de eksikse uyarı önler
  local PMA_CFG="/etc/phpMyAdmin/config.inc.php"
  if [[ -f "${PMA_CFG}" ]] && grep -q "cfg\['blowfish_secret'\] = ''" "${PMA_CFG}" 2>/dev/null; then
    local secret
    secret="$(openssl rand -base64 32 | tr -d '/\n=' | head -c 32)"
    run sed -i "s|cfg\['blowfish_secret'\] = '';|cfg['blowfish_secret'] = '${secret}';|" "${PMA_CFG}"
  fi
}

# ─── phpMyAdmin Apache config + auth_type=cookie (Debian) ──────────────────
_configure_phpmyadmin_debian() {
  # Debian apache2 conf-enabled altına symlink kurar, /phpmyadmin/ default'tur
  if [[ -f /etc/apache2/conf-available/phpmyadmin.conf ]]; then
    info "phpMyAdmin Apache conf etkinleştiriliyor"
    a2enconf phpmyadmin >> "$LOG" 2>&1 || true
    if systemctl is-active --quiet apache2; then
      systemctl reload apache2 >> "$LOG" 2>&1 \
        || warn "apache2 reload başarısız (kritik değil)"
    else
      info "Apache henüz başlatılmadı — config step 13'te aktif olacak"
    fi
  fi

  local PMA_CFG="/etc/phpmyadmin/config.inc.php"
  if [[ -f "${PMA_CFG}" ]] && grep -q "cfg\['blowfish_secret'\] = ''" "${PMA_CFG}" 2>/dev/null; then
    local secret
    secret="$(openssl rand -base64 32 | tr -d '/\n=' | head -c 32)"
    run sed -i "s|cfg\['blowfish_secret'\] = '';|cfg['blowfish_secret'] = '${secret}';|" "${PMA_CFG}"
  fi
}

# ─── Mail Stack Installer (Debian) ─────────────────────────────────────────
_install_mail_stack_debian() {
  info "Mail Stack — Postfix + Dovecot"
  # Postfix non-interactive
  run sh -c 'echo "postfix postfix/main_mailer_type select Internet Site" | debconf-set-selections'
  run sh -c "echo \"postfix postfix/mailname string ${PANEL_HOSTNAME}\" | debconf-set-selections"
  pkg_install \
    postfix \
    dovecot-core dovecot-imapd dovecot-pop3d dovecot-managesieved dovecot-sieve dovecot-mysql \
    sasl2-bin libsasl2-modules
  try_run systemctl enable --now postfix || warn "postfix enable başarısız (config sonra düzeltilebilir)"
  try_run systemctl enable --now dovecot || warn "dovecot enable başarısız"

  if [[ "$ENABLE_SPAM_FILTER" -eq 1 ]]; then
    info "Rspamd Spam Filter (opsiyonel — fail durumunda atlanır)"
    # IPv4 force + retry + lokal indir
    if ! dpkg -l rspamd &>/dev/null; then
      local rspamd_ok=1
      try_run mkdir -p /etc/apt/keyrings || rspamd_ok=0

      if [[ $rspamd_ok -eq 1 ]]; then
        info "  GPG key indiriliyor (IPv4 + retry)"
        if curl -4 -fsSL --retry 3 --connect-timeout 10 --max-time 30 \
             https://rspamd.com/apt-stable/gpg.key 2>>"$LOG" \
             | gpg --dearmor -o /etc/apt/keyrings/rspamd.gpg 2>>"$LOG"; then
          :
        else
          warn "Rspamd GPG key indirilemedi (rspamd.com erişimi yok)"
          rspamd_ok=0
        fi
      fi

      if [[ $rspamd_ok -eq 1 ]]; then
        try_run sh -c "echo 'deb [signed-by=/etc/apt/keyrings/rspamd.gpg] http://rspamd.com/apt-stable/ $(lsb_release -cs) main' > /etc/apt/sources.list.d/rspamd.list"
        try_run apt-get update || true
        if DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends rspamd >> "$LOG" 2>&1; then
          try_run systemctl enable --now rspamd || warn "rspamd servisi başlatılamadı"
          success "Rspamd kuruldu"
        else
          warn "Rspamd paketi kurulamadı — spam filter atlandı"
          ENABLE_SPAM_FILTER=0
        fi
      else
        warn "Spam filter atlandı — Postfix + Dovecot yine kurulu olacak"
        ENABLE_SPAM_FILTER=0
      fi
    fi
  fi

  if [[ "$ENABLE_WEBMAIL" -eq 1 ]]; then
    case "$WEBMAIL_DRIVER" in
      roundcube) _install_roundcube_debian ;;
      snappymail) _install_snappymail ;;
      none) info "Webmail driver=none, atlandı" ;;
      *) warn "Bilinmeyen webmail driver: $WEBMAIL_DRIVER — atlandı" ;;
    esac
  fi
}

_install_roundcube_debian() {
  info "Roundcube (webmail) — Debian package"
  # Non-interactive — dbconfig-common cevapları
  run sh -c 'echo "roundcube-core roundcube/dbconfig-install boolean true" | debconf-set-selections'
  run sh -c 'echo "roundcube-core roundcube/database-type select mysql" | debconf-set-selections'
  pkg_install roundcube roundcube-core roundcube-mysql roundcube-plugins
}

_install_packages_debian() {
  info "Base paketler (Debian/Ubuntu) — temel + ortak"
  pkg_install \
    "${MARIADB_PKG}" mariadb-client \
    "${REDIS_PKG}" \
    fail2ban \
    ufw \
    quota quotatool xfsprogs \
    openssh-server \
    cron chrony \
    git curl wget tar \
    jq inotify-tools htop lsof net-tools dnsutils \
    certbot \
    libapache2-mod-security2 modsecurity-crs

  # Web server seçime göre
  _install_web_server_debian

  # Antivirüs (opsiyonel)
  if [[ "$ENABLE_ANTIVIRUS" -eq 1 ]]; then
    info "ClamAV antivirüs (Debian)"
    pkg_install clamav clamav-daemon clamav-freshclam
    try_run systemctl stop clamav-freshclam || true
    try_run freshclam || warn "freshclam ilk güncelleme başarısız"
    run systemctl enable --now clamav-freshclam
    run systemctl enable --now clamav-daemon
  fi

  local PHP_EXTS="mysql opcache gd curl mbstring zip xml intl bcmath imagick"
  for ver in "${PHP_VERSIONS[@]}"; do
    info "PHP ${ver} (sury)"
    local pkgs="php${ver}-fpm php${ver}-cli"
    for ext in $PHP_EXTS; do
      pkgs="${pkgs} php${ver}-${ext}"
    done
    # shellcheck disable=SC2086
    pkg_install ${pkgs}
  done

  info "PowerDNS + gmysql"
  # Debian'da pdns-backend-mysql servisi otomatik başlatmaya çalışır ama DB henüz yok — geçici disable
  run sh -c 'echo "exit 101" > /usr/sbin/policy-rc.d && chmod +x /usr/sbin/policy-rc.d'
  pkg_install "${PDNS_PKG}" "${PDNS_BACKEND_PKG}"
  run rm -f /usr/sbin/policy-rc.d

  info "pure-ftpd"
  pkg_install pure-ftpd

  info "phpMyAdmin — DB yönetimi için web arayüz"
  # Debian phpmyadmin paketi dbconfig-common ile interaktif sorar; non-interactive olarak skip et
  echo "phpmyadmin phpmyadmin/dbconfig-install boolean false" | debconf-set-selections 2>/dev/null || true
  echo "phpmyadmin phpmyadmin/reconfigure-webserver multiselect" | debconf-set-selections 2>/dev/null || true
  pkg_install phpmyadmin || warn "phpMyAdmin kurulamadı"
  _configure_phpmyadmin_debian

  if [[ "$ENABLE_MAIL_SERVER" -eq 1 ]]; then
    _install_mail_stack_debian
  else
    info "Mail server kurulumu atlandı (--no-mail-server)"
  fi

  info "Node.js 20"
  # NodeSource resmi repo
  if ! command -v node &>/dev/null || [[ "$(node -v 2>/dev/null | cut -d. -f1 | tr -d v)" -lt 20 ]]; then
    run curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
    run bash /tmp/nodesource_setup.sh
    pkg_install nodejs
    rm -f /tmp/nodesource_setup.sh
  fi
}

# ---------------------------------------------------------------------------
# STEP 5 — Firewall
# RHEL: firewalld
# Debian/Ubuntu: ufw
# ---------------------------------------------------------------------------
step_configure_firewall() {
  step "5/17  Firewall (${FIREWALL_TOOL})"

  if [[ "$FIREWALL_TOOL" == "firewalld" ]]; then
    run systemctl enable --now firewalld

    for svc in http https smtp imaps ftp ssh dns; do
      run firewall-cmd --permanent --add-service="${svc}"
    done

    # FTP passive port aralığı
    run firewall-cmd --permanent --add-port=40000-40100/tcp

    run firewall-cmd --reload
  else
    # UFW
    run ufw default deny incoming
    run ufw default allow outgoing
    run ufw allow 22/tcp       # SSH
    run ufw allow 80/tcp       # HTTP
    run ufw allow 443/tcp      # HTTPS
    run ufw allow 53/tcp       # DNS
    run ufw allow 53/udp       # DNS
    run ufw allow 21/tcp       # FTP
    run ufw allow 40000:40100/tcp  # FTP passive
    run ufw allow 25/tcp       # SMTP (Phase 2)
    run ufw allow 993/tcp      # IMAPS (Phase 2)
    run ufw --force enable
  fi

  success "Firewall yapılandırıldı"
}

# ---------------------------------------------------------------------------
# STEP 6 — SELinux booleans + fcontext (RHEL) / AppArmor istisnaları (Debian)
# ---------------------------------------------------------------------------
step_configure_selinux() {
  step "6/17  Erişim kontrolü (SELinux/AppArmor)"

  if [[ "$HAS_SELINUX" -eq 1 ]]; then
    _configure_selinux_rhel
  else
    _configure_apparmor_debian
  fi
}

_configure_selinux_rhel() {
  local sel_mode
  sel_mode=$(getenforce 2>/dev/null || echo "Disabled")
  if [[ "$sel_mode" == "Disabled" ]]; then
    warn "SELinux Disabled — boolean/fcontext adımları atlanıyor"
    return 0
  fi

  run setsebool -P httpd_can_network_connect 1
  run setsebool -P httpd_enable_homedirs 1
  run setsebool -P httpd_read_user_content 1

  run semanage fcontext -a -t httpd_sys_content_t \
    '/home/[^/]+/public_html(/.*)?' 2>/dev/null || \
  run semanage fcontext -m -t httpd_sys_content_t \
    '/home/[^/]+/public_html(/.*)?'

  run semanage fcontext -a -t httpd_sys_rw_content_t \
    '/home/[^/]+/public_html/wp-content(/.*)?' 2>/dev/null || \
  run semanage fcontext -m -t httpd_sys_rw_content_t \
    '/home/[^/]+/public_html/wp-content(/.*)?'

  local panel_pub="${ONOX_HOME}/public(/.*)?"
  local panel_stor="${ONOX_HOME}/storage(/.*)?"

  semanage fcontext -a -t httpd_sys_content_t "${panel_pub}"     2>/dev/null \
    || semanage fcontext -m -t httpd_sys_content_t "${panel_pub}"
  semanage fcontext -a -t httpd_sys_rw_content_t "${panel_stor}" 2>/dev/null \
    || semanage fcontext -m -t httpd_sys_rw_content_t "${panel_stor}"

  try_run restorecon -R /home 2>/dev/null || true
  try_run restorecon -R "${ONOX_HOME}" 2>/dev/null || true

  success "SELinux yapılandırıldı"
}

_configure_apparmor_debian() {
  # Debian/Ubuntu varsayılan AppArmor profili Apache+PHP-FPM için çoğunlukla yeterli.
  # Pure-FTPd ve PowerDNS profilleri stack ile gelir; özel istisna gerekiyorsa runtime'da
  # /etc/apparmor.d/local/ altında eklenir. Phase 1 MVP'de ek profil zorlanmıyor.

  if command -v aa-status &>/dev/null; then
    info "AppArmor aktif: $(aa-status --enabled && echo "evet" || echo "hayır")"
  else
    warn "AppArmor kurulu değil — Debian/Ubuntu default'u olmadan ilerleniyor"
  fi

  success "Erişim kontrolü yapılandırıldı (AppArmor default)"
}

# ---------------------------------------------------------------------------
# STEP 7 — Sistem dizinleri ve gruplar
# ---------------------------------------------------------------------------
step_create_onoxsoft_user() {
  step "7/17  Sistem dizinleri ve gruplar"

  # groupadd 9 = "already exists" — die etme, idempotent davran
  if ! getent group onoxsoft-users &>/dev/null; then
    run groupadd --system onoxsoft-users
  else
    info "onoxsoft-users grubu zaten var"
  fi
  if ! getent group onoxsoft-sftp &>/dev/null; then
    run groupadd --system onoxsoft-sftp
  else
    info "onoxsoft-sftp grubu zaten var"
  fi

  run mkdir -p "${ONOX_BIN}"
  run mkdir -p "${ONOX_ETC}"
  run mkdir -p "${ONOX_ACME}"
  run mkdir -p /var/log/onoxsoft
  run mkdir -p /var/lib/onoxsoft/backups
  run mkdir -p /var/lib/onoxsoft/snapshots

  run chmod 750 "${ONOX_BIN}"
  run chmod 700 "${ONOX_ETC}"
  # WEB_GROUP henüz set olmamış olabilir (step 13'te belirleniyor) — fallback chown root
  if [[ -n "${WEB_GROUP:-}" ]] && getent group "$WEB_GROUP" &>/dev/null; then
    run chown -R "root:${WEB_GROUP}" "${ONOX_BIN}"
  else
    run chown -R root:root "${ONOX_BIN}"
  fi

  success "Dizinler hazırlandı"
}

# ---------------------------------------------------------------------------
# STEP 8 — acme.sh (Let's Encrypt client)
# ---------------------------------------------------------------------------
step_install_acme_sh() {
  step "8/17  acme.sh"

  if [[ -f "${ONOX_ACME}/acme.sh" ]]; then
    info "acme.sh mevcut, güncelleniyor"
    try_run "${ONOX_ACME}/acme.sh" --upgrade --home "${ONOX_ACME}" \
      || warn "acme.sh upgrade başarısız (ağ sorunu olabilir)"
    success "acme.sh hazır: ${ONOX_ACME}/acme.sh"
    return 0
  fi

  info "acme.sh klonlanıyor (IPv4 + timeout)"
  rm -rf /tmp/acme-src
  if ! timeout 60 git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=10 \
       clone --depth 1 https://github.com/acmesh-official/acme.sh.git /tmp/acme-src >> "$LOG" 2>&1; then
    warn "acme.sh GitHub clone başarısız — fallback: codeload tarball"
    rm -rf /tmp/acme-src
    if curl -4 -fsSL --retry 3 --connect-timeout 10 --max-time 60 \
         https://github.com/acmesh-official/acme.sh/archive/refs/heads/master.tar.gz \
         -o /tmp/acme.tar.gz 2>>"$LOG"; then
      mkdir -p /tmp/acme-src
      tar -xzf /tmp/acme.tar.gz -C /tmp/acme-src --strip-components=1
      rm -f /tmp/acme.tar.gz
    else
      warn "acme.sh kurulamadı — SSL Phase 1.5'a ertelendi. Sonra elle: dnf install certbot"
      return 0
    fi
  fi

  info "  acme.sh --install (60sn timeout)"
  # acme.sh --install bazı durumlarda parent dir already exists hatası verir.
  # Boş bir geçici home'a kur sonra ONOX_ACME'ye taşı.
  local acme_tmp_home=/tmp/acme-install-home
  rm -rf "$acme_tmp_home"
  if timeout 60 sh /tmp/acme-src/acme.sh \
       --install \
       --home "$acme_tmp_home" \
       --accountemail "${ADMIN_EMAIL}" \
       --nocron \
       --noprofile >> "$LOG" 2>&1; then
    # ONOX_ACME altına kopyala (mkdir step 7'de yapıldı)
    cp -rT "$acme_tmp_home" "${ONOX_ACME}"
    rm -rf "$acme_tmp_home"
    success "acme.sh kuruldu: ${ONOX_ACME}/acme.sh"
  else
    # acme.sh --install fail — manuel kopya yöntemiyle dene
    warn "  --install fail, manuel kopya yöntemi deneniyor"
    cp /tmp/acme-src/acme.sh "${ONOX_ACME}/acme.sh"
    chmod +x "${ONOX_ACME}/acme.sh"
    # Account email kaydet
    mkdir -p "${ONOX_ACME}/account.conf.d"
    echo "ACCOUNT_EMAIL='${ADMIN_EMAIL}'" > "${ONOX_ACME}/account.conf"
    success "acme.sh manuel kuruldu: ${ONOX_ACME}/acme.sh"
  fi
  rm -rf /tmp/acme-src
}

# ---------------------------------------------------------------------------
# STEP 9 — MariaDB: panel + powerdns DB
# ---------------------------------------------------------------------------
step_setup_mariadb() {
  step "9/17  MariaDB"

  run systemctl enable --now "${MARIADB_SVC}"

  local root_cnf=/root/.onox-mysql-root.cnf

  # 3 senaryo:
  #   A) Mevcut root_cnf çalışıyor → idempotent install, kullan
  #   B) Passwordless root (fresh) → yeni şifre üret + kaydet
  #   C) Şifre ayarlı ama bilinmiyor → kurtarma talimatı ver

  if [[ -f "$root_cnf" ]] && mysql --defaults-file="$root_cnf" -e "SELECT 1" &>/dev/null; then
    info "Mevcut ${root_cnf} ile bağlanılıyor (idempotent)"

  elif mysql -u root -e "SELECT 1" &>/dev/null; then
    info "MariaDB passwordless root — yeni şifre üretiliyor"
    local db_root_pass
    db_root_pass=$(openssl rand -hex 24)
    mysql -u root --connect-expired-password -e \
      "ALTER USER 'root'@'localhost' IDENTIFIED BY '${db_root_pass}'; FLUSH PRIVILEGES;" \
      >> "$LOG" 2>&1 \
      || die "Root şifresi ayarlanamadı"
    cat > "${root_cnf}" <<EOF
[client]
host     = localhost
user     = root
password = ${db_root_pass}
EOF
    chmod 600 "${root_cnf}"
    info "Yeni root şifresi: ${root_cnf}"

  else
    # Bilinmeyen şifreyle takılı — kurtarma seçeneklerini öner
    warn "MariaDB root şifresi ayarlı ve ${root_cnf} bilinmiyor."
    warn ""
    warn "Çözüm 1 (TEMİZ RESET — DB'leri SİLER):"
    warn "  systemctl stop ${MARIADB_SVC}"
    warn "  rm -rf /var/lib/mysql/*"
    warn "  mysql_install_db --user=mysql --datadir=/var/lib/mysql"
    warn "  systemctl start ${MARIADB_SVC}"
    warn "  bash /tmp/install.sh --admin-email ${ADMIN_EMAIL} --ip-only  # tekrar"
    warn ""
    warn "Çözüm 2 (root şifre reset, DB'yi koru):"
    warn "  systemctl stop ${MARIADB_SVC}"
    warn "  mysqld_safe --skip-grant-tables --skip-networking &"
    warn "  mysql -u root -e \"UPDATE mysql.user SET authentication_string=PASSWORD('YENI_SIFRE') WHERE User='root'; FLUSH PRIVILEGES;\""
    warn "  pkill mysqld; sleep 2; systemctl start ${MARIADB_SVC}"
    warn ""
    die "MariaDB root erişimi olmadan devam edilemiyor"
  fi

  # db.env zaten varsa → şifreleri ondan oku (idempotent + user şifreleri eşleşir).
  # Yoksa yeni rastgele üret + ALTER USER ile MariaDB user'ların şifresini güncelle.
  local db_panel_pass db_pdns_pass
  if [[ -f "${ONOX_ETC}/db.env" ]] && grep -q '^DB_PASSWORD=' "${ONOX_ETC}/db.env"; then
    info "Mevcut db.env'den şifreler okunuyor (idempotent)"
    db_panel_pass=$(grep '^DB_PASSWORD=' "${ONOX_ETC}/db.env" | cut -d= -f2-)
    db_pdns_pass=$(grep '^PDNS_DB_PASSWORD=' "${ONOX_ETC}/db.env" | cut -d= -f2-)
  else
    info "Yeni DB şifreleri üretiliyor"
    db_panel_pass=$(openssl rand -hex 24)
    db_pdns_pass=$(openssl rand -hex 24)
  fi

  # CREATE DATABASE + ALTER USER (varsa şifre güncelle, yoksa CREATE USER)
  mysql --defaults-file="${root_cnf}" <<SQL
CREATE DATABASE IF NOT EXISTS onoxsoft_panel
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'onoxsoft_panel'@'localhost' IDENTIFIED BY '${db_panel_pass}';
ALTER USER 'onoxsoft_panel'@'localhost' IDENTIFIED BY '${db_panel_pass}';
GRANT ALL PRIVILEGES ON onoxsoft_panel.* TO 'onoxsoft_panel'@'localhost';

CREATE DATABASE IF NOT EXISTS onoxsoft_pdns
  CHARACTER SET latin1;
CREATE USER IF NOT EXISTS 'onoxsoft_pdns'@'localhost' IDENTIFIED BY '${db_pdns_pass}';
ALTER USER 'onoxsoft_pdns'@'localhost' IDENTIFIED BY '${db_pdns_pass}';
GRANT ALL PRIVILEGES ON onoxsoft_pdns.* TO 'onoxsoft_pdns'@'localhost';

DELETE FROM mysql.user WHERE User='';
DROP DATABASE IF EXISTS test;
FLUSH PRIVILEGES;
SQL

  mkdir -p "${ONOX_ETC}"
  cat > "${ONOX_ETC}/db.env" <<EOF
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=onoxsoft_panel
DB_USERNAME=onoxsoft_panel
DB_PASSWORD=${db_panel_pass}
PDNS_DB_DATABASE=onoxsoft_pdns
PDNS_DB_USERNAME=onoxsoft_pdns
PDNS_DB_PASSWORD=${db_pdns_pass}
EOF
  chmod 600 "${ONOX_ETC}/db.env"

  success "MariaDB yapılandırıldı"
}

# ---------------------------------------------------------------------------
# STEP 10 — PowerDNS: gmysql schema + pdns.conf
# ---------------------------------------------------------------------------
step_setup_powerdns() {
  step "10/17  PowerDNS"

  # shellcheck source=/dev/null
  source "${ONOX_ETC}/db.env"

  local root_cnf=/root/.onox-mysql-root.cnf

  info "gmysql schema import ediliyor"
  local schema_url="https://raw.githubusercontent.com/PowerDNS/pdns/master/modules/gmysqlbackend/schema.mysql.sql"
  if curl -fsSL --max-time 30 "$schema_url" -o /tmp/pdns-schema.sql 2>/dev/null; then
    mysql --defaults-file="${root_cnf}" "${PDNS_DB_DATABASE}" < /tmp/pdns-schema.sql 2>/dev/null \
      || info "Schema zaten var, atlanıyor"
    rm -f /tmp/pdns-schema.sql
  else
    warn "PowerDNS schema indirilemedi — elle import gerekebilir"
  fi

  local pdns_conf="${PDNS_CONF_DIR}/pdns.conf"
  info "pdns.conf yazılıyor: ${pdns_conf}"

  # Debian'da default pdns.conf yapılandırması farklı — temizle
  if [[ "$OS_FAMILY" == "debian" ]]; then
    # Debian ek launcher dosyaları kullanır (/etc/powerdns/pdns.d/*.conf)
    run mkdir -p "${PDNS_CONF_DIR}/pdns.d"
    # Default sqlite backend'i devre dışı bırak
    [[ -f "${PDNS_CONF_DIR}/pdns.d/bind.conf" ]] && run rm -f "${PDNS_CONF_DIR}/pdns.d/bind.conf"
  fi

  cat > "${pdns_conf}" <<EOF
# ONOX — PowerDNS gmysql configuration
# Generated by install.sh v${SCRIPT_VERSION}

launch=gmysql
local-address=0.0.0.0
local-port=53

gmysql-host=127.0.0.1
gmysql-port=3306
gmysql-dbname=${PDNS_DB_DATABASE}
gmysql-user=${PDNS_DB_USERNAME}
gmysql-password=${PDNS_DB_PASSWORD}
gmysql-dnssec=yes

setuid=pdns
setgid=pdns
log-dns-queries=no
loglevel=4
EOF

  # guardian yalnızca RHEL pdns paketinde — Debian'da systemd kullanılır
  [[ "$OS_FAMILY" == "rhel" ]] && echo "guardian=yes" >> "${pdns_conf}"

  chmod 640 "${pdns_conf}"
  chown root:pdns "${pdns_conf}"

  # Servis adı — Debian'da pdns, RHEL'de pdns
  run systemctl enable --now pdns

  success "PowerDNS yapılandırıldı"
}

# ---------------------------------------------------------------------------
# STEP 11 — Redis
# ---------------------------------------------------------------------------
step_setup_redis() {
  step "11/17  Redis"

  local redis_conf
  if [[ -f /etc/redis/redis.conf ]]; then
    redis_conf=/etc/redis/redis.conf
  elif [[ -f /etc/redis.conf ]]; then
    redis_conf=/etc/redis.conf
  else
    warn "Redis conf bulunamadı, default ile devam ediliyor"
    redis_conf=""
  fi

  if [[ -n "$redis_conf" ]] && ! grep -q '^bind 127.0.0.1' "$redis_conf"; then
    run sed -i 's/^bind .*/bind 127.0.0.1/' "$redis_conf"
  fi

  run systemctl enable --now "${REDIS_SVC}"

  success "Redis çalışıyor"
}

# ---------------------------------------------------------------------------
# STEP 11.5 — Mail Stack konfigürasyonu (Postfix + Dovecot + Rspamd + Webmail)
# ---------------------------------------------------------------------------
step_configure_mail_stack() {
  if [[ "$ENABLE_MAIL_SERVER" -ne 1 ]]; then
    info "Mail stack atlandı (--no-mail-server)"
    return 0
  fi

  step "11.5/17  Mail Stack konfigürasyonu"
  _configure_mail_stack
  success "Mail stack çalışıyor — Postfix + Dovecot${ENABLE_SPAM_FILTER:+ + Rspamd}${ENABLE_WEBMAIL:+ + ${WEBMAIL_DRIVER^}}"
}

# Postfix + Dovecot + Rspamd entegrasyonu
_configure_mail_stack() {
  # mail.<hostname> alias
  local mail_host="mail.${PANEL_HOSTNAME}"

  # ─── Postfix main.cf — temel ayarlar ───────────────────────────────────────
  info "Postfix yapılandırması"
  run postconf -e "myhostname = ${mail_host}"
  run postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost"
  run postconf -e "inet_interfaces = all"
  run postconf -e "inet_protocols = all"
  run postconf -e "home_mailbox = Maildir/"
  run postconf -e "smtpd_banner = \$myhostname ESMTP"

  # Dovecot LMTP delivery (Postfix → Dovecot socket → Maildir)
  run postconf -e "virtual_transport = lmtp:unix:private/dovecot-lmtp"
  run postconf -e "mailbox_command ="
  run postconf -e "mailbox_size_limit = 0"
  run postconf -e "message_size_limit = 52428800"  # 50MB max

  # SASL — Dovecot üzerinden auth
  run postconf -e "smtpd_sasl_type = dovecot"
  run postconf -e "smtpd_sasl_path = private/auth"
  run postconf -e "smtpd_sasl_auth_enable = yes"
  run postconf -e "smtpd_sasl_security_options = noanonymous"
  run postconf -e "broken_sasl_auth_clients = yes"
  run postconf -e "smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated reject_unauth_destination"

  # TLS (self-signed başta — Let's Encrypt sonradan)
  if [[ ! -f /etc/pki/dovecot/certs/dovecot.pem ]] && [[ ! -f /etc/ssl/certs/dovecot.pem ]]; then
    info "Self-signed TLS sertifika (geçici — Let's Encrypt sonra)"
    run mkdir -p /etc/pki/dovecot/{certs,private}
    run openssl req -new -x509 -days 365 -nodes \
      -out /etc/pki/dovecot/certs/dovecot.pem \
      -keyout /etc/pki/dovecot/private/dovecot.pem \
      -subj "/CN=${mail_host}" 2>/dev/null || warn "Self-signed cert oluşturulamadı"
    run chmod 600 /etc/pki/dovecot/private/dovecot.pem
  fi

  run postconf -e "smtpd_tls_cert_file = /etc/pki/dovecot/certs/dovecot.pem"
  run postconf -e "smtpd_tls_key_file = /etc/pki/dovecot/private/dovecot.pem"
  run postconf -e "smtpd_tls_security_level = may"
  run postconf -e "smtpd_use_tls = yes"
  run postconf -e "smtp_tls_security_level = may"

  # Submission port 587 — auth zorunlu, TLS zorunlu
  if ! grep -q "^submission" /etc/postfix/master.cf 2>/dev/null; then
    cat >> /etc/postfix/master.cf <<'EOF'

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
EOF
  fi

  # ─── Dovecot config ────────────────────────────────────────────────────────
  info "Dovecot yapılandırması"
  local DOVECOT_CONF=/etc/dovecot/dovecot.conf
  local DOVECOT_LOCAL=/etc/dovecot/local.conf

  cat > "$DOVECOT_LOCAL" <<EOF
# Onoxsoft Panel — Dovecot config
protocols = imap pop3 lmtp sieve
mail_location = maildir:~/Maildir
mail_privileged_group = mail

# SSL — Postfix ile aynı sertifika
ssl = yes
ssl_cert = </etc/pki/dovecot/certs/dovecot.pem
ssl_key  = </etc/pki/dovecot/private/dovecot.pem
ssl_min_protocol = TLSv1.2

# SASL — Postfix auth proxy
service auth {
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  }
}

# LMTP — Postfix → Dovecot maildir teslimat
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}

# IMAP — limitler
protocol imap {
  mail_max_userip_connections = 20
}

# Sieve (server-side mail filter)
protocol lda {
  mail_plugins = \$mail_plugins sieve
}
protocol lmtp {
  mail_plugins = \$mail_plugins sieve
}

# Quota (gerçek backend Phase 2'de)
plugin {
  sieve = ~/.dovecot.sieve
  sieve_dir = ~/sieve
}

# Auth
auth_mechanisms = plain login
disable_plaintext_auth = yes
EOF

  # Postfix/Dovecot restart non-critical: config sorunu varsa panel yine çalışır,
  # mail sonra manuel düzeltilebilir. Detaylı hata logu için: journalctl -u postfix -n 50
  info "Postfix + Dovecot servisleri başlatılıyor"
  try_run systemctl enable postfix 2>/dev/null || true
  if try_run systemctl restart postfix; then
    success "Postfix çalışıyor"
  else
    warn "Postfix restart başarısız — config'i sonra inceleyebilirsin:"
    warn "  journalctl -u postfix -n 50"
    warn "  postfix check"
    warn "Panel çalışmaya devam edecek, mail step 11.5 sonrası elle düzeltilebilir."
  fi
  try_run systemctl enable dovecot 2>/dev/null || true
  if try_run systemctl restart dovecot; then
    success "Dovecot çalışıyor"
  else
    warn "Dovecot restart başarısız — journalctl -u dovecot -n 50"
  fi

  # ─── Rspamd milter integration ─────────────────────────────────────────────
  if [[ "$ENABLE_SPAM_FILTER" -eq 1 ]] && command -v rspamd &>/dev/null; then
    info "Rspamd → Postfix milter bağlantısı"
    run postconf -e "smtpd_milters = inet:127.0.0.1:11332"
    run postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
    run postconf -e "milter_protocol = 6"
    run postconf -e "milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}"
    run postconf -e "milter_default_action = accept"

    # Rspamd controller password (Web UI auth)
    local rspamd_pw
    rspamd_pw="$(openssl rand -base64 20 | tr -d '=+/' | head -c 20)"
    local rspamd_pw_hash
    rspamd_pw_hash="$(rspamadm pw -p "$rspamd_pw" 2>/dev/null || echo "")"
    if [[ -n "$rspamd_pw_hash" ]]; then
      mkdir -p /etc/rspamd/local.d
      cat > /etc/rspamd/local.d/worker-controller.inc <<EOF
password = "${rspamd_pw_hash}"
bind_socket = "127.0.0.1:11334";
EOF
      echo "RSPAMD_WEBUI_PASS=${rspamd_pw}" > /etc/onoxsoft/rspamd.env
      chmod 600 /etc/onoxsoft/rspamd.env
      info "Rspamd Web UI parolası: /etc/onoxsoft/rspamd.env"
    fi

    try_run systemctl enable --now rspamd || warn "rspamd enable başarısız"
    try_run systemctl restart rspamd || warn "rspamd restart başarısız"
    try_run systemctl restart postfix || warn "postfix milter sonrası restart başarısız"
  fi

  # ─── Webmail Apache/Nginx vhost ────────────────────────────────────────────
  if [[ "$ENABLE_WEBMAIL" -eq 1 ]] && [[ "$WEBMAIL_DRIVER" != "none" ]]; then
    info "Webmail (${WEBMAIL_DRIVER}) — Apache vhost"
    _setup_webmail_vhost
  fi

  # ─── Firewall: mail portları ───────────────────────────────────────────────
  info "Firewall: mail portları (25, 465, 587, 143, 993, 110, 995)"
  if [[ "$FIREWALL_TOOL" == "firewalld" ]]; then
    run firewall-cmd --permanent --add-service=smtp
    run firewall-cmd --permanent --add-service=smtps
    run firewall-cmd --permanent --add-service=imap
    run firewall-cmd --permanent --add-service=imaps
    run firewall-cmd --permanent --add-service=pop3
    run firewall-cmd --permanent --add-service=pop3s
    run firewall-cmd --permanent --add-port=587/tcp  # submission
    run firewall-cmd --reload
  elif [[ "$FIREWALL_TOOL" == "ufw" ]]; then
    run ufw allow 25/tcp
    run ufw allow 465/tcp
    run ufw allow 587/tcp
    run ufw allow 143/tcp
    run ufw allow 993/tcp
    run ufw allow 110/tcp
    run ufw allow 995/tcp
  fi

  # ─── SELinux booleans (RHEL) ───────────────────────────────────────────────
  if [[ "$HAS_SELINUX" -eq 1 ]]; then
    info "SELinux mail boolean'ları"
    try_run setsebool -P httpd_can_network_connect 1 2>/dev/null || true
    try_run setsebool -P httpd_can_sendmail 1 2>/dev/null || true
  fi
}

_setup_webmail_vhost() {
  local webmail_path
  local webmail_url_path

  case "$WEBMAIL_DRIVER" in
    roundcube)
      webmail_path="/usr/share/roundcubemail"
      webmail_url_path="/webmail"
      ;;
    snappymail)
      webmail_path="/usr/share/snappymail"
      webmail_url_path="/mail"
      ;;
    *)
      warn "Bilinmeyen webmail driver: $WEBMAIL_DRIVER"
      return 0
      ;;
  esac

  # Apache alias (panel hostname üzerinde)
  if [[ "$WEB_SERVER" == "apache" ]]; then
    local vhost_file
    if [[ "$OS_FAMILY" == "rhel" ]]; then
      vhost_file="${WEB_CONF_DIR}/webmail.conf"
    else
      vhost_file="${WEB_CONF_DIR}/webmail.conf"
    fi

    cat > "$vhost_file" <<EOF
# Onoxsoft Panel — ${WEBMAIL_DRIVER} alias
Alias ${webmail_url_path} ${webmail_path}

<Directory ${webmail_path}>
    Options +FollowSymLinks
    AllowOverride All
    Require all granted

    <IfModule mod_dir.c>
        DirectoryIndex index.php
    </IfModule>
</Directory>

# Block sensitive dirs
<Directory ${webmail_path}/config>
    Require all denied
</Directory>
<Directory ${webmail_path}/temp>
    Require all denied
</Directory>
<Directory ${webmail_path}/logs>
    Require all denied
</Directory>
EOF
    # Apache henüz başlatılmamış olabilir (step 13'te start ediliyor).
    if systemctl is-active --quiet "${WEB_SVC}"; then
      systemctl reload "${WEB_SVC}" >> "$LOG" 2>&1 \
        || warn "${WEB_SVC} reload başarısız (vhost step'te restart edilir)"
    else
      info "Apache henüz başlatılmadı — webmail vhost step 13'te aktif olacak"
    fi
  fi
}

# ---------------------------------------------------------------------------
# STEP 12 — Panel uygulamasını deploy et (kaynak çek + composer + npm + .env)
# ---------------------------------------------------------------------------
step_clone_panel() {
  step "12/17  Panel uygulaması deploy"

  # shellcheck source=/dev/null
  source "${ONOX_ETC}/db.env"

  mkdir -p "${ONOX_HOME}"

  # Kaynak çekme stratejisi (öncelik sırası):
  #   1. /opt/onox-source/  varsa: yerel kaynak (manuel SCP veya CI deploy)
  #   2. ${ONOX_HOME}/.git/ varsa: git pull (mevcut kurulum güncelleme)
  #   3. get.onox.com.tr/paneltr.tar.gz erişilebilir: tarball indir + SHA256 doğrula (PRODUCTION)
  #   4. GitHub fallback: git clone (geliştirici/CI)
  if [[ -d /opt/onox-source ]]; then
    info "Yerel kaynak /opt/onox-source bulundu — kopyalanıyor"
    run cp -r /opt/onox-source/. "${ONOX_HOME}/"
  elif [[ -d "${ONOX_HOME}/.git" ]]; then
    info "Mevcut git repo bulundu — pull"
    run git -C "${ONOX_HOME}" pull origin main
  elif curl -fsI --max-time 5 "${ONOX_RELEASE_URL}" &>/dev/null; then
    info "Release tarball indiriliyor: ${ONOX_RELEASE_URL}"
    local tarball="/tmp/paneltr.tar.gz"
    local checksum_file="/tmp/paneltr.tar.gz.sha256"

    run curl -fsSL -o "${tarball}" "${ONOX_RELEASE_URL}"

    # SHA256 doğrulaması — release dosyasının yanında bir .sha256 dosyası bekleriz.
    # Yoksa uyar ama devam et (development relase'ler için).
    if curl -fsI --max-time 5 "${ONOX_RELEASE_SHA256_URL}" &>/dev/null; then
      run curl -fsSL -o "${checksum_file}" "${ONOX_RELEASE_SHA256_URL}"
      pushd /tmp > /dev/null
      if ! sha256sum -c "${checksum_file}" 2>/dev/null | grep -q "OK"; then
        die "Tarball SHA256 doğrulaması başarısız — release bozuk veya değiştirilmiş olabilir"
      fi
      info "SHA256 doğrulandı"
      popd > /dev/null
    else
      warn "SHA256 dosyası bulunamadı (${ONOX_RELEASE_SHA256_URL}) — integrity doğrulanmadan devam"
    fi

    info "Tarball çıkarılıyor: ${ONOX_HOME}"
    run tar -xzf "${tarball}" -C "${ONOX_HOME}" --strip-components=1
    rm -f "${tarball}" "${checksum_file}"
  else
    info "GitHub'dan klonlanıyor: ${ONOX_REPO}"
    run git clone "${ONOX_REPO}" "${ONOX_HOME}"
  fi

  # Tarball'dan extract edilmişse vendor/ ve public/build/ zaten dolu.
  # composer/npm gerçekten çalıştırmaya gerek yok — ama doğrulama için sessiz çalıştır.

  info "composer install (10dk timeout)"
  timeout 600 composer install \
    --no-dev --optimize-autoloader \
    --working-dir="${ONOX_HOME}" \
    --no-interaction \
    --no-progress \
    >> "$LOG" 2>&1 \
    && success "  composer hazır" \
    || { warn "composer install timeout/fail — vendor/ tarball'dan kullanılacak"; \
         [[ -f "${ONOX_HOME}/vendor/autoload.php" ]] || die "vendor/autoload.php yok ve composer install başarısız"; }

  # Vite build — node_modules tarball'da yok, public/build/ var. İhtiyaca göre.
  if [[ ! -f "${ONOX_HOME}/public/build/manifest.json" ]]; then
    info "npm install + build (10dk timeout)"
    timeout 300 npm install --prefix "${ONOX_HOME}" --silent --no-audit --no-fund >> "$LOG" 2>&1 \
      || { warn "npm install timeout/fail — public/build manifest yok!"; die "Vite build gerekli (panel UI çalışmaz)"; }
    timeout 300 npm run build --prefix "${ONOX_HOME}" >> "$LOG" 2>&1 \
      || die "npm run build başarısız — Vite manifest oluşturulamadı"
    success "  Vite build tamam"
  else
    info "Vite build mevcut (public/build/manifest.json) — atlanıyor"
  fi

  if [[ ! -f "${ONOX_HOME}/.env" ]]; then
    run cp "${ONOX_HOME}/.env.example" "${ONOX_HOME}/.env"
  fi

  grep -q '^APP_KEY=base64:' "${ONOX_HOME}/.env" 2>/dev/null \
    || run php "${ONOX_HOME}/artisan" key:generate --no-interaction

  local app_key
  app_key=$(grep '^APP_KEY=' "${ONOX_HOME}/.env" | cut -d= -f2-)

  cat > "${ONOX_HOME}/.env" <<EOF
APP_NAME=ONOXSOFT
APP_ENV=production
APP_KEY=${app_key}
APP_DEBUG=false
APP_TIMEZONE=Europe/Istanbul
APP_URL=https://${PANEL_HOSTNAME}:${PANEL_SSL_PORT}

APP_LOCALE=tr
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=tr_TR

LOG_CHANNEL=daily
LOG_LEVEL=warning

DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=${DB_DATABASE}
DB_USERNAME=${DB_USERNAME}
DB_PASSWORD=${DB_PASSWORD}

SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax

QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=onoxsoft

REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_FROM_ADDRESS="noreply@${PANEL_HOSTNAME}"
MAIL_FROM_NAME="ONOXSOFT"

ONOXSOFT_OS_FAMILY=${OS_FAMILY}
ONOXSOFT_DISTRO=${DISTRO}
ONOXSOFT_WEB_USER=${WEB_USER}
ONOXSOFT_WEB_SVC=${WEB_SVC}

# Panel portları — sabit, cPanel/WHM pattern. Müşteri 80/443 kalır.
ONOXSOFT_HOSTNAME=${PANEL_HOSTNAME}
ONOXSOFT_PANEL_HTTP_PORT=${PANEL_HTTP_PORT}
ONOXSOFT_PANEL_SSL_PORT=${PANEL_SSL_PORT}
EOF

  run chown -R "${WEB_USER}:${WEB_GROUP}" "${ONOX_HOME}/storage"
  run chown -R "${WEB_USER}:${WEB_GROUP}" "${ONOX_HOME}/bootstrap/cache"

  # ÖNEMLİ: Önceki kurulumdan bootstrap/cache/config.php kalmış olabilir,
  # eski DB credentials cached. Migrate öncesi temizle.
  info "Cache temizleniyor (eski config.php varsa)"
  rm -f "${ONOX_HOME}/bootstrap/cache/config.php" "${ONOX_HOME}/bootstrap/cache/routes-v7.php" 2>/dev/null
  try_run php "${ONOX_HOME}/artisan" config:clear || true
  try_run php "${ONOX_HOME}/artisan" cache:clear || true

  # DB connection sanity check (proper error gösterir, migrate'den önce)
  info "DB bağlantısı test ediliyor"
  if ! php "${ONOX_HOME}/artisan" db:show --database=mariadb 2>>"$LOG" >/dev/null; then
    warn "  db:show başarısız — daha ayrıntılı tanılama:"
    php "${ONOX_HOME}/artisan" tinker --execute='DB::connection()->getPdo();' 2>&1 | tee -a "$LOG" | tail -20
    die "DB bağlantısı kurulamadı — /opt/onoxsoft/.env DB_PASSWORD ile MariaDB user şifresi eşleşmiyor olabilir. db.env: ${ONOX_ETC}/db.env"
  fi
  success "  DB bağlantısı OK"

  info "Veritabanı migration (verbose — hata olursa görünür)"
  if ! php "${ONOX_HOME}/artisan" migrate --force --no-interaction 2>&1 | tee -a "$LOG"; then
    warn "Migration fail — son satırları yukarıda görürsün."
    warn "Manuel deneme: cd /opt/onoxsoft && php artisan migrate --force -vvv"
    die "Migration başarısız"
  fi

  info "Cache + config + route cache"
  run php "${ONOX_HOME}/artisan" config:cache
  run php "${ONOX_HOME}/artisan" route:cache
  run php "${ONOX_HOME}/artisan" view:cache

  success "Panel deploy edildi"
}

# ---------------------------------------------------------------------------
# STEP 13 — Apache vhost + PHP-FPM pool
# ---------------------------------------------------------------------------
step_setup_panel_vhost() {
  step "13/17  Apache vhost + PHP-FPM pool"

  local php_sock="${PHP_FPM_SOCK_DIR}/onoxsoft-panel.sock"

  info "PHP-FPM pool: ${PHP_FPM_POOL_DIR}/onoxsoft-panel.conf"
  cat > "${PHP_FPM_POOL_DIR}/onoxsoft-panel.conf" <<EOF
[onoxsoft-panel]
user = ${WEB_USER}
group = ${WEB_GROUP}
listen = ${php_sock}
listen.owner = ${WEB_USER}
listen.group = ${WEB_GROUP}
listen.mode = 0660

pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500

php_admin_value[error_log] = /var/log/onoxsoft/php-fpm-panel.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 60
php_admin_value[upload_max_filesize] = 100M
php_admin_value[post_max_size] = 100M
EOF

  run systemctl enable --now "${PHP_FPM_SVC}"
  run systemctl reload "${PHP_FPM_SVC}"

  # Apache vhost — RHEL ve Debian dosya yolları farklı
  local vhost_file
  if [[ "$OS_FAMILY" == "rhel" ]]; then
    vhost_file="${WEB_CONF_DIR}/onoxsoft-panel.conf"
  else
    vhost_file="${WEB_CONF_DIR}/onoxsoft-panel.conf"
  fi

  cat > "${vhost_file}" <<EOF
# ONOXSOFT Panel vhost — generated by install.sh v${SCRIPT_VERSION}
# Sabit panel portları: HTTP=${PANEL_HTTP_PORT}, SSL=${PANEL_SSL_PORT}
# (cPanel/WHM pattern — müşteri site'leri 80/443'te kalır, panel kendi portunda)

Listen ${PANEL_HTTP_PORT}

<VirtualHost *:${PANEL_HTTP_PORT}>
    ServerName ${PANEL_HOSTNAME}
    DocumentRoot ${ONOX_HOME}/public

    <Directory ${ONOX_HOME}/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <FilesMatch \.php\$>
        SetHandler "proxy:unix:${php_sock}|fcgi://localhost"
    </FilesMatch>

    ErrorLog  ${WEB_LOG_DIR}/onoxsoft-panel-error.log
    CustomLog ${WEB_LOG_DIR}/onoxsoft-panel-access.log combined
</VirtualHost>
EOF

  # firewalld — panel portunu aç (RHEL ailesi)
  if [[ "$OS_FAMILY" == "rhel" ]] && command -v firewall-cmd &>/dev/null; then
    run firewall-cmd --permanent --add-port="${PANEL_HTTP_PORT}/tcp"
    run firewall-cmd --reload
  fi
  # ufw — Debian/Ubuntu
  if [[ "$OS_FAMILY" == "debian" ]] && command -v ufw &>/dev/null; then
    run ufw allow "${PANEL_HTTP_PORT}/tcp"
  fi
  # SELinux — httpd port kabulü
  if command -v semanage &>/dev/null; then
    run semanage port -a -t http_port_t -p tcp "${PANEL_HTTP_PORT}" 2>/dev/null \
      || run semanage port -m -t http_port_t -p tcp "${PANEL_HTTP_PORT}" 2>/dev/null \
      || true
  fi

  # Debian: a2ensite ile aktifleştir, default site'ı kapat
  if [[ "$OS_FAMILY" == "debian" ]]; then
    run a2ensite onoxsoft-panel
    try_run a2dissite 000-default 2>/dev/null || true
  fi

  run systemctl enable --now "${WEB_SVC}"
  run systemctl reload "${WEB_SVC}"

  success "Apache vhost (HTTP) yapılandırıldı"
}

# ---------------------------------------------------------------------------
# STEP 14 — SSL via acme.sh
# ---------------------------------------------------------------------------
step_setup_panel_ssl() {
  step "14/17  SSL sertifikası"

  local ssl_dir="${ONOX_ETC}/ssl"
  run mkdir -p "${ssl_dir}"
  chmod 700 "${ssl_dir}"

  # IP-only kurulum: Let's Encrypt IP'ye cert vermez, self-signed üret.
  # Admin daha sonra FQDN ayarladığında "onoxsoft:panel:reissue-ssl" cron veya
  # Settings UI'dan gerçek LE cert alınabilir.
  if [[ "$INSTALL_MODE" == "ip-only" ]]; then
    warn "IP modunda kurulum — Let's Encrypt atlanıyor, self-signed cert üretiliyor"
    run openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
      -subj "/CN=${PANEL_HOSTNAME}" \
      -addext "subjectAltName=IP:${PANEL_HOSTNAME},DNS:localhost" \
      -keyout "${ssl_dir}/${PANEL_HOSTNAME}.key" \
      -out    "${ssl_dir}/${PANEL_HOSTNAME}.fullchain"
    cp "${ssl_dir}/${PANEL_HOSTNAME}.fullchain" "${ssl_dir}/${PANEL_HOSTNAME}.crt"
    chmod 600 "${ssl_dir}/${PANEL_HOSTNAME}.key"
    success "Self-signed sertifika oluşturuldu (tarayıcı uyarısı normal — FQDN sonradan ayarlandığında LE alınacak)"
  else
    local acme="${ONOX_ACME}/acme.sh"
    [[ -f "$acme" ]] || { warn "acme.sh bulunamadı, SSL atlanıyor"; return 0; }

    local resolved_ip
    resolved_ip=$(dig +short "${PANEL_HOSTNAME}" A 2>/dev/null | head -1 || true)
    if [[ -z "$resolved_ip" ]]; then
      warn "${PANEL_HOSTNAME} DNS'te çözümlenemiyor — self-signed cert üretiliyor"
      run openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
        -subj "/CN=${PANEL_HOSTNAME}" \
        -keyout "${ssl_dir}/${PANEL_HOSTNAME}.key" \
        -out    "${ssl_dir}/${PANEL_HOSTNAME}.fullchain"
      cp "${ssl_dir}/${PANEL_HOSTNAME}.fullchain" "${ssl_dir}/${PANEL_HOSTNAME}.crt"
      chmod 600 "${ssl_dir}/${PANEL_HOSTNAME}.key"
      warn "DNS hazır olunca LE almak için:"
      warn "  ${acme} --issue -d ${PANEL_HOSTNAME} --webroot ${ONOX_HOME}/public"
    else
      info "SSL sertifikası isteniyor: ${PANEL_HOSTNAME}"
      run "${acme}" --issue -d "${PANEL_HOSTNAME}" --webroot "${ONOX_HOME}/public" --force

      run "${acme}" --install-cert -d "${PANEL_HOSTNAME}" \
        --cert-file      "${ssl_dir}/${PANEL_HOSTNAME}.crt" \
        --key-file       "${ssl_dir}/${PANEL_HOSTNAME}.key" \
        --fullchain-file "${ssl_dir}/${PANEL_HOSTNAME}.fullchain" \
        --reloadcmd      "systemctl reload ${WEB_SVC}"

      chmod 600 "${ssl_dir}/${PANEL_HOSTNAME}.key"
    fi
  fi

  local php_sock="${PHP_FPM_SOCK_DIR}/onoxsoft-panel.sock"
  local vhost_file="${WEB_CONF_DIR}/onoxsoft-panel.conf"

  cat >> "${vhost_file}" <<EOF

Listen ${PANEL_SSL_PORT} https

<VirtualHost *:${PANEL_SSL_PORT}>
    ServerName ${PANEL_HOSTNAME}
    DocumentRoot ${ONOX_HOME}/public

    SSLEngine on
    SSLCertificateFile    ${ssl_dir}/${PANEL_HOSTNAME}.fullchain
    SSLCertificateKeyFile ${ssl_dir}/${PANEL_HOSTNAME}.key

    <Directory ${ONOX_HOME}/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    <FilesMatch \.php\$>
        SetHandler "proxy:unix:${php_sock}|fcgi://localhost"
    </FilesMatch>

    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-Content-Type-Options "nosniff"

    ErrorLog  ${WEB_LOG_DIR}/onoxsoft-panel-ssl-error.log
    CustomLog ${WEB_LOG_DIR}/onoxsoft-panel-ssl-access.log combined
</VirtualHost>
EOF

  # firewalld + ufw + SELinux — SSL port da açılmalı
  if [[ "$OS_FAMILY" == "rhel" ]] && command -v firewall-cmd &>/dev/null; then
    run firewall-cmd --permanent --add-port="${PANEL_SSL_PORT}/tcp"
    run firewall-cmd --reload
  fi
  if [[ "$OS_FAMILY" == "debian" ]] && command -v ufw &>/dev/null; then
    run ufw allow "${PANEL_SSL_PORT}/tcp"
  fi
  if command -v semanage &>/dev/null; then
    run semanage port -a -t http_port_t -p tcp "${PANEL_SSL_PORT}" 2>/dev/null \
      || run semanage port -m -t http_port_t -p tcp "${PANEL_SSL_PORT}" 2>/dev/null \
      || true
  fi

  # HTTP → HTTPS redirect ekle
  sed -i '/<\/VirtualHost>/i\    # HTTP -> HTTPS\n    RewriteEngine On\n    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]' \
    "${vhost_file}" 2>/dev/null || true

  run systemctl reload "${WEB_SVC}"

  # Cron renew
  cat > /etc/cron.d/onoxsoft-acme <<EOF
# acme.sh auto-renew — ONOXSOFT
0 3 * * * root ${acme} --cron --home ${ONOX_ACME} >> /var/log/onoxsoft/acme-renew.log 2>&1
EOF

  success "SSL sertifikası kuruldu"
}

# ---------------------------------------------------------------------------
# STEP 15 — Sysapi script kurulumu
# ---------------------------------------------------------------------------
step_install_sysapi_scripts() {
  step "15/17  Sysapi script kurulumu"

  local src=""
  if [[ -d "${ONOX_HOME}/scripts/sysapi" ]]; then
    src="${ONOX_HOME}/scripts/sysapi"
  elif [[ -d "$(dirname "$0")/scripts/sysapi" ]]; then
    src="$(cd "$(dirname "$0")/scripts/sysapi" && pwd)"
  fi

  if [[ -z "$src" ]]; then
    warn "scripts/sysapi dizini bulunamadı. Sysapi script'leri elle kurun:"
    warn "  cp scripts/sysapi/onx-* ${ONOX_BIN}/ && chmod 750 ${ONOX_BIN}/onx-*"
    return 0
  fi

  local count=0
  for script in "${src}"/onx-*; do
    [[ -f "$script" ]] || continue
    cp "${script}" "${ONOX_BIN}/"
    chmod 750 "${ONOX_BIN}/$(basename "$script")"
    (( count++ ))
  done

  # _lib klasörü de kopyala (varsa) — script'ler shared helper'lar kullanıyor olabilir
  if [[ -d "${src}/_lib" ]]; then
    cp -r "${src}/_lib" "${ONOX_BIN}/"
  fi

  chown -R "root:${WEB_GROUP}" "${ONOX_BIN}" 2>/dev/null \
    || chown -R root:root "${ONOX_BIN}"

  success "Sysapi script'leri kuruldu: ${count} dosya"

  # ─── Autodiscover endpoint kurulumu ──────────────────────────────────
  # mail/autodiscover/autoconfig system subdomain'leri /usr/local/onoxsoft/autodiscover/
  # altındaki XML endpoint script'lerine vhost via RewriteRule yönlendirir.
  local autodiscover_src=""
  if [[ -d "${ONOX_HOME}/scripts/autodiscover" ]]; then
    autodiscover_src="${ONOX_HOME}/scripts/autodiscover"
  elif [[ -d "$(dirname "$0")/scripts/autodiscover" ]]; then
    autodiscover_src="$(cd "$(dirname "$0")/scripts/autodiscover" && pwd)"
  fi

  if [[ -n "$autodiscover_src" && -f "${autodiscover_src}/install.sh" ]]; then
    bash "${autodiscover_src}/install.sh" \
      || warn "Autodiscover endpoint kurulumu başarısız (devam ediliyor)"
  fi
}

# ---------------------------------------------------------------------------
# STEP 16 — Sudoers
# Web user (apache veya www-data) onx-* script'lerini şifresiz çalıştırabilir
# ---------------------------------------------------------------------------
step_install_sudoers() {
  step "16/17  Sudoers (sysapi yetkilendirme)"

  # ─── ONOX_BIN dizini sertleştirme — KRİTİK ────────────────────────────────
  # Eğer ${ONOX_BIN} dizinine web user (apache/www-data) yazabilirse, oraya
  # yeni "onx-evil" script bırakıp sudo ile root olarak çalıştırabilir → LPE.
  # Bu yüzden DİZİN root:root + 755 olmalı, içindeki SCRIPT'ler root:root + 750.
  # Tek sudoers wildcard'ı ${ONOX_BIN}/onx-* — dosya yaratma izni olan kimse
  # bu wildcard'a yeni dosya ekleyebilir.

  if [[ ! -d "${ONOX_BIN}" ]]; then
    mkdir -p "${ONOX_BIN}"
  fi

  chown root:root "${ONOX_BIN}"
  chmod 755 "${ONOX_BIN}"

  # Dizinde mevcut onx-* dosyalarını da güvenli mode'a çek (root:root, 750)
  if compgen -G "${ONOX_BIN}/onx-*" > /dev/null; then
    chown root:root "${ONOX_BIN}"/onx-* 2>/dev/null || true
    chmod 750 "${ONOX_BIN}"/onx-* 2>/dev/null || true
  fi

  # _lib subdir varsa (helper scripts) — apache okuyabilsin ama yazamasın
  if [[ -d "${ONOX_BIN}/_lib" ]]; then
    chown -R root:root "${ONOX_BIN}/_lib"
    chmod 755 "${ONOX_BIN}/_lib"
    chmod 644 "${ONOX_BIN}/_lib"/*.sh 2>/dev/null || true
  fi

  info "ONOX_BIN sertleştirildi: $(stat -c '%U:%G %a' "${ONOX_BIN}") ${ONOX_BIN}"

  # ─── /etc/sudoers.d/onoxsoft ──────────────────────────────────────────────
  cat > /etc/sudoers.d/onoxsoft <<EOF
# ONOX Panel — sysapi script yetkilendirmesi
# Otomatik üretildi: install.sh v${SCRIPT_VERSION}
# OS: ${DISTRO} ${DISTRO_VER} (${OS_FAMILY})

# Web worker (${WEB_USER}) bu script'leri root olarak, şifresiz çalıştırabiliyor.
# GÜVENLİK: ${ONOX_BIN} dizini root:root + 0755, içindeki script'ler 0750.
# Web user dizine YAZAMAZ; sadece sudo ile mevcut whitelist'tekileri çalıştırır.
# Yeni script eklenmek istenirse install.sh tekrar koşulmalı (root olarak).
${WEB_USER} ALL=(root) NOPASSWD: ${ONOX_BIN}/onx-*

# Güvenlik: tty zorunluluğu kaldırıldı; I/O kayıt altına alınsın
Defaults!${ONOX_BIN}/onx-* !requiretty
Defaults!${ONOX_BIN}/onx-* log_input, log_output

# Secure path — sudo PATH'ini kısıtla, ${WEB_USER}'ın PATH manipülasyonu
# script içinde sudo çağrılarını yanıltamasın
Defaults!${ONOX_BIN}/onx-* secure_path = /sbin:/bin:/usr/sbin:/usr/bin:${ONOX_BIN}

# Env reset — sysapi script'ler temiz environment'ta başlasın
Defaults!${ONOX_BIN}/onx-* env_reset
Defaults!${ONOX_BIN}/onx-* env_keep += "LANG LC_ALL LC_CTYPE"
EOF

  chmod 440 /etc/sudoers.d/onoxsoft
  chown root:root /etc/sudoers.d/onoxsoft

  visudo -c -f /etc/sudoers.d/onoxsoft \
    || die "sudoers geçersiz: /etc/sudoers.d/onoxsoft — kurulum başarısız!"

  # ─── Verify: ${WEB_USER} dizine yazamadığını doğrula ──────────────────────
  if sudo -u "${WEB_USER}" test -w "${ONOX_BIN}" 2>/dev/null; then
    warn "GÜVENLİK UYARISI: ${WEB_USER} kullanıcısı ${ONOX_BIN} dizinine yazabiliyor!"
    warn "  → LPE riski var. install.sh root olarak çalıştırılmalı."
    warn "  → chown root:root ${ONOX_BIN} && chmod 755 ${ONOX_BIN} manuel çalıştırın."
  else
    success "sudoers yapılandırıldı (+ ${ONOX_BIN} root:root 0755 doğrulandı)"
  fi
}

# ---------------------------------------------------------------------------
# STEP 16.5 — fail2ban default jails
# Onoxsoft varsayılan jail'ları: sshd, pure-ftpd, panel-login, recidive
# ---------------------------------------------------------------------------
step_install_fail2ban_defaults() {
  step "16.5/17  fail2ban default jails"

  if ! command -v fail2ban-client &>/dev/null; then
    warn "fail2ban-client bulunamadı — varsayılan jail kurulumu atlanıyor"
    return 0
  fi

  local f2b_dir=/etc/fail2ban/jail.d
  run mkdir -p "${f2b_dir}"

  # Backend seçimi: firewalld varsa fail2ban-firewalld, yoksa varsayılan
  local f2b_backend="auto"

  # panel-login filter (Laravel access log üzerinden)
  if [[ ! -f /etc/fail2ban/filter.d/onoxsoft-panel-login.conf ]]; then
    cat > /etc/fail2ban/filter.d/onoxsoft-panel-login.conf <<'EOF'
# Onoxsoft panel-login filter
# Laravel auth.failed log satırlarını eşleştirir
[Definition]
failregex = ^.*\[ONOX-AUTH\] failed login.*ip=<HOST>.*$
ignoreregex =
EOF
  fi

  # /etc/fail2ban/jail.d/onoxsoft-defaults.local — tek dosyada hepsi
  cat > "${f2b_dir}/onoxsoft-defaults.local" <<EOF
# Onoxsoft managed — DO NOT EDIT MANUALLY
# Yazıldı: $(date -u +"%Y-%m-%dT%H:%M:%SZ")

[DEFAULT]
banaction = ${f2b_backend}
bantime   = 3600
findtime  = 600
maxretry  = 5

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/secure
          /var/log/auth.log
maxretry = 3
bantime  = 7200

[pure-ftpd]
enabled  = true
port     = ftp,ftp-data,ftps,ftps-data
filter   = pure-ftpd
logpath  = /var/log/pureftpd.log
          /var/log/messages
maxretry = 5
bantime  = 1800

[panel-login]
enabled  = true
port     = http,https
filter   = onoxsoft-panel-login
logpath  = ${ONOX_HOME}/storage/logs/laravel.log
maxretry = 5
findtime = 300
bantime  = 1800

[recidive]
enabled  = true
filter   = recidive
logpath  = /var/log/fail2ban.log
bantime  = 604800
findtime = 86400
maxretry = 3
EOF

  chmod 0644 "${f2b_dir}/onoxsoft-defaults.local"

  # Start / enable & reload
  run systemctl enable fail2ban
  try_run systemctl restart fail2ban || warn "fail2ban restart sonrası status: $(systemctl is-active fail2ban 2>&1)"

  if systemctl is-active --quiet fail2ban; then
    fail2ban-client reload 2>/dev/null || warn "fail2ban-client reload başarısız"
    success "fail2ban: 4 varsayılan jail kuruldu (sshd, pure-ftpd, panel-login, recidive)"
  else
    warn "fail2ban servisi aktif değil"
  fi
}

# ---------------------------------------------------------------------------
# STEP 16.6 — Firewall infra init (nftables base + sets + chains)
# onx-firewall-init: `inet onox` tablosu, trusted/threat/country/ratelimit chain'leri,
# /var/lib/onox/ dizini, panel-login + panel-api fail2ban filter'ları.
# ---------------------------------------------------------------------------
step_init_firewall_infra() {
  step "16.6/17 Firewall altyapısı (nftables base + onx- sets)"

  if [[ ! -x "${ONOX_BIN}/onx-firewall-init" ]]; then
    warn "onx-firewall-init bulunamadı — atlanıyor"
    return 0
  fi

  # firewalld aktifse uyar: firewalld kendi tablosunu yönetir, onox kendi tablosunu.
  # İkisi paralel çalışabilir ama yönetici fark etmeli.
  if systemctl is-active --quiet firewalld 2>/dev/null; then
    info "firewalld aktif — onox tablosu paralel çalışacak (priority -100..-5 aralığı kullanıyor)"
  fi

  if echo '{}' | "${ONOX_BIN}/onx-firewall-init" > /tmp/onox-fw-init.json 2>&1; then
    local warnings
    warnings=$(jq -r '.warnings | length' /tmp/onox-fw-init.json 2>/dev/null || echo 0)
    if (( warnings > 0 )); then
      warn "firewall-init: ${warnings} uyarı (eksik soft-dep) — /tmp/onox-fw-init.json"
    fi
    success "Firewall altyapısı hazır (inet onox table + trusted/threat/country/ratelimit chains)"
  else
    warn "onx-firewall-init başarısız — manuel çalıştırın:  ${ONOX_BIN}/onx-firewall-init"
  fi

  # fail2ban reload — yeni panel-login + panel-api filter'lar için
  if command -v fail2ban-client &>/dev/null; then
    fail2ban-client reload &>/dev/null || true
  fi
}

# ---------------------------------------------------------------------------
# STEP 17 — Admin kullanıcı + özet
# ---------------------------------------------------------------------------
step_create_admin_user() {
  step "17/17  Admin kullanıcısı"

  # Geçici şifre üret — kullanıcı ilk login'de değiştirir
  ADMIN_TEMP_PASS=$(openssl rand -base64 18 | tr -d '/+=' | head -c 16)

  if php "${ONOX_HOME}/artisan" onox:admin:create \
      --email="${ADMIN_EMAIL}" \
      --password="${ADMIN_TEMP_PASS}" \
      --no-interaction 2>/dev/null; then
    success "Admin kullanıcısı oluşturuldu: ${ADMIN_EMAIL}"
  else
    warn "onox:admin:create başarısız/bulunamadı."
    warn "Elle oluşturun: php ${ONOX_HOME}/artisan onox:admin:create --email=${ADMIN_EMAIL}"
    ADMIN_TEMP_PASS=""
  fi
}

step_print_summary() {
  local pub_ip
  pub_ip=$(curl -4 -fsS --max-time 5 https://ipv4.icanhazip.com 2>/dev/null || echo "SERVER_IP")

  local panel_url="https://${PANEL_HOSTNAME}:${PANEL_SSL_PORT}"
  local pass_line="${ADMIN_TEMP_PASS:-<artisan ile manuel oluşturun>}"

  echo "" | tee -a "$LOG"
  echo -e "${GREEN}${UI_TL}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_TR}${NC}" | tee -a "$LOG"
  echo -e "${GREEN}${UI_V}${NC}  ${WHITE}${BOLD}${UI_STAR} ONOXSOFT Panel Kuruldu — v${SCRIPT_VERSION}${NC}                          ${GREEN}${UI_V}${NC}" | tee -a "$LOG"
  echo -e "${GREEN}${UI_BL}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_H}${UI_BR}${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"
  echo -e "  ${BLUE}${UI_ARROW}${NC}  ${BOLD}Erişim${NC}" | tee -a "$LOG"
  echo -e "      Panel URL  : ${CYAN}${BOLD}${panel_url}${NC}" | tee -a "$LOG"
  echo -e "      HTTP →     : ${GREY}http://${PANEL_HOSTNAME}:${PANEL_HTTP_PORT}${NC} ${GREY}(SSL'e redirect)${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"
  echo -e "  ${BLUE}${UI_ARROW}${NC}  ${BOLD}Admin${NC}" | tee -a "$LOG"
  echo -e "      E-posta    : ${YELLOW}${ADMIN_EMAIL}${NC}" | tee -a "$LOG"
  echo -e "      Şifre      : ${YELLOW}${BOLD}${pass_line}${NC}  ${RED}${BOLD}← İlk girişten sonra değiştir${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"
  echo -e "  ${BLUE}${UI_ARROW}${NC}  ${BOLD}Sistem${NC}" | tee -a "$LOG"
  echo -e "      OS         : ${DISTRO} ${DISTRO_VER} ${GREY}(${OS_FAMILY})${NC}" | tee -a "$LOG"
  echo -e "      Sunucu IP  : ${pub_ip}" | tee -a "$LOG"
  echo -e "      MariaDB    : ${GREY}/root/.onox-mysql-root.cnf${NC}" | tee -a "$LOG"
  echo -e "      DB env     : ${GREY}${ONOX_ETC}/db.env${NC}" | tee -a "$LOG"
  echo -e "      Log        : ${GREY}${LOG}${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"
  echo -e "  ${YELLOW}${UI_STAR}${NC}  ${BOLD}Sonraki adımlar${NC}" | tee -a "$LOG"
  if [[ "$INSTALL_MODE" == "ip-only" ]]; then
    echo -e "      1. Panel'e gir: ${CYAN}${panel_url}${NC}" | tee -a "$LOG"
    echo -e "         ${GREY}(Self-signed SSL — tarayıcı uyarısını kabul et)${NC}" | tee -a "$LOG"
    echo -e "      2. Şifreyi değiştir + 2FA kur" | tee -a "$LOG"
    echo -e "      3. ${YELLOW}Hostname ayarla:${NC} Settings → Sistem → Hostname" | tee -a "$LOG"
    echo -e "         FQDN girince Let's Encrypt sertifikası otomatik alınır" | tee -a "$LOG"
  else
    echo -e "      1. DNS A kaydı oluştur: ${BOLD}${PANEL_HOSTNAME} → ${pub_ip}${NC}" | tee -a "$LOG"
    echo -e "      2. Panel'e gir: ${CYAN}${panel_url}${NC}" | tee -a "$LOG"
    echo -e "      3. Şifreyi değiştir + 2FA kur" | tee -a "$LOG"
  fi
  echo -e "      4. Lisans anahtarını gir: ${GREY}Settings → Lisans${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"
  echo -e "  ${MAGENTA}${BOLD}  onox.com.tr${NC} ${GREY}— Türkiye'nin yerli hosting kontrol paneli${NC}" | tee -a "$LOG"
  echo "" | tee -a "$LOG"

  # Whiptail varsa şifreyi dialog'da da göster (daha okunaklı, screenshot için)
  if [[ $HAS_WHIPTAIL -eq 1 && -t 0 ]]; then
    ui_msg "Kurulum başarıyla tamamlandı!\n\nPanel URL : ${panel_url}\nE-posta   : ${ADMIN_EMAIL}\nŞifre     : ${pass_line}\n\nİlk girişten sonra şifreni değiştir ve 2FA aktif et.\n\nLog: ${LOG}" \
      "ONOXSOFT - Kurulum Tamamlandı ${UI_STAR}"
  fi
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
  parse_args "$@"

  mkdir -p "$(dirname "$LOG")"
  : > "$LOG"
  echo "# ONOX install.sh v${SCRIPT_VERSION} — $(date --iso-8601=seconds)" >> "$LOG"

  # Görsel banner + UI başlat (whiptail varsa)
  show_banner
  ui_init

  # İnteraktif moddaysa kullanıcıya kuruluma başlamadan onay sor
  if [[ -t 0 && $HAS_WHIPTAIL -eq 1 ]] && [[ -z "$ADMIN_EMAIL" || -z "$PANEL_HOSTNAME" ]]; then
    ui_yesno "ONOXSOFT Panel kurulumuna başlamak istiyor musun?\n\n• ~10 dakika sürer\n• Apache + MariaDB + PHP + PowerDNS + Redis\n• Panel adresi: https://hostname:${PANEL_SSL_PORT}\n\nDevam edelim mi?" \
      "ONOXSOFT Installer - Hoşgeldin" \
      || die "Kurulum kullanıcı tarafından iptal edildi"
  fi

  info "ONOX Panel Master Installer v${SCRIPT_VERSION}"
  info "Log: ${LOG}"

  [[ "$SKIP_PREFLIGHT" -eq 0 ]] && step_preflight \
    || warn "Preflight atlandı (--skip-preflight)"

  # OS algılanmazsa diğer adımlar değişken hatası verir — preflight skip edildiyse manuel detect
  [[ -z "$OS_FAMILY" ]] && detect_os

  step_prompt_admin
  step_install_repos
  step_install_packages
  step_configure_firewall
  step_configure_selinux
  step_create_onoxsoft_user
  step_install_acme_sh
  step_setup_mariadb
  step_setup_powerdns
  step_setup_redis
  step_configure_mail_stack
  step_clone_panel
  step_setup_panel_vhost
  step_setup_panel_ssl
  step_install_sysapi_scripts
  step_install_sudoers
  step_install_fail2ban_defaults
  step_init_firewall_infra
  step_create_admin_user
  step_print_summary
}

main "$@"
