from pathlib import Path
import subprocess,csv,json,hashlib,xml.etree.ElementTree as E
p=Path(__file__).resolve().parent;b=p.parent;workspace=b.parents[1]
cli='/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli'
boards=[('drill','motor_A92',b/'drill_header_holes_A92/drill_motor_a85_cutoff.kicad_sch'),('drill','controller_A32',b/'sleep_gpio_A32/release/controller/drill_control_evt_a3_pf1_sleep.kicad_sch'),('vacuum','main_A111',b/'vacuum_display_pullups_A111/cad/vacuum_control_evt_a5_logic_fet.kicad_sch'),('vacuum','panel_A107',b/'vacuum_panel_board_A107/cad/vacuum_panel_a107.kicad_sch'),('vacuum','pressure_fixture_A114',b/'vacuum_pressure_adapter_A114/cad/vacuum_pressure_a114.kicad_sch'),('lamp','power_A70',b/'lamp_control_decoupling_A70/power_board/lamp_buck_lmr51450_a6_local_ldo.kicad_sch'),('lamp','safety_A39',b/'lamp_IR_PCB_A39/board/lamp_safety_evt_a3_IR_PCB.kicad_sch'),('lamp','driver_A59',b/'lamp_local_decoupling_A59/board/led_driver.kicad_sch'),('lamp','camera_EVB',workspace/'work/camera_evb_a0-metadata-sync-candidate.nosync/camera_evb_a0.kicad_sch')]
for q in ['netlists']:(p/q).mkdir(exist_ok=True)
rows=[];receipts=[]
for product,board,source in boards:
 xml=p/'netlists'/f'{board}.xml';result=subprocess.run([cli,'sch','export','netlist','--format','kicadxml','--output',str(xml),str(source)],capture_output=True,text=True);assert result.returncode==0,result.stderr
 r=E.parse(xml).getroot()
 for c in r.findall('./components/comp'):
  props={x.get('name') for x in c.findall('property')};fields={x.get('name'):x.text or '' for x in c.findall('./fields/field')}
  dnp='dnp' in props;excluded='exclude_from_bom' in props
  rows.append({'Product':product,'Board':board,'Ref':c.get('ref'),'Value':c.findtext('value') or '', 'Footprint':c.findtext('footprint') or '', 'DNP':dnp,'ExcludedFromBOM':excluded,'QtyPerEngineeringBoard':0 if dnp or excluded else 1,'QtyFor1000EngineeringBoards':0 if dnp or excluded else 1000,'CandidateMPN':fields.get('MPN',''),'MPNSource':'schematic MPN field' if fields.get('MPN') else '', 'UnitPriceUSD1000':'','QuoteStatus':'NO_QUOTE','Qualification':'NOT_PHYSICALLY_QUALIFIED','ProductionBoardArchitecture':'NOT_FROZEN'})
  if not rows[-1]['CandidateMPN']:rows[-1]['CandidateMPN']='NOT_LOCKED'
 receipts.append({'board':board,'source':str(source),'source_sha256':hashlib.sha256(source.read_bytes()).hexdigest(),'netlist_sha256':hashlib.sha256(xml.read_bytes()).hexdigest()})
# Import only exact ref/footprint-compatible candidate data. Do not guess from Value.
imports=[('motor_A92',b/'drill_header_holes_A92/motor-A92-candidate-BOM.csv','Ref','PCBFootprint','CandidateMPN'),('safety_A39',b/'lamp_IR_PCB_A39/safety_board_38refs_candidate_BOM.csv','ref','footprint','exact_MPN')]
for board,file,refkey,fpkey,mpnkey in imports:
 data={x[refkey]:x for x in csv.DictReader(file.open(encoding='utf-8-sig'))}
 for row in rows:
  if row['Board']!=board or row['Ref'] not in data:continue
  item=data[row['Ref']];assert item[fpkey]==row['Footprint'],(board,row['Ref'],'footprint mismatch')
  if item[mpnkey]:row['CandidateMPN']=item[mpnkey];row['MPNSource']=str(file)
 receipts.append({'candidate_bom':str(file),'sha256':hashlib.sha256(file.read_bytes()).hexdigest()})
with (p/'electrical-board-register.csv').open('w',encoding='utf-8-sig',newline='') as f:
 w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
counts={}
for product,board,_ in boards:
 group=[r for r in rows if r['Board']==board];counts[board]={'product':product,'references':len(group),'fitted_references':sum(r['QtyPerEngineeringBoard'] for r in group),'fitted_with_explicit_candidate_MPN':sum(r['QtyPerEngineeringBoard'] for r in group if r['CandidateMPN'] not in ['','NOT_LOCKED'])}
(p/'source-receipts.json').write_text(json.dumps(receipts,ensure_ascii=False,indent=2)+'\n')
(p/'coverage.json').write_text(json.dumps({'status':'ENGINEERING_ELECTRICAL_REGISTER_ONLY','boards':counts,'whole_unit_cost_complete':False,'manufacturing_release':False},indent=2)+'\n');print(json.dumps(counts))
