"""D's generic local scheduler and independent B's global shortest-path builder."""
import heapq, json, time
from common import CAP, BudgetError, canonical, digest
from host import neighbors, cues
from program import local_update
def package(h,cells,cost,trace):
    # Serialization only: no topology repair, path finding, or role selection.
    selected=set()
    for u,c in cells.items():
        for v in c['routes'].values(): selected.add(tuple(sorted((u,v))))
    lookup={tuple(sorted((e['u'],e['v']))):e for e in h['bonds']}
    edges=[lookup[k] for k in sorted(selected)]
    a=dict(version='bounded-latch-handshake-v2',K=h['K'],cells=cells,edges=edges,
           terminals=h['terminals'],positions={u:h['positions'][u] for u in cells},
           initial=dict(clock=0,sequence=0,queue=[],stored={u:0 for u,c in cells.items() if c['role']=='latch'},
                        pending=None,outputs=[],sent=0,peak_queue=0,peak_state_bytes=0))
    cost.update(candidate_sites=len(h['positions']),peak_occupied=cost['occupied'],
                occupied_sites=len(cells),active_execution_cells=len(cells),unused_sites=len(h['positions'])-len(cells),
                selected_edges=len(edges),wire_length=sum(__import__('math').dist(h['positions'][e['u']],h['positions'][e['v']]) for e in edges),
                latch_value_bits=2*h['K'],routing_bytes=sum(len(c['routes']) for c in cells.values()),
                structure_bytes=len(canonical(a).encode()),circuit_hash=digest(a))
    return a,cost,trace
def develop(h):
    started=time.perf_counter(); cpu=time.process_time()
    # Only this runtime holds the host. Each call into program.py gets one local
    # state, its own boundary cues, adjacent handles/delays and one delivered msg.
    adj=neighbors(h); boundary=cues(h); root=next(t['site'] for t in h['terminals'] if t['kind']=='query')
    states={}; queue=[]; seq=0; sent=0; tick=0; peak=0; trace=[]; applications=0
    heapq.heappush(queue,(0,root,'',seq,'grow',None)); seq+=1
    while queue:
        tick,u,source,_,kind,payload=heapq.heappop(queue); applications+=1
        if tick>CAP['development_ticks']: raise BudgetError('D development ticks')
        born=u not in states
        state,actions=local_update(states.get(u),boundary[u],adj[u],(kind,source or None,payload))
        if state is None: continue
        states[u]=state
        if len(canonical(state).encode())>CAP['local_development_bytes']: raise BudgetError('D local state')
        if born: trace.append(dict(t=tick,site=u,event='birth',parent=source or None,boundary=boundary[u]))
        if kind=='register': trace.append(dict(t=tick,site=u,event='register',tag=payload,from_site=source))
        for _,v,k,p in actions:
            d=dict(adj[u])[v]; sent+=1
            heapq.heappush(queue,(tick+d,v,u,seq,k,p)); seq+=1
        peak=max(peak,len(queue))
        if sent>CAP['development_messages'] or sent*16>CAP['development_bytes'] or peak>CAP['development_queue']:
            raise BudgetError('D development communication')
        if time.perf_counter()-started>CAP['construction_seconds']: raise BudgetError('D construction time')
    cells={}
    for u in sorted(states):
        _,actions=local_update(states[u],boundary[u],adj[u],('quiet',None,None)); applications+=1
        for _,c in actions:
            cells[u]=c; trace.append(dict(t=tick,site=u,event='differentiate',cell=c))
    cost=dict(occupied=len(states),development_ticks=tick,development_messages=sent,development_bytes=sent*16,
              peak_development_queue=peak,peak_local_state_bytes=max(len(canonical(s).encode()) for s in states.values()),
              peak_total_development_state_bytes=sum(len(canonical(s).encode()) for s in states.values()),
              rule_applications=applications,global_builder_work=0,
              construction_wall_seconds=time.perf_counter()-started,construction_cpu_seconds=time.process_time()-cpu)
    return package(h,cells,cost,trace)
def build(h):
    started=time.perf_counter(); cpu=time.process_time(); adj=neighbors(h)
    root=next(t['site'] for t in h['terminals'] if t['kind']=='query')
    response=next(t['site'] for t in h['terminals'] if t['kind']=='response')
    writes={t['a']:t['site'] for t in h['terminals'] if t['kind']=='write'}
    distances={root:0}; parent={root:None}; heap=[(0,root)]; visits=0; pops=0; pushes=1
    # Independent sensible weighted global planner; no developmental tree access.
    while heap:
        d,u=heapq.heappop(heap); pops+=1
        if d!=distances[u]: continue
        for v,w in adj[u]:
            visits+=1; nd=d+w
            if nd<distances.get(v,10**9):
                distances[v]=nd; parent[v]=u; heapq.heappush(heap,(nd,v)); pushes+=1
    def path(u):
        p=[]
        while u is not None: p.append(u); u=parent[u]
        return list(reversed(p))
    storage={}; used={root,response}; planning=0
    # Place each independent scalar nearer the query on its shortest write path,
    # reducing read traffic without detouring writes. Other write sites reserved.
    for a in sorted(writes):
        candidates=[u for u in path(writes[a])[1:] if u not in used and (u not in writes.values() or u==writes[a])]
        planning+=len(path(writes[a])); u=min(candidates,key=lambda x:(distances[x],x)); storage[a]=u; used.add(u)
    active={root}
    for u in list(writes.values())+[response]: active.update(path(u))
    cells={u:dict(role='interface' if u==root else 'latch' if u in storage.values() else 'router',
                  address=next((a for a,v in storage.items() if v==u),None),response=u==response,routes={}) for u in sorted(active)}
    def route(p,key):
        nonlocal planning
        for u,v in zip(p,p[1:]): cells[u]['routes'][key]=v; planning+=1
    for a in sorted(writes):
        p=path(writes[a]); index=p.index(storage[a]); q=p[:index+1]
        route(list(reversed(p[index:])),'W'+str(a)); route(q,'Q'+str(a)); route(list(reversed(q)),'V')
    route(path(response),'A')
    cost=dict(occupied=len(cells),development_ticks=0,development_messages=0,development_bytes=0,peak_development_queue=0,
              peak_local_state_bytes=0,peak_total_development_state_bytes=0,rule_applications=0,
              global_builder_work=visits+pops+pushes+planning,neighbor_scans=visits,heap_pops=pops,heap_pushes=pushes,
              path_route_visits=planning,construction_wall_seconds=time.perf_counter()-started,construction_cpu_seconds=time.process_time()-cpu)
    if cost['construction_wall_seconds']>CAP['construction_seconds']: raise BudgetError('B construction time')
    return package(h,cells,cost,[])
