"""质量校验模块：交叉验证树节点 Card 字段与子节点证据。

验证策略:
- L2 entities: 仅保留在子 L3 文本语料中模糊匹配到的实体。
- L2 visible_text: 仅保留在子 L3 visible_text 中出现的条目。
- L1 visible_text: 仅保留在后代 L2/L3 visible_text 中出现的条目。
- L1 key_entities: 仅保留在后代 L2/L3 文本语料中模糊匹配到的实体。

Card 为 frozen dataclass，无法原地修改——移除幻觉字段时
创建新 Card 实例并赋值给 node.card（Node 非 frozen）。
"""

from __future__ import annotations

import string
from dataclasses import dataclass

from loguru import logger

from app.tree.index import (
    L1Card,
    L1Node,
    L2Card,
    L2Node,
    TreeIndex,
)

# ---------------------------------------------------------------------------
# 校验统计
# ---------------------------------------------------------------------------


@dataclass
class VerifyStats:
    """校验统计信息。"""

    l2_entities_kept: int = 0
    l2_entities_removed: int = 0
    l2_visible_text_kept: int = 0
    l2_visible_text_removed: int = 0
    l1_visible_text_kept: int = 0
    l1_visible_text_removed: int = 0
    l1_key_entities_kept: int = 0
    l1_key_entities_removed: int = 0


# ---------------------------------------------------------------------------
# 文本归一化 & 模糊匹配
# ---------------------------------------------------------------------------


def _normalize(text: str) -> str:
    """归一化文本：小写 + 去除标点。

    参数:
        text: 原始文本。

    返回:
        归一化后的纯小写无标点字符串。
    """
    return text.lower().translate(str.maketrans("", "", string.punctuation))


def fuzzy_match(entity: str | None, corpus: str | None) -> bool:
    """模糊子串匹配：归一化后判断 entity 是否为 corpus 的子串。

    参数:
        entity: 待匹配的实体文本（None 视为不匹配）。
        corpus: 证据语料文本（None 视为空）。

    返回:
        True 表示匹配成功。
    """
    if not entity or not corpus:
        return False
    return _normalize(str(entity)) in _normalize(str(corpus))


# ---------------------------------------------------------------------------
# 语料收集
# ---------------------------------------------------------------------------


def _collect_l3_text(l2_node: L2Node) -> str:
    """收集 L2 节点所有子 L3 的文本语料。

    从每个 L3 子节点的 card 和顶层字段中提取:
    frame_summary、visible_text、subtitle。

    参数:
        l2_node: L2 节点。

    返回:
        拼接后的文本语料（用换行分隔）。
    """
    parts: list[str] = []
    for l3 in l2_node.children:
        parts.append(l3.card.frame_summary)
        parts.extend(l3.card.visible_text)
        if l3.card.subtitle:
            parts.append(l3.card.subtitle)
    return "\n".join(parts)


def _collect_descendant_visible_text(l1_node: L1Node) -> str:
    """收集 L1 节点所有后代（L2/L3）的 visible_text。

    参数:
        l1_node: L1 节点。

    返回:
        所有后代 visible_text 拼接后的文本（用换行分隔）。
    """
    parts: list[str] = []
    for l2 in l1_node.children:
        parts.extend(l2.card.visible_text)
        for l3 in l2.children:
            parts.extend(l3.card.visible_text)
    return "\n".join(parts)


def _collect_descendant_text_corpus(l1_node: L1Node) -> str:
    """收集 L1 节点所有后代（L2/L3）的完整文本语料。

    用于 L1 key_entities 的交叉验证，范围包括
    L2/L3 的所有文本字段（frame_summary、visible_text、subtitle 等）。

    参数:
        l1_node: L1 节点。

    返回:
        所有后代文本语料拼接后的文本（用换行分隔）。
    """
    parts: list[str] = []
    for l2 in l1_node.children:
        parts.append(l2.card.event_description)
        parts.extend(l2.card.entities)
        parts.extend(l2.card.visible_text)
        for l3 in l2.children:
            parts.append(l3.card.frame_summary)
            parts.extend(l3.card.visible_text)
            if l3.card.subtitle:
                parts.append(l3.card.subtitle)
    return "\n".join(parts)


# ---------------------------------------------------------------------------
# 主校验函数
# ---------------------------------------------------------------------------


def verify_tree(index: TreeIndex) -> VerifyStats:
    """交叉验证视频树的 Card 字段与子节点证据，原地替换不合格的 Card。

    Cards 为 frozen dataclass，移除幻觉字段时创建新 Card 实例
    并赋值给 node.card。

    参数:
        index: 树索引（会被原地修改）。

    返回:
        VerifyStats 校验统计。
    """
    stats = VerifyStats()

    for l1 in index.roots:
        # Phase 1: L2 字段验证
        for l2 in l1.children:
            _verify_l2(l2, stats)

        # Phase 2: L1 字段验证
        _verify_l1(l1, stats)

    logger.info(
        "verify_tree: source={} "
        "l2_ent_kept={} l2_ent_rm={} "
        "l2_vt_kept={} l2_vt_rm={} "
        "l1_vt_kept={} l1_vt_rm={} "
        "l1_ke_kept={} l1_ke_rm={}",
        index.metadata.source_path,
        stats.l2_entities_kept,
        stats.l2_entities_removed,
        stats.l2_visible_text_kept,
        stats.l2_visible_text_removed,
        stats.l1_visible_text_kept,
        stats.l1_visible_text_removed,
        stats.l1_key_entities_kept,
        stats.l1_key_entities_removed,
    )

    return stats


def _verify_l2(l2: L2Node, stats: VerifyStats) -> None:
    """校验单个 L2 节点的 entities 和 visible_text。

    参数:
        l2: L2 节点（card 可能被替换）。
        stats: 统计对象（原地累加）。
    """
    corpus = _collect_l3_text(l2)
    old_card = l2.card

    # entities: 模糊匹配过滤
    kept_entities = [e for e in old_card.entities if fuzzy_match(e, corpus)]
    stats.l2_entities_kept += len(kept_entities)
    stats.l2_entities_removed += len(old_card.entities) - len(kept_entities)

    # visible_text: 子 L3 visible_text 中必须存在
    l3_visible = _collect_l3_visible_text_set(l2)
    kept_vt = [vt for vt in old_card.visible_text if _text_in_set(vt, l3_visible)]
    stats.l2_visible_text_kept += len(kept_vt)
    stats.l2_visible_text_removed += len(old_card.visible_text) - len(kept_vt)

    # 创建新 Card 替换（frozen dataclass）
    l2.card = L2Card(
        event_description=old_card.event_description,
        entities=kept_entities,
        actions=old_card.actions,
        action_subjects=old_card.action_subjects,
        visible_text=kept_vt,
        spatial_relations=old_card.spatial_relations,
        state_changes=old_card.state_changes,
        subtitle=old_card.subtitle,
    )


def _verify_l1(l1: L1Node, stats: VerifyStats) -> None:
    """校验单个 L1 节点的 visible_text 和 key_entities。

    参数:
        l1: L1 节点（card 可能被替换）。
        stats: 统计对象（原地累加）。
    """
    old_card = l1.card

    # visible_text: 必须出现在后代 L2/L3 visible_text 中
    descendant_vt = _collect_descendant_visible_text(l1)
    kept_vt = [vt for vt in old_card.visible_text if fuzzy_match(vt, descendant_vt)]
    stats.l1_visible_text_kept += len(kept_vt)
    stats.l1_visible_text_removed += len(old_card.visible_text) - len(kept_vt)

    # key_entities: 交叉验证后代文本语料
    descendant_corpus = _collect_descendant_text_corpus(l1)
    kept_ke = [ke for ke in old_card.key_entities if fuzzy_match(ke, descendant_corpus)]
    stats.l1_key_entities_kept += len(kept_ke)
    stats.l1_key_entities_removed += len(old_card.key_entities) - len(kept_ke)

    # 创建新 Card 替换（frozen dataclass）
    l1.card = L1Card(
        scene_summary=old_card.scene_summary,
        main_setting=old_card.main_setting,
        key_entities=kept_ke,
        main_actions=old_card.main_actions,
        topic_keywords=old_card.topic_keywords,
        visible_text=kept_vt,
        temporal_flow=old_card.temporal_flow,
    )


# ---------------------------------------------------------------------------
# 辅助函数
# ---------------------------------------------------------------------------


def _collect_l3_visible_text_set(l2: L2Node) -> set[str]:
    """收集 L2 下所有 L3 子节点的 visible_text 归一化集合。

    参数:
        l2: L2 节点。

    返回:
        归一化后的 visible_text 集合。
    """
    result: set[str] = set()
    for l3 in l2.children:
        for vt in l3.card.visible_text:
            result.add(_normalize(vt))
    return result


def _text_in_set(text: str, normalized_set: set[str]) -> bool:
    """检查文本归一化后是否存在于集合中。

    参数:
        text: 待检查文本。
        normalized_set: 归一化后的文本集合。

    返回:
        True 表示匹配成功。
    """
    return _normalize(text) in normalized_set
