#!/bin/sh
# =============================================================================
# Srasta Platform — Quick Install
#
# Canonical source for the customer installer:
#     scripts/get-srasta.sh        ← owned by the installable platform repo
# This file ships INSIDE each published release (publish:release-compatibility
# sends it to license-server with the release manifest) and is served back at
# https://licensing.srasta.ai/pub/install.sh; https://get.srasta.ai proxies
# that and keeps no copy. Customers always run the script of the release they
# install -- edits here reach them with the next tagged release, nothing else
# to update anywhere.
# =============================================================================
#
# 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_AGENT_PREV_SAVED=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
}

# Escape a value for a JSON string (backslash, double quote; control characters dropped).
_srasta_json_escape() {
    printf '%s' "$1" | tr -d '\000-\037' | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# A trial key has three base64url segments (a signed JWT). Shape only: the agent verifies the signature against the
# retained JWKS during install, so a forged or mistyped key is refused there, not trusted here.
_srasta_looks_like_license_key() {
    printf '%s' "$1" | grep -Eq '^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$'
}

# Ask license-server to EMAIL a free trial key to $1 (organization $2), then read the pasted key. The request body and the
# consent wording are defined once in installer/license_trial.py (trial_request_payload / TRIAL_CONSENT_TEXT) and a test
# pins this copy to it. On success SRASTA_LICENSE_KEY is exported; otherwise this exits non-zero with the next step.
_srasta_obtain_trial_key() {
    _otk_email="$1"
    _otk_org="$2"
    _otk_url="${SRASTA_LICENSE_SERVER_URL:-https://licensing.srasta.ai}"
    printf '\nA free 30-day trial key will be emailed to %s.\n' "${_otk_email}"
    printf 'Email me the trial license and product follow-up. Deployment intelligence only; no prompts, responses,\n'
    printf 'documents, embeddings, secrets, or audit payload content.\n'
    printf 'Send the trial key to %s? [y/N]: ' "${_otk_email}"
    _otk_answer=""
    IFS= read -r _otk_answer < /dev/tty || true
    case "${_otk_answer}" in
        y | Y | yes | YES | Yes) ;;
        *)
            echo "Error: a license key is required to install Srasta." >&2
            echo "       Run this again and accept to be emailed a free trial key, or set SRASTA_LICENSE_KEY." >&2
            exit 1
            ;;
    esac
    _otk_body="$(printf '{"email":"%s","company":"%s","source":"installer-agent-trial","hardware_intent":"evaluating-hardware","consent":true,"consent_text":"Email me the trial license and product follow-up. Deployment intelligence only; no prompts, responses, documents, embeddings, secrets, or audit payload content."}' \
        "$(_srasta_json_escape "${_otk_email}")" "$(_srasta_json_escape "${_otk_org}")")"
    _otk_response="$(mktemp)"
    _otk_status="$(curl -sS -o "${_otk_response}" -w '%{http_code}' --max-time 20 -X POST \
        -H 'Content-Type: application/json' -d "${_otk_body}" "${_otk_url%/}/v1/requests/trial" 2>/dev/null || true)"
    case "${_otk_status}" in
        200) rm -f "${_otk_response}" ;;
        429)
            rm -f "${_otk_response}"
            echo "A trial key was requested for ${_otk_email} a moment ago. Check that inbox (and spam) and paste the key below." >&2
            ;;
        *)
            echo "Error: could not request a trial key (HTTP ${_otk_status:-none}): $(head -c 300 "${_otk_response}" 2>/dev/null)" >&2
            rm -f "${_otk_response}"
            echo "       Request one at https://srasta.ai/try, then re-run with SRASTA_LICENSE_KEY=<the key>." >&2
            exit 1
            ;;
    esac
    printf 'Check %s for your Srasta trial key.\n' "${_otk_email}"
    _otk_attempt=0
    while [ "${_otk_attempt}" -lt 3 ]; do
        _otk_attempt=$((_otk_attempt + 1))
        _otk_key="$(_srasta_read_tty_secret 'Paste the trial key from the email: ' | tr -d '[:space:]')"
        printf '\n'
        if _srasta_looks_like_license_key "${_otk_key}"; then
            export SRASTA_LICENSE_KEY="${_otk_key}"
            return 0
        fi
        echo "That does not look like a Srasta license key (it should be one long string with two dots). Try again." >&2
    done
    echo "Error: no valid license key was entered. Re-run this installer when the email arrives." >&2
    exit 1
}

# 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}"
)

# Install the verified, self-checked staged agent binary. The binary it replaces is kept first (cp reads, so this
# works while that agent is running); then the new one is renamed into place.
_srasta_commit_agent_binary() {
    if [ -e "${AGENT_BIN}" ]; then
        if ! cp -p "${AGENT_BIN}" "${AGENT_BIN_PREV}"; then
            echo "ERROR: could not keep the previous agent binary at ${AGENT_BIN_PREV}; not replacing it." >&2
            return 1
        fi
        _SRASTA_AGENT_PREV_SAVED=1
    fi
    mv -f "${AGENT_BIN_NEW}" "${AGENT_BIN}"
}

_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
        # Put the previous BINARY back before resuming the service: the unit starts whatever is at AGENT_BIN, so
        # resuming first would run the NEW agent against the OLD stack (it fails closed and reconciles nothing,
        # but it is not the agent the host had). The new binary stays reachable as AGENT_BIN_NEW only to issue the
        # verb, because an older build may predate resume-service.
        _srasta_resume_bin="${AGENT_BIN}"
        if [ "${_SRASTA_AGENT_PREV_SAVED}" = "1" ] && [ -e "${AGENT_BIN_PREV}" ]; then
            cp -p "${AGENT_BIN}" "${AGENT_BIN_NEW}" 2>/dev/null && _srasta_resume_bin="${AGENT_BIN_NEW}"
            mv -f "${AGENT_BIN_PREV}" "${AGENT_BIN}" \
                || echo "WARNING: the previous Srasta Agent binary could not be put back at ${AGENT_BIN}." >&2
        fi
        "${_srasta_resume_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
        rm -f "${AGENT_BIN_NEW}"
    fi
    exit "${_srasta_exit_status}"
}

# Where this host's install keeps its state (the same rule the licence reuse below applies).
_srasta_state_dir() {
    _sd="${SRASTA_STATE_HOST_DIR:-}"
    if [ -z "${_sd}" ]; then
        if [ "$(uname -s)" = "Darwin" ]; then
            _sd="${SRASTA_STATE_DIR:-${HOME}/.local/share/srasta-installer}"
        else
            _sd="${SRASTA_STATE_DIR:-/opt/srasta}"
        fi
    fi
    printf '%s' "${_sd}"
}

# Value of KEY in this host's existing install .env, or nothing. Only ever called for the non-secret settings
# listed in _srasta_adopt_existing_settings; never for a credential.
_srasta_existing_env() {
    _ef="$(_srasta_state_dir)/.env"
    [ -r "${_ef}" ] || return 0
    grep -E "^$1=" "${_ef}" 2>/dev/null | tail -1 | cut -d= -f2-
}

# A re-run on a host that already has an install must start from what that install is, not from nothing:
# anything the operator did not state THIS time is taken from the existing install. An explicit setting always
# wins. Without it a non-interactive re-run defaulted the ingress to loopback (which would silently take a
# public site offline) and demanded the registration email and organization again.
_srasta_adopt_existing_settings() {
    for _k in SRASTA_REGISTER_EMAIL SRASTA_REGISTER_ORG; do
        eval "_cur=\${${_k}:-}"
        if [ -z "${_cur}" ]; then
            _v="$(_srasta_existing_env "${_k}")"
            [ -n "${_v}" ] && export "${_k}=${_v}"
        fi
    done
    if [ -z "${SRASTA_INGRESS_BACKEND:-}" ]; then
        _v="$(_srasta_existing_env SRASTA_INGRESS_BACKEND)"
        if [ -n "${_v}" ]; then
            export SRASTA_INGRESS_BACKEND="${_v}"
            for _k in SRASTA_BASE_DOMAIN SRASTA_CLOUDFLARE_TUNNEL_NAME SRASTA_CLOUDFLARE_API_TOKEN_FILE SRASTA_ACME_EMAIL; do
                eval "_cur=\${${_k}:-}"
                if [ -z "${_cur}" ]; then
                    _v2="$(_srasta_existing_env "${_k}")"
                    [ -n "${_v2}" ] && export "${_k}=${_v2}"
                fi
            done
            echo "Keeping this install's existing ingress (${SRASTA_INGRESS_BACKEND})." \
                "To change it, set SRASTA_INGRESS_BACKEND, or run the configure-ingress verb." >&2
        fi
    fi
}

# 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.
# _SRASTA_TAG_AUTO_RESOLVED tracks whether SRASTA_IMAGE_TAG is a REAL, signed
# release version we resolved ourselves (verifiable, finding #15,
# 2026-08-24 security audit) vs an operator-chosen moving channel (dev/main/
# branch, an already-disclosed unsigned escape hatch -- floating tags are
# never per-image cosign-signed, only version tags are, see
# .gitlab-ci.yml's release:artifacts:tag job).
# Front-door trust root (supply-chain plan W4).  The Srasta release PUBLIC key (ECDSA P-256, PEM) used to verify the
# agent binary with plain `openssl` -- no `cosign` needed on the operator's machine.  Provisioned (AWS KMS alias
# srasta-image-signing); verification is MANDATORY: a missing signature, a
# missing `openssl`, or a mismatch aborts the install.  Must equal the key(s) in installer/image_signing_keys.json
# (a test enforces it).  Honest limit: `curl | sh` trusts get.srasta.ai + TLS for THIS script; the key
# fingerprint is published out of band (docs) so it can be checked, and `--bundle vX.Y.Z` is the digest-pinned path.
_SRASTA_RELEASE_PUBKEY_PEM='-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQzwRTPxyiI0yASWQn/TxKnc5ajaL
zsp6qCPVBxDJAhbCN1KHXTCp4ywIqrqs5TllUq1Ob9VI0PXWX+K4t+7okw==
-----END PUBLIC KEY-----'
_SRASTA_TAG_AUTO_RESOLVED=""
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}"
    _SRASTA_TAG_AUTO_RESOLVED=1
    # The release record is served by a DIFFERENT origin (licensing.srasta.ai) than the registry that holds the
    # image, and carries the installer's digest.  When it does, that digest is the authority: we pull BY DIGEST
    # below and the registry's mutable tag becomes irrelevant (supply-chain plan W4).  Older records without the
    # field fall back to tag + post-pull digest pinning.
    _srasta_release_installer_digest="$(printf '%s' "${_srasta_release_current}" | tr -d '\n' | sed -n 's/.*"installer_digest"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
fi
# _SRASTA_IMAGE_EXPLICIT: an operator-supplied image ref (env var or --image)
# is a local/dev-build escape hatch (same class as SRASTA_IMAGE_TAG above) --
# there's no registry provenance to verify a signature against.
_SRASTA_IMAGE_EXPLICIT="${SRASTA_INSTALLER_IMAGE:+1}"
INSTALLER_IMAGE="${SRASTA_INSTALLER_IMAGE:-registry.gitlab.com/gandiva/srasta-installer/installer:${SRASTA_IMAGE_TAG}}"
# ── Release-record digest overrides the tag (auto-resolved release installs only) ────────────────────────
if [ -n "${_SRASTA_TAG_AUTO_RESOLVED}" ] && [ -z "${_SRASTA_IMAGE_EXPLICIT}" ] \
    && [ -n "${_srasta_release_installer_digest:-}" ]; then
    case "${_srasta_release_installer_digest}" in
        sha256:????????????????????????????????????????????????????????????????) ;;
        *)
            echo "ERROR: the release record's installer_digest is malformed: ${_srasta_release_installer_digest}" >&2
            echo "  Refusing to install from an unverifiable release record." >&2
            exit 1
            ;;
    esac
    INSTALLER_IMAGE="registry.gitlab.com/gandiva/srasta-installer/installer@${_srasta_release_installer_digest}"
fi
BUNDLE_VERSION=""
JOIN_BUNDLE=""
# --pairing-code (2026-09-16, install-journey redesign Group 5 item 23): the operator-
# facing default going forward for "Add a Node to This Cluster" -- a short, human-
# typeable code (+ QR) instead of the raw base64 join bundle. Requires
# --control-plane-url alongside it (a bare code carries no WHERE-to-resolve-it fact the
# way a bundle does) -- mutually exclusive with --join, resolved via
# `srasta-agent join --pairing-code` itself (join_bundle.py::resolve_pairing_code), not
# here in shell.
PAIRING_CODE=""
CONTROL_PLANE_URL=""
# What PRECHECK Charter a joining node checks itself against (2026-09-16) -- "multi" for
# a human pasting the bare-metal "Add a Node" card's command (the default, matching
# installer/join_bundle.py::join_command's own default), "cloud" when this line came from
# provision()'s cloud-init onstart script. Meaningless without --join/--pairing-code;
# ignored otherwise.
JOIN_HOST_ROLE="multi"
SRASTA_GITLAB_URL="${SRASTA_GITLAB_URL:-https://gitlab.com/gandiva-tech/srasta}"
# --skip-precheck (2026-09-16): skips ONLY the standalone, front-loaded PRECHECK this
# script runs before `install drive` (below) — an escape hatch for scripted/CI callers
# that already know the target host, mirroring the Tauri wizard's own "Validate" stage
# being separate from its blocking install sequence. The PRECHECK `install drive` itself
# runs internally as its own first step (HostAgentRunner._handle_precheck) is NOT
# skippable — this flag only controls whether the terminal path front-loads a second,
# human-readable look at it before committing to anything.
SKIP_PRECHECK=""

# Parse args
while [ $# -gt 0 ]; do
    case "$1" in
        --image)          INSTALLER_IMAGE="$2"; _SRASTA_IMAGE_EXPLICIT=1; 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 ;;
        --pairing-code)   PAIRING_CODE="$2";    shift 2 ;;
        --control-plane-url) CONTROL_PLANE_URL="$2"; shift 2 ;;
        --host-role)      JOIN_HOST_ROLE="$2";  shift 2 ;;
        --skip-precheck)  SKIP_PRECHECK=1;      shift ;;
        *)         echo "Unknown option: $1"; exit 1 ;;
    esac
done

if [ -n "${JOIN_BUNDLE}" ] && [ -n "${PAIRING_CODE}" ]; then
    echo "ERROR: --join and --pairing-code are mutually exclusive." >&2
    exit 1
fi
if [ -n "${PAIRING_CODE}" ] && [ -z "${CONTROL_PLANE_URL}" ]; then
    echo "ERROR: --pairing-code requires --control-plane-url." >&2
    exit 1
fi
# True for EITHER join path -- a joining node (bundle or pairing-code) skips the
# full-install-only registration/license-key prompts below, same reasoning either way:
# it's enrolling into an ALREADY-registered, already-licensed cluster.
_JOINING=""
if [ -n "${JOIN_BUNDLE}" ] || [ -n "${PAIRING_CODE}" ]; then
    _JOINING=1
fi

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.)

# ── Production warning (#291): nudge default-mode users toward verified bundles ─
#
# 2026-08-24 security audit finding #15: this used to unconditionally claim
# "no signature verification" for default mode, which stopped being true once
# the ── Verify ── block below started cosign-verifying the auto-resolved
# release image (when cosign is installed).  Still tell operators to use
# --bundle for the strictly stronger guarantee (hard digest pin, no
# dependency on cosign being present on this box) — just not with a claim
# that's no longer accurate for the common case.
if [ -z "$BUNDLE_VERSION" ]; then
    echo "  ┌──────────────────────────────────────────────────────────────────┐"
    echo "  │  ⚠ Default mode signature-verifies the resolved release (if      │"
    echo "  │    cosign is installed) but does not pin the image by digest.    │"
    echo "  │    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@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
        --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..."
_SRASTA_PULLED_FROM_REGISTRY=1
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}."
        _SRASTA_PULLED_FROM_REGISTRY=""
    else
        echo "ERROR: Failed to pull ${INSTALLER_IMAGE} and it isn't present locally."
        echo "  Check your network or registry credentials."
        exit 1
    fi
}

# ── Pin the pulled installer image by DIGEST ────────────────────────────────
# Supply-chain plan W4 (docs/roadmap/supply-chain-hardening-plan.md).  A version TAG is mutable: anyone who
# can push to the registry can repoint it after we verified it, and the Srasta-Agent binds an image signature
# to a DIGEST -- a tag ref can never be verified there, so an enforcing agent would reject every install that
# left this image on a tag.  After a registry pull, resolve the digest and use ONLY the digest from here on:
# what we verify below, what we `docker create` from, and what SRASTA_INSTALLER_IMAGE hands the agent (and
# through it compose) are then the same immutable bytes.
case "${INSTALLER_IMAGE}" in
    *@sha256:*) ;;   # already digest-pinned (bundle mode)
    *)
        if [ -n "${_SRASTA_PULLED_FROM_REGISTRY}" ]; then
            _SRASTA_PINNED="$(docker image inspect "${INSTALLER_IMAGE}" \
                --format '{{range .RepoDigests}}{{.}}{{"\n"}}{{end}}' 2>/dev/null \
                | grep "^${INSTALLER_IMAGE%%:*}@" | head -1)"
            if [ -n "${_SRASTA_PINNED}" ]; then
                INSTALLER_IMAGE="${_SRASTA_PINNED}"
                SRASTA_INSTALLER_IMAGE="${INSTALLER_IMAGE}"
                export SRASTA_INSTALLER_IMAGE
                echo "Pinned the installer image by digest: ${INSTALLER_IMAGE}"
            elif [ -n "${_SRASTA_TAG_AUTO_RESOLVED}" ] && [ -z "${_SRASTA_IMAGE_EXPLICIT}" ]; then
                echo "ERROR: could not resolve a registry digest for ${INSTALLER_IMAGE} to pin." >&2
                echo "  Refusing to install a release from a mutable tag." >&2
                exit 1
            fi
        fi
        ;;
esac

# ── Verify (2026-08-24 security audit finding #15) ──────────────────────────
#
# Bundle mode already cryptographically anchors the image (blob-signature +
# digest pin) before it's ever pulled, so this only covers the DEFAULT path.
# The default path's auto-resolved tag is a real release version, and every
# release-tag build IS cosign-signed per-image in CI
# (release:artifacts:tag -> scripts/sign-image-attestations.sh), using the
# SAME keyless identity/issuer bundle mode already verifies against below --
# no new signing infrastructure needed, just actually checking it here too.
#
# Skipped (not a gap, a disclosed escape hatch) when: bundle mode already
# verified; the operator explicitly chose a moving channel or image (dev/
# main/branch/--image/SRASTA_INSTALLER_IMAGE -- floating tags are never
# per-image signed); or the pull fell back to a local-only image (no
# registry provenance to check a signature against at all).
if [ -z "$BUNDLE_VERSION" ] && [ -n "${_SRASTA_TAG_AUTO_RESOLVED}" ] \
    && [ -z "${_SRASTA_IMAGE_EXPLICIT}" ] && [ -n "${_SRASTA_PULLED_FROM_REGISTRY}" ]; then
    if command -v cosign >/dev/null 2>&1; then
        case "${INSTALLER_IMAGE}" in
            *@sha256:*) _SRASTA_PULLED_DIGEST="${INSTALLER_IMAGE}" ;;   # pinned just above
            *)
                _SRASTA_PULLED_DIGEST="$(docker image inspect "${INSTALLER_IMAGE}" \
                    --format '{{range .RepoDigests}}{{.}}{{"\n"}}{{end}}' 2>/dev/null \
                    | grep "^${INSTALLER_IMAGE%%:*}@" | head -1)"
                ;;
        esac
        if [ -z "${_SRASTA_PULLED_DIGEST}" ]; then
            echo "ERROR: could not resolve a registry digest for ${INSTALLER_IMAGE} to verify." >&2
            echo "  Refusing to trust an unverifiable release image." >&2
            exit 1
        fi
        echo "Verifying installer image signature (Sigstore keyless) ..."
        cosign verify \
            --certificate-identity-regexp 'https://gitlab\.com/gandiva-tech/srasta//\.gitlab-ci\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
            --certificate-oidc-issuer 'https://gitlab.com' \
            "${_SRASTA_PULLED_DIGEST}" >/dev/null || {
            echo "ERROR: cosign signature verification FAILED for ${_SRASTA_PULLED_DIGEST}." >&2
            echo "  This release image did not verify against Srasta's release identity." >&2
            echo "  Refusing to install a release image that fails signature verification." >&2
            exit 1
        }
        echo "  Signature verified: ${_SRASTA_PULLED_DIGEST}"
    else
        # Most hosts don't have cosign installed -- this is the ordinary case for a
        # default install, not a partial failure, so it shouldn't read like one (⚠).
        # The image itself is still pulled by the version tag CI just built and signed;
        # what's skipped is this SCRIPT verifying that signature locally.
        echo "  ℹ cosign not installed, so this run did not verify the installer image's"
        echo "    signature locally. For a fully verified install (recommended for"
        echo "    production), install cosign (brew install cosign / github.com/sigstore/cosign)"
        echo "    and re-run, or use --bundle vX.Y.Z for a digest-pinned, signature-verified"
        echo "    install regardless of whether cosign is present."
    fi
fi

# ── Extract the compose project (SRASTA_APP_DIR) ────────────────────────────
#
# Root-caused live 2026-09-22: `install drive`/`join` need docker-compose.yml,
# release-manifest.json, setup/, compose/ on disk (installer/install_plane.py's
# APP_DIR). Leaving SRASTA_APP_DIR unset (as this script did until now) falls
# back to the agent binary's own onefile-embedded copy -- but
# scripts/build-agent-binary.sh only bundles release-manifest.json "if present
# in the repo checkout at build time", and that file is generated by
# build:digest-manifest, which runs AFTER build:agent:* in the CI DAG (images
# need the agent binary first; the manifest needs the images' digests). So the
# embedded copy can NEVER include it for a real release build, and every
# host-native install via this script hard-failed at pull-images with a
# missing SRASTA_RELEASE_MANIFEST_HASH -- for every release since the
# onefile-embed design shipped (2026-09-16). The admin-desktop wizard was
# never affected: wizard.rs::ensure_app_dir has always extracted the compose
# project this same way (docker cp from the resolved installer image) instead
# of trusting the binary's embedded copy. This makes the terminal path match
# that already-proven mechanism, rather than inventing a new one.
SRASTA_APP_DIR="${HOME}/.srasta/agent/app"
rm -rf "${SRASTA_APP_DIR}"
mkdir -p "${SRASTA_APP_DIR}"
_app_cid="$(docker create "${INSTALLER_IMAGE}")" || {
    echo "ERROR: could not create a container from ${INSTALLER_IMAGE} to extract the compose project." >&2
    exit 1
}
if ! docker cp "${_app_cid}:/srv/srasta-installer/." "${SRASTA_APP_DIR}/" 2>/dev/null; then
    docker rm -f "${_app_cid}" >/dev/null 2>&1 || true
    echo "ERROR: could not extract the compose project from ${INSTALLER_IMAGE}." >&2
    exit 1
fi
docker rm -f "${_app_cid}" >/dev/null 2>&1 || true
export SRASTA_APP_DIR

# 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
# A re-run on a host that already has an install starts from that install: the registration and the ingress below
# are read from the environment, so whatever the operator did not state now is filled in first (a node joining an
# existing cluster is exempt, as it is for registration and ingress themselves).
if [ -z "${_JOINING}" ]; then
    _srasta_adopt_existing_settings
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 (bundle or pairing-code) is already authorized by its credential (the
# joining node is enrolling into an ALREADY-registered cluster), so it is exempt --
# same as the license-key skip right below.
# -----------------------------------------------------------------------------
if [ -z "${_JOINING}" ]; 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 credential.
if [ -z "${_JOINING}" ]; then
    _lic_key="${SRASTA_LICENSE_KEY:-}"
    _runtime_state_dir="$(_srasta_state_dir)"
    _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 to be emailed a free trial key): ')"
    fi
    _lic_key="$(printf '%s' "${_lic_key}" | tr -d '[:space:]')"
    if [ -n "${_lic_key}" ]; then
        export SRASTA_LICENSE_KEY="${_lic_key}"
    else
        # No key anywhere (env, cached file, prompt). A trial key is never handed to the caller: license-server EMAILS it to
        # the address that asks (POST /v1/requests/trial, the same intake the website's try page uses), and the operator
        # pastes it, so receiving the email is the proof they own the address. Interactive only; a script has no inbox.
        if _srasta_has_tty; then
            _srasta_obtain_trial_key "${SRASTA_REGISTER_EMAIL}" "${SRASTA_REGISTER_ORG}"
        else
            echo "Error: no Srasta license key, and this is not an interactive session." >&2
            echo "       A free trial key is emailed to the address that requests it: request one at" >&2
            echo "       https://srasta.ai/try, then re-run with SRASTA_LICENSE_KEY=<the key from the email>." >&2
            exit 1
        fi
    fi
fi

# -----------------------------------------------------------------------------
# Public-exposure choice (2026-09-24, root-caused live installing v1.0.39 on
# gdlab-spark1): this used to be a silent loopback default the operator had to
# separately discover the standalone `configure-ingress` verb exists to change
# later. Same precedence as registration above: explicit env (headless/CI)
# wins; otherwise prompt on a TTY; ALWAYS falls through to loopback when
# unanswered -- unlike registration/license, this choice is never required, so
# there is no exit-1 path here. Join mode is exempt: ingress is a CLUSTER-level
# decision the already-registered cluster already made, not a per-node one.
# -----------------------------------------------------------------------------
if [ -z "${_JOINING}" ]; then
    _ingress_backend="${SRASTA_INGRESS_BACKEND:-}"
    if [ -z "${_ingress_backend}" ] && _srasta_has_tty; then
        printf '\nHow should this platform be reached?\n'
        printf '  A) Just this host / internal network (default) -- no public exposure\n'
        printf '  B) Public internet, my own domain (any registrar; Lets Encrypt)\n'
        printf '  C) Public internet via Cloudflare Tunnel (no inbound ports needed)\n'
        printf '  D) Deploying into an existing Kubernetes cluster (you configure ingress\n'
        printf '     yourself via your clusters own Ingress/cert-manager -- not automated here)\n'
        printf 'Choice [A]: '
        IFS= read -r _ingress_choice < /dev/tty || true
        case "${_ingress_choice}" in
            [Bb]*) _ingress_backend="nginx-letsencrypt" ;;
            [Cc]*) _ingress_backend="cloudflare-tunnel" ;;
            [Dd]*) _ingress_backend="k8s-ingress" ;;
            *) _ingress_backend="loopback" ;;
        esac
        if [ "${_ingress_backend}" != "loopback" ] && [ "${_ingress_backend}" != "k8s-ingress" ]; then
            _base_domain="${SRASTA_BASE_DOMAIN:-}"
            if [ -z "${_base_domain}" ]; then
                printf 'Domain (e.g. srasta.example.com): '
                IFS= read -r _base_domain < /dev/tty || true
            fi
            _base_domain="$(printf '%s' "${_base_domain}" | tr -d '[:space:]')"
            if [ -z "${_base_domain}" ]; then
                echo "No domain supplied -- falling back to internal/loopback access only." >&2
                _ingress_backend="loopback"
            else
                export SRASTA_BASE_DOMAIN="${_base_domain}"
            fi
        fi
        if [ "${_ingress_backend}" = "cloudflare-tunnel" ] && [ -z "${SRASTA_CLOUDFLARE_API_TOKEN_FILE:-}" ]; then
            _cf_token="$(_srasta_read_tty_secret 'Cloudflare API token (Zone:DNS:Edit, Zone:Zone:Read, Account:Cloudflare Tunnel:Edit): ')"
            _cf_token="$(printf '%s' "${_cf_token}" | tr -d '[:space:]')"
            if [ -z "${_cf_token}" ]; then
                echo "No Cloudflare API token supplied -- falling back to internal/loopback access only." >&2
                _ingress_backend="loopback"
            else
                _cf_token_file="${_runtime_state_dir}/.srasta/cloudflare-api-token"
                mkdir -p "$(dirname "${_cf_token_file}")"
                printf '%s' "${_cf_token}" > "${_cf_token_file}"
                chmod 600 "${_cf_token_file}"
                export SRASTA_CLOUDFLARE_API_TOKEN_FILE="${_cf_token_file}"
            fi
        fi
    fi
    export SRASTA_INGRESS_BACKEND="${_ingress_backend:-loopback}"
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.  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.
#
# 2026-09-16: the agent binary now CARRIES its own compose project + setup as
# embedded data (installer/agent_package_data.py's discover_data_dirs(), see
# install_plane.py's APP_DIR comment) -- it is the complete distributable unit
# on its own.  Default mode below downloads it directly, cosign-verified, no
# Docker image pull needed just to extract it.  Bundle mode (below, unchanged)
# still extracts from its digest-pinned INSTALLER_IMAGE via docker cp -- it
# already has a stronger, self-contained cryptographic guarantee than a live
# license-server lookup could add, and doesn't need this treatment.

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"
# The new binary is downloaded, verified and self-checked at AGENT_BIN_NEW and only then renamed over AGENT_BIN
# (_srasta_commit_agent_binary). AGENT_BIN_PREV keeps the binary it replaced.
AGENT_BIN_NEW="${AGENT_BIN}.new"
AGENT_BIN_PREV="${AGENT_BIN}.prev"
mkdir -p "${AGENT_HOME}/bin"
rm -f "${AGENT_BIN_NEW}"

# Portable sha256: prefer sha256sum (Linux, GNU coreutils on macOS); fall back to
# `shasum -a 256` (macOS's actual default -- unlike bundle mode above, default mode
# must not hard-require a tool most fresh macOS installs don't have).
_srasta_sha256() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" | awk '{print $1}'
    else
        shasum -a 256 "$1" | awk '{print $1}'
    fi
}

echo "Installing the Srasta Agent (${TRIPLE})..."

if [ -n "$BUNDLE_VERSION" ] || [ -n "$_SRASTA_IMAGE_EXPLICIT" ]; then
    # ── Bundle mode, or an explicit SRASTA_INSTALLER_IMAGE override: the agent
    # binary comes from that same installer image, extracted via a throwaway
    # container (docker create + cp + rm) ───────────────────────────────────────
    #
    # The explicit-image case is the local-testing escape hatch (same class as
    # SRASTA_INSTALLER_IMAGE itself): without this, a locally-built installer
    # image was wired in for the compose/config/install-plan path, but the agent
    # binary -- the thing that actually RUNS the install -- still came from
    # licensing.srasta.ai's published *current release*, unconditionally,
    # regardless of SRASTA_INSTALLER_IMAGE. That is a different commit than the
    # one under test in the common case (testing unreleased fixes), so "test
    # HW runs only BUILT ARTIFACTS" silently didn't hold for the one artifact
    # that mattered most. Found live 2026-09-24 validating the discovery-probe
    # + ingress-activation fixes on gdlab-spark1: the local image built fine,
    # but this step still pulled production's stale v1.0.39 binary (and then
    # 404'd fetching its .sig, since that release predates the D29 signing fix
    # -- a real, separate confirmation of D29's production impact).
    _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_NEW}" 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 rm -f "${_cid}" >/dev/null 2>&1 || true
else
    # ── Default mode: agent binary downloaded directly from the current release,
    # no Docker image pull needed just to extract it ────────────────────────────
    if ! command -v jq >/dev/null 2>&1; then
        echo "ERROR: jq is required (not found in PATH)." >&2
        echo "  jq: usually preinstalled or via your package manager" >&2
        exit 1
    fi
    _release_url="${SRASTA_LICENSE_SERVER_URL%/}/pub/release/current"
    _release_json="$(curl -fsSL --max-time 15 "${_release_url}")" || {
        echo "ERROR: could not fetch the current release from ${_release_url}." >&2
        exit 1
    }
    _agent_bin_url="$(printf '%s' "${_release_json}" | jq -r --arg t "${TRIPLE}" '.agent_binary_urls[$t] // empty')"
    if [ -z "${_agent_bin_url}" ]; then
        echo "ERROR: the current release has no published agent binary for ${TRIPLE}." >&2
        echo "  It may not have been built for this platform yet." >&2
        exit 1
    fi
    curl -fsSL -o "${AGENT_BIN_NEW}" "${_agent_bin_url}" || {
        echo "ERROR: could not download the agent binary from ${_agent_bin_url}." >&2
        exit 1
    }

    echo "Verifying checksum ..."
    _expected_sha="$(curl -fsSL "${_agent_bin_url}.sha256" | awk '{print $1}')"
    # The sha256 next to the binary comes from the SAME server as the binary, so it only catches corruption.
    # When the release record (licensing.srasta.ai -- a DIFFERENT origin) carries a per-platform sha256, it is the
    # authority; the two origins must agree (supply-chain plan W4).  Older records without it keep the old check.
    _record_sha="$(printf '%s' "${_release_json}" | jq -r --arg t "${TRIPLE}" '.agent_binary_sha256[$t] // empty' 2>/dev/null || true)"
    if [ -n "${_record_sha}" ]; then
        if [ -n "${_expected_sha}" ] && [ "${_expected_sha}" != "${_record_sha}" ]; then
            echo "ERROR: the agent binary's checksum differs between the release record and the download server." >&2
            echo "  Two independent origins disagree; refusing to trust either." >&2
            exit 1
        fi
        _expected_sha="${_record_sha}"
    fi
    if [ -z "${_expected_sha}" ] || [ "${_expected_sha}" != "$(_srasta_sha256 "${AGENT_BIN_NEW}")" ]; then
        echo "ERROR: agent binary failed checksum verification." >&2
        echo "  Refusing to trust a corrupted or tampered download." >&2
        exit 1
    fi

    # Same lenient-but-strict-if-present posture the installer image verification
    # above already uses for default mode: verify strictly when cosign is
    # available, warn (not fail) when it isn't -- --bundle is the guarantee that
    # never depends on cosign being present on this box.
    if [ -n "${_SRASTA_RELEASE_PUBKEY_PEM}" ]; then
        # MANDATORY keypair verification (no cosign required).  cosign's blob signature is a base64 ECDSA
        # signature over the SHA-256 of the blob, which `openssl dgst -verify` checks directly.
        if ! command -v openssl >/dev/null 2>&1; then
            echo "ERROR: openssl is required to verify the agent binary's signature and was not found." >&2
            echo "  Refusing to run an unverified agent binary." >&2
            exit 1
        fi
        _agent_keysig_b64="$(mktemp -t srasta-agent-keysig.XXXXXX)"
        _agent_keysig_der="$(mktemp -t srasta-agent-keysig-der.XXXXXX)"
        _agent_pubkey="$(mktemp -t srasta-agent-pub.XXXXXX)"
        trap 'rm -f "${_agent_keysig_b64}" "${_agent_keysig_der}" "${_agent_pubkey}"' EXIT
        printf '%s\n' "${_SRASTA_RELEASE_PUBKEY_PEM}" > "${_agent_pubkey}"
        curl -fsSL -o "${_agent_keysig_b64}" "${_agent_bin_url}.keysig" || {
            echo "ERROR: could not download the agent binary's signature from ${_agent_bin_url}.keysig." >&2
            echo "  Refusing to run an agent binary that has no signature." >&2
            exit 1
        }
        # base64 -d (GNU) / -D (older BSD); both decode the same signature.
        { base64 -d < "${_agent_keysig_b64}" > "${_agent_keysig_der}" 2>/dev/null \
            || base64 -D < "${_agent_keysig_b64}" > "${_agent_keysig_der}" 2>/dev/null; } || {
            echo "ERROR: the agent binary's signature is not valid base64." >&2
            exit 1
        }
        echo "Verifying the agent binary's signature (release key) ..."
        openssl dgst -sha256 -verify "${_agent_pubkey}" -signature "${_agent_keysig_der}" "${AGENT_BIN_NEW}" >/dev/null 2>&1 || {
            echo "ERROR: the agent binary's signature does NOT verify against Srasta's release key." >&2
            echo "  Refusing to run an agent binary that fails signature verification." >&2
            exit 1
        }
        rm -f "${_agent_keysig_b64}" "${_agent_keysig_der}" "${_agent_pubkey}"
        trap - EXIT
        echo "  Signature verified (release key)"
    elif command -v cosign >/dev/null 2>&1; then
        _agent_sig="$(mktemp -t srasta-agent-sig.XXXXXX)"
        trap 'rm -f "${_agent_sig}"' EXIT
        curl -fsSL -o "${_agent_sig}" "${_agent_bin_url}.sig" || {
            echo "ERROR: could not download the signature from ${_agent_bin_url}.sig." >&2
            exit 1
        }
        echo "Verifying cosign signature (Sigstore keyless) ..."
        cosign verify-blob \
            --certificate-identity-regexp 'https://gitlab\.com/gandiva-tech/srasta//\.gitlab-ci\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$' \
            --certificate-oidc-issuer 'https://gitlab.com' \
            --bundle "${_agent_sig}" \
            "${AGENT_BIN_NEW}" >/dev/null || {
            echo "ERROR: cosign signature verification FAILED for the agent binary." >&2
            echo "  Refusing to run an agent binary that fails signature verification." >&2
            exit 1
        }
        rm -f "${_agent_sig}"
        trap - EXIT
        echo "  Signature verified"
    else
        echo "  ⚠ The release signing key is not provisioned in this script yet, and cosign is not"
        echo "    installed, so this run did NOT verify the agent binary's signature."
        echo "  ℹ cosign not installed, so this run did not verify the agent binary's"
        echo "    signature locally (its checksum was still verified above). For a fully"
        echo "    verified install, install cosign (brew install cosign /"
        echo "    github.com/sigstore/cosign) and re-run, or use --bundle vX.Y.Z for a"
        echo "    digest-pinned install regardless of whether cosign is present."
    fi
fi
chmod +x "${AGENT_BIN_NEW}"

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

# Commit: keep the previous binary, then rename the verified one into place. A rename works even while the old agent
# is RUNNING from that path; writing to it does not (Linux: "text file busy", seen as `curl: (23) Failure writing
# output` on a host whose agent was up). The previous binary stays at ${AGENT_BIN_PREV} for the failure path and
# for a manual rollback.
_srasta_commit_agent_binary || 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. Two credential shapes reach
# here (2026-09-16): the raw base64 --bundle, or a short --pairing-code (+
# --control-plane-url) Admin's "Add a Node" card mints by default now -- the agent
# binary itself resolves the code to a bundle (join_bundle.py::resolve_pairing_code),
# not this shell script.
if [ -n "${_JOINING}" ]; then
    echo "Joining the Srasta cluster..."
    if [ -n "${PAIRING_CODE}" ]; then
        "${AGENT_BIN}" join --pairing-code "${PAIRING_CODE}" \
            --control-plane-url "${CONTROL_PLANE_URL}" \
            --host-role "${JOIN_HOST_ROLE}" </dev/null || {
            echo "ERROR: join failed — the node did not enrol.  Check the pairing code"
            echo "  (it may have expired or already been used) + that the control-plane"
            echo "  is reachable from this node, or the PRECHECK punch-list above if"
            echo "  that's what failed."
            exit 1
        }
    else
        "${AGENT_BIN}" join --bundle "${JOIN_BUNDLE}" --host-role "${JOIN_HOST_ROLE}" </dev/null || {
            echo "ERROR: join failed — the node did not enrol.  Check the bundle + that the"
            echo "  control-plane is reachable from this node, or the PRECHECK punch-list"
            echo "  above if that's what failed."
            exit 1
        }
    fi
    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

# Standalone PRECHECK (2026-09-16), front-loaded before `install drive` runs at all --
# same read-only §12 check `install drive` runs internally as its own first step
# (HostAgentRunner._handle_precheck, same "single" host-role -- this is the single-node/
# first-node path), just surfaced to the operator BEFORE anything touches the host
# instead of scrolling past minutes into an install log. Mirrors the Tauri wizard's own
# "Validate" stage (wizard.rs::run_precheck) gating its "Start Install" button -- the
# terminal path gets the same up-front look the GUI already gives.
if [ -z "${SKIP_PRECHECK}" ]; then
    echo "Running PRECHECK on this host..."
    echo ""
    _precheck_rc=0
    # SRASTA_STATE_DIR must match what `install drive` below uses (STATE_HOST_DIR) --
    # both processes' PRECHECK bootstrap a Catalog Authority install-token if none is
    # cached yet (installer/catalog_authority/http_client.py's
    # STATE_DIR/.srasta/install-token.json). Without this, this standalone call fell
    # back to ~/.srasta/install-token.json (no SRASTA_STATE_DIR set) while `install
    # drive`'s own internal PRECHECK used STATE_HOST_DIR (/opt/srasta by default) --
    # two different cache paths, so a truly fresh host bootstrapped TWICE seconds
    # apart and could trip the catalog bootstrap endpoint's rate limit. Root-caused
    # live on gdlab-spark1 (2026-09-24): `--skip-precheck` masked it by dropping to a
    # single bootstrap, not by fixing the cache-path mismatch.
    SRASTA_STATE_DIR="${STATE_HOST_DIR}" \
        "${AGENT_BIN}" precheck --host-role single --pretty </dev/null || _precheck_rc=$?
    if [ "${_precheck_rc}" -ne 0 ]; then
        echo ""
        echo "ERROR: PRECHECK found a blocking issue on this host — see the punch-list above."
        echo "  For the full diagnostic bundle to share for help, run:"
        echo "    ${AGENT_BIN} precheck --host-role single --bundle"
        echo "  Fix the CRITICAL finding(s) above and re-run this installer, or pass"
        echo "  --skip-precheck to bypass this standalone check (install drive still runs"
        echo "  its own internal PRECHECK regardless -- this flag only skips the early look)."
        exit 1
    fi
    echo ""
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 (extracted from the resolved
# installer image above -- see that section for why the binary's own
# onefile-embedded copy can't be trusted for release-manifest.json) 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_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_RUNNER_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_RUNNER_DISCOVERY_TOKEN_FILE="$(grep -E '^SRASTA_RUNNER_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 "${_DAY2_RUNNER_DISCOVERY_TOKEN_FILE}" ]; then
    case "${_DAY2_RUNNER_DISCOVERY_TOKEN_FILE}" in
        /*) ;;
        *) _DAY2_RUNNER_DISCOVERY_TOKEN_FILE="${STATE_HOST_DIR}/${_DAY2_RUNNER_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}" ] \
    && [ -n "${_DAY2_RUNNER_DISCOVERY_TOKEN_FILE}" ] && [ -f "${_DAY2_RUNNER_DISCOVERY_TOKEN_FILE}" ] \
    && [ -r "${_DAY2_RUNNER_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_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}" \
            --runner-discovery-token-file "${_DAY2_RUNNER_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 ""
# The agent already assembled the handover pack (sign-in URL, admin console URL,
# proof receipts, day-2 runbook) as one of install drive's own internal steps and
# streamed a pretty summary of it past earlier in this output -- but that scrolled
# by minutes ago along with everything else. Re-read the durable, secrets-masked
# copy it wrote to disk (installer/srasta_handover.py's --out target) and repeat
# the two URLs an operator actually needs right here, in the last thing they read.
_HANDOVER_JSON="${STATE_HOST_DIR}/.srasta/handover/handover.json"
if [ -r "${_HANDOVER_JSON}" ] && command -v jq >/dev/null 2>&1; then
    _SIGN_IN_URL="$(jq -r '.access.operator_admin.sign_in_url // empty' "${_HANDOVER_JSON}" 2>/dev/null)"
    _ADMIN_CONSOLE_URL="$(jq -r '.access.operator_admin.admin_console_url // empty' "${_HANDOVER_JSON}" 2>/dev/null)"
    if [ -n "${_SIGN_IN_URL}" ] || [ -n "${_ADMIN_CONSOLE_URL}" ]; then
        echo "     Sign in:        ${_SIGN_IN_URL:-—}"
        echo "     Admin console:  ${_ADMIN_CONSOLE_URL:-—}"
        echo ""
        echo "     Full handover pack (sign-in is write-once — keep this file safe):"
        echo "       ${_HANDOVER_JSON}"
        echo ""
    fi
fi
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 ""
