#!/bin/sh
# install.sh — one-line installer for the `jurniti` developer CLI.
#
# Served from the marketing origin so the documented one-liner is:
#
#     curl -fsSL https://jurniti.com/install.sh | sh
#
# It detects the host OS (linux/darwin) + arch (amd64/arm64) with `uname`,
# downloads the matching pinned release tarball, VERIFIES it against the
# published SHA256SUMS (aborting on mismatch), extracts the `jurniti` binary,
# installs it to /usr/local/bin (falling back to ~/.local/bin when that isn't
# writable), marks it executable, and prints the `jurniti login` next step.
# POSIX sh only — no bashisms — so it runs under dash/ash/sh.
#
# Download source (override with JURNITI_INSTALL_BASE):
#   default → the marketing origin, https://jurniti.com/dl. That path is a
#   Vercel rewrite to a public S3 releases bucket (us-west-2); the source repo
#   is never in the user's download path. Asset naming is version-independent
#   (jurniti_<os>_<arch>.tar.gz) so /dl/<asset> always resolves to the newest
#   release — the CI cli-release job (.github/workflows/cli-release.yml)
#   uploads exactly these names to S3 on every tag.
#
# This script is HARNESS-AGNOSTIC platform infra (per the two-layer rule);
# it never names a harness.

set -eu

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
JURNITI_INSTALL_BASE="${JURNITI_INSTALL_BASE:-https://jurniti.com/dl}"
BIN_NAME="jurniti"

# ---------------------------------------------------------------------------
# Small output helpers (stderr for chatter, so `| sh` stays quiet on stdout)
# ---------------------------------------------------------------------------
info() { printf '%s\n' "$*" >&2; }
err() { printf 'jurniti install: %s\n' "$*" >&2; }
die() {
	err "$*"
	exit 1
}

# detect_os maps `uname -s` to the release OS token, or prints nothing (the
# caller treats empty as "unsupported"). Kept as a pure function of its one
# argument so it is unit-testable without a real host.
detect_os() {
	case "$1" in
	Linux) printf 'linux' ;;
	Darwin) printf 'darwin' ;;
	*) printf '' ;;
	esac
}

# detect_arch maps `uname -m` to the release arch token. amd64 and arm64 are
# the only two we cross-build; everything else prints empty (unsupported).
detect_arch() {
	case "$1" in
	x86_64 | amd64) printf 'amd64' ;;
	aarch64 | arm64) printf 'arm64' ;;
	*) printf '' ;;
	esac
}

# have checks for a command on PATH.
have() { command -v "$1" >/dev/null 2>&1; }

# verify_checksum aborts unless the downloaded tarball matches its published
# SHA256SUMS entry. Args: <dir> <asset> <sumsfile>. The CI cli-release job
# publishes a single SHA256SUMS listing all four tarballs; we slice out just
# our asset's line (awk exact-match on the filename column) so the checker
# doesn't fail on the three tarballs it references but we didn't download,
# then run `sha256sum -c` (GNU) or `shasum -a 256 -c` (macOS) from inside the
# download dir so the referenced basename resolves. Neither tool present ⇒
# abort rather than silently install an unverified binary — this is the whole
# point of the check (supply-chain integrity on a public `curl … | sh`).
verify_checksum() {
	_dir="$1"
	_asset="$2"
	_sums="$3"
	_line="${_dir}/${_asset}.sha256"
	awk -v f="$_asset" '$2 == f { print; found = 1 } END { exit found ? 0 : 1 }' \
		"$_sums" >"$_line" ||
		die "SHA256SUMS has no entry for ${_asset} — cannot verify download"
	if have sha256sum; then
		( cd "$_dir" && sha256sum -c "${_asset}.sha256" >/dev/null 2>&1 ) ||
			die "checksum verification FAILED for ${_asset} — refusing to install a tampered or corrupt binary"
	elif have shasum; then
		( cd "$_dir" && shasum -a 256 -c "${_asset}.sha256" >/dev/null 2>&1 ) ||
			die "checksum verification FAILED for ${_asset} — refusing to install a tampered or corrupt binary"
	else
		die "need sha256sum or shasum to verify the download — refusing to install an unverified binary"
	fi
}

# download fetches URL ($1) to file ($2) using curl or wget, whichever exists.
download() {
	url="$1"
	dest="$2"
	if have curl; then
		curl -fsSL "$url" -o "$dest"
	elif have wget; then
		wget -qO "$dest" "$url"
	else
		die "need curl or wget to download $url"
	fi
}

# choose_install_dir prints the first writable install target: /usr/local/bin
# if writable (or creatable via sudo-less mkdir), else ~/.local/bin. It never
# invokes sudo itself — a non-writable /usr/local/bin transparently falls back
# to the per-user dir so `curl … | sh` works without a password prompt.
choose_install_dir() {
	if [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
		printf '/usr/local/bin'
		return 0
	fi
	# ~/.local/bin is the XDG per-user fallback; create it if missing.
	fallback="${HOME}/.local/bin"
	mkdir -p "$fallback" 2>/dev/null || die "cannot create $fallback"
	printf '%s' "$fallback"
}

main() {
	os="$(detect_os "$(uname -s)")"
	arch="$(detect_arch "$(uname -m)")"
	[ -n "$os" ] || die "unsupported OS $(uname -s) (need Linux or macOS)"
	[ -n "$arch" ] || die "unsupported arch $(uname -m) (need x86_64/amd64 or arm64/aarch64)"

	# The download source MUST be TLS: an over-HTTP tarball can be swapped in
	# flight, defeating the checksum (an attacker who controls the tarball
	# controls the SHA256SUMS served alongside it). Reject a plaintext override.
	case "$JURNITI_INSTALL_BASE" in
	https://*) ;;
	*) die "JURNITI_INSTALL_BASE must be an https:// URL (got: ${JURNITI_INSTALL_BASE})" ;;
	esac

	asset="${BIN_NAME}_${os}_${arch}.tar.gz"
	url="${JURNITI_INSTALL_BASE}/${asset}"
	sums_url="${JURNITI_INSTALL_BASE}/SHA256SUMS"

	tmp="$(mktemp -d 2>/dev/null || mktemp -d -t jurniti)"
	# Best-effort cleanup on any exit.
	trap 'rm -rf "$tmp"' EXIT INT TERM

	info "Downloading ${BIN_NAME} (${os}/${arch}) …"
	download "$url" "$tmp/${asset}" || die "download failed: $url"

	info "Verifying checksum …"
	download "$sums_url" "$tmp/SHA256SUMS" || die "could not download SHA256SUMS: $sums_url"
	verify_checksum "$tmp" "$asset" "$tmp/SHA256SUMS"

	info "Extracting …"
	tar -xzf "$tmp/${asset}" -C "$tmp" || die "extract failed (corrupt archive?)"
	[ -f "$tmp/${BIN_NAME}" ] || die "archive did not contain a ${BIN_NAME} binary"

	dir="$(choose_install_dir)"
	install_path="${dir}/${BIN_NAME}"
	# `mv` then chmod: atomic-ish replace of any existing binary.
	mv "$tmp/${BIN_NAME}" "$install_path" || die "could not write $install_path"
	chmod +x "$install_path" || die "could not chmod +x $install_path"

	info ""
	info "Installed ${BIN_NAME} → ${install_path}"
	case ":${PATH}:" in
	*":${dir}:"*) ;;
	*) info "NOTE: ${dir} is not on your PATH — add it: export PATH=\"${dir}:\$PATH\"" ;;
	esac

	# Anonymous install success ping (floor for operator analytics). Never
	# fails the install: empty JURNITI_INSTALL_TELEMETRY_URL disables; network
	# errors are ignored. Default hits the public control-plane track endpoint.
	report_install_telemetry "$os" "$arch"

	info ""
	info "Next step:"
	info "  ${BIN_NAME} login"
}

# report_install_telemetry POSTs a closed-set {os,arch} JSON body. Best-effort
# only — `|| true` / short timeout so a telemetry outage never fails install.
report_install_telemetry() {
	_os="$1"
	_arch="$2"
	# Unset → production default. Explicit empty string disables.
	if [ "${JURNITI_INSTALL_TELEMETRY_URL+set}" = "set" ] && [ -z "${JURNITI_INSTALL_TELEMETRY_URL}" ]; then
		return 0
	fi
	_url="${JURNITI_INSTALL_TELEMETRY_URL:-https://api.jurniti.com/v1/track/cli-install}"
	_body="{\"os\":\"${_os}\",\"arch\":\"${_arch}\"}"
	if have curl; then
		curl -fsS -m 2 -X POST \
			-H 'Content-Type: application/json' \
			-d "$_body" \
			"$_url" >/dev/null 2>&1 || true
	elif have wget; then
		wget -q -T 2 -O /dev/null \
			--header='Content-Type: application/json' \
			--post-data="$_body" \
			"$_url" 2>/dev/null || true
	fi
}

# Sourcing guard: a unit test can `JURNITI_INSTALL_LIB=1 . install.sh` to load
# the detection functions WITHOUT running the installer. `return` is valid in a
# sourced script; the guard means it's only ever reached when sourced.
if [ "${JURNITI_INSTALL_LIB:-}" = "1" ]; then
	return 0 2>/dev/null || exit 0
fi

main "$@"
