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

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
DOCKER_COMPOSE_CMD=()

load_repo_root_env() {
  local env_file="$REPO_ROOT/.env"
  if [[ -f "$env_file" ]]; then
    local existing_cn_quickstart_dir="${CN_QUICKSTART_DIR:-}"
    set -a
    # shellcheck disable=SC1090
    source "$env_file"
    set +a
    if [[ -n "$existing_cn_quickstart_dir" ]]; then
      CN_QUICKSTART_DIR="$existing_cn_quickstart_dir"
    fi
  fi
}

normalize_quickstart_dir_candidate() {
  local candidate="$1"

  if [[ -z "$candidate" ]]; then
    return 1
  fi

  if [[ -d "$candidate/quickstart" ]]; then
    candidate="$candidate/quickstart"
  fi

  if [[ -f "$candidate/compose.yaml" && -d "$candidate/docker/modules" ]]; then
    cd -- "$candidate" && pwd
    return 0
  fi

  return 1
}

resolve_quickstart_dir() {
  local candidate
  for candidate in \
    "${CN_QUICKSTART_DIR:-}" \
    "$SCRIPT_DIR/../cn-quickstart/quickstart" \
    "$SCRIPT_DIR/../../cn-quickstart/quickstart"
  do
    if normalize_quickstart_dir_candidate "$candidate"; then
      return 0
    fi
  done

  echo "Unable to locate CN Quickstart. Set CN_QUICKSTART_DIR in $REPO_ROOT/.env or place the checkout in a supported location." >&2
  return 1
}

make_target_exists() {
  local target="$1"
  make -qp 2>/dev/null | grep -Eq "^${target}:"
}

read_env_value_from_files() {
  local key="$1"
  shift

  local file
  for file in "$@"; do
    if [[ -f "$file" ]]; then
      local line
      line="$(grep -E "^${key}=" "$file" | tail -n 1 || true)"
      if [[ -n "$line" ]]; then
        printf '%s\n' "${line#*=}"
        return 0
      fi
    fi
  done
  return 1
}

read_env_value() {
  read_env_value_from_files "$1" .env.local .env
}

generated_dir() {
  printf '%s\n' "${START_LOCAL_GENERATED_DIR:-$SCRIPT_DIR/.generated/start-local}"
}

resolve_es256_enabled() {
  local value="${LOCALNET_ES256_JWT:-0}"
  if [[ "$value" != "0" && "$value" != "1" ]]; then
    echo "LOCALNET_ES256_JWT must be 0 or 1." >&2
    return 1
  fi
  printf '%s\n' "$value"
}

es256_runtime_dir() {
  printf '%s\n' "${START_LOCAL_ES256_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-es256}"
}

resolve_no_auth_enabled() {
  local value="${LOCALNET_NO_AUTH:-0}"
  if [[ "$value" != "0" && "$value" != "1" ]]; then
    echo "LOCALNET_NO_AUTH must be 0 or 1." >&2
    return 1
  fi
  printf '%s\n' "$value"
}

no_auth_runtime_dir() {
  printf '%s\n' "${START_LOCAL_NO_AUTH_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-no-auth}"
}

resolve_tls_enabled() {
  local value="${LOCALNET_TLS:-0}"
  if [[ "$value" != "0" && "$value" != "1" ]]; then
    echo "LOCALNET_TLS must be 0 or 1." >&2
    return 1
  fi
  printf '%s\n' "$value"
}

localnet_http_scheme() {
  if [[ "$(resolve_tls_enabled)" == "1" ]]; then
    printf 'https\n'
  else
    printf 'http\n'
  fi
}

resolve_tls_rotate() {
  local value="${LOCALNET_TLS_ROTATE:-0}"
  if [[ "$value" != "0" && "$value" != "1" ]]; then
    echo "LOCALNET_TLS_ROTATE must be 0 or 1." >&2
    return 1
  fi
  printf '%s\n' "$value"
}

tls_runtime_dir() {
  printf '%s\n' "${START_LOCAL_TLS_RUNTIME_DIR:-$REPO_ROOT/.generated/localnet-tls}"
}

prepare_tls_runtime_files() {
  local enabled
  enabled="$(resolve_tls_enabled)"
  if [[ "$enabled" != "1" ]]; then
    return 0
  fi

  local runtime_dir
  runtime_dir="$(tls_runtime_dir)"
  local ca_key_path="$runtime_dir/ca.key"
  local ca_certificate_path="$runtime_dir/ca.crt"
  local server_key_path="$runtime_dir/server.key"
  local server_certificate_path="$runtime_dir/server.crt"
  local compose_file="$runtime_dir/compose-localnet.yaml"
  local tls_fragment_file="$runtime_dir/canton-tls.conf"
  local canton_config_file="$runtime_dir/canton-localnet.conf"
  local onboarding_utils_source="$MODULES_DIR/splice-onboarding/docker/utils.sh"
  local onboarding_utils_file="$runtime_dir/splice-onboarding-utils.sh"
  local supplied_count=0

  [[ -n "${LOCALNET_TLS_CERT_CHAIN_PATH:-}" ]] && ((supplied_count += 1))
  [[ -n "${LOCALNET_TLS_PRIVATE_KEY_PATH:-}" ]] && ((supplied_count += 1))
  [[ -n "${LOCALNET_TLS_CA_CERT_PATH:-}" ]] && ((supplied_count += 1))

  mkdir -p "$runtime_dir"
  chmod 700 "$runtime_dir"
  if (( supplied_count > 0 )); then
    if (( supplied_count != 3 )); then
      echo "LOCALNET_TLS_CERT_CHAIN_PATH, LOCALNET_TLS_PRIVATE_KEY_PATH, and LOCALNET_TLS_CA_CERT_PATH must be set together." >&2
      return 1
    fi
    local supplied_server_certificate_path="$LOCALNET_TLS_CERT_CHAIN_PATH"
    local supplied_server_key_path="$LOCALNET_TLS_PRIVATE_KEY_PATH"
    local supplied_ca_certificate_path="$LOCALNET_TLS_CA_CERT_PATH"
    server_certificate_path="$supplied_server_certificate_path"
    server_key_path="$supplied_server_key_path"
    ca_certificate_path="$supplied_ca_certificate_path"
    if [[ ! -r "$server_certificate_path" || ! -r "$server_key_path" || ! -r "$ca_certificate_path" ]]; then
      echo "Supplied TLS certificate, private key, or CA certificate is not readable." >&2
      return 1
    fi
    if ! openssl x509 -in "$server_certificate_path" -noout >/dev/null 2>&1 \
      || ! openssl pkey -in "$server_key_path" -noout >/dev/null 2>&1 \
      || ! openssl x509 -in "$ca_certificate_path" -noout >/dev/null 2>&1 \
      || ! openssl verify -CAfile "$ca_certificate_path" "$server_certificate_path" >/dev/null 2>&1; then
      echo "Supplied TLS material is not valid PEM certificate/key material." >&2
      return 1
    fi
    local certificate_public_key private_public_key
    if ! certificate_public_key="$(openssl x509 -in "$server_certificate_path" -pubkey -noout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')" \
      || ! private_public_key="$(openssl pkey -in "$server_key_path" -pubout | openssl pkey -pubin -outform DER | sha256sum | awk '{print $1}')" \
      || [[ "$certificate_public_key" != "$private_public_key" ]]; then
      echo "Supplied TLS private key does not match the server certificate." >&2
      return 1
    fi
    if [[ "$supplied_server_certificate_path" != "$runtime_dir/server.crt" ]]; then
      cp "$supplied_server_certificate_path" "$runtime_dir/server.crt"
    fi
    if [[ "$supplied_server_key_path" != "$runtime_dir/server.key" ]]; then
      cp "$supplied_server_key_path" "$runtime_dir/server.key"
    fi
    if [[ "$supplied_ca_certificate_path" != "$runtime_dir/ca.crt" ]]; then
      cp "$supplied_ca_certificate_path" "$runtime_dir/ca.crt"
    fi
    chmod 644 "$runtime_dir/server.crt" "$runtime_dir/server.key" "$runtime_dir/ca.crt"
    server_certificate_path="$runtime_dir/server.crt"
    server_key_path="$runtime_dir/server.key"
    ca_certificate_path="$runtime_dir/ca.crt"
  else
    if [[ "$(resolve_tls_rotate)" == "1" ]]; then
      rm -f "$ca_key_path" "$ca_certificate_path" "$server_key_path" "$server_certificate_path"
    fi
    if [[ ! -e "$ca_key_path" || ! -e "$ca_certificate_path" ]]; then
      rm -f "$ca_key_path" "$ca_certificate_path"
      openssl genrsa -out "$ca_key_path" 2048 >/dev/null 2>&1
      chmod 600 "$ca_key_path"
      openssl req -x509 -new -sha256 -days 3650 -key "$ca_key_path" \
        -subj "/CN=localnet-tls-ca" -out "$ca_certificate_path" >/dev/null 2>&1
    fi
    if [[ -e "$server_certificate_path" ]] \
      && ! openssl x509 -in "$server_certificate_path" -noout -ext subjectAltName 2>/dev/null \
        | grep -Fq 'IP Address:127.0.0.1, IP Address:0.0.0.0'; then
      rm -f "$server_key_path" "$server_certificate_path"
    fi
    if [[ ! -e "$server_key_path" || ! -e "$server_certificate_path" ]]; then
      rm -f "$server_key_path" "$server_certificate_path"
      local csr_path="$runtime_dir/server.csr"
      local extensions_path="$runtime_dir/server-extensions.cnf"
      openssl genrsa -out "$server_key_path" 2048 >/dev/null 2>&1
      chmod 600 "$server_key_path"
      openssl req -new -sha256 -key "$server_key_path" -subj "/CN=localhost" -out "$csr_path" >/dev/null 2>&1
      cat > "$extensions_path" <<'EOF'
[server_ext]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:localhost, DNS:canton, IP:127.0.0.1, IP:0.0.0.0
EOF
      openssl x509 -req -sha256 -days 3650 -in "$csr_path" \
        -CA "$ca_certificate_path" -CAkey "$ca_key_path" -CAcreateserial \
        -out "$server_certificate_path" -extfile "$extensions_path" -extensions server_ext >/dev/null 2>&1
      rm -f "$csr_path" "$extensions_path" "$ca_certificate_path.srl"
    fi
    chmod 644 "$server_key_path" "$server_certificate_path" "$ca_certificate_path"
    if ! openssl x509 -in "$server_certificate_path" -noout >/dev/null 2>&1 \
      || ! openssl pkey -in "$server_key_path" -noout >/dev/null 2>&1 \
      || ! openssl x509 -in "$ca_certificate_path" -noout >/dev/null 2>&1; then
      echo "Generated TLS material is invalid." >&2
      return 1
    fi
  fi

  if [[ ! -r "$onboarding_utils_source" ]]; then
    echo "Unable to read Splice onboarding utilities at $onboarding_utils_source." >&2
    return 1
  fi
  sed \
    -e 's#http://\$participant#${LOCALNET_HTTP_SCHEME:-http}://$participant#g' \
    -e 's#http://\$validator#${LOCALNET_HTTP_SCHEME:-http}://$validator#g' \
    "$onboarding_utils_source" > "$onboarding_utils_file"

  : > "$tls_fragment_file"
  local participant profile
  for participant in app-provider app-user sv; do
    case "$participant" in
      app-provider) profile="$APP_PROVIDER_PROFILE" ;;
      app-user) profile="$APP_USER_PROFILE" ;;
      sv) profile="$SV_PROFILE" ;;
    esac
    if [[ "$profile" != "off" ]]; then
      cat >> "$tls_fragment_file" <<EOF
canton.participants.${participant}.ledger-api.tls {
  cert-chain-file = "/app/localnet-tls/server.crt"
  private-key-file = "/app/localnet-tls/server.key"
  trust-collection-file = "/app/localnet-tls/ca.crt"
}
canton.participants.${participant}.admin-api.tls {
  cert-chain-file = "/app/localnet-tls/server.crt"
  private-key-file = "/app/localnet-tls/server.key"
  trust-collection-file = "/app/localnet-tls/ca.crt"
}
EOF
    fi
  done
  cat > "$canton_config_file" <<'EOF'
include file("/app/base-app.conf")
include file("/app/localnet-tls.conf")
EOF
  cat > "$compose_file" <<EOF
services:
  canton:
    volumes:
      - "${LOCALNET_DIR}/conf/canton/app.conf:/app/base-app.conf:ro"
      - "$canton_config_file:/app/app.conf:ro"
      - "$tls_fragment_file:/app/localnet-tls.conf:ro"
      - "$server_certificate_path:/app/localnet-tls/server.crt:ro"
      - "$server_key_path:/app/localnet-tls/server.key:ro"
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
  splice:
    volumes:
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
    environment:
      ADDITIONAL_CONFIG_LOCALNET_TLS: |
        # Trust the localnet CA for Splice's Canton clients.
EOF
  for participant in app-provider app-user sv; do
    case "$participant" in
      app-provider) profile="$APP_PROVIDER_PROFILE" ;;
      app-user) profile="$APP_USER_PROFILE" ;;
      sv) profile="$SV_PROFILE" ;;
    esac
    if [[ "$profile" == "off" ]]; then
      continue
    fi
    if [[ "$participant" == "sv" ]]; then
      cat >> "$compose_file" <<'EOF'
        canton.validator-apps.sv-validator_backend.participant-client {
          admin-api.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
          ledger-api.client-config.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
        }
        canton.scan-apps.scan-app.participant-client {
          admin-api.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
          ledger-api.client-config.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
        }
        canton.sv-apps.sv.participant-client {
          admin-api.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
          ledger-api.client-config.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
        }
EOF
    else
      cat >> "$compose_file" <<EOF
        canton.validator-apps.${participant}-validator_backend.participant-client {
          admin-api.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
          ledger-api.client-config.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
        }
EOF
    fi
  done
  cat >> "$compose_file" <<EOF
  splice-onboarding:
    volumes:
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
      - "$onboarding_utils_file:/app/utils.sh:ro"
    environment:
      - LOCALNET_HTTP_SCHEME=https
      - CURL_CA_BUNDLE=/app/localnet-tls/ca.crt
  pqs-app-provider:
    environment:
      SCRIBE_SOURCE_LEDGER_TLS_CAFILE: /app/localnet-tls/ca.crt
    volumes:
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
  pqs-app-user:
    environment:
      SCRIBE_SOURCE_LEDGER_TLS_CAFILE: /app/localnet-tls/ca.crt
    volumes:
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
  pqs-sv:
    environment:
      SCRIBE_SOURCE_LEDGER_TLS_CAFILE: /app/localnet-tls/ca.crt
    volumes:
      - "$ca_certificate_path:/app/localnet-tls/ca.crt:ro"
EOF
  export LOCALNET_TLS_COMPOSE_FILE="$compose_file"
  export LOCALNET_TLS_FRAGMENT_FILE="$tls_fragment_file"
  export LOCALNET_TLS_CA_CERT_PATH="$ca_certificate_path"
}

prepare_es256_runtime_files() {
  local runtime_dir
  runtime_dir="$(es256_runtime_dir)"
  local enabled
  enabled="$(resolve_es256_enabled)"
  if [[ "$enabled" != "1" ]]; then
    return 0
  fi

  local private_key_path="$runtime_dir/es256-private-key.pem"
  local certificate_path="$runtime_dir/es256-certificate.pem"
  local token_path="$runtime_dir/ledger-api-user.token"
  local compose_file="$runtime_dir/compose-es256.yaml"
  local canton_config_file="$runtime_dir/canton-es256.conf"
  local subject="${LOCALNET_ES256_SUBJECT:-ledger-api-user}"
  local ttl_seconds="${LOCALNET_ES256_TOKEN_TTL_SECONDS:-600}"

  if [[ -n "${LOCALNET_ES256_PRIVATE_KEY_PATH:-}" || -n "${LOCALNET_ES256_CERTIFICATE_PATH:-}" ]]; then
    if [[ -z "${LOCALNET_ES256_PRIVATE_KEY_PATH:-}" || -z "${LOCALNET_ES256_CERTIFICATE_PATH:-}" ]]; then
      echo "LOCALNET_ES256_PRIVATE_KEY_PATH and LOCALNET_ES256_CERTIFICATE_PATH must be set together." >&2
      return 1
    fi
    private_key_path="$LOCALNET_ES256_PRIVATE_KEY_PATH"
    certificate_path="$LOCALNET_ES256_CERTIFICATE_PATH"
    if [[ ! -r "$private_key_path" || ! -r "$certificate_path" ]]; then
      echo "Supplied ES256 private key or certificate is not readable." >&2
      return 1
    fi
  else
    if [[ "${LOCALNET_ES256_ROTATE:-0}" == "1" ]]; then
      rm -f "$private_key_path" "$certificate_path"
    fi
    if [[ ! -f "$private_key_path" ]]; then
      mkdir -p "$runtime_dir"
      node "$SCRIPT_DIR/es256-jwt.mjs" init --runtime-dir "$runtime_dir" >/dev/null
    fi
    if [[ ! -f "$certificate_path" ]]; then
      openssl req -x509 -new -sha256 -days 3650 -key "$private_key_path" -subj "/CN=localnet-es256" -out "$certificate_path" >/dev/null 2>&1
    fi
  fi

  mkdir -p "$runtime_dir"
  node "$SCRIPT_DIR/es256-jwt.mjs" mint \
    --private-key-path "$private_key_path" \
    --subject "$subject" \
    --audience "https://canton.network.global/es256" \
    --ttl-seconds "$ttl_seconds" > "$token_path"
  chmod 600 "$token_path"

  cat > "$canton_config_file" <<EOF
include file("/app/base-app.conf")

$(if [[ "$(resolve_tls_enabled)" == "1" ]]; then printf 'include file("/app/localnet-tls.conf")\n'; fi)

EOF
  local participant profile
  for participant in app-provider app-user sv; do
    case "$participant" in
      app-provider) profile="$APP_PROVIDER_PROFILE" ;;
      app-user) profile="$APP_USER_PROFILE" ;;
      sv) profile="$SV_PROFILE" ;;
    esac
    if [[ "$profile" != "off" ]]; then
      cat >> "$canton_config_file" <<EOF
canton.participants.${participant}.ledger-api.auth-services = [
  {
    type = unsafe-jwt-hmac-256
    target-audience = "https://canton.network.global"
    secret = "unsafe"
  },
  {
    type = jwt-es-256-crt
    certificate = "/app/es256-certificate.pem"
    target-audience = "https://canton.network.global/es256"
  }
]
EOF
    fi
  done

  cat > "$compose_file" <<EOF
services:
  canton:
    volumes:
      - "${LOCALNET_DIR}/conf/canton/app.conf:/app/base-app.conf:ro"
      - "$canton_config_file:/app/app.conf:ro"
      - "$certificate_path:/app/es256-certificate.pem:ro"
EOF
  export LOCALNET_ES256_COMPOSE_FILE="$compose_file"
  export LOCALNET_ES256_TOKEN_PATH="$token_path"
}

prepare_no_auth_runtime_files() {
  if [[ "$(resolve_no_auth_enabled)" != "1" ]]; then
    return 0
  fi

  local runtime_dir
  runtime_dir="$(no_auth_runtime_dir)"
  local compose_file="$runtime_dir/compose-no-auth.yaml"
  local canton_config_file="$runtime_dir/canton-no-auth.conf"
  mkdir -p "$runtime_dir"

  cat > "$canton_config_file" <<EOF
include file("/app/base-app.conf")

$(if [[ "$(resolve_tls_enabled)" == "1" ]]; then printf 'include file("/app/localnet-tls.conf")\n'; fi)

EOF
  local participant profile
  for participant in app-provider app-user sv; do
    case "$participant" in
      app-provider) profile="$APP_PROVIDER_PROFILE" ;;
      app-user) profile="$APP_USER_PROFILE" ;;
      sv) profile="$SV_PROFILE" ;;
    esac
    if [[ "$profile" != "off" ]]; then
      cat >> "$canton_config_file" <<EOF
canton.participants.${participant}.ledger-api.auth-services = []
EOF
    fi
  done

  cat > "$compose_file" <<EOF
services:
  canton:
    volumes:
      - "${LOCALNET_DIR}/conf/canton/app.conf:/app/base-app.conf:ro"
      - "$canton_config_file:/app/app.conf:ro"
  pqs-app-provider:
    environment:
      SCRIBE_SOURCE_LEDGER_AUTH: NoAuth
  pqs-app-user:
    environment:
      SCRIBE_SOURCE_LEDGER_AUTH: NoAuth
  pqs-sv:
    environment:
      SCRIBE_SOURCE_LEDGER_AUTH: NoAuth
EOF
  export LOCALNET_NO_AUTH_COMPOSE_FILE="$compose_file"
}

append_no_auth_args() {
  local -n compose_args_ref="$1"
  if [[ "$(resolve_no_auth_enabled)" == "1" ]]; then
    local compose_file="$(no_auth_runtime_dir)/compose-no-auth.yaml"
    if [[ -f "$compose_file" ]]; then
      compose_args_ref+=( -f "$compose_file" )
    fi
  fi
}

append_es256_args() {
  local -n compose_args_ref="$1"
  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
    compose_args_ref+=( -f "$LOCALNET_ES256_COMPOSE_FILE" )
  fi
}

append_tls_args() {
  local -n compose_args_ref="$1"
  if [[ "$(resolve_tls_enabled)" == "1" ]]; then
    compose_args_ref+=( -f "$LOCALNET_TLS_COMPOSE_FILE" )
  fi
}

resolve_docker_compose_cmd() {
  if (( ${#DOCKER_COMPOSE_CMD[@]} > 0 )); then
    return 0
  fi

  if docker compose version >/dev/null 2>&1; then
    DOCKER_COMPOSE_CMD=(docker compose)
    return 0
  fi

  if docker-compose version >/dev/null 2>&1; then
    DOCKER_COMPOSE_CMD=(docker-compose)
    return 0
  fi

  echo "Unable to find a working Docker Compose command. Install 'docker compose' or 'docker-compose'." >&2
  return 1
}

docker_compose() {
  resolve_docker_compose_cmd
  "${DOCKER_COMPOSE_CMD[@]}" "$@"
}

resolve_extra_participant_auth() {
  local index="$1"
  local var="EXTRA_PARTICIPANT_${index}_AUTH"
  local value="${!var:-}"
  if [[ -z "$value" ]]; then
    if [[ "$(resolve_no_auth_enabled)" == "1" ]]; then value="none"; else value="jwt"; fi
  fi
  case "$value" in
    jwt|none) ;;
    *)
      echo "EXTRA_PARTICIPANT_${index}_AUTH must be 'jwt' or 'none'." >&2
      return 1
      ;;
  esac
  printf '%s\n' "$value"
}

resolve_extra_participants() {
  local value="${EXTRA_PARTICIPANTS:-0}"
  if [[ ! "$value" =~ ^[0-9]+$ ]]; then
    echo "EXTRA_PARTICIPANTS must be a non-negative integer." >&2
    return 1
  fi
  if [[ "$value" == "0" ]]; then
    printf '0\n'
    return 0
  fi
  printf '%s\n' "$value"
}

load_localnet_common_env() {
  local common_env_file="$1"
  set -a
  # shellcheck disable=SC1090
  source "$common_env_file"
  set +a
  PARTICIPANT_LEDGER_API_PORT_SUFFIX="${PARTICIPANT_LEDGER_API_PORT_SUFFIX:-901}"
  PARTICIPANT_ADMIN_API_PORT_SUFFIX="${PARTICIPANT_ADMIN_API_PORT_SUFFIX:-902}"
  PARTICIPANT_JSON_API_PORT_SUFFIX="${PARTICIPANT_JSON_API_PORT_SUFFIX:-975}"
  VALIDATOR_ADMIN_API_PORT_SUFFIX="${VALIDATOR_ADMIN_API_PORT_SUFFIX:-903}"
  CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX="${CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX:-900}"
  CANTON_GRPC_HEALTHCHECK_PORT_SUFFIX="${CANTON_GRPC_HEALTHCHECK_PORT_SUFFIX:-961}"
}

participant_port() {
  local prefix="$1"
  local suffix="$2"
  printf '%s%s\n' "$prefix" "$suffix"
}

published_port() {
  local container_port="$1"
  local test_port
  test_port="${TEST_PORT:-$(read_env_value TEST_PORT || true)}"
  if [[ -n "$test_port" ]]; then
    printf '%s%s\n' "$test_port" "$container_port"
    return 0
  fi
  printf '%s\n' "$container_port"
}

extra_pqs_postgres_port() {
  local index="$1"
  printf '554%s\n' "$index"
}

resolve_party_hint_base() {
  local compose_env_file="$1"

  (
    set -a
    if [[ -f .env ]]; then
      # shellcheck disable=SC1091
      source .env
    fi
    if [[ -f .env.local ]]; then
      # shellcheck disable=SC1091
      source .env.local
    fi
    # shellcheck disable=SC1090
    source "$compose_env_file"
    set +a
    printf '%s\n' "${PARTY_HINT:-${DOCKER_NETWORK:-localnet}-localparty-1}"
  )
}

write_extra_participant_runtime_files() {
  local count="$1"
  local output_dir="$2"
  local party_hint_base="$3"
  local auth_mode="$4"
  mkdir -p "$output_dir"

  local env_file="$output_dir/extra-participants.env"
  local canton_config_file="$output_dir/additional-config.extra-participants.conf"
  local splice_config_file="$output_dir/additional-config.extra-validators.conf"
  local compose_file="$output_dir/compose-extra-participants.yaml"

  {
    printf 'START_LOCAL_GENERATED_DIR=%s\n' "$output_dir"
    local index
    for ((index = 1; index <= count; index++)); do
      local prefix=$((index + 4))
      local name="extra-${index}"
      local database="participant-${name}"
      local party_hint="extra_${index}_${party_hint_base}"
      local pqs_database="pqs-${name}"
      local pqs_name="pqs-${name}"
      local pqs_user_name="${name}-pqs-user"
      local pqs_postgres_name="postgres-pqs-${name}"
      local pqs_postgres_published_port
      local validator_database="validator-${name}"
      local validator_party_hint="validator_${index}_${party_hint_base}"
      local validator_onboarding_secret="${name}-validator-onboarding-secret"
      local validator_user_name="ledger-api-user"
      local wallet_admin_user_name="$name"
      local auth_audience="https://canton.network.global"
      local ledger_port admin_port json_port validator_admin_port http_health_port grpc_health_port
      local published_ledger_port published_admin_port published_json_port published_validator_admin_port
      ledger_port="$(participant_port "$prefix" "$PARTICIPANT_LEDGER_API_PORT_SUFFIX")"
      admin_port="$(participant_port "$prefix" "$PARTICIPANT_ADMIN_API_PORT_SUFFIX")"
      json_port="$(participant_port "$prefix" "$PARTICIPANT_JSON_API_PORT_SUFFIX")"
      validator_admin_port="$(participant_port "$prefix" "$VALIDATOR_ADMIN_API_PORT_SUFFIX")"
      http_health_port="$(participant_port "$prefix" "$CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX")"
      grpc_health_port="$(participant_port "$prefix" "$CANTON_GRPC_HEALTHCHECK_PORT_SUFFIX")"
      pqs_postgres_published_port="$(extra_pqs_postgres_port "$index")"
      published_ledger_port="$(published_port "$ledger_port")"
      published_admin_port="$(published_port "$admin_port")"
      published_json_port="$(published_port "$json_port")"
      published_validator_admin_port="$(published_port "$validator_admin_port")"

      printf 'CREATE_DATABASE_EXTRA_%s=%s\n' "$index" "$database"
      printf 'CREATE_DATABASE_EXTRA_VALIDATOR_%s=%s\n' "$index" "$validator_database"
      printf 'CREATE_DATABASE_EXTRA_PQS_%s=%s\n' "$index" "$pqs_database"
      printf 'EXTRA_PARTICIPANT_%s_NAME=%s\n' "$index" "$name"
      printf 'EXTRA_PARTICIPANT_%s_DB=%s\n' "$index" "$database"
      printf 'EXTRA_PARTICIPANT_%s_PARTY_HINT=%s\n' "$index" "$party_hint"
      printf 'EXTRA_PARTICIPANT_%s_LEDGER_API_PORT=%s\n' "$index" "$ledger_port"
      printf 'EXTRA_PARTICIPANT_%s_ADMIN_API_PORT=%s\n' "$index" "$admin_port"
      printf 'EXTRA_PARTICIPANT_%s_JSON_API_PORT=%s\n' "$index" "$json_port"
      printf 'EXTRA_PARTICIPANT_%s_HTTP_HEALTHCHECK_PORT=%s\n' "$index" "$http_health_port"
      printf 'EXTRA_PARTICIPANT_%s_GRPC_HEALTHCHECK_PORT=%s\n' "$index" "$grpc_health_port"
      printf 'EXTRA_PARTICIPANT_%s_LEDGER_API_PUBLISHED_PORT=%s\n' "$index" "$published_ledger_port"
      printf 'EXTRA_PARTICIPANT_%s_ADMIN_API_PUBLISHED_PORT=%s\n' "$index" "$published_admin_port"
      printf 'EXTRA_PARTICIPANT_%s_JSON_API_PUBLISHED_PORT=%s\n' "$index" "$published_json_port"
      printf 'EXTRA_VALIDATOR_%s_DB=%s\n' "$index" "$validator_database"
      printf 'EXTRA_VALIDATOR_%s_PARTY_HINT=%s\n' "$index" "$validator_party_hint"
      printf 'EXTRA_VALIDATOR_%s_ONBOARDING_SECRET=%s\n' "$index" "$validator_onboarding_secret"
      printf 'EXTRA_VALIDATOR_%s_ADMIN_API_PORT=%s\n' "$index" "$validator_admin_port"
      printf 'EXTRA_VALIDATOR_%s_ADMIN_API_PUBLISHED_PORT=%s\n' "$index" "$published_validator_admin_port"
      printf 'EXTRA_VALIDATOR_%s_USER_NAME=%s\n' "$index" "$validator_user_name"
      printf 'EXTRA_VALIDATOR_%s_WALLET_ADMIN_USER_NAME=%s\n' "$index" "$wallet_admin_user_name"
      printf 'EXTRA_VALIDATOR_%s_AUTH_AUDIENCE=%s\n' "$index" "$auth_audience"
      printf 'EXTRA_VALIDATOR_%s_AUTH_MODE=%s\n' "$index" "$auth_mode"
      printf 'EXTRA_PQS_%s_NAME=%s\n' "$index" "$pqs_name"
      printf 'EXTRA_PQS_%s_USER_NAME=%s\n' "$index" "$pqs_user_name"
      printf 'EXTRA_PQS_%s_POSTGRES_NAME=%s\n' "$index" "$pqs_postgres_name"
      printf 'EXTRA_PQS_%s_POSTGRES_DB=%s\n' "$index" "$pqs_database"
      printf 'EXTRA_PQS_%s_POSTGRES_PUBLISHED_PORT=%s\n' "$index" "$pqs_postgres_published_port"
      printf 'EXTRA_PQS_%s_CONFIG_FILE=%s-pqs.conf\n' "$index" "$name"
    done
  } > "$env_file"

  {
    local index
    for ((index = 1; index <= count; index++)); do
      cat <<EOF
canton.participants.extra-${index} = \$\${_participant} {
  storage.config.properties.databaseName = "participant-extra-${index}"
  init {
    generate-topology-transactions-and-keys = true
    identity.type = auto
  }
  monitoring {
    http-health-server.port = \${EXTRA_PARTICIPANT_${index}_HTTP_HEALTHCHECK_PORT}
    grpc-health-server.port = \${EXTRA_PARTICIPANT_${index}_GRPC_HEALTHCHECK_PORT}
  }
  http-ledger-api.port = \${EXTRA_PARTICIPANT_${index}_JSON_API_PORT}
  admin-api.port = \${EXTRA_PARTICIPANT_${index}_ADMIN_API_PORT}
  ledger-api.port = \${EXTRA_PARTICIPANT_${index}_LEDGER_API_PORT}
  ledger-api {
    auth-services = [{
      type = unsafe-jwt-hmac-256
      target-audience = "\${EXTRA_VALIDATOR_${index}_AUTH_AUDIENCE}"
      secret = "unsafe"
    }]

    user-management-service.additional-admin-user-id = "\${EXTRA_VALIDATOR_${index}_USER_NAME}"
  }
}

EOF
    done
  } > "$canton_config_file"

  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
    local index
    for ((index = 1; index <= count; index++)); do
      cat >> "$canton_config_file" <<EOF
canton.participants.extra-${index}.ledger-api.auth-services = [
  {
    type = unsafe-jwt-hmac-256
    target-audience = "https://canton.network.global"
    secret = "unsafe"
  },
  {
    type = jwt-es-256-crt
    certificate = "/app/es256-certificate.pem"
    target-audience = "https://canton.network.global/es256"
  }
]

EOF
    done
  fi

  local no_auth_index
  for ((no_auth_index = 1; no_auth_index <= count; no_auth_index++)); do
    if [[ "$(resolve_extra_participant_auth "$no_auth_index")" == "none" ]]; then
      cat >> "$canton_config_file" <<EOF
canton.participants.extra-${no_auth_index}.ledger-api.auth-services = []

EOF
    fi
  done

  if [[ "$(resolve_tls_enabled)" == "1" ]]; then
    local index
    for ((index = 1; index <= count; index++)); do
      cat >> "$canton_config_file" <<EOF
canton.participants.extra-${index}.ledger-api.tls {
  cert-chain-file = "/app/localnet-tls/server.crt"
  private-key-file = "/app/localnet-tls/server.key"
  trust-collection-file = "/app/localnet-tls/ca.crt"
}
canton.participants.extra-${index}.admin-api.tls {
  cert-chain-file = "/app/localnet-tls/server.crt"
  private-key-file = "/app/localnet-tls/server.key"
  trust-collection-file = "/app/localnet-tls/ca.crt"
}

EOF
    done
  fi

  {
    local index
    for ((index = 1; index <= count; index++)); do
      local validator_backend_ref='$${_validator_backend}'
      cat <<EOF
canton.validator-apps.extra-${index}-validator_backend = ${validator_backend_ref} {
  onboarding.secret = "\${EXTRA_VALIDATOR_${index}_ONBOARDING_SECRET}"
  domain-migration-dump-path = "/domain-upgrade-dump/domain_migration_dump-extra-${index}.json"
  storage.config.properties.databaseName = "\${EXTRA_VALIDATOR_${index}_DB}"
  admin-api.port = \${EXTRA_VALIDATOR_${index}_ADMIN_API_PORT}
  canton-identifier-config.participant = extra-${index}
  participant-client {
    admin-api.port = \${EXTRA_PARTICIPANT_${index}_ADMIN_API_PORT}
    ledger-api {
      client-config.port = \${EXTRA_PARTICIPANT_${index}_LEDGER_API_PORT}
      auth-config = {
        type = "self-signed"
        user = "\${EXTRA_VALIDATOR_${index}_USER_NAME}"
        audience = "\${EXTRA_VALIDATOR_${index}_AUTH_AUDIENCE}"
        secret = "unsafe"
      }
    }
  }
  auth = {
    algorithm = "hs-256-unsafe"
    audience = "\${EXTRA_VALIDATOR_${index}_AUTH_AUDIENCE}"
    secret = "unsafe"
  }
  ledger-api-user = "\${EXTRA_VALIDATOR_${index}_USER_NAME}"
  validator-wallet-users.0 = "\${EXTRA_VALIDATOR_${index}_WALLET_ADMIN_USER_NAME}"
  validator-party-hint = "\${EXTRA_VALIDATOR_${index}_PARTY_HINT}"

  domains.global.buy-extra-traffic {
    min-topup-interval = \${?MIN_TRAFFIC_TOPUP_INTERVAL}
    target-throughput = \${?TARGET_TRAFFIC_THROUGHPUT}
  }
}

canton.sv-apps.sv.expected-validator-onboardings += { secret = "\${EXTRA_VALIDATOR_${index}_ONBOARDING_SECRET}" }

EOF
    done
  } > "$splice_config_file"

  if [[ "$(resolve_tls_enabled)" == "1" ]]; then
    local index
    for ((index = 1; index <= count; index++)); do
      cat >> "$splice_config_file" <<EOF
canton.validator-apps.extra-${index}-validator_backend.participant-client {
  admin-api.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
  ledger-api.client-config.tls.trust-collection-file = "/app/localnet-tls/ca.crt"
}

EOF
    done
  fi

  {
    cat <<'EOF'
volumes:
EOF
    local index
    for ((index = 1; index <= count; index++)); do
      cat <<EOF
  postgres-pqs-extra-${index}:
EOF
    done
    cat <<'EOF'

services:
  postgres:
    env_file:
      - ${START_LOCAL_GENERATED_DIR}/extra-participants.env
  canton:
    env_file:
      - ${START_LOCAL_GENERATED_DIR}/extra-participants.env
    environment:
      ADDITIONAL_CONFIG_EXTRA_PARTICIPANTS: |
EOF
    sed 's/^/        /' "$canton_config_file"
    cat <<'EOF'
    ports:
EOF
    for ((index = 1; index <= count; index++)); do
      cat <<EOF
      - "\${EXTRA_PARTICIPANT_${index}_LEDGER_API_PUBLISHED_PORT}:\${EXTRA_PARTICIPANT_${index}_LEDGER_API_PORT}"
      - "\${EXTRA_PARTICIPANT_${index}_ADMIN_API_PUBLISHED_PORT}:\${EXTRA_PARTICIPANT_${index}_ADMIN_API_PORT}"
      - "\${EXTRA_PARTICIPANT_${index}_JSON_API_PUBLISHED_PORT}:\${EXTRA_PARTICIPANT_${index}_JSON_API_PORT}"
EOF
    done
    cat <<'EOF'
  splice:
    env_file:
      - ${START_LOCAL_GENERATED_DIR}/extra-participants.env
    environment:
      ADDITIONAL_CONFIG_EXTRA_VALIDATORS: |
EOF
    sed -e 's/\${?/\$\${?/g' -e 's/^/        /' "$splice_config_file"
    cat <<'EOF'
    ports:
EOF
    for ((index = 1; index <= count; index++)); do
      cat <<EOF
      - "\${EXTRA_VALIDATOR_${index}_ADMIN_API_PUBLISHED_PORT}:\${EXTRA_VALIDATOR_${index}_ADMIN_API_PORT}"
EOF
    done
    for ((index = 1; index <= count; index++)); do
      cat <<EOF
  postgres-pqs-extra-${index}:
    image: "postgres:\${POSTGRES_VERSION}"
    container_name: postgres-pqs-extra-${index}
    volumes:
      - postgres-pqs-extra-${index}:/var/lib/postgresql/data
      - \${LOCALNET_DIR}/docker/postgres/postgres-entrypoint.sh:/postgres-entrypoint.sh
    environment:
      POSTGRES_USER: \${DB_USER}
      POSTGRES_PASSWORD: \${DB_PASSWORD}
      POSTGRES_DB: postgres
      CREATE_DATABASE_EXTRA_PQS_${index}: \${EXTRA_PQS_${index}_POSTGRES_DB}
    ports:
      - "\${EXTRA_PQS_${index}_POSTGRES_PUBLISHED_PORT}:5432"
    entrypoint: /postgres-entrypoint.sh
    healthcheck:
      test: "pg_isready -U \${DB_USER} -d postgres"
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s
    user: "postgres"
    command:
      - postgres
      - -c
      - max_connections=1000
  pqs-extra-${index}:
    image: \${SCRIBE_IMAGE}:\${SCRIBE_VERSION}
    container_name: pqs-extra-${index}
    environment:
      SCRIBE_SOURCE_LEDGER_HOST: canton
      SCRIBE_SOURCE_LEDGER_PORT: \${EXTRA_PARTICIPANT_${index}_LEDGER_API_PORT}
      SCRIBE_SOURCE_LEDGER_AUTH: $(if [[ "$(resolve_extra_participant_auth "$index")" == "none" ]]; then printf 'NoAuth'; else printf 'OAuth'; fi)
      SCRIBE_TARGET_POSTGRES_HOST: postgres-pqs-extra-${index}
      SCRIBE_TARGET_POSTGRES_PORT: 5432
      SCRIBE_TARGET_POSTGRES_DATABASE: \${EXTRA_PQS_${index}_POSTGRES_DB}
      SCRIBE_TARGET_POSTGRES_USERNAME: \${DB_USER}
      SCRIBE_TARGET_POSTGRES_PASSWORD: \${DB_PASSWORD}
      SCRIBE_CONFIG: /onboarding/extra-${index}-pqs.conf
EOF
    if [[ "$(resolve_tls_enabled)" == "1" ]]; then
      cat <<'EOF'
      SCRIBE_SOURCE_LEDGER_TLS_CAFILE: /app/localnet-tls/ca.crt
EOF
    fi
    cat <<EOF
    volumes:
      - onboarding:/onboarding
EOF
    if [[ "$(resolve_tls_enabled)" == "1" ]]; then
      printf '      - "%s:/app/localnet-tls/ca.crt:ro"\n' "${LOCALNET_TLS_CA_CERT_PATH}"
    fi
    cat <<EOF
    command:
      - pipeline
      - ledger
      - postgres-document
    depends_on:
      canton:
        condition: service_healthy
      splice-onboarding:
        condition: service_healthy
      postgres-pqs-extra-${index}:
        condition: service_healthy
    restart: on-failure:100
EOF
    done
  } > "$compose_file"
}

append_extra_participant_args() {
  local count="$1"
  local auth_mode="$2"
  local -n compose_args_ref="$3"
  local output_dir
  output_dir="$(generated_dir)"

  if (( count == 0 )); then
    rm -rf "$output_dir"
    return 0
  fi

  write_extra_participant_runtime_files "$count" "$output_dir" "$PARTY_HINT_BASE" "$auth_mode"
  compose_args_ref+=(
    -f "$output_dir/compose-extra-participants.yaml"
    --env-file "$output_dir/extra-participants.env"
  )
}

extra_participants_healthy() {
  local count="$1"
  if (( count == 0 )); then
    return 0
  fi

  local checks=()
  local index
  for ((index = 1; index <= count; index++)); do
    local prefix=$((index + 4))
    local health_port
    health_port="$(participant_port "$prefix" "$CANTON_HTTP_HEALTHCHECK_PORT_SUFFIX")"
    checks+=("wget -q --timeout=2 --tries=1 -O /dev/null http://localhost:${health_port}/health")
  done

  local joined_checks
  joined_checks="$(printf '%s && ' "${checks[@]}")"
  joined_checks="${joined_checks% && }"
  docker exec canton bash -lc "$joined_checks" >/dev/null 2>&1
}

prerequisite_services() {
  local auth_mode="$1"
  local services=(postgres canton)
  if [[ "$auth_mode" == "oauth2" ]]; then
    services=(keycloak nginx-keycloak "${services[@]}")
  fi
  printf '%s\n' "${services[@]}"
}

resolve_pqs_enabled() {
  local value="${LOCALNET_PQS:-1}"
  if [[ "$value" != "0" && "$value" != "1" ]]; then
    echo "LOCALNET_PQS must be 0 or 1." >&2
    return 1
  fi
  printf '%s\n' "$value"
}

dependent_services() {
  if [[ "$(resolve_pqs_enabled)" == "0" ]]; then
    return 0
  fi
  printf '%s\n' pqs-app-provider pqs-sv
}

start_splice_services() {
  local -n compose_args_ref="$1"
  local onboarding_container

  # Start the combined Splice process without waiting for its own healthcheck.
  # Compose still applies splice-onboarding's service_healthy dependency even
  # with --no-deps when both services are passed to the same `up` invocation.
  docker_compose "${compose_args_ref[@]}" up -d --no-recreate --no-deps splice
  docker_compose "${compose_args_ref[@]}" create splice-onboarding
  onboarding_container="$(docker_compose "${compose_args_ref[@]}" ps -a -q splice-onboarding | tail -n 1)"
  if [[ -z "$onboarding_container" ]]; then
    echo "Unable to resolve the splice-onboarding container." >&2
    return 1
  fi
  docker start "$onboarding_container" >/dev/null
}

extra_pqs_services() {
  local count="$1"
  local index
  for ((index = 1; index <= count; index++)); do
    printf 'pqs-extra-%s\n' "$index"
  done
}

container_readiness_state() {
  local container="$1"
  docker inspect --format '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container" \
    2>/dev/null | tail -n 1 || true
}

container_health_is_ready() {
  local _container="$1"
  local _lifecycle_state="$2"
  local health_status="$3"
  [[ "$lifecycle_state" == "running" && "$health_status" == "healthy" ]]
}

canton_and_extras_are_ready() {
  local _container="$1"
  local _lifecycle_state="$2"
  local health_status="$3"
  local extra_participants="$4"
  [[ "$lifecycle_state" == "running" && "$health_status" == "healthy" ]] \
    && extra_participants_healthy "$extra_participants"
}

print_container_logs() {
  local container="$1"
  echo "Recent logs for $container:" >&2
  docker logs --tail 40 "$container" >&2 || true
}

readiness_elapsed_seconds() {
  if [[ -n "${START_LOCAL_TEST_READINESS_CLOCK_FILE:-}" ]]; then
    cat "$START_LOCAL_TEST_READINESS_CLOCK_FILE"
    return 0
  fi
  printf '%s\n' "$SECONDS"
}

wait_for_container_readiness() {
  local container="$1"
  local readiness_predicate="$2"
  shift 2

  local last_readiness_state=""
  local last_diagnostic_at
  last_diagnostic_at="$(readiness_elapsed_seconds)"

  while true; do
    local lifecycle_state health_status readiness_state
    read -r lifecycle_state health_status <<< "$(container_readiness_state "$container")"
    lifecycle_state="${lifecycle_state:-missing}"
    health_status="${health_status:-unknown}"

    if [[ "$lifecycle_state" != "running" && "$lifecycle_state" != "restarting" ]]; then
      echo "$container is not running (state: $lifecycle_state)." >&2
      print_container_logs "$container"
      return 1
    fi

    readiness_state="$lifecycle_state/$health_status"
    if [[ "$readiness_state" != "$last_readiness_state" ]]; then
      echo "$container readiness: $readiness_state"
      last_readiness_state="$readiness_state"
    fi

    if "$readiness_predicate" "$container" "$lifecycle_state" "$health_status" "$@"; then
      return 0
    fi

    local now
    now="$(readiness_elapsed_seconds)"
    if (( now - last_diagnostic_at >= 30 )); then
      echo "$container is still not ready (state: $lifecycle_state, health: $health_status)." >&2
      print_container_logs "$container"
      last_diagnostic_at="$now"
    fi

    sleep 2
  done
}

wait_for_canton_health() {
  local extra_participants="${1:-0}"
  wait_for_container_readiness canton canton_and_extras_are_ready "$extra_participants"
}

container_is_healthy() {
  local _container="$1"
  local lifecycle_state="$2"
  local health_status="$3"
  [[ "$lifecycle_state" == "running" && "$health_status" == "healthy" ]]
}

wait_for_onboarding_health() {
  local onboarding_container
  onboarding_container="$(docker ps -a --filter name=splice-onboarding --format '{{.Names}}' | head -n 1)"
  if [[ -z "$onboarding_container" ]]; then
    echo "Unable to resolve the splice-onboarding container for readiness." >&2
    return 1
  fi
  wait_for_container_readiness "$onboarding_container" container_is_healthy
}

provision_extra_pqs() {
  local count="$1"
  if (( count == 0 )); then
    return 0
  fi

  local index
  for ((index = 1; index <= count; index++)); do
    local name="extra-${index}"
    local json_port
    json_port="$(published_port "$(participant_port "$((index + 4))" "$PARTICIPANT_JSON_API_PORT_SUFFIX")")"

    docker exec \
      -e EXTRA_PARTICIPANT_NAME="$name" \
      -e EXTRA_PARTICIPANT_JSON_API_PORT="$json_port" \
      -e EXTRA_PARTICIPANT_PARTY_HINT="extra_${index}_${PARTY_HINT_BASE}" \
      -e EXTRA_PQS_USER_NAME="${name}-pqs-user" \
      -e EXTRA_PQS_CONFIG_FILE="${name}-pqs.conf" \
      -e LOCALNET_HTTP_SCHEME="$(localnet_http_scheme)" \
      splice-onboarding \
      bash -lc '
set -euo pipefail
source /app/utils.sh
participant="canton:${EXTRA_PARTICIPANT_JSON_API_PORT}"
admin_token="$(generate_jwt "participant_admin" "$AUTH_APP_PROVIDER_AUDIENCE")"
party="$(allocate_party "$admin_token" "$EXTRA_PARTICIPANT_PARTY_HINT" "$participant")"
create_user "$admin_token" "$EXTRA_PQS_USER_NAME" "$EXTRA_PQS_USER_NAME" "$party" "$participant"
grant_rights "$admin_token" "$EXTRA_PQS_USER_NAME" "$party" "ReadAs" "$participant"
pqs_user_token="$(generate_jwt "$EXTRA_PQS_USER_NAME" "$AUTH_APP_PROVIDER_AUDIENCE")"
share_file "$EXTRA_PQS_CONFIG_FILE" <<EOF
pipeline.datasource=TransactionTreeStream
pipeline.ledger.start=Oldest
pipeline.oauth.accessToken="${pqs_user_token}"
EOF
'

    grant_user_read_as_any_party_right \
      "${name}-pqs-user" \
      "https://canton.network.global" \
      "$json_port"
  done
}

grant_validator_read_as_any_party_right() {
  local validator_user_name="$1"
  local auth_audience="$2"
  local participant_json_port="$3"

  docker exec \
    -e VALIDATOR_USER_NAME="$validator_user_name" \
    -e VALIDATOR_AUTH_AUDIENCE="$auth_audience" \
    -e PARTICIPANT_JSON_API_PORT="$participant_json_port" \
    -e LOCALNET_HTTP_SCHEME="$(localnet_http_scheme)" \
    splice-onboarding \
    bash -lc '
set -euo pipefail
source /app/utils.sh
participant="canton:${PARTICIPANT_JSON_API_PORT}"
validator_token="$(generate_jwt "$VALIDATOR_USER_NAME" "$VALIDATOR_AUTH_AUDIENCE")"
payload="$(cat <<EOF
{
  "userId": "${VALIDATOR_USER_NAME}",
  "identityProviderId": "",
  "rights": [
    {"kind":{"CanReadAsAnyParty":{"value":{}}}}
  ]
}
EOF
)"
curl_check "${LOCALNET_HTTP_SCHEME:-http}://$participant/v2/users/$VALIDATOR_USER_NAME/rights" "$validator_token" "application/json" \
  --data-raw "$payload" >/dev/null
'
}

grant_user_read_as_any_party_right() {
  local user_name="$1"
  local auth_audience="$2"
  local participant_json_port="$3"

  docker exec \
    -e TARGET_USER_NAME="$user_name" \
    -e TARGET_AUTH_AUDIENCE="$auth_audience" \
    -e PARTICIPANT_JSON_API_PORT="$participant_json_port" \
    -e LOCALNET_HTTP_SCHEME="$(localnet_http_scheme)" \
    splice-onboarding \
    bash -lc '
set -euo pipefail
source /app/utils.sh
participant="canton:${PARTICIPANT_JSON_API_PORT}"
admin_token="$(generate_jwt "participant_admin" "$TARGET_AUTH_AUDIENCE")"
payload="$(cat <<EOF
{
  "userId": "${TARGET_USER_NAME}",
  "identityProviderId": "",
  "rights": [
    {"kind":{"CanReadAsAnyParty":{"value":{}}}}
  ]
}
EOF
)"
curl_check "${LOCALNET_HTTP_SCHEME:-http}://$participant/v2/users/$TARGET_USER_NAME/rights" "$admin_token" "application/json" \
  --data-raw "$payload" >/dev/null
'
}

grant_localnet_validator_read_rights() {
  local extra_participants="${1:-0}"
  local validator_user_name="ledger-api-user"
  local auth_audience="https://canton.network.global"

  grant_validator_read_as_any_party_right \
    "$validator_user_name" \
    "$auth_audience" \
    "$(published_port "$(participant_port 3 "$PARTICIPANT_JSON_API_PORT_SUFFIX")")"

  grant_validator_read_as_any_party_right \
    "$validator_user_name" \
    "$auth_audience" \
    "$(published_port "$(participant_port 4 "$PARTICIPANT_JSON_API_PORT_SUFFIX")")"

  local index
  for ((index = 1; index <= extra_participants; index++)); do
    grant_validator_read_as_any_party_right \
      "$validator_user_name" \
      "$auth_audience" \
      "$(published_port "$(participant_port "$((index + 4))" "$PARTICIPANT_JSON_API_PORT_SUFFIX")")"
  done
}

start_ledger_stack() {
  local auth_mode="${1:-shared-secret}"
  export AUTH_MODE="$auth_mode"
  local modules_dir="$QUICKSTART_DIR/docker/modules"
  local localnet_dir="$modules_dir/localnet"
  local splice_version
  splice_version="$(read_env_value SPLICE_VERSION || true)"
  export MODULES_DIR="$modules_dir"
  export LOCALNET_DIR="$localnet_dir"
  export LOCALNET_ENV_DIR="$localnet_dir/env"
  export IMAGE_TAG="${IMAGE_TAG:-$splice_version}"
  export APP_PROVIDER_PROFILE=on
  export APP_USER_PROFILE=off
  export SV_PROFILE=on
  export PQS_SV_PROFILE=on
  load_localnet_common_env "$localnet_dir/env/common.env"
  prepare_tls_runtime_files
  prepare_es256_runtime_files
  prepare_no_auth_runtime_files
  local extra_participants
  extra_participants="$(resolve_extra_participants)" 
  if (( extra_participants > 0 )) && [[ "$auth_mode" != "shared-secret" ]]; then
    echo "EXTRA_PARTICIPANTS with extra PQS currently supports AUTH_MODE=shared-secret only." >&2
    return 1
  fi
  PARTY_HINT_BASE="$(resolve_party_hint_base "$localnet_dir/compose.env")"
  export PARTY_HINT_BASE
  local compose_args=(
    -f compose.yaml
    -f "$localnet_dir/compose.yaml"
    -f "$modules_dir/splice-onboarding/compose.yaml"
    -f "$modules_dir/pqs/compose.yaml"
    --env-file .env
    --env-file .env.local
    --env-file "$localnet_dir/compose.env"
    --env-file "$localnet_dir/env/common.env"
    --env-file "$modules_dir/pqs/compose.env"
    --profile app-provider
    --profile pqs-app-provider
    --profile pqs-sv
  )

  if [[ "$auth_mode" == "oauth2" ]]; then
    compose_args+=(
      -f "$modules_dir/keycloak/compose.yaml"
      --env-file "$modules_dir/keycloak/compose.env"
      --profile keycloak
    )
  fi

  append_extra_participant_args "$extra_participants" "$auth_mode" compose_args
  append_tls_args compose_args
  append_es256_args compose_args
  append_no_auth_args compose_args
  docker_compose "${compose_args[@]}" down -v --remove-orphans
  mapfile -t startup_services < <(prerequisite_services "$auth_mode")
  mapfile -t followup_services < <(dependent_services)

  docker_compose "${compose_args[@]}" up -d --no-recreate "${startup_services[@]}"
  wait_for_canton_health "$extra_participants"

  # SV initialization waits for onboarding contracts, while splice health waits
  # for SV and scan initialization. Start onboarding without Compose dependency
  # gating so its healthcheck can retry until those apps are ready; PQS still
  # starts afterward and retains its onboarding health dependency.
  start_splice_services compose_args
  if (( ${#followup_services[@]} > 0 )); then
    docker_compose "${compose_args[@]}" up -d --no-recreate "${followup_services[@]}"
  else
    # Without PQS nothing carries a service_healthy dependency on splice-onboarding, so wait explicitly —
    # the participant is not usable until onboarding finishes.
    wait_for_onboarding_health
  fi

  if (( extra_participants > 0 )); then
    wait_for_container_readiness splice container_health_is_ready
    wait_for_container_readiness splice-onboarding container_health_is_ready
    if [[ "$auth_mode" == "shared-secret" ]]; then
      grant_localnet_validator_read_rights "$extra_participants"
    fi
    if [[ "$(resolve_pqs_enabled)" == "1" ]]; then
      provision_extra_pqs "$extra_participants"
      local extra_pqs_services_list=()
      mapfile -t extra_pqs_services_list < <(extra_pqs_services "$extra_participants")
      docker_compose "${compose_args[@]}" up -d --no-recreate "${extra_pqs_services_list[@]}"
    fi
  fi
}

load_repo_root_env
QUICKSTART_DIR="$(resolve_quickstart_dir)"
cd "$QUICKSTART_DIR"

if [[ "$(resolve_no_auth_enabled)" == "1" && "$(resolve_es256_enabled)" == "1" ]]; then
  echo "LOCALNET_NO_AUTH=1 conflicts with LOCALNET_ES256_JWT=1: pick one auth mode." >&2
  exit 1
fi

if [[ ! -f .env.local ]]; then
  echo ".env.local not found. Bootstrapping Quickstart with 'make setup'."
  make setup
fi

if [[ "$(resolve_es256_enabled)" != "1" && "$(resolve_tls_enabled)" != "1" && "$(resolve_no_auth_enabled)" != "1" ]] && make_target_exists start-local-ledger; then
  make start-local-ledger
else
  auth_mode="$(read_env_value AUTH_MODE || true)"
  auth_mode="${auth_mode:-shared-secret}"
  if [[ "$(resolve_es256_enabled)" == "1" || "$(resolve_tls_enabled)" == "1" || "$(resolve_no_auth_enabled)" == "1" ]]; then
    echo "Generated localnet transport overlay enabled. Starting ledger-only compose stack directly."
  else
    echo "start-local-ledger target not found. Starting ledger-only compose stack directly."
  fi
  start_ledger_stack "$auth_mode"
  if [[ "$(resolve_es256_enabled)" == "1" ]]; then
    echo "ES256 bearer token written to $LOCALNET_ES256_TOKEN_PATH"
  fi
fi
