"""长任务 `jobs` 表**写侧** Repository（创建/进度/完成/失败/僵尸回收；ARCH §7.4）。

`jobs` 表 + `GET /jobs/{id}` 读端点已在 T0.3 建齐；本模块补**写/进度/回收**层，
供 T4.3「学文风走 jobs」（`POST /style` 写一行 → BackgroundTask 跑提取 → 置 done/failed）
复用。命名沿用读侧无歧义（无读侧同名 repo，故不加前缀）。

**提交边界**（同 M2/M3 写侧 repo 与 memory/gotchas「写库副作用归调用方」纪律）：
所有状态写方法**只 `flush()`（+`refresh`）不 `commit()`**——提交交调用方：
`run_job`（job_runner）/端点事务负责 commit。
**唯一例外** `reap_zombies`：它在 lifespan 启动期**独立调用**（无外层事务/调用方），
故自己 `commit()`。

`JobView`（frozen）镜像 `GET /jobs/{id}` 出参字段：id/kind/status/progress/result/error。
"""

from __future__ import annotations

import uuid
from typing import Any, Protocol, cast

from pydantic import BaseModel
from sqlalchemy import CursorResult, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from ww_db.models import Job

# job 状态常量（与 DB server_default 'queued' 对齐；ARCH §7.4）。
STATUS_QUEUED = "queued"
STATUS_RUNNING = "running"
STATUS_DONE = "done"
STATUS_FAILED = "failed"
# 多章链 interrupt 命中：暂停等作者裁决（chain-workflow §7，零迁移复用 jobs.status 自由 Text 列）。
STATUS_AWAITING = "awaiting_input"
PROGRESS_COMPLETE = 100


class JobView(BaseModel):
    """长任务只读快照（snake_case，frozen）——镜像 GET /jobs/{id} 出参。"""

    model_config = {"frozen": True}

    id: uuid.UUID
    project_id: uuid.UUID | None = None
    kind: str
    status: str
    progress: int = 0
    result: dict[str, Any] | None = None
    error: str | None = None


class JobRepo(Protocol):
    """长任务写侧接口（状态写方法只 flush 不 commit；`reap_zombies` 自 commit）。"""

    async def create(self, project_id: uuid.UUID | None, kind: str) -> JobView:
        """创建一行 job（status=queued, progress=0）。"""
        ...

    async def set_running(self, job_id: uuid.UUID) -> JobView:
        """置 status=running（任务开始）。"""
        ...

    async def set_progress(self, job_id: uuid.UUID, pct: int) -> JobView:
        """更新进度百分比（0..100，越界夹取）。"""
        ...

    async def complete(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        """置 status=done, progress=100, result=<dict>。"""
        ...

    async def set_awaiting(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        """置 status=awaiting_input, result=<dict>（多章链 interrupt 暂停等裁决；chain §5/§7）。"""
        ...

    async def claim_awaiting_to_running(self, job_id: uuid.UUID) -> JobView | None:
        """**原子**抢占 awaiting_input → running，返回抢到的 JobView；未抢到（不存在/非
        awaiting/已被并发抢走）返 None。供 resume 端点防并发 resume 竞态（chain §3/审评 #3）。"""
        ...

    async def fail(self, job_id: uuid.UUID, error: str) -> JobView:
        """置 status=failed, error=<str>。"""
        ...

    async def get(self, job_id: uuid.UUID) -> JobView | None:
        """按 id 取单条，无则 None。"""
        ...

    async def reap_zombies(self) -> int:
        """把所有 status=running 的僵尸行标 failed，返回被改条数（启动期自 commit）。"""
        ...


def _to_view(row: Job) -> JobView:
    return JobView(
        id=row.id,
        project_id=row.project_id,
        kind=row.kind,
        status=row.status,
        progress=row.progress,
        result=row.result,
        error=row.error,
    )


def _clamp_pct(pct: int) -> int:
    return max(0, min(PROGRESS_COMPLETE, pct))


class SqlJobRepo:
    """SQLAlchemy 实现：创建 + 状态流转 + 进度 + 僵尸回收。

    状态写方法只 flush（+refresh）；`reap_zombies` 启动期独立调用故自 commit。
    """

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

    async def _find(self, job_id: uuid.UUID) -> Job | None:
        return (await self._s.execute(select(Job).where(Job.id == job_id))).scalar_one_or_none()

    async def _require(self, job_id: uuid.UUID) -> Job:
        row = await self._find(job_id)
        if row is None:
            raise LookupError(f"job not found: {job_id}")
        return row

    async def create(self, project_id: uuid.UUID | None, kind: str) -> JobView:
        row = Job(
            project_id=project_id,
            kind=kind,
            status=STATUS_QUEUED,
            progress=0,
        )
        self._s.add(row)
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def set_running(self, job_id: uuid.UUID) -> JobView:
        row = await self._require(job_id)
        row.status = STATUS_RUNNING
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def set_progress(self, job_id: uuid.UUID, pct: int) -> JobView:
        row = await self._require(job_id)
        row.progress = _clamp_pct(pct)
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def complete(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        row = await self._require(job_id)
        row.status = STATUS_DONE
        row.progress = PROGRESS_COMPLETE
        row.result = dict(result)
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def set_awaiting(self, job_id: uuid.UUID, result: dict[str, Any]) -> JobView:
        row = await self._require(job_id)
        row.status = STATUS_AWAITING
        row.result = dict(result)
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

    async def claim_awaiting_to_running(self, job_id: uuid.UUID) -> JobView | None:
        """原子 `awaiting_input → running`（条件 UPDATE，returning 行）。

        防并发 resume 竞态（审评 #3）：两个并发 resume 仅一个能把 awaiting→running，另一个
        UPDATE 命中 0 行 → 返 None（端点据此返 409）。避免「两个 resume 都过 awaiting 守卫 →
        同 thread_id 双 `Command(resume=...)` → 图损坏」。提交交调用方（端点事务）。
        """
        result = await self._s.execute(
            update(Job)
            .where(Job.id == job_id, Job.status == STATUS_AWAITING)
            .values(status=STATUS_RUNNING)
            .returning(
                Job.id, Job.project_id, Job.kind, Job.status, Job.progress, Job.result, Job.error
            )
        )
        row = result.one_or_none()
        if row is None:
            return None
        return JobView(
            id=row.id,
            project_id=row.project_id,
            kind=row.kind,
            status=row.status,
            progress=row.progress,
            result=row.result,
            error=row.error,
        )

    async def fail(self, job_id: uuid.UUID, error: str) -> JobView:
        row = await self._require(job_id)
        row.status = STATUS_FAILED
        row.error = error
        await self._s.flush()
        await self._s.refresh(row)
        return _to_view(row)

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

    async def reap_zombies(self) -> int:
        """启动期僵尸回收：进程重启会丢 BackgroundTask，残留 running 行标 failed
        （让用户看到失败可重试，而非进度条永转；§7.4 缓解）。独立调用故自 commit。"""
        result = await self._s.execute(
            update(Job)
            .where(Job.status == STATUS_RUNNING)
            .values(status=STATUS_FAILED, error="job interrupted (process restart)")
        )
        await self._s.commit()
        # bulk UPDATE → CursorResult.rowcount = 受影响行数（`Result` 基类无此属性）。
        return cast("CursorResult[Any]", result).rowcount
