#!/usr/bin/env bash
# ChatGPT 桌面版安装脚本(macOS)
# 用法:curl -fsSL https://aishop.homes/chatgpt/install.sh | bash
#
# 行为:检测环境 → 从官方 CDN 下载 dmg → 挂载 → 退出正在运行的 App → 安装到「应用程序」
#       → 校验代码签名 → 去掉下载隔离属性 → 读取版本号自检 → 回传诊断码
# 说明:安装的是 OpenAI 官方当前版本的 ChatGPT 桌面版(包标识 com.openai.codex,约 650MB);
#       已装版本不低于官方最新版本时会自动跳过下载。
# 边界:不申请管理员权限(仅写入用户可写的 /Applications 或回落到 ~/Applications)、
#       不修改系统代理与证书、不上传密钥与用户名。

set -u

SCRIPT_VERSION="1.0.0"
SITE="${CHATGPT_INSTALL_SITE:-${CODEX_INSTALL_SITE:-https://aishop.homes}}"
DMG_URL="${CHATGPT_DMG_URL:-https://persistent.oaistatic.com/codex-app-prod/Codex.dmg}"
APPCAST_URL="${CHATGPT_APPCAST_URL:-https://persistent.oaistatic.com/codex-app-prod/appcast.xml}"
EXPECTED_BUNDLE_ID="com.openai.codex"
REPORT_ENABLED="1"
DRY_RUN="0"
LAUNCH_APP="1"
KEEP_QUARANTINE="0"
TARGET_DIR=""
TARGET_EXPLICIT="0"
APP_NAME="ChatGPT.app"

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

usage() {
  cat <<'EOF'
ChatGPT 桌面版安装脚本(macOS)

选项:
  --dry-run              只检测环境,不下载、不安装、不上报
  --no-report            安装后不上报安装结果
  --no-launch            安装完成后不自动打开应用
  --keep-quarantine      保留系统下载隔离提示(不去掉 quarantine 属性)
  --target-dir=<目录>    指定安装位置(默认 /Applications,不可写时回落 ~/Applications;
                         显式指定时不会退出正在运行的 ChatGPT)
  --dmg=<url>            指定安装镜像地址(默认官方 CDN)
  --site=<url>           安装结果上报的站点地址
  -h, --help             显示帮助
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
    --dry-run) DRY_RUN="1" ;;
    --no-report) REPORT_ENABLED="0" ;;
    --no-launch) LAUNCH_APP="0" ;;
    --keep-quarantine) KEEP_QUARANTINE="1" ;;
    --target-dir=*) TARGET_DIR="${1#*=}"; TARGET_EXPLICIT="1" ;;
    --dmg=*) DMG_URL="${1#*=}" ;;
    --site=*) SITE="${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' 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
}

os_version() {
  sw_vers -productVersion 2>/dev/null || echo "macOS"
}

current_app_path() {
  if [ -n "$TARGET_DIR" ] && [ -d "${TARGET_DIR}/${APP_NAME}" ]; then
    printf '%s' "${TARGET_DIR}/${APP_NAME}"
    return 0
  fi
  if [ -d "/Applications/${APP_NAME}" ]; then
    printf '%s' "/Applications/${APP_NAME}"
    return 0
  fi
  if [ -d "${HOME}/Applications/${APP_NAME}" ]; then
    printf '%s' "${HOME}/Applications/${APP_NAME}"
    return 0
  fi
  return 1
}

app_version() {
  local app_path="$1"
  defaults read "${app_path}/Contents/Info.plist" CFBundleShortVersionString 2>/dev/null \
    || plutil -extract CFBundleShortVersionString raw "${app_path}/Contents/Info.plist" 2>/dev/null
}

app_bundle_id() {
  local app_path="$1"
  defaults read "${app_path}/Contents/Info.plist" CFBundleIdentifier 2>/dev/null \
    || plutil -extract CFBundleIdentifier raw "${app_path}/Contents/Info.plist" 2>/dev/null
}

latest_version() {
  curl -fsS --max-time 20 "$APPCAST_URL" 2>>"$LOG_FILE" \
    | sed -n 's/.*<sparkle:shortVersionString>\([^<]*\)<\/sparkle:shortVersionString>.*/\1/p' \
    | head -1
}

# 版本号逐段数值比较:a >= b 返回 0
version_ge() {
  awk -v a="$1" -v b="$2" 'BEGIN{
    n = split(a, x, "."); m = split(b, y, ".");
    limit = (n > m) ? n : m;
    for (i = 1; i <= limit; i++) {
      av = (i <= n) ? x[i] + 0 : 0;
      bv = (i <= m) ? y[i] + 0 : 0;
      if (av > bv) { exit 0 }
      if (av < bv) { exit 1 }
    }
    exit 0
  }'
}

app_is_running() {
  pgrep -f "${APP_NAME}/Contents/MacOS/" >/dev/null 2>&1
}

quit_app() {
  osascript -e 'tell application "ChatGPT" to quit' >/dev/null 2>&1 || true
  local waited=0
  while [ "$waited" -lt 10 ]; do
    app_is_running || return 0
    sleep 1
    waited=$((waited + 1))
  done
  app_is_running && return 1
  return 0
}

download_dmg() {
  local output="$1" attempt=1
  while [ "$attempt" -le 3 ]; do
    if [ -s "$output" ]; then
      add_repair "download:resume"
      step "download" "继续上次未完成的下载(断点续传)"
      curl -fL --progress-bar --connect-timeout 15 --max-time 1800 -C - -o "$output" "$DMG_URL" && return 0
    else
      curl -fL --progress-bar --connect-timeout 15 --max-time 1800 -o "$output" "$DMG_URL" && return 0
    fi
    log "  ✗ 第 ${attempt} 次下载未完成(详见上方 curl 输出)"
    attempt=$((attempt + 1))
    if [ "$attempt" -le 3 ]; then
      add_repair "download:retry-${attempt}"
      step "download" "下载中断,正在第 ${attempt} 次重试(断点续传)"
      sleep 2
    fi
  done
  return 1
}

build_report_json() {
  local log_tail repairs_json="[]"
  log_tail="$(tail -c 2000 "$LOG_FILE" 2>/dev/null | redact)"
  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 '"product":"chatgpt",'
  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 "darwin")"
  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 "chatgpt-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,' "$(( (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 '"pr":"chatgpt",'
  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":"darwin",'
  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,' "$(( (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}/install/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 "✅ ChatGPT 桌面版安装成功"
  log "   版本:     ${VERSION_AFTER}"
  log "   安装位置: ${INSTALLED_PATH}"
  if [ -n "$DIAG_CODE" ]; then
    log "   诊断码:   ${DIAG_CODE}"
    log "   结果页:   ${SITE}/codex/r/${DIAG_CODE}"
  fi
  log "   下一步:   打开 ChatGPT 登录账号;界面语言与中文回复设置见 ${SITE}/codex#chatgpt"
  log ""
}

print_failure() {
  log ""
  log "❌ ChatGPT 桌面版安装失败"
  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

case "$(uname -s 2>/dev/null || echo unknown)" in
  Darwin) ;;
  *)
    ARCH="unsupported"
    FAILED_STEP="detect"
    fail_with "ENV_UNSUPPORTED" "此脚本只支持 macOS;Windows 请使用 /chatgpt/install.ps1"
    if [ "$REPORT_ENABLED" = "1" ]; then
      DIAG_CODE="$(make_local_code "$(build_local_json)")"
    fi
    print_failure
    exit 1
    ;;
esac

case "$(uname -m 2>/dev/null || echo unknown)" in
  arm64|aarch64) ARCH="arm64" ;;
  x86_64|amd64) ARCH="x64" ;;
  *) ARCH="unsupported" ;;
esac

step "detect" "系统 macOS $(os_version),架构 ${ARCH},shell ${SHELL:-unknown}"
if EXISTING="$(current_app_path)"; then
  INSTALLED_PATH="$EXISTING"
  VERSION_BEFORE="$(app_version "$EXISTING")"
  EXISTING_BUNDLE_ID="$(app_bundle_id "$EXISTING")"
  if [ -n "$EXISTING_BUNDLE_ID" ] && [ "$EXISTING_BUNDLE_ID" != "$EXPECTED_BUNDLE_ID" ]; then
    add_repair "upgrade:legacy-app(${EXISTING_BUNDLE_ID})"
  fi
  step "detect" "检测到已安装:${EXISTING} ${VERSION_BEFORE:-版本未知}(${EXISTING_BUNDLE_ID:-未知包标识})"
fi

if [ "$ARCH" = "unsupported" ]; then
  FAILED_STEP="detect"
  fail_with "ENV_UNSUPPORTED" "当前 CPU 架构不在支持范围内"
  if [ "$REPORT_ENABLED" = "1" ]; then
    DIAG_CODE="$(make_local_code "$(build_local_json)")"
  fi
  print_failure
  exit 1
fi

# 目标目录:显式指定优先;否则 /Applications 可写就用它,不可写回落 ~/Applications
if [ -z "$TARGET_DIR" ]; then
  if [ -w "/Applications" ]; then
    TARGET_DIR="/Applications"
  else
    TARGET_DIR="${HOME}/Applications"
    add_repair "target:user-applications"
  fi
fi

if [ "$DRY_RUN" = "1" ]; then
  step "detect" "下载地址连通性检测:${DMG_URL}"
  if curl -fsS --max-time 12 -r 0-100 -o /dev/null "$DMG_URL" 2>>"$LOG_FILE"; then
    log "  ✓ 官方下载地址可达"
  else
    log "  ✗ 下载地址不可达(错误码 DMG_DOWNLOAD_FAILED)"
  fi
  DRY_LATEST="$(latest_version || true)"
  if [ -n "$DRY_LATEST" ]; then
    log "官方最新版本:${DRY_LATEST}"
    if [ -n "$VERSION_BEFORE" ] && version_ge "$VERSION_BEFORE" "$DRY_LATEST"; then
      log "  ✓ 本机已是最新,正式运行时会跳过约 650MB 下载"
    else
      log "  → 本机需要更新,正式运行时会下载约 650MB 镜像"
    fi
  else
    log "官方最新版本:读取失败(不影响安装,脚本会直接下载镜像)"
  fi
  log "安装目录:${TARGET_DIR}(可写:$([ -w "$TARGET_DIR" ] || [ -w "$(dirname "$TARGET_DIR")" ] && echo 是 || echo 否))"
  log "磁盘可用:$(df -h "$HOME" | tail -1 | awk '{print $4}')"
  log "已安装版本:${VERSION_BEFORE:-无}"
  log "检测完成(--dry-run,未下载也未安装)"
  exit 0
fi

# 已装版本不低于官方最新版本时,直接跳过约 650MB 的镜像下载
LATEST_VERSION="$(latest_version || true)"
if [ -n "$LATEST_VERSION" ]; then
  step "check" "官方最新版本:${LATEST_VERSION}"
fi
if [ -n "$VERSION_BEFORE" ] && [ -n "$LATEST_VERSION" ] && version_ge "$VERSION_BEFORE" "$LATEST_VERSION"; then
  step "check" "已安装 ${VERSION_BEFORE},已经是官方最新版本,无需重新下载"
  add_repair "skip:already-latest"
  VERSION_AFTER="$VERSION_BEFORE"
  STATUS="success"
  ERROR_CODE="OK"
fi

WORK_DIR="$(mktemp -d -t chatgpt-install.XXXXXX)"
DMG_FILE="${WORK_DIR}/ChatGPT.dmg"
MOUNT_POINT="${WORK_DIR}/mnt"
mkdir -p "$MOUNT_POINT"

if [ "$STATUS" != "success" ]; then
  step "download" "从官方 CDN 下载 ChatGPT 安装镜像(约 650MB,视网速需要 1-3 分钟)"
  if ! download_dmg "$DMG_FILE"; then
    FAILED_STEP="download"
    fail_with "DMG_DOWNLOAD_FAILED" "下载失败:${DMG_URL}"
  fi

  if [ -z "$ERROR_CODE" ]; then
    DMG_SIZE="$(stat -f%z "$DMG_FILE" 2>/dev/null || echo 0)"
    if [ "$DMG_SIZE" -lt 200000000 ]; then
      FAILED_STEP="download"
      fail_with "DMG_DOWNLOAD_FAILED" "下载文件不完整(${DMG_SIZE} 字节)"
    fi
  fi
fi

if [ -z "$ERROR_CODE" ]; then
  step "mount" "挂载安装镜像"
  if ! hdiutil attach -nobrowse -quiet -mountpoint "$MOUNT_POINT" "$DMG_FILE" >>"$LOG_FILE" 2>&1; then
    add_repair "mount:retry"
    sleep 2
    if ! hdiutil attach -nobrowse -quiet -mountpoint "$MOUNT_POINT" "$DMG_FILE" >>"$LOG_FILE" 2>&1; then
      FAILED_STEP="mount"
      fail_with "DMG_MOUNT_FAILED" "hdiutil 挂载失败"
    fi
  fi
fi

SOURCE_APP=""
if [ -z "$ERROR_CODE" ]; then
  SOURCE_APP="$(find "$MOUNT_POINT" -maxdepth 2 -name "$APP_NAME" -type d 2>/dev/null | head -1)"
  if [ -z "$SOURCE_APP" ]; then
    SOURCE_APP="$(find "$MOUNT_POINT" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)"
  fi
  if [ -z "$SOURCE_APP" ]; then
    FAILED_STEP="mount"
    fail_with "DMG_MOUNT_FAILED" "镜像里没有找到可安装的应用"
  fi
fi

if [ -z "$ERROR_CODE" ]; then
  SOURCE_BUNDLE_ID="$(app_bundle_id "$SOURCE_APP")"
  if [ "$SOURCE_BUNDLE_ID" != "$EXPECTED_BUNDLE_ID" ]; then
    FAILED_STEP="mount"
    fail_with "UNEXPECTED_APP" "镜像内的应用不是预期的 ChatGPT 桌面版(${SOURCE_BUNDLE_ID:-未知包标识})"
  else
    step "mount" "镜像内应用:${SOURCE_APP##*/} ${SOURCE_BUNDLE_ID} $(app_version "$SOURCE_APP")"
  fi
fi

if [ -z "$ERROR_CODE" ] && [ "$TARGET_EXPLICIT" = "0" ] && app_is_running; then
  step "prepare" "退出正在运行的 ChatGPT"
  if quit_app; then
    add_repair "app:quit-before-install"
  else
    FAILED_STEP="prepare"
    fail_with "APP_RUNNING" "ChatGPT 仍在运行,请手动退出后重试"
  fi
fi

if [ -z "$ERROR_CODE" ]; then
  step "install" "安装到 ${TARGET_DIR}/${APP_NAME}"
  mkdir -p "$TARGET_DIR" 2>>"$LOG_FILE" || true
  if ! ditto "$SOURCE_APP" "${TARGET_DIR}/${APP_NAME}" >>"$LOG_FILE" 2>&1; then
    if [ "$TARGET_DIR" = "/Applications" ]; then
      add_repair "target:user-applications"
      TARGET_DIR="${HOME}/Applications"
      mkdir -p "$TARGET_DIR" 2>>"$LOG_FILE" || true
    fi
    if ! ditto "$SOURCE_APP" "${TARGET_DIR}/${APP_NAME}" >>"$LOG_FILE" 2>&1; then
      FAILED_STEP="install"
      fail_with "INSTALL_DIR_NOT_WRITABLE" "写入 ${TARGET_DIR} 失败"
    fi
  fi
  INSTALLED_PATH="${TARGET_DIR}/${APP_NAME}"
fi

if [ -n "$SOURCE_APP" ]; then
  hdiutil detach "$MOUNT_POINT" >>"$LOG_FILE" 2>&1 || hdiutil detach "$MOUNT_POINT" -force >>"$LOG_FILE" 2>&1 || true
fi

if [ -z "$ERROR_CODE" ]; then
  step "verify" "校验代码签名"
  if codesign --verify --deep --strict "$INSTALLED_PATH" >>"$LOG_FILE" 2>&1; then
    SIGNER="$(codesign -dv --verbose=2 "$INSTALLED_PATH" 2>&1 | grep -m1 'Authority=' | sed 's/^Authority=//')"
    step "verify" "签名有效:${SIGNER:-未知签发者}"
  else
    FAILED_STEP="verify"
    fail_with "SIGNATURE_INVALID" "代码签名校验未通过,已放弃安装"
  fi
fi

if [ -z "$ERROR_CODE" ] && [ "$KEEP_QUARANTINE" = "0" ]; then
  if xattr -dr com.apple.quarantine "$INSTALLED_PATH" >>"$LOG_FILE" 2>&1; then
    add_repair "quarantine:removed"
  fi
fi

if [ -z "$ERROR_CODE" ]; then
  VERSION_AFTER="$(app_version "$INSTALLED_PATH")"
  if [ -z "$VERSION_AFTER" ]; then
    FAILED_STEP="verify"
    fail_with "VERIFY_FAILED" "读不到应用版本号"
  fi
fi

if [ -z "$ERROR_CODE" ]; then
  STATUS="success"
  ERROR_CODE="OK"
fi

rm -rf "$WORK_DIR" 2>/dev/null || true

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

if [ "$STATUS" = "success" ]; then
  print_success
  if [ "$LAUNCH_APP" = "1" ]; then
    open -a "$INSTALLED_PATH" >/dev/null 2>&1 || true
  fi
  exit 0
fi

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