"""题族规格声明 — 定义 5 大问题家族及其采样、泄漏检测、提示模板约束。

每个 QuestionFamilySpec 对应一种失败机制（skill_target M1–M5），
由 get_family_for_slot 在出题时按权重分配。
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import random


@dataclass(frozen=True)
class LeakTestProfile:
    """泄漏测试配置 — 定义快捷答题捷径类型与通过阈值。

    Attributes:
        shortcut_type: 捷径类型标识。
        probe_template: store/prompts/question_gen/ 下的探测模板文件名。
        pass_threshold: 通过阈值（0–1），低于此值视为存在泄漏。
    """

    shortcut_type: str
    probe_template: str
    pass_threshold: float


@dataclass(frozen=True)
class SamplingConstraint:
    """采样约束 — 对树节点的最低要求。

    Attributes:
        min_subtitles: 最少字幕段数。
        min_l3_nodes: 最少 L3 节点数。
        require_frames: 是否要求帧图像可用。
        cross_l2_span: 是否要求跨 L2 段采样。
    """

    min_subtitles: int
    min_l3_nodes: int
    require_frames: bool
    cross_l2_span: bool


@dataclass(frozen=True)
class QuestionFamilySpec:
    """问题家族规格 — 一个家族的完整声明。

    Attributes:
        name: 家族标识名（如 "RETRIEVAL"）。
        skill_target: 目标失败机制编号（M1–M5）。
        sampling: 采样约束。
        legal_task_types: 该家族合法的任务类型集合（frozenset）。
        leak_profile: 泄漏测试配置。
        prompt_template: 出题 prompt 模板文件名。
    """

    name: str
    skill_target: str
    sampling: SamplingConstraint
    legal_task_types: frozenset[str]
    leak_profile: LeakTestProfile
    prompt_template: str


# ---------------------------------------------------------------------------
# 5 大家族实例
# ---------------------------------------------------------------------------

RETRIEVAL_FAMILY = QuestionFamilySpec(
    name="RETRIEVAL",
    skill_target="M1",
    sampling=SamplingConstraint(
        min_subtitles=2,
        min_l3_nodes=3,
        require_frames=False,
        cross_l2_span=False,
    ),
    legal_task_types=frozenset(
        [
            "Action Recognition",
            "Action Reasoning",
            "Action Prediction",
            "Action Sequence",
            "Object Recognition",
            "Object Reasoning",
            "Object Interaction",
            "Scene Understanding",
            "Event Reasoning",
            "Causal Reasoning",
            "Temporal Reasoning",
            "Spatial Reasoning",
        ]
    ),
    leak_profile=LeakTestProfile(
        shortcut_type="temporal_proximity",
        probe_template="gate_leak_retrieval.md",
        pass_threshold=0.6,
    ),
    prompt_template="retrieval.md",
)

REASONING_FAMILY = QuestionFamilySpec(
    name="REASONING",
    skill_target="M2",
    sampling=SamplingConstraint(
        min_subtitles=3,
        min_l3_nodes=4,
        require_frames=False,
        cross_l2_span=True,
    ),
    legal_task_types=frozenset(
        [
            "Action Reasoning",
            "Object Reasoning",
            "Event Reasoning",
            "Causal Reasoning",
            "Temporal Reasoning",
            "Spatial Reasoning",
        ]
    ),
    leak_profile=LeakTestProfile(
        shortcut_type="frequency",
        probe_template="gate_leak_reasoning.md",
        pass_threshold=0.5,
    ),
    prompt_template="reasoning.md",
)

ENUMERATION_FAMILY = QuestionFamilySpec(
    name="ENUMERATION",
    skill_target="M3",
    sampling=SamplingConstraint(
        min_subtitles=2,
        min_l3_nodes=5,
        require_frames=False,
        cross_l2_span=False,
    ),
    legal_task_types=frozenset(
        [
            "Action Sequence",
            "Object Recognition",
            "Object Interaction",
            "Scene Understanding",
        ]
    ),
    leak_profile=LeakTestProfile(
        shortcut_type="option_length",
        probe_template="gate_leak_enumeration.md",
        pass_threshold=0.6,
    ),
    prompt_template="enumeration.md",
)

VISUAL_FAMILY = QuestionFamilySpec(
    name="VISUAL",
    skill_target="M4",
    sampling=SamplingConstraint(
        min_subtitles=0,
        min_l3_nodes=3,
        require_frames=True,
        cross_l2_span=False,
    ),
    legal_task_types=frozenset(
        [
            "Object Recognition",
            "Scene Understanding",
            "Action Recognition",
            "Spatial Reasoning",
        ]
    ),
    leak_profile=LeakTestProfile(
        shortcut_type="visual_salience",
        probe_template="gate_leak_visual.md",
        pass_threshold=0.5,
    ),
    prompt_template="visual.md",
)

SPATIAL_FAMILY = QuestionFamilySpec(
    name="SPATIAL",
    skill_target="M5",
    sampling=SamplingConstraint(
        min_subtitles=0,
        min_l3_nodes=3,
        require_frames=True,
        cross_l2_span=False,
    ),
    legal_task_types=frozenset(
        [
            "Spatial Reasoning",
            "Object Interaction",
            "Scene Understanding",
        ]
    ),
    leak_profile=LeakTestProfile(
        shortcut_type="spatial_default",
        probe_template="gate_leak_spatial.md",
        pass_threshold=0.5,
    ),
    prompt_template="spatial.md",
)

ALL_FAMILIES: tuple[QuestionFamilySpec, ...] = (
    RETRIEVAL_FAMILY,
    REASONING_FAMILY,
    ENUMERATION_FAMILY,
    VISUAL_FAMILY,
    SPATIAL_FAMILY,
)

# 按 name 索引，用于 get_family_for_slot 快速查找
_FAMILY_BY_NAME: dict[str, QuestionFamilySpec] = {f.name: f for f in ALL_FAMILIES}


def get_family_for_slot(
    task_type: str,
    family_ratios: dict[str, float],
    rng: random.Random,
) -> QuestionFamilySpec:
    """根据任务类型和家族权重比例，随机选择一个合法的问题家族。

    Args:
        task_type: 任务类型字符串（必须是 12 种合法类型之一）。
        family_ratios: 家族名称到权重的映射（如 {"RETRIEVAL": 0.30, ...}）。
        rng: 随机数生成器实例（确保可复现）。

    Returns:
        被选中的 QuestionFamilySpec。

    Raises:
        ValueError: task_type 不在任何家族的 legal_task_types 中。
        ValueError: 给定 task_type 下没有合法家族（所有合法族权重为 0 或不在 ratios 中）。
    """
    # 检查 task_type 是否被任一家族接受
    all_legal_types: set[str] = set()
    for family in ALL_FAMILIES:
        all_legal_types.update(family.legal_task_types)
    if task_type not in all_legal_types:
        msg = f"task_type '{task_type}' 不在任何家族的合法类型中"
        raise ValueError(msg)

    # 过滤出接受该 task_type 且在 ratios 中有正权重的家族
    candidates: list[QuestionFamilySpec] = []
    weights: list[float] = []
    for family_name, weight in family_ratios.items():
        family = _FAMILY_BY_NAME.get(family_name)
        if family is None:
            continue
        if task_type in family.legal_task_types and weight > 0:
            candidates.append(family)
            weights.append(weight)

    if not candidates:
        msg = f"task_type '{task_type}' 下没有合法家族可选（检查 family_ratios）"
        raise ValueError(msg)

    # 归一化权重 + 加权随机选择
    chosen = rng.choices(candidates, weights=weights, k=1)[0]
    return chosen
