#!/bin/bash
set -euo pipefail

# squirrelscan installer
# Usage: curl -fsSL https://install.squirrelscan.com/install.sh | bash
# Or: curl -fsSL https://raw.githubusercontent.com/squirrelscan/squirrelscan/main/install.sh | bash
#
# Environment variables:
#   SQUIRREL_VERSION   - Pin to specific version (e.g., v0.0.15)
#   SQUIRREL_CHANNEL   - Release channel: stable or beta (default: stable)
#   SQUIRREL_BIN_DIR   - Override bin directory for symlink
#   GITHUB_TOKEN       - GitHub token to avoid API rate limits (optional)

REPO="squirrelscan/squirrelscan"

# Detect if stdout is a terminal for colors
if [ -t 1 ]; then
  RED='\033[0;31m'
  GREEN='\033[0;32m'
  YELLOW='\033[0;33m'
  BLUE='\033[0;34m'
  NC='\033[0m'
else
  RED='' GREEN='' YELLOW='' BLUE='' NC=''
fi

log() { echo -e "${GREEN}==>${NC} $1" >&2; }
warn() { echo -e "${YELLOW}Warning:${NC} $1" >&2; }
error() {
  echo -e "${RED}Error:${NC} $1" >&2
  LAST_ERROR_MSG="$1"
  # Also persist to a file: error() often runs inside a command-substitution
  # subshell (e.g. version=$(get_latest_version)), where the LAST_ERROR_MSG
  # assignment can't reach the parent that runs the EXIT trap.
  [ -n "${ERROR_MSG_FILE:-}" ] && printf '%s' "$1" >"$ERROR_MSG_FILE" 2>/dev/null || true
  exit 1
}
info() { echo -e "${BLUE}::${NC} $1" >&2; }

# --- Failure reporting ----------------------------------------------------
# On failure, fire a tiny anonymous report to the installer worker (→ Sentry)
# so we can see when installs break in the field. Strictly opt-out via
# NO_TELEMETRY (mirrors the CLI, apps/cli/src/self/telemetry.ts), fire-and-
# forget (never blocks or fails the install), and carries only coarse context
# (os/arch/step/exit code) — never paths, env, hostname, or secrets. #1013
# v2 adds `error_output` — the tail of the failing command's own output (#1538).
INSTALLER_REPORT_VERSION="2"
ERROR_ENDPOINT="${SQUIRREL_ERROR_ENDPOINT:-https://install.squirrelscan.com/error}"
# Release metadata (latest version per channel) — R2-backed, no rate limits.
RELEASES_ENDPOINT="${SQUIRREL_RELEASES_ENDPOINT:-https://install.squirrelscan.com/releases}"
ERROR_LINE_MAX=200
# Chars of captured command output carried in a report. The worker clamps again.
ERROR_OUTPUT_MAX=1000
CURRENT_STEP="init"
LAST_ERROR_MSG=""
# Captured stdout/stderr of the step that failed, when we ran it under tee.
LAST_ERROR_OUTPUT=""
# Real exit code of a failed sub-command, when it isn't the one error() exits with.
LAST_ERROR_CODE=""
TMPDIR_TO_CLEAN=""
# Survives command-substitution subshells where LAST_ERROR_MSG can't (see error()).
ERROR_MSG_FILE="$(mktemp 2>/dev/null || true)"

# Make a value safe inside a JSON string: keep only printable ASCII (drops
# control chars incl. ESC from ANSI colour codes, AND non-ASCII bytes — either
# would make the JSON invalid and the worker would reject the whole report),
# then escape backslashes and double-quotes.
json_escape() {
  printf '%s' "$1" | LC_ALL=C tr -cd '\40-\176' | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}

# Reduce a value to printable ASCII (control chars incl. newlines AND non-ASCII
# bytes → space) BEFORE truncating, so `cut -c` can't split a multibyte char
# into invalid UTF-8. Scrub $HOME → '~' so no local path leaks. Command output
# keeps its TAIL (that's where the error is); a message keeps its head. The
# worker re-clamps and redacts too.
scrub_for_report() {
  local text="$1" max="$2" keep_tail="${3:-head}" scrubbed tilde="~"
  if [ -z "$text" ]; then
    return 0
  fi
  scrubbed=$(printf '%s' "$text" | LC_ALL=C tr -c '\40-\176' ' ')
  if [ -n "${HOME:-}" ]; then
    scrubbed=${scrubbed//"$HOME"/$tilde}
  fi
  if [ "$keep_tail" = tail ] && [ "${#scrubbed}" -gt "$max" ]; then
    printf '%s' "${scrubbed:${#scrubbed}-max}"
  else
    printf '%s' "$scrubbed" | cut -c"1-$max"
  fi
}

report_error() {
  local step="$1" code="$2" line="${3:-}" output="${4:-}"
  # Presence disables reporting, including an explicitly empty value.
  [ "${NO_TELEMETRY+x}" = x ] && return 0
  command -v curl >/dev/null 2>&1 || return 0

  local os arch scrubbed="" scrubbed_output=""
  os=$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')
  arch=$(uname -m 2>/dev/null)

  scrubbed=$(scrub_for_report "$line" "$ERROR_LINE_MAX")
  # The failing command's own stdout/stderr: without it a self_install failure
  # carried an exit code and nothing else (#1538).
  scrubbed_output=$(scrub_for_report "$output" "$ERROR_OUTPUT_MAX" tail)

  local payload
  payload=$(printf '{"script":"sh","script_version":"%s","channel":"%s","os":"%s","arch":"%s","step":"%s","exit_code":%s,"error_line":"%s","error_output":"%s"}' \
    "$(json_escape "$INSTALLER_REPORT_VERSION")" \
    "$(json_escape "${SQUIRREL_CHANNEL:-stable}")" \
    "$(json_escape "$os")" \
    "$(json_escape "$arch")" \
    "$(json_escape "$step")" \
    "${code:-1}" \
    "$(json_escape "$scrubbed")" \
    "$(json_escape "$scrubbed_output")")

  # Fire-and-forget: run the POST in a DETACHED subshell so it never blocks the
  # installer. `( cmd & )` backgrounds curl and lets the subshell exit
  # immediately (non-blocking); curl is reparented to init so it still gets to
  # finish after the script exits. Tight timeouts are the backstop; stdio
  # discarded so a closed pipe can't SIGPIPE it.
  ( curl -fsS -m 3 --connect-timeout 2 -X POST \
      -H 'Content-Type: application/json' \
      --data "$payload" "$ERROR_ENDPOINT" >/dev/null 2>&1 & ) 2>/dev/null || true
}

# Single EXIT trap: cleans the temp dir and reports genuine failures. Replaces
# the per-call tmpdir trap (a second `trap ... EXIT` would clobber this one).
report_on_exit() {
  local code=$?
  set +e  # cleanup/reporting must never derail the exit path
  [ -n "$TMPDIR_TO_CLEAN" ] && rm -rf "$TMPDIR_TO_CLEAN"
  # Prefer the in-process message; fall back to the file for subshell failures.
  local msg="$LAST_ERROR_MSG"
  if [ -z "$msg" ] && [ -n "${ERROR_MSG_FILE:-}" ] && [ -f "$ERROR_MSG_FILE" ]; then
    msg=$(cat "$ERROR_MSG_FILE" 2>/dev/null)
  fi
  [ -n "${ERROR_MSG_FILE:-}" ] && rm -f "$ERROR_MSG_FILE"
  [ "$code" -eq 0 ] && return 0
  # error() always exits 1; prefer the failing sub-command's own code when the
  # call site recorded one (self install, which runs under tee).
  [ -n "$LAST_ERROR_CODE" ] && code="$LAST_ERROR_CODE"
  report_error "$CURRENT_STEP" "$code" "$msg" "$LAST_ERROR_OUTPUT"
}
trap report_on_exit EXIT

# --- Banner ---------------------------------------------------------------
# Blocky lowercase "squirrelscan" wordmark, matching the CLI's own banner
# (apps/cli/src/cli/banner.ts) instead of the old camel-case figlet art.
# BANNER_ART_COLOR is a precomputed copy of that file's gradient-string
# output for the autumn palette (#CD853F -> #D2691E -> #8B4513 -> #A0522D) --
# this installer has no Node/gradient-string available at curl|bash time.
BANNER_ART_PLAIN=' ▄█▀ ▄▀█ █ █ █ █▀▄ █▀▄ █▀▀ █   ▄█▀ ▄▀▀ ▄▀█ █▄ █
 ▀▄  █ █ █ █ █ ██▀ ██▀ █▀  █   ▀▄  █   █▀█ █ ▀█
 █▄▀ ▀▀█ ▀▄▀ █ █ █ █ █ █▄▄ █▄▄ █▄▀ ▀▄▄ █ █ █  █'

BANNER_ART_COLOR=$' \033[38;2;205;133;63m▄\033[39m\033[38;2;205;132;62m█\033[39m\033[38;2;205;131;61m▀\033[39m \033[38;2;206;130;60m▄\033[39m\033[38;2;206;129;58m▀\033[39m\033[38;2;206;128;57m█\033[39m \033[38;2;206;127;56m█\033[39m \033[38;2;206;126;55m█\033[39m \033[38;2;206;125;54m█\033[39m \033[38;2;207;124;53m█\033[39m\033[38;2;207;123;52m▀\033[39m\033[38;2;207;122;50m▄\033[39m \033[38;2;207;121;49m█\033[39m\033[38;2;207;120;48m▀\033[39m\033[38;2;207;119;47m▄\033[39m \033[38;2;208;119;46m█\033[39m\033[38;2;208;118;45m▀\033[39m\033[38;2;208;117;44m▀\033[39m \033[38;2;208;116;43m█\033[39m   \033[38;2;208;115;41m▄\033[39m\033[38;2;208;114;40m█\033[39m\033[38;2;209;113;39m▀\033[39m \033[38;2;209;112;38m▄\033[39m\033[38;2;209;111;37m▀\033[39m\033[38;2;209;110;36m▀\033[39m \033[38;2;209;109;35m▄\033[39m\033[38;2;209;108;33m▀\033[39m\033[38;2;210;107;32m█\033[39m \033[38;2;210;106;31m█\033[39m\033[38;2;210;105;30m▄\033[39m \033[38;2;207;104;30m█\033[39m\n \033[38;2;205;102;29m▀\033[39m\033[38;2;202;101;29m▄\033[39m  \033[38;2;200;100;28m█\033[39m \033[38;2;197;99;28m█\033[39m \033[38;2;195;97;28m█\033[39m \033[38;2;192;96;27m█\033[39m \033[38;2;190;95;27m█\033[39m \033[38;2;187;93;26m█\033[39m\033[38;2;185;92;26m█\033[39m\033[38;2;182;91;26m▀\033[39m \033[38;2;180;90;25m█\033[39m\033[38;2;177;88;25m█\033[39m\033[38;2;175;87;25m▀\033[39m \033[38;2;172;86;24m█\033[39m\033[38;2;169;84;24m▀\033[39m  \033[38;2;167;83;23m█\033[39m   \033[38;2;164;82;23m▀\033[39m\033[38;2;162;81;23m▄\033[39m  \033[38;2;159;79;22m█\033[39m   \033[38;2;157;78;22m█\033[39m\033[38;2;154;77;21m▀\033[39m\033[38;2;152;75;21m█\033[39m \033[38;2;149;74;21m█\033[39m \033[38;2;147;73;20m▀\033[39m\033[38;2;144;72;20m█\033[39m\n \033[38;2;142;70;19m█\033[39m\033[38;2;139;69;19m▄\033[39m\033[38;2;140;69;20m▀\033[39m \033[38;2;141;70;21m▀\033[39m\033[38;2;141;70;22m▀\033[39m\033[38;2;142;71;23m█\033[39m \033[38;2;143;71;24m▀\033[39m\033[38;2;144;72;25m▄\033[39m\033[38;2;144;72;26m▀\033[39m \033[38;2;145;73;26m█\033[39m \033[38;2;146;73;27m█\033[39m \033[38;2;147;74;28m█\033[39m \033[38;2;147;74;29m█\033[39m \033[38;2;148;75;30m█\033[39m \033[38;2;149;75;31m█\033[39m\033[38;2;150;76;32m▄\033[39m\033[38;2;150;76;33m▄\033[39m \033[38;2;151;76;34m█\033[39m\033[38;2;152;77;35m▄\033[39m\033[38;2;153;77;36m▄\033[39m \033[38;2;153;78;37m█\033[39m\033[38;2;154;78;38m▄\033[39m\033[38;2;155;79;39m▀\033[39m \033[38;2;156;79;39m▀\033[39m\033[38;2;156;80;40m▄\033[39m\033[38;2;157;80;41m▄\033[39m \033[38;2;158;81;42m█\033[39m \033[38;2;159;81;43m█\033[39m \033[38;2;159;82;44m█\033[39m  \033[38;2;160;82;45m█\033[39m'

BANNER_TEXT_FALLBACK='squirrelscan'

# Half-block glyphs need a UTF-8 locale to render correctly; CI logs and
# dumb terminals often run C/POSIX. Fall back to plain text there.
is_utf8_locale() {
  local charmap=""
  if command -v locale >/dev/null 2>&1; then
    charmap=$(locale charmap 2>/dev/null || true)
  fi
  case "$charmap" in
    *UTF-8*|*utf-8*|*UTF8*|*utf8*) return 0 ;;
  esac
  local loc="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
  case "$loc" in
    *UTF-8*|*utf-8*|*UTF8*|*utf8*) return 0 ;;
    *) return 1 ;;
  esac
}

print_banner() {
  echo ""
  if ! is_utf8_locale; then
    echo "  $BANNER_TEXT_FALLBACK"
  elif [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    printf '%s\n' "$BANNER_ART_COLOR"
  else
    printf '%s\n' "$BANNER_ART_PLAIN"
  fi
  echo ""
}

# Check for required commands
check_deps() {
  command -v curl >/dev/null 2>&1 || error "curl is required but not installed"

  if ! command -v jq >/dev/null 2>&1; then
    warn "jq not found, using grep fallback (less reliable)"
    USE_JQ=false
  else
    USE_JQ=true
  fi
}

# Compute SHA256 checksum with fallbacks
compute_sha256() {
  local file="$1"
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$file" | cut -d' ' -f1
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$file" | cut -d' ' -f1
  elif command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$file" | awk '{print $NF}'
  else
    error "No SHA256 tool found (need sha256sum, shasum, or openssl)"
  fi
}

# Fetch with retry and timeout
fetch_with_retry() {
  local url="$1"
  local output="$2"
  local attempts=3
  local timeout_connect=10
  local timeout_max=120

  for i in $(seq 1 $attempts); do
    if curl -fsSL --connect-timeout "$timeout_connect" --max-time "$timeout_max" "$url" -o "$output" 2>/dev/null; then
      return 0
    fi
    if [ "$i" -lt "$attempts" ]; then
      warn "Download failed, retrying ($i/$attempts)..."
      sleep 2
    fi
  done
  return 1
}

# Detect libc (glibc vs musl)
detect_libc() {
  # Method 1: Check for musl loader (most reliable)
  if ls /lib/ld-musl-*.so.1 >/dev/null 2>&1; then
    echo "-musl"
    return
  fi

  # Method 2: Check Alpine release file
  if [ -f /etc/alpine-release ]; then
    echo "-musl"
    return
  fi

  # Method 3: ldd version string
  if command -v ldd >/dev/null 2>&1; then
    if ldd --version 2>&1 | grep -qi musl; then
      echo "-musl"
      return
    fi
  fi

  # Default: glibc (no suffix)
  echo ""
}

# Detect platform and architecture
detect_platform() {
  local os arch libc=""

  os=$(uname -s | tr '[:upper:]' '[:lower:]')
  arch=$(uname -m)

  case "$os" in
    darwin) os="darwin" ;;
    linux)
      os="linux"
      libc=$(detect_libc)
      ;;
    freebsd)
      error "FreeBSD is not yet supported. See: https://github.com/${REPO}/issues"
      ;;
    mingw*|msys*|cygwin*)
      error "This installer is for macOS/Linux. On Windows, run this in PowerShell instead:\n  powershell -c \"irm https://install.squirrelscan.com/install.ps1 | iex\"\n  Or download manually from: https://github.com/${REPO}/releases"
      ;;
    *) error "Unsupported OS: $os" ;;
  esac

  case "$arch" in
    x86_64|amd64) arch="x64" ;;
    arm64|aarch64) arch="arm64" ;;
    *) error "Unsupported architecture: $arch" ;;
  esac

  echo "${os}-${arch}${libc}"
}

# Ensure the musl C++ runtime is present.
# bun's musl --compile binary dynamically links libstdc++.so.6 + libgcc_s.so.1,
# which a bare Alpine image lacks — without them the binary can't even run
# `self install`. Auto-install as root via apk; otherwise print the exact
# command and exit cleanly (better than a wall of relocation errors).
ensure_musl_runtime() {
  # Already resolvable by the musl loader (/lib or /usr/lib)?
  if ls /usr/lib/libstdc++.so.6 >/dev/null 2>&1 ||
    ls /lib/libstdc++.so.6 >/dev/null 2>&1; then
    return 0
  fi

  if command -v apk >/dev/null 2>&1 && [ "$(id -u)" = "0" ]; then
    log "Installing required runtime library (libstdc++)..."
    if apk add --no-cache libstdc++ >/dev/null 2>&1; then
      info "Installed libstdc++"
      return 0
    fi
    warn "Auto-install of libstdc++ failed"
  fi

  # Non-root, no apk, or apk failed → clear, actionable instructions.
  warn "squirrel needs libstdc++ to run on Alpine/musl."
  if command -v apk >/dev/null 2>&1; then
    if [ "$(id -u)" = "0" ]; then
      echo "  Install it, then re-run the installer:" >&2
      echo "      apk add libstdc++" >&2
    else
      echo "  Install it, then re-run the installer:" >&2
      echo "      sudo apk add libstdc++" >&2
    fi
  else
    echo "  Install libstdc++ (and libgcc) with your package manager, then re-run." >&2
  fi
  error "Missing libstdc++ (required by the musl build)"
}

# Find a writable bin directory that's in PATH
find_bin_dir() {
  # If user explicitly set bin dir, use it
  if [ -n "${SQUIRREL_BIN_DIR:-}" ]; then
    mkdir -p "$SQUIRREL_BIN_DIR" 2>/dev/null || true
    if [ -d "$SQUIRREL_BIN_DIR" ] && [ -w "$SQUIRREL_BIN_DIR" ]; then
      echo "$SQUIRREL_BIN_DIR"
      return 0
    else
      warn "SQUIRREL_BIN_DIR=$SQUIRREL_BIN_DIR is not writable, searching PATH..."
    fi
  fi

  # Priority order of common bin directories
  local common_dirs=(
    "$HOME/.local/bin"      # XDG standard
    "$HOME/bin"             # Traditional user bin
    "/usr/local/bin"        # System-wide
    "/opt/homebrew/bin"     # macOS ARM Homebrew
  )

  # Parse PATH into array
  local path_dirs
  IFS=':' read -ra path_dirs <<< "$PATH"

  # First: check if any common dir is already in PATH and writable
  for dir in "${common_dirs[@]}"; do
    for path_dir in "${path_dirs[@]}"; do
      if [ "$dir" = "$path_dir" ]; then
        if [ -d "$dir" ] && [ -w "$dir" ]; then
          echo "$dir"
          return 0
        elif [ ! -e "$dir" ]; then
          # Directory doesn't exist but parent might be writable
          local parent
          parent=$(dirname "$dir")
          if [ -w "$parent" ]; then
            mkdir -p "$dir" 2>/dev/null && echo "$dir" && return 0
          fi
        fi
      fi
    done
  done

  # Fallback: create ~/.local/bin (will need PATH modification)
  mkdir -p "$HOME/.local/bin" 2>/dev/null
  echo "$HOME/.local/bin"
  return 1  # Signal that PATH update needed
}

# JSON value extraction with grep fallback
json_get() {
  local json="$1" key="$2"
  echo "$json" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | \
    sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1
}

# JSON nested value extraction (for binaries["platform"])
json_get_nested() {
  local json="$1" outer="$2" inner="$3"
  # Extract the outer block first, then the inner value
  local block
  block=$(echo "$json" | tr '\n' ' ' | grep -o "\"$outer\"[[:space:]]*:[[:space:]]*{[^}]*}" | head -1)
  echo "$block" | grep -o "\"$inner\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | \
    sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/' | head -1
}

# Get latest release version.
# Primary: install.squirrelscan.com/releases/{channel} — R2-backed release
# metadata with no rate limits. Fallback: the GitHub API, which anonymous
# clients share at 60 req/hr per IP — corporate NAT/VPN/CI egress hits 403s.
get_latest_version() {
  local channel="${1:-stable}"
  local api_url="https://api.github.com/repos/${REPO}/releases"
  local response version=""

  info "Fetching releases (channel: $channel)..."

  if response=$(curl -fsSL -H "User-Agent: squirrelscan-installer" \
      --connect-timeout 5 --max-time 15 \
      "${RELEASES_ENDPOINT}/${channel}" 2>/dev/null); then
    if [ "$USE_JQ" = true ]; then
      version=$(echo "$response" | jq -r '.version // empty')
    else
      version=$(json_get "$response" "version")
    fi
    if [ -n "$version" ]; then
      # Manifest versions are bare ("0.0.73"); release tags carry the v prefix.
      echo "v${version#v}"
      return 0
    fi
  fi
  warn "Release metadata endpoint unavailable, falling back to GitHub API..."

  # Build curl args - add auth header if GITHUB_TOKEN is set (avoids rate limits)
  local curl_args=(-fsSL -H "User-Agent: squirrelscan-installer" --connect-timeout 10 --max-time 30)
  if [ -n "${GITHUB_TOKEN:-}" ]; then
    curl_args+=(-H "Authorization: token $GITHUB_TOKEN")
  fi

  if ! response=$(curl "${curl_args[@]}" "$api_url" 2>&1); then
    error "Failed to fetch releases\n  URL: $api_url\n  Response: $response"
  fi

  if [ -z "$response" ] || [ "$response" = "[]" ]; then
    error "No releases found\n  Check: https://github.com/${REPO}/releases"
  fi

  version=""
  if [ "$USE_JQ" = true ]; then
    if [ "$channel" = "stable" ]; then
      version=$(echo "$response" | jq -r '[.[] | select(.prerelease == false)] | .[0].tag_name // empty')
    else
      version=$(echo "$response" | jq -r '.[0].tag_name // empty')
    fi
  else
    # Grep fallback - just get first tag (works for beta, imprecise for stable)
    version=$(echo "$response" | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)
  fi

  echo "$version"
}

# Download binary and run self install
download_and_install() {
  local version="$1"
  local platform="$2"
  local bin_dir="$3"
  local tmpdir

  tmpdir=$(mktemp -d)
  # Cleaned by the EXIT trap (report_on_exit) — do NOT set a local EXIT trap
  # here or it clobbers the failure-reporting trap.
  TMPDIR_TO_CLEAN="$tmpdir"

  local release_url="https://github.com/${REPO}/releases/download/${version}"

  # Download manifest to get binary filename and checksum
  CURRENT_STEP="download_manifest"
  log "Downloading manifest..."
  local manifest_url="${release_url}/manifest.json"
  if ! fetch_with_retry "$manifest_url" "$tmpdir/manifest.json"; then
    error "Failed to download manifest\n  URL: $manifest_url"
  fi

  # Read manifest content
  local manifest
  manifest=$(cat "$tmpdir/manifest.json")

  # Extract binary info
  local filename sha256
  if [ "$USE_JQ" = true ]; then
    filename=$(echo "$manifest" | jq -r ".binaries[\"${platform}\"].filename // empty")
    sha256=$(echo "$manifest" | jq -r ".binaries[\"${platform}\"].sha256 // empty")
  else
    filename=$(json_get_nested "$manifest" "$platform" "filename")
    sha256=$(json_get_nested "$manifest" "$platform" "sha256")
  fi

  if [ -z "$filename" ] || [ -z "$sha256" ]; then
    error "No binary for platform: $platform\n  See: https://github.com/${REPO}/releases/tag/${version}"
  fi

  # Download binary
  CURRENT_STEP="download_binary"
  log "Downloading squirrel ${version}..."
  local binary_url="${release_url}/${filename}"
  if ! fetch_with_retry "$binary_url" "$tmpdir/squirrel"; then
    error "Failed to download binary\n  URL: $binary_url"
  fi

  # Verify checksum
  CURRENT_STEP="verify_checksum"
  log "Verifying checksum..."
  local actual_sha256
  actual_sha256=$(compute_sha256 "$tmpdir/squirrel")

  if [ "$actual_sha256" != "$sha256" ]; then
    error "Checksum mismatch!\n  Expected: ${sha256}\n  Actual:   ${actual_sha256}"
  fi
  info "Checksum verified: ${sha256:0:16}..."

  # Make executable and run self install with bin dir
  chmod +x "$tmpdir/squirrel"

  CURRENT_STEP="self_install"
  log "Running self install..."
  # Tee rather than run bare: the user still sees the output live, and a failure
  # can report what the binary actually said instead of a bare exit code
  # (#1538). PIPESTATUS[0] is the binary's code, not tee's.
  local self_install_log="$tmpdir/self-install.log"
  set +e
  "$tmpdir/squirrel" self install --bin-dir "$bin_dir" 2>&1 | tee "$self_install_log"
  local rc=${PIPESTATUS[0]}
  set -e
  if [ "$rc" -ne 0 ]; then
    # Last lines, not last bytes: a byte cut can split a $HOME path so the
    # scrubber no longer recognizes it and the username rides along. The tail
    # is where the failure is; report_error scrubs and clamps what we pass.
    LAST_ERROR_OUTPUT=$(tail -n 40 "$self_install_log" 2>/dev/null || true)
    LAST_ERROR_CODE="$rc"
    error "Self install failed with exit code $rc"
  fi
}

# Detect user's shell and config file
detect_shell_config() {
  local shell="${SHELL:-}"

  # Try to detect from SHELL env var
  if [ -n "$shell" ]; then
    case "$shell" in
      */zsh)  echo "zsh:$HOME/.zshrc" ;;
      */bash)
        # Prefer .bashrc, but use .bash_profile on macOS if .bashrc doesn't exist
        if [ -f "$HOME/.bashrc" ]; then
          echo "bash:$HOME/.bashrc"
        elif [ -f "$HOME/.bash_profile" ]; then
          echo "bash:$HOME/.bash_profile"
        else
          echo "bash:$HOME/.bashrc"
        fi
        ;;
      */fish) echo "fish:$HOME/.config/fish/config.fish" ;;
      */sh)   echo "sh:$HOME/.profile" ;;
      *)      echo "unknown:$HOME/.profile" ;;
    esac
    return
  fi

  # Fallback: check which shell configs exist
  if [ -f "$HOME/.zshrc" ]; then
    echo "zsh:$HOME/.zshrc"
  elif [ -f "$HOME/.bashrc" ]; then
    echo "bash:$HOME/.bashrc"
  elif [ -f "$HOME/.bash_profile" ]; then
    echo "bash:$HOME/.bash_profile"
  else
    echo "unknown:$HOME/.profile"
  fi
}

# Get-started epilogue: one scannable next-steps block instead of the old
# scattered "Tip:"/"Installation complete!"/skill-hint tail (#1029).
# "squirrel skills install" is the canonical path (installs both skills,
# no --skill filter); the npx fallback line stays copy-paste-able for
# docs/agents that can't shell out to the freshly-installed binary.
print_epilogue() {
  local version="$1"
  # Same UTF-8 gate as the banner -- CI logs / dumb terminals shouldn't get
  # mojibake from the checkmark/arrow glyphs either (codex review, #1029).
  local check="✓" arrow="→"
  if ! is_utf8_locale; then
    check="v"
    arrow="->"
  fi
  echo ""
  echo -e "${GREEN}${check}${NC} squirrel ${version} installed"
  echo ""
  echo "Get started:"
  echo "  1. Run your first audit:   squirrel audit https://your-site.com"
  echo "  2. Add agent skills:       squirrel skills install   (Claude Code, Cursor, Codex, ...)"
  echo "                             or: npx skills add squirrelscan/skills -y -g"
  echo "  3. Unlock cloud audits:    squirrel auth login       ${arrow} https://squirrelscan.com/login"
  echo "  Shell completions:         squirrel self completion <bash|zsh|fish>"
  echo "  Docs: https://docs.squirrelscan.com"
  echo ""
}

# Print shell profile instructions
print_path_instructions() {
  local bin_dir="$1"
  local shell_info rc_file shell_name

  shell_info=$(detect_shell_config)
  shell_name="${shell_info%%:*}"
  rc_file="${shell_info#*:}"

  warn "$bin_dir is not in your PATH"
  echo ""

  case "$shell_name" in
    fish)
      echo "Add to $rc_file:"
      echo ""
      echo "  fish_add_path $bin_dir"
      echo ""
      echo "Or run now:"
      echo ""
      echo "  echo 'fish_add_path $bin_dir' >> $rc_file && source $rc_file"
      ;;
    zsh|bash|sh|unknown)
      echo "Add to $rc_file:"
      echo ""
      echo "  export PATH=\"$bin_dir:\$PATH\""
      echo ""
      echo "Or run now:"
      echo ""
      echo "  echo 'export PATH=\"$bin_dir:\$PATH\"' >> $rc_file && source $rc_file"
      ;;
  esac

  echo ""
  echo "After updating PATH, verify with: squirrel self doctor"
}

main() {
  local channel="${SQUIRREL_CHANNEL:-stable}"

  print_banner

  log "Installing squirrel..."

  CURRENT_STEP="check_deps"
  check_deps

  local platform version bin_dir needs_path_update=false
  CURRENT_STEP="detect_platform"
  platform=$(detect_platform)
  log "Detected platform: $platform"

  # musl builds need libstdc++ at runtime — ensure it before we exec the binary.
  case "$platform" in
    *-musl) CURRENT_STEP="musl_runtime"; ensure_musl_runtime ;;
  esac

  # Find writable bin directory in PATH
  CURRENT_STEP="find_bin_dir"
  if ! bin_dir=$(find_bin_dir); then
    needs_path_update=true
  fi
  info "Bin directory: $bin_dir"

  # Version: pinned or latest
  if [ -n "${SQUIRREL_VERSION:-}" ]; then
    version="$SQUIRREL_VERSION"
    log "Installing pinned version: $version"
  else
    CURRENT_STEP="fetch_releases"
    version=$(get_latest_version "$channel")
    if [ -z "$version" ]; then
      if [ "$channel" = "stable" ]; then
        error "No stable releases found. Try:\n  SQUIRREL_CHANNEL=beta curl -fsSL ... | bash"
      else
        error "No releases found for channel '$channel'\n  Check: https://github.com/${REPO}/releases"
      fi
    fi
    log "Latest version: $version (channel: $channel)"
  fi

  download_and_install "$version" "$platform" "$bin_dir"

  print_epilogue "$version"

  # Print PATH instructions if needed
  if [ "$needs_path_update" = true ]; then
    echo ""
    print_path_instructions "$bin_dir"
  fi

  echo ""
}

main "$@"
