"""审稿节点 + 审稿上下文拼装（C4 扩 / ARCH §5.2 §5.4 reviewer 行）。

M2 先接 **continuity**（C6 `continuity_spec`），设计成可扩——图按一组 review specs
循环加并行分支（M3/M4 再加 foreshadow/style/pace 三审）。

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

不变量 #2：agent 只声明 `tier`，绝不传具体 model（spec.tier 透传）。
不变量 #3：审稿节点**只读、不写库**——结构化结果回到 state，落库经 collect→review_repo。
不变量 #9：`spec.system_prompt` 进缓存断点前块（cache=True）；草稿+近况材料进 `input`（断点后）。

任一审失败不阻塞其余（§5.2）：节点内捕获网关异常 → 返回该审「未完成」结构化占位，
不上抛、不毁图——其余并行分支照常汇入 collect。
"""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from typing import Any

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

from ._protocols import GatewayRun
from .state import ChapterState

log = structlog.get_logger(__name__)

# 审稿分项状态（落 state[reviews][name].status；collect / SSE 据此分流）。
REVIEW_OK = "ok"
REVIEW_INCOMPLETE = "incomplete"  # 该审网关失败 → 标未完成，不阻塞其余（§5.2）

# 已绑定 gateway 的节点形（图工厂 / T2.5 直接跑审稿时用）。
BoundReviewNode = Callable[[ChapterState], Awaitable[dict[str, Any]]]

__all__ = [
    "REVIEW_INCOMPLETE",
    "REVIEW_OK",
    "BoundReviewNode",
    "GatewayRun",
    "build_review_context",
    "build_review_request",
    "make_review_node",
    "run_review",
]


def build_review_context(*, draft: str, stable_core: str, volatile: str) -> str:
    """拼装审稿注入文本（确定性，无时间戳）。

    审稿材料 = 本章草稿 + 近况摘要/人物卡/世界观硬规则——后者复用 `memory.assemble`
    的 `stable_core`(世界观硬规则/定型主角/文风) + `volatile`(注入卡片/伏笔/近况)。
    纯拼接、无随机/时间戳 → 同输入同输出，便于单测与缓存。
    """
    return (
        "## 审稿材料：作品真相源（硬规则 / 人物卡 / 近况）\n"
        f"{stable_core}\n\n{volatile}\n\n"
        "## 待审本章草稿\n"
        f"{draft}"
    )


def build_review_request(spec: AgentSpec, state: ChapterState) -> LlmRequest:
    """据 spec + 审稿上下文构造审稿请求（纯函数）。

    `spec.system_prompt` → `system` 缓存断点前块（cache=True，不变量 #9）；
    `review_context`（草稿 + 近况材料）→ `input`（断点后）。续审用 `run()` 非 `stream()`。
    """
    return LlmRequest(
        tier=spec.tier,  # 不变量 #2：只传档位，不传 model
        system=[Block(text=spec.system_prompt, cache=True)],
        input=state["review_context"],
        output_schema=spec.output_schema,
        scope=Scope(user_id=state["user_id"], project_id=state["project_id"]),
    )


def make_review_node(spec: AgentSpec, gateway: GatewayRun) -> BoundReviewNode:
    """工厂：把一个审稿 `AgentSpec` + 网关包成 LangGraph 节点 `async (state) -> dict`。

    网关经闭包绑定（langgraph 不收 functools.partial，见 gotcha；用 `async def` 闭包）。
    节点产 `{"reviews": {spec.name: {...}}}`——结构化结果回到 state（**只读不写库**，
    不变量 #3）。`run_review` 是裸函数（显式 gateway 关键字），供直接单测注入 mock。
    T2.5 若想「直接跑某审」可用本工厂或 `run_review`。
    """

    async def _node(state: ChapterState) -> dict[str, Any]:
        return await run_review(spec, state, gateway=gateway)

    return _node


async def run_review(
    spec: AgentSpec,
    state: ChapterState,
    *,
    gateway: GatewayRun,
) -> dict[str, Any]:
    """跑单个审稿：构请求 → `gateway.run` → 归一为 `{spec.name: {status, result}}`。

    直接调用本函数（注入 mock 网关）即可单测，无需图运行时。
    任一审失败 → 捕获异常、返回 `incomplete` 占位（§5.2：不阻塞其余、不毁图）。
    返回**增量字典**（LangGraph 合并进 `state["reviews"]`），不原地改 `state`。
    """
    req = build_review_request(spec, state)
    try:
        resp = await gateway.run(req)
    except Exception as exc:  # noqa: BLE001 — 该审失败隔离：标未完成，不上抛（§5.2）
        log.warning(
            "review_node_incomplete",
            review=spec.name,
            project_id=str(state["project_id"]),
            chapter_no=state["chapter_no"],
            error=str(exc),
        )
        return {"reviews": {spec.name: {"status": REVIEW_INCOMPLETE, "result": None}}}

    parsed = resp.parsed
    result = parsed.model_dump() if parsed is not None else None
    return {"reviews": {spec.name: {"status": REVIEW_OK, "result": result}}}
