"""验收事务 + 冲突 gate（T2.4；ARCH §5.5 四步，不变量 #3/#4/#5）。

把作者裁决后的变更**单原子事务**落库（R3/R4）：晋升终稿 → 终稿提炼 digest →
裁决留痕 →（占位）人物/伏笔状态。digest 提炼在**事务外**先做（R2）。

冲突 gate（R5，事务前拦截）：核对最近一次审稿留痕的**每个冲突**在裁决清单里都有
采纳/忽略/手改之一；缺判 → `AppError(CONFLICT_UNRESOLVED)`，不写库。

accept 是**确定性事务代码**，不跑 graph 正文/审稿节点；真相从领域表（`chapters` /
`chapter_reviews`）重读（R3，不变量 #5）。提交边界：全部只 flush → 末尾一次
`await session.commit()`；任一步失败 → 整体回滚。
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass
from typing import Protocol

import structlog
from ww_core.domain.chapter_repo import ChapterRepo
from ww_core.domain.digest_repo import DigestAppendRepo
from ww_core.domain.review_repo import ReviewRepo, ReviewView
from ww_shared import AppError, ErrorCode

from ww_api.schemas.projects import ConflictDecision

log = structlog.get_logger(__name__)


class Committable(Protocol):
    """验收事务对 session 的**最小**依赖：仅需 `commit()`（去掉 object + type ignore）。

    真实运行注 `AsyncSession`；测试注 fake session（同形即可）——两者都满足本 Protocol。
    """

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


@dataclass(frozen=True)
class AcceptOutcome:
    """验收事务结果（供端点组「本次将更新」清单）。"""

    accepted_version: int
    digest_added: bool
    decisions_recorded: int
    review_id: uuid.UUID | None


def assert_conflicts_resolved(
    latest_review: ReviewView | None,
    decisions: list[ConflictDecision],
) -> None:
    """冲突 gate（R5）：最近审稿的每个冲突都须有裁决，否则 `CONFLICT_UNRESOLVED`。

    判据：以最近审稿留痕的 `conflicts` 列表下标为冲突身份；裁决清单里出现的
    `conflict_index` 集合必须**覆盖** `range(len(conflicts))`。缺判 → 拦截（不写库）。
    无审稿留痕或零冲突 → 直接通过（无需裁决）。纯函数：只判定、不副作用。
    """
    if latest_review is None:
        return
    conflict_count = len(latest_review.conflicts)
    if conflict_count == 0:
        return
    decided = {d.conflict_index for d in decisions}
    missing = [i for i in range(conflict_count) if i not in decided]
    if missing:
        raise AppError(
            ErrorCode.CONFLICT_UNRESOLVED,
            "存在未裁决的冲突，无法验收：请对每个冲突选择 采纳/忽略/手改",
            {"missing_conflict_indices": missing, "conflict_count": conflict_count},
        )


def _serialize_decisions(
    latest_review: ReviewView | None,
    decisions: list[ConflictDecision],
) -> dict[str, object]:
    """把裁决清单序列化为可落 `chapter_reviews.decisions` 的 JSON 形（确定性）。"""
    return {
        "items": [
            {
                "conflict_index": d.conflict_index,
                "verdict": d.verdict,
                "note": d.note,
            }
            for d in sorted(decisions, key=lambda x: x.conflict_index)
        ],
        "conflict_count": len(latest_review.conflicts) if latest_review else 0,
    }


async def run_accept_transaction(
    *,
    session: Committable,
    chapter_repo: ChapterRepo,
    digest_repo: DigestAppendRepo,
    review_repo: ReviewRepo,
    project_id: uuid.UUID,
    chapter_no: int,
    final_text: str,
    digest_facts: dict[str, object],
    latest_review: ReviewView | None,
    decisions: list[ConflictDecision],
) -> AcceptOutcome:
    """单原子事务落库（R3/R4，§5.5 步骤 1–4），末尾一次 commit。

    `digest_facts` 已在事务外提炼好（R2）。各 repo 写方法只 flush；本函数统一在末尾
    `await session.commit()`，任一步抛错由调用方/上下文回滚（不显式半提交）。
    `session` 用最小 `Committable` Protocol（只需 `commit()`）以免绑定 SQLAlchemy，
    测试注入 fake session 亦满足（去掉了 `object` + `type: ignore`）。
    """
    # 步骤 1：终稿晋升 accepted 新 version（max+1，草稿行保留，R4）。
    chapter = await chapter_repo.promote_to_accepted(project_id, chapter_no, content=final_text)

    # 步骤 2：终稿 digest 追加（append-only，不变量 #4）。
    await digest_repo.append(project_id, chapter_no, facts=digest_facts)

    # 步骤 4：裁决留痕（写到最近一次审稿行；无审稿行则跳过）。
    review_id: uuid.UUID | None = None
    if latest_review is not None:
        updated = await review_repo.set_decisions(
            latest_review.id,
            decisions=_serialize_decisions(latest_review, decisions),
        )
        review_id = updated.id

    # 步骤 3：伏笔到期扫描——验收提交**后**经端点登记的 BackgroundTask 跑（M3-b/d，
    # `services/foreshadow_scan.run_overdue_scan`：current_ch > expected_close_to 且未 CLOSED
    # → 置 OVERDUE，确定性纯函数非 AI，不变量 #3）。放在事务外/提交后是因为它是验收**结果**
    # 的副作用、且自建独立 session（请求 session 此时已关闭）。
    # 人物 latest_state 更新仍留后续（M4+）：本事务只落晋升 + digest + 裁决留痕。

    await session.commit()

    log.info(
        "chapter_accepted",
        project_id=str(project_id),
        chapter_no=chapter_no,
        accepted_version=chapter.version,
        decisions_recorded=len(decisions),
        review_id=str(review_id) if review_id else None,
    )
    return AcceptOutcome(
        accepted_version=chapter.version,
        digest_added=True,
        decisions_recorded=len(decisions),
        review_id=review_id,
    )
