"""多章链节点（chain-workflow 设计 §3.2）——write → review → decide → accept 循环。

节点是**小确定性单元**：LLM 非确定性锁在网关后；`decide` 是纯逻辑。每个做 DB IO 的
节点用**短事务、独立 session**（每章 accept 原子提交，仿 `run_overdue_scan` 自建 session）。
checkpoint 只存控制流，正文/审稿落领域表——resume 时重读领域表为真相（不变量 #1/#3/#5）。

依赖注入：本模块的节点是裸异步函数（显式关键字依赖），供直接单测注入 fake；图工厂
（`graph.py`）用默认参绑定把依赖闭包绑进图节点（langgraph 不收 functools.partial，见 gotcha）。
"""

from __future__ import annotations

import uuid
from collections.abc import Awaitable, Callable, Sequence
from contextlib import AbstractAsyncContextManager
from typing import Any, Protocol

import structlog
from ww_agents import AgentSpec

from .._protocols import GatewayRun
from ..collect import collect_reviews
from ..generation_node import build_continuation_context
from ..review_node import build_review_context, run_review
from ..state import ChapterState
from ..write_node import build_write_request
from .state import ChainState

log = structlog.get_logger(__name__)

#: 续写式链 key：写章以上一章已验收正文末尾作前文引子（F2）。
CHAIN_CONTINUE_VOLUME = "continue_volume"

# ---- 注入缝（图工厂经默认参绑定，单测直接注 fake）----

#: `session -> Repo`：节点自建短事务里从 session 造 repo（仿端点依赖工厂）。
SessionFactory = Callable[[], AbstractAsyncContextManager[Any]]

#: `session -> Gateway`：按**节点当前 session** 建网关（不变量 #1）。
#: 关键：网关的 ledger sink 绑节点 session，节点末尾 `commit()` 一并持久 usage_ledger 行——
#: 绝不能复用请求阶段建好的网关（其 session 在 BackgroundTask 跑时已关闭，记账行会被静默丢弃）。
GatewayBuilder = Callable[[Any], Awaitable[GatewayRun]]


class AssembleContext(Protocol):
    """`assemble` 产出的最小读形——只需 stable_core/volatile（构造写章/审稿请求）。"""

    stable_core: str
    volatile: str


#: `(repos, project_id, chapter_no) -> AssembledContext`：组装记忆（确定性，不变量 #6/#9）。
AssembleFn = Callable[[Any, uuid.UUID, int], Awaitable[AssembleContext]]


class MemoryReposFactory(Protocol):
    """`session -> MemoryRepos`（assemble 所需 8 repo 捆绑）。"""

    def __call__(self, session: Any) -> Any: ...


class DraftView(Protocol):
    """章草稿读形最小依赖——只需 `content`（重读正文构审稿上下文）。"""

    content: str


class ChapterDraftRepo(Protocol):
    """章草稿 repo 最小依赖：write 落稿 `save_draft` + review 重读 `get_draft`（只 flush）。

    续写式链（`continue_volume`）写章另需 `latest_accepted`——读上一章已验收终稿正文作前文引子。
    """

    async def save_draft(
        self, project_id: uuid.UUID, chapter_no: int, *, text: str, volume: int = 1
    ) -> Any: ...

    async def get_draft(self, project_id: uuid.UUID, chapter_no: int) -> DraftView | None: ...

    async def latest_accepted(self, project_id: uuid.UUID, chapter_no: int) -> DraftView | None: ...


class ReviewRecordRepo(Protocol):
    """review/accept 节点对审稿 repo 的最小依赖：collect 留痕 + accept 重读最近审稿。"""

    async def record(
        self,
        project_id: uuid.UUID,
        chapter_no: int,
        *,
        chapter_version: int | None = None,
        conflicts: list[dict[str, Any]],
        foreshadow_sug: list[dict[str, Any]] | None = None,
        style: dict[str, Any] | None = None,
        pace: dict[str, Any] | None = None,
        characterization: dict[str, Any] | None = None,
        health_score: int | None = None,
    ) -> Any: ...

    async def list_for_chapter(self, project_id: uuid.UUID, chapter_no: int) -> list[Any]: ...


class CommitSession(Protocol):
    """节点短事务对 session 的最小依赖——只需 `commit()`（write/review 节点末尾提交）。"""

    async def commit(self) -> None: ...


#: accept 步：把作者裁决落库（晋升终稿 + digest + 裁决留痕 + 伏笔到期），各自独立 session。
#: 编排（链）在 core；具体验收事务（apps/api）经此闭包注入，守目录所有权 + 不变量 #3/#4。
#: 入参 `decisions` 为裁决清单（`accept_chapter` 节点经 interrupt 恢复值拿到，无冲突=[]）。
AcceptChapterOp = Callable[..., Awaitable[None]]


# ---- 节点本体（裸函数，显式依赖；图工厂绑定闭包）----


async def write_chapter(
    state: ChainState,
    *,
    gateway_builder: GatewayBuilder,
    session_factory: SessionFactory,
    memory_repos_factory: MemoryReposFactory,
    chapter_repo_factory: Callable[[Any], ChapterDraftRepo],
    assemble: AssembleFn,
) -> dict[str, Any]:
    """write 节点：组装记忆 → 收集版写章（非 SSE）→ 落 `chapters` draft → commit。

    短事务、独立 session（不跨章共享）。state 不变（正文在 DB）。返回空增量。
    收集版写章复用 `build_write_request` 同一 prompt 组装路径（stream=False → `gateway.run`），
    守不变量 #9（stable_core 进缓存前缀）；不确定性锁在网关后，注 mock 即可单测。

    网关经 `gateway_builder(session)` 按**本节点 session** 现建（不变量 #1）：其 ledger sink
    绑此 session，末尾 `commit()` 一并持久 usage_ledger——绝不复用请求阶段已关闭 session 的网关。
    """
    project_id = state["project_id"]
    chapter_no = state["current_chapter_no"]
    user_id = state["user_id"]
    is_continuation = state.get("chain_key") == CHAIN_CONTINUE_VOLUME
    async with session_factory() as session:
        gateway = await gateway_builder(session)
        repos = memory_repos_factory(session)
        context = await assemble(repos, project_id, chapter_no)
        chapter_repo = chapter_repo_factory(session)
        # 续写模式：volatile 改写为「设定 + 上一章已验收正文末尾」（不变量 #9：仅改断点后块，
        # stable_core 仍进缓存前缀）。前文经注入的 chapter_repo 重读（core 不 import apps/api）。
        if is_continuation:
            prior_text = await _read_prior_accepted(chapter_repo, project_id, chapter_no)
            volatile = build_continuation_context(
                project_context=context.volatile, prior_text=prior_text
            )
        else:
            volatile = context.volatile
        req = build_write_request(
            stable_core=context.stable_core,
            volatile=volatile,
            user_id=user_id,
            project_id=project_id,
        )
        # 收集版：非流式，一次 run 拿全文（链批量量产不走 SSE）。
        collect_req = req.model_copy(update={"stream": False})
        resp = await gateway.run(collect_req)
        await chapter_repo.save_draft(project_id, chapter_no, text=resp.text)
        await _commit(session)
    log.info(
        "chain_write_chapter",
        project_id=str(project_id),
        chapter_no=chapter_no,
        draft_len=len(resp.text),
    )
    return {}


async def review_chapter(
    state: ChainState,
    *,
    gateway_builder: GatewayBuilder,
    session_factory: SessionFactory,
    memory_repos_factory: MemoryReposFactory,
    chapter_repo_factory: Callable[[Any], ChapterDraftRepo],
    review_repo_factory: Callable[[Any], ReviewRecordRepo],
    assemble: AssembleFn,
    review_specs: Sequence[AgentSpec],
) -> dict[str, Any]:
    """review 节点：复用四审 → 落 `chapter_reviews` 留痕（不变量 #3 只读、不写正文）→ commit。

    复用现有 `run_review × specs` + `collect_reviews`（直接顺序跑各审，非起子图——链节点本身
    已是图节点，无需嵌套图）。从 DB 重读本章草稿构审稿上下文（真相在表，不信 state）。

    网关经 `gateway_builder(session)` 按**本节点 session** 现建（不变量 #1）：四审各调的
    usage_ledger 行随节点末尾 `commit()` 一并持久——绝不复用请求阶段已关闭 session 的网关。
    """
    project_id = state["project_id"]
    chapter_no = state["current_chapter_no"]
    user_id = state["user_id"]
    async with session_factory() as session:
        gateway = await gateway_builder(session)
        repos = memory_repos_factory(session)
        context = await assemble(repos, project_id, chapter_no)
        chapter_repo = chapter_repo_factory(session)
        draft_view = await _read_draft(chapter_repo, project_id, chapter_no)
        review_context = build_review_context(
            draft=draft_view,
            stable_core=context.stable_core,
            volatile=context.volatile,
        )
        review_state: ChapterState = {
            "project_id": project_id,
            "chapter_no": chapter_no,
            "user_id": user_id,
            "review_context": review_context,
        }
        reviews: dict[str, Any] = {}
        for spec in review_specs:
            part = await run_review(spec, review_state, gateway=gateway)
            reviews.update(part.get("reviews", {}))
        review_state["reviews"] = reviews
        review_repo = review_repo_factory(session)
        await collect_reviews(review_state, review_repo=review_repo)
        await _commit(session)
    log.info(
        "chain_review_chapter",
        project_id=str(project_id),
        chapter_no=chapter_no,
        reviews=sorted(reviews.keys()),
    )
    return {}


async def decide(
    state: ChainState,
    *,
    session_factory: SessionFactory,
    review_repo_factory: Callable[[Any], ReviewRecordRepo],
) -> dict[str, Any]:
    """decide 节点（确定性纯逻辑，无 LLM）：读最近审稿留痕 → `has_conflicts`。

    冲突明文**不入** state（不变量 #5）；只写控制流标志，accept 时再从领域表重读冲突。
    """
    project_id = state["project_id"]
    chapter_no = state["current_chapter_no"]
    async with session_factory() as session:
        review_repo = review_repo_factory(session)
        history = await review_repo.list_for_chapter(project_id, chapter_no)
    latest = history[0] if history else None
    conflicts = _conflicts_of(latest)
    has_conflicts = len(conflicts) > 0
    log.info(
        "chain_decide",
        project_id=str(project_id),
        chapter_no=chapter_no,
        has_conflicts=has_conflicts,
        conflict_count=len(conflicts),
    )
    return {"has_conflicts": has_conflicts}


async def accept_chapter(
    state: ChainState,
    *,
    decisions: list[Any],
    accept_op: AcceptChapterOp,
) -> dict[str, Any]:
    """accept 节点：用裁决落库（晋升终稿 + digest + 裁决留痕 + 伏笔到期），推进章号。

    具体验收事务（重读 `chapter_reviews`、`assert_conflicts_resolved` gate、终稿提炼 digest、
    `run_accept_transaction`、`run_overdue_scan`）属 apps/api，经 `accept_op` 闭包注入——守目录
    所有权 + 不变量 #3/#4（缺判 → `CONFLICT_UNRESOLVED` 由 op 抛，链失败）。

    `decisions`：无冲突章 = []；有冲突章 = interrupt 恢复值（作者裁决清单）。
    返回 `current_chapter_no += 1` + `written` 追加本章——推进控制流（不可变增量）。
    """
    project_id = state["project_id"]
    chapter_no = state["current_chapter_no"]
    user_id = state["user_id"]
    await accept_op(
        project_id=project_id,
        chapter_no=chapter_no,
        user_id=user_id,
        decisions=decisions,
    )
    written = list(state.get("written") or [])
    written.append(chapter_no)
    log.info(
        "chain_accept_chapter",
        project_id=str(project_id),
        chapter_no=chapter_no,
        written_count=len(written),
    )
    return {
        "current_chapter_no": chapter_no + 1,
        "written": written,
        "has_conflicts": False,
    }


# ---- 辅助（纯函数 / 小工具）----


def _conflicts_of(latest_review: Any) -> list[dict[str, Any]]:
    """从最近审稿留痕取冲突清单（兼容 ReviewView Pydantic 与内存 fake dict）。"""
    if latest_review is None:
        return []
    conflicts = getattr(latest_review, "conflicts", None)
    if conflicts is None and isinstance(latest_review, dict):
        conflicts = latest_review.get("conflicts")
    return list(conflicts or [])


async def _read_draft(
    chapter_repo: ChapterDraftRepo, project_id: uuid.UUID, chapter_no: int
) -> str:
    """从 repo 重读本章草稿正文（真相在表，不信 state）。"""
    view = await chapter_repo.get_draft(project_id, chapter_no)
    return view.content if view is not None else ""


async def _read_prior_accepted(
    chapter_repo: ChapterDraftRepo, project_id: uuid.UUID, chapter_no: int
) -> str:
    """续写式链：读上一章（chapter_no-1）已验收终稿正文作前文引子（不变量 #1/#4）。

    第一章（前一章号 < 1）或前一章未验收 → 空串，由 `build_continuation_context` 降级占位。
    """
    if chapter_no <= 1:
        return ""
    view = await chapter_repo.latest_accepted(project_id, chapter_no - 1)
    return view.content if view is not None else ""


async def _commit(session: CommitSession) -> None:
    """提交短事务（session 满足 `CommitSession`：只需 `commit()`）。"""
    await session.commit()


# decide 暴露给条件边的辅助（纯判定，便于单测）。
def has_conflicts(state: ChainState) -> bool:
    """条件边读 state 的冲突标志（缺省 False）。"""
    return bool(state.get("has_conflicts"))


__all__ = [
    "CHAIN_CONTINUE_VOLUME",
    "AcceptChapterOp",
    "AssembleContext",
    "AssembleFn",
    "ChainState",
    "ChapterDraftRepo",
    "MemoryReposFactory",
    "ReviewRecordRepo",
    "SessionFactory",
    "GatewayBuilder",
    "accept_chapter",
    "decide",
    "has_conflicts",
    "review_chapter",
    "write_chapter",
]
