#!/usr/bin/env bash

# `use` and `err` always exit, so code following them is reachable but flagged.
# shellcheck disable=SC2317

set -eo pipefail

# NOTE: if you make modifications to this script, please increment the version number.
# WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date.
FOUNDRYUP_INSTALLER_VERSION="1.9.2"

BASE_DIR=${XDG_CONFIG_HOME:-$HOME}
FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"}
FOUNDRY_VERSIONS_DIR="$FOUNDRY_DIR/versions"
FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin"
FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1"
FOUNDRY_BIN_URL="https://raw.githubusercontent.com/foundry-rs/foundry/HEAD/foundryup/foundryup"
FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup"
FOUNDRYUP_JOBS=""
FOUNDRYUP_IGNORE_VERIFICATION=false

# ----------------------------------------------------------------------------
# Rust foundryup migration shim
#
# The bash foundryup is being replaced by the Rust rewrite
# (https://github.com/foundry-rs/foundryup). When opted in, this script
# bootstraps the Rust `foundryup` binary into a sidecar location and execs it,
# migrating the caller (including GitHub Actions that re-fetch this script).
#
# Migration is OPT-IN for now: set FOUNDRYUP_MIGRATE=1 to use the Rust
# foundryup. The default path remains the legacy bash implementation until the
# migration is proven, after which the default will flip.
# ----------------------------------------------------------------------------

# Pinned Rust foundryup installer version to bootstrap from.
FOUNDRYUP_BOOTSTRAP_VERSION="${FOUNDRYUP_BOOTSTRAP_VERSION:-0.0.5}"
# `foundryup-init.sh` for the pinned bootstrap version.
FOUNDRYUP_INIT_URL="https://raw.githubusercontent.com/foundry-rs/foundryup/v${FOUNDRYUP_BOOTSTRAP_VERSION}/foundryup-init.sh"
# Sidecar home for the Rust foundryup binary. Kept separate from the bash shim
# at $FOUNDRY_BIN_DIR/foundryup so the installer never overwrites this script.
FOUNDRYUP_RUST_HOME="${FOUNDRYUP_RUST_HOME:-"$FOUNDRY_DIR/foundryup"}"

# Retry/backoff settings used for `fetch` (GitHub API calls).
# Recovers from transient HTTP 403/429/5xx responses returned by
# api.github.com under heavy load or per-IP rate limiting.
FOUNDRYUP_MAX_RETRIES=5
FOUNDRYUP_RETRY_DELAY=2
FOUNDRYUP_RETRY_MAX_TIME=60
FOUNDRYUP_NIGHTLY_CANDIDATE_LIMIT=5

BINS=(forge cast anvil chisel solar)
HASH_NAMES=()
HASH_VALUES=()
FOUNDRYUP_NIGHTLY_CANDIDATES=()

export RUSTFLAGS="${RUSTFLAGS:--C target-cpu=native}"

main() {
  need_cmd git
  need_cmd curl

  while [[ -n $1 ]]; do
    case $1 in
      --)               shift; break;;

      -v|--version)     shift; version;;
      -U|--update)      shift; update;;
      -r|--repo)        shift; FOUNDRYUP_REPO=$1;;
      -b|--branch)      shift; FOUNDRYUP_BRANCH=$1;;
      -i|--install)     shift; FOUNDRYUP_VERSION=$1;;
      -l|--list)        shift; list;;
      -u|--use)         shift; FOUNDRYUP_VERSION=$1; use;;
      -p|--path)        shift; FOUNDRYUP_LOCAL_REPO=$1;;
      -P|--pr)          shift; FOUNDRYUP_PR=$1;;
      -C|--commit)      shift; FOUNDRYUP_COMMIT=$1;;
      -j|--jobs)        shift; FOUNDRYUP_JOBS=$1;;
      -n|--network)     shift; FOUNDRYUP_NETWORK=$1;;
      -f|--force)       FOUNDRYUP_IGNORE_VERIFICATION=true;;
      --arch)           shift; FOUNDRYUP_ARCH=$1;;
      --platform)       shift; FOUNDRYUP_PLATFORM=$1;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        warn "unknown option: $1"
        usage
        exit 1
    esac; shift
  done

  CARGO_BUILD_ARGS=(--release)

  if [ -n "$FOUNDRYUP_JOBS" ]; then
    CARGO_BUILD_ARGS+=(--jobs "$FOUNDRYUP_JOBS")
  fi

  # Print the banner after successfully parsing args
  banner

  # Check if the foundryup installer is up to date, warn the user if not
  check_installer_up_to_date

  if [ -n "$FOUNDRYUP_PR" ]; then
    if [ -z "$FOUNDRYUP_BRANCH" ]; then
      FOUNDRYUP_BRANCH="refs/pull/$FOUNDRYUP_PR/head"
    else
      err "can't use --pr and --branch at the same time"
    fi
  fi

  check_bins_in_use

  # Installs foundry from a local repository if --path parameter is provided
  if [[ -n "$FOUNDRYUP_LOCAL_REPO" ]]; then
    need_cmd cargo

    # Ignore branches/versions as we do not want to modify local git state
    if [ -n "$FOUNDRYUP_REPO" ] || [ -n "$FOUNDRYUP_BRANCH" ] || [ -n "$FOUNDRYUP_VERSION" ]; then
      warn "--branch, --install, --use, and --repo arguments are ignored during local install"
    fi

    # Enter local repo and build
    say "installing from $FOUNDRYUP_LOCAL_REPO"
    cd "$FOUNDRYUP_LOCAL_REPO"
    clear_built_bins "$PWD/target/release"
    ensure cargo build --bins "${CARGO_BUILD_ARGS[@]}"

    for bin in "${BINS[@]}"; do
      # Remove prior installations if they exist
      rm -f "$FOUNDRY_BIN_DIR/$bin"
      if is_optional_bin "$bin" && [ ! -f "$PWD/target/release/$bin" ]; then
        continue
      fi
      # Symlink from local repo binaries to bin dir
      ensure ln -s "$PWD/target/release/$bin" "$FOUNDRY_BIN_DIR/$bin"
    done

    say "done"
    exit 0
  fi

  if [[ -n "$FOUNDRYUP_NETWORK" ]]; then
    warn "--network is deprecated and ignored; installing the regular Foundry release"
  fi

  FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry}

  # Install by downloading binaries
  if [[ "$FOUNDRYUP_REPO" == "foundry-rs/foundry" && -z "$FOUNDRYUP_BRANCH" && -z "$FOUNDRYUP_COMMIT" ]]; then
    FOUNDRYUP_VERSION=${FOUNDRYUP_VERSION:-latest}
    FOUNDRYUP_REQUESTED_VERSION="$FOUNDRYUP_VERSION"
    resolve_version_and_tag

    say "installing foundry (version ${FOUNDRYUP_VERSION}, tag ${FOUNDRYUP_TAG})"

    # Detect platform and architecture.
    detect_platform_arch

    if [[ "$FOUNDRYUP_REQUESTED_VERSION" == "nightly" && ${#FOUNDRYUP_NIGHTLY_CANDIDATES[@]} -gt 1 ]]; then
      FOUNDRYUP_TAG=$(select_installable_nightly_tag)
      say "resolved installable nightly release tag: ${FOUNDRYUP_TAG}"
    fi

    # Compute the URL of the release tarball in the Foundry repository.
    RELEASE_URL="https://github.com/${FOUNDRYUP_REPO}/releases/download/${FOUNDRYUP_TAG}/"
    ATTESTATION_URL="${RELEASE_URL}foundry_${FOUNDRYUP_VERSION}_${PLATFORM}_${ARCHITECTURE}.attestation.txt"
    BIN_ARCHIVE_URL="${RELEASE_URL}foundry_${FOUNDRYUP_VERSION}_${PLATFORM}_${ARCHITECTURE}.$EXT"
    MAN_TARBALL_URL="${RELEASE_URL}foundry_man_${FOUNDRYUP_VERSION}.tar.gz"

    ensure mkdir -p "$FOUNDRY_VERSIONS_DIR"

    # If `--force` is set, skip the SHA verification.
    if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = false ]; then
      # Check if the version is already installed by downloading the attestation file.
      say "checking if forge, cast, anvil, chisel, and solar for $FOUNDRYUP_TAG version are already installed"

      # Create a temporary directory to store the attestation link and artifact.
      tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir"
      tmp="$tmp_dir/attestation.txt"
      ensure download "$ATTESTATION_URL" "$tmp"

      # Read the first line of the attestation file to get the artifact link.
      # The first line should contain the link to the attestation artifact.
      attestation_artifact_link="$(head -n1 "$tmp" | tr -d '\r')"
      attestation_missing=false

      # If the attestation artifact link is empty or the file contains 'Not Found',
      # we consider the attestation missing and skip the SHA verification.
      if [ -z "$attestation_artifact_link" ] || grep -q 'Not Found' "$tmp"; then
        attestation_missing=true
      fi

      # Clean up the temporary attestation file.
      rm -f "$tmp"

      if $attestation_missing; then
        say "no attestation found for this release, skipping SHA verification"
      else
        say "found attestation for $FOUNDRYUP_TAG version, downloading attestation artifact, checking..."

        # Download the attestation artifact JSON file.
        tmp="$tmp_dir/foundry-attestation.sigstore.json"
        ensure download "${attestation_artifact_link}/download" "$tmp"

        # Extract the payload from the JSON file.
        payload_b64=$(awk '/"payload":/ {gsub(/[",]/, "", $2); print $2; exit}' "$tmp")
        payload_json=$(printf '%s' "$payload_b64" | base64 -d 2>/dev/null || printf '%s' "$payload_b64" | base64 -D)


        # Extract the names and hashes from the payload JSON.
        # The payload is expected to be a JSON array of objects with "name" and "sha256" fields.
        while read -r name_line && read -r sha_line; do
          name=$(echo "$name_line" | sed -nE 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
          sha=$(echo "$sha_line"  | sed -nE 's/.*"sha256"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p')
          if [ -n "$name" ] && [ -n "$sha" ]; then
            HASH_NAMES+=("$name")
            HASH_VALUES+=("$sha")
          fi
        done < <(echo "$payload_json" | tr '{}' '\n' | grep -E '"name"|sha256')

        # Clean up the temporary attestation artifact.
        # The hashes are now stored in the HASHES associative array.
        rm -f "$tmp"

        # Check if the binaries are already installed and match the expected hashes.
        # If they do, skip the download.
        version_dir="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG"
        all_match=true
        for bin in "${BINS[@]}"; do
          expected=""
          for i in "${!HASH_NAMES[@]}"; do
            if [ "${HASH_NAMES[$i]}" = "$bin" ] || [ "${HASH_NAMES[$i]}" = "$bin.exe" ]; then
              expected="${HASH_VALUES[$i]}"
              break
            fi
          done

          path="$version_dir/$bin"

          if [ -z "$expected" ]; then
            if is_missing_optional_bin "$bin" "$expected" "$path"; then
              continue
            fi
            all_match=false
            break
          fi

          if [ ! -x "$path" ]; then
            all_match=false
            break
          fi

          actual=$(compute_sha256 "$path")
          if [ "$actual" != "$expected" ]; then
            all_match=false
            break
          fi
        done

        if $all_match; then
          say "version $FOUNDRYUP_TAG already installed and verified, activating..."
          FOUNDRYUP_VERSION=$FOUNDRYUP_TAG
          use
          say "done!"
          exit 0
        fi
      fi

      # If we reach here, we need to download the binaries.
      say "binaries not found or do not match expected hashes, downloading new binaries"
    fi

    # Download and extract the binaries archive
    say "downloading forge, cast, anvil, chisel, and solar for $FOUNDRYUP_TAG version"
    archive_tmp_dir="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir"
    staged_dir="$archive_tmp_dir/extracted"
    ensure mkdir -p "$staged_dir"
    if [ "$PLATFORM" = "win32" ]; then
      tmp="$archive_tmp_dir/foundry.zip"
      ensure download "$BIN_ARCHIVE_URL" "$tmp"
      ensure unzip "$tmp" -d "$staged_dir"
    else
      tmp="$archive_tmp_dir/foundry.tar.gz"
      ensure download "$BIN_ARCHIVE_URL" "$tmp"
      # Make sure it's a valid tar archive.
      ensure tar tf "$tmp" 1> /dev/null
      ensure tar -C "$staged_dir" -xvf "$tmp"
    fi
    validate_staged_bins "$staged_dir"

    # Optionally download the manuals
    if check_cmd tar; then
      say "downloading manpages"
      mkdir -p "$FOUNDRY_MAN_DIR"
      if ! download "$MAN_TARBALL_URL" | tar -xzC "$FOUNDRY_MAN_DIR"; then
        warn "skipping manpage download: unavailable or invalid archive"
      fi
    else
      say 'skipping manpage download: missing "tar"'
    fi

    if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = true ]; then
      say "skipped SHA verification for downloaded binaries due to --force flag"
    else
      # Verify the downloaded binaries against the attestation file.
      # If the attestation file was not found or is empty, we skip the verification.
      if $attestation_missing; then
        say "no attestation found for these binaries, skipping SHA verification for downloaded binaries"
      else
        say "verifying downloaded binaries against the attestation file"

        failed=false
        for bin in "${BINS[@]}"; do
          expected=""
          for i in "${!HASH_NAMES[@]}"; do
            if [ "${HASH_NAMES[$i]}" = "$bin" ] || [ "${HASH_NAMES[$i]}" = "$bin.exe" ]; then
              expected="${HASH_VALUES[$i]}"
              break
            fi
          done

          path="$staged_dir/$bin"

          if [ -z "$expected" ]; then
            if is_missing_optional_bin "$bin" "$expected" "$path"; then
              continue
            fi
            say "no expected hash for $bin"
            failed=true
            continue
          fi

          if [ ! -x "$path" ]; then
            say "binary $bin not found at $path"
            failed=true
            continue
          fi

          actual=$(compute_sha256 "$path")
          if [ "$actual" != "$expected" ]; then
            say "$bin hash verification failed:"
            say "  expected: $expected"
            say "  actual:   $actual"
            failed=true
          else
            say "$bin verified ✓"
          fi
        done

        if $failed; then
          err "one or more binaries failed post-installation verification"
        fi
      fi
    fi

    replace_version_bins "$staged_dir" "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_TAG"
    cleanup_staged_bins "$archive_tmp_dir" "$staged_dir" "$tmp"

    # Use newly installed version.
    FOUNDRYUP_VERSION=$FOUNDRYUP_TAG
    use

    say "done!"
  # Install by cloning the repo with the provided branch/tag
  else
    need_cmd cargo
    FOUNDRYUP_BRANCH=${FOUNDRYUP_BRANCH:-master}
    REPO_PATH="$FOUNDRY_DIR/$FOUNDRYUP_REPO"
    AUTHOR="$(echo "$FOUNDRYUP_REPO" | cut -d'/' -f1 -)"

    # If repo path does not exist, grab the author from the repo, make a directory in .foundry, cd to it and clone.
    if [ ! -d "$REPO_PATH" ]; then
      ensure mkdir -p "$FOUNDRY_DIR/$AUTHOR"
      cd "$FOUNDRY_DIR/$AUTHOR"
      ensure git clone "https://github.com/$FOUNDRYUP_REPO"
    fi

    # Force checkout, discarding any local changes
    cd "$REPO_PATH"
    ensure git fetch origin "${FOUNDRYUP_BRANCH}:remotes/origin/${FOUNDRYUP_BRANCH}"
    ensure git checkout "origin/${FOUNDRYUP_BRANCH}"

    # Create custom version based on the install method, e.g.:
    # - foundry-rs-commit-c22c4cc96b0535cd989ee94b79da1b19d236b8db
    # - foundry-rs-pr-1
    # - foundry-rs-branch-chore-bump-forge-std
    if [ -n "$FOUNDRYUP_COMMIT" ]; then
      # If set, checkout specific commit from branch
      ensure git checkout "$FOUNDRYUP_COMMIT"
      FOUNDRYUP_VERSION=$AUTHOR-commit-$FOUNDRYUP_COMMIT
    elif [ -n "$FOUNDRYUP_PR" ]; then
     FOUNDRYUP_VERSION=$AUTHOR-pr-$FOUNDRYUP_PR
    else
      if [ -n "$FOUNDRYUP_BRANCH" ]; then
        NORMALIZED_BRANCH="$(echo "$FOUNDRYUP_BRANCH" | tr / -)"
        FOUNDRYUP_VERSION=$AUTHOR-branch-$NORMALIZED_BRANCH
      fi
    fi
    say "installing version $FOUNDRYUP_VERSION"

    # Build the repo.
    clear_built_bins "$PWD/target/release"
    ensure cargo build --bins "${CARGO_BUILD_ARGS[@]}"
    # Create foundry custom version directory.
    ensure mkdir -p "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION"
    for bin in "${BINS[@]}"; do
      rm -f \
        "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION/$bin" \
        "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION/$bin.exe"
      for try_path in target/release/$bin target/release/$bin.exe; do
        if [ -f "$try_path" ]; then
          mv -f "$try_path" "$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION"
        fi
      done
    done

    # Use newly built version.
    use

    # If help2man is installed, use it to add Foundry man pages.
    if check_cmd help2man; then
      for bin in "${BINS[@]}"; do
        help2man -N "$FOUNDRY_BIN_DIR/$bin" > "$FOUNDRY_MAN_DIR/$bin.1"
      done
    fi

    say "done"
  fi
}

usage() {
  cat 1>&2 <<EOF
The installer for Foundry.

Update or revert to a specific Foundry version with ease.

By default, the latest version is installed from built binaries.

USAGE:
    foundryup <OPTIONS>

OPTIONS:
    -h, --help      Print help information
    -v, --version   Print the version of foundryup
    -U, --update    Update foundryup to the latest version
    -i, --install   Install a specific version (latest, nightly, nightly-<SHA>, or v1.2.3)
    -l, --list      List versions installed from built binaries
    -u, --use       Use a specific installed version from built binaries
    -b, --branch    Build and install a specific branch
    -P, --pr        Build and install a specific Pull Request
    -C, --commit    Build and install a specific commit
    -r, --repo      Build and install from a remote GitHub repo (uses default branch if no other options are set)
    -p, --path      Build and install a local repository
    -j, --jobs      Number of CPUs to use for building Foundry (default: all CPUs)
    -n, --network   Deprecated and ignored; installs the regular Foundry release
    -f, --force     Skip SHA verification for downloaded binaries (INSECURE - use with caution)
    --arch          Install a specific architecture (supports amd64 and arm64)
    --platform      Install a specific platform (supports win32, linux, darwin and alpine)
EOF
}

version() {
  say "$FOUNDRYUP_INSTALLER_VERSION"
  exit 0
}

update() {
  say "updating foundryup..."

  current_version="$FOUNDRYUP_INSTALLER_VERSION"

  # Download the new version.
  tmp_file="$(mktemp)"
  ensure download "$FOUNDRY_BIN_URL" "$tmp_file"

  # Extract new version from downloaded file.
  new_version=$(grep -Eo 'FOUNDRYUP_INSTALLER_VERSION="[0-9]+\.[0-9]+\.[0-9]+"' "$tmp_file" | cut -d'"' -f2)

  # If the new version could not be determined, exit gracefully.
  # This prevents from upgrading to an empty or invalid version.
  if [ -z "$new_version" ]; then
    warn "could not determine new foundryup version. Exiting."
    rm -f "$tmp_file"
    exit 0
  fi

  # If the new version is not greater than the current version, skip the update.
  # This is to prevent downgrades or unnecessary updates.
  if ! version_gt "$new_version" "$current_version"; then
    say "foundryup is already up to date (installed: $current_version, remote: $new_version)."
    rm -f "$tmp_file"
    exit 0
  fi

  # Overwrite existing foundryup
  ensure mv "$tmp_file" "$FOUNDRY_BIN_PATH"
  ensure chmod +x "$FOUNDRY_BIN_PATH"

  say "successfully updated foundryup: $current_version → $new_version"
  exit 0
}

list() {
  if [ -d "$FOUNDRY_VERSIONS_DIR" ]; then
    for VERSION in "$FOUNDRY_VERSIONS_DIR"/*; do
      say "${VERSION##*/}"
      for bin in "${BINS[@]}"; do
        bin_path="$VERSION/$bin"
        if is_optional_bin "$bin" && [ ! -e "$bin_path" ]; then
          continue
        fi
        say "- $(ensure "$bin_path" -V)"
      done
      printf "\n"
    done
  else
    for bin in "${BINS[@]}"; do
      bin_path="$FOUNDRY_BIN_DIR/$bin"
      if is_optional_bin "$bin" && [ ! -e "$bin_path" ]; then
        continue
      fi
      say "- $(ensure "$bin_path" -V)"
    done
  fi
  exit 0
}

use() {
  [ -z "$FOUNDRYUP_VERSION" ] && err "no version provided"

  # If the requested version is a channel (`latest`, `stable`, `nightly`) or a bare semver
  # version (e.g. `1.7.0`, `1.6.0-rc1`), resolve it to the immutable tag directory created by
  # `--install` (channels hit the GitHub API; semver versions get a `v` prefix).
  # Falls back to the literal value for locally-built versions (branches, PRs, commits, custom names).
  case "$FOUNDRYUP_VERSION" in
    latest|stable|nightly|[0-9]*.[0-9]*.[0-9]*)
      resolve_version_and_tag
      FOUNDRYUP_VERSION="$FOUNDRYUP_TAG"
      ;;
  esac

  FOUNDRY_VERSION_DIR="$FOUNDRY_VERSIONS_DIR/$FOUNDRYUP_VERSION"
  if [ -d "$FOUNDRY_VERSION_DIR" ]; then

    check_bins_in_use
    ensure mkdir -p "$FOUNDRY_BIN_DIR"

    for bin in "${BINS[@]}"; do
      bin_path="$FOUNDRY_BIN_DIR/$bin"
      # Remove first: `$bin_path` may be a symlink created by a previous
      # `foundryup --path` invocation, pointing at e.g.
      # `<repo>/target/release/$bin`. A plain `cp src symlink` follows the
      # symlink and overwrites the local build artifact, which corrupts the
      # workspace and silently makes the next `--path` activation use the
      # downloaded release binary instead of a fresh local build.
      ensure rm -f "$bin_path"
      if is_optional_bin "$bin" && [ ! -e "$FOUNDRY_VERSION_DIR/$bin" ]; then
        continue
      fi
      ensure cp "$FOUNDRY_VERSION_DIR/$bin" "$bin_path"
      # Print usage msg
      say "use - $(ensure "$bin_path" -V)"

      # Check if the default path of the binary is not in FOUNDRY_BIN_DIR
      which_path="$(command -v "$bin" || true)"
      if [ -n "$which_path" ] && [ "$which_path" != "$bin_path" ]; then
        warn ""
        cat 1>&2 <<EOF
There are multiple binaries with the name '$bin' present in your 'PATH'.
This may be the result of installing '$bin' using another method,
like Cargo or other package managers.
You may need to run 'rm $which_path' or move '$FOUNDRY_BIN_DIR'
in your 'PATH' to allow the newly installed version to take precedence!

EOF
      fi
    done
    exit 0
  else
    err "version $FOUNDRYUP_VERSION not installed"
  fi
}

say() {
  printf "foundryup: %s\n" "$1"
}

warn() {
  say "warning: ${1}" >&2
}

err() {
  say "$1" >&2
  exit 1
}

tolower() {
  echo "$1" | awk '{print tolower($0)}'
}

compute_sha256() {
  if check_cmd sha256sum; then
    # Use sed to strip leading backslash (sha256sum on Windows/Git Bash outputs \ prefix for binary files)
    sha256sum "$1" | cut -d' ' -f1 | sed 's/^\\//'
  else
    shasum -a 256 "$1" | awk '{print $1}'
  fi
}

need_cmd() {
  if ! check_cmd "$1"; then
    err "need '$1' (command not found)"
  fi
}

check_cmd() {
  command -v "$1" &>/dev/null
}

is_optional_bin() {
  [ "$1" = "solar" ]
}

is_missing_optional_bin() {
  is_optional_bin "$1" && [ -z "$2" ] && [ ! -e "$3" ]
}

clear_version_bins() {
  ensure mkdir -p "$1"
  for bin in "${BINS[@]}"; do
    rm -f "$1/$bin" "$1/$bin.exe"
  done
}

clear_built_bins() {
  for bin in "${BINS[@]}"; do
    rm -f "$1/$bin" "$1/$bin.exe"
  done
}

validate_staged_bins() {
  for bin in "${BINS[@]}"; do
    path="$1/$bin"
    if is_optional_bin "$bin" && [ ! -e "$path" ]; then
      continue
    fi
    [ -x "$path" ] || err "binary $bin not found at $path"
  done
}

replace_version_bins() {
  staged_dir="$1"
  version_dir="$2"
  clear_version_bins "$version_dir"
  for bin in "${BINS[@]}"; do
    if [ -f "$staged_dir/$bin" ]; then
      ensure cp "$staged_dir/$bin" "$version_dir/$bin"
    elif ! is_optional_bin "$bin"; then
      err "binary $bin not found at $staged_dir/$bin"
    fi
  done
}

cleanup_staged_bins() {
  for bin in "${BINS[@]}"; do
    rm -f "$2/$bin" "$2/$bin.exe"
  done
  rm -f "$3"
  rmdir "$2" "$1" 2>/dev/null || true
}

detect_platform_arch() {
  uname_s=$(uname -s)
  PLATFORM=$(tolower "${FOUNDRYUP_PLATFORM:-$uname_s}")
  EXT="tar.gz"
  case $PLATFORM in
    linux|alpine) ;;
    darwin|mac*)
      PLATFORM="darwin"
      ;;
    mingw*|win*)
      EXT="zip"
      PLATFORM="win32"
      ;;
    *)
      err "unsupported platform: $PLATFORM"
      ;;
  esac

  uname_m=$(uname -m)
  ARCHITECTURE=$(tolower "${FOUNDRYUP_ARCH:-$uname_m}")
  if [ "${ARCHITECTURE}" = "x86_64" ]; then
    # Redirect stderr to /dev/null to avoid printing errors if non Rosetta.
    if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
      ARCHITECTURE="arm64" # Rosetta.
    else
      ARCHITECTURE="amd64" # Intel.
    fi
  elif [ "${ARCHITECTURE}" = "arm64" ] ||[ "${ARCHITECTURE}" = "aarch64" ] ; then
    ARCHITECTURE="arm64" # Arm.
  else
    ARCHITECTURE="amd64" # Amd.
  fi
}

check_installer_up_to_date() {
  say "checking if foundryup is up to date..."

  remote_version=$(fetch "$FOUNDRY_BIN_URL" | grep -Eo 'FOUNDRYUP_INSTALLER_VERSION="[0-9]+\.[0-9]+\.[0-9]+"' | cut -d'"' -f2 || true)

  if [ -z "$remote_version" ]; then
    warn "Could not determine remote foundryup version. Skipping version check."
    return 0
  fi

  if version_gt "$remote_version" "$FOUNDRYUP_INSTALLER_VERSION"; then
    printf '
Your installation of foundryup is out of date.

Installed: %s → Latest: %s

To update, run:

  foundryup --update

Updating is highly recommended as it gives you access to the latest features and bug fixes.

' "$FOUNDRYUP_INSTALLER_VERSION" "$remote_version" >&2
  else
    say "foundryup is up to date."
  fi
}

# Compares two version strings in the format "major.minor.patch".
# Returns 0 if $1 is greater than $2, 1 if $1 is less than $2, and 1 if they are equal.
#
# Assumes that the version strings are well-formed and contain three numeric components separated by dots.
#
# Example: version_gt "1.2.3" "1.2.4"
#          returns 1 (1.2.3 < 1.2.4)
#          version_gt "1.2.3" "1.2.3"
#          returns 1 (1.2.3 == 1.2.3)
#          version_gt "1.2.4" "1.2.3"
#          returns 0 (1.2.4 > 1.2.3)
version_gt() {
  [ "$1" = "$2" ] && return 1

  IFS=. read -r major1 minor1 patch1 <<EOF
$1
EOF
  IFS=. read -r major2 minor2 patch2 <<EOF
$2
EOF

  [ "$major1" -gt "$major2" ] && return 0
  [ "$major1" -lt "$major2" ] && return 1
  [ "$minor1" -gt "$minor2" ] && return 0
  [ "$minor1" -lt "$minor2" ] && return 1
  [ "$patch1" -gt "$patch2" ] && return 0
  [ "$patch1" -lt "$patch2" ] && return 1

  return 1
}

check_bins_in_use() {
  if check_cmd pgrep; then
    for bin in "${BINS[@]}"; do
      if pgrep -x -u "$(id -u)" "$bin" >/dev/null; then
        err "Error: '$bin' is currently running. Please stop the process and try again."
      fi
    done
  else
    warn "Make sure no foundry process is running during the install process!"
  fi
}

# Run a command that should never fail. If the command fails execution
# will immediately terminate with an error showing the failing command.
ensure() {
  if ! "$@"; then err "command failed: $*"; fi
}

# Normalizes `FOUNDRYUP_VERSION` and resolves it to a concrete release tag,
# populating `FOUNDRYUP_TAG`. Handles the `latest`/`stable`/`nightly` channels
# (looked up via the GitHub API).
resolve_version_and_tag() {
  FOUNDRYUP_REPO=${FOUNDRYUP_REPO:-foundry-rs/foundry}
  if [[ "$FOUNDRYUP_VERSION" == "latest" || "$FOUNDRYUP_VERSION" == "stable" ]]; then
    # Resolve to the latest release (non-prerelease) via the `releases/latest`
    # web redirect, falling back to the GitHub API if it cannot be resolved.
    say "fetching latest release tag from ${FOUNDRYUP_REPO}..."
    FOUNDRYUP_TAG=$(resolve_latest_tag_via_redirect "$FOUNDRYUP_REPO")
    if [ -z "$FOUNDRYUP_TAG" ]; then
      FOUNDRYUP_TAG=$(fetch "https://api.github.com/repos/${FOUNDRYUP_REPO}/releases/latest" | awk '
        /"tag_name"[[:space:]]*:/ && !found { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); print; found=1 }
      ') || err "failed to fetch release tags from GitHub API"
    fi
    if [ -z "$FOUNDRYUP_TAG" ]; then
      err "could not find a latest release tag for ${FOUNDRYUP_REPO}"
    fi
    say "resolved release tag: ${FOUNDRYUP_TAG}"
    FOUNDRYUP_VERSION="$FOUNDRYUP_TAG"
  elif [[ "$FOUNDRYUP_VERSION" == "nightly" ]]; then
    # Nightlies are published as immutable `nightly-<sha>` releases. Resolve the
    # most recent one from GitHub's public releases Atom feed, which works
    # unauthenticated and is not subject to the `api.github.com` REST rate limit.
    say "fetching latest nightly release tag from ${FOUNDRYUP_REPO}..."
    FOUNDRYUP_NIGHTLY_CANDIDATES=()
    while IFS= read -r nightly_candidate; do
      FOUNDRYUP_NIGHTLY_CANDIDATES+=("$nightly_candidate")
    done < <(resolve_nightly_tags_via_feed "$FOUNDRYUP_REPO")
    FOUNDRYUP_TAG="${FOUNDRYUP_NIGHTLY_CANDIDATES[0]:-}"
    if [ -z "$FOUNDRYUP_TAG" ]; then
      FOUNDRYUP_TAG=$(resolve_nightly_tag_via_api "$FOUNDRYUP_REPO") || err "failed to fetch release tags from GitHub API"
    fi
    if [ -z "$FOUNDRYUP_TAG" ]; then
      err "could not find a nightly release tag for ${FOUNDRYUP_REPO}"
    fi
    say "resolved nightly release tag: ${FOUNDRYUP_TAG}"
    FOUNDRYUP_VERSION="nightly"
  elif [[ "$FOUNDRYUP_VERSION" =~ ^nightly- ]]; then
    # Specific nightly tag (e.g. nightly-abc123...)
    FOUNDRYUP_TAG="$FOUNDRYUP_VERSION"
    FOUNDRYUP_VERSION="nightly"
  elif [[ "$FOUNDRYUP_VERSION" == [[:digit:]]* ]]; then
    # Add v prefix
    FOUNDRYUP_VERSION="v${FOUNDRYUP_VERSION}"
    FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}"
  else
    FOUNDRYUP_TAG="${FOUNDRYUP_VERSION}"
  fi
}

resolve_nightly_tag_via_api() {
  local _repo="$1"
  # The GitHub API does not guarantee that releases are returned in
  # chronological order, so collect matching nightlies with their published_at
  # timestamps and sort them explicitly.
  fetch "https://api.github.com/repos/${_repo}/releases" | awk '
    /"tag_name"[[:space:]]*:/ { gsub(/.*"tag_name"[[:space:]]*:[[:space:]]*"/, ""); gsub(/".*/, ""); tag=$0 }
    /"published_at"[[:space:]]*:[[:space:]]*"/ {
      pub=$0
      gsub(/.*"published_at"[[:space:]]*:[[:space:]]*"/, "", pub)
      gsub(/".*/, "", pub)
      if (tag ~ /^nightly-/) print pub "\t" tag
      tag=""
    }
  ' | sort -r | awk -F '\t' 'NR==1 { print $2 }'
}

# Resolves the latest release tag for repo $1 by following the `releases/latest`
# redirect and reading the tag from the final URL. Prints the tag (e.g.
# `v1.7.1`) to stdout, or nothing if it could not be resolved.
resolve_latest_tag_via_redirect() {
  local _repo="$1"
  local _url="https://github.com/${_repo}/releases/latest"
  local _final=""
  if check_cmd curl; then
    _final=$(curl -fsSIL \
      --retry "$FOUNDRYUP_MAX_RETRIES" \
      --retry-delay "$FOUNDRYUP_RETRY_DELAY" \
      --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \
      --retry-all-errors \
      -o /dev/null -w '%{url_effective}' "$_url" 2>/dev/null) || return 0
  else
    _final=$(wget -q -O /dev/null --max-redirect=10 --server-response "$_url" 2>&1 \
      | awk 'tolower($1) == "location:" { loc = $2 } END { print loc }' \
      | tr -d '\r') || return 0
  fi
  # Extract the tag from the `.../releases/tag/<tag>` URL and validate it;
  # anything unexpected returns nothing so the caller falls back to the API.
  local _tag="${_final##*/releases/tag/}"
  [ "$_tag" != "$_final" ] || return 0
  case "$_tag" in
    *[!A-Za-z0-9._-]*) return 0 ;;
    v[0-9]*) printf '%s\n' "$_tag" ;;
    *) return 0 ;;
  esac
}

# Resolves recent nightly tags for repo $1 from GitHub's public releases
# Atom feed (`releases.atom`), which works unauthenticated and is not subject to
# the api.github.com REST rate limit.
# Prints up to `FOUNDRYUP_NIGHTLY_CANDIDATE_LIMIT` unique `nightly-<sha>` tags,
# newest-first by entry `<updated>` timestamp, or nothing if the feed cannot be
# resolved.
resolve_nightly_tags_via_feed() {
  local _repo="$1"
  local _url="https://github.com/${_repo}/releases.atom"
  local _feed=""
  if check_cmd curl; then
    _feed=$(curl -fsSL \
      --retry "$FOUNDRYUP_MAX_RETRIES" \
      --retry-delay "$FOUNDRYUP_RETRY_DELAY" \
      --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \
      --retry-all-errors \
      "$_url" 2>/dev/null) || return 0
  else
    _feed=$(wget --tries="$FOUNDRYUP_MAX_RETRIES" \
      --waitretry="$FOUNDRYUP_RETRY_DELAY" \
      --retry-on-http-error=403,408,429,500,502,503,504 \
      -qO- "$_url" 2>/dev/null) || return 0
  fi
  # Return a small unique candidate set so callers can skip draft or
  # still-uploading immutable nightly releases. Restrict matching to each Atom
  # entry's release link; changelog compare links in the content can also contain
  # nightly tags and are not release candidates.
  printf '%s\n' "$_feed" \
    | awk '
      /<entry>/ { updated=""; tag="" }
      /<updated>/ {
        updated=$0
        gsub(/.*<updated>/, "", updated)
        gsub(/<\/updated>.*/, "", updated)
      }
      /href="[^"]*\/releases\/tag\/nightly-[0-9a-f]{40}"/ {
        tag=$0
        gsub(/.*\/releases\/tag\//, "", tag)
        gsub(/".*/, "", tag)
      }
      /<\/entry>/ && updated != "" && tag ~ /^nightly-[0-9a-f]{40}$/ {
        print updated "\t" tag
      }
    ' \
    | sort -r \
    | awk -F '\t' '!seen[$2]++ { print $2; count++ } count >= limit { exit }' limit="$FOUNDRYUP_NIGHTLY_CANDIDATE_LIMIT"
}

# Backwards-compatible helper for tests/callers that only need the newest
# candidate from the feed.
resolve_nightly_tag_via_feed() {
  resolve_nightly_tags_via_feed "$1" | head -n1
}

select_installable_nightly_tag() {
  local _tag _release_url _archive_url _tmp
  for _tag in "${FOUNDRYUP_NIGHTLY_CANDIDATES[@]}"; do
    _release_url="https://github.com/${FOUNDRYUP_REPO}/releases/download/${_tag}/"
    _archive_url="${_release_url}foundry_nightly_${PLATFORM}_${ARCHITECTURE}.${EXT}"
    _tmp="$(mktemp -d 2>/dev/null)" || err "failed to create temp dir"
    if [ "$PLATFORM" = "win32" ]; then
      _tmp="$_tmp/foundry.zip"
      if download "$_archive_url" "$_tmp" && unzip -t "$_tmp" >/dev/null 2>&1; then
        rm -f "$_tmp"
        printf '%s\n' "$_tag"
        return 0
      fi
    else
      _tmp="$_tmp/foundry.tar.gz"
      if download "$_archive_url" "$_tmp" && tar tf "$_tmp" >/dev/null 2>&1; then
        rm -f "$_tmp"
        printf '%s\n' "$_tag"
        return 0
      fi
    fi
    rm -f "$_tmp"
    warn "nightly ${_tag} is not installable yet, trying previous nightly"
  done
  err "could not find an installable nightly release for ${FOUNDRYUP_REPO}"
}

# Silently fetches $1 to stdout.
fetch() {
  if check_cmd curl; then
    curl -fsSL \
      --retry "$FOUNDRYUP_MAX_RETRIES" \
      --retry-delay "$FOUNDRYUP_RETRY_DELAY" \
      --retry-max-time "$FOUNDRYUP_RETRY_MAX_TIME" \
      --retry-all-errors \
      "$1"
  else
    wget --tries="$FOUNDRYUP_MAX_RETRIES" \
         --waitretry="$FOUNDRYUP_RETRY_DELAY" \
         --retry-on-http-error=403,408,429,500,502,503,504 \
         -qO- "$1"
  fi
}

# Downloads $1 into $2 or stdout
download() {
  if [ -n "$2" ]; then
    # output into $2
    if check_cmd curl; then
      curl -#o "$2" -L "$1"
    else
      wget --show-progress -qO "$2" "$1"
    fi
  else
    # output to stdout
    if check_cmd curl; then
      curl -#L "$1"
    else
      wget --show-progress -qO- "$1"
    fi
  fi
}

# Banner prompt for Foundry
banner() {
  printf '

.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx

 ╔═╗ ╔═╗ ╦ ╦ ╔╗╔ ╔╦╗ ╦═╗ ╦ ╦         Portable and modular toolkit
 ╠╣  ║ ║ ║ ║ ║║║  ║║ ╠╦╝ ╚╦╝    for Ethereum Application Development
 ╚   ╚═╝ ╚═╝ ╝╚╝ ═╩╝ ╩╚═  ╩                 written in Rust.

.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx

Repo       : https://github.com/foundry-rs/foundry
Book       : https://book.getfoundry.sh/
Chat       : https://t.me/foundry_rs/
Support    : https://t.me/foundry_support/
Contribute : https://github.com/foundry-rs/foundry/blob/HEAD/CONTRIBUTING.md

.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx.xOx

'
}


# Absolute path to the sidecar Rust foundryup binary (with `.exe` on Windows).
rust_foundryup_bin() {
  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*) printf '%s\n' "$FOUNDRYUP_RUST_HOME/bin/foundryup.exe" ;;
    *)                    printf '%s\n' "$FOUNDRYUP_RUST_HOME/bin/foundryup" ;;
  esac
}

# Prints the version of the sidecar Rust foundryup at $1 (e.g. `0.0.5`), or
# nothing if it is not installed or the version cannot be determined. The
# trailing `|| true` keeps a failing/unparsable `--version` from aborting under
# `set -eo pipefail`, so the caller falls through to a reinstall instead.
sidecar_version() {
  [ -x "$1" ] || return 0
  "$1" --version 2>/dev/null \
    | sed -nE 's/^foundryup[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+).*/\1/p' \
    | head -1 || true
}

# Downloads `foundryup-init.sh` and installs the Rust foundryup into the
# sidecar home, leaving the executable at $1.
install_rust_foundryup() {
  local _bin="$1"

  local _init
  _init="$(mktemp)"
  ensure download "$FOUNDRYUP_INIT_URL" "$_init"

  # Run the installer with:
  #  - FOUNDRY_DIR pointed at the sidecar home, so it installs to
  #    $FOUNDRYUP_RUST_HOME/bin/foundryup instead of overwriting this shim.
  #  - FOUNDRYUP_VERSION scoped to the installer invocation only (it means the
  #    installer version here, but the Foundry tool version to the Rust binary),
  #    so it never leaks into the user's environment.
  ensure env FOUNDRY_DIR="$FOUNDRYUP_RUST_HOME" FOUNDRYUP_VERSION="$FOUNDRYUP_BOOTSTRAP_VERSION" \
    sh "$_init" -y
  rm -f "$_init"

  # The v0.0.5 installer writes the Windows binary without a `.exe` extension;
  # normalize it so the exec below resolves correctly.
  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
      if [ ! -x "$_bin" ] && [ -f "$FOUNDRYUP_RUST_HOME/bin/foundryup" ]; then
        mv -f "$FOUNDRYUP_RUST_HOME/bin/foundryup" "$_bin"
      fi
      ;;
  esac

  [ -x "$_bin" ] || err "failed to bootstrap the Rust foundryup binary"
}

# Ensures a Rust foundryup of at least $FOUNDRYUP_BOOTSTRAP_VERSION is present
# in the sidecar home, (re)installing it otherwise. Reusing the sidecar only
# when it is recent enough means a later $FOUNDRYUP_BOOTSTRAP_VERSION bump
# actually migrates users who already bootstrapped an older binary, while a
# self-updated newer binary is never downgraded.
ensure_rust_foundryup() {
  local _bin _have
  _bin="$(rust_foundryup_bin)"
  _have="$(sidecar_version "$_bin")"
  if [ -n "$_have" ] && ! version_gt "$FOUNDRYUP_BOOTSTRAP_VERSION" "$_have"; then
    return 0
  fi

  say "migrating to the Rust foundryup (v$FOUNDRYUP_BOOTSTRAP_VERSION)..."
  install_rust_foundryup "$_bin"
}

# Skip execution when sourced by tests.
if [ "${FOUNDRYUP_TEST:-0}" != "1" ]; then
  # Opt-in migration to the Rust foundryup. When enabled, the user's
  # environment (including FOUNDRY_DIR and any FOUNDRYUP_VERSION) is preserved
  # for the exec, so the Rust binary installs Foundry into the usual location.
  if [ "${FOUNDRYUP_MIGRATE:-0}" = "1" ]; then
    ensure_rust_foundryup
    exec "$(rust_foundryup_bin)" "$@"
  else
    main "$@"
  fi
fi
