#!/bin/sh
# The structure command, installed in one line:
#
#   curl -fsSL https://runstructure.com/install.sh | sh
#   sh install.sh [--version <stamp>] [--to <dir>]
#
# In order: the machine is read (macOS arm64, Linux x86_64 or Linux arm64; nothing else is served, and one line
# says so); the version is --version's stamp, or the latest, read from the release page's /releases/latest
# redirect; the tarball and its published .sha256 are downloaded into a temp folder under ${TMPDIR:-/tmp}; the
# checksum is verified BEFORE anything is moved; the one file `structure` is placed in --to (default ~/.local/bin),
# and one line says how to put that folder on PATH when it is not. No elevated rights are asked for, nothing is
# written anywhere else, and the temp folder is removed on exit, success or failure. Every failure is one plain
# sentence and a non-zero exit. Over https only; nothing fetched is run, except the command itself once verified.
#
# The two STRUCTURE_INSTALL_TEST_* variables are the test's seams (sites/tests/install_test.ts): a stand-in release
# page on the loopback address, and a forced `uname -s` / `uname -m` pair, read only beside the first. A release
# address that is not loopback http is refused before any request is made.
set -eu

ORG=runstructure
RELEASES="https://github.com/$ORG/structure/releases"
TMPROOT="${TMPDIR:-/tmp}"
proto='=https'

say() { printf '%s\n' "$1"; }
fail() { printf '%s\n' "$1" >&2; exit "${2:-1}"; }
usage() { say 'Usage: sh install.sh [--version <stamp>] [--to <dir>]'; }
fetch() { curl -fsSL --proto "$proto" --proto-redir "$proto" -o "$2" "$1" 2>/dev/null; }

version=""
to=""
while [ $# -gt 0 ]; do
  case "$1" in
    --version) [ $# -ge 2 ] || { usage >&2; exit 2; }; version="$2"; shift 2 ;;
    --version=*) version="${1#--version=}"; shift ;;
    --to) [ $# -ge 2 ] || { usage >&2; exit 2; }; to="$2"; shift 2 ;;
    --to=*) to="${1#--to=}"; shift ;;
    -h|--help) usage; exit 0 ;;
    *) usage >&2; exit 2 ;;
  esac
done
# the tag's spelling (build-<stamp>) is taken as the stamp; a stamp is a date, with a re-cut's digits after it
version="${version#build-}"
case "$version" in
  ''|[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][.0-9]*) ;;
  *) fail 'The version is a stamp like 2026-10-23.' 2 ;;
esac

base="$RELEASES"
if [ -n "${STRUCTURE_INSTALL_TEST_RELEASES:-}" ]; then
  case "$STRUCTURE_INSTALL_TEST_RELEASES" in
    http://127.0.0.1:*|http://localhost:*) base="$STRUCTURE_INSTALL_TEST_RELEASES"; proto='=https,http' ;;
    *) fail "STRUCTURE_INSTALL_TEST_RELEASES is the test's seam and must be a loopback http address." ;;
  esac
fi

if [ -n "${STRUCTURE_INSTALL_TEST_UNAME:-}" ] && [ -n "${STRUCTURE_INSTALL_TEST_RELEASES:-}" ]; then
  os="${STRUCTURE_INSTALL_TEST_UNAME% *}"; arch="${STRUCTURE_INSTALL_TEST_UNAME#* }"
else
  os="$(uname -s)"; arch="$(uname -m)"
fi
case "$os/$arch" in
  Darwin/arm64) platform=macos-arm64; shown="macOS arm64" ;;
  Linux/x86_64) platform=linux-x86_64; shown="Linux x86_64" ;;
  Linux/aarch64|Linux/arm64) platform=linux-arm64; shown="Linux arm64" ;;
  *) fail "There is no structure build for $os $arch yet. The builds are for macOS arm64, Linux x86_64 and Linux arm64." ;;
esac

command -v curl >/dev/null 2>&1 || fail 'curl is needed to download structure. Install curl and run this again.'
command -v tar >/dev/null 2>&1 || fail 'tar is needed to unpack structure. Install tar and run this again.'
if command -v sha256sum >/dev/null 2>&1; then
  digest() { sha256sum "$1" | cut -d' ' -f1; }
elif command -v shasum >/dev/null 2>&1; then
  digest() { shasum -a 256 "$1" | cut -d' ' -f1; }
else
  fail 'Neither sha256sum nor shasum is on this machine, so the download cannot be verified. Install one and run this again.'
fi

if [ -z "$to" ]; then
  [ -n "${HOME:-}" ] || fail 'HOME is not set. Pass --to <dir>.'
  to="$HOME/.local/bin"
fi

if [ -z "$version" ]; then
  final="$(curl -fsSL --proto "$proto" --proto-redir "$proto" -o /dev/null -w '%{url_effective}' "$base/latest" 2>/dev/null)" || final=""
  tag="${final##*/}"
  case "$tag" in
    build-*) version="${tag#build-}" ;;
    *) fail "The latest version could not be read from $RELEASES. Check your connection, or pass --version <stamp>." ;;
  esac
fi

tmp="$(mktemp -d "$TMPROOT/structure-install.XXXXXX")" || fail "A temporary folder could not be made under $TMPROOT."
trap 'rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"; exit 130' HUP INT TERM

asset="structure-$platform.tar.gz"
url="$base/download/build-$version/$asset"
say "Downloading structure $version for $shown."
fetch "$url" "$tmp/$asset" || fail "The download of $asset for version $version failed. Check the version at $RELEASES and your connection, then run this again."
fetch "$url.sha256" "$tmp/$asset.sha256" || fail "The published checksum for $asset (version $version) could not be downloaded, so nothing was installed. Run this again later."
# the published file is shasum's form: <sha256>  <name>
want="$(head -n 1 "$tmp/$asset.sha256" | tr -d '\r' | cut -d' ' -f1 | tr 'A-F' 'a-f')"
case "$want" in *[!0-9a-f]*) want="" ;; esac
[ "${#want}" -eq 64 ] || fail "The published checksum for $asset could not be read, so nothing was installed. Run this again later."
got="$(digest "$tmp/$asset")"
[ "$got" = "$want" ] || fail 'The download does not match its published checksum, so nothing was installed. Run this again later.'
tar -xzf "$tmp/$asset" -C "$tmp" 2>/dev/null || fail 'The download could not be unpacked, so nothing was installed. Run this again later.'
[ -f "$tmp/structure" ] || fail 'The download did not hold the structure command, so nothing was installed.'
chmod 755 "$tmp/structure"
mkdir -p "$to" 2>/dev/null && to_abs="$(cd "$to" 2>/dev/null && pwd)" || fail "$to could not be made. Pass --to <dir> with a folder you can write to."
# a folder (or a link to one) already at the destination: mv would move the command inside it and say it succeeded
[ ! -d "$to_abs/structure" ] || fail "A folder already stands at $to_abs/structure, so structure was not placed there. Move it aside, or pass --to <dir>."
mv -f "$tmp/structure" "$to_abs/structure" 2>/dev/null || fail "structure could not be placed in $to. Pass --to <dir> with a folder you can write to."
dest="$to_abs/structure"

case ":${PATH:-}:" in
  *":$to_abs:"*|*":$to:"*) ;;
  *)
    if [ "$to_abs" = "${HOME:-}/.local/bin" ]; then
      say 'Add ~/.local/bin to your PATH to run it from anywhere: export PATH="$HOME/.local/bin:$PATH"'
    else
      dir="$to_abs"
      say "Add $dir to your PATH to run it from anywhere: export PATH=\"$dir:\$PATH\""
    fi ;;
esac
say "structure $version installed at $dest"
