#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Verify Foundation mathematics W29–30 source, daily scripts and exact sets."""
from __future__ import annotations

import argparse
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]
CROSSWALK = ROOT.parents[2] / "english/term-3/weeks-29-30/CURRICULUM-CROSSWALK.md"
SOURCE_SHA = "db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3"
CODES = {"AC9MFA01", "AC9MFN01", "AC9MFN03"}


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


def official() -> None:
    workbook = STUDIO / "research/sources/acara-australian-curriculum-v9-download-2026-09-29.xlsx"
    data = json.loads((STUDIO / "data/frameworks/acara-v9.json").read_text(encoding="utf-8"))
    need(hashlib.sha256(workbook.read_bytes()).hexdigest() == SOURCE_SHA == data["source_sha256"], "Pinned official workbook/import mismatch")
    rows = {r["code"]: r for r in data["records"] if r.get("record_type") == "content_description"}
    crosswalk = CROSSWALK.read_text(encoding="utf-8")
    for code in CODES:
        m = re.search(rf"^\| {code} \| (\d+) \| Mathematics · Foundation Year \| (.*?) \|$", crosswalk, re.M)
        need(m is not None, f"Exact crosswalk absent: {code}")
        row = rows[code]
        need(row["attributes"]["level"] == "Foundation Year" and row["attributes"]["learning_area"] == "Mathematics", f"Wrong source level/area: {code}")
        need(row["source_row"] == int(m.group(1)) and " ".join(row["plain_text"].split()) == m.group(2), f"Wrong source row/description: {code}")


def lessons() -> None:
    content = (ROOT / "LESSONS.md").read_text(encoding="utf-8")
    marks = list(re.finditer(r"^## Week (\d+) · Day (\d+)\b", content, re.M))
    need([int(m.group(2)) for m in marks] == list(range(141, 151)), "Ten maths days 141–150 required")
    for i, mark in enumerate(marks):
        day = int(mark.group(2))
        need(int(mark.group(1)) == (day-1)//5+1, f"Wrong week for Day {day}")
        body = content[mark.end():marks[i+1].start() if i+1 < len(marks) else len(content)]
        times = [int(x) for x in re.findall(r"^\d+\. \*\*[^\n]*?\b(\d+) min\.", body, re.M)]
        need(len(times) == 6 and sum(times) == 25, f"Day {day} timings: {times}")
        code_set = set(re.findall(r"\bAC9[A-Z0-9]+\b", body))
        need(code_set == ({"AC9MFA01"} if day <= 145 else {"AC9MFN01", "AC9MFN03"}), f"Day {day} code mismatch: {code_set}")
    need("first child-controlled" in content and "out of view" in content, "Evidence/held-out boundary absent")


def cards_and_checks() -> None:
    cards = (ROOT / "LEARNER-CARDS.md").read_text(encoding="utf-8")
    marks = list(re.finditer(r"^## Day (\d+)\b", cards, re.M))
    need([int(m.group(1)) for m in marks] == list(range(141, 151)), "Ten child card days required")
    for i, mark in enumerate(marks):
        body = cards[mark.end():marks[i+1].start() if i+1 < len(marks) else len(cards)]
        need(len(re.findall(r"^- \*\*", body, re.M)) == 3, f"Day {mark.group(1)} needs three choices")
    need("ASSESSMENT.md" not in cards and "TEACHER-KEY" not in cards, "Teacher copy leaked")
    check = (ROOT / "ASSESSMENT.md").read_text(encoding="utf-8")
    key = (ROOT / "TEACHER-KEY.md").read_text(encoding="utf-8")
    practice = (ROOT / "MATERIALS.md").read_text(encoding="utf-8")
    need("STAR / BAR / BAR / STAR / BAR / BAR" in check and "STAR, BAR, BAR" in key, "Fresh ABB sequence or key wrong")
    need("17 identical" in check and "14 identical" in check and "A = **17**, B = **14**" in key, "Fresh count/key wrong")
    need("13 identical" in practice and "15 identical" in practice and "12 identical" in practice and "16" in practice, "Practice sets incomplete")
    need("17 identical" not in practice and "14 identical" not in practice, "Assessment quantities leaked into practice")
    need("first child-controlled" in check and "tactile" in check, "Assessment access/first-response safeguard missing")


def assets() -> None:
    ns = {"s": "http://www.w3.org/2000/svg"}
    for stem in ("pattern-unit-strip", "count-check-design", "teacher-held-pattern"):
        svg, pdf = ROOT / f"{stem}.svg", ROOT / f"{stem}.pdf"
        root = ET.parse(svg).getroot()
        need(root.attrib.get("width") == "210mm" and root.attrib.get("height") == "297mm", f"{stem} not A4")
        need(root.find("s:title", ns) is not None and root.find("s:desc", ns) is not None, f"{stem} SVG title/desc absent")
        need("(A4)" in subprocess.check_output(["pdfinfo", str(pdf)], text=True), f"{stem} PDF not A4")
        need("CC BY 4.0" in subprocess.check_output(["pdftotext", str(pdf), "-"], text=True), f"{stem} PDF rights absent")
    pattern = subprocess.check_output(["pdftotext", str(ROOT / "pattern-unit-strip.pdf"), "-"], text=True)
    held = subprocess.check_output(["pdftotext", str(ROOT / "teacher-held-pattern.pdf"), "-"], text=True)
    count = subprocess.check_output(["pdftotext", str(ROOT / "count-check-design.pdf"), "-"], text=True)
    need(pattern.count("CIRCLE") == 3 and pattern.count("SQUARE") == 3 and pattern.count("TAP") == 4 and pattern.count("REST") == 2, "Practice printed pattern wrong")
    need(held.count("STAR") >= 2 and held.count("BAR") >= 4 and "TEACHER COPY" in held, "Teacher-only print pattern wrong")
    need(all(re.search(rf"\b{n}\b", count) for n in range(1, 21)), "Count mat lacks 1–20 places")
    alt = (ROOT / "MATERIALS.md").read_text(encoding="utf-8").lower()
    need(all(term in alt for term in ("exact text alternative", "tactile", "count each object once", "one-to-one")), "Text/tactile equivalents missing")


def links() -> int:
    n = 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
            target = (path.parent / unquote(url.partition("#")[0])).resolve()
            need(target.exists(), f"Broken link {path.name}: {url}")
            n += 1
    return n


def manifest_data() -> dict:
    paths = sorted(p for p in ROOT.rglob("*") if p.is_file() and p.name != "manifest.json" and "__pycache__" not in p.parts)
    return {
        "pack_id": "subjectnest-acara-v9-foundation-mathematics-t3-w29-30",
        "pack_version": "0.1.0-draft", "created_at": "2026-09-29",
        "review_status": "pending_human_educator_accessibility_local_syllabus_safety_and_classroom_review",
        "year_label": "Australian Curriculum Foundation Year / Queensland Prep",
        "alignment_status": "proposed_partial_not_authority_approved",
        "curriculum_codes": sorted(CODES),
        "curriculum_source": {"authority": "Australian Curriculum, Assessment and Reporting Authority", "source_url": "https://www.australiancurriculum.edu.au/content/dam/en/curriculum/ac-version-9/downloads/curriculum-workbook.xlsx", "retrieved_at": "2026-09-29", "source_hash": SOURCE_SHA, "terms_url": "https://www.australiancurriculum.edu.au/copyright-and-terms-of-use"},
        "rights": "Original SubjectNest sets, scripts, checks and vector aids CC BY 4.0; ACARA retains own terms.",
        "original_assets": [{"item_code_or_locator": str(p.relative_to(ROOT)), "sha256": hashlib.sha256(p.read_bytes()).hexdigest()} for p in paths],
    }


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write-manifest", action="store_true")
    args = parser.parse_args()
    official()
    lessons()
    cards_and_checks()
    assets()
    n = links()
    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")
    need(json.loads(manifest.read_text(encoding="utf-8")) == expected, "Manifest stale/missing")
    print(f"PASS Foundation maths W29–30: 10x25m, 30 choices, 2 held-out checks, {len(CODES)} exact codes, 3 A4 aids, {n} links, hashes")


if __name__ == "__main__":
    main()
