#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
: 'Check the version status of installed ports and compare them to
version in remote repositories and the local ports index.

:Author:    Franz Glasner
:Copyright: (c) 2017-2024 Franz Glasner.
            All rights reserved.
:License:   BSD 3-Clause "New" or "Revised" License.
            See LICENSE for details.
            If you cannot find LICENSE see
            <https://opensource.org/licenses/BSD-3-Clause>
:ID:        @(#)@@SIMPLEVERSIONTAG@@

'

# shellcheck disable=SC2034    # VERSION appears unused
VERSION='@@VERSION@@'

USAGE='
USAGE: check-ports [options] [args...]

Options:

  -V   Print the program name and version number to stdout and exit

  -h   Print this help message to stdout and exit

  -A   Print for every package the status of all repositories

  -a   Print the data of all repos that have the package

  -n   Print the status of given packages in `args` in all details.
       No other options are respected.

  -s   Print the status of all packages that need some attention

  -v   Print the title and repository of every installed package always

Per Default (without any option) the status of every package is
printed with respect to repositories that have the package and have
differing versions.
'

#
# Configuration directory
#
: ${CONFIGDIR:=@@ETCDIR@@}

test -r "${CONFIGDIR}/pkgtools.conf" && . "${CONFIGDIR}/pkgtools.conf"

#
# Mapping configuration: installed package name -> original package name
# Note: This is independent of any repo
#
: ${PACKAGE_MAPPING:=${CONFIGDIR}/package-mapping.conf}

#
# Local repository with non-public packages and/or ports with changed
# OPTIONS (i.e. not using the defaults) or forks of official packages with
# other package names.
# This repo is strictly *local* to the host and/or jail.
#
: ${LOCAL_REPO:=LocalRepo}

#
# Shared local repository with non-public packages and/or ports with
# changed OPTIONS (i.e. not using the defaults).
# Contrary to LOCAL_REPO this repository may be shared.
#
: ${SHARED_LOCAL_REPO:=SharedLocalRepo}

#
# (Shared) repository with ports with default OPTIONS (i.e. unchanged)
# but newer than the packages in the "FreeBSD" repository.
# Can also contain non-FreeBSD *public* packages.
# May be shared.
# Some sort of a fast-track repository.
#
: ${LOCALBSDPORTS_REPO:=LocalBSDPorts}

#
# The official FreeBSD binary repository
#
: ${FREEBSD_REPO:=FreeBSD}

#
# Directly installed from ports
#
: ${PORTS_DIRECT_INSTALLED_REPO:=unknown-repository}

#
# For the workaround of the bug in pkg rquery -I
#
: ${PORTSDIR:=/usr/ports}


test_exists_local_index() {
    : 'Determine whether there exists a ports directory with an index
    file.

    Returns:
        status 0 iff the local index exists
    '
    pkg version -I -n DUMMY >/dev/null 2>/dev/null
}


get_ports_index_directory() {
    : 'Ask the packager configuration for the `INDEXDIR` and/or `PORTSDIR`
    configuration value: either `INDEXDIR` or -- if `INDEXDIR` is empty --
    `PORTSDIR` is used.

    Output (stdout)
        the directory where the index database file lives

    '
    local _dir

    _dir="$(pkg config INDEXDIR)"
    if [ -z "${_dir}" ]; then
        _dir="$(pkg config PORTSDIR)"
    fi
    printf '%s' "${_dir}"
}


get_ports_index_version() {
    : 'Determine for package `_package` the version of the package in the
    local ports index.

    Args:
        _package: the package name to search for

    Returns:
        0 on success, 1 on errors or if the package is not in the local
        ports index

    Output (stdout):
        the version number of `_package` in the local ports index

    '
    local _package _line _fqpn _n _lines
    local _indexdir _indexfile

    _package="$1"

#    _val=$(pkg rquery -I "${_package}" | cut -f 1 -d '|')
#    _rv=$?
#    immediate_index_version=${_val##*-}
    #    return ${_rv}

    _indexdir="$(get_ports_index_directory)"
    _indexfile="$(pkg config INDEXFILE)"

    if [ -r "${_indexdir}/${_indexfile}" ] ; then
        #
        # Note: Direct piping does not set immediate_index_version at return correctly
        #       "_line" is set correctly and parsing works, but the return 0 seems to kill
        #       some of the previous effects.
        #
        # "grep" does a fast pre-selection, reading, parsing and comparing is done for
        # exact matching.
        #
        # Use BREs because a `+' char needs to be handled as an ordinary
        # character (e.g. for the "lucene++" package).
        # Note that `^' at the start of an RE is not an ordinary
        # character. See re_format(7).
        #
        _lines=$(/usr/bin/grep -G '^'"${_package}" "${_indexdir}/${_indexfile}")
        while read _line ; do
            _fqpn="${_line%%|*}"
            _n=${_fqpn%-*}
	    if [ "${_package}" = "${_n}" ] ; then
                printf '%s' "${_fqpn##*-}"
                return 0
            fi
        done <<EOF1334TGH1
${_lines}
EOF1334TGH1
    fi
    return 1
}


get_mapping() {
    : 'Determine whether a package `_package` is essentially the same as
    another package.

    Args:
        _package: the new name of the package

    Returns:
        0 when a package mapping has been found, 1 otherwise

    Output (stdout):
        the name of the package on which `_package` is based on

    This command reads from the mapping database in in file
    `/usr/local/etc/local-bsdtools/package-mapping.conf`.
    Example::

        #
        # _package                         mapped_package_name
        #
        fmg-nextcloud-php71                nextcloud-php71
        fmg-nextcloud-twofactor_totp-php71 nextcloud-twofactor_totp-php71

    '
    local _package _n _mapped

    _package="$1"

    if [ -r "${PACKAGE_MAPPING}" ] ; then
        while read _n _mapped ; do
            if [ "${_n}" = "${_package}" ] ; then
                printf '%s' "${_mapped}"
                return 0
            fi
        done < ${PACKAGE_MAPPING}
    fi
    return 1
}


print_title() {
    : 'Print the output title line for a package

    Args:
        _package: the package name
        _version: the package version
        _repo:    the repository name

    Input (Globals).
        title_printed: a global that determines if the title really needs
                       to be printed.

                       If it is an empty string the the title is
                       really printed and the variable is set to
                       "yes".

    Output (Globals):
        title_printed: set to "yes" if the title has been printed

    '
    local _package _version _repo

    _package="$1"
    _version="$2"
    _repo="$3"
    if [ -z "${title_printed}" ]; then
        printf '%-36s %-17s (%s)\n' "${_package}" "${_version}" "${_repo}"
        title_printed=yes
    fi
}


print_detail_item() {
    : 'Print a detail item to stdout.

    The description `_descr` will not be printed if the label `_label`
    is ``?``.

    Args:
        _repo: the repository name
        _version: the version number to print to
        _label: the label (aka comparison character) to print to
        _descr: the description to print to
        _indent: (optional) extra indentation number (default 0)

    Output (stdout):
        the formatted detail line

    '
    local _repo _version _label _descr _indent
    local _real_descr

    _repo="$1"
    _version="$2"
    _label="$3"
    _descr="$4"
    _indent="$5"

    if [ -z "${_indent}" ]; then
        _indent="0"
    fi
    if [ "${_label}" = '?' ]; then
        _real_descr=''
    else
        _real_descr="${_descr}"
    fi

    printf '%-*s  %-15s: %-17s %s %s\n' $((_indent)) '' "${_repo}" "${_version}" "${_label}" "${_real_descr}"
}


check_ports() {
    : 'Implementation of all command variants besides of `-n`

    '
    local _ipackage _iversion _irepo _mapped_package_name _dummy
    local _print_detail _local_index_exists
    local _index_version _index_label _index_descr
    local _remote_version_FreeBSD _remote_label_FreeBSD _remote_descr_FreeBSD
    local _remote_version_LocalBSDPorts _remote_label_LocalBSDPorts _remote_descr_LocalBSDPorts
    local _remote_version_SharedLocalRepo _remote_label_SharedLocalRepo _remote_descr_SharedLocalRepo
    local _remote_version_LocalRepo _remote_label_LocalRepo _remote_descr_LocalRepo

    if test_exists_local_index; then
        _local_index_exists="1"
    fi
    pkg query '%n %v %R' |
        while read -r _ipackage _iversion _irepo; do
            title_printed=""
            _print_detail=""
            if [ -n "${_local_index_exists}" ]; then
                read -r _dummy _index_label _index_descr <<EOF_INDEX
$(pkg version -U -I -n "${_ipackage}" -v)
EOF_INDEX
            fi
            read -r _dummy  _remote_label_FreeBSD _remote_descr_FreeBSD <<EOF_FreeBSD
$(pkg version -U -R -r "${FREEBSD_REPO}" -n "${_ipackage}" -v)
EOF_FreeBSD
            read -r _dummy _remote_label_LocalBSDPorts _remote_descr_LocalBSDPorts <<EOF_LocalBSDPorts
$(pkg version -U -R -r "${LOCALBSDPORTS_REPO}" -n "${_ipackage}" -v)
EOF_LocalBSDPorts
            read -r _remote_fpname_SharedLocalRepo _remote_label_SharedLocalRepo _remote_descr_SharedLocalRepo <<EOF_SharedLocalRepo
$(pkg version -U -R -r "${SHARED_LOCAL_REPO}" -n "${_ipackage}" -v)
EOF_SharedLocalRepo
            read -r _remote_fpname_LocalRepo _remote_label_LocalRepo _remote_descr_LocalRepo <<EOF_LocalRepo
$(pkg version -U -R -r "${LOCAL_REPO}" -n "${_ipackage}" -v)
EOF_LocalRepo

            if [ -n "${option_verbose}" ]; then
                print_title "${_ipackage}" "${_iversion}" "${_irepo}"
            fi
            if get_mapping "${_ipackage}" >/dev/null;  then
                _print_detail="1"
            fi
            if [ -n "${option_alldata}" ]; then
                _print_detail="1"
            else
                if [ -n "${option_short}" ]; then
                    case "${_irepo}" in
                        "${FREEBSD_REPO}")
                            if [  -n "${_local_index_exists}" ]; then
                                if [ "${_index_label}" != '<' -a "${_index_label}" != '=' ]; then
                                    _print_detail=1
                                fi
                            fi
                            if [ "${_remote_label_FreeBSD}" != '=' -o "${_remote_label_SharedLocalRepo}" != '?' -o "${_remote_label_LocalRepo}" != '?' -o "${_remote_label_LocalBSDPorts}" != '?' ]; then
                                _print_detail=1
                            fi
                            ;;
                        "${LOCALBSDPORTS_REPO}")
                            if [  -n "${_local_index_exists}" ]; then
                                if [ "${_index_label}" != '=' ]; then
                                    _print_detail=1
                                fi
                            fi
                            if [ "${_remote_label_FreeBSD}" != '>' -o "${_remote_label_LocalRepo}" != '?' -o "${_remote_label_SharedLocalRepo}" != '?' -o "${_remote_label_LocalBSDPorts}" = '?' -o "${_remote_label_LocalBSDPorts}" = '<' ]; then
                                _print_detail=1
                            fi
                            ;;
                        "${SHARED_LOCAL_REPO}")
                            _print_detail=1
                            ;;
                        "${LOCAL_REPO}")
                            _print_detail=1
                            ;;
                        "${PORTS_DIRECT_INSTALLED_REPO}")
                            _print_detail=1
                            ;;
                        *)
                            echo "ERROR: unhandled repository: ${_irepo}" >&2
                            exit 1
                            ;;
                    esac
                else
                    if [ -n "${_local_index_exists}" ]; then
                        if [ "${_index_label}" != '?' -a "${_index_label}" != '=' ]; then
                            _print_detail=1
                        fi
                    fi
                    if [ "${_remote_label_FreeBSD}" != '?' -a "${_remote_label_FreeBSD}" != '=' ]; then
                        _print_detail=1
                    fi
                    if [ "${_remote_label_LocalBSDPorts}" != '?' -a "${_remote_label_LocalBSDPorts}" != '=' ]; then
                        _print_detail=1
                    fi
                    if [ "${_remote_label_SharedLocalRepo}" != '?' -a "${_remote_label_SharedLocalRepo}" != '=' ]; then
                        _print_detail=1
                    fi
                    if [ "${_remote_label_LocalRepo}" != '?' -a "${_remote_label_LocalRepo}" != '=' ]; then
                        _print_detail=1
                    fi
                fi
            fi
            if [ -n "${_print_detail}" ]; then
                print_title "${_ipackage}" "${_iversion}" "${_irepo}"
                if [ -n "${_local_index_exists}" ]; then
                    _index_version="$(get_ports_index_version "${_ipackage}")"
                    print_detail_item "INDEX" "${_index_version}" "${_index_label}" "${_index_descr}"
                fi
                if [ -n "${option_alldata_FreeBSD}" -o "${_remote_label_FreeBSD}" != '?' ]; then
                    _remote_version_FreeBSD="$(pkg rquery -U -r "${FREEBSD_REPO}" '%v' "${_ipackage}")"
                    print_detail_item "${FREEBSD_REPO}" "${_remote_version_FreeBSD}" "${_remote_label_FreeBSD}" "${_remote_descr_FreeBSD}"
                fi
                if [ -n "${option_alldata_LocalBSDPorts}" -o "${_remote_label_LocalBSDPorts}" != '?' ]; then
                    _remote_version_LocalBSDPorts="$(pkg rquery -U -r "${LOCALBSDPORTS_REPO}" '%v' "${_ipackage}")"
                    print_detail_item "${LOCALBSDPORTS_REPO}" "${_remote_version_LocalBSDPorts}" "${_remote_label_LocalBSDPorts}" "${_remote_descr_LocalBSDPorts}"
                fi
                if [ -n "${option_alldata_SharedLocalRepo}" -o "${_remote_label_SharedLocalRepo}" != '?' ]; then
                    _remote_version_SharedLocalRepo="$(pkg rquery -U -r "${SHARED_LOCAL_REPO}" '%v' "${_ipackage}")"
                    print_detail_item "${SHARED_LOCAL_REPO}" "${_remote_version_SharedLocalRepo}" "${_remote_label_SharedLocalRepo}" "${_remote_descr_SharedLocalRepo}"
                fi
                if [ -n "${option_alldata_LocalRepo}" -o "${_remote_label_LocalRepo}" != '?' ]; then
                    _remote_version_LocalRepo="$(pkg rquery -U -r "${LOCAL_REPO}" '%v' "${_ipackage}")"
                    print_detail_item "${LOCAL_REPO}" "${_remote_version_LocalRepo}" "${_remote_label_LocalRepo}" "${_remote_descr_LocalRepo}"
                fi
                _mapped_package_name="$(get_mapping "${_ipackage}")"
                if [ -n "${_mapped_package_name}" ] ; then
	            printf '%18s %s %s (%s)\n' "--------------->" "${_mapped_package_name}" "$(pkg rquery -U '%v' "${_mapped_package_name}")" "$(pkg rquery -U '%R' "${_mapped_package_name}")"
                    if [ -n "${_local_index_exists}" ]; then
                        print_detail_item "INDEX" "$(get_ports_index_version "${_mapped_package_name}")" "" ""
                    fi
                    print_detail_item "${FREEBSD_REPO}" "$(pkg rquery -U -r "${FREEBSD_REPO}" '%v' "${_mapped_package_name}")" "" ""
                    print_detail_item "${LOCALBSDPORTS_REPO}" "$(pkg rquery -U -r "${LOCALBSDPORTS_REPO}" '%v' "${_mapped_package_name}")" "" ""
                    print_detail_item "${SHARED_LOCAL_REPO}" "$(pkg rquery -U -r "${SHARED_LOCAL_REPO}" '%v' "${_mapped_package_name}")" "" ""
                    print_detail_item "${LOCAL_REPO}" "$(pkg rquery -U -r "${LOCAL_REPO}" '%v' "${_mapped_package_name}")" "" ""
                fi
            fi
        done
}


check_given_packages() {
    : 'Check the status of all given packages in the most detail possible

    Args:
        $@: the name of packaged to handle to

    '
    local _package _version _label _repo _descr _dummy
    local _local_index_exists _mapped_package_name

    if test_exists_local_index; then
        _local_index_exists="1"
    fi

    while [ $# -gt 0 ]; do
        _package="$1"
        shift
        read -r _version _repo <<EOF_INSTALLED
$(pkg query '%v %R' "${_package}")
EOF_INSTALLED
        if [ -n "${_version}" ]; then
            title_printed=""
            print_title "${_package}" "${_version}" "${_repo}"
            if [ -n "${_local_index_exists}" ]; then
                read -r _dummy _label _descr <<EOF_INDEX
$(pkg version -U -I -n "${_package}" -v)
EOF_INDEX
                _version="$(get_ports_index_version "${_package}")"
                print_detail_item "INDEX" "${_version}" "${_label}" "${_descr}"
            fi
            for _repo in "${FREEBSD_REPO}" "${LOCALBSDPORTS_REPO}" "${SHARED_LOCAL_REPO}" "${LOCAL_REPO}"; do
                read -r _dummy  _label _descr <<EOF_REPO
$(pkg version -U -R -r "${_repo}" -n "${_package}" -v)
EOF_REPO
                _version="$(pkg rquery -U -r "${_repo}" '%v' "${_package}")"
                print_detail_item "${_repo}" "${_version}" "${_label}" "${_descr}"
            done
            _mapped_package_name="$(get_mapping "${_ipackage}")"
            if [ -n "${_mapped_package_name}" ] ; then
                printf '%18s %s %s (%s)\n' "--------------->" "${_mapped_package_name}" "$(pkg rquery -U '%v' "${_mapped_package_name}")" "$(pkg rquery -U '%R' "${_mapped_package_name}")"
                if [ -n "${_local_index_exists}" ]; then
                    print_detail_item "INDEX" "$(get_ports_index_version "${_mapped_package_name}")" "" ""
                fi
                for _repo in "${FREEBSD_REPO}" "${LOCALBSDPORTS_REPO}" "${SHARED_LOCAL_REPO}" "${LOCAL_REPO}"; do
                    print_detail_item "${_repo}" "$(pkg rquery -U -r "${_repo}" '%v' "${_mapped_package_name}")" "" ""
                done
            fi
        fi
    done
}


option_alldata=""
option_alldata_FreeBSD=""
option_alldata_LocalBSDPorts=""
opeion_alldata_SharedLocalRepo=""
option_alldata_LocalRepo=""
option_short=""
option_verbose=""
option_packages=""

while getopts "VhAansv" _opt ; do
    case ${_opt} in
	V)
            printf 'check-ports %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
	    ;;
        h)
            echo "${USAGE}"
            exit 0
            ;;
        A)
            # Print for every package the status of all repositories
            option_alldata="1"
            option_alldata_FreeBSD="1"
            option_alldata_LocalBSDPorts="1"
            option_alldata_SharedLocalRepo="1"
            option_alldata_LocalRepo="1"
            ;;
        a)
            # Print the data of all repos that have the package
            option_alldata="1"
            ;;
        n)
            #
            # Print status of given packages in all details.
            # No other options are respected.
            #
            option_packages="1"
            ;;
        s)
            # "short" output: if installed from FreeBSD repo: don't
            # report if only the index is newer
            option_short="1"
            ;;
        v)
            #
            # Print all titles and repo of every installed package always.
            # The output of the other repo status nevertheless depends on the
            # other flag settings.
            #
            option_verbose="1"
            ;;
        \?)
            exit 2
            ;;
        *)
            echo "option handling failed" >&2
            exit 2
            ;;
    esac
done

#
# Reset the Shell's option handling system to prepare for handling
# command-local options.
#
shift $((OPTIND-1))
OPTIND=1

if [ -n "${option_short}" -a -n "${option_alldata}" ]; then
    echo "the -s option cannot be combined with -A or -a" >&2
    exit 2
fi

if [ -n "${option_packages}" ]; then
    check_given_packages "$@"
else
    check_ports
fi
