from pathlib import Path
import sys,json,re,hashlib,csv,xml.etree.ElementTree as ET
R=Path(__file__).resolve().parent;B=R.parent;sys.path.insert(0,str(B/'parity_metadata_closure_A22'));from prepare_candidates import forms,run
from align_and_verify import footprint_rows,netlist
D=R/'board';N='lamp_safety_evt_a3_IR_PCB';P=D/(N+'.kicad_pcb');S=D/(N+'.kicad_sch')
j=json.loads((D/'drc.json').read_text());assert not j['violations'] and not j['unconnected_items'] and not j['schematic_parity'],j
run(['sch','erc','--severity-all','--format','json','--output',str(D/'erc.json'),str(S)],D/'erc.log');e=json.loads((D/'erc.json').read_text());assert not [v for sh in e['sheets'] for v in sh.get('violations',[])]
pins=netlist(D/'netlist.xml')[1];seen={};text=P.read_text()
for _,_,f,ref,_ in footprint_rows(text):
 for _,_,pad in forms(f,'pad'):
  pin=re.match(r'\(pad "([^"]+)"',pad)[1];name=re.search(r'\(net "([^"]+)"',pad)[1].replace('{slash}','/');seen[(ref,pin)]=name
assert seen==pins and len(seen)==129
old=(R/'original_board.txt').read_text()
def records(text,tag):return {re.search(r'\(uuid "([^"]+)"',f)[1]:re.sub(r'\s+',' ',f) for _,_,f in forms(text,tag)}
changed={};removed={};added={}
for tag in ['segment','via']:
 a=records(old,tag);b=records(text,tag);changed[tag]=[k for k in a.keys()&b.keys() if a[k]!=b[k]];removed[tag]=sorted(a.keys()-b.keys());added[tag]=sorted(b.keys()-a.keys())
 assert not changed[tag]
assert removed['segment']==sorted(['50159e8b-9ddc-4543-b545-b33e9034a073','245e0b27-dd95-4d2b-bcc3-57d7da35a737']) and not removed['via']
# All original footprint placement and all original pad parameters other than
# U1 pin3 NC/net metadata remain exactly the same semantic records.
fo={ref:f for _,_,f,ref,_ in footprint_rows(old)};fn={ref:f for _,_,f,ref,_ in footprint_rows(text)}
for ref,f in fo.items():
 a=re.sub(r'\s+',' ',f);b=re.sub(r'\s+',' ',fn[ref])
 if ref=='U1':a=a.replace('(net "unconnected-(U1-PC15-Pad3)")','(net "/IR_HAND")')
 assert a==b,ref
for folder in ['gerbers','drill']:(D/folder).mkdir(exist_ok=True)
lay='F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts'
for args,log in [(['pcb','export','gerbers','--layers',lay,'--output',str(D/'gerbers')+'/',str(P)],'gerber-export.log'),(['pcb','export','drill','--format','excellon','--excellon-separate-th','--output',str(D/'drill')+'/',str(P)],'drill-export.log'),(['pcb','export','pos','--format','csv','--units','mm','--side','front','--smd-only','--exclude-dnp','--output',str(D/'centroid-front.csv'),str(P)],'centroid-export.log')]:assert run(args,D/log)==0
assert len(list((D/'gerbers').glob('*.g*')))==10 # nine layer plots and job manifest
with (D/'centroid-front.csv').open() as f:rows=list(csv.DictReader(f));assert len(rows)==32,len(rows)
root=ET.parse(D/'netlist.xml').getroot()
with (R/'safety_board_38refs_candidate_BOM.csv').open('w',newline='') as f:
 w=csv.writer(f);w.writerow(['ref','candidate_value','footprint','quantity_per_board','exact_MPN','unit_price','qualification'])
 for c in root.findall('./components/comp'):
  if c.get('ref').startswith('#'):continue
  w.writerow([c.get('ref'),c.findtext('value'),c.findtext('footprint'),1,'','','CANDIDATE_PENDING_EXACT_MPN_AND_PHYSICAL_VALIDATION'])
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
checks={'status':'A39_IR_PCB_LOCAL_NATIVE_PASS_FABRICATION_HOLD','ERC':0,'DRC':0,'unconnected_items':0,'schematic_parity':0,'matched_pin_net_pairs':129,'old_footprints_unchanged':34,'added_footprints':['J6','R20','R21','C8'],'old_copper_changed':changed,'old_copper_removed':removed,'added_copper_counts':{t:len(v) for t,v in added.items()},'old_other_copper_records_unchanged':True,'CAM_layers':9,'front_SMD_placements':32,'PCB_sha256':sha(P),'IR_firmware_bound':False,'physical_validation':'NOT_RUN_NO_SAMPLE','sensor_exact_MPN':'UNSELECTED','physical_verified_requirements':0,'reference_requirements':20}
(R/'checks.json').write_text(json.dumps(checks,indent=2)+'\n');print(json.dumps(checks,indent=2))
