#!/usr/bin/env bash
# ============================================================
# zqagent 节点一键安装（全新机器 / 无需 Node）
#
#   curl -fsSL https://agent.cd-zq.com/downloads/zqagent/install-node.sh | sudo bash
#
# 做什么：
#   1. 从官方清单读取最新版本，按平台/架构下载单文件二进制
#   2. 校验 SHA-256、自检版本号后安装到 /usr/local/bin/zqagent
#   3. 装成系统服务（Linux: systemd；macOS: launchd），开机自启 + 崩溃拉起
#   4. 之后由节点自己静默下载新版本、空闲时自动重启（需配置验签公钥）
#
# 通过管道执行时无法交互，所有配置都由参数/环境变量给定。
# 需要自定义端口、数据目录、验签公钥时，请先下载再执行：
#   curl -fsSLO https://agent.cd-zq.com/downloads/zqagent/install-node.sh
#   sudo bash install-node.sh --port 9000 --data-dir /data/zqagent --pubkey "$PUBKEY"
#
# 子命令：
#   install（默认） 安装或升级到清单中的最新版本并（重）装服务
#   status          查看服务与更新状态
#   logs            跟踪日志
#   restart         重启服务
#   rollback        回滚到上一次自动更新前的版本
#   uninstall       停止并移除服务（保留数据目录）
#   print-config    只打印将要执行的动作，不落地
#
# 参数 / 环境变量：
#   --version X      安装指定版本（默认取清单里的最新版）
#   --channel NAME   清单通道，默认 stable
#   --base-url URL   清单与产物根地址，默认 https://agent.cd-zq.com/downloads/zqagent
#   --pubkey KEY     自动更新的验签公钥（也可用 ZQAGENT_UPDATE_PUBKEY）；
#                    不给时自动从 $BASE_URL/update-public.b64 获取并打印指纹
#   --port N         监听端口，默认 8787
#   --host ADDR      监听地址，默认 127.0.0.1
#   --data-dir DIR   数据目录
#   --bin-dir DIR    二进制安装目录（root 默认 /usr/local/bin，普通用户 ~/.local/bin）
#   --no-service     只装二进制，不装服务
#   --service-name N Linux systemd unit 名，默认 zqagent
#   --label NAME     macOS launchd label，默认 com.zqagent.node
# ============================================================
set -euo pipefail

ACTION="install"
CHANNEL="${CHANNEL:-stable}"
BASE_URL="${BASE_URL:-https://agent.cd-zq.com/downloads/zqagent}"
WANT_VERSION="${VERSION:-}"
PUBKEY="${ZQAGENT_UPDATE_PUBKEY:-}"
PORT="${PORT:-8787}"
HOST="${HOST:-127.0.0.1}"
DATA_DIR="${DATA_DIR:-}"
SERVICE_NAME="${SERVICE_NAME:-zqagent}"
LABEL="${LABEL:-com.zqagent.node}"
INSTALL_SERVICE=1

if [ -z "${BIN_DIR:-}" ]; then
  if [ "$(id -u)" -eq 0 ]; then BIN_DIR="/usr/local/bin"; else BIN_DIR="$HOME/.local/bin"; fi
fi

while [ $# -gt 0 ]; do
  case "$1" in
    install|status|logs|restart|rollback|uninstall|print-config) ACTION="$1"; shift;;
    --version) WANT_VERSION="${2:-}"; shift 2;;
    --channel) CHANNEL="${2:-}"; shift 2;;
    --base-url) BASE_URL="${2:-}"; shift 2;;
    --pubkey) PUBKEY="${2:-}"; shift 2;;
    --port) PORT="${2:-}"; shift 2;;
    --host) HOST="${2:-}"; shift 2;;
    --data-dir) DATA_DIR="${2:-}"; shift 2;;
    --bin-dir) BIN_DIR="${2:-}"; shift 2;;
    --service-name) SERVICE_NAME="${2:-}"; shift 2;;
    --label) LABEL="${2:-}"; shift 2;;
    --no-service) INSTALL_SERVICE=0; shift;;
    -h|--help) sed -n '2,45p' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
    *) echo "未知参数：$1（--help 查看用法）" >&2; exit 1;;
  esac
done
BASE_URL="${BASE_URL%/}"
# 内网 http 源（仅自测）：节点默认要求 https，这里显式放行并提示
UPDATE_INSECURE=""
case "$BASE_URL" in
  http://*) UPDATE_INSECURE="1"; echo "⚠️  使用 http 清单源（仅限内网自测）：$BASE_URL";;
esac

OS="$(uname -s)"
case "$OS" in
  Darwin)
    PLATFORM_NAME="macos"
    DEFAULT_DATA="$HOME/Library/Application Support/zqagent"
    DEFAULT_LOG="$HOME/Library/Logs/zqagent/node.log"
    ;;
  Linux)
    PLATFORM_NAME="linux"
    DEFAULT_DATA="/var/lib/zqagent"
    DEFAULT_LOG="/var/log/zqagent/node.log"
    ;;
  *) echo "不支持的系统：${OS}（Windows 请使用桌面应用或 WSL）" >&2; exit 1;;
esac

MACHINE="$(uname -m)"
case "$MACHINE" in
  arm64|aarch64) ARCH="arm64";;
  x86_64|amd64) ARCH="x64";;
  *) echo "不支持的架构：$MACHINE" >&2; exit 1;;
esac
PLATFORM_KEY="${PLATFORM_NAME}-${ARCH}"
DATA_DIR="${DATA_DIR:-$DEFAULT_DATA}"
LOG="${LOG:-$DEFAULT_LOG}"
ZQAGENT_BIN="$BIN_DIR/zqagent"

need_root() {
  if [ "$(id -u)" -ne 0 ]; then
    echo "需要 root 权限：请用 sudo 执行（或先 sudo -i）" >&2
    exit 1
  fi
}

# Linux 的系统级 systemd 单元必须 root；macOS 普通用户可装 LaunchAgent
require_service_privilege() {
  if [ "$OS" = "Linux" ]; then need_root; fi
}

need_bin_access() {
  local dir="$1"
  mkdir -p "$dir" 2>/dev/null || need_root
  if [ ! -w "$dir" ]; then need_root; fi
}

fetch() { # url dest
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --retry 3 --connect-timeout 15 "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O "$2" "$1"
  else
    echo "缺少 curl 或 wget，无法下载" >&2
    return 1
  fi
}

sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    echo "缺少 sha256sum/shasum，无法校验下载" >&2
    return 1
  fi
}

# 未显式提供公钥时，从下载站取随发布同步的 update-public.b64。
# 注意：该文件与清单同源，属便利性回退——下载站被攻破时两者可被一起替换，
# 需要强保证的场合请改用 --pubkey 传带外保存的公钥。
fetch_public_key() {
  local url="$BASE_URL/update-public.b64" tmp key
  tmp="$(mktemp)"
  if ! fetch "$url" "$tmp" 2>/dev/null; then
    rm -f "$tmp"
    return 1
  fi
  key="$(tr -d ' \t\r\n' < "$tmp")"
  rm -f "$tmp"
  case "$key" in
    ""|*[!A-Za-z0-9+/=]*) return 1 ;;
  esac
  PUBKEY="$key"
  PUBKEY_SOURCE="$url"
  return 0
}

# 公钥指纹：对 base64 文本求 SHA-256，便于与带外副本人工核对
pubkey_fingerprint() {
  local tmp h
  tmp="$(mktemp)"
  printf '%s' "$1" > "$tmp"
  h="$(sha256_of "$tmp" 2>/dev/null || true)"
  rm -f "$tmp"
  printf '%s' "$h"
}

# 从纯文本清单取本平台的 url / sha256 / 版本
resolve_from_manifest() {
  local txt
  txt="$(mktemp)"
  if ! fetch "$BASE_URL/$CHANNEL.txt" "$txt"; then
    rm -f "$txt"
    echo "无法获取清单 $BASE_URL/$CHANNEL.txt" >&2
    return 1
  fi
  local line
  line="$(awk -v p="$PLATFORM_KEY" -v v="$WANT_VERSION" '
    $1 == p && (v == "" || $2 == v) { found = $0 }
    END { if (found != "") print found }
  ' "$txt")"
  rm -f "$txt"
  if [ -z "$line" ]; then
    echo "清单里没有 ${PLATFORM_KEY} 的产物${WANT_VERSION:+（请求 v${WANT_VERSION}）}" >&2
    return 1
  fi
  RESOLVED_VERSION="$(echo "$line" | awk '{print $2}')"
  RESOLVED_URL="$(echo "$line" | awk '{print $3}')"
  RESOLVED_SHA="$(echo "$line" | awk '{print $4}')"
}

# ---------- 服务描述（内嵌，安装后不依赖仓库脚本） ----------
write_systemd_unit() {
  local unit="/etc/systemd/system/$SERVICE_NAME.service"
  local run_user="${SUDO_USER:-root}"
  cat > "$unit" <<EOF
[Unit]
Description=zqagent node (agent server)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=10

[Service]
Type=simple
User=$run_user
WorkingDirectory=$DATA_DIR
ExecStart=$ZQAGENT_BIN serve --host $HOST --port $PORT --data-dir $DATA_DIR
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=15
Environment=NODE_ENV=production
Environment=PATH=$(dirname "$ZQAGENT_BIN"):/usr/local/bin:/usr/bin:/bin
Environment=ZQAGENT_SERVICE_MANAGER=systemd
Environment=ZQAGENT_SERVICE_NAME=$SERVICE_NAME
Environment=ZQAGENT_AUTO_UPDATE=1
Environment=ZQAGENT_UPDATE_CHANNEL=$CHANNEL
Environment=ZQAGENT_UPDATE_PUBKEY=$PUBKEY
Environment=ZQAGENT_UPDATE_URL=$BASE_URL/$CHANNEL.json
Environment=ZQAGENT_UPDATE_INSECURE=$UPDATE_INSECURE
EnvironmentFile=-/etc/zqagent.env
NoNewPrivileges=true
StandardOutput=append:$LOG
StandardError=append:$LOG

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
  systemctl restart "$SERVICE_NAME"
  echo "✅ systemd 服务已安装：$unit"
}

# root 安装用 LaunchDaemon（开机即启动）；普通用户装 LaunchAgent
launchd_domain() {
  if [ "$(id -u)" -eq 0 ]; then echo "system"; else echo "gui/$(id -u)"; fi
}
launchd_plist_path() {
  if [ "$(id -u)" -eq 0 ]; then
    echo "/Library/LaunchDaemons/$LABEL.plist"
  else
    echo "$HOME/Library/LaunchAgents/$LABEL.plist"
  fi
}

write_launchd_plist() {
  local plist
  plist="$(launchd_plist_path)"
  mkdir -p "$(dirname "$plist")"
  cat > "$plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>$LABEL</string>
  <key>ProgramArguments</key>
  <array>
    <string>$ZQAGENT_BIN</string>
    <string>serve</string>
    <string>--host</string><string>$HOST</string>
    <string>--port</string><string>$PORT</string>
    <string>--data-dir</string><string>$DATA_DIR</string>
  </array>
  <key>WorkingDirectory</key><string>$DATA_DIR</string>
  <key>RunAtLoad</key><true/>
  <!-- 无条件保活：自动更新替换文件后进程会主动退出，需要 launchd 拉起新版本 -->
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>5</integer>
  <key>EnvironmentVariables</key>
  <dict>
    <key>PATH</key><string>$(dirname "$ZQAGENT_BIN"):/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    <key>NODE_ENV</key><string>production</string>
    <key>ZQAGENT_SERVICE_MANAGER</key><string>launchd</string>
    <key>ZQAGENT_SERVICE_LABEL</key><string>$LABEL</string>
    <key>ZQAGENT_AUTO_UPDATE</key><string>1</string>
    <key>ZQAGENT_UPDATE_CHANNEL</key><string>$CHANNEL</string>
    <key>ZQAGENT_UPDATE_PUBKEY</key><string>$PUBKEY</string>
    <key>ZQAGENT_UPDATE_URL</key><string>$BASE_URL/$CHANNEL.json</string>
    <key>ZQAGENT_UPDATE_INSECURE</key><string>$UPDATE_INSECURE</string>
  </dict>
  <key>StandardOutPath</key><string>$LOG</string>
  <key>StandardErrorPath</key><string>$LOG</string>
</dict>
</plist>
EOF
  launchctl bootout "$(launchd_domain)/$LABEL" >/dev/null 2>&1 || true
  # bootout 是异步的：立刻 bootstrap 偶尔返回 "Input/output error 5"，重试几次
  local ok=0
  local i
  for i in 1 2 3 4 5; do
    if launchctl bootstrap "$(launchd_domain)" "$plist" 2>/dev/null; then ok=1; break; fi
    sleep 1
    launchctl bootout "$(launchd_domain)/$LABEL" >/dev/null 2>&1 || true
  done
  if [ "$ok" -ne 1 ]; then
    echo "✗ launchd 装载失败，重跑一次以查看原始错误：" >&2
    launchctl bootstrap "$(launchd_domain)" "$plist"
    exit 1
  fi
  echo "✅ launchd 服务已安装：$plist"
}

install_service() {
  mkdir -p "$DATA_DIR" "$(dirname "$LOG")"
  if [ "$OS" = "Linux" ]; then
    local run_user="${SUDO_USER:-root}"
    if id "$run_user" >/dev/null 2>&1; then
      chown -R "$run_user" "$DATA_DIR" "$(dirname "$LOG")" 2>/dev/null || true
    fi
    write_systemd_unit
  else
    write_launchd_plist
  fi
}

# ---------- 子命令 ----------
do_install() {
  require_service_privilege
  if [ "$INSTALL_SERVICE" = "1" ]; then need_bin_access "$BIN_DIR"; fi

  resolve_from_manifest

  # 装服务时若没给公钥，尝试从下载站自动获取（失败则退回「不自动更新」）
  if [ -z "$PUBKEY" ] && [ "$INSTALL_SERVICE" = "1" ]; then
    fetch_public_key || true
  fi

  echo "平台      $PLATFORM_KEY"
  echo "版本      v$RESOLVED_VERSION"
  echo "下载      $RESOLVED_URL"
  if [ -n "$PUBKEY" ]; then
    if [ -n "${PUBKEY_SOURCE:-}" ]; then
      echo "验签公钥  已自动获取（$PUBKEY_SOURCE）"
      echo "          指纹 sha256:$(pubkey_fingerprint "$PUBKEY")"
      echo "          可与你带外保存的公钥比对；不一致请改用 --pubkey 显式指定"
    else
      echo "验签公钥  已通过 --pubkey / ZQAGENT_UPDATE_PUBKEY 提供"
    fi
  else
    echo "⚠️  未提供验签公钥（--pubkey）：节点能正常运行，但不会自动更新"
  fi

  local tmp
  tmp="$(mktemp -d)"
  # 立即展开路径：EXIT 时局部变量已销毁（set -u 会报 unbound）
  trap "rm -rf '$tmp'" EXIT
  echo "下载安装包…"
  fetch "$RESOLVED_URL" "$tmp/zqagent"
  chmod +x "$tmp/zqagent"

  local actual
  actual="$(sha256_of "$tmp/zqagent")"
  if [ "$actual" != "$RESOLVED_SHA" ]; then
    echo "✗ SHA-256 校验失败（期望 ${RESOLVED_SHA:0:12}…，实际 ${actual:0:12}…）" >&2
    exit 1
  fi
  echo "✓ 校验通过"

  local ver_out
  if ! ver_out="$("$tmp/zqagent" --version 2>/dev/null)"; then
    echo "✗ 安装包无法执行（架构不匹配？）" >&2
    exit 1
  fi
  case "$ver_out" in
    *"${RESOLVED_VERSION}"*) ;;
    *)
      echo "✗ 版本不符：清单为 v${RESOLVED_VERSION}，产物为 ${ver_out:-无输出}" >&2
      exit 1
      ;;
  esac

  mkdir -p "$DATA_DIR/updates/backup"
  if [ -x "$ZQAGENT_BIN" ]; then
    cp "$ZQAGENT_BIN" "$DATA_DIR/updates/backup/zqagent-preinstall" 2>/dev/null || true
  fi

  mkdir -p "$BIN_DIR"
  # 覆盖可能正在运行的二进制：先改名再落新文件，避免 ETXTBSY。
  # 必须落「同目录新文件」再 rename：直接 cp 到已存在的文件会沿用其 inode，
  # 若目标文件曾被污染成 tmp_t（历史版本跨目录 mv 所致），标签不会被纠正，
  # systemd 执行会以 status=203/EXEC 反复失败。
  if [ -e "$ZQAGENT_BIN" ]; then mv -f "$ZQAGENT_BIN" "${ZQAGENT_BIN}.old" 2>/dev/null || true; fi
  NEW_BIN="$(mktemp "$BIN_DIR/.zqagent.new.XXXXXX")"
  cp "$tmp/zqagent" "$NEW_BIN"
  chmod 755 "$NEW_BIN"
  mv -f "$NEW_BIN" "$ZQAGENT_BIN"
  rm -f "${ZQAGENT_BIN}.old"
  # 兜底纠正 SELinux 标签（openEuler/麒麟/统信等 Enforcing 系统必需；其它系统无此命令，忽略）
  restorecon -v "$ZQAGENT_BIN" >/dev/null 2>&1 || true
  echo "✅ 已安装 ${ZQAGENT_BIN}（$("$ZQAGENT_BIN" --version 2>/dev/null | head -1)）"

  if [ "$INSTALL_SERVICE" = "1" ]; then
    install_service
    echo ""
    echo "服务已启动，查看方式："
    if [ "$OS" = "Linux" ]; then
      echo "  systemctl status $SERVICE_NAME"
      echo "  journalctl -u $SERVICE_NAME -f    或    tail -f $LOG"
    else
      echo "  launchctl print $(launchd_domain)/$LABEL"
      echo "  tail -f $LOG"
    fi
    echo "  地址：http://$HOST:$PORT    数据目录：$DATA_DIR"
    echo ""
    echo "接入用户中心（可选）："
    echo "  $ZQAGENT_BIN link --hub https://hub.cd-zq.com --code <接入码> \\"
    echo "    --data-dir \"$DATA_DIR\" --port $PORT"
  fi
}

do_status() {
  if [ "$OS" = "Linux" ]; then
    systemctl status "$SERVICE_NAME" --no-pager 2>/dev/null || echo "未安装 systemd 服务 $SERVICE_NAME"
  else
    launchctl print "$(launchd_domain)/$LABEL" 2>/dev/null | grep -E "^[[:space:]]+(state|pid|last exit code)" || echo "未安装 launchd 服务 $LABEL"
  fi
  if [ -x "$ZQAGENT_BIN" ]; then
    echo "二进制：${ZQAGENT_BIN}（$("$ZQAGENT_BIN" --version 2>/dev/null | head -1)）"
    echo "更新状态："
    # 独立进程默认看不到服务体系变量，这里补上以显示真实托管状态
    if [ "$OS" = "Linux" ]; then
      ZQAGENT_SERVICE_MANAGER=systemd ZQAGENT_SERVICE_NAME="$SERVICE_NAME" \
        "$ZQAGENT_BIN" update --status --data-dir "$DATA_DIR" 2>/dev/null || true
    else
      ZQAGENT_SERVICE_MANAGER=launchd ZQAGENT_SERVICE_LABEL="$LABEL" \
        "$ZQAGENT_BIN" update --status --data-dir "$DATA_DIR" 2>/dev/null || true
    fi
  fi
}

do_uninstall() {
  require_service_privilege
  if [ "$OS" = "Linux" ]; then
    systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true
    rm -f "/etc/systemd/system/$SERVICE_NAME.service"
    systemctl daemon-reload 2>/dev/null || true
  else
    launchctl bootout "$(launchd_domain)/$LABEL" >/dev/null 2>&1 || true
    rm -f "$(launchd_plist_path)"
  fi
  echo "✅ 服务已移除（数据目录保留在 ${DATA_DIR}）"
  echo "   彻底清理：sudo rm -rf '$DATA_DIR' '$ZQAGENT_BIN'"
  echo "   注：安装时若用了 --data-dir/--bin-dir，请带上相同参数执行本命令"
}

case "$ACTION" in
  install) do_install;;
  status) do_status;;
  logs) tail -n 100 -f "$LOG" 2>/dev/null || { need_root; tail -n 100 -f "$LOG"; };;
  restart)
    require_service_privilege
    if [ "$OS" = "Linux" ]; then
      systemctl restart "$SERVICE_NAME"
    else
      launchctl kickstart -k "$(launchd_domain)/$LABEL"
    fi
    ;;
  rollback)
    require_service_privilege
    if [ ! -x "$ZQAGENT_BIN" ]; then echo "未找到 $ZQAGENT_BIN" >&2; exit 1; fi
    ZQAGENT_NO_RESTART_HINT=1 "$ZQAGENT_BIN" update --rollback --data-dir "$DATA_DIR"
    if [ "$OS" = "Linux" ]; then
      systemctl restart "$SERVICE_NAME"
    else
      launchctl kickstart -k "$(launchd_domain)/$LABEL"
    fi
    echo "✅ 已回滚并重启"
    ;;
  uninstall) do_uninstall;;
  print-config)
    echo "ACTION=$ACTION"
    echo "PLATFORM=$PLATFORM_KEY"
    echo "MANIFEST=$BASE_URL/$CHANNEL.txt"
    echo "VERSION=${WANT_VERSION:-<最新>}"
    echo "BIN=$ZQAGENT_BIN"
    echo "DATA_DIR=$DATA_DIR"
    echo "LOG=$LOG"
    echo "HOST=$HOST PORT=$PORT"
    echo "PUBKEY=${PUBKEY:+已配置}"
    if [ "$INSTALL_SERVICE" = "1" ]; then echo "SERVICE=yes"; else echo "SERVICE=no"; fi
    ;;
esac
