import json,io,zipfile,sys
from dataclasses import asdict
from pathlib import Path
import numpy as np
from core import Material,Settings,validate,load_model,grid,candidates,solve,estimates,channels,snapshot_interval,GridResolutionError
from filling import FillSettings,screen
from front import FrontSettings,run_front
from channel_design import design_channels
state=None

def send_result(job,config):
 global state
 s=Settings(**{k:float(v) for k,v in config.get('settings',{}).items()});m=Material(**{k:float(v) for k,v in config.get('material',{}).items()});validate(s,m)
 mesh=load_model('/input.stl',s)
 try:mask,o=grid(mesh,s)
 except GridResolutionError as error:return json.dumps({'error':error.details})
 c=candidates(mesh,mask,o,s)
 if config.get('channels'):
  for name in ('gate','riser'):
   p=np.array(config['channels'][name],dtype=float)
   if p.shape!=(3,) or not np.isfinite(p).all():raise ValueError('Invalid channel coordinates')
  for p in config['channels']['vents']:
   if len(p)!=3 or not np.isfinite(p).all():raise ValueError('Invalid vent coordinates')
  if len(config['channels']['vents'])>20:raise ValueError('Maximum 20 vents')
  c.update({name:config['channels'][name] for name in ('gate','riser','vents')})
 design=None
 if config.get('channel_design',{}).get('enabled'):
  design=design_channels(mesh,s,c,FillSettings(**config.get('hydraulics',{})),FrontSettings(**{k:float(v) for k,v in config.get('front',{}).items()}),config['channel_design'],mask=mask)
 from js import progressCallback
 def progress(f,text):progressCallback(float(f))
 result=None
 if job=='thermal':result=solve(mask,s,m,progress,target_temperature=config.get('thermal_control',{}).get('target_temperature_C'))
 elif job=='flow':result=screen(mask,o,s,m,c,FillSettings(**config.get('hydraulics',{})),progress)
 elif job=='front':result=run_front(mask,o,s,m,c,FillSettings(**config.get('hydraulics',{})),FrontSettings(**{k:float(v) for k,v in config.get('front',{}).items()}),progress,snapshot_plane=True)
 state=(mesh,mask,o,s,m,c,job,result,config)
 preview=mesh.triangles.tolist()
 parts=channels(mesh,c,s)
 data={'job':job,'geometry':{'triangles':preview,'bounds':mesh.bounds.tolist(),'parts':{k:v.triangles.tolist() for k,v in parts.items()}},'channels':c,
       'metrics':estimates(mesh,s,m),'grid':{'shape':list(mask.shape),'volume_cm3':float(mask.sum()*(s.pitch_mm/10)**3),'cells':int(mask.size)},'material':asdict(m),'channel_design':design,'summary':{},'series':[],'slices':[]}
 j=mask.shape[1]//2
 def plane(arr):return np.where(mask[:,j,:],arr[:,j,:],np.nan).T.tolist()
 if result is not None:
  if job=='thermal':
   data['phase_history']=result['phase_history'].tolist()
   data['freeze_map']={'values':np.where(mask[:,j,:].T,result['freeze_time'][:,j,:].T,np.nan).tolist(),'mask':mask[:,j,:].T.tolist(),'origin_mm':[float(o[0]),float(o[2])],'pitch_mm':s.pitch_mm,'y_mm':float(o[1]+j*s.pitch_mm)}
   frozen=result['freeze_time'][mask];valid=frozen[np.isfinite(frozen)]
   def frozen_point(t):
    indices=np.argwhere(mask&np.isfinite(result['freeze_time'])&(result['freeze_time']==t))
    return (o+indices[0]*s.pitch_mm).tolist() if len(indices) else None
   data['cooling']={'target_temperature_C':result['target_temperature_C'],'target_reached':result['target_reached'],'first_solid_s':float(valid.min()) if valid.size else None,'last_solid_s':float(valid.max()) if valid.size else None,'first_solid_point_mm':frozen_point(float(valid.min())) if valid.size else None,'last_solid_point_mm':frozen_point(float(valid.max())) if valid.size else None,'all_solid':bool(np.isfinite(frozen).all()),'initial_temperature_C':s.metal_temp,'final_max_C':float(result['temperature'][mask].max())}
   data['series']=result['history'].tolist();data['summary']={'energy_error_J':result['energy_error_J'],'energy_error_relative':result['energy_error_relative'],'liquid_fraction':float(result['history'][-1,4])}
   data['slices']=[{'time':float(t),'values':np.where(mask[:,j,:].T,a,np.nan).tolist()} for t,a in zip(result['snapshot_times'],result['temperature_slices'])]
  elif job=='flow':
   data['summary']=result['summary']
   arrival=result['arrival_s']; finite=np.isfinite(arrival)&mask
   end=float(arrival[finite].max()) if finite.any() else 0.
   interval=snapshot_interval(s,mask.shape,end)
   times=np.linspace(0,end,int(np.ceil(end/interval))+1) if end>0 else [0.]
   section=arrival[:,j,:].T;section_mask=mask[:,j,:].T
   data['slices']=[{'time':float(t),'values':np.where(section_mask,(np.isfinite(section)&(section<=t)).astype(float),np.nan).tolist()} for t in times]
   ordered=np.sort(arrival[finite])
   data['series']=[[float(t),float(np.searchsorted(ordered,t,side='right')/mask.sum())] for t in times]
  else:
   data['summary']=result['summary'];data['series']=result['history'].tolist()
   data['slices']=[{'time':float(t),'values':np.where(mask[:,j,:].T,a,np.nan).tolist()} for t,a in zip(result['snapshot_times'],result['snapshots'])]
 import scipy,trimesh
 methods=['G1','G2','G3','E1','C1']
 methods+= {'thermal':['T1','T2','T3','T4','N1','X1'],'flow':['F1','F2','F3','X1'],'front':['F1','P1','P2','P3','X1']}.get(job,[])
 data['diagnostics']={
  'schema':1,'methodology_revision':'feeding-2026-11-11','methodology_version':'CL-METHODS-2026-11-09','job':job,'methods_used':methods,
  'effective_config':{'settings':asdict(s),'material':asdict(m),'hydraulics':asdict(FillSettings(**config.get('hydraulics',{}))),'front':asdict(FrontSettings(**{k:float(v) for k,v in config.get('front',{}).items()}))},
  'thermal_control':config.get('thermal_control',{}),'channel_design_options':config.get('channel_design',{}),
  'packages':{'python':sys.version.split()[0],'numpy':np.__version__,'scipy':scipy.__version__,'trimesh':trimesh.__version__},
  'channels':c,'grid':{'shape':list(mask.shape),'origin_mm':o.tolist(),'metal_cells':int(mask.sum()),'total_cells':int(mask.size),'pitch_mm':s.pitch_mm},
  'metrics':data['metrics'],'summary':data['summary'],
  'history_columns':{'thermal':['time_s','T_min_C','T_mean_C','T_max_C','liquid_fraction','heat_out_J','energy_error_J'],'front':['time_s','filled_fraction','gas_gauge_pa','no_outlet_volume_cm3','interface_exposure_m2_s','gas_remaining_mol','gas_escaped_mol','gas_generated_mol']}.get(job,[]),
  'limitations':result.get('limitations',[]) if isinstance(result,dict) else [],
  'snapshot_count':len(data['slices']),'snapshot_interval_requested_s':s.snapshot_dt,
  'snapshot_largest_gap_s':max((b['time']-a['time'] for a,b in zip(data['slices'],data['slices'][1:])),default=0),
  'solver':{k:result[k] for k in ('initial_energy_J','min_dt_s','rejected_steps','energy_error_J','energy_error_relative','snapshot_interval_s') if k in result} if isinstance(result,dict) else {}
 }
 if job=='thermal':
  data['diagnostics']['cooling']=data['cooling']
  from core import LIMITATIONS
  data['diagnostics']['limitations']=LIMITATIONS
 if design:
  if job=='front':
   design['front_check']={'filled_fraction':data['summary']['filled_fraction_reduced_model'],'fill_time_s':data['summary']['time_s'],'maximum_gas_gauge_pa':data['summary']['maximum_gas_gauge_pa'],'scope':'casting-cavity-front; complete assembly filling volume checked separately by capacity inequalities','target_met':bool(data['summary']['filled_fraction_reduced_model']>=1-1e-6 and data['summary']['time_s']<=design['target_fill_s'] and data['summary']['maximum_gas_gauge_pa']<=design['allowed_gas_gauge_pa'])}
  data['diagnostics']['channel_design']=design
  data['diagnostics']['methods_used']+=['C3','C4']
 # NaNs converted to null, for JSON.parse compatibility.
 def clean(x):
  if isinstance(x,float) and not np.isfinite(x):return None
  if isinstance(x,dict):return {k:clean(v) for k,v in x.items()}
  if isinstance(x,list):return [clean(v) for v in x]
  return x
 return json.dumps(clean(data))

def boolean_payload():
 mesh,mask,o,s,m,c,job,result,config=state
 meshes=[mesh,*channels(mesh,c,s).values()]
 return json.dumps([{'vertices':x.vertices.tolist(),'faces':x.faces.tolist()} for x in meshes])

def print_payload():
 if state is None:raise ValueError('Complete geometry preparation before exporting.')
 mesh,mask,o,s,m,c,job,result,config=state
 parts=channels(mesh,c,s)
 top=float(mesh.bounds[1,2]+s.head_mm)
 ports=[{'name':'sprue','point':[float(c['gate'][0]),float(mesh.bounds[0,1]-20),top],'diameter_mm':s.gate_mm}]
 ports += [{'name':f'vent_{i+1}','point':[float(v[0]),float(v[1]),top],'diameter_mm':s.vent_mm} for i,v in enumerate(c['vents'])]
 riser_top=c.get('feeding_geometry',{}).get('top_z_mm',float(c['riser'][2]+1.5*s.riser_mm))
 ports += [{'name':'riser','point':[float(c['riser'][0]),float(c['riser'][1]),riser_top],'diameter_mm':s.riser_mm}]
 return json.dumps({'meshes':[{'name':name,'vertices':part.vertices.tolist(),'faces':part.faces.tolist()} for name,part in [('casting',mesh),*parts.items()]],'ports':ports,'channels':c,'settings':asdict(s),'material':asdict(m),'job':job,'manufacturing_top_mm':c.get('feeding_geometry',{}).get('top_z_mm')})

def export_zip():
 mesh,mask,o,s,m,c,job,result,config=state
 out=io.BytesIO()
 names={'vtok_spojka':'runner','vtok_svisly':'sprue','nalitek':'riser'}
 with zipfile.ZipFile(out,'w',zipfile.ZIP_DEFLATED) as z:
  z.writestr('model.stl',mesh.export(file_type='stl'))
  for name,part in channels(mesh,c,s).items():z.writestr(names.get(name,name.replace('odvzdusneni_','vent_'))+'.stl',part.export(file_type='stl'))
  z.writestr('settings.json',json.dumps(config,indent=2))
  z.writestr('effective_settings.json',json.dumps(asdict(s),indent=2))
  z.writestr('channels.json',json.dumps(c,indent=2))
  z.writestr('README.txt','Research prototype. NOT validated casting CFD. Thermal, hydraulic and gas/front models are separate approximations. No resolved free surface, oxide film transport or coupled freezing flow. See the web application help. STL units: mm. Channels are geometric candidates, not an approved mold design. Individual STL files overlap; use the separate Boolean assembly export for a joined mesh.\n')
  if result:
   arrays={k:v for k,v in result.items() if isinstance(v,np.ndarray)}
   arrays.update(mask=mask,origin_mm=o,pitch_mm=np.array(s.pitch_mm))
   b=io.BytesIO();np.savez_compressed(b,**arrays);z.writestr('fields.npz',b.getvalue())
   if 'history' in result:
    b=io.StringIO();np.savetxt(b,result['history'],delimiter=',');z.writestr('history.csv',b.getvalue())
  if Path('/assembly.stl').exists():z.writestr('assembly_union.stl',Path('/assembly.stl').read_bytes())
  if Path('/run_protocol.json').exists():z.writestr('run_protocol.json',Path('/run_protocol.json').read_bytes())
  for name in ('pattern_with_channels.stl','mold_A_print.stl','mold_B_print.stl','mold_halves_layout.stl','manufacturing_manifest.json'):
   if Path('/'+name).exists():z.writestr(name,Path('/'+name).read_bytes())
 Path('/results.zip').write_bytes(out.getvalue())
