#!/usr/bin/env bash
#
# ONOXSOFT Panel Master Installer (Phase 1)
#
# Desteklenen OS aileleri:
#   RHEL family : AlmaLinux 9/10, Rocky Linux 9/10, RHEL 9/10, CentOS Stream 9/10, Oracle Linux 9/10
#   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.1"
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
OS_MAJOR=""            # RHEL ailesi major: "9" | "10" — repo URL türetimi (Remi/EPEL/Rspamd)
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} Gelişmiş Kontrol Panel  ${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=$((i + 1))
    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/10 / Rocky 9/10 / RHEL 9/10 / CentOS Stream 9/10 / Oracle Linux 9/10
  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)
  --enable-antivirus        ClamAV kurulumunu zorunlu hale getir (varsayilan 1'dir)
  --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 ;;
      --enable-antivirus) ENABLE_ANTIVIRUS=1;  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|10)$ ]] || die "RHEL ailesi yalnızca v9/v10 destekleniyor — tespit: ${DISTRO} ${DISTRO_VER}"
      OS_FAMILY="rhel"
      OS_MAJOR="$major"          # 9|10 — repo URL'leri (Remi/EPEL/Rspamd) buradan türetilir
      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"
      # el9: 7.4/8.1 hâlâ Remi'de + yaygın legacy app → kur. el10: 7.4/8.1 EOL +
      # libxcrypt bağımlılığı sorunlu → kurulu set 8.2+ (Remi el10 82/83/84 sağlar).
      if [[ "$major" == "10" ]]; then
        PHP_VERSIONS=("82" "83" "84")
      else
        PHP_VERSIONS=("74" "81" "82" "83")
      fi
      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"
      # el10: RHEL 10 'redis'i kaldırdı → 'valkey' (Redis çatallaması, 6379'da protokol-uyumlu;
      # panel REDIS_* config'i aynen çalışır). Paket + servis + conf yolu valkey olur.
      if [[ "$major" == "10" ]]; then
        REDIS_PKG="valkey"
        REDIS_SVC="valkey"
      fi

      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-10, 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
    # el10: Remi/EPEL bazı bağımlılıkları CRB (CodeReady Builder) reposundan çeker.
    # el9'da gerekmiyor → davranışı değiştirmemek için SADECE el10'da etkinleştir.
    if [[ "$OS_MAJOR" == "10" ]]; then
      rpm -q dnf-plugins-core >/dev/null 2>&1 || pkg_install dnf-plugins-core || true
      dnf config-manager --set-enabled crb >/dev/null 2>&1 || true
    fi

    # Re-run güvenliği: önceki başarısız denemeden kalan pdns.repo dnf'i KİLİTLER
    # (el10'da auth-49 404 → EPEL install bile patlar, çünkü dnf TÜM repo metadata'sını
    # tazeler). Stale PowerDNS repo'yu temizle; ileride (aşağıda) doğru sürümle yeniden yazılır.
    rm -f /etc/yum.repos.d/pdns.repo

    info "EPEL ${OS_MAJOR}"
    pkg_install epel-release

    info "Remi PHP repo (el${OS_MAJOR})"
    pkg_install "https://rpms.remirepo.net/enterprise/remi-release-${OS_MAJOR}.rpm"

    # PowerDNS auth sürümü OS'a göre: el8/el9 → auth-49 (4.9); el10 → auth-50 (5.0).
    # PowerDNS 4.9'un el10 PAKETİ YOK — repo.powerdns.com/el/x86_64/10/ altında SADECE
    # auth-50 + auth-51 var (auth-49 yok) → eski kod el10'da "404 repomd.xml" verip
    # dnf update'i patlatıyordu. (2025'te repo dosya adı el-pdns-49.repo → el-auth-49.repo oldu.)
    if [[ "$OS_MAJOR" == "10" ]]; then
      PDNS_REPO_VER="auth-50"
    else
      PDNS_REPO_VER="auth-49"
    fi
    info "PowerDNS ${PDNS_REPO_VER} repo (el${OS_MAJOR})"
    # Resmi repo dosyasını dene (doğru baseurl'i içerir); erişilemezse DOĞRU baseurl ile manuel
    # oluştur — eski fallback /repo-files/<arch>/auth-49/ YANLIŞ path'ti (gerçek path
    # /el/<arch>/<releasever>/<ver>/), o yüzden fallback'e düşülse de repo bozuk oluyordu.
    if ! curl -fsSL "https://repo.powerdns.com/repo-files/el-${PDNS_REPO_VER}.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-${PDNS_REPO_VER}]
name=PowerDNS Authoritative Server - \$basearch
baseurl=https://repo.powerdns.com/el/\$basearch/\$releasever/${PDNS_REPO_VER}/
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

  # PHP CLI 'php' garantisi: Remi versiyonlu binary kurar (php82/83/84). el10'da 'php'
  # (unversioned) alternatives KURULMAZ → composer 'php -r hash_file' çağırınca komut bulunamaz,
  # hash BOŞ döner, SHA-384 "uyuşmuyor" sanılıp kurulum durur. Sonraki 'php artisan' çağrıları da
  # patlardı. PHP_CLI_PATH'e symlink garantile (yoksa) — composer + tüm php CLI çağrıları çalışır.
  command -v php >/dev/null 2>&1 || ln -sf "${PHP_CLI_PATH}" /usr/bin/php

  # 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
    # MED-16: resmi SHA-384 doğrulaması (installer integrity — supply-chain). Beklenen hash
    # composer tarafından yayınlanır; eşleşmezse FAIL-CLOSED. Sig URL geçici erişilemezse
    # uyar + devam (tek-seferlik installer'ı anlık ağ hatasıyla bloklama).
    COMPOSER_SIG="$(curl -fsSL https://composer.github.io/installer.sig 2>/dev/null || true)"
    if [[ -n "$COMPOSER_SIG" ]]; then
      COMPOSER_ACTUAL="$(php -r "echo hash_file('sha384','/tmp/composer-setup.php');" 2>/dev/null || true)"
      if [[ "$COMPOSER_SIG" != "$COMPOSER_ACTUAL" ]]; then
        rm -f /tmp/composer-setup.php
        die "Composer installer SHA-384 uyuşmuyor (supply-chain riski) — kurulum durduruldu."
      fi
      info "  composer installer SHA-384 doğrulandı"
    else
      warn "  composer installer.sig alınamadı — checksum atlandı (geçici ağ?)"
    fi
    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)"
  # el10: mod_security_crs (OWASP CRS) paketlenmemiş → kritik listeden ÇIKAR ki tüm install
  # bloke olmasın. modsec MOTORU kurulur; CRS kuralları el10'da ayrı kaynaktan gelir (follow-up).
  local _modsec_crs="mod_security_crs"
  [[ "$OS_MAJOR" == "10" ]] && _modsec_crs=""
  pkg_install \
    httpd mod_ssl mod_security ${_modsec_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"

  # el10: modern glibc/crypt — lsphp/OLS + bazı PHP/araçlar libxcrypt-compat ister (el9'da gereksiz).
  [[ "$OS_MAJOR" == "10" ]] && pkg_install libxcrypt-compat

  # Backup offsite destination CLI'leri — fresh kurulumda backup hedefleri (Local/S3/B2/Wasabi/
  # SFTP/FTP/Restic/Google Drive) kutudan çıkar çıkmaz çalışsın:
  #   rclone=Google Drive + S3-prune · aws(awscli2)=S3/B2/Wasabi · lftp=FTP · restic=Restic
  #   rsync+sshpass=SFTP. Eksikse ilgili hedef "araç kurulu değil" ile fail eder (kritik değil).
  info "Backup destination araçları (rclone/aws/lftp/restic/rsync/sshpass)"
  pkg_install lftp awscli2 rsync
  pkg_install --enablerepo=epel restic rclone sshpass \
    || warn "Bazı backup-dest araçları kurulamadı (EPEL erişimi) — ilgili offsite hedefler kısıtlı olur"

  local PHP_EXTS="mysqlnd opcache gd curl mbstring zip xml intl bcmath json process sodium"
  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-${OS_MAJOR}/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

  # ── Roundcube config.inc.php (CRITICAL — yoksa "CONFIGURATION ERROR" ─────
  # Paket /etc/roundcubemail/ dizinine .sample dosyaları koyar ama config.inc.php
  # YAZMAZ. config.inc.php yoksa Roundcube hiç açılmaz, "config.inc.php was not
  # found" hatası verir. ONOXSOFT default config'i burada yaz.
  local des_key
  des_key="$(openssl rand -hex 12)aaaa"  # Roundcube 24-char DES key bekler
  info "Roundcube config.inc.php yazılıyor (mynetworks bypass — NO AUTH, NO TLS)"
  # KRİTİK: smtp_host = 'localhost' (port AYRI parametre) + smtp_auth_type=''
  # Eğer smtp_auth_type='PLAIN' verilirse Roundcube AUTH zorluyor → Postfix
  # submission/587'ye düşüyor → STARTTLS 530. Production'da 19 Mayıs 2026'da
  # bu sorunu fixledik. Multi-line array (smtp_conn_options) artık YOK çünkü
  # sed silindiğinde orphan kalıyor — boş array literal kullanıyoruz.
  local helo_host
  helo_host="$(hostname -f 2>/dev/null || echo "${PANEL_FQDN:-panel.localhost}")"
  cat > /etc/roundcubemail/config.inc.php <<RCEOF
<?php
\$config = [];

// Database
\$config['db_dsnw'] = 'mysql://roundcube:${rcube_pass}@localhost/roundcubemail';

// IMAP — localhost loopback (TLS skip, trusted private net)
\$config['imap_host'] = 'localhost:143';
\$config['imap_auth_type'] = 'PLAIN';
\$config['imap_conn_options'] = null;

// SMTP — Postfix port 25 over mynetworks (127.0.0.1) → NO AUTH, NO TLS
// smtp_host MUST be 'localhost' (NOT 'localhost:25' — Roundcube 1.5 parse bug)
// smtp_auth_type MUST be '' (boş string — PLAIN dersek AUTH zorlanır)
\$config['smtp_host']         = 'localhost';
\$config['smtp_port']         = 25;
\$config['smtp_user']         = '';
\$config['smtp_pass']         = '';
\$config['smtp_auth_type']    = '';
\$config['smtp_helo_host']    = '${helo_host}';
\$config['smtp_conn_options'] = [];
\$config['smtp_timeout']      = 30;

// Genel
\$config['support_url']        = 'https://onoxsoft.com.tr/support';
\$config['product_name']       = 'ONOXSOFT Webmail';
\$config['des_key']            = '${des_key}';
\$config['plugins']            = ['onoxsoft_sso', 'autologon', 'archive', 'zipdownload', 'managesieve'];
\$config['default_charset']    = 'UTF-8';
\$config['skin']               = 'elastic';
\$config['language']           = 'tr_TR';
\$config['mail_domain']        = '%d';
\$config['mime_param_folding'] = 0;
\$config['session_lifetime']   = 30;
\$config['enable_installer']   = false;

// Autologon plugin (panel SSO için)
\$config['autologon_username'] = '';
\$config['autologon_password'] = '';
RCEOF
  # v79 MULTI-WEBSERVER PERMISSIONS: chgrp `webserver` (Apache+OLS+Nginx+Caddy ortak group)
  # Apache mode'da PHP-FPM apache user → primary group=apache + supplementary=webserver → reads via group
  # OLS mode'da LSAPI extprocessor extUser=apache extGroup=webserver → effective group=webserver
  # `root:apache` 640 OLS'da okumaz (effective group=webserver, dosya group=apache).
  # `root:webserver` 640 her iki mode'da çalışır (apache user webserver group üyesi).
  if ! getent group webserver >/dev/null 2>&1; then
    run groupadd -r webserver
  fi
  if ! id -nG apache 2>/dev/null | grep -qw webserver; then
    run usermod -a -G webserver apache
  fi
  run chown -R root:webserver /etc/roundcubemail/
  run chmod -R g+rX /etc/roundcubemail/

  # Roundcube temp + logs (apache user yazsın, webserver group ortak)
  run mkdir -p /usr/share/roundcubemail/temp /usr/share/roundcubemail/logs /var/log/roundcubemail /var/lib/roundcubemail/temp
  run chown -R apache:webserver /usr/share/roundcubemail/temp /usr/share/roundcubemail/logs /var/log/roundcubemail /var/lib/roundcubemail
  run chmod -R 770 /usr/share/roundcubemail/temp /usr/share/roundcubemail/logs /var/log/roundcubemail /var/lib/roundcubemail

  # v79 RHEL Roundcube paket bug fix: /usr/share/roundcubemail/config yok (RPM kurulumda
  # eksik). Sembolik link kur — eğer aktif web server OLS ise sembolik link restrained
  # mode'da çalışır (zaten restrained 0 yapıyoruz vhost-add-ols template'inde).
  if [[ ! -e /usr/share/roundcubemail/config ]]; then
    run ln -sf /etc/roundcubemail /usr/share/roundcubemail/config
  fi

  # ── Apache webmail.conf — PHP handler ile (CRITICAL — yoksa 503) ─────────
  # Paket /etc/httpd/conf.d/roundcubemail.conf yazar AMA <FilesMatch \.php$>
  # SetHandler proxy:unix:... eklemez. PHP-FPM panel pool socket'i ile
  # Roundcube PHP'i çalıştırılır. Aksi takdirde Apache 503 döner.
  info "Webmail Apache config (PHP-FPM handler)"
  local panel_sock="/var/opt/remi/php82/run/php-fpm/onoxsoft-panel.sock"
  # Fallback — default location
  [[ ! -S "${panel_sock}" ]] && panel_sock="/run/php-fpm/onoxsoft-panel.sock"

  cat > /etc/httpd/conf.d/webmail.conf <<WEBMAILEOF
# ONOXSOFT — Roundcube webmail Apache config
Alias /webmail /usr/share/roundcubemail

<Directory /usr/share/roundcubemail>
    Options +FollowSymLinks
    AllowOverride All
    Require all granted
    <IfModule mod_dir.c>
        DirectoryIndex index.php
    </IfModule>

    # Roundcube PHP — panel FPM pool ile çalıştır (paket bunu eklemiyor)
    <FilesMatch \.php\$>
        SetHandler "proxy:unix:${panel_sock}|fcgi://localhost"
    </FilesMatch>
</Directory>

# Sensitive dirs block
<Directory /usr/share/roundcubemail/config>
    Require all denied
</Directory>
<Directory /usr/share/roundcubemail/temp>
    Require all denied
</Directory>
<Directory /usr/share/roundcubemail/logs>
    Require all denied
</Directory>
<Directory /usr/share/roundcubemail/SQL>
    Require all denied
</Directory>
WEBMAILEOF

  # Apache reload (sadece çalışıyorsa)
  if systemctl is-active --quiet httpd; then
    systemctl reload httpd >> "$LOG" 2>&1 || warn "httpd reload failed"
  fi

  # ── ONOXSOFT Custom SSO Plugin (token-based, one-click webmail) ──────
  # Customer "Webmail Aç" → Panel token üretir + /var/lib/onoxsoft/sso_tokens/<token>.json
  # → Browser redirect /webmail/?_onx_sso=<token>
  # → Plugin authenticate hook: token file oku, $args['valid']=true → IMAP login
  # → INBOX direkt — password yazılmaz (Plesk/cPanel parity)
  info "Roundcube ONOXSOFT SSO plugin kurulumu"
  run mkdir -p /usr/share/roundcubemail/plugins/onoxsoft_sso
  run mkdir -p /var/lib/onoxsoft/sso_tokens
  run chown apache:apache /var/lib/onoxsoft/sso_tokens
  run chmod 770 /var/lib/onoxsoft/sso_tokens

  cat > /usr/share/roundcubemail/plugins/onoxsoft_sso/onoxsoft_sso.php <<'SSOPLUGIN_EOF'
<?php
/**
 * ONOXSOFT SSO Plugin — token-based one-click webmail (autologon pattern).
 * Token: /var/lib/onoxsoft/sso_tokens/<token>.json (5dk TTL, tek-kullanım).
 */
class onoxsoft_sso extends rcube_plugin
{
    public $task = 'login|';
    private $token_dir = '/var/lib/onoxsoft/sso_tokens';

    public function init()
    {
        $this->add_hook('startup', [$this, 'startup']);
        $this->add_hook('authenticate', [$this, 'authenticate']);
    }

    public function startup($args)
    {
        if (empty($_SESSION['user_id']) && $this->getToken() !== null) {
            $args['action'] = 'login';
        }
        return $args;
    }

    public function authenticate($args)
    {
        $token = $this->getToken();
        if ($token === null) return $args;
        $creds = $this->loadCreds($token);
        if ($creds === null) return $args;

        $args['user']        = $creds['user'];
        $args['pass']        = $creds['pass'];
        $args['host']        = $creds['host'] ?? 'localhost';
        $args['cookiecheck'] = false;
        $args['valid']       = true;

        $this->deleteToken($token);
        return $args;
    }

    private function getToken(): ?string
    {
        $raw = $_GET['_onx_sso'] ?? null;
        if (!is_string($raw) || !preg_match('/^[a-f0-9]{32,64}$/', $raw)) return null;
        return $raw;
    }

    private function loadCreds(string $token): ?array
    {
        $file = $this->token_dir . '/' . $token . '.json';
        if (!is_readable($file)) return null;
        $raw = @file_get_contents($file);
        if (!$raw) return null;
        $data = @json_decode($raw, true);
        if (!is_array($data) || empty($data['user']) || empty($data['pass'])) return null;
        if (!empty($data['expires']) && $data['expires'] < time()) {
            @unlink($file);
            return null;
        }
        return $data;
    }

    private function deleteToken(string $token): void
    {
        $file = $this->token_dir . '/' . $token . '.json';
        if (is_file($file)) @unlink($file);
    }
}
SSOPLUGIN_EOF

  run chmod 644 /usr/share/roundcubemail/plugins/onoxsoft_sso/onoxsoft_sso.php

  # Plugin'i Roundcube config'e enable et (idempotent)
  if ! grep -q "'onoxsoft_sso'" /etc/roundcubemail/config.inc.php 2>/dev/null; then
    # plugins array'in başına ekle
    run sed -i "s|\$config\['plugins'\] = \[|\$config['plugins'] = ['onoxsoft_sso', |" /etc/roundcubemail/config.inc.php
  fi

  success "Roundcube DB + config.inc.php + Apache PHP handler + ONOXSOFT SSO plugin 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

  # ─── ONOXSOFT Signon SSO Deploy ─────────────────────────────────────────
  _deploy_pma_signon "rhel"
}

# ─── 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

  # ─── ONOXSOFT Signon SSO Deploy ─────────────────────────────────────────
  _deploy_pma_signon "debian"
}

# ─── phpMyAdmin Signon SSO Deploy (cross-distro) ─────────────────────────
# /etc/phpMyAdmin/onx-signon.php + config.inc.php snippet ekler.
# Idempotent — marker ('ONOXSOFT-SSO-BEGIN') varsa atlanır.
_deploy_pma_signon() {
  local family="${1:-rhel}"
  local pma_etc pma_cfg
  if [[ "${family}" == "rhel" ]]; then
    pma_etc="/etc/phpMyAdmin"
    pma_cfg="${pma_etc}/config.inc.php"
  else
    pma_etc="/etc/phpmyadmin"
    pma_cfg="${pma_etc}/config.inc.php"
  fi

  if [[ ! -d "${pma_etc}" ]]; then
    info "phpMyAdmin etc dizini yok — signon deploy atlanıyor: ${pma_etc}"
    return 0
  fi

  # Signon script template'ini kopyala (panel repo'da templates/phpmyadmin/)
  local src_signon="${ONOX_HOME}/config/onoxsoft/templates/phpmyadmin/onx-signon.php.stub"
  local src_snippet="${ONOX_HOME}/config/onoxsoft/templates/phpmyadmin/config.snippet.stub"
  local dst_signon="${pma_etc}/onx-signon.php"

  if [[ -f "${src_signon}" ]]; then
    run cp -f "${src_signon}" "${dst_signon}"
    chown "root:${WEB_GROUP}" "${dst_signon}"
    chmod 0640 "${dst_signon}"
    success "phpMyAdmin signon script kuruldu: ${dst_signon}"
  else
    warn "Signon template bulunamadı: ${src_signon} (panel repo eksik mi?)"
    return 0
  fi

  # config.inc.php snippet ekle — idempotent (marker check)
  if [[ -f "${pma_cfg}" ]] && [[ -f "${src_snippet}" ]]; then
    if grep -q "ONOXSOFT-SSO-BEGIN" "${pma_cfg}" 2>/dev/null; then
      info "phpMyAdmin SSO snippet zaten config.inc.php'de — atlanıyor (idempotent)"
    else
      # Snippet'in <?php satırını çıkar (zaten config.inc.php içinde PHP)
      local tmp_snip
      tmp_snip="$(mktemp)"
      grep -v "^<?php" "${src_snippet}" > "${tmp_snip}"
      cat "${tmp_snip}" >> "${pma_cfg}"
      rm -f "${tmp_snip}"
      success "phpMyAdmin SSO snippet eklendi: ${pma_cfg}"
    fi
  fi

  # SELinux — onx-signon.php panel'in port ${PANEL_SSL_PORT}'una cURL yapacak
  if command -v setsebool &>/dev/null; then
    run setsebool -P httpd_can_network_connect 1 2>/dev/null || true
  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

  # Backup offsite destination CLI'leri — fresh kurulumda backup hedefleri (Local/S3/B2/Wasabi/
  # SFTP/FTP/Restic/Google Drive) kutudan çıkar çıkmaz çalışsın (rclone/aws/lftp/restic/rsync/sshpass).
  info "Backup destination araçları (rclone/awscli/lftp/restic/rsync/sshpass)"
  pkg_install rclone awscli lftp restic rsync sshpass \
    || warn "Bazı backup-dest araçları kurulamadı — ilgili offsite hedefler kısıtlı olur"

  # 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 4.5 — Time sync (chrony + timezone)
# Mail (DKIM/Received headers), SSL cert validity, audit log ordering — hepsi
# accurate clock'a bağımlı. Drift >1s mail-tester score düşürür, browser cert
# warning verir, audit log analizi karışır.
# ---------------------------------------------------------------------------
step_configure_time_sync() {
  step "4.5/17  Zaman senkronu (chrony + TZ)"

  # 1. Timezone — config/onoxsoft.php'den veya default Europe/Istanbul
  local target_tz="${ONOX_TIMEZONE:-Europe/Istanbul}"

  if [[ -f "/usr/share/zoneinfo/${target_tz}" ]]; then
    info "Timezone: ${target_tz}"
    run timedatectl set-timezone "${target_tz}"
  else
    warn "Geçersiz timezone '${target_tz}', UTC fallback"
    run timedatectl set-timezone UTC
  fi

  # 2. chrony enable + TR + global NTP poolu
  # chrony zaten paket kurulumunda var (step 4), şimdi config + enable
  if [[ ! -f /etc/chrony.conf ]] && [[ ! -f /etc/chrony/chrony.conf ]]; then
    warn "chrony config dosyası bulunamadı — paket kurulumu eksik"
    return 0
  fi

  local chrony_conf=/etc/chrony.conf
  [[ ! -f "$chrony_conf" ]] && chrony_conf=/etc/chrony/chrony.conf

  # ONOXSOFT TR-pool + global fallback
  if ! grep -q "ONOXSOFT NTP pools" "$chrony_conf" 2>/dev/null; then
    info "chrony config — TR + global NTP pools"
    cat >> "$chrony_conf" <<'CHRONY_EOF'

# ONOXSOFT NTP pools — TR pool öncelikli, global fallback
pool 0.tr.pool.ntp.org iburst maxsources 4
pool 1.tr.pool.ntp.org iburst maxsources 2
pool 0.pool.ntp.org    iburst maxsources 2
pool time.google.com   iburst maxsources 2

# Force step if drift > 1 second (mail timestamps için kritik)
makestep 1.0 3

# Drift dosyası
driftfile /var/lib/chrony/drift

# Log drift değişimleri
logdir /var/log/chrony
log measurements statistics tracking
CHRONY_EOF
  fi

  # Enable + start + force initial sync
  run systemctl enable chronyd
  if try_run systemctl restart chronyd; then
    sleep 2
    run chronyc makestep || true
    run chronyc burst 4/4 || true
    sleep 2
    success "Zaman senkronu aktif (drift: $(chronyc tracking 2>/dev/null | grep 'System time' | awk -F': ' '{print $2}' || echo 'unknown'))"
  else
    warn "chronyd start başarısız"
  fi

  # 3. PHP timezone (tüm versions: 74/81/82/83) — Remi modular
  info "PHP timezone (date.timezone = ${target_tz})"
  for ver in 74 80 81 82 83; do
    local php_ini="/etc/opt/remi/php${ver}/php.ini"
    if [[ -f "$php_ini" ]]; then
      if grep -qE "^;?\s*date\.timezone" "$php_ini"; then
        run sed -i "s|^;\?\s*date\.timezone.*|date.timezone = ${target_tz}|" "$php_ini"
      else
        echo "date.timezone = ${target_tz}" >> "$php_ini"
      fi
    fi
  done
  # Default php.ini (system)
  if [[ -f /etc/php.ini ]] && grep -qE "^;?\s*date\.timezone" /etc/php.ini; then
    run sed -i "s|^;\?\s*date\.timezone.*|date.timezone = ${target_tz}|" /etc/php.ini
  fi

  # 4. MariaDB time_zone — system_time_zone yerine SET GLOBAL
  if command -v mysql >/dev/null 2>&1 && systemctl is-active --quiet mariadb 2>/dev/null; then
    # MySQL TZ tables yüklü değilse yükle (mariadb tz table init)
    if [[ -f /usr/share/mysql/mysql_system_tables_data.sql ]] || command -v mysql_tzinfo_to_sql >/dev/null 2>&1; then
      mysql_tzinfo_to_sql /usr/share/zoneinfo 2>/dev/null | mysql --defaults-file=/root/.onox-mysql-root.cnf mysql 2>/dev/null || true
    fi

    # my.cnf'e default-time-zone ekle (kalıcı)
    local mariadb_conf=/etc/my.cnf.d/onoxsoft.cnf
    if [[ ! -f "$mariadb_conf" ]] || ! grep -q "default-time-zone" "$mariadb_conf"; then
      mkdir -p "$(dirname $mariadb_conf)"
      cat > "$mariadb_conf" <<MARIADB_EOF
[mysqld]
default-time-zone = '${target_tz}'
MARIADB_EOF
      systemctl restart mariadb 2>/dev/null || true
    fi
  fi

  # 5. Laravel APP_TIMEZONE env
  if [[ -f "${ONOX_HOME}/.env" ]]; then
    if grep -q "^APP_TIMEZONE=" "${ONOX_HOME}/.env"; then
      run sed -i "s|^APP_TIMEZONE=.*|APP_TIMEZONE=${target_tz}|" "${ONOX_HOME}/.env"
    else
      echo "APP_TIMEZONE=${target_tz}" >> "${ONOX_HOME}/.env"
    fi
  fi

  success "Zaman senkronu + timezone kuruldu (chrony + PHP + MariaDB + Laravel)"
}

# ---------------------------------------------------------------------------
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

  # Apache user'ı onoxsoft-users grubuna ekle — user'ların public_html
  # klasörleri mode 0750 root:onoxsoft-users; apache traverse edebilmek için
  # grup üyeliği lazım. Aksi halde her subdomain 403 Forbidden döner.
  # Idempotent — usermod -aG zaten varsa no-op.
  if id apache &>/dev/null; then
    if ! id -nG apache | grep -qw onoxsoft-users; then
      run usermod -aG onoxsoft-users apache
      info "apache user → onoxsoft-users grubuna eklendi"
      # httpd reload — group üyeliği aktif olsun (running process'in groups'unu refresh için restart)
      if systemctl is-active --quiet httpd 2>/dev/null; then
        run systemctl restart httpd
      fi
    else
      info "apache zaten onoxsoft-users grubunda"
    fi
  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 7.5 — Disk quota enable (XFS uquota / ext4 usrquota)
#
# Per-account disk limit için filesystem-level quota gerekir. Hesap oluşturma
# sırasında setquota çağrılır; mount option olmadan setquota fail eder.
#
# Mount option iki yöntemle aktive edilir:
#   1) /etc/fstab'a eklenir (kalıcı, ama remount/reboot gerekli)
#   2) Şu anki mount'a remount option ile (anlık, fstab değişmezse reboot'ta kaybolur)
#
# Phase 1 demo: /home ayrı mount değilse / (root) üzerinde uygulanır.
# Eğer mount remount başarısız olursa (root fs için tehlikeli olabilir),
# warning verilir; admin sonradan reboot'la kalıcılaştırır.
# ---------------------------------------------------------------------------
step_enable_disk_quota() {
  step "7.5/17 Disk quota"

  # /home/users — müşteri hesapları için ayrı klasör (cPanel-style)
  # Tüm onx_* hesapları burada yaşar; /home root'u sistem user'larına bırakılır.
  if [[ ! -d /home/users ]]; then
    run mkdir -p /home/users
    # Apache 711 traversal — diğer hesapları görmeden geçebilsin
    run chmod 755 /home/users
    run chown root:root /home/users
    info "  /home/users müşteri hesapları için oluşturuldu"
  fi

  # /home ayrı mount mu?
  local quota_target="/home"
  local fs_type
  if mountpoint -q /home 2>/dev/null; then
    quota_target="/home"
  else
    quota_target="/"
    info "  /home ayrı mount değil — / (root) üzerinde quota aktif edilecek"
  fi

  fs_type=$(findmnt -no FSTYPE "${quota_target}" 2>/dev/null || echo "unknown")
  info "  Filesystem: ${fs_type} on ${quota_target}"

  case "${fs_type}" in
    xfs)
      # XFS: pquota|uquota|gquota mount option ile aktive, remount yetmez
      # — reboot gerekir. Şimdilik uyarı + fstab değişikliği.
      if grep -qE "\s${quota_target}\s.*xfs.*(uquota|usrquota|prjquota)" /etc/fstab 2>/dev/null; then
        info "  XFS quota fstab'da zaten ayarlı"
      else
        # /etc/fstab'da quota_target satırına uquota,gquota ekle
        run cp /etc/fstab /etc/fstab.bak.$(date +%s)
        run sed -i "s|\(${quota_target//\//\\/}\s\+xfs\s\+\)\([^ ]*\)|\1\2,uquota,gquota|" /etc/fstab
        warn "  XFS quota /etc/fstab'a eklendi — REBOOT GEREKLİ (mount remount XFS quota'yı aktive etmez)"
        warn "  Reboot sonrası 'xfs_quota -x -c \"state\" ${quota_target}' kontrol et"
      fi
      ;;
    ext4|ext3|ext2)
      # ext: usrquota,grpquota mount option, remount + quotacheck + quotaon
      if ! grep -qE "\s${quota_target}\s.*ext[234].*(usrquota|grpquota)" /etc/fstab 2>/dev/null; then
        run cp /etc/fstab /etc/fstab.bak.$(date +%s)
        run sed -i "s|\(${quota_target//\//\\/}\s\+ext[234]\s\+\)\([^ ]*\)|\1\2,usrquota,grpquota|" /etc/fstab
        info "  ext quota /etc/fstab'a eklendi"
      fi
      # Live remount
      if mount -o remount,usrquota,grpquota "${quota_target}" 2>/dev/null; then
        # quotacheck (rebuild aquota files)
        if command -v quotacheck &>/dev/null; then
          run quotacheck -ugm "${quota_target}" 2>/dev/null || true
        fi
        run quotaon -v "${quota_target}" 2>/dev/null || true
        success "  ext quota aktif: ${quota_target}"
      else
        warn "  Live remount başarısız — reboot sonrası fstab quota aktive olur"
      fi
      ;;
    btrfs)
      # btrfs: quota komutu farklı, btrfs qgroup
      if command -v btrfs &>/dev/null; then
        run btrfs quota enable "${quota_target}" 2>/dev/null || true
        info "  btrfs qgroup aktive denendi"
      fi
      warn "  btrfs quota sınırlı destek — per-user XFS gibi çalışmaz"
      ;;
    *)
      warn "  Bilinmeyen filesystem: ${fs_type} — quota atlandı, hesap-disk limiti uygulanmaz"
      ;;
  esac
}

# ---------------------------------------------------------------------------
# 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

  # KRİTİK: Default CA Let's Encrypt yapılır. ZeroSSL'in serbest kullanım
  # rate-limit'i çok agresif (retry-after 86400s = 24 saat); Let's Encrypt
  # daha cömert (50/hafta/account, IP başına 100/3saat). LE ayrıca production
  # için endüstri standardı.
  if [[ -x "${ONOX_ACME}/acme.sh" ]]; then
    try_run "${ONOX_ACME}/acme.sh" --set-default-ca --server letsencrypt
    try_run "${ONOX_ACME}/acme.sh" --register-account -m "${ADMIN_EMAIL}" --server letsencrypt
    info "  acme.sh default CA: Let's Encrypt"
  fi

  # v89 — SSL backend defensive verify:
  # SSL auto-issue (Agent 3) needs acme.sh OR certbot. Both are normally pulled
  # in earlier (acme.sh above, certbot from STEP 5 dnf), but on partial reruns
  # / dnf failures one can be missing. Last-mile heal — install whatever's gone.
  info "  SSL backend deps verify (acme.sh + certbot)"
  if [[ ! -x "${ONOX_ACME}/acme.sh" ]]; then
    warn "  acme.sh kurulu degil — son care: net-install"
    mkdir -p "${ONOX_ACME}"
    # Cron-friendly: --no-cron --noprofile (panel kendi schedule eder)
    if curl -fsS --max-time 30 https://get.acme.sh \
         | sh -s -- --home "${ONOX_ACME}" --no-cron --noprofile 2>&1 | tail -5 >> "$LOG"
    then
      success "  acme.sh net-install OK"
    else
      warn "  acme.sh net-install fail — SSL provisioning certbot'a duser"
    fi
  fi
  if ! command -v certbot >/dev/null 2>&1; then
    warn "  certbot kurulu degil — dnf install"
    if pkg_install certbot 2>&1 | tail -3 >> "$LOG"; then
      success "  certbot dnf install OK"
    else
      warn "  certbot install fail (acme.sh kullanilacak)"
    fi
  fi
  # Final report — at least ONE must be present.
  if [[ -x "${ONOX_ACME}/acme.sh" ]] || command -v certbot >/dev/null 2>&1; then
    success "  SSL backend ready: $([[ -x "${ONOX_ACME}/acme.sh" ]] && echo -n "acme.sh "; command -v certbot >/dev/null 2>&1 && echo -n "certbot")"
  else
    warn "  HICBIR SSL backend yok — Let's Encrypt cert issue calismaz, manuel kurulum gerek"
  fi
}

# ---------------------------------------------------------------------------
# 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

  # v3.66: local-address adaptif. Başka bir resolver (unbound recursor /
  # systemd-resolved) 127.0.0.x:53'ü tutuyorsa pdns 0.0.0.0:53'e bind EDEMEZ
  # ("Address already in use" → systemd restart fırtınası → load patlaması + DNS
  # down; bir reboot sonrası yaşandı). Bu durumda pdns'i PRIMARY public IP'ye
  # bind et (authoritative zaten public IP'de cevap verir); recursor 127.0.0.1'de
  # kalır. Çakışma yoksa 0.0.0.0 korunur (taze kurulumda davranış değişmez).
  local PDNS_LOCAL_ADDR="0.0.0.0"
  if command -v ss >/dev/null 2>&1 && ss -lnpH 'sport = :53' 2>/dev/null | grep -qE '127\.[0-9.]+:53'; then
    local _pdns_ip
    _pdns_ip=$(ip -4 route get 8.8.8.8 2>/dev/null | grep -oP 'src \K[0-9.]+' | head -1)
    if [[ "$_pdns_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
      PDNS_LOCAL_ADDR="$_pdns_ip"
      warn "127.0.0.x:53 başka resolver'da (unbound?) — pdns ${PDNS_LOCAL_ADDR}:53'e bind ediliyor (çakışma önleme)"
    fi
  fi

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

launch=gmysql
local-address=${PDNS_LOCAL_ADDR}
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/valkey/valkey.conf ]]; then
    redis_conf=/etc/valkey/valkey.conf
  elif [[ -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.4 — Pure-FTPd: chroot + PAM auth + enable & start
# Phase 1 için basit PAM auth (Linux user). Virtual user (MySQL backend)
# Epic 11'de eklenecek; o zamana kadar `onx_*` hesapları PAM ile FTP yapabilir.
# ---------------------------------------------------------------------------
step_setup_pureftpd() {
  step "11.4/17  Pure-FTPd"

  if ! command -v pure-ftpd &>/dev/null && ! command -v pure-config.pl &>/dev/null; then
    warn "Pure-FTPd binary bulunamadı — atlanıyor"
    return 0
  fi

  # RHEL: /etc/pure-ftpd/pure-ftpd.conf  |  Debian: /etc/pure-ftpd/conf/*
  local conf="/etc/pure-ftpd/pure-ftpd.conf"
  if [[ -f "${conf}" ]]; then
    info "Pure-FTPd config ayarları (chroot + PAM)"

    # ChrootEveryone yes — hepsi kendi /home/*'da kalsın
    if grep -qE '^[#\s]*ChrootEveryone' "${conf}"; then
      run sed -i 's|^[#\s]*ChrootEveryone.*|ChrootEveryone               yes|' "${conf}"
    else
      echo "ChrootEveryone               yes" >> "${conf}"
    fi

    # PAMAuthentication yes — Linux user'larla auth (UID >= 1000)
    if grep -qE '^[#\s]*PAMAuthentication' "${conf}"; then
      run sed -i 's|^[#\s]*PAMAuthentication.*|PAMAuthentication            yes|' "${conf}"
    else
      echo "PAMAuthentication            yes" >> "${conf}"
    fi

    # NoAnonymous yes — anonymous FTP kapalı
    if grep -qE '^[#\s]*NoAnonymous' "${conf}"; then
      run sed -i 's|^[#\s]*NoAnonymous.*|NoAnonymous                  yes|' "${conf}"
    else
      echo "NoAnonymous                  yes" >> "${conf}"
    fi

    # MinUID 1000 — sistem user'larla login engelle
    if grep -qE '^[#\s]*MinUID' "${conf}"; then
      run sed -i 's|^[#\s]*MinUID.*|MinUID                       1000|' "${conf}"
    else
      echo "MinUID                       1000" >> "${conf}"
    fi
  fi

  # firewalld FTP portu (passive range yok, sadece kontrol portu)
  if [[ "$OS_FAMILY" == "rhel" ]] && command -v firewall-cmd &>/dev/null; then
    run firewall-cmd --permanent --add-service=ftp 2>/dev/null || true
    run firewall-cmd --reload 2>/dev/null || true
  fi

  try_run systemctl enable --now pure-ftpd \
    || warn "pure-ftpd başlatılamadı: $(systemctl status pure-ftpd 2>&1 | head -5)"

  if systemctl is-active --quiet pure-ftpd; then
    success "Pure-FTPd çalışıyor (PAM auth + chroot)"
  else
    warn "Pure-FTPd inactive — Epic 11'de MySQL backend ile yeniden yapılandırılacak"
  fi
}

# ---------------------------------------------------------------------------
# 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 ───────────────────────────────────────
  # Postfix mydomain'i myhostname'den otomatik çıkarır (son 2 segment).
  # Eğer PANEL_HOSTNAME bir IP ise (örn 149.102.145.172) mydomain de IP olur
  # ve Postfix "bad parameter value: numeric" fatal'iyle çöker.
  # Çözüm: IP-only modda placeholder FQDN (localdomain) kullan, sonradan
  # admin gerçek domain set ettiğinde panel arayüzünden değiştirilebilir.
  local postfix_host="${mail_host}"
  local postfix_domain="localdomain"
  if [[ "$PANEL_HOSTNAME" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    postfix_host="mail.localdomain"
    warn "Postfix: IP-only kurulum tespit edildi — placeholder FQDN kullanılıyor (mail.localdomain)"
    warn "Sonradan: panel Sistem Ayarları → Hostname'i gerçek FQDN'e set et, mail stack'i restart et"
  fi

  info "Postfix yapılandırması (host=${postfix_host}, domain=${postfix_domain})"
  run postconf -e "myhostname = ${postfix_host}"
  run postconf -e "mydomain = ${postfix_domain}"
  # v58: myorigin = $mydomain — envelope From ana domain'i kullansin.
  # Default $myhostname = panel.X.com.tr (subdomain) → SPF lookup subdomain icin
  # ayri TXT gerektirir. $mydomain ile ana domain SPF'i kullanilir → tek SPF kaydi yeter.
  run postconf -e "myorigin = \$mydomain"
  # v77.8: mydestination'a $mydomain ekle — panel kendi domain'inden notification
  # gönderirse (MAIL_FROM=noreply@$mydomain) Postfix local delivery yapar, "loops
  # back to myself" 5.4.6 bounce engellenir. Olmadan Laravel notification job'lari
  # postqueue'da birikir, CPU spam.
  run postconf -e "mydestination = \$myhostname, localhost.\$mydomain, localhost, \$mydomain"
  run postconf -e "inet_interfaces = all"
  # IPv4-only outbound — IPv6 PTR set edilmediği sürece Gmail/Outlook
  # 550-5.7.25 "no PTR record" ile reject eder. Contabo gibi VPS'lerde IPv6
  # default açık ama PTR ayrı set edilmesi gerek (manuel iş). IPv4-only
  # zorlama bunu bypass eder; sadece IPv4 PTR set edilirse mail gönder.
  run postconf -e "inet_protocols = ipv4"
  run postconf -e "smtp_address_preference = ipv4"
  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

  # v77.8: noreply alias — Laravel notification spam (MAIL_FROM=noreply@$mydomain)
  # /dev/null'a discard edilir. Panel UI bildirim spam'i + bounce loop önlenir.
  if [[ -f /etc/aliases ]] && ! grep -q "^noreply:" /etc/aliases; then
    info "Postfix noreply alias → /dev/null (notification discard)"
    echo "noreply: /dev/null" >> /etc/aliases
    run newaliases
  fi

  # Postfix map tipi: el8/el9 + Debian → hash (Berkeley DB). RHEL 10 libdb'yi KALDIRDI →
  # Postfix 'hash' desteği olmadan derlenir (default lmdb) → 'postmap hash:' "unsupported
  # dictionary type" hatası verir. el10'da lmdb kullan (tüm map referansları). plain 'postmap
  # /path' default_database_type kullanır (el10=lmdb, el9=hash) → ${pf_map} ile zaten uyumlu.
  local pf_map="hash"
  [[ "$OS_FAMILY" == "rhel" && "$OS_MAJOR" == "10" ]] && pf_map="lmdb"

  # Mailing list & alias hash map — onx-mail-alias-set buraya yazar
  # (virtual_alias_maps zincirine ek olarak; SQL backend ile birlikte çalışır)
  info "Postfix mailing list hash map (virtual_alias_maps)"
  run touch /etc/postfix/onoxsoft-mailing-lists
  run chown root:root /etc/postfix/onoxsoft-mailing-lists
  run chmod 644 /etc/postfix/onoxsoft-mailing-lists
  run postmap /etc/postfix/onoxsoft-mailing-lists 2>/dev/null || true

  # Mevcut virtual_alias_maps değerini al, hash:'i ekle (zaten varsa skip)
  local current_aliases
  current_aliases=$(postconf -h virtual_alias_maps 2>/dev/null || echo "")
  if [[ "$current_aliases" != *"${pf_map}:/etc/postfix/onoxsoft-mailing-lists"* ]]; then
    if [[ -z "$current_aliases" ]]; then
      run postconf -e "virtual_alias_maps = ${pf_map}:/etc/postfix/onoxsoft-mailing-lists"
    else
      run postconf -e "virtual_alias_maps = ${current_aliases}, ${pf_map}:/etc/postfix/onoxsoft-mailing-lists"
    fi
  fi

  # Email Routing — per-domain transport_maps (cPanel "Email Routing" muadili)
  # onx-postfix-transport-write buraya yazar (local/remote/backup/auto modes)
  info "Postfix email routing transport_maps (per-domain delivery)"
  run touch /etc/postfix/onoxsoft-transport
  run chown root:root /etc/postfix/onoxsoft-transport
  run chmod 644 /etc/postfix/onoxsoft-transport
  run postmap /etc/postfix/onoxsoft-transport 2>/dev/null || true

  local current_transport
  current_transport=$(postconf -h transport_maps 2>/dev/null || echo "")
  if [[ "$current_transport" != *"${pf_map}:/etc/postfix/onoxsoft-transport"* ]]; then
    if [[ -z "$current_transport" ]]; then
      run postconf -e "transport_maps = ${pf_map}:/etc/postfix/onoxsoft-transport"
    else
      run postconf -e "transport_maps = ${current_transport}, ${pf_map}:/etc/postfix/onoxsoft-transport"
    fi
  fi

  # 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"

  # ── SNI map (multi-tenant per-domain TLS cert) ───────────────────────────
  # Apple Mail / Outlook / Thunderbird `mail.<domain>` ile bağlanırken doğru
  # cert sunmak için. AutoSslManager cert renew sonrası onx-mail-cert-link
  # sysapi'sini çağırarak her domain için SNI map'e satır ekler:
  #   mail.example.com /etc/letsencrypt/live/example.com/privkey.pem  /etc/letsencrypt/live/example.com/fullchain.pem
  local sni_map=/etc/postfix/tls_sni_map
  if [[ ! -f "${sni_map}" ]]; then
    run touch "${sni_map}"
    run chmod 644 "${sni_map}"
    # Boş hash db oluştur (postmap)
    run postmap -F "${pf_map}:${sni_map}"
  fi
  run postconf -e "tls_server_sni_maps = ${pf_map}:${sni_map}"

  # 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

  # ── Default IMAP klasörleri (Drafts/Sent/Junk/Trash/Archive) ────────────
  # IMAP client'lar veya webmail bağlandığında otomatik oluşur + subscribe.
  # cPanel/Plesk parity — yeni mailbox'ta direkt 5 klasör görünür.
  cat > /etc/dovecot/conf.d/15-mailboxes.conf <<'MAILBOXES_EOF'
# ONOXSOFT — Default IMAP klasörleri (auto-create + special-use)
namespace inbox {
    inbox = yes

    mailbox Drafts {
        auto = subscribe
        special_use = \Drafts
    }
    mailbox Sent {
        auto = subscribe
        special_use = \Sent
    }
    mailbox Junk {
        auto = subscribe
        special_use = \Junk
        autoexpunge = 30d
    }
    mailbox Trash {
        auto = subscribe
        special_use = \Trash
        autoexpunge = 30d
    }
    mailbox Archive {
        auto = subscribe
        special_use = \Archive
    }
    mailbox "Sent Messages" {
        special_use = \Sent
    }
    mailbox "Deleted Messages" {
        special_use = \Trash
    }
}
MAILBOXES_EOF

  chown root:dovecot /etc/dovecot/conf.d/15-mailboxes.conf 2>/dev/null
  chmod 640 /etc/dovecot/conf.d/15-mailboxes.conf

  # v77.13: Default sieve template — spam mail INBOX'da kal, sadece \Flagged
  # işareti ekle. Quarantine/Junk fileinto YASAK çünkü Roundcube subscribed
  # olmayan klasörleri göstermez, kullanıcı mail'ini kaçırır.
  # Kullanıcı kendisi Roundcube'dan ManageSieve ile özel rule yazabilir.
  info "Dovecot default sieve template (flag-only spam policy)"
  mkdir -p /etc/dovecot/sieve
  cat > /etc/dovecot/sieve/default.sieve <<'SIEVE_DEFAULT_EOF'
require ["fileinto", "imap4flags"];

# v77.13: Spam mail INBOX'da kal, sadece \\Flagged işareti ekle.
# Eski Quarantine/Junk fileinto kullanıcının mail'i kaçırmasına sebep oldu
# (Roundcube Quarantine subscribed olmayan klasör → görünmüyor).
# Kullanıcı isterse Junk klasörüne manuel taşır veya ManageSieve ile
# kendi rule'unu yazar.

if header :contains "X-Spam" "Yes" {
    addflag "\\Flagged";
}

if header :contains "X-Spam-Status" "Yes" {
    addflag "\\Flagged";
}
SIEVE_DEFAULT_EOF
  if command -v sievec >/dev/null 2>&1; then
    sievec /etc/dovecot/sieve/default.sieve 2>/dev/null || true
  fi
  chown -R root:dovecot /etc/dovecot/sieve 2>/dev/null || true
  chmod 644 /etc/dovecot/sieve/default.sieve

  # 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

  # v92 Agent 2: /etc/alternatives/mta — Laravel Mail::raw + cron sendmail CLI yolu.
  # install.sh default'ta Postfix kurar; alternatives'i de Postfix'e zorla.
  # Bazı sistemlerde alternatives default'u sendmail/exim olabilir → silent mail loss.
  # Eğer Exim sysapi ile sonradan kurulmuşsa (active state), Exim'e yönlendir.
  # Ref: feedback_onoxsoft_mta_alternatives_switch.md
  if command -v alternatives >/dev/null 2>&1; then
    if systemctl is-active --quiet exim 2>/dev/null && [[ -f /usr/sbin/sendmail.exim ]]; then
      info "alternatives mta → sendmail.exim (Exim aktif tespit edildi)"
      alternatives --set mta /usr/sbin/sendmail.exim 2>/dev/null || \
        ln -sf /usr/sbin/sendmail.exim /etc/alternatives/mta 2>/dev/null || true
    elif [[ -f /usr/sbin/sendmail.postfix ]]; then
      info "alternatives mta → sendmail.postfix (varsayılan)"
      alternatives --set mta /usr/sbin/sendmail.postfix 2>/dev/null || \
        ln -sf /usr/sbin/sendmail.postfix /etc/alternatives/mta 2>/dev/null || true
    fi
  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

    # ─── Generic DKIM signing config (path-based, multi-tenant) ──────────
    # Path placeholder Rspamd'a "her domain için /var/lib/rspamd/dkim/
    # $domain.$selector.key dosyasını ara" der. Yeni domain provision olunca
    # onx-dkim-keygen otomatik bu path'e yazıyor → ekstra Rspamd config GEREK
    # YOK (cPanel/Plesk parity).
    mkdir -p /var/lib/rspamd/dkim
    if id _rspamd >/dev/null 2>&1; then
        chown _rspamd:_rspamd /var/lib/rspamd/dkim 2>/dev/null
    elif id rspamd >/dev/null 2>&1; then
        chown rspamd:rspamd /var/lib/rspamd/dkim 2>/dev/null
    fi
    chmod 750 /var/lib/rspamd/dkim

    cat > /etc/rspamd/local.d/dkim_signing.conf <<'DKIMSIG_EOF'
# ONOXSOFT — Generic DKIM signing (multi-tenant path-based)
# Her domain için ayrı config gerekmez; onx-dkim-keygen sysapi
# /var/lib/rspamd/dkim/<domain>.<selector>.key dosyasını yazıyor,
# Rspamd path placeholder ile otomatik buluyor.
enabled = true;
sign_local = true;
sign_authenticated = true;
allow_username_mismatch = true;
use_domain = "header";
use_esld = true;
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector = "default";
DKIMSIG_EOF

    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

  # Kaynak-tarball'da vendor/ + public/build/ YOK (boyut) — composer install + npm build
  # burada GERÇEKTEN çalışır. (Tam-vendor'lu release gönderilirse adımlar no-op'a düşer.)
  #
  # COMPOSER_ALLOW_SUPERUSER: installer root koşar; yoksa composer plugin'leri (php-http/
  # discovery, pest-plugin) "güvenlik için" devre dışı bırakır.
  export COMPOSER_ALLOW_SUPERUSER=1
  # Laravel 11'in TÜM yayınlı sürümleri (v11.31 … v11.54+) şu an packagist güvenlik
  # advisory'leriyle (PKSA-*) işaretli. Yeni Composer'ın policy.advisories.block'u (varsayılan
  # AÇIK) composer.lock YOKKEN yapılan update çözümünü komple reddeder ("Your requirements
  # could not be resolved") → kurulum kırılır. Sürüm pinli + operatör sorumluluğunda → engeli
  # kapat (eski composer bilmezse sessiz geç). Lock gönderilince zaten install-from-lock olur.
  composer config --global policy.advisories.block false >/dev/null 2>&1 || true

  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"; }

  # v3.79: Laravel Sanctum (müşteri REST API / "API Token'larım"). Yoksa routes/api.php
  # class_exists(Sanctum) guard'ı tüm /api/v1 grubunu register ETMEZ → API Token sayfası
  # "Sanctum kurulu değil" der. personal_access_tokens tablosu sonraki migrate adımında
  # oluşur (Laravel 11 paket migration'ını otomatik yükler).
  if [[ ! -d "${ONOX_HOME}/vendor/laravel/sanctum" ]]; then
    info "Laravel Sanctum kuruluyor (müşteri REST API / API Token)"
    timeout 300 composer require laravel/sanctum --no-interaction --no-progress \
      --working-dir="${ONOX_HOME}" >> "$LOG" 2>&1 \
      && success "  Sanctum kuruldu" \
      || warn "  Sanctum kurulamadı — müşteri API kapalı kalır (elle: composer require laravel/sanctum)"
  fi

  # v3.81: Billing modüllerini (WHMCS/WiseCP/Blesta/HostBill/Generic) kurulabilir .zip
  # olarak paketle → public/downloads/modules/. Admin bunları panelden indirip kendi
  # faturalama yazılımına yükler (modüller ONOXSOFT paneline değil, WHMCS'e kurulur).
  info "Billing modülleri paketleniyor (.zip)"
  ( cd "${ONOX_HOME}" && php artisan onox:billing:package-modules >> "$LOG" 2>&1 ) \
    && success "  Billing modülleri public/downloads/modules/ altına paketlendi" \
    || warn "  Modül paketleme atlandı (PHP zip extension gerekebilir: php82-php-pecl-zip)"
  [[ -d "${ONOX_HOME}/public/downloads" ]] \
    && chown -R "${WEB_USER}:${WEB_GROUP}" "${ONOX_HOME}/public/downloads" 2>/dev/null || true

  # Case-sensitive filesystem fix — bazı Vue dosyaları @/Composables (büyük C)
  # import ediyor ama gerçek klasör 'composables' (küçük c). Linux case-sensitive
  # filesystem'da bu fail eder. Symlink ile çözüyoruz; Vite build geçer.
  # (Phase 1.5'ta tüm import'lar lowercase'e normalize edilecek.)
  if [[ -d "${ONOX_HOME}/resources/js/composables" ]] \
     && [[ ! -e "${ONOX_HOME}/resources/js/Composables" ]]; then
    run ln -sfn composables "${ONOX_HOME}/resources/js/Composables"
    info "  Composables → composables symlink eklendi (case-sensitive fix)"
  fi

  # 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-)

  # v3.57: Public IPv4 detect — DeliverabilityChecker PTR/RBL için zorunlu.
  # 3 method: (1) ip route interface, (2) curl public IP service, (3) gethostip.
  # PANEL_HOSTNAME IP ise (ip-only mode) direkt onu kullan.
  local ONOXSOFT_SERVER_IP_DETECTED=""
  if [[ "$PANEL_HOSTNAME" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    ONOXSOFT_SERVER_IP_DETECTED="${PANEL_HOSTNAME}"
  else
    # Method 1: interface IP (non-private)
    ONOXSOFT_SERVER_IP_DETECTED=$(ip -4 -o addr show scope global 2>/dev/null \
      | awk '{print $4}' | cut -d/ -f1 \
      | grep -vE '^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|169\.254\.)' \
      | head -1)
    # Method 2: curl fallback
    if [[ -z "$ONOXSOFT_SERVER_IP_DETECTED" ]]; then
      ONOXSOFT_SERVER_IP_DETECTED=$(curl -4 -fsS --max-time 5 https://ifconfig.me 2>/dev/null \
        || curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null \
        || curl -4 -fsS --max-time 5 https://ipv4.icanhazip.com 2>/dev/null \
        || echo "")
    fi
    # Sanity check
    [[ "$ONOXSOFT_SERVER_IP_DETECTED" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] \
      || ONOXSOFT_SERVER_IP_DETECTED=""
  fi
  [[ -n "$ONOXSOFT_SERVER_IP_DETECTED" ]] \
    && info "Public IPv4: ${ONOXSOFT_SERVER_IP_DETECTED} (PTR/RBL + DNS provisioning için)" \
    || warn "Public IPv4 tespit edilemedi — sonradan 'php artisan onoxsoft:env-bootstrap' ile düzelt"

  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=sendmail
MAIL_FROM_ADDRESS="noreply@${PANEL_HOSTNAME}"
MAIL_FROM_NAME="ONOXSOFT"
# v65: sendmail CLI driver — Postfix port 25/2525'i bypass et, fail2ban ile aynı yol.
# -f envelope sender zorla (Symfony Mailer bazen IP gönderiyor → Gmail SPF reject).
# MAIL_FROM_ADDRESS panel domain'inden olmalı (gmail/yahoo public address YASAK).
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -t -i -f noreply@${PANEL_HOSTNAME}"

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}

# v3.57: Public IPv4 — DeliverabilityChecker PTR/RBL kontrolleri + DNS A
# record provisioning + mail HELO için kullanılır. Tespit edilemezse boş kalır,
# bu durumda 'php artisan onoxsoft:env-bootstrap' ile sonradan doldurulabilir (heredoc backtick DEGIL).
ONOXSOFT_SERVER_IP=${ONOXSOFT_SERVER_IP_DETECTED}
EOF

  # v3.75 GÜVENLİK: License MASTER public key (TÜM kurulumlarda AYNI).
  # ESKİ DAVRANIŞ (piracy açığı): install.sh her kuruluma AYRI sodium keypair
  # üretip HEM public HEM private'ı .env'e yazıyordu → panel kendi key'iyle verify
  # ettiği için MÜŞTERİ kendi lisansını self-sign edebiliyordu (dev-grant ile).
  #
  # YENİ MODEL: Lisans server'ın TEK master keypair'i var; private SADECE license
  # server'da. install.sh panele SADECE master PUBLIC key'i yazar (imzalama yok).
  # Panel verify eder, imzalayamaz → self-sign/piracy İMKANSIZ. Lisans yalnızca
  # license server'dan (issueManual / order) gelir.
  #
  # Master public key'i license server'da üret + buraya/ENV'e koy:
  #   (license server) php artisan onoxsoft:license-keygen
  #   → ONOXSOFT_LICENSE_PUBLIC_KEY çıktısını ONOXSOFT_MASTER_LICENSE_PUBLIC_KEY
  #     env'i ile install.sh'a geçir VEYA aşağıdaki MASTER_PUBKEY'e gömülü yapıştır.
  info "License master public key + intent secret yaziliyor (panelde private key YOK)"
  local intent_secret
  intent_secret=$(openssl rand -hex 32)

  # Shipped master public key — JwtVerifier::ONOX_LICENSE_PUBLIC_KEY sabitiyle AYNI
  # olmalı (panel production'da o sabitle verify eder; bu .env değeri yalnız dev/test'te
  # kullanılır). Master keypair'in private'ı SADECE license server'da (onox.com.tr/lisans).
  # Public key gizli değil — zaten JwtVerifier.php ile her kuruluma gidiyor.
  local MASTER_PUBKEY="A4oY0uXTgEaF66XVNB5rCmp4qYRdOvQd4yRtAA+64ZA="
  local master_pubkey="${ONOXSOFT_MASTER_LICENSE_PUBLIC_KEY:-$MASTER_PUBKEY}"
  if [[ -z "$master_pubkey" ]]; then
    warn "Master license public key BOŞ — lisans doğrulanamaz."
    warn "  License server'da: php artisan onoxsoft:license-keygen"
    warn "  Public key'i ONOXSOFT_MASTER_LICENSE_PUBLIC_KEY env ile install et veya .env'e elle yaz."
  fi

  cat >> "${ONOX_HOME}/.env" <<EOF

# License Server — MASTER public key (TÜM kurulumlarda AYNI; license server'ın tek
# keypair'inin public'i). Panel SADECE verify eder, imzalamaz. Private key YALNIZCA
# license server'da → müşteri self-sign edemez (piracy yok). Lisans /activate veya
# order ile license server'dan gelir.
ONOXSOFT_LICENSE_PUBLIC_KEY=${master_pubkey}
ONOXSOFT_INTENT_SECRET=${intent_secret}
EOF

  run chown -R "${WEB_USER}:${WEB_GROUP}" "${ONOX_HOME}/storage"
  run chown -R "${WEB_USER}:${WEB_GROUP}" "${ONOX_HOME}/bootstrap/cache"
  # storage permissions — 775 + httpd_sys_rw_content_t SELinux context
  chmod -R 775 "${ONOX_HOME}/storage" "${ONOX_HOME}/bootstrap/cache" 2>/dev/null || true
  if command -v chcon &>/dev/null; then
    try_run chcon -R -t httpd_sys_rw_content_t "${ONOX_HOME}/storage" "${ONOX_HOME}/bootstrap/cache" 2>/dev/null || true
  fi

  # Ö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

  # v3.57: env-bootstrap defansif çalıştırma — heredoc'ta IP detect başarısız
  # olduysa runtime'da tekrar dene + duplicate key tespit + boş value doldurma.
  # Idempotent: tüm key'ler doğruysa noop. PTR/RBL'in mutlaka IP'yi görmesi için
  # config:cache'ten ÖNCE çalışmalı.
  info "Env bootstrap (eksik .env key'lerini auto-detect)"
  if php "${ONOX_HOME}/artisan" onoxsoft:env-bootstrap 2>&1 | tee -a "$LOG"; then
    success "  env-bootstrap OK"
  else
    warn "  env-bootstrap fail — sonradan manuel çalıştır: php artisan onoxsoft:env-bootstrap"
  fi

  info "Cache + config + route cache (optimizasyon — fail olunca panel yine çalışır)"
  if php "${ONOX_HOME}/artisan" config:cache 2>&1 | tee -a "$LOG" | grep -q INFO; then
    success "  config:cache OK"
  else
    warn "  config:cache başarısız (kritik değil)"
  fi
  if php "${ONOX_HOME}/artisan" route:cache 2>&1 | tee -a "$LOG"; then
    success "  route:cache OK"
  else
    warn "  route:cache başarısız (closure route veya duplicate name olabilir)"
    warn "  Panel yine çalışır — sadece route lookup biraz yavaş"
  fi
  if php "${ONOX_HOME}/artisan" view:cache 2>&1 | tee -a "$LOG"; then
    success "  view:cache OK"
  else
    warn "  view:cache başarısız (kritik değil)"
  fi

  success "Panel deploy edildi"
}

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

  # ── WebDAV (webdisk system subdomain için) ─────────────────────────────────
  # mod_dav'ın "DAV On" direktifi global DAVLockDB gerektiriyor; yoksa apachectl
  # configtest fail eder ve webdisk vhost rollback olur (19 Mayıs 2026 production
  # fix). Per-vhost DAV On <Location> içinde olmalı (VirtualHost root'unda değil).
  info "WebDAV (mod_dav) global lock DB ve dizin"
  run mkdir -p /var/lib/dav
  run chown apache:apache /var/lib/dav
  run chmod 755 /var/lib/dav

  cat > /etc/httpd/conf.d/00-dav-lockdb.conf <<DAVEOF
# WebDAV lock database — required when "DAV On" directive used in vhosts
# (webdisk.<customer-domain> system subdomain için)
DAVLockDB /var/lib/dav/lockdb
DAVEOF

  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

    # HTTP → HTTPS redirect — SSL portuna açıkça yönlendir (HTTP_HOST aynı portu korur, kullanma)
    RewriteEngine On
    RewriteRule ^(.*)$ https://${PANEL_HOSTNAME}:${PANEL_SSL_PORT}\$1 [R=301,L]

    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

  # Customer vhost dizini — onx-vhost-add bu yola yazar.
  # Apache default config'inde conf.d/*.conf otomatik include ediliyor ama
  # subdirectory (sites/) include edilmediği için ekstra IncludeOptional gerekli.
  run mkdir -p "${WEB_CONF_DIR}/sites"
  run chmod 755 "${WEB_CONF_DIR}/sites"

  # AlmaLinux "Web Server Test Page" (welcome.conf) devre dışı —
  # default vhost öne geçer ve customer domain'ler için test sayfası gösterir.
  # Müşteri sitelerinin doğru render olması için kapatılmalı.
  if [[ "$OS_FAMILY" == "rhel" && -f /etc/httpd/conf.d/welcome.conf ]]; then
    run mv /etc/httpd/conf.d/welcome.conf /etc/httpd/conf.d/welcome.conf.disabled
    info "  Apache welcome.conf devre dışı (default test page kapatıldı)"
  fi
  if [[ "$OS_FAMILY" == "rhel" ]]; then
    if ! grep -qE "IncludeOptional\s+conf\.d/sites" /etc/httpd/conf/httpd.conf 2>/dev/null; then
      echo "" >> /etc/httpd/conf/httpd.conf
      echo "# Onoxsoft customer vhost'ları — install.sh v${SCRIPT_VERSION}" >> /etc/httpd/conf/httpd.conf
      echo "IncludeOptional conf.d/sites/*.conf" >> /etc/httpd/conf/httpd.conf
      info "  Apache conf.d/sites/ include eklendi"
    fi
  else
    # Debian — sites-enabled zaten Apache default'ta include ediliyor; ek setup yok
    :
  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"
  local acme="${ONOX_ACME}/acme.sh"   # cron tarafından da kullanılır, baştan tanımla
  run mkdir -p "${ssl_dir}"
  # chmod 711: apache user dizine traverse edebilir ama listeleyemez (cert dosyalarını
  # path ile direkt okuyabilir). chmod 700 idi → apache cert metadata'sını okuyamıyor,
  # SSL Manager UI "Sertifika yok" gösteriyordu. cPanel WHM SSL dizinleri standart
  # olarak ssh-traversable (root:wheel 755 veya 711). 711 = root sahip + traverse all,
  # listeleme yok (defense-in-depth).
  chmod 711 "${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"
    # Cert dosyaları public bilgi — apache user (SSL Manager UI) okuyabilmeli
    chmod 644 "${ssl_dir}/${PANEL_HOSTNAME}.crt" "${ssl_dir}/${PANEL_HOSTNAME}.fullchain"
    success "Self-signed sertifika oluşturuldu (tarayıcı uyarısı normal — FQDN sonradan ayarlandığında LE alınacak)"
  else
    [[ -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"
      chmod 644 "${ssl_dir}/${PANEL_HOSTNAME}.crt" "${ssl_dir}/${PANEL_HOSTNAME}.fullchain"
      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"
      # Cert + fullchain public bilgi — apache user (SSL Manager UI) okuyabilmeli
      chmod 644 "${ssl_dir}/${PANEL_HOSTNAME}.crt" "${ssl_dir}/${PANEL_HOSTNAME}.fullchain"
    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

    # ModSecurity panel vhost'unda KAPALI — Inertia PATCH/PUT/DELETE ve REST
    # API çağrıları OWASP CRS default kurallarınca bloklanıyordu (rule 911100
    # "Method is not allowed by policy"). Panel admin-only ve ayrıca IP whitelist
    # + 2FA korumalı; mod_security overhead'i + false-positive maliyeti taşımaya
    # değmez. Müşteri site'lerinde (port 80/443) ModSecurity aktif kalır.
    <IfModule mod_security2.c>
        SecRuleEngine Off
    </IfModule>

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

    # phpMyAdmin alias — panel SSO redirect URL'lerinin :${PANEL_SSL_PORT} altında
    # çalışması için. Hedef dizin EPEL phpMyAdmin paketinin /usr/share/phpMyAdmin
    # konumu. (Debian'da /usr/share/phpmyadmin)
    Alias /phpmyadmin /usr/share/phpMyAdmin
    Alias /phpMyAdmin /usr/share/phpMyAdmin
    <Directory /usr/share/phpMyAdmin>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
        <IfModule mod_php.c>
            php_admin_value upload_tmp_dir /var/lib/php/upload_tmp
            php_admin_value session.save_path /var/lib/php/session
        </IfModule>
    </Directory>
    <Directory /usr/share/phpMyAdmin/libraries>
        Require all denied
    </Directory>
    <Directory /usr/share/phpMyAdmin/setup/lib>
        Require all denied
    </Directory>

    <FilesMatch \.php\$>
        SetHandler "proxy:unix:${php_sock}|fcgi://localhost"
        # Laravel Inertia için: scheme + port bilgisi forward et — TrustProxies olmadan loop'suz
        SetEnv HTTPS on
        SetEnv HTTP_X_FORWARDED_PROTO https
        SetEnv HTTP_X_FORWARDED_PORT ${PANEL_SSL_PORT}
    </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

  # NOT: Eski versiyonda burada bir sed RewriteRule eklenirdi — her iki vhost'a
  # da uygulanıp redirect loop yaratıyordu. Şimdi HTTP vhost'una step 13'te
  # inline yazıyoruz (doğru SSL portuna).

  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

  # ─── Laravel scheduler cron — KRİTİK ────────────────────────────────────
  # routes/console.php'deki Schedule::call/command çağrılarının çalışabilmesi
  # için her dakika `php artisan schedule:run` çalışmalı. Bu olmadan:
  #   - system_metrics_history dolmaz (24h grafik boş)
  #   - SSL expiry checks çalışmaz
  #   - License heartbeat çalışmaz (panel offline grace period sonra kilitlenir)
  #   - Mailbox/quota/bandwidth recalc çalışmaz
  # PHP binary tespit — Remi modular php82 öncelikli, base /usr/bin/php fallback.
  # AlmaLinux 9 default repo PHP 8.0; Laravel 11 PHP 8.2+ gerektirir,
  # bu yüzden Remi'den php82 kuruluyor (step_install_packages_rhel).
  local php_bin="/usr/bin/php82"
  if [[ ! -x "${php_bin}" ]]; then
    php_bin="/usr/bin/php"
  fi

  cat > /etc/cron.d/onoxsoft-scheduler <<EOF
# Laravel scheduler — ONOXSOFT Panel
# Her dakika çalışır, schedule.php içindeki tüm scheduled job'ları tetikler.
# php_bin = ${php_bin} (Remi modular paket, Laravel 11 uyumlu)
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
* * * * * ${WEB_USER} cd ${ONOX_HOME} && ${php_bin} artisan schedule:run >> /var/log/onoxsoft/scheduler.log 2>&1
EOF
  chmod 0644 /etc/cron.d/onoxsoft-scheduler
  mkdir -p /var/log/onoxsoft
  touch /var/log/onoxsoft/scheduler.log
  chown "${WEB_USER}:${WEB_GROUP}" /var/log/onoxsoft/scheduler.log
  chmod 0644 /var/log/onoxsoft/scheduler.log

  # Cron reload
  systemctl reload crond 2>/dev/null || systemctl reload cron 2>/dev/null || true

  success "SSL sertifikası + cron'lar (acme renew + Laravel scheduler) 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")"
    # NOT: (( count++ )) bash post-increment 0 değer döndürür, set -e öldürür.
    # Pre-increment veya aritmetik atama kullan.
    count=$((count + 1))
  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 WEB_GROUP yoksa root fallback — || true ile die etmesin
  chown -R "root:${WEB_GROUP}" "${ONOX_BIN}" 2>/dev/null \
    || chown -R root:root "${ONOX_BIN}" 2>/dev/null \
    || true

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

  # ─── Sysapi templates kurulumu ───────────────────────────────────────────
  # Vhost, FPM pool, PowerDNS zone vd. template stub'ları
  # /usr/local/onoxsoft/templates/ altına kopyalanır. onx-vhost-add ve diğer
  # script'ler bu yolu hard-code ediyor.
  local tmpl_src="${ONOX_HOME}/config/onoxsoft/templates"
  local tmpl_dst="/usr/local/onoxsoft/templates"
  if [[ -d "${tmpl_src}" ]]; then
    run mkdir -p "${tmpl_dst}"
    # Sadece .stub uzantılı root level template'ler — alt klasörler (phpmyadmin/)
    # kendi deploy fonksiyonlarıyla kuruluyor.
    local tmpl_count=0
    for f in "${tmpl_src}"/*.stub; do
      [[ -f "$f" ]] || continue
      cp -f "$f" "${tmpl_dst}/"
      tmpl_count=$((tmpl_count + 1))
    done
    chown -R "root:${WEB_GROUP}" "${tmpl_dst}" 2>/dev/null || true
    chmod 755 "${tmpl_dst}"
    chmod 644 "${tmpl_dst}"/*.stub 2>/dev/null || true
    info "  ${tmpl_count} vhost/fpm template /usr/local/onoxsoft/templates/'e kuruldu"
  else
    warn "Templates dizini bulunamadı: ${tmpl_src}"
  fi

  # ─── 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

# KRİTİK: PTY kapalı tutulsun ki PHP'nin Process::input() ile gönderdiği JSON
# stdin'i bash script'e pipe ile ulaşsın. AlmaLinux 9 default: use_pty açık →
# sudo yeni PTY allocate eder, pipe içeriği kaybolur, INPUT=$(cat) boş döner.
Defaults!${ONOX_BIN}/onx-* !use_pty

# PAM/audit altyapısı bazı container/SELinux profillerinde apache'den engelliyor
Defaults!${ONOX_BIN}/onx-* !pam_session
Defaults!${ONOX_BIN}/onx-* !syslog

# 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

  # ─── /etc/pam.d/sudo: ${WEB_USER} bypass (KRİTİK SELinux+PAM fix) ─────────
  # AlmaLinux 9 + SELinux Enforcing'de apache (httpd_t) sudo NOPASSWD bile olsa
  # PAM system-auth stack'i unix_chkpwd'yi tetikliyor → /etc/shadow (shadow_t)
  # erişim denied → sudo fail → tüm sysapi çağrıları silent crash.
  # Production'da bu cascade failure → "server gitti" semptomu (VNC bile down).
  # Fix: ${WEB_USER} için PAM stack erken çıksın (pam_succeed_if quiet bypass).
  if [[ "${OS_FAMILY}" == "rhel" ]]; then
    cp -a /etc/pam.d/sudo /etc/pam.d/sudo.bak.onox 2>/dev/null || true
    cat > /etc/pam.d/sudo <<EOF
#%PAM-1.0
# Onoxsoft — minimal sudo PAM stack
# Web user (${WEB_USER}) NOPASSWD sudo çağrıları için pam_unix bypass.
# Aksi halde unix_chkpwd → /etc/shadow read → SELinux denied cascade.
#
# KRİTİK: 'sufficient' kullanılır. '[success=ok default=ignore]' YANLIŞ —
# success durumunda da sonraki module'leri ÇALIŞTIRIR (ignore = don't skip).
# 'sufficient' = match olursa STOP + return SUCCESS (next module SKIP).
auth       sufficient   pam_rootok.so
auth       sufficient   pam_succeed_if.so user = ${WEB_USER} quiet_success
auth       include      system-auth
account    sufficient   pam_succeed_if.so user = ${WEB_USER} quiet_success
account    include      system-auth
session    include      system-auth
EOF
    chmod 644 /etc/pam.d/sudo
    chown root:root /etc/pam.d/sudo
    info "PAM sudo stack: ${WEB_USER} bypass (sufficient) eklendi"
  fi
}

# ---------------------------------------------------------------------------
# STEP 16.2 — SELinux custom policies (kritik SELinux fix)
# Apache + fail2ban + httpd domain transition'ları için custom policy modülleri.
# ---------------------------------------------------------------------------
step_install_selinux_policies() {
  step "16.2/17  SELinux Permissive moduna geç + booleans"

  # SELinux yoksa atla
  if ! command -v getenforce &>/dev/null; then
    info "SELinux yok — bu step atlanıyor"
    return 0
  fi

  local selmode
  selmode="$(getenforce 2>/dev/null || echo unknown)"
  if [[ "$selmode" == "Disabled" ]]; then
    info "SELinux Disabled — bu step atlanıyor"
    return 0
  fi

  # ─── ÖNERİLEN: PERMISSIVE moduna geç ──────────────────────────────────────
  # Enforcing'de hosting panel sysapi cagrilari surekli yeni AVC denial
  # uretiyor (httpd_t → systemctl/nftables/dbus/audit_write transition'lari).
  # Production'da %70 panel Permissive cikar — diger katmanlar yeterli:
  # nftables ratelimit, firewalld, fail2ban, sudoers whitelist, ONOX_BIN
  # sertleştirme. SELinux MAC log'lanir ama block etmez.
  #
  # Enforcing kullanmak isteyenler manuel: sed -i 's/permissive/enforcing/'
  # /etc/selinux/config && setenforce 1 (sonra audit2allow -M ile policy uret)
  info "SELinux Permissive moduna geçiliyor (production-standard)"
  setenforce 0 2>/dev/null || true
  if [[ -f /etc/selinux/config ]]; then
    sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
    success "  /etc/selinux/config: SELINUX=permissive (kalıcı)"
  fi

  # checkmodule/semodule_package için policycoreutils-devel gerekli
  if ! command -v checkmodule &>/dev/null || ! command -v semodule_package &>/dev/null; then
    info "  policycoreutils-devel kuruluyor (SELinux policy builder)"
    dnf install -y policycoreutils-devel >> "$LOG" 2>&1 \
      || { warn "policycoreutils-devel kurulamadı — SELinux policy atlandı"; return 0; }
  fi

  # ─── RHEL OFFICIAL booleans (custom policy oncesi en temiz fix) ───────────
  # httpd_mod_auth_pam = apache → PAM authentication (audit_write capability)
  # httpd_execmem      = OPcache JIT mmap
  # domain_can_mmap_files = log_t dosyalarini mmap (var_log_t)
  for sebool in httpd_mod_auth_pam httpd_execmem domain_can_mmap_files; do
    setsebool -P "$sebool" on 2>/dev/null \
      && info "  setsebool ${sebool} = on (kalıcı)" \
      || warn "  setsebool ${sebool} başarısız (devam)"
  done

  local pol_dir=/var/lib/onoxsoft/selinux
  run mkdir -p "${pol_dir}"

  # ─── Policy 1: fail2ban → Laravel storage erişimi ───
  # fail2ban_t domain'i panel-login jail için /opt/onoxsoft/storage/logs/laravel*.log
  # (httpd_sys_rw_content_t) okuması lazım. Default policy denies.
  cat > "${pol_dir}/onoxsoft-fail2ban.te" <<'EOF'
module onoxsoft-fail2ban 1.0;
require {
    type fail2ban_t;
    type httpd_sys_rw_content_t;
    class dir { search read };
    class file { read open getattr };
}
allow fail2ban_t httpd_sys_rw_content_t:dir { search read };
allow fail2ban_t httpd_sys_rw_content_t:file { read open getattr };
EOF

  # ─── Policy 2: Apache sudo PAM → shadow read ───
  # /etc/pam.d/sudo'da apache bypass eklendi, AMA bazı PAM modülleri yine de
  # unix_chkpwd chain'i tetikliyor. Bu policy belt-and-suspenders.
  cat > "${pol_dir}/onoxsoft-apache-sudo.te" <<'EOF'
module onoxsoft-apache-sudo 1.0;
require {
    type httpd_t;
    type shadow_t;
    type chkpwd_exec_t;
    class file { read open getattr execute execute_no_trans };
    class capability { audit_write };
}
allow httpd_t shadow_t:file { read open getattr };
allow httpd_t chkpwd_exec_t:file { execute execute_no_trans };
allow httpd_t self:capability { audit_write };
EOF

  # Compile + install both modules (idempotent)
  local installed=0 skipped=0
  for mod in onoxsoft-fail2ban onoxsoft-apache-sudo; do
    if semodule -l 2>/dev/null | grep -q "^${mod}"; then
      info "  ${mod}: zaten kurulu (skip)"
      skipped=$((skipped + 1))
      continue
    fi
    if checkmodule -M -m -o "${pol_dir}/${mod}.mod" "${pol_dir}/${mod}.te" >> "$LOG" 2>&1 \
       && semodule_package -o "${pol_dir}/${mod}.pp" -m "${pol_dir}/${mod}.mod" >> "$LOG" 2>&1 \
       && semodule -i "${pol_dir}/${mod}.pp" >> "$LOG" 2>&1; then
      success "  ${mod}: yüklendi"
      installed=$((installed + 1))
    else
      warn "  ${mod}: compile/install başarısız — log: $LOG"
    fi
  done

  info "SELinux custom policy: ${installed} yeni + ${skipped} mevcut"
}

# ---------------------------------------------------------------------------
# STEP 16.3 — /var/lib/onox dizin yapısı (apache traversal-safe)
# GeoIP, lock, threats vd. apache user'in okuyabilmesi için 755 mode gerekiyor.
# Default 750 (root:root) → apache traversal yapamaz → GeoIP progress bar fail.
# ---------------------------------------------------------------------------
step_install_onox_dirs() {
  step "16.3/17  /var/lib/onox dizin yapısı (apache erişimli)"

  # Parent dir: 755 ki apache traversal yapabilsin (read content değil, sadece geç)
  run mkdir -p /var/lib/onox
  chmod 755 /var/lib/onox  # OTHERS x bit kritik (traversal)
  chown root:root /var/lib/onox

  # Alt dizinler: apache owned (write + read)
  for sub in geoip geoip/cidrs geoip/cidrs-v6 geoip/cache threats lock; do
    run mkdir -p "/var/lib/onox/${sub}"
    chown "${WEB_USER}:${WEB_USER}" "/var/lib/onox/${sub}" 2>/dev/null \
      || chown apache:apache "/var/lib/onox/${sub}" 2>/dev/null \
      || true
    chmod 755 "/var/lib/onox/${sub}"
  done

  # SELinux context — httpd_var_lib_t apache okuyabilsin
  if command -v semanage &>/dev/null; then
    semanage fcontext -a -t httpd_var_lib_t '/var/lib/onox(/.*)?' 2>/dev/null \
      || semanage fcontext -m -t httpd_var_lib_t '/var/lib/onox(/.*)?' 2>/dev/null \
      || true
    restorecon -R /var/lib/onox 2>/dev/null || true
    info "  SELinux context: httpd_var_lib_t /var/lib/onox(/.*)? eklendi"
  fi

  success "/var/lib/onox dizin yapısı oluşturuldu (755 + apache subdirs)"
}

# ---------------------------------------------------------------------------
# STEP 16.4 — GeoIP CIDR initial download (~250 ülke, ipdeny.com)
# Firewall "Son Aktivite" tab country lookup için. Olmazsa Ülke kolonu boş gelir.
# ---------------------------------------------------------------------------
step_install_geoip_cidrs() {
  step "16.4/17  GeoIP CIDR initial download"

  # Laravel base path tespit
  local laravel_base="${ONOX_HOME:-/opt/onoxsoft}"
  if [[ ! -f "${laravel_base}/artisan" ]]; then
    warn "Laravel artisan bulunamadı (${laravel_base}/artisan) — GeoIP atlanıyor"
    return 0
  fi

  # Cron için weekly schedule + initial download
  # --countries=all → ipdeny.com tüm ~250 ülke (5-10 dakika)
  # --install-cron  → /etc/cron.d/onoxsoft-geoip (Pazar 03:00 refresh)
  info "  GeoIP CIDR listeleri indiriliyor (~250 ülke, ipdeny.com) — 5-10dk"
  (
    cd "${laravel_base}"
    php artisan onox:install:geoip --countries=all --install-cron >> "$LOG" 2>&1 \
      && success "  GeoIP CIDR indirildi + cron kuruldu" \
      || warn "GeoIP indirme başarısız — manuel: php artisan onox:install:geoip"
  )

  # Owner check — /var/lib/onox/geoip/cidrs/ apache okumalı
  if [[ -d /var/lib/onox/geoip/cidrs ]]; then
    chown -R "${WEB_USER}:${WEB_USER}" /var/lib/onox/geoip/cidrs 2>/dev/null \
      || chown -R apache:apache /var/lib/onox/geoip/cidrs 2>/dev/null \
      || true
    local cidr_count=$(ls /var/lib/onox/geoip/cidrs/ 2>/dev/null | wc -l)
    info "  ${cidr_count} ülke CIDR dosyası kurulu"
  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 firewallcmd-rich-rules, nftables varsa
  # nftables-multiport, yoksa iptables-multiport.
  # NOT: "auto" geçerli bir banaction DEĞİLDİR; eski versiyonda yanlışlıkla
  # yazılmış ve "Found no accessible config files for 'action.d/auto'" hatası
  # üretmiş — bu yüzden eksplisit detection yapılır.
  local f2b_backend="iptables-multiport"
  if systemctl is-active --quiet firewalld 2>/dev/null \
     || systemctl is-enabled --quiet firewalld 2>/dev/null; then
    f2b_backend="firewallcmd-rich-rules"
  elif command -v nft &>/dev/null && [[ -f /etc/fail2ban/action.d/nftables-multiport.conf ]]; then
    f2b_backend="nftables-multiport"
  fi
  info "fail2ban banaction: ${f2b_backend}"

  # recidive jail için /var/log/fail2ban.log dosyası mevcut olmalı — yoksa
  # "Have not found any log file for recidive jail" hatasıyla fail2ban başlamaz.
  if [[ ! -f /var/log/fail2ban.log ]]; then
    touch /var/log/fail2ban.log
    chown root:adm /var/log/fail2ban.log 2>/dev/null || true
    chmod 0640 /var/log/fail2ban.log
  fi

  # panel-login jail Laravel log dosyasını okur — install.sh sırasında
  # Laravel henüz log yazmamış olabilir, fail2ban başlangıçta "log not found"
  # ile fail eder. Boş bir dosya oluştur ki fail2ban happy olsun.
  local laravel_log="${ONOX_HOME}/storage/logs/laravel.log"
  if [[ ! -f "${laravel_log}" ]]; then
    mkdir -p "${ONOX_HOME}/storage/logs"
    touch "${laravel_log}"
    chown "${WEB_USER}:${WEB_GROUP}" "${laravel_log}"
    chmod 0664 "${laravel_log}"
    # SELinux context — apache yazsın, fail2ban (root) okusun
    if command -v chcon &>/dev/null; then
      chcon -t httpd_log_t "${laravel_log}" 2>/dev/null || true
    fi
  fi

  # 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

  # ─── DNS NS varsayılan değerleri ───────────────────────────────────────
  # PANEL_HOSTNAME'den NS subdomain'lerini türet (örn. panel.onoxsoft.com.tr
  # → ns3.onoxsoft.com.tr, ns4.onoxsoft.com.tr). Sonradan Sistem Ayarları'ndan
  # değiştirilebilir. install.sh OS-level hesaplamasını burada yapıyor ki
  # ilk admin login öncesi DNS provision'lar default'la patlamasın.
  local root_domain ns1 ns2
  if [[ "$PANEL_HOSTNAME" =~ \.[a-zA-Z]+\.[a-zA-Z]{2,}$ ]]; then
    # panel.alanadi.com.tr → alanadi.com.tr
    root_domain=$(echo "$PANEL_HOSTNAME" | awk -F'.' '{n=NF; print $(n-2)"."$(n-1)"."$n}')
    # alanadi.com.tr (3 segmentli, son .com.tr)
    [[ ! "$root_domain" =~ ^[a-zA-Z0-9-]+\.(com|net|org|gen|info|biz)\.[a-z]{2,}$ ]] \
        && root_domain=$(echo "$PANEL_HOSTNAME" | awk -F'.' '{n=NF; print $(n-1)"."$n}')
    ns1="ns3.${root_domain}"
    ns2="ns4.${root_domain}"
  else
    ns1="ns3.onoxsoft.com.tr"
    ns2="ns4.onoxsoft.com.tr"
  fi

  info "  DNS NS varsayılan: ${ns1}, ${ns2}"

  mysql --defaults-file=/root/.onox-mysql-root.cnf onoxsoft_panel <<SQL 2>/dev/null || true
INSERT INTO server_settings (\`key\`, value, type, \`group\`, created_at, updated_at)
VALUES
  ('dns_ns1', '${ns1}', 'string', 'dns', NOW(), NOW()),
  ('dns_ns2', '${ns2}', 'string', 'dns', NOW(), NOW()),
  ('hostname', '${PANEL_HOSTNAME}', 'string', 'system', NOW(), NOW())
ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=NOW();
SQL

  # Hatırlatma: kullanıcı registrar'da NS A kayıtları + glue records ekler
  info ""
  info "  ÖNEMLİ — Manuel adımlar (kurulum sonrası):"
  info "  1) Domain registrar'ında (${root_domain:-domain.com}):"
  info "     ${ns1}  A  $(curl -4 -fsS --max-time 5 https://ipv4.icanhazip.com 2>/dev/null || echo SUNUCU_IP)"
  info "     ${ns2}  A  $(curl -4 -fsS --max-time 5 https://ipv4.icanhazip.com 2>/dev/null || echo SUNUCU_IP)"
  info "  2) TPMK/nic.tr'de glue records aynı IP'ye"
  info "  3) Müşteri domain'lerinin NS'leri: ${ns1}, ${ns2}"
}

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
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Antivirüs (ClamAV) — kapsamlı kurulum + feed'ler + FTP hook
# Paket kurulumu _install_packages_* içinde yapılır; bu fonksiyon
# step_install_sysapi_scripts SONRASI çağrılır (onx-clamav-sigupdate gerektirir).
# ---------------------------------------------------------------------------
enable_antivirus() {
    info "Antivirüs (ClamAV) kuruluyor…"
    local CLAMD_SVC="clamd@scan" FRESH_SVC="clamav-freshclam"
    if command -v dnf &>/dev/null; then
        # RHEL/AlmaLinux: EPEL + paketler + jq
        dnf install -y epel-release &>/dev/null || true
        dnf install -y jq clamav clamd clamav-update clamav-freshclam
        # 'Example' satırlarını kapat + clamd local socket'i aç (aksi halde clamd başlamaz)
        sed -i 's/^Example/#Example/' /etc/freshclam.conf /etc/clamd.d/scan.conf 2>/dev/null || true
        sed -i '/^#LocalSocket/s/^#//' /etc/clamd.d/scan.conf 2>/dev/null || true
        # SELinux: clamd'in /home'u taramasına izin ver (RHEL'de ŞART)
        setsebool -P antivirus_can_scan_system 1 2>/dev/null || true
    elif command -v apt-get &>/dev/null; then
        # Debian/Ubuntu
        DEBIAN_FRONTEND=noninteractive apt-get install -y jq clamav clamav-daemon clamav-freshclam
        CLAMD_SVC="clamav-daemon"
    else
        warn "Desteklenmeyen paket yöneticisi — ClamAV elle kurulmalı"; return 0
    fi
    mkdir -p /var/lib/onoxsoft/quarantine && chmod 700 /var/lib/onoxsoft/quarantine
    mkdir -p /var/log/onoxsoft
    # İlk imza DB (daemon'ı durdurup manuel freshclam)
    systemctl stop "$FRESH_SVC" &>/dev/null || true
    freshclam --quiet || true
    systemctl enable --now "$FRESH_SVC" &>/dev/null || true
    systemctl enable --now "$CLAMD_SVC" &>/dev/null || true
    # Ek imza feed'leri
    /usr/local/onoxsoft/bin/onx-clamav-sigupdate <<<'{}' || true
    # FTP on-upload tarama (Pure-FTPd) — pure-uploadscript systemd unit (reboot'a dayanıklı;
    # bare `-B` daemon reboot sonrası kaybolurdu).
    if command -v pure-uploadscript &>/dev/null; then
        PUS_BIN="$(command -v pure-uploadscript)"
        echo '/usr/local/onoxsoft/bin/onx-clamav-upload-scan' > /etc/pure-ftpd/conf/CallUploadScript 2>/dev/null || true
        cat >/etc/systemd/system/onoxsoft-pure-uploadscript.service <<UNIT
[Unit]
Description=OnoxSoft Pure-FTPd upload AV scanner
After=pure-ftpd.service
[Service]
ExecStart=${PUS_BIN} -r /usr/local/onoxsoft/bin/onx-clamav-upload-scan
Restart=on-failure
[Install]
WantedBy=multi-user.target
UNIT
        systemctl daemon-reload &>/dev/null || true
        systemctl enable --now onoxsoft-pure-uploadscript.service &>/dev/null || true
    fi
    info "Antivirüs kurulumu tamamlandı."
}

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_time_sync
  step_configure_firewall
  step_configure_selinux
  step_create_onoxsoft_user
  step_enable_disk_quota
  step_install_acme_sh
  step_setup_mariadb
  step_setup_powerdns
  step_setup_redis
  step_setup_pureftpd
  step_configure_mail_stack
  step_clone_panel
  step_setup_panel_vhost
  step_setup_panel_ssl
  step_install_sysapi_scripts
  [[ "$ENABLE_ANTIVIRUS" -eq 1 ]] && enable_antivirus || info "Antivirüs kurulumu atlandı (--no-antivirus)"
  step_install_sudoers
  step_install_selinux_policies        # 16.2 — KRİTİK: PAM cascade fix
  step_install_onox_dirs               # 16.3 — /var/lib/onox apache-traversable
  step_install_geoip_cidrs             # 16.4 — GeoIP CIDR initial (country lookup için)
  step_install_fail2ban_defaults
  step_init_firewall_infra
  step_install_queue_worker
  step_install_service_enforcement
  step_bootstrap_panel_domain        # v92 Agent 1 — DKIM + SPF/DMARC önerileri
  step_create_admin_user
  step_print_summary
}

# ---------------------------------------------------------------------------
# STEP 17 — Queue Worker (Horizon veya queue:work)
#
# Laravel async job'lar (AutoSSL, mailbox provision, backup, audit, vd.)
# için systemd unit. Horizon yüklüyse Horizon, değilse queue:work.
# ---------------------------------------------------------------------------
step_install_queue_worker() {
  step "17/17  Queue worker (systemd)"

  local has_horizon=0
  [[ -d "${ONOX_HOME}/vendor/laravel/horizon" ]] && has_horizon=1

  local svc_name php_bin
  php_bin="/usr/bin/php82"
  [[ -x "${php_bin}" ]] || php_bin="/usr/bin/php"

  if [[ $has_horizon -eq 1 ]]; then
    svc_name="onoxsoft-horizon"
    cat > "/etc/systemd/system/${svc_name}.service" <<EOF
[Unit]
Description=ONOXSOFT Laravel Horizon Queue Worker
After=network.target ${REDIS_SVC}.service ${MARIADB_SVC}

[Service]
Type=simple
User=${WEB_USER}
Group=${WEB_GROUP}
WorkingDirectory=${ONOX_HOME}
Environment="HOME=/tmp"
ExecStart=${php_bin} ${ONOX_HOME}/artisan horizon
ExecStop=${php_bin} ${ONOX_HOME}/artisan horizon:terminate
Restart=always
RestartSec=5
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target
EOF
  else
    svc_name="onoxsoft-queue"
    cat > "/etc/systemd/system/${svc_name}.service" <<EOF
[Unit]
Description=ONOXSOFT Laravel Queue Worker
After=network.target ${REDIS_SVC}.service ${MARIADB_SVC}

[Service]
Type=simple
User=${WEB_USER}
Group=${WEB_GROUP}
WorkingDirectory=${ONOX_HOME}
Environment="HOME=/tmp"
ExecStart=${php_bin} ${ONOX_HOME}/artisan queue:work --queue=provisioning,ssl,default,webhooks,backups,audit,notifications --tries=3 --timeout=300 --sleep=3
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  fi

  run systemctl daemon-reload
  run systemctl enable --now "${svc_name}"

  if systemctl is-active --quiet "${svc_name}"; then
    success "Queue worker çalışıyor: ${svc_name}"
  else
    warn "Queue worker başlatılamadı: ${svc_name} — manuel: systemctl status ${svc_name}"
  fi
}

# ════════════════════════════════════════════════════════════════════
# Step 18/18 — Service-level license enforcement (advisory mode default)
# ════════════════════════════════════════════════════════════════════
# Premium servisleri (postfix/dovecot/whmcs/ai/cluster/reseller) systemd
# ExecStartPre + onoxd watchdog + binary wrapper ile gate'ler.
#
# Advisory mode (varsayilan): lisans yoksa log + crack-report, servis durmaz
# Enforce mode: touch /opt/onoxsoft/.onox-enforce ile aktive olur, servis
#               baslamaz/durur. Production'da 1-2 hafta advisory'de gozle.
#
# Source dosyalar: ${ONOX_HOME}/scripts/license-enforcement/
# Installer:       php artisan onox:install:service-enforcement
step_install_service_enforcement() {
  step "18/18  License enforcement (advisory mode)"

  local php_bin
  php_bin="/usr/bin/php82"
  [[ -x "${php_bin}" ]] || php_bin="/usr/bin/php"

  local enforcement_dir="${ONOX_HOME}/scripts/license-enforcement"
  if [[ ! -d "${enforcement_dir}" ]]; then
    warn "license-enforcement scripts bulunamadi (${enforcement_dir}) — atlandi"
    return 0
  fi

  # Idempotent artisan installer (--no-wrappers Layer 3 atlanir, dnf reinstall
  # ile bozulma riski varsa kapali default. Operator istegine gore --enforce
  # da eklenebilir ama yeni kurulumda advisory default = guvenli)
  cd "${ONOX_HOME}" || { error "${ONOX_HOME} dizinine girilemedi"; return 1; }

  if run "${php_bin}" artisan onox:install:service-enforcement --no-wrappers; then
    success "Service enforcement kuruldu (advisory mode)"
    info "Log:     journalctl -u onoxd -f"
    info "Logs:    /var/log/onoxsoft/onoxd.log"
    info "Audit:   /var/log/onoxsoft/license-violations.log"
    info "Enforce: touch ${ONOX_HOME}/.onox-enforce (sonra: systemctl restart onoxd)"
  else
    warn "Service enforcement install fail oldu — manuel: php artisan onox:install:service-enforcement --dry-run"
  fi
}

# ════════════════════════════════════════════════════════════════════
# v92 Agent 1 — Panel system domain bootstrap (DKIM + SPF/DMARC önerileri)
# ════════════════════════════════════════════════════════════════════
# Panel kendi domain'i (örn. onoxsoft.com.tr / panel.onoxsoft.com.tr) bir
# Account değil → DomainAdded eventi fire olmaz → DKIM key + DNS TXT yok
# → sistem mail'leri DKIM=permerror. Bu adım idempotent: DKIM zaten
# varsa skip, DNS TXT yayınlıysa skip, sadece eksikleri tamamlar.
#
# install.sh hatasında kurulum bozulmasın diye `|| true` ile tolere edilir
# — admin sonradan `php artisan onoxsoft:panel:bootstrap --dry-run` ile
# elle inceleyebilir. Output'un son 30 satırı log'a düşer, geri kalanı
# /var/log/onoxsoft/panel-bootstrap.log (cron'un birden yazdığı dosya).
step_bootstrap_panel_domain() {
  step "Panel bootstrap (DKIM + SPF/DMARC önerileri) — v92"

  local php_bin
  php_bin="/usr/bin/php82"
  [[ -x "${php_bin}" ]] || php_bin="/usr/bin/php"

  cd "${ONOX_HOME}" || { warn "${ONOX_HOME} dizinine girilemedi — panel bootstrap atlandı"; return 0; }

  # --force flag: ilk çalışmada cache invalidate + her step deneme
  info "Komut: ${php_bin} artisan onoxsoft:panel:bootstrap --force"
  if "${php_bin}" artisan onoxsoft:panel:bootstrap --force 2>&1 | tee -a "$LOG" | tail -30; then
    success "Panel bootstrap tamam (DKIM/SPF/DMARC raporu yukarıda)"
  else
    warn "Panel bootstrap tamamlandı (uyarılar olabilir) — detay: $LOG"
  fi

  info "Tekrar çalıştırmak için: ${php_bin} artisan onoxsoft:panel:bootstrap --dry-run"
  info "Cron drift check: her gece 04:30 (routes/console.php)"
}

main "$@"
