"""HeadTrainingRun — persisted lifecycle of a head-training batch (#114).

A persisted run row (not transient frontend state) so the run SURVIVES
navigation and the admin card can show live + historical status.
Training is idempotent (it upserts tag_head rows), so a SIGKILL'd run is harmless
— a maintenance recovery sweep flips a stalled `running` row to `error`, and the
next run re-trains. State machine: running → ready / error.
"""

from datetime import datetime
from typing import Any

from sqlalchemy import DateTime, Integer, String, Text, func
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column

from .base import Base


class HeadTrainingRun(Base):
    __tablename__ = "head_training_run"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    # Training parameters: {min_positives, neg_ratio, precision_target, ...}.
    params: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    status: Mapped[str] = mapped_column(
        String(16), nullable=False, default="running", index=True
    )
    # running | ready | error
    started_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, server_default=func.now()
    )
    finished_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    # How many concepts got a (re)trained head vs were skipped (too few labels).
    n_trained: Mapped[int | None] = mapped_column(Integer, nullable=True)
    n_skipped: Mapped[int | None] = mapped_column(Integer, nullable=True)
    error: Mapped[str | None] = mapped_column(Text, nullable=True)
    # Last time the task made progress — the recovery sweep tells a live run
    # from a SIGKILL'd one by this.
    last_progress_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
