"""Extend the previous portable canvas player, preserving real archived pixels."""
from pathlib import Path
import argparse,json,shutil,hashlib
ROOT=Path(__file__).resolve().parents[1];A=ROOT/'analysis';V=ROOT/'visuals'
p=argparse.ArgumentParser();p.add_argument('--study',type=Path,required=True);args=p.parse_args();old=args.study/'visuals';(V/'data').mkdir(exist_ok=True)
# Copy assets/evidence to this independent worktree. Originals stay read-only.
for folder in ['assets','evidence']:
    shutil.copytree(old/folder,V/folder,dirs_exist_ok=True)
shutil.copy2(old/'style.css',V/'style.css')
index=[];summary=[]
for meta in json.loads((A/'runs.json').read_text()):
    rid=meta['run'];s=(old/'data'/(rid+'.js')).read_text();r=json.loads(s[s.index('(')+1:s.rfind(')')]);r.pop('prefix_regions',None)
    # Retain exact final Dominator baseline for an explicitly posthoc context view only.
    r['regions']=[x for x in r['regions'] if x['method']=='A-final'];r['shadow_snapshots']=[]
    directory=A/f'replay/{rid}/250';ix=json.loads((directory/'index.json').read_text())
    for item in ix['snapshots']:
        sn=json.loads((directory/(item['id']+'.json')).read_text());regs=[]
        for rr in sn['communities']:
            m=rr['metrics'];episodes=[]
            for ep in m['episodes']:
                evs=r['events'][ep['start']-1:ep['end']];episodes.append(dict(start=ep['start']-1,end=ep['end']-1,segment=ep['segment'],left_censored=ep['left_censored'],right_censored=ep['right_censored'],source_steps=ep['steps'],new_state=sum(e['first_observed_target_raw'] for e in evs),new_transition=sum(e['first_model_transition_flow'] for e in evs)))
            metrics={**m,'observed_members':m['size'],'boundary_flow':m['boundary_entries']+m['boundary_exits'],'confirmed_entries':m['boundary_entries'],'confirmed_exits':m['boundary_exits'],'confirmed_reentries_within_segment':m['reentries'],'left_censored':sum(p['left_censored'] for p in episodes),'right_censored':sum(p['right_censored'] for p in episodes),'native_snapshot_count':m['progress']['native_progress_records'],'native_saturated_snapshot_count':m['progress']['native_state_saturated'],'native_unvisited_positive':m['progress']['native_unvisited_positive'],'episodes':episodes,'late':m['recent_progress'],'all':m['progress'],'entry_edges':[],'exit_edges':[]}
            associations=[]
            for a in rr['entry_associations']:
                associations.append(dict(function=' / '.join(a['functions']),entry_targets_in_region=a['direct_target_count'],executions=sum(o['group_id']==a['group_id'] for o in sn['entry_occurrences']),next5_in_region=a['trajectory_distinct_steps'].get('5',0),next5_source_steps=len({seq for w in sn['trajectory_windows'] if w['group_id']==a['group_id'] and w['window']==5 for seq in w['events']})))
            regs.append({**rr,'method':'Shadow','metrics':metrics,'entry_associations':associations})
        r['shadow_snapshots'].append({**{k:v for k,v in sn.items() if k in ['id','max_event_seq','confirmed_count','lineage_links','lineage_events','entry_occurrences','trajectory_windows','infomap_ms','reasons']},'regions':regs})
    (V/'data'/(rid+'.js')).write_text('FLOW.register('+json.dumps(r,ensure_ascii=False,separators=(',',':'))+');\n')
    index.append(r['summary']);summary.append(dict(run=rid,snapshots=len(ix['snapshots']),events=len(r['events'])))
(V/'data/index.js').write_text('FLOW.index='+json.dumps(dict(runs=index),ensure_ascii=False)+';\n')
h=(old/'index.html').read_text().replace('探索流量与时间','Region Shadow · 社区演化').replace('哪些状态群，在真实探索中反复亮起？','被动事件 → 历史快照 → 功能入口与预算证据')
a=h.index('<label>区域算法');b=h.index('<label>当前区域',a)
h=h[:a]+'''<label>视图<select id="algorithm"><option value="Shadow">Infomap Shadow · 250步</option><option value="A-final">Dominator · 事后收尾对照</option></select></label><label>快照方式<select id="snapmode"><option value="auto">随执行游标 · 只用当时快照</option><option value="frozen">冻结快照 → 之后留出事实</option></select></label><label>Community Snapshot<select id="snapshot"></select></label><span hidden><select id="variant"><option value="G1">G1</option></select><select id="prefix"><option value="full">full</option></select></span>'''+h[b:]
h=h.replace('<div id="status"','<p class="notice">入口标签全部为 ORACLE REPLAY：既有探索性审核，非人工金标，也非当时已在线识别。自动模式只使用游标之前的 Snapshot；冻结模式将后续事实明确标为留出。布局与历史代表图不参与前缀计算。</p><div id="status"')
h=h.replace('<section class="bottom">','<section class="bottom"><article><h2>Snapshot 差异与 Lineage</h2><div id="lineage"></div><h3>Ⅰ类入口发生</h3><select id="entry"></select><div id="entryfacts"></div></article><article><h2>社区身份时间轴</h2><canvas id="lineagecanvas" style="width:100%;height:240px"></canvas><p>每列一个真实快照；颜色只在有明确连续关系时沿用。点击列跳至其事件时刻；split/merge 不强行延续身份。</p><div id="lineagehistory"></div></article></section><section class="bottom">')
h=h.replace('最后20%','最近100步窗口').replace('不修改 Native / 不启用 SUPPRESS','独立 Region Worktree · Native被动采集 · 不启用 SUPPRESS').replace('../analysis/run-summary.csv','../analysis/performance-summary.csv')
(V/'index.html').write_text(h)
js=(old/'app.js').read_text()
js=js.replace('let run=null,index=0,','let shadowSnapshot=null;\nlet run=null,index=0,')
a=js.index("$('allruns').innerHTML=");b=js.index("$('allruns').addEventListener",a)
js=js[:a]+"$('allruns').innerHTML='<table><tr><th>Run</th><th>确认执行</th><th>历史State</th></tr>'+FLOW.index.runs.map(r=>`<tr><td><button data-run=\"${esc(r.run)}\">${esc(r.run)}</button></td><td>${r.executions}</td><td>${r.states}</td></tr>`).join('')+'</table>';\n"+js[b:]
js=js.replace("function activate(r){run=r;","function activate(r){run=r;shadowSnapshot=null;$('snapshot').innerHTML=r.shadow_snapshots.map(s=>`<option value=\"${s.id}\">${s.id} · #${s.max_event_seq} · ${s.reasons.join('/')}</option>`).join('');")
a=js.index('function selectAlgorithm()');b=js.index('function canvas(',a)
js=js[:a]+'''function selectAlgorithm(){if(!run)return;inspect=null;const alg=$('algorithm').value;
if(alg==='Shadow'){
  if($('snapmode').value==='auto')shadowSnapshot=run.shadow_snapshots.filter(s=>s.max_event_seq<=index+1).at(-1)||null;
  else shadowSnapshot=run.shadow_snapshots.find(s=>s.id===$('snapshot').value)||run.shadow_snapshots[0];
  if(shadowSnapshot)$('snapshot').value=shadowSnapshot.id;
  rs=shadowSnapshot?shadowSnapshot.regions:[];
}else{shadowSnapshot=null;rs=run.regions;}
rs=[...rs].sort((a,b)=>b.metrics.source_steps-a.metrics.source_steps);$('region').innerHTML=rs.map(r=>`<option value="${esc(r.id)}">${r.lineage_id||r.id} · ${r.metrics.size} State / ${r.metrics.source_steps}步</option>`).join('');
const focus=run.events[index];const best=rs.find(r=>r.members.includes(focus.target_state))||rs[0];if(best)$('region').value=best.id;
buildGraph();selectRegion();shadowDetails();}
function buildGraph(){const cut=$('algorithm').value==='Shadow'?(shadowSnapshot?.max_event_seq||0):run.events.length;const nodes=new Set(),edges=new Map();
for(const e of run.events.slice(0,cut)){nodes.add(e.state);nodes.add(e.target_state);const k=e.state+'|'+e.target_state;edges.set(k,(edges.get(k)||0)+1)}
viewNodes=[...nodes];viewEdges=[...edges].map(([k,w])=>({u:k.split('|')[0],v:k.split('|')[1],w}));$('graphnote').textContent=$('algorithm').value==='Shadow'?`快照截止 #${cut}；游标 #${index+1}；不含未来边`:'事后最终Dominator，仅作结构对照';}
function selectRegion(){selected=rs.find(r=>r.id===$('region').value)||null;regionDetails();shadowDetails();render();}
function shadowDetails(){const sn=shadowSnapshot;
if(!sn){$('lineage').textContent='游标之前尚无快照（首次250步），不以最终图补齐。';$('entry').innerHTML='';$('entryfacts').textContent='';$('lineagehistory').textContent='';const cv=$('lineagecanvas');cv.getContext('2d').clearRect(0,0,cv.width,cv.height);return}
const r=selected,links=sn.lineage_links.filter(l=>l.current===r?.id),changes=sn.lineage_events.filter(l=>l.current.includes(r?.id));
$('lineage').innerHTML=`<b>${sn.id} / 截止 #${sn.max_event_seq} / Infomap ${sn.infomap_ms.toFixed(2)}ms</b><p>${index+1>sn.max_event_seq?'游标已在快照之后：新增证据尚待更新；冻结区域用于留出审查。':index+1<sn.max_event_seq?'手动浏览该快照；其形成晚于当前游标，不能用于当时判断。':'当时可用证据截止。'}</p><p>${esc(r?.lineage_id)} · ${esc(r?.lineage_status)}</p><pre>${esc(JSON.stringify({delta:r?.member_delta,links,previous_members:links.map(l=>({id:l.previous,members:run.shadow_snapshots.flatMap(s=>s.regions).find(z=>z.id===l.previous)?.members})),changes,navigation_candidates:r?.navigation_candidate_states,mixed_functions:r?.functions},null,2))}</pre>`;
$('entry').innerHTML='<option value="">选择入口发生（仅快照之前）</option>'+sn.entry_occurrences.map(o=>`<option value="${o.event_seq}">#${o.event_seq} ${esc(o.function)} · ${esc(o.source_state)} → ${esc(o.target_state)}</option>`).join('');
const mh=run.shadow_snapshots.map(s=>{const rr=s.regions.find(z=>z.lineage_id===r?.lineage_id);return {at:s.max_event_seq,regions:s.regions.length,related:rr?.lineage_id}});$('lineagehistory').innerHTML='<pre>'+esc(JSON.stringify(mh,null,2))+'</pre>';
const [c,w,h]=canvas('lineagecanvas',240),ss=run.shadow_snapshots,bw=w/Math.max(ss.length,1);ss.forEach((s,i)=>{const rr=s.regions.filter(x=>x.members.length>1).slice(0,14),hh=190/Math.max(1,rr.length);rr.forEach((z,j)=>{c.fillStyle=COLORS[+z.lineage_id.slice(1)%COLORS.length];c.fillRect(i*bw+1,18+j*hh,bw-2,Math.max(2,hh-2));});c.fillStyle='#17333e';c.font='9px monospace';c.fillText(s.max_event_seq,i*bw+1,225);if(s.id===sn.id){c.strokeStyle='#c07725';c.lineWidth=3;c.strokeRect(i*bw,0,bw,235)}});
}
''' +js[b:]
# No future-derived episode recomputation: every shadow metric/episode is saved at snapshot cutoff.
js=js.replace("'最后20%步 / S+含跨出 / T+'","'最近100步 / S+含跨出 / T+'")
js=js.replace("function seek(i){index=Math.max(0,Math.min(run.events.length-1,Math.round(i)));inspect=null;render()}","function seek(i){index=Math.max(0,Math.min(run.events.length-1,Math.round(i)));inspect=null;if($('algorithm').value==='Shadow'&&$('snapmode').value==='auto'){const s=run.shadow_snapshots.filter(s=>s.max_event_seq<=index+1).at(-1)||null;if(s?.id!==shadowSnapshot?.id){selectAlgorithm();return}}render()}")
js=js.replace("$('run').onchange=loadRun;['algorithm','variant','prefix']", "$('snapmode').onchange=selectAlgorithm;$('snapshot').onchange=()=>{$('snapmode').value='frozen';selectAlgorithm()};$('entry').onchange=()=>{const seq=+$('entry').value,o=shadowSnapshot?.entry_occurrences.find(o=>o.event_seq===seq);if(!o)return;const ws=shadowSnapshot.trajectory_windows.filter(w=>w.entry_event_seq===seq);$('entryfacts').innerHTML='<p>ORACLE REPLAY · 冻结的发生时关联与当前快照关联分别保留</p><pre>'+esc(JSON.stringify({occurrence:o,observed_windows:ws},null,2))+'</pre>';seek(seq-1)};$('lineagecanvas').onclick=e=>{const r=$('lineagecanvas').getBoundingClientRect(),i=Math.floor((e.clientX-r.left)/r.width*run.shadow_snapshots.length),s=run.shadow_snapshots[i];if(s){$('snapshot').value=s.id;$('snapmode').value='frozen';seek(s.max_event_seq-1);selectAlgorithm()}};\n$('run').onchange=loadRun;['algorithm','variant','prefix']")
js=js.replace("'当前为全程事后描述。切换25% / 50% / 75%前缀，查看当时区域与之后真实事实。'", "(shadowSnapshot?'指标只用截至 #'+shadowSnapshot.max_event_seq+' 的历史；之后活动需通过游标留出审查。':'当前为最终结构对照。')")
js=js.replace("const pre=$('prefix').value,cut=pre==='full'?null:Math.floor(run.events.length*+pre);", "const pre=$('prefix').value,cut=shadowSnapshot?shadowSnapshot.max_event_seq:(pre==='full'?null:Math.floor(run.events.length*+pre));")
(V/'app.js').write_text(js)
manifest=[dict(path=str(p.relative_to(V)),bytes=p.stat().st_size,sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in V.rglob('*') if p.is_file()]
(A/'viewer-assets.json').write_text(json.dumps(manifest,ensure_ascii=False));(A/'viewer-build.json').write_text(json.dumps(dict(runs=summary,files=len(manifest),max_bytes=max(p['bytes'] for p in manifest),offline=True,source=str(args.study),image_alignment='inherited; same_GUI false stays unverified; no invented screenshots'),ensure_ascii=False,indent=2));print('VIEWER',len(manifest),max(p['bytes'] for p in manifest))
