from ac28_audit import *
import hashlib,shutil
from PIL import Image

def main():
    web=DATA/'viewer/explorer_data';sources=[]
    for path in sorted(DATA.rglob('*')):
        if not path.is_file() or 'viewer' in path.parts:continue
        if path.suffix.lower() not in ('.stp','.ply','.xls','.csv'):continue
        sha=hashlib.sha256()
        with path.open('rb') as f:
            for block in iter(lambda:f.read(8*1024*1024),b''):sha.update(block)
        sources.append({'path':str(path.relative_to(ROOT)),'bytes':path.stat().st_size,'sha256':sha.hexdigest()})
    (web/'source_manifest.json').write_text(json.dumps({'hash':'SHA-256','sources':sources},indent=2,ensure_ascii=False))
    for path in OUT.glob('*_graph_observations.npz'):shutil.copy2(path,web/path.name)
    meta=json.loads((web/'metadata.json').read_text())
    scanbase=next(p for p in DATA.glob('1. PLY*') if p.is_dir())
    for name,info in meta['images'].items():
        folder=next(scanbase.glob('*16*' if name=='16h' else '*18*'))
        img=Image.open(folder/'Merged_Image.png');x,y=info['dent_pixel']
        box=[max(0,x-512),max(0,y-512),min(img.width,x+512),min(img.height,y+512)]
        img.crop(box).save(web/f'dent_crop_{name}.png');info['crop_box']=box
    (web/'metadata.json').write_text(json.dumps(meta,indent=2,ensure_ascii=False))
    g=np.load(web/'cad_graph.npz');n=len(g['pos_mm']);e=g['edge_index']
    assert e.shape[0]==2 and e.min()>=0 and e.max()<n
    assert np.all(e[0]!=e[1])
    assert set(map(tuple,e.T))==set(map(tuple,e[::-1].T))
    assert np.isfinite(g['pos_mm']).all() and np.isfinite(g['edge_length_mm']).all()
    assert np.isfinite(g['k1_per_mm'][g['curvature_valid']]).all()
    baseline=json.loads((ROOT/'analysis/baseline_transforms.json').read_text())
    for key,v in meta['variants'].items():
        t=np.array(v['transform']);r=t[:3,:3]
        assert np.allclose(r.T@r,np.eye(3),atol=1e-8),key
        assert np.allclose(np.abs(np.linalg.det(r)),1,atol=1e-8),key
        obs=np.load(web/f'{key}_graph_observations.npz');assert len(obs['training_valid'])==n and not obs['training_valid'].any()
        with (web/f'{key}.bin').open('rb') as f:
            count=int(np.frombuffer(f.read(4),dtype='<u4')[0]);a=np.frombuffer(f.read(),dtype='<f4').reshape(count,9)
        assert count==v['n_display'] and np.isfinite(a[:,:5]).all()
        assert np.all(a[:,3]>=0)
        assert np.all(np.abs(a[:,4])<=a[:,3]+.002),key
        assert v['median_mm']<=v['p90_mm']<=v['p95_mm']
        assert np.allclose(t,obs['transform_raw_to_cad'])
    assert np.array_equal(np.array(meta['variants']['18h01']['transform']),np.array(baseline['18h01']))
    result={'source_count':len(sources),'graph_nodes':n,'directed_edges':e.shape[1],'curvature_mapped_nodes':int(g['curvature_valid'].sum()),'checks':['orthogonal transforms; determinant magnitude 1','18:01 baseline transform bit-for-bit preserved','finite exported coordinates and distances','normal projection bounded by Euclidean distance','graph indices, reverse edges, and no self edges','all training_valid masks false','binary sizes and displayed counts match metadata']}
    (OUT/'numerical_validation.json').write_text(json.dumps(result,indent=2));print(result)

if __name__=='__main__':main()
