"""SQLAlchemy ORM models for jobs + extraction results.

We store the structured result as JSON. PaddleOCR's `raw_text` can run into
the tens of kilobytes for multi-page documents; that's well within Postgres'
JSONB row-size budget. SQLite stores it as TEXT under the hood.

Schema choice: we keep the result inline on the same row instead of a
separate `extraction_results` table. The 1:1 relationship would otherwise
add a join on every read, with no real benefit since results are immutable
once written and there's no use-case for fetching just the metadata.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4

from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, String, Uuid
from sqlalchemy.orm import Mapped, mapped_column

from ocr_sprint.db.base import Base


def _utcnow() -> datetime:
    return datetime.now(timezone.utc)


class JobRow(Base):
    __tablename__ = "jobs"

    # SQLAlchemy 2.0's Uuid type maps to native UUID on Postgres and CHAR(32)
    # on SQLite, so the same model works in both environments.
    job_id: Mapped[UUID] = mapped_column(Uuid, primary_key=True, default=uuid4)
    status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
    source_kind: Mapped[str] = mapped_column(String(16), nullable=False, default="unknown")
    filename: Mapped[str] = mapped_column(String(512), nullable=False, default="")
    blob_key: Mapped[str | None] = mapped_column(String(512), nullable=True)

    confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
    review_flags: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list)
    result: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
    error: Mapped[str | None] = mapped_column(String(2048), nullable=True)

    # Phase 6 — HITL review state.
    # Once ``approved=True`` the row is immutable except to admin users;
    # corrections after that point are rejected by the route. ``reviewed_by``
    # stores the free-form user identifier the reviewer sent via the
    # ``X-User-Id`` header (best-effort attribution — no full RBAC yet).
    approved: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
    reviewed_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
    reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)

    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, default=_utcnow
    )
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, default=_utcnow, onupdate=_utcnow
    )

    def __repr__(self) -> str:
        return f"JobRow(job_id={self.job_id!s}, status={self.status!r})"


class JobCorrectionRow(Base):
    """One correction event on a job's ``result``.

    Each PATCH call writes one row per changed field path so we have a
    full audit trail. Rows are append-only — never updated, never
    deleted — so the history is reproducible and usable as ground-truth
    data for future fine-tuning.
    """

    __tablename__ = "job_corrections"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    job_id: Mapped[UUID] = mapped_column(
        Uuid, ForeignKey("jobs.job_id", ondelete="CASCADE"), nullable=False, index=True
    )
    # Dotted JSON path into ExtractionResult, e.g. "header.nomor_sprint" or
    # "personel[3].nrp". Kept as a plain string for simplicity — we don't
    # parse it server-side beyond the allow-list check in the repository.
    field_path: Mapped[str] = mapped_column(String(256), nullable=False)
    old_value: Mapped[Any | None] = mapped_column(JSON, nullable=True)
    new_value: Mapped[Any | None] = mapped_column(JSON, nullable=True)
    corrected_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
    reason: Mapped[str | None] = mapped_column(String(512), nullable=True)
    corrected_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), nullable=False, default=_utcnow
    )

    def __repr__(self) -> str:
        return (
            f"JobCorrectionRow(job_id={self.job_id!s}, "
            f"field={self.field_path!r}, by={self.corrected_by!r})"
        )
