"""伏笔登记 / 状态变更端点（C3 扩 / ARCH §6.2, §7.2；不变量 #3）。

- POST  /projects/:id/foreshadow          作者显式登记一条伏笔（status=OPEN）。
- PATCH /projects/:id/foreshadow/:code     状态机转移 和/或 追加进展。

登记/改状态是**作者显式动作**（不变量 #3：伏笔入库不经 AI 静默写库；验收后到期扫描
是确定性纯函数置位，见 `services/foreshadow_scan.py`）。

提交边界：`ForeshadowLedgerRepo` 写方法只 `flush()`，端点写后 `await session.commit()`。

**T3.5 续接点**：本 router 是 foreshadow 端点的归属处。T3.5 的看板查询
`GET /projects/:id/foreshadow?status=` 直接加到本 router（调 `repo.list_by_status`）；
大纲 `POST /projects/:id/outline` 与伏笔无关，建议落独立 `routers/outline.py`
（或 projects router），不混进本文件。
"""

from __future__ import annotations

import uuid
from typing import Annotated

from fastapi import APIRouter, Depends, Request
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from ww_core.domain import ForeshadowLedgerRepo, ForeshadowLedgerView
from ww_core.domain.foreshadow_state import ForeshadowStatus, InvalidTransition
from ww_core.domain.project_repo import ProjectRepo
from ww_db import get_session
from ww_shared import AppError, ErrorCode

from ww_api.logging_config import get_logger
from ww_api.schemas.foreshadow import (
    ForeshadowBoardResponse,
    ForeshadowRegisterRequest,
    ForeshadowTransitionRequest,
    ForeshadowView,
)
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.project_deps import get_foreshadow_repo, get_project_repo

log = get_logger("ww.api.foreshadow")

router = APIRouter(prefix="/projects", tags=["foreshadow"])

ForeshadowRepoDep = Annotated[ForeshadowLedgerRepo, Depends(get_foreshadow_repo)]
ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
SessionDep = Annotated[AsyncSession, Depends(get_session)]


def _to_view(v: ForeshadowLedgerView) -> ForeshadowView:
    # ForeshadowLedgerView 与 ForeshadowView 字段同名，逐字段映射（snake_case 契约）。
    return ForeshadowView.model_validate(v, from_attributes=True)


@router.post("/{project_id}/foreshadow", status_code=201)
async def register_foreshadow(
    project_id: uuid.UUID,
    body: ForeshadowRegisterRequest,
    request: Request,
    repo: ForeshadowRepoDep,
    session: SessionDep,
) -> ForeshadowView:
    """登记一条伏笔（status=OPEN）。重复 `code` → DB 唯一约束冲突 → VALIDATION 信封。"""
    request_id = getattr(request.state, "request_id", None)
    try:
        view = await repo.register(
            project_id,
            code=body.code,
            title=body.title,
            planted_at=body.planted_at,
            content=body.content,
            expected_close_from=body.expected_close_from,
            expected_close_to=body.expected_close_to,
            importance=body.importance,
        )
        await session.commit()
    except IntegrityError as exc:
        # `(project_id, code)` 唯一约束冲突（register 仅 INSERT、不 upsert，见 foreshadow_repo）。
        await session.rollback()
        raise AppError(
            ErrorCode.VALIDATION,
            f"伏笔代号已存在：{body.code}",
            {"field": "code", "code": body.code, "reason": "duplicate"},
        ) from exc
    log.info(
        "foreshadow_registered",
        project_id=str(project_id),
        request_id=request_id,
        code=body.code,
    )
    return _to_view(view)


@router.get("/{project_id}/foreshadow")
async def list_foreshadow(
    project_id: uuid.UUID,
    repo: ForeshadowRepoDep,
    project_repo: ProjectRepoDep,
    status: str | None = None,
) -> ForeshadowBoardResponse:
    """伏笔看板：按 `status` 过滤（缺省=全部），按 code 升序。

    项目不存在 → 404（与写端点一致，避免不存在 project 返回误导性空 200）。
    `status` 非法（不在 OPEN/PARTIAL/CLOSED/OVERDUE）→ VALIDATION 信封。四泳道前端
    据 `status` 分组；OVERDUE 泳道 + 逾期标记用 `expected_close_to`（看板字段已齐）。
    """
    if await project_repo.get(STUB_OWNER_ID, project_id) is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project {project_id} not found")
    if status is not None and status not in {s.value for s in ForeshadowStatus}:
        raise AppError(
            ErrorCode.VALIDATION,
            f"非法状态过滤：{status}",
            {"field": "status", "reason": "invalid_status"},
        )
    views = await repo.list_by_status(project_id, status)
    return ForeshadowBoardResponse(foreshadow=[_to_view(v) for v in views])


@router.patch("/{project_id}/foreshadow/{code}")
async def update_foreshadow(
    project_id: uuid.UUID,
    code: str,
    body: ForeshadowTransitionRequest,
    request: Request,
    repo: ForeshadowRepoDep,
    session: SessionDep,
) -> ForeshadowView:
    """状态机转移 和/或 追加进展。非法转移 → VALIDATION 信封；不存在 → NOT_FOUND。

    `to_status` 与 `progress_entry` 皆可选；两者都缺 → VALIDATION（无操作）。先转移、后追加。
    """
    request_id = getattr(request.state, "request_id", None)
    if body.to_status is None and body.progress_entry is None:
        raise AppError(
            ErrorCode.VALIDATION,
            "至少提供 `to_status` 或 `progress_entry` 之一",
            {"reason": "empty_update"},
        )

    view: ForeshadowLedgerView
    try:
        if body.to_status is not None:
            view = await repo.transition(project_id, code, to_status=body.to_status)
        if body.progress_entry is not None:
            view = await repo.record_progress(project_id, code, entry=body.progress_entry)
        await session.commit()
    except InvalidTransition as exc:
        await session.rollback()
        raise AppError(
            ErrorCode.VALIDATION,
            str(exc),
            {"field": "to_status", "code": code, "reason": "invalid_transition"},
        ) from exc
    except LookupError as exc:
        await session.rollback()
        raise AppError(ErrorCode.NOT_FOUND, f"foreshadow not found: {code}") from exc

    log.info(
        "foreshadow_updated",
        project_id=str(project_id),
        request_id=request_id,
        code=code,
        to_status=body.to_status,
        has_progress=body.progress_entry is not None,
    )
    return _to_view(view)
