# SPDX-License-Identifier: Apache-2.0
"""Build original play cards, text alternatives, eight A4 aids and a local helper.

Default operation renders into a temporary directory and checks reproduction.
Use --write deliberately for initial generation or reviewed source changes.
"""

from __future__ import annotations

import argparse
import hashlib
import html
import json
import os
import shutil
import subprocess
import tempfile
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
INK = "#193e47"
EPOCH = "1790726400"
AIDS = {
    "play-board": (
        "A place of my own",
        "One part is enough. Make it, tell it, or direct a maker.",
        (
            "One large empty rounded rectangular space, with no scene, child choice "
            "or character supplied. Use any part for a chosen imaginary place. "
            "An empty part can have a deliberately agreed meaning, or remain unchosen."
        ),
    ),
    "visitor-cards": (
        "A visitor, or my own idea",
        "Look at broad shapes. No cutting or naming needed.",
        (
            "Three supplied broad shape suggestions across the upper row: an open "
            "round shape, a tall open rectangle and an open triangle. Three large "
            "empty rounded spaces below can carry own ideas. No face, job or pupil "
            "choice is supplied. Shapes have no universal cultural meaning."
        ),
    ),
    "route-map": (
        "A way through my place",
        "Choose a start and somewhere to go, or stay still.",
        (
            "One large empty rectangular map. A blank Start strip and a blank "
            "Somewhere to go strip sit underneath; neither names a location. "
            "No route, obstacle, result or shortest path is supplied. Use broad "
            "marks, description or adult-directed tracing in an imagined place."
        ),
    ),
    "message-board": (
        "A little message",
        "A mark, picture, word, sign or another way can carry it.",
        (
            "One large empty message area above two smaller empty areas labelled "
            "Its meaning here and For whom / private. No words, child message "
            "or recipient are filled. The adult can read labels and record an "
            "agreed meaning; no writing or letter-formation task is required."
        ),
    ),
    "sort-mat": (
        "Groups for my purpose",
        "Choose the meanings. A thing can fit both, or stay out.",
        (
            "Two equal large empty rounded group areas side by side, each with "
            "a separate blank meaning strip underneath. A wide empty shared / "
            "not chosen yet area sits below. No objects or grouping rules are "
            "supplied; there is no required two-group completion."
        ),
    ),
    "change-panels": (
        "Keep a part, change a part",
        "A different version does not have to be better.",
        (
            "Two equal large empty rounded panels side by side, labelled One "
            "idea and A possible change. Neither contains a scene, earlier child "
            "work, action, result or improvement. A broad blank strip below is "
            "available for a wanted unchanged part, with none required."
        ),
    ),
    "little-guide": (
        "One useful thing for a visitor",
        "Use one panel or more. No folding or reading test.",
        (
            "Three equal tall empty panels across the page. No scene, order, "
            "message or finished guide is supplied. One panel is a useful "
            "start; all may stay empty. The page need not be folded, cut, "
            "written on, shared or completed to take part in the play."
        ),
    ),
    "repeat-menu": (
        "What shall we do next?",
        "Keep, change, different or finish. You choose.",
        (
            "Four equal broad boxes in a two-by-two arrangement: Keep upper "
            "left, Change upper right, Different lower left, Finish lower right. "
            "Each has ample empty space. No selected option or reward is shown. "
            "An adult can name the options; the child's usual communication or "
            "own alternative can choose. Finish stays available throughout."
        ),
    ),
}


def word(x: int, y: int, value: str, size: int = 22, bold: bool = False) -> str:
    """Return selectable, escaped vector text."""
    return (
        f'<text x="{x}" y="{y}" font-family="DejaVu Sans" font-size="{size}" '
        f'font-weight="{700 if bold else 400}" fill="{INK}">'
        f"{html.escape(value)}</text>"
    )


def box(x: int, y: int, width: int, height: int, label: str = "") -> str:
    """Draw one broad open area with an optional adult-readable label."""
    result = (
        f'<rect x="{x}" y="{y}" width="{width}" height="{height}" rx="14" '
        f'fill="white" stroke="{INK}" stroke-width="3"/>'
    )
    if label:
        result += word(x + 18, y + 36, label, 20, True)
    return result


def aid_svg(stem: str) -> str:
    """Draw simple open aids with specific linear alternatives and no answers."""
    title, instruction, description = AIDS[stem]
    body = ""
    if stem == "play-board":
        body = box(44, 173, 706, 818)
    elif stem == "visitor-cards":
        body = (
            f'<circle cx="161" cy="331" r="77" fill="white" stroke="{INK}" '
            'stroke-width="4"/>'
            + box(330, 220, 134, 220)
            + f'<path d="M631 220 L717 440 L545 440 Z" fill="white" '
            f'stroke="{INK}" stroke-width="4"/>'
        )
        for x in (44, 286, 528):
            body += box(x, 530, 222, 440)
    elif stem == "route-map":
        body = (
            box(44, 173, 706, 573)
            + box(44, 776, 706, 92, "Start · your own place")
            + box(44, 897, 706, 92, "Somewhere to go · or stay")
        )
    elif stem == "message-board":
        body = (
            box(44, 173, 706, 571)
            + box(44, 779, 706, 92, "Its meaning here")
            + box(44, 901, 706, 92, "For whom / private")
        )
    elif stem == "sort-mat":
        body = (
            box(44, 173, 339, 509)
            + box(411, 173, 339, 509)
            + box(44, 712, 339, 90, "My group meaning")
            + box(411, 712, 339, 90, "My group meaning")
            + box(44, 839, 706, 154, "Shared / not chosen yet")
        )
    elif stem == "change-panels":
        body = (
            box(44, 173, 339, 646, "One idea")
            + box(411, 173, 339, 646, "A possible change")
            + box(44, 857, 706, 136, "Anything to keep? · none needed")
        )
    elif stem == "little-guide":
        for x in (44, 286, 528):
            body += box(x, 173, 222, 820)
    elif stem == "repeat-menu":
        for x, y, label in (
            (44, 173, "Keep"),
            (411, 173, "Change"),
            (44, 594, "Different"),
            (411, 594, "Finish"),
        ):
            body += box(x, y, 339, 397, label)
    return (
        '<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" '
        'viewBox="0 0 794 1123" role="img">'
        f"<title>{html.escape(title)}</title><desc>{html.escape(description)}</desc>"
        '<rect width="794" height="1123" fill="white"/>'
        + word(44, 68, title, 29, True)
        + word(44, 115, instruction, 19)
        + body
        + word(44, 1054, "Small world, big ideas · original open play aid", 17)
        + word(
            44,
            1090,
            "© NeuroForgeIO Pty Ltd 2026 · CC BY 4.0 · adult-supported play",
            12,
        )
        + "</svg>\n"
    )


def card_text(card: dict) -> str:
    """A complete parent card is rebuilt from the authored, adaptable source."""
    lines = [
        f"# {card['id']:02d} · {card['title']}",
        "",
        "About ten minutes if useful. Pause, repeat, change or finish anytime.",
        "",
        "## Say this, or use your own words",
        "",
        card["opening"],
        "",
        f"**More words, only if useful:** {card['invitation']}",
        "",
        "## Ready in a moment",
        "",
        card["ready"],
        "",
        "## A loose play path",
        "",
        *[f"{i}. {step}" for i, step in enumerate(card["steps"], 1)],
        "",
        "## Choose the useful route",
        "",
        *[f"- {route}" for route in card["routes"]],
        "",
        "## Two ideas to borrow or change",
        "",
        *[f"- {example}" for example in card["examples"]],
        "",
        "## Notice, then choose the next move",
        "",
        card["notice"],
        "",
        f"**Later, with or without this session:** {card['later']}",
        "",
        f"**More play, only if wanted:** {card['extra']}",
        "",
        (
            f"**Optional aid:** [{AIDS[card['aid']][0]}](../print/{card['aid']}.pdf) "
            f"· [exact text alternative](../print/TEXT-ALTERNATIVES.md#{card['aid']})"
        ),
        "",
        f"**Adult intention:** {card['aim']}",
        "",
        (
            "[All starts](../README.md) · [Parent guide](../PARENT-GUIDE.md) "
            "· [Play helper](../digital/index.html) · [Sources and limits](../SOURCE-NOTES.md)"
        ),
        "",
        "© NeuroForgeIO Pty Ltd 2026 · CC BY 4.0. Adapt freely with credit.",
    ]
    return "\n".join(lines) + "\n"


def snapshot() -> dict:
    """Bind authored bytes, sizes and modification times outside caches."""
    return {
        str(p.relative_to(HERE)): (
            hashlib.sha256(p.read_bytes()).hexdigest(),
            p.stat().st_size,
            p.stat().st_mtime_ns,
        )
        for p in HERE.rglob("*")
        if p.is_file() and not {"__pycache__", ".ruff_cache"}.intersection(p.parts)
    }


def generate(target: Path) -> list[Path]:
    """Render actual temporary PDFs and regenerate exact cards/helper copies."""
    data = json.loads((HERE / "play-path.json").read_text())
    outputs = []
    for folder in ("cards", "print", "digital"):
        (target / folder).mkdir(parents=True, exist_ok=True)
    for card in data["starts"]:
        path = target / "cards" / f"{card['slug']}.md"
        path.write_text(card_text(card))
        outputs.append(path)
    readaloud = [
        "# Read-aloud invitations",
        "",
        (
            "Ten original scripts for a familiar adult to read or adapt. No generated "
            "narration or listening review is claimed. Words can be signed or "
            "translated by a competent speaker using the child's familiar language."
        ),
    ]
    alternatives = [
        "# Exact print alternatives",
        "",
        (
            "The stored A4 PDFs contain selectable text but are untagged. These "
            "linear alternatives carry every instruction and meaningful layout. "
            "An adult can describe them, draw broad spaces or offer an oral/sign "
            "route; no printing, physical handling or visual discrimination required."
        ),
    ]
    for card in data["starts"]:
        readaloud += [
            "",
            f"## {card['id']:02d} · {card['title']}",
            "",
            f"**Open with:** {card['opening']}",
            "",
            f"**Optional prompts:** {card['invitation']}",
        ]
    readaloud_path = target / "READ-ALOUDS.md"
    readaloud_path.write_text("\n".join(readaloud) + "\n")
    outputs.append(readaloud_path)
    env = {**os.environ, "SOURCE_DATE_EPOCH": EPOCH, "TZ": "UTC"}
    for stem, (title, instruction, description) in AIDS.items():
        svg = target / "print" / f"{stem}.svg"
        pdf = target / "print" / f"{stem}.pdf"
        svg.write_text(aid_svg(stem))
        ET.fromstring(svg.read_bytes())
        subprocess.run(
            ["rsvg-convert", "--format=pdf", "--output", str(pdf), str(svg)],
            env=env,
            check=True,
            capture_output=True,
        )
        outputs += [svg, pdf]
        alternatives += [
            "",
            f'<a id="{stem}"></a>',
            "",
            f"## {title}",
            "",
            instruction,
            "",
            description,
            "",
            (
                "Footer: Small world, big ideas · original open play aid. "
                "© NeuroForgeIO Pty Ltd 2026 · CC BY 4.0 · adult-supported play."
            ),
        ]
    alt_path = target / "print" / "TEXT-ALTERNATIVES.md"
    alt_path.write_text("\n".join(alternatives) + "\n")
    outputs.append(alt_path)
    template = (HERE / "digital" / "index.template.html").read_text()
    linked = "\n".join(
        f'<li><a href="../cards/{c["slug"]}.md">{c["id"]:02d} · '
        f"{html.escape(c['title'])}</a><p>{html.escape(c['opening'])}</p></li>"
        for c in data["starts"]
    )
    helper = template.replace("__ALL_STARTS__", linked).replace(
        "__CARD_DATA__", json.dumps(data, ensure_ascii=True).replace("<", "\\u003c")
    )
    helper_path = target / "digital" / "index.html"
    helper_path.write_text(helper)
    outputs.append(helper_path)
    return outputs


def main() -> None:
    """Run the explicitly selected authoring or read-only verification mode."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--write", action="store_true")
    args = parser.parse_args()
    before = snapshot()
    with tempfile.TemporaryDirectory(prefix="subjectnest-small-world-build-") as tmp:
        target = Path(tmp)
        paths = generate(target)
        for path in paths:
            stored = HERE / path.relative_to(target)
            if args.write:
                stored.parent.mkdir(parents=True, exist_ok=True)
                if not stored.exists() or stored.read_bytes() != path.read_bytes():
                    shutil.copyfile(path, stored)
            elif not stored.exists() or stored.read_bytes() != path.read_bytes():
                raise AssertionError(f"Reproduction drift: {stored.relative_to(HERE)}")
    if not args.write:
        assert snapshot() == before, "Read-only build changed authored files"
    print(json.dumps({"genuine_outputs": len(paths), "write": args.write}))


if __name__ == "__main__":
    main()
