#!/bin/bash

# Stop script on NZEC
set -e
# Stop script if unbound variable found (use ${var:-} if intentional)
set -u
# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success
# This is causing it to fail
set -o pipefail

# Use in the the functions: eval $invocation
invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"'

# standard output may be used as a return value in the functions
# we need a way to write text on the screen in the functions so that
# it won't interfere with the return value.
# Exposing stream 3 as a pipe to standard output of the script itself
exec 3>&1

# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.
# See if stdout is a terminal
if [ -t 1 ] && command -v tput > /dev/null; then
    # see if it supports colors
    ncolors=$(tput colors || echo 0)
    if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
        bold="$(tput bold       || echo)"
        normal="$(tput sgr0     || echo)"
        black="$(tput setaf 0   || echo)"
        red="$(tput setaf 1     || echo)"
        green="$(tput setaf 2   || echo)"
        yellow="$(tput setaf 3  || echo)"
        blue="$(tput setaf 4    || echo)"
        magenta="$(tput setaf 5 || echo)"
        cyan="$(tput setaf 6    || echo)"
        white="$(tput setaf 7   || echo)"
    fi
fi

say_warning() {
    printf "%b\n" "${yellow:-}flexxagent_install: Warning: $1${normal:-}" >&3
}

say_err() {
    printf "%b\n" "${red:-}flexxagent_install: Error: $1${normal:-}" >&2
}

say() {
    # using stream 3 (defined in the beginning) to not interfere with stdout of functions
    # which may be used as return value
    printf "%b\n" "${cyan:-}flexxagent-install:${normal:-} $1" >&3
}

say_verbose() {
    if [ "$verbose" = true ]; then
        say "$1"
    fi
}

# Check privileges
if [ "$EUID" -ne 0 ]
  then say_err "Please run as root!"
  exit
fi

get_specific_agent_version_url() {
    eval "$invocation"

    local package_download_link="$1"
    local pvFileName="flexxagent.$2"
    local download_link=null

    if [ -z "$package_download_link" ]; then
        download_link="${BASE_AGENT_URL}/${pvFileName}"
    else
        download_link="${package_download_link}${BASE_AGENT_URL}/${pvFileName}"
    fi

    say_verbose "Constructed package link: $download_link"
    echo "$download_link"
    return 0
}

download_specific_agent_version() {
    eval "$invocation"

    temporary_file_template="$(mktemp -u).$1"

    local firstBaseUrl="https://update.workspaces.flexxible.com"
    local secondBaseUrl="https://flexxibleglobal.blob.core.windows.net"

    # Try to get the version number, using the productVersion.txt file located next to the installer file.
    local download_links=(
        "$(get_specific_agent_version_url "$firstBaseUrl" "$1")"
        "$(get_specific_agent_version_url "$secondBaseUrl" "$1")"
    )

    local download_failed=false
    local download_completed=false
    output_path="${output_path:-"$temporary_file_template"}"
    error_logs="${error_logs:-"$temporary_file_log"}"
    say_verbose "Download output path: $output_path"

    for download_link in "${download_links[@]}"
    do
        say "Downloading agent ..."

        # The download function will set variables $http_code and $download_error_msg in case of failure.
        download_failed=false
        download "$download_link" "$output_path" 2>&1 || download_failed=true
        final_download_link="$download_link"

        if [ "$download_failed" = true ]; then
          clear_tmp
        else
            download_completed=true
            break
        fi
    done

    if [[ "$download_completed" == false ]]; then
        say_err "Could not find \`FlexxAgent\` with version = $version"
        return 1
    fi

    return 0
}

download() {
    eval "$invocation"

     local remote_path="$1"
     local out_path="${2:-}"

     if [[ "$remote_path" != "http"* ]]; then
         cp "$remote_path" "$out_path" 2>&1
         return $?
     fi

     local failed=false
     local attempts=0
     while [ $attempts -lt 3 ]; do
         attempts=$((attempts+1))
         failed=false
         if machine_has "curl"; then
             download_curl "$remote_path" "$out_path" || failed=true
         elif machine_has "wget"; then
             download_wget "$remote_path" "$out_path" || failed=true
         else
             say_err "Missing dependency: neither curl or wget was found."
             exit 1
         fi

         if [ "$failed" = false ] || [ $attempts -ge 3 ]; then
             break
         fi

         say "Download attempt #$attempts has failed: $download_error_msg"
         #say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds."
         #sleep $((attempts*10))
     done

     if [ "$failed" = true ]; then
         say_verbose "Download failed: $remote_path"
         return 1
     fi
     return 0
}

download_curl() {
     eval "$invocation"
     unset http_code
     unset download_error_msg
     local remote_path="$1"
     local out_path="${2:-}"
     local remote_path_with_credential="${remote_path}"
     local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs "
     local curl_exit_code=0;
     if [ -z "$out_path" ]; then
         curl $curl_options "$remote_path_with_credential" 2>&1
         curl_exit_code=$?
     else
         curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1
         curl_exit_code=$?
     fi
     
     if [ $curl_exit_code -gt 0 ]; then
         download_error_msg="Unable to download $remote_path."
         # Check for curl timeout codes
         if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then
             download_error_msg+=" Failed to reach the server: connection timeout."
         fi
         say_verbose "$download_error_msg"
         return 1
     fi
     return 0
 }

download_wget() {
     eval "$invocation"
     unset http_code
     unset download_error_msg
     local remote_path="$1"
     local out_path="${2:-}"
     local remote_path_with_credential="${remote_path}"
     local wget_options="--tries 20 "
 
     local wget_options_extra=''
     local wget_result=''
 
     # Test for options that aren't supported on all wget implementations.
     if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then
         wget_options_extra="--waitretry 2 --connect-timeout 15 "
     else
         say "wget extra options are unavailable for this environment"
     fi
 
     if [ -z "$out_path" ]; then
         wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1
         wget_result=$?
     else
         wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1
         wget_result=$?
     fi
 
     if [[ $wget_result != 0 ]]; then
         download_error_msg="Unable to download $remote_path."
         # wget exit code 4 stands for network-issue
         if [[ $wget_result == 4 ]]; then
             download_error_msg+=" Failed to reach the server: connection timeout."
         fi
         say_verbose "$download_error_msg"
         return 1
     fi
 
     return 0
}

is_agent_package_installed() {
    eval "$invocation"

    if [ -f "$install_root" ]; then
        return 0
    elif [ -d "$install_root" ]; then
        return 0
    else
        return 1
    fi
}

machine_has() {
    eval "$invocation"

    command -v "$1" > /dev/null 2>&1
    return $?
}

install_agent_ubuntu() {
    eval "$invocation"
    say "Starting agent installation ..."
  
    # Download agent
    download_specific_agent_version "deb"

    # Check if a conf file is found
    if [ -z "$config_file" ]; then
        say "Configuration file not found. Installing Agent without configuration!"
    else
        say "Configuration file found. Installing Agent with config: $config_file"

        # Copy configuration
        say_verbose "Copy \`$config_file\` to /etc/flexx/settings.conf ..."
        mkdir -p "/etc/flexx"
        cp "$config_file" "/etc/flexx/settings.conf"
    fi

    # Install dependencies
    say "Installing agent dependencies ..."
    apt-get install x11-xserver-utils imvirt dmidecode -y

    # Install the deb file using dpkg
    say "Installing $version agent ..."
    dpkg -i "$output_path" 2> "$error_logs" &

    # Wait for installation finish
    while pgrep dpkg > /dev/null; do
        sleep 1
    done

    # Check error
    if [ -s "$error_logs" ]; then
    	if tail -n 10 "$error_logs" | grep -q "dependency problems prevent"; then
    		error_message=$(tail -n 10 "$error_logs" | grep "dependency problems prevent" | tail -n 1)
		else
			say_warning "$(cat "$error_logs")"
			error_message=""
    	fi
    	
        if [ -n "$error_message" ]; then
            say_err "Installation of the .deb file failed. Attempting to fix broken dependencies..."
            apt-get install --fix-broken -y > /dev/null 2>&1

            # Check if fixing broken dependencies was successful
            if [ $? -ne 0 ]; then
                say_err "Unable to fix broken dependencies. Installation failed."

                clear_tmp
                return 1
            fi
        else
            dpkg --configure -a > /dev/null 2>&1

            # Check if fixing broken dependencies was successful
            if [ $? -ne 0 ]; then
                say_err "Unable to fix installation. Installation failed."

                clear_tmp
                return 1
            fi
        fi
    fi

    # Check if the standard SDK version is installed.
    say_verbose "Checking installation: version = $version"
    if is_agent_package_installed; then
        say "Installed version is $version!"

        # Check linux service
        systemctl enable flexxagent.service
        systemctl start flexxagent.service
        systemctl --global enable flexxagent.service

        # Get list of logged-in users, remove duplicates, and ignore the system and service accounts
        active_users=$(who | awk '{print $1}' | sort | uniq)

        # Loop through each active user and start the service
        for user in $active_users; do
            dbus_path="/run/user/$(id -u $user)/bus"
            sanitized_user="${user//\\/\\\\}"
            if [ -r "$dbus_path" ]; then
                say "Starting service for user: $sanitized_user"
                su "$sanitized_user" -s /bin/bash -c "export DBUS_SESSION_BUS_ADDRESS="unix:path=$dbus_path" \
                && systemctl --user daemon-reload; systemctl --user start flexxagent.service; \
                systemctl --user daemon-reload"
            else
                say "Not starting service for user: $sanitized_user"
            fi
        done

        clear_tmp
        return 0
    fi

    # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
    say_err "Failed to verify the version installed.\nInstallation source: $final_download_link\nInstallation location: $install_root."
    say_err "FlexxAgent with \`$version\` failed to install with an error."

    clear_tmp
    return 1
}

install_agent_fedora() {
    eval "$invocation"
    say "Starting agent installation ..."
  
    # Download agent
    download_specific_agent_version "rpm"

    # Check if a conf file is found
    if [ -z "$config_file" ]; then
        say "Configuration file not found. Installing Agent without configuration!"
    else
        say "Configuration file found. Installing Agent with config: $config_file"

        # Copy configuration
        say_verbose "Copy \`$config_file\` to /etc/flexx/settings.conf ..."
        mkdir -p "/etc/flexx"
        cp "$config_file" "/etc/flexx/settings.conf"
    fi

    # Install dependencies
    say "Installing agent dependencies ..."
    dnf install xrandr imvirt dmidecode -y

    # Install the deb file using dpkg
    say "Installing $version agent ..."
    dnf install "$output_path" -y 2> "$error_logs" &

    # Wait for installation finish
    while pgrep dnf > /dev/null; do
        sleep 1
    done

    # Check error
    if [ -s "$error_logs" ]; then
        # TODO: Possible error handling on dnf?
        # Currently there are not any known errors
        say_warning "$(cat "$error_logs")"
        clear_tmp
    fi

    # Check if the standard SDK version is installed.
    say_verbose "Checking installation: version = $version"
    if is_agent_package_installed; then
        say "Installed version is $version!"

        # Check linux service
        systemctl daemon-reload

        # Get list of logged-in users, remove duplicates, and ignore the system and service accounts
        active_users=$(who | awk '{print $1}' | sort | uniq)

        # Loop through each active user and start the service
        for user in $active_users; do
            dbus_path="/run/user/$(id -u "$user")/bus"
            if [ -r "$dbus_path" ]; then
                say "Starting service for user: $user"
                su "$user" -s /bin/bash -c "export DBUS_SESSION_BUS_ADDRESS="unix:path=$dbus_path" \
                && systemctl --user daemon-reload; systemctl --user start flexxagent.user.service; \
                systemctl --user daemon-reload"
            else
                say "Not starting service for user: $user"
            fi
        done

        clear_tmp
        return 0
    fi

    # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
    say_err "Failed to verify the version installed.\nInstallation source: $final_download_link\nInstallation location: $install_root."
    say_err "FlexxAgent with \`$version\` failed to install with an error."

    clear_tmp
    return 1
}

install_agent_osx() {
    eval "$invocation"
    say "Starting agent installation ..."
  
    # Download agent
    download_specific_agent_version "pkg"

    # Check if a conf file is found
    if [ -z "$config_file" ]; then
        say "Configuration file not found. Installing Agent without configuration!"
    else
        say "Configuration file found. Installing Agent with config: $config_file"

        # Copy configuration
        say_verbose "Copy \`$config_file\` to /etc/flexx/settings.conf ..."
        mkdir -p "/etc/flexx"
        cp "$config_file" "/etc/flexx/settings.conf"
    fi

    # Install the deb file using dpkg
    say "Installing $version agent ..."
    installer -verbose -pkg "$output_path" -target /Applications/ 2> "$error_logs"

    # Check error
    if [ -s "$error_logs" ]; then
        # Currently there are not any known errors
        say_warning "$(cat "$error_logs")"
        clear_tmp
    fi

    # Check if the standard SDK version is installed.
    say_verbose "Checking installation: version = $version"
    if is_agent_package_installed; then
        say "Installed version is $version!"
        clear_tmp
        return 0
    fi

    # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
    say_err "Failed to verify the version installed.\nInstallation source: $final_download_link\nInstallation location: $install_root."
    say_err "FlexxAgent with \`$version\` failed to install with an error."

    clear_tmp
    return 1
}

get_linux_platform_name() {
    eval $invocation

	if [ -e /etc/os-release ]; then
		. /etc/os-release
		echo "$ID"
		return 0
	elif [ -e /etc/redhat-release ]; then
		local redhatRelease=$(</etc/redhat-release)
		if [[ $redhatRelease == "CentOS release 6."* || $redhatRelease == "Red Hat Enterprise Linux "*" release 6."* ]]; then
			echo "rhel.6"
			return 0
		fi
	fi

    say_verbose "Linux specific platform name and version could not be detected: UName = $uname"
    return 1
}

is_musl_based_distro() {
    (ldd --version 2>&1 || true) | grep -q musl
}

get_current_os_name() {
    eval $invocation

    local uname=$(uname)
    if [ "$uname" = "Darwin" ]; then
        echo "osx"
        return 0
    elif [ "$uname" = "FreeBSD" ]; then
        echo "freebsd"
        return 0
    elif [ "$uname" = "Linux" ]; then
        local linux_platform_name=""
        linux_platform_name="$(get_linux_platform_name)" || true

        if [ "$linux_platform_name" = "rhel.6" ]; then
            echo "$linux_platform_name"
            return 0
        elif is_musl_based_distro; then
            echo "linux-musl"
            return 0
        elif [ "$linux_platform_name" = "linux-musl" ]; then
            echo "linux-musl"
            return 0
        else
            echo "$linux_platform_name"
            return 0
        fi
    fi

    say_err "OS name could not be detected: UName = $uname"
    return 1
}

get_machine_architecture() {
    eval $invocation

    if command -v uname > /dev/null; then
        CPUName=$(uname -m)
        case $CPUName in
        armv*l)
            echo "arm"
            return 0
            ;;
        aarch64|arm64)
            if [ "$(getconf LONG_BIT)" -lt 64 ]; then
                # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)
                echo "arm"
                return 0
            fi
            echo "arm64"
            return 0
            ;;
        esac
    fi

    # Always default to 'x64'
    echo "x64"
    return 0
}

clear_tmp() {
    eval "$invocation"
    rm -f "$output_path" > /dev/null
    rm -f "$error_logs" > /dev/null
    say_verbose "Temporary file $output_path was removed"
}

# ---------------------------------------------------------------------------

args=("$@")
temporary_file_log="$(mktemp -u).log"

final_download_link="<auto>"
install_root="/usr/bin/flexxagent"
config_file=""
version="latest"
verbose=false

# Load os information
OS_NAME="$(get_current_os_name)"
OS_ARCH="$(get_machine_architecture)"

# Get arguments
while [ $# -ne 0 ]
do
    name="$1"
    case "$name" in
        -v|--version|-[Vv]ersion)
            shift
            version="${1//[$'\t\r\n']}"
            if [[ "$version" =~ ^[0-9.]*$ ]]; then
                version="v${version}"
            fi
            ;;
        -c|--config|-[Cc]onfig)
            shift
            config_file="$1"
            ;;
        -d|--distro|-[Dd]istro)
            shift
            OS_NAME="$1"
            ;;
        --verbose|-[Vv]erbose)
            verbose=true
            ;;
        -e|--environment|-[Ee]nvironment)
            verbose=true
            ;;
        -?|--?|-h|--help|-[Hh]elp)
            script_name="$(basename "$0")"
            echo "FlexxAgent Staging Installer"
            echo "Usage:"
            echo ""
            echo "  # Install the FlexxAgent with configuration file:"
            echo "  $script_name [-c|--config <path/file.conf>]"
            echo ""
            echo "  # Install the FlexxAgent of a specific version"
            echo "  $script_name [-v|--version <VERSION>]"
            echo ""
            echo "  # Force installation of FlexxAgent for a specific distro"
            echo "  $script_name [-d|--distro <DISTRO>]"
            echo ""
            echo "  # Install the FlexxAgent from a specific environment"
            echo "  $script_name [-e|--environment <ENVIRONMENT>]"
            echo ""
            echo "  $script_name -h|-?|--help"
            echo ""
            echo "Options:"
            echo "  -v,--version <VERSION>         Use specific VERSION. Defaults to \`$version\`."
            echo "  -d,--distro <DISTRO>           Use specific DISTRO. Autodetect the current distro is not set."
            echo "  --verbose,-Verbose             Display diagnostics information."
            echo "  -c,--config <CONFFILE>         Determines the agent configuration from a user specified settings file. Defaults to \`settings.conf\`."
            echo "  -?,--?,-h,--help,-Help         Shows this help message"
            echo ""
            exit 0
            ;;
        *)
            say_err "Unknown argument \`$name\`"
            exit 1
            ;;
    esac

    shift
done

case "$OS_NAME" in
    "ubuntu"|"debian")
	BASE_AGENT_URL="/agents/FlexxAgent/$version/debian/$OS_ARCH/"
    install_agent_ubuntu
    ;;
    "osx")
    install_root="/Applications/FlexxAgent.app/"
	BASE_AGENT_URL="/agents/FlexxAgent/$version/macos/$OS_ARCH/"
    install_agent_osx
    ;;
    "fedora")
	BASE_AGENT_URL="/agents/FlexxAgent/$version/fedora/$OS_ARCH/"
    install_agent_fedora
    ;;
    *)
    script_name="$(basename "$0")"
    say_err "Unsupported distro detected. Currently supported distros: [debian, fedora, ubuntu, osx].
    You can force installation for a specific supported distro with distro option:
    $script_name [-d|--distro <DISTRO>]"
    ;;
esac
