#!/bin/bash

# Copyright UiPath 2021
#
# =================
# LICENSE AGREEMENT
# -----------------
#   Use of paid UiPath products and services is subject to the licensing agreement
#   executed between you and UiPath. Unless otherwise indicated by UiPath, use of free
#   UiPath products is subject to the associated licensing agreement available here:
#   https://www.uipath.com/legal/trust-and-security/legal-terms (or successor website).
#   You must not use this file separately from the product it is a part of or is associated with.

# kill all background processes when the script exits
trap "trap - TERM && { jobs -p | xargs -r kill; exit; }" INT TERM EXIT

set -eu

VERSION="2022.10.1"

# if not already set in env
: "${UNATTENDED_ACTION:="none"}"
: "${INSTALLER_FOLDER:="/opt/UiPathAutomationSuite"}"
: "${CONFIG_PATH:="${INSTALLER_FOLDER}/cluster_config.json"}"

# general config
CONFIG_BUNDLE_URL="https://download.uipath.com/automation-suite/${VERSION}/installer-${VERSION}.zip"

CONFIG_CLUSTER_MULTINODE=false
CONFIG_CLUSTER_AIRGAPPED=false
CONFIG_CLUSTER_FQDN=""
CONFIG_ADMIN_USERNAME="admin"
CONFIG_SQL_SERVER=""
CONFIG_SQL_PORT="1433"
CONFIG_SQL_USERNAME=""
CONFIG_SQL_PASSWORD=""
CONFIG_SQL_CREATE_DB=true
CONFIG_INTEGRATED_AUTH_ENABLED=false
CONFIG_INTEGRATED_AUTH_AD_DOMAIN=""
CONFIG_INTEGRATED_AUTH_AD_USERNAME=""
CONFIG_INTEGRATED_AUTH_USER_KEYTAB=""
CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME="8"
CONFIG_READINESS_VALIDATION=false


# installer specific
BUNDLE_PATH="${INSTALLER_FOLDER}/${VERSION}/installer"
: "${BUNDLE_FILE:="$BUNDLE_PATH.zip"}"
INSTALLER_ENTRYPOINT="${BUNDLE_PATH}/install-uipath.sh"
UIPATHCTL_ENTRYPOINT="${BUNDLE_PATH}/bin/uipathctl"
INSTALLER_LOGFILE="${INSTALLER_FOLDER}/install-$(date +'%Y-%m-%dT%H_%M_%S').log"
INSTALLER_SUMMARY="${INSTALLER_FOLDER}/install-summary-$(date +'%Y-%m-%d').txt"
INSTALLER_OUTPUTFILE="${INSTALLER_FOLDER}/output.json"
BUNDLE_K8S_SECRET="installer-bundle"
SERVICES_ENABLED=()
ADDITIONAL_CA_CERTS_PATH=""

#external storage specific
BUCKET_PREFIX=""
BUCKET_SUFFIX=""
S3_FQDN=""
S3_PORT=443
S3_ACCESSKEY=""
S3_SECRETKEY=""
AZURE_CLIENT_ID=""
S3_REGION="us-east-1"
AZURE_SUFFIX="core.windows.net"
AZURE_ACCOUNTNAME=""
USE_MANAGED_IDENTITY=true
EXTERNAL_STORAGE_TYPE="azure"
AZURE_ACCOUNTKEY=""
EXTERNAL_OBJECT_STORAGE_ENABLED=true
EXTERNAL_STORAGE_CREATE_BUCKET=true

# flags
UPDATE_SQL_TEMPLATE=0
UPDATE_PM_SQL_TEMPLATE=0

# AI Center standalone/external orchestrator configurations
AICENTER_ORCHESTRATOR_URL=""
AICENTER_IDENTITY_URL=""
AICENTER_ORCHESTRATOR_CERT=""
AICENTER_IDENTITY_CERT=""
AICENTER_METERING_KEY="nil"

# Process Mining config values
PROCESSMINING_WH_SQL_SERVER=""
PROCESSMINING_WH_SQL_PORT=1433
PROCESSMINING_WH_SQL_USERNAME=""
PROCESSMINING_WH_SQL_PASSWORD=""
CONFIG_PROCESSMINING_WAREHOUSE_DOTNET_CONNECTION=""
CONFIG_PROCESSMINING_WAREHOUSE_PYODBC_CONNECTION=""

## service_matrix holds an entry for each service where:
# column 1: cluster config json path
# column 2: friendly service name (should be the official service name)
# column 3: global variable name used by this script
declare -A service_matrix
readonly service_fields=3
service_matrix[1,1]="action_center"; service_matrix[1,2]="Action Center"; service_matrix[1,3]="CONFIG_ACTION_CENTER_ENABLED"
service_matrix[2,1]="aicenter"; service_matrix[2,2]="AI Center"; service_matrix[2,3]="CONFIG_AICENTER_ENABLED"
service_matrix[3,1]="apps"; service_matrix[3,2]="Apps"; service_matrix[3,3]="CONFIG_APPS_ENABLED"
service_matrix[4,1]="automation_hub"; service_matrix[4,2]="Automation Hub"; service_matrix[4,3]="CONFIG_AUTOMATION_HUB_ENABLED"
service_matrix[5,1]="automation_ops"; service_matrix[5,2]="Automation Ops"; service_matrix[5,3]="CONFIG_AUTOMATION_OPS_ENABLED"
service_matrix[6,1]="dataservice"; service_matrix[6,2]="Dataservice"; service_matrix[6,3]="CONFIG_DATASERVICE_ENABLED"
service_matrix[7,1]="documentunderstanding"; service_matrix[7,2]="Document Understanding"; service_matrix[7,3]="CONFIG_DU_ENABLED"
service_matrix[8,1]="insights"; service_matrix[8,2]="Insights"; service_matrix[8,3]="CONFIG_INSIGHTS_ENABLED"
service_matrix[9,1]="orchestrator"; service_matrix[9,2]="Orchestrator"; service_matrix[9,3]="CONFIG_ORCHESTRATOR_ENABLED"
service_matrix[10,1]="processmining"; service_matrix[10,2]="Process Mining"; service_matrix[10,3]="CONFIG_PROCESSMINING_ENABLED"
service_matrix[11,1]="asrobots"; service_matrix[11,2]="Automation Suite Robots"; service_matrix[11,3]="CONFIG_ASROBOTS_ENABLED"
service_matrix[12,1]="task_mining"; service_matrix[12,2]="Task Mining"; service_matrix[12,3]="CONFIG_TASK_MINING_ENABLED"
service_matrix[13,1]="test_manager"; service_matrix[13,2]="Test Manager"; service_matrix[13,3]="CONFIG_TEST_MANAGER_ENABLED"
declare -i matrix_length=$(( ${#service_matrix[@]}/service_fields ))

function get_enabled_services() {
    local value
    for((i=1;i<=matrix_length;i++)) do
        value="$(get_config_value ".${service_matrix[$i,1]}.enabled")"
        declare -g "${service_matrix[$i,3]}=${value}"
        [[ "${value}" == "true" ]] && SERVICES_ENABLED+=("${service_matrix[$i,2]}")
    done
    return 0
}

function print_enabled_services() {
  for service in "${SERVICES_ENABLED[@]}";do
    indent; ok "${service}"
  done
}

function select_all_products() {
  for((i=1;i<=matrix_length;i++)) do
      declare -g "${service_matrix[$i,3]}=true"
  done
  declare -g "ALL_PRODUCTS=true"
}

function select_custom_products() {
  local input
  clean_screen
  printf "Select the products you would like to install:\n\n"
  
  for((i=1;i<=matrix_length;i++)) do
      printf "[%i] %s\n" "$i" "${service_matrix[$i,2]}"
  done
  echo 
  while :
  do
      for((i=1;i<=matrix_length;i++)) do
          declare -g "${service_matrix[$i,3]}=false"
      done
      printf "Enter your choices by separating them with a comma (i.e 1,2,3): "
      IFS=',' read -r -a input

      for option in "${input[@]}"; do
          [[ ! -v 'service_matrix[$option,1]' ]] && {
            echo "Invalid choice: ${option}."
            continue 2
          }
          declare -g "${service_matrix[$option,3]}=true"
      done

      if [[ "${CONFIG_AICENTER_ENABLED:-false}" == "true" ]]; then
        set_aicenter_config
      fi
      set_services_json
      cat<<< "{$services_json}" > "${CONFIG_PATH}.services.tmp"
      # 22.10 hack: `uipathctl manifest list-applications` requires a valid input, which is not possible at this point in the interactive flow
      # to go around this, we'll add the required keys in the temporary json file ¯\_(ツ)_/¯
      jq '.+ {sql_connection_string_template: "PLACEHOLDER", sql_connection_string_template_odbc: "PLACEHOLDER", sql_connection_string_template_jdbc: "PLACEHOLDER" }' "${CONFIG_PATH}.services.tmp" > "${CONFIG_PATH}.services.merged" && mv "${CONFIG_PATH}.services.merged" "${CONFIG_PATH}.services.tmp"
      "${UIPATHCTL_ENTRYPOINT}" manifest list-applications "${CONFIG_PATH}.services.tmp" 1>/dev/null || continue
      rm -f "${CONFIG_PATH}.services.tmp"
      echo
      break
  done
  echo

  printf "You selected:\n"
  for option in "${input[@]}"; do
      ENABLED_SERVICES_MASK+=$option
      declare -g "${service_matrix[$option,3]}=true"
      printf ' + %s\n' "${service_matrix[$option,2]}"
  done
  echo

  local default_choice="1"
  printf "Confirm your selection:\n" 
  printf "[1] Yes\n"
  printf "[2] No\n"

  while :
  do
      printf "Enter your choice [%s]: " "${default_choice}"
      read -r input

      case "${input}" in
      1) break;;
      2) select_custom_products; break;;
      "") break;;
      *) printf "\nInvalid choice\n";
      esac
  done
}

function check_prereqs(){
  if [ "${CAN_CURL}" != "true" ] && [ "${CAN_WGET}" != "true" ]; then
    error "The script requires curl or wget to be present and executable."
  fi

  if [ "${CAN_UNZIP}" != "true" ]; then
    error "The script requires unzip to be present and executable."
  fi

  if [ "${CAN_JQ}" != "true" ]; then
    error "The script requires jq to be present and executable."
  fi
}

function generate_random_string(){
  tr -dc 'a-zA-Z0-9' </dev/urandom | fold -w 16 | head -n 1
}

# Output functions
function error() {
  printf "[ERROR] %s\n" "$*"
  [[ -d "${INSTALLER_FOLDER}" ]] && {
    printf "[ERROR] %s\n" "$*" >> "${INSTALLER_LOGFILE}"
    printf "\xE2\x9D\x8C %s\nFor more details check the logfile: ${INSTALLER_LOGFILE}\n" "$*" >&2
  }

  exit 1
}

function warn() {
  printf "\xe2\x9d\x95 %b\n" "$*"
}

function info() {
  printf "[INFO] %b\n" "$*"
}

function ok() {
  printf "\xe2\x9c\x85 %b\n" "$*"
}

function notok() {
  printf "\xE2\x9D\x8C %s\n" "$*"
}

function pending() {
  printf "\xE2\x8F\xB0 %b\n" "$*"
}

function waiting_for_pid() {
  local pid="$1"
  local frames=(".  " ".. " "..." "   ")
  while [ -d "/proc/${pid}" ];do
    for f in "${frames[@]}"
    do
      echo -ne "${f}\r"
      sleep 1s
    done
  done
  wait "${pid}" # used to return the exit code of the process we are waiting for
}

#shellcheck disable=SC2120
function indent(){
  printf "\t"
}

# Menu controls
function clean_screen() {
  printf "\033c"
}

function quit(){
  printf "\n"
  read -rp "Press [Enter] to quit..."
  printf "\nQuitting. Bye!\n"
  exit 0
}

# Menus
function check_eula() {
  clean_screen
  cat << 'EOF'
===============================================================================
                              License Agreement
===============================================================================
Use of paid UiPath products and services is subject to the licensing agreement
executed between you and UiPath. Unless otherwise indicated by UiPath, use of free
UiPath products is subject to the associated licensing agreement available here:
https://www.uipath.com/legal/trust-and-security/legal-terms (or successor website).

EOF

  printf "\nDo you accept the license agreement?\n"
  printf "[1] Yes\n"
  printf "[2] No\n"

  while :
  do
    printf "Enter your choice: "
    read -r input

    case "${input}" in
      1) break;;
      2) exit 0;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function main_menu() {
  while :
  do
  clean_screen
  welcome_message
  cat <<EOF

===============================================================================
                                  Main Menu
===============================================================================
EOF
    printf "[1] Single-node deployment (recommended for demo/evaluation purposes)\n"
    printf "[2] Multi-node deployment (recommended for production use)\n"
    printf "\n"
    printf "Enter your choice (q to quit) [1]: "
    read -r input

    case "${input}" in
      1) single_node; break;;
      2) multi_node; break;;
      [q]*) exit 0;;
      "") single_node;;
      *) printf "\nInvalid choice\n";
        printf "Press [Enter] to continue...\n" ; read -r;;
    esac
  done
}

function single_node(){
  clean_screen
  CONFIG_CLUSTER_MULTINODE=false
  cat <<EOF
===============================================================================
                      Single-node evaluation profile
===============================================================================

We recommend a single-node evaluation profile for evaluation, demo, development or test
purposes only.

EOF

  while :
  do
    printf "[1] Continue installing\n"
    printf "[2] Go back\n"
    printf "Enter your choice [1]: "
    read -r input

    case "${input}" in
      1) install_menu; break;;
      2) main_menu; break;;
      "") install_menu; break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function multi_node(){
  clean_screen
  CONFIG_CLUSTER_MULTINODE=true
  cat <<EOF
===============================================================================
                     Multi-node HA-ready production profile
===============================================================================

We recommend a multi-node HA-ready production profile for production use. Multi-node
HA-ready production profile requires at least three (virtual) machines.

EOF

  while :
  do
    printf "[1] Continue installing\n"
    printf "[2] Go back\n"
    printf "Enter your choice [1]: "
    read -r input

    case "${input}" in
      1) install_menu; break;;
      2) main_menu; break;;
      "") install_menu; break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function print_hw_requirements(){
  clean_screen

  [[ "${ALL_PRODUCTS:-false}" != "true" ]] && { 
    cat <<EOF
===============================================================================
                      Custom product selection
===============================================================================

To determine the minimum hardware and software requirements based on the products you
 selected, please use the capacity planning tool:
https://docs.uipath.com/automation-suite/v2022.10/docs/capacity-planning

EOF
    printf "Press [Enter] to continue...\n" ; read -r;
    return 0
  }

  [[ "${CONFIG_CLUSTER_MULTINODE}" == "true" ]] && {
    cat <<EOF
===============================================================================
                     Multi-node HA-ready production profile
===============================================================================

Minimum hardware requirements for each (virtual) machine in a multi-node HA-ready
production profile are:

  - Processor: 64-bit, 96 (v-)CPU/cores (min 16 per node)
  - RAM: 192 GiB (min 32 per node)
  - Hard disk space: 256 GiB SSD (cluster binaries), 2 TiB SSD (data),
    16 GiB SSD (etcd), 512 GiB SSD (UiPath bundle, offline only)
  - Operating system: RHEL 8.3+
  - Dedicated agent node for Task Mining: 20 (v-)CPU/cores, 60 GiB RAM, 256 GiB SSD disk

Database engine requirements for a single-node deployment are:
- Microsoft SQL Server 2016, 2017, and 2019 Enterprise or Standard

EOF
  }

  [[ "${CONFIG_CLUSTER_MULTINODE}" == "true" ]] || {
    cat <<EOF
===============================================================================
                      Single-node evaluation profile
===============================================================================

Minimum hardware requirements for a single-node evaluation profile are:

  - Processor: 64-bit, 32 (v-)CPU/cores
  - RAM: 64 GiB
  - Hard disk space: 256 GiB SSD (cluster binaries), 2 TiB SSD (data),
    16 GiB SSD (etcd), 512 GiB SSD (UiPath bundle, offline only)
  - Operating system: RHEL 8.2+
  - Dedicated agent node for Task Mining: 20 (v-)CPU/cores, 60 GiB RAM, 256 GiB SSD disk

Database engine requirements for a single-node deployment are:
- Microsoft SQL Server 2016, 2017, and 2019 Enterprise or Standard

EOF
  }

  printf "Press [Enter] to continue...\n" ; read -r;
}

function install_menu() {
  check_install_directory
  [[ -f "${CONFIG_PATH}" ]] || new_config_menu
  if [[ "${CONFIG_READINESS_VALIDATION}" == "false" ]]; then
    interactive_install_steps
    quit
  fi
}

function use_existing_config (){
  printf  "\nFound existent config file at: %s\n" "${CONFIG_PATH}"
  while :
  do
    print_existing_config
    printf "[1] Continue installing with existing config\n"
    printf "[2] Edit the config\n"
    printf "\n"
    printf "For advanced settings, quit now and manually edit the config file.\n"
    printf "Once the configuration file is updated, run the deployment wizard again and follow the instructions.\n"
    printf "Enter your choice (or q to quit) [1]: "
    read -r input

    case "${input}" in

      1) break;;
      2) edit_config_menu; generate_config; clean_screen;;
      [q]*) exit 0;;
      "") break;;
      *) printf "\nInvalid choice\n";
        printf "Press [Enter] to continue...\n" ; read -r;;
    esac
  done

  [[ -r "${CONFIG_PATH}" ]] || error "${CONFIG_PATH} is not readable"
  printf "\n"
  ok "Using existing config: $(basename "${CONFIG_PATH}")"

  print_config_details

  while :
  do
    printf "[1] Continue installing\n"
    printf "[2] Exit and manually update the config file\n"
    printf "Enter your choice [1]: "
    read -r input

    case "${input}" in
      1) install_menu; break;;
      2) exit 0;;
      "") install_menu; break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function new_config_menu() {
  [[ "${CONFIG_READINESS_VALIDATION}" == "true" ]] || install_guide

  [[ "${CONFIG_READINESS_VALIDATION}" == "false" ]] || readiness_guide

  cat <<EOF

===============================================================================
                            Deployment configuration
===============================================================================
EOF

  [[ "${CONFIG_READINESS_VALIDATION}" == "true" ]] || config_set_airgapped
  config_set_bundle
  print_hw_requirements
  config_set_fqdn;
  config_set_external_object_storage;
  config_set_sql_integrated_auth_enabled;
  if [[ ${CONFIG_INTEGRATED_AUTH_ENABLED} == "true" ]]; then
    config_set_sql_integrated_auth_ad_domain
    config_set_sql_integrated_auth_lifetime
    config_set_sql_integrated_auth_ad_username
    config_set_sql_integrated_auth_user_keytab
  fi
  config_set_sql_url;
  config_set_sql_port;
  if [[ ${CONFIG_INTEGRATED_AUTH_ENABLED} == "false" ]]; then
    config_set_sql_username
    config_set_sql_password
  fi
  if [[ "${CONFIG_PROCESSMINING_ENABLED:-false}" == "true" ]]; then
    set_processmining_config
  fi
  config_set_sql_create_db
  config_set_additional_crt
  print_existing_config
  generate_config

  while :
  do
    printf "[1] Continue installing with the default config\n"
    printf "[2] Edit the config\n"
    printf "\n"
    printf "For advanced settings, quit now and manually edit the config file.\n"
    printf "Once the configuration file is updated, run the deployment wizard again and follow the instructions.\n"
    printf "Enter your choice (q to quit) [1]: "
    read -r input

    case "${input}" in

      1) break;;
      2) edit_config_menu; generate_config; clean_screen;;
      [q]*) exit 0;;
      "") break;;
      *) printf "\nInvalid choice\n";
        printf "Press [Enter] to continue...\n" ; read -r;;
    esac
  done

  read_existing_config_file
  print_config_details

  while :
  do
    printf "[1] Continue installing\n"
    printf "[2] Exit\n"
    printf "Enter your choice [1]: "
    read -r input

    case "${input}" in
      1) break;;
      2) exit 0;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function edit_config_menu(){
  local input
  while :
  do
    clean_screen
    cat <<EOF

===============================================================================
                    Choose what setting you want to edit
===============================================================================
EOF
    printf "[1] Multi node: %s\n" "${CONFIG_CLUSTER_MULTINODE}"
    printf "[2] Airgapped: %s\n" "${CONFIG_CLUSTER_AIRGAPPED}"
    printf "[3] Automation Suite FQDN: %s\n" "${CONFIG_CLUSTER_FQDN}"
    printf "[4] Sql server FQDN: %s\n" "${CONFIG_SQL_SERVER}"
    printf "[5] Sql port: %s\n" "${CONFIG_SQL_PORT}"
    printf "[6] Sql username: %s\n" "${CONFIG_SQL_USERNAME}"
    printf "[7] Sql password:***\n"
    printf "[8] Create sql databases: %s\n" "${CONFIG_SQL_CREATE_DB}"
    printf "[9] Kerberos Auth enabled: %s\n" "${CONFIG_INTEGRATED_AUTH_ENABLED}"
    printf "[10] External Object Storage enabled: %s\n" "${EXTERNAL_OBJECT_STORAGE_ENABLED}"
    if [[ "${EXTERNAL_OBJECT_STORAGE_ENABLED}" == "true" ]]; then
      if [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" ]]; then
        printf "[12] Bucket prefix: %s\n" "${BUCKET_PREFIX}"
        printf "[13] Buckets suffix: %s\n" "${BUCKET_SUFFIX}"
        printf "[14] S3 region: %s\n" "${S3_REGION}"
        printf "[15] S3 FQDN: %s\n" "${S3_FQDN}"
        printf "[16] S3 port: %s\n" "${S3_PORT}"
        printf "[17] S3 access key: %s\n" "${S3_ACCESSKEY}"
        printf "[18] S3 secret key: %s\n" "***"
      elif [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
        printf "[19] Use managed identity: %s\n" "${USE_MANAGED_IDENTITY}"
        printf "[20] Azure account name: %s\n" "${AZURE_ACCOUNTNAME}"
        printf "[21] Azure endpoint suffix: %s\n" "${AZURE_SUFFIX}"
        if [[ "${USE_MANAGED_IDENTITY}" == "false" ]]; then
          printf "[22] Azure account key: ***\n"
        else
          printf "[23] Azure client id: %s\n" "${AZURE_CLIENT_ID}"
        fi
      fi
    fi
    printf "[24] Additional CA Certificate file: %s\n" "${ADDITIONAL_CA_CERTS_PATH}"
    if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
      printf "[25] Kerberos Auth Active Directory domain: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}"
      printf "[26] Kerberos Auth TGT lifetime in hours: %s\n" "${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME}"
      printf "[27] Kerberos Auth default Active Directory username: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_USERNAME}"
      printf "[28] Kerberos Auth default user keytab:***\n"
    fi
    printf "\n"
    printf "Warning: Updating any SQL parameter will regenerate the connection string templates in cluster_config.json."
    printf "\n\n"
    printf "Enter your choice (q to quit the config menu) [q]: "
    read -r input

    case "${input}" in
      1) config_set_multinode;;
      2) config_set_airgapped;;
      3) config_set_fqdn;;
      4) config_set_sql_url;;
      5) config_set_sql_port;;
      6) config_set_sql_username;;
      7) config_set_sql_password;;
      8) config_set_sql_create_db;;
      9) config_set_sql_integrated_auth_enabled;;
      10) config_set_external_object_storage;;
      11) config_set_s3_external_storage;;
      12) config_external_storage_bucket_prefix;;
      13) config_external_storage_bucket_suffix;;
      14) config_set_s3_region;;
      15) config_set_s3_fqdn;;
      16) config_set_s3_port;;
      17) config_set_s3_access_key;;
      18) config_set_s3_secret_key;;
      19) config_set_azure_external_storage;;
      20) config_set_azure_account_name;;
      21) config_set_azure_suffix;;
      22) config_set_azure_account_key;;
      23) config_set_azure_client_id;;
      24) config_set_additional_crt;;
      25) config_set_sql_integrated_auth_ad_domain;;
      26) config_set_sql_integrated_auth_lifetime;;
      27) config_set_sql_integrated_auth_ad_username;;
      28) config_set_sql_integrated_auth_user_keytab;;
      [q]*) break;;
      "") break;;
      *) printf "\nInvalid choice\n";
        printf "Press [Enter] to continue...\n" ; read -r;;
    esac
  done
}

function welcome_message() {
  cat <<'EOF'
                                          _   ___      _   _
                                    /\ /\(_) / _ \__ _| |_| |__
                                   / / \ \ |/ /_)/ _` | __| '_ \
                                   \ \_/ / / ___/ (_| | |_| | | |
                                    \___/|_\/    \__,_|\__|_| |_|
              _         _                        _   _               __       _ _
             /_\  _   _| |_ ___  _ __ ___   __ _| |_(_) ___  _ __   / _\_   _(_) |_ ___
            //_\\| | | | __/ _ \| '_ ` _ \ / _` | __| |/ _ \| '_ \  \ \| | | | | __/ _ \
           /  _  \ |_| | || (_) | | | | | | (_| | |_| | (_) | | | | _\ \ |_| | | ||  __/
           \_/ \_/\__,_|\__\___/|_| |_| |_|\__,_|\__|_|\___/|_| |_| \__/\__,_|_|\__\___|
EOF

cat <<EOF

                                      version: ${VERSION}

Welcome to the UiPath Automation Suite deployment wizard.

The wizard will guide you through the deployment of the UiPath Automation Suite which includes:
- Action Center
- AI Center
- Apps
- Automation Hub
- Automation Ops
- Data Service
- Document Understanding
- Insights
- Orchestrator
- Automation Suite Robots
- Task Mining
- Process Mining
- Test Suite

Before installing, please review the minimum software and hardware requirements.

To continue the deployment choose one of the following:

EOF
}

# Functions used to generate the default config
function set_services_json() {
  read -r -d '' services_json << EOF || true
  "platform": {
    "enabled": ${CONFIG_ORCHESTRATOR_ENABLED:-true}
  },
  "orchestrator": {
    "enabled": ${CONFIG_ORCHESTRATOR_ENABLED:-true}, 
    "testautomation": {
      "enabled": ${CONFIG_ORCHESTRATOR_TESTAUTOMATION_ENABLED:-true}
    },
    "updateserver": {
      "enabled": ${CONFIG_ORCHESTRATOR_UPDATESERVER_ENABLED:-true}
    }
  },
  "action_center": {
    "enabled": ${CONFIG_ACTION_CENTER_ENABLED:-true}
  },
  "test_manager": {
    "enabled": ${CONFIG_TEST_MANAGER_ENABLED:-true}
  },
  "insights": {
    "enabled": ${CONFIG_INSIGHTS_ENABLED:-true}
  },
  "automation_hub": {
    "enabled": ${CONFIG_AUTOMATION_HUB_ENABLED:-true}
  },
  "automation_ops": {
    "enabled": ${CONFIG_AUTOMATION_OPS_ENABLED:-true}
  },
  "processmining": {
    "enabled": ${CONFIG_PROCESSMINING_ENABLED:-false},
    "warehouse": {
        "sql_connection_str": "${CONFIG_PROCESSMINING_WAREHOUSE_DOTNET_CONNECTION:-""}",
        "sqlalchemy_pyodbc_sql_connection_str": "${CONFIG_PROCESSMINING_WAREHOUSE_PYODBC_CONNECTION:-""}"
    }
  },
  "aicenter": {
    "enabled": ${CONFIG_AICENTER_ENABLED:-false},
    "orchestrator_url": "${AICENTER_ORCHESTRATOR_URL:-""}",
    "identity_server_url": "${AICENTER_IDENTITY_URL:-""}",
    "orchestrator_cert_file_path": "${AICENTER_ORCHESTRATOR_CERT:-""}",
    "identity_cert_file_path": "${AICENTER_IDENTITY_CERT:-""}",
    "metering_api_key": "${AICENTER_METERING_KEY:-""}"
  },
  "apps": {
    "enabled": ${CONFIG_APPS_ENABLED:-false}
  },
  "documentunderstanding": {
    "enabled": ${CONFIG_DU_ENABLED:-false}
  },
  "dataservice": {
    "enabled": ${CONFIG_DATASERVICE_ENABLED:-true}
  },
  "asrobots": {
    "enabled": ${CONFIG_ASROBOTS_ENABLED:-false},
    "packagecaching": ${CONFIG_ASROBOTS_PACKAGE_CACHING_ENABLED:-true},
    "packagecachefolder": "${CONFIG_ASROBOTS_PACKAGE_CACHE_FOLDER:-"/uipath_asrobots_package_cache"}"
  },
  "task_mining": {
    "enabled": ${CONFIG_TASK_MINING_ENABLED:-false}
  }
EOF
}

function generate_config(){
  local escaped_sql_password
  local escaped_odbc_jdbc_password
  local escaped_dotnet_password

  # escape backslash and double quotes to generate valid JSON
  escaped_sql_password=$( echo "${CONFIG_SQL_PASSWORD}" | sed -r 's/\\/\\\\/g' | sed -r 's/\o042+/\\\o042/g' )

  escaped_odbc_jdbc_password=$(escape_jdbc_odbc_sql_password "${escaped_sql_password}")
  escaped_dotnet_password=$(escape_dotnet_sql_password "${escaped_sql_password}")
  escaped_sql_servername=$(escape_sql_servername "${CONFIG_SQL_SERVER}")

  local urlencoded_pyodbc_username
  local urlencoded_pyodbc_password
  urlencoded_pyodbc_username=$(urlencoded_pyodbc_string "${CONFIG_SQL_USERNAME}")
  urlencoded_pyodbc_password=$(urlencoded_pyodbc_string "${CONFIG_SQL_PASSWORD}")

  local telemetry_optout_default
  telemetry_optout_default=$([[ "${CONFIG_CLUSTER_AIRGAPPED}" == "true" ]] && echo true || echo false)
  # Enable the SQL TrustServerCertificate for single node setups
  local dotnet_cert_trust="True"
  local jdbc_cert_trust="true"
  local odbc_cert_trust="YES"
  local sqlalchemy_pyodbc_cert_trust="YES"

  # Disable the SQL TrustServerCertificate for multinode setups
  [[ "${CONFIG_CLUSTER_MULTINODE}" == "true" ]] && {
    local dotnet_cert_trust="False"
    local jdbc_cert_trust="false"
    local odbc_cert_trust="NO"
    local sqlalchemy_pyodbc_cert_trust="NO"
  }

  if [[ "$CONFIG_SQL_SERVER" == *"\\"* ]]; then
    JDBC_SQL_SERVER=$(echo "$escaped_sql_servername" | cut -d "\\" -f 1)
    JDBC_INSTANCE_NAME=";instanceName=$(echo "$CONFIG_SQL_SERVER" | cut -d "\\" -f 2)"
  else
    JDBC_SQL_SERVER="$CONFIG_SQL_SERVER"
    JDBC_INSTANCE_NAME=""
  fi

  local sql_dotnet_template="Server=tcp:${escaped_sql_servername},${CONFIG_SQL_PORT};Initial Catalog=DB_NAME_PLACEHOLDER;Persist Security Info=False;User Id=${CONFIG_SQL_USERNAME};Password='${escaped_dotnet_password}';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=${dotnet_cert_trust};Connection Timeout=30;Max Pool Size=100;"
  local sql_jdbc_template="jdbc:sqlserver://${JDBC_SQL_SERVER}:${CONFIG_SQL_PORT};database=DB_NAME_PLACEHOLDER;user=${CONFIG_SQL_USERNAME};password={${escaped_odbc_jdbc_password}};encrypt=true;trustServerCertificate=${jdbc_cert_trust};Connection Timeout=30;hostNameInCertificate=${JDBC_SQL_SERVER}${JDBC_INSTANCE_NAME}"
  local sql_odbc_template="SERVER=${escaped_sql_servername},${CONFIG_SQL_PORT};DATABASE=DB_NAME_PLACEHOLDER;DRIVER={ODBC Driver 17 for SQL Server};UID=${CONFIG_SQL_USERNAME};PWD={${escaped_odbc_jdbc_password}};MultipleActiveResultSets=False;Encrypt=YES;TrustServerCertificate=${odbc_cert_trust};Connection Timeout=30;"
  local sql_sqlalchemy_pyodbc_template="mssql+pyodbc://${urlencoded_pyodbc_username}:${urlencoded_pyodbc_password}@${CONFIG_SQL_SERVER}:${CONFIG_SQL_PORT}/DB_NAME_PLACEHOLDER?driver=ODBC+Driver+17+for+SQL+Server&TrustServerCertificate=${sqlalchemy_pyodbc_cert_trust}&Encrypt=YES"

  if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
    sql_dotnet_template="Server=tcp:${escaped_sql_servername},${CONFIG_SQL_PORT};Initial Catalog=DB_NAME_PLACEHOLDER;Persist Security Info=False;Integrated Security=true;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=${dotnet_cert_trust};Connection Timeout=30;Max Pool Size=100;"
    sql_jdbc_template="jdbc:sqlserver://${JDBC_SQL_SERVER}:${CONFIG_SQL_PORT};database=DB_NAME_PLACEHOLDER;integratedSecurity=true;authenticationScheme=JavaKerberos;Connection Timeout=30;encrypt=true;trustServerCertificate=${jdbc_cert_trust};Connection Timeout=30;hostNameInCertificate=${JDBC_SQL_SERVER}${JDBC_INSTANCE_NAME}"
    sql_odbc_template="SERVER=${escaped_sql_servername},${CONFIG_SQL_PORT};DATABASE=DB_NAME_PLACEHOLDER;DRIVER={ODBC Driver 17 for SQL Server};Trusted_Connection=yes;MultipleActiveResultSets=False;Encrypt=YES;TrustServerCertificate=${odbc_cert_trust};Connection Timeout=30;"
    sql_sqlalchemy_pyodbc_template="mssql+pyodbc://${urlencoded_pyodbc_username}:${urlencoded_pyodbc_password}@${CONFIG_SQL_SERVER}:${CONFIG_SQL_PORT}/DB_NAME_PLACEHOLDER?driver=ODBC+Driver+17+for+SQL+Server&TrustServerCertificate=${sqlalchemy_pyodbc_cert_trust}&Encrypt=YES&Trusted_Connection=yes"
  fi

  # Do not override templates if SQL values were not altered
  if [[ "${UPDATE_SQL_TEMPLATE}" == 0 ]]; then
    sql_dotnet_template="${CONFIG_SQL_NET_TEMPLATE}"
    sql_jdbc_template="${CONFIG_SQL_JDBC_TEMPLATE}"
    sql_odbc_template="${CONFIG_SQL_ODBC_TEMPLATE}"
    sql_sqlalchemy_pyodbc_template="${CONFIG_SQL_SQLALCHEMY_PYODBC_TEMPLATE}"
  fi

  if [[ "${CONFIG_PROCESSMINING_ENABLED:-false}" == "true" && "${UPDATE_PM_SQL_TEMPLATE}" != 0 ]]; then  
    local urlencoded_wh_pyodbc_username
    local urlencoded_wh_pyodbc_password
    urlencoded_wh_pyodbc_username=$(urlencoded_pyodbc_string "${PROCESSMINING_WH_SQL_USERNAME}")
    urlencoded_wh_pyodbc_password=$(urlencoded_pyodbc_string "${PROCESSMINING_WH_SQL_PASSWORD}")
    escaped_wh_sql_password=$( echo "${PROCESSMINING_WH_SQL_PASSWORD}" | sed -r 's/\\/\\\\/g' | sed -r 's/\o042+/\\\o042/g' )
    escaped_wh_dotnet_password=$(escape_dotnet_sql_password "${escaped_wh_sql_password}")
    escaped_wh_servername=$(escape_sql_servername "${PROCESSMINING_WH_SQL_SERVER}")

    if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
      CONFIG_PROCESSMINING_WAREHOUSE_DOTNET_CONNECTION="Server=tcp:${escaped_wh_servername},${PROCESSMINING_WH_SQL_PORT};Initial Catalog=DB_NAME_PLACEHOLDER;Persist Security Info=False;Integrated Security=true;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=${dotnet_cert_trust};Connection Timeout=30;Max Pool Size=100;"
      CONFIG_PROCESSMINING_WAREHOUSE_PYODBC_CONNECTION="mssql+pyodbc://${urlencoded_wh_pyodbc_username}:${urlencoded_wh_pyodbc_password}@${PROCESSMINING_WH_SQL_SERVER}:${PROCESSMINING_WH_SQL_PORT}/DB_NAME_PLACEHOLDER?driver=ODBC+Driver+17+for+SQL+Server&TrustServerCertificate=${sqlalchemy_pyodbc_cert_trust}&Encrypt=YES&Trusted_Connection=yes"
    else
      CONFIG_PROCESSMINING_WAREHOUSE_DOTNET_CONNECTION="Server=tcp:${escaped_wh_servername},${PROCESSMINING_WH_SQL_PORT};Initial Catalog=DB_NAME_PLACEHOLDER;Persist Security Info=False;User Id=${PROCESSMINING_WH_SQL_USERNAME};Password='${escaped_wh_dotnet_password}';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=${dotnet_cert_trust};Connection Timeout=30;Max Pool Size=100;"
      CONFIG_PROCESSMINING_WAREHOUSE_PYODBC_CONNECTION="mssql+pyodbc://${urlencoded_wh_pyodbc_username}:${urlencoded_wh_pyodbc_password}@${PROCESSMINING_WH_SQL_SERVER}:${PROCESSMINING_WH_SQL_PORT}/DB_NAME_PLACEHOLDER?driver=ODBC+Driver+17+for+SQL+Server&TrustServerCertificate=${sqlalchemy_pyodbc_cert_trust}&Encrypt=YES"
    fi
  fi

  set_services_json
  [[ -f "${CONFIG_PATH}" ]] || echo '{}' >  "${CONFIG_PATH}"
  cat<<EOF > "${CONFIG_PATH}.tmp"
{
  "fqdn": "${CONFIG_CLUSTER_FQDN}",
  "fixed_rke_address": "${CONFIG_CLUSTER_FQDN}",
  "airgapped": ${CONFIG_CLUSTER_AIRGAPPED:-false},
  "profile": "$([[ ${CONFIG_CLUSTER_MULTINODE} == "true" ]] && echo "ha" || echo "default")",
  "rke_token": "${CONFIG_RKE_TOKEN:-"$(generate_random_string)"}",
  "admin_username": "${CONFIG_ADMIN_USERNAME:-"admin"}",
  "admin_password": "${CONFIG_ADMIN_PASSWORD:-"$(generate_random_string)"}",
  "infra": {
    "docker_registry": {
      "username": "${CONFIG_DOCKER_USERNAME:-"admin"}",
      "password": "${CONFIG_DOCKER_PASSWORD:-"$(generate_random_string)"}"
    }
  },
  "sql": {
    "server_url": "${escaped_sql_servername}",
    "username": "${CONFIG_SQL_USERNAME}",
    "password": "${escaped_sql_password}",
    "port": "${CONFIG_SQL_PORT:-"1433"}",
    "create_db": ${CONFIG_SQL_CREATE_DB:-"true"}
  },
  "kerberos_auth_config": {
    "enabled": ${CONFIG_INTEGRATED_AUTH_ENABLED:-false},
    "ticket_lifetime_in_hour": "${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME:-"8"}",
    "ad_domain": "${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}",
    "default_ad_username": "${CONFIG_INTEGRATED_AUTH_AD_USERNAME}",
    "default_user_keytab": "${CONFIG_INTEGRATED_AUTH_USER_KEYTAB}"
  },
  "sql_connection_string_template": "${sql_dotnet_template}",
  "sql_connection_string_template_jdbc": "${sql_jdbc_template}",
  "sql_connection_string_template_odbc": "${sql_odbc_template}",
  "sql_connection_string_template_sqlalchemy_pyodbc": "${sql_sqlalchemy_pyodbc_template}",
  "telemetry_optout": ${CONFIG_TELEMETRY_OPTOUT:-$telemetry_optout_default},
  "fabric": {
    "redis":{
      "ha": ${CONFIG_REDIS_HA:-false}
    }
  },
  "additional_ca_certs": "${ADDITIONAL_CA_CERTS_PATH}",
  "proxy": {
    "enabled": ${CONFIG_PROXY_ENABLED:-false}
  },
  ${services_json}
}
EOF

  add_external_storage_info_to_config

  jq -s '.[0] * .[1]' "${CONFIG_PATH}" "${CONFIG_PATH}.tmp" > "${CONFIG_PATH}.merged"
  rm -f "${CONFIG_PATH}" "${CONFIG_PATH}.tmp"
  mv "${CONFIG_PATH}.merged" "${CONFIG_PATH}"

  printf "\n"
  ok "The cluster configuration file was generated at ${CONFIG_PATH}:"
  printf "\n"

  if [[ "${CONFIG_READINESS_VALIDATION}" == "false" && "${CONFIG_CLUSTER_AIRGAPPED}" == "true" ]];then
    cat <<EOF

The interactive installer is not able to finish the air-gapped setup yet.
Until this feature is available you can continue with the steps described here:
https://docs.uipath.com/automation-suite/docs/offline-single-node-installation or
https://docs.uipath.com/automation-suite/docs/offline-multi-node-installation

EOF
    quit
  fi

  return 0
}

function add_external_storage_info_to_config() {
  config="$(cat ${CONFIG_PATH})"
  config=$(jq "del(.external_object_storage)" <<< "$config")
  config=$(jq ".external_object_storage.enabled=${EXTERNAL_OBJECT_STORAGE_ENABLED}" <<< "$config")

  if [[ "${EXTERNAL_OBJECT_STORAGE_ENABLED}" == "true" ]]; then
    config=$(jq ".external_object_storage.create_bucket=${EXTERNAL_STORAGE_CREATE_BUCKET}" <<< "$config")
    [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" || "${EXTERNAL_STORAGE_TYPE}" == "s3_compatible" ]] && config=$(jq ".external_object_storage.storage_type=\"s3\"" <<< "$config") || config=$(jq ".external_object_storage.storage_type=\"azure\"" <<< "$config")
    if [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" ]]; then
      config=$(set_config_for_s3 "${config}")
    elif [[ "${EXTERNAL_STORAGE_TYPE}" == "s3_compatible" ]]; then
      config=$(set_config_for_s3_compatible "${config}")
    elif [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
      config=$(set_config_for_azure "${config}")
    fi
  fi

  echo "${config}" > "${CONFIG_PATH}"
}

function set_config_for_s3() {
  local config="$1"
  S3_PORT=443
  config=$(jq ".external_object_storage.region=\"${S3_REGION}\"" <<< "$config")
  config=$(jq ".external_object_storage.bucket_name_prefix=\"${BUCKET_PREFIX}\"" <<< "$config")
  config=$(jq ".external_object_storage.bucket_name_suffix=\"${BUCKET_SUFFIX}\"" <<< "$config")
  config=$(jq ".external_object_storage.access_key=\"${S3_ACCESSKEY}\"" <<< "$config")
  config=$(jq ".external_object_storage.secret_key=\"${S3_SECRETKEY}\"" <<< "$config")
  config=$(jq ".external_object_storage.port=${S3_PORT}" <<< "$config")
  config=$(jq ".external_object_storage.fqdn=\"${S3_FQDN}\"" <<< "$config")

  echo "${config}"
}

function set_config_for_azure() {
  local config="$1"
  config=$(jq ".external_object_storage.use_managed_identity=${USE_MANAGED_IDENTITY}" <<< "$config")
  config=$(jq ".external_object_storage.account_name=\"${AZURE_ACCOUNTNAME}\"" <<< "$config")
  config=$(jq ".external_object_storage.azure_fqdn_suffix=\"${AZURE_SUFFIX}\"" <<< "$config")
  if [[ "${USE_MANAGED_IDENTITY}" == "false" ]]; then
    config=$(jq ".external_object_storage.account_key=\"${AZURE_ACCOUNTKEY}\"" <<< "$config")
  elif [[ "${USE_MANAGED_IDENTITY}" == "true" && -n "${AZURE_CLIENT_ID}" ]]; then
    config=$(jq ".external_object_storage.client_id=\"${AZURE_CLIENT_ID}\"" <<< "$config")
  fi

  echo "${config}"
}

function set_config_for_s3_compatible() {
  local config="$1"
  S3_REGION="us-east-1"
  config=$(jq ".external_object_storage.region=\"${S3_REGION}\"" <<< "$config")
  config=$(jq ".external_object_storage.access_key=\"${S3_ACCESSKEY}\"" <<< "$config")
  config=$(jq ".external_object_storage.secret_key=\"${S3_SECRETKEY}\"" <<< "$config")
  config=$(jq ".external_object_storage.port=${S3_PORT}" <<< "$config")
  config=$(jq ".external_object_storage.fqdn=\"${S3_FQDN}\"" <<< "$config")
  config=$(jq ".external_object_storage.bucket_name_prefix=\"${BUCKET_PREFIX}\"" <<< "$config")
  config=$(jq ".external_object_storage.bucket_name_suffix=\"${BUCKET_SUFFIX}\"" <<< "$config")

  echo "${config}"
}

function get_config_value(){
    local json_path="$1"
    local value

    value="$(jq -r "${json_path}" < "${CONFIG_PATH}")"
    if [[ ${value} != null ]]; then
      echo "${value}"
    else
      echo ""
    fi
}

function get_external_object_storage_params() {
  EXTERNAL_OBJECT_STORAGE_ENABLED=$(jq -r '.external_object_storage.enabled' "${CONFIG_PATH}")
  EXTERNAL_STORAGE_CREATE_BUCKET=$(jq -r '.external_object_storage.create_bucket' "${CONFIG_PATH}")
  EXTERNAL_STORAGE_TYPE="$(jq -r '.external_object_storage.storage_type // ""' "${CONFIG_PATH}")"
  if [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
    AZURE_ACCOUNTKEY="$(jq -r '.external_object_storage.account_key // ""' "${CONFIG_PATH}")"
    AZURE_CLIENT_ID="$(jq -r '.external_object_storage.client_id // ""' "${CONFIG_PATH}")"
    AZURE_ACCOUNTNAME="$(jq -r '.external_object_storage.account_name // ""' "${CONFIG_PATH}")"
    USE_MANAGED_IDENTITY=$(jq -r '.external_object_storage.use_managed_identity' "${CONFIG_PATH}")
    AZURE_SUFFIX="$(jq -r '.external_object_storage.azure_fqdn_suffix // ""' "${CONFIG_PATH}")"
  else
    S3_FQDN="$(jq -r '.external_object_storage.fqdn // ""' "${CONFIG_PATH}")"
    S3_PORT=$(jq -r '.external_object_storage.port' "${CONFIG_PATH}")
    S3_ACCESSKEY="$(jq -r '.external_object_storage.access_key // ""' "${CONFIG_PATH}")"
    S3_SECRETKEY="$(jq -r '.external_object_storage.secret_key // ""' "${CONFIG_PATH}")"
    S3_REGION="$(jq -r '.external_object_storage.region // ""' "${CONFIG_PATH}")"
    BUCKET_PREFIX="$(jq -r '.external_object_storage.bucket_name_prefix // ""' "${CONFIG_PATH}")"
    BUCKET_SUFFIX="$(jq -r '.external_object_storage.bucket_name_suffix // ""' "${CONFIG_PATH}")"
  fi
}

function read_existing_config_file(){
  if [[ -f "${CONFIG_PATH}" ]]; then
    CONFIG_RKE_TOKEN="$(jq -r '.rke_token // ""' "${CONFIG_PATH}")"
    profile="$(jq -r '.profile // ""' "${CONFIG_PATH}")"
    if [[ "${profile}" == "ha" ]];
    then
      CONFIG_CLUSTER_MULTINODE=true
    else
      CONFIG_CLUSTER_MULTINODE=false
    fi
    CONFIG_CLUSTER_AIRGAPPED="$(jq -r '.airgapped' "${CONFIG_PATH}")"
    CONFIG_CLUSTER_FQDN="$(jq -r '.fqdn // ""' "${CONFIG_PATH}")"
    CONFIG_ADMIN_USERNAME="$(jq -r '.admin_username // ""' "${CONFIG_PATH}")"
    CONFIG_ADMIN_PASSWORD="$(jq -r '.admin_password // ""' "${CONFIG_PATH}")"
    CONFIG_DOCKER_USERNAME="$(jq -r '.infra.docker_registry.username // ""' "${CONFIG_PATH}")"
    CONFIG_DOCKER_PASSWORD="$(jq -r '.infra.docker_registry.password // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_SERVER="$(jq -r '.sql.server_url // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_PORT="$(jq -r '.sql.port // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_USERNAME="$(jq -r '.sql.username // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_PASSWORD="$(jq -r '.sql.password // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_CREATE_DB="$(jq -r '.sql.create_db // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_NET_TEMPLATE="$(jq -r '.sql_connection_string_template // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_JDBC_TEMPLATE="$(jq -r '.sql_connection_string_template_jdbc // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_ODBC_TEMPLATE="$(jq -r '.sql_connection_string_template_odbc // ""' "${CONFIG_PATH}")"
    CONFIG_SQL_SQLALCHEMY_PYODBC_TEMPLATE="$(jq -r '.sql_connection_string_template_sqlalchemy_pyodbc // ""' "${CONFIG_PATH}")"
    CONFIG_TELEMETRY_OPTOUT="$(jq -r '.telemetry_optout' "${CONFIG_PATH}")"
    CONFIG_REDIS_HA="$(jq -r '.fabric.redis.ha' "${CONFIG_PATH}")"
    CONFIG_PROXY_ENABLED="$(jq -r '.proxy.enabled' "${CONFIG_PATH}")"
    CONFIG_INTEGRATED_AUTH_ENABLED="$(jq -r '.kerberos_auth_config.enabled' "${CONFIG_PATH}")"
    CONFIG_INTEGRATED_AUTH_AD_DOMAIN="$(jq -r '.kerberos_auth_config.ad_domain // ""' "${CONFIG_PATH}")"
    CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME="$(jq -r '.kerberos_auth_config.ticket_lifetime_in_hour // ""' "${CONFIG_PATH}")"
    CONFIG_INTEGRATED_AUTH_AD_USERNAME="$(jq -r '.kerberos_auth_config.default_ad_username // ""' "${CONFIG_PATH}")"
    CONFIG_INTEGRATED_AUTH_USER_KEYTAB="$(jq -r '.kerberos_auth_config.default_user_keytab // ""' "${CONFIG_PATH}")"
    CONFIG_ORCHESTRATOR_UPDATESERVER_ENABLED="$(jq -r '.orchestrator.updateserver.enabled' "${CONFIG_PATH}")"
    CONFIG_ORCHESTRATOR_TESTAUTOMATION_ENABLED="$(jq -r '.orchestrator.testautomation.enabled' "${CONFIG_PATH}")"
    CONFIG_ASROBOTS_PACKAGE_CACHING_ENABLED="$(jq -r '.asrobots.packagecaching' "${CONFIG_PATH}")"
    CONFIG_ASROBOTS_PACKAGE_CACHE_FOLDER="$(jq -r '.asrobots.packagecachefolder // ""' "${CONFIG_PATH}")"
    ADDITIONAL_CA_CERTS_PATH="$(jq -r '.additional_ca_certs // ""' "${CONFIG_PATH}")"
    AICENTER_ORCHESTRATOR_URL="$(jq -r '.aicenter.orchestrator_url // ""' "${CONFIG_PATH}")"
    AICENTER_IDENTITY_URL="$(jq -r '.aicenter.identity_server_url // ""' "${CONFIG_PATH}")"
    AICENTER_ORCHESTRATOR_CERT="$(jq -r '.aicenter.orchestrator_cert_file_path // ""' "${CONFIG_PATH}")"
    AICENTER_IDENTITY_CERT="$(jq -r '.aicenter.identity_cert_file_path // ""' "${CONFIG_PATH}")"
    AICENTER_METERING_KEY="$(jq -r '.aicenter.metering_api_key // ""' "${CONFIG_PATH}")"
    CONFIG_PROCESSMINING_WAREHOUSE_DOTNET_CONNECTION="$(jq -r '.processmining.warehouse.sql_connection_str // ""' "${CONFIG_PATH}")"
    CONFIG_PROCESSMINING_WAREHOUSE_PYODBC_CONNECTION="$(jq -r '.processmining.warehouse.sqlalchemy_pyodbc_sql_connection_str // ""' "${CONFIG_PATH}")"

    get_enabled_services
    get_external_object_storage_params
  fi
}

function print_existing_config(){
  cat <<EOF

===============================================================================
                            Current config values
===============================================================================
EOF
  printf "Multi node: %s\n" "${CONFIG_CLUSTER_MULTINODE}"
  printf "Airgapped: %s\n" "${CONFIG_CLUSTER_AIRGAPPED}"
  printf "Automation Suite FQDN: %s\n" "${CONFIG_CLUSTER_FQDN}"
  printf "Sql server FQDN: %s\n" "${CONFIG_SQL_SERVER}"
  printf "Sql port: %s\n" "${CONFIG_SQL_PORT}"
  printf "Sql username: %s\n" "${CONFIG_SQL_USERNAME}"
  printf "Sql password: ***\n"
  printf "Create sql databases: %s\n" "${CONFIG_SQL_CREATE_DB}"
  printf "Kerberos Auth enabled: %s\n" "${CONFIG_INTEGRATED_AUTH_ENABLED}"
  if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
    printf "Kerberos Auth Active Directory domain: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}"
    printf "Kerberos Auth TGT lifetime in hours: %s\n" "${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME}"
    printf "Kerberos Auth default Active Directory username: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_USERNAME}"
    printf "Kerberos Auth default user keytab: ***\n"
  fi
  
  printf "File path containing the additional CA Certificate: %s\n" "${ADDITIONAL_CA_CERTS_PATH}"
  printf "External object storage enabled: %s\n" "${EXTERNAL_OBJECT_STORAGE_ENABLED}"

  if [[ "${EXTERNAL_OBJECT_STORAGE_ENABLED}" == "true" ]]; then
    printf "Create storage buckets: %s\n" "${EXTERNAL_STORAGE_CREATE_BUCKET}"
    if [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" || "${EXTERNAL_STORAGE_TYPE}" == "s3_compatible" ]]; then
      printf "Storage type: s3\n"
      [[ -n "${BUCKET_PREFIX}" ]] && printf "Buckets prefix: %s\n" "${BUCKET_PREFIX}"
      [[ -n "${BUCKET_SUFFIX}" ]] && printf "Buckets suffix: %s\n" "${BUCKET_SUFFIX}"
      printf "Buckets region: %s\n" "${S3_REGION}"
      printf "S3 access key: %s\n" "${S3_ACCESSKEY}"
      printf "S3 secret key: ***\n"
      printf "S3 fqdn: %s\n" "${S3_FQDN}"
      printf "S3 port: %s\n" "${S3_PORT}"        
    elif [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
      printf "Storage type: azure\n"
      printf "Use managed identity based authentication: %s\n" "${USE_MANAGED_IDENTITY}"
      printf "Azure account name: %s\n" "${AZURE_ACCOUNTNAME}"
      printf "Azure fqdn suffix: %s\n" "${AZURE_SUFFIX}"
      if [[ "${USE_MANAGED_IDENTITY}" == "true" ]]; then
        if [[ -n "${AZURE_CLIENT_ID}" ]]; then
          printf "Azure client id: %s\n" "${AZURE_CLIENT_ID}"
        fi
      else
        printf "Azure account key: ***\n"
      fi
    fi
  fi  
  printf "\n"
}

function config_set_external_object_storage(){
  local default_choice="1"

  printf "\nWould you like to bring your own object store for all the products you've selected?\n"
  printf "[1] Yes\n"
  printf "[2] No\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      [1]*) EXTERNAL_OBJECT_STORAGE_ENABLED=true; break;;
      [2]*) EXTERNAL_OBJECT_STORAGE_ENABLED=false; break;;
      "") EXTERNAL_OBJECT_STORAGE_ENABLED=true; break;;
      *) printf "\nInvalid choice\n";
    esac
  done

  if [[ "${EXTERNAL_OBJECT_STORAGE_ENABLED}" == "true" ]]; then
    config_set_external_storage_type
  fi
}

function config_set_external_storage_type() {
  local default_choice
  default_choice="1"
  printf "\nSelect a storage type\n"
  printf "[1] Azure Storage Account\n"
  printf "[2] AWS S3\n"
  printf "[3] Other S3 Compatible storage\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      [1]*) EXTERNAL_STORAGE_TYPE="azure"; break;;
      [2]*) EXTERNAL_STORAGE_TYPE="s3"; break;;
      [3]*) EXTERNAL_STORAGE_TYPE="s3_compatible"; break;;
      "") EXTERNAL_STORAGE_TYPE="azure"; break;;
      *) printf "\nInvalid choice\n";
    esac
  done

  if [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" ]]; then
    config_set_s3_external_storage
  elif [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
    config_set_azure_external_storage
  elif [[ "${EXTERNAL_STORAGE_TYPE}" == "s3_compatible" ]]; then
    config_set_s3_compatible_external_storage
  fi

  config_set_create_buckets
}

function config_set_create_buckets() {
  local default_choice
  local provisioning_message
  default_choice="1"
  if [[ "${EXTERNAL_STORAGE_TYPE}" == "s3" || "${EXTERNAL_STORAGE_TYPE}" == "s3_compatible" ]]; then
    provisioning_message="Would you like the buckets to be automatically provisioned for all the products you've selected?"
  elif [[ "${EXTERNAL_STORAGE_TYPE}" == "azure" ]]; then
    provisioning_message="Would you like the containers to be automatically provisioned for all the products you've selected?"
  fi

  printf "\n%s\n" "${provisioning_message}"
  printf "[1] Yes\n"
  printf "[2] No\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      [1]*) EXTERNAL_STORAGE_CREATE_BUCKET=true; break;;
      [2]*) EXTERNAL_STORAGE_CREATE_BUCKET=false; break;;
      "") EXTERNAL_STORAGE_CREATE_BUCKET=true; break;;
      *) printf "\nInvalid choice\n";
    esac
  done

  print_bucket_info
}

function get_bucket_name() {
  local default_bucket_name="$1"
  local bucket_name
  bucket_name=""

  [[ -n "${BUCKET_PREFIX}" ]] && bucket_name="${BUCKET_PREFIX}-"
  bucket_name="${bucket_name}${default_bucket_name}"
  [[ -n "${BUCKET_SUFFIX}" ]] && bucket_name="${bucket_name}-${BUCKET_SUFFIX}"

  echo "${bucket_name}"
}

function print_bucket_info() {
  local title
  if [[ "${EXTERNAL_STORAGE_CREATE_BUCKET}" == "true" ]]; then
    title="Below buckets will be created during the installation of Automation Suite"
  else
    title="Please ensure below buckets are created before the installation of Automation Suite"
  fi

  cat <<EOF
$(printf "%s\n" "${title}")
$(echo "- Shared suite capabilities: $(get_bucket_name "uipath-as-platform")")
$([[ "${CONFIG_DATASERVICE_ENABLED}" == "true" ]] && echo "- Data Service: $(get_bucket_name "uipath-as-dataservice")")
$([[ "${CONFIG_PROCESSMINING_ENABLED}" == "true" ]] && echo "- Process Mining: $(get_bucket_name "uipath-as-processmining")")
$([[ "${CONFIG_ORCHESTRATOR_ENABLED}" == "true" ]] && echo "- Orchestrator: $(get_bucket_name "uipath-as-orchestrator")")
$([[ "${CONFIG_AICENTER_ENABLED}" == "true" ]] && echo "- Aicenter: $(get_bucket_name "uipath-as-aicenter")")
$([[ "${CONFIG_TASK_MINING_ENABLED}" == "true" ]] && echo "- Task Mining: $(get_bucket_name "uipath-as-taskmining")")
$([[ "${CONFIG_DU_ENABLED}" == "true" ]] && echo "- Orchestrator: $(get_bucket_name "uipath-as-du")")
$([[ "${CONFIG_TEST_MANAGER_ENABLED}" == "true" ]] && echo "- Orchestrator: $(get_bucket_name "uipath-as-testmanager")")
$([[ "${CONFIG_APPS_ENABLED}" == "true" ]] && echo "- Orchestrator: $(get_bucket_name "uipath-as-apps")")
EOF
  
  printf "\n"
}

function config_set_s3_compatible_external_storage() {
  S3_REGION="us-east-1"
  config_external_storage_bucket_prefix
  config_external_storage_bucket_suffix
  config_set_s3_fqdn
  config_set_s3_port
  config_set_s3_access_key
  config_set_s3_secret_key
}

function config_set_azure_external_storage() {
  local default_choice="1"

  printf "\nWould you like to use managed identity based authentication with your Azure storage account?\n"
  printf "[1] Yes\n"
  printf "[2] No\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      [1]*) USE_MANAGED_IDENTITY=true; break;;
      [2]*) USE_MANAGED_IDENTITY=false; break;;
      "") USE_MANAGED_IDENTITY=true; break;;
      *) printf "\nInvalid choice\n";
    esac
  done

  config_set_azure_account_name
  config_set_azure_suffix
  if [[ "${USE_MANAGED_IDENTITY}" == "true" ]]; then
    config_set_azure_client_id
  elif [[ "${USE_MANAGED_IDENTITY}" == "false" ]]; then
    config_set_azure_account_key
  fi

  BUCKET_PREFIX=""
  BUCKET_SUFFIX=""
}

function config_set_azure_account_name() {
  local input
  while :;do
    read -rp "Specify the Azure storage account name: " input
    if [[ -n "${input}" ]];then
      AZURE_ACCOUNTNAME="${input}"
      break
    fi
    printf "\naccount_name for storage account can't be empty \n"
  done
}

function config_set_azure_client_id() {
  local input
  read -rp "Specify the Client ID: " input
  AZURE_CLIENT_ID="${input}"
}

function config_set_azure_suffix() {
  local input
  while :;do
    printf "Specify the azure storage account endpoint suffix [%s]: " "${AZURE_SUFFIX}"
    read -r input
    if [[ -z "${input}" && -n "${AZURE_SUFFIX}" ]];then
      input="${AZURE_SUFFIX}"
    fi
    if [[ -n "${input}" ]]; then
      AZURE_SUFFIX="${input}"
      break
    fi

    printf "\nInvalid choice. It must be a valid azure storage account endpoint suffix. Please try again.\n"
  done
}

function config_set_s3_region() {
  local input
  while :;do
    printf "Specify the AWS region where buckets are hosted [%s]: " "${S3_REGION}"
    read -r input
    if [[ -z "${input}" && -n "${S3_REGION}" ]];then
      input="${S3_REGION}"
    fi
    if [[ -n "${input}" ]]; then
      S3_REGION="${input}"
      break
    fi
    printf "\nInvalid choice. It must be a valid region. Please try again.\n"
  done
}

function config_set_azure_account_key() {
  local input
  local confirm
  while :;do
    read -srp "Specify the Azure Account Key: " input
    if [[ -z "${input}" ]];then
      input="${AZURE_ACCOUNTKEY}"
    else
      printf "\n"
      read -srp "Enter Azure Account Key again to confirm: " confirm
      if [[ "${confirm}" != "${input}" ]]; then
        printf "\nAccount keys do not match.\n"
        continue
      fi
    fi

    if [[ -n "${input}" ]]; then
      AZURE_ACCOUNTKEY="${input}"
      break
    fi

    printf "\nInvalid choice. It must be a valid azure account key. Please try again.\n"
  done
  printf "\n"  
}

function config_set_s3_fqdn(){
  local input
  while :;do
    read -rp "Specify the S3 server FQDN: " input

    if [[ -z "${input}" ]];then
      echo "Invalid choice. It must be a valid fqdn. Please try again."
      continue
    fi

    if check_fqdn "${input}";then
      S3_FQDN="${input}";
      break
    fi
    printf "\nInvalid choice. It must be a valid fqdn. Please try again.\n"
  done
}

function config_set_s3_port(){
  local input
  while :;do
    read -rp "Specify the S3 PORT [${S3_PORT}]: " input
    if [[ -z "${input}" ]] && [[ -n "${S3_PORT}" ]];then
      input="${S3_PORT}"
    fi
    if check_port "${input}";then
      S3_PORT="${input}";
      break
    fi
    printf "\nInvalid choice. It must be a valid port between 1 and 65535\n"
  done
}

function config_external_storage_bucket_prefix() {
  local input
  while :;do
    read -rp "Specify the prefix for the buckets name: " input
    BUCKET_PREFIX="${input}"
    if [[ -z "${BUCKET_PREFIX}" && "${EXTERNAL_STORAGE_TYPE}" == "s3" ]]; then
      printf "\nInvalid choice. It must be a valid buckets prefix\n"
    else
      break
    fi
  done
}

function config_external_storage_bucket_suffix() {
  local input
  while :;do
    read -rp "Specify the suffix for the buckets name: " input
    BUCKET_SUFFIX="${input}"
    if [[ -z "${BUCKET_SUFFIX}" && "${EXTERNAL_STORAGE_TYPE}" == "s3" ]]; then
      printf "\nInvalid choice. It must be a valid bucket suffix\n"
    else
      break
    fi
  done
}

function config_set_s3_access_key() {
  local input
  while :;do
    read -rp "Specify Access Key for S3 account: " input
    if [[ -n "${input}" ]];then
      S3_ACCESSKEY="${input}"
      break
    fi
    printf "\nInvalid choice. It must be a valid s3 access key. Please try again\n"
  done
  printf "\n"
}

function config_set_s3_secret_key() {
  local input
  local confirm
  while :;do
    read -srp "Specify Secret Key for S3 account: " input
    if [[ -z "${input}" ]];then
      input="${S3_SECRETKEY}"
    else
      printf "\n"
      read -srp "Enter Secret Key for S3 account again to confirm: " confirm
      if [[ "${confirm}" != "${input}" ]]; then
        printf "\nSecret key do not match.\n"
        continue
      fi
    fi

    if [[ -n "${input}" ]];then
      S3_SECRETKEY="${input}"
      break
    fi

    printf "\nInvalid choice. It must be a valid s3 secret key. Please try again.\n"
  done
  printf "\n"
}

function config_set_s3_external_storage() {
  local default_choice="1"

  config_external_storage_bucket_prefix
  config_external_storage_bucket_suffix
  config_set_s3_region
  config_set_s3_access_key
  config_set_s3_secret_key
  S3_FQDN="s3.${S3_REGION}.amazonaws.com"
  S3_PORT="443"
}

function config_set_additional_crt() {
  clean_screen
  cat <<EOF
[!] For secure communication between external servers and products 
installed on the automation suite, a TLS handshake mechanism is used 
to trust the authenticity of the server to which the automation suite 
is connecting. These external servers can be SQL Server, S3 Server, 
SMTP Server etc. If your external server uses the certificate signed 
by your private certificate authority, the deployment will fail unless 
a public certificate of signing authority is provided.

[!] The installer accepts only Base64 encoded DER certificate in PEM format. 
If the Certificate Authority for all the external servers is different, 
you can concatenate all the public CA certificates in a single file. 

EOF

  local input
  while :;do
    read -rp "Specify the file path containing the public additional CA Certificate: " input

    if [[ -f "${input}" || -z "${input}" ]];then
      ADDITIONAL_CA_CERTS_PATH="${input}"
      break
    fi

    echo "Invalid file path. No file exists at ${input} location. Please try again."
    printf "\n"
  done
  printf "\n"
}

function print_config_details(){
  clean_screen
  printf "\nThe following products will be installed:\n"
  print_enabled_services

  cat <<EOF

[!] A self-signed certificate will be generated for you which is valid for 90 days
and only recommended for evaluation demo, development or test deployments.
We encourage you to replace it with a valid certificate to avoid any service disruption
in the future.

To start with a valid certificate, to enable any of the additional services (Task Mining,
AI Center, Document understanding and Apps) or for advanced settings not available
in the wizard (like SQL Integrated Authentication) please manually edit the config file:
${CONFIG_PATH}

[!] The installation enables sending telemetry by default. This is used for business
and operational purposes by UiPath used to improve our installation experience, no PII
is collected. If you do not want UiPath collecting telemetry, please open the configuration
file and set the telemetry_optout flag to true

Guidance can be found here:
https://docs.uipath.com/automation-suite/docs/advanced-installation-experience

EOF
}

function config_set_fqdn(){
  local input
  while :;do
    read -rp "Enter the Automation Suite FQDN [${CONFIG_CLUSTER_FQDN}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_CLUSTER_FQDN}"
    fi
    check_fqdn "${input}" && { CONFIG_CLUSTER_FQDN="${input}"; break; }
    echo "Invalid URL (must be a valid fqdn), please try again."
  done
}

function config_set_sql_url(){
  UPDATE_SQL_TEMPLATE=1
  local input
  while :;do
    read -rp "Specify the SQL server FQDN [${CONFIG_SQL_SERVER}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_SQL_SERVER}"
    fi

    local input_without_instance=${input%%\\*}
    if check_fqdn "${input_without_instance}" || check_hostname "${input_without_instance}" || check_ipv4 "${input_without_instance}";then
      CONFIG_SQL_SERVER="${input}";
      break
    fi
    echo "Invalid choice. It must be a valid fqdn/hostname/ipv4 address. Please try again."
  done
}

function config_set_sql_port(){
  UPDATE_SQL_TEMPLATE=1
  local input
  while :;do
    read -rp "Specify the SQL server connection PORT [${CONFIG_SQL_PORT}]: " input
    if [[ -z "${input}" ]] && [[ -n "${CONFIG_SQL_PORT}" ]];then
      input="${CONFIG_SQL_PORT}"
    fi

    if check_port "${input}";then
      CONFIG_SQL_PORT="${input}";
      break
    fi
    printf "\nInvalid choice. It must be a valid port between 1 and 65535\n"
  done
}

function config_set_sql_username(){
  UPDATE_SQL_TEMPLATE=1
  local input
  while :;do
    read -rp "Specify the SQL server username [${CONFIG_SQL_USERNAME}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_SQL_USERNAME}"
    fi

    if check_sql_username "${input}";then
      CONFIG_SQL_USERNAME="${input}";
      break
    fi
    echo "Invalid choice. It must be 128 characters or less."
  done
}

function config_set_sql_password(){
  UPDATE_SQL_TEMPLATE=1
  local input
  local confirm
  while :;do
    read -srp "Specify the SQL server password [${CONFIG_SQL_PASSWORD:+"***"}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_SQL_PASSWORD}"
    else
      printf "\n"
      read -srp "Enter the SQL server password again to confirm: " confirm
      if [[ "${confirm}" != "${input}" ]]; then
        printf "\nPasswords do not match.\n"
        continue
      fi
    fi

    if check_sql_password "${input}";then
      # escape backslash and double quotes to generate valid JSON
      CONFIG_SQL_PASSWORD="${input}"
      break
    fi
    echo "Invalid choice. It must be 128 characters or less."
  done
}

function config_set_multinode() {
  local default_choice="1"
  [[ "${CONFIG_CLUSTER_MULTINODE}" == "true" ]] && default_choice="2"

  printf "\nAre you performing an evaluation/development/test/demo or a production deployment?\n"
  printf "[1] Evaluation/development/test/demo deployment (single-node)\n"
  printf "[2] Production deployment (multi-node)\n"

  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      1) CONFIG_CLUSTER_MULTINODE=false; break;;
      2) CONFIG_CLUSTER_MULTINODE=true; break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function config_set_bundle() {
  local default_choice="1"

  printf "\nWhich product selection would you like to deploy?\n"
  printf "[1] Complete (All products)\n"
  printf "[2] Select products\n"

  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      1) select_all_products; break;;
      2) select_custom_products; break;;
      "") select_all_products; break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function config_set_airgapped(){
  local default_choice="1"
  [[ "${CONFIG_CLUSTER_AIRGAPPED:-"false"}" == "true" ]] && default_choice="2"

  printf "\nWill your deployment have access to Internet (online) or is it physically isolated from unsecured networks (air-gapped)?\n"
  printf "[1] Online\n"
  printf "[2] Air-gapped\n"

  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      1) CONFIG_CLUSTER_AIRGAPPED=false; break;;
      2) CONFIG_CLUSTER_AIRGAPPED=true; break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function config_set_sql_create_db(){
  local default_choice="1"
  [[ "${CONFIG_SQL_CREATE_DB}" == "true" ]] || default_choice="2"

  printf "\nWould you like the databases to be automatically provisioned for all the products you've selected?\n"
  printf "[1] Yes\n"
  printf "[2] No\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      [1]*) CONFIG_SQL_CREATE_DB=true; break;;
      [2]*) CONFIG_SQL_CREATE_DB=false; break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done

[[ "${CONFIG_SQL_CREATE_DB}" == "false" ]] || cat <<EOF

The following databases will be provisioned automatically:
- Shared suite capabilities: AutomationSuite_Platform
$([[ "${CONFIG_ORCHESTRATOR_ENABLED}" == "true" ]] && echo "- Orchestrator: AutomationSuite_Orchestrator")
$([[ "${CONFIG_TEST_MANAGER_ENABLED}" == "true" ]] && echo "- Test Manager: AutomationSuite_Test_Manager")
$([[ "${CONFIG_INSIGHTS_ENABLED}" == "true" ]] && echo "- Insights: AutomationSuite_Insights")
$([[ "${CONFIG_AUTOMATION_HUB_ENABLED}" == "true" ]] && echo "- Automation Hub: AutomationSuite_Automation_Hub")
$([[ "${CONFIG_AUTOMATION_OPS_ENABLED}" == "true" ]] && echo "- Automation Ops: AutomationSuite_Automation_Ops")
$([[ "${CONFIG_AICENTER_ENABLED}" == "true" ]] && echo "- AI Center: AutomationSuite_AICenter")
$([[ "${CONFIG_DU_ENABLED}" == "true" ]] && echo "- Document Understanding: AutomationSuite_DU_Datamanager")
$([[ "${CONFIG_DATASERVICE_ENABLED}" == "true" ]] && echo "- Data Service: AutomationSuite_DataService")
$([[ "${CONFIG_ASROBOTS_ENABLED}" == "true" ]] && echo "- Automation Suite Robots: AutomationSuite_Orchestrator")
$([[ "${CONFIG_PROCESSMINING_ENABLED}" == "true" ]] && echo "- Process Mining: AutomationSuite_ProcessMining_Metadata AutomationSuite_Airflow")
EOF
}

function set_aicenter_config(){
  local default_choice="2"
  local input

  printf "\nDoes AI Center use an external Orchestrator?\n"
  printf "[1] Yes\n"
  printf "[2] No\n"

  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input

    case "${input}" in
      1) config_set_aicenter_external_orchestrator; break;;
      2) break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function set_processmining_config(){
  local input
  UPDATE_PM_SQL_TEMPLATE=1

  printf "\n\nProcess Mining requires a second SQL server in a production environment to not impact other Automation Suite products.\n"

  while :;do
    read -rp "Specify the Process Mining data warehouse SQL server FQDN [${PROCESSMINING_WH_SQL_SERVER}]: " input
    if [[ -z "${input}" ]];then
      input="${PROCESSMINING_WH_SQL_SERVER}"
    fi

    local input_without_instance=${input%%\\*}
    if check_fqdn "${input_without_instance}" || check_hostname "${input_without_instance}" || check_ipv4 "${input_without_instance}";then
      PROCESSMINING_WH_SQL_SERVER="${input}";
      break
    fi
    echo "Invalid choice. It must be a valid fqdn/hostname/ipv4 address. Please try again."
  done

  while :;do
    read -rp "Specify the Process Mining data warehouse SQL server connection PORT [${PROCESSMINING_WH_SQL_PORT}]: " input
    if [[ -z "${input}" ]] && [[ -n "${PROCESSMINING_WH_SQL_PORT}" ]];then
      input="${PROCESSMINING_WH_SQL_PORT}"
    fi

    if check_port "${input}";then
      PROCESSMINING_WH_SQL_PORT="${input}";
      break
    fi
    printf "\nInvalid choice. It must be a valid port between 1 and 65535\n"
  done

  if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" != "true" ]]; then
    while :;do
      read -rp "Specify the Process Mining data warehouse SQL server username [${PROCESSMINING_WH_SQL_USERNAME}]: " input
      if [[ -z "${input}" ]];then
        input="${PROCESSMINING_WH_SQL_USERNAME}"
      fi

      if check_sql_username "${input}";then
        PROCESSMINING_WH_SQL_USERNAME="${input}";
        break
      fi
      echo "Invalid choice. It must be 128 characters or less."
    done

    local confirm
    while :;do
      read -srp "Specify the Process Mining data warehouse SQL server password [${PROCESSMINING_WH_SQL_PASSWORD:+"***"}]: " input
      if [[ -z "${input}" ]];then
        input="${PROCESSMINING_WH_SQL_PASSWORD}"
      else
        printf "\n"
        read -srp "Enter the SQL server password again to confirm: " confirm
        if [[ "${confirm}" != "${input}" ]]; then
          printf "\nPasswords do not match.\n"
          continue
        fi
      fi

      if check_sql_password "${input}";then
        # escape backslash and double quotes to generate valid JSON
        PROCESSMINING_WH_SQL_PASSWORD="${input}"
        break
      fi
      echo "Invalid choice. It must be 128 characters or less."
    done
  fi
}

function config_set_aicenter_external_orchestrator() {
  config_set_aicenter_external_orchestrator_url
  config_set_aicenter_external_identity
  config_set_aicenter_orchestrator_cert
  config_set_aicenter_identity_cert
  config_set_metering_api_key
}

function config_set_aicenter_external_orchestrator_url() {
  local input
  while :;do
    read -rp "Specify the Orchestrator URL for AI Center [${AICENTER_ORCHESTRATOR_URL}]: " input
    if [[ -z "${input}" ]];then
      input="${AICENTER_ORCHESTRATOR_URL}"
    fi
    if check_fqdn "${input}";then
      AICENTER_ORCHESTRATOR_URL="${input}";
      break
    fi
    echo "Invalid choice. It must be a valid fqdn. Please try again."
  done
}

function config_set_aicenter_external_identity() {
  local input
  while :;do
    read -rp "Specify the Identity URL for AI Center [${AICENTER_IDENTITY_URL}]: " input
    if [[ -n "${input}" ]];then
      AICENTER_IDENTITY_URL="${input}";
      break
    fi
    echo "Invalid choice. It must be a valid fqdn. Please try again."
  done
}

function config_set_aicenter_orchestrator_cert() {
  local input
  while :;do
    read -rp "Specify the path to Orchestrator certificate file [${AICENTER_ORCHESTRATOR_CERT}]: " input
    if [[ -n "${input}" ]];then
      AICENTER_ORCHESTRATOR_CERT="${input}";
      break
    fi
    echo "Invalid choice. It must be non empty. Please try again."
  done
}

function config_set_aicenter_identity_cert() {
  local input
  while :;do
    read -rp "Specify the path to Identity certificate file [${AICENTER_IDENTITY_CERT}]: " input
    if [[ -n "${input}" ]];then
      AICENTER_IDENTITY_CERT="${input}";
      break
    fi
    echo "Invalid choice. It must be non empty. Please try again."
  done
}

function config_set_metering_api_key() {
  local input
  while :;do
    read -rp "Specify the metering api key, leave nil if installing offline [${AICENTER_METERING_KEY}]: " input
    if [[ -n "${input}" ]];then
      AICENTER_METERING_KEY="${input}";
    fi
    break
  done
}

function config_set_sql_integrated_auth_enabled(){
  UPDATE_SQL_TEMPLATE=1
  local default_choice="1"
  [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]] || default_choice="2"
  printf "\nWould you like to enable Kerberos Auth? This will be used to connect to SQL Databases and Active Directory Lightweight Directory Adaptor if configured.\n"
  printf "[1] Yes\n"
  printf "[2] No\n"
  printf "\n"
  while :
  do
    printf "Enter your choice [%s]: " "${default_choice}"
    read -r input
    case "${input}" in
      [1]*) CONFIG_INTEGRATED_AUTH_ENABLED=true; break;;
      [2]*) CONFIG_INTEGRATED_AUTH_ENABLED=false; break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function config_set_sql_integrated_auth_ad_domain(){
  local input
  while :;do
    read -rp "Specify the Active Directory domain for Kerberos Auth [${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}"
    fi
    if check_fqdn "${input}";then
      CONFIG_INTEGRATED_AUTH_AD_DOMAIN="${input}";
      break
    fi
    echo "Invalid choice. It must be a valid fqdn. Please try again."
  done
}

function config_set_sql_integrated_auth_ad_username(){
  local input
  while :;do
    read -rp "Specify the default Active Directory username for Kerberos Auth [${CONFIG_INTEGRATED_AUTH_AD_USERNAME}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_INTEGRATED_AUTH_AD_USERNAME}"
    fi
    if check_ad_username "${input}";then
      CONFIG_INTEGRATED_AUTH_AD_USERNAME="${input}";
      break
    fi
  done
}

function config_set_sql_integrated_auth_lifetime(){
  local input
  while :;do
    read -rp "Specify the Ticket Granting Ticket lifetime (TGT) in hours for Kerberos Auth [${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME}"
    fi

    if check_integrated_auth_ticket_lifetime "${input}";then
      CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME="${input}";
      break
    fi
    printf "\nInvalid choice. It must be a valid Ticket Granting Ticket lifetime (TGT) in hours between 8-168 \n"
  done
}

function config_set_sql_integrated_auth_user_keytab(){
  local input
  while :;do
    read -srp "Specify the default Active Directory user's keytab for Kerberos Auth [${CONFIG_INTEGRATED_AUTH_USER_KEYTAB:+"***"}]: " input
    if [[ -z "${input}" ]];then
      input="${CONFIG_INTEGRATED_AUTH_USER_KEYTAB}"
    fi
    if check_kdc_userkeytab "${input}";then
      CONFIG_INTEGRATED_AUTH_USER_KEYTAB="${input}";
      break
    fi
    printf "\nInvalid choice. It can't be empty.\n"
  done
  printf "\n"
}

# Functions used to validate config user input
function check_fqdn(){
  # should not have more than 255 characters
  echo "$1" | grep -q -E '^.{1,255}$' || return 1

  # each subdomain should be no more than 63 characters
  # should not have more than 127 subdomains
  echo "$1" | grep -q -E '^.{1,63}\.{1,127}.*$' || return 1

  # the top level domain should not contain only numbers
  echo "$1" | grep -q -vE '^.*\.[0-9]+$' || return 1

  # the top level domain should only contain letters, number and hyphens.
  # the top level domain should start with a letter and not start or end with a hyphen
  # the top level domain should be at last 2 characters long
  echo "$1" | grep -q -E '^.*\.[a-zA-Z]+[a-zA-Z0-9-]{0,61}[a-zA-Z0-9]+\.?$' || return 1

  return 0
}

function check_hostname(){
  echo "$1" | grep -q -e '[^-0-9A-Z_a-z]' -e '^[^0-9A-Za-z]' -e '[^0-9A-Za-z]$' -e '-_' -e '_-' && return 1
  return 0
}

function check_port(){
  local port
  port="${1}"
  [[ ${port} =~ ^[1-9]*[0-9]*$ ]] || return 1
  (( port > 1 && port < 65535 )) || return 1
  return 0
}

function check_ipv4(){
  local octets
  IFS="." read -ra octets <<< "$1"

  [[ ${#octets[@]} -ne 4 ]] && return 1

  for octet in "${octets[@]}"
  do
    if [[ ${octet} =~ ^[0-9]{1,3}$ ]]
    then
      ((octet <= 255 )) || return 1
    else
      return 1
    fi
  done
  return 0
}

function check_sql_username(){
  local string="$1"
  local string_length=${#string}
  (( string_length!=0 && string_length < 128 )) || return 1
  return 0
}

function check_sql_password(){
  local string="$1"
  local string_length=${#string}
  (( string_length!=0 && string_length < 128 )) || return 1
  return 0
}

function urlencoded_pyodbc_string() {
  local string="$1"
  printf %s "$string" | jq -sRr @uri
}

function escape_jdbc_odbc_sql_password(){
  local string="$1"
  echo "$string" | sed -r 's/}+/}}/g'
}

function escape_dotnet_sql_password(){
  local string="$1"
  echo "$string" | sed -r 's/\o047/\o047\o047/g'
}

function escape_sql_servername(){
  local string="$1"
  echo "$string" | sed -r 's/\\/\\\\/g'
}

# Download/Extract/Install
function check_install_directory(){
  if [[ ! -d "${INSTALLER_FOLDER}" ]]; then
    mkdir -p "${INSTALLER_FOLDER}" || error "Could not create install folder: ${INSTALLER_FOLDER}"
  fi

  chmod 755 "${INSTALLER_FOLDER}"

  [[ -r "${INSTALLER_FOLDER}" ]] || error "Install folder: ${INSTALLER_FOLDER} not readable"
  [[ -w "${INSTALLER_FOLDER}" ]] || error "Install folder: ${INSTALLER_FOLDER} not writable"

  ok "Using installer folder: ${INSTALLER_FOLDER}"
}

function check_ad_username(){
  local string="$1"
  local string_length=${#string}
  if [[ string_length -eq 0 || string_length -ge 128  ]]; then
    printf "\nInvalid choice. It must be less than 128 characters.\n"
    return 1
  fi
  if [[ "$input" = HTTP\\* ]];then
    printf "\nInvalid input. use HTTP/, not HTTP\\ \n";
    return 1
  fi
  return 0
}

function check_kdc_userkeytab(){
  local string="$1"
  local string_length=${#string}
  (( string_length!=0 )) || return 1
  return 0
}

function check_integrated_auth_ticket_lifetime(){
  local ticket_lifetime
  ticket_lifetime="${1}"
  [[ ${ticket_lifetime} =~ ^[1-9]*[0-9]*$ ]] || return 1
  (( ticket_lifetime >= 8 && ticket_lifetime <= 168 )) || return 1
  return 0
}

function download_bundle() {
  local bundle_url="${CONFIG_BUNDLE_URL}"

  local bundle_destination="${BUNDLE_FILE}"

  printf "\n"

  if [[ -f "${bundle_destination}" ]]; then
    ok "Found existing bundle: $(basename "${bundle_destination}")"
    return;
  fi

  pending "Downloading bundle..."
  mkdir -p "${BUNDLE_PATH}" || true
  if [ "${CAN_CURL}" == "true" ]; then
    curl -sSfL "${bundle_url}" -o "${bundle_destination}" &>>"${INSTALLER_LOGFILE}" &
    waiting_for_pid "$!" || error "Download failed"
  elif [ "${CAN_WGET}" == "true" ]; then
    wget -q -O "${bundle_destination}" "${bundle_url}" &>>"${INSTALLER_LOGFILE}" &
    waiting_for_pid "$!" | error "Download failed"
  fi

  ok "Download complete"
}

function extract_bundle() {
  mkdir -p "${BUNDLE_PATH}"
  pending "Extracting bundle to ${BUNDLE_PATH}/"
  unzip -qq -n "${BUNDLE_FILE}" -d "${BUNDLE_PATH}" &>/dev/null &
  waiting_for_pid $! || error "Failed to extract the bundle"
  chmod -R +r,+w,+x "${BUNDLE_PATH}"/*
  ok "Extraction complete"
}

function install_guide(){
  cat <<EOF

===============================================================================
                                    Next steps:
===============================================================================
1. The wizard will let you choose or specify a few deployment parameters and
will then automatically generate the cluster configuration file. You will have
an option to customize the configuration file if you need.

2. Once the configuration file is ready, the wizard will download the required
products and components and install them on the first (virtual) machine.

3. If you chose a multi-node deployment the wizard will provide the commands you
will need to perform on the remaining (virtual) machines to install the required
products and components and join them to the cluster.

The wizard will provide step by step instructions as you progress through the
deployment process.

EOF
}

function readiness_guide(){
  cat <<EOF

===============================================================================
                                    Next steps:
===============================================================================
1. The wizard will let you choose or specify a few readiness check parameters
and will then automatically generate the cluster configuration file. You will
have an option to customize the configuration file if you need.

2. Once the configuration file is ready, the wizard will download the required
products and components and run readiness validation on the virtual machine.

The wizard will provide step by step instructions as you progress through the
validation process.

EOF
}

function check_old_cluster_config(){
  pending "Checking for config changes..."
  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}" \
          --compare-config \
          --accept-license-agreement \
          3>&1 1>>"${INSTALLER_LOGFILE}" 2>&1
  return $?
}

function install_infra(){
  pending "Setting up the cluster infrastructure..."
  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}" \
          -k \
          --accept-license-agreement \
          --skip-compare-config \
          1>>"${INSTALLER_LOGFILE}" 2>&1 &

  waiting_for_stage $! "Infrastructure" || error "Failed the cluster infrastructure setup"

  ok "Completed the cluster infrastructure setup"
}

function join_node(){
  local type="$1"

  pending "Joining ${type}..."

  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}.infra" \
          -k \
          -j "${type}" \
          --accept-license-agreement \
          --skip-compare-config \
          1>>"${INSTALLER_LOGFILE}" 2>&1 &

  waiting_for_pid $! || error "Failed to join ${type}"

  ok "Successfully joined ${type}"
}

function install_fabric(){
  pending "Setting up the cluster management layer..."

  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}" \
          -f \
          --accept-license-agreement \
          --skip-compare-config \
          1>>"${INSTALLER_LOGFILE}" 2>&1 &

  waiting_for_stage $! "ManagementTools" || error "Failed to setup the cluster management layer"

  ok "Completed the cluster management layer setup..."
}

function install_services(){
  pending "Setting up the UiPath products..."

  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}" \
          -s \
          --accept-license-agreement \
          --skip-compare-config \
          1>>"${INSTALLER_LOGFILE}" 2>&1 &

  waiting_for_stage $! "Products" || error "Failed to set up UiPath products"

  ok "Completed the UiPath products setup"
  ok "Done. Happy automating!"
}

function interactive_install_steps() {
  if [[ "${CONFIG_CLUSTER_AIRGAPPED}" == "true" ]];then
    printf "\nThe install process will take about 2 hours to complete.\n"
  else
    printf "\nThe install process will take about 1 hour to complete.\n"
  fi

  check_old_cluster_config

  install_infra

  multinode_next_steps

  if [[ "${CONFIG_CLUSTER_MULTINODE}" == "false" ]];then
    install_fabric
    install_services
    certificates_warning
    install_summary
  fi
}

function certificates_warning(){
  cat <<EOF

===============================================================================
                                    Security considerations
===============================================================================

Before moving forward please make sure:

- To update the TLS & token signing certificates in case you opted in for self-signed ones.
  Please refer to Configuring the cluster https://docs.uipath.com/automation-suite/docs/configuring-the-cluster#managing-server-certificates for more details.

- To update the SQL connection certificate in case you opted in for a self-signed one.
  Please refer to Configuring the cluster https://docs.uipath.com/automation-suite/docs/configuring-the-cluster#managing-server-certificates for more details.

- To encrypt or delete the cluster_config.json file as it contains sensitive information in plain text.
EOF
  if [[ "${UNATTENDED_ACTION}" == "none" ]];then
    printf "\n"
    read -rp "Press [Enter] to continue..."
  fi
}

function install_summary(){
  [[ "${UNATTENDED_ACTION}" == "none" ]] && clean_screen
  local token

  token=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.token}" | base64 -d)

  cat <<EOF | tee "${INSTALLER_SUMMARY}"

===============================================================================
                              Deployment summary
===============================================================================
This summary is saved to ${INSTALLER_SUMMARY}.

Before running any kubectl commands run the following commands:
> sudo su -
> export KUBECONFIG="/etc/rancher/rke2/rke2.yaml" \\
&& export PATH="\$PATH:/usr/local/bin:/var/lib/rancher/rke2/bin"

Continue to run the rest of the commands as root.

==============================================================================

Complete your installation and learn how to manage your Automation Suite by accessing the 
Cluster Administration portal:

https://${CONFIG_CLUSTER_FQDN}/uipath-management/

===============================================================================

To configure shared suite capabilities and start building automations, go to
the Portal:

- URL: https://${CONFIG_CLUSTER_FQDN}
- Switch to the "Default" organization
- Credentials: The username is "orgadmin". Run the following command to retrieve the password:
> kubectl get secrets/platform-service-secrets -n uipath \\
-o "jsonpath={.data['identity\.hostAdminPassword']}" | echo \$(base64 -d)

Using the same command to retrieve the org admin and the host admin passwords is by design -
the two passwords are initially the same. However, upon the first login, the org admin must
change their password.

===============================================================================

To access the Host organization:

- URL: https://${CONFIG_CLUSTER_FQDN}
- Switch to the "Host" organization
- Credentials: The username is "admin". Run the following command to retrieve the password:
> kubectl get secrets/platform-service-secrets -n uipath \\
-o "jsonpath={.data['identity\.hostAdminPassword']}" | echo \$(base64 -d)

Using the same command to retrieve the org admin and the host admin passwords is by design -
the two passwords are initially the same. However, upon the first login, the org admin must
change their password.

===============================================================================

To manage the Kubernetes cluster (monitoring & troubleshooting), go to the
Rancher console.

- URL: https://monitoring.${CONFIG_CLUSTER_FQDN}
- Credentials: The username is "admin". Run the following command to retrieve the password:
> kubectl get secrets/rancher-admin-password -n cattle-system \\
-o "jsonpath={.data['password']}" | echo \$(base64 -d)


===============================================================================

To manage the installed products (install/uninstall & configure) and certificates,
go to the ArgoCD console.

- URL: https://alm.${CONFIG_CLUSTER_FQDN}
- Credentials: The username is "argocdro". Run the following command to retrieve the password:
> kubectl get secrets/argocd-user-password -n argocd \\
-o "jsonpath={.data['password']}" | echo \$(base64 -d)

===============================================================================

About self-signed certificates and token signing

The auto generated self-signed certificate is valid for 90 days and is used for both secure
connection to the website as well as for signing the authentication tokens issued
by the Automation Suite identity server.

For production deployments we strongly recommend using a public/trusted certificate.
To replace the self-signed certificate and to learn more about the certificates used
by the Automation Suite, refer to the public documentation:
https://docs.uipath.com/automation-suite/docs


To retrieve the current certificate, run the following command:

> kubectl get secrets/istio-ingressgateway-certs -n istio-system \\
-o "jsonpath={.data['tls\.crt']}" | echo \$(base64 -d)

EOF

[[ "${CONFIG_CLUSTER_MULTINODE}" == "false" ]] && return 0
[[ -n "${UNATTENDED_ACTION}" ]] && return 0

cat <<EOF | tee -a "${INSTALLER_SUMMARY}"

===============================================================================

To join new nodes to the cluster, run the following commands as root on the
target nodes (replace "join_server" with "join_agent" where needed):

token="${token}";
curl -k --header "Authorization: Bearer \$token" \
  -s https://${CONFIG_CLUSTER_FQDN}:6443/api/v1/namespaces/default/secrets/${BUNDLE_K8S_SECRET}?pretty=true \
  | grep '"installer"' | cut -d '"' -f 4 | base64 -d > installer.sh;
curl -k --header "Authorization: Bearer \$token" \
  -s https://${CONFIG_CLUSTER_FQDN}:6443/api/v1/namespaces/default/secrets/${BUNDLE_K8S_SECRET}?pretty=true \
  | grep '"config"' | cut -d '"' -f 4 | base64 -d > config.json;
chmod +x installer.sh config.json ;
CONFIG_PATH=./config.json UNATTENDED_ACTION="accept_eula,download_bundle,extract_bundle,join_server" \
  ./installer.sh

EOF
}

function multinode_next_steps() {
  local token

  kubectl apply -f - <<'EOF' &>/dev/null
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: get-secrets
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: get-secrets
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: get-secrets
subjects:
- namespace: default
  kind: ServiceAccount
  name: default
EOF

  token=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.token}" | base64 -d)

  kubectl delete secret ${BUNDLE_K8S_SECRET} &>/dev/null || true
  kubectl create secret generic ${BUNDLE_K8S_SECRET} --from-file=installer="${SCRIPT_PATH}" \
          --from-file=config="${CONFIG_PATH}" &>/dev/null \
          || error "Failed to create installer bundle secret"

  [[ "${CONFIG_CLUSTER_MULTINODE}" == "false" ]] || cat << EOF
===============================================================================
To finish the multinode setup follow the next steps
===============================================================================

  1. Run the following commands as root on the other nodes that should be part of the cluster: (replace "join_server" with "join_agent" where needed). The cluster should have least 3 server nodes.

token="${token}";
curl -k --header "Authorization: Bearer \$token" -s https://${CONFIG_CLUSTER_FQDN}:6443/api/v1/namespaces/default/secrets/${BUNDLE_K8S_SECRET}?pretty=true | grep '"installer"' | cut -d '"' -f 4 | base64 -d > installer.sh;
curl -k --header "Authorization: Bearer \$token" -s https://${CONFIG_CLUSTER_FQDN}:6443/api/v1/namespaces/default/secrets/${BUNDLE_K8S_SECRET}?pretty=true | grep '"config"' | cut -d '"' -f 4 | base64 -d > config.json;
chmod +x installer.sh config.json ;
CONFIG_PATH=./config.json UNATTENDED_ACTION="accept_eula,download_bundle,extract_bundle,join_server" ./installer.sh

  2. Task Mining and Automation Suite Robots require dedicated nodes. Steps for configuring and
  joining those nodes can be found at:

  https://docs.uipath.com/automation-suite/v2022.10/docs/adding-a-dedicated-agent-node-for-automation-suite-robots
  https://docs.uipath.com/automation-suite/v2022.10/docs/adding-a-dedicated-agent-node-for-task-mining

  3. Once all nodes are joined run the following command on this server:

UNATTENDED_ACTION="accept_eula,install_fabric,install_services" "${SCRIPT_PATH}"

EOF
}

function get_stage_status(){
  local stage="$1"
  local operations
  local log_output

  # use tac to reverse the file and get the latest status
  # print just the name and status for each operation
  # !a[\$NF]++ removes duplicates based on the last field
  log_output="$(tac "${INSTALLER_LOGFILE}" |  awk -F"] " "/\[${stage}\]/ && !a[\$NF]++  { printf \"%s] %s\\n\" ,\$(NF-1),\$NF }")"

  # use tac to reverse again and print the latest updates in the original order
  operations="$(echo "${log_output}" | tac)"
  echo -n "${operations}"
}

function waiting_for_stage() {
  local pid="$1"
  local stage="$2"
  local status=()
  local frames=(".  " ".. " "..." "   ")

  while [ -d "/proc/${pid}" ];do
    IFS=$'\n' read -d '' -r -a status <<< "$(get_stage_status "${stage}")"

    for line in "${status[@]}";do
      indent; echo "$line"; tput el
    done


    # to keep printing the status in place for the curent stage
    [[ "${#status[@]}" != 0 ]] && tput cuu "${#status[@]}" # move the cursor up for the number of lines we just printed
  done
   
  # once done, poll one more time for the last status
  IFS=$'\n' read -d '' -r -a status <<< "$(get_stage_status "${stage}")"
  for line in "${status[@]}";do
    indent; echo "$line"; tput el
  done
  wait "${pid}" # used to return the exit code of the process we're waiting for
}

function create_readiness_config() {
  local install_type="$1"
  local readiness_config_file="$2"
  CONFIG_READINESS_VALIDATION=true
  [[ -n "${install_type}" && "${install_type}" == "offline" ]] && CONFIG_CLUSTER_AIRGAPPED=true
  [[ -n "${readiness_config_file}" ]] && CONFIG_PATH="${readiness_config_file}"
  read_existing_config_file
  [[ -f "${CONFIG_PATH}" ]] || main_menu
  CONFIG_READINESS_VALIDATION=false
}

function main() {
  SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
  if [[ -z "${SCRIPT_DIR}" ]]; then
    error "Could not determine script dir"
  fi

  SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
  if [[ -z "${SCRIPT_PATH}" ]]; then
    error "Could not determine script path"
  fi
  export PATH=$PATH:/var/lib/rancher/rke2/bin:${SCRIPT_DIR}/bin:/usr/local/bin
  export KUBECONFIG="/etc/rancher/rke2/rke2.yaml"

  CAN_CURL="$([ -x "$(command -v curl)" ] && echo true || echo false)"
  CAN_WGET="$([ -x "$(command -v wget)" ] && echo true || echo false)"
  CAN_UNZIP="$([ -x "$(command -v unzip)" ] && echo true || echo false)"
  CAN_JQ="$([ -x "$(command -v jq)" ] && echo true || echo false)"

  check_prereqs
  read_existing_config_file
  [[ "${UNATTENDED_ACTION}" =~ "accept_eula" ]] || check_eula
  [[ "${UNATTENDED_ACTION}" =~ "download_bundle" ]] && download_bundle
  [[ "${UNATTENDED_ACTION}" =~ "extract_bundle" ]] && extract_bundle
  if [[ "${CONFIG_READINESS_VALIDATION}" == "false" ]]; then
    download_bundle
    extract_bundle
  fi
  [[ "${UNATTENDED_ACTION}" =~ install_|join_ ]] && { check_install_directory; check_old_cluster_config; }
  [[ "${UNATTENDED_ACTION}" =~ "install_infra" ]] && install_infra
  [[ "${UNATTENDED_ACTION}" =~ "join_asrobots" ]] && join_node "asrobots"
  [[ "${UNATTENDED_ACTION}" =~ "join_server" ]] && join_node "server"
  [[ "${UNATTENDED_ACTION}" =~ "join_agent" ]] && join_node "agent"
  [[ "${UNATTENDED_ACTION}" =~ "join_task_mining" ]] && join_node "task-mining"
  [[ "${UNATTENDED_ACTION}" =~ "join_gpu" ]] && join_node "gpu"
  [[ "${UNATTENDED_ACTION}" =~ "install_fabric" ]] && install_fabric
  [[ "${UNATTENDED_ACTION}" =~ "install_services" ]] && { install_services; certificates_warning; install_summary; }

  if [[ "${UNATTENDED_ACTION}" == "none" ]]; then
    [[ -f "${CONFIG_PATH}" ]] && use_existing_config
    [[ -f "${CONFIG_PATH}" ]] || main_menu
  fi

  exit 0
}

# Don't run main if sourced
[[ "${BASH_SOURCE[0]}" != "${0}" ]] || main "$@"

