#!/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="2021.10.4"

# 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"

# service flags
CONFIG_ACTION_CENTER_ENABLED="true"
CONFIG_TEST_MANAGER_ENABLED="true"
CONFIG_INSIGHTS_ENABLED="true"
CONFIG_AUTOMATION_HUB_ENABLED="true"
CONFIG_AUTOMATION_OPS_ENABLED="true"
CONFIG_AICENTER_ENABLED="false"
CONFIG_APPS_ENABLED="false"
CONFIG_DU_ENABLED="false"
CONFIG_TASK_MINING_ENABLED="false"
CONFIG_ORCHESTRATOR_UPDATESERVER_ENABLED="true"
CONFIG_ORCHESTRATOR_TESTAUTOMATION_ENABLED="true"

# installer specific
ONLINE_BUNDLE_PATH="${INSTALLER_FOLDER}/online_installer_${VERSION}"
OFFLINE_BUNDLE_PATH="${INSTALLER_FOLDER}/offline_installer_${VERSION}"
BUNDLE_PATH="${ONLINE_BUNDLE_PATH}"
BUNDLE_FILE="${BUNDLE_PATH}.zip"
INSTALLER_ENTRYPOINT="${BUNDLE_PATH}/install-uipath.sh"
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=("Platform" "Orchestrator")

# flags
UPDATE_SQL_TEMPLATE=0

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.

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

For the basic selection (Action Center, Automation Hub, Automation Ops,
Insights, Orchestrator, Test Suite):
  - Processor: 64-bit, 16 (v-)CPU/cores
  - RAM: 32 GiB
  - Hard disk space: 256 GiB SSD (cluster binaries), 512 GiB SSD (data),
    16 GiB SSD (etcd), 512 GiB SSD (UiPath bundle, offline only)
  - Operating system: RHEL 8.2+

For the complete selection (Basic + AI Center, Apps, Document Understanding, Task Mining):
  - 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

For more details on minimum hardware and software requirements based on the products selection
you choose, please see: 
https://docs.uipath.com/automation-suite/v2021.10.0-GA/docs/automation-suite-hardware-requirements

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.

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

For the basic selection (Action Center, Automation Hub, Automation Ops,
Insights, Orchestrator, Test Suite):
  - Processor: 64-bit, 48 (v-)CPU/cores (min 16 per node)
  - RAM: 96 GiB (min 16 per node)
  - Hard disk space: 256 GiB SSD (cluster binaries), 512 GiB SSD (data),
    16 GiB SSD (etcd), 512 GiB SSD (UiPath bundle, offline only)
  - Operating system: RHEL 8.2+

For the complete selection (Basic + AI Center, Apps, Document Understanding, Task Mining):
  - 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.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

For more details on minimum hardware and software requirements based on the products selection
you choose, please see: 
https://docs.uipath.com/automation-suite/v2021.10.0-GA/docs/automation-suite-hardware-requirements

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 install_menu() {
  check_install_directory
  [[ -f "${CONFIG_PATH}" ]] || new_config_menu
  download_bundle
  extract_bundle
  interactive_install_steps
  quit
}

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}")"

  generate_config
  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() {
  install_guide

  cat <<EOF

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

  config_set_airgapped
  config_set_bundle
  config_set_fqdn;
  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
  config_set_sql_create_db
  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
  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}"
    if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
      printf "[10] Kerberos Auth Active Directory domain: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_DOMAIN}"
      printf "[11] Kerberos Auth TGT lifetime in hours: %s\n" "${CONFIG_INTEGRATED_AUTH_TICKET_LIFETIME}"
      printf "[12] Kerberos Auth default Active Directory username: %s\n" "${CONFIG_INTEGRATED_AUTH_AD_USERNAME}"
      printf "[13] 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_sql_integrated_auth_ad_domain;;
      11) config_set_sql_integrated_auth_lifetime;;
      12) config_set_sql_integrated_auth_ad_username;;
      13) 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
- Document Understanding
- Insights
- Orchestrator
- Task 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 generate_config(){
  local escaped_odbc_jdbc_password
  local escaped_dotnet_password
  escaped_odbc_jdbc_password=$(escape_jdbc_odbc_sql_password "${CONFIG_SQL_PASSWORD}")
  escaped_dotnet_password=$(escape_dotnet_sql_password "${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"

  # 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 sql_dotnet_template="Server=tcp:${CONFIG_SQL_SERVER},${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://${CONFIG_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=${CONFIG_SQL_SERVER}"
  local sql_odbc_template="SERVER=${CONFIG_SQL_SERVER},${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;"

  if [[ "${CONFIG_INTEGRATED_AUTH_ENABLED}" == "true" ]]; then
    sql_dotnet_template="Server=tcp:${CONFIG_SQL_SERVER},${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://${CONFIG_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=${CONFIG_SQL_SERVER}"
    sql_odbc_template="SERVER=${CONFIG_SQL_SERVER},${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;"
  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}"
  fi

  [[ -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"}",
  "multinode": "${CONFIG_CLUSTER_MULTINODE:-"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": "${CONFIG_SQL_SERVER}",
    "username": "${CONFIG_SQL_USERNAME}",
    "password": "${CONFIG_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}",
  "orchestrator": {
    "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"}"
  },
  "aicenter": {
    "enabled": "${CONFIG_AICENTER_ENABLED:-"false"}"
  },
  "apps": {
    "enabled": "${CONFIG_APPS_ENABLED:-"false"}"
  },
  "documentunderstanding": {
    "enabled": "${CONFIG_DU_ENABLED:-"false"}"
  },
  "task_mining": {
    "enabled": "${CONFIG_TASK_MINING_ENABLED:-"false"}"
  },
  "telemetry_optout": "${CONFIG_TELEMETRY_OPTOUT:-$telemetry_optout_default}",
  "fabric": {
    "redis":{
      "ha": "${CONFIG_REDIS_HA:-"false"}"
    }
  },
  "proxy": {
    "enabled": "${CONFIG_PROXY_ENABLED:-"false"}"
  }
}
EOF

  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_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

}

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_enabled_services() {
  local value

  #action_center
  value="$(get_config_value ".action_center.enabled")"
  CONFIG_ACTION_CENTER_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Action Center")

  #automation_hub
  value="$(get_config_value ".automation_hub.enabled")"
  CONFIG_AUTOMATION_HUB_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Automation Hub")

  #automation_ops
  value="$(get_config_value ".automation_ops.enabled")"
  CONFIG_AUTOMATION_OPS_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Automation Ops")

  #insights
  value="$(get_config_value ".insights.enabled")"
  CONFIG_INSIGHTS_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Insights")

  #test_manager
  value="$(get_config_value ".test_manager.enabled")"
  CONFIG_TEST_MANAGER_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Test Suite")

  #aicenter
  value="$(get_config_value ".aicenter.enabled")"
  CONFIG_AICENTER_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("AI Center")

  #apps
  value="$(get_config_value ".apps.enabled")"
  CONFIG_APPS_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Apps")

  #du
  value="$(get_config_value ".documentunderstanding.enabled")"
  CONFIG_DU_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Document Understanding")
  
  #taskmining
  value="$(get_config_value ".task_mining.enabled")"
  CONFIG_TASK_MINING_ENABLED="${value}"
  [ "${value}" == "true" ] && SERVICES_ENABLED+=("Task Mining")

  true
}

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

function read_existing_config(){
  if [[ -f "${CONFIG_PATH}" ]]; then
    CONFIG_RKE_TOKEN="$(jq -r '.rke_token // ""' "${CONFIG_PATH}")"
    CONFIG_CLUSTER_MULTINODE="$(jq -r '.multinode // ""' "${CONFIG_PATH}")"
    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_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}")"
    get_enabled_services
  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 "\n"
}

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

  [[ "${CONFIG_CLUSTER_MULTINODE}" == "false" ]] && cat <<EOF

[!] SQL connection string uses trustServerCertificate property is set to true. This setting is common for allowing
connections in test environments, such as where the SQL Server instance has only a self-signed certificate and is not
recommended for production use. To set the property to false and provide a trusted certificate via the configuration
file please refer to https://docs.uipath.com/automation-suite/docs/advanced-installation-experience.
To perform this operation upon deployment completion please refer to https://docs.uipath.com/automation-suite/docs/configuring-the-cluster.
EOF

  [[ "${CONFIG_CLUSTER_MULTINODE}" == "true" ]] && cat <<EOF

[!] SQL connection string uses trustServerCertificate property is set to false. This setting enforces validating the
SQL Server TLS certificate. Validating the server certificate is a part of the TLS handshake and ensures that the server
is the correct server to connect to. The deployment will fail unless a trusted certificate is provided via the configuration
file as described here https://docs.uipath.com/automation-suite/docs/advanced-installation-experience.
EOF

  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=$( echo "${input}" | sed -r 's/\\/\\\\/g' | sed -r 's/\o042+/\\\o042/g' )
      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] Basic (Action Center, Automation Hub, Automation Ops, Insights, Orchestrator, Test Suite)\n"
  printf "[2] Complete (Basic + AI Center, Apps, Document Understanding, Task Mining)\n"

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

    case "${input}" in
      1) break;;
      2)
        CONFIG_AICENTER_ENABLED="true";
        CONFIG_APPS_ENABLED="true";
        CONFIG_DU_ENABLED="true";
        CONFIG_TASK_MINING_ENABLED="true";
        break;;
      "") break;;
      *) printf "\nInvalid choice\n";
    esac
  done
}

function config_set_airgapped(){
  local default_choice="1"
  [[ "${CONFIG_CLUSTER_AIRGAPPED}" == "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
- 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")

EOF
}

function config_set_sql_integrated_auth_enabled(){
  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
    printf "\nInvalid choice. It must 128 characters or less.\n"
  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 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'
}

# 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}
  (( string_length!=0 && string_length < 128 )) || return 1
  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}"

  if [[ "${CONFIG_CLUSTER_AIRGAPPED}" == "true" ]];then
    BUNDLE_PATH="${OFFLINE_BUNDLE_PATH}"
  fi

  printf "\n"

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

  pending "Downloading bundle..."

  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 install_infra(){
  pending "Setting up the cluster infrastructure..."
  "${INSTALLER_ENTRYPOINT}" -i "${CONFIG_PATH}" \
          -o "${INSTALLER_OUTPUTFILE}" \
          -k \
          --accept-license-agreement \
          &>>"${INSTALLER_LOGFILE}" &

  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 \
          &>>"${INSTALLER_LOGFILE}" &

  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 \
          &>>"${INSTALLER_LOGFILE}" &

  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 \
          &>>"${INSTALLER_LOGFILE}" &

  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
    BUNDLE_PATH="${OFFLINE_BUNDLE_PATH}"
    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

  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.

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

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 "admin". Run the following command to retrieve the password:
> kubectl get secrets/argocd-admin-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" ]] || 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. 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
  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

    for f in "${frames[@]}";do
      indent; echo -ne "${f}\r"
      sleep 1s
    done

    [[ "${#status[@]}" != 0 ]] && tput cuu "${#status[@]}"
  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
  # [[ "${#status[@]}" != 0 ]] && tput cuu "${#status[@]}"
  # tput cud "${#status[@]}"
  wait "${pid}" # used to return the exit code of the process we're waiting for
}

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
  [[ "${UNATTENDED_ACTION}" =~ "accept_eula" ]] || check_eula
  [[ "${UNATTENDED_ACTION}" != "none" ]] && check_install_directory
  [[ "${UNATTENDED_ACTION}" =~ "download_bundle" ]] && download_bundle
  [[ "${UNATTENDED_ACTION}" =~ "extract_bundle" ]] && extract_bundle
  [[ "${UNATTENDED_ACTION}" =~ "install_infra" ]] && install_infra
  [[ "${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}" =~ "join_jit" ]] && join_node "jit"
  [[ "${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 "$@"

