"""Fixed spatial corridors, two constructors, integer latch/rendezvous runtime.
Only the environment generator/validator sees true positions. Constructors consume
identical local cue views; D's rule receives one site's offers at a time.
"""
import copy, hashlib, heapq, json, math, random, time, sys

VERSION = 'spatial-latch-rendezvous-v1'
RULE = [
    'Start one relay seed at the public input site.',
    'An incoming local offer replicates into its adjacent vacant offered site.',
    'Differentiate by local material: input/fork/relay/latch/interface.',
    'Form the offered positive-delay execution connection to the parent.',
    'Pass cumulative metric plus the next local edge metric to each outgoing offer.',
    'At the interface, hold each faster channel by max(C,M)-channel_metric.'
]
SCHEDULE = ['deliver by (tick,destination,edge,sequence)', 'commit writes',
            'forward messages', 'join interface inputs due this tick',
            'emit at most one answer per interface transaction; discard unmatched inputs']
CAP = dict(cells=64, edges=512, development_ticks=64, development_messages=512,
           development_message_bytes=32768, messages_per_event=64,
           message_bytes_per_event=8192, buffered_messages=64,
           buffered_per_cell=8, model_bytes=1048576, execution_ticks=4096)

def canonical(x):
    return json.dumps(x, sort_keys=True, separators=(',', ':'))

def sha(x):
    return hashlib.sha256(canonical(x).encode()).hexdigest()

def length(p, q):
    return max(1, math.ceil(math.sqrt(sum((a-b)**2 for a,b in zip(p,q)))))

def substrate(m, r, d, seed):
    """Two supplied guide corridors/address. Not a discovered morphology.
    Neighborhood = undirected listed guide bonds; Euclidean distance <= 12.
    A directed port offer marks the forward direction on each such local bond.
    """
    if not (1 <= m <= 4 and 1 <= r <= 4 and d in range(1,6)):
        raise ValueError('substrate envelope')
    rng = random.Random(seed)
    sites = {'P':dict(material='input', address=-1, position=[-1,4.5,-2])}
    bonds = []
    def site(key, material, a, p):
        sites[key] = dict(material=material, address=a,
            position=[round(v+rng.uniform(-.08,.08),6) for v in p])
    def bond(u,v,ch):
        bonds.append(dict(u=u,v=v,channel=ch))
    for a in range(4):
        f,g = f'F{a}',f'G{a}'
        site(f,'fork',a,[0,3*a,0]); site(g,'interface',a,[r+1,3*a+3,1])
        bond('P',f,'T')
        for ch,n in [('M',m),('C',r)]:
            prev=f
            for i in range(1,n+1):
                key=f'{ch}{a}_{i}'
                p=[d+i,3*a,(-1)**i*.3] if ch=='M' else [i,3*a+3,1+.3*(i%2)]
                site(key,'latch' if ch=='M' else 'relay',a,p)
                bond(prev,key,ch); prev=key
            bond(prev,g,ch)
    bonds.sort(key=lambda e:(e['u'],e['v'],e['channel']))
    for i,e in enumerate(bonds):
        e['id']=i; e['delay']=length(sites[e['u']]['position'],sites[e['v']]['position'])
        if e['delay']>12: raise ValueError('neighborhood radius')
    # A nonzero tetrahedral volume establishes genuinely noncoplanar positions.
    p=[sites[k]['position'] for k in ['P','F0','C0_1','M0_1']]
    v=[[p[i][j]-p[0][j] for j in range(3)] for i in [1,2,3]]
    det=sum(v[0][j]*(v[1][(j+1)%3]*v[2][(j+2)%3]-v[1][(j+2)%3]*v[2][(j+1)%3]) for j in range(3))
    if abs(det)<1e-6: raise ValueError('coplanar')
    return dict(version=VERSION,m=m,r=r,d=d,sites=sites,bonds=bonds,volume6=abs(det))

def cue_view(env, mismatch=False):
    # No alternative absolute coordinates enter either constructor. Public metric
    # is the length/speed calibration; neighborhood/material/addresses stay intact.
    sites={k:{'material':v['material'],'address':v['address']} for k,v in env['sites'].items()}
    offers={k:[] for k in sites}
    for e in env['bonds']:
        offers[e['u']].append(dict(to=e['v'],channel=e['channel'],edge=e['id'],
                                  metric=1 if mismatch else e['delay']))
    return dict(sites=sites,offers=offers)

def cell(cue, offers):
    return dict(role=cue['material'],address=cue['address'],
        terminal=cue['material']=='latch' and any(x['to'].startswith('G') for x in offers),
        holds={'C':0,'M':0})

def local_rule(cue, offers, incoming_metric):
    """One local observation; no environment, evaluator, or global graph argument."""
    return cell(cue,offers), [dict(x,arrival=incoming_metric+x['metric']) for x in offers]

def finish(cells, edges, arrivals, cost, start):
    for k,c in cells.items():
        if c['role']=='interface':
            x=arrivals[k]; top=max(x['C'],x['M'])
            c['holds']={ch:top-x[ch] for ch in ['C','M']}
    artifact=dict(version=VERSION,cells=cells,edges=sorted(edges,key=lambda e:e['id']),
        initial=dict(clock=0,sequence=0,stored={k:0 for k,c in cells.items() if c['role']=='latch'},
                     queue=[],outputs=[],peak_buffer=0,sent=0,logical_comm_bytes=0),schedule=SCHEDULE)
    validate(artifact)
    cost['construction_wall_seconds']=time.perf_counter()-start
    cost['construction_cpu_seconds']=time.process_time()-cost.pop('_cpu')
    if cost['construction_wall_seconds']>2: raise TimeoutError('construction >2 seconds')
    cost.update(cells=len(cells),edges=len(edges),serialized_circuit_bytes=len(canonical(artifact).encode()),
                persistent_latch_bytes=len(artifact['initial']['stored']),
                reserved_buffer_payload_bytes=64*24,
                interface_hold_parameter_bytes=4*2*2,
                common_validation_visits=len(cells)+len(edges))
    return artifact,cost

def develop(view):
    start=time.perf_counter(); cpu=time.process_time()
    cells={}; edges=[]; arrivals={}; pending=[('P',0)]
    ticks=offers_sent=reads=applications=0
    # Frontier entries are occupied/newborn site-local offers. Newly replicated
    # cells first act on the following development tick. Global scheduler only
    # enforces occupancy, deterministic order, and finite budgets.
    incoming={}
    while pending:
        ticks+=1
        if ticks>CAP['development_ticks']: raise ValueError('development tick budget')
        following=[]
        for k,metric in sorted(pending):
            if k in cells: continue
            cue=view['sites'][k]; local=view['offers'][k]
            reads+=1+len(local); applications+=1
            c,actions=local_rule(cue,local,metric); cells[k]=c
            for x in actions:
                offers_sent+=1
                incoming.setdefault(x['to'],{})[x['channel']]=x['arrival']
                edges.append(dict(id=x['edge'],u=k,v=x['to'],channel=x['channel'],delay=x['metric']))
                following.append((x['to'],x['arrival']))
        pending=following
        if offers_sent>CAP['development_messages'] or offers_sent*32>CAP['development_message_bytes']:
            raise ValueError('development message budget')
    arrivals=incoming
    return finish(cells,edges,arrivals,dict(_cpu=cpu,construction_ticks=ticks,
        cue_reads=reads,rule_applications=applications,edge_visits=offers_sent,
        development_messages=offers_sent,development_message_bytes=offers_sent*32),start)

def build(view):
    start=time.perf_counter(); cpu=time.process_time()
    # Ordinary global loops plus a topological pass; same primitives and cues.
    cells={k:cell(v,view['offers'][k]) for k,v in sorted(view['sites'].items())}
    edges=[dict(id=x['edge'],u=k,v=x['to'],channel=x['channel'],delay=x['metric'])
           for k in sorted(cells) for x in view['offers'][k]]
    incoming={}; frontier=['P']; cumulative={'P':0}; visits=0; ticks=0
    # A sensible parameterized construction, not global task answering.
    while frontier:
        ticks+=1; following=[]
        for k in sorted(set(frontier)):
            for x in view['offers'][k]:
                visits+=1; arrival=cumulative[k]+x['metric']
                incoming.setdefault(x['to'],{})[x['channel']]=arrival
                cumulative[x['to']]=arrival; following.append(x['to'])
        frontier=following
        if ticks>64: raise ValueError('builder pass budget')
    return finish(cells,edges,incoming,dict(_cpu=cpu,construction_ticks=ticks,
        cue_reads=len(cells)+len(edges),rule_applications=len(cells),edge_visits=2*len(edges),
        development_messages=0,development_message_bytes=0),start)

def validate(c):
    if len(c['cells'])>64 or len(c['edges'])>512 or len(canonical(c))>1048576:
        raise ValueError('construction resource limit')
    if c['schedule']!=SCHEDULE: raise ValueError('schedule')
    for e in c['edges']:
        if e['u'] not in c['cells'] or e['v'] not in c['cells'] or not 1<=e['delay']<=12:
            raise ValueError('invalid execution edge')
    if {x['role'] for x in c['cells'].values()} != {'input','fork','relay','latch','interface'}:
        raise ValueError('operator inventory')
    reached={'P'}
    for _ in range(64):
        enlarged=reached|{e['v'] for e in c['edges'] if e['u'] in reached}
        if enlarged==reached:break
        reached=enlarged
    if reached!=set(c['cells']):raise ValueError('disconnected construction')
    for x in c['cells'].values():
        if any(not 0<=h<=256 for h in x['holds'].values()):raise ValueError('hold bound')
    for a in range(4):
        if f'G{a}' not in c['cells'] or f'F{a}' not in c['cells']: raise ValueError('port')

def execute_in(env,circuit,timing=False):
    c=copy.deepcopy(circuit)
    true={e['id']:e['delay'] for e in env['bonds']}
    for e in c['edges']:e['delay']=true[e['id']]
    changes=[]
    if timing:
        # Frozen targeted permutation: first unequal C/M edge pair per address,
        # in edge-ID order. No target, score, or achieved latency is consulted.
        for a in range(4):
            groups={ch:[e for e in c['edges'] if e['channel']==ch and c['cells'][e['v']]['address']==a] for ch in ['C','M']}
            pairs=[(x,y) for x in groups['C'] for y in groups['M'] if x['delay']!=y['delay']]
            if not pairs: raise AssertionError('timing intervention no-op')
            x,y=pairs[0]; changes.append([x['id'],y['id'],x['delay'],y['delay']])
            x['delay'],y['delay']=y['delay'],x['delay']
        if sorted(e['delay'] for e in c['edges'])!=sorted(true.values()):raise AssertionError('multiset')
    c['positions']={k:v['position'] for k,v in env['sites'].items()}
    return c,changes

def object_bytes(value, seen=None):
    seen=set() if seen is None else seen
    if id(value) in seen:return 0
    seen.add(id(value)); total=sys.getsizeof(value)
    if isinstance(value,dict):total+=sum(object_bytes(k,seen)+object_bytes(v,seen) for k,v in value.items())
    elif isinstance(value,(list,tuple)):total+=sum(object_bytes(x,seen) for x in value)
    return total

class Runtime:
    def __init__(self,artifact):
        self.c=copy.deepcopy(artifact); self.s=copy.deepcopy(artifact['initial'])
        self.out={k:[] for k in artifact['cells']}
        for e in artifact['edges']:self.out[e['u']].append(e)
        self.trajectory=[]; self.trace=[]; self.trace_enabled=False; self.event_sent=0
        self.max_serialized_state=0; self.max_python_state=0
    def snapshot(self):
        return copy.deepcopy(self.s)
    def restore(self,state):
        self.s=copy.deepcopy(state)
    def compiled(self):
        return dict(self.c,initial=self.snapshot())
    def record(self):
        # Complete mutable execution state including clock, buffers and sequence.
        encoded=canonical(self.s);self.trajectory.append(encoded)
        if len(encoded)>self.max_serialized_state:
            self.max_serialized_state=len(encoded);self.max_python_state=max(self.max_python_state,object_bytes(self.s))
    def enqueue(self,t,dest,edge,msg):
        if not (msg[0] in ['W','Q','C','R','V'] and 0<=msg[1]<=65535 and 0<=msg[2]<4 and isinstance(msg[3],int) and -4<=msg[3]<=4):raise AssertionError('message format')
        self.s['sequence']+=1
        heapq.heappush(self.s['queue'],[t,dest,edge,self.s['sequence'],list(msg)])
        q=self.s['queue']; self.s['peak_buffer']=max(self.s['peak_buffer'],len(q))
        if len(q)>64 or sum(x[1]==dest for x in q)>8:raise ValueError('buffer budget')
    def send(self,now,e,msg):
        self.event_sent+=1; self.s['sent']+=1; self.s['logical_comm_bytes']+=24
        if self.event_sent>64 or self.event_sent*24>8192:raise ValueError('message/event budget')
        self.enqueue(now+e['delay'],e['v'],e['id'],msg)
    def inject(self,event):
        if self.s['queue']:raise AssertionError('nonoverlapping transaction violated')
        self.event_sent=0; self.s['clock']=event['t']
        kind='W' if event['kind']=='W' else 'Q'
        for e in self.out['P']:self.send(event['t'],e,[kind,event['id'],event['a'],event['v']])
        self.record()
    def advance(self,until):
        if until>4096:raise ValueError('execution tick budget')
        while self.s['queue'] and self.s['queue'][0][0]<=until:
            now=self.s['queue'][0][0]; self.s['clock']=now
            deliveries=[]
            while self.s['queue'] and self.s['queue'][0][0]==now:
                deliveries.append(heapq.heappop(self.s['queue']))
            # Heap order includes destination, edge, sequence; state commits before
            # interface joins. There are no zero-delay intercell transmissions.
            ready={}
            for _,k,edge,seq,msg in deliveries:
                kind,tid,a,v=msg; c=self.c['cells'][k]; role=c['role']
                if self.trace_enabled:self.trace.append(dict(t=now,cell=k,edge=edge,message=msg))
                if role=='fork':
                    if kind=='W':
                        if a==c['address']:
                            for e in self.out[k]:
                                if e['channel']=='M':self.send(now,e,msg)
                    elif kind=='Q':
                        for e in self.out[k]:self.send(now,e,['C' if e['channel']=='C' else 'R',tid,a,0])
                elif role=='relay':
                    for e in self.out[k]:self.send(now,e,msg)
                elif role=='latch':
                    if kind=='W':
                        self.s['stored'][k]=v
                        if not c['terminal']:
                            for e in self.out[k]:self.send(now,e,msg)
                    elif kind=='R':
                        accumulated=v+self.s['stored'][k]
                        new=['V',tid,a,(accumulated>0)-(accumulated<0)] if c['terminal'] else ['R',tid,a,accumulated]
                        for e in self.out[k]:self.send(now,e,new)
                elif role=='interface' and a==c['address']:
                    if edge>=0:
                        ch='C' if kind=='C' else 'M'
                        self.enqueue(now+c['holds'][ch],k,-1,msg)
                    else:
                        ready.setdefault(k,[]).append(msg)
            # Process internal hold=0 arrivals at this same commit boundary.
            while self.s['queue'] and self.s['queue'][0][0]==now:
                _,k,edge,seq,msg=heapq.heappop(self.s['queue'])
                if edge!=-1:raise AssertionError('same-tick intercell delivery')
                ready.setdefault(k,[]).append(msg)
            for k,items in sorted(ready.items()):
                controls=[x for x in items if x[0]=='C']; values=[x for x in items if x[0]=='V']
                if len(controls)==1 and len(values)==1 and controls[0][1:3]==values[0][1:3]:
                    self.s['outputs'].append([controls[0][1],now,values[0][3],k])
            self.record()
        self.s['clock']=until; self.record()
    def transact(self,e,end):
        before=len(self.s['outputs']); self.inject(e); self.advance(end)
        return self.s['outputs'][before:]

def paths(env):
    answer={}
    for a in range(4):
        root=next(e['delay'] for e in env['bonds'] if e['u']=='P' and e['v']==f'F{a}')
        sums={ch:sum(e['delay'] for e in env['bonds'] if e['channel']==ch and env['sites'][e['v']]['address']==a) for ch in ['C','M']}
        answer[a]=dict(root=root,**sums)
    return answer

def module_extents(env):
    out={}
    for ch,role in [('memory','latch'),('routing','relay')]:
        points=[x['position'] for x in env['sites'].values() if x['material']==role]
        out[ch]=dict(occupied_cells=len(points),per_address=len(points)//4,
            bounding_box_extent=[max(x[i] for x in points)-min(x[i] for x in points) for i in range(3)])
    return out
