#!/usr/bin/env python3
"""WCAG 2.1 contrast audit for makeover theme files.

Reads any makeover .toml (surface/content/action/status/line/
category sections), converts hex to WCAG 2.1 relative luminance,
and reports pass/fail against AA-text (>= 4.5) and AA-UI (>= 3.0)
for every affordance-carrying token pair.

Also computes Alloy's derived tokens (border-subtle, border-strong)
via mix formulas and audits those too, so a makeover file that
was authored without Alloy's discipline still gets a full report.

Usage:
    python3 tools/wcag_audit.py <path/to/theme.toml>

Example:
    python3 tools/wcag_audit.py https://git.sr.ht/~maxmj/makeover/tree/main/item/themes/akari-dawn.toml
"""
import sys
import os

# ---------------------------------------------------------------- toml load

def _load_toml(path):
    try:
        import tomllib
    except ImportError:
        try:
            import tomli as tomllib  # noqa
        except ImportError:
            sys.exit("need tomllib (Python 3.11+) or `pip install --user tomli`")
    with open(path, "rb") as f:
        return tomllib.load(f)

# ---------------------------------------------------------------- color math

def hex_to_srgb(h):
    """#rrggbb -> (r, g, b) in [0, 1] sRGB (gamma-encoded)."""
    h = h.strip().lstrip("#")
    if len(h) != 6:
        raise ValueError(f"expected 6-hex color, got {h!r}")
    r = int(h[0:2], 16) / 255.0
    g = int(h[2:4], 16) / 255.0
    b = int(h[4:6], 16) / 255.0
    return r, g, b

def _linearize(c):
    """sRGB gamma -> linear sRGB per WCAG 2.1."""
    return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4

def relative_luminance_hex(h):
    r, g, b = (_linearize(c) for c in hex_to_srgb(h))
    return 0.2126 * r + 0.7152 * g + 0.0722 * b

def contrast(a_hex, b_hex):
    Ya = relative_luminance_hex(a_hex)
    Yb = relative_luminance_hex(b_hex)
    lo, hi = sorted((Ya, Yb))
    return (hi + 0.05) / (lo + 0.05)

def mix_hex(a_hex, b_hex, t):
    """Linear-sRGB mix. t=0 => a, t=1 => b. Returns #rrggbb."""
    ar, ag, ab = (_linearize(c) for c in hex_to_srgb(a_hex))
    br, bg, bb = (_linearize(c) for c in hex_to_srgb(b_hex))
    mr = ar + (br - ar) * t
    mg = ag + (bg - ag) * t
    mb = ab + (bb - ab) * t
    def _delinearize(c):
        return 12.92 * c if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055
    r = round(_delinearize(mr) * 255)
    g = round(_delinearize(mg) * 255)
    b = round(_delinearize(mb) * 255)
    return "#{:02x}{:02x}{:02x}".format(max(0, min(255, r)),
                                        max(0, min(255, g)),
                                        max(0, min(255, b)))

# ---------------------------------------------------------------- derivation

def derive(theme):
    """Compute Alloy's extended tokens from a makeover theme.

    makeover ships one border tone (line.border); Alloy renders
    three tiers via mix. Formula lives here (not in the theme file)
    so any makeover .toml downloaded from the wild gets a full
    Alloy-shaped token map.
    """
    border  = theme["line"]["border"]
    surface = theme["surface"]["page"]
    primary = theme["content"]["primary"]
    # border-strong needs 3.0:1 against page for focus rings / selected
    # rows. Themes vary widely in border softness; mixing 65% toward
    # text gets there on both crisp (dark border) and soft (Akari-tier)
    # borders. border-subtle is decorative — 60% toward surface reads
    # as "hint of a divider" without adding contrast.
    return {
        "border-subtle": mix_hex(border, surface, 0.60),
        "border-strong": mix_hex(border, primary, 0.65),
    }

# ---------------------------------------------------------------- reporting

TEXT_TARGET = 4.5
UI_TARGET   = 3.0

def _tag(ratio):
    if ratio >= 7.0: return "AAA"
    if ratio >= 4.5: return "AA-text"
    if ratio >= 3.0: return "AA-UI"
    return "sub-3"

def _row(name, ratio, target):
    status = "PASS" if ratio >= target else "FAIL"
    print(f"  {status}  {ratio:6.2f}:1  [{_tag(ratio):8s}]  {name}")

def audit(theme_path):
    theme = _load_toml(theme_path)
    meta  = theme.get("meta", {})
    surf  = theme["surface"]
    text  = theme["content"]
    line  = theme["line"]
    act   = theme["action"]
    stat  = theme["status"]
    derived = derive(theme)

    name    = meta.get("name", os.path.basename(theme_path))
    variant = meta.get("variant", "?")
    print(f"\n============ {name} ({variant}) ============\n")

    # Text on surfaces
    print("Text on surfaces (target >= 4.5 for text; muted target 3.0)")
    for key in ("primary", "secondary", "muted"):
        target = UI_TARGET if key == "muted" else TEXT_TARGET
        for s_key in ("page", "raised", "sunken", "overlay"):
            r = contrast(text[key], surf[s_key])
            _row(f"content.{key} on surface.{s_key}", r, target)

    # Borders on surfaces
    print("\nBorders on surfaces (border-strong target 3.0; others decorative)")
    for b_name, b_hex in (
        ("border-strong", derived["border-strong"]),
        ("line.border",   line["border"]),
        ("border-subtle", derived["border-subtle"]),
    ):
        target = UI_TARGET if b_name == "border-strong" else 0.0
        for s_key in ("page", "raised", "overlay"):
            r = contrast(b_hex, surf[s_key])
            _row(f"{b_name} on surface.{s_key}", r, target)

    # Accents on surfaces
    print("\nAccents on surfaces (target >= 4.5 text, or >= 3.0 for glyphs)")
    accents = [("action.primary", act["primary"])]
    accents += [(f"status.{k}", stat[k]) for k in ("danger", "success", "warning", "info")]
    for a_name, a_hex in accents:
        for s_key in ("page", "raised", "sunken", "overlay"):
            r = contrast(a_hex, surf[s_key])
            _row(f"{a_name} on surface.{s_key}", r, TEXT_TARGET)

    # Surface elevation deltas (perceptual)
    print("\nSurface elevation deltas (perceptual; not WCAG)")
    tiers = ("sunken", "page", "raised", "overlay")
    for a, b in zip(tiers, tiers[1:]):
        Ya = relative_luminance_hex(surf[a])
        Yb = relative_luminance_hex(surf[b])
        r  = contrast(surf[a], surf[b])
        print(f"  surface.{a:8s} -> surface.{b:8s}  ratio {r:5.2f}   dY {Yb-Ya:+.4f}")

    # Derived tokens (for downstream consumers wanting to eyeball)
    print(f"\nDerived tokens:")
    print(f"  border-subtle  = {derived['border-subtle']}   (mix border, surface.page 60%)")
    print(f"  border-strong  = {derived['border-strong']}   (mix border, content.primary 65%)")

if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("usage: wcag_audit.py <path/to/theme.toml>")
    audit(sys.argv[1])
