#!/bin/sh
# Ycodium runtime 安装器（POSIX sh，目标机器可能只有 dash，禁止 bashism）。
#
# 语义对齐 packages/shared/src/runtime.ts 与 CONTRACT 里的分发契约：
#   - target 拼写、on-disk 文件/目录名、latest.json 与 manifest.json 的形状都是
#     那份契约的唯一事实源；本文件运行在没有这个仓库、没有 node_modules 的远端
#     机器上，没法 import 它，所以下面把需要的字面量原样抄一份，靠
#     scripts/install-runtime.test.ts 里的交叉校验测试防止两边漂移。
#   - latest.json / <版本>/manifest.json 假设是"每字段独占一行"的美化 JSON
#     （发布流程保证）；本脚本的行级 sed 解析不支持压成一行的 JSON。
#
# 安装事务：只升不降判定 → 下载/校验 → 解压到 versions/ 下的 staging 目录 →
# 校验清单 → smoke test → 写 sentinel → 原子 mv 到 versions/<版本> → 发布
# bin/ycodium → 写 install-origin.json → 写 current.json → 最后清理保留窗口外的
# 旧版本。任何一步失败都直接 exit，已安装的旧版本与 current.json 保持原样；清理
# 排在 current.json 之后且只 warn，绝不危及已经提交的安装。
#
# 只升不降是这里的产品约束，不是调用方的自觉（packages/ssh 的 launch 脚本按这
# 条约束决定能不能停一台跑着旧版本的 Host）：目标版本 ≤ 已装版本、或版本号形状
# 比不出大小时一律跳过。平级重装与降级只有显式 --force 才允许，且 --force 也不
# 会删掉正在被跑的版本目录。
set -eu
umask 077

# 仅用于本文件自己的测试 harness：设为 1 时跳过下面的自动执行，方便测试脚本
# `.` 进来后直接调用内部函数。不是产品契约的一部分。
YCODIUM_INSTALL_TEST_MODE=${YCODIUM_INSTALL_TEST_MODE:-0}

RUNTIME_TARGETS="linux-x64 linux-arm64 darwin-x64 darwin-arm64 win32-x64"
RUNTIME_VERSIONS_DIR_NAME=versions
RUNTIME_MANIFEST_FILE_NAME=runtime-manifest.json
RUNTIME_CURRENT_FILE_NAME=current.json
RUNTIME_ORIGIN_FILE_NAME=install-origin.json
RUNTIME_SENTINEL_FILE_NAME=.install-complete
RUNTIME_KEEP_VERSIONS=3
# 下载的放弃条件。没有它们,curl 会一直挂着等,唯一的兜底是调用方(桌面走 SSH 时
# 是 15 分钟)的整体超时——网络一卡就必然烧满那段时间,还只能报一句笼统的失败。
RUNTIME_CONNECT_TIMEOUT_SECONDS=20
# 索引是几百字节的小对象,正常一秒就完,给它一个干脆的硬上限。
RUNTIME_INDEX_MAX_SECONDS=60
# 归档几十上百 MB,不能用固定时限:慢链路上正常下载本来就要很久。判据改成"几乎
# 不动"——连续 RUNTIME_STALL_SECONDS 秒低于 RUNTIME_STALL_BYTES_PER_SECOND
# 才算停滞。5 KB/s 的破网络照样下得完,只有真卡死的会被判掉。
RUNTIME_STALL_BYTES_PER_SECOND=1024
RUNTIME_STALL_SECONDS=60
DEFAULT_UPDATE_ROOT="https://dl.vpnla.pro/runtime"

archive_path=
requested_version=
origin=download
force_install=0
# 进度开关走环境变量而不是命令行参数：桌面可能比 CDN 上这份脚本新，多传一个它
# 不认得的参数会让 parse_args 直接 fail，把装机整条路堵死；不认得的环境变量则
# 是无害的。
report_progress=0
progress_pid=
HASH_TOOL=
tmp_dir=
staging_dir=

fail() {
  printf '%s\n' "$1" >&2
  exit 1
}

cleanup() {
  stop_download_progress
  [ -n "$tmp_dir" ] && rm -rf "$tmp_dir"
  [ -n "$staging_dir" ] && rm -rf "$staging_dir"
  return 0
}

# 下载进度只在 YCODIUM_INSTALL_PROGRESS=1 时打，而且带固定前缀：调用方按前缀
# 过滤，剩下的输出保持原样。`curl … | sh` 的人不需要每秒一行刷屏，默认关闭。
#
# 计的是落盘字节而不是 curl 自己的进度条：curl 的进度写在 stderr、用 \r 刷新，
# 解析它要跨平台对付一堆终端控制码，而归档本来就在往一个文件里 tee。
PROGRESS_PREFIX="ycodium-progress:"

emit_download_progress() {
  edp_file=$1
  edp_total=$2
  if [ -f "$edp_file" ]; then
    edp_size=$(wc -c < "$edp_file" 2>/dev/null | tr -d ' ')
  else
    edp_size=0
  fi
  [ -n "$edp_size" ] || edp_size=0
  printf '%s download %s %s\n' "$PROGRESS_PREFIX" "$edp_size" "$edp_total"
}

start_download_progress() {
  [ "$report_progress" = "1" ] || return 0
  sdp_file=$1
  sdp_total=$2
  (
    sdp_last=
    while :; do
      sdp_line=$(emit_download_progress "$sdp_file" "$sdp_total")
      if [ "$sdp_line" != "$sdp_last" ]; then
        printf '%s\n' "$sdp_line"
        sdp_last=$sdp_line
      fi
      sleep 1
    done
  ) &
  progress_pid=$!
}

stop_download_progress() {
  [ -n "$progress_pid" ] || return 0
  kill "$progress_pid" >/dev/null 2>&1 || true
  wait "$progress_pid" 2>/dev/null || true
  progress_pid=
  return 0
}

parse_args() {
  while [ $# -gt 0 ]; do
    case $1 in
      --archive)
        [ $# -ge 2 ] || fail "--archive requires a value."
        archive_path=$2
        shift 2
        ;;
      --version)
        [ $# -ge 2 ] || fail "--version requires a value."
        requested_version=$2
        shift 2
        ;;
      --origin)
        [ $# -ge 2 ] || fail "--origin requires a value."
        origin=$2
        shift 2
        ;;
      --force)
        force_install=1
        shift
        ;;
      *)
        fail "Unknown argument: $1"
        ;;
    esac
  done
}

# 只允许 [A-Za-z0-9._-]，且不为空/不为 . 或 ..。runtime.ts 的 VERSION_PATTERN
# 本身不禁止 ".."——这里额外拒绝，是因为本脚本把这个值当成单一路径段拼进
# versions/<版本>，不能让它变成穿越序列。对 --version 参数与解出的
# resolved_version（不管来自 latest.json 还是归档自带的 manifest）都要过这关。
validate_version_string() {
  candidate=$1
  case $candidate in
    "" | . | ..)
      fail "Version is invalid: $candidate"
      ;;
  esac
  case $candidate in
    *[!A-Za-z0-9._-]*)
      fail "Version contains characters outside the allowed set: $candidate"
      ;;
  esac
}

# 逐段校验相对路径：只允许 [A-Za-z0-9._-] 与 /，且每段首字符不得是 .（一并拒绝
# . 和 .. 穿越）。语义对齐 runtime.ts 的 PATH_SAFE_SEGMENTS_PATTERN，用于
# latest.json 的 url 字段与 runtime-manifest.json 的 node/entry/launcher 字段——
# 校验通过之后，跟一个已知安全的根目录做字符串拼接才是安全的。
validate_relative_path() {
  candidate=$1
  case $candidate in
    "" | /* | *'://'*)
      fail "Path is not a safe relative path: $candidate"
      ;;
  esac
  case $candidate in
    *[!A-Za-z0-9._/-]*)
      fail "Path contains characters outside the allowed set: $candidate"
      ;;
  esac
  remainder=$candidate
  while [ -n "$remainder" ]; do
    case $remainder in
      */*)
        segment=${remainder%%/*}
        remainder=${remainder#*/}
        ;;
      *)
        segment=$remainder
        remainder=
        ;;
    esac
    case $segment in
      "" | .*)
        fail "Path has an unsafe segment: $candidate"
        ;;
    esac
  done
}

# 把 uname -s / uname -m 拼成契约里唯一认可的 target 拼写；认不出就 fail closed。
detect_target() {
  sysname=$1
  machine=$2
  case $sysname in
    Linux | linux) os=linux ;;
    Darwin | darwin) os=darwin ;;
    *) fail "Ycodium runtime installer does not support this OS: $sysname" ;;
  esac
  case $machine in
    x86_64 | amd64 | AMD64) arch=x64 ;;
    aarch64 | arm64 | ARM64) arch=arm64 ;;
    *) fail "Ycodium runtime installer does not support this CPU architecture: $machine" ;;
  esac
  candidate="$os-$arch"
  for known in $RUNTIME_TARGETS; do
    [ "$known" = "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
  done
  fail "Ycodium runtime installer does not support $candidate."
}

# 只有 YCODIUM_RUNTIME_ROOT 是绝对路径时才生效，否则落回 $HOME/.ycodium/runtime。
resolve_install_root() {
  candidate=${YCODIUM_RUNTIME_ROOT:-}
  case $candidate in
    /*) printf '%s\n' "$candidate" ;;
    *) printf '%s/.ycodium/runtime\n' "$HOME" ;;
  esac
}

select_hash_tool() {
  if command -v sha256sum >/dev/null 2>&1; then
    HASH_TOOL=sha256sum
  elif command -v shasum >/dev/null 2>&1; then
    HASH_TOOL=shasum
  elif command -v openssl >/dev/null 2>&1; then
    HASH_TOOL=openssl
  else
    fail "No SHA-256 tool found (looked for sha256sum, shasum, openssl)."
  fi
}

# 从 stdin 读字节，输出 64 位小写 hex 摘要，不带文件名或多余空白。
compute_sha256() {
  case $HASH_TOOL in
    sha256sum) sha256sum | awk '{print $1}' ;;
    shasum) shasum -a 256 | awk '{print $1}' ;;
    openssl) openssl dgst -sha256 -r | awk '{print $1}' ;;
  esac
}

# 把 JSON 规整成"每个 { 或 , 后面都换行"，兼容压缩成一行发布的 JSON——
# latest.json/manifest.json 里所有字段值都只含 [A-Za-z0-9._/-]，不会出现逗号
# 或花括号，这个改写不会动到任何字段值本身。
normalize_json_lines() {
  sed 's/[,{]/&\
/g'
}

# 从 stdin 读 JSON 文本取字符串字段值。字段值本身不含引号——
# latest.json/manifest.json 里出现的所有字段都满足。
json_string_field() {
  normalize_json_lines | sed -n "s/^[[:space:]]*\"$1\":[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -n 1
}

# 同上，但取不带引号的数字字段（目前只有 size 用得到）。
json_number_field() {
  normalize_json_lines | sed -n "s/^[[:space:]]*\"$1\":[[:space:]]*\([0-9][0-9]*\).*/\1/p" | head -n 1
}

# 抽取 targets.<target> 对象所在的行范围，供上面两个字段提取函数在其内部按名
# 取值——不依赖字段在 JSON 里的先后顺序。
json_target_block() {
  normalize_json_lines | sed -n "/\"$1\":[[:space:]]*{/,/}/p"
}

# latest.json / manifest.json 里的 url 字段解析后与 update root 拼接前的同源
# 校验。相对路径已经过 validate_relative_path（不含 .. / 不是绝对路径 / 不含
# scheme），加上 update root 本身必须是 https，拼接结果天然同源、天然落在
# root 前缀之下，等价于 runtime.ts 里 resolveRuntimeArtifactUrl 的效果。
validate_update_root() {
  case $1 in
    https://*) ;;
    *) fail "Runtime update root must be https: $1" ;;
  esac
}

# 只做路径穿越/绝对路径防护，不做符号链接类型探测——不同 tar 实现的
# -tvzf 输出格式差异较大，保持这一步简单可靠更重要。
# 打包惯用 `tar -czf x -C dir .`，会带一条代表归档根目录本身的 "./" 条目，
# 归一化后是空字符串——这是无害的，只拒绝真正的穿越/绝对路径。
validate_archive_entries() {
  archive_to_check=$1
  tar -tzf "$archive_to_check" 2>/dev/null | {
    unsafe=0
    while IFS= read -r raw_entry; do
      entry=${raw_entry#./}
      entry=${entry%/}
      [ -z "$entry" ] && continue
      case $entry in
        /* | .. | ../* | */../* | */.. | *'\'*)
          printf 'Ycodium runtime archive contains an unsafe path: %s\n' "$raw_entry" >&2
          unsafe=1
          break
          ;;
      esac
    done
    [ "$unsafe" = 0 ]
  } || fail "Ycodium runtime archive failed its path-safety check."
}

# 先写临时文件再 rename，保证不会在目标路径留下写了一半的文件。
write_json_atomic() {
  target_path=$1
  content=$2
  tmp_file="$target_path.tmp.$$"
  printf '%s\n' "$content" > "$tmp_file"
  mv "$tmp_file" "$target_path"
}

write_install_origin() {
  write_json_atomic "$1/$RUNTIME_ORIGIN_FILE_NAME" "{\"schemaVersion\":1,\"origin\":\"$2\"}"
}

write_current_pointer() {
  write_json_atomic "$1/$RUNTIME_CURRENT_FILE_NAME" "{\"schemaVersion\":1,\"version\":\"$2\"}"
}

read_current_version() {
  current_json="$1/$RUNTIME_CURRENT_FILE_NAME"
  if [ -f "$current_json" ]; then
    json_string_field version < "$current_json"
  fi
}

# current.json 指向的版本必须真的能用，才有资格当"只升不降"的基准：版本号形状
# 不合法、或那个版本目录没有 manifest，都当成没装——否则一条坏指针会把真正的
# 安装永远挡在门外。
read_installed_baseline_version() {
  baseline_root=$1
  baseline_version=$(read_current_version "$baseline_root")
  case $baseline_version in
    "" | . | .. | *[!A-Za-z0-9._-]*)
      return 0
      ;;
  esac
  if [ -f "$baseline_root/$RUNTIME_VERSIONS_DIR_NAME/$baseline_version/$RUNTIME_MANIFEST_FILE_NAME" ]; then
    printf '%s\n' "$baseline_version"
  fi
}

# 语义对齐 packages/shared/src/semver.ts 的 compareSemverVersions：先比
# major/minor/patch 数字，再按 semver 规则比 prerelease（带 prerelease 的小于
# 不带的；逐段比较，纯数字段按数值比且排在非数字段之前；段少的更小）。
# 输出 -1 / 0 / 1。任一侧不是 major[.minor[.patch]] 形状就返回状态 1，由调用方
# 按"判断不了"处理——这里不做字符串兜底比较，猜错的方向是静默降级。
# 非数字 prerelease 段用 shell 的字节序比较，对齐 TS 侧的 localeCompare 只在
# 纯 ASCII 标识符上成立，发布号只用 rc/beta 这类 ASCII 词。
compare_semver() {
  cs_left_main=${1%%-*}
  cs_right_main=${2%%-*}
  case $1 in
    *-*) cs_left_pre=${1#*-} ;;
    *) cs_left_pre= ;;
  esac
  case $2 in
    *-*) cs_right_pre=${2#*-} ;;
    *) cs_right_pre= ;;
  esac
  if [ -n "$(printf '%s' "$cs_left_main" | cut -d. -f4-)" ]; then
    return 1
  fi
  if [ -n "$(printf '%s' "$cs_right_main" | cut -d. -f4-)" ]; then
    return 1
  fi
  cs_index=1
  while [ "$cs_index" -le 3 ]; do
    cs_left_part=$(printf '%s' "$cs_left_main" | cut -d. -f"$cs_index")
    cs_right_part=$(printf '%s' "$cs_right_main" | cut -d. -f"$cs_index")
    [ -n "$cs_left_part" ] || cs_left_part=0
    [ -n "$cs_right_part" ] || cs_right_part=0
    case $cs_left_part in *[!0-9]*) return 1 ;; esac
    case $cs_right_part in *[!0-9]*) return 1 ;; esac
    if [ "$cs_left_part" -gt "$cs_right_part" ]; then
      printf '%s\n' "1"
      return 0
    fi
    if [ "$cs_left_part" -lt "$cs_right_part" ]; then
      printf '%s\n' "-1"
      return 0
    fi
    cs_index=$((cs_index + 1))
  done
  if [ -z "$cs_left_pre" ] && [ -z "$cs_right_pre" ]; then
    printf '%s\n' "0"
    return 0
  fi
  if [ -z "$cs_left_pre" ]; then
    printf '%s\n' "1"
    return 0
  fi
  if [ -z "$cs_right_pre" ]; then
    printf '%s\n' "-1"
    return 0
  fi
  cs_left_count=$(printf '%s' "$cs_left_pre" | awk -F. '{print NF}')
  cs_right_count=$(printf '%s' "$cs_right_pre" | awk -F. '{print NF}')
  cs_index=1
  while [ "$cs_index" -le "$cs_left_count" ] && [ "$cs_index" -le "$cs_right_count" ]; do
    cs_left_part=$(printf '%s' "$cs_left_pre" | cut -d. -f"$cs_index")
    cs_right_part=$(printf '%s' "$cs_right_pre" | cut -d. -f"$cs_index")
    cs_left_numeric=0
    cs_right_numeric=0
    case $cs_left_part in "" | *[!0-9]*) ;; *) cs_left_numeric=1 ;; esac
    case $cs_right_part in "" | *[!0-9]*) ;; *) cs_right_numeric=1 ;; esac
    if [ "$cs_left_numeric" = 1 ] && [ "$cs_right_numeric" = 1 ]; then
      if [ "$cs_left_part" -gt "$cs_right_part" ]; then
        printf '%s\n' "1"
        return 0
      fi
      if [ "$cs_left_part" -lt "$cs_right_part" ]; then
        printf '%s\n' "-1"
        return 0
      fi
    elif [ "$cs_left_numeric" = 1 ]; then
      printf '%s\n' "-1"
      return 0
    elif [ "$cs_right_numeric" = 1 ]; then
      printf '%s\n' "1"
      return 0
    else
      if [ "$cs_left_part" \> "$cs_right_part" ]; then
        printf '%s\n' "1"
        return 0
      fi
      if [ "$cs_left_part" \< "$cs_right_part" ]; then
        printf '%s\n' "-1"
        return 0
      fi
    fi
    cs_index=$((cs_index + 1))
  done
  if [ "$cs_left_count" -gt "$cs_right_count" ]; then
    printf '%s\n' "1"
    return 0
  fi
  if [ "$cs_left_count" -lt "$cs_right_count" ]; then
    printf '%s\n' "-1"
    return 0
  fi
  printf '%s\n' "0"
}

# 只升不降是这个安装器的产品约束，不是调用方的自觉：目标版本 ≤ 已装版本一律
# 跳过，比不出大小（版本号不是 semver 形状）也跳过。平级重装与降级只有显式
# --force 才允许。
should_install_version() {
  siv_target=$1
  siv_current=$2
  if [ -z "$siv_current" ]; then
    return 0
  fi
  if [ "$force_install" = 1 ]; then
    return 0
  fi
  if siv_comparison=$(compare_semver "$siv_target" "$siv_current"); then
    if [ "$siv_comparison" = "1" ]; then
      return 0
    fi
    return 1
  fi
  return 1
}

describe_install_skip() {
  if [ "$1" = "$2" ]; then
    echo "Ycodium runtime $2 is already current."
  else
    echo "Ycodium runtime $2 is installed and newer than or equal to $1; keeping it. Pass --force to install $1 anyway."
  fi
}

# 删除一个已存在的版本目录之前的保护：那个目录里的程序如果正在跑，rm -rf 就是
# 打断一台正在服务的 Host。判定形状与 packages/ssh 的进程身份比对一致——只看
# argv[0]/argv[1] 是否落在该目录之下。既读不到 /proc 也没有 ps 时返回"在用"：
# 判断不了就不删。
version_dir_in_use() {
  vdiu_prefix="${1%/}/"
  if [ -d /proc ]; then
    for vdiu_cmdline in /proc/[0-9]*/cmdline; do
      [ -r "$vdiu_cmdline" ] || continue
      vdiu_rendered=$(tr '\0' ' ' < "$vdiu_cmdline" 2>/dev/null) || continue
      set -f
      # shellcheck disable=SC2086
      set -- $vdiu_rendered
      set +f
      case ${1:-} in "$vdiu_prefix"*) return 0 ;; esac
      case ${2:-} in "$vdiu_prefix"*) return 0 ;; esac
    done
    return 1
  fi
  command -v ps >/dev/null 2>&1 || return 0
  vdiu_hit=$(
    ps -A -o args= 2>/dev/null | {
      vdiu_found=
      set -f
      while IFS= read -r vdiu_line; do
        # shellcheck disable=SC2086
        set -- $vdiu_line
        case ${1:-} in "$vdiu_prefix"*) vdiu_found=1; break ;; esac
        case ${2:-} in "$vdiu_prefix"*) vdiu_found=1; break ;; esac
      done
      set +f
      printf '%s' "$vdiu_found"
    }
  )
  [ -n "$vdiu_hit" ]
}

# 把 0.2.1313 变成定长零填充 key，纯字典序即数值序；sort -V 是 GNU 扩展，远端
# 可能没有。仅供 prune 排序用——真正的版本先后判定走 compare_semver，它才认
# prerelease 语义。
version_sort_key() {
  vsk_key=""
  vsk_rest=$1
  while [ -n "$vsk_rest" ]; do
    case $vsk_rest in
      *.*) vsk_seg=${vsk_rest%%.*}; vsk_rest=${vsk_rest#*.} ;;
      *) vsk_seg=$vsk_rest; vsk_rest="" ;;
    esac
    # 先剥前导零：printf %d 把 "08" 当八进制会直接报错。非数字段（预发布后缀
    # 之类）归零，排在同位的数字段之前。
    while :; do
      case $vsk_seg in 0?*) vsk_seg=${vsk_seg#0} ;; *) break ;; esac
    done
    case $vsk_seg in "" | *[!0-9]*) vsk_seg=0 ;; esac
    vsk_key="$vsk_key$(printf '%010d' "$vsk_seg")."
  done
  printf '%s' "$vsk_key"
}

# $1 严格新于 $2 时返回 0。优先问 compare_semver——它认 prerelease，
# 0.0.34 必须新于 0.0.34-beta.1，而零填充 key 会把这两个算成相等。只有
# compare_semver 判不出形状（返回 1）时才退回 key 的字典序比较。
version_is_newer() {
  if vin_comparison=$(compare_semver "$1" "$2"); then
    [ "$vin_comparison" = "1" ]
    return
  fi
  vin_a=$(version_sort_key "$1")
  vin_b=$(version_sort_key "$2")
  [ "$vin_a" != "$vin_b" ] \
    && [ "$(printf '%s\n%s\n' "$vin_a" "$vin_b" | LC_ALL=C sort | head -n 1)" = "$vin_b" ]
}

# 列出 versions/ 下当前有活进程在跑的目录名，每行一个。必须全量扫描 /proc——
# 只取前若干个 pid 会漏掉正在服务的进程；扫描途中进程退出属正常，stderr 丢弃。
# 没有 /proc 的（macOS）退回 ps。用 awk 的 index() 做定长前缀匹配而不是正则，
# 安装根路径里的 . 之类字符不会被当成元字符。
list_in_use_version_dirs() {
  liu_versions_dir=$1
  if [ -d /proc ]; then
    for liu_proc in /proc/[0-9]*; do
      tr '\0' '\n' < "$liu_proc/cmdline" 2>/dev/null || true
    done
  else
    ps -ww -A -o command= 2>/dev/null || true
  fi | awk -v prefix="$liu_versions_dir/" '
    {
      at = index($0, prefix)
      if (at == 0) next
      name = substr($0, at + length(prefix))
      sub("[/ ].*", "", name)
      if (name != "") print name
    }
  ' | LC_ALL=C sort -u
}

# 只保留最新的 RUNTIME_KEEP_VERSIONS 个版本目录（current 算其中一个）。四类目录
# 永不删：current 指向的、pack-* 前缀（ADR-0014 已退休的 host pack 布局）、. 开头
# 的（staging/sentinel）、以及有活进程正在运行的。
prune_old_versions() {
  pov_root=$1
  pov_current=$2
  pov_dir="$pov_root/$RUNTIME_VERSIONS_DIR_NAME"
  pov_in_use=$(list_in_use_version_dirs "$pov_dir")

  pov_candidates=$(
    for pov_path in "$pov_dir"/*; do
      [ -d "$pov_path" ] || continue
      pov_name=${pov_path##*/}
      case $pov_name in
        "$pov_current" | pack-* | .*) continue ;;
      esac
      if printf '%s\n' "$pov_in_use" | grep -qxF -e "$pov_name"; then continue; fi
      printf '%s %s\n' "$(version_sort_key "$pov_name")" "$pov_name"
    done
  )

  # 降序排完，current 已经占掉保留窗口的一个名额，所以候选里只留 KEEP-1 个：
  # 从第 KEEP 行起全部删掉。
  printf '%s\n' "$pov_candidates" \
    | LC_ALL=C sort -r \
    | tail -n +"$RUNTIME_KEEP_VERSIONS" \
    | while read -r _ pov_name; do
      if [ -n "$pov_name" ]; then rm -rf "$pov_dir/$pov_name"; fi
    done
}

# 把 versions/<版本>/<launcher> 同步为 <root>/bin/ycodium 这个稳定路径。
# 版本号不来自参数猜测，而是回读该版本自己的 manifest，两边永远一致。
publish_stable_bin() {
  publish_root=$1
  publish_version=$2
  publish_version_dir="$publish_root/$RUNTIME_VERSIONS_DIR_NAME/$publish_version"
  publish_manifest="$publish_version_dir/$RUNTIME_MANIFEST_FILE_NAME"
  [ -f "$publish_manifest" ] || fail "Cannot publish the Ycodium launcher: $publish_manifest is missing."
  publish_launcher=$(json_string_field launcher < "$publish_manifest")
  [ -n "$publish_launcher" ] || fail "Cannot publish the Ycodium launcher: runtime-manifest.json has no launcher field."
  validate_relative_path "$publish_launcher"
  publish_launcher_path="$publish_version_dir/$publish_launcher"
  [ -f "$publish_launcher_path" ] || fail "Cannot publish the Ycodium launcher: $publish_launcher_path is missing."
  mkdir -p "$publish_root/bin"
  publish_tmp="$publish_root/bin/.ycodium.tmp.$$"
  cp "$publish_launcher_path" "$publish_tmp"
  chmod 755 "$publish_tmp"
  mv "$publish_tmp" "$publish_root/bin/ycodium"
}

print_path_hint() {
  hint_root=$1
  echo "Run: $hint_root/bin/ycodium <command>"
  case ":${PATH:-}:" in
    *":$hint_root/bin:"*) ;;
    *) echo "Add it to your PATH: export PATH=\"$hint_root/bin:\$PATH\"" ;;
  esac
}

main() {
  parse_args "$@"
  [ "${YCODIUM_INSTALL_PROGRESS:-0}" = "1" ] && report_progress=1
  [ -n "$requested_version" ] && validate_version_string "$requested_version"
  case $origin in
    download | desktop) ;;
    *) fail "--origin must be 'download' or 'desktop', got: $origin" ;;
  esac

  # curl 只有自己去取字节时才需要。`--archive` 的机器往往正是因为够不着网络才
  # 走这条路，在这里拦掉它等于把唯一的退路也堵上。
  [ -n "$archive_path" ] || command -v curl >/dev/null 2>&1 || fail "curl is required."
  command -v tar >/dev/null 2>&1 || fail "tar is required."
  select_hash_tool

  target=$(detect_target "$(uname -s)" "$(uname -m)")
  root=$(resolve_install_root)
  mkdir -p "$root/$RUNTIME_VERSIONS_DIR_NAME"

  tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/ycodium-runtime-install.XXXXXX")
  trap cleanup EXIT INT TERM

  current_version=$(read_installed_baseline_version "$root")
  resolved_version=$requested_version

  if [ -n "$archive_path" ]; then
    [ -f "$archive_path" ] || fail "Archive not found: $archive_path"
    archive_file=$archive_path
  else
    update_root=${YCODIUM_RUNTIME_UPDATE_ROOT:-$DEFAULT_UPDATE_ROOT}
    update_root=${update_root%/}
    validate_update_root "$update_root"

    pointer_file="$tmp_dir/release-index.json"
    if [ -n "$requested_version" ]; then
      pointer_url="$update_root/$requested_version/manifest.json"
    else
      pointer_url="$update_root/latest.json"
    fi
    curl -fsSL --retry 3 \
      --connect-timeout "$RUNTIME_CONNECT_TIMEOUT_SECONDS" \
      --max-time "$RUNTIME_INDEX_MAX_SECONDS" \
      "$pointer_url" -o "$pointer_file" \
      || fail "Failed to fetch the Ycodium runtime release index from $pointer_url."

    resolved_version=$(json_string_field version < "$pointer_file")
    [ -n "$resolved_version" ] || fail "Ycodium runtime release index at $pointer_url has no version."
    if [ -n "$requested_version" ] && [ "$resolved_version" != "$requested_version" ]; then
      fail "Ycodium runtime manifest for $requested_version reports version $resolved_version; refusing to continue."
    fi

    # 只升不降的第一道闸：在下载归档之前就判掉，省掉一次没用的下载。
    if ! should_install_version "$resolved_version" "$current_version"; then
      publish_stable_bin "$root" "$current_version"
      write_install_origin "$root" "$origin"
      describe_install_skip "$resolved_version" "$current_version"
      exit 0
    fi

    target_block=$(json_target_block "$target" < "$pointer_file")
    [ -n "$target_block" ] || fail "Ycodium runtime release index has no entry for target $target."
    relative_url=$(printf '%s\n' "$target_block" | json_string_field url)
    expected_sha256=$(printf '%s\n' "$target_block" | json_string_field sha256)
    expected_size=$(printf '%s\n' "$target_block" | json_number_field size)
    if [ -z "$relative_url" ] || [ -z "$expected_sha256" ] || [ -z "$expected_size" ]; then
      fail "Ycodium runtime release index entry for $target is incomplete."
    fi
    validate_relative_path "$relative_url"
    download_url="$update_root/$relative_url"

    archive_file="$tmp_dir/archive.tar.gz"
    curl_exit_file="$tmp_dir/.curl-exit"
    : > "$archive_file"
    start_download_progress "$archive_file" "$expected_size"
    actual_sha256=$(
      { curl -fsSL --retry 3 \
          --connect-timeout "$RUNTIME_CONNECT_TIMEOUT_SECONDS" \
          --speed-limit "$RUNTIME_STALL_BYTES_PER_SECOND" \
          --speed-time "$RUNTIME_STALL_SECONDS" \
          "$download_url"; printf '%s' "$?" > "$curl_exit_file"; } \
        | tee "$archive_file" \
        | compute_sha256
    )
    stop_download_progress
    curl_status=$(cat "$curl_exit_file")
    [ "$curl_status" = "0" ] || fail "Downloading the Ycodium runtime archive failed (curl exit $curl_status)."
    actual_size=$(wc -c < "$archive_file" | tr -d ' ')
    [ "$actual_size" = "$expected_size" ] \
      || fail "Ycodium runtime archive size mismatch (expected $expected_size, got $actual_size)."
    [ "$actual_sha256" = "$expected_sha256" ] || fail "Ycodium runtime archive checksum mismatch."
  fi

  validate_archive_entries "$archive_file"

  staging_dir="$root/$RUNTIME_VERSIONS_DIR_NAME/.staging-$$"
  rm -rf "$staging_dir"
  mkdir -p "$staging_dir"
  tar -xzf "$archive_file" -C "$staging_dir" || fail "Failed to extract the Ycodium runtime archive."

  manifest_path="$staging_dir/$RUNTIME_MANIFEST_FILE_NAME"
  [ -f "$manifest_path" ] || fail "Extracted Ycodium runtime archive is missing $RUNTIME_MANIFEST_FILE_NAME."
  manifest_version=$(json_string_field version < "$manifest_path")
  manifest_target=$(json_string_field target < "$manifest_path")
  manifest_node_version=$(json_string_field nodeVersion < "$manifest_path")
  manifest_node=$(json_string_field node < "$manifest_path")
  manifest_entry=$(json_string_field entry < "$manifest_path")
  manifest_launcher=$(json_string_field launcher < "$manifest_path")
  if [ -z "$manifest_version" ] || [ -z "$manifest_target" ] || [ -z "$manifest_node_version" ] \
    || [ -z "$manifest_node" ] || [ -z "$manifest_entry" ] || [ -z "$manifest_launcher" ]; then
    fail "Extracted Ycodium runtime manifest is missing a required field."
  fi
  validate_relative_path "$manifest_node"
  validate_relative_path "$manifest_entry"
  validate_relative_path "$manifest_launcher"

  [ "$manifest_target" = "$target" ] \
    || fail "Ycodium runtime manifest target ($manifest_target) does not match this host ($target)."

  if [ -n "$archive_path" ]; then
    if [ -n "$requested_version" ] && [ "$manifest_version" != "$requested_version" ]; then
      fail "Ycodium runtime archive version ($manifest_version) does not match the requested version ($requested_version)."
    fi
    resolved_version=$manifest_version
  else
    [ "$manifest_version" = "$resolved_version" ] \
      || fail "Ycodium runtime manifest version ($manifest_version) does not match the release index ($resolved_version)."
  fi
  # resolved_version 接下来会被当成单一路径段拼进 versions/<版本>；不管它来自
  # latest.json 还是归档自带的 manifest，都要在这里统一把关，不能只信任
  # --version 参数那一次校验。
  validate_version_string "$resolved_version"

  # 归档路径的版本只有解包后才知道，所以只升不降的闸在这里再过一次；下载路径
  # 早在拉包之前就判过了，走到这里的一定是归档安装。拿旧归档去装一台已经更新
  # 的机器必须安全地不做事，而不是把它退回旧版本。
  if ! should_install_version "$resolved_version" "$current_version"; then
    publish_stable_bin "$root" "$current_version"
    write_install_origin "$root" "$origin"
    describe_install_skip "$resolved_version" "$current_version"
    exit 0
  fi

  node_path="$staging_dir/$manifest_node"
  entry_path="$staging_dir/$manifest_entry"
  launcher_path="$staging_dir/$manifest_launcher"
  [ -f "$node_path" ] || fail "Extracted Ycodium runtime is missing its Node binary at $manifest_node."
  [ -f "$entry_path" ] || fail "Extracted Ycodium runtime is missing its server entry at $manifest_entry."
  [ -f "$launcher_path" ] || fail "Extracted Ycodium runtime is missing its launcher at $manifest_launcher."
  chmod 755 "$node_path"
  chmod 755 "$launcher_path"

  node_version_output=$("$node_path" -v 2>/dev/null) || fail "Smoke test failed: the bundled Node binary did not run."
  node_version_output=${node_version_output#v}
  [ "$node_version_output" = "$manifest_node_version" ] \
    || fail "Smoke test failed: bundled Node reports $node_version_output, manifest declares $manifest_node_version."

  entry_version_output=$("$node_path" "$entry_path" --version 2>/dev/null) \
    || fail "Smoke test failed: the server entry did not run."
  case $entry_version_output in
    *"$resolved_version"*) ;;
    *) fail "Smoke test failed: server entry reports '$entry_version_output', expected version $resolved_version." ;;
  esac

  printf '%s\n' "$resolved_version" > "$staging_dir/$RUNTIME_SENTINEL_FILE_NAME"

  final_version_dir="$root/$RUNTIME_VERSIONS_DIR_NAME/$resolved_version"
  # 这个目录已经存在，说明是平级重装或装回一个装过的版本（只有 --force 或
  # 归档同版本能走到这里）。它正在被跑就不能删——那是一台正在服务的 Host。
  if [ -d "$final_version_dir" ]; then
    if version_dir_in_use "$final_version_dir"; then
      fail "Ycodium runtime $resolved_version is running from $final_version_dir right now, so reinstalling it would delete a live server. Stop that server first, then run this again."
    fi
  fi
  rm -rf "$final_version_dir"
  mv "$staging_dir" "$final_version_dir"
  staging_dir=

  publish_stable_bin "$root" "$resolved_version"
  write_install_origin "$root" "$origin"
  write_current_pointer "$root" "$resolved_version"

  # 清理排在 current.json 之后，且失败只 warn：安装事务此刻已经提交，回收磁盘
  # 不该有能力把它弄坏。
  prune_old_versions "$root" "$resolved_version" \
    || echo "Ycodium runtime installed, but pruning old versions failed; they are still on disk." >&2

  echo "Ycodium runtime $resolved_version installed."
  print_path_hint "$root"
}

[ "$YCODIUM_INSTALL_TEST_MODE" = "1" ] || main "$@"
