#!/usr/bin/env bash

# The eight accents, and the one place they are written down.
#
# They used to be written down five times: ThemeProfileModel.js for QML,
# hypr/looks.lua for the compositor, and again in panama-theme-apps and
# panama-lock. panama-theme-apps said so in its own comment -- "there is no
# shared source between QML and a shell script" -- which is an accurate
# description of a bug waiting to happen. Adding a ninth accent meant editing
# five files, and the one most likely to be missed was the lock screen, which
# fails silently: the machine locks in a stale colour and nothing says why.
#
# config/palette.json is now that source. Everything outside QML reads it:
# looks.lua through prefs.readJson, the two shell generators through
# scripts/panama-palette.
#
# QML still carries the table, because a .js module imported into QML cannot
# read a file. That is a copy, and copies drift, so this contract exists to
# make the drift a failing test instead of a wrong lock screen. It compares
# them value by value rather than checking that both merely exist.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
palette="$repo_dir/config/dot/quickshell/config/palette.json"
model="$repo_dir/config/dot/quickshell/services/ThemeProfileModel.js"
helper="$repo_dir/config/dot/quickshell/scripts/panama-palette"
looks="$repo_dir/config/dot/hypr/looks.lua"
theme_apps="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps"
lock="$repo_dir/config/dot/quickshell/scripts/panama-lock"

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

[[ -r "$palette" ]] || { printf 'palette contract: %s is missing\n' "$palette" >&2; exit 1; }
jq -e . "$palette" >/dev/null 2>&1 || { printf 'palette contract: palette.json is not valid JSON\n' >&2; exit 1; }

# ── The QML table and the palette agree, value by value ──────────────────────

python3 - "$palette" "$model" <<'PY' || note 'the QML accent table and config/palette.json disagree; see the lines above'
import json, re, sys

palette = json.load(open(sys.argv[1]))["accents"]
source = open(sys.argv[2], encoding="utf-8").read()
block = source[source.index("var CURATED = {"):]
block = block[:block.index("\n};") + 3]

qml = {}
for match in re.finditer(
        r'(\w+):\s*\{\s*dark:\s*"#([0-9a-f]{6})",\s*darkSecondary:\s*"#([0-9a-f]{6})",'
        r'\s*light:\s*"#([0-9a-f]{6})",\s*lightSecondary:\s*"#([0-9a-f]{6})",'
        r'\s*label:\s*"([^"]*)",\s*gnome:\s*"([^"]*)"', block):
    name, dark, dark2, light, light2, label, gnome = match.groups()
    qml[name] = {"dark": dark, "darkSecondary": dark2, "light": light,
                 "lightSecondary": light2, "label": label, "gnome": gnome}

problems = []
for name in sorted(set(palette) | set(qml)):
    if name not in palette:
        problems.append(f"{name}: in the QML table but not in palette.json")
        continue
    if name not in qml:
        problems.append(f"{name}: in palette.json but not in the QML table")
        continue
    for field in ("dark", "darkSecondary", "light", "lightSecondary", "label", "gnome"):
        if palette[name].get(field) != qml[name].get(field):
            problems.append(
                f"{name}.{field}: palette.json has {palette[name].get(field)!r}, "
                f"the QML table has {qml[name].get(field)!r}")

if problems:
    print("\n".join("  " + p for p in problems), file=sys.stderr)
    raise SystemExit(1)
PY

# ── Every consumer reads it rather than restating it ─────────────────────────

grep -q 'palette.json' "$helper" || note 'the shell palette helper does not read palette.json'
grep -q 'panama-palette' "$theme_apps" || note 'panama-theme-apps does not use the shared palette helper'
grep -q 'panama-palette' "$lock" || note 'panama-lock does not use the shared palette helper'
grep -q 'prefs.readJson' "$looks" || note 'looks.lua does not read the palette, so the compositor has its own copy again'

# A restated table is the thing this exists to prevent. Each of these once held
# all eight; a file holding more than a single fallback pair has grown one back.
for file in "$theme_apps" "$lock"; do
    count="$(grep -cE '^\s*(orchid|teal|green|amber|orange|rose|slate)\)' "$file" || true)"
    (( count == 0 )) || note "$(basename "$file") has an accent table again ($count entries)"
done
looks_accents="$(grep -cE '^\s+(orchid|teal|green|amber|orange|rose|slate)\s*=' "$looks" || true)"
(( looks_accents == 0 )) || note "looks.lua has an accent table again ($looks_accents entries)"

# ── The helper resolves what the palette says ────────────────────────────────

while read -r name; do
    for scheme in dark light; do
        want="$(jq -r --arg n "$name" --arg s "$scheme" '.accents[$n][$s]' "$palette")"
        got="$(bash -c "source '$helper'; accent_hex '$name' '$scheme'")"
        [[ "$got" == "$want" ]] \
            || note "the helper resolves $name/$scheme as '$got'; the palette says '$want'"
    done
done < <(jq -r '.accents | keys[]' "$palette")

# An unknown accent must fall back rather than produce an empty string, which
# would reach sed and generate a config with a colour of "".
fallback="$(bash -c "source '$helper'; accent_hex 'not-an-accent' dark")"
[[ "$fallback" == "$(jq -r '.accents[.default].dark' "$palette")" ]] \
    || note "an unknown accent resolves to '$fallback' rather than the default"
empty="$(bash -c "source '$helper'; accent_hex '' dark")"
[[ -n "$empty" ]] || note 'an empty accent name resolves to an empty string, which would reach a config generator'

# ── Lua reads it too ─────────────────────────────────────────────────────────

if command -v lua >/dev/null 2>&1; then
    lua_result="$(cd "$repo_dir/config/dot/hypr" && PANAMA_TEST_PALETTE="$palette" lua -e '
        package.path = "./?.lua;" .. package.path
        local prefs = require("prefs")
        local parsed = prefs.readJson(os.getenv("PANAMA_TEST_PALETTE"))
        local accents = parsed.accents
        if not accents then print("none") os.exit(0) end
        local count = 0
        for _ in pairs(accents) do count = count + 1 end
        print(count .. " " .. tostring(accents.orchid and accents.orchid.dark))
    ' 2>/dev/null)"
    expected_count="$(jq -r '.accents | length' "$palette")"
    expected_orchid="$(jq -r '.accents.orchid.dark' "$palette")"
    [[ "$lua_result" == "$expected_count $expected_orchid" ]] \
        || note "Lua reads the palette as '$lua_result', expected '$expected_count $expected_orchid'"

    # A palette that cannot be read must cost the colours and nothing else.
    lua_missing="$(cd "$repo_dir/config/dot/hypr" && PANAMA_TEST_PALETTE="/nonexistent/palette.json" lua -e '
        package.path = "./?.lua;" .. package.path
        local prefs = require("prefs")
        local parsed = prefs.readJson(os.getenv("PANAMA_TEST_PALETTE"))
        print(type(parsed) == "table" and "table" or "raised")
    ' 2>&1)"
    [[ "$lua_missing" == "table" ]] \
        || note 'reading a missing palette does not return an empty table, so a bad file would break the compositor config'
fi

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

printf 'palette contract: PASS (%s accents, one source)\n' "$(jq -r '.accents | length' "$palette")"
