#!/bin/sh
# =============================================================================
# Srasta Platform — Quick Install
#
# Canonical source for the customer installer:
#     scripts/get-srasta.sh        ← owned by the installable platform repo
# The hosted copy deployed at https://get.srasta.ai lives with the public
# website in srasta-control-services.
#
# When editing installer behavior, update the hosted copy as part of the
# matching srasta-control-services website release.
# =============================================================================
#
# Default mode:
#   curl -fsSL https://get.srasta.ai | sh
#   - docker pulls registry.gitlab.com/gandiva/srasta-installer/installer:main
#   - extracts the Srasta Agent binary + the compose project from that image
#   - THE AGENT drives the install on this host: detect hardware → the validated
#     model → bring the whole stack up → prove it (G1/G2/G3) → stay resident for
#     day-2.  No wizard container, no host-serve watcher, no SSH.
#
# Join mode (multi-host, zero-SSH):
#   curl -fsSL https://get.srasta.ai | sh -s -- --join <bundle>
#   - run on each ADDITIONAL node; the control-plane minted <bundle>
#   - installs the agent, registers it against the control-plane in the bundle,
#     and converges the slice that node is assigned.  We never SSH into nodes —
#     each node pulls this script and enrols itself.
#
# Bundle mode (P3.2.2 of #165):
#   curl -fsSL https://get.srasta.ai | sh -s -- --bundle v1.0.0
#   - downloads the v1.0.0 release bundle from GitLab Releases
#   - cosign-verifies the signature (Sigstore-keyless)
#   - sha256-verifies the tarball
#   - extracts + runs the installer image at the digest pinned in the
#     bundle's release-manifest.json.  Cryptographically anchored install.
#
# The AGENT handles everything else: hardware detection, model resolution,
# service deployment, secrets management, and verification.
# =============================================================================
set -e

_SRASTA_INSTALL_COMPLETE=0
_SRASTA_AGENT_REPLACEMENT_PENDING=0
_srasta_install_interrupted() {
    echo "" >&2
    echo "ERROR: installation interrupted; Srasta was not handed over." >&2
    exit 130
}
trap '_srasta_install_interrupted' INT TERM HUP

_srasta_has_tty() {
    { [ -r /dev/tty ] && [ -w /dev/tty ] && stty -g < /dev/tty >/dev/null; } 2>/dev/null
}

# Read long secrets from the controlling terminal without echoing them. macOS
# canonical TTY input truncates lines around 1 KiB, while signed Srasta license
# JWTs can exceed that size. Temporarily disabling canonical mode removes that
# ceiling; the saved terminal state is restored on success or interruption.
_srasta_read_tty_secret() (
    _srasta_secret_prompt="$1"
    _srasta_tty_state="$(stty -g < /dev/tty 2>/dev/null || true)"

    if [ -z "${_srasta_tty_state}" ]; then
        printf '%s' "${_srasta_secret_prompt}" > /dev/tty
        IFS= read -r _srasta_secret_value < /dev/tty || true
        printf '%s' "${_srasta_secret_value}"
        exit 0
    fi

    _srasta_restore_secret_tty() {
        stty "${_srasta_tty_state}" < /dev/tty 2>/dev/null || true
    }
    trap '_srasta_restore_secret_tty' 0
    trap '_srasta_restore_secret_tty; exit 130' HUP INT TERM

    printf '%s' "${_srasta_secret_prompt}" > /dev/tty
    stty -echo -icanon min 1 time 0 < /dev/tty
    IFS= read -r _srasta_secret_value < /dev/tty || true
    _srasta_restore_secret_tty
    trap - 0 HUP INT TERM
    printf '\n' > /dev/tty
    printf '%s' "${_srasta_secret_value}"
)

_srasta_restore_agent_on_failure() {
    _srasta_exit_status=$?
    trap - 0
    if [ "${_SRASTA_AGENT_REPLACEMENT_PENDING}" = "1" ] \
        && [ "${_SRASTA_INSTALL_COMPLETE}" != "1" ]; then
        echo "Restoring the previous Srasta Agent after incomplete replacement..." >&2
        "${AGENT_BIN}" resume-service \
            --host-id "${_EXISTING_AGENT_HOST_ID}" \
            --platform-root "${STATE_HOST_DIR}" \
            </dev/null \
            || echo "WARNING: the previous Srasta Agent could not be restored automatically." >&2
    fi
    exit "${_srasta_exit_status}"
}

# SRASTA_IMAGE_TAG selects the image CHANNEL (main | dev | <branch>) for the
# installer AND every gandiva service image (docker-compose.yml resolves them via
# the same var) — `curl … | SRASTA_IMAGE_TAG=dev sh` tests the whole stack against
# the dev branch's freshly-built images, no dev->main promotion.
# NOTE: the env var MUST be prefixed to `sh`, not `curl` — `SRASTA_IMAGE_TAG=dev
# curl … | sh` only scopes the assignment to `curl` (a no-op there); the piped
# `sh` never sees it and silently falls back to `main`.
# (Bundle mode overrides INSTALLER_IMAGE with a pinned digest below, so channel
# only affects the unpinned default path.)
#
# L0 (docs/roadmap/platform-upgrade-and-entitlement-plan.md): customers were
# installing a moving channel, not a release, so there was no answer to "what
# version is this customer on." Default now resolves and pins to the real
# current release via license-server, unless the operator explicitly set
# SRASTA_IMAGE_TAG (dev/test/branch escape hatch, unchanged). Fails closed —
# a fresh install already hard-requires network for mandatory registration,
# so one more required call before that isn't a new risk class.
if [ -z "${SRASTA_IMAGE_TAG:-}" ]; then
    _srasta_release_url="${SRASTA_LICENSE_SERVER_URL:-https://licensing.srasta.ai}/pub/release/current"
    _srasta_release_current="$(curl -fsSL --max-time 10 "${_srasta_release_url}" 2>/dev/null || true)"
    _srasta_release_version="$(printf '%s' "${_srasta_release_current}" | tr -d '\n' | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
    if [ -z "${_srasta_release_version}" ]; then
        echo "Error: could not resolve the current Srasta release from ${_srasta_release_url}." >&2
        echo "       Set SRASTA_IMAGE_TAG=main (or a branch name) to install a moving channel instead," >&2
        echo "       or retry when licensing.srasta.ai is reachable." >&2
        exit 1
    fi
    SRASTA_IMAGE_TAG="v${_srasta_release_version}"
fi
INSTALLER_IMAGE="${SRASTA_INSTALLER_IMAGE:-registry.gitlab.com/gandiva/srasta-installer/installer:${SRASTA_IMAGE_TAG}}"
INSTALLER_PORT="${SRASTA_PORT:-17724}"
CONTAINER_NAME="srasta-installer"
BUNDLE_VERSION=""
JOIN_BUNDLE=""
SRASTA_GITLAB_URL="${SRASTA_GITLAB_URL:-https://gitlab.com/gandiva-tech/srasta}"

# Parse args
while [ $# -gt 0 ]; do
    case "$1" in
        --port)    INSTALLER_PORT="$2"; shift 2 ;;
        --image)   INSTALLER_IMAGE="$2"; shift 2 ;;
        --name)    CONTAINER_NAME="$2";  shift 2 ;;
        --bundle)  BUNDLE_VERSION="$2";  shift 2 ;;
        # --join <bundle>: this node JOINS an existing cluster (multi-host, zero-SSH).  It
        # installs the agent and registers against the control-plane in the bundle, then pulls
        # its slice — instead of doing a full local day-0 install.
        --join)    JOIN_BUNDLE="$2";     shift 2 ;;
        *)         echo "Unknown option: $1"; exit 1 ;;
    esac
done

echo ""
echo "  ┌─────────────────────────────────────┐"
echo "  │       Srasta Platform Installer      │"
echo "  └─────────────────────────────────────┘"
echo ""

# ── Preflight ────────────────────────────────────────────────────────────────

if ! command -v docker >/dev/null 2>&1; then
    echo "ERROR: Docker is not installed."
    echo "  Install Docker: https://docs.docker.com/get-docker/"
    exit 1
fi
if ! docker info >/dev/null 2>&1; then
    # Distinguish "daemon not running" from "user not in docker group" —
    # they look the same to Docker CLI but the fix is very different.
    # See #201 §3 (install bootstrap pre-flight checks).
    if [ -S /var/run/docker.sock ]; then
        echo "ERROR: Cannot talk to Docker daemon, but the socket exists at"
        echo "  /var/run/docker.sock.  Most likely your user is not in the"
        echo "  'docker' group."
        echo ""
        echo "  Fix:"
        echo "    sudo usermod -aG docker \$USER"
        echo "    # then log out and back in, OR run 'newgrp docker' in this shell"
        echo ""
        echo "  Verify with:  groups | grep docker"
    else
        echo "ERROR: Docker daemon is not running."
        echo "  Start Docker and try again:"
        echo "    sudo systemctl start docker  # Linux"
        echo "    open -a Docker                # macOS"
    fi
    exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
    echo "ERROR: Docker Compose v2 plugin is missing."
    echo "  Install: https://docs.docker.com/compose/install/"
    echo "  Verify with:  docker compose version"
    exit 1
fi
# (No SSH keypair pre-flight: multi-host is zero-SSH pull-enrolment — a worker joins with
# `curl get.srasta.ai | sh -s -- --join <bundle>`, the control plane never reaches into it.)
if command -v lsof >/dev/null 2>&1; then
    if lsof -i ":${INSTALLER_PORT}" >/dev/null 2>&1; then
        echo "ERROR: Port ${INSTALLER_PORT} is already in use."
        echo "  Use --port N to choose a different port."
        exit 1
    fi
fi

# ── Production warning (#291): nudge default-mode users toward verified bundles ─

if [ -z "$BUNDLE_VERSION" ]; then
    echo "  ┌──────────────────────────────────────────────────────────────────┐"
    echo "  │  ⚠ Default mode pulls installer:main by tag — no signature       │"
    echo "  │    verification.  For production / regulated installs, use:      │"
    echo "  │                                                                  │"
    echo "  │      curl -fsSL https://get.srasta.ai | sh -s -- --bundle vX.Y.Z │"
    echo "  │                                                                  │"
    echo "  │    Bundle mode cosign-verifies the release before extracting     │"
    echo "  │    and pins the installer image by sha256 digest.  See:          │"
    echo "  │      https://srasta.ai/security  /  docs/operations/             │"
    echo "  │      release-verification-runbook.md                             │"
    echo "  └──────────────────────────────────────────────────────────────────┘"
    echo ""
fi

# ── Bundle mode ──────────────────────────────────────────────────────────────

if [ -n "$BUNDLE_VERSION" ]; then
    echo "Bundle mode: ${BUNDLE_VERSION}"
    for cmd in curl tar sha256sum cosign jq; do
        if ! command -v "$cmd" >/dev/null 2>&1; then
            echo "ERROR: bundle mode requires '$cmd' (not found in PATH)."
            echo "  cosign:   brew install cosign  / github.com/sigstore/cosign"
            echo "  jq + tar: usually preinstalled or via your package manager"
            exit 1
        fi
    done

    WORKDIR="$(mktemp -d -t srasta-install.XXXXXX)"
    trap 'rm -rf "$WORKDIR"' EXIT

    BASE_URL="${SRASTA_GITLAB_URL}/-/releases/${BUNDLE_VERSION}/downloads"
    BUNDLE="srasta-${BUNDLE_VERSION}.tar.gz"

    echo "Fetching bundle from ${BASE_URL}/${BUNDLE} ..."
    curl -fsSL -o "${WORKDIR}/${BUNDLE}"          "${BASE_URL}/${BUNDLE}"
    curl -fsSL -o "${WORKDIR}/${BUNDLE}.sha256"   "${BASE_URL}/${BUNDLE}.sha256"
    curl -fsSL -o "${WORKDIR}/${BUNDLE}.sig"      "${BASE_URL}/${BUNDLE}.sig"

    echo "Verifying sha256 ..."
    ( cd "$WORKDIR" && sha256sum -c "${BUNDLE}.sha256" )

    echo "Verifying cosign signature (Sigstore keyless) ..."
    cosign verify-blob \
        --certificate-identity-regexp 'https://gitlab\.com/gandiva-tech/srasta//\.gitlab-ci\.yml@.+' \
        --certificate-oidc-issuer 'https://gitlab.com' \
        --bundle "${WORKDIR}/${BUNDLE}.sig" \
        "${WORKDIR}/${BUNDLE}" >/dev/null

    echo "Extracting ..."
    ( cd "$WORKDIR" && tar -xzf "${BUNDLE}" )

    EXTRACTED="$(find "$WORKDIR" -maxdepth 1 -type d -name 'srasta-*' | head -1)"
    if [ -z "$EXTRACTED" ]; then
        echo "ERROR: extracted bundle not found in $WORKDIR" >&2
        exit 1
    fi

    # Pull the installer by digest from the verified manifest.
    INSTALLER_DIGEST="$(jq -r '.images[] | select(.name | endswith("/installer")) | .digest' "${EXTRACTED}/release-manifest.json" | head -1)"
    INSTALLER_NAME="$(jq -r '.images[] | select(.name | endswith("/installer")) | .name' "${EXTRACTED}/release-manifest.json" | head -1)"
    if [ -z "$INSTALLER_DIGEST" ] || [ -z "$INSTALLER_NAME" ]; then
        echo "ERROR: installer image not found in bundle's release-manifest.json" >&2
        exit 1
    fi
    INSTALLER_IMAGE="${INSTALLER_NAME}@${INSTALLER_DIGEST}"
    echo "Cryptographically anchored installer: ${INSTALLER_IMAGE}"
fi

# The selected installer artifact is part of the runtime provenance contract.
# Export it unconditionally so a replacement install overwrites any local/dirty
# image reference persisted by an earlier hardware proof. The control-plane and
# Srasta-Agent services both run this image; allowing stale state to select it
# would mix channels and make a dev/main install try to pull srasta-local/*.
SRASTA_INSTALLER_IMAGE="${INSTALLER_IMAGE}"
export SRASTA_INSTALLER_IMAGE

# ── Funnel telemetry: emit pull-start event (#244) ──────────────────────────
#
# Anchored on a stable INSTALL_ID UUID per operator machine — generated
# once at ~/.srasta/install-id and reused on every install retry.  The
# wizard reads the same file so heartbeats from the deployed cluster
# carry the same install_id (powers the pull-start → wizard-launch →
# topology-applied → deploy-complete → license-activated funnel report).
#
# Event POST is intentionally best-effort: timeouts after 3s, ignores
# errors, never blocks the install path.  No customer data — just the
# UUID + version + OS arch (count + shape, never content; same
# privacy contract as the heartbeat path, see /api/telemetry/v1/heartbeat).
SRASTA_FUNNEL_ENDPOINT="${SRASTA_FUNNEL_ENDPOINT:-https://srasta.ai/api/funnel/event}"
SRASTA_INSTALL_ID_FILE="${HOME}/.srasta/install-id"

if [ ! -f "$SRASTA_INSTALL_ID_FILE" ]; then
    mkdir -p "$(dirname "$SRASTA_INSTALL_ID_FILE")" 2>/dev/null || true
    if command -v uuidgen >/dev/null 2>&1; then
        uuidgen | tr '[:upper:]' '[:lower:]' > "$SRASTA_INSTALL_ID_FILE" 2>/dev/null || true
    elif [ -r /proc/sys/kernel/random/uuid ]; then
        cat /proc/sys/kernel/random/uuid > "$SRASTA_INSTALL_ID_FILE" 2>/dev/null || true
    elif command -v python3 >/dev/null 2>&1; then
        python3 -c "import uuid; print(uuid.uuid4())" > "$SRASTA_INSTALL_ID_FILE" 2>/dev/null || true
    fi
fi
SRASTA_INSTALL_ID="$(tr -d '[:space:]' < "$SRASTA_INSTALL_ID_FILE" 2>/dev/null || true)"
if [ -n "$SRASTA_INSTALL_ID" ]; then
    export SRASTA_INSTALL_ID
fi

if [ -n "$SRASTA_INSTALL_ID" ] && command -v curl >/dev/null 2>&1; then
    SRASTA_OS="$(uname -s 2>/dev/null || echo unknown)"
    SRASTA_ARCH="$(uname -m 2>/dev/null || echo unknown)"
    SRASTA_FUNNEL_PAYLOAD=$(cat <<JSON
{"install_id":"${SRASTA_INSTALL_ID}","event_type":"pull-start","os":"${SRASTA_OS}","arch":"${SRASTA_ARCH}","payload":{"installer_image":"${INSTALLER_IMAGE}","bundle_version":"${BUNDLE_VERSION:-}"}}
JSON
)
    curl -sS -m 3 -X POST "$SRASTA_FUNNEL_ENDPOINT" \
        -H "content-type: application/json" \
        -d "$SRASTA_FUNNEL_PAYLOAD" >/dev/null 2>&1 || true
fi

# ── Pull + Start ─────────────────────────────────────────────────────────────

echo "Pulling installer image..."
docker pull "${INSTALLER_IMAGE}" || {
    # Pull failed — fall back to a locally-present image (a dev-built or already-
    # pulled installer image; e.g. SRASTA_INSTALLER_IMAGE=<local-tag> for testing a
    # locally-built installer without a registry round-trip).  Only abort if it's
    # absent locally too.
    if docker image inspect "${INSTALLER_IMAGE}" >/dev/null 2>&1; then
        echo "WARN: pull failed; using locally-present ${INSTALLER_IMAGE}."
    else
        echo "ERROR: Failed to pull ${INSTALLER_IMAGE} and it isn't present locally."
        echo "  Check your network or registry credentials."
        exit 1
    fi
}

# Stop existing container if any
docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true

echo "Starting installer on port ${INSTALLER_PORT}..."

# State directory — where the wizard writes installer_key, state.json,
# generated .env, etc.  On Linux this lives at /opt/srasta (the standard
# third-party-software path, survives operator $HOME wipes); on macOS we
# can't bind-mount /opt because Docker Desktop blocks it via File
# Sharing by default, so we fall back to ~/.local/share/srasta-installer
# which Docker Desktop allows out of the box.
#
# The container always sees its state at /opt/srasta — only the host
# path differs.  This keeps the wizard logic + bundled-compose paths
# consistent across operator OS.
if [ "$(uname -s)" = "Darwin" ]; then
    SRASTA_STATE_HOST_DIR="${SRASTA_STATE_DIR:-${HOME}/.local/share/srasta-installer}"
    SRASTA_TARGET_UID="$(id -u)"
    SRASTA_TARGET_GID="$(id -g)"
    export SRASTA_TARGET_UID
    export SRASTA_TARGET_GID
    mkdir -p "${SRASTA_STATE_HOST_DIR}"
    # Path congruence (single-host-local E2E, 2026-06-04): the wizard drives
    # `docker compose` over the mounted socket; compose resolves bind-mount
    # SOURCES relative to the compose project dir (= the container's STATE_DIR)
    # and hands ABSOLUTE paths to the HOST daemon.  On Docker Desktop the daemon
    # only sees host paths, so the container's STATE_DIR must be IDENTICAL to
    # the host path AND under a shared root (/Users).  Mapping the host dir to
    # /opt/srasta broke deploys with "mounts denied: /opt/srasta/setup/... is
    # not shared from the host" (/opt isn't a Docker Desktop shared path and the
    # files live under ~/.local).  So bind congruently and override the image's
    # baked SRASTA_STATE_DIR=/opt/srasta to this host path.
else
    # Linux — pre-create /opt/srasta with the container's effective UID so
    # the wizard can write state there.  Without this the bind-mount lands
    # as root-owned and `_ensure_state_dir()` inside the container fails
    # with EACCES.
    #
    # Two cases:
    #   * Operator is UID 1000 → no `--user` override → container runs as
    #     baked-in srasta UID 1000 → chown to 1000 (legacy gdlab path).
    #   * Operator is UID != 1000 → install.sh adds `--user $(id -u):$(id -g)`
    #     (multi-host UID fix #25) → chown to the operator's UID so the
    #     container can write through the bind-mount.
    # Also: if /opt/srasta already exists from a previous install with the
    # wrong owner, re-chown — otherwise switching operator UIDs leaves a
    # silently-broken install.
    #
    # #363: resolve the OPERATOR's actual UID/GID HERE before the chown,
    # not at line 405 (used to be too late).  Previously the chown
    # defaulted to 1000 when _OPERATOR_UID was unset; any operator with
    # UID != 1000 then hit EACCES writing /opt/srasta/etc-hosts a few
    # lines later (caught on gdlab-spark 2026-05-25 — gdlab user is UID
    # 1001, /opt/srasta got created owned by 1000, script ran as 1001).
    _OPERATOR_UID="${_OPERATOR_UID:-$(id -u)}"
    _OPERATOR_GID="${_OPERATOR_GID:-$(id -g)}"
    _TARGET_UID="${_OPERATOR_UID}"
    _TARGET_GID="${_OPERATOR_GID}"
    export SRASTA_TARGET_UID="${_TARGET_UID}"
    export SRASTA_TARGET_GID="${_TARGET_GID}"
    if [ ! -d /opt/srasta ]; then
        sudo install -d -o "${_TARGET_UID}" -g "${_TARGET_GID}" -m 755 /opt/srasta || {
            echo "ERROR: could not create /opt/srasta — sudo required for first install."
            exit 1
        }
    else
        # Existing dir — ensure ownership matches the container's effective
        # UID even if a previous install used a different one.
        _CURRENT_OWNER="$(stat -c %u /opt/srasta 2>/dev/null || echo "")"
        if [ "${_CURRENT_OWNER}" != "${_TARGET_UID}" ]; then
            sudo chown -R "${_TARGET_UID}:${_TARGET_GID}" /opt/srasta || {
                echo "ERROR: could not re-chown /opt/srasta to ${_TARGET_UID}:${_TARGET_GID}."
                exit 1
            }
        fi
    fi

    # ── Phase 1 of OOM-prevention loop ────────────────────────────────────
    # Drop OS page caches to maximize MemAvailable before launching the
    # installer + smoke gate.  On unified-memory accelerators (V1 = GB10
    # Blackwell-edge per project_v1_blackwell_scope), the OS page cache
    # competes with vLLM for the same physical RAM that CUDA reports as
    # "GPU memory".  A host with 50+ GB of cached HF dataset files makes
    # CUDA see less "free GPU memory" than what the certified Cell budgets for,
    # producing OOM at vLLM engine init even though the scorer said the
    # model fits.  Caught 2026-05-25 on gdlab-spark single-host install
    # (Qwen3-Coder-Next-FP8 needed 60 GB, only 50 GB available because
    # 51 GB was in page cache).
    #
    # Drop is cheap (~ms); the OS re-caches on demand.  Discrete-VRAM
    # hardware (B200, RTX 50xx) is unaffected — their VRAM isn't unified
    # with host RAM; the operation is a near-free no-op there.
    # See project_oom_prevention_strategic_loop.
    _MEM_BEFORE=$(awk '/^MemAvailable:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)
    if sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' 2>/dev/null; then
        _MEM_AFTER=$(awk '/^MemAvailable:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)
        _MEM_FREED=$(( _MEM_AFTER - _MEM_BEFORE ))
        if [ "$_MEM_FREED" -gt 100 ]; then
            echo "Freed ${_MEM_FREED} MiB of OS page cache (MemAvailable ${_MEM_BEFORE} → ${_MEM_AFTER} MiB) for inference."
        fi
    else
        echo "Note: could not drop OS page caches (sudo unavailable).  Install will proceed; on unified-memory hardware (GB10), consider running: sudo sync && sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' before installing."
    fi
fi
# -----------------------------------------------------------------------------
# Mandatory registration (P1.2d) — community installs identify themselves so we
# can support and notify operators.  The installer needs a valid email + org to
# bootstrap catalog access; without them no certified models are served.
#
# Precedence: explicit env (headless/CI) wins; otherwise prompt on a TTY.  We
# export the values and forward them by NAME (docker -e NAME) so an org name
# with spaces survives — building "-e NAME=value" would word-split on the space.
#
# Join mode is already authorized by its bundle (the joining node is enrolling
# into an ALREADY-registered cluster), so it is exempt — same as the license-key
# skip right below.
# -----------------------------------------------------------------------------
if [ -z "${JOIN_BUNDLE}" ]; then
    _reg_email="${SRASTA_REGISTER_EMAIL:-}"
    _reg_org="${SRASTA_REGISTER_ORG:-}"
    if { [ -z "${_reg_email}" ] || [ -z "${_reg_org}" ]; } && _srasta_has_tty; then
        printf '\nSrasta community installs are registered so we can support and notify you.\n'
        if [ -z "${_reg_email}" ]; then
            printf 'Work email: '
            IFS= read -r _reg_email < /dev/tty || true
        fi
        if [ -z "${_reg_org}" ]; then
            printf 'Organization: '
            IFS= read -r _reg_org < /dev/tty || true
        fi
    fi
    _reg_email="$(printf '%s' "${_reg_email}" | tr -d '[:space:]')"
    _reg_org="$(printf '%s' "${_reg_org}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
    if [ -z "${_reg_email}" ] || [ -z "${_reg_org}" ]; then
        echo "Error: registration required to install Srasta." >&2
        echo "       Set SRASTA_REGISTER_EMAIL and SRASTA_REGISTER_ORG (a valid work email" >&2
        echo "       and organization), or run interactively so we can prompt for them." >&2
        exit 1
    fi
    export SRASTA_REGISTER_EMAIL="${_reg_email}"
    export SRASTA_REGISTER_ORG="${_reg_org}"
fi

# Full installs are license-activated. Registration identifies the install;
# SRASTA_LICENSE_KEY is the signed entitlement that lets license-server mint the
# short-lived registry credential. Join mode is already authorized by its bundle.
if [ -z "${JOIN_BUNDLE}" ]; then
    _lic_key="${SRASTA_LICENSE_KEY:-}"
    _runtime_state_dir="${SRASTA_STATE_HOST_DIR:-}"
    if [ -z "${_runtime_state_dir}" ]; then
        if [ "$(uname -s)" = "Darwin" ]; then
            _runtime_state_dir="${SRASTA_STATE_DIR:-${HOME}/.local/share/srasta-installer}"
        else
            _runtime_state_dir="${SRASTA_STATE_DIR:-/opt/srasta}"
        fi
    fi
    _runtime_license_file="${_runtime_state_dir}/.srasta/credentials/license.key"
    if [ -z "${_lic_key}" ] && [ -r "${_runtime_license_file}" ]; then
        _lic_key="$(tr -d '[:space:]' < "${_runtime_license_file}" 2>/dev/null || true)"
        if [ -n "${_lic_key}" ]; then
            echo "Reusing existing owner-only Srasta runtime license for replacement install."
        fi
    fi
    if [ -z "${_lic_key}" ] && _srasta_has_tty; then
        _lic_key="$(_srasta_read_tty_secret 'Srasta license key (leave blank for a free self-serve trial): ')"
    fi
    _lic_key="$(printf '%s' "${_lic_key}" | tr -d '[:space:]')"
    if [ -n "${_lic_key}" ]; then
        export SRASTA_LICENSE_KEY="${_lic_key}"
    else
        # No key supplied anywhere (env, cached file, or interactive prompt) --
        # SRASTA_LICENSE_KEY stays unset. installer.install_bootstrap.bootstrap() self-issues
        # a real trial license from SRASTA_REGISTER_EMAIL/SRASTA_REGISTER_ORG (already
        # exported above) via license-server's own self-serve POST /v1/issue/trial
        # primitive -- the SAME zero-employee-involvement path its public try-page uses,
        # not a human-mediated one. This script stays a thin front door; that call lives
        # in the agent, not here.
        echo "No license key supplied — continuing with a free self-serve trial."
    fi
fi

# Forward the image channel so the wizard's `docker compose` (run inside the
# container) resolves the gandiva service images to the same tag as the installer.
export SRASTA_IMAGE_TAG


# License-server authority. SRASTA_LICENSE_SERVER_URL is canonical for
# activation, discovery-token minting, and registry-token minting.
if [ -z "${SRASTA_LICENSE_SERVER_URL:-}" ]; then
    SRASTA_LICENSE_SERVER_URL="https://licensing.srasta.ai"
fi
if [ -n "${SRASTA_LICENSE_SERVER_URL:-}" ]; then
    export SRASTA_LICENSE_SERVER_URL
fi
# Local-e2e / testing escape hatch (same class as SRASTA_INSTALLER_IMAGE): point
# the installer at a locally-built catalog instead of production. Empty in a
# normal install.
if [ -n "${SRASTA_CATALOG_SERVER_URL:-}" ]; then
    export SRASTA_CATALOG_SERVER_URL
fi
if [ -z "${SRASTA_LICENSE_JWKS:-}" ] && [ -z "${SRASTA_LICENSE_JWKS_FILE:-}" ] && [ -n "${SRASTA_LICENSE_SERVER_URL:-}" ]; then
    _jwks_url="${SRASTA_LICENSE_SERVER_URL%/}/.well-known/jwks.json"
    _lic_jwks="$(curl -fsSL --max-time 10 "${_jwks_url}" 2>/dev/null || true)"
    case "${_lic_jwks}" in
        *'"keys"'*'"kid"'*)
            SRASTA_LICENSE_JWKS="${_lic_jwks}"
            ;;
        *)
            echo "Error: could not fetch Srasta license JWKS from ${_jwks_url}." >&2
            echo "       Set SRASTA_LICENSE_JWKS_FILE or retry when licensing.srasta.ai is reachable." >&2
            exit 1
            ;;
    esac
fi
if [ -n "${SRASTA_LICENSE_JWKS:-}" ]; then
    export SRASTA_LICENSE_JWKS
fi
if [ -n "${SRASTA_LICENSE_JWKS_FILE:-}" ]; then
    export SRASTA_LICENSE_JWKS_FILE
fi
if [ -n "${SRASTA_REQUIRE_CERTIFIED:-}" ]; then
    export SRASTA_REQUIRE_CERTIFIED
fi

# ── Agent-first install (the Srasta Agent IS the installer) ──────────────────
#
# Instead of running a long-lived wizard CONTAINER that drives the install over
# the mounted docker socket (and, for multi-host, SSHes out to nodes), we put
# the Srasta AGENT on the host and let IT drive.  Two things the agent needs
# live inside the installer image and are extracted here:
#
#   * the compiled agent binary            /srv/srasta-agent/srasta-agent-<triple>
#   * the pristine compose project + setup /srv/srasta-installer/  (SRASTA_APP_DIR)
#
# The agent then converges the WHOLE stack on this host (`install`), proving
# G1/G2/G3, and registers itself as a host service for day-2 (`install-service`).
# No wizard container, no host-serve watcher, no SSH — day-0 ≡ day-2.

STATE_HOST_DIR="${SRASTA_STATE_HOST_DIR:-/opt/srasta}"
COMPOSE_RUNNER_EXECUTABLE="$(command -v docker 2>/dev/null || true)"
case "${COMPOSE_RUNNER_EXECUTABLE}" in
    /*) ;;
    *)
        echo "ERROR: the Compose Runner requires an absolute Docker executable path." >&2
        exit 1
        ;;
esac

# (OS, CPU arch) — the only axis the BINARY varies on.  Hardware/accelerator is
# data inside the image, not a second binary.
_os="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "$(uname -m)" in
    arm64 | aarch64) _arch="arm64" ;;
    x86_64 | amd64) _arch="amd64" ;;
    *) echo "ERROR: unsupported CPU arch $(uname -m)"; exit 1 ;;
esac
TRIPLE="${_os}-${_arch}"

AGENT_HOME="${HOME}/.srasta/agent"
AGENT_BIN="${AGENT_HOME}/bin/srasta-agent"
APP_HOST_DIR="${AGENT_HOME}/app"   # the pristine compose project, on the host
mkdir -p "${AGENT_HOME}/bin" "${APP_HOST_DIR}"

# Extract the agent binary + compose project from the image via a throwaway
# container (docker create + cp + rm — no long-lived container).
echo "Installing the Srasta Agent (${TRIPLE})..."
_cid="$(docker create "${INSTALLER_IMAGE}")" || {
    echo "ERROR: could not create a container from ${INSTALLER_IMAGE} to extract the agent."
    exit 1
}
if ! docker cp "${_cid}:/srv/srasta-agent/srasta-agent-${TRIPLE}" "${AGENT_BIN}" 2>/dev/null; then
    docker rm -f "${_cid}" >/dev/null 2>&1 || true
    echo "ERROR: the installer image has no agent binary for ${TRIPLE}."
    echo "  It was built without this platform (CI builds darwin-arm64, linux-arm64, linux-amd64)."
    exit 1
fi
docker cp "${_cid}:/srv/srasta-installer/." "${APP_HOST_DIR}/" >/dev/null 2>&1 || {
    docker rm -f "${_cid}" >/dev/null 2>&1 || true
    echo "ERROR: could not extract the compose project from the installer image."
    exit 1
}
docker rm -f "${_cid}" >/dev/null 2>&1 || true
chmod +x "${AGENT_BIN}"

# Prove the binary is intact before we depend on it (bundles code AND its data).
"${AGENT_BIN}" selfcheck </dev/null || {
    echo "ERROR: the agent binary failed its self-check — not installing over a broken agent."
    exit 1
}

# Replacement installs preserve the Srasta-Agent identity and files, but the
# resident service must not race a newly-created control plane with its previous
# bootstrap credential. Ask the new Agent binary to quiesce the old service
# through its host-native Runner adapter. ``install-service`` reactivates it only
# after convergence and handover have staged one authoritative credential.
_EXISTING_AGENT_HOST_ID=""
_EXISTING_AGENT_ENV="${AGENT_HOME}/etc/agent.env"
if [ -r "${_EXISTING_AGENT_ENV}" ]; then
    _EXISTING_AGENT_HOST_ID="$(grep -E '^SRASTA_AGENT_HOST_ID=' "${_EXISTING_AGENT_ENV}" | tail -1 | cut -d= -f2- || true)"
fi
if [ -z "${_EXISTING_AGENT_HOST_ID}" ]; then
    _EXISTING_AGENT_HOST_ID="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo srasta-local)"
fi
"${AGENT_BIN}" quiesce-service --host-id "${_EXISTING_AGENT_HOST_ID}" </dev/null || {
    echo "ERROR: the existing Srasta Agent could not be quiesced for replacement." >&2
    echo "  Refusing to let two Agent credentials race the handover authority." >&2
    exit 1
}
_SRASTA_AGENT_REPLACEMENT_PENDING=1
trap '_srasta_restore_agent_on_failure' 0

# ── JOIN mode: this node attaches to an EXISTING cluster (multi-host, zero-SSH) ─
# The agent registers against the control-plane in the bundle and converges the
# slice it is assigned — NO full local install, NO SSH.  We never reach into the
# node; the node pulled this script and enrols itself.
if [ -n "${JOIN_BUNDLE}" ]; then
    echo "Joining the Srasta cluster..."
    "${AGENT_BIN}" join --bundle "${JOIN_BUNDLE}" </dev/null || {
        echo "ERROR: join failed — the node did not enrol.  Check the bundle + that the"
        echo "  control-plane is reachable from this node."
        exit 1
    }
    echo ""
    echo "  ✅ This node joined the cluster and is converging its assigned slice."
    echo "     The agent runs as a host service and stays converged."
    echo ""
    exit 0
fi

# The agent drives day-0 on THIS host: detect hardware → the validated model →
# materialize the compose project → bring the stack up → prove G1/G2/G3.  It
# reads the compose project from SRASTA_APP_DIR and writes the install to
# SRASTA_STATE_DIR.  Registration (email/org) + image channel are already
# exported above and inherited by the agent's environment.
echo "The agent is converging the platform on this host..."
SRASTA_APP_DIR="${APP_HOST_DIR}" \
SRASTA_STATE_DIR="${STATE_HOST_DIR}" \
SRASTA_COMPOSE_RUNNER_EXECUTABLE="${COMPOSE_RUNNER_EXECUTABLE}" \
    "${AGENT_BIN}" install drive </dev/null || {
    echo ""
    echo "ERROR: the agent-driven install did not complete.  The stack is not proven."
    echo "  Logs: ${STATE_HOST_DIR}/.srasta/  —  re-run this command to retry (idempotent)."
    exit 1
}

# ── Register the agent for day-2 (systemd/launchd) ───────────────────────────
#
# Day-0 proved the stack.  Now install the SAME binary as a host service so it
# keeps the node converged — detecting drift, applying day-2 changes, and (on
# Apple) owning host-native serving directly (Metal).  A host-resident agent
# reaches launchd and Metal itself, which is what retired the old docker-cp'd
# host-serve watcher and its stdlib-only straitjacket.
#
# The agent renders non-secret routing and owner-only credential file paths.
# Raw handoff credentials never enter .env or process arguments.
_ENV_FILE="${STATE_HOST_DIR}/.env"
_AGENT_TOKEN_FILE=""
_AGENT_HOST_ID=""
_DAY2_CONTROL_PLANE_URL=""
_DAY2_DISCOVERY_URL=""
_DAY2_DISCOVERY_TOKEN_FILE=""
_DAY2_IDENTITY_URL=""
_DAY2_AGENT_INSTALLED=0
if [ -r "${_ENV_FILE}" ]; then
    _AGENT_TOKEN_FILE="$(grep -E '^SRASTA_AGENT_AUTH_TOKEN_FILE=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _AGENT_HOST_ID="$(grep -E '^SRASTA_AGENT_HOST_ID=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _DAY2_CONTROL_PLANE_URL="$(grep -E '^SRASTA_AGENT_CONTROL_PLANE_URL=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _DAY2_DISCOVERY_URL="$(grep -E '^SRASTA_AGENT_DISCOVERY_URL=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _DAY2_DISCOVERY_TOKEN_FILE="$(grep -E '^SRASTA_AGENT_DISCOVERY_TOKEN_FILE=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _DAY2_IDENTITY_URL="$(grep -E '^SRASTA_AGENT_IDENTITY_URL=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
    _DAY2_IDENTITY_HOST_HEADER="$(grep -E '^SRASTA_AGENT_IDENTITY_HOST_HEADER=' "${_ENV_FILE}" | tail -1 | cut -d= -f2- || true)"
fi
if [ -n "${_AGENT_TOKEN_FILE}" ]; then
    case "${_AGENT_TOKEN_FILE}" in /*) ;; *) _AGENT_TOKEN_FILE="${STATE_HOST_DIR}/${_AGENT_TOKEN_FILE}" ;; esac
fi
if [ -n "${_DAY2_DISCOVERY_TOKEN_FILE}" ]; then
    case "${_DAY2_DISCOVERY_TOKEN_FILE}" in /*) ;; *) _DAY2_DISCOVERY_TOKEN_FILE="${STATE_HOST_DIR}/${_DAY2_DISCOVERY_TOKEN_FILE}" ;; esac
fi
if [ -n "${_AGENT_TOKEN_FILE}" ] && [ -f "${_AGENT_TOKEN_FILE}" ] \
    && [ -r "${_AGENT_TOKEN_FILE}" ] && [ -n "${_DAY2_DISCOVERY_TOKEN_FILE}" ] \
    && [ -f "${_DAY2_DISCOVERY_TOKEN_FILE}" ] && [ -r "${_DAY2_DISCOVERY_TOKEN_FILE}" ]; then
    if [ -z "${_AGENT_HOST_ID}" ]; then
        _AGENT_HOST_ID="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo localhost)"
    fi
    if [ -z "${_DAY2_CONTROL_PLANE_URL}" ] || [ -z "${_DAY2_DISCOVERY_URL}" ] \
        || [ -z "${_DAY2_IDENTITY_URL}" ] || [ -z "${_DAY2_IDENTITY_HOST_HEADER}" ]; then
        echo "ERROR: day-2 Srasta-Agent handoff facts are incomplete." >&2
        echo "  Required: SRASTA_AGENT_CONTROL_PLANE_URL, SRASTA_AGENT_DISCOVERY_URL," >&2
        echo "            SRASTA_AGENT_IDENTITY_URL, SRASTA_AGENT_IDENTITY_HOST_HEADER." >&2
        echo "  The installer will not guess localhost fallback URLs." >&2
        exit 1
    fi
    SRASTA_APP_DIR="${APP_HOST_DIR}" SRASTA_STATE_DIR="${STATE_HOST_DIR}" \
        "${AGENT_BIN}" install-service \
            --host-id "${_AGENT_HOST_ID}" \
            --token-file "${_AGENT_TOKEN_FILE}" \
            --control-plane-url "${_DAY2_CONTROL_PLANE_URL}" \
            --identity-url "${_DAY2_IDENTITY_URL}" \
            --identity-host-header "${_DAY2_IDENTITY_HOST_HEADER}" \
            --discovery-url "${_DAY2_DISCOVERY_URL}" \
            --discovery-token-file "${_DAY2_DISCOVERY_TOKEN_FILE}" \
            --platform-root "${STATE_HOST_DIR}" \
            </dev/null \
        && _DAY2_AGENT_INSTALLED=1 \
        || {
            echo "ERROR: day-2 Srasta-Agent service registration failed." >&2
            echo "  The runtime may be healthy, but control was not handed over." >&2
            exit 1
        }
else
    echo "ERROR: protected agent handoff credential is missing or unreadable." >&2
    exit 1
fi

_SRASTA_INSTALL_COMPLETE=1
_SRASTA_AGENT_REPLACEMENT_PENDING=0
trap - 0
trap - INT TERM HUP
echo ""
echo "  ✅ Srasta is installed and proven on this host."
echo ""
echo "     The Srasta Agent brought the platform up and verified it (G1/G2/G3)."
if [ "${_DAY2_AGENT_INSTALLED}" = "1" ]; then
    echo "     It now runs as a host service to keep this node converged."
else
    echo "     Day-2 host service registration was skipped; keep this terminal output"
    echo "     and run: ${AGENT_BIN} install-bundle --install-root ${STATE_HOST_DIR}"
    echo "     if support needs the sanitized install evidence."
fi
echo ""
echo "     State + logs:  ${STATE_HOST_DIR}"
echo "     Re-run this command any time — the agent reconciles idempotently."
echo ""
echo "     Questions or stuck on something? Join the Srasta community:"
echo "       https://discord.gg/GhdyStB8W4"
echo ""
