#!/usr/bin/env sh
# bRRAIn installer + in-place upgrader.
#
#   curl -fsSL https://install.brrain.io | sh                  # install or upgrade
#   curl -fsSL https://install.brrain.io | sh -s -- --check    # report, change nothing
#   curl -fsSL https://install.brrain.io | sh -s -- --version 1.2.75
#
# This script is the ONLY installer. `deploy/docker/pod-bootstrap.sh`, which
# every RunPod pod curls at boot, is a thin shim that calls this with
# `--mode=pod`. Keeping one implementation means the path customers run on
# their own hardware is the same path every hosted pod runs, exercised by
# every deploy rather than by RunPod alone.
#
# WHAT CHANGED, AND WHY (2026-09-02)
# ----------------------------------
# This script used to download a per-OS binary from
# github.com/Qosil/bRRAIn/releases. That has been unreachable since
# 2026-05-10, when the source repository was made private under the standing
# directive "I do not want to use GitHub for deployments, I only want to use
# our own systems." The GitHub API returns 404 to anonymous callers, so the
# installer could not have worked from that date. It now resolves everything
# through the artifact manifest on bRRAIn-controlled object storage.
#
# It also installed a bare binary and told the operator to run two more
# commands. It now performs the whole install — runtime, artifact, model,
# vault, service — so that one command line is genuinely one command line.
#
# IDEMPOTENT. Re-running is the upgrade path: if the installed version
# already matches the target, it exits without touching anything. There is
# no background updater and nothing self-updates; per the [Eve-oc] learning
# (OpenClaw auto-updated between sessions and broke the service) an upgrade
# only ever happens because a human invoked it.
#
# ATOMIC + REVERSIBLE. The artifact is staged in full, verified against the
# manifest digest, and only then swapped in one `mv` per top-level path. The
# outgoing files move to ${INSTALL_ROOT}/.previous, so a failed upgrade is
# recoverable with `brrain upgrade --rollback` without a re-download.

set -eu

# ─── Defaults ────────────────────────────────────────────────────────────────
MANIFEST_BASE="${BRRAIN_MANIFEST_BASE:-https://brrain-deployments.fsn1.your-objectstorage.com/brain-images}"
BOOTSTRAP_BASE="${BRRAIN_BOOTSTRAP_BASE_URL:-https://brrain-deployments.fsn1.your-objectstorage.com/bootstrap}"
INSTALL_ROOT="${BRRAIN_INSTALL_ROOT:-/opt/brrain}"
VAULT_ROOT="${BRRAIN_VAULT_ROOT:-/var/lib/brrain/vault}"
MODEL_NAME="${BRRAIN_HANDLER_MODEL:-brrain-handler:v10}"
LISTEN_ADDR="${BRRAIN_LISTEN_ADDR:-:7843}"

MODE=host
TARGET_VERSION=""
CHECK_ONLY=0
FORCE=0

usage() {
    cat <<'EOF'
bRRAIn installer + in-place upgrader (Linux).

USAGE:
  curl -fsSL https://install.brrain.io | sh
  curl -fsSL https://install.brrain.io | sh -s -- [FLAGS]

FLAGS:
  --version V   install/upgrade to exactly V (default: current stable)
  --check       report installed vs available, then exit
  --mode M      host (default) | pod
  --force       reinstall even when already at the target version
  -h, --help    this help

ENVIRONMENT:
  BRRAIN_INSTALL_ROOT   install prefix           (default /opt/brrain)
  BRRAIN_VAULT_ROOT     vault location           (default /var/lib/brrain/vault)
  BRRAIN_MANIFEST_BASE  artifact manifest origin
  BRRAIN_LISTEN_ADDR    listen address           (default :7843)

Re-run this command to upgrade. Nothing updates on its own.
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
        --version) TARGET_VERSION="$2"; shift 2 ;;
        --version=*) TARGET_VERSION="${1#*=}"; shift ;;
        --mode) MODE="$2"; shift 2 ;;
        --mode=*) MODE="${1#*=}"; shift ;;
        --check) CHECK_ONLY=1; shift ;;
        --force) FORCE=1; shift ;;
        -h|--help) usage; exit 0 ;;
        # pod-bootstrap.sh historically passed a bare `serve` argument
        # through to the entrypoint. Accept and ignore it so an older pod
        # whose dockerArgs still carry it does not fail to boot.
        serve) shift ;;
        *) printf 'install: unknown flag: %s\n' "$1" >&2; usage >&2; exit 2 ;;
    esac
done

case "$MODE" in
    host|pod) ;;
    *) printf 'install: --mode must be host or pod (got %s)\n' "$MODE" >&2; exit 2 ;;
esac

info() { printf '[brrain-install] %s\n' "$*"; }
err()  { printf '[brrain-install] %s\n' "$*" >&2; }
die()  { err "FATAL: $*"; exit 1; }

# ─── Platform gate ───────────────────────────────────────────────────────────
# The artifact is a single linux/amd64 tarball carrying the brrain binary,
# the handler Modelfile, the GGUF and the continuity templates. It is not a
# per-OS build matrix. Refuse other platforms outright rather than
# downloading five gigabytes that cannot execute.
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
    x86_64|amd64) ARCH=amd64 ;;
    arm64|aarch64) ARCH=arm64 ;;
    *) ARCH="$ARCH_RAW" ;;
esac
if [ "$OS" != linux ] || [ "$ARCH" != amd64 ]; then
    err "unsupported platform: ${OS}/${ARCH}"
    err "  bRRAIn currently publishes a linux/amd64 artifact only."
    err "  macOS and arm64 hosts are not yet served by this installer."
    exit 1
fi

need_root() {
    if [ "$(id -u)" -ne 0 ]; then
        die "this installer writes to ${INSTALL_ROOT} and manages a system service; re-run as root (or with sudo)"
    fi
}
need_root

for c in curl tar; do
    command -v "$c" >/dev/null 2>&1 || die "missing required command: $c"
done
if command -v sha256sum >/dev/null 2>&1; then
    SHA_CHECK="sha256sum -c -"
elif command -v shasum >/dev/null 2>&1; then
    SHA_CHECK="shasum -a 256 -c -"
else
    die "neither sha256sum nor shasum available"
fi

# ─── Resolve the manifest ────────────────────────────────────────────────────
# One file is the source of truth for version, URL and digest. See
# deploy/install/publish-manifest.sh.
#
# POD MODE HONOURS THE PER-POD PIN. brrain-web injects BRRAIN_ARTIFACT_URL at
# deploy time to choose the version a given pod runs; that is the existing
# provisioning contract and this script must not override it. So in pod mode
# the target is derived from that URL rather than from latest.json, which also
# means the download is now digest-verified against the published manifest —
# the legacy entrypoint only verified when someone remembered to set
# BRRAIN_ARTIFACT_SHA256, and it was usually unset.
if [ -z "$TARGET_VERSION" ] && [ "$MODE" = pod ] && [ -n "${BRRAIN_ARTIFACT_URL:-}" ]; then
    DERIVED="$(printf '%s' "$BRRAIN_ARTIFACT_URL" | sed -n 's|.*/brrain-handler-\([0-9][0-9.]*\)\.tar\.gz$|\1|p')"
    if [ -n "$DERIVED" ]; then
        TARGET_VERSION="$DERIVED"
        info "pod pinned by BRRAIN_ARTIFACT_URL to ${TARGET_VERSION}"
    fi
fi

if [ -n "$TARGET_VERSION" ]; then
    MANIFEST_URL="${MANIFEST_BASE}/${TARGET_VERSION}.json"
else
    MANIFEST_URL="${MANIFEST_BASE}/latest.json"
fi

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT INT TERM

info "resolving ${MANIFEST_URL}"
if ! curl -fsSL --retry 3 --retry-delay 3 --max-time 60 -o "$WORK/manifest.json" "$MANIFEST_URL"; then
    err "could not fetch the artifact manifest at ${MANIFEST_URL}"
    if [ -n "$TARGET_VERSION" ]; then
        err "  version ${TARGET_VERSION} may not be published; see ${MANIFEST_BASE}/latest.json"
    fi
    exit 1
fi

# Field extraction without a JSON parser — the manifest is machine-written
# with one value per line, and requiring python3/jq on a bare host would
# defeat the point of a one-line installer.
mval() { sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$WORK/manifest.json" | head -1; }
VERSION="$(mval version)"
ARTIFACT_URL="$(mval url)"
ARTIFACT_SHA="$(mval sha256)"
MIN_FROM="$(mval min_upgrade_from)"

[ -n "$VERSION" ]      || die "manifest has no version field"
[ -n "$ARTIFACT_URL" ] || die "manifest has no artifact url"
[ -n "$ARTIFACT_SHA" ] || die "manifest has no artifact sha256 — refusing to install unverifiable artifact"

# Prefer the stamp written by the last successful swap, exactly as
# upgrade.go's installedArtifactVersion does — the two must agree on what
# "installed" means or the pod-boot guard below can be defeated.
#
# Order matters for safety, not just tidiness. Asking the binary first looks
# equivalent, but a binary that cannot answer — truncated download, wrong
# arch, missing loader — yields an empty string, which reads as "nothing is
# installed" and would let a pod reinstall itself to the current stable on its
# next boot. That is the silent version change the guard below exists to
# prevent, so the stamp, which is a plain file and cannot fail to execute,
# is authoritative.
INSTALLED=""
if [ -f "${INSTALL_ROOT}/.version" ]; then
    INSTALLED="$(tr -d ' \t\r\n' < "${INSTALL_ROOT}/.version" 2>/dev/null || true)"
fi
if [ -z "$INSTALLED" ] && [ -x "${INSTALL_ROOT}/brrain" ]; then
    INSTALLED="$("${INSTALL_ROOT}/brrain" version 2>/dev/null | sed -n 's/^brrain \([0-9][^ ]*\).*/\1/p' | head -1 || true)"
fi
# An artifact on disk that can identify itself in neither way is still an
# install. Treat it as present-but-unknown so a pod boot leaves it alone
# rather than silently replacing it.
if [ -z "$INSTALLED" ] && [ -e "${INSTALL_ROOT}/brrain" ]; then
    INSTALLED="unknown"
fi

info "installed: ${INSTALLED:-none}    available: ${VERSION}"

if [ "$CHECK_ONLY" -eq 1 ]; then
    if [ "$INSTALLED" = "$VERSION" ]; then
        info "up to date"
    elif [ -z "$INSTALLED" ]; then
        info "not installed — run without --check to install ${VERSION}"
    else
        info "upgrade available: ${INSTALLED} -> ${VERSION}"
    fi
    exit 0
fi

if [ "$INSTALLED" = "$VERSION" ] && [ "$FORCE" -eq 0 ]; then
    info "already at ${VERSION}; nothing to do (use --force to reinstall)"
    exit 0
fi

# A POD BOOT MUST NEVER CHANGE VERSION BY ITSELF. pod-bootstrap.sh runs this
# script on every container start, including ordinary restarts. If an artifact
# is already present, coming back on a different version than the one that went
# down would be exactly the silent auto-update that broke OpenClaw between
# sessions ([Eve-oc], 2026-03-31) and the reason `brrain update` was built to
# require explicit consent. Pod upgrades happen through `brrain upgrade`, on
# purpose, or not at all.
if [ "$MODE" = pod ] && [ -n "$INSTALLED" ] && [ "$FORCE" -eq 0 ]; then
    info "pod already has ${INSTALLED} installed; leaving it alone (upgrade with: brrain upgrade)"
    exit 0
fi

# ─── Prerequisites ───────────────────────────────────────────────────────────
# zstd is required by Ollama's installer (its tarball is zstd-compressed);
# without it `ollama install` aborts with a bare "requires zstd" message.
MISSING=""
for t in curl tar gzip zstd; do
    command -v "$t" >/dev/null 2>&1 || MISSING="$MISSING $t"
done
if [ -n "$MISSING" ]; then
    info "installing system prerequisites:${MISSING}"
    if command -v apt-get >/dev/null 2>&1; then
        DEBIAN_FRONTEND=noninteractive apt-get update -qq
        # shellcheck disable=SC2086
        DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates $MISSING
    elif command -v dnf >/dev/null 2>&1; then
        # shellcheck disable=SC2086
        dnf install -y ca-certificates $MISSING
    else
        die "cannot install${MISSING} automatically on this distribution; install them and re-run"
    fi
fi

# ─── Ollama ──────────────────────────────────────────────────────────────────
if ! command -v ollama >/dev/null 2>&1; then
    info "installing Ollama"
    curl -fsSL https://ollama.com/install.sh | sh
    command -v ollama >/dev/null 2>&1 || die "Ollama install completed but the binary is not on PATH"
fi
info "Ollama present: $(ollama --version 2>&1 | head -1)"

# ─── Download + verify ───────────────────────────────────────────────────────
TARBALL="$WORK/artifact.tar.gz"
info "downloading ${VERSION} artifact (this is several GB)"
curl -fL --retry 3 --retry-delay 5 -o "$TARBALL" "$ARTIFACT_URL" \
    || die "artifact download failed from ${ARTIFACT_URL}"

info "verifying sha256"
printf '%s  %s\n' "$ARTIFACT_SHA" "$TARBALL" | $SHA_CHECK >/dev/null \
    || die "sha256 mismatch — artifact does not match the manifest; refusing to install"
info "sha256 verified"

# ─── Stage ───────────────────────────────────────────────────────────────────
# Extract in full before touching the live install, so a truncated download
# or a bad tarball can never leave a half-replaced runtime on disk.
STAGE="${INSTALL_ROOT}.staged"
rm -rf "$STAGE"
mkdir -p "$STAGE"
info "extracting to ${STAGE}"
tar -xzf "$TARBALL" -C "$STAGE" || die "extract failed"
[ -x "$STAGE/brrain" ] || [ -f "$STAGE/brrain" ] || die "artifact contains no brrain binary"
chmod +x "$STAGE/brrain"

# ─── Stop the service before the swap ────────────────────────────────────────
# On a host the systemd unit owns the process. In a pod the supervision loop
# in handler-entrypoint-thin.sh owns it and will restart it from the new
# binary on its own, so nothing is stopped here.
SERVICE_WAS_RUNNING=0
if [ "$MODE" = host ] && command -v systemctl >/dev/null 2>&1; then
    if systemctl is-active --quiet brrain 2>/dev/null; then
        SERVICE_WAS_RUNNING=1
        info "stopping brrain service for the swap"
        systemctl stop brrain
    fi
fi

# ─── Swap ────────────────────────────────────────────────────────────────────
# Move one top-level path at a time, keeping everything the tarball does not
# own. That distinction matters: ${INSTALL_ROOT}/extensions holds installed
# marketplace apps and ${INSTALL_ROOT}/ollama holds the model blob store.
# Replacing the whole directory would destroy both and is exactly what makes
# a pod recreate so expensive.
mkdir -p "$INSTALL_ROOT"
PREV="${INSTALL_ROOT}/.previous"
rm -rf "$PREV"
mkdir -p "$PREV"

for path in "$STAGE"/*; do
    [ -e "$path" ] || continue
    name="$(basename "$path")"
    if [ -e "${INSTALL_ROOT}/${name}" ]; then
        mv "${INSTALL_ROOT}/${name}" "${PREV}/${name}"
    fi
    # rename(2) over a path whose old inode is held open by a running
    # process is safe on Linux — the running binary keeps its inode and the
    # new file takes the name. This is why the swap needs no ETXTBSY dance.
    mv "$path" "${INSTALL_ROOT}/${name}"
done
rm -rf "$STAGE"
printf '%s\n' "$VERSION" > "${INSTALL_ROOT}/.version"
info "artifact ${VERSION} in place at ${INSTALL_ROOT}"

# ─── Register the handler model ──────────────────────────────────────────────
# Idempotent: skip when the model is already registered under this name and
# the Modelfile is unchanged since the last registration.
#
# NOT IN POD MODE. `ollama create` needs a running Ollama daemon, and inside a
# pod there is no systemd to have started one — handler-entrypoint-thin.sh
# starts and supervises it after this script returns. So the pod defers
# registration to the entrypoint, which does it once the daemon is up and
# guards it with the same idempotence check. Registering here would fail
# against a socket that does not exist yet.
MODELFILE="${INSTALL_ROOT}/handler/Modelfile"
if [ "$MODE" = pod ]; then
    info "pod mode: deferring model registration to the entrypoint (Ollama daemon not up yet)"
elif [ -f "$MODELFILE" ]; then
    MF_SUM="$(sha256sum "$MODELFILE" 2>/dev/null | awk '{print $1}' || true)"
    MF_STAMP="${INSTALL_ROOT}/.modelfile.sha256"
    NEED_REGISTER=1
    if ollama list 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$MODEL_NAME"; then
        if [ -f "$MF_STAMP" ] && [ "$(cat "$MF_STAMP")" = "$MF_SUM" ]; then
            NEED_REGISTER=0
        fi
    fi
    if [ "$NEED_REGISTER" -eq 1 ]; then
        info "registering ${MODEL_NAME}"
        ( cd "$(dirname "$MODELFILE")" && ollama create "$MODEL_NAME" -f "$MODELFILE" )
        printf '%s\n' "$MF_SUM" > "$MF_STAMP"
    else
        info "${MODEL_NAME} already registered and Modelfile unchanged"
    fi
    # Ollama keeps its own blob copy; the source GGUF is ~9 GB of dead weight.
    rm -f "${INSTALL_ROOT}/handler/brrain-handler-v10.gguf"
else
    info "no Modelfile in artifact; skipping model registration"
fi

# ─── Pod mode stops here ─────────────────────────────────────────────────────
# The caller (pod-bootstrap.sh) hands off to handler-entrypoint-thin.sh,
# which supervises Ollama and brrain inside the container.
if [ "$MODE" = pod ]; then
    # handler-entrypoint-thin.sh skips its own legacy first-boot install when
    # this sentinel is present. Touching it here means the artifact is placed
    # once, by the path that verifies its digest, instead of being downloaded
    # a second time by the unverified fallback.
    touch "${INSTALL_ROOT}/.installed"
    info "pod mode: artifact ready at ${VERSION}; returning to bootstrap for handoff"
    exit 0
fi

# ─── Vault ───────────────────────────────────────────────────────────────────
# Never re-initialise an existing vault. `brrain init` prints a recovery
# phrase exactly once, and running it over live data is unrecoverable.
if [ ! -d "$VAULT_ROOT" ] || [ -z "$(ls -A "$VAULT_ROOT" 2>/dev/null || true)" ]; then
    info "provisioning a new vault at ${VAULT_ROOT}"
    mkdir -p "$(dirname "$VAULT_ROOT")"
    "${INSTALL_ROOT}/brrain" init "$VAULT_ROOT" || die "brrain init failed"
    info ""
    info "  ^^^ RECORD THE RECOVERY PHRASE ABOVE. It is shown once and cannot be reissued."
    info ""
else
    info "existing vault at ${VAULT_ROOT} left untouched"
fi

# ─── Service ─────────────────────────────────────────────────────────────────
# systemd is the supervisor on a host: Restart=always means `brrain upgrade`
# only has to swap files and restart the unit.
if command -v systemctl >/dev/null 2>&1; then
    UNIT=/etc/systemd/system/brrain.service
    if [ ! -f "$UNIT" ]; then
        info "installing systemd unit"
        cat > "$UNIT" <<UNITEOF
[Unit]
Description=bRRAIn persistent AI memory
After=network-online.target ollama.service
Wants=network-online.target

[Service]
Type=simple
Environment=BRRAIN_VAULT_ROOT=${VAULT_ROOT}
Environment=BRRAIN_INSTALL_ROOT=${INSTALL_ROOT}
Environment=BRRAIN_HANDLER_MODEL=${MODEL_NAME}
Environment=BRRAIN_LISTEN_ADDR=${LISTEN_ADDR}
Environment=OLLAMA_KEEP_ALIVE=-1
ExecStart=${INSTALL_ROOT}/brrain serve
Restart=always
RestartSec=2
# The service owns its install prefix and its vault, nothing else.
ReadWritePaths=${INSTALL_ROOT} ${VAULT_ROOT}

[Install]
WantedBy=multi-user.target
UNITEOF
        systemctl daemon-reload
        systemctl enable brrain >/dev/null 2>&1 || true
    else
        systemctl daemon-reload
    fi

    info "starting brrain"
    systemctl restart brrain
    sleep 2
    if systemctl is-active --quiet brrain; then
        info "brrain is running"
    else
        err "brrain failed to start — recent log:"
        journalctl -u brrain -n 30 --no-pager >&2 || true
        err "the previous install is preserved at ${PREV}; roll back with: brrain upgrade --rollback"
        exit 1
    fi
elif [ "$SERVICE_WAS_RUNNING" -eq 1 ]; then
    err "systemd is not available; restart brrain yourself: ${INSTALL_ROOT}/brrain serve"
fi

# ─── Convenience symlink ─────────────────────────────────────────────────────
if [ -d /usr/local/bin ] && [ ! -e /usr/local/bin/brrain ]; then
    ln -sf "${INSTALL_ROOT}/brrain" /usr/local/bin/brrain
fi

cat <<EOF

  bRRAIn ${VERSION} installed.

    vault    ${VAULT_ROOT}
    service  systemctl status brrain
    address  http://localhost${LISTEN_ADDR}/version

  To upgrade later, run the same command again, or:

    brrain upgrade

  Nothing updates on its own — an upgrade only happens when you ask for one.

EOF
