#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Verify Foundation mathematics W27–28 assets, exact source codes and checks."""
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-27-28/CURRICULUM-CROSSWALK.md"
SOURCE_SHA = "db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3"
CODES = {"AC9MFM01", "AC9MFST01"}


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 ACARA workbook/import mismatch")
    rows = {r["code"]: r for r in data["records"] if r.get("record_type") == "content_description"}
    text = CROSSWALK.read_text(encoding="utf-8")
    for code in CODES:
        m = re.search(rf"^\| {code} \| (\d+) \| Mathematics · Foundation Year \| (.*?) \|", text, re.M)
        need(m is not None, f"Exact crosswalk row absent: {code}")
        row = rows[code]
        need(row["attributes"]["level"] == "Foundation Year" and row["attributes"]["learning_area"] == "Mathematics", f"Wrong code area/level {code}")
        need(row["source_row"] == int(m.group(1)) and " ".join(row["plain_text"].split()) == m.group(2), f"Crosswalk wording/row mismatch {code}")


def check_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(131, 141)), "Ten maths days 131–140 required")
    for i, m in enumerate(marks):
        day = int(m.group(2))
        need(int(m.group(1)) == (day-1)//5+1, f"Wrong week Day {day}")
        body = content[m.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} timing {times}")
        codes = set(re.findall(r"\bAC9[A-Z0-9]+\b", body))
        need(codes == ({"AC9MFM01"} if day <= 135 else {"AC9MFST01"}), f"Day {day} code mismatch {codes}")
    need("first child-controlled" in content and "fictional" in content.lower(), "Evidence/data boundary missing")


def check_cards_and_keys() -> 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(131, 141)), "Ten maths child days required")
    for i, m in enumerate(marks):
        body = cards[m.end():marks[i+1].start() if i+1 < len(marks) else len(cards)]
        need(len(re.findall(r"^\- \*\*", body, re.M)) == 3, f"Day {m.group(1)} requires three choices")
    need("ASSESSMENT.md" not in cards and "key" not in cards.lower(), "Learner key leakage")
    checks = (ROOT / "ASSESSMENT.md").read_text(encoding="utf-8")
    need(all(x in checks for x in ("45 mm", "68 mm", "three identical", "six of the same", "LEAF **4**", "BOOK **3**", "STAR **1**")), "Unseen set/keys incomplete")
    need("preflight" in checks and "invented" in checks, "Physical/data safeguards missing")
    practice = (ROOT / "MATERIALS.md").read_text(encoding="utf-8")
    need(all(x in practice for x in ("50 mm", "75 mm", "equal-arm balance", "circle **2**", "triangle **3**", "square **1**")), "Practice set incomplete")


def check_assets() -> None:
    ns = {"s": "http://www.w3.org/2000/svg"}
    for stem in ("compare-and-record", "fictional-response-cards", "heldout-tab-cards"):
        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} accessibility metadata missing")
        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 credit missing")
    comp = ET.parse(ROOT / "compare-and-record.svg").getroot()
    bars = [(float(e.attrib.get("x", -1)), float(e.attrib.get("width", -1))) for e in comp.iter() if e.tag.endswith("rect") and e.attrib.get("fill") == "#174f64"]
    need(bars == [(20, 50), (20, 75)], f"Practice printed bars not exact/aligned: {bars}")
    practice_svg = (ROOT / "fictional-response-cards.svg").read_text(encoding="utf-8")
    held_svg = (ROOT / "heldout-tab-cards.svg").read_text(encoding="utf-8")
    need([len(re.findall(rf">{x}<", practice_svg)) for x in ("CIRCLE", "TRIANGLE", "SQUARE")] == [2, 3, 1], "Practice cards wrong")
    need([len(re.findall(rf">{x}<", held_svg)) for x in ("LEAF", "BOOK", "STAR")] == [4, 3, 1], "Held-out cards wrong")
    alt = (ROOT / "MATERIALS.md").read_text(encoding="utf-8").lower()
    need("nonvisual" in alt and "tactile" in alt and "one card" in alt, "Text/tactile equivalent incomplete")


def check_links() -> int:
    count = 0
    for path in ROOT.rglob("*.md"):
        for url in re.findall(r"(?<!!)\[[^]]+\]\(([^)]+)\)", path.read_text(encoding="utf-8")):
            if url.startswith(("http://", "https://", "mailto:", "#")):
                continue
            target = (path.parent / unquote(url.partition("#")[0])).resolve()
            need(target.exists(), f"Broken link {path.name}: {url}")
            count += 1
    return count


def make_manifest() -> 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-w27-28",
        "pack_version": "0.1.0-draft", "created_at": "2026-09-29",
        "review_status": "pending_human_educator_accessibility_local_syllabus_safety_privacy_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/AERO retain 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()
    check_lessons()
    check_cards_and_keys()
    check_assets()
    path = ROOT / "manifest.json"
    expected = make_manifest()
    if args.write_manifest:
        path.write_text(json.dumps(expected, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    need(json.loads(path.read_text(encoding="utf-8")) == expected, "Manifest stale or missing")
    links = check_links()
    print(f"PASS Foundation maths W27–28: 10x25m days, 30 choices, 2 held-out checks, 2 exact Foundation codes, 3 A4 SVG/PDF aids, {links} links and hashes")


if __name__ == "__main__":
    main()
