"""Reproduce a bounded, synthetic SUMO experiment on an archived Seoul OSM extract.
Requires Python 3.12, eclipse-sumo==1.28.0 and pyproj (see requirements.lock). No live traffic or real vehicle control.
Run: python traffic.py MAP_DIRECTORY NEW_OUTPUT_DIRECTORY
"""
import sys,pathlib,json,subprocess,random,time,hashlib,xml.etree.ElementTree as ET,gzip,datetime
import sumo
SUMO_HOME=pathlib.Path(sumo.__file__).parent
sys.path.insert(0,str(SUMO_HOME/'tools'))
import sumolib
ROOT=pathlib.Path(sys.argv[1]).resolve();OUT=pathlib.Path(sys.argv[2]).resolve();OUT.mkdir()
SEED=27;HORIZON=1800;N=600;DEMAND_SEED=20261028
sha=lambda p:hashlib.sha256(pathlib.Path(p).read_bytes()).hexdigest()
began=time.monotonic();net=sumolib.net.readNet(str(ROOT/'seoul.net.xml'))
edges=[e for e in net.getEdges() if e.allows('passenger') and e.getLength()>30]
anchors={'origin':[126.9774,37.5657],'destination':[126.9850,37.5764]}
def nearest(point):
    xy=net.convertLonLat2XY(*point)
    return sorted(edges,key=lambda e:min((x-xy[0])**2+(y-xy[1])**2 for x,y in e.getShape()))[:12]
pair=None
for oi,a in enumerate(nearest(anchors['origin'])):
    for di,b in enumerate(nearest(anchors['destination'])):
        path,cost=net.getFastestPath(a,b,vClass='passenger')
        if path and sum(e.getLength() for e in path)>800:
            rank=oi+di
            if pair is None or rank<pair[0]:pair=(rank,a,b,path,cost)
if pair is None:raise RuntimeError('No valid City Hall–Anguk route')
_,origin,destination,egoPath,freeCost=pair
rng=random.Random(DEMAND_SEED);demand=[]
for attempt in range(30000):
    a,b=rng.sample(edges,2);path,cost=net.getFastestPath(a,b,vClass='passenger')
    if path and 500<=sum(e.getLength() for e in path)<=4500:
        i=len(demand);demand.append({'id':f'car-{i:04d}','depart':round(i*1.5,2),'route':[e.getID() for e in path]})
        if len(demand)==N:break
if len(demand)!=N:raise RuntimeError('Insufficient valid traffic trips')
OUT.joinpath('demand.json').write_text(json.dumps(demand)+'\n')
netroot=ET.parse(ROOT/'seoul.net.xml').getroot()
lanes=[]
for e in netroot.findall('edge'):
    if e.get('function')=='internal':continue
    edge=net.getEdge(e.get('id'))
    if not edge.allows('passenger'):continue
    for lane in edge.getLanes():
        if lane.allows('passenger'):lanes.append({'id':lane.getID(),'edge':edge.getID(),'name':edge.getName(),'speed':lane.getSpeed(),'shape':[[round(x,2),round(y,2)] for x,y in lane.getShape()]})
signals=[{'id':j.get('id'),'x':float(j.get('x')),'y':float(j.get('y'))}for j in netroot.findall('junction')if j.get('type')=='traffic_light']
network={'bounds':net.getBoundary(),'lanes':lanes,'signals':signals,'anchors':anchors,'egoRoute':[e.getID() for e in egoPath],
         'origin':origin.getID(),'destination':destination.getID(),'originName':origin.getName(),'destinationName':destination.getName(),'source':json.loads((ROOT/'map-source.json').read_text()),'license':'ODbL-1.0'}
OUT.joinpath('network.json').write_text(json.dumps(network,ensure_ascii=False)+'\n')
cases=[]
for name,busy,adaptive in [('light-fixed',False,False),('busy-fixed',True,False),('busy-reroute',True,True)]:
    case=OUT/name;case.mkdir();route=ET.Element('routes')
    ET.SubElement(route,'vType',id='car',vClass='passenger',accel='2.6',decel='4.5',sigma='0',speedFactor='1',speedDev='0',length='5',minGap='2.5',maxSpeed='13.89')
    selected=[d for i,d in enumerate(demand)if busy or i%5==0]
    records=[{**d,'ego':False}for d in selected]+[{'id':'ego','depart':120,'route':[e.getID() for e in egoPath],'ego':True}]
    for d in sorted(records,key=lambda d:(d['depart'],d['id'])):
        v=ET.SubElement(route,'vehicle',id=d['id'],type='car',depart=str(d['depart']),departLane='best',departSpeed='max')
        ET.SubElement(v,'route',edges=' '.join(d['route']))
        if adaptive and d['ego']:ET.SubElement(v,'param',key='has.rerouting.device',value='true')
    ET.indent(route);ET.ElementTree(route).write(case/'routes.xml',encoding='utf-8',xml_declaration=True)
    command=[str(SUMO_HOME/'bin/sumo'),'-n',str(ROOT/'seoul.net.xml'),'-r',str(case/'routes.xml'),'--seed',str(SEED),'--end',str(HORIZON),'--step-length','0.5',
       '--no-step-log','true','--duration-log.disable','true','--time-to-teleport','-1','--collision.action','warn',
       '--device.rerouting.probability','0','--device.rerouting.period','30','--device.rerouting.adaptation-interval','10',
       '--fcd-output',str(case/'fcd.xml'),'--device.fcd.period','3','--tripinfo-output',str(case/'tripinfo.xml'),
       '--tripinfo-output.write-unfinished','true','--tripinfo-output.write-undeparted','true',
       '--vehroute-output',str(case/'executed-routes.xml'),'--vehroute-output.write-unfinished','true','--vehroute-output.route-length','true',
       '--statistic-output',str(case/'statistics.xml')]
    tick=time.monotonic()
    with(case/'sumo.log').open('w')as log:subprocess.run(command,stdout=log,stderr=subprocess.STDOUT,check=True)
    triproot=ET.parse(case/'tripinfo.xml').getroot();trips=[dict(t.attrib)for t in triproot.findall('tripinfo')]
    ego=next(t for t in trips if t['id']=='ego')
    stats=ET.parse(case/'statistics.xml').getroot();statistics={c.tag:dict(c.attrib)for c in stats}
    frames=[]
    for event,el in ET.iterparse(case/'fcd.xml',events=['end']):
        if el.tag=='timestep':
            frames.append({'t':float(el.get('time')),'vehicles':[[v.get('id'),round(float(v.get('x')),1),round(float(v.get('y')),1),round(float(v.get('angle')),1),round(float(v.get('speed')),2),v.get('lane')]for v in el.findall('vehicle')]});el.clear()
    vehicle=next(v for v in ET.parse(case/'executed-routes.xml').getroot().findall('vehicle')if v.get('id')=='ego')
    routes=[dict(r.attrib)for r in vehicle.iter('route')]
    # The FCD ends once a vehicle arrives; all simulation times, including empty periods, remain recorded.
    replay={'scenario':name,'stepSeconds':3,'frames':frames,'ego':ego,'egoRoutes':routes}
    OUT.joinpath(f'{name}.json').write_text(json.dumps(replay,separators=(',',':'))+'\n')
    result={'name':name,'backgroundVehicles':len(selected),'adaptiveEgo':adaptive,'elapsedSeconds':time.monotonic()-tick,'ego':ego,'statistics':statistics,
       'arrived':sum(float(t.get('arrival','-1'))>=0 for t in trips),'unfinished':sum(float(t.get('arrival','-1'))<0 for t in trips),'egoRoutes':routes,'command':command}
    cases.append(result);print(json.dumps({name:{'ego':ego,'arrived':result['arrived'],'unfinished':result['unfinished'],'statistics':statistics.get('safety')}}),flush=True)
    # Archive raw outputs without placing multi-megabyte XML documents in the web viewer's critical path.
    for filename in ['fcd.xml','tripinfo.xml','executed-routes.xml','statistics.xml','routes.xml','sumo.log']:
        p=case/filename
        with gzip.open(OUT/f'{name}-{filename}.gz','wb')as out:out.write(p.read_bytes())
version=subprocess.check_output([str(SUMO_HOME/'bin/sumo'),'--version'],text=True).splitlines()[0]
results={'runDate':datetime.datetime.now(datetime.timezone.utc).isoformat(),'simulator':version,'platform':sys.platform,'python':sys.version.split()[0],
 'seed':SEED,'demandSeed':DEMAND_SEED,'horizonSeconds':HORIZON,'egoDeparture':120,'freeFlowCost':freeCost,'cases':cases,'network':{'edges':len(edges),'lanes':len(lanes),'signals':len(signals)},
 'sourceSha256':sha(__file__),'netSha256':sha(ROOT/'seoul.net.xml'),'osmSha256':sha(ROOT/'seoul.osm.xml'),'elapsedSeconds':time.monotonic()-began,
 'limits':['Actual OSM road geometry; synthetic demand, guessed/default road attributes and signal plans.','Rule-based SUMO car-following and lane changing, not a learned autonomous driving or fly-brain controller.','No real-time Seoul traffic, pedestrians, weather or sensor perception.','One demand seed and one driving seed; scenario differences are descriptive, not statistically validated.','Rerouting changes only the marked vehicle; other trips are shared across the two busy scenarios.']}
OUT.joinpath('results.json').write_text(json.dumps(results,ensure_ascii=False,indent=2)+'\n')
for source in ['seoul.osm.xml','seoul.net.xml']:
    with gzip.open(OUT/f'{source}.gz','wb')as f:f.write((ROOT/source).read_bytes())
OUT.joinpath('map-source.json').write_bytes((ROOT/'map-source.json').read_bytes())
OUT.joinpath('traffic.py').write_bytes(pathlib.Path(__file__).read_bytes())
manifest={p.name:sha(p)for p in OUT.iterdir()if p.is_file()}
OUT.joinpath('manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
