#!/bin/bash

export PATH="${PATH}:/bin:/sbin:/usr/bin:/usr/sbin"

readonly AEMBIT_GROUP_NAME="aembit"
readonly AEMBIT_NETID_ATTESTOR_USER_NAME="aembit_netid_attestor"
readonly AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT="aembit_netid_attestor.service"
readonly AEMBIT_NETID_ATTESTOR_VERSION="1.27.78"
readonly AEMBIT_BASE_INSTALL_DIR="/opt/aembit"
readonly AEMBIT_NETID_ATTESTOR_INSTALL_DIR="${AEMBIT_BASE_INSTALL_DIR}/edge/netid_attestor"
readonly AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BASE="${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}"/"${AEMBIT_NETID_ATTESTOR_VERSION}"
readonly AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BIN="${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BASE}"/bin
readonly AEMBIT_NETID_ATTESTOR_INSTALL_DIR_SCRIPTS="${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BASE}"/scripts
readonly AEMBIT_NETID_ATTESTOR_JOURNALD_CONFIG_FILE="/etc/systemd/journald@aembit_netid_attestor.conf"
readonly AEMBIT_NETID_ATTESTOR_VERSION_PATTERN="[[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
readonly AEMBIT_NETID_ATTESTOR_BIN="aembit_netid_attestor"

INSTALLER_DIR=$(dirname "${0}")
readonly LOG_FILE="${INSTALLER_DIR}"/installer.log

readonly SYSTEMD_UNIT_FILES_FOLDER="/etc/systemd/system"
readonly AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH="${SYSTEMD_UNIT_FILES_FOLDER}"/"${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}"
readonly MIN_SYSTEMD_VERSION_WITH_NAMESPACE_JOURNALS=245

# OPTIONS
TO_UPGRADE=false

parse_options() {
    for _ in $(getopt --options "" --longoptions "upgrade" -- "${@}"); do
        case $1 in
            --upgrade)
                TO_UPGRADE=true ;;
        esac
        shift
    done

}

log() {
    local level="${1}"
    shift

    local log_fmt="%s %s\n"
    if [ "${level}" ]; then
        log_fmt="%s ${level} %s\n"
    fi

    for line in "$@"; do
        # shellcheck disable=SC2059
        printf "${log_fmt}" "$(date +"%H:%M:%S")" "${line}" | tee -a "${LOG_FILE}"
    done
}

log_info() {
    log "Info:" "$@"
}

log_warn() {
    log "Warning:" "$@"
}

log_err() {
    log "Error:" "$@"
}

check_running_as_root() {
    if [ "$(id --user)" -ne 0 ]; then
        log_err "Installer must be run as root."
        exit 1
    fi
}

check_user_file_access() {
    user=$1
    file_path=$2

    if sudo -u "$user" test -r "$file_path" && sudo -u "$user" test -x "$file_path"; then
        return 0
    else
        log_err "User '${user}' does not have access to ${file_path}"
        return 1
    fi
}

check_for_required_env_var() {
    env_var_name=$1
    env_var_value="${!env_var_name}"

    if [ -z "${env_var_value}" ]; then
        log_err "Required environment variable \"${env_var_name}\" is not defined."
        exit 1
    fi
}

check_for_optional_env_vars() {
    until [ -z "$1" ]; do
        env_var_name=$1
        # `env_var_value` contents will be `unset` if the var is unset and `null` if it's set but null/empty string
        env_var_value="${!env_var_name-unset}"
        shift

        if [ -z "${env_var_value}" ]; then
            log_err "Optional environment variable \"${env_var_name}\" is set to null but a value is required."
            exit 1
        fi
    done
}

check_if_binary_missing() {
    binary=$1

    if ! which "${binary}" > /dev/null 2>&1; then
        if [ -z "${missing_binaries}" ]; then
            missing_binaries="${binary}"
        else
            missing_binaries="${missing_binaries}, ${binary}"
        fi
    fi
}

check_required_dependencies() {
    log_info "Checking for required package dependencies."

    check_if_binary_missing pgrep
    check_if_binary_missing grep
    check_if_binary_missing id
    check_if_binary_missing getent
    check_if_binary_missing useradd
    check_if_binary_missing groupadd

    if [ -n "${missing_binaries}" ]; then
        log_err "One or more necessary binaries were missing. Please install the following from the appropriate packages and then rerun the installer: ${missing_binaries}"
        exit 1
    fi
}


netid_attestor_is_already_installed() {
    if ! [ -d "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}" ]; then
        return 1
    fi

    for file in ${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/${AEMBIT_NETID_ATTESTOR_VERSION_PATTERN}; do
        # If there is no match, $file just refers to the pattern, instead of a
        # file name. No file exists with the name of the pattern literal.
        if [ -e "${file}" ]; then
            return 0
        else
            return 1
        fi
    done
}

netid_attestor_is_already_running() {
    if pgrep --euid "${AEMBIT_NETID_ATTESTOR_USER_NAME}" &>/dev/null; then
        return 0
    else
        return 1
    fi
}

# Whether the installer is being run in upgrade mode.
is_upgrade() {
    if [ "${TO_UPGRADE}" == "true" ]; then
        return 0
    else
        return 1
    fi
}

# Exits 0 if the current platform is $1, else exits with 1.
#
# Examples:
# * is_platform rhel
# * is_platform ubuntu
# * is_platform "Red Hat Enterprise Linux"
# * is_platform Jammy
is_platform() {
    platform=$1
    val=$(cat /etc/*-release | grep -c "$platform")
    if [[ $val -gt 0 ]]; then
        return 0
    else
        return 1
    fi
}

handle_existing_netid_attestor() {
    if (netid_attestor_is_already_running || netid_attestor_is_already_installed) && ! is_upgrade; then
        log_err "Aembit NetID Attestor is already installed. Please uninstall it first or use the \"--upgrade\" option."
        exit 1
    fi
}

create_user_and_group() {
    if getent group "${AEMBIT_GROUP_NAME}" > /dev/null 2>&1; then
        log_info "Group \"${AEMBIT_GROUP_NAME}\" exists"
    else
        log_info "Group \"${AEMBIT_GROUP_NAME}\" does not exist. Creating."
        if groupadd "${AEMBIT_GROUP_NAME}"; then
            log_info "Group created"
        else
            log_err "Error creating the \"${AEMBIT_GROUP_NAME}\" group."
            exit 1
        fi
    fi

    if getent passwd "${AEMBIT_NETID_ATTESTOR_USER_NAME}" > /dev/null 2>&1; then
        log_info "User \"${AEMBIT_NETID_ATTESTOR_USER_NAME}\" exists"
    else
        log_info "User \"${AEMBIT_NETID_ATTESTOR_USER_NAME}\" does not exist. Creating."
        if useradd --system --shell /bin/bash  --gid "${AEMBIT_GROUP_NAME}" "${AEMBIT_NETID_ATTESTOR_USER_NAME}"; then
            log_info "User created"
        else
            log_err "There was an error creating the \"${AEMBIT_NETID_ATTESTOR_USER_NAME}\" user."
            exit 1
        fi
    fi
}

set_file_permissions() {
    file_path=$1
    permissions=$2

    if ! chmod "${permissions}" "${file_path}"; then
        log_err "Unable to set the permissions of ${file_path}."
        exit 1
    fi
}

set_file_user_and_group_owner() {
    file_path=$1
    user=$2
    group=$3

    if ! chown "${user}:${group}" "${file_path}"; then
        log_err "Unable to set the ownership of ${file_path}."
        exit 1
    fi
}

set_recursive_file_permissions() {
    file_path=$1
    permissions=$2

    if ! chmod --recursive "${permissions}" "${file_path}"; then
        log_err "Unable to set the permissions of ${file_path}."
        exit 1
    fi
}

set_recursive_file_user_and_group_owner() {
    file_path=$1
    user=$2
    group=$3

    if ! chown --recursive "${user}:${group}" "${file_path}"; then
        log_err "Unable to set the ownership of ${file_path}."
        exit 1
    fi
}

# Sets the group owner and permissions for all directories from $base_dir to $leaf_dir, inclusive.
set_group_owner_and_permissions_in_range() {
    base_dir=$1
    leaf_dir=$2
    perms=$3
    group=$4

    current_dir="${leaf_dir}"
    while true; do
        chgrp "${group}" "${current_dir}"
        chmod "${perms}" "${current_dir}"
        if [ "${current_dir}" = "${base_dir}" ]; then
            break
        fi
        current_dir="$(dirname "${current_dir}")"
    done
}

# get systemd version using systemctl
get_systemd_version() {
    systemd_version_output=$(systemctl --version)
    echo "$systemd_version_output" | grep -oP 'systemd \K\d+' | tr -dc '0-9'
}

set_up_logs() {
    # From "man systemd-journald.service"
    # "On systems where /var/log/journal/ does not exist yet but where persistent logging is
    #  desired (and the default journald.conf is used), it is sufficient to create the
    #  directory, and ensure it has the correct access modes and ownership:"
    if [ ! -d "/var/log/journal" ]; then
        mkdir -p /var/log/journal
        chgrp systemd-journal /var/log/journal
        if ! systemd-tmpfiles --create --prefix /var/log/journal; then
            exit 1
        fi
    fi

    if [ "$(get_systemd_version)" -ge "${MIN_SYSTEMD_VERSION_WITH_NAMESPACE_JOURNALS}" ]; then
        if ! cp "${INSTALLER_DIR}"/installer_components/journald@aembit_netid_attestor.conf "${AEMBIT_NETID_ATTESTOR_JOURNALD_CONFIG_FILE}"; then
            log_err "Unable to copy the journald configuration file to host."
            exit 1
        fi

        set_file_permissions "${AEMBIT_NETID_ATTESTOR_JOURNALD_CONFIG_FILE}" 400 || exit 1
        set_file_user_and_group_owner "${AEMBIT_NETID_ATTESTOR_JOURNALD_CONFIG_FILE}" root root || exit 1
    fi
}

copy_static_components_to_host() {
    if ! mkdir --parents "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BIN}"; then
        log_err "Unable to make installation directory for NetID Attestor binary."
        exit 1
    fi

    if ! cp "${INSTALLER_DIR}"/aembit_netid_attestor "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BIN}"/"${AEMBIT_NETID_ATTESTOR_BIN}"; then
        log_err "Unable to copy the NetID Attestor binary to the installation directory."
        exit 1
    fi

    if ! mkdir --parents "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_SCRIPTS}"; then
        log_err "Unable to make installation directory for NetID Attestor scripts."
        exit 1
    fi

    if ! cp "${INSTALLER_DIR}"/uninstall "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_SCRIPTS}"/uninstall; then
        log_err "Unable to copy the NetID Attestor uninstall script to the installation directory."
        exit 1
    fi

    # Ensure the installed files are accessible throughout the installation dir structure.
    # Needed when installing into environments with very restrictive umasks.
    set_group_owner_and_permissions_in_range "${AEMBIT_BASE_INSTALL_DIR}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/.." 550 "${AEMBIT_GROUP_NAME}"
    set_recursive_file_permissions "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}" 500
    set_recursive_file_user_and_group_owner "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}" "${AEMBIT_NETID_ATTESTOR_USER_NAME}" "${AEMBIT_GROUP_NAME}"
}

replace_in_service_unit() {
  template_text="${1?:replace_in_service_unit requires a template pattern arg}"
  replacement_text="${2?:replace_in_service_unit requires replacement text arg}"
  sed --in-place "s#${template_text}#${replacement_text}#" "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}"
}

OPTIONAL_ENV_VARS=("AEMBIT_NETID_LISTENER_IP" "NETID_LISTENER_PORT" "AEMBIT_LOG_LEVEL" "AEMBIT_VCENTER_SESSION_TIMEOUT_SECS")

install_systemd_service() {
    if ! cp "${INSTALLER_DIR}"/installer_components/aembit_netid_attestor.service "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}"; then
        log_err "Unable to copy the systemd service file to the destination."
        exit 1
    fi

    set_file_permissions "${SYSTEMD_UNIT_FILES_FOLDER}/${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}" 400 || exit 1
    set_file_user_and_group_owner "${SYSTEMD_UNIT_FILES_FOLDER}/${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}" root root || exit 1

    if ! replace_in_service_unit "{{ AEMBIT_VCENTER_URL }}" "${AEMBIT_VCENTER_URL}"; then
        log_err "Unable to update the NodeDB vCenter URL in the systemd service file."
        exit 1
    fi

    if ! replace_in_service_unit "{{ INSTALLED_TLS_PEM_PATH }}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.crt"; then
        log_err "Unable to update the TLS certificate path in the systemd service file."
        exit 1
    fi

    if ! replace_in_service_unit "{{ INSTALLED_TLS_KEY_PATH }}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.key"; then
        log_err "Unable to update the TLS key path in the systemd service file."
        exit 1
    fi

    # Default to username if not set
    aembit_log_namespace_value="${AEMBIT_LOG_NAMESPACE:-${AEMBIT_NETID_ATTESTOR_USER_NAME}}"
    if ! replace_in_service_unit "{{ AEMBIT_LOG_NAMESPACE }}" "${aembit_log_namespace_value}"; then
        log_err "Unable to update optional log level in the systemd service file."
        exit 1
    fi

    for env_var_name in "${OPTIONAL_ENV_VARS[@]}"; do
        if [ -n "${!env_var_name}" ]; then
            echo "Environment=${env_var_name}=${!env_var_name}" >> "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}"
        fi
    done

    if ! systemctl enable "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}"; then
        log_err "Unable to enable the NetID Attestor systemd service."
        exit 1
    fi
}

start_systemd_service() {
    if ! systemctl start "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}"; then
        log_err "Unable to start the NetID Attestor systemd service."
        exit 1
    fi
}

upgrade_systemd_service() {
    # If the NetID Attestor service is not running or not loaded, continue
    # with the upgrade process.
    systemctl stop "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}" 2> /dev/null

    install_systemd_service || exit 1

    if ! systemctl daemon-reload; then
        log_err "Unable to reload the NetID Attestor systemd service."
        exit 1
    fi

    if ! systemctl start "${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT}"; then
        log_err "Unable to start the updated NetID Attestor systemd service."
        exit 1
    fi

    if ! systemctl restart systemd-journald; then
        log_err "Unable to restart journald with the updated log configuration."
        exit 1
    fi
}

remove_all_previous_installations() {
    log_info "Deleting old NetID Attestor versions."
    for dir in ${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/${AEMBIT_NETID_ATTESTOR_VERSION_PATTERN}; do
        case "${dir}" in
            # Don't delete the upgraded NetID Attestor version.
            *"${AEMBIT_NETID_ATTESTOR_VERSION}"*)
                # Do nothing
                ;;
            *)
                rm -rf "${dir}"
                ;;
        esac
    done
}

# Read and set environment variables based upon the environment variables of the
# installed agent proxy service.
# These environment variables can be overwritten by the ones passed as part of the
# installation script.
read_env_vars_from_service() {
    env_vars=$(systemctl show -p Environment ${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT} | cut -d '=' -f 2-)

    # Split the string into an array
    IFS=' ' read -r -a env_array <<< "$env_vars"

    # Iterate over the array and set all the environment values which are not
    # set.
    for var in "${env_array[@]}"; do
        # Split each element into variable name and value
        IFS='=' read -r key value <<< "$var"
        if [[ ! -v "${key}" ]] && [ -n "$value" ]; then
            export "$key"="$value"
        fi
    done
}

check_valid_url() {
    env_var_name=$1
    env_var_value="${!env_var_name}"
    http_regex='^http(s)?://([a-zA-Z0-9.-]+)(:[0-9]+)?$'

    if [[ ! $env_var_value =~ $http_regex ]]; then
        log_err "The value of $env_var_name should be in the format: \"http[s]://HOST:PORT\"."
        exit 1
    fi
}

check_file_exists() {
    until [ -z "$1" ]; do
        env_var_name=$1
        # `env_var_value` contents will be `unset` if the var is unset and `null` if it's set but null/empty string
        env_var_value="${!env_var_name}"
        shift

        # Check if a file does not exist at the specified path
        if [ ! -f "$env_var_value" ]; then
            log_err "File not found at the path specified by $env_var_name: $env_var_value"
            exit 1
        fi
    done
}

get_credential_string() {
    # Read from file
    if [ -n "$AEMBIT_VCENTER_CREDENTIALS_FILE" ]; then
        check_file_exists AEMBIT_VCENTER_CREDENTIALS_FILE
        VCENTER_CREDENTIAL_STRING=$(cat "$AEMBIT_VCENTER_CREDENTIALS_FILE")
    # Prompt for credentials
    elif [ -t 0 ]; then
      username=$(systemd-ask-password "Enter vCenter API Username")
      password=$(systemd-ask-password "Enter vCenter API Password")

      if [ -z "$username" ] || [ -z "$password" ]; then
          log_err "Both username and password must be provided." >&2
          exit 1
      fi

      VCENTER_CREDENTIAL_STRING="$username:$password"
    # Read from stdin
    else
        read -r VCENTER_CREDENTIAL_STRING
    fi

    if [ -z "$VCENTER_CREDENTIAL_STRING" ] || ! [[ "$VCENTER_CREDENTIAL_STRING" == *":"* ]]; then
        log_err "vCenter API credentials were either not provided or are in an invalid format." >&2
        echo "Expected 'username:password'." >&2
        exit 1
    fi
}

create_systemd_vcenter_credentials() {
    sudo mkdir -p "$AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH.d"
    echo "$VCENTER_CREDENTIAL_STRING" | ( echo "[Service]" && systemd-creds encrypt --name=vcenter_credentials --with-key=auto --pretty - - ) >"${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}.d/50-credentials.conf"
}

create_systemd_certificate_credentials() {
    ( systemd-creds encrypt --name=attestation_signing_certificate --with-key=auto --pretty - - ) < "$AEMBIT_ATTESTATION_SIGNING_CERTIFICATE_PATH" >>"${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}.d/50-credentials.conf"
    ( systemd-creds encrypt --name=attestation_signing_key --with-key=auto --pretty - - ) < "$AEMBIT_ATTESTATION_SIGNING_KEY_PATH" >>"${AEMBIT_NETID_ATTESTOR_SYSTEMD_UNIT_PATH}.d/50-credentials.conf"
}

copy_tls_cert() {
    if [[ -n "${TLS_PEM_PATH}" && -n "${TLS_KEY_PATH}" ]]; then
        if [[ -f "${TLS_PEM_PATH}" && -f "${TLS_KEY_PATH}" ]]; then
            cp "${TLS_PEM_PATH}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.crt"
            cp "${TLS_KEY_PATH}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.key"
            set_file_user_and_group_owner "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.crt" "${AEMBIT_NETID_ATTESTOR_USER_NAME}" "${AEMBIT_GROUP_NAME}"
            set_file_user_and_group_owner "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.key" "${AEMBIT_NETID_ATTESTOR_USER_NAME}" "${AEMBIT_GROUP_NAME}"
            set_file_permissions "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.crt" 400
            set_file_permissions "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR}/tls.key" 400
            log_info "Copied TLS pem and key."
        else
            log_info "TLS_PEM_PATH and/or TLS_KEY_PATH are not valid."
        fi
    else
        log_info "Both TLS_PEM_PATH and TLS_KEY_PATH must be set for TLS."
    fi
}

run_installer() {
    check_running_as_root || exit 1
    parse_options "${@}"

    if is_upgrade; then
        read_env_vars_from_service
    fi

    check_for_required_env_var AEMBIT_VCENTER_URL AEMBIT_ATTESTATION_SIGNING_CERTIFICATE_PATH AEMBIT_ATTESTATION_SIGNING_KEY_PATH AEMBIT_VCENTER_CREDENTIALS_FILE TLS_PEM_PATH TLS_KEY_PATH || exit 1
    check_valid_url AEMBIT_VCENTER_URL || exit 1
    check_file_exists AEMBIT_ATTESTATION_SIGNING_CERTIFICATE_PATH AEMBIT_ATTESTATION_SIGNING_KEY_PATH TLS_PEM_PATH TLS_KEY_PATH || exit 1
    check_for_optional_env_vars AEMBIT_LOG_NAMESPACE || exit 1
    check_required_dependencies || exit 1
    get_credential_string || exit 1
    create_systemd_vcenter_credentials || exit 1
    create_systemd_certificate_credentials || exit 1
    handle_existing_netid_attestor || exit 1
    create_user_and_group || exit 1
    set_up_logs || exit 1
    copy_static_components_to_host || exit 1
    copy_tls_cert || exit 1
    check_user_file_access "${AEMBIT_NETID_ATTESTOR_USER_NAME}" "${AEMBIT_NETID_ATTESTOR_INSTALL_DIR_BIN}/${AEMBIT_NETID_ATTESTOR_BIN}"  || exit 1

    if is_upgrade; then
        upgrade_systemd_service || exit 1
        remove_all_previous_installations || exit 1
    else
        install_systemd_service || exit 1
        start_systemd_service || exit 1
    fi
}

main() {
    set -o pipefail
    log_info "Installing Aembit NetID Attestor"
    run_installer "${@}" 2>&1
}

main "${@}"|| exit 1
