"""Session 34: exact frequency bounds for period-2 and period-3 row decoders.
Each (phase, previous ciphertext rune) may choose ANY fixed bijection on the
29 next-rune values. This deliberately relaxes keyed-difference constraints.
No unknown-key recovery. NumPy only, with existing local corpus helpers.
"""
from pathlib import Path
from itertools import permutations,product
import hashlib,json
import numpy as np
from word_core import vals,lp,GROUPS,LET
from keyed_difference_audit import entropy,frequencies
ROOT=Path(__file__).resolve().parent
SEED=3301034
NSIM=999
PERIODS=(2,3)
POLICIES=('section','page','global')

def corpus():
    source='%'.join(lp[:56]);direct=np.concatenate([np.arange(len(vals(p))) for p in lp[:56]])
    items=[];cursor=0;chars=0
    for section,text in enumerate(source.split('$')):
        c=vals(text);n=len(c)
        if n>100:
            page=direct[cursor:cursor+n]
            # Independent character scan validates original-page counters,
            # including sections beginning in the middle of a page.
            current=len(vals(source[:chars].rsplit('%',1)[-1]));checked=[]
            for ch in text:
                if ch=='%':current=0
                elif ch in 'ᚠᚢᚦᚩᚱᚳᚷᚹᚻᚾᛁᛄᛇᛈᛉᛋᛏᛒᛖᛗᛚᛝᛟᛞᚪᚫᚣᛡᛠ':checked.append(current);current+=1
            assert np.array_equal(page,checked)
            items.append(dict(section=section,c=c,section_counter=np.arange(n),page_counter=page,
                global_counter=np.arange(cursor,cursor+n)))
        cursor+=n;chars+=len(text)+1
    assert cursor==12956 and [x['section'] for x in items]==[0,1,2,4,5,6,7,8]
    return items

def table(seqs,phases,period):
    out=np.zeros((period*29,29),dtype=np.int64)
    for c,ph in zip(seqs,phases):
        ids=(ph[1:]*29+c[:-1])*29+c[1:]
        out+=np.bincount(ids,minlength=period*29*29).reshape(period*29,29)
    return out

def bounds(rows,include_rows=True):
    n=int(rows.sum());most=np.sort(rows,axis=1)[:,::-1].sum(0)
    conditional=sum(float(row.sum())/n*entropy(row) for row in rows if row.sum())
    result=dict(edges=n,contexts=len(rows),nonempty_contexts=int((rows.sum(1)>0).sum()),
        mean_edges_per_context=n/len(rows),minimum_entropy_bits=entropy(most),
        maximum_rune_frequency=float(most[0]/n),conditional_entropy_bits=conditional,
        most_concentrated_relaxed_histogram=most.tolist())
    if include_rows:result['transition_rows']=rows.tolist()
    return result

def envelope(samples,obs):
    a=np.asarray(samples)
    return dict(observed=float(obs),mean=float(a.mean()),central_95_percent=np.quantile(a,[.025,.975]).tolist(),
        minimum=float(a.min()),maximum=float(a.max()),samples=a.tolist())

def random_partition_check(seqs,phases,period,seed,observed):
    rng=np.random.default_rng(seed);hs=[];peaks=[]
    for _ in range(NSIM):
        # Preserve number of edges assigned to each phase within every section.
        # First section rune remains excluded, as in the real measurement.
        shuffled=[np.r_[ph[0],rng.permutation(ph[1:])] for ph in phases]
        rows=table(seqs,shuffled,period);hist=np.sort(rows,axis=1)[:,::-1].sum(0)
        hs.append(entropy(hist));peaks.append(float(hist[0]/hist.sum()))
    return dict(seed=seed,simulations=NSIM,
        method='Shuffle phase labels over scored edges within each section, preserving phase counts and all ciphertext transitions. These are arbitrary partitions, not new valid periodic ciphertext sequences.',
        interpretation='Descriptive check of sample fragmentation; intervals are not confidence intervals for plaintext or a multiple-testing-adjusted significance test.',
        minimum_entropy=envelope(hs,observed['minimum_entropy_bits']),
        maximum_frequency=envelope(peaks,observed['maximum_rune_frequency']))

def encode(p,ph,keys,r):
    inverses=np.argsort(keys,axis=1);c=np.empty(len(p),dtype=np.int64);prev=0
    for i,x in enumerate(p):
        h=int(ph[i]);c[i]=inverses[h,(keys[h,prev]+int(x)-r)%29];prev=int(c[i])
    return c

def decode(c,ph,keys,r):
    return (keys[ph,c]-keys[ph,np.r_[0,c[:-1]]]+r)%29

def control(plain,phases,period,policy,seed):
    rng=np.random.default_rng(seed);keys=np.array([rng.permutation(29) for _ in range(period)]);r=28
    cipher=[encode(p,ph,keys,r) for p,ph in zip(plain,phases)]
    for c,p,ph in zip(cipher,plain,phases):
        assert np.array_equal(decode(c,ph,keys,r),p)
        assert np.array_equal(c[1:]==c[:-1],p[1:]==r)
    b=bounds(table(cipher,phases,period),False)
    b1=bounds(table(cipher,[np.zeros(len(c),dtype=np.int64) for c in cipher],1),False)
    f=frequencies(plain)
    assert b['minimum_entropy_bits']<=f['entropy_bits']+1e-12
    assert b['maximum_rune_frequency']>=f['maximum_frequency']-1e-12
    return dict(period=period,policy=policy,seed=seed,construction_keys=keys.tolist(),r=r,
        known_key_roundtrip=True,repeat_identity_verified=True,plaintext=f,correct_phase_bound=b,
        period_one_bound=b1,period_one_excludes_actual_plaintext_entropy=b1['minimum_entropy_bits']>f['entropy_bits'],
        note='Constructed known-key recovery and bound check; no unknown-key learning. Cycled LP1 runes, observed section lengths and selected phase schedule.')

def small_fixture():
    rows=np.array([[3,0,1],[0,2,2],[1,4,0],[3,1,1]])
    best=float('inf');peak=0;choices=list(permutations(range(3)))
    for ps in product(choices,repeat=4):
        hist=sum(row[list(p)] for row,p in zip(rows,ps))
        best=min(best,entropy(hist));peak=max(peak,int(hist.max()))
    b=bounds(rows)
    assert abs(best-b['minimum_entropy_bits'])<1e-12
    assert abs(peak/rows.sum()-b['maximum_rune_frequency'])<1e-12
    return dict(rows=rows.tolist(),assignments=6**4,minimum_entropy=best,maximum_count=peak,verified=True)

def main():
    items=corpus();seqs=[x['c'] for x in items];known=[vals(s) for s in GROUPS.values()]
    base=np.concatenate(known);plain=[base[(np.arange(len(c))+j*37)%len(base)] for j,c in enumerate(seqs)]
    benchmark=frequencies(known)
    baseline=bounds(table(seqs,[np.zeros(len(c),dtype=np.int64) for c in seqs],1))
    old=json.loads((ROOT/'keyed_difference_results.json').read_text())
    assert abs(baseline['minimum_entropy_bits']-old['pooled']['minimum_marginal_entropy_any_row_bijection'])<1e-12
    out=dict(seed=SEED,periods=list(PERIODS),counter_policies=list(POLICIES),
        model='p[i] = G[phase[i], c[i-1]](c[i]), with each G an arbitrary fixed bijection on 29 symbols. Phase mappings shared across selected sections.',
        more_specific_control_formula='p[i] = (pi[phase[i],c[i]] - pi[phase[i],c[i-1]] + r) mod 29',
        scope='12,939 transitions in eight substantial sections. First rune of every section excluded; previous ciphertext carries across internal page boundaries. Page resets affect phase only.',
        phase_convention='Phase uses current decoded position, not its predecessor. Section counter starts at 0; original-page counter starts at 0 each page; global counter counts all runes in LP2 pages 0-55, including the unscored nine-rune section.',
        fixed_design='Periods 2 and 3 only, each with section, original-page and global counters. No further periods, arbitrary per-section phases or alternative history lengths selected after results.',
        baseline_period_one=baseline,benchmark=benchmark,
        sections=[dict(section=x['section'],runes=len(x['c']),first_page_offset=int(x['page_counter'][0]),global_start=int(x['global_counter'][0])) for x in items],
        models=[],controls=[],small_exhaustive_fixture=small_fixture(),
        transcript_sha256=hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),
        script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
    for j,(policy,period) in enumerate(product(POLICIES,PERIODS)):
        phases=[x[policy+'_counter']%period for x in items]
        pooled=bounds(table(seqs,phases,period));train=bounds(table(seqs[:4],phases[:4],period));evaluation=bounds(table(seqs[4:],phases[4:],period))
        # Refining rows cannot increase the relaxed entropy minimum or reduce its maximum frequency.
        assert pooled['minimum_entropy_bits']<=baseline['minimum_entropy_bits']+1e-12
        assert pooled['maximum_rune_frequency']>=baseline['maximum_rune_frequency']-1e-12
        a=next(x for x in items if x['section']==6);b=next(x for x in items if x['section']==8)
        anchor_phases=[int(a[policy+'_counter'][28]%period),int(b[policy+'_counter'][3310]%period)]
        part=random_partition_check(seqs,phases,period,SEED+100+j,pooled)
        out['models'].append(dict(policy=policy,period=period,pooled=pooled,training=train,evaluation=evaluation,
            repeated_phrase_start_phases=anchor_phases,phrase_phases_match=anchor_phases[0]==anchor_phases[1],
            phase_partition_comparison=part))
        out['controls'].append(control(plain,phases,period,policy,SEED+200+j))
        print(json.dumps(dict(policy=policy,period=period,H=pooled['minimum_entropy_bits'],max_frequency=pooled['maximum_rune_frequency'],
            shuffled_H_mean=part['minimum_entropy']['mean'],shuffled_H_95=part['minimum_entropy']['central_95_percent'],
            control_roundtrip=out['controls'][-1]['known_key_roundtrip'])),flush=True)
    out['limits']=['Exact empirical histogram bounds, not population entropy estimates or p-values.',
        'Relaxed independent context permutations need not be realizable by any specific difference-cipher key.',
        'A known-text benchmark does not constrain every possible English passage, other language or intermediate encoding.',
        'Previously examined corpus; separate first-four/last-four checks are retrospective, not blind validation.',
        'One mapping per phase shared across selected sections; independently changing section keys and unknown section phases are outside scope.',
        'No word resets, punctuation advances, selective skips, nulls, variable output lengths or previous plaintext feedback.',
        'More contexts automatically loosen bounds on finite data; random partitions quantify that effect but do not constitute a cipher test or new plaintext.',
        'Control recovery uses known keys; no unknown-key search or recovered real plaintext.']
    (ROOT/'short_cycle_feedback_results.json').write_text(json.dumps(out,indent=2)+'\n')
    print('DONE',len(out['models']),'models,',len(out['controls']),'controls',flush=True)
if __name__=='__main__':main()
