#!/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 same argument, one level up: the THEME resolver ──────────────────────
#
# An accent is one colour. A theme is the whole palette, and the two shell
# generators both need the active one -- panama-theme-apps to render kitty,
# GTK, btop, tmux, Vicinae and Firefox, panama-lock to render the lock screen.
# They answered that question with their own tables of Tokyo Night literals
# until the resolution moved into panama-palette beside accent_hex, for exactly
# the reason recorded above. link-dotfiles is the third caller: it seeds the
# lock screen at install time.

themes="$repo_dir/config/dot/quickshell/config/themes.json"
linker="$repo_dir/setup/scripts/link-dotfiles"

grep -q 'themes.json' "$helper" || note 'the shell palette helper does not read themes.json, so nothing shared resolves a theme'
for consumer in "$theme_apps" "$lock"; do
    grep -q 'resolve_theme' "$consumer" \
        || note "$(basename "$consumer") does not resolve the active theme through the shared function"
done
grep -q 'render_hyprlock' "$linker" \
    || note 'link-dotfiles seeds the lock screen without the shared renderer, so it carries its own colour table again'

# ── The resolver answers with a complete theme ───────────────────────────────

if [[ -r "$themes" ]] && jq -e . "$themes" >/dev/null 2>&1; then
    resolver_fixture="$(mktemp -d /tmp/panama-theme-resolve.XXXXXX)"
    trap 'rm -rf "$resolver_fixture"' EXIT
    mkdir -p "$resolver_fixture/panama"

    while read -r id; do
        scheme="$(jq -r --arg id "$id" '.themes[] | select(.id == $id) | .scheme' "$themes")"
        jq -n --arg id "$id" '{themeProfileId: $id}' >"$resolver_fixture/panama/settings.json"
        resolved="$(XDG_CONFIG_HOME="$resolver_fixture" \
            bash -c "source '$helper'; resolve_theme '$scheme'")"

        [[ "$(jq -r '.id' <<<"$resolved")" == "$id" ]] \
            || note "the resolver does not find the shipped theme '$id'"
        # 19 palette tokens and 16 ansi keys, or a generator writes a config
        # with a colour of "" in it.
        [[ "$(jq -r '.palette | length' <<<"$resolved")" == "19" ]] \
            || note "'$id' resolves with $(jq -r '.palette | length' <<<"$resolved") palette keys rather than 19"
        [[ "$(jq -r '.ansi | length' <<<"$resolved")" == "16" ]] \
            || note "'$id' resolves with $(jq -r '.ansi | length' <<<"$resolved") ansi keys rather than 16"
    done < <(jq -r '.themes[].id' "$themes")

    # A custom carrying only an accent -- which is every profile saved before
    # themes existed. It must keep its accent AND inherit a full palette.
    jq -n '{themeProfileId: "bare",
            themeProfiles: [{id: "bare", name: "Bare", scheme: "dark",
                             accent: "#ff0088", secondary: "#00ffcc"}]}' \
        >"$resolver_fixture/panama/settings.json"
    bare="$(XDG_CONFIG_HOME="$resolver_fixture" bash -c "source '$helper'; resolve_theme dark")"
    [[ "$(jq -r '.accent' <<<"$bare")" == "#ff0088" ]] \
        || note 'a custom theme without a palette loses its own accent'
    default_bg="$(jq -r --arg id "$(jq -r '.defaultDark' "$themes")" \
        '.themes[] | select(.id == $id) | .palette.bg' "$themes")"
    [[ "$(jq -r '.palette.bg' <<<"$bare")" == "$default_bg" ]] \
        || note 'a custom theme without a palette does not inherit the scheme default, so every profile saved before themes existed resolves to no colours at all'

    # An unknown id must land on the scheme default rather than on an empty
    # record: a settings file naming a deleted theme is a normal state.
    jq -n '{themeProfileId: "no-such-theme"}' >"$resolver_fixture/panama/settings.json"
    unknown="$(XDG_CONFIG_HOME="$resolver_fixture" bash -c "source '$helper'; resolve_theme light")"
    [[ "$(jq -r '.id' <<<"$unknown")" == "$(jq -r '.defaultLight' "$themes")" ]] \
        || note "an unknown theme id resolves to '$(jq -r '.id' <<<"$unknown")' rather than to the light default"

    # The per-mode keys are the selection; themeProfileId is the fallback.
    jq -n --arg dark "$(jq -r '[.themes[] | select(.scheme == "dark")][-1].id' "$themes")" \
        '{themeProfileId: "day", themeDark: $dark}' >"$resolver_fixture/panama/settings.json"
    per_mode="$(XDG_CONFIG_HOME="$resolver_fixture" bash -c "source '$helper'; resolve_theme dark")"
    [[ "$(jq -r '.id' <<<"$per_mode")" == "$(jq -r '[.themes[] | select(.scheme == "dark")][-1].id' "$themes")" ]] \
        || note 'themeDark does not win over themeProfileId, so a scheme flip lands on the wrong theme'
fi

# ── 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")"
