"""Session 39: exact histogram screen for two-ciphertext-rune linear feedback.
Run with Python 3 + NumPy. No network, key-word guesses or language-model fitting.
"""
from pathlib import Path
import hashlib,json
import numpy as np
from word_core import R,LET,vals,lp,GROUPS
ROOT=Path(__file__).resolve().parent

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

def histogram(seqs):
    return np.bincount(np.concatenate([x[2:] for x in seqs]),minlength=29)

def describe(h):
    return dict(runes=int(h.sum()),histogram=h.tolist(),entropy_bits=entropy(h),
                maximum_frequency=float(h.max()/h.sum()))

def sweep(seqs,k=29,keep=False):
    now=np.concatenate([x[2:] for x in seqs]);prev=np.concatenate([x[1:-1] for x in seqs]);prev2=np.concatenate([x[:-2] for x in seqs])
    records=[]
    for a in range(k):
        for b in range(1,k):
            h=np.bincount((now+a*prev+b*prev2)%k,minlength=k)
            records.append(dict(a=a,b=b,peak_count=int(h.max()),entropy_bits=entropy(h),histogram=h.tolist()))
    high=max(records,key=lambda r:r['peak_count']);low=min(records,key=lambda r:r['entropy_bits'])
    out=dict(runes=len(now),normalized_rules=len(records),maximum_single_rune_count=high['peak_count'],
             maximum_single_rune_frequency=high['peak_count']/len(now),peak_example=high,
             minimum_entropy_bits=low['entropy_bits'],entropy_example=low)
    if keep:out['all_normalized_rules']=records
    return out

def encode(p,A,B,C,K,initial=(7,19),k=29):
    older,prev=initial;out=[];inv=pow(A,-1,k)
    for x in p:
        cur=((int(x)-B*prev-C*older-K)*inv)%k
        out.append(cur);older,prev=prev,cur
    return np.array(out,dtype=np.int64)

def decode(c,A,B,C,K,initial=(7,19),k=29):
    padded=np.r_[initial,c]
    return (A*padded[2:]+B*padded[1:-1]+C*padded[:-2]+K)%k

def controls(lengths):
    streams=[np.concatenate([vals(s) for s in GROUPS.values()]),vals(lp[57])]
    results=[]
    for name,v,key in zip(['LP1 cycled','LP2 page 57 cycled'],streams,[(7,11,23,3),(19,0,5,28)]):
        plain=[v[(np.arange(n)+37*j)%len(v)] for j,n in enumerate(lengths)]
        cipher=[encode(p,*key) for p in plain]
        assert all(np.array_equal(p,decode(c,*key)) for p,c in zip(plain,cipher))
        bound=sweep(cipher);h=histogram(plain)
        A,B,C,K=key;inv=pow(A,-1,29);a=B*inv%29;b=C*inv%29
        q=np.concatenate([(c[2:]+a*c[1:-1]+b*c[:-2])%29 for c in cipher])
        recovered=(A*q+K)%29
        assert np.array_equal(recovered,np.concatenate([p[2:] for p in plain]))
        assert bound['maximum_single_rune_frequency']>=h.max()/h.sum()-1e-12
        assert bound['minimum_entropy_bits']<=entropy(h)+1e-12
        results.append(dict(name=name,key=list(key),roundtrip_exact=True,normalized_decode_exact=True,
                            plaintext=describe(h),sweep=bound))
    # Exhaust every key over a small prime alphabet, independently of normalization.
    k=5;c=np.array([0,4,2,1,1,0,3,4,2,0,1,3,3,4,0,2,1])
    high=0;low=99.;count=0
    for A in range(1,k):
        for B in range(k):
            for C in range(1,k):
                for K in range(k):
                    h=np.bincount((A*c[2:]+B*c[1:-1]+C*c[:-2]+K)%k,minlength=k)
                    high=max(high,int(h.max()));low=min(low,entropy(h));count+=1
    bound=sweep([c],k)
    assert high==bound['maximum_single_rune_count'] and abs(low-bound['minimum_entropy_bits'])<1e-12
    return results,dict(alphabet_size=k,keys_exhausted=count,normalized_rules=20,exact_extrema_agreement=True)

def main():
    sections=[(i,vals(s)) for i,s in enumerate('%'.join(lp[:56]).split('$')) if len(vals(s))>100]
    assert [i for i,c in sections]==[0,1,2,4,5,6,7,8]
    seqs=[c for i,c in sections]
    anchor=np.array([23,11,1,17,18,10]);hits=[]
    for page in range(56):
        c=vals(lp[page])
        for j in range(2,len(c)-5):
            if np.array_equal(c[j:j+6],anchor):
                hits.append(dict(page=page,page_rune_offset=j,prior_two_values=c[j-2:j].tolist(),
                                 prior_two_labels=[LET[x] for x in c[j-2:j]]))
    assert [(h['page'],h['page_rune_offset']) for h in hits]==[(27,28),(55,70)]
    lead_a=np.array(hits[0]['prior_two_values']);lead_b=np.array(hits[1]['prior_two_values'])
    delta=(lead_a-lead_b)%29
    # Difference cancels A and K; exhaust all 29*28 permitted B,C pairs.
    checks=0
    for B in range(29):
        for C in range(1,29):
            da=decode(np.r_[lead_a,anchor],7,B,C,11)[2:]
            db=decode(np.r_[lead_b,anchor],7,B,C,11)[2:]
            expected=np.array([(B*delta[1]+C*delta[0])%29,C*delta[1]%29,0,0,0,0])
            assert np.array_equal((da-db)%29,expected) and da[1]!=db[1]
            checks+=1
    pooled=sweep(seqs,keep=True)
    per=[dict(section=i,**sweep([c])) for i,c in sections]
    known=describe(histogram([vals(s) for s in GROUPS.values()]))
    tests,tiny=controls([len(c) for c in seqs])
    # Section-specific affine shifts can align each peak to the same output rune.
    independent=sum(x['maximum_single_rune_count'] for x in per)/pooled['runes']
    result=dict(formula='p_i = (A*c_i + B*c_(i-1) + C*c_(i-2) + K) mod 29; A,C nonzero',
        normalization='a=B/A, b=C/A modulo 29; q_i=c_i+a*c_(i-1)+b*c_(i-2). A*q_i+K only permutes histogram bins.',
        assumptions=['Natural Gematria index order 0..28; one output rune per input rune.',
            'Coefficients fixed across all eight sections for pooled exact extrema; separate per-section extrema also reported.',
            'Continuous across punctuation, words and pages inside each section; first two runes of every section excluded.',
            'No nulls, F skips, counter, additional key stream or arbitrary reordering before arithmetic.',
            'Benchmarks measure solved rune text, not a necessary property of every possible plaintext.'],
        scope='Eight substantial sections in LP2 pages 0..55; nine-rune section 3 excluded. Previously studied data, not a fresh holdout.',
        pooled=pooled,per_section=per,
        independently_keyed_sections_maximum_single_rune_frequency=independent,
        lp1_benchmark=known,lp2_page57_benchmark=describe(histogram([vals(lp[57])])),
        repeated_phrase=dict(runes=''.join(R[x] for x in anchor),labels=[LET[x] for x in anchor],occurrences=hits,
            predecessor_differences_earlier_minus_later=delta.tolist(),BC_pairs_checked=checks,
            difference_formula=[f'{int(delta[1])}*B+{int(delta[0])}*C mod 29',f'{int(delta[1])}*C mod 29',0,0,0,0],
            note='Under a shared key, last four plaintext runes match; second differs; first may match. This does not identify any plaintext. Separate section keys remove this cross-occurrence constraint.'),
        controls=tests,small_alphabet_exhaustive_check=tiny,
        source_sha256=hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),
        script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
        limitations=['Exact finite-corpus extrema, not significance tests or a proof of non-English plaintext.',
            'No unknown-key plaintext recovery attempted. Controls verify inversion, normalization and bound admissibility.',
            'An arbitrary substitution AFTER this decoder preserves the frequency limits; arbitrary input alphabet ordering is not tested.',
            'A changing key, nonlinear two-rune rule, longer state or intermediate encoded text remains outside scope.'])
    assert pooled['runes']==12931
    (ROOT/'two_rune_feedback_results.json').write_text(json.dumps(result,indent=2)+'\n')
    summary={k:v for k,v in result.items() if k not in ['pooled','controls','per_section']}
    summary['pooled']={k:v for k,v in pooled.items() if k!='all_normalized_rules'}
    summary['per_section']=[{k:v for k,v in x.items() if k not in ['peak_example','entropy_example']} for x in per]
    summary['controls']=[dict(name=x['name'],roundtrip=x['roundtrip_exact'],normalized=x['normalized_decode_exact'],
        plaintext_peak=x['plaintext']['maximum_frequency'],sweep_peak=x['sweep']['maximum_single_rune_frequency']) for x in tests]
    print(json.dumps(summary,indent=2))
if __name__=='__main__':main()
