#!/bin/bash
#
#
#
####[ Variables ]#######################################################################


## Colors for output
YELLOW="$(printf '\033[0;33m')"
GREEN="$(printf '\033[0;32m')"
BLUE="$(printf '\033[0;34m')"
CYAN="$(printf '\033[0;36m')"
RED="$(printf '\033[1;31m')"
NC="$(printf '\033[0m')"  # No color.
readonly YELLOW GREEN BLUE CYAN RED NC

## Toastielab project details
readonly API_URL="https://toastielab.dev/api"

## other constants.
readonly BOT_EXECUTABLE="EllieBot"
readonly BIN_DIR="elliebot-bin"
readonly BIN_DIR_OLD="elliebot-bin.old"
readonly CREDS_FILE="creds.yml"
readonly CREDS_EXAMPLE_FILE="creds_example.yml"
# Packages `ffmpeg` and `ffprobe` are highly recommended dependencies for `yt-dlp`.
# NOTE: Do not add `python`, `python3`, or `python-is-python3` to this list, as they are
#   checked separately.
readonly REQUIRED_TOOLS=("jq" "yt-dlp")


####[ Functions ]#######################################################################


####
# Display the main menu.
display_menu() {
    clear
    echo "${BLUE}===== EllieBot Release Installer =====${NC}"
    echo "1. Install"
    echo "2. Run"
    echo "3. Edit credentials"
    echo "4. Exit"
    echo -n "${CYAN}Enter your choice:${NC} "
}

####
# Identify the system architecture, and return a string that represents
#
# RETURNS:
#   - linux-x64: For x86_64.
#   - linux-arm64: For aarch64 or arm64.
#   - unsupported: For all other architectures.
get_arch() {
    case $(uname -m) in
        x86_64)        echo "linux-x64" ;;
        aarch64|arm64) echo "linux-arm64" ;;
        *)             echo "unsupported" ;;
    esac
}

####
# Perform a download operation using either 'wget' or 'curl'.
#
# NEW GLOBALS:
#   - METHOD: The method used to download files ('wget' or 'curl').
#
# PARAMETERS:
#   - $1: url (Required)
#       - The full URL to download the item from.
#   - $2: output_file (Optional, Default: "")
#       - The name of the file to save the item to.
#       - IMPORTANT: If no output file is specified, the content at the URL will be
#           output to stdout.
#   - $3: error_message (Optional, Default: "ERROR: Failed to download the item")
#       - The message to display if the download fails.
#   - $4: is_silent (Optional, Default: false)
#       - If true, the download will be silent.
#       - Valid Options:
#           - true
#           - false
#
# RETURNS:
#   - 0: On successful download.
#   - 1: On failure to download the item.
dl() {
    local url="$1"
    local output_file="${2:-}"
    local error_message="${3:-ERROR: Failed to download the item}"
    local is_silent="${4:-false}"
    local flags=()

    ## Sets a global constant to indicate whether 'wget' or 'curl' is should be used to
    ## download files. Additionally, set any applicable flags that are to be used.
    if [[ -z $METHOD ]]; then
        if hash curl &>/dev/null; then
            METHOD="curl"
            # Set a silent flag if the user wants to download silently.
            [[ $is_silent == true ]] && flags+=("-s")
            # If an output file is specified, add the '-o' flag.
            [[ -n $output_file ]] && flags+=("-L" "-o" "$output_file")
        elif hash wget &>/dev/null; then
            METHOD="wget"
            # Set a silent flag if the user wants to download silently.
            [[ $is_silent == true ]] && flags+=("-q")

            ## If an output file is specified, add the '-O' flag with the file name.
            if [[ -n $output_file ]]; then
                flags+=("-O" "$output_file")
            ## If no output file is specified, add the '-O-' flag to output to stdout.
            else
                flags+=("-O-")
            fi
        else
            echo "${RED}ERROR: Neither 'wget' nor 'curl' is installed${NC}" >&2
            echo "${CYAN}Please install one of them and try again${NC}" >&2
            return 1
        fi
    fi

    ## Download the file.
    "$METHOD" "${flags[@]}" "$url" || {
        echo "${RED}$error_message${NC}" >&2
        return 1
    }
    return 0
}

####
# Download EllieBot's archive.
#
# PARAMETERS:
#   - $1: url (Required)
#       - The full URL to download the archive from.
#   - $2: output (Required)
#       - The name of the file to save the archive to.
#
# RETURNS:
#   - 0: On successful download.
#   - 1: On failure to download the archive.
download_archive() {
    local url="$1"
    local output="$2"

    dl "$url" "$output" "ERROR: Failed to download the archive" || return 1
    return 0
}

# TODO: Still needs to move data from the old directory to the new one.
####
# Downloads the latest release, extracts it, and sets up EllieBot's directory. If
# the directory already exists, it will be renamed to `elliebot-bin.old`. If
# `elliebot-bin.old` already exists, it will be deleted.
#
# PARAMETERS:
#   - $1: version (Required)
#       - The release version to download.
#
# RETURNS:
#   - 1: On failure of some operation.
install_software() {
    local version="$1"
    local archive_dir_name
    local archive_name="elliebot-v${version}.tar"
    local arch; arch=$(get_arch)
    local tar_url="${API_URL}/packages/EllieBotDevs/generic/EllieBot-build/${version}/${version}-${arch}-build.tar"

    ## NOTE: We could move this outside of the function, such that when the script is
    ##  executed, the architecture is determined once, then exit the script if the
    ##  architecture is unsupported.
    if [[ $arch == "unsupported" ]]; then
        echo "${RED}ERROR: Unsupported architecture${NC}" >&2
        return 1
    fi

    echo "${BLUE}Downloading '${version}' for '${arch}'...${NC}"
    download_archive "$tar_url" "$archive_name" || return 1

    echo "${BLUE}Extracting...${NC}"
    tar -xf "$archive_name" || {
        echo "${RED}ERROR: Failed to extract the release${NC}" >&2
        return 1
    }

    archive_dir_name=$(tar -tf "$archive_name" | head -1 | cut -f1 -d"/")

    ## If `elliebot-bin.old` already exists, delete it, so that we can rename the
    ## current `elliebot-bin` to it.
    if [[ -d $BIN_DIR_OLD ]]; then
        echo "${BLUE}Removing '$BIN_DIR_OLD'...${NC}"
        rm -rf "$BIN_DIR_OLD"
    fi

    ## If the directory already exists, rename it to `elliebot-bin.old`.
    if [[ -d $BIN_DIR ]]; then
        echo "${BLUE}Renaming '$BIN_DIR' to '$BIN_DIR_OLD'...${NC}"
        mv "$BIN_DIR" "$BIN_DIR_OLD"
    fi

    echo "${BLUE}Renaming '$archive_dir_name' to '$BIN_DIR'...${NC}"
    if [[ -d $archive_dir_name ]]; then
        mv "$archive_dir_name" "$BIN_DIR"
    else
        echo "${RED}ERROR: Unarchived directory '$archive_dir_name' not found${NC}" >&2
        return 1
    fi

    rm "$archive_name"
    chmod +x "${BIN_DIR}/${BOT_EXECUTABLE}"
    echo "${GREEN}Installation complete!${NC}"
}

####
# Display a list of available versions, and prompt the user to select one to install.
install_submenu() {
    local versions
    # NOTE: This works because the `jq` command outputs a newline-separated list.
    mapfile -t versions < <(
        dl "${API_URL}/releases" "" "ERROR: Failed to get releases" \
            true | jq -r '.[].tag_name' | sort -V -r
    )

    echo "${CYAN}Select version to install:${NC}"
    select version in "${versions[@]}"; do
        if [[ -n $version ]]; then
            install_software "$version"
            break
        else
            echo "${RED}ERROR: Invalid selection${NC}"
        fi
    done
}

####
# Check if the 'token' in 'creds.yml' is set.
#
# NOTE: This function is not foolproof, as it only checks if the 'token' is set to an
#   empty string. It does not check if the token is valid.
#
# RETURNS:
#   - 0: If the 'token' is not set.
#   - 1: If the 'token' is set.
is_token_set() {
    if [[ ! -f $BIN_DIR/$CREDS_FILE ]]; then
        return 0
    elif grep -Eq '^token: '\'\''' "$BIN_DIR/$CREDS_FILE"; then
        return 1
    else
        return 0
    fi
}


####
# Verify that the bot is installed, the token in the 'creds.yml' file is set, and then
# run the bot.
#
# RETURNS:
#   - 1: If the bot is not installed, the executable is not found, or the token is not
#       set.
run_bot() {
    ## Ensure that the bot is installed.
    if [[ ! -d $BIN_DIR ]]; then
        echo "${RED}ERROR: EllieBot not installed. Please install it first.${NC}" >&2
        return 1
    fi

    ## Ensures that the executable exists.
    if [[ ! -f $BIN_DIR/$BOT_EXECUTABLE ]]; then
        echo "${RED}ERROR: EllieBot executable not found${NC}" >&2
        return 1
    fi

    ## Create the creds file if it doesn't exist.
    if [[ ! -f $BIN_DIR/$CREDS_FILE ]]; then
        cp -f "$BIN_DIR/$CREDS_EXAMPLE_FILE" "$BIN_DIR/$CREDS_FILE"
    fi

    ## Ensure that the token is set. Do note that it won't say if the token is invalid.
    if ! is_token_set; then
        echo "${YELLOW}WARNING: 'token' is not set in '$CREDS_FILE'. Please add your" \
            "token and try again.${NC}" >&2
        return 1
    fi

    echo "${BLUE}Attempting to run EllieBot...${NC}"
    pushd "$BIN_DIR" >/dev/null || return 1
    ./$BOT_EXECUTABLE
    popd >/dev/null || return 1
}

####
# Verify that Python 3 is installed, and that the version is 3.9 or higher.
#
# RETURNS:
#   - 0: If the verification passes.
#   - 1: If the verification fails.
python_verification() {
    local verification_passed=true

    ## Verify that Python 3 is installed.
    if ! hash python3 &>/dev/null; then
        echo "${YELLOW}WARNING: Python 3 is not installed${NC}" >&2
        verification_passed=false
    fi

    ## Some systems, specifically Linux distributions like Ubuntu, don't have `python`
    ## aliased to `python3`. This check is to ensure that `python` is available.
    if ! hash python &>/dev/null; then
        echo "${YELLOW}WARNING: The 'python' command is not available${NC}" >&2
        echo "${CYAN}You may need to install 'python-is-python3'${NC}" >&2
        verification_passed=false
    fi

    ## Some systems still have python2 available and installed. We need to ensure that
    ## the version of python is 3.9 or higher.
    if ! python -c "import sys; assert sys.version_info >= (3, 9)" &>/dev/null; then
        echo "${YELLOW}WARNING: Python 3.9 or higher is required${NC}" >&2
        verification_passed=false
    fi

    [[ $verification_passed == false ]] && return 1
    return 0
}

####
# Check if external tools that often need to be installed are available. If any are
# missing, print an error message and exit.
#
# EXITS:
#   - 1: If any of the required tools are not installed.
verify_tools() {
    local all_tools_installed=true

    for tool in "${REQUIRED_TOOLS[@]}"; do
        if ! hash "$tool" &>/dev/null; then
            all_tools_installed=false
            echo "${YELLOW}WARNING: '$tool' is not installed${NC}" >&2
        fi
    done

    python_verification

    if [[ $all_tools_installed == false ]]; then
        echo "${CYAN}Please install the required tools and try again${NC}" >&2
        exit 1
    fi
}

# TODO: Still needs to be edited/expanded upon.
edit_creds() {
    local creds_file="${BIN_DIR}/creds.yml"

    if [[ ! -d $BIN_DIR ]]; then
	echo "Please install the bot first."
	return 1
    fi

    if [[ ! -f $creds_file ]]; then
	cp $BIN_DIR/creds_example.yml $creds_file
    fi

    local file="$1"
    local editors=("nano" "micro" "code" "vim" "emacs" "gedit")

    if [ ! -f "$file" ]; then
        echo "ERROR: File '$file' does not exist." >&2
        return 1
    fi

    for editor in "${editors[@]}"; do
        if command -v "$editor" >/dev/null 2>&1; then
            "$editor" "$file"
            return 0
        fi
    done

    echo "ERROR: No known text editors (${editors[*]}) are available on this system." >&2
    return 1
}


####[ Main ]############################################################################


verify_tools

while true; do
    display_menu
    read -r choice

    case $choice in
        1) install_submenu ;;
        2) run_bot ;;
        3) edit_creds ;;
        4) echo "${GREEN}Exiting...${NC}"; exit 0 ;;
        *) echo "${RED}ERROR: Invalid option${NC}" ;;
    esac

    echo "${CYAN}Press [Enter] to continue...${NC}"
    read -r
done