#!/bin/bash

# =============================================================================
# launch_script Scope
# =============================================================================
# 1. Check the requirements needed to run NodeZero and generated warnings or errors if those checks fail. 
# 2. Clean up previous NodeZero artifacts that may be left behind from previous runs.
# 3. Run independently as a check script on a NodeZero host system
# =============================================================================

# Check if interactive shell is available
USE_INTERACTIVE_SHELL=false
if [[ $- == *i* ]]; then
    USE_INTERACTIVE_SHELL=true
fi





# Formatting Variables
if [[ $USE_INTERACTIVE_SHELL == true ]]; then
    BOLD=$(tput bold)
    NORMAL=$(tput sgr0)
    RED=$(tput setaf 1)
    GREEN=$(tput setaf 2)
    YELLOW=$(tput setaf 3)
    MAGENTA=$(tput setaf 5)
    CYAN=$(tput setaf 6)
    FANCY=$(echo -e "cuu1\nel" | tput -S)
else
    BOLD=""
    NORMAL=""
    RED=""
    GREEN=""
    YELLOW=""
    MAGENTA=""
    CYAN=""
    FANCY=""
fi

# Create utility functions
function HeaderMsg {
    echo -e "\n${BOLD}[${CYAN}#${NORMAL}${BOLD}] ${CYAN}$@${NORMAL}"
}


function PassMsg {
    echo -e "${BOLD}[${GREEN}+${NORMAL}${BOLD}] ${NORMAL}${GREEN}PASSED: $@${NORMAL}"
}


function GenMsg {
    echo -e "${BOLD}[${GREEN}+${NORMAL}${BOLD}]${NORMAL}${GREEN} $@${NORMAL}"
}


function FailMsg {
    echo -e "${BOLD}[${RED}!${NORMAL}${BOLD}] ${NORMAL}${RED}FAILED: $@${NORMAL}"
}


function WarnMsg {
    echo -e "${BOLD}[${YELLOW}!${NORMAL}${BOLD}] ${NORMAL}${YELLOW}WARNING: $@${NORMAL}"
}


function AskMsg {
    read -p "${BOLD}[${MAGENTA}-${NORMAL}${BOLD}] ${NORMAL}${MAGENTA}CONFIRM: $@ (y|n) ${NORMAL}" -n 1 -r </dev/tty
}


function ExitMsg {
    echo -e "\n${BOLD}[${CYAN}#${NORMAL}${BOLD}] ${CYAN}$@${NORMAL}"
}

# Global Variables
UNAMES=$(uname -s)
UNAMER=$(uname -r)

# Detect container runtime (prefer docker, fallback to podman)
if command -v docker &> /dev/null; then
    CONTAINER_RUNTIME=docker
elif command -v podman &> /dev/null; then
    CONTAINER_RUNTIME=podman
    WarnMsg "Docker not found. Using Podman as container runtime."
else
    FailMsg "Neither Docker nor Podman found. Please install Docker or Podman and retry."
    exit 1
fi

# Detect SELinux status
SELINUX_ENABLED=false
if command -v getenforce &> /dev/null && [[ $(getenforce 2>/dev/null) != "Disabled" ]]; then
    SELINUX_ENABLED=true
fi

# Set volume suffix for SELinux bind mounts
if [[ $SELINUX_ENABLED == true ]]; then
    VOLUME_SUFFIX=":z"
else
    VOLUME_SUFFIX=""
fi

# Update CONTAINER_CMD and CONTAINER_RUN with detected runtime
if [[ $CONTAINER_RUNTIME == "podman" ]] || [[ $SUDO_REQ == YES ]]; then
    CONTAINER_CMD="sudo $CONTAINER_RUNTIME"
else
    CONTAINER_CMD=$CONTAINER_RUNTIME
fi

PROXY_CONFIGURED=false

# Check for proxy settings
# Docker: ~/.docker/config.json, Podman: ~/.config/containers/containers.conf
docker_proxy_config="$HOME/.docker/config.json"
if [[ $CONTAINER_RUNTIME == "docker" ]] && [ -f $docker_proxy_config ]; then
    if [ $(cat $docker_proxy_config | grep -io '}' | wc -l) -ge 2  ]; then

        # Check if proxy is set in default config
        if [ $(grep "httpsProxy" $docker_proxy_config | wc -l) -eq 1 ]; then

            # Find proxy and extract value
            proxy=`grep -o 'httpsProxy"\: "[^"]*' $docker_proxy_config | sed "s/.*\"//g"`
            if [ ! -z "$proxy" ]; then
                gateway_proxy_config='"proxies": {
                    "default": {
                        "httpProxy": "'${proxy}'",
                        "httpsProxy": "'${proxy}'",
                        "noProxy": "localhost,127.0.0.1,::1,172.16.0.0/16,10.0.0.0/8,192.168.0.0/16"
                    }
                }'
            fi
            # Proxy is empty
        fi
        # did not find "httpsProxy" in file
    fi
    # File does not have json content, or is empty
fi

if [[ "$CONTAINER_RUNTIME" == "podman" ]]; then
    container_proxy_config="$HOME/.config/containers/containers.conf"
    if [ -f "$container_proxy_config" ]; then
        # Podman containers.conf uses TOML with env = ["https_proxy=..."]
        if grep -qi 'https_proxy' "$container_proxy_config" 2>/dev/null; then
            proxy=$(grep -i 'https_proxy' "$container_proxy_config" | grep -o 'http[s]*://[^"]*' | head -1)
        fi
    fi
fi

if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
    unset DOCKER_CONFIG_PATH
    DOCKER_CONFIG_PATH=$(mktemp -d -t docker-XXXX)
fi



REGISTRY_URL="gateway.horizon3ai.eu"
REPOSITORY="docker/library"
IMAGE="$REGISTRY_URL/$REPOSITORY/hello-world:latest"
# If consolidated endpoint is NOT configured, use default time API for now
TIME_API="https://api.horizon3ai.com/v1/time"
# Docker-specific config: write HttpHeaders and auths to temp config.json
if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
    # Proxy not found or empty
    if [ -z "${gateway_proxy_config}" ]; then
        cat > $DOCKER_CONFIG_PATH/config.json << EOF
{
    "HttpHeaders": {
        "X-Meta-Docker": "True"
    },
    "auths": {
        "gateway.horizon3ai.eu": {}
    }
}
EOF

    # Proxy was found
    else
        cat > $DOCKER_CONFIG_PATH/config.json << EOF
{
    "HttpHeaders": {
        "X-Meta-Docker": "True"
    },
    "auths": {
        "gateway.horizon3ai.eu": {}
    },
    $gateway_proxy_config
}
EOF
    fi
fi



# Set up CONTAINER_RUN for running containers
# Docker: --config is a global flag (before subcommand) for HttpHeaders and auths
# Podman: does not use --config; uses registry. subdomain to route through public ECR
if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
    CONTAINER_RUN="$CONTAINER_CMD --config $DOCKER_CONFIG_PATH"
else
    CONTAINER_RUN="$CONTAINER_CMD"
    # Podman uses the registry. subdomain so the /v2/ ping and pulls
    # route to public ECR via the gateway's ecr-public-registries route
    REGISTRY_URL="registry.$REGISTRY_URL"
    IMAGE="$REGISTRY_URL/$REPOSITORY/hello-world:latest"
fi


# Docker proxy config checks and evaluation
# Sets: PROXY_CONFIGURED
function DockerProxyChk {
    local docker_config=false
    local docker_daemon=false
    local docker_service_proxy=false

    # Check ~/.docker/config.json (user's actual config, not the temp one we create)
    if [ -f ${DOCKER_CONFIG_PATH}/config.json ]; then
        docker_config_path_proxies=`sudo cat ${DOCKER_CONFIG_PATH}/config.json | grep -io proxy | wc -l`
        GenMsg "Found ${docker_config_path_proxies} of 3 proxy settings in ${DOCKER_CONFIG_PATH}/config.json"
        if [ $docker_config_path_proxies -ge 3 ]; then
            docker_config=true
        fi
    fi

    # Check /etc/docker/daemon.json
    if [ -f /etc/docker/daemon.json ]; then
        docker_daemon_proxies=`sudo cat /etc/docker/daemon.json | grep -io proxy | wc -l`
        GenMsg "Found ${docker_daemon_proxies} of 3 proxy settings in /etc/docker/daemon.json"
        if [ $docker_daemon_proxies -ge 3 ]; then
            docker_daemon=true
        fi
    fi

    # Check /etc/systemd/system/docker.service.d/http-proxy.conf
    if sudo [ -f /etc/systemd/system/docker.service.d/http-proxy.conf ]; then
        docker_service_proxies=`sudo cat /etc/systemd/system/docker.service.d/http-proxy.conf | grep -io proxy | wc -l`
        GenMsg "Found ${docker_service_proxies} of 6 proxy settings in /etc/systemd/system/docker.service.d/http-proxy.conf"
        if [ $docker_service_proxies -ge 6 ]; then
            docker_service_proxy=true
        fi
    fi

    # Evaluate Docker proxy state
    if [ "$docker_config" = true ] && [ "$docker_daemon" = true ] && [ "$docker_service_proxy" = true ] && [ "$ENV_PROXIES" = true ] && [ "$ENV_VARS" = true ]; then
        PROXY_CONFIGURED=true
        PassMsg "Proxy is detected"
    elif [ "$docker_config" = false ] && [ "$docker_daemon" = false ] && [ "$docker_service_proxy" = false ] && [ "$ENV_PROXIES" = false ] && [ "$ENV_VARS" = false ]; then
        PROXY_CONFIGURED=false
        PassMsg "Proxy is NOT configured"
    else
        # Dont exit, there could be multiple proxies or commented out etc...
        FailMsg "Proxy is not configured correctly, Please refer to https://docs.horizon3.ai/proxy-setup for guidance on setting a proxy"
    fi
}

# Podman proxy config checks and evaluation
# Sets: PROXY_CONFIGURED
function PodmanProxyChk {
    local podman_config_found=false

    # Podman reads proxy settings from ~/.config/containers/containers.conf
    podman_config="$HOME/.config/containers/containers.conf"
    if [ -f "$podman_config" ]; then
        podman_proxy_count=$(sudo cat ${podman_config} | grep -io proxy | wc -l)
        podman_proxy_count=${podman_proxy_count:-0}
        GenMsg "Found ${podman_proxy_count} of 3 proxy settings in $podman_config"
        if [ "$podman_proxy_count" -ge 3 ]; then
            podman_config_found=true
        fi
    fi

    # Evaluate Podman proxy state
    if [ "$podman_config_found" = true ] && [ "$ENV_PROXIES" = true ] && [ "$ENV_VARS" = true ]; then
        PROXY_CONFIGURED=true
        PassMsg "Proxy is detected"
    elif [ "$podman_config_found" = false ] && [ "$ENV_PROXIES" = false ] && [ "$ENV_VARS" = false ]; then
        PROXY_CONFIGURED=false
        PassMsg "Proxy is NOT configured"
    else
        # Dont exit, there could be multiple proxies or commented out etc...
        FailMsg "Proxy is not configured correctly, Please refer to https://docs.horizon3.ai/proxy-setup for guidance on setting a proxy"
    fi
}

# Check if a proxy is configured and setup
function ProxyChk {
    HeaderMsg "Checking Proxy settings and configurations:"

    ENV_VARS=false
    ENV_PROXIES=false
    PROXY_CONFIGURED=false

    # Check /etc/environment
    if [ -f /etc/environment ]; then
        env_proxies=`sudo cat /etc/environment | grep -ic proxy`
        GenMsg "Found ${env_proxies} of 6 proxy settings in /etc/environment"
        if [ $env_proxies -ge 6 ]; then
            ENV_PROXIES=true
        fi
    fi

    # Check if shell environment has http_proxy set
    env_shell_proxies=`env | grep -ic 'http_proxy\|https_proxy\|no_proxy'`
    GenMsg "Found ${env_shell_proxies} of 6 proxy settings in env"
    if [ $env_shell_proxies -ge 6 ]; then
        ENV_VARS=true
    fi

    # Runtime-specific proxy config checks and evaluation
    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        DockerProxyChk
    else
        PodmanProxyChk
    fi
}


# Validate supported OS is being used and set OS specific variables
function OsChk {

    HeaderMsg "Checking Operating System:"

    case $UNAMES in
        Linux*)
            OS="Linux"
            PassMsg "$OS is a supported Operating System."

            ArchChk

            HeaderMsg "Gathering environmental variables to conduct further checks:"
            if [[ $UNAMER =~ microsoft ]]; then
                FREE_SPACE_KB=$(df -Pk /init | awk 'NR==2 { print $4 }')
            else
                if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
                    CONTAINER_STORAGE_PATH=$($CONTAINER_CMD info 2>/dev/null | grep -i "Docker Root Dir" | sed 's/Docker Root Dir:\ //')
                else
                    CONTAINER_STORAGE_PATH=$($CONTAINER_CMD info --format '{{.Store.GraphRoot}}' 2>/dev/null)
                fi
                FREE_SPACE_KB=$(df -Pk ${CONTAINER_STORAGE_PATH:-.} | awk 'NR==2 { print $4 }')
            fi
            SPACE_DOC="https://docs.docker.com/config/pruning/"
            FS_DOC="https://docs.docker.com/docker-for-windows/#file-sharing"
            MEM_B=$(free -b | awk 'NR==2 { print $2 }')
            CPU=$(grep -c ^processor /proc/cpuinfo)
            PassMsg "All environmental variables set and proceeding with next checks."

            TimeChk
            ;;
        Darwin*)
            OS="macOS"
            PassMsg "$OS is a supported Operating System."

            ArchChk

            HeaderMsg "Gathering environmental variables to conduct further checks:"
            if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
                DOCKER_RAW="$HOME/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
                if [[ ! -f $DOCKER_RAW ]]; then
                    WarnMsg "A Docker configuration file is not in it's default location, an attempt to find it will be made that may ask for access to folders. If disallowed to search, the disk size validation check will not complete and report as a failed check."
                    DOCKER_RAW=$(find $HOME/Library/Containers -name Docker.raw 2>/dev/null | grep .)
                    EXIT=$?
                    if [[ $EXIT == 1 ]]; then
                        FailMsg "Unable to locate the Docker.raw file to validate enough HDD space is available. Please ensure Docker is installed properly and retry. See https://docs.docker.com/docker-for-mac/install/ for more information on how to install Docker Desktop for macOS."
                        exit 1
                    fi
                fi
                USED_SPACE_KB=$(ls -ksn $DOCKER_RAW | awk '{ print $1 }')
                MAX_SPACE_KB=$(( $(ls -ksn $DOCKER_RAW | awk '{ print $6 }') / 1024 ))
                FREE_SPACE_KB=$(( $MAX_SPACE_KB - $USED_SPACE_KB ))
                SPACE_DOC="https://docs.docker.com/docker-for-mac/space/"
                FS_DOC="https://docs.docker.com/docker-for-mac/#file-sharing"
            else
                # Podman on macOS uses a different VM approach; use df for free space
                FREE_SPACE_KB=$(df -Pk . | awk 'NR==2 { print $4 }')
                SPACE_DOC="https://docs.podman.io"
                FS_DOC="https://docs.podman.io"
            fi

            MEM_B=$(sysctl -a | grep hw.memsize_usable | awk 'NR==1{ print $2 }')
            if [[ -z "$MEM_B" ]]; then
                MEM_B=$(sysctl -a | grep hw.mem | awk 'NR==1{ print $2 }')
            fi
            CPU=$(sysctl -a | grep hw.logicalcpu_max | awk '{ print $2 }')
            if [[ -z "$MEM_B" ]]; then
                FailMsg "Unable to determine system properties. Please ensure sysctl can be found on the system PATH."
                exit 1
            fi
            PassMsg "All environmental variables set and proceeding with next checks."

            TimeChk
            ;;
        *)
            FailMsg "$UNAMES is not a currently supported Operating System. Please provide feedback to add this Operating System."
            exit 1
            ;;
    esac
}


# Check time against UTC for max of 5 minutes of drift
# Test time drift by disabling ntp and adjusting time using:
# timedatectl set-ntp off
# date -s "now - 12 minutes"
function TimeChk {

    HeaderMsg "Checking host time against current UTC time:"

    TIME=$(curl -k -s -m 3 $TIME_API)
    EXIT=$?
    if [[ $EXIT != 0 ]]; then
        sleep 30
        TIME=$(curl -k -s -m 3 $TIME_API)
    	EXIT=$?
    	if [[ $EXIT != 0 ]]; then
            WarnMsg "Unable to determine current UTC time to validate time is within a 5 minute variance. The URL $TIME_API was unreachable."
            return 0
        fi
	fi

    VALID_TIME=$(echo "$TIME" | grep EpochTime)
    EXIT=$?
    if [[ $EXIT != 0 ]]; then
        WarnMsg "Unable to determine current UTC time to validate time is within a 5 minute variance. The URL $TIME_API did not return a valid timestamp."
        return 0
    fi

    declare -i UTC=$(echo "$TIME" | sed -n 's/.*"EpochTime"\s*:\s*"\([0-9]\{10\}\).*/\1/p')
    declare -i LOCAL=$(date +%s)

    ABSDIFF=$(( UTC > LOCAL ? UTC - LOCAL : LOCAL - UTC ))
    if [[ $ABSDIFF -le 300 ]]; then
        PassMsg "System time is within 5 minutes of UTC time."
    else
        FailMsg "The system time is off by more than 5 minutes from UTC time. Please set or resync your system time before running again. For additional information, see: (https://docs.horizon3.ai/t/time-oos)"
        exit 1
    fi
}


# Check if you can run the container runtime
function ContainerRuntimeChk {

    HeaderMsg "Checking ${CONTAINER_RUNTIME^} functionality by running the hello-world test container:"

    CleanUp

    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        declare -i DOCKER_VERSION=$($CONTAINER_RUNTIME -v | awk '{ print $3 }' | awk -F '.' '{ print $1 }')
        if [[ $DOCKER_VERSION -ge 20 ]]; then
            PassMsg "Docker version installed meets the minimum required version 20.10."
        else
            FailMsg "Docker major version $DOCKER_VERSION is unsupported, please upgrade Docker to the minimum required version 20.10."
            exit 1
        fi
    else
        declare -i PODMAN_VERSION=$($CONTAINER_RUNTIME -v | awk '{ print $3 }' | awk -F '.' '{ print $1 }')
        if [[ $PODMAN_VERSION -ge 4 ]]; then
            PassMsg "Podman version installed meets the minimum required version 4.0."
        else
            FailMsg "Podman major version $PODMAN_VERSION is unsupported, please upgrade Podman to the minimum required version 4.0."
            exit 1
        fi
    fi

    local retry_count=0
    local max_retries=5
    local EXIT=0

    $CONTAINER_RUN run --rm --name h3-test $IMAGE &> /dev/null
    EXIT=$?

    while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
        FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Retrying check (attempt $retry_count)..."

        retry_count=$((retry_count + 1))
        sleep 2  # Wait for 2 second before retrying

        $CONTAINER_RUN run --rm --name h3-test $IMAGE &> /dev/null
        EXIT=$?
    done

    case $EXIT in
        0*)
            PassMsg "${CONTAINER_RUNTIME^} is installed and functioning properly."
            PermChk
            ;;
        125*)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Verify the ${CONTAINER_RUNTIME} daemon/service is started and retry."
            return 1
            ;;
        126*)
            
            SudoChk
            
            ;;
        127*)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Verify that ${CONTAINER_RUNTIME} is installed and retry."
            return 1
            ;;
        *)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Please ensure ${CONTAINER_RUNTIME} is installed and this user has permission to execute."
            return 1
            ;;
    esac

    CleanUp
}


# Retry container runtime check with "docker hub sourced hello-world" image if the initial check fails
function RetryContainerRuntimeChk {

    HeaderMsg "Retrying ${CONTAINER_RUNTIME^} functionality check with Docker Hub sourced hello-world image:"

    # Deprecation message
    WarnMsg "Deprecation Notice: Docker Hub (docker.io) sourced images will no longer be available after the May 2025 release."
    WarnMsg "Please update your network allowlist configuration per https://docs.horizon3.ai/quickstart/network_requirements/ before May 2025."

    REGISTRY_URL="docker.io"
    REPOSITORY="library"
    IMAGE="$REGISTRY_URL/$REPOSITORY/hello-world:latest"

    $CONTAINER_RUN run --rm --name h3-test $IMAGE &> /dev/null
    EXIT=$?

    case $EXIT in
        0*)
            PassMsg "${CONTAINER_RUNTIME^} is installed and functioning properly with the hello-world image."
            PermChk
            ;;
        125*)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^} with the hello-world image. Verify the ${CONTAINER_RUNTIME} daemon/service is started and retry."
            exit 1
            ;;
        126*)
            
            SudoChk
            
            ;;
        127*)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^} with the hello-world image. Verify that ${CONTAINER_RUNTIME} is installed and retry."
            exit 1
            ;;
        *)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^} with the hello-world image. Please ensure ${CONTAINER_RUNTIME} is installed and this user has permission to execute."
            exit 1
            ;;
    esac

    CleanUp

}


function ContainerProxyCheck {

    if [ "$PROXY_CONFIGURED" = true ]; then
        HeaderMsg "Checking ${CONTAINER_RUNTIME^} proxy settings:"

        local retry_count=0
        local max_retries=5
        local EXIT=0

        container_proxies=`$CONTAINER_RUN run --rm $REGISTRY_URL/$REPOSITORY/alpine sh -c 'env | grep -ic proxy'`
        EXIT=$?

        while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}, retrying check (attempt $retry_count)..."

            retry_count=$((retry_count + 1))
            sleep 2  # Wait for 2 second before retrying

            container_proxies=`$CONTAINER_RUN run --rm $REGISTRY_URL/$REPOSITORY/alpine sh -c 'env | grep -ic proxy'`
            EXIT=$?
        done

        if [ $retry_count -eq $max_retries ]; then
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Verify the ${CONTAINER_RUNTIME} daemon/service is started and retry."
            exit 1
        fi

        GenMsg "Found ${container_proxies} of 6 proxy settings in env"
        AlpineCleanUp
        if [ $container_proxies -eq 6 ]; then
            PassMsg "Container has proxy configured"
        else
            FailMsg "Proxy environment variables not found in container"
            exit 1
        fi
    fi
}


# Check permissions for container runtime to mount the read-only Op Config
function PermChk {

    # Use $HOME instead of $PWD for the mount test — the runner service may
    # launch from / which is not writable or mountable in most configurations
    PERM_CHK_DIR="${HOME:-.}"
    HeaderMsg "Checking ${CONTAINER_RUNTIME^} permissions to volume mount files from $PERM_CHK_DIR directory:"

    mkdir -p $PERM_CHK_DIR/h3.txt &> /dev/null
    $CONTAINER_RUN run --rm --name h3-test -v $PERM_CHK_DIR/h3.txt:/h3.txt${VOLUME_SUFFIX} $IMAGE &> /dev/null
    EXIT=$?
    rmdir $PERM_CHK_DIR/h3.txt &> /dev/null

    case $EXIT in
        0*)
            PassMsg "${CONTAINER_RUNTIME^} permissions are correct for the $PERM_CHK_DIR directory location."
            CleanUp
            ;;
        125*)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. The path $PERM_CHK_DIR is not shared and is not known to ${CONTAINER_RUNTIME}. Retry from a directory path that allows ${CONTAINER_RUNTIME} to mount files from $PERM_CHK_DIR. See $FS_DOC for more information."
            exit 1
            ;;
        *)
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Please ensure ${CONTAINER_RUNTIME} is installed and this user has permission to execute."
            exit 1
            ;;
    esac

    CleanUp
}


# Check if sudo is required to run container runtime commands
function SudoChk {

    WarnMsg "Unable to validate ${CONTAINER_RUNTIME^} due to insufficient privileges."
    AskMsg "Would you like to retry using sudo privileges?"

    echo
    if [[ $REPLY =~ ^[Yy]$ ]]; then

        HeaderMsg "Attempting to validate ${CONTAINER_RUNTIME^} using sudo privileges:"

        # Check that sudo is used
        if echo "$CONTAINER_RUN" | grep -q 'sudo'; then
            SUDO_CONTAINER_RUN=$CONTAINER_RUN
        else
            SUDO_CONTAINER_RUN="sudo $CONTAINER_RUN"
        fi

        local retry_count=0
        local max_retries=5
        local EXIT=0

        $SUDO_CONTAINER_RUN run --rm --name h3-test $IMAGE &> /dev/null
        EXIT=$?

        while [[ $EXIT -eq 125 && $retry_count -lt $max_retries ]]; do
            FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Retrying check (attempt $retry_count)..."

            retry_count=$((retry_count + 1))
            sleep 2  # Wait for 2 second before retrying

            $SUDO_CONTAINER_RUN run --rm --name h3-test $IMAGE &> /dev/null
            EXIT=$?
        done

        case $EXIT in
            0*)
                WarnMsg "Check completed successfully, sudo will be used to run all ${CONTAINER_RUNTIME^} commands with this user."
                SUDO_REQ="YES"
                CONTAINER_CMD="sudo $CONTAINER_RUNTIME"
                if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
                    CONTAINER_RUN="$CONTAINER_CMD --config $DOCKER_CONFIG_PATH"
                else
                    CONTAINER_RUN="$CONTAINER_CMD"
                fi
                PermChk
                ;;
            1*)
                FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Verify this account is in the sudoers file and retry."
                exit 1
                ;;
            126*)
                FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Verify this account has permissions to run ${CONTAINER_RUNTIME} and retry."
                exit 1
                ;;
            *)
                FailMsg "Failed to validate ${CONTAINER_RUNTIME^}. Please ensure ${CONTAINER_RUNTIME} is installed and this user has permission to execute."
                exit 1
                ;;
        esac

        CleanUp
    else
        CleanUp
        ExitMsg "Exiting without validating ${CONTAINER_RUNTIME^}, please allow this user ${CONTAINER_RUNTIME} privileges and retry."
        exit 1
    fi
}


# Check if disk space has room; $FREE_SPACE_KB is set in OsChk().
function DiskChk {

    HeaderMsg "Checking HDD space requirements (minimum 30GB Recommended, 20GB Required):"

    # IMG_SIZE_KB is to account for NodeZero image size if provided.
    if [[ $IMG_SIZE_KB -gt 0 ]]; then
        FREE_SPACE_KB=$(( $FREE_SPACE_KB + $IMG_SIZE_KB ))
    fi

    FREE_SPACE_GB="$(( $FREE_SPACE_KB / 1024 / 1024 ))GB"
    if [[ $FREE_SPACE_KB -ge 31457280 ]]; then
        PassMsg "There is enough space for the NodeZero container: $FREE_SPACE_GB"
    elif [[ $FREE_SPACE_KB -le 20971520 ]]; then
        FailMsg "$FREE_SPACE_GB is not enough space to support NodeZero. Remove unused containers and volumes to reclaim space and retry. See $SPACE_DOC for more information on how to remove unused images or containers to reclaim space."  # Only exit with checkscript, for launch_script allow the script to continue
        exit 1
        
    else
        WarnMsg "$FREE_SPACE_GB is less than the recommended 30GB free space on this disk, please ensure to prune old images before running Node Zero again."
    fi
}


# Check if enough memory; $MEM_B variable should be in bytes
function MemChk {

    HeaderMsg "Checking 8GB RAM requirement:"

    MEM_GB="$(( $MEM_B / 1024 / 1024 / 1024 ))GB"
    # Leave a little buffer for kernel reservations
    if [[ $MEM_B -ge 8053063680 ]]; then
        PassMsg "This system meets the recommended minimum RAM to support NodeZero."
    else
        WarnMsg "It is recommended to have a minimum of 8GB RAM to run NodeZero. This system has $MEM_GB RAM and you may experience sluggish NodeZero performance."
    fi
}


# Check if enough compute
function CpuChk {

    HeaderMsg "Checking compute resource requirements:"

    if [[ $CPU -ge 2 ]]; then
        PassMsg "This system has $CPU CPUs which meets the minimum logical CPU requirements to run NodeZero."
    else
        WarnMsg "It is recommended to have a minimum of 2 logical CPUs to run NodeZero. This system has $CPU CPUs and you may experience sluggish NodeZero performance."
    fi
}


# Check if CPU architecture is supported (x86_64/amd64 only)
function ArchChk {

    HeaderMsg "Checking CPU architecture compatibility:"

    ARCH=$(uname -m)

    case $ARCH in
        x86_64|amd64)
            PassMsg "CPU architecture '$ARCH' is supported by NodeZero."
            ;;
        arm64|aarch64|armv*)
            FailMsg "CPU architecture '$ARCH' is not supported. NodeZero requires an x86_64 (amd64) processor. ARM-based systems (including Apple Silicon Macs and ARM servers) are not supported."
            exit 1
            ;;
        *)
            FailMsg "Unknown CPU architecture '$ARCH'. NodeZero requires an x86_64 (amd64) processor."
            exit 1
            ;;
    esac
}


function EdrDefinitions {
    # Get list of common EDR processes to check for
    # CrowdStrike Falcon (falcon)
    cs_falcon="csfalconservice|csagent|falcon"

    # Microsoft Defender for Endpoint (mdatp)
    ms_defender="msmpeng|mssense|nissrv|sensendr|mdatp"

    # SentinelOne Singularity (sentinel)
    sentinel="sentinelone|sentineld|sentinelctl"

    # Palo Alto Networks Cortex XDR (pmd|dypd|traps)
    pa_cortex="pmd|dypd|traps|cortex|cyveraservice|cyserver"

    # Sophos Intercept X (sophos)
    sophos="savservice|sophoshealth|sophossps|sophosfilescanner|sophosclean|sophososquery"

    # Elastic Security (elastic)
    elastic="elastic-endpoint|elastic-agent"

    # VMware Carbon Black (cbagent)
    vm_cb="cbdaemon|cbagent|cbdefense|repmgr|rtvscand"

    # Symantec/Broadcom
    sb="ccsvchst|smc|symcorpui"

    # Cisco Secure Endpoint (amp)
    cisco_se="ampcli|ampscansvc|ampdaemon"

    # Cybereason (cybereason)
    cr="cybereason|minionhost|crsensor"

    # Cylance/ArcticWolf (cylance)
    aw="arcticwolfagent|arcticwolfdesktop|cylancesvc|wazuh"

    # McAfee/Trellix/FireEye
    mcafee="mfetpd|mfetp|mfemactl|isectpd|mvision|mvedr|mvedrcontrol|xagt"

    # Trend Micro/Apex One
    tm="tmntsrv|tmlisten|ds_agent"

    # Bitdefender
    bitdefender="bdagent|vsserv|bdservice|bdsec|bitdefender-security-tools"

    # Fortinet
    fortinet="fortiedr|fortiesnac"

    # Malwarebytes
    malwarebytes="mbamservice|mbam"

    # Tanium
    tanium="taniumclient"

    # Rapid7
    rapid7="ir_agent"

    echo "$cs_falcon|$ms_defender|$sentinel|$pa_cortex|$sophos|$elastic|$vm_cb|$cisco_se|$cr|$aw|$sb|$mcafee|$tm|$bitdefender|$fortinet|$malwarebytes|$tanium|$rapid7"

}

# Checks if an EDR is running on the NodeZero host that may interfere with NodeZero operations.
function EdrChk {

    local EDR_DEFINITIONS=$(EdrDefinitions)

    # EDR_PROCESSES=$(ps aux | grep -i "$EDR_DEFINITIONS" | awk '{print $2, $11}' | grep -v 'grep')
    EDR_PROCESSES=$(pgrep -aif "$EDR_DEFINITIONS" || true)

    if command -v systemctl &> /dev/null; then
        EDR_SERVICES=$(systemctl list-units | grep -i "$EDR_DEFINITIONS")
    fi


    if [[ -n "$EDR_PROCESSES" ]] || [[ -n "$EDR_SERVICES" ]]; then
        WarnMsg "EDR processes or services detected!!!"
        WarnMsg "Please disable the EDR on this system that may interfere with NodeZero operations."

        if [[ -n "$EDR_PROCESSES" ]]; then
            WarnMsg "Detected EDR Processes:"
            echo "$EDR_PROCESSES"
        fi
        if [[ -n "$EDR_SERVICES" ]]; then
            WarnMsg "Detected EDR Services:"
            echo "$EDR_SERVICES"
        fi
        sleep 5 # Sleep to allow user to read the detected EDR processes and services before the script continues
    else
        PassMsg "No common EDR processes or services detected on this system."
    fi
}


# Check network connectivity and SSL certificate validity for required hosts
# Reference: https://docs.horizon3.ai/quickstart/network_requirements/#your-portal-region
function NetworkChk {

    HeaderMsg "Checking network connectivity and SSL certificates for required hosts:"

    NETWORK_WARNINGS=0
    NETWORK_DOC="https://docs.horizon3.ai/quickstart/network_requirements/"

    # Determine which hosts to check based on the gateway/registry URL
    
    GATEWAY_HOST="gateway.horizon3ai.eu"
    USING_GATEWAY="false"
    

    # Define hosts based on region (URLs for SSL certificate checks)
    if [[ "$GATEWAY_HOST" == *"horizon3ai.eu"* && "$USING_GATEWAY" == "false" ]]; then
        # EU Region hosts - https://docs.horizon3.ai/quickstart/network_requirements/#eu-based-portal
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.eu"
            "https://interact.gateway.horizon3ai.eu"
            "https://api.gateway.horizon3ai.eu"
            "https://registry.gateway.horizon3ai.eu"
            "https://api.horizon3ai.eu"
            "https://cognito-identity.eu-central-1.amazonaws.com"
            "https://cognito-idp.eu-central-1.amazonaws.com"
            "https://downloads.horizon3ai.com"
            "https://sqs.eu-central-1.amazonaws.com"
            "https://ecr.eu-central-1.amazonaws.com"
            "https://queue.amazonaws.com"
            "https://s3.amazonaws.com"
            "https://s3.eu-central-1.amazonaws.com"
            "https://s3-w.eu-central-1.amazonaws.com"
            "https://s3-r-w.eu-central-1.amazonaws.com"
            "https://canonical.com"
             # "https://ineracth3t.eu" In public docs but host check fails. Need to follow up on this host.
            "https://ubuntu.com"
        )
    elif [[ "$GATEWAY_HOST" == *"horizon3ai.eu"* && "$USING_GATEWAY" == "true" ]]; then
        # EU Gateway URLs - https://docs.horizon3.ai/quickstart/network_requirements/#eu-based-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.eu"
            "https://interact.gateway.horizon3ai.eu"
            "https://api.gateway.horizon3ai.eu"
            "https://registry.gateway.horizon3ai.eu"
        )
    elif [[ "$GATEWAY_HOST" == *"gov-horizon3ai.com"* ]]; then
        # FedRAMP/Federal hosts - https://docs.horizon3.ai/quickstart/network_requirements/#fedramp-high-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.gov-horizon3ai.com"
            "https://interact.gateway.gov-horizon3ai.com"
            "https://api.gateway.gov-horizon3ai.com"
        )
    elif [[ "$USING_GATEWAY" == "false" ]]; then
        # US Region hosts - https://docs.horizon3.ai/quickstart/network_requirements/#us-based-portal
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.com"
            "https://interact.gateway.horizon3ai.com"
            "https://api.gateway.horizon3ai.com"
            "https://registry.gateway.horizon3ai.com"
            "https://api.horizon3ai.com"
            "https://cognito-identity.us-east-2.amazonaws.com"
            "https://cognito-idp.us-east-2.amazonaws.com"
            "https://downloads.horizon3ai.com"
            "https://sqs.us-east-2.amazonaws.com"
            "https://ecr.us-east-2.amazonaws.com"
            "https://queue.amazonaws.com"
            "https://s3.amazonaws.com"
            "https://s3.us-east-1.amazonaws.com"
            "https://s3.us-east-2.amazonaws.com"
            "https://s3-w.us-east-2.amazonaws.com"
            "https://ubuntu.com"
            "https://canonical.com"
            # "https://interacth3.io" In public docs but host check fails. Need to follow up on this host.
        )
    else
        # US Region gateway hosts - https://docs.horizon3.ai/quickstart/network_requirements/#us-based-nodezero-gateway
        REQUIRED_URLS=(
            "https://gateway.horizon3ai.com"
            "https://interact.gateway.horizon3ai.com"
            "https://api.gateway.horizon3ai.com"
            "https://registry.gateway.horizon3ai.com"
            "https://api.horizon3ai.com"
        )
    fi

    GenMsg "Checking connectivity and SSL certificates for region: $GATEWAY_HOST"

    for url in "${REQUIRED_URLS[@]}"; do
        # Extract host and port from URL
        host=$(echo "$url" | sed -E 's#https?://([^:/]+).*#\1#')
        port=$(echo "$url" | grep -oE ':[0-9]+' | tr -d ':')
        [[ -z "$port" ]] && port=443

        echo -ne "  Checking: $url ... "

        # First check SSL certificate using openssl
        # Use timeout on Linux, gtimeout on macOS (if available), or no timeout as fallback
        if [[ "$UNAMES" == "Darwin" ]]; then
            if command -v gtimeout &>/dev/null; then
                cert_output=$(echo | gtimeout 10 openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
            else
                cert_output=$(echo | openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
            fi
        else
            cert_output=$(echo | timeout 10 openssl s_client -servername "$host" -connect "$host:$port" 2>/dev/null)
        fi
        OPENSSL_EXIT=$?

        if [[ $OPENSSL_EXIT -ne 0 ]] || [[ -z "$cert_output" ]]; then
            # Connection failed - determine why using curl for better error messages
            HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -m 10 "$url" 2>/dev/null)
            CURL_EXIT=$?

            if [[ $CURL_EXIT -eq 6 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Cannot resolve hostname $host - DNS resolution failed"
                WarnMsg "  Please ensure your DNS can resolve $host or add it to your hosts file"
                WarnMsg "  See $NETWORK_DOC for network requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            elif [[ $CURL_EXIT -eq 7 ]] || [[ $CURL_EXIT -eq 28 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Cannot connect to $host - connection refused or timed out"
                WarnMsg "  This host may be blocked by a firewall. Please ensure outbound HTTPS (443/TCP) is allowed"
                WarnMsg "  See $NETWORK_DOC for network requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            elif [[ $CURL_EXIT -eq 60 ]] || [[ $CURL_EXIT -eq 51 ]]; then
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "SSL certificate issue connecting to $host - your firewall may be intercepting HTTPS traffic"
                WarnMsg "  This can happen when a corporate firewall or proxy performs SSL inspection"
                WarnMsg "  Please ensure $host is allowlisted in your firewall. See $NETWORK_DOC"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            else
                echo -e "${RED}FAIL${NORMAL}"
                WarnMsg "Failed to connect to $host (connection error)"
                WarnMsg "  Please verify network connectivity. See $NETWORK_DOC for requirements"
                NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            fi
            continue
        fi

        # Check SSL certificate expiration
        not_after=$(echo "$cert_output" | openssl x509 -noout -enddate 2>/dev/null | sed 's/notAfter=//')
        if [[ -z "$not_after" ]]; then
            echo -e "${RED}FAIL${NORMAL}"
            WarnMsg "Could not parse SSL certificate for $host"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
            continue
        fi

        # Convert certificate expiration to epoch (handle both Linux and macOS date formats)
        if [[ "$UNAMES" == "Darwin" ]]; then
            end_epoch=$(date -j -f "%b %d %H:%M:%S %Y %Z" "$not_after" +%s 2>/dev/null)
        else
            end_epoch=$(date -d "$not_after" +%s 2>/dev/null)
        fi
        now_epoch=$(date +%s)

        if [[ -z "$end_epoch" ]]; then
            echo -e "${YELLOW}WARN${NORMAL}"
            WarnMsg "Could not determine certificate expiration date for $host"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
        elif [[ $now_epoch -gt $end_epoch ]]; then
            echo -e "${RED}FAIL${NORMAL}"
            WarnMsg "SSL certificate for $host has EXPIRED (expired: $not_after)"
            WarnMsg "  This may indicate a firewall/proxy intercepting HTTPS traffic with an expired certificate"
            WarnMsg "  Please ensure $host is allowlisted in your firewall. See $NETWORK_DOC"
            NETWORK_WARNINGS=$((NETWORK_WARNINGS + 1))
        else
            echo -e "${GREEN}PASS${NORMAL}"
        fi
    done

    if [[ $NETWORK_WARNINGS -eq 0 ]]; then
        PassMsg "All required hosts are reachable with valid SSL certificates."
    else
        WarnMsg "$NETWORK_WARNINGS host(s) had connectivity or SSL certificate issues. NodeZero may not function correctly."
        WarnMsg "Please review the warnings above and ensure all required hosts are accessible."
        WarnMsg "For full network requirements, see: $NETWORK_DOC"
    fi
}


# Cleanup hello-world containers created by checks
function CleanUp {
    $CONTAINER_CMD rmi $IMAGE -f &> /dev/null
}


function AlpineCleanUp {
    $CONTAINER_CMD rmi $REGISTRY_URL/$REPOSITORY/alpine:latest -f &> /dev/null
}


# If nodezero_version is defined, remove all nodezero images except the version defined to reclaim space
function KeepCurrentNodeZeroImage {
    # Find all NodeZero images except the current version
    # Match h3/n0: anywhere in the repository name (handles both h3/n0: and registry/h3/n0:)
    OLD_IMAGES=$($CONTAINER_CMD images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null | grep "h3/n0:" | grep -v ":${NODEZERO_VERSION}$" || true)

    if [[ -z "$OLD_IMAGES" ]]; then
        GenMsg "No old NodeZero images found to clean up."
        return 0
    fi

    for image in $OLD_IMAGES; do
        # Get image size in bytes, then convert to KB
        IMG_SIZE_BYTES=$($CONTAINER_CMD images --format "{{.Size}}" "$image" 2>/dev/null | head -1)

        # Try to remove the image
        if $CONTAINER_CMD rmi "$image" -f &> /dev/null; then
            GenMsg "Removed old NodeZero image: $image (${IMG_SIZE_BYTES})"
        else
            WarnMsg "Could not remove image: $image (may be in use)"
        fi
    done
}


# Check if previous NodeZero images exist and remove to reclaim space
# if nodezero_version is defined, remove all container images except the version defined
# if nodezero_version is not defined, remove all container images except the most recent image based on creation date
function RemoveNodeZeroArtifacts {

    HeaderMsg "Checking for previous NodeZero configuration file artifacts:"

    # Check in .nodezero folder
    if ls ~/.nodezero/n0*.conf &>/dev/null; then
        # legacy: deletes configs from PWD
        rm $PWD/n0*.conf &>/dev/null
        rm ~/.nodezero/n0*.conf
        PassMsg "Identified and deleted all previous NodeZero configuration files."
    else
        GenMsg "No previous NodeZero configuration files identified."
    fi

    HeaderMsg "Checking for previous NodeZero container artifacts to remove and reclaim space:"
    # Cleanup unused containers
    GenMsg "Remove exited node-zero containers"
    $CONTAINER_CMD rm $($CONTAINER_CMD ps -a -q -f status=exited -f "name=n0-[0-9A-Fa-f]{4}") 2>/dev/null || true

    HeaderMsg "Checking for previous NodeZero image artifacts to remove and reclaim space:"
    # Capture current state of images
    ALL_IMG=$($CONTAINER_CMD images --format='table {{.ID}}\t{{.Repository}}\t{{.Tag}}')
    GenMsg "Current state of images is as follows:"
    echo "$ALL_IMG"



    # Remove all n0 images except the most recent
    IMG=$($CONTAINER_CMD images --format='table {{.ID}}\t{{.Repository}}\t{{.Tag}}' | grep h3\/n0 | awk '{ print $1 }')
    IMG_CNT=$(echo $IMG | wc -w)

    if [[ $IMG_CNT -ge 1 ]]; then
        GenMsg "Identified at least one NodeZero image, checking if latest and attempting to remove all others not being used:"

        # Get most recent image date among the NodeZero images to identify the latest image to keep
        NEWEST_IMG=0 # Initialize to a very old date
        for X in $IMG; do
            # Get the most recent image date
            IMG_DATE_RAW=$($CONTAINER_CMD inspect $X --format='{{ .Created }}')
            GenMsg "Image $X was created on: $IMG_DATE_RAW"
            # Normalize Go-style timestamps: strip nanoseconds and trailing "UTC"
            # e.g. "2026-05-05 16:28:20.935794066 +0000 UTC" -> "2026-05-05 16:28:20 +0000"
            IMG_DATE_NORMALIZED=$(echo "$IMG_DATE_RAW" | sed 's/\.[0-9]*//' | sed 's/ UTC$//')
            IMG_DATE=$(date -d "$IMG_DATE_NORMALIZED" +%s 2>/dev/null) # Convert to seconds since epoch

            if [[ -z "$IMG_DATE" || $IMG_DATE -eq 0 ]]; then
                FailMsg "Unable to parse image date for $X, skipping."
                continue
            elif [[ $NEWEST_IMG -lt $IMG_DATE ]]; then
                NEWEST_IMG=$IMG_DATE
            fi
        done

        # Delete all but the most recent image
        for X in $IMG; do
            IMG_DATE_RAW=$($CONTAINER_CMD inspect $X --format='{{ .Created }}')
            # Normalize Go-style timestamps
            IMG_DATE_NORMALIZED=$(echo "$IMG_DATE_RAW" | sed 's/\.[0-9]*//' | sed 's/ UTC$//')
            IMG_DATE=$(date -d "$IMG_DATE_NORMALIZED" +%s 2>/dev/null) # Convert to seconds since epoch

            if [[ $IMG_DATE -ne $NEWEST_IMG ]]; then
                GenMsg "Removing image $X..."
                $CONTAINER_CMD rmi $X -f &>/dev/null
                EXIT=$?
                if [[ $EXIT == 0 ]]; then
                    PassMsg "Successfully removed $X image."
                else
                    FailMsg "Unable to remove $X image. It may be used by a container or referenced in multiple repositories. Please stop the dependent container if not needed and/or attempt to remove manually."
                fi
            else
                GenMsg "The $X image is the latest and will NOT be deleted."
                image_size=$($CONTAINER_CMD inspect $X --format='{{ .Size }}')
                IMG_SIZE_KB=$(( image_size / 1024 ))
            fi
        done
    else
        GenMsg "No previous NodeZero images identified."
    fi



    # Remove untagged images
    ALL_DANGLING=$($CONTAINER_CMD images -f dangling=true -q)
    if [[ -n "$ALL_DANGLING" ]]; then
        GenMsg "Dangling images found, Checking if any are NodeZero images..."
        for X in $ALL_DANGLING; do
            # Looks at the Cmd value to get the start NodeZero command
            if $CONTAINER_CMD image inspect $X --format='{{.Config.Cmd}}' | grep 'app/nodezero.py' > /dev/null; then
                GenMsg "Found dangling NodeZero image. Attempting to remove dangling image $X..."
                $CONTAINER_CMD rmi $X
            fi
        done
    else
        GenMsg "No dangling images identified."
    fi

    PassMsg "NodeZero artifact cleanup completed."

}





# Allow clean exit by trapping ctrl+c and calling ctrl_c()
trap ctrl_c INT


function ctrl_c() {
    echo
    ExitMsg "Ctrl+C detected, exiting gracefully..."
    exit 1
}


function main {

    HeaderMsg "Conducting pre-checks to validate the environment is NodeZero ready:"

    NetworkChk
    ProxyChk
    ContainerRuntimeChk
    CONTAINERCHKRC=$?
    if [ $CONTAINERCHKRC -ne 0 ]; then
        RetryContainerRuntimeChk
    fi
    ContainerProxyCheck
    RemoveNodeZeroArtifacts

    OsChk
    DiskChk
    MemChk
    CpuChk
    EdrChk

    HeaderMsg ${BOLD}${MAGENTA}"Pre-check validation completed successfully."${NORMAL}

    

    if [[ "$CONTAINER_RUNTIME" == "docker" ]]; then
        rm -R $DOCKER_CONFIG_PATH &> /dev/null
        rc=$?
        if [[ $rc != 0 ]]; then
            WarnMsg "Failed to remove temporary config directory $DOCKER_CONFIG_PATH, please remove manually."
        fi
    fi
}


# Beginning of script execution
main
