"""FALL-R2-RUNTIME-01: real current objects only; admission never gates display."""
from collections import Counter
import time
from app.contracts.frames import FrameEnvelope,PoseObservation
from app.pose_config import POSE_MODEL
from app.intrusion import inside
from app.event_cooldown import EventCooldown,allow_event
from app.fight_r5.notification import NotificationState
from .core import R2Core

class R2Runtime:
    def __init__(self,config,event_id_factory,wall_ns,contains=None):
        self.config=config;self.event_id_factory=event_id_factory;self.wall_ns=wall_ns;self.contains=contains
        self.core=R2Core(config.policy.person_score,config.policy.joint_score,config=config.r2)
        self.metrics=Counter();self.clear()

    def clear(self):
        self.core.clear();self.notification=NotificationState();self._fallback_admission=EventCooldown(10.)
        self._identity=None;self._last_pts=None;self._last_seq=None;self._event_id=None;self.last_result=None

    def evaluate(self,frame,poses,ids,*,gap_before=False):
        from app.fall_runtime import FallDisplay,FallEvent
        if not isinstance(frame,FrameEnvelope) or type(gap_before) is not bool:raise ValueError('Invalid FALL R2 Frame.')
        poses,ids=tuple(poses),tuple(ids)
        if len(poses)!=len(ids):raise ValueError('Pose/Tracking association differs.')
        seen=set()
        for p,tid in zip(poses,ids):
            if not isinstance(p,PoseObservation) or p.frame!=frame or p.model_key!=POSE_MODEL:raise ValueError('FALL R2 requires same-Frame canonical Pose.')
            if tid is not None:
                if type(tid) is not int or tid<1 or tid in seen:raise ValueError('Invalid FALL track ID.')
                seen.add(tid)
        points=tuple(((p.bbox_xyxy[0]+p.bbox_xyxy[2])/2,p.bbox_xyxy[3]) for p in poses)
        selected=[i for i,p in enumerate(points) if (self.contains(p) if self.contains else inside(p,self.config.roi))]
        rows=[FallDisplay(p,i in selected,'MONITORING' if i in selected else 'OUTSIDE',self.config.confidence) for i,p in enumerate(points)]
        pts=frame.pts_ns
        if type(pts) is not int or not 0<=pts<2**63 or type(frame.frame_seq) is not int or frame.frame_seq<0:
            self.metrics['invalid_pts']+=1
            return tuple(FallDisplay(p,i in selected,'TIME_UNAVAILABLE',self.config.confidence) for i,p in enumerate(points)),()
        identity=(frame.server_id,frame.source_id,frame.stream_id,frame.run_id,frame.run_generation,frame.frame_width,frame.frame_height,frame.config_version)
        if self._identity is not None and identity!=self._identity:raise ValueError('FALL R2 run/source/config/geometry changed; clear or create a new engine.')
        if self._last_pts is not None and (pts<=self._last_pts or frame.frame_seq<=self._last_seq):
            self.metrics['stale_frame']+=1;return tuple(rows),()
        self._identity=identity;self._last_pts=pts;self._last_seq=frame.frame_seq
        if not self.config.enabled:return tuple(rows),()
        w,h=frame.frame_width,frame.frame_height
        persons=[dict(score=poses[i].score,bbox=[v*(w if k%2==0 else h) for k,v in enumerate(poses[i].bbox_xyxy)],
                      keypoints=[[x*w,y*h,s] for x,y,s in poses[i].keypoints],track_id=ids[i],query=i) for i in selected]
        result,people,_=self.core.step(dict(seconds=pts/1e9,persons=persons))
        self.last_result=result;events=[]
        candidate=self.notification.observe(pts/1e9,result['hit'])
        if not self.notification.latched:self._event_id=None
        if candidate:
            admitted=allow_event(self,'FALL') if getattr(self,'event_admission',None) else self._fallback_admission.allow('FALL')
            if admitted:
                i=next(p['query'] for p in people if p['hit']);eid=self.event_id_factory()
                if type(eid) is not str or not eid or len(eid)>100:raise ValueError('Invalid FALL Event ID.')
                self._event_id=eid
                events.append(FallEvent(eid,frame,ids[i],self.config.geometry.roi_id,str(i),poses[i].score,
                                        poses[i].bbox_xyxy,poses[i].model_key,self.wall_ns()//1000000))
                self.notification.admitted(time.monotonic());self.metrics['events_admitted']+=1
            else:self.notification.denied();self.metrics['events_suppressed_by_web_cooldown']+=1
        for color in result['colors']:
            if color['state'] not in ('RED_CURRENT','YELLOW_HOLD'):continue
            i=color['query'];rows[i]=FallDisplay(points[i],True,'FALL_CONFIRMED',self.config.confidence,self._event_id)
        return tuple(rows),tuple(events)

    def notification_status(self):
        r=self.last_result or {}
        return dict(self.notification.snapshot(),current_hit=bool(r.get('hit')),
                    display_active=bool(r.get('display')),counts=dict(self.metrics),delivery_state='SEE_ARTIFACT_STATUS')
