"""运行配置：RunConfig frozen dataclass 与 YAML + CLI + .env 三层加载。

三层合并优先级：CLI > .env > YAML（遵循 CLAUDE.md §4.5 配置管理规范）。
- YAML：科研实验配置（会在实验中反复扫动的参数），存放于 config/ 下。
- .env：工程配置（少变路径如 workspace_dir、store_dir），通过环境变量注入。
- CLI：单次临时覆盖。
"""

from __future__ import annotations

import dataclasses
import os
from dataclasses import dataclass
from pathlib import Path

import yaml

_VALID_MODES = {"infer", "train", "diagnose", "evolve", "eval", "promote"}
_VALID_SKILL_MODES = {"auto", "manual", "none"}
_VALID_SKILL_UPDATE_MODES = {"patch", "rewrite"}
_VALID_POOL_SPLIT_MODES = {"global", "per_category"}
_PATH_FIELDS = {"workspace_dir", "store_dir"}

# Video-MME 的任务类型数量：验证池每类至少保底 eval_min_per_class 题，共 12 类。
_VIDEO_MME_TASK_TYPE_COUNT = 12

# .env 工程配置字段映射（环境变量名 → RunConfig 字段名）。
# 仅路径类工程配置走 .env，科研实验参数走 YAML。
_ENV_FIELD_MAP: dict[str, str] = {
    "HARNESS_WORKSPACE_DIR": "workspace_dir",
    "HARNESS_STORE_DIR": "store_dir",
}


@dataclass(frozen=True)
class RunConfig:
    """实验运行配置，所有参数的唯一归口。

    frozen=True 确保配置在创建后不可变，防止运行中被意外修改。
    三层合并优先级：CLI > .env > YAML。

    字段:
        workspace_dir: Workspace 根目录。
        store_dir: Store 根目录。
        mode: 运行模式，"infer" / "train" / "diagnose" / "evolve" / "eval" / "promote"。
        concurrency: 并行 worker 数。
        max_steps: AgentLoop 单题最大步数。
        skill_mode: Skill 加载模式，"auto" / "manual" / "none"。
        n_samples: 题目截取数，0 表示全量。
        questions: 题目在 questions/ 下的相对路径。
        skills_version: Skills 版本号。
        prompts_version: Prompts 版本号。
        epochs: 训练轮数。
        diag_size: 诊断池题目数。
        diag_correct_ratio: 诊断池中正确题目占比。
        val_size: 验证池题目数。
        val_correct_ratio: 验证池中正确题目占比。
        edit_budget_start: 编辑预算前期上限。
        edit_budget_end: 编辑预算后期下限。
        batch_size: mini-batch 单批题目数。
        min_class_per_batch: 单批中每个任务类型至少保留的题目数（< batch_size）。
        eval_min_per_class: 验证池中每个任务类型至少保底的题目数。
        early_stop_patience: 全局 best 连续未提升的容忍轮数，达到即早停。
        test_size: held-out 测试池题目数。
        use_slow_momentum: 是否启用快慢双速进化中的慢速 momentum 更新。
        gate_e_confirm: CE-Gate CONFIRMED 接受的 e 值门槛（1/alpha，Ville 界假阳率 alpha）。
        gate_e_provisional: 题尽暂定接受门 + futility 提前止损的代数界。
        gate_w_net_min: 题尽暂定接受要求的最小净胜数（win - loss）。
        gate_delta_min: 最小点估计效应量下限（承接旧 margin 语义）。
        gate_lambda_dir: Wald 方向拒绝的对数似然比阈值（必须为负）。
        gate_e_rollback: 试用期对称回滚门（回滚 e 值门槛）。
        gate_block: 块序贯验证的块大小（=推理并发度，块内跑满）。
        gate_n_max: 单次 gate 消耗的题数上限。
        gate_p_low: 信息量阶梯 p-hat 保留区间下界（剔除必错零信息题）。
        gate_p_high: 信息量阶梯 p-hat 保留区间上界（剔除必对零信息题）。
        gate_probe_quota: 冷启动探针集比例（全错题中插尾的比例）。
        gate_gamma_decay: 逐题正确率估计 p-hat 的 EMA 衰减系数。
        gate_cooldown_steps: 回滚后该题型跳过进化的冷却 step 数。
        gate_guard_err: gate 内跨块累计 INFRA 错误率护栏。
        skill_update_mode: skill 进化模式，"patch"（局部 edit）/ "rewrite"（整篇重写）。
        appendix_consolidate_threshold: appendix note 条数达此值触发 LLM consolidation。
        run_id: diagnose/evolve 模式要分析的运行 ID，默认空字符串。
        batch_correct_ratio: 单批中正确题目占比，范围 [0, 1)。
        momentum_samples: 慢速 momentum 更新时从诊断池采样的题目数，必须 >= 1。
        seed: fresh 训练的种子名（对应 seed.json），默认 "initial"。
        version: eval/promote 模式指定的 store 版本号（如 "v3"）。
        resume: train 模式是否从已有 checkpoint 续训。
        fresh: train 模式是否从种子全新开始。
        task_types: 限定参与的任务类型子集，None 表示全部。
        pool_split_mode: 池划分策略，"global"（全局统一划分）/ "per_category"（按类别独立划分）。
        train_ratio: 训练集占比，范围 (0, 1)。
        test_questions: 测试题目集路径（相对路径）。
        run_holdout_eval: 是否在 epoch 末执行 held-out 四向评估，默认 True。
    """

    # ── 必填字段（无默认值，来自 YAML 或 CLI） ──
    workspace_dir: Path
    store_dir: Path
    mode: str
    concurrency: int
    max_steps: int
    skill_mode: str
    n_samples: int
    questions: str
    skills_version: str
    prompts_version: str
    epochs: int
    diag_size: int
    diag_correct_ratio: float
    val_size: int
    val_correct_ratio: float
    edit_budget_start: int
    edit_budget_end: int
    batch_size: int
    min_class_per_batch: int
    eval_min_per_class: int
    early_stop_patience: int
    test_size: int
    use_slow_momentum: bool
    gate_e_confirm: float
    gate_e_provisional: float
    gate_w_net_min: int
    gate_delta_min: float
    gate_lambda_dir: float
    gate_e_rollback: float
    gate_block: int
    gate_n_max: int
    gate_p_low: float
    gate_p_high: float
    gate_probe_quota: float
    gate_gamma_decay: float
    gate_cooldown_steps: int
    gate_guard_err: float
    skill_update_mode: str
    appendix_consolidate_threshold: int

    # ── 有默认值的字段（通常由 CLI 传入或可选） ──
    run_id: str = ""
    batch_correct_ratio: float = 0.5
    momentum_samples: int = 20
    seed: str = "initial"
    version: str = ""
    resume: bool = False
    fresh: bool = False
    task_types: tuple[str, ...] | None = None
    pool_split_mode: str = "global"
    train_ratio: float = 0.667
    test_questions: str = "benchmarks/Video-MME"
    run_holdout_eval: bool = True


def _validate(config: RunConfig) -> None:
    """校验 RunConfig 全部字段约束。

    六层校验链：mode → 基础标量 → 编辑预算 → mini-batch → gate 阈值 → gate 阶梯。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 任一字段值不合法。
    """
    _validate_mode(config)
    _validate_mode_deps(config)
    _validate_basic(config)
    _validate_edit_budget(config)
    _validate_minibatch(config)
    _validate_gate(config)


def _validate_mode(config: RunConfig) -> None:
    """校验运行模式枚举合法性。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: mode 值不在合法集合中。
    """
    if config.mode not in _VALID_MODES:
        raise ValueError(f"mode 必须为 {_VALID_MODES} 之一，实际: {config.mode!r}")


def _validate_mode_deps(config: RunConfig) -> None:
    """校验各运行模式的依赖字段（run_id、version）。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 模式依赖字段缺失。
    """
    if config.mode in ("diagnose", "evolve") and not config.run_id:
        raise ValueError(f"mode 为 {config.mode!r} 时必须提供 run_id。")
    if config.mode in ("eval", "promote") and not config.version:
        raise ValueError(f"mode 为 {config.mode!r} 时必须提供 --version。")
    if config.mode == "promote" and not config.run_id:
        raise ValueError("promote 必须提供 --run-id（指定 canonical eval run）。")
    _validate_train_run_id(config)


def _validate_train_run_id(config: RunConfig) -> None:
    """校验 train 模式非 resume/fresh 时必须提供 run_id。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: train 模式既非 resume 也非 fresh 且缺少 run_id。
    """
    if config.mode != "train":
        return
    if config.resume or config.fresh:
        return
    if not config.run_id:
        raise ValueError("train 非 resume/fresh 时必须提供 run_id（旧式基线 run）。")


def _validate_basic(config: RunConfig) -> None:
    """校验基础标量字段：枚举合法性与正整数约束。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 任一基础字段值不合法。
    """
    if config.skill_mode not in _VALID_SKILL_MODES:
        raise ValueError(
            f"skill_mode 必须为 {_VALID_SKILL_MODES} 之一，实际: {config.skill_mode!r}"
        )
    if config.concurrency <= 0:
        raise ValueError(f"concurrency 必须 > 0，实际: {config.concurrency}")
    if config.max_steps <= 0:
        raise ValueError(f"max_steps 必须 > 0，实际: {config.max_steps}")
    if config.n_samples < 0:
        raise ValueError(f"n_samples 必须 >= 0，实际: {config.n_samples}")
    if config.epochs <= 0:
        raise ValueError(f"epochs 必须 > 0，实际: {config.epochs}")
    if config.skill_update_mode not in _VALID_SKILL_UPDATE_MODES:
        raise ValueError(
            f"skill_update_mode 必须为 {_VALID_SKILL_UPDATE_MODES} 之一，"
            f"实际: {config.skill_update_mode!r}"
        )
    if config.appendix_consolidate_threshold < 1:
        raise ValueError(
            f"appendix_consolidate_threshold 必须 >= 1，"
            f"实际: {config.appendix_consolidate_threshold}"
        )
    if config.pool_split_mode not in _VALID_POOL_SPLIT_MODES:
        raise ValueError(
            f"pool_split_mode 必须为 {_VALID_POOL_SPLIT_MODES} 之一，"
            f"实际: {config.pool_split_mode!r}"
        )
    if not (0 < config.train_ratio < 1):
        raise ValueError(f"train_ratio 必须在 (0, 1) 内，实际: {config.train_ratio}")


def _validate_edit_budget(config: RunConfig) -> None:
    """校验编辑预算退火的前期/后期上限约束。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: edit_budget_start < edit_budget_end，或 end <= 0。
    """
    if config.edit_budget_start < config.edit_budget_end:
        raise ValueError(
            f"edit_budget_start({config.edit_budget_start}) 必须 >= "
            f"edit_budget_end({config.edit_budget_end})"
        )
    if config.edit_budget_end <= 0:
        raise ValueError(f"edit_budget_end 必须 > 0，实际: {config.edit_budget_end}")


def _validate_minibatch(config: RunConfig) -> None:
    """校验 mini-batch 自进化闭环参数约束。

    参数:
        config: 待校验的 RunConfig 配置对象。

    异常:
        ValueError: 任一约束被违反。

    关键实现细节:
        pool_split_mode != "per_category" 时，val_size 必须 >= eval_min_per_class *
        _VIDEO_MME_TASK_TYPE_COUNT，保证验证池能为 Video-MME 的全部 12 个任务类型
        各保底 eval_min_per_class 题。per_category 模式下跳过此硬编码 12 类保底检查。
    """
    if config.batch_size <= 0:
        raise ValueError(f"batch_size 必须 > 0，实际: {config.batch_size}")
    if not (1 <= config.min_class_per_batch < config.batch_size):
        raise ValueError(
            f"min_class_per_batch 必须满足 1 <= 值 < batch_size"
            f"({config.batch_size})，实际: {config.min_class_per_batch}"
        )
    if config.eval_min_per_class < 1:
        raise ValueError(f"eval_min_per_class 必须 >= 1，实际: {config.eval_min_per_class}")
    if config.pool_split_mode != "per_category":
        floor = config.eval_min_per_class * _VIDEO_MME_TASK_TYPE_COUNT
        if config.val_size < floor:
            raise ValueError(
                f"val_size 必须 >= eval_min_per_class * {_VIDEO_MME_TASK_TYPE_COUNT}"
                f"(={floor})：Video-MME 共 {_VIDEO_MME_TASK_TYPE_COUNT} 个任务类型，"
                f"每类需 eval_min_per_class 题保底，故验证池下限为 {floor}，"
                f"实际: {config.val_size}"
            )
    if config.early_stop_patience <= 0:
        raise ValueError(f"early_stop_patience 必须 > 0，实际: {config.early_stop_patience}")
    if config.test_size <= 0:
        raise ValueError(f"test_size 必须 > 0，实际: {config.test_size}")
    if not (0 <= config.batch_correct_ratio < 1):
        raise ValueError(
            f"batch_correct_ratio 必须满足 0 <= 值 < 1，实际: {config.batch_correct_ratio}"
        )
    if config.momentum_samples < 1:
        raise ValueError(f"momentum_samples 必须 >= 1，实际: {config.momentum_samples}")


def _validate_gate(config: RunConfig) -> None:
    """校验 CE-Gate 全部参数：判据阈值 + 信息量阶梯。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 任一 gate 参数不合法。
    """
    _validate_gate_thresholds(config)
    _validate_gate_ladder(config)


def _validate_gate_thresholds(config: RunConfig) -> None:
    """校验 CE-Gate 判据阈值参数（e 值、净胜数、效应量、方向拒绝）。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 任一阈值参数不合法。
    """
    if config.gate_e_confirm <= 1:
        raise ValueError(f"gate_e_confirm 必须 > 1，实际: {config.gate_e_confirm}")
    if not (1 < config.gate_e_provisional <= config.gate_e_confirm):
        raise ValueError(
            f"gate_e_provisional 必须在 (1, gate_e_confirm] 内，实际: {config.gate_e_provisional}"
        )
    if config.gate_e_rollback <= 1:
        raise ValueError(f"gate_e_rollback 必须 > 1，实际: {config.gate_e_rollback}")
    if config.gate_w_net_min < 1:
        raise ValueError(f"gate_w_net_min 必须 >= 1，实际: {config.gate_w_net_min}")
    if config.gate_delta_min < 0:
        raise ValueError(f"gate_delta_min 必须 >= 0，实际: {config.gate_delta_min}")
    if config.gate_lambda_dir >= 0:
        raise ValueError(f"gate_lambda_dir 必须 < 0，实际: {config.gate_lambda_dir}")


def _validate_gate_ladder(config: RunConfig) -> None:
    """校验 CE-Gate 信息量阶梯与块序贯参数。

    参数:
        config: 待校验的配置实例。

    异常:
        ValueError: 任一阶梯参数不合法。
    """
    if config.gate_block <= 0 or config.gate_n_max < config.gate_block:
        raise ValueError(
            f"需 0 < gate_block <= gate_n_max，"
            f"实际: block={config.gate_block}, n_max={config.gate_n_max}"
        )
    if not (0 <= config.gate_p_low < config.gate_p_high <= 1):
        raise ValueError(
            f"需 0 <= gate_p_low < gate_p_high <= 1，"
            f"实际: [{config.gate_p_low}, {config.gate_p_high}]"
        )
    if not (0 <= config.gate_probe_quota <= 1):
        raise ValueError(f"gate_probe_quota 须在 [0,1]，实际: {config.gate_probe_quota}")
    if not (0 < config.gate_gamma_decay < 1):
        raise ValueError(f"gate_gamma_decay 须在 (0,1)，实际: {config.gate_gamma_decay}")
    if config.gate_cooldown_steps < 1:
        raise ValueError(f"gate_cooldown_steps 必须 >= 1，实际: {config.gate_cooldown_steps}")
    if not (0 < config.gate_guard_err < 1):
        raise ValueError(f"gate_guard_err 须在 (0,1)，实际: {config.gate_guard_err}")


def _apply_env_var_overrides(data: dict) -> None:
    """从环境变量覆盖路径字段（原地修改）。

    .env 文件由入口脚本 load_dotenv 加载到环境变量，本函数仅从 os.environ 读取。
    仅覆盖 _ENV_FIELD_MAP 中声明的工程配置字段（workspace_dir、store_dir）。

    参数:
        data: 待覆盖的配置字典。
    """
    for env_key, field_name in _ENV_FIELD_MAP.items():
        env_val = os.environ.get(env_key)
        if env_val is not None:
            data[field_name] = env_val


def load_config(
    yaml_path: Path,
    cli_overrides: dict[str, object] | None = None,
) -> RunConfig:
    """从 YAML 加载配置，叠加 .env 和 CLI 覆盖层后构造 RunConfig。

    三层合并优先级：CLI > .env > YAML。

    参数:
        yaml_path: YAML 配置文件路径，需包含 ``harness`` 段。
        cli_overrides: CLI 参数字典，值为 None 表示未传入（不覆盖）。

    返回:
        构造并校验后的 RunConfig 实例。

    异常:
        FileNotFoundError: YAML 文件不存在。
        ValueError: 校验失败。
    """
    # Phase 1: 加载 YAML 基础层
    with open(yaml_path, encoding="utf-8") as f:
        raw: dict = yaml.safe_load(f)

    # 支持嵌套 harness 段和扁平 YAML 两种格式
    yaml_data: dict = raw.get("harness", raw)

    # Phase 2: .env 覆盖层（仅工程配置字段）
    _apply_env_var_overrides(yaml_data)

    # Phase 3: CLI 覆盖层（最高优先级）
    valid_fields = {f.name for f in dataclasses.fields(RunConfig)}
    if cli_overrides:
        for key, value in cli_overrides.items():
            if value is not None and key in valid_fields:
                yaml_data[key] = value

    # Phase 4: 类型转换 — 路径字段转 Path
    for field_name in _PATH_FIELDS:
        if field_name in yaml_data:
            yaml_data[field_name] = Path(yaml_data[field_name])

    # Phase 4b: 类型转换 — task_types list → tuple
    if "task_types" in yaml_data and yaml_data["task_types"] is not None:
        yaml_data["task_types"] = tuple(yaml_data["task_types"])

    # Phase 5: 构造并校验
    config = RunConfig(**{k: v for k, v in yaml_data.items() if k in valid_fields})
    _validate(config)
    return config
