"""Session 46: exact finite-context substitution bounds, with repeat channel fixed.
NumPy required. No unknown key or plaintext is recovered by this script.
"""
from pathlib import Path
from itertools import product
import json,hashlib
import numpy as np
from word_core import lp,vals,GROUPS,LET
F=Path(__file__).resolve().parent

def entropy(v):
 p=v[v>0]/v.sum();return float(-(p*np.log2(p)).sum())

def table(seqs,m,q=29):
 h=np.zeros((q**m,q),dtype=np.int64)
 for s in seqs:
  ids=np.zeros(len(s)-m,dtype=np.int64)
  for j in range(m):ids=q*ids+s[j:len(s)-m+j]
  np.add.at(h,(ids,s[m:]),1)
 return h

def bound(h):
 q=h.shape[1];n=h.sum(1);rows=np.arange(len(h));diag=rows%q
 d=int(h[rows,diag].sum());off=h.copy();off[rows,diag]=-1
 ordered=np.sort(off,axis=1)[:,1:][:,::-1]
 v=np.r_[ordered.sum(0),d]
 return dict(scored=int(v.sum()),repeat_channel=d,concentrated_histogram=v.tolist(),peak_count=int(v.max()),peak_frequency=float(v.max()/v.sum()),minimum_entropy_bits=entropy(v),observed_contexts=int(np.count_nonzero(n)),singleton_contexts=int(np.sum(n==1)),singleton_context_position_fraction=float(np.sum(n==1)/n.sum()),median_context_count=float(np.median(n[n>0])))

def rank_map(h):
 q=h.shape[1];mapping=np.zeros(h.shape,dtype=int)
 for row in range(len(h)):
  prev=row%q;other=[c for c in range(q) if c!=prev]
  other.sort(key=lambda c:(-h[row,c],c))
  mapping[row,other]=np.arange(q-1);mapping[row,prev]=q-1
 return mapping

def mapped_hist(h,mapping):return np.bincount(mapping.ravel(),weights=h.ravel(),minlength=h.shape[1]).astype(int)

def describe(v):return dict(scored=int(v.sum()),histogram=v.tolist(),peak_frequency=float(v.max()/v.sum()),entropy_bits=entropy(v),bin0_frequency=float(v[0]/v.sum()))

def transfer(train,test):
 mapping=rank_map(train);a=mapped_hist(train,mapping);b=mapped_hist(test,mapping)
 assert a.tolist()==bound(train)['concentrated_histogram']
 return dict(training=describe(a),evaluation=describe(b),evaluation_positions_with_unseen_context=int(test[train.sum(1)==0].sum()))

def controls(plain):
 out=[];rng=np.random.default_rng(330146)
 pooled=np.bincount(np.concatenate(plain),minlength=29);rare=int(np.flatnonzero(pooled==min(pooled[pooled>0]))[0]);labels=np.array([x for x in range(29) if x!=rare])
 for m in [1,2,3]:
  mp=np.empty((29**m,29),dtype=int)
  for row in range(len(mp)):
   other=[x for x in range(29) if x!=row%29];mp[row,other]=rng.permutation(labels);mp[row,row%29]=rare
  inv=np.argsort(mp,axis=1);cipher=[]
  for p in plain:
   ctx=tuple(range(7,7+m));cc=[];decoded=[]
   for x in p:
    row=0
    for c in ctx:row=29*row+c
    c=int(inv[row,x]);cc.append(c);decoded.append(int(mp[row,c]));ctx=(*ctx[1:],c)
   assert np.array_equal(p,decoded);cipher.append(np.array(cc))
  h=table(cipher,m);b=bound(h);pv=np.bincount(np.concatenate([p[m:] for p in plain]),minlength=29)
  actual=mapped_hist(h,mp);assert np.array_equal(actual,pv)
  assert b['peak_frequency']+1e-12>=pv.max()/pv.sum() and b['minimum_entropy_bits']<=entropy(pv)+1e-12
  assert b['repeat_channel']==pv[rare]
  out.append(dict(memory=m,repeat_plaintext_label=LET[rare],exact_roundtrip=True,true_plaintext_peak=float(pv.max()/pv.sum()),true_plaintext_entropy=entropy(pv),bound=b))
 # Small independent exhaustive check: arbitrary permutations of the two off-diagonal symbols.
 small=[]
 for m in [1,2]:
  h=rng.integers(0,6,size=(3**m,3));b=bound(h);peak=0;ent=99.
  for bits in product([0,1],repeat=3**m):
   mp=np.empty_like(h)
   for row,bit in enumerate(bits):
    other=[c for c in range(3) if c!=row%3];mp[row,row%3]=2;mp[row,other]=[bit,1-bit]
   v=mapped_hist(h,mp);peak=max(peak,int(v.max()));ent=min(ent,entropy(v))
  assert peak==b['peak_count'] and abs(ent-b['minimum_entropy_bits'])<1e-12
  small.append(dict(memory=m,alphabet=3,maps_exhausted=2**(3**m),extrema_agree=True))
 return out,small

def main():
 sec=[(i,vals(s)) for i,s in enumerate('%'.join(lp[:56]).split('$')) if len(vals(s))>100];seq=[v for _,v in sec];plain=[vals(s) for s in GROUPS.values()]
 results=[]
 for m in [1,2,3]:
  h=table(seq,m);b=bound(h);pv=np.bincount(np.concatenate([v[m:] for v in plain]),minlength=29)
  gap=np.cumsum(np.sort(b['concentrated_histogram'])[::-1])/b['scored']-np.cumsum(np.sort(pv)[::-1])/pv.sum()
  separate=np.sum([bound(table([v],m))['concentrated_histogram'] for v in seq],axis=0)
  results.append(dict(memory=m,shared=b,separate_sections=describe(separate),benchmark=describe(pv),majorization_failing_k=(np.flatnonzero(gap[:-1]<-1e-12)+1).tolist(),transfer=transfer(table(seq[:4],m),table(seq[4:],m))))
 cc,small=controls(plain)
 rng=np.random.default_rng(3301461);rate=sum(int(np.sum(v[1:]==v[:-1])) for v in seq)/sum(len(v)-1 for v in seq);sim=[]
 for _ in range(199):
  ss=[]
  for v in seq:
   inc=rng.integers(1,29,len(v)-1);inc[rng.random(len(inc))<rate]=0
   ss.append(np.r_[rng.integers(29),inc].cumsum()%29)
  b=bound(table(ss,2));t=transfer(table(ss[:4],2),table(ss[4:],2))
  sim.append([b['peak_frequency'],b['minimum_entropy_bits'],t['evaluation']['bin0_frequency']])
 a=np.array(sim)
 out=dict(protocol=dict(family='P_i = permutation[previous m ciphertext runes](C_i), m=1,2,3. One fixed table shared across sections unless stated.',repeat_rule='Every row maps C_i == C_(i-1) to the same plaintext rune R; every other current symbol maps bijectively to the remaining 28.',scope='Eight substantial sections; no word/page resets; omit first m runes of each section. No plaintext-conditioned state, counters, insertions or deletions.',transfer='Sort non-diagonal row counts using sections 0,1,2,4; lock rank assignments; evaluate on 5,6,7,8. Ties use ciphertext index. Bins are anonymous frequency ranks, not plaintext guesses.',limitations='Frequency bounds and normalized partial-sum comparisons are necessary screens, not sufficient conditions for any specific target text. Transfer is one frequency-greedy learner on previously explored sections, not exclusion of every mapping.'),source_sha256=hashlib.sha256((F/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),results=results,controls=cc,exhaustive_checks=small,simulation=dict(seed=3301461,draws=199,repeat_probability=rate,model='Uniform initial rune; repeat previous with pooled observed probability, otherwise uniform among other 28; no plaintext.',columns=['memory2_peak_bound','memory2_entropy_bound','memory2_evaluation_bin0_frequency'],values=sim,median=np.median(a,axis=0).tolist(),central95=np.quantile(a,[.025,.975],axis=0).tolist()))
 (F/'context_memory_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2)+'\n')
 for r in results:print(r['memory'],r['shared'], 'TRANSFER',r['transfer'],flush=True)
 print('SIM',out['simulation']['median'],out['simulation']['central95'],flush=True)
if __name__=='__main__':main()
