"""SSE 归一层（C4 / ARCH §7.3）——把网关 `Delta` 流归一为 SSE 事件对象。

§7.3 全量事件：`token`/`section`/`conflict`/`done`/`error`。**M1 写章只需 `token`/`done`/`error`**
（`section`/`conflict` 属 M2 四审）。本模块产出**与传输无关的事件对象**——
HTTP `StreamingResponse`/`text/event-stream` 编码归 T1.4（apps/api），不在此处。

错误处理（CLAUDE.md「Logging」）：底层流抛错 → 发一条 `error` 事件（带 `code`/`message`/
`request_id`，对齐错误信封 §7.1）后**正常收尾**，绝不把异常泄到调用方循环里。
"""

from __future__ import annotations

from collections.abc import AsyncIterator, Mapping
from typing import Any

import structlog
from pydantic import BaseModel
from ww_llm_gateway.types import Delta
from ww_shared import AppError, ErrorCode

from .collect import CHARACTERIZATION, CONTINUITY, FORESHADOW, PACE, STYLE
from .review_node import REVIEW_OK

log = structlog.get_logger(__name__)

# §7.3 事件名（全集）。完整清单以 ARCHITECTURE §7.3 为准。
EVENT_TOKEN = "token"  # 正文增量（写章 draft）
EVENT_SECTION = "section"  # 四审分项开始/完成（审稿 review）
EVENT_CONFLICT = "conflict"  # continuity 冲突命中（审稿 review）
EVENT_FORESHADOW = "foreshadow"  # foreshadow 建议命中（新埋/回收，审稿 review）
EVENT_PACE = "pace"  # pace 节奏诊断（注水/钩子/节拍图，审稿 review）
EVENT_STYLE = "style"  # style 文风漂移诊断（整体相似度 + 漂移段，审稿 review）
EVENT_CHARACTERIZATION = "characterization"  # 人物塑造诊断（advisory issues，审稿 review）
EVENT_DONE = "done"
EVENT_ERROR = "error"

# section 状态（对齐 review_node.REVIEW_OK / REVIEW_INCOMPLETE，外加分项「开始」）。
SECTION_STARTED = "started"
SECTION_DONE = "done"
SECTION_INCOMPLETE = "incomplete"


class SseEvent(BaseModel):
    """归一后的 SSE 事件对象（snake_case）；T1.4 据此编码为 text/event-stream。"""

    model_config = {"frozen": True}

    event: str
    data: dict[str, object]


def token_event(text: str) -> SseEvent:
    return SseEvent(event=EVENT_TOKEN, data={"text": text})


def done_event(*, length: int) -> SseEvent:
    """完成事件：只带长度（不回灌全文——脱敏 + 前端已逐 token 累积）。"""
    return SseEvent(event=EVENT_DONE, data={"length": length})


def section_event(*, name: str, status: str) -> SseEvent:
    """四审分项事件（§7.3 `section`）：`{name, status}`——前端报告分区进度。"""
    return SseEvent(event=EVENT_SECTION, data={"name": name, "status": status})


def conflict_event(
    *,
    type: str,
    where: str,
    refs: list[str],
    suggestion: str,
    original: str | None = None,
    replacement: str | None = None,
) -> SseEvent:
    """冲突命中事件（§7.3 `conflict`）——形对齐 C6 `Conflict`，供正文就地朱砂标注。

    `original`/`replacement` 为可选一键采纳补丁对：可局部修复时由审稿提议，
    前端「采纳改法」据此把 original→replacement find-replace 进终稿（不可定位则为 None）。
    """
    return SseEvent(
        event=EVENT_CONFLICT,
        data={
            "type": type,
            "where": where,
            "refs": refs,
            "suggestion": suggestion,
            "original": original,
            "replacement": replacement,
        },
    )


def foreshadow_event(
    *,
    kind: str,
    code: str | None,
    title: str,
    where: str | None,
    note: str | None,
) -> SseEvent:
    """伏笔建议事件（审稿 review）——前端伏笔看板联动。

    `kind` ∈ planted（新埋）/ resolved（回收）；作者验收裁决后才登记/改状态（只读建议）。
    """
    return SseEvent(
        event=EVENT_FORESHADOW,
        data={"kind": kind, "code": code, "title": title, "where": where, "note": note},
    )


def pace_event(*, water: list[dict[str, object]], hook: bool, beat_map: list[int]) -> SseEvent:
    """节奏诊断事件（审稿 review）——前端节奏报告/节拍图 ▁▃▅ 可视化。

    一次审产一条：注水段清单 + 章末钩子有无 + 逐段节拍强度序列。
    """
    return SseEvent(
        event=EVENT_PACE,
        data={"water": water, "hook": hook, "beat_map": beat_map},
    )


def style_event(*, score: int, segments: list[dict[str, object]]) -> SseEvent:
    """文风漂移事件（审稿 review）——前端 ◔ 整体相似度 + 漂移段列表（一键回炉）。

    一次审产一条：整体相似度 score + 逐段漂移 segments（每段 idx/score/label）。
    """
    return SseEvent(
        event=EVENT_STYLE,
        data={"score": score, "segments": segments},
    )


def characterization_event(*, issues: list[dict[str, object]]) -> SseEvent:
    """人物塑造诊断事件（advisory，审稿 review）——前端人物塑造面板只展示、无裁决 UI。

    一次审产一条：人物塑造问题清单（每条 character/aspect/where/quote/diagnosis/suggestion/
    confidence）。仅建议、不阻断验收（灵感 D2）。
    """
    return SseEvent(
        event=EVENT_CHARACTERIZATION,
        data={"issues": issues},
    )


def error_event(*, code: str, message: str, request_id: str | None = None) -> SseEvent:
    """错误事件，形对齐错误信封 §7.1（`code`/`message`），附 `request_id` 便于贯通排查。"""
    return SseEvent(
        event=EVENT_ERROR,
        data={"code": code, "message": message, "request_id": request_id},
    )


async def normalize_deltas(
    deltas: AsyncIterator[Delta],
    *,
    request_id: str | None = None,
) -> AsyncIterator[SseEvent]:
    """把 `Delta` 流归一为 SSE 事件流：每个非空 `Delta` → `token`；正常收尾发 `done`；
    底层异常 → `error`（不再向上抛）。

    这是 **T1.4 消费的缝**：apps/api 拿 `stream_chapter_draft(...)` 传进来，
    本函数产出 `SseEvent`，T1.4 包成 `StreamingResponse`。
    """
    total = 0
    try:
        async for delta in deltas:
            if delta.text:
                total += len(delta.text)
                yield token_event(delta.text)
    except AppError as exc:
        log.warning(
            "sse_stream_error",
            code=str(exc.code),
            request_id=request_id,
            chars_before_error=total,
        )
        yield error_event(code=str(exc.code), message=exc.message, request_id=request_id)
        return
    except Exception as exc:  # noqa: BLE001 — 边界兜底：任何意外都归一为 error 事件，不泄异常
        log.error(
            "sse_stream_unexpected_error",
            error=str(exc),
            request_id=request_id,
            chars_before_error=total,
        )
        yield error_event(
            code=str(ErrorCode.INTERNAL),
            message="internal error during streaming",
            request_id=request_id,
        )
        return
    yield done_event(length=total)


def _section_result_events(name: str, result: dict[str, Any]) -> list[SseEvent]:
    """把某审 `result` dict 按审种 surface 为结构化事件（纯函数，便于单测）。

    畸形 result（非 dict 等）由调用方 try/except 兜成 `error` 事件；这里只按已知形读。
    """
    events: list[SseEvent] = []
    if name == CONTINUITY:
        for conflict in result.get("conflicts") or []:
            events.append(
                conflict_event(
                    type=conflict.get("type", ""),
                    where=conflict.get("where", ""),
                    refs=list(conflict.get("refs") or []),
                    suggestion=conflict.get("suggestion", ""),
                    original=conflict.get("original"),
                    replacement=conflict.get("replacement"),
                )
            )
    elif name == FORESHADOW:
        for kind in ("planted", "resolved"):
            for sug in result.get(kind) or []:
                events.append(
                    foreshadow_event(
                        kind=kind,
                        code=sug.get("code"),
                        title=sug.get("title", ""),
                        where=sug.get("where"),
                        note=sug.get("note"),
                    )
                )
    elif name == PACE:
        events.append(
            pace_event(
                water=[dict(w) for w in result.get("water") or []],
                hook=bool(result.get("hook", False)),
                beat_map=[int(b) for b in result.get("beat_map") or []],
            )
        )
    elif name == STYLE:
        events.append(
            style_event(
                score=int(result.get("score", 100)),
                segments=[
                    {
                        "idx": int(seg.get("idx", 0)),
                        "score": int(seg.get("score", 0)),
                        "label": seg.get("label"),
                    }
                    for seg in result.get("segments") or []
                ],
            )
        )
    elif name == CHARACTERIZATION:
        events.append(
            characterization_event(
                issues=[
                    {
                        "character": issue.get("character", ""),
                        "aspect": issue.get("aspect", ""),
                        "where": issue.get("where", ""),
                        "quote": issue.get("quote", ""),
                        "diagnosis": issue.get("diagnosis", ""),
                        "suggestion": issue.get("suggestion", ""),
                        "confidence": float(issue.get("confidence", 0.0)),
                    }
                    for issue in result.get("issues") or []
                ]
            )
        )
    return events


async def normalize_review(
    reviews: Mapping[str, Any],
    *,
    request_id: str | None = None,
) -> AsyncIterator[SseEvent]:
    """把审稿子图的结构化产出（`state["reviews"]`）归一为 SSE 事件序列（M3 三审齐）。

    供 `POST .../review` 端点消费：跑 `build_review_graph` 后拿 `final["reviews"]`
    （形 `{name: {status, result}}`），本函数每审先发 `section`（done/incomplete），再按审种
    surface 结构化结果：
    - continuity → 每冲突一条 `conflict`（形对齐 C6 `Conflict`）；
    - foreshadow → 每条建议一条 `foreshadow`（planted/resolved，前端看板联动）；
    - pace       → 一条 `pace`（注水/钩子/节拍图，前端节奏报告/▁▃▅）；
    - style      → 一条 `style`（整体相似度 + 漂移段，前端 ◔ + 一键回炉）。
    末尾 `done{length=审项数}`。HTTP event-stream 编码归端点，不在此处。

    错误处理纪律（同 `normalize_deltas`）：任何意外 → 一条 `error` 事件后正常收尾，不上抛。
    某审 `incomplete`（网关失败被隔离，§5.2）→ 仅发 `section{status:incomplete}`，不发其结果。
    `reviews` 键按字典序遍历 → 确定性事件顺序（便于单测/前端稳定渲染）。向后兼容：
    既有 `section`/`conflict` 形不变，仅新增 `foreshadow`/`pace`。
    """
    try:
        section_count = 0
        for name in sorted(reviews.keys()):
            entry = reviews.get(name) or {}
            status = entry.get("status")
            if status != REVIEW_OK:
                yield section_event(name=name, status=SECTION_INCOMPLETE)
                section_count += 1
                continue
            yield section_event(name=name, status=SECTION_DONE)
            section_count += 1
            result = entry.get("result") or {}
            for event in _section_result_events(name, result):
                yield event
    except Exception as exc:  # noqa: BLE001 — 边界兜底：任何意外都归一为 error 事件，不泄异常
        log.error("sse_review_unexpected_error", error=str(exc), request_id=request_id)
        yield error_event(
            code=str(ErrorCode.INTERNAL),
            message="internal error during review",
            request_id=request_id,
        )
        return
    yield done_event(length=section_count)
