#!/bin/sh
# 云鸢（Kite）daemon 一键接入脚本（Linux / macOS）。
#
# 本脚本由 relay 服务端模板渲染：RELAY_WS / 下载基址 / 版本 pin 由 relay 据自身域名插入，
# 故 `curl https://kite.lumenlan.com/install | sh` 与 `…/kite.soundlan.com/install | sh`
# 自动产出对应环境的脚本，零脚本端 if（域名自适应靠模板，不硬编码）。
#
# 装好后 daemon 首启走「生态设备授权」：弹浏览器登录灵澜账号，或无浏览器时显示机器码在网页授权，
# 拿到 per-machine token 存系统 keyring——**无需手工粘 token**。
#
# 幂等：set -eu + 每步可重入（二进制 .new 原子覆盖、服务 unit 无条件覆盖 + 重载 + 重启）；
# 跑第二次 = 覆盖重装，不报「已安装」。
#
# ⚠️ 约定：变量展开若**紧贴**中日韩全角标点（如 `${BIN_NAME}（…`）必须加花括号 `${VAR}`，不能裸写 `$VAR（`。
# macOS `/bin/sh` = bash 3.2，其多字节词法有 bug：裸 `$VAR` 后紧跟全角字符时会把该字符首字节（如 `（`=0xEF）
# 吞进变量名 → 得到一个未定义的名字 → `set -u` 报 `unbound variable`（真机踩过：`BIN_NAME?: unbound variable`）。
# 花括号显式界定名字边界即根治；ASCII 分隔（`/`、空格）后的 `$VAR` 无此问题。
set -eu

# ── 模板注入（relay 渲染，禁脚本端硬编码域名 / 下载源）──
RELAY_WS="wss://kite.lumenlan.com/v1/daemon/connect"               # wss://<relay-host>/v1/daemon/connect
ACCOUNT_BASE="https://account.lumenlan.com"    # 灵澜账号 base（device flow），空=daemon 用内置默认
RELEASE_BASE="https://kite.lumenlan.com/dl" # daemon 二进制下载基址（kite.soundlan.com/dl，由 relay env 注入）
VERSION="v0.14.7"          # 版本 pin（如 v0.1.0），空=latest

INSTALL_DIR="${KITE_INSTALL_DIR:-/usr/local/bin}" # launcher 装这（可能需一次 sudo，之后永不自更新）
BIN_NAME="kite-daemon"
LAUNCHER_NAME="kite-launcher"
# CONF_DIR / CONF_FILE / PAYLOAD_DIR 在 OS 探测（第 1 步）后定，因默认值按平台分叉（XDG vs macOS App Support）。
# 自更新防变砖布局（[plan 16 WS-5]）：launcher（永不自更新）装 INSTALL_DIR；daemon payload（自更新对象）
# 落**用户可写** PAYLOAD_DIR——daemon 跑用户态，payload 在 root-owned 目录则无权覆盖自己、自更新不可用。

log() { printf '\033[36m▸\033[0m %s\n' "$1" >&2; }
die() { printf '\033[31m✗\033[0m %s\n' "$1" >&2; exit 1; }

# ── 1. 检测 OS / arch → 拼二进制名 ──
os="$(uname -s)"
case "$os" in
  Linux)  GOOS="linux" ;;
  Darwin) GOOS="darwin" ;;
  *) die "不支持的操作系统：${os}（仅 Linux / macOS；Windows 用 install.ps1）" ;;
esac

arch="$(uname -m)"
case "$arch" in
  x86_64 | amd64)        GOARCH="amd64" ;;
  arm64 | aarch64)       GOARCH="arm64" ;;
  *) die "不支持的架构：${arch}（仅 amd64 / arm64）" ;;
esac

# 配置目录默认落**登录用户可写、用户态 daemon 可读**处（plan 05 WS-2 主线：daemon 跑登录用户态，
# 以登录用户身份起 systemd --user / LaunchAgent，**无权读 root:600 的 /etc/kite/daemon.env**，
# 叠加 config.go loadEnvFile 对 ReadFile 错误静默 return → daemon 拿不到 relay 地址静默起不来）。
#   · Linux ：$XDG_CONFIG_HOME/kite（默认 ~/.config/kite）——遵 XDG，用户拥有、用户态可读写。
#   · macOS ：$HOME/Library/Application Support/Kite——Apple 推荐的 per-user 应用数据目录。
# device-auth.txt 也落 conf 同目录（见 daemon deviceauth.go），故此目录必须用户态可**写**——
# 以登录用户身份（不 sudo）写、mode 600 user-owned，与服务进程身份一致。
if [ -n "${KITE_CONF_DIR:-}" ]; then
  CONF_DIR="$KITE_CONF_DIR"
elif [ "$GOOS" = "darwin" ]; then
  CONF_DIR="$HOME/Library/Application Support/Kite"
else
  CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/kite"
fi
CONF_FILE="$CONF_DIR/daemon.env"

# PAYLOAD_DIR：daemon payload 的**用户可写**安装目录（[plan 16 WS-5]，与 selfupdate.PayloadDir 同款规则）。
#   · Linux ：${XDG_DATA_HOME:-$HOME/.local/share}/kite/bin
#   · macOS ：$HOME/Library/Application Support/Kite/bin
# 覆盖：KITE_DAEMON_PAYLOAD_DIR。launcher 据同款默认/同一 env 定位 payload，两端一致。
if [ -n "${KITE_DAEMON_PAYLOAD_DIR:-}" ]; then
  PAYLOAD_DIR="$KITE_DAEMON_PAYLOAD_DIR"
elif [ "$GOOS" = "darwin" ]; then
  PAYLOAD_DIR="$HOME/Library/Application Support/Kite/bin"
else
  PAYLOAD_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/kite/bin"
fi

if [ -z "$RELEASE_BASE" ]; then
  die "relay 未配置 daemon 下载基址（KITE_RELAY_DAEMON_RELEASE_BASE）——联系管理员。"
fi

# 资产名固定不带版本号：kite-<comp>-<os>-<arch>（版本由 Release tag / latest 别名经 URL 路径体现，
# 不进文件名——与 build-daemon.sh / release.yml 同款命名；带版本会让 latest/download 别名永远命不中）。
# asset_url <资产基名> → 回完整下载 URL（按 VERSION pin / latest 分支）。
asset_url() {
  _asset="$1-${GOOS}-${GOARCH}"
  if [ -n "$VERSION" ]; then
    echo "${RELEASE_BASE%/}/download/${VERSION}/${_asset}"
  else
    echo "${RELEASE_BASE%/}/latest/download/${_asset}"
  fi
}
DAEMON_URL="$(asset_url "$BIN_NAME")"
LAUNCHER_URL="$(asset_url "$LAUNCHER_NAME")"

# ── 2. 下载二进制（.new 原子 mv -f 覆盖，重入安全）──
# launcher 装 INSTALL_DIR（可能需一次 sudo）；daemon payload 落 PAYLOAD_DIR（用户家目录、用户态可写，
# **绝不** sudo——daemon 跑用户态、自更新要能覆盖自己；root-owned 则自更新不可用）。配置同样不 sudo。
need_sudo=""
if [ ! -w "$INSTALL_DIR" ]; then
  if [ "$(id -u)" -ne 0 ]; then
    command -v sudo >/dev/null 2>&1 || die "需要 root 写 ${INSTALL_DIR}，且无 sudo——请以 root 运行或设 KITE_INSTALL_DIR 指向可写目录。"
    need_sudo="sudo"
  fi
fi

dl() {
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$2" "$1"
  else
    die "缺 curl / wget，无法下载。"
  fi
}

# 2a. launcher → INSTALL_DIR（[plan 16 WS-5] 防变砖兜底 supervisor，装一次、永不自更新）。
ltmp="$(mktemp)"
log "下载 launcher：$LAUNCHER_URL"
dl "$LAUNCHER_URL" "$ltmp" || die "下载失败：$LAUNCHER_URL"
chmod +x "$ltmp"
log "安装 launcher 到 $INSTALL_DIR/$LAUNCHER_NAME"
$need_sudo mkdir -p "$INSTALL_DIR"
$need_sudo mv -f "$ltmp" "$INSTALL_DIR/$LAUNCHER_NAME"
LAUNCHER_PATH="$INSTALL_DIR/$LAUNCHER_NAME"

# 2b. daemon payload → PAYLOAD_DIR（用户可写，自更新对象；**不 sudo**）。
dtmp="$(mktemp)"
log "下载 daemon：$DAEMON_URL"
dl "$DAEMON_URL" "$dtmp" || die "下载失败：$DAEMON_URL"
chmod +x "$dtmp"
log "安装 daemon payload 到 $PAYLOAD_DIR/${BIN_NAME}（用户可写，自更新对象）"
mkdir -p "$PAYLOAD_DIR"
mv -f "$dtmp" "$PAYLOAD_DIR/$BIN_NAME"
DAEMON_BIN="$PAYLOAD_DIR/$BIN_NAME"

# 现存机迁移（[plan 16 WS-5]）：旧版把 daemon 装在 INSTALL_DIR（root-owned），且 supervisor ExecStart
# 直指它。新布局 ExecStart=launcher、daemon 在 PAYLOAD_DIR。旧 INSTALL_DIR/kite-daemon 留着无害（不再被
# 引用），但为免版本困惑、best-effort 清掉（失败不致命——下方 service install 会用新 ExecStart 覆盖）。
# 只删**真旧二进制**，不删下方我们建的软链（`! -L`）——否则每次重装先删掉刚要建的软链、白折腾。
if [ -e "$INSTALL_DIR/$BIN_NAME" ] && [ ! -L "$INSTALL_DIR/$BIN_NAME" ]; then
  log "迁移：移除旧位 $INSTALL_DIR/${BIN_NAME}（改由 PAYLOAD_DIR 管理 + launcher supervise）"
  $need_sudo rm -f "$INSTALL_DIR/$BIN_NAME" || true
fi

# kite-daemon 入 PATH（[plan 07 档 E]，用户 2026-07-04「禁给用户不可用命令」）：INSTALL_DIR（默认
# /usr/local/bin）在 PATH，软链指向 PAYLOAD_DIR 的 daemon → 用户可直接敲 `kite-daemon service uninstall`
# / `kite-daemon logout`（否则 payload 不在任何 PATH，报 command not found——真机踩过）。自更新在 payload
# 原地原子替换、软链恒有效。放在迁移 rm 之后（否则被当旧位删掉）。失败仅提示、不致命（不阻断安装）。
if $need_sudo ln -sf "$DAEMON_BIN" "$INSTALL_DIR/$BIN_NAME" 2>/dev/null; then
  log "已建 PATH 软链 $INSTALL_DIR/$BIN_NAME → ${DAEMON_BIN}（可直接敲 kite-daemon 子命令）"
else
  log "提示：未能在 $INSTALL_DIR 建 kite-daemon 软链；如需直接敲 kite-daemon，可手动：ln -sf \"$DAEMON_BIN\" \"$INSTALL_DIR/$BIN_NAME\""
fi

# ── 3. 写配置（用户态、**不 sudo**、mode 600 user-owned，relay 地址来自模板）──
#   写到用户家目录的 conf，故无需 sudo；user-owned 600 让登录用户态 daemon 既能读 conf、
#   又能往同目录写 device-auth.txt。绝不用 $need_sudo 写这里。
log "写配置 $CONF_FILE"
mkdir -p "$CONF_DIR"
conf_tmp="$(mktemp)"
{
  echo "# 云鸢 daemon 配置（install.sh 生成；token 不在此文件——走设备授权存 keyring）。"
  echo "KITE_DAEMON_RELAY_URL=$RELAY_WS"
  [ -n "$ACCOUNT_BASE" ] && echo "KITE_DAEMON_ACCOUNT_BASE_URL=$ACCOUNT_BASE"
  # [plan08 WS-2] 默认空：不再烤死探测/允许白名单。探测恒为 kite 支持全集（含 copilot/qoder/trae），
  # 允许范围(A)由网页「可驱动智能体」控制。旧默认 claude,codex,opencode 会把本机锁死成这三个、
  # 装了 copilot 也探不到/起不了。仅高安部署可显式设 KITE_DAEMON_AGENTS 收窄。
  echo "KITE_DAEMON_AGENTS=${KITE_DAEMON_AGENTS:-}"
  # 合并写入（[v0.5.3 plan09] 重装不覆写用户自定义行）：既有 conf 里非管理键（如 HTTPS_PROXY /
  # KITE_DAEMON_USE_SYSTEM_PROXY / ALLOW_SHELL）原样保留——走代理出网的机器重装后不再失联。
  # AUTOSTART 是已退役的旧受管键：继续列在过滤式里，只为重装时清掉存量 `=true`，绝不保留回来。
  if [ -f "$CONF_FILE" ]; then
    preserved="$(grep -vE '^KITE_DAEMON_(RELAY_URL|ACCOUNT_BASE_URL|AGENTS|AUTOSTART)=' "$CONF_FILE" \
      | grep -v '^# 云鸢 daemon 配置' | grep -v '^# ── 以下为用户自定义行' \
      | grep -vE '^[[:space:]]*$' || true)"
    if [ -n "$preserved" ]; then
      echo "# ── 以下为用户自定义行（重装保留）──"
      printf '%s\n' "$preserved"
    fi
  fi
} > "$conf_tmp"
install -m 600 "$conf_tmp" "$CONF_FILE"
rm -f "$conf_tmp"

# ── 4. 装用户态服务（systemd --user / launchd LaunchAgent），用 daemon 内置 service 子命令 ──
#   plan 05 WS-2：daemon 改**登录用户态**运行（继承用户 PATH/profile/keyring/桌面），
#   故 service install 必须以**当前登录用户**身份跑（不能 sudo——sudo 会落到 root 的 user bus）。
#   只有下载二进制到 /usr/local/bin 可能需 sudo；写用户态 conf / 注册自启服务都不需、也不该用 sudo。
log "注册并启动用户态自启服务（幂等覆盖）"
# [v0.5.9 plan04] 清陈旧授权提示文件（上轮未完成授权的过期机器码）——服务重启后 daemon 需要授权时
# 会写**新**的 device-auth.txt，下方直显轮询据此只可能读到本轮的新码。
AUTH_FILE="$CONF_DIR/device-auth.txt"
rm -f "$AUTH_FILE" 2>/dev/null || true
# service install 子命令读 $CONF_FILE 注册 systemd --user unit / ~/Library/LaunchAgents plist 并 enable+restart。
# [plan 16 WS-5]：注入 KITE_LAUNCHER_PATH + KITE_DAEMON_PAYLOAD_DIR → daemon 把 supervisor ExecStart
# 指向 launcher（永不自更新、负责回滚），且 launcher 与 install 用同一 payload 目录。
env KITE_DAEMON_CONF_FILE="$CONF_FILE" \
    KITE_LAUNCHER_PATH="$LAUNCHER_PATH" \
    KITE_DAEMON_PAYLOAD_DIR="$PAYLOAD_DIR" \
    "$DAEMON_BIN" service install || \
  die "服务注册失败——可手动跑：env KITE_LAUNCHER_PATH=$LAUNCHER_PATH KITE_DAEMON_PAYLOAD_DIR=$PAYLOAD_DIR $DAEMON_BIN service install"

# ── 5. 探测并引导安装 agent CLI（只展示官网当前原生安装入口，不静默执行第三方脚本）──
probe_agents() {
  log "探测本机编码 agent CLI"

  # claude：探到即用；未探到则展示官网原生安装脚本，无 Node/npm 前置。
  if command -v claude >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m claude 已安装：%s\n' "$(command -v claude)" >&2
  else
    printf '  \033[33m·\033[0m 未探测到 claude（Claude Code CLI）。\n' >&2
    printf '    官方安装：\033[36mcurl -fsSL https://claude.ai/install.sh | bash\033[0m\n' >&2
  fi

  # codex / opencode：官方安装命令属 plan §7 调研项，未定前不硬写安装命令（红线：不确定不硬写）。
  # TODO(plan 05 §7 调研): 确认 codex / opencode 官方安装来源 + node 依赖后，补探测+可选安装引导。
  # 用 if（非 `cmd && printf`）：set -e 下 `A && B` 若 A 失败会让整条语句非零 → 误中断脚本。
  if command -v codex >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m codex 已安装：%s\n' "$(command -v codex)" >&2
  fi
  if command -v opencode >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m opencode 已安装：%s\n' "$(command -v opencode)" >&2
  fi
  # [v0.6.11 plan02] Kimi Code 官方原生安装器；只展示，不由 Kite 静默执行第三方脚本。
  if command -v kimi >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m kimi 已安装：%s\n' "$(command -v kimi)" >&2
  else
    printf '  \033[33m·\033[0m 未探测到 kimi（Kimi Code）。官方安装：\n' >&2
    printf '      \033[36mcurl -fsSL https://code.kimi.com/kimi-code/install.sh | bash\033[0m\n' >&2
  fi
  if command -v codebuddy >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m codebuddy 已安装：%s\n' "$(command -v codebuddy)" >&2
  elif command -v cbc >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m codebuddy 已安装（cbc）：%s\n' "$(command -v cbc)" >&2
  else
    printf '  \033[33m·\033[0m 未探测到 codebuddy（CodeBuddy Code）。官方安装：\n' >&2
    printf '      \033[36mcurl -fsSL https://www.codebuddy.cn/cli/install.sh | bash\033[0m\n' >&2
  fi
  # [v0.9.0] DeepSeek Harness（bin `dsh`）。本仓约定「官网有原生安装器时不附带 npm」，这家是
  # 显式例外：官方当前只发 npm 包 @deepseek-ai/dsh（2026-08-13 核实 0.1.0-rc.6，无原生安装脚本、
  # 无 brew/winget 入口），故这里只能给 npm 命令；官方一旦发原生安装器就换回原生入口。
  if command -v dsh >/dev/null 2>&1; then
    printf '  \033[32m✓\033[0m dsh 已安装：%s\n' "$(command -v dsh)" >&2
  else
    printf '  \033[33m·\033[0m 未探测到 dsh（DeepSeek Harness）。官方安装（需先有 Node.js）：\n' >&2
    printf '      \033[36mnpm i -g @deepseek-ai/dsh\033[0m\n' >&2
  fi
}
probe_agents

# ── 6. 设备授权直显（[v0.5.9 plan04 需求②]，与 install.ps1 第 6 步同款）——
#    daemon 需要授权时把机器码 + 网址写 $AUTH_FILE（deviceauth.go），此前只能 journalctl 才看到
#    （无界面 Linux 真机反馈：「非要我执行命令，这对用户来讲太复杂了」）。这里轮询直印到安装终端：
#    · 文件出现 → 打印机器码框，继续等授权完成（授权 + 入册成功后 daemon 会删除该文件）；
#    · 窗口内未出现 → 本机多半已授权过（重装 / token 仍有效），如实说明后收尾。
log "检查是否需要设备授权（最多等 ~20s）…"
auth_shown=""
i=0
while [ $i -lt 20 ]; do
  if [ -f "$AUTH_FILE" ]; then auth_shown=1; break; fi
  i=$((i + 1)); sleep 1
done
if [ -n "$auth_shown" ]; then
  printf '\n  \033[33m┌─ 云鸢设备授权 ────────────────────────────────\033[0m\n' >&2
  while IFS= read -r _line; do
    printf '  \033[33m│\033[0m %s\n' "$_line" >&2
  done < "$AUTH_FILE"
  printf '  \033[33m└───────────────────────────────────────────────\033[0m\n\n' >&2
  log "在任意设备打开上方网址输码授权；本终端将等待授权完成（最多 ~3 分钟，Ctrl-C 退出不影响后台授权）…"
  j=0
  while [ $j -lt 180 ]; do
    [ ! -f "$AUTH_FILE" ] && break
    j=$((j + 1)); sleep 1
  done
  if [ ! -f "$AUTH_FILE" ]; then
    printf '\n\033[32m✓ 授权完成，机器已接入你的灵澜账号。\033[0m\n' >&2
  else
    printf '\n\033[33m· 尚未完成授权（不影响：随时可授权）。\033[0m机器码见 %s，或 journalctl --user -u kite-daemon -f\n' "$AUTH_FILE" >&2
  fi
else
  log "未出现授权提示：本机此前已授权过（服务已启动、将自动连回）。"
fi

printf '\n\033[32m✓ 云鸢 daemon 已安装并启动。\033[0m\n' >&2
cat >&2 <<EOF

  · 配置：$CONF_FILE
  · 服务：登录用户态自启（systemd --user / launchd LaunchAgent），崩溃自重启

如需再次查看授权提示 / 运行状态：
  · 授权文件：${AUTH_FILE}（需要授权时才存在）
  · Linux ：journalctl --user -u kite-daemon -f
  · macOS ：log stream --predicate 'process == "kite-daemon"'
EOF
