"""大纲节点（C6 扩 / ARCH §5.4 outliner 行 / §6.2 伏笔窗口）。

大纲是**独立生成**——不在写章/审稿流水线（不接进 review 图）。端点（T3.5）直接调
`run_outline` 拿结构化 `OutlineResult`，再持久化到 `outline` 表（含 foreshadow_windows）。

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

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

from __future__ import annotations

import uuid

import structlog
from ww_agents import AgentSpec, OutlineResult
from ww_llm_gateway.types import Block, LlmRequest, Scope

from ._protocols import GatewayRun

log = structlog.get_logger(__name__)


def build_outline_request(
    spec: AgentSpec,
    *,
    context: str,
    user_id: uuid.UUID,
    project_id: uuid.UUID,
) -> LlmRequest:
    """据 spec + 注入材料构造大纲请求（纯函数）。

    `spec.system_prompt` → `system` 缓存断点前块（cache=True，不变量 #9）；
    `context`（设定/伏笔/人物/世界观的序列化文本）→ `input`（断点后）。
    排大纲用 `run()` 非 `stream()`（要结构化产物，非流式正文）。
    """
    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),
    )


async def run_outline(
    spec: AgentSpec,
    *,
    context: str,
    gateway: GatewayRun,
    user_id: uuid.UUID,
    project_id: uuid.UUID,
) -> OutlineResult:
    """跑大纲生成：构请求 → `gateway.run` → 返回结构化 `OutlineResult`。

    裸函数（显式 gateway 关键字），注入 mock 网关即可单测，无需图运行时。
    大纲是独立生成（非并行审）：网关失败直接上抛（端点/T3.5 处理），不静默吞。
    **只产结构化大纲、不写库**（持久化属 T3.5，不变量 #3）。
    """
    req = build_outline_request(spec, context=context, user_id=user_id, project_id=project_id)
    resp = await gateway.run(req)
    parsed = resp.parsed
    if not isinstance(parsed, OutlineResult):
        # 带 output_schema 时 parsed 必非 None（C1）；违约则明确报错而非返回空大纲。
        log.error(
            "outline_node_missing_parsed",
            project_id=str(project_id),
            parsed_type=type(parsed).__name__,
        )
        raise ValueError("gateway returned no parsed OutlineResult for outline request")
    return parsed
