"""Table `jobs` du socle (spec 2026-08-31 « socle jobs » §1).

Socle **neutre** : aucun type de job métier n'est nommé ici — les types vivent dans le
registre (`app.jobs.registry`), déclarés par les modules qui les possèdent.
"""

import uuid
from datetime import datetime
from typing import Any

from sqlalchemy import (
    CheckConstraint,
    DateTime,
    Index,
    Integer,
    Text,
    UniqueConstraint,
    func,
    text,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column

from app.db.base import Base

STATUSES = ("queued", "running", "done", "failed")


class Job(Base):
    __tablename__ = "jobs"
    # RETURNING sur INSERT : id (uuidv7), status, attempts, created_at sont lus au flush.
    __mapper_args__ = {"eager_defaults": True}

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), primary_key=True, server_default=text("uuidv7()")
    )
    # Id Better Auth opaque — jamais de FK inter-bases. Pas d'index dédié : le btree de
    # `ix_jobs_org_id_type_created_at` est préfixé par org_id et sert déjà les filtres.
    org_id: Mapped[str] = mapped_column(Text, nullable=False)
    type: Mapped[str] = mapped_column(Text, nullable=False)
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'queued'"))
    # Incrémenté à chaque claim ; plafond `max_attempts` déclaré par type au registre.
    attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
    # Anti-empilement, pas anti-rejeu. Colonne dédiée : jamais d'index UNIQUE sur du jsonb.
    # Le moment du relâchement dépend du `dedup_scope` du type (spec §1).
    dedup_key: Mapped[str | None] = mapped_column(Text, nullable=True)
    payload: Mapped[dict[str, Any]] = mapped_column(
        JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    # Écrit par le job en cours d'exécution (heartbeat `updated_at`, curseur de reprise).
    progress: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
    # Écrit une fois, au passage à done/failed.
    report: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
    error: Mapped[str | None] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
    finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)

    __table_args__ = (
        CheckConstraint("status IN ('queued', 'running', 'done', 'failed')", name="ck_jobs_status"),
        UniqueConstraint("dedup_key", name="uq_jobs_dedup_key"),
        # File d'attente et surveillance : la table grossit (jamais purgée en v1), la file
        # reste petite — l'index partiel ne référence que les jobs vivants.
        Index(
            "ix_jobs_queue",
            "status",
            "created_at",
            postgresql_where=text("status IN ('queued', 'running')"),
        ),
    )


# Hors `__table_args__` : `Job.created_at.desc()` a besoin de la classe déjà construite.
Index("ix_jobs_org_id_type_created_at", Job.org_id, Job.type, Job.created_at.desc())
