"""Session 35: bounded previous-plaintext feedback with prefix-only selection.
Python + NumPy and g++ (C++17). Compiled executable and input live in a temporary
folder, not the published site. All 659,344 specified settings per target.
"""
from pathlib import Path
import hashlib,json,subprocess,tempfile,time
from word_core import *
ROOT=Path(__file__).resolve().parent
SEED=3301035
SETTINGS=28*28*29*29

def decode(c,key):
 a,b,k,state=key;out=np.empty(len(c),dtype=np.int64)
 for i,x in enumerate(c):out[i]=(a*int(x)+b*state+k)%29;state=int(out[i])
 return out

def encode(p,key):
 a,b,k,state=key;inv=pow(a,-1,29);out=np.empty(len(p),dtype=np.int64)
 for i,x in enumerate(p):out[i]=inv*(int(x)-b*state-k)%29;state=int(x)
 return out

def target(source,name,reference=None,plain=None):
 lengths=[len(w['values']) for w in tokenize(source)];cs=np.cumsum(lengths)
 nw=max(1,int(np.searchsorted(cs,min(256,int(.6*int(cs[-1]))),side='right')))
 cut=int(cs[nw-1]);assert 0<cut<len(vals(source))
 return dict(name=name,source=source,c=vals(source),lengths=lengths,nw=nw,cut=cut,
             reference=reference or [1,1,0,0],plain=plain)

def render_words(d,lengths):
 out=[];start=0
 for n in lengths:out.append(''.join(LET[int(v)] for v in d[start:start+n]));start+=n
 return ' '.join(out)

def complete(raw,t):
 assert raw['settings']==SETTINGS
 cut,nw=t['cut'],t['nw'];ll=t['lengths'];records=[]
 for rec in raw['top']:
  d=decode(t['c'],rec['key']);python_score=float(FULL_MODEL.score(d[None,:cut],ll[:nw])[0])
  assert abs(python_score-rec['prefix_score'])<1e-10
  records.append(dict(key=dict(zip(['a','b','k','initial_plaintext_state'],rec['key'])),
      prefix_score=python_score,suffix_score=float(FULL_MODEL.score(d[None,cut:],ll[nw:])[0]),
      prefix_text=render_words(d[:cut],ll[:nw]),suffix_text=render_words(d[cut:],ll[nw:]),
      decoded_rune_values=d.tolist()))
 r=dict(target=t['name'],runes=len(t['c']),prefix_runes=cut,prefix_words=nw,
        suffix_runes=len(t['c'])-cut,settings=SETTINGS,top_prefix_candidates=records)
 reference=decode(t['c'],t['reference'])
 assert abs(float(FULL_MODEL.score(reference[None,:cut],ll[:nw])[0])-raw['reference_score'])<1e-10
 if t['plain'] is not None:
  exact=[i+1 for i,x in enumerate(records) if np.array_equal(x['decoded_rune_values'],t['plain'])]
  r.update(construction_key=t['reference'],exact_plaintext_rank=exact[0] if exact else None,
           true_key_rank=raw['reference_rank'],true_key_prefix_score=raw['reference_score'],
           exact_full_plaintext_recovered=bool(exact))
  assert exact and exact[0]==1,r
 return r

def algebra_checks():
 rng=np.random.default_rng(SEED+1);phrase=np.array([23,11,1,17,18,10]);tests=0
 # Test all feedback coefficients with same ciphertext and different entering plaintext states.
 for b in range(1,29):
  key=[5,b,11,3];other=[5,b,11,17]
  x=decode(phrase,key);y=decode(phrase,other)
  expected=np.array([pow(b,j+1,29)*(3-17)%29 for j in range(6)])
  assert np.array_equal((x-y)%29,expected) and np.all(x!=y);tests+=1
 # Independent encode/decode round trips and the repeated-neighbor identity.
 for _ in range(29):
  key=[int(rng.integers(1,29)),int(rng.integers(1,29)),int(rng.integers(29)),int(rng.integers(29))]
  p=rng.integers(29,size=200);c=encode(p,key);assert np.array_equal(p,decode(c,key))
  assert np.array_equal(c[2:]==c[1:-1],(p[2:]-(1+key[1])*p[1:-1]+key[1]*p[:-2])%29==0)
 return dict(phrase_difference_coefficients_checked=tests,random_roundtrips_and_repeat_identities=29,
   repeated_phrase_rule='With identical a,b,k, aligned plaintext differences equal b**(j+1) times the entering-state difference mod29. For b!=0, either all six corresponding plaintext runes match or none do.',
   double_rule='For i>=2, c[i]==c[i-1] iff p[i]-(1+b)*p[i-1]+b*p[i-2] == 0 mod29. For b=1 this means three plaintext values in arithmetic progression, including constant triples.')

def main():
 start=time.monotonic();control_keys=[[5,7,11,13],[17,28,3,22],[1,1,0,9]];tasks=[]
 plain=vals(lp[57])
 for j,key in enumerate(control_keys):
  c=encode(plain,key);assert np.array_equal(decode(c,key),plain)
  tasks.append(target(replace_values(lp[57],c),f'control_{j+1}',key,plain))
 pieces=[(i,s) for i,s in enumerate('%'.join(lp[:56]).split('$')) if len(vals(s))>100]
 for i,s in pieces:tasks.append(target(s,f'section_{i}'))
 rng=np.random.default_rng(SEED);n=len(vals(pieces[0][1]));null=np.empty(n,dtype=np.int64);null[0]=rng.integers(29)
 for i in range(1,n):null[i]=null[i-1] if rng.random()<86/12939 else (null[i-1]+rng.integers(1,29))%29
 tasks.append(target(replace_values(pieces[0][1],null),'random_anti_repeat_baseline'))
 out=dict(seed=SEED,formula='p[i] = (a*c[i] + b*p[i-1] + k) mod29',
    ranges=dict(a='1..28',b='1..28',k='0..28',initial_plaintext_state='0..28'),
    counter_and_reset='Natural GP zero-based rune values. Continuous plaintext state through each marked section; punctuation, words and pages do not reset or advance it. Initial state searched separately for every target.',
    selection='First 60% capped at 256 runes, cut at a complete word. Top 20 settings retained by prefix score only. Decode whole section once; carry resulting plaintext state into suffix without refitting.',
    scoring='Unchanged LP1-trained word-boundary trigram model FULL_MODEL. Mean natural-log score per rune plus word-end event; a heuristic, not a calibrated probability.',
    algebra_checks=algebra_checks(),controls=[],sections=[],random_baseline=None,
    transcript_sha256=hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),
    python_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
    cpp_sha256=hashlib.sha256((ROOT/'plaintext_feedback_search.cpp').read_bytes()).hexdigest())
 with tempfile.TemporaryDirectory(prefix='lp-plaintext-feedback-') as tmp:
  exe=Path(tmp)/'search';inp=Path(tmp)/'input.txt'
  subprocess.run(['g++','-O3','-std=c++17',str(ROOT/'plaintext_feedback_search.cpp'),'-o',str(exe)],check=True)
  with inp.open('w') as f:
   f.write(' '.join(format(float(v),'.17g') for v in FULL_MODEL.log.ravel())+'\n'+str(len(tasks))+'\n')
   for t in tasks:
    f.write(f"{t['cut']} {t['nw']} "+' '.join(map(str,t['reference']))+'\n')
    f.write(' '.join(map(str,t['c'][:t['cut']]))+'\n')
    f.write(' '.join(map(str,t['lengths'][:t['nw']]))+'\n')
  proc=subprocess.Popen([str(exe),str(inp)],stdout=subprocess.PIPE,text=True)
  received=0
  for line in proc.stdout:
   raw=json.loads(line);assert raw['task']==received;t=tasks[received];received+=1;r=complete(raw,t)
   if t['name'].startswith('control'):out['controls'].append(r)
   elif t['name'].startswith('section'):out['sections'].append(r)
   else:out['random_baseline']=r
   best=r['top_prefix_candidates'][0]
   print(json.dumps(dict(target=r['target'],settings=r['settings'],prefix=r['prefix_runes'],suffix=r['suffix_runes'],
       best_key=best['key'],prefix_score=best['prefix_score'],suffix_score=best['suffix_score'],
       exact_rank=r.get('exact_plaintext_rank'))),flush=True)
  assert proc.wait()==0 and received==len(tasks)
 out['real_text_settings']=sum(r['settings'] for r in out['sections'])
 out['total_settings_including_controls_and_baseline']=sum(r['settings'] for r in out['sections']+out['controls']+[out['random_baseline']])
 out['elapsed_seconds']=time.monotonic()-start
 out['limits']=['Exhaustive only for stated affine recurrence and parameter ranges; no arbitrary symbol permutation, nonlinear update, longer memory, variable key or additional encoding layer.',
   'b=0 has no plaintext feedback and was covered by earlier affine tests; it is omitted here.',
   'A separate key may be selected for each section. Retained candidates are ranked solely by prefixes, not by suffix scores.',
   'Language model and corpus were used in prior sessions: suffix exclusion applies to this fitting procedure, not to all historical exploration.',
   'Controls reuse one held-out 95-rune solved passage with three keys; they verify recovery in those constructions, not all natural language.',
   'Only the top 20 prefix candidates per target have full continuation scores and saved texts; lower-ranked settings were all prefix-scored but not exhaustively judged as complete plaintext.',
   'One random anti-repeat baseline uses section-0 layout and p=86/12939; it is descriptive, not a null ensemble or significance test.',
   'No result is declared a solution based solely on a language score.']
 (ROOT/'plaintext_feedback_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2)+'\n')
 print('DONE real settings',out['real_text_settings'],'total',out['total_settings_including_controls_and_baseline'],flush=True)
if __name__=='__main__':main()
