#!/usr/bin/env python3
"""Fail-closed local structural, provenance, link, print and audio checks."""
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'];codes=set()
for name in menus:
 s=(B/name).read_text();blocks=re.split(r'(?=^### Day \d+ ·)',s,flags=re.MULTILINE)[1:]
 days=[int(re.search(r'^### Day (\d+)',x).group(1)) for x in blocks]
 check(days==list(range(11,21)),f'{name}: Days 11–20 incomplete {days}')
 for day,block in zip(days,blocks):
  header=block.split('\n',1)[0];codes.update(re.findall(r'AC9[A-Z0-9]+',header))
  for stage in ['0–2:','2–4:','4–10:','10–12:']:
   check(stage in block,f'{name} D{day}: missing {stage}')
  check('**Kit:**' in block,f'{name} D{day}: kit missing')
  check('10–12: Check:' in block,f'{name} D{day}: closure check missing')
  routes=re.search(r'\*\*Routes:\*\* (.*?) \*\*Response move:\*\*',block,re.DOTALL)
  check(bool(routes),f'{name} D{day}: routes missing')
  if routes:check(len(routes.group(1).split(';'))>=3,f'{name} D{day}: fewer than 3 routes')
  check('**Response move:**' in block and '**Home:**' in block,f'{name} D{day}: response/home missing')
arts=(B/'ARTS.md').read_text()
for form in ['Dance','Drama','Media Arts','Music','Visual Arts']:
 check(len(re.findall(rf'^### Day \d+ · {re.escape(form)}:',arts,re.MULTILINE))==2,f'Arts: {form} needs 2 days')
learner=(B/'LEARNER-COPY.md').read_text();checks=(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 11–20 incomplete')
for subj in ['Science','HASS','HPE','Technologies','Arts']:
 for letter in 'AB':
  check(bool(re.search(rf'^### {subj} {letter} ·',checks,re.MULTILINE)),f'learner check {subj} {letter} missing')
  check(bool(re.search(rf'^### {subj} {letter}$',key,re.MULTILINE)),f'staff key {subj} {letter} missing')
check('### Science A' not in learner and '### Science B' not in learner,'key/check copied into daily learner page')
alt=(B/'ALTERNATIVE-CONTEXTS.md').read_text();rows=re.findall(r'^\| (\d{2}) \|(.+)\|$',alt,re.MULTILINE)
check([int(n) for n,_ in rows]==list(range(11,21)),'50 swap-in contexts incomplete')
for n,row in rows:check(len(row.split('|'))==5,f'D{n}: expected five swap-in cells')
fw=json.loads((ROOT/'data/frameworks/acara-v9.json').read_text());sourcehash='db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3'
check(fw['source_sha256']==sourcehash,'pinned framework source hash changed')
workbook=ROOT/'research/sources/acara-australian-curriculum-v9-download-2026-09-29.xlsx'
check(workbook.exists(),'pinned official workbook missing')
if workbook.exists():check(hashlib.sha256(workbook.read_bytes()).hexdigest()==sourcehash,'official workbook hash mismatch')
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==codes,f'crosswalk codes differ from menus {sorted(rowcodes^codes)}')
for c in codes:
 r=records.get(c);check(bool(r),f'unknown code {c}')
 if not r:continue
 level=r['attributes']['level'];expected='Year 6' if c.startswith(('AC9S6','AC9HS6')) else 'Years 5 and 6'
 check(level==expected,f'{c}: level {level} not {expected}')
 desc=r['attributes']['content_description'].replace('|','\\|').replace('\n',' ')
 check(f'| {c} | {level} | {r["source_row"]} | {desc} |' in cross,f'{c}: row/wording not exact')
links=0
for f in B.rglob('*.md'):
 for dest in re.findall(r'\[[^\]]*\]\(([^)]+)\)',f.read_text()):
  if dest.startswith(('https://','http://','#','mailto:')):continue
  path=dest.split('#',1)[0].split('?',1)[0]
  check((f.parent/path).exists(),f'{f.relative_to(B)} broken link {path}');links+=1
pairs=['evidence-mat','decision-mat','observation-card','design-data-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 (svg.exists() and pdf.exists()):continue
 s=svg.read_text();check('viewBox="0 0 794 1123"' in s and 'role="img"' in s and '<title' in s and '<desc' in s,f'{stem}: SVG access/A4 metadata missing')
 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 not extractable')
 fonts=subprocess.run(['pdffonts',str(pdf)],capture_output=True,text=True,check=False).stdout
 check('DejaVuSans' in fonts and 'yes yes yes' in fonts,f'{stem}: font subset not embedded')
audio=B/'audio/eight-beat-cue.wav'
with wave.open(str(audio)) as w:
 check((w.getnchannels(),w.getsampwidth(),w.getframerate(),w.getnframes())==(1,2,16000,57600),'audio format or duration mismatch')
check('STRONG soft soft | STRONG soft soft | STRONG soft' in (B/'audio/TEXT-ALTERNATIVE.md').read_text(),'audio text/tactile pattern missing')
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 drift {set(files)^set(manifest["files"])}')
for name,p in files.items():
 e=manifest['files'].get(name,{})
 check(e.get('sha256')==hashlib.sha256(p.read_bytes()).hexdigest() and e.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-ins; 10 fresh subject checks; {len(codes)} exact official rows; 5 A4 SVG/PDF/text aids; 1 original WAV; {links} local links; {len(files)} hashed files")
for x in ERR:print(' - '+x)
sys.exit(bool(ERR))
