"""Session 54: known-answer representation review and source-event ledger.
No cipher search or proposed plaintext.
"""
from pathlib import Path
from collections import Counter,defaultdict
import json,hashlib
import numpy as np
from lp_core import R,vals,pages,lp,SEQS
from word_core import GROUPS,replace_values
from lexical_words import tokenize_words
ROOT=Path(__file__).resolve().parent

def profile(sources):
    vs=[vals(s) for s in sources];ws=[w for s in sources for w in tokenize_words(s)]
    length=[len(w['values']) for w in ws];v=np.concatenate(vs);h=np.bincount(v,minlength=29)
    ic=sum(int(np.sum(np.bincount(a,minlength=29)*(np.bincount(a,minlength=29)-1))) for a in vs)/sum(len(a)*(len(a)-1) for a in vs)
    double=sum(int(np.sum(a[1:]==a[:-1])) for a in vs);eligible=sum(max(0,len(a)-1) for a in vs)
    return {'runes':len(v),'words':len(ws),'word_length_histogram':dict(sorted(Counter(length).items())),'mean_runes_per_word':len(v)/len(ws),'singleton_words':sum(n==1 for n in length),'singleton_fraction':sum(n==1 for n in length)/len(length),'rune_peak_frequency':float(h.max()/h.sum()),'within_source_index_of_coincidence':ic,'adjacent_equal':double,'adjacent_positions':eligible,'adjacent_equal_fraction':double/eligible}

def main():
    checks=[]
    specs=[('warning',[0]),('wisdom',[3]),('koan',[4,5,6,7]),('divinity',[8,9,10,11]),('instruction',[14])]
    for name,inds in specs:
        cipher='%'.join(pages[i] for i in inds);plain=GROUPS[name]
        old=[len(w['values']) for w in tokenize_words(cipher)];new=[len(w['values']) for w in tokenize_words(plain)]
        assert old==new
        checks.append({'name':name,'method':'reverse alphabet' if name=='warning' else 'shifted reverse alphabet' if name=='koan' else 'direct rune reading','word_lengths_identical':True,'cipher':profile([cipher]),'plain':profile([plain])})
    c=vals(lp[56]);ix=np.arange(len(c));ix[57:]-=1
    p=(c-SEQS['prime_minus_one'][ix])%29;p[56]=c[56]
    roundtrip=(p+SEQS['prime_minus_one'][ix])%29;roundtrip[56]=p[56];assert np.array_equal(roundtrip,c)
    plain56=replace_values(lp[56],p)
    assert [len(w['values']) for w in tokenize_words(lp[56])]==[len(w['values']) for w in tokenize_words(plain56)]
    checks.append({'name':'LP2 page56','method':'prime-minus-one with known F exception at rune56','word_lengths_identical':True,'cipher':profile([lp[56]]),'plain':profile([plain56]),'exact_roundtrip':True})
    checks.append({'name':'LP2 page57','method':'direct rune reading','word_lengths_identical':True,'cipher':profile([lp[57]]),'plain':profile([lp[57]])})
    sections=[s for s in '%'.join(lp[:56]).split('$') if len(vals(s))>100]
    out={'source_sha256':hashlib.sha256((ROOT/'liber-primus__transcription--master.txt').read_bytes()).hexdigest(),'known_pairs':checks,'solved_LP1_pooled':profile(list(GROUPS.values())),'unsolved_substantial_sections':profile(sections),'unsolved_by_section':[profile([s]) for s in sections],'scope_note':'Five established LP1 groups and two LP2 solved pages, not all solved LP1 methods. Histograms describe rune words; headings/labels included; non-runic blocks excluded.'}
    events=[]
    source=json.loads((ROOT/'solved_punctuation_results.json').read_text())
    for rec in source['records']:
        actual=hashlib.sha256((ROOT/rec['file']).read_bytes()).hexdigest();assert actual==rec['sha256']
        for ordinal,g in enumerate(rec['groups']):
            events.append({'event_id':f"{rec['file']}:red:{ordinal}",'image':rec['file'],'image_sha256':actual,'bbox':g['bbox'],'page_rune_offset':g['page_rune_offset'],'color':'red','dot_count':g['dots'],'role_interpretation':g['role'],'observation_source':'Session 20 / solved_punctuation_results.json','alignment_method':g['alignment']})
    phrase=json.loads((ROOT/'phrase_image_results.json').read_text())
    for rec in phrase['occurrences']:
        actual=hashlib.sha256((ROOT/rec['image']).read_bytes()).hexdigest();assert actual==rec['image_sha256']
        events.append({'event_id':f"{rec['image']}:phrase-nearby-red",'image':rec['image'],'image_sha256':actual,'bbox':rec['red_mark_box'],'page_rune_offset':rec['red_mark_rune_offset'],'color':'red','dot_count':rec['red_dot_count'],'role_interpretation':'near the audited repeated phrase; not an inferred cipher reset','observation_source':'Session 32 / phrase_image_results.json','alignment_method':'retained visual alignment'})
    positions=defaultdict(list)
    for e in events:positions[(e['image'],e['page_rune_offset'])].append(e['event_id'])
    duplicates=[{'image':k[0],'page_rune_offset':k[1],'event_ids':v} for k,v in positions.items() if len(v)>1]
    assert len(events)==32 and len({e['event_id'] for e in events})==32
    ledger={'scope':'32 previously audited red dot marks on 13 images, drawn from audits of 15 images. Incomplete sample, not new image transcription. A missing record means unaudited, not no mark. Raw positions/counts are separate from interpreted roles.','image_coverage':[{'image':rec['file'],'observed_event_count':len(rec['groups']),'scope':'Session 20 selected solved-page detector/visual audit'} for rec in source['records']]+[{'image':rec['image'],'observed_event_count':1,'scope':'Session 32 local phrase context only'} for rec in phrase['occurrences']],'events':events,'multiple_events_at_same_rune_offset':duplicates}
    (ROOT/'source_event_ledger.json').write_text(json.dumps(ledger,indent=2)+'\n')
    out['ledger_summary']={'events':len(events),'images_with_events':len({e['image'] for e in events}),'audited_source_images':len(ledger['image_coverage']),'duplicate_offset_groups':duplicates,'image_hashes_match_retained_audits':True}
    (ROOT/'representation_review_results.json').write_text(json.dumps(out,indent=2)+'\n')
    for key in ['solved_LP1_pooled','unsolved_substantial_sections']:print(key,out[key])
    print('page56',checks[-2])
    print('ledger',out['ledger_summary'])
if __name__=='__main__':main()
