#!/usr/bin/env bash
# =============================================================================
# rusttrae.sh — RustDesk 服务端一键部署（大陆/海外 + 多系统 + 双模式完整修复版）
# =============================================================================
set -Eeuo pipefail

RUSTDESK_DIR="/opt/rustdesk"
DATA_DIR="${RUSTDESK_DIR}/data"
COMPOSE_FILE="${RUSTDESK_DIR}/docker-compose.yml"
MODE_FILE="${RUSTDESK_DIR}/.install_mode"
PUB_KEY_FILE="${DATA_DIR}/id_ed25519.pub"
RUSTDESK_IMAGE="${RUSTDESK_IMAGE:-rustdesk/rustdesk-server:latest}"
RUSTDESK_PUBLIC_IP="${RUSTDESK_PUBLIC_IP:-}"
RUSTDESK_PUBLIC_IPV6="${RUSTDESK_PUBLIC_IPV6:-}"
RUSTDESK_DOMAIN="${RUSTDESK_DOMAIN:-}"
CERT_IP_FULLCHAIN="${RUSTDESK_IP_CERT:-/root/cert/ip/fullchain.pem}"
CERT_IP_KEY="${RUSTDESK_IP_KEY:-/root/cert/ip/privkey.pem}"
NGINX_WSS_SITE="/etc/nginx/sites-available/rustdesk-wss"
NGINX_WSS_LINK="/etc/nginx/sites-enabled/rustdesk-wss"
WS_LOCAL_HBBS="12718"
WS_LOCAL_HBBR="12719"
INSTALL_MODE="host"
OS_ID="unknown"
OS_ID_ORIG="unknown"
OS_NAME="Unknown"
PKG_MANAGER=""
DOCKER_REPO_OS="centos"

# ---------- 颜色 ----------
if [[ -t 1 ]]; then
  C_RESET='\033[0m'; C_BOLD='\033[1m'; C_BLUE='\033[38;5;39m'; C_CYAN='\033[38;5;45m'
  C_GREEN='\033[38;5;82m'; C_YELLOW='\033[38;5;220m'; C_RED='\033[38;5;196m'; C_DIM='\033[2m'
else
  C_RESET=''; C_BOLD=''; C_BLUE=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED=''; C_DIM=''
fi

print_line() { printf '%b\n' "${C_DIM}------------------------------------------------------------${C_RESET}"; }

print_banner() {
  clear 2>/dev/null || true
  printf '%b\n' "${C_BLUE}${C_BOLD}"
  printf '  ____            _   ____            _    \n'
  printf ' |  _ \\ _   _ ___| |_|  _ \\  ___  ___| | __\n'
  printf ' | |_) | | | / __| __| | | |/ _ \\/ __| |/ /\n'
  printf ' |  _ <| |_| \\__ \\ |_| |_| |  __/\\__ \\   < \n'
  printf ' |_| \\_\\\\__,_|___/\\__|____/ \\___||___/_|\\_\\\n'
  printf '%b\n' "${C_RESET}"
  printf '%b\n' "${C_CYAN}RustDesk 多系统一键安装管理脚本 (安全修复版)${C_RESET}"
  printf '%b\n' "${C_DIM}1=标准  2=强制中继(仍需UDP:21116)  + host网络 + BBR${C_RESET}"
  print_line
}

log()   { printf '%b %s\n' "${C_GREEN}[INFO]${C_RESET}" "$1" >&2; }
warn()  { printf '%b %s\n' "${C_YELLOW}[WARN]${C_RESET}" "$1" >&2; }
fail()  { printf '%b %s\n' "${C_RED}[ERROR]${C_RESET}" "$1" >&2; exit 1; }
pause() { [[ -t 0 ]] && { printf '\n'; read -r -p "按回车继续..." _; } || log "非交互环境，跳过等待"; }

require_root()    { [[ $(id -u) -eq 0 ]] || fail "请使用 root 用户运行此脚本。"; }
command_exists()  { command -v "$1" >/dev/null 2>&1; }

format_host_port() {
  local host="$1" port="$2"
  if [[ "${host}" == *":"* && "${host}" != *"."*"."*"."* ]]; then
    printf '[%s]:%s' "${host}" "${port}"
  else
    printf '%s:%s' "${host}" "${port}"
  fi
}

mode_label() {
  case "${1:-host}" in
    tcp|TCP)
      printf '强制中继版 (host / ALWAYS_USE_RELAY / 仍需 UDP:21116 注册)'
      ;;
    *)
      printf '标准版 (host / 含 UDP 打洞能力)'
      ;;
  esac
}

# ---------- 多系统检测 ----------
detect_os() {
  OS_ID="unknown"; OS_ID_ORIG="unknown"; OS_NAME="Unknown"; DOCKER_REPO_OS="centos"
  if [[ -r /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    OS_ID_ORIG="${ID:-unknown}"; OS_ID="${ID:-unknown}"; OS_NAME="${PRETTY_NAME:-${NAME:-Unknown}}"
  fi
  local like_list="${ID_LIKE:-}" candidate=""
  for candidate in ${OS_ID} ${like_list}; do
    case "${candidate}" in
      ubuntu|debian|centos|rhel|rocky|almalinux|ol|fedora|arch|manjaro|opensuse*|sles| \
      opencloudos|tencentos|alinux|anolis|amzn|openeuler|ctyunos|kylin|uos|opencloud|euleros)
        OS_ID="${candidate}"; break ;;
    esac
  done
  case "${OS_ID}" in
    ubuntu|debian)
      PKG_MANAGER="apt"; DOCKER_REPO_OS="${OS_ID}" ;;
    linuxmint|kali|raspbian|pop|elementary)
      PKG_MANAGER="apt"; DOCKER_REPO_OS="ubuntu" ;;
    fedora)
      PKG_MANAGER="dnf"; DOCKER_REPO_OS="fedora" ;;
    centos|rhel|rocky|almalinux|ol|opencloudos|tencentos|alinux|anolis|amzn|openeuler|ctyunos|kylin|uos|opencloud|euleros)
      if command_exists dnf; then PKG_MANAGER="dnf"; else PKG_MANAGER="yum"; fi
      DOCKER_REPO_OS="centos"
      ;;
    arch|manjaro|endeavouros)
      PKG_MANAGER="pacman"; DOCKER_REPO_OS="arch" ;;
    opensuse*|sles)
      PKG_MANAGER="zypper"; DOCKER_REPO_OS="sles" ;;
    *)
      if command_exists apt-get; then PKG_MANAGER="apt"; DOCKER_REPO_OS="ubuntu"
      elif command_exists dnf; then PKG_MANAGER="dnf"; DOCKER_REPO_OS="centos"
      elif command_exists yum; then PKG_MANAGER="yum"; DOCKER_REPO_OS="centos"
      elif command_exists pacman; then PKG_MANAGER="pacman"; DOCKER_REPO_OS="arch"
      elif command_exists zypper; then PKG_MANAGER="zypper"; DOCKER_REPO_OS="sles"
      else PKG_MANAGER=""; DOCKER_REPO_OS="centos"
      fi
      ;;
  esac
}

# ---------- 网络区域检测 ----------
detect_network_region() {
  local mainland_sites=("https://mirrors.aliyun.com" "https://mirrors.tuna.tsinghua.edu.cn" "https://mirrors.cloud.tencent.com")
  local overseas_sites=("https://registry-1.docker.io/v2/" "https://cloudflare.com" "https://github.com")
  local mainland_times=() overseas_times=() probe code tm

  for url in "${mainland_sites[@]}"; do
    probe="$(curl -o /dev/null -s -w '%{http_code}:%{time_total}' --max-time 4 "${url}" 2>/dev/null || true)"
    code="${probe%%:*}"; tm="${probe#*:}"
    case "$code" in 2??|3??) [[ -n "$tm" ]] && mainland_times+=("$tm") ;; esac
  done
  for url in "${overseas_sites[@]}"; do
    probe="$(curl -o /dev/null -s -w '%{http_code}:%{time_total}' --max-time 4 "${url}" 2>/dev/null || true)"
    code="${probe%%:*}"; tm="${probe#*:}"
    case "$code" in 2??|3??|401|403) [[ -n "$tm" ]] && overseas_times+=("$tm") ;; esac
  done

  _median() { local n=("$@"); [[ ${#n[@]} -eq 0 ]] && { printf '999'; return; }; printf '%s\n' "${n[@]}" | sort -g | awk '{a[NR]=$1} END{m=NR; if(m==1)print a[1]; else if(m%2==1)print a[(m+1)/2]; else print (a[m/2]+a[m/2+1])/2}'; }
  _min()    { local n=("$@"); [[ ${#n[@]} -eq 0 ]] && { printf '999'; return; }; printf '%s\n' "${n[@]}" | sort -g | head -n1; }

  local mmed omin mmin omin_min
  mmed="$(_median "${mainland_times[@]}")"; omin="$(_median "${overseas_times[@]}")"
  mmin="$(_min "${mainland_times[@]}")"; omin_min="$(_min "${overseas_times[@]}")"
  local mn=${#mainland_times[@]} on=${#overseas_times[@]}
  log "区域探测: 大陆=${mn}(中位=${mmed}s) 海外=${on}(中位=${omin}s)"

  if [[ $mn -gt 0 && $on -gt 0 ]]; then
    local win
    win=$(awk -v mm="$mmed" -v om="$omin" -v mn2="$mmin" 'BEGIN{
      if(mm<om*0.65 && mm<0.9) print "mainland";
      else if(mn2<0.25 && mm<0.7 && mm<om*0.8) print "mainland";
      else if(om<mm*0.5 && om<0.5) print "overseas";
      else if(mm>1.5 && om<0.8) print "overseas";
      else print "unknown"
    }')
    printf '%s' "$win"; return 0
  elif [[ $mn -gt 0 ]]; then
    [[ $on -eq 0 ]] && { printf 'mainland'; return 0; }
    awk -v m="$mmin" 'BEGIN{exit !(m<0.4)}' && { printf 'mainland'; return 0; }
    printf 'unknown'; return 0
  elif [[ $on -gt 0 ]]; then
    [[ $mn -eq 0 ]] && { printf 'overseas'; return 0; }
    awk -v o="$omin_min" 'BEGIN{exit !(o<0.3)}' && { printf 'overseas'; return 0; }
    printf 'unknown'; return 0
  fi
  printf 'unknown'
}

# ---------- 基础包安装 ----------
install_base_packages() {
  detect_os
  if [[ "${OS_ID_ORIG}" != "${OS_ID}" ]]; then
    log "系统: ${OS_NAME} (原始=${OS_ID_ORIG}, 映射=${OS_ID}, pkg=${PKG_MANAGER})"
  else
    log "系统: ${OS_NAME} (pkg=${PKG_MANAGER})"
  fi
  case "${PKG_MANAGER}" in
    apt)
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y || true
      apt-get install -y curl ca-certificates grep sed gawk gnupg lsb-release apt-transport-https iproute2 nginx openssl
      ;;
    dnf)
      dnf install -y curl ca-certificates grep sed gawk dnf-plugins-core iproute nginx openssl || true
      ;;
    yum)
      yum install -y curl ca-certificates grep sed gawk yum-utils iproute nginx openssl || true
      ;;
    pacman)
      pacman -Sy --noconfirm curl ca-certificates grep sed gawk iproute2
      ;;
    zypper)
      zypper --non-interactive refresh || true
      zypper --non-interactive install curl ca-certificates grep sed gawk iproute2
      ;;
    *)
      warn "未识别发行版，跳过基础依赖安装"
      ;;
  esac
  command_exists curl || fail "curl 不可用，请先安装 curl"
}

# ---------- Docker 源/镜像加速 ----------
get_docker_repo_base() {
  case "${1:-unknown}" in
    mainland) printf 'https://mirrors.aliyun.com/docker-ce' ;;
    *)        printf 'https://download.docker.com' ;;
  esac
}

get_docker_gpg_url() {
  local os_family="${2:-linux}"
  case "${1:-unknown}" in
    mainland) printf 'https://mirrors.aliyun.com/docker-ce/linux/%s/gpg' "${os_family}" ;;
    *)        printf 'https://download.docker.com/linux/%s/gpg' "${os_family}" ;;
  esac
}

resolve_version_codename() {
  local codename=""
  [[ -r /etc/os-release ]] && codename="$(. /etc/os-release && printf '%s' "${VERSION_CODENAME:-}")"
  [[ -z "$codename" ]] && command_exists lsb_release && codename="$(lsb_release -cs 2>/dev/null || true)"
  [[ -z "$codename" ]] && case "${DOCKER_REPO_OS}" in ubuntu) codename="jammy" ;; debian) codename="bookworm" ;; esac
  printf '%s' "${codename}"
}

check_docker_daemon_running() { docker ps >/dev/null 2>&1; }

configure_docker_mirrors() {
  local region="${1:-unknown}" daemon_file="/etc/docker/daemon.json" need_restart="false"
  local mainland_mirrors=("https://docker.1ms.run" "https://docker.m.daocloud.io" "https://hub-mirror.c.163.com" "https://docker.mirrors.ustc.edu.cn")

  case "${region}" in
    overseas) log "海外网络，跳过国内镜像加速器配置"; return 0 ;;
  esac

  local validated=() m mtest
  log "校验国内 Docker 镜像加速器连通性..."
  for m in "${mainland_mirrors[@]}"; do
    mtest="$(curl -o /dev/null -s -w '%{http_code}' --max-time 5 "${m}/v2/" 2>/dev/null || true)"
    case "${mtest:-000}" in 200|400|401|403|405) validated+=("$m"); log "  ✓ ${m}" ;; *) log "  ✗ ${m}" ;; esac
  done

  [[ ${#validated[@]} -eq 0 ]] && { warn "无可用国内镜像加速器，直接使用 Docker Hub"; return 0; }

  mkdir -p /etc/docker
  if [[ -f "$daemon_file" ]] && grep -q "registry-mirrors" "$daemon_file" 2>/dev/null; then
    log "daemon.json 已含 registry-mirrors，跳过覆盖"; return 0
  fi

  log "写入 ${#validated[@]} 个镜像加速器"
  printf '{\n  "registry-mirrors": [\n' > "$daemon_file"
  local i=0 total=${#validated[@]}
  for m in "${validated[@]}"; do
    i=$((i + 1))
    [[ $i -lt $total ]] && printf '    "%s",\n' "$m" >> "$daemon_file" || printf '    "%s"\n' "$m" >> "$daemon_file"
  done
  printf '  ]\n}\n' >> "$daemon_file"
  need_restart="true"

  if [[ "$need_restart" == "true" ]]; then
    if check_docker_daemon_running; then
      log "重启 Docker daemon 以应用镜像加速器..."
      command_exists systemctl && systemctl restart docker 2>/dev/null || true
      local w=0; while [[ $w -lt 15 ]] && ! check_docker_daemon_running; do sleep 2; w=$((w+1)); done
      check_docker_daemon_running && log "Docker daemon 已恢复" || warn "Docker 重启后未就绪，请手动检查"
    else
      log "daemon.json 已更新，Docker 下次启动时生效"
    fi
  fi
}

# 🛠️ 修复：加强 Docker GPG 密钥文件下载与格式有效性检查
install_docker_apt() {
  local region="${1:-unknown}" repo_base gpg_url codename
  repo_base="$(get_docker_repo_base "$region")"
  gpg_url="$(get_docker_gpg_url "$region" "${DOCKER_REPO_OS}")"
  codename="$(resolve_version_codename)"
  [[ -n "$codename" ]] || fail "无法解析 VERSION_CODENAME"

  install -m 0755 -d /etc/apt/keyrings
  if [[ ! -f /etc/apt/keyrings/docker.asc ]] || ! grep -q "BEGIN PGP" /etc/apt/keyrings/docker.asc 2>/dev/null; then
    rm -f /etc/apt/keyrings/docker.asc
    if ! curl -fsSL --max-time 30 "$gpg_url" -o /etc/apt/keyrings/docker.asc 2>/dev/null || ! grep -q "BEGIN PGP" /etc/apt/keyrings/docker.asc 2>/dev/null; then
      gpg_url="$(get_docker_gpg_url "unknown" "${DOCKER_REPO_OS}")"
      curl -fsSL --max-time 30 "$gpg_url" -o /etc/apt/keyrings/docker.asc || fail "Docker GPG 密钥下载失败"
    fi
    chmod a+r /etc/apt/keyrings/docker.asc
  fi
  printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.asc] %s/linux/%s %s stable\n' \
    "$(dpkg --print-architecture)" "$repo_base" "${DOCKER_REPO_OS}" "$codename" > /etc/apt/sources.list.d/docker.list
  apt-get update -y || fail "Docker APT 源配置失败"
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
}

install_docker_dnf() {
  local region="${1:-unknown}" repo_base repo_os
  repo_base="$(get_docker_repo_base "$region")"
  repo_os="${DOCKER_REPO_OS:-centos}"
  log "Docker 仓库路径: linux/${repo_os}"
  if ! dnf config-manager --add-repo "${repo_base}/linux/${repo_os}/docker-ce.repo" 2>/dev/null; then
    repo_base="$(get_docker_repo_base 'unknown')"
    dnf config-manager --add-repo "${repo_base}/linux/${repo_os}/docker-ce.repo" \
      || dnf config-manager --add-repo "${repo_base}/linux/centos/docker-ce.repo" \
      || fail "Docker DNF 仓库配置失败"
  fi
  dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
    || fail "Docker 安装失败（dnf）"
}

install_docker_yum() {
  local region="${1:-unknown}" repo_base repo_os
  repo_base="$(get_docker_repo_base "$region")"
  repo_os="${DOCKER_REPO_OS:-centos}"
  log "Docker 仓库路径: linux/${repo_os}"
  if ! yum-config-manager --add-repo "${repo_base}/linux/${repo_os}/docker-ce.repo" 2>/dev/null; then
    repo_base="$(get_docker_repo_base 'unknown')"
    yum-config-manager --add-repo "${repo_base}/linux/${repo_os}/docker-ce.repo" \
      || yum-config-manager --add-repo "${repo_base}/linux/centos/docker-ce.repo" \
      || fail "Docker YUM 仓库配置失败"
  fi
  yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \
    || fail "Docker 安装失败（yum）"
}

install_docker_pacman()  { pacman -Sy --noconfirm docker docker-compose; }
install_docker_zypper() { zypper --non-interactive install docker docker-compose; }

ensure_docker() {
  local need_install="true" region
  region="$(detect_network_region)"
  log "网络区域: ${region}"

  if command_exists docker; then
    if check_docker_daemon_running; then
      log "Docker 已安装且运行正常"; need_install="false"
    elif command_exists dockerd || dpkg -l docker-ce 2>/dev/null | grep -q '^ii' || rpm -q docker-ce >/dev/null 2>&1; then
      log "Docker 已安装但未运行，尝试启动"; need_install="false"
    else
      warn "Docker CLI 存在但 daemon 包缺失，执行补装"
    fi
  fi

  if [[ "$need_install" == "true" ]]; then
    log "安装 Docker..."
    case "${PKG_MANAGER}" in
      apt)    install_docker_apt "$region" ;;
      dnf)    install_docker_dnf "$region" ;;
      yum)    install_docker_yum "$region" ;;
      pacman) install_docker_pacman ;;
      zypper) install_docker_zypper ;;
      *)      fail "当前发行版无内置 Docker 安装流程，请手动安装 Docker 后重试" ;;
    esac
  fi

  configure_docker_mirrors "$region"

  if ! check_docker_daemon_running; then
    log "启动 Docker daemon..."
    if command_exists systemctl; then
      systemctl enable --now docker 2>/dev/null || true; sleep 2
      check_docker_daemon_running || systemctl restart docker 2>/dev/null || true
    elif command_exists rc-service; then
      rc-update add docker default 2>/dev/null || true; rc-service docker start 2>/dev/null || true
    elif command_exists service; then
      service docker start 2>/dev/null || true
    fi
    local w=0; while [[ $w -lt 10 ]] && ! check_docker_daemon_running; do sleep 2; w=$((w+1)); done
  fi
  check_docker_daemon_running && log "Docker daemon 就绪" || warn "Docker daemon 未就绪，后续步骤可能失败"
}

# ---------- 公网IP / 身份 / 模式 ----------
detect_existing_ip() {
  local configured_ip="" raw=""
  if [[ -f "$COMPOSE_FILE" ]]; then
    raw="$(grep -Eo '\-r[", ]*[^"]+' "$COMPOSE_FILE" 2>/dev/null | head -n1 || true)"
    raw="${raw#-r}"
    raw="${raw#\"}"
    raw="${raw%,*}"
    raw="$(printf '%s' "$raw" | sed 's/^[[:space:],"]*//;s/[[:space:]"]*$//')"
    # prefer first IPv4 in relay list
    configured_ip="$(printf '%s' "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1 || true)"
    if [[ -z "$configured_ip" ]]; then
      configured_ip="$(printf '%s' "$raw" | sed -n 's/.*\[\([^]]*\)\].*/\1/p; t; s/:21117.*//; p' | head -n1)"
    fi
  fi
  printf '%s' "${configured_ip}"
}

detect_public_ip() {
  [[ -n "$RUSTDESK_PUBLIC_IP" ]] && { printf '%s' "$RUSTDESK_PUBLIC_IP"; return 0; }
  local svc detected_ip
  for svc in "https://api.ipify.org" "https://ipv4.icanhazip.com" "https://ifconfig.me" "https://ip.3322.net" "https://ip.sb" "https://myip.ipip.net"; do
    detected_ip="$(curl -4fsSL --max-time 5 "$svc" 2>/dev/null || true)"
    detected_ip="$(printf '%s' "$detected_ip" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1 || true)"
    [[ -n "$detected_ip" ]] && { printf '%s' "$detected_ip"; return 0; }
  done
  detected_ip="$(detect_existing_ip)"
  [[ -n "$detected_ip" ]] && { warn "公网IP探测失败，回退使用现有配置: $detected_ip"; printf '%s' "$detected_ip"; return 0; }
  fail "无法获取公网IP，请通过 RUSTDESK_PUBLIC_IP 环境变量手动指定"
}

detect_public_ipv6() {
  [[ -n "$RUSTDESK_PUBLIC_IPV6" ]] && { printf '%s' "$RUSTDESK_PUBLIC_IPV6"; return 0; }
  local svc detected_ip
  for svc in "https://api64.ipify.org" "https://ipv6.icanhazip.com" "https://ifconfig.co"; do
    detected_ip="$(curl -6fsSL --max-time 5 "$svc" 2>/dev/null || true)"
    detected_ip="$(printf '%s' "$detected_ip" | tr -d '[:space:]')"
    [[ "$detected_ip" == *:* ]] && { printf '%s' "$detected_ip"; return 0; }
  done
  detected_ip="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | head -n1 | cut -d/ -f1 || true)"
  [[ "$detected_ip" == *:* ]] && { printf '%s' "$detected_ip"; return 0; }
  printf ''
}

build_relay_servers() {
  local ipv4="$1" ipv6="${2:-}" out=""
  out="$(format_host_port "$ipv4" "21117")"
  if [[ -n "$ipv6" ]]; then
    out="${out},$(format_host_port "$ipv6" "21117")"
  fi
  printf '%s' "$out"
}

has_ip_cert() {
  [[ -s "$CERT_IP_FULLCHAIN" && -s "$CERT_IP_KEY" ]]
}

# 发现域名证书: 输出 "server_name|fullchain|key" 多行
discover_domain_cert_lines() {
  local name full key
  if [[ -n "$RUSTDESK_DOMAIN" ]]; then
    for full in \
      "/etc/letsencrypt/live/${RUSTDESK_DOMAIN}/fullchain.pem" \
      "/root/ssl_out/${RUSTDESK_DOMAIN}/fullchain.pem" \
      "/root/.acme.sh/${RUSTDESK_DOMAIN}_ecc/fullchain.cer"
    do
      if [[ -s "$full" ]]; then
        if [[ "$full" == *"/fullchain.cer" ]]; then
          key="/root/.acme.sh/${RUSTDESK_DOMAIN}_ecc/${RUSTDESK_DOMAIN}.key"
        elif [[ "$full" == *"/ssl_out/"* ]]; then
          key="/root/ssl_out/${RUSTDESK_DOMAIN}/privkey.pem"
        else
          key="/etc/letsencrypt/live/${RUSTDESK_DOMAIN}/privkey.pem"
        fi
        [[ -s "$key" ]] && { printf '%s|%s|%s\n' "$RUSTDESK_DOMAIN" "$full" "$key"; return 0; }
      fi
    done
  fi
  # auto scan common locations
  for full in /etc/letsencrypt/live/*/fullchain.pem /root/ssl_out/*/fullchain.pem; do
    [[ -s "$full" ]] || continue
    name="$(basename "$(dirname "$full")")"
    [[ "$name" == "README" ]] && continue
    if [[ "$full" == /etc/letsencrypt/* ]]; then
      key="/etc/letsencrypt/live/${name}/privkey.pem"
    else
      key="/root/ssl_out/${name}/privkey.pem"
    fi
    [[ -s "$key" ]] || continue
    printf '%s|%s|%s\n' "$name" "$full" "$key"
  done
  for full in /root/.acme.sh/*_ecc/fullchain.cer; do
    [[ -s "$full" ]] || continue
    name="$(basename "$(dirname "$full")")"
    name="${name%_ecc}"
    [[ "$name" =~ ^[0-9.]+$ ]] && continue  # skip IP acme dirs; use CERT_IP_*
    key="$(dirname "$full")/${name}.key"
    [[ -s "$key" ]] || continue
    printf '%s|%s|%s\n' "$name" "$full" "$key"
  done
}

wss_available() {
  has_ip_cert && return 0
  [[ -n "$(discover_domain_cert_lines 2>/dev/null | head -n1)" ]] && return 0
  return 1
}

has_existing_identity() { [[ -s "${DATA_DIR}/id_ed25519" && -s "${PUB_KEY_FILE}" ]]; }

detect_current_mode() {
  if [[ -f "$MODE_FILE" ]]; then
    local m; m="$(tr -d '[:space:]' < "$MODE_FILE")"
    case "$m" in
      tcp) printf 'tcp'; return 0 ;;
      host|bridge-wss) printf 'host'; return 0 ;;
    esac
  fi
  if [[ -f "$COMPOSE_FILE" ]]; then
    # 有 ALWAYS_USE_RELAY 且无 host 网络 → 仍按用户模式文件优先；否则看 compose
    if grep -q 'ALWAYS_USE_RELAY=Y' "$COMPOSE_FILE" 2>/dev/null && ! grep -q 'network_mode:[[:space:]]*host' "$COMPOSE_FILE" 2>/dev/null; then
      # bridge/wss 部署：默认视为 host（标准输出习惯），除非 MODE 明确 tcp
      printf 'host'; return 0
    fi
    if grep -q 'network_mode:[[:space:]]*host' "$COMPOSE_FILE" 2>/dev/null; then
      printf 'host'; return 0
    fi
    if grep -q 'ports:' "$COMPOSE_FILE" 2>/dev/null; then
      printf 'tcp'; return 0
    fi
  fi
  printf 'unknown'
}

save_install_mode() {
  mkdir -p "$RUSTDESK_DIR"
  printf '%s\n' "${1}" > "$MODE_FILE"
}

# 🛠️ 修复：防止 ss 命令缺失导致 set -e 崩溃
detect_ssh_port() {
  local port=""
  if command_exists ss; then
    port="$(ss -lntp 2>/dev/null | grep -E '":22[[:space:]]|sshd' | awk -F'[[:space:]]+' '{addr=$4; n=split(addr,a,":"); print a[n]; exit}' 2>/dev/null || true)"
  fi
  [[ -z "$port" && -f /etc/ssh/sshd_config ]] && port="$(awk 'tolower($1)=="port" && $2 !~ /^#/{print $2; exit}' /etc/ssh/sshd_config 2>/dev/null || true)"
  [[ -n "$port" && "$port" -gt 0 ]] 2>/dev/null && printf '%s' "$port" || printf '22'
}

configure_firewall() {
  local mode="${1:-host}"
  local ssh_port; ssh_port="$(detect_ssh_port)"
  local manage_fw="${RUSTDESK_MANAGE_FIREWALL:-false}"
  case "$manage_fw" in
    false|FALSE|no|NO|0)
      log "跳过本地防火墙（请确认云安全组放行 21115-21119/TCP 与 21116/UDP）"
      return 0
      ;;
  esac

  if command_exists ufw; then
    if ufw status 2>/dev/null | grep -q "Status: active"; then
      ufw allow "${ssh_port}/tcp" 2>/dev/null || true
      ufw allow 21115:21119/tcp 2>/dev/null || true
      ufw allow 21116/udp 2>/dev/null || true
      log "UFW 已追加 RustDesk 端口（TCP 21115-21119 + UDP 21116）"
    else
      ufw allow "${ssh_port}/tcp"; ufw allow 21115:21119/tcp; ufw allow 21116/udp
      ufw --force enable
      log "已启用 UFW，SSH: ${ssh_port}/tcp"
    fi
    return 0
  fi

  if command_exists firewall-cmd; then
    systemctl enable --now firewalld >/dev/null 2>&1 || true
    if systemctl is-active --quiet firewalld 2>/dev/null; then
      firewall-cmd --permanent --add-port="${ssh_port}/tcp" 2>/dev/null || true
      firewall-cmd --permanent --add-port=21115-21119/tcp 2>/dev/null || true
      firewall-cmd --permanent --add-port=21116/udp 2>/dev/null || true
      firewall-cmd --reload 2>/dev/null || true
      log "已配置 firewalld（模式: $(mode_label "$mode")）"
    else
      log "firewalld 未启用，跳过"
    fi
    return 0
  fi

  if command_exists iptables; then
    iptables -C INPUT -p tcp --dport "$ssh_port" -j ACCEPT 2>/dev/null \
      || iptables -I INPUT -p tcp --dport "$ssh_port" -j ACCEPT 2>/dev/null || true
    local p
    for p in 21115 21116 21117 21118 21119; do
      iptables -C INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null \
        || iptables -I INPUT -p tcp --dport "$p" -j ACCEPT 2>/dev/null || true
    done
    iptables -C INPUT -p udp --dport 21116 -j ACCEPT 2>/dev/null \
      || iptables -I INPUT -p udp --dport 21116 -j ACCEPT 2>/dev/null || true
    log "已尝试用 iptables 放行 RustDesk 端口"
    return 0
  fi

  warn "未检测到防火墙工具，请确认云安全组已放行 21115-21119/TCP 与 21116/UDP"
}

# ---------- Compose / 镜像 ----------
write_compose_file() {
  local public_ip="$1" mode="${2:-host}" public_ipv6="${3:-}" relay_addr use_wss="false"
  relay_addr="$(build_relay_servers "$public_ip" "$public_ipv6")"
  mkdir -p "${DATA_DIR}"
  wss_available && use_wss="true"

  if [[ "$use_wss" == "true" ]]; then
    # bridge + 本机 WS，由 nginx 提供 HTTP(S)/WSS；对外仍是 21115-21119
    cat > "${COMPOSE_FILE}" <<EOF
# RustDesk server
# mode=${mode}  (用户模式保留 host/tcp 习惯；实际: bridge + nginx WSS；IPv4/IPv6 双栈)
services:
  hbbr:
    container_name: rustdesk-hbbr
    image: ${RUSTDESK_IMAGE}
    command: ["hbbr"]
    volumes:
      - ./data:/root
    ports:
      - "21117:21117"
      - "127.0.0.1:${WS_LOCAL_HBBR}:21119"
    restart: unless-stopped

  hbbs:
    container_name: rustdesk-hbbs
    image: ${RUSTDESK_IMAGE}
    command: ["hbbs", "-r", "${relay_addr}", "-k", "_"]
    environment:
      - ALWAYS_USE_RELAY=Y
    volumes:
      - ./data:/root
    ports:
      - "21115:21115"
      - "21116:21116"
      - "21116:21116/udp"
      - "127.0.0.1:${WS_LOCAL_HBBS}:21118"
    depends_on:
      - hbbr
    restart: unless-stopped
EOF
  else
    cat > "${COMPOSE_FILE}" <<EOF
# RustDesk server
# mode=${mode}  (host=标准含UDP提示 / tcp=禁UDP机房，客户端需 disable-udp=Y；未检测到证书时回退 host)
services:
  hbbr:
    container_name: rustdesk-hbbr
    image: ${RUSTDESK_IMAGE}
    command: ["hbbr"]
    volumes:
      - ./data:/root
    network_mode: host
    restart: unless-stopped

  hbbs:
    container_name: rustdesk-hbbs
    image: ${RUSTDESK_IMAGE}
    command: ["hbbs", "-r", "${relay_addr}", "-k", "_"]
    environment:
      - ALWAYS_USE_RELAY=Y
    volumes:
      - ./data:/root
    network_mode: host
    depends_on:
      - hbbr
    restart: unless-stopped
EOF
  fi
  save_install_mode "$mode"
  write_client_hint "$public_ip" "$mode"
}

write_wss_nginx() {
  local public_ip="$1" line name full key
  mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
  {
    printf '# Auto-generated by rust.sh — RustDesk WS/WSS (IPv4+IPv6)\n'
    if has_ip_cert; then
      cat <<EOF
server {
    listen 21118 ssl default_server;
    listen [::]:21118 ssl default_server;
    server_name ${public_ip} _;
    ssl_certificate     ${CERT_IP_FULLCHAIN};
    ssl_certificate_key ${CERT_IP_KEY};
    ssl_protocols       TLSv1.2 TLSv1.3;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBS};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
server {
    listen 21119 ssl default_server;
    listen [::]:21119 ssl default_server;
    server_name ${public_ip} _;
    ssl_certificate     ${CERT_IP_FULLCHAIN};
    ssl_certificate_key ${CERT_IP_KEY};
    ssl_protocols       TLSv1.2 TLSv1.3;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBR};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
EOF
    else
      # 无 IP 证书时仍提供明文 WS，兼容旧 Web/探测
      cat <<EOF
server {
    listen 21118 default_server;
    listen [::]:21118 default_server;
    server_name _;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBS};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
server {
    listen 21119 default_server;
    listen [::]:21119 default_server;
    server_name _;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBR};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
EOF
    fi

    while IFS='|' read -r name full key; do
      [[ -n "$name" && -n "$full" && -n "$key" ]] || continue
      cat <<EOF
server {
    listen 21118 ssl;
    listen [::]:21118 ssl;
    server_name ${name};
    ssl_certificate     ${full};
    ssl_certificate_key ${key};
    ssl_protocols       TLSv1.2 TLSv1.3;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBS};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
server {
    listen 21119 ssl;
    listen [::]:21119 ssl;
    server_name ${name};
    ssl_certificate     ${full};
    ssl_certificate_key ${key};
    ssl_protocols       TLSv1.2 TLSv1.3;
    location / {
        proxy_pass http://127.0.0.1:${WS_LOCAL_HBBR};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
EOF
    done < <(discover_domain_cert_lines | awk -F'|' '!seen[$1]++')
  } > "$NGINX_WSS_SITE"

  ln -sfn "$NGINX_WSS_SITE" "$NGINX_WSS_LINK"
}

enable_wss_front() {
  local public_ip="$1"
  wss_available || { log "未检测到可用证书，21118/21119 使用明文 WS（IPv4/IPv6）"; }
  if ! command_exists nginx; then
    warn "未安装 nginx，跳过 WSS 前端（请安装后重跑安装）"
    return 0
  fi
  write_wss_nginx "$public_ip"
  if nginx -t >/dev/null 2>&1; then
    systemctl enable nginx >/dev/null 2>&1 || true
    systemctl reload nginx 2>/dev/null || systemctl restart nginx 2>/dev/null || true
    log "已配置 21118/21119 HTTP(S)/WSS 前端（双栈）"
  else
    warn "nginx 配置检测失败，已保留站点文件: ${NGINX_WSS_SITE}"
    nginx -t 2>&1 | while read -r l; do warn "$l"; done || true
  fi
}

disable_wss_front() {
  rm -f "$NGINX_WSS_LINK" "$NGINX_WSS_SITE"
  if command_exists nginx; then
    nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
  fi
}

write_client_hint() {
  local public_ip="$1" mode="${2:-host}" hint="${RUSTDESK_DIR}/client-tcp-hint.txt"
  [[ "$mode" == "tcp" ]] || { rm -f "$hint" 2>/dev/null || true; return 0; }
  local key=""
  [[ -f "$PUB_KEY_FILE" ]] && key="$(tr -d '\r\n' < "$PUB_KEY_FILE")"
  cat > "$hint" <<EOF
# RustDesk「强制中继版」客户端说明
#
# ID 服务器: ${public_ip}:21116
# 中继服务器: ${public_ip}:21117
# Key: ${key:-<见安装输出>}
EOF
}

compose_cmd() {
  if docker compose version >/dev/null 2>&1; then docker compose -f "$COMPOSE_FILE" "$@"; return 0; fi
  command_exists docker-compose && { docker-compose -f "$COMPOSE_FILE" "$@"; return 0; }
  fail "未找到 Docker Compose"
}

pull_rustdesk_image() {
  check_docker_daemon_running || fail "Docker daemon 未就绪"
  local attempt=1
  while [[ $attempt -le 3 ]]; do
    log "拉取 RustDesk 镜像 (${attempt}/3): ${RUSTDESK_IMAGE}"
    docker pull "$RUSTDESK_IMAGE" && { log "镜像拉取成功"; return 0; }
    warn "拉取失败"; [[ $attempt -lt 3 ]] && { warn "5秒后重试"; sleep 5; }
    attempt=$((attempt + 1))
  done
  fail "连续 3 次拉取 ${RUSTDESK_IMAGE} 失败，请检查网络或用 RUSTDESK_IMAGE 指定镜像"
}

# 🛠️ 修复：改用 /etc/sysctl.d/ 保证重启后 BBR/CAKE 持久化生效
apply_sysctl_tuning() {
  local sysctl_conf="/etc/sysctl.d/99-rustdesk-bbr.conf"
  [[ -r /proc/sys/net/ipv4/tcp_available_congestion_control ]] || { log "容器化环境，跳过 sysctl 优化"; return 0; }
  local available_cc use_bbr="false" use_cake="false" orig_qdisc
  orig_qdisc="$(sysctl -n net.core.default_qdisc 2>/dev/null || printf 'pfifo_fast')"
  available_cc="$(sysctl -n net.ipv4.tcp_available_congestion_control 2>/dev/null || true)"
  if [[ -n "$available_cc" && " $available_cc " == *" bbr "* ]]; then
    sysctl -w net.ipv4.tcp_congestion_control=bbr >/dev/null 2>&1 && use_bbr="true" || warn "BBR 启用失败"
  else
    warn "内核不支持 BBR"
  fi
  sysctl -w net.core.default_qdisc=cake >/dev/null 2>&1 && use_cake="true" || { warn "CAKE 不支持，保持 $orig_qdisc"; sysctl -w "net.core.default_qdisc=$orig_qdisc" >/dev/null 2>&1 || true; }
  
  mkdir -p /etc/sysctl.d/
  rm -f "$sysctl_conf"
  [[ "$use_bbr" == "true" ]] && printf 'net.ipv4.tcp_congestion_control = bbr\n' >> "$sysctl_conf"
  [[ "$use_cake" == "true" ]] && printf 'net.core.default_qdisc = cake\n' >> "$sysctl_conf"
  sysctl --system >/dev/null 2>&1 || sysctl -p >/dev/null 2>&1 || warn "部分 sysctl 参数未能立即应用"
  
  local applied=""
  [[ "$use_bbr" == "true" ]] && applied="BBR"
  [[ "$use_cake" == "true" ]] && applied="${applied:+${applied} + }CAKE"
  [[ -n "$applied" ]] && log "网络优化已应用并持久化: ${applied}" || log "BBR/CAKE 均不支持，跳过"
}

wait_for_key() {
  local retries=30
  while [[ ! -f "$PUB_KEY_FILE" && $retries -gt 0 ]]; do sleep 1; retries=$((retries - 1)); done
  [[ -f "$PUB_KEY_FILE" ]] && cat "$PUB_KEY_FILE" || printf '%s' "密钥暂未生成，请稍后查看 ${PUB_KEY_FILE}"
}

# ---------- 状态检查 ----------
port_listening() {
  local port="$1" proto="${2:-tcp}"
  command_exists ss || return 1
  case "$proto" in
    tcp) ss -lnt 2>/dev/null | awk -v p="$port" 'BEGIN{ok=0} {addr=$4; n=split(addr,a,":"); if(a[n]+0==p+0){ok=1;exit}} END{exit (ok?0:1)}' ;;
    udp) ss -lnu 2>/dev/null | awk -v p="$port" 'BEGIN{ok=0} {addr=$4; n=split(addr,a,":"); if(a[n]+0==p+0){ok=1;exit}} END{exit (ok?0:1)}' ;;
  esac
}

check_rustdesk_ports() {
  local mode="${1:-$(detect_current_mode)}"
  local specs=("21115 tcp hbbs" "21116 tcp hbbs" "21116 udp hbbs" "21117 tcp hbbr" "21118 tcp hbbs" "21119 tcp hbbr")
  printf '%b\n' "${C_CYAN}${C_BOLD}端口监听检查${C_RESET}"
  print_line
  local spec port proto service
  for spec in "${specs[@]}"; do
    read -r port proto service <<< "$spec"
    if port_listening "$port" "$proto"; then
      printf '%b %s/%s -> %s\n' "${C_GREEN}[OK]${C_RESET}" "$port" "$proto" "$service"
    else
      printf '%b %s/%s -> %s\n' "${C_RED}[FAIL]${C_RESET}" "$port" "$proto" "$service"
    fi
  done
  if [[ "$mode" == "tcp" ]]; then
    printf '%b\n' "${C_DIM}注: 强制中继版仍需 UDP/21116 供客户端注册上线${C_RESET}"
  fi
}

container_status() {
  command_exists docker || { printf '%s\n' "Docker 未安装"; return 0; }
  docker ps --format '{{.Names}}: {{.Status}}' 2>/dev/null | grep -E '^rustdesk-hbbs:|^rustdesk-hbbr:' || printf '%s\n' "未发现运行中的 RustDesk 容器"
}

show_summary() {
  local public_ip="$1" public_key="$2" mode="${3:-host}"
  write_client_hint "$public_ip" "$mode"
  [[ -f "$PUB_KEY_FILE" ]] && public_key="$(tr -d '\r\n' < "$PUB_KEY_FILE")"

  printf '\n'; print_line
  printf '%b\n' "${C_GREEN}${C_BOLD}RustDesk 安装/更新完成${C_RESET}"
  printf '系统: %s\n' "${OS_NAME}"
  printf '部署模式: %s\n' "$(mode_label "$mode")"
  printf 'ID 服务器: %s\n' "$(format_host_port "${public_ip}" "21116")"
  printf '中继服务器: %s\n' "$(format_host_port "${public_ip}" "21117")"
  printf '公钥 Key: %s\n' "${public_key}"
  printf '强制加密: -k _\n'
  printf '强制中继: ALWAYS_USE_RELAY=Y\n'
  printf '地址来源: %s\n' "${RUSTDESK_PUBLIC_IP:-自动探测}"
  if [[ "$mode" == "tcp" ]]; then
    print_line
    printf '%b\n' "${C_YELLOW}${C_BOLD}【说明】强制中继版${C_RESET}"
    printf '服务端已启用 ALWAYS_USE_RELAY=Y（强制中继，减少打洞失败）。\n'
    printf '%b仍须放行 UDP 21116%b（开源客户端注册依赖它；屏蔽会导致无法上线）。\n' "${C_RED}" "${C_RESET}"
    printf '同时放行 TCP 21115-21119。\n'
    printf '详细: %s\n' "${RUSTDESK_DIR}/client-tcp-hint.txt"
  else
    printf '安全组请放行: 21115-21119/TCP + 21116/UDP\n'
  fi
  print_line
  check_rustdesk_ports "$mode"
  print_line
}

# 🛠️ 修复：规范清理流程，不破坏用户非脚本建立的外部容器
cleanup_bridge_leftovers() {
  log "清理 Compose 节点及可能残留的旧 bridge 规则..."
  if [[ -f "$COMPOSE_FILE" ]] && command_exists docker; then
    compose_cmd down --remove-orphans >/dev/null 2>&1 || true
  fi
  if command_exists iptables-save && iptables-save 2>/dev/null | grep -qE 'DNAT.*--(to-destination|to-ports).*2111[5-9]|2111[5-9].*DNAT'; then
    warn "检测到 21115-21119 的 Docker DNAT 残留，重启 Docker 以清理"
    if command_exists systemctl; then
      systemctl restart docker 2>/dev/null || true
      local w=0
      while [[ $w -lt 20 ]] && ! check_docker_daemon_running; do sleep 2; w=$((w+1)); done
    fi
  fi
}

verify_listen_ports() {
  local mode="${1:-host}"
  command_exists ss || return 0
  local ok=1
  for p in 21115 21116 21117 21118 21119; do
    if ! ss -lnt 2>/dev/null | awk -v p="$p" '{n=split($4,a,":"); if(a[n]+0==p+0) found=1} END{exit !found}'; then
      warn "TCP $p 未监听"
      ok=0
    fi
  done
  if [[ "$mode" != "tcp" ]]; then
    if ! ss -lnu 2>/dev/null | awk '{n=split($4,a,":"); if(a[n]+0==21116) found=1} END{exit !found}'; then
      warn "UDP 21116 未监听（标准版异常）"
      ok=0
    fi
  fi
  # bridge+WSS 会必然出现 Docker DNAT，属正常；仅 host 模式才告警
  if grep -q 'network_mode:[[:space:]]*host' "$COMPOSE_FILE" 2>/dev/null; then
    if command_exists iptables-save && iptables-save 2>/dev/null | grep -qE 'DNAT.*2111[5-9]'; then
      warn "仍存在 Docker DNAT→2111x，连接可能异常；请执行: systemctl restart docker 后重装"
      ok=0
    fi
  fi
  if [[ "$ok" == "1" ]]; then
    log "端口/DNAT 检查通过"
  fi
  return 0
}

# ---------- 主功能 ----------
install_or_update() {
  local mode="${1:-host}" public_ip="" public_ipv6="" public_key="" old_mode
  INSTALL_MODE="$mode"
  print_banner
  log "开始安装/更新 RustDesk — $(mode_label "$mode")"

  old_mode="$(detect_current_mode)"
  if [[ "$old_mode" != "unknown" && "$old_mode" != "$mode" ]]; then
    warn "检测到已有部署模式为 $(mode_label "$old_mode")，将切换为 $(mode_label "$mode")"
    warn "会重建 Compose（密钥默认保留）"
  fi

  install_base_packages
  public_ip="$(detect_public_ip)"
  public_ipv6="$(detect_public_ipv6 || true)"
  log "服务器地址: ${public_ip}"
  [[ -n "$public_ipv6" ]] && log "检测到 IPv6: ${public_ipv6}"
  [[ -n "$RUSTDESK_PUBLIC_IP" ]] && log "已通过环境变量指定地址"
  has_existing_identity && log "检测到现有密钥，保留当前身份" || log "将自动生成新 Key"
  ensure_docker
  configure_firewall "$mode"
  pull_rustdesk_image

  cleanup_bridge_leftovers

  write_compose_file "$public_ip" "$mode" "$public_ipv6"
  compose_cmd up -d --remove-orphans || fail "容器启动失败，请执行: docker compose -f $COMPOSE_FILE logs --tail=100"
  sleep 2
  if wss_available || ! grep -q 'network_mode:[[:space:]]*host' "$COMPOSE_FILE" 2>/dev/null; then
    enable_wss_front "$public_ip"
  else
    disable_wss_front
  fi
  verify_listen_ports "$mode"
  apply_sysctl_tuning
  public_key="$(wait_for_key)"
  show_summary "$public_ip" "$public_key" "$mode"
  pause
}

show_config() {
  local public_ip="未检测到" public_key="未检测到" tcp_cc="未设置" qdisc="未设置" mode
  print_banner
  if [[ ! -f "$COMPOSE_FILE" && ! -f "$PUB_KEY_FILE" ]]; then
    warn "RustDesk 尚未安装，请先选择安装"
    pause
    return
  fi
  mode="$(detect_current_mode)"
  [[ -f "$COMPOSE_FILE" ]] && public_ip="$(detect_existing_ip)"
  [[ -n "$public_ip" ]] || public_ip="未检测到"
  [[ -f "$PUB_KEY_FILE" ]] && public_key="$(cat "$PUB_KEY_FILE")" || public_key="未检测到"
  sysctl net.ipv4.tcp_congestion_control >/dev/null 2>&1 && tcp_cc="$(sysctl -n net.ipv4.tcp_congestion_control 2>/dev/null || printf '未设置')"
  sysctl net.core.default_qdisc >/dev/null 2>&1 && qdisc="$(sysctl -n net.core.default_qdisc 2>/dev/null || printf '未设置')"

  printf '%b\n' "${C_CYAN}${C_BOLD}当前配置概览${C_RESET}"
  print_line
  printf '部署目录: %s\n' "$RUSTDESK_DIR"
  printf 'Compose: %s\n' "$COMPOSE_FILE"
  printf '部署模式: %s\n' "$(mode_label "$mode")"
  if [[ "$public_ip" == "未检测到" ]]; then
    printf 'ID 服务器: %s\n' "$public_ip"
    printf '中继服务器: %s\n' "$public_ip"
  else
    printf 'ID 服务器: %s\n' "$(format_host_port "$public_ip" "21116")"
    printf '中继服务器: %s\n' "$(format_host_port "$public_ip" "21117")"
  fi
  printf '公钥 Key: %s\n' "$public_key"
  printf '强制加密: %s\n' "-k _"
  printf '强制中继: %s\n' "ALWAYS_USE_RELAY=Y"
  printf '网络优化: tcp_cc=%s, qdisc=%s\n' "$tcp_cc" "$qdisc"
  if [[ "$mode" == "tcp" ]]; then
    printf '监听端口: 21115-21119/TCP（可不开放 UDP；客户端须 disable-udp=Y）\n'
    printf '客户端说明: %s\n' "${RUSTDESK_DIR}/client-tcp-hint.txt"
  else
    printf '监听端口: 21115/tcp, 21116/tcp+udp, 21117/tcp, 21118/tcp, 21119/tcp\n'
  fi
  print_line
  printf '%b\n' "${C_CYAN}${C_BOLD}容器状态${C_RESET}"
  container_status
  printf '\n'
  check_rustdesk_ports "$mode"
  printf '\n'
  if [[ "$mode" == "tcp" ]]; then
    printf '%b\n' "${C_YELLOW}${C_BOLD}客户端必配${C_RESET}"
    print_line
    printf '高级设置增加: disable-udp=Y\n'
    printf '（仅服务端无法强制关 UDP；不配则禁 UDP 网络下会连不上）\n'
    print_line
  fi
  if [[ -f "$COMPOSE_FILE" ]]; then
    printf '%b\n' "${C_CYAN}${C_BOLD}Compose 内容${C_RESET}"
    print_line
    cat "$COMPOSE_FILE"
  else
    warn "未检测到 Compose 配置文件"
  fi
  pause
}

uninstall_rustdesk() {
  local answer="" remove_data=""
  print_banner
  printf '%b\n' "${C_YELLOW}${C_BOLD}即将卸载 RustDesk 服务端${C_RESET}"
  printf '停止并删除 RustDesk 容器及 Compose 配置（数据目录默认保留）\n\n'
  if [[ -t 0 ]]; then
    read -r -p "确认卸载吗？[y/N]: " answer || answer=""
  else
    answer="y"; log "非交互环境，自动确认卸载（保留数据目录）"
  fi
  case "$answer" in y|Y|yes|YES) ;; *) warn "已取消卸载"; pause; return 0 ;; esac

  if [[ -f "$COMPOSE_FILE" ]] && command_exists docker; then
    compose_cmd down --remove-orphans || warn "停止容器异常，请手动检查"
  else
    warn "未检测到 Compose 文件或 Docker"
  fi
  rm -f "$COMPOSE_FILE" "$MODE_FILE" /etc/sysctl.d/99-rustdesk-bbr.conf
  disable_wss_front
  log "已删除 Compose / 模式标记 / sysctl / WSS 配置"

  if [[ -t 0 ]]; then
    read -r -p "是否同时删除数据目录 ${DATA_DIR}？[y/N]: " remove_data || remove_data=""
  else
    remove_data="N"
  fi
  case "$remove_data" in
    y|Y|yes|YES)
      [[ -z "$RUSTDESK_DIR" || "$RUSTDESK_DIR" == "/" || "$RUSTDESK_DIR" == "/root" || ${#RUSTDESK_DIR} -lt 5 ]] && fail "RUSTDESK_DIR 路径不安全，已中止"
      rm -rf "$RUSTDESK_DIR"
      log "已删除 ${RUSTDESK_DIR}"
      ;;
    *) warn "已保留数据目录，可用于后续恢复" ;;
  esac
  pause
}

# ---------- 菜单 ----------
menu_loop() {
  local choice
  [[ -t 0 ]] || { warn "非交互环境，请使用: install | install-tcp | config | uninstall"; return 0; }
  while true; do
    print_banner
    printf '%b\n' "${C_CYAN}${C_BOLD}请选择操作${C_RESET}"
    printf '  %b1.%b 安装 / 更新 — 标准版 (host，TCP+UDP)\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
    printf '  %b2.%b 安装 / 更新 — 强制中继版 (host，仍需 UDP:21116)\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
    printf '  %b3.%b 查看当前配置\n' "${C_GREEN}${C_BOLD}" "${C_RESET}"
    printf '  %b4.%b 卸载 RustDesk\n' "${C_RED}${C_BOLD}" "${C_RESET}"
    printf '  %b0.%b 退出脚本\n' "${C_DIM}" "${C_RESET}"
    print_line
    printf '%b注意: 旧「bridge 无UDP」方案已废弃（会连不上）。开源版无法真正免 UDP 注册。%b\n' "${C_DIM}" "${C_RESET}"
    local cur; cur="$(detect_current_mode)"
    [[ "$cur" != "unknown" ]] && printf '%b当前已部署: %s%b\n' "${C_DIM}" "$(mode_label "$cur")" "${C_RESET}"
    read -r -p "请输入选项 [0-4]: " choice || choice="0"
    case "$choice" in
      1) install_or_update host ;;
      2) install_or_update tcp ;;
      3) show_config ;;
      4) uninstall_rustdesk ;;
      0) break ;;
      *) warn "无效选项"; sleep 1 ;;
    esac
  done
}

main() {
  require_root
  case "${1:-menu}" in
    install|update|install-host|host) install_or_update host ;;
    install-tcp|tcp|tcp-only)         install_or_update tcp ;;
    config|show)                      show_config ;;
    uninstall|remove)                 uninstall_rustdesk ;;
    menu)                             menu_loop ;;
    *)
      warn "未知参数: ${1}"
      printf '可用参数:\n'
      printf '  menu | install | install-tcp | config | uninstall\n'
      printf '环境变量:\n'
      printf '  RUSTDESK_PUBLIC_IP=公网IP\n'
      printf '  RUSTDESK_PUBLIC_IPV6=公网IPv6\n'
      printf '  RUSTDESK_DOMAIN=域名(可选,自动匹配证书)\n'
      printf '  RUSTDESK_IMAGE=自定义镜像\n'
      printf '  RUSTDESK_MANAGE_FIREWALL=true\n'
      exit 1 ;;
  esac
}

main "$@"