#!/usr/bin/env python3
import json, os, socketserver, threading, http.server, time, urllib.parse, math
from pathlib import Path
from playwright.sync_api import sync_playwright

ROOT = Path('/mnt/data/nex_v302_left_foot_ik_source_fix/NEXT11_V302_LEFT_FOOT_IK_SOURCE_FIX_ROOT')
OUT = ROOT / 'diagnostics/V302_BROWSER_RUNTIME_AUDIT.json'
SHOT_DIR = ROOT / 'diagnostics/v302_browser_screens'
SHOT_DIR.mkdir(parents=True, exist_ok=True)

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

def start_server():
    os.chdir(ROOT)
    srv=socketserver.TCPServer(('127.0.0.1',0), QuietHandler)
    threading.Thread(target=srv.serve_forever,daemon=True).start()
    return srv, srv.server_address[1]

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_V301_FINGER_CHILD_LAYER_GATE || window.__NEXPROD01_V297_PREFINGER_CHILD_LAYER_GATE || null;
  const v110=window.__NEXPROD01_V110_DISTAL_GRIP_COUPLED || null;
  const v151=window.__NEXPROD01_V151_ACTUAL_MESH_TRUTH || 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, canvasCount=document.querySelectorAll('canvas').length;
  try { if(model){ boneCount=0; model.traverse(o=>{ if(o?.isBone) boneCount++; if(o?.isSkinnedMesh) skinnedMeshCount++; }); } } catch(e) {}
  const getPts=(section)=>probe?.[section]?.points || null;
  return {href:location.href, ready:document.readyState, hasDebug:!!debug, hasModel:!!model, boneCount, skinnedMeshCount, canvasCount, windowMarkers:keys, markerCount:keys.length, gripSolverV1:grip, v301FingerGate:finger, v110DistalGrip:v110, v151MeshTruth:v151, activeHandProbe:probe,
    footPoints:{rawAuthored:getPts('rawAuthored'), finalNormalized:getPts('finalNormalized')},
    bodyText:(document.body?.innerText||'').slice(0,1000)};
}"""

def vec(a,b):
    if not (isinstance(a,dict) and isinstance(b,dict)): return None
    return {k: b[k]-a[k] for k in ('x','y','z') if k in a and k in b}

def vlen(v):
    if not isinstance(v,dict) or len(v)<3: return None
    return math.sqrt(sum(v[k]*v[k] for k in ('x','y','z')))

def norm(v):
    L=vlen(v)
    if not L: return None
    return {k:v[k]/L for k in ('x','y','z')}

def r(v,n=6):
    if isinstance(v,float): return round(v,n)
    if isinstance(v,dict): return {k:round(v[k],n) for k in v}
    return v

def summarize(final, side):
    final=final or {}
    grip=final.get('gripSolverV1') if isinstance(final,dict) else None
    finger=final.get('v301FingerGate') if isinstance(final,dict) else None
    probe=final.get('activeHandProbe') if isinstance(final,dict) else None
    v151=final.get('v151MeshTruth') if isinstance(final,dict) else None
    stages=(grip or {}).get('stages') or {} if isinstance(grip,dict) else {}
    side_gate=stages.get('v301SideSurfaceContactGate') or {}
    checks=side_gate.get('checks') or {}
    pts=(final.get('footPoints') or {}).get('rawAuthored') or {}
    F=pts.get(f'{side}_Foot')
    T=pts.get(f'{side}_ToeBase')
    ft=vec(F,T); ftLen=vlen(ft); ftDir=norm(ft)
    return {
      'hasDebug': bool(final.get('hasDebug')),
      'hasModel': bool(final.get('hasModel')),
      'boneCount': final.get('boneCount'),
      'skinnedMeshCount': final.get('skinnedMeshCount'),
      'hasGripSolverV1': bool(grip),
      'runtimeGripVersion': grip.get('version') if isinstance(grip,dict) else None,
      'gripSide': grip.get('side') if isinstance(grip,dict) else None,
      'gripPass': grip.get('pass') if isinstance(grip,dict) else None,
      'gripStatus': grip.get('status') if isinstance(grip,dict) else None,
      'sideSurfaceGatePass': side_gate.get('pass'),
      'outsideContact': checks.get('outsideContact'),
      'noGrossPenetration': checks.get('noGrossPenetration'),
      'handPalmTowardObject': checks.get('handPalmTowardObject'),
      'fingerBlocked': finger.get('blocked') if isinstance(finger,dict) else None,
      'fingerReady': finger.get('ready') if isinstance(finger,dict) else None,
      'fingerReleaseEnabled': finger.get('releaseEnabled') if isinstance(finger,dict) else None,
      'probeSide': probe.get('side') if isinstance(probe,dict) else None,
      'meshTruthSide': v151.get('side') if isinstance(v151,dict) else None,
      'rawFoot': F,
      'rawToeBase': T,
      'rawFootToToeVector': r(ft),
      'rawFootToToeLengthM': r(ftLen),
      'rawFootToToeDir': r(ftDir),
    }

cases=[
  {'name':'left','side':'L','caseId':'v94_left_full','exercise':'hurdle-stretch','phase':0.25},
  {'name':'right','side':'R','caseId':'v94_right_full','exercise':'hurdle-stretch','phase':0.75},
]

srv, port = start_server()
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=*'])
    context=browser.new_context(viewport={'width':1280,'height':900}, device_scale_factor=1)
    for case in cases:
      page=context.new_page()
      params={'exercise':case['exercise'],'caseId':case['caseId'],'phase':str(case['phase']),'play':'0','debug':'0','visualMode':'nex','nativeRuntimeParity':'1','nativeSkin':'1','skinPreset':'freeridehuman','readyTimeoutMs':'12000','hostLifecycle':'v302-left-foot-ik-source-fix-browser-audit','build':'NEXPROD01_RUNTIME_V302_LEFT_FOOT_IK_SOURCE_FIX_2026_08_19','safeBoot':'0','lowPower':'1','pixelRatioCap':'1.15','antialias':'1','maxFPS':'24','staticAfterReady':'0','idleSleepAfterReady':'0','v':'V302_'+case['name']}
      url=f'http://127.0.0.1:{port}/assets/nexprod/nexprod01-player.html?'+urllib.parse.urlencode(params)
      nav_error=None
      try: page.goto(url, wait_until='domcontentloaded', timeout=15000)
      except Exception as e: nav_error=repr(e)
      polls=[]; final=None
      for i in range(10):
        page.wait_for_timeout(1000)
        try: val=page.evaluate(EVAL_JS)
        except Exception as e:
          polls.append({'t':i+1,'evalError':repr(e)}); continue
        final=val
        polls.append({'t':i+1,'hasDebug':val.get('hasDebug'),'hasModel':val.get('hasModel'),'boneCount':val.get('boneCount'),'markerCount':val.get('markerCount'),'gripSchema':(val.get('gripSolverV1') or {}).get('schema')})
        if val.get('hasModel') and val.get('gripSolverV1'):
          page.wait_for_timeout(1000)
          final=page.evaluate(EVAL_JS)
          break
      shot=str(SHOT_DIR / f"V302_{case['name']}_left_foot_source_fix_player.png")
      try: page.screenshot(path=shot, full_page=False, timeout=5000)
      except Exception: shot=None
      results.append({'case':case,'url':url,'navigationError':nav_error,'polls':polls,'final':final,'screenshot':shot,'summary':summarize(final,case['side'])})
      page.close()
    browser.close()
finally:
  srv.shutdown(); srv.server_close()

left=next((r for r in results if r['case']['name']=='left'),None)
right=next((r for r in results if r['case']['name']=='right'),None)
audit={'schema':'NEXT11_V302_BROWSER_RUNTIME_LEFT_FOOT_IK_SOURCE_FIX_AUDIT','generatedAt':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'root':str(ROOT),'cases':results}
audit['passBrowserRuntime']=all(r['summary']['hasModel'] and r['summary']['hasGripSolverV1'] for r in results)
audit['passSideSurfaceGate']=all(r['summary']['sideSurfaceGatePass'] is True and r['summary']['outsideContact'] is True and r['summary']['noGrossPenetration'] is True and r['summary']['handPalmTowardObject'] is True for r in results)
audit['passNoFingerReleaseByDefault']=all(r['summary']['fingerBlocked'] is True and r['summary']['fingerReady'] is True and r['summary']['fingerReleaseEnabled'] is False for r in results)
audit['passLeftFootSourceFix']=bool(left and left['summary']['rawToeBase']=={'x':0.048,'y':0.834,'z':-0.405} and left['summary']['rawFootToToeDir'] and left['summary']['rawFootToToeDir']['z'] < -0.7 and left['summary']['rawFootToToeDir']['y'] > 0.18)
audit['passRightFootStillMirrored']=bool(right and right['summary']['rawFootToToeDir'] and right['summary']['rawFootToToeDir']['z'] < -0.7 and right['summary']['rawFootToToeDir']['y'] > 0.18)
audit['pass']=audit['passBrowserRuntime'] and audit['passSideSurfaceGate'] and audit['passNoFingerReleaseByDefault'] and audit['passLeftFootSourceFix']
OUT.write_text(json.dumps(audit,indent=2),encoding='utf-8')
print(json.dumps({'out':str(OUT),'pass':audit['pass'],'passBrowserRuntime':audit['passBrowserRuntime'],'passSideSurfaceGate':audit['passSideSurfaceGate'],'passNoFingerReleaseByDefault':audit['passNoFingerReleaseByDefault'],'passLeftFootSourceFix':audit['passLeftFootSourceFix'],'passRightFootStillMirrored':audit['passRightFootStillMirrored'],'summaries':[r['summary'] for r in results]},indent=2))
