#!/usr/bin/env bash

# A default-application role owns a FAMILY of types, and every type in it must
# agree.
#
# Each role used to map to a single representative type, so setting "images"
# changed image/png and left image/jpeg wherever it happened to land. That is
# how this desktop ended up opening PNGs in a pixel-art editor, MP3s in a video
# transcoder and PDFs in an image editor -- nobody chose any of it, the
# applications registered themselves and the roles governed one type each.
#
# The failure is invisible until someone double-clicks a file, which is the
# worst possible moment to discover it.
#
# Read-only: this inspects the schema of the roles and the state of the machine.
# It never changes a default, because doing so on the daily driver would be
# rude and the point is to detect drift, not create it.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-default-apps"

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

[[ -x "$helper" ]] || fail 'panama-default-apps is missing or not executable'

# The role table, read from the helper itself so the two cannot disagree.
families="$(python3 - "$helper" <<'PY'
import ast, re, sys
source = open(sys.argv[1]).read()
match = re.search(r"ROLE_TARGETS = (\{.*?\n\})", source, re.S)
if not match:
    raise SystemExit("ROLE_TARGETS not found")
table = ast.literal_eval(match.group(1))
for role, (kind, targets) in table.items():
    if kind != "mime":
        continue
    print(role + "\t" + ",".join(targets))
PY
)" || fail 'could not read ROLE_TARGETS from the helper'

[[ -n "$families" ]] || fail 'no mime-backed roles found'

command -v xdg-mime >/dev/null 2>&1 || { printf 'default apps family contract: SKIP (no xdg-mime)\n'; exit 0; }

checked=0
while IFS=$'\t' read -r role types; do
    [[ -n "$role" ]] || continue

    first=""
    disagreeing=""
    for mime in ${types//,/ }; do
        handler="$(xdg-mime query default "$mime" 2>/dev/null)"
        # A type nothing claims is not drift; it is simply unclaimed, and
        # forcing a handler for every exotic type is not this role's job.
        [[ -n "$handler" ]] || continue
        if [[ -z "$first" ]]; then
            first="$handler"
            continue
        fi
        [[ "$handler" == "$first" ]] || disagreeing+="$mime->$handler "
    done

    [[ -z "$disagreeing" ]] \
        || fail "the \"$role\" role is split: its first type opens with $first but $disagreeing-- setting the role must write every type in the family"

    checked=$((checked + 1))
done <<<"$families"

# Every role the helper can set must be reachable from the settings page, and
# every row on the page must name a role the helper knows. A role that exists
# only in the helper is unreachable from the UI; a row naming a role the helper
# does not have does nothing when someone taps it.
page="$repo_dir/config/dot/quickshell/modules/settings/ApplicationsPage.qml"
[[ -r "$page" ]] || fail 'ApplicationsPage.qml is missing'

helper_roles="$(python3 - "$helper" <<'ROLES'
import ast, re, sys
source = open(sys.argv[1]).read()
table = ast.literal_eval(re.search(r"ROLE_TARGETS = (\{.*?\n\})", source, re.S).group(1))
print("\n".join(sorted(table)))
ROLES
)"
page_roles="$(grep -o 'key: "[a-z]*"' "$page" | sed 's/key: "//; s/"//' | sort -u)"

missing="$(comm -23 <(printf '%s\n' "$helper_roles") <(printf '%s\n' "$page_roles") | tr '\n' ' ')"
extra="$(comm -13 <(printf '%s\n' "$helper_roles") <(printf '%s\n' "$page_roles") | tr '\n' ' ')"
[[ -z "${missing// }" ]] || fail "the helper can set these roles but the settings page never offers them: $missing"
[[ -z "${extra// }" ]] || fail "the settings page offers roles the helper cannot set: $extra"

# Every application a role is curated to must be installed by Panama's own
# package lists.
#
# Seeding is deliberately conservative: a role whose preferred applications are
# all missing is left alone rather than forced. That is the right behavior and
# it is also silent -- so a curated handler nobody installs presents as the
# machine quietly going back to deciding defaults by installation order, which
# is the whole problem this was built to fix.
#
# The package name for each handler is spelled out here because a desktop id
# does not carry one; rpm and flatpak name the same application differently and
# neither name is derivable from the other.
declare -A HANDLER_PACKAGE=(
    [org.gnome.Loupe.desktop]=loupe
    [org.gnome.Papers.desktop]=papers
    [org.gnome.Decibels.desktop]=decibels
    [org.gnome.Nautilus.desktop]=nautilus
    [io.mpv.Mpv.desktop]=io.mpv.Mpv
)

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

preferred="$(python3 - "$helper" <<'PREFERRED'
import ast, re, sys
source = open(sys.argv[1]).read()
table = ast.literal_eval(re.search(r"PREFERRED_HANDLERS = (\{.*?\n\})", source, re.S).group(1))
for role, candidates in table.items():
    # Only the first choice has to be installable: the rest are fallbacks for
    # machines that happen to have something else, and demanding all of them be
    # declared would mean installing three image viewers.
    print(role + "\t" + candidates[0])
PREFERRED
)" || fail 'could not read PREFERRED_HANDLERS from the helper'

while IFS=$'\t' read -r role handler; do
    [[ -n "$role" ]] || continue
    # Entries this repository ships itself are installed by link-dotfiles, not
    # by a package manager.
    [[ -r "$repo_dir/config/local/share/applications/$handler" ]] && continue
    package="${HANDLER_PACKAGE[$handler]:-}"
    [[ -n "$package" ]] \
        || fail "the \"$role\" role prefers $handler, which this contract has no package name for -- add it to HANDLER_PACKAGE"
    grep -qx "$package" <<<"$declared" \
        || fail "the \"$role\" role prefers $handler but nothing declares \"$package\", so a fresh machine seeds nothing for it"
done <<<"$preferred"

# The editor Panama ships must launch in Panama's terminal. The stock
# nvim.desktop sets Terminal=true, which hands the launch to whatever the
# system considers default -- not necessarily the terminal this desktop themes.
entry="$repo_dir/config/local/share/applications/panama-nvim.desktop"
[[ -r "$entry" ]] || fail 'panama-nvim.desktop is missing'
grep -q '^Exec=kitty ' "$entry" || fail 'the shipped editor entry does not launch kitty explicitly'
grep -q '^Terminal=false' "$entry" \
    || fail 'the editor entry sets Terminal=true, which defers to the system terminal rather than kitty'

printf 'default apps family contract: PASS (%d role families consistent)\n' "$checked"
