"""轻量四门质量检查 — 对生成题目执行 4 个独立的 LLM 质量门控。

四门：
1. key_verify: 验证答案在来源素材中有证据支撑。
2. blind_answer: 无上下文时 LLM 能否答对（若答对 → 题目泄漏）。
3. multi_true: 检测是否有多个选项可被视为正确。
4. leak_test: 按策略特定模板探测答题捷径。

设计要点：
- run_gates 先做 verbatim_ratio 前置短路（> 0.5 直接 FAIL key_verify）。
- 四门并发执行（asyncio.gather）。
- 每门加载 store/prompts/question_gen/ 下对应模板，构造 messages，调 LLM。
- LLM 返回 JSON {"verdict": "pass"|"fail", "reason": "..."}，解析失败视为 FAIL。
- session_id 必须透传至每次 LLM 调用（遥测关联）。
"""

from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from app.question_gen.postprocess import PostprocessResult
    from app.tree.index import TreeIndex
    from core.protocols import LLMProvider, VLMProvider

# ---------------------------------------------------------------------------
# 常量
# ---------------------------------------------------------------------------

_PROMPTS_DIR = Path(__file__).resolve().parent.parent.parent / "store" / "prompts" / "question_gen"

_VERBATIM_THRESHOLD = 0.5


# ---------------------------------------------------------------------------
# CandidateQuestion（Task 5 尚未实现，本地定义，后续迁移至 generator_v2.py）
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class CandidateQuestion:
    """候选题目 — 出题管线生成、待门控审核的题目数据。

    属性:
        question_id: 题目唯一标识。
        video_id: 所属视频标识。
        task_type: 题型（如 "Action Reasoning"）。
        skill_target: 目标失败机制编号（M1-M5）。
        question: 题目文本。
        options: 选项元组（如 ("A. ...", "B. ...", "C. ...", "D. ...")）。
        answer: 正确答案字母（如 "A"）。
        source_nodes: 来源节点 ID 元组。
        difficulty: 难度等级。
        subtitle_sentences: 字幕句子元组。
        frame_paths: 帧图片路径元组。
    """

    question_id: str
    video_id: str
    task_type: str
    skill_target: str
    question: str
    options: tuple[str, ...]
    answer: str
    source_nodes: tuple[str, ...]
    difficulty: str
    subtitle_sentences: tuple[str, ...] = field(default_factory=tuple)
    frame_paths: tuple[str, ...] = field(default_factory=tuple)


# ---------------------------------------------------------------------------
# 门控结果类型
# ---------------------------------------------------------------------------


class GateVerdict(Enum):
    """门控判决枚举。"""

    PASS = "pass"
    FAIL = "fail"
    SKIP = "skip"


@dataclass(frozen=True)
class GateResult:
    """单门判决结果。

    属性:
        verdict: 门控判决（PASS / FAIL / SKIP）。
        reason: 判决原因说明。
        raw_response: LLM 原始返回内容。
    """

    verdict: GateVerdict
    reason: str
    raw_response: str


@dataclass(frozen=True)
class GateReport:
    """四门判决汇总报告。

    属性:
        key_verify: 关键验证门结果。
        blind_answer: 盲答门结果。
        multi_true: 多正确门结果。
        leak_test: 泄漏测试门结果。
    """

    key_verify: GateResult
    blind_answer: GateResult
    multi_true: GateResult
    leak_test: GateResult

    @property
    def passed(self) -> bool:
        """全门 PASS 或 SKIP 时视为通过。"""
        for gate in (self.key_verify, self.blind_answer, self.multi_true, self.leak_test):
            if gate.verdict == GateVerdict.FAIL:
                return False
        return True

    @property
    def reject_reason(self) -> str | None:
        """首个 FAIL 门的 reason，全部通过返回 None。"""
        for gate in (self.key_verify, self.blind_answer, self.multi_true, self.leak_test):
            if gate.verdict == GateVerdict.FAIL:
                return gate.reason
        return None


# ---------------------------------------------------------------------------
# 内部工具函数
# ---------------------------------------------------------------------------


def _load_prompt_template(filename: str) -> str:
    """加载 store/prompts/question_gen/ 下的模板文件。

    参数:
        filename: 模板文件名（如 "gate_key_verify.md"）。

    返回:
        模板内容字符串。

    异常:
        FileNotFoundError: 模板文件不存在。
    """
    path = _PROMPTS_DIR / filename
    if not path.exists():
        msg = f"门控模板文件不存在: {path}"
        raise FileNotFoundError(msg)
    return path.read_text(encoding="utf-8")


def _resolve_source_text(candidate: CandidateQuestion, tree: TreeIndex) -> str:
    """从树中解析候选题的来源节点文本。

    遍历树的所有层级，匹配 source_nodes 中的 ID，提取对应描述文本。

    参数:
        candidate: 候选题目。
        tree: 视频树索引。

    返回:
        拼接的来源节点描述文本。
    """
    texts: list[str] = []
    target_ids = set(candidate.source_nodes)

    for l1 in tree.roots:
        if l1.id in target_ids:
            texts.append(f"[L1 {l1.id}] {l1.card.scene_summary}")
        for l2 in l1.children:
            if l2.id in target_ids:
                texts.append(f"[L2 {l2.id}] {l2.card.event_description}")
            for l3 in l2.children:
                if l3.id in target_ids:
                    texts.append(f"[L3 {l3.id}] {l3.card.frame_summary}")

    if not texts:
        logger.warning(
            "未找到来源节点: candidate={}, source_nodes={}",
            candidate.question_id,
            candidate.source_nodes,
        )
        return "(no source material found)"

    return "\n".join(texts)


def _format_options(options: tuple[str, ...]) -> str:
    """将选项元组格式化为可读字符串。"""
    return "\n".join(options)


def _parse_gate_response(raw_content: str) -> tuple[GateVerdict, str]:
    """解析 LLM 返回的门控 JSON 响应。

    期望格式: {"verdict": "pass"|"fail", "reason": "..."}
    解析失败时返回 FAIL + parse_error。

    参数:
        raw_content: LLM 返回的原始文本。

    返回:
        (verdict, reason) 元组。
    """
    try:
        # 尝试从文本中提取 JSON（可能被 markdown 代码块包裹）
        content = raw_content.strip()
        if "```" in content:
            # 提取代码块中的内容
            parts = content.split("```")
            for part in parts:
                stripped = part.strip()
                if stripped.startswith("json"):
                    stripped = stripped[4:].strip()
                if stripped.startswith("{"):
                    content = stripped
                    break

        data = json.loads(content)
        verdict_str = data.get("verdict", "").lower()
        reason = data.get("reason", "")

        if verdict_str == "pass":
            return GateVerdict.PASS, reason
        elif verdict_str == "fail":
            return GateVerdict.FAIL, reason
        else:
            return GateVerdict.FAIL, f"parse_error: invalid verdict '{verdict_str}'"

    except (json.JSONDecodeError, AttributeError, TypeError) as e:
        return GateVerdict.FAIL, f"parse_error: {e}"


# ---------------------------------------------------------------------------
# 四门实现
# ---------------------------------------------------------------------------


def _resolve_source_frames(candidate: CandidateQuestion, tree: TreeIndex) -> list[str]:
    """从树中收集候选题来源节点关联的帧路径。

    参数:
        candidate: 候选题目。
        tree: 视频树索引。

    返回:
        去重后的帧路径列表（最多 10 张，避免 VLM 输入过长）。
    """
    frames: list[str] = []
    target_ids = set(candidate.source_nodes)

    for l1 in tree.roots:
        for l2 in l1.children:
            if l2.id in target_ids:
                for l3 in l2.children:
                    if l3.frame_path:
                        frames.append(l3.frame_path)
            for l3 in l2.children:
                if l3.id in target_ids and l3.frame_path:
                    frames.append(l3.frame_path)

    # 也使用候选题自带的帧路径
    frames.extend(candidate.frame_paths)

    seen: set[str] = set()
    unique: list[str] = []
    for f in frames:
        if f not in seen:
            seen.add(f)
            unique.append(f)
    return unique[:10]


async def _gate_key_verify(
    candidate: CandidateQuestion,
    tree: TreeIndex,
    vlm: VLMProvider,
    *,
    session_id: str,
) -> GateResult:
    """关键验证门 — 使用 VLM 检查答案在来源素材（文本+帧画面）中是否有证据支撑。

    参数:
        candidate: 候选题目。
        tree: 视频树索引。
        vlm: VLM 图文调用端口（同时看文本和帧画面）。
        session_id: 会话 ID（遥测关联）。

    返回:
        GateResult 实例。
    """
    source_text = _resolve_source_text(candidate, tree)
    frames = _resolve_source_frames(candidate, tree)
    template = _load_prompt_template("gate_key_verify.md")
    prompt = template.format(
        source_text=source_text,
        question=candidate.question,
        options=_format_options(candidate.options),
        answer=candidate.answer,
    )

    if frames:
        response = await vlm.chat_with_images(
            [{"role": "user", "content": prompt}],
            images=frames,
            session_id=session_id,
        )
    else:
        # 无帧时降级为纯文本（不应常见）
        logger.warning("key_verify 无可用帧，降级纯文本: {}", candidate.question_id)
        response = await vlm.chat_with_images(
            [{"role": "user", "content": prompt}],
            images=[],
            session_id=session_id,
        )

    verdict, reason = _parse_gate_response(response.content)
    return GateResult(verdict=verdict, reason=reason, raw_response=response.content)


async def _gate_blind_answer(
    candidate: CandidateQuestion,
    llm: LLMProvider,
    *,
    session_id: str,
) -> GateResult:
    """盲答门 — 无上下文时 LLM 能否答对（答对 → 题目泄漏）。

    参数:
        candidate: 候选题目。
        llm: LLM 调用端口。
        session_id: 会话 ID（遥测关联）。

    返回:
        GateResult 实例。
    """
    template = _load_prompt_template("gate_blind_answer.md")
    prompt = template.format(
        question=candidate.question,
        options=_format_options(candidate.options),
    )

    response = await llm.chat(
        [{"role": "user", "content": prompt}],
        session_id=session_id,
    )

    verdict, reason = _parse_gate_response(response.content)
    return GateResult(verdict=verdict, reason=reason, raw_response=response.content)


async def _gate_multi_true(
    candidate: CandidateQuestion,
    tree: TreeIndex,
    llm: LLMProvider,
    *,
    session_id: str,
) -> GateResult:
    """多正确门 — 检测是否有多个选项可被视为正确。

    参数:
        candidate: 候选题目。
        tree: 视频树索引。
        llm: LLM 调用端口。
        session_id: 会话 ID（遥测关联）。

    返回:
        GateResult 实例。
    """
    source_text = _resolve_source_text(candidate, tree)
    template = _load_prompt_template("gate_multi_true.md")
    prompt = template.format(
        source_text=source_text,
        question=candidate.question,
        options=_format_options(candidate.options),
    )

    response = await llm.chat(
        [{"role": "user", "content": prompt}],
        session_id=session_id,
    )

    verdict, reason = _parse_gate_response(response.content)
    return GateResult(verdict=verdict, reason=reason, raw_response=response.content)


async def _gate_leak_test(
    candidate: CandidateQuestion,
    leak_probe_template: str,
    llm: LLMProvider,
    *,
    session_id: str,
) -> GateResult:
    """泄漏测试门 — 按策略特定模板探测答题捷径。

    参数:
        candidate: 候选题目。
        leak_probe_template: 泄漏探测模板文件名（store/prompts/question_gen/ 下）。
        llm: LLM 调用端口。
        session_id: 会话 ID（遥测关联）。

    返回:
        GateResult 实例。
    """
    template = _load_prompt_template(leak_probe_template)
    prompt = template.format(
        question=candidate.question,
        options=_format_options(candidate.options),
        answer=candidate.answer,
    )

    response = await llm.chat(
        [{"role": "user", "content": prompt}],
        session_id=session_id,
    )

    verdict, reason = _parse_gate_response(response.content)
    return GateResult(verdict=verdict, reason=reason, raw_response=response.content)


# ---------------------------------------------------------------------------
# 编排入口
# ---------------------------------------------------------------------------


async def run_gates(
    candidate: CandidateQuestion,
    tree: TreeIndex,
    llm: LLMProvider,
    leak_probe_template: str,
    postprocess: PostprocessResult,
    *,
    vlm: VLMProvider | None = None,
    session_id: str,
) -> GateReport:
    """编排四门并发执行，返回汇总报告。

    前置规则：verbatim_ratio > 0.5 时直接短路 key_verify 为 FAIL（不调 LLM）。

    参数:
        candidate: 候选题目。
        tree: 视频树索引。
        llm: LLM 调用端口。
        leak_probe_template: 泄漏探测模板文件名（store/prompts/question_gen/ 下）。
        postprocess: 后处理结果（含 verbatim_ratio）。
        vlm: VLM 图文调用端口（key_verify 使用，None 时降级为 LLM）。
        session_id: 会话 ID（遥测关联）。

    返回:
        GateReport 四门汇总。
    """
    # Phase 1: verbatim 前置短路 — 不调 LLM，直接返回
    if postprocess.verbatim_ratio > _VERBATIM_THRESHOLD:
        logger.info(
            "verbatim_ratio={:.3f} > {:.1f}，key_verify 短路 FAIL，其余三门 SKIP",
            postprocess.verbatim_ratio,
            _VERBATIM_THRESHOLD,
        )
        skip_result = GateResult(
            verdict=GateVerdict.SKIP,
            reason="skipped due to verbatim short-circuit",
            raw_response="",
        )
        return GateReport(
            key_verify=GateResult(
                verdict=GateVerdict.FAIL,
                reason=f"verbatim_ratio={postprocess.verbatim_ratio:.3f} exceeds threshold {_VERBATIM_THRESHOLD}",
                raw_response="",
            ),
            blind_answer=skip_result,
            multi_true=skip_result,
            leak_test=skip_result,
        )

    # Phase 2: 四门并发执行（key_verify 使用 VLM 看帧+文本）
    key_verify_provider = vlm if vlm is not None else llm
    key_result, blind_result, multi_result, leak_result = await asyncio.gather(
        _gate_key_verify(candidate, tree, key_verify_provider, session_id=session_id),
        _gate_blind_answer(candidate, llm, session_id=session_id),
        _gate_multi_true(candidate, tree, llm, session_id=session_id),
        _gate_leak_test(candidate, leak_probe_template, llm, session_id=session_id),
    )

    report = GateReport(
        key_verify=key_result,
        blind_answer=blind_result,
        multi_true=multi_result,
        leak_test=leak_result,
    )

    if report.passed:
        logger.debug("四门全部通过: question_id={}", candidate.question_id)
    else:
        logger.info(
            "门控拒绝: question_id={}, reason={}",
            candidate.question_id,
            report.reject_reason,
        )

    return report
