#!/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.4.0.1"

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" 2>/dev/null || true) | 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() {
    # Skip root check in test mode
    if [[ "${LIVE_BUILD_TEST_MODE:-}" == "true" ]]; then
        return 0
    fi
    
    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
    local IS_BUILTIN_SOURCE="false"
    if [ ! -f "${LOCAL_SRC_LIST}" ]; then
        if [ -f "${SOURCES_SRC_LIST}" ]; then
            cp -f "${SOURCES_SRC_LIST}" "${LOCAL_SRC_LIST}"
            IS_BUILTIN_SOURCE="true"
        else
            error "The local repository list ${LOCAL_SRC_LIST} is missing."
            exit 1
        fi
    fi

    validate_sources "${LOCAL_SRC_LIST}" "${IS_BUILTIN_SOURCE}"

    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
        # Handle optional [options] field: deb [options] URL distro components...
        if [[ "${LINE}" =~ ^deb[[:space:]]+\[.*\][[:space:]]+([^[:space:]]+) ]]; then
            MIRROR_URL="${BASH_REMATCH[1]}"
        else
            MIRROR_URL=$(echo "${LINE}" | cut -d ' ' -f 2)
        fi
        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
# Arguments:
#   $1 - path to sources list file
#   $2 - "true" if this is a built-in source list, "false" otherwise (optional, defaults to "false")
validate_sources() {
    local IS_BUILTIN="${2:-false}"

    # 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, handling optional [options] field
                # Format: deb [options] URL distro components...
                local URL DISTRO
                if [[ "${LINE}" =~ ^deb[[:space:]]+\[.*\][[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+) ]]; then
                    # Line has [options] field
                    URL="${BASH_REMATCH[1]}"
                    DISTRO="${BASH_REMATCH[2]}"
                else
                    # Standard format without [options]
                    URL=$(echo "${LINE}" | cut -d ' ' -f 2)
                    DISTRO=$(echo "${LINE}" | cut -d ' ' -f 3)
                fi

                # Check if the "Release" file exists in the repository
                if ! curl --output /dev/null --silent --head --fail "${URL}/dists/${DISTRO}/Release"; then
                    if [ "${IS_BUILTIN}" = "true" ]; then
                        # For built-in sources, show a warning after 
                        error "Release file does not exist in repository: ${CYAN}${URL}/dists/${DISTRO}${ENDCOLOR}"
                        warning "Possible reasons: repository does not exist yet, has been moved, or is temporarily unavailable."
                        warning "Please verify repository accessibility manually or specify a custom repository using the ${CYAN}-r${ENDCOLOR} option."
                        ERRORS=$((ERRORS + 1))
                    else
                        # For user-provided sources, report an error
                        error "Release file does not exist in repository: ${CYAN}${URL}/dists/${DISTRO}${ENDCOLOR}"
                        ERRORS=$((ERRORS + 1))
                    fi
                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}"
        local EXIT_CODE=$?

        # 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 ${EXIT_CODE}
    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" "ISO_VOLUME_ID" "ISO_GRUB_ENTRY" "ISO_DISK_INFO" "ISO_MENU" "ISO_LAUNCHER" "ISO_LAUNCHER_RAM" "ISO_INCLUDE_REPO" "REPO_DOWNLOAD_THREADS"
        "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"
        [ISO_VOLUME_ID]="--iso-volume-id"
        [ISO_GRUB_ENTRY]="--iso-grub-entry"
        [ISO_DISK_INFO]="--iso-disk-info"
        [ISO_MENU]="--iso-menu"
        [ISO_LAUNCHER]="--iso-launcher"
        [ISO_LAUNCHER_RAM]="--iso-launcher-ram"
        [ISO_INCLUDE_REPO]="--iso-include-repo"
        [ISO_REPOS]="--iso-repos"
        [REPO_DOWNLOAD_THREADS]="--repo-download-threads"
        [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
            ;;
        ISO_VOLUME_ID)
            MAX_LENGTH=32
            check_length
            check_format
            ;;
        ISO_GRUB_ENTRY)
            MAX_LENGTH=64
            check_length
            ;;
        ISO_DISK_INFO)
            MAX_LENGTH=64
            check_length
            ;;
        ISO_MENU)
            check_not_empty
            VALID_VALUES=("themed" "hidden")
            check_preset_values
            ;;
        ISO_LAUNCHER)
            check_not_empty
            VALID_VALUES=("off" "auto" "force")
            check_preset_values
            ;;
        ISO_LAUNCHER_RAM)
            check_not_empty
            if ! [[ "${VALUE}" =~ ^[0-9]+$ ]]; then
                get_executable_label
                error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR}. The value must be a positive integer in MB."
                ERRORS=$((ERRORS + 1))
            elif [ "${VALUE}" -lt 256 ] || [ "${VALUE}" -gt 65536 ]; then
                get_executable_label
                error "Invalid ${CYAN}${KEY_LABEL}${ENDCOLOR} specified: ${CYAN}${VALUE}${ENDCOLOR}. Allowed range is 256..65536 MB."
                ERRORS=$((ERRORS + 1))
            fi
            ;;
        ISO_INCLUDE_REPO)
            VALID_VALUES=("true" "false")
            check_preset_values
            ;;
        REPO_DOWNLOAD_THREADS)
            if ! [[ "${VALUE}" =~ ^[0-9]+$ ]]; then
                error "${VALUE} must be a positive integer."
                ERRORS=$((ERRORS + 1))
            elif [ "${VALUE}" -lt 1 ] || [ "${VALUE}" -gt 32 ]; then
                error "${VALUE} must be between 1 and 32."
                ERRORS=$((ERRORS + 1))
            fi
            ;;
        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 wget 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 essential filesystems
    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"

    # Mount /run for systemd/systemctl support (needed for packages that query systemctl)
    mkdir -p "${DIR_PATH}/run"
    mount --bind /run "${DIR_PATH}/run"

    # Mount parsecfs if available
    if mountpoint -q /parsecfs 2>/dev/null; then
        mkdir -p "${DIR_PATH}/parsecfs"
        mount --bind /parsecfs "${DIR_PATH}/parsecfs"
    fi

    if [ "${CONVERT_TO_LIVE:=}" = "true" ]; then
        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"
    
    # Get Astra version if available
    local ASTRA_VERSION="1.4.0.1"
    if [[ -f "${INSTALL_DIR}/etc/astra/build_version" ]]; then
        ASTRA_VERSION=$(cat "${INSTALL_DIR}/etc/astra/build_version")
    fi
    
    # Generate .disk/info in the same format as official ISO
    if [[ -n "${ASTRA_VERSION}" ]]; then
        echo "OS Astra Linux ${ASTRA_VERSION} ${DISTRIBUTION} DVD" > "${BUILD_DIR}/image/.disk/info"
    else
        echo "${ISO_DISK_INFO:-Astra Linux}" > "${BUILD_DIR}/image/.disk/info"
    fi
}

# 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 keystatus echo all_video disk ntfs ext2" \
            --modules="linux normal iso9660 biosdisk search keystatus" \
            --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

    NOAUTOLOGIN=""
    if [ "${AUTOLOGIN}" != "true" ]; then
        NOAUTOLOGIN=" noautologin nox11autologin"
    fi

    local MENU_STYLE="${ISO_MENU:-themed}"

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

# Hide menu and boot quickly
set timeout=3
set timeout_style=hidden

menuentry "${ISO_GRUB_ENTRY:-Astra Linux}" {
    linux /live/vmlinuz boot=live${NOAUTOLOGIN} net.ifnames=0 parsec.mac=0 parsec.max_ilev=0 quiet splash
    initrd /live/initrd.img
}

menuentry "${ISO_GRUB_ENTRY:-Astra Linux} (recovery mode)" {
    linux /live/vmlinuz boot=live${NOAUTOLOGIN} net.ifnames=0 parsec.mac=0 parsec.max_ilev=0 quiet splash ro recovery
    initrd /live/initrd.img
}
EOF
        return
    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

    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 "${ISO_GRUB_ENTRY:-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 "${ISO_GRUB_ENTRY:-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}"

    local DEBOOTSTRAP_EXIT_CODE=$?
    if [ $DEBOOTSTRAP_EXIT_CODE -ne 0 ]; then
        error "$(gettext "debootstrap failed with exit code") ${DEBOOTSTRAP_EXIT_CODE}. $(gettext "Please check the error messages above.")"
        unmount_dirs "${INSTALL_DIR}"
        exit $DEBOOTSTRAP_EXIT_CODE
    fi

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

    mkdir -p "${BUILD_DIR}/rootfs"
    
    # Use verbose flag only for VERBOSITY_LEVEL >= 2
    if [ "${VERBOSITY_LEVEL}" -ge 2 ]; then
        run_with_spinner "Creating rootfs tarball" tar -czvf "${ROOTFS_TARBALL}" -C "${INSTALL_DIR}" .
    else
        run_with_spinner "Creating rootfs tarball" tar -czf "${ROOTFS_TARBALL}" -C "${INSTALL_DIR}" .
    fi

    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
            
            # Create marker file if this is an installer image (has repository)
            if [[ "${IMAGE_TYPE}" == "iso" ]] && ([[ "${ISO_INCLUDE_REPO}" == "true" ]] || [[ ${#ISO_REPOS[@]} -gt 0 ]]); then
                mkdir -p "${WORK_DIR}/etc/astra-live"
                touch "${WORK_DIR}/etc/astra-live/installer-image"
            fi
            
            #mount_system_dirs "${BUILD_DIR}/union/${IMAGE_TYPE}/merged"
            set +e
            run_hooks "${IMAGE_TYPE}" "${WORK_DIR}"
            set -e
            
            # Remove marker file after hooks
            rm -f "${WORK_DIR}/etc/astra-live/installer-image"
            
            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
}

# Determine repository source type from URL/path
# Returns: http, rsync, or local
_get_repo_source_type() {
    local SOURCE="$1"
    
    if [[ "$SOURCE" =~ ^https?:// ]]; then
        echo "http"
    elif [[ "$SOURCE" =~ ^rsync:// ]]; then
        echo "rsync"
    elif [[ "$SOURCE" =~ ^file:// ]] || [[ "$SOURCE" =~ ^/ ]]; then
        echo "local"
    else
        error "Unsupported repository source format: $SOURCE"
        error "Supported formats: http://, https://, rsync://, file://, /absolute/path"
        return 1
    fi
}

# Check availability of download tools and return the best one for given source type
# Returns: apt-mirror2, rsync, or cp
_check_download_tools() {
    local SOURCE_TYPE="$1"
    
    case "$SOURCE_TYPE" in
        http)
            # apt-mirror2 is the only supported HTTP/HTTPS download method
            if command -v apt-mirror &>/dev/null; then
                echo "apt-mirror2"
            else
                error "apt-mirror (apt-mirror2) not found for HTTP/HTTPS downloads"
                error "Please install apt-mirror2: apt install apt-mirror2"
                return 1
            fi
            ;;
        rsync)
            if command -v rsync &>/dev/null; then
                echo "rsync"
            else
                error "rsync not found for rsync:// downloads"
                error "Please install rsync package"
                return 1
            fi
            ;;
        local)
            echo "cp"  # cp is always available
            ;;
        *)
            error "Unknown source type: $SOURCE_TYPE"
            return 1
            ;;
    esac
}

# Decode URL-encoded filenames and directory names (e.g., %2B -> +)
# Must process from deepest to shallowest (bottom-up) to avoid breaking paths
_decode_url_filenames() {
    local DIR="$1"
    
    # Check if there are any files with % in the name
    if ! find "${DIR}" -name '*%*' -print -quit 2>/dev/null | grep -q .; then
        return 0
    fi
    
    information "Decoding URL-encoded filenames in ${DIR}..."
    
    while IFS= read -r encoded_path; do
        # Get the directory and base name
        local dir_name=$(dirname "$encoded_path")
        local base_name=$(basename "$encoded_path")
        
        # Decode only the base name using URL decoding
        local decoded_base
        decoded_base=$(printf '%b' "$(printf '%s' "$base_name" | sed 's/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g')")
        local decoded_path="${dir_name}/${decoded_base}"
        
        if [[ "$encoded_path" != "$decoded_path" ]]; then
            mv "$encoded_path" "$decoded_path" 2>/dev/null || warning "Failed to rename: $encoded_path"
        fi
    done < <(find "${DIR}" -depth -name '*%*' 2>/dev/null)
}

# Download repository files using apt-mirror2 (modern asyncio-based APT mirror tool)
# This is the preferred method for HTTP/HTTPS repositories with incremental updates
# NOTE: apt-mirror2 downloads BOTH dists/ and pool/ in a single run, so we need to track
#       which repositories were already mirrored to avoid duplicate downloads
_download_with_apt_mirror2() {
    local REMOTE_DIR=$1
    local LOCAL_DIR=$2
    local REPO_URL=$3
    
    # Remove trailing slash
    REPO_URL="${REPO_URL%/}"
    
    # Create a marker file path to track if this repo was already mirrored
    local REPO_HASH=$(echo -n "$REPO_URL" | md5sum | cut -d' ' -f1)
    local MARKER_FILE="/tmp/apt-mirror2-${REPO_HASH}.done"
    
    # If this repository was already mirrored in this session, skip
    if [[ -f "$MARKER_FILE" ]]; then
        information "Repository ${REPO_URL} already mirrored (skipping ${REMOTE_DIR})"
        return 0
    fi
    
    information "Downloading repository from ${REPO_URL} using apt-mirror2..."
    
    # Create temporary mirror.list configuration
    local TEMP_CONFIG=$(mktemp)
    local TEMP_BASE=$(mktemp -d)
    local TEMP_VAR="${TEMP_BASE}/var"
    local TEMP_SKEL="${TEMP_BASE}/skel"
    
    # Ensure temporary directories exist
    mkdir -p "${TEMP_VAR}" "${TEMP_SKEL}"
    
    # Detect architecture (default to amd64 if not detected)
    local ARCH=$(dpkg --print-architecture 2>/dev/null || echo "amd64")
    
    # Parse distribution and components from REPO_URL
    # Expected format: https://host/path/to/repo/
    # We need to extract: suite and components
    # For Astra Linux: typically something like "1.8_x86-64 main contrib non-free"
    
    # Extract host and path from URL
    local URL_WITHOUT_PROTOCOL="${REPO_URL#*://}"
    local HOST="${URL_WITHOUT_PROTOCOL%%/*}"
    local PATH_PART="${URL_WITHOUT_PROTOCOL#*/}"
    
    # Try to determine the suite from the path or by checking dists/ directory
    local SUITE=""
    if [[ "$PATH_PART" =~ (1\.[0-9]+_[^/]+) ]]; then
        SUITE="${BASH_REMATCH[1]}"
    elif [[ "$PATH_PART" =~ /stable/?$ ]]; then
        SUITE="stable"
    elif [[ "$PATH_PART" =~ /testing/?$ ]]; then
        SUITE="testing"
    else
        # Try to auto-detect suite by listing dists/ directory
        local DISTS_HTML=$(wget -q -O - "${REPO_URL}/dists/" 2>/dev/null)
        if [[ -n "$DISTS_HTML" ]]; then
            # Look for directory links that match version patterns
            SUITE=$(echo "$DISTS_HTML" | grep -oP 'href="\K[^"]+' | grep -E '^[0-9]+\.[0-9]+_' | head -1 | sed 's|/$||')
        fi
        
        # Final fallback
        if [[ -z "$SUITE" ]]; then
            SUITE="stable"
            warning "Could not detect suite from URL, using default: ${SUITE}"
        fi
    fi
    
    information "Detected architecture: ${ARCH}, suite: ${SUITE}"
    
    # Generate mirror.list configuration
    cat > "${TEMP_CONFIG}" <<EOF
############# config ##################
# Global settings for apt-mirror2

# Base path for all mirror files
set base_path         ${TEMP_BASE}
set skel_path         ${TEMP_SKEL}
set var_path          ${TEMP_VAR}

# Number of parallel download threads
set nthreads          ${REPO_DOWNLOAD_THREADS:-8}

# Disable GPG verification (we trust the source)
set gpg_verify        off

# Use weak size check for faster incremental updates
set weak_size_check   1

# Disable file lists (we don't need them)
set write_file_lists  off

# Use atomic dists/ replacement
set use_dists_move    1

############# mirrors ##################
# Repository mirrors to download

# Specify custom mirror_path to avoid nested host/path structure
mirror_path ${REPO_URL} ${LOCAL_DIR}

# Disable HTTP/2 to avoid LocalProtocolError connection issues
# This forces HTTP/1.1 which is more stable for large file downloads
http2-disable ${REPO_URL}

# Download repository
deb-${ARCH} ${REPO_URL} ${SUITE} main contrib non-free non-free-firmware

############# end config ##############
EOF
    
    information "Generated apt-mirror2 configuration:"
    cat "${TEMP_CONFIG}" | grep -v "^#" | grep -v "^$"
    
    # Run apt-mirror (apt-mirror2)
    information "Starting apt-mirror download (this may take a while)..."
    local APT_MIRROR_EXIT_CODE=0
    
    apt-mirror "${TEMP_CONFIG}" 2>&1 || APT_MIRROR_EXIT_CODE=$?
    
    # Check exit code
    if [ $APT_MIRROR_EXIT_CODE -ne 0 ]; then
        error "apt-mirror failed with exit code: $APT_MIRROR_EXIT_CODE"
        rm -f "${TEMP_CONFIG}"
        rm -rf "${TEMP_BASE}"
        return 1
    fi
    
    information "apt-mirror download completed successfully"
    
    # Mark this repository as mirrored
    touch "$MARKER_FILE"
    
    # Cleanup temporary files
    rm -f "${TEMP_CONFIG}"
    rm -rf "${TEMP_BASE}"
    
    return 0
}

# Download repository files using wget (fallback when aria2c is not available)
_download_with_wget() {
    local REMOTE_DIR=$1
    local LOCAL_DIR=$2
    local REPO_URL=$3

    information "Downloading ${REMOTE_DIR} from ${REPO_URL} using wget..."
    
    # Calculate cut-dirs value based on URL structure
    # Example: https://releases.devos.astralinux.ru/frozen/1.8/latest/latest/installation/
    # We need to remove protocol://host and path prefix to get just dists/ or pool/
    # The path "frozen/1.8/latest/latest/installation" has 4 slashes but 5 segments
    # So we count slashes and add 1 to get the number of directory components to cut
    local URL_PATH="${REPO_URL#*://}"  # Remove protocol://
    URL_PATH="${URL_PATH#*/}"           # Remove host
    URL_PATH="${URL_PATH%/}"            # Remove trailing slash if present
    local SLASH_COUNT=$(echo "$URL_PATH" | tr -cd '/' | wc -c)
    local SECTION_COUNT=$((SLASH_COUNT + 1))
    
    # Create target directory
    mkdir -p "${LOCAL_DIR}/${REMOTE_DIR}"
    
    # Use wget with recursive download
    # --no-parent: don't ascend to parent directory
    # --cut-dirs: remove N directory components from saved path
    # -nH: don't create host directory
    # --reject-regex: skip Apache directory listing parameters
    # --reject: skip index files
    # -P: download prefix directory
    local WGET_EXIT_CODE=0
    wget -r -np -nH \
         --cut-dirs="${SECTION_COUNT}" \
         -e robots=off \
         --reject-regex '/\?C=[A-Z];O=[A-Z]$' \
         --reject "index.html*" \
         --no-verbose \
         --show-progress \
         -P "${LOCAL_DIR}" \
         "${REPO_URL}/${REMOTE_DIR}/" 2>&1 || WGET_EXIT_CODE=$?
    
    if [ $WGET_EXIT_CODE -ne 0 ] && [ $WGET_EXIT_CODE -ne 8 ]; then
        # Exit code 8 is "server error" which can happen for missing files, continue anyway
        error "Error downloading repository files (wget exit code: $WGET_EXIT_CODE)"
        return 1
    fi
    
    information "Download completed for ${REMOTE_DIR}"
    
    # Decode URL-encoded filenames
    _decode_url_filenames "${LOCAL_DIR}"
}

# Download repository files using rsync
_download_with_rsync() {
    local REMOTE_DIR=$1
    local LOCAL_DIR=$2
    local REPO_URL=$3

    information "Syncing ${REMOTE_DIR} from ${REPO_URL} using rsync..."
    
    # Ensure target directory exists
    mkdir -p "${LOCAL_DIR}/${REMOTE_DIR}"
    
    # Remove trailing slash from REPO_URL if present for consistent behavior
    REPO_URL="${REPO_URL%/}"
    
    # rsync parameters:
    # -a: archive mode (preserves permissions, timestamps, etc.)
    # -v: verbose
    # --progress: show progress
    # --exclude: skip temporary files
    local RSYNC_EXIT_CODE=0
    rsync -av --progress \
          --exclude='*.tmp' \
          --exclude='.~tmp~' \
          "${REPO_URL}/${REMOTE_DIR}/" \
          "${LOCAL_DIR}/${REMOTE_DIR}/" 2>&1 || RSYNC_EXIT_CODE=$?
    
    if [ $RSYNC_EXIT_CODE -ne 0 ]; then
        error "Error syncing repository files (rsync exit code: $RSYNC_EXIT_CODE)"
        return 1
    fi
    
    information "Sync completed for ${REMOTE_DIR}"
}

# Copy repository files from local source (supports hardlinks when on same filesystem)
_copy_from_local() {
    local REMOTE_DIR=$1
    local LOCAL_DIR=$2
    local SOURCE_PATH=$3
    
    # Remove file:// prefix if present
    SOURCE_PATH="${SOURCE_PATH#file://}"
    
    # Ensure source path ends without slash for consistent behavior
    SOURCE_PATH="${SOURCE_PATH%/}"
    
    information "Copying ${REMOTE_DIR} from ${SOURCE_PATH}..."
    
    # Check if source directory exists
    if [[ ! -d "${SOURCE_PATH}/${REMOTE_DIR}" ]]; then
        warning "Directory ${SOURCE_PATH}/${REMOTE_DIR} not found, skipping"
        return 0
    fi
    
    # Ensure target directory exists
    mkdir -p "${LOCAL_DIR}/${REMOTE_DIR}"
    
    # Check if source and destination are on the same filesystem (for hardlink support)
    local SOURCE_FS=$(stat -f -c '%i' "${SOURCE_PATH}" 2>/dev/null || stat -c '%d' "${SOURCE_PATH}" 2>/dev/null)
    local DEST_FS=$(stat -f -c '%i' "${LOCAL_DIR}" 2>/dev/null || stat -c '%d' "${LOCAL_DIR}" 2>/dev/null)
    
    local CP_EXIT_CODE=0
    if [[ "$SOURCE_FS" == "$DEST_FS" ]] && [[ -n "$SOURCE_FS" ]]; then
        information "Same filesystem detected, using hardlinks to save space..."
        # Use cp with hardlinks (-l) and archive mode (-a)
        cp -al "${SOURCE_PATH}/${REMOTE_DIR}/." "${LOCAL_DIR}/${REMOTE_DIR}/" 2>&1 || CP_EXIT_CODE=$?
        
        # If hardlink failed (e.g., cross-device), fall back to regular copy
        if [ $CP_EXIT_CODE -ne 0 ]; then
            warning "Hardlink failed, falling back to regular copy..."
            CP_EXIT_CODE=0
            cp -a "${SOURCE_PATH}/${REMOTE_DIR}/." "${LOCAL_DIR}/${REMOTE_DIR}/" 2>&1 || CP_EXIT_CODE=$?
        fi
    else
        # Different filesystems, use regular copy
        cp -a "${SOURCE_PATH}/${REMOTE_DIR}/." "${LOCAL_DIR}/${REMOTE_DIR}/" 2>&1 || CP_EXIT_CODE=$?
    fi
    
    if [ $CP_EXIT_CODE -ne 0 ]; then
        error "Error copying repository files (cp exit code: $CP_EXIT_CODE)"
        return 1
    fi
    
    information "Copy completed for ${REMOTE_DIR}"
}

# Main function for downloading repository files - routes to appropriate method
_download_repo_files() {
    local REMOTE_DIR=$1
    local LOCAL_DIR=$2
    local REPO_URL=$3
    
    # Determine source type and select appropriate download tool
    local SOURCE_TYPE
    SOURCE_TYPE=$(_get_repo_source_type "$REPO_URL") || return 1
    
    local DOWNLOAD_TOOL
    DOWNLOAD_TOOL=$(_check_download_tools "$SOURCE_TYPE") || return 1
    
    information "Repository source type: ${SOURCE_TYPE}, using: ${DOWNLOAD_TOOL}"
    
    case "$DOWNLOAD_TOOL" in
        apt-mirror2)
            _download_with_apt_mirror2 "$REMOTE_DIR" "$LOCAL_DIR" "$REPO_URL"
            ;;
        rsync)
            _download_with_rsync "$REMOTE_DIR" "$LOCAL_DIR" "$REPO_URL"
            ;;
        cp)
            _copy_from_local "$REMOTE_DIR" "$LOCAL_DIR" "$REPO_URL"
            ;;
        *)
            error "Unknown download tool: $DOWNLOAD_TOOL"
            return 1
            ;;
    esac
}

# Parse sources.list file to extract repository URLs
parse_sources_list() {
    local SOURCES_FILE=$1
    local REPOS=()
    
    if [[ ! -f "${SOURCES_FILE}" ]]; then
        return
    fi
    
    # Parse each line and extract URLs
    while IFS= read -r line; do
        # Skip comments and empty lines
        [[ "$line" =~ ^#.*$ ]] && continue
        [[ -z "$line" ]] && continue
        
        # Extract URL from "deb URL DIST COMPONENTS" format
        # Example: deb https://download.astralinux.ru/astra/stable/1.8_x86-64/repository-main/ 1.8_x86-64 main contrib
        if [[ "$line" =~ ^deb[[:space:]]+([^[:space:]]+) ]]; then
            local url="${BASH_REMATCH[1]}"
            # Remove trailing slash for consistency
            url="${url%/}"
            REPOS+=("$url")
        fi
    done < "${SOURCES_FILE}"
    
    # Return array as string (will be parsed by caller)
    printf '%s\n' "${REPOS[@]}"
}

download_repo_content() {
    current_function
    
    # Clean up old apt-mirror2 marker files from previous runs
    rm -f /tmp/apt-mirror2-*.done 2>/dev/null || true
    
    # Determine which repositories to download
    local REPOS_TO_DOWNLOAD=()
    
    # Priority 1: Explicit repositories specified via --iso-repos
    if [[ ${#ISO_REPOS[@]} -gt 0 ]]; then
        REPOS_TO_DOWNLOAD=("${ISO_REPOS[@]}")
        information "Using repositories from --iso-repos: ${ISO_REPOS[*]}"
        
    # Priority 2: All repositories from -r/REPO_LIST
    elif [[ "${ISO_INCLUDE_REPO}" == "true" ]] && [[ ${#REPO_LIST[@]} -gt 0 ]]; then
        REPOS_TO_DOWNLOAD=("${REPO_LIST[@]}")
        information "Using all repositories from -r: ${REPO_LIST[*]}"
        
    # Priority 3: Parse default sources.list file
    elif [[ "${ISO_INCLUDE_REPO}" == "true" ]]; then
        local SOURCES_FILE="${SCRIPT_DIR}/sources/${DISTRIBUTION}.list"
        if [[ -f "${SOURCES_FILE}" ]]; then
            information "Parsing default sources from ${SOURCES_FILE}..."
            local oldIFS="${IFS-}"
            IFS=$'\n'
            REPOS_TO_DOWNLOAD=($(parse_sources_list "${SOURCES_FILE}"))
            IFS="${oldIFS}"
            
            if [[ ${#REPOS_TO_DOWNLOAD[@]} -eq 0 ]]; then
                warning "No repositories found in ${SOURCES_FILE}"
                return 0
            fi
            information "Using repositories from sources.list: ${REPOS_TO_DOWNLOAD[*]}"
        else
            warning "ISO_INCLUDE_REPO is set but no repositories specified and ${SOURCES_FILE} not found"
            return 0
        fi
    else
        # No repository download requested
        return 0
    fi
    
    # Download all repositories
    for REPO_URL in "${REPOS_TO_DOWNLOAD[@]}"; do
        information "Downloading repository: ${REPO_URL}"
        _download_repo_files "dists" "${BUILD_DIR}/image" "${REPO_URL}"
        _download_repo_files "pool" "${BUILD_DIR}/image" "${REPO_URL}"
    done
    
    # Create stable symlink in dists directory (after download)
    if [[ -d "${BUILD_DIR}/image/dists" ]]; then
        pushd "${BUILD_DIR}/image/dists" > /dev/null
        if [[ ! -L "stable" ]] && [[ ! -e "stable" ]]; then
            ln -s "${DISTRIBUTION}" stable
            information "Created symlink: stable -> ${DISTRIBUTION}"
        fi
        popd > /dev/null
    fi
    
    # Download gostsums.txt from first repository (if available)
    if [[ ${#REPOS_TO_DOWNLOAD[@]} -gt 0 ]]; then
        local FIRST_REPO="${REPOS_TO_DOWNLOAD[0]}"
        pushd "${BUILD_DIR}/image" > /dev/null
        information "Downloading gostsums.txt from ${FIRST_REPO}..."
        # Use -O to force overwrite existing file (prevent .1, .2 duplicates)
        wget --quiet -O gostsums.txt "${FIRST_REPO}/gostsums.txt" 2>&1 || echo "" > gostsums.txt
        popd > /dev/null
    fi
}

# Generate repository indices using apt-ftparchive (Debian/Ubuntu way)
generate_repository_indices() {
    current_function
    
    # Only generate indices if repository was downloaded
    if [[ "${ISO_INCLUDE_REPO}" != "true" ]] && [[ ${#ISO_REPOS[@]} -eq 0 ]]; then
        return 0
    fi
    
    local IMAGE_DIR="${BUILD_DIR}/image"
    local DIST_DIR="${IMAGE_DIR}/dists/${DISTRIBUTION}"
    
    # Check if dists directory exists
    if [[ ! -d "${DIST_DIR}" ]]; then
        warning "Distribution directory ${DIST_DIR} not found, skipping index generation"
        return 0
    fi
    
    # If Release file already exists (downloaded from repository), use it as-is
    # Don't regenerate indices to avoid hash mismatch
    if [[ -f "${DIST_DIR}/Release" ]]; then
        information "Release file already exists, skipping index regeneration to preserve hashes"
        return 0
    fi
    
    information "Generating repository indices using apt-ftparchive..."
    
    # Create temporary config directory
    local CONFIG_DIR=$(mktemp -d)
    local ARCH="${ARCHITECTURE}"
    
    # Detect components from pool directory
    local COMPONENTS=()
    if [[ -d "${IMAGE_DIR}/pool/main" ]]; then
        COMPONENTS+=("main")
    fi
    if [[ -d "${IMAGE_DIR}/pool/contrib" ]]; then
        COMPONENTS+=("contrib")
    fi
    if [[ -d "${IMAGE_DIR}/pool/non-free" ]]; then
        COMPONENTS+=("non-free")
    fi
    if [[ -d "${IMAGE_DIR}/pool/non-free-firmware" ]]; then
        COMPONENTS+=("non-free-firmware")
    fi
    
    if [[ ${#COMPONENTS[@]} -eq 0 ]]; then
        warning "No components found in pool/, skipping index generation"
        rm -rf "${CONFIG_DIR}"
        return 0
    fi
    
    local COMPONENTS_STR=$(printf "%s " "${COMPONENTS[@]}")
    information "Found components: ${COMPONENTS_STR% }"
    
    # Read Astra version for Release file
    local ASTRA_VERSION="1.4.0.1"
    if [[ -f "${INSTALL_DIR}/etc/astra/build_version" ]]; then
        ASTRA_VERSION=$(cat "${INSTALL_DIR}/etc/astra/build_version")
    fi
    
    # Generate apt-ftparchive config for .deb packages
    cat > "${CONFIG_DIR}/apt-ftparchive-deb.conf" <<EOF
Dir {
  ArchiveDir "${IMAGE_DIR}";
};

TreeDefault {
  Directory "pool/";
};

EOF
    
    # Add BinDirectory sections for each component
    for COMPONENT in "${COMPONENTS[@]}"; do
        cat >> "${CONFIG_DIR}/apt-ftparchive-deb.conf" <<EOF
BinDirectory "pool/${COMPONENT}" {
  Packages "dists/${DISTRIBUTION}/${COMPONENT}/binary-${ARCH}/Packages";
};

EOF
    done
    
    # Add default section
    cat >> "${CONFIG_DIR}/apt-ftparchive-deb.conf" <<EOF
Default {
  Packages {
    Extensions ".deb";
    Compress ". gzip";
  };
};

Contents {
  Compress "gzip";
};
EOF
    
    # Generate apt-ftparchive config for .udeb packages (debian-installer)
    cat > "${CONFIG_DIR}/apt-ftparchive-udeb.conf" <<EOF
Dir {
  ArchiveDir "${IMAGE_DIR}";
};

TreeDefault {
  Directory "pool/";
};

EOF
    
    for COMPONENT in "${COMPONENTS[@]}"; do
        if [[ -d "${DIST_DIR}/${COMPONENT}/debian-installer" ]]; then
            cat >> "${CONFIG_DIR}/apt-ftparchive-udeb.conf" <<EOF
BinDirectory "pool/${COMPONENT}" {
  Packages "dists/${DISTRIBUTION}/${COMPONENT}/debian-installer/binary-${ARCH}/Packages";
};

EOF
        fi
    done
    
    cat >> "${CONFIG_DIR}/apt-ftparchive-udeb.conf" <<EOF
Default {
  Packages {
    Extensions ".udeb";
    Compress ". gzip";
  };
};

Contents {
  Compress "gzip";
};
EOF
    
    # Create Release config
    cat > "${CONFIG_DIR}/release.conf" <<EOF
APT::FTPArchive::Release::Label "main";
APT::FTPArchive::Release::Suite "stable";
APT::FTPArchive::Release::Codename "${DISTRIBUTION}";
EOF
    
    # Add Version field if available
    if [[ -n "${ASTRA_VERSION}" ]]; then
        cat >> "${CONFIG_DIR}/release.conf" <<EOF
APT::FTPArchive::Release::Version "${ASTRA_VERSION}";
EOF
    fi
    
    # Continue with remaining fields
    cat >> "${CONFIG_DIR}/release.conf" <<EOF
APT::FTPArchive::Release::Architectures "${ARCH}";
APT::FTPArchive::Release::Components "$(IFS=' '; echo "${COMPONENTS[*]}")";
APT::FTPArchive::Release::Description "AstraLinux-${ASTRA_VERSION} Repository main";
EOF
    
    # Generate Packages files
    information "Generating Packages files for .deb packages..."
    apt-ftparchive generate "${CONFIG_DIR}/apt-ftparchive-deb.conf" 2>&1 | grep -v "^W:" || true
    
    information "Generating Packages files for .udeb packages..."
    apt-ftparchive generate "${CONFIG_DIR}/apt-ftparchive-udeb.conf" 2>&1 | grep -v "^W:" || true
    
    # Generate Release file (generate outside DIST_DIR to avoid self-reference)
    information "Generating Release file..."
    rm -f "${DIST_DIR}/Release" "${DIST_DIR}/Release.new" "${DIST_DIR}/Release.tmp"
    apt-ftparchive -c "${CONFIG_DIR}/release.conf" release "${DIST_DIR}" > "${CONFIG_DIR}/Release.tmp"
    mv "${CONFIG_DIR}/Release.tmp" "${DIST_DIR}/Release"
    
    # Sign Release file if GPG key is available (optional)
    if command -v gpg >/dev/null 2>&1; then
        information "Signing Release file with GPG..."
        rm -f "${DIST_DIR}/Release.gpg"
        gpg --batch --yes --output "${DIST_DIR}/Release.gpg" --detach-sign "${DIST_DIR}/Release" 2>/dev/null || {
            warning "Failed to sign Release file (GPG key not configured or not available)"
        }
    fi
    
    # Cleanup
    rm -rf "${CONFIG_DIR}"
    
    information "Repository indices generated successfully"
}

# Build repository for ISO image
build_repo() {
    # Only needed for ISO images with repository inclusion
    if [[ ! " ${GENERATE_IMAGES[@]} " =~ " iso " ]]; then
        return 0
    fi

    # Ensure marker is not stale between builds
    rm -f "${BUILD_DIR}/image/ASTRA_INSTALLER"
    
    # Only if repository download is requested
    if [[ "${ISO_INCLUDE_REPO}" != "true" ]] && [[ ${#ISO_REPOS[@]} -eq 0 ]]; then
        return 0
    fi
    
    current_process
    
    # Download repository content
    download_repo_content
    
    # Generate repository indices
    generate_repository_indices
    
    # Create ASTRA_INSTALLER marker file for astra-installer-qt
    touch "${BUILD_DIR}/image/ASTRA_INSTALLER"
    information "Created ASTRA_INSTALLER marker file"
}

# 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"}
        ISO_VOLUME_ID=${ISO_VOLUME_ID:-"Astra Linux"}

        if [ "${ARCHITECTURE}" = "amd64" ]; then
            run_with_spinner "Creating ISO image ${ISO_NAME}" \
                xorriso \
                -as mkisofs \
                -iso-level 3 \
                -full-iso9660-filenames \
                --joliet \
                -r \
                -volid "${ISO_VOLUME_ID}" \
                -checksum_algorithm_iso sha256,sha512 \
                -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 --interval:appended_partition_2:all:: \
                -no-emul-boot \
                -iso_mbr_part_type 0x83 \
                -appended_part_as_gpt \
                -partition_offset 16 \
                -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/boot/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 "${ISO_VOLUME_ID}" \
                -partition_cyl_align all \
                -eltorito-alt-boot \
                -e EFI/boot/efiboot.img \
                -no-emul-boot \
                -append_partition 2 0xef ${BUILD_DIR}/scratch/efiboot.img \
                -output "${BUILD_DIR}/${ISO_NAME}" \
                -graft-points \
                "${BUILD_DIR}/image" \
                /EFI/boot/efiboot.img=${BUILD_DIR}/scratch/efiboot.img
        fi

        # Generate checksums
        if [ -f "${BUILD_DIR}/${ISO_NAME}" ]; then
            md5sum "${BUILD_DIR}/${ISO_NAME}" > "${BUILD_DIR}/${ISO_NAME}.md5"
            if command -v gostsum >/dev/null 2>&1; then
                gostsum -d "${BUILD_DIR}/${ISO_NAME}" > "${BUILD_DIR}/${ISO_NAME}.gost"
            fi
        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
}
