"""Store 版本操作 + Seed 管理。

Store 存储版本化资源（视频、题目、Skill、Prompt），
通过版本号（v1, v2, ...）管理资源的演化历史。
Seed 是可复现的实验起点，包含权重快照 + baseline 数据库。
"""

from __future__ import annotations

import json
import re
import shutil
import sqlite3
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from pathlib import Path


def _now_iso() -> str:
    """返回当前 UTC 时间的 ISO 格式字符串。"""
    return datetime.now(UTC).isoformat()


def _parse_version(name: str) -> int:
    """解析版本目录名 ``v\\d+`` 为整数。

    参数:
        name: 版本目录名，如 ``"v1"``、``"v10"``。

    返回:
        版本号整数。

    异常:
        ValueError: 版本目录名格式不合法（不匹配 ``v\\d+``）。
    """
    match = re.match(r"v(\d+)$", name)
    if not match:
        raise ValueError(f"无效版本号: {name}")
    return int(match.group(1))


def list_versions(store_dir: Path, resource_type: str) -> list[str]:
    """列出 Store 中某类资源的所有版本号，按数字值排序。

    按数字排序保证 v10 排在 v2 后面（而非字典序 v10 < v2）。

    参数:
        store_dir: Store 根目录。
        resource_type: 资源类型路径，如 ``"skills"``、``"questions/generated"``。

    返回:
        排序后的版本号列表，如 ``["v1", "v2", "v10"]``。
    """
    resource_dir = store_dir / resource_type
    if not resource_dir.is_dir():
        return []
    versions = []
    for entry in resource_dir.iterdir():
        if entry.is_dir() and re.match(r"v\d+$", entry.name):
            versions.append(entry.name)
    return sorted(versions, key=_parse_version)


def next_version(store_dir: Path, resource_type: str) -> str:
    """返回某类资源的下一个可用版本号。

    参数:
        store_dir: Store 根目录。
        resource_type: 资源类型路径。

    返回:
        下一个版本号字符串，如 ``"v3"``。
    """
    versions = list_versions(store_dir, resource_type)
    if not versions:
        return "v1"
    latest = _parse_version(versions[-1])
    return f"v{latest + 1}"


def _write_meta(target_dir: Path, version: str, source: str, **extra: str | None) -> None:
    """写入版本元数据文件 ``meta.json``。

    参数:
        target_dir: 版本目录。
        version: 版本号。
        source: 来源标识（``"manual"`` / ``"evolution"`` / ``"auto-gen"``）。
        **extra: 额外字段（parent, trigger_run, trigger_workspace, description）。
    """
    meta = {
        "version": version,
        "created_at": _now_iso(),
        "parent": extra.get("parent"),
        "source": source,
        "trigger_run": extra.get("trigger_run"),
        "trigger_workspace": extra.get("trigger_workspace"),
        "description": extra.get("description", ""),
    }
    (target_dir / "meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2))


def advance_version(
    store_dir: Path,
    resource_type: str,
    source_dir: Path,
    meta: dict,
) -> str:
    """将 source_dir 的内容写入 Store 的下一个版本目录，写入 meta.json。

    参数:
        store_dir: Store 根目录。
        resource_type: 资源类型路径，如 ``"skills"``、``"questions/generated"``。
        source_dir: 包含新版本资源文件的源目录。
        meta: 元数据字典，至少包含 ``source`` 字段。

    返回:
        新版本号字符串，如 ``"v2"``。
    """
    version = next_version(store_dir, resource_type)
    target = store_dir / resource_type / version
    shutil.copytree(source_dir, target)
    _write_meta(
        target,
        version,
        meta.get("source", "manual"),
        parent=meta.get("parent"),
        trigger_run=meta.get("trigger_run"),
        trigger_workspace=meta.get("trigger_workspace"),
        description=meta.get("description", ""),
    )
    logger.info("Store 版本推进: {}/{}", resource_type, version)
    return version


def init_store(
    store_dir: Path,
    videos_source: Path,
    skills_dir: Path,
    prompts_dir: Path,
) -> None:
    """初始化 Store：拷贝视频数据，创建 skills/v1、prompts/v1 和 questions 目录。

    参数:
        store_dir: Store 目标路径（不得已存在）。
        videos_source: 视频数据源目录。
        skills_dir: 初始 Skill 文件目录。
        prompts_dir: 初始 Prompt 文件目录。

    异常:
        FileExistsError: Store 目录已存在。
    """
    if store_dir.exists():
        raise FileExistsError(f"Store 已存在: {store_dir}")
    store_dir.mkdir(parents=True)
    shutil.copytree(videos_source, store_dir / "videos")
    (store_dir / "questions" / "benchmarks").mkdir(parents=True)
    (store_dir / "questions" / "generated").mkdir(parents=True)
    shutil.copytree(skills_dir, store_dir / "skills" / "v1")
    _write_meta(
        store_dir / "skills" / "v1",
        "v1",
        "manual",
        description="手工创建的初始版本",
    )
    shutil.copytree(prompts_dir, store_dir / "prompts" / "v1")
    _write_meta(
        store_dir / "prompts" / "v1",
        "v1",
        "manual",
        description="手工创建的初始版本",
    )
    logger.info("Store 初始化完成: {}", store_dir)


# ---------------------------------------------------------------------------
# 种子库（Seed）函数
# ---------------------------------------------------------------------------


def init_seed(
    store_dir: Path,
    name: str,
    skills_dir: Path,
    prompts_dir: Path,
    baseline_db: Path,
    baseline_run_id: str,
    parent: str | None,
    description: str,
) -> Path:
    """在 store/seeds/<name> 写一个种子：权重 + baseline.db + seed.json。

    参数:
        store_dir: Store 根目录。
        name: 种子名（如 ``'initial'``、``'from-evolve-v20'``）。
        skills_dir: 该版本 Skill 权重源目录。
        prompts_dir: 该版本 Prompt 权重源目录。
        baseline_db: 该版本全量记录 db（含 _runs + predictions 行）。
        baseline_run_id: 全量记录的 run_id，fresh 时注入 build_pools。
        parent: 来源（initial 为 None）。
        description: 人类可读说明。

    返回:
        种子目录路径。

    异常:
        FileExistsError: 同名种子已存在（不覆盖）。
    """
    seed_dir = store_dir / "seeds" / name
    if seed_dir.exists():
        raise FileExistsError(f"种子已存在，不覆盖: {seed_dir}")
    seed_dir.mkdir(parents=True)
    shutil.copytree(skills_dir, seed_dir / "skills")
    shutil.copytree(prompts_dir, seed_dir / "prompts")
    shutil.copy2(baseline_db, seed_dir / "baseline.db")
    (seed_dir / "seed.json").write_text(
        json.dumps(
            {
                "baseline_run_id": baseline_run_id,
                "parent": parent,
                "created_at": _now_iso(),
                "description": description,
            },
            ensure_ascii=False,
            indent=2,
        )
    )
    logger.info("种子创建完成: {}", seed_dir)
    return seed_dir


def list_seeds(store_dir: Path) -> list[str]:
    """列出 store/seeds 下所有种子名（按名排序）。

    参数:
        store_dir: Store 根目录。

    返回:
        种子名列表（仅含 seed.json 存在的目录），按名排序。
    """
    seeds_root = store_dir / "seeds"
    if not seeds_root.is_dir():
        return []
    return sorted(e.name for e in seeds_root.iterdir() if (e / "seed.json").exists())


def read_seed(store_dir: Path, name: str) -> dict:
    """读取种子 seed.json；不存在则报错。

    参数:
        store_dir: Store 根目录。
        name: 种子名。

    返回:
        seed.json 解析后的字典。

    异常:
        FileNotFoundError: 该种子不存在。
    """
    seed_json = store_dir / "seeds" / name / "seed.json"
    if not seed_json.exists():
        raise FileNotFoundError(f"种子不存在: {name}（{seed_json}）")
    return json.loads(seed_json.read_text())


def extract_run_db(src_db: Path, dst_db: Path, run_id: str) -> None:
    """从 src_db 抽出某 run_id 的 _runs + predictions 行，写一个最小 db（种子 baseline.db）。

    用源表的**原始 CREATE 语句**重建目标表，保留主键/列类型/约束——
    ``_runs.run_id TEXT PRIMARY KEY`` 是 HarnessLog ``INSERT OR IGNORE`` 去重的依据，
    若 seed db 丢主键则续训/fresh-bootstrap 的去重失效。

    参数:
        src_db: 源 harness.db。
        dst_db: 目标 db（不得已存在）。
        run_id: 要抽取的 run。

    异常:
        RuntimeError: 源中无该表或无该 run 的行。
    """
    src = sqlite3.connect(src_db)
    dst = sqlite3.connect(dst_db)
    try:
        for table in ("_runs", "predictions"):
            create_sql = src.execute(
                "SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
                (table,),
            ).fetchone()
            if create_sql is None or create_sql[0] is None:
                raise RuntimeError(f"源 db 无表 {table}")
            dst.execute(create_sql[0])
            cols = [r[1] for r in src.execute(f"PRAGMA table_info({table})")]
            col_sql = ", ".join(cols)
            rows = src.execute(
                f"SELECT {col_sql} FROM {table} WHERE run_id=?", (run_id,)
            ).fetchall()
            if not rows:
                raise RuntimeError(f"{table} 中无 run_id={run_id} 的行")
            ph = ", ".join("?" * len(cols))
            dst.executemany(f"INSERT INTO {table} ({col_sql}) VALUES ({ph})", rows)
        dst.commit()
    finally:
        dst.close()
        src.close()


def promote_to_seed(
    workspace_dir: Path,
    store_dir: Path,
    version: str,
    eval_run_id: str,
    name: str,
    description: str,
) -> Path:
    """把 workspace 的指定版本 + 配套 prompts + 指定 eval run 全量记录固化成新种子。

    强校验 eval_run_id 对应的 _runs 行中 skills_version 必须与 version 一致，
    且 skills_version/prompts_version 均不得为 NULL。

    参数:
        workspace_dir: 来源 workspace。
        store_dir: Store 根目录。
        version: skills 版本号。
        eval_run_id: canonical eval run（其 _runs 行提供配套 prompts 版本与全量记录）。
        name: 新种子名（冲突报错不覆盖）。
        description: 说明。

    返回:
        新种子目录。

    异常:
        ValueError: eval_run_id 不存在，或其 skills_version 与 version 不符，或版本为 NULL。
        FileExistsError: 同名种子已存在（由 init_seed 抛出）。
    """
    con = sqlite3.connect(workspace_dir / "harness.db")
    con.row_factory = sqlite3.Row
    try:
        row = con.execute(
            "SELECT skills_version, prompts_version FROM _runs WHERE run_id=?",
            (eval_run_id,),
        ).fetchone()
    finally:
        con.close()

    if row is None:
        raise ValueError(f"eval run 不存在: {eval_run_id}")

    skills_v, prompts_v = row["skills_version"], row["prompts_version"]

    # 强校验——eval run 的版本必须与 --version 一致，且不得为 NULL
    if skills_v is None or prompts_v is None:
        raise ValueError(f"eval run {eval_run_id} 的 _runs 版本对为 NULL（未回填？），无法 promote")
    if skills_v != version:
        raise ValueError(f"eval run {eval_run_id} 的版本 {skills_v} 与 --version {version} 不符")

    tmp_db = workspace_dir / "_promote_tmp.db"
    if tmp_db.exists():
        tmp_db.unlink()
    extract_run_db(workspace_dir / "harness.db", tmp_db, eval_run_id)
    try:
        seed_dir = init_seed(
            store_dir,
            name,
            workspace_dir / "skills" / skills_v,
            workspace_dir / "prompts" / prompts_v,
            tmp_db,
            baseline_run_id=eval_run_id,
            parent=f"{workspace_dir.name}:{version}",
            description=description,
        )
    finally:
        tmp_db.unlink()

    logger.info("Promote 完成: {} -> {}", workspace_dir.name, seed_dir)
    return seed_dir
