#!/bin/sh
# ns-brain installer
# Usage: curl -fsSL https://brain.fightclub.pro/install.sh | sh
set -e

BASE_URL="https://brain.fightclub.pro/dl"

# The release keys, the same two internal/brain/releasekeys.go compiles into the binary:
# the current signing key and the next one, which takes over if the current one is lost or
# stolen. Every release's SHA256SUMS is signed by one of them.
#
# Be honest about what this buys on a FIRST install: this script comes from the same host as
# the binary, so whoever controls that host can hand you a script with their own key in it.
# A first install is trust-on-first-use unless you compare the fingerprint printed below with
# the one published in the repository and with your contract, or build from source. Upgrades
# after that are checked against the keys compiled into the binary you already trust.
CUR_ID="1d869a02c032edd0"
CUR_FPR="1d869a02c032edd0d21aa6b2f8f67b66128b724aacfaf22c0c8ff5601a28ca5e"
CUR_PEM="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA4PjFwFRaIYT3Wgw539aQL1WZXP5PQoXqMj+vvuOeTNs=
-----END PUBLIC KEY-----"
NEXT_ID="812e71286f35c9b1"
NEXT_FPR="812e71286f35c9b136898831a4760a8ef81525826fa764ad85faff556f57a7e7"
NEXT_PEM="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEABzz/7kAPDiK3R5n5jzBuwvxafWQj04alj3etlkGkT/U=
-----END PUBLIC KEY-----"
# The signature covers "version <v>" and the SHA-256 of SHA256SUMS under this label, so a
# release cannot be replayed under another version's name (internal/brain/releasesig.go).
MANIFEST_LABEL="ns-brain/release/manifest/v2"
RETIRE_LABEL="ns-brain/release/retire/v1"
RETIRE_HEADER="ns-brain release key retirement v1"
# Where ns-brain keeps this machine's record of retired release keys, under the home the
# account database names (never $HOME, which one variable moves): releasesig.go's
# ReleaseTrustPath. On Linux the database is read as the binary reads it, this file first and
# then getent.
TRUST_REL=".config/ns-brain/release-trust.json"
ACCOUNT_DB="/etc/passwd"

# Options, for `curl -fsSL https://brain.fightclub.pro/install.sh | sh -s -- <options>`:
#   --to <version>      install this version (or NS_BRAIN_VERSION=<version>)
#   --allow-downgrade   replace a newer installed ns-brain; only with --to
#   --user              install to ~/.local/bin, no sudo (what an agent should use)
#   --dir <path>        install into this directory (absolute), no sudo if it is writable
PIN_VERSION="${NS_BRAIN_VERSION:-}"
ALLOW_DOWNGRADE="${INSTALL_ALLOW_DOWNGRADE:-}"
OPT_DIR=""
while [ $# -gt 0 ]; do
  case "$1" in
    --to)
      [ $# -ge 2 ] || { echo "  x --to needs a version (yymmddNN)."; exit 1; }
      PIN_VERSION="$2"; shift 2 ;;
    --to=*) PIN_VERSION="${1#--to=}"; shift ;;
    --allow-downgrade) ALLOW_DOWNGRADE=1; shift ;;
    --user)
      [ -z "$OPT_DIR" ] || { echo "  x --user and --dir both say where to install; pass one."; exit 1; }
      [ -n "${HOME:-}" ] || { echo "  x --user needs HOME to be set."; exit 1; }
      OPT_DIR="$HOME/.local/bin"; shift ;;
    --dir|--dir=*)
      [ -z "$OPT_DIR" ] || { echo "  x --user and --dir both say where to install; pass one."; exit 1; }
      if [ "$1" = "--dir" ]; then
        [ $# -ge 2 ] || { echo "  x --dir needs a directory."; exit 1; }
        OPT_DIR="$2"; shift 2
      else
        OPT_DIR="${1#--dir=}"; shift
      fi
      case "$OPT_DIR" in
        /*) ;;
        *) echo "  x --dir must be an absolute path, and '$OPT_DIR' is not. Nothing was installed."; exit 1 ;;
      esac ;;
    *) echo "  x Unknown option '$1'. Options: --to <version>, --allow-downgrade (with --to), --user, --dir <path>."; exit 1 ;;
  esac
done
# The version a downgrade lands on must be one the person typed. latest-version.txt is not
# signed, so an origin that points it at an old release would otherwise get the downgrade
# through on nothing more than the flag.
if [ "$ALLOW_DOWNGRADE" = "1" ] && [ -z "$PIN_VERSION" ]; then
  echo "  x --allow-downgrade needs --to <version>: the version to go back to has to be the"
  echo "    one you chose, never whatever latest-version.txt says. Refusing."
  exit 1
fi

# Fail closed. This used to fall back to a hardcoded version, which went stale the moment
# the next release shipped: an installer that cannot reach the version file would silently
# put a client older than the schema most stores are on, and that client then refuses to
# open them. No answer is better than a wrong one.
VERSION="${PIN_VERSION:-$(curl -fsSL "https://brain.fightclub.pro/latest-version.txt" 2>/dev/null || true)}"
VERSION="$(printf '%s' "$VERSION" | tr -d '[:space:]')"
if [ -z "$VERSION" ]; then
  echo "  x Cannot reach https://brain.fightclub.pro/latest-version.txt to find out which"
  echo "    version to install, and refusing to guess: an old client will not open a newer"
  echo "    store. Check your connection, or set NS_BRAIN_VERSION=<version> to pin one."
  exit 1
fi
case "$VERSION" in
  [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]) ;;
  *) echo "  x '${VERSION}' is not a release version (yymmddNN). Refusing."; exit 1 ;;
esac

detect_platform() {
  OS=$(uname -s | tr '[:upper:]' '[:lower:]')
  ARCH=$(uname -m)

  case "$OS" in
    linux)  OS="linux" ;;
    darwin) OS="darwin" ;;
    mingw*|msys*|cygwin*) OS="windows" ;;
    *) echo "Unsupported OS: $OS"; exit 1 ;;
  esac

  case "$ARCH" in
    x86_64|amd64)  ARCH="amd64" ;;
    aarch64|arm64) ARCH="arm64" ;;
    armv7l|armv6l) ARCH="arm" ;;
    i386|i686)     ARCH="386" ;;
    *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
  esac

  echo "${OS}/${ARCH}"
}

# fetch URL FILE: files, never shell variables, because $(...) strips trailing newlines and
# the signature covers SHA256SUMS byte for byte.
fetch() {
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$2" "$1"
  else
    echo "  x Neither curl nor wget found"; exit 1
  fi
}

fail() {
  echo "  x $*"
  exit 1
}

# OpenSSL 3 verifies Ed25519 over raw bytes; LibreSSL and OpenSSL 1.x do not. Stock macOS
# puts LibreSSL first on PATH, so Homebrew's keg-only openssl@3 is looked for by path too.
find_openssl3() {
  for c in openssl /opt/homebrew/opt/openssl@3/bin/openssl /usr/local/opt/openssl@3/bin/openssl; do
    command -v "$c" >/dev/null 2>&1 || continue
    case "$("$c" version 2>/dev/null)" in
      OpenSSL\ [3-9].*) OPENSSL3="$c"; return 0 ;;
    esac
  done
  return 1
}

# The second independent path, for machines without OpenSSL 3: Python's cryptography package,
# when it is genuinely installed (it is not in the standard library). -I keeps the current
# directory off the import path, so a planted module beside the download cannot answer.
have_py_cryptography() {
  command -v python3 >/dev/null 2>&1 || return 1
  python3 -I -c 'from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey' >/dev/null 2>&1
}

# verify_ed25519 PEM_FILE SIG_B64 MSG_FILE, with the verifier pick_verifier chose.
verify_ed25519() {
  if [ "$VERIFIER" = openssl ]; then
    printf '%s' "$2" | "$OPENSSL3" base64 -d -A > "$WORK/v.sig" 2>/dev/null || return 1
    "$OPENSSL3" pkeyutl -verify -pubin -inkey "$1" -rawin -in "$3" -sigfile "$WORK/v.sig" >/dev/null 2>&1
  else
    python3 -I -c '
import base64, sys
from cryptography.hazmat.primitives.serialization import load_pem_public_key
key = load_pem_public_key(open(sys.argv[1], "rb").read())
key.verify(base64.b64decode(sys.argv[2], validate=True), open(sys.argv[3], "rb").read())
' "$1" "$2" "$3" >/dev/null 2>&1
  fi
}

# Position in the pinned order, current first; -1 for a key this installer does not pin.
pinned_index() {
  case "$1" in
    "$CUR_ID") echo 0 ;;
    "$NEXT_ID") echo 1 ;;
    *) echo -1 ;;
  esac
}

pinned_pem() {
  case "$1" in
    "$CUR_ID") printf '%s\n' "$CUR_PEM" ;;
    "$NEXT_ID") printf '%s\n' "$NEXT_PEM" ;;
  esac
}

# retire_field FILE NAME: the value of the one "NAME value" line.
retire_field() {
  sed -n "s/^[[:space:]]*$2[[:space:]]\{1,\}\(.*[^[:space:]]\)[[:space:]]*\$/\1/p" "$1"
}

# check_retirement FILE URL: judged as ParseRetirement judges it. A statement from a signer
# this installer does not pin is ignored, as a binary from before that rotation ignores it; one
# from a pinned signer that does not verify, or that retires a key AFTER its signer in the
# pinned order, refuses the install, because something altered it or a stolen current key is
# trying to retire its successor.
check_retirement() {
  tr -d '\r' < "$1" > "$1.clean"
  [ "$(sed -n '1{s/^[[:space:]]*//;s/[[:space:]]*$//;p;}' "$1.clean")" = "$RETIRE_HEADER" ] ||
    fail "$2 is not a key retirement statement. Refusing."
  for n in retired by sig; do
    [ "$(grep -c "^[[:space:]]*$n[[:space:]]" "$1.clean" || true)" = 1 ] ||
      fail "$2 is malformed (it needs exactly one '$n' line). Refusing."
  done
  R_ID=$(retire_field "$1.clean" retired)
  R_BY=$(retire_field "$1.clean" by)
  R_SIG=$(retire_field "$1.clean" sig)
  [ -n "$R_ID" ] && [ -n "$R_BY" ] && [ -n "$R_SIG" ] || fail "$2 is malformed. Refusing."
  BY_IDX=$(pinned_index "$R_BY")
  [ "$BY_IDX" -ge 0 ] || return 0
  if [ "$VERIFIER" != none ]; then
    printf '%s\nkey %s retired\n' "$RETIRE_LABEL" "$R_ID" > "$WORK/retire.msg"
    pinned_pem "$R_BY" > "$WORK/retire.pem"
    verify_ed25519 "$WORK/retire.pem" "$R_SIG" "$WORK/retire.msg" ||
      fail "$2 claims key ${R_BY} retired key ${R_ID}, and the signature does not verify. Something altered it in transit or at the origin. Refusing."
  fi
  R_IDX=$(pinned_index "$R_ID")
  if [ "$R_IDX" -ge 0 ] && [ "$R_IDX" -ge "$BY_IDX" ]; then
    fail "$2 has key ${R_BY} retiring key ${R_ID}, which comes after it in the pinned order. Only a successor retires a key. Refusing."
  fi
  RETIRED_IDS="${RETIRED_IDS}${R_ID} "
}

# account_home UID: the home the account database names. Exit 2 is an account no database
# knows, which has never had a record kept (the binary keeps that one in memory only); exit 1
# is a lookup that failed, after which the record may well exist and cannot be read.
account_home() {
  if [ "$OS" = darwin ]; then
    AH_LINE=$(/usr/bin/id -P "$1" 2>/dev/null) || return 1
    AH_HOME=$(printf '%s\n' "$AH_LINE" | head -n 1 | cut -d: -f9)
  else
    AH_KNOWN=0
    if [ -r "$ACCOUNT_DB" ]; then
      AH_KNOWN=1
      AH_HOME=$(awk -F: -v u="$1" '$3 == u { print $6; exit }' "$ACCOUNT_DB")
    fi
    if [ -z "${AH_HOME:-}" ]; then
      AH_GETENT=""
      for g in /usr/bin/getent /bin/getent /usr/sbin/getent; do
        [ -x "$g" ] && { AH_GETENT="$g"; break; }
      done
      if [ -z "$AH_GETENT" ]; then
        [ "$AH_KNOWN" = 1 ] && return 2
        return 1
      fi
      AH_RC=0
      AH_LINE=$(cd / && env -i "$AH_GETENT" passwd "$1" 2>/dev/null) || AH_RC=$?
      [ "$AH_RC" = 2 ] && return 2
      [ "$AH_RC" = 0 ] || return 1
      AH_HOME=$(printf '%s\n' "$AH_LINE" | head -n 1 | cut -d: -f6)
    fi
  fi
  case "$AH_HOME" in
    /*) printf '%s\n' "$AH_HOME" ;;
    *) return 1 ;;
  esac
}

# read_trust_record UID: adds the keys this machine recorded as retired for that account. The
# record is JSON the binary writes; the shell takes every id it names as retired (the map keys
# and every "retired" value), which can only add refusals, and refuses a record it cannot read
# or that is not the shape the binary writes, because skipping it would un-retire a key.
read_trust_record() {
  TR_RC=0
  TR_HOME=$(account_home "$1") || TR_RC=$?
  [ "$TR_RC" = 2 ] && return 0
  [ "$TR_RC" = 0 ] ||
    fail "Cannot look up the home directory of uid $1 in the account database, so this machine's record of retired release keys cannot be read. Refusing rather than skipping it: run the installer again once the lookup works, or install with 'ns-brain upgrade' from the client you have."
  TR_FILE="${TR_HOME}/${TRUST_REL}"
  [ -e "$TR_FILE" ] || [ -L "$TR_FILE" ] || return 0
  [ -r "$TR_FILE" ] && [ -f "$TR_FILE" ] ||
    fail "${TR_FILE} records retired release keys and cannot be read. Refusing rather than skipping it."
  TR_FLAT=$(tr -d '[:space:]' < "$TR_FILE")
  case "$TR_FLAT" in
    *'\'*) fail "${TR_FILE} contains escapes this installer cannot read safely. Refusing rather than skipping it." ;;
    '{'*'}') ;;
    *) fail "${TR_FILE} is corrupt. It records retired release keys, so it is not ignored: restore it, or delete it knowingly." ;;
  esac
  TR_IDS=$( { printf '%s' "$TR_FLAT" | grep -o '"[^"]*":{' | sed 's/^"\([^"]*\)":{$/\1/'
              printf '%s' "$TR_FLAT" | grep -o '"retired":"[^"]*"' | sed 's/^"retired":"\([^"]*\)"$/\1/'; } |
            grep -vx 'retired' || true)
  for id in $TR_IDS; do
    LOCAL_RETIRED="${LOCAL_RETIRED}${id} "
    TRUST_FILES="${TRUST_FILES}${id}=${TR_FILE} "
  done
}

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
  else
    fail "Neither sha256sum nor shasum found, so the download cannot be verified. Install one and rerun."
  fi
}

# version_of BINARY: the yymmddNN that `ns-brain version` prints, or nothing. Run from the
# work directory with the update check off, so it touches no project and no network.
version_of() {
  (cd "$WORK" && NS_BRAIN_NO_UPDATE_CHECK=1 "$1" version 2>/dev/null) |
    sed -n 's/^ns-brain \([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\)\( .*\)\{0,1\}$/\1/p' | head -n 1
}

main() {
  PLATFORM=$(detect_platform)
  OS="${PLATFORM%/*}"
  ARCH="${PLATFORM#*/}"

  BINARY="ns-brain-${OS}-${ARCH}"
  [ "$OS" = "windows" ] && BINARY="${BINARY}.exe"

  INSTALL_DIR="${OPT_DIR:-/usr/local/bin}"
  if [ -n "$OPT_DIR" ] && [ ! -d "$INSTALL_DIR" ]; then
    mkdir -p "$INSTALL_DIR" || fail "Could not create ${INSTALL_DIR}; nothing was installed."
  fi
  TARGET="${INSTALL_DIR}/ns-brain"

  echo ""
  echo "  ns-brain installer v${VERSION}"
  echo "  --------------------------------"
  echo "  Platform:  ${OS}/${ARCH}"
  echo "  Binary:    ${BINARY}"
  echo ""

  WORK=$(mktemp -d)
  trap 'rm -rf "$WORK"' EXIT
  BIN="$WORK/$BINARY"

  echo "  -> Downloading..."
  fetch "${BASE_URL}/${VERSION}/${BINARY}" "$BIN"

  # Verify before installing, and refuse when we cannot. A brain holds the reasoning behind
  # your decisions, and `ns-brain upgrade` refuses an unverified binary; an installer that
  # skipped a check whenever a file or a tool was missing would be the one path that installs
  # whatever it was handed.
  fetch "${BASE_URL}/${VERSION}/SHA256SUMS" "$WORK/SHA256SUMS" 2>/dev/null ||
    fail "Could not fetch the checksum file for ${VERSION}. Refusing to install an unverified binary."
  fetch "${BASE_URL}/${VERSION}/SHA256SUMS.sig" "$WORK/SHA256SUMS.sig" 2>/dev/null ||
    fail "Release ${VERSION} has no SHA256SUMS.sig. Every release is signed; refusing an unsigned one."
  [ -s "$WORK/SHA256SUMS" ] || fail "The checksum file for ${VERSION} is empty. Refusing."
  [ -s "$WORK/SHA256SUMS.sig" ] || fail "The signature for ${VERSION} is empty. Refusing."

  GOT=$(sha256_of "$BIN")
  WANT=$(grep " \*\{0,1\}${BINARY}\$" "$WORK/SHA256SUMS" | cut -d' ' -f1 || true)
  [ -n "$WANT" ] || fail "The checksum file for ${VERSION} does not list ${BINARY}. Refusing to install it."
  if [ "$GOT" != "$WANT" ]; then
    echo "  x Checksum mismatch for ${BINARY}."
    echo "    expected ${WANT}"
    echo "    got      ${GOT}"
    exit 1
  fi
  echo "  -> Checksum verified"

  SIG_KEY=$(sed -n 's/^key \([0-9a-f]*\)$/\1/p' "$WORK/SHA256SUMS.sig")
  SIG_B64=$(sed -n 's/^vsig \(.*\)$/\1/p' "$WORK/SHA256SUMS.sig")
  SIG_VERSION=$(sed -n 's/^version \([0-9]*\)$/\1/p' "$WORK/SHA256SUMS.sig")
  case "$SIG_KEY" in
    "$CUR_ID")  KEY_FPR="$CUR_FPR";  KEY_PEM="$CUR_PEM" ;;
    "$NEXT_ID") KEY_FPR="$NEXT_FPR"; KEY_PEM="$NEXT_PEM" ;;
    *) fail "SHA256SUMS.sig names key '${SIG_KEY}', which this installer does not pin. Refusing." ;;
  esac
  [ -n "$SIG_B64" ] || fail "SHA256SUMS.sig carries no version-bound signature. Refusing."
  [ "$SIG_VERSION" = "$VERSION" ] ||
    fail "SHA256SUMS.sig was signed as version '${SIG_VERSION}' and served as ${VERSION}: a release replayed under another name. Refusing."

  VERIFIER=none
  if find_openssl3; then
    VERIFIER=openssl
  elif have_py_cryptography; then
    VERIFIER=python
  else
    echo ""
    echo "  !! WARNING: neither OpenSSL 3 nor Python's cryptography package is installed, so the"
    echo "  !! only thing left to check the signatures is the downloaded binary itself. A hostile"
    echo "  !! binary reports success. That check is NOT independent."
    if [ "${INSTALL_ALLOW_SELF_VERIFY:-}" != "1" ]; then
      echo "  !! Install OpenSSL 3 (macOS: brew install openssl@3) and rerun, or set"
      echo "  !! INSTALL_ALLOW_SELF_VERIFY=1 to accept the self-check and compare the fingerprint"
      echo "  !! printed below with the one in the repository yourself."
      fail "No independent signature check is available. Refusing."
    fi
    echo "  !! INSTALL_ALLOW_SELF_VERIFY=1 is set: proceeding on the binary's own check. A"
    echo "  !! retirement statement is then honoured by the key it names, unverified, which can"
    echo "  !! only refuse."
    echo ""
  fi

  # Every retirement statement the release ships, the standing one and the release's own, is
  # judged as the binary judges it, and so is the record this machine keeps of the ones it has
  # seen: a re-install through this script must refuse whatever `ns-brain upgrade` would.
  RETIRED_IDS=" "
  RETIRE_ARGS=""
  N=0
  for SRC in "${BASE_URL}/retire.sig" "${BASE_URL}/${VERSION}/retire.sig"; do
    N=$((N + 1))
    F="$WORK/retire.$N.sig"
    if fetch "$SRC" "$F" 2>/dev/null && [ -s "$F" ]; then
      check_retirement "$F" "$SRC"
      RETIRE_ARGS="$RETIRE_ARGS --retire $F"
    fi
  done
  case "$RETIRED_IDS" in
    *" $SIG_KEY "*) fail "Release ${VERSION} is signed by key ${SIG_KEY}, which has been RETIRED. Treat the download as hostile." ;;
  esac
  LOCAL_RETIRED=" "
  TRUST_FILES=" "
  read_trust_record "$(id -u)"
  # Under sudo the person's own record counts too; reading another one can only add refusals.
  if [ "$(id -u)" = 0 ] && [ -n "${SUDO_UID:-}" ] && [ "$SUDO_UID" != 0 ]; then
    read_trust_record "$SUDO_UID"
  fi
  case "$LOCAL_RETIRED" in
    *" $SIG_KEY "*)
      TR_AT=$(printf '%s' "$TRUST_FILES" | tr ' ' '\n' | sed -n "s/^${SIG_KEY}=//p" | head -n 1)
      fail "Release ${VERSION} is signed by key ${SIG_KEY}, which this machine recorded as RETIRED (${TR_AT}). A retired key that still signs is a stolen key until shown otherwise: treat the download origin as compromised, stay on the version you have, and check the published key-compromise notice." ;;
  esac

  # First, a check that does not depend on the binary being installed: openssl, or Python's
  # cryptography, verifies the signature before that binary has run at all. Without either the
  # only checker left is the downloaded binary itself, whose own "ok" proves nothing if it is
  # hostile, so that case is refused unless the person opts in knowingly.
  printf '%s\nversion %s\nsha256 %s\n' "$MANIFEST_LABEL" "$VERSION" "$(sha256_of "$WORK/SHA256SUMS")" > "$WORK/msg"
  printf '%s\n' "$KEY_PEM" > "$WORK/key.pem"
  INDEPENDENT=0
  if [ "$VERIFIER" != none ]; then
    verify_ed25519 "$WORK/key.pem" "$SIG_B64" "$WORK/msg" ||
      fail "The signature on SHA256SUMS does not verify against release key ${SIG_KEY} for version ${VERSION}. Refusing."
    INDEPENDENT=1
    if [ "$VERIFIER" = openssl ]; then
      echo "  -> Signature verified by openssl (key ${SIG_KEY})"
    else
      echo "  -> Signature verified by python3 cryptography (key ${SIG_KEY})"
    fi
  fi

  chmod +x "$BIN"
  # shellcheck disable=SC2086
  NS_BRAIN_NO_UPDATE_CHECK=1 "$BIN" verify-release --sums "$WORK/SHA256SUMS" \
      --sig "$WORK/SHA256SUMS.sig" --version "$VERSION" --key "$KEY_FPR" --binary "$BIN" $RETIRE_ARGS >/dev/null ||
    fail "The release signature check failed. Refusing to install."
  echo "  -> Signature verified by the release binary"

  # Only now is the binary asked what it is: before the signature verified, its answer was
  # the word of whoever served it. A signed release whose binary names another version is a
  # build that does not match its own manifest, and is refused.
  GOT_VERSION=$(version_of "$BIN")
  [ "$GOT_VERSION" = "$VERSION" ] ||
    fail "The verified binary reports version '${GOT_VERSION}', not the ${VERSION} it was signed as. Refusing."
  echo "  -> Binary reports version ${GOT_VERSION}"

  # A signed release can still be an OLD one: a compromised origin can serve last year's
  # genuinely signed client under the latest name, or simply point latest-version.txt back
  # at it. Whatever ns-brain is already here, at the install target or first on PATH, is the
  # floor unless the person asks to go below it.
  for EXISTING in "$TARGET" "$(command -v ns-brain 2>/dev/null || true)"; do
    [ -n "$EXISTING" ] && [ -x "$EXISTING" ] || continue
    HAVE=$(version_of "$EXISTING")
    if [ -z "$HAVE" ]; then
      echo "  -> ${EXISTING} does not report a version this installer can read; not comparing"
      continue
    fi
    if [ "$VERSION" -lt "$HAVE" ]; then
      if [ "$ALLOW_DOWNGRADE" = "1" ]; then
        echo "  !! --allow-downgrade --to ${VERSION}: replacing ${HAVE} (${EXISTING}) with the older ${VERSION}"
      else
        fail "${EXISTING} is version ${HAVE}, newer than ${VERSION}. Refusing a downgrade: an origin that serves an old signed release under a new name gets exactly this. If you meant to go back: sh -s -- --to ${VERSION} --allow-downgrade"
      fi
    fi
  done
  echo ""
  echo "  Release key: ${SIG_KEY}"
  echo "  Fingerprint: ${KEY_FPR}"
  if [ "$INDEPENDENT" = "0" ]; then
    echo "  (no independent verifier was available, so only the downloaded binary checked its own"
    echo "   signature. Compare the fingerprint above with the one in the repository before trusting it.)"
  fi
  echo ""

  # Staged in the target directory and renamed over the old binary, as `ns-brain upgrade`
  # does: a mv from $WORK (often another filesystem) is a copy into the live path, and every
  # project's hooks exec it on each prompt. Under sudo the file is root's and 0755: a mv kept
  # the invoking user as owner, so anything later running as that user could replace a binary
  # that root and other accounts run, without a sudo prompt.
  STAGE="${INSTALL_DIR}/.ns-brain.new.$$"
  if [ -w "$INSTALL_DIR" ]; then
    if ! { cp "$BIN" "$STAGE" && chmod 0755 "$STAGE" && mv -f "$STAGE" "$TARGET"; }; then
      rm -f "$STAGE"
      fail "Could not install to ${TARGET}; nothing was replaced."
    fi
  else
    # A sudo prompt with nobody at a terminal (an agent running the one-liner) waits for a
    # password that never comes. Say what works instead, before asking.
    if [ -n "$OPT_DIR" ]; then
      fail "${INSTALL_DIR} is not writable by you. Pick a directory you can write, or use --user."
    fi
    if ! command -v sudo >/dev/null 2>&1; then
      fail "${INSTALL_DIR} needs root and sudo is not installed. Install for this account instead: curl -fsSL https://brain.fightclub.pro/install.sh | sh -s -- --user"
    fi
    if ! sudo -n true 2>/dev/null && ! (: </dev/tty) 2>/dev/null; then
      fail "${INSTALL_DIR} needs sudo and there is no terminal to type the password into. Install for this account instead: curl -fsSL https://brain.fightclub.pro/install.sh | sh -s -- --user"
    fi
    echo "  -> Installing to ${INSTALL_DIR} as root (requires sudo)..."
    if ! { sudo install -m 0755 -o root -g 0 "$BIN" "$STAGE" && sudo mv -f "$STAGE" "$TARGET"; }; then
      sudo rm -f "$STAGE" 2>/dev/null || true
      fail "Could not install to ${TARGET} with sudo; nothing was replaced."
    fi
  fi

  echo "  ok Installed to ${TARGET}"
  # Every hook calls plain `ns-brain`, so a directory off PATH installs a brain nothing runs.
  case ":${PATH}:" in
    *":${INSTALL_DIR}:"*) ;;
    *)
      echo ""
      echo "  !! ${INSTALL_DIR} is not on your PATH, and the agent hooks call plain ns-brain."
      echo "     Add it, for example: echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.profile" ;;
  esac
  echo ""
  echo "  Get started, inside a project:"
  echo "    ns-brain init                 # create the brain and wire the hooks"
  echo "    ns-brain bootstrap            # seed it from past sessions"
  echo "    ns-brain recall \"<topic>\"     # ask it something"
  echo ""
  echo "  Your memories never leave your machine. No account, no telemetry; the only"
  echo "  network call is a once-a-day version check (NS_BRAIN_NO_UPDATE_CHECK=1 turns it off)."
  echo ""
}

main
