#!/bin/bash
#
# Eon MongoDB Atlas Onboarding Script
# ------------------------------------
# Creates database users and a dedicated Eon API key for a "source" or "restore"
# environment in MongoDB Atlas. Authentication is done via browser login using
# the OAuth 2.0 Device Authorization flow (same as the Atlas CLI).
#
#   - Source (org):     readAnyDatabase + clusterMonitor database users per project + org-level Eon API key
#   - Source (project): readAnyDatabase + clusterMonitor database user + project-level Eon API key
#   - Restore (project): readWriteAnyDatabase + enableSharding database user + project-level Eon API key
#                        (+ atlasAdmin when --grant-balancer-control is passed; see that flag's help text)
#
# When --project-id is provided, the setup is scoped to that project.
# Otherwise (source only), the setup covers all projects in the organization.
#
# Network access: Eon connects to your clusters over the public internet, so each project's IP access list
# must include Eon's static IPs. The list is project-wide and applies to clusters on every cloud provider
# (AWS, Azure and GCP). Pass --eon-static-ips to add the entries, or manage the list yourself.
#
# Workload Identity Federation (optional): pass --oidc-gcp-issuer-uri/--oidc-gcp-principal
# and/or --oidc-azure-issuer-uri/--oidc-azure-principal to also create OIDC database users (same roles as
# the AWS IAM user, plus clusterMonitor on restore) for the GCP service account and Azure app Eon signs in
# as in every onboarded project. Source and restore take different GCP principals, but Azure restores still
# sign in as the scanning app: when one project is both source and restore, pass the Azure ones on the
# restore run only, since Atlas allows one user per OIDC identity in a project.
# Eon's Azure and GCP runners log in to Atlas with these users. An Atlas Organization Owner must first add
# Eon's Workload Identity Providers to the Atlas organization; see --help for the values. For GCP,
# --create-oidc-gcp-idp has the script add and connect that IdP instead. The script looks each IdP ID up
# in the organization's federation settings by issuer URI; --oidc-*-idp-id overrides the lookup when needed.
#
# Authentication: The script uses MongoDB Atlas OAuth device authorization.
# A browser window will open for you to log in with your Atlas credentials.
# No API keys need to be created or passed as arguments.
#
# Usage (Source - org-scoped):
#   ./mongo-atlas-eon-setup.sh \
#       --account-type Source \
#       --scanning-node-role-arn arn:aws:iam::123456789012:role/EonScanningNodeRole \
#       --eon-account-id 6b2e5764-a189-43ae-a173-8ad44f6bf160
#
# Usage (Source - project-scoped):
#   ./mongo-atlas-eon-setup.sh \
#       --account-type Source \
#       --project-id 60b8e1234567890abcdef345 \
#       --scanning-node-role-arn arn:aws:iam::123456789012:role/EonScanningNodeRole \
#       --eon-account-id 6b2e5764-a189-43ae-a173-8ad44f6bf160
#
# Usage (Restore - project-scoped):
#   ./mongo-atlas-eon-setup.sh \
#       --account-type Restore \
#       --project-id 60b8e1234567890abcdef345 \
#       --restore-node-role-arn arn:aws:iam::123456789012:role/EonRestoreNodeRole \
#       --eon-account-id 6b2e5764-a189-43ae-a173-8ad44f6bf160 \
#       [--grant-balancer-control]
#
set -e

ACCOUNT_TYPE=""
PROJECT_ID=""
ORG_ID=""
SCANNING_NODE_ROLE_ARN=""
RESTORE_NODE_ROLE_ARN=""
EON_ACCOUNT_ID=""
GRANT_BALANCER_CONTROL=false
VERBOSE=false
EON_STATIC_IPS=""
OIDC_GCP_IDP_ID=""
OIDC_GCP_PRINCIPAL=""
OIDC_GCP_ISSUER_URI=""
OIDC_GCP_CREATE_IDP=false
OIDC_GCP_AUDIENCE=""
OIDC_AZURE_IDP_ID=""
OIDC_AZURE_PRINCIPAL=""
OIDC_AZURE_ISSUER_URI=""
EON_API_PUBLIC_KEY=""
EON_API_PRIVATE_KEY=""
EON_MONGO_ATLAS_SOURCE_VERSION="1.0.6"
EON_MONGO_ATLAS_RESTORE_VERSION="1.0.11"

ATLAS_BASE="https://cloud.mongodb.com"
ATLAS_API_BASE="${ATLAS_BASE}/api/atlas/v2"
# Public OAuth client ID from the Atlas CLI source (mongodb/mongodb-atlas-cli).
# Not a secret — used by every `atlas auth login` invocation.
ATLAS_OAUTH_CLIENT_ID="0oabtxactgS3gHIR0297"
ATLAS_ACCESS_TOKEN=""

####
# Colors
end="\033[0m"
green="\033[0;32m"
blue="\033[0;34m"
red="\033[0;31m"

##############################################
# Logging + Helper Functions
##############################################
log_info()    { echo -e "\033[1;34m[INFO] $1\033[0m"; }
log_success() { echo -e "\033[1;32m[SUCCESS] $1\033[0m"; }
log_error()   { echo -e "\033[1;31m[ERROR] $1\033[0m" >&2; }
log_warn()    { echo -e "\033[1;33m[WARN] $1\033[0m"; }
log_verbose() {
    if [ "$VERBOSE" = true ]; then
        echo -e "\033[1;36m[VERBOSE] $1\033[0m"
    fi
}

# URL-encode a string (for use in URL path segments)
urlencode() {
  jq -rn --arg v "$1" '$v|@uri'
}

# Build MongoDB Atlas database user description as JSON.
# Args: account_type version method
build_description() {
  local account_type="$1"
  local version="$2"
  local method="$3"

  local label
  if [[ "$account_type" == "source" ]]; then
    label="Eon.io Source Account"
  else
    label="Eon.io Restore Account"
  fi

  jq -c -n \
    --arg description "$label" \
    --arg version "$version" \
    --arg method "$method" \
    '{description: $description, version: $version, method: $method, vars: {}}'
}

show_help() {
  log_info "Usage: $0 [options]"
  echo ""
  echo "Options:"
  echo "  --account-type <Source|Restore>                (Required) Which account type logic to run."
  echo "  --project-id <PROJECT_ID>                     (Optional for Source, Required for Restore) MongoDB Atlas Project ID (24-char hex)."
  echo "                                                 When provided, the setup is scoped to this project."
  echo "  --org-id <ORG_ID>                             (Optional) MongoDB Atlas Organization ID (24-char hex)."
  echo "                                                 Required for org-scoped source when your account has multiple organizations."
  echo "                                                 Auto-detected when you belong to a single organization."
  echo "  --eon-account-id <UUID>                        (Required) Eon account UUID for naming/tracking."
  echo "  --scanning-node-role-arn <ARN>                 (Required for Source) AWS IAM role ARN for the Eon scanning node."
  echo "  --restore-node-role-arn <ARN>                  (Required for Restore) AWS IAM role ARN for the Eon restore node."
  echo "  --eon-static-ips <CIDR,CIDR,...>               (Optional) Comma-separated Eon static IPs/CIDRs to add to the IP access"
  echo "                                                 list of every onboarded project. See \"Network access\" below."
  echo "  --grant-balancer-control                       (Optional, Restore only) Additionally grant the atlasAdmin role to the"
  echo "                                                 Eon restore database user so restores into sharded clusters can pause"
  echo "                                                 the balancer while bulk-loading (significantly faster sharded restores;"
  echo "                                                 chunk migrations compete with restore inserts). atlasAdmin is the only"
  echo "                                                 Atlas mechanism carrying the needed privilege (update on config.settings):"
  echo "                                                 Atlas rejects clusterManager for database users, and custom roles cannot"
  echo "                                                 grant write actions on the config database. Without this flag Eon skips"
  echo "                                                 balancer control — restores still work, just slower on sharded clusters."
  echo "  --oidc-gcp-issuer-uri <URI>                    (Optional) Issuer URI of the GCP Atlas Workload Identity"
  echo "                                                 Provider (https://accounts.google.com). The script resolves the IdP ID"
  echo "                                                 from your organization's federation settings. Requires --oidc-gcp-principal."
  echo "  --oidc-gcp-idp-id <IDP_ID>                     (Optional) Explicit IdP ID; overrides the issuer URI lookup"
  echo "                                                 (multiple IdPs with the same issuer, or no federation read access)."
  echo "  --oidc-gcp-principal <UNIQUE_ID>               (Optional) Numeric unique ID of Eon's GCP service account"
  echo "                                                 (not its email), provided by Eon."
  echo "  --create-oidc-gcp-idp                          (Optional) Create the GCP Workload Identity Provider"
  echo "                                                 (reusing one with the same issuer URI and audience). Connect it to the"
  echo "                                                 organization afterwards in the Atlas UI. Requires --oidc-gcp-issuer-uri,"
  echo "                                                 --oidc-gcp-principal, --oidc-gcp-audience and Atlas Organization Owner."
  echo "  --oidc-gcp-audience <AUDIENCE>                 (Optional) Audience of the GCP Workload Identity Provider,"
  echo "                                                 provided by Eon. Narrows the issuer URI lookup to IdPs with this audience"
  echo "                                                 (every GCP IdP shares Google's issuer); --create-oidc-gcp-idp uses it too."
  echo "  --oidc-azure-issuer-uri <URI>                  (Optional) Issuer URI of the Azure Atlas Workload Identity"
  echo "                                                 Provider (provided by Eon). The script resolves the IdP ID from your"
  echo "                                                 organization's federation settings. Requires --oidc-azure-principal."
  echo "  --oidc-azure-idp-id <IDP_ID>                   (Optional) Explicit IdP ID; overrides the issuer URI lookup"
  echo "                                                 (multiple IdPs with the same issuer, or no federation read access)."
  echo "  --oidc-azure-principal <OBJECT_ID>             (Optional) Object ID of the service principal of Eon's"
  echo "                                                 Azure scanning data access app (not its client ID), provided by Eon."
  echo "  --verbose                                      Print verbose logs."
  echo "  --help                                         Show help."
  echo ""
  echo "Network access:"
  echo "  Eon connects to your clusters over the public internet, so the IP access list of each onboarded"
  echo "  project must include Eon's static IPs. The Atlas IP access list is project-wide: it applies to"
  echo "  clusters on every cloud provider (AWS, Azure and GCP), including multi-cloud clusters."
  echo "  Pass --eon-static-ips to add the entries, or add them yourself (Atlas UI > Network Access)."
  echo ""
  echo "Workload Identity Federation (optional, Source only):"
  echo "  Eon's Azure and GCP runners authenticate to Atlas with OIDC workload identity. Without the"
  echo "  --oidc-* options nothing OIDC-related is created. To enable it:"
  echo "  1. An Atlas Organization Owner adds one Workload Identity Provider per cloud in your Atlas"
  echo "     organization (Organization > Federation Management > Identity Providers > Workload Identity,"
  echo "     protocol OIDC) and connects it to the organization that contains the onboarded projects:"
  echo "       Issuer URI           GCP: https://accounts.google.com    Azure: <EON_AZURE_OIDC_ISSUER_URI>"
  echo "       Audience             GCP: <EON_GCP_OIDC_AUDIENCE>        Azure: <EON_AZURE_OIDC_AUDIENCE>"
  echo "       Authorization Type   User ID"
  echo "       User Claim           sub (default, do not change)"
  echo "     For GCP, --create-oidc-gcp-idp --oidc-gcp-audience <EON_GCP_OIDC_AUDIENCE> creates the IdP;"
  echo "     connecting it to the organization stays a manual step in the Atlas UI."
  echo "     The script finds the IdP automatically by its issuer URI (and --oidc-gcp-audience); pass --oidc-gcp-idp-id or"
  echo "     --oidc-azure-idp-id only to override the lookup (e.g. two IdPs sharing one issuer)."
  echo "  2. Run this script with the --oidc-* options. The principal values are:"
  echo "       --oidc-gcp-principal     <EON_GCP_SERVICE_ACCOUNT_UNIQUE_ID>  (numeric unique ID, not the email)"
  echo "       --oidc-azure-principal   <EON_AZURE_DATA_ACCESS_APP_OBJECT_ID>  (object ID, not the client ID)"
  echo "     The script creates a database user <IdP ID>/<principal> (readAnyDatabase + clusterMonitor, like"
  echo "     the AWS IAM user) in every onboarded project."
  echo "  3. Eon's Azure and GCP runners have no static egress IPs (GCP runners use automatically assigned"
  echo "     public IPs). Either allow public access to each onboarded project (IP access list 0.0.0.0/0)"
  echo "     or set up private link (private endpoint) to it."
  echo "  Eon publishes the <EON_*> values at: <TBD: location not decided yet; ask Eon support until then>."
  echo "  Requires dedicated (M10+) clusters running MongoDB 7.0.11 or later."
  echo "  See https://www.mongodb.com/docs/atlas/workload-oidc/"
  echo ""
  echo "Authentication:"
  echo "  A browser window will open for you to log in with your MongoDB Atlas credentials."
  echo "  No API keys need to be created or passed as arguments."
  echo ""
  log_info "Example (Source - org-scoped):"
  echo "  $0 --account-type Source \\"
  echo "     --scanning-node-role-arn arn:aws:iam::123456789012:role/EonScanningNodeRole \\"
  echo "     --eon-account-id 6b2e5764-a189-43ae-a173-8ad44f6bf160"
  echo ""
  log_info "Example (Restore - project-scoped, with faster sharded restores):"
  echo "  $0 --account-type Restore \\"
  echo "     --project-id 60b8e1234567890abcdef345 \\"
  echo "     --restore-node-role-arn arn:aws:iam::123456789012:role/EonRestoreNodeRole \\"
  echo "     --eon-account-id 6b2e5764-a189-43ae-a173-8ad44f6bf160 \\"
  echo "     --grant-balancer-control"
}

parse_arguments() {
  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      --account-type)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          ACCOUNT_TYPE=$(echo "$2" | tr '[:upper:]' '[:lower:]')
          shift 2
        else
          log_error "Error: --account-type requires a value (Source|Restore)."
          show_help
          exit 1
        fi
        ;;
      --project-id)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          PROJECT_ID="$2"
          shift 2
        else
          log_error "Error: --project-id requires a value."
          show_help
          exit 1
        fi
        ;;
      --org-id)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          ORG_ID="$2"
          shift 2
        else
          log_error "Error: --org-id requires a value."
          show_help
          exit 1
        fi
        ;;
      --scanning-node-role-arn)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          SCANNING_NODE_ROLE_ARN="$2"
          shift 2
        else
          log_error "Error: --scanning-node-role-arn requires a value."
          show_help
          exit 1
        fi
        ;;
      --restore-node-role-arn)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          RESTORE_NODE_ROLE_ARN="$2"
          shift 2
        else
          log_error "Error: --restore-node-role-arn requires a value."
          show_help
          exit 1
        fi
        ;;
      --eon-account-id)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          EON_ACCOUNT_ID="$2"
          shift 2
        else
          log_error "Error: --eon-account-id requires a value."
          show_help
          exit 1
        fi
        ;;
      --eon-static-ips)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          EON_STATIC_IPS="$2"
          shift 2
        else
          log_error "Error: --eon-static-ips requires a value (comma-separated CIDRs)."
          show_help
          exit 1
        fi
        ;;
      --oidc-gcp-idp-id)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_GCP_IDP_ID="$2"
          shift 2
        else
          log_error "Error: --oidc-gcp-idp-id requires a value."
          show_help
          exit 1
        fi
        ;;
      --oidc-gcp-issuer-uri)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_GCP_ISSUER_URI="$2"
          shift 2
        else
          log_error "Error: --oidc-gcp-issuer-uri requires a value."
          show_help
          exit 1
        fi
        ;;
      --oidc-gcp-principal)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_GCP_PRINCIPAL="$2"
          shift 2
        else
          log_error "Error: --oidc-gcp-principal requires a value."
          show_help
          exit 1
        fi
        ;;
      --create-oidc-gcp-idp)
        OIDC_GCP_CREATE_IDP=true
        shift
        ;;
      --oidc-gcp-audience)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_GCP_AUDIENCE="$2"
          shift 2
        else
          log_error "Error: --oidc-gcp-audience requires a value."
          show_help
          exit 1
        fi
        ;;
      --oidc-azure-idp-id)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_AZURE_IDP_ID="$2"
          shift 2
        else
          log_error "Error: --oidc-azure-idp-id requires a value."
          show_help
          exit 1
        fi
        ;;
      --oidc-azure-issuer-uri)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_AZURE_ISSUER_URI="$2"
          shift 2
        else
          log_error "Error: --oidc-azure-issuer-uri requires a value."
          show_help
          exit 1
        fi
        ;;
      --oidc-azure-principal)
        if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
          OIDC_AZURE_PRINCIPAL="$2"
          shift 2
        else
          log_error "Error: --oidc-azure-principal requires a value."
          show_help
          exit 1
        fi
        ;;
      --grant-balancer-control)
        GRANT_BALANCER_CONTROL=true
        shift
        ;;
      --verbose)
        VERBOSE=true
        shift
        ;;
      --help)
        show_help
        exit 0
        ;;
      *)
        log_error "Unknown option: $1"
        show_help
        exit 1
        ;;
    esac
  done
}

validate_arguments() {
  local has_errors=false

  if [[ -z "$ACCOUNT_TYPE" ]]; then
    log_error "--account-type is required (Source|Restore)."
    has_errors=true
  elif [[ "$ACCOUNT_TYPE" != "source" && "$ACCOUNT_TYPE" != "restore" ]]; then
    log_error "--account-type must be 'Source' or 'Restore'."
    has_errors=true
  fi

  if [[ -n "$PROJECT_ID" && ! "$PROJECT_ID" =~ ^[0-9a-f]{24}$ ]]; then
    log_error "--project-id must be a 24-character hex string."
    has_errors=true
  fi

  if [[ -n "$ORG_ID" && ! "$ORG_ID" =~ ^[0-9a-f]{24}$ ]]; then
    log_error "--org-id must be a 24-character hex string."
    has_errors=true
  fi

  if [[ "$ACCOUNT_TYPE" == "restore" && -z "$PROJECT_ID" ]]; then
    log_error "--project-id is required for restore accounts."
    has_errors=true
  fi

  if [[ "$ACCOUNT_TYPE" == "source" ]]; then
    if [[ -z "$SCANNING_NODE_ROLE_ARN" ]]; then
      log_error "--scanning-node-role-arn is required for source accounts."
      has_errors=true
    elif [[ ! "$SCANNING_NODE_ROLE_ARN" =~ ^arn:aws:iam::[0-9]{12}:role/.+$ ]]; then
      log_error "--scanning-node-role-arn must be a valid AWS IAM role ARN."
      has_errors=true
    fi
  fi

  if [[ "$ACCOUNT_TYPE" == "restore" ]]; then
    if [[ -z "$RESTORE_NODE_ROLE_ARN" ]]; then
      log_error "--restore-node-role-arn is required for restore accounts."
      has_errors=true
    elif [[ ! "$RESTORE_NODE_ROLE_ARN" =~ ^arn:aws:iam::[0-9]{12}:role/.+$ ]]; then
      log_error "--restore-node-role-arn must be a valid AWS IAM role ARN."
      has_errors=true
    fi
  fi

  if [[ -n "$OIDC_GCP_IDP_ID" || -n "$OIDC_GCP_PRINCIPAL" || -n "$OIDC_GCP_ISSUER_URI" ]]; then
    if [[ -z "$OIDC_GCP_IDP_ID" && -z "$OIDC_GCP_ISSUER_URI" ]]; then
      log_error "--oidc-gcp-principal requires --oidc-gcp-issuer-uri (or --oidc-gcp-idp-id)."
      has_errors=true
    fi
    if [[ -z "$OIDC_GCP_PRINCIPAL" ]]; then
      log_error "--oidc-gcp-issuer-uri and --oidc-gcp-idp-id require --oidc-gcp-principal."
      has_errors=true
    elif [[ ! "$OIDC_GCP_PRINCIPAL" =~ ^[0-9]+$ ]]; then
      log_error "--oidc-gcp-principal must be the numeric unique ID of the GCP service account (not its email)."
      has_errors=true
    fi
    if [[ -n "$OIDC_GCP_IDP_ID" && ! "$OIDC_GCP_IDP_ID" =~ ^[0-9a-f]{24}$ ]]; then
      log_error "--oidc-gcp-idp-id must be a 24-character hex string."
      has_errors=true
    fi
    if [[ -n "$OIDC_GCP_ISSUER_URI" && ! "$OIDC_GCP_ISSUER_URI" =~ ^https:// ]]; then
      log_error "--oidc-gcp-issuer-uri must be an https:// issuer URI."
      has_errors=true
    fi
  fi

  if [[ "$OIDC_GCP_CREATE_IDP" = true ]]; then
    if [[ -z "$OIDC_GCP_ISSUER_URI" || -z "$OIDC_GCP_PRINCIPAL" || -z "$OIDC_GCP_AUDIENCE" ]]; then
      log_error "--create-oidc-gcp-idp requires --oidc-gcp-issuer-uri, --oidc-gcp-principal and --oidc-gcp-audience."
      has_errors=true
    fi
    if [[ -n "$OIDC_GCP_IDP_ID" ]]; then
      log_error "--create-oidc-gcp-idp and --oidc-gcp-idp-id are mutually exclusive."
      has_errors=true
    fi
  elif [[ -n "$OIDC_GCP_AUDIENCE" && -z "$OIDC_GCP_ISSUER_URI" ]]; then
    log_error "--oidc-gcp-audience requires --oidc-gcp-issuer-uri."
    has_errors=true
  fi

  if [[ -n "$OIDC_AZURE_IDP_ID" || -n "$OIDC_AZURE_PRINCIPAL" || -n "$OIDC_AZURE_ISSUER_URI" ]]; then
    if [[ -z "$OIDC_AZURE_IDP_ID" && -z "$OIDC_AZURE_ISSUER_URI" ]]; then
      log_error "--oidc-azure-principal requires --oidc-azure-issuer-uri (or --oidc-azure-idp-id)."
      has_errors=true
    fi
    if [[ -z "$OIDC_AZURE_PRINCIPAL" ]]; then
      log_error "--oidc-azure-issuer-uri and --oidc-azure-idp-id require --oidc-azure-principal."
      has_errors=true
    elif [[ ! "$OIDC_AZURE_PRINCIPAL" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]; then
      log_error "--oidc-azure-principal must be the object ID GUID of the Azure data access app service principal (not its client ID)."
      has_errors=true
    fi
    if [[ -n "$OIDC_AZURE_IDP_ID" && ! "$OIDC_AZURE_IDP_ID" =~ ^[0-9a-f]{24}$ ]]; then
      log_error "--oidc-azure-idp-id must be a 24-character hex string."
      has_errors=true
    fi
    if [[ -n "$OIDC_AZURE_ISSUER_URI" && ! "$OIDC_AZURE_ISSUER_URI" =~ ^https:// ]]; then
      log_error "--oidc-azure-issuer-uri must be an https:// issuer URI."
      has_errors=true
    fi
  fi

  if [[ -z "$EON_ACCOUNT_ID" ]]; then
    log_error "--eon-account-id is required."
    has_errors=true
  elif [[ ! "$EON_ACCOUNT_ID" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
    log_error "--eon-account-id must be a valid UUID."
    has_errors=true
  fi

  if [ "$has_errors" = true ]; then
    echo ""
    show_help
    exit 1
  fi
}

# Authenticate via OAuth 2.0 Device Authorization Grant (same flow as Atlas CLI)
# Usage: atlas_device_auth <version>
atlas_device_auth() {
  local version="$1"
  log_info "Authenticating with MongoDB Atlas..."
  echo ""

  local response
  response=$(curl --silent --show-error -X POST \
    "${ATLAS_BASE}/api/private/unauth/account/device/authorize" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -A "Eon-Atlas-Onboarding/${version}" \
    -d "client_id=${ATLAS_OAUTH_CLIENT_ID}&scope=openid+profile+offline_access")

  local user_code device_code verification_uri interval
  user_code=$(echo "$response" | jq -r '.user_code')
  device_code=$(echo "$response" | jq -r '.device_code')
  verification_uri=$(echo "$response" | jq -r '.verification_uri')
  interval=$(echo "$response" | jq -r '.interval // 5')

  if [[ -z "$user_code" || "$user_code" == "null" ]]; then
    log_error "Failed to initiate device authorization. Response: $response"
    exit 1
  fi

  echo -e "${blue}To authenticate, open the following URL in your browser:${end}"
  echo ""
  echo -e "  ${green}${verification_uri}${end}"
  echo ""
  echo -e "${blue}And enter the code:${end}  ${green}${user_code}${end}"
  echo ""

  # Attempt to open the browser automatically
  if command -v open &>/dev/null; then
    open "$verification_uri" 2>/dev/null || true
  elif command -v xdg-open &>/dev/null; then
    xdg-open "$verification_uri" 2>/dev/null || true
  fi

  log_info "Waiting for browser authentication..."

  while true; do
    sleep "$interval"

    local token_response
    token_response=$(curl --silent --show-error -X POST \
      "${ATLAS_BASE}/api/private/unauth/account/device/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -A "Eon-Atlas-Onboarding/${version}" \
      -d "client_id=${ATLAS_OAUTH_CLIENT_ID}&device_code=${device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code")

    ATLAS_ACCESS_TOKEN=$(echo "$token_response" | jq -r '.access_token // empty')
    if [[ -n "$ATLAS_ACCESS_TOKEN" ]]; then
      break
    fi

    local error_code
    error_code=$(echo "$token_response" | jq -r '.errorCode // empty')
    if [[ -n "$error_code" && "$error_code" != "DEVICE_AUTHORIZATION_PENDING" ]]; then
      log_error "Authentication failed: $error_code"
      exit 1
    fi
  done

  log_success "Authenticated successfully."
  echo ""
}

# Make an Atlas API call with Bearer token
# Args: method url [data] [api-version]  (default version: 2023-02-01)
atlas_api() {
  local method="$1"
  local url="$2"
  local data="$3"
  local api_version="${4:-2023-02-01}"

  local curl_args=(
    --silent
    --show-error
    --fail
    --header "Authorization: Bearer ${ATLAS_ACCESS_TOKEN}"
    --header "Content-Type: application/json"
    --header "Accept: application/vnd.atlas.${api_version}+json"
    -X "$method"
  )

  if [[ -n "$data" ]]; then
    curl_args+=(--data "$data")
  fi

  curl "${curl_args[@]}" "$url"
}

# Make an Atlas API call that tolerates 409 (already exists) by falling back to PATCH
# Args: post_url patch_url data
atlas_api_create_or_update() {
  local post_url="$1"
  local patch_url="$2"
  local data="$3"

  local http_code
  local response

  # Try POST first
  response=$(curl \
    --silent \
    --show-error \
    --header "Authorization: Bearer ${ATLAS_ACCESS_TOKEN}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/vnd.atlas.2023-02-01+json" \
    -X POST \
    --data "$data" \
    -w "\n%{http_code}" \
    "$post_url") || true

  http_code=$(echo "$response" | tail -n1)
  local body
  body=$(echo "$response" | sed '$d')

  if [[ "$http_code" == "200" || "$http_code" == "201" || "$http_code" == "202" ]]; then
    log_verbose "  Created successfully."
    return 0
  elif [[ "$http_code" == "409" ]]; then
    log_verbose "  Already exists (HTTP 409). Updating via PATCH..."
    atlas_api "PATCH" "$patch_url" "$data" > /dev/null
    log_verbose "  Updated successfully."
    return 0
  else
    log_error "  Unexpected HTTP $http_code: $body"
    return 1
  fi
}

##############################################
# List all projects (with pagination)
##############################################
list_all_projects() {
  log_info "Listing all projects in organization: $ORG_ID..."

  ALL_PROJECTS=()
  local page_num=1
  local include_count="true"

  while true; do
    local url="${ATLAS_API_BASE}/orgs/${ORG_ID}/groups?pageNum=${page_num}&itemsPerPage=100&includeCount=${include_count}"
    local response
    response=$(atlas_api "GET" "$url")

    local results
    results=$(echo "$response" | jq -r '.results[] | .id')

    if [[ -z "$results" ]]; then
      break
    fi

    while IFS= read -r project_id; do
      ALL_PROJECTS+=("$project_id")
    done <<< "$results"

    local total_count
    total_count=$(echo "$response" | jq -r '.totalCount // 0')
    local fetched=${#ALL_PROJECTS[@]}

    if [[ "$fetched" -ge "$total_count" ]]; then
      break
    fi

    page_num=$((page_num + 1))
    include_count="false"
  done

  log_success "Found ${#ALL_PROJECTS[@]} project(s)."
}

##############################################
# List clusters per project (logging)
##############################################
list_clusters_per_project() {
  local project_id="$1"

  log_verbose "  Listing clusters in project: $project_id"

  local response
  response=$(atlas_api "GET" "${ATLAS_API_BASE}/groups/${project_id}/clusters")

  local cluster_names
  cluster_names=$(echo "$response" | jq -r '.results[]? | .name' 2>/dev/null || echo "")

  if [[ -n "$cluster_names" ]]; then
    while IFS= read -r name; do
      log_verbose "    Cluster: $name"
    done <<< "$cluster_names"
  else
    log_verbose "    No clusters found."
  fi
}

##############################################
# Create source database user (readAnyDatabase + clusterMonitor)
##############################################
create_source_database_user() {
  local project_id="$1"

  log_info "Creating source database user in project: $project_id (username: $SCANNING_NODE_ROLE_ARN)"

  local payload
  payload=$(jq -c -n \
    --arg username "$SCANNING_NODE_ROLE_ARN" \
    --arg eon_account_id "$EON_ACCOUNT_ID" \
    --arg version "$EON_MONGO_ATLAS_SOURCE_VERSION" \
    '{
      databaseName: "$external",
      username: $username,
      awsIAMType: "ROLE",
      roles: [
        {roleName: "readAnyDatabase", databaseName: "admin"},
        {roleName: "clusterMonitor",  databaseName: "admin"}
      ],
      labels: [
        {key: "managed-by", value: "eon"},
        {key: "eon-account-id", value: $eon_account_id},
        {key: "eon-version", value: $version},
        {key: "eon-method", value: "bash"}
      ]
    }')

  local post_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers"
  local encoded_arn
  encoded_arn=$(urlencode "$SCANNING_NODE_ROLE_ARN")
  local patch_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers/\$external/${encoded_arn}"

  atlas_api_create_or_update "$post_url" "$patch_url" "$payload"
}

##############################################
# Optional: database users authenticated with Atlas Workload Identity
# Federation (OIDC), one per configured Eon workload identity (GCP/Azure),
# with the scan roles on source accounts and the restore roles on restore.
##############################################
create_oidc_database_user() {
  local project_id="$1"
  local idp_id="$2"
  local principal="$3"

  # Atlas names Workload Identity Federation users "<IdP ID>/<user claim value>".
  local username="${idp_id}/${principal}"
  log_info "Creating $ACCOUNT_TYPE OIDC database user in project: $project_id (username: $username)"

  local version="$EON_MONGO_ATLAS_SOURCE_VERSION"
  local roles='[{"roleName": "readAnyDatabase", "databaseName": "admin"}, {"roleName": "clusterMonitor", "databaseName": "admin"}]'
  if [[ "$ACCOUNT_TYPE" == "restore" ]]; then
    version="$EON_MONGO_ATLAS_RESTORE_VERSION"
    roles=$(jq -c -n --argjson grant_balancer_control "$GRANT_BALANCER_CONTROL" '[
      {roleName: "readWriteAnyDatabase", databaseName: "admin"},
      {roleName: "enableSharding", databaseName: "admin"},
      {roleName: "clusterMonitor", databaseName: "admin"}
    ] + (if $grant_balancer_control then [{roleName: "atlasAdmin", databaseName: "admin"}] else [] end)')
  fi

  local payload
  payload=$(jq -c -n \
    --arg username "$username" \
    --arg eon_account_id "$EON_ACCOUNT_ID" \
    --arg version "$version" \
    --argjson roles "$roles" \
    '{
      databaseName: "$external",
      username: $username,
      oidcAuthType: "USER",
      roles: $roles,
      labels: [
        {key: "managed-by", value: "eon"},
        {key: "eon-account-id", value: $eon_account_id},
        {key: "eon-version", value: $version},
        {key: "eon-method", value: "bash"}
      ]
    }')

  local post_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers"
  local encoded_username
  encoded_username=$(urlencode "$username")
  local patch_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers/\$external/${encoded_username}"

  atlas_api_create_or_update "$post_url" "$patch_url" "$payload"
}

create_oidc_database_users() {
  local project_id="$1"

  if [[ -n "$OIDC_GCP_IDP_ID" ]]; then
    create_oidc_database_user "$project_id" "$OIDC_GCP_IDP_ID" "$OIDC_GCP_PRINCIPAL"
  fi
  if [[ -n "$OIDC_AZURE_IDP_ID" ]]; then
    create_oidc_database_user "$project_id" "$OIDC_AZURE_IDP_ID" "$OIDC_AZURE_PRINCIPAL"
  fi
}

##############################################
# Resolve Atlas Workload Identity Provider IDs from the organization's
# federation settings, matching each requested issuer URI. Runs after
# resolve_org_id; skipped for flags already given explicitly. With
# --create-oidc-gcp-idp, a missing GCP IdP is created and connected to the
# organization instead of failing the lookup. Reading the federation settings
# requires Atlas Organization Owner (or an org API key with federation read
# access); creating and connecting an IdP requires Organization Owner.
##############################################
resolve_oidc_idp_ids() {
  local need_gcp=false need_azure=false
  if [[ -n "$OIDC_GCP_ISSUER_URI" && -z "$OIDC_GCP_IDP_ID" ]]; then
    need_gcp=true
  fi
  if [[ -n "$OIDC_AZURE_ISSUER_URI" && -z "$OIDC_AZURE_IDP_ID" ]]; then
    need_azure=true
  fi
  if [[ "$need_gcp" = false && "$need_azure" = false ]]; then
    return 0
  fi

  log_info "Resolving Atlas Workload Identity Provider IDs from federation settings..."

  local fs_response fs_id
  fs_response=$(atlas_api "GET" "${ATLAS_API_BASE}/orgs/${ORG_ID}/federationSettings" "" "2025-03-12")
  fs_id=$(echo "$fs_response" | jq -r '.id // empty')
  if [[ -z "$fs_id" ]]; then
    log_error "Could not read federation settings for organization $ORG_ID."
    log_error "Resolving IdP IDs requires Atlas Organization Owner (or an org API key with federation"
    log_error "  read access). Alternatively pass the IDs explicitly via --oidc-*-idp-id."
    exit 1
  fi

  local page_num=1
  local include_count="true"
  local all_idp_results="[]"

  while true; do
    local idp_page
    idp_page=$(atlas_api "GET" "${ATLAS_API_BASE}/federationSettings/${fs_id}/identityProviders?protocol=OIDC&idpType=WORKLOAD&pageNum=${page_num}&itemsPerPage=500&includeCount=${include_count}" "" "2025-03-12")

    local page_results
    page_results=$(echo "$idp_page" | jq -c '.results // []')
    all_idp_results=$(jq -cn --argjson acc "$all_idp_results" --argjson page "$page_results" '$acc + $page')

    local total_count
    total_count=$(echo "$idp_page" | jq -r '.totalCount // 0')
    local fetched
    fetched=$(echo "$all_idp_results" | jq 'length')

    if [[ "$fetched" -ge "$total_count" ]] || [[ $(echo "$page_results" | jq 'length') -eq 0 ]]; then
      break
    fi

    page_num=$((page_num + 1))
    include_count="false"
  done

  local idp_response
  idp_response=$(jq -cn --argjson results "$all_idp_results" '{results: $results}')

  # Every GCP IdP shares Google's issuer, and only one with Eon's audience accepts Eon's tokens.
  local gcp_idp_response="$idp_response"
  if [[ "$need_gcp" = true && -n "$OIDC_GCP_AUDIENCE" ]]; then
    gcp_idp_response=$(echo "$idp_response" | jq -c --arg audience "$OIDC_GCP_AUDIENCE" \
      '{results: [.results[]? | select(.audience == $audience)]}')
  fi

  if [[ "$need_gcp" = true && "$OIDC_GCP_CREATE_IDP" = true ]]; then
    local gcp_match_count
    gcp_match_count=$(echo "$gcp_idp_response" | jq --arg issuer "$OIDC_GCP_ISSUER_URI" \
      '[.results[] | select(.issuerUri == $issuer)] | length')
    if [[ "$gcp_match_count" -eq 0 ]]; then
      OIDC_GCP_IDP_ID=$(create_oidc_gcp_idp "$fs_id")
      log_success "Created GCP Workload Identity Provider: $OIDC_GCP_IDP_ID"
    else
      OIDC_GCP_IDP_ID=$(pick_oidc_idp_id "$gcp_idp_response" "$OIDC_GCP_ISSUER_URI" "--oidc-gcp-idp-id" "$OIDC_GCP_AUDIENCE")
      log_success "Reusing GCP Workload Identity Provider with issuer URI $OIDC_GCP_ISSUER_URI and audience $OIDC_GCP_AUDIENCE: $OIDC_GCP_IDP_ID"
    fi
    log_warn "Connect Workload Identity Provider $OIDC_GCP_IDP_ID to organization $ORG_ID if it isn't already"
    log_warn "  (Organization > Federation Management > Organizations > the organization > Workload Identity)."
  elif [[ "$need_gcp" = true ]]; then
    OIDC_GCP_IDP_ID=$(pick_oidc_idp_id "$gcp_idp_response" "$OIDC_GCP_ISSUER_URI" "--oidc-gcp-idp-id" "$OIDC_GCP_AUDIENCE")
    log_success "Resolved GCP Workload Identity Provider ID: $OIDC_GCP_IDP_ID"
  fi
  if [[ "$need_azure" = true ]]; then
    OIDC_AZURE_IDP_ID=$(pick_oidc_idp_id "$idp_response" "$OIDC_AZURE_ISSUER_URI" "--oidc-azure-idp-id")
    log_success "Resolved Azure Workload Identity Provider ID: $OIDC_AZURE_IDP_ID"
  fi
}

# Creates the OIDC Workload Identity Provider for Eon's GCP scanning service
# account and prints its ID.
create_oidc_gcp_idp() {
  local fs_id="$1"

  local payload
  payload=$(jq -cn \
    --arg name "Eon GCP (${EON_ACCOUNT_ID})" \
    --arg issuer "$OIDC_GCP_ISSUER_URI" \
    --arg audience "$OIDC_GCP_AUDIENCE" \
    '{
      displayName: $name,
      description: "Eon GCP scanning service account workload identity, created by the Eon onboarding script",
      protocol: "OIDC",
      idpType: "WORKLOAD",
      issuerUri: $issuer,
      audience: $audience,
      authorizationType: "USER",
      userClaim: "sub"
    }')

  local response idp_id
  response=$(atlas_api "POST" "${ATLAS_API_BASE}/federationSettings/${fs_id}/identityProviders" "$payload" "2023-11-15")
  idp_id=$(echo "$response" | jq -r '.idpId // .id // empty')
  if [[ -z "$idp_id" ]]; then
    log_error "Atlas did not return an ID for the created GCP Workload Identity Provider."
    exit 1
  fi
  echo "$idp_id"
}

# Prints the IdP ID whose issuerUri matches, from a listIdentityProviders
# response. Exits with a clear message on zero or multiple matches.
pick_oidc_idp_id() {
  local response="$1"
  local issuer_uri="$2"
  local flag_name="$3"
  local audience="${4:-}"

  local match_desc="issuer URI \"$issuer_uri\""
  if [[ -n "$audience" ]]; then
    match_desc="$match_desc and audience \"$audience\""
  fi

  local match_count
  match_count=$(echo "$response" | jq --arg issuer "$issuer_uri" \
    '[.results[]? | select(.issuerUri == $issuer) | (.idpId // .id)] | length')

  if [[ "$match_count" -eq 0 ]]; then
    log_error "No Atlas Workload Identity Provider found for $match_desc."
    log_error "  Create one (Organization > Federation Management > Identity Providers > Workload"
    log_error "  Identity, protocol OIDC) with that issuer URI and the audience shown in the Eon"
    log_error "  console, or pass $flag_name to specify the IdP explicitly."
    exit 1
  elif [[ "$match_count" -gt 1 ]]; then
    log_error "Found $match_count Workload Identity Providers with $match_desc."
    log_error "  Pass $flag_name to pick one explicitly (its ID is shown in the Atlas UI)."
    exit 1
  fi

  echo "$response" | jq -r --arg issuer "$issuer_uri" \
    '[.results[]? | select(.issuerUri == $issuer) | (.idpId // .id)] | .[0]'
}

##############################################
# Create restore database user (readWriteAnyDatabase + enableSharding, plus
# atlasAdmin when --grant-balancer-control is passed — see that flag's help
# text for why balancer control requires atlasAdmin).
##############################################
create_restore_database_user() {
  local project_id="$1"

  log_info "Creating restore database user in project: $project_id (username: $RESTORE_NODE_ROLE_ARN)"

  local payload
  payload=$(jq -c -n \
    --arg username "$RESTORE_NODE_ROLE_ARN" \
    --arg eon_account_id "$EON_ACCOUNT_ID" \
    --arg version "$EON_MONGO_ATLAS_RESTORE_VERSION" \
    --argjson grant_balancer_control "$GRANT_BALANCER_CONTROL" \
    '{
      databaseName: "$external",
      username: $username,
      awsIAMType: "ROLE",
      roles: ([
        {roleName: "readWriteAnyDatabase", databaseName: "admin"},
        {roleName: "enableSharding", databaseName: "admin"}
      ] + (if $grant_balancer_control then [{roleName: "atlasAdmin", databaseName: "admin"}] else [] end)),
      labels: [
        {key: "managed-by", value: "eon"},
        {key: "eon-account-id", value: $eon_account_id},
        {key: "eon-version", value: $version},
        {key: "eon-method", value: "bash"}
      ]
    }')

  local post_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers"
  local encoded_arn
  encoded_arn=$(urlencode "$RESTORE_NODE_ROLE_ARN")
  local patch_url="${ATLAS_API_BASE}/groups/${project_id}/databaseUsers/\$external/${encoded_arn}"

  atlas_api_create_or_update "$post_url" "$patch_url" "$payload"
}

##############################################
# Create IP access list entries for a project
##############################################
create_ip_access_list_entries() {
  local project_id="$1"

  if [[ -z "$EON_STATIC_IPS" ]]; then
    log_verbose "  No static IPs provided, skipping IP access list for project: $project_id"
    return 0
  fi

  log_info "Creating IP access list entries in project: $project_id"

  IFS=',' read -ra IP_ARRAY <<< "$EON_STATIC_IPS"
  for cidr in "${IP_ARRAY[@]}"; do
    cidr=$(echo "$cidr" | xargs)  # trim whitespace
    if [[ -z "$cidr" ]]; then
      continue
    fi
    # Ensure CIDR notation (append /32 to bare IPs)
    if [[ "$cidr" != */* ]]; then
      cidr="${cidr}/32"
    fi

    log_info "  Adding IP access list entry: $cidr"

    local payload
    payload=$(jq -c -n \
      --arg cidr "$cidr" \
      --arg comment "Eon control plane (account: ${EON_ACCOUNT_ID})" \
      '[{cidrBlock: $cidr, comment: $comment}]')

    local http_code
    local response
    response=$(curl \
      --silent \
      --show-error \
      --header "Authorization: Bearer ${ATLAS_ACCESS_TOKEN}" \
      --header "Content-Type: application/json" \
      --header "Accept: application/vnd.atlas.2023-02-01+json" \
      -X POST \
      --data "$payload" \
      -w "\n%{http_code}" \
      "${ATLAS_API_BASE}/groups/${project_id}/accessList") || true

    http_code=$(echo "$response" | tail -n1)
    local body
    body=$(echo "$response" | sed '$d')

    if [[ "$http_code" == "201" || "$http_code" == "200" ]]; then
      log_verbose "    Added successfully."
    elif [[ "$http_code" == "409" ]]; then
      log_verbose "    Already exists (HTTP 409), skipping."
    else
      log_error "    Failed to add IP access list entry $cidr (HTTP $http_code): $body"
      return 1
    fi
  done

  log_success "IP access list entries created for project: $project_id"
}

##############################################
# Resolve the organization ID
# Uses --org-id if provided, otherwise auto-detects.
# Fails if multiple orgs are found and --org-id was not provided.
##############################################
resolve_org_id() {
  if [[ -n "$ORG_ID" ]]; then
    log_info "Using provided organization ID: $ORG_ID"
    return 0
  fi

  # If a project ID is provided, resolve org ID by listing all accessible projects
  if [[ -n "$PROJECT_ID" ]]; then
    log_info "Resolving organization ID from project ID: $PROJECT_ID..."

    local page_num=1
    local total_count=0

    while true; do
      local include_count="false"
      if [[ "$page_num" -eq 1 ]]; then
        include_count="true"
      fi

      local url="${ATLAS_API_BASE}/groups?pageNum=${page_num}&itemsPerPage=100&includeCount=${include_count}"
      local response
      response=$(atlas_api "GET" "$url")

      local results
      results=$(echo "$response" | jq -r '.results[]? | .id')

      if [[ -z "$results" ]]; then
        break
      fi

      local org_id_found
      org_id_found=$(echo "$response" | jq -r --arg pid "$PROJECT_ID" '.results[] | select(.id == $pid) | .orgId')

      if [[ -n "$org_id_found" ]]; then
        ORG_ID="$org_id_found"
        log_success "Auto-detected organization ID from project: $ORG_ID"
        return 0
      fi

      if [[ "$page_num" -eq 1 ]]; then
        total_count=$(echo "$response" | jq -r '.totalCount // 0')
      fi

      local fetched=$((page_num * 100))

      if [[ "$fetched" -ge "$total_count" ]]; then
        break
      fi

      page_num=$((page_num + 1))
    done

    log_error "Could not find project $PROJECT_ID in accessible projects. Verify the project ID and your permissions."
    exit 1
  fi

  log_info "Resolving organization ID..."

  local response
  response=$(atlas_api "GET" "${ATLAS_API_BASE}/orgs?itemsPerPage=100&includeCount=true")

  local org_count
  org_count=$(echo "$response" | jq -r '.totalCount // 0')

  if [[ "$org_count" -eq 0 ]]; then
    log_error "No organizations found. Verify your Atlas account has Organization Owner permissions."
    exit 1
  elif [[ "$org_count" -eq 1 ]]; then
    ORG_ID=$(echo "$response" | jq -r '.results[0].id')
    local org_name
    org_name=$(echo "$response" | jq -r '.results[0].name')
    log_success "Auto-detected organization: $org_name (ID: $ORG_ID)"
  else
    log_error "Multiple organizations found ($org_count). Please specify --org-id to select one:"
    echo ""
    echo "$response" | jq -r '.results[] | "  \(.id)  \(.name)"'
    echo ""
    exit 1
  fi
}

##############################################
# Create a dedicated Eon API key
##############################################
create_eon_api_key() {
  log_info "Creating dedicated Eon API key..."

  local description="Eon ${ACCOUNT_TYPE} API key (account: ${EON_ACCOUNT_ID})"

  if [[ "$ACCOUNT_TYPE" == "source" && -z "$PROJECT_ID" ]]; then
    # Org-scoped source: create org-level key with ORG_READ_ONLY (ORG_ID resolved earlier)
    local payload
    payload=$(jq -c -n \
      --arg desc "$description" \
      '{desc: $desc, roles: ["ORG_READ_ONLY"]}')

    local response
    response=$(atlas_api "POST" "${ATLAS_API_BASE}/orgs/${ORG_ID}/apiKeys" "$payload")

    EON_API_PUBLIC_KEY=$(echo "$response" | jq -r '.publicKey')
    EON_API_PRIVATE_KEY=$(echo "$response" | jq -r '.privateKey')
    local api_key_id
    api_key_id=$(echo "$response" | jq -r '.id')

    if [[ -z "$EON_API_PUBLIC_KEY" || "$EON_API_PUBLIC_KEY" == "null" ]]; then
      log_error "Failed to create Eon API key."
      exit 1
    fi

    log_success "Created org-level Eon API key: $EON_API_PUBLIC_KEY"

    # Assign GROUP_READ_ONLY to each project
    for project_id in "${ALL_PROJECTS[@]}"; do
      log_info "  Assigning Eon key to project: $project_id"
      local assign_payload
      assign_payload=$(jq -c -n '[{"roles": ["GROUP_READ_ONLY"]}]')

      local assign_response assign_http_code assign_body
      assign_response=$(curl \
        --silent \
        --show-error \
        --header "Authorization: Bearer ${ATLAS_ACCESS_TOKEN}" \
        --header "Content-Type: application/json" \
        --header "Accept: application/vnd.atlas.2025-03-12+json" \
        -X POST \
        --data "$assign_payload" \
        -w "\n%{http_code}" \
        "${ATLAS_API_BASE}/groups/${project_id}/apiKeys/${api_key_id}") || true

      assign_http_code=$(echo "$assign_response" | tail -n1)
      assign_body=$(echo "$assign_response" | sed '$d')

      if [[ "$assign_http_code" == "200" || "$assign_http_code" == "201" || "$assign_http_code" == "204" ]]; then
        log_verbose "    Assigned successfully."
      else
        log_error "    Failed to assign key to project $project_id (HTTP $assign_http_code): $assign_body"
        return 1
      fi
    done

    log_success "Assigned Eon key to ${#ALL_PROJECTS[@]} project(s)."
  else
    # Project-scoped (source or restore): create project-level key
    local role_names
    if [[ "$ACCOUNT_TYPE" == "source" ]]; then
      role_names='["GROUP_READ_ONLY"]'
    else
      role_names='["GROUP_READ_ONLY", "GROUP_CLUSTER_CREATOR"]'
    fi

    local payload
    payload=$(jq -c -n \
      --arg desc "$description" \
      --argjson roles "$role_names" \
      '{desc: $desc, roles: $roles}')

    local response
    response=$(atlas_api "POST" "${ATLAS_API_BASE}/groups/${PROJECT_ID}/apiKeys" "$payload")

    EON_API_PUBLIC_KEY=$(echo "$response" | jq -r '.publicKey')
    EON_API_PRIVATE_KEY=$(echo "$response" | jq -r '.privateKey')

    if [[ -z "$EON_API_PUBLIC_KEY" || "$EON_API_PUBLIC_KEY" == "null" ]]; then
      log_error "Failed to create Eon API key."
      exit 1
    fi

    log_success "Created project-level Eon API key: $EON_API_PUBLIC_KEY"
  fi
}

##############################################
# Main
##############################################
main() {
  parse_arguments "$@"
  validate_arguments

  # Check dependencies
  for cmd in curl jq; do
    if ! command -v "$cmd" &>/dev/null; then
      log_error "$cmd is required but not installed."
      exit 1
    fi
  done

  local version
  if [[ "$ACCOUNT_TYPE" == "source" ]]; then
    version="$EON_MONGO_ATLAS_SOURCE_VERSION"
  else
    version="$EON_MONGO_ATLAS_RESTORE_VERSION"
  fi

  # Authenticate via browser
  atlas_device_auth "$version"

  local scope="org"
  if [[ -n "$PROJECT_ID" ]]; then
    scope="project"
  fi

  log_info "Starting MongoDB Atlas onboarding (account_type: $ACCOUNT_TYPE, scope: $scope, version: $version)"
  if [[ -n "$PROJECT_ID" ]]; then
    log_info "Project ID: $PROJECT_ID"
  fi
  if [[ "$ACCOUNT_TYPE" == "source" ]]; then
    log_info "Scanning Node Role ARN: $SCANNING_NODE_ROLE_ARN"
  else
    log_info "Restore Node Role ARN: $RESTORE_NODE_ROLE_ARN"
  fi
  log_info "Eon Account ID: $EON_ACCOUNT_ID"

  # Resolve org ID (auto-detects from project ID if provided, or from orgs list)
  resolve_org_id

  # Resolve OIDC IdP IDs by issuer URI (flags override the lookup)
  resolve_oidc_idp_ids

  if [[ "$ACCOUNT_TYPE" == "source" ]]; then
    if [[ -n "$PROJECT_ID" ]]; then
      # Project-scoped: create db user in the single target project
      list_clusters_per_project "$PROJECT_ID"
      create_source_database_user "$PROJECT_ID"
      create_oidc_database_users "$PROJECT_ID"
      create_ip_access_list_entries "$PROJECT_ID"
      echo ""
    else
      # Org-scoped: enumerate all projects in the organization
      list_all_projects
      echo ""

      for project_id in "${ALL_PROJECTS[@]}"; do
        list_clusters_per_project "$project_id"
        create_source_database_user "$project_id"
        create_oidc_database_users "$project_id"
        create_ip_access_list_entries "$project_id"
        echo ""
      done
    fi
  else
    # Restore: create db user in the target project
    list_clusters_per_project "$PROJECT_ID"
    create_restore_database_user "$PROJECT_ID"
    create_oidc_database_users "$PROJECT_ID"
    create_ip_access_list_entries "$PROJECT_ID"
    echo ""
  fi

  # Create a dedicated Eon API key with minimal permissions
  create_eon_api_key
  echo ""

  # Output
  log_success "========================================"
  log_success "MongoDB Atlas onboarding complete!"
  log_success "Account type: $ACCOUNT_TYPE (scope: $scope)"
  log_success "========================================"
  echo ""
  echo -e "${blue}Eon API Key (enter these in the Eon UI to complete the connection):${end}"
  echo -e "  ${green}Public Key:${end}  $EON_API_PUBLIC_KEY"
  echo -e "  ${green}Private Key:${end} $EON_API_PRIVATE_KEY"
  echo ""
  echo -e "${blue}Network access:${end}"
  if [[ -n "$EON_STATIC_IPS" ]]; then
    echo "  Eon's static IPs were added to the IP access list of each onboarded project."
  else
    echo "  No --eon-static-ips were passed: make sure the IP access list of each onboarded project includes Eon's static IPs."
  fi
  echo "  The Atlas IP access list applies to clusters on every cloud provider (AWS, Azure and GCP)."
  if [[ -n "$OIDC_GCP_IDP_ID" || -n "$OIDC_AZURE_IDP_ID" ]]; then
    echo ""
    echo -e "${blue}Workload Identity Federation:${end}"
    echo "  OIDC database users were created for Eon's workload identities in each onboarded project."
  fi
}

main "$@"
