#!/bin/sh
# Install semlith from a GitHub release: download this platform's binary, verify
# it against SHA256SUMS and its build provenance, then hand off to `semlith setup`.
# SEMLITH_VERSION=v0.10.0  pin a release tag (default: the latest release)
# SEMLITH_HOME=<dir>       install into <dir>/bin (default: ~/.semlith/bin)
# SEMLITH_YES=1            answer yes to every `semlith setup` prompt
# SEMLITH_NO_SERVICE=1     do not install the daemon as a login service
# Everything is a function and the last line calls `main`: a pipe cut off
# part-way through this file defines things and runs nothing.
set -eu
repo=semlith/semlith
# One origin, with no way to be told another: a script that read one from the
# environment would install whatever a hostile shell profile pointed it at.
# `tests/install.rs` rewrites this line in a copy rather than setting a variable.
origin=https://github.com
# curl may speak only the origin's scheme, which is https in every shipped copy.
# Derived so that a copy pointed at an http fixture differs in that line alone.
proto="=${origin%%:*}"
say() { printf '%s\n' "$*"; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }

detect_target() {
  arch=$(uname -m)
  case "$arch" in
    x86_64 | amd64) arch=x86_64 ;;
    aarch64 | arm64) arch=aarch64 ;;
    *) die "unsupported architecture: $arch" ;;
  esac
  os=$(uname -s)
  case "$os" in
    Linux) target="$arch-unknown-linux-gnu" ;;
    Darwin)
      if [ "$arch" != aarch64 ]; then
        printf '%s\n' \
          "semlith has no prebuilt binary for Intel (x86_64) macOS." \
          "ONNX Runtime stopped publishing osx-x86_64 builds, so semlith's" \
          "embedding runtime cannot be linked for that target and the release" \
          "workflow does not produce one." \
          "" \
          "Build it from source instead:" \
          "    cargo install semlith" >&2
        exit 1
      fi
      target=aarch64-apple-darwin
      ;;
    *) die "unsupported operating system: $os" ;;
  esac
}

# A progress bar drawn from a size we already know.
#
# curl's own `--progress-bar` is not usable here. It draws a bouncing `#=O=-`
# indicator for as long as it does not know the transfer size, and over HTTPS
# that is the whole connect, TLS and header phase: measured against the release
# asset, 72 of 100 frames were that indicator and only the last 28 were a bar,
# with no redirects involved at all. It reads as line noise rather than as
# progress, which is worse than showing nothing.
bar() {
  [ "$2" -gt 0 ] 2>/dev/null || return 0
  pct=$(( $1 * 100 / $2 ))
  [ "$pct" -gt 100 ] && pct=100
  fill=$(( pct * 36 / 100 ))
  printf '\r  %-36s %3d%%' "$(printf "%${fill}s" '' | tr ' ' '#')" "$pct"
}

pick_tools() {
  if command -v curl >/dev/null 2>&1; then
    fetch() {
      # The last Content-Length in the header chain, so a redirect is followed
      # first and the size is the one the body will actually have.
      size=$(curl --proto "$proto" --tlsv1.2 -fsSLI "$1" 2>/dev/null | tr -d '\r' \
        | awk 'tolower($1)=="content-length:"{n=$2} END{print n+0}')
      curl --proto "$proto" --tlsv1.2 -fsSL -o "$2" "$1" &
      dl=$!
      while kill -0 "$dl" 2>/dev/null; do
        # Guarded because the file does not exist until curl creates it, and the
        # shell reports a failed redirection itself — `wc`'s own stderr is not
        # what would leak here.
        have=0
        [ -f "$2" ] && have=$(wc -c < "$2" 2>/dev/null || echo 0)
        bar "$have" "${size:-0}"
        sleep 0.2
      done
      # The download's exit status is the function's: a failed fetch has to fail
      # the install, and a bar that reached 100% must not hide a truncated body.
      if wait "$dl"; then
        bar "${size:-0}" "${size:-0}"
        [ "${size:-0}" -gt 0 ] && printf '\n'
        return 0
      fi
      printf '\n'
      return 1
    }
    final_url() { curl --proto "$proto" --tlsv1.2 -fsSLI -o /dev/null -w '%{url_effective}' "$1"; }
  elif command -v wget >/dev/null 2>&1; then
    fetch() { wget -q --show-progress -O "$2" "$1"; }
    final_url() {
      wget -q -S --spider --max-redirect=10 "$1" 2>&1 |
        sed -n 's/^[[:space:]]*Location:[[:space:]]*//p' | tail -n 1
    }
  else
    die "need curl or wget to download semlith"
  fi
  if command -v sha256sum >/dev/null 2>&1; then
    check_sums() { sha256sum -c "$1"; }
  elif command -v shasum >/dev/null 2>&1; then
    check_sums() { shasum -a 256 -c "$1"; }
  else
    die "need sha256sum or shasum to verify the download"
  fi
}

# The checksum proves the archive matches SHA256SUMS, and both came from the
# same place. The attestation proves who built it: signed for the archive's
# digest by this repository's release workflow. gh is optional, so its absence
# is said rather than fatal; a gh that checks and does not confirm is fatal.
verify_provenance() {
  manual="To check it yourself: gh attestation verify $archive --repo $repo"
  # Attestations start with v0.36.0: an older tag has none, and refusing it
  # would refuse every pinned install of a release that was never attested.
  case "$tag" in
    v0.[0-9].* | v0.[12][0-9].* | v0.3[0-5].*)
      say "Provenance not verified: $tag predates release attestations (v0.36.0)"; return 0 ;;
  esac
  if ! command -v gh >/dev/null 2>&1; then
    say "Provenance not verified: gh is not installed. $manual"; return 0
  fi
  say "Verifying provenance with gh attestation verify"
  status=0
  gh attestation verify "$tmp/$archive" --repo "$repo" || status=$?
  case "$status" in
    0) say "Provenance verified: $archive was built by $repo's release workflow" ;;
    # gh's documented "needs authentication" code: the check never ran.
    4) say "Provenance not verified: gh is not logged in (gh auth login). $manual" ;;
    *) die "provenance check failed: gh could not confirm $repo built $archive (gh 2.49+ needed); nothing was installed" ;;
  esac
}

run_setup() {
  # The login service is installed unless this says not to. A user who never
  # reads a prompt is exactly the user a login service is for, and a piped
  # install has no prompt to read — so the opt-out is an environment variable
  # rather than nothing at all.
  no_service=
  if [ "${SEMLITH_NO_SERVICE:-}" = 1 ]; then
    no_service=--no-service
  fi
  if [ "${SEMLITH_YES:-}" = 1 ]; then
    "$bin_dir/semlith" setup --yes $no_service
  elif [ -t 0 ]; then
    "$bin_dir/semlith" setup $no_service
  elif (: </dev/tty) 2>/dev/null; then
    # `curl | sh` leaves stdin on the pipe, so read the terminal instead of
    # silently taking defaults. Opening it is the test: in a container
    # /dev/tty exists and passes -r but cannot be opened.
    "$bin_dir/semlith" setup $no_service </dev/tty
  else
    "$bin_dir/semlith" setup --yes $no_service
  fi
}

main() {
  detect_target
  pick_tools
  if [ -n "${SEMLITH_VERSION:-}" ]; then
    tag=$SEMLITH_VERSION
  else
    say "Resolving the latest semlith release..."
    tag=$(final_url "$origin/$repo/releases/latest")
    tag=${tag##*/}
  fi
  case "$tag" in
    v*) ;;
    *) die "could not resolve a release tag (got '$tag')" ;;
  esac

  name="semlith-$tag-$target"
  archive="$name.tar.gz"
  base="$origin/$repo/releases/download/$tag"
  say "Installing semlith $tag for $target"

  tmp=$(mktemp -d)
  trap 'rm -rf "$tmp"' EXIT INT TERM

  say "Downloading $archive"
  fetch "$base/$archive" "$tmp/$archive"
  say "Downloading SHA256SUMS"
  fetch "$base/SHA256SUMS" "$tmp/SHA256SUMS"

  say "Verifying checksum"
  grep "  $archive\$" "$tmp/SHA256SUMS" >"$tmp/expected" ||
    die "SHA256SUMS has no entry for $archive"
  (cd "$tmp" && check_sums expected) ||
    die "checksum mismatch: $archive does not match its SHA256SUMS entry; nothing was installed"
  verify_provenance

  say "Unpacking"
  tar xzf "$tmp/$archive" -C "$tmp"

  bin_dir="${SEMLITH_HOME:-$HOME/.semlith}/bin"
  mkdir -p "$bin_dir"
  chmod 700 "$bin_dir" 2>/dev/null || :
  mv "$tmp/$name/semlith" "$bin_dir/semlith"
  chmod +x "$bin_dir/semlith"
  # The Linux archives carry ONNX Runtime beside the binary, which is where it is
  # loaded from at start (issue #57). macOS links its runtime in.
  if [ -f "$tmp/$name/libonnxruntime.so" ]; then
    mv "$tmp/$name/libonnxruntime.so" "$bin_dir/libonnxruntime.so"
    say "Installed $bin_dir/libonnxruntime.so"
  fi
  say "Installed $bin_dir/semlith"

  if "$bin_dir/semlith" setup --help >/dev/null 2>&1; then
    run_setup
  else
    say ""
    say "This release has no 'setup' command. Add semlith to your PATH by putting"
    say "this line in your shell rc file (~/.zshrc, ~/.bashrc or ~/.profile):"
    say "    export PATH=\"$bin_dir:\$PATH\""
  fi
}

main "$@"
