#!/bin/sh
# shellcheck shell=sh
#
# repyh-console installer for sh.repyhlabs.dev.
#
# Fetches the matching prebuilt binary through the console backend's
# CLI release proxy (which in turn reads from the private GitHub repo
# server-side using a PAT), verifies its SHA256 against the published
# checksums, and installs it to $REPYH_CONSOLE_INSTALL_DIR (default
# $HOME/.local/bin).
#
# The backend proxy is what makes `curl | sh` work at all: the source
# repo is private, so direct GitHub release URLs return 404 to anonymous
# clients. /api/v1/cli/{latest,download} are explicitly unauth'd for
# this install flow.
#
# Author-time source lives at installer/install.rs (rust-script). This
# file is the artifact served to users -- POSIX sh, no dependencies on a
# Rust toolchain at run time.
#
# Usage:
#   curl --proto '=https' --tlsv1.2 -sSf https://sh.repyhlabs.dev | sh
#
# Env:
#   REPYH_CONSOLE_VERSION      Pin a specific release, e.g. v0.1.0.
#                              Defaults to the latest non-prerelease tag.
#   REPYH_CONSOLE_INSTALL_DIR  Install destination. Default:
#                              $HOME/.local/bin. Use /usr/local/bin to
#                              install system-wide (re-run with sudo
#                              then, since $HOME won't be writable).
#   REPYH_CONSOLE_SERVER       Backend URL. Default:
#                              https://console.repyhlabs.dev. Override
#                              for staging or an isolated environment.

set -eu

BIN_NAME="repyh-console"
SERVER="${REPYH_CONSOLE_SERVER:-https://console.repyhlabs.dev}"

main() {
	need curl
	need uname
	need mkdir
	need chmod
	need mktemp
	need_sha256

	target="$(detect_target)"
	os="${target%-*}"
	arch="${target#*-}"

	version="${REPYH_CONSOLE_VERSION:-}"
	declared_sha256=""
	if [ -z "$version" ]; then
		# The latest manifest gives us both the resolved version and the
		# sha256 for our platform, so we only pay one round-trip.
		latest_json="$(fetch_latest_manifest)"
		version="$(json_field "$latest_json" version)"
		if [ -z "$version" ]; then
			err "manifest had no version field; server response: $latest_json"
			exit 1
		fi
		declared_sha256="$(platform_sha256 "$latest_json" "$os" "$arch")"
	fi

	install_dir="${REPYH_CONSOLE_INSTALL_DIR:-$HOME/.local/bin}"
	mkdir -p "$install_dir"

	asset="${BIN_NAME}-${target}"
	tmpdir="$(mktemp -d 2>/dev/null || mktemp -d -t repyh-console-install)"
	# Want $tmpdir expanded at trap-registration time, not firing time.
	# shellcheck disable=SC2064
	trap "rm -rf '$tmpdir'" EXIT INT HUP TERM

	info "Downloading ${asset} (${version})…"
	download_url="${SERVER}/api/v1/cli/download?version=${version}&os=${os}&arch=${arch}"
	download "$download_url" "${tmpdir}/${asset}"

	if [ -n "$declared_sha256" ]; then
		info "Verifying SHA256…"
		verify_sha256 "${tmpdir}/${asset}" "$declared_sha256"
	else
		# Happens when the user pinned REPYH_CONSOLE_VERSION explicitly
		# (we skip the manifest round-trip in that case). Warn so the
		# behaviour is audible -- this isn't the normal flow.
		info "warning: no published SHA256 to verify against; skipping"
	fi

	info "Installing to ${install_dir}/${BIN_NAME}…"
	chmod +x "${tmpdir}/${asset}"
	if ! mv "${tmpdir}/${asset}" "${install_dir}/${BIN_NAME}" 2>/dev/null; then
		err "cannot write to ${install_dir}/${BIN_NAME}"
		err "set REPYH_CONSOLE_INSTALL_DIR or re-run with sudo"
		exit 1
	fi

	info ""
	info "✓ Installed ${BIN_NAME} ${version} → ${install_dir}/${BIN_NAME}"
	warn_path "$install_dir"
	info ""
	info "Next:  ${BIN_NAME} auth login"
}

# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

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

need() {
	command -v "$1" >/dev/null 2>&1 || {
		err "required command '$1' not found in PATH"
		exit 1
	}
}

# sha256 utility discovery: macOS ships shasum, Linux ships sha256sum.
need_sha256() {
	if command -v sha256sum >/dev/null 2>&1; then
		SHA256_CMD="sha256sum"
	elif command -v shasum >/dev/null 2>&1; then
		SHA256_CMD="shasum -a 256"
	else
		err "neither sha256sum nor shasum is available"
		exit 1
	fi
}

# detect_target maps uname to the <os>-<arch> suffix used in release
# filenames. Hard fails on anything we don't publish a binary for.
detect_target() {
	os="$(uname -s)"
	arch="$(uname -m)"

	case "$os" in
		Darwin) os="darwin" ;;
		Linux) os="linux" ;;
		*)
			err "unsupported OS '$os'; see ${SERVER}/api/v1/cli/latest"
			exit 1
			;;
	esac

	case "$arch" in
		x86_64 | amd64) arch="amd64" ;;
		arm64 | aarch64) arch="arm64" ;;
		*)
			err "unsupported architecture '$arch'; see ${SERVER}/api/v1/cli/latest"
			exit 1
			;;
	esac

	printf '%s-%s' "$os" "$arch"
}

# fetch_latest_manifest returns the full /api/v1/cli/latest JSON body or
# exits with a clear error if the endpoint is unreachable.
fetch_latest_manifest() {
	url="${SERVER}/api/v1/cli/latest"
	body="$(curl --proto '=https' --tlsv1.2 -sSfL "$url" 2>/dev/null)" || {
		err "could not fetch ${url}"
		err "is ${SERVER} reachable? try a browser or ping."
		exit 1
	}
	printf '%s' "$body"
}

# json_field is a tiny single-field parser for top-level string fields.
# Avoids a jq dependency -- the installer must work on stock macOS /
# minimal Linux where jq isn't installed. The release manifest is small
# and shaped deterministically by the server, so this narrow parser is
# safe for our use.
json_field() {
	body="$1"
	field="$2"
	printf '%s' "$body" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n1 | sed -E "s/\"${field}\"[[:space:]]*:[[:space:]]*\"([^\"]*)\"/\1/"
}

# platform_sha256 digs the sha256 for a specific os/arch asset out of the
# manifest's assets array. The manifest's JSON structure for each asset
# is flat (os/arch/filename/sha256) with no nesting, so a line-oriented
# sed/awk combo is enough.
platform_sha256() {
	body="$1"
	want_os="$2"
	want_arch="$3"
	# Flatten to one asset per line, then find the matching one.
	printf '%s' "$body" \
		| tr '{' '\n' \
		| grep "\"os\"[[:space:]]*:[[:space:]]*\"${want_os}\"" \
		| grep "\"arch\"[[:space:]]*:[[:space:]]*\"${want_arch}\"" \
		| grep -oE '"sha256"[[:space:]]*:[[:space:]]*"[^"]+"' \
		| head -n1 \
		| sed -E 's/"sha256"[[:space:]]*:[[:space:]]*"([^"]+)"/\1/'
}

download() {
	src="$1"
	dst="$2"
	if ! curl --proto '=https' --tlsv1.2 -sSfL -o "$dst" "$src"; then
		err "failed to download $src"
		exit 1
	fi
}

verify_sha256() {
	file="$1"
	expected="$2"
	got="$($SHA256_CMD "$file" | awk '{print $1}')"
	if [ "$got" != "$expected" ]; then
		err "SHA256 mismatch for $file; refusing to install"
		err "  expected: $expected"
		err "  got:      $got"
		exit 1
	fi
}

# warn_path prints a heads-up if the install dir isn't on $PATH.
warn_path() {
	dir="$1"
	case ":$PATH:" in
		*":$dir:"*) return 0 ;;
	esac
	info ""
	info "Note: ${dir} is not on your PATH."
	info "Add this to your shell rc file (~/.bashrc, ~/.zshrc, etc.):"
	info ""
	info "    export PATH=\"${dir}:\$PATH\""
}

main "$@"
