#!/usr/bin/env python3
"""Generate `spec/api/capability-matrix.md` from `xy.styling.capabilities`.

The committed table is an artifact, never hand-edited. `--check` fails when it
has fallen behind the registry, and `tests/test_capability_registry.py` runs
that check — so the document cannot drift from the registry, and the registry
cannot drift from `styles.py`.

    uv run python scripts/gen_capability_matrix.py --write
    uv run python scripts/gen_capability_matrix.py --check
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "python"))

from xy.styling import capabilities as caps  # noqa: E402

REPORT = ROOT / "spec" / "api" / "capability-matrix.md"
PUBLIC = ROOT / "docs" / "styling" / "capabilities.md"


def _shipped_renderer_claim() -> str:
    """How the summary may describe shipped properties, read from the registry.

    The sentence used to hard-code "drawn by all three renderers". If a shipped
    property ever lands `partial` somewhere, that prose would overclaim in the
    generated document — the exact failure the registry exists to prevent.
    """
    shipped = [p for p in caps.MARK_STYLE_PROPERTIES if p.status == "shipped"]
    if all(level == "full" for prop in shipped for level in prop.support.values()):
        return "drawn by all three renderers"
    partial = sorted(prop.id for prop in shipped if any(v != "full" for v in prop.support.values()))
    return "not all drawn identically everywhere — see " + ", ".join(f"`{p}`" for p in partial)


def render() -> str:
    """The engineering-facing capability matrix, as markdown."""
    counts = caps.summary()
    claim = _shipped_renderer_claim()
    lines = [
        "# Capability matrix",
        "",
        "<!-- generated by scripts/gen_capability_matrix.py --write; do not edit -->",
        "",
        "What XY can be styled and extended with, per renderer. Generated from",
        "`python/xy/styling/capabilities.py`, which `tests/test_capability_registry.py`",
        "pins to `styles.py` and `dom.py`, so it cannot list a property the",
        "implementation does not compile or omit one it does.",
        "",
        "`full` means the renderer draws the property as specified. `partial` means it",
        "draws something the notes have to qualify. `none` means it does not draw it —",
        "which is sometimes deliberate, and the notes say which.",
        "",
        "## In one line",
        "",
        f"- **{counts['mark_style_properties_shipped']}** mark style properties across "
        f"**{counts['mark_kinds']}** mark kinds, {claim}.",
        f"- **{counts['chart_slots']}** stable chrome slots, CSS- and Tailwind-addressable "
        "in the browser; "
        f"**{counts['slots_styleable_natively']}** of them reach the native writers — "
        "nine through `styles={slot: ...}` itself, and `root` through the "
        "chart-level `style=` token bag.",
        f"- **{counts['extension_points_shipped']}** shipped extension point.",
        f"- **{counts['known_renderer_divergences']}** known default divergence between "
        "renderers, listed below rather than left to be discovered.",
        "",
        "## Mark style properties",
        "",
        "The subset a mark's `style=` mapping accepts. Anything outside it raises",
        "before data is ingested, so no renderer silently drops a declaration another",
        "one honors.",
        "",
    ]
    lines += caps.markdown_mark_property_table()
    lines += ["", "### Notes", ""]
    for prop in caps.MARK_STYLE_PROPERTIES:
        if prop.notes:
            lines.append(f"- **`{prop.id}`** — {prop.notes}")
    lines += [
        "",
        "## Chrome slots",
        "",
        "Stable `data-xy-slot` names that accept `class_names=` and `styles=` in the",
        "browser. The native raster and vector writers have no cascade, so they read a",
        "property subset rather than a stylesheet: the chart-level `style=` token bag,",
        "plus `styles={slot: ...}` for the slots that name chrome a file actually",
        "contains. `class_names=` reaches neither — a class selects a rule out of a",
        "stylesheet an exported file does not have. That boundary is",
        "contracted in [export.md](export.md) §9 and pinned by",
        "`tests/test_export_style_survival.py`.",
        "",
    ]
    lines += caps.markdown_slot_table()
    lines += ["", "### Notes", ""]
    for slot in caps.CHART_SLOTS:
        if slot.notes:
            lines.append(f"- **`{slot.id}`** (via `{slot.channel}`) — {slot.notes}")
    lines += [
        "",
        "## Extension points",
        "",
        "Ways to add behavior the core does not ship, without forking it.",
        "",
    ]
    lines += caps.markdown_extension_table()
    lines += ["", "### Notes", ""]
    for point in caps.EXTENSION_POINTS:
        lines.append(f"- **{point.id}** — {point.notes}")
    lines += [
        "",
        "## Known renderer divergences",
        "",
        "Differences in the *defaults*, which no style property selects — invisible",
        "until someone diffs two exports. Listed here for that reason.",
        "",
        "| what | webgl | svg | native | visible when | tracked by |",
        "|---|---|---|---|---|---|",
    ]
    for div in caps.KNOWN_RENDERER_DIVERGENCES:
        lines.append(
            f"| {div.what} | {div.webgl} | {div.svg} | {div.native} | "
            f"{div.visible_when} | {div.tracked_by} |"
        )
    lines += [
        "",
        "## Regenerating",
        "",
        "```bash",
        "uv run python scripts/gen_capability_matrix.py --write",
        "```",
        "",
        "`--check` fails if this file is stale, and the test suite runs it.",
        "",
    ]
    return "\n".join(lines)


def render_public() -> str:
    """The same tables, as a public docs page.

    Generated from the same registry as the spec version rather than written
    twice: a hand-kept public copy is exactly the drift this whole chain exists
    to prevent.
    """
    counts = caps.summary()
    claim = _shipped_renderer_claim()
    lines = [
        "---",
        "title: Capability Matrix",
        "description: What XY can be styled and extended with, per renderer, "
        "generated from the capability registry.",
        "---",
        "",
        "# Capability Matrix",
        "",
        "<!-- generated by scripts/gen_capability_matrix.py --write; do not edit -->",
        "",
        "Every styling question about XY has the same two halves: *can I change this*,",
        "and *does the change survive where I need it*. This page answers both from the",
        "registry the implementation is checked against.",
        "",
        f"- **{counts['mark_style_properties_shipped']}** mark style properties across "
        f"**{counts['mark_kinds']}** mark kinds, {claim}.",
        f"- **{counts['chart_slots']}** stable chrome slots for CSS and Tailwind in the browser.",
        f"- **{counts['extension_points_shipped']}** way to add a mark kind XY does not "
        "ship, without forking it.",
        "",
        "## Mark style properties",
        "",
        "What a mark's `style=` accepts. Anything outside this raises while the chart is",
        "built — one renderer never silently ignores what another draws.",
        "",
    ]
    lines += caps.markdown_mark_property_table()
    lines += ["", "### Notes", ""]
    for prop in caps.MARK_STYLE_PROPERTIES:
        if prop.notes:
            lines.append(f"- **`{prop.id}`** — {prop.notes}")
    lines += [
        "",
        "## Chrome slots",
        "",
        "Stable `data-xy-slot` names that take `class_names=` and `styles=`. The native",
        "raster and vector writers have no cascade, so per-slot styling is a browser",
        "mechanism; put anything that must survive export in the chart-level `style=`",
        "token bag or in mark and axis `style=`, which every renderer reads.",
        "",
    ]
    lines += caps.markdown_slot_table()
    lines += ["", "### Notes", ""]
    for slot in caps.CHART_SLOTS:
        if slot.notes:
            lines.append(f"- **`{slot.id}`** (via `{slot.channel}`) — {slot.notes}")
    lines += [
        "",
        "## Extension points",
        "",
        "See [Custom Marks](/docs/xy/advanced/custom-marks/) for the worked example.",
        "",
    ]
    lines += caps.markdown_extension_table()
    lines += [
        "",
        "## Where renderers still disagree",
        "",
        "Differences in the defaults that no property selects. They are listed because",
        "an undocumented difference reads as a bug; a documented one is a contract.",
        "",
        "| what | browser | svg | native png | visible when |",
        "|---|---|---|---|---|",
    ]
    for div in caps.KNOWN_RENDERER_DIVERGENCES:
        lines.append(
            f"| {div.what} | {div.webgl} | {div.svg} | {div.native} | {div.visible_when} |"
        )
    lines += [
        "",
        "For what is still alpha, see",
        "[Limitations and Alpha Status](/docs/xy/api-reference/limitations-and-alpha-status/).",
        "",
    ]
    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    """Print, write, or check the generated capability documents."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--write", action="store_true", help="regenerate the committed matrix")
    parser.add_argument(
        "--check", action="store_true", help="fail if the committed matrix is stale"
    )
    parser.add_argument("--json", action="store_true", help="print the summary counts as JSON")
    args = parser.parse_args(argv)

    if args.json:
        print(json.dumps(caps.summary(), indent=2))
        return 0

    targets = ((REPORT, render()), (PUBLIC, render_public()))
    if args.write:
        for path, document in targets:
            path.write_text(document, encoding="utf-8")
            print(f"wrote {path.relative_to(ROOT)}")
        return 0
    if args.check:
        stale = [
            path
            for path, document in targets
            if (path.read_text(encoding="utf-8") if path.exists() else "") != document
        ]
        if stale:
            names = ", ".join(str(p.relative_to(ROOT)) for p in stale)
            print(
                f"{names} is stale; run scripts/gen_capability_matrix.py --write",
                file=sys.stderr,
            )
            return 1
        print("capability matrix is current")
        return 0
    print(targets[0][1])
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
