#!/usr/bin/env python3
import json, os, socketserver, threading, http.server, time, urllib.parse
from pathlib import Path
from playwright.sync_api import sync_playwright
ROOT=Path('/mnt/data/nex_v307_actual_mcp_palm_snap/NEXT11_V307_ACTUAL_MCP_PALM_SNAP_ROOT')
OUT=ROOT/'diagnostics/v307'; OUT.mkdir(parents=True, exist_ok=True)
class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self,*args): pass
EVAL_JS="""() => {
 const keys=Object.keys(window).filter(k=>k.startsWith('__NEXPROD01')).sort();
 const grip=window.__NEXPROD01_GRIP_SOLVER_V1||null;
 const finger=window.__NEXPROD01_V307_ZPLUS_SEQUENTIAL_FINGER_CONTACT||window.__NEXPROD01_V306_ZPLUS_SEQUENTIAL_FINGER_CONTACT||window.__NEXPROD01_V304_ZPLUS_SEQUENTIAL_FINGER_CONTACT||window.__NEXPROD01_V301_FINGER_CHILD_LAYER_GATE||null;
 const v307=window.__NEXPROD01_V307_ACTUAL_MCP_PALM_PREFINGER_GATE||null;
 const v306=window.__NEXPROD01_V306_ACTUAL_SURFACE_PREFINGER_GATE||null;
 const v151=window.__NEXPROD01_V151_ACTUAL_MESH_TRUTH||null;
 const v153=window.__NEXPROD01_V153_MESH_FEEDBACK||null;
 const probe=window.__NEXPROD01_V93_ACTIVE_HAND_CONTROL_PROBE||null;
 const debug=window.__NEXPROD01_DEBUG_SCENE||null;
 const model=debug?.model||null;
 let boneCount=null, skinnedMeshCount=0;
 try{ if(model){ boneCount=0; model.traverse(o=>{if(o?.isBone) boneCount++; if(o?.isSkinnedMesh) skinnedMeshCount++;}); }}catch(e){}
 return {ready:document.readyState, href:location.href, hasDebug:!!debug, hasModel:!!model,boneCount,skinnedMeshCount,canvasCount:document.querySelectorAll('canvas').length,windowMarkers:keys,gripSolverV1:grip,fingerLayer:finger,v307ActualGate:v307,v306ActualGate:v306,v151MeshTruth:v151,v153MeshFeedback:v153,activeHandProbe:probe,bodyText:(document.body?.innerText||'').slice(0,1000)};
}"""
def summarise(val):
    val=val or {}; grip=val.get('gripSolverV1') or {}; stages=grip.get('stages') or {}; finger=val.get('fingerLayer') or {}; v307=val.get('v307ActualGate') or {}; entry=stages.get('v297EntryFrameGate') or {}; repair=stages.get('v306EntryFrameNeutralizeRotation') or {}; side=stages.get('v301SideSurfaceContactGate') or {}; mcp=(v307.get('v307ActualMcpPalmGate') or {}) if isinstance(v307,dict) else {}; mcpMetrics=mcp.get('metrics') or {}
    return {
      'hasModel':val.get('hasModel'),'boneCount':val.get('boneCount'),'skinnedMeshCount':val.get('skinnedMeshCount'),
      'runtimeVersion':grip.get('version'),'side':grip.get('side'),'gripApplied':grip.get('applied'),'gripPass':grip.get('pass'),'gripStatus':grip.get('status'),'gripReason':grip.get('reason'),'nextPhase':grip.get('nextPhase'),
      'entryPass':entry.get('pass'),'entryGate':entry.get('gate'),'entryRequiredOutwardShiftM':entry.get('requiredOutwardShiftM'),'entrySignedOutsideM':entry.get('signedOutsideM'),
      'repairApplied':repair.get('applied'),'repairPass':repair.get('pass'),'repairReason':repair.get('reason'),'repairRollDeg':repair.get('rollDeg'),
      'sideSurfacePass':side.get('pass'),'sideSurfaceStatus':side.get('status'),
      'v307GatePass':v307.get('pass'),'v307EntryOk':v307.get('entryOk'),'v307SolverOk':v307.get('solverOk'),
      'actualMcpPass':mcp.get('pass'),'actualMcpStatus':mcp.get('status'),
      'actualMcpPalmFacesObject':(mcp.get('basis') or {}).get('palmFacesObject'),
      'actualMcpIndexDist':((mcpMetrics.get('index') or {}).get('distanceToSurfaceM')),
      'actualMcpPinkyDist':((mcpMetrics.get('pinky') or {}).get('distanceToSurfaceM')),
      'actualMcpIndexPen':((mcpMetrics.get('index') or {}).get('penetrationM')),
      'actualMcpPinkyPen':((mcpMetrics.get('pinky') or {}).get('penetrationM')),
      'actualMcpIndexSide':((mcpMetrics.get('index') or {}).get('sideOk')),
      'actualMcpPinkySide':((mcpMetrics.get('pinky') or {}).get('sideOk')),
      'fingerSchema':finger.get('schema'),'fingerActive':finger.get('active'),'fingerBlocked':finger.get('blocked'),'fingerReason':finger.get('reason'),'fingerReady':finger.get('ready'),'fingerReleaseEnabled':finger.get('releaseEnabled')
    }
def main():
    os.chdir(ROOT)
    srv=socketserver.TCPServer(('127.0.0.1',0), Quiet); threading.Thread(target=srv.serve_forever,daemon=True).start(); port=srv.server_address[1]
    cases=[('right','v94_right_full','0.75'),('left','v94_left_full','0.25')]
    results=[]
    try:
      with sync_playwright() as p:
        env=os.environ.copy(); env['LIBGL_ALWAYS_SOFTWARE']='1'; env['VK_ICD_FILENAMES']='/usr/lib/chromium/vk_swiftshader_icd.json'; env['DISPLAY']=''
        browser=p.chromium.launch(executable_path='/usr/bin/chromium',headless=True,env=env,args=['--no-sandbox','--disable-dev-shm-usage','--ignore-gpu-blocklist','--enable-webgl','--enable-webgl2','--enable-unsafe-swiftshader','--disable-gpu-sandbox','--ozone-platform=headless','--use-gl=angle','--use-angle=swiftshader-webgl','--remote-allow-origins=*'])
        ctx=browser.new_context(viewport={'width':1600,'height':1000},device_scale_factor=1)
        for name,caseId,phase in cases:
          page=ctx.new_page()
          params={'exercise':'hurdle-stretch','caseId':caseId,'phase':phase,'play':'0','debug':'0','visualMode':'nex','nativeRuntimeParity':'1','nativeSkin':'1','skinPreset':'freeridehuman','readyTimeoutMs':'12000','hostLifecycle':'v307-actual-mcp-palm-snap','build':'NEXPROD01_RUNTIME_V307_ACTUAL_MCP_PALM_SNAP_2026_08_19','safeBoot':'0','lowPower':'1','pixelRatioCap':'1.15','antialias':'1','maxFPS':'24','staticAfterReady':'0','idleSleepAfterReady':'0','v':'V307_'+name}
          url=f'http://127.0.0.1:{port}/assets/nexprod/nexprod01-player.html?'+urllib.parse.urlencode(params)
          navErr=None
          try: page.goto(url,wait_until='domcontentloaded',timeout=15000)
          except Exception as e: navErr=repr(e)
          final=None; polls=[]
          for i in range(22):
            page.wait_for_timeout(700)
            try: val=page.evaluate(EVAL_JS)
            except Exception as e: polls.append({'i':i,'evalErr':repr(e)}); continue
            final=val; polls.append({'i':i,'hasModel':val.get('hasModel'),'hasGrip':bool(val.get('gripSolverV1'))})
            if val.get('hasModel') and val.get('gripSolverV1'):
              page.wait_for_timeout(500); final=page.evaluate(EVAL_JS); break
          shot=str(OUT/f'V307_{name}_canvas.png')
          try: page.locator('canvas').first.screenshot(path=shot, timeout=8000)
          except Exception:
            try: page.screenshot(path=shot, full_page=False)
            except Exception: shot=None
          results.append({'name':name,'caseId':caseId,'phase':phase,'url':url,'navigationError':navErr,'polls':polls,'final':final,'summary':summarise(final),'screenshot':shot})
          page.close()
        browser.close()
    finally:
      srv.shutdown(); srv.server_close()
    audit={'schema':'NEXT11_V307_BROWSER_ACTUAL_MCP_PALM_SNAP_AUDIT','generatedAt':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'root':str(ROOT),'results':results}
    audit['passBrowserRuntime']=all(r['summary']['hasModel'] and r['summary']['runtimeVersion']=='V307_ACTUAL_MCP_PALM_SNAP' for r in results)
    audit['mcpSnapGatePresent']=all(r['summary']['actualMcpStatus'] is not None for r in results)
    audit['noFingerReleaseByDefault']=all(r['summary']['fingerBlocked'] is True and r['summary']['fingerActive'] is not True for r in results)
    audit['summaries']=[r['summary'] for r in results]
    (OUT/'V307_BROWSER_ACTUAL_MCP_PALM_SNAP_AUDIT.json').write_text(json.dumps(audit,indent=2),encoding='utf-8')
    print(json.dumps({'audit':str(OUT/'V307_BROWSER_ACTUAL_MCP_PALM_SNAP_AUDIT.json'),'passBrowserRuntime':audit['passBrowserRuntime'],'mcpSnapGatePresent':audit['mcpSnapGatePresent'],'noFingerReleaseByDefault':audit['noFingerReleaseByDefault'],'summaries':audit['summaries']},indent=2))
if __name__=='__main__': main()
