#!/usr/bin/env bash
# Atria 后端一键安装/升级（幂等）。
#   curl -fsSL https://cdn.jsdelivr.net/npm/atria-server/install.sh | bash
# （jsDelivr 从 atria-server 元包里取同一份文件。主推它而不是 GitHub Release 资产：
#  引导脚本从前只挂在 release-assets.githubusercontent.com 上，那是整条链里最容易
#  连不上的一台主机——本脚本内部的下载链早就是三级回退了，唯独取到本脚本这一步是单点。
#  jsDelivr 边缘缓存 12 小时，所以刚发布的 install.sh 改动最多晚半天才铺开；
#  真要立刻拿到新的就走 GitHub Release 资产或 npmmirror 的元包 tarball。）
# 环境变量:
#   ATRIA_HOME         安装/数据目录（默认 ~/.atria）
#   ATRIA_VERSION      指定版本 tag（默认 latest）
#   ATRIA_TARBALL      本地 tarball 路径（跳过下载；SFTP 推包/离线场景）
#   ATRIA_DIST_BASE    强制指定下载源基址（跳过自动选择）
#   ATRIA_DIST_MIRRORS 额外自建镜像列表（空格分隔；默认空——正常下载链是
#                      npmmirror → npmjs → GitHub 三级自动回退，国内外零配置都能装）。
#                      镜像一律用 IP——未备案域名的 HTTP 会被云厂商劫持成 ICP 拦截页
#   ATRIA_PORT         服务端口（默认 8790，写入 $ATRIA_HOME/env 后生效）
#   ATRIA_NO_AUTOSTART 置 1 跳过开机自启配置（测试用）
#
# 普通升级保留正在持有终端的 ptyhost；仅检测到旧 PTY 协议时，安装器会走
# atriad cutover-old-pty 这条显式破坏性迁移路径（旧终端会被终止）。
# 约定（iOS 自动部署解析）: 成功时【最后一行】输出机器可读 JSON:
#   {"port":8790,"token":"<t>","version":"x.y.z"}
set -euo pipefail

REPO="${ATRIA_RELEASE_REPO:-nerslm/atria-release}"
ATRIA_HOME="${ATRIA_HOME:-$HOME/.atria}"
WANT="${ATRIA_VERSION:-latest}"
PORT="${ATRIA_PORT:-8790}"

log() { echo "[atria-install] $*" >&2; }

# 下载器：curl 优先，wget 兜底（最小化镜像常常只有其一）。
# curl 必须带 --speed-limit/--speed-time 停滞保护：--connect-timeout 只管连接阶段，
# 管不了"连上了但传输停滞"——生产踩过 GitHub 连接成功、85MB 传到几 MB 后彻底断流，
# curl 永远挂着、永远轮不到下一个源。低于 1KB/s 持续 30 秒即中止判失败。
# （wget 的 -T 本身覆盖读超时，无此问题。）
fetch() { # $1=url $2=dest
  if command -v curl >/dev/null 2>&1; then
    curl -fSL --retry 3 --speed-limit 1024 --speed-time 30 -o "$2" "$1" >&2
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O "$2" "$1" >&2
  else
    log "curl or wget required"; exit 1
  fi
}
# 短超时下载：源自动选择用——GitHub 在部分网络不可达，8 秒连不上就换源，
# 不能像 fetch 那样长重试把用户吊死在第一个源上
fetch_quick() { # $1=url $2=dest
  if command -v curl >/dev/null 2>&1; then
    curl -fSL --connect-timeout 8 --retry 1 --speed-limit 1024 --speed-time 30 -o "$2" "$1" >&2
  else
    wget -q -T 8 -t 2 -O "$2" "$1" >&2
  fi
}
fetch_text() { # $1=url → stdout（小文件，直接整体限时兜底停滞）
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --connect-timeout 8 -m 20 "$1" 2>/dev/null
  else
    wget -qO- -T 8 "$1" 2>/dev/null
  fi
}
verify_sha256() { # $1=asset $2=期望 hex 或 checksum 文件
  local file="$1" expected="$2" actual
  if [ -f "$expected" ]; then
    expected="$(sed -n 's/^\([0-9a-fA-F]\{64\}\).*/\1/p' "$expected" | head -1)"
  fi
  expected="$(printf '%s' "$expected" | tr 'A-F' 'a-f')"
  printf '%s' "$expected" | grep -Eq '^[0-9a-f]{64}$' \
    || { log "Invalid or missing SHA-256 for $(basename "$file")"; return 1; }
  if command -v sha256sum >/dev/null 2>&1; then
    actual="$(sha256sum "$file" | awk '{print $1}')"
  else
    actual="$(shasum -a 256 "$file" | awk '{print $1}')"
  fi
  [ "$actual" = "$expected" ] \
    || { log "SHA-256 mismatch for $(basename "$file")"; return 1; }
}
# 就绪探测：同上双兜底
probe() { # $1=url
  if command -v curl >/dev/null 2>&1; then
    curl -fsS -m 3 "$1" >/dev/null 2>&1
  else
    wget -q -T 3 -O /dev/null "$1" >/dev/null 2>&1
  fi
}

case "$(uname -s)" in
  Linux)  OS=linux ;;
  Darwin) OS=darwin ;;
  *) log "Unsupported OS: $(uname -s)"; exit 1 ;;
esac
case "$(uname -m)" in
  x86_64|amd64) ARCH=x64 ;;
  arm64|aarch64) ARCH=arm64 ;;
  *) log "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
PLATFORM="$OS-$ARCH"
ASSET="atria-server-$PLATFORM.tar.gz"

# 预检：端口被「不归本安装器管理」的服务占用时，下载前就明确失败
# （典型场景：手动部署的旧版 Atria 还在跑——先停掉它再重试）
if [ ! -e "$ATRIA_HOME/dist/current" ] && probe "http://127.0.0.1:$PORT/version"; then
  log "Port $PORT is already in use — something not managed by this installer is listening."
  log "  If it is an older or hand-deployed Atria, stop it first (e.g. systemctl stop atria-gateway atria-controller atria-ptyhost atria) and retry."
  exit 1
fi

mkdir -p "$ATRIA_HOME/dist"

# ---- 取包 ----
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
if [ -n "${ATRIA_TARBALL:-}" ]; then
  log "Using local tarball: $ATRIA_TARBALL"
  cp "$ATRIA_TARBALL" "$TMP/$ASSET"
  if [ -n "${ATRIA_TARBALL_SHA256:-}" ]; then
    verify_sha256 "$TMP/$ASSET" "$ATRIA_TARBALL_SHA256"
  else
    log "Local tarball has no ATRIA_TARBALL_SHA256; authenticity is the caller's responsibility"
  fi
elif [ -n "${ATRIA_DIST_BASE:-}" ]; then
  BASE="${ATRIA_DIST_BASE%/}"
  if [ "$WANT" = "latest" ]; then
    URL="$BASE/$ASSET"
  else
    URL="$BASE/$WANT/$ASSET"
  fi
  log "Downloading (pinned source) $URL"
  fetch "$URL" "$TMP/$ASSET"
  fetch "$URL.sha256" "$TMP/$ASSET.sha256"
  verify_sha256 "$TMP/$ASSET" "$TMP/$ASSET.sha256"
else
  # 自动选源，逐级短超时回退——国内外零配置，npm 系 registry 优先：
  #   npmmirror（国内 CDN）→ npmjs 官方源 → GitHub Releases（保底）→ ATRIA_DIST_MIRRORS
  # GitHub 直连在部分网络"连接成功但中途断流"，只当最后保底，不再打头阵。
  MIRRORS="${ATRIA_DIST_MIRRORS:-}"
  NPMPKG="atria-server-$PLATFORM"

  # 从 npm registry 取包（npmmirror/npmjs 同协议）：/latest 拿版本 → tarball 两层解包
  # （npm 包内容物就是 release tarball 原样，package/<ASSET>）。
  # 版本赋值必须 || true 兜底：set -o pipefail 下 /latest 404 会让整条管道失败，
  # 脚本会在赋值处直接暴毙、连"全部源不可达"都打不出来（生产踩过）
  try_npm_registry() { # $1=registry 基址
    local base="${1%/}" ver
    if [ "$WANT" = "latest" ]; then
      # 版本发现元包（atria-server）优先：npmmirror 的平台包元数据惯性滞后，
      # 曾停在旧版且非空——按"平台包优先"会把过期答案当真，升级变无声空转
      # （v1.0.19 发布当晚实锤）。元包小、同步快，iOS 检查更新同样以它为准；
      # 平台包 /latest 只作元包拿不到时的兜底。tarball 按版本号直取不受影响
      ver="$(fetch_text "$base/atria-server/latest" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1 || true)"
      if [ -z "$ver" ]; then
        ver="$(fetch_text "$base/$NPMPKG/latest" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1 || true)"
      fi
    else
      ver="${WANT#v}"
    fi
    [ -n "$ver" ] || return 1
    log "Downloading $base/$NPMPKG/-/$NPMPKG-$ver.tgz"
    rm -rf "$TMP/package"
    if fetch_quick "$base/$NPMPKG/-/$NPMPKG-$ver.tgz" "$TMP/npm-pkg.tgz" \
      && tar -xzf "$TMP/npm-pkg.tgz" -C "$TMP" "package/$ASSET" 2>/dev/null \
      && [ -s "$TMP/package/$ASSET" ]; then
      mv "$TMP/package/$ASSET" "$TMP/$ASSET"
      tar -xzf "$TMP/npm-pkg.tgz" -C "$TMP" "package/$ASSET.sha256" 2>/dev/null || true
      [ ! -s "$TMP/package/$ASSET.sha256" ] || mv "$TMP/package/$ASSET.sha256" "$TMP/$ASSET.sha256"
      if [ ! -s "$TMP/$ASSET.sha256" ]; then
        # v1.0.60 以前的平台 npm 包只装 tarball；精确回装旧版时仍可从对应 GitHub
        # release 取得校验值。新包一律把 checksum 与签名一起放进 npm 包。
        fetch_text "https://github.com/$REPO/releases/download/v$ver/$ASSET.sha256" > "$TMP/$ASSET.sha256" || true
      fi
      verify_sha256 "$TMP/$ASSET" "$TMP/$ASSET.sha256" || return 1
      return 0
    fi
    return 1
  }

  OK=""
  if try_npm_registry "https://registry.npmmirror.com"; then
    OK=1
  elif try_npm_registry "https://registry.npmjs.org"; then
    OK=1
  else
    if [ "$WANT" = "latest" ]; then
      URL="https://github.com/$REPO/releases/latest/download/$ASSET"
    else
      URL="https://github.com/$REPO/releases/download/$WANT/$ASSET"
    fi
    log "npm registries unreachable, falling back to GitHub: $URL"
    if fetch_quick "$URL" "$TMP/$ASSET" \
      && fetch_quick "$URL.sha256" "$TMP/$ASSET.sha256" \
      && verify_sha256 "$TMP/$ASSET" "$TMP/$ASSET.sha256"; then OK=1; fi
  fi
  # 逃生口：自建镜像（默认无）
  if [ -z "$OK" ]; then
    for M in $MIRRORS; do
      M="${M%/}"
      if [ "$WANT" = "latest" ]; then MURL="$M/$ASSET"; else MURL="$M/$WANT/$ASSET"; fi
      log "Falling back to mirror $MURL"
      if fetch_quick "$MURL" "$TMP/$ASSET" \
        && fetch_quick "$MURL.sha256" "$TMP/$ASSET.sha256" \
        && verify_sha256 "$TMP/$ASSET" "$TMP/$ASSET.sha256"; then OK=1; break; fi
    done
  fi
  [ -n "$OK" ] || { log "All download sources unreachable"; exit 1; }
fi

tar -xzf "$TMP/$ASSET" -C "$TMP"
VERSION="$(cat "$TMP/atria/VERSION")"
BUILD_ID=""
if [ -f "$TMP/atria/MANIFEST.json" ]; then
  BUILD_ID="$(sed -n 's/.*"sourceCommit": *"\([^"]*\)".*/\1/p' "$TMP/atria/MANIFEST.json" | head -1)"
fi
BUILD_SUFFIX=""
[ -n "$BUILD_ID" ] && BUILD_SUFFIX="-${BUILD_ID:0:12}"
DEST="$ATRIA_HOME/dist/atria-$VERSION$BUILD_SUFFIX"

# ---- 幂等快路径：版本和构建身份都相同且服务健康 → 直接出 JSON ----
# 只比较 VERSION 会让同版本修复包永远无法覆盖旧/残缺二进制。
CUR="$ATRIA_HOME/dist/current"
CURRENT_BUILD_ID=""
if [ -f "$CUR/MANIFEST.json" ]; then
  CURRENT_BUILD_ID="$(sed -n 's/.*"sourceCommit": *"\([^"]*\)".*/\1/p' "$CUR/MANIFEST.json" | head -1)"
fi
if [ -e "$CUR/VERSION" ] \
  && [ "$(cat "$CUR/VERSION")" = "$VERSION" ] \
  && [ -n "$BUILD_ID" ] \
  && [ "$CURRENT_BUILD_ID" = "$BUILD_ID" ]; then
  if probe "http://127.0.0.1:$PORT/version"; then
    TOKEN="$(cat "$ATRIA_HOME/token" 2>/dev/null || true)"
    if [ -n "$TOKEN" ]; then
      log "Already on v$VERSION (build ${BUILD_ID:0:12}) and running. Nothing to do."
      echo "{\"port\":$PORT,\"token\":\"$TOKEN\",\"version\":\"$VERSION\"}"
      exit 0
    fi
  fi
fi

# ---- 安装 ----
log "Installing v$VERSION -> $DEST"
rm -rf "$DEST"
mv "$TMP/atria" "$DEST"

# token：dist 之外持久化，升级不换
if [ ! -s "$ATRIA_HOME/token" ]; then
  ( umask 077; head -c 24 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=\n' > "$ATRIA_HOME/token" )
  log "Access token generated"
fi
# 端口偏好写入 env（atriad 会 source）
if [ "$PORT" != "8790" ] && ! grep -q ATRIA_PORT "$ATRIA_HOME/env" 2>/dev/null; then
  echo "ATRIA_PORT=$PORT" >> "$ATRIA_HOME/env"
fi

# 先用新版本 atriad 识别当前 socket，再切链接。v1 ptyhost 在普通升级中原地保留；
# 只有旧协议进入命名明确的 cutover-old-pty 分支，避免把普通 restart 变成隐式断会话。
PTY_STATE="$(ATRIA_HOME="$ATRIA_HOME" ATRIA_DIST="$DEST" "$DEST/bin/atriad" pty-protocol)"
log "PTY protocol: $PTY_STATE"
case "$PTY_STATE" in
  v1|absent|legacy) ;;
  *)
    log "PTY socket state unsafe or unrecognized ($PTY_STATE) — refusing to switch current"
    exit 1
    ;;
esac

ln -sfn "$DEST" "$CUR"
mkdir -p "$ATRIA_HOME/bin"
ln -sfn "$CUR/bin/atriad" "$ATRIA_HOME/bin/atriad"

# 把 atriad 放进交互 shell 的 PATH，省掉每次敲全路径（幂等；失败不阻塞安装）。
# 只加 $ATRIA_HOME/bin，不加 dist/current/bin——那里面是 atria-server 和四个指向它
# 的软链（含 agent 的 atria），版本切换时整个换掉，不该出现在用户的长期 PATH 里。
if [ -z "${ATRIA_NO_PATH_SETUP:-}" ]; then
  # 追加而不是前置：~/.atria/bin 与用户自行安装的 atria-code CLI 共用。前置会让这里
  # 的东西盖住系统同名命令；追加则系统的优先，只有系统没有时才用这里的。atriad 本身
  # 不重名，放哪一侧都能被找到。
  PATH_LINE="export PATH=\"\$PATH:\$HOME/.atria/bin\"  # atria"
  for RC in "$HOME/.bashrc" "$HOME/.zshrc"; do
    [ -f "$RC" ] || continue
    grep -qF '.atria/bin' "$RC" 2>/dev/null && continue
    printf '\n%s\n' "$PATH_LINE" >> "$RC" \
      && log "Added ~/.atria/bin to PATH in $RC (takes effect in a new shell)"
  done
fi

if [ "$PTY_STATE" = "legacy" ]; then
  log "Legacy PTY protocol detected — running explicit cutover. Existing terminals will be killed."
  ATRIA_HOME="$ATRIA_HOME" "$CUR/bin/atriad" cutover-old-pty >&2
else
  # restart 只重启 gateway/controller；若 ptyhost 不存在才新建，已有 v1 PTY 不受影响。
  ATRIA_HOME="$ATRIA_HOME" "$CUR/bin/atriad" restart >&2
fi

# 开机自启（Linux：cron @reboot，幂等；失败不阻塞安装）
if [ -z "${ATRIA_NO_AUTOSTART:-}" ] && [ "$OS" = "linux" ]; then
  if command -v crontab >/dev/null 2>&1; then
    MARK="# atria-autostart"
    ENTRY="@reboot ATRIA_HOME=$ATRIA_HOME $ATRIA_HOME/bin/atriad start $MARK"
    if ! crontab -l 2>/dev/null | grep -qF "$MARK"; then
      ( crontab -l 2>/dev/null; echo "$ENTRY" ) | crontab - \
        && log "Autostart configured (cron @reboot)" \
        || log "Autostart setup failed (install itself is fine). Add manually: crontab -e -> '$ENTRY'"
    fi
  else
    log "No crontab found, skipping autostart. For systemd see $CUR/deploy/*.service"
  fi
elif [ "$OS" = "darwin" ]; then
  log "No autostart on macOS (needs launchd). After a reboot run: $ATRIA_HOME/bin/atriad start"
fi

# ---- 等就绪，输出契约 JSON ----
for _ in $(seq 1 30); do
  if probe "http://127.0.0.1:$PORT/version"; then
    TOKEN="$(cat "$ATRIA_HOME/token")"
    # 只提示，不直接印码。二维码里带着 token、且是短时效的东西，无条件冲进滚动历史
    # （以及 iOS SSH 自动部署的日志）并不合适——什么时候要码由用户自己决定。
    # 中继地址已是内置默认值，所以这里不需要再教用户配任何东西。
    # atriad 已软链进 $ATRIA_HOME/bin，PATH 也写进了 rc——但 rc 要新 shell 才读，
    # 刚装完的这个终端里 `atriad` 还不认。脚本改不了父 shell 的环境（子进程改不了
    # 父进程），所以没法"装完自动刷新"，只能如实说清楚：首装提示新开终端，
    # 升级（PATH 里已经有了）就直接给短命令。
    if command -v atriad >/dev/null 2>&1; then
      log "Pair your phone: run  atriad pair  and scan the code (in the app: tap the server name -> Scan QR)"
      log "Done. Manage: atriad status|log|restart  (use restart-ptyhost for the PTY host)"
    else
      log "Pair your phone: open a NEW terminal and run  atriad pair"
      log "                 then scan the code (in the app: tap the server name -> Scan QR)"
      log "Done. Manage: atriad status|log|restart  (new terminal required; use restart-ptyhost for the PTY host)"
    fi
    echo "{\"port\":$PORT,\"token\":\"$TOKEN\",\"version\":\"$VERSION\"}"
    exit 0
  fi
  sleep 1
done
log "Server did not become ready within 30s. Recent log:"
tail -20 "$ATRIA_HOME/log/gateway.log" >&2 || true
exit 1
