# SPDX-License-Identifier: Apache-2.0
"""Read-only source, pedagogy, print and receipt checks for Design W11-12."""

from __future__ import annotations

import argparse
import ast
import hashlib
import json
import re
import subprocess
import xml.etree.ElementTree as ET
from pathlib import Path
from urllib.parse import unquote

ROOT = Path(__file__).resolve().parent
STUDIO = ROOT.parents[4]
SOURCE_SHA = "db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3"
ROWS = {"AC9TDEFK01": 19725, "AC9TDEFP01": 19733}
DESCRIPTIONS = {
    "AC9TDEFK01": "explore how familiar products, services and environments are designed by people",
    "AC9TDEFP01": "generate, communicate and evaluate design ideas, and use materials, equipment and steps to safely make a solution for a purpose",
}
QCAA_URL = "https://www.qcaa.qld.edu.au/p-10/aciq/version-9/learning-areas/p-10-technologies/design-and-technologies"
QCAA_ALIGNMENT_URL = "https://www.qcaa.qld.edu.au/downloads/aciqv9/technologies/curriculum/ac9_tech_design_prep_as_cd_alignment.pdf"
STEMS = (
    "ten-card-brief",
    "board-ideas",
    "safe-board-steps",
    "design-record",
    "shared-board-brief",
    "local-source-slot",
    "check-a-library-returns",
    "check-b-shared-art-station",
)
EXPECTED_CODES = {
    51: {"AC9TDEFK01"},
    52: {"AC9TDEFK01", "AC9TDEFP01"},
    53: {"AC9TDEFP01"},
    54: {"AC9TDEFP01"},
    55: {"AC9TDEFK01", "AC9TDEFP01"},
    56: {"AC9TDEFP01"},
    57: {"AC9TDEFP01"},
    58: {"AC9TDEFP01"},
    59: {"AC9TDEFK01", "AC9TDEFP01"},
    60: {"AC9TDEFP01"},
}
PDF_PHRASES = {
    "ten-card-brief": ("CARD 1", "CARD 5", "CARD 10", "No board, user or real placement"),
    "board-ideas": ("ONE LONG ROW", "TWO ROWS OF FIVE", "not physical card-size templates"),
    "safe-board-steps": ("1 ADULT CHECKS", "2 MARK TEN PLACES", "3 PLACE BROAD CARDS", "4 ASK AND RECORD", "FIT / OVERLAP / NOT TRIED"),
    "design-record": ("USER / PURPOSE", "TWO IDEAS / CHOICE", "MAKER / CARD RESULT", "VIEWER / NEXT QUESTION"),
    "shared-board-brief": ("ONE BOARD", "TEN PRETEND PICTURE CARDS", "CARD HOME", "READY / IN USE"),
    "local-source-slot": ("REAL PRODUCT / SERVICE / PLACE", "SOURCE / DATE / PERMISSION", "CHILD-SEEN FEATURE", "PURPOSE QUESTION"),
    "check-a-library-returns": ("TEN UNPLACED PICTURE CARDS", "BOOK A", "BOOK J", "name list, not a board arrangement", "IDEA ONE", "IDEA TWO"),
    "check-b-shared-art-station": ("TEN ABSTRACT-SHAPE CARDS", "CARD HOME", "READY / IN USE", "FIT / OVERLAP / NOT TRIED", "IDEA ONE", "IDEA TWO"),
}


def need(ok: object, message: str) -> None:
    if not ok:
        raise AssertionError(message)


def read(name: str) -> str:
    return (ROOT / name).read_text(encoding="utf-8")


def source() -> None:
    workbook = STUDIO / "research/sources/acara-australian-curriculum-v9-download-2026-09-29.xlsx"
    imported = json.loads((STUDIO / "data/frameworks/acara-v9.json").read_text(encoding="utf-8"))
    snapshot = json.loads(read("source-snapshot.json"))
    crosswalk = read("CURRICULUM-CROSSWALK.md")
    need(
        hashlib.sha256(workbook.read_bytes()).hexdigest()
        == imported["source_sha256"]
        == snapshot["workbook_sha256"]
        == SOURCE_SHA,
        "Official ACARA workbook, import or snapshot SHA drift",
    )
    need(snapshot["content_rows"] == ROWS, "Official row pin drift")
    need(
        snapshot["queensland_prep_entry_point_url"] == QCAA_URL
        and snapshot["queensland_prep_alignment_url"] == QCAA_ALIGNMENT_URL
        and QCAA_URL in crosswalk
        and QCAA_ALIGNMENT_URL in crosswalk,
        "QCAA source link drift",
    )
    records = {
        item["code"]: item
        for item in imported["records"]
        if item.get("record_type") == "content_description" and item.get("code") in ROWS
    }
    need(set(records) == set(ROWS), "Foundation Design descriptions missing")
    for code, row in ROWS.items():
        item = records[code]
        attrs = item["attributes"]
        need(
            item["source_row"] == row
            and item["plain_text"] == DESCRIPTIONS[code]
            and attrs["learning_area"] == "Technologies"
            and attrs["subject"] == "Design and Technologies"
            and attrs["level"] == "Foundation Year",
            f"Exact official import drift: {code}",
        )
        pattern = (
            rf"^\| {code} \| {row} \| Technologies · Design and Technologies · "
            rf"Foundation Year \| {re.escape(DESCRIPTIONS[code])} \|"
        )
        need(re.search(pattern, crosswalk, re.MULTILINE), f"Exact crosswalk drift: {code}")
    need(
        "school-selected" in crosswalk.lower()
        and "actual observed material action" in crosswalk.lower()
        and "not secure exams" in crosswalk.lower(),
        "Curriculum/evidence boundary missing",
    )
    print("PASS two exact Foundation Design rows and Queensland Prep source links")


def pedagogy() -> None:
    lessons = read("LESSONS.md")
    headings = list(re.finditer(r"^## Week (\d+) · Day (\d+)\b", lessons, re.MULTILINE))
    need([int(m.group(2)) for m in headings] == list(range(51, 61)), "Teacher day sequence drift")
    for index, mark in enumerate(headings):
        day = int(mark.group(2))
        end = headings[index + 1].start() if index + 1 < len(headings) else len(lessons)
        body = lessons[mark.end():end]
        minutes = [int(v) for v in re.findall(r"^\d+\. \*\*[^\n]*? · (\d+) min\.\*\*", body, re.MULTILINE)]
        need(
            int(mark.group(1)) == (11 if day <= 55 else 12)
            and minutes == [3, 4, 5, 7, 4, 2]
            and sum(minutes) == 25,
            f"Day {day} week/timing drift: {minutes}",
        )
        target = re.search(r"^\*\*Target/codes:\*\* ([^\n]+)", body, re.MULTILINE)
        need(target is not None, f"Day {day} target missing")
        codes = set(re.findall(r"\bAC9TDEF(?:K|P)\d\d\b", target.group(1)))
        need(codes == EXPECTED_CODES[day], f"Day {day} curriculum code drift: {codes}")
        need(
            "**Check/respond · 4 min.**" in body
            and any(term in body for term in ("Record", "Collect")),
            f"Day {day} formative response absent",
        )
    cards = read("LEARNER-CARDS.md")
    card_headings = list(re.finditer(r"^## Day (\d+)\b", cards, re.MULTILINE))
    need([int(m.group(1)) for m in card_headings] == list(range(51, 61)), "Learner day sequence drift")
    for index, mark in enumerate(card_headings):
        end = card_headings[index + 1].start() if index + 1 < len(card_headings) else len(cards)
        body = cards[mark.end():end]
        need(
            "**Shared target:**" in body
            and all(re.search(rf"^- \*\*{route} ·", body, re.MULTILINE) for route in "ABC")
            and "**Home:**" in body,
            f"Day {mark.group(1)} access/home drift",
        )
    swaps = re.findall(r"^\| (\d+) \| (.+) \| (.+) \|$", read("PRACTICE-SWAPS.md"), re.MULTILINE)
    need(
        [int(day) for day, _, _ in swaps] == list(range(51, 61))
        and all(a.startswith("**") and b.startswith("**") for _, a, b in swaps),
        "Twenty worked swaps missing",
    )
    need("not fixed learning styles" in cards and "NOT TRIED" in cards and "school-approved" in lessons, "Access/safety language missing")
    print("PASS ten 25-minute scripts, 30 routes, 20 worked domain swaps")


def checks() -> None:
    child = read("STUDENT-CHECKS.md")
    key = read("teacher/KEY-AND-NEXT.md")
    routine = read("LEARNER-CARDS.md") + read("PRACTICE-SWAPS.md")
    need(
        re.findall(r"^## Check ([AB]) · Day (\d+)\b", child, re.MULTILINE)
        == [("A", "55"), ("B", "60")],
        "Check schedule drift",
    )
    need("teacher/KEY-AND-NEXT.md" not in child and "not secure exams" in child.lower() and "public by url" in key.lower(), "Public check/key boundary missing")
    for held in ("Library Return Board", "Shared Art Station", "BOOK A", "BOOK J", "TEN ABSTRACT-SHAPE CARDS"):
        need(held not in routine, f"Held assessment source leaked into routine: {held}")
    for phrase in ("BOOK A", "BOOK J", "CARD HOME", "READY / IN USE", "FIT", "OVERLAP", "NOT TRIED", "No prior result is printed"):
        need(phrase.lower() in key.lower(), f"Teacher key missing: {phrase}")
    need(
        "No trial or librarian response is supplied" in child
        and "no real user response" in child.lower()
        and "adult" in key.lower(),
        "Result/handler boundary missing",
    )
    print("PASS two fresh public formative checks and conditional separate key")


def svg_values(stem: str, attribute: str) -> list[str]:
    top = ET.parse(ROOT / "print" / f"{stem}.svg").getroot()
    ns = {"svg": "http://www.w3.org/2000/svg"}
    return [node.get(attribute, "") for node in top.findall(".//svg:rect", ns) if node.get(attribute) is not None]


def assets() -> None:
    alternatives = read("print/TEXT-ALTERNATIVES.md")
    source_tree = ast.parse(read("print/generate_print.py"))
    pages = next(
        node.value
        for node in source_tree.body
        if isinstance(node, ast.Assign)
        and any(isinstance(t, ast.Name) and t.id == "PAGES" for t in node.targets)
    )
    need(isinstance(pages, ast.Dict) and {k.value for k in pages.keys if isinstance(k, ast.Constant)} == set(STEMS), "Generator inventory drift")
    ns = {"svg": "http://www.w3.org/2000/svg"}
    alt_flat = " ".join(alternatives.lower().split())
    for stem in STEMS:
        top = ET.parse(ROOT / "print" / f"{stem}.svg").getroot()
        pdf = ROOT / "print" / f"{stem}.pdf"
        need(top.get("width") == "210mm" and top.get("height") == "297mm" and top.find("svg:title", ns) is not None and top.find("svg:desc", ns) is not None, f"SVG metadata/A4 drift: {stem}")
        info = subprocess.check_output(["pdfinfo", str(pdf)], text=True)
        words = subprocess.check_output(["pdftotext", str(pdf), "-"], text=True)
        pdf_flat = " ".join(words.lower().split())
        need(re.search(r"Pages:\s+1\b", info) and "(A4)" in info and "CC BY 4.0" in words and len(words.split()) >= 35 and f"{stem}.svg" in alternatives and f"{stem}.pdf" in alternatives, f"PDF/text/alternative drift: {stem}")
        for phrase in PDF_PHRASES[stem]:
            need(phrase.lower() in pdf_flat and phrase.lower() in alt_flat, f"PDF/linear mismatch: {stem}: {phrase}")
    need(
        svg_values("ten-card-brief", "data-card") == [f"CARD {i}" for i in range(1, 11)]
        and svg_values("board-ideas", "data-idea") == ["ONE LONG ROW", "TWO ROWS OF FIVE"]
        and svg_values("board-ideas", "data-space") == [f"ROW {i}" for i in range(1, 11)] + [f"GRID {i}" for i in range(1, 11)]
        and svg_values("safe-board-steps", "data-step") == ["1 ADULT CHECKS", "2 MARK TEN PLACES", "3 PLACE BROAD CARDS", "4 ASK AND RECORD"]
        and svg_values("shared-board-brief", "data-fact") == ["ONE BOARD", "TEN PRETEND PICTURE CARDS", "TWO PRETEND USERS", "CARD HOME · empty", "READY / IN USE · not selected"]
        and svg_values("check-a-library-returns", "data-canvas") == ["IDEA ONE", "IDEA TWO"]
        and svg_values("check-b-shared-art-station", "data-canvas") == ["IDEA ONE", "IDEA TWO"],
        "Original source/order metadata drift",
    )
    need("untagged" in alternatives and "tactile" in alternatives.lower() and "no gate/result" in alternatives.lower(), "Access/evidence alternative absent")
    print("PASS eight original one-page A4 SVG/PDF pairs, exact labels and linear/tactile routes")


def slug(title: str) -> str:
    title = re.sub(r"\[[^]]+\]\([^)]+\)", "", title)
    title = re.sub(r"[*_]", "", title).strip().lower()
    return re.sub(r"[^\w -]", "", title).replace(" ", "-")


def links(*, bootstrap: bool) -> None:
    count = 0
    for path in ROOT.rglob("*.md"):
        for url in re.findall(r"(?<!!)\[[^]]+\]\(([^)]+)\)", path.read_text(encoding="utf-8")):
            if url.startswith(("https://", "http://", "mailto:")):
                continue
            filename, marker, anchor = unquote(url).partition("#")
            target = (path.parent / filename).resolve() if filename else path
            if bootstrap and target == ROOT / "manifest.json":
                count += 1
                continue
            need(target.is_file(), f"Broken local link: {path.relative_to(ROOT)} -> {url}")
            if marker and target.suffix == ".md":
                headings = [slug(text) for text in re.findall(r"^#{1,6} (.+)$", target.read_text(encoding="utf-8"), re.MULTILINE)]
                need(anchor in headings, f"Broken heading anchor: {url}")
            count += 1
    print(f"PASS {count} local file/heading links")


def manifest_data() -> dict:
    files = sorted(path for path in ROOT.rglob("*") if path.is_file() and path.name != "manifest.json" and "__pycache__" not in path.parts)
    return {
        "schema": "subjectnest-authored-pack-manifest-v1",
        "pack": "foundation/design-and-technologies/term-2/weeks-11-12",
        "created_at": "2026-09-30",
        "review_status": "author_desk_checked_pending_school_material_child_and_accessibility_review",
        "curriculum_source_sha256": SOURCE_SHA,
        "curriculum_codes_partial_conditional": sorted(ROWS),
        "files": {str(path.relative_to(ROOT)): {"sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "bytes": path.stat().st_size} for path in files},
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write-manifest", action="store_true")
    args = parser.parse_args()
    source()
    pedagogy()
    checks()
    assets()
    links(bootstrap=args.write_manifest)
    expected = manifest_data()
    manifest = ROOT / "manifest.json"
    if args.write_manifest:
        manifest.write_text(json.dumps(expected, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        print(f"WROTE SHA-256 manifest for {len(expected['files'])} files")
    else:
        need(json.loads(manifest.read_text(encoding="utf-8")) == expected, "SHA-256 manifest missing or stale")
        print(f"PASS SHA-256 manifest for {len(expected['files'])} files")
    print("PACK PASS · author desk QA only; no physical or child trial")


if __name__ == "__main__":
    main()
