#!/usr/bin/env bash
#
# Name: Utilities
# Author: kleb
# Created: 2021/12/23
# Last modified: 2025/12/01
#
# Version: 2.3
#

TRUE="TRUE"
FALSE="FALSE"

checksumManifest="checksums.sha256"

declare -A colors=(
    ["MAGENTA"]=$'\033[1;35m'
    ["RED"]=$'\033[1;31m'
    ["GREEN"]=$'\033[1;32m'
    ["YELLOW"]=$'\033[1;33m'
    ["CYAN"]=$'\033[1;36m'
    ["WHITE"]=$'\033[1;37m'
    ["BOLD"]=$'\033[1m'
    ["RESET"]=$'\033[0m'
)

declare -A moduleIndent
currentModule=""
baseIndent="    "

cursor="on"
spinnerIdx=0
spinnerFramesL=('|' '/' '-' '\')
spinnerFramesR=('|' '\' '-' '/')
spinnerMessage=""

toggleCursor() {
    if [[ "$cursor" == "on" ]]; then
        tput civis 2>/dev/null || true
        cursor="off"
    else
        tput cnorm 2>/dev/null || true
        cursor="on"
    fi
}

restoreCursor() {
    tput cnorm 2>/dev/null || true
    cursor="on"
}

getIndent() {
    local indent="${moduleIndent[$currentModule]}"

    if [[ "$currentModule" != "kInit" ]]; then
        indent="$indent$baseIndent"
    fi

    echo "$indent"
}

spinnerTick() {
    local frameL="${spinnerFramesL[$spinnerIdx]}"
    local frameR="${spinnerFramesR[$spinnerIdx]}"

    printf "\r%s%s  %s %s %s%s" "$(getIndent)" "${colors[CYAN]}" "$frameL" "$spinnerMessage" "$frameR" "${colors[RESET]}"

    spinnerIdx=$(( (spinnerIdx + 1) % 4 ))
}

runWithSpinner() {
    local message="$1"
    local command="$2"
    local timeout="${3:-}"
    local doneMessage="${4:-$message}"

    spinnerMessage="$message"
    tput civis 2>/dev/null || true

    if [[ -n "$timeout" ]]; then
        timeout "$timeout" bash -c "$command" >/dev/null 2>&1 &
    else
        if [[ "${DEBUG_OUTPUT:-}" == "$TRUE" ]]; then
            eval "$command" &
        else
            eval "$command" >/dev/null 2>&1 &
        fi
    fi

    local cmdPid=$!
    local result=0

    while kill -0 "$cmdPid" 2>/dev/null; do
        spinnerTick
        sleep 0.12
    done

    wait "$cmdPid" 2>/dev/null
    result=$?

    printf "\r\033[K%s%s  -> %s%s\n" "$(getIndent)" "${colors[CYAN]}" "$doneMessage" "${colors[RESET]}"

    tput cnorm 2>/dev/null || true

    spinnerMessage=""

    if [[ -n "$timeout" && $result -eq 124 ]]; then
        logWarning "Command timed out after ${timeout}s"
    fi

    return $result
}

logInfo() {
    local message="$1"
    echo -e "$(getIndent)${colors[CYAN]}  -> ${message}${colors[RESET]}"
}

logError() {
    local message="$1"
    echo -e "$(getIndent)${colors[RED]}  [x] ${message}${colors[RESET]}"
}

logWarning() {
    local message="$1"
    echo -e "$(getIndent)${colors[YELLOW]}  [!] ${message}${colors[RESET]}"
}

initProgress() {
    local module="$1"
    local parentModule="${2:-}"

    currentModule="$module"

    if [[ -n "$parentModule" ]]; then
        moduleIndent["$module"]="${moduleIndent[$parentModule]:-}${baseIndent}"
    else
        moduleIndent["$module"]=""
    fi

    echo -e "${moduleIndent[$module]}${colors[BOLD]}${colors[CYAN]}=== Starting $module Module ===${colors[RESET]}"
}

updateProgress() {
    local message="$1"
    local percentage="$2"

    if [[ -n "$message" ]]; then
        local progressIndent="${moduleIndent[$currentModule]}"

        if [ "$percentage" -eq 100 ]; then
            if [[ "$currentModule" != "kInit" ]]; then
                progressIndent="$progressIndent "
                echo -e "${progressIndent}${colors[GREEN]}== ${message} [+] ==${colors[RESET]}"
            else
                echo -e "${progressIndent}${colors[GREEN]}${message} [+]${colors[RESET]}"
            fi
        else
            if [[ "$currentModule" != "kInit" ]]; then
                progressIndent="$progressIndent "
                echo -e "${progressIndent}${colors[CYAN]}== ${currentModule}${colors[RESET]} [${colors[YELLOW]}${percentage}%${colors[RESET]}]: ${colors[WHITE]}${message}${colors[RESET]} =="
            else
                echo -e "${progressIndent}${colors[CYAN]}${currentModule}${colors[RESET]} [${colors[YELLOW]}${percentage}%${colors[RESET]}]: ${colors[WHITE]}${message}${colors[RESET]}"
            fi
        fi
    fi
}

noOut() {
    if [[ "${DEBUG_OUTPUT:-}" == "$TRUE" ]]; then
        "$@"
    else
        "$@" >/dev/null 2>&1
    fi
}

withTimeout() {
    local timeout="${1:-30}"
    local command="$2"

    if [[ -z "$command" ]]; then
        logError "withTimeout: command is required"
        return 1
    fi

    if timeout "$timeout" bash -c "$command" >/dev/null 2>&1; then
        return 0
    else
        local exitCode=$?
        if [[ $exitCode -eq 124 ]]; then
            logError "Command timed out after ${timeout}s"
        fi
        return $exitCode
    fi
}

retryCommand() {
    local maxAttempts="${1:-3}"
    local delay="${2:-2}"
    local command="${3:-}"

    if [[ -z "$command" ]]; then
        return 1
    fi

    local attempt=1
    while [[ $attempt -le $maxAttempts ]]; do
        if [[ "${DEBUG_OUTPUT:-}" == "$TRUE" ]]; then
            if eval "$command"; then
                return 0
            fi
        else
            if eval "$command" >/dev/null 2>&1; then
                return 0
            fi
        fi

        if [[ $attempt -lt $maxAttempts ]]; then
            logWarning "Command failed (attempt $attempt/$maxAttempts), retrying in ${delay}s..."
            sleep "$delay"
            attempt=$((attempt + 1))
        else
            logError "Command failed after $maxAttempts attempts"
            return 1
        fi
    done
}

executeScript() {
    local scriptFile="$1"
    local debug="$2"
    local parentModule="$3"

    chmod +x "$scriptFile"

    if [[ "$debug" == "$TRUE" ]]; then
        if ! bash -x "$scriptFile" "$parentModule"; then
            return 1
        fi
    else
        if ! bash "$scriptFile" "$parentModule"; then
            return 1
        fi
    fi
}

executeWithPrompt() {
    local inputAnswer="$1"
    local command="$2"

    if [[ -z "$inputAnswer" || -z "$command" ]]; then
        logError "executeWithPrompt: inputAnswer and command are required"
        return 1
    fi

    echo "$inputAnswer" | eval "$command" >/dev/null 2>&1
}

getInput() {
    local prompt="$1"
    local default="${2:-}"
    local response

    if [[ ! -t 0 ]]; then
        logError "No TTY available for input."
        return 1
    fi

    local displayPrompt
    if [[ -z "$prompt" ]]; then
        displayPrompt="Please enter a value"
    else
        displayPrompt="$prompt"
    fi

    if [[ -n "$default" ]]; then
        displayPrompt="$displayPrompt [$default]"
    fi

    read -r -p "$displayPrompt: " response

    if [[ -z "$response" && -n "$default" ]]; then
        response="$default"
    fi

    echo "$response"
}

download() {
    local file="$1"
    local output="${2:-}"
    local bypassValidation="${3:-}"

    if [[ -n "$output" && -f "$output" ]]; then
        logInfo "File $output already exists. Removing it."
        remove "$output"
    fi

    if [[ -z "$file" ]]; then
        logError "No URL provided for download"
        return 1
    fi

    local downloadCmd
    if [[ -z "$output" ]]; then
        downloadCmd="wget --timeout=30 -q '$file'"
    else
        downloadCmd="wget --timeout=30 -q -O '$output' '$file'"
    fi

    local displayName="${output:-${file##*/}}"
    local attempt=1
    local maxAttempts=3

    while [[ $attempt -le $maxAttempts ]]; do
        if runWithSpinner "Downloading $displayName..." "$downloadCmd" "" "Downloaded $displayName"; then
            break
        fi

        if [[ $attempt -lt $maxAttempts ]]; then
            logWarning "Download failed (attempt $attempt/$maxAttempts), retrying..."
            sleep 2
            attempt=$((attempt + 1))
        else
            logError "Failed to download $file after $maxAttempts attempts"
            return 1
        fi
    done

    if [[ "$bypassValidation" != "$TRUE" ]]; then
        local actualFilename="${output:-${file##*/}}"
        validate "$file" "$actualFilename"
    fi
}

validate() {
    local url="$1"
    local filename="${2:-${url##*/}}"
    local relativePath="${url#*://*/}"

    if [[ ! -f "$filename" ]]; then
        logError "File for validation not found: $filename"
        return 1
    fi

    if [[ ! -f "$checksumManifest" ]]; then
        logWarning "Checksum manifest not found: $checksumManifest"
        return 1
    fi

    local line
    line=$(grep -E " +${relativePath}$" "$checksumManifest" || true)

    if [[ -z "$line" ]]; then
        logWarning "No checksum entry for $relativePath in $checksumManifest"
        return 1
    fi

    local expectedChecksum
    expectedChecksum=$(echo "$line" | awk '{print $1}')

    local checkLine
    checkLine="${line/$relativePath/$filename}"

    local generatedChecksum
    generatedChecksum=$(sha256sum "$filename" | awk '{print $1}')

    local truncatedExpected="${expectedChecksum: -8}"
    local truncatedGenerated="${generatedChecksum: -8}"

    if echo "$checkLine" | sha256sum -c --status -; then
        logInfo "Checksum validation successful for $filename ($truncatedGenerated/$truncatedExpected)"
        return 0
    else
        logError "Checksum validation failed for $filename ($truncatedGenerated/$truncatedExpected)"
        return 2
    fi
}

remove() {
    local file="$1"

    if [[ -f "$file" ]]; then
        rm "$file"
    fi
}

move() {
    local file="$1"
    local destination="$2"

    local filePath=$(readlink -f "$file")
    local destinationPath=$(readlink -f "$destination" 2>/dev/null || echo "")

    if [[ "$filePath" == "$destinationPath" ]]; then
        return 0
    fi

    if ! mv "$file" "$destination"; then
        logError "Failed to move $file to $destination"
        return 1
    fi
}

appendToFile() {
    local content="$1"
    local file="$2"

    if [[ -z "$content" || -z "$file" ]]; then
        logError "appendToFile: content and file are required"
        return 1
    fi

    if [[ -f "$file" ]] && grep -Fxq "$content" "$file" 2>/dev/null; then
        logInfo "Content already exists in $file, skipping"
        return 0
    fi

    echo "$content" >> "$file"
    local displayContent="$content"
    if [[ ${#displayContent} -gt 16 ]]; then
        displayContent="${displayContent:0:13}..."
    fi
    logInfo "Added to $file: $displayContent"
}

writeToFile() {
    local content="$1"
    local file="$2"

    if [[ -z "$content" || -z "$file" ]]; then
        logError "writeToFile: content and file are required"
        return 1
    fi

    printf '%s\n' "$content" > "$file"
    logInfo "Created ${file##*/}"
}

replace() {
    local file="$1"
    local pattern="$2"
    local replacement="$3"
    local mustMatch="${4:-$TRUE}"

    if [[ -z "$file" || -z "$pattern" ]]; then
        logError "Replace: file and pattern are required"
        return 1
    fi

    if [[ ! -f "$file" ]]; then
        logError "Replace: file not found: $file"
        return 1
    fi

    if [[ "$mustMatch" == "$TRUE" ]]; then
        if ! grep -q "$pattern" "$file"; then
            logError "Replace: pattern not found in $file: $pattern"
            return 1
        fi
    fi

    if ! sed -i "s|$pattern|$replacement|g" "$file"; then
        logError "Replace: failed to replace pattern in $file"
        return 1
    fi
}

installPackage() {
    local args=("$@")
    local keepConf
    local packages
    local dpkgOptions

    if [[ "${args[-1]}" == "$TRUE" || "${args[-1]}" == "$FALSE" ]]; then
        keepConf="${args[-1]}"
        packages=("${args[@]:0:${#args[@]}-1}")
    else
        keepConf="$FALSE"
        packages=("${args[@]}")
    fi

    if [[ "$keepConf" == "$TRUE" ]]; then
        dpkgOptions="--force-confold"
    else
        dpkgOptions="--force-confnew"
    fi

    for package in "${packages[@]}"; do
        if [[ "$package" == *.deb ]]; then
            if ! runWithSpinner "Installing $package..." "dpkg -i '$package'" "" "Installed $package"; then
                logError "Failed to install $package"
                return 1
            fi
            noOut apt install -f -y
        else
            if ! runWithSpinner "Installing $package..." "apt install -y -o Dpkg::Options::='$dpkgOptions' '$package'" "" "Installed $package"; then
                logError "Failed to install $package"
                return 1
            fi
        fi
    done
}

enableService() {
    local serviceName="$1"
    local timeout="${2:-30}"

    if ! runWithSpinner "Enabling $serviceName..." "systemctl enable '$serviceName'" "$timeout" "Enabled $serviceName"; then
        logWarning "Failed to enable $serviceName (continuing)"
    fi

    if ! runWithSpinner "Starting $serviceName..." "systemctl start '$serviceName'" "$timeout" "Started $serviceName"; then
        logWarning "Failed to start $serviceName (continuing)"
    fi
}

restartSSHService() {
    local timeout="${1:-30}"

    if systemctl status ssh >/dev/null 2>&1; then
        if ! runWithSpinner "Restarting ssh..." "systemctl restart ssh" "$timeout" "Restarted ssh"; then
            logWarning "Failed to restart ssh service (continuing)"
        fi
    elif systemctl status sshd >/dev/null 2>&1; then
        if ! runWithSpinner "Restarting sshd..." "systemctl restart sshd" "$timeout" "Restarted sshd"; then
            logWarning "Failed to restart sshd service (continuing)"
        fi
    elif service ssh status >/dev/null 2>&1; then
        if ! runWithSpinner "Restarting ssh..." "service ssh restart" "$timeout" "Restarted ssh"; then
            logWarning "Failed to restart ssh service (continuing)"
        fi
    elif service sshd status >/dev/null 2>&1; then
        if ! runWithSpinner "Restarting sshd..." "service sshd restart" "$timeout" "Restarted sshd"; then
            logWarning "Failed to restart sshd service (continuing)"
        fi
    else
        logWarning "No SSH service found to restart"
    fi
}

cleanAptSources() {
    local sourcesFile="/etc/apt/sources.list"

    if [[ ! -f "$sourcesFile" ]]; then
        return 0
    fi

    if grep -q "^deb cdrom:" "$sourcesFile" 2>/dev/null; then
        logInfo "Removing CD-ROM entries from $sourcesFile"
        sed -i '/^deb cdrom:/d' "$sourcesFile"
    fi
}

addCronjob() {
    local cronLine="$1"
    local tempFile=$(mktemp)

    crontab -l 2>/dev/null >"$tempFile" || true

    if ! grep -Fxq "$cronLine" "$tempFile"; then
        echo "$cronLine" >>"$tempFile"

        if crontab "$tempFile"; then
            logInfo "Cron job added: $cronLine"
        else
            logError "Failed to install new crontab!"
            rm -f "$tempFile"
            return 1
        fi
    else
        logInfo "Cron job already present, skipping: $cronLine"
    fi

    rm -f "$tempFile"
}

ensureMailto() {
    local mail

    if [[ -n "${ADD_MAIL:-}" ]]; then
        mail="MAILTO=\"$ADD_MAIL\""
    else
        mail="MAILTO=\"\""
    fi

    local tempFile
    tempFile=$(mktemp)

    crontab -l 2>/dev/null >"$tempFile" || true

    if grep -qE '^[[:space:]]*MAILTO=' "$tempFile"; then
        awk -v m="$mail" '
            BEGIN {done=0}
            /^[[:space:]]*MAILTO=/ && !done {print m; done=1; next}
            {print}
        ' "$tempFile" >"$tempFile.new"
    else
        {
            echo "$mail"
            cat "$tempFile"
        } >"$tempFile.new"
    fi

    if ! cmp -s "$tempFile" "$tempFile.new"; then
        if crontab "$tempFile.new"; then
            logInfo "MAILTO set in crontab: $mail"
        else
            logError "Failed to install new crontab!"
            rm -f "$tempFile" "$tempFile.new"
            return 1
        fi
    else
        logInfo "MAILTO already set, no changes made."
    fi

    rm -f "$tempFile" "$tempFile.new"
}

addFishAlias() {
    local name="$1"
    local definition="$2"

    if [[ -z "$name" || -z "$definition" ]]; then
        logError "addFishAlias: name and definition are required"
        return 1
    fi

    mkdir -p "$HOME/.config/fish/conf.d"

    local aliasFile="$HOME/.config/fish/conf.d/aliases.fish"
    local aliasLine="abbr $name $definition"

    if [[ -f "$aliasFile" ]] && grep -Fq "abbr $name " "$aliasFile" 2>/dev/null; then
        logInfo "Fish alias already exists: $name"
        return 0
    fi

    echo "$aliasLine" >> "$aliasFile"
    logInfo "Added fish alias: $name -> $definition"
}
