#!/usr/bin/env bash
# CodeCompass installer — https://codecompass.run
#
#   curl -fsSL https://codecompass.run/install | bash
#
# Installs the `ccx` client (a small static binary), points it at the hosted
# CodeCompass backend, authenticates, and makes the current repo queryable.
# It does NOT install CodeCompass itself — all analysis runs server-side.
#
# Safe to inspect before running:
#   curl -fsSL https://codecompass.run/install -o ccx-install.sh && less ccx-install.sh && bash ccx-install.sh
# Every release publishes a signed SHA-256 manifest; this script authenticates it and verifies the binary before execution.
#
# Everything runs inside main() so a truncated download can't execute a half-script.
set -euo pipefail

main() {
  # --- config (baked at publish time; all PUBLIC, no secrets) ---
  CCX_VERSION="${CCX_VERSION:-0.1.26}"
  GATEWAY="${CCX_GATEWAY:-https://codecompass.fm-e39.workers.dev}"          # the hosted backend
  # ccx binaries are served from R2 via the Worker's /dl route today; dl.codecompass.run (an R2
  # custom domain) is a later DNS step. Override with CCX_DL_BASE if you wire the custom domain.
  DL_BASE="${CCX_DL_BASE:-${GATEWAY}/dl/ccx/${CCX_VERSION}}"
  INSTALL_DIR="${CCX_INSTALL_DIR:-$HOME/.local/bin}"

  # Color only when the target stream is a TTY and NO_COLOR is unset (no-color.org). say->stdout, err->stderr.
  if [ -t 1 ] && [ -z "${NO_COLOR+x}" ]; then SAY_C=$'\033[1;36m'; C_RST=$'\033[0m'; else SAY_C=""; C_RST=""; fi
  if [ -t 2 ] && [ -z "${NO_COLOR+x}" ]; then ERR_C=$'\033[1;31m'; E_RST=$'\033[0m'; else ERR_C=""; E_RST=""; fi
  say()  { printf '%scodecompass%s %s\n' "$SAY_C" "$C_RST" "$*"; }
  err()  { printf '%serror%s %s\n' "$ERR_C" "$E_RST" "$*" >&2; exit 1; }
  have() { command -v "$1" >/dev/null 2>&1; }

  have curl || err "curl is required."
  have shasum || have sha256sum || err "shasum/sha256sum is required."
  have od || err "od is required to validate the release manifest."
  have openssl || err "openssl is required to verify the release signature. Install it (Debian/Ubuntu: apt-get install -y openssl · Alpine: apk add openssl · Fedora/RHEL: dnf install -y openssl · macOS ships it), then re-run."
  # SHA-256 of a file, portable across macOS (shasum) and Linux (sha256sum).
  sha256_of() {
    if have sha256sum; then sha256sum "$1" | awk '{print $1}'
    else shasum -a 256 "$1" | awk '{print $1}'; fi
  }

  valid_version() {
    [[ "$1" =~ ^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$ ]]
  }
  valid_version "$CCX_VERSION" || err "invalid CCX_VERSION '${CCX_VERSION}' (expected MAJOR.MINOR.PATCH with no leading zeros)."

  # --- release-signature verification: AUTHENTICITY, not just integrity ---
  # The SHA256SUMS manifest is signed with CodeCompass's ECDSA P-256 release key; the matching PUBLIC key is
  # PINNED below. Verifying the signature proves the manifest — and therefore every binary hash it commits to —
  # was signed by the holder of this release key and has not changed. It does not prove source review,
  # pipeline identity, or global freshness. A checksum alone cannot provide this key-possession guarantee.
  #
  # Tests and deployment gates parse ONLY this delimited block. Keep IDs ordered and fingerprints lowercase.
  # The direct migration is now K2-only: K1 is deliberately absent and cannot authenticate any release.
  # The K2 public key is fingerprint-checked before any network request.
  # === BEGIN CCX ACTIVE RELEASE KEYS ===
  CCX_ACTIVE_RELEASE_KEY_IDS='k2'
  CCX_RELEASE_KEY_K2_SPKI_SHA256='25ea2b323c7516e9ddfb593e6c5505a4a825bef21091e6b100a45145c21768df'
  CCX_RELEASE_KEY_K2_PEM='-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6SrFqcDmqCmhiMCNFprepQ2jNY71
JFlvNerIxdBAgJHzPFxMa86myeTCxR3czB21B76ZLIWvf7+IYIONwgD8aA==
-----END PUBLIC KEY-----'
  # === END CCX ACTIVE RELEASE KEYS ===

  prepare_release_keys() {
    local key_id expected_fp public_pem key_file der_file actual_fp key_info seen='' seen_fps='' count=0
    [ -n "$CCX_ACTIVE_RELEASE_KEY_IDS" ] || err "no active release key is configured."
    for key_id in $CCX_ACTIVE_RELEASE_KEY_IDS; do
      case " $seen " in *" $key_id "*) err "duplicate active release key ID: $key_id." ;; esac
      seen="$seen $key_id"
      case "$key_id" in
        k2) expected_fp="$CCX_RELEASE_KEY_K2_SPKI_SHA256"; public_pem="$CCX_RELEASE_KEY_K2_PEM" ;;
        *) err "unknown active release key ID: $key_id." ;;
      esac
      case "$expected_fp:$public_pem" in *@CCX_K2_*) err "release key K2 is not provisioned; refusing to install." ;; esac
      [ "${#expected_fp}" -eq 64 ] || err "invalid fingerprint for release key $key_id."
      case "$expected_fp" in *[!0-9a-f]*) err "invalid fingerprint for release key $key_id." ;; esac
      [ "$(printf '%s\n' "$public_pem" | grep -c '^-----BEGIN PUBLIC KEY-----$')" -eq 1 ] \
        && [ "$(printf '%s\n' "$public_pem" | grep -c '^-----END PUBLIC KEY-----$')" -eq 1 ] \
        || err "invalid public-key encoding for release key $key_id."
      key_file="$tmp/release-key-$key_id.pem"
      der_file="$tmp/release-key-$key_id.der"
      printf '%s\n' "$public_pem" > "$key_file"
      openssl pkey -pubin -in "$key_file" -pubout -outform DER -out "$der_file" 2>/dev/null \
        || err "invalid public key for release key $key_id."
      key_info="$(openssl pkey -pubin -in "$key_file" -text -noout 2>/dev/null)" \
        || err "invalid public key for release key $key_id."
      case "$key_info" in
        *'ASN1 OID: prime256v1'*|*'NIST CURVE: P-256'*) ;;
        *) err "release key $key_id is not ECDSA P-256." ;;
      esac
      actual_fp="$(sha256_of "$der_file")"
      [ "$actual_fp" = "$expected_fp" ] || err "fingerprint mismatch for release key $key_id."
      case " $seen_fps " in *" $actual_fp "*) err "active release keys must have distinct fingerprints." ;; esac
      seen_fps="$seen_fps $actual_fp"
      count=$((count + 1))
    done
    [ "$count" -gt 0 ] || err "no active release key is configured."
  }

  verify_manifest() {  # verify_manifest <manifest> <sig> — fail closed unless an active key verifies
    local key_id kf
    for key_id in $CCX_ACTIVE_RELEASE_KEY_IDS; do
      kf="$tmp/release-key-$key_id.pem"
      if openssl dgst -sha256 -verify "$kf" -signature "$2" "$1" >/dev/null 2>&1; then
        say "release signature verified"; return 0
      fi
    done
    err "release signature verification FAILED — refusing to install (the download may be tampered or corrupt)."
  }

  validate_manifest() { # validate_manifest <manifest> <expected-version>
    local manifest="$1" expected_version="$2" bytes lines
    bytes="$(LC_ALL=C wc -c < "$manifest" | tr -d '[:space:]')"
    lines="$(LC_ALL=C wc -l < "$manifest" | tr -d '[:space:]')"
    [ -n "$bytes" ] && [ "$bytes" -le 1024 ] && [ "$lines" -eq 5 ] \
      || err "release manifest is not canonical for v${expected_version}."
    LC_ALL=C od -An -v -tu1 "$manifest" | LC_ALL=C awk '
      { for (i=1; i<=NF; i++) if ($i == 0 || $i == 13) exit 1 }
    ' || err "release manifest is not canonical for v${expected_version}."
    LC_ALL=C awk -v v="$expected_version" '
      BEGIN {
        names[1]="ccx-darwin-arm64"; names[2]="ccx-darwin-x64"
        names[3]="ccx-linux-arm64"; names[4]="ccx-linux-x64"
      }
      NR == 1 { if ($0 != "ccx-release-version " v) bad=1; next }
      NR >= 2 && NR <= 5 {
        hash=substr($0,1,64); sep=substr($0,65,2); name=substr($0,67)
        if (length(hash) != 64 || hash !~ /^[0-9a-f]+$/ || sep != "  " || name != names[NR-1]) bad=1
      }
      END { if (NR != 5 || bad) exit 1 }
    ' "$manifest" || err "release manifest is not canonical for v${expected_version}."
  }

  download_bounded() { # download_bounded <url> <destination> <max-bytes> <max-seconds> <description>
    local url="$1" destination="$2" max_bytes="$3" max_seconds="$4" description="$5" bytes
    curl -fsSL --proto '=https' --tlsv1.2 --connect-timeout 20 --max-time "$max_seconds" \
      --speed-limit 1024 --speed-time 30 --max-filesize "$max_bytes" "$url" -o "$destination" \
      || err "$description isn't available or exceeded its safe size limit."
    bytes="$(LC_ALL=C wc -c < "$destination" | tr -d '[:space:]')"
    [ -n "$bytes" ] && [ "$bytes" -gt 0 ] && [ "$bytes" -le "$max_bytes" ] \
      || err "$description is empty or exceeded its safe size limit."
  }

  # --- brand palette (mint on dark, matching codecompass.run) — color only on a TTY with NO_COLOR unset.
  # xterm 256-color (renders on Terminal.app too, not only truecolor): mint 115 ≈ #7fd1ae, dim-mint 65.
  if [ -t 1 ] && [ -z "${NO_COLOR+x}" ]; then
    MINT=$'\033[38;5;115m'; MINTD=$'\033[38;5;65m'; MUTE=$'\033[38;5;245m'
    DIM=$'\033[2m'; BOLD=$'\033[1m'; RST=$'\033[0m'
  else
    MINT=""; MINTD=""; MUTE=""; DIM=""; BOLD=""; RST=""
  fi
  RULE_STR="$(printf '─%.0s' {1..51})"   # 51 dashes = exactly the brand_row display width
  rule()      { printf '%s%s%s\n' "$DIM" "$RULE_STR" "$RST"; }
  # "  <label>   <value…>  ● ready" — value padded to a 30-wide field, then a LITERAL 2-space gap, then ● ready.
  # The gap is explicit (not leftover %-padding), so it can't be eaten: a value longer than 30 is truncated to
  # 29+"~" so it fills the field. All three callers pass ASCII (the label literals + the ASCII gateway host +
  # the ccx version), so byte-count == display-width and every row is exactly 2+9+1+30+2+7 = 51 cols = the rule.
  # (For a multibyte value the row would drift off 51, since printf pads by BYTES while ${#v} counts code points
  # — not worth bash display-width machinery for an input that is always ASCII.)
  brand_row() { local v="$2"; [ "${#v}" -gt 30 ] && v="${v:0:29}~"; printf '  %s%-9s%s %-30s  %s● ready%s\n' "$MUTE" "$1" "$RST" "$v" "$MINT" "$RST"; }

  # --- 1. detect OS + arch ---
  local os arch asset
  os="$(uname -s | tr '[:upper:]' '[:lower:]')"
  case "$os" in linux|darwin) ;; *) err "unsupported OS: $os" ;; esac
  arch="$(uname -m)"
  case "$arch" in
    arm64|aarch64) arch="arm64" ;;
    x86_64|amd64)  arch="x64" ;;
    *) err "unsupported arch: $arch" ;;
  esac
  asset="ccx-${os}-${arch}"

  # --- 2. resolve target digest, reuse-or-download, VERIFY before executing ---
  # These are intentionally global (not `local`) so every exit path can clean them.
  tmp="$(mktemp -d)"; install_tmp=''
  cleanup() {
    [ -z "${install_tmp:-}" ] || rm -f -- "$install_tmp"
    [ -z "${tmp:-}" ] || rm -rf -- "$tmp"
  }
  trap cleanup EXIT
  trap 'exit 129' HUP
  trap 'exit 130' INT
  trap 'exit 143' TERM
  local ccx="$INSTALL_DIR/ccx"

  # Never let an existing filesystem object change the meaning of the destination path. In
  # particular, some mv implementations treat a symlink to a directory as a directory target.
  # Refuse symlinks (including dangling ones) and every non-regular destination before downloads
  # or staging; a normal installed binary is the only existing object we will replace.
  [ ! -L "$ccx" ] || err "refusing to replace symlink destination: $ccx. Remove it and re-run the installer."
  if [ -e "$ccx" ] && [ ! -f "$ccx" ]; then
    err "refusing to replace non-regular destination: $ccx. Remove it and re-run the installer."
  fi
  prepare_release_keys

  # Always fetch the checksum manifest first (a few bytes) so we know THIS version's target digest
  # before deciding whether to transfer the (multi-MB) binary.
  download_bounded "${DL_BASE}/SHA256SUMS" "$tmp/SHA256SUMS" 1024 60 "release manifest for v${CCX_VERSION}"
  # Authenticate the manifest BEFORE trusting any digest in it — this covers BOTH the cache-reuse and the
  # download path below, so the per-binary checksum becomes a real authenticity check, not just integrity.
  download_bounded "${DL_BASE}/SHA256SUMS.sig" "$tmp/SHA256SUMS.sig" 80 60 "release signature for v${CCX_VERSION}"
  verify_manifest "$tmp/SHA256SUMS" "$tmp/SHA256SUMS.sig"
  validate_manifest "$tmp/SHA256SUMS" "$CCX_VERSION"
  local want got
  want="$(LC_ALL=C awk -v a="$asset" 'substr($0,67)==a {print substr($0,1,64)}' "$tmp/SHA256SUMS")"
  [ -n "$want" ] || err "no checksum published for ${asset}."

  # Idempotent, content-addressed cache: if an installed ccx already hashes to the published digest,
  # it is byte-for-byte the release binary — skip the download entirely. A changed binary (new
  # version, or a re-published same version) has a different digest and re-downloads; an unchanged one
  # is reused with zero binary transfer. Exact across macOS/Linux (SHA-256, not mtime/size), and the
  # digest is re-verified every run, so a corrupted/tampered local copy is caught and replaced.
  if [ -f "$ccx" ] && [ ! -L "$ccx" ] && [ "$(sha256_of "$ccx")" = "$want" ]; then
    say "ccx v${CCX_VERSION} already present and verified ($ccx) — skipping download"
    chmod 0755 "$ccx" 2>/dev/null || true
  else
    say "downloading ${asset} (v${CCX_VERSION})"
    download_bounded "${DL_BASE}/${asset}" "$tmp/ccx" 134217728 1800 "release binary ${asset} for v${CCX_VERSION}"
    # The manifest was signature-verified above; this checksum ties THIS binary to that AUTHENTICATED
    # manifest, so a compromised CDN can't swap the binary without producing an invalid signature.
    say "verifying checksum"
    got="$(sha256_of "$tmp/ccx")"
    [ "$want" = "$got" ] || err "checksum mismatch — refusing to install (want ${want}, got ${got})."
    mkdir -p "$INSTALL_DIR"
    # Stage on the destination filesystem, then rename over the old binary atomically.
    install_tmp="$(mktemp "$INSTALL_DIR/.ccx-install.XXXXXX")"
    install -m 0755 "$tmp/ccx" "$install_tmp"
    mv -f -- "$install_tmp" "$ccx"
    install_tmp=''
    say "installed ccx -> $ccx"
  fi
  case ":$PATH:" in
    *":$INSTALL_DIR:"*) ;;
    *) say "add to PATH:  export PATH=\"$INSTALL_DIR:\$PATH\"" ;;
  esac

  # --- 3. point ccx at the backend ---
  "$ccx" config set gateway "$GATEWAY" || true

  # --- 4. authenticate (optional at install time) ---
  # CI/agents can pass an API key non-interactively; humans sign in later via `ccx login` (ccx itself
  # says so when a start needs it). `if`-guarded so a failed key never aborts the installer (set -e).
  local authed=0
  if [ -n "${CODECOMPASS_API_KEY:-}" ]; then
    if "$ccx" login --api-key "$CODECOMPASS_API_KEY"; then authed=1; fi
  fi

  # --- branded status banner (screen 1): the install work above is done + verified ---
  local gw_host vpad
  gw_host="${GATEWAY#*://}"; gw_host="${gw_host%%/*}"          # host only, no scheme/path
  # Right-align the version to the 51-col rule for ANY version length: 18 = len("CodeCompass  setup"), 1 = "v".
  vpad=$(( 51 - 18 - 1 - ${#CCX_VERSION} )); [ "$vpad" -lt 1 ] && vpad=1
  printf '\n%s%sCodeCompass%s  %ssetup%s%*s%sv%s%s\n' "$BOLD" "$MINT" "$RST" "$MINTD" "$RST" "$vpad" "" "$DIM" "$CCX_VERSION" "$RST"
  rule
  brand_row "client"  "ccx ${CCX_VERSION}"
  if [ "$authed" -eq 1 ]; then brand_row "account" "account"; fi
  brand_row "gateway" "$gw_host"
  rule

  # --- 5. next steps (cloud-only) ---
  # ccx targets the hosted backend; `ccx start` tars this repo's tracked files (+ .git; secrets and
  # build artifacts excluded) and uploads them to an isolated container. That command IS the upload-
  # consent surface, so the installer never uploads on its own — it just points the way. (ccx itself
  # honours CCX_NO_SYNC.)
  if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
    local proj nfiles
    proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")"
    nfiles="$(git ls-files 2>/dev/null | wc -l | tr -d ' ')" || nfiles=0   # guard: a failing ls-files (corrupt index) must not trip set -e/pipefail
    printf '  %s %s·%s %s tracked files\n' "$proj" "$DIM" "$RST" "$nfiles"
    if [ "$authed" -eq 1 ]; then
      printf '  %sready.%s Index this repo:  %sccx start%s  %s(uploads tracked files to %s)%s\n' "$DIM" "$RST" "$BOLD" "$RST" "$DIM" "$gw_host" "$RST"
    else
      printf '  %snext:%s  %sccx login%s  then  %sccx start%s  %s(uploads tracked files to %s)%s\n' "$DIM" "$RST" "$BOLD" "$RST" "$BOLD" "$RST" "$DIM" "$gw_host" "$RST"
    fi
  else
    printf '  %sinstalled — cd into a git repo, then%s  %sccx login%s (if needed) and  %sccx start%s\n' "$DIM" "$RST" "$BOLD" "$RST" "$BOLD" "$RST"
  fi
}

main "$@"
