"""伏笔账本状态机——确定性纯函数（ARCH §6.2 / PRODUCT_SPEC §4.2）。

状态机（§6.2）::

            登记            首次照应          完成回收
      ─────▶ OPEN ──────▶ PARTIAL ─────────▶ CLOSED
               │             │
               └─────────────┴──▶ OVERDUE  (章号 > expected_close_to 且未 CLOSED)
                                  (到期扫描置位; 回收后可转 CLOSED)

- 登记 → OPEN（DB server_default）。
- 首次照应 → PARTIAL。
- 完成回收 → CLOSED（可从 OPEN/PARTIAL/OVERDUE 一步到达）。
- 到期判定（M3-d 纯函数，非 Agent）：
  `current_chapter > expected_close_to AND status≠CLOSED → OVERDUE`。
- **CLOSED 终态**：不被 OVERDUE 覆盖、不可重开（非法转移显式抛 `InvalidTransition`）。

全部为纯函数：不读 DB、不可变、可单测。写库由 `foreshadow_repo` 落地（只 flush）。
枚举值与 DB `foreshadow.status` 文本严格一致（`StrEnum` → 直接当字符串用）。
"""

from __future__ import annotations

from enum import StrEnum


class ForeshadowStatus(StrEnum):
    """伏笔状态——值与 DB `status` 文本一致。"""

    OPEN = "OPEN"
    PARTIAL = "PARTIAL"
    CLOSED = "CLOSED"
    OVERDUE = "OVERDUE"


# 便捷常量（供 repo / 测试 / 调用方按名引用，避免散落字面量）。
OPEN = ForeshadowStatus.OPEN
PARTIAL = ForeshadowStatus.PARTIAL
CLOSED = ForeshadowStatus.CLOSED
OVERDUE = ForeshadowStatus.OVERDUE


class InvalidTransition(ValueError):
    """非法状态转移（如 CLOSED 重开）。"""


# 合法转移图：from → 允许到达的状态集合（不含同态幂等，单独放行）。
_ALLOWED: dict[ForeshadowStatus, frozenset[ForeshadowStatus]] = {
    OPEN: frozenset({PARTIAL, CLOSED, OVERDUE}),
    PARTIAL: frozenset({CLOSED, OVERDUE}),
    OVERDUE: frozenset({PARTIAL, CLOSED}),  # 逾期后仍可（部分）回收
    CLOSED: frozenset(),  # 终态：不可再转移
}


def transition(current: ForeshadowStatus, to_status: ForeshadowStatus) -> ForeshadowStatus:
    """应用状态机校验后返回新状态（纯函数）。

    - 同态（current == to_status）幂等放行。
    - CLOSED 为终态：任何离开 CLOSED 的转移都非法。
    - 其余按 `_ALLOWED` 校验；非法 → `InvalidTransition`。
    """
    if current == to_status:
        return to_status
    if to_status not in _ALLOWED[current]:
        raise InvalidTransition(f"非法伏笔转移: {current.value} → {to_status.value}")
    return to_status


def is_overdue(
    *,
    current_chapter: int,
    expected_close_to: int | None,
    status: ForeshadowStatus,
) -> bool:
    """到期判据（M3-d，§6.2）：`current_chapter > expected_close_to AND status≠CLOSED`。

    - 无 `expected_close_to`（未设回收窗口）→ 永不逾期。
    - CLOSED → 永不逾期（已回收）。
    - `current_chapter == expected_close_to` → 仍在窗口内，未逾期（严格大于才算）。
    """
    if expected_close_to is None:
        return False
    if status == CLOSED:
        return False
    return current_chapter > expected_close_to


def apply_overdue_scan(
    status: ForeshadowStatus,
    *,
    current_chapter: int,
    expected_close_to: int | None,
) -> ForeshadowStatus:
    """到期扫描应用（验收后触发，§6.2）：命中判据 → 置 `OVERDUE`，否则保持原状态。

    幂等：已 OVERDUE 且仍逾期 → 保持 OVERDUE；CLOSED 永不被改写（is_overdue 已排除）。
    """
    if is_overdue(
        current_chapter=current_chapter,
        expected_close_to=expected_close_to,
        status=status,
    ):
        return OVERDUE
    return status
