#!/bin/sh
# Matrix light-client installer (Phase 1 B5). ONE copy-paste command from the
# web page brings a machine online:
#
#   curl -fsSL <cloud>/install.sh | sh -s -- --token <t> --cloud <cloud>
#
# It installs a user-level Node runtime (only if the system Node is too old),
# unpacks the client bundle to ~/.matrix/versions/<ver>, points ~/.matrix/current
# at it, drops a `matrix` launcher on PATH, and hands the one-time token to
# `matrix onboard` (redeem → persist membership → discover Provider connections
# read-only → open the browser). Provider activation, public sharing and
# autostart are separate explicit actions. NO sudo, NO silent fallback.
set -eu

# ---- loud failure reporting -------------------------------------------------
STEP="starting"
install_transaction_active=0
install_committed=0
runtime_swap_active=0
runtime_had_previous=0
runtime_new_at_dest=0
runtime_phase=0
runtime_stage=""
runtime_next=""
runtime_prev=""
runtime_dest=""
tmpn=""
bundle_swap_active=0
version_had_previous=0
bundle_new_at_dest=0
bundle_phase=0
current_had_previous=0
current_previous_target=""
current_switched=0
current_switch_pending=0
install_stage=""
vnext=""
vprev=""
VDIR=""
current=""
current_next=""
tmpb=""
launcher_stage=""
launcher_prev=""
launcher_had_previous=0
launcher_switched=0
launcher_phase=0
LAUNCHER=""
rc_swap_active=0
rc_had_previous=0
rc_switched=0
rc_phase=0
rc_path=""
rc_stage=""
rc_prev=""
install_lock_held=0
install_lock_path=""
install_lock_owner_file=""
install_identity=""
install_identity_marker=".matrix-install-identity"

directory_owned_by_install() {
  owned_directory="$1"
  owned_marker="$owned_directory/$install_identity_marker"
  [ -d "$owned_directory" ] && [ ! -L "$owned_directory" ] && [ -f "$owned_marker" ] \
    || return 1
  owned_value=""
  IFS= read -r owned_value < "$owned_marker" || return 1
  [ "$owned_value" = "$install_identity" ]
}

file_owned_by_install() {
  owned_file="$1"
  [ -f "$owned_file" ] && [ ! -L "$owned_file" ] || return 1
  while IFS= read -r owned_line; do
    [ "$owned_line" = "# matrix-install-identity: $install_identity" ] && return 0
  done < "$owned_file"
  return 1
}

symlink_points_to() {
  owned_link="$1"
  expected_target="$2"
  [ -L "$owned_link" ] || return 1
  actual_target="$(readlink "$owned_link" 2>/dev/null)" || return 1
  [ "$actual_target" = "$expected_target" ]
}

release_install_lock() {
  [ "$install_lock_held" -eq 1 ] || return 0
  lock_owner=""
  if [ -f "$install_lock_owner_file" ]; then
    IFS= read -r lock_owner < "$install_lock_owner_file" || lock_owner=""
  fi
  if [ "$lock_owner" != "$install_identity" ]; then
    printf '[matrix-install] WARNING: install lock ownership changed; refusing to remove %s\n' "$install_lock_path" >&2
    install_lock_held=0
    return 1
  fi
  rm -f "$install_lock_owner_file" \
    || { printf '[matrix-install] WARNING: could not release install lock owner file\n' >&2; return 1; }
  rmdir "$install_lock_path" \
    || { printf '[matrix-install] WARNING: could not release install lock %s\n' "$install_lock_path" >&2; return 1; }
  install_lock_held=0
  return 0
}

acquire_install_lock() {
  install_lock_path="$MATRIX_HOME/.install.lock"
  install_lock_owner_file="$install_lock_path/owner"
  identity_seed="$(mktemp "${TMPDIR:-/tmp}/matrix-install-identity.XXXXXX")" \
    || err "could not create install transaction identity"
  install_identity="$$-${identity_seed##*.}"
  rm -f "$identity_seed" || err "could not finalize install transaction identity"
  if ! mkdir "$install_lock_path" 2>/dev/null; then
    err "another Matrix installer is already running for $MATRIX_HOME"
  fi
  if ! printf '%s\n' "$install_identity" > "$install_lock_owner_file"; then
    rmdir "$install_lock_path" 2>/dev/null || true
    err "could not record Matrix install lock ownership"
  fi
  install_lock_held=1
}

rollback_install() {
  rollback_label="${1:-failed install}"
  # Every path is either an installer-owned mktemp/stage name or an exact
  # destination established by this process. Keep going after one cleanup
  # failure so signal handlers still exit with their required 130/143 code.
  set +e
  rollback_failed=0

  # The phase is set before each external rename. The backup/stage paths then
  # reveal whether that rename completed even when a pending signal runs before
  # the following shell assignment.
  if [ "$rc_swap_active" -eq 1 ]; then
    if [ -n "$rc_prev" ] && { [ -e "$rc_prev" ] || [ -L "$rc_prev" ]; }; then
      if ! { [ -e "$rc_path" ] || [ -L "$rc_path" ]; } || file_owned_by_install "$rc_path"; then
        rm -f "$rc_path" || rollback_failed=1
        mv "$rc_prev" "$rc_path" || rollback_failed=1
      else
        rollback_failed=1
      fi
    elif [ "$rc_had_previous" -eq 0 ] && [ "$rc_phase" -ge 3 ] \
      && [ -n "$rc_stage" ] && ! { [ -e "$rc_stage" ] || [ -L "$rc_stage" ]; }; then
      if file_owned_by_install "$rc_path"; then
        rm -f "$rc_path" || rollback_failed=1
      elif [ -e "$rc_path" ] || [ -L "$rc_path" ]; then
        rollback_failed=1
      fi
    fi
    if [ -n "$rc_stage" ]; then
      rm -f "$rc_stage" || rollback_failed=1
    fi
  fi

  if [ "$launcher_phase" -gt 0 ]; then
    if [ -n "$launcher_prev" ] && { [ -e "$launcher_prev" ] || [ -L "$launcher_prev" ]; }; then
      if ! { [ -e "$LAUNCHER" ] || [ -L "$LAUNCHER" ]; } || file_owned_by_install "$LAUNCHER"; then
        rm -f "$LAUNCHER"
        mv "$launcher_prev" "$LAUNCHER" || rollback_failed=1
      else
        rollback_failed=1
      fi
    elif [ "$launcher_had_previous" -eq 0 ] && [ "$launcher_phase" -ge 3 ] \
      && [ -n "$launcher_stage" ] \
      && ! { [ -e "$launcher_stage" ] || [ -L "$launcher_stage" ]; }; then
      if file_owned_by_install "$LAUNCHER"; then
        rm -f "$LAUNCHER" || rollback_failed=1
      elif [ -e "$LAUNCHER" ] || [ -L "$LAUNCHER" ]; then
        rollback_failed=1
      fi
    fi
    if [ -n "$launcher_stage" ]; then
      rm -f "$launcher_stage" || rollback_failed=1
    fi
  fi

  if [ "$bundle_swap_active" -eq 1 ]; then
    if [ "$current_switch_pending" -eq 1 ] || [ "$current_switched" -eq 1 ]; then
      if symlink_points_to "$current" "$VDIR" && directory_owned_by_install "$VDIR"; then
        rm -f "$current" || rollback_failed=1
        if [ "$current_had_previous" -eq 1 ]; then
          ln -s "$current_previous_target" "$current" || rollback_failed=1
        fi
      elif [ "$current_switched" -eq 1 ]; then
        rollback_failed=1
      fi
    fi
    if [ -n "$current_next" ] && symlink_points_to "$current_next" "$VDIR"; then
      rm -f "$current_next" || rollback_failed=1
    fi

    if [ -n "$vprev" ] && { [ -e "$vprev" ] || [ -L "$vprev" ]; }; then
      if ! { [ -e "$VDIR" ] || [ -L "$VDIR" ]; } || directory_owned_by_install "$VDIR"; then
        rm -rf "$VDIR" || rollback_failed=1
        mv "$vprev" "$VDIR" || rollback_failed=1
      else
        rollback_failed=1
      fi
    elif [ "$version_had_previous" -eq 0 ] && [ "$bundle_phase" -ge 3 ] \
      && [ -n "$vnext" ] && ! { [ -e "$vnext" ] || [ -L "$vnext" ]; }; then
      if directory_owned_by_install "$VDIR"; then
        rm -rf "$VDIR" || rollback_failed=1
      elif [ -e "$VDIR" ] || [ -L "$VDIR" ]; then
        rollback_failed=1
      fi
    fi
    if [ -n "$install_stage" ]; then
      rm -rf "$install_stage" || rollback_failed=1
    fi
  fi
  if [ -n "$tmpb" ]; then
    rm -rf "$tmpb" || rollback_failed=1
  fi

  if [ "$runtime_swap_active" -eq 1 ]; then
    runtime_next_was_present=0
    if [ -n "$runtime_next" ] && { [ -e "$runtime_next" ] || [ -L "$runtime_next" ]; }; then
      runtime_next_was_present=1
      rm -rf "$runtime_next" || rollback_failed=1
    fi
    if [ -n "$runtime_prev" ] && { [ -e "$runtime_prev" ] || [ -L "$runtime_prev" ]; }; then
      if ! { [ -e "$runtime_dest" ] || [ -L "$runtime_dest" ]; } || directory_owned_by_install "$runtime_dest"; then
        rm -rf "$runtime_dest" || rollback_failed=1
        mv "$runtime_prev" "$runtime_dest" || rollback_failed=1
      else
        rollback_failed=1
      fi
    elif [ "$runtime_had_previous" -eq 0 ] && [ "$runtime_phase" -ge 3 ] \
      && [ -n "$runtime_next" ] && [ "$runtime_next_was_present" -eq 0 ]; then
      if directory_owned_by_install "$runtime_dest"; then
        rm -rf "$runtime_dest" || rollback_failed=1
      elif [ -e "$runtime_dest" ] || [ -L "$runtime_dest" ]; then
        rollback_failed=1
      fi
    fi
    if [ -n "$runtime_stage" ]; then
      rm -rf "$runtime_stage" || rollback_failed=1
    fi
  fi
  if [ -n "$tmpn" ]; then
    rm -rf "$tmpn" || rollback_failed=1
  fi
  install_transaction_active=0 runtime_swap_active=0 runtime_had_previous=0 \
    runtime_new_at_dest=0 bundle_swap_active=0 version_had_previous=0 \
    bundle_new_at_dest=0 current_had_previous=0 current_switched=0 \
    current_switch_pending=0 launcher_had_previous=0 launcher_switched=0 \
    rc_swap_active=0 rc_had_previous=0 rc_switched=0
  if [ "$rollback_failed" -ne 0 ]; then
    printf '[matrix-install] WARNING: %s rollback could not fully restore the previous install\n' "$rollback_label" >&2
  else
    printf '[matrix-install] %s rolled back\n' "$rollback_label" >&2
  fi
  set -e
}

commit_install() {
  # This assignment-only simple command is the commit boundary. POSIX shells
  # process a pending trap between commands, never halfway through one, so a
  # later cleanup error or signal can no longer trigger destructive rollback.
  install_transaction_active=0 install_committed=1 runtime_swap_active=0 \
    bundle_swap_active=0 current_switch_pending=0 current_switched=0 \
    launcher_switched=0 rc_swap_active=0 rc_switched=0

  set +e
  commit_cleanup_failed=0
  for identity_dir in "$runtime_dest" "$VDIR"; do
    [ -n "$identity_dir" ] || continue
    identity_marker="$identity_dir/$install_identity_marker"
    if [ -f "$identity_marker" ]; then
      marker_value=""
      IFS= read -r marker_value < "$identity_marker" || marker_value=""
      if [ "$marker_value" = "$install_identity" ]; then
        rm -f "$identity_marker" || commit_cleanup_failed=1
      fi
    fi
  done
  for cleanup_dir in \
    "$runtime_prev" "$runtime_stage" "$runtime_next" "$tmpn" \
    "$vprev" "$install_stage" "$tmpb"
  do
    [ -n "$cleanup_dir" ] || continue
    if [ -e "$cleanup_dir" ] || [ -L "$cleanup_dir" ]; then
      if ! rm -rf "$cleanup_dir"; then
        printf '[matrix-install] WARNING: committed install could not clean %s; remove it manually\n' "$cleanup_dir" >&2
        commit_cleanup_failed=1
      fi
    fi
  done
  for cleanup_file in "$current_next" "$launcher_prev" "$launcher_stage" "$rc_prev" "$rc_stage"; do
    [ -n "$cleanup_file" ] || continue
    if [ -e "$cleanup_file" ] || [ -L "$cleanup_file" ]; then
      if ! rm -f "$cleanup_file"; then
        printf '[matrix-install] WARNING: committed install could not clean %s; remove it manually\n' "$cleanup_file" >&2
        commit_cleanup_failed=1
      fi
    fi
  done
  if [ "$commit_cleanup_failed" -ne 0 ]; then
    printf '[matrix-install] WARNING: install is committed and will not be rolled back because backup cleanup failed\n' >&2
  fi
  set -e
}

on_exit() {
  code=$?
  if [ "$code" -ne 0 ]; then
    trap - INT TERM
    transaction_active=0
    if [ "$install_transaction_active" -eq 1 ]; then
      transaction_active=1
      rollback_install "failed install"
    fi
    printf '\n[matrix-install] FAILED during: %s (exit %s)\n' "$STEP" "$code" >&2
    if [ "$install_committed" -eq 0 ]; then
      printf '[matrix-install] nothing was launched. Fix the above and re-run the install command.\n' >&2
    else
      printf '[matrix-install] the verified client remains installed, but onboarding did not complete; re-run matrix onboard.\n' >&2
    fi
  fi
  release_install_lock || true
}
on_signal() {
  signal_name="$1"
  signal_code="$2"
  trap - INT TERM
  STEP="handling $signal_name"
  if [ "$install_transaction_active" -eq 1 ]; then
    rollback_install "interrupted install"
  else
    printf '[matrix-install] interrupted after the install commit; the verified client remains installed\n' >&2
  fi
  exit "$signal_code"
}
trap on_exit EXIT
trap 'on_signal INT 130' INT
trap 'on_signal TERM 143' TERM

log() { printf '[matrix-install] %s\n' "$*"; }
err() { printf '[matrix-install] ERROR: %s\n' "$*" >&2; exit 1; }

ensure_real_directory() {
  directory_path="$1"
  directory_label="$2"
  if [ -L "$directory_path" ]; then
    err "$directory_label must be a real, non-symlink directory: $directory_path"
  fi
  if [ -e "$directory_path" ]; then
    [ -d "$directory_path" ] \
      || err "$directory_label must be a real, non-symlink directory: $directory_path"
  else
    mkdir "$directory_path" \
      || err "could not create $directory_label directory: $directory_path"
  fi
  [ -d "$directory_path" ] && [ ! -L "$directory_path" ] \
    || err "$directory_label must be a real, non-symlink directory: $directory_path"
}

download() {
  # download <url> <out> — curl or wget, fail loud (no partial file left behind)
  _url="$1"; _out="$2"
  if command -v curl >/dev/null 2>&1; then
    curl -fSL --retry 2 "$_url" -o "$_out" || err "download failed: $_url"
  elif command -v wget >/dev/null 2>&1; then
    wget -qO "$_out" "$_url" || err "download failed: $_url"
  else
    err "need curl or wget to download $_url — install one and re-run"
  fi
}

# ---- args -------------------------------------------------------------------
TOKEN=""
TOKEN_STDIN=0
CLOUD=""
HEADLESS=0
SERVER=0
NO_AUTOSTART=0
EMAIL=""
PASSWORD_STDIN=0
while [ $# -gt 0 ]; do
  case "$1" in
    --token) TOKEN="${2:-}"; shift 2 ;;
    --token-stdin) TOKEN_STDIN=1; shift ;;
    --cloud) CLOUD="${2:-}"; shift 2 ;;
    --headless) HEADLESS=1; shift ;;
    --server) SERVER=1; shift ;;
    --no-autostart) NO_AUTOSTART=1; shift ;;
    --email) EMAIL="${2:-}"; shift 2 ;;
    --password-stdin) PASSWORD_STDIN=1; shift ;;
    *) err "unknown flag: $1 (usage: [--token-stdin | --token <安装码>] [--cloud <url>] [--headless --server] [--no-autostart])" ;;
  esac
done
[ "$TOKEN_STDIN" -eq 0 ] || [ -z "$TOKEN" ] || err "use only one of --token or --token-stdin"
if [ "$TOKEN_STDIN" -eq 1 ]; then
  IFS= read -r TOKEN || err "--token-stdin requires one installation-token line on stdin"
  [ "${#TOKEN}" -le 16384 ] || err "--token-stdin installation token is too long"
fi
# 零参数可跑 (装了再登录): cloud falls back to env then the public cloud; a
# missing token is fine INTERACTIVELY — `matrix onboard` opens the browser and
# asks for the 安装码. Headless still requires the token (onboard fails loud).
[ -n "$CLOUD" ] || CLOUD="${MATRIX_CLOUD:-https://matrix.hicode.com}"
[ -n "$TOKEN" ] || [ "$HEADLESS" -eq 0 ] || err "--token-stdin or --token <安装码> is required with --headless"
[ -n "$TOKEN" ] || log "no --token — after install, the browser will open for 注册/登录, then paste the 安装码 back here"
# strip a trailing slash from the cloud URL so "$CLOUD/path" never doubles it
CLOUD="${CLOUD%/}"

MATRIX_HOME="$HOME/.matrix"
ensure_real_directory "$MATRIX_HOME" "Matrix home"
acquire_install_lock
ensure_real_directory "$MATRIX_HOME/runtime" "Matrix runtime"
ensure_real_directory "$MATRIX_HOME/versions" "Matrix versions"
ensure_real_directory "$MATRIX_HOME/bin" "Matrix bin"
# Matrix needs the unflagged node:sqlite runtime. Pin a release that provides it
# and reject older system Node builds that fail the extracted CLI preflight.
NODE_VERSION="${MATRIX_NODE_VERSION:-v22.23.1}"
valid_node_version() {
  case "$1" in
    v*) _node_semver="${1#v}" ;;
    *) return 1 ;;
  esac
  _node_major="${_node_semver%%.*}"
  [ "$_node_major" != "$_node_semver" ] || return 1
  _node_rest="${_node_semver#*.}"
  _node_minor="${_node_rest%%.*}"
  [ "$_node_minor" != "$_node_rest" ] || return 1
  _node_patch="${_node_rest#*.}"
  case "$_node_patch" in *.*) return 1 ;; esac
  for _node_part in "$_node_major" "$_node_minor" "$_node_patch"; do
    case "$_node_part" in ''|*[!0-9]*) return 1 ;; esac
  done
  return 0
}
valid_node_version "$NODE_VERSION" \
  || err "MATRIX_NODE_VERSION must match v<major>.<minor>.<patch> (got: $NODE_VERSION)"

# ---- 1. Node runtime (>=22.23) ---------------------------------------------
STEP="checking Node runtime"
node_ok() {
  # exit 0 iff "$1" has the unflagged node:sqlite runtime Matrix needs.
  "$1" -e 'var v=process.versions.node.split(".").map(Number);process.exit((v[0]>22||(v[0]===22&&v[1]>=23))?0:1)' 2>/dev/null
}
NODE=""
if command -v node >/dev/null 2>&1 && node_ok node; then
  NODE="$(command -v node)"
  log "using system Node: $NODE ($("$NODE" --version 2>/dev/null))"
else
  STEP="downloading Node runtime"
  os="$(uname -s)"; arch="$(uname -m)"
  case "$os" in
    Darwin) case "$arch" in
        arm64) plat="darwin-arm64" ;;
        x86_64) plat="darwin-x64" ;;
        *) err "unsupported macOS architecture: $arch" ;;
      esac ;;
    Linux) case "$arch" in
        x86_64) plat="linux-x64" ;;
        aarch64|arm64) plat="linux-arm64" ;;
        *) err "unsupported Linux architecture: $arch" ;;
      esac ;;
    *) err "unsupported OS: $os (need Darwin or Linux)" ;;
  esac
  tarball="node-${NODE_VERSION}-${plat}.tar.gz"
  url="https://nodejs.org/dist/${NODE_VERSION}/${tarball}"
  log "system Node missing or < 22.23 — installing Node ${NODE_VERSION} for ${plat} (user-level, no sudo)"
  rtdir="$MATRIX_HOME/runtime"
  ensure_real_directory "$rtdir" "Matrix runtime"
  tmpn="$(mktemp -d "${TMPDIR:-/tmp}/matrix-node.XXXXXX")"
  download "$url" "$tmpn/$tarball"
  download "https://nodejs.org/dist/${NODE_VERSION}/SHASUMS256.txt" "$tmpn/SHASUMS256.txt"
  [ -x /usr/bin/awk ] || err "trusted checksum parser /usr/bin/awk is unavailable"
  node_expected_sha="$(
    /usr/bin/awk -v wanted="$tarball" '
      {
        if (NF != 2) next
        digest = $1
        name = $2
        sub(/^\*/, "", name)
        if (name == wanted && length(digest) == 64 && digest !~ /[^A-Fa-f0-9]/) {
          count += 1
          selected = tolower(digest)
        }
      }
      END {
        if (count != 1) exit 1
        print selected
      }
    ' "$tmpn/SHASUMS256.txt"
  )" || err "official Node checksum entry is missing or ambiguous for $tarball"
  case "$node_expected_sha" in
    ''|*[!a-f0-9]*) err "official Node checksum is invalid for $tarball" ;;
  esac
  [ "${#node_expected_sha}" -eq 64 ] \
    || err "official Node checksum is invalid for $tarball"
  if [ -x /usr/bin/shasum ]; then
    node_hash_line="$(/usr/bin/shasum -a 256 "$tmpn/$tarball")"
  elif [ -x /usr/bin/sha256sum ]; then
    node_hash_line="$(/usr/bin/sha256sum "$tmpn/$tarball")"
  elif [ -x /bin/sha256sum ]; then
    node_hash_line="$(/bin/sha256sum "$tmpn/$tarball")"
  else
    err "no trusted sha256 tool found (need /usr/bin/shasum or sha256sum)"
  fi
  node_actual_sha="${node_hash_line%% *}"
  [ -x /usr/bin/tr ] || err "trusted checksum normalizer /usr/bin/tr is unavailable"
  node_actual_sha="$(printf '%s' "$node_actual_sha" | /usr/bin/tr 'A-F' 'a-f')"
  case "$node_actual_sha" in
    ''|*[!a-f0-9]*) err "trusted sha256 tool returned an invalid digest for $tarball" ;;
  esac
  [ "${#node_actual_sha}" -eq 64 ] \
    || err "trusted sha256 tool returned an invalid digest for $tarball"
  [ "$node_actual_sha" = "$node_expected_sha" ] \
    || err "Node archive sha256 mismatch (got $node_actual_sha, expected $node_expected_sha)"
  log "Node archive sha256 verified against SHASUMS256.txt"
  install_transaction_active=1
  runtime_swap_active=1
  runtime_phase=1
  runtime_stage="$(mktemp -d "$rtdir/.node-stage.XXXXXX")"
  tar -xzf "$tmpn/$tarball" -C "$runtime_stage" || err "could not extract $tarball"
  runtime_candidate="$runtime_stage/node-${NODE_VERSION}-${plat}"
  runtime_candidate_node="$runtime_candidate/bin/node"
  [ -x "$runtime_candidate_node" ] || err "downloaded node binary not executable at $runtime_candidate_node"
  node_ok "$runtime_candidate_node" || err "downloaded node failed the >=22.5 version check"
  printf '%s\n' "$install_identity" > "$runtime_candidate/$install_identity_marker" \
    || err "could not bind the staged Node runtime to this install transaction"

  # The runtime is itself part of the working old installation. Keep it until
  # the downloaded candidate has been extracted and executed successfully, then
  # swap within one filesystem and retain `previous` until the new path passes
  # the same live check. A failed move/verification restores the old runtime.
  runtime_id="${runtime_stage##*.}"
  runtime_next="$rtdir/.node-next.$runtime_id"
  runtime_prev="$rtdir/.node-prev.$runtime_id"
  runtime_dest="$rtdir/node"
  if ! mv "$runtime_candidate" "$runtime_next"; then
    err "could not stage the verified Node runtime"
  fi
  runtime_had_previous=0
  if [ -e "$runtime_dest" ] || [ -L "$runtime_dest" ]; then
    runtime_had_previous=1
    runtime_phase=2
    if ! mv "$runtime_dest" "$runtime_prev"; then
      err "could not preserve the existing Node runtime"
    fi
  fi
  runtime_phase=3
  if ! mv "$runtime_next" "$runtime_dest"; then
    err "could not switch to the verified Node runtime"
  fi
  runtime_new_at_dest=1
  runtime_phase=4
  NODE="$runtime_dest/bin/node"
  if [ ! -x "$NODE" ] || ! node_ok "$NODE"; then
    err "installed Node runtime failed verification; the transaction will restore the previous runtime"
  fi
  rm -rf "$runtime_stage" "$tmpn"
  runtime_stage=""
  tmpn=""
  log "installed Node runtime at $NODE ($("$NODE" --version 2>/dev/null))"
fi

# ---- 2. client bundle -------------------------------------------------------
STEP="locating client bundle"
tmpb="$(mktemp -d "${TMPDIR:-/tmp}/matrix-bundle.XXXXXX")"
# Offline-first (mirrors matrix.ps1): the Standalone ZIP ships
# client-bundle.tar.gz next to this script, so prefer it and skip the network.
# Under `curl … | sh` $0 is the shell name with no path component — guard.
SCRIPT_DIR=""
case "$0" in
  */*) if [ -f "$0" ]; then SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"; fi ;;
esac
if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/client-bundle.tar.gz" ]; then
  [ -f "$SCRIPT_DIR/latest.json" ] \
    || err "offline bundle is missing latest.json; refusing an unverified install"
  cp "$SCRIPT_DIR/client-bundle.tar.gz" "$tmpb/client-bundle.tar.gz"
  cp "$SCRIPT_DIR/latest.json" "$tmpb/latest.json"
  log "using local client bundle (offline): $SCRIPT_DIR/client-bundle.tar.gz"
else
  STEP="downloading client bundle"
  # Metadata is mandatory and downloaded first. It supplies the exact safe
  # version and digest that must match the subsequently downloaded payload.
  download "$CLOUD/latest.json" "$tmpb/latest.json"
  download "$CLOUD/client-bundle.tar.gz" "$tmpb/client-bundle.tar.gz"
fi

# Parse metadata and hash the opaque archive with Node before tar sees any
# archive entry. VERSION is also the future directory basename, so reject
# everything outside the same semver-safe grammar used by matrix.ps1 and
# `matrix self-update`. The validated value is written to a private temp file
# rather than re-parsed by shell code.
STEP="verifying client bundle metadata and sha256"
verified_version_file="$tmpb/verified-version"
"$NODE" - "$tmpb/latest.json" "$tmpb/client-bundle.tar.gz" "$verified_version_file" <<'NODE' \
  || err "client bundle metadata/version/sha256 verification failed"
const fs = require('node:fs');
const crypto = require('node:crypto');
const [metadataPath, bundlePath, versionOut] = process.argv.slice(2);
let metadata;
try {
  metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
} catch (error) {
  console.error(`[matrix-install] invalid latest.json: ${error.message}`);
  process.exit(1);
}
const version = String(metadata?.version || '').trim();
const expectedSha = String(metadata?.sha256 || '').trim().toLowerCase();
if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) {
  console.error(`[matrix-install] unsafe latest.json version: ${version || '(missing)'}`);
  process.exit(1);
}
if (!/^[a-f0-9]{64}$/.test(expectedSha)) {
  console.error('[matrix-install] latest.json is missing a valid sha256');
  process.exit(1);
}
const actualSha = crypto.createHash('sha256').update(fs.readFileSync(bundlePath)).digest('hex');
if (actualSha !== expectedSha) {
  console.error(`[matrix-install] client bundle sha256 mismatch (got ${actualSha}, expected ${expectedSha})`);
  process.exit(1);
}
fs.writeFileSync(versionOut, version, { mode: 0o600 });
NODE
VERSION="$(cat "$verified_version_file")"
log "client bundle sha256 verified"

STEP="extracting client bundle"
mkdir -p "$tmpb/x"
tar -xzf "$tmpb/client-bundle.tar.gz" -C "$tmpb/x" || err "could not extract client-bundle.tar.gz"

STEP="validating client bundle"
# The co-published metadata, the archive VERSION marker, and the root package
# version must identify one exact release. This check runs before importing the
# bundled CLI and before any installed version/current path can be removed.
"$NODE" - "$VERSION" "$tmpb/x/VERSION" "$tmpb/x/package.json" <<'NODE' \
  || err "client bundle version markers do not match latest.json"
const fs = require('node:fs');
const [expected, versionPath, packagePath] = process.argv.slice(2);
let marker;
let packageVersion;
try {
  marker = fs.readFileSync(versionPath, 'utf8').trim();
} catch {
  console.error('[matrix-install] bad bundle: missing VERSION marker');
  process.exit(1);
}
try {
  packageVersion = String(JSON.parse(fs.readFileSync(packagePath, 'utf8'))?.version || '').trim();
} catch {
  console.error('[matrix-install] bad bundle: missing or invalid package.json');
  process.exit(1);
}
if (marker !== expected || packageVersion !== expected) {
  console.error(`[matrix-install] bundle version mismatch (latest ${expected}, VERSION ${marker || '(missing)'}, package ${packageVersion || '(missing)'})`);
  process.exit(1);
}
NODE
[ -f "$tmpb/x/services/control-plane/src/server.mjs" ] || err "bad bundle: missing services/control-plane/src/server.mjs"
[ -f "$tmpb/x/workers/matrix-worker/src/cli.mjs" ] || err "bad bundle: missing workers/matrix-worker/src/cli.mjs"
# Load the exact extracted CLI before deleting/replacing any installed version.
# `help` reaches the usage boundary without credentials, network, or side effects,
# while still importing the full Worker runtime dependency graph.
MATRIX_BUNDLE_ROOT="$tmpb/x" "$NODE" "$tmpb/x/workers/matrix-worker/src/cli.mjs" help >/dev/null \
  || err "bad bundle: Matrix CLI preflight failed before install"
log "client bundle CLI preflight passed"

STEP="installing bundle to ~/.matrix/versions/$VERSION"
versions_dir="$MATRIX_HOME/versions"
ensure_real_directory "$versions_dir" "Matrix versions"
printf '%s\n' "$install_identity" > "$tmpb/x/$install_identity_marker" \
  || err "could not bind the staged bundle to this install transaction"
VDIR="$versions_dir/$VERSION"
install_transaction_active=1
bundle_swap_active=1
bundle_phase=1
install_stage="$(mktemp -d "$versions_dir/.install-stage.XXXXXX")"
vnext="$install_stage/next"
vprev="$install_stage/previous"
if ! mv "$tmpb/x" "$vnext"; then
  err "could not stage verified bundle $VERSION"
fi

version_had_previous=0
if [ -e "$VDIR" ] || [ -L "$VDIR" ]; then
  version_had_previous=1
  bundle_phase=2
  if ! mv "$VDIR" "$vprev"; then
    err "could not preserve existing bundle version $VERSION"
  fi
fi
bundle_phase=3
if ! mv "$vnext" "$VDIR"; then
  err "could not switch staged bundle into $VDIR"
fi
bundle_new_at_dest=1
bundle_phase=4

# Build the replacement pointer before touching `current`, then rename it in
# the same directory. If pointer replacement fails, restore the prior same-
# version directory (when any) so the existing current symlink remains usable.
current="$MATRIX_HOME/current"
current_next="$MATRIX_HOME/.current.next.${install_stage##*.}"
rm -f "$current_next"
if [ -e "$current" ] && [ ! -L "$current" ]; then
  err "refusing to replace non-symlink $current"
fi
if [ -L "$current" ]; then
  current_previous_target="$(readlink "$current")" \
    || err "could not read the existing current symlink before replacement"
  current_had_previous=1
fi
if ! ln -s "$VDIR" "$current_next"; then
  err "could not stage current pointer"
fi
current_switch_pending=1
if ! "$NODE" -e 'require("node:fs").renameSync(process.argv[1], process.argv[2])' "$current_next" "$current"; then
  err "could not switch current install pointer"
fi
current_switched=1
rm -rf "$tmpb"
tmpb=""
log "installed bundle → $VDIR (current -> $VERSION)"

# ---- 3. launcher on PATH ----------------------------------------------------
STEP="writing launcher"
ensure_real_directory "$MATRIX_HOME/bin" "Matrix bin"
LAUNCHER="$MATRIX_HOME/bin/matrix"
launcher_phase=1
launcher_stage="$(mktemp "$MATRIX_HOME/bin/.matrix-launcher-stage.XXXXXX")"
launcher_prev="$MATRIX_HOME/bin/.matrix-launcher-prev.${launcher_stage##*.}"
cat > "$launcher_stage" <<EOF
#!/bin/sh
# Matrix CLI launcher (generated by install.sh). Runs the bundled worker CLI
# with MATRIX_BUNDLE_ROOT pinned to the current version.
# matrix-install-identity: $install_identity
export MATRIX_BUNDLE_ROOT="\$HOME/.matrix/current"
exec "$NODE" "\$HOME/.matrix/current/workers/matrix-worker/src/cli.mjs" "\$@"
EOF
chmod +x "$launcher_stage"
if [ -e "$LAUNCHER" ] || [ -L "$LAUNCHER" ]; then
  launcher_had_previous=1
  launcher_phase=2
  mv "$LAUNCHER" "$launcher_prev" || err "could not preserve the existing Matrix launcher"
fi
launcher_phase=3
if ! mv "$launcher_stage" "$LAUNCHER"; then
  err "could not switch to the staged Matrix launcher"
fi
launcher_switched=1
launcher_phase=4
log "wrote launcher $LAUNCHER"

STEP="updating PATH"
add_path_block() {
  rc_input="$1"
  rc_path="$("$NODE" - "$rc_input" <<'NODE'
const fs = require('node:fs');
const path = require('node:path');
const input = process.argv[2];
let target = input;
try {
  const entry = fs.lstatSync(input);
  if (entry.isSymbolicLink()) target = fs.realpathSync(input);
} catch (error) {
  if (error?.code !== 'ENOENT') {
    console.error(`[matrix-install] could not inspect shell rc file ${input}: ${error.message}`);
    process.exit(1);
  }
}
try {
  const targetEntry = fs.lstatSync(target);
  if (!targetEntry.isFile() || targetEntry.isSymbolicLink()) {
    console.error(`[matrix-install] shell rc target must be a regular file: ${target}`);
    process.exit(1);
  }
} catch (error) {
  if (error?.code !== 'ENOENT') {
    console.error(`[matrix-install] could not inspect shell rc target ${target}: ${error.message}`);
    process.exit(1);
  }
  const parent = fs.statSync(path.dirname(target));
  if (!parent.isDirectory()) {
    console.error(`[matrix-install] shell rc parent is not a directory: ${path.dirname(target)}`);
    process.exit(1);
  }
}
process.stdout.write(target);
NODE
  )" || err "could not resolve shell rc file safely"

  rc_swap_active=1
  rc_phase=0
  rc_stage="$(mktemp "${rc_path}.matrix-stage.XXXXXX")" \
    || err "could not create atomic PATH update beside $rc_path"
  rc_prev="${rc_path}.matrix-prev.${rc_stage##*.}"
  "$NODE" - "$rc_path" "$rc_stage" "$install_identity" <<'NODE' \
    || err "could not prepare complete Matrix PATH block"
const fs = require('node:fs');
const [sourcePath, stagePath, installIdentity] = process.argv.slice(2);
const begin = '# >>> matrix cli >>>';
const end = '# <<< matrix cli <<<';
const identityLine = `# matrix-install-identity: ${installIdentity}`;
const pathLine = 'case ":$PATH:" in *":$HOME/.matrix/bin:"*) ;; *) PATH="$HOME/.matrix/bin:$PATH" ;; esac';
const exportLine = 'export PATH';
let source = '';
let sourceMode = 0o600;
try {
  const stat = fs.lstatSync(sourcePath);
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('not a regular file');
  source = fs.readFileSync(sourcePath, 'utf8');
  sourceMode = stat.mode & 0o777;
} catch (error) {
  if (error?.code !== 'ENOENT') {
    console.error(`[matrix-install] could not read shell rc file ${sourcePath}: ${error.message}`);
    process.exit(1);
  }
}

const chunks = source.match(/[^\n]*(?:\n|$)/g)?.filter(Boolean) || [];
const lineAt = (index) => chunks[index].replace(/\n$/, '').replace(/\r$/, '');
const kept = [];
for (let index = 0; index < chunks.length; index += 1) {
  const line = lineAt(index);
  if (line === begin) {
    let matchingEnd = -1;
    for (let cursor = index + 1; cursor < chunks.length; cursor += 1) {
      if (lineAt(cursor) === end) {
        matchingEnd = cursor;
        break;
      }
      if (lineAt(cursor) === begin) break;
    }
    if (matchingEnd >= 0) {
      index = matchingEnd;
      continue;
    }
    while (index + 1 < chunks.length) {
      const next = lineAt(index + 1);
      if (next !== pathLine && next !== exportLine) break;
      index += 1;
    }
    continue;
  }
  if (line === end) continue;
  kept.push(chunks[index]);
}

let next = kept.join('');
if (next.length > 0 && !next.endsWith('\n')) next += '\n';
if (next.length > 0 && !next.endsWith('\n\n')) next += '\n';
next += `${begin}\n${identityLine}\n${pathLine}\n${exportLine}\n${end}\n`;
fs.writeFileSync(stagePath, next, { mode: sourceMode });
fs.chmodSync(stagePath, sourceMode);
NODE

  rc_phase=1
  rc_had_previous=0
  if [ -e "$rc_path" ] || [ -L "$rc_path" ]; then
    rc_had_previous=1
    rc_phase=2
    mv "$rc_path" "$rc_prev" || err "could not preserve shell rc file before PATH update"
  fi
  rc_phase=3
  mv "$rc_stage" "$rc_path" || err "could not atomically install complete Matrix PATH block"
  rc_switched=1
  rc_phase=4
  log "added ~/.matrix/bin to PATH in $rc_input"
}
shellname="$(basename "${SHELL:-/bin/sh}")"
case "$shellname" in
  zsh) rcfile="$HOME/.zshrc" ;;
  bash) rcfile="$HOME/.bashrc" ;;
  *) rcfile="$HOME/.profile" ;;
esac
add_path_block "$rcfile"
PATH="$HOME/.matrix/bin:$PATH"; export PATH

# ---- 4. onboard (redeem, enroll privately, discover read-only, open browser) --
STEP="onboarding"
set -- onboard --cloud "$CLOUD"
[ -n "$TOKEN" ] && set -- "$@" --token-stdin
[ "$HEADLESS" -eq 1 ] && set -- "$@" --headless
[ "$SERVER" -eq 1 ] && set -- "$@" --server
[ "$NO_AUTOSTART" -eq 1 ] && set -- "$@" --no-autostart
[ -n "$EMAIL" ] && set -- "$@" --email "$EMAIL"
[ "$PASSWORD_STDIN" -eq 1 ] && set -- "$@" --password-stdin
# Installation tokens are single-use credentials. Never echo the value or
# forward it into the Matrix child argv.
if [ -n "$TOKEN" ]; then
  shown="matrix onboard --cloud $CLOUD --token [REDACTED]"
  [ "$HEADLESS" -eq 0 ] || shown="$shown --headless"
  [ "$SERVER" -eq 0 ] || shown="$shown --server"
  [ "$NO_AUTOSTART" -eq 0 ] || shown="$shown --no-autostart"
  log "launching: $shown"
else
  log "launching: matrix onboard --cloud $CLOUD (interactive token entry)"
fi
commit_install
if [ -n "$TOKEN" ]; then
  # Keep the single-use installation credential out of the Matrix child argv.
  # (The original shell command may still be visible/history-retained when the
  # user explicitly supplied --token; interactive paste avoids that as well.)
  printf '%s\n' "$TOKEN" | "$LAUNCHER" "$@"
  exit $?
fi
"$LAUNCHER" "$@"
exit $?
