"""Supplied bounded router, scalar latch and one-slot control/value handshake.
No constructor, host generator, target/history oracle or task-event import.
"""
import copy, heapq
from common import CAP, BudgetError, IntegrityError, canonical, digest
def validate(a,h):
    cells=a['cells']; root=next(t['site'] for t in a['terminals'] if t['kind']=='query')
    response=next(t['site'] for t in a['terminals'] if t['kind']=='response')
    writes={t['a']:t['site'] for t in a['terminals'] if t['kind']=='write'}
    if len(cells)>min(CAP['cells'],len(h['positions'])) or len(a['edges'])>CAP['edges']: raise BudgetError('deployment size')
    if len(canonical(a).encode())>CAP['model_bytes']: raise BudgetError('deployment bytes')
    legal={tuple(sorted((e['u'],e['v']))):e['delay'] for e in h['bonds']}
    edges={tuple(sorted((e['u'],e['v']))):e['delay'] for e in a['edges']}
    if any(k not in legal or d!=legal[k] or not 1<=d<=2 for k,d in edges.items()): raise IntegrityError('illegal physical edge')
    if any(u not in h['positions'] for u in cells) or any(t['site'] not in cells for t in a['terminals']): raise IntegrityError('invalid occupancy')
    stores={c['address']:u for u,c in cells.items() if c['role']=='latch'}
    if len([c for c in cells.values() if c['role']=='latch'])!=a['K'] or set(stores)!=set(range(a['K'])): raise IntegrityError('independent storage')
    if sum(c['role']=='interface' for c in cells.values())!=1 or cells[root]['role']!='interface': raise IntegrityError('control interface')
    if not any(c['role']=='router' for c in cells.values()): raise IntegrityError('heterogeneous transport role')
    for u,c in cells.items():
        if c['role'] not in ('latch','router','interface'): raise IntegrityError('role')
        for v in c['routes'].values():
            if v not in cells or tuple(sorted((u,v))) not in edges: raise IntegrityError('route edge')
    def reach(u,v,key):
        seen=set()
        while u!=v:
            if u in seen or key not in cells[u]['routes']: raise IntegrityError('route unreachable')
            seen.add(u); u=cells[u]['routes'][key]
    for addr in range(a['K']):
        reach(writes[addr],stores[addr],'W'+str(addr)); reach(root,stores[addr],'Q'+str(addr)); reach(stores[addr],root,'V')
    reach(root,response,'A')
    return True
class Runtime:
    def __init__(self,a):
        self.a=copy.deepcopy(a); self.s=copy.deepcopy(a['initial'])
        self.edges={tuple(sorted((e['u'],e['v']))):e['delay'] for e in a['edges']}
        self.root=next(t['site'] for t in a['terminals'] if t['kind']=='query')
        self.response=next(t['site'] for t in a['terminals'] if t['kind']=='response')
        self.writes={t['a']:t['site'] for t in a['terminals'] if t['kind']=='write'}
        self.trajectory=[]; self.fault=None; self.base_sent=0
        # Evaluator-only observation. Not part of execution state or messages.
        self.commits=[]; self.deliveries=[]; self.trace=False
    def snapshot(self):
        # Complete mutable state, including clock, sequence, counters and buffers.
        return digest(self.s)
    def audit(self):
        # Peak serialization measurement excludes this measurement counter itself.
        s=dict(self.s); s.pop('peak_state_bytes')
        self.s['peak_state_bytes']=max(self.s['peak_state_bytes'],len(canonical(s).encode()))
        self.trajectory.append(self.snapshot())
    def clone(self):
        r=Runtime(self.a); r.s=copy.deepcopy(self.s); r.fault=self.fault; return r
    def reset(self):
        self.s=copy.deepcopy(self.a['initial']); self.trajectory=[]; self.base_sent=0
        self.commits=[]; self.deliveries=[]
    def send(self,u,v,kind,tid,addr,value,terminal=False):
        d=1 if terminal else self.edges.get(tuple(sorted((u,v))))
        if d is None: raise IntegrityError('runtime disconnected edge')
        if self.fault=='disconnect' and not terminal: return
        s=self.s; s['sent']+=1
        if s['sent']-self.base_sent>CAP['messages_per_transaction']: raise BudgetError('task messages')
        heapq.heappush(s['queue'],[s['clock']+d,v,u,s['sequence'],kind,tid,addr,value]); s['sequence']+=1
        s['peak_queue']=max(s['peak_queue'],len(s['queue']))
        if len(s['queue'])>CAP['task_queue'] or sum(x[1]==v for x in s['queue'])>CAP['incoming_per_cell']: raise BudgetError('task queue')
    def inject(self,e):
        if not 0<=e['id']<64 or not 0<=e['a']<self.a['K'] or e['kind'] not in ('W','Q'): raise IntegrityError('command schema')
        if self.s['queue'] or self.s['pending'] is not None: raise IntegrityError('transaction not settled')
        self.s['clock']=e['t']; self.base_sent=self.s['sent']
        u=self.writes[e['a']] if e['kind']=='W' else self.root
        addr=0 if self.fault=='query_blind' and e['kind']=='Q' else e['a']
        self.send('external',u,e['kind'],e['id'],addr,e['v'],True); self.audit()
    def forward(self,u,kind,tid,addr,value):
        c=self.a['cells'][u]; key=kind+str(addr) if kind in ('W','Q') else kind
        if key not in c['routes']: raise IntegrityError('missing runtime route')
        self.send(u,c['routes'][key],kind,tid,addr,value)
    def settle(self,end):
        s=self.s
        while s['queue'] and s['queue'][0][0]<=end:
            t,u,source,seq,kind,tid,addr,value=heapq.heappop(s['queue']); s['clock']=t
            if self.trace: self.deliveries.append([t,u,source,seq,kind,tid,addr,value])
            if t>CAP['episode_ticks']: raise BudgetError('episode ticks')
            if u=='external':
                s['outputs'].append(dict(id=tid,a=addr,t=t,v=value))
                if self.fault=='duplicate': s['outputs'].append(dict(id=tid,a=addr,t=t,v=value))
                if self.fault=='late': s['outputs'][-1]['t']+=2000
                if len(s['outputs'])>CAP['output_records']: raise BudgetError('output buffer')
            else:
                c=self.a['cells'][u]
                if u==self.root and kind=='Q':
                    if s['pending'] is not None: raise BudgetError('interface slot occupied')
                    s['pending']=[tid,addr]
                    self.forward(u,kind,tid,addr,value)
                elif c['role']=='latch' and c['address']==addr and kind in ('W','Q'):
                    if kind=='W':
                        s['stored'][u]=value
                        self.commits.append(dict(id=tid,a=addr,t=t,v=value,site=u))
                    else:
                        if self.fault=='erase': s['stored'][u]=0
                        self.forward(u,'V',tid,addr,s['stored'][u])
                elif u==self.root and kind=='V':
                    if s['pending']!=[tid,addr]: raise IntegrityError('handshake token/address')
                    s['pending']=None; self.forward(u,'A',tid,addr,value)
                elif u==self.response and kind=='A': self.send(u,'external','A',tid,addr,value,True)
                else: self.forward(u,kind,tid,addr,value)
            self.audit()
        s['clock']=end; self.audit()
        out=s['outputs']; s['outputs']=[]; self.audit()
        return out
    def run(self,stream):
        responses=[]; boundary=[]
        from common import GAP
        for e in stream:
            self.inject(e); out=self.settle(e['t']+GAP-1); responses.extend(out)
            boundary.append(self.snapshot())
        return dict(responses=responses,trajectory=self.trajectory,boundary=boundary,final=copy.deepcopy(self.s))
