"""Exact adjacent-pair preserving surrogates via last-exit-tree sampling.
For each nonterminal vertex select its last outgoing edge uniformly from edge
instances; reject unless these form a directed tree to the terminal vertex.
Shuffle all other outgoing edges, then follow them from the original start.
Report simulation comparisons, not a probability of a cipher hypothesis.
"""
import json
from collections import Counter
from word_core import *

def pairs(c):return Counter(zip(map(int,c[:-1]),map(int,c[1:])))

def shuffle_pairs(c,rng):
    if len(c)<2:return c.copy(),0
    adj=[[] for _ in range(29)]
    for a,b in zip(c,c[1:]):adj[int(a)].append(int(b))
    root=int(c[-1]);nodes=[i for i in range(29) if adj[i] and i!=root];attempts=0
    while True:
        attempts+=1;chosen={i:int(rng.integers(len(adj[i]))) for i in nodes}
        parent={i:adj[i][chosen[i]] for i in nodes};valid=True
        for i in nodes:
            seen=set();j=i
            while j!=root:
                if j in seen or j not in parent:valid=False;break
                seen.add(j);j=parent[j]
            if not valid:break
        if valid:break
        if attempts>100000:raise RuntimeError('Last-exit-tree rejection cap reached')
    lists=[]
    for i,a in enumerate(adj):
        b=a.copy()
        if i in chosen:
            last=b.pop(chosen[i]);rng.shuffle(b);b.append(last)
        else:rng.shuffle(b)
        lists.append(b)
    idx=[0]*29;v=int(c[0]);out=[v]
    for _ in range(len(c)-1):
        out.append(lists[v][idx[v]]);idx[v]+=1;v=out[-1]
    out=np.array(out,dtype=np.int64)
    assert out[0]==c[0] and out[-1]==c[-1]
    assert np.array_equal(np.bincount(out,minlength=29),np.bincount(c,minlength=29))
    assert pairs(out)==pairs(c)
    return out,attempts

def phrase_count(v,words):
    phrases=[]
    for i in range(len(words)-1):
        a,b=words[i],words[i+1];na=len(a['values']);nb=len(b['values'])
        if a['section']!=b['section'] or a['paragraph']!=b['paragraph'] or na+nb<6:continue
        av=tuple(v[a['rune_offset']:a['rune_offset']+na]);bv=tuple(v[b['rune_offset']:b['rune_offset']+nb]);phrases.append((av,bv))
    return sum(n*(n-1)//2 for n in Counter(phrases).values())

if __name__=='__main__':
    rng=np.random.default_rng(3301005);source='%'.join(lp[:56]);words=tokenize(source);original=vals(source)
    pages_v=[vals(p) for p in lp[:56]];null=[];attempts=[]
    for i in range(999):
        result=[shuffle_pairs(c,rng) for c in pages_v];v=np.concatenate([r[0] for r in result]);null.append(phrase_count(v,words));attempts.append(sum(r[1] for r in result))
        if (i+1)%100==0:print('Pair-preserving surrogate',i+1,flush=True)
    observed=phrase_count(original,words)
    out={'surrogates':999,'seed':3301005,'observed_phrase_collision_pairs':observed,'surrogate_phrase_collision_pairs':null,'surrogates_at_least_observed':sum(x>=observed for x in null),'mean':float(np.mean(null)),'sampling_attempts':attempts,'checks':'Every generated page checked for identical length, rune inventory, ordered bigram counts and endpoints. Original word/paragraph/section layout retained.','interpretation':'Exploratory comparison for an already known pattern; not independent evidence of a translation. The null is conditional on exact per-page bigrams, and is stricter than independent-rune shuffling.'}
    (ROOT/'transition_surrogate_results.json').write_text(json.dumps(out,indent=2));print('DONE',observed,out['surrogates_at_least_observed'],out['mean'],flush=True)
