#!/usr/bin/env python3
"""批量并行建树入口：多视频并发构建三层 TreeIndex。

并发模型（Spec-2）:
    视频级 Semaphore(video_concurrency) + gather —— 复刻 repair_trees.py 惯例；
    全局共享一个 API Semaphore(api_concurrency) 注入所有 VideoTreeBuilder，
    端点压力与单视频建树完全一致，吞吐提升来自非 API 阶段跨视频重叠。

用法:
    conda activate Video-Tree-TRM
    python tools/build_trees.py --videos-dir <dir> [--out-dir store/videos]
        [--srt-dir <dir>] [--video-concurrency 16] [--limit 0]

api_concurrency 为工程配置，从 .env 读取 TREE_BUILD_API_CONCURRENCY（默认 16）。
app/core/adapters 不 import 此脚本。
"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import sys
import time
from pathlib import Path
from typing import TYPE_CHECKING

# 确保项目根目录在 sys.path 中
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

import yaml  # noqa: E402
from dotenv import load_dotenv  # noqa: E402
from loguru import logger  # noqa: E402

load_dotenv(PROJECT_ROOT / ".env")

from app.tree.config import TreeConfig  # noqa: E402
from app.tree.index import TreeIndex  # noqa: E402
from app.tree.subtitle import parse_srt  # noqa: E402
from app.tree.video_builder import VideoTreeBuilder  # noqa: E402

if TYPE_CHECKING:
    from adapters.llm import GovernedLLMClient
    from adapters.vlm import GovernedVLMClient
    from app.tree.subtitle import SRTEntry

# ---------------------------------------------------------------------------
# 日志配置：不缓存，立即输出
# ---------------------------------------------------------------------------

logger.remove()
logger.add(
    sys.stderr,
    format="{time:HH:mm:ss} | {level:<7} | {message}",
    level="DEBUG",
    colorize=True,
)
logger.add(
    PROJECT_ROOT / "logs" / "build_trees.log",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level:<7} | {message}",
    level="DEBUG",
    rotation="50 MB",
)

# ---------------------------------------------------------------------------
# 断点续跑 — progress 文件管理（复刻 repair_trees.py 惯例）
# ---------------------------------------------------------------------------

PROGRESS_FILE = "build_progress.json"

_VIDEO_SUFFIXES = frozenset({".mp4", ".mkv", ".avi", ".webm"})


def load_progress(path: Path) -> set[str]:
    """读取 progress 文件，返回已完成视频 ID 集合。

    参数:
        path: progress JSON 文件路径。

    返回:
        已完成视频 ID 集合。文件不存在或损坏时返回空集。
    """
    if not path.exists():
        return set()
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        return set(data.get("finished_video_ids", []))
    except (json.JSONDecodeError, KeyError, TypeError, AttributeError):
        logger.warning("progress 文件损坏，忽略: {}", path)
        return set()


async def save_progress(path: Path, lock: asyncio.Lock, vid: str) -> None:
    """原子追加一个视频 ID 到 progress 文件。

    参数:
        path: progress JSON 文件路径。
        lock: asyncio.Lock，防并发读改写丢更新。
        vid: 要追加的视频 ID。
    """
    async with lock:
        finished = load_progress(path)
        finished.add(vid)
        tmp = path.with_suffix(".tmp")
        tmp.write_text(
            json.dumps({"finished_video_ids": sorted(finished)}, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )
        os.replace(str(tmp), str(path))


# ---------------------------------------------------------------------------
# 待建发现与完整性校验
# ---------------------------------------------------------------------------


def _tree_is_complete(tree_path: Path) -> bool:
    """判断 tree.json 是否存在且可加载为非空树。

    参数:
        tree_path: tree.json 路径。

    返回:
        True 表示完整（跳过重建）；文件缺失/损坏/空树返回 False。
    """
    if not tree_path.exists():
        return False
    try:
        index = TreeIndex.load_json(str(tree_path))
    except (json.JSONDecodeError, KeyError, TypeError, ValueError, AssertionError) as exc:
        logger.warning("tree.json 无法加载，视为不完整: {} ({})", tree_path, exc)
        return False
    return len(index.roots) > 0


def _discover_pending(
    videos_dir: Path,
    out_dir: Path,
    finished: set[str],
) -> list[Path]:
    """扫描视频目录，返回待建视频文件列表（按文件名排序）。

    跳过条件：video_id 在 progress 中，或 out_dir/<video_id>/tree.json 完整。

    参数:
        videos_dir: 视频文件目录。
        out_dir: 树输出根目录。
        finished: progress 中已完成的视频 ID 集合。

    返回:
        待建视频文件路径列表。
    """
    pending: list[Path] = []
    for f in sorted(videos_dir.iterdir()):
        if not f.is_file() or f.suffix.lower() not in _VIDEO_SUFFIXES:
            continue
        vid = f.stem
        if vid in finished:
            continue
        if _tree_is_complete(out_dir / vid / "tree.json"):
            continue
        pending.append(f)
    return pending


def _find_srt_entries(video_path: Path, srt_dir: Path) -> list[SRTEntry] | None:
    """按视频同名规则查找并解析 SRT 字幕。

    参数:
        video_path: 视频文件路径。
        srt_dir: SRT 目录。

    返回:
        SRTEntry 列表；无同名 .srt 时返回 None。
    """
    srt_path = srt_dir / f"{video_path.stem}.srt"
    if not srt_path.exists():
        return None
    return parse_srt(str(srt_path))


# ---------------------------------------------------------------------------
# LLM/VLM 客户端构建（复刻 repair_trees.py 惯例）
# ---------------------------------------------------------------------------


def _build_clients(api_concurrency: int) -> tuple[GovernedLLMClient, GovernedVLMClient]:
    """构建 GovernedLLMClient（LLM + VLM），熔断阈值随 API 并发缩放。

    参数:
        api_concurrency: 全局 API 并发上限（熔断阈值取 max(cfg, api_concurrency*2)）。

    返回:
        (llm_client, vlm_client) 元组。
    """
    from adapters.breaker import CircuitBreaker
    from adapters.llm import GovernedLLMClient
    from adapters.telemetry import SQLiteTelemetryRecorder
    from adapters.vlm import GovernedVLMClient

    (PROJECT_ROOT / "logs").mkdir(exist_ok=True)
    telemetry = SQLiteTelemetryRecorder(str(PROJECT_ROOT / "logs" / "build_trees_telemetry.db"))

    breaker_threshold = int(os.getenv("LLM_CIRCUIT_BREAKER_THRESHOLD", "5"))
    breaker_threshold = max(breaker_threshold, api_concurrency * 2)
    breaker_cooldown = int(os.getenv("LLM_CIRCUIT_BREAKER_COOLDOWN", "60"))
    timeout_s = float(os.getenv("LLM_TIMEOUT", "120"))
    max_retries = int(os.getenv("LLM_MAX_RETRIES", "3"))
    base_delay = float(os.getenv("LLM_RETRY_BASE_DELAY", "2.0"))
    max_delay = float(os.getenv("LLM_RETRY_MAX_DELAY", "30.0"))
    ttft = float(os.getenv("LLM_TTFT_TIMEOUT", "30"))
    inter_token = float(os.getenv("LLM_INTER_TOKEN_TIMEOUT", "15"))

    llm = GovernedLLMClient(
        model=os.environ["SEARCH_LLM_MODEL"],
        base_url=os.environ["SEARCH_LLM_BASE_URL"],
        api_key=os.environ["SEARCH_LLM_API_KEY"],
        provider="deepseek",
        thinking=False,
        breaker=CircuitBreaker(fail_threshold=breaker_threshold, cooldown_s=breaker_cooldown),
        cache=None,
        telemetry=telemetry,
        timeout_s=timeout_s,
        ttft_timeout_s=ttft,
        inter_token_timeout_s=inter_token,
        max_retries=max_retries,
        retry_base_delay_s=base_delay,
        retry_max_delay_s=max_delay,
    )
    vlm_base = GovernedLLMClient(
        model=os.environ["VL_LLM_MODEL"],
        base_url=os.environ["VL_LLM_BASE_URL"],
        api_key=os.environ["VL_LLM_API_KEY"],
        provider="qwen",
        thinking=False,
        breaker=CircuitBreaker(fail_threshold=breaker_threshold, cooldown_s=breaker_cooldown),
        cache=None,
        telemetry=telemetry,
        timeout_s=timeout_s,
        ttft_timeout_s=ttft,
        inter_token_timeout_s=inter_token,
        max_retries=max_retries,
        retry_base_delay_s=base_delay,
        retry_max_delay_s=max_delay,
    )
    return llm, GovernedVLMClient(vlm_base)


# ---------------------------------------------------------------------------
# 主编排
# ---------------------------------------------------------------------------


async def main_async(args: argparse.Namespace) -> None:
    """异步主流程：视频级并发建树 + 全局共享 API 信号量。

    参数:
        args: CLI 参数（videos_dir/out_dir/srt_dir/video_concurrency/limit/progress_path）。
    """
    videos_dir = Path(args.videos_dir)
    out_dir = Path(args.out_dir)
    srt_dir = Path(args.srt_dir)
    assert videos_dir.is_dir(), f"视频目录不存在: {videos_dir}"

    api_concurrency = int(os.getenv("TREE_BUILD_API_CONCURRENCY", "16"))
    if api_concurrency < 1:
        raise ValueError(f"TREE_BUILD_API_CONCURRENCY 必须 >= 1，当前值: {api_concurrency}")
    if args.video_concurrency < 1:
        raise ValueError(f"--video-concurrency 必须 >= 1，当前值: {args.video_concurrency}")

    # Phase 1: 待建发现（progress + 完整性双重跳过）
    progress_path = Path(args.progress_path)
    finished = load_progress(progress_path)
    if finished:
        logger.info("progress 已记录 {} 个完成视频", len(finished))
    pending = _discover_pending(videos_dir, out_dir, finished)
    if args.limit > 0:
        pending = pending[: args.limit]
    logger.info(
        "待建 {} 个视频, video_concurrency={}, api_concurrency={}",
        len(pending),
        args.video_concurrency,
        api_concurrency,
    )
    if not pending:
        return

    # Phase 2: 客户端与共享信号量
    llm, vlm = _build_clients(api_concurrency)
    with open(PROJECT_ROOT / "config" / "default.yaml", encoding="utf-8") as f:
        tree_cfg = TreeConfig.from_dict(yaml.safe_load(f)["tree"])
    api_sem = asyncio.Semaphore(api_concurrency)
    video_sem = asyncio.Semaphore(args.video_concurrency)
    progress_lock = asyncio.Lock()
    start_time = time.time()
    completed = 0
    failed: list[str] = []

    # Phase 3: 视频级并发编排（复刻 repair_trees 模式）
    async def _build_one(video_path: Path) -> None:
        nonlocal completed
        async with video_sem:
            vid = video_path.stem
            logger.info("开始建树 {}", vid)
            try:
                builder = VideoTreeBuilder(vlm=vlm, llm=llm, config=tree_cfg, api_semaphore=api_sem)
                srt_entries = _find_srt_entries(video_path, srt_dir)
                index = await builder.build_async(str(video_path), srt_entries)
                tree_path = out_dir / vid / "tree.json"
                tree_path.parent.mkdir(parents=True, exist_ok=True)
                index.save_json(str(tree_path))
                await save_progress(progress_path, progress_lock, vid)
                completed += 1
            except Exception as exc:
                # WHY: 单视频错误隔离——一个视频失败不拖垮整批；
                # 失败清单汇总上报，vid 不进 progress，下次重跑自动重建。
                logger.error("建树失败 {} ({}): {}", vid, type(exc).__name__, exc)
                failed.append(vid)
                return
            if completed % 5 == 0:
                elapsed = time.time() - start_time
                rate = completed / elapsed * 60 if elapsed > 0 else 0
                logger.info(
                    "进度: {}/{}, 已用 {:.0f}s, 速率 {:.2f} 视频/分钟",
                    completed,
                    len(pending),
                    elapsed,
                    rate,
                )

    await asyncio.gather(*[asyncio.create_task(_build_one(p)) for p in pending])

    # Phase 4: 汇总
    elapsed = time.time() - start_time
    logger.info(
        "批量建树完成: 成功 {}, 失败 {}, 总耗时 {:.0f}s{}",
        completed,
        len(failed),
        elapsed,
        f", 失败清单: {failed}" if failed else "",
    )


def main() -> None:
    """同步入口。"""
    parser = argparse.ArgumentParser(description="批量并行建树")
    parser.add_argument("--videos-dir", type=str, required=True, help="视频文件目录")
    parser.add_argument("--out-dir", type=str, default="store/videos", help="树输出根目录")
    parser.add_argument("--srt-dir", type=str, default="", help="SRT 目录（默认同 videos-dir）")
    parser.add_argument("--video-concurrency", type=int, default=16, help="同时在建视频数")
    parser.add_argument("--limit", type=int, default=0, help="只建前 N 个（0=全部，烟测用）")
    parser.add_argument(
        "--progress-path",
        type=str,
        default=str(PROJECT_ROOT / "logs" / PROGRESS_FILE),
        dest="progress_path",
        help="progress 文件路径",
    )
    args = parser.parse_args()
    if not args.srt_dir:
        args.srt_dir = args.videos_dir
    asyncio.run(main_async(args))


if __name__ == "__main__":
    main()
