#!/usr/bin/env bash
# offerdao daemon 一键安装器。由服务端 /install.sh 路由动态注入 https://offerdao.ai 后 serve。
# 用法（环境变量前缀须在 `| bash` 之前）：
#   curl -fsSL <origin>/install.sh | OFFERDAO_API_KEY=sk-xxx OFFERDAO_SERVER_URL=<origin> bash
#
# 把 daemon 整组源码下载到 ~/.offerdao/daemon/，并注册开机自启常驻：
#   - macOS：launchd LaunchAgent（登录自启 + 崩溃自动拉起）
#   - Linux：systemd --user 服务
#   - 其它平台 / 注册失败：回退 --detach 后台启动（重启电脑后需重跑安装命令）
# 管道执行时 stdin 被占用，全程不交互 read；下载到临时目录全成功后再原子替换，
# 避免半截安装；只替换 daemon/ 代码目录，保留同级 config.json / agent/ / 日志。
set -euo pipefail

# ---- 服务端注入（占位符在 serve 时被 resolvePublicOrigin 替换）----
BASE_URL_DEFAULT="https://offerdao.ai"

# ---- 配置 ----
INSTALL_DIR="${HOME}/.offerdao/daemon"
SERVER_URL="${OFFERDAO_SERVER_URL:-$BASE_URL_DEFAULT}"
API_KEY="${OFFERDAO_API_KEY:-}"

# 安装清单不再硬编码：从服务端 /daemon-src/manifest.json 拉取（见下方 2.5 节）。
# 单一真值源 = server.js 的 DAEMON_SRC_FILES 白名单，daemon 自更新用的也是同一份
# 清单——新增文件只改服务端白名单一处，本脚本与自更新自动跟上。

err()  { printf '\033[31m[offerdao]\033[0m %s\n' "$*" >&2; }
info() { printf '\033[36m[offerdao]\033[0m %s\n' "$*"; }
ok()   { printf '\033[32m[offerdao]\033[0m %s\n' "$*"; }

# ---- 1. 前置检查：bash / curl / node ----
if [ -z "${BASH_VERSION:-}" ]; then
  err "请用 bash 执行（… | bash）。"; exit 1
fi
command -v curl >/dev/null 2>&1 || { err "未找到 curl，请先安装 curl。"; exit 1; }

if ! command -v node >/dev/null 2>&1; then
  err "未检测到 Node.js。daemon 需要 Node ≥ 20。"
  err "安装：https://nodejs.org/  或  brew install node / nvm install 20"
  exit 1
fi
# 取主版本：解析 process.versions.node 比解析 `node -v`（带 v 前缀/nightly 后缀）稳。
NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))' 2>/dev/null || echo 0)"
if [ "${NODE_MAJOR:-0}" -lt 20 ]; then
  err "Node 版本过低（当前 $(node -v 2>/dev/null)），需要 ≥ 20。"
  err "升级：nvm install 20 && nvm use 20"
  exit 1
fi

# ---- 2. 校验必填参数 ----
if [ -z "$API_KEY" ]; then
  err "缺少 OFFERDAO_API_KEY。正确用法："
  err "  curl -fsSL ${SERVER_URL}/install.sh | OFFERDAO_API_KEY=sk-xxx OFFERDAO_SERVER_URL=${SERVER_URL} bash"
  err "API key 在网页端「我的设置 → API keys」创建。"
  exit 1
fi
case "$API_KEY" in
  sk-*) : ;;
  *) err "OFFERDAO_API_KEY 格式不对，应以 sk- 开头。"; exit 1 ;;
esac
SERVER_URL="${SERVER_URL%/}"   # 去尾部斜杠
# 校验是否为合法 origin。注意：不能用 "https://offerdao.ai" 字面量做哨兵——服务端
# /install.sh 路由对脚本做全局 https://offerdao.ai→origin 的替换，会把这里的哨兵也
# 一起换掉，导致「SERVER_URL 恰好等于 origin 就误报」。改判 http(s) 前缀：脚本
# 未经 serve 直接运行（占位符未替换）时 SERVER_URL 不以 http 开头，照样被拦下。
case "$SERVER_URL" in
  http://*|https://*) : ;;
  *)
    err "缺少有效的 OFFERDAO_SERVER_URL，请显式传入 OFFERDAO_SERVER_URL=<origin>（http(s)://…）。"
    exit 1
    ;;
esac
SRC_BASE="${SERVER_URL}/daemon-src"

# ---- 2.5 拉取安装清单 ----
# rel 既是远端 /daemon-src/<rel> 的 key，也是本地落盘路径。node 解析时顺带做路径
# 防御（拒绝 .. / 绝对路径 / 奇异字符）。package.json 必须在列（含 "type":"module"），
# 否则 ~/.offerdao/daemon 下的 .js 会被 Node 当 CommonJS，import 语句直接报 SyntaxError。
info "获取安装清单 ${SRC_BASE}/manifest.json …"
MANIFEST_JSON="$(curl -fsSL "${SRC_BASE}/manifest.json")" || {
  err "拉取安装清单失败：${SRC_BASE}/manifest.json"
  exit 1
}
FILES=()
while IFS= read -r rel; do
  [ -n "$rel" ] && FILES+=("$rel")
done < <(printf '%s' "$MANIFEST_JSON" | node -e '
  let s = "";
  process.stdin.on("data", (d) => (s += d));
  process.stdin.on("end", () => {
    for (const rel of Object.keys(JSON.parse(s).files || {})) {
      if (/^[A-Za-z0-9._\/-]+$/.test(rel) && !rel.startsWith("/") && !rel.split("/").includes("..")) console.log(rel);
    }
  });
')
if [ "${#FILES[@]}" -eq 0 ]; then
  err "安装清单为空或解析失败（服务端版本过旧？）。"
  exit 1
fi
case " ${FILES[*]} " in
  *" package.json "*) : ;;
  *) err "安装清单缺少 package.json，中止。"; exit 1 ;;
esac

# ---- 3. 下载到临时目录（全成功才落地，避免半截安装）----
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/offerdao-install.XXXXXX")"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT INT TERM

info "从 ${SRC_BASE} 下载 daemon（${#FILES[@]} 个文件）…"
for rel in "${FILES[@]}"; do
  dest="${TMP_DIR}/${rel}"
  mkdir -p "$(dirname "$dest")"
  if ! curl -fsSL "${SRC_BASE}/${rel}" -o "$dest"; then
    err "下载失败：${SRC_BASE}/${rel}"
    exit 1
  fi
  if [ ! -s "$dest" ]; then
    err "下载到空文件：${rel}"
    exit 1
  fi
done

# ---- 4. 原子替换（只动 daemon/ 代码目录，保留用户数据）----
mkdir -p "${HOME}/.offerdao"
rm -rf "${INSTALL_DIR}.new" "${INSTALL_DIR}.old"
cp -R "$TMP_DIR" "${INSTALL_DIR}.new"
[ -d "$INSTALL_DIR" ] && mv "$INSTALL_DIR" "${INSTALL_DIR}.old"
mv "${INSTALL_DIR}.new" "$INSTALL_DIR"
rm -rf "${INSTALL_DIR}.old"
ok "已安装到 ${INSTALL_DIR}"

ENTRY="${INSTALL_DIR}/bin/offerdao-daemon.js"

# ---- 5. 重装/升级：先停旧实例（单实例守卫下新实例不会用新代码重启）----
node "$ENTRY" --stop >/dev/null 2>&1 || true
# --stop 是异步的：SIGTERM 后 daemon 还要 flush 画像（最多 ~3s）。等旧进程真正
# 退出再拉新实例，避免单实例守卫看到残影误判「已在运行」。
OLD_PID="$(cat "${HOME}/.offerdao/daemon.pid" 2>/dev/null || true)"
if [ -n "$OLD_PID" ]; then
  for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
    kill -0 "$OLD_PID" 2>/dev/null || break
    sleep 0.5
  done
fi

# ---- 6. 注册开机自启并启动 ----
LAUNCHD_LABEL="ai.offerdao.daemon"
LAUNCHD_PLIST="${HOME}/Library/LaunchAgents/${LAUNCHD_LABEL}.plist"
SYSTEMD_UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
SYSTEMD_UNIT="${SYSTEMD_UNIT_DIR}/offerdao-daemon.service"

# 写进 plist/unit 的值做最小转义：XML 的 &<>；systemd 的 %（specifier）。
xml_escape() { printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'; }
sd_escape() { printf '%s' "$1" | sed -e 's/%/%%/g'; }

# daemon 由 launchd/systemd 直接拉起（无登录 shell），PATH 必须自带 node 与
# claude/codex 等 CLI 所在目录：取安装时的完整 PATH，再补常见目录兜底。
runtime_path_env() { printf '%s' "${PATH}:/opt/homebrew/bin:/usr/local/bin:${HOME}/.local/bin"; }

# macOS：生成 LaunchAgent 并加载。RunAtLoad=登录即启动（load 时也立即启动一次）；
# KeepAlive={SuccessfulExit:false}=非零退出（崩溃、开机时网络未就绪注册失败）自动
# 拉起，正常退出（--stop、网页端断开 410、key 失效 401）不再重启——daemon 对
# 410/401 还会自删本 plist，杜绝「断开设备后开机又复活」。
install_autostart_launchd() {
  local node_bin
  node_bin="$(command -v node)"
  mkdir -p "${HOME}/Library/LaunchAgents" "${HOME}/.offerdao"
  launchctl unload "$LAUNCHD_PLIST" 2>/dev/null || true
  cat > "$LAUNCHD_PLIST" <<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>${LAUNCHD_LABEL}</string>
  <key>ProgramArguments</key><array>
    <string>$(xml_escape "$node_bin")</string>
    <string>$(xml_escape "$ENTRY")</string>
  </array>
  <key>EnvironmentVariables</key><dict>
    <key>OFFERDAO_SERVER_URL</key><string>$(xml_escape "$SERVER_URL")</string>
    <key>OFFERDAO_API_KEY</key><string>$(xml_escape "$API_KEY")</string>
    <key>OFFERDAO_MANAGED</key><string>launchd</string>
    <key>PATH</key><string>$(xml_escape "$(runtime_path_env)")</string>
    <key>HOME</key><string>${HOME}</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>ThrottleInterval</key><integer>15</integer>
  <key>StandardOutPath</key><string>${HOME}/.offerdao/daemon.log</string>
  <key>StandardErrorPath</key><string>${HOME}/.offerdao/daemon.log</string>
</dict></plist>
PLIST_EOF
  chmod 600 "$LAUNCHD_PLIST"   # 含 API key 明文，仅本人可读（与 config.json 同级别）
  if launchctl load "$LAUNCHD_PLIST" 2>/dev/null; then
    return 0
  fi
  rm -f "$LAUNCHD_PLIST"   # 加载失败则不留半套配置，走 --detach 回退
  return 1
}

# Linux：systemd --user 服务。Restart=on-failure 与 launchd 语义对齐。
install_autostart_systemd() {
  local node_bin
  node_bin="$(command -v node)"
  mkdir -p "$SYSTEMD_UNIT_DIR"
  cat > "$SYSTEMD_UNIT" <<UNIT_EOF
# offerdao daemon —— 由 install.sh 生成，重跑安装命令会重建本文件。
[Unit]
Description=offerdao daemon (AI CLI bridge + local job agent)

[Service]
ExecStart=$(sd_escape "$node_bin") $(sd_escape "$ENTRY")
Environment="OFFERDAO_SERVER_URL=$(sd_escape "$SERVER_URL")"
Environment="OFFERDAO_API_KEY=$(sd_escape "$API_KEY")"
Environment="OFFERDAO_MANAGED=systemd"
Environment="PATH=$(sd_escape "$(runtime_path_env)")"
Restart=on-failure
RestartSec=15

[Install]
WantedBy=default.target
UNIT_EOF
  chmod 600 "$SYSTEMD_UNIT"   # 含 API key 明文，仅本人可读
  if systemctl --user daemon-reload 2>/dev/null \
    && systemctl --user enable offerdao-daemon.service >/dev/null 2>&1 \
    && systemctl --user restart offerdao-daemon.service 2>/dev/null; then
    return 0
  fi
  rm -f "$SYSTEMD_UNIT"
  return 1
}

STARTED_VIA=""
case "$(uname -s)" in
  Darwin)
    if command -v launchctl >/dev/null 2>&1; then
      if install_autostart_launchd; then
        STARTED_VIA="launchd"
        ok "已注册开机自启（launchd），daemon 已由 launchd 启动。"
      else
        err "launchctl 注册失败，回退为普通后台启动（无开机自启）。"
      fi
    fi
    ;;
  Linux)
    # systemctl --user 可达才算有 user manager（容器 / 无 systemd 发行版会失败）。
    if command -v systemctl >/dev/null 2>&1 && systemctl --user show-environment >/dev/null 2>&1; then
      if install_autostart_systemd; then
        STARTED_VIA="systemd"
        ok "已注册开机自启（systemd --user），daemon 已启动。"
        if command -v loginctl >/dev/null 2>&1 \
          && ! loginctl show-user "$USER" --property=Linger 2>/dev/null | grep -q '=yes'; then
          info "提示：服务器 / 无桌面环境如需「无人登录也常驻」，执行一次：sudo loginctl enable-linger $USER"
        fi
      else
        err "systemd 注册失败，回退为普通后台启动（无开机自启）。"
      fi
    fi
    ;;
esac

if [ -z "$STARTED_VIA" ]; then
  info "启动 daemon（后台常驻；本平台未注册开机自启，重启电脑后请重跑本安装命令）…"
  OFFERDAO_SERVER_URL="$SERVER_URL" OFFERDAO_API_KEY="$API_KEY" \
    node "$ENTRY" --detach
fi

# ---- 7. 成功提示 + 后续命令 ----
ok "完成。后续管理命令："
echo "  查看状态：node ${ENTRY} --status"
echo "  停止：    node ${ENTRY} --stop"
echo "  日志：    ${HOME}/.offerdao/daemon.log"
echo "  重装/升级：重新跑一次本安装命令即可。"
case "$STARTED_VIA" in
  launchd)
    echo "  开机自启：已启用（登录后自动拉起，崩溃自动重启；--stop 只停到下次开机）。"
    echo "  取消自启：launchctl unload ${LAUNCHD_PLIST} && rm ${LAUNCHD_PLIST}"
    ;;
  systemd)
    echo "  开机自启：已启用（登录后自动拉起，崩溃自动重启；--stop 只停到下次开机）。"
    echo "  取消自启：systemctl --user disable --now offerdao-daemon"
    ;;
esac
