#!/bin/sh
# SecureLink Agent 一键安装（由 https://install.sclink.dev/ 提供）
#
# 用法：
#   curl -fsSL https://install.sclink.dev/install.sh | sh
#
# 脚本做的事：识别平台架构 → 读取当前版本（latest.txt）→ 下载对应二进制
# 与 SHA256SUMS 并**先校验再落盘** → 安装到 /usr/local/bin/securelink-agent
# → 打印下一步（配置与 install 命令由你自己执行：脚本不知道你的租户配置，
# 替你跑 install 只会装出一个连不上控制面的服务）。
#
# 可用环境变量（管道时写在 sh 前，如 `curl ... | SL_INSTALL_DIR=/tmp/x sh`）：
#   SL_DOWNLOAD_BASE  下载源站（默认 https://install.sclink.dev）
#   SL_INSTALL_DIR    安装目录（默认 /usr/local/bin）
#   SL_VERSION        固定版本号（默认取 latest.txt）
#   SL_TIMEOUT        单次下载超时秒数（默认 300）
#   SL_RETRIES        下载重试次数上限（默认 3）
#
# ⚠️ 输出纪律（2026-10-08 事故后加，别删）：
#   用户在 debian10 上装 Agent 时，11.8MB 的下载中途被重置，而脚本**全程零输出**，
#   看起来像死了。所以：
#     · 任何网络请求**之前**必须先打印平台识别结果（敲下命令就要有反应）；
#     · 大文件下载前先报体量、下载中持续报进度（非交互环境也要有心跳输出）；
#     · 失败不说 `curl: (35) ...` 那种裸错误，收敛成人话 + 第几次重试；
#     · 用 `-C -` 断点续传，但**续传出来的文件照样做完整 SHA256 校验** ——
#       不存在「续传成功就跳过校验」的分支（见文件末尾的校验段）。
set -eu

BASE="${SL_DOWNLOAD_BASE:-https://install.sclink.dev}"
DEST_DIR="${SL_INSTALL_DIR:-/usr/local/bin}"
# 源站实测：hk 打过去 11.8MB 要 ~17s（约 680KB/s）。原先的 120s 看着宽裕，
# 但一旦回源抖起来根本不够，一步跨到 300s；再不够用 SL_TIMEOUT 覆盖。
TIMEOUT="${SL_TIMEOUT:-300}"
TRIES="${SL_RETRIES:-3}"
BEAT=3   # 非交互环境下的心跳间隔（秒）

log()  { printf '%s\n' "$*" >&2; }
fail() { log "install.sh: $*"; exit 1; }

command -v curl  >/dev/null 2>&1 || fail "需要 curl（请先安装）"
command -v sha256sum >/dev/null 2>&1 || fail "需要 sha256sum（请先安装 coreutils）"

human_size() { # human_size <字节数>
  awk -v b="$1" 'BEGIN {
    if (b >= 1073741824) printf "%.1f GB\n", b/1073741824;
    else if (b >= 1048576) printf "%.1f MB\n", b/1048576;
    else if (b >= 1024) printf "%.1f KB\n", b/1024;
    else printf "%d B\n", b }'
}

file_size() { # file_size <路径> -> 字节数（不存在则 0）
  if [ -f "$1" ]; then wc -c <"$1" | tr -d ' \n'; else printf '0'; fi
}

# ---- 平台识别：**在任何网络请求之前**就要有输出 ----
# 顺序不能改：原脚本第一行就发网络请求，之前一片空白，用户以为命令没执行。
os="$(uname -s)"; arch="$(uname -m)"
case "$os" in
  Linux) ;;
  Darwin) fail "macOS 不能作为 Agent 主机（可作为成员设备，走官方 WireGuard 客户端）。见 https://sclink.dev/docs/install/" ;;
  *) fail "仅支持 Linux；Windows 手动步骤见 https://sclink.dev/docs/install/" ;;
esac
case "$arch" in
  x86_64|amd64)  plat="linux_amd64" ;;
  aarch64|arm64) plat="linux_arm64" ;;
  *) fail "不支持的架构: ${arch}（支持 x86_64 / arm64）" ;;
esac

log "SecureLink Agent 一键安装"
log "  平台    : $os / $arch → $plat"
log "  源站    : $BASE"
log "  安装到  : $DEST_DIR"
log ""

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

# 小文件（latest.txt / SHA256SUMS）：静默重试，失败时说人话。
# curl 的裸错误一律丢进 $tmp/curl.err —— 用户不该被 `curl: (56) ...` 砸脸。
fetch_small() { # fetch_small <url> <输出文件>
  url="$1"; out="$2"; i=1
  while :; do
    if curl -fsSL --connect-timeout 15 -m 60 "$url" -o "$out" 2>"$tmp/curl.err"; then
      return 0
    fi
    if [ "$i" -ge "$TRIES" ]; then break; fi
    i=$((i + 1))
    log "    连接源站失败，网络不稳，正在重试 ($i/$TRIES)…"
    sleep 2
  done
  log "    已重试 $TRIES 次仍失败：$url"
  return 1
}

content_length() { # content_length <url> -> 字节数（拿不到则空）
  curl -fsSI --connect-timeout 15 -m 30 "$1" 2>/dev/null \
    | tr -d '\r' | awk 'tolower($1) == "content-length:" { n = $2 } END { print (n == "" ? "" : n) }'
}

# 大文件：先报体量 → 边下边报进度（非交互环境用心跳兜底）→ 失败可续传重试。
#
# 进度为什么自己报：curl 的 --progress-bar 只在终端里好看，而本脚本最常见的
# 用法是 `curl ... | sh`（stderr 可能不是终端）。所以除了让 curl 画它那份，
# 我们再每 BEAT 秒打一行「已下载 x / y」—— 无论有没有终端都有反馈。
download_big() { # download_big <url> <输出文件> <期望字节数或空>
  url="$1"; out="$2"; size="$3"

  curl -fSL --progress-bar --connect-timeout 15 -m "$TIMEOUT" -C - \
       "$url" -o "$out" 2>"$tmp/curl.err" &
  cpid=$!
  while kill -0 "$cpid" 2>/dev/null; do
    sleep "$BEAT"
    if kill -0 "$cpid" 2>/dev/null; then
      if [ -n "$size" ]; then
        log "    已下载 $(human_size "$(file_size "$out")") / $(human_size "$size")"
      else
        log "    已下载 $(human_size "$(file_size "$out")")"
      fi
    fi
  done
  if wait "$cpid"; then
    # 终端里把 curl 那份进度条补出来（非终端下它是噪音，就不放）。
    if [ -t 2 ] && [ -s "$tmp/curl.err" ]; then cat "$tmp/curl.err" >&2; fi
    return 0
  fi
  return 1   # curl 的裸错误留在 $tmp/curl.err，故意不打印
}

fetch_big() { # fetch_big <url> <输出文件> <说明>
  url="$1"; out="$2"; label="$3"; i=1

  size="$(content_length "$url")"
  if [ -n "$size" ]; then
    log "    体量 $(human_size "$size")（$size 字节），源站支持断点续传"
  else
    log "    体量未知（源站没给 Content-Length）"
  fi

  while :; do
    # 上一次可能已经把文件下全了（例如刚好在收尾时超时）：再 `-C -` 会要一个
    # 越界的 Range，源站回 416。这里直接认它下完了 —— 反正末尾还有完整校验。
    if [ -n "$size" ] && [ "$(file_size "$out")" = "$size" ]; then
      return 0
    fi
    if download_big "$url" "$out" "$size"; then
      log "    下载完成：$(human_size "$(file_size "$out")")"
      return 0
    fi
    if [ "$i" -ge "$TRIES" ]; then break; fi
    i=$((i + 1))
    # ⚠️ 续传并非总是可用 —— 这是「重试了但永远装不上」的成因，别简化掉：
    #    源站（或中间的 CDN / 反向代理）对 Range 请求回 **200 而不是 206** 时，
    #    curl 会直接拒绝续传（`cannot resume`，退出码 33）。此时继续用 `-C -`
    #    只是把同一堵墙撞第二遍，重试次数用光也照样失败。
    #    ⇒ 第二轮起丢弃残文件从头下：多下一次 11MB，远好过卡死。
    if [ "$i" -gt 2 ] && [ -f "$out" ]; then
      rm -f "$out"
      log "    续传两次未成功（源站可能不支持断点续传），改为从头下载 ($i/$TRIES)…"
    else
      log "    下载中断（网络不稳），正在重试 ($i/$TRIES)…已下好的部分会续传"
    fi
  done
  log "    已重试 $TRIES 次仍未下完：$label"
  return 1
}

# ---- 版本：来自 latest.txt，格式必须像 v0.1.0，防源站被篡改时注入怪内容 ----
ver="${SL_VERSION:-}"
if [ -z "$ver" ]; then
  log "==> 读取当前版本号（latest.txt）"
  # ⚠️ 这里必须写 ${BASE} 而不是 $BASE：紧跟其后的全角括号「（」在 UTF-8 区域里
  #    会被部分 shell 当成变量名的一部分（实测 bash 报 `BASE（: unbound variable`），
  #    而这个脚本要在用户的 dash 下跑 —— 加花括号是一劳永逸的做法。
  fetch_small "${BASE}/latest.txt" "$tmp/latest.txt" || fail "连不上下载源站 ${BASE}（请检查网络或稍后再试）"
  ver="$(tr -d ' \t\r\n' < "$tmp/latest.txt")"
fi
case "$ver" in
  v[0-9]*) ;;
  *) fail "无法获取有效版本号（得到: '$ver'）" ;;
esac
log "    版本 $ver"

# ---- 下载 + 校验（校验不过就不落盘）----
log "==> 下载 securelink-agent_$plat"
# ⚠️ 落盘名必须**带平台后缀**：SHA256SUMS 里的条目就是这个名字。
#    原脚本存成 `securelink-agent`（不带后缀），于是 `sha256sum -c --ignore-missing`
#    会把这条当成「文件缺失」而**跳过** —— 校验等于没做（GNU 的实现还会因为
#    “no file was verified” 直接失败）。名字对不上是这类校验最常见的假绿来源。
asset="securelink-agent_$plat"
fetch_big "${BASE}/$ver/$asset" "$tmp/$asset" "二进制" \
  || fail "下载失败。可稍后重试；若持续失败，请把上面的输出反馈给管理员"
log "==> 下载校验文件（SHA256SUMS）"
fetch_small "$BASE/$ver/SHA256SUMS" "$tmp/SHA256SUMS" || fail "下载校验文件失败"

# ⚠️ 这一步**没有例外**：哪怕上面走了断点续传，也要对**完整文件**算一遍 SHA256。
# 「续传成功就跳过校验」这类分支一旦出现，静默损坏就有了容身之处。
log "==> 校验 SHA256（完整文件，含续传得到的部分）"
want="$(awk -v f="$asset" '$2 == f { print $1; exit }' "$tmp/SHA256SUMS")"
if [ -z "$want" ]; then
  fail "SHA256SUMS 里没有 $asset 这一条 —— 源站内容不对，已中止"
fi
got="$(sha256sum "$tmp/$asset" | awk '{print $1}')"
if [ "$want" != "$got" ]; then
  fail "校验失败：期望 $want / 实际 $got —— 下载不完整或被篡改，已中止，请反馈给管理员"
fi
log "    一致：$got"

# ---- 落盘：能写则写，不能写则借 sudo，再不行落到当前目录 ----
name="securelink-agent"
if [ -w "$DEST_DIR" ] 2>/dev/null || [ "$(id -u)" = "0" ]; then
  install -m 0755 "$tmp/$asset" "$DEST_DIR/$name"
elif command -v sudo >/dev/null 2>&1; then
  log "==> 写入 $DEST_DIR 需要 root，使用 sudo"
  sudo install -m 0755 "$tmp/$asset" "$DEST_DIR/$name"
else
  DEST_DIR="$PWD"
  log "==> 无 sudo，安装到当前目录 $DEST_DIR"
  install -m 0755 "$tmp/$asset" "$DEST_DIR/$name"
fi

log "==> 已安装: $DEST_DIR/$name"
log ""
log "下一步："
log "  1. 设置 Agent 配置（SAAS_ENDPOINT、TENANT_UID、ENROLLMENT_TOKEN_FILE 等，"
log "     令牌在控制台「开通引导」页生成——变量清单见文档第五节）"
log "  2. 预览服务定义:  $DEST_DIR/$name install --dry-run"
log "  3. 真正安装:      sudo -E $DEST_DIR/$name install"
log ""
log "完整说明: https://sclink.dev/docs/install/"
