"""Portable file:// viewer: archived images, full confirmed trace, no CDN/server."""
from pathlib import Path
import json, shutil, hashlib, sys
from collections import Counter,defaultdict
import networkx as nx
ROOT=Path(__file__).resolve().parents[1];OLD=ROOT.parent/'region-action-opportunity-study-20261107';sys.path.insert(0,str(Path(__file__).parent))
from analyze import metrics,dump
ASSETS=set();EVIDENCE=set();runs=[];cases=[]
FOCUS={'odkcollect-Rep1':('g16e255:1978','odkcollect.download'),'odkcollect-Rep2':('g11e101:1899','odkcollect.download'),'activitydiary-Rep1':('g28e189:231','activitydiary.edit-existing-activity'),'activitydiary-Rep2':('g27e277:2208','activitydiary.add-activity'),'omninotes-Rep1':('g14e571:1005',None),'omninotes-Rep2':('g10e438:1243',None),'scarletnotes-Rep1':(None,'scarletnotes.create-note-empty-state'),'scarletnotes-Rep2':('g66e336:2270','scarletnotes.create-note-empty-state'),'wikipedia-Rep1':(None,'wikipedia.global-search'),'wikipedia-Rep2':(None,'wikipedia.global-search')}
for rp in sorted((ROOT/'analysis/runs').glob('*.json')):
 r=json.loads(rp.read_text());rid=r['summary']['run'];es=[json.loads(l) for l in (ROOT/f'analysis/traces/{rid}.jsonl').open()];s=(OLD/f'visuals/data/{rid}.js').read_text();old=json.loads(s[s.index('(')+1:s.rfind(')')]);oldes=old['events'];stateimages={}
 for ev in oldes.values():
  for which,st in [('source',ev['state']),('target',ev['target_state'])]:
   img=ev.get(which+'_image')
   if img:
    ASSETS.add(img['asset']);stateimages.setdefault(st,img)
 for i,e in enumerate(es):
  olde=oldes.get(e['transition_id'],{})
  for which,st in [('source',e['state']),('target',e['target_state'])]:
   img=olde.get(which+'_image')
   if not img and st in stateimages:img={**stateimages[st],'same_GUI':False,'alignment':'unverified_state_representative','note':'同State另一历史图；未认证当次对齐，仅虚线bounds投影'}
   if img:e[which+'_image']=img;ASSETS.add(img['asset'])
  if olde.get('trace_url'):
   e['raw_trace_url']=olde['trace_url'];e['raw_trace_line']=olde['trace_line'];EVIDENCE.add(olde['trace_url'])
  for o in olde.get('gui_observations',[]):
   if o.get('url'):EVIDENCE.add(o['url'])
  e['gui_observations']=[{k:o.get(k) for k in ['gui_id','timestamp','available_ns','url']} for o in olde.get('gui_observations',[])]
  # Keep full trace/evidence in analysis; viewer compact but action and native snapshot preserved.
  for k in ['identity','raw_event']:e.pop(k,None)
 g=nx.DiGraph();g.add_nodes_from(r['states']);g.add_weighted_edges_from((e['source'],e['target'],e['weight']**.5) for e in r['edges'])
 xy=nx.spring_layout(g,seed=11,iterations=100,weight='weight');r['positions']={s:[round(float(v),6) for v in pos] for s,pos in xy.items()}
 for region in r['regions']:
  region['metrics']['activity_counts']=dict(Counter(e['source_activity'] or 'UNKNOWN' for e in es if e['state'] in set(region['members'])))
 # Prefix memberships truly frozen; display their full future temporal projection explicitly as holdout.
 prefix_regions=[]
 for f in [.25,.5,.75]:
  count=Counter()
  for p in r['prefix']:
   if p['prefix']!=f:continue
   method=p['method'];count[method]+=1
   prefix_regions.append(dict(id=f'p{int(f*100)}-{method}-{count[method]:03d}',method=method,members=p['members'],header=p.get('header'),function=p.get('function'),source_activity=p.get('source_activity'),prefix=f,cutoff_order=p['cutoff_order'],metrics=metrics(es,p['members'],True),training=p['training'],holdout=p['holdout'],entry_associations=[]))
 
 for rr in r['regions']+prefix_regions:
  for kk in ['episodes','top_paths','entry_edges','exit_edges']:rr['metrics'].pop(kk,None)
 r['prefix_regions']=prefix_regions;r.pop('prefix',None);r['events']=es;r['state_images']=stateimages;r['summary']['source_images']=sum(bool(e.get('source_image')) for e in es)
 focus,func=FOCUS[rid]
 if focus and focus not in {e['transition_id'] for e in es}:focus=None
 if focus is None:
  ee=[e for e in es if e.get('label',{}).get('function')==func];focus=(ee[-max(1,len(ee)//4)]['transition_id'] if ee else es[0]['transition_id'])
 f_event=next(e for e in es if e['transition_id']==focus);rr=next((z for z in r['regions'] if z['method']=='Infomap-G1' and f_event['target_state'] in z['members']),None)
 case=dict(id=rid,run=rid,title={'odkcollect':'Download：局部与公共导航','activitydiary':'Add/Edit：共享编辑与结果分歧','omninotes':'Snooze / Settings：组合操作','scarletnotes':'CreateNote：动态内容与反复新建','wikipedia':'Search：持续新增与切分'}[r['summary']['app']],focus_event=focus,focus_order=f_event['order'],region=rr['id'] if rr else None,function=func)
 r['case']=case;cases.append(case)
 (ROOT/f'visuals/data/{rid}.js').write_text('FLOW.register('+json.dumps(r,ensure_ascii=False,separators=(',',':'))+');\n')
 runs.append({**r['summary'],'case':case})
 print('VIEW',rid,'events',len(es),'images',r['summary']['source_images'],flush=True)
for rel in sorted(ASSETS|EVIDENCE):
 src=OLD/'visuals'/rel;dst=ROOT/'visuals'/rel;dst.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(src,dst)
oldindex=json.loads((OLD/'evidence/image-index.json').read_text());imageindex={Path(rel).name:oldindex[Path(rel).name] for rel in ASSETS}
dump('evidence/image-index.json',imageindex);dump('analysis/cases.json',cases)
(ROOT/'visuals/data/index.js').write_text('FLOW.index='+json.dumps(dict(runs=runs,cases=cases),ensure_ascii=False,separators=(',',':'))+';\n')
dump('evidence/viewer-assets.json',dict(images=len(ASSETS),evidence_files=len(EVIDENCE),files=[dict(path=p,sha256=hashlib.sha256((ROOT/'visuals'/p).read_bytes()).hexdigest(),source='study6/visuals/'+p) for p in sorted(ASSETS|EVIDENCE)]))
