#!/bin/sh
# BoxLite CLI installer.
#
# Usage:
#   curl -fsSL https://github.com/boxlite-ai/boxlite/releases/latest/download/install.sh | sh
#
# Environment variables:
#   BOXLITE_VERSION           Pin a specific tag (default: this script's CURRENT_VERSION)
#   BOXLITE_INSTALL_DIR       Install dir (default: $HOME/.local/bin)
#   BOXLITE_EXPECTED_SHA256   Out-of-band expected digest for the CLI tarball.
#                             Overrides every other source of expected checksum
#                             below. Use this when pinning a version: it is the
#                             only mode whose trust root is independent of the
#                             release page (caller-supplied, e.g. read from
#                             SHA256SUMS verified with `gh attestation verify`).
#   BOXLITE_DEBUG=1           Print every command
#   BOXLITE_QUIET=1           Silence info output
#
# Trust model for the expected-checksum lookup (highest authority first):
#   1. BOXLITE_EXPECTED_SHA256, if set. Caller has obtained this digest out of
#      band (recommended for pinned installs).
#   2. The CHECKSUM_* constants embedded in this script at release time, when
#      the requested version equals CURRENT_VERSION. These constants share a
#      sigstore attestation with install.sh itself, so verifying install.sh
#      before piping it transitively pins the CLI tarball.
#   3. The `<artifact>.sha256` sidecar fetched from the target release. This
#      is a TLS-trust-only anchor — anyone who can replace the tarball in a
#      release can replace this sidecar too. Used as a fallback when the
#      requested version does not match CURRENT_VERSION and no out-of-band
#      digest was supplied. A warning is emitted in this mode so the user
#      knows they are in the weaker trust tier.
#
# The runtime (boxlite-shim, boxlite-guest, libkrunfw) is embedded in the
# binary via include_bytes!; nothing else needs to be installed.

set -eu

REPO="boxlite-ai/boxlite"
CURRENT_VERSION="v0.9.4"

# Checksums for CURRENT_VERSION, embedded at release time. Used as fast-path
# when the requested version matches; otherwise the script falls back to
# fetching `<artifact>.sha256` from the release.
CHECKSUM_AARCH64_APPLE_DARWIN="1a6078a5f8c57b4fa5b06806d74646943d309a7abc07deea833bb1eb2d489830"
CHECKSUM_X86_64_UNKNOWN_LINUX_GNU="715a2974872e192fca046494dff1bae5eb58d5b65383e5574c58b8ca4869a15a"
CHECKSUM_AARCH64_UNKNOWN_LINUX_GNU="75c5367a618f996d9dbae86985347d3edcc63b3ad5bc207956174f4b4aeced4b"

#-----------------------------------------------------------------------------
# logging
#-----------------------------------------------------------------------------
if [ "${BOXLITE_QUIET-}" = "1" ]; then
  info() { :; }
else
  info() { printf '%s\n' "$*" >&2; }
fi

if [ "${BOXLITE_DEBUG-}" = "1" ]; then
  set -x
fi

error() { printf 'error: %s\n' "$*" >&2; exit 1; }

#-----------------------------------------------------------------------------
# platform detection
#-----------------------------------------------------------------------------
detect_target() {
  os=$(uname -s)
  arch=$(uname -m)
  case "${os}-${arch}" in
    Darwin-arm64)
      echo "aarch64-apple-darwin" ;;
    Darwin-x86_64)
      error "macOS Intel is not supported. BoxLite requires Apple Silicon. See https://github.com/${REPO}#supported-platforms" ;;
    Linux-x86_64)
      echo "x86_64-unknown-linux-gnu" ;;
    Linux-aarch64|Linux-arm64)
      echo "aarch64-unknown-linux-gnu" ;;
    *)
      error "unsupported platform: ${os}-${arch}. Build from source: https://github.com/${REPO}" ;;
  esac
}

embedded_checksum_for() {
  case "$1" in
    aarch64-apple-darwin)        echo "$CHECKSUM_AARCH64_APPLE_DARWIN" ;;
    x86_64-unknown-linux-gnu)    echo "$CHECKSUM_X86_64_UNKNOWN_LINUX_GNU" ;;
    aarch64-unknown-linux-gnu)   echo "$CHECKSUM_AARCH64_UNKNOWN_LINUX_GNU" ;;
    *)                            echo "" ;;
  esac
}

#-----------------------------------------------------------------------------
# tool selection
#-----------------------------------------------------------------------------
require() {
  command -v "$1" >/dev/null 2>&1 || error "missing required tool: $1"
}

# Picks `sha256sum` (Linux) or `shasum -a 256` (macOS).
sha256_check() {
  # $1 = file, $2 = expected hex digest
  expected="$2"
  if command -v sha256sum >/dev/null 2>&1; then
    actual=$(sha256sum "$1" | awk '{print $1}')
  elif command -v shasum >/dev/null 2>&1; then
    actual=$(shasum -a 256 "$1" | awk '{print $1}')
  else
    error "neither sha256sum nor shasum is installed"
  fi
  [ "$actual" = "$expected" ] || error "checksum mismatch: expected $expected, got $actual"
}

#-----------------------------------------------------------------------------
# download + install
#-----------------------------------------------------------------------------
download() {
  # $1 = url, $2 = output path
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$2" "$1"
  else
    error "neither curl nor wget is installed"
  fi
}

main() {
  require uname
  require tar
  require mktemp

  target=$(detect_target)
  version=${BOXLITE_VERSION:-$CURRENT_VERSION}
  install_dir=${BOXLITE_INSTALL_DIR:-$HOME/.local/bin}

  archive="boxlite-cli-${version}-${target}.tar.gz"
  base_url="https://github.com/${REPO}/releases/download/${version}"

  tmpdir=$(mktemp -d)
  # Staged binary lives inside install_dir so the final rename is on the same
  # filesystem (rename(2) is atomic). $$ avoids collisions between concurrent
  # installs; the final `mv -f` is still last-writer-wins, which matches the
  # prior behavior.
  staged=""
  cleanup() {
    rm -rf "$tmpdir"
    [ -n "$staged" ] && rm -f "$staged"
    return 0
  }
  trap cleanup EXIT INT TERM

  info "Downloading $archive ..."
  download "${base_url}/${archive}" "${tmpdir}/${archive}"

  # Resolve the expected digest in the order documented at the top of this
  # script: caller-supplied → embedded (current version) → remote sidecar.
  if [ -n "${BOXLITE_EXPECTED_SHA256-}" ]; then
    expected="$BOXLITE_EXPECTED_SHA256"
    info "Using BOXLITE_EXPECTED_SHA256 as the expected digest."
  elif [ "$version" = "$CURRENT_VERSION" ]; then
    expected=$(embedded_checksum_for "$target")
    [ -n "$expected" ] || error "no embedded checksum for target $target"
  else
    # Pinned to a different version, no out-of-band digest. Surface the
    # weaker trust boundary so callers know they should verify SHA256SUMS
    # via `gh attestation verify` and pass BOXLITE_EXPECTED_SHA256 if they
    # require an integrity guarantee independent of the release page.
    info "warning: BOXLITE_VERSION=${version} differs from this installer's"
    info "         CURRENT_VERSION=${CURRENT_VERSION}, and BOXLITE_EXPECTED_SHA256"
    info "         is unset. Falling back to the remote .sha256 sidecar, which"
    info "         shares its trust root with the tarball. For a stronger"
    info "         guarantee, verify SHA256SUMS (covered by build provenance"
    info "         attestation) and re-run with BOXLITE_EXPECTED_SHA256=<digest>."
    info "Fetching checksum for $version ..."
    download "${base_url}/${archive}.sha256" "${tmpdir}/${archive}.sha256"
    expected=$(awk '{print $1}' < "${tmpdir}/${archive}.sha256")
  fi

  info "Verifying checksum ..."
  sha256_check "${tmpdir}/${archive}" "$expected"

  info "Extracting to ${install_dir} ..."
  mkdir -p "$install_dir"
  # --no-same-owner: don't restore the CI runner's UID into the extracted
  # binary (matters when the user runs this as root for /usr/local/bin).
  tar --no-same-owner -xzf "${tmpdir}/${archive}" -C "$tmpdir" boxlite
  # Atomic replace: stage inside install_dir (same filesystem so rename(2)
  # is atomic) then mv -f over the target. GNU coreutils install(1) on
  # Linux opens an existing destination with O_TRUNC and writes in place
  # (see coreutils src/copy.c), so a failed mid-copy would corrupt the
  # existing binary. macOS BSD install already uses a temp file internally,
  # but we apply the same pattern on both for portability.
  staged="${install_dir}/.boxlite.tmp.$$"
  rm -f "$staged"
  # install(1) sets owner to the calling user and mode 0755 explicitly, so a
  # sudoed install ends up owned by root rather than whatever was in the
  # tarball. Present on macOS BSD and GNU coreutils.
  install -m 0755 "${tmpdir}/boxlite" "$staged"
  mv -f "$staged" "${install_dir}/boxlite"
  staged=""  # final file is in place; cleanup must not touch it

  info ""
  info "✓ Installed boxlite ${version} to ${install_dir}/boxlite"
  case ":${PATH-}:" in
    *":${install_dir}:"*) ;;
    *)
      info ""
      info "  ${install_dir} is not in your PATH. Add this to your shell rc:"
      info "    export PATH=\"${install_dir}:\$PATH\"" ;;
  esac
}

main "$@"
