#!/usr/bin/env bash
# Codex 一键安装脚本(macOS / Linux)
# 用法:curl -fsSL https://aishop.homes/codex/install.sh | bash
#
# 行为:检测环境 -> 选择国内镜像 -> 安装官方 Codex 包 -> 写入 PATH -> codex --version 自检 -> 回传诊断码
# 边界:全程用户级安装,不 sudo、不改系统设置、不上传密钥与用户名。

set -u

SCRIPT_VERSION="1.0.0"
SITE="${CODEX_INSTALL_SITE:-https://aishop.homes}"
MIRROR="${CODEX_INSTALL_MIRROR:-https://registry.npmmirror.com}"
ROUTE="auto"
REPORT_ENABLED="1"
DRY_RUN="0"
REQUESTED_VERSION=""

INSTALL_ROOT="${HOME}/.local"
BIN_DIR="${INSTALL_ROOT}/bin"
TARGET_BIN="${BIN_DIR}/codex"

STATUS="failed"
ERROR_CODE=""
FAILED_STEP=""
ROUTE_USED="unknown"
VERSION_BEFORE=""
VERSION_AFTER=""
REPAIRS=""
LOG_FILE="$(mktemp -t codex-install.XXXXXX)"
START_SECONDS=${SECONDS}

usage() {
  cat <<'EOF'
Codex 一键安装脚本(macOS / Linux)

选项:
  --dry-run            只检测环境,不安装、不上报
  --no-report          安装后不上报安装结果
  --route=auto|npm|tarball   指定安装路线(默认 auto)
  --version=X.Y.Z      指定 Codex 版本(默认取镜像最新版)
  --site=URL           安装结果上报的站点地址
  --mirror=URL         软件包镜像地址
  -h, --help           显示帮助
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN="1" ;;
    --no-report) REPORT_ENABLED="0" ;;
    --route=*) ROUTE="${1#*=}" ;;
    --version=*) REQUESTED_VERSION="${1#*=}" ;;
    --site=*) SITE="${1#*=}" ;;
    --mirror=*) MIRROR="${1#*=}" ;;
    -h|--help) usage; exit 0 ;;
    *) printf '未知参数: %s\n' "$1"; usage; exit 2 ;;
  esac
  shift
done

log() {
  printf '%s\n' "$*"
  printf '%s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true
}

step() {
  log "[$1] $2"
}

add_repair() {
  if [ -z "$REPAIRS" ]; then
    REPAIRS="$1"
  else
    REPAIRS="${REPAIRS},${1}"
  fi
}

fail_with() {
  ERROR_CODE="$1"
  if [ $# -gt 1 ] && [ -n "${2:-}" ]; then
    log "  ✗ $2"
  fi
}

redact() {
  sed -E \
    -e "s#${HOME}#~#g" \
    -e 's/sk-[A-Za-z0-9_-]{6,}/sk-***/g' \
    -e 's/tp-[A-Za-z0-9_-]{6,}/tp-***/g' \
    -e 's/(Bearer )[A-Za-z0-9._-]+/\1***/g' \
    -e 's/((api[_-]?key|token|secret|password)[=:])[^[:space:]]+/\1***/gI' 2>/dev/null || true
}

json_escape() {
  local value="${1-}"
  value="${value//\\/\\\\}"
  value="${value//\"/\\\"}"
  value="${value//$'\r'/}"
  value="${value//$'\n'/\\n}"
  value="${value//$'\t'/\\t}"
  printf '%s' "$value"
}

json_string() {
  printf '"%s"' "$(json_escape "${1-}")"
}

json_string_or_null() {
  if [ -z "${1:-}" ]; then
    printf 'null'
  else
    json_string "$1"
  fi
}

json_number_or_null() {
  if [ -z "${1:-}" ]; then
    printf 'null'
  else
    printf '%s' "$1"
  fi
}

detect_platform() {
  local uname_s uname_m
  uname_s="$(uname -s 2>/dev/null || echo unknown)"
  uname_m="$(uname -m 2>/dev/null || echo unknown)"
  case "$uname_s" in
    Darwin) PLATFORM="darwin" ;;
    Linux) PLATFORM="linux" ;;
    *) PLATFORM="unsupported" ;;
  esac
  case "$uname_m" in
    arm64|aarch64) ARCH="arm64" ;;
    x86_64|amd64) ARCH="x64" ;;
    *) ARCH="unsupported" ;;
  esac
}

os_version() {
  if [ "$PLATFORM" = "darwin" ]; then
    sw_vers -productVersion 2>/dev/null || echo "macOS"
  else
    if [ -r /etc/os-release ]; then
      # shellcheck disable=SC1091
      . /etc/os-release 2>/dev/null
      printf '%s' "${PRETTY_NAME:-Linux}"
    else
      echo "Linux"
    fi
  fi
}

target_triple() {
  case "${PLATFORM}-${ARCH}" in
    darwin-arm64) echo "aarch64-apple-darwin" ;;
    darwin-x64) echo "x86_64-apple-darwin" ;;
    linux-arm64) echo "aarch64-unknown-linux-musl" ;;
    linux-x64) echo "x86_64-unknown-linux-musl" ;;
    *) echo "" ;;
  esac
}

current_codex_version() {
  if command -v codex >/dev/null 2>&1; then
    codex --version 2>/dev/null | head -1
  elif [ -x "$TARGET_BIN" ]; then
    "$TARGET_BIN" --version 2>/dev/null | head -1
  fi
}

node_version_ok() {
  command -v node >/dev/null 2>&1 || return 1
  command -v npm >/dev/null 2>&1 || return 1
  local major
  major="$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/')"
  [ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null
}

check_mirror() {
  curl -fsS --max-time 12 -o /dev/null "${MIRROR}/@openai/codex/latest" 2>>"$LOG_FILE"
}

resolve_version() {
  if [ -n "$REQUESTED_VERSION" ]; then
    printf '%s' "$REQUESTED_VERSION"
    return 0
  fi
  local payload
  payload="$(curl -fsS --max-time 20 "${MIRROR}/@openai/codex/latest" 2>>"$LOG_FILE")" || return 1
  printf '%s' "$payload" | sed -n 's/.*"version":"\([0-9][^"]*\)".*/\1/p' | head -1
}

download_file() {
  local url="$1" output="$2" attempt=1
  while [ "$attempt" -le 3 ]; do
    if [ -s "$output" ]; then
      add_repair "download:resume"
      curl -fL --connect-timeout 15 --max-time 900 -C - -o "$output" "$url" >>"$LOG_FILE" 2>&1 && return 0
    else
      curl -fL --connect-timeout 15 --max-time 900 -o "$output" "$url" >>"$LOG_FILE" 2>&1 && return 0
    fi
    attempt=$((attempt + 1))
    if [ "$attempt" -le 3 ]; then
      add_repair "download:retry-${attempt}"
      step "install-tarball" "下载中断,正在第 ${attempt} 次重试(断点续传)"
      sleep 2
    fi
  done
  return 1
}

install_via_npm() {
  step "install-npm" "使用本机 npm 安装 Codex ${1}(用户级目录)"
  if npm install -g "@openai/codex@${1}" --registry="$MIRROR" --prefix "$INSTALL_ROOT" >>"$LOG_FILE" 2>&1; then
    return 0
  fi
  add_repair "retry:npm-user-prefix"
  step "install-npm" "npm 首次安装失败,清理缓存后用用户级目录重试"
  npm cache clean --force >>"$LOG_FILE" 2>&1 || true
  npm install -g "@openai/codex@${1}" --registry="$MIRROR" --prefix "$INSTALL_ROOT" >>"$LOG_FILE" 2>&1
}

install_via_tarball() {
  local version="$1" triple tmpdir tarball extracted
  triple="$(target_triple)"
  tmpdir="$(mktemp -d -t codex-pkg.XXXXXX)"
  tarball="${tmpdir}/codex.tgz"

  step "install-tarball" "下载免 Node 安装包 codex-${version}-${PLATFORM}-${ARCH}.tgz"
  if ! download_file "${MIRROR}/@openai/codex/-/codex-${version}-${PLATFORM}-${ARCH}.tgz" "$tarball"; then
    rm -rf "$tmpdir"
    fail_with "NET_MIRROR_DOWN" "安装包下载失败"
    return 1
  fi
  extracted="${tmpdir}/package"
  mkdir -p "$extracted"
  if ! tar -xzf "$tarball" -C "$tmpdir" >>"$LOG_FILE" 2>&1; then
    add_repair "retry:extract"
    if ! tar -xzf "$tarball" -C "$tmpdir" >>"$LOG_FILE" 2>&1; then
      rm -rf "$tmpdir"
      fail_with "EXTRACT_FAIL" "安装包解压失败"
      return 1
    fi
  fi

  local candidate=""
  # 官方包布局:package/vendor/<target-triple>/bin/codex
  if [ -n "$triple" ] && [ -f "${extracted}/vendor/${triple}/bin/codex" ]; then
    candidate="${extracted}/vendor/${triple}/bin/codex"
  fi
  if [ -z "$candidate" ]; then
    candidate="$(find "$tmpdir" -type f -name codex -perm -u+x 2>/dev/null | head -1)"
  fi
  if [ -z "$candidate" ]; then
    candidate="$(find "$tmpdir" -type f -name codex 2>/dev/null | head -1)"
  fi
  if [ -z "$candidate" ]; then
    rm -rf "$tmpdir"
    fail_with "EXTRACT_FAIL" "解压后没有找到 codex 可执行文件"
    return 1
  fi

  mkdir -p "$BIN_DIR"
  cp "$candidate" "$TARGET_BIN" >>"$LOG_FILE" 2>&1 || {
    rm -rf "$tmpdir"
    fail_with "NPM_PERM" "无法写入安装目录 ${BIN_DIR}"
    return 1
  }
  chmod +x "$TARGET_BIN" 2>/dev/null || true
  rm -rf "$tmpdir"
  add_repair "install:standalone-tarball"
  return 0
}

ensure_path() {
  case "${SHELL:-}" in
    */zsh) RC_FILE="${HOME}/.zshrc" ;;
    */bash) RC_FILE="${HOME}/.bashrc"; [ -f "${HOME}/.bash_profile" ] && RC_FILE="${HOME}/.bash_profile" ;;
    *) RC_FILE="${HOME}/.profile" ;;
  esac

  case ":${PATH}:" in
    *":${BIN_DIR}:"*) return 0 ;;
  esac

  if [ -f "$RC_FILE" ] && grep -q 'codex-install >>>' "$RC_FILE" 2>/dev/null; then
    PATH="${BIN_DIR}:${PATH}"
    add_repair "path:profile-already-present"
    return 0
  fi

  {
    printf '\n# >>> codex-install >>>\n'
    printf 'export PATH="%s:$PATH"\n' "$BIN_DIR"
    printf '# <<< codex-install <<<\n'
  } >>"$RC_FILE" 2>>"$LOG_FILE" || {
    PATH="${BIN_DIR}:${PATH}"
    fail_with "PATH_NOT_SET" "无法写入 ${RC_FILE},本次会话已临时加入 PATH"
    return 1
  }

  PATH="${BIN_DIR}:${PATH}"
  add_repair "path:profile-written"
  return 0
}

verify_install() {
  local output
  output="$("$TARGET_BIN" --version 2>>"$LOG_FILE" | head -1)" || true
  if [ -n "$output" ]; then
    VERSION_AFTER="$output"
    return 0
  fi
  if command -v codex >/dev/null 2>&1; then
    output="$(codex --version 2>>"$LOG_FILE" | head -1)" || true
    if [ -n "$output" ]; then
      VERSION_AFTER="$output"
      return 0
    fi
  fi
  fail_with "VERIFY_FAILED" "codex --version 没有返回版本号"
  return 1
}

build_report_json() {
  local log_tail
  log_tail="$(tail -c 2000 "$LOG_FILE" 2>/dev/null | redact)"
  local repairs_json="[]"
  if [ -n "$REPAIRS" ]; then
    repairs_json="[$(printf '%s' "$REPAIRS" | awk -F',' '{for (i=1;i<=NF;i++) printf "%s\"%s\"", (i>1?",":""), $i}')]"
  fi
  printf '{'
  printf '"status":%s,' "$(json_string "$STATUS")"
  printf '"code":%s,' "$(json_string_or_null "$ERROR_CODE")"
  printf '"step":%s,' "$(json_string_or_null "$FAILED_STEP")"
  printf '"platform":%s,' "$(json_string "$PLATFORM")"
  printf '"arch":%s,' "$(json_string "$ARCH")"
  printf '"osVersion":%s,' "$(json_string_or_null "$(os_version)")"
  printf '"shell":%s,' "$(json_string_or_null "$(basename "${SHELL:-unknown}")")"
  printf '"scriptVersion":%s,' "$(json_string "sh-${SCRIPT_VERSION}")"
  printf '"route":%s,' "$(json_string "$ROUTE_USED")"
  printf '"versionBefore":%s,' "$(json_string_or_null "$VERSION_BEFORE")"
  printf '"versionAfter":%s,' "$(json_string_or_null "$VERSION_AFTER")"
  printf '"repairs":%s,' "$repairs_json"
  printf '"durationMs":%s,' "$(json_number_or_null "$(( (SECONDS - START_SECONDS) * 1000 ))")"
  printf '"logTail":%s' "$(json_string_or_null "$log_tail")"
  printf '}'
}

build_local_json() {
  local repairs_json="[]"
  if [ -n "$REPAIRS" ]; then
    repairs_json="[$(printf '%s' "$REPAIRS" | awk -F',' '{n=0;for (i=1;i<=NF && n<4;i++){printf "%s\"%s\"", (n>0?",":""), $i; n++}}')]"
  fi
  printf '{'
  printf '"v":1,'
  printf '"s":%s,' "$(json_string "$STATUS")"
  printf '"c":%s,' "$(json_string_or_null "$ERROR_CODE")"
  printf '"st":%s,' "$(json_string_or_null "$FAILED_STEP")"
  printf '"p":%s,' "$(json_string "$PLATFORM")"
  printf '"a":%s,' "$(json_string "$ARCH")"
  printf '"r":%s,' "$(json_string "$ROUTE_USED")"
  printf '"vb":%s,' "$(json_string_or_null "$VERSION_BEFORE")"
  printf '"va":%s,' "$(json_string_or_null "$VERSION_AFTER")"
  printf '"d":%s,' "$(json_number_or_null "$(( (SECONDS - START_SECONDS) * 1000 ))")"
  printf '"t":%s,' "$(date +%s)"
  printf '"rp":%s' "$repairs_json"
  printf '}'
}

make_local_code() {
  printf 'l.%s' "$(printf '%s' "$1" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')"
}

post_report() {
  local payload="$1" response
  response="$(curl -fsS --max-time 20 -X POST "${SITE}/codex/api/report" \
    -H 'content-type: application/json' --data "$payload" 2>>"$LOG_FILE")" || return 1
  printf '%s' "$response" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p' | head -1
}

print_success() {
  log ""
  log "✅ Codex 安装成功"
  log "   版本:     ${VERSION_AFTER}"
  log "   安装位置: ${TARGET_BIN}"
  if [ -n "$DIAG_CODE" ]; then
    log "   诊断码:   ${DIAG_CODE}"
    log "   结果页:   ${SITE}/codex/r/${DIAG_CODE}"
  fi
  log "   下一步:   新开一个终端,执行 codex --version"
  log ""
}

print_failure() {
  log ""
  log "❌ 安装失败"
  log "   失败步骤: ${FAILED_STEP:-unknown}"
  log "   错误码:   ${ERROR_CODE:-UNKNOWN}"
  if [ -n "$REPAIRS" ]; then
    log "   已尝试修复: ${REPAIRS}"
  fi
  if [ -n "$DIAG_CODE" ]; then
    log "   诊断码:   ${DIAG_CODE}"
    log "   结果页:   ${SITE}/codex/r/${DIAG_CODE}"
  fi
  log "   售后:     把上面的诊断码发给我们即可定位原因"
  log ""
}

trap 'rm -f "$LOG_FILE" 2>/dev/null || true' EXIT

detect_platform
step "detect" "系统 ${PLATFORM}/${ARCH},版本 $(os_version),shell ${SHELL:-unknown}"
VERSION_BEFORE="$(current_codex_version)"
if [ -n "$VERSION_BEFORE" ]; then
  step "detect" "检测到已安装:${VERSION_BEFORE}"
fi

if [ "$PLATFORM" = "unsupported" ] || [ "$ARCH" = "unsupported" ]; then
  FAILED_STEP="detect"
  fail_with "ENV_UNSUPPORTED" "当前系统或架构不在支持范围内(${PLATFORM}/${ARCH})"
  if [ "$REPORT_ENABLED" = "1" ]; then
    DIAG_CODE="$(make_local_code "$(build_local_json)")"
  fi
  print_failure
  exit 1
fi

if [ "$DRY_RUN" = "1" ]; then
  step "detect" "镜像连通性检测:${MIRROR}"
  if check_mirror; then
    log "  ✓ 镜像可达"
  else
    log "  ✗ 镜像不可达(错误码 NET_MIRROR_DOWN)"
  fi
  log "已安装版本:${VERSION_BEFORE:-无}"
  if node_version_ok; then
    log "Node: $(node -v 2>/dev/null)"
  else
    log "Node: 未安装(将使用免 Node 安装包)"
  fi
  log "检测完成(--dry-run,未安装任何东西)"
  exit 0
fi

step "resolve-version" "解析镜像上的最新 Codex 版本"
VERSION="$(resolve_version || true)"
if [ -z "$VERSION" ]; then
  FAILED_STEP="resolve-version"
  if check_mirror; then
    fail_with "UNKNOWN" "镜像可达但无法解析版本号"
  else
    fail_with "NET_MIRROR_DOWN" "无法连接镜像 ${MIRROR}"
  fi
else
  step "resolve-version" "目标版本:${VERSION}"
fi

if [ -z "$ERROR_CODE" ]; then
  case "$ROUTE" in
    npm)
      FAILED_STEP="install-npm"
      ROUTE_USED="npm"
      install_via_npm "$VERSION" || true
      ;;
    tarball)
      FAILED_STEP="install-tarball"
      ROUTE_USED="tarball"
      install_via_tarball "$VERSION" || true
      ;;
    *)
      if node_version_ok; then
        FAILED_STEP="install-npm"
        ROUTE_USED="npm"
        if ! install_via_npm "$VERSION"; then
          ERROR_CODE=""
          add_repair "fallback:npm-to-tarball"
          step "install-tarball" "npm 路线失败,自动切换免 Node 安装包"
          FAILED_STEP="install-tarball"
          ROUTE_USED="tarball"
          install_via_tarball "$VERSION" || true
        fi
      else
        FAILED_STEP="install-tarball"
        ROUTE_USED="tarball"
        install_via_tarball "$VERSION" || true
      fi
      ;;
  esac
fi

if [ -z "$ERROR_CODE" ]; then
  ensure_path || true
fi

if [ -z "$ERROR_CODE" ]; then
  step "verify" "执行 codex --version 自检"
  if verify_install; then
    STATUS="success"
    ERROR_CODE="OK"
  else
    FAILED_STEP="verify"
  fi
fi

DIAG_CODE=""
if [ "$REPORT_ENABLED" = "1" ]; then
  REPORT_JSON="$(build_report_json)"
  DIAG_CODE="$(post_report "$REPORT_JSON" || true)"
fi
if [ -z "$DIAG_CODE" ]; then
  DIAG_CODE="$(make_local_code "$(build_local_json)")"
fi

if [ "$STATUS" = "success" ]; then
  print_success
  exit 0
fi

[ -z "$ERROR_CODE" ] && ERROR_CODE="UNKNOWN"
print_failure
exit 1
