#!/usr/bin/env bash
set -euo pipefail

download_base="${RELEASEPASSPORT_DOWNLOAD_BASE:-https://releasepassport.com/downloads/trial}"
version="${RELEASEPASSPORT_VERSION:-latest}"
installer_revision="2026-05-24.selfhosted.18"
official_registry="registry.releasepassport.com/releasepassport"
install_dir="${RELEASEPASSPORT_INSTALL_DIR:-/usr/local/bin}"
binary_name="${RELEASEPASSPORT_BINARY:-releasepassport}"
download_token="${RELEASEPASSPORT_DOWNLOAD_TOKEN:-}"
download_auth_header="${RELEASEPASSPORT_DOWNLOAD_AUTH_HEADER:-}"
install_token="${RELEASEPASSPORT_INSTALL_TOKEN:-}"
install_exchange_url="${RELEASEPASSPORT_INSTALL_EXCHANGE_URL:-https://releasepassport.com/releasepassport/v1/billing/install-token/exchange}"

target="auto"
namespace="releasepassport"
auth_mode="basic"
admin_email="${RELEASEPASSPORT_ADMIN_EMAIL:-ops@example.com}"
admin_password="${RELEASEPASSPORT_ADMIN_PASSWORD:-}"
domain=""
domain_mode="${RELEASEPASSPORT_DOMAIN_MODE:-auto}"
storage_mode="${RELEASEPASSPORT_STORAGE:-auto}"
first_connector="${RELEASEPASSPORT_FIRST_CONNECTOR:-}"
detected_connectors="${RELEASEPASSPORT_DETECTED_CONNECTORS:-}"
connector_bootstrap_json="${RELEASEPASSPORT_CONNECTOR_BOOTSTRAP_JSON:-}"
registry="${RELEASEPASSPORT_REGISTRY:-${official_registry}}"
registry_secret_name="${RELEASEPASSPORT_REGISTRY_SECRET_NAME:-}"
registry_server="${RELEASEPASSPORT_REGISTRY_SERVER:-registry.releasepassport.com}"
registry_username="${RELEASEPASSPORT_REGISTRY_USERNAME:-}"
registry_password="${RELEASEPASSPORT_REGISTRY_PASSWORD:-}"
license_file=""
license_inline="${RELEASEPASSPORT_LICENSE:-}"
license_public_key="${RELEASEPASSPORT_LICENSE_PUBLIC_KEY:-}"
license_public_key_url="${RELEASEPASSPORT_LICENSE_PUBLIC_KEY_URL:-https://releasepassport.com/releasepassport/v1/billing/license/public-key?format=text}"
auto_detect="false"
auto_enable_detected="false"
dry_run="false"
install_cli="true"
assume_yes="${RELEASEPASSPORT_ASSUME_YES:-false}"
interactive_mode="${RELEASEPASSPORT_INTERACTIVE:-auto}"
ai_provider="${AI_PROVIDER:-${RELEASEPASSPORT_AI_PROVIDER:-}}"
ai_api_key="${AI_API_KEY:-${RELEASEPASSPORT_AI_API_KEY:-${DEEPSEEK_API_KEY:-${OPENAI_API_KEY:-}}}}"
ai_base_url="${AI_BASE_URL:-${RELEASEPASSPORT_AI_BASE_URL:-${DEEPSEEK_BASE_URL:-${OPENAI_BASE_URL:-https://api.openai.com/v1}}}}"
ai_model="${AI_MODEL:-${RELEASEPASSPORT_AI_MODEL:-${DEEPSEEK_MODEL:-${OPENAI_MODEL:-gpt-4.1-mini}}}}"
require_public_access="${RELEASEPASSPORT_REQUIRE_PUBLIC_ACCESS:-false}"

usage() {
  cat <<'USAGE'
Release Passport self-hosted installer

Installer revision: 2026-05-24.selfhosted.18

Usage:
  curl -fsSL https://releasepassport.com/install.sh | bash -s -- --install-token <portal-install-token>

Smart default:
  With only --install-token, the installer detects Kubernetes vs Docker Compose,
  chooses no-domain port-forward unless you enter a domain, uses existing DATABASE_URL/VALKEY_URL when present,
  suggests the first likely connector, exchanges the token for license + registry access,
  and prints the console URL.

Flags:
  --target auto|kubernetes|compose|host|local-demo
  --namespace releasepassport
  --license-file ./releasepassport.license
  --install-token rp_install_v1...
  --version latest|0.1.0-trial
  --admin-email ops@example.com
  --domain release-passport.example.com
  --domain-mode port-forward|domain
  --storage auto|bundled|existing
  --first-connector auto|github-actions|gitlab-ci|jenkins|bitbucket|azure-devops|argocd|flux|kubernetes|prometheus|datadog|newrelic|sentry|pagerduty|jira|slack|sonarqube|snyk|trivy|grype|opa|kyverno|launchdarkly|argo-rollouts|flagger
  --registry registry.releasepassport.com/releasepassport
  --registry-secret-name releasepassport-registry
  --registry-server registry.releasepassport.com
  RELEASEPASSPORT_REGISTRY_SERVER=registry.releasepassport.com
  RELEASEPASSPORT_REGISTRY_USERNAME=<user>
  RELEASEPASSPORT_REGISTRY_PASSWORD=<token>
  RELEASEPASSPORT_REGISTRY_SECRET_NAME=releasepassport-registry
  RELEASEPASSPORT_INSTALL_TOKEN=rp_install_v1...
  RELEASEPASSPORT_INSTALL_ID=<existing-install-id>
  RELEASEPASSPORT_LICENSE_PUBLIC_KEY=<base64-ed25519-public-key>
  RELEASEPASSPORT_LICENSE_PUBLIC_KEY_URL=https://releasepassport.com/releasepassport/v1/billing/license/public-key?format=text
  RELEASEPASSPORT_REQUIRE_PUBLIC_ACCESS=true
  --auth basic|oidc|proxy
  --auto-detect
  --auto-enable-detected
  --dry-run[=true|false]
  --no-cli
  --interactive
  --non-interactive
  --yes
USAGE
}

normalize_bool() {
  case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in
    true|1|yes|on) printf 'true' ;;
    false|0|no|off) printf 'false' ;;
    *) echo "invalid boolean for $2: $1" >&2; exit 2 ;;
  esac
}

require_public_access="$(normalize_bool "$require_public_access" "RELEASEPASSPORT_REQUIRE_PUBLIC_ACCESS")"

can_prompt() {
  [[ -t 1 && -r /dev/tty && -w /dev/tty ]]
}

prompt_input() {
  local prompt="$1"
  local default_value="${2:-}"
  local answer=""
  if ! can_prompt; then
    printf '%s' "$default_value"
    return 0
  fi
  if [[ -n "$default_value" ]]; then
    printf "%s [%s]: " "$prompt" "$default_value" >/dev/tty
  else
    printf "%s: " "$prompt" >/dev/tty
  fi
  IFS= read -r answer </dev/tty || answer=""
  if [[ -z "$answer" ]]; then
    answer="$default_value"
  fi
  printf '%s' "$answer"
}

prompt_yes_no() {
  local prompt="$1"
  local default_value="${2:-no}"
  local suffix="[y/N]"
  local answer=""
  if [[ "$(printf '%s' "$default_value" | tr '[:upper:]' '[:lower:]')" == "yes" ]]; then
    suffix="[Y/n]"
  fi
  if ! can_prompt || [[ "$assume_yes" == "true" ]]; then
    [[ "$default_value" == "yes" ]]
    return $?
  fi
  printf "%s %s " "$prompt" "$suffix" >/dev/tty
  IFS= read -r answer </dev/tty || answer=""
  answer="$(printf '%s' "${answer:-$default_value}" | tr '[:upper:]' '[:lower:]')"
  case "$answer" in
    y|yes) return 0 ;;
    *) return 1 ;;
  esac
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --target=*) target="${1#*=}"; shift ;;
    --target) target="${2:-}"; shift 2 ;;
    --namespace=*) namespace="${1#*=}"; shift ;;
    --namespace) namespace="${2:-}"; shift 2 ;;
    --license-file=*) license_file="${1#*=}"; shift ;;
    --license-file) license_file="${2:-}"; shift 2 ;;
    --install-token=*) install_token="${1#*=}"; shift ;;
    --install-token) install_token="${2:-}"; shift 2 ;;
    --version=*) version="${1#*=}"; shift ;;
    --version) version="${2:-}"; shift 2 ;;
    --admin-email=*) admin_email="${1#*=}"; shift ;;
    --admin-email) admin_email="${2:-}"; shift 2 ;;
    --domain=*) domain="${1#*=}"; shift ;;
    --domain) domain="${2:-}"; shift 2 ;;
    --domain-mode=*) domain_mode="${1#*=}"; shift ;;
    --domain-mode) domain_mode="${2:-}"; shift 2 ;;
    --storage=*) storage_mode="${1#*=}"; shift ;;
    --storage) storage_mode="${2:-}"; shift 2 ;;
    --first-connector=*) first_connector="${1#*=}"; auto_detect="true"; shift ;;
    --first-connector) first_connector="${2:-}"; auto_detect="true"; shift 2 ;;
    --registry=*) registry="${1#*=}"; shift ;;
    --registry) registry="${2:-}"; shift 2 ;;
    --registry-secret-name=*) registry_secret_name="${1#*=}"; shift ;;
    --registry-secret-name) registry_secret_name="${2:-}"; shift 2 ;;
    --registry-server=*) registry_server="${1#*=}"; shift ;;
    --registry-server) registry_server="${2:-}"; shift 2 ;;
    --auth=*) auth_mode="${1#*=}"; shift ;;
    --auth) auth_mode="${2:-}"; shift 2 ;;
    --auto-detect) auto_detect="true"; shift ;;
    --auto-enable-detected) auto_enable_detected="true"; auto_detect="true"; shift ;;
    --dry-run=*) dry_run="$(normalize_bool "${1#*=}" "--dry-run")"; shift ;;
    --dry-run) dry_run="true"; shift ;;
    --interactive) interactive_mode="true"; shift ;;
    --non-interactive) interactive_mode="false"; assume_yes="true"; shift ;;
    --yes) assume_yes="true"; shift ;;
    --no-cli=*)
      no_cli_value="$(normalize_bool "${1#*=}" "--no-cli")"
      if [[ "$no_cli_value" == "true" ]]; then
        install_cli="false"
      else
        install_cli="true"
      fi
      shift
      ;;
    --no-cli) install_cli="false"; shift ;;
    -h|--help) usage; exit 0 ;;
    *) echo "unknown flag: $1" >&2; usage; exit 2 ;;
  esac
done

requested_target="$target"

case "$target" in
  auto|kubernetes|compose|host|local-demo) ;;
  *) echo "unsupported target: ${target}" >&2; usage; exit 2 ;;
esac

case "$auth_mode" in
  basic|oidc|proxy) ;;
  *) echo "unsupported auth mode: ${auth_mode}" >&2; exit 2 ;;
esac

case "$domain_mode" in
  auto|port-forward|domain) ;;
  *) echo "unsupported domain mode: ${domain_mode}" >&2; exit 2 ;;
esac

reserved_self_hosted_domain() {
  local value
  value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's#^https?://##; s#/.*$##; s#:[0-9]+$##; s#[.]$##')"
  case "$value" in
    releasepassport.com|www.releasepassport.com|api.releasepassport.com|registry.releasepassport.com)
      return 0
      ;;
    *)
      return 1
      ;;
  esac
}

validate_self_hosted_domain() {
  local value="$1"
  if [[ -z "$value" ]]; then
    return 0
  fi
  if reserved_self_hosted_domain "$value"; then
    cat >&2 <<'EOF'
releasepassport.com is the public commercial portal, not a customer self-hosted runtime domain.

Use one of these instead:
  - no domain: omit --domain and use the printed localhost/SSH tunnel URLs
  - customer domain: --domain rp.customer.example.com
EOF
    exit 2
  fi
}

case "$storage_mode" in
  auto|bundled|existing) ;;
  *) echo "unsupported storage mode: ${storage_mode}" >&2; exit 2 ;;
esac

case "$interactive_mode" in
  auto|true|false) ;;
  *) echo "unsupported interactive mode: ${interactive_mode}" >&2; exit 2 ;;
esac

case "$first_connector" in
  ""|auto|github-actions|gitlab-ci|jenkins|bitbucket|azure-devops|argocd|flux|kubernetes|prometheus|datadog|newrelic|sentry|pagerduty|jira|slack|sonarqube|snyk|trivy|grype|opa|kyverno|launchdarkly|argo-rollouts|flagger) ;;
  *) echo "unsupported first connector: ${first_connector}" >&2; exit 2 ;;
esac

if [[ "$domain_mode" == "port-forward" ]]; then
  domain=""
fi
validate_self_hosted_domain "$domain"

curl_args=(-fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 1 --retry-all-errors)
if [[ -n "$download_auth_header" ]]; then
  curl_args+=(-H "$download_auth_header")
elif [[ -n "$download_token" ]]; then
  curl_args+=(-H "Authorization: Bearer ${download_token}")
fi

fetch() {
  curl "${curl_args[@]}" "$@"
}

post_json() {
  local url="$1"
  curl "${curl_args[@]}" -H "Content-Type: application/json" -X POST --data-binary @- "$url"
}

normalize_registry() {
  printf '%s' "${1%/}"
}

sha256_file() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  else
    shasum -a 256 "$1" | awk '{print $1}'
  fi
}

install_id_hash() {
  local value="releasepassport-install-id:${1}"
  if command -v openssl >/dev/null 2>&1; then
    printf '%s' "$value" | openssl dgst -binary -sha256 | openssl base64 -A | tr '+/' '-_' | tr -d '='
  elif command -v python3 >/dev/null 2>&1; then
    python3 - "$1" <<'PY'
import base64
import hashlib
import sys

value = "releasepassport-install-id:" + sys.argv[1].strip()
print(base64.urlsafe_b64encode(hashlib.sha256(value.encode()).digest()).decode().rstrip("="))
PY
  else
    echo "openssl or python3 is required to hash RELEASEPASSPORT_INSTALL_ID" >&2
    exit 1
  fi
}

json_field() {
  local path="$1"
  if ! command -v python3 >/dev/null 2>&1; then
    echo "python3 is required when --install-token is used." >&2
    exit 1
  fi
  python3 -c '
import json
import sys

path = sys.argv[1].split(".")
payload = json.load(sys.stdin)
value = payload
for part in path:
    if isinstance(value, dict):
        value = value.get(part, "")
    else:
        value = ""
if isinstance(value, (dict, list)):
    print(json.dumps(value, separators=(",", ":")))
elif value is None:
    print("")
else:
    print(str(value))
' "$path"
}

json_escape() {
  if command -v python3 >/dev/null 2>&1; then
    python3 - "$1" <<'PY'
import json
import sys

print(json.dumps(sys.argv[1]))
PY
  else
    printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')"
  fi
}

random_password() {
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -base64 24 | tr '+/' '-_' | tr -d '=\n'
  else
    date +%s | shasum -a 256 | awk '{print $1}'
  fi
}

random_install_id() {
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -hex 32
  else
    date +%s | shasum -a 256 | awk '{print $1}'
  fi
}

require_command() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo "$1 is required for target ${target}." >&2
    exit 1
  fi
}

has_docker_compose() {
  command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1
}

has_kubernetes_commands() {
  command -v kubectl >/dev/null 2>&1 && command -v helm >/dev/null 2>&1
}

has_kubernetes_tooling() {
  has_kubernetes_commands && kubectl cluster-info --request-timeout=3s >/dev/null 2>&1
}

require_kubernetes_runtime() {
  require_command kubectl
  require_command helm
  if ! kubectl cluster-info --request-timeout=3s >/dev/null 2>&1; then
    cat >&2 <<EOF
kubectl and Helm are installed, but kubectl cannot reach an active cluster context.

Verify the target cluster first:
  kubectl config current-context
  kubectl cluster-info --request-timeout=3s
  helm version

Detected platform: $(detect_platform_label)

EOF
    print_runtime_dependency_next_steps
    exit 1
  fi
}

detect_platform_label() {
  local kernel=""
  kernel="$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]' || true)"
  case "$kernel" in
    darwin) printf 'macos' ;;
    linux)
      if [[ -r /etc/os-release ]]; then
        # shellcheck disable=SC1091
        . /etc/os-release
        printf '%s' "${ID:-linux}"
      else
        printf 'linux'
      fi
      ;;
    msys*|mingw*|cygwin*) printf 'windows-bash' ;;
    *) printf '%s' "${kernel:-unknown}" ;;
  esac
}

print_runtime_dependency_next_steps() {
  local platform=""
  platform="$(detect_platform_label)"
  case "$platform" in
    ubuntu|debian)
      cat >&2 <<'EOF'
Detected Debian/Ubuntu.

Install Docker Engine + Compose v2:
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl gnupg
  # Use Docker's official apt repository for docker-compose-plugin:
  # https://docs.docker.com/engine/install/debian/ or /ubuntu/
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Or install Kubernetes tooling:
  # kubectl and Helm must point at a reachable intended cluster before rerunning.
  kubectl config current-context
  kubectl cluster-info --request-timeout=3s
  helm version
EOF
      ;;
    rhel|rocky|almalinux|centos|fedora)
      cat >&2 <<'EOF'
Detected RHEL/Fedora family.

Install Docker Engine + Compose v2 from Docker's official yum/dnf repository,
then verify:
  docker version
  docker compose version

Or configure Kubernetes tooling first:
  kubectl config current-context
  kubectl cluster-info --request-timeout=3s
  helm version
EOF
      ;;
    macos)
      cat >&2 <<'EOF'
Detected macOS.

Install a local runtime:
  brew install --cask docker
  open -a Docker
  docker compose version

Or install Kubernetes tooling:
  brew install kubectl helm
  kubectl config current-context
  kubectl cluster-info --request-timeout=3s
  helm version
EOF
      ;;
    windows-bash)
      cat >&2 <<'EOF'
Detected Windows-style bash.

Use the PowerShell launcher so the runtime install runs inside WSL2:
  powershell -ExecutionPolicy Bypass -Command "irm https://releasepassport.com/install.ps1 | iex; Install-ReleasePassport -InstallToken '<portal-install-token>'"

Inside WSL2, install Docker Engine + Compose v2 or configure kubectl + Helm.
EOF
      ;;
    *)
      cat >&2 <<'EOF'
Install one runtime path, then rerun the same one-step install command:
  - Docker Engine plus Docker Compose v2
  - kubectl plus Helm 3 with an active cluster context
EOF
      ;;
  esac
}

print_runtime_dependency_help() {
  cat >&2 <<EOF
No self-hosted runtime target is available on this machine.

Release Passport runtime install needs one of these:
  - Docker Engine plus Docker Compose v2 for --target compose
  - kubectl plus Helm 3 with an active cluster context for --target kubernetes

Detected platform: $(detect_platform_label)

EOF
  print_runtime_dependency_next_steps
  cat >&2 <<'EOF'

What to run next:
  curl -fsSL https://releasepassport.com/install.sh | bash -s -- --install-token <portal-install-token>

The installer did not continue as CLI-only because Trial is meant to install the
self-hosted runtime unless you explicitly choose --target host.
EOF
}

require_compose_runtime() {
  if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
    return 0
  fi
  cat >&2 <<EOF
Docker Engine with Docker Compose v2 is required for --target compose.

Install Docker Engine and the docker compose plugin, then verify:
  docker version
  docker compose version

Detected platform: $(detect_platform_label)

EOF
  print_runtime_dependency_next_steps
  cat >&2 <<'EOF'

If this is a Kubernetes install, use --target kubernetes after kubectl and Helm
are configured. If you only want the CLI, use --target host explicitly.
EOF
  exit 1
}

detect_connector_hints() {
  local hints=()
  add_hint() {
    local candidate="$1"
    local existing=""
    for existing in "${hints[@]}"; do
      if [[ "$existing" == "$candidate" ]]; then
        return 0
      fi
    done
    hints+=("$candidate")
  }

  if [[ "${GITHUB_ACTIONS:-}" == "true" || -n "${RELEASEPASSPORT_GITHUB_REPOSITORY:-}" ]]; then
    add_hint "github-actions"
  fi
  if [[ "${GITLAB_CI:-}" == "true" || -n "${CI_PROJECT_ID:-}" || -n "${RELEASEPASSPORT_GITLAB_PROJECT_ID:-}" ]]; then add_hint "gitlab-ci"; fi
  if [[ -n "${JENKINS_URL:-}" || -n "${BUILD_TAG:-}" ]]; then add_hint "jenkins"; fi
  if [[ -n "${BITBUCKET_WORKSPACE:-}" || -n "${BITBUCKET_REPO_SLUG:-}" ]]; then add_hint "bitbucket"; fi
  if [[ "${TF_BUILD:-}" == "True" || "${TF_BUILD:-}" == "true" || -n "${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI:-}" ]]; then add_hint "azure-devops"; fi
  if [[ -n "${RELEASEPASSPORT_ARGOCD_BASE_URL:-}" || -n "${ARGOCD_SERVER:-}" || -n "${ARGOCD_AUTH_TOKEN:-}" ]]; then add_hint "argocd"; fi
  if [[ -n "${FLUX_NAMESPACE:-}" || -n "${RELEASEPASSPORT_FLUX_NAMESPACE:-}" ]] || command -v flux >/dev/null 2>&1; then add_hint "flux"; fi
  if command -v kubectl >/dev/null 2>&1; then add_hint "kubernetes"; fi
  if [[ -n "${RELEASEPASSPORT_PROMETHEUS_URL:-}" || -n "${PROMETHEUS_URL:-}" ]]; then add_hint "prometheus"; fi
  if [[ -n "${DD_API_KEY:-}" || -n "${DATADOG_API_KEY:-}" || -n "${DD_SITE:-}" ]]; then add_hint "datadog"; fi
  if [[ -n "${NEW_RELIC_API_KEY:-}" || -n "${NEW_RELIC_ACCOUNT_ID:-}" || -n "${NEW_RELIC_LICENSE_KEY:-}" ]]; then add_hint "newrelic"; fi
  if [[ -n "${SENTRY_AUTH_TOKEN:-}" || -n "${SENTRY_DSN:-}" || -n "${SENTRY_ORG:-}" ]]; then add_hint "sentry"; fi
  if [[ -n "${PAGERDUTY_API_KEY:-}" || -n "${PAGERDUTY_SERVICE_ID:-}" ]]; then add_hint "pagerduty"; fi
  if [[ -n "${JIRA_BASE_URL:-}" || -n "${JIRA_URL:-}" || -n "${ATLASSIAN_SITE_URL:-}" ]]; then add_hint "jira"; fi
  if [[ -n "${SLACK_WEBHOOK_URL:-}" || -n "${SLACK_BOT_TOKEN:-}" ]]; then add_hint "slack"; fi
  if [[ -n "${SONAR_HOST_URL:-}" || -n "${SONAR_TOKEN:-}" ]]; then add_hint "sonarqube"; fi
  if [[ -n "${SNYK_TOKEN:-}" ]] || command -v snyk >/dev/null 2>&1; then add_hint "snyk"; fi
  if command -v trivy >/dev/null 2>&1; then add_hint "trivy"; fi
  if command -v grype >/dev/null 2>&1; then add_hint "grype"; fi
  if command -v opa >/dev/null 2>&1; then add_hint "opa"; fi
  if command -v kyverno >/dev/null 2>&1; then add_hint "kyverno"; fi
  if [[ -n "${LAUNCHDARKLY_ACCESS_TOKEN:-}" || -n "${LD_ACCESS_TOKEN:-}" || -n "${LAUNCHDARKLY_PROJECT_KEY:-}" ]]; then add_hint "launchdarkly"; fi
  if command -v kubectl-argo-rollouts >/dev/null 2>&1; then add_hint "argo-rollouts"; fi
  if [[ -n "${FLAGGER_NAMESPACE:-}" || -n "${RELEASEPASSPORT_FLAGGER_NAMESPACE:-}" ]]; then add_hint "flagger"; fi

  local IFS=,
  printf '%s' "${hints[*]-}"
}

connector_bootstrap_entry() {
  local hint="$1"
  local id=""
  local name=""
  local group=""
  local provider=""
  local required=""
  local next_action=""
  case "$hint" in
    github-actions) id="connector-ci-github-actions"; name="GitHub Actions"; group="ci"; provider="github_actions"; required="repository,apiUrl,credentialSecretRef"; next_action="Confirm repository scope and add a GitHub token secret ref before live sync." ;;
    gitlab-ci) id="connector-ci-gitlab"; name="GitLab CI"; group="ci"; provider="gitlab_ci"; required="projectId,pipelineId,apiUrl,credentialSecretRef"; next_action="Confirm project/pipeline scope and add a GitLab token secret ref before live sync." ;;
    jenkins) id="connector-ci-jenkins"; name="Jenkins"; group="ci"; provider="jenkins"; required="baseUrl,job,credentialSecretRef"; next_action="Add Jenkins base URL, job name, and credential secret ref." ;;
    bitbucket) id="connector-source-bitbucket"; name="Bitbucket"; group="source"; provider="bitbucket"; required="repository,credentialSecretRef"; next_action="Confirm repository scope and add a Bitbucket credential secret ref." ;;
    azure-devops) id="connector-source-azure-devops"; name="Azure DevOps Repos"; group="source"; provider="azure_devops"; required="repository,credentialSecretRef"; next_action="Confirm project/repository scope and add an Azure DevOps credential secret ref." ;;
    argocd) id="connector-gitops-argocd"; name="Argo CD"; group="gitops"; provider="argocd"; required="baseUrl,application,credentialSecretRef"; next_action="Add Argo CD base URL, application, and scoped token secret ref." ;;
    flux) id="connector-gitops-flux"; name="Flux"; group="gitops"; provider="flux"; required="namespace"; next_action="Confirm Flux namespace and GitRepository/Kustomization scope." ;;
    kubernetes) id="connector-orchestration-kubernetes"; name="Kubernetes"; group="orchestration"; provider="kubernetes"; required="apiUrl,serviceAccountSecretRef"; next_action="Confirm namespace scope and service account permissions." ;;
    prometheus) id="connector-metrics-prometheus"; name="Prometheus"; group="metrics"; provider="prometheus"; required="baseUrl"; next_action="Add Prometheus base URL and release SLO query scope." ;;
    datadog) id="connector-metrics-datadog"; name="Datadog metrics"; group="metrics"; provider="datadog"; required="baseUrl,credentialSecretRef"; next_action="Add Datadog site/API scope and credential secret ref." ;;
    newrelic) id="connector-metrics-newrelic"; name="New Relic"; group="metrics"; provider="new_relic"; required="baseUrl,credentialSecretRef"; next_action="Add New Relic account/region scope and credential secret ref." ;;
    sentry) id="connector-incident-sentry"; name="Sentry"; group="incident"; provider="sentry"; required="baseUrl,project,credentialSecretRef"; next_action="Add Sentry organization/project scope and token secret ref." ;;
    pagerduty) id="connector-incident-pagerduty"; name="PagerDuty"; group="incident"; provider="pagerduty"; required="baseUrl,service,credentialSecretRef"; next_action="Add PagerDuty service scope and API credential secret ref." ;;
    jira) id="connector-change-jira"; name="Jira"; group="change"; provider="jira"; required="baseUrl,project,credentialSecretRef"; next_action="Add Jira site/project scope and credential secret ref." ;;
    slack) id="connector-incident-slack"; name="Slack alerts"; group="incident"; provider="slack"; required="webhookSecretRef,channel"; next_action="Add Slack channel scope and webhook secret ref." ;;
    sonarqube) id="connector-security-sonarqube"; name="SonarQube"; group="security"; provider="sonarqube"; required="baseUrl,project,credentialSecretRef"; next_action="Add SonarQube URL/project scope and token secret ref." ;;
    snyk) id="connector-security-snyk"; name="Snyk"; group="security"; provider="snyk"; required="baseUrl,credentialSecretRef"; next_action="Add Snyk org/project scope and token secret ref." ;;
    trivy) id="connector-security-trivy"; name="Trivy"; group="security"; provider="trivy"; required="artifactPath"; next_action="Point Release Passport to the Trivy report artifact path." ;;
    grype) id="connector-security-grype"; name="Grype"; group="security"; provider="grype"; required="artifactPath"; next_action="Point Release Passport to the Grype report artifact path." ;;
    opa) id="connector-security-opa"; name="OPA"; group="security"; provider="opa"; required="url"; next_action="Add OPA endpoint or policy bundle evidence path." ;;
    kyverno) id="connector-security-kyverno"; name="Kyverno"; group="security"; provider="kyverno"; required="namespace"; next_action="Confirm Kyverno namespace and policy report scope." ;;
    launchdarkly) id="connector-orchestration-launchdarkly"; name="LaunchDarkly"; group="orchestration"; provider="launchdarkly"; required="project,credentialSecretRef"; next_action="Add LaunchDarkly project/environment scope and token secret ref." ;;
    argo-rollouts) id="connector-orchestration-argo-rollouts"; name="Argo Rollouts"; group="orchestration"; provider="argo_rollouts"; required="apiUrl,namespace"; next_action="Confirm rollout namespace and analysis template scope." ;;
    flagger) id="connector-orchestration-flagger"; name="Flagger"; group="orchestration"; provider="flagger"; required="namespace"; next_action="Confirm Flagger namespace and canary scope." ;;
    *) return 1 ;;
  esac
  printf '{"id":%s,"name":%s,"group":%s,"provider":%s,"status":"detected","confidence":92,"signals":[%s],"config":{"bootstrapSource":"installer","bootstrapHint":%s,"requiredConfig":%s,"nextAction":%s}}' \
    "$(json_escape "$id")" \
    "$(json_escape "$name")" \
    "$(json_escape "$group")" \
    "$(json_escape "$provider")" \
    "$(json_escape "installer:auto-detect:${hint}")" \
    "$(json_escape "$hint")" \
    "$(json_escape "$required")" \
    "$(json_escape "$next_action")"
}

connector_bootstrap_json_from_hints() {
  local hints_csv="$1"
  if [[ -z "$hints_csv" ]]; then
    printf '[]'
    return 0
  fi
  local old_ifs="$IFS"
  IFS=,
  read -r -a hints <<<"$hints_csv"
  IFS="$old_ifs"
  local first="true"
  local hint=""
  local entry=""
  printf '['
  for hint in "${hints[@]}"; do
    entry="$(connector_bootstrap_entry "$hint" || true)"
    if [[ -z "$entry" ]]; then
      continue
    fi
    if [[ "$first" != "true" ]]; then
      printf ','
    fi
    printf '%s' "$entry"
    first="false"
  done
  printf ']'
}

first_connector_from_hints() {
  local hints="$1"
  printf '%s' "${hints%%,*}"
}

resolve_smart_inputs() {
  if [[ -z "$install_token" && -z "$license_file" && -z "$license_inline" && -z "$registry_username" && "$dry_run" != "true" ]]; then
    install_token="$(prompt_input "Release Passport install token from https://releasepassport.com/portal" "")"
    if [[ -z "$install_token" ]]; then
      cat >&2 <<'EOF'
Install token is required for the normal self-hosted bootstrap.

Open https://releasepassport.com/portal, generate an install token, then run:
  curl -fsSL https://releasepassport.com/install.sh | bash -s -- --install-token <portal-install-token>
EOF
      exit 2
    fi
  fi

  if [[ "$domain_mode" == "auto" ]]; then
    if [[ -n "$domain" ]]; then
      domain_mode="domain"
    elif prompt_yes_no "Do you already have a DNS name for this runtime?" "no"; then
      domain="$(prompt_input "Runtime domain" "rp.customer.example.com")"
      domain_mode="domain"
    else
      domain_mode="port-forward"
    fi
  fi
  if [[ "$domain_mode" == "domain" && -z "$domain" ]]; then
    domain="$(prompt_input "Runtime domain" "")"
    if [[ -z "$domain" ]]; then
      echo "--domain is required when --domain-mode domain is selected." >&2
      exit 2
    fi
  fi
  validate_self_hosted_domain "$domain"
  if [[ "$domain_mode" == "port-forward" ]]; then
    domain=""
  fi

  if [[ "$storage_mode" == "auto" ]]; then
    if [[ -n "${DATABASE_URL:-}" && -n "${VALKEY_URL:-}" ]]; then
      storage_mode="existing"
    else
      storage_mode="bundled"
    fi
  fi

  if [[ -z "$detected_connectors" ]]; then
    detected_connectors="$(detect_connector_hints)"
  fi
  if [[ -z "$first_connector" || "$first_connector" == "auto" ]]; then
    first_connector="$(first_connector_from_hints "$detected_connectors")"
  fi
  if [[ -n "$detected_connectors" || -n "$first_connector" ]]; then
    auto_detect="true"
  fi
  if [[ -z "$connector_bootstrap_json" && -n "$detected_connectors" ]]; then
    connector_bootstrap_json="$(connector_bootstrap_json_from_hints "$detected_connectors")"
  fi
}

resolve_runtime_target() {
  if [[ "$target" != "auto" && "$target" != "local-demo" ]]; then
    return 0
  fi
  local kube="false"
  local compose="false"
  if has_kubernetes_tooling; then
    kube="true"
  fi
  if has_docker_compose; then
    compose="true"
  fi
  if [[ "$kube" == "true" && "$compose" == "true" && "$assume_yes" != "true" && "$dry_run" != "true" && can_prompt ]]; then
    local choice
    choice="$(prompt_input "Detected both Kubernetes and Docker Compose. Install target (kubernetes/compose)" "kubernetes")"
    case "$choice" in
      kubernetes|kube) target="kubernetes" ;;
      compose|docker|docker-compose) target="compose" ;;
      *) echo "unsupported install target choice: ${choice}" >&2; exit 2 ;;
    esac
  elif [[ "$kube" == "true" ]]; then
    target="kubernetes"
  elif [[ "$compose" == "true" ]]; then
    target="compose"
  else
    print_runtime_dependency_help
    exit 1
  fi
}

detect_host_ip() {
  if command -v hostname >/dev/null 2>&1; then
    hostname -I 2>/dev/null | awk '{print $1}' || true
  elif command -v ipconfig >/dev/null 2>&1; then
    ipconfig getifaddr en0 2>/dev/null || true
  fi
}

print_access_guidance() {
  local runtime_target="$1"
  local app_url="$2"
  local api_url="$3"
  if [[ -n "$domain" ]]; then
    echo "Public runtime URL: ${app_url}"
    echo "Public API URL: ${api_url}"
    if [[ "$runtime_target" == "compose" ]]; then
      echo "Domain mode: point DNS for ${domain} at this host and terminate HTTPS with your reverse proxy."
      echo "Proxy web traffic to 127.0.0.1:18080 and API traffic under /releasepassport/v1 to 127.0.0.1:18081."
    else
      echo "Domain mode: route ${domain} through your Kubernetes Gateway/TLS configuration."
    fi
    return 0
  fi

  echo "Local dashboard URL: ${app_url}"
  echo "Local API URL: ${api_url}"
  local host_ip
  host_ip="$(detect_host_ip | tr -d '[:space:]')"
  if [[ -n "$host_ip" ]]; then
    echo "Host IP detected: ${host_ip}"
    echo "For a remote VM, prefer an SSH tunnel from your laptop:"
    echo "  ssh -L 18080:127.0.0.1:18080 -L 18081:127.0.0.1:18081 <user>@${host_ip}"
  fi
}

wait_http_ready() {
  local label="$1"
  local url="$2"
  local attempts="${3:-60}"
  local delay="${4:-2}"
  local i=1
  echo "Waiting for ${label} readiness: ${url}"
  while [[ "$i" -le "$attempts" ]]; do
    if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then
      echo "${label} is ready."
      return 0
    fi
    sleep "$delay"
    i=$((i + 1))
  done
  echo "${label} did not become ready at ${url} within $((attempts * delay))s." >&2
  return 1
}

wait_compose_ready() {
  echo "Waiting for Release Passport compose services to become ready."
  docker compose --env-file "$env_file" -f "$compose_file" ps
  if ! wait_http_ready "Release Passport web" "${compose_ready_web_url}/readyz" 60 2; then
    echo "Compose diagnostics:" >&2
    docker compose --env-file "$env_file" -f "$compose_file" ps >&2 || true
    docker compose --env-file "$env_file" -f "$compose_file" logs --tail=80 web api worker >&2 || true
    return 1
  fi
  if ! wait_http_ready "Release Passport API" "${compose_ready_api_url}/releasepassport/v1/readyz" 60 2; then
    echo "Compose diagnostics:" >&2
    docker compose --env-file "$env_file" -f "$compose_file" ps >&2 || true
    docker compose --env-file "$env_file" -f "$compose_file" logs --tail=80 web api worker >&2 || true
    return 1
  fi
}

wait_kubernetes_ready() {
  echo "Waiting for Release Passport Kubernetes deployments to become ready."
  if ! kubectl -n "${namespace}" rollout status deploy/releasepassport-api --timeout=180s ||
    ! kubectl -n "${namespace}" rollout status deploy/releasepassport-worker --timeout=180s ||
    ! kubectl -n "${namespace}" rollout status deploy/releasepassport-web --timeout=180s; then
    echo "Kubernetes diagnostics:" >&2
    kubectl -n "${namespace}" get deploy,pod,svc >&2 || true
    kubectl -n "${namespace}" describe deploy/releasepassport-api deploy/releasepassport-worker deploy/releasepassport-web >&2 || true
    exit 1
  fi
}

resolve_domain_records() {
  local host="$1"
  if command -v getent >/dev/null 2>&1; then
    getent ahosts "$host" 2>/dev/null | awk '{print $1}' | sort -u | tr '\n' ' ' || true
  elif command -v dig >/dev/null 2>&1; then
    { dig +short A "$host"; dig +short AAAA "$host"; } 2>/dev/null | sort -u | tr '\n' ' ' || true
  elif command -v host >/dev/null 2>&1; then
    host "$host" 2>/dev/null | awk '/has address|has IPv6 address/ {print $NF}' | sort -u | tr '\n' ' ' || true
  elif command -v nslookup >/dev/null 2>&1; then
    nslookup "$host" 2>/dev/null | awk '/^Address: / {print $2}' | sort -u | tr '\n' ' ' || true
  fi
}

probe_public_url() {
  local label="$1"
  local url="$2"
  if curl -fsS --max-time 8 "$url" >/dev/null 2>&1; then
    echo "  ✓ ${label}: ${url}"
    return 0
  fi
  echo "  ✗ ${label}: ${url}" >&2
  return 1
}

check_public_exposure() {
  local runtime_target="$1"
  local app_url="$2"
  local api_url="$3"
  if [[ -z "$domain" ]]; then
    echo "Public exposure check: skipped because no customer domain was configured."
    echo "This install is local/SSH-tunnel only until you configure DNS, TLS, and ingress/reverse proxy."
    return 0
  fi

  echo "Checking customer domain exposure for ${domain}."
  local records
  records="$(resolve_domain_records "$domain" | sed -E 's/[[:space:]]+$//')"
  local failed="false"
  if [[ -n "$records" ]]; then
    echo "  ✓ DNS resolves: ${records}"
  else
    echo "  ✗ DNS does not resolve for ${domain}" >&2
    failed="true"
  fi

  probe_public_url "web readiness" "${app_url}/readyz" || failed="true"
  probe_public_url "API readiness" "${api_url}/readyz" || failed="true"

  if [[ "$failed" == "false" ]]; then
    echo "Public exposure check passed for ${domain}."
    return 0
  fi

  cat >&2 <<EOF
Public exposure check did not pass for ${domain}.

The Release Passport runtime is installed, but the customer domain is not fully reachable from this installer host.
Fix the public path before handing this URL to users:
  - DNS A/AAAA/CNAME must point to the customer load balancer, gateway, or VM.
  - Firewall/security group rules must allow inbound 80/443 from the networks that should use the console.
  - TLS/reverse proxy/Gateway must route ${app_url}/readyz and ${api_url}/readyz to the Release Passport web/API services.
  - Compose domain mode requires your reverse proxy to forward web traffic to 127.0.0.1:18080 and API traffic to 127.0.0.1:18081.

Set RELEASEPASSPORT_REQUIRE_PUBLIC_ACCESS=true to make this check fail the installer in production automation.
EOF
  [[ "$require_public_access" == "true" ]] && return 1
  return 0
}

print_install_plan() {
  echo
  echo "Release Passport install plan"
  echo "  installer: ${installer_revision}"
  echo "  target: ${target}"
  echo "  auth: ${auth_mode}"
  echo "  storage: ${storage_mode}"
  echo "  domain mode: ${domain_mode}"
  if [[ -n "$domain" ]]; then
    echo "  domain: ${domain}"
  fi
  if [[ -n "$first_connector" ]]; then
    echo "  first connector hint: ${first_connector}"
  else
    echo "  first connector hint: none detected"
  fi
  if [[ -n "$detected_connectors" ]]; then
    echo "  detected connector hints: ${detected_connectors}"
    echo "  connector bootstrap: non-secret metadata will seed the runtime connector wizard"
  fi
  if [[ -n "$ai_api_key" ]]; then
    echo "  AI review: BYOK provider configured (${ai_provider:-openai-compatible}, ${ai_model})"
  else
    echo "  AI review: deterministic fallback until AI_API_KEY or OPENAI_API_KEY is provided"
  fi
  if [[ -n "$install_token" ]]; then
    echo "  entitlement: portal install token exchange"
  elif [[ -n "$license_inline" || -n "$license_file" ]]; then
    echo "  entitlement: provided license material"
  else
    echo "  entitlement: registry credentials required"
  fi
  echo
}

confirm_install_plan() {
  if [[ "$dry_run" == "true" || "$assume_yes" == "true" || "$interactive_mode" == "false" ]]; then
    return 0
  fi
  if ! can_prompt; then
    if [[ "$interactive_mode" == "true" ]]; then
      cat >&2 <<'EOF'
Interactive install was requested, but no writable terminal was detected.

Run from an interactive shell or download then execute:
  curl -fsSL https://releasepassport.com/install.sh -o install.sh
  bash install.sh --install-token <portal-install-token> --interactive
EOF
      exit 2
    fi
    echo "No interactive terminal detected; continuing with the smart install plan."
    echo "Use --interactive from a TTY to review target, domain, storage, and connector choices."
    return 0
  fi
  if ! prompt_yes_no "Continue with this Release Passport install plan?" "yes"; then
    cat >&2 <<'EOF'
Install cancelled.

Rerun with explicit choices, for example:
  bash install.sh --install-token <portal-install-token> --target compose --domain rp.customer.example.com
EOF
    exit 130
  fi
}

discover_license_public_key() {
  if [[ -n "$license_public_key" ]]; then
    return 0
  fi
  echo "Fetching Release Passport license verification public key."
  if ! license_public_key="$(fetch "$license_public_key_url" | tr -d '[:space:]')"; then
    echo "Unable to fetch Release Passport license public key from ${license_public_key_url}" >&2
    echo "Set RELEASEPASSPORT_LICENSE_PUBLIC_KEY to the published Ed25519 public key and rerun the installer." >&2
    exit 1
  fi
  if [[ -z "$license_public_key" ]]; then
    echo "Release Passport license public key endpoint returned an empty response." >&2
    exit 1
  fi
}

exchange_install_token() {
  if [[ -z "$install_token" || "$dry_run" == "true" ]]; then
    return 0
  fi
  if [[ -n "$registry_username" && -n "$registry_password" && -n "$license_inline" ]]; then
    return 0
  fi
  local hash payload response
  hash="$(install_id_hash "$install_id")"
  payload="$(printf '{"installToken":%s,"installIdHash":%s,"target":%s,"version":%s}' \
    "$(json_escape "$install_token")" \
    "$(json_escape "$hash")" \
    "$(json_escape "$target")" \
    "$(json_escape "$version")")"
  echo "Exchanging Release Passport install token for Trial license and registry access."
  if ! response="$(printf '%s' "$payload" | post_json "$install_exchange_url")"; then
    echo "Unable to exchange Release Passport install token at ${install_exchange_url}." >&2
    echo "Open https://releasepassport.com/portal, generate a fresh install token, and rerun the installer." >&2
    exit 1
  fi
  registry_server="$(printf '%s' "$response" | json_field data.registry.server)"
  registry_username="$(printf '%s' "$response" | json_field data.registry.username)"
  registry_password="$(printf '%s' "$response" | json_field data.registry.password)"
  license_inline="$(printf '%s' "$response" | json_field data.license)"
  if [[ -z "$registry_server" || -z "$registry_username" || -z "$registry_password" || -z "$license_inline" ]]; then
    echo "Install token exchange response did not include license and registry pull credentials." >&2
    echo "Response was intentionally not printed because it can contain credentials." >&2
    exit 1
  fi
  registry_secret_name="${registry_secret_name:-releasepassport-registry}"
}

require_registry_access() {
  if [[ -n "$registry_username" && -n "$registry_password" ]]; then
    return 0
  fi
  if [[ "$target" == "kubernetes" && -n "$registry_secret_name" ]]; then
    return 0
  fi
  if [[ "$dry_run" == "true" ]]; then
    return 0
  fi
  cat >&2 <<EOF
Release Passport runtime images are in the private official registry.

Fix:
  1. Open https://releasepassport.com/portal and generate a Trial install token.
  2. Rerun with --install-token <token> or RELEASEPASSPORT_INSTALL_TOKEN=<token>.

Operator fallback:
  set RELEASEPASSPORT_REGISTRY_SERVER, RELEASEPASSPORT_REGISTRY_USERNAME, and
  RELEASEPASSPORT_REGISTRY_PASSWORD from an entitled package channel.
EOF
  exit 1
}

docker_registry_login() {
  if [[ -z "$registry_username" || -z "$registry_password" || "$dry_run" == "true" ]]; then
    return 0
  fi
  echo "Logging in to ${registry_server} for Release Passport runtime image pulls."
  printf '%s' "$registry_password" | docker login "$registry_server" --username "$registry_username" --password-stdin >/dev/null
}

if [[ "$version" == "latest" ]]; then
  version="$(fetch "${download_base%/}/latest/version.txt" | tr -d '[:space:]')"
  if [[ -z "$version" ]]; then
    echo "latest version metadata is empty" >&2
    exit 1
  fi
fi

case "$(uname -s | tr '[:upper:]' '[:lower:]')" in
  linux) os="linux" ;;
  darwin) os="darwin" ;;
  mingw*|msys*|cygwin*)
    cat >&2 <<'EOF'
Native Windows bash is not a supported runtime installer target yet.
Use WSL2/Linux, a Linux VM, or Kubernetes from a Linux/macOS workstation.
EOF
    exit 1
    ;;
  *) echo "unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac

case "$(uname -m)" in
  x86_64|amd64) arch="amd64" ;;
  arm64|aarch64) arch="arm64" ;;
  *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac

resolve_smart_inputs
resolve_runtime_target
print_install_plan
confirm_install_plan

registry="$(normalize_registry "$registry")"
if [[ "$registry" != "$official_registry" ]]; then
  cat >&2 <<EOF
Release Passport customer runtime images are distributed only from:
  ${official_registry}

The installer does not support GHCR, customer mirrors, or fallback registries
for customer installs. Remove --registry/RELEASEPASSPORT_REGISTRY overrides and
rerun the installer with the official registry.
EOF
  exit 1
fi

tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT

if [[ "$install_cli" == "true" ]]; then
  if [[ ! -d "$install_dir" || ! -w "$install_dir" ]]; then
    install_dir="${HOME}/.local/bin"
    mkdir -p "$install_dir"
  fi

  archive="releasepassport_${version}_${os}_${arch}.tar.gz"
  url="${download_base%/}/${version}/cli/${archive}"
  checksum_url="${url}.sha256"

  echo "Downloading Release Passport CLI package: ${url}"
  fetch "$url" -o "${tmp_dir}/${archive}"

  if fetch "$checksum_url" -o "${tmp_dir}/${archive}.sha256"; then
    expected="$(awk '{print $1}' "${tmp_dir}/${archive}.sha256")"
    actual="$(sha256_file "${tmp_dir}/${archive}")"
    if [[ "$expected" != "$actual" ]]; then
      echo "checksum mismatch for ${archive}" >&2
      exit 1
    fi
  elif [[ "${RELEASEPASSPORT_SKIP_CHECKSUM:-}" != "1" ]]; then
    echo "checksum file is required: ${checksum_url}" >&2
    echo "set RELEASEPASSPORT_SKIP_CHECKSUM=1 only for an internal artifact test" >&2
    exit 1
  fi

  tar -xzf "${tmp_dir}/${archive}" -C "$tmp_dir"
  if [[ "$dry_run" == "true" ]]; then
    echo "DRY RUN: install -m 0755 ${tmp_dir}/releasepassport ${install_dir}/${binary_name}"
  else
    install -m 0755 "${tmp_dir}/releasepassport" "${install_dir}/${binary_name}"
    echo "Installed ${binary_name} to ${install_dir}/${binary_name}"
  fi
fi

chart_url="${download_base%/}/${version}/helm/releasepassport-0.1.0.tgz"
values_url="${download_base%/}/${version}/helm/values-trial.yaml"
compose_url="${download_base%/}/${version}/compose/compose.yaml"
image_tag="${RELEASEPASSPORT_IMAGE_TAG:-${version%%-*}}"

if [[ "$target" == "host" ]]; then
  if [[ "$requested_target" != "host" ]]; then
    print_runtime_dependency_help
    exit 1
  fi
  echo "Host service packaging is not the default production path yet."
  if [[ "$install_cli" == "true" ]]; then
    echo "Installed CLI only. Use --target kubernetes or --target compose for self-hosted runtime bootstrap."
  else
    echo "No runtime installed. Use --target kubernetes or --target compose for self-hosted runtime bootstrap."
  fi
  exit 0
fi

discover_license_public_key

if [[ "$target" == "compose" ]]; then
  require_compose_runtime
  if [[ -z "$admin_password" ]]; then
    admin_password="$(random_password)"
  fi
  install_id="${RELEASEPASSPORT_INSTALL_ID:-}"
  db_password="${RELEASEPASSPORT_POSTGRES_PASSWORD:-$(random_password)}"
  gate_token="${RELEASEPASSPORT_GATE_TOKEN:-$(random_password)}"
  internal_token="${RELEASEPASSPORT_INTERNAL_ADMIN_TOKEN:-$(random_password)}"
  object_key="${OBJECT_STORAGE_ACCESS_KEY:-releasepassport}"
  object_secret="${OBJECT_STORAGE_SECRET_KEY:-$(random_password)}"
  if [[ "$storage_mode" == "existing" && ( -z "${DATABASE_URL:-}" || -z "${VALKEY_URL:-}" ) ]]; then
    echo "--storage existing requires DATABASE_URL and VALKEY_URL for compose installs." >&2
    exit 2
  fi
  compose_object_storage_external="false"
  if [[ -n "${OBJECT_STORAGE_ENDPOINT:-}" ]]; then
    compose_object_storage_external="true"
  fi
  compose_database_url="${DATABASE_URL:-postgres://releasepassport:${db_password}@postgres:5432/releasepassport?sslmode=disable}"
  compose_valkey_url="${VALKEY_URL:-redis://redis:6379/0}"
  compose_object_storage_endpoint="${OBJECT_STORAGE_ENDPOINT:-http://seaweedfs:8333}"
  if [[ -n "$domain" ]]; then
    public_app_url="https://${domain}"
    public_api_url="https://${domain}/releasepassport/v1"
    public_api_origin="https://${domain}"
  else
    compose_web_port="${RELEASEPASSPORT_WEB_PORT:-18080}"
    compose_api_port="${RELEASEPASSPORT_API_PORT:-18081}"
    public_app_url="http://127.0.0.1:${compose_web_port}"
    public_api_url="http://127.0.0.1:${compose_api_port}/releasepassport/v1"
    public_api_origin="http://127.0.0.1:${compose_api_port}"
  fi
  compose_ready_web_url="http://127.0.0.1:${RELEASEPASSPORT_WEB_PORT:-18080}"
  compose_ready_api_url="http://127.0.0.1:${RELEASEPASSPORT_API_PORT:-18081}"
  compose_dir="${RELEASEPASSPORT_COMPOSE_DIR:-${PWD}/releasepassport-self-hosted}"
  compose_file="${compose_dir}/compose.yaml"
  compose_existing_storage_file="${compose_dir}/compose.existing-storage.yaml"
  env_file="${compose_dir}/.env"
  if [[ -z "$install_id" && -f "$env_file" ]]; then
    install_id="$(awk -F= '$1 == "RELEASEPASSPORT_INSTALL_ID" {print $2; exit}' "$env_file")"
  fi
  if [[ -z "$install_id" ]]; then
    install_id="$(random_install_id)"
  fi
  if [[ -n "$license_file" ]]; then
    if [[ ! -f "$license_file" ]]; then
      echo "license file not found: ${license_file}" >&2
      exit 1
    fi
    license_inline="$(tr -d '\n' <"$license_file")"
  fi
  exchange_install_token
  require_registry_access
  if [[ "$dry_run" == "true" ]]; then
    echo "DRY RUN: mkdir -p ${compose_dir}"
    echo "DRY RUN: curl ${compose_url} -o ${compose_file}"
    echo "DRY RUN: write ${env_file} with generated runtime secrets"
    if [[ "$storage_mode" == "existing" ]]; then
      echo "DRY RUN: write ${compose_existing_storage_file} to disable bundled Postgres/Valkey dependencies"
    fi
    if [[ -n "$registry_username" ]]; then
      echo "DRY RUN: docker login ${registry_server} --username <redacted> --password-stdin"
    fi
    if [[ "$storage_mode" == "existing" ]]; then
      echo "DRY RUN: docker compose --env-file ${env_file} -f ${compose_file} -f ${compose_existing_storage_file} up -d api worker web"
    else
      echo "DRY RUN: docker compose --env-file ${env_file} -f ${compose_file} up -d"
    fi
    echo "DRY RUN: wait for ${compose_ready_web_url}/readyz and ${compose_ready_api_url}/releasepassport/v1/readyz"
    if [[ -n "$domain" ]]; then
      echo "DRY RUN: check public exposure for https://${domain}/readyz and https://${domain}/releasepassport/v1/readyz"
    fi
    print_access_guidance "compose" "${public_app_url}" "${public_api_url}"
    exit 0
  fi
  mkdir -p "$compose_dir"
  fetch "$compose_url" -o "$compose_file"
  cat >"$env_file" <<EOF
RELEASEPASSPORT_VERSION=${image_tag}
RELEASEPASSPORT_AUTH_MODE=${auth_mode}
RELEASEPASSPORT_REQUIRE_INSTALL_ID_LICENSE_BINDING=true
RELEASEPASSPORT_BOOTSTRAP_ADMIN_EMAIL=${admin_email}
RELEASEPASSPORT_BOOTSTRAP_ADMIN_PASSWORD_SECRET=${admin_password}
RELEASEPASSPORT_GATE_TOKEN=${gate_token}
RELEASEPASSPORT_INTERNAL_ADMIN_TOKEN=${internal_token}
RELEASEPASSPORT_POSTGRES_PASSWORD=${db_password}
DATABASE_URL=${compose_database_url}
VALKEY_URL=${compose_valkey_url}
OBJECT_STORAGE_ENDPOINT=${compose_object_storage_endpoint}
OBJECT_STORAGE_ACCESS_KEY=${object_key}
OBJECT_STORAGE_SECRET_KEY=${object_secret}
OBJECT_STORAGE_BUCKET=releasepassport
PUBLIC_APP_URL=${public_app_url}
PUBLIC_API_URL=${public_api_url}
RELEASEPASSPORT_RUNTIME_PUBLIC_APP_URL=${public_app_url}
RELEASEPASSPORT_RUNTIME_PUBLIC_API_URL=${public_api_url}
RELEASEPASSPORT_PUBLIC_API_ORIGIN=${public_api_origin}
RELEASEPASSPORT_LICENSE=${license_inline}
RELEASEPASSPORT_LICENSE_PUBLIC_KEY=${license_public_key}
RELEASEPASSPORT_INSTALL_ID=${install_id}
RELEASEPASSPORT_TRIAL_STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
RELEASEPASSPORT_RUNTIME_CHECKS_ENABLED=false
RELEASEPASSPORT_FIRST_CONNECTOR_HINT=${first_connector}
RELEASEPASSPORT_DETECTED_CONNECTOR_HINTS=${detected_connectors}
RELEASEPASSPORT_CONNECTOR_BOOTSTRAP_JSON=${connector_bootstrap_json}
AI_PROVIDER=${ai_provider}
OPENAI_API_KEY=${ai_api_key}
OPENAI_BASE_URL=${ai_base_url}
OPENAI_MODEL=${ai_model}
EOF
  chmod 0600 "$env_file"
  if [[ "$storage_mode" == "existing" ]]; then
    if [[ "$compose_object_storage_external" == "true" ]]; then
      cat >"$compose_existing_storage_file" <<'EOF'
services:
  api:
    depends_on: !reset []
EOF
    else
      cat >"$compose_existing_storage_file" <<'EOF'
services:
  api:
    depends_on: !override
      seaweedfs-init:
        condition: service_completed_successfully
EOF
    fi
  fi
  docker_registry_login
  if [[ "$storage_mode" == "existing" ]]; then
    docker compose --env-file "$env_file" -f "$compose_file" -f "$compose_existing_storage_file" up -d api worker web
  else
    docker compose --env-file "$env_file" -f "$compose_file" up -d
  fi
  wait_compose_ready
  check_public_exposure "compose" "${public_app_url}" "${public_api_url}"
  echo
  echo "Release Passport compose bootstrap complete and ready."
  print_access_guidance "compose" "${public_app_url}" "${public_api_url}"
  echo "Admin email: ${admin_email}"
  echo "Generated admin password: ${admin_password}"
  echo "Install ID: stored in ${env_file}; keep it secret for license binding and restores."
  echo "Compose directory: ${compose_dir}"
  if [[ -n "$first_connector" ]]; then
    echo "First connector hint: ${first_connector}. Validate it from Settings > Integrations after login."
  fi
  if [[ -n "$detected_connectors" ]]; then
    echo "Detected connector hints: ${detected_connectors}."
  fi
  exit 0
fi

if [[ "$target" != "kubernetes" ]]; then
  echo "unsupported target: ${target}" >&2
  exit 2
fi

require_kubernetes_runtime

if [[ -z "$admin_password" ]]; then
  admin_password="$(random_password)"
fi
db_password="${RELEASEPASSPORT_BUNDLED_POSTGRES_PASSWORD:-$(random_password)}"
database_url="${DATABASE_URL:-postgres://releasepassport:${db_password}@releasepassport-postgres:5432/releasepassport?sslmode=disable}"
valkey_url="${VALKEY_URL:-redis://releasepassport-valkey:6379/0}"
bundled_storage_enabled="true"
if [[ "$storage_mode" == "existing" ]]; then
  bundled_storage_enabled="false"
  if [[ -z "${DATABASE_URL:-}" || -z "${VALKEY_URL:-}" ]]; then
    echo "--storage existing requires DATABASE_URL and VALKEY_URL for Kubernetes installs." >&2
    exit 2
  fi
fi
install_id="${RELEASEPASSPORT_INSTALL_ID:-}"
if [[ -z "$install_id" ]]; then
  existing_install_id="$(kubectl -n "${namespace}" get secret releasepassport-runtime -o jsonpath='{.data.RELEASEPASSPORT_INSTALL_ID}' 2>/dev/null || true)"
  if [[ -n "$existing_install_id" ]]; then
    if decoded_install_id="$(printf '%s' "$existing_install_id" | base64 --decode 2>/dev/null)"; then
      install_id="$decoded_install_id"
    elif decoded_install_id="$(printf '%s' "$existing_install_id" | base64 -D 2>/dev/null)"; then
      install_id="$decoded_install_id"
    fi
  fi
fi
if [[ -z "$install_id" ]]; then
  install_id="$(random_install_id)"
fi

if [[ -n "$domain" ]]; then
  public_app_url="https://${domain}"
  public_api_url="https://${domain}/releasepassport/v1"
  route_enabled="true"
  if ! kubectl get crd httproutes.gateway.networking.k8s.io >/dev/null 2>&1; then
    cat >&2 <<EOF
Gateway API CRD not found, so the chart cannot create the public domain route for ${domain}.

Fix:
  - Install/configure Gateway API support for this cluster, then rerun; or
  - Omit --domain for a no-domain trial and use the printed localhost or SSH tunnel URLs; or
  - Install with port-forward and configure your own ingress/reverse proxy manually.
EOF
    exit 1
  fi
else
  public_app_url="http://127.0.0.1:18080"
  public_api_url="http://127.0.0.1:18081/releasepassport/v1"
  route_enabled="false"
fi

if [[ -n "$license_file" ]]; then
  if [[ ! -f "$license_file" ]]; then
    echo "license file not found: ${license_file}" >&2
    exit 1
  fi
  license_inline="$(tr -d '\n' <"$license_file")"
fi
exchange_install_token
require_registry_access

fetch "$chart_url" -o "${tmp_dir}/releasepassport.tgz"
fetch "$values_url" -o "${tmp_dir}/values-trial.yaml"

override_values="${tmp_dir}/releasepassport-values.override.yaml"
cat >"$override_values" <<EOF
global:
  registry: ${registry}
bundled:
  postgres:
    enabled: ${bundled_storage_enabled}
  valkey:
    enabled: ${bundled_storage_enabled}
gateway:
  enabled: ${route_enabled}
config:
  PUBLIC_APP_URL: ${public_app_url}
  PUBLIC_API_URL: ${public_api_url}
  CORS_ALLOWED_ORIGINS: ${public_app_url}
  RELEASEPASSPORT_PUBLIC_WEB_ORIGIN: ${public_app_url}
  RELEASEPASSPORT_PUBLIC_API_ORIGIN: ${public_api_url%/releasepassport/v1}
  CUSTOMER_RUNTIME_BOUNDARY: customer
  RELEASEPASSPORT_RUNTIME_BOUNDARY: customer
  RELEASEPASSPORT_PACKAGE_PROFILE: customer
  RELEASEPASSPORT_AUTH_MODE: ${auth_mode}
  RELEASEPASSPORT_REQUIRE_INSTALL_ID_LICENSE_BINDING: "true"
  NEXT_PUBLIC_RELEASEPASSPORT_AUTH_MODE: ${auth_mode}
  NEXT_PUBLIC_RELEASEPASSPORT_RUNTIME_BOUNDARY: customer
  NEXT_PUBLIC_RELEASEPASSPORT_PACKAGE_PROFILE: customer
  RELEASEPASSPORT_LICENSE_PUBLIC_KEY: "${license_public_key}"
  RELEASEPASSPORT_BOOTSTRAP_ADMIN_EMAIL: ${admin_email}
  RELEASEPASSPORT_CONSOLE_ACCESS: ${auth_mode}
  RELEASEPASSPORT_ENABLE_DEFAULT_CONNECTORS: "false"
  RELEASEPASSPORT_DETECTION_MODE: suggest
  RELEASEPASSPORT_AUTO_ENABLE_DETECTED_CONNECTORS: "${auto_enable_detected}"
  RELEASEPASSPORT_FIRST_CONNECTOR_HINT: "${first_connector}"
  RELEASEPASSPORT_DETECTED_CONNECTOR_HINTS: "${detected_connectors}"
  RELEASEPASSPORT_CONNECTOR_BOOTSTRAP_JSON: '${connector_bootstrap_json}'
  AI_PROVIDER: "${ai_provider}"
  OPENAI_BASE_URL: "${ai_base_url}"
  OPENAI_MODEL: "${ai_model}"
  RELEASEPASSPORT_RUNTIME_CHECKS_ENABLED: "false"
  RELEASEPASSPORT_PLAYWRIGHT_RUNNER_ENABLED: "false"
  RELEASEPASSPORT_PLAYWRIGHT_ALLOWED_BINARIES: "playwright,npx,pnpm,npm,yarn"
  RELEASEPASSPORT_PAYMENT_ENABLED: "false"
  RELEASEPASSPORT_TRIAL_STARTED_AT: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
components:
  web:
    replicas: 1
    imageRepository: ${registry}/customer-web
    route:
      enabled: ${route_enabled}
      host: ${domain}
  api:
    replicas: 1
    imageRepository: ${registry}/customer-api
    route:
      enabled: ${route_enabled}
      host: ${domain}
  worker:
    replicas: 1
    imageRepository: ${registry}/customer-worker
EOF

if [[ -n "$registry_password" || -n "$registry_username" ]]; then
  registry_secret_name="${registry_secret_name:-releasepassport-registry}"
  if [[ -z "$registry_server" || -z "$registry_username" || -z "$registry_password" ]]; then
    echo "RELEASEPASSPORT_REGISTRY_SERVER, RELEASEPASSPORT_REGISTRY_USERNAME, and RELEASEPASSPORT_REGISTRY_PASSWORD are required together." >&2
    exit 1
  fi
fi

if [[ -n "$registry_secret_name" ]]; then
  {
    echo "imagePullSecrets:"
    echo "  - name: ${registry_secret_name}"
  } >>"$override_values"
fi

echo "Preparing Kubernetes namespace and runtime secret in ${namespace}"
secret_args=(
  --from-literal=RELEASEPASSPORT_BOOTSTRAP_ADMIN_PASSWORD_SECRET="${admin_password}"
  --from-literal=RELEASEPASSPORT_BUNDLED_POSTGRES_PASSWORD="${db_password}"
  --from-literal=RELEASEPASSPORT_GATE_TOKEN="${RELEASEPASSPORT_GATE_TOKEN:-$(random_password)}"
  --from-literal=RELEASEPASSPORT_INTERNAL_ADMIN_TOKEN="${RELEASEPASSPORT_INTERNAL_ADMIN_TOKEN:-$(random_password)}"
  --from-literal=RELEASEPASSPORT_INSTALL_ID="${install_id}"
  --from-literal=RELEASEPASSPORT_LICENSE_PUBLIC_KEY="${license_public_key}"
  --from-literal=OPENAI_API_KEY="${ai_api_key}"
  --from-literal=DATABASE_URL="${database_url}"
  --from-literal=VALKEY_URL="${valkey_url}"
  --from-literal=OBJECT_STORAGE_ACCESS_KEY="${OBJECT_STORAGE_ACCESS_KEY:-}"
  --from-literal=OBJECT_STORAGE_SECRET_KEY="${OBJECT_STORAGE_SECRET_KEY:-}"
)
if [[ -n "$license_file" ]]; then
  secret_args+=(--from-file=RELEASEPASSPORT_LICENSE="${license_file}")
elif [[ -n "$license_inline" ]]; then
  secret_args+=(--from-literal=RELEASEPASSPORT_LICENSE="${license_inline}")
fi

if [[ "$dry_run" == "true" ]]; then
  echo "DRY RUN: kubectl create namespace ${namespace} --dry-run=client -o yaml | kubectl apply -f -"
  echo "DRY RUN: kubectl -n ${namespace} create secret generic releasepassport-runtime ... | kubectl apply -f -"
  if [[ -n "$registry_secret_name" && -n "$registry_password" ]]; then
    echo "DRY RUN: kubectl -n ${namespace} create secret docker-registry ${registry_secret_name} --docker-server=${registry_server} ..."
  fi
  echo "DRY RUN: helm upgrade --install releasepassport ${tmp_dir}/releasepassport.tgz --namespace ${namespace} --create-namespace -f ${tmp_dir}/values-trial.yaml -f ${override_values}"
  echo "DRY RUN: kubectl -n ${namespace} rollout status deploy/releasepassport-api deploy/releasepassport-worker deploy/releasepassport-web"
  if [[ -n "$domain" ]]; then
    echo "DRY RUN: check public exposure for https://${domain}/readyz and https://${domain}/releasepassport/v1/readyz"
  fi
  print_access_guidance "kubernetes" "${public_app_url}" "${public_api_url}"
  exit 0
fi

kubectl create namespace "${namespace}" --dry-run=client -o yaml | kubectl apply -f -
kubectl -n "${namespace}" create secret generic releasepassport-runtime "${secret_args[@]}" --dry-run=client -o yaml | kubectl apply -f -
if [[ -n "$registry_secret_name" && -n "$registry_password" ]]; then
  kubectl -n "${namespace}" create secret docker-registry "${registry_secret_name}" \
    --docker-server="${registry_server}" \
    --docker-username="${registry_username}" \
    --docker-password="${registry_password}" \
    --dry-run=client -o yaml | kubectl apply -f -
fi
helm upgrade --install releasepassport "${tmp_dir}/releasepassport.tgz" \
  --namespace "${namespace}" \
  --create-namespace \
  -f "${tmp_dir}/values-trial.yaml" \
  -f "${override_values}"
wait_kubernetes_ready
check_public_exposure "kubernetes" "${public_app_url}" "${public_api_url}"

if [[ "$auto_detect" == "true" ]]; then
  echo "Connector detector will run in suggest mode. Enable candidates from Settings or CLI after login."
fi

echo
echo "Release Passport self-hosted bootstrap complete and ready."
echo "Installer revision: ${installer_revision}"
print_access_guidance "kubernetes" "${public_app_url}" "${public_api_url}"
echo "Admin email: ${admin_email}"
echo "Generated admin password: ${admin_password}"
echo "Install ID: stored in Kubernetes secret ${namespace}/releasepassport-runtime; keep it secret for license binding and restores."
if [[ -n "$first_connector" ]]; then
  echo "First connector hint: ${first_connector}. Validate it from Settings > Integrations after login."
fi
if [[ -n "$detected_connectors" ]]; then
  echo "Detected connector hints: ${detected_connectors}."
fi
echo "First gate command:"
echo "  ${binary_name} gate --api-url ${public_api_url} --token <gate-token> --service <service> --release-id <release-id> --mode shadow"
if [[ -z "$domain" ]]; then
  echo
  echo "No domain mode:"
  echo "  kubectl -n ${namespace} port-forward svc/releasepassport-web 18080:80"
  echo "  kubectl -n ${namespace} port-forward svc/releasepassport-api 18081:80"
fi
