#!/bin/bash
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
[[ -z ${ARTOOLS_INCLUDE_VERSION_CHECK_SH:-} ]] || return 0
ARTOOLS_INCLUDE_VERSION_CHECK_SH=1

#LIBDIR=${LIBDIR:-/usr/share/artools}
# source "${LIBDIR}"/base/message.sh

# shellcheck source=src/lib/pkg/util/term.sh
source "${LIBDIR}"/pkg/util/term.sh

# source /usr/share/makepkg/util/message.sh

set -eo pipefail

readonly ARTIXPKG_VERSION_CHECK_EXIT_UP_TO_DATE=0
export ARTIXPKG_VERSION_CHECK_EXIT_UP_TO_DATE
readonly ARTIXPKG_VERSION_CHECK_EXIT_OUT_OF_DATE=2
export ARTIXPKG_VERSION_CHECK_EXIT_OUT_OF_DATE
readonly ARTIXPKG_VERSION_CHECK_EXIT_FAILURE=3
export ARTIXPKG_VERSION_CHECK_EXIT_FAILURE

# shellcheck disable=2034
ARTOOLS_VALID_VERSION_OUTPUT_FORMAT=(
    pretty
    json
)

artixpkg_version_check_usage() {
    local -r COMMAND=${_ARTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
    cat <<- _EOF_
    Usage: ${COMMAND} [OPTIONS] [PKGBASE]...

    Compares the versions of packages in the local packaging repository against
    their latest upstream versions.

    Upon execution, it generates a grouped list that provides detailed insights
    into each package's status. For each package, it displays the current local
    version alongside the latest version available upstream.

    Outputs a summary of up-to-date packages, out-of-date packages, and any
    check failures.

    OPTIONS
        -h, --help           Show this help text

    FILTER OPTIONS
        -v, --verbose        Display all results including up-to-date versions

    OUTPUT OPTIONS
        --json               Enable printing in JSON; Shorthand for '--format json'
        -F, --format FORMAT  Controls the output format of the results;
                            FORMAT is 'pretty', or 'json' (default: pretty)

    EXAMPLES
        $ ${COMMAND} neovim vim
_EOF_
}

artixpkg_version_check() {
    local pkgbases=()
    local verbose=0
    local output_format=pretty

    local path status_file path pkgbase upstream_version result

    local yaml_data=()
    local up_to_date=()
    local out_of_date=()
    local failure=()
    local current_item=0
    local section_separator=''
    local exit_code=${ARTIXPKG_VERSION_CHECK_EXIT_UP_TO_DATE}

    while (( $# )); do
        case $1 in
            -h|--help)
                artixpkg_version_check_usage
                exit 0
            ;;
            --json)
                output_format=json
                shift
            ;;
            -F|--format)
                (( $# <= 1 )) && die "missing argument for %s" "$1"
                output_format="${2}"
                if ! in_array "${output_format}" "${ARTOOLS_VALID_VERSION_OUTPUT_FORMAT[@]}"; then
                    die "Unknown output format: %s" "${output_format}"
                fi
                shift 2
            ;;
            -v|--verbose)
                verbose=1
                shift
            ;;
            --)
                shift
                break
            ;;
            -*)
                die "invalid argument: %s" "$1"
            ;;
            *)
                pkgbases=("$@")
                break
            ;;
        esac
    done

    if ! command -v nvchecker &>/dev/null; then
        die "The \"$_ARTOOLS_COMMAND\" command requires 'nvchecker'"
    fi

    # Check if used without pkgbases in a packaging directory
    if (( ${#pkgbases[@]} == 0 )); then
        if [[ -f PKGBUILD ]]; then
            pkgbases=(".")
        else
            artixpkg_version_check_usage
            exit 1
        fi
    fi

    # enable verbose mode when we only have a single item to check
    if (( ${#pkgbases[@]} == 1 )); then
        verbose=1
    fi

    if [[ ${output_format} == pretty ]]; then
        # start a terminal spinner as checking versions takes time
        status_dir=$(mktemp --tmpdir="${WORKDIR}" --directory artixpkg-version-check-spinner.XXXXXXXXXX)
        term_spinner_start "${status_dir}"
    fi

    for path in "${pkgbases[@]}"; do
        # skip paths that are not directories
        if [[ ! -d "${path}" ]]; then
            continue
        fi
        pushd "${path}" >/dev/null

        if [[ ${output_format} == pretty ]]; then
            # update the current terminal spinner status
            (( ++current_item ))
            artixpkg_version_check_spinner \
                "${status_dir}" \
                "${#up_to_date[@]}" \
                "${#out_of_date[@]}" \
                "${#failure[@]}" \
                "${current_item}" \
                "${#pkgbases[@]}"
        fi

        if [[ ! -f "PKGBUILD" ]]; then
            result="no PKGBUILD found"
            pkgbase=$(basename "${path}")
            yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" failure "${result}" false null null)")
            failure+=("${BOLD}${pkgbase}${ALL_OFF}: ${result}")
            popd >/dev/null
            continue
        fi

        # reset common PKGBUILD variables
        unset pkgbase pkgname arch source pkgver pkgrel validpgpkeys
        # shellcheck source=contrib/makepkg/PKGBUILD.proto
        . ./PKGBUILD
        pkgbase=${pkgbase:-$pkgname}

        if ! result=$(get_upstream_version); then
            yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" failure "${result}" false "${pkgver}" null)")
            failure+=("${BOLD}${pkgbase}${ALL_OFF}: ${result}")
            popd >/dev/null
            continue
        fi
        upstream_version=${result}

        if ! result=$(vercmp "${upstream_version}" "${pkgver}"); then
            result="failed to compare version ${upstream_version} against ${pkgver}"
            yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" failure "${result}" false "${pkgver}" "${upstream_version}")")
            result="${BOLD}${pkgbase}${ALL_OFF}: ${result}"
            failure+=("${result}")
            popd >/dev/null
            continue
        fi

        if (( result == 0 )); then
            if (( verbose )); then
                yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" success null false "${pkgver}" "${upstream_version}")")
            fi
            result="${BOLD}${pkgbase}${ALL_OFF}: current version ${PURPLE}${pkgver}${ALL_OFF} is latest"
            up_to_date+=("${result}")
        elif (( result < 0 )); then
            if (( verbose )); then
                yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" warning "local version is newer than upstream" false "${pkgver}" "${upstream_version}")")
            fi
            result="${BOLD}${pkgbase}${ALL_OFF}: current version ${PURPLE}${pkgver}${ALL_OFF} is newer than ${DARK_GREEN}${upstream_version}${ALL_OFF}"
            up_to_date+=("${result}")
        elif (( result > 0 )); then
            yaml_data+=("$(build_yaml_package_version_entry "${pkgbase}" success null true "${pkgver}" "${upstream_version}")")
            result="${BOLD}${pkgbase}${ALL_OFF}: upgrade from version ${PURPLE}${pkgver}${ALL_OFF} to ${DARK_GREEN}${upstream_version}${ALL_OFF}"
            out_of_date+=("${result}")
        fi

        popd >/dev/null
    done

    if [[ ${output_format} == pretty ]]; then
        # stop the terminal spinner after all checks
        term_spinner_stop "${status_dir}"
    fi

    # if [[ ${output_format} == json ]]; then
    #     yaml_data="${yaml_data[@]}" \
    #     yq -n '[env(yaml_data)]'
    #     return 0
    # fi

    # early exit for json output
    if [[ ${output_format} == json ]]; then
        jq --null-input \
            '$ARGS.positional' \
            --jsonargs "${yaml_data[@]}" | yq -p json -o yaml '.'
        return 0
    fi


    if (( verbose )) && (( ${#up_to_date[@]} > 0 )); then
        printf "%sUp-to-date%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
        section_separator=$'\n'
        for result in "${up_to_date[@]}"; do
            msg_success " ${result}"
        done
    fi

    if (( ${#failure[@]} > 0 )); then
        exit_code=${ARTIXPKG_VERSION_CHECK_EXIT_FAILURE}
        printf "%sFailure%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
        section_separator=$'\n'
        for result in "${failure[@]}"; do
            msg_error " ${result}"
        done
    fi

    if (( ${#out_of_date[@]} > 0 )); then
        exit_code=${ARTIXPKG_VERSION_CHECK_EXIT_OUT_OF_DATE}
        printf "%sOut-of-date%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
        section_separator=$'\n'
        for result in "${out_of_date[@]}"; do
            msg_warn " ${result}"
        done
    fi

    # Show summary when processing multiple packages
    if (( ${#pkgbases[@]} > 1 )); then
        printf '%s' "${section_separator}"
        artixpkg_version_check_summary \
            "${#up_to_date[@]}" \
            "${#out_of_date[@]}" \
            "${#failure[@]}"
    fi

    # return status based on results
    return "${exit_code}"
}

build_yaml_package_version_entry() {
    local pkgbase=$1
    local status=$2
    local message=$3
    local is_out_of_date=$4
    local local_version=$5
    local upstream_version=$6

    jq --null-input \
            --arg pkgbase "${pkgbase}" \
            --arg status "${status}" \
            --arg message "${message}" \
            --arg local_version "${local_version}" \
            --arg upstream_version "${upstream_version}" \
            --argjson out_of_date "${is_out_of_date}" \
            '$ARGS.named | walk(if type == "string" and (. == "null" or . == "") then null else . end)'

    # pkgbase=${pkgbase} \
    # status=${status} \
    # message=${message} \
    # local_version=${local_version} \
    # upstream_version=${upstream_version} \
    # is_out_of_date=${is_out_of_date} \
    # yq --null-input \
    #     '
    #     .pkgbase = env(pkgbase) |
    #     .status = env(status) |
    #     .message = env(message) |
    #     .local_version = env(local_version) |
    #     .upstream_version = env(upstream_version) |
    #     .out_of_date = env(is_out_of_date)
    #     '
}

get_upstream_version() {
    local config=.nvchecker.toml
    local output errors upstream_version
    local output
    local opts=()
    local keyfile="${XDG_CONFIG_HOME:-${HOME}/.config}/nvchecker/keyfile.toml"

    # check nvchecker config file
    if ! errors=$(nvchecker_check_config "${config}"); then
        printf "%s" "${errors}"
        return 1
    fi

    # populate keyfile to nvchecker opts
    if [[ -f ${keyfile} ]]; then
        opts+=(--keyfile "${keyfile}")
    fi

    # todo: use yq
    # if ! output=$(GIT_TERMINAL_PROMPT=0 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
    #     nvchecker --file "${config}" --logger json "${opts[@]}" | \
    #     yq -o yaml -p json 'select((.level != "debug") and (.event != "ignoring invalid version"))'); then
    #         printf "failed to run nvchecker: %s" "${output}"
    #     return 1
    # fi

    if ! output=$(GIT_TERMINAL_PROMPT=0 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
            nvchecker --file "${config}" --logger json "${opts[@]}" 2>&1 | \
            jq --raw-output 'select((.level != "debug") and (.event != "ignoring invalid version"))'); then
        printf "failed to run nvchecker: %s" "${output}"
        return 1
    fi

    if ! errors=$(nvchecker_check_error "${output}"); then
        printf "%s" "${errors}"
        return 1
    fi

    # local name
    # todo: use yq
    # if ! name=$(yq --exit-status '.name' <<< "${output}"); then
    #     printf "failed to select pkgbase result from output"
    #     return 1
    # fi

    # if ! upstream_version=$(yq --exit-status '.version' <<< "${output}"); then
    #     printf "failed to select version from result"
    #     return 1
    # fi

    if ! output=$(jq --raw-output --exit-status 'select(.name == "'"${pkgbase}"'")' <<< "${output}"); then
        printf "failed to select pkgbase result from output"
        return 1
    fi

    if ! upstream_version=$(jq --raw-output --exit-status '.version' <<< "${output}"); then
        printf "failed to select version from result"
        return 1
    fi

    # implement none source for packages like meta and virtual packages that are always up to date
    if [[ ${upstream_version} == None ]] && grep --quiet --extended-regexp '^source *= *"manual"' "${config}" &>/dev/null; then
        printf '%s' "${pkgver:-${upstream_version}}"
        return 0
    fi

    printf "%s" "${upstream_version}"
    return 0
}

nvchecker_check_config() {
    local config=$1

    local restricted_properties=(keyfile httptoken token)
    local property

    # check if the config file exists
    if [[ ! -f ${config} ]]; then
        printf "configuration file not found: %s" "${config}"
        return 1
    fi

    # check if config contains any restricted properties like secrets
    for property in "${restricted_properties[@]}"; do
        if grep --max-count=1 --quiet "^${property}" < "${config}"; then
            printf "restricted property in %s: %s" "${config}" "${property}"
            return 1
        fi
    done

    # check if the config contains a pkgbase section
    if [[ -n ${pkgbase} ]] && ! grep --max-count=1 --extended-regexp --quiet "^\\[\"?${pkgbase//+/\\+}\"?\\]" < "${config}"; then
        printf "missing pkgbase section in %s: %s" "${config}" "${pkgbase}"
        return 1
    fi

    # check if the config contains any section other than pkgbase
    if [[ -n ${pkgbase} ]] && property=$(grep --max-count=1 --perl-regexp "^\\[(?!\"?${pkgbase//+/\\+}(:.+)?\"?\\]).+\\]" < "${config}"); then
        printf "non-pkgbase section not supported in %s: %s" "${config}" "${property}"
        return 1
    fi

    # check if the config is using the 'cmd' source
    if grep --extended-regexp --quiet '^\s*source\s*=\s*["'\'']cmd["'\''].*' "${config}"; then
        printf "using the 'cmd' source in %s is disallowed" "${config}"
        return 1
    fi
}

nvchecker_check_error() {
    local result=$1
    local errors

    # todo: use yq
    if ! errors=$(jq --raw-output --exit-status \
        'select((.level == "error") and (.error != null)) | "\(.event)" + if .error then ": \(.error)" else "" end' \
        <<< "${result}"); then
        return 0
    fi

    mapfile -t errors <<< "${errors}"
    printf "%s\n" "${errors[@]}"
    return 1
}

artixpkg_version_check_summary() {
    local up_to_date_count=$1
    local out_of_date_count=$2
    local failure_count=$3

    # print nothing if all stats are zero
    if (( up_to_date_count == 0 )) && \
        (( out_of_date_count == 0 )) && \
        (( failure_count == 0 )); then
        return 0
    fi

    # print summary for all none zero stats
    printf "%sSummary%s\n" "${BOLD}${UNDERLINE}" "${ALL_OFF}"
    if (( up_to_date_count > 0 )); then
        msg_success " Up-to-date: ${BOLD}${up_to_date_count}${ALL_OFF}" 2>&1
    fi
    if (( failure_count > 0 )); then
        msg_error " Failure: ${BOLD}${failure_count}${ALL_OFF}" 2>&1
    fi
    if (( out_of_date_count > 0 )); then
        msg_warn " Out-of-date: ${BOLD}${out_of_date_count}${ALL_OFF}" 2>&1
    fi
}

artixpkg_version_check_spinner() {
    local status_dir=$1
    local up_to_date_count=$2
    local out_of_date_count=$3
    local failure_count=$4
    local current=$5
    local total=$6

    local percentage=$(( 100 * current / total ))
    local tmp_file="${status_dir}/tmp"
    local status_file="${status_dir}/status"

    # print the current summary
    artixpkg_version_check_summary \
        "${up_to_date_count}" \
        "${out_of_date_count}" \
        "${failure_count}" > "${tmp_file}"

    # print the progress status
    printf "📡 Checking: %s/%s [%s] %%spinner%%" \
    "${BOLD}${current}" "${total}" "${percentage}%${ALL_OFF}"  \
        >> "${tmp_file}"

    # swap the status file
    mv "${tmp_file}" "${status_file}"
}
