#!/usr/bin/env bash

# Adding a settings page means editing four separate files, and missing one
# fails quietly rather than loudly:
#
#   SettingsSidebar.qml   the row you click
#   SettingsShell.qml     the case that maps that row to a component, AND the
#                         Component declaration itself
#   ShellState.qml        the allow-list openSettings() checks -- a page missing
#                         here silently redirects to Home, so a deep link or a
#                         search result lands on the wrong page with no error
#   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
#
# Nothing at runtime cross-checks the four. This does, statically.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
settings_dir="$repo_dir/config/dot/quickshell/modules/settings"
sidebar="$settings_dir/SettingsSidebar.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 "$sidebar" "$shell_file" "$qmldir" "$shell_state"; do
    [[ -r "$required" ]] || fail "cannot read $required"
done

# ── Every sidebar row resolves everywhere ────────────────────────────────────
pages="$(grep -oE '\{ page: "[a-z-]+"' "$sidebar" | sed 's/.*"\(.*\)"/\1/')"
[[ -n "$pages" ]] || fail 'no pages found in the sidebar -- this contract is not reading it correctly'

allowed_line="$(grep -m1 'const allowed = \[' "$shell_state")" \
    || fail 'could not find the allow-list in ShellState'

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 "the sidebar offers \"$page\" but SettingsShell has no case for it, so clicking it shows Home"
    fi

    grep -qF "\"$page\"" <<<"$allowed_line" \
        || fail "\"$page\" is missing from ShellState's allow-list, so openSettings(\"$page\") silently redirects to Home"
done <<<"$pages"

# ── 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\n'
