"""Fail-closed local structural, provenance, link and print checks for this pack."""
import hashlib
import json
import re
import subprocess
import sys
import wave
from pathlib import Path

B=Path(__file__).resolve().parent
ROOT=B.parents[4]
ERR=[]
def check(ok,msg):
    if not ok: ERR.append(msg)
menus=['SCIENCE.md','HASS.md','HPE.md','TECHNOLOGIES.md','ARTS.md']
allcodes=set()
for m in menus:
    s=(B/m).read_text()
    chunks=re.split(r'(?=^### Day \d+ ·)',s,flags=re.MULTILINE)[1:]
    nums=[int(re.search(r'^### Day (\d+)',c).group(1)) for c in chunks]
    check(nums==list(range(11,21)),f'{m}: expected Days 11–20, got {nums}')
    for n,c in zip(nums,chunks):
        for stage in ['0–2:','2–4:','4–10:','10–12:']:
            check(stage in c,f'{m} Day {n}: missing {stage}')
        route=re.search(r'\*\*Routes:\*\* (.+?) \*\*Next/home:\*\*',c,re.DOTALL)
        check(bool(route),f'{m} Day {n}: no routes/home')
        if route: check(len(route.group(1).split(';'))>=3,f'{m} Day {n}: fewer than 3 routes')
        check('**Kit:**' in c,f'{m} Day {n}: no kit')
        check(bool(re.search(r'10–12:.*?Check|10–12:.*?check|10–12:.*?Staff',c,re.DOTALL)),f'{m} Day {n}: no closure check')
        allcodes.update(re.findall(r'AC9[A-Z0-9]+',c.split('\n',1)[0]))
arts=(B/'ARTS.md').read_text()
for art in ['Dance','Drama','Media Arts','Music','Visual Arts']:
    check(len(re.findall(rf'^### Day \d+ · {re.escape(art)}:',arts,re.MULTILINE))==2,f'Arts: {art} needs 2 days')
learner=(B/'LEARNER-COPY.md').read_text();assess=(B/'ASSESSMENT.md').read_text();key=(B/'TEACHER-KEY.md').read_text()
check([int(x) for x in re.findall(r'^## Day (\d+)$',learner,re.MULTILINE)]==list(range(11,21)),'learner copy days incomplete')
for subj in ['Science','HASS','HPE','Technologies','Arts']:
    for letter in 'AB':
        check(bool(re.search(rf'^### {subj} {letter} ·',assess,re.MULTILINE)),f'missing learner {subj} {letter}')
        check(bool(re.search(rf'^### {subj} {letter}$',key,re.MULTILINE)),f'missing private key {subj} {letter}')
check('### Science A' not in learner and '### Science B' not in learner,'answer key/check copied into everyday learner page')
alternatives=(B/'ALTERNATIVE-CONTEXTS.md').read_text()
altrows=re.findall(r'^\| (\d{2}) \|(.+)\|$',alternatives,re.MULTILINE)
check([int(n) for n,_ in altrows]==list(range(11,21)),'swap-in contexts need Days 11–20')
for n,row in altrows:
    check(len(row.split('|'))==5,f'Day {n}: expected one alternative per subject')
fw=json.loads((ROOT/'data/frameworks/acara-v9.json').read_text())
check(fw['source_sha256']=='db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3','framework source hash changed')
workbook=ROOT/'research/sources/acara-australian-curriculum-v9-download-2026-09-29.xlsx'
check(workbook.exists(),'pinned workbook missing')
if workbook.exists(): check(hashlib.sha256(workbook.read_bytes()).hexdigest()==fw['source_sha256'],'workbook hash differs')
records={r['code']:r for r in fw['records'] if r['record_type']=='content_description'}
cross=(B/'CURRICULUM-CROSSWALK.md').read_text()
rowcodes=set(re.findall(r'^\| (AC9[A-Z0-9]+) \|',cross,re.MULTILINE))
check(rowcodes==allcodes,f'crosswalk codes differ from menus {sorted(rowcodes^allcodes)}')
for c in allcodes:
    r=records.get(c)
    check(bool(r),f'unknown curriculum code {c}')
    if not r: continue
    level=r['attributes']['level']; desc=r['attributes']['content_description'].replace('|','\\|').replace('\n',' ')
    check(f'| {c} | {level} | {r["source_row"]} | {desc} |' in cross,f'{c}: level/row/wording not exact')
    expect='Year 5' if c.startswith(('AC9S5','AC9HS5')) else 'Years 5 and 6'
    check(level==expect,f'{c}: unexpected level {level}')
    check('Partial' in cross or 'Conditional' in cross,'crosswalk missing scope boundary')
# Local markdown links, including font-rights path outside this new pack.
links=0
for f in B.rglob('*.md'):
    for dest in re.findall(r'\[[^\]]*\]\(([^)]+)\)',f.read_text()):
        if dest.startswith(('https://','http://','#','mailto:')): continue
        dest=dest.split('#',1)[0].split('?',1)[0]
        check((f.parent/dest).exists(),f'{f.relative_to(B)} broken link: {dest}')
        links+=1
pairs=['inquiry-mat','observation-card','design-code-mat','arts-process-card']
for stem in pairs:
    svg=B/'print'/f'{stem}.svg';pdf=B/'print'/f'{stem}.pdf'
    check(svg.exists() and pdf.exists(),f'{stem} missing SVG/PDF')
    if not pdf.exists(): continue
    info=subprocess.run(['pdfinfo',str(pdf)],capture_output=True,text=True,check=False)
    check(info.returncode==0 and 'Pages:           1' in info.stdout and '595.276 x 841.89 pts (A4)' in info.stdout,f'{stem} PDF not one A4 page')
    extracted=subprocess.run(['pdftotext',str(pdf),'-'],capture_output=True,text=True,check=False)
    check(extracted.returncode==0 and len(extracted.stdout.strip())>100,f'{stem} PDF text missing')
    check('DejaVuSans' in subprocess.run(['pdffonts',str(pdf)],capture_output=True,text=True,check=False).stdout,f'{stem} font not embedded')
audio=B/'audio/four-beat-demo.wav'
with wave.open(str(audio)) as w:
    check((w.getnchannels(),w.getsampwidth(),w.getframerate(),w.getnframes())==(1,2,16000,38400),'original audio format/duration changed')
check('tone – silence – tone – tone' in (B/'audio/TEXT-ALTERNATIVE.md').read_text(),'audio text alternative missing pattern')
manifest=json.loads((B/'manifest.json').read_text())
files={p.relative_to(B).as_posix():p for p in B.rglob('*') if p.is_file() and p.name!='manifest.json' and '__pycache__' not in p.parts}
check(set(files)==set(manifest['files']),f'manifest file set mismatch {set(files)^set(manifest["files"])}')
for name,p in files.items():
    entry=manifest['files'].get(name,{})
    check(entry.get('sha256')==hashlib.sha256(p.read_bytes()).hexdigest() and entry.get('bytes')==p.stat().st_size,f'{name}: manifest hash/size mismatch')
print(f"{'PASS' if not ERR else 'FAIL'}: 5 menus × 10 days = 50 optional 12-minute blocks; 50 swap-in contexts; 10 fresh subject checks; {len(allcodes)} exact codes; 4 A4 SVG/PDF pairs; 1 original WAV; {links} local links; {len(files)} hashed files")
for e in ERR: print(' - '+e)
sys.exit(bool(ERR))
