"""Read-only actual URL/file verification. A 200 fallback catalogue is a failure."""
from pathlib import Path
import requests,concurrent.futures,json,hashlib,datetime,io,argparse,re
from urllib.parse import urlsplit
from PIL import Image
root=Path(__file__).resolve().parents[1];p=argparse.ArgumentParser();p.add_argument('--base',required=True);p.add_argument('--output',default='hosted-integrity.json');p.add_argument('--full-archives',action='store_true');a=p.parse_args();base=a.base.rstrip('/')+'/'
media=json.loads((root/'manifests/media.json').read_text())['assets'];manifest=json.loads((root/'site/review/asset-manifest.json').read_text())['assets']
headers={'User-Agent':'Mozilla/5.0 Komplex Readback','Accept-Encoding':'identity'}
paths={x['path'].split('?')[0].split('#')[0] for x in manifest if not x['path'].startswith('../')}
paths|={'app/app.js','app/app.css','app/lessons.json','app/demo-video.mp4','flow/flow.js','flow/flow.css','flow/emails.json','canvas.js','canvas.css'}
media_by={x['file']:x for x in media}

def normalize_original(text):
 text=re.sub(r'<a\b(?=[^>]*\bhref\s*=\s*[\"\x27]mailto:)[^>]*>[\s\S]*?</a>','__CF_EMAIL__',text,flags=re.I)
 return re.sub(r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}','__CF_EMAIL__',text,flags=re.I)
def normalize_delivery(text):
 decoded=[]
 def anchor(m):
  e=re.search(r'data-cfemail\s*=\s*[\"\x27]([^\"\x27]+)',m.group(0),re.I).group(1);key=int(e[:2],16)
  decoded.append(bytes(int(e[i:i+2],16)^key for i in range(2,len(e),2)).decode('utf-8'))
  return '__CF_EMAIL__'
 text=re.sub(r'<a\b(?=[^>]*\b__cf_email__\b)(?=[^>]*\bdata-cfemail\s*=)[^>]*>[\s\S]*?</a>',anchor,text,flags=re.I)
 text=re.sub(r'<script\b[^>]*\bsrc\s*=\s*[\"\x27][^\"\x27]*/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode\.min\.js[^\"\x27]*[\"\x27][^>]*>\s*</script>','',text,flags=re.I)
 text=re.sub(r'<script\b[^>]*\bdata-cfasync\s*=\s*[\"\x27]false[\"\x27][^>]*>\s*</script>','',text,flags=re.I)
 return text,decoded
def verify(path):
 local=root/'private/mirror-recovery/verified-host-snapshot'/urlsplit(base).path.strip('/')/path;url=base+path
 try:
  if local.suffix.lower()=='.zip' and not a.full_archives:
   resp=requests.get(url,headers={**headers,'Range':'bytes=0-7'},timeout=45,stream=True);resp.raise_for_status();signature=next(resp.iter_content(chunk_size=8),b'');resp.close();ok=signature.startswith(b'PK\x03\x04');return {'path':path,'url':url,'status':resp.status_code,'check':'actual ZIP signature','pass':ok}
  resp=requests.get(url,headers=headers,timeout=45);resp.raise_for_status();expected=hashlib.sha256(local.read_bytes()).hexdigest();actual=hashlib.sha256(resp.content).hexdigest();row={'path':path,'url':url,'final_url':resp.url,'status':resp.status_code,'expected_sha256':expected,'actual_sha256':actual,'pass':expected==actual}
  if not row['pass'] and local.suffix=='.html':
   normalized,decoded=normalize_delivery(resp.text);source=local.read_text();emails=set(re.findall(r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}',source,re.I))
   row['normalized_delivery_exact']=bool(decoded) and normalized==normalize_original(source) and set(decoded)<=emails
   row['known_cf_email_transform_only']=row['normalized_delivery_exact'];row['pass']=row['normalized_delivery_exact']
  if path in media_by:
   im=Image.open(io.BytesIO(resp.content));row['pixels']=list(im.size);row['pass'] &= list(im.size)==[media_by[path]['width'],media_by[path]['height']]
  return row
 except Exception as e:return {'path':path,'url':url,'pass':False,'error':str(e)}
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:rows=list(pool.map(verify,sorted(paths)))
archive_check = 'Complete downloaded ZIP bytes match their SHA-256' if a.full_archives else 'ZIPs verified by actual ZIP signature'
receipt={'at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'base':base,'records':rows,'checks':len(rows),'passed':sum(x['pass'] for x in rows),'failed':[x for x in rows if not x['pass']],'scope':f'Every manifest asset path, actual media bytes/dimensions and static runtime. {archive_check}, not only 200/HEAD. Root review widget is injected during publication and verified separately. Known Cloudflare email transformations are accepted only after exact normalized-source and decoded-address equality.'}
(root/'evidence'/a.output).write_text(json.dumps(receipt,indent=2));print(json.dumps({k:v for k,v in receipt.items() if k!='records'},indent=2));assert not receipt['failed'],'Hosted assets or bytes failed verification'
