#!/usr/bin/env bash
#
# oloop installer — https://oloop.run
#
# Installs the oloop engine and CLI into one location you own. It creates
# nothing else and writes nothing into your current directory. The CLI ships as
# a self-contained bundle, so no package manager runs after the tarball.
#
#   install dir   ~/.oloop        the engine + the `oloop` command
#   project root  <cwd>/.oloop     created later by oloop itself, per project
#
# Removal: rm -rf ~/.oloop
# oloop never modifies your PATH, your shell rc files, or any tool you have.
#
#   usage: install.sh [--claim-id <id>]
#
# With --claim-id the installer redeems a reservation made on the site, then
# prints how to start a host. The id is passed to `oloop` on the command line
# and nowhere else — it is never written to a URL, a log line, or a file.

set -eu

BASE="${OLOOP_BASE:-https://oloop.run}"
BASE="${BASE%/}"
INSTALL_DIR="${OLOOP_HOME:-$HOME/.oloop}"
CLAIM_ID=""

# SIG-1: the pinned oloop release public key (Ed25519, SPKI PEM). The owner
# replaces this placeholder with the real key when the offline key is generated
# (engine: `npm run sign-release generate-release-key`). A private key never
# ships here. With no pinned key the installer verifies the checksum only and
# says so; with a key it verifies the detached SHA256SUMS.sig as well.
RELEASE_PUBKEY="${OLOOP_RELEASE_PUBKEY:-PLACEHOLDER_RELEASE_PUBKEY}"

fail() {
  echo "oloop install: ERROR: $*" >&2
  exit 1
}

usage() {
  echo "usage: install.sh [--claim-id <id>]"
}

while [ $# -gt 0 ]; do
  case "$1" in
    --claim-id)
      shift
      CLAIM_ID="${1:-}"
      [ -n "$CLAIM_ID" ] || fail "--claim-id needs a value"
      shift
      ;;
    --claim-id=*)
      CLAIM_ID="${1#*=}"
      [ -n "$CLAIM_ID" ] || fail "--claim-id needs a value"
      shift
      ;;
    -h | --help)
      usage
      exit 0
      ;;
    *) fail "unknown argument: $1" ;;
  esac
done

echo "oloop installer"
echo "source: $BASE"

# --- checks ---------------------------------------------------------------

if ! command -v node >/dev/null 2>&1; then
  fail "Node.js is not installed. oloop needs Node 20 or newer.
  Fix: install Node from https://nodejs.org, open a new terminal, and re-run."
fi
NODE_MAJOR="$(node -v | sed 's/^v//' | cut -d. -f1)"
case "$NODE_MAJOR" in
  '' | *[!0-9]*) fail "cannot parse Node version '$(node -v)' — reinstall Node from https://nodejs.org" ;;
esac
if [ "$NODE_MAJOR" -lt 20 ]; then
  fail "Node $(node -v) is too old. oloop needs Node 20 or newer.
  Fix: install Node from https://nodejs.org, open a new terminal, and re-run."
fi
command -v tar >/dev/null 2>&1 || fail "tar not found — it is required to unpack the engine."
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 ||
  fail "curl or wget is required to download from $BASE."

download() { # <url> <dest>
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL --retry 3 --retry-delay 2 -o "$2" "$1"
  else
    wget -q -O "$2" "$1"
  fi
}

hash_file() { # <file> — prints the lowercase sha256
  if command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  else
    openssl dgst -r -sha256 "$1" | awk '{print $1}'
  fi
}

# SIG-1: verify a detached Ed25519 signature with the Node runtime we already
# require. Exit 0 only on a valid signature; never throws a shell error.
verify_release_signature() { # <pubkey-pem> <payload-file> <signature-b64-file>
  node -e '
    const fs = require("node:fs");
    const crypto = require("node:crypto");
    const payload = fs.readFileSync(process.argv[2]);
    const signature = Buffer.from(fs.readFileSync(process.argv[3], "utf8").trim(), "base64");
    const ok = crypto.verify(null, payload, crypto.createPublicKey(process.argv[1]), signature);
    process.exit(ok ? 0 : 1);
  ' -- "$1" "$2" "$3" 2>/dev/null
}

# --- engine ---------------------------------------------------------------

mkdir -p "$INSTALL_DIR"
echo "install dir: $INSTALL_DIR (created if missing)"

TMP="$(mktemp -d "$INSTALL_DIR/.dl.XXXXXX")"
trap 'rm -rf "$TMP"' 0

echo "downloading $BASE/engine/SHA256SUMS"
download "$BASE/engine/SHA256SUMS" "$TMP/SHA256SUMS"
TARBALL="$(awk 'NR == 1 {print $2; exit}' "$TMP/SHA256SUMS")"
EXPECTED="$(awk 'NR == 1 {print $1; exit}' "$TMP/SHA256SUMS" | tr 'A-F' 'a-f')"
[ -n "$TARBALL" ] || fail "could not read the release name from SHA256SUMS"
[ -n "$EXPECTED" ] || fail "could not read the expected sha256 from SHA256SUMS"

echo "downloading $BASE/engine/$TARBALL"
download "$BASE/engine/$TARBALL" "$TMP/$TARBALL"
ACTUAL="$(hash_file "$TMP/$TARBALL")"
if [ "$ACTUAL" != "$EXPECTED" ]; then
  fail "sha256 mismatch for $TARBALL —
  expected $EXPECTED
  got      $ACTUAL
  Nothing was installed. Re-run to retry."
fi

# SIG-1: a checksum from the same origin proves integrity, not authenticity.
# When the origin publishes SHA256SUMS.sig, verify it against the pinned key.
SIGNATURE_STATE="none"
if download "$BASE/engine/SHA256SUMS.sig" "$TMP/SHA256SUMS.sig" 2>/dev/null; then
  if [ "$RELEASE_PUBKEY" = "PLACEHOLDER_RELEASE_PUBKEY" ]; then
    SIGNATURE_STATE="unpinned"
  elif verify_release_signature "$RELEASE_PUBKEY" "$TMP/SHA256SUMS" "$TMP/SHA256SUMS.sig"; then
    SIGNATURE_STATE="verified"
  else
    SIGNATURE_STATE="invalid"
  fi
fi
case "$SIGNATURE_STATE" in
  verified)
    echo "signature verified (release key)"
    ;;
  invalid)
    echo "warning: release signature did NOT verify for SHA256SUMS" >&2
    if [ "${OLOOP_REQUIRE_SIGNATURE:-0}" = "1" ]; then
      fail "release signature verification failed for SHA256SUMS"
    fi
    echo "archive checksum verified: $ACTUAL"
    ;;
  unpinned)
    echo "archive checksum verified: $ACTUAL"
    echo "note: a signature was published but this installer has no pinned key yet"
    ;;
  *)
    echo "archive checksum verified: $ACTUAL"
    echo "note: no release signature published yet — checksum only"
    ;;
esac

VERSION="${TARBALL#oloop-}"
VERSION="${VERSION%.tgz}"

# INS-1 stage-and-swap: extract into package.new (never over the live tree),
# smoke-test the staged CLI, then swap it in. A failure before the swap leaves
# the existing install untouched, so `oloop --version` keeps working. The CLI is
# bundled, so nothing fetches a runtime dependency here.
STAGE="$INSTALL_DIR/package.new"
OLD="$INSTALL_DIR/package.old"
rm -rf "$STAGE"
mkdir -p "$STAGE"
tar -xzf "$TMP/$TARBALL" -C "$STAGE" --strip-components=1
[ -f "$STAGE/dist/bin/oloop.js" ] || fail "tarball is missing dist/bin/oloop.js"

echo "smoke-testing the staged CLI"
if ! node "$STAGE/dist/bin/oloop.js" --version >/dev/null 2>&1; then
  rm -rf "$STAGE"
  fail "the staged CLI failed its smoke test. Your existing oloop install was not touched."
fi

# Swap: move the old tree aside, promote the staged tree, relink the command.
rm -rf "$OLD"
if [ -d "$INSTALL_DIR/package" ]; then mv "$INSTALL_DIR/package" "$OLD"; fi
if ! mv "$STAGE" "$INSTALL_DIR/package"; then
  if [ -d "$OLD" ]; then mv "$OLD" "$INSTALL_DIR/package"; fi
  fail "could not promote the staged install. Your existing oloop install was restored."
fi
mkdir -p "$INSTALL_DIR/bin"
chmod +x "$INSTALL_DIR/package/dist/bin/oloop.js"
ln -sfn "$INSTALL_DIR/package/dist/bin/oloop.js" "$INSTALL_DIR/bin/oloop"
rm -rf "$OLD"

echo ""
echo "installed oloop $VERSION (bundled; no package manager needed)"
echo "  location: $INSTALL_DIR"
echo "  sha256:   $ACTUAL"
echo "  command:  $INSTALL_DIR/bin/oloop"
echo ""
echo "Add this one line to your shell yourself (oloop never edits your rc files):"
printf '  export PATH="%s/bin:$PATH"\n' "$INSTALL_DIR"
echo ""
echo "Remove oloop completely (it touches nothing else):"
echo "  rm -rf $INSTALL_DIR"
echo ""

# --- optional reservation redemption (requirement 20) ----------------------

if [ -n "$CLAIM_ID" ]; then
  "$INSTALL_DIR/bin/oloop" hub claim --claim-id "$CLAIM_ID" --wait
  echo ""
  echo "Next: \`oloop host start\`. It sets up a free handle on first use."
else
  echo "Next: \`oloop host start\`. It sets up a free handle on first use."
fi
