"""
Contextra Analytics — 베이스 추론 룰 (Base Inference Rules)  [MLOps 발판]

역할별 기본 모델 + 폴백 체인의 **단일 진실 공급원(SSOT)**.

왜 필요한가 (현재 상태의 문제):
- det/pose/seg 모델이 app/multi_pipeline.py에 하드코딩되어 있고,
  inference_engine.py에는 REST용 DEFAULT_MODEL_NAME만 따로 존재 → 분산.
- 폴백 체인이 0개라 기본 모델(예: RFDETRMedium)이 로드 실패/OOM 시
  그 역할의 추론이 통째로 죽음 (silent degradation).

이 모듈이 하는 일 (라이브 경로 무변경 — 선언 + 가시성만):
1. ROLES: det/pose/seg 각각 기본 모델 + 폴백 순서(큰→작은, 리소스 압박 시 강등) 정의.
2. resolve_active(role): 체인에서 Triton READY인 첫 모델을 반환 (결정적·테스트 가능).
3. rules_snapshot(): 활성 룰 + 폴백 상태를 dict로 노출 → /api/v1/inference-rules.

확장 포인트 (시나리오 팀/다음 단계용 — 지금은 미연결):
- multi_pipeline.py의 DET_CFG/POSE_CFG/SEG_CFG를 이 모듈의 resolve_active() 결과로
  대체하면, 모델 강등이 런타임에 자동 반영된다. 그 전환은 엔진 재검증 + 컨테이너
  재시작을 필요로 하므로 의도적으로 분리해 둔다 (MLOps: blast radius 최소화).

env 오버라이드 (운영자가 코드 수정 없이 기본 모델을 조정):
- DET_MODEL / POSE_MODEL / SEG_MODEL  → 해당 역할의 기본(첫) 모델 교체.
  폴백 체인은 코드에 고정(문서화) — 의도적으로 env로 안 열어둠 (오설정 방지).
"""

from __future__ import annotations

import os
from typing import Callable, Optional


# ---------------------------------------------------------------------------
# 역할별 추론 룰 정의
#   primary : 기본 모델 (env로 교체 가능)
#   fallback: primary가 READY 아님/실패 시 순서대로 강등 (큰→작은 = 정확도↓ 리소스↓)
# ---------------------------------------------------------------------------
ROLES: dict[str, dict] = {
    "det": {
        "primary_env": "DET_MODEL",
        "default_primary": "RFDETRMedium",
        # RF-DETR det 계열 — 정확도 우선, VRAM 압박 시 작은 모델로 강등.
        "fallback": ["RFDETRSmall", "RFDETRBase", "RFDETRNano"],
        # 명시적 업그레이드 후보 (per-stream profile 선택만 허용, 자동 폴백에 미사용).
        "allowed_extra": ["RFDETRLarge", "RFDETRFireSmoke", "Awiros_person_and_head_detection"],
    },
    "pose": {
        "primary_env": "POSE_MODEL",
        "default_primary": "RFDETRKeypointPreview",
        # 현재 keypoint 모델은 1종만 상주 → 폴백 없음 (추가 시 여기에 확장).
        "fallback": [],
        "allowed_extra": [],
    },
    "seg": {
        "primary_env": "SEG_MODEL",
        "default_primary": "RFDETRSegMedium",
        # seg 계열 — Medium 기본, 압박 시 Nano로 강등.
        "fallback": ["RFDETRSegSmall", "RFDETRSegNano"],
        # 명시적 업그레이드 후보 (per-stream profile 선택만 허용).
        "allowed_extra": ["RFDETRSegLarge", "RFDETRSegXLarge", "RFDETRSeg2XLarge"],
    },
}


def catalog_allowed(role: str) -> list[str]:
    """역할의 전체 허용 모델 집합 = primary + fallback + allowed_extra (중복 제거).

    per-stream profile 검증용. 폴백 *순서*(_chain)와 구분된다 —
    allowed_extra는 자동 강등 경로에 포함되지 않고 명시적 선택만 가능하다.
    """
    spec = ROLES[role]
    seen: set[str] = set()
    out: list[str] = []
    for m in [spec["default_primary"], *spec.get("fallback", []), *spec.get("allowed_extra", [])]:
        if m not in seen:
            seen.add(m)
            out.append(m)
    return out


def _chain(role: str) -> list[str]:
    """역할의 전체 후보 순서 = [primary(또는 env 오버라이드)] + fallback."""
    spec = ROLES[role]
    primary = os.environ.get(spec["primary_env"], spec["default_primary"])
    chain = [primary] + [m for m in spec["fallback"] if m != primary]
    # 중복 제거 (env로 primary를 fallback에 이미 있는 모델로 지정했을 때)
    seen: set[str] = set()
    out: list[str] = []
    for m in chain:
        if m not in seen:
            seen.add(m)
            out.append(m)
    return out


def resolve_active(
    role: str,
    is_ready: Optional[Callable[[str], bool]] = None,
) -> Optional[str]:
    """역할의 활성 모델 = 체인에서 Triton READY인 첫 모델.

    Args:
        role: "det" | "pose" | "seg"
        is_ready: 모델명 → READY 여부 (기본 engine.is_model_ready). 테스트용 주입 가능.

    Returns:
        READY인 첫 모델명, 하나도 없으면 None (호출부에서 no-op/에러 처리 결정).
    """
    if role not in ROLES:
        raise KeyError(f"unknown inference role: {role!r} (expected one of {list(ROLES)})")

    if is_ready is None:
        # 지연 import — Triton 없어도 모듈 import는 가능하게 유지.
        from app.inference_engine import engine  # noqa: WPS433 (lazy)
        is_ready = engine.is_model_ready

    for model in _chain(role):
        try:
            if is_ready(model):
                return model
        except Exception:
            continue
    return None


def rules_snapshot(
    is_ready: Optional[Callable[[str], bool]] = None,
) -> dict:
    """/api/v1/inference-rules용 스냅샷 — 활성 룰 + 폴백 상태 (읽기 전용)."""
    roles_out: dict[str, dict] = {}
    for role in ROLES:
        chain = _chain(role)
        active = resolve_active(role, is_ready=is_ready)
        ready_map = {m: bool(_safe_ready(m, is_ready)) for m in chain}
        roles_out[role] = {
            "primary": chain[0],
            "fallback_chain": chain,
            "active_model": active,
            "ready": ready_map,
            # primary가 죽고 fallback으로 강등된 상태인지 (운영자가 바로 알아야 함)
            "degraded": bool(active is not None and active != chain[0]),
        }

    return {
        "roles": roles_out,
        "note": (
            "베이스 추론 룰(SSOT). 현재는 선언+가시성만 — multi_pipeline의 "
            "nvinferserver config 전환은 엔진 재검증 후 별도 단계."
        ),
    }


def _safe_ready(model: str, is_ready: Optional[Callable[[str], bool]]) -> bool:
    if is_ready is None:
        from app.inference_engine import engine  # noqa: WPS433 (lazy)
        is_ready = engine.is_model_ready
    try:
        return bool(is_ready(model))
    except Exception:
        return False
