"""Connected NAS intake architecture; dimensions and registration are estimates.

Preserve independently reconstructed component sizes while replacing the buried
manifold flange and overlapping assembly pose. This is a whole-car overlay;
the historical engine export and its interface audit are not rewritten.
"""
from pathlib import Path
import hashlib
import json
import math
import cadquery as cq
from OCP.BRepMesh import BRepMesh_IncrementalMesh
from OCP.BRepTools import BRepTools
import build_engine as E

OUT=Path(__file__).parent
X,Y,Z=cq.Vector(1,0,0),cq.Vector(0,1,0),cq.Vector(0,0,1)
CARB_SHIFT=-48.0
CLEANER_SHIFT=-29.0
CARB_INLET_X=-246.0
CARB_OUTLET_X=-162.0
MANIFOLD_FACE_X=-161.0
PORT_Y=55.0
BORE=E.P['carb_nominal_throat_radius']
# Diagonal map/clocking follows plate shape only; 64 mm pitch is unmeasured.
OFF=32/math.sqrt(2)
HOLES=[(PORT_Y-OFF,-OFF),(PORT_Y+OFF,OFF)]
HOLE_R=4.5
parts=[]
def cyl(r,l,p,a=X):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=X):return cyl(ro,l,p,a).cut(cyl(ri,l+2,p-a,a))
def box(w,h,d,p):return cq.Workplane('XY').box(w,h,d).translate(p.toTuple()).val()
def compound(shapes):return cq.Compound.makeCompound(shapes)
def add(n,s,c=(.43,.46,.43)):
    assert s.isValid() and s.Volume()>0,n
    parts.append((n,s,c));return s
def flange(x,t):
    # Outline and diagonal bores are construction, not a gasket tracing.
    s=cq.Workplane('YZ',origin=(x,PORT_Y,0)).rect(82,74).extrude(t).edges('|X').fillet(12).val()
    s=s.cut(cyl(BORE,t+2,cq.Vector(x-1,PORT_Y,0)))
    for y,z in HOLES:s=s.cut(cyl(HOLE_R,t+2,cq.Vector(x-1,y,z)))
    return s
def circle_loft(a,b,ra,rb):
    return cq.Solid.makeLoft([cq.Workplane(cq.Plane(origin=p,normal=X)).circle(r).val() for p,r in [(a,ra),(b,rb)]],ruled=True)

# Reconstruct the original head-side branches and collector in their old frame.
# Omit the buried nominal flange and old straight collector-entry cutter.
d=E.MANIFOLD_PORT_REF['estimated_model_dimensions_mm']
m=E.DATA['inlet_manifold_reconstruction']['estimated_model_dimensions_mm']
h=E.P['head_axis_offset_x'];outside=[];voids=[]
for z in d['inlet_z_stations']:
    pad=cq.Workplane('XY').box(8,42,43).translate((h-77,d['inlet_center_y'],z)).edges('|X').fillet(8).val()
    start=(h-82,d['inlet_center_y'],z)
    end=(m['collector_center_x'],m['collector_center_y'],-m['runner_junction_abs_z'] if z<0 else m['runner_junction_abs_z'])
    outside.extend([pad,E.tapered_between(start,end,16,20),cyl(16,12,cq.Vector(h-85,d['inlet_center_y'],z)),cq.Solid.makeSphere(16,cq.Vector(*start),angleDegrees1=-90,angleDegrees2=90)])
    runner=E.tapered_between(start,end,m['runner_head_inner_radius'],m['runner_collector_inner_radius'])
    runner=runner.intersect(cq.Solid.makeBox(1000,1000,1000,cq.Vector(h-82-1000,-500,-500)))
    voids.extend([runner,cyl(m['runner_head_inner_radius'],15,cq.Vector(h-87,d['inlet_center_y'],z))])
collector=cq.Vector(m['collector_center_x'],m['collector_center_y'],0)
outside.append(cyl(m['collector_outer_radius'],2*m['collector_half_length_z'],collector-Z*m['collector_half_length_z'],Z))
voids.append(cyl(m['collector_inner_radius'],2*m['collector_cavity_half_length_z'],collector-Z*m['collector_cavity_half_length_z'],Z))
# Oblique hollow neck: a separate exposed mounting plane, not a buried disc.
outside.extend([circle_loft(cq.Vector(-155,PORT_Y,0),collector,25,m['collector_outer_radius']),flange(MANIFOLD_FACE_X,8)])
voids.extend([cyl(BORE,10,cq.Vector(MANIFOLD_FACE_X-1,PORT_Y,0)),
              circle_loft(cq.Vector(-153,PORT_Y,0),collector,BORE,m['collector_inner_radius'])])
outer=E.union_solids(*outside).intersect(cq.Solid.makeBox(1000,1000,1000,cq.Vector(h-73-1000,-500,-500)))
manifold=outer.cut(E.union_solids(*voids)).clean()
for y,z in HOLES:manifold=manifold.cut(cyl(HOLE_R,12,cq.Vector(MANIFOLD_FACE_X-1,y,z)))
add('intake_reconstructed_manifold_with_exposed_neck_and_flange',manifold)

# Reuse actual exported component solids and their independently sized details.
source_json=OUT/'midget-1500-measured-core.json'
source_data=json.loads(source_json.read_text());source_colors={p['name']:p['color'] for p in source_data['parts']}
source_meshes={p['name']:p for p in source_data['parts']}
source=cq.Assembly.importStep(str(OUT/'midget-1500-measured-core.step'))
carb_names=[n for n in source_colors if n.startswith('zenith_stromberg_') or n=='carb_damper_cap_photo_reference_provisional']
cleaner_names=[n for n in source_colors if n.startswith('air_cleaner_') or n.startswith('intake_wire_clamp_') or n=='intake_coupling_photo_reference_provisional']
original_volumes={}
for n in carb_names+cleaner_names:
    shape=source.objects[n].obj
    if isinstance(shape,cq.Workplane):shape=shape.val()
    original_volumes[n]=shape.Volume()
    shape=shape.translate((CARB_SHIFT if n in carb_names else CLEANER_SHIFT,0,0))
    if n=='zenith_stromberg_photo_catalog_reference_provisional':
        shape=shape.fuse(flange(CARB_INLET_X,6)).fuse(flange(CARB_OUTLET_X-5,5))
        for y,z in HOLES:shape=shape.cut(cyl(HOLE_R,100,cq.Vector(CARB_INLET_X-1,y,z)))
        shape=shape.clean()
    if n=='air_cleaner_housing_photo_reference_provisional':
        for y,z in HOLES:shape=shape.cut(cyl(HOLE_R,32,cq.Vector(-273,y,z)))
    add(n,shape,source_colors[n])

# Factory p124: one UKC490-role gasket, two studs/nuts/spring washers.
# No spacer block or TKC1990 fitted heatshield is inferred.
add('intake_carb_to_manifold_gasket_role',flange(CARB_OUTLET_X,1),(.30,.25,.18))
studs=[];nuts=[]
for y,z in HOLES:
    studs.append(cyl(3.96875,30,cq.Vector(-180,y,z)))
    head=cq.Workplane(cq.Plane(origin=(-174.5,y,z),normal=X)).polygon(6,14.5).extrude(6).val().cut(cyl(4.05,8,cq.Vector(-175.5,y,z)))
    nuts.extend([head,ring(7,4.1,1.5,cq.Vector(-168.5,y,z))])
add('intake_two_manifold_stud_envelopes',compound(studs),(.51,.53,.48))
add('intake_two_manifold_nut_washer_envelopes',compound(nuts),(.51,.53,.48))
# Factory p122: one148006-role gasket, two screws/washers. Unassigned small
# gasket apertures are not interpreted as fitted air circuits.
add('intake_cleaner_to_carb_gasket_role',flange(-247,1),(.30,.25,.18))
screws=[]
for y,z in HOLES:
    head=cq.Workplane(cq.Plane(origin=(-256.5,y,z),normal=X)).polygon(6,14.5).extrude(5).val()
    screws.extend([cyl(3.96875,22,cq.Vector(-251.5,y,z)),head,ring(7,4.1,1.5,cq.Vector(-251.5,y,z))])
add('intake_two_cleaner_screw_washer_envelopes',compound(screws),(.51,.53,.48))

# Geometry checks support construction only. Preserve source conflicts elsewhere.
body=next(s for n,s,c in parts if n=='zenith_stromberg_photo_catalog_reference_provisional')
assert manifold.intersect(body).Volume()<1e-4
assert manifold.intersect(cyl(BORE-.2,8,cq.Vector(-162,PORT_Y,0))).Volume()<1e-5
for y,z in HOLES:
    for s in [manifold,body]:assert s.intersect(cyl(HOLE_R-.2,14,cq.Vector(-169,y,z))).Volume()<1e-5
assert manifold.intersect(ring(BORE+2,BORE+.2,1,cq.Vector(-162,PORT_Y,0))).Volume()<1e-5
assert manifold.intersect(ring(BORE+2,BORE+.2,1,cq.Vector(-160.8,PORT_Y,0))).Volume()>1

# Shared head-side service boundary. The reference bare-head photograph has
# one flat port land; the old full-width box extended into both manifolds.
# Reconstruct a LOCAL flat land, preserving deck/top/opposite-side geometry.
old_head=source.objects['head_provisional_casting'].obj
if isinstance(old_head,cq.Workplane):old_head=old_head.val()
head_tool=box(300,73,352,cq.Vector(-237,37.5,0))  # X ends at -87; Y1..74
revised_head=old_head.cut(head_tool).clean()
assert revised_head.isValid() and len(revised_head.Solids())==1
add('intake_head_with_reference_flat_manifold_land',revised_head,source_colors['head_provisional_casting'])
# One simplified common gasket. The Payen comparison image has four separate
# round and four rectangular windows; other supplier variants remain distinct.
outline=[(-176,1),(-150,1),(-132,2),(-112,14),(-90,1),(-62,1),(-40,1),
         (40,1),(62,1),(90,1),(112,14),(132,2),(150,1),(176,1),(176,72),(-176,72)]
gasket=cq.Workplane('YZ',origin=(-88,0,0)).polyline([(y,z) for z,y in outline]).close().extrude(1).val()
for z in d['inlet_z_stations']:gasket=gasket.cut(cyl(d['inlet_radius'],3,cq.Vector(-89,d['inlet_center_y'],z)))
for z in d['exhaust_z_stations']:
    tool=cq.Workplane('YZ',origin=(-89,d['exhaust_center_y'],z)).rect(d['exhaust_height_y'],d['exhaust_width_z']).extrude(3).edges('|X').fillet(d['exhaust_corner_radius']).val()
    gasket=gasket.cut(tool)
# Six upper/four lower aperture cues; no fitted stud/dowel identity is assigned.
gasket_holes=[(66,z,3.2 if abs(z)==90 else 4.5) for z in [-145,-90,-30,30,90,145]]+[(7,z,4.5) for z in [-170,-64,64,170]]
for y,z,r in gasket_holes:gasket=gasket.cut(cyl(r,3,cq.Vector(-89,y,z)))
add('intake_common_head_manifold_gasket_role',gasket,(.38,.33,.24))
head_reference=dict(owner_dimensions_applied=False,head_land_plane_x_mm=-87,
    supplier_sources=['https://rimmerbros.com/Item--i-TKC1409', 'https://mossmotors.com/694-120-gasket-manifold-payen-brand'],
    estimated_local_face_region_native_mm=dict(y=[1,74],z=[-176,176]),
    manifold_face_x_mm=-88,estimated_gasket_thickness_mm=1,
    gasket_unassigned_hole_centers_yzr_mm=gasket_holes,
    source_head_volume_mm3=old_head.Volume(),revised_head_volume_mm3=revised_head.Volume(),
    basis='Factory p65 separates one GEG648 head/manifold gasket. Supplier TKC1409 bare-head image shows a common flat manifold-side port land; Payen694-120 comparison shows four round/four rectangular windows and subsidiary fixing apertures. No fitted identity or measured map is established.',
    limits='The local face plane, footprint, gasket outline/gauge/windows and all ten subsidiary aperture locations are construction estimates. No actual head material removal, machining instruction, fitted gasket variant, stud/dowel correspondence, clamp map, sealing land width, torque, internal flow or verified replacement path is established. Deck/top/opposite side, eight port centers and inherited blind recess ends remain. A1 mm source-plane interval is not a measured gasket thickness.')
assert manifold.intersect(revised_head).Volume()<1e-5
assert revised_head.intersect(gasket).Volume()<1e-5

assembly=cq.Assembly(name='unregistered_connected_NAS_intake_reference');meshes=[]
for n,s,c in parts:
    assembly.add(s,name=n,color=cq.Color(*c))
    if n in original_volumes and n not in ['zenith_stromberg_photo_catalog_reference_provisional','air_cleaner_housing_photo_reference_provisional']:
        # Reuse unchanged mesh topology at the same rigid translation as STEP.
        # Retessellating springs and wire clamps adds data without new evidence.
        old=source_meshes[n];shift=CARB_SHIFT if n in carb_names else CLEANER_SHIFT
        meshes.append(dict(name=n,color=c,vertices=[round(v+(shift if i%3==0 else 0),4) for i,v in enumerate(old['vertices'])],triangles=old['triangles']))
        continue
    BRepTools.Clean_s(s.wrapped);mesher=BRepMesh_IncrementalMesh(s.wrapped,.08,False,.16,False);assert mesher.IsDone()
    vs,ts=s.tessellate(.1,.16)
    meshes.append(dict(name=n,color=c,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for t in ts for i in t]))
assembly.export(str(OUT/'intake-mounting-reference.step'))
meta=dict(units='mm',source_frame='Maintained native engine mm +X vehicle-left +Y up +Z front.',owner_dimensions_applied=False,
    owner_photos=['IMG_6854','IMG_6885','IMG_6887','IMG_7034','IMG_7035'],
    factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=65','https://www.spridgets.de/downloads/Parts-1977.pdf#page=122','https://www.spridgets.de/downloads/Parts-1977.pdf#page=124'],
    source_sha256={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in [source_json,OUT/'midget-1500-measured-core.step']},
    replaced_core_names=['inlet_manifold_catalog_reference_provisional','head_provisional_casting']+carb_names+cleaner_names,
    head_face_reference=head_reference,
    original_component_volumes_mm3=original_volumes,
    construction=dict(carb_translation_mm=[CARB_SHIFT,0,0],cleaner_translation_mm=[CLEANER_SHIFT,0,0],manifold_face_x_mm=MANIFOLD_FACE_X,carb_outlet_face_x_mm=CARB_OUTLET_X,carb_inlet_face_x_mm=CARB_INLET_X,cleaner_face_x_mm=-247,port_center_y_mm=PORT_Y,estimated_hole_centers_yz_mm=HOLES,estimated_hole_radius_mm=HOLE_R,estimated_gasket_thickness_mm=1),
    basis='Factory NAS plates distinguish a single carb/manifold gasket and paired studs/nuts/washers from the twin-SU arrangement; a separate gasket and two screws/washers attach the cleaner. Existing photos support assembly order and neighboring obstacles, not these dimensions. Old manifold collector/head-port datums and individual component dimensions are retained. New neck/flanges and coordinated outward translations correct a buried reference flange and source overlap, not fitted registration.',
    limits='All neck/flange outlines, hole maps, clocking, gasket profiles/thicknesses, axial stations and stud/screw/nut/washer dimensions are estimates or nominal family comparisons. The two gasket shapes are simplified role envelopes, not UKC490/148006 tracings; unassigned subsidiary gasket/air passages remain unresolved. No fitted part identity, actual thread, engagement, torque, sealing, coolant/air circuit, heatshield branch, loaded support, tool access or removal path is verified. The source engine study retains its old intake pose and audits. Local throttle references move with the carb; their remote cable endpoint is still untraced. Case/filter sizes are unchanged. Whole-car mounting and surrounding source contacts remain unregistered. Separation offsets are inspection positions, not procedures.',parts=meshes)
(OUT/'intake-mounting-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'intake-mounting-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(parts=len(parts),solids=len(restored.Solids()),step_valid=True,manifold_carb_overlap_mm3=manifold.intersect(body).Volume(),new_flange_exposed=True)),flush=True)
