#!/usr/bin/env bash
set -euo pipefail

PROGRAM_NAME="cc-oneclick"
SCRIPT_VERSION="0.4.2"
DEFAULT_MIRROR_BASE_URL="${CC_ONECLICK_MIRROR_BASE_URL:-https://cc.fdonglawyer.asia}"
DEFAULT_PROVIDER_NAME="DeepSeek V4 (cc-oneclick)"
DEEPSEEK_BASE_URL="https://api.deepseek.com/anthropic"
DEEPSEEK_MAIN_MODEL="deepseek-v4-pro[1m]"
DEEPSEEK_LIGHT_MODEL="deepseek-v4-flash"
DEEPSEEK_BASE_URL_ENV_NAME="ANTHROPIC_BASE_URL"
DEEPSEEK_AUTH_ENV_NAME="ANTHROPIC_AUTH_TOKEN"
DEEPSEEK_MODEL_ENV_NAME="ANTHROPIC_MODEL"
DEEPSEEK_LIGHT_ENV_NAME="ANTHROPIC_SMALL_FAST_MODEL"

DRY_RUN=0
ASSUME_YES=0
NO_LAUNCH=0
DESKTOP_LAUNCHER_CREATED=0
MANIFEST_URL="${CC_ONECLICK_MANIFEST_URL:-}"
MIRROR_BASE_URL="$DEFAULT_MIRROR_BASE_URL"
WORK_DIR=""
MANIFEST_FILE=""

info() {
  printf '\n[信息] %s\n' "$*"
}

warn() {
  printf '\n[提醒] %s\n' "$*" >&2
}

die() {
  printf '\n[错误] %s\n' "$*" >&2
  exit 1
}

read_user_input() {
  local __var_name="$1"

  if [ -r /dev/tty ]; then
    IFS= read -r "$__var_name" </dev/tty
  else
    IFS= read -r "$__var_name"
  fi
}

read_secret_input() {
  local __var_name="$1"
  local value="" char status=0

  if [ -r /dev/tty ]; then
    while IFS= read -r -s -n 1 char </dev/tty; do
      case "$char" in
        ""|$'\n'|$'\r')
          break
          ;;
        $'\177'|$'\b')
          if [ -n "$value" ]; then
            value="${value%?}"
            printf '\b \b' >/dev/tty
          fi
          ;;
        *)
          value+="$char"
          printf '*' >/dev/tty
          ;;
      esac
    done
    status=$?
    printf -v "$__var_name" '%s' "$value"
    return "$status"
  fi

  IFS= read -r "$__var_name"
}

print_user_text() {
  if [ -w /dev/tty ]; then
    cat >/dev/tty
  else
    cat >&2
  fi
}

printf_user() {
  if [ -w /dev/tty ]; then
    printf "$@" >/dev/tty
  else
    printf "$@" >&2
  fi
}

print_dong_logo() {
  cat <<'TEXT'

██████╗   ██████╗  ███╗   ██╗  ██████╗
██╔══██╗ ██╔═══██╗ ████╗  ██║ ██╔════╝
██║  ██║ ██║   ██║ ██╔██╗ ██║ ██║  ███╗
██║  ██║ ██║   ██║ ██║╚██╗██║ ██║   ██║
██████╔╝ ╚██████╔╝ ██║ ╚████║ ╚██████╔╝
╚═════╝   ╚═════╝  ╚═╝  ╚═══╝  ╚═════╝
TEXT
}

print_terminal_qr_matrix() {
  local upper lower max_len i upper_cell lower_cell fg bg has_rows=0

  while IFS= read -r upper; do
    [ -n "$upper" ] || continue
    has_rows=1

    if ! IFS= read -r lower; then
      lower=""
    fi

    max_len="${#upper}"
    [ "${#lower}" -gt "$max_len" ] && max_len="${#lower}"

    for ((i = 0; i < max_len; i++)); do
      upper_cell="${upper:i:1}"
      lower_cell="${lower:i:1}"

      case "$upper_cell" in
        1) fg=30 ;;
        *) fg=97 ;;
      esac

      case "$lower_cell" in
        1) bg=40 ;;
        *) bg=107 ;;
      esac

      printf '\033[%s;%sm▀' "$fg" "$bg"
    done
    printf '\033[0m\n'
  done

  [ "$has_rows" = "1" ]
}

print_wechat_qr() {
  print_terminal_qr_matrix <<'QR'
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000001111111001001011011000111111100000
000001000001010011001010010100000100000
000001011101011111011111010101110100000
000001011101001110111001100101110100000
000001011101010000101111100101110100000
000001000001011010101000100100000100000
000001111111010101010101010111111100000
000000000000011001010011110000000000000
000001101001100100100110100111011000000
000000010010100110110110001100100100000
000001001101001100100101100011111000000
000001100100100000110001100110011000000
000000010011100001000000111100101100000
000000111100000111010011111000000000000
000000010011000001010001010000111100000
000000010100111001101111011000101000000
000001111011011110000110011010001000000
000000101000110000000111001110100100000
000001000111001100110011000100001100000
000000000100010010000110100101001100000
000001001011010111100100011111010000000
000000000000011011111111110001011100000
000001111111010111110110010101001000000
000001000001001011110001010001111000000
000001011101001111001100011111001000000
000001011101010110010011010111111000000
000001011101000110100001111011110100000
000001000001010001111010110000001000000
000001111111010101000110110011001000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
000000000000000000000000000000000000000
QR
}

usage() {
  cat <<'USAGE'
cc-oneclick macOS 安装向导

用法:
  bash install.sh [选项]

选项:
  --manifest-url URL_OR_PATH     指定 manifest.json 地址或本地路径
  --mirror-base-url URL          指定国内镜像根地址
  --dry-run                      只打印计划，不下载安装
  --yes                          使用默认选项，减少确认提示
  --no-launch                    安装结束后不启动 Claude Code
  --version                      显示脚本版本
  --help                         显示帮助

示例:
  curl -fsSL https://cc.fdonglawyer.asia | bash
  curl -fsSL https://cc.fdonglawyer.asia/install.sh | bash
  curl -fsSL https://cc.fdonglawyer.asia/install.sh | bash -s -- --mirror-base-url https://其他镜像域名
  CC_ONECLICK_MANIFEST_URL=https://cc.fdonglawyer.asia/manifest.json bash install.sh
USAGE
}

parse_args() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --manifest-url)
        [ "$#" -ge 2 ] || die "--manifest-url 需要一个参数"
        MANIFEST_URL="$2"
        shift 2
        ;;
      --mirror-base-url)
        [ "$#" -ge 2 ] || die "--mirror-base-url 需要一个参数"
        MIRROR_BASE_URL="${2%/}"
        shift 2
        ;;
      --dry-run)
        DRY_RUN=1
        shift
        ;;
      --yes|-y)
        ASSUME_YES=1
        shift
        ;;
      --no-launch)
        NO_LAUNCH=1
        shift
        ;;
      --version)
        printf '%s %s\n' "$PROGRAM_NAME" "$SCRIPT_VERSION"
        exit 0
        ;;
      --help|-h)
        usage
        exit 0
        ;;
      *)
        die "未知参数：$1。请运行 --help 查看用法。"
        ;;
    esac
  done

  if [ -z "$MANIFEST_URL" ]; then
    MANIFEST_URL="${MIRROR_BASE_URL%/}/manifest.json"
  fi
}

make_work_dir() {
  WORK_DIR="${TMPDIR:-/tmp}/${PROGRAM_NAME}.$$"
  mkdir -p "$WORK_DIR"
  chmod 700 "$WORK_DIR"
}

cleanup() {
  if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
    rm -rf "$WORK_DIR"
  fi
}

urlencode() {
  local input="$1"
  local length=${#input}
  local i char encoded=""

  LC_ALL=C
  for ((i = 0; i < length; i++)); do
    char="${input:i:1}"
    case "$char" in
      [a-zA-Z0-9.~_-])
        encoded+="$char"
        ;;
      *)
        printf -v char '%%%02X' "'$char"
        encoded+="$char"
        ;;
    esac
  done

  printf '%s' "$encoded"
}

build_deepseek_deeplink() {
  local api_key="$1"
  local name endpoint key model haiku sonnet opus notes

  name="$(urlencode "$DEFAULT_PROVIDER_NAME")"
  endpoint="$(urlencode "$DEEPSEEK_BASE_URL")"
  key="$(urlencode "$api_key")"
  model="$(urlencode "$DEEPSEEK_MAIN_MODEL")"
  haiku="$(urlencode "$DEEPSEEK_LIGHT_MODEL")"
  sonnet="$(urlencode "$DEEPSEEK_MAIN_MODEL")"
  opus="$(urlencode "$DEEPSEEK_MAIN_MODEL")"
  notes="$(urlencode "Imported by cc-oneclick. Manage future changes in CC Switch UI.")"

  printf 'ccswitch://v1/import?resource=provider&app=claude&name=%s&endpoint=%s&apiKey=%s&model=%s&haikuModel=%s&sonnetModel=%s&opusModel=%s&enabled=true&notes=%s' \
    "$name" "$endpoint" "$key" "$model" "$haiku" "$sonnet" "$opus" "$notes"
}

manifest_value() {
  local file="$1"
  local path="$2"

  [ -f "$file" ] || return 1

  if command -v plutil >/dev/null 2>&1; then
    if plutil -extract "$path" raw -o - "$file" 2>/dev/null; then
      return 0
    fi
  fi

  if command -v python3 >/dev/null 2>&1; then
    python3 - "$file" "$path" <<'PY'
import json
import sys

with open(sys.argv[1], "r", encoding="utf-8") as fh:
    data = json.load(fh)

value = data
for part in sys.argv[2].split("."):
    value = value[part]

if isinstance(value, bool):
    print("true" if value else "false")
else:
    print(value)
PY
    return 0
  fi

  return 1
}

manifest_array_values() {
  local file="$1"
  local path="$2"

  [ -f "$file" ] || return 1

  if command -v plutil >/dev/null 2>&1; then
    if plutil -extract "$path" json -o - "$file" 2>/dev/null \
      | tr -d '\n' \
      | sed 's/^\[//; s/\]$//' \
      | awk -v RS='","' '
          {
            gsub(/^"/, "")
            gsub(/"$/, "")
            gsub(/\\\//, "/")
            if (length($0) > 0) print
          }
        '; then
      return 0
    fi
  fi

  command -v python3 >/dev/null 2>&1 || return 1

  python3 - "$file" "$path" <<'PY'
import json
import sys

with open(sys.argv[1], "r", encoding="utf-8") as fh:
    data = json.load(fh)

value = data
for part in sys.argv[2].split("."):
    value = value[part]

if not isinstance(value, list):
    raise SystemExit(1)

for item in value:
    if isinstance(item, str):
        print(item)
PY
}

sha256_file() {
  shasum -a 256 "$1" | awk '{print $1}'
}

verify_sha256() {
  local file="$1"
  local expected="$2"
  local actual

  [ -f "$file" ] || {
    printf 'checksum mismatch: file missing\n' >&2
    return 1
  }

  actual="$(sha256_file "$file")"
  if [ "$actual" != "$expected" ]; then
    printf 'checksum mismatch: expected %s, got %s\n' "$expected" "$actual" >&2
    return 1
  fi
}

ignore_existing_installs() {
  [ "${CC_ONECLICK_IGNORE_EXISTING:-0}" = "1" ]
}

ensure_local_bin_on_path() {
  local user_bin="$HOME/.local/bin"

  case ":$PATH:" in
    *":$user_bin:"*) ;;
    *) export PATH="$user_bin:$PATH" ;;
  esac
}

append_claude_path_block() {
  local target="$1"
  local block_marker="cc-oneclick: Claude Code PATH"

  if [ -f "$target" ] && grep -F "$block_marker" "$target" >/dev/null 2>&1; then
    return 0
  fi

  mkdir -p "$(dirname "$target")" || return 1
  {
    printf '\n'
    printf '# %s\n' "$block_marker"
    printf 'if [ -d "$HOME/.local/bin" ]; then\n'
    printf '  case ":$PATH:" in\n'
    printf '    *":$HOME/.local/bin:"*) ;;\n'
    printf '    *) export PATH="$HOME/.local/bin:$PATH" ;;\n'
    printf '  esac\n'
    printf 'fi\n'
  } >>"$target"
}

register_claude_command() {
  local shell_name target display_targets=()
  local targets=("$HOME/.zprofile" "$HOME/.zshrc")

  ensure_local_bin_on_path

  shell_name="$(basename "${SHELL:-zsh}")"
  if [ "$shell_name" = "bash" ]; then
    targets+=("$HOME/.bash_profile" "$HOME/.bashrc")
  fi

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 注册 claude 命令到 ~/.zprofile 和 ~/.zshrc，以后打开 Terminal 输入 claude 即可启动。\n'
    return 0
  fi

  for target in "${targets[@]}"; do
    append_claude_path_block "$target" || die "写入 ${target#$HOME/} 失败。请确认用户目录可写。"
    display_targets+=("~/${target#$HOME/}")
  done

  info "Claude 命令已注册。以后打开 Terminal，输入 claude 就能启动。已写入：$(IFS='、'; printf '%s' "${display_targets[*]}")"
}

detect_platform() {
  local os arch

  case "$(uname -s)" in
    Darwin) os="darwin" ;;
    *) return 1 ;;
  esac

  case "$(uname -m)" in
    x86_64|amd64) arch="x64" ;;
    arm64|aarch64) arch="arm64" ;;
    *) return 1 ;;
  esac

  if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
    if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = "1" ]; then
      arch="arm64"
    fi
  fi

  printf '%s-%s' "$os" "$arch"
}

version_ge() {
  local current="$1"
  local minimum="$2"
  local c_major c_minor m_major m_minor

  c_major="${current%%.*}"
  c_minor="${current#*.}"
  c_minor="${c_minor%%.*}"
  m_major="${minimum%%.*}"
  m_minor="${minimum#*.}"
  m_minor="${m_minor%%.*}"

  [ "${c_major:-0}" -gt "${m_major:-0}" ] && return 0
  [ "${c_major:-0}" -lt "${m_major:-0}" ] && return 1
  [ "${c_minor:-0}" -ge "${m_minor:-0}" ]
}

download_or_copy() {
  local source="$1"
  local target="$2"
  local label="$3"

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 下载 %s: %s -> %s\n' "$label" "$source" "$target"
    if [ "$label" = "安装清单 manifest.json" ]; then
      case "$source" in
        http://*|https://*)
          curl --fail --location --retry 3 --connect-timeout 15 --silent --show-error -o "$target" "$source"
          ;;
        file://*)
          cp "${source#file://}" "$target"
          ;;
        *)
          cp "$source" "$target"
          ;;
      esac
    elif [ -f "$source" ]; then
      cp "$source" "$target"
    fi
    return 0
  fi

  case "$source" in
    http://*|https://*)
      printf '正在下载 %s\n' "$label"
      curl --fail --location --retry 3 --connect-timeout 15 --progress-bar -o "$target" "$source"
      ;;
    file://*)
      cp "${source#file://}" "$target"
      ;;
    *)
      cp "$source" "$target"
      ;;
  esac
}

download_or_copy_any() {
  local target="$1"
  local label="$2"
  shift 2

  local source last_source

  for source in "$@"; do
    [ -n "$source" ] || continue
    if download_or_copy "$source" "$target" "$label"; then
      return 0
    fi
    last_source="$source"
    warn "$label 这一路下载没成功，我会自动换一个备用地址再试。"
  done

  die "$label 下载失败。最后尝试的地址：${last_source:-无}"
}

wait_with_spinner() {
  local label="$1"
  local pid="$2"
  local log_file="${3:-}"
  local start elapsed spinner status
  spinner='-\|/'
  start="$(date +%s)"

  while kill -0 "$pid" 2>/dev/null; do
    elapsed=$(( $(date +%s) - start ))
    printf '\r%s %s（已用 %ss）' "$label" "${spinner:elapsed%4:1}" "$elapsed"
    sleep 1
  done

  if wait "$pid"; then
    printf '\r%s 完成。                    \n' "$label"
    return 0
  else
    status=$?
  fi

  printf '\r%s 失败。                    \n' "$label" >&2
  if [ -n "$log_file" ] && [ -f "$log_file" ]; then
    sed -n '1,80p' "$log_file" >&2 || true
  fi
  return "$status"
}

run_with_spinner_to_log() {
  local label="$1"
  local log_file="$2"
  shift 2

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] %s\n' "$label"
    return 0
  fi

  "$@" >"$log_file" 2>&1 &
  wait_with_spinner "$label" "$!" "$log_file"
}

run_with_spinner() {
  local label="$1"
  shift

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] %s: %s\n' "$label" "$*"
    return 0
  fi

  run_with_spinner_to_log "$label" "$WORK_DIR/command.log" "$@"
}

ask_yes_no() {
  local prompt="$1"
  local default="${2:-yes}"
  local answer

  if [ "$ASSUME_YES" = "1" ]; then
    [ "$default" = "yes" ]
    return $?
  fi

  if [ "$default" = "yes" ]; then
    printf '%s [Y/n] ' "$prompt"
  else
    printf '%s [y/N] ' "$prompt"
  fi

  read_user_input answer || die "无法读取键盘输入。请直接在 Terminal 里运行安装命令，或加 --yes 使用默认选项。"
  case "$answer" in
    y|Y|yes|YES|Yes) return 0 ;;
    n|N|no|NO|No) return 1 ;;
    "") [ "$default" = "yes" ] ;;
    *) return 1 ;;
  esac
}

ensure_macos() {
  if [ "${CC_ONECLICK_ASSUME_MACOS:-0}" = "1" ]; then
    return 0
  fi

  [ "$(uname -s)" = "Darwin" ] || die "当前脚本只支持 macOS。V1 暂不支持 Windows。"
}

check_macos_version() {
  local minimum current
  minimum="$(manifest_value "$MANIFEST_FILE" "minimumMacOS" 2>/dev/null || printf '13.0')"

  if [ "${CC_ONECLICK_ASSUME_MACOS:-0}" = "1" ]; then
    current="13.0"
  else
    current="$(sw_vers -productVersion)"
  fi

  if ! version_ge "$current" "$minimum"; then
    die "当前 macOS 版本是 $current，最低需要 $minimum。请升级系统后再运行。"
  fi

  info "macOS 版本检查通过：$current"
}

command_line_tools_ready() {
  [ "${CC_ONECLICK_SKIP_CLT_CHECK:-0}" = "1" ] && return 0

  /usr/bin/xcode-select -p >/dev/null 2>&1 || return 1
  /usr/bin/xcrun -find git >/dev/null 2>&1 || return 1
}

ensure_command_line_tools() {
  local developer_dir

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 检查 macOS 命令行开发者工具（含 git）。如缺失，会引导打开 xcode-select --install。\n'
    return 0
  fi

  if command_line_tools_ready; then
    developer_dir="$(/usr/bin/xcode-select -p 2>/dev/null || true)"
    info "命令行开发者工具检查通过：${developer_dir:-已安装}"
    return 0
  fi

  cat <<'TEXT'

接下来需要安装苹果的“命令行开发者工具”。
它是 macOS 官方提供的小工具包，里面包含 git 等基础工具。很多 AI 编程工具都默认需要它。

等会儿系统可能会弹出一个苹果自己的安装窗口，你点“安装”，然后等它走完即可。
这个步骤可能需要几分钟，窗口不要关。
TEXT

  if ask_yes_no "现在打开命令行开发者工具安装窗口吗？" "yes"; then
    /usr/bin/xcode-select --install >/dev/null 2>&1 || true
  else
    die "没有命令行开发者工具，Claude Code 后续使用时很容易缺 git。请安装后再运行。"
  fi

  printf '安装窗口完成后，回到这里按回车继续：'
  read_user_input _ || die "无法读取键盘输入。请直接在 Terminal 里运行安装命令。"

  if command_line_tools_ready; then
    developer_dir="$(/usr/bin/xcode-select -p 2>/dev/null || true)"
    info "命令行开发者工具已就绪：${developer_dir:-已安装}"
    return 0
  fi

  die "还没有检测到命令行开发者工具。请确认系统安装窗口已经完成，然后重新运行脚本。"
}

manifest_platform_checksum() {
  local file="$1"
  local platform="$2"
  local value

  value="$(manifest_value "$file" "platforms.$platform.checksum" 2>/dev/null || true)"
  if [ -n "$value" ]; then
    printf '%s' "$value"
    return 0
  fi

  tr -d '\n\r\t ' < "$file" | sed -n "s/.*\"$platform\":{[^}]*\"checksum\":\"\([a-fA-F0-9]\{64\}\)\".*/\1/p"
}

claude_binary_version() {
  local binary_path="$1"
  local version_line version

  version_line="$("$binary_path" --version 2>/dev/null | head -n 1 || true)"
  version="${version_line%% *}"

  case "$version" in
    [0-9]*.[0-9]*.[0-9]*)
      printf '%s' "$version"
      ;;
    *)
      printf '无法读取 Claude Code 版本号：%s\n' "${version_line:-无输出}" >&2
      return 1
      ;;
  esac
}

write_claude_app_bundle() {
  local installed_binary="$1"
  local app_dir="$HOME/.local/share/claude/ClaudeCode.app"
  local macos_dir="$app_dir/Contents/MacOS"

  mkdir -p "$macos_dir" || return 1
  cp "$installed_binary" "$macos_dir/claude" || return 1
  chmod 755 "$macos_dir/claude" || return 1
  cat >"$app_dir/Contents/Info.plist" <<'PLIST'
<?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>CFBundleDisplayName</key>
  <string>Claude Code</string>
  <key>CFBundleExecutable</key>
  <string>claude</string>
  <key>CFBundleIdentifier</key>
  <string>com.anthropic.claude-code</string>
  <key>CFBundleName</key>
  <string>Claude Code</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>LSUIElement</key>
  <true/>
  <key>NSMicrophoneUsageDescription</key>
  <string>Claude Code uses the microphone for voice dictation.</string>
</dict>
</plist>
PLIST
}

install_claude_binary() {
  local binary_path="$1"
  local expected_version="${2:-}"
  local version versions_dir bin_dir installed_binary tmp_binary claude_link

  version="$(claude_binary_version "$binary_path")" || return 1
  if [ -n "$expected_version" ] && [ "$version" != "$expected_version" ]; then
    printf 'Claude Code 版本不一致：manifest 是 %s，二进制是 %s。\n' "$expected_version" "$version" >&2
    return 1
  fi

  versions_dir="$HOME/.local/share/claude/versions"
  bin_dir="$HOME/.local/bin"
  installed_binary="$versions_dir/$version"
  tmp_binary="$versions_dir/.$version.$$"
  claude_link="$bin_dir/claude"

  mkdir -p "$versions_dir" "$bin_dir" || return 1
  cp "$binary_path" "$tmp_binary" || return 1
  chmod 755 "$tmp_binary" || return 1
  mv "$tmp_binary" "$installed_binary" || return 1

  if [ -e "$claude_link" ] || [ -L "$claude_link" ]; then
    rm -f "$claude_link" || return 1
  fi
  ln -s "$installed_binary" "$claude_link" || return 1
  write_claude_app_bundle "$installed_binary" || return 1
  ensure_local_bin_on_path
}

install_claude_code_from_npm_tarball() {
  local platform="$1"
  local channel="$2"
  local url archive_sha binary_sha binary_member archive_path extract_dir binary_path expected_version

  url="$(manifest_value "$MANIFEST_FILE" "claude.platforms.$platform.url" 2>/dev/null || true)"
  archive_sha="$(manifest_value "$MANIFEST_FILE" "claude.platforms.$platform.sha256" 2>/dev/null || true)"
  binary_sha="$(manifest_value "$MANIFEST_FILE" "claude.platforms.$platform.binarySha256" 2>/dev/null || true)"
  binary_member="$(manifest_value "$MANIFEST_FILE" "claude.platforms.$platform.binaryPath" 2>/dev/null || printf 'package/claude')"
  expected_version="$(manifest_value "$MANIFEST_FILE" "claude.version" 2>/dev/null || true)"

  [ -n "$url" ] || die "manifest.json 缺少 claude.platforms.$platform.url。"
  [ -n "$archive_sha" ] || die "manifest.json 缺少 claude.platforms.$platform.sha256。"

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 将从 npm 镜像原生包安装 Claude Code：%s\n' "$url"
    printf '[dry-run] 会安装到 ~/.local/share/claude/versions，并创建 ~/.local/bin/claude。\n'
    printf '[dry-run] 不再调用 claude install，避免触发官方 downloads.claude.ai 下载。\n'
    printf '[dry-run] 预期原生包 SHA256：%s\n' "$archive_sha"
    if [ -n "$binary_sha" ]; then
      printf '[dry-run] 预期原生二进制 SHA256：%s\n' "$binary_sha"
    fi
    return 0
  fi

  archive_path="$WORK_DIR/claude-$platform.tgz"
  extract_dir="$WORK_DIR/claude-$platform"
  mkdir -p "$extract_dir"

  download_or_copy "$url" "$archive_path" "Claude Code 原生 npm 包"
  run_with_spinner "正在校验 Claude Code 原生 npm 包" verify_sha256 "$archive_path" "$archive_sha" || die "Claude Code 原生包校验失败。请联系维护者刷新镜像。"

  tar -xzf "$archive_path" -C "$extract_dir" "$binary_member" || die "Claude Code 原生包解压失败。请联系维护者刷新镜像。"
  binary_path="$extract_dir/$binary_member"
  [ -f "$binary_path" ] || die "Claude Code 原生包中找不到 $binary_member。"
  chmod +x "$binary_path"

  if [ -n "$binary_sha" ]; then
    run_with_spinner "正在校验 Claude Code 原生二进制" verify_sha256 "$binary_path" "$binary_sha" || die "Claude Code 二进制校验失败。请联系维护者刷新镜像。"
  fi

  run_with_spinner "正在安装 Claude Code 原生版" install_claude_binary "$binary_path" "$expected_version" || die "Claude Code 安装失败。请联系维护者检查镜像包。"
}

install_claude_code() {
  local existing version_url binary_path platform checksum actual_version base channel install_mode
  local expected_version expected_latest_sha expected_manifest_sha

  if ! ignore_existing_installs && existing="$(locate_claude)" && "$existing" --version >/dev/null 2>&1; then
    info "检测到已安装 Claude Code：$("$existing" --version 2>/dev/null | head -n 1)"
    if ask_yes_no "电脑里已经有 Claude Code。一般保留现有版本就好，要保留吗？" "yes"; then
      return 0
    fi
  fi

  platform="$(detect_platform)" || die "无法识别当前 Mac 芯片架构。"
  channel="$(manifest_value "$MANIFEST_FILE" "claude.channel" 2>/dev/null || printf 'stable')"
  install_mode="$(manifest_value "$MANIFEST_FILE" "claude.installMode" 2>/dev/null || printf 'releaseMirror')"

  if [ "$install_mode" = "npmTarball" ]; then
    install_claude_code_from_npm_tarball "$platform" "$channel"
    return 0
  fi

  base="$(manifest_value "$MANIFEST_FILE" "claude.downloadBaseUrl")"
  expected_version="$(manifest_value "$MANIFEST_FILE" "claude.version" 2>/dev/null || true)"
  expected_latest_sha="$(manifest_value "$MANIFEST_FILE" "claude.latestSha256" 2>/dev/null || true)"
  expected_manifest_sha="$(manifest_value "$MANIFEST_FILE" "claude.manifestSha256" 2>/dev/null || true)"

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 将从 %s 安装 Claude Code 原生版，平台 %s，频道 %s。\n' "$base" "$platform" "$channel"
    printf '[dry-run] 会安装到 ~/.local/share/claude/versions，并创建 ~/.local/bin/claude。\n'
    return 0
  fi

  version_url="${base%/}/latest"
  download_or_copy "$version_url" "$WORK_DIR/claude-latest.txt" "Claude Code 版本信息"
  if [ -n "$expected_latest_sha" ]; then
    run_with_spinner "正在校验 Claude Code 版本信息" verify_sha256 "$WORK_DIR/claude-latest.txt" "$expected_latest_sha" || die "Claude Code 版本信息校验失败。请联系维护者刷新镜像。"
  fi
  actual_version="$(tr -d '[:space:]' < "$WORK_DIR/claude-latest.txt")"
  case "$actual_version" in
    [0-9]*.[0-9]*.[0-9]*) ;;
    *) die "Claude Code 版本信息异常：$actual_version。请检查镜像是否完整。" ;;
  esac
  if [ -n "$expected_version" ] && [ "$actual_version" != "$expected_version" ]; then
    die "Claude Code 镜像版本不一致：manifest 是 $expected_version，latest 是 $actual_version。请联系维护者刷新镜像。"
  fi

  download_or_copy "${base%/}/$actual_version/manifest.json" "$WORK_DIR/claude-manifest.json" "Claude Code 校验清单"
  if [ -n "$expected_manifest_sha" ]; then
    run_with_spinner "正在校验 Claude Code 校验清单" verify_sha256 "$WORK_DIR/claude-manifest.json" "$expected_manifest_sha" || die "Claude Code 校验清单校验失败。请联系维护者刷新镜像。"
  fi
  checksum="$(manifest_platform_checksum "$WORK_DIR/claude-manifest.json" "$platform")"
  [ -n "$checksum" ] || die "Claude Code manifest 中找不到平台 $platform 的校验值。"

  binary_path="$WORK_DIR/claude-$actual_version-$platform"
  download_or_copy "${base%/}/$actual_version/$platform/claude" "$binary_path" "Claude Code 原生二进制"
  chmod +x "$binary_path"

  run_with_spinner "正在校验 Claude Code 原生二进制" verify_sha256 "$binary_path" "$checksum" || die "Claude Code 二进制校验失败。请联系维护者刷新镜像。"
  run_with_spinner "正在安装 Claude Code 原生版" install_claude_binary "$binary_path" "$actual_version" || die "Claude Code 安装失败。请联系维护者检查镜像包。"
}

install_cc_switch() {
  local dmg_url dmg_sha dmg_path mount_point app_source mount_log mirror_url
  local dmg_urls=()

  if ! ignore_existing_installs && [ -d "/Applications/CC Switch.app" ]; then
    info "检测到 /Applications/CC Switch.app"
    if ask_yes_no "电脑里已经有 CC Switch。一般保留现有版本就好，要保留吗？" "yes"; then
      return 0
    fi
  fi

  dmg_url="$(manifest_value "$MANIFEST_FILE" "ccSwitch.dmg.url")"
  dmg_sha="$(manifest_value "$MANIFEST_FILE" "ccSwitch.dmg.sha256")"
  dmg_path="$WORK_DIR/cc-switch.dmg"
  dmg_urls=("$dmg_url")
  while IFS= read -r mirror_url; do
    [ -n "$mirror_url" ] && dmg_urls+=("$mirror_url")
  done < <(manifest_array_values "$MANIFEST_FILE" "ccSwitch.dmg.mirrorUrls" 2>/dev/null || true)

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 将下载并安装 CC Switch：%s\n' "$dmg_url"
    if [ "${#dmg_urls[@]}" -gt 1 ]; then
      printf '[dry-run] CC Switch 备用镜像数量：%s\n' "$(( ${#dmg_urls[@]} - 1 ))"
    fi
    printf '[dry-run] 预期 SHA256：%s\n' "$dmg_sha"
    printf '[dry-run] 挂载 DMG、复制 CC Switch.app 到 /Applications、启动应用。\n'
    return 0
  fi

  download_or_copy_any "$dmg_path" "CC Switch 安装镜像" "${dmg_urls[@]}"
  run_with_spinner "正在校验 CC Switch 安装镜像" verify_sha256 "$dmg_path" "$dmg_sha" || die "CC Switch DMG 校验失败。请联系维护者刷新镜像。"

  mount_log="$WORK_DIR/hdiutil-attach.log"
  run_with_spinner_to_log "正在挂载 CC Switch 安装镜像" "$mount_log" hdiutil attach "$dmg_path" -nobrowse -readonly || die "无法挂载 CC Switch DMG。请关闭可能已打开的安装镜像后重试。"
  mount_point="$(awk '/\/Volumes\// {for (i=3; i<=NF; i++) {printf "%s%s", (i==3?"":" "), $i}; print ""}' "$mount_log" | tail -n 1)"
  [ -n "$mount_point" ] || die "无法挂载 CC Switch DMG。请关闭可能已打开的安装镜像后重试。"

  app_source="$mount_point/CC Switch.app"
  [ -d "$app_source" ] || {
    hdiutil detach "$mount_point" -quiet || true
    die "DMG 中没有找到 CC Switch.app。请联系维护者确认安装包。"
  }

  if ! run_with_spinner "正在复制 CC Switch 到 /Applications" ditto "$app_source" "/Applications/CC Switch.app"; then
    hdiutil detach "$mount_point" -quiet || true
    die "复制 CC Switch 失败。请确认 /Applications 可写，并关闭已打开的 CC Switch 后重试。"
  fi

  run_with_spinner "正在卸载 CC Switch 安装镜像" hdiutil detach "$mount_point" -quiet || warn "安装镜像卸载失败。稍后可以在 Finder 侧边栏手动推出。"
}

locate_claude() {
  local candidate

  for candidate in "$HOME/.local/bin/claude" "$HOME/.claude/local/claude" "$HOME/.claude/bin/claude"; do
    if [ -x "$candidate" ]; then
      printf '%s' "$candidate"
      return 0
    fi
  done

  if candidate="$(command -v claude 2>/dev/null)"; then
    printf '%s' "$candidate"
    return 0
  fi

  return 1
}

copy_existing_claude_icon() {
  local target="$1"
  local candidate

  for candidate in \
    "$HOME/.local/share/claude/ClaudeCode.app/Contents/Resources/ClaudeCode.icns" \
    "$HOME/.local/share/claude/ClaudeCode.app/Contents/Resources/AppIcon.icns" \
    "/Applications/Claude.app/Contents/Resources/AppIcon.icns" \
    "/Applications/Claude.app/Contents/Resources/Claude.icns" \
    "/Applications/Claude Code.app/Contents/Resources/AppIcon.icns" \
    "/Applications/Claude Code.app/Contents/Resources/ClaudeCode.icns"; do
    if [ -f "$candidate" ] && [ "$candidate" != "$target" ]; then
      cp "$candidate" "$target" && return 0
    fi
  done

  return 1
}

generate_builtin_claude_icon() {
  local target="$1"
  local temp_dir iconset base_ppm base_png cleanup_temp=0
  local spec src_size dst_name dst_size

  if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
    if [ -n "${WORK_DIR:-}" ] && [ -d "$WORK_DIR" ]; then
      temp_dir="$WORK_DIR/claude-icon"
      rm -rf "$temp_dir"
      mkdir -p "$temp_dir"
    else
      temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/cc-oneclick-icon.XXXXXX")"
      cleanup_temp=1
    fi

    iconset="$temp_dir/ClaudeCode.iconset"
    base_ppm="$temp_dir/ClaudeCode.ppm"
    base_png="$temp_dir/ClaudeCode.png"
    mkdir -p "$iconset"

    awk '
      BEGIN {
        size = 256
        cx = size / 2
        cy = size / 2
        pi = atan2(0, -1)
        print "P3"
        print size, size
        print 255
        for (y = 0; y < size; y++) {
          for (x = 0; x < size; x++) {
            dx = x - cx
            dy = y - cy
            r = 218
            g = 126
            b = 82
            cream = 0
            for (k = 0; k < 6; k++) {
              a = pi * k / 3
              proj = dx * cos(a) + dy * sin(a)
              perp = -dx * sin(a) + dy * cos(a)
              if (proj > -12 && proj < 78 && perp * perp < 210) {
                cream = 1
              }
            }
            if (dx * dx + dy * dy < 900) {
              cream = 1
            }
            if (cream) {
              r = 249
              g = 238
              b = 222
            }
            print r, g, b
          }
        }
      }
    ' >"$base_ppm"

    if sips -s format png "$base_ppm" --out "$base_png" >/dev/null 2>&1; then
      for spec in \
        "16 icon_16x16.png" \
        "32 icon_16x16@2x.png" \
        "32 icon_32x32.png" \
        "64 icon_32x32@2x.png" \
        "128 icon_128x128.png" \
        "256 icon_128x128@2x.png" \
        "256 icon_256x256.png" \
        "512 icon_256x256@2x.png" \
        "512 icon_512x512.png" \
        "1024 icon_512x512@2x.png"; do
        src_size="${spec%% *}"
        dst_name="${spec#* }"
        dst_size="$src_size"
        sips -z "$dst_size" "$dst_size" "$base_png" --out "$iconset/$dst_name" >/dev/null 2>&1 || return 1
      done

      if iconutil -c icns "$iconset" -o "$target" >/dev/null 2>&1 && [ -s "$target" ]; then
        [ "$cleanup_temp" = "1" ] && rm -rf "$temp_dir"
        return 0
      fi
    fi

    [ "$cleanup_temp" = "1" ] && rm -rf "$temp_dir"
  fi

  printf 'Claude Code icon placeholder\n' >"$target"
}

write_claude_launcher_icon() {
  local target="$1"

  copy_existing_claude_icon "$target" && return 0
  generate_builtin_claude_icon "$target"
}

write_desktop_launcher_plist() {
  local plist="$1"

  cat >"$plist" <<'PLIST'
<?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>CFBundleDisplayName</key>
  <string>Claude Code</string>
  <key>CFBundleExecutable</key>
  <string>Claude Code</string>
  <key>CFBundleIconFile</key>
  <string>ClaudeCode</string>
  <key>CFBundleIdentifier</key>
  <string>com.fdonglawyer.cc-oneclick.claude-code-launcher</string>
  <key>CFBundleName</key>
  <string>Claude Code</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>LSMinimumSystemVersion</key>
  <string>13.0</string>
</dict>
</plist>
PLIST
}

write_desktop_launcher_script() {
  local launcher="$1"
  local claude_bin="$2"

  cat >"$launcher" <<EOF
#!/usr/bin/env bash
set -euo pipefail

CLAUDE_BIN="$claude_bin"
if [ ! -x "\$CLAUDE_BIN" ]; then
  CLAUDE_BIN="\$(command -v claude 2>/dev/null || true)"
fi

if [ -z "\$CLAUDE_BIN" ] || [ ! -x "\$CLAUDE_BIN" ]; then
  /usr/bin/osascript -e 'display dialog "没有找到 claude 命令。请重新运行 cc-oneclick 安装脚本，或打开 Terminal 输入 claude --version 检查。" buttons {"知道了"} default button 1 with title "Claude Code"'
  exit 1
fi

quoted_claude="\$(printf '%q' "\$CLAUDE_BIN")"
/usr/bin/osascript <<OSA
tell application "Terminal"
  activate
  do script "\$quoted_claude"
end tell
OSA
EOF
  chmod 755 "$launcher"
}

create_desktop_launcher() {
  local claude_bin desktop_dir app_dir contents_dir macos_dir resources_dir launcher plist icon backup

  claude_bin="$(locate_claude)" || die "创建桌面启动程序前没有找到 claude 命令。请重新运行安装脚本。"
  desktop_dir="$HOME/Desktop"
  app_dir="$desktop_dir/Claude Code.app"
  contents_dir="$app_dir/Contents"
  macos_dir="$contents_dir/MacOS"
  resources_dir="$contents_dir/Resources"
  launcher="$macos_dir/Claude Code"
  plist="$contents_dir/Info.plist"
  icon="$resources_dir/ClaudeCode.icns"

  if [ "$DRY_RUN" = "1" ]; then
    DESKTOP_LAUNCHER_CREATED=1
    printf '[dry-run] 创建桌面一键启动程序：~/Desktop/Claude Code.app（含 Claude 图标）。\n'
    return 0
  fi

  mkdir -p "$desktop_dir"
  if [ -e "$app_dir" ]; then
    if [ -f "$plist" ] && grep -F "com.fdonglawyer.cc-oneclick.claude-code-launcher" "$plist" >/dev/null 2>&1; then
      rm -rf "$app_dir"
    else
      backup="$desktop_dir/Claude Code.app.backup.$(date +%Y%m%d%H%M%S)"
      mv "$app_dir" "$backup"
      warn "桌面上已有一个 Claude Code.app，我已先备份到：$backup"
    fi
  fi

  mkdir -p "$macos_dir" "$resources_dir"
  write_desktop_launcher_script "$launcher" "$claude_bin"
  write_desktop_launcher_plist "$plist"
  write_claude_launcher_icon "$icon" || warn "桌面启动程序已创建，但图标生成失败。功能不受影响。"
  touch "$app_dir"
  DESKTOP_LAUNCHER_CREATED=1

  info "已在桌面创建 Claude Code.app。以后可以双击它启动，也可以打开 Terminal 输入 claude。"
}

verify_claude_code() {
  local claude_bin version

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 验证 Claude Code：claude --version\n'
    return 0
  fi

  claude_bin="$(locate_claude)" || die "Claude Code 安装后仍无法找到。请重新打开 Terminal 输入 claude --version；如果仍失败，请联系维护者。"
  version="$("$claude_bin" --version 2>/dev/null | head -n 1 || true)"
  [ -n "$version" ] || die "Claude Code 已找到，但 claude --version 无法运行。请重新运行安装脚本。"

  info "Claude Code 验证通过：$version"
}

verify_cc_switch() {
  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 验证 /Applications/CC Switch.app 并打开应用。\n'
    return 0
  fi

  [ -d "/Applications/CC Switch.app" ] || die "没有找到 /Applications/CC Switch.app。请重新运行安装脚本。"
  run_with_spinner "正在打开 CC Switch" open -a "CC Switch" --args --register-protocol || \
    run_with_spinner "正在打开 CC Switch" open -a "CC Switch" || \
    die "CC Switch 已复制，但无法自动打开。请在 Finder 的“应用程序”里手动打开 CC Switch。"

  info "CC Switch 验证通过：/Applications/CC Switch.app"
}

prompt_deepseek_key() {
  local key

  if [ "${CC_ONECLICK_DEEPSEEK_API_KEY:-}" ]; then
    printf '%s' "$CC_ONECLICK_DEEPSEEK_API_KEY"
    return 0
  fi

  if [ "$DRY_RUN" = "1" ]; then
    printf 'sk-dry-run'
    return 0
  fi

  while true; do
    print_user_text <<'TEXT'

现在需要填一下 DeepSeek API Key。
这个 Key 通常是 sk- 开头的一长串字符，像密码一样保管就好，不要发到群里。

如果你还没有 Key，可以这样找：
1. 打开 https://platform.deepseek.com
2. 登录后，在页面里找到 API keys
3. 创建一个新的 Key，或者复制已有的 sk- 开头 Key
4. 回到这个窗口粘贴

粘贴时不会显示原文，只会出现一串 *，这是正常的，表示已经粘贴进来了。
TEXT
    printf_user '把 DeepSeek API Key 粘贴到这里，然后按回车：'
    read_secret_input key || die "无法读取 DeepSeek API Key。请确认是在 Terminal 里运行安装命令。"
    printf_user '\n'

    case "$key" in
      sk-*)
        if [ "${#key}" -ge 12 ]; then
          printf '%s' "$key"
          return 0
        fi
        ;;
    esac

    warn "看起来不是完整的 DeepSeek API Key。请确认是 sk- 开头，然后重新粘贴一次。"
  done
}

check_deepseek_key() {
  local key="$1"
  local response http_code="" http_code_file curl_config curl_log curl_pid

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 跳过 DeepSeek 连通性检查。\n'
    return 0
  fi

  response="$WORK_DIR/deepseek-response.json"
  http_code_file="$WORK_DIR/deepseek-http-code.txt"
  curl_config="$WORK_DIR/deepseek-curl.conf"
  curl_log="$WORK_DIR/deepseek-curl.log"

  cat >"$curl_config" <<EOF
silent
show-error
location
connect-timeout = 20
max-time = 60
request = "POST"
url = "$DEEPSEEK_BASE_URL/v1/messages"
output = "$response"
write-out = "%{http_code}"
header = "Content-Type: application/json"
header = "x-api-key: $key"
header = "anthropic-version: 2023-06-01"
data = "{\"model\":\"$DEEPSEEK_LIGHT_MODEL\",\"max_tokens\":8,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}"
EOF

  curl --config "$curl_config" >"$http_code_file" 2>"$curl_log" &
  curl_pid=$!
  if ! wait_with_spinner "正在验证 DeepSeek API Key" "$curl_pid" "$curl_log"; then
    warn "DeepSeek API 暂时没连上。可能是网络波动，也可能是 Key/余额问题，我们再试一次。"
    return 1
  fi

  http_code="$(cat "$http_code_file" 2>/dev/null || true)"

  if [ "${http_code:-}" = "200" ]; then
    info "DeepSeek API Key 验证通过。"
    return 0
  fi

  warn "DeepSeek API 验证没有通过，HTTP 状态码：${http_code:-无响应}。请确认 Key 没有多复制空格，账户也有 API 余额。"
  sed -n '1,8p' "$response" >&2 || true
  return 1
}

import_deepseek_provider() {
  local key="$1"
  local deeplink

  deeplink="$(build_deepseek_deeplink "$key")"

  cat <<'TEXT'

接下来会弹出 CC Switch 的导入确认窗口。
请帮我看一下这几项，如果都对，就点确认导入：
1. 应用类型：Claude
2. Provider 名称：DeepSeek V4 (cc-oneclick)
3. Endpoint：api.deepseek.com/anthropic
TEXT

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 打开深链：%s\n' "$deeplink"
    return 0
  fi

  open "$deeplink" || die "无法自动打开 CC Switch 导入窗口。请先手动打开 CC Switch，然后重新运行脚本。"
  info "CC Switch 导入链接已打开。"
  printf '如果没有看到弹窗，请先把 CC Switch 点到前面看一下。确认导入后，回到这里按回车继续。'
  read_user_input _ || die "无法读取键盘输入。请确认是在 Terminal 里运行安装命令。"
}

smoke_test_claude_with_deepseek_env() {
  local key="$1"
  local claude_bin smoke_log smoke_version

  if [ "$DRY_RUN" = "1" ]; then
    printf '[dry-run] 使用 DeepSeek 临时环境运行 claude --version 做最终 smoke test。\n'
    return 0
  fi

  claude_bin="$(locate_claude)" || die "无法找到 claude 命令，不能完成最终 smoke test。请重新打开 Terminal 后运行 claude --version。"
  smoke_log="$WORK_DIR/claude-smoke.log"

  run_with_spinner_to_log "正在运行 Claude Code 最终 smoke test" "$smoke_log" \
    env \
    "$DEEPSEEK_BASE_URL_ENV_NAME=$DEEPSEEK_BASE_URL" \
    "$DEEPSEEK_AUTH_ENV_NAME=$key" \
    "$DEEPSEEK_MODEL_ENV_NAME=$DEEPSEEK_MAIN_MODEL" \
    "$DEEPSEEK_LIGHT_ENV_NAME=$DEEPSEEK_LIGHT_MODEL" \
    "$claude_bin" --version || die "Claude Code 最终 smoke test 失败。请重新运行安装脚本。"

  smoke_version="$(sed -n '1p' "$smoke_log" 2>/dev/null || true)"
  info "Claude Code smoke test 通过：${smoke_version:-claude --version 可运行}"
}

prompt_desktop_launcher() {
  cat <<'TEXT'

以后启动 Claude Code 有两种方式：
1. 打开 Terminal，输入 claude
2. 双击桌面上的 Claude Code.app

如果你平时不想碰命令行，我建议创建桌面启动程序。
TEXT

  if ask_yes_no "要不要在桌面放一个 Claude Code 一键启动程序？" "yes"; then
    create_desktop_launcher
  else
    info "好的。以后打开 Terminal，输入 claude 就能启动 Claude Code。"
  fi
}

print_completion_hint() {
  if [ "${DESKTOP_LAUNCHER_CREATED:-0}" = "1" ]; then
    info "都设置好了。以后打开 Terminal 输入 claude，或者双击桌面的 Claude Code.app，就会走 DeepSeek。"
  else
    info "都设置好了。以后打开 Terminal 输入 claude，就会走 DeepSeek。"
  fi

  cat <<'TEXT'

等会儿第一次打开 Claude Code，可能还会问你两件小事：

1. 选择主题
   这个只是界面颜色，深色、浅色都可以，按自己看着舒服的选。
   它不影响功能，也不会影响 DeepSeek 配置。

2. 是否信任当前文件夹
   这个要稍微认真看一下。
   Claude Code 会把当前文件夹当成工作区，之后读取、修改文件，主要都围绕这个文件夹来做。
   如果你确认这是你准备用来工作的文件夹，就选择信任。
   如果只是第一次试一下，可以先建一个测试文件夹，别一上来就选放了很多正式材料的目录。

记住一句就行：不想让 AI 看的材料，就不要放进你信任的文件夹里。

祝你用得顺手。
真卡住了就截图发我，我来帮你看。
TEXT
}

launch_claude_if_requested() {
  local claude_bin answer

  [ "$NO_LAUNCH" = "0" ] || return 0

  if [ "$ASSUME_YES" = "1" ] || [ "$DRY_RUN" = "1" ]; then
    answer=""
  else
    printf '\n最后一步：现在打开 Claude Code 试一下吗？按回车打开，输入 n 跳过：'
    read_user_input answer || die "无法读取键盘输入。请确认是在 Terminal 里运行安装命令。"
  fi

  case "$answer" in
    n|N|no|NO|No)
      return 0
      ;;
  esac

  if claude_bin="$(locate_claude)"; then
    if [ "$DRY_RUN" = "1" ]; then
      printf '[dry-run] 启动 Claude Code：%s\n' "$claude_bin"
      return 0
    fi
    info "正在启动 Claude Code。"
    exec "$claude_bin"
  fi

  warn "当前终端暂时找不到 claude 命令。请关闭这个终端，重新打开 Terminal，然后输入：claude"
}

load_manifest() {
  MANIFEST_FILE="$WORK_DIR/manifest.json"
  download_or_copy "$MANIFEST_URL" "$MANIFEST_FILE" "安装清单 manifest.json"
  manifest_value "$MANIFEST_FILE" "minimumMacOS" >/dev/null || die "manifest.json 格式不正确：缺少 minimumMacOS。"
  manifest_value "$MANIFEST_FILE" "claude.downloadBaseUrl" >/dev/null || die "manifest.json 格式不正确：缺少 claude.downloadBaseUrl。"
  manifest_value "$MANIFEST_FILE" "ccSwitch.dmg.url" >/dev/null || die "manifest.json 格式不正确：缺少 ccSwitch.dmg.url。"
  manifest_value "$MANIFEST_FILE" "ccSwitch.dmg.sha256" >/dev/null || die "manifest.json 格式不正确：缺少 ccSwitch.dmg.sha256。"
}

intro() {
  print_dong_logo
  cat <<'TEXT'

方冬丨律师 · Claude Code 一键安装

这个脚本由 方冬丨律师 制作，只供学习、研讨和交流使用。
它不是正式产品，也不是律所统一采购的软件。坦率讲，就是把我自己已经跑通的一套流程整理出来，让大家少踩一点坑。

安装过程中如果卡住、报错，或者你也想聊聊 AI 工具在法律工作里的用法，可以加我微信。
不用研究报错是什么意思，直接把当前窗口截图发我就行。
TEXT

  if print_wechat_qr; then
    cat <<'TEXT'
上面是我的微信二维码，扫码添加即可。
TEXT
  else
    cat <<'TEXT'
（微信二维码待嵌入。维护者请先用 scripts/make-terminal-qr.swift 生成终端矩阵。）
TEXT
  fi

  cat <<'TEXT'
你好，这个窗口会一步一步帮你把 Claude Code 配好。
中间需要你做的事不多：按回车、粘贴 DeepSeek API Key、在 CC Switch 弹窗里点一次确认。

这次会自动完成：
1. 检查苹果命令行开发者工具（含 git），缺了就引导安装
2. 安装 Claude Code 原生版（保留官方自动升级能力）
3. 自动注册 claude 命令，以后打开 Terminal 输入 claude 就能用
4. 安装 CC Switch
5. 首次导入 DeepSeek API 配置
6. 询问是否创建桌面一键启动程序
7. 安装完成后直接启动 Claude Code

下载和安装时会显示进度条。时间可能稍长，这个窗口不要关闭就行。
DeepSeek API Key 只会用于本机配置和连通性检查，不会写进日志。
TEXT

  if [ "$ASSUME_YES" = "0" ]; then
    printf '\n准备好了就按回车开始，输入 n 可以退出：'
    read_user_input answer || die "无法读取键盘输入。请直接在 Terminal 里运行安装命令，或加 --yes 使用默认选项。"
    case "$answer" in
      n|N|no|NO|No) exit 0 ;;
    esac
  fi
}

main_test_mode() {
  case "${1:-}" in
    --test-urlencode)
      shift
      urlencode "$*"
      ;;
    --test-build-deeplink)
      [ "$#" -eq 2 ] || die "--test-build-deeplink 需要 API key"
      build_deepseek_deeplink "$2"
      ;;
    --test-manifest-value)
      [ "$#" -eq 3 ] || die "--test-manifest-value 需要 file 和 path"
      manifest_value "$2" "$3"
      ;;
    --test-verify-sha256)
      [ "$#" -eq 3 ] || die "--test-verify-sha256 需要 file 和 sha256"
      verify_sha256 "$2" "$3"
      printf 'ok\n'
      ;;
    --test-detect-platform)
      detect_platform
      ;;
    --test-install-claude-binary)
      [ "$#" -eq 3 ] || die "--test-install-claude-binary 需要 binary 和 version"
      install_claude_binary "$2" "$3"
      locate_claude
      ;;
    --test-register-claude-command)
      register_claude_command
      ;;
    --test-create-desktop-launcher)
      create_desktop_launcher
      ;;
    --test-completion-hint)
      [ "$#" -eq 2 ] || die "--test-completion-hint 需要 0 或 1"
      DESKTOP_LAUNCHER_CREATED="$2"
      print_completion_hint
      ;;
    --test-install-claude-code-existing)
      [ "$#" -eq 2 ] || die "--test-install-claude-code-existing 需要 manifest"
      MANIFEST_FILE="$2"
      ASSUME_YES=1
      install_claude_code
      ;;
    --test-prompt-deepseek-key-capture)
      local key
      key="$(prompt_deepseek_key)"
      printf 'KEY=%s\n' "$key"
      ;;
    --test-check-deepseek-key)
      [ "$#" -eq 3 ] || die "--test-check-deepseek-key 需要 key 和 workdir"
      WORK_DIR="$3"
      mkdir -p "$WORK_DIR"
      check_deepseek_key "$2"
      ;;
    *)
      die "未知测试子命令：${1:-}"
      ;;
  esac
}

main() {
  if [ "${CC_ONECLICK_TEST_MODE:-0}" = "1" ]; then
    main_test_mode "$@"
    return
  fi

  parse_args "$@"
  make_work_dir
  trap cleanup EXIT

  intro
  ensure_macos
  load_manifest
  check_macos_version
  ensure_command_line_tools
  install_claude_code
  register_claude_command
  verify_claude_code
  install_cc_switch
  verify_cc_switch

  local deepseek_key
  deepseek_key="$(prompt_deepseek_key)"
  while ! check_deepseek_key "$deepseek_key"; do
    warn "请确认 Key 有效且账户有 API 余额。"
    deepseek_key="$(prompt_deepseek_key)"
  done

  import_deepseek_provider "$deepseek_key"
  smoke_test_claude_with_deepseek_env "$deepseek_key"
  prompt_desktop_launcher

  print_completion_hint
  launch_claude_if_requested
}

main "$@"
