"""A starter set of token ROLES, offered when a design system is created.

WHY THIS EXISTS
---------------
A literal gets written into a stylesheet when there is no role to reach for.
That is the mechanism, and this codebase produced a clean demonstration of it:
the house style had no "text on a filled colour" role, so 76 call sites wrote
a pure-white literal — not out of defiance, but because nothing existed to write
instead (#2275). The correction was not a better ban list. It was declaring the
missing role.

So the useful moment is CREATION. A system whose roles are named on day one
never presents the occasion for a literal, and never needs a list of values it
forbids.

WHAT SHIPS AND WHAT DOES NOT (rule #115)
----------------------------------------
The ROLES ship: `surface-page`, `text-primary`, `action-destructive` are
generic CSS-design vocabulary, not one operator's kit. Every install that has a
page has a page background.

The VALUES never ship. Each token is created with an empty `value_by_mode`, so
a fresh system is a set of named, deliberately-unanswered questions. No hex
appears anywhere in this file, and none should ever be added to it — a default
palette would be this operator's palette wearing product clothes.

A valueless token is already legible downstream: `render_stylesheet` emits it as
a commented-out declaration in its group (#2299), and `stylesheet_for_system`
reports it under `valueless`. So a blank role reads as "to be decided" rather
than as breakage, without anything new.

THE PREFIX IS THE INSTALL'S
---------------------------
`--fs-` is FabledSword's convention, not the product's. The prefix is a
parameter with a neutral default; a caller that has a house convention passes
it. Baking `--fs-` in would put one family's naming into every install.

FLAT, NOT PRESET
----------------
One list, every group individually skippable, all on by default (operator's
call, 2026-08-03). Presets keyed to app shape — web / CLI / docs — were
considered and rejected: they would require the product to hold opinions about
app categories, and a wrong category is worse than a generic list someone
prunes once.
"""
from __future__ import annotations

DEFAULT_TOKEN_PREFIX = "--ds-"

# group -> (what the group is for, ((role suffix, purpose), ...))
#
# Purposes are written as the QUESTION the operator is answering, because that
# is what an unfilled role is. "Page background, the deepest surface" tells you
# what to put there; "Colour 1" does not.
STARTER_ROLE_GROUPS: dict[str, tuple[str, tuple[tuple[str, str], ...]]] = {
    "surface": (
        "Backgrounds, by elevation",
        (
            ("surface-page", "Page background, the deepest surface"),
            ("surface-raised", "Cards and raised elements"),
            ("surface-hover", "Hovered surfaces, secondary elevation"),
        ),
    ),
    "text": (
        "Foreground colours, by emphasis",
        (
            ("text-primary", "Primary text on a page or raised surface"),
            ("text-secondary", "Secondary text and captions"),
            ("text-tertiary", "Hints and metadata"),
            # The role whose absence caused 76 literals. It is in the starter
            # set deliberately: text on a filled colour is NOT the page text
            # colour, because the surface under it does not change with the
            # mode while the page does.
            ("text-on-action", "Text on a filled colour — buttons, badges"),
        ),
    ),
    "action": (
        "What the user can do — kept separate from the accent, which is identity",
        (
            ("action-primary", "The confirming action: Save, Submit"),
            ("action-secondary", "Non-destructive alternates"),
            ("action-destructive", "Irreversible actions — delete, revoke"),
        ),
    ),
    "semantic": (
        "What the system is telling you",
        (
            ("success", "Something worked"),
            ("warning", "Something needs attention"),
            ("error", "Something failed — distinct from destructive"),
            ("info", "Neutral information"),
        ),
    ),
    "border": (
        "Boundaries and dividers",
        (
            ("border-color", "The line colour itself"),
            ("border", "The default structural border, as a shorthand"),
            ("border-hover", "Border on hover or emphasis"),
            ("border-active", "Selected or current — the one border that may carry the accent"),
        ),
    ),
    "accent": (
        "This install's identity — not its actions",
        (
            ("accent", "The single signature colour"),
            ("accent-soft", "Tinted backgrounds — pills, tags"),
            ("accent-faint", "The faintest wash"),
        ),
    ),
    "radius": (
        "Corner rounding",
        (
            ("radius-sm", "Pills, tags, code spans"),
            ("radius-md", "Buttons, inputs, small cards"),
            ("radius-lg", "Cards, panels, modals"),
        ),
    ),
    "space": (
        "The spacing scale — a gap not on the scale is a decision to justify",
        tuple((f"space-{i}", f"Spacing step {i}") for i in range(1, 11)),
    ),
    "motion": (
        "Transition timing — motion supports the interaction, never performs",
        (
            ("ease", "The one easing curve, used by every transition"),
            ("dur-fast", "Hovers, colour and border changes"),
            ("dur-base", "Most state changes"),
            ("dur-slow", "Larger surface or layout shifts"),
        ),
    ),
    "state": (
        "Cross-cutting states that are otherwise improvised per view",
        (
            ("disabled-opacity", "Opacity for disabled controls"),
            ("overlay", "Scrim behind modals and dialogs"),
        ),
    ),
}

ALL_GROUPS: tuple[str, ...] = tuple(STARTER_ROLE_GROUPS)


def starter_tokens(
    groups: list[str] | tuple[str, ...] | None = None,
    prefix: str = DEFAULT_TOKEN_PREFIX,
) -> list[dict]:
    """Token rows for the chosen groups — names and purposes only, no values.

    `groups` of None means every group; an empty list means none, which is a
    real answer and not the same as None. An operator who wants three tokens
    should be able to get three.

    Unknown group names are ignored rather than raising: this feeds a
    checkbox list, and a stale name from an older client should not fail a
    creation that is otherwise fine.
    """
    chosen = ALL_GROUPS if groups is None else [g for g in groups if g in STARTER_ROLE_GROUPS]
    rows: list[dict] = []
    for group in chosen:
        _, roles = STARTER_ROLE_GROUPS[group]
        for index, (suffix, purpose) in enumerate(roles, start=1):
            rows.append({
                "name": f"{prefix}{suffix}",
                "group_name": group,
                "purpose": purpose,
                # Empty, not absent: the column is NOT NULL with a {} default,
                # so absence has exactly one spelling here as it does there.
                "value_by_mode": {},
                "order_index": index,
            })
    return rows


def describe_groups() -> list[dict]:
    """The catalogue, for a UI to render as a checklist."""
    return [
        {
            "group": group,
            "description": description,
            "token_count": len(roles),
            "names": [suffix for suffix, _ in roles],
        }
        for group, (description, roles) in STARTER_ROLE_GROUPS.items()
    ]
