"""记忆注入与 prompt 组装（ARCH §5.3）。

assemble = 确定性选择 → 渲染卡片 → 序列化为 stable_core(缓存前缀) + volatile(断点后)。
稳定内核只放真正定型、无 latest_state 的内容；latest_state/本章大纲等易变内容入 volatile。
全程排序、无时间戳/UUID，保证缓存前缀字节稳定（不变量 #9）。
"""

from __future__ import annotations

import json
import uuid
from typing import Any

from ww_config import genre_craft_fragment

from ww_core.domain.injection_repo import InjectionOverride
from ww_core.domain.repositories import (
    CharacterView,
    DigestView,
    ForeshadowView,
    MemoryRepos,
    OutlineView,
    ProjectSpecView,
    RuleView,
    StyleView,
    WorldEntityView,
)
from ww_core.domain.review_repo import ReviewView

from .render import render_cards
from .selection import MAIN_ROLES, RECENT_DIGEST_COUNT, EntityKey, select_relevant_entities
from .types import AssembledContext, EntityKind

# 四级规则合并优先级：global → genre → style → project（越具体越靠后/越优先）
_LEVEL_RANK: dict[str, int] = {"global": 0, "genre": 1, "style": 2, "project": 3}

# 开篇黄金三章特判上限：前 N 章在 volatile 追加开篇提示（灵感①）。
OPENING_CHAPTER_LIMIT = 3

# 冲突裁决反哺（灵感②/D3）：只反哺"保留项"裁决——作者采纳(accept)或手改(manual)；
# ignore（作者判为非冲突）不反哺。这三个控制值对齐 schemas.projects.Verdict。
_RETAINED_VERDICTS: frozenset[str] = frozenset({"accept", "manual"})

# 上一章冲突提醒有界上限（top-N）——控作者/模型每章注意力预算（灵感② 校验 M）。
PRIOR_CONFLICT_NOTE_LIMIT = 3


def _ser(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, sort_keys=True)


def _kind(raw: str) -> EntityKind:
    """把覆盖里的字符串 kind 收敛成 EntityKind 字面量（未知值按 character 处理，不匹配亦无害）。"""
    return "world_entity" if raw == "world_entity" else "character"


def merge_rules(rules: list[RuleView]) -> list[RuleView]:
    """按 global→genre→style→project 排序（同级按内容排序，确定性）。"""
    return sorted(rules, key=lambda r: (_LEVEL_RANK.get(r.level, 99), r.level, r.content))


def _section(title: str, body: str) -> str | None:
    return f"## {title}\n{body}" if body else None


def _build_spec_section(spec: ProjectSpecView | None) -> str | None:
    """作品书级蓝本（标题/一句话梗概/前提/主题）——稳定、定型 → 缓存前缀。

    保证哪怕世界观/角色/大纲全空，stable_core 也非空、有方向（修空 prompt 400）。
    """
    if spec is None:
        return None
    lines = [f"【作品】{spec.title}"]
    if spec.logline:
        lines.append(f"【一句话梗概】{spec.logline}")
    if spec.premise:
        lines.append(f"【前提】{spec.premise}")
    if spec.theme:
        lines.append(f"【主题】{spec.theme}")
    # 立项书级常量（灵感④）：每书稳定 → 随蓝本进缓存前缀，跨章字节不变（不变量 #9）。
    if spec.tone:
        lines.append(f"【基调】{spec.tone}")
    if spec.narrative_pov:
        lines.append(f"【叙事视角】{spec.narrative_pov}")
    if spec.ending_type:
        lines.append(f"【结局取向】{spec.ending_type}")
    return _section("作品蓝本", "\n".join(lines))


def _build_genre_craft_section(genre: str | None) -> str | None:
    """按题材注入 craft 片段（语域/节奏/钩子/字数/套路，灵感 F1）。

    genre 是每书稳定项（来自 spec）→ 入缓存前缀 stable_core，跨章字节稳定（不变量 #9）。
    命不中预设题材（自由填写/未设）则不注入。品类无关内核在 write_craft.md，不在此处。
    """
    fragment = genre_craft_fragment(genre)
    return _section("本作题材写法", fragment) if fragment else None


def _build_stable(
    spec: ProjectSpecView | None,
    world_entities: list[WorldEntityView],
    main_characters: list[CharacterView],
    style: StyleView | None,
    rules: list[RuleView],
) -> str:
    world_lines = [
        f"【世界规则】{w.name}：{_ser(w.rules)}"
        for w in sorted(world_entities, key=lambda w: w.name)
        if w.rules
    ]
    # 定型主角：只放稳定字段，绝不含 latest_state（易变）
    char_lines = [
        f"【主角】{c.name}｜定位：{c.role or '未定'}"
        + (f"｜动机：{c.motive}" if c.motive else "")
        + (f"｜背景：{c.backstory}" if c.backstory else "")
        for c in sorted(main_characters, key=lambda c: c.name)
    ]
    rule_lines = [f"[{r.level}] {r.content}" for r in merge_rules(rules)]

    sections = [
        # 题材写法领衔——同题材作品共享此前缀，利于跨书隐式前缀缓存（不变量 #9）。
        _build_genre_craft_section(spec.genre if spec else None),
        _build_spec_section(spec),
        _section("世界观硬规则", "\n".join(world_lines)),
        _section("定型主角", "\n".join(char_lines)),
        _section("文风指纹", _ser(style.dimensions) if style else ""),
        _section("规则", "\n".join(rule_lines)),
    ]
    return "\n\n".join(s for s in sections if s)


def _opening_marker(chapter_no: int) -> str | None:
    """开篇黄金三章特判（灵感①）：前 N 章显式提示，不赌模型自读章号。

    含章号 → 每章易变 → 只入 volatile（断点后），绝不进缓存前缀（不变量 #9）。
    """
    if chapter_no > OPENING_CHAPTER_LIMIT:
        return None
    return _section(
        "开篇黄金三章",
        f"本章为开篇第 {chapter_no} 章，适用黄金三章原则："
        "开篇即抛出核心冲突或身份钩子；三章之内交付第一个小爽点或转折；"
        "避免大段世界观倾泻，边推进情节边带出设定。",
    )


def _conflict_constraint(conflict: dict[str, Any], note: str | None) -> str | None:
    """把一条已裁决 continuity 冲突改写成本章的前瞻正向约束（确定性模板，绝不调 LLM）。

    优先用作者手改备注（`note`，作者自己的话）；否则用审稿改法建议（`suggestion`——
    作者点「采纳」即背书之，故仍是外部 oracle 驱动，不违 #3/#4）。只做正向「延续设定」
    措辞，绝无道歉/自指（守 #3/#4：不自评自改、不磨平文风）。两者皆空则返回 None。
    """
    detail = (note or "").strip() or str(conflict.get("suggestion") or "").strip()
    if not detail:
        return None
    ctype = str(conflict.get("type") or "").strip()
    prefix = f"【{ctype}】" if ctype else ""
    return f"{prefix}延续上一章已确认的设定：{detail}"


def _prior_conflict_notes(
    latest_review: ReviewView | None,
    *,
    limit: int = PRIOR_CONFLICT_NOTE_LIMIT,
) -> str | None:
    """上一已验收章的 continuity 冲突作者裁决保留项 → 本章前瞻约束（灵感②/D3）。

    - 仅当上一章已验收（`decisions` 非空——只在验收事务写入）才反哺（守风险#5：不读未定稿）。
    - 按 `decisions["items"][].conflict_index` join 回 `conflicts[index]` 取正文，
      过滤 `verdict∈{accept,manual}`（保留项）、去重、按 conflict_index 排序、有界 top-N。
    - 确定性纯函数、字节稳定、无 LLM 调用（守 #6/#7：assemble 可缓存的纯函数）。
    - 软批评类（style/pace/characterization）无作者裁决 oracle → 永不经此反哺（守 #3/#4）。
    """
    if latest_review is None or latest_review.decisions is None:
        return None
    items = latest_review.decisions.get("items") or []
    conflicts = latest_review.conflicts
    constraints: list[str] = []
    seen: set[str] = set()
    for item in sorted(items, key=lambda d: d.get("conflict_index", 0)):
        if item.get("verdict") not in _RETAINED_VERDICTS:
            continue
        idx = item.get("conflict_index")
        if not isinstance(idx, int) or not (0 <= idx < len(conflicts)):
            continue
        line = _conflict_constraint(conflicts[idx], item.get("note"))
        if line is None or line in seen:
            continue
        seen.add(line)
        constraints.append(line)
    bounded = constraints[:limit]
    if not bounded:
        return None
    return "\n".join(f"- {c}" for c in bounded)


def _build_volatile(
    chapter_no: int,
    cards: str,
    foreshadows: list[ForeshadowView],
    digests: list[DigestView],
    beats: dict[str, Any],
    directive: str | None = None,
    prior_conflict_notes: str | None = None,
) -> str:
    fore_lines = [
        f"【伏笔】{f.code} {f.title} [{f.status}]"
        for f in sorted(foreshadows, key=lambda f: f.code)
    ]
    digest_lines = [
        f"第{d.chapter_no}章：{_ser(d.facts)}" for d in sorted(digests, key=lambda d: d.chapter_no)
    ]
    sections = [
        # 作者本章指令领衔——易变的每章输入，只入 volatile（断点后），守缓存前缀不变量 #9。
        _section("本章指令", directive.strip()) if directive and directive.strip() else None,
        # 写章指令始终在场——保证 volatile（断点后的 input）永不为空（修空 prompt 400）。
        _section("写作指令", f"请创作第 {chapter_no} 章的正文。"),
        # 开篇黄金三章标记（仅前 N 章）——每章易变，只入 volatile。
        _opening_marker(chapter_no),
        # 上一章冲突裁决反哺（灵感②）——依赖上一章裁决，每章易变，绝不进缓存前缀（#9）。
        _section("上一章冲突提醒", prior_conflict_notes) if prior_conflict_notes else None,
        _section("本章注入卡片", cards),
        _section("相关伏笔窗口", "\n".join(fore_lines)),
        _section("近况摘要", "\n".join(digest_lines)),
        _section("本章节拍", _ser(beats) if beats else ""),
    ]
    return "\n\n".join(s for s in sections if s)


async def assemble(
    repos: MemoryRepos,
    project_id: uuid.UUID,
    chapter_no: int,
    recent_k: int = RECENT_DIGEST_COUNT,
    *,
    override: InjectionOverride | None = None,
    directive: str | None = None,
) -> AssembledContext:
    """组装本章 prompt 上下文（确定性、可缓存）。

    `override`（B0 可控版作者覆盖）也是确定性输入：pin 强制纳入 / excluded 强制剔除 /
    recent_n 覆盖近况回看章数。draft 与注入读端点传同一覆盖，故「看到的=写章用的」（不变量 #6）。

    `directive`（T4-b 本章指令）是临时的每章输入——只入 volatile（断点后），绝不污染
    stable_core 缓存前缀（不变量 #9）；不持久化。
    """
    # recent_n 覆盖优先于默认 recent_k；非正值视为未设（守住至少回看若干章）。
    effective_k = override.recent_n if (override and override.recent_n) else recent_k
    pinned: frozenset[EntityKey] = (
        frozenset((_kind(r.kind), r.name) for r in override.pinned) if override else frozenset()
    )
    excluded: frozenset[EntityKey] = (
        frozenset((_kind(r.kind), r.name) for r in override.excluded) if override else frozenset()
    )

    outline = await repos.outline.get(project_id, chapter_no) or OutlineView(
        volume=0, chapter_no=chapter_no
    )
    characters = await repos.character.list_for_project(project_id)
    world_entities = await repos.world_entity.list_for_project(project_id)
    recent_digests = await repos.digest.recent(project_id, effective_k)

    selection = select_relevant_entities(
        outline=outline,
        characters=characters,
        world_entities=world_entities,
        recent_digests=recent_digests,
        pinned=pinned,
        excluded=excluded,
    )

    codes = [str(w["code"]) for w in outline.foreshadow_windows if w.get("code")]
    foreshadows = await repos.foreshadow.list_for_codes(project_id, codes)
    style = await repos.style.latest(project_id)
    rules = await repos.rules.all_for_project(project_id)
    spec = await repos.project.spec(project_id)

    main_characters = [c for c in characters if (c.role or "") in MAIN_ROLES]

    # 冲突裁决反哺（灵感②/D3）：读上一已验收章的 continuity 冲突作者裁决保留项 →
    # 本章 volatile 前瞻约束。review 未注入（默认 None）或本章为第 1 章 → 优雅降级不反哺。
    prior_conflict_notes: str | None = None
    if repos.review is not None and chapter_no > 1:
        prior_reviews = await repos.review.list_for_chapter(project_id, chapter_no - 1)
        latest_review = prior_reviews[0] if prior_reviews else None  # list_for_chapter 新→旧
        prior_conflict_notes = _prior_conflict_notes(latest_review)

    stable_core = _build_stable(spec, world_entities, main_characters, style, rules)
    cards = render_cards(selection, characters, world_entities)
    volatile = _build_volatile(
        chapter_no,
        cards,
        foreshadows,
        recent_digests,
        outline.beats,
        directive,
        prior_conflict_notes,
    )

    return AssembledContext(stable_core=stable_core, volatile=volatile, selection=selection)
