"""Word/rune feedback rules selected on prefix; boundary-aware scoring.
Families: d_i=a*c_i+b*feature_i+k mod29, a=1..28, b,k=0..28.
The previous-rune feature uses ciphertext, not decoded plaintext.
"""
import json,time
from word_core import *

def features(source):
    c=vals(source);words=tokenize(source);n=len(c)
    out={}
    for resetname,reset in [('continuous',''),('word','-.,;&$'),('page','%'),('paragraph','&$')]:
        prev=np.r_[0,c[:-1]];_,flags=positions(source,reset=reset);prev[flags]=0
        if resetname=='word':prev[[w['rune_offset'] for w in words]]=0
        out['previous_rune_'+resetname]=prev
    wi=[];lengths=[];prevsum=[];prevgp=[];lastsum=0;lastgp=0
    for i,w in enumerate(words):
        nn=len(w['values']);wi.extend([i]*nn);lengths.extend([nn]*nn);prevsum.extend([lastsum]*nn);prevgp.extend([lastgp]*nn)
        lastsum=sum(w['values']);lastgp=sum(int(pr[v]) for v in w['values'])
    out.update({'word_index':np.array(wi),'word_length':np.array(lengths),'previous_word_rune_sum':np.array(prevsum),'previous_word_GP_sum':np.array(prevgp)})
    return {k:v%29 for k,v in out.items()}

BETA=np.repeat(np.arange(29),29);K=np.tile(np.arange(29),29)

def sweep(source,label):
    words=tokenize(source);ll=np.array([len(w['values']) for w in words]);cs=np.cumsum(ll)
    # First60%, capped256, ending on a complete-word boundary.
    nw=max(1,int(np.searchsorted(cs,min(256,int(.6*cs[-1])),side='right')));cut=int(cs[nw-1]);c=vals(source)
    ff=features(source);records=[];count=0
    for family,f in ff.items():
        for a in range(1,29):
            d=(a*c[None,:cut]+BETA[:,None]*f[None,:cut]+K[:,None])%29
            sc=FULL_MODEL.score(d,ll[:nw]);j=int(np.argmax(sc))
            records.append({'family':family,'a':a,'b':int(BETA[j]),'k':int(K[j]),'prefix_score':float(sc[j])});count+=len(sc)
    records.sort(key=lambda x:x['prefix_score'],reverse=True)
    top=records[:20]
    for r in top:
        d=(r['a']*c+r['b']*ff[r['family']]+r['k'])%29
        r['suffix_score']=float(FULL_MODEL.score(d[None,cut:],ll[nw:])[0]);r['text']=render(source,d);r['decoded_rune_values']=d.tolist()
    return {'target':label,'runes':len(c),'prefix_runes':cut,'prefix_words':nw,'candidate_settings':count,'top_prefix_candidates':top}

def control(family):
    source=lp[57];plain=vals(source);c=np.empty_like(plain);a,b,k=5,7,11;inverse=pow(a,-1,29)
    if family=='previous_rune_continuous':
        prev=0
        for i,p in enumerate(plain):c[i]=inverse*(int(p)-b*prev-k)%29;prev=int(c[i])
    elif family=='previous_word_rune_sum':
        prev=0
        for w in tokenize(source):
            start=w['rune_offset'];end=start+len(w['values']);c[start:end]=inverse*(plain[start:end]-b*prev-k)%29;prev=int(c[start:end].sum())
    else:raise ValueError(family)
    text=replace_values(source,c);out=sweep(text,'synthetic_'+family)
    exact=[i for i,r in enumerate(out['top_prefix_candidates']) if r['decoded_rune_values']==plain.tolist()]
    assert exact,out['top_prefix_candidates'][0]
    return {'family':family,'constructed_parameters':{'a':a,'b':b,'k':k},'exact_plaintext_rank':exact[0]+1,'best_candidate':out['top_prefix_candidates'][0]}

if __name__=='__main__':
    start=time.time();out={'formula':'d_i=(a*c_i+b*feature_i+k)mod29','ranges':{'a':'1..28','b':'0..28','k':'0..28'},'limits':['Ciphertext feedback only; no plaintext-autokey in this pass.','Reset state is0; no F skips; punctuation consumes no state.','Each section starts word index0 and previous-word sums0.','No new plaintext verified by a favorable heuristic score alone.'],'controls':[],'sections':[],'candidate_settings':0}
    for family in ['previous_rune_continuous','previous_word_rune_sum']:
        out['controls'].append(control(family));print('Control recovered',family,flush=True)
    sections=[s for s in '%'.join(lp[:56]).split('$') if len(vals(s))]
    for i,s in enumerate(sections):
        if len(vals(s))<60:continue
        r=sweep(s,f'section_{i}');out['sections'].append(r);out['candidate_settings']+=r['candidate_settings'];t=r['top_prefix_candidates'][0]
        print('Section',i,t['family'],t['prefix_score'],t['suffix_score'],flush=True)
    out['elapsed_seconds']=time.time()-start
    (ROOT/'feedback_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2));print('DONE',out['candidate_settings'],flush=True)
