"""Actual Three.js / Chromium evidence. Run from the kit directory.

python3 browser_check.py --preflight
python3 browser_check.py                 # final kit; optional test_house.glb
"""
import argparse
import asyncio
import functools
import hashlib
import http.server
import json
import subprocess
import threading
from pathlib import Path

from playwright.async_api import async_playwright

ROOT = Path(__file__).resolve().parent
ATOMS = ['dormer_gabled','dormer_hipped','dormer_wall','louvre_ridge','chimney_cluster','chimney_twin','sign_boot','sign_pig','sign_key','sign_pretzel','oriel','door_arched_stone','door_cellar','window_cellar','window_shuttered','finial','thatch_panel','thatch_ridge','pillory','well_wheel']


class QuietHandler(http.server.SimpleHTTPRequestHandler):
    def log_message(self, *_):
        pass


def claim(path):
    resource = 'local:' + str(path)
    cli = '/home/claude-agent/.claude/coord/coord'
    subprocess.run([cli, 'check', resource], check=True, capture_output=True)
    subprocess.run([cli, 'claim', resource, '--owner', 'runtime', '--ttl', '3h'], check=True, capture_output=True)


def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


async def main(preflight=False,kit_only=False,assembly_only=False):
    data = ROOT / 'viewer/preflight' if preflight else ROOT
    manifest = json.loads((data/'kit.json').read_text())
    assert preflight or sorted(x['node_name'] for x in manifest['atoms']) == sorted(ATOMS)
    handler = functools.partial(QuietHandler, directory=str(ROOT))
    server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), handler)
    threading.Thread(target=server.serve_forever, daemon=True).start()
    errors, failed_requests, http_findings = [], [], []
    report = {'status': 'RUNNING', 'captures': {}}
    output = ROOT/('preflight_runtime.json' if preflight else 'runtime_check.json')
    if assembly_only:
        report=json.loads(output.read_text())
        assert report['status']=='PASS', 'Previous kit captures must pass before assembly supplementation'
        for file,digest in {**report['inputHashes'],**report['textureHashes']}.items():
            if file!='test_house.glb':assert sha(ROOT/file)==digest, 'Kit capture input changed: '+file
        report['status']='RUNNING'
    claim(output)
    try:
      async with async_playwright() as p:
        browser=await p.chromium.launch(headless=True,args=['--no-sandbox','--use-angle=swiftshader','--enable-unsafe-swiftshader'])
        page=await browser.new_page(viewport={'width':1440,'height':1080},device_scale_factor=1)
        page.on('pageerror',lambda e:errors.append(str(e)))
        page.on('requestfailed',lambda r:failed_requests.append({'url':r.url,'error':r.failure}))
        page.on('response',lambda r:http_findings.append({'url':r.url,'status':r.status}) if r.status>=400 else None)
        url=f'http://127.0.0.1:{server.server_port}/viewer/?qa=1'+('&base=./preflight/' if preflight else '')
        if kit_only:url+='&kit-only=1'
        await page.goto(url)
        await page.wait_for_function('window.__KIT_QA__?.ready || window.__KIT_QA__?.error',timeout=120000)
        report.update(await page.evaluate('window.__KIT_QA__'))
        assert report.get('ready'),report.get('error','Kit viewer did not become ready')
        assert sorted(report['atoms'])==sorted(a['node_name'] for a in manifest['atoms'])
        for name, row in report['parity'].items():
            assert row['rawPrimitives']==row['runtimePrimitives'],(name,row)
            assert row['rawTriangles']==row['runtimeTriangles']==row['manifestTriangles'],(name,row)
        assert report['instancing']['one']['assetMainPass']['calls']==report['instancing']['three']['assetMainPass']['calls']
        assert report['instancing']['one']['assetMainPass']['triangles']*3==report['instancing']['three']['assetMainPass']['triangles']
        assert all(not t['flipY'] for t in report['textureBinding'])
        async def capture(name, rear=False, clay=False, camera='orbit'):
            await page.evaluate('([n,r,c,m])=>{window.kitRear(r);window.kitClay(c);window.kitView(n);window.kitCamera(m)}',[name,rear,clay,camera])
            await page.wait_for_timeout(200)
            metrics=await page.evaluate('window.kitMetrics()')
            assert metrics['selection']==name and metrics['rear']==rear and metrics['clay']==clay and metrics['camera']['mode']==camera,metrics
            assert metrics['assetMainPass']['triangles']==metrics['batch']['tris'],metrics
            assert metrics['assetMainPass']['calls']==metrics['batch']['drawCalls'],metrics
            assert metrics['assetMainPass']['triangles']>0,metrics
            stem=name+('_'+camera if camera!='orbit' else '')+('_rear' if rear else '')+('_clay' if clay else '')
            target=ROOT/(('preflight_runtime_' if preflight else 'runtime_')+stem+'.png')
            claim(target)
            await page.screenshot(path=str(target))
            metrics.update({'file':target.name,'sha256':sha(target)})
            report['captures'][stem]=metrics
        if not assembly_only:
            await capture('all')
            report['all_metrics']=report['captures']['all']
        assert report['all_metrics']['assetMainPass']['triangles']==manifest['total_triangles']
        if preflight:
            assert report['parity']['probe']['rawPrimitives']==2
            # The two colored primitives must both appear in actual pixels.
            from PIL import Image
            import numpy as np
            pixels=np.asarray(Image.open(ROOT/'preflight_runtime_all.png').convert('RGB'))[150:-55]
            red=((pixels[:,:,0]>pixels[:,:,2]*1.25)&(pixels[:,:,0]>pixels[:,:,1]*1.25)).sum()
            blue=((pixels[:,:,2]>pixels[:,:,0]*1.25)&(pixels[:,:,2]>pixels[:,:,1]*1.1)).sum()
            report['primitivePixels']={'red':int(red),'blue':int(blue)}
            assert red>100 and blue>100,report['primitivePixels']
        else:
            if not assembly_only:
                for name in ATOMS:
                    await capture(name)
                for name in ['dormer_gabled','dormer_hipped','dormer_wall','louvre_ridge','oriel','door_arched_stone','sign_boot','sign_pig','sign_key','sign_pretzel','well_wheel','pillory','thatch_panel','thatch_ridge']:
                    await capture(name,rear=True,clay=True)
                await capture('all',clay=True)
            if assembly_only:assert report['assemblyAvailable'],'Final assembly missing'
            if report['assemblyAvailable']:
                await capture('assembly')
                await capture('assembly',camera='front')
                await capture('assembly',camera='cottage')
                await capture('assembly',rear=True,clay=True)
            report['assemblyStatus']='SKIPPED FOR KIT-ONLY RUN' if kit_only else ('MEASURED' if report['assemblyAvailable'] else 'NOT PROVIDED')
        report['errors']=errors;report['failedRequests']=failed_requests;report['httpFindings']=http_findings
        assert not errors and not failed_requests,(errors,failed_requests)
        assert all(r['status']==404 and r['url'].endswith(('style.json','test_house.glb')) for r in http_findings),http_findings
        report['inputHashes']={str(path.relative_to(ROOT)):sha(path) for path in [data/'kit.glb',data/'kit.json',data/'materials.json',ROOT/'browser_check.py',ROOT/'viewer/index.html',ROOT/'viewer/main.mjs',ROOT/'viewer/runtime-loader.mjs',ROOT/'viewer/shelf-extras.mjs',ROOT/'viewer/lib/three.module.js',ROOT/'viewer/lib/three.core.js',ROOT/'viewer/lib/jsm/loaders/GLTFLoader.js']}
        shelf=json.loads((data/'materials.json').read_text())
        texture_files=sorted({m[key] for m in shelf['materials'].values() for key in ['base','normal','orm','height'] if m.get(key)})
        report['textureHashes']={str((data/file).relative_to(ROOT)):sha(data/file) for file in texture_files}
        if report['assemblyAvailable']:report['inputHashes']['test_house.glb']=sha(ROOT/'test_house.glb')
        report['browserVersion']=browser.version
        report['performance_note']='Chromium SwiftShader software WebGL. No device FPS or timing claim. assetMainPass is a measured asset-only render with floor and shadow passes disabled. fullRender is the displayed main-pass renderer.info count including the floor; Three r184 resets renderer.info after its shadow pass, so shadow draws are not included.'
        report['sharedBytes_note']='Estimate from decoded geometry and texture sizes in loader; does not include allocation overhead.'
        report['loader_note']='Copied production render.mjs snapshot from medieval-trades; narrow patches compact per-material groups, reject missing shelf materials, and include index bytes in geometry footprint. Shared maps use flipY=false and shelf emissive metadata.'
        report['status']='PASS'
        await browser.close()
    except Exception as e:
        report.update(status='FAIL',failure=str(e),errors=errors,failedRequests=failed_requests)
        raise
    finally:
        output.write_text(json.dumps(report,indent=2))
        server.shutdown()
        print(json.dumps({k:report.get(k) for k in ['status','failure','all_metrics','primitivePixels','assemblyStatus','errors','failedRequests']}))


if __name__=='__main__':
    parser=argparse.ArgumentParser();modes=parser.add_mutually_exclusive_group()
    modes.add_argument('--preflight',action='store_true');modes.add_argument('--kit-only',action='store_true');modes.add_argument('--assembly-only',action='store_true')
    args=parser.parse_args();asyncio.run(main(args.preflight,args.kit_only,args.assembly_only))
