#!/bin/sh
# Install the karts CLI.
#
#   curl -fsSL https://karts.kartikey.fyi/install.sh | sh
#
# Downloads the release for this OS and CPU over HTTPS, checks it against the release's
# SHA256SUMS, and installs `karts` to ~/.local/bin without sudo. It refuses, and installs
# nothing, when the checksum is missing or does not match.
#
# Settings (environment):
#   KARTS_VERSION        a version to install, e.g. 1.2.3 (default: the latest release)
#   KARTS_INSTALL_DIR    where to put karts (default: $HOME/.local/bin)
#   KARTS_DOWNLOAD_BASE  release server, https only (default: https://karts.kartikey.fyi/download)
#   KARTS_OS, KARTS_ARCH force the target (darwin|linux, amd64|arm64). For tests: a binary
#                        for another platform is installed but not run.
#
# Everything is inside functions and the last line calls main, so a download cut short
# defines nothing it runs.
set -eu

KARTS_DEFAULT_BASE=https://karts.kartikey.fyi/download

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

die() {
	printf 'karts install: %s\n' "$*" >&2
	exit 1
}

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

native_os() {
	case $(uname -s) in
	Darwin) echo darwin ;;
	Linux) echo linux ;;
	*) echo "unsupported:$(uname -s)" ;;
	esac
}

native_arch() {
	m=$(uname -m)
	# A shell under Rosetta reports x86_64 on Apple silicon; the arm64 build is the right one.
	if [ "$m" = x86_64 ] && [ "$(uname -s)" = Darwin ] &&
		[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then
		m=arm64
	fi
	case $m in
	x86_64 | amd64) echo amd64 ;;
	arm64 | aarch64) echo arm64 ;;
	*) echo "unsupported:$m" ;;
	esac
}

valid_version() {
	printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'
}

# fetch URL FILE: HTTPS only, including every redirect; fails on any HTTP error.
fetch() {
	curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
		--retry 3 --connect-timeout 20 --output "$2" "$1"
}

sha256_of() {
	if have sha256sum; then
		sha256sum "$1" | awk '{print $1}'
	elif have shasum; then
		shasum -a 256 "$1" | awk '{print $1}'
	else
		return 1
	fi
}

main() {
	have curl || die "curl is required; install it and run this again"
	have tar || die "tar is required"
	have sha256sum || have shasum || die "sha256sum or shasum is required to verify the download"

	base=${KARTS_DOWNLOAD_BASE:-$KARTS_DEFAULT_BASE}
	base=${base%/}
	case $base in
	https://*) ;;
	*) die "KARTS_DOWNLOAD_BASE must be an https:// URL, not \"$base\"" ;;
	esac

	host_os=$(native_os)
	host_arch=$(native_arch)
	os=${KARTS_OS:-$host_os}
	arch=${KARTS_ARCH:-$host_arch}
	case $os in
	darwin | linux) ;;
	*) die "no karts build for this OS (${os#unsupported:}); builds exist for darwin and linux" ;;
	esac
	case $arch in
	amd64 | arm64) ;;
	*) die "no karts build for this CPU (${arch#unsupported:}); builds exist for amd64 and arm64" ;;
	esac

	tmp=$(mktemp -d "${TMPDIR:-/tmp}/karts-install.XXXXXX")
	trap 'rm -rf "$tmp"' EXIT
	trap 'exit 130' INT TERM

	version=${KARTS_VERSION:-}
	version=${version#v}
	if [ -z "$version" ]; then
		fetch "$base/latest.txt" "$tmp/latest.txt" || die "could not read $base/latest.txt"
		version=$(tr -d ' \t\r\n' <"$tmp/latest.txt")
	fi
	valid_version "$version" || die "\"$version\" is not a karts version"

	name="karts_${version}_${os}_${arch}.tar.gz"
	url="$base/$version"
	say "downloading karts $version for $os/$arch"
	fetch "$url/$name" "$tmp/$name" || die "could not download $url/$name"
	fetch "$url/SHA256SUMS" "$tmp/SHA256SUMS" ||
		die "could not download $url/SHA256SUMS; refusing to install an unverified binary"

	want=$(awk -v f="$name" '$2 == f || $2 == "*" f { print $1 }' "$tmp/SHA256SUMS")
	case $want in
	"") die "SHA256SUMS has no entry for $name; refusing to install an unverified binary" ;;
	*[!0-9a-f]*) die "SHA256SUMS entry for $name is not one sha256; refusing to install" ;;
	esac
	[ ${#want} -eq 64 ] || die "SHA256SUMS entry for $name is not one sha256; refusing to install"
	got=$(sha256_of "$tmp/$name") || die "could not compute the sha256 of $name"
	if [ "$got" != "$want" ]; then
		die "checksum mismatch for $name (expected $want, got $got); nothing installed"
	fi
	say "verified sha256 $got"

	mkdir "$tmp/x"
	tar -xzf "$tmp/$name" -C "$tmp/x" karts || die "could not unpack $name"
	if [ ! -f "$tmp/x/karts" ] || [ -L "$tmp/x/karts" ]; then
		die "$name has no karts binary"
	fi

	dir=${KARTS_INSTALL_DIR:-${HOME:?HOME is not set; set KARTS_INSTALL_DIR}/.local/bin}
	mkdir -p "$dir" || die "could not create $dir; set KARTS_INSTALL_DIR to a directory you can write"
	# Copy next to the target, then rename over it: a running karts is never half-written.
	cp "$tmp/x/karts" "$dir/.karts.install.$$" ||
		die "could not write to $dir; set KARTS_INSTALL_DIR to a directory you can write"
	chmod 755 "$dir/.karts.install.$$"
	mv -f "$dir/.karts.install.$$" "$dir/karts" || {
		rm -f "$dir/.karts.install.$$"
		die "could not install $dir/karts"
	}
	say "installed $dir/karts"

	case ":${PATH:-}:" in
	*":$dir:"*) ;;
	*)
		say "$dir is not on your PATH. Add it, for example:"
		# shellcheck disable=SC2016 # $PATH is meant literally, for the user's shell profile.
		printf '    echo '\''export PATH="%s:$PATH"'\'' >> ~/.profile   # or ~/.zshrc, ~/.bashrc\n' "$dir"
		;;
	esac

	if [ "$os/$arch" != "$host_os/$host_arch" ]; then
		say "not running it: the build is for $os/$arch and this machine is $host_os/$host_arch"
		return 0
	fi
	"$dir/karts" version || die "$dir/karts did not run"
}

main "$@"
