#!/bin/bash
# Script:      libastralive
# Title:       Astra Linux Live Build Library
# Description: Provides common functions for building Astra Linux live images. Includes
#              utilities for configuration file handling, messaging, repository management,
#              and more.

# Script version
VERSION="1.2.12.5"

MIRROR_URL=""
PACKAGE_LIST=("")
# It is desirable not to set the variable to true, as this functionality is experimental, not finalized, and is left as a future work in progress.
MULTIPLE_IMAGES="false"

################################################################################
# Common functions
################################################################################

# Display script version
version() {
    echo "$(basename $0) $VERSION"
    exit 0
}

# Define console colors
console_colors() {
    RED="\e[31m"
    GREEN="\e[32m"
    YELLOW="\e[33m"
    BLUE="\e[34m"
    MAGENTA="\e[35m"
    CYAN="\e[36m"
    LIGHTGRAY="\e[37m"
    DARKGRAY="\e[90m"
    LIGHTRED="\e[91m"
    LIGHTGREEN="\e[92m"
    LIGHTYELLOW="\e[93m"
    LIGHTBLUE="\e[94m"
    LIGHTMAGENTA="\e[95m"
    LIGHTCYAN="\e[96m"
    BOLD="\e[1m"
    DIM="\e[2m"
    UNDERLINED="\e[4m"
    BLINK="\e[5m"
    REVERSE="\e[7m"
    HIDDEN="\e[8m"
    ENDCOLOR="\e[0m"
}

# Display the current running process
current_process() {
    echo -e "${LIGHTYELLOW}=====> ${CYAN}${FUNCNAME[1]}${ENDCOLOR}${LIGHTYELLOW} is executing ...${ENDCOLOR}"
}

# Display the current executing function
current_function() {
    echo -e "=====> ${CYAN}${FUNCNAME[1]}${ENDCOLOR} function is executing ..."
}

# A function for reading from a configuration file in bash.
# Usage:
#   read_config CONFIG_FILE [VAR1] [VAR2] [...]
#
# Arguments:
#   CONFIG_FILE - required, this is the path to your configuration file.
#   VAR1, VAR2, etc - the names of variables you wish to read from the configuration file.
#   If variable names are not provided, the function will read all variables found in the file.
read_config() {
    # The configuration file is passed as the first argument.
    local CONFIG_FILE="${1}"
    shift

    # Check if the configuration file is set, exists and is readable.
    if [[ ! "${CONFIG_FILE}" ]]; then
        error "No configuration file given."
        return 1
    fi
    if [[ ! -f "${CONFIG_FILE}" ]]; then
        error "${CYAN}${CONFIG_FILE}${ENDCOLOR} is not a file!"
        return 1
    fi
    if [[ ! -r "${CONFIG_FILE}" ]]; then
        error "${CYAN}${CONFIG_FILE}${ENDCOLOR} is not readable!"
        return 1
    fi

    local ARGS=()

    if (($# > 0)); then
        # Use provided variable names.
        ARGS=("$@")
    else
        # Extract variable names from the config file.
        ARGS=($(grep -v '^#' "${CONFIG_FILE}" | awk -F '=' '{print $1}'))
    fi

    for ARG in "${ARGS[@]}"; do
        if grep -q "^${ARG}=" "${CONFIG_FILE}"; then
            # Value extraction strips out both single and double quotes at the beginning and end of a line.
            local VALUE=$(sed -n -e "s/^${ARG}=\(.*\)$/\1/p" "${CONFIG_FILE}" | sed -e "s/^['\"]//;s/['\"]$//")

            # Check whether variable to be declared as an array.
            if [[ "${VALUE}" =~ ^\((.*)\)$ ]]; then
                # Declare variable as a global array.
                eval "declare -ag ${ARG}=${VALUE}"
            else
                # Declare variable as a regular global variable.
                declare -g "${ARG}=${VALUE}"
            fi
        else
            error "Variable ${CYAN}${ARG}${ENDCOLOR} not found in file ${CYAN}${CONFIG_FILE}${ENDCOLOR}"
            return 1
        fi
    done
}

# A function for updating a configuration file in bash.
# Usage:
#   update_config [-a] CONFIG_FILE [VAR1] [VAR2] [...]
#
# Arguments:
#   -a: Update only declared variables, even if empty.
#   CONFIG_FILE - required, this is the path to your configuration file.
#   VAR1, VAR2, etc - the names of variables you wish to update in the configuration file.
#   If variable names are not provided, the function will update all variables found in the file.
update_config() {
    local ALL_DECLARED=false
    if [[ "$1" == "-a" ]]; then
        ALL_DECLARED=true
        shift
    fi

    local CONFIG_FILE="$1"
    shift

    if [[ ! "$CONFIG_FILE" ]]; then
        error "No configuration file given."
        exit 1
    fi
    if [[ ! -f "$CONFIG_FILE" ]]; then
        error "$CONFIG_FILE is not a file!"
        exit 1
    fi
    if [[ ! -r "$CONFIG_FILE" ]]; then
        error "$CONFIG_FILE is not readable!"
        exit 1
    fi

    local -a ARGS
    if (($# > 0)); then
        ARGS=("$@")
    else
        ARGS=($(grep -v '^#' "$CONFIG_FILE" | awk -F '=' '{print $1}'))
    fi

    for ARG in "${ARGS[@]}"; do
        local -n VAR="$ARG"
        local NEW_VALUE ELEMENT QUOTE_TYPE

        if ! $ALL_DECLARED && [[ -z "${VAR[@]}" ]]; then
            continue
        elif $ALL_DECLARED && [[ -z "${VAR+x}" ]]; then
            continue
        fi

        QUOTE_TYPE=$(grep "^$ARG=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed -e 's/^\s*//;s/\s*$//' | cut -c1)
        case "$QUOTE_TYPE" in
        "'") ;;
        '"') ;;
        *)
            QUOTE_TYPE='"'
            ;;
        esac

        case "$(declare -p "$ARG" 2>/dev/null)" in
        "declare -a"*)
            NEW_VALUE="$ARG=("
            for ELEMENT in "${VAR[@]}"; do
                NEW_VALUE+="${QUOTE_TYPE}$ELEMENT${QUOTE_TYPE}"
                [[ "$ELEMENT" != "${VAR[-1]}" ]] && NEW_VALUE+=" "
            done
            NEW_VALUE+=")"
            ;;
        *)
            NEW_VALUE="$ARG=${QUOTE_TYPE}${VAR}${QUOTE_TYPE}"
            ;;
        esac

        if grep -q "^$ARG=" "$CONFIG_FILE"; then
            sed -i "s|^$ARG=.*|$NEW_VALUE|" "$CONFIG_FILE"
        else
            echo -e "\n$NEW_VALUE" >>"$CONFIG_FILE"
        fi
    done
}

# =============================================================================
# read_config_value takes two arguments: a filename and a key.
# It reads the configuration file and searches for the line with the specified
# key.
# The function returns the last value of this key.
# =============================================================================
read_config_value() {
    if [ ! -f "$1" ]; then
        echo ""
        return
    fi
    cat $1 2>/dev/null |
        egrep -o "(^|[[:space:]])$2=[^[:space:]]*" |
        tr -d " " |
        cut -d "=" -f 2- |
        tail -n 1 |
        sed -e "s/^['\"]//;s/['\"]$//"
}

# Check if the script is run as root
allow_root_only() {
    if [ $(id -u) -ne 0 ]; then
        error "$(gettext "This script should be run as") ${CYAN}root${ENDCOLOR}!" >&2
        exit 1
    fi

    export HOME=/root
}

# Check the original value of the set options.
determine_option_status() {
    local OPTION=$1
    local SET_OPTION="SET_${OPTION^^}"

    if [[ $- == *${OPTION}* ]]; then
        eval $SET_OPTION="true"
    else
        eval $SET_OPTION="false"
    fi
}

# =============================================================================
# The function toggle_shell_options is designed for temporary switching of bash
# shell options. It allows you to disable active options for specific code
# segments and enable them back after executing these segments. If a shell
# option was inactive at the time of the function call, its state remains
# unchanged.
# =============================================================================

# Usage: toggle_shell_options "eu"
# In this example, the function will toggle the state of the shell options -e and -u.

# Example of usage in code:
# #!/bin/bash
# set -eu
# SET_E=""
# SET_U=""
# toggle_shell_options "e" # option "e" is disabled
# ### your code
# toggle_shell_options "e" # option "e" is enabled back

# Important: before using the function in the script, you need to declare variables SET_x for each option you plan to toggle.
# For example, for the option "e", you need to declare the variable SET_E.
toggle_shell_options() {
    local OPTIONS="${1}"
    for ((i = 0; i < ${#OPTIONS}; i++)); do
        local OPTION="${OPTIONS:$i:1}"
        local SET_OPTION="SET_${OPTION^^}"

        if [ -z "${!SET_OPTION}" ]; then
            determine_option_status "${OPTION}"
            if [ "${!SET_OPTION}" = "true" ]; then
                set "+${OPTION}"
                continue
            fi
        fi

        if [ "${!SET_OPTION}" = "true" ]; then
            set "-${OPTION}"
            eval "${SET_OPTION}=''"
        fi
    done
}

# Display an error message.
error() {
    local MESSAGE="${1}"
    echo -e "${BOLD}${RED}Error:  ${ENDCOLOR} ${MESSAGE}" >&2
}

# Display an info message.
information() {
    local MESSAGE="${1}"
    echo -e "${BOLD}${BLUE}Info:   ${ENDCOLOR} ${MESSAGE}"
}

# Display an warning message.
warning() {
    local MESSAGE="${1}"
    echo -e "${BOLD}${YELLOW}Warning:${ENDCOLOR} ${MESSAGE}"
}

################################################################################
# Functions for processing and analyzing command line arguments
################################################################################
# The `process_flag` function handles a flag and its arguments.
# Its usage is as follows:
#    process_flag CHECK TYPE VAR [CMDLINE]
#
# Parameters:
#    CHECK: It is a string that can be either "check" or "skip".
#           - If CHECK is set to "check", it triggers the validation of the option. This ensures that the option is
#             provided with a valid argument. If the argument is missing, nonexistent, or another option
#             (starts with `-` or `--`), it will terminate the program with an error.
#           - If CHECK is set to "skip", the function will skip the validation of the option and proceed directly
#             to parsing the argument. "skip" is mainly used for flags that do not always require an argument.
#
#    TYPE: Specifies the type of the variable to be updated. This can either be "string" or "array".
#          Depending on the TYPE, the function will correctly parse the arguments and assign them to VAR:
#              - If TYPE is "string", the function assigns the single argument to VAR.
#              - If TYPE is "array", the function parses multiple arguments and appends them to VAR.
#
#    VAR: Is the variable name that will be updated. VAR is passed by reference and is updated with parsed arguments.
#
#    CMDLINE: The rest of the command-line arguments. The function will stop parsing arguments once another flag (an argument starting with "-" or "--") is encountered.
#
# Returns:
#    The function returns the number of arguments processed (including the flag itself). This is typically used
#    with 'shift' command to remove the processed arguments from the positional parameters list.
#
# It's commonly called inside a loop that goes through each command-line argument, like so:
#
#    while (("${#}")); do
#      case "${1}" in
#        -yf | --your-flag)
#          process_flag "check" "string" "YOUR_VAR" "${@}"
#          shift "$?"
#          ;;
#      esac
#    done
#
# In this example, the loop checks each command-line argument. If it encounters "--your-flag", it invokes `process_flag`,
# which validates the argument following the flag, parses it, and assigns its value to YOUR_VAR.
#
# Example:
#    process_flag "check" "string" "AUTOLOGIN" "${@}"
#
# Note:
#    This function, including the functions it calls (i.e., check_option, parse_arguments), will not work as expected when 'set -e' is enabled in the script.
#    To handle errors within these functions and keep the script running after an error, consider using custom error handling and avoid using 'set -e'.

# Check if an option has a valid argument.
check_option() {
    local FLAG="${1}"
    local ARG="${2}"

    if [[ -z "${ARG}" || "${ARG}" == -* || "${ARG}" == --* ]]; then
        error "No arguments provided for the option ${FLAG}"
        exit 1
    fi
}

# Parse arguments, assign variables, and return the number of arguments processed.
parse_arguments() {
    local -n VAR="${1}"
    local TYPE="${2}"
    local FLAG="${3}"
    shift 3
    local SHIFT_COUNT=1

    while (("${#}")) && [[ "${1}" != -* ]] && [[ "${1}" != --* ]]; do
        ARG="${1}"
        if [ "${TYPE}" = "array" ]; then
            IFS='; , ' read -ra ADDR <<<"${ARG}"
            for i in "${ADDR[@]}"; do
                if [[ -n "${i}" ]]; then
                    VAR+=("${i}")
                fi
            done
        else
            VAR="${ARG}"
        fi
        shift
        SHIFT_COUNT=$((SHIFT_COUNT + 1))
    done

    return "${SHIFT_COUNT}"
}

# Process a flag and its arguments.
process_flag() {
    local CHECK="${1}"
    local TYPE="${2}"
    local VAR="${3}"
    local FLAG="${4}"
    local ARG="${5}"
    shift 3

    if [ "${CHECK}" = "check" ]; then
        check_option "${FLAG}" "${ARG}"
    fi
    parse_arguments "${VAR}" "${TYPE}" "${@}"
}

################################################################################
# live-build-astra functions
################################################################################

# Start an HTTP server for a given image/directory.
start_http_server() {
    local REPO="${1}"
    local IDX="${2}"
    local PORT="${3}"

    if [ ! -d "${REPO}" ]; then
        local RBIND=$(mktemp -d /tmp/astra-image-XXXXX)
        mount $(realpath "${REPO}") "${RBIND}" #2>/dev/null
        if [ $? -ne 0 ]; then
            error "Failed to mount image ${REPO} at ${RBIND}"
            exit 1
        fi
        RBINDS+=("${RBIND}")
        REPO="${RBIND}"
    fi

    python3 -m http.server --bind 127.0.0.2 --directory "${REPO}" "${PORT}" >/dev/null 2>&1 &
    PIDS+=($!)
    toggle_shell_options e
    local BINDED=$(netstat -npl | grep python3 | grep -e "127.0.0.2:${PORT}" | wc -l)
    while [ "${BINDED}" -eq 0 ]; do
        BINDED=$(netstat -npl | grep python3 | grep -e "127.0.0.2:${PORT}" | wc -l)
        sleep 0.2
    done
    toggle_shell_options e
    if ! curl -Is "http://127.0.0.2:${PORT}" | head -1 | grep "200 OK" >/dev/null; then
        error "Web server at http://127.0.0.2:${PORT} is not accessible"
        exit 1
    fi
}

# Function to stop the web server.
stop_http_server() {
    toggle_shell_options e
    netstat -npl 2>/dev/null | grep python3 | grep -e "127.0.0.2" | awk '{print $7}' | cut -d'/' -f1 | xargs -r kill 2>/dev/null
    unmount_dirs "/tmp/astra-image-"
    rm -rf "/tmp/astra-image-"* 2>/dev/null
    toggle_shell_options e
}

process_repository() {
    local REPO=$1
    local IDX=$2
    local PORT=$3

    # Check if REPO is a URL
    if [[ "${REPO}" =~ ^http://|^https://|^ftp:// ]]; then
        information "Accessing remote repository: ${CYAN}${REPO}${ENDCOLOR}..."
        if ! curl --output /dev/null --silent --head --fail "${REPO}/dists/${DISTRIBUTION}/Release"; then
            error "Remote repository: ${CYAN}${REPO}${ENDCOLOR} is not accessible."
            exit 1
        fi

        # Check if the repository has astra-ce component
        if curl -s "${REPO}/conf/distributions" | grep -q "^Components:.*astra-ce"; then
            # If no GPG key, mark the source as trusted
            if ! curl -sf "${REPO}/dists/${DISTRIBUTION}/Release.gpg" >/dev/null; then
                warning "No GPG key found. Marking the repository as trusted for use."
                echo "deb [trusted=yes] ${REPO} ${DISTRIBUTION} main contrib non-free astra-ce" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
            else
                information "Adding ${CYAN}${REPO}${ENDCOLOR} repository to the sources list."
                echo "deb ${REPO} ${DISTRIBUTION} main contrib non-free astra-ce" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
            fi
        else
            # Same as above but for repositories without astra-ce component
            if ! curl -sf "${REPO}/dists/${DISTRIBUTION}/Release.gpg" >/dev/null; then
                warning "No GPG key found. Marking the repository as trusted for use."
                echo "deb [trusted=yes] ${REPO} ${DISTRIBUTION} main contrib non-free" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
            else
                information "Adding ${CYAN}${REPO}${ENDCOLOR} repository to the sources list."
                echo "deb ${REPO} ${DISTRIBUTION} main contrib non-free" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
            fi
        fi
    else
        # If REPO is not URL convert it to absolute path
        REPO=$(readlink -f "${REPO}")
        information "Accessing local repository: ${CYAN}${REPO}${ENDCOLOR}..."

        # Check if repo file or directory exists
        if [ ! -e "${REPO}" ]; then
            error "File or directory $(basename "${REPO}") does not exist."
            exit 1
        fi

        # Start a HTTP server for local repository
        information "Starting a HTTP server for the local repository at ${CYAN}${REPO}${ENDCOLOR}.
This will make it accessible from the chroot environment where the system is being installed."
        start_http_server "${REPO}" "${IDX}" "${PORT}"

        # Show the URL of the repository
        information "The repository is now available at the following URL: ${CYAN}http://127.0.0.2:${PORT}${ENDCOLOR}"

        # The rest of the steps are similar to the steps we do for URL based repositories
        if curl -s "http://127.0.0.2:${PORT}/conf/distributions" | grep -q "^Components:.*astra-ce"; then
            information "Found astra-ce in the local repository. Adding it to the sources list..."
            echo "deb http://127.0.0.2:${PORT} ${DISTRIBUTION} main contrib non-free astra-ce" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
        else
            information "Adding the local repository to the sources list..."
            echo "deb http://127.0.0.2:${PORT} ${DISTRIBUTION} main contrib non-free" >>"${BUILD_DIR}/sources/${DISTRIBUTION}.list"
        fi
    fi
}

handle_repositories() {
    current_function

    # Setting up list file paths
    LOCAL_SRC_LIST="${BUILD_DIR}/sources/${DISTRIBUTION}.list"
    SCRIPT_SRC_LIST="${SCRIPT_DIR}/sources/${DISTRIBUTION}.list"
    USR_SRC_LIST="/usr/share/astra-live/sources/${DISTRIBUTION}.list"
    SYSTEM_SRC_LIST="/etc/apt/sources.list"
    if [ -z ${ERRORS} ]; then
        ERRORS=0
    fi

    # Create a sources directory if it doesn't exist
    mkdir -p "${BUILD_DIR}/sources"

    # If system sources are set to be used
    if [ ${USE_SYSTEM_SOURCES} ]; then
        information "System sources are specified. Skipping manual repository handling..."
        cp "${SYSTEM_SRC_LIST}" "${LOCAL_SRC_LIST}"
    # If there is a REPO_LIST specified
    elif [ -n "${REPO_LIST}" ]; then
        local IDX=0
        local PORT=18010

        information "Preparing to process the repositories..."
        echo -n '' >"${LOCAL_SRC_LIST}"
        information "Stopping any previously running HTTP server..."
        stop_http_server

        warning "The ${CYAN}main${ENDCOLOR} repository or its counterpart containing the base packages must be listed first."

        for REPO in ${REPO_LIST[@]}; do
            # Check if the given REPO path is a real file or directory
            if [[ ! ${REPO} =~ ^http://|^https://|^ftp:// && ! -e "${REPO}" ]]; then
                error "Directory or file ${CYAN}${REPO}${ENDCOLOR} does not exist."
                exit 1
            fi

            # Processing each repository
            process_repository "${REPO}" "${IDX}" "${PORT}"
            IDX=$((IDX + 1))
            PORT=$((PORT + 1))
        done
    fi

    # If the sources list is missing, copy from preset sources or use the system source
    if [ ! -f "${LOCAL_SRC_LIST}" ]; then
        if [ -f "${SCRIPT_SRC_LIST}" ]; then
            cp -f "${SCRIPT_SRC_LIST}" "${LOCAL_SRC_LIST}"
        elif [ -f "${USR_SRC_LIST}" ]; then
            cp -f "${USR_SRC_LIST}" "${LOCAL_SRC_LIST}"
        else
            error "The local repository list ${LOCAL_SRC_LIST} is missing."
            exit 1
        fi
    fi

    validate_sources "${LOCAL_SRC_LIST}"

    if [ $ERRORS -gt 0 ]; then
        # Add echo statement to report number of errors found
        echo -e "${RED}The sources file ${BOLD}${LOCAL_SRC_LIST}${ENDCOLOR}${RED} has error(s). Please make the necessary changes and try again.${ENDCOLOR}"
        exit 1
    fi

    if [[ ("${DISTRIBUTION}" == "1.7_x86-64" || "${DISTRIBUTION}" == "4.7_arm") && " ${GENERATE_IMAGES[@]} " =~ " iso " ]]; then
        local BASE_REPO_FOUND=false
        local REPO_URL VERSION_STRING RELEASE_URL LINE

        while IFS= read -r LINE; do
            # Skip comments and empty lines, only process actual deb lines
            [[ "$LINE" =~ ^# ]] || [[ -z "$LINE" ]] || [[ ! "$LINE" =~ ^deb ]] && continue

            REPO_URL=$(echo "$LINE" | awk '{print $2}')
            # Use the global DISTRIBUTION variable for the Release file path check
            RELEASE_URL="${REPO_URL}/dists/${DISTRIBUTION}/Release"

            # Attempt to fetch version string silently, failing on HTTP errors, discard stderr
            VERSION_STRING=$(curl -sSf "${RELEASE_URL}" 2>/dev/null | grep '^Version:' | awk '{print $2}')

            # Check if curl succeeded (exit code 0) and version string ends with -base
            if [ $? -eq 0 ] && [[ "${VERSION_STRING}" == *-base ]]; then
                BASE_REPO_FOUND=true
                break # Found one, no need to check further
            fi
        done <"${LOCAL_SRC_LIST}"

        if [ "$BASE_REPO_FOUND" = false ]; then
            error "$(gettext "To build an ISO image based on ${CYAN}${DISTRIBUTION}${ENDCOLOR}, the ${CYAN}Base${ENDCOLOR} repository is additionally required.")"
            exit 1
        fi
    fi

    # Extract the mirror URL from the sources list
    while read -r LINE; do
        MIRROR_URL=$(echo "${LINE}" | cut -d ' ' -f 2)
        break
    done < <(grep "^deb" "${LOCAL_SRC_LIST}") # Run the loop for each line starting with 'deb' in the file

    # Handle if mirror URL extraction failed
    if [ -z "${MIRROR_URL}" ]; then
        error "Unable to extract the mirror URL from the distribution list. Please check the ${LOCAL_SRC_LIST} structure and content."
        exit 1
    fi

    # Grant necessary permissions
    grant_write_permissions "${LOCAL_SRC_LIST}"

    # Save the determined MIRROR_URL back to the config for subsequent runs
    update_config "${BUILD_DIR}/config" MIRROR_URL
}

# Function to validate the sources list
validate_sources() {

    # Check if the file exists at the provided path
    if [ -f "${1}" ]; then
        # Check and handle if the sources list is empty
        if [ ! -s "${1}" ]; then
            error "Detected an empty ${CYAN}${1}${ENDCOLOR} file. Please populate file with a valid repository list and try again."
            # Increment the error count by 1
            ERRORS=$((ERRORS + 1))
        # Check if there is at least one line in the file starting with 'deb'
        elif ! grep -q "^deb" "${1}"; then
            # If there isn't a line starting with 'deb', report an error
            error "The file ${CYAN}${1}${ENDCOLOR} does not contain any line starting with deb. At least one line should start with 'deb'."
            ERRORS=$((ERRORS + 1))
        else
            # Read the file and for each line perform the following actions
            while read -r LINE; do
                # Extract the URL from the line
                local URL=$(echo "${LINE}" | cut -d ' ' -f 2)
                # Extract the distro name from the line
                local DISTRO=$(echo "${LINE}" | cut -d ' ' -f 3)

                # Check if the "Release" file exists in the repository
                if ! curl --output /dev/null --silent --head --fail "${URL}/dists/${DISTRO}/Release"; then
                    # If the file doesn't exist, report an error and increment the error count by 1
                    error "Release file does not exist in repository: ${CYAN}${URL}/dists/${DISTRO}${ENDCOLOR}"
                    ERRORS=$((ERRORS + 1))
                fi
            done < <(grep "^deb" "${1}") # Run the loop for each line starting with 'deb' in the file
        fi
    fi
}

# Convert tasksel tasks to a list of packages
handle_tasks_chroot() {
    local TASK AVAILABLE_TASKS
    if [ -n "${TASKS}" ]; then
        AVAILABLE_TASKS=($(tasksel --list | awk '{print $2}'))
        #>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
        # To avoid the exim4 error, exim4 packages must be installed from the astra-ce component of the extended repository. Since the versions in this repository are older, we need to fix them.
        if [ "${DISTRIBUTION}" = "1.7_x86-64" ] || [ "${DISTRIBUTION}" = "4.7_arm" ]; then
            echo "exim4-base=4.92-8+deb10u6.astra.se20
exim4-config=4.92-8+deb10u6.astra.se20
exim4-daemon-light=4.92-8+deb10u6.astra.se20" >>"/packages.list"
        fi
        tasksel --task-packages hidden >>"/packages.list"
        for TASK in "${TASKS[@]}"; do
            if [[ "${AVAILABLE_TASKS[@]}" =~ "${TASK}" ]]; then
                tasksel --task-packages "$TASK" >>"/packages.list"
            else
                error "Task $TASK not found in the list of available tasks."
                echo "Available tasks are: ${AVAILABLE_TASKS[@]}."
                exit 1
            fi
        done
    fi
}

# Function for removing empty elements from arrays. !!!Not used!!!
remove_empty_elements() {
    local -n ARR=$1
    local NEW_ARR=()
    for ITEM in "${ARR[@]}"; do
        if [[ -n "${ITEM}" ]]; then
            NEW_ARR+=("${ITEM}")
        fi
    done
    ARR=("${NEW_ARR[@]}")
}

check_requirements() {
    local REQUIRED_COMMAND="qemu-img"

    if [[ " ${GENERATE_IMAGES[@]} " =~ " qcow2 " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " vmdk " ]]; then
        if ! command -v "${REQUIRED_COMMAND}" &>/dev/null; then
            error "${REQUIRED_COMMAND} could not be found."
            echo "Please install it using the following command:"
            echo "sudo apt-get install qemu-utils"
            exit 1
        fi
    fi
}

################################################################################
# astra-live functions
################################################################################

# Declaring and assigning variables
set_variables() {
    for VAR in "$@"; do
        KEY="${VAR%%=*}"
        VALUE="${VAR#*=}"
        if [ -z "${!KEY}" ]; then
            if [ "$KEY" == "$VALUE" ]; then
                eval $KEY=""
            else
                eval $KEY="$VALUE"
            fi
        fi
    done
}

# Function to create a log file to record script output
create_log_file() {
    if [ -z "${LOG_FILE}" ]; then
        export LOG_FILE="${BUILD_DIR}/build-$(date +%Y%m%d-%H%M%S).log"

        if [ -z "${BUILD_DIR}" ]; then
            BUILD_DIR="${PWD}"
        fi
        mkdir -p "${BUILD_DIR}"

        ARGS=""
        # Loop over all arguments and add them to ARGS in quotes
        for VAR in "$@"; do
            ARGS="${ARGS}\"${VAR}\" "
        done
        script -q -e -c "${0} ${ARGS}" "${LOG_FILE}"

        # Check if LOG_FILE exists and clean it from escape sequences
        if [ -f "${LOG_FILE}" ]; then
            sed -i 's/\x1B\[[0-9;]*[JKmsu]//g' "${LOG_FILE}"
        fi

        exit $?
    fi
}

# Checks the index of a given command string in the context of global CMD
# array. If the command doesn't exist in CMD, it displays the help.
find_command_index() {
    local i
    for ((i = 0; i < "${#CMD[*]}"; i++)); do
        if [ "${CMD[i]}" == "${1}" ]; then
            INDEX="${i}"
            return
        fi
    done
    help "$(gettext 'Command not found:') ${1}"
}

# Processes script arguments to decide a range, defined by a start
# index and end index, of commands to execute from the CMD array.
process_command_range() {
    if (($# < 1 || $# > 3)); then
        help
    fi

    DASH_FLAG="false"
    START_INDEX="0"
    END_INDEX="${#CMD[@]}"

    for ARG in "$@"; do
        if [[ "${ARG}" == "-" ]]; then
            DASH_FLAG="true"
            continue
        fi
        find_command_index "${ARG}"
        if [[ "${DASH_FLAG}" == "false" ]]; then
            START_INDEX="${INDEX}"
        else
            END_INDEX=$((${INDEX} + 1))
        fi
    done

    if [[ "${DASH_FLAG}" == "false" ]]; then
        END_INDEX=$((${START_INDEX} + 1))
    fi
}

################################################################################
# Functions for checking the configuration file
################################################################################

check_config_variables() {
    local VAR VARS ERRORS
    declare -a VARS=("HOST_NAME" "USER_NAME" "USER_PASSWORD_HASH" "AUTOLOGIN" "BUILD_DIR" "DISTRIBUTION" "ARCHITECTURE"
        "COMP_TYPE" "REMOVE_SOURCES" "KEEP_CACHE" "KEEP_IMAGE" "KEEP_LOG" "REPO_LIST"
        "ADD_PACKAGES" "ADD_PACKAGES_LIST" "DELETE_PACKAGES" "DELETE_PACKAGES_LIST"
        "REPLACE_PACKAGES" "REPLACE_PACKAGES_LIST" "TASKS" "GENERATE_IMAGES" "ISO_NAME"
        "DOCKER_NAME" "DOCKER_TAG" "QCOW2_NAME" "RAW_NAME" "TAR_NAME" "VMDK_NAME" "WSL_NAME")
    ERRORS=0
    for VAR in "${VARS[@]}"; do
        if ! grep -q "$VAR" "$CONFIG_FILE"; then
            error "The ${CYAN}$VAR${ENDCOLOR} variable is missing from the ${CYAN}${CONFIG_FILE}${ENDCOLOR} configuration file."
            ERRORS=$((ERRORS + 1))
        fi
    done

    if [ $ERRORS -gt 0 ]; then
        # Add echo statement to report number of errors found
        echo -e "${RED}The configuration file ${RED}${BOLD}${CONFIG_FILE}${ENDCOLOR}${RED} is missing ${BOLD}${ERRORS}${ENDCOLOR}${RED} required variables. Please make the necessary changes and try again.${ENDCOLOR}"
        return 1
    else
        return 0
    fi
}

# Selects the appropriate executable label based on the MAIN_EXECUTABLE variable
get_executable_label() {
    KEY_LABEL=$([[ "${MAIN_EXECUTABLE}" = "astra-live" ]] && echo "$KEY" || echo "${OPTION_NAMES[$KEY]}")
}

# Verifies if the given value is valid according to a preset list of valid values
check_preset_values() {
    for ITEM in "${VALUE[@]}"; do
        if [[ ! " ${VALID_VALUES[@]} " =~ " ${ITEM} " ]]; then
            get_executable_label
            error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${ITEM}${ENDCOLOR}. The value should be one of: ${CYAN}${VALID_VALUES[*]}${ENDCOLOR}."
            ERRORS=$((ERRORS + 1))
        fi
    done
}

# Checks whether a non-empty value has been given
check_not_empty() {
    if [ -z "${VALUE}" ]; then
        get_executable_label
        error "No argument provided for ${CYAN}${KEY_LABEL}${ENDCOLOR}. An argument is required."
        ERRORS=$((ERRORS + 1))
    fi
}

# Checks whether the provided value does not exceed a maximum length
check_length() {
    if [ "${#VALUE}" -gt "${MAX_LENGTH}" ]; then
        get_executable_label
        error "The argument provided for ${CYAN}${KEY_LABEL}${ENDCOLOR} is too long: ${RED}${#VALUE}${ENDCOLOR}. Maximum length: ${CYAN}${MAX_LENGTH}${ENDCOLOR}."
        ERRORS=$((ERRORS + 1))
    fi
}

# Checks if the input value matches a pre-defined format specified by the regex
check_format() {
    case "${KEY}" in
    "HOST_NAME")
        REGEX='^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$'
        REGEX_MESSAGE="The value can only contain Latin letters (case independent), numbers, and hyphens. It also cannot start or end with a hyphen."
        ;;
    "USER_NAME")
        REGEX='^[a-z][-a-z0-9]*$'
        REGEX_MESSAGE="The value should start with a lowercase Latin letter, followed by any number of lowercase Latin letters, numbers, or hyphens."
        ;;
    "ISO_NAME" | "DOCKER_NAME" | "QCOW2_NAME" | "RAW_NAME" | "TAR_NAME" | "VMDK_NAME" | "WSL_NAME")
        REGEX='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]*[a-zA-Z0-9]$'
        REGEX_MESSAGE="The value should start with a Latin letter (case independent), number, period, or underscore. In the middle, it can contain any number of Latin letters (case independent), numbers, periods, underscores, or hyphens. The value must end with a Latin letter (case independent) or a number."
        ;;
    "ADD_PACKAGES_LIST" | "DELETE_PACKAGES_LIST" | "REPLACE_PACKAGES_LIST")
        REGEX='^[^~].*$'
        REGEX_MESSAGE="The value should not start with a tilde (~)."
        ;;
    "USER_PASSWORD_HASH")
        if [[ "${VALUE}" =~ ^[a-fA-F0-9]{64}$ ]]; then
            REGEX='^[a-fA-F0-9]{64}$'
            REGEX_MESSAGE="The password hash must be a valid SHA-256 format (64 hexadecimal characters)."
        elif [[ "${VALUE}" =~ ^[a-fA-F0-9]{32}$ ]]; then
            REGEX='^[a-fA-F0-9]{32}$'
            REGEX_MESSAGE="The password hash must be a valid MD5 format (32 hexadecimal characters)."
        elif [[ "${VALUE}" =~ ^\$2[aby]\$[0-9]{2}\$[./A-Za-z0-9]{53}$ ]]; then
            REGEX='^\$2[aby]\$[0-9]{2}\$[./A-Za-z0-9]{53}$'
            REGEX_MESSAGE="The password hash must be a valid bcrypt format."
        else
            REGEX='^$'
            REGEX_MESSAGE="The password hash is invalid."
        fi
        ;;
    esac

    if [[ -n "${VALUE}" && ! "${VALUE}" =~ ${REGEX} ]]; then
        get_executable_label
        error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR}. ${REGEX_MESSAGE}"
        ERRORS=$((ERRORS + 1))
    fi

    if [[ -n "${MAX_LENGTH}" ]]; then
        for LENGTH_MESSAGE in "${REGEX_MESSAGE[@]}"; do
            REGEX_MESSAGE="The value should be less than ${MAX_LENGTH} characters long. ${REGEX_MESSAGE}"
        done
    fi
}

# Checks the given file
check_file() {
    if [ -n "${VALUE}" ]; then
        if [[ ! -f "${VALUE}" ]]; then
            get_executable_label
            error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR} does not exist."
            ERRORS=$((ERRORS + 1))
        elif [[ ! -r "${VALUE}" ]]; then
            get_executable_label
            error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR} cannot be read."
            ERRORS=$((ERRORS + 1))
        elif [[ ! -s "${VALUE}" ]]; then
            get_executable_label
            error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR} is empty."
            ERRORS=$((ERRORS + 1))
        fi
    fi
}

# Function to check repositories
check_repositories() {
    # Setting up the parameters for the dialog box dimensions
    local WT_HEIGHT=15
    local WT_WIDTH=50
    local WT_MENU_HEIGHT=6
    local LINE
    local BUILTIN_REPO_LIST

    # Depending on the MAIN_EXECUTABLE variable,
    # the function selects and assigns the suitable label to the KEY_LABEL variable
    get_executable_label

    # If REPO_LIST is set, validate each path/URL
    if [[ -n "${REPO_LIST}" ]]; then
        for URL in ${REPO_LIST[@]}; do
            # check if repo is a local path (iso or folder)
            if [[ -d "${URL}" || -f "${URL}" ]]; then
                information "Local repository ${URL} exists."
            # else check if repo is a URL (http, https, ftp)
            elif echo ${URL} | grep -P '^https?://|^ftp://' &>/dev/null; then
                if curl --output /dev/null --silent --head --fail "${URL}/dists/${DISTRIBUTION}/Release"; then
                    information "Remote repository ${URL} is reachable."
                else
                    error "Remote repository ${URL} is not reachable."
                    ERRORS=$((ERRORS + 1))
                fi
            else
                error "${URL} is neither a valid local nor a remote repository."
                ERRORS=$((ERRORS + 1))
            fi
        done
    fi

    # Checking if there's a distribution-specific source list in the script directory or astra directory
    # The directory is placed in BUILTIN_REPO_LIST variable
    if [ -f "${SCRIPT_DIR}/sources/${DISTRIBUTION}.list" ]; then
        BUILTIN_REPO_LIST="${SCRIPT_DIR}/sources/${DISTRIBUTION}.list"
    elif [ -f "/usr/share/astra-live/sources/${DISTRIBUTION}.list" ]; then
        BUILTIN_REPO_LIST="/usr/share/astra-live/sources/${DISTRIBUTION}.list"
    else
        BUILTIN_REPO_LIST=""
    fi

    # If no distro specific source list is found, and no repo list is populated yet, try to fetch it from astra_version file
    # This path is only followed if astra_version file exists and can be read
    if [ ! -f "${BUILD_DIR}/sources/${DISTRIBUTION}.list" ] && [ -z "${REPO_LIST}" ] && [ -z "${BUILTIN_REPO_LIST}" ]; then
        if [ -f /etc/astra_version ] && read -r LINE </etc/astra_version; then
            # If the first line of astra_version matches distro and a default source list exists, use that instead
            # User confirmation is prompted before using system sources
            if [ "$(awk -F'_' '{print $1}' <<<${DISTRIBUTION})" == "$(awk -F. '{print $1"."$2}' <<<${LINE})" ] && [ -f /etc/apt/sources.list ]; then
                local MSG="\nNo argument provided for ${KEY_LABEL}.\nAlso, there is no built-in repository list for ${DISTRIBUTION}, do you want to use the repositories specified in /etc/apt/sources.list instead?"
                if (whiptail --title "Confirmation Required" --yesno "${MSG}" ${WT_HEIGHT} ${WT_WIDTH}); then
                    USE_SYSTEM_SOURCES=true
                fi
                if [ ! ${USE_SYSTEM_SOURCES} ]; then
                    if [ ! -f "${BUILD_DIR}/sources/${DISTRIBUTION}.list" ] && [ ! -n "${BUILTIN_REPO_LIST}" ]; then
                        error "No argument provided for ${CYAN}${KEY_LABEL}${ENDCOLOR}. Also, there is no built-in repository list for ${CYAN}${DISTRIBUTION}${ENDCOLOR}."
                        ERRORS=$((ERRORS + 1))
                    fi
                fi
            fi
        fi
    fi
}

# Set DISTRIBUTION based on its current value
update_distribution() {
    case "${DISTRIBUTION}" in
    "1.7")
        DISTRIBUTION="1.7_x86-64"
        ;;
    "1.8")
        DISTRIBUTION="1.8_x86-64"
        ;;
    "4.7")
        DISTRIBUTION="4.7_arm"
        ;;
    "4.8")
        DISTRIBUTION="4.8_arm"
        ;;
    esac
}

# Checks and sets the DISTRIBUTION, and the ARCHITECTURE based on it
check_and_set_distribution() {
    # Initialize dialog box parameters
    local WT_HEIGHT=15
    local WT_WIDTH=50
    local WT_MENU_HEIGHT=6
    local LINE

    # Determine the label for the key
    get_executable_label

    # If DISTRIBUTION is not set and there exists a /etc/astra_version, read the first line
    if [ -z "${DISTRIBUTION}" ] && [ -f /etc/astra_version ] && read -r LINE </etc/astra_version; then
        # Construct user confirmation message
        local MSG="\nNo argument provided for ${KEY_LABEL}.\nThe value specified in /etc/astra_version will be used.\n\nContinue?"

        # Display the message and obtain user confirmation
        if (whiptail --title "Confirmation Required" --yesno "${MSG}" ${WT_HEIGHT} ${WT_WIDTH}); then
            # Parse and set DISTRIBUTION value if confirmed
            DISTRIBUTION=$(awk -F. '{print $1"."$2}' <<<"$LINE")
        fi
    fi

    # If DISTRIBUTION is still unset, check for non-emptiness.
    if [ -z "${DISTRIBUTION}" ]; then
        check_not_empty
        return
    fi

    # Update DISTRIBUTION based on its current value
    update_distribution

    VALUE="${DISTRIBUTION}"

    # Check if DISTRIBUTION is within the domain of supported values
    VALID_VALUES=("1.7_x86-64" "1.8_x86-64" "4.7_arm" "4.8_arm")
    check_preset_values

    # Determine and set the value of ARCHITECTURE based on DISTRIBUTION
    case "${DISTRIBUTION}" in
    1.7_x86-64 | 1.8_x86-64)
        ARCHITECTURE="amd64"
        ;;
    4.7_arm | 4.8_arm)
        ARCHITECTURE="arm64"
        ;;
    esac
}

check_package_variables() {
    local ERR_MSG
    local PKG_VARS=(REPLACE_PACKAGES REPLACE_PACKAGES_LIST)
    local CONFIG_VARS

    for VAR in "${PKG_VARS[@]}"; do
        if [ "${MAIN_EXECUTABLE}" = "live-build-astra" ]; then
            ERR_MSG="The ${RED}${OPTION_NAMES[$VAR]}${ENDCOLOR} and ${RED}${OPTION_NAMES[TASKS]}${ENDCOLOR} cannot both be specified."
        elif [ "${MAIN_EXECUTABLE}" = "astra-live" ]; then
            ERR_MSG="The ${CYAN}${VAR}${ENDCOLOR} and ${CYAN}TASKS${ENDCOLOR} cannot both be specified."
        fi
        if [[ -n "${!VAR}" ]] && [[ -n "${TASKS}" ]]; then
            error "${ERR_MSG}"
            ERRORS=$((ERRORS + 1))
        fi
    done
}

# Check for available `tasksel` tasks
check_tasks() {
    local TASK
    local COMMON_TASKS=("Fly" "Internet" "Office" "Graphics" "Multimedia" "Games" "Base" "Ufw" "Fly-qml" "Fly-ssh")
    local UNAVAILABLE_TASKS=()
    local AVAILABLE_TASKS=()

    case "${DISTRIBUTION}" in
    "1.7_x86-64" | "4.7_arm")
        AVAILABLE_TASKS=("${COMMON_TASKS[@]}" "Fly-virtualization")
        ;;
    "1.8_x86-64" | "4.8_arm")
        AVAILABLE_TASKS=("${COMMON_TASKS[@]}" "Virtualization")
        ;;
    esac

    if ((${#TASKS[@]})); then
        for TASK in "${TASKS[@]}"; do
            if [[ ! " ${AVAILABLE_TASKS[*]} " =~ " ${TASK} " ]]; then
                UNAVAILABLE_TASKS+=($TASK)
            fi
        done
    fi

    if ((${#UNAVAILABLE_TASKS[@]})); then
        error "The following tasks were not found in the list of available tasks: ${CYAN}${UNAVAILABLE_TASKS[*]}${ENDCOLOR}."
        ERRORS=$((ERRORS + 1))
        information "Available tasks for ${CYAN}${DISTRIBUTION}${ENDCOLOR} are: ${CYAN}${AVAILABLE_TASKS[*]}${ENDCOLOR}."
    fi
}

# Checks whether the images to be generated are compatible or not
check_image_compatibility() {
    # Define compatible image type groups
    COMPATIBLE_GROUP1=("docker" "wsl")
    COMPATIBLE_GROUP2=("iso" "qcow2" "raw" "vmdk")

    # Copy image arrays
    CHECK_GROUP1=(${GENERATE_IMAGES[@]})
    CHECK_GROUP2=(${GENERATE_IMAGES[@]})

    # Remove incompatible image types from each group
    for i in "${!CHECK_GROUP1[@]}"; do
        if [[ ! " ${COMPATIBLE_GROUP1[*]} " == *" ${CHECK_GROUP1[$i]} "* ]]; then
            unset 'CHECK_GROUP1[i]'
        fi
    done

    for i in "${!CHECK_GROUP2[@]}"; do
        if [[ ! " ${COMPATIBLE_GROUP2[*]} " == *" ${CHECK_GROUP2[$i]} "* ]]; then
            unset 'CHECK_GROUP2[i]'
        fi
    done

    # If both CHECK_GROUP arrays are still not empty, it's an incompatibility problem
    if [[ ${#CHECK_GROUP1[@]} -gt 0 ]] && [[ ${#CHECK_GROUP2[@]} -gt 0 ]]; then
        error "Generation of ${CYAN}${CHECK_GROUP1[*]}${ENDCOLOR} images is incompatible with generation of ${CYAN}${CHECK_GROUP2[*]}${ENDCOLOR} images."
        ERRORS=$((ERRORS + 1))
        echo "Permissible combinations of image types for generation:"
        echo "${COMPATIBLE_GROUP1[*]} tar"
        echo "${COMPATIBLE_GROUP2[*]} tar"
    fi
}

# Checks if there is more than one image type to generate while disallowing duplicates
prohibit_multiple_images() {
    if [[ ${#GENERATE_IMAGES[@]} -gt 1 ]]; then
        UNIQUE_IMAGES=($(printf "%s\n" "${GENERATE_IMAGES[@]}" | sort -u))

        if [[ ${#GENERATE_IMAGES[@]} -ne ${#UNIQUE_IMAGES[@]} ]]; then
            error "Duplicate values in GENERATE_IMAGES array are not allowed."
            ERRORS=$((ERRORS + 1))
        fi
    fi

    # Prohibits multiple image types with the exception of tar
    if [[ " ${GENERATE_IMAGES[@]} " =~ " tar " ]]; then
        if [[ ${#GENERATE_IMAGES[@]} -gt 2 ]]; then
            error "Generating multiple image types is not allowed. You can combine any image type only with tar."
            ERRORS=$((ERRORS + 1))
        fi
    else
        if [[ ${#GENERATE_IMAGES[@]} -gt 1 ]]; then
            error "Generating multiple image types is not allowed. You can combine any image type only with tar."
            ERRORS=$((ERRORS + 1))
        fi
    fi
}

# Validates the config file
check_config_file() {
    current_function

    local CONFIG_FILE OPTION_NAMES VARS_TO_CHECK KEY KEYS_TO_CHECK VALUE KEY_LABEL REGEX REGEX_MESSAGE VALID_VALUES MAX_VALUES
    CONFIG_FILE="${1}"
    shift
    VARS_TO_CHECK=("$@")

    if ! bash -n "${CONFIG_FILE}"; then
        echo -e "${RED}The configuration file ${BOLD}${CONFIG_FILE}${ENDCOLOR}${RED} contains syntax errors. Please make the necessary changes and try again.${ENDCOLOR}"
        exit 1
    fi

    if ! check_config_variables; then
        exit 1
    fi

    declare -A OPTION_NAMES=(
        # System Configuration
        [HOST_NAME]="--host-name"
        [USER_NAME]="--user-name"
        [AUTOLOGIN]="--no-autologin"
        # Build Configuration
        [BUILD_DIR]="--build-dir"
        [DISTRIBUTION]="--distribution"
        [ARCHITECTURE]=""
        [COMP_TYPE]="--compression"
        [REMOVE_SOURCES]="--remove-sources"
        [KEEP_CACHE]="--keep-cache"
        [KEEP_IMAGE]="--keep-image"
        [KEEP_LOG]="--keep-log"
        # Repository Configuration
        [REPO_LIST]="--repository"
        # Packages Configuration
        [ADD_PACKAGES]="--add-packages"
        [ADD_PACKAGES_LIST]="--add-packages-list"
        [DELETE_PACKAGES]="--delete-packages"
        [DELETE_PACKAGES_LIST]="--delete-packages-list"
        [REPLACE_PACKAGES]="--replace-packages"
        [REPLACE_PACKAGES_LIST]="--replace-packages-list"
        [TASKS]="--tasks"
        # Images Configuration
        [GENERATE_IMAGES]="GENERATE_IMAGES"
        [ISO_NAME]="--iso"
        [DOCKER_NAME]="--docker"
        [DOCKER_TAG]="--docker-tag"
        [QCOW2_NAME]="--qcow2"
        [RAW_NAME]="--raw"
        [TAR_NAME]="--tar"
        [VMDK_NAME]="--vmdk"
        [WSL_NAME]="--wsl"
    )

    ERRORS=0

    # Check if the file exists
    if [[ ! -f "${CONFIG_FILE}" ]]; then
        error "The file ${CONFIG_FILE} cannot be found."
        ERRORS=$((ERRORS + 1))
    fi

    # Source the config file
    source "${CONFIG_FILE}"

    toggle_shell_options u
    # If there are no variables to check, we check all OPTION_NAMES keys.
    # Otherwise, we only check the keys that were passed in the function argument.
    if [ ${#VARS_TO_CHECK[@]} -eq 0 ]; then
        KEYS_TO_CHECK=("${!OPTION_NAMES[@]}")
    else
        KEYS_TO_CHECK=("${VARS_TO_CHECK[@]}")
    fi

    for KEY in "${KEYS_TO_CHECK[@]}"; do
        VALUE=""
        KEY_LABEL=""
        VALID_VALUES=""
        MAX_VALUES=""
        REGEX=""
        REGEX_MESSAGE=""

        # Checking if the variable, named by KEY, has a value
        if [ -n "$(eval echo \$${KEY})" ]; then
            # If the value exists, assign it to the VALUE variable
            eval VALUE=\$$KEY
        fi

        case "${KEY}" in
        # System Configuration
        HOST_NAME)
            check_not_empty
            MAX_LENGTH=63
            check_length
            check_format
            ;;
        USER_NAME)
            check_not_empty
            MAX_LENGTH=32
            check_length
            check_format
            ;;
        AUTOLOGIN)
            check_not_empty
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        # Build Configuration
        DISTRIBUTION)
            check_and_set_distribution
            update_config "${CONFIG_FILE}" DISTRIBUTION ARCHITECTURE
            ;;
        COMP_TYPE)
            check_not_empty
            VALID_VALUES=("zstd" "gzip" "lzma" "xz" "lzo" "lz4")
            check_preset_values
            ;;
        REMOVE_SOURCES)
            check_not_empty
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        KEEP_CACHE)
            check_not_empty
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        KEEP_IMAGE)
            check_not_empty
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        KEEP_LOG)
            check_not_empty
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        # Repository Configuration
        REPO_LIST)
            check_repositories
            ;;
        # Packages Configuration
        ADD_PACKAGES)
            MAX_LENGTH=128
            check_length
            check_format
            ;;
        ADD_PACKAGES_LIST)
            check_format
            check_file
            ;;
        DELETE_PACKAGES)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        DELETE_PACKAGES_LIST)
            check_format
            check_file
            ;;
        REPLACE_PACKAGES)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        REPLACE_PACKAGES_LIST)
            check_format
            check_file
            ;;
        TASKS)
            update_distribution
            check_package_variables
            check_tasks
            ;;
        # Images Configuration
        GENERATE_IMAGES)
            VALUE=(${GENERATE_IMAGES[@]})
            check_not_empty
            VALID_VALUES=("iso" "raw" "qcow2" "vmdk" "docker" "wsl" "tar")
            check_preset_values
            unset VALUE
            if [ "${MULTIPLE_IMAGES}" = "true" ]; then
                check_image_compatibility
            else
                prohibit_multiple_images
            fi
            update_config "${CONFIG_FILE}" GENERATE_IMAGES
            ;;
        ISO_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        DOCKER_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        DOCKER_TAG)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        QCOW2_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        RAW_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        TAR_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        VMDK_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        WSL_NAME)
            MAX_LENGTH=64
            check_length
            check_format
            ;;
        esac
    done
    toggle_shell_options u

    if [ $ERRORS -gt 0 ]; then
        # Add echo statement to report number of errors found
        echo -e "${RED}The configuration file ${BOLD}${CONFIG_FILE}${ENDCOLOR}${RED} has ${BOLD}${ERRORS}${ENDCOLOR}${RED} error(s). Please make the necessary changes and try again.${ENDCOLOR}"
        return 1
    else
        return 0
    fi
}

check_required_packages() {
    # List of common packages
    local COMMON_PACKAGES="bash debootstrap sed gawk curl coreutils findutils net-tools zstd lz4 xz-utils gzip lzop squashfs-tools xorriso mtools dosfstools parted python3-minimal apt gettext"

    # Architecture-specific packages
    if [ "${ARCHITECTURE}" = "amd64" ]; then
        REQUIRED_PACKAGES="${COMMON_PACKAGES} grub-pc-bin grub-efi-amd64-bin grub-efi-ia32-bin"
    elif [ "${ARCHITECTURE}" = "arm64" ]; then
        REQUIRED_PACKAGES="${COMMON_PACKAGES} grub-efi-arm64-bin"
    else
        error "Unsupported architecture: ${ARCHITECTURE}"
        exit 1
    fi

    if [[ " ${GENERATE_IMAGES[@]} " =~ " qcow2 " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " vmdk " ]]; then
        REQUIRED_PACKAGES="${REQUIRED_PACKAGES} qemu-utils"
    fi

    # Check for installed packages
    MISSING_PACKAGES=""
    for PACKAGE in ${REQUIRED_PACKAGES}; do
        dpkg -s "${PACKAGE}" >/dev/null 2>&1 || {
            MISSING_PACKAGES="${MISSING_PACKAGES} ${PACKAGE}"
        }
    done

    # Offer to install missing packages
    if [ -n "${MISSING_PACKAGES}" ]; then
        error "${CYAN}${MAIN_EXECUTABLE}${ENDCOLOR} requires certain applications to be installed in order to function. The following required packages are missing from your system:"
        echo -e "${CYAN}${MISSING_PACKAGES}${ENDCOLOR}"
        exit 1
    fi

}

################################################################################
# Chroot functions
################################################################################
# Check if certain directories are mounted
check_if_mounted() {
    local DIR="$1"
    if mount | grep -q "${DIR}"; then
        echo 0
        return 0
    else
        return 1
    fi
}

# Check and set the installation directory
check_chroot() {
    current_function

    echo -e "The working directory is ${MAGENTA}${1}${ENDCOLOR}."
    if [ -d "${1}" ] && [ "$(ls -A "${1}" 2>/dev/null)" ]; then
        remove_chroot
    fi
}

# Unmount directories
unmount_dirs() {
    current_function

    local DIR_PATH="$1"
    local MOUNTS
    local FAILED=0

    # Normalize DIR_PATH to absolute path
    DIR_PATH="$(realpath -m "${DIR_PATH}")"

    # 1) Collect all mount points strictly inside DIR_PATH (deepest first)
    mapfile -t MOUNTS < <(
        findmnt -rn -o TARGET |
            while read -r TARGET; do
                TARGET_ABS="$(realpath -m "${TARGET}")"
                if [[ "${TARGET_ABS}" == "${DIR_PATH}/"* ]]; then
                    echo "${TARGET_ABS}"
                fi
            done |
            awk '{ print length, $0 }' |
            sort -rn |
            cut -d' ' -f2-
    )

    if [ "${#MOUNTS[@]}" -eq 0 ]; then
        information "No mount points found under ${DIR_PATH}."
        return 0
    fi

    # 2) Attempt to unmount each mount point
    for MOUNT in "${MOUNTS[@]}"; do
        information "Attempting to unmount ${MOUNT}..."
        if umount "${MOUNT}" 2>/dev/null; then
            information "Successfully unmounted ${MOUNT}."
            continue
        fi

        # If first attempt failed, kill any processes using that mount
        warning "Initial unmount failed for ${MOUNT}; terminating processes using it..."
        # First send SIGTERM
        fuser -m "${MOUNT}" 2>/dev/null | xargs -r kill -TERM
        sleep 1
        # Then send SIGKILL for any that remain
        fuser -m "${MOUNT}" 2>/dev/null | xargs -r kill -KILL

        information "Retrying unmount of ${MOUNT}..."
        if umount "${MOUNT}" 2>/dev/null; then
            information "Successfully unmounted ${MOUNT} after killing processes."
        else
            error "Could not unmount ${MOUNT} even after killing processes."
            ((FAILED++))
        fi
    done

    # 3) Final status report
    if [ "${FAILED}" -gt 0 ]; then
        error "Failed to unmount ${FAILED} mount point(s). Remaining mounted:"
        for MOUNT in "${MOUNTS[@]}"; do
            if mountpoint -q "${MOUNT}"; then
                warning "  * ${MOUNT}"
            fi
        done
        return 1
    fi

    information "All file systems under ${DIR_PATH} have been successfully unmounted."
    return 0
}

# Mount directories inside the chroot environment
mount_system_dirs() {
    current_function

    local DIR_PATH="$1"

    mount none -t proc "${DIR_PATH}/proc"
    mount none -t sysfs "${DIR_PATH}/sys"
    mount none -t devpts "${DIR_PATH}/dev/pts"
    mount none -t tmpfs "${DIR_PATH}/dev/shm"
    mount none -t tmpfs "${DIR_PATH}/tmp"

    mkdir -p "${BUILD_DIR}/aptcache/${DISTRIBUTION}"
    mount --bind "${BUILD_DIR}/aptcache/${DISTRIBUTION}" "${DIR_PATH}/var/cache/apt/archives"
}

# Checks if the package is available in the repository or if the file is a valid deb package.
check_package() {
    local PACKAGE_NAME="$1"

    if [[ "${PACKAGE_NAME}" == *"/"* ]]; then
        # Check if the file is a deb package
        if [[ -f "${PACKAGE_NAME}" ]] && dpkg-deb --info "${PACKAGE_NAME}" &>/dev/null; then
            return 0
        else
            return 1
        fi
    else
        if echo $(LANG=C apt-cache --quiet=0 policy "${PACKAGE_NAME}" 2>&1) | grep -q 'Candidate: (none)\|Unable to locate package\|No packages found'; then
            return 1
        else
            return 0
        fi
    fi
}

# Delete files based on the provided conditions
delete_files() {
    local DIR=$1

    local CONDITIONS=(-mindepth 1)

    if [[ "${KEEP_CACHE}" == "true" ]]; then
        CONDITIONS+=(-path "${DIR}/aptcache" -prune -o)
    fi

    if [[ "${KEEP_IMAGE}" == "true" ]]; then
        CONDITIONS+=(-name '*.iso' -prune -o -name '*.tar' -prune -o -name '*.img' -prune -o -name '*.qcow2' -prune -o -name '*.vmdk' -prune -o)
    fi

    if [[ "${KEEP_LOG}" == "true" ]]; then
        CONDITIONS+=(-name '*.log' -prune -o)
    fi

    CONDITIONS+=(-not -path "${LOG_FILE}")
    CONDITIONS+=(-not -path "${DIR}/config")

    toggle_shell_options e
    find "${DIR}" "${CONDITIONS[@]}" -exec rm -rf {} + 2>/dev/null
    toggle_shell_options e

    return 0
}

# This function removes sources from the build directory if specified.
# It first checks if the REMOVE_SOURCES environment variable is set to "true".
# If so, it unmounts any directories that are mounted within the BUILD_DIR.
remove_sources() {
    if [ "${REMOVE_SOURCES}" = "true" ]; then
        unmount_dirs "${BUILD_DIR}"
        if ! mount | grep -q "${BUILD_DIR}"; then
            delete_files "${BUILD_DIR}"
        else
            error "Cannot clear the build directory ${BUILD_DIR}. Make sure there are no mounted directories in it and try again."
            exit 1
        fi
    fi
}

# Remove INSTALL_DIR based on the command in CMD array
remove_chroot() {
    current_function
    # Check the current command and perform cleanup accordingly
    if [ "${CMD[iterator]}" = "build_environment" ]; then
        unmount_dirs "${INSTALL_DIR}"
        if ! mount | grep -q "${INSTALL_DIR}"; then
            rm -rf "${INSTALL_DIR}"
        else
            error "Cannot clear the chroot directory ${INSTALL_DIR}. Make sure there are no mounted directories in it and try again."
            exit 1
        fi
    fi
}

# Clean up the chroot environment
cleanup_chroot() {
    current_function

    toggle_shell_options eu

    rm -f $1/config 2>/dev/null
    rm -f $1/install 2>/dev/null
    rm -f $1/libastralive 2>/dev/null
    rm -f $1/packages.list 2>/dev/null
    rm -rf $1/packages 2>/dev/null
    rm -f $1/etc/apt/sources.list~ 2>/dev/null
    rm -f $1/etc/fstab 2>/dev/null
    rm -f $1/etc/mtab 2>/dev/null
    #rm -f $1/etc/ssh/ssh_host* 2>/dev/null
    rm -f $1/root/.bash_history 2>/dev/null
    rm -f $1/root/.wget-hsts 2>/dev/null
    rm -f $1/var/backups/* 2>/dev/null
    rm -f $1/var/cache/apt/*.bin 2>/dev/null
    #rm -f $1/var/cache/apt/archives/*.deb 2>/dev/null
    #!!!Uncommenting this line breaks autologin
    #rm -f $1/var/cache/debconf/* 2>/dev/null
    rm -f $1/var/cache/debconf/*-old 2>/dev/null
    rm -f $1/var/cache/fontconfig/* 2>/dev/null
    rm -f $1/var/cache/ldconfig/* 2>/dev/null
    rm -f $1/var/lib/apt/extended_states 2>/dev/null
    rm -f $1/var/lib/apt/lists/deb.* 2>/dev/null
    rm -f $1/var/lib/dhcp/dhclient.leases 2>/dev/null
    rm -f $1/var/lib/dpkg/*-old 2>/dev/null
    rm -f $1/var/lib/systemd/random-seed 2>/dev/null
    #rm -f $1/var/log/* 2>/dev/null
    find $1/var/log/ -type f -exec rm -f {} \; 2>/dev/null
    rm -f $1/var/log/*/* 2>/dev/null
    rm -f $1/var/log/*/*/* 2>/dev/null
    rm -Rf $1/etc/systemd/system/timers.target.wants 2>/dev/null
    rm -Rf $1/root/.cache 2>/dev/null
    rm -Rf $1/root/.local/share/mc 2>/dev/null
    for _DIRECTORY in $1/tmp $1/var/tmp; do
        rm -rf ${_DIRECTORY}

        mkdir -p ${_DIRECTORY}
        chmod 1777 ${_DIRECTORY}
    done

    toggle_shell_options eu
}

################################################################################
# Live functions
################################################################################

# Compress the chroot environment into a SquashFS filesystem
compress_chroot() {
    current_function

    local DIR_PATH="$1"
    EXCLUDE_FILE="${BUILD_DIR}/exclude"

    cat <<EOF >${EXCLUDE_FILE}
${DIR_PATH}/boot
${DIR_PATH}/config
${DIR_PATH}/etc/apt/sources.list~
${DIR_PATH}/etc/systemd/system/timers.target.wants
${DIR_PATH}/install
${DIR_PATH}/libastralive
${DIR_PATH}/packages.list
${DIR_PATH}/packages_to_check.list
${DIR_PATH}/root/.bash_history
${DIR_PATH}/root/.cache
${DIR_PATH}/root/.local/share/mc
${DIR_PATH}/root/.wget-hsts
${DIR_PATH}/var/lib/apt/extended_states
${DIR_PATH}/var/lib/dhcp/dhclient.leases
${DIR_PATH}/var/lib/systemd/random-seed
${DIR_PATH}/initrd.img
${DIR_PATH}/initrd.img.old
${DIR_PATH}/vmlinuz
${DIR_PATH}/vmlinuz.old
EOF
    toggle_shell_options e
    while IFS= read -r LINE; do
        if [ ! -e ${LINE} ]; then
            grep -vF "${LINE}" "${EXCLUDE_FILE}" >temp && mv temp "${EXCLUDE_FILE}"
        fi
    done <"${EXCLUDE_FILE}"

    find ${DIR_PATH}/var/backups -type f >>${BUILD_DIR}/exclude 2>/dev/null 2>/dev/null
    find ${DIR_PATH}/var/cache/apt -name "*.bin" >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/cache/apt/archives -name "*.deb" >>${BUILD_DIR}/exclude 2>/dev/null
    #!!!Uncommenting this line breaks autologin
    #find ${DIR_PATH}/var/cache/debconf -type f >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/cache/fontconfig -type f >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/cache/ldconfig -type f >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/lib/apt/lists -name "*InRelease" >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/lib/apt/lists -name "*Packages" >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/lib/dpkg -name "*-old" >>${BUILD_DIR}/exclude 2>/dev/null
    find ${DIR_PATH}/var/log -type f >>${BUILD_DIR}/exclude 2>/dev/null
    toggle_shell_options e

    if [ $COMP_TYPE = "zstd" ]; then
        time mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -Xcompression-level 19 -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE} || exit
    elif [ $COMP_TYPE = "xz" ]; then
        time mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -Xbcj x86 -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE} || exit
    else
        time mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE} || exit
    fi
}

# Copy the kernel and initramfs from the install directory to the build directory
copy_kernel_initramfs() {
    current_function

    cp ${INSTALL_DIR}/boot/vmlinuz-* \
        ${BUILD_DIR}/image/live/vmlinuz &&
        cp ${INSTALL_DIR}/boot/initrd.img-* \
            ${BUILD_DIR}/image/live/initrd.img
}

# Create menu configuration file for grub
create_grub_cfg() {
    current_function

    cat <<'EOF' >"${BUILD_DIR}/scratch/grub.cfg"
search --set=root --file /.disk/info
set prefix=($root)/boot/grub
configfile ($root)/boot/grub/grub.cfg
EOF
    cat <<'EOF' >"${BUILD_DIR}/image/boot/grub/loopback.cfg"
source /boot/grub/grub.cfg
EOF
}

# Create special file in image named .disk/info
create_disk_info() {
    current_function

    mkdir -p ${BUILD_DIR}/image/.disk
    echo "Astra Linux" >${BUILD_DIR}/image/.disk/info
}

# Create UEFI image
create_uefi_image() {
    current_function

    if [ ${ARCHITECTURE} = "amd64" ]; then
        grub-mkstandalone \
            --format=x86_64-efi \
            --output=${BUILD_DIR}/scratch/bootx64.efi \
            --locales="" \
            --fonts="" \
            "boot/grub/grub.cfg=${BUILD_DIR}/scratch/grub.cfg"
        grub-mkstandalone \
            --format=i386-efi \
            --output=${BUILD_DIR}/scratch/bootia32.efi \
            --locales="" \
            --fonts="" \
            "boot/grub/grub.cfg=${BUILD_DIR}/scratch/grub.cfg"
    elif [ ${ARCHITECTURE} = "arm64" ]; then
        grub-mkstandalone \
            --format=arm64-efi \
            --output=${BUILD_DIR}/scratch/bootaa64.efi \
            --locales="" \
            --fonts="" \
            "boot/grub/grub.cfg=${BUILD_DIR}/scratch/grub.cfg"
    fi
}

# Create FAT16 UEFI boot disk image
create_uefi_boot_disk_image() {
    current_function

    size=0
    for file in "${BUILD_DIR}/scratch/"*.efi \
        "${BUILD_DIR}/scratch/grub.cfg"; do
        size=$(($size + $(stat -c %s "$file")))
    done
    # directories: EFI EFI/boot boot boot/grub
    size=$(($size + 4096 * 4))
    blocks=$((($size / 1024 + 55) / 32 * 32))
    rm -f "${BUILD_DIR}/scratch/efiboot.img"
    (
        cd "${BUILD_DIR}/scratch"
        mkfs.msdos -C ./efiboot.img $blocks >/dev/null
        mmd -i efiboot.img efi efi/boot
        if [ ${ARCHITECTURE} = "amd64" ]; then
            mcopy -i efiboot.img ./bootx64.efi ::efi/boot/
            mcopy -i efiboot.img ./bootia32.efi ::efi/boot/
        elif [ ${ARCHITECTURE} = "arm64" ]; then
            mcopy -i efiboot.img ./bootaa64.efi ::efi/boot/
        fi
    )
}

# Create BIOS image
create_bios_image() {
    if [ ${ARCHITECTURE} = "amd64" ]; then
        current_function

        grub-mkstandalone \
            --format=i386-pc \
            --output=${BUILD_DIR}/scratch/core.img \
            --install-modules="linux normal search iso9660 configfile memdisk tar part_msdos part_gpt fat ls" \
            --modules="linux normal iso9660 biosdisk search" \
            --locales="" \
            --fonts="" \
            "boot/grub/grub.cfg=${BUILD_DIR}/scratch/grub.cfg"
    fi
}

# Combine GRUB cdboot.img with our boot image
combine_boot_images() {
    if [ ${ARCHITECTURE} = "amd64" ]; then
        current_function

        cat /usr/lib/grub/i386-pc/cdboot.img \
            ${BUILD_DIR}/scratch/core.img \
            >${BUILD_DIR}/scratch/bios.img
    fi
}

# Copy GRUB modules, excluding those that are already built-in, to a specified directory.
copy_grub_modules() {
    current_function

    local OUTDIR PLATFORM X
    # Copyright (C) 2010, 2011 Canonical Ltd.
    # Author: Colin Watson <cjwatson@ubuntu.com>

    # Copy GRUB modules.

    if [ -z "$1" ] || [ -z "$2" ]; then
        echo "usage: $0 OUTPUT-DIRECTORY GRUB-PLATFORM"
        exit 1
    fi

    OUTDIR="$1"
    PLATFORM="$2"

    # Copy over GRUB modules, except for those already built in.
    cp -a "/usr/lib/grub/${PLATFORM}"/*.lst "${OUTDIR}/boot/grub/${PLATFORM}/"
    for X in "/usr/lib/grub/${PLATFORM}"/*.mod; do
        case $(basename "$X" .mod) in
        fshelp | iso9660 | memdisk | search | search_fs_file | search_fs_uuid | search_label | tar)
            # included in boot image
            ;;
        affs | afs | afs_be | befs | befs_be | minix | nilfs2 | sfs | zfs | zfsinfo)
            # unnecessary filesystem modules
            ;;
        example_functional_test | functional_test | hello)
            # other cruft
            ;;
        *)
            cp -a "$X" "${OUTDIR}/boot/grub/${PLATFORM}/"
            ;;
        esac
    done
}

# Move files from the GRUB source directory to a target directory.
copy_grub_files() {
    current_function

    local GRUB_SOURCE_DIR

    if [ -d "${SCRIPT_DIR}/grub" ]; then
        GRUB_SOURCE_DIR="${SCRIPT_DIR}/grub"
    elif [ -d "/usr/share/astra-live/grub" ]; then
        GRUB_SOURCE_DIR="/usr/share/astra-live/grub"
    fi

    # Copy the files from the GRUB_SOURCE_DIR to the destination
    if [ -n "${GRUB_SOURCE_DIR}" ]; then
        cp -r "${GRUB_SOURCE_DIR}/"* "${BUILD_DIR}/image/boot/grub"
    fi

    cat <<EOF >"${BUILD_DIR}/image/boot/grub/grub.cfg"
# Set default boot entry to the first one (0-indexed)
set default=0

# Set timeout to 10 seconds
set timeout=10

# Load fonts
loadfont \$prefix/dejavu-bold-16.pf2
loadfont \$prefix/dejavu-bold-14.pf2
loadfont \$prefix/roboto-bold-20.pf2
loadfont \$prefix/roboto-regular-20.pf2
loadfont \$prefix/unicode.pf2

# Load modules
insmod all_video
insmod gfxterm
insmod png

# Set default colors for terminal
set color_normal=light-gray/black
set color_highlight=white/black
EOF
    if [ "${DISTRIBUTION}" = "1.8_x86-64" ] || [ "${DISTRIBUTION}" = "4.8_arm" ]; then
        cat <<EOF >>"${BUILD_DIR}/image/boot/grub/grub.cfg"
# Platform-specific settings (BIOS vs. UEFI)
if [ "\$grub_platform" = "pc" ]; then  # BIOS platform
    # Set graphics mode
    set gfxmode=1024x768x32
    # Set VGA mode
    set vga="vga=791"

    # Load BIOS theme if it exists
    if [ -e /boot/grub/live-theme/theme.txt ]; then
        set theme=/boot/grub/live-theme/theme.txt
    else
        # Fallback menu colors
        set menu_color_normal=cyan/blue
        set menu_color_highlight=white/blue
    fi
else  # UEFI platform
    # Auto-detect graphics mode
    set gfxmode=auto

    # Load UEFI theme if it exists
    if [ -e /boot/grub/live-theme/theme-efi.txt ]; then
        set theme=/boot/grub/live-theme/theme-efi.txt
    else
        # Fallback menu colors
        set menu_color_normal=cyan/blue
        set menu_color_highlight=white/blue
    fi
fi
EOF
        mv -f "${BUILD_DIR}/image/boot/grub/grub-1.8-4x3.png" "${BUILD_DIR}/image/boot/grub/grub-4x3.png"
        mv -f "${BUILD_DIR}/image/boot/grub/grub-1.8-16x9.png" "${BUILD_DIR}/image/boot/grub/grub-16x9.png"
        rm -f "${BUILD_DIR}/image/boot/grub/grub-1.7-4x3.png"
    else
        cat <<EOF >>"${BUILD_DIR}/image/boot/grub/grub.cfg"
# Platform-specific settings (BIOS vs. UEFI)
if [ "\$grub_platform" = "pc" ]; then  # BIOS platform
    # Set graphics mode
    set gfxmode=1024x768x32
    # Set VGA mode
    set vga="vga=791"
else
    # Auto-detect graphics mode
    set gfxmode=auto
fi

    # Load theme if it exists
    if [ -e /boot/grub/live-theme/theme.txt ]; then
        set theme=/boot/grub/live-theme/theme.txt
    else
        # Fallback menu colors for
        set menu_color_normal=cyan/blue
        set menu_color_highlight=white/blue
    fi

EOF
        rm -f "${BUILD_DIR}/image/boot/grub/live-theme/theme-efi.txt"
        mv -f "${BUILD_DIR}/image/boot/grub/grub-1.7-4x3.png" "${BUILD_DIR}/image/boot/grub/grub-4x3.png"
        rm -f "${BUILD_DIR}/image/boot/grub/grub-1.8-4x3.png"
        rm -f "${BUILD_DIR}/image/boot/grub/grub-1.8-16x9.png"
    fi

    cat <<EOF >>"${BUILD_DIR}/image/boot/grub/grub.cfg"
# Set terminal output to gfxterm
terminal_output gfxterm


# Play boot sound (optional)
insmod play
play 960 440 1 0 4 440 1

# --- Boot Menu Entries ---

menuentry "Astra Linux (стандартная загрузка)" --class live {
    linux	/live/vmlinuz \$vga boot=live noautologin nox11autologin net.ifnames=0 parsec.mac=0 parsec.max_ilev=0 quiet splash
    initrd	/live/initrd.img
}

menuentry "Astra Linux (загрузка в ОЗУ)" --class ram {
    linux	/live/vmlinuz \$vga boot=live noautologin nox11autologin net.ifnames=0 parsec.mac=0 parsec.max_ilev=0 quiet splash toram
    initrd	/live/initrd.img
}

menuentry "Перезагрузка" --class reboot {
    reboot
}
EOF

}

################################################################################
#
################################################################################

set_packages_list() {
    current_function
    local DIR
    if [ ! -f "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot" ]; then
        for DIR in "${SCRIPT_DIR}" "/usr/share/astra-live"; do
            if [ -f "${DIR}/packages/${DISTRIBUTION}.list.chroot" ]; then
                cp "${DIR}/packages/${DISTRIBUTION}.list.chroot" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot" || error "Failed to copy the reference file to ${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot."
                break
            fi
        done
    fi
    grant_write_permissions "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
}

grant_write_permissions() {
    local FILE="${1}"
    local GROUP="astra-admin"
    local PERMS="664"

    # Check if the group exists
    if getent group "${GROUP}" >/dev/null; then
        if [ "$(stat -c '%G' ${FILE})" != "${GROUP}" ] || [ "$(stat -c '%a' ${FILE})" != "${PERMS}" ]; then
            chgrp "${GROUP}" "${FILE}"
            chmod "${PERMS}" "${FILE}"
            warning "Permissions for the file ${FILE} have been granted to the '${GROUP}' group."
        fi
    else
        warning "The group '${GROUP}' does not exist. No operations were performed on the file ${FILE}."
    fi
}

build_environment() {
    current_process

    # Set up the installation directory
    check_chroot "${INSTALL_DIR}"

    mkdir -p "${BUILD_DIR}"/{image/boot/grub,image/live,packages,sources,scratch} "${INSTALL_DIR}/var/cache/apt/archives" "${BUILD_DIR}/aptcache/${DISTRIBUTION}"

    toggle_shell_options u
    handle_repositories
    toggle_shell_options u
    set_packages_list
}

# Bootstrap and configure Debian
build_bootstrap() {
    current_process

    local INCLUDE_PACKAGES INCLUDES PACKAGE

    INCLUDE_PACKAGES=("apt-transport-https" "ca-certificates")
    toggle_shell_options u
    if [ -n "${TASKS}" ]; then
        INCLUDE_PACKAGES+=("tasksel")
    fi
    toggle_shell_options u

    INCLUDES=""

    for PACKAGE in "${INCLUDE_PACKAGES[@]}"; do
        INCLUDES+="--include=${PACKAGE} "
    done

    mkdir -p "${BUILD_DIR}/aptcache/${DISTRIBUTION}"
    mount --bind "${BUILD_DIR}/aptcache/${DISTRIBUTION}" "${INSTALL_DIR}/var/cache/apt/archives"
    if [ ! -e "/usr/share/debootstrap/scripts/${DISTRIBUTION}" ]; then
        ln -nfs sid "/usr/share/debootstrap/scripts/${DISTRIBUTION}"
    fi
    debootstrap \
        --no-check-gpg \
        --arch="${ARCHITECTURE}" \
        --variant=minbase \
        --components=main,contrib,non-free \
        ${INCLUDES} \
        "${DISTRIBUTION}" \
        "${INSTALL_DIR}" \
        "${MIRROR_URL}"
}

# Execute hooks in a specified directory.
run_hooks() {
    current_function

    local HOOKS_DIR
    if [ -d "${SCRIPT_DIR}/hooks" ]; then
        HOOKS_DIR="${SCRIPT_DIR}/hooks/${1}"
    elif [ -d /usr/share/astra-live/hooks ]; then
        HOOKS_DIR="/usr/share/astra-live/hooks/${1}"
    fi

    declare -A HOOKS
    local SUBDIR
    local FILE
    for SUBDIR in "${HOOKS_DIR}"/*; do
        if [ -d "${SUBDIR}" ]; then
            for FILE in "${SUBDIR}"/*; do
                if [ -x "${FILE}" ]; then
                    HOOKS["$(basename "${FILE}")"]="${FILE}"
                fi
            done
        fi
    done

    local KEYS
    IFS=$'\n' KEYS=($(sort <<<"${!HOOKS[*]}"))
    unset IFS

    for KEY in "${KEYS[@]}"; do
        FILE="${HOOKS[$KEY]}"
        echo -e "${BLUE}=====> running ${CYAN}${KEY}${ENDCOLOR}${BLUE} ...${ENDCOLOR}"
        chroot "${2}" bash -c "$(cat "${FILE}")" >/dev/null
    done
}

create_union() {
    current_function

    local DIR_PATH="$1"

    mkdir -p "${DIR_PATH}"/{upper,work,merged}
    mount -t overlay overlay -o lowerdir=${BUILD_DIR}/chroot,upperdir=${DIR_PATH}/upper,workdir=${DIR_PATH}/work ${DIR_PATH}/merged
}

# Chroot to Debian environment and configure
build_chroot() {
    current_process
    mount_system_dirs "${INSTALL_DIR}"

    # Copy the generated list of repository sources.
    if [ -f "${BUILD_DIR}/sources/${DISTRIBUTION}.list" ]; then
        cp -f "${BUILD_DIR}/sources/${DISTRIBUTION}.list" "${INSTALL_DIR}/etc/apt/sources.list"
    else
        error "There is no list of repository sources."
        exit 1
    fi
    # Copy the configuration file.
    cp -f "${BUILD_DIR}/config" "${INSTALL_DIR}/config"
    # Copy the main library.
    if [ -f "${SCRIPT_DIR}/libastralive" ]; then
        cp -f "${SCRIPT_DIR}/libastralive" "${INSTALL_DIR}/libastralive"
    elif [ -f "/usr/lib/astra-live/libastralive" ]; then
        cp -f "/usr/lib/astra-live/libastralive" "${INSTALL_DIR}/libastralive"
    else
        error "No libastralive library found."
    fi
    # Copy the chroot installation script.
    if [ -f "${SCRIPT_DIR}/chroot-install" ]; then
        cp -f "${SCRIPT_DIR}/chroot-install" "${INSTALL_DIR}/install"
    elif [ -f "/usr/share/astra-live/chroot-install" ]; then
        cp -f "/usr/share/astra-live/chroot-install" "${INSTALL_DIR}/install"
    else
        error "No chroot-install script found."
    fi

    # If tasks are defined, then form a list of packages in chroot from them.
    toggle_shell_options u
    if [ -n "${TASKS}" ]; then
        chroot "${INSTALL_DIR}" bin/bash -c "
. /config || exit 1
. /libastralive || exit 1
apt-get update
handle_tasks_chroot"
        if [ -f "${INSTALL_DIR}/packages.list" ]; then
            cp "${INSTALL_DIR}/packages.list" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
        fi
    fi
    toggle_shell_options u

    if [[ " ${GENERATE_IMAGES[@]} " =~ " iso " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " raw " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " qcow2 " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " vmdk " ]]; then
        if [ ! -f "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot" ]; then
            if [ -f "${SCRIPT_DIR}/packages/${DISTRIBUTION}.list.chroot" ]; then
                cp "${SCRIPT_DIR}/packages/${DISTRIBUTION}.list.chroot" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
            elif [ -f "/usr/share/astra-live/packages/${DISTRIBUTION}.list.chroot" ]; then
                cp "/usr/share/astra-live/packages/${DISTRIBUTION}.list.chroot" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
            fi
        fi
    fi

    toggle_shell_options eu
    if [ -n "${REPLACE_PACKAGES[0]}" ] || [ -n "${REPLACE_PACKAGES_LIST}" ]; then
        echo "" >"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
        if [ -n "${REPLACE_PACKAGES[0]}" ]; then
            for PACKAGE in "${REPLACE_PACKAGES[@]}"; do
                echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
            done
        fi
        if [ -n "${REPLACE_PACKAGES_LIST}" ]; then
            REPLACE_PACKAGES_LIST=$(realpath "${REPLACE_PACKAGES_LIST}")
            cat "${REPLACE_PACKAGES_LIST}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
        fi
    else
        local NEW_LINE_ADDED=0
        if [ -n "${ADD_PACKAGES[0]}" ]; then
            LAST_LINE=$(tail -n 1 "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot")
            if [ -n "${LAST_LINE}" ]; then
                echo -e "\n" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                NEW_LINE_ADDED=1
            fi
            for PACKAGE in "${ADD_PACKAGES[@]}"; do
                if [[ "${PACKAGE}" == */* ]]; then
                    if [[ "${PACKAGE}" =~ ^(http|https|ftp):// ]]; then
                        if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                        fi
                    elif [ -f "${PACKAGE}" ]; then
                        PACKAGE=$(realpath "${PACKAGE}")
                        if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                        fi
                    elif [ -d "${PACKAGE}" ]; then
                        find "${PACKAGE}" -type f -name "*.deb" -exec realpath {} \; | while read -r DEB_PACKAGE; do
                            if ! grep -Fxq "${DEB_PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                                echo "${DEB_PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                            fi
                        done
                    else
                        if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                        fi
                    fi
                else
                    if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                        echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                    fi
                fi
            done
        fi
        if [ -n "${ADD_PACKAGES_LIST}" ]; then
            if [ "${NEW_LINE_ADDED}" -eq 0 ]; then
                LAST_LINE=$(tail -n 1 "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot")
                if [ -n "${LAST_LINE}" ]; then
                    echo -e "\n" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                fi
            fi
            TEMP_ADD_FILE=$(mktemp)
            cp ${ADD_PACKAGES_LIST} ${TEMP_ADD_FILE}
            if [ "$(tail -c 1 "${TEMP_ADD_FILE}")" != "" ]; then
                echo "" >>${TEMP_ADD_FILE}
            fi
            while IFS= read -r PACKAGE; do
                if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"; then
                    echo "${PACKAGE}" >>"${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
                fi
            done <"${TEMP_ADD_FILE}"
            rm ${TEMP_ADD_FILE}
        fi
        if [ -n "${DELETE_PACKAGES[0]}" ]; then
            for PACKAGE in "${DELETE_PACKAGES[@]}"; do
                sed -i "/${PACKAGE}/d" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
            done
        fi
        if [ -n "${DELETE_PACKAGES_LIST}" ]; then
            TEMP_DELETE_FILE=$(mktemp)
            cp ${DELETE_PACKAGES_LIST} ${TEMP_DELETE_FILE}
            if [ "$(tail -c 1 "${TEMP_DELETE_FILE}")" != "" ]; then
                echo "" >>${TEMP_DELETE_FILE}
            fi
            while IFS= read -r PACKAGE; do
                sed -i "/${PACKAGE}/d" "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot"
            done <"${TEMP_DELETE_FILE}"
            rm ${TEMP_DELETE_FILE}
        fi
    fi
    toggle_shell_options eu

    if [ -f "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot" ]; then
        cp "${BUILD_DIR}/packages/${DISTRIBUTION}.list.chroot" "${INSTALL_DIR}/packages.list"
        mkdir -p "${INSTALL_DIR}/packages"
        while IFS= read -r LINE; do
            if [[ "${LINE}" =~ ^(http|https|ftp):// ]]; then
                FILENAME=$(basename -- "${LINE}")
                wget -O "${INSTALL_DIR}/packages/${FILENAME}" "${LINE}"
                sed -i "s|${LINE}|/packages/${FILENAME}|" "${INSTALL_DIR}/packages.list"
            elif [ -f "${LINE}" ] && [[ "${LINE}" == *.deb ]]; then
                FILENAME=$(basename -- "${LINE}")
                cp "${LINE}" "${INSTALL_DIR}/packages"
                sed -i "s|${LINE}|/packages/${FILENAME}|" "${INSTALL_DIR}/packages.list"
            fi
        done <"${INSTALL_DIR}/packages.list"
    fi

    if [ -f "${INSTALL_DIR}/install" ]; then
        chmod +x "${INSTALL_DIR}/install"
        chroot "${INSTALL_DIR}" /install
    fi

    # Replacing the list of repositories with a pre-installed one
    if [ "${KEEP_SOURCES_LIST}" != "true" ]; then # Only replace if KEEP_SOURCES is not set
        if [ -f "${SCRIPT_DIR}/sources/${DISTRIBUTION}.list" ]; then
            cp -f "${SCRIPT_DIR}/sources/${DISTRIBUTION}.list" "${INSTALL_DIR}/etc/apt/sources.list" 2>/dev/null
        elif [ -f "/usr/share/astra-live/sources/${DISTRIBUTION}.list" ]; then
            cp -f "/usr/share/astra-live/sources/${DISTRIBUTION}.list" "${INSTALL_DIR}/etc/apt/sources.list" 2>/dev/null
        fi
    fi
    unmount_dirs "${INSTALL_DIR}"
    cleanup_chroot "${INSTALL_DIR}"

    for IMAGE_TYPE in "iso" "raw" "qcow2" "vmdk"; do
        if [[ " ${GENERATE_IMAGES[@]} " =~ " ${IMAGE_TYPE} " ]]; then
            if [ "${MULTIPLE_IMAGES}" = "true" ]; then
                create_union "${BUILD_DIR}/union/${IMAGE_TYPE}"
                WORK_DIR="${BUILD_DIR}/union/${IMAGE_TYPE}/merged"
            else
                WORK_DIR="${INSTALL_DIR}"
            fi
            #mount_system_dirs "${BUILD_DIR}/union/${IMAGE_TYPE}/merged"
            toggle_shell_options e
            run_hooks "${IMAGE_TYPE}" "${WORK_DIR}"
            toggle_shell_options e
            unmount_dirs "${BUILD_DIR}"
        fi
    done

}

# Prepare the live build environment
build_live() {
    current_process

    # Unmounts directories inside the chroot environment
    unmount_dirs "${INSTALL_DIR}"
    if [[ " ${GENERATE_IMAGES[@]} " =~ " iso " ]]; then
        if [ "${MULTIPLE_IMAGES}" = "true" ]; then
            create_union "${BUILD_DIR}/union/iso"
            WORK_DIR="${BUILD_DIR}/union/iso/merged"
        else
            WORK_DIR="${INSTALL_DIR}"
        fi
        # Compresses the chroot environment into a Squash filesystem
        compress_chroot "${WORK_DIR}"
    fi
}

# Prepare the boot build environment
build_boot() {
    if [[ " ${GENERATE_IMAGES[@]} " =~ " iso " ]]; then
        current_process

        # Copy kernel and initramfs to live directory
        copy_kernel_initramfs

        # Create menu configuration file for grub
        create_grub_cfg

        # Copy GRUB modules
        if [ "${ARCHITECTURE}" = "amd64" ]; then
            for DIR in "x86_64-efi" "i386-efi" "i386-pc"; do
                if [ -d "/usr/lib/grub/${DIR}" ]; then
                    mkdir -p "${BUILD_DIR}/image/boot/grub/${DIR}"
                    # Copy the GRUB modules
                    copy_grub_modules "${BUILD_DIR}/image" "${DIR}"
                fi
            done
        elif [ "${ARCHITECTURE}" = "arm64" ]; then
            for DIR in "arm64-efi"; do
                if [ -d "/usr/lib/grub/${DIR}" ]; then
                    mkdir -p "${BUILD_DIR}/image/boot/grub/${DIR}"
                    # Copy the GRUB modules
                    copy_grub_modules "${BUILD_DIR}/image" "${DIR}"
                fi
            done
        fi

        # Copy GRUB theme and menu
        copy_grub_files

        # Create special file in image named /.disk/info
        create_disk_info

        # Create UEFI image
        create_uefi_image

        # Create FAT16 UEFI boot disk image
        create_uefi_boot_disk_image

        # Create BIOS image
        create_bios_image

        # Combine GRUB cdboot.img with our boot image
        combine_boot_images
    fi
}

# Function to setup the loop or nbd device
setup_device() {
    local IMAGE_NAME=$1
    local IMAGE_TYPE=$2

    local DIR_SIZE=$(du -sb ${BUILD_DIR}/chroot | cut -f1)
    local DIR_SIZE_BYTES=$(echo "${DIR_SIZE} + 0" | bc) # Convert to bytes
    local TOTAL_SIZE_BYTES=$(echo "${DIR_SIZE_BYTES} + 2 * 1024 * 1024 * 1024" | bc)

    if [[ "${IMAGE_TYPE}" == "raw" ]]; then
        dd if=/dev/zero of="${BUILD_DIR}/${IMAGE_NAME}" bs=1M count=0 seek=$(echo "${TOTAL_SIZE_BYTES} / 1024 / 1024" | bc)
        LOOPDEV=$(losetup --show -f -P "${BUILD_DIR}/${IMAGE_NAME}")
    else
        qemu-img create -f ${IMAGE_TYPE} "${BUILD_DIR}/${IMAGE_NAME}" "${TOTAL_SIZE_BYTES}"
        sleep 5
        qemu-nbd -c /dev/${NBD_DEVICE} "${BUILD_DIR}/${IMAGE_NAME}"
        LOOPDEV="/dev/${NBD_DEVICE}"
    fi
}

# Function to create partitions and file systems
create_partitions_and_filesystems() {
    local DEVICE=$1

    parted "${DEVICE}" -- mklabel msdos
    parted "${DEVICE}" -- mkpart primary fat32 1M 100M
    parted "${DEVICE}" -- mkpart primary ext4 100M 100%

    mkfs.fat -F32 ${DEVICE}p1
    mkfs.ext4 ${DEVICE}p2
}

# Function to setup the mount points and install grub
setup_mount_and_grub() {
    local DEVICE=$1
    local IMAGE_TYPE=$2

    mkdir -p "${BUILD_DIR}/mountpoint"
    mount ${DEVICE}p2 "${BUILD_DIR}/mountpoint"
    mkdir -p "${BUILD_DIR}/mountpoint/boot/efi"
    mount ${DEVICE}p1 "${BUILD_DIR}/mountpoint/boot/efi"
    time cp -a ${INSTALL_DIR}/. "${BUILD_DIR}/mountpoint/"
    mount_system_dirs "${BUILD_DIR}/mountpoint/"
    echo 'GRUB_DISABLE_OS_PROBER=true' | tee -a ${BUILD_DIR}/mountpoint/etc/default/grub
    echo 'GRUB_CMDLINE_LINUX="net.ifnames=0"' | tee -a ${BUILD_DIR}/mountpoint/etc/default/grub
    mount --bind /dev "${BUILD_DIR}/mountpoint/dev"
    if [ "${ARCHITECTURE}" = "amd64" ]; then
        chroot "${BUILD_DIR}/mountpoint" bin/bash -c "grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=astra --recheck --no-nvram --removable"
        chroot "${BUILD_DIR}/mountpoint" bin/bash -c "grub-install --target=i386-pc --recheck ${DEVICE}"
    elif [ "${ARCHITECTURE}" = "arm64" ]; then
        chroot "${BUILD_DIR}/mountpoint" bin/bash -c "grub-install --target=arm64-efi --efi-directory=/boot/efi --bootloader-id=astra --recheck --no-nvram --removable"
    fi
    chroot "${BUILD_DIR}/mountpoint" bin/bash -c "update-grub"
    sed -i '/GRUB_DISABLE_OS_PROBER=true/d' ${BUILD_DIR}/mountpoint/etc/default/grub

    cat <<EOF >${BUILD_DIR}/mountpoint/etc/fstab
# /etc/fstab: static file system information.
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
UUID=$(blkid -s UUID -o value ${DEVICE}p2) / ext4 defaults 0 1
EOF

    cat <<EOF >${BUILD_DIR}/mountpoint/etc/hosts
127.0.0.1 localhost
127.0.1.1 ${HOST_NAME}
EOF

    unmount_dirs ${BUILD_DIR}/mountpoint

    if [[ "${IMAGE_TYPE}" == "raw" ]]; then
        losetup -d ${DEVICE}
    else
        qemu-nbd -d /dev/${NBD_DEVICE}
    fi
}

# Generate ISO file
build_image() {
    current_process
    local IMAGE IMAGES

    DATETIME=$(date +"%d.%m.%y_%H.%M")
    ASTRA_VERSION=$(cat ${INSTALL_DIR}/etc/astra/build_version)

    # ISO image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " iso " ]]; then
        ISO_NAME=${ISO_NAME:-"livecd-${ASTRA_VERSION}-${DATETIME}.iso"}

        if [ "${ARCHITECTURE}" = "amd64" ]; then
            xorriso \
                -as mkisofs \
                -iso-level 3 \
                -full-iso9660-filenames \
                --joliet \
                -r \
                -volid "Astra Linux" \
                -eltorito-boot \
                boot/grub/bios.img \
                -no-emul-boot \
                -boot-load-size 4 \
                -boot-info-table \
                --eltorito-catalog boot/grub/boot.cat \
                --grub2-boot-info \
                --grub2-mbr /usr/lib/grub/i386-pc/boot_hybrid.img \
                -eltorito-alt-boot \
                -e EFI/efiboot.img \
                -no-emul-boot \
                -append_partition 2 0xef ${BUILD_DIR}/scratch/efiboot.img \
                -output "${BUILD_DIR}/${ISO_NAME}" \
                -graft-points \
                "${BUILD_DIR}/image" \
                /boot/grub/bios.img=${BUILD_DIR}/scratch/bios.img \
                /EFI/efiboot.img=${BUILD_DIR}/scratch/efiboot.img

        elif [ "${ARCHITECTURE}" = "arm64" ]; then
            xorriso \
                -as mkisofs \
                -iso-level 3 \
                -full-iso9660-filenames \
                -volid "Astra Linux" \
                -eltorito-alt-boot \
                -e EFI/BOOT/bootaa64.efi \
                -no-emul-boot \
                -append_partition 2 0xef ${BUILD_DIR}/scratch/efiboot.img \
                -output "${BUILD_DIR}/${ISO_NAME}" \
                -graft-points \
                "${BUILD_DIR}/image" \
                /EFI/BOOT/bootaa64.efi=${BUILD_DIR}/scratch/efiboot.img
        fi
    fi

    # RAW image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " raw " ]]; then
        RAW_NAME=${RAW_NAME:-"raw-${ASTRA_VERSION}-${DATETIME}.img"}
        setup_device "${RAW_NAME}" "raw"
        create_partitions_and_filesystems ${LOOPDEV}
        setup_mount_and_grub ${LOOPDEV} "raw"
    fi

    # QCOW2 image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " qcow2 " ]]; then
        QCOW2_NAME=${QCOW2_NAME:-"qcow2-${ASTRA_VERSION}-${DATETIME}.qcow2"}

        # Check if nbd is loaded
        if ! lsmod | grep "nbd" &>/dev/null; then
            modprobe nbd max_part=63
        fi

        # Find the next unused nbd device
        for i in /sys/class/block/nbd*; do
            if [[ -e "$i/size" && $(cat "$i/size") == 0 ]]; then
                NBD_DEVICE=$(basename $i)
                break
            fi
        done

        if [[ -z "${NBD_DEVICE+x}" ]]; then
            echo "Couldn't find an unused nbd device."
            exit 1
        fi

        setup_device "${QCOW2_NAME}" "qcow2"
        create_partitions_and_filesystems ${LOOPDEV}
        setup_mount_and_grub ${LOOPDEV} "qcow2"
    fi

    # VMDK image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " vmdk " ]]; then
        VMDK_NAME=${VMDK_NAME:-"vmdk-${ASTRA_VERSION}-${DATETIME}.vmdk"}

        # Check if nbd is loaded
        if ! lsmod | grep "nbd" &>/dev/null; then
            modprobe nbd max_part=63
        fi

        # Find the next unused nbd device
        for i in /sys/class/block/nbd*; do
            if [[ -e "$i/size" && $(cat "$i/size") == 0 ]]; then
                NBD_DEVICE=$(basename $i)
                break
            fi
        done

        if [[ -z "${NBD_DEVICE+x}" ]]; then
            echo "Couldn't find an unused nbd device."
            exit 1
        fi

        setup_device "${VMDK_NAME}" "vmdk"
        create_partitions_and_filesystems ${LOOPDEV}
        setup_mount_and_grub ${LOOPDEV} "vmdk"
    fi

    # Docker image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " docker " ]]; then
        DOCKER_NAME=${DOCKER_NAME:-"docker-${ASTRA_VERSION}-${DATETIME}.tar"}

        cd "${BUILD_DIR}"
        cat <<EOF >${BUILD_DIR}/Dockerfile
FROM scratch
ADD $(basename ${INSTALL_DIR}) /
ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV LANG ru_RU.UTF-8
CMD [\"/bin/bash\"]

EOF
        docker build -t ${DOCKER_TAG} .
        docker save -o ${BUILD_DIR}/${DOCKER_NAME} ${DOCKER_TAG}
        cd -
    fi

    # WSL image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " wsl " ]]; then
        WSL_NAME=${WSL_NAME:-"wsl-${ASTRA_VERSION}-${DATETIME}.tar"}

        cd ${INSTALL_DIR}
        tar cfp "${BUILD_DIR}/${WSL_NAME}" ./
    fi

    # Display information about the creation of the image
    IMAGES=("ISO_NAME" "RAW_NAME" "QCOW2_NAME" "VMDK_NAME" "DOCKER_NAME" "WSL_NAME")
    IMAGE_NAME=""
    toggle_shell_options u
    for IMAGE in "${IMAGES[@]}"; do
        if [[ -n ${!IMAGE} ]] && [[ -z $IMAGE_NAME ]]; then
            IMAGE_NAME=${!IMAGE}
            if [ -f "${BUILD_DIR}/${IMAGE_NAME}" ]; then
                echo ">>>>>> image ${BUILD_DIR}/${IMAGE_NAME} has been created."
            fi
            break
        fi
    done
    toggle_shell_options u

    # TAR image generation
    if [[ " ${GENERATE_IMAGES[@]} " =~ " tar " ]]; then
        TAR_NAME=${TAR_NAME:-"tarball-${ASTRA_VERSION}-${DATETIME}.tar"}

        tar cfp "${BUILD_DIR}/${TAR_NAME}" -C ${INSTALL_DIR} ./
        if [ -f "${BUILD_DIR}/${TAR_NAME}" ]; then
            echo ">>>>>> image ${BUILD_DIR}/${TAR_NAME} has been created."
        fi
    fi
}
