"""step 级续训 checkpoint：_TrainState 可持久化字段的序列化 / 反序列化。

_TrainState 的累加包均为扁平纯数据 dataclass，经 dataclasses.asdict 序列化为
纯 JSON dict；反序列化时用 Cls(**d) 还原，其中 SystemCasePack 含嵌套 CaseSample
列表、Probation 含嵌套 RejectedEdit 列表，需逐个重建。

不持久化的字段：gate_pools / baseline_cache（各自文件级自持久化，resume 时按
指纹重载）、best_*（从 manifest best 指针读）、global_step（存 progress 块，
由 train 单独赋值）。gate_epoch_observed 持久化：warm p-hat 在 gate_pools.json
幸存，观测开关须随行，否则 resume 后阶梯排序回退冷启动序。
"""

from __future__ import annotations

import json
import os
from dataclasses import asdict
from typing import TYPE_CHECKING, Any

from app.harness.validate import Probation
from core.evolution.types import (
    CaseSample,
    RejectedEdit,
    SystemCasePack,
    ToolCasePack,
)

if TYPE_CHECKING:
    from pathlib import Path

CHECKPOINT_SCHEMA_VERSION = 1

# ---------------------------------------------------------------------------
# 结构性 / 决策性指纹键
# ---------------------------------------------------------------------------

_STRUCTURAL_KEYS = (
    "batch_size",
    "min_class_per_batch",
    "epochs",
    "diag_size",
    "val_size",
    "batch_correct_ratio",
)

_DECISION_KEYS = (
    "edit_budget_start",
    "edit_budget_end",
    "early_stop_patience",
    "use_slow_momentum",
    "skill_update_mode",
    "appendix_consolidate_threshold",
    "momentum_samples",
    "gate_e_confirm",
    "gate_e_provisional",
    "gate_w_net_min",
    "gate_delta_min",
    "gate_lambda_dir",
    "gate_e_rollback",
    "gate_block",
    "gate_n_max",
    "gate_p_low",
    "gate_p_high",
    "gate_probe_quota",
    "gate_gamma_decay",
    "gate_cooldown_steps",
    "gate_guard_err",
)


# ---------------------------------------------------------------------------
# 序列化 / 反序列化
# ---------------------------------------------------------------------------


def serialize_state(state: Any) -> dict[str, Any]:
    """把 _TrainState 的可持久化字段转为纯 JSON dict。

    参数:
        state: _TrainState 实例（duck-typed，仅需含可持久化字段）。

    返回:
        纯 JSON 可序列化的 dict，不含 gate_pools / baseline_cache /
        best_* / global_step。

    关键实现细节:
        - changed_task_types_this_epoch 是 set，JSON 无 set，故 sorted 成有序列表。
        - dataclass 均经 asdict 递归转 dict（含 SystemCasePack 嵌套 CaseSample、
          Probation 嵌套 RejectedEdit）。
    """
    return {
        "correctness": state.correctness,
        "eval_prev_acc": state.eval_prev_acc,
        "eval_prev_run_id": state.eval_prev_run_id,
        "baseline_skills_version": state.baseline_skills_version,
        "baseline_prompts_version": state.baseline_prompts_version,
        "steps_since_best_improved": state.steps_since_best_improved,
        "epoch_start_skills": state.epoch_start_skills,
        "changed_task_types_this_epoch": sorted(state.changed_task_types_this_epoch),
        "rejected_buffer": {k: [asdict(x) for x in v] for k, v in state.rejected_buffer.items()},
        "system_packs": [asdict(x) for x in state.system_packs],
        "tool_packs": [asdict(x) for x in state.tool_packs],
        "probations": {t: asdict(p) for t, p in state.probations.items()},
        "gate_cooldown": state.gate_cooldown,
        "gate_epoch_observed": state.gate_epoch_observed,
    }


def _restore_system_pack(d: dict[str, Any]) -> SystemCasePack:
    """还原 SystemCasePack，含嵌套 CaseSample 列表。

    参数:
        d: asdict(SystemCasePack) 产出的 dict。

    返回:
        复活的 SystemCasePack；failure_cases / success_cases 重建为 CaseSample 实例。
    """
    return SystemCasePack(
        stats=d["stats"],
        failure_cases=[CaseSample(**c) for c in d["failure_cases"]],
        success_cases=[CaseSample(**c) for c in d["success_cases"]],
    )


def deserialize_state_fields(d: dict[str, Any]) -> dict[str, Any]:
    """把序列化 dict 还原为可填入 _TrainState 的字段字典（dataclass 复活）。

    参数:
        d: serialize_state 产出并经 JSON 往返的 dict。

    返回:
        字段名 -> 值的 dict，可直接铺到 _TrainState；其中各 dataclass 已复活、
        changed_task_types_this_epoch 还原为 set。

    关键实现细节:
        - RejectedEdit / ToolCasePack 字段均为标量/dict/list[dict]，Cls(**d) 直接构造。
        - SystemCasePack 含嵌套 CaseSample，交由 _restore_system_pack 重建。
        - Probation 含嵌套 RejectedEdit 列表（pending_edits），先重建内层再构造外层。
        - 直接取 d[...] 不用 .get 兜底：serialize 后的 checkpoint 必带全部键，
          缺键即 checkpoint 损坏，应硬失败（P5 不掩盖）。
    """
    return {
        "correctness": d["correctness"],
        "eval_prev_acc": d["eval_prev_acc"],
        "eval_prev_run_id": d["eval_prev_run_id"],
        "baseline_skills_version": d["baseline_skills_version"],
        "baseline_prompts_version": d["baseline_prompts_version"],
        "steps_since_best_improved": d["steps_since_best_improved"],
        "epoch_start_skills": d["epoch_start_skills"],
        "changed_task_types_this_epoch": set(d["changed_task_types_this_epoch"]),
        "rejected_buffer": {
            k: [RejectedEdit(**x) for x in v] for k, v in d["rejected_buffer"].items()
        },
        "system_packs": [_restore_system_pack(x) for x in d["system_packs"]],
        "tool_packs": [ToolCasePack(**x) for x in d["tool_packs"]],
        "probations": {
            t: Probation(
                **{
                    **d_p,
                    "pending_edits": [RejectedEdit(**x) for x in d_p["pending_edits"]],
                }
            )
            for t, d_p in d["probations"].items()
        },
        "gate_cooldown": d["gate_cooldown"],
        "gate_epoch_observed": d["gate_epoch_observed"],
    }


# ---------------------------------------------------------------------------
# 配置指纹
# ---------------------------------------------------------------------------


def compute_fingerprint(config: Any) -> dict[str, Any]:
    """采集影响训练轨迹的配置项（结构性 + 决策性）。

    参数:
        config: 训练配置对象（duck-typed，需含 _STRUCTURAL_KEYS + _DECISION_KEYS 属性）。

    返回:
        指纹 dict，键为配置项名，值为对应配置值。
    """
    return {k: getattr(config, k) for k in _STRUCTURAL_KEYS + _DECISION_KEYS}


def check_fingerprint(saved: dict[str, Any], config: Any) -> tuple[list[str], list[str]]:
    """比对保存的指纹与当前配置。返回 (结构性不一致项, 决策性不一致项)。

    参数:
        saved: checkpoint 中保存的 config_fingerprint。
        config: 当前训练配置对象。

    返回:
        (structural, decision) 两个不一致项名列表。

    关键实现细节:
        结构性不一致（batch_size/min_class_per_batch/epochs/diag_size/val_size/
        batch_correct_ratio）→ 调用方应拒绝 resume；决策性不一致 → 仅告警放行。
    """
    cur = compute_fingerprint(config)
    structural = [k for k in _STRUCTURAL_KEYS if saved.get(k) != cur[k]]
    decision = [k for k in _DECISION_KEYS if saved.get(k) != cur[k]]
    return structural, decision


# ---------------------------------------------------------------------------
# 读写 checkpoint
# ---------------------------------------------------------------------------


def write_checkpoint(
    workspace_dir: Path,
    *,
    state: Any,
    epoch: int,
    step_completed: int,
    phase: str,
    global_step: int,
    total_steps: int,
    version_snapshot: dict[str, str],
    epoch_batches: list[list[str]],
    config: Any,
) -> None:
    """原子写 checkpoint.json（.tmp 再 os.replace）。

    参数:
        workspace_dir: workspace 目录，checkpoint.json 写入其下。
        state: _TrainState 实例，交由 serialize_state 序列化。
        epoch: 当前 epoch 序号。
        step_completed: 本 epoch 内已完成的 step 数。
        phase: 续训阶段标识（如 "in_epoch"）。
        global_step: 全局 step 序号。
        total_steps: 全局总 step 数。
        version_snapshot: skills/prompts 版本快照。
        epoch_batches: 本 epoch 的 batch 划分（unit_id 列表的列表，孪生对折叠为
            单个 unit_id；纯非 AR 下 unit_id==question_id）。
        config: 训练配置对象，用于计算 config_fingerprint。

    关键实现细节:
        先写 checkpoint.json.tmp 再 os.replace，保证 checkpoint 不被写一半的中断破坏。
    """
    payload = {
        "schema_version": CHECKPOINT_SCHEMA_VERSION,
        "progress": {
            "epoch": epoch,
            "step_completed": step_completed,
            "phase": phase,
            "global_step": global_step,
            "total_steps": total_steps,
        },
        "version_snapshot": version_snapshot,
        "epoch_batches": epoch_batches,
        "config_fingerprint": compute_fingerprint(config),
        "state": serialize_state(state),
    }
    path = workspace_dir / "checkpoint.json"
    tmp = path.with_name("checkpoint.json.tmp")
    tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
    os.replace(tmp, path)


def load_checkpoint(workspace_dir: Path) -> dict[str, Any] | None:
    """读 checkpoint.json，不存在返回 None。

    参数:
        workspace_dir: workspace 目录。

    返回:
        checkpoint payload dict；checkpoint.json 不存在时返回 None。
    """
    path = workspace_dir / "checkpoint.json"
    if not path.exists():
        return None
    return json.loads(path.read_text())
