#!/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.3.3+ci5"

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} step is executing ...${ENDCOLOR}"
}

# Display the current executing function
current_function() {
    if [ "${VERBOSITY_LEVEL}" -ge 2 ]; then
        echo -e "=====> ${CYAN}${FUNCNAME[1]}${ENDCOLOR} function is executing ..."
    fi
}

# 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
        # Store script path and arguments for re-execution
        local SCRIPT_PATH="${SCRIPT_PATH:-$0}"
        local SCRIPT_ARGS="${SCRIPT_ARGS:-}"

        # Try pkexec first if GUI is available
        if [ -n "${DISPLAY:-}" ]; then
            echo -e "${BOLD}${YELLOW}W:${ENDCOLOR} $(gettext 'Root privileges required. Requesting authorization...')" >&2
            if [ -n "${SCRIPT_ARGS}" ]; then
                exec pkexec env ORIGINAL_PWD="${ORIGINAL_PWD}" BUILD_DIR="${BUILD_DIR:-}" REMOVE_SOURCES="${REMOVE_SOURCES:-}" KEEP_CACHE="${KEEP_CACHE:-}" KEEP_ROOTFS="${KEEP_ROOTFS:-}" KEEP_IMAGE="${KEEP_IMAGE:-}" SHOW_SPINNER_OUTPUT="${SHOW_SPINNER_OUTPUT:-false}" VERBOSITY_LEVEL="${VERBOSITY_LEVEL:-1}" "${SCRIPT_PATH}" ${SCRIPT_ARGS}
            else
                exec pkexec env ORIGINAL_PWD="${ORIGINAL_PWD}" BUILD_DIR="${BUILD_DIR:-}" REMOVE_SOURCES="${REMOVE_SOURCES:-}" KEEP_CACHE="${KEEP_CACHE:-}" KEEP_ROOTFS="${KEEP_ROOTFS:-}" KEEP_IMAGE="${KEEP_IMAGE:-}" SHOW_SPINNER_OUTPUT="${SHOW_SPINNER_OUTPUT:-false}" VERBOSITY_LEVEL="${VERBOSITY_LEVEL:-1}" "${SCRIPT_PATH}"
            fi
        fi

        # Try sudo as fallback
        echo -e "${BOLD}${YELLOW}W:${ENDCOLOR} $(gettext 'Root privileges required. Attempting to use sudo...')" >&2
        if [ -n "${SCRIPT_ARGS}" ]; then
            exec sudo ORIGINAL_PWD="${ORIGINAL_PWD}" BUILD_DIR="${BUILD_DIR:-}" REMOVE_SOURCES="${REMOVE_SOURCES:-}" KEEP_CACHE="${KEEP_CACHE:-}" KEEP_ROOTFS="${KEEP_ROOTFS:-}" KEEP_IMAGE="${KEEP_IMAGE:-}" SHOW_SPINNER_OUTPUT="${SHOW_SPINNER_OUTPUT:-false}" VERBOSITY_LEVEL="${VERBOSITY_LEVEL:-1}" "${SCRIPT_PATH}" ${SCRIPT_ARGS}
        else
            exec sudo ORIGINAL_PWD="${ORIGINAL_PWD}" BUILD_DIR="${BUILD_DIR:-}" REMOVE_SOURCES="${REMOVE_SOURCES:-}" KEEP_CACHE="${KEEP_CACHE:-}" KEEP_ROOTFS="${KEEP_ROOTFS:-}" KEEP_IMAGE="${KEEP_IMAGE:-}" SHOW_SPINNER_OUTPUT="${SHOW_SPINNER_OUTPUT:-false}" VERBOSITY_LEVEL="${VERBOSITY_LEVEL:-1}" "${SCRIPT_PATH}"
        fi
    fi

    export HOME=/root
}

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

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

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

run_with_spinner() {
    local MSG="$1"
    shift
    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        "$@"
    else
        # Create temporary log file for interactive output
        local COMBINED_LOG="/tmp/astra_build_log_$$"

        # Run command with stdout and stderr redirected to log file
        "$@" >"${COMBINED_LOG}" 2>&1 &
        local CMD_PID="$!"

        local DELAY=0.1
        local SPINSTR='|/-\\'
        # Use environment variable for show output toggle (like VERBOSITY_LEVEL)
        # Initialize to false only if not already set
        : ${SHOW_SPINNER_OUTPUT:="false"}
        local TAIL_PID=""
        local LAST_LINE_COUNT=0
        local OUTPUT_LINES=0
        local OUTPUT_ANIM_COUNTER=0
        local LAST_REFRESH_TIME=$(date +%s)
        console_colors

        while kill -0 "${CMD_PID}" 2>/dev/null; do
            if [ "$SHOW_SPINNER_OUTPUT" = "false" ]; then
                for ((i = 0; i < ${#SPINSTR}; i++)); do
                    # Check if process is still running before showing spinner
                    if ! kill -0 "${CMD_PID}" 2>/dev/null; then
                        break 2
                    fi

                    printf "\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} [${CYAN}${SPINSTR:$i:1}${ENDCOLOR}] ${DIM}(press ${ENDCOLOR}${BOLD}R${ENDCOLOR}${DIM} to toggle output)${ENDCOLOR}" >&2

                    # Check for 'r' key with multiple attempts for better responsiveness
                    local KEY_PRESSED=false
                    for attempt in {1..3}; do
                        if read -t 0.05 -n 1 -s KEY 2>/dev/null && [[ "$KEY" == "r" || "$KEY" == "R" || "$KEY" == "к" || "$KEY" == "К" ]]; then
                            KEY_PRESSED=true
                            break
                        fi
                    done
                    if [ "$KEY_PRESSED" = "true" ]; then
                        SHOW_SPINNER_OUTPUT="true"
                        # Clear input buffer to prevent multiple rapid keypresses
                        while read -t 0.01 -n 1 -s 2>/dev/null; do :; done
                        printf "\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} ${DIM}(press ${ENDCOLOR}${BOLD}R${ENDCOLOR}${DIM} to toggle output)${ENDCOLOR}$(tput el)\n"
                        # Show separator line
                        printf "${DIM}────────────────────────────────────────────────────────────────────────────────${ENDCOLOR}\n"
                        # Always show exactly 10 lines, padding with empty lines if needed
                        local AVAILABLE_LINES=$(wc -l <"${COMBINED_LOG}" 2>/dev/null || echo 0)
                        local LINES_TO_SHOW=10
                        if [ "$AVAILABLE_LINES" -gt 0 ]; then
                            tail -n ${LINES_TO_SHOW} "${COMBINED_LOG}" 2>/dev/null
                            local ACTUAL_LINES=$(tail -n ${LINES_TO_SHOW} "${COMBINED_LOG}" 2>/dev/null | wc -l)
                            # Pad with empty lines if we have fewer than 10 lines
                            for ((i = ACTUAL_LINES; i < LINES_TO_SHOW; i++)); do
                                echo ""
                            done
                        else
                            # No content yet, show 10 empty lines
                            for ((i = 0; i < LINES_TO_SHOW; i++)); do
                                echo ""
                            done
                        fi
                        # Show bottom separator line
                        printf "${DIM}────────────────────────────────────────────────────────────────────────────────${ENDCOLOR}\n"
                        # Set current line count so we only show new lines from this point
                        LAST_LINE_COUNT=$(wc -l <"${COMBINED_LOG}" 2>/dev/null || echo 0)
                        OUTPUT_LINES=$((1 + 1 + 10 + 1)) # Count header + top separator + exactly 10 lines + bottom separator
                        break
                    fi
                done
            else
                # In output mode - maintain sliding window of exactly 10 lines
                local CURRENT_LINE_COUNT=$(wc -l <"${COMBINED_LOG}" 2>/dev/null || echo 0)
                local CURRENT_TIME=$(date +%s)
                local TIME_SINCE_REFRESH=$((CURRENT_TIME - LAST_REFRESH_TIME))

                # Update if new lines appeared OR if 10 seconds passed (for progress updates like mksquashfs)
                if [ "$CURRENT_LINE_COUNT" -gt "$LAST_LINE_COUNT" ] || [ "$TIME_SINCE_REFRESH" -ge 10 ]; then
                    # Always show exactly 10 lines, replacing the entire output area
                    local MAX_LINES=10

                    # Move to start of output area (after header)
                    if [ "$OUTPUT_LINES" -gt 1 ]; then
                        printf "\033[$((OUTPUT_LINES - 1))A"
                    fi

                    # Clear the output area and show latest 10 lines with separators
                    printf "$(tput ed)"
                    printf "${DIM}────────────────────────────────────────────────────────────────────────────────${ENDCOLOR}\n"
                    # Always show exactly 10 lines, padding with empty lines if needed
                    local AVAILABLE_LINES=$(wc -l <"${COMBINED_LOG}" 2>/dev/null || echo 0)
                    if [ "$AVAILABLE_LINES" -gt 0 ]; then
                        tail -n ${MAX_LINES} "${COMBINED_LOG}" 2>/dev/null
                        local ACTUAL_LINES=$(tail -n ${MAX_LINES} "${COMBINED_LOG}" 2>/dev/null | wc -l)
                        # Pad with empty lines if we have fewer than 10 lines
                        for ((i = ACTUAL_LINES; i < MAX_LINES; i++)); do
                            echo ""
                        done
                    else
                        # No content yet, show 10 empty lines
                        for ((i = 0; i < MAX_LINES; i++)); do
                            echo ""
                        done
                    fi
                    printf "${DIM}────────────────────────────────────────────────────────────────────────────────${ENDCOLOR}\n"

                    LAST_LINE_COUNT="$CURRENT_LINE_COUNT"
                    LAST_REFRESH_TIME="$CURRENT_TIME"
                    OUTPUT_LINES=$((1 + 1 + 10 + 1)) # Header + top separator + exactly 10 lines + bottom separator
                fi

                # Animate the header line with spinner (same as normal mode but different color)
                OUTPUT_ANIM_COUNTER=$((OUTPUT_ANIM_COUNTER + 1))
                local SPINNER_CHAR=${SPINSTR:$((OUTPUT_ANIM_COUNTER % ${#SPINSTR})):1}

                # Update the first line with spinner animation
                if [ "$OUTPUT_LINES" -gt 0 ]; then
                    printf "\033[${OUTPUT_LINES}A\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} [${LIGHTGREEN}${SPINNER_CHAR}${ENDCOLOR}] ${DIM}(press ${ENDCOLOR}${BOLD}R${ENDCOLOR}${DIM} to toggle output)${ENDCOLOR}$(tput el)\033[${OUTPUT_LINES}B\r"
                fi

                # Check for 'r' to return to spinner with multiple attempts
                local KEY_PRESSED=false
                for attempt in {1..3}; do
                    if read -t 0.05 -n 1 -s KEY 2>/dev/null && [[ "$KEY" == "r" || "$KEY" == "R" || "$KEY" == "к" || "$KEY" == "К" ]]; then
                        KEY_PRESSED=true
                        break
                    fi
                done
                if [ "$KEY_PRESSED" = "true" ]; then
                    SHOW_SPINNER_OUTPUT="false"
                    # Clear input buffer to prevent multiple rapid keypresses
                    while read -t 0.01 -n 1 -s 2>/dev/null; do :; done
                    # Move cursor up to the first "Output mode" line and replace it
                    printf "\033[${OUTPUT_LINES}A\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} [${CYAN}${SPINSTR:0:1}${ENDCOLOR}] ${DIM}(press ${ENDCOLOR}${BOLD}R${ENDCOLOR}${DIM} to toggle output)${ENDCOLOR}$(tput el)$(tput ed)"
                    OUTPUT_LINES=0
                else
                    # Short sleep to prevent high CPU usage in output mode
                    sleep 0.1
                fi
            fi
        done

        # Clean up
        if [ -n "$TAIL_PID" ]; then
            kill "$TAIL_PID" 2>/dev/null
        fi

        wait "${CMD_PID}"
        local EXIT_CODE=$?

        rm -f "${COMBINED_LOG}"

        # Move to the header line and show [done], clearing everything below
        if [ "$SHOW_SPINNER_OUTPUT" = "true" ] && [ "$OUTPUT_LINES" -gt 0 ]; then
            printf "\033[${OUTPUT_LINES}A\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} [${GREEN}done${ENDCOLOR}]$(tput el)$(tput ed)\n" >&2
        else
            printf "\r${BOLD}${CYAN}I:${ENDCOLOR} ${MSG} [${GREEN}done${ENDCOLOR}]$(tput el)\n" >&2
        fi

        return $EXIT_CODE
    fi
}

################################################################################
# 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+=($!)
    set +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
    set -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() {
    set +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
    set +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"
    SOURCES_SRC_LIST="${SOURCES_DIR}/${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 "${SOURCES_SRC_LIST}" ]; then
            cp -f "${SOURCES_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

    # 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}"
}

# 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
        mkdir -p "/astra-live/packages"
        AVAILABLE_TASKS=($(tasksel --list | awk '{print $2}'))
        echo "
!linux-image-latest-generic -i=docker -i=wsl
!linux-astra-modules-common -i=docker -i=wsl +d=1.7_x86-64 +d=4.7_arm
systemd-sysv -i=docker
whiptail
console-setup -i=docker -i=wsl
keyboard-configuration -i=docker -i=wsl
!live-boot +i=iso
!live-config +i=iso
!live-config-systemd +i=iso
!user-setup +i=iso
# 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.
exim4-base==4.92-8+deb10u6.astra.se20 +d=1.7_x86-64 +d=4.7_arm
exim4-config==4.92-8+deb10u6.astra.se20 +d=1.7_x86-64 +d=4.7_arm
exim4-daemon-light==4.92-8+deb10u6.astra.se20 +d=1.7_x86-64 +d=4.7_arm
grub-efi +i=raw +i=qcow2 +i=vmdk
grub-pc-bin +i=raw +i=qcow2 +i=vmdk +a=amd64" >>"/astra-live/packages/packages.list"
        tasksel --task-packages hidden >>"/astra-live/packages/packages.list"
        for TASK in "${TASKS[@]}"; do
            if [[ "${AVAILABLE_TASKS[@]}" =~ "${TASK}" ]]; then
                tasksel --task-packages "$TASK" >>"/astra-live/packages/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 to unmount everything. !!!Not used!!!
unmount_all() {
    set +e
    for DIR in dev/pts dev/shm sys proc tmp var/cache/apt/archives; do
        umount "${INSTALL_DIR}/${DIR}" 2>/dev/null
        if [ $? -ne 0 ]; then
            # Check if the directory is still mounted.
            mountpoint -q "${INSTALL_DIR}/${DIR}"
            if [ $? -eq 0 ]; then
                error "Failed to unmount the directory from the chroot environment: ${DIR}"
                exit 1
            fi
        fi
    done
    for RBIND in "${RBINDS[@]}"; do
        umount ${RBIND} 2>/dev/null
        if [ $? -eq 0 ]; then rmdir ${RBIND}; fi
    done
    for PID in "${PIDS[@]}"; do kill -9 ${PID} 2>/dev/null; done
    set -e
}

# 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#*=}"
        # Use default value to avoid error if variable is unset
        if [ -z "${!KEY:-}" ]; then
            if [ "$KEY" == "$VALUE" ]; then
                eval "$KEY=''" # Set empty string if no value provided
            else
                eval "$KEY='$VALUE'" # Assign provided 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 "${SOURCES_DIR}/${DISTRIBUTION}.list" ]; then
        BUILTIN_REPO_LIST="${SOURCES_DIR}/${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
                # Automatically use system sources without prompting
                USE_SYSTEM_SOURCES=true
                information "Using repositories from /etc/apt/sources.list (no built-in list for ${DISTRIBUTION})"
            else
                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
}

# 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 [ "${CONVERT_TO_LIVE}" = "true" ] && [ -z "${DISTRIBUTION}" ] && [ -f /etc/astra_version ] && read -r LINE </etc/astra_version; then
        DISTRIBUTION=$(awk -F. '{print $1"."$2}' <<<"$LINE")
    elif [ -z "${DISTRIBUTION}" ] && [ -f /etc/astra_version ] && read -r LINE </etc/astra_version; then
        # Automatically use distribution from /etc/astra_version without prompting
        DISTRIBUTION=$(awk -F. '{print $1"."$2}' <<<"$LINE")
        if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
            information "Using distribution ${DISTRIBUTION} from /etc/astra_version"
        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}"

    set +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)
            if ! ((${#GENERATE_IMAGES[@]})); then
                GENERATE_IMAGES+=("iso")
            fi
            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
    set -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

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

# Unmount all file systems under a given directory
# Parameters:
#   $1 - Absolute or relative path to the directory
unmount_dirs() {
    # Get the directory path from argument and normalize to absolute
    local DIR_PATH="$(realpath -m "$1")"
    local MOUNTS
    local FAILED=0

    # 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 |
            # Sort by path length descending to unmount deepest first
            awk '{ print length, $0 }' |
            sort -rn |
            cut -d' ' -f2-
    )

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

    local MAX_PASSES=3
    local PASS=1
    local TO_UNMOUNT=("${MOUNTS[@]}")

    while [ "$PASS" -le "$MAX_PASSES" ] && [ "${#TO_UNMOUNT[@]}" -gt 0 ]; do
        if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
            information "Pass $PASS: attempting to unmount ${#TO_UNMOUNT[@]} point(s)..."
        fi
        local NEXT=()
        for MOUNT in "${TO_UNMOUNT[@]}"; do
            if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
                information "Processing ${MOUNT} (pass $PASS)"
            fi

            # Check if mount point is still mounted before attempting to unmount
            if findmnt "${MOUNT}" >/dev/null 2>&1; then
                if umount "${MOUNT}" 2>/dev/null; then
                    if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
                        information "Successfully unmounted ${MOUNT}."
                    fi
                else
                    # Double-check if it's still mounted after failed umount
                    # (it might have been unmounted by another process between checks)
                    if findmnt "${MOUNT}" >/dev/null 2>&1; then
                        if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
                            warning "Could not unmount ${MOUNT} on pass $PASS; will retry."
                        fi
                        NEXT+=("${MOUNT}")
                    else
                        if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
                            information "Mount point ${MOUNT} was unmounted between checks, skipping."
                        fi
                    fi
                fi
            else
                if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
                    information "Mount point ${MOUNT} is already unmounted, skipping."
                fi
            fi
        done
        TO_UNMOUNT=("${NEXT[@]}")
        ((PASS++))
    done

    if [ "${#TO_UNMOUNT[@]}" -gt 0 ]; then
        error "Failed to unmount ${#TO_UNMOUNT[@]} point(s) after $MAX_PASSES passes. Remaining:"
        for M in "${TO_UNMOUNT[@]}"; do
            warning "  * ${M}"
        done
        return 1
    fi

    if [ ${VERBOSITY_LEVEL:-1} -ge 2 ]; then
        information "All file systems under ${DIR_PATH} have been successfully unmounted."
    fi
    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"

    if [ "${CONVERT_TO_LIVE:=}" = "true" ]; then
        mount --bind /run "${DIR_PATH}/run"
        mount --bind /etc/resolv.conf "${DIR_PATH}/etc/resolv.conf"
        mount --bind /etc/hosts "${DIR_PATH}/etc/hosts"
    else
        mkdir -p "${BUILD_DIR}/aptcache/${DISTRIBUTION}"
        mount --bind "${BUILD_DIR}/aptcache/${DISTRIBUTION}" "${DIR_PATH}/var/cache/apt/archives"
    fi
}

# 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_ROOTFS}" == "true" ]]; then
        CONDITIONS+=(-path "${DIR}/rootfs" -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")

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

    return 0
}

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

    set +eu

    rm -rf $1/astra-live 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

    set -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}/astra-live
${DIR_PATH}/etc/apt/sources.list~
${DIR_PATH}/etc/systemd/system/timers.target.wants
${DIR_PATH}/install
${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
    set +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
    set -e

    mkdir -p ${BUILD_DIR}/image/live
    if [ $COMP_TYPE = "zstd" ]; then
        run_with_spinner "Compressing filesystem with ${COMP_TYPE}" mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -Xcompression-level 19 -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE}
    elif [ $COMP_TYPE = "xz" ]; then
        run_with_spinner "Compressing filesystem with ${COMP_TYPE}" mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -Xbcj x86 -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE}
    else
        run_with_spinner "Compressing filesystem with ${COMP_TYPE}" mksquashfs ${DIR_PATH} ${BUILD_DIR}/image/live/filesystem.squashfs -comp ${COMP_TYPE} -b 1024K -always-use-fragments -noappend -ef ${EXCLUDE_FILE}
    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_x86-64" ]; 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

    NOAUTOLOGIN=""
    if [ "${AUTOLOGIN}" != "true" ]; then
        NOAUTOLOGIN=" noautologin nox11autologin"
    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}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}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/packages.list" ]; then
        for DIR in "${SCRIPT_DIR}" "/usr/share/astra-live"; do
            if [ -f "${DIR}/packages/packages.list" ]; then
                cp "${DIR}/packages/packages.list" "${BUILD_DIR}/packages/packages.list" || error "Failed to copy the reference file to ${BUILD_DIR}/packages/packages.list."
                break
            fi
        done
    fi
    grant_write_permissions "${BUILD_DIR}/packages/packages.list"
}

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}"
            if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
                warning "Permissions for the file ${FILE} have been granted to the '${GROUP}' group."
            fi
        fi
    else
        if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
            warning "The group '${GROUP}' does not exist. No operations were performed on the file ${FILE}."
        fi
    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}"
    # Copy packages files from source location
    if [ -d "${PACKAGES_DIR}" ]; then
        cp -r "${PACKAGES_DIR}/"* "${BUILD_DIR}/packages/"
    fi

    set +u
    handle_repositories
    set -u
    set_packages_list
}

extract_tarball() {
    local TARBALL="${1}"
    local DESTINATION="${2}"
    if [ ! -f "${TARBALL}" ]; then
        error "File ${TARBALL} not found!"
        exit 1
    fi
    mkdir -p "${DESTINATION}"

    run_with_spinner "Extracting tarball ${TARBALL}" tar -xzf "${TARBALL}" -C "${DESTINATION}"

    if [ $? -ne 0 ]; then
        error "Error extracting tarball ${TARBALL}!"
        exit 1
    fi
}

# Bootstrap и настройка Debian с применением spinner для долгих операций
build_bootstrap() {
    current_process

    local INCLUDE_PACKAGES INCLUDES PACKAGE
    local ROOTFS_TARBALL="${BUILD_DIR}/rootfs/${DISTRIBUTION}-rootfs.tar.gz"

    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        information "$(gettext "Preparing package list for inclusion...")"
    fi
    INCLUDE_PACKAGES=("apt-transport-https" "ca-certificates" "tasksel")

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

    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        information "$(gettext "Creating apt cache directory and binding mount...")"
    fi
    mkdir -p "${BUILD_DIR}/aptcache/${DISTRIBUTION}"
    mount --bind "${BUILD_DIR}/aptcache/${DISTRIBUTION}" "${INSTALL_DIR}/var/cache/apt/archives"

    if [ -f "${ROOTFS_TARBALL}" ]; then
        if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
            information "$(gettext "Found rootfs tarball") (${ROOTFS_TARBALL}). $(gettext "Extracting...")"
        fi
        extract_tarball "${ROOTFS_TARBALL}" "${INSTALL_DIR}"
        if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
            information "$(gettext "Extraction completed. Bootstrapping finished.")"
        fi
        return 0
    fi

    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        information "$(gettext "Checking for debootstrap script for distribution") ${DISTRIBUTION}..."
    fi
    if [ ! -e "/usr/share/debootstrap/scripts/${DISTRIBUTION}" ]; then
        warning "$(gettext "Script for") ${DISTRIBUTION} $(gettext "not found. Creating symlink to 'sid'.")"
        ln -nfs sid "/usr/share/debootstrap/scripts/${DISTRIBUTION}"
    fi

    run_with_spinner "Running debootstrap for ${DISTRIBUTION}" \
        debootstrap \
        --no-check-gpg \
        --arch="${ARCHITECTURE}" \
        --variant=minbase \
        --components=main,contrib,non-free \
        ${INCLUDES} \
        "${DISTRIBUTION}" \
        "${INSTALL_DIR}" \
        "${MIRROR_URL}"

    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        information "$(gettext "debootstrap completed. Unmounting directories...")"
    fi
    unmount_dirs "${INSTALL_DIR}"

    mkdir -p "${BUILD_DIR}/rootfs"
    run_with_spinner "Creating rootfs tarball" tar -czvf "${ROOTFS_TARBALL}" -C "${INSTALL_DIR}" .

    if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
        information "$(gettext "Rootfs tarball") ${ROOTFS_TARBALL} $(gettext "created successfully. Bootstrapping complete.")"
    fi
}

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

    local HOOK_SUBDIR
    if [ -d "${HOOKS_DIR}" ]; then
        HOOK_SUBDIR="${HOOKS_DIR}/${1}"
    else
        return
    fi

    declare -A HOOKS
    local SUBDIR
    local FILE
    for SUBDIR in "${HOOK_SUBDIR}"/*; 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]}"
        if [ "${VERBOSITY_LEVEL}" -ge 1 ]; then
            echo -e "${BLUE}=====> running ${CYAN}${KEY}${ENDCOLOR}${BLUE} ...${ENDCOLOR}"
        fi
        chroot "${2}" bash -c "$(cat "${FILE}")" >/dev/null
    done
}

create_union() {
    current_function

    local DIR_PATH="$1"
    local LOWER_DIR="${2:-${BUILD_DIR}/chroot}"

    mkdir -p "${DIR_PATH}"/{upper,work,merged}
    mount -t overlay overlay -o lowerdir="${LOWER_DIR}",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
    # Create astra-live directory structure
    mkdir -p "${INSTALL_DIR}/astra-live"
    mkdir -p "${INSTALL_DIR}/astra-live/packages"

    # Copy the configuration file.
    cp -f "${BUILD_DIR}/config" "${INSTALL_DIR}/astra-live/config"
    # Copy the main library.
    if [ -f "${SCRIPT_DIR}/libastralive" ]; then
        cp -f "${SCRIPT_DIR}/libastralive" "${INSTALL_DIR}/astra-live/libastralive"
    elif [ -f "/usr/lib/astra-live/libastralive" ]; then
        cp -f "/usr/lib/astra-live/libastralive" "${INSTALL_DIR}/astra-live/libastralive"
    else
        error "No libastralive library found."
    fi
    # Copy the scripts folder.
    if [ -d "${SCRIPTS_DIR}" ]; then
        cp -rf "${SCRIPTS_DIR}" "${INSTALL_DIR}/astra-live/scripts"
    else
        error "No scripts folder found."
    fi

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

    if [[ " ${GENERATE_IMAGES[@]} " =~ " iso " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " raw " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " qcow2 " ]] || [[ " ${GENERATE_IMAGES[@]} " =~ " vmdk " ]]; then
        if [ ! -f "${BUILD_DIR}/packages/packages.list" ]; then
            if [ -f "${PACKAGES_DIR}/packages.list" ]; then
                cp "${PACKAGES_DIR}/packages.list" "${BUILD_DIR}/packages/packages.list"
            fi
        fi
    fi

    set +eu
    if [ -n "${REPLACE_PACKAGES[0]}" ] || [ -n "${REPLACE_PACKAGES_LIST}" ]; then
        echo "" >"${BUILD_DIR}/packages/packages.list"
        if [ -n "${REPLACE_PACKAGES[0]}" ]; then
            for PACKAGE in "${REPLACE_PACKAGES[@]}"; do
                echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
            done
        fi
        if [ -n "${REPLACE_PACKAGES_LIST}" ]; then
            REPLACE_PACKAGES_LIST=$(realpath "${REPLACE_PACKAGES_LIST}")
            cat "${REPLACE_PACKAGES_LIST}" >>"${BUILD_DIR}/packages/packages.list"
        fi
    else
        local NEW_LINE_ADDED=0
        if [ -n "${ADD_PACKAGES[0]}" ]; then
            LAST_LINE=$(tail -n 1 "${BUILD_DIR}/packages/packages.list")
            if [ -n "${LAST_LINE}" ]; then
                echo -e "\n" >>"${BUILD_DIR}/packages/packages.list"
                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/packages.list"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                        fi
                    elif [ -f "${PACKAGE}" ]; then
                        PACKAGE=$(realpath "${PACKAGE}")
                        if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/packages.list"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                        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/packages.list"; then
                                echo "${DEB_PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                            fi
                        done
                    else
                        if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/packages.list"; then
                            echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                        fi
                    fi
                else
                    if ! grep -Fxq "${PACKAGE}" "${BUILD_DIR}/packages/packages.list"; then
                        echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                    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/packages.list")
                if [ -n "${LAST_LINE}" ]; then
                    echo -e "\n" >>"${BUILD_DIR}/packages/packages.list"
                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/packages.list"; then
                    echo "${PACKAGE}" >>"${BUILD_DIR}/packages/packages.list"
                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/packages.list"
            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/packages.list"
            done <"${TEMP_DELETE_FILE}"
            rm ${TEMP_DELETE_FILE}
        fi
    fi
    set -eu

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

    # Make all files in the scripts folder executable.
    if [ -d "${INSTALL_DIR}/astra-live/scripts" ]; then
        chmod -R +x "${INSTALL_DIR}/astra-live/scripts"
    else
        error "The scripts folder does not exist in ${INSTALL_DIR}/astra-live."
    fi

    chroot "${INSTALL_DIR}" /astra-live/scripts/chroot-install

    # 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 "${SOURCES_DIR}/${DISTRIBUTION}.list" ]; then
            cp -f "${SOURCES_DIR}/${DISTRIBUTION}.list" "${INSTALL_DIR}/etc/apt/sources.list" 2>/dev/null
        fi
    fi

    handle_rootcopy

    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"
            set +e
            run_hooks "${IMAGE_TYPE}" "${WORK_DIR}"
            set -e
            unmount_dirs "${BUILD_DIR}"
        fi
    done
}

handle_rootcopy() {
    current_function

    if [ ! -d "${ROOTCOPY_DIR}" ]; then
        error "Rootcopy directory not found at ${ROOTCOPY_DIR}"
        exit 1
    fi

    # Copy common files
    if [ -d "${ROOTCOPY_DIR}/common" ]; then
        run_with_spinner "Copying common files from rootcopy" cp -a "${ROOTCOPY_DIR}/common/." "${INSTALL_DIR}/"
    fi

    # Copy image-specific files
    for IMAGE_TYPE in "${GENERATE_IMAGES[@]}"; do
        if [ -d "${ROOTCOPY_DIR}/${IMAGE_TYPE}" ]; then
            run_with_spinner "Copying ${IMAGE_TYPE}-specific files from rootcopy" cp -a "${ROOTCOPY_DIR}/${IMAGE_TYPE}/." "${INSTALL_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
    sleep 300
    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
            run_with_spinner "Creating ISO image ${ISO_NAME}" \
                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
            run_with_spinner "Creating ISO image ${ISO_NAME}" \
                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
            error "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
            error "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=""
    set +u
    for IMAGE in "${IMAGES[@]}"; do
        if [[ -n ${!IMAGE} ]] && [[ -z $IMAGE_NAME ]]; then
            IMAGE_NAME=${!IMAGE}
            if [ -f "${BUILD_DIR}/${IMAGE_NAME}" ]; then
                information ">>>>>> image ${BUILD_DIR}/${IMAGE_NAME} has been created."
            fi
            break
        fi
    done
    set -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
            information ">>>>>> image ${BUILD_DIR}/${TAR_NAME} has been created."
        fi
    fi
}
