#!/usr/bin/env bash

# What each keybind is FOR.
#
# The cheatsheet and the Shortcuts settings page both group by this, and until
# now the grouping was guessed from the description by substring matching --
# which put "Close window" and "Close the notification list" in the same group
# and left anything phrased unusually in whichever bucket matched first.
#
# hypr/keybinds.lua now says it outright. The sections of that file already ARE
# the categories, so a section sets one and every bind below it inherits it,
# which keeps the annotation to one line per section rather than one per bind.
# That is cheap, and it has exactly one failure mode worth testing: a section
# that forgets to set its category silently inherits the previous section's,
# and the binds land somewhere plausible-looking but wrong. That is what
# happened to the media keys the first time this was written -- the dictation
# section sits in the middle of them and its category leaked onto everything
# below it.
#
# So this checks the manifest the compositor actually produced, not the source.
# Generating it needs a running Hyprland; without one, the structural checks
# still run.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
keybinds="$repo_dir/config/dot/hypr/keybinds.lua"
service="$repo_dir/config/dot/quickshell/services/Keybinds.qml"
manifest="${XDG_STATE_HOME:-$HOME/.local/state}/panama/keybind-categories.json"

findings=()
note() { findings+=("$1"); }

# ── The source says what it is doing ─────────────────────────────────────────

grep -q 'local function category' "$keybinds" \
    || note 'keybinds.lua has no category() marker, so nothing records what a bind is for'
grep -q 'categories\[chord\]' "$keybinds" \
    || note 'the bind wrapper does not record a category'
grep -q 'write_categories()' "$keybinds" \
    || note 'the manifest is never written'

# Writing must never cost the keymap. A read-only state directory is a bad day,
# not a machine without shortcuts.
grep -q 'if file == nil then' "$keybinds" \
    || note 'the manifest writer does not tolerate being unable to open the file'

# The user's own shortcuts are a category too, and they are the one group whose
# membership is not written in this file. They are emitted last and outside the
# `bind` wrapper -- deliberately, so keybindOverrides (which is keyed by a
# SHIPPED chord) can never reach one -- which means they would be invisible to
# the manifest unless the loop records the category itself.
grep -q 'category("Custom")' "$keybinds" \
    || note 'custom shortcuts are emitted without a category, so they land in Other'
grep -q 'customBinds' "$keybinds" \
    || note 'keybinds.lua never reads customBinds, so shortcuts the user invents are not bound'

# The shell prefers the authored category and still works without one.
grep -q 'categoryManifest' "$service" \
    || note 'the shell never reads the category manifest'
grep -q 'onLoadFailed: root.categoryManifest' "$service" \
    || note 'a missing manifest is not handled, so a machine that has not reloaded its compositor would break'

# ── Every section sets a category ────────────────────────────────────────────
#
# Walk the file: each `-- ── Name ──` header should be followed by a category()
# call before the next bind(). A header with binds under it and no category
# between is a section inheriting the previous one's, which is the mistake.

python3 - "$keybinds" <<'PY' || note 'a section of keybinds.lua has binds but never sets a category, so they inherit the section above'
import re, sys
lines = open(sys.argv[1], encoding="utf-8").read().splitlines()
pending_header = None
seen_category = True
problems = []
for number, line in enumerate(lines, 1):
    if re.match(r"^-- ── ", line):
        pending_header = (number, line)
        seen_category = False
        continue
    if re.match(r"^category\(", line):
        seen_category = True
        continue
    if re.match(r"^bind\(", line) and not seen_category and pending_header:
        problems.append(f"line {pending_header[0]}: {pending_header[1][:60]}")
        seen_category = True
if problems:
    print("\n".join(problems), file=sys.stderr)
    raise SystemExit(1)
PY

# ── The manifest the compositor produced ─────────────────────────────────────

if ! command -v hyprctl >/dev/null 2>&1 || ! hyprctl version >/dev/null 2>&1; then
    printf 'keybind categories contract: PASS (structure only; no running compositor)\n'
    exit 0
fi

[[ -r "$manifest" ]] || {
    # A reload regenerates it. Ask for one rather than failing on a machine
    # that simply has not reloaded since this landed.
    hyprctl reload >/dev/null 2>&1
    sleep 2
}
[[ -r "$manifest" ]] || { note 'the compositor produced no category manifest'; }

if [[ -r "$manifest" ]]; then
    total="$(jq 'length' "$manifest" 2>/dev/null || echo 0)"
    (( total > 100 )) || note "the manifest holds $total categories; the keymap has well over a hundred binds"

    # Every category is one the shell knows how to order. A typo produces a
    # group that sorts last and looks like a bug in the cheatsheet.
    #
    # Custom is the one group the user fills: shortcuts they invented, read
    # from customBinds at the end of keybinds.lua. It is legitimately empty on
    # a machine nobody has customized and legitimately full on one somebody
    # has, so unlike Other it carries no count expectation -- only the
    # requirement that it be a name the shell orders (Keybinds.qml's groupOrder
    # puts it first) rather than an unknown that sorts last.
    known='Windows Workspaces Applications Shell Session Media & hardware Custom Other'
    while read -r value; do
        [[ -n "$value" ]] || continue
        grep -qF "$value" <<<"$known" \
            || note "the manifest contains an unknown category: $value"
    done < <(jq -r '[.[]] | unique | .[]' "$manifest" 2>/dev/null)

    # Nothing should land in Other: it is the default for a bind written above
    # the first category() call, which means somebody added a section without
    # one.
    others="$(jq -r '[to_entries[] | select(.value == "Other") | .key] | join(", ")' "$manifest" 2>/dev/null)"
    [[ -n "$others" && "$others" != "" ]] \
        && note "these binds have no category and fell back to Other: $others"

    # Spot checks. Chosen because each one sits at a boundary where the
    # category is inherited rather than obvious, which is where this breaks.
    check() {
        local chord="$1" want="$2"
        local got
        got="$(jq -r --arg c "$chord" '.[$c] // ""' "$manifest" 2>/dev/null)"
        [[ "$got" == "$want" ]] \
            || note "$chord is categorised as '${got:-nothing}', expected '$want'"
    }
    check "SUPER + Q" "Windows"
    check "ALT + 1" "Workspaces"
    check "SUPER + T" "Applications"
    check "CTRL + ALT + L" "Session"
    check "SUPER + D" "Shell"
    # The two that leaked the first time: both sit below the dictation section,
    # which is physically in the middle of the media binds.
    check "XF86AudioPlay" "Media & hardware"
    check "XF86MonBrightnessUp" "Media & hardware"
    # And the hardware application keys at the very end of the file.
    check "XF86Calculator" "Applications"
fi

if (( ${#findings[@]} > 0 )); then
    printf 'keybind categories contract: %d finding(s)\n' "${#findings[@]}" >&2
    printf '  - %s\n' "${findings[@]}" >&2
    exit 1
fi

printf 'keybind categories contract: PASS\n'
