#!/usr/bin/env bash
set -euo pipefail
# An installer script for GitHub releases
# Usage: github-release-installer [--test|-t|--debug|-d|--dry-run|-n] "App Name" "binary_name" "repo/name"

# Globals
GITHUB_API_BASE_URL="https://api.github.com"
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
GITHUB_TOKEN_FILE="${GITHUB_TOKEN_FILE:-$HOME/.github_public_token.env}"
CURL_OPTS=("-s" "-L")

# User authentication for GitHub API requests
if [[ -z $GITHUB_TOKEN ]]; then
    [[ -f $GITHUB_TOKEN_FILE ]] && source $GITHUB_TOKEN_FILE
fi

# If we have a GitHub token, use it for authentication
[[ -n $GITHUB_TOKEN ]] && CURL_OPTS+=("--header" "Authorization: token $GITHUB_TOKEN")

# Load github-specific curl options, if present
[[ -f $HOME/.curlrc-github ]] && CURL_OPTS+=("--config" "$HOME/.curlrc-github")

function discover_system_and_architecture() {
    # Detect system and architecture, unless overridden
    SYSTEM="${SYSTEM:-$(uname -s)}"
    ARCH="${ARCH:-$(uname -m)}"

    case "${SYSTEM,,}" in
        linux)
            SYSTEM=Linux
            SYSTEM_RE="linux"
            ROOT_USER=root
            ROOT_GROUP=root

            case "${ARCH,,}" in
                x86_64 | amd64)
                    ARCH="Intel 64-bit"
                    ARCH_RE="(x64|amd64)"
                    ;;
                aarch64 | arm64)
                    ARCH="ARM 64-bit"
                    ARCH_RE="arm64"
                    ;;
                *)
                    echo "This script only supports x86_64 and arm64 architectures." >&2
                    return 1
                    ;;
            esac
            ;;

        darwin|osx|macos)
            SYSTEM=MacOS
            SYSTEM_RE="(darwin|osx)"
            ROOT_USER=root
            ROOT_GROUP=wheel

            case "${ARCH,,}" in
                x86_64)
                    ARCH="Intel 64-bit"
                    ARCH_RE="(x86_64|x64|amd64|universal)"
                    ;;
                arm64)
                    ARCH="ARM 64-bit"
                    ARCH_RE="(arm64|universal)"
                    ;;
                *)
                    echo "This script only supports x86_64 and arm64 architectures." >&2
                    return 1
                    ;;
            esac
            ;;

        *)
            echo "This script only supports Linux and macOS." >&2
            return 1
            ;;
    esac

    return 0
}

function github_assets() {
    if [[ -z $REPO_NAME ]]; then
        echo "ERROR: Repository name is required to list GitHub assets." >&2
        return 1
    fi

    # Expand combined asset types
    if [[ -z $ASSET_TYPE ]]; then
        # Configure asset selector to match operating system packages and archives
        case $SYSTEM in
            Linux)
                ASSET_TYPE='\.(deb|rpm|zip|tar\.gz|tgz)$'
                ;;
            MacOS)
                ASSET_TYPE='\.(pkg|zip|tar\.gz|tgz)$'
                ;;
            *)
                ASSET_TYPE='\.(zip|tar\.gz|tgz)$'
                ;;
        esac
    elif [[ $ASSET_TYPE == "archive" ]]; then
        ASSET_TYPE='\.(zip|tar\.gz|tgz)$'
    elif [[ $ASSET_TYPE == "package" ]]; then
        ASSET_TYPE='\.(deb|pkg|rpm)$'
    fi

    # Check, if the asset type has a '$' at the end
    if [[ ! $ASSET_TYPE =~ \$\$?$ ]]; then
        ASSET_TYPE="${ASSET_TYPE}$"
    fi

    # Asset type should start with a dot
    if [[ ! $ASSET_TYPE =~ ^\\\. ]]; then
        ASSET_TYPE="\.$ASSET_TYPE"
    fi

    # Preprocess GitHub releases API response
    if [[ -z $ASSET_NAME || $ASSET_NAME =~ ^~ ]]; then
        # No asset name provided, or asset name is a regex pattern
        if [[ -z $ASSET_NAME ]]; then
            PATTERN=".*"
            [[ $TEST_MODE -gt 1 ]] && echo "[DEBUG]: Filtering assets for system regex: '$SYSTEM_RE' and architecture regex: '$ARCH_RE'." >&2
        else
            PATTERN="${ASSET_NAME:1}"
            [[ $TEST_MODE -gt 1 ]] && echo "[DEBUG]: Filtering assets for system regex: '$SYSTEM_RE', architecture regex: '$ARCH_RE', asset type regex: '$ASSET_TYPE'." >&2
        fi

        curl "${CURL_OPTS[@]}" "$GITHUB_API_BASE_URL/repos/$REPO_NAME/releases/latest" |
            jq -r \
                --arg PATTERN "$PATTERN" \
                --arg SYSTEM "$SYSTEM_RE" \
                --arg ARCH "$ARCH_RE" \
                --arg ASSET_TYPE "$ASSET_TYPE" \
                '{version: .tag_name | sub("^v"; ""; "i"), assets: [.assets[] | {name, browser_download_url} | select(.name | test("^" + $PATTERN + ".*" + $SYSTEM + "[_-]" + $ARCH + ".*" + $ASSET_TYPE; "i"))]}'
    else
        # Specific asset name provided
        curl "${CURL_OPTS[@]}" "$GITHUB_API_BASE_URL/repos/$REPO_NAME/releases/latest" |
            jq -r \
                --arg ASSET_NAME "$ASSET_NAME" \
                '{version: .tag_name | sub("^v"; ""; "i"), assets: [.assets[] | {name, browser_download_url} | select(.name == $ASSET_NAME)]}'
    fi
}

function define_sudo_cmd() {
    if [[ $(id -u) -ne 0 ]]; then
        # Not running as root, need sudo for installation
        SUDOCMD="sudo"
        if ! command -v sudo &> /dev/null; then
            echo "ERROR: sudo is not installed."
            return 1
        fi
    else
        SUDOCMD=""
    fi
}

function github_release_installer() {
    if ! command -v jq &> /dev/null || ! command -v curl &> /dev/null; then
        echo "ERROR: jq or curl is not installed."
        exit 1
    fi

    # Check, if required variables are set
    if [[ -z $APP_NAME || -z $BINARY_NAME || -z $REPO_NAME ]]; then
        # Check, if the values were provided as function arguments
        if [[  $# -ne 3 ]]; then
            echo "ERROR: APP_NAME, BINARY_NAME, and REPO_NAME must be set before calling github_release_installer." >&2
            return 1
        fi

        # Set from function arguments
        APP_NAME="$1"
        BINARY_NAME="$2"
        REPO_NAME="$3"
    fi

    # Detect system and architecture
    discover_system_and_architecture || return 1

    echo "Detected system: $SYSTEM, architecture: $ARCH"

    # Download GitHub releases API response
    GITHUB_API_RESPONSE="$(github_assets)"
    if [[ -z $GITHUB_API_RESPONSE ]]; then
        echo "ERROR: Could not download GitHub releases for '$APP_NAME'." >&2
        return 1
    fi
    [[ $TEST_MODE -gt 1 ]] && echo "[DEBUG]: GitHub API response for '$APP_NAME' is:" && cat <<< "$GITHUB_API_RESPONSE" >&2

    if [[ $TEST_MODE -gt 1 ]]; then
        echo "The filtered GitHub API response for '$APP_NAME' is:"
        echo "$GITHUB_API_RESPONSE"
    fi

    VERSION=$(jq -r .version <<< "$GITHUB_API_RESPONSE")
    if [[ -z $VERSION ]]; then
        echo "ERROR: Could not determine the latest version of '$APP_NAME'." >&2
        return 1
    fi

    [[ $TEST_MODE -gt 1 ]] && echo "[DEBUG]: Latest version of '$APP_NAME' is $VERSION."
    [[ $TEST_MODE -gt 1 ]] && echo "[DEBUG]: Asset name to download: ${ASSET_NAME:-<not specified>}."

    NUM_ASSETS=$(jq -r '.assets | length' <<< "$GITHUB_API_RESPONSE")
    if [[ $NUM_ASSETS -eq 0 ]]; then
        echo "ERROR: No assets found for '$APP_NAME' version $VERSION for $SYSTEM $ARCH." >&2
        return 1
    elif [[ $NUM_ASSETS -gt 1 ]]; then
        echo "WARNING: Multiple assets found for '$APP_NAME' version $VERSION for $SYSTEM $ARCH."
        echo -e "Please specify an asset name from the list below:\n"
        jq -r '.assets[] | "- \(.name)"' <<< "$GITHUB_API_RESPONSE"
        echo ""
        return 0
    fi

    if [[ -z $ASSET_NAME ]]; then
        # No asset name provided, check how many assets are available
        DOWNLOAD_URL=$(jq -r '.assets[].browser_download_url' <<< "$GITHUB_API_RESPONSE")
    elif [[ $ASSET_NAME =~ ^~ ]]; then
        # Asset name is a regex pattern
        PATTERN="${ASSET_NAME:1}"
        [[ $TEST_MODE -gt 0 ]] && echo "[DEBUG]: Using regex pattern to find asset: $PATTERN" >&2
        DOWNLOAD_URL=$(jq -r --arg PATTERN "$PATTERN" '.assets[] | select(.name | test($PATTERN; "i")) | .browser_download_url' <<< "$GITHUB_API_RESPONSE")
    else
        DOWNLOAD_URL=$(jq -r --arg ASSET_NAME "$ASSET_NAME" '.assets[] | select(.name == $ASSET_NAME) | .browser_download_url' <<< "$GITHUB_API_RESPONSE")
    fi

    if [[ -z $DOWNLOAD_URL ]]; then
        echo "ERROR: Could not find a suitable download URL for '$APP_NAME' version $VERSION for $SYSTEM $ARCH." >&2
        return 1
    fi

    # If in test mode, exit here
    [[ $TEST_MODE -gt 0 ]] && return 0

    # if output directory is specified, just download the file there
    if [[ -n $OUTPUT_DIR ]]; then
        if [[ ! -d $OUTPUT_DIR ]]; then
            echo "ERROR: Output directory '$OUTPUT_DIR' does not exist." >&2
            return 1
        fi
        # Remove "-s" from CURL_OPTS to show download progress
        CURL_OPTS=("${CURL_OPTS[@]:1}")
        OUTPUT_NAME=$(basename "$DOWNLOAD_URL")
        OUTPUT_PATH="$OUTPUT_DIR/$OUTPUT_NAME"
        echo "Downloading '$APP_NAME' version $VERSION to '$OUTPUT_PATH'..."
        # Execute curl to download the file and exit
        exec curl "${CURL_OPTS[@]}" -o "$OUTPUT_PATH" "$DOWNLOAD_URL"
    fi

    echo -e "Will download '$APP_NAME' version: $VERSION\nDownload URL: \"$DOWNLOAD_URL\"."

    define_sudo_cmd || return 1

    TMP=$(mktemp -d -t github-release-installer-XXXXXX)

    # Ensure temporary artifacts are cleaned up on exit
    trap "test -d $TMP && rm -rf $TMP" EXIT

    # Change to temporary directory
    cd "$TMP" || return 1
    # Create extraction directory
    mkdir -p $TMP/extracted || return 1

    echo "Downloading '$APP_NAME' version $VERSION..."

    # Check, if the binary name is different from the original
    if grep -q ":" <<< "$BINARY_NAME"; then
        ORIGINAL_NAME="${BINARY_NAME%%:*}"
        DESTINATION_NAME="${BINARY_NAME#*:}"
    else
        ORIGINAL_NAME="$BINARY_NAME"
        DESTINATION_NAME="$BINARY_NAME"
    fi

    # Check, the file type
    if [[ $DOWNLOAD_URL == *.zip ]]; then
        curl "${CURL_OPTS[@]}" -o $TMP/archive.zip $DOWNLOAD_URL || return 1
        echo "Uncompressing ZIP archive of '$APP_NAME'..."
        unzip -q $TMP/archive.zip -d $TMP/extracted || return 1
    elif [[ $DOWNLOAD_URL =~ \.(tar\.gz|tgz)$ ]]; then
        curl "${CURL_OPTS[@]}" -o $TMP/archive.tgz $DOWNLOAD_URL || return 1
        echo "Uncompressing GZIPped TAR archive of '$APP_NAME'..."
        tar -x -z -C $TMP/extracted -f $TMP/archive.tgz || return 1
    elif [[ $DOWNLOAD_URL =~ deb$ ]]; then
        echo "Downloading Debian package of '$APP_NAME'..."
        curl "${CURL_OPTS[@]}" -LO $DOWNLOAD_URL || return 1
        # Install the deb package to extract its contents
        echo "Installing Debian package of '$APP_NAME'..."
        $SUDOCMD dpkg -i $(basename $DOWNLOAD_URL)
        return $?
    elif [[ $DOWNLOAD_URL =~ pkg$ ]]; then
        echo "Downloading macOS package of '$APP_NAME'..."
        curl "${CURL_OPTS[@]}" -LO $DOWNLOAD_URL || return 1
        # Install the pkg package to extract its contents
        echo "Installing macOS package of '$APP_NAME'..."
        $SUDOCMD installer -pkg $(basename $DOWNLOAD_URL) -target /
        return $?
    elif [[ $DOWNLOAD_URL =~ rpm$ ]]; then
        echo "Downloading RPM package of '$APP_NAME'..."
        curl "${CURL_OPTS[@]}" -LO $DOWNLOAD_URL || return 1
        # Install the rpm package to extract its contents
        echo "Installing RPM package of '$APP_NAME'..."
        $SUDOCMD rpm -i $(basename $DOWNLOAD_URL)
        return $?
    else
        echo "Downloading binary release of '$APP_NAME'..."
        (cd $TMP/extracted && curl "${CURL_OPTS[@]}" -LO $DOWNLOAD_URL) || return 1
        return 0
    fi

    # Find the binary file by looking for a file with the exact name
    BINARY_PATH=$(find $TMP/extracted -name "$ORIGINAL_NAME" -type f)

    if [[ -z $BINARY_PATH ]]; then
        # Try with system and architecture suffix
        BINARY_PATH=$(find $TMP/extracted -name "${ORIGINAL_NAME}-${SYSTEM}-${ARCH}" -type f)
    fi

    if [[ -z $BINARY_PATH ]]; then
        echo "ERROR: Could not find binary file '$ORIGINAL_NAME' to install. Specify the correct binary name with the -b option." >&2
        echo ""
        echo "Available files in the extracted archive:"
        find $TMP/extracted -type f | sed "s|$TMP/extracted/||"
        echo ""
        return 1
    fi

    echo "Installing '$APP_NAME' binary "$ORIGINAL_NAME" to /usr/local/bin/$DESTINATION_NAME..."
    $SUDOCMD install -m 755 $BINARY_PATH /usr/local/bin/$DESTINATION_NAME
}

function usage() {
    cat << EOF
Usage: github-release-installer [options] "repo/name"

Options:
  --dry-run                                          Run in test mode. Optional level (default: 9).
                                                     Or set TEST_MODE environment variable to a number > 0.
  -l --list "repo/name"                              List available assets for the given repository.
  -a, --app-name "App Name"                          Name of the application (optional).
  -b, --binary-name "binary_name[:destination_name]" Name of the binary to install and optional destination name (optional).
  -j, --releases-json                                Only download the releases JSON file.
  -o, --output-directory                             Only download the file to the specified directory.
  -f, --file-name "file_name"                        Name of the file to download (optional).
  -t, --type "type"                                  Type of the asset to download: archive (zip, tar.gz, tgz) or package (deb, pkg, rpm) (optional).

Example:
  github-release-installer -a "GitHub CLI" -b gh cli/cli 
EOF
}

# Default values
APP_NAME=""
BINARY_NAME=""
REPO_NAME=""
ASSET_NAME=""
ASSET_TYPE=""
OUTPUT_DIR=""
RELEASES_ONLY=0
SYSTEM=""
ARCH=""

# Use value from environment variable or default to 0
TEST_MODE=${TEST_MODE:-0}

# Check, if we have been executed or sourced.
if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
    return 0
fi

shopt -s nocasematch
# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            usage
            exit 0
            ;;
        --dry-run)
            # Check, if the next argument is present and is a number
            if [[ ${2:-} =~ ^[0-9]+$ ]]; then
                TEST_MODE="$2"
                shift 2
            else
                TEST_MODE=1
                shift
            fi
            ;;
        -l|--list)
            REPO_NAME="$2"
            shift 2
            curl "${CURL_OPTS[@]}" "$GITHUB_API_BASE_URL/repos/$REPO_NAME/releases/latest" |
                jq -r '"- \(.assets[].browser_download_url)"'
            exit $?
            ;;
        -a|--app-name)
            APP_NAME="$2"
            shift 2
            ;;
        -b|--binary-name)
            BINARY_NAME="$2"
            shift 2
            ;;
        -o|--output-directory)
            OUTPUT_DIR="$2"
            shift 2
            # That trick mimicks realpath
            OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)"
            ;;
        -j|--releases-json)
            RELEASES_ONLY=1
            shift
            ;;
        -f|--file-name)
            ASSET_NAME="$2"
            shift 2
            ;;
        -t|--type)
            if [[ $2 =~ ^(zip|gzip|gz|tar|tar\.gz|tgz|deb|pkg|rpm)$ ]]; then
                ASSET_TYPE="$2$"
            elif [[ $2 =~ ^(archive|package)$ ]]; then
                ASSET_TYPE="$2"
            else
                echo "ERROR: Unknown asset type: $2" >&2
                usage
                exit 1
            fi
            shift 2
            ;;
        # Override system and architecture detection
        --system)
            SYSTEM="$2"
            shift 2
            ;;
        --arch)
            ARCH="$2"
            shift 2
            ;;
        -*)
            echo "Unknown option: $1" >&2
            usage
            exit 1
            ;;
        *)
            break
            ;;
    esac
done

# First positional argument is REPO_NAME
REPO_NAME="${1:-}"
shift

if [[ -z $REPO_NAME ]]; then
    echo "ERROR: Repository name is required." >&2
    usage
    exit 1
fi

# If APP_NAME or BINARY_NAME are missing, try to infer them
# Use last part of REPO_NAME as BINARY_NAME if not provided
if [[ -z $BINARY_NAME ]]; then
    BINARY_NAME="${REPO_NAME##*/}"
fi

# Use capitalized last part of REPO_NAME as APP_NAME if not provided
if [[ -z $APP_NAME ]]; then
    APP_NAME="${REPO_NAME##*/}"
    APP_NAME="${APP_NAME^[a-z]}"
fi

if [[ $RELEASES_ONLY -ne 0 ]]; then
    URL="$GITHUB_API_BASE_URL/repos/$REPO_NAME/releases/latest"
    echo "Downloading GitHub releases for '$APP_NAME' from $URL..."

    OUTPUT_PATH="${BINARY_NAME}.releases.json"
    if [[ -n $OUTPUT_DIR ]]; then
        OUTPUT_PATH="$OUTPUT_DIR/$OUTPUT_PATH"
    fi

    if ! curl "${CURL_OPTS[@]}" -o "$OUTPUT_PATH" "$URL"; then
        echo "ERROR: Could not download GitHub releases for '$APP_NAME' from $URL." >&2
        exit 1
    fi
    echo "Downloaded GitHub releases to $OUTPUT_PATH."
    exit 0
fi

github_release_installer
exit $?
