#!/usr/bin/env bash

# Adding a settings page means editing four separate files, and missing one
# fails quietly rather than loudly:
#
#   services/SettingsRoutes.qml   the taxonomy: which category the page belongs
#                         to, and whether it is a tab inside one or a category
#                         of its own. This is the single source of truth -- the
#                         sidebar, the tab strip, ShellState's route
#                         resolution and the launcher command generator all
#                         derive from it, so a page absent here is a page that
#                         exists nowhere
#   SettingsShell.qml     the case that maps a leaf to a component, AND the
#                         Component declaration itself
#   modules/settings/qmldir   the component registration -- without it the page
#                         is "not a type" and the whole settings window fails
#                         to load, taking every other page with it
#   the .qml file itself
#
# ShellState no longer keeps its own allow-list: it asks SettingsRoutes to
# resolve whatever id it is handed. That removed a fifth place to forget, and
# this contract pins that it stays removed -- a literal list reappearing there
# would silently disagree with the taxonomy again.
#
# Nothing at runtime cross-checks any of this. This does, statically.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
settings_dir="$repo_dir/config/dot/quickshell/modules/settings"
routes="$repo_dir/config/dot/quickshell/services/SettingsRoutes.qml"
shell_file="$settings_dir/SettingsShell.qml"
qmldir="$settings_dir/qmldir"
shell_state="$repo_dir/config/dot/quickshell/services/ShellState.qml"

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

for required in "$routes" "$shell_file" "$qmldir" "$shell_state"; do
    [[ -r "$required" ]] || fail "cannot read $required"
done

# ── Reading the taxonomy ─────────────────────────────────────────────────────
# A category line carries an icon; a tab line does not. A category whose tabs
# are empty is a leaf itself, which is why the two shapes are read separately.
category_ids="$(grep -oE '\{ page: "[a-z-]+", label: "[^"]*", icon:' "$routes" \
    | sed -E 's/\{ page: "([a-z-]+)".*/\1/')"
tabless_ids="$(grep -oE '\{ page: "[a-z-]+", label: "[^"]*", icon: "[^"]*", tabs: \[\] \}' "$routes" \
    | sed -E 's/\{ page: "([a-z-]+)".*/\1/')"
tab_ids="$(grep -oE '\{ page: "[a-z-]+", label: "[^"]*" \}' "$routes" \
    | sed -E 's/\{ page: "([a-z-]+)".*/\1/')"

[[ -n "$category_ids" ]] || fail 'no categories found in SettingsRoutes -- this contract is not reading it correctly'
[[ -n "$tab_ids" ]] || fail 'no tabs found in SettingsRoutes -- this contract is not reading it correctly'

# The leaves: every page a person can actually land on.
leaves="$(printf '%s\n%s\n' "$tabless_ids" "$tab_ids" | sed '/^$/d')"

# ── The taxonomy addresses each leaf exactly once ─────────────────────────────
# ShellState.settingsPage holds a leaf id, and the sidebar highlights the
# category that owns it. A leaf in two categories makes "which row lights up"
# depend on iteration order, and makes the breadcrumb a coin flip.
duplicate_tabs="$(sort <<<"$tab_ids" | uniq -d)"
[[ -z "$duplicate_tabs" ]] \
    || fail "these tab pages appear in more than one category, so the sidebar highlight and the breadcrumb become ambiguous: $(tr '\n' ' ' <<<"$duplicate_tabs")"

# A category with tabs may share its id with its first tab -- "applications",
# "users" and "privacy" do, and both readings land on the same category. A
# category *without* tabs is a leaf, so sharing an id with a tab elsewhere
# would put one page in two places.
while read -r page; do
    [[ -n "$page" ]] || continue
    grep -qx "$page" <<<"$tab_ids" \
        && fail "\"$page\" is a category with no tabs and also a tab of another category, so the same page id names two different places"
done <<<"$tabless_ids"

duplicate_categories="$(sort <<<"$category_ids" | uniq -d)"
[[ -z "$duplicate_categories" ]] \
    || fail "these category ids are declared twice: $(tr '\n' ' ' <<<"$duplicate_categories")"

# ── Retired ids still land on a real page ────────────────────────────────────
# Old Vicinae commands, shell history and muscle memory keep handing over page
# ids that no longer exist, and resolve() consults the retired map before
# anything else. An entry aimed at a leaf that has itself since been renamed
# sends every one of those callers to Home without saying so, and an entry that
# names a live leaf shadows the real page.
retired_block="$(sed -n '/property var retired:/,/})/p' "$routes")"
retired_pairs="$(grep -oE '"[a-z-]+"[[:space:]]*:[[:space:]]*"[a-z-]+"' <<<"$retired_block" || true)"
retired_count=0
while read -r pair; do
    [[ -n "$pair" ]] || continue
    retired_id="$(sed -E 's/"([a-z-]+)".*/\1/' <<<"$pair")"
    retired_target="$(sed -E 's/.*"([a-z-]+)"$/\1/' <<<"$pair")"
    retired_count=$((retired_count + 1))

    if ! grep -qx "$retired_target" <<<"$leaves"; then
        fail "the retired id \"$retired_id\" resolves to \"$retired_target\", which is not a leaf, so everyone still holding it silently lands on Home"
    fi
    if grep -qx "$retired_id" <<<"$leaves"; then
        fail "\"$retired_id\" is listed as retired and is also a live leaf, so resolve() answers with the retired target instead of the page itself"
    fi
done <<<"$retired_pairs"

# ── Every leaf resolves everywhere ───────────────────────────────────────────
while read -r page; do
    [[ -n "$page" ]] || continue

    # Home is the switch's default arm rather than a case, since it is also
    # where an unknown page falls back to.
    if [[ "$page" != "home" ]]; then
        grep -qE "case \"$page\": return [a-zA-Z]+;" "$shell_file" \
            || fail "SettingsRoutes offers \"$page\" but SettingsShell has no case for it, so opening it shows Home"
    fi
done <<<"$leaves"

# ── ShellState defers to the taxonomy instead of restating it ────────────────
grep -q 'SettingsRoutes\.resolve(' "$shell_state" \
    || fail 'showSettings() does not route through SettingsRoutes.resolve, so a category id or an unknown page has no defined destination'
grep -q 'const allowed = \[' "$shell_state" \
    && fail 'ShellState has grown a literal allow-list again -- it will drift from SettingsRoutes, and a page missing from it silently redirects to Home'

# ── Every routed component is declared and registered ────────────────────────
# The case arms name a Component id; each must have a declaration, and the type
# it instantiates must appear in qmldir.
while read -r component; do
    [[ -n "$component" ]] || continue

    declaration="$(grep -oE "Component \{ id: $component; [A-Za-z]+ \{\} \}" "$shell_file")" \
        || fail "SettingsShell routes to \"$component\" but never declares it"

    type_name="$(sed -E 's/.*; ([A-Za-z]+) \{\} \}/\1/' <<<"$declaration")"
    grep -qE "^$type_name [0-9.]+ $type_name\.qml$" "$qmldir" \
        || fail "$type_name is not registered in modules/settings/qmldir -- it will fail to load as \"not a type\", and the whole settings window fails with it"

    [[ -r "$settings_dir/$type_name.qml" ]] \
        || fail "$type_name is registered in qmldir but $type_name.qml does not exist"
done < <({
    grep -oE 'case "[a-z-]+": return [a-zA-Z]+;' "$shell_file" | sed -E 's/.*return ([a-zA-Z]+);/\1/'
    grep -oE 'default: return [a-zA-Z]+;' "$shell_file" | sed -E 's/.*return ([a-zA-Z]+);/\1/'
} | sort -u)

# ── Every page file is reachable ─────────────────────────────────────────────
# A page nobody can navigate to is dead code that still has to compile. The
# scaffold SettingsPage.qml is the one file here that is a base class rather
# than a page.
while read -r page_file; do
    type_name="$(basename "$page_file" .qml)"
    [[ "$type_name" == "SettingsPage" ]] && continue

    grep -qE "; $type_name \{\} \}" "$shell_file" \
        || fail "$type_name.qml exists but nothing in SettingsShell instantiates it"
done < <(find "$settings_dir" -maxdepth 1 -name '*Page.qml')

printf 'settings nav contract: PASS (%d categories, %d leaves, %d retired ids)\n' \
    "$(grep -c . <<<"$category_ids")" "$(grep -c . <<<"$leaves")" "$retired_count"
