#!/usr/bin/env python3
"""Build explicit sourcing candidates, with nominal checks and unclosed gates."""
import csv
import hashlib
import json
import math
import re
from pathlib import Path

ROOT=Path(__file__).resolve().parent
BASE=ROOT.parent
SOURCES=json.loads((ROOT/'manufacturer_source_fetch.json').read_text())

def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def norm(s):return re.sub(r'[^a-z0-9]','',s.lower())
def end_form(t,start):
    depth=0;quoted=escaped=False
    for i in range(start,len(t)):
        c=t[i]
        if quoted:
            if escaped:escaped=False
            elif c=='\\':escaped=True
            elif c=='"':quoted=False
        elif c=='"':quoted=True
        elif c=='(':depth+=1
        elif c==')':
            depth-=1
            if depth==0:return i+1
    raise ValueError('Unbalanced form')
def forms(t,tag):
    pos=0
    pattern=re.compile(r'\(\s*'+re.escape(tag)+r'\s+')
    while (m:=pattern.search(t,pos)):
        en=end_form(t,m.start());yield t[m.start():en];pos=en
def pcb_parts(p):
    parts={}
    for b in forms(p.read_text(),'footprint'):
        ref=re.search(r'\(property\s+"Reference"\s+"([^"]+)"',b).group(1)
        parts[ref]=re.match(r'\(footprint\s+"([^"]+)"',b).group(1)
    return parts
def read_csv(p):
    with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def write_csv(p,rows):
    with p.open('w',encoding='utf-8-sig',newline='') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
def capacitance(value):
    m=re.search(r'(\d+(?:\.\d+)?)\s*(uF|nF|pF|u|n)(?![a-z])',value,re.I)
    assert m,value
    scale={'uf':1e-6,'nf':1e-9,'pf':1e-12,'u':1e-6,'n':1e-9}[m.group(2).lower()]
    return float(m.group(1))*scale

RESISTORS={'10k':'10K','100k':'100K','1k':'1K','56k':'56K','330k':'330K','100R':'100R','49.9k':'49K9','16.5k':'16K5','19.1k':'19K1','68.1k':'68K1'}
catalog={}
for value,code in RESISTORS.items():
    mpn='RC0603FR-07'+code+'L'
    amount=value[:-1] if value.endswith('R') else value[:-1]
    ohmunit='Ohms' if value.endswith('R') else 'kOhms'
    catalog[mpn]={'manufacturer':'YAGEO','source_key':mpn,'case':'0603','nominal':value,'required_text':[amount+' '+ohmunit,'1%','0603','0.1 W'],'qualification':'Confirm actual voltage/power derating and application tolerance; factory price/stock not verified; not frozen AVL.'}
caps={
 'CC0603KRX7R9BB104':('0603','100nF 50V X7R 10%',['0.1 uF','50 VDC','X7R','0603']),
 'CC0603KRX7R9BB102':('0603','1nF 50V X7R 10%',['1,000 pF','50 VDC','X7R','0603']),
 'CC0603KRX7R8BB105':('0603','1uF 25V X7R 10%',['1 uF','25 VDC','X7R','0603']),
 'CC1206KKX7R8BB106':('1206','10uF 25V X7R 10%',['10 uF','25 VDC','X7R','1206']),
 'CC0805KKX5R6BB106':('0805','10uF 10V X5R 10%',['10 uF','10 VDC','X5R','0805']),
 'CC1206KKX7R9BB475':('1206','4.7uF 50V X7R 10%',['4.7 uF','50 VDC','X7R','1206']),
 'CC1206KRX7R9BB104':('1206','100nF 50V X7R 10%',['0.1 uF','50 VDC','X7R','1206']),
 'CC0603JRNPO9BN472':('0603','4.7nF 50V C0G 5%',['4,700 pF','50 VDC','C0G','0603']),
 'CC0805MKX5R6BB226':('0805','22uF 10V X5R 20%',['22 uF','10 VDC','X5R','0805'])}
for mpn,(case,nominal,tokens) in caps.items():
    catalog[mpn]={'manufacturer':'YAGEO','source_key':mpn,'case':case,'nominal':nominal,'required_text':tokens,'qualification':'Nominal capacitance/rating/case only verified. Effective capacitance under DC bias, aging, tolerance, ripple, thermal and controller stability remain unqualified; no factory quote.'}
for mpn,key in [('LMR51450SDRRR','lmr51450'),('TPS22992SRXNR','tps22992'),('INA180A1IDBVR','ina180')]:
    catalog[mpn]={'manufacturer':'Texas Instruments','source_key':key,'case':None,'nominal':'Explicit semiconductor MPN already present in schematic value','required_text':[mpn],'qualification':'Existing schematic identity only; not a new functional design approval. Package/pin/current/thermal and actual assembly qualification remain required; no factory quote.'}
source_checks={}
for mpn,spec in catalog.items():
    src=SOURCES[spec['source_key']]
    assert src['status']=='FETCHED_PDF'
    pdf=ROOT/'manufacturer_sources'/(spec['source_key']+'.pdf')
    txt=(ROOT/'manufacturer_sources'/(spec['source_key']+'.txt')).read_text()
    assert sha(pdf)==src['sha256']
    assert norm(mpn) in norm(txt),mpn
    for token in spec['required_text']:assert norm(token) in norm(txt),(mpn,token)
    source_checks[mpn]={'url':src['url'],'sha256':src['sha256'],'exact_mpn_and_declared_nominal_tokens_verified':True,'nominal':spec['nominal'],'manufacturer':spec['manufacturer']}

assignments={
 'drill':{'C13':'CC0805MKX5R6BB226'},
 'vacuum':{
  'R1':'RC0603FR-0710KL','R2':'RC0603FR-07100KL','R3':'RC0603FR-0710KL',
  'R4':'RC0603FR-071KL','R5':'RC0603FR-0756KL','R6':'RC0603FR-07330KL',
  'R7':'RC0603FR-07100RL','R8':'RC0603FR-07100KL','R9':'RC0603FR-0710KL',
  'R10':'RC0603FR-0710KL','R11':'RC0603FR-0716K5L','R12':'RC0603FR-07100RL',
  'R13':'RC0603FR-0749K9L','R14':'RC0603FR-07100RL','R15':'RC0603FR-07100KL','R16':'RC0603FR-0749K9L',
  'C2':'CC1206KKX7R8BB106','C4':'CC1206KKX7R8BB106','C5':'CC0805KKX5R6BB106',
  'C3':'CC0603KRX7R9BB104','C6':'CC0603KRX7R9BB104','C7':'CC0603KRX7R9BB104','C9':'CC0603KRX7R9BB104','C10':'CC0603KRX7R9BB104',
  'C8':'CC0603KRX7R9BB102','C11':'CC0603KRX7R8BB105'},
 'lamp':{
  'U1':'LMR51450SDRRR','U2':'INA180A1IDBVR','U3':'TPS22992SRXNR',
  'R2':'RC0603FR-07100KL','R3':'RC0603FR-07100KL','R4':'RC0603FR-0719K1L',
  'R7':'RC0603FR-07100KL','R8':'RC0603FR-07100KL','R9':'RC0603FR-071KL',
  'R10':'RC0603FR-071KL','R11':'RC0603FR-0768K1L','R12':'RC0603FR-0710KL',
  'C1':'CC1206KKX7R9BB475','C2':'CC1206KKX7R9BB475','C3':'CC1206KRX7R9BB104',
  'C4':'CC0603KRX7R9BB104','C11':'CC0603KRX7R9BB104','C12':'CC0603KRX7R9BB104','C13':'CC0603JRNPO9BN472'}}
input_specs={
 'drill':('电磨电机板 A2 metadata','drill_motor_run_gate_r1/BOM_motor_RUN_GATE_A1_candidate.csv','parity_metadata_closure_A22/drill/drill_motor_evt_a2_metadata.kicad_pcb','位号','原始BOM值','封装','候选MPN'),
 'vacuum':('吸尘器控制板 A1 metadata','engineering_handoff_local/vacuum_a0_bom_cost_screen.csv','parity_metadata_closure_A22/vacuum/vacuum_control_evt_a1_metadata.kicad_pcb','Ref','Schematic value','Footprint','MPN / price reference'),
 'lamp':('美甲灯电源板 A2','lamp_buck_lmr51450_a2_costdown/bom-active-only.csv','lamp_buck_lmr51450_a2_costdown/lamp_buck_lmr51450_a2_costdown.kicad_pcb','Refs','Value','Footprint','MPN')}
tables={};stats={}
for product,(board,bomrel,pcbrel,refcol,valcol,fpcol,mpncol) in input_specs.items():
    bp=BASE/bomrel;pcb=BASE/pcbrel; rows=read_csv(bp);parts=pcb_parts(pcb)
    out=[];new=[];openrefs=[];before=0
    for row in rows:
        ref=row[refcol];old=(row[mpncol] or '').strip()
        present=bool(old) and not old.startswith('NOT_LOCKED')
        before+=present
        selected=assignments[product].get(ref,old if present else '')
        if selected.startswith('TDK '):selected=selected[4:]
        assert ref in parts,(product,ref)
        assert selected==old or not present or old=='TDK '+selected,(product,ref,old,selected)
        manufacturer='';url='';sourcehash='';checks='';remaining='Actual load/interface/thermal/DFM/availability not qualified.'
        if ref in assignments[product]:
            spec=catalog[selected]
            if spec['case']:assert spec['case'] in parts[ref],(product,ref,parts[ref],spec['case'])
            # Fixed assignment must preserve the BOM's nominal numeric value.
            # Explicit diode/MOS substitutions are intentionally excluded.
            if ref.startswith('R'):
                val=norm(row[valcol].replace('10.0k','10k'))
                expected=norm(spec['nominal'])
                assert expected in val,(product,ref,row[valcol],spec['nominal'])
            elif ref.startswith('C'):
                assert math.isclose(capacitance(row[valcol]),capacitance(spec['nominal']),rel_tol=1e-8),(product,ref,row[valcol],spec['nominal'])
                required_voltage=re.search(r'(\d+(?:\.\d+)?)\s*V',row[valcol],re.I)
                chosen_voltage=re.search(r'(\d+(?:\.\d+)?)\s*V',spec['nominal'],re.I)
                if required_voltage:assert chosen_voltage and float(chosen_voltage.group(1))>=float(required_voltage.group(1))
                if 'X7R' in row[valcol]:assert 'X7R' in spec['nominal']
                if 'C0G' in row[valcol]:assert 'C0G' in spec['nominal']
            src=SOURCES[spec['source_key']]
            manufacturer=spec['manufacturer'];url=src['url'];sourcehash=src['sha256']
            checks=spec['nominal']+'; exact manufacturer specification identity checked; current PCB case checked for passives.'
            remaining=spec['qualification']
            status='EXPLICIT_SCHEMATIC_MPN_RECORDED' if ref.startswith('U') else 'NOMINAL_SPEC_RFQ_CANDIDATE'
            new.append(ref)
        elif present:
            status='EXISTING_CANDIDATE_NOT_REQUALIFIED_A23'
            manufacturer='YAGEO' if selected.startswith(('RC','CC')) else 'TDK' if selected.startswith('C4532') else 'Texas Instruments' if selected.startswith(('DRV','INA','TPS')) else 'Hynetek' if selected.startswith('HUSB238') else 'STMicroelectronics' if selected.startswith(('USBLC6','STM32')) else ''
            if selected in catalog:
                src=SOURCES[catalog[selected]['source_key']];url=src['url'];sourcehash=src['sha256']
                checks='Manufacturer nominal sheet checked in A23; original application qualification still open.'
                remaining=catalog[selected]['qualification']
        else:
            status='OPEN_LOAD_MECHANICAL_OR_COMPONENT_QUALIFICATION';openrefs.append(ref)
        if product=='lamp' and ref=='R13':
            status='OPEN_QOD_PULSE_POWER_QUALIFICATION'
            remaining='100 ohm discharge resistor: at 5 V, upper-bound initial power 0.25 W before internal QOD resistance. Ordinary RC0603 0.1 W cannot be frozen from continuous rating alone; downstream capacitance, repetition, pulse curve and temperatures required.'
            url=SOURCES['tps22992']['url'];sourcehash=SOURCES['tps22992']['sha256']
        out.append({'Product':product,'Board':board,'Ref':ref,'QtyPerBoard':'1','DesignValue':row[valcol],'CurrentPCBFootprint':parts[ref],'OriginalBOMFootprint':row[fpcol],'CandidateManufacturer':manufacturer,'CandidateMPN':selected,'SelectionStatus':status,'PrimarySourceURL':url,'PrimarySourceSHA256':sourcehash,'ChecksPerformed':checks,'RemainingQualification':remaining,'UnitPriceUSD1000':'','QuoteStatus':'NO_NEW_FACTORY_OR_1K_PRICE_VERIFIED_A23'})
    assert len({r['Ref'] for r in out})==len(out)
    assert set(assignments[product])<=set(r['Ref'] for r in out)
    write_csv(ROOT/(product+'_candidate_bom_A23.csv'),out)
    tables[product]=out
    stats[product]={'positions':len(out),'mpn_identified_before':before,'mpn_identified_after':sum(bool(r['CandidateMPN']) for r in out),'new_candidates':len(new),'new_refs':new,'open_refs':openrefs,'source_bom_sha256':sha(bp),'source_pcb_sha256':sha(pcb),'priced_rows_added':0,'manufacturing_status':'FABRICATION_HOLD'}
allrows=[r for rows in tables.values() for r in rows]
write_csv(ROOT/'three_board_candidate_bom_A23.csv',allrows)
summary={'release':'A23','scope':'Three named board candidates only; not full three-product BOM or qualified AVL. No hardware/CAM/firmware changes. Source PDFs remain local; package references manufacturer URLs and hashes.','boards':stats,'positions':len(allrows),'identified_mpn_positions':sum(bool(r['CandidateMPN']) for r in allrows),'new_candidates':sum(v['new_candidates'] for v in stats.values()),'manufacturer_nominal_checks':source_checks,'physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD','factory_quote':'NOT_RECEIVED'}
(ROOT/'bom_candidate_checks.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({k:v for k,v in summary.items() if k!='manufacturer_nominal_checks'},ensure_ascii=False,indent=2))
