"""生成节点（C6 扩 / ARCH §5.4 worldbuilder·character-gen 行 / §6.5 / §4.5）。

worldbuilder 与 character-gen 都是**独立生成**——不在写章/审稿流水线（不接进 review 图），
仿 `outline_node` / `style_extract_node`。入库端点（T5.2）直接调本模块拿结构化产物，
再持久化到 `world_entities` / `characters` 表。

三件事：
1. `run_worldbuilder` — 据需求 + 作品设定产 `WorldGenResult`（硬规则显式）。
2. `run_character_gen` — 据需求 + 世界观约束 + **已有角色 + 本批已生成卡**产 `CharacterGenResult`
   （群像防雷同：差异化上下文经 `build_character_gen_context` 注入，M5-a）。
3. `precheck_generated_cards` — **入库前 continuity 校验缝**（ARCH §6.5）：编排器在角色入库前
   追加一道 continuity 检查（**非 character-gen 直接互调**，守 §5.4 数据流），把生成卡 vs
   世界观/已有角色真相源比对、返回冲突清单。T5.2 入库端点调它做 gate。

确定性边界（CLAUDE.md「LangGraph」纪律）：节点逻辑无 LLM 非确定性——不确定性藏在网关后，
节点只做 `AgentSpec + 上下文 → LlmRequest` 的纯构造 + 转发 `Gateway.run` 的 `parsed`。
注入 mock 网关（产 `parsed`）即可单测，无需图运行时、无需真 Postgres。

不变量 #2：agent 只声明 `tier`，绝不传具体 model（spec.tier 透传）。
不变量 #3：节点**只读、不写库**——结构化产物返回调用方，落库经入库端点（T5.2）。
不变量 #9：`spec.system_prompt` 进缓存断点前块（cache=True）；注入材料进 `input`（断点后）。
"""

from __future__ import annotations

import uuid
from collections.abc import Mapping, Sequence

import structlog
from pydantic import BaseModel
from ww_agents import (
    AgentSpec,
    CharacterCard,
    CharacterGenResult,
    Conflict,
    ContinuityReview,
    WorldGenResult,
)
from ww_llm_gateway.types import Block, LlmRequest, Scope

from ._protocols import GatewayRun

log = structlog.get_logger(__name__)


def _build_request(
    spec: AgentSpec,
    *,
    context: str,
    user_id: uuid.UUID,
    project_id: uuid.UUID | None,
) -> LlmRequest:
    """据 spec + 注入材料构造生成请求（纯函数，worldbuilder/character-gen 共用）。

    `spec.system_prompt` → `system` 缓存断点前块（cache=True，不变量 #9）；
    `context`（序列化的需求/约束/已有/已生成文本）→ `input`（断点后）。
    生成用 `run()` 非 `stream()`（要结构化产物，非流式正文）。
    `project_id` 可空——立项方案（⑤）在向导阶段跑，project 尚未建（usage_ledger.project_id 可空）。
    """
    return LlmRequest(
        tier=spec.tier,  # 不变量 #2：只传档位，不传 model
        system=[Block(text=spec.system_prompt, cache=True)],
        input=context,
        output_schema=spec.output_schema,
        scope=Scope(user_id=user_id, project_id=project_id),
    )


# ---- 通用生成器执行器（T6 · 创作工具箱通用框架）----


def build_brief_context(*, brief: str, project_context: str) -> str:
    """通用「brief_only」上下文：作品设定 + 作者一句话需求（确定性，无时间戳）。

    覆盖最简生成器（脑洞/书名…）：仅需作品设定 + 一句话方向。需求为空时给降级占位，
    交由 system_prompt 的「按题材自由发散」纪律处理。
    """
    brief_block = brief.strip() or "（作者未填具体方向，按题材自由发散）"
    return f"## 作品设定\n{project_context or '（暂无设定）'}\n\n## 创作需求\n{brief_block}"


def build_outline_chapter_context(
    *, chapter_no: int, beats: Sequence[str], project_context: str
) -> str:
    """通用「with_outline_chapter」上下文：作品设定 + 指定章的大纲节拍（确定性，无时间戳）。

    覆盖按章展开的生成器（开篇/细纲…）：注入作品设定 + 该章已排定的粗节拍，供模型
    据节拍展开。无节拍时给降级占位，交由 system_prompt 处理。
    """
    if beats:
        beats_block = "\n".join(f"{i + 1}. {beat}" for i, beat in enumerate(beats))
    else:
        beats_block = "（本章暂无大纲节拍）"
    return (
        f"## 作品设定\n{project_context or '（暂无设定）'}\n\n"
        f"## 第 {chapter_no} 章大纲节拍\n{beats_block}"
    )


def build_continuation_context(
    *, project_context: str, prior_text: str, beats: Sequence[str] | None = None
) -> str:
    """续写「with_prior_chapter」上下文：作品设定 + 前文正文 + 可选大纲节拍。

    覆盖续写生成器：注入最新已写正文供无缝承接；给了本章大纲节拍则一并注入供服务。
    确定性拼接、无时间戳/UUID（守不变量 #9 可缓存）。前文为空时给降级占位，交由
    system_prompt 处理。
    """
    prior_block = prior_text.strip() or "（暂无前文，请按设定与节拍起笔）"
    if beats:
        beats_block = "\n".join(f"{i + 1}. {beat}" for i, beat in enumerate(beats))
    else:
        beats_block = "（本章暂无大纲节拍，按前文自然推进）"
    return (
        f"## 作品设定\n{project_context or '（暂无设定）'}\n\n"
        f"## 前文正文（续写须无缝承接）\n{prior_block}\n\n"
        f"## 本章大纲节拍\n{beats_block}"
    )


def build_text_input_context(*, project_context: str, text: str, brief: str) -> str:
    """原文输入「text_input」上下文：作品设定 + 待处理原文 + 作者需求。

    覆盖扩写/降AI率/拆书：注入作者提供的原文样本 + 一句话方向。确定性拼接、无时间戳。
    需求为空时给降级占位，交由 system_prompt 的纪律处理。
    """
    text_block = text.strip() or "（未提供原文）"
    brief_block = brief.strip() or "（作者未填具体方向，按默认目标处理）"
    return (
        f"## 作品设定\n{project_context or '（暂无设定）'}\n\n"
        f"## 原文\n{text_block}\n\n"
        f"## 创作需求\n{brief_block}"
    )


async def run_generator(
    spec: AgentSpec,
    *,
    context: str,
    gateway: GatewayRun,
    user_id: uuid.UUID,
    project_id: uuid.UUID | None = None,
) -> BaseModel:
    """通用生成器执行器（T6）：构请求 → `gateway.run` → 按 `spec.output_schema` 校验 parsed。

    任一声明式生成器共用此路径（不绑定具体产物类型，据 spec 的 output_schema 校验）。
    **只读、不写库**（纯预览，不变量 #3）；独立生成——网关失败直接上抛（端点处理），不静默吞。
    `project_id` 可空——立项方案（⑤）在向导阶段跑，project 尚未建（usage_ledger.project_id 可空）。
    """
    expected = spec.output_schema
    if expected is None:
        raise ValueError(f"generator spec {spec.name!r} has no output_schema")
    req = _build_request(spec, context=context, user_id=user_id, project_id=project_id)
    resp = await gateway.run(req)
    parsed = resp.parsed
    if not isinstance(parsed, expected):
        log.error(
            "generator_node_missing_parsed",
            spec=spec.name,
            project_id=str(project_id),
            parsed_type=type(parsed).__name__,
        )
        raise ValueError(f"gateway returned no parsed {expected.__name__} for {spec.name} request")
    return parsed


# ---- worldbuilder ----


def build_worldbuilder_context(*, brief: str, project_context: str) -> str:
    """拼装 worldbuilder 注入文本（确定性，无时间戳）。"""
    return f"## 作品设定\n{project_context}\n\n## 世界观需求\n{brief}"


async def run_worldbuilder(
    spec: AgentSpec,
    *,
    brief: str,
    project_context: str,
    gateway: GatewayRun,
    user_id: uuid.UUID,
    project_id: uuid.UUID,
) -> WorldGenResult:
    """跑世界观生成：构请求 → `gateway.run` → 返回结构化 `WorldGenResult`。

    独立生成（非并行审）：网关失败直接上抛（端点/T5.2 处理），不静默吞。
    **只产结构化世界观、不写库**（持久化属 T5.2，不变量 #3）。
    """
    context = build_worldbuilder_context(brief=brief, project_context=project_context)
    req = _build_request(spec, context=context, user_id=user_id, project_id=project_id)
    resp = await gateway.run(req)
    parsed = resp.parsed
    if not isinstance(parsed, WorldGenResult):
        log.error(
            "worldbuilder_node_missing_parsed",
            project_id=str(project_id),
            parsed_type=type(parsed).__name__,
        )
        raise ValueError("gateway returned no parsed WorldGenResult for worldbuilder request")
    return parsed


# ---- character-gen（群像防雷同）----


def _render_cards_for_context(cards: Sequence[CharacterCard]) -> str:
    """把角色卡序列化为简表（name / role / traits）供防雷同对照与校验。

    确定性（保入参顺序）、无时间戳/UUID。只取差异化判定相关的关键字段。
    """
    lines: list[str] = []
    for card in cards:
        traits = "、".join(card.traits) if card.traits else "（未列）"
        lines.append(f"- {card.name}（{card.role}）：{traits}")
    return "\n".join(lines)


def _render_role_mix(role_mix: Mapping[str, int]) -> str:
    """把定位配比序列化为清单（保插入序，确定性，无时间戳）。"""
    return "\n".join(f"- {role}：{n} 个" for role, n in role_mix.items())


def build_character_gen_context(
    *,
    brief: str,
    count: int,
    role: str | None,
    world_context: str,
    existing_chars: Sequence[CharacterCard],
    generated_so_far: Sequence[CharacterCard],
    role_mix: Mapping[str, int] | None = None,
) -> str:
    """拼装 character-gen 注入文本（含群像防雷同上下文，M5-a；⑦ 群像按定位配比）。

    注入「已有角色」+「本批已生成卡」，要求新卡与二者差异化（避免一群人一个模子）。
    `role_mix` 在场时改注入定位配比块（各定位产出对应数量、合计等于数量），覆盖单一 `role`。
    确定性拼接、无时间戳——同输入同输出，便于单测与缓存。
    """
    if role_mix:
        role_line = (
            "角色定位配比（严格按此分配，各定位产出对应数量，合计等于数量）：\n"
            f"{_render_role_mix(role_mix)}"
        )
    else:
        role_line = f"角色定位：{role}" if role else "角色定位：未指定（由你按需求判定）"
    existing_block = (
        _render_cards_for_context(existing_chars) if existing_chars else "（暂无已有角色）"
    )
    generated_block = (
        _render_cards_for_context(generated_so_far) if generated_so_far else "（本批尚无已生成卡）"
    )
    return (
        "## 世界观约束（取名/能力须契合的硬规则）\n"
        f"{world_context or '（暂无世界观设定）'}\n\n"
        "## 已有角色（新角色须与之区分；可与之建关系）\n"
        f"{existing_block}\n\n"
        "## 本批已生成卡（后续卡须与之差异化，防雷同）\n"
        f"{generated_block}\n\n"
        "## 本次生成需求\n"
        f"需求：{brief}\n"
        f"数量：{count}\n"
        f"{role_line}"
    )


async def run_character_gen(
    spec: AgentSpec,
    *,
    brief: str,
    count: int,
    role: str | None,
    world_context: str,
    existing_chars: Sequence[CharacterCard],
    generated_so_far: Sequence[CharacterCard],
    gateway: GatewayRun,
    user_id: uuid.UUID,
    project_id: uuid.UUID,
    role_mix: Mapping[str, int] | None = None,
) -> CharacterGenResult:
    """跑角色/群像生成：构防雷同上下文 → `gateway.run` → 返回 `CharacterGenResult`。

    群像防雷同（M5-a）：注入「已有角色 + 本批已生成卡」要求差异化（§4.5 / §6.5）。
    `role_mix`（⑦）在场时按定位配比分配（各定位对应数量、合计等于数量）。
    独立生成（非并行审）：网关失败直接上抛（端点/T5.2 处理），不静默吞。
    **只产结构化角色卡、不写库**——入库前过 `precheck_generated_cards` 校验、再经
    入库端点持久化（持久化属 T5.2，不变量 #3）。
    """
    context = build_character_gen_context(
        brief=brief,
        count=count,
        role=role,
        world_context=world_context,
        existing_chars=existing_chars,
        generated_so_far=generated_so_far,
        role_mix=role_mix,
    )
    req = _build_request(spec, context=context, user_id=user_id, project_id=project_id)
    resp = await gateway.run(req)
    parsed = resp.parsed
    if not isinstance(parsed, CharacterGenResult):
        log.error(
            "character_gen_node_missing_parsed",
            project_id=str(project_id),
            parsed_type=type(parsed).__name__,
        )
        raise ValueError("gateway returned no parsed CharacterGenResult for character-gen request")
    return parsed


# ---- 入库前 continuity 校验缝（ARCH §6.5 / §4.5 三关键能力之一）----


def build_precheck_context(
    *,
    cards: Sequence[CharacterCard],
    world_context: str,
    characters_context: str,
) -> str:
    """拼装「入库前 continuity 校验」注入文本（确定性，无时间戳）。

    校验材料 = 待入库的**生成角色卡** + 世界观硬规则 + 已有角色真相源——
    让 continuity 续审逐项比对，找出与既有设定/力量体系/已有角色的冲突。
    """
    return (
        "## 待校验：本次生成的角色卡（尚未入库）\n"
        f"{_render_cards_for_context(cards)}\n\n"
        "## 真相源：世界观硬规则\n"
        f"{world_context or '（暂无世界观设定）'}\n\n"
        "## 真相源：已有角色\n"
        f"{characters_context or '（暂无已有角色）'}"
    )


async def precheck_generated_cards(
    spec: AgentSpec,
    *,
    cards: Sequence[CharacterCard],
    world_context: str,
    characters_context: str,
    gateway: GatewayRun,
    user_id: uuid.UUID,
    project_id: uuid.UUID,
) -> list[Conflict]:
    """入库前 continuity 校验（ARCH §6.5）：跑 `continuity_spec` 比对生成卡 vs 真相源。

    **编排器追加的一道检查**（非 character-gen 直接互调，守 §5.4 数据流/不变量 #1）。
    复用 `continuity_spec`（analyst 档，只读）；返回冲突清单供 T5.2 入库端点做 gate
    （有冲突 → 提示作者裁决/调整，不静默入库）。

    裸函数（显式 gateway 关键字），注入 mock 网关即可单测。校验失败语义同独立生成：
    网关失败直接上抛（端点处理）；带 `output_schema` 时 parsed 必非 None（C1），违约报错。
    本缝**只读、不写库**（不变量 #3）。
    """
    context = build_precheck_context(
        cards=cards,
        world_context=world_context,
        characters_context=characters_context,
    )
    req = _build_request(spec, context=context, user_id=user_id, project_id=project_id)
    resp = await gateway.run(req)
    parsed = resp.parsed
    if not isinstance(parsed, ContinuityReview):
        log.error(
            "precheck_node_missing_parsed",
            project_id=str(project_id),
            parsed_type=type(parsed).__name__,
        )
        raise ValueError("gateway returned no parsed ContinuityReview for precheck request")
    return list(parsed.conflicts)
