#!/bin/sh
# ElysiOS installer.
#
#   curl -fsSL https://get.elysios.ai | sh
#
# ORDER OF OPERATIONS, and why it is this order. Canon (ELYSIOS.md:752) requires
# that install "verifies package/signature/provenance BEFORE mutation" and that
# unsupported environments "fail before mutation":
#
#   1. fetch the update manifest   - control plane first, GitHub Releases second
#   2. refuse an unsigned release  - known from the manifest, so it costs nothing
#   3. download the package        - into a temp dir; this is not mutation
#   4. verify SHA-256              - integrity
#   5. check the supported-host matrix, USING THE CODE FROM THE PACKAGE
#   6. verify the release signature - authenticity
#   7. install atomically          - the first and only step that mutates
#
# Step 4 deliberately runs the matrix check out of the downloaded package rather
# than fetching it from the repository. The repository is private, so a source
# fetch would need credentials the installer has no business holding, and it
# would also mean checking the host with different code than the one being
# installed.
#
# POSIX sh on purpose: must run on a stock Ubuntu LTS image and on macOS with
# nothing installed first.

set -eu

# --- configuration ----------------------------------------------------------
# PROVISIONAL HOSTNAME. `api.elysios.ai` already denotes the IAM-protected
# hosted tenant API (deploy/cloudrun/tests/test_deploy_contract.py:993), so the
# public control plane takes a distinct name rather than colliding with it.
# Collapsing the two later costs one DNS record; colliding now would cost a
# redesign. Founder gate 4.
CONTROL_PLANE="${ELYSIOS_CONTROL_PLANE:-https://control.elysios.ai}"
REPO="${ELYSIOS_REPO:-ElysiOS-Aurea/aurea-core}"
RING="${ELYSIOS_RING:-fleet}"
TENANT="${ELYSIOS_TENANT:-ElysiOS}"

# The repository is private, so release assets require a token. The control
# plane issues a short-lived, read-only, per-licence one from POST
# /v1/install-token (Phase 3). Until that endpoint exists, a token may be
# supplied directly for testing.
INSTALL_TOKEN="${ELYSIOS_INSTALL_TOKEN:-}"
LICENCE="${ELYSIOS_LICENCE:-}"

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

need() { command -v "$1" >/dev/null 2>&1 || die "this installer needs '$1' and it is not on PATH"; }
need curl
need python3
need tar

WORK="$(mktemp -d)"
cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT INT TERM

fetch() { # fetch <url> <dest>
  if [ -n "$INSTALL_TOKEN" ]; then
    curl -fsSL -H "Authorization: Bearer $INSTALL_TOKEN" "$1" -o "$2"
  else
    curl -fsSL "$1" -o "$2"
  fi
}

# --- 0. obtain an install token, if the control plane can issue one ----------
if [ -z "$INSTALL_TOKEN" ] && [ -n "$LICENCE" ]; then
  say "Requesting an install token..."
  if curl -fsSL --max-time 15 -X POST "$CONTROL_PLANE/v1/install-token" \
       -H 'content-type: application/json' \
       -d "{\"licence\":\"$LICENCE\"}" -o "$WORK/token.json" 2>/dev/null; then
    INSTALL_TOKEN="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("token",""))' "$WORK/token.json")"
    [ -n "$INSTALL_TOKEN" ] && say "  issued"
  else
    say "  control plane did not issue a token; continuing without one"
  fi
fi

# --- 1. the update manifest -------------------------------------------------
MANIFEST="$WORK/latest.json"
say "Fetching the update manifest..."
if curl -fsSL --max-time 15 "$CONTROL_PLANE/v1/manifest" -o "$MANIFEST" 2>/dev/null; then
  say "  from the control plane"
elif fetch "https://github.com/$REPO/releases/latest/download/latest.json" "$MANIFEST" 2>/dev/null; then
  say "  control plane unreachable; served from GitHub Releases"
else
  die "no update manifest available from either the control plane or GitHub Releases"
fi

# --- 2. resolve this platform's artifact ------------------------------------
eval "$(python3 - "$MANIFEST" <<'PY'
import json, platform, shlex, sys
manifest = json.load(open(sys.argv[1]))
system = platform.system().lower()
machine = platform.machine().lower()
arch = {"x86_64": "x86_64", "amd64": "x86_64",
        "arm64": "arm64", "aarch64": "arm64"}.get(machine, machine)
key = f"{system}-{arch}"
assets = manifest.get("assets", {})
def emit(n, v): print(f"{n}={shlex.quote(str(v))}")
emit("PLATFORM", key)
emit("VERSION", manifest.get("version", ""))
emit("SIGNED", "true" if manifest.get("signed") else "false")
emit("SIGNATURE_URL", manifest.get("signature_url", ""))
emit("MANIFEST_URL", manifest.get("manifest_url", ""))
emit("MIN_SUPPORTED", manifest.get("min_supported_version", "0.0.0"))
if key in assets:
    emit("ASSET_URL", assets[key]["url"])
    emit("ASSET_SHA", assets[key]["sha256"])
    emit("ASSET_NAME", assets[key]["artifact"])
    emit("HAVE_ASSET", "1")
else:
    emit("HAVE_ASSET", "0")
PY
)"

[ "$HAVE_ASSET" = "1" ] || die "this release names no artifact for '$PLATFORM'. A platform appears in the manifest only once it has a passing install receipt."

say "ElysiOS $VERSION for $PLATFORM"

# --- refuse an unsigned release before spending a download on it ------------
# Signed-ness is known from the manifest, so this costs nothing and saves the
# user a pointless fetch and unpack. Canon security boundary 8 (ELYSIOS.md:623)
# requires updates to be signed and verified before activation.
if [ "$SIGNED" != "true" ] || [ -z "$SIGNATURE_URL" ]; then
  say ""
  say "  This release is NOT SIGNED."
  say ""
  say "  A checksum would prove the download is not corrupted. It would not prove"
  say "  the release came from ElysiOS: whoever can serve you the file can serve"
  say "  a matching checksum beside it."
  say ""
  say "  Canon requires updates to be signed and verified before activation"
  say "  (ELYSIOS.md:623, security boundary 8), so this installer stops here."
  say "  Release signing is pending one founder decision: which authority signs"
  say "  in CI. The machinery exists and is unwired, not missing."
  die "refusing to install an unsigned release"
fi

# --- 3. download and verify integrity ---------------------------------------
say "Downloading $ASSET_NAME..."
fetch "$ASSET_URL" "$WORK/$ASSET_NAME" \
  || die "download failed. If the repository is private, supply ELYSIOS_LICENCE so the control plane can issue an install token, or set ELYSIOS_INSTALL_TOKEN."

say "Verifying SHA-256..."
ACTUAL="$(python3 -c '
import hashlib, sys
h = hashlib.sha256()
with open(sys.argv[1], "rb") as f:
    for chunk in iter(lambda: f.read(1 << 20), b""):
        h.update(chunk)
print(h.hexdigest())
' "$WORK/$ASSET_NAME")"
[ "$ACTUAL" = "$ASSET_SHA" ] || die "checksum mismatch. expected $ASSET_SHA, got $ACTUAL"
say "  sha256 ok"

# --- 4. supported host, checked with the package's own matrix ---------------
say "Checking this machine against the supported-hardware matrix..."
mkdir -p "$WORK/pkg"
tar -xzf "$WORK/$ASSET_NAME" -C "$WORK/pkg" || die "could not unpack the package"
PKG_SCRIPTS="$WORK/pkg/scripts"
[ -d "$PKG_SCRIPTS" ] || die "the package does not contain the installer"

# The package ships its own dependency payload under .elysios-runtime rather than
# expecting the host to already have PyYAML, cryptography, Pillow and the rest.
# The payload is a site-packages tree, NOT an interpreter, and it is built for one
# exact ABI (elysios-python-payload.json records cpython-312 with native
# .cpython-312-*.so modules), so it has to be paired with a host CPython of that
# same version. Running the installer on a bare python3 without this on the path
# fails with ModuleNotFoundError deep inside the package, which is what happens if
# you skip this block.
PKG_PAYLOAD="$WORK/pkg/.elysios-runtime/python-packages"
if [ -d "$PKG_PAYLOAD" ]; then
  PKG_PYTHONPATH="$PKG_SCRIPTS:$PKG_PAYLOAD"
else
  PKG_PYTHONPATH="$PKG_SCRIPTS"
fi

# Pick an interpreter matching the payload's ABI, and refuse a mismatch here
# rather than failing later with an obscure import error.
PAYLOAD_META="$PKG_PAYLOAD/elysios-python-payload.json"
PAYLOAD_PY=""
if [ -f "$PAYLOAD_META" ]; then
  PAYLOAD_PY="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("python",""), end="")' "$PAYLOAD_META" 2>/dev/null || true)"
fi
RUNNER=python3
if [ -n "$PAYLOAD_PY" ]; then
  if command -v "python$PAYLOAD_PY" >/dev/null 2>&1; then
    RUNNER="python$PAYLOAD_PY"
  else
    HOST_PY="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2], end="")')"
    if [ "$HOST_PY" != "$PAYLOAD_PY" ]; then
      die "this package needs CPython $PAYLOAD_PY (its dependency payload is built for that ABI); this machine offers $HOST_PY. Install python$PAYLOAD_PY and re-run."
    fi
  fi
fi
say "  interpreter: $RUNNER (payload ABI ${PAYLOAD_PY:-unknown})"

PYTHONPATH="$PKG_PYTHONPATH" "$RUNNER" - <<'PY' || die "this machine is not a supported ElysiOS host"
import sys
from elysios_install.matrix import probe_host, verify_host

probe = probe_host()
result = verify_host(probe)
print(f"  host: {probe.os_name}/{probe.arch} {probe.os_version} "
      f"{probe.ram_gb:.0f}GB RAM, {probe.cpu_count} CPU")
print(f"  matrix: {result.summary()}")
if not result.supported:
    print("")
    print("  ElysiOS refuses to install on an unsupported host, and there is no")
    print("  override. Supported tiers:")
    print("    Apple Silicon  macOS 14-26")
    print("    Intel Mac      macOS 13-26, 16GB or more")
    print("    VPS            Ubuntu 22.04 or 24.04 LTS, 4 vCPU, 16GB")
    sys.exit(1)
PY

# --- 5. authenticity, then install ------------------------------------------
# Unsigned releases were already refused above. The trust anchor ships inside the
# package, never alongside the release, so a forged release cannot present its
# own public key.
ANCHOR="${ELYSIOS_RELEASE_PUBLIC_KEY:-$WORK/pkg/deploy/config/release-signing.pub}"

[ -f "$ANCHOR" ] || die "no release trust anchor found. Expected it at deploy/config/release-signing.pub inside the package, or set ELYSIOS_RELEASE_PUBLIC_KEY."

say "Verifying the release signature..."
fetch "$SIGNATURE_URL" "$WORK/elysios.signature.json" || die "signature download failed"

say "Installing..."
PYTHONPATH="$PKG_PYTHONPATH" "$RUNNER" -m elysios_install install-bundle \
  --package "$WORK/$ASSET_NAME" \
  --signature "$WORK/elysios.signature.json" \
  --release-public-key "$ANCHOR" \
  --tenant "$TENANT" \
  --ring "$RING" \
  || die "signature verification or installation failed"

say ""
say "ElysiOS $VERSION installed."
say "The installer wrote an inspectable receipt; see docs/INSTALL.md."
