"""Three local rule families; no clip labels, frame numbers, or role truth inputs."""
from math import hypot


def cosine(x,y):
    den=hypot(*x)*hypot(*y)
    return sum(a*b for a,b in zip(x,y))/den if den>1e-12 else 1.


def gates(f,cfg,seed=None,contact=None):
    """Evidence and exclusions are evaluated for one actor/limb/partner only."""
    eps=1e-12; hand=f['kind']=='hand'; rules=[]; tests={}; exclusions=[]
    speed=f['speed']; ap=f['approach']/f['elapsed']; ext=f['extension']/f['elapsed']
    min_speed=cfg['hand_speed'] if hand else cfg['foot_speed']
    near=cfg['hand_near'] if hand else cfg['foot_near']
    level=cfg['hand_vertical'] if hand else cfg['foot_vertical']
    tests['motion']=speed+eps>=min_speed
    tests['target_distance']=f['near']<=near+eps
    tests['endpoint_level']=f['vertical']<=level+eps
    tests['toward']=f['cosine']+eps>=cfg['direction']
    tests['approach']=ap+eps>=cfg['approach']
    tests['extension']=ext+eps>=-.10
    # Hand-to-hand elevated contact is only an exclusion candidate, not an action label.
    hand_contact=(hand and f.get('hand_contact') is not None and f['hand_contact']<=cfg['hand_contact_max']
                  and f.get('target_hand_offset') is not None and f['target_hand_offset']<=cfg['target_hand_level']
                  and f['vertical']<=cfg['hand_vertical'])
    if hand_contact:exclusions.append('MUTUAL_HAND_CONTACT')
    low_swing=not tests['endpoint_level']
    if low_swing:exclusions.append('LOW_ENDPOINT')
    falling=(f.get('shoulder_vy') is not None and f.get('hip_vy') is not None
             and f['shoulder_vy']>.30 and f['hip_vy']>.20
             and f['root_vy']>max(.35,speed*1.4))
    if falling:exclusions.append('BODY_DROP_DOMINANT')
    horizontal=(f.get('torso_horizontal') is not None and f['torso_horizontal']>.80
                and speed<1.0)
    if horizontal:exclusions.append('HORIZONTAL_BODY_LOW_RESIDUAL')
    direct=all(tests.values())
    if direct:rules.append('ARM_DIRECT' if hand else 'LEG_DIRECT')
    curve=(hand and tests['motion'] and tests['target_distance'] and tests['endpoint_level']
           and f['cosine']>=.05 and ap>=.10 and f['angular'] is not None
           and abs(f['angular'])>=12 and f['motion']>=.035)
    if curve:rules.append('ARM_CURVE')
    follow=(seed is not None and tests['motion'] and tests['endpoint_level']
            and f['near']<=near+.05+eps
            and (cosine(seed['delta'],f['delta'])<=-.15 or tests['toward']))
    if follow:rules.append('ARM_RETURN' if hand else 'LEG_RETURN')
    # Close interaction needs observed limb motion and a reversal, not proximity alone.
    tests['torso_response']=(f.get('response_speed') is not None and f['response_speed']>=cfg['response_min']
                            and f['response_motion']>=.02 and f['response_cosine']>=.25)
    close=(hand and contact is not None and speed>=cfg['contact_speed'] and tests['torso_response']
           and f['near']<=cfg['contact_near'] and f['vertical']<=.20
           and f['target_region']=='torso' and cosine(contact['delta'],f['delta'])<=-.25
           and f['motion']>=.02)
    if close:rules.append('CLOSE_REVERSAL')
    blanket=[x for x in exclusions if x!='MUTUAL_HAND_CONTACT']
    accepted=[] if blanket else [x for x in rules if not (hand_contact and x in ('ARM_RETURN','CLOSE_REVERSAL'))]
    suppressed=[x for x in rules if x not in accepted]
    applied=bool(suppressed)
    return dict(tests=tests,rules_present=rules,rules=accepted,suppressed_rules=suppressed,
                suppression_evidence=exclusions,suppression_applied=applied,
                seed=bool((direct or curve) and not blanket),
                contact=bool(hand and speed>=cfg['contact_speed'] and f['near']<=cfg['contact_near']
                             and f['vertical']<=.20 and f['target_region']=='torso' and not exclusions))
