"""SCENARIO-FANOUT-01: worker-owned independent engines share tracked observations."""
from app.intrusion import Intrusion, IntrusionConfig
from app.loitering import Loitering, LoiteringConfig


def name(config):
    return 'LOITERING' if isinstance(config, LoiteringConfig) else getattr(config, 'scenario_name', 'INTRUSION')


def members(value):
    return value if isinstance(value, tuple) else (() if value is None else (value,))


class RegionFanout:
    def __init__(self, engines):
        self.engines = tuple(engines)
        self.config = tuple(engine.config for engine in self.engines)

    def evaluate(self, frame, observations, ids):
        decisions = [[] for _ in observations]
        events = []
        for engine in self.engines:
            rows, emitted = engine.evaluate(frame, observations, ids)
            for target, decision in zip(decisions, rows):
                if decision is not None:
                    target.append(decision)
            events.extend(emitted)
        return tuple(tuple(row) or None for row in decisions), tuple(events)

    def expire(self):
        for engine in self.engines:
            engine.expire()

    def clear(self):
        for engine in self.engines:
            engine.clear()


def alerted(decision):
    return any(d.state in ('INTRUSION', 'LOITERING', 'FIRE', 'FALL_CONFIRMED', 'RECOVERING', 'FIGHT') for d in members(decision))


def event_ids(decision):
    return tuple(d.event_id for d in members(decision) if d.event_id)


def validate(spec):
    intrusion = IntrusionConfig.parse(spec.get('intrusion', {}))
    loitering = LoiteringConfig.parse(spec.get('loitering', {}))
    if loitering.enabled:
        if not spec.get('server_id') or not spec.get('source_id'):
            raise ValueError('Canonical identity is required for LOITERING.')
        from app.pose_config import execution_plan, PERSON_MODELS
        plan = execution_plan(spec)
        profile = spec.get('inference_profile') or {}
        if (plan['model'] not in PERSON_MODELS
                or profile.get('det_model', 'RFDETRMedium') not in PERSON_MODELS
                or len(profile.get('roles', ['det'])) != 1):
            raise ValueError('LOITERING requires one person provider.')
        if (spec.get('tracking') or {}).get('enabled', True) is not True:
            raise ValueError('LOITERING requires Tracking On.')
    return intrusion, loitering


def compose(spec):
    intrusion, loitering = validate(spec)
    engines = []
    if intrusion.enabled:
        engines.append(Intrusion(intrusion))
    if loitering.enabled:
        engines.append(Loitering(loitering))
    return RegionFanout(engines) if len(engines) > 1 else engines[0] if engines else None


def visible(score, decision):
    """Preserve the legacy strict INTRUSION comparison; LOITERING is inclusive."""
    from app.loitering import LoiteringDecision
    from app.fall_runtime import FallDisplay
    from app.fight_runtime import FightDisplay
    return any(score >= d.confidence if isinstance(d, (LoiteringDecision, FallDisplay, FightDisplay))
               else score > d.confidence for d in members(decision))


def project_loitering(spec, scenarios):
    """Project existing Web fields only; AI-owned timers survive every Web Start.

    Caller holds the Manager command lock and supplies a private candidate spec.
    Disabled/absent LOITERING leaves legacy specs byte-for-byte unchanged.
    """
    request = next(s for s in scenarios if s['scenarioCode'] == 'LOITERING')
    if not request['enabled'] and 'loitering' not in spec:
        return
    cfg = dict(spec.get('loitering') or {})
    cfg.update(enabled=request['enabled'],
               confidence=.5 if request['confidence'] is None else request['confidence'])
    roi = request['rois'][0] if request['rois'] else None
    cfg['roi'] = roi['points'] if roi else [[0., 0.], [1., 0.], [1., 1.], [0., 1.]]
    if roi:
        cfg['roi_name'] = roi['label']
    else:
        cfg.pop('roi_name', None)
    spec['loitering'] = LoiteringConfig.parse(cfg).payload()
