#!/usr/bin/env bash
#
# Spandrel CLI installer
#
# Install the latest signed release:
#   curl -fsSL https://cli.ndrel.spa | bash
#
# Pass options with: curl ... | bash -s -- --help

set -euo pipefail
shopt -s lastpipe 2>/dev/null || true
umask 022

readonly DEFAULT_BASE_URL="https://cli.ndrel.spa"
readonly RELEASE_IDENTITY="spandrel-release"
readonly RELEASE_NAMESPACE="spandrel-cli-release"
readonly RELEASE_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINe9wu7K8ctVV6Pzfn4yroGxBWOaMBMKAVguxuyJ0REE"
readonly ALIAS_MARKER_NAME=".spandrel-sp-alias"
readonly ALIAS_MARKER_CONTENT="spandrel-cli-managed-alias-v1"
readonly MAX_BINARY_SIZE=104857600
readonly MAX_BUNDLE_SIZE=1048576
readonly MAX_METADATA_SIZE=65536
readonly MAX_SIGNATURE_SIZE=65536

BASE_URL="${SPANDREL_DOWNLOAD_BASE_URL:-$DEFAULT_BASE_URL}"
DESTINATION="${SPANDREL_INSTALL_DIR:-$HOME/.local/bin}"
DESTINATION_EXPLICIT=0
[[ -n "${SPANDREL_INSTALL_DIR:-}" ]] && DESTINATION_EXPLICIT=1
ALIAS_SUMMARY="not installed"
VERSION=""
OFFLINE_DIRECTORY=""
QUIET=0
NO_GUM=0
FORCE=0
ALLOW_DOWNGRADE=0
RUN_SETUP=1
MODIFY_PATH=1
HAS_GUM=0
TEMP_DIRECTORY=""
LOCK_DIRECTORY=""
LOCK_OWNED=0
STAGED_BINARY=""
PROXY_ARGS=()

usage() {
  cat <<'USAGE'
Spandrel CLI installer

Usage:
  install.sh [options]
  curl -fsSL URL | bash -s -- [options]

Options:
  --version VERSION       Install an immutable version instead of latest
  --dest DIRECTORY        Installation directory (default: a safe writable PATH directory,
                          otherwise ~/.local/bin)
  --offline DIRECTORY     Install from signed metadata and binary in DIRECTORY
  --force                 Reinstall even when this version is already present
  --allow-downgrade       Permit a downgrade selected with --version VERSION
  --no-setup              Do not launch interactive `spandrel setup`
  --no-modify-path        Do not add ~/.local/bin to the active shell profile
  --quiet                 Suppress non-error installer messages
  --no-gum                Disable optional gum formatting
  -h, --help              Show this help

Offline DIRECTORY must contain metadata.json, metadata.json.sshsig, and
spandrel. Signature and SHA-256 verification are mandatory in every mode.
USAGE
}

while (($#)); do
  case "$1" in
    --version)
      [[ $# -ge 2 ]] || { printf 'error: --version requires a value\n' >&2; exit 2; }
      VERSION="$2"
      shift 2
      ;;
    --dest)
      [[ $# -ge 2 ]] || { printf 'error: --dest requires a value\n' >&2; exit 2; }
      DESTINATION="$2"
      DESTINATION_EXPLICIT=1
      shift 2
      ;;
    --offline)
      [[ $# -ge 2 ]] || { printf 'error: --offline requires a directory\n' >&2; exit 2; }
      OFFLINE_DIRECTORY="$2"
      shift 2
      ;;
    --force) FORCE=1; shift ;;
    --allow-downgrade) ALLOW_DOWNGRADE=1; shift ;;
    --no-setup) RUN_SETUP=0; shift ;;
    --no-modify-path) MODIFY_PATH=0; shift ;;
    --quiet) QUIET=1; shift ;;
    --no-gum) NO_GUM=1; shift ;;
    -h|--help) usage; exit 0 ;;
    *) printf 'error: unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
  esac
done

if ((ALLOW_DOWNGRADE == 1)) && [[ -z "$VERSION" ]]; then
  printf 'error: --allow-downgrade requires --version VERSION\n' >&2
  exit 2
fi

if command -v gum >/dev/null 2>&1 && [[ -t 1 ]] && ((NO_GUM == 0)); then
  HAS_GUM=1
fi

info() {
  ((QUIET == 1)) && return 0
  if ((HAS_GUM == 1)); then
    gum style --foreground 39 "→ $*"
  else
    printf '\033[0;34m→\033[0m %s\n' "$*"
  fi
}

ok() {
  ((QUIET == 1)) && return 0
  if ((HAS_GUM == 1)); then
    gum style --foreground 42 "✓ $*"
  else
    printf '\033[0;32m✓\033[0m %s\n' "$*"
  fi
}

warn() {
  ((QUIET == 1)) && return 0
  if ((HAS_GUM == 1)); then
    gum style --foreground 214 "! $*"
  else
    printf '\033[1;33m!\033[0m %s\n' "$*"
  fi
}

err() {
  if ((HAS_GUM == 1)); then
    gum style --foreground 196 "✗ $*" >&2
  else
    printf '\033[0;31m✗\033[0m %s\n' "$*" >&2
  fi
}

terminal_link() {
  local url=$1
  if [[ -t 2 && -z "${NO_COLOR:-}" && "${TERM:-}" != "dumb" ]]; then
    printf '\033]8;;%s\033\\%s ↗\033]8;;\033\\\n' "$url" "$url" >&2
  else
    printf '%s ↗\n' "$url" >&2
  fi
}

draw_box() {
  local line width=0 padding
  for line in "$@"; do
    ((${#line} > width)) && width=${#line}
  done
  printf '╔'
  printf '═%.0s' $(seq 1 $((width + 2)))
  printf '╗\n'
  for line in "$@"; do
    padding=$((width - ${#line}))
    printf '║ %s%*s ║\n' "$line" "$padding" ''
  done
  printf '╚'
  printf '═%.0s' $(seq 1 $((width + 2)))
  printf '╝\n'
}

draw_logo() {
  local line index=0
  local lines=(
    "███████╗██████╗  █████╗ ███╗   ██╗██████╗ ██████╗ ███████╗██╗     "
    "██╔════╝██╔══██╗██╔══██╗████╗  ██║██╔══██╗██╔══██╗██╔════╝██║     "
    "███████╗██████╔╝███████║██╔██╗ ██║██║  ██║██████╔╝█████╗  ██║     "
    "╚════██║██╔═══╝ ██╔══██║██║╚██╗██║██║  ██║██╔══██╗██╔══╝  ██║     "
    "███████║██║     ██║  ██║██║ ╚████║██████╔╝██║  ██║███████╗███████╗"
    "╚══════╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═══╝╚═════╝ ╚═╝  ╚═╝╚══════╝╚══════╝"
  )
  local colors=(
    "128;238;100"
    "123;235;99"
    "111;229;98"
    "90;218;96"
    "61;202;93"
    "9;175;88"
  )

  if [[ -n "${NO_COLOR:-}" || "${TERM:-}" == "dumb" || ! -t 1 ]]; then
    printf '%s\n' "${lines[@]}"
  else
    for line in "${lines[@]}"; do
      printf '\033[38;2;%sm%s\033[0m\n' "${colors[$index]}" "$line"
      index=$((index + 1))
    done
  fi
  printf '\n'
}

header() {
  ((QUIET == 1)) && return 0
  draw_logo
  if ((HAS_GUM == 1)); then
    gum style \
      --border normal --border-foreground 39 --padding "0 1" --margin "1 0" \
      "$(gum style --foreground 42 --bold 'Spandrel Skill Installer')" \
      "$(gum style --foreground 245 'Agent skills for biotech')"
  else
    printf '\033[1;32m'
    draw_box "Spandrel Skill Installer" "Agent skills for biotech"
    printf '\033[0m'
  fi
}

cleanup() {
  local status=$?
  trap - EXIT INT TERM
  if [[ -n "$TEMP_DIRECTORY" && -d "$TEMP_DIRECTORY" ]]; then
    rm -rf -- "$TEMP_DIRECTORY"
  fi
  if [[ -n "$STAGED_BINARY" \
    && (-f "$STAGED_BINARY" || -L "$STAGED_BINARY") ]]; then
    rm -f -- "$STAGED_BINARY"
  fi
  if ((LOCK_OWNED == 1)) && [[ -n "$LOCK_DIRECTORY" && -d "$LOCK_DIRECTORY" ]]; then
    rm -rf -- "$LOCK_DIRECTORY"
  fi
  exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

setup_proxy() {
  if [[ -n "${HTTPS_PROXY:-}" ]]; then
    PROXY_ARGS=(--proxy "$HTTPS_PROXY")
    info "Using the configured HTTPS proxy"
  elif [[ -n "${https_proxy:-}" ]]; then
    PROXY_ARGS=(--proxy "$https_proxy")
    info "Using the configured HTTPS proxy"
  elif [[ -n "${HTTP_PROXY:-}" ]]; then
    PROXY_ARGS=(--proxy "$HTTP_PROXY")
    info "Using the configured HTTP proxy"
  elif [[ -n "${http_proxy:-}" ]]; then
    PROXY_ARGS=(--proxy "$http_proxy")
    info "Using the configured HTTP proxy"
  fi
}

require_command() {
  command -v "$1" >/dev/null 2>&1 || {
    err "$1 is required but was not found"
    return 1
  }
}

detect_target() {
  local os arch
  os=$(uname -s | tr '[:upper:]' '[:lower:]')
  arch=$(uname -m)
  case "$arch" in
    x86_64|amd64) arch="x86_64" ;;
    arm64|aarch64) arch="aarch64" ;;
    *) err "Unsupported architecture: $arch"; return 1 ;;
  esac
  case "$os" in
    darwin) TARGET="${arch}-apple-darwin" ;;
    linux) TARGET="${arch}-unknown-linux-musl" ;;
    *) err "Unsupported operating system: $os"; return 1 ;;
  esac
  if [[ "$os" == "linux" ]] && grep -qi microsoft /proc/version 2>/dev/null; then
    warn "WSL detected; Spandrel will use the Linux release"
  fi
  readonly TARGET
}

directory_on_path() {
  case ":$PATH:" in
    *:"$1":*) return 0 ;;
    *) return 1 ;;
  esac
}

choose_destination() {
  ((DESTINATION_EXPLICIT == 0)) || return 0
  directory_on_path "$DESTINATION" && return 0

  local candidate
  for candidate in "$HOME/bin" /opt/homebrew/bin /usr/local/bin; do
    if directory_on_path "$candidate" && [[ -d "$candidate" && -w "$candidate" ]]; then
      DESTINATION=$candidate
      info "Installing to $DESTINATION so spandrel works in this shell"
      return 0
    fi
  done
}

chatgpt_desktop_installed() {
  [[ "$TARGET" == *-apple-darwin ]] || return 1
  [[ -d "/Applications/ChatGPT.app" \
    || -d "/Applications/Codex.app" \
    || -d "$HOME/Applications/ChatGPT.app" \
    || -d "$HOME/Applications/Codex.app" ]]
}

detect_installed_agents() {
  ((RUN_SETUP == 1)) || return 0

  info "Checking for installed agents"
  local found=0 codex_cli=0 codex_config=0 codex_desktop=0
  local codex_home="${CODEX_HOME:-$HOME/.codex}"

  if command -v codex >/dev/null 2>&1; then
    codex_cli=1
  fi
  if [[ -d "$codex_home" ]]; then
    codex_config=1
  fi
  if chatgpt_desktop_installed; then
    codex_desktop=1
  fi
  if ((codex_cli == 1 || codex_config == 1 || codex_desktop == 1)); then
    if ((codex_cli == 1 && codex_desktop == 1)); then
      ok "Found Codex (CLI and ChatGPT Desktop)"
    elif ((codex_desktop == 1)); then
      ok "Found Codex (ChatGPT Desktop)"
    else
      ok "Found Codex"
    fi
    found=1
  fi

  if command -v claude >/dev/null 2>&1 || [[ -d "$HOME/.claude" ]]; then
    ok "Found Claude Code"
    found=1
  fi

  ((found == 1)) && return 0
  if [[ "$TARGET" == *-apple-darwin ]]; then
    err "No coding agents found. Please install ChatGPT Desktop to get started"
    terminal_link "https://chatgpt.com/download/"
  else
    err "No coding agents found. Please install the Codex CLI to get started"
    terminal_link "https://developers.openai.com/codex/cli/"
  fi
  return 1
}

acquire_lock() {
  local lock_base owner_pid lock_owner current_uid
  lock_base="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}"
  LOCK_DIRECTORY="$lock_base/spandrel-installer-$(id -u).lock"
  if mkdir -m 700 "$LOCK_DIRECTORY" 2>/dev/null; then
    LOCK_OWNED=1
  else
    owner_pid=$(cat "$LOCK_DIRECTORY/pid" 2>/dev/null || true)
    if [[ "$owner_pid" =~ ^[0-9]+$ ]] && kill -0 "$owner_pid" 2>/dev/null; then
      err "Another Spandrel installer is running (PID $owner_pid)"
      return 1
    fi
    if [[ -d "$LOCK_DIRECTORY" && ! -L "$LOCK_DIRECTORY" ]]; then
      current_uid=$(id -u)
      lock_owner=$(stat -f '%u' "$LOCK_DIRECTORY" 2>/dev/null \
        || stat -c '%u' "$LOCK_DIRECTORY" 2>/dev/null \
        || true)
      [[ "$lock_owner" == "$current_uid" ]] || {
        err "Stale installer lock is not owned by the current user"
        return 1
      }
      rm -f -- "$LOCK_DIRECTORY/pid"
      rmdir "$LOCK_DIRECTORY" 2>/dev/null || {
        err "Stale installer lock could not be removed: $LOCK_DIRECTORY"
        return 1
      }
    fi
    mkdir -m 700 "$LOCK_DIRECTORY"
    LOCK_OWNED=1
  fi
  printf '%s\n' "$$" >"$LOCK_DIRECTORY/pid"
}

check_destination() {
  mkdir -p "$DESTINATION"
  [[ -d "$DESTINATION" && -w "$DESTINATION" ]] || {
    err "Installation directory is not writable: $DESTINATION"
    return 1
  }
}

check_free_space() {
  local path=$1 required_bytes=$2 description=$3
  local available_kb required_kb
  available_kb=$(df -Pk "$path" | awk 'NR==2 {print $4}')
  required_kb=$(((required_bytes + 1023) / 1024 + 1024))
  if [[ "$available_kb" =~ ^[0-9]+$ ]] && ((available_kb < required_kb)); then
    err "$description needs at least $required_kb KB of free disk space"
    return 1
  fi
}

download() {
  local url=$1 destination=$2 description=$3 maximum_size=$4
  if ((HAS_GUM == 1 && QUIET == 0)); then
    gum spin --spinner dot --title "$description" -- \
      curl --proto '=https' --tlsv1.2 --fail --silent --show-error \
        --location --retry 3 --retry-delay 1 --connect-timeout 10 \
        --max-time 300 \
        --max-filesize "$maximum_size" \
        "${PROXY_ARGS[@]+${PROXY_ARGS[@]}}" "$url" --output "$destination"
  else
    info "$description"
    curl --proto '=https' --tlsv1.2 --fail --silent --show-error \
      --location --retry 3 --retry-delay 1 --connect-timeout 10 \
      --max-time 300 \
      --max-filesize "$maximum_size" \
      "${PROXY_ARGS[@]+${PROXY_ARGS[@]}}" "$url" --output "$destination"
  fi
  local downloaded_size
  downloaded_size=$(wc -c <"$destination" | tr -d '[:space:]')
  if [[ ! "$downloaded_size" =~ ^[0-9]+$ ]] || ((downloaded_size > maximum_size)); then
    err "$description exceeded its maximum allowed size"
    return 1
  fi
}

decode_base64_to_file() {
  local encoded=$1 destination=$2
  if [[ ! "$encoded" =~ ^[A-Za-z0-9+/]*={0,2}$ ]] || ((${#encoded} % 4 != 0)); then
    err "Release bundle contains invalid base64"
    return 1
  fi
  if printf '%s' "$encoded" | base64 --decode >"$destination" 2>/dev/null; then
    return 0
  fi
  if printf '%s' "$encoded" | base64 -D >"$destination" 2>/dev/null; then
    return 0
  fi
  rm -f -- "$destination"
  err "Could not decode the release bundle with the installed base64 tool"
  return 1
}

verify_metadata_signature() {
  local metadata=$1 signature=$2 allowed_signers=$TEMP_DIRECTORY/allowed_signers
  printf '%s %s\n' "$RELEASE_IDENTITY" "$RELEASE_PUBLIC_KEY" >"$allowed_signers"
  if ! ssh-keygen -Y verify \
    -f "$allowed_signers" \
    -I "$RELEASE_IDENTITY" \
    -n "$RELEASE_NAMESPACE" \
    -s "$signature" <"$metadata" >/dev/null 2>&1; then
    err "Release metadata signature verification failed"
    return 1
  fi
  ok "Release signature verified"
}

json_string_field() {
  local field=$1 file=$2 compact
  compact=$(tr -d '\r\n' <"$file")
  printf '%s' "$compact" | sed -n "s/.*\"$field\":\"\([^\"]*\)\".*/\1/p"
}

json_number_field() {
  local field=$1 file=$2 compact
  compact=$(tr -d '\r\n' <"$file")
  printf '%s' "$compact" | sed -n "s/.*\"$field\":\([0-9][0-9]*\).*/\1/p"
}

sha256_file() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    err "A SHA-256 tool (sha256sum or shasum) is required"
    return 1
  fi
}

is_semver() {
  local version=$1 core prerelease="" build="" identifier
  local major minor patch
  local -a identifiers

  core=${version%%+*}
  if [[ "$version" == *+* ]]; then
    build=${version#*+}
    [[ -n "$build" && "$build" != *+* ]] || return 1
  fi
  if [[ "$core" == *-* ]]; then
    prerelease=${core#*-}
    core=${core%%-*}
    [[ -n "$prerelease" ]] || return 1
  fi
  [[ "$core" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
  IFS='.' read -r major minor patch <<<"$core"
  for identifier in "$major" "$minor" "$patch"; do
    [[ "$identifier" == "0" || "$identifier" =~ ^[1-9][0-9]*$ ]] || return 1
  done

  if [[ -n "$prerelease" ]]; then
    [[ "$prerelease" != .* && "$prerelease" != *. && "$prerelease" != *..* ]] \
      || return 1
    IFS='.' read -r -a identifiers <<<"$prerelease"
    for identifier in "${identifiers[@]}"; do
      [[ -n "$identifier" && "$identifier" =~ ^[0-9A-Za-z-]+$ ]] || return 1
      if [[ "$identifier" =~ ^[0-9]+$ && "$identifier" != "0" ]]; then
        [[ "$identifier" =~ ^[1-9][0-9]*$ ]] || return 1
      fi
    done
  fi
  if [[ -n "$build" ]]; then
    [[ "$build" != .* && "$build" != *. && "$build" != *..* ]] || return 1
    IFS='.' read -r -a identifiers <<<"$build"
    for identifier in "${identifiers[@]}"; do
      [[ -n "$identifier" && "$identifier" =~ ^[0-9A-Za-z-]+$ ]] || return 1
    done
  fi
}

compare_decimal() {
  local left=$1 right=$2
  DECIMAL_ORDER=0
  if ((${#left} < ${#right})); then
    DECIMAL_ORDER=-1
  elif ((${#left} > ${#right})); then
    DECIMAL_ORDER=1
  elif [[ "$left" < "$right" ]]; then
    DECIMAL_ORDER=-1
  elif [[ "$left" > "$right" ]]; then
    DECIMAL_ORDER=1
  fi
}

compare_semver() {
  local left=$1 right=$2 left_core right_core left_pre="" right_pre=""
  local left_major left_minor left_patch right_major right_minor right_patch
  local left_identifier right_identifier index limit
  local -a left_identifiers right_identifiers
  local LC_ALL=C
  SEMVER_ORDER=0

  left_core=${left%%+*}
  right_core=${right%%+*}
  if [[ "$left_core" == *-* ]]; then
    left_pre=${left_core#*-}
    left_core=${left_core%%-*}
  fi
  if [[ "$right_core" == *-* ]]; then
    right_pre=${right_core#*-}
    right_core=${right_core%%-*}
  fi
  IFS='.' read -r left_major left_minor left_patch <<<"$left_core"
  IFS='.' read -r right_major right_minor right_patch <<<"$right_core"

  compare_decimal "$left_major" "$right_major"
  ((DECIMAL_ORDER == 0)) || { SEMVER_ORDER=$DECIMAL_ORDER; return 0; }
  compare_decimal "$left_minor" "$right_minor"
  ((DECIMAL_ORDER == 0)) || { SEMVER_ORDER=$DECIMAL_ORDER; return 0; }
  compare_decimal "$left_patch" "$right_patch"
  ((DECIMAL_ORDER == 0)) || { SEMVER_ORDER=$DECIMAL_ORDER; return 0; }

  if [[ -z "$left_pre" && -n "$right_pre" ]]; then
    SEMVER_ORDER=1
    return 0
  fi
  if [[ -n "$left_pre" && -z "$right_pre" ]]; then
    SEMVER_ORDER=-1
    return 0
  fi
  [[ -n "$left_pre" ]] || return 0

  IFS='.' read -r -a left_identifiers <<<"$left_pre"
  IFS='.' read -r -a right_identifiers <<<"$right_pre"
  limit=${#left_identifiers[@]}
  ((${#right_identifiers[@]} < limit)) && limit=${#right_identifiers[@]}
  for ((index = 0; index < limit; index++)); do
    left_identifier=${left_identifiers[index]}
    right_identifier=${right_identifiers[index]}
    if [[ "$left_identifier" =~ ^[0-9]+$ && "$right_identifier" =~ ^[0-9]+$ ]]; then
      compare_decimal "$left_identifier" "$right_identifier"
      ((DECIMAL_ORDER == 0)) || { SEMVER_ORDER=$DECIMAL_ORDER; return 0; }
    elif [[ "$left_identifier" =~ ^[0-9]+$ ]]; then
      SEMVER_ORDER=-1
      return 0
    elif [[ "$right_identifier" =~ ^[0-9]+$ ]]; then
      SEMVER_ORDER=1
      return 0
    elif [[ "$left_identifier" < "$right_identifier" ]]; then
      SEMVER_ORDER=-1
      return 0
    elif [[ "$left_identifier" > "$right_identifier" ]]; then
      SEMVER_ORDER=1
      return 0
    fi
  done
  if ((${#left_identifiers[@]} < ${#right_identifiers[@]})); then
    SEMVER_ORDER=-1
  elif ((${#left_identifiers[@]} > ${#right_identifiers[@]})); then
    SEMVER_ORDER=1
  fi
}

read_spandrel_version() {
  local executable=$1 output version
  output=$("$executable" --version 2>/dev/null) || return 1
  [[ "$output" == "spandrel "* && "$output" != *$'\n'* ]] || return 1
  version=${output#"spandrel "}
  [[ -n "$version" && "$version" != *[[:space:]]* ]] || return 1
  is_semver "$version" || return 1
  printf '%s\n' "$version"
}

load_release() {
  local metadata=$TEMP_DIRECTORY/metadata.json
  local signature=$TEMP_DIRECTORY/metadata.json.sshsig
  local binary=$TEMP_DIRECTORY/spandrel
  if [[ -n "$OFFLINE_DIRECTORY" ]]; then
    [[ -d "$OFFLINE_DIRECTORY" ]] || {
      err "Offline directory does not exist: $OFFLINE_DIRECTORY"
      return 1
    }
    cp "$OFFLINE_DIRECTORY/metadata.json" "$metadata"
    cp "$OFFLINE_DIRECTORY/metadata.json.sshsig" "$signature"
    cp "$OFFLINE_DIRECTORY/spandrel" "$binary"
  elif [[ -n "$VERSION" ]]; then
    is_semver "$VERSION" || {
      err "Invalid version: $VERSION"
      return 1
    }
    local metadata_path="/v1/releases/$VERSION/$TARGET/metadata.json"
    download \
      "$BASE_URL$metadata_path" \
      "$metadata" \
      "Downloading release metadata" \
      "$MAX_METADATA_SIZE"
    download \
      "$BASE_URL$metadata_path.sshsig" \
      "$signature" \
      "Downloading release signature" \
      "$MAX_SIGNATURE_SIZE"
  else
    local bundle=$TEMP_DIRECTORY/release.bundle.json
    local raw_signature=$TEMP_DIRECTORY/metadata.json.sig
    download \
      "$BASE_URL/v1/latest/$TARGET.bundle.json" \
      "$bundle" \
      "Downloading signed release bundle" \
      "$MAX_BUNDLE_SIZE"
    local bundle_schema metadata_base64 signature_base64 ssh_signature_base64
    bundle_schema=$(json_number_field schema_version "$bundle")
    metadata_base64=$(json_string_field metadata_base64 "$bundle")
    signature_base64=$(json_string_field signature_base64 "$bundle")
    ssh_signature_base64=$(json_string_field ssh_signature_base64 "$bundle")
    [[ "$bundle_schema" == "1" ]] || {
      err "Release bundle has an unsupported schema"
      return 1
    }
    [[ -n "$metadata_base64" \
      && -n "$signature_base64" \
      && -n "$ssh_signature_base64" ]] || {
      err "Release bundle is missing a signed component"
      return 1
    }
    decode_base64_to_file "$metadata_base64" "$metadata"
    decode_base64_to_file "$signature_base64" "$raw_signature"
    decode_base64_to_file "$ssh_signature_base64" "$signature"
    [[ $(wc -c <"$metadata" | tr -d '[:space:]') -le $MAX_METADATA_SIZE ]] || {
      err "Release metadata exceeds its maximum allowed size"
      return 1
    }
    [[ $(wc -c <"$raw_signature" | tr -d '[:space:]') -le $MAX_SIGNATURE_SIZE ]] || {
      err "Release signature exceeds its maximum allowed size"
      return 1
    }
    [[ $(wc -c <"$signature" | tr -d '[:space:]') -le $MAX_SIGNATURE_SIZE ]] || {
      err "Release SSH signature exceeds its maximum allowed size"
      return 1
    }
  fi

  verify_metadata_signature "$metadata" "$signature"
  RELEASE_SCHEMA=$(json_number_field schema_version "$metadata")
  RELEASE_VERSION=$(json_string_field version "$metadata")
  RELEASE_TARGET=$(json_string_field target "$metadata")
  RELEASE_PATH=$(json_string_field path "$metadata")
  RELEASE_SHA256=$(json_string_field sha256 "$metadata")
  RELEASE_SIZE=$(json_number_field size "$metadata")
  readonly \
    RELEASE_SCHEMA \
    RELEASE_VERSION \
    RELEASE_TARGET \
    RELEASE_PATH \
    RELEASE_SHA256 \
    RELEASE_SIZE
  [[ "$RELEASE_SCHEMA" == "1" ]] || {
    err "Signed metadata has an unsupported schema"
    return 1
  }
  is_semver "$RELEASE_VERSION" || {
    err "Signed metadata contains an invalid version"
    return 1
  }
  [[ "$RELEASE_TARGET" == "$TARGET" ]] || {
    err "Signed metadata target does not match this machine"
    return 1
  }
  [[ "$RELEASE_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
    err "Signed metadata contains an invalid SHA-256 digest"
    return 1
  }
  if [[ ! "$RELEASE_SIZE" =~ ^[1-9][0-9]*$ ]] \
    || ((RELEASE_SIZE > MAX_BINARY_SIZE)); then
    err "Signed metadata contains an invalid artifact size"
    return 1
  fi
  [[ "$RELEASE_PATH" == "/v1/releases/$RELEASE_VERSION/$TARGET/spandrel" ]] || {
    err "Signed metadata contains an unexpected artifact path"
    return 1
  }
  if [[ -n "$VERSION" && "$RELEASE_VERSION" != "$VERSION" ]]; then
    err "The server returned version $RELEASE_VERSION instead of $VERSION"
    return 1
  fi

  check_free_space "$DESTINATION" "$RELEASE_SIZE" "Staging the CLI"
  if [[ -z "$OFFLINE_DIRECTORY" ]]; then
    check_free_space "$TEMP_DIRECTORY" "$RELEASE_SIZE" "Downloading the CLI"
    download \
      "$BASE_URL$RELEASE_PATH" \
      "$binary" \
      "Downloading Spandrel $RELEASE_VERSION" \
      "$RELEASE_SIZE"
  fi
  local actual_sha256 actual_size
  actual_size=$(wc -c <"$binary" | tr -d '[:space:]')
  [[ "$actual_size" == "$RELEASE_SIZE" ]] || {
    err "Binary size verification failed"
    return 1
  }
  actual_sha256=$(sha256_file "$binary")
  if [[ "$actual_sha256" != "$RELEASE_SHA256" ]]; then
    err "Binary checksum verification failed"
    err "Expected $RELEASE_SHA256"
    err "Received $actual_sha256"
    return 1
  fi
  ok "Binary checksum verified"
}

install_binary() {
  local source=$TEMP_DIRECTORY/spandrel
  local staged=$DESTINATION/.spandrel.install.$$
  local installed=$DESTINATION/spandrel installed_version=""
  if [[ -x "$installed" ]]; then
    if ! installed_version=$(read_spandrel_version "$installed"); then
      if ((FORCE == 0)); then
        err "The existing Spandrel binary reports an invalid version"
        info "Inspect it or rerun with --force to replace it"
        return 1
      fi
    else
      compare_semver "$RELEASE_VERSION" "$installed_version"
      if ((SEMVER_ORDER < 0)) \
        && { [[ -z "$VERSION" ]] || ((ALLOW_DOWNGRADE == 0)); }; then
        err "Refusing to downgrade Spandrel $installed_version to $RELEASE_VERSION"
        info "A latest release may be stale or replayed"
        info "For an intentional downgrade, use --version $RELEASE_VERSION --allow-downgrade"
        return 1
      fi
      if [[ -z "$VERSION" && -z "$OFFLINE_DIRECTORY" \
        && "$installed_version" != "$RELEASE_VERSION" ]] \
        && ((SEMVER_ORDER == 0)); then
        err "Refusing an ambiguous latest-channel replacement of Spandrel $installed_version with $RELEASE_VERSION"
        info "The releases have equal SemVer precedence but different identities"
        info "For an intentional replacement, use --version $RELEASE_VERSION"
        return 1
      fi
      if ((FORCE == 0)) && [[ "$installed_version" == "$RELEASE_VERSION" ]]; then
        ok "Spandrel $RELEASE_VERSION is already installed"
        return 0
      fi
    fi
  fi
  STAGED_BINARY=$staged
  install -m 0755 "$source" "$staged"
  local staged_version
  if ! staged_version=$(read_spandrel_version "$staged"); then
    err "The downloaded binary could not run on this machine"
    return 1
  fi
  if [[ "$staged_version" != "$RELEASE_VERSION" ]]; then
    err "The downloaded binary reports an unexpected version"
    return 1
  fi
  mv -f "$staged" "$DESTINATION/spandrel"
  STAGED_BINARY=""
  read_spandrel_version "$DESTINATION/spandrel" >/dev/null
  ok "Installed Spandrel $RELEASE_VERSION to $DESTINATION/spandrel"
}

install_short_alias() {
  local alias_path=$DESTINATION/sp
  local marker_path=$DESTINATION/$ALIAS_MARKER_NAME
  local existing="" target=""

  if [[ -L "$alias_path" ]]; then
    target=$(readlink "$alias_path")
    if { [[ "$target" == spandrel || "$target" == "$DESTINATION/spandrel" ]]; } \
      && [[ -f "$marker_path" && ! -L "$marker_path" ]] \
      && [[ "$(cat -- "$marker_path")" == "$ALIAS_MARKER_CONTENT" ]]; then
      ALIAS_SUMMARY="sp"
      ok "The sp alias is ready"
      return 0
    fi
    warn "Not installing the sp alias because $alias_path is already user-managed"
    ALIAS_SUMMARY="skipped (existing $alias_path)"
    return 0
  fi
  if [[ -e "$alias_path" ]]; then
    warn "Not installing the sp alias because $alias_path already exists"
    ALIAS_SUMMARY="skipped (existing $alias_path)"
    return 0
  fi

  if [[ -e "$marker_path" || -L "$marker_path" ]]; then
    if [[ -f "$marker_path" && ! -L "$marker_path" ]] \
      && [[ "$(cat -- "$marker_path")" == "$ALIAS_MARKER_CONTENT" ]]; then
      rm -f -- "$marker_path"
    else
      warn "Not installing the sp alias because $marker_path is already user-managed"
      ALIAS_SUMMARY="skipped (existing $marker_path)"
      return 0
    fi
  fi

  existing=$(command -v sp 2>/dev/null || true)
  if [[ -n "$existing" ]]; then
    warn "Not installing the sp alias because sp already resolves to $existing"
    ALIAS_SUMMARY="skipped (sp resolves to $existing)"
    return 0
  fi

  if ! ln -s spandrel "$alias_path"; then
    warn "The sp alias could not be created; use spandrel instead"
    ALIAS_SUMMARY="not installed"
    return 0
  fi

  if (set -o noclobber; printf '%s\n' "$ALIAS_MARKER_CONTENT" >"$marker_path"); then
    ALIAS_SUMMARY="sp"
    ok "Installed the sp alias"
  else
    target=$(readlink "$alias_path" 2>/dev/null || true)
    if [[ "$target" == spandrel || "$target" == "$DESTINATION/spandrel" ]]; then
      rm -f -- "$alias_path"
    fi
    warn "The sp alias ownership record could not be created; use spandrel instead"
    ALIAS_SUMMARY="not installed"
  fi
}

configure_path() {
  case ":$PATH:" in
    *:"$DESTINATION":*) return 0 ;;
  esac
  if ((MODIFY_PATH == 0)) || [[ "$DESTINATION" != "$HOME/.local/bin" ]]; then
    warn "$DESTINATION is not on PATH"
    info "Add this line to your shell profile: export PATH=\"$DESTINATION:\$PATH\""
    return 0
  fi

  local shell_name profile timestamp
  shell_name=$(basename "${SHELL:-}")
  case "$shell_name" in
    zsh) profile=$HOME/.zshrc ;;
    bash) profile=$HOME/.bashrc ;;
    *)
      warn "$DESTINATION is not on PATH"
      info "Add this line to your shell profile: export PATH=\"$DESTINATION:\$PATH\""
      return 0
      ;;
  esac
  if [[ -f "$profile" ]] && grep -Fq '# >>> spandrel >>>' "$profile"; then
    return 0
  fi
  timestamp=$(date -u +%Y%m%dT%H%M%SZ)
  if [[ -f "$profile" ]]; then
    cp -p "$profile" "$profile.spandrel.bak.$timestamp"
  fi
  {
    printf '\n# >>> spandrel >>>\n'
    # Keep HOME and PATH dynamic for future shell sessions.
    # shellcheck disable=SC2016
    printf 'export PATH="$HOME/.local/bin:$PATH"\n'
    printf '# <<< spandrel <<<\n'
  } >>"$profile"
  ok "Added ~/.local/bin to $profile for new terminal sessions"
  warn "A piped installer cannot change the PATH of the shell that launched it"
  info "Use Spandrel now: $DESTINATION/spandrel doctor"
}

run_setup() {
  ((RUN_SETUP == 1)) || return 0
  local executable=$DESTINATION/spandrel
  # curl occupies stdin in `curl ... | bash`, so reuse an inherited terminal
  # descriptor instead of reopening /dev/tty. On macOS, Crossterm cannot
  # register a newly opened /dev/tty descriptor with kqueue (EINVAL), while a
  # duplicate of the shell's existing terminal descriptor works normally.
  # Stderr is the conventional interactive channel for a piped installer and
  # is inherited read-write from ordinary terminal and SSH sessions.
  if [[ -t 2 ]]; then
    info "Starting secure Spandrel setup"
    if ! SPANDREL_INSTALLER_SETUP=1 "$executable" setup <&2 >&2; then
      err "Interactive setup did not complete"
      info "Retry with: $executable setup"
      return 1
    fi
  else
    warn "No interactive terminal is available"
    info "Finish setup with: $executable setup"
  fi
}

main() {
  header
  setup_proxy
  require_command uname
  require_command install
  require_command ssh-keygen
  if [[ -z "$OFFLINE_DIRECTORY" ]]; then
    require_command curl
    if [[ -z "$VERSION" ]]; then
      require_command base64
    fi
  fi
  detect_target
  detect_installed_agents
  choose_destination
  check_destination
  acquire_lock
  TEMP_DIRECTORY=$(mktemp -d "${TMPDIR:-/tmp}/spandrel-install.XXXXXX")
  load_release
  install_binary
  install_short_alias
  configure_path
  run_setup
  if ((QUIET == 0)); then
    draw_box \
      "Spandrel $RELEASE_VERSION is installed" \
      "Binary: spandrel" \
      "Alias: $ALIAS_SUMMARY" \
      "Verify now: spandrel doctor" \
      "Uninstall: spandrel uninstall"
  fi
}

main
