#!/usr/bin/env bash
# cloudtaser installer — https://cloudtaser.io
# Usage:
#   curl -fsSL https://docs.cloudtaser.io/install.sh | bash                   # install CLI binary only (default)
#   curl -fsSL https://docs.cloudtaser.io/install.sh | bash -s -- --full      # install CLI + deploy Helm chart
#   curl -fsSL https://docs.cloudtaser.io/install.sh | bash -s -- --demo      # install CLI + Helm + OpenBao + demo pods
# Installer-Revision: 865047c92199f2500a883e17945ed7cb4db5fe4e
# Installer-Published-At: 2026-07-21T12:36:27Z
set -euo pipefail

NAMESPACE="cloudtaser-system"
DEMO_NS="cloudtaser-demo"
VAULT_NS="cloudtaser-vault"
CHART_REPO="https://charts.cloudtaser.io"
OPENBAO_CHART_VERSION="0.2.0"
OPENBAO_APP_VERSION="2.6.0"
OPENBAO_RELEASE="cloudtaser-openbao"
OPENBAO_CA_CONFIGMAP="cloudtaser-openbao-ca"
CLI_RELEASE_BASE="https://releases.cloudtaser.io/cli/latest"
# Trust root for CloudTaser release signatures. The matching PEM is embedded
# below so a compromise of releases.cloudtaser.io cannot replace both an
# artifact and the key used to authenticate it. Rotate the signer only after a
# reviewed installer update publishes the new key and overlap policy.
CLI_RELEASE_KEY_SHA256="036dd71c9d3c07a19bff06d4392874014573f695620ac8554d9df175094c4a31"
COSIGN_VERSION="3.1.1"
COSIGN_RELEASE_BASE="https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}"
INSTALLER_REVISION="865047c92199f2500a883e17945ed7cb4db5fe4e"
INSTALLER_PUBLISHED_AT="2026-07-21T12:36:27Z"
CLI_ONLY=false
FULL=false
DEMO=false
COSIGN_BOOTSTRAPPED=false

# The repository source retains visible placeholders. The docs build stamps
# both values into the public artifact from the exact commit being published.
if [[ "$INSTALLER_REVISION" == @@*@@ ]]; then
  INSTALLER_REVISION="unpublished-source"
  INSTALLER_PUBLISHED_AT="not-published"
fi

info()  { echo -e "\033[1;34m==>\033[0m $*"; }
ok()    { echo -e "\033[1;32m  ✓\033[0m $*"; }
warn()  { echo -e "\033[1;33m  !\033[0m $*"; }
fail()  { echo -e "\033[1;31m  ✗\033[0m $*"; exit 1; }
sha256_file() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    fail "sha256sum or shasum is required to verify the downloaded binary"
  fi
}
write_pinned_release_key() (
  umask 077
  cat >"$1" <<'CLOUDTASER_RELEASE_PUBLIC_KEY'
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEORWOI3V47dAPvCBNec5aDeoGd0my
SxOZVlFj9e3OyEC2KD00sLeZoLKH4hewaY/+dmzYk1iuZXwNW56YmnNz2A==
-----END PUBLIC KEY-----
CLOUDTASER_RELEASE_PUBLIC_KEY
)
ensure_cosign() {
  if COSIGN_BIN="$(command -v cosign 2>/dev/null)"; then
    return
  fi

  # Pinned from the official v${COSIGN_VERSION}/cosign_checksums.txt release asset.
  # Update the version and all four hashes together.
  case "${OS}/${ARCH}" in
    darwin/amd64) COSIGN_EXPECTED_SHA="14d2678dfbfde18798151e86fbd91ebdadbb7424b18412a42a155dd8a2df4c7a" ;;
    darwin/arm64) COSIGN_EXPECTED_SHA="94b42a9e697be95675f6160ab031a9a5f1ec1e646d6f648d7b2f5cd59ececbc5" ;;
    linux/amd64) COSIGN_EXPECTED_SHA="ae1ecd212663f3693ad9edf8b1a183900c9a52d3155ba6e354237f9a0f6463fc" ;;
    linux/arm64) COSIGN_EXPECTED_SHA="2ec865872e331c32fd12b08dae15332d3f92c0aa029219589684a4903ca85d11" ;;
    *) fail "Automatic cosign setup is unsupported on ${OS}/${ARCH}; install cosign from https://docs.sigstore.dev/cosign/installation/" ;;
  esac

  COSIGN_ASSET="cosign-${OS}-${ARCH}"
  COSIGN_BIN="${INSTALL_TMPDIR}/cosign"
  info "cosign not found; downloading verified cosign v${COSIGN_VERSION} for this install"
  curl -fsSL --retry 3 "${COSIGN_RELEASE_BASE}/${COSIGN_ASSET}" -o "$COSIGN_BIN"
  COSIGN_ACTUAL_SHA="$(sha256_file "$COSIGN_BIN")"
  [ "$COSIGN_ACTUAL_SHA" = "$COSIGN_EXPECTED_SHA" ] || fail "checksum mismatch for downloaded cosign v${COSIGN_VERSION}"
  chmod 0755 "$COSIGN_BIN"
  COSIGN_BOOTSTRAPPED=true
  ok "Downloaded and verified temporary cosign v${COSIGN_VERSION}"
}

CLEANUP_FAILED=false
CLEANUP_SELECTOR="app.kubernetes.io/part-of=cloudtaser"

cleanup_delete() {
  local kind="$1"
  shift
  if ! kubectl delete "$kind" "$@" --ignore-not-found; then
    warn "Failed to delete one or more ${kind} resources"
    CLEANUP_FAILED=true
  fi
}

cleanup_delete_selector() {
  local kind="$1"
  if ! kubectl delete "$kind" -l "$CLEANUP_SELECTOR" --ignore-not-found; then
    warn "Failed to delete ${kind} resources labeled ${CLEANUP_SELECTOR}"
    CLEANUP_FAILED=true
  fi
}

cleanup_verify_names() {
  local kind="$1"
  shift
  local name output
  for name in "$@"; do
    if ! output="$(kubectl get "$kind" "$name" --ignore-not-found -o name 2>/dev/null)"; then
      warn "Could not verify removal of ${kind}/${name}"
      CLEANUP_FAILED=true
    elif [ -n "$output" ]; then
      warn "Cleanup left ${output} behind"
      CLEANUP_FAILED=true
    fi
  done
}

cleanup_verify_selector() {
  local kind="$1"
  local output
  if ! output="$(kubectl get "$kind" -l "$CLEANUP_SELECTOR" -o name 2>/dev/null)"; then
    warn "Could not verify labeled ${kind} cleanup"
    CLEANUP_FAILED=true
  elif [ -n "$output" ]; then
    warn "Cleanup left labeled resources behind: ${output//$'\n'/, }"
    CLEANUP_FAILED=true
  fi
}

cleanup_api_resource_exists() {
  local resource="$1"
  kubectl api-resources --namespaced=false -o name 2>/dev/null | grep -Fxq "$resource"
}

run_cleanup() {
  command -v kubectl >/dev/null 2>&1 || fail "kubectl is required for --cleanup"
  info "Uninstalling cloudtaser"

  local helm_output
  helm_output="$(mktemp)"
  if ! command -v helm >/dev/null 2>&1; then
    warn "helm not found; continuing with explicit resource cleanup"
  elif helm uninstall cloudtaser -n "$NAMESPACE" >"$helm_output" 2>&1; then
    cat "$helm_output"
  elif grep -Eqi 'release[^[:alnum:]]+not found|release: not found' "$helm_output"; then
    info "No Helm release found; cleaning server-side-applied resources"
  else
    cat "$helm_output" >&2
    warn "Helm uninstall failed; explicit cleanup will continue"
    CLEANUP_FAILED=true
  fi
  rm -f "$helm_output"

  # Delete every currently labeled cluster-scoped resource, then explicit
  # historical/unlabeled names left by older chart and CLI install paths.
  local kind
  for kind in \
    mutatingwebhookconfiguration \
    validatingwebhookconfiguration \
    clusterrole \
    clusterrolebinding \
    customresourcedefinition; do
    cleanup_delete_selector "$kind"
  done

  cleanup_delete mutatingwebhookconfiguration \
    cloudtaser-operator-webhook
  cleanup_delete validatingwebhookconfiguration \
    cloudtaser-operator-webhook-validating
  cleanup_delete clusterrole \
    cloudtaser-operator \
    cloudtaser-ebpf \
    cloudtaser-migrate-access \
    cloudtaser-vs-broker \
    cloudtaser-crd-adopt
  cleanup_delete clusterrolebinding \
    vault-tokenreview \
    vault-auth-tokenreview \
    cloudtaser-auth-tokenreview \
    cloudtaser-operator \
    cloudtaser-ebpf \
    cloudtaser-migrate-access \
    cloudtaser-service-account-issuer-discovery \
    cloudtaser-vs-broker \
    cloudtaser-crd-adopt
  cleanup_delete customresourcedefinition \
    cloudtaserconfigs.api.cloudtaser.io \
    secretmappings.api.cloudtaser.io \
    debugreports.api.cloudtaser.io \
    cloudtaserpolicies.api.cloudtaser.io

  if cleanup_api_resource_exists validatingadmissionpolicies.admissionregistration.k8s.io; then
    cleanup_delete_selector validatingadmissionpolicy
    cleanup_delete validatingadmissionpolicy cloudtaser-require-image-digest
  fi
  if cleanup_api_resource_exists validatingadmissionpolicybindings.admissionregistration.k8s.io; then
    cleanup_delete_selector validatingadmissionpolicybinding
    cleanup_delete validatingadmissionpolicybinding cloudtaser-require-image-digest
  fi
  if cleanup_api_resource_exists clusterpolicies.kyverno.io; then
    cleanup_delete_selector clusterpolicy
    cleanup_delete clusterpolicy cloudtaser-verify-image-signatures
  fi

  cleanup_delete namespace "$NAMESPACE" "$DEMO_NS" "$VAULT_NS" --timeout=120s

  for kind in \
    mutatingwebhookconfiguration \
    validatingwebhookconfiguration \
    clusterrole \
    clusterrolebinding \
    customresourcedefinition; do
    cleanup_verify_selector "$kind"
  done
  cleanup_verify_names mutatingwebhookconfiguration \
    cloudtaser-operator-webhook
  cleanup_verify_names validatingwebhookconfiguration \
    cloudtaser-operator-webhook-validating
  cleanup_verify_names clusterrole \
    cloudtaser-operator \
    cloudtaser-ebpf \
    cloudtaser-migrate-access \
    cloudtaser-vs-broker \
    cloudtaser-crd-adopt
  cleanup_verify_names clusterrolebinding \
    vault-tokenreview \
    vault-auth-tokenreview \
    cloudtaser-auth-tokenreview \
    cloudtaser-operator \
    cloudtaser-ebpf \
    cloudtaser-migrate-access \
    cloudtaser-service-account-issuer-discovery \
    cloudtaser-vs-broker \
    cloudtaser-crd-adopt
  cleanup_verify_names customresourcedefinition \
    cloudtaserconfigs.api.cloudtaser.io \
    secretmappings.api.cloudtaser.io \
    debugreports.api.cloudtaser.io \
    cloudtaserpolicies.api.cloudtaser.io
  cleanup_verify_names namespace "$NAMESPACE" "$DEMO_NS" "$VAULT_NS"

  if cleanup_api_resource_exists validatingadmissionpolicies.admissionregistration.k8s.io; then
    cleanup_verify_selector validatingadmissionpolicy
    cleanup_verify_names validatingadmissionpolicy cloudtaser-require-image-digest
  fi
  if cleanup_api_resource_exists validatingadmissionpolicybindings.admissionregistration.k8s.io; then
    cleanup_verify_selector validatingadmissionpolicybinding
    cleanup_verify_names validatingadmissionpolicybinding cloudtaser-require-image-digest
  fi
  if cleanup_api_resource_exists clusterpolicies.kyverno.io; then
    cleanup_verify_selector clusterpolicy
    cleanup_verify_names clusterpolicy cloudtaser-verify-image-signatures
  fi

  if $CLEANUP_FAILED; then
    fail "cloudtaser cleanup incomplete; review the reported errors and leftovers"
  fi
  ok "cloudtaser uninstalled; verified no managed cluster-scoped resources remain"
}

MUTATING_WEBHOOK_NAME="cloudtaser-operator-webhook"
VALIDATING_WEBHOOK_NAME="cloudtaser-operator-webhook-validating"
MUTATING_WEBHOOK_EXISTED=false
VALIDATING_WEBHOOK_EXISTED=false
HELM_RELEASE_EXISTED=false
HELM_PREVIOUS_REVISION=""

capture_webhook_snapshot() {
  local kind="$1"
  local name="$2"
  local snapshot="$3"
  local existed_var="$4"
  local current

  if ! current="$(kubectl get "$kind" "$name" --ignore-not-found -o name 2>"${snapshot}.error")"; then
    cat "${snapshot}.error" >&2
    fail "Cannot read the existing ${kind}/${name}; refusing a non-transactional upgrade"
  fi
  rm -f "${snapshot}.error"

  if [ -z "$current" ]; then
    return
  fi
  if ! kubectl get "$kind" "$name" -o jsonpath='{.webhooks}' >"$snapshot"; then
    fail "Cannot snapshot ${kind}/${name}; refusing a non-transactional upgrade"
  fi
  [ -s "$snapshot" ] || fail "Snapshot for ${kind}/${name} was empty"
  printf -v "$existed_var" '%s' true
}

restore_webhook_snapshot() {
  local kind="$1"
  local manifest_kind="$2"
  local name="$3"
  local existed="$4"
  local snapshot="$5"

  if ! $existed; then
    # The prior state was absence. This deletion is failure recovery for a new
    # object, never a pre-upgrade deletion of an active webhook.
    kubectl delete "$kind" "$name" --ignore-not-found >/dev/null
    return
  fi

  local payload="${snapshot}.patch.json"
  {
    printf '%s' '{"webhooks":'
    cat "$snapshot"
    printf '%s\n' '}'
  } >"$payload"

  if kubectl get "$kind" "$name" >/dev/null 2>&1; then
    kubectl patch "$kind" "$name" --type=merge --patch-file "$payload" >/dev/null
    return
  fi

  local manifest="${snapshot}.restore.json"
  {
    printf '{"apiVersion":"admissionregistration.k8s.io/v1","kind":"%s","metadata":{"name":"%s"},"webhooks":' \
      "$manifest_kind" "$name"
    cat "$snapshot"
    printf '%s\n' '}'
  } >"$manifest"
  kubectl create -f "$manifest" >/dev/null
}

restore_admission_state() {
  local restored=true
  if ! restore_webhook_snapshot \
    mutatingwebhookconfiguration MutatingWebhookConfiguration \
    "$MUTATING_WEBHOOK_NAME" "$MUTATING_WEBHOOK_EXISTED" \
    "$MUTATING_WEBHOOK_SNAPSHOT"; then
    restored=false
  fi
  if ! restore_webhook_snapshot \
    validatingwebhookconfiguration ValidatingWebhookConfiguration \
    "$VALIDATING_WEBHOOK_NAME" "$VALIDATING_WEBHOOK_EXISTED" \
    "$VALIDATING_WEBHOOK_SNAPSHOT"; then
    restored=false
  fi
  $restored
}

admission_backend_ready() {
  local ready_endpoints
  kubectl -n "$NAMESPACE" wait \
    --for=condition=Available deployment/cloudtaser-operator \
    --timeout=180s >/dev/null 2>&1 || return 1
  kubectl get mutatingwebhookconfiguration \
    "$MUTATING_WEBHOOK_NAME" >/dev/null 2>&1 || return 1
  kubectl get validatingwebhookconfiguration \
    "$VALIDATING_WEBHOOK_NAME" >/dev/null 2>&1 || return 1
  ready_endpoints="$(kubectl -n "$NAMESPACE" get endpointslice \
    -l kubernetes.io/service-name=cloudtaser-operator-webhook \
    -o jsonpath='{range .items[*].endpoints[*]}{.conditions.ready}{"\n"}{end}' \
    2>/dev/null)" || return 1
  grep -Fxq true <<<"$ready_endpoints"
}

restore_previous_release_after_postcheck() {
  local rollback_log="$1"
  if $HELM_RELEASE_EXISTED; then
    helm rollback cloudtaser "$HELM_PREVIOUS_REVISION" -n "$NAMESPACE" \
      --wait --timeout 180s >"$rollback_log" 2>&1
    return
  fi

  if helm uninstall cloudtaser -n "$NAMESPACE" \
    --wait --timeout 180s >"$rollback_log" 2>&1; then
    return
  fi
  grep -Eqi 'release[^[:alnum:]]+not found|release: not found' "$rollback_log"
}

info "Installer revision ${INSTALLER_REVISION} (published ${INSTALLER_PUBLISHED_AT})"

for arg in "$@"; do
  case "$arg" in
    --demo) DEMO=true ;;
    --full) FULL=true ;;
    --cli-only) CLI_ONLY=true ;;
    --cleanup)
      run_cleanup
      exit 0
      ;;
    --help|-h)
      echo "cloudtaser installer"
      echo ""
      echo "Usage:"
      echo "  curl -fsSL https://docs.cloudtaser.io/install.sh | bash                   # CLI binary only (default)"
      echo "  curl -fsSL https://docs.cloudtaser.io/install.sh | bash -s -- --full      # CLI + Helm chart deploy"
      echo "  curl -fsSL https://docs.cloudtaser.io/install.sh | bash -s -- --demo      # CLI + Helm + OpenBao + demo pods"
      echo ""
      echo "Options:"
      echo "  --full      Install CLI binary and deploy the cloudtaser Helm chart to your cluster"
      echo "  --demo      Install CLI binary, cloudtaser, the hardened OpenBao chart, and a demo workload"
      echo "  --cli-only  Explicit alias for the default: install CLI binary only"
      echo "  --cleanup   Uninstall cloudtaser and remove all resources"
      echo "  --help      Show this help"
      exit 0
      ;;
  esac
done

# ── CLI binary install (always) ────────────────────────────────────────
info "Installing cloudtaser-cli"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
  x86_64|amd64) ARCH="amd64" ;;
  aarch64|arm64) ARCH="arm64" ;;
  *) fail "Unsupported architecture: $ARCH" ;;
esac
BINARY="cloudtaser-cli-${OS}-${ARCH}"
INSTALL_DIR="${CLOUDTASER_INSTALL_DIR:-/usr/local/bin}"
INSTALL_TMPDIR="$(mktemp -d)"
trap 'rm -rf "$INSTALL_TMPDIR"' EXIT
ensure_cosign
info "Downloading $BINARY"
curl -fsSL "${CLI_RELEASE_BASE}/${BINARY}" -o "${INSTALL_TMPDIR}/${BINARY}"
curl -fsSL "${CLI_RELEASE_BASE}/SHA256SUMS" -o "${INSTALL_TMPDIR}/SHA256SUMS"
write_pinned_release_key "${INSTALL_TMPDIR}/release.pub"
CLI_RELEASE_KEY_ACTUAL_SHA="$(sha256_file "${INSTALL_TMPDIR}/release.pub")"
[ "$CLI_RELEASE_KEY_ACTUAL_SHA" = "$CLI_RELEASE_KEY_SHA256" ] || \
  fail "installer-embedded release key failed its integrity check"
if curl -fsSL "${CLI_RELEASE_BASE}/SHA256SUMS.sigstore.json" -o "${INSTALL_TMPDIR}/SHA256SUMS.sigstore.json" 2>/dev/null; then
  SIGSTORE_BUNDLE="${INSTALL_TMPDIR}/SHA256SUMS.sigstore.json"
else
  SIGSTORE_BUNDLE=""
  curl -fsSL "${CLI_RELEASE_BASE}/SHA256SUMS.sig" -o "${INSTALL_TMPDIR}/SHA256SUMS.sig"
fi
info "Verifying release checksums"
if [ -n "$SIGSTORE_BUNDLE" ]; then
  if ! "$COSIGN_BIN" verify-blob \
    --key "${INSTALL_TMPDIR}/release.pub" \
    --bundle "$SIGSTORE_BUNDLE" \
    "${INSTALL_TMPDIR}/SHA256SUMS" >/dev/null; then
    fail "cosign signature verification failed for SHA256SUMS"
  fi
  VERIFICATION_SUMMARY="signed checksum bundle against the installer-embedded CloudTaser release key, including its transparency proof"
else
  warn "No transparency-log bundle was published for this release"
  info "Verifying the detached checksum signature against the installer-embedded CloudTaser release key (${CLI_RELEASE_KEY_SHA256})"
  warn "This authenticates the checksum signer but does not provide transparency-log inclusion"
  COSIGN_VERIFY_STDERR="${INSTALL_TMPDIR}/cosign-verify.stderr"
  if ! "$COSIGN_BIN" verify-blob \
    --key "${INSTALL_TMPDIR}/release.pub" \
    --insecure-ignore-sct \
    --insecure-ignore-tlog \
    --signature "${INSTALL_TMPDIR}/SHA256SUMS.sig" \
    "${INSTALL_TMPDIR}/SHA256SUMS" >/dev/null 2>"$COSIGN_VERIFY_STDERR"; then
    cat "$COSIGN_VERIFY_STDERR" >&2
    fail "cosign signature verification failed for SHA256SUMS"
  fi
  while IFS= read -r cosign_line; do
    case "$cosign_line" in
      "Flag --signature has been deprecated"*) ;;
      "Verified OK") ;;
      *) printf '%s\n' "$cosign_line" >&2 ;;
    esac
  done <"$COSIGN_VERIFY_STDERR"
  VERIFICATION_SUMMARY="detached checksum signature against the installer-embedded CloudTaser release key; no transparency proof was published"
fi
EXPECTED_SHA="$(awk -v binary="$BINARY" '$2 == binary {print $1}' "${INSTALL_TMPDIR}/SHA256SUMS")"
[ -n "$EXPECTED_SHA" ] || fail "SHA256SUMS does not contain $BINARY"
ACTUAL_SHA="$(sha256_file "${INSTALL_TMPDIR}/${BINARY}")"
[ "$ACTUAL_SHA" = "$EXPECTED_SHA" ] || fail "checksum mismatch for $BINARY"
ok "Verified ${VERIFICATION_SUMMARY}, then matched the binary checksum"
if [ -w "$INSTALL_DIR" ]; then
  install -m 0755 "${INSTALL_TMPDIR}/${BINARY}" "${INSTALL_DIR}/cloudtaser-cli"
else
  sudo install -m 0755 "${INSTALL_TMPDIR}/${BINARY}" "${INSTALL_DIR}/cloudtaser-cli"
fi
ok "Installed to ${INSTALL_DIR}/cloudtaser-cli"

# Component-install commands (for example `source install port`) invoke cosign
# after this script exits to verify container images. Preserve only the pinned,
# checksum-verified bootstrap binary; a cosign already supplied by the operator
# remains untouched.
if $COSIGN_BOOTSTRAPPED; then
  if [ -w "$INSTALL_DIR" ]; then
    install -m 0755 "$COSIGN_BIN" "${INSTALL_DIR}/cosign"
  else
    sudo install -m 0755 "$COSIGN_BIN" "${INSTALL_DIR}/cosign"
  fi
  ok "Installed verified cosign v${COSIGN_VERSION} to ${INSTALL_DIR}/cosign"
fi

# ── Default (no --full / --demo): print summary and exit ──────────────
if ! $FULL && ! $DEMO; then
  echo ""
  echo "  Installed: cloudtaser CLI only."
  echo "  No CloudTaser components were deployed to a Kubernetes cluster."
  echo "  Version:  $(${INSTALL_DIR}/cloudtaser-cli version 2>/dev/null || echo 'unknown')"
  echo "  Docs:     https://docs.cloudtaser.io"
  echo ""
  echo "  Next steps:"
  echo "  Source side (beacon + Vault/OpenBao):"
  echo "    sudo cloudtaser-cli beacon install --type systemd --advertise-addr beacon.example.com:443"
  echo "    cloudtaser-cli source install bao --namespace cloudtaser-vault"
  echo ""
  echo "  Target side (Kubernetes cluster):"
  echo "    cloudtaser-cli target install --mode direct --secretstore-address=https://vault.eu.example.com"
  echo ""
  echo "  Full setup guide: https://docs.cloudtaser.io/quickstart/"
  echo ""
  exit 0
fi

# ── Prerequisites ──────────────────────────────────────────────────────
info "Checking prerequisites"
command -v kubectl >/dev/null 2>&1 || fail "kubectl not found — install from https://kubernetes.io/docs/tasks/tools/"
command -v helm >/dev/null 2>&1    || fail "helm not found — install from https://helm.sh/docs/tasks/tools/"
kubectl cluster-info >/dev/null 2>&1 || fail "Cannot reach Kubernetes cluster — check your kubeconfig"
ok "kubectl and helm available, cluster reachable"

# ── Helm repo ──────────────────────────────────────────────────────────
info "Adding cloudtaser Helm repo"
if ! helm repo add cloudtaser "$CHART_REPO" --force-update >/dev/null; then
  fail "Could not configure the CloudTaser Helm repository at ${CHART_REPO}"
fi
if ! helm repo update cloudtaser >/dev/null; then
  fail "Could not refresh the CloudTaser Helm repository at ${CHART_REPO}"
fi
ok "Helm repo added: $CHART_REPO"

# ── Demo mode: deploy the maintained hardened OpenBao chart ───────────
if $DEMO; then
  info "Demo mode: installing hardened OpenBao chart v${OPENBAO_CHART_VERSION} (OpenBao ${OPENBAO_APP_VERSION})"

  # Resolve and inspect the exact chart before removing resources created by
  # the retired development manifest. This keeps a temporary repository or
  # publication outage from destroying an otherwise working demo.
  OPENBAO_CHART_METADATA="${INSTALL_TMPDIR}/openbao-chart.yaml"
  if ! helm show chart cloudtaser/cloudtaser-openbao \
    --version "$OPENBAO_CHART_VERSION" >"$OPENBAO_CHART_METADATA"; then
    fail "OpenBao chart v${OPENBAO_CHART_VERSION} is unavailable from ${CHART_REPO}; no demo resources were changed"
  fi
  grep -Fxq 'name: cloudtaser-openbao' "$OPENBAO_CHART_METADATA" || \
    fail "The resolved OpenBao chart has an unexpected name; no demo resources were changed"
  grep -Fxq "version: ${OPENBAO_CHART_VERSION}" "$OPENBAO_CHART_METADATA" || \
    fail "The resolved OpenBao chart does not match pinned version ${OPENBAO_CHART_VERSION}; no demo resources were changed"
  grep -Fxq "appVersion: ${OPENBAO_APP_VERSION}" "$OPENBAO_CHART_METADATA" || \
    fail "OpenBao chart v${OPENBAO_CHART_VERSION} does not contain expected OpenBao ${OPENBAO_APP_VERSION}; no demo resources were changed"

  if ! kubectl get namespace "$DEMO_NS" >/dev/null 2>&1; then
    kubectl create namespace "$DEMO_NS" >/dev/null || fail "Could not create demo namespace ${DEMO_NS}"
  fi
  kubectl label namespace "$DEMO_NS" \
    cloudtaser.io/openbao-client=enabled --overwrite >/dev/null || \
    fail "Could not opt the demo namespace into the OpenBao client policy"

  OPENBAO_HELM_LOG="$(mktemp "${TMPDIR:-/tmp}/cloudtaser-openbao-helm.XXXXXX")"
  OPENBAO_HELM_ARGS=(
    "$OPENBAO_RELEASE" cloudtaser/cloudtaser-openbao
    --version "$OPENBAO_CHART_VERSION"
    --namespace "$VAULT_NS" --create-namespace
    --set "namespace=$VAULT_NS"
    --set openbao.server.ha.enabled=false
    --set openbao.server.standalone.enabled=true
    --set bootstrap.shamir.shares=1
    --set bootstrap.shamir.threshold=1
    --set bootstrap.role.name=demo
    --set bootstrap.role.boundServiceAccountNames=default
    --set "bootstrap.role.boundServiceAccountNamespaces=$DEMO_NS"
    --set operatorConfig.enabled=false
    --cleanup-on-fail
    --timeout 12m
  )
  # Do not add --wait/--atomic here: Helm delays post-install hooks until
  # ordinary resources are ready, while the chart's bootstrap hook is what
  # initializes and unseals OpenBao. Helm waits for the hook Job itself.
  if ! helm upgrade --install "${OPENBAO_HELM_ARGS[@]}" >"$OPENBAO_HELM_LOG" 2>&1; then
    warn "OpenBao chart output (last 40 lines):"
    tail -n 40 "$OPENBAO_HELM_LOG" >&2
    kubectl -n "$VAULT_NS" logs job/${OPENBAO_RELEASE}-preflight --all-containers >&2 || true
    kubectl -n "$VAULT_NS" logs job/${OPENBAO_RELEASE}-bootstrap --all-containers >&2 || true
    fail "Hardened OpenBao chart installation failed; full Helm log: $OPENBAO_HELM_LOG"
  fi
  rm -f "$OPENBAO_HELM_LOG"
  ok "Installed maintained OpenBao chart v${OPENBAO_CHART_VERSION}"

  info "Confirming the chart bootstrap and verified-TLS endpoint"
  if ! kubectl -n "$VAULT_NS" wait \
    --for=condition=Complete job/${OPENBAO_RELEASE}-bootstrap \
    --timeout=30s >/dev/null; then
    kubectl -n "$VAULT_NS" logs job/${OPENBAO_RELEASE}-bootstrap --all-containers >&2 || true
    fail "OpenBao bootstrap did not complete successfully"
  fi
  kubectl -n "$VAULT_NS" rollout status \
    statefulset/${OPENBAO_RELEASE} --timeout=180s >/dev/null || \
    fail "OpenBao did not become ready after bootstrap"

  OPENBAO_CA_FILE="${INSTALL_TMPDIR}/openbao-ca.crt"
  if ! kubectl -n "$VAULT_NS" get secret openbao-ca \
    -o 'jsonpath={.data.ca\.crt}' | openssl base64 -d -A >"$OPENBAO_CA_FILE"; then
    fail "Could not read the public CA certificate created by the OpenBao chart"
  fi
  [ -s "$OPENBAO_CA_FILE" ] || fail "The OpenBao chart returned an empty public CA certificate"
  chmod 0644 "$OPENBAO_CA_FILE"

  if ! kubectl -n "$DEMO_NS" create configmap "$OPENBAO_CA_CONFIGMAP" \
    --from-file=ca.crt="$OPENBAO_CA_FILE" \
    --dry-run=client -o yaml | kubectl -n "$DEMO_NS" apply -f - >/dev/null; then
    fail "Could not publish the OpenBao public CA to the demo namespace"
  fi

  VAULT_ADDR="https://${OPENBAO_RELEASE}.${VAULT_NS}.svc:8200"
  ok "OpenBao ${OPENBAO_APP_VERSION} is ready over verified TLS at ${VAULT_ADDR}"

  # Retire the old plaintext deployment only after the replacement is ready
  # and its CA is available to clients. If chart installation fails,
  # --cleanup-on-fail removes its partial resources while the previous demo
  # remains available for recovery.
  info "Removing resources from the retired OpenBao development manifest"
  if ! kubectl -n "$VAULT_NS" delete \
    pod/vault service/vault secret/openbao-demo-root-token \
    --ignore-not-found >/dev/null; then
    fail "Could not remove the retired OpenBao development resources"
  fi
  if ! kubectl delete clusterrolebinding/vault-tokenreview \
    --ignore-not-found >/dev/null; then
    fail "Could not remove the retired broad OpenBao token-review binding"
  fi
else
  # CLOUDTASER_VAULT_ADDRESS is the legacy env var; CLOUDTASER_SECRETSTORE_ADDRESS
  # is preferred. Read the new name first, fall back to the legacy name.
  VAULT_ADDR="${CLOUDTASER_SECRETSTORE_ADDRESS:-${CLOUDTASER_VAULT_ADDRESS:-}}"
  if [ -z "$VAULT_ADDR" ]; then
    warn "No secret store address specified"
    warn "Set CLOUDTASER_SECRETSTORE_ADDRESS or use --demo for an in-cluster secret store"
    warn "Installing without a secret store address — set it later via helm upgrade"
    VAULT_ADDR=""
  fi
fi

# ── Install cloudtaser ─────────────────────────────────────────────────
info "Installing cloudtaser"

# Keep active admission policy in place throughout the upgrade. Snapshot the
# behavior-bearing webhook arrays before Helm changes anything so a failed
# transaction can restore the exact prior admission configuration in-place.
MUTATING_WEBHOOK_SNAPSHOT="${INSTALL_TMPDIR}/mutating-webhooks.json"
VALIDATING_WEBHOOK_SNAPSHOT="${INSTALL_TMPDIR}/validating-webhooks.json"
capture_webhook_snapshot mutatingwebhookconfiguration \
  "$MUTATING_WEBHOOK_NAME" "$MUTATING_WEBHOOK_SNAPSHOT" \
  MUTATING_WEBHOOK_EXISTED
capture_webhook_snapshot validatingwebhookconfiguration \
  "$VALIDATING_WEBHOOK_NAME" "$VALIDATING_WEBHOOK_SNAPSHOT" \
  VALIDATING_WEBHOOK_EXISTED

HELM_STATUS_LOG="${INSTALL_TMPDIR}/helm-status.log"
if helm status cloudtaser -n "$NAMESPACE" >"$HELM_STATUS_LOG" 2>&1; then
  HELM_RELEASE_EXISTED=true
  HELM_PREVIOUS_REVISION="$(awk '$1 == "REVISION:" {print $2; exit}' "$HELM_STATUS_LOG")"
  case "$HELM_PREVIOUS_REVISION" in
    ''|*[!0-9]*)
      cat "$HELM_STATUS_LOG" >&2
      fail "Cannot identify the deployed Helm revision; no changes were made"
      ;;
  esac
elif ! grep -Eqi 'release[^[:alnum:]]+not found|release: not found' "$HELM_STATUS_LOG"; then
  cat "$HELM_STATUS_LOG" >&2
  fail "Cannot determine the current Helm release state; no changes were made"
fi

HELM_ARGS=(
  cloudtaser cloudtaser/cloudtaser
  --namespace "$NAMESPACE" --create-namespace
  --set ebpf.enabled=true
  --set ebpf.enforceMode=true
)

# In --demo mode, the secret store runs in-cluster so we set
# operator.secretstore.address directly. For production, the default is beacon
# mode (P2P relay) — use cloudtaser-cli to configure the beacon connection
# instead of setting operator.secretstore.address here.
# See: https://docs.cloudtaser.io/architecture/beacon-relay/
if [ -n "$VAULT_ADDR" ]; then
  HELM_ARGS+=(--set "operator.secretstore.address=$VAULT_ADDR")
fi
if $DEMO; then
  HELM_ARGS+=(
    --set operator.secretBackend=vault
    --set operator.enableReverseConnect=false
    --set operator.broker.beacon.enabled=false
    --set operator.secretstore.authRole=cloudtaser-operator
    --set-file "operator.secretstore.caCert=$OPENBAO_CA_FILE"
  )
fi

# Helm 4 renamed --atomic to --rollback-on-failure. Both modes preserve the
# prior successful release and wait for workloads before reporting success.
if helm upgrade --help 2>/dev/null | grep -q -- '--rollback-on-failure'; then
  HELM_ARGS+=(--rollback-on-failure --wait --timeout 180s)
else
  HELM_ARGS+=(--atomic --wait --timeout 180s)
fi

HELM_LOG="$(mktemp "${TMPDIR:-/tmp}/cloudtaser-helm.XXXXXX")"
if helm upgrade --install "${HELM_ARGS[@]}" >"$HELM_LOG" 2>&1; then
  info "Confirming the replacement admission backend"
  if ! admission_backend_ready; then
    ROLLBACK_LOG="${INSTALL_TMPDIR}/helm-rollback.log"
    warn "The replacement admission backend did not become ready; rolling back"
    if ! restore_previous_release_after_postcheck "$ROLLBACK_LOG"; then
      cat "$ROLLBACK_LOG" >&2
      restore_admission_state || true
      fail "Admission readiness failed and Helm could not restore the previous release; Helm upgrade log: $HELM_LOG"
    fi
    if ! restore_admission_state; then
      fail "Helm rolled back, but the previous admission configuration could not be restored; Helm upgrade log: $HELM_LOG"
    fi
    fail "Admission readiness failed; the previous release and webhook configuration were restored. Helm upgrade log: $HELM_LOG"
  fi
else
  warn "Helm output (last 40 lines):"
  tail -n 40 "$HELM_LOG" >&2
  # Restore an explicit known-good revision even when Helm's transaction flag
  # reports that it already rolled back. A second rollback to that exact
  # revision is safe and avoids inferring prior state from a generic exit code.
  FAILURE_ROLLBACK_LOG="${INSTALL_TMPDIR}/helm-failed-install-rollback.log"
  if ! restore_previous_release_after_postcheck "$FAILURE_ROLLBACK_LOG"; then
    cat "$FAILURE_ROLLBACK_LOG" >&2
    restore_admission_state || true
    fail "Helm failed and could not restore the previous release state; full Helm log: $HELM_LOG"
  fi
  if ! restore_admission_state; then
    fail "Helm failed and the previous admission configuration could not be restored; full Helm log: $HELM_LOG"
  fi
  fail "Helm failed; the previous release state and admission configuration were restored. Full log: $HELM_LOG"
fi
# Preserve chart NOTES, including security posture warnings and next steps.
cat "$HELM_LOG"
rm -f "$HELM_LOG"
ok "cloudtaser installed transactionally in namespace $NAMESPACE"
ok "Operator and admission webhooks are ready"

# Check eBPF
EBPF_READY=$(kubectl -n "$NAMESPACE" get daemonset cloudtaser-ebpf -o jsonpath='{.status.numberReady}' 2>/dev/null || echo 0)
EBPF_DESIRED=$(kubectl -n "$NAMESPACE" get daemonset cloudtaser-ebpf -o jsonpath='{.status.desiredNumberScheduled}' 2>/dev/null || echo 0)
ok "eBPF agents: $EBPF_READY/$EBPF_DESIRED nodes"

# ── Demo mode: deploy test pods ───────────────────────────────────────
if $DEMO; then
  info "Seeding a scoped demo secret through Kubernetes authentication"
  if ! DEMO_OPERATOR_JWT="$(kubectl -n "$NAMESPACE" create token \
    cloudtaser-operator --duration=10m)"; then
    fail "Could not mint a short-lived operator ServiceAccount token for demo setup"
  fi
  [ -n "$DEMO_OPERATOR_JWT" ] || fail "Kubernetes returned an empty operator ServiceAccount token"
  DEMO_PASSWORD="demo-$(openssl rand -hex 12)"
  DEMO_AUTH_PAYLOAD="$(printf '{\"role\":\"cloudtaser-operator\",\"jwt\":\"%s\"}' "$DEMO_OPERATOR_JWT")"
  DEMO_SECRET_PAYLOAD="$(printf '{\"username\":\"cloudtaser_user\",\"password\":\"%s\",\"host\":\"db.eu-west.example.com\",\"port\":\"5432\"}' "$DEMO_PASSWORD")"
  if ! {
    printf '%s\n' "$DEMO_AUTH_PAYLOAD"
    printf '%s\n' "$DEMO_SECRET_PAYLOAD"
  } | kubectl -n "$VAULT_NS" exec -i "${OPENBAO_RELEASE}-0" -c openbao -- \
    /bin/sh -ec '
      IFS= read -r auth_payload
      IFS= read -r secret_payload
      token="$(printf "%s" "$auth_payload" | bao write -field=token auth/kubernetes/login -)"
      export BAO_TOKEN="$token"
      printf "%s" "$secret_payload" | bao kv put secret/cloudtaser/demo/db/credentials - >/dev/null
      unset BAO_TOKEN token auth_payload secret_payload
    '; then
    unset DEMO_OPERATOR_JWT DEMO_PASSWORD DEMO_AUTH_PAYLOAD DEMO_SECRET_PAYLOAD
    fail "Could not seed the demo secret through the chart-configured Kubernetes auth role"
  fi
  unset DEMO_OPERATOR_JWT DEMO_PASSWORD DEMO_AUTH_PAYLOAD DEMO_SECRET_PAYLOAD
  ok "Seeded secret/cloudtaser/demo/db/credentials without a root credential"

  info "Deploying demo workload with secret injection"

  # Wait for webhook
  for i in $(seq 1 30); do
    kubectl get mutatingwebhookconfiguration cloudtaser-operator-webhook >/dev/null 2>&1 && break
    sleep 1
  done
  sleep 3

  cat <<DEMO_EOF | kubectl -n "$DEMO_NS" apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: demo-app
  labels:
    app: demo-app
    app.kubernetes.io/name: cloudtaser-demo
    cloudtaser.io/openbao-client: "enabled"
  annotations:
    cloudtaser.io/inject: "true"
    cloudtaser.io/ebpf: "true"
    cloudtaser.io/secretstore-address: "$VAULT_ADDR"
    cloudtaser.io/secretstore-role: "demo"
    cloudtaser.io/secret-paths: "secret/data/cloudtaser/demo/db/credentials"
    cloudtaser.io/env-map: "username=DB_USER,password=DB_PASSWORD,host=DB_HOST,port=DB_PORT"
    cloudtaser.io/vault-ca-configmap: "$OPENBAO_CA_CONFIGMAP"
    cloudtaser.io/vault-ca-configmap-key: "ca.crt"
spec:
  automountServiceAccountToken: true
  containers:
    - name: app
      image: busybox:1
      securityContext:
        readOnlyRootFilesystem: false
      command: ["sh", "-c"]
      args:
        - |
          echo "=== cloudtaser Demo ==="
          echo "DB_USER:     \${DB_USER:-NOT SET}"
          echo "DB_PASSWORD: \$([ -n \"\$DB_PASSWORD\" ] && echo '********' || echo 'NOT SET')"
          echo "DB_HOST:     \${DB_HOST:-NOT SET}"
          echo "DB_PORT:     \${DB_PORT:-NOT SET}"
          echo ""
          echo "Secrets fetched from the verified-TLS OpenBao demo, delivered via memfd_secret."
          echo "Try: kubectl exec demo-app -n $DEMO_NS -- cat /proc/1/environ"
          echo "     (wrapper's environ is structurally clean: secrets enter only after exec)"
          echo "     (child's environ is protected by eBPF kprobe on openat)"
          sleep infinity
DEMO_EOF

  info "Waiting for demo pod"
  if kubectl -n "$DEMO_NS" wait --for=condition=Ready pod/demo-app --timeout=180s >/dev/null 2>&1; then
    ok "Demo pod running"
  else
    warn "Demo pod not ready yet — check: kubectl -n $DEMO_NS describe pod demo-app"
  fi
fi

# ── Summary ────────────────────────────────────────────────────────────
echo ""
info "cloudtaser is running"
echo ""
echo "  Namespace:  $NAMESPACE"
echo "  Components: operator, webhook, eBPF agent ($EBPF_READY nodes)"
if [ -n "$VAULT_ADDR" ]; then
  echo "  Vault:      $VAULT_ADDR"
fi
echo ""

if $DEMO; then
  echo "  OpenBao:    hardened chart v${OPENBAO_CHART_VERSION} (OpenBao ${OPENBAO_APP_VERSION}, verified TLS)"
  echo "  Recovery:   capture the single-node demo unseal key now:"
  echo "              kubectl logs -n $VAULT_NS job/${OPENBAO_RELEASE}-bootstrap | awk '/CLOUDTASER OPENBAO BOOTSTRAP/,/END CLOUDTASER OPENBAO BOOTSTRAP/'"
  echo "  Demo pod:   kubectl -n $DEMO_NS logs demo-app"
  echo "  Score:      kubectl -n $DEMO_NS logs demo-app | grep 'protection score'"
  echo "  Secrets:    kubectl -n $DEMO_NS exec demo-app -- cat /proc/1/environ"
  echo "              (should show NO secrets — they're in memfd_secret)"
  echo ""
fi

echo "  Docs:       https://docs.cloudtaser.io"
echo "  Annotate:   https://docs.cloudtaser.io/configuration/annotations/"
echo ""
