"""Session 47: exact screen of an exponential two-ciphertext-rune scale rule.
Requires NumPy. Exhaustive within the stated family, not all nonlinear ciphers.
"""
from pathlib import Path
from itertools import product
import hashlib,json
import numpy as np
from word_core import vals,lp,GROUPS,LET
F=Path(__file__).resolve().parent
POW=np.array([pow(2,e,29) for e in range(28)])
assert set(POW)==set(range(1,29))

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

def components(seqs):
 return tuple(np.concatenate([x[k:len(x)-2+k] for x in seqs]) for k in [0,1,2])

def normalized(c,a,b):
 older,prev,now=components(c)
 return (POW[(a*prev+b*older)%28]*(now-prev))%29

def sweep(seqs):
 rows=[];older,prev,now=components(seqs);delta=(now-prev)%29
 for a,b in product(range(28),repeat=2):
  q=POW[(a*prev+b*older)%28]*delta%29;h=np.bincount(q,minlength=29)
  assert h[0]==np.sum(delta==0)
  rows.append(dict(a=a,b=b,peak_count=int(h.max()),entropy_bits=entropy(h),histogram=h.tolist()))
 hi=max(rows,key=lambda r:r['peak_count']);lo=min(rows,key=lambda r:r['entropy_bits'])
 return dict(scored=len(now),normalized_settings=len(rows),maximum_count=hi['peak_count'],maximum_frequency=hi['peak_count']/len(now),minimum_entropy_bits=lo['entropy_bits'],peak_witness=hi,entropy_witness=lo,all_settings=rows)

def encode(p,a,b,A,R,initial=(7,19)):
 older,prev=initial;out=[]
 for x in p:
  scale=A*int(POW[(a*prev+b*older)%28])%29
  cur=(prev+(int(x)-R)*pow(scale,-1,29))%29
  out.append(cur);older,prev=prev,cur
 return np.array(out,dtype=int)

def decode(c,a,b,A,R,initial=(7,19)):
 return (A*normalized([np.r_[initial,c]],a,b)+R)%29

def recover_prefix(c,p,cut):
 hits=[]
 for a,b in product(range(28),repeat=2):
  q=normalized([c[:cut]],a,b);target=p[2:cut]
  different=np.flatnonzero(q!=q[0]);assert len(different)>0
  j=int(different[0]);A=int((target[j]-target[0])*pow(int(q[j]-q[0])%29,-1,29)%29)
  if A==0:continue
  R=int((target[0]-A*q[0])%29)
  if np.array_equal((A*q+R)%29,target):hits.append([a,b,A,R])
 return hits

def controls():
 out=[]
 for name,p,key in [('decoded LP1 concatenation',np.concatenate([vals(s) for s in GROUPS.values()]),[7,11,5,14]),('decoded LP2 page 57',vals(lp[57]),[20,6,19,14])]:
  c=encode(p,*key);assert np.array_equal(decode(c,*key),p)
  cut=len(p)//2;hits=recover_prefix(c,p,cut);assert key in hits
  continuation=[]
  for k in hits:
   d=decode(c,*k);continuation.append(bool(np.array_equal(d[cut:],p[cut:])))
  assert all(continuation)
  s=sweep([c]);prefix_screen=sweep([c[:cut]]);h=np.bincount(p[2:],minlength=29)
  assert s['maximum_count']>=int(h.max()) and s['minimum_entropy_bits']<=entropy(h)+1e-12
  out.append(dict(name=name,runes=len(p),key=key,training_prefix_end=cut,recovered_keys=hits,all_continuations_exact=all(continuation),roundtrip_exact=True,prefix_screen_peak=prefix_screen['maximum_frequency'],true_peak=float(h.max()/h.sum()),screen_maximum=s['maximum_frequency'],screen_entropy_minimum=s['minimum_entropy_bits']))
 # Independent small-alphabet exhaustive check of normalization.
 qmod=5;g=2;smallc=np.array([0,4,2,1,1,0,3,4,2,0,1,3,3,4,0,2,1]);n=len(smallc)-2;peak=0;low=99.;full=0;normal=[]
 for a,b in product(range(4),repeat=2):
  q=np.array([pow(g,(a*int(v)+b*int(u))%4,qmod)*(int(w)-int(v))%qmod for u,v,w in zip(smallc,smallc[1:],smallc[2:])])
  h=np.bincount(q,minlength=qmod);normal.append((int(h.max()),entropy(h)))
  for A,R in product(range(1,5),range(5)):
   # Independently evaluate the full scalar expression, not the q vector.
   d=[(R+A*pow(g,(a*int(v)+b*int(u))%4,qmod)*(int(w)-int(v)))%qmod for u,v,w in zip(smallc,smallc[1:],smallc[2:])]
   h=np.bincount(d,minlength=qmod);peak=max(peak,int(h.max()));low=min(low,entropy(h));full+=1
 assert peak==max(x[0] for x in normal) and abs(low-min(x[1] for x in normal))<1e-12
 return out,dict(alphabet=5,full_settings=full,normalized_settings=16,peak_and_entropy_agree=True)

def main():
 cc,small=controls()
 sections=[(i,vals(s)) for i,s in enumerate('%'.join(lp[:56]).split('$')) if len(vals(s))>100];seq=[v for _,v in sections]
 shared=sweep(seq);train=sweep(seq[:4]);evaluation=sweep(seq[4:]);per=[dict(section=i,**sweep([v])) for i,v in sections]
 bench=np.bincount(np.concatenate([vals(s)[2:] for s in GROUPS.values()]),minlength=29)
 benchmark=dict(scored=int(bench.sum()),peak_count=int(bench.max()),peak_frequency=float(bench.max()/bench.sum()),entropy_bits=entropy(bench),histogram=bench.tolist())
 threshold=benchmark['peak_frequency'];eligible=[r for r in train['all_settings'] if r['peak_count']/train['scored']>=threshold]
 # Fixed declared decision: continuation/ranking requires a training peak able
 # to reach the solved LP1 benchmark. This is a heuristic compatibility gate,
 # not a theorem about every English plaintext.
 separate_count=sum(r['maximum_count'] for r in per)
 anchor=np.array([23,11,1,17,18,10]);hits=[]
 for page in range(56):
  c=vals(lp[page])
  for off in range(2,len(c)-5):
   if np.array_equal(c[off:off+6],anchor):hits.append(dict(page=page,offset=off,prior_two=c[off-2:off].tolist()))
 assert len(hits)==2
 x,y=[np.r_[h['prior_two'],anchor] for h in hits]
 equal_second=0
 for a,b in product(range(28),repeat=2):
  qx=normalized([x],a,b);qy=normalized([y],a,b)
  assert np.array_equal(qx[2:],qy[2:])
  assert (qx[1]==qy[1])==(b%2==0)
  if b%2:assert (qx[1]+qy[1])%29==0
  equal_second+=int(qx[1]==qy[1])
 out=dict(protocol=dict(formula='P_i = R + A*2^((a*C_(i-1)+b*C_(i-2)) mod28)*(C_i-C_(i-1)) mod29',ranges='a,b=0..27; A=1..28; R=0..28',alphabet='standard 29-rune indices',full_parameter_settings=28*28*28*29,normalization='A and R only permute the output histogram; 784 (a,b) histograms cover all full settings.',initial_state='Exclude first two symbols of every section; words/pages/punctuation do not reset or advance state.',gate='Training sections 0,1,2,4 must attain the solved LP1 peak frequency before real-text candidate ranking/continuation.',limitation='Peak compatibility is a chosen reference screen, not a proof of English impossibility; does not cover arbitrary nonlinear scale functions, rune permutations or changing keys within sections.'),source_sha256=hashlib.sha256((F/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),controls=cc,exhaustive_small_check=small,shared=shared,training=train,evaluation=evaluation,per_section=per,separate_keys=dict(maximum_count=separate_count,scored=shared['scored'],maximum_frequency=separate_count/shared['scored']),benchmark=benchmark,training_survivors=len(eligible),continuation_status='not run: no training survivor' if not eligible else 'requires follow-up',phrase=dict(locations=hits,tail_four_equal_for_all_784=True,second_equal_settings=equal_second,second_equal_iff_b_even=True,odd_b_reflection='P_second_left + P_second_right = 2R mod29'))
 (F/'nonlinear_scale_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2)+'\n')
 print('shared',shared['maximum_count'],shared['maximum_frequency'],shared['minimum_entropy_bits'],'independent',out['separate_keys'],flush=True)
 print('train',train['maximum_frequency'],'evaluation',evaluation['maximum_frequency'],'survivors',len(eligible),'benchmark',benchmark,flush=True)
 print('controls',cc,'small',small,'phrase',out['phrase'],flush=True)
if __name__=='__main__':main()
