#!/bin/bash

#============================================================================
#
#  Copyright (c) 2021 Microsoft Corporation.  All rights reserved.
#
#  Abstract:
#    MDE installation script 
#    - Fingerprinting OS and manually installs MDE as described in the online documentation
#      https://learn.microsoft.com/en-us/defender-endpoint/linux-install-manually?view=o365-worldwide
#    - Runs additional optional checks: minimal requirements, fanotify subscribers, etc.
#
#============================================================================

SCRIPT_VERSION="0.8.3" # MDE installer version set this to track the changes in the script used by tools like ansible, MDC etc.
ASSUMEYES=-y
CHANNEL=
MDE_VERSION=
DISTRO=
DISTRO_FAMILY=
ARCHITECTURE=
PKG_MGR=
INSTALL_MODE=
DEBUG=0
VERBOSE=
PMC_URL=https://packages.microsoft.com/config
SCALED_VERSION=
VERSION=
ONBOARDING_SCRIPT=
OFFBOARDING_SCRIPT=
MIN_REQUIREMENTS=
SKIP_CONFLICTING_APPS=
PASSIVE_MODE=
RTP_MODE=
MIN_CORES=1
MIN_MEM_MB=1024
MIN_DISK_SPACE_MB=2048
declare -A tags

# Error codes
SUCCESS=0
ERR_INTERNAL=1
ERR_INVALID_ARGUMENTS=2
ERR_INSUFFICIENT_PRIVILAGES=3
ERR_CONFLICTING_APPS=5
ERR_UNSUPPORTED_DISTRO=10
ERR_UNSUPPORTED_VERSION=11
ERR_INSUFFICIENT_REQUIREMENTS=12
ERR_CORRUPT_MDE_INSTALLED=15
ERR_MDE_NOT_INSTALLED=20
ERR_INSTALLATION_FAILED=21
ERR_UNINSTALLATION_FAILED=22
ERR_FAILED_DEPENDENCY=23
ERR_FAILED_REPO_SETUP=24
ERR_INVALID_CHANNEL=25
ERR_FAILED_REPO_CLEANUP=26
ERR_ONBOARDING_NOT_FOUND=30
ERR_ONBOARDING_FAILED=31
ERR_OFFBOARDING_NOT_FOUND=32
ERR_OFFBOARDING_FAILED=33
ERR_TAG_NOT_SUPPORTED=40
ERR_PARAMETER_SET_FAILED=41
ERR_INSTALL_PATH_SETUP=51
ERR_INSTALL_PATH_PERMISSIONS=52

# Predefined values
export DEBIAN_FRONTEND=noninteractive

_log() {
    level="$1"
    dest="$2"
    msg="${*:3}"
    ts=$(date -u +"%Y-%m-%dT%H:%M:%S")

    if [ "$dest" = "stdout" ]; then
       echo "$msg"
    elif [ "$dest" = "stderr" ]; then
       >&2 echo "$msg"
    fi

    if [ -n "$log_path" ]; then
       echo "$ts $level $msg" >> "$log_path"
    fi
}

log_debug() {
    _log "DEBUG" "stdout" "$@"
}

log_info() {
    _log "INFO " "stdout" "$@"
}

log_warning() {
    _log "WARN " "stderr" "$@"
}

log_error() {
    _log "ERROR" "stderr" "$@"
}

script_exit()
{
    if [ -z "$1" ]; then
        log_error "[!] INTERNAL ERROR. script_exit requires an argument"
        exit $ERR_INTERNAL
    fi

    if [ "$DEBUG" != "0" ]; then
        print_state
    fi

    if [ "$2" = "0" ]; then
        log_info "[v] $1"
    else
	    log_error "[x] $1"
    fi

    if [ -z "$2" ]; then
        exit $ERR_INTERNAL
    elif ! [ "$2" -eq "$2" ] 2> /dev/null; then #check error is number
        exit $ERR_INTERNAL
    else
        log_info "[*] exiting ($2)"
        cleanup $2
	    exit $2
    fi
}

get_python() {
   if which python3 &> /dev/null; then
      echo "python3"
   elif which python2 &> /dev/null; then
      echo "python2"
   else
      echo "python"
   fi
}


parse_uri() {
   cat <<EOF | /usr/bin/env "$(get_python)"
import sys

if sys.version_info < (3,):
   from urlparse import urlparse
else:
   from urllib.parse import urlparse

uri = urlparse("$1")
print(uri.scheme or "")
print(uri.hostname or "")
print(uri.port or "")
EOF
}

get_rpm_proxy_params() {
    proxy_params=""
    if [ -n "$http_proxy" ]; then
	    proxy_host=$(parse_uri "$http_proxy" | sed -n '2p')
        if [ -n "$proxy_host" ];then
           proxy_params="$proxy_params --httpproxy $proxy_host"
        fi

	    proxy_port=$(parse_uri "$http_proxy" | sed -n '3p')
        if [ -n "$proxy_port" ]; then
           proxy_params="$proxy_params --httpport $proxy_port"
        fi
    fi
    if [ -n "$ftp_proxy" ];then
       proxy_host=$(parse_uri "$ftp_proxy" | sed -n '2p')
       if [ -n "$proxy_host" ];then
          proxy_params="$proxy_params --ftpproxy $proxy_host"
       fi

       proxy_port=$(parse_uri "$ftp_proxy" | sed -n '3p')
       if [ -n "$proxy_port" ]; then
          proxy_params="$proxy_params --ftpport $proxy_port"
       fi
    fi
    echo $proxy_params
}

cleanup()
{
    # If installation failed in case of custom installation, delete symlink
    if [ "$1" = "$ERR_INSTALLATION_FAILED" ] && [ -n "$INSTALL_PATH" ]; then
        local mdatp_symlink="/opt/microsoft/mdatp"
        delete_sym_link "$mdatp_symlink"
    fi
}

run_quietly()
{
    # run_quietly <command> <error_msg> [<error_code>]
    # use error_code for script_exit

    if [ $# -lt 2 ] || [ $# -gt 3 ]; then
        log_error "[!] INTERNAL ERROR. run_quietly requires 2 or 3 arguments"
        exit 1
    fi

    local out exit_code 

    out=$(eval $1 2>&1; echo "$?")
    exit_code=$(echo "$out" | tail -n1)

    if [ -n "$VERBOSE" ]; then
        log_info "$out"
    fi
    
    if [ "$exit_code" != "0" ]; then
        if [ "$DEBUG" != "0" ]; then
            log_debug "[>] Running command: $1"
            log_debug "[>] Command output: $out"
            log_debug "[>] Command exit_code: $exit_code"
        fi

        if [ $# -eq 2 ]; then
            log_error "$2"
        else
            script_exit "$2" "$3"
        fi
    fi

    return "$exit_code"
}

retry_quietly()
{
    # retry_quietly <retries> <command> <error_msg> [<error_code>]
    # use error_code for script_exit
    
    if [ $# -lt 3 ] || [ $# -gt 4 ]; then
        log_error "[!] INTERNAL ERROR. retry_quietly requires 3 or 4 arguments"
        exit 1
    fi

    local exit_code=
    local retries=$1

    while [ $retries -gt 0 ]
    do

        if run_quietly "$2" "$3"; then
            exit_code=0
        else
            exit_code=1
        fi
        
        if [ $exit_code -ne 0 ]; then
            sleep 1
            ((retries--))
            log_info "[r] $(($1-$retries))/$1"
        else
            retries=0
        fi
    done

    if [ $# -eq 4 ] && [ $exit_code -ne 0 ]; then
        script_exit "$3" "$4"
    fi

    return $exit_code
}

get_health_field()
{
    # get_health_field <field>
    # get the health field from mdatp health --field <field>
    # extract the value from the output and return it

    if ! command -v mdatp >/dev/null 2>&1; then
        return 1
    fi

    local val
    val=$(mdatp health --field "$1" 2>/dev/null)
    cmd_status=$?
    if [ $cmd_status -ne 0 ]; then
        return 1
    fi
    clean_output=$(echo "$val" | sed '1{/^ATTENTION/d}' | sed 's/^"\(.*\)"$/\1/')
    echo "$clean_output"
    return 0
}

print_state()
{
    if ( ! check_if_pkg_is_installed mdatp ) && ( ! command -v mdatp >/dev/null 2>&1 ); then
        log_warning "[S] MDE not installed."
    else
        log_info "[S] MDE installed."
        if run_quietly "mdatp health" "[S] Could not connect to the daemon -- MDE is not ready to connect yet."; then
            log_info "[S] Version: $(get_health_field "app_version")"
            log_info "[S] Onboarded: $(get_health_field "licensed")"
            log_info "[S] Passive mode: $(get_health_field "passive_mode_enabled")"
            log_info "[S] Device tags: $(get_health_field "edr_device_tags")"
            log_info "[S] Subsystem: $(get_health_field "real_time_protection_subsystem")"
            log_info "[S] Conflicting applications: $(get_health_field "conflicting_applications")"
        fi
    fi
}

detect_arch()
{
    arch=$(uname -m)
    ARCHITECTURE=$arch
    log_info "[v] detected: $ARCHITECTURE architecture"
}

detect_distro()
{
    if [ -f /etc/os-release ] || [ -f /etc/mariner-release ]; then
        . /etc/os-release
        DISTRO=$ID
        VERSION=$VERSION_ID
        VERSION_NAME=$VERSION_CODENAME
    elif [ -f /etc/redhat-release ]; then
        if [ -f /etc/oracle-release ]; then
            DISTRO="ol"
        elif [[ $(grep -o -i "Red\ Hat" /etc/redhat-release) ]]; then
            DISTRO="rhel"
        elif [[ $(grep -o -i "Centos" /etc/redhat-release) ]]; then
            DISTRO="centos"
        fi
        VERSION=$(grep -o "release .*" /etc/redhat-release | cut -d ' ' -f2)
    else
        script_exit "unable to detect distro" $ERR_UNSUPPORTED_DISTRO
    fi

    # change distro to ubuntu for linux mint support
    if [ "$DISTRO" = "linuxmint" ]; then
        DISTRO="ubuntu"
    fi

    if [ "$DISTRO" = "debian" ] || [ "$DISTRO" = "ubuntu" ]; then
        DISTRO_FAMILY="debian"
    elif [ "$DISTRO" = "rhel" ] || [ "$DISTRO" = "centos" ] || [ "$DISTRO" = "ol" ] || [ "$DISTRO" = "fedora" ] || [ "$DISTRO" = "amzn" ] || [ "$DISTRO" = "almalinux" ] || [ "$DISTRO" = "rocky" ]; then
        DISTRO_FAMILY="fedora"
    elif [ "$DISTRO" = "mariner" ]; then
        DISTRO_FAMILY="mariner"
    elif [ "$DISTRO" = "azurelinux" ]; then
        DISTRO_FAMILY="azurelinux"
    elif [ "$DISTRO" = "sles" ] || [ "$DISTRO" = "sle-hpc" ] || [ "$DISTRO" = "sles_sap" ]; then
        DISTRO_FAMILY="sles"
    else
        script_exit "unsupported distro $DISTRO $VERSION" $ERR_UNSUPPORTED_DISTRO
    fi

    log_info "[v] detected: $DISTRO $VERSION $VERSION_NAME ($DISTRO_FAMILY)"
}

verify_channel()
{
    if [ "$CHANNEL" != "prod" ] && [ "$CHANNEL" != "insiders-fast" ] && [ "$CHANNEL" != "insiders-slow" ]; then
        script_exit "Invalid channel: $CHANNEL. Please provide valid channel. Available channels are prod, insiders-fast, insiders-slow" $ERR_INVALID_CHANNEL
    fi
}

verify_privileges()
{
    if [ -z "$1" ]; then
        script_exit "Internal error. verify_privileges require a parameter" $ERR_INTERNAL
    fi

    if [ "$(id -u)" -ne 0 ]; then
        script_exit "root privileges required to perform $1 operation" $ERR_INSUFFICIENT_PRIVILAGES
    fi
}

verify_min_requirements()
{
    # verifying minimal reuirements: $MIN_CORES cores, $MIN_MEM_MB MB RAM, $MIN_DISK_SPACE_MB MB disk space
    
    local cores mem_mb disk_space_mb

    cores=$(nproc --all)
    if [ "$cores" -lt $MIN_CORES ]; then
        script_exit "MDE requires $MIN_CORES cores or more to run, found $cores." $ERR_INSUFFICIENT_REQUIREMENTS
    fi

    mem_mb=$(free -m | grep Mem | awk '{print $2}')
    if [ "$mem_mb" -lt $MIN_MEM_MB ]; then
        script_exit "MDE requires at least $MIN_MEM_MB MB of RAM to run. found $mem_mb MB." $ERR_INSUFFICIENT_REQUIREMENTS
    fi

    disk_space_mb=$(df -m . | tail -1 | awk '{print $4}')
    if [ "$disk_space_mb" -lt $MIN_DISK_SPACE_MB ]; then
        script_exit "MDE requires at least $MIN_DISK_SPACE_MB MB of free disk space for installation. found $disk_space_mb MB." $ERR_INSUFFICIENT_REQUIREMENTS
    fi

    log_info "[v] minimal requirements met"
}

find_service()
{
    if [ -z "$1" ]; then
        script_exit "INTERNAL ERROR. find_service requires an argument" $ERR_INTERNAL
    fi

	lines=$(systemctl status $1 2>&1 | grep "Active: active" | wc -l)
	
    if [ $lines -eq 0 ]; then
		return 1
	fi

	return 0
}

verify_mdatp_installed()
{
    op=$(command -v mdatp)
            #make sure mdatp is installed
    if [ ! -z $op ]; then
        #check if mdatp is onboarded or not
        check_missing_license=$(get_health_field "health_issues" | grep "missing license" -c)
        onboard_file=/etc/opt/microsoft/mdatp/mdatp_onboard.json
        if [ "$check_missing_license" -gt 0 ] || [ ! -f "$onboard_file" ]; then
            log_info "[i] MDE already installed but not onboarded. Please use --onboard command to onboard the product."
        else
            current_mdatp_version=$(get_health_field "app_version")
            org_id=$(get_health_field "org_id")          
            if [ ! -z "$MDE_VERSION" ]; then
                local current_version requested_version
                current_version=$(echo "$current_mdatp_version" | sed 's/"//' | awk '{print $NF}' | awk -F. '{ printf("%d%05d%05d\n", $1,$2,$3); }')
                requested_version=$(echo "$MDE_VERSION" | awk -F. '{ printf("%d%05d%05d\n", $1,$2,$3); }')

                if [[ "$current_version" -lt "$requested_version" ]]; then
                    log_info "[i] Found MDE version $current_mdatp_version already installed and onboarded with org_id $org_id. To install newer version please use --upgrade option"
                elif [[ "$current_version" -gt "$requested_version" ]]; then
                    log_info "[i] Found MDE version $current_mdatp_version already installed and onboarded with org_id $org_id. To install older version please use --downgrade option"
                else
                    log_info "[i] The requested MDE version $current_mdatp_version already installed and onboarded with org_id $org_id."
                fi
            else
                log_info "[i] Found MDE already installed and onboarded with org_id $org_id and app_version $current_mdatp_version. Either try to upgrade/downgrade your MDE version using --upgrade/--downgrade option or Please verify that the onboarded linux server appears in Microsoft 365 Defender."
            fi
        fi
    else
        script_exit "Seems like, previous version of MDE is corrupted. Please, first try to uninstall the previous version of MDE using --remove option, aborting" $ERR_CORRUPT_MDE_INSTALLED
    fi
}

verify_conflicting_applications()
{
    # identifying conflicting applications (fanotify mounts)
    if ! command -v timeout >/dev/null 2>&1; then
        log_warning "[!] 'timeout' command not found. Skipping conflicting application check"
        return
    fi

    # find applications that are using fanotify
    local conflicting_apps
    conflicting_apps=$(timeout 5m find /proc/*/fdinfo/ -type f -print0 2>/dev/null \
        | xargs -r0 grep -Fl "fanotify mnt_id" 2>/dev/null \
        | xargs -I {} -r sh -c 'tr "\0" "" < "$(dirname {})/../cmdline"' 2>/dev/null)

    if [ ! -z "$conflicting_apps" ]; then
        if [ "$conflicting_apps" = "/opt/microsoft/mdatp/sbin/wdavdaemon" ]; then
            verify_mdatp_installed
        elif [[ "$conflicting_apps" =~ "fapolicyd" ]]; then
            # fapolicyd is supported with MDE in non-blocking mode
            log_warning "[!] [$conflicting_apps] is supported with MDE but for any run time conflicts, please check mdatp health output."
        else
            script_exit "found conflicting applications: [$conflicting_apps], aborting" $ERR_CONFLICTING_APPS
        fi

    fi

    # find known security services
    # | Vendor      | Service       |
    # |-------------|---------------|
    # | CrowdStrike | falcon-sensor | (removed in 0.8.2)
    # | CarbonBlack | cbsensor      |
    # | McAfee      | MFEcma        |
    # | Trend Micro | ds_agent      |

    local conflicting_services=('ds_agent' 'cbsensor' 'MFEcma')
    for t in "${conflicting_services[@]}"
    do
        set -- $t
        # locating service: $1
        if find_service $1; then
            script_exit "found conflicting service: [$1], aborting" $ERR_CONFLICTING_APPS
        fi        
    done 

    log_info "[v] no conflicting applications found"
}

set_package_manager()
{
    if [ "$DISTRO_FAMILY" = "debian" ]; then
        PKG_MGR=apt
        PKG_MGR_INVOKER="apt $ASSUMEYES"
    elif [ "$DISTRO_FAMILY" = "fedora" ]; then
        PKG_MGR=yum
        PKG_MGR_INVOKER="yum $ASSUMEYES"
    elif [ "$DISTRO_FAMILY" = "mariner" ] || [ "$DISTRO_FAMILY" = "azurelinux" ]; then
        PKG_MGR=dnf
        PKG_MGR_INVOKER="dnf $ASSUMEYES"
    elif [ "$DISTRO_FAMILY" = "sles" ]; then
        DISTRO="sles"
        PKG_MGR="zypper"
        PKG_MGR_INVOKER="zypper --non-interactive"
    else    
        script_exit "unsupported distro", $ERR_UNSUPPORTED_DISTRO
    fi

    log_info "[v] set package manager: $PKG_MGR"
}

check_if_pkg_is_installed()
{
    if [ -z "$1" ]; then
        script_exit "INTERNAL ERROR. check_if_pkg_is_installed requires an argument" $ERR_INTERNAL
    fi

    if [ "$PKG_MGR" = "apt" ]; then
        dpkg -s $1 2> /dev/null | grep Status | grep "install ok installed" 1> /dev/null
    else
        # shellcheck disable=SC2046
        rpm --quiet --query $(get_rpm_proxy_params) $1
    fi

    return $?
}

check_if_device_is_onboarded()
{
    local onboarded
    onboarded=$(get_health_field "licensed")
    if [ "$onboarded" = "true" ]; then
        return 0
    fi
    return 1
}

skip_if_mde_installed()
{
    if check_if_pkg_is_installed mdatp; then
        verify_mdatp_installed
        pkg_version=$(get_health_field "app_version") || script_exit "unable to fetch the app version. please upgrade to latest version $?" $ERR_INTERNAL
        log_info "[i] MDE already installed ($pkg_version)"
        return 0
    else
        return 1
    fi
}

exit_if_mde_not_installed()
{
    if ! check_if_pkg_is_installed mdatp; then
        script_exit "MDE package is not installed. Please install it with --install option" $ERR_MDE_NOT_INSTALLED
    fi
}

get_mdatp_version()
{
    local PKG_VERSION=""

    if [ "$DISTRO_FAMILY" = "debian" ]; then
        PKG_VERSION=$(dpkg -s mdatp | grep -i version)
    else
        PKG_VERSION=$(rpm -qi mdatp | grep -i version)
    fi

    echo $PKG_VERSION
}

get_mdatp_channel()
{
    local release_ring=""
    release_ring=$(mdatp health --field release_ring)
    if [ "$?" = "0" ] && [ -n "$release_ring" ]; then
        release_ring=$(echo "$release_ring" | tail -n 1 | awk -F'"' '{print $2}')
    else
        install_log=/var/log/microsoft/mdatp/install.log
        if [ -e "$install_log" ]; then
            release_ring=$(cat "$install_log" | grep "Release ring: " | tail -n 1 | awk -F': ' '{print $2}')
        fi
    fi

    local channel=""
    if [[ "$release_ring" == *"Production"* ]]; then
        channel="prod"
    elif [[ "$release_ring" == *"InsiderFast"* ]]; then
        channel="insiders-fast"
    elif [[ "$release_ring" == *"External"* ]]; then
        channel="insiders-slow"
    else
        channel="dogfood"
    fi

    echo $channel
}

install_required_pkgs()
{
    local packages=()
    local pkgs_to_be_installed=
    local exit_on_failure=1  # Default: exit on failure

    if [ -z "$1" ]; then
        script_exit "INTERNAL ERROR. install_required_pkgs requires an argument" $ERR_INTERNAL
    fi

    if [[ "$1" == "--no-exit" ]]; then
        exit_on_failure=0
        shift 1  # Remove the flag from arguments
    fi

    packages=("$@")
    for pkg in "${packages[@]}"
    do
        if  ! check_if_pkg_is_installed $pkg; then
            pkgs_to_be_installed="$pkgs_to_be_installed $pkg"
        fi
    done

    if [ ! -z "$pkgs_to_be_installed" ]; then
        log_info "[>] installing $pkgs_to_be_installed"

        if [ "$exit_on_failure" -eq 1 ]; then
            run_quietly "$PKG_MGR_INVOKER install $pkgs_to_be_installed" "Unable to install the required packages ($?)" $ERR_FAILED_DEPENDENCY
        else
            run_quietly "$PKG_MGR_INVOKER install $pkgs_to_be_installed" "Unable to install the required packages ($?)"
        fi
    else
        log_info "[v] required pkgs are installed"
    fi
}

wait_for_package_manager_to_complete()
{
    local lines=
    local counter=120

    while [ $counter -gt 0 ]
    do
        lines=$(ps axo pid,comm | grep "$PKG_MGR" | grep -v grep -c)
        if [ "$lines" -eq 0 ]; then
            log_info "[>] package manager freed, resuming installation"
            return
        fi
        sleep 1
        ((counter--))
    done

    log_info "[!] pkg_mgr blocked"
}

validate_mde_version()
{
    if ! [[ "$MDE_VERSION" =~ ^101\.[0-9]{1,5}\.[0-9]{4}$ ]]; then
        echo ""
        return 1
    fi

    local sep='_'
    local suffix='-1'
    local prefix='-'
    if [ "$DISTRO_FAMILY" = "debian" ]; then
        sep='-'
        suffix=''
        prefix='='
    fi
    local version
    if [ "$CHANNEL" = "insiders-fast" ]; then
        version="${MDE_VERSION}${sep}insiderfast${suffix}"
    elif [ "$CHANNEL" = "insiders-slow" ]; then
        version="${MDE_VERSION}${sep}external${suffix}"
    else
        version="$MDE_VERSION"
    fi

    local search_command
    if [ "$PKG_MGR" = "apt" ]; then
        search_command='apt $ASSUMEYES policy mdatp 2>/dev/null | grep "$version" &> /dev/null'
    elif [ "$PKG_MGR" = "yum" ]; then
        check_option="yum --help | grep '\-\-showduplicates' &> /dev/null"
        eval $check_option
        cmd_status=$?
        if [ $cmd_status -eq 0 ]; then
            search_command='yum $ASSUMEYES -v list mdatp --showduplicates 2>/dev/null | grep "$version"  &> /dev/null'
        else
            search_command='echo &>/dev/null'
        fi
    elif [ "$PKG_MGR" = "dnf" ]; then
        search_command='dnf $ASSUMEYES search --showduplicates mdatp -y 2>/dev/null | grep "$version"  &> /dev/null'
    elif [ "$PKG_MGR" = "zypper" ]; then
        search_command='zypper search -s mdatp $ASSUMEYES 2>/dev/null | grep "$version"  &> /dev/null'
    fi

    eval $search_command
    if [ $? -eq 0 ]; then
        echo "${prefix}${version}"
    else
        echo ""
    fi
}

create_sym_link()
{
    local source_path="$1"
    local target_path="$2"

    if ! [ -d "$target_path" ]; then
        log_error "[x] Error: $target_path does not exist"
        return 1
    fi

    ## Check if source path's parent exists
    if ! [ -d "$(dirname "$source_path")" ]; then
        log_error "[x] Error: Parent directory of $source_path does not exist"
        return 2
    fi

    if [ -L "$source_path" ]; then
        log_debug "[i] Symlink already exists at $source_path"
        # Check if symlink is pointing to correct path
        if [ "$(readlink -f "$source_path")" = "$target_path" ]; then
            log_warning "[!] Warning: Symlink is already pointing to correct path"
            return 0
        else
            log_error "[x] Error: Symlink is not pointing to correct path"
            return 3
        fi
    fi

    if ! ln -sf "$target_path" "$source_path"; then
        log_error "[x] Error: Failed to create symlink at $target_path"
        return 4
    fi

    return 0
}

delete_sym_link()
{
    local source_path="$1"

    if [ -L "$source_path" ]; then
        if ! rm -f "$source_path"; then
            log_error "[!] Error: Failed to delete symlink $source_path"
            return 1
        fi
    fi

    return 0
}

validate_custom_path_installation_version()
{
    # Custom Path installation is available from version 101.25062.0003
    requested_version="$1"
    if [ -n "$requested_version" ] && [ -n "$MDE_VERSION" ]; then
        # Split version into major, minor, patch
        IFS='.' read -r major minor patch <<< "$MDE_VERSION"
        # Custom path install supported from 101.25062.0003
        if (( major < 101 )) || (( major == 101 && minor < 25062 )) || (( major == 101 && minor == 25062 && patch < 3 )); then
            return 1
        fi
    fi
    return 0
}

handle_custom_installation() 
{
    # Check that the directory exists and has the right access permission
    if [ ! -d "$INSTALL_PATH" ]; then
        log_info "[>] INSTALL_PATH=$INSTALL_PATH does not exist, creating it."
        mkdir -p "$INSTALL_PATH" || script_exit "Failed to create directory $INSTALL_PATH" $ERR_INSTALL_PATH_SETUP
    fi
    
    local installation_path="$INSTALL_PATH/microsoft_mdatp"
    mkdir -p $installation_path || script_exit "Failed to create directory $installation_path" $ERR_INSTALL_PATH_SETUP
    chmod 755 "$installation_path" || script_exit "Failed to set permissions on $installation_path" $ERR_INSTALL_PATH_PERMISSIONS
    local mde_config_dir="/etc/opt/microsoft/mdatp"
    local mde_config_path="$mde_config_dir/mde_path.json"
    mkdir -p $mde_config_dir || script_exit "Failed to create directory mde_config_dir" $ERR_INSTALL_PATH_SETUP

    # Create a JSON file to set the installation path
    echo "{\"path\": \"$installation_path\"}" > $mde_config_path || script_exit "Failed to write installation path to JSON file" $ERR_INSTALL_PATH_SETUP
    chmod 644 $mde_config_path || script_exit "Failed to set permissions for JSON file" $ERR_INSTALL_PATH_PERMISSIONS

    # Create a symlink to the installation path
    local src_path="/opt/microsoft/mdatp"
    local target_path="$installation_path/opt/microsoft/mdatp"
    mkdir -p "$(dirname "$src_path")" || script_exit "Failed to create directory for symlink source path" $ERR_INSTALL_PATH_SETUP
    mkdir -p "$target_path" || script_exit "Failed to create directory for symlink target path" $ERR_INSTALL_PATH_SETUP
    create_sym_link "$src_path" "$target_path" || script_exit "Failed to create symlink from $src_path to $target_path" $ERR_INSTALL_PATH_SETUP
}

install_on_debian()
{
    local packages=()
    local pkg_version=

    if [ -z "$SKIP_PMC_SETUP" ]; then 
        packages=(curl apt-transport-https gnupg)

        install_required_pkgs "${packages[@]}"

        ### Configure the repository ###
        log_info "[>] configuring the repository"

        rm -f microsoft.list > /dev/null
        run_quietly "curl -s -o microsoft.list $PMC_URL/$DISTRO/$SCALED_VERSION/$CHANNEL.list" "unable to fetch repo list" $ERR_FAILED_REPO_SETUP
        run_quietly "mv ./microsoft.list /etc/apt/sources.list.d/microsoft-$CHANNEL.list" "unable to copy repo to location" $ERR_FAILED_REPO_SETUP

        ### Fetch the gpg key ###

        if { [ "$DISTRO" = "ubuntu" ] && [ "$VERSION" = "24.04" ]; } || { [ "$DISTRO" = "debian" ] && [ "$VERSION" = "12" ]; }; then
            if [ ! -f /usr/share/keyrings/microsoft-prod.gpg ]; then
                run_quietly "curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg" "unable to fetch the gpg key" $ERR_FAILED_REPO_SETUP
            fi
        else
            run_quietly "curl -s https://packages.microsoft.com/keys/microsoft.asc | apt-key add -" "unable to fetch the gpg key" $ERR_FAILED_REPO_SETUP
        fi
    else
        # Try to install/find curl, don't exit the script if it fails.
        packages=(curl)
        install_required_pkgs --no-exit "${packages[@]}"
    fi
    run_quietly "apt-get update" "[!] unable to refresh the repos properly"

    local version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting installion" $ERR_INSTALLATION_FAILED
        fi
    fi

    if [ -n "$INSTALL_PATH" ]; then
		validate_custom_path_installation_version $version  || script_exit "Custom Path installation is not supported on version $version, Minimum expected version : 101.25062.0003" $ERR_INSTALLATION_FAILED
        handle_custom_installation
    fi

    ### Install MDE ###
    log_info "[>] installing MDE"
    if [ -z "$CHANNEL" ]; then
        run_quietly "$PKG_MGR_INVOKER install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
    elif [ "$CHANNEL" = "prod" ]; then
        if [[ -z "$VERSION_NAME" ]]; then
            run_quietly "$PKG_MGR_INVOKER install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
        else
            run_quietly "$PKG_MGR_INVOKER -t $VERSION_NAME install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
        fi
    else
        run_quietly "$PKG_MGR_INVOKER -t $CHANNEL install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
    fi

    sleep 5
    log_info "[v] Installation complete!"
}

install_on_mariner()
{
    local packages=()
    local pkg_version=
    local repo=

    if [ -z "$SKIP_PMC_SETUP" ]; then 
        # To use config-manager plugin, install dnf-plugins-core package
        run_quietly "$PKG_MGR_INVOKER install dnf-plugins-core" "failed to install dnf-plugins-core"

        ### Install MDE ###
        log_info "[>] installing MDE"
        if [ "$CHANNEL" = "prod" ]; then
            run_quietly "$PKG_MGR_INVOKER install mariner-repos-extras" "unable to install mariner-repos-extras"
            run_quietly "$PKG_MGR_INVOKER config-manager --enable mariner-official-extras" "unable to enable extras repo"
            run_quietly "$PKG_MGR_INVOKER config-manager --disable mariner-official-extras-preview" "unable to disable extras-preview repo"
        elif [ "$CHANNEL" = "insiders-slow" ]; then
            ### Add Preview Repo File ###
            run_quietly "$PKG_MGR_INVOKER install mariner-repos-extras-preview" "unable to install mariner-repos-extras-preview"
            run_quietly "$PKG_MGR_INVOKER config-manager --enable mariner-official-extras-preview" "unable to enable extras-preview repo"
        else
            # mariner is only supported on prod and insiders-slow channels
            script_exit "Invalid channel: $CHANNEL. Available channels for $DISTRO_FAMILY are prod and insiders-slow channel only." $ERR_INVALID_CHANNEL
        fi
    fi

    local version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting installion" $ERR_INSTALLATION_FAILED
        fi
    fi

    if [ -n "$INSTALL_PATH" ]; then
		validate_custom_path_installation_version $version  || script_exit "Custom Path installation is not supported on version $version, Minimum expected version : 101.25062.0003" $ERR_INSTALLATION_FAILED
        handle_custom_installation
    fi

    ### Install MDE ###
    log_info "[>] installing MDE"
    run_quietly "$PKG_MGR_INVOKER install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED

    sleep 5
    log_info "[v] Installation complete!"
}

install_on_azurelinux()
{
    local packages=()
    local pkg_version=
    local repo=

    if [ -z "$SKIP_PMC_SETUP" ]; then 
        # To use config-manager plugin, install dnf-plugins-core package
        run_quietly "$PKG_MGR_INVOKER install dnf-plugins-core" "failed to install dnf-plugins-core"

        ### Configure the repository ###
        log_info "[>] configuring the repository"
        if [ "$CHANNEL" = "prod" ]; then
            run_quietly "$PKG_MGR_INVOKER install azurelinux-repos-ms-non-oss" "unable to install azurelinux-repos-ms-non-oss"
            run_quietly "$PKG_MGR_INVOKER config-manager --enable azurelinux-repos-ms-non-oss" "unable to enable extras repo"
            run_quietly "$PKG_MGR_INVOKER config-manager --disable azurelinux-repos-ms-non-oss-preview" "unable to disable extras-preview repo"
        else
            ### Add Preview Repo File ###
            run_quietly "$PKG_MGR_INVOKER install azurelinux-repos-ms-non-oss-preview" "unable to install azurelinux-repos-ms-non-oss-preview"
        fi
    fi

    local version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting installion" $ERR_INSTALLATION_FAILED
        fi
    fi

    if [ -n "$INSTALL_PATH" ]; then
		validate_custom_path_installation_version $version  || script_exit "Custom Path installation is not supported on version $version, Minimum expected version : 101.25062.0003" $ERR_INSTALLATION_FAILED
        handle_custom_installation
    fi

    ### Install MDE ###
    log_info "[>] installing MDE"
    run_quietly "$PKG_MGR_INVOKER install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED

    sleep 5
    log_info "[v] Installation complete!"
}

install_on_fedora()
{
    local packages=()
    local pkg_version=
    local repo=packages-microsoft-com
    local effective_distro=

    # curl-minimal results into issues when present and trying to install curl, so skip installing
    # the curl over Amazon Linux 2023
    if ! ([[ "$VERSION" == "2023" ]] && [[ "$DISTRO" == "amzn" ]] && check_if_pkg_is_installed curl-minimal); then
        packages=(curl)
    fi

    if [ -z "$SKIP_PMC_SETUP" ]; then 
        packages=("${packages[@]}" yum-utils)

        if [[ $SCALED_VERSION == 7* ]] && [[ "$DISTRO" == "rhel" ]]; then
            packages=("${packages[@]}" deltarpm)
        fi

        install_required_pkgs "${packages[@]}"

        ### Configure the repository ###
        log_info "[>] configuring the repository"
        ### Configure the repo name from which package should be installed
        local repo_name=${repo}-${CHANNEL}

        if [[ $SCALED_VERSION == 7* ]] && [[ "$CHANNEL" != "prod" ]]; then
            repo_name=packages-microsoft-com-prod-${CHANNEL}
        fi

        if [ "$CHANNEL" == "insiders-slow" ] && [ "$DISTRO" != "rocky" ] && [ "$DISTRO" != "almalinux" ] && ! { [ "$DISTRO" == "rhel" ] && [[ "$SCALED_VERSION" == 9* ]]; }; then  # in case of insiders slow repo [except rocky and alma], the repo name is packages-microsoft-com-slow-prod
            #repo_name=${repo}-slow-prod
            repo_name="packages-microsoft-com-insiders-slow"
        fi

        if [ "$DISTRO" = "ol" ] || [ "$DISTRO" = "fedora" ]; then
            effective_distro="rhel"
        elif [ "$DISTRO" = "almalinux" ]; then
            effective_distro="alma"
        elif [ "$DISTRO" = "amzn" ]; then
            effective_distro="amazonlinux"
        else
            effective_distro="$DISTRO"
        fi

        # Configure repository if it does not exist
        yum -q repolist "$repo_name" | grep "$repo_name"
        found_repo=$?
        if [ $found_repo -eq 0 ]; then
            log_info "[i] repository already configured"
        else
            log_info "[>] configuring the repository"
            run_quietly "yum-config-manager --add-repo=$PMC_URL/$effective_distro/$SCALED_VERSION/$CHANNEL.repo" "Unable to fetch the repo ($?)" $ERR_FAILED_REPO_SETUP
        fi

        ### Fetch the gpg key ###
        run_quietly "curl https://packages.microsoft.com/keys/microsoft.asc > microsoft.asc" "unable to fetch gpg key $?" $ERR_FAILED_REPO_SETUP
        run_quietly "rpm $(get_rpm_proxy_params) --import microsoft.asc" "unable to import gpg key" $ERR_FAILED_REPO_SETUP
    else
        # Try to install/find packages, don't exit the script if it fails.
        install_required_pkgs --no-exit "${packages[@]}"
    fi

    local version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting installion" $ERR_INSTALLATION_FAILED
        fi
    fi

    if [ -n "$INSTALL_PATH" ]; then
		validate_custom_path_installation_version $version  || script_exit "Custom Path installation is not supported on version $version, Minimum expected version : 101.25062.0003" $ERR_INSTALLATION_FAILED
        handle_custom_installation
    fi

    ### Install MDE ###
    log_info "[>] installing MDE"

    if [ "$ARCHITECTURE" = "aarch64" ] || [ -z "$repo_name" ]; then
        run_quietly "$PKG_MGR_INVOKER install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
    else
        run_quietly "$PKG_MGR_INVOKER --enablerepo=$repo_name install mdatp$version" "unable to install MDE ($?)" $ERR_INSTALLATION_FAILED
    fi

    sleep 5
    log_info "[v] Installation complete!"
}

install_on_sles()
{
    local packages=()
    local pkg_version=
    local repo=packages-microsoft-com

    packages=(curl)
    if [ -z "$SKIP_PMC_SETUP" ]; then 
        install_required_pkgs "${packages[@]}"

        wait_for_package_manager_to_complete

        ### Configure the repository ###
        local repo_name=${repo}-${CHANNEL}
        if [ "$CHANNEL" = "insiders-slow" ]; then  # in case of insiders slow repo, the repo name is packages-microsoft-com-slow-prod
            repo_name=${repo}-slow-prod
        fi

        # add repository if it does not exist
        lines=$($PKG_MGR_INVOKER lr | grep "$repo_name" | wc -l)

        if [ $lines -eq 0 ]; then
            log_info "[>] configuring the repository"
            run_quietly "$PKG_MGR_INVOKER addrepo -c -f -n $repo_name https://packages.microsoft.com/config/$DISTRO/$SCALED_VERSION/$CHANNEL.repo" "unable to load repo" $ERR_FAILED_REPO_SETUP
        else
            log_info "[i] repository already configured"
        fi

        ### Fetch the gpg key ###
        run_quietly "rpm $(get_rpm_proxy_params) --import https://packages.microsoft.com/keys/microsoft.asc > microsoft.asc" "unable to fetch gpg key $?" $ERR_FAILED_REPO_SETUP

        wait_for_package_manager_to_complete
    else
        # Try to install/find packages, don't exit the script if it fails.
        install_required_pkgs --no-exit "${packages[@]}"
    fi

    local version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting installion" $ERR_INSTALLATION_FAILED
        fi
    fi

    if [ ! -z "$INSTALL_PATH" ]; then
		validate_custom_path_installation_version $version  || script_exit "Custom Path installation is not supported on version $version, Minimum expected version : 101.25062.0003" $ERR_INSTALLATION_FAILED
        handle_custom_installation
    fi

    ### Install MDE ###
    log_info "[>] installing MDE"

    if [ -z "$repo_name" ]; then
        run_quietly "$PKG_MGR_INVOKER install $ASSUMEYES mdatp$version" "[!] failed to install MDE (1/2)"
    else
        run_quietly "$PKG_MGR_INVOKER install $ASSUMEYES ${repo_name}:mdatp$version" "[!] failed to install MDE (1/2)"
    fi
    
    if ! check_if_pkg_is_installed mdatp; then
        log_warning "[r] retrying"
        sleep 2
        run_quietly "$PKG_MGR_INVOKER install $ASSUMEYES mdatp" "unable to install MDE 2/2 ($?)" $ERR_INSTALLATION_FAILED
    fi

    sleep 5
    log_info "[v] Installation complete!"
}

remove_repo()
{
    # Remove mdatp if installed
    if check_if_pkg_is_installed mdatp; then
        current_channel=$(get_mdatp_channel)
        if [ "$CHANNEL" = "$current_channel" ]; then
            log_info "[i] MDE is installed for $CHANNEL"
            remove_mdatp
        fi
    fi

    log_info "[>] Removing repo for $CHANNEL"

    local cmd_status
    # Remove configured packages.microsoft.com repository
    if [ "$DISTRO" = "sles" ] || [ "$DISTRO" = "sle-hpc" ]; then
        local repo=packages-microsoft-com
        local repo_name=${repo}-${CHANNEL}
        if [ "$CHANNEL" = "insiders-slow" ]; then  # in case of insiders slow repo, the repo name is packages-microsoft-com-slow-prod
            repo_name=${repo}-slow-prod
        fi
        run_quietly "$PKG_MGR_INVOKER removerepo $repo_name" "failed to remove repo"
    
    elif [ "$DISTRO_FAMILY" = "fedora" ]; then
        local repo=packages-microsoft-com
        local repo_name="$repo-$CHANNEL"

        if [ "$CHANNEL" = "insiders-slow" ]; then  # in case of insiders slow repo, the repo name is packages-microsoft-com-slow-prod
            repo_name=${repo}-slow-prod
        fi

        if [[ $SCALED_VERSION == 7* ]] && [[ "$CHANNEL" != "prod" ]]; then
            repo_name=${repo}-prod
        fi

        yum -q repolist $repo_name | grep "$repo_name" &> /dev/null
        cmd_status=$?
        if [ $cmd_status -eq 0 ]; then
            run_quietly "yum-config-manager --disable $repo_name" "Unable to disable the repo ($?)" $ERR_FAILED_REPO_CLEANUP
            run_quietly "find /etc/yum.repos.d -exec grep -lqR \"\[$repo_name\]\" '{}' \; -delete" "Unable to remove repo ($?)" $ERR_FAILED_REPO_CLEANUP
        else
            log_info "[i] nothing to clean up"
        fi
    
    elif [ "$DISTRO_FAMILY" = "debian" ]; then
        if [ -f "/etc/apt/sources.list.d/microsoft-$CHANNEL.list" ]; then
            run_quietly "rm -f '/etc/apt/sources.list.d/microsoft-$CHANNEL.list'" "unable to remove repo list ($?)" $ERR_FAILED_REPO_CLEANUP
        fi
    elif [ "$DISTRO_FAMILY" = "mariner" ]; then # in case of mariner, do not remove the repo
        log_info "[i] nothing to clean up"
        return
    else
        script_exit "unsupported distro for remove repo $DISTRO" $ERR_UNSUPPORTED_DISTRO
    fi

    log_info "[v] Repo removed for $CHANNEL"
}

upgrade_mdatp()
{
    if [ -z "$1" ]; then
        script_exit "INTERNAL ERROR. upgrade_mdatp requires an argument (the upgrade command)" $ERR_INTERNAL
    fi

    exit_if_mde_not_installed

    local VERSION_BEFORE_UPDATE VERSION_AFTER_UPDATE version current_version requested_version
    VERSION_BEFORE_UPDATE=$(get_mdatp_version)
    log_info "[i] Current $VERSION_BEFORE_UPDATE"

    version=""
    if [ ! -z "$MDE_VERSION" ]; then
        version=$(validate_mde_version)
        if [ -z "$version" ]; then
            script_exit "Couldn't find the version $MDE_VERSION for channel $CHANNEL. Aborting upgrade" $ERR_INSTALLATION_FAILED
        fi
    fi

    current_version=$(echo "$VERSION_BEFORE_UPDATE" | sed 's/^[ \t\n]*//;s/[ \t\n]*$//' | awk '{print $NF}' | awk -F. '{ printf("%d%05d%05d\n", $1,$2,$3); }')
    requested_version=$(echo "$MDE_VERSION" | awk -F. '{ printf("%d%05d%05d\n", $1,$2,$3); }')

    if [[ "$INSTALL_MODE" == "d" && "$current_version" -lt "$requested_version" ]]; then
        script_exit "For downgrade the requested version[$MDE_VERSION] should be older than current version[$VERSION_BEFORE_UPDATE]"
    elif [[ "$INSTALL_MODE" == "u" && ! -z "$MDE_VERSION" && "$current_version" -gt "$requested_version" ]]; then
        script_exit "For upgrade the requested version[$MDE_VERSION] should be newer than current version[$VERSION_BEFORE_UPDATE]. If you want to move to an older version instead, retry with --downgrade flag"
    fi

    run_quietly "$PKG_MGR_INVOKER $1 mdatp$version" "Unable to upgrade MDE $?" $ERR_INSTALLATION_FAILED

    VERSION_AFTER_UPDATE=$(get_mdatp_version)
    if [ "$VERSION_BEFORE_UPDATE" = "$VERSION_AFTER_UPDATE" ]; then
        log_info "[i] MDE is already up to date."
    else
        log_info "[v] Upgrade successful!" 
    fi
}

remove_mdatp()
{
    exit_if_mde_not_installed

    log_info "[>] Removing MDE" 

    run_quietly "$PKG_MGR_INVOKER remove mdatp" "unable to remove MDE $?" $ERR_UNINSTALLATION_FAILED
}

rhel6_supported_version()
{
    local SUPPORTED_RHEL6_VERSIONS=("6.7" "6.8" "6.9" "6.10")
    for version in "${SUPPORTED_RHEL6_VERSIONS[@]}"; do
        if [[ "$1" == "$version" ]]; then 
            return 0
        fi
    done
    return 1    
}

scale_version_id()
{
    ### We dont have pmc repos for rhel versions > 7.4. Generalizing all the 7* repos to 7 and 8* repos to 8
    if [ "$DISTRO_FAMILY" = "fedora" ]; then
        if [[ $VERSION == 6* ]]; then
            if rhel6_supported_version $VERSION; then # support versions 6.7+
                if [ "$DISTRO" = "centos" ] || [ "$DISTRO" = "rhel" ]; then
                    SCALED_VERSION=6
                else
                    script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
                fi
            else
               script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
            fi

        elif [[ $VERSION == 7* ]]; then
            SCALED_VERSION=7
        elif [[ $VERSION == 8* ]] || [[ "$DISTRO" == "fedora" ]]; then
            SCALED_VERSION=8
        elif [[ $VERSION == 9* ]]; then
            if [[ $DISTRO == "almalinux" || $DISTRO == "rocky" ]]; then
                SCALED_VERSION=9
            else
                SCALED_VERSION=9.0
            fi
		elif [[ "$VERSION" == 10* ]] && [[ "$DISTRO" == "centos" || "$DISTRO" == "rhel" ]]; then
			SCALED_VERSION=10
        elif [[ $DISTRO == "amzn" ]] &&  [[ $VERSION == "2" || $VERSION == "2023" ]]; then # For Amazon Linux the scaled version is 2023 or 2
            SCALED_VERSION=$VERSION
        else
            script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
        fi
    elif [ "$DISTRO_FAMILY" = "mariner" ]; then
        if [[ $VERSION == 2* ]]; then
            SCALED_VERSION=2
        else
            script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
        fi
    elif [ "$DISTRO_FAMILY" = "azurelinux" ]; then
        if [[ $VERSION == 3* ]]; then
            SCALED_VERSION=3
        else
            script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
        fi
    elif [ "$DISTRO_FAMILY" = "sles" ]; then
        if [[ $VERSION == 12* ]]; then
            SCALED_VERSION=12
        elif [[ $VERSION == 15* ]]; then
            SCALED_VERSION=15
        else
            script_exit "unsupported version: $DISTRO $VERSION" $ERR_UNSUPPORTED_VERSION
        fi
    elif [[ $DISTRO == "ubuntu" ]] && [[ $VERSION != "16.04" ]] && [[ $VERSION != "18.04" ]] && [[ $VERSION != "20.04" ]] && [[ $VERSION != "22.04" ]] && [[ $VERSION != "24.04" ]]; then
        SCALED_VERSION=18.04
    else
        # no problems with 
        SCALED_VERSION=$VERSION
    fi
    log_info "[v] scaled: $SCALED_VERSION"
}

onboard_device()
{
    log_info "[>] onboarding script: $ONBOARDING_SCRIPT"

    exit_if_mde_not_installed

    if check_if_device_is_onboarded; then
        log_info "[i] MDE already onboarded"
        return
    fi

    if [ ! -f $ONBOARDING_SCRIPT ]; then
        script_exit "error: onboarding script not found." $ERR_ONBOARDING_NOT_FOUND
    fi

    if [[ $ONBOARDING_SCRIPT == *.py ]]; then
        # Make sure python is installed
        PYTHON=$(which python 2>/dev/null || which python3 2>/dev/null)

        if [ $? -ne 0 ]; then
            script_exit "error: cound not locate python." $ERR_FAILED_DEPENDENCY
        fi

        #remove mdatp_offboard.json if present
        mdatp_offboard_file=/etc/opt/microsoft/mdatp/mdatp_offboard.json
        if [ -f "$mdatp_offboard_file" ]; then
            echo "found mdatp_offboard file"
            run_quietly "rm -f $mdatp_offboard_file" "error: failed to remove offboarding blob" $ERR_ONBOARDING_FAILED
            if [ ! -f "$mdatp_offboard_file" ]; then
                echo "removed mdatp_offboard file"
            else
                echo "failed to remove mdatp_offboard file"
            fi
        fi

        # Run onboarding script
        sleep 1
        run_quietly "$PYTHON $ONBOARDING_SCRIPT" "error: python onboarding failed" $ERR_ONBOARDING_FAILED

    elif [[ $ONBOARDING_SCRIPT == *.sh ]]; then        
        shebang_line=$(head -n 1 "$ONBOARDING_SCRIPT")
        if [[ "$shebang_line" == \#!* ]]; then
            interpreter_path=${shebang_line:2}
            run_quietly "$interpreter_path $ONBOARDING_SCRIPT" "error: $interpreter_path onboarding failed" $ERR_ONBOARDING_FAILED
        else
            run_quietly "sh $ONBOARDING_SCRIPT" "error: sh onboarding failed" $ERR_ONBOARDING_FAILED
        fi

    elif [[ $ONBOARDING_SCRIPT == *.json ]]; then
        local onboarding_dir=/etc/opt/microsoft/mdatp/
        if [ -d "$onboarding_dir" ]; then
            run_quietly "cp $ONBOARDING_SCRIPT $onboarding_dir/mdatp_onboard.json" "error: JSON onboarding failed" $ERR_ONBOARDING_FAILED
        else
            script_exit "error: JSON onboarding failed. mdatp is not installed or installation failed." $ERR_ONBOARDING_FAILED
        fi
    else
        script_exit "error: unknown onboarding script type." $ERR_ONBOARDING_FAILED
    fi

    # validate onboarding
    local license_found
    license_found=false

    for ((i = 1; i <= 8; i++)); do
        sleep 10 # Delay for 10 seconds before checking the license status

        # Check if licensed field is true
        if check_if_device_is_onboarded; then
            license_found=true
            break
        fi
    done

    if [ "$license_found" = "false" ]; then
        script_exit "onboarding failed" $ERR_ONBOARDING_FAILED
    fi

    log_info "[v] Onboarded"
}

offboard_device()
{
    log_info "[>] offboarding script: $OFFBOARDING_SCRIPT"

    exit_if_mde_not_installed

    if ! check_if_device_is_onboarded; then
        log_info "[i] MDE already offboarded"
        return
    fi

    if [ ! -f $OFFBOARDING_SCRIPT ]; then
        script_exit "error: offboarding script not found." $ERR_OFFBOARDING_NOT_FOUND
    fi
    local cmd_status
    if [[ $OFFBOARDING_SCRIPT == *.py ]]; then
        # Make sure python is installed
        PYTHON=$(which python || which python3)

        cmd_status=$?
        if [ $cmd_status -ne 0 ]; then
            script_exit "error: cound not locate python." $ERR_FAILED_DEPENDENCY
        fi

        # Run offboarding script
        sleep 1
        run_quietly "$PYTHON $OFFBOARDING_SCRIPT" "error: python offboarding failed" $ERR_OFFBOARDING_FAILED

    elif [[ $OFFBOARDING_SCRIPT == *.sh ]]; then        
        run_quietly "sh $OFFBOARDING_SCRIPT" "error: bash offboarding failed" $ERR_OFFBOARDING_FAILED

    else
        script_exit "error: unknown offboarding script type." $ERR_OFFBOARDING_FAILED
    fi

    # validate offboarding
    local license_found
    license_found=true
    for ((i = 1; i <= 15; i++)); do
        sleep 10 # Delay for 10 seconds before checking the license status

        # Check if licensed field is false
        if ! check_if_device_is_onboarded; then
            license_found=false
            break
        fi
    done

    if [ "$license_found" = "true" ]; then
        script_exit "offboarding failed" $ERR_OFFBOARDING_FAILED "offboarding_failed"
    fi

    log_info "[v] Offboarded"
}

set_epp_to_passive_mode()
{
    exit_if_mde_not_installed

    if [[ $(get_health_field passive_mode_enabled) == "false" ]]; then
        log_info "[>] setting MDE/EPP to passive mode"
        retry_quietly 3 "mdatp config passive-mode --value enabled" "failed to set MDE to passive-mode" $ERR_PARAMETER_SET_FAILED
    else
        log_info "[i] MDE/EPP already in passive mode"
        return
    fi
    
    log_info "[v] passive mode set"
}

set_epp_to_rtp_mode()
{
    exit_if_mde_not_installed

    if [[ $(get_health_field real_time_protection_enabled) == "false" ]]; then
        log_info "[>] setting MDE/EPP to real time protection mode"
        retry_quietly 3 "mdatp config real-time-protection --value enabled" "failed to set MDE to rtp-mode" $ERR_PARAMETER_SET_FAILED
    else
        log_info "[i] MDE/EPP already in real time protection mode"
        return
    fi

    log_info "[v] real time protection mode set"
}

set_device_tags()
{
    for tag_key in "${!tags[@]}"; do
        tag_value="${tags[$tag_key]}"

        local set_tags tag_exists result value
        if [ "$tag_key" = "GROUP" ] || [ "$tag_key" = "SecurityWorkspaceId" ] || [ "$tag_key" = "AzureResourceId" ] || [ "$tag_key" = "SecurityAgentId" ]; then
            set_tags=$(get_health_field edr_device_tags)
            tag_exists=0

            result=$(echo "$set_tags" | grep -q "\"key\":\"$tag_key\""; echo "$?")
            if [ $result -eq 0 ]; then
                value=$(echo "$set_tags" | grep -o "\"key\":\"$tag_key\".*\"" | cut -d '"' -f 8)
                if [ "$value" = "$tag_value" ]; then
                    log_warning "[i] tag $tag_key already set to value $tag_value"
                    tag_exists=1
                fi
            fi

            if [ $tag_exists -eq 0 ]; then
                log_debug "[>] setting tag: ($tag_key, $tag_value)"
                retry_quietly 2 "mdatp edr tag set --name $tag_key --value $tag_value" "failed to set tag" $ERR_PARAMETER_SET_FAILED
            fi
        else
            script_exit "invalid tag name: $tag_key. supported tags: GROUP, SecurityWorkspaceId, AzureResourceId and SecurityAgentId" $ERR_TAG_NOT_SUPPORTED
        fi
    done
    log_info "[v] tags set."   
}

usage()
{
    echo "mde_installer.sh v$SCRIPT_VERSION"
    echo "usage: $1 [OPTIONS]"
    echo "Options:"
    echo " -c|--channel         specify the channel(insiders-fast / insiders-slow / prod) from which you want to install. Default: prod"
    echo " -i|--install         install the product"
    echo " -r|--remove          uninstall the product"
    echo " -u|--upgrade         upgrade the existing product to a newer version if available"
    echo " -l|--downgrade       downgrade the existing product to a older version if available"
    echo " -o|--onboard         onboard the product with <onboarding_script>"
    echo " -f|--offboard        offboard the product with <offboarding_script>"
    echo " -p|--passive-mode    set real time protection to passive mode"
    echo " -a|--rtp-mode        set real time protection to active mode. passive-mode and rtp-mode are mutually exclusive"
    echo " -t|--tag             set a tag by declaring <name> and <value>, e.g: -t GROUP Coders"
    echo " -m|--min_req         enforce minimum requirements"
    echo " -x|--skip_conflict   skip conflicting application verification"
    echo " -w|--clean           remove repo from package manager for a specific channel"
    echo " -y|--yes             assume yes for all mid-process prompts (default, depracated)"
    echo " -n|--no              remove assume yes sign"
    echo " -s|--verbose         verbose output"
    echo " -v|--version         print out script version"
    echo " -d|--debug           set debug mode"
    echo " --log-path <PATH>    also log output to PATH"
    echo " --http-proxy <URL>   set http proxy"
    echo " --https-proxy <URL>  set https proxy"
    echo " --ftp-proxy <URL>    set ftp proxy"
    echo " --mdatp              specific version of mde to be installed. will use the latest if not provided"
    echo " --use-local-repo     this will skip the MDE repo setup and use the already configured repo instead"
    echo " -b|--install-path    specify the installation and configuration path for MDE. Default: /"
    echo " -h|--help            display help"
}

if [ $# -eq 0 ]; then
    usage
    script_exit "no arguments were provided. specify --help for details" $ERR_INVALID_ARGUMENTS
fi

while [ $# -ne 0 ];
do
    case "$1" in
        -c|--channel)
            if [ -z "$2" ]; then
                script_exit "$1 option requires an argument" $ERR_INVALID_ARGUMENTS
            fi
            CHANNEL=$2
            verify_channel
            shift 2
            ;;
        -i|--install)
            INSTALL_MODE="i"
            verify_privileges "install"
            shift 1
            ;;
        -u|--upgrade|--update)
            INSTALL_MODE="u"
            verify_privileges "upgrade"
            shift 1
            ;;
        -l|--downgrade)
            INSTALL_MODE="d"
            verify_privileges "downgrade"
            shift 1
            ;;
        -r|--remove)
            INSTALL_MODE="r"
            verify_privileges "remove"
            shift 1
            ;;
        -o|--onboard)
            if [ -z "$2" ]; then
                script_exit "$1 option requires an argument" $ERR_INVALID_ARGUMENTS
            fi
            ONBOARDING_SCRIPT=$2
            verify_privileges "onboard"
            shift 2
            ;;
        -f|--offboard)
            if [ -z "$2" ]; then
                script_exit "$1 option requires an argument" $ERR_INVALID_ARGUMENTS
            fi        
            OFFBOARDING_SCRIPT=$2
            verify_privileges "offboard"
            shift 2
            ;;
        -m|--min_req)
            MIN_REQUIREMENTS=1
            shift 1
            ;;
        -x|--skip_conflict)
            SKIP_CONFLICTING_APPS=1
            shift 1
            ;;
        -p|--passive-mode)
            verify_privileges "passive-mode"
            PASSIVE_MODE=1
            shift 1
            ;;
        -a|--rtp-mode)
            verify_privileges "rtp-mode"
            RTP_MODE=1
            shift 1
            ;;
        -t|--tag)
            if [[ -z "$2" || -z "$3" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            verify_privileges "set-tag"
            tags["$2"]="$3"
            shift 3
            ;;
        -w|--clean)
            INSTALL_MODE='c'
            verify_privileges "clean"
            shift 1
            ;;
        -h|--help)
            usage "basename $0" >&2
            exit 0
            ;;
        -y|--yes)
            ASSUMEYES=-y
            shift 1
            ;;
        -n|--no)
            ASSUMEYES=
            shift 1
            ;;            
        -s|--verbose)
            VERBOSE=1
            shift 1
            ;;
        -v|--version)
            script_exit "$SCRIPT_VERSION" $SUCCESS
            ;;
        -d|--debug)
            DEBUG=1
            shift 1
            ;;
        --http-proxy)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            export http_proxy=$2
            shift 2
            ;;
        --https-proxy)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            export https_proxy=$2
            shift 2
            ;;
        --ftp-proxy)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            export ftp_proxy=$2
            shift 2
            ;;
        --log-path)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            export log_path=$2
            shift 2
            ;;
        --mdatp)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            fi
            export MDE_VERSION=$2
            shift 2
            ;;
        --use-local-repo)
            SKIP_PMC_SETUP=1
            shift 1
            ;;
        -b|--install-path)
            if [[ -z "$2" ]]; then
                script_exit "$1 option requires two arguments" $ERR_INVALID_ARGUMENTS
            else
                INSTALL_PATH=$(realpath "$2") || script_exit "Failed to resolve absolute path for $2" $ERR_INVALID_ARGUMENTS
                INSTALL_PATH="${INSTALL_PATH%/}"
            fi
            shift 2
            ;;
        *)
            echo "use -h or --help for details"
            script_exit "unknown argument $1" $ERR_INVALID_ARGUMENTS
            ;;
    esac
done

if command -v mdatp >/dev/null 2>&1; then 
    INSTALLED_MDE_CHANNEL=$(get_mdatp_channel)
    if [ "$INSTALLED_MDE_CHANNEL" != "$CHANNEL" ] && [ "$INSTALL_MODE" != 'c' ]; then
        if [ ! -z "$CHANNEL" ]; then
            echo "[i] MDE Installed with $INSTALLED_MDE_CHANNEL. Cannot switch channel to $CHANNEL. Channel is being set to $INSTALLED_MDE_CHANNEL. To update chanel remove and re-install MDE with $CHANNEL"
        fi
        CHANNEL=$INSTALLED_MDE_CHANNEL
    fi
fi

if [[ -z "${INSTALL_MODE}" && -z "${ONBOARDING_SCRIPT}" && -z "${OFFBOARDING_SCRIPT}" && -z "${PASSIVE_MODE}" && -z "${RTP_MODE}" && ${#tags[@]} -eq 0 ]]; then
    script_exit "no installation mode specified. Specify --help for help" $ERR_INVALID_ARGUMENTS
fi

# Check for mutually exclusive options
if [ ! -z "$PASSIVE_MODE" ] && [ ! -z "$RTP_MODE" ]; then
    echo "Options --passive-mode and --rtp-mode are mutually exclusive."
    usage
    exit 1
fi

if [ ! -z "$SKIP_PMC_SETUP" ]; then
    if [ "$INSTALL_MODE" == 'c' ]; then
        script_exit "--clean repo option is not supported with --use-local-repo" $ERR_INVALID_ARGUMENTS
    fi

    if [ ! -z "$CHANNEL" ]; then
        script_exit "--use-local-repo option is not supported with --channel" $ERR_INVALID_ARGUMENTS
    fi
else
    if [ -z "$CHANNEL" ]; then
        if [ "$INSTALL_MODE" == 'i' ]; then
            log_info "[i] Specify the install channel using \"--channel\" argument. If not provided, mde will be installed for prod by default. Expected channel values: prod, insiders-slow, insiders-fast."
        elif [ "$INSTALL_MODE" == 'c' ]; then
            log_info "[i] Specify the cleanup channel using \"--channel\" argument. If not provided, prod repo will be cleaned up by default. Expected channel values: prod, insiders-slow, insiders-fast."
        fi
        CHANNEL=prod
    fi
fi

if [[ "$INSTALL_MODE" == 'd' && -z "$MDE_VERSION" ]]; then
    script_exit "Specify the mdatp version using --mdatp argument when using --downgrade option" $ERR_INVALID_ARGUMENTS
fi

if [[ ! -z "$INSTALL_PATH" && "$INSTALL_MODE" != 'i' ]]; then
    script_exit "install path is only supported for installation" $ERR_INVALID_ARGUMENTS
fi

if [[ -z "$MDE_VERSION" && ( "$INSTALL_MODE" == 'i' || "$INSTALL_MODE" == 'u' ) ]]; then
    log_info "[i] Specify the version to be installed using \"--mdatp\" argument. If not provided, latest mde will be installed by default."
fi


# echo "--- mde_installer.sh v$SCRIPT_VERSION ---"
log_info "--- mde_installer.sh v$SCRIPT_VERSION ---"

### Validate mininum requirements ###
if [ $MIN_REQUIREMENTS ]; then
    verify_min_requirements
fi

## Detect the architecture type
detect_arch

### Detect the distro and version number ###
detect_distro

### Scale the version number according to repos avaiable on pmc ###
scale_version_id

### Set package manager ###
set_package_manager

### Act according to arguments ###
if [ "$INSTALL_MODE" = "i" ]; then

    if ! skip_if_mde_installed; then

        if [ -z $SKIP_CONFLICTING_APPS ]; then
            verify_conflicting_applications
        fi

        if [ "$DISTRO_FAMILY" = "debian" ]; then
            install_on_debian
        elif [ "$DISTRO_FAMILY" = "fedora" ]; then
            install_on_fedora
        elif [ "$DISTRO_FAMILY" = "mariner" ]; then
            install_on_mariner
        elif [ "$DISTRO_FAMILY" = "azurelinux" ]; then
            install_on_azurelinux
        elif [ "$DISTRO_FAMILY" = "sles" ]; then
            install_on_sles
        else
            script_exit "unsupported distro $DISTRO $VERSION" $ERR_UNSUPPORTED_DISTRO
        fi
    fi

elif [ "$INSTALL_MODE" = "u" ]; then

    if [ "$DISTRO_FAMILY" = "debian" ]; then
        upgrade_mdatp "$ASSUMEYES install --only-upgrade"
    elif [ "$DISTRO_FAMILY" = "fedora" ]; then
        upgrade_mdatp "$ASSUMEYES update"
    elif [ "$DISTRO_FAMILY" = "mariner" ]; then
        upgrade_mdatp "$ASSUMEYES update"
    elif [ "$DISTRO_FAMILY" = "azurelinux" ]; then
        upgrade_mdatp "$ASSUMEYES update"
    elif [ "$DISTRO_FAMILY" = "sles" ]; then
        upgrade_mdatp "up $ASSUMEYES"
    else    
        script_exit "unsupported distro $DISTRO $VERSION" $ERR_UNSUPPORTED_DISTRO
    fi

elif [ "$INSTALL_MODE" = "d" ]; then

    if [ "$DISTRO_FAMILY" = "debian" ]; then
        upgrade_mdatp "$ASSUMEYES install --allow-downgrades"
    elif [ "$DISTRO_FAMILY" = "fedora" ]; then
        upgrade_mdatp "$ASSUMEYES downgrade"
    elif [ "$DISTRO_FAMILY" = "mariner" ]; then
        upgrade_mdatp "$ASSUMEYES downgrade"
    elif [ "$DISTRO_FAMILY" = "azurelinux" ]; then
        upgrade_mdatp "$ASSUMEYES downgrade"
    elif [ "$DISTRO_FAMILY" = "sles" ]; then
        upgrade_mdatp "install --oldpackage $ASSUMEYES"
    else
        script_exit "unsupported distro $DISTRO $VERSION" $ERR_UNSUPPORTED_DISTRO
    fi

elif [ "$INSTALL_MODE" = "r" ]; then
    if remove_mdatp; then
        script_exit "removed MDE" $SUCCESS
    fi

elif [ "$INSTALL_MODE" = "c" ]; then
    if remove_repo; then
        script_exit "removed repo" $SUCCESS
    fi
fi

if [ ! -z $PASSIVE_MODE ]; then
    set_epp_to_passive_mode
elif [ ! -z $RTP_MODE ]; then
    set_epp_to_rtp_mode
fi

if [ ! -z $ONBOARDING_SCRIPT ]; then
    onboard_device
fi

if [ ! -z $OFFBOARDING_SCRIPT ]; then
    offboard_device
fi

if [ ${#tags[@]} -gt 0 ]; then
    set_device_tags
fi

script_exit "--- mde_installer.sh ended. ---" $SUCCESS
