"""Session 31. Three fixed one-sided tests for clustering of repeat events.
Consecutive-event gaps <=10, longest empty edge stretch, overlapping doubles.
Condition on section counts; 99,999 simulations; three-test Bonferroni correction.
Python + NumPy. No network, decryption, threshold selection or parameter fitting.
"""
import hashlib,itertools,json,math
from lp_core import ROOT,lp,vals,np

SEED=3301031
B=99999
SHORT=10
NAMES=['consecutive_event_gaps_at_most_10','longest_empty_edge_stretch','overlapping_doubles']

def sample(rng,n,k,rows):
    a=rng.integers(n,size=(rows,k))
    bad=np.any(np.diff(np.sort(a,axis=1),axis=1)==0,axis=1)
    while bad.any():
        a[bad]=rng.integers(n,size=(int(bad.sum()),k))
        bad=np.any(np.diff(np.sort(a,axis=1),axis=1)==0,axis=1)
    return np.sort(a,axis=1)

def stats(a,n,short=SHORT):
    a=np.atleast_2d(a);delta=np.diff(a,axis=1)
    empty=np.diff(np.concatenate([np.full((len(a),1),-1),a,np.full((len(a),1),n)],axis=1),axis=1)-1
    return np.column_stack([(delta<=short).sum(axis=1),empty.max(axis=1),(delta==1).sum(axis=1)])

def expected_short(n,k,short=SHORT):
    return sum((n-d)*math.comb(n-d-1,k-2)/math.comb(n,k)
               for d in range(1,min(short,n-k+1)+1))

def controls():
    assert stats(np.array([1,2,8]),12).tolist()==[[2,5,1]]
    all_orders=np.array(list(itertools.combinations(range(8),3)))
    observed=stats(all_orders,8,2)
    assert np.isclose(observed[:,0].mean(),expected_short(8,3,2))
    assert np.isclose(observed[:,2].mean(),3*2/8)
    assert observed[:,1].min()==2 and observed[:,1].max()==5
    return dict(manual_example=dict(eligible_edges=12,event_positions=[1,2,8],statistics=[2,5,1]),
                exhaustive_control=dict(eligible_edges=8,event_count=3,configurations=len(all_orders),
                    short_threshold=2,mean_short_gaps=float(observed[:,0].mean()),
                    mean_overlaps=float(observed[:,2].mean()),minimum_longest_gap=2,maximum_longest_gap=5),passed=True)

def main():
    checks=controls();rng=np.random.default_rng(SEED)
    old=json.loads((ROOT/'double_context_results.json').read_text())['event_inventory']
    sources=[(i,vals(s)) for i,s in enumerate('%'.join(lp[:56]).split('$')) if len(vals(s))>100]
    simulated=np.zeros((B,3),dtype=int);observed=np.zeros(3,dtype=int);artificial=np.zeros(3,dtype=int)
    rows=[];gaps=[];empty_intervals=[];short_mu=0.;overlap_mu=0.
    for section,c in sources:
        n=len(c)-1;a=np.flatnonzero(c[:-1]==c[1:]);k=len(a)
        assert a.tolist()==[e['left_section_offset'] for e in old if e['section']==section]
        obs=stats(a,n)[0];sim=stats(sample(rng,n,k,B),n)
        control=stats(np.arange(k),n)[0]
        for total,item in [(observed,obs),(artificial,control)]:
            total[0]+=item[0];total[1]=max(total[1],item[1]);total[2]+=item[2]
        simulated[:,0]+=sim[:,0];simulated[:,1]=np.maximum(simulated[:,1],sim[:,1]);simulated[:,2]+=sim[:,2]
        es=expected_short(n,k);eo=k*(k-1)/n;short_mu+=es;overlap_mu+=eo
        rows.append(dict(section=section,runes=len(c),eligible_edges=n,event_count=k,
            event_positions=a.tolist(),observed_statistics=obs.tolist(),
            analytic_expected_short_gaps=es,analytic_expected_overlaps=eo))
        for left,right in zip(a[:-1],a[1:]):
            gaps.append(dict(section=section,left_event=int(left),right_event=int(right),distance=int(right-left)))
        anchors=np.r_[-1,a,n]
        for left,right in zip(anchors[:-1],anchors[1:]):
            empty_intervals.append(dict(section=section,start_edge_inclusive=int(left+1),
                end_edge_exclusive=int(right),length=int(right-left-1),
                touches_section_start=bool(left==-1),touches_section_end=bool(right==n)))
    assert sum(r['event_count'] for r in rows)==86
    assert sum(r['eligible_edges'] for r in rows)==12939 and len(gaps)==78
    results=[];control_results=[]
    for j,name in enumerate(NAMES):
        ge=int(np.count_nonzero(simulated[:,j]>=observed[j]))
        raw=(1+ge)/(B+1)
        results.append(dict(statistic=name,observed=int(observed[j]),
            simulation_mean=float(simulated[:,j].mean()),
            central_95_percent=np.quantile(simulated[:,j],[.025,.975]).tolist(),
            at_or_above=ge,raw_upper_tail=raw,three_test_bonferroni=min(1.,3*raw)))
        cge=int(np.count_nonzero(simulated[:,j]>=artificial[j]))
        adjusted=min(1.,3*(1+cge)/(B+1));assert adjusted<=.001
        control_results.append(dict(statistic=name,observed=int(artificial[j]),three_test_bonferroni=adjusted))
    out=dict(seed=SEED,simulations=B,short_gap_threshold=SHORT,
        scope='Same eight substantial LP2 sections: 86 repeat events, 12,939 eligible edges, 78 consecutive-event gaps. Section boundaries not crossed.',
        definitions={'short':'Count gaps <=10 between consecutive repeat-event LEFT-rune positions, pooled over sections. A gap of 10 means event starts ten rune positions apart.',
            'long':'Maximum number of consecutive eligible edges with no repeat event, over all eight sections. Includes leading/trailing section intervals; excludes the bounding events. Not a printed-page or word distance.',
            'overlap':'Count consecutive-event distances equal to 1, corresponding to AAA. A run AAAA contributes two overlapping-event gaps.'},
        null='Uniformly choose each section observed number of distinct eligible edges, independently by section. Same simulated configuration used for all three tests. Event labels only; rune identities, event spacing and page-level event counts are not preserved.',
        correction='Three predeclared one-sided upper-tail Monte Carlo comparisons, each with Bonferroni factor 3. No normal approximation and no selected threshold sweep.',
        measurements=results,analytic_means=dict(short_gaps=short_mu,overlaps=overlap_mu),
        sections=rows,consecutive_event_gaps=gaps,longest_empty_intervals=sorted(empty_intervals,key=lambda x:-x['length'])[:10],
        calculation_controls=checks,
        artificial_cluster_control=dict(description='Place each section events consecutively at its start, retaining all section counts. Deliberately extreme event-label pattern, not encrypted text.',results=control_results),
        event_inventory_crosscheck=True,
        transcription_sha256=hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),
        limits=['Retrospective comparison on previously inspected text; no untouched validation set.',
            'Null requires exchangeability of event locations within sections, not a claimed model of encryption.',
            'Longest interval uses edges without equality, not letters without any repetition at any distance.',
            'Only bursts/excesses are tested; unusually regular spacing or a deficit of overlaps is not a separate tested hypothesis.',
            'Correction covers these three checks only, not every historical research decision.',
            'No full new image audit, rune-specific context test, state-reset attribution, key search or plaintext recovery.',
            'A non-detection does not establish independent events; power is limited by 86 events and the fixed statistics.'])
    (ROOT/'repeat_spacing_results.json').write_text(json.dumps(out,indent=2)+'\n')
    print(json.dumps({k:out[k] for k in ['measurements','analytic_means','longest_empty_intervals','artificial_cluster_control']},indent=2))
if __name__=='__main__':main()
