"""
Contextra AI Analytics — Scenario Activation Policy (v2 구동 정책)

ai_engine 시나리오 철학 이식 (현대화):
  1. 선언적 의존성 — 각 시나리오는 "내가 필요한 capability(클래스/role)"를
     registry에 스스로 선언한다. 플랫폼이 활성 시나리오로부터 역추적한다.
  2. 명시적 활성화 해석 — runnable = requested ∩ (deps ⊆ capabilities) ∧ zone 존재 ∧ enabled.
     비활성화되는 모든 시나리오는 *명시적 사유*를 가진다 (silent fallback 금지).
  3. 3계층 cfg deep-merge — engine defaults → site override → per-stream profile.
     하드코딩 변수 없음: 모든 튜닝 파라미터는 config 계층에서 온다.

제거된 구형 잔재: NMS(불필요), visualizer 결합, coordinator monolith, dict-soup data_bus.
"""

from __future__ import annotations

import logging
from copy import deepcopy
from dataclasses import dataclass, field
from typing import Any, Dict, FrozenSet, List, Optional, Tuple

logger = logging.getLogger(__name__)


# ============================================================================
# Capability — 스트림이 제공할 수 있는 추론 능력
# ============================================================================

@dataclass(frozen=True)
class CapabilitySet:
    """스트림의 활성 추론 capability (inference profile로부터 계산).

    - classes: det role이 활성이면 COCO 전체 라벨, 아니면 빈 집합.
      (RF-DETR det은 80클래스 단일 모델 — per-class on/off는 문턱값으로 제어)
    - roles: 활성 inference role (det/pose/seg).
    """
    classes: FrozenSet[str] = frozenset()
    roles: FrozenSet[str] = frozenset()

    def has_class(self, name: str) -> bool:
        return name in self.classes


def compute_capabilities(profile=None) -> CapabilitySet:
    """StreamProfile → CapabilitySet. profile=None이면 기본(전부 활성)."""
    from app.class_catalog import COCO_NAMES
    if profile is None:
        return CapabilitySet(classes=frozenset(COCO_NAMES), roles=frozenset({"det", "pose", "seg"}))
    classes = frozenset(COCO_NAMES) if "det" in profile.roles else frozenset()
    return CapabilitySet(classes=classes, roles=frozenset(profile.roles))


# ============================================================================
# Activation — 명시적 활성화 해석 (silent fallback 금지)
# ============================================================================

@dataclass(frozen=True)
class ActivationDecision:
    """시나리오 1개의 활성화 판정 + 사유.

    비활성화 시 reason은 반드시 구체적이어야 한다 (관측/디버깅용):
      - "disabled_by_profile"        : per-stream profile에서 enabled=False
      - "missing_capability:<cls>"   : deps 클래스가 capability에 없음
      - "missing_role:<role>"        : deps role이 활성 role에 없음
      - "zone_not_defined:<name>"    : 요구 zone이 DB에 미정의 (기존 가드 정책 유지)
      - "not_registered"             : registry에 없는 이름
    """
    name: str
    active: bool
    reason: str = ""


@dataclass(frozen=True)
class ActivationPlan:
    """스트림 1개의 시나리오 활성화 전체 계획."""
    stream_id: str
    active: Tuple[str, ...] = ()
    decisions: Tuple[ActivationDecision, ...] = ()

    @property
    def disabled(self) -> List[Tuple[str, str]]:
        return [(d.name, d.reason) for d in self.decisions if not d.active]


def resolve_activation(
    requested: List[str],
    capabilities: CapabilitySet,
    registry_meta: Dict[str, Dict[str, Any]],
    zones_present: set,
    stream_overrides: Optional[Dict[str, Any]] = None,
    stream_id: str = "?",
) -> ActivationPlan:
    """ai_engine 철학의 핵심 — 의존성 기반 활성화 해석.

    Args:
        requested: 기동 요청 시나리오 이름 목록 (site cfg 또는 explicit).
        capabilities: 스트림의 추론 능력 (compute_capabilities 결과).
        registry_meta: {name: {"dependencies": [...], "zone_name": str|None, ...}}
            — @register_scenario에 선언된 metadata.
        zones_present: DB에 활성으로 등록된 zone 이름 집합.
        stream_overrides: per-stream profile.scenarios ({name: {"enabled","cfg"}}).

    Returns:
        ActivationPlan — active 목록 + 모든 요청 시나리오의 명시적 판정/사유.
    """
    overrides = stream_overrides or {}
    decisions: List[ActivationDecision] = []
    active: List[str] = []

    for name in requested:
        meta = registry_meta.get(name)
        if meta is None:
            decisions.append(ActivationDecision(name, False, "not_registered"))
            continue

        # 1) per-stream enabled (최우선 — 운영자가 명시적으로 끄는 것)
        ov = overrides.get(name) or {}
        if ov.get("enabled") is False:
            decisions.append(ActivationDecision(name, False, "disabled_by_profile"))
            continue

        # 2) capability 검증 (deps ⊆ available) — silent fallback 금지.
        missing_cls = [c for c in meta.get("dependencies", []) if not capabilities.has_class(c)]
        if missing_cls:
            decisions.append(ActivationDecision(name, False, f"missing_capability:{'/'.join(missing_cls)}"))
            continue

        # 3) zone 요구 검증 (Phase 2 가드 정책 유지 — 미정의 시 비활성화).
        zname = meta.get("zone_name")
        if zname and zname not in zones_present:
            decisions.append(ActivationDecision(name, False, f"zone_not_defined:{zname}"))
            continue

        active.append(name)
        decisions.append(ActivationDecision(name, True))

    plan = ActivationPlan(stream_id=stream_id, active=tuple(active), decisions=tuple(decisions))
    for d in plan.decisions:
        if not d.active:
            logger.warning("[policy] %s DISABLED on %s: %s", d.name, stream_id, d.reason)
    return plan


# ============================================================================
# Config — 3계층 deep-merge (하드코딩 변수 제거)
# ============================================================================

def deep_merge(base: Dict[str, Any], override: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """재귀 dict 병합. override가 base를 부분 갱신한다 (ai_engine _deep_merge 동일)."""
    merged = deepcopy(base or {})
    for key, value in (override or {}).items():
        if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
            merged[key] = deep_merge(merged[key], value)
        else:
            merged[key] = deepcopy(value)
    return merged


def build_scenario_cfg(
    name: str,
    engine_defaults: Dict[str, Any],
    site_cfg: Optional[Dict[str, Any]] = None,
    stream_override: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """3계층 cfg 병합 (ai_engine 철학 — 모든 튜닝 파라미터는 config 계층에서).

    Layer 1: engine defaults   — app/scenarios/<name>/config.py의 DEFAULT_*_CONFIG
                                 (코드와 함께 버전 관리되는 기본값)
    Layer 2: site override     — /etc/contextra/scenario_config.py SCENARIO_DEFINITIONS
                                 (사이트 전체 공통 조정, git 리뷰 대상)
    Layer 3: per-stream        — streams.yaml inference_profile.scenarios[name].cfg
                                 (스트림별 미세 조정)

    Returns:
        병합된 cfg dict (엔진 __init__(cfg=...)로 전달).
    """
    merged = deep_merge(engine_defaults, site_cfg)
    if stream_override and isinstance(stream_override.get("cfg"), dict):
        merged = deep_merge(merged, stream_override["cfg"])
    return merged
