"""PR13 provider dependency plan for one channel.

Scenario activation chooses the minimum provider set. Pose owns the person lane
when FALL/FIGHT or Pose display requires keypoints; otherwise Medium owns the
person lane. FIRE and CROWD are independent non-tracking providers.
"""
from __future__ import annotations

from dataclasses import dataclass

from app.pose_config import POSE_MODEL, PoseConfig
from app.fire_config import FIRE_MODEL, FireConfig
from app.crowd_config import CROWD_MODEL, CrowdConfig
from app.intrusion import IntrusionConfig
from app.loitering import LoiteringConfig
from app.fall_runtime import FallSettings
from app.fight_runtime import FightSettings

MEDIUM_MODEL = 'RFDETRMedium'


@dataclass(frozen=True)
class ProviderSpec:
    key: str
    role: str
    model: str
    outputs: tuple[str, ...]
    scenarios: tuple[str, ...] = ()
    person_owner: bool = False

    def payload(self):
        return dict(key=self.key, role=self.role, model=self.model,
                    outputs=list(self.outputs), scenarios=list(self.scenarios),
                    person_owner=self.person_owner)


@dataclass(frozen=True)
class ProviderPlan:
    providers: tuple[ProviderSpec, ...]
    person_owner: str | None
    requested_scenarios: tuple[str, ...]

    @property
    def keys(self):
        return tuple(p.key for p in self.providers)

    def payload(self):
        return dict(person_owner=self.person_owner,
                    requested_scenarios=list(self.requested_scenarios),
                    providers=[p.payload() for p in self.providers])


def plan(spec):
    intrusion = IntrusionConfig.parse(spec.get('intrusion') or {})
    loitering = LoiteringConfig.parse(spec.get('loitering') or {})
    fall = FallSettings.parse(spec.get('fall') or {})
    fight = FightSettings.parse(spec.get('fight') or {})
    pose = PoseConfig.parse(spec.get('pose') or {})
    fire = FireConfig.parse(spec.get('fire') or {})
    crowd = CrowdConfig.parse(spec.get('crowd') or {})

    requested = []
    if intrusion.enabled:
        requested.append('INTRUSION')
    if loitering.enabled:
        requested.append('LOITERING')
    if fall.enabled:
        requested.append('FALL')
    if fight.enabled:
        requested.append('FIGHT')
    if fire.event_enabled:
        requested.append('FIRE')
    if crowd.enabled:
        requested.append('CROWD')

    pose_needed = pose.required or fall.enabled or fight.enabled
    person_needed = intrusion.enabled or loitering.enabled or fall.enabled or fight.enabled
    person_owner = 'pose' if pose_needed else 'medium' if person_needed else None

    providers = []
    if person_owner == 'medium':
        providers.append(ProviderSpec(
            'medium', 'det', MEDIUM_MODEL, ('bbox',),
            tuple(s for s in ('INTRUSION', 'LOITERING') if s in requested), True))
    if pose_needed:
        providers.append(ProviderSpec(
            'pose', 'pose', POSE_MODEL, ('bbox', 'keypoints'),
            tuple(s for s in ('INTRUSION', 'LOITERING', 'FALL', 'FIGHT') if s in requested),
            person_owner == 'pose'))

    # Future non-person COCO visualization requests need Medium even if Pose owns person.
    categories = (spec.get('visualization') or {}).get('categories')
    if categories:
        from app.class_catalog import COCO_NAMES
        non_person = any(k in COCO_NAMES and k != 'person' for k in categories)
        if non_person and not any(p.key == 'medium' for p in providers):
            providers.insert(0, ProviderSpec('medium', 'det', MEDIUM_MODEL, ('bbox',), (), False))

    if fire.enabled or fire.event_enabled:
        providers.append(ProviderSpec('fire', 'det', FIRE_MODEL, ('bbox',),
                                      ('FIRE',) if fire.event_enabled else ()))
    if crowd.enabled:
        providers.append(ProviderSpec('crowd', 'det', CROWD_MODEL, ('bbox',), ('CROWD',)))

    # A canonical channel with no scenario/display requirement keeps the historic
    # Medium detection provider instead of creating an empty pipeline.
    if not providers:
        person_owner = 'medium'
        providers.append(ProviderSpec('medium', 'det', MEDIUM_MODEL, ('bbox',), (), True))

    if any(p.person_owner for p in providers) and (spec.get('tracking') or {}).get('enabled', True) is not True:
        if person_needed:
            raise ValueError('Person scenarios require shared Tracking On.')
    if requested and (not spec.get('server_id') or not spec.get('source_id')):
        raise ValueError('Canonical identity is required for requested scenarios.')

    return ProviderPlan(tuple(providers), person_owner, tuple(requested))


def scenario_dependencies(provider_plan):
    return {scenario: provider.key
            for provider in provider_plan.providers
            for scenario in provider.scenarios}


def resolve(provider_plan, is_ready=None):
    """Validate required model readiness without silently substituting semantics."""
    if is_ready is None:
        from app.inference_engine import engine
        is_ready = engine.is_model_ready
    unavailable = []
    for provider in provider_plan.providers:
        try:
            ready = bool(is_ready(provider.model))
        except Exception:
            ready = False
        if not ready:
            unavailable.append(provider.key)
    return tuple(unavailable)
