#!/bin/bash
# AidNet Linux 一键安装脚本
# 用法: curl -sSL https://aid.wkteam.cn/releases/linux/install.sh | sudo bash
# 可选: AIDNET_DISPATCHER_URL=https://你的域名 curl -sSL https://你的域名/releases/linux/install.sh | sudo bash
set -e
umask 077

INSTALL_DIR="/opt/aidnet"
BIN_DIR="$INSTALL_DIR/bin"
NODE_VERSION="16.20.2"
NODE_RELEASE_ORIGIN="https://aid.wkteam.cn"
NODE_DOWNLOAD_MAX_TIME="300"
NODE_X64_ARCHIVE_SIZE="22556484"
NODE_X64_ARCHIVE_SHA256="874463523f26ed528634580247f403d200ba17a31adf2de98a7b124c6eb33d87"
NODE_ARM64_ARCHIVE_SIZE="21993928"
NODE_ARM64_ARCHIVE_SHA256="e88d86154d1ce53dc52fd74d79d4bfdf0b05f58c0bb2639adfa36e9378b770c4"
UPDATE_AUTHORITY_JSON='{"schemaVersion":1,"domain":"aidnet-linux-update-authority","keyId":"linux-update-ed25519-2026-09","algorithm":"Ed25519","brand":"gewe","origin":"https://aid.wkteam.cn","releasePath":"/releases/linux","publicKeySpkiBase64":"MCowBQYDK2VwAyEAZx+5rqT7MZZDV6/Zp+mxKG8FaXkYaTJE5i8ZiTxpdhA="}'
WORK_DIR=''
DOWNLOAD_SEQUENCE=0
BUNDLE_STARTED=0
STOP_PHASE_STARTED=0
BUNDLE_DONE=0
KEEP_WORK=0
TARGETS=() STAGES=() SERVICES=() OLD_ACTIVE=() OLD_ENABLED=() HAD_FILE=()

if [ "$(id -u)" -ne 0 ]; then
  echo "错误: 安装需要 root 权限（会写入 /opt 和 /etc/systemd）。" >&2
  echo "请使用：" >&2
  echo "  curl -sSL https://aid.wkteam.cn/releases/linux/install.sh | sudo bash" >&2
  exit 1
fi

ARCH=$(uname -m)
case "$ARCH" in
  x86_64)
    ARCH_TAG="x64"
    NODE_EXPECTED_ARCH="x64"
    NODE_ARCHIVE_SIZE="$NODE_X64_ARCHIVE_SIZE"
    NODE_ARCHIVE_SHA256="$NODE_X64_ARCHIVE_SHA256"
    ;;
  aarch64|arm64)
    ARCH_TAG="arm64"
    NODE_EXPECTED_ARCH="arm64"
    NODE_ARCHIVE_SIZE="$NODE_ARM64_ARCHIVE_SIZE"
    NODE_ARCHIVE_SHA256="$NODE_ARM64_ARCHIVE_SHA256"
    ;;
  *) echo "不支持的架构: $ARCH"; exit 1 ;;
esac

BASE_URL="${AIDNET_DISPATCHER_URL:-https://aid.wkteam.cn}"

valid_initial_host() {
  local host="$1"
  local label=''
  local last_label=''
  local -a labels=()

  if [[ "$host" =~ ^[0-9.]+$ ]]; then
    IFS='.' read -r -a labels <<< "$host"
    [ "${#labels[@]}" -eq 4 ] || return 1
    for label in "${labels[@]}"; do
      [[ "$label" =~ ^(0|[1-9][0-9]{0,2})$ ]] || return 1
      (( 10#$label <= 255 )) || return 1
    done
    return 0
  fi

  [ "${#host}" -le 253 ] || return 1
  case "$host" in
    .*|*.|*..*) return 1 ;;
  esac
  IFS='.' read -r -a labels <<< "$host"
  for label in "${labels[@]}"; do
    if [[ "$label" =~ ^[A-Za-z0-9]$ ]] \
      || [[ "$label" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$ ]]; then
      last_label="$label"
    else
      return 1
    fi
  done
  [[ "$last_label" =~ [A-Za-z] ]]
}

canonical_https_origin_preflight() {
  value="${1%/}"
  case "$value" in
    https://*) ;;
    *) return 1 ;;
  esac
  authority="${value#https://}"
  case "$authority" in
    ''|*/*|*@*|*\?*|*\#*|*\\*|*[[:space:]]*) return 1 ;;
  esac
  if [[ ! "$authority" =~ ^[A-Za-z0-9.-]+(:([0-9]{1,5}))?$ ]]; then
    return 1
  fi
  port="${BASH_REMATCH[2]:-}"
  if [ -n "$port" ] && (( 10#$port > 65535 )); then
    return 1
  fi
  host="$authority"
  if [ -n "$port" ]; then host="${authority%:*}"; fi
  valid_initial_host "$host" || return 1
  printf '%s' "$value"
}

normalize_legacy_dispatcher_origin() {
  if [ "$1" = "https://aid.nodemsg.com" ]; then
    printf '%s' "https://aid.wkteam.cn"
  else
    printf '%s' "$1"
  fi
}

if ! BASE_URL="$(canonical_https_origin_preflight "$BASE_URL")"; then
  echo "错误: AIDNET_DISPATCHER_URL 必须是规范 HTTPS 地址（仅允许 https://主机[:端口]，不得包含账号、路径、查询或片段）。" >&2
  exit 1
fi
BASE_URL="$(normalize_legacy_dispatcher_origin "$BASE_URL")"

download() {
  url="$1"
  out="$2"
  legacy_tmp="${out}.tmp"
  DOWNLOAD_SEQUENCE=$((DOWNLOAD_SEQUENCE + 1))
  tmp="$WORK_DIR/download.$DOWNLOAD_SEQUENCE"
  if [ -z "$WORK_DIR" ] || [ ! -d "$WORK_DIR" ] || [ -L "$WORK_DIR" ]; then
    return 1
  fi
  if ! rm -f -- "$legacy_tmp" "$tmp"; then
    return 1
  fi
  if ! curl --proto '=https' --proto-redir '=https' -fL --retry 3 --connect-timeout 10 --progress-bar "$url" -o "$tmp"; then
    rm -f "$tmp"
    return 1
  fi
  if [ ! -f "$tmp" ] || [ -L "$tmp" ]; then
    rm -f "$tmp"
    return 1
  fi
  if ! chown 0:0 "$tmp" || ! chmod 0600 "$tmp" \
    || [ "$(stat -c '%u:%g:%a:%h' -- "$tmp" 2>/dev/null)" != "0:0:600:1" ]; then
    rm -f "$tmp"
    return 1
  fi
  if ! mv -T "$tmp" "$out"; then
    rm -f "$tmp"
    return 1
  fi
}

node_is_exact() {
  candidate="$1"
  [ -x "$candidate" ] || return 1
  identity=$("$candidate" -p 'process.versions.node + " " + process.arch' 2>/dev/null) || return 1
  [ "$identity" = "$NODE_VERSION $NODE_EXPECTED_ARCH" ]
}

canonical_https_origin() {
  "$NODE_BIN" - "$1" <<'NODE'
const value = String(process.argv[2] || '').trim();
let parsed;
try {
  parsed = new URL(value);
} catch {
  process.exit(1);
}
if (
  parsed.protocol !== 'https:'
  || !parsed.hostname
  || parsed.username
  || parsed.password
  || parsed.pathname !== '/'
  || parsed.search
  || parsed.hash
) {
  process.exit(1);
}
process.stdout.write(parsed.origin);
NODE
}

install_node_runtime() {
  dist="node-v${NODE_VERSION}-linux-${ARCH_TAG}"
  pkg="$WORK_DIR/node-bootstrap.tar.xz"
  stage_dir="$WORK_DIR/node-bootstrap.stage"
  candidate="$stage_dir/${dist}/bin/node"
  install_tmp="$WORK_DIR/node"
  find "$BIN_DIR" -maxdepth 1 -type d -name 'node-extract.*' -exec rm -rf -- {} +
  rm -rf "$BIN_DIR/node-bootstrap.stage" "$BIN_DIR/node-bootstrap.tar.xz.tmp" \
    "$BIN_DIR/${dist}.tar.xz.tmp" "${NODE_BIN}.tmp"
  rm -rf "$stage_dir" "$pkg" "$install_tmp"
  echo "==> 下载 Aid 专用 Node.js ($ARCH_TAG，约22MB)"
  downloaded=0
  for attempt in 1 2 3 4; do
    rm -f "$pkg"
    if (
      set -o pipefail
      curl --proto '=https' --proto-redir '=https' --max-redirs 0 \
        --max-filesize "$NODE_ARCHIVE_SIZE" --max-time "$NODE_DOWNLOAD_MAX_TIME" \
        -f --connect-timeout 10 --progress-bar \
        "$NODE_RELEASE_ORIGIN/releases/linux/${dist}.tar.xz" \
        | head -c "$((NODE_ARCHIVE_SIZE + 1))" > "$pkg"
    ); then
      downloaded=1
      break
    fi
  done
  if [ "$downloaded" -ne 1 ]; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法下载固定版本的 Aid 专用 Node.js。" >&2
    return 1
  fi

  actual_size=$(wc -c < "$pkg")
  actual_size=${actual_size//[[:space:]]/}
  if [ "$actual_size" != "$NODE_ARCHIVE_SIZE" ]; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: Aid 专用 Node.js 大小校验失败。" >&2
    return 1
  fi

  if ! checksum_line=$(sha256sum "$pkg"); then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法计算 Aid 专用 Node.js SHA-256。" >&2
    return 1
  fi
  actual_sha256=${checksum_line%%[[:space:]]*}
  if [ "$actual_sha256" != "$NODE_ARCHIVE_SHA256" ]; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: Aid 专用 Node.js SHA-256 完整性校验失败。" >&2
    return 1
  fi

  if ! mkdir -m 0700 "$stage_dir"; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法创建 Aid 专用 Node.js 安全暂存目录。" >&2
    return 1
  fi
  if ! tar --no-same-owner -xJf "$pkg" -C "$stage_dir" "${dist}/bin/node" 2>/dev/null; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: Aid 专用 Node.js 压缩包无法解压。" >&2
    return 1
  fi
  if [ ! -f "$candidate" ] || [ -L "$candidate" ]; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: Aid 专用 Node.js 候选文件类型不安全。" >&2
    return 1
  fi
  if ! chown 0:0 "$candidate" || ! chmod 0755 "$candidate"; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法安全设置 Aid 专用 Node.js 的所有者或权限。" >&2
    return 1
  fi
  if ! node_is_exact "$candidate"; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: Aid 专用 Node.js 版本或架构不匹配。" >&2
    return 1
  fi
  if ! mv -T "$candidate" "$install_tmp"; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法暂存已验证的 Aid 专用 Node.js。" >&2
    return 1
  fi
  if ! mv -T "$install_tmp" "$NODE_BIN"; then
    rm -rf "$stage_dir" "$pkg" "$install_tmp"
    echo "错误: 无法原子安装已验证的 Aid 专用 Node.js。" >&2
    return 1
  fi
  rm -rf "$stage_dir" "$pkg"
}

if ! command -v sha256sum >/dev/null 2>&1; then
  echo "错误: 缺少 sha256sum，无法校验 Aid 专用 Node.js。" >&2
  exit 1
fi
if ! command -v head >/dev/null 2>&1; then
  echo "错误: 缺少 head，无法限制 Aid 专用 Node.js 下载大小。" >&2
  exit 1
fi
for required_command in mkdir tar chown chmod mv mktemp stat cp timeout systemctl; do
  if ! command -v "$required_command" >/dev/null 2>&1; then
    echo "错误: 缺少 $required_command，无法安全安装 Aid 专用 Node.js。" >&2
    exit 1
  fi
done
if ! command -v flock >/dev/null 2>&1; then
  echo "错误: 缺少 flock，无法安全串行安装 Aid 专用 Node.js。" >&2
  exit 1
fi

echo "==> 创建目录 $INSTALL_DIR"
if [ -L "$INSTALL_DIR" ]; then
  echo "错误: AidNet 安装目录不能是符号链接。" >&2
  exit 1
fi
if ! mkdir -p "$INSTALL_DIR"; then
  echo "错误: 无法创建 AidNet 安装目录。" >&2
  exit 1
fi
if [ -L "$INSTALL_DIR" ]; then
  echo "错误: AidNet 安装目录不能是符号链接。" >&2
  exit 1
fi
if ! chown 0:0 "$INSTALL_DIR" || ! chmod 0755 "$INSTALL_DIR" \
  || [ "$(stat -c '%u:%g:%a' -- "$INSTALL_DIR" 2>/dev/null)" != "0:0:755" ]; then
  echo "错误: 无法安全设置 AidNet 安装目录的所有者或权限。" >&2
  exit 1
fi
if [ -L "$BIN_DIR" ]; then
  echo "错误: AidNet bin 目录不能是符号链接。" >&2
  exit 1
fi
if ! mkdir -p "$BIN_DIR"; then
  echo "错误: 无法创建 AidNet bin 目录。" >&2
  exit 1
fi
if [ -L "$BIN_DIR" ]; then
  echo "错误: AidNet bin 目录不能是符号链接。" >&2
  exit 1
fi
if ! chown 0:0 "$BIN_DIR" || ! chmod 0755 "$BIN_DIR" \
  || [ "$(stat -c '%u:%g:%a' -- "$BIN_DIR" 2>/dev/null)" != "0:0:755" ]; then
  echo "错误: 无法安全设置 AidNet bin 目录的所有者或权限。" >&2
  exit 1
fi
NODE_BIN="$BIN_DIR/node"
NODE_BOOTSTRAP_LOCK="$BIN_DIR/node-bootstrap.lock"
if [ -L "$NODE_BOOTSTRAP_LOCK" ] \
  || { [ -e "$NODE_BOOTSTRAP_LOCK" ] && [ ! -f "$NODE_BOOTSTRAP_LOCK" ]; }; then
  echo "错误: Aid 专用 Node.js 安装锁必须是普通文件。" >&2
  exit 1
fi
if [ -e "$NODE_BOOTSTRAP_LOCK" ] \
  && [ "$(stat -c '%u:%g:%a:%h' -- "$NODE_BOOTSTRAP_LOCK" 2>/dev/null)" != "0:0:600:1" ]; then
  echo "错误: 已有 Aid 专用 Node.js 安装锁的所有者、权限或链接数不安全。" >&2
  exit 1
fi
if ! exec 9>>"$NODE_BOOTSTRAP_LOCK"; then
  echo "错误: 无法创建 Aid 专用 Node.js 安装锁。" >&2
  exit 1
fi
if ! chown 0:0 "$NODE_BOOTSTRAP_LOCK" || ! chmod 0600 "$NODE_BOOTSTRAP_LOCK" \
  || [ "$(stat -c '%u:%g:%a:%h' -- "$NODE_BOOTSTRAP_LOCK" 2>/dev/null)" != "0:0:600:1" ]; then
  echo "错误: 无法安全设置 Aid 专用 Node.js 安装锁。" >&2
  exit 1
fi
if ! flock -n 9; then
  echo "错误: 另一个 AidNet 安装正在进行，请稍后重试。" >&2
  exit 1
fi

if [ -n "$(find "$BIN_DIR" -mindepth 1 -maxdepth 1 -name '.aidnet-install.*' -print -quit)" ]; then
  echo "错误: 存在未完成安装的私有目录；请先核验并手动恢复，安装器不会删除恢复证据。" >&2
  exit 1
fi
if ! WORK_DIR="$(mktemp -d "$BIN_DIR/.aidnet-install.XXXXXX")"; then
  echo "错误: 无法创建 AidNet 私有安装目录。" >&2
  exit 1
fi
if [ -L "$WORK_DIR" ] || [ ! -d "$WORK_DIR" ] \
  || ! chown 0:0 "$WORK_DIR" || ! chmod 0700 "$WORK_DIR" \
  || [ "$(stat -c '%u:%g:%a' -- "$WORK_DIR" 2>/dev/null)" != "0:0:700" ]; then
  rm -rf -- "$WORK_DIR"
  WORK_DIR=''
  echo "错误: AidNet 私有安装目录不安全。" >&2
  exit 1
fi
# BEGIN GENERATED installer-transaction.sh
# 单一维护源；修改后运行 node linux-client/scripts/embed-installer-transaction.js --write。
service_enabled_state() {
  local state load_state
  state=$(systemctl is-enabled "$1" 2>/dev/null) || true
  if [ -z "$state" ]; then
    # 部分 systemd 对不存在的 unit 只写 stderr；必须由 LoadState 明确确认。
    load_state=$(systemctl show "$1" --property=LoadState --value 2>/dev/null) || return 1
    [ "$load_state" = not-found ] || return 1
    state=not-found
  fi
  case "$state" in enabled|disabled|not-found) printf '%s' "$state";; *) return 1;; esac
}

snapshot_bundle() {
  local i target state
  for i in "${!TARGETS[@]}"; do
    target="${TARGETS[$i]}"
    if [ -e "$target" ] || [ -L "$target" ]; then
      [ -f "$target" ] && [ ! -L "$target" ] && [ "$(stat -c '%h' -- "$target")" = 1 ] || return 1
      cp -p -- "$target" "$WORK_DIR/backup.$i" || return 1
      HAD_FILE[$i]=1
    else
      HAD_FILE[$i]=0
    fi
  done
  for i in "${!SERVICES[@]}"; do
    state=$(systemctl is-active "${SERVICES[$i]}" 2>/dev/null) || true
    case "$state" in active|inactive|failed|unknown) OLD_ACTIVE[$i]="$state";; *) return 1;; esac
    state=$(service_enabled_state "${SERVICES[$i]}") || { echo "错误: 不支持的已有服务启用状态，未开始提交。" >&2; return 1; }
    OLD_ENABLED[$i]="$state"
  done
  declare -p TARGETS HAD_FILE SERVICES OLD_ACTIVE OLD_ENABLED > "$WORK_DIR/snapshot-state" || return 1
}

restore_stopped_services() {
  local i failed=0
  # 此阶段未改 payload 或 enable 状态；只恢复原本 active 的服务。
  for i in "${!SERVICES[@]}"; do
    if [ "${OLD_ACTIVE[$i]}" = active ]; then
      if [ "$(systemctl is-active "${SERVICES[$i]}" 2>/dev/null)" != active ]; then
        systemctl start "${SERVICES[$i]}" || failed=1
        [ "$(systemctl is-active "${SERVICES[$i]}" 2>/dev/null)" = active ] || failed=1
      fi
    fi
  done
  [ "$failed" -eq 0 ]
}

rollback_bundle() {
  local i state active failed=0
  # 所有新进程确认停止后才允许恢复旧文件；失败保留现场，不启动混合版本。
  for i in "${!SERVICES[@]}"; do
    state=$(service_enabled_state "${SERVICES[$i]}") || return 1
    active=$(systemctl is-active "${SERVICES[$i]}" 2>/dev/null) || true
    if [ "$state" = not-found ] && { [ "$active" = inactive ] || [ "$active" = unknown ]; }; then continue; fi
    systemctl stop "${SERVICES[$i]}" || failed=1
    if [ "${OLD_ENABLED[$i]}" = not-found ]; then systemctl disable "${SERVICES[$i]}" || failed=1; fi
  done
  [ "$failed" -eq 0 ] || return 1
  for i in "${!TARGETS[@]}"; do
    if [ "${HAD_FILE[$i]}" = 1 ]; then
      cp -p -- "$WORK_DIR/backup.$i" "$WORK_DIR/restore.$i" \
        && mv -T "$WORK_DIR/restore.$i" "${TARGETS[$i]}" || failed=1
    else
      rm -f -- "${TARGETS[$i]}" || failed=1
    fi
  done
  [ "$failed" -eq 0 ] || return 1
  systemctl daemon-reload || return 1
  for i in "${!SERVICES[@]}"; do
    [ "${OLD_ENABLED[$i]}" != not-found ] || continue
    if [ "${OLD_ENABLED[$i]}" = enabled ]; then
      systemctl enable "${SERVICES[$i]}" || failed=1
    else
      systemctl disable "${SERVICES[$i]}" || failed=1
    fi
  done
  [ "$failed" -eq 0 ] || return 1
  for i in "${!SERVICES[@]}"; do
    if [ "${OLD_ACTIVE[$i]}" = active ]; then systemctl start "${SERVICES[$i]}" || failed=1; fi
  done
  [ "$failed" -eq 0 ]
}

commit_bundle() {
  local i
  STOP_PHASE_STARTED=1
  # 提交前停止两个旧服务，避免新旧 runtime 并行写同一身份。
  for i in "${!SERVICES[@]}"; do
    if [ "${OLD_ENABLED[$i]}" = not-found ] && [ "${OLD_ACTIVE[$i]}" != active ]; then continue; fi
    systemctl stop "${SERVICES[$i]}" || return 1
  done
  BUNDLE_STARTED=1
  printf '%s\n' 'manual bundle commit incomplete' > "$WORK_DIR/incomplete"
  for i in "${!TARGETS[@]}"; do
    mv -T "${STAGES[$i]}" "${TARGETS[$i]}" || return 1
  done
  systemctl daemon-reload || return 1
  for i in "${!SERVICES[@]}"; do systemctl enable "${SERVICES[$i]}" || return 1; done
  for i in "${!SERVICES[@]}"; do
    systemctl restart "${SERVICES[$i]}" || return 1
    [ "$(systemctl is-active "${SERVICES[$i]}")" = active ] || return 1
  done
  BUNDLE_DONE=1
}

cleanup_installer_workspace() {
  if [ "${BUNDLE_STARTED:-0}" = 1 ] && [ "${BUNDLE_DONE:-0}" = 0 ]; then
    if ! rollback_bundle; then KEEP_WORK=1; fi
  elif [ "${STOP_PHASE_STARTED:-0}" = 1 ] && [ "${BUNDLE_DONE:-0}" = 0 ]; then
    if ! restore_stopped_services; then KEEP_WORK=1; fi
  fi
  if [ "${KEEP_WORK:-0}" = 1 ]; then
    printf '%s\n' 'rollback failed; manual recovery required' > "$WORK_DIR/rollback-failed"
    echo "错误: 回滚失败，保留恢复证据: $WORK_DIR" >&2
  elif [ -n "${WORK_DIR:-}" ]; then
    rm -rf -- "$WORK_DIR"
    WORK_DIR=''
  fi
}
# END GENERATED installer-transaction.sh
trap cleanup_installer_workspace EXIT
trap 'exit 1' HUP INT TERM

AUTHORITY_TMP="$WORK_DIR/update-authority.json"
if ! rm -f -- "$INSTALL_DIR/update-authority.json.tmp" "$AUTHORITY_TMP"; then
  echo "错误: 无法清理 AidNet 更新 authority 临时文件。" >&2
  exit 1
fi
if ! printf '%s\n' "$UPDATE_AUTHORITY_JSON" > "$AUTHORITY_TMP"; then
  rm -f "$AUTHORITY_TMP"
  echo "错误: 无法写入 AidNet 更新 authority。" >&2
  exit 1
fi
if [ ! -f "$AUTHORITY_TMP" ] || [ -L "$AUTHORITY_TMP" ] \
  || ! chown 0:0 "$AUTHORITY_TMP" || ! chmod 0644 "$AUTHORITY_TMP" \
  || [ "$(stat -c '%u:%g:%a:%h' -- "$AUTHORITY_TMP" 2>/dev/null)" != "0:0:644:1" ]; then
  rm -f "$AUTHORITY_TMP"
  echo "错误: 无法安全发布 AidNet 更新 authority。" >&2
  exit 1
fi
install_node_runtime
if ! node_is_exact "$NODE_BIN"; then
  echo "错误: Aid 专用 Node.js 无法运行，请联系工作人员排查系统兼容性。" >&2
  exit 1
fi
if ! BASE_URL="$(canonical_https_origin "$BASE_URL")"; then
  echo "错误: AIDNET_DISPATCHER_URL 必须是规范 HTTPS 地址（仅允许 https://主机[:端口]，不得包含账号、路径、查询或片段）。" >&2
  exit 1
fi

echo "==> 从服务端获取安装配置"
BOOT_JSON="$(curl --proto '=https' --proto-redir '=https' --max-redirs 0 -fsSL --retry 2 --connect-timeout 10 "$BASE_URL/api/linux/bootstrap")" || {
  echo "错误: 无法安全获取 Linux 安装配置（要求 HTTPS 且不允许重定向）" >&2
  exit 1
}
SECRET="$(printf '%s' "$BOOT_JSON" | "$NODE_BIN" -e 'const fs=require("fs"); const o=JSON.parse(fs.readFileSync(0,"utf8")); process.stdout.write(String(o.client_secret||""));')"
REMOTE_URL="$(printf '%s' "$BOOT_JSON" | "$NODE_BIN" -e 'const fs=require("fs"); const o=JSON.parse(fs.readFileSync(0,"utf8")); process.stdout.write(String(o.dispatcher_url||""));')"
BRAND="$(printf '%s' "$BOOT_JSON" | "$NODE_BIN" -e 'const fs=require("fs"); const o=JSON.parse(fs.readFileSync(0,"utf8")); process.stdout.write(String(o.app_brand||"gewe").trim().toLowerCase());')"
if [ "$BRAND" != "gewe" ]; then
  echo "错误: 服务端返回的 app_brand 必须是 gewe，已停止安装" >&2
  exit 1
fi
if [ -n "$REMOTE_URL" ]; then
  if ! BASE_URL="$(canonical_https_origin "$REMOTE_URL")"; then
    echo "错误: 服务端返回的 dispatcher_url 不是规范 HTTPS origin" >&2
    exit 1
  fi
  BASE_URL="$(normalize_legacy_dispatcher_origin "$BASE_URL")"
fi
if [ -z "$SECRET" ]; then
  echo "错误: 服务端未返回 client_secret" >&2
  exit 1
fi

stage_payload() {
  download "$BASE_URL/releases/linux/aidnet.js" "$WORK_DIR/runtime.js" || return 1
  download "$BASE_URL/releases/linux/local-ui.js" "$WORK_DIR/local-ui.js" || return 1
  download "$BASE_URL/releases/linux/gost-$ARCH_TAG" "$WORK_DIR/gost" || return 1
  download "$BASE_URL/releases/linux/frpc-$ARCH_TAG" "$WORK_DIR/frpc" || return 1
  chmod 0755 "$WORK_DIR/runtime.js" "$WORK_DIR/local-ui.js" "$WORK_DIR/gost" "$WORK_DIR/frpc" || return 1
  timeout 15 "$NODE_BIN" --check "$WORK_DIR/runtime.js" || return 1
  timeout 15 "$NODE_BIN" --check "$WORK_DIR/local-ui.js" || return 1
  timeout 10 "$WORK_DIR/gost" -V >/dev/null 2>&1 || return 1
  timeout 10 "$WORK_DIR/frpc" -v >/dev/null 2>&1 || return 1
}
echo "==> 下载并验证完整 bundle（验证完成前不修改现有应用）"
stage_payload

echo "==> 写入配置"
CONFIG_TMP="$WORK_DIR/config.json"
if ! rm -f -- "$INSTALL_DIR/config.json.tmp" "$CONFIG_TMP"; then
  echo "错误: 无法清理 AidNet 配置临时文件。" >&2
  exit 1
fi
write_config() {
  cat > "$WORK_DIR/write-config.js" <<'NODE'
  const fs = require('fs');
  const crypto = require('crypto');
  const [file, dispatcherUrl, appBrand] = process.argv.slice(2);
  const clientSecret = fs.readFileSync(0, 'utf8');
  let seed = '';
  try { seed = fs.readFileSync('/etc/machine-id', 'utf8').trim(); } catch {}
  if (!seed) {
    seed = require('os').hostname() || 'aidnet-fallback-seed';
  }
  const salt = crypto.randomBytes(16);
  const iv = crypto.randomBytes(12);
  const key = crypto.pbkdf2Sync(seed, salt, 120000, 32, 'sha256');
  const c = crypto.createCipheriv('aes-256-gcm', key, iv);
  const cipher = Buffer.concat([c.update(clientSecret, 'utf8'), c.final()]);
  const tag = c.getAuthTag();
  const payload = ['v1', salt.toString('hex'), iv.toString('hex'), tag.toString('hex'), cipher.toString('hex')].join(':');
  fs.writeFileSync(file, JSON.stringify({ DISPATCHER_URL: dispatcherUrl, CLIENT_SECRET_ENC: payload, APP_BRAND: appBrand || 'gewe' }, null, 2) + '\n', { mode: 0o600 });
NODE
  printf '%s' "$SECRET" | "$NODE_BIN" "$WORK_DIR/write-config.js" "$CONFIG_TMP" "$BASE_URL" "$BRAND"
}
if ! write_config; then
  rm -f "$CONFIG_TMP"
  echo "错误: 无法写入 AidNet 加密配置。" >&2
  exit 1
fi
if [ ! -f "$CONFIG_TMP" ] || [ -L "$CONFIG_TMP" ] \
  || ! chown 0:0 "$CONFIG_TMP" || ! chmod 0600 "$CONFIG_TMP" \
  || [ "$(stat -c '%u:%g:%a:%h' -- "$CONFIG_TMP" 2>/dev/null)" != "0:0:600:1" ]; then
  rm -f "$CONFIG_TMP"
  echo "错误: 无法安全发布 AidNet 加密配置。" >&2
  exit 1
fi

echo "==> 创建 systemd 服务"
cat > "$WORK_DIR/aidnet.service" <<EOF
[Unit]
Description=AidNet CLI 反向代理出口
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=$INSTALL_DIR
ExecStart=$NODE_BIN $INSTALL_DIR/aidnet.js
Restart=always
RestartSec=5
RestartPreventExitStatus=78
StandardOutput=journal
StandardError=journal
Environment=DATA_DIR=/var/lib/aidnet
Environment=APP_BRAND=$BRAND

[Install]
WantedBy=multi-user.target
EOF

cat > "$WORK_DIR/aidnet-local-ui.service" <<EOF
[Unit]
Description=AidNet 本地后台
After=network-online.target aidnet.service
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=simple
WorkingDirectory=$INSTALL_DIR
ExecStart=$NODE_BIN $INSTALL_DIR/local-ui.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment=DATA_DIR=/var/lib/aidnet
Environment=LOCAL_UI_PORT=18080
Environment=INSTALL_DIR=$INSTALL_DIR

[Install]
WantedBy=multi-user.target
EOF

mkdir -p /var/lib/aidnet
chmod 0644 "$WORK_DIR/aidnet.service" "$WORK_DIR/aidnet-local-ui.service"
TARGETS=("$INSTALL_DIR/aidnet.js" "$INSTALL_DIR/local-ui.js" "$BIN_DIR/gost" "$BIN_DIR/frpc"
  "$INSTALL_DIR/config.json" "$INSTALL_DIR/update-authority.json"
  "/etc/systemd/system/aidnet.service" "/etc/systemd/system/aidnet-local-ui.service")
STAGES=("$WORK_DIR/runtime.js" "$WORK_DIR/local-ui.js" "$WORK_DIR/gost" "$WORK_DIR/frpc"
  "$CONFIG_TMP" "$AUTHORITY_TMP" "$WORK_DIR/aidnet.service" "$WORK_DIR/aidnet-local-ui.service")
SERVICES=(aidnet aidnet-local-ui)
snapshot_bundle
commit_bundle

echo ""
echo "✓ 安装完成！"
echo ""
echo "  查看状态:  systemctl status aidnet"
echo "  本地后台:  http://盒子IP:18080"
echo "  查看日志:  journalctl -u aidnet -f"
echo "  后台日志:  journalctl -u aidnet-local-ui -f"
echo "  重启服务:  systemctl restart aidnet"
echo ""
echo "启动后 Aid 编号会打印在日志里:"
echo "  journalctl -u aidnet --no-pager | grep 'Aid 编号'"
