"""确定性记忆选择（ARCH §3.4）——无向量，纯函数、可单测、可调试。

选择来源（并集去重）：①显式引用(beats 点名) ②主角常驻 ③近况实体(近 N 章摘要)
④命中本章伏笔窗口。pg_trgm 按名匹配兜底属 P-later，此处不做。
"""

from __future__ import annotations

from typing import Any

from ww_core.domain.repositories import CharacterView, DigestView, OutlineView, WorldEntityView

from .types import EntityKind, SelectedEntity, SelectionReason, SelectionTrace

# 视为常驻主角的 role 取值
MAIN_ROLES: frozenset[str] = frozenset({"主角", "核心", "protagonist", "lead"})
# 近况实体回看的章数（ARCH §3.4 默认 3–5）
RECENT_DIGEST_COUNT = 5
# 选择结果排序时的 kind 次序
_KIND_RANK: dict[EntityKind, int] = {"character": 0, "world_entity": 1}
_REASON_RANK: dict[SelectionReason, int] = {
    "explicit_beat": 0,
    "main_character": 1,
    "recent_digest": 2,
    "foreshadow_window": 3,
    "author_pin": 4,
}

# 实体引用键（kind, name）——pin/排除集合用，确定性、可 hash。
EntityKey = tuple[EntityKind, str]


def _flatten_strings(obj: Any) -> list[str]:
    """递归收集任意 JSON 结构里的字符串叶子（确定性、用于按名匹配）。"""
    if isinstance(obj, str):
        return [obj]
    if isinstance(obj, dict):
        out: list[str] = []
        for key in sorted(obj):
            out.extend(_flatten_strings(obj[key]))
        return out
    if isinstance(obj, (list, tuple)):
        out = []
        for item in obj:
            out.extend(_flatten_strings(item))
        return out
    return []


def _explicit_names(beats: dict[str, Any]) -> tuple[set[str], str]:
    """返回 (显式列出的实体名集合, beats 扁平文本) 供按名匹配。"""
    listed = {str(n) for n in beats.get("entities", []) if isinstance(n, str)}
    text = "\n".join(_flatten_strings({k: v for k, v in beats.items() if k != "entities"}))
    return listed, text


def _digest_names(digests: list[DigestView]) -> tuple[set[str], str]:
    listed: set[str] = set()
    texts: list[str] = []
    for d in digests:
        listed |= {str(n) for n in d.facts.get("entities", []) if isinstance(n, str)}
        texts.extend(_flatten_strings({k: v for k, v in d.facts.items() if k != "entities"}))
    return listed, "\n".join(texts)


def _window_entity_names(outline: OutlineView) -> set[str]:
    names: set[str] = set()
    for win in outline.foreshadow_windows:
        for n in win.get("entities", []):
            if isinstance(n, str):
                names.add(n)
    return names


def _matches(name: str, listed: set[str], text: str) -> bool:
    return name in listed or (name in text)


def select_relevant_entities(
    *,
    outline: OutlineView,
    characters: list[CharacterView],
    world_entities: list[WorldEntityView],
    recent_digests: list[DigestView],
    pinned: frozenset[EntityKey] = frozenset(),
    excluded: frozenset[EntityKey] = frozenset(),
) -> SelectionTrace:
    """确定性并集选择 → SelectionTrace（每实体带去重、排序的入选理由）。

    作者覆盖（B0 可控版）：`excluded` 强制剔除（优先于一切，即便自动命中也不注入）；
    `pinned` 强制纳入并加 `author_pin` 理由（即便零自动理由也注入）。excluded 优先于 pinned，
    因为「显式排除」是更强的作者意图（同一实体不应同时出现在两个集合，前端保证）。
    """
    beat_listed, beat_text = _explicit_names(outline.beats)
    digest_listed, digest_text = _digest_names(recent_digests)
    window_names = _window_entity_names(outline)

    selected: list[SelectedEntity] = []

    def reasons_for(name: str, *, is_main: bool) -> list[SelectionReason]:
        reasons: list[SelectionReason] = []
        if _matches(name, beat_listed, beat_text):
            reasons.append("explicit_beat")
        if is_main:
            reasons.append("main_character")
        if _matches(name, digest_listed, digest_text):
            reasons.append("recent_digest")
        if name in window_names:
            reasons.append("foreshadow_window")
        return reasons

    def collect(kind: EntityKind, name: str, *, is_main: bool) -> None:
        key: EntityKey = (kind, name)
        if key in excluded:  # 显式排除优先于一切
            return
        reasons = reasons_for(name, is_main=is_main)
        if key in pinned:
            reasons.append("author_pin")
        if reasons:
            deduped = sorted(set(reasons), key=lambda r: _REASON_RANK[r])
            selected.append(SelectedEntity(kind=kind, name=name, reasons=deduped))

    for c in characters:
        collect("character", c.name, is_main=(c.role or "") in MAIN_ROLES)
    for w in world_entities:
        collect("world_entity", w.name, is_main=False)

    selected.sort(key=lambda e: (_KIND_RANK[e.kind], e.name))
    return SelectionTrace(selected=selected)
