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

# codex-mac-setup.sh -- one-shot macOS installer for Codex Desktop:
# downloads & installs the Codex desktop app, then auto-configures the
# VC model provider + API key. (Codex CLI is intentionally NOT installed.)
#
#   1. config : ~/.codex/config.toml (VC provider) + ~/.codex/auth.json (key)
#   2. app    : official Codex.dmg from OpenAI's CDN -> /Applications
#
#   provider : VC        (base_url https://td.geeknow.top/v1, wire_api responses)
#   model    : gpt-5.5
#   auth     : OPENAI_API_KEY in ~/.codex/auth.json  +  requires_openai_auth = true
#
# macOS only, works with stock /bin/bash 3.2. No python/jq/node required.
# Sibling scripts (config-only):
#   codex-vc-setup.sh   (macOS / Linux / Git Bash)
#   codex-vc-setup.ps1  (Windows-native)
#
# Official download sources used (verified 2026-07-15):
#   The download buttons on OpenAI's official Codex app page
#     https://developers.openai.com/codex/app
#   link to these URLs on OpenAI's static CDN (persistent.oaistatic.com).
#   NOTE: the app was renamed "Codex Desktop" -> "ChatGPT Desktop"
#   (dmg ships ChatGPT.app, bundle id is still com.openai.codex):
#   App (arm64): https://persistent.oaistatic.com/codex-app-prod/ChatGPT.dmg
#   App (intel): https://persistent.oaistatic.com/codex-app-prod/ChatGPT-latest-x64.dmg

DEFAULT_BASE_URL="https://td.geeknow.top/v1"
DEFAULT_MODEL="gpt-5.5"
PROVIDER_NAME="VC"
MARKER_START="# >>> codex-vc-provider >>>"
MARKER_END="# <<< codex-vc-provider <<<"

# Official release URLs. Since 2026-07 the desktop app is "ChatGPT Desktop"
# (was "Codex Desktop"); ChatGPT.* are the canonical dmg names. The legacy
# Codex.dmg / Codex-latest-x64.dmg URLs serve the same renamed builds.
APP_DMG_URL_ARM64="https://persistent.oaistatic.com/codex-app-prod/ChatGPT.dmg"
APP_DMG_URL_X64="https://persistent.oaistatic.com/codex-app-prod/ChatGPT-latest-x64.dmg"

print_usage() {
  cat <<'USAGE'
One-shot macOS setup for Codex Desktop: download, install, configure.
Stock macOS bash (3.2+) only; no python/jq/node needed.
The Codex CLI is intentionally NOT installed.

Both steps run by default:
  1. config  Smoke-tests the API key, then writes ~/.codex/config.toml
             (VC provider) and ~/.codex/auth.json (OPENAI_API_KEY).
             Existing auth.json fields (e.g. ChatGPT OAuth) are preserved.
  2. app     Downloads the official dmg from OpenAI's CDN and installs the
             desktop app (renamed ChatGPT.app, bundle id com.openai.codex)
             into /Applications (or ~/Applications).

Usage:
  bash codex-mac-setup.sh [options]

Options:
  --config-only   Only step 1 (same behavior as codex-vc-setup.sh).
  --app-only      Only step 2.
  --no-config     Skip step 1.
  --no-app        Skip step 2.
  --force         Reinstall the app even if already present.
  --no-smoke      Do not smoke-test the API key before writing config.
  --smoke-only    Only smoke-test the key against the provider; change nothing.
  --restore       Restore the first pre-install config.toml backup, then exit.
  --help          Show this help.

Optional environment variables:
  OPENAI_API_KEY     API key (falls back to ~/.codex/auth.json, then prompt).
  VC_BASE_URL        Defaults to https://td.geeknow.top/v1
  VC_MODEL           Defaults to gpt-5.5
  CODEX_HOME         Defaults to ~/.codex
  CODEX_APP_DMG_URL  Override the desktop app DMG download URL.
USAGE
}

require_command() {
  local command_name="$1"
  if ! command -v "$command_name" >/dev/null 2>&1; then
    echo "Missing required command: $command_name" >&2
    exit 1
  fi
}

escape_double_quoted_value() {
  printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# Print the existing OPENAI_API_KEY from auth.json (empty string if none).
# Pure sed -- handles pretty-printed or minified JSON.
read_auth_key() {
  local auth_file="$1"
  if [[ -f "$auth_file" ]]; then
    sed -n 's/.*"OPENAI_API_KEY"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$auth_file" | head -n 1
  fi
}

# Merge the key into auth.json, preserving any other fields (tokens, etc.).
# Pure awk, operating on the whole file as one string so single-line and
# multi-line JSON are both handled. Creates the file if missing/empty.
write_auth_key() {
  local auth_file="$1" key="$2"
  mkdir -p "$(dirname "$auth_file")"
  local tmp="${auth_file}.tmp"
  awk -v file="$auth_file" -v key="$key" '
    BEGIN {
      s = ""
      while ((getline l < file) > 0) s = s l "\n"
      close(file)
      if (match(s, /"OPENAI_API_KEY"[[:space:]]*:[[:space:]]*"[^"]*"/)) {
        # key already present -> replace its value in place
        s = substr(s, 1, RSTART-1) "\"OPENAI_API_KEY\": \"" key "\"" substr(s, RSTART+RLENGTH)
      } else {
        p = index(s, "{")
        if (p == 0) {
          # no JSON object at all -> write a fresh one
          s = "{\n  \"OPENAI_API_KEY\": \"" key "\"\n}\n"
        } else {
          rest = substr(s, p+1)
          t = rest
          sub(/^[[:space:]]+/, "", t)
          if (substr(t, 1, 1) == "}") {
            # empty object {} -> insert without a trailing comma
            s = substr(s, 1, p) "\n  \"OPENAI_API_KEY\": \"" key "\"\n" substr(s, p+1)
          } else {
            # non-empty object -> insert as first field with a comma
            s = substr(s, 1, p) "\n  \"OPENAI_API_KEY\": \"" key "\"," substr(s, p+1)
          }
        }
      }
      printf "%s", s
    }
  ' > "$tmp" && mv "$tmp" "$auth_file"
  chmod 600 "$auth_file"
}

read_api_key() {
  local auth_file="$1"
  # Priority: explicit OPENAI_API_KEY env override -> existing auth.json -> prompt.
  local api_key="${OPENAI_API_KEY:-}"
  if [[ -z "$api_key" ]]; then
    api_key="$(read_auth_key "$auth_file")"
  fi
  if [[ -n "$api_key" ]]; then
    printf '%s' "$api_key"
    return 0
  fi
  if [[ -t 0 ]]; then
    printf 'Enter your API key (input hidden): ' >&2
    stty -echo 2>/dev/null || true
    read -r api_key
    stty echo 2>/dev/null || true
    printf '\n' >&2
    printf '%s' "$api_key"
    return 0
  fi
  echo "No API key found. Set OPENAI_API_KEY, add it to $auth_file, or run interactively." >&2
  exit 1
}

smoke_test_key() {
  local api_key="$1" base_url="$2" model="$3"
  local tmp_dir curl_auth_config response_body status
  tmp_dir="$(mktemp -d)"
  trap "rm -rf '$tmp_dir'" RETURN
  curl_auth_config="$tmp_dir/curl-auth.conf"
  response_body="$tmp_dir/response.json"
  umask 077
  printf 'header = "Authorization: Bearer %s"\n' "$api_key" > "$curl_auth_config"
  status="$(curl -sS -m 180 -o "$response_body" -w '%{http_code}' \
    -q --config "$curl_auth_config" \
    -H 'Content-Type: application/json' \
    -H 'User-Agent: Codex Desktop/vc-install-smoke' \
    --data-binary @- "$base_url/responses" <<JSON
{"model":"$model","input":"Reply with only: connected","max_output_tokens":32,"stream":false}
JSON
)"
  if [[ "$status" != "200" ]]; then
    echo "Provider /responses smoke failed: HTTP $status" >&2
    head -c 1000 "$response_body" >&2 || true
    echo >&2
    echo "No Codex config was changed." >&2
    return 1
  fi
  rm -rf "$tmp_dir"
  trap - RETURN
  echo "Provider API smoke passed for base URL: $base_url"
}

write_restore_script() {
  local restore_file="$1" codex_home="$2"
  cat > "$restore_file" <<RESTORE
#!/usr/bin/env bash
set -euo pipefail
codex_home="$(escape_double_quoted_value "$codex_home")"
backup_dir="\$codex_home/backups"
original_backup="\$backup_dir/config.toml.pre-vc-original.bak"
if [[ ! -f "\$original_backup" ]]; then
  echo "No pre-install backup found in \$backup_dir" >&2
  exit 1
fi
cp "\$original_backup" "\$codex_home/config.toml"
chmod 600 "\$codex_home/config.toml"
echo "Restored Codex config from: \$original_backup"
echo "Note: ~/.codex/auth.json was left untouched."
echo "Restart Codex Desktop after restore."
RESTORE
  chmod 700 "$restore_file"
}

restore_config() {
  local codex_home="$1"
  local backup_dir="$codex_home/backups"
  local original_backup="$backup_dir/config.toml.pre-vc-original.bak"
  if [[ ! -f "$original_backup" ]]; then
    echo "No pre-install backup found in $backup_dir" >&2
    exit 1
  fi
  cp "$original_backup" "$codex_home/config.toml"
  chmod 600 "$codex_home/config.toml"
  echo "Restored Codex config from: $original_backup"
  echo "Note: ~/.codex/auth.json was left untouched."
  echo "Restart Codex Desktop after restore."
}

# Rewrite config.toml: remove any managed marker block + any pre-existing
# [model_providers.VC] table, upsert top-level model/model_provider, then append
# one fresh provider block. Pure awk (line-oriented; TOML tables are line-based).
update_codex_config() {
  local config_file="$1" provider_name="$2" base_url="$3" model="$4"
  local marker_start="$5" marker_end="$6"
  local tmp="${config_file}.tmp"
  awk -v provider="$provider_name" -v base_url="$base_url" -v model="$model" \
      -v mstart="$marker_start" -v mend="$marker_end" '
    {
      line = $0
      # 1) skip a previously-managed marker block
      if (index(line, mstart) > 0) { inmarker = 1; next }
      if (inmarker) { if (index(line, mend) > 0) inmarker = 0; next }
      # 2) skip a pre-existing [model_providers.<name>] table (and sub-tables)
      s = line; gsub(/^[[:space:]]+/, "", s)
      if (substr(s, 1, 1) == "[") {
        body = substr(s, 2)
        prefix = "model_providers." provider
        if (substr(body, 1, length(prefix)) == prefix) {
          c = substr(body, length(prefix)+1, 1)
          if (c == "]" || c == ".") { skip = 1; next }
        }
        skip = 0
      }
      if (skip) next
      lines[++n] = line
    }
    END {
      # trim trailing blank/whitespace-only lines
      while (n > 0 && lines[n] ~ /^[[:space:]]*$/) n--
      # locate first table header
      first = n + 1
      for (i = 1; i <= n; i++) {
        t = lines[i]; gsub(/^[[:space:]]+/, "", t)
        if (substr(t, 1, 1) == "[") { first = i; break }
      }
      # upsert top-level model / model_provider in the header region
      hm = 0; hp = 0
      for (i = 1; i < first; i++) {
        if (lines[i] ~ /^[[:space:]]*model[[:space:]]*=/) {
          if (!hm) { lines[i] = "model = \"" model "\""; hm = 1 } else del[i] = 1
        } else if (lines[i] ~ /^[[:space:]]*model_provider[[:space:]]*=/) {
          if (!hp) { lines[i] = "model_provider = \"" provider "\""; hp = 1 } else del[i] = 1
        }
      }
      if (!hm) printf "model = \"%s\"\n", model
      if (!hp) printf "model_provider = \"%s\"\n", provider
      for (i = 1; i <= n; i++) { if (del[i]) continue; printf "%s\n", lines[i] }
      # append one fresh provider block
      printf "\n"
      printf "%s\n", mstart
      printf "# VC provider (auto-configured). API key is read from ~/.codex/auth.json.\n"
      printf "[model_providers.%s]\n", provider
      printf "base_url = \"%s\"\n", base_url
      printf "name = \"%s\"\n", provider
      printf "requires_openai_auth = true\n"
      printf "wire_api = \"responses\"\n"
      printf "%s\n", mend
    }
  ' "$config_file" > "$tmp" && mv "$tmp" "$config_file"
  chmod 600 "$config_file"
}

# --- step 1: config ----------------------------------------------------------
run_config() {
  local codex_home="$1" api_key="$2" base_url="$3" model="$4" run_smoke="$5"
  local config_file="$codex_home/config.toml"
  local auth_file="$codex_home/auth.json"
  local backup_dir="$codex_home/backups"
  local bin_dir="$codex_home/bin"
  local restore_file="$bin_dir/vc-provider-restore.sh"
  local original_backup="$backup_dir/config.toml.pre-vc-original.bak"
  local timestamp
  if [[ "$run_smoke" == "true" ]]; then
    echo "Testing API key before changing Codex config..."
    smoke_test_key "$api_key" "$base_url" "$model"
  fi
  timestamp="$(date +%Y%m%d%H%M%S)"
  mkdir -p "$codex_home" "$backup_dir" "$bin_dir"
  touch "$config_file"
  chmod 600 "$config_file"
  if [[ ! -f "$original_backup" ]]; then
    cp "$config_file" "$original_backup"
    chmod 600 "$original_backup"
  fi
  cp "$config_file" "$backup_dir/config.toml.pre-vc-$timestamp.bak"
  chmod 600 "$backup_dir/config.toml.pre-vc-$timestamp.bak"
  write_auth_key "$auth_file" "$api_key"
  write_restore_script "$restore_file" "$codex_home"
  update_codex_config "$config_file" "$PROVIDER_NAME" "$base_url" "$model" "$MARKER_START" "$MARKER_END"
  cat <<SUMMARY
VC provider installed and API key configured.

Files written:
  $config_file   (model_provider = "VC", model = "$model")
  $auth_file     (OPENAI_API_KEY set; other fields preserved)

Backup:
  $original_backup

Manual rollback:
  bash $restore_file
SUMMARY
}

# --- step 2: Codex desktop app ----------------------------------------------
# NOTE (2026-07-15): OpenAI renamed the desktop app "Codex Desktop" ->
# "ChatGPT Desktop". The official dmg now ships ChatGPT.app (bundle id is
# still com.openai.codex, still signed by OpenAI OpCo). App detection below
# is name-agnostic so a future rename cannot break it again.
install_app() {
  local force="$1"
  local arch url target_dir
  arch="$(uname -m)"
  case "$arch" in
    arm64)  url="${CODEX_APP_DMG_URL:-$APP_DMG_URL_ARM64}" ;;
    x86_64) url="${CODEX_APP_DMG_URL:-$APP_DMG_URL_X64}" ;;
    *) echo "Unsupported Mac architecture for the desktop app: $arch" >&2; return 1 ;;
  esac

  local macos_major
  macos_major="$(sw_vers -productVersion 2>/dev/null | cut -d. -f1)"
  if [[ -n "$macos_major" && "$macos_major" -lt 13 && "$force" != "true" ]]; then
    echo "Codex desktop app requires macOS 13+; this Mac runs $(sw_vers -productVersion). Skipping app install." >&2
    echo "(use --force to install anyway)"
    return 0
  fi

  if [[ -w /Applications ]]; then
    target_dir="/Applications"
  else
    target_dir="$HOME/Applications"
    mkdir -p "$target_dir"
  fi

  # Renamed lineage: accept either ChatGPT.app or Codex.app as an existing
  # install of the same app (bundle id com.openai.codex).
  local existing="" candidate
  for candidate in ChatGPT.app Codex.app; do
    if [[ -d "$target_dir/$candidate" ]]; then existing="$candidate"; break; fi
  done
  if [[ -n "$existing" && "$force" != "true" ]]; then
    echo "Codex desktop app already present at $target_dir/$existing (use --force to reinstall)."
    return 0
  fi

  require_command hdiutil

  local tmp_dir dmg mnt
  tmp_dir="$(mktemp -d)"
  dmg="$tmp_dir/codex.dmg"
  mnt="$tmp_dir/mnt"
  mkdir -p "$mnt"

  echo "Downloading Codex desktop app ($arch):"
  echo "  $url"
  if ! curl -fL --retry 3 -m 1800 -o "$dmg" "$url"; then
    echo "Download of the Codex desktop DMG failed." >&2
    rm -rf "$tmp_dir"
    return 1
  fi

  echo "Mounting disk image..."
  if ! hdiutil attach -nobrowse -readonly -mountpoint "$mnt" "$dmg" >/dev/null; then
    echo "Failed to mount the DMG (corrupt or blocked download?)." >&2
    rm -rf "$tmp_dir"
    return 1
  fi

  # Name-agnostic app detection (Codex.app -> ChatGPT.app rename): take the
  # first .app in the volume; one level of nesting tolerated.
  local app_src app_name
  app_src="$(find "$mnt" -maxdepth 2 -type d -name '*.app' | head -n 1)"
  if [[ -z "$app_src" ]]; then
    echo "No .app bundle found in the disk image. Top-level contents:" >&2
    ls -la "$mnt" >&2
    hdiutil detach "$mnt" -quiet || true
    rm -rf "$tmp_dir"
    return 1
  fi
  app_name="$(basename "$app_src")"
  echo "Found app bundle: $app_name"

  echo "Installing to $target_dir/$app_name ..."
  rm -rf "$target_dir/ChatGPT.app" "$target_dir/Codex.app"
  if ! ditto "$app_src" "$target_dir/$app_name"; then
    echo "Copy to $target_dir failed." >&2
    hdiutil detach "$mnt" -quiet || true
    rm -rf "$tmp_dir"
    return 1
  fi
  hdiutil detach "$mnt" -quiet || hdiutil detach "$mnt" -force || true

  # curl downloads never get the quarantine xattr, but clear it anyway.
  xattr -dr com.apple.quarantine "$target_dir/$app_name" 2>/dev/null || true

  # Verify the installed copy is the genuine OpenAI build:
  #   codesign --verify passes, Team ID is OpenAI's (2DC432GLL2), and the
  #   bundle id is com.openai.codex. A hard verify failure removes the copy
  #   (and prints the reason); an inconclusive identity check keeps the app
  #   and prints full diagnostics instead of deleting a validly-signed app.
  #   Identity is keyed off TeamIdentifier, NOT the Authority= string: the
  #   Authority line is absent for some valid signature types, while the
  #   TeamIdentifier line is always present in codesign -dv output.
  if command -v codesign >/dev/null 2>&1; then
    local verify_out cs_info team_id bundle_id
    verify_out="$(codesign --verify --verbose=2 "$target_dir/$app_name" 2>&1)" || {
      echo "WARNING: codesign --verify failed; removing the installed copy." >&2
      printf '%s\n' "$verify_out" >&2
      rm -rf "$target_dir/$app_name"
      rm -rf "$tmp_dir"
      return 1
    }
    cs_info="$(codesign -dv "$target_dir/$app_name" 2>&1 || true)"
    team_id="$(printf '%s\n' "$cs_info" | sed -n 's/^TeamIdentifier=\(.*\)$/\1/p' | head -n 1)"
    bundle_id="$(defaults read "$target_dir/$app_name/Contents/Info.plist" CFBundleIdentifier 2>/dev/null || echo unknown)"
    if [[ "$team_id" == "2DC432GLL2" && "$bundle_id" == "com.openai.codex" ]]; then
      echo "Signature verified (official OpenAI build): $(printf '%s\n' "$cs_info" | sed -n 's/^Authority=\(.*\)$/\1/p' | head -n 1)"
      echo "Team ID: $team_id   Bundle id: $bundle_id"
    else
      echo "NOTE: the app is validly signed, but its identity could not be confirmed"
      echo "      (TeamIdentifier='$team_id', bundle id='$bundle_id'; expected"
      echo "      2DC432GLL2 / com.openai.codex). The app was left installed."
      echo "      codesign -dv output:"
      printf '%s\n' "$cs_info" | head -n 20
    fi
  fi

  rm -rf "$tmp_dir"
  echo "Codex desktop app installed: $target_dir/$app_name"
}

# --- main --------------------------------------------------------------------
main() {
  local do_config=true do_app=true
  local restore=false smoke_only=false run_smoke=true force=false
  for arg in "$@"; do
    case "$arg" in
      --help|-h) print_usage; exit 0 ;;
      --config-only) do_app=false ;;
      --app-only) do_config=false ;;
      --no-config) do_config=false ;;
      --no-app) do_app=false ;;
      --restore) restore=true ;;
      --smoke-only) smoke_only=true ;;
      --no-smoke) run_smoke=false ;;
      --force) force=true ;;
      *) echo "Unknown option: $arg" >&2; print_usage >&2; exit 1 ;;
    esac
  done

  if [[ "$(uname -s 2>/dev/null || echo unknown)" != "Darwin" ]]; then
    echo "codex-mac-setup.sh is macOS-only." >&2
    echo "Use codex-vc-setup.sh (Linux / Git Bash) or codex-vc-setup.ps1 (Windows)." >&2
    exit 1
  fi
  require_command curl
  require_command sed
  require_command awk

  local codex_home="${CODEX_HOME:-$HOME/.codex}"
  local auth_file="$codex_home/auth.json"
  local base_url="${VC_BASE_URL:-${OPENAI_BASE_URL:-$DEFAULT_BASE_URL}}"
  local model="${VC_MODEL:-$DEFAULT_MODEL}"

  if [[ "$restore" == "true" ]]; then
    restore_config "$codex_home"
    exit 0
  fi

  echo "==> Codex macOS setup"
  echo "    config: $do_config   app: $do_app   smoke-test: $run_smoke   force: $force"

  local api_key=""
  if [[ "$do_config" == "true" || "$smoke_only" == "true" ]]; then
    api_key="$(read_api_key "$auth_file")"
  fi
  if [[ "$smoke_only" == "true" ]]; then
    smoke_test_key "$api_key" "$base_url" "$model"
    exit 0
  fi

  # A bad key makes everything else pointless: let any config failure abort.
  if [[ "$do_config" == "true" ]]; then
    echo
    echo "==> [config] VC provider + API key -> ~/.codex"
    run_config "$codex_home" "$api_key" "$base_url" "$model" "$run_smoke"
  fi

  local failures=0
  if [[ "$do_app" == "true" ]]; then
    echo
    echo "==> [app] installing Codex desktop app"
    install_app "$force" || { echo "WARNING: Codex desktop app install failed." >&2; failures=1; }
  fi

  echo
  if [[ "$failures" == "0" ]]; then
    echo "==> Done. Restart Codex Desktop, then test with:  Reply with only: connected"
  else
    echo "==> Finished with warnings (see above)." >&2
  fi
  exit "$failures"
}

main "$@"
