"""题目加载与分层采样。

从 benchmark JSON 目录加载题目，提供按对错比例的分层采样。
对应训练循环中的 DataLoader 角色。
"""

from __future__ import annotations

import json
import random
from typing import TYPE_CHECKING

from core.types import GeneratedQuestion

if TYPE_CHECKING:
    from pathlib import Path

    from core.types import QuestionUnit

_LEGACY_DEFAULT_DIFFICULTY = "medium"


def load_benchmark(questions_dir: Path) -> list[GeneratedQuestion]:
    """从 benchmark JSON 目录加载题目列表。

    video_id 优先使用题目 JSON 中的 ``video_id`` 字段；若缺失则回退到
    文件名（不含扩展名）。Video-MME benchmark 按视频拆文件（文件名即
    video_id），v2 生成题把多视频题目合并在单个 JSON 中（每条记录自带
    ``video_id``），两种格式均兼容。

    pair 契约字段（``pair_id`` / ``question_role`` / ``flip_axis`` / ``unit_id``）
    用 ``.get`` 读取：旧 benchmark 无这些键时退化为 single（``question_role``
    默认 "single"，``unit_id`` 留空由 __post_init__ 回填为 question_id），
    保证历史题库可无缝加载。

    参数:
        questions_dir: 包含 *.json 文件的目录路径。

    返回:
        按文件名排序加载的题目列表。
    """
    results: list[GeneratedQuestion] = []
    for path in sorted(questions_dir.glob("*.json")):
        fallback_video_id = path.stem
        with open(path, encoding="utf-8") as f:
            qa_list: list[dict] = json.load(f)
        for qa in qa_list:
            results.append(
                GeneratedQuestion(
                    question_id=qa["question_id"],
                    video_id=qa.get("video_id", fallback_video_id),
                    task_type=qa["task_type"],
                    question=qa["question"],
                    options=tuple(qa["options"]),
                    answer=qa["answer"],
                    source_nodes=tuple(qa.get("source_nodes", ())),
                    difficulty=qa.get("difficulty", _LEGACY_DEFAULT_DIFFICULTY),
                    family=qa.get("family"),
                    skill_target=qa.get("skill_target"),
                    difficulty_steps=qa.get("difficulty_steps"),
                    sub_pattern=qa.get("sub_pattern"),
                    # pair 契约字段：旧 benchmark 无这些键时按 single 默认兜底，
                    # unit_id 留空交由 GeneratedQuestion.__post_init__ 回填。
                    pair_id=qa.get("pair_id"),
                    question_role=qa.get("question_role", "single"),
                    flip_axis=qa.get("flip_axis"),
                    unit_id=qa.get("unit_id", ""),
                )
            )
    return results


def stratified_sample(
    questions: list[GeneratedQuestion],
    correctness: dict[str, bool],
    size: int,
    correct_ratio: float | None,
    task_types: list[str] | None,
    seed: int,
    min_per_class: int | None,
) -> list[GeneratedQuestion]:
    """按题型过滤后采样 size 个单元，可选按对错比例分层并按题型保底。

    参数:
        questions: 候选题目全集（single 与孪生对成员可混含）。
        correctness: question_id -> 基线是否答对（单元级正确性取成员 AND）。
        size: 采样单元总量（single 计 1、pair 计 1）。
        correct_ratio: 采样中"基线答对"单元的占比；None 表示自然分布。
        task_types: 限定题型；None 表示不限。
        seed: 随机种子，保证可复现。
        min_per_class: 每个题型补足到的单元下限；None 表示不补足。

    返回:
        采样后的题目列表（pair 单元展开为原始的两道题）。

    异常:
        ValueError: 自然分布时单元池不足 size，或分层时某层单元不足。

    关键实现:
        以 **QuestionUnit 为采样原子**（single 计 1、pair 计 1），size /
        correct_ratio / min_per_class 均按 unit 计数，孪生对两题永不被劈开。
        采样完成后 flatten_units 展开回逐题列表。纯 single 输入时 build_units
        与题目一一对应、顺序不变，rng 消耗与旧逐题实现完全一致（字节级回归）。

        build_units / flatten_units 采用函数内延迟导入：loader 属 question_gen，
        question_units 属 harness，而 harness 包初始化会反向 import question_gen，
        模块级导入将触发循环依赖（沿用 adversarial_filter 的既有做法）。
    """
    from app.harness.question_units import build_units, flatten_units

    rng = random.Random(seed)
    units = build_units(questions)
    pool = [u for u in units if task_types is None or u.task_type in task_types]

    if correct_ratio is None:
        if len(pool) < size:
            raise ValueError(f"自然分布采样不足: 需 {size} 个单元, 实有 {len(pool)} 个")
        sampled = rng.sample(pool, size)
    else:
        sampled = _ratio_stratified_sample(pool, correctness, size, correct_ratio, rng)

    if min_per_class is not None:
        sampled = _backfill_per_class(sampled, pool, min_per_class, rng)
    return flatten_units(sampled)


def _unit_correct(unit: QuestionUnit, correctness: dict[str, bool]) -> bool:
    """单元级正确性：成员全部答对才算对（缺失按 False，宽松口径）。

    参数:
        unit: 目标单元（single 1 题，pair 2 题）。
        correctness: question_id -> 基线是否答对。

    返回:
        pair 走双向 AND、single 即单题正确性；任一成员缺失或答错即 False。
    """
    return all(correctness.get(q.question_id, False) for q in unit.questions)


def _ratio_stratified_sample(
    pool: list[QuestionUnit],
    correctness: dict[str, bool],
    size: int,
    correct_ratio: float,
    rng: random.Random,
) -> list[QuestionUnit]:
    """按对错比例分层采样：对单元占 correct_ratio，其余为错单元。

    参数:
        pool: 题型过滤后的候选单元。
        correctness: question_id -> 基线是否答对。
        size: 采样单元总量。
        correct_ratio: 对单元占比。
        rng: 随机数发生器。

    返回:
        采样后的单元列表（对单元在前、错单元在后）。

    异常:
        ValueError: 对单元或错单元层不足。
    """
    correct = [u for u in pool if _unit_correct(u, correctness)]
    wrong = [u for u in pool if not _unit_correct(u, correctness)]
    n_correct = round(size * correct_ratio)
    n_wrong = size - n_correct
    if len(correct) < n_correct or len(wrong) < n_wrong:
        raise ValueError(
            f"分层不足: 需对{n_correct}/错{n_wrong}, 实有对{len(correct)}/错{len(wrong)}"
        )
    return rng.sample(correct, n_correct) + rng.sample(wrong, n_wrong)


def _backfill_per_class(
    sampled: list[QuestionUnit],
    pool: list[QuestionUnit],
    min_per_class: int,
    rng: random.Random,
) -> list[QuestionUnit]:
    """对候选池中出现的每个题型，将采样单元补足到 min_per_class 个。

    遍历对象是候选池 pool 里出现的全部题型（非仅 sampled 命中的），
    保证任意稀疏题型都能拿到足额样本。补足以 unit 为原子，孪生对整进整出。

    参数:
        sampled: 主采样结果单元（不修改，返回新列表）。
        pool: 候选单元全集（补足来源 + 题型枚举来源）。
        min_per_class: 每个题型的单元下限。
        rng: 随机数发生器。

    返回:
        补足后的单元列表。
    """
    selected_ids = {u.unit_id for u in sampled}
    result = list(sampled)
    counts: dict[str, int] = {}
    for u in sampled:
        counts[u.task_type] = counts.get(u.task_type, 0) + 1
    ordered_task_types: dict[str, None] = {}
    for u in pool:
        ordered_task_types.setdefault(u.task_type, None)
    for task_type in ordered_task_types:
        deficit = min_per_class - counts.get(task_type, 0)
        if deficit <= 0:
            continue
        candidates = [u for u in pool if u.task_type == task_type and u.unit_id not in selected_ids]
        take = rng.sample(candidates, min(deficit, len(candidates)))
        for u in take:
            selected_ids.add(u.unit_id)
            result.append(u)
    return result
