#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
"""Fail-closed local checks for Year 7 optional supplementary Weeks 3–4."""
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];errors=[]
def check(ok,msg):
 if not ok:errors.append(msg)
menus=['SCIENCE.md','HASS.md','HPE.md','TECHNOLOGIES.md','ARTS.md'];codes=set()
for name in menus:
 text=(B/name).read_text();blocks=re.split(r'(?=^### Day \d+ ·)',text,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 timed stage {stage}')
  check('**Kit:**' in block and '10–12: Check:' in block,f'{name} D{day}: kit/closure 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 three access 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 two days')
hass=(B/'HASS.md').read_text()
for area in ['Geography','History','Civics','Economics']:
 check(area in hass,f'HASS: {area} missing')
learner=(B/'LEARNER-COPY.md').read_text();assessment=(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 daily copy incomplete')
for sub in ['Science','HASS','HPE','Technologies','Arts']:
 for letter in 'AB':
  check(bool(re.search(rf'^### {sub} {letter} ·',assessment,re.MULTILINE)),f'{sub} {letter} fresh learner check missing')
  check(bool(re.search(rf'^### {sub} {letter}$',key,re.MULTILINE)),f'{sub} {letter} private staff key missing')
check('### Science A' not in learner and '### Science B' not in learner,'daily learner copy contains check answer material')
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)),'swap-in rows need Days 11–20')
for n,row in rows:check(len(row.split('|'))==5,f'D{n}: fewer than five subject alternatives')
sourcehash='db446882d2c00cf7c085a03e250e2442fda6c44011fc114680c46c1dc7a822c3'
fw=json.loads((ROOT/'data/frameworks/acara-v9.json').read_text());check(fw['source_sha256']==sourcehash,'normalized source hash drift')
workbook=ROOT/'research/sources/acara-australian-curriculum-v9-download-2026-09-29.xlsx';check(workbook.exists(),'official workbook missing')
if workbook.exists():check(hashlib.sha256(workbook.read_bytes()).hexdigest()==sourcehash,'official workbook hash drift')
records={r['code']:r for r in fw['records'] if r['record_type']=='content_description'}
cross=(B/'CURRICULUM-CROSSWALK.md').read_text();listed=set(re.findall(r'^\| (AC9[A-Z0-9]+) \|',cross,re.MULTILINE))
check(listed==codes,f'crosswalk/menu code mismatch {sorted(listed^codes)}')
for c in codes:
 r=records.get(c);check(bool(r),f'unknown code {c}')
 if not r:continue
 lev=r['attributes']['level'];expect='Year 7' if c.startswith(('AC9S7','AC9HG7','AC9HH7','AC9HC7','AC9HE7')) else 'Years 7 and 8'
 check(lev==expect,f'{c}: level {lev} not {expect}')
 desc=r['attributes']['content_description'].replace('|','\\|').replace('\n',' ')
 check(f'| {c} | {lev} | {r["source_row"]} | {desc} |' in cross,f'{c}: wording/row not exact')
links=0
for file in B.rglob('*.md'):
 for raw in re.findall(r'\[[^\]]*\]\(([^)]+)\)',file.read_text()):
  if raw.startswith(('https://','http://','#','mailto:')):continue
  dest=raw.split('#',1)[0].split('?',1)[0];check((file.parent/dest).exists(),f'{file.relative_to(B)} broken link {dest}');links+=1
pairs=['inquiry-sheet','source-decision-sheet','observation-card','design-data-sheet','arts-process-sheet','binary-grid']
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}: A4/access 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}: not one-page A4')
 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}: 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 not embedded')
with wave.open(str(B/'audio/layered-eight-beat.wav')) as w:
 check((w.getnchannels(),w.getsampwidth(),w.getframerate(),w.getnframes())==(1,2,16000,64000),'original cue format/duration drift')
check('Layer A: ● – ● – ● – ● –' in (B/'audio/TEXT-ALTERNATIVE.md').read_text(),'audio text/tactile grid missing')
code=subprocess.run(['python3',str(B/'code/route_choice.py')],capture_output=True,text=True,check=False)
check(code.returncode==0 and code.stdout.count('match=False')==1 and code.stdout.count('match=True')==2,'starter no longer has one intentional debug case')
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}: hash/size mismatch')
print(f"{'PASS' if not errors else 'FAIL'}: 5 menus × 10 days = 50 optional 12-minute blocks; 50 swap-ins; 10 fresh subject checks; {len(codes)} exact official rows; 6 A4 SVG/PDF/text aids; 1 original WAV; 1 runnable offline code lab; {links} local links; {len(files)} hashed files")
for x in errors:print(' - '+x)
sys.exit(bool(errors))
