"""Adapt normalized same-frame Pose to R5; publish only current real objects."""
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 allow_event
from .core import R5Core
from .notification import NotificationState
from app.event_cooldown import EventCooldown


class R5Runtime:
    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.metrics=Counter()
        self.core=R5Core(config.r5,person_score=config.policy.person_score,joint_score=config.policy.joint_score)
        self.clear()

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

    def _break_motion(self):
        from .geometry import Spatial
        from .decision import Refined
        self.core.spatial=Spatial(self.core.geometry)
        self.core.decision=Refined(self.config.r5.decision(),dict(compact_hand=.12,compact_max_speed=1.))
        self.core.holds.clear();self.core.episode.previous_quiet=False;self.core.episode.quiet=0.

    def evaluate(self, frame, poses, ids, *, gap_before=False):
        from app.fight_runtime import FightDisplay, FightEvent
        if not isinstance(frame,FrameEnvelope) or type(gap_before) is not bool:
            raise ValueError('Invalid FIGHT R5 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('FIGHT R5 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 FIGHT 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=[(FightDisplay(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._gap_pending=True;self.metrics['invalid_pts']+=1;return tuple(rows),()
        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 self._identity!=identity:
            raise ValueError('FIGHT R5 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),()
        if gap_before or self._gap_pending:self._break_motion()
        self._gap_pending=False
        w,h=frame.frame_width,frame.frame_height
        persons=[]
        for i in selected:
            p=poses[i]
            persons.append(dict(score=p.score,bbox=[v*(w if k%2==0 else h) for k,v in enumerate(p.bbox_xyxy)],
                keypoints=[[x*w,y*h,s] for x,y,s in p.keypoints],track_id=ids[i],query=i))
        r,_=self.core.step(dict(seconds=pts/1e9,width=w,height=h,persons=persons))
        self.last_result=r;events=[]
        candidate=self.notification.observe(pts/1e9,r['hit'])
        if not self.notification.latched:self._event_id=None
        if candidate:
            admitted=(allow_event(self,'FIGHT') if getattr(self,'event_admission',None)
                      else self._fallback_admission.allow('FIGHT'))
            if admitted:
                i=selected[r['current_orders'][0]];eid=self.event_id_factory();self._event_id=eid
                events.append(FightEvent(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.metrics['events_admitted']+=1
                self.notification.admitted(time.monotonic())
            else:
                self.metrics['events_suppressed_by_web_cooldown']+=1
                self.notification.denied()
        for order in r['display_orders']:
            i=selected[order]
            rows[i]=(FightDisplay(points[i],True,'FIGHT',self.config.confidence,self._event_id),)
        return tuple(rows),tuple(events)

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