#!/bin/bash
set -euo pipefail

# SquirrelScan installer
# Usage: curl -fsSL https://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; exit 1; }
info() { echo -e "${BLUE}::${NC} $1" >&2; }

# 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 "Windows is not supported via this installer. Download manually from:\n  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
get_latest_version() {
  local channel="${1:-stable}"
  local api_url="https://api.github.com/repos/${REPO}/releases"
  local response

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

  # 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

  local 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)
  trap "rm -rf '$tmpdir'" EXIT

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

  # Download manifest to get binary filename and checksum
  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
  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
  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"

  log "Running self install..."
  "$tmpdir/squirrel" self install --bin-dir "$bin_dir"
}

# 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
}

# Coding agents (Claude Code, Cursor, …) can drive squirrel via a skill. We don't
# install it automatically — print how to add it so the choice stays explicit.
print_skill_hint() {
  echo ""
  log "Using a coding agent? Add the audit skill so it can run squirrel:"
  info "npx skills add squirrelscan/skills --skill audit-website -y -g"
}

# 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
}

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

  echo ""
  echo "  ____              _                _   ____"
  echo " / ___|  __ _ _   _(_)_ __ _ __ ___| | / ___|  ___ __ _ _ __"
  echo " \\___ \\ / _\` | | | | | '__| '__/ _ \\ | \\___ \\ / __/ _\` | '_ \\"
  echo "  ___) | (_| | |_| | | |  | | |  __/ |  ___) | (_| (_| | | | |"
  echo " |____/ \\__, |\\__,_|_|_|  |_|  \\___|_| |____/ \\___\\__,_|_| |_|"
  echo "           |_|"
  echo ""

  log "Installing SquirrelScan..."

  check_deps

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

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

  # Find writable bin directory in PATH
  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
    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"

  echo ""
  log "Installation complete!"

  # Remind (don't auto-run) how to add the skill for coding agents.
  print_skill_hint

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

  echo ""
}

main "$@"
