"""HeadAutoApplyRun — persisted lifecycle of an earned-auto-apply sweep (#114).

A graduated head can apply its tag to images it scores above the head's
auto-apply threshold, without a human. This row tracks one such sweep (or a
dry-run PREVIEW of it) so the result survives navigation and the admin card can
show what fired / what would fire. Mirrors HeadTrainingRun. State machine:
running → ready / error. The `report` JSONB holds per-concept counts
(applied / projected / scanned).
"""

from datetime import datetime
from typing import Any

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

from .base import Base


class HeadAutoApplyRun(Base):
    __tablename__ = "head_auto_apply_run"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    # dry_run=True is a PREVIEW: scores + counts what WOULD apply, writes nothing
    # (preview/apply parity, rule 93).
    dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    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
    )
    # Total tags applied across all heads this sweep (0 for a clean dry-run).
    n_applied: Mapped[int | None] = mapped_column(Integer, nullable=True)
    # Per-concept breakdown: [{tag_id, name, applied, scanned, threshold}, ...].
    report: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
    error: Mapped[str | None] = mapped_column(Text, nullable=True)
    last_progress_at: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
