"""Causal, body-relative interaction features. Scores are not action probabilities."""
from dataclasses import dataclass, asdict
from math import dist, hypot, isfinite


@dataclass(frozen=True)
class Settings:
    person_score: float = .25
    joint_score: float = .10
    reference_joint_score: float = .05
    pair_distance_ratio: float = 1.8
    window_seconds: float = 1.0
    motion_lag_seconds: float = .2
    max_step_gap: float = .15
    pair_match_grace: float = .3
    pair_retention: float = 5.0
    pair_close_hold: float = .3
    required_strike_pulses: int = 2
    strike_interval: float = .25
    strike_motion: float = .18
    strike_toward: float = .10
    strike_approach: float = .04
    strike_hand_distance: float = 1.25
    max_arm_length: float = 2.2
    max_relative_speed: float = 8.0
    contact_hand_distance: float = 1.0
    contact_motion: float = .07
    pair_relative_change: float = .08
    contact_seconds: float = .4
    dynamic_contact_seconds: float = .2
    min_dynamic_samples: int = 3
    spatial_center_gate: float = .4
    spatial_scale_gate: float = 1.8
    spatial_ambiguity_margin: float = .06
    display_hold: float = .4
    rearm_quiet: float = 2.0
    event_cooldown: float = 2.0
    max_people: int = 32
    max_pairs: int = 128
    max_states: int = 512

    def __post_init__(self):
        if not 0<=self.reference_joint_score<=self.joint_score<=1 or not 0<=self.person_score<=1:
            raise ValueError('INVALID_SCORE_THRESHOLDS')
        if not 0<self.motion_lag_seconds<=self.window_seconds<=3 or not 0<self.pair_close_hold<=self.window_seconds:
            raise ValueError('INVALID_CAUSAL_WINDOW')
        if not 0<self.dynamic_contact_seconds<=self.contact_seconds<=self.window_seconds:
            raise ValueError('INVALID_CONTACT_WINDOW')

    def payload(self):return asdict(self)


def sub(a,b):return (a[0]-b[0],a[1]-b[1])
def div(a,s):return (a[0]/s,a[1]/s)
def norm(a):return hypot(*a)
def dot(a,b):return a[0]*b[0]+a[1]*b[1]
def mid(a,b):return ((a[0]+b[0])/2,(a[1]+b[1])/2)


def bodies(row,cfg):
    result=[];rejected=[];eligible=0
    for order,p in enumerate(row['persons']):
        score=p['score'];box=p['bbox'];reason=None
        if not isfinite(score) or not 0<=score<=1:reason='INVALID_SCORE'
        elif score<cfg.person_score:reason='PERSON_SCORE_BELOW'
        elif len(box)!=4 or not all(isfinite(x) for x in box) or box[0]>=box[2] or box[1]>=box[3]:reason='INVALID_BBOX'
        if reason:rejected.append(dict(order=order,reason=reason));continue
        eligible+=1
        if eligible>cfg.max_people:rejected.append(dict(order=order,reason='PEOPLE_CAP'));continue
        joints={}
        for j,k in enumerate(p['keypoints']):
            if len(k)==3 and all(isfinite(x) for x in k) and 0<=k[0]<=row['width'] and 0<=k[1]<=row['height'] and cfg.joint_score<=k[2]<=1:
                joints[j]=tuple(k[:2])
        options=[]
        if all(j in joints for j in (5,6,11,12)):options.append(('mid',mid(joints[5],joints[6]),mid(joints[11],joints[12])))
        for tag,sh,hip in (('left',5,11),('right',6,12)):
            if sh in joints and hip in joints:options.append((tag,joints[sh],joints[hip]))
        anchor=next(((tag,mid(sh,hip),dist(sh,hip)) for tag,sh,hip in options if dist(sh,hip)>1),None)
        if anchor is None:rejected.append(dict(order=order,reason='TORSO_ANCHOR_MISSING'));continue
        tag,center,scale=anchor
        arms={}
        for side,sh,elbow,wr in (('L',5,7,9),('R',6,8,10)):
            if sh in joints and wr in joints:
                relative=div(sub(joints[wr],joints[sh]),scale)
                if norm(relative)<=cfg.max_arm_length:
                    arms[side]=dict(shoulder=joints[sh],wrist=joints[wr],relative=relative,elbow_present=elbow in joints)
        tid=p['track_id']
        result.append(dict(order=order,query=p['query'],tid=tid,ref=('T'+str(tid) if tid is not None else 'q'+str(p['query'])),
            bbox=list(box),height=box[3]-box[1],center=center,scale=scale,torso_source=tag,arms=arms))
    return result,rejected


def match_cost(old,current,cfg):
    distances=[]
    for a,b in zip(old,current):
        if max(a['scale']/b['scale'],b['scale']/a['scale'])>cfg.spatial_scale_gate:return None
        d=dist(a['center'],b['center'])/max(1,(a['height']+b['height'])/2)
        if d>cfg.spatial_center_gate:return None
        distances.append(d)
    return max(distances)


def features(old,current,elapsed,cfg):
    arms=[];observable=[False,False]
    oldscale=sum(p['scale'] for p in old)/2;scale=sum(p['scale'] for p in current)/2
    pair_change=dist(div(sub(old[1]['center'],old[0]['center']),oldscale),div(sub(current[1]['center'],current[0]['center']),scale))
    for actor,(a,b) in enumerate(zip(old,current)):
        other=1-actor
        if a['torso_source']!=b['torso_source'] or old[other]['torso_source']!=current[other]['torso_source']:continue
        for side,arm in b['arms'].items():
            before=a['arms'].get(side)
            if before is None:continue
            delta=sub(arm['relative'],before['relative']);motion=norm(delta)
            direction=sub(current[other]['center'],arm['shoulder']);length=norm(direction)
            if length<=1:continue
            toward=dot(delta,div(direction,length))
            approach=dist(before['wrist'],old[other]['center'])/a['scale']-dist(arm['wrist'],current[other]['center'])/b['scale']
            hand_distance=dist(arm['wrist'],current[other]['center'])/current[other]['scale']
            speed=motion/elapsed
            valid=speed<=cfg.max_relative_speed
            observable[actor]|=valid
            strike=valid and motion>=cfg.strike_motion and toward>=cfg.strike_toward and approach>=cfg.strike_approach and hand_distance<=cfg.strike_hand_distance
            contact=valid and hand_distance<=cfg.contact_hand_distance
            dynamic=contact and motion>=cfg.contact_motion and pair_change>=cfg.pair_relative_change
            arms.append(dict(actor=actor,side=side,relative_motion=motion,toward=toward,approach=approach,
                hand_distance=hand_distance,relative_speed=speed,valid=valid,strike=bool(strike),contact=bool(contact),dynamic=bool(dynamic)))
    return dict(arms=arms,observable=observable,pair_relative_change=pair_change,
        strike=any(a['strike'] for a in arms),contact=any(a['contact'] for a in arms),dynamic=any(a['dynamic'] for a in arms))
