#!/usr/bin/env bash
# deploy.sh — 单机全栈部署,为小内存机(512MB)调优:SQLite(文件)+ Redis(apt)
# + 控制面 + agent + sing-box,全部以**非特权 pangolin 用户**运行。免 Docker。
#
# 幂等:重跑只刷新二进制/配置,不轮换 REALITY 密钥或数据库(已发的客户端配置不失效)。
#
# 用法(root):
#   sudo VPS_IP=1.2.3.4 bash deploy/single-node/deploy.sh
#
# 二进制:机上有 `go` 则就地构建;否则需**预先**把 pangolin-{server,agent,migrate}+nodectl
# 放进 /usr/local/bin(512MB 推荐:别处交叉编译 `CGO_ENABLED=0 GOOS=linux GOARCH=amd64`,
# 因 SQLite 走纯 Go 的 modernc,无需 CGO)。
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
HERE="$REPO_ROOT/deploy/single-node"

NODE_UUID="${NODE_UUID:-22222222-2222-2222-2222-222222222222}"
REALITY_SNI="${REALITY_SNI:-www.apple.com}"
REGION="${REGION:-HK}"
NAME_ZH="${NAME_ZH:-单节点 · 测试}"
NAME_EN="${NAME_EN:-Single Node}"
HTTP_PORT="${HTTP_PORT:-8080}"
GRPC_PORT="${GRPC_PORT:-9443}"
REALITY_PORT="${REALITY_PORT:-443}"   # 与 bootstrap 防火墙放行的 443/tcp 对齐
# Hy2 默认【关闭】:控制面尚未接线 Hy2 TLS 证书(handler_grpc.go 仅发 ListenPort,
# 不发 CertPath/KeyPath)→ 开启会令 sing-box 报 "missing certificate"。待代码补全
# Hy2 证书下发后再传 HY2_PORT=443 开启。
HY2_PORT="${HY2_PORT:-}"

ETC="/etc/pangolin"
AGENT_ETC="/etc/pangolin-agent"
SB_ETC="/etc/sing-box"
DATA_DIR="/var/lib/pangolin"
DB_FILE="$DATA_DIR/pangolin.db"
BIN="/usr/local/bin"
PUSER="pangolin"

log()  { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; exit 1; }

[ "$(id -u)" -eq 0 ] || die "must run as root (sudo)."
command -v openssl >/dev/null 2>&1 || die "missing dependency: openssl"

export DEBIAN_FRONTEND=noninteractive

# ── 0. pangolin 非特权用户 ────────────────────────────────────────────────────
if ! id -u "$PUSER" >/dev/null 2>&1; then
  log "创建非特权用户 $PUSER"
  useradd --system --create-home --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$PUSER"
fi

# ── 1. 系统依赖(apt:Redis + sqlite3;免 Docker)───────────────────────────────
miss=""
# polkitd:让非特权 pangolin 经 polkit 规则重启 sing-box(精简 Debian 默认无)。
for p in redis-server sqlite3 curl ca-certificates polkitd; do
  dpkg -s "$p" >/dev/null 2>&1 || miss="$miss $p"
done
if [ -n "$miss" ]; then
  log "安装依赖:$miss"
  apt-get update -q
  # shellcheck disable=SC2086
  apt-get install -y --no-install-recommends $miss
fi

if ! command -v sing-box >/dev/null 2>&1; then
  warn "sing-box 未安装 — 尝试官方脚本安装 ..."
  curl -fsSL https://sing-box.app/install.sh | sh \
    || die "sing-box 安装失败;请手动装后重跑。"
fi

if [ -z "${VPS_IP:-}" ]; then
  VPS_IP="$(curl -fsS --max-time 5 https://api.ipify.org || true)"
  [ -n "$VPS_IP" ] || die "无法自动探测 VPS_IP;请显式传 VPS_IP=..."
  log "自动探测 VPS_IP=$VPS_IP"
fi
ENDPOINT="$VPS_IP:$REALITY_PORT"

mkdir -p "$ETC" "$AGENT_ETC" "$SB_ETC" "$DATA_DIR"

# ── 2. 二进制(有 go 则构建;否则用预置)──────────────────────────────────────
if command -v go >/dev/null 2>&1; then
  log "就地构建二进制(server/agent/nodectl/migrate)..."
  (
    cd "$REPO_ROOT/server"
    for cmd in server agent nodectl migrate; do
      CGO_ENABLED=0 go build -o "$BIN/pangolin-tmp-$cmd" "./cmd/$cmd"
    done
  )
  mv -f "$BIN/pangolin-tmp-server"  "$BIN/pangolin-server"
  mv -f "$BIN/pangolin-tmp-agent"   "$BIN/pangolin-agent"
  mv -f "$BIN/pangolin-tmp-nodectl" "$BIN/nodectl"
  mv -f "$BIN/pangolin-tmp-migrate" "$BIN/pangolin-migrate"
else
  log "机上无 go → 使用预置二进制"
  for b in pangolin-server pangolin-agent nodectl pangolin-migrate; do
    [ -x "$BIN/$b" ] || die "缺二进制 $BIN/$b:机上无 go,请先交叉编译并放入 $BIN"
  done
fi

# ── 3. Redis(apt 原生 systemd 服务,绑 127.0.0.1)──────────────────────────────
log "启动 Redis ..."
systemctl enable --now redis-server >/dev/null 2>&1 || systemctl enable --now redis >/dev/null 2>&1 || true
until redis-cli ping >/dev/null 2>&1; do sleep 1; done
log "Redis ready."

# ── 4. 控制面密钥(幂等)──────────────────────────────────────────────────────
if [ ! -f "$ETC/jwt_private.pem" ]; then
  log "生成 RS256 JWT 密钥对 ..."
  openssl genrsa -out "$ETC/jwt_private.pem" 2048
  openssl rsa -in "$ETC/jwt_private.pem" -pubout -out "$ETC/jwt_public.pem"
fi
[ -f "$ETC/webhook_secret" ] || openssl rand -hex 32 > "$ETC/webhook_secret"
[ -f "$ETC/derive_key" ]     || openssl rand -hex 32 > "$ETC/derive_key"
WEBHOOK_SECRET="$(cat "$ETC/webhook_secret")"
NODE_DERIVE_KEY="$(cat "$ETC/derive_key")"

# Node CA(ECDSA P-256,SEC1 "EC PRIVATE KEY";mtls.NewCA 读它)
if [ ! -f "$ETC/ca.key" ] || [ ! -f "$ETC/ca.crt" ]; then
  log "生成 Node CA ..."
  openssl ecparam -name prime256v1 -genkey -noout -out "$ETC/ca.key"
  openssl req -x509 -new -key "$ETC/ca.key" -days 3650 -out "$ETC/ca.crt" \
    -subj "/O=Pangolin/CN=Pangolin Node CA" \
    -addext "basicConstraints=critical,CA:TRUE" \
    -addext "keyUsage=critical,keyCertSign,cRLSign"
fi

# gRPC server cert(CA 签发,SAN localhost+127.0.0.1)
if [ ! -f "$ETC/grpc.key" ] || [ ! -f "$ETC/grpc.crt" ]; then
  log "签发 gRPC server 证书 ..."
  openssl ecparam -name prime256v1 -genkey -noout -out "$ETC/grpc.key"
  openssl req -new -key "$ETC/grpc.key" -out "$ETC/grpc.csr" -subj "/CN=pangolin-control-plane"
  EXT="$(mktemp)"
  cat > "$EXT" <<EOF
subjectAltName=DNS:localhost,IP:127.0.0.1
extendedKeyUsage=serverAuth
EOF
  openssl x509 -req -in "$ETC/grpc.csr" -CA "$ETC/ca.crt" -CAkey "$ETC/ca.key" \
    -CAcreateserial -days 825 -out "$ETC/grpc.crt" -extfile "$EXT"
  rm -f "$EXT" "$ETC/grpc.csr"
fi

# ── 5. REALITY 密钥对(持久,跨重部署稳定)─────────────────────────────────────
if [ ! -f "$ETC/reality.env" ]; then
  log "生成 REALITY 密钥对 ..."
  KP="$(sing-box generate reality-keypair)"
  R_PRK="$(printf '%s\n' "$KP" | awk '/PrivateKey/{print $2}')"
  R_PBK="$(printf '%s\n' "$KP" | awk '/PublicKey/{print $2}')"
  R_SID="$(openssl rand -hex 8)"
  [ -n "$R_PRK" ] && [ -n "$R_PBK" ] || die "解析 reality-keypair 失败。"
  {
    printf 'REALITY_PRK=%s\n' "$R_PRK"
    printf 'REALITY_PBK=%s\n' "$R_PBK"
    printf 'REALITY_SHORT_ID=%s\n' "$R_SID"
  } > "$ETC/reality.env"
fi
# shellcheck disable=SC1091
. "$ETC/reality.env"

# ── 6. server.env(SQLite)─────────────────────────────────────────────────────
log "写 server.env(DB_DRIVER=sqlite)..."
cat > "$ETC/server.env" <<EOF
DB_DRIVER=sqlite
DB_DSN=$DB_FILE
REDIS_ADDR=127.0.0.1:6379
ADDR=:$HTTP_PORT
JWT_PRIVATE_KEY_PATH=$ETC/jwt_private.pem
JWT_KEY_ID=node-key-1
JWT_PUBLIC_KEYS=node-key-1:$ETC/jwt_public.pem
WEBHOOK_SECRET=$WEBHOOK_SECRET
NODE_DERIVE_KEY=$NODE_DERIVE_KEY
GRPC_ADDR=:$GRPC_PORT
CA_KEY_PATH=$ETC/ca.key
CA_CERT_PATH=$ETC/ca.crt
GRPC_CERT_PATH=$ETC/grpc.crt
GRPC_KEY_PATH=$ETC/grpc.key
PANGOLIN_PUBLIC_URL=http://$VPS_IP:$HTTP_PORT
PANGOLIN_RULES_DIR=$DATA_DIR/rules
EOF
if [ -n "${SMTP_HOST:-}" ]; then
  cat >> "$ETC/server.env" <<EOF
SMTP_HOST=$SMTP_HOST
SMTP_PORT=${SMTP_PORT:-587}
SMTP_USERNAME=${SMTP_USERNAME:-}
SMTP_PASSWORD=${SMTP_PASSWORD:-}
SMTP_FROM=${SMTP_FROM:-no-reply@pangolin.app}
EOF
fi

# ── 6b. 国内分流 rule-set(#5)──────────────────────────────────────────────────
# 自托管 geoip-cn / geosite-cn(控制面 /v1/rules/*.srs 静态服务,客户端 sing-box
# 直连下载)。GitHub 在国内被墙,故控制面代为下发,避免鸡生蛋。
log "拉取国内分流 rule-set(geoip-cn / geosite-cn)..."
RULES_DIR="$DATA_DIR/rules"
install -d -m 755 "$RULES_DIR"
curl -fsSL -o "$RULES_DIR/geoip-cn.srs" \
  https://github.com/SagerNet/sing-geoip/raw/rule-set/geoip-cn.srs \
  || log "WARN: geoip-cn.srs 拉取失败,国内分流将不可用"
curl -fsSL -o "$RULES_DIR/geosite-cn.srs" \
  https://github.com/SagerNet/sing-geosite/raw/rule-set/geosite-cn.srs \
  || log "WARN: geosite-cn.srs 拉取失败,国内分流将不可用"

# ── 7. 迁移 + seed(SQLite)────────────────────────────────────────────────────
log "执行迁移(sqlite)..."
DB_DRIVER=sqlite DB_DSN="$DB_FILE" "$BIN/pangolin-migrate" up

log "seed 节点行 ..."
SEED="$(mktemp)"
HY2_SEED="${HY2_PORT:-NULL}"   # 空 → SQL NULL(不下发 Hy2)
# shellcheck disable=SC2153
sed \
  -e "s|__NODE_UUID__|$NODE_UUID|g" \
  -e "s|__REGION__|$REGION|g" \
  -e "s|__NAME_ZH__|$NAME_ZH|g" \
  -e "s|__NAME_EN__|$NAME_EN|g" \
  -e "s|__ENDPOINT__|$ENDPOINT|g" \
  -e "s|__HY2_PORT__|$HY2_SEED|g" \
  -e "s|__REALITY_PBK__|$REALITY_PBK|g" \
  -e "s|__REALITY_PRK__|$REALITY_PRK|g" \
  -e "s|__REALITY_SHORT_ID__|$REALITY_SHORT_ID|g" \
  -e "s|__REALITY_SNI__|$REALITY_SNI|g" \
  "$HERE/seed-node.sql.tmpl" > "$SEED"
sqlite3 "$DB_FILE" < "$SEED"
rm -f "$SEED"

# ── 8. 发 bootstrap token + 写 agent.env ──────────────────────────────────────
log "签发 agent bootstrap token ..."
TOKEN="$(REDIS_ADDR=127.0.0.1:6379 "$BIN/nodectl" bootstrap-token -node="$NODE_UUID")"
[ -n "$TOKEN" ] || die "签发 bootstrap token 失败。"
cp -f "$ETC/ca.crt" "$AGENT_ETC/ca.crt"
cat > "$AGENT_ETC/agent.env" <<EOF
PANGOLIN_AGENT_CONTROL_PLANE=localhost:$GRPC_PORT
PANGOLIN_AGENT_SERVER_NAME=localhost
PANGOLIN_AGENT_BOOTSTRAP_TOKEN=$TOKEN
PANGOLIN_AGENT_DERIVE_KEY=$NODE_DERIVE_KEY
PANGOLIN_AGENT_SINGBOX_CONFIG=$SB_ETC/config.json
PANGOLIN_AGENT_SINGBOX_UNIT=sing-box
PANGOLIN_AGENT_STATE_DIR=$AGENT_ETC
EOF

# ── 9. 降权:目录/文件归 pangolin ─────────────────────────────────────────────
chown -R "$PUSER:$PUSER" "$ETC" "$AGENT_ETC" "$SB_ETC" "$DATA_DIR"
chmod 700 "$ETC" "$AGENT_ETC"
chmod 600 "$ETC/jwt_private.pem" "$ETC/ca.key" "$ETC/grpc.key" \
          "$ETC/webhook_secret" "$ETC/derive_key" "$ETC/reality.env" \
          "$ETC/server.env" "$AGENT_ETC/agent.env"

# ── 10. systemd 单元 + polkit(允许 pangolin 重启 sing-box)────────────────────
log "安装 systemd 单元 + polkit 规则 ..."
install -m 644 "$HERE/systemd/pangolin-server.service" /etc/systemd/system/
install -m 644 "$HERE/systemd/pangolin-agent.service"  /etc/systemd/system/
# sing-box 路径按实际安装位置渲染(官方 .deb 装在 /usr/bin,而非 /usr/local/bin)。
SB_BIN="$(command -v sing-box)"
sed "s#/usr/local/bin/sing-box#${SB_BIN}#g" \
  "$HERE/systemd/sing-box.service" > /etc/systemd/system/sing-box.service
chmod 644 /etc/systemd/system/sing-box.service
install -d -m 755 /etc/polkit-1/rules.d
install -m 644 "$HERE/polkit/49-pangolin-singbox.rules" /etc/polkit-1/rules.d/
systemctl daemon-reload

log "启动控制面(pangolin-server,以 $PUSER 运行)..."
systemctl enable --now pangolin-server.service
log "等待控制面 HTTP :$HTTP_PORT ..."
until curl -fsS --max-time 2 "http://127.0.0.1:$HTTP_PORT/healthz" >/dev/null 2>&1; do sleep 1; done
log "控制面健康。"

log "启用 sing-box + 启动 agent ..."
systemctl enable sing-box.service
systemctl enable --now pangolin-agent.service

# 放行控制面 API 端口(若 ufw 启用)。⚠️ 明文,生产应前置 TLS。
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
  ufw allow "${HTTP_PORT}/tcp" >/dev/null 2>&1 || true
  log "ufw 放行 ${HTTP_PORT}/tcp(控制面 API,明文)"
fi

# ── 11. 摘要 ──────────────────────────────────────────────────────────────────
log "完成。单机栈已起(SQLite + pangolin 用户)。"
cat <<EOF

  控制面 API   : http://$VPS_IP:$HTTP_PORT   (⚠️ 明文;防火墙需放行 ${HTTP_PORT}/tcp)
  Node uuid    : $NODE_UUID
  Endpoint     : $ENDPOINT   SNI=$REALITY_SNI   (REALITY ${REALITY_PORT}/tcp)
  REALITY pbk  : $REALITY_PBK
  Hy2          : ${HY2_PORT:+${HY2_PORT}/udp}${HY2_PORT:-关闭(控制面 Hy2 证书未接线)}

验证:
  journalctl -u pangolin-agent -n 50 --no-pager     # enrolled + Register
  systemctl status sing-box --no-pager              # active (running)
  grep -A2 reality-in $SB_ETC/config.json           # listen_port $REALITY_PORT
  ss -tlnp | grep -E '$HTTP_PORT|$REALITY_PORT'     # 监听
EOF
