#!/bin/sh
#
# step1-create-recover-list.sh
#
# Generate a list of ports origins that are candidates for restoration in /var/db/ports.
#
# Policy:
#   - Packages in the pkg database that have a `built_by` annotation are
#     considered to originate from binary packages created by Poudriere or
#     similar tools and are execluded.
#   - Packages without a `built_by` annotation are treated as candidates for
#     restoration, as they may have been installed directly from the ports.
#
# This script does not modify /var/db/ports.
#
# Output:
#   recover.list
#       Origins present in the current ports tree that are candidates for 
#       restoration
#
#   recover-packages.tsv
#       Details of packages identified as candidates for restoration
#
#   binary-packages.tsv
#       Packages excluded because they have a `built_by` entry
#
#   missing-origins.tsv
#       Candidates for restoration where the origin does not exist in the
#       current ports tree
#
#   no-origin-packages.tsv
#       Packages with an empty or invalid origin
#
# Example usage:
#   mkdir /root/ports-options-recovery
#   cd /root/ports-options-recovery
#   sh ./step1-create-recover-list.sh
#

set -eu

PORTSDIR=${PORTSDIR:-/usr/ports}
OUTDIR=${OUTDIR:-.}

RECOVER_LIST="${OUTDIR}/recover.list"
RECOVER_PACKAGES="${OUTDIR}/recover-packages.tsv"
BINARY_PACKAGES="${OUTDIR}/binary-packages.tsv"
MISSING_ORIGINS="${OUTDIR}/missing-origins.tsv"
NO_ORIGIN_PACKAGES="${OUTDIR}/no-origin-packages.tsv"

TMPDIR_BASE=${TMPDIR:-/tmp}
WORKDIR=$(mktemp -d "${TMPDIR_BASE}/ports-options-step1.XXXXXX")

cleanup()
{
    rm -rf "${WORKDIR}"
}
trap cleanup EXIT HUP INT TERM

ALL_PACKAGES="${WORKDIR}/all-packages.tsv"
BUILT_BY_PACKAGES="${WORKDIR}/built-by-packages.tsv"
BUILT_BY_NAMES="${WORKDIR}/built-by-names"
CANDIDATE_PACKAGES="${WORKDIR}/candidate-packages.tsv"
VALID_ORIGINS="${WORKDIR}/valid-origins"

if ! command -v pkg >/dev/null 2>&1; then
    echo "Error: pkg command was not found." >&2
    exit 1
fi

if [ ! -d "${PORTSDIR}" ]; then
    echo "Error: ports tree was not found: ${PORTSDIR}" >&2
    echo "Set PORTSDIR if the ports tree is located elsewhere." >&2
    exit 1
fi

mkdir -p "${OUTDIR}"

echo "Reading installed package information..."

# package name, version, origin, automatic flag
#
# %a:
#   0 = manually installed
#   1 = automatically installed as a dependency
#
# The "automatic" flag will not be used in this round of selection, but it
# will be saved as reference information for when a human reviews the items
# later.
pkg query '%n	%v	%o	%a' |
    sort -t '	' -k1,1 > "${ALL_PACKAGES}"

# %At is the annotation tag name, and %Av is its value.
#
# Since a single package may contain multiple annotations, we will extract 
# only the lines that contain the "built_by" tag.
pkg query '%n	%v	%o	%a	%At	%Av' |
    awk -F '	' '$5 == "built_by"' |
    sort -t '	' -k1,1 > "${BUILT_BY_PACKAGES}"

cut -f1 "${BUILT_BY_PACKAGES}" |
    sort -u > "${BUILT_BY_NAMES}"

# Limit the candidates to only those packages without a "built_by" field.
#
# Since package names are unique within the pkg database, compare them using
# the first field as the key.
awk -F '	' '
    NR == FNR {
        excluded[$1] = 1
        next
    }
    !($1 in excluded) {
        print
    }
' "${BUILT_BY_NAMES}" "${ALL_PACKAGES}" > "${CANDIDATE_PACKAGES}"

# Initialize the output files.
: > "${RECOVER_PACKAGES}"
: > "${BINARY_PACKAGES}"
: > "${MISSING_ORIGINS}"
: > "${NO_ORIGIN_PACKAGES}"
: > "${VALID_ORIGINS}"

printf 'package\tversion\torigin\tautomatic\tbuilt_by\n' \
    > "${BINARY_PACKAGES}"

awk -F '	' '
    BEGIN {
        OFS = "\t"
    }
    {
        print $1, $2, $3, $4, $6
    }
' "${BUILT_BY_PACKAGES}" >> "${BINARY_PACKAGES}"

printf 'package\tversion\torigin\tautomatic\n' \
    > "${RECOVER_PACKAGES}"

printf 'package\tversion\torigin\tautomatic\treason\n' \
    > "${MISSING_ORIGINS}"

printf 'package\tversion\torigin\tautomatic\treason\n' \
    > "${NO_ORIGIN_PACKAGES}"

while IFS="$(printf '\t')" read -r pkgname version origin automatic
do
    case "${origin}" in
        ""|"-")
            printf '%s\t%s\t%s\t%s\t%s\n' \
                "${pkgname}" "${version}" "${origin}" "${automatic}" \
                "package has no usable origin" \
                >> "${NO_ORIGIN_PACKAGES}"
            continue
            ;;
    esac

    # Exclude unusual origins that contain absolute paths or "..".
    case "${origin}" in
        /*|../*|*/../*|*/..)
            printf '%s\t%s\t%s\t%s\t%s\n' \
                "${pkgname}" "${version}" "${origin}" "${automatic}" \
                "invalid origin" \
                >> "${NO_ORIGIN_PACKAGES}"
            continue
            ;;
    esac

    if [ -d "${PORTSDIR}/${origin}" ] &&
       [ -f "${PORTSDIR}/${origin}/Makefile" ]; then
        printf '%s\t%s\t%s\t%s\n' \
            "${pkgname}" "${version}" "${origin}" "${automatic}" \
            >> "${RECOVER_PACKAGES}"

        printf '%s\n' "${origin}" >> "${VALID_ORIGINS}"
    else
        printf '%s\t%s\t%s\t%s\t%s\n' \
            "${pkgname}" "${version}" "${origin}" "${automatic}" \
            "origin not found in current ports tree" \
            >> "${MISSING_ORIGINS}"
    fi
done < "${CANDIDATE_PACKAGES}"

sort -u "${VALID_ORIGINS}" > "${RECOVER_LIST}"

all_count=$(wc -l < "${ALL_PACKAGES}" | tr -d ' ')
binary_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${BINARY_PACKAGES}")
candidate_pkg_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${RECOVER_PACKAGES}")
origin_count=$(wc -l < "${RECOVER_LIST}" | tr -d ' ')
missing_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${MISSING_ORIGINS}")
no_origin_count=$(awk 'NR > 1 { count++ } END { print count + 0 }' \
    "${NO_ORIGIN_PACKAGES}")

cat <<EOF

Step 1 completed.

Installed packages:                  ${all_count}
Excluded packages with built_by:    ${binary_count}
Candidate packages with valid port: ${candidate_pkg_count}
Unique candidate origins:           ${origin_count}
Origins absent from current tree:   ${missing_count}
Packages without usable origin:     ${no_origin_count}

Generated files:

  ${RECOVER_LIST}
      Origins proposed for recovery.

  ${RECOVER_PACKAGES}
      Packages selected as recovery candidates.

  ${BINARY_PACKAGES}
      Packages excluded because built_by is present.

  ${MISSING_ORIGINS}
      Candidate packages whose origins are absent from ${PORTSDIR}.

  ${NO_ORIGIN_PACKAGES}
      Packages without a usable origin.

Review ${RECOVER_LIST} before proceeding to Step 2.
EOF

