#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Read-only Year 7 curriculum, arithmetic, lesson, asset and hash audit."""
from __future__ import annotations

import argparse
import hashlib
import json
import math
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
from decimal import Decimal
from fractions import Fraction
from pathlib import Path
from urllib.parse import unquote

ROOT = Path(__file__).resolve().parent
ACARA = ROOT.parents[4] / "data/frameworks/acara-v9.json"
SOURCE_SHA = "db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3"
ROWS = {
    "AC9M7N01": (17953, "describe the relationship between perfect square numbers and square roots, and use squares of numbers and square roots of perfect square numbers to solve problems"),
    "AC9M7N02": (17959, "represent natural numbers as products of powers of prime numbers using exponent notation"),
    "AC9M7N04": (17967, "find equivalent representations of rational numbers and represent rational numbers on a number line"),
    "AC9M7N05": (17972, "round decimals to a given accuracy appropriate to the context and use appropriate rounding and estimation to check the reasonableness of solutions"),
    "AC9M7N06": (17976, "use the 4 operations with positive rational numbers including fractions, decimals and percentages to solve problems using efficient calculation strategies"),
}
REQUIRED = {"README.md", "LESSONS.md", "MATERIALS.md", "LEARNER.md", "DAILY-CHOICES.md",
            "DAILY-EXTRAS.md", "STUDENT-CHECKS.md", "teacher/ANSWER-AND-NEXT.md",
            "CURRICULUM-CROSSWALK.md", "SOURCES-AND-REVIEW.md", "RUN-THROUGH.md",
            "CODE-LICENSE.txt", "verify_pack.py", "print/generate_print.py",
            "print/TEXT-ALTERNATIVES.md", "print/FONT-RIGHTS.md", "print/dejavu-font-copyright.txt"}
STEMS = ("operation-check", "prime-factor-tree", "square-root-link")
for stem in STEMS:
    REQUIRED |= {f"print/{stem}.svg", f"print/{stem}.pdf"}


def require(ok: bool, detail: str) -> None:
    if not ok:
        raise AssertionError(detail)


def source_records() -> tuple[dict, dict[str, dict]]:
    data = json.loads(ACARA.read_text(encoding="utf-8"))
    require(data["source_sha256"] == SOURCE_SHA, "ACARA source workbook SHA drift")
    require(data["framework"] == "Australian Curriculum Version 9.0", "framework drift")
    require(data["source_url"].startswith("https://www.australiancurriculum.edu.au/"), "official source URL drift")
    found = {r["code"]: r for r in data["records"] if r.get("record_type") == "content_description" and r.get("code") in ROWS}
    require(set(found) == set(ROWS), "Year 7 mathematics source rows missing")
    for code, (row, wording) in ROWS.items():
        r = found[code]
        require((r["source_row"], r["plain_text"]) == (row, wording), f"{code}: row/wording drift")
        a = r["attributes"]
        require((a["level"], a["learning_area"], a["subject"]) == ("Year 7", "Mathematics", "Mathematics"),
                f"{code}: level/area/subject mismatch")
    return data, found


def crosswalk(data: dict, found: dict[str, dict]) -> str:
    slices = {
        "AC9M7N01": "Day 19–20: selected perfect squares and positive principal roots checked by squaring; this does not teach every square-root problem.",
        "AC9M7N02": "Days 16–20: factor trees, prime products and exponent notation, including a small shared-factor application; a first sequence, not general mastery.",
        "AC9M7N04": "Days 11,13,18: equivalent fraction names for operations and 90/150=3/5. The earlier fortnight taught number-line representation; these days revisit only a slice.",
        "AC9M7N05": "Days 12,14–15: nearest-whole rounding of decimal inputs and estimates/inverse checks; appropriate precision across wider contexts needs further work.",
        "AC9M7N06": "Days 11–15: fraction addition/subtraction/multiplication, decimal addition/subtraction/grouping division and familiar percentages with efficient checks. Other rational cases remain for later weeks.",
    }
    out = ["# Australian Curriculum v9 crosswalk · Year 7 mathematics Weeks 3–4", "",
           "These five links are **partial content-description encounters**, not full Year 7 curriculum coverage, an achievement-standard judgement or local state/territory approval. Exact wording, code and row below are pinned to the official workbook import; the taught slice states the work actually present and its limit.", "",
           f"Source: [official ACARA v9 workbook]({data['source_url']}), retrieved {data['retrieved_at']}; workbook SHA-256 `{data['source_sha256']}`. Row numbers permit audit against the same snapshot.", "",
           "| Official code | Workbook row | Level / area | Official content description | Taught slice and limit |",
           "| --- | ---: | --- | --- | --- |"]
    for code in ROWS:
        r = found[code]
        cells = [code, str(r["source_row"]), "Year 7 / Mathematics", r["plain_text"], slices[code]]
        out.append("| " + " | ".join(c.replace("|", "\\|") for c in cells) + " |")
    out += ["",
            "© Australian Curriculum, Assessment and Reporting Authority (ACARA) 2010 to present, unless otherwise indicated. Downloaded from the Australian Curriculum website (accessed 29 September 2026) and modified for plain-text display. Curriculum material is [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), subject to [terms and exclusions](https://www.australiancurriculum.edu.au/copyright-and-terms-of-use). ACARA does not endorse SubjectNest; SubjectNest is not affiliated with, sponsored or approved by ACARA.", "",
            "This is a dated source snapshot, not live synchronisation. Check current official material and each jurisdiction's implementation before reissue. See [source/rights ledger](SOURCES-AND-REVIEW.md).", ""]
    return "\n".join(out)


def local_links() -> None:
    for md in ROOT.rglob("*.md"):
        content = md.read_text(encoding="utf-8")
        for raw in re.findall(r"\]\(([^)]+)\)", content):
            target = unquote(raw.split("#", 1)[0])
            if not target or target.startswith(("https://", "http://", "mailto:")):
                continue
            require((md.parent / target).is_file(), f"Broken local link in {md.relative_to(ROOT)}: {raw}")


def content_checks() -> None:
    for item in REQUIRED:
        require((ROOT / item).is_file(), f"Missing pack file: {item}")
    lesson = (ROOT / "LESSONS.md").read_text(encoding="utf-8")
    learner = (ROOT / "LEARNER.md").read_text(encoding="utf-8")
    choices = (ROOT / "DAILY-CHOICES.md").read_text(encoding="utf-8")
    extras = (ROOT / "DAILY-EXTRAS.md").read_text(encoding="utf-8")
    checks = (ROOT / "STUDENT-CHECKS.md").read_text(encoding="utf-8")
    key = (ROOT / "teacher/ANSWER-AND-NEXT.md").read_text(encoding="utf-8")
    for name, body, marker in (("lessons", lesson, r"^### Day (\d+) ·"),
                               ("learner", learner, r"^## Day (\d+) ·"),
                               ("choices", choices, r"^## Day (\d+) ·"),
                               ("extras", extras, r"^\| (\d+) \|")):
        require([int(x) for x in re.findall(marker, body, flags=re.MULTILINE)] == list(range(11, 21)),
                f"{name}: need exactly Days 11–20 in order")
    sections = re.split(r"^## Day \d+ ·", choices, flags=re.MULTILINE)[1:]
    for day, section in enumerate(sections, 11):
        require(re.findall(r"^- \*\*([ABC]) ·", section, flags=re.MULTILINE) == ["A", "B", "C"],
                f"Day {day}: three A/B/C choices required")
        for letter in "ABC":
            require(f"D{day}-{letter}" in key, f"D{day}-{letter}: staff answer missing")
    day_sections = re.split(r"^### Day \d+ ·", lesson, flags=re.MULTILINE)[1:]
    for day, section in enumerate(day_sections, 11):
        for stage, minutes in (("Launch", 2), ("Model", 5), ("Guide", 6), ("Note", 2)):
            require(f"**{stage} · {minutes} min.**" in section, f"Day {day}: timing for {stage}")
        if day in (15, 20):
            require("**Independent check · 6 min.**" in section and "**Check exit · 4 min.**" in section,
                    f"Day {day}: fresh-check timing")
            require("**Choice · 6 min.**" not in section, f"Day {day}: choice used before check")
        else:
            require("**Choice · 6 min.**" in section and "**Exit · 4 min.**" in section,
                    f"Day {day}: practice/exit timing")
        require("**Optional home/extension:**" in section and "**Code" in section,
                f"Day {day}: home work or code absent")
        codes = re.findall(r"AC9M7N\d{2}", section)
        require(codes and set(codes) <= set(ROWS), f"Day {day}: unsupported code")
    require(set(re.findall(r"AC9M7N\d{2}", lesson)) == set(ROWS), "lesson and crosswalk code sets differ")
    require(len(re.findall(r"^\d\. \*\*", checks, flags=re.MULTILINE)) == 6, "need three plus three fresh items")
    require("## Day 15 held-out key" in key and "## Day 20 held-out key" in key, "separate staff check key missing")
    pre15 = choices.split("## Day 15")[0] + extras.split("| 15 |")[0] + learner.split("## Day 15")[0]
    for marker in ("2/3+1/4", "7.2÷0.3", "15%", "80 equal cells"):
        require(marker not in pre15, f"Day15 answer detail leaked into prior learner material: {marker}")
    pre20 = choices.split("## Day 20")[0] + extras.split("| 20 |")[0] + learner.split("## Day 20")[0]
    for number in (108, 196, 75):
        require(not re.search(rf"(?<![0-9.]){number}(?![0-9.])", pre20),
                f"Day20 held-out value leaked into learner practice: {number}")
    require("Later practice" in choices and "after the fresh check" in extras, "assessment-practice boundary unclear")
    local_links()


def prime_factors(n: int) -> list[int]:
    out = []
    divisor = 2
    while divisor * divisor <= n:
        while n % divisor == 0:
            out.append(divisor)
            n //= divisor
        divisor += 1
    if n > 1:
        out.append(n)
    return out


def mathematics_checks() -> None:
    fraction_examples = {
        "day11_add": (Fraction(1, 2) + Fraction(3, 8), Fraction(7, 8)),
        "day11_sub": (Fraction(5, 6) - Fraction(1, 3), Fraction(1, 2)),
        "day13_mul": (Fraction(3, 4) * Fraction(2, 3), Fraction(1, 2)),
        "day13_alt": (Fraction(2, 5) * Fraction(3, 4), Fraction(3, 10)),
        "day15_add": (Fraction(2, 3) + Fraction(1, 4), Fraction(11, 12)),
        "day18_reduce": (Fraction(90, 150), Fraction(3, 5)),
    }
    for name, (value, expected) in fraction_examples.items():
        require(value == expected, f"fraction arithmetic: {name}")
    decimal_examples = {
        "day12_add": (Decimal("4.75") + Decimal("2.60"), Decimal("7.35")),
        "day12_sub": (Decimal("10.00") - Decimal("7.35"), Decimal("2.65")),
        "day14_div": (Decimal("2.4") / Decimal("0.6"), Decimal(4)),
        "day14_quarters": (Decimal("1.5") / Decimal("0.25"), Decimal(6)),
        "day15_div": (Decimal("7.2") / Decimal("0.3"), Decimal(24)),
        "day15_percent": (Decimal("0.15") * Decimal(80), Decimal(12)),
    }
    for name, (value, expected) in decimal_examples.items():
        require(value == expected, f"decimal arithmetic: {name}")
    products = {60: [2, 2, 3, 5], 72: [2, 2, 2, 3, 3], 84: [2, 2, 3, 7],
                90: [2, 3, 3, 5], 100: [2, 2, 5, 5], 108: [2, 2, 3, 3, 3],
                120: [2, 2, 2, 3, 5], 144: [2, 2, 2, 2, 3, 3],
                150: [2, 3, 5, 5], 196: [2, 2, 7, 7], 75: [3, 5, 5]}
    for n, factors in products.items():
        require(prime_factors(n) == factors and math.prod(factors) == n, f"prime factorisation: {n}")
    require(math.isqrt(144) == 12 and math.isqrt(196) == 14 and math.isqrt(81) == 9,
            "positive square root audit")
    require(3**2 * 5**2 == 225 and 3 * 5**2 == 75, "Day20 false product audit")
    key = (ROOT / "teacher/ANSWER-AND-NEXT.md").read_text(encoding="utf-8")
    for fragment in ("`2/3+1/4=8/12+3/12=11/12`", "`7.2÷0.3=72÷3=24`", "15%=12 cells",
                     "`108=4×27", "`196=14×14", "`3²×5²=9×25=225`"):
        require(fragment in key, f"staff key arithmetic phrase missing: {fragment}")


def print_checks() -> None:
    subprocess.run([sys.executable, str(ROOT / "print/generate_print.py")], check=True, capture_output=True, text=True)
    ns = "{http://www.w3.org/2000/svg}"
    for stem in STEMS:
        svg = ROOT / f"print/{stem}.svg"
        pdf = ROOT / f"print/{stem}.pdf"
        tree = ET.parse(svg).getroot()
        require((tree.attrib.get("width"), tree.attrib.get("height"), tree.attrib.get("viewBox")) ==
                ("210mm", "297mm", "0 0 210 297"), f"{stem}: SVG not A4")
        require(tree.find(ns + "title") is not None and tree.find(ns + "desc") is not None,
                f"{stem}: SVG accessible title/description absent")
        info = subprocess.check_output(["pdfinfo", str(pdf)], text=True)
        require("Pages:           1" in info and "Page size:       595.276 x 841.89 pts (A4)" in info,
                f"{stem}: PDF not single-page A4")
        copied = subprocess.check_output(["pdftotext", str(pdf), "-"], text=True)
        require(len(copied.strip()) > 150 and "SubjectNest" in copied, f"{stem}: PDF text not selectable")
        fonts = subprocess.check_output(["pdffonts", str(pdf)], text=True)
        require("DejaVu" in fonts, f"{stem}: embedded font drift")


def manifest_content() -> str:
    rows = []
    for path in sorted(ROOT.rglob("*")):
        if path.is_file() and path.name != "MANIFEST.sha256" and "__pycache__" not in path.parts:
            rows.append(f"{hashlib.sha256(path.read_bytes()).hexdigest()}  {path.relative_to(ROOT).as_posix()}")
    return "\n".join(rows) + "\n"


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write-crosswalk", action="store_true")
    parser.add_argument("--write-manifest", action="store_true")
    args = parser.parse_args()
    data, found = source_records()
    crosswalk_file = ROOT / "CURRICULUM-CROSSWALK.md"
    wanted = crosswalk(data, found)
    if args.write_crosswalk:
        crosswalk_file.write_text(wanted, encoding="utf-8")
    else:
        require(crosswalk_file.is_file() and crosswalk_file.read_text(encoding="utf-8") == wanted,
                "exact source crosswalk missing/stale")
    content_checks()
    mathematics_checks()
    print_checks()
    manifest_file = ROOT / "MANIFEST.sha256"
    hashes = manifest_content()
    if args.write_manifest:
        manifest_file.write_text(hashes, encoding="utf-8")
    else:
        require(manifest_file.is_file() and manifest_file.read_text(encoding="utf-8") == hashes,
                "hash manifest missing/stale")
    print("PASS: five exact Year 7 ACARA rows; 10 x 25-minute lessons; 30 routes; "
          "6 fresh items; rational/factor/root arithmetic; three accessible A4 aid pairs; links; hashes")


if __name__ == "__main__":
    try:
        main()
    except (AssertionError, subprocess.CalledProcessError, KeyError, ValueError) as exc:
        print(f"FAIL: {exc}", file=sys.stderr)
        raise SystemExit(1)
