"""Evaluate only the new engine; all reference decisions are immutable files."""
import bootstrap
from bootstrap import PRIOR
from collections import Counter
from dataclasses import replace
import gzip
import itertools
import json
import os
from pathlib import Path
import resource
import statistics
import time
from data_robust import OUT,read,sha,paths,lines,reference
from inputs import evidence
from anchor_features import AnchorBuilder
from approach_link import ApproachLink
from features import Settings
from state import Episode
from evaluate import stats
from evaluate_integrated import group_states


def thin(r):
    return {k:r[k] for k in ('sample','source_frame','seconds','displayed','reference','hit','raw','observable','episode')}


def main():
    clip=os.environ['CLIP'];cfg=read(OUT/'candidates.json');feedback=read(OUT/'feedback.json')
    assert cfg['frozen'] and sha(OUT/'design.ko.md')==cfg['design_sha256']
    candidate_hash=sha(OUT/'candidates.json');fp,bp,source=paths(clip)
    spec=read(OUT/'specs.json')[clip]
    cache=evidence(spec['cache'])/'inputs/pose.json';assert sha(cache)==spec['cache_sha256']
    dest=OUT/'download'/clip/'data';dest.mkdir(parents=True,exist_ok=True)
    output=dest/'ApproachLink_diagnostics.jsonl.gz';complete=dest/'summary.json'
    code_hashes={p.name:sha(p) for p in (OUT/'helpers/approach_link.py',OUT/'helpers/anchor_features.py')}
    if complete.exists():
        s=read(complete);assert s['candidate_sha256']==candidate_hash and s['code_sha256']==code_hashes
        assert sha(output)==s['trace_sha256'];print('REUSED',clip,flush=True);return
    assert not output.exists()
    builder=AnchorBuilder();engine=ApproachLink(cfg['config'],cfg['link']);ep=Episode(replace(Settings(),display_hold=1.0))
    holds={};current=[];baseline=[];new_red=[];lost_positive=[];removed_negative=[];new_negative=[]
    event_added=[];event_removed=[];windows={};changes=[];timings=[];missing=Counter();admissions=Counter();follow=Counter()
    began=time.monotonic();processed=0;first=None
    with gzip.open(output,'wt',encoding='utf-8',compresslevel=1) as out:
        for row,old in itertools.zip_longest(lines(fp),lines(bp)):
            assert row is not None and old is not None
            assert all(row[k]==old[k] for k in ('sample','source_frame','seconds','displayed'))
            t=row['seconds'];first=t if first is None else first;processed+=1
            before=time.perf_counter();row=builder.step(row);v=engine.step(row)
            episode=ep.step(t,v['hit'],v['quiet']);groups=group_states(row,v,t,holds)
            timings.append((time.perf_counter()-before)*1000)
            tag,ids=reference(clip,old,feedback)
            source_pairs={p['token']:p for p in row['pairs']}
            for p in v['pairs']:
                p['relation']=source_pairs[p['token']]['relation']
                p['insufficiency_reasons']=source_pairs[p['token']]['feature_rejections']
                for f in p['diagnostics']:
                    assert first<=f['input_start']<f['input_end']==t
                    if f['active_first_approach']:
                        seed=f['active_first_approach'];assert first<=seed['start_time']<seed['time']<t
                    if f['anchor_reason']!='OBSERVED':missing[f['anchor_reason']]+=1
                    admissions[f['first_approach']['status']]+=1;follow[f['follow_link']['reason']]+=1
            r=dict(sample=row['sample'],source_frame=row['source_frame'],seconds=t,displayed=row['displayed'],
                reference=tag,feedback_ids=ids,case_ids=row['case_ids'],dedupe_key=row['dedupe_key'],review_frame=row['review_frame'],
                groups=groups,episode=episode,valid_people=len(row['bodies']),rejected=row['rejected'],
                ambiguous=row['ambiguous'],pairs_over_cap=row['pairs_over_cap'],retained_absent=row['retained_absent'],**v)
            if clip=='SELF_L02':r.update(part_id='L01_P02',l01_frame=row['source_frame'],l02_frame=row['source_frame']-2542,l02_seconds=t-105.352669)
            out.write(json.dumps(r,separators=(',',':'))+'\n')
            old=dict(old,reference=tag);current.append(thin(r));baseline.append(thin(old))
            if not r['displayed']:continue
            loc=dict(source_frame=r['source_frame'],seconds=t,review_frame=r['review_frame'],reference=tag)
            if clip=='SELF_L02':loc.update(l02_frame=r['l02_frame'],l02_seconds=r['l02_seconds'])
            if r['hit'] and not old['hit']:
                new_red.append(loc)
                if tag=='negative_reference':new_negative.append(loc)
            if old['hit'] and not r['hit']:
                if tag=='positive_reference':lost_positive.append(loc)
                if tag=='negative_reference':removed_negative.append(loc)
            if episode['event'] and not old['episode']['event']:event_added.append(loc)
            if old['episode']['event'] and not episode['event']:event_removed.append(loc)
            if r['hit']!=old['hit'] or episode['event']!=old['episode']['event']:
                changes.append(dict(**loc,baseline_hit=old['hit'],candidate_hit=r['hit'],baseline_event=old['episode']['event'],
                    candidate_event=episode['event'],baseline_rules=sorted({x for p in old['pairs'] if p['hit'] for x in p['rules']}),
                    candidate_rules=sorted({x for p in r['pairs'] if p['hit'] for x in p['rules']}),
                    candidate_follow_reasons=dict(Counter(f['follow_link']['reason'] for p in r['pairs'] for f in p['diagnostics']))))
            for key in ids:
                w=windows.setdefault(key,dict(N=0,observable=0,baseline_hit_frames=[],candidate_hit_frames=[],baseline_event_frames=[],candidate_event_frames=[]))
                w['N']+=1;w['observable']+=r['observable']
                if old['hit']:w['baseline_hit_frames'].append(r['source_frame'])
                if r['hit']:w['candidate_hit_frames'].append(r['source_frame'])
                if old['episode']['event']:w['baseline_event_frames'].append(r['source_frame'])
                if episode['event']:w['candidate_event_frames'].append(r['source_frame'])
    current_stats=stats(current);baseline_stats=stats(baseline)
    assert current_stats['displayed']==len(spec['selected_samples'])==baseline_stats['displayed']
    refs=read(PRIOR/'reports/probe_robust_weak.json')
    for variant in ('R4','R5'):
        for label,counts in refs[variant]['clips'][clip]['labels'].items():
            b=baseline_stats['reference'][label]
            assert (counts['N'],counts['base_hit'],counts['base_events'])==(b['N'],b['hit'],b['events'])
    value=dict(clip=clip,status='PASS',trace=output.name,trace_sha256=sha(output),candidate_sha256=candidate_hash,
        code_sha256=code_hashes,cache_sha256=spec['cache_sha256'],feature_sha256=source['feature_sha256'],
        baseline_sha256=source['baseline_sha256'],processed=processed,displayed=current_stats['displayed'],
        baseline=baseline_stats,candidate=current_stats,R4=refs['R4']['clips'][clip],R5=refs['R5']['clips'][clip],
        newly_current=new_red,new_negative_current=new_negative,lost_positive=lost_positive,removed_negative=removed_negative,
        event_added=event_added,event_removed=event_removed,changed_observations=changes,feedback=windows,
        feature_diagnostics=dict(anchor_missing=dict(missing),first_admission=dict(admissions),follow=dict(follow)),
        initial_time=first,first_hit=current[0]['hit'],first_event=current[0]['episode']['event'],
        timing=dict(wall_seconds=time.monotonic()-began,p95_ms=statistics.quantiles(timings,n=100,method='inclusive')[94],
                    max_ms=max(timings),maxrss_kib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),
        caveats=['Reference ranges are approximate, not exact GT','Cxx/L02 overlap and have different inputs/history',
                 'R4/R5 full Event timelines were not retained; only saved fields compared'])
    complete.write_text(json.dumps(value,ensure_ascii=False,indent=2))
    print('EVAL_PASS',clip,{k:(baseline_stats['reference'][k]['hit'],current_stats['reference'][k]['hit']) for k in current_stats['reference']},
          'new_negative',len(new_negative),'lost_positive',len(lost_positive),flush=True)


if __name__=='__main__':main()
