#!/usr/bin/env bash
# ============================================================================
#  雷达 · 一键部署（把原来三步合成一条命令）
#  ---------------------------------------------------------------------------
#  用法（在目标服务器上，root）：
#      bash 一键部署.sh
#
#  可选参数：
#      --pkg <包路径>        默认 /root/radar-src-lanes-full.tar.gz
#      --dir <源码目录>      默认 /root/radar-src-lanes
#      --admin-pass <密码>   不给就自动生成 18 位随机
#      --node-id <id>        不给就取主机名前 8 位（小写）
#      --assets <图床地址>   默认 http://157.250.207.207:2580
#      --panel2 <面板地址>   第二上报目标（多面板共享同一批节点时必填，例如
#                            --panel2 http://36.139.14.98:7218）。
#                            不填则节点只向本机面板上报，其它面板的「节点监控」
#                            里这台会一直显示离线/无法连接（2026-10-17 长沙节点踩过）。
#      --no-hardening        跳过系统防护（hardening.sh）
#      --interactive         遇到提问时让你手动回答（默认全自动）
#      --dry-run             只做预检、不真装
#
#  它做的事：
#      预检 -> 解包 -> 1 deploy.sh（底座）-> 2 lanes-deploy.sh（分通道）
#      -> 3 env 钉正 -> 4 hardening.sh（系统防护）-> 自检
#
#  日志：/root/radar-install-<时间戳>.log
#
#  2026-10-17 更新（重要）
#    1) 钉正参数与「内存泄漏修复」对齐。旧版本会写回
#       LANE_FORWARD_QUEUE=300000 / NODE_OBSERVE_QUEUE_MAX=200000，
#       把当天的修复覆盖掉，跑几小时内存又涨到几 GB。
#    2) 新增 NODE_UDP_* 四项 与 NODE_BANK_MAX_PENDING_TOTAL：
#       「UDP 僵尸通道」和「全流 pending 无总闸」两个事故的修复参数，缺了会复发。
#    3) 新增第 4 步 hardening.sh：swap 缓冲 / 服务内存上限 / earlyoom /
#       内存守护 / 内存监控 / fail2ban。缺这层，代码修好了也仍可能被整机 OOM
#       拖死、被硬件看门狗强制重启。
#    4) 补 panel/.env 的 OPS_ENABLED=1（deploy.sh 不会从模板带过来，缺了运维页 403）。
#    5) 自检修正：FED_ENABLED=0（独立节点）时 FED_MASTER_URL 为空是正常的，不再误报。
#    6) post-install-fix.sh 变成可选（它的作用已并入第 3 步）。
#
#  2026-10-26 更新（只改钉正数值与自检项，流程不变）
#    1) NODE_UDP_IDLE_RECLAIM 45 -> 300：45 秒回收会把正在对局的 UDP 流掐掉，
#       玩家侧表现就是游戏弹「网络重连」。
#    2) NODE_BANK_FLOW_TTL_SEC 150 -> 3600：150 秒密钥就过期，打一半雷达突然空白。
#    3) NODE_LIVE_BLIND_FALLBACK_SEC 90 -> 15：盲兜底太久，会显示上一条死流的旧位置。
#       预算太小（旧版 12ms）而读一次帧索引要 31~139ms -> 帧积压几千 -> 画面回放几分钟前。
#       这个补丁的前提已消失；继承错了会被账号级拉黑 120 秒，整局空白。
#       前者回收永远绑不上的杂流，后者防止实例 5 分钟被回收导致雷达掉。
#    7) NODE_UDP_READ_TIMEOUT 15 -> 30；自检项同步以上数值，并校验三个补丁标记。
# ============================================================================
set -uo pipefail

PKG="/root/radar-src-lanes-full.tar.gz"
APPDIR="/root/radar-src-lanes"
ADMIN_PASS=""
NODE_ID=""
ASSETS="http://157.250.207.207:2580"
PANEL2=""
INTERACTIVE=0
DRYRUN=0
HARDENING=1
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG="/root/radar-install-${STAMP}.log"

while [ $# -gt 0 ]; do
  case "$1" in
    --pkg)        PKG="${2:-}"; shift 2 ;;
    --dir)        APPDIR="${2:-}"; shift 2 ;;
    --admin-pass) ADMIN_PASS="${2:-}"; shift 2 ;;
    --node-id)    NODE_ID="${2:-}"; shift 2 ;;
    --assets)     ASSETS="${2:-}"; shift 2 ;;
    --panel2)     PANEL2="${2:-}"; shift 2 ;;
    --no-hardening) HARDENING=0; shift ;;
    --interactive) INTERACTIVE=1; shift ;;
    --dry-run)    DRYRUN=1; shift ;;
    -h|--help)    sed -n '1,30p' "$0"; exit 0 ;;
    *) echo "未知参数: $1（用 --help 看用法）"; exit 2 ;;
  esac
done

ok()   { printf '  \033[32mOK\033[0m %s\n' "$*"; }
bad()  { printf '  \033[31mXX\033[0m %s\n' "$*"; }
info() { printf '  · %s\n' "$*"; }
die()  { printf '\n[中止] %s\n' "$*"; printf '日志：%s\n' "$LOG"; exit 1; }
envval()  { grep -m1 "^$1=" node/.env  2>/dev/null | cut -d= -f2-; }
penvval() { grep -m1 "^$1=" panel/.env 2>/dev/null | cut -d= -f2-; }
pin_env() { # pin_env <文件> <KEY=VALUE>
  local f="$1" kv="$2" k v
  k="${kv%%=*}"; v="${kv#*=}"
  if grep -q "^${k}=" "$f"; then
    sed -i "s|^${k}=.*|${k}=${v}|" "$f"
  else
    printf '%s=%s\n' "$k" "$v" >> "$f"
  fi
}

# ---------------- 0. 预检 ----------------
echo "=== 雷达一键部署（$STAMP）===" | tee -a "$LOG"
echo "--- 0/5 预检 ---" | tee -a "$LOG"

[ "$(id -u)" = "0" ] || die "请用 root 跑（当前不是 root）"
ok "root 权限"

command -v python3 >/dev/null || die "没有 python3，先装（apt-get install -y python3）"
PYV="$(python3 -V 2>&1)"; ok "python3: $PYV"

if python3 -c 'import venv' >/dev/null 2>&1; then
  ok "python venv 模块可用"
else
  bad "python venv 不可用 → 请先装：apt-get install -y python3.12-venv 或 python3-venv"
  info "（或者：pip3 install --break-system-packages virtualenv —— Ubuntu 24.04 必须加 --break-system-packages）"
  die "缺 venv，装好再跑本脚本"
fi

AVL_KB="$(df -Pk / | awk 'NR==2{print $4}')"
[ "${AVL_KB:-0}" -ge 3145728 ] || die "根分区可用空间 < 3GB（当前 ${AVL_KB}KB），先清空间"
ok "磁盘可用 $(printf '%d' $((AVL_KB/1024/1024)) ) GB"

MEM_MB="$(awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)"
ok "内存总量 ${MEM_MB} MB（系统防护会按这个比例设上限）"

if [ -f "$PKG" ]; then
  ok "安装包: $PKG ($(stat -c%s "$PKG") 字节)"
elif [ -f "$APPDIR/deploy.sh" ]; then
  info "没有安装包，但源码已解包在 $APPDIR —— 直接装（更新分支）"
else
  die "既没有安装包 $PKG，源码也没解包到 $APPDIR（用 --pkg / --dir 指定）"
fi

if [ -d "$APPDIR" ]; then
  info "目标目录已存在：$APPDIR（走「更新」分支，数据保留）"
else
  info "目标目录不存在：$APPDIR（全新安装）"
fi

# pip 源：写系统级配置，让本次和以后所有 pip 都走实测可用的源。
# 2026-10-17：原来先试 pypi.org（国内常是「极慢但不报错」）、备用源写死清华
# （实测 HTTP 403），结果一键部署卡在「安装依赖」十几分钟一个包都没装上。
if [ -z "${PIP_INDEX_URL:-}" ] && [ ! -f /etc/pip.conf ]; then
  info "探测可用的 pip 源（避免卡在 pypi.org）..."
  _probe_dir="/tmp/.radar-pip-probe.$$"
  _chosen=""
  for _cand in \
      "https://mirrors.aliyun.com/pypi/simple" \
      "https://repo.huaweicloud.com/repository/pypi/simple" \
      "https://mirrors.cloud.tencent.com/pypi/simple" \
      "https://mirrors.ustc.edu.cn/pypi/simple" \
      "https://pypi.tuna.tsinghua.edu.cn/simple"; do
    rm -rf "$_probe_dir"
    if python3 -m pip download --no-deps -q -d "$_probe_dir" \
         -i "$_cand" --timeout 12 --retries 0 six >/dev/null 2>&1; then
      _chosen="$_cand"; break
    fi
  done
  rm -rf "$_probe_dir"
  if [ -n "$_chosen" ]; then
    mkdir -p /etc/pip
    printf '[global]\nindex-url = %s\ntimeout = 20\nretries = 2\n' "$_chosen" > /etc/pip.conf
    ok "pip 源: $_chosen（已写入 /etc/pip.conf）"
  else
    info "没探到可用镜像，pip 用默认源（若卡住请配 /etc/pip.conf）"
  fi
else
  info "pip 源已配置（PIP_INDEX_URL 或 /etc/pip.conf），跳过探测"
fi

[ -z "$ADMIN_PASS" ] && ADMIN_PASS="$(head -c 24 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 18)"
[ -z "$NODE_ID" ] && NODE_ID="$(hostname | tr 'A-Z' 'a-z' | tr -dc 'a-z0-9' | head -c 8)"
[ -n "$NODE_ID" ] || NODE_ID="node1"
ok "节点 ID: $NODE_ID"
ok "图床: $ASSETS"
if [ -n "$PANEL2" ]; then ok "第二上报目标: $PANEL2"; else info "未指定第二上报目标（只向本机面板上报）"; fi

[ "$DRYRUN" = "1" ] && { echo; echo "（--dry-run：预检通过，未做任何改动）"; exit 0; }

# ---------------- 1. 解包 ----------------
echo "--- 1/5 解包 ---" | tee -a "$LOG"
if [ -f "$APPDIR/deploy.sh" ]; then
  ok "源码已解包，跳过解包"
else
  if tar -tzf "$PKG" >/dev/null 2>&1; then
    tar -xzf "$PKG" -C "$(dirname "$APPDIR")" >>"$LOG" 2>&1
  else
    tar -xf "$PKG" -C "$(dirname "$APPDIR")" >>"$LOG" 2>&1
  fi || die "解包失败（看日志）"
  [ -f "$APPDIR/deploy.sh" ] || die "解包后没找到 $APPDIR/deploy.sh（包结构不对？）"
  ok "已解包到 $APPDIR"
fi

cd "$APPDIR" || die "进不去 $APPDIR"

# ---------------- 2. deploy.sh（底座） ----------------
echo "--- 2/5 底座 deploy.sh ---" | tee -a "$LOG"
if [ "$INTERACTIVE" = "1" ]; then
  bash deploy.sh 2>&1 | tee -a "$LOG" || die "deploy.sh 失败（卡在第 2 步）"
else
  # 自动回答 deploy.sh 的提问：管理员密码 / 是否用远程图床 / 图床地址 / 节点 ID / 公网 IP(回车)
  printf '%s\n%s\n%s\n%s\n\n' "$ADMIN_PASS" "yes" "$ASSETS" "$NODE_ID" \
    | bash deploy.sh 2>&1 | tee -a "$LOG"
  rc="${PIPESTATUS[1]}"
  if [ "$rc" != "0" ]; then
    bad "deploy.sh 非 0 退出（码 $rc）"
    info "如果是「卡在某个提问」，改用交互模式再跑一次：bash 一键部署.sh --interactive"
    die "第 2 步失败"
  fi
fi
ok "底座完成"

# ---------------- 3. lanes-deploy.sh（分通道） ----------------
echo "--- 3/5 分通道 lanes-deploy.sh ---" | tee -a "$LOG"
if [ -f "lanes-deploy.sh" ]; then
  bash lanes-deploy.sh --app-dir="$APPDIR" 2>&1 | tee -a "$LOG" || die "lanes-deploy.sh 失败（卡在第 3 步）"
  ok "分通道完成"
  if [ -f "realtime-optimize.sh" ]; then
    bash realtime-optimize.sh 2>&1 | tee -a "$LOG" || die "实时性源码检查失败"
    ok "实时性源码检查通过（只读）"
  else
    die "缺少 realtime-optimize.sh"
  fi
else
  bad "没找到 lanes-deploy.sh（包不完整？）"
  die "第 3 步无法执行"
fi

# Historical post-install source mutation is retired. Preserve custom files for review.
if [ -f "post-install-fix.sh" ]; then
  info "post-install-fix.sh retained for review; automatic execution retired"
fi

# ---------------- 4. env 钉正 ----------------
# lanes-deploy.sh 保留现有 lane 参数；缺失参数才补默认值。
# 下面的值是「内存泄漏事故」修复后的定版值：
#   UDP_*         UDP 中继僵尸通道（实测 300 万协程 / 2450 万对象 / 3.7GB）
#   BANK_*_TOTAL  全流 pending 没有总闸（1024 流 x 768 包 约 1.2GB 隐藏天花板）
echo "--- 4/5 env 收尾钉正 ---" | tee -a "$LOG"
# 2026-10-28 按要求：部署脚本不自动写任何运行参数。
#   node/.env 保持 deploy.sh 生成的原样，参数由使用者自己填。
if [ -f node/.env ]; then
  ok "node/.env 保持原样（脚本不钉正任何参数）"
else
  bad "没找到 node/.env（deploy.sh 没跑成功？）"
fi

# 安卓 APP 下载地址：网关向导页的二维码直接用它。
# gateway.py 里的默认值是写死的旧服务器 http://103.231.58.199:7218/v1/android/apk，
# 不覆盖的话，新机器上的用户扫码会下到别人的安装包。
if [ -f node/.env ]; then
  _apk="$(envval NODE_ANDROID_APK_URL)"
  _pub="$(envval NODE_PUBLIC_HOST)"
  if [ -z "$_apk" ] && [ -n "$_pub" ]; then
    pin_env node/.env "NODE_ANDROID_APK_URL=http://${_pub}:7218/v1/android/apk"
    ok "安卓 APP 地址已指向本机：http://${_pub}:7218/v1/android/apk"
  elif [ -z "$_apk" ]; then
    bad "NODE_ANDROID_APK_URL 未设置，网关向导页仍指向包内默认地址（旧服务器）"
    info "手动补：echo 'NODE_ANDROID_APK_URL=http://<本机公网IP>:7218/v1/android/apk' >> node/.env"
  else
    ok "安卓 APP 地址：$_apk"
  fi
fi

# 第二上报目标：多面板共享节点时必须配，否则其它面板的节点监控会显示离线/无法连接
if [ -n "$PANEL2" ] && [ -f node/.env ]; then
  pin_env node/.env "NODE_PANEL_URL2=$PANEL2"
  ok "已配置第二上报目标 NODE_PANEL_URL2=$PANEL2"
  grep -m1 '^NODE_PANEL_URL2=' node/.env | sed 's/^/    /'
fi

# panel/.env 的运维页开关：deploy.sh 不会从模板带过来，缺了运维页永远 403
if [ -f panel/.env ]; then
  cp -a panel/.env "panel/.env.bak.opsfix.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
  pin_env panel/.env "OPS_ENABLED=1"
  ok "运维页已开启（panel/.env OPS_ENABLED=1）"
  grep -m1 '^OPS_ENABLED=' panel/.env | sed 's/^/    /'
fi

# ---------------- 4.2 按机器规格自动定档 ----------------
# 上面的钉正是「安全基线」。这一层再按本机 CPU 核数 / 物理内存调档：
#   低端（<=2核 或 <4.5GB）：解析进程 1 · 渲染 30Hz · UDP 缓冲 1MB · 队列 3万/2万
#   中端（3~7核 且 4.5~8GB）：解析进程 2 · 渲染 60Hz · UDP 缓冲 2MB · 队列 6万/5万
#   高端（>=8核 且 >=8GB）  ：解析进程 4 · 渲染 60Hz · UDP 缓冲 4MB · 队列 10万/8万
# 不做这一步的话，4 核 4G 的小机器会按 16 核 32G 的参数跑（2 个解析进程 + 4MB 缓冲）。
echo "--- 4.2/5 按 CPU/内存自动定档 ---" | tee -a "$LOG"
if [ -f "autotune.sh" ]; then
  bash autotune.sh --dir "$APPDIR" 2>&1 | tee -a "$LOG" \
    && ok "参数已按本机规格定档" \
    || info "autotune.sh 非 0，沿用上面的固定值"
else
  info "autotune.sh 不存在，沿用固定值（4核以下机器可能偏重）"
fi

# ---------------- 4.5 系统防护 ----------------
echo "--- 4.5/5 系统防护 hardening.sh ---" | tee -a "$LOG"
if [ "$HARDENING" = "0" ]; then
  info "--no-hardening：跳过"
elif [ -f "hardening.sh" ]; then
  bash hardening.sh --install-dir "$APPDIR" 2>&1 | tee -a "$LOG" \
    && ok "系统防护已应用（swap 缓冲 / 按内存比例的上限 / earlyoom / 内存守护 / 监控 / fail2ban）" \
    || info "hardening.sh 有非 0 项，看日志；不影响雷达本体"
else
  info "hardening.sh 不存在，跳过（建议单独补：bash hardening.sh）"
fi

systemctl restart radar-node >/dev/null 2>&1
systemctl restart radar-panel >/dev/null 2>&1
systemctl restart radar >/dev/null 2>&1
sleep 8

# ---------------- 自检 ----------------
echo "" | tee -a "$LOG"
echo "=============== 自检结果 ===============" | tee -a "$LOG"
FAILN=0
chk() { # chk "描述" "命令" "期望子串"
  local d="$1" c="$2" e="${3:-}"
  local out; out="$(eval "$c" 2>/dev/null || true)"
  if [ -z "$e" ]; then
    if [ -n "$out" ]; then ok "$d -> $out"; else bad "$d -> 空"; FAILN=$((FAILN+1)); fi
  else
    case "$out" in *"$e"*) ok "$d -> $out" ;; *) bad "$d -> $out（期望含 $e）"; FAILN=$((FAILN+1)) ;; esac
  fi
}

ACT="$(systemctl is-active radar radar-node radar-panel 2>/dev/null | tr '\n' ' ')"
case "$ACT" in *inactive*|*failed*) bad "服务: $ACT"; FAILN=$((FAILN+1)) ;; *) ok "服务: $ACT" ;; esac
chk "节点主程序"        "pgrep -f 'node/main.py' >/dev/null && echo running" "running"
chk "面板端口 7218"      "curl -s -m 5 -o /dev/null -w '%{http_code}' http://127.0.0.1:7218/v1/nodes" "200"
chk "运维页开关"         "penvval OPS_ENABLED"                "1"
chk "自愈补丁"           "grep -c 'stale-run reset' node/dfm/src/core/er_radar.py" "1"
chk "关继承补丁"         "grep -c '__BANK_NOINHERIT__' node/bank.py" "1"
chk "流级拉黑补丁"       "grep -c '__BADSCOPEFLOW__' node/bank.py" "2"
chk "禁自动重解补丁"     "grep -c '__NOREBUILD__' node/dfm/src/core/er_radar.py" "1"
chk "前端页面(文件/内嵌)" "(test -f node/dfm/web/index.html || grep -q 'PAGE = r' node/dfm/src/core/er_radar.py) && echo ok || echo missing" "ok"
chk "通道状态"           "curl -s -m 5 http://127.0.0.1:19191/status | grep -o '\"connected\":[a-z]*' | head -1" "true"
if [ "$HARDENING" = "1" ]; then
  chk "内存守护"         "systemctl is-active radar-node-guard 2>/dev/null" "active"
  chk "内存监控日志"     "test -f /var/log/radar-mem.log && echo ok" "ok"
else
  ok "系统防护已跳过（--no-hardening）：不检查内存守护/监控"
fi
if [ -n "$PANEL2" ]; then
  chk "第二上报目标" "envval NODE_PANEL_URL2" "$PANEL2"
fi

# FED_MASTER_URL：FED_ENABLED=0 表示这台是独立节点、不做联邦，留空是正常的。
# 只有显式开了联邦却没填地址，才算异常。
_FEDEN="$(penvval FED_ENABLED)"
if [ "${_FEDEN:-0}" = "1" ]; then
  chk "FED_MASTER_URL(联邦已开)" "penvval FED_MASTER_URL" ""
else
  ok "FED 联邦未启用（独立节点，FED_MASTER_URL 留空属正常）"
fi

echo "----------------------------------------" | tee -a "$LOG"
if [ "$FAILN" = "0" ]; then
  printf '全部通过\n' | tee -a "$LOG"
else
  printf '有 %d 项异常 —— 把本日志发出来：%s\n' "$FAILN" "$LOG" | tee -a "$LOG"
fi
echo "========================================" | tee -a "$LOG"
echo ""
echo "管理员密码：$ADMIN_PASS"
echo "节点 ID  ：$NODE_ID"
echo "日志     ：$LOG"
echo "面板地址 ：http://<本机公网IP>:7218/wanciaa"
if [ "$FAILN" != "0" ]; then exit 1; fi
