"""collect 节点（C4 扩 / ARCH §5.2 collect 行）——汇总并行审 → 落 `chapter_reviews` 留痕。

并行审各把 `{spec.name: {status, result}}` 合并进 `state["reviews"]`（reducer，见 state.py）；
collect 把它们汇总，**一次** `review_repo.record(...)` append 一行 `chapter_reviews`（真相源留痕），
按 spec.name 把各审结果映射到对应列。

不变量 #3：审稿节点只读不写库；**唯一写入点是这里经 `review_repo`**（仍是「留痕」非「生效」，
AI 产出真正入库经验收事务的裁决）。

记账边界（gotcha「网关 ledger 只 flush」）：审稿经网关产 usage（网关内 `flush` 进同一 session）；
collect 与 review_repo 同样**只 flush 不 commit**——`commit` 归 HTTP 端点 / 验收事务。
本节点绝不 commit。

M4 四审齐 → 列映射（一次 record 落齐本章四审）+ 灵感③ advisory 人物塑造第五审：
- continuity      → `conflicts`（冲突清单 list）；
- foreshadow      → `foreshadow_sug`（planted/resolved 扁平为建议 list，每条带 `kind` 标记）；
- pace            → `pace`（节奏诊断 dict：water/hook/beat_map）；
- style           → `style`（文风漂移诊断 dict：score/segments）；
- characterization→ `characterization`（人物塑造诊断 dict：issues；advisory，不进验收 gate）。
任一审 `incomplete`（网关失败隔离，§5.2）→ 该列留空/None，不阻塞其余。
"""

from __future__ import annotations

import uuid
from typing import Any, Protocol

import structlog

from .review_node import REVIEW_OK
from .state import ChapterState

log = structlog.get_logger(__name__)

CONTINUITY = "continuity"
FORESHADOW = "foreshadow"
PACE = "pace"
STYLE = "style"
CHARACTERIZATION = "characterization"  # 灵感③ advisory 人物塑造审（仅建议、不阻断验收）

# foreshadow 建议扁平化时的来源标记（planted=新埋 / resolved=回收）。
FORESHADOW_KIND_PLANTED = "planted"
FORESHADOW_KIND_RESOLVED = "resolved"


class ReviewRecorder(Protocol):
    """collect 对审稿留痕 repo 的最小依赖——只需 `record`（注入真 repo 或内存 fake）。

    形对齐 `domain.review_repo.ReviewRepo.record`（只 flush 不 commit）。
    """

    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: ...


def _ok_result(reviews: dict[str, Any], name: str) -> dict[str, Any] | None:
    """取某审的结果 dict，仅当 `ok` 且有结果；否则 None（incomplete/缺席）。"""
    entry = reviews.get(name)
    if not entry or entry.get("status") != REVIEW_OK:
        return None
    result = entry.get("result")
    return result if isinstance(result, dict) else None


def extract_conflicts(reviews: dict[str, Any]) -> list[dict[str, Any]]:
    """从 continuity 分项抽冲突清单（纯函数）。

    仅当该审 `ok` 且有结果时取其 `conflicts`；未完成/缺席 → 空列表（不臆造）。
    """
    result = _ok_result(reviews, CONTINUITY)
    if result is None:
        return []
    return [dict(c) for c in result.get("conflicts") or []]


def extract_foreshadow_sug(reviews: dict[str, Any]) -> list[dict[str, Any]]:
    """从 foreshadow 分项扁平化建议清单（纯函数）。

    `ForeshadowReview{planted,resolved}` → 单个 list，每条加 `kind` 标记来源
    （`foreshadow_sug` 列是 JSONB list）。未完成/缺席 → 空列表。
    """
    result = _ok_result(reviews, FORESHADOW)
    if result is None:
        return []
    out: list[dict[str, Any]] = []
    for sug in result.get("planted") or []:
        out.append({**dict(sug), "kind": FORESHADOW_KIND_PLANTED})
    for sug in result.get("resolved") or []:
        out.append({**dict(sug), "kind": FORESHADOW_KIND_RESOLVED})
    return out


def extract_pace(reviews: dict[str, Any]) -> dict[str, Any] | None:
    """从 pace 分项取节奏诊断 dict（纯函数）。

    `PaceReview{water,hook,beat_map}` 整体入 `pace` 列（JSONB dict）。
    未完成/缺席 → None（列留空，§5.2 不阻塞其余）。
    """
    result = _ok_result(reviews, PACE)
    if result is None:
        return None
    return dict(result)


def extract_style(reviews: dict[str, Any]) -> dict[str, Any] | None:
    """从 style（第四审）分项取文风漂移诊断 dict（纯函数）。

    `StyleDriftReview{score,segments}` 整体入 `style` 列（JSONB dict）。
    未完成/缺席 → None（列留空，§5.2 不阻塞其余）。无指纹降级（score=100/空段）
    仍是 `ok` 结果，照常落库（区别于 incomplete）。
    """
    result = _ok_result(reviews, STYLE)
    if result is None:
        return None
    return dict(result)


def extract_characterization(reviews: dict[str, Any]) -> dict[str, Any] | None:
    """从 characterization（灵感③ advisory 审）分项取人物塑造诊断 dict（纯函数）。

    `CharacterizationReview{issues}` 整体入 `characterization` 列（JSONB dict）。
    未完成/缺席 → None（列留空，§5.2 不阻塞其余）。空 issues 降级仍是 `ok` 结果、照常落库。
    advisory：只留痕、不进 conflicts、不碰验收 gate（灵感 D2）。
    """
    result = _ok_result(reviews, CHARACTERIZATION)
    if result is None:
        return None
    return dict(result)


async def collect_reviews(
    state: ChapterState,
    *,
    review_repo: ReviewRecorder,
) -> dict[str, Any]:
    """collect 节点：汇总并行审 → 各列映射 → 一次 `review_repo.record` 留痕（只 flush 不 commit）。

    直接调用本函数（注入内存 fake repo）即可单测，无需图运行时。
    `chapter_version=None`：留痕行不绑具体 version（验收时再绑/裁决，R3/R4）。
    某审 incomplete → 该列空/None，不阻塞其余审落库（§5.2）。
    返回空增量字典——审稿真相已在表，state 不作真相源（不变量 #5）。
    """
    reviews = state.get("reviews") or {}
    conflicts = extract_conflicts(reviews)
    foreshadow_sug = extract_foreshadow_sug(reviews)
    pace = extract_pace(reviews)
    style = extract_style(reviews)
    characterization = extract_characterization(reviews)
    await review_repo.record(
        state["project_id"],
        state["chapter_no"],
        chapter_version=None,
        conflicts=conflicts,
        foreshadow_sug=foreshadow_sug,
        style=style,
        pace=pace,
        characterization=characterization,
    )
    log.info(
        "collect_reviews_recorded",
        project_id=str(state["project_id"]),
        chapter_no=state["chapter_no"],
        conflict_count=len(conflicts),
        foreshadow_sug_count=len(foreshadow_sug),
        has_pace=pace is not None,
        has_style=style is not None,
        has_characterization=characterization is not None,
        reviews=sorted(reviews.keys()),
    )
    # 留痕已在表（真相源）；不在节点 commit（端点/事务层负责，见模块 docstring）。
    return {}
