#!/usr/bin/env bash
# =============================================================================
# pritunl_oneclick.sh — Pritunl 社区版 + MongoDB 一键部署脚本（全内网镜像源）
#
# 目标：一条命令完成 Pritunl VPN 服务端从零部署，交付形态：
#   bash -c "$(curl -sSL http://<镜像服务器>/pritunl/oneclick/pritunl_oneclick.sh)"
# 带参数形式：
#   curl -sSL <脚本地址> | bash -s -- --mirror http://172.19.1.162:18080/pritunl
#
# 子命令：install（默认）| selfcheck | status | version | help
#
# 设计要点：
#   1. 全内网镜像：pritunl / pritunl-ndppd / pkcs11-helper / openvpn / lzo /
#      mongodb-org 全家桶一律来自自建镜像（baseurl 指向内网文件服务器），
#      不访问 repo.pritunl.com / repo.mongodb.org / GitHub / EPEL 外网源；
#      基础依赖（python3/openssl/iptables 等）来自本机已配置的 OS 源，
#      可用 --os-baseurl 整体切换到内网 Rocky 镜像。
#   2. 自动识别 Rocky / AlmaLinux / RHEL / CentOS / Oracle Linux 8/9（x86_64）。
#   3. 失败重试：所有下载（断点续传 + SHA256 校验 + 指数退避重试）、
#      dnf 操作（3 次重试）、服务启动（等待循环）均有重试与明确报错。
#   4. 幂等不破坏：已安装则跳过安装只做校验；永不覆盖既有 /etc/pritunl.conf；
#      已存在 setup_key 不重新生成；防火墙只增不删；不改动已有管控平台。
#   5. 部署后自检：服务状态 + Web API 探活（/state 期望 401，证明 API 在线且
#      认证生效）+ MongoDB ping + 开机自检 systemd unit。
#
# 关联文档：docs/pritunl-oneclick-deploy.md（依赖清单 / 镜像结构 / 排障手册）
#
# 依赖清单（与 172.19.1.162 参考机实测一致，Pritunl 1.34.4763.45 / Rocky 9.8）：
#   —— 自建镜像提供 ——
#   pritunl-1.34.4763.45-1.el9         Pritunl 本体（repo.pritunl.com）
#   pritunl-ndppd-0.2.5-2.el9          NDProxy 守护进程（pritunl 依赖）
#   pkcs11-helper-1.31.0-3.el9         openvpn 的 PKCS#11 支持（EPEL）
#   openvpn-2.5.11-2.el9               OpenVPN 服务端（EPEL，pritunl 依赖）
#   lzo-2.10-7.el9                     openvpn 依赖（EPEL）
#   mongodb-org-7.0.45-1.el9           数据库全家桶 9 个 RPM（repo.mongodb.org）
#   —— OS 源提供（Rocky BaseOS/AppStream，可 --os-baseurl 切内网）——
#   python3 / openssl / ca-certificates / iptables-nft(提供 iptables) / ipset /
#   net-tools / psmisc / procps-ng / kernel-modules-extra(wireguard 模块)
# =============================================================================
set -Eeuo pipefail

# ---- 脚本自身版本 -----------------------------------------------------------------
readonly ONECLICK_VERSION="1.0.1"

# ---- 可调参数（命令行 > 环境变量 > 默认值）-----------------------------------------
MIRROR_BASE="${MIRROR_BASE:-http://172.19.1.162:18080/pritunl}"   # 自建镜像根 URL（团队测试镜像，生产必须用 --mirror 覆盖）
LOCAL_DIR="${LOCAL_DIR:-}"                    # 镜像已在本机时的目录（file:// 模式）
OS_BASEURL="${OS_BASEURL:-}"                  # 内网 Rocky 镜像根（可选，形如 http://rocky-mirror/rocky）
MONGO_VERSION="${MONGO_VERSION:-7.0.45}"      # mongodb-org 版本
PRITUNL_VERSION="${PRITUNL_VERSION:-1.34.4763.45}"  # pritunl 版本
VPN_PORTS="${VPN_PORTS:-10000-20000/udp}"     # VPN 数据端口（按需扩充，空格分隔）
REINSTALL=0                                   # --reinstall 强制重装
ENABLE_FIREWALL=0                             # --enable-firewall 启用未运行的 firewalld
INSTALL_FIREWALL=0                            # --install-firewall 自动安装 firewalld
SKIP_FIREWALL=0                               # --skip-firewall 跳过防火墙配置
WITH_CP=0                                     # --with-control-platform 顺带部署管控平台
DL_ATTEMPTS="${DL_ATTEMPTS:-5}"               # 单文件下载重试次数
DL_RETRY_DELAY="${DL_RETRY_DELAY:-3}"         # 下载重试基础退避（秒，逐次翻倍）
DNF_ATTEMPTS="${DNF_ATTEMPTS:-3}"             # dnf 操作重试次数
SVC_WAIT="${SVC_WAIT:-90}"                    # 服务就绪等待上限（秒）
SUBCOMMAND="install"

# ---- 内部状态 ---------------------------------------------------------------------
CURRENT_STAGE="初始化"
LOG_DIR="/var/log/pritunl-oneclick"
LOG_FILE=""
WORK_DIR=""
WEB_PORT="443"
API_CODE=""
API_BODY=""
SETUP_KEY=""
OS_ID="" OS_VERSION="" EL_MAJOR="" EL_DIR="" ARCH=""
DNF_BIN="dnf"

# ---- 基础日志 ---------------------------------------------------------------------
ts() { date '+%Y-%m-%d %H:%M:%S'; }

_plain() { printf '%s\n' "$*"; }

log_info()  { printf '[%s] [INFO] %s\n'  "$(ts)" "$*"; [ -n "${LOG_FILE:-}" ] && printf '[%s] [INFO] %s\n'  "$(ts)" "$*" >>"$LOG_FILE" || true; }
log_ok()    { printf '[%s] [ OK ] %s\n'  "$(ts)" "$*"; [ -n "${LOG_FILE:-}" ] && printf '[%s] [ OK ] %s\n'  "$(ts)" "$*" >>"$LOG_FILE" || true; }
log_warn()  { printf '[%s] [WARN] %s\n'  "$(ts)" "$*"; [ -n "${LOG_FILE:-}" ] && printf '[%s] [WARN] %s\n'  "$(ts)" "$*" >>"$LOG_FILE" || true; }
log_err()   { printf '[%s] [FAIL] %s\n'  "$(ts)" "$*" >&2; [ -n "${LOG_FILE:-}" ] && printf '[%s] [FAIL] %s\n' "$(ts)" "$*" >>"$LOG_FILE" || true; }

stage() { CURRENT_STAGE="$*"; printf '\n========== [%s] %s ==========\n' "$(ts)" "$*"; [ -n "${LOG_FILE:-}" ] && printf '\n========== [%s] %s ==========\n' "$(ts)" "$*" >>"$LOG_FILE" || true; }

die() {
  log_err "$*"
  if [ -n "${LOG_FILE:-}" ]; then
    log_err "阶段=${CURRENT_STAGE}，完整日志：$LOG_FILE"
    log_err "排障入口：curl -sSI <镜像根>/yum/<el>/pritunl/repodata/repomd.xml；journalctl -u mongod -u pritunl --no-pager"
  fi
  exit 1
}

usage() {
  cat <<'USAGE'
pritunl_oneclick.sh — Pritunl + MongoDB 一键部署（全内网镜像源）

用法：
  bash -c "$(curl -sSL http://<镜像服务器>/pritunl/oneclick/pritunl_oneclick.sh)"
  curl -sSL <脚本地址> | bash -s -- [选项]

子命令：
  install     部署 Pritunl（默认）
  selfcheck   运行开机自检脚本（部署后可用）
  status      查看服务与 API 状态
  version     打印脚本版本

选项：
  --mirror URL            自建镜像根 URL（默认 http://172.19.1.162:18080/pritunl）
  --local-dir PATH        镜像树已在本机的目录（离线安装，等价 file:// URL）
  --os-baseurl URL        内网 Rocky 镜像根（切换 BaseOS/AppStream/Extras 源）
  --mongo-version VER     mongodb-org 版本（默认 7.0.45）
  --pritunl-version VER   pritunl 版本（默认 1.34.4763.45）
  --vpn-ports SPEC        VPN 数据端口，空格分隔（默认 10000-20000/udp）
  --reinstall             强制重装（默认已安装则跳过安装只做校验）
  --enable-firewall       firewalld 已安装但未运行时，启用并配置
  --install-firewall      firewalld 未安装时自动安装
  --skip-firewall         跳过防火墙配置
  --with-control-platform 顺带部署 Pritunl 管控平台（镜像内离线包）
  -h, --help              显示本帮助

环境变量（与选项等价）：MIRROR_BASE / LOCAL_DIR / OS_BASEURL / MONGO_VERSION /
  PRITUNL_VERSION / VPN_PORTS / DL_ATTEMPTS / DNF_ATTEMPTS / SVC_WAIT

示例：
  # 团队测试镜像
  bash -c "$(curl -sSL http://172.19.1.162:18080/pritunl/oneclick/pritunl_oneclick.sh)"
  # 生产镜像 + 管控平台
  bash -c "$(curl -sSL http://mirror.example.com/pritunl/oneclick/pritunl_oneclick.sh)" -- --mirror http://mirror.example.com/pritunl --with-control-platform
USAGE
}

# ---- 参数解析 ---------------------------------------------------------------------
parse_args() {
  while [ $# -gt 0 ]; do
    case "$1" in
      install|selfcheck|status|version|help|-h|--help)
        if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then SUBCOMMAND="help"; else SUBCOMMAND="$1"; fi
        shift ;;
      --mirror)              [ $# -ge 2 ] || die "选项 $1 缺少参数"; MIRROR_BASE="$2"; shift 2 ;;
      --local-dir)           [ $# -ge 2 ] || die "选项 $1 缺少参数"; LOCAL_DIR="$2"; shift 2 ;;
      --os-baseurl)          [ $# -ge 2 ] || die "选项 $1 缺少参数"; OS_BASEURL="$2"; shift 2 ;;
      --mongo-version)       [ $# -ge 2 ] || die "选项 $1 缺少参数"; MONGO_VERSION="$2"; shift 2 ;;
      --pritunl-version)     [ $# -ge 2 ] || die "选项 $1 缺少参数"; PRITUNL_VERSION="$2"; shift 2 ;;
      --vpn-ports)           [ $# -ge 2 ] || die "选项 $1 缺少参数"; VPN_PORTS="$2"; shift 2 ;;
      --reinstall)           REINSTALL=1; shift ;;
      --enable-firewall)     ENABLE_FIREWALL=1; shift ;;
      --install-firewall)    INSTALL_FIREWALL=1; shift ;;
      --skip-firewall)       SKIP_FIREWALL=1; shift ;;
      --with-control-platform) WITH_CP=1; shift ;;
      *) usage; die "未知参数：$1" ;;
    esac
  done
}

# ---- 执行辅助 ---------------------------------------------------------------------
# run：执行命令，输出进日志文件；失败时打印日志尾部并返回原退出码
run() {
  printf '%s\n' "+ $*" >>"$LOG_FILE"
  if "$@" >>"$LOG_FILE" 2>&1; then
    return 0
  fi
  local rc=$?
  log_err "命令执行失败（rc=$rc）：$*"
  tail -n 15 "$LOG_FILE" | sed 's/^/    | /' >&2 || true
  return "$rc"
}

# retry_cmd：指数退避重试。用法 retry_cmd <次数> <基础延迟秒> <命令...>
retry_cmd() {
  local attempts=$1 delay=$2
  shift 2
  local i=1
  while :; do
    if "$@" >>"$LOG_FILE" 2>&1; then
      return 0
    fi
    local rc=$?
    if [ "$i" -ge "$attempts" ]; then
      log_err "命令连续 ${attempts} 次失败（rc=$rc）：$*"
      tail -n 20 "$LOG_FILE" | sed 's/^/    | /' >&2 || true
      return "$rc"
    fi
    log_warn "第 ${i}/${attempts} 次尝试失败（rc=$rc），${delay}s 后重试：$*"
    sleep "$delay"
    i=$((i + 1))
    delay=$((delay * 2))
  done
}

# fetch_file：下载单个文件（断点续传 + 指数退避重试 + 可选 SHA256 校验）
# 用法：fetch_file <url|file://path> <目标路径> [期望sha256]
fetch_file() {
  local url=$1 dest=$2 expected_sha=${3:-}
  local attempt=1 delay=$DL_RETRY_DELAY
  while [ "$attempt" -le "$DL_ATTEMPTS" ]; do
    local ok=0
    if [[ "$url" == file://* ]]; then
      if cp -f "${url#file://}" "$dest" 2>>"$LOG_FILE"; then ok=1; fi
    else
      if curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors \
           --speed-time 30 --speed-limit 10240 -C - \
           -o "$dest" "$url" 2>>"$LOG_FILE"; then ok=1; fi
    fi
    if [ "$ok" -eq 1 ] && [ -n "$expected_sha" ]; then
      if printf '%s  %s\n' "$expected_sha" "$dest" | sha256sum -c --status 2>>"$LOG_FILE"; then
        :
      else
        log_warn "SHA256 校验失败（第 ${attempt} 次）：$url"
        ok=0
      fi
    fi
    if [ "$ok" -eq 1 ] && [ -s "$dest" ]; then
      log_ok "已获取 $(basename "$dest")（$(du -h "$dest" | cut -f1)）"
      return 0
    fi
    rm -f "$dest"
    if [ "$attempt" -lt "$DL_ATTEMPTS" ]; then
      log_warn "获取失败（第 ${attempt}/${DL_ATTEMPTS} 次），${delay}s 后重试：$url"
      sleep "$delay"
      delay=$((delay * 2))
    fi
    attempt=$((attempt + 1))
  done
  die "获取失败（已重试 ${DL_ATTEMPTS} 次）：$url"
}

wait_tcp() {
  local host=$1 port=$2 timeout=$3 waited=0
  while [ "$waited" -lt "$timeout" ]; do
    if timeout 3 bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null; then
      return 0
    fi
    sleep 2
    waited=$((waited + 2))
  done
  return 1
}

wait_unit() {
  local unit=$1 timeout=$2 waited=0
  while [ "$waited" -lt "$timeout" ]; do
    if systemctl is-active "$unit" >/dev/null 2>&1; then
      return 0
    fi
    sleep 2
    waited=$((waited + 2))
  done
  return 1
}

wait_file() {
  local path=$1 timeout=$2 waited=0
  while [ "$waited" -lt "$timeout" ]; do
    [ -s "$path" ] && return 0
    sleep 2
    waited=$((waited + 2))
  done
  return 1
}

# ---- 系统识别 ---------------------------------------------------------------------
detect_os() {
  [ -r /etc/os-release ] || die "无法读取 /etc/os-release，不支持的系统"
  OS_ID="$(. /etc/os-release && printf '%s' "${ID:-unknown}")"
  OS_VERSION="$(. /etc/os-release && printf '%s' "${VERSION_ID:-unknown}")"
  EL_MAJOR="${OS_VERSION%%.*}"
  ARCH="$(uname -m)"

  case "$OS_ID" in
    rocky|almalinux|rhel|centos|oraclelinux|anolis|opencloudos|kylin|uniontech) : ;;
    *) die "当前发行版 ${OS_ID} 不在支持列表（ rocky / almalinux / rhel / centos / oraclelinux 8/9 ）。Debian/Ubuntu 请使用 apt 版脚本（规划中）。" ;;
  esac
  case "$EL_MAJOR" in
    8) EL_DIR="el8" ;;
    9) EL_DIR="el9" ;;
    *) die "不支持的 EL 大版本：${OS_VERSION}（仅支持 8/9）" ;;
  esac
  case "$ARCH" in
    x86_64) : ;;
    aarch64) die "检测到 ${ARCH}，当前自建镜像仅提供 x86_64 制品，请先补充对应架构 RPM 后再部署。" ;;
    *) die "不支持的 CPU 架构：${ARCH}" ;;
  esac
  log_ok "系统识别：${OS_ID} ${OS_VERSION}（${EL_DIR} / ${ARCH}）"
}

mirror_root() {
  if [ -n "$LOCAL_DIR" ]; then
    printf 'file://%s' "${LOCAL_DIR%/}"
  else
    printf '%s' "${MIRROR_BASE%/}"
  fi
}

# ---- 阶段：环境预检 -----------------------------------------------------------------
preflight() {
  stage "环境预检"
  [ "$(id -u)" = "0" ] || die "必须使用 root 运行（可用 sudo -i 切换）"
  command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ] \
    || die "未检测到 systemd（可能在容器内），Pritunl 部署依赖 systemd 服务管理"
  command -v dnf >/dev/null 2>&1 || DNF_BIN="yum"

  # curl：镜像下载的载体。缺失且非纯本地模式时先尝试用 OS 源安装
  if ! command -v curl >/dev/null 2>&1; then
    if [ -n "$LOCAL_DIR" ]; then
      log_warn "未安装 curl，本地模式继续"
    else
      log_warn "未安装 curl，尝试通过 OS 源安装"
      retry_cmd 2 3 "$DNF_BIN" install -y curl || die "curl 安装失败，请手动安装后重试"
    fi
  fi

  # dnf-plugins-core：提供 config-manager（关闭官方慢源用）；装不上不影响主流程（有 sed 兜底）
  if ! "$DNF_BIN" config-manager --help >/dev/null 2>&1; then
    retry_cmd 1 3 "$DNF_BIN" install -y dnf-plugins-core || log_warn "dnf-plugins-core 安装失败，官方源将用 sed 方式关闭"
  fi

  # 磁盘：/tmp（下载暂存）>= 2G，/var/lib（数据库）>= 3G
  local tmp_free var_free
  tmp_free="$(df -Pm /tmp | awk 'NR==2{print $4}')"
  var_free="$(df -Pm /var/lib | awk 'NR==2{print $4}')"
  [ "${tmp_free:-0}" -ge 2048 ] || log_warn "/tmp 可用空间不足 2G（${tmp_free}MB），大包下载可能失败"
  [ "${var_free:-0}" -ge 3072 ] || log_warn "/var/lib 可用空间不足 3G（${var_free}MB），MongoDB 数据目录可能紧张"

  # 内存：mongod 建议 >= 1.5G（仅警告）
  local mem_mb
  mem_mb="$(awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo)"
  [ "${mem_mb:-0}" -ge 1536 ] || log_warn "物理内存 ${mem_mb}MB 偏小，MongoDB 建议 >= 1.5G"

  # SELinux：仅报告，不自动改策略（避免破坏既有安全策略）
  if command -v getenforce >/dev/null 2>&1; then
    local se
    se="$(getenforce 2>/dev/null || echo unknown)"
    if [ "$se" = "Enforcing" ]; then
      log_warn "SELinux 为 Enforcing；若 Pritunl 启动异常请评估 setenforce 0 或补充 audit2allow 策略（脚本不自动修改安全策略）"
    else
      log_ok "SELinux：${se}"
    fi
  fi

  # 工作目录与日志
  WORK_DIR="$(mktemp -d /tmp/pritunl-oneclick.XXXXXX)"
  mkdir -p "$LOG_DIR"
  LOG_FILE="${LOG_DIR}/deploy-$(date +%Y%m%d-%H%M%S).log"
  {
    printf '# pritunl_oneclick.sh %s\n' "$ONECLICK_VERSION"
    printf '# 时间：%s\n' "$(ts)"
    printf '# 参数：MIRROR_BASE=%s LOCAL_DIR=%s MONGO_VERSION=%s PRITUNL_VERSION=%s VPN_PORTS=%s\n' \
      "${MIRROR_BASE}" "${LOCAL_DIR:-}" "$MONGO_VERSION" "$PRITUNL_VERSION" "$VPN_PORTS"
    printf '# 系统：%s %s %s\n' "$OS_ID" "$OS_VERSION" "$ARCH"
  } >>"$LOG_FILE"
  log_ok "日志文件：$LOG_FILE"
}

# ---- 阶段：配置自建镜像源 -----------------------------------------------------------
configure_mirror() {
  stage "配置自建镜像源"
  local base root_url
  root_url="$(mirror_root)"
  base="${root_url}/yum/${EL_DIR}"

  cat > /etc/yum.repos.d/00-pritunl-mirror.repo <<EOF
# 由 pritunl_oneclick.sh 生成 —— 全部指向自建镜像，禁止走外网官方源
[pritunl-mirror]
name=Pritunl Mirror (self-hosted)
baseurl=${base}/pritunl
enabled=1
gpgcheck=0
repo_gpgcheck=0

[mongodb-mirror]
name=MongoDB ${MONGO_VERSION} Mirror (self-hosted)
baseurl=${base}/mongodb-org-${MONGO_VERSION%.*}
enabled=1
gpgcheck=0
repo_gpgcheck=0

[openvpn-mirror]
name=OpenVPN Mirror (self-hosted, EPEL stack)
baseurl=${base}/epel-openvpn
enabled=1
gpgcheck=0
repo_gpgcheck=0
EOF
  log_ok "已写入 /etc/yum.repos.d/00-pritunl-mirror.repo（baseurl=${base}/...）"

  # 关闭外网官方慢源，确保 Pritunl 相关组件只能来自自建镜像
  # 优先 config-manager；不可用则对已知官方仓库文件做 sed 兜底
  local official_disabled=0
  if "$DNF_BIN" config-manager --set-disabled pritunl mongodb-org-7.0 epel epel-cisco-openh264 >>"$LOG_FILE" 2>&1; then
    official_disabled=1
  fi
  if [ "$official_disabled" != "1" ]; then
    local rf
    for rf in /etc/yum.repos.d/pritunl.repo /etc/yum.repos.d/mongodb-org-7.0.repo \
              /etc/yum.repos.d/epel.repo /etc/yum.repos.d/epel-cisco-openh264.repo; do
      if [ -f "$rf" ]; then
        sed -i 's/^enabled=1/enabled=0/' "$rf"
        log_warn "已用 sed 关闭官方源文件：$rf"
      fi
    done
    [ "$official_disabled" = "1" ] || log_warn "config-manager 不可用或部分官方源不存在（不影响部署）"
  else
    log_ok "已关闭外网官方源（pritunl / mongodb-org-7.0 / epel）"
  fi

  # 可选：OS 基础源切换到内网 Rocky 镜像（完全离线场景）
  if [ -n "$OS_BASEURL" ]; then
    local os_root="${OS_BASEURL%/}"
    cat > /etc/yum.repos.d/00-rocky-mirror.repo <<EOF
[baseos-mirror]
name=Rocky Linux ${EL_MAJOR} BaseOS (internal mirror)
baseurl=${os_root}/Rocky/${EL_MAJOR}/BaseOS/${ARCH}/os/
enabled=1
gpgcheck=0

[appstream-mirror]
name=Rocky Linux ${EL_MAJOR} AppStream (internal mirror)
baseurl=${os_root}/Rocky/${EL_MAJOR}/AppStream/${ARCH}/os/
enabled=1
gpgcheck=0

[extras-mirror]
name=Rocky Linux ${EL_MAJOR} Extras (internal mirror)
baseurl=${os_root}/Rocky/${EL_MAJOR}/Extras/${ARCH}/os/
enabled=1
gpgcheck=0
EOF
    log_ok "已切换 OS 基础源到内网镜像：${os_root}"
  fi

  # 刷新缓存（重试）
  retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" makecache --refresh \
    || die "dnf makecache 失败，请检查镜像服务器与网络"

  # 连通性验证：三个镜像仓库的 repomd.xml 必须可取回
  local repo sub
  for sub in "pritunl" "mongodb-org-${MONGO_VERSION%.*}" "epel-openvpn"; do
    repo="${base}/${sub}/repodata/repomd.xml"
    fetch_file "$repo" "${WORK_DIR}/repomd-${sub}.xml" \
      || die "镜像仓库不可达：${repo}（请确认镜像服务已启动、URL 正确、目录结构完整）"
    rm -f "${WORK_DIR}/repomd-${sub}.xml"
  done
  log_ok "镜像源连通性验证通过"
}

# ---- 阶段：安装依赖与组件 -----------------------------------------------------------
install_packages() {
  stage "安装依赖与 Pritunl / MongoDB"

  # 1) OS 基础依赖（来自 OS 源；已安装的 dnf 自动跳过）
  log_info "安装 OS 基础依赖（python3/openssl/iptables-nft/ipset/net-tools/psmisc/procps-ng 等）"
  retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" install -y \
    dnf-plugins-core python3 openssl ca-certificates \
    iptables-nft ipset net-tools psmisc procps-ng \
    || die "OS 基础依赖安装失败（可尝试 --os-baseurl 切换到内网 Rocky 镜像）"

  # 2) wireguard 内核模块：优先精确匹配运行内核，失败再装通用包
  if ! modinfo wireguard >/dev/null 2>&1; then
    log_info "安装 kernel-modules-extra（wireguard 内核模块，内核 $(uname -r)）"
    retry_cmd 2 3 "$DNF_BIN" install -y "kernel-modules-extra-$(uname -r)" \
      || retry_cmd 1 3 "$DNF_BIN" install -y kernel-modules-extra \
      || log_warn "kernel-modules-extra 安装失败，WireGuard 协议可能不可用（OpenVPN 不受影响）"
  fi

  # 3) OpenVPN 栈（EPEL 制品，来自自建镜像；lzo/pkcs11-helper 自动解析）
  if ! rpm -q openvpn >/dev/null 2>&1 || [ "$REINSTALL" = "1" ]; then
    log_info "安装 OpenVPN（自建镜像，EPEL 栈）"
    retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" install -y openvpn \
      || die "openvpn 安装失败（检查镜像 epel-openvpn 仓库是否包含 openvpn/lzo/pkcs11-helper）"
  else
    log_ok "openvpn 已安装（$(rpm -q --qf '%{VERSION}' openvpn)），跳过"
  fi

  # 4) Pritunl 本体
  if rpm -q "pritunl-${PRITUNL_VERSION}" >/dev/null 2>&1 && [ "$REINSTALL" != "1" ]; then
    log_ok "pritunl ${PRITUNL_VERSION} 已安装，跳过（--reinstall 可强制重装）"
  else
    log_info "安装 Pritunl ${PRITUNL_VERSION} + pritunl-ndppd（自建镜像）"
    retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" install -y \
      "pritunl-${PRITUNL_VERSION}" pritunl-ndppd \
      || die "pritunl 安装失败（检查镜像 pritunl 仓库是否包含 pritunl/pritunl-ndppd/pkcs11-helper）"
  fi

  # 5) MongoDB
  if rpm -q "mongodb-org-${MONGO_VERSION}" >/dev/null 2>&1 && [ "$REINSTALL" != "1" ]; then
    log_ok "mongodb-org ${MONGO_VERSION} 已安装，跳过（--reinstall 可强制重装）"
  else
    log_info "安装 MongoDB ${MONGO_VERSION} 全家桶（自建镜像）"
    retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" install -y "mongodb-org-${MONGO_VERSION}" \
      || die "mongodb-org 安装失败（检查镜像 mongodb-org-${MONGO_VERSION%.*} 仓库完整性）"
  fi

  # 安装结果核验
  local p
  for p in "pritunl-${PRITUNL_VERSION}" pritunl-ndppd openvpn "mongodb-org-${MONGO_VERSION}"; do
    rpm -q "$p" >/dev/null 2>&1 || die "安装后核验失败：$p 未正确安装"
  done
  log_ok "组件安装核验通过：pritunl ${PRITUNL_VERSION} / openvpn $(rpm -q --qf '%{VERSION}' openvpn) / mongodb-org ${MONGO_VERSION}"
}

# ---- 阶段：部署数据库 ---------------------------------------------------------------
setup_mongodb() {
  stage "部署 MongoDB 服务"
  run systemctl enable mongod || true
  if ! systemctl is-active mongod >/dev/null 2>&1; then
    retry_cmd 2 3 systemctl start mongod || die "mongod 启动失败，请查看 journalctl -u mongod"
  fi
  wait_unit mongod "$SVC_WAIT" || die "mongod 未在 ${SVC_WAIT}s 内就绪"
  wait_tcp 127.0.0.1 27017 30 || die "mongod 未监听 127.0.0.1:27017"

  if command -v mongosh >/dev/null 2>&1; then
    if retry_cmd 3 3 mongosh --quiet --host 127.0.0.1 --port 27017 --eval 'db.adminCommand({ping:1})'; then
      log_ok "MongoDB ping 正常（127.0.0.1:27017）"
    else
      die "MongoDB ping 失败"
    fi
  fi

  # 安全提醒：mongod 不应对外网开放
  if ss -tlnp 2>/dev/null | grep -q '0.0.0.0:27017\|:::27017'; then
    log_warn "mongod 监听在非回环地址，建议在 /etc/mongod.conf 将 net.bindIp 收敛为 127.0.0.1"
  else
    log_ok "mongod 仅监听回环地址（符合最小暴露原则）"
  fi
}

# ---- 阶段：部署 Pritunl 服务 --------------------------------------------------------
detect_web_port() {
  local p=""
  if [ -r /var/lib/pritunl/pritunl_web_env ]; then
    p="$(sed -n 's/^BIND_PORT=//p' /var/lib/pritunl/pritunl_web_env | head -n1)"
  fi
  if [ -z "$p" ] && [ -r /etc/pritunl.conf ]; then
    p="$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9]\{1,5\}\).*/\1/p' /etc/pritunl.conf | head -n1)"
  fi
  WEB_PORT="${p:-443}"
}

setup_pritunl() {
  stage "部署 Pritunl 服务"

  # 1) wireguard 内核模块 + 持久化
  if modinfo wireguard >/dev/null 2>&1; then
    modprobe wireguard 2>/dev/null || log_warn "wireguard 模块加载失败（可重启后重试）"
    if ! grep -qs '^wireguard$' /etc/modules-load.d/wireguard.conf 2>/dev/null; then
      printf 'wireguard\n' > /etc/modules-load.d/wireguard.conf
    fi
    log_ok "wireguard 内核模块就绪并已配置开机加载"
  else
    log_warn "未找到 wireguard 内核模块，WireGuard 协议服务器将无法启动（OpenVPN 不受影响）"
  fi

  # 2) IP 转发（VPN 路由必需；pritunl 运行期也会设置，此处做持久化）
  if [ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo 0)" != "1" ]; then
    printf 'net.ipv4.ip_forward = 1\n' > /etc/sysctl.d/99-pritunl.conf
    sysctl -w net.ipv4.ip_forward=1 >>"$LOG_FILE" 2>&1 || log_warn "net.ipv4.ip_forward 设置失败"
    log_ok "已持久化 net.ipv4.ip_forward=1"
  else
    log_ok "net.ipv4.ip_forward 已为 1"
  fi

  # 3) 启动 pritunl 守护进程
  #    注意：Pritunl 1.34.x 的 pritunl.service 会自行拉起 pritunl-web 子进程，
  #    不存在也不需独立操作 pritunl-web.service（该 unit 为 static 且依赖
  #    /var/lib/pritunl/pritunl_web_env 文件，本版本不单独生成该文件）。
  run systemctl enable pritunl || true
  if ! systemctl is-active pritunl >/dev/null 2>&1; then
    retry_cmd 2 3 systemctl start pritunl || die "pritunl 守护进程启动失败，请查看 journalctl -u pritunl"
  fi
  wait_unit pritunl "$SVC_WAIT" || die "pritunl 守护进程未在 ${SVC_WAIT}s 内就绪"
  log_ok "pritunl 守护进程已启动（Web 由其自行托管，无需独立 pritunl-web.service）"

  # 4) 等待 Web 端口就绪（pritunl 启动后数秒内在 443 / /etc/pritunl.conf 端口监听 HTTPS）
  detect_web_port
  wait_tcp 127.0.0.1 "$WEB_PORT" 30 || die "Web 端口 ${WEB_PORT} 未监听（检查 /etc/pritunl.conf 与 journalctl -u pritunl）"
  log_ok "Web 服务监听 127.0.0.1:${WEB_PORT}"

  # 5) API 探活（见下方循环）

  local attempt=1
  while [ "$attempt" -le 5 ]; do
    API_CODE="$(curl -sk -o "${WORK_DIR}/api_state.json" -w '%{http_code}' \
      --max-time 15 "https://127.0.0.1:${WEB_PORT}/state" 2>>"$LOG_FILE" || echo 000)"
    [ "$API_CODE" = "401" ] && break
    log_warn "API 探活返回 ${API_CODE}（第 ${attempt}/5 次，期望 401）"
    sleep 3
    attempt=$((attempt + 1))
  done
  API_BODY="$(head -c 200 "${WORK_DIR}/api_state.json" 2>/dev/null || true)"
  [ "$API_CODE" = "401" ] \
    || die "Web API 探活失败（/state -> ${API_CODE}），Pritunl 服务异常"
  log_ok "Web API 探活通过：GET /state -> 401（API 在线，认证生效，响应：${API_BODY}）"

  # 6) 初始化 setup-key（仅全新部署；已存在则不重新生成，避免破坏既有认证）
  if [ -f /var/lib/pritunl/setup_key ]; then
    log_ok "setup_key 已存在，跳过生成（保护既有管理员认证）"
  else
    SETUP_KEY="$(pritunl setup-key 2>>"$LOG_FILE" | tail -n1 || true)"
    if [ -n "$SETUP_KEY" ]; then
      log_ok "已生成 setup-key：${SETUP_KEY}（首次 Web 登录创建管理员时使用）"
    else
      log_warn "setup-key 生成失败，可部署后手动执行 pritunl setup-key"
    fi
  fi
}

# ---- 阶段：防火墙 -------------------------------------------------------------------
setup_firewall() {
  stage "配置防火墙"
  if [ "$SKIP_FIREWALL" = "1" ]; then
    log_warn "按参数跳过防火墙配置"
    return 0
  fi
  if ! command -v firewall-cmd >/dev/null 2>&1; then
    if [ "$INSTALL_FIREWALL" = "1" ]; then
      retry_cmd "$DNF_ATTEMPTS" 3 "$DNF_BIN" install -y firewalld || die "firewalld 安装失败"
    else
      log_warn "未安装 firewalld，跳过防火墙配置（--install-firewall 可自动安装）"
      return 0
    fi
  fi
  if ! firewall-cmd --state >/dev/null 2>&1; then
    if [ "$ENABLE_FIREWALL" = "1" ]; then
      run systemctl enable --now firewalld || die "firewalld 启用失败"
      sleep 3
    else
      log_warn "firewalld 未运行，跳过端口配置（--enable-firewall 可启用；当前网络策略保持不变）"
      return 0
    fi
  fi

  # 只增不删：绝不 flush、绝不删除既有端口/规则
  local specs=("80/tcp" "443/tcp")
  if [ "$WEB_PORT" != "80" ] && [ "$WEB_PORT" != "443" ]; then
    specs+=("${WEB_PORT}/tcp")
  fi
  # VPN_PORTS 形如 "10000-20000/udp 500/udp"，按空格拆分为多条
  local extra
  for extra in $VPN_PORTS; do
    [ -n "$extra" ] && specs+=("$extra")
  done
  local s
  for s in "${specs[@]}"; do
    if firewall-cmd --permanent --query-port="$s" >/dev/null 2>&1; then
      log_ok "端口 ${s} 已放通"
    else
      run firewall-cmd --permanent --add-port="$s" || log_warn "端口 ${s} 放通失败"
    fi
  done
  run firewall-cmd --reload || log_warn "firewalld reload 失败"
  log_ok "防火墙配置完成（additive only，未改动既有策略）"
}

# ---- 阶段：开机自检 -----------------------------------------------------------------
install_selfcheck() {
  stage "安装开机自检"

  cat > /usr/local/sbin/pritunl-onecheck <<'ONECHECK_EOF'
#!/usr/bin/env bash
# pritunl-onecheck — Pritunl 一键部署开机自检（由 pritunl-onecheck.service 调用，也可手动执行）
# 检查项：mongod/pritunl 服务、Web API 探活（/state 期望 401）、MongoDB ping、wireguard 模块
set -u
LOG_DIR=/var/log/pritunl-oneclick
mkdir -p "$LOG_DIR"
TS="$(date +%Y%m%d-%H%M%S)"
LOG="${LOG_DIR}/onecheck-${TS}.log"
FAIL=0
say() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; }

check_unit() {
  if systemctl is-active "$1" >/dev/null 2>&1; then
    say "OK   服务 $1 运行中"
  else
    say "FAIL 服务 $1 未运行（journalctl -u $1）"
    FAIL=1
  fi
}

# Web 端口识别：pritunl_web_env(BIND_PORT) > /etc/pritunl.conf(port) > 443
WEB_PORT=443
if [ -r /var/lib/pritunl/pritunl_web_env ]; then
  p="$(sed -n 's/^BIND_PORT=//p' /var/lib/pritunl/pritunl_web_env | head -n1)"
  [ -n "$p" ] && WEB_PORT="$p"
elif [ -r /etc/pritunl.conf ]; then
  p="$(sed -n 's/.*"port"[[:space:]]*:[[:space:]]*\([0-9]\{1,5\}\).*/\1/p' /etc/pritunl.conf | head -n1)"
  [ -n "$p" ] && WEB_PORT="$p"
fi

say "---- pritunl-onecheck $(date '+%F %T') (web port ${WEB_PORT}) ----"
check_unit mongod
check_unit pritunl

code="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 "https://127.0.0.1:${WEB_PORT}/state" 2>/dev/null || echo 000)"
if [ "$code" = "401" ]; then
  say "OK   Web API 探活通过（/state -> 401，API 在线且认证生效）"
else
  say "FAIL Web API 探活异常（/state -> ${code}，期望 401）"
  FAIL=1
fi

if command -v mongosh >/dev/null 2>&1; then
  if mongosh --quiet --host 127.0.0.1 --port 27017 --eval 'db.adminCommand({ping:1})' >/dev/null 2>&1; then
    say "OK   MongoDB 连接正常"
  else
    say "FAIL MongoDB 连接失败"
    FAIL=1
  fi
fi

if modinfo wireguard >/dev/null 2>&1; then
  say "OK   wireguard 内核模块可用"
else
  say "WARN wireguard 内核模块不可用（WireGuard 协议服务器将无法启动）"
fi

if [ "$FAIL" -eq 0 ]; then
  say "SELFCHECK PASS"
else
  say "SELFCHECK FAIL"
fi
printf '%s %s\n' "$TS" "$([ "$FAIL" -eq 0 ] && echo PASS || echo FAIL)" >> "${LOG_DIR}/onecheck.status"
# 仅保留最近 20 份自检日志
ls -1t "${LOG_DIR}"/onecheck-*.log 2>/dev/null | tail -n +21 | xargs -r rm -f
exit "$FAIL"
ONECHECK_EOF
  chmod +x /usr/local/sbin/pritunl-onecheck

  cat > /etc/systemd/system/pritunl-onecheck.service <<'UNIT_EOF'
[Unit]
Description=Pritunl OneClick Boot Self-Check
After=mongod.service pritunl.service
Wants=mongod.service pritunl.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/pritunl-onecheck
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
UNIT_EOF
  run systemctl daemon-reload
  run systemctl enable pritunl-onecheck.service
  log_ok "开机自检已安装：/usr/local/sbin/pritunl-onecheck + pritunl-onecheck.service（已 enable）"
}

# ---- 阶段：管控平台（可选）-----------------------------------------------------------
deploy_control_platform() {
  stage "部署 Pritunl 管控平台（可选）"
  local base cp_dir
  base="$(mirror_root)"
  cp_dir="${WORK_DIR}/control-platform"
  mkdir -p "$cp_dir"

  fetch_file "${base}/control-platform/VERSION" "${cp_dir}/VERSION"
  local cp_ver
  cp_ver="$(tr -d ' \n' < "${cp_dir}/VERSION")"
  [ -n "$cp_ver" ] || die "镜像内 control-platform/VERSION 为空"
  log_info "管控平台版本：${cp_ver}"

  fetch_file "${base}/control-platform/pritunl-control-${cp_ver}.tar.gz" \
    "${cp_dir}/pritunl-control-${cp_ver}.tar.gz"
  fetch_file "${base}/control-platform/pritunl-control-${cp_ver}.tar.gz.sha256" \
    "${cp_dir}/pritunl-control-${cp_ver}.tar.gz.sha256"
  (cd "$cp_dir" && sha256sum -c "pritunl-control-${cp_ver}.tar.gz.sha256") >>"$LOG_FILE" 2>&1 \
    || die "管控平台离线包 SHA256 校验失败，镜像内容可能损坏"
  fetch_file "${base}/control-platform/quick_start.sh" "${cp_dir}/quick_start.sh"

  log_info "执行管控平台安装（离线包模式，安装目录 /opt/pritunl-control）"
  if bash "${cp_dir}/quick_start.sh" --package "${cp_dir}/pritunl-control-${cp_ver}.tar.gz" \
      --dir /opt/pritunl-control 2>&1 | tee -a "$LOG_FILE"; then
    log_ok "管控平台安装命令执行完成"
  else
    die "管控平台安装失败（详见 $LOG_FILE）"
  fi

  # 验证 API 后台：平台约定 HTTP 恒 200，未认证时 body.code=401
  local code body
  code="$(curl -s -o "${cp_dir}/api_probe.json" -w '%{http_code}' --max-time 15 \
    "http://127.0.0.1:8080/api/v1/servers" 2>>"$LOG_FILE" || echo 000)"
  body="$(head -c 120 "${cp_dir}/api_probe.json" 2>/dev/null || true)"
  if [ "$code" = "200" ] && printf '%s' "$body" | grep -q '401'; then
    log_ok "管控平台 API 探活通过：GET /api/v1/servers -> HTTP 200, body.code=401（API 在线且认证生效）"
  else
    log_warn "管控平台 API 探活结果：HTTP ${code}（body: ${body}）；请执行 systemctl status pritunl-control 排查"
  fi
}

# ---- 阶段：最终验证 -----------------------------------------------------------------
final_verify() {
  stage "最终验证"

  log_info "—— 版本证据 ——"
  rpm -q "pritunl-${PRITUNL_VERSION}" pritunl-ndppd openvpn "mongodb-org-${MONGO_VERSION}" 2>/dev/null \
    | tee -a "$LOG_FILE" || true
  mongod --version 2>/dev/null | head -n1 | tee -a "$LOG_FILE" || true
  # pritunl 运行期版本（日志启动行），取不到则退回 RPM 版本
  local run_ver
  run_ver="$(journalctl -u pritunl --no-pager 2>/dev/null | grep -im1 'pritunl version' | head -n1 || true)"
  [ -n "$run_ver" ] && log_info "运行期版本行：${run_ver}" || log_info "运行期版本行未捕获（以 RPM 版本为准）"

  log_info "—— 服务状态 ——"
  local u
  for u in mongod pritunl pritunl-onecheck; do
    if systemctl is-enabled "$u" >/dev/null 2>&1; then
      log_ok "${u}: $(systemctl is-active "$u") / $(systemctl is-enabled "$u")"
    fi
  done

  log_info "—— 监听端口 ——"
  ss -tlnp 2>/dev/null | grep -E ":(80|443|${WEB_PORT}|27017|8080)\b" | tee -a "$LOG_FILE" || true

  log_info "—— Web API ——"
  log_ok "GET https://127.0.0.1:${WEB_PORT}/state -> ${API_CODE}（401=未认证，API 与认证均正常）"

  # 管控平台已在运行（非本次部署）时，确认其 API 未受影响
  if systemctl list-unit-files 2>/dev/null | grep -q '^pritunl-control.service'; then
    local ccode cbody
    ccode="$(curl -s -o "${WORK_DIR}/cp_probe.json" -w '%{http_code}' --max-time 10 \
      "http://127.0.0.1:8080/api/v1/servers" 2>/dev/null || echo 000)"
    cbody="$(head -c 120 "${WORK_DIR}/cp_probe.json" 2>/dev/null || true)"
    if [ "$ccode" = "200" ]; then
      log_ok "既有管控平台 API 未受影响：GET /api/v1/servers -> HTTP 200（${cbody}）"
    else
      log_warn "既有管控平台 API 探活异常：HTTP ${ccode}（${cbody}）"
    fi
  fi

  # 防火墙现状
  if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
    log_info "防火墙放通端口：$(firewall-cmd --list-ports 2>/dev/null | tr '\n' ' ')"
  fi

  # 落一份部署报告
  local report="${LOG_DIR}/deploy-report.txt"
  {
    printf 'Pritunl 一键部署报告\n'
    printf '生成时间：%s\n' "$(ts)"
    printf '脚本版本：pritunl_oneclick.sh %s\n' "$ONECLICK_VERSION"
    printf '系统：%s %s %s\n' "$OS_ID" "$OS_VERSION" "$ARCH"
    printf '镜像源：%s\n' "$(mirror_root)"
    printf 'Pritunl 版本：%s（RPM）\n' "$PRITUNL_VERSION"
    printf 'MongoDB 版本：%s\n' "$MONGO_VERSION"
    printf 'OpenVPN 版本：%s\n' "$(rpm -q --qf '%{VERSION}' openvpn 2>/dev/null || echo unknown)"
    printf 'Web 端口：%s\n' "$WEB_PORT"
    printf 'Web API 探活：GET /state -> %s\n' "$API_CODE"
    printf 'setup-key：%s\n' "${SETUP_KEY:-（已存在或未生成，见 /var/lib/pritunl/setup_key）}"
    printf '开机自检：/usr/local/sbin/pritunl-onecheck（pritunl-onecheck.service 已启用）\n'
    printf '部署日志：%s\n' "$LOG_FILE"
  } > "$report"
  log_ok "部署报告：$report"
}

print_summary() {
  printf '\n'
  printf '==================== 部署完成 ====================\n'
  printf ' Pritunl 管理界面 : https://<本机IP>:%s\n' "$WEB_PORT"
  printf ' Pritunl 版本     : %s\n' "$PRITUNL_VERSION"
  printf ' MongoDB          : %s（127.0.0.1:27017，仅回环）\n' "$MONGO_VERSION"
  printf ' 镜像源           : %s\n' "$(mirror_root)"
  printf ' 部署日志         : %s\n' "$LOG_FILE"
  printf ' 部署报告         : %s/deploy-report.txt\n' "$LOG_DIR"
  if [ -n "$SETUP_KEY" ]; then
    printf ' setup-key        : %s\n' "$SETUP_KEY"
    printf '   （浏览器打开管理界面，用该 key 创建首个管理员账号）\n'
  else
    printf ' setup-key        : 已存在（/var/lib/pritunl/setup_key），未重新生成\n'
  fi
  printf ' 开机自检         : systemctl status pritunl-onecheck\n'
  printf '  手动自检        : /usr/local/sbin/pritunl-onecheck\n'
  printf '==================================================\n'
  printf ' 后续步骤建议：\n'
  printf '  1. 浏览器访问 https://<本机IP>:%s ，用 setup-key 创建管理员\n' "$WEB_PORT"
  printf '  2. 在 Pritunl 控制台添加 Organization/Server，按需选择 UDP/TCP 端口\n'
  printf '  3. VPN 数据端口默认放通 %s ，如使用其他端口请同步 firewall-cmd\n' "$VPN_PORTS"
  printf '  4. 如需部署管控平台（API 后台），加 --with-control-platform 重跑本脚本\n'
  printf '==================================================\n'
}

# ---- 主流程 -------------------------------------------------------------------------
cmd_install() {
  detect_os
  preflight
  configure_mirror
  install_packages
  setup_mongodb
  setup_pritunl
  setup_firewall
  install_selfcheck
  if [ "$WITH_CP" = "1" ]; then
    deploy_control_platform
  fi
  final_verify
  # 立即做一次自检，作为交付证据
  /usr/local/sbin/pritunl-onecheck || log_warn "开机自检存在未通过项，请检查上方输出"
  print_summary
}

cmd_status() {
  mkdir -p "$LOG_DIR"
  LOG_FILE="${LOG_DIR}/status-$(date +%Y%m%d-%H%M%S).log"
  detect_web_port
  stage "Pritunl 状态"
  local u
  for u in mongod pritunl pritunl-control pritunl-onecheck; do
    systemctl is-active "$u" >/dev/null 2>&1 && log_ok "${u}: $(systemctl is-active "$u")"
  done
  detect_web_port
  local code
  code="$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 "https://127.0.0.1:${WEB_PORT}/state" 2>/dev/null || echo 000)"
  log_info "Web API（端口 ${WEB_PORT}）：GET /state -> ${code}"
  if [ -x /usr/local/sbin/pritunl-onecheck ]; then
    /usr/local/sbin/pritunl-onecheck || true
  fi
}

# ---- 错误与退出处理 ------------------------------------------------------------------
on_error() {
  local rc=$?
  log_err "部署失败：阶段=${CURRENT_STAGE} 退出码=${rc}"
  log_err "完整日志：${LOG_FILE:-（未创建）}"
  for u in mongod pritunl; do
    printf -- '---- journalctl -u %s (tail 15) ----\n' "$u" >>"${LOG_FILE:-/dev/null}" 2>/dev/null || true
    journalctl -u "$u" --no-pager -n 15 >>"${LOG_FILE:-/dev/null}" 2>&1 || true
  done
  {
    printf '排障建议：\n'
    printf '  1. 镜像不可达：curl -sSI %s/yum/%s/pritunl/repodata/repomd.xml\n' "${MIRROR_BASE}" "${EL_DIR:-el9}"
    printf '  2. 镜像目录结构不完整：对照 docs/pritunl-oneclick-deploy.md「自建镜像文件结构」\n'
    printf '  3. 服务异常：journalctl -u mongod -u pritunl --no-pager\n'
    printf '  4. 磁盘/内存：df -h / /var/lib；free -m\n'
    printf '  5. 完整日志：%s\n' "${LOG_FILE:-}"
  } >>"${LOG_FILE:-/dev/null}" 2>/dev/null || true
  [ -n "${LOG_FILE:-}" ] && tail -n 30 "$LOG_FILE" >&2 || true
  exit "$rc"
}

on_exit() {
  local rc=$?
  if [ "$rc" -eq 0 ] && [ -n "${WORK_DIR:-}" ] && [ -d "${WORK_DIR:-}" ]; then
    rm -rf "$WORK_DIR"
  elif [ -n "${WORK_DIR:-}" ] && [ -d "${WORK_DIR:-}" ]; then
    printf '[%s] [WARN] 保留临时目录以便排查：%s\n' "$(ts)" "$WORK_DIR" >&2 || true
  fi
}

main() {
  parse_args "$@"
  case "$SUBCOMMAND" in
    help)      usage ;;
    version)   printf 'pritunl_oneclick.sh %s\n' "$ONECLICK_VERSION" ;;
    selfcheck)
      [ -x /usr/local/sbin/pritunl-onecheck ] || die "自检脚本未安装（请先执行 install）"
      exec /usr/local/sbin/pritunl-onecheck
      ;;
    status)    trap on_error ERR; cmd_status ;;
    install)   trap on_error ERR; trap on_exit EXIT; cmd_install ;;
    *)         usage; die "未知子命令：$SUBCOMMAND" ;;
  esac
}

main "$@"
