#!/usr/bin/env bash

# The theme catalog is the one source of truth for every shipped palette, and
# three consumers read it three ways: ThemeCatalog.qml with FileView, the
# render pipeline with jq, and this contract with Node. What breaks silently:
#
#   - a theme missing a palette key renders as QML's "undefined" black
#   - moon/day drifting from the shell's pre-theme literals repaints every
#     machine that never chose a theme
#   - ThemeCatalog.qml's embedded fallback drifting from the catalog means the
#     shell renders differently for the instant before the file loads
#
# Nothing at runtime cross-checks any of that. This does, statically.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
shell_dir="$repo_dir/config/dot/quickshell"
catalog="$shell_dir/config/themes.json"
catalog_qml="$shell_dir/services/ThemeCatalog.qml"
model="$shell_dir/services/ThemeProfileModel.js"

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

[ -f "$catalog" ] || fail "themes.json is missing"
[ -f "$catalog_qml" ] || fail "ThemeCatalog.qml is missing"

node - "$catalog" "$model" "$catalog_qml" <<'EOF'
const fs = require("fs");
const [catalogPath, modelPath, qmlPath] = process.argv.slice(2);
const model = require(modelPath);
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));

const fail = message => { console.error("theme catalog contract: " + message); process.exit(1); };

// ── Shape ────────────────────────────────────────────────────────────────
const themes = catalog.themes ?? [];
if (themes.length !== 10)
    fail(`expected 10 shipped themes, found ${themes.length}`);
const ids = new Set();
for (const theme of themes) {
    if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(theme.id ?? ""))
        fail(`bad theme id: ${theme.id}`);
    if (ids.has(theme.id))
        fail(`duplicate theme id: ${theme.id}`);
    ids.add(theme.id);
    if (theme.scheme !== "dark" && theme.scheme !== "light")
        fail(`${theme.id}: bad scheme`);
    // The model's validators are the authority on key sets and hex shape —
    // if they reject a shipped theme, the shell would fall back silently.
    if (!model.normalizePalette(theme.palette))
        fail(`${theme.id}: palette rejected by ThemeProfileModel.normalizePalette`);
    if (!model.normalizeAnsi(theme.ansi))
        fail(`${theme.id}: ansi rejected by ThemeProfileModel.normalizeAnsi`);
    for (const color of [theme.accent, theme.secondary])
        if (!/^#[0-9a-f]{6}$/.test(color ?? ""))
            fail(`${theme.id}: bad accent pair`);
}
if (!ids.has(catalog.defaultDark) || !ids.has(catalog.defaultLight))
    fail("defaultDark/defaultLight name unknown themes");
const darkCount = themes.filter(theme => theme.scheme === "dark").length;
if (darkCount !== 6 || themes.length - darkCount !== 4)
    fail(`expected 6 dark and 4 light themes, found ${darkCount}/${themes.length - darkCount}`);

// ── moon and day are the shell's pre-theme literals ──────────────────────
// These exact values were Theme.qml's ternaries before themes existed; a
// machine that never chose a theme must keep rendering byte-identically.
const pinned = {
    moon: { bg: "#222436", bgDark: "#1e2030", bgHighlight: "#2f334d",
        bgPanel: "#2e2f3d", bgPopover: "#21212f", fg: "#c8d3f5",
        fgDim: "#828bb8", fgMuted: "#636da6", gutter: "#3b4261",
        accentAlt: "#65bcff", cyan: "#86e1fc", teal: "#4fd6be",
        green: "#c3e88d", yellow: "#ffc777", orange: "#ff966c",
        red: "#ff757f", redDeep: "#c53b53", magenta: "#c099ff",
        pink: "#fca7ea", accent: "#82aaff", secondary: "#b172b0" },
    day: { bg: "#e1e2e7", bgDark: "#d3d5de", bgHighlight: "#c4c8da",
        bgPanel: "#d9dae3", bgPopover: "#eaeaee", fg: "#3760bf",
        fgDim: "#6172b0", fgMuted: "#848cb5", gutter: "#a8aecb",
        accentAlt: "#007197", cyan: "#007197", teal: "#118c74",
        green: "#587539", yellow: "#8c6c3e", orange: "#b15c00",
        red: "#f52a65", redDeep: "#c64343", magenta: "#9854f1",
        pink: "#d20065", accent: "#2e7de9", secondary: "#9854f1" }
};
for (const [id, expected] of Object.entries(pinned)) {
    const theme = themes.find(entry => entry.id === id);
    if (!theme)
        fail(`shipped theme ${id} is missing`);
    for (const [key, value] of Object.entries(expected)) {
        const actual = key === "accent" || key === "secondary" ? theme[key] : theme.palette[key];
        if (actual !== value)
            fail(`${id}.${key} drifted: ${actual} (expected ${value})`);
    }
}

// ── The QML fallback carries moon and day byte-identically ───────────────
// ThemeCatalog.qml embeds the two defaults for the instant before FileView
// loads; every pinned hex above must appear in the QML, and every hex in the
// fallback block must exist somewhere in the catalog's moon/day records.
const qml = fs.readFileSync(qmlPath, "utf8");
for (const [id, expected] of Object.entries(pinned))
    for (const [key, value] of Object.entries(expected))
        if (!qml.includes(value))
            fail(`ThemeCatalog.qml fallback is missing ${id}.${key} = ${value}`);
if (!/fallbackThemes/.test(qml))
    fail("ThemeCatalog.qml no longer declares fallbackThemes");

// ── The model and the catalog agree on the token sets ────────────────────
const paletteKeys = Object.keys(themes[0].palette).sort();
if (JSON.stringify(paletteKeys) !== JSON.stringify([...model.PALETTE_KEYS].sort()))
    fail("themes.json palette keys diverge from ThemeProfileModel.PALETTE_KEYS");
const ansiKeys = Object.keys(themes[0].ansi).sort();
if (JSON.stringify(ansiKeys) !== JSON.stringify([...model.ANSI_KEYS].sort()))
    fail("themes.json ansi keys diverge from ThemeProfileModel.ANSI_KEYS");

console.log(`theme catalog contract: ok (${themes.length} themes, ${model.PALETTE_KEYS.length} palette keys)`);
EOF

# Theme.qml must read every palette token from the resolver, never a literal
# ternary — a reintroduced literal would silently stop following themes.
if grep -E 'readonly property color (bg|fg|gutter|cyan|teal|green|yellow|orange|red|magenta|pink)[A-Za-z]*:.*root\.dark \?' \
        "$shell_dir/config/Theme.qml" >/dev/null; then
    fail "Theme.qml has regrown a hardcoded palette ternary"
fi
grep -q 'ThemeProfiles.activePalette' "$shell_dir/config/Theme.qml" \
    || fail "Theme.qml no longer reads ThemeProfiles.activePalette"

printf 'theme catalog contract: ok\n'
