#!/bin/sh
# Installs the mysetup command-line client for the current user.
#
#   curl -fsSL https://mysetup.ai/install.sh | sh
#
# What this script does, and nothing more:
#   1. Picks the build for this computer (macOS, or Linux on x86-64 or ARM64).
#   2. Reads the current version from https://mysetup.ai/cli/version.
#   3. Downloads that version's SHA256SUMS.txt, its minisign signature and the archive,
#      from mysetup.ai only, over HTTPS. Any response other than 200, including a
#      redirect, stops the install.
#   4. Verifies the signature with the public key below when `minisign` is installed
#      (otherwise prints the command to check it yourself), requires the archive's
#      checksum to match both SHA256SUMS.txt and the published release
#      (/cli/latest.sha256), and refuses to continue on any mismatch.
#   5. Copies the single `mysetup` program into ~/.local/bin (or MYSETUP_INSTALL_DIR),
#      a folder you own.
# It never uses sudo, never runs anything it downloads, ignores ~/.curlrc, and never
# changes your shell profile, services or sign-in. On Windows, use Scoop or the zip from
# mysetup.ai/download.
#
# The whole script is one function called on the last line, so a partial download
# cannot run half of it.

set -eu

MYSETUP_ORIGIN="https://mysetup.ai"
MYSETUP_ALLOW_UNSIGNED=0
# The minisign public key for release checksums (key id 2D2C943E74E7ED5B; the same value as
# releaseSigningPublicKey in shared/downloads.ts). Without a valid key this script refuses to
# install from production.
MYSETUP_MINISIGN_PUBLIC_KEY="RWRb7ed0PpQsLY2ls30JciQE8bmyfLp23edRJzDkZEfq8nlMwARPQnxV"

workdir=""
staged=""

say() {
  printf '%s\n' "$*"
}

warn() {
  printf 'mysetup install: %s\n' "$*" >&2
}

fail() {
  warn "$*"
  exit 1
}

cleanup() {
  if [ -n "$staged" ]; then rm -f "$staged"; fi
  if [ -n "$workdir" ]; then rm -rf "$workdir"; fi
}

need() {
  command -v "$1" >/dev/null 2>&1 || fail "this installer needs '$1', which was not found."
}

# -q (first) ignores ~/.curlrc. HTTPS only, TLS 1.2 or newer; redirects are not followed,
# and anything but a 200 response is an error.
fetch() {
  status=$(curl -q --proto '=https' --tlsv1.2 --fail --silent --show-error \
    --retry 2 --connect-timeout 20 --write-out '%{http_code}' \
    --output "$2" "$MYSETUP_ORIGIN$1") || return 1
  if [ "$status" != "200" ]; then
    warn "unexpected response $status from $MYSETUP_ORIGIN$1"
    return 1
  fi
}

sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | cut -d ' ' -f 1
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | cut -d ' ' -f 1
  else
    fail "this installer needs 'sha256sum' or 'shasum' to verify the download."
  fi
}

# The checksum a SHA256SUMS-format file lists for one file name, or nothing.
listed_sha256() {
  awk -v name="$2" '$2 == name || $2 == "*" name { print $1 }' "$1"
}

detect_target() {
  os=$(uname -s)
  arch=$(uname -m)
  case "$os" in
    Linux)
      case "$arch" in
        x86_64 | amd64) echo "linux-x86_64" ;;
        aarch64 | arm64) echo "linux-arm64" ;;
        *) fail "there is no mysetup build for Linux on $arch yet." ;;
      esac
      ;;
    Darwin) echo "macos-universal" ;;
    MINGW* | MSYS* | CYGWIN* | Windows_NT)
      fail "on Windows, install with Scoop or download the zip from $MYSETUP_ORIGIN/download instead."
      ;;
    *) fail "there is no mysetup build for $os yet." ;;
  esac
}

signing_key_configured() {
  printf '%s\n' "$MYSETUP_MINISIGN_PUBLIC_KEY" | grep -Eq '^RW[A-Za-z0-9+/]{54}$'
}

verify_signature() {
  sums="$1"
  signature="$2"
  if command -v minisign >/dev/null 2>&1; then
    minisign -V -q -m "$sums" -x "$signature" -P "$MYSETUP_MINISIGN_PUBLIC_KEY" >/dev/null 2>&1 ||
      fail "the release checksums are not signed by mysetup, so nothing was installed."
    say "Verified the release signature"
  else
    say "minisign is not installed, so the release signature was not checked. To check it:"
    say "  minisign -Vm SHA256SUMS.txt -P $MYSETUP_MINISIGN_PUBLIC_KEY"
  fi
}

main() {
  need curl
  need tar
  need uname
  need mktemp
  need awk
  need id

  target=$(detect_target)

  if [ -n "${MYSETUP_INSTALL_DIR:-}" ]; then
    install_dir=$MYSETUP_INSTALL_DIR
  else
    [ -n "${HOME:-}" ] || fail "HOME is not set; set MYSETUP_INSTALL_DIR to choose a folder."
    install_dir="$HOME/.local/bin"
  fi
  case "$install_dir" in
    /*) ;;
    *) fail "MYSETUP_INSTALL_DIR must be an absolute path." ;;
  esac

  # Staging serves this script with MYSETUP_ALLOW_UNSIGNED=1: its candidate releases are not
  # signed with the release key, so it never asks for a signature (production always does).
  if [ "$MYSETUP_ALLOW_UNSIGNED" = "1" ]; then
    signed=0
    warn "WARNING: this is a staging installer; it does not check a release signature."
    warn "WARNING: the download is checked against published checksums but not a signature."
  elif signing_key_configured; then
    signed=1
  else
    fail "this installer has no release signing key yet, so it cannot verify mysetup. Nothing was installed."
  fi

  workdir=$(mktemp -d 2>/dev/null || mktemp -d -t mysetup-install)
  trap cleanup EXIT
  trap 'exit 1' INT TERM

  fetch /cli/version "$workdir/version" ||
    fail "could not reach $MYSETUP_ORIGIN. Check your connection and try again."
  version=$(head -n 1 "$workdir/version" | tr -d '\r')
  printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})\.(0|[1-9][0-9]{0,3})$' ||
    fail "the published version was not recognised."

  archive="mysetup-$version-$target.tar.gz"
  say "Downloading mysetup $version for $target"
  fetch /cli/latest.sha256 "$workdir/latest.sha256" ||
    fail "could not download the published release checksums."
  published=$(listed_sha256 "$workdir/latest.sha256" "$archive")
  printf '%s\n' "$published" | grep -Eq '^[0-9a-f]{64}$' ||
    fail "mysetup $version is not available for $target yet."

  fetch "/cli/releases/$version/SHA256SUMS.txt" "$workdir/SHA256SUMS.txt" ||
    fail "could not download the checksums for mysetup $version."
  if [ "$signed" = "1" ]; then
    fetch "/cli/releases/$version/SHA256SUMS.txt.minisig" "$workdir/SHA256SUMS.txt.minisig" ||
      fail "could not download the signature for mysetup $version."
    verify_signature "$workdir/SHA256SUMS.txt" "$workdir/SHA256SUMS.txt.minisig"
  fi
  expected=$(listed_sha256 "$workdir/SHA256SUMS.txt" "$archive")
  [ "$expected" = "$published" ] ||
    fail "the release checksums disagree for $archive, so nothing was installed."

  fetch "/cli/releases/$version/$archive" "$workdir/$archive" ||
    fail "could not download $archive."
  actual=$(sha256_of "$workdir/$archive")
  [ "$actual" = "$expected" ] ||
    fail "the download did not match its published SHA-256 checksum, so nothing was installed."
  say "Verified SHA-256 $actual"

  # Extract only the one expected program, then check it is a plain file.
  member="mysetup-$version-$target/mysetup"
  mkdir "$workdir/extract"
  tar -xzf "$workdir/$archive" -C "$workdir/extract" "$member" 2>/dev/null ||
    fail "the archive did not contain the mysetup program."
  binary="$workdir/extract/$member"
  if [ ! -f "$binary" ] || [ -L "$binary" ]; then
    fail "the archive did not contain the mysetup program."
  fi

  mkdir -p "$install_dir" 2>/dev/null || fail "could not create $install_dir."
  if [ -z "$(find "$install_dir" -maxdepth 0 -type d -user "$(id -u)" 2>/dev/null)" ]; then
    fail "$install_dir is not a folder you own. Set MYSETUP_INSTALL_DIR to one; this installer never uses sudo."
  fi
  [ -w "$install_dir" ] ||
    fail "$install_dir is not writable. Set MYSETUP_INSTALL_DIR to a folder you own; this installer never uses sudo."
  if [ -d "$install_dir/mysetup" ] || [ -L "$install_dir/mysetup" ]; then
    fail "$install_dir/mysetup is a folder or link; move it aside and try again."
  fi
  staged="$install_dir/.mysetup-install.$$"
  cp "$binary" "$staged"
  chmod 755 "$staged"
  mv -f "$staged" "$install_dir/mysetup"
  staged=""

  say "Installed mysetup $version to $install_dir/mysetup"
  case ":${PATH:-}:" in
    *":$install_dir:"*) ;;
    *) say "Add $install_dir to your PATH to run mysetup from any folder." ;;
  esac
  say "Next: run 'mysetup auth login' to sign in, or 'mysetup help' to get started."
}

main "$@"
