#!/usr/bin/env python3
"""Whole-swap theme asset + CSS self-containment (BACKLOG #62).

waybar.nix applies themes/<slug>/waybar.css *raw* — no palette @define-color
block is prepended. A whole-swap that references @text without defining it
is broken at apply time (neon-glass, 2026-07-09). This gate fails that class
of bug before it ships.

Also asserts every themes/*.json has preview.png + non-empty backgrounds/.

Wired as `checks.theme-wholeswap` in flake.nix; runnable directly:
    python3 tools/check-theme-wholeswap.py themes/
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

# CSS at-rules that look like @name but are not GTK/CSS color names.
AT_RULES = frozenset(
    {
        "keyframes",
        "media",
        "import",
        "supports",
        "font-face",
        "charset",
        "page",
        "define-color",
        "layer",
        "container",
        "property",
        "scope",
    }
)

COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
DEFINE_RE = re.compile(r"@define-color\s+([A-Za-z_][\w-]*)")
# Color uses: @name, alpha(@name, …) — same token shape as defines.
AT_REF_RE = re.compile(r"@([A-Za-z_][\w-]*)")


def strip_comments(css: str) -> str:
    return COMMENT_RE.sub("", css)


def check_waybar_css(path: Path, failures: list[str]) -> None:
    raw = path.read_text(encoding="utf-8")
    css = strip_comments(raw)
    defs = set(DEFINE_RE.findall(css))
    refs = set(AT_REF_RE.findall(css)) - AT_RULES - defs
    # define-color lines also match AT_REF_RE for the rule name only via
    # @define-color — already in AT_RULES. Color names on define lines
    # appear as the capture of DEFINE_RE only when after define-color;
    # AT_REF_RE on "@define-color foo" yields "define-color", not "foo".
    # On "color: @text" yields "text". Good.
    if refs:
        missing = ", ".join(sorted(refs))
        failures.append(f"{path}: undefined color refs: {missing}")


def check_assets(themes_dir: Path, failures: list[str]) -> None:
    jsons = sorted(themes_dir.glob("*.json"))
    if not jsons:
        failures.append(f"{themes_dir}: no themes/*.json found")
        return
    for jp in jsons:
        slug = jp.stem
        tdir = themes_dir / slug
        preview = tdir / "preview.png"
        if not preview.is_file():
            failures.append(f"{slug}: missing {preview.relative_to(themes_dir)}")
        bg = tdir / "backgrounds"
        if not bg.is_dir():
            failures.append(f"{slug}: missing backgrounds/")
        elif not any(bg.iterdir()):
            failures.append(f"{slug}: backgrounds/ is empty")


def main() -> int:
    if len(sys.argv) != 2:
        print(f"usage: {sys.argv[0]} <themes-dir>", file=sys.stderr)
        return 2
    themes_dir = Path(sys.argv[1])
    if not themes_dir.is_dir():
        print(f"not a directory: {themes_dir}", file=sys.stderr)
        return 2

    failures: list[str] = []
    check_assets(themes_dir, failures)
    for css in sorted(themes_dir.glob("*/waybar.css")):
        check_waybar_css(css, failures)

    if failures:
        for f in failures:
            print(f"theme-wholeswap: {f}", file=sys.stderr)
        print(f"theme-wholeswap: {len(failures)} failure(s)", file=sys.stderr)
        return 1

    n_css = len(list(themes_dir.glob("*/waybar.css")))
    n_json = len(list(themes_dir.glob("*.json")))
    print(
        f"theme-wholeswap: ok — {n_json} presets (preview+backgrounds), "
        f"{n_css} whole-swap waybar.css self-contained"
    )
    return 0


if __name__ == "__main__":
    sys.exit(main())
