# SPDX-License-Identifier: Apache-2.0
"""Reproduce nine original A4 SVG/PDF source aids and exact alternatives."""
from __future__ import annotations

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

from PIL import ImageFont

HERE = Path(__file__).resolve().parent
INK = "#183743"
FILLS = ("#e7f2f6", "#edf4e8", "#fff0de", "#f0eafa", "#eef0f2")
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"


def text(x: float, y: float, value: str, size: int = 20, bold: bool = False) -> str:
    """Return visible selectable text with a shared dark ink colour."""
    return f'<text x="{x}" y="{y}" font-family="DejaVu Sans" font-size="{size}" font-weight="{700 if bold else 400}" fill="{INK}">{escape(value)}</text>'


def box(x: float, y: float, w: float, h: float, fill: str = "white") -> str:
    """Draw a clearly outlined panel; colour is supplementary."""
    return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="12" fill="{fill}" stroke="{INK}" stroke-width="3"/>'


def path(d: str, width: int = 4) -> str:
    """Draw an original rounded line illustration."""
    return f'<path d="{d}" fill="none" stroke="{INK}" stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round"/>'


def circle(x: float, y: float, r: float, fill: str = "white") -> str:
    """Draw an outlined or dark filled circular mark."""
    return f'<circle cx="{x}" cy="{y}" r="{r}" fill="{fill}" stroke="{INK}" stroke-width="3"/>'


def paragraph(
    value: str,
    x: int,
    y: int,
    width: int = 640,
    size: int = 20,
    leading: int = 30,
    bold: bool = False,
) -> tuple[str, int]:
    """Wrap measured text within a panel and return the next baseline."""
    font = ImageFont.truetype(BOLD if bold else FONT, size)
    rows: list[str] = []
    current = ""
    for word in value.split():
        candidate = f"{current} {word}".strip()
        if font.getlength(candidate) > width and current:
            rows.append(current)
            current = word
        else:
            current = candidate
    rows.append(current)
    return (
        "".join(
            text(x, y + i * leading, row, size, bold) for i, row in enumerate(rows)
        ),
        y + len(rows) * leading,
    )


def block(label: str, value: str, y: int, h: int, fill: str, size: int = 20) -> str:
    """Place an exact source quotation in an outlined labelled panel."""
    out = box(48, y, 698, h, fill) + text(68, y + 34, label, 18, True)
    body, end = paragraph(value, 68, y + 70, 655, size, 29 if size >= 19 else 23)
    if end > y + h + 5:
        raise ValueError(f"{label}: text exceeds panel")
    return '<g data-panel="true">' + out + body + "</g>"


def page(title: str, desc: str, body: str) -> str:
    """Wrap a semantic SVG with A4 geometry and original-material attribution."""
    return "\n".join(
        [
            '<svg xmlns="http://www.w3.org/2000/svg" width="210mm" height="297mm" viewBox="0 0 794 1123" role="img" aria-labelledby="title desc">',
            f'<title id="title">{escape(title)}</title>',
            f'<desc id="desc">{escape(desc)}</desc>',
            '<rect width="794" height="1123" fill="white"/>',
            text(42, 53, "SUBJECTNEST · FOUNDATION HASS · W39–40", 17, True),
            text(42, 95, title, 25, True),
            paragraph(desc, 42, 130, 705, 16, 23)[0],
            body,
            path("M42 1038 L752 1038", 2),
            text(
                42,
                1064,
                "Original teaching material · SubjectNest by NeuroForgeIO Pty Ltd",
                12,
            ),
            text(
                42,
                1085,
                "© 2026 · CC BY 4.0 · exact text/tactile alternatives in pack",
                12,
            ),
            "</svg>",
            "",
        ]
    )


SOURCES = {
    "A": "Story Day Four · Morning Bell · Paper Room · view from inside the closed pane. The invented drawing shows a square planter, one tall leaf and a plain patch above it. These are drawn marks, not an actual weather record.",
    "B": "Story Day Nine · Morning Bell · Paper Room · same closed-pane view and planter cue. The invented drawing shows a square planter, two tall leaves and three drop marks outside. It does not say why the marks differ or what a real season is.",
    "C": "I am Teo. The Paper Room planter matters to me because I like looking at its leaf shapes. This is my own reason, not everyone’s. In this paper model, keep the pane shut and return source copies to the envelope.",
    "D": "I am the paper maker. I made both drawings for practice. I have not observed local rain, plant growth, a season, Country/Place or any child using this folder.",
    "E": "This undated model fragment says PLANTER. Its record says an adult supplied the word. No first answer, actual child, earlier date or second matching response is available. It cannot show a child improved.",
    "F": "Choose a plain tab, flat folder or spoken guide. Ask before sharing a source copy. Keep private work closed. Return copies to the envelope. This paper request is not actual permission or a viewer reply.",
}
FRESH = {
    "gate": [
        "Story Day Two · Paper Gate · closed-pane view beside the square opening. An invented drawing shows one cloud mark above the opening. It is not actual local weather.",
        "Story Day Six · Paper Gate · the same closed-pane view beside the square opening. An invented drawing shows two cloud marks. No cause or season is reported.",
        "I am Sera. The opening matters to me because I like a quiet look from inside. This is my own reason, not everyone’s.",
        "No earlier child work is available. Choose a source tab or spoken guide; ask before sharing copies. No viewer reply or actual permission is supplied.",
    ],
    "nook": [
        "Story Day Three · Paper Nook · front view from inside. The invented drawing shows two dot marks above a low shelf.",
        "Story Day Nine · Paper Nook · side view from inside. The invented drawing shows one dot mark beside a low shelf. The views differ; no weather or change report is supplied.",
        "I am Jori. The shelf matters to me because I like finding its paper books. This is my own reason. Keep copies in their envelope in this paper model.",
        "An undated model label says SHELF; an adult gave the word. No earlier child answer is available. Making a folder now does not show year-long growth, real use or Country/Place knowledge.",
    ],
}


def view(kind: str, x: int, y: int, w: int, h: int) -> str:
    """Draw source-keyed model marks with explicit bounded view labels."""
    out = box(x, y, w, h, FILLS[0]) + f'<g data-view-kind="{kind}">'
    out += box(x + 18, y + 18, w - 36, h - 36)
    if kind in ("first", "second"):
        side = min(h * 0.36, w * 0.25)
        px = x + w * 0.35
        py = y + h * 0.52
        out += f'<rect data-object="square-planter" x="{px}" y="{py}" width="{side}" height="{side}" fill="white" stroke="{INK}" stroke-width="3"/>'
        count = 1 if kind == "first" else 2
        for i in range(count):
            xx = px + side * (i + 1) / (count + 1)
            out += f'<path data-object="leaf" d="M{xx} {py} Q{xx-18} {py-45} {xx} {py-68} Q{xx+18} {py-45} {xx} {py}" fill="white" stroke="{INK}" stroke-width="3"/>'
        if kind == "second":
            for i in range(3):
                xx = x + w * 0.70 + i * 22
                yy = y + h * 0.25
                out += f'<path data-object="drop" d="M{xx} {yy} Q{xx-10} {yy+14} {xx} {yy+18} Q{xx+10} {yy+14} {xx} {yy} Z" fill="white" stroke="{INK}" stroke-width="2"/>'
    elif kind.startswith("gate"):
        side = min(h * 0.25, w * 0.23)
        px = x + w * 0.30
        py = y + h * 0.55
        out += f'<rect data-object="square-opening" x="{px}" y="{py}" width="{side}" height="{side}" fill="white" stroke="{INK}" stroke-width="3"/>'
        for i in range(1 if kind == "gate1" else 2):
            xx = x + w * 0.35 + i * 60
            yy = y + h * 0.40
            out += f'<path data-object="cloud" d="M{xx-19} {yy+12} Q{xx-30} {yy-2} {xx-14} {yy-4} Q{xx-5} {yy-22} {xx+6} {yy-7} Q{xx+27} {yy-12} {xx+25} {yy+12} Z" fill="white" stroke="{INK}" stroke-width="2"/>'
    else:
        px = x + w * 0.23
        py = y + h * 0.61
        out += f'<rect data-object="low-shelf" x="{px}" y="{py}" width="{w*.50}" height="{h*.14}" fill="white" stroke="{INK}" stroke-width="3"/>'
        if kind == "nook1":
            for i in range(2):
                out += f'<circle data-object="dot" cx="{x+w*.37+i*35}" cy="{y+h*.36}" r="8" fill="white" stroke="{INK}" stroke-width="2"/>'
        else:
            out += f'<circle data-object="dot" cx="{x+w*.84}" cy="{y+h*.61}" r="8" fill="white" stroke="{INK}" stroke-width="2"/>'
    return out + "</g>"


def blank(n: int, label: str, value: str, y: int, h: int) -> str:
    out = (
        box(48, y, 698, h, FILLS[(n - 1) % 5])
        + circle(80, y + 34, 18)
        + text(74, y + 40, str(n), 17, True)
        + text(112, y + 41, label, 17, True)
    )
    body, end = paragraph(value, 72, y + 80, 647, 17, 25)
    if end > y + h - 18:
        raise ValueError(label + ": blank panel overflow")
    return (
        '<g data-panel="true">'
        + out
        + body
        + path(f"M74 {y+h-19} L716 {y+h-19}", 2)
        + "</g>"
    )


def leaf(kind: str) -> str:
    k = "A" if kind == "first" else "B"
    body = block(
        k + " · INVENTED SOURCE", "“" + SOURCES[k] + "”", 194, 235, FILLS[0], 20
    )
    body += view(kind, 48, 453, 698, 265)
    body += blank(
        1,
        "SOURCE / DATE / VIEW / QUESTION",
        "Point, draw, tell or direct an adult. Keep source words, drawn marks and actual observations separate. No real weather or season recorded.",
        745,
        263,
    )
    return page(
        "First view leaf" if kind == "first" else "Second view leaf",
        "Original Paper Room drawing. Same closed-pane cue; Story Days are invented. Read the exact source.",
        body,
    )


def first() -> str:
    return leaf("first")


def second() -> str:
    return leaf("second")


def accounts() -> str:
    body = block("C · TEO SAYS", "“" + SOURCES["C"] + "”", 194, 224, FILLS[1], 20)
    body += block("D · MAKER SAYS", "“" + SOURCES["D"] + "”", 432, 184, FILLS[0], 20)
    body += box(48, 639, 698, 164, FILLS[2]) + path(
        "M95 665 L327 665 L327 758 L171 758 L126 786 L126 758 L95 758 Z M439 665 L695 665 L695 758 L545 758 L477 786 L477 758 L439 758 Z",
        3,
    )
    body += text(115, 703, "PERSON SAYS", 17, True) + text(
        461, 703, "MAKER SAYS", 17, True
    )
    body += blank(
        1,
        "ACCOUNT / OWN WORDS OR PASS",
        "Keep source and speaker. One person’s reason is not everyone’s. Actual willing account/date/scope blank unless obtained locally.",
        824,
        184,
    )
    return page(
        "Person’s words · maker’s scope",
        "Separate attributed source flaps. No personal reason or cultural identity disclosure required.",
        body,
    )


def prior() -> str:
    body = block(
        "E · UNDATED MODEL FRAGMENT", "“" + SOURCES["E"] + "”", 194, 227, FILLS[2], 20
    )
    body += box(48, 443, 698, 245, FILLS[0]) + path(
        "M101 472 L402 472 L402 650 L101 650 Z M472 486 L682 486 L682 642 L472 642 Z", 3
    )
    body += (
        text(128, 533, "PLANTER", 25, True)
        + text(128, 575, "UNDATED MODEL", 17, True)
        + text(128, 609, "ADULT GAVE WORD", 17)
        + circle(502, 521, 12)
        + text(528, 527, "EARLIER", 17, True)
        + text(489, 571, "CHILD WORK", 17, True)
        + text(482, 611, "NOT AVAILABLE", 17, True)
    )
    body += blank(
        1,
        "TODAY / HELP / EARLIER GAP",
        "Record your actual paper action and exact help. Real earlier child work only if available/selected; unknown help stays unknown. No growth proof.",
        716,
        292,
    )
    return page(
        "What work is really available?",
        "The model label is not an earlier child response. No learner/date/history has been supplied.",
        body,
    )


def pockets() -> str:
    body = ""
    for n, (label, value) in enumerate(
        [
            (
                "WHAT SOURCE SHOWS",
                "Source ID; invented or actual; date/time precision; view cue; visible fact; person says. Missing stays missing.",
            ),
            (
                "WHAT I ACTUALLY DID",
                "Today’s first words/action and exact help. Genuine earlier selected piece/date/help only if available; otherwise NOT AVAILABLE.",
            ),
            (
                "NEXT QUESTION",
                "What source/view/date/account/permission do we still need? Leave the question open; no season, cause or year grade guessed.",
            ),
        ],
        1,
    ):
        body += blank(n, label, value, 194 + (n - 1) * 271, 244)
    return page(
        "Three useful pockets",
        "Folded, flat or spoken guide. Broad fields keep source content, today’s actual work and missing evidence distinct.",
        body,
    )


def actual() -> str:
    body = ""
    items = [
        (
            "ACTUAL SOURCE / DATE / VIEW",
            "Source/owner/type; familiar-place cue; actual time precision; image capture versus viewing time; view anchor; visible/not visible; first words/help.",
        ),
        (
            "WILLING ACCOUNT / CARE",
            "Speaker/date/scope; exact or confirmed paraphrase; particular-person specialness; actual care procedure; choice/permission/observed action or pass.",
        ),
        (
            "AVAILABLE WORK / HELP",
            "Genuine earlier child-selected piece/date precision/first answer/help or NOT AVAILABLE. Today first/revised/help separate; private scope.",
        ),
        (
            "ELEMENT DECISION / HANDOVER",
            "Familiar/belonging; feature; specialness; care: source/response/gaps. Audience/current scope; action/time/help or UNOBSERVED; return/retention.",
        ),
    ]
    for n, (label, value) in enumerate(items, 1):
        body += blank(n, label, value, 194 + (n - 1) * 205, 188)
    return page(
        "Actual place record · blank",
        "Use complete educator preflight and expanded local fields. No actual source/date/person/permission/action/outcome supplied.",
        body,
    )


def cover() -> str:
    body = block("F · PAPER REQUEST", "“" + SOURCES["F"] + "”", 194, 222, FILLS[3], 20)
    body += (
        box(48, 436, 698, 202, FILLS[0])
        + path("M135 475 L659 475 L659 603 L135 603 Z M135 475 L397 572 L659 475", 3)
        + circle(397, 575, 16)
        + text(294, 462, "PRIVATE COVER", 18, True)
    )
    body += blank(
        1,
        "CHILD CHOICE / CURRENT SCOPE",
        "Tab / flat folder / spoken guide / private no-share. Actual owner/audience/copy/retention/withdrawal permission blank unless obtained.",
        654,
        175,
    )
    body += blank(
        2,
        "DONE / NOT DONE / RETURN",
        "Actual actor/time/help and recipient words only if observed/consented, else UNOBSERVED. Model replies are not actual use.",
        840,
        176,
    )
    return page(
        "Ask · close · return",
        "A plain cover keeps private evidence closed. A request is not actual permission or a viewer reply.",
        body,
    )


def fresh(kind: str) -> str:
    gate = kind == "gate"
    label = "A" if gate else "B"
    k1 = "gate1" if gate else "nook1"
    k2 = "gate2" if gate else "nook2"
    body = view(k1, 48, 194, 335, 110) + view(k2, 410, 194, 335, 110)
    body += text(
        62, 326, "TWO / SAME VIEW" if gate else "THREE / FRONT VIEW", 12, True
    ) + text(424, 326, "SIX / SAME VIEW" if gate else "NINE / SIDE VIEW", 12, True)
    y = 341
    for i, value in enumerate(FRESH[kind], 1):
        body += block(
            f"{label}{i} · EXACT INVENTED SOURCE",
            "“" + value + "”",
            y,
            162,
            FILLS[(i - 1) % 5],
            16,
        )
        y += 168
    return page(
        "Fresh " + label + " · Paper " + ("Gate" if gate else "Nook"),
        "Hold until Day "
        + ("195" if gate else "200")
        + ". Neutral source reading only; learner prompts separate after release.",
        body,
    )


def fresh_a() -> str:
    return fresh("gate")


def fresh_b() -> str:
    return fresh("nook")


PAGES = {
    "first-view-leaf": first,
    "second-view-leaf": second,
    "place-source-accounts": accounts,
    "available-work-strip": prior,
    "three-pocket-mat": pockets,
    "actual-place-record": actual,
    "private-handover-cover": cover,
    "check-a-paper-gate": fresh_a,
    "check-b-paper-nook": fresh_b,
}
TACTILE = {
    "first-view-leaf": "One tall leaf rises from a square planter inside a broad closed-pane border; plain patch above, no drop marks. Source A/date/view explicitly invented. Read exact source and describe every relation without touch; broad raised/flat pane/planter/leaf cues or adult-directed drawing preserve the same bounded observation/source goal. No real weather, season, plant cause or cultural meaning inferred.",
    "second-view-leaf": "Two tall leaves rise from the same square planter/closed-pane cue; three drop outlines above/right outside planter. B gives exact invented later date and same view. Raised/flat broad shape cues or exact no-touch reading/adult-directed leaf record preserve facts; drop marks are drawn marks, not actual rain. No season/growth/causal/cultural conclusion from geometry.",
    "place-source-accounts": "Two complete source panels C and D plus separate broad empty speech flaps labelled PERSON SAYS and MAKER SAYS. No faces, identity or cultural symbols drawn. Read exact attribution and reason/scope; broad raised flaps or flat/no-touch directed source fields equivalent. Optional child own words/pass separate; no universal significance, actual permission or speaker authority inferred.",
    "available-work-strip": "Undated model fragment with PLANTER, ADULT GAVE WORD; separate earlier-child-work panel says NOT AVAILABLE. Two outlined rectangles and round field cue carry no actual history. Raised/flat broad fragments or exact no-touch reading/directing adult preserve available-source versus missing-child-response distinction. Today/help field blank; no previous child answer, date, progress or annual evidence supplied.",
    "three-pocket-mat": "Three broad numbered outlined fields in order SOURCE, ACTUALLY DID, NEXT QUESTION with writing line; no data entered. Folded/flat/raised fields or exact oral/no-touch adult-directed sequence equivalent. Read every prompt and keep source content, actual first action/help and missing question distinct. No small cutting, colour or handwriting required, no knowledge outcome from pocket completion.",
    "actual-place-record": "Four broad numbered blank local fields: actual source/date/view; willing account/care; available work/help; element decision/handover. Circle cues and writing lines are neutral, no actual facts. Exact reading, broad raised/flat panels, scribe/sign/AAC/adult direction equivalent. Actual source/date/place/speaker/permission/action/knowledge are blank; expanded local preflight required and private data kept outside repository.",
    "private-handover-cover": "Exact F paper request panel above outlined closed envelope with a round closure, then two blank current-scope/action fields. Broad raised/flat cover or exact no-touch reading/directing adult equivalent; closure does not show consent, handover or viewer learning. Private/no-share/pass/source-only guide complete. No real permission, recipient, cultural symbol or year certificate supplied.",
    "check-a-paper-gate": "Two invented closed-pane square-opening diagrams: first one cloud outline, second two. Labels TWO/SIX SAME VIEW and exact source dates establish model cue only. Four full sources read neutrally; broad raised/flat cues or exact no-touch relations/adult-directed fields preserve prompt facts. No source interpretation/answer supplied by tactile adaptation; no real weather, season/cause, earlier child work or recipient reply.",
    "check-b-paper-nook": "First invented front-view low shelf has two dots above; second invented side-view low shelf has one dot beside. Labels THREE FRONT/NINE SIDE and exact sources preserve changed view cue. Four full sources read neutrally with broad raised/flat marks or exact no-touch spatial description; adult handling only as directed. No real change/season/cause/child progress/Country or viewer outcome depicted.",
}


def write() -> None:
    """Write only nine named A4 pairs and full exact alternatives."""
    readings = [
        "# Exact linear readings and tactile/no-print routes",
        "",
        "Semantic SVG metadata and selectable PDF text support access. PDFs are not tagged; no PDF/UA or classroom accessibility certification is claimed. Each exact reading includes every visible text item in SVG/PDF order, including attribution. Read [source script](../MATERIALS.md), [held fresh prompts](../STUDENT-CHECKS.md) or [actual pathway](../ACTUAL-SOURCE-PATHWAY.md) neutrally. Shape/word labels carry meaning; colour supplementary. Raised construction needs local individual review and has not been physically learner-tested. No worked check answers appended.",
        "",
    ]
    ns = {"s": "http://www.w3.org/2000/svg"}
    for stem, fn in PAGES.items():
        content = fn()
        svg, pdf = HERE / f"{stem}.svg", HERE / f"{stem}.pdf"
        svg.write_text(content, encoding="utf-8")
        with pdf.open("wb") as output:
            subprocess.run(
                ["rsvg-convert", "--format", "pdf", str(svg)],
                check=True,
                stdout=output,
                env=dict(os.environ, SOURCE_DATE_EPOCH="1780000000"),
            )
        top = ET.fromstring(content)
        exact = " ".join("".join(n.itertext()) for n in top.findall(".//s:text", ns))
        readings += [
            f"## {stem}.svg / {stem}.pdf",
            "",
            "**Exact visible text:** " + exact,
            "",
            "**Illustration and tactile/no-print:** " + TACTILE[stem],
            "",
        ]
    (HERE / "TEXT-ALTERNATIVES.md").write_text("\n".join(readings), encoding="utf-8")


def check_reproduction() -> None:
    """Generate only in a temporary directory; preserve every pack file."""
    global HERE
    original = HERE
    pack = HERE.parent

    def state():
        return {
            p.relative_to(pack).as_posix(): (
                hashlib.sha256(p.read_bytes()).hexdigest(),
                p.stat().st_size,
                p.stat().st_mtime_ns,
            )
            for p in pack.rglob("*")
            if p.is_file()
        }

    before = state()
    names = [f"{stem}.{ext}" for stem in PAGES for ext in ("svg", "pdf")] + [
        "TEXT-ALTERNATIVES.md"
    ]
    try:
        with tempfile.TemporaryDirectory(
            prefix="subjectnest-hass37-repro-"
        ) as directory:
            HERE = Path(directory)
            write()
            for name in names:
                if (HERE / name).read_bytes() != (original / name).read_bytes():
                    raise AssertionError("reproduction drift: " + name)
    finally:
        HERE = original
    if before != state():
        raise AssertionError("reproduction changed pack bytes/size/mtime")
    print(
        "PASS: 19/19 print artifacts reproduced in temporary storage; all pack bytes, sizes and modification times preserved"
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument(
        "--regenerate",
        action="store_true",
        help="Deliberately rewrite only named print assets/alternatives",
    )
    group.add_argument(
        "--check-reproduction",
        action="store_true",
        help="Generate in temporary storage and compare; pack stays byte/mtime unchanged",
    )
    args = parser.parse_args()
    if args.check_reproduction:
        check_reproduction()
    else:
        write()
        print("Generated nine original A4 SVG/PDF pairs and exact alternatives")
