"""Technical and reference-interval audit, not a semantic accuracy evaluation."""
from collections import Counter
import csv
import gzip
import hashlib
import json
from pathlib import Path
import shutil
from inputs import OUT,load


def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest()
def read_lines(path):return [json.loads(line) for line in path.read_text().splitlines()]
def write_csv(path,rows):
    with path.open('w',newline='',encoding='utf-8-sig') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)


def main():
    specs=json.loads((OUT/'specs.json').read_text());manifest=json.loads((OUT/'candidates.json').read_text())
    dest=OUT/'download';validation=dest/'validation';validation.mkdir(exist_ok=True)
    contract=json.loads((OUT/'execution_contract.json').read_text());develop=set(contract['development'])
    summaries={};traces={};table=[];reference_table=[];checks=Counter();times=[];case_rows=[];limitations={}
    for clip in sorted(specs):
        rows,chosen,spec,cache,maps=load(clip)
        views=read_lines(OUT/'work'/clip/'features.jsonl');fs=json.loads((OUT/'work'/clip/'features_summary.json').read_text())
        checks['cache_hashes']+=1
        assert sha(OUT/'work'/clip/'features.jsonl')==fs['sha256']
        assert [(r['sample'],r['source_frame'],r['seconds']) for r in rows]==[(r['sample'],r['source_frame'],r['seconds']) for r in views]
        assert set(r['sample'] for r in views if r['displayed'])==chosen
        assert clip!='L01' and not clip.startswith('L01_')
        reasons=Counter();caps=Counter()
        for v in views:
            if v['displayed']:
                caps['pair_candidates_over_cap']+=v['pairs_over_cap']
                caps['ambiguous_links']+=v['ambiguous']
                for rejected in v.get('rejected',[]):reasons[rejected['reason']]+=1
            for b in v['bodies']:
                assert b['score']>=.25
                assert all(j[2]>=.1 for j in b['joints'].values())
            for p in v['pairs']:
                if v['displayed']:
                    for rejected in p['feature_rejections']:reasons[rejected['reason']]+=1
                assert len(p['orders'])==len(set(p['orders']))==2
                assert p['hold_age']<=1+1e-8
                assert not p['reset'] or not p['features']
                for f in p['features']:
                    assert f['input_start']<f['input_end']==v['seconds']
                    assert .055-1e-8<=f['elapsed']<=.3+1e-8 and f['confidence']>=.1
                    checks['causal_features']+=1
        limitations[clip]=dict(feature_rejections=dict(reasons),candidate_limits=dict(caps),
            warning='Counts are technical evidence shortages, not semantic causes or detection accuracy.')
        info=json.loads((dest/clip/'data/summary.json').read_text());summaries[clip]=info
        assert info['candidate_manifest_sha256']==sha(OUT/'candidates.json')
        traces[clip]={};times.append(fs)
        for c in manifest['candidates']:
            cid=c['id'];s=info['candidates'][cid];path=dest/clip/'data'/s['trace'];trace=read_lines(path)
            assert sha(path)==s['sha256']
            assert [(r['sample'],r['source_frame'],r['seconds']) for r in trace]==[(r['sample'],r['source_frame'],r['seconds']) for r in views]
            shown=[r for r in trace if r['displayed']];traces[clip][cid]=shown
            assert len(shown)==len(chosen)
            last_event=None
            for r,v in zip(trace,views):
                assert r['hit']==any(p['hit'] for p in r['pairs'])
                assert not r['hit'] or r['raw'] and r['observable']
                assert not r['episode']['event'] or r['hit']
                if r['episode']['event']:
                    assert last_event is None or r['seconds']-last_event>=2-1e-8
                    last_event=r['seconds']
                for p in r['pairs']:
                    assert not p['hit'] or p['passing'] and p['actors']
                    assert all(p['counts'][i]>=c['config']['min_samples'] for i in p['actors'])
                    assert all(f['input_end']==r['seconds'] and f['input_start']<r['seconds'] for f in p['passing'])
            for label,rs in s['reference'].items():
                values=[r for r in shown if r['reference']==label]
                duration=sum(b['seconds']-a['seconds'] for a,b in zip(shown,shown[1:])
                    if a['reference']==b['reference']==label and a['hit'] and b['hit'] and 0<b['seconds']-a['seconds']<=.15+1e-8)
                reference_table.append(dict(clip=clip,candidate=cid,reference=label,**rs,
                    condition_percent=100*rs['hit']/rs['N'] if rs['N'] else None,
                    observable_percent=100*rs['observable']/rs['N'] if rs['N'] else None,
                    observed_condition_interval_seconds=duration))
                assert rs['N']==len(values) and rs['hit']==sum(r['hit'] for r in values)
            table.append(dict(clip=clip,candidate=cid,N=len(chosen),candidate_frames=fs['candidate_frames'],
                observable=fs['observable_frames'],condition=s['condition_frames'],display=s['display_frames'],
                events=len(s['test_events']),baseline_condition=None if not info['baseline'] else info['baseline']['condition_frames'],
                baseline_events=None if not info['baseline'] else info['baseline']['test_events'],
                positive_gap_seconds=s['positive_longest_observed_no_hit_span_seconds'],
                feature_p95_ms=fs['feature_p95_ms'],decision_p95_ms=s['decision_p95_ms']))
            for r in shown:
                for case in r['case_ids']:
                    case_rows.append(dict(clip=clip,candidate=cid,case_id=case,sample=r['sample'],review_frame=r['review_frame'],
                        source_frame=r['source_frame'],seconds=r['seconds'],reference=r['reference'],hit=r['hit'],event=r['episode']['event']))
            checks['trace_alignment']+=1
        # Package new derived features, not RGB or original Pose cache.
        packed=dest/clip/'data/features.jsonl.gz'
        if not packed.exists():
            with packed.open('wb') as out,gzip.GzipFile(fileobj=out,mode='wb',mtime=0) as z:z.write((OUT/'work'/clip/'features.jsonl').read_bytes())
        with gzip.open(packed,'rb') as z:assert hashlib.sha256(z.read()).hexdigest()==fs['sha256']
        shutil.copy2(OUT/'work'/clip/'features_summary.json',dest/clip/'data/features_summary.json')
        mapping=[dict(sample=v['sample'],source_frame=v['source_frame'],seconds=v['seconds'],displayed=v['displayed'],
                      review_frame=v['review_frame'],reference=v['reference'],dedupe_key=v['dedupe_key']) for v in views]
        write_csv(dest/clip/'data/observations.csv',mapping)
    aggregation={};search_check={}
    for c in manifest['candidates']:
        cid=c['id'];all_seen={};dev_seen={};disagreement=0
        for clip in sorted(traces):
            for r in traces[clip][cid]:
                key=r['dedupe_key']
                if key in all_seen and all_seen[key]['hit']!=r['hit']:disagreement+=1
                all_seen.setdefault(key,r)
                if clip in develop:dev_seen.setdefault(key,(clip,r))
        totals={}
        for label in ('positive_reference','negative_reference','unknown','conflict_or_pending'):
            values=[r for r in all_seen.values() if r['reference']==label]
            totals[label]=dict(N=len(values),observable=sum(r['observable'] for r in values),hit=sum(r['hit'] for r in values))
        aggregation[cid]=dict(reference=totals,deduplication='lexical clip order, same self review frame once',
            overlap_hit_disagreements=disagreement,raw_displayed=sum(len(v[cid]) for v in traces.values()),unique_displayed=len(all_seen))
        search=json.loads((OUT/'reports'/c['search_report']).read_text())
        trial=next(x for x in search['trials_data'] if x['index']==c['search_index'])
        assert trial['config']==c['config']
        current={}
        for clip in sorted(develop):
            vals=[r for name,r in dev_seen.values() if name==clip]
            current[clip]=dict(positive_hits=sum(r['hit'] for r in vals if r['reference']=='positive_reference'),
                positive_N=sum(r['reference']=='positive_reference' for r in vals),
                negative_hits=sum(r['hit'] for r in vals if r['reference']=='negative_reference'),
                negative_N=sum(r['reference']=='negative_reference' for r in vals))
        assert current==trial['by_clip'],(cid,current,trial['by_clip'])
        search_check[cid]='PASS: search and frozen chronological replay agree after identical deduplication'
    for clip in ('11_KISA_fall_C00_001_0018','12_KISA_fall_C00_010_0004','13_KISA_fall_C00_034_0005','15_KISA_fall_C00_096_0008'):
        assert all(s['condition_frames']==s['raw_frames']==0 for s in summaries[clip]['candidates'].values())
    write_csv(dest/'comparison.csv',table);write_csv(dest/'reference_intervals.csv',reference_table)
    write_csv(dest/'action_case_observations.csv',case_rows)
    report=dict(status='PASS',clips=len(specs),candidates=len(manifest['candidates']),checks=dict(checks),
        reference_aggregation=aggregation,search_replay_parity=search_check,holdout_not_retuned=True,L01_executed=False,
        feature_p95_range_ms=[min(x['feature_p95_ms'] for x in times),max(x['feature_p95_ms'] for x in times)],
        feature_max_ms=max(x['feature_max_ms'] for x in times),
        input_diagnostics=limitations,
        limitations=['Not accuracy/recall or operational false-alarm rate','Approximate user reference intervals',
        'Condition duration counts only adjacent actual hit observations; no missing-time fill',
        'Self overlap prediction differences remain visible and are not independent cases'])
    (validation/'technical_audit.json').write_text(json.dumps(report,ensure_ascii=False,indent=2))
    (validation/'all_summaries.json').write_text(json.dumps(summaries,ensure_ascii=False,indent=2))
    for name in ('candidates.json','specs.json','cases.json','self_maps.json','execution_contract.json','checkpoint.ko.md'):
        shutil.copy2(OUT/name,validation/name)
    for name in ('search.json','search_seed_follow.json'):shutil.copy2(OUT/'reports'/name,validation/name)
    print(json.dumps(report,ensure_ascii=False),flush=True)


if __name__=='__main__':main()
