"""记忆注入与 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 .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


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}")
    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 _build_volatile(
    chapter_no: int,
    cards: str,
    foreshadows: list[ForeshadowView],
    digests: list[DigestView],
    beats: dict[str, Any],
    directive: 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),
        _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]

    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
    )

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