"""Site-source FALL baselines and one fixed spatial candidate; no Event I/O."""
from collections import Counter
from dataclasses import fields
import json
import math
import os
import time
from common import OUT,load,read,save,sha,annotation
import app
app.__path__.insert(0,str(OUT/'site_source/app'))
import app.contracts
app.contracts.__path__.insert(0,str(OUT/'site_source/app/contracts'))
from app.contracts.frames import FrameEnvelope,PoseObservation
from app.fall_scenario import FallEngine
from app.fall_config import FallConfig
from app.pose_config import POSE_MODEL
from app.event_cooldown import EventCooldown
from fall_candidate import features,SpatialState


class Legacy(FallEngine):
    def _advance(self,track,geometry,pts,*,gap):
        evidence,created=super()._advance(track,geometry,pts,gap=gap)
        self.created[id(track)]=created
        return evidence,created

    def observe(self,frame,poses,ids,now,gap):
        self.created={};self.clock[0]=now
        out=self.evaluate(frame,poses,ids,gap_before=gap)
        people=[]
        for d in out.decisions:
            direct=d.track_id is not None and d.evidence.geometry.torso_angle is not None and d.evidence.geometry.torso_angle+1e-12>=self.config.down_angle_deg
            temporal=self.created.get(id(self._tracks.get(d.track_id)),False)
            people.append(dict(query=int(d.object_ref),track_id=d.track_id,current_hit=bool(direct or temporal),
                direct=direct,temporal=temporal,display=d.alert,state=d.state,
                torso_angle=d.evidence.geometry.torso_angle,down=d.evidence.down))
        return dict(current_hit=any(p['current_hit'] for p in people),display=any(p['display'] for p in people),
            observable=any(p['torso_angle'] is not None for p in people),people=people,
            events=[dict(object_ref=e.object_ref,track_id=e.track_id,episode=e.episode) for e in out.event_candidates])


def engine(policy,cooldown):
    e=Legacy(FallConfig.parse(policy),direct_down=True,direct_down_cooldown_seconds=2.)
    e.clock=[0.];gate=EventCooldown(cooldown,clock=lambda:e.clock[0]);e.event_admission=gate.allow
    return e


def canonical(clip,row):
    w,h=row['width'],row['height']
    frame=FrameEnvelope('isolated','stored-cache',clip,clip+'-offline',1,row['sample'],round(row['seconds']*1e9),0,w,h,'frozen')
    poses=[];clipped=0
    for p in row['persons']:
        box=tuple(v/s for v,s in zip(p['bbox'],(w,h,w,h)))
        bounded=tuple(min(1.,max(0.,v)) for v in box);clipped+=box!=bounded
        poses.append(PoseObservation(frame,POSE_MODEL,p['score'],bounded,
            tuple((k[0]/w,k[1]/h,k[2]) for k in p['keypoints'])))
    return frame,poses,clipped


def run(clip):
    started=time.monotonic();rows,selected,spec,cache,mapping=load(clip)
    dest=OUT/'download'/clip/'data';dest.mkdir(parents=True,exist_ok=True)
    summary_path=dest/'summary.json'
    if summary_path.exists():
        s=read(summary_path);assert sha(dest/'diagnostics.jsonl')==s['trace_sha256'];print('REUSE_DIAG',clip);return s
    tracking=read(OUT/'work'/clip/'tracking30.json');track_validation=read(OUT/'work'/clip/'tracking_validation.json')
    assert sha(OUT/'work'/clip/'tracking30.json')==track_validation['derived_sha256']
    config=read(OUT/'conditions.json');site=read(OUT/'site_baseline.json')
    raw=site['runtime']['settings']['effective']['fall'];names={x.name for x in fields(FallConfig)}
    site_policy={k:v for k,v in raw.items() if k in names};site_policy['enabled']=True
    shared=dict(site_policy,person_score=config['person_score'],joint_score=config['joint_score'])
    cooldown=config['cooldown_seconds']
    baselines={'site_scores':engine(site_policy,cooldown),'matched_scores':engine(shared,cooldown),'stored10_matched_scores':engine(shared,cooldown)}
    candidate=SpatialState(config,clip)
    cases=read(OUT/'cases.json')['cases'];references=read(OUT/'references.json')
    stats=Counter();segments=[];frame_lists={k:[] for k in ('candidate','site','matched','stored10','added','removed','events')}
    last=None;trace=dest/'diagnostics.jsonl';assert not trace.exists()
    with trace.open('x') as f:
        for row in rows:
            frame,poses,clipped=canonical(clip,row);old_ids=[p['track_id'] for p in row['persons']]
            ids=[tracking[str(row['sample'])][str(p['query'])] for p in row['persons']]
            now=row['seconds']-rows[0]['seconds'];gap=last is not None and row['seconds']-last>.15
            baseline={k:e.observe(frame,poses,old_ids if k=='stored10_matched_scores' else ids,now,gap) for k,e in baselines.items()}
            people=[]
            for p,tid in zip(row['persons'],ids):
                person=dict(p,track_id=tid);people.append(features(person,config))
            result=candidate.evaluate(row['seconds'],people,monotonic_now=now)
            note=annotation(clip,row,spec,mapping,cases,references)
            displayed=row['sample'] in selected
            record=dict(sample=row['sample'],source_frame=row['source_frame'],seconds=row['seconds'],displayed=displayed,
                annotation=note,tracking_mode=spec['tracking_mode'],people=people,baseline=baseline,candidate=result,
                canonical_bbox_clips=clipped)
            f.write(json.dumps(record,ensure_ascii=False,separators=(',',':'),allow_nan=False)+'\n');last=row['seconds']
            if not displayed:continue
            stats['observations']+=1;stats['eligible_persons']+=sum(p['eligible'] for p in people)
            stats['observed_input']+=any(p['eligible'] for p in people);stats['observable']+=result['observable']
            stats['insufficient']+=not result['observable'];stats['candidate']+=result['hit'];stats['hold_only']+=result['display_hold']
            stats['site']+=baseline['site_scores']['current_hit'];stats['site_display']+=baseline['site_scores']['display']
            stats['matched']+=baseline['matched_scores']['current_hit'];stats['stored10']+=baseline['stored10_matched_scores']['current_hit']
            stats['matched_display']+=baseline['matched_scores']['display'];stats['canonical_bbox_clips']+=clipped
            a=baseline['matched_scores']['current_hit'];b=result['hit'];stats['added']+=b and not a;stats['removed']+=a and not b
            label=note['reference'];stats[label+'_observations']+=1;stats[label+'_observable']+=result['observable']
            stats[label+'_candidate']+=b;stats[label+'_matched']+=a;stats[label+'_site']+=baseline['site_scores']['current_hit']
            stats[label+'_added']+=b and not a;stats[label+'_removed']+=a and not b
            stats['events']+=result['event'] is not None
            for k,truth in [('candidate',b),('site',baseline['site_scores']['current_hit']),('matched',a),('stored10',baseline['stored10_matched_scores']['current_hit']),('added',b and not a),('removed',a and not b),('events',result['event'] is not None)]:
                if truth:frame_lists[k].append(dict(sample=row['sample'],frame=row['source_frame'],seconds=row['seconds'],reference=label))
            for p in people:
                for reason in p['reasons']:stats['reason_'+reason]+=1
    save(dest/'frame_lists.json',frame_lists)
    save(dest/'tracking_validation.json',track_validation)
    result=dict(status='PASS',clip=clip,counts=dict(stats),tracking_mode=spec['tracking_mode'],
        trace_sha256=sha(trace),conditions_sha256=sha(OUT/'conditions.json'),cache_sha256=spec['cache_sha256'],
        site_policy=site_policy,matched_policy=shared,cooldown_seconds=cooldown,
        current_baseline_definition='current direct-down torso condition with existing ID or newly confirmed temporal condition; NOT latched display',
        bbox_adapter='Only baseline canonical bbox clipped to [0,1]; original joints/geometry retained; FALL core geometry does not read bbox',
        elapsed_seconds=time.monotonic()-started,full_cache_observations=len(rows))
    save(summary_path,result);print('DIAG_PASS',clip,json.dumps(result['counts']),flush=True);return result


if __name__=='__main__':
    clip=os.environ['CLIP'];names=list(read(OUT/'specs.json')) if clip=='all' else [clip]
    for name in names:run(name)
