"""多章工作流链端点（chain-workflow 设计 §6）——发起 / 续跑，进度走 `GET /jobs/{id}`。

三端点：
- `POST /projects/{pid}/chains/{chain_key}/run` → 202 `ChainRunAccepted{job_id,...}`：
  写一行 jobs(queued, kind=chain) 立即返 202，链经 BackgroundTask `run_chain_job` 跑
  （**自建独立 session**，请求 session 已关闭）。无凭据 → 流前 `LLM_UNAVAILABLE`（503，
  调度 job 前拦下）。未知 chain_key → 404；`count` 1..50 越界 → 422（schema Field 约束）。
- `GET /jobs/{job_id}`：复用现有 jobs 端点轮询进度/awaiting（不在本 router）。
- `POST /projects/{pid}/chains/runs/{job_id}/resume` → 202：仅当 job=awaiting_input；
  带裁决经 BackgroundTask resume 续跑。非 awaiting 态 → 409。

不变量：链是**编排**新增、非业务重写——复用同一 `assemble`/审稿 spec/`run_accept_transaction`
（经 `accept_op` 闭包注入），守 #1/#3/#4/#5/#9。token 绝不入 job result/状态/日志（§5）。
"""

from __future__ import annotations

import uuid
from typing import Annotated, Any

from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from ww_core.domain import JobRepo, ProjectRepo
from ww_core.domain.job_repo import STATUS_AWAITING
from ww_db import get_session
from ww_llm_gateway import Gateway
from ww_shared import AppError, ErrorCode, ErrorEnvelope

from ww_api.logging_config import get_logger
from ww_api.schemas.chain import (
    ChainResumeAccepted,
    ChainResumeRequest,
    ChainRunAccepted,
    ChainRunRequest,
)
from ww_api.services.chain_deps import (
    CheckpointerFactory,
    get_checkpointer_factory,
)
from ww_api.services.chain_runner import (
    JOB_KIND_CHAIN,
    build_accept_op,
    run_chain_job,
)
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.foreshadow_scan import SessionFactory
from ww_api.services.project_deps import (
    GatewayChainBuilder,
    GatewayDigestBuilder,
    get_chain_gateway,
    get_chain_gateway_builder,
    get_digest_gateway_builder,
    get_job_repo,
    get_project_repo,
    get_session_factory,
)

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

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

# 内置链（§1.3/§3）：draft_volume=从 start 章按记忆量产；continue_volume=每章以上一章已验收
# 正文末尾作前文引子续写（F2）。未知 key → 404（系统边界，fail fast）。
SUPPORTED_CHAINS = frozenset({"draft_volume", "continue_volume"})

ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)]
JobRepoDep = Annotated[JobRepo, Depends(get_job_repo)]
ChainGatewayDep = Annotated[Gateway, Depends(get_chain_gateway)]
SessionDep = Annotated[AsyncSession, Depends(get_session)]
SessionFactoryDep = Annotated[SessionFactory, Depends(get_session_factory)]
CheckpointerFactoryDep = Annotated[CheckpointerFactory, Depends(get_checkpointer_factory)]
DigestBuilderDep = Annotated[GatewayDigestBuilder, Depends(get_digest_gateway_builder)]
ChainBuilderDep = Annotated[GatewayChainBuilder, Depends(get_chain_gateway_builder)]

_RUN_ERRORS: dict[int | str, dict[str, Any]] = {
    404: {"model": ErrorEnvelope, "description": "项目或链 key 不存在"},
    422: {"model": ErrorEnvelope, "description": "请求参数非法（count 越界等）"},
    503: {"model": ErrorEnvelope, "description": "无可用 LLM 凭据"},
}
_RESUME_ERRORS: dict[int | str, dict[str, Any]] = {
    404: {"model": ErrorEnvelope, "description": "项目或 job 不存在"},
    409: {"model": ErrorEnvelope, "description": "job 非 awaiting_input 态，不可续跑"},
}


async def _require_project(project_repo: ProjectRepo, project_id: uuid.UUID) -> None:
    if await project_repo.get(STUB_OWNER_ID, project_id) is None:
        raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")


@router.post(
    "/{project_id}/chains/{chain_key}/run",
    status_code=202,
    responses=_RUN_ERRORS,
)
async def run_chain(
    project_id: uuid.UUID,
    chain_key: str,
    body: ChainRunRequest,
    request: Request,
    response: Response,
    background_tasks: BackgroundTasks,
    project_repo: ProjectRepoDep,
    job_repo: JobRepoDep,
    gateway: ChainGatewayDep,  # 凭据探测（无凭据 → dep 解析阶段 503）；不用于实际 LLM 调用
    session: SessionDep,
    session_factory: SessionFactoryDep,
    checkpointer_factory: CheckpointerFactoryDep,
    digest_gateway_builder: DigestBuilderDep,
    chain_gateway_builder: ChainBuilderDep,
) -> ChainRunAccepted:
    """发起多章链：写一行 job 返 202，链经 BackgroundTask 异步跑。

    项目不存在 → 404；未知 chain_key → 404；无凭据 → 503（dep 解析阶段拦下）；
    `count` 越界由 schema Field 在解析阶段 → 422。`gateway` 仅做请求阶段凭据探测——
    链本体在 BackgroundTask 里用独立 session 重建网关跑（请求 session 即将关闭）。
    """
    request_id = getattr(request.state, "request_id", None)

    await _require_project(project_repo, project_id)
    if chain_key not in SUPPORTED_CHAINS:
        raise AppError(ErrorCode.NOT_FOUND, f"unknown chain key: {chain_key}")

    job = await job_repo.create(project_id, JOB_KIND_CHAIN)
    await session.commit()  # job 行须在 202 返回前持久化（供前端立即轮询）。

    accept_op = build_accept_op(
        session_factory=session_factory,
        digest_gateway_builder=digest_gateway_builder,
        request_id=request_id,
    )
    background_tasks.add_task(
        run_chain_job,
        session_factory,
        job.id,
        project_id=project_id,
        user_id=STUB_OWNER_ID,
        chain_key=chain_key,
        start_chapter_no=body.start_chapter_no,
        count=body.count,
        chain_gateway_builder=chain_gateway_builder,
        checkpointer_ctx=checkpointer_factory,
        accept_op=accept_op,
        chapter_repo_factory=_chapter_repo_factory,
        review_repo_factory=_review_repo_factory,
        request_id=request_id,
    )

    log.info(
        "chain_run_scheduled",
        project_id=str(project_id),
        request_id=request_id,
        job_id=str(job.id),
        chain_key=chain_key,
        start_chapter_no=body.start_chapter_no,
        count=body.count,
    )
    response.status_code = 202
    return ChainRunAccepted(
        job_id=job.id,
        chain_key=chain_key,
        start_chapter_no=body.start_chapter_no,
        count=body.count,
    )


@router.post(
    "/{project_id}/chains/runs/{job_id}/resume",
    status_code=202,
    responses=_RESUME_ERRORS,
)
async def resume_chain(
    project_id: uuid.UUID,
    job_id: uuid.UUID,
    body: ChainResumeRequest,
    request: Request,
    response: Response,
    background_tasks: BackgroundTasks,
    project_repo: ProjectRepoDep,
    job_repo: JobRepoDep,
    gateway: ChainGatewayDep,  # 凭据探测（无凭据 → dep 解析阶段 503）；不用于实际 LLM 调用
    session: SessionDep,
    session_factory: SessionFactoryDep,
    checkpointer_factory: CheckpointerFactoryDep,
    digest_gateway_builder: DigestBuilderDep,
    chain_gateway_builder: ChainBuilderDep,
) -> ChainResumeAccepted:
    """裁决续跑：仅当 job=awaiting_input，带裁决经 BackgroundTask resume。

    项目不存在 → 404；job 不存在 / 不属于该项目 → 404（同案，不枚举）；非 awaiting 态 →
    409（CONFLICT）。`awaiting→running` 在 HTTP 处理器内**原子抢占**（条件 UPDATE），避免两个
    并发 resume 都过守卫后双 `Command(resume=...)` 损坏图（审评 #3）；抢不到（已被并发抢走/非
    awaiting）→ 409。resume 经 `Command(resume=decisions)` 从 interrupt 续跑（同 thread_id）。
    """
    request_id = getattr(request.state, "request_id", None)

    await _require_project(project_repo, project_id)
    job = await job_repo.get(job_id)
    # 所有权校验（审评 #4）：job 不存在 **或** 不属于该项目 → 404（同案，避免跨项目枚举 job）。
    if job is None or job.project_id != project_id:
        raise AppError(ErrorCode.NOT_FOUND, f"job not found: {job_id}")
    if job.status != STATUS_AWAITING:
        raise AppError(
            ErrorCode.CONFLICT,
            f"job {job_id} 非 awaiting_input 态（当前 {job.status}），不可续跑",
            {"job_status": job.status},
        )

    # 原子抢占 awaiting→running（审评 #3）：抢不到（并发竞态/已非 awaiting）→ 409，绝不调度。
    claimed = await job_repo.claim_awaiting_to_running(job_id)
    await session.commit()  # 抢占须在 202 返回前持久化（防并发 resume 双调度）。
    if claimed is None:
        raise AppError(
            ErrorCode.CONFLICT,
            f"job {job_id} 已被并发续跑抢占或非 awaiting_input 态，不可重复续跑",
        )

    # chain_key 真值：run 时 interrupt 把图 state 的 chain_key 持久进 job.result（_chain_result）。
    # 读回它，而非按 job.kind 推断（kind 恒为通用常量 JOB_KIND_CHAIN，从不在 SUPPORTED_CHAINS）。
    chain_key = _chain_key_from_result(claimed.result)

    accept_op = build_accept_op(
        session_factory=session_factory,
        digest_gateway_builder=digest_gateway_builder,
        request_id=request_id,
    )
    background_tasks.add_task(
        run_chain_job,
        session_factory,
        job_id,
        project_id=project_id,
        user_id=STUB_OWNER_ID,
        chain_key=chain_key,
        start_chapter_no=1,  # resume 不重置区间：图从检查点续，初值不再使用。
        count=1,
        chain_gateway_builder=chain_gateway_builder,
        checkpointer_ctx=checkpointer_factory,
        accept_op=accept_op,
        chapter_repo_factory=_chapter_repo_factory,
        review_repo_factory=_review_repo_factory,
        resume_decisions=body.decisions,
        request_id=request_id,
    )

    log.info(
        "chain_resume_scheduled",
        project_id=str(project_id),
        request_id=request_id,
        job_id=str(job_id),
        decision_count=len(body.decisions),
    )
    response.status_code = 202
    return ChainResumeAccepted(job_id=job_id, chain_key=chain_key)


def _chain_key_from_result(result: dict[str, Any] | None) -> str:
    """从 awaiting job 的 result 取真 chain_key（run 时由 _chain_result 持久化）。

    缺失/非受支持 key（旧行/异常）→ 回退 "draft_volume"（保守默认，不崩）。
    """
    key = (result or {}).get("chain_key")
    if isinstance(key, str) and key in SUPPORTED_CHAINS:
        return key
    return "draft_volume"


def _chapter_repo_factory(session: AsyncSession) -> object:
    """链节点（review 重读草稿）按 session 建 chapter draft repo。"""
    from ww_core.domain.chapter_repo import SqlChapterRepo

    return SqlChapterRepo(session)


def _review_repo_factory(session: AsyncSession) -> object:
    """链节点（review 落留痕 / decide 重读）按 session 建审稿 repo。"""
    from ww_core.domain.review_repo import SqlReviewRepo

    return SqlReviewRepo(session)
