"""Candidate-only local hand evidence vetoes; labels never enter the rule engine."""
import bootstrap
from collections import deque
from math import dist,hypot
from integrated import gates as original_gates


def augment(row):
    for p in row['pairs']:
        for f in p['features']:
            b=p['bodies'][f['actor']]
            joints={int(k):v for k,v in b['joints'].items()}
            heads=[dist(f['endpoint'],joints[k][:2])/max(1,b['height']) for k in range(5) if k in joints]
            f['own_head_distance']=min(heads) if heads else None
            f['own_shoulder_distance']=hypot(*f['relative_end']) if f['kind']=='hand' else None
    return row


def gates(f,cfg,extra,seed,contact):
    g=original_gates(f,cfg,seed,contact)
    why=[]
    if f['kind']=='hand':
        reach=f['own_shoulder_distance']
        weak=f['speed']<=extra.get('compact_max_speed',float('inf')) and f['motion']<=extra.get('compact_max_motion',float('inf'))
        if reach<extra['compact_hand'] and weak:
            why.append('SELF_PROXIMAL_HAND')
        if extra.get('self_head') and f['own_head_distance'] is not None and f['own_head_distance']<=extra['self_head'] and reach<=.23:
            why.append('SELF_HEAD_NEAR_HAND')
        limit=extra.get('mutual_hand')
        if limit and f.get('hand_contact') is not None and f['hand_contact']<=limit and f.get('target_hand_offset') is not None and f['target_hand_offset']<=.15 and f['vertical']<=.12 and f['speed']<=1.2:
            why.append('WEAK_ELEVATED_HAND_CONTACT')
    original=list(g['rules'])
    if why:
        g['suppressed_rules']=sorted(set(g['suppressed_rules'])|set(g['rules']))
        g['rules']=[];g['seed']=False;g['contact']=False
    g['suppression_evidence']+=why
    g['suppression_applied']=bool(g['suppressed_rules'])
    g['refinement_evidence']=why
    g['refinement_suppressed']=original if why else []
    return g


class Refined:
    def __init__(self,cfg,extra):
        self.cfg=cfg;self.extra=extra;self.states={};self.last_time=None

    def step(self,row):
        t=row['seconds'];cfg=self.cfg
        assert self.last_time is None or t>self.last_time
        self.last_time=t
        self.states={k:v for k,v in self.states.items() if t-v['time']<=cfg['window']+1e-8}
        output=[]
        for p in row['pairs']:
            s=self.states.setdefault(p['token'],dict(time=t,seeds={},contacts={},history=deque(maxlen=40)))
            if p['reset']:s['seeds'].clear();s['contacts'].clear();s['history'].clear()
            s['time']=t
            s['seeds']={k:v for k,v in s['seeds'].items() if t-v['time']<=cfg['seed_seconds']+1e-8}
            s['contacts']={k:v for k,v in s['contacts'].items() if t-v['time']<=.35+1e-8}
            while s['history'] and t-s['history'][0]['time']>cfg['window']+1e-8:s['history'].popleft()
            diagnostics=[];updates={};contacts={}
            for f in p['features']:
                key=(f['actor'],f['limb']);g=gates(f,cfg,self.extra,s['seeds'].get(key),s['contacts'].get(key))
                diagnostics.append(dict(f,**g))
                if g['seed'] and (key not in updates or f['approach']/f['elapsed']>updates[key]['approach']/updates[key]['elapsed']):updates[key]=f
                if g['contact'] and (key not in contacts or f['speed']>contacts[key]['speed']):contacts[key]=f
            for key,f in updates.items():s['seeds'][key]=dict(time=t,delta=f['delta'])
            for key,f in contacts.items():s['contacts'][key]=dict(time=t,delta=f['delta'])
            accepted=[f for f in diagnostics if f['rules']]
            flags=[any(f['actor']==i for f in accepted) for i in (0,1)]
            contact_flags=[any(f['actor']==i and 'CLOSE_REVERSAL' in f['rules'] for f in accepted) for i in (0,1)]
            s['history'].append(dict(time=t,flags=flags,contact=contact_flags))
            counts=[sum(r['flags'][i] for r in s['history']) for i in (0,1)]
            contact_counts=[sum(r['contact'][i] for r in s['history']) for i in (0,1)]
            actors=[]
            for i in (0,1):
                strike=any(f['actor']==i and any(x!='CLOSE_REVERSAL' for x in f['rules']) for f in accepted)
                close=contact_flags[i] and contact_counts[i]>=cfg['contact_min_samples']
                if counts[i]>=cfg['min_samples'] and (strike or close):actors.append(i)
            output.append(dict(token=p['token'],refs=p['refs'],orders=p['orders'],mode=p['mode'],
                candidate_state=p['candidate_state'],hold_age=p['hold_age'],reset=p['reset'],
                observable=bool(p['features']),raw=any(flags),hit=bool(actors),actors=actors,counts=counts,
                contact_counts=contact_counts,passing=accepted,diagnostics=diagnostics,
                rules=sorted({x for f in accepted if f['actor'] in actors for x in f['rules']}),
                suppression_applied=any(f['suppression_applied'] for f in diagnostics),
                reason='CURRENT_QUALIFIED_RULE' if actors else 'RECENT_COUNT_BELOW' if any(flags)
                    else 'LOCAL_SUPPRESSION' if any(f['suppression_applied'] for f in diagnostics)
                    else 'CONDITIONS_BELOW' if p['features'] else 'INSUFFICIENT_OBSERVATIONS'))
        return dict(pairs=output,hit=any(p['hit'] for p in output),raw=any(p['raw'] for p in output),
            observable=any(p['observable'] for p in output),
            quiet=bool(output) and all(p['observable'] for p in output) and not any(p['raw'] for p in output)
                  and not row['ambiguous'] and not row['pairs_over_cap'])
