"""Bounded prime/Fibonacci hypotheses; source-square reconstruction and held-out tests.
Run with NumPy. Exact nested primes are computed by sieve, never by reducing the
index modulo 29. Modular extensions are separate, explicitly named hypotheses.
"""
import json, math, time
from collections import Counter
from lp_core import *

SQUARE=np.array([[3258,3222,3152,3038],[3278,3299,3298,2838],[3288,3294,3296,2472],[4516,1206,708,1820]])
SPIRAL=[(1,1),(1,2),(2,2),(2,1),(2,0),(1,0),(0,0),(0,1),(0,2),(0,3),(1,3),(2,3),(3,3),(3,2),(3,1),(3,0)]
INDEX=[0,1]
while len(INDEX)<33: INDEX.append(2 if len(INDEX)==2 else INDEX[-1]+INDEX[-2])
# Upper bound n(log n + log log n), with margin; nth prime for n>=6.
n=INDEX[-1]+1
limit=int(n*(math.log(n)+math.log(math.log(n))))+100
sieve=np.ones(limit+1,dtype=bool);sieve[:2]=False
for i in range(2,math.isqrt(limit)+1):
    if sieve[i]: sieve[i*i::i]=False
large_primes=np.flatnonzero(sieve);del sieve
assert len(large_primes)>INDEX[-1]
exact_primes=large_primes[INDEX];del large_primes
square_values=np.array([int(SQUARE[r,c]) for r,c in SPIRAL])
assert np.array_equal(abs(3301-exact_primes[:16]),square_values)
assert pr[463]==3301
reconstruction={'zero_based_prime_indices':INDEX[:16], 'primes':exact_primes[:16].tolist(),'expected_spiral':square_values.tolist(),'calculated_spiral':abs(3301-exact_primes[:16]).tolist(),'all_16_match':True,'index_convention':'prime[0]=2; prime[463]=3301','exact_sequence_computed_through_position':32,'largest_exact_prime_index':INDEX[-1], 'largest_exact_prime':int(exact_primes[-1])}

L=18000
# F_n modulo m, n=0,1,... . The square skips the duplicate F_2=1.
def fibmod(m,size=L+2):
    out=np.zeros(size,dtype=np.int64);out[1]=1
    for i in range(2,size):out[i]=(out[i-1]+out[i-2])%m
    return out

def skip_duplicate(a):return np.concatenate((a[:2],a[3:]))[:L]

STREAMS={};DESCRIPTIONS={}
def add(name,seq,description):
    STREAMS[name]=np.asarray(seq,dtype=np.int64)%29
    DESCRIPTIONS[name]=description
f=fibmod(29);p=pr[:L]%29
for name,key,desc in [('prime_plus_fib',p+f[:L],'p[n]+F[n]'),('prime_minus_fib',p-f[:L],'p[n]-F[n]'),('prime_times_fib',p*f[:L],'p[n]*F[n]')]:add(name,key,desc+'; zero-based primes and F[0]=0,F[1]=1')
# F modulo29 is periodic; calculate through the largest actual prime index.
long_fib=fibmod(29,int(pr[L-1])+1)
add('fib_at_prime',long_fib[pr[:L]],'F[p[n]], with exact prime index; only the Fibonacci value is reduced modulo29')
add('fib_at_prime_minus_one',long_fib[pr[:L]-1],'F[p[n]-1], with exact prime index')
for standard in (True,False):
    tag='standard_fib' if standard else 'square_fib'
    if standard:
        ep=np.concatenate((exact_primes[:2],exact_primes[1:]))
    else:ep=exact_primes
    add('exact_prime_'+tag,ep,'Exact p at Fibonacci indices; finite sequence, no wrap. '+tag)
    add('exact_distance_'+tag,abs(3301-ep),'Exact |3301-p[index]|; finite sequence, no wrap. '+tag)
    for modulus in (29,464,3301):
        fi=fibmod(modulus);fi=fi[:L] if standard else skip_duplicate(fi)
        pv=pr[fi]
        add(f'mod{modulus}_prime_{tag}',pv,f'p[F index mod {modulus}], zero-based; exploratory modular extension, '+tag)
        add(f'mod{modulus}_distance_{tag}',abs(3301-pv),f'|3301-p[F index mod {modulus}]|; exploratory modular extension, '+tag)
for kind,spiral in [('square_values',square_values),('square_primes',exact_primes[:16]),('square_indices',np.array(INDEX[:16]))]:
    matrix=np.empty((4,4),dtype=np.int64)
    for (r,c),v in zip(SPIRAL,spiral):matrix[r,c]=v
    for order,arr in [('spiral',spiral),('row',matrix.ravel()),('column',matrix.T.ravel())]:
        for reverse in (False,True):
            seq=arr[::-1] if reverse else arr
            add(f'cyclic_{kind}_{order}_rev{int(reverse)}',np.resize(seq,L),f'Observed sixteen {kind}, read {order}, reversed={reverse}, then repeated; repetition is a hypothesis')
# Cumulative subtraction of Fibonacci numbers from one-based prime index464.
# Finite unwrapped trace is recorded; modular versions are separate conjectures.
trace=[];idx=464;a,b=0,1
while idx-a>=1:
    idx-=a;trace.append({'step_fibonacci':a,'one_based_prime_index':idx,'prime':int(pr[idx-1])});a,b=b,a+b
reconstruction['finite_464_walk']=trace
for modulus in (464,3301):
    steps=fibmod(modulus)[:L]
    indices=(463-np.cumsum(steps))%modulus
    pv=pr[indices]
    add(f'walk464_mod{modulus}',pv,f'One-based index starts464, subtract cumulative F[n], wrap to1..{modulus}; exploratory extension')
    add(f'walk464_mod{modulus}_distance',abs(3301-pv),f'|3301-prime| from cumulative Fibonacci subtraction and index wrap1..{modulus}')

RESETS={'continuous':'','page':'%','paragraph':'&$','clause':'.;&$','word':'-.,;&$'}
OFFSETS=np.arange(17)
# Includes ordinary, prime-minus-one, Atbash, and +/-3301 shift conventions.
SHIFTS=np.array([0,1,28,3301%29,(-3301)%29])

def sweep(source,label,retain=True):
    c=vals(source);cut=min(256,int(len(c)*.6));records=[];count=0;by_stream=Counter()
    for resetname,reset in RESETS.items():
        for skipf in (False,True):
            ix,_=positions(source,reset=reset,skip_f=skipf)
            for name,seq in STREAMS.items():
                offsets=OFFSETS[OFFSETS+int(ix.max())<len(seq)]
                if not len(offsets):continue
                key=seq[offsets[:,None]+ix[None,:cut]]
                for a in (1,-1):
                    for sign in (-1,1):
                        d=(a*c[None,None,:cut]+sign*key[:,None,:]+SHIFTS[None,:,None])%29
                        flat=d.reshape(-1,cut)
                        if skipf:flat[:,c[:cut]==0]=0
                        sc=scores(flat);j=int(np.argmax(sc));oi,si=np.unravel_index(j,d.shape[:2])
                        records.append({'sequence':name,'reset':resetname,'skip_all_cipher_F':skipf,'a':a,'sign':sign,'offset':int(offsets[oi]),'shift':int(SHIFTS[si]),'prefix_score':float(sc[j])})
                        count+=len(flat);by_stream[name]+=len(flat)
    records.sort(key=lambda r:r['prefix_score'],reverse=True)
    if not retain:return records[0]['prefix_score']
    top=records[:20]
    for r in top:
        ix,_=positions(source,reset=RESETS[r['reset']],skip_f=r['skip_all_cipher_F'])
        d=(r['a']*c+r['sign']*STREAMS[r['sequence']][ix+r['offset']]+r['shift'])%29
        if r['skip_all_cipher_F']:d[c==0]=0
        r['suffix_score']=float(scores(d[None,cut:])[0]);r['text']=render(source,d)
        r['decoded_rune_values']=d.tolist()
    return {'target':label,'rune_count':len(c),'prefix_length':cut,'candidate_settings':count,'settings_per_stream':dict(by_stream),'top_prefix_candidates':top}

def synthetic_control(name,reset,offset):
    source=lp[57];plain=vals(source);ix,_=positions(source,reset=RESETS[reset])
    cipher=(plain+STREAMS[name][ix+offset])%29
    it=iter(cipher);ct=''.join(R[int(next(it))] if ch in R else ch for ch in source)
    found=sweep(ct,'synthetic_'+name)
    exact=[i for i,r in enumerate(found['top_prefix_candidates']) if r['decoded_rune_values']==plain.tolist()]
    assert exact,found['top_prefix_candidates'][0]
    return {'construction':{'stream':name,'reset':reset,'offset':offset},'exact_recovery_top20':True,'best_exact_rank':exact[0]+1,'best_candidate':found['top_prefix_candidates'][0]}

if __name__=='__main__':
    start=time.time()
    out={'square_reconstruction':reconstruction,'stream_definitions':DESCRIPTIONS,'configuration':{'offsets':OFFSETS.tolist(),'shifts':SHIFTS.tolist(),'resets':RESETS,'prefix_selection':'min(256,floor(.6*target_length)); suffix not used for parameter selection','score_model':'small LP1-only bi/trigram model from lp_core.py','limits':'Negative result covers only stated rules. Finite exact streams are used only where every target position is in range. Skips either none or every ciphertext F. No selective skip beam in this session.'},'controls':[],'sections':[],'candidate_settings':0}
    for conf in [('exact_prime_square_fib','word',3),('walk464_mod464','paragraph',7)]:
        control=synthetic_control(*conf);out['controls'].append(control);print('Exact control recovered',conf,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
        result=sweep(s,f'marked_section_{i}');out['sections'].append(result);out['candidate_settings']+=result['candidate_settings']
        r=result['top_prefix_candidates'][0]
        print('Section',i,'settings',result['candidate_settings'],'top',r['prefix_score'],'suffix',r['suffix_score'],r['sequence'],flush=True)
    out['seconds']=time.time()-start
    (ROOT/'combined_results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2))
    print('DONE',out['candidate_settings'],'settings',round(out['seconds'],1),'seconds',flush=True)
