"""Session 59 follow-up: test a merged-dot explanation and retain reset scenarios.
This diagnostic was specified after the full six-page dot inventory.
The reference glyph and coordinates were selected from original pixels.
"""
from pathlib import Path
from collections import Counter
import json
import numpy as np
from PIL import Image,ImageDraw,ImageFont
from image_first_audit import read_master,R,sha
ROOT=Path(__file__).resolve().parent

def main():
 inv=json.loads((ROOT/'separator_pixel_results.json').read_text());im=Image.open(ROOT/'63.jpg').convert('RGB');a=np.asarray(im)
 # Target A before gap96 and another A at rune offset106 on the same line.
 target=[1060,1426,1095,1540];reference=[1538,1426,1573,1540]
 event=next(e for r in inv['records']if r['page']==46 for e in r['events']if e['page_rune_gap']==96)
 dots=sorted(event['dots'],key=lambda d:d[1]);top,right,bottom=dots
 cx=(top[0]+top[2]+bottom[0]+bottom[2])/4
 slot=[int(2*cx-right[2]),right[1],int(2*cx-right[0]),right[3]]
 crop=lambda m,b:m[b[1]:b[3],b[0]:b[2]]
 checks=[]
 for th in [80,100,120]:
  m=a.max(axis=2)<th;t=crop(m,target);r=crop(m,reference);s=crop(m,slot)
  checks.append({'threshold':th,'target_ink_pixels':int(t.sum()),'reference_ink_pixels':int(r.sum()),'binary_difference_pixels':int(np.count_nonzero(t!=r)),'expected_fourth_slot_ink_pixels':int(s.sum()),'expected_fourth_slot_white_pixels':int((~s).sum()),'expected_fourth_slot_total_pixels':int(s.size)})
 geometry={'source':'63.jpg','page':46,'line':5,'target_rune_offset':95,'reference_rune_offset':106,'target_rune_bbox':target,'reference_rune_bbox':reference,'expected_fourth_dot_bbox':slot,'slot_method':'Reflect the observed right-hand square through the shared vertical centerline of the upper and lower squares. This assumes the same diamond geometry as the observed four-dot marks.','checks':checks,'conclusion':'No added ink relative to the comparison A. The predicted fourth-square slot contains 71 white pixels out of 100 at each threshold; a full square at that location is absent.','limit':'Rules out the simple expected-position merged-square explanation, not every hypothetical hidden or nonstandard drawing. Does not establish the three-dot mark\'s cipher role.'}
 assert all(c['binary_difference_pixels']==0 and c['expected_fourth_slot_white_pixels']==71 for c in checks)
 # Source ledger merges only gap identity, not semantic meaning.
 gaps=[]
 for rec in inv['records']:
  by={}
  for e in rec['events']:by.setdefault(e['page_rune_gap'],[]).append(e)
  for off,es in sorted(by.items()):
   counts=[e['dot_count']for e in es]
   kind='split_three_plus_one'if counts==[3,1]else{1:'single_dot',3:'three_dot_inline',4:'four_dot'}[counts[0]]
   gaps.append({'page':rec['page'],'rune_gap':off,'physical_class':kind,'physical_event_ids':[e['event_id']for e in es],'visible_dot_counts':counts,'source_boxes':[e['bbox']for e in es],'master_marks':es[0]['master_marks_at_gap'],'role_status':'unresolved'if kind in ('split_three_plus_one','three_dot_inline')else'physical_class_only; cipher reset role unproven'})
 ledger={'scope':'Six audited LP2 pages; source offsets inherit Session 58 reviewed rune sequences.','inventory_sha256':sha(ROOT/'separator_pixel_results.json'),'master_is_unchanged':True,'gaps':gaps,'class_counts':dict(Counter(g['physical_class']for g in gaps))}
 (ROOT/'separator_event_ledger.json').write_text(json.dumps(ledger,indent=2)+'\n')
 raw=(ROOT/'liber-primus__transcription--master.txt').read_text();lp=raw[raw.index('ᚱ-'):].split('%')[:73][15:]
 base={r['page']:sorted(e['offset']for e in read_master(lp[r['page']])['events']if e['mark']==':')for r in inv['records']}
 physical={r['page']:sorted(g['rune_gap']for g in gaps if g['page']==r['page']and g['physical_class']=='four_dot')for r in inv['records']}
 assert base==physical
 def counters(n,resets):
  last=0;v=[]
  for k in range(n):
   if k in resets:last=k
   v.append(k-last)
  return np.asarray(v)
 scenarios=[]
 for label,extra in [('literal_four_dot_only',[]),('also_reset_at_split_mark',[(11,250)]),('also_reset_at_inline_three_dot',[(46,96)]),('also_reset_at_both',[(11,250),(46,96)])]:
  changes=[]
  for r in inv['records']:
   p=r['page'];n=r['runes'];b=counters(n,base[p]);v=counters(n,base[p]+[off for pg,off in extra if pg==p]);ix=np.flatnonzero(b!=v)
   if len(ix):changes.append({'page':p,'first_changed_rune':int(ix[0]),'last_changed_rune':int(ix[-1]),'changed_runes':len(ix),'counter_change_values':list(map(int,np.unique(v[ix]-b[ix])))})
  scenarios.append({'name':label,'added_resets':extra,'total_resets':sum(map(len,base.values()))+len(extra),'changed_counter_positions':sum(x['changed_runes']for x in changes),'changes':changes})
 result={'inventory_sha256':sha(ROOT/'separator_pixel_results.json'),'merged_dot_diagnostic':geometry,'gap_class_counts':ledger['class_counts'],'reset_reference':'Counter starts at zero for each sampled page solely to compare local index schedules. Reset before the rune at each gap. This is not evidence that real keys restart per page or at any punctuation.','literal_master_periods_match_twenty_visible_four_dot_groups':True,'reset_scenarios':scenarios,'no_cipher_search_or_plaintext_ranking':True}
 (ROOT/'separator_diagnostics.json').write_text(json.dumps(result,indent=2)+'\n')
 # Annotated source pixels; no synthesis or inferred reconstruction.
 f=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',19);small=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',16)
 out=Image.new('RGB',(1000,575),'#eef1f5');d=ImageDraw.Draw(out)
 d.text((24,18),'Page 46, line 5: testing the missing-square explanation',font=f,fill='#162432')
 d.text((24,58),'A before gap 96; blue box = expected fourth square',font=small,fill='#162432')
 box=[1045,1416,1168,1550];c=im.crop(tuple(box)).resize((369,402),Image.Resampling.NEAREST);out.paste(c,(66,90));sb=[66+(slot[0]-box[0])*3,90+(slot[1]-box[1])*3,66+(slot[2]-box[0])*3,90+(slot[3]-box[1])*3];d.rectangle(sb,outline='#216bb0',width=3)
 d.text((530,58),'Comparison A on the same line, rune 106',font=small,fill='#162432')
 c=im.crop(tuple(reference)).resize((105,342),Image.Resampling.NEAREST);out.paste(c,(660,112));d.text((530,470),'Both masks: 1,638 ink pixels; zero differences',font=small,fill='#162432')
 d.text((24,525),'Expected square: 29 black pixels, 71 white. A full square is absent at this position.',font=small,fill='#162432');d.text((24,550),'Binary comparisons agree at thresholds 80, 100 and 120. Separator meaning remains unknown.',font=small,fill='#162432');out.save(ROOT/'separator_overlap_diagnostic.png')
 print(json.dumps({'geometry':geometry,'gap_classes':ledger['class_counts'],'scenarios':scenarios},indent=2))
if __name__=='__main__':main()
