#!/usr/bin/env python3
"""Concept 28 fresh nominal audit, adapted from reviewed final 27 trace methods. Read-only after build completion.
No source PASS label or envelope-only check is used as a Boolean fit result.
"""
from pathlib import Path
import argparse,hashlib,json,itertools,time
import build123d as b
import numpy as np
ROOT=Path(__file__).resolve().parents[1]

def norm(s):
 if s is None:return None
 return b.Compound(children=list(s)) if isinstance(s,(list,tuple,b.ShapeList)) else s
def sol(s):return [] if s is None else list(norm(s).solids())
def volume(s):return sum(q.volume for q in sol(s))
def bbox(s):
 q=norm(s).bounding_box();return tuple(float(v) for v in (q.min.X,q.max.X,q.min.Y,q.max.Y,q.min.Z,q.max.Z))
def candidate(A,B,eps=1e-7):return all(min(A[k+1],B[k+1])-max(A[k],B[k])>eps for k in (0,2,4))
def boxdistance(A,B):return sum(max(0,A[k]-B[k+1],B[k]-A[k+1])**2 for k in (0,2,4))**.5
class Item:
 def __init__(self,name,s,kind):
  self.name=name;self.s=norm(s);self.kind=kind;self.solids=sol(s);self.boxes=[bbox(q) for q in self.solids];self.bb=bbox(s)
 def moved(self,t):return Item(self.name,self.s.moved(b.Location(t)),self.kind)
def overlap(a,c,details=False):
 if not candidate(a.bb,c.bb):return [] if details else 0.
 vals=[]
 for i,s in enumerate(a.solids):
  for j,t in enumerate(c.solids):
   if candidate(a.boxes[i],c.boxes[j]):
    v=volume(s.intersect(t))
    if v>1e-7:vals.append({'a_solid':i+1,'b_solid':j+1,'volume_mm3':float(v)})
 return vals if details else sum(x['volume_mm3'] for x in vals)
def distance(a,c):
 best=float('inf')
 order=sorted((boxdistance(A,B),i,j) for i,A in enumerate(a.boxes) for j,B in enumerate(c.boxes))
 for low,i,j in order:
  if low>best+1e-6:break
  best=min(best,float(a.solids[i].distance_to(c.solids[j])))
  if best<1e-7:return 0.
 return best

def load():
 baseline=ROOT/'inputs/final27'
 M=json.loads((baseline/'audit/Concept_27_R2_build_manifest.json').read_text());items={};actual={}
 for row in M['part_manifest']:
  n=row['name'];p=ROOT/'cad/parts'/f'{n}.step'
  num=n.split('_')[2]
  if num.isdigit():
   revised=list((ROOT/'cad/parts').glob(f'C28_R1_{num}_*.step'))
   if revised:p=revised[0]
  items[n]=Item(n,b.import_step(p),row['material']);actual[n]=p.name
 for p in sorted((ROOT/'cad/parts').glob('C28_R1_*.step')):
  if p.name in actual.values():continue
  mat='bond' if 'Bond' in p.name else 'sheet' if 'Scrim' in p.name else 'steel'
  items[p.stem]=Item(p.stem,b.import_step(p),mat);actual[p.stem]=p.name
 for row in M['fasteners']:
  n=row['name'];p=ROOT/'cad/parts'/f'{n}.step';items[n]=Item(n,b.import_step(p),'fastener');actual[n]=p.name
 for n in M['hardware_transforms']:
  key='C27_R2_REF_'+n;p=ROOT/'cad/hardware'/f'{key}.step';items[key]=Item(key,b.import_step(p),'electronics');actual[key]=p.name
 for row in M.get('routed_lead_references',[]):
  n=row['name'];p=ROOT/'cad/hardware'/f'{n}.step';items[n]=Item(n,b.import_step(p),'flexible_route');actual[n]=p.name
 (ROOT/'audit/Concept_28_R1_functional_inputs.json').write_text(json.dumps({'actual_export_files_by_inherited_function':actual,'note':'C27_R2 function labels map to actual 28 exported geometry, not an inherited PASS.'},indent=2))
 return M,items

def intended(a,c):
 # Intent is accepted only inside the specific matching pilot's thread region.
 if a.kind=='fastener' or c.kind=='fastener':
  bolt=a if a.kind=='fastener' else c;part=c if a.kind=='fastener' else a
  n=bolt.name
  if '_HW_Closure_' in n: start,end,r,partnum=-12.,bolt.bb[5],1.25,1
  elif '_HW_Front_Clamp_' in n: start,end,r,partnum=-10.85,-2.6,1.,1
  elif '_HW_Speaker_' in n: start,end,r,partnum=-4.95,-1.1,1.,1
  elif '_HW_Mic_' in n: start,end,r,partnum=-5.1,-2.,1.,1
  elif '_HW_LED_' in n: start,end,r,partnum=-8.3,-5.2,1.,1
  elif '_HW_LTE_' in n: start,end,r,partnum=-22.35,-20.33,1.,2
  else:return None
  if not part.name.startswith(f'C27_R2_{partnum:02d}_'):return None
  x=(bolt.bb[0]+bolt.bb[1])/2;y=(bolt.bb[2]+bolt.bb[3])/2
  allowed=b.Solid.make_cylinder(r+1e-5,end-start+2e-5,b.Plane(origin=(x,y,start-1e-5)))
  hit=norm(bolt.s.intersect(part.s));outside=volume(norm(hit.cut(allowed))) if hit is not None else 0
  if outside<1e-5:return 'matching nominal thread envelope only; head and off-axis clashes are not masked'
 if ((a.name.startswith('C27_R2_01_') and c.name.startswith('C27_R2_04_')) or
     (c.name.startswith('C27_R2_01_') and a.name.startswith('C27_R2_04_'))):
  v=overlap(a,c)
  if abs(v-.5920181273019306)<1e-5:return 'R1 lens press-fit preserved at0.5920181273 mm3; physical force/tolerance unqualified'
 return None

def static(M,I):
 result={'method':'exact OCC solid-by-solid intersections after bounding-box broad phase','threshold_mm3':1e-5,'pairs_checked':0,'broad_phase_candidates':0,'unintended':[],'intentional':[],'electronic_internal_overlap_policy':'Internal intersections within each immutable supplied multi-solid OEM reference are not assembly interferences; electronics groups are never fused or trimmed.'}
 for a,c in itertools.combinations(I.values(),2):
  result['pairs_checked']+=1
  if not candidate(a.bb,c.bb):continue
  result['broad_phase_candidates']+=1
  v=overlap(a,c)
  if v>1e-5:
   reason=intended(a,c);r={'a':a.name,'b':c.name,'overlap_mm3':v}
   if reason:r['reason']=reason
   result['intentional' if reason else 'unintended'].append(r)
   print('OVERLAP',r,flush=True)
 (ROOT/'audit/Concept_28_R1_static_audit.json').write_text(json.dumps(result,indent=2))
 return result

def retention(M,I):
 R=[];fixture=[q for q in I.values() if q.kind not in ('electronics','fastener','flexible_route')]
 for name in M['hardware_transforms']:
  original=I['C27_R2_REF_'+name]
  if name=='PCB':original=Item(original.name,max(original.solids,key=lambda q:q.volume),'electronics')
  B=original.bb;extent=[B[0]-1.21,B[1]+1.21,B[2]-1.21,B[3]+1.21,B[4]-1.21,B[5]+1.21]
  clip=b.Solid.make_box(extent[1]-extent[0],extent[3]-extent[2],extent[5]-extent[4],b.Plane(origin=(extent[0],extent[2],extent[4])))
  local=[]
  for q in fixture:
   if candidate(q.bb,tuple(extent)):
    sub=norm(q.s.intersect(clip))
    if volume(sub)>1e-7:local.append(Item(q.name,sub,q.kind))
  local.sort(key=lambda q:(q.kind not in ('foam','silicone','bond'),len(q.s.faces())))
  for di,(axis,sign) in enumerate([(0,-1),(0,1),(1,-1),(1,1),(2,-1),(2,1)]):
   row={'component':name,'direction':('+' if sign>0 else '-')+'XYZ'[axis],'method':'rigid translation of exact supplied component against installed rigid and compliant fixture geometry','force_validation':False,'blocks':[],'exact_fixture_clip_bounds_mm':extent,'body_checked':'actual imported FR4 substrate (primary rigid PCB location body)' if name=='PCB' else 'complete supplied component body'}
   for shift in (.1,.25,.35,.5,.8,1.2):
    t=[0.,0.,0.];t[axis]=sign*shift;m=original.moved(t)
    hits=[]
    for q in local:
     v=overlap(m,q)
     if v>1e-4:hits.append({'part':q.name,'material':q.kind,'overlap_mm3':v,'interpretation':'geometric compression only, not a force result' if q.kind in ('foam','silicone','bond') else 'rigid geometric stop'});break
    if hits:row['first_tested_block_mm']=shift;row['blocks']=hits;break
   row['blocked_within_1_2mm']=bool(row['blocks']);R.append(row);print('RETENTION',name,row['direction'],row.get('first_tested_block_mm'),flush=True)
 (ROOT/'audit/Concept_28_R1_retention_audit.json').write_text(json.dumps(R,indent=2));return R

def motion(M,I):
 controls=[('Front',3,[0,0,-1],.7,'Front_Switch',2),('PTT',6,[1,0,0],.65,'PTT_Switch',3),('Volume_Up',7,[-1,0,0],.65,'Volume_Up_Switch',3),('Volume_Down',8,[-1,0,0],.65,'Volume_Down_Switch',3)]
 sets={3:[3,10,11],6:[6,16,17],7:[7,12,13],8:[8,14,15]};results=[]
 for name,idx,axis,travel,hw,plungeridx in controls:
  keys=[q for q in I.values() if any(q.name.startswith(f'C27_R2_{j:02d}_') for j in sets[idx])]
  staticparts=[q for q in I.values() if q not in keys and q.name!='C27_R2_REF_'+hw]
  switch=I['C27_R2_REF_'+hw];plunger=Item('plunger',switch.solids[plungeridx-1],'actuator')
  housing=Item('fixed_switch_body',b.Compound(children=[q for i,q in enumerate(switch.solids) if i!=plungeridx-1]),'electronics')
  assert abs(plunger.s.volume-(9.5040008 if plungeridx==3 else plunger.s.volume))<1e-4
  rest=distance(keys[0],plunger);row={'control':name,'axis':axis,'rest_gap_mm':rest,'max_key_travel_mm':travel,'method':'0.05mm exact-BRep sampled key sweep with independently translated actual actuator solid; reverse path is the same nominal geometry','physical_return_force_qualified':False,'samples':[]}
  for mm in np.linspace(0,travel,int(round(travel/.05))+1):
   t=[float(x*mm) for x in axis];mkeys=[q.moved(t) for q in keys]
   ap=max(float(mm)-rest,0);p=plunger.moved([x*ap for x in axis]);hits=[]
   for q in mkeys:
    for obstacle in staticparts+[housing]:
     v=overlap(q,obstacle)
     if v>1e-4:hits.append({'moving':q.name,'obstacle':obstacle.name,'overlap_mm3':v})
   row['samples'].append({'key_displacement_mm':float(mm),'actuator_displacement_mm':ap,'key_actuator_gap_mm':min(distance(q,p) for q in mkeys),'unintended_hits':hits})
  row['actual_plunger_one_based_index']=plungeridx
  row['actual_plunger_volume_mm3']=plunger.s.volume
  row['actuator_contact_reached']=any(x['actuator_displacement_mm']>0 and x['key_actuator_gap_mm']<1e-4 for x in row['samples'])
  row['cad_sweep_clear']=not any(x['unintended_hits'] for x in row['samples'])
  row['allowed_switch_overtravel_qualified']=False
  results.append(row);print('MOTION',name,rest,sum(bool(s['unintended_hits']) for s in row['samples']),flush=True)
 (ROOT/'audit/Concept_28_R1_switch_motion.json').write_text(json.dumps(results,indent=2));return results

def closure(M,I):
 moving=[];stationary=[]
 for q in I.values():
  n=q.name
  rear=(n.startswith('C27_R2_02_') or n.startswith('C27_R2_LTE_') or n=='C27_R2_REF_LTE' or '_HW_LTE_' in n or 'PCB_Rear_Preload' in n or 'Battery_Rear_Pad' in n or 'Battery_Side_Liner' in n or 'Battery_Upper_Liner' in n or 'Battery_Lower_Liner' in n)
  if '_HW_Closure_' in n or n.startswith('C27_R2_09_') or 'Rim_Grip_Pad' in n:continue
  (moving if rear else stationary).append(q)
 result={'method':'rear-cover subassembly straight +Z insertion, 0.5mm sampled intervals over final 12mm','moving_parts':[q.name for q in moving],'samples':[],'physical_pad_compression_not_simulated':True,'assembly_condition':'soft rim not installed; rear closure screws installed only after seating'}
 for gap in np.arange(12,-.01,-.5):
  hits=[]
  for q in moving:
   m=q.moved([0,0,-float(gap)])
   for f in stationary:
    v=overlap(m,f)
    if v>1e-4:
     # Only the explicitly named free-state pad overlaps may be compressive.
     hits.append({'moving':q.name,'fixed':f.name,'overlap_mm3':v})
  result['samples'].append({'remaining_travel_mm':float(gap),'hits':hits});print('COVER',gap,len(hits),flush=True)
 (ROOT/'audit/Concept_28_R1_cover_insertion.json').write_text(json.dumps(result,indent=2));return result

def main():
 ap=argparse.ArgumentParser();ap.add_argument('--stage',choices=['static','retention','motion','closure','all'],default='static');args=ap.parse_args();M,I=load();t=time.time()
 for k,fn in [('static',static),('retention',retention),('motion',motion),('closure',closure)]:
  if args.stage in (k,'all'):fn(M,I)
 print('AUDIT SECONDS',time.time()-t,flush=True)
if __name__=='__main__':main()
