"""验收后伏笔到期扫描（M3-b/d；ARCH §6.2, §7.4，不变量 #3）。

接 §5.5 验收事务后的 `TODO(M3)` 占位：验收**提交成功后**触发一次伏笔到期扫描，把
`current_ch > expected_close_to AND status≠CLOSED` 的伏笔确定性置 `OVERDUE`（M3-d 纯
函数，非 Agent；不变量 #3：状态变更是确定性扫描而非 AI 静默写库）。

挂载方式（端点）：FastAPI `BackgroundTasks` 在 accept 端点登记 `run_overdue_scan`。
BackgroundTask 在请求-response 发回、请求 session 关闭**之后**才跑——故本函数**自建新
session**（不复用请求 session）并自己 `commit()`。

**持久性局限（§7.4）**：BackgroundTasks 在 API 进程内跑，进程重启/崩溃会丢失未跑完的
任务——原型可接受，这里把触发/完成/失败都记结构化日志便于查错；**不引入 `jobs` 表**
（那是 M4/T4.1 的工作，本任务不越界）。

可测性：扫描逻辑抽成纯 async 函数 `run_overdue_scan(session_factory, ...)`，端点用
BackgroundTasks 调它；单测直接 `await` 它（注入 fake session 工厂）断言行被置 OVERDUE，
**绝不真起不可控后台线程**。
"""

from __future__ import annotations

import uuid
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
from typing import Protocol

import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from ww_core.domain import ForeshadowLedgerView, SqlForeshadowLedgerRepo

log = structlog.get_logger(__name__)

# 新建独立 session 的工厂：调用得到一个 async 上下文管理器（`async with` → AsyncSession）。
SessionFactory = Callable[[], AbstractAsyncContextManager[AsyncSession]]


class OverdueScanRepo(Protocol):
    """到期扫描对账本 repo 的最小依赖（仅 `scan_overdue`）。"""

    async def scan_overdue(
        self, project_id: uuid.UUID, *, current_chapter: int
    ) -> list[ForeshadowLedgerView]: ...


# repo 工厂：从新 session 造账本 repo。默认建 SQL 实现；测试注入 fake（避免真连 DB）。
RepoFactory = Callable[[AsyncSession], OverdueScanRepo]


def _default_repo_factory(session: AsyncSession) -> OverdueScanRepo:
    return SqlForeshadowLedgerRepo(session)


async def run_overdue_scan(
    session_factory: SessionFactory,
    *,
    project_id: uuid.UUID,
    chapter_no: int,
    request_id: str | None = None,
    repo_factory: RepoFactory = _default_repo_factory,
) -> int:
    """验收后伏笔到期扫描：新建 session → `scan_overdue` 置 OVERDUE → `commit()`。

    返回被置 OVERDUE 的伏笔条数（供日志/测试断言）。任何异常被捕获并记错误日志后
    吞掉——后台任务失败不应冒泡崩进程，也不影响已成功的验收事务（§7.4 局限可接受）。
    `repo_factory` 是可注入缝：默认建 SQL 账本 repo，测试注 fake（直接 await、不联网）。
    """
    try:
        async with session_factory() as session:
            repo = repo_factory(session)
            changed = await repo.scan_overdue(project_id, current_chapter=chapter_no)
            if changed:
                await session.commit()
            log.info(
                "foreshadow_overdue_scan",
                project_id=str(project_id),
                chapter_no=chapter_no,
                request_id=request_id,
                overdue_count=len(changed),
                overdue_codes=[v.code for v in changed],
            )
            return len(changed)
    except Exception as exc:  # noqa: BLE001 — 后台任务边界：记错误、不冒泡崩进程（§7.4）。
        log.error(
            "foreshadow_overdue_scan_failed",
            project_id=str(project_id),
            chapter_no=chapter_no,
            request_id=request_id,
            error=str(exc),
        )
        return 0
