"""GpuJob — a unit of GPU work the desktop agent pulls over HTTP (#114).

The durable work list that lets the agent stay HTTP-only: the server enqueues a
job per (image, task) — e.g. detect figures + CCIP-embed — and the agent LEASES a
batch, computes on its GPU, then SUBMITS results, all over the already-exposed web
API. Redis/Postgres stay private. A lease has an expiry; the lease query itself
re-claims expired leases (agent died / stopped mid-batch), so the queue is
self-healing without a separate sweep. One job is per ITEM; the agent fans a
VIDEO out into per-frame instances internally (see image_region.frame_time).

State: pending → leased → done | error (a failure under the attempt cap returns to
pending for another agent).
"""

from datetime import datetime

from sqlalchemy import (
    DateTime,
    ForeignKey,
    Index,
    Integer,
    String,
    Text,
    func,
    text,
)
from sqlalchemy.orm import Mapped, mapped_column

from .base import Base


class GpuJob(Base):
    __tablename__ = "gpu_job"

    # Partial indexes over just the live slice (see migration 0070): the lease
    # reads the lowest-id pending jobs on the hot path, and reclaims expired
    # leases as a backstop — both stay O(batch) as done/error history grows.
    __table_args__ = (
        Index("ix_gpu_job_pending", "id", postgresql_where=text("status = 'pending'")),
        Index(
            "ix_gpu_job_leased_expires", "lease_expires_at",
            postgresql_where=text("status = 'leased'"),
        ),
    )

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    image_record_id: Mapped[int] = mapped_column(
        ForeignKey("image_record.id", ondelete="CASCADE"), index=True
    )
    # What to compute, e.g. 'ccip' (detect figures + CCIP-embed) or 'siglip_region'.
    task: Mapped[str] = mapped_column(String(32), nullable=False)
    status: Mapped[str] = mapped_column(
        String(16), nullable=False, default="pending", index=True
    )
    # pending | leased | done | error
    lease_token: Mapped[str | None] = mapped_column(String(64), nullable=True)
    leased_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    lease_expires_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
    error: Mapped[str | None] = mapped_column(Text, nullable=True)
    # Triage verdict for an ERRORED job (#125): NULL = not yet probed;
    # 'defect' = the integrity probe says the FILE itself is bad (surfaced for
    # recovery, excluded from /retry_errors); 'file_ok' = the file passes —
    # the failure was operational (timeout/transient), safe to retry.
    triage_status: Mapped[str | None] = mapped_column(String(16), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
