"""Portable static viewer; file:// works without fetch, CDN, services or GPU."""
import base64
import io
import shutil
from PIL import Image,ImageDraw
from analyze import *

def main():
    es=lines(ROOT/'analysis/events.jsonl');ocs=read(ROOT/'analysis/entry-occurrences.json');cases=read(ROOT/'analysis/cases.json')
    cps=read(ROOT/'analysis/temporal-checkpoints.json');qs=read(ROOT/'analysis/archive-inventory-coverage.json')
    gr=read(ROOT/'analysis/GUI-flag-observations.json');sharingrows=read(ROOT/'analysis/cross-entry-transfer.json')
    (ROOT/'visuals/data').mkdir(exist_ok=True)
    assets={};cache={};contacts=[];proofs=[];runstats=[]
    def pic(p):
        p=Path(p)
        if str(p) in cache:return cache[str(p)]
        b=p.read_bytes();h=hashlib.sha256(b).hexdigest()
        with Image.open(io.BytesIO(b)) as im:
            dims=list(im.size);im=im.convert('RGB');im.thumbnail((390,950));buf=io.BytesIO();im.save(buf,'JPEG',quality=77)
        out=buf.getvalue();name=h[:24]+'.jpg';(ROOT/'visuals/assets'/name).write_bytes(out)
        assets[name]=dict(original_path=str(p),original_sha256=h,original_dimensions=dims,
            copy_sha256=hashlib.sha256(out).hexdigest(),transformation='real pixels resized JPEG; no generated GUI')
        cache[str(p)]=dict(asset='assets/'+name,width=dims[0],height=dims[1]);return cache[str(p)]
    for meta in read(ROOT/'evidence/archive-manifest.json'):
        rid=meta['app']+'-'+meta['rep'];base=Path(meta['graph_path']).parent;graph=read(meta['graph_path'])
        assert READ_FILES[str(base/'jev-graph.json')]==meta['graph_sha256']
        states={s['id']:s for s in graph['states']};shots=defaultdict(list)
        for p in sorted((base/'jev-audit/screenshots').glob('*.json')):
            sh=read(p);shots[(sh.get('gui_id'),sh.get('gui_timestamp'))].append(sh)
        def photo(e,side):
            gid=e[side+'_gui_id'];ts=e[side+'_gui_timestamp']
            for sh in shots[(gid,ts)]:
                if sh.get('path') and (base/sh['path']).is_file():
                    return dict(**pic(base/sh['path']),same_GUI=True,alignment=sh.get('alignment','unverified'),
                        capture_ns=sh.get('capture_finished_ns'),capture_phase=sh.get('capture_phase'),archive_path=sh['path'],
                        note='同GUI ID/timestamp的审计关联；不是原子Tree+Bitmap，不认证当次控件框对齐')
            sid=e['state'] if side=='source' else e['target_state']
            options=[base/p for p in states.get(sid,{}).get('screenshot_paths',[])]+sorted((base/'ape-state-screenshots').glob(sid+'-*.png'))
            p=next((p for p in options if p.is_file()),None)
            return dict(**pic(p),same_GUI=False,alignment='unverified_state_representative',archive_path=str(p.relative_to(base)),
                note='同State历史代表图；不是当次执行截图，虚线框只作bounds投影') if p else dict(missing=True,note='归档缺图；没有生成替代图片')
        ee=[e for e in es if e['app']+'-'+e['rep']==rid];oo=[o for o in ocs if o['run']==rid];cc=[c for c in cases if c['run']==rid]
        used={t for o in oo for s in o['segments'].values() for t in s['ids']}|{c['focus_event'] for c in cc}
        for c in cps:
            if c['run']==rid:used.update(c['future_validation']['discovery_events'])
        gg=[g for g in gr if g['run']==rid];guiidx=defaultdict(list);guiportable={}
        for g in gg:
            doc=read(g['path']);h=READ_FILES[g['path']];name='gui-'+h[:24]+'.json';dest=ROOT/'visuals/evidence'/name
            # Original bytes copied without alteration, corresponding original hash retained.
            dest.write_bytes(Path(g['path']).read_bytes());guiportable[name]=dict(original_path=g['path'],original_sha256=h)
            guiidx[(g['gui_id'],g['timestamp'])].append(dict(gui_id=g['gui_id'],timestamp=g['timestamp'],available_ns=g['available_ns'],
                state=g['state'],version=g['version'],url='evidence/'+name,candidates=g['candidates'],
                status='PARTIAL: retained nodes complete, Native inventory/validity incomplete'))
        events={};portable=[];history=[]
        fields=['transition_id','order','monotonic_ns','input_ns','input_step','wall_time_ms','state','target_state','version','input_version',
            'input_model_action','model_action','source_activity','target_activity','source_gui_id','source_gui_timestamp','target_gui_id','target_gui_timestamp',
            'action_label','action_type','resource_id','text','content_desc','class','bounds','node_path','identity_digest','label',
            'planned_input_text','new_state','first_model_transition','first_ID_only_model_transition','first_local_concrete','first_activity_concrete',
            'concrete_key','activity_concrete_key','source_key','target_key','action_class','inventory_level','event_line','target_activity_evidence']
        for e in ee:
            # Full past-only history lets the viewer change the selected time without final-graph action membership.
            history.append([e['monotonic_ns'],e['source_key'],e['concrete_key'],e['input_model_action'],e['action_label'],e['target_key'],e['transition_id'],e['activity_concrete_key']])
            if e['transition_id'] not in used:continue
            out={k:e.get(k) for k in fields};out['source_image']=photo(e,'source');out['target_image']=photo(e,'target')
            out['latest_selection']=e['latest_selection'];out['gui_observations']=guiidx[(e['source_gui_id'],e['source_gui_timestamp'])]
            out['event_path']=str(Path(e['event_path']).relative_to(base));out['input_path']=str(Path(e['input_path']).relative_to(base))
            out['receipt_path']=str(Path(e['receipt_path']).relative_to(base))
            inp=read(e['input_path']);rec=read(e['receipt_path'])
            out['input_sha256']=READ_FILES[e['input_path']];out['receipt_sha256']=READ_FILES[e['receipt_path']]
            portable.append(dict(run=rid,transition_id=e['transition_id'],input_path=out['input_path'],input_sha256=out['input_sha256'],
                receipt_path=out['receipt_path'],receipt_sha256=out['receipt_sha256'],event_path=out['event_path'],event_line=e['event_line'],
                raw_input=inp,raw_receipt=rec,raw_transition=e['raw_event']))
            # Bounded bundles, under Pages per-asset limit; one link resolves to exact row.
            part=(len(portable)-1)//400+1;out['trace_url']=f'evidence/{rid}-traces-{part:02}.jsonl';out['trace_line']=(len(portable)-1)%400+1
            events[e['transition_id']]=out
        for start in range(0,len(portable),400):
            name=f'{rid}-traces-{start//400+1:02}.jsonl'
            (ROOT/'visuals/evidence'/name).write_text(''.join(json.dumps(x,ensure_ascii=False,separators=(',',':'))+'\n' for x in portable[start:start+400]))
        # Compact occurrence metrics for dynamic graphs; canonical full table stays under analysis/.
        compact=[]
        for o in oo:
            compact.append({**{k:o[k] for k in ['id','run','function','entry','order','monotonic_ns','source_state','target_state']},
                'segments':{k:{**{f:s[f] for f in ['ids','stop','complete','boundary']},'metrics':{f:s['metrics'][f] for f in ['native_new_state','first_ID_only_model_transition','first_local_concrete']}} for k,s in o['segments'].items()}})
        data=dict(id=rid,app=meta['app'],rep=meta['rep'],run_id=meta['run_id'],quality=next(q for q in qs if q['run']==rid),
            occurrences=compact,events=events,history=history,checks=[c for c in cps if c['run']==rid],cases=cc,
            sharing=[s for s in sharingrows if s['run']==rid])
        safe=json.dumps(data,ensure_ascii=False,separators=(',',':')).replace('<','\\u003c')
        (ROOT/'visuals/data'/f'{rid}.js').write_text('OPPORTUNITY.runs.push('+safe+');\n')
        proofs.extend(dict(run=rid,transition_id=p['transition_id'],input_sha256=p['input_sha256'],receipt_sha256=p['receipt_sha256']) for p in portable)
        for c in cc:
            e=events[c['focus_event']];contacts.append((c,e['source_image'],e['target_image']))
        runstats.append(dict(run=rid,viewer_events=len(events),occurrences=len(compact),trace_records=len(portable),GUI_copies=len(guiportable)))
        print(rid,'viewer',len(events),'events',len(compact),'occurrences',flush=True)
    scripts='\n'.join('<script src="data/'+r['run']+'.js"></script>' for r in runstats)
    template=(ROOT/'analysis/viewer-template.html').read_text()
    (ROOT/'visuals/index.html').write_text(template.replace('__DATA_SCRIPTS__',scripts))
    write('evidence/image-index.json',assets);write('evidence/viewer-inventory.json',runstats);write('evidence/viewer-record-hashes.json',proofs)
    write('evidence/viewer-read-source-sha256.json',READ_FILES)
    for b in range(math.ceil(len(contacts)/4)):
        sheet=Image.new('RGB',(1000,860),'#eeeae0');d=ImageDraw.Draw(sheet)
        for i,(c,src,tgt) in enumerate(contacts[b*4:b*4+4]):
            x=(i%2)*500;y=(i//2)*430;d.text((x+8,y+5),c['id'],fill='#152b32')
            for j,im in enumerate([src,tgt]):
                if im.get('asset'):
                    img=Image.open(ROOT/'visuals'/im['asset']);img.thumbnail((190,390));sheet.paste(img,(x+12+235*j,y+27))
        sheet.save(ROOT/'evidence'/f'contact-review-{b+1}.jpg',quality=85)
    print('images',len(assets),'cases',len(cases),'portable records',len(proofs))

if __name__=='__main__':main()
