#!/bin/sh
# SPDX-License-Identifier: BSD-3-Clause-Open-MPI
# Racora installer — the platform front door.
#
# Racora is a RAN platform that ships as a distribution. This script selects the
# cluster platform, validates the host or cluster against it, and hands off to
# the platform module (platforms/<name>/install.sh). Run it as a normal user;
# every privileged step elevates per command through $SUDO.
#
#   k3s (default) — stand up k3s + the RAN on this host, join a worker, uninstall:
#       curl -sfL https://get.racora.io | sh -
#       curl -sfL https://get.racora.io | INSTALL_RACORA_VERSION=vX.Y.Z sh -
#       curl -sfL https://get.racora.io | INSTALL_RACORA_MODE=node INSTALL_RACORA_VERSION=vX.Y.Z K3S_URL=https://<control>:6443 K3S_TOKEN=<t> sh -
#       INSTALL_RACORA_ARTIFACT_PATH=/path/to/bundle sh install-racora.sh        # offline bundle
#   kubernetes    — lay the RAN onto a cluster you already run (helm, no host changes):
#       curl -sfL https://get.racora.io | INSTALL_RACORA_PLATFORM=kubernetes sh -
#   provision-rf  — provision this host as an RF/DU node only (platform-independent):
#       curl -sfL https://get.racora.io | INSTALL_RACORA_MODE=provision-rf INSTALL_RACORA_VERSION=vX.Y.Z sh -
#   uninstall     — scope follows ownership (the Uninstall page): ran | host | cluster
#       curl -sfL https://get.racora.io | INSTALL_RACORA_MODE=uninstall sh -
#
# The copy served at get.racora.io is a bootstrapper: for a release install it
# resolves the version, downloads that release's installer package
# (racora-installer-<tag>.tar.gz: this script plus platforms/, node-provisioning/
# and the core modules, checksum-verified) and runs it, so what runs on the host
# is exactly the installer that shipped and was tested with that release. The
# same script run from a checkout, an offline bundle or the persisted copy
# (/opt/racora/installer) is already that tree and installs directly.
#
# Racora environment — every variable the front door and the platform modules
# read (test/installer-matrix.sh checks this list stays complete). Column two is
# the default ("-": unset). Lines marked [test] are developer/test hooks.
#   INSTALL_RACORA_PLATFORM          k3s        k3s | kubernetes
#   INSTALL_RACORA_MODE              server     k3s: server | node | provision-rf | uninstall;
#                                               kubernetes: attach (default: lay the RAN onto the cluster) | provision-rf | uninstall
#   INSTALL_RACORA_UNINSTALL_SCOPE   -          ran | host | cluster; default = what racora installed here:
#                                               cluster where racora installed k3s (servers AND workers),
#                                               ran on attach. ran is refused on a worker (the RAN lives on the server)
#   INSTALL_RACORA_VERSION           -          release tag; default = latest release (bundles need none)
#   INSTALL_RACORA_ARTIFACT_PATH     -          offline bundle directory (k3s platform); alias RACORA_ARTIFACT_PATH
#   INSTALL_RACORA_ARTIFACT_NO_IMAGES  -        1: accept a bundle without the image archive (images then pull
#                                               from a registry the cluster can reach)
#   INSTALL_RACORA_PROVISION         off        off | check | ensure; node mode defaults to ensure. In server
#                                               mode `ensure` on a host that is not rf-ready STARTS provisioning
#                                               and exits (re-run once rf-ready); node mode joins by itself
#   INSTALL_RACORA_GPU               -          provisioner pass-through (auto | on | off); node mode defaults to off
#   INSTALL_RACORA_PRO_TOKEN, INSTALL_RACORA_ISOLCPUS, INSTALL_RACORA_FRONTHAUL, INSTALL_RACORA_PTP_MODE
#                                    -          provisioner pass-throughs (the node provisioner reference on the docs site)
#   INSTALL_RACORA_CORE              open5gs    the 5G core provider (cores/<name>): open5gs | external; on a k3s host
#                                               racora installed, a server re-run and an uninstall default to the
#                                               provider recorded at install time (/etc/racora/install.env)
#   INSTALL_RACORA_AMF_ADDR          -          external only: the AMF the CU-CP dials (DNS name or IP the
#                                               cluster reaches over SCTP); recorded on the k3s server and reused
#                                               by a server re-run and an uninstall when not given
#   INSTALL_RACORA_UE_CIDR           10.45.0.0/16   UE pool the core provider's host egress NAT masquerades
#                                               (open5gs); must contain the open5gs chart's ueIpBase (k3s server only)
#   INSTALL_RACORA_SKIP_VALIDATE     -          true: downgrade validator refusals (exit 2) to warnings
#   RACORA_DRY_RUN                   -          1: resolve + validate statically, print the plan, exit 0
#   RACORA_REF                       <release>  [developer] git ref the subtrees (platforms/, node-provisioning/,
#                                               cores/) are fetched from when the script is not run from a
#                                               versioned tree; setting it disables the release-package bootstrap.
#                                               Default: the release tag being installed, `main` when no release is
#                                               involved (provision-rf, uninstall). Alias RACORA_PROVISION_REF
#   RACORA_BOOTSTRAPPED              -          [internal] 1: this is the release's own installer, run by the front
#                                               door (never set it by hand)
#   RACORA_ARTIFACT_URL              https://gitlab.com/api/v4/projects/cognitive-network-solutions%2Fracora/packages/generic/racora  release package host (GitLab generic package registry)
#   RACORA_RELEASES_API              <api>      [developer] where the latest release tag is resolved
#   RACORA_REPO_ARCHIVE              <archive>  [developer] subtree source: <archive>/<ref>/racora.tar.gz?path=<subtree>
#   RACORA_INSTALL_URL               get.racora.io  [developer] where this script is re-fetched when $0 is not a copy of it
#   RACORA_PLATFORMS_DIR, RACORA_PROVISION_DIR, RACORA_CORES_DIR   -   [developer] local subtree overrides (a checkout)
#   RACORA_PLATFORM_DIR              -          [test] the resolved platforms/<name> directory
#   RACORA_CORE_DIR                  -          [test] the resolved cores/<name> directory
#   RACORA_CHART_TGZ, RACORA_CRD_FILE   -       [developer] local payload overrides (k3s platform)
#   RACORA_OCI_CHART                 oci://registry.gitlab.com/cognitive-network-solutions/racora/charts/racora  kubernetes platform: the chart source
#   RACORA_REGISTRY                  registry.gitlab.com/cognitive-network-solutions/racora  kubernetes platform: the image registry every image is pulled from
#   RACORA_HELM_EXTRA_ARGS           -          kubernetes platform: extra `helm upgrade` arguments (word-split), e.g. -f my-values.yaml
#   RACORA_UNINSTALL_KEEP_CRD, RACORA_UNINSTALL_KEEP_DATA, RACORA_UNINSTALL_DRY_RUN
#                                    -          1: the uninstall wrappers' --keep-crd / --keep-data / --dry-run, as env
#   RACORA_SYSTEMD_DIR, RACORA_RKE2_DIR   /etc/systemd/system, /var/lib/rancher/rke2   [test] host probes
#   RACORA_INSTALL_RECORD            /etc/racora/install.env   [test] the ownership record a k3s server wrote
#   RACORA_SOURCE_ONLY               -          [test] 1: define the functions, run nothing
# The k3s platform is amd64 only (the mirrored binaries). Every INSTALL_K3S_* /
# K3S_* variable of the upstream k3s installer still applies there (documented at the
# top of the k3s platform module) EXCEPT: INSTALL_K3S_VERSION is set from
# INSTALL_RACORA_VERSION (the racora tag addresses the mirrored binary),
# INSTALL_K3S_ARTIFACT_URL defaults to RACORA_ARTIFACT_URL, and
# INSTALL_K3S_CHANNEL / INSTALL_K3S_CHANNEL_URL are never consulted (the k3s
# version is the release's pin).
set -e
set -o noglob

# --- output helpers (shared with the platform modules) ---
info()        { echo '[INFO] ' "$@"; }
warn()        { echo '[WARN] ' "$@" >&2; }
fatal()       { echo '[ERROR] ' "$@" >&2; exit 1; }
# validator verdicts: the platform/mode does not fit this host or cluster (exit 2);
# the platform is registered but not shipped yet (exit 3). CI asserts on these.
refuse()      { if [ "${INSTALL_RACORA_SKIP_VALIDATE}" = true ]; then warn "SKIPPED REFUSAL:" "$@"; return 0; fi; echo '[REFUSED] ' "$@" >&2; exit 2; }
unsupported() { echo '[UNSUPPORTED] ' "$@" >&2; exit 3; }

# --- platform registry ---
RACORA_PLATFORMS_SUPPORTED="k3s kubernetes"
RACORA_PLATFORMS_PLANNED="sylva openshift"
# --- core provider registry (cores/README.md) ---
RACORA_CORES_SUPPORTED="open5gs external"

# --- environment ---
INSTALL_RACORA_PLATFORM=${INSTALL_RACORA_PLATFORM:-}
INSTALL_RACORA_MODE=${INSTALL_RACORA_MODE:-}
INSTALL_RACORA_UNINSTALL_SCOPE=${INSTALL_RACORA_UNINSTALL_SCOPE:-}
INSTALL_RACORA_VERSION=${INSTALL_RACORA_VERSION:-}
INSTALL_RACORA_ARTIFACT_PATH=${INSTALL_RACORA_ARTIFACT_PATH:-${RACORA_ARTIFACT_PATH:-}}
INSTALL_RACORA_ARTIFACT_NO_IMAGES=${INSTALL_RACORA_ARTIFACT_NO_IMAGES:-}
INSTALL_RACORA_PROVISION=${INSTALL_RACORA_PROVISION:-}
INSTALL_RACORA_PRO_TOKEN=${INSTALL_RACORA_PRO_TOKEN:-}
INSTALL_RACORA_ISOLCPUS=${INSTALL_RACORA_ISOLCPUS:-}
INSTALL_RACORA_GPU=${INSTALL_RACORA_GPU:-}
INSTALL_RACORA_FRONTHAUL=${INSTALL_RACORA_FRONTHAUL:-}
INSTALL_RACORA_PTP_MODE=${INSTALL_RACORA_PTP_MODE:-}
INSTALL_RACORA_CORE=${INSTALL_RACORA_CORE:-}
INSTALL_RACORA_AMF_ADDR=${INSTALL_RACORA_AMF_ADDR:-}
INSTALL_RACORA_UE_CIDR=${INSTALL_RACORA_UE_CIDR:-}
INSTALL_RACORA_SKIP_VALIDATE=${INSTALL_RACORA_SKIP_VALIDATE:-}
RACORA_DRY_RUN=${RACORA_DRY_RUN:-}
RACORA_REF=${RACORA_REF:-${RACORA_PROVISION_REF:-}}   # empty = follow the release (racora_resolve_ref)
RACORA_REF_GIVEN=${RACORA_REF}                         # an explicit ref = developer path, no bootstrap
RACORA_PROVISION_REF=${RACORA_REF}
RACORA_BOOTSTRAPPED=${RACORA_BOOTSTRAPPED:-}
RACORA_ARTIFACT_URL=${RACORA_ARTIFACT_URL:-https://gitlab.com/api/v4/projects/cognitive-network-solutions%2Fracora/packages/generic/racora}
RACORA_INSTALL_URL=${RACORA_INSTALL_URL:-https://get.racora.io}
RACORA_REPO_ARCHIVE=${RACORA_REPO_ARCHIVE:-https://gitlab.com/cognitive-network-solutions/racora/-/archive}
RACORA_RELEASES_API=${RACORA_RELEASES_API:-https://gitlab.com/api/v4/projects/cognitive-network-solutions%2Fracora/releases}
RACORA_INSTALLER_HOME=/opt/racora/installer      # persisted copy of this tree (node joins, uninstall wrappers)
RACORA_INSTALL_RECORD=${RACORA_INSTALL_RECORD:-/etc/racora/install.env}    # ownership record written by platforms that install a cluster
RACORA_NP=${RACORA_NP:-}
RACORA_PLATFORM_DIR=${RACORA_PLATFORM_DIR:-}
RACORA_CORE_DIR=${RACORA_CORE_DIR:-}
RACORA_FRONT_DOOR=1

# sudo if not already root. Defined before anything privileged runs; the k3s
# module's setup_env re-derives the same value.
racora_detect_sudo() {
    SUDO=sudo
    [ "$(id -u)" -eq 0 ] && SUDO=
    return 0
}

racora_setup_tmp() {
    RACORA_TMP=$(mktemp -d -t racora-install.XXXXXXXXXX)
    racora_cleanup() { code=$?; set +e; trap - EXIT; rm -rf "${RACORA_TMP}"; exit $code; }
    trap racora_cleanup INT EXIT
}

# --- mode resolution: aliases, per-platform defaults, provisioning defaults ---
racora_resolve_mode() {
    if [ "${INSTALL_RACORA_MODE}" = cluster-attach ]; then
        if [ -n "${INSTALL_RACORA_PLATFORM}" ] && [ "${INSTALL_RACORA_PLATFORM}" != kubernetes ]; then
            refuse "INSTALL_RACORA_MODE=cluster-attach means an existing cluster (INSTALL_RACORA_PLATFORM=kubernetes), but INSTALL_RACORA_PLATFORM=${INSTALL_RACORA_PLATFORM} was given"
        fi
        warn "INSTALL_RACORA_MODE=cluster-attach is deprecated; use INSTALL_RACORA_PLATFORM=kubernetes"
        INSTALL_RACORA_PLATFORM=kubernetes
        INSTALL_RACORA_MODE=attach
    fi
    # uninstall with no platform given: follow the record of what racora installed here
    if [ -z "${INSTALL_RACORA_PLATFORM}" ] && [ "${INSTALL_RACORA_MODE}" = uninstall ] && [ -r "${RACORA_INSTALL_RECORD}" ]; then
        INSTALL_RACORA_PLATFORM="$(sed -n 's/^RACORA_PLATFORM=//p' "${RACORA_INSTALL_RECORD}" | head -1)"
    fi
    INSTALL_RACORA_PLATFORM=${INSTALL_RACORA_PLATFORM:-k3s}
    # no core given on a k3s host racora installed: a server re-run (an upgrade) and an
    # uninstall follow the record, so the provider selected at install time stays selected
    if [ -z "${INSTALL_RACORA_CORE}" ] && [ "${INSTALL_RACORA_PLATFORM}" = k3s ] && [ -r "${RACORA_INSTALL_RECORD}" ]; then
        case "${INSTALL_RACORA_MODE:-server}" in
            server|uninstall)
                INSTALL_RACORA_CORE="$(sed -n 's/^RACORA_CORE=//p' "${RACORA_INSTALL_RECORD}" | head -1)"
                [ -n "${INSTALL_RACORA_AMF_ADDR}" ] || INSTALL_RACORA_AMF_ADDR="$(sed -n 's/^RACORA_AMF_ADDR=//p' "${RACORA_INSTALL_RECORD}" | head -1)" ;;
        esac
    fi
    _racora_core_given=$([ -n "${INSTALL_RACORA_CORE}" ] && echo 1 || echo 0)
    INSTALL_RACORA_CORE=${INSTALL_RACORA_CORE:-open5gs}
    if [ -z "${INSTALL_RACORA_MODE}" ]; then
        case "${INSTALL_RACORA_PLATFORM}" in
            kubernetes) INSTALL_RACORA_MODE=attach ;;
            *)          INSTALL_RACORA_MODE=server ;;
        esac
    fi
    # a joined worker is meant to be a DU/RF node: provisioning on, GPU off (overridable)
    if [ "${INSTALL_RACORA_MODE}" = node ]; then
        [ -z "${INSTALL_RACORA_PROVISION}" ] && INSTALL_RACORA_PROVISION=ensure
        [ -z "${INSTALL_RACORA_GPU}" ] && INSTALL_RACORA_GPU=off
    else
        [ -z "${INSTALL_RACORA_PROVISION}" ] && INSTALL_RACORA_PROVISION=off
    fi
    return 0
}

# --- static validation: everything decidable without touching the host or network ---
racora_static_validate() {
    case " ${RACORA_PLATFORMS_SUPPORTED} " in
        *" ${INSTALL_RACORA_PLATFORM} "*) ;;
        *)
            case " ${RACORA_PLATFORMS_PLANNED} " in
                *" ${INSTALL_RACORA_PLATFORM} "*)
                    unsupported "INSTALL_RACORA_PLATFORM=${INSTALL_RACORA_PLATFORM} is not yet supported in this release — see platforms/${INSTALL_RACORA_PLATFORM}/README.md for status and prerequisites" ;;
                *)
                    fatal "unknown INSTALL_RACORA_PLATFORM='${INSTALL_RACORA_PLATFORM}' (supported: ${RACORA_PLATFORMS_SUPPORTED}; planned: ${RACORA_PLATFORMS_PLANNED})" ;;
            esac ;;
    esac
    case "${INSTALL_RACORA_PLATFORM}/${INSTALL_RACORA_MODE}" in
        k3s/server|k3s/node|k3s/provision-rf|k3s/uninstall) ;;
        kubernetes/attach|kubernetes/provision-rf|kubernetes/uninstall) ;;
        kubernetes/node|kubernetes/server)
            refuse "INSTALL_RACORA_MODE=${INSTALL_RACORA_MODE} is a k3s-platform mode. On INSTALL_RACORA_PLATFORM=kubernetes your cluster tooling installs and joins nodes; provision an RF host with INSTALL_RACORA_MODE=provision-rf, then label it (docs/howto/existing-cluster.md)" ;;
        *)
            fatal "INSTALL_RACORA_MODE='${INSTALL_RACORA_MODE}' is not valid for INSTALL_RACORA_PLATFORM=${INSTALL_RACORA_PLATFORM}" ;;
    esac
    case " ${RACORA_CORES_SUPPORTED} " in
        *" ${INSTALL_RACORA_CORE} "*) ;;
        *) fatal "unknown INSTALL_RACORA_CORE='${INSTALL_RACORA_CORE}' (supported core providers: ${RACORA_CORES_SUPPORTED}; see cores/README.md)" ;;
    esac
    case "${INSTALL_RACORA_MODE}" in server|attach)
        if [ "${INSTALL_RACORA_CORE}" = external ] && [ -z "${INSTALL_RACORA_AMF_ADDR}" ]; then
            fatal "INSTALL_RACORA_CORE=external needs INSTALL_RACORA_AMF_ADDR=<the AMF the CU-CP dials: a DNS name or IP the cluster reaches over SCTP>"
        fi
        if [ "${INSTALL_RACORA_CORE}" != external ] && [ -n "${INSTALL_RACORA_AMF_ADDR}" ]; then
            fatal "INSTALL_RACORA_AMF_ADDR is only meaningful with INSTALL_RACORA_CORE=external (the ${INSTALL_RACORA_CORE} provider exposes its own AMF)"
        fi ;;
    esac
    if [ "${INSTALL_RACORA_MODE}" = node ]; then
        if [ -z "${K3S_URL}" ]; then
            fatal "INSTALL_RACORA_MODE=node joins an existing Racora cluster, but K3S_URL is not set.
  On your control node run:  sudo cat /var/lib/rancher/k3s/server/node-token
  then re-run here with:     K3S_URL=https://<control-node-ip>:6443 K3S_TOKEN=<token>"
        fi
        case "${K3S_URL}" in https://*) ;; *) fatal "K3S_URL must look like https://<control-node-ip>:6443 (got '${K3S_URL}')" ;; esac
        if [ -z "${K3S_TOKEN}" ] && [ -z "${K3S_TOKEN_FILE}" ]; then
            fatal "INSTALL_RACORA_MODE=node requires K3S_TOKEN (from: sudo cat /var/lib/rancher/k3s/server/node-token on the control node)."
        fi
    fi
    if [ "${INSTALL_RACORA_MODE}" = uninstall ] && [ -n "${INSTALL_RACORA_UNINSTALL_SCOPE}" ]; then
        case "${INSTALL_RACORA_UNINSTALL_SCOPE}" in ran|host|cluster) ;; *) fatal "INSTALL_RACORA_UNINSTALL_SCOPE must be ran, host or cluster" ;; esac
    fi
    if [ -n "${INSTALL_RACORA_ARTIFACT_PATH}" ]; then
        [ -d "${INSTALL_RACORA_ARTIFACT_PATH}" ] || fatal "INSTALL_RACORA_ARTIFACT_PATH=${INSTALL_RACORA_ARTIFACT_PATH} is not a directory"
        if [ "${INSTALL_RACORA_PLATFORM}" = k3s ] && [ "${INSTALL_RACORA_MODE}" != uninstall ]; then
            for _f in k3s sha256sum-amd64.txt; do
                [ -f "${INSTALL_RACORA_ARTIFACT_PATH}/${_f}" ] || fatal "offline bundle ${INSTALL_RACORA_ARTIFACT_PATH} is missing '${_f}'"
            done
            # the image archive may be split into .partNN pieces (release asset size cap);
            # a bundle without one is only usable when images can be pulled — say so explicitly
            if [ "${INSTALL_RACORA_ARTIFACT_NO_IMAGES}" != 1 ] && [ ! -f "${INSTALL_RACORA_ARTIFACT_PATH}/racora-airgap-images.tar.zst" ] && ! racora_glob_first "${INSTALL_RACORA_ARTIFACT_PATH}" 'racora-airgap-images.tar.zst.part*' >/dev/null; then
                fatal "offline bundle ${INSTALL_RACORA_ARTIFACT_PATH} is missing 'racora-airgap-images.tar.zst' (or its .partNN pieces); set INSTALL_RACORA_ARTIFACT_NO_IMAGES=1 to install from it anyway and pull images from a registry"
            fi
            if [ "${INSTALL_RACORA_MODE}" = server ]; then
                [ -f "${INSTALL_RACORA_ARTIFACT_PATH}/racora-crds.yaml" ] || fatal "offline bundle is missing 'racora-crds.yaml'"
                racora_glob_first "${INSTALL_RACORA_ARTIFACT_PATH}" 'racora-*.tgz' >/dev/null || fatal "offline bundle is missing the chart 'racora-<version>.tgz'"
            fi
        fi
    fi
    if [ "${INSTALL_RACORA_PLATFORM}" = k3s ] && [ "${INSTALL_RACORA_MODE}" != provision-rf ] && [ "$(uname -m)" != x86_64 ]; then
        refuse "racora publishes amd64 k3s binaries only (this host: $(uname -m))"
    fi
    return 0
}

# --- release version: explicit, else the latest release (online; bundles carry their own) ---
racora_resolve_version() {
    [ -n "${INSTALL_RACORA_VERSION}" ] && return 0
    [ -n "${INSTALL_RACORA_ARTIFACT_PATH}" ] && return 0
    [ "${INSTALL_RACORA_MODE}" = provision-rf ] || [ "${INSTALL_RACORA_MODE}" = uninstall ] && return 0
    [ "${RACORA_DRY_RUN}" = 1 ] && return 0
    [ -n "${INSTALL_K3S_VERSION}" ] && return 0     # k3s-installer style pin still honoured
    INSTALL_RACORA_VERSION=$(curl -sfL "${RACORA_RELEASES_API}?per_page=1" 2>/dev/null | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)
    [ -n "${INSTALL_RACORA_VERSION}" ] || fatal "could not determine the latest racora release — set INSTALL_RACORA_VERSION, or use INSTALL_RACORA_ARTIFACT_PATH for an offline bundle"
    info "installing racora ${INSTALL_RACORA_VERSION} (latest release)"
}

# --- subtree ref: the release being installed, unless RACORA_REF was given ---
# A pinned install must be pinned end to end: platforms/, node-provisioning/ and
# cores/ are fetched from the repo archive at this ref, so they match the
# chart, CRD and k3s binary of the same release. `final` (after the version is
# resolved) falls back to main when no release is involved (provision-rf,
# uninstall, a bundle, an old-style INSTALL_K3S_VERSION pin).
racora_resolve_ref() {
    [ -n "${RACORA_REF}" ] && return 0
    if [ -n "${INSTALL_RACORA_VERSION}" ]; then
        RACORA_REF=${INSTALL_RACORA_VERSION}
    elif [ "${1:-}" = final ]; then
        RACORA_REF=main
    fi
    RACORA_PROVISION_REF=${RACORA_REF}
    return 0
}

# --- subtree resolution: local checkout -> bundle -> persisted copy -> fetch by ref ---
# racora_resolve_subtree VAR subdir marker-file
# Sets VAR to a directory holding <subdir>/<marker-file>. Never fetches when a
# local copy exists, so bundle and checkout installs need no network.
racora_subtree_source=""
racora_resolve_subtree() {
    _var=$1; _sub=$2; _marker=$3
    eval "_cur=\${${_var}}"
    if [ -n "${_cur}" ] && [ -f "${_cur}/${_marker}" ]; then racora_subtree_source=override; return 0; fi
    _self="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
    if [ -f "$0" ] && [ -f "${_self}/${_sub}/${_marker}" ]; then
        eval "${_var}=\"${_self}/${_sub}\""; racora_subtree_source=checkout; return 0
    fi
    if [ -n "${INSTALL_RACORA_ARTIFACT_PATH}" ] && [ -f "${INSTALL_RACORA_ARTIFACT_PATH}/installer/${_sub}/${_marker}" ]; then
        eval "${_var}=\"${INSTALL_RACORA_ARTIFACT_PATH}/installer/${_sub}\""; racora_subtree_source=bundle; return 0
    fi
    if [ -f "${RACORA_INSTALLER_HOME}/${_sub}/${_marker}" ]; then
        eval "${_var}=\"${RACORA_INSTALLER_HOME}/${_sub}\""; racora_subtree_source=persisted; return 0
    fi
    [ "${RACORA_DRY_RUN}" = 1 ] && { racora_subtree_source=fetch; return 0; }
    _dest="${RACORA_TMP}/subtrees"
    _url="${RACORA_REPO_ARCHIVE}/${RACORA_REF}/racora.tar.gz?path=${_sub}"
    info "fetching ${_sub} (ref ${RACORA_REF})..."
    mkdir -p "${_dest}"
    if ! curl -sfL "${_url}" | tar -xz -C "${_dest}" --strip-components=1 2>/dev/null; then
        warn "could not fetch ${_sub} from ${_url}"; return 1
    fi
    if [ -f "${_dest}/${_sub}/${_marker}" ]; then
        eval "${_var}=\"${_dest}/${_sub}\""; racora_subtree_source=fetch; return 0
    fi
    warn "fetched archive has no ${_sub}/${_marker} (bad RACORA_REF '${RACORA_REF}'?)"; return 1
}
racora_resolve_platform()     { RACORA_PLATFORM_DIR="${RACORA_PLATFORM_DIR:-${RACORA_PLATFORMS_DIR:+${RACORA_PLATFORMS_DIR}/${INSTALL_RACORA_PLATFORM}}}"; racora_resolve_subtree RACORA_PLATFORM_DIR "platforms/${INSTALL_RACORA_PLATFORM}" install.sh; }
racora_resolve_provisioning() { RACORA_NP="${RACORA_NP:-${RACORA_PROVISION_DIR:-}}"; racora_resolve_subtree RACORA_NP node-provisioning provision-rf-node.sh; }
racora_resolve_core()         { RACORA_CORE_DIR="${RACORA_CORE_DIR:-${RACORA_CORES_DIR:+${RACORA_CORES_DIR}/${INSTALL_RACORA_CORE}}}"; racora_resolve_subtree RACORA_CORE_DIR "cores/${INSTALL_RACORA_CORE}" values.yaml; }

# --- core provider module (cores/README.md) ---------------------------------
# racora_core_render_values <out>: the provider's values overlay with its one
# placeholder filled (@AMF_ADDR@ <- INSTALL_RACORA_AMF_ADDR); anything else left
# unfilled is a broken module, not a value to ship.
racora_core_render_values() {
    [ -n "${RACORA_CORE_DIR}" ] && [ -f "${RACORA_CORE_DIR}/values.yaml" ] || fatal "core provider '${INSTALL_RACORA_CORE}' has no values.yaml (RACORA_CORE_DIR='${RACORA_CORE_DIR}')"
    sed "s|@AMF_ADDR@|${INSTALL_RACORA_AMF_ADDR}|g" "${RACORA_CORE_DIR}/values.yaml" > "$1"
    if grep -q '@[A-Z_]*@' "$1"; then fatal "core provider '${INSTALL_RACORA_CORE}': unfilled placeholder in values.yaml: $(grep -o '@[A-Z_]*@' "$1" | head -1)"; fi
}
# racora_core_host_units: the unit names the provider ships (host/<name>.service), one per line; nothing for a provider without host needs
racora_core_host_units() {
    [ -n "${RACORA_CORE_DIR}" ] && [ -d "${RACORA_CORE_DIR}/host" ] || return 0
    set +f
    for _svc in "${RACORA_CORE_DIR}"/host/*.service; do
        [ -e "${_svc}" ] || continue
        _u=$(basename "${_svc}" .service); echo "${_u}"
    done
    set -f
    return 0
}

# --- RF node provisioning (platform-independent host work) ---
# Start the self-driving provisioner with the install's pass-through options. Extra
# args are forwarded (node mode passes --on-complete <join-finalizer>).
racora_run_provisioner() {
    racora_resolve_provisioning || fatal "provisioning scripts unavailable (set RACORA_PROVISION_DIR, or RACORA_REF to a ref that has node-provisioning/)"
    set -- $SUDO bash "${RACORA_NP}/provision-rf-node.sh" --install-service "$@"
    if [ -n "${INSTALL_RACORA_PRO_TOKEN}" ]; then set -- "$@" --pro-token "${INSTALL_RACORA_PRO_TOKEN}"; fi
    if [ -n "${INSTALL_RACORA_ISOLCPUS}" ]; then set -- "$@" --isolcpus "${INSTALL_RACORA_ISOLCPUS}"; fi
    if [ -n "${INSTALL_RACORA_GPU}" ]; then set -- "$@" --gpu "${INSTALL_RACORA_GPU}"; fi
    if [ -n "${INSTALL_RACORA_FRONTHAUL}" ]; then set -- "$@" --fronthaul "${INSTALL_RACORA_FRONTHAUL}"; fi
    if [ -n "${INSTALL_RACORA_PTP_MODE}" ]; then set -- "$@" --ptp-mode "${INSTALL_RACORA_PTP_MODE}"; fi
    "$@"
}

# Pre-flight for a cluster install on this host: is it RF-provisioned? Skip if
# yes; provision (ensure) or warn (check) if not. Uses validate-rf-node.sh.
racora_provision_preflight() {
    if [ "${INSTALL_RACORA_PROVISION}" = off ]; then return 0; fi
    if ! racora_resolve_provisioning; then
        warn "RF pre-flight: provisioning scripts unavailable — skipping."
        return 0
    fi
    _vargs=""
    if [ -n "${INSTALL_RACORA_GPU}" ]; then _vargs="--gpu ${INSTALL_RACORA_GPU}"; fi
    info "RF pre-flight: checking node provisioning..."
    if bash "${RACORA_NP}/validate-rf-node.sh" ${_vargs} >/dev/null 2>&1; then
        info "node is already RF-provisioned (rf_ready) — continuing install."
        return 0
    fi
    if [ "${INSTALL_RACORA_PROVISION}" = check ]; then
        warn "node is NOT RF-provisioned — DU/CU-IP workloads need a provisioned node."
        warn "  provision it: INSTALL_RACORA_PROVISION=ensure (or provision-rf-node.sh --install-service)"
        return 0
    fi
    info "node not RF-ready — starting self-driving provisioning (the node will reboot)."
    racora_run_provisioner
    info "Provisioning started. Once rf-ready, re-run this install to bring up the cluster."
    exit 0
}

# Load the SCTP kernel module (and persist it across reboots) on every Racora
# node. The RAN control plane rides SCTP — F1-C (DU↔CU-CP), E1 (CU-CP↔CU-UP),
# N2/NGAP (CU-CP↔AMF) — over ClusterIP Services, and kube-proxy needs the sctp
# module to handle SCTP ClusterIP; it is not auto-loaded.
racora_load_sctp() {
    if [ -d /etc/modules-load.d ]; then
        echo "sctp" | $SUDO tee /etc/modules-load.d/racora-sctp.conf >/dev/null 2>&1 || true
    fi
    $SUDO modprobe sctp 2>/dev/null || warn "could not modprobe sctp — cross-node SCTP (F1/E1/N2) may need it loaded manually"
}

# racora_sha256_verify <file> <sums-file> <name-in-sums>
racora_sha256_verify() {
    _want=$(grep " $3\$" "$2" 2>/dev/null | head -1); _want=${_want%%[[:blank:]]*}
    [ -n "${_want}" ] || fatal "no checksum for '$3' in $2"
    _got=$(sha256sum "$1"); _got=${_got%%[[:blank:]]*}
    [ "${_want}" = "${_got}" ] || fatal "checksum mismatch for $1: expected ${_want}, got ${_got}"
}

# racora_glob_first <dir> <pattern>: print the first match of a shell pattern
# inside <dir>, or fail. The script runs with `set -o noglob` (the k3s installer
# convention), so wildcards must be expanded explicitly and locally.
racora_glob_first() {
    set +f
    for _g in "$1"/$2; do
        if [ -e "${_g}" ]; then set -f; echo "${_g}"; return 0; fi
    done
    set -f
    return 1
}

# Persist this installer tree (front door + every resolved subtree) so the
# post-provision join hook and the uninstall wrappers can run without the network.
racora_persist_installer() {
    $SUDO mkdir -p "${RACORA_INSTALLER_HOME}"
    if [ -f "$0" ] && grep -q 'INSTALL_RACORA_PLATFORM' "$0" 2>/dev/null; then
        $SUDO cp "$0" "${RACORA_INSTALLER_HOME}/install-racora.sh"
    else
        $SUDO curl -sfL "${RACORA_INSTALL_URL}" -o "${RACORA_INSTALLER_HOME}/install-racora.sh" \
            || fatal "could not fetch the installer from ${RACORA_INSTALL_URL} to persist it"
    fi
    $SUDO chmod 700 "${RACORA_INSTALLER_HOME}/install-racora.sh"
    for _pair in "RACORA_PLATFORM_DIR:platforms/${INSTALL_RACORA_PLATFORM}" "RACORA_NP:node-provisioning" "RACORA_CORE_DIR:cores/${INSTALL_RACORA_CORE}"; do
        _v=${_pair%%:*}; _s=${_pair#*:}
        eval "_d=\${${_v}}"
        [ -n "${_d}" ] && [ -d "${_d}" ] || continue
        case "${_d}" in "${RACORA_INSTALLER_HOME}"/*) continue ;; esac   # already the persisted copy
        $SUDO mkdir -p "${RACORA_INSTALLER_HOME}/${_s}"
        $SUDO cp -R "${_d}/." "${RACORA_INSTALLER_HOME}/${_s}/"
    done
    $SUDO chmod -R go-rwx "${RACORA_INSTALLER_HOME}"
}

# --- bootstrap: run the release's own installer package -------------------------
# racora_bootstrap_plan decides (no side effects) and sets RACORA_BOOTSTRAP to:
#   checkout  this script sits in a versioned tree (a checkout, an unpacked
#             installer package, /opt/racora/installer): install directly
#   bundle    an offline bundle carries its installer: run that one
#   ref       RACORA_REF was given: developer path, subtrees fetched from the ref
#   none      no release is involved (provision-rf, uninstall, an INSTALL_K3S_VERSION
#             pin): the subtrees come from `main`, as before
#   done      RACORA_BOOTSTRAPPED=1: this already is the release's installer
#   tarball   a release install from the served copy: download + verify + run
racora_bootstrap_plan() {
    RACORA_BOOTSTRAP=tarball
    if [ "${RACORA_BOOTSTRAPPED}" = 1 ]; then RACORA_BOOTSTRAP=done; return 0; fi
    _self="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
    if [ -f "$0" ] && [ -d "${_self}/platforms" ]; then RACORA_BOOTSTRAP=checkout; return 0; fi
    if [ -n "${INSTALL_RACORA_ARTIFACT_PATH}" ] && [ -f "${INSTALL_RACORA_ARTIFACT_PATH}/installer/install-racora.sh" ]; then
        RACORA_BOOTSTRAP=bundle; return 0
    fi
    if [ -n "${RACORA_REF_GIVEN}" ]; then RACORA_BOOTSTRAP=ref; return 0; fi
    if [ -n "${INSTALL_RACORA_ARTIFACT_PATH}" ] || [ "${INSTALL_RACORA_MODE}" = provision-rf ] || [ "${INSTALL_RACORA_MODE}" = uninstall ] || { [ -z "${INSTALL_RACORA_VERSION}" ] && [ -n "${INSTALL_K3S_VERSION}" ]; }; then
        RACORA_BOOTSTRAP=none; return 0
    fi
    return 0
}

# Run <installer> with the resolved version and the same arguments, as a child
# so this shell's temp dir is cleaned up afterwards; exit with its status.
racora_bootstrap_run() {
    _installer=$1; shift
    set +e
    RACORA_BOOTSTRAPPED=1 INSTALL_RACORA_VERSION="${INSTALL_RACORA_VERSION}" sh "${_installer}" "$@"
    _rc=$?
    set -e
    exit ${_rc}
}

racora_bootstrap() {
    racora_bootstrap_plan
    case "${RACORA_BOOTSTRAP}" in
        bundle)
            info "running the installer that ships in the bundle (${INSTALL_RACORA_ARTIFACT_PATH}/installer)"
            racora_bootstrap_run "${INSTALL_RACORA_ARTIFACT_PATH}/installer/install-racora.sh" "$@" ;;
        tarball)
            [ -n "${INSTALL_RACORA_VERSION}" ] || fatal "no release version to bootstrap (set INSTALL_RACORA_VERSION)"
            _dl="${RACORA_TMP}/bootstrap"; mkdir -p "${_dl}"
            _tar="racora-installer-${INSTALL_RACORA_VERSION}.tar.gz"
            for _f in "${_tar}" SHA256SUMS; do
                curl -sfL -o "${_dl}/${_f}" "${RACORA_ARTIFACT_URL}/${INSTALL_RACORA_VERSION}/${_f}" \
                    || fatal "could not download ${_f} for ${INSTALL_RACORA_VERSION} from ${RACORA_ARTIFACT_URL} (is it a published release?)"
            done
            racora_sha256_verify "${_dl}/${_tar}" "${_dl}/SHA256SUMS" "${_tar}"
            tar -xzf "${_dl}/${_tar}" -C "${_dl}" || fatal "could not unpack ${_tar}"
            [ -f "${_dl}/installer/install-racora.sh" ] || fatal "${_tar} carries no installer/install-racora.sh"
            info "running the ${INSTALL_RACORA_VERSION} installer (from its release package)"
            racora_bootstrap_run "${_dl}/installer/install-racora.sh" "$@" ;;
        *) ;;   # checkout | ref | none | done: install from this tree
    esac
}

# --- dry run: print the resolved plan, touch nothing ---
racora_dry_run_report() {
    echo "racora installer dry run"
    echo "platform=${INSTALL_RACORA_PLATFORM}"
    echo "mode=${INSTALL_RACORA_MODE}"
    echo "core=${INSTALL_RACORA_CORE}${INSTALL_RACORA_AMF_ADDR:+ amf=${INSTALL_RACORA_AMF_ADDR}}"
    echo "version=${INSTALL_RACORA_VERSION:-latest (unresolved in dry run)}"
    echo "bundle=${INSTALL_RACORA_ARTIFACT_PATH:-none}"
    echo "ref=${RACORA_REF:-<the release tag, resolved at install time>}"
    racora_bootstrap_plan; echo "bootstrap=${RACORA_BOOTSTRAP}"
    echo "provision=${INSTALL_RACORA_PROVISION} gpu=${INSTALL_RACORA_GPU:-auto} fronthaul=${INSTALL_RACORA_FRONTHAUL:-auto}"
    if [ "${INSTALL_RACORA_MODE}" = uninstall ]; then
        echo "uninstall_scope=${INSTALL_RACORA_UNINSTALL_SCOPE:-default (from ${RACORA_INSTALL_RECORD})}"
    fi
    for _pair in "RACORA_PLATFORM_DIR:platforms/${INSTALL_RACORA_PLATFORM}:install.sh" "RACORA_NP:node-provisioning:provision-rf-node.sh" "RACORA_CORE_DIR:cores/${INSTALL_RACORA_CORE}:values.yaml"; do
        _v=${_pair%%:*}; _rest=${_pair#*:}; _s=${_rest%%:*}; _m=${_rest#*:}
        if racora_resolve_subtree "${_v}" "${_s}" "${_m}"; then echo "${_s}=${racora_subtree_source}"; fi
    done
    exit 0
}

# --- dispatch ---
racora_main() {
    racora_detect_sudo
    racora_resolve_mode
    racora_static_validate
    racora_resolve_ref                  # an explicit version pins the subtrees too (the dry run shows it)
    if [ "${RACORA_DRY_RUN}" = 1 ]; then racora_dry_run_report; fi
    racora_setup_tmp
    racora_resolve_version
    racora_bootstrap "$@"               # a release install hands off to that release's installer (never returns)
    racora_resolve_ref final            # the resolved latest release, else main
    if [ "${INSTALL_RACORA_MODE}" = provision-rf ]; then
        racora_run_provisioner
        exit 0
    fi
    racora_resolve_platform || fatal "platform module '${INSTALL_RACORA_PLATFORM}' unavailable (set RACORA_PLATFORMS_DIR to a checkout, or RACORA_REF to a ref that has platforms/)"
    case "${INSTALL_RACORA_MODE}" in server|attach)
        racora_resolve_core || fatal "core provider '${INSTALL_RACORA_CORE}' unavailable (set RACORA_CORES_DIR to a checkout, or RACORA_REF to a ref that has cores/)" ;;
    esac
    # shellcheck source=/dev/null
    . "${RACORA_PLATFORM_DIR}/install.sh"
    platform_validate
    if [ "${INSTALL_RACORA_MODE}" = uninstall ]; then
        platform_uninstall "${INSTALL_RACORA_UNINSTALL_SCOPE}"
    else
        platform_run "$@"
    fi
}

# Tests source this file for its helpers: RACORA_SOURCE_ONLY=1 . ./install-racora.sh
if [ "${RACORA_SOURCE_ONLY}" != 1 ]; then
    racora_main "$@"
fi
