#!/bin/sh
# Orrery harness installer: puts the `orrery` CLI on this machine.
#
#   curl -fsSL https://getorrery.dev/install.sh | sh
#
# Settings (environment variables, all optional):
#   ORRERY_VERSION=0.2.0       install this version instead of the latest
#   ORRERY_INSTALL_DIR=DIR     where `orrery` goes (default: /usr/local/bin
#                              when writable or root, else ~/.local/bin).
#                              On Unix the binary goes to ../lib/orrery, and
#                              DIR/orrery links to it. The archive's extensions/
#                              are copied to ~/.orrery/extensions, not beside it
#   ORRERY_WITH_RELAY=1        also install `orrery-relay` (self-hosted relay)
#   ORRERY_JOIN_ENV=FILE       a cloud instance's join file: after installing,
#                              `orrery node join --env-file FILE`, then make
#                              sure the node runs and starts at boot; the file
#                              is shredded once the join succeeds
#   ORRERY_RELAY=URL           join this relay with a device code, then
#                              `orrery node up --install`
#   ORRERY_MANIFEST_URL=URL    the release manifest to read (mirrors, tests)
#   ORRERY_TARGET=TRIPLE       skip detection (tests; e.g. x86_64-unknown-linux-musl)
#
# The manifest (`harness-latest.json`) names one archive per target with its
# sha256; nothing is installed unless the download matches it.
# Windows: install.ps1 (`irm https://getorrery.dev/install.ps1 | iex`);
# this script also works from Git Bash.

set -eu

RELEASES="https://github.com/kouji-dev/orrery-releases"
LATEST_URL="https://raw.githubusercontent.com/kouji-dev/orrery-releases/main/harness-latest.json"

say() { printf 'orrery-install: %s\n' "$*" >&2; }
die() {
  printf 'orrery-install: error: %s\n' "$*" >&2
  exit 1
}

# ---------------------------------------------------------------- target
detect_target() {
  os=$(uname -s)
  arch=$(uname -m)
  case "$arch" in
    x86_64 | amd64) arch=x86_64 ;;
    aarch64 | arm64) arch=aarch64 ;;
    *) die "no build for CPU '$arch' (built: x86_64, aarch64)" ;;
  esac
  case "$os" in
    Linux) echo "${arch}-unknown-linux-musl" ;;
    Darwin)
      if [ "$arch" = x86_64 ]; then
        # A shell under Rosetta reports x86_64 on Apple silicon; ask the kernel.
        if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = 1 ]; then
          echo "aarch64-apple-darwin"
          return
        fi
        die "no build for Intel macOS yet (built: Apple silicon). From source: cargo install --git https://github.com/kouji-dev/orrery orrery-cli"
      fi
      echo "aarch64-apple-darwin"
      ;;
    MINGW* | MSYS* | CYGWIN*) echo "x86_64-pc-windows-msvc" ;;
    *) die "no build for '$os' (built: Linux, macOS, Windows)" ;;
  esac
}

# ---------------------------------------------------------------- tools
# is_loopback_http URL: plain http to this machine only (a local mirror, tests).
is_loopback_http() {
  case "$1" in
    http://127.0.0.1 | http://127.0.0.1[:/]* | http://localhost | http://localhost[:/]* | http://\[::1\]*) return 0 ;;
    *) return 1 ;;
  esac
}

# need_https URL WHAT: https, or http to this machine; anything else is refused.
need_https() {
  case "$1" in
    https://*) ;;
    *) is_loopback_http "$1" || die "$2 must be an https:// URL (plain http only to 127.0.0.1/localhost): $1" ;;
  esac
}

fetch() { # fetch URL FILE: https only (redirects too), but for loopback http
  if is_loopback_http "$1"; then
    proto='=http,https'
  else
    proto='=https'
  fi
  if command -v curl >/dev/null 2>&1; then
    curl --proto "$proto" --proto-redir '=https' -fsSL --retry 3 -o "$2" "$1"
  elif command -v wget >/dev/null 2>&1; then
    if is_loopback_http "$1"; then
      wget -q -O "$2" "$1"
    elif wget --help 2>&1 | grep -q -- --https-only; then
      wget -q --https-only -O "$2" "$1"
    else
      # BusyBox wget has no --https-only; the URL itself is https.
      wget -q -O "$2" "$1"
    fi
  else
    die "needs curl or wget"
  fi
}

sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | cut -d' ' -f1
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | cut -d' ' -f1
  elif command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$1" | sed 's/^.*= //'
  else
    die "needs sha256sum, shasum or openssl to verify the download"
  fi
}

# json_target MANIFEST TRIPLE FIELD: one field of one target, without jq.
# The manifest is flattened, the target's object cut out, then the field.
json_target() {
  tr -d '\n\r\t ' <"$1" |
    sed -n "s/.*\"$2\":{\([^}]*\)}.*/\1/p" |
    tr ',' '\n' |
    sed -n "s/^\"$3\":\"\{0,1\}\([^\"]*\)\"\{0,1\}\$/\1/p"
}

# json_top MANIFEST FIELD: a top-level string field.
json_top() {
  tr -d '\n\r\t ' <"$1" | sed -n "s/^{\"$2\":\"\([^\"]*\)\".*/\1/p"
}

# ---------------------------------------------------------------- where
pick_dir() {
  if [ -n "${ORRERY_INSTALL_DIR:-}" ]; then
    echo "$ORRERY_INSTALL_DIR"
  elif [ "$target" = x86_64-pc-windows-msvc ]; then
    echo "$HOME/.orrery/bin"
  elif [ "$(id -u)" = 0 ] || [ -w /usr/local/bin ]; then
    echo /usr/local/bin
  else
    echo "$HOME/.local/bin"
  fi
}

# Replace a binary that may be running (a node is): move the old one aside
# first, so an executing file is never overwritten in place.
place() { # place SRC DEST
  if [ -e "$2" ]; then
    rm -f "$2.old" 2>/dev/null || true
    mv -f "$2" "$2.old" || die "cannot replace $2"
  fi
  cp "$1" "$2"
  chmod 755 "$2"
  # Windows keeps a running .exe locked; the .old stays until the next install.
  rm -f "$2.old" 2>/dev/null || true
}

# ---------------------------------------------------------------- main
main() {
  # cloud-init's runcmd runs without HOME; the node keeps its key under it.
  if [ -z "${HOME:-}" ]; then
    HOME=$( (getent passwd "$(id -u)" 2>/dev/null || true) | cut -d: -f6)
    [ -n "$HOME" ] || HOME=/root
    export HOME
  fi
  target=${ORRERY_TARGET:-$(detect_target)}
  exe=""
  case "$target" in *windows*) exe=".exe" ;; esac

  pin=""
  if [ -n "${ORRERY_VERSION:-}" ]; then
    pin=${ORRERY_VERSION#harness-}
    pin=${pin#v}
  fi
  if [ -n "${ORRERY_MANIFEST_URL:-}" ]; then
    manifest_url=$ORRERY_MANIFEST_URL
  elif [ -n "$pin" ]; then
    manifest_url="$RELEASES/releases/download/harness-v$pin/harness-latest.json"
  else
    manifest_url=$LATEST_URL
  fi

  tmp=$(mktemp -d 2>/dev/null || mktemp -d -t orrery)
  trap 'rm -rf "$tmp"' EXIT
  trap 'exit 130' INT TERM

  need_https "$manifest_url" "the release manifest (ORRERY_MANIFEST_URL)"
  say "reading $manifest_url"
  fetch "$manifest_url" "$tmp/manifest.json" ||
    die "could not read the release manifest ($manifest_url). Releases: $RELEASES/releases (pin one with ORRERY_VERSION=X.Y.Z)"

  version=$(json_top "$tmp/manifest.json" version)
  [ -n "$version" ] || die "$manifest_url is not a harness manifest (no version)"
  if [ -n "$pin" ] && [ "$version" != "$pin" ]; then
    die "asked for $pin, the manifest says $version"
  fi
  url=$(json_target "$tmp/manifest.json" "$target" url)
  want=$(json_target "$tmp/manifest.json" "$target" sha256)
  if [ -z "$url" ] || [ -z "$want" ]; then
    die "release $version has no build for $target"
  fi

  need_https "$url" "the archive URL in $manifest_url"
  archive="$tmp/$(basename "$url")"
  say "downloading orrery $version ($target)"
  fetch "$url" "$archive" || die "download failed: $url"
  got=$(sha256_of "$archive")
  [ "$got" = "$want" ] || die "checksum mismatch for $(basename "$url"): expected $want, got $got. Nothing was installed."

  mkdir -p "$tmp/x"
  case "$archive" in
    *.zip) (cd "$tmp/x" && unzip -q "$archive") || die "could not unpack $archive (needs unzip)" ;;
    *) tar -xzf "$archive" -C "$tmp/x" || die "could not unpack $archive" ;;
  esac
  [ -f "$tmp/x/orrery$exe" ] || die "the archive has no orrery$exe"

  dir=$(pick_dir)
  mkdir -p "$dir" || die "cannot create $dir (set ORRERY_INSTALL_DIR)"
  [ -w "$dir" ] || die "$dir is not writable: run as root, or set ORRERY_INSTALL_DIR=\$HOME/.local/bin"
  # The binary sits in $lib, off PATH; on Unix $dir/orrery links to it
  # (current_exe resolves the link). Windows keeps it in $dir.
  case "$target" in
    *windows*) lib=$dir ;;
    *) lib=$(dirname "$dir")/lib/orrery ;;
  esac
  mkdir -p "$lib" || die "cannot create $lib (set ORRERY_INSTALL_DIR)"
  [ -w "$lib" ] || die "$lib is not writable: run as root, or set ORRERY_INSTALL_DIR=\$HOME/.local/bin"
  place "$tmp/x/orrery$exe" "$lib/orrery$exe"
  # An extensions/ left beside the binary by an older install goes.
  rm -rf "$lib/extensions"
  # Our extensions go to ~/.orrery/extensions (decision 0069), one folder per
  # id the archive carries; other folders there are left alone.
  if [ -d "$tmp/x/extensions" ]; then
    ext="$HOME/.orrery/extensions"
    mkdir -p "$ext" || die "cannot create $ext"
    for src in "$tmp/x/extensions"/*/; do
      [ -d "$src" ] || continue
      id=$(basename "$src")
      rm -rf "$ext/$id"
      cp -R "$src" "$ext/$id"
    done
    say "extensions installed to $ext"
  fi
  if [ "$lib" != "$dir" ]; then
    ln -sf "$lib/orrery$exe" "$dir/orrery$exe"
  fi
  say "installed $dir/orrery$exe"
  if [ "${ORRERY_WITH_RELAY:-}" = 1 ]; then
    [ -f "$tmp/x/orrery-relay$exe" ] || die "the archive has no orrery-relay$exe"
    place "$tmp/x/orrery-relay$exe" "$dir/orrery-relay$exe"
    say "installed $dir/orrery-relay$exe"
  fi

  orrery="$dir/orrery$exe"
  "$orrery" --version >/dev/null 2>&1 || die "$orrery does not run on this machine"

  case ":$PATH:" in
    *":$dir:"*) ;;
    *) say "$dir is not on PATH; add it: export PATH=\"$dir:\$PATH\"" ;;
  esac

  # A cloud instance: join with its one-time file, then make sure the node
  # runs and starts at boot. `join --env-file` already starts it (a system
  # unit when root); `up --install` covers a node that is not running.
  if [ -n "${ORRERY_JOIN_ENV:-}" ]; then
    [ -r "$ORRERY_JOIN_ENV" ] || die "ORRERY_JOIN_ENV=$ORRERY_JOIN_ENV is not readable"
    say "joining Orrery Cloud with $ORRERY_JOIN_ENV"
    "$orrery" node join --env-file "$ORRERY_JOIN_ENV" ||
      die "orrery node join failed; $ORRERY_JOIN_ENV is kept for a retry"
    if command -v shred >/dev/null 2>&1; then
      shred -u "$ORRERY_JOIN_ENV"
    else
      rm -f "$ORRERY_JOIN_ENV"
    fi
    if ! "$orrery" --json node status 2>/dev/null | grep -q '"running":true'; then
      "$orrery" node up --install
    fi
  elif [ -n "${ORRERY_RELAY:-}" ]; then
    say "joining $ORRERY_RELAY: approve the code it shows in the web app"
    if [ -r /dev/tty ]; then
      "$orrery" node join "$ORRERY_RELAY" </dev/tty || die "orrery node join failed"
    else
      "$orrery" node join "$ORRERY_RELAY" || die "orrery node join failed"
    fi
    "$orrery" node up --install
  else
    say "next: orrery                         start a session here"
    say "      orrery node join https://<relay>   enrol this machine with Orrery Cloud"
  fi
}

main "$@"
