#!/usr/bin/env bash

# Every external command Panama's own scripts invoke must be installed by
# Panama's own package lists.
#
# This exists because the lists had drifted badly. jq is used by thirty-one call
# sites across the helpers and the contracts; kitty has a full shipped config
# and a dock pin; tmux and btop have shipped themes that the color scheme
# switches. None of the four were declared. So a fresh machine that followed
# this repository's own install instructions would not have them.
#
# The failure is quiet by design, which is what makes it worth a test: the
# helpers are written to report "not installed" rather than crash, so a missing
# dependency presents as a feature that silently is not there.
#
# Commands from coreutils and the shell itself are not checked -- nothing
# installs those separately, and listing them would be noise.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

fail() {
    printf 'declared dependencies contract: %s\n' "$1" >&2
    exit 1
}

# Shell syntax and builtins. These are not commands anyone installs, and the
# first version of this contract reported `then`, `esac` and `done` as missing
# packages, which buried the four real findings in a hundred lines of noise.
SHELL_WORDS='^(if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time|coproc|break|continue|return|exit|local|readonly|declare|export|unset|shift|eval|exec|source|trap|set|shopt|alias|unalias|builtin|command|enable|help|let|read|mapfile|printf|echo|test|true|false|wait|jobs|bg|fg|kill|pwd|cd|dirs|pushd|popd|umask|type|hash|getopts|split|sync)$'

# Provided by any Fedora install: coreutils, util-linux, the shell, and the
# systemd/session tooling. Nothing here is a choice Panama makes.
BASELINE='^(sh|bash|cat|cut|sed|awk|gawk|grep|egrep|head|tail|sort|uniq|tr|wc|find|xargs|basename|dirname|mkdir|rm|cp|mv|ln|chmod|chown|stat|df|du|date|sleep|env|id|tee|touch|mktemp|readlink|realpath|seq|comm|join|paste|od|file|nl|fold|column|tput|timeout|flock|install|sha256sum|md5sum|base64|nproc|uptime|free|uname|hostname|whoami|ps|pgrep|pkill|kill|killall|lsblk|mount|umount|sudo|su|rpm|dnf|flatpak|git|python3|ss|ip|lsof)$'

SESSION='^(systemctl|busctl|journalctl|loginctl|hostnamectl|localectl|systemd-inhibit|systemd-run|udevadm|gsettings|dconf|dbus-send|dbus-monitor|hyprctl|qs|quickshell|gnf|panama|wl-copy|wl-paste)$'

# Installed by install-packages itself, because no repository carries them.
# They are deliberately absent from the package lists, and install-packages
# probes for them with `command -v` precisely because they arrive out of band --
# so that probe must not be read as an undeclared dependency. Anything added
# here needs a matching install block and a stated reason for the exception.
SELF_INSTALLED='^(bun|claude)$'

# jq programs are quoted arguments, but the scanner is line-based and cannot
# tell a filter from a command. `not` is a jq builtin appearing inside one.
JQ_BUILTINS='^(not|empty|error|env|input|inputs)$'

declared="$(cat "$repo_dir"/setup/packages/* 2>/dev/null | sed 's/#.*//' | tr -d ' ' | grep -v '^$' | sort -u)"
[[ -n "$declared" ]] || fail 'no package lists found'

# A package is not always named after its command. Only the genuine mismatches
# are mapped, so an unmapped command is a real omission rather than a lookup
# failure.
package_for() {
    case "$1" in
        zbarimg)        printf 'zbar' ;;
        fc-list|fc-match) printf 'fontconfig' ;;
        lspci)          printf 'pciutils' ;;
        getenforce)     printf 'libselinux-utils' ;;
        nmcli)          printf 'NetworkManager' ;;
        wpctl)          printf 'wireplumber' ;;
        nvim)           printf 'neovim' ;;
        fwupdmgr)       printf 'fwupd' ;;
        dnf4)           printf 'python3-dnf' ;;
        notify-send)    printf 'libnotify' ;;
        wl-copy|wl-paste) printf 'wl-clipboard' ;;
        ssh-keygen)     printf 'openssh' ;;
        ssh|ssh-add)    printf 'openssh-clients' ;;
        rg)             printf 'ripgrep' ;;
        xdg-mime|xdg-settings|xdg-open) printf 'xdg-utils' ;;
        update-desktop-database|desktop-file-validate) printf 'desktop-file-utils' ;;
        python3)        printf 'python3' ;;
        *)              printf '%s' "$1" ;;
    esac
}

missing=()
checked=0

while read -r script; do
    [[ -n "$script" ]] || continue
    head -1 "$script" | grep -qE 'bash|/sh' || continue

    # Commands appearing at the start of a statement or after a pipe. Crude, but
    # it is looking for undeclared dependencies, not building a call graph.
    #
    # No minimum length. An earlier version required three characters, which
    # quietly excluded the most-used dependency in the repository -- jq, at
    # thirty-one call sites -- along with rg, ss and ip. A dependency checker
    # with a blind spot for short names is worse than none, because it reports
    # PASS.
    while read -r cmd; do
        [[ -n "$cmd" ]] || continue
        [[ "$cmd" =~ $SHELL_WORDS ]] && continue
        [[ "$cmd" =~ $BASELINE ]] && continue
        [[ "$cmd" =~ $SESSION ]] && continue
        [[ "$cmd" =~ $SELF_INSTALLED ]] && continue
        [[ "$cmd" =~ $JQ_BUILTINS ]] && continue

        pkg="$(package_for "$cmd")"
        grep -qx "$pkg" <<<"$declared" && continue

        # Only report a command that actually exists on this machine. An
        # invented name in a comment or a heredoc is a false positive; a real
        # binary that nothing declares is the thing being looked for.
        command -v "$cmd" >/dev/null 2>&1 || continue

        missing+=("$cmd (from $(basename "$script"), package: $pkg)")
    done < <({
        # Statement-initial or after a pipe.
        grep -oE '(^|[|;&]|\$\()[[:space:]]*[a-z][a-z0-9_-]+' "$script" \
            | grep -oE '[a-z][a-z0-9_-]+$'

        # Behind a wrapper. ddcutil is always invoked as `timeout 10 ddcutil`,
        # so it never appears statement-initial and was missed entirely.
        grep -oE '\b(timeout[[:space:]]+[0-9.]+|sudo|nohup|env)[[:space:]]+[a-z][a-z0-9_-]+' "$script" \
            | grep -oE '[a-z][a-z0-9_-]+$'

        # `command -v X` is how these helpers probe for a tool before using it,
        # which makes it the clearest possible statement of a dependency.
        grep -oE 'command -v[[:space:]]+[a-z][a-z0-9_-]+' "$script" \
            | grep -oE '[a-z][a-z0-9_-]+$'
    } | sort -u)

    checked=$((checked + 1))
done < <(find "$repo_dir/config/dot/quickshell/scripts" \
              "$repo_dir/config/local/share/vicinae/scripts" \
              "$repo_dir/setup/scripts" "$repo_dir/bin" \
              -type f 2>/dev/null)

if (( ${#missing[@]} > 0 )); then
    printf 'declared dependencies contract: commands used but never installed:\n' >&2
    printf '  %s\n' "${missing[@]}" | sort -u >&2
    fail 'add each to a list in setup/packages/, or the feature silently will not exist on a fresh machine'
fi

printf 'declared dependencies contract: PASS (%d scripts)\n' "$checked"
