"""Candidate rules use independent observed evidence, not named action classes."""
from collections import deque


def passes(f,cfg):
    # Derived ratios need a numerical boundary tolerance, not score rounding.
    eps=1e-12
    if f['kind']=='foot' and f['vertical']>cfg['foot_vertical_max']+eps:return False
    return (f['speed']+eps>=cfg['speed_min'] and f['near']<=cfg['near_max']+eps
        and f['approach']/f['elapsed']+eps>=cfg['approach_speed_min']
        and f['cosine']+eps>=cfg['cosine_min']
        and f['extension']/f['elapsed']+eps>=cfg['extension_speed_min'])


class Candidate:
    def __init__(self,cfg):
        self.cfg=cfg;self.histories={};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
        for key,h in list(self.histories.items()):
            if not h or t-h[-1][0]>cfg['window']+1e-8:del self.histories[key]
        pairs=[]
        for p in row['pairs']:
            token=p['token'];h=self.histories.setdefault(token,deque(maxlen=40))
            if p['reset']:h.clear()
            while h and t-h[0][0]>cfg['window']+1e-8:h.popleft()
            passing=[f for f in p['features'] if passes(f,cfg)]
            flags=[any(f['actor']==i for f in passing) for i in (0,1)]
            h.append((t,flags));counts=[sum(x[i] for _,x in h) for i in (0,1)]
            actors=[i for i in (0,1) if flags[i] and counts[i]>=cfg['min_samples']]
            pairs.append(dict(token=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,passing=passing,features_count=len(p['features']),
                reason='CURRENT_OBSERVED_EVIDENCE' if actors else 'RECENT_COUNT_BELOW' if any(flags)
                else 'FEATURE_CONDITIONS_BELOW' if p['features'] else 'INSUFFICIENT_OBSERVATIONS'))
        return dict(pairs=pairs,hit=any(p['hit'] for p in pairs),raw=any(p['raw'] for p in pairs),
            observable=any(p['observable'] for p in pairs),
            quiet=bool(pairs) and all(p['observable'] for p in pairs) and not any(p['raw'] for p in pairs)
                  and not row['ambiguous'] and not row['pairs_over_cap'])
