#!/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/v318work/NEXT11_V317_FINAL_ACTUAL_MCP_AUTHORITY_GATE_ROOT')
OUT=ROOT/'diagnostics/v318'; OUT.mkdir(parents=True, exist_ok=True)
class Quiet(http.server.SimpleHTTPRequestHandler):
    def log_message(self,*args): pass
PROBE_JS=r'''() => {
 const debug=window.__NEXPROD01_DEBUG_SCENE||null;
 const model=debug?.model||null;
 const scene=debug?.scene||null;
 const gate=window.__NEXPROD01_V317_ACTUAL_MCP_PALM_PREFINGER_GATE||window.__NEXPROD01_V311_ACTUAL_MCP_PALM_PREFINGER_GATE||window.__NEXPROD01_V308_ACTUAL_MCP_PALM_PREFINGER_GATE||null;
 const grip=window.__NEXPROD01_GRIP_SOLVER_V1||null;
 if(!model||!gate||!grip||!window.THREE) return {ok:false, reason:'missing_model_gate_grip_or_THREE',hasModel:!!model,hasGate:!!gate,hasGrip:!!grip};
 const side=String(grip.side||'').toUpperCase()==='L'?'L':'R';
 const mcp=gate.v307ActualMcpPalmGate||gate.actualGate?.v307ActualMcpPalmGate||gate.actualGate||null;
 const pts=mcp?.points||{};
 const palmPts={index:pts.index||null,pinky:pts.pinky||null};
 if(!palmPts.index||!palmPts.pinky) return {ok:false,reason:'missing_actual_mcp_offset_points',side, gateKeys:Object.keys(gate||{}),mcpKeys:Object.keys(mcp||{})};
 model.updateMatrixWorld(true);
 let body=null; model.traverse(o=>{if(o?.isSkinnedMesh && (o.name==='Body'||!body)) body=o;});
 if(!body||!body.geometry?.attributes?.position||!body.geometry?.attributes?.skinIndex||!body.geometry?.attributes?.skinWeight) return {ok:false,reason:'no_skinned_body',side};
 body.skeleton.update();
 const pos=body.geometry.attributes.position, si=body.geometry.attributes.skinIndex, sw=body.geometry.attributes.skinWeight;
 const names=body.skeleton.bones.map(b=>String(b.name||''));
 const ids={calf:new Set(),foot:new Set(),toe:new Set()};
 names.forEach((n,i)=>{if(n===`${side}_Calf`)ids.calf.add(i); if(n===`${side}_Foot`)ids.foot.add(i); if(n===`${side}_ToeBase`)ids.toe.add(i);});
 const categories={calf:[],foot:[],toe:[],footToe:[],legAll:[]};
 const v=new THREE.Vector3();
 const count=pos.count; const stride=Math.max(1,Math.floor(count/5000));
 for(let i=0;i<count;i+=stride){
   let cw=0,fw=0,tw=0;
   for(let j=0;j<4;j++){const id=si.getComponent(i,j), w=sw.getComponent(i,j)||0; if(ids.calf.has(id))cw+=w; if(ids.foot.has(id))fw+=w; if(ids.toe.has(id))tw+=w;}
   if(Math.max(cw,fw,tw)<0.05) continue;
   v.fromBufferAttribute(pos,i); body.applyBoneTransform(i,v); v.applyMatrix4(body.matrixWorld);
   const q={x:v.x,y:v.y,z:v.z,i,cw,fw,tw};
   if(cw>=0.12)categories.calf.push(q);
   if(fw>=0.12)categories.foot.push(q);
   if(tw>=0.12)categories.toe.push(q);
   if(fw+tw>=0.12)categories.footToe.push(q);
   if(cw+fw+tw>=0.12)categories.legAll.push(q);
 }
 const nearest=(p,arr)=>{let best=null,bd=Infinity; for(const q of arr){const d=Math.hypot(p.x-q.x,p.y-q.y,p.z-q.z); if(d<bd){bd=d;best=q;}} return best?{distanceM:bd,point:{x:best.x,y:best.y,z:best.z},vertex:best.i,weights:{calf:best.cw,foot:best.fw,toe:best.tw}}:null;};
 const result={ok:true,side,bodyName:body.name,vertexCount:count,categoryCounts:Object.fromEntries(Object.entries(categories).map(([k,a])=>[k,a.length])),palmPoints:palmPts,nearest:{}};
 for(const [pn,p] of Object.entries(palmPts)){
   result.nearest[pn]={}; for(const [cat,arr] of Object.entries(categories)) result.nearest[pn][cat]=nearest(p,arr);
 }
 // actual MCP roots too, for sanity
 const roots={}; for(const n of [`${side}_Index1`,`${side}_Pinky1`,`${side}_Mid1`,`${side}_Hand`,`${side}_Calf`,`${side}_Foot`,`${side}_ToeBase`]){const b=model.getObjectByName(n); if(b){const w=new THREE.Vector3();b.getWorldPosition(w);roots[n]={x:w.x,y:w.y,z:w.z};}}
 result.boneRoots=roots;
 // markers: actual palm point, nearest CALF, nearest FOOT/TOE. Remove previous probe group.
 try{
   const old=scene?.getObjectByName?.('V318_ACTUAL_SKIN_PROBE'); if(old) scene.remove(old);
   if(scene){
     const g=new THREE.Group(); g.name='V318_ACTUAL_SKIN_PROBE';
     const sphere=(p,c,r=0.012)=>{const m=new THREE.Mesh(new THREE.SphereGeometry(r,16,12),new THREE.MeshBasicMaterial({color:c,depthTest:false}));m.position.set(p.x,p.y,p.z);m.renderOrder=999;g.add(m);return m;};
     const line=(a,b,c)=>{const geo=new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(a.x,a.y,a.z),new THREE.Vector3(b.x,b.y,b.z)]);const l=new THREE.Line(geo,new THREE.LineBasicMaterial({color:c,depthTest:false}));l.renderOrder=998;g.add(l);};
     for(const [pn,p] of Object.entries(palmPts)){
       sphere(p,pn==='index'?0xff2b2b:0xff00ff,0.013);
       const nc=result.nearest[pn].calf?.point, nf=result.nearest[pn].footToe?.point;
       if(nc){sphere(nc,0x00ff66,0.010);line(p,nc,0x00ff66);} // green = actual calf surface
       if(nf){sphere(nf,0xffcc00,0.010);line(p,nf,0xffcc00);} // yellow = foot/toe surface
     }
     scene.add(g);
   }
 }catch(e){result.markerError=String(e)}
 return result;
}'''
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]
 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':1400,'height':1100},device_scale_factor=1)
   for name,caseId,phase in [('right','v94_right_full','0.75'),('left','v94_left_full','0.25')]:
    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':'v318-actual-skin-probe','build':'NEXPROD01_RUNTIME_V317_WRIST_NODE_180_RESTART_2026_08_19','safeBoot':'0','lowPower':'1','pixelRatioCap':'1.15','antialias':'1','maxFPS':'24','staticAfterReady':'0','idleSleepAfterReady':'0','v':'V318probe_'+name}
    url=f'http://127.0.0.1:{port}/assets/nexprod/nexprod01-player.html?'+urllib.parse.urlencode(params)
    err=None
    try: page.goto(url,wait_until='domcontentloaded',timeout=15000)
    except Exception as e: err=repr(e)
    ready=False
    for _ in range(30):
     page.wait_for_timeout(500)
     try:
      ready=page.evaluate("() => !!(window.__NEXPROD01_DEBUG_SCENE?.model && window.__NEXPROD01_GRIP_SOLVER_V1)")
     except: ready=False
     if ready: break
    page.wait_for_timeout(500)
    try: probe=page.evaluate(PROBE_JS)
    except Exception as e: probe={'ok':False,'reason':'probe_eval_error','error':repr(e)}
    page.wait_for_timeout(300)
    canvas=OUT/f'V318_{name}_actual_skin_probe_canvas.png'; full=OUT/f'V318_{name}_actual_skin_probe_page.png'
    try: page.locator('canvas').first.screenshot(path=str(canvas),timeout=8000)
    except Exception: page.screenshot(path=str(canvas),full_page=False)
    try: page.screenshot(path=str(full),full_page=False)
    except: pass
    results.append({'name':name,'caseId':caseId,'phase':phase,'url':url,'navigationError':err,'ready':ready,'probe':probe,'canvas':str(canvas),'page':str(full)})
    page.close()
   browser.close()
 finally:
  srv.shutdown();srv.server_close()
 audit={'schema':'NEXT11_V318_ACTUAL_SKIN_SURFACE_ROOTCAUSE_PROBE','generatedAt':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'root':str(ROOT),'results':results}
 # conclusions machine-readable
 for r in results:
  pr=r.get('probe') or {}; n=pr.get('nearest') or {}; summary={}
  for pn in ('index','pinky'):
   x=n.get(pn) or {}; summary[pn]={k:((x.get(k) or {}).get('distanceM')) for k in ('calf','footToe','legAll')}
  r['distanceSummary']=summary
 (OUT/'V318_ACTUAL_SKIN_SURFACE_PROBE.json').write_text(json.dumps(audit,indent=2),encoding='utf-8')
 print(json.dumps({'audit':str(OUT/'V318_ACTUAL_SKIN_SURFACE_PROBE.json'),'results':[{'name':r['name'],'ready':r['ready'],'ok':(r.get('probe') or {}).get('ok'),'summary':r.get('distanceSummary'),'counts':(r.get('probe') or {}).get('categoryCounts')} for r in results]},indent=2))
if __name__=='__main__': main()
