"""伏笔账本**写侧** Repository（登记/状态机/进展/到期扫描；ARCH §6.2 / PRODUCT_SPEC §4.2）。

读侧（`list_for_codes`，供 assemble 注入伏笔窗口）已在 `ww_core.memory.sql_repositories`
+ `domain.repositories.ForeshadowRepo`/`ForeshadowView` 提供（最小 View，C5 稳定，不动）。
本模块加**写**能力 + 看板查询，命名加 `Ledger` 前缀避免与读侧同名歧义
（同 T2.3 `DigestAppendRepo` 区别于读侧 `DigestRepo` 的先例，见 memory/decisions）。
`ForeshadowLedgerView` 比读侧 View 多 `importance/links/progress`（看板/账本需要）。

**提交边界**：所有写方法**只 `flush()` 不 `commit()`**——提交交调用方：
- T3.2 验收后扫描挂 BackgroundTask / 接 §5.5 `TODO(M3)`，由那条事务/任务提交；
- T3.5 登记/状态端点由端点事务提交。
与 M2 验收-side repos 一致（写库副作用归编排/事务层，见 memory/gotchas）。
含 nullable 列的唯一约束 `(project_id, code)` 不用 PG `ON CONFLICT`——`register` 仅
显式 INSERT（重复 code 让 DB 唯一约束/调用方处理），不做 upsert（见 gotcha）。
"""

from __future__ import annotations

import uuid
from typing import Any, Protocol

from pydantic import BaseModel, Field
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from ww_db.models import Foreshadow

from ww_core.domain.foreshadow_state import (
    CLOSED,
    OVERDUE,
    ForeshadowStatus,
)
from ww_core.domain.foreshadow_state import (
    transition as apply_transition,
)


class ForeshadowLedgerView(BaseModel):
    """伏笔账本只读快照（snake_case，frozen）——看板/账本用，含进展与重要度。"""

    model_config = {"frozen": True}

    code: str
    title: str
    status: str
    planted_at: int | None = None
    content: str | None = None
    expected_close_from: int | None = None
    expected_close_to: int | None = None
    importance: str | None = None
    links: list[dict[str, Any]] = Field(default_factory=list)
    progress: list[dict[str, Any]] = Field(default_factory=list)


class ForeshadowLedgerRepo(Protocol):
    """伏笔账本写侧接口（按 project_id 隔离；写方法只 flush 不 commit）。"""

    async def register(
        self,
        project_id: uuid.UUID,
        *,
        code: str,
        title: str,
        planted_at: int | None = None,
        content: str | None = None,
        expected_close_from: int | None = None,
        expected_close_to: int | None = None,
        importance: str | None = None,
    ) -> ForeshadowLedgerView:
        """登记一条伏笔（status=OPEN）；`(project_id, code)` 唯一。"""
        ...

    async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None:
        """按 code 取单条，无则 None。"""
        ...

    async def list_by_status(
        self, project_id: uuid.UUID, status: str | None = None
    ) -> list[ForeshadowLedgerView]:
        """看板查询：按状态过滤（None=全部）。供 T3.5 `GET /foreshadow?status=`。"""
        ...

    async def transition(
        self, project_id: uuid.UUID, code: str, *, to_status: str
    ) -> ForeshadowLedgerView:
        """应用状态机校验后写 `status`（非法转移抛 `InvalidTransition`）。"""
        ...

    async def record_progress(
        self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any]
    ) -> ForeshadowLedgerView:
        """append 一条进展到 `progress` JSONB（append-only，不覆盖历史）。"""
        ...

    async def scan_overdue(
        self, project_id: uuid.UUID, *, current_chapter: int
    ) -> list[ForeshadowLedgerView]:
        """到期扫描：命中行置 OVERDUE，返回**被改的行**。供 T3.2 验收后扫描。"""
        ...


def _to_view(row: Foreshadow) -> ForeshadowLedgerView:
    return ForeshadowLedgerView(
        code=row.code,
        title=row.title,
        status=row.status,
        planted_at=row.planted_at,
        content=row.content,
        expected_close_from=row.expected_close_from,
        expected_close_to=row.expected_close_to,
        importance=row.importance,
        links=list(row.links or []),
        progress=list(row.progress or []),
    )


class SqlForeshadowLedgerRepo:
    """SQLAlchemy 实现：登记 + 状态机转移 + 进展 append + 到期扫描（只 flush 不 commit）。"""

    def __init__(self, session: AsyncSession) -> None:
        self._s = session

    async def _find(self, project_id: uuid.UUID, code: str) -> Foreshadow | None:
        return (
            await self._s.execute(
                select(Foreshadow).where(
                    Foreshadow.project_id == project_id,
                    Foreshadow.code == code,
                )
            )
        ).scalar_one_or_none()

    async def register(
        self,
        project_id: uuid.UUID,
        *,
        code: str,
        title: str,
        planted_at: int | None = None,
        content: str | None = None,
        expected_close_from: int | None = None,
        expected_close_to: int | None = None,
        importance: str | None = None,
    ) -> ForeshadowLedgerView:
        # 仅 INSERT；唯一约束 (project_id, code) 由 DB 强制（不做 ON CONFLICT upsert，见模块注释）。
        row = Foreshadow(
            project_id=project_id,
            code=code,
            title=title,
            status=ForeshadowStatus.OPEN.value,
            planted_at=planted_at,
            content=content,
            expected_close_from=expected_close_from,
            expected_close_to=expected_close_to,
            importance=importance,
            links=[],
            progress=[],
        )
        self._s.add(row)
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None:
        row = await self._find(project_id, code)
        return _to_view(row) if row is not None else None

    async def list_by_status(
        self, project_id: uuid.UUID, status: str | None = None
    ) -> list[ForeshadowLedgerView]:
        stmt = select(Foreshadow).where(Foreshadow.project_id == project_id)
        if status is not None:
            stmt = stmt.where(Foreshadow.status == status)
        rows = (await self._s.execute(stmt.order_by(Foreshadow.code))).scalars()
        return [_to_view(r) for r in rows]

    async def transition(
        self, project_id: uuid.UUID, code: str, *, to_status: str
    ) -> ForeshadowLedgerView:
        row = await self._find(project_id, code)
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        # 状态机校验（非法转移抛 InvalidTransition，事务回滚由调用方控制）。
        row.status = apply_transition(
            ForeshadowStatus(row.status), ForeshadowStatus(to_status)
        ).value
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def record_progress(
        self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any]
    ) -> ForeshadowLedgerView:
        row = await self._find(project_id, code)
        if row is None:
            raise LookupError(f"foreshadow not found: {code}")
        # 不可变 append：建新 list 重赋值，确保 ORM 侦测 JSONB 变更（避免原地 mutate 不脏标记）。
        row.progress = [*(row.progress or []), dict(entry)]
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def scan_overdue(
        self, project_id: uuid.UUID, *, current_chapter: int
    ) -> list[ForeshadowLedgerView]:
        # 谓词下推 + 批量 UPDATE ... RETURNING（不再拉全表到内存逐行过滤/更新，P1-5）。
        # 判据与 `foreshadow_state.is_overdue` 一致：expected_close_to 非空且
        # current_chapter > expected_close_to 且当前状态既非 CLOSED（终态）也非 OVERDUE
        # （已逾期则无变更）——后两者排除使本批量更新等价于「只改真正发生状态翻转的行」。
        stmt = (
            update(Foreshadow)
            .where(
                Foreshadow.project_id == project_id,
                Foreshadow.expected_close_to.is_not(None),
                Foreshadow.expected_close_to < current_chapter,
                Foreshadow.status.not_in([CLOSED.value, OVERDUE.value]),
            )
            .values(status=OVERDUE.value)
            .returning(Foreshadow)
        )
        rows = (await self._s.execute(stmt)).scalars().all()
        # 先把命中行物化为不可变 View（脱离 ORM 身份），再 expire——避免 UPDATE...RETURNING
        # 与 session 里已加载实例的状态不一致（后续读重取最新）。
        views = [_to_view(row) for row in rows]
        self._s.expire_all()
        return views
