"""SQLAlchemy models for initiative persistence (subset used by API; full schema in migrations)."""

from __future__ import annotations

import uuid
from datetime import date, datetime
from typing import Any, Optional

from sqlalchemy import BigInteger, Boolean, Date, DateTime, Float, ForeignKey, Integer, Numeric, Text, UniqueConstraint, text
from sqlalchemy.dialects.postgresql import ENUM, JSONB, UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


class Base(DeclarativeBase):
    pass


submission_status_enum = ENUM(
    "draft",
    "submitted",
    "under_review",
    "approved",
    "rejected",
    name="submission_status",
    create_type=False,
)

recognition_tier_enum = ENUM("excellent", "good", name="recognition_tier", create_type=False)

user_role_enum = ENUM(
    "applicant",
    "council_member",
    "editor",
    "admin",
    "viewer",
    name="user_role",
    create_type=False,
)

profile_verification_status_enum = ENUM(
    "draft",
    "pending",
    "verified",
    "rejected",
    name="profile_verification_status",
    create_type=False,
)

audit_action_enum = ENUM(
    "create",
    "read",
    "update",
    "delete",
    "login",
    "logout",
    "login_failed",
    name="audit_action",
    create_type=False,
)


class Unit(Base):
    """Faculty / department catalog (see migrations/001_initiative_schema.sql)."""

    __tablename__ = "units"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name: Mapped[str] = mapped_column(Text, nullable=False)
    parent_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("units.id", ondelete="SET NULL"), nullable=True
    )
    address: Mapped[Optional[str]] = mapped_column(Text, nullable=True)


class User(Base):
    __tablename__ = "users"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    email: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
    password_hash: Mapped[str] = mapped_column(Text, nullable=False)
    full_name: Mapped[str] = mapped_column(Text, nullable=False)
    phone: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    unit_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("units.id", ondelete="SET NULL"), nullable=True
    )
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    email_verified: Mapped[bool] = mapped_column(
        Boolean, nullable=False, server_default=text("false"), default=False
    )
    credential_version: Mapped[int] = mapped_column(
        Integer, nullable=False, server_default=text("0"), default=0
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))

    staff_profile: Mapped[Optional["UserStaffProfile"]] = relationship(
        "UserStaffProfile",
        back_populates="user",
        uselist=False,
        foreign_keys="UserStaffProfile.user_id",
    )


class UserRoleRow(Base):
    __tablename__ = "user_roles"

    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
    )
    role: Mapped[str] = mapped_column(user_role_enum, primary_key=True)
    admin_from_email_policy: Mapped[bool] = mapped_column(
        Boolean, nullable=False, server_default=text("false")
    )


class AcademicTitle(Base):
    """Lookup for academic ranks / degrees (avoid Postgres ENUM drift for title list)."""

    __tablename__ = "academic_titles"

    code: Mapped[str] = mapped_column(Text, primary_key=True)
    label_vi: Mapped[str] = mapped_column(Text, nullable=False)
    label_en: Mapped[str] = mapped_column(Text, nullable=False)
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))


class UserStaffProfile(Base):
    """1:1 institutional profile + verification state (HR scalars; not MinIO)."""

    __tablename__ = "user_staff_profiles"

    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
    )
    employee_id: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    academic_title_code: Mapped[Optional[str]] = mapped_column(
        Text, ForeignKey("academic_titles.code"), nullable=True
    )
    academic_title_other: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    unit_name_freetext: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    job_title: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

    profile_verification_status: Mapped[str] = mapped_column(
        profile_verification_status_enum, nullable=False, server_default=text("'draft'")
    )
    verification_submitted_at: Mapped[Optional[datetime]] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    verified_by_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id"), nullable=True
    )
    rejection_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

    version: Mapped[int] = mapped_column(Integer, nullable=False, server_default="1")
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))

    user: Mapped["User"] = relationship(
        "User",
        back_populates="staff_profile",
        foreign_keys=[user_id],
    )


class EmailVerificationToken(Base):
    """One-time email verification link secrets (store hash only)."""

    __tablename__ = "email_verification_tokens"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
    used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )


class RegistrationOtpCode(Base):
    """Hashed 6-digit OTP for registration verification (magic-link flow superseded on register)."""

    __tablename__ = "registration_otp_codes"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    otp_hash: Mapped[str] = mapped_column(Text, nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
    failed_attempts: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
    used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )


class PasswordResetToken(Base):
    """One-time password reset secrets (store hash only)."""

    __tablename__ = "password_reset_tokens"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    token_hash: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
    used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )


class Initiative(Base):
    __tablename__ = "initiatives"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    case_code: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
    owner_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
    status: Mapped[str] = mapped_column(submission_status_enum, nullable=False, server_default="draft")
    recognition_tier: Mapped[Optional[str]] = mapped_column(recognition_tier_enum, nullable=True)
    submitted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))

    drafts: Mapped[list["Draft"]] = relationship(
        "Draft",
        back_populates="initiative",
        cascade="all, delete-orphan",
    )


class Draft(Base):
    __tablename__ = "drafts"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    draft_code: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    version: Mapped[int] = mapped_column(Integer, nullable=False, server_default="1")
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))

    initiative: Mapped["Initiative"] = relationship("Initiative", back_populates="drafts")


class AuditLog(Base):
    __tablename__ = "audit_log"

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    actor_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
    action: Mapped[str] = mapped_column(Text, nullable=False)
    entity: Mapped[str] = mapped_column(Text, nullable=False)
    entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False)
    diff: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class AuditEvent(Base):
    """Append-only CRUD / auth audit trail (see migrations/008_audit_events.sql)."""

    __tablename__ = "audit_events"

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    occurred_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )
    actor_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    actor_email: Mapped[str] = mapped_column(Text, nullable=False)
    actor_role: Mapped[str] = mapped_column(Text, nullable=False)
    action: Mapped[str] = mapped_column(audit_action_enum, nullable=False)
    entity_type: Mapped[str] = mapped_column(Text, nullable=False)
    entity_id: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    before: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    after: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    metadata_: Mapped[dict[str, Any]] = mapped_column(
        "metadata", JSONB, nullable=False, server_default=text("'{}'::jsonb")
    )
    request_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), nullable=True)


class DraftTabSnapshot(Base):
    """Per-tab JSONB version history for an initiative draft (autosave / explicit save)."""

    __tablename__ = "draft_tab_snapshots"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    draft_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("drafts.id", ondelete="SET NULL"), nullable=True
    )
    tab: Mapped[str] = mapped_column(Text, nullable=False)
    tab_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default="1")
    payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    source: Mapped[str] = mapped_column(Text, nullable=False, server_default="autosave")
    captured_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ApplicationSubmitSnapshot(Base):
    """Immutable snapshot of merged tabs + metadata at submit time."""

    __tablename__ = "application_submit_snapshots"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    submission_record_id: Mapped[str] = mapped_column(Text, nullable=False)
    merged_tabs: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    submit_metadata: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
    full_pdf_uri: Mapped[str] = mapped_column(Text, nullable=False)
    captured_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ApplicationWorkflow(Base):
    """Council-facing workflow projection (review status, deadlines, assignments)."""

    __tablename__ = "application_workflow"

    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), primary_key=True
    )
    review_status: Mapped[str] = mapped_column(Text, nullable=False, server_default="not_reviewed")
    review_deadline: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
    reviewer: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    supervisor: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    conference: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ApplicationTaxonomy(Base):
    """Subject / group / topic type for list views and analytics."""

    __tablename__ = "application_taxonomy"

    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), primary_key=True
    )
    subject_id: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
    group_id: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
    topic_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ApplicationArtifact(Base):
    """Registered deliverables (full PDF, evidence kinds, future abstract/poster)."""

    __tablename__ = "application_artifacts"
    __table_args__ = (UniqueConstraint("initiative_id", "role", name="uq_application_artifacts_init_role"),)

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    role: Mapped[str] = mapped_column(Text, nullable=False)
    storage_kind: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    storage_uri: Mapped[str] = mapped_column(Text, nullable=False)
    original_name: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    mime_type: Mapped[str] = mapped_column(Text, nullable=False, server_default="application/pdf")
    byte_size: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
    sha256: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    uploaded_by: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
    uploaded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    # Staff (admin / hội đồng) — minh chứng upload; applicants do not set these
    review_status: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    reviewed_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)


class ApplicationAdminResult(Base):
    """Quản trị ghi nhận kết quả Duyệt/Từ chối theo initiative (một bản ghi / hồ sơ)."""

    __tablename__ = "application_admin_results"
    __table_args__ = (UniqueConstraint("initiative_id", name="uq_application_admin_results_initiative"),)

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    decision: Mapped[str] = mapped_column(Text, nullable=False)
    feedback: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
    rationale: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    updated_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )


class UserNotification(Base):
    """Applicant inbox row; created when admin saves adjudication (best-effort, post-commit)."""

    __tablename__ = "user_notifications"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    recipient_user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    type: Mapped[str] = mapped_column(Text, nullable=False)
    title: Mapped[str] = mapped_column(Text, nullable=False)
    body: Mapped[str] = mapped_column(Text, nullable=False)
    application_id: Mapped[str] = mapped_column(Text, nullable=False)
    related_initiative_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="SET NULL"), nullable=True
    )
    source_admin_result_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("application_admin_results.id", ondelete="SET NULL"), nullable=True
    )
    decision: Mapped[str] = mapped_column(Text, nullable=False)
    merit_category_label: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    feedback_text: Mapped[str] = mapped_column(Text, nullable=False, server_default="")
    rationale_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    read_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ApplicationReviewDocument(Base):
    """Versioned ReviewPanel JSON bundles for DOCX/backoffice pipelines."""

    __tablename__ = "application_review_documents"
    __table_args__ = (UniqueConstraint("initiative_id", "document_version", name="uq_review_docs_init_ver"),)

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    initiative_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("initiatives.id", ondelete="CASCADE"), nullable=False
    )
    case_id: Mapped[str] = mapped_column(Text, nullable=False)
    document_version: Mapped[int] = mapped_column(Integer, nullable=False, server_default="1")
    official_bieu_mau: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, server_default=text("'{}'::jsonb"))
    template_data: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    full_bundle: Mapped[Optional[dict[str, Any]]] = mapped_column(JSONB, nullable=True)
    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class DocumentTemplate(Base):
    """Admin-managed DOCX template (file in MinIO) + extracted Jinja placeholder fields."""

    __tablename__ = "document_templates"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name: Mapped[str] = mapped_column(Text, nullable=False)
    description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    storage_key: Mapped[str] = mapped_column(Text, nullable=False)
    original_filename: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    content_sha256: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    fields: Mapped[list[dict[str, Any]]] = mapped_column(
        JSONB, nullable=False, server_default=text("'[]'::jsonb")
    )
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


# --------------------------------------------------------------------------- #
# Research projects (Thuyết minh đề tài) + PI cockpit entities — migration 016.
# The proposal row IS the project across its lifecycle (draft→submitted→approved|rejected);
# child research_project_* tables hold the cockpit entities. Owner+admin authz (v1).
# --------------------------------------------------------------------------- #
class ResearchProject(Base):
    """A research-project proposal that becomes a managed project on approval."""

    __tablename__ = "research_projects"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    owner_user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'draft'"))
    code: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    title: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    level: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    pi_name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    period_months: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
    budget_total: Mapped[Optional[float]] = mapped_column(Numeric(14, 2), nullable=True)
    content: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, server_default=text("'{}'::jsonb"))
    submitted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    reviewed_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
    review_note: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectMember(Base):
    __tablename__ = "research_project_members"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    role: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    access: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    org: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    email: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    months: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
    tasks: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectDataset(Base):
    __tablename__ = "research_project_datasets"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    type: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    records: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
    source: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    sensitivity: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    ethics: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    owner: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectModel(Base):
    __tablename__ = "research_project_models"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    task: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    framework: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    version: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    dataset: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    auc: Mapped[Optional[float]] = mapped_column(Numeric(6, 4), nullable=True)
    sensitivity: Mapped[Optional[float]] = mapped_column(Numeric(6, 4), nullable=True)
    specificity: Mapped[Optional[float]] = mapped_column(Numeric(6, 4), nullable=True)
    accuracy: Mapped[Optional[float]] = mapped_column(Numeric(6, 4), nullable=True)
    owner: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    notes: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectAsset(Base):
    __tablename__ = "research_project_assets"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    category: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    acquisition: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    value: Mapped[Optional[float]] = mapped_column(Numeric(14, 2), nullable=True)
    owner: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    notes: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectMilestone(Base):
    __tablename__ = "research_project_milestones"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    sort_order: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    title: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    deliverable: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    start_period: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    end_period: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    owner: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    budget: Mapped[Optional[float]] = mapped_column(Numeric(14, 2), nullable=True)
    progress: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ResearchProjectAudit(Base):
    """Append-only audit trail for a research project (lifecycle + entity CRUD)."""

    __tablename__ = "research_project_audit"

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    project_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="CASCADE"), nullable=False
    )
    occurred_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )
    actor_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    actor_name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    role_label: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    action: Mapped[str] = mapped_column(Text, nullable=False)
    subject: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    detail: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))


# --------------------------------------------------------------------------- #
# ImageHub: content-addressed imaging dataset versioning — migration 017.
# A dataset is owned by a user; files dedupe into imagehub_blobs by sha256; an
# imagehub_version freezes a manifest snapshot of the working files. Owner+admin authz (v1).
# --------------------------------------------------------------------------- #
class ImagehubDataset(Base):
    """An ImageHub dataset (a versioned collection of imaging files)."""

    __tablename__ = "imagehub_datasets"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    owner_user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    slug: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    description: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    visibility: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'private'"))
    modality_tags: Mapped[Any] = mapped_column(JSONB, nullable=False, server_default=text("'[]'::jsonb"))
    # Per-dataset value->name label map for multi-label masks (migration 027), e.g. {"1":"kidney"}.
    label_map: Mapped[Any] = mapped_column(JSONB, nullable=False, server_default=text("'{}'::jsonb"))
    default_branch: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'main'"))
    # The research project (the "workspace") this dataset belongs to, if any (migration 024).
    research_project_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("research_projects.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubBlob(Base):
    """Globally content-addressed blob registry (dedup by sha256)."""

    __tablename__ = "imagehub_blobs"

    sha256: Mapped[str] = mapped_column(Text, primary_key=True)
    size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False, server_default="0")
    media_type: Mapped[str] = mapped_column(
        Text, nullable=False, server_default=text("'application/octet-stream'")
    )
    storage_bucket: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    storage_key: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubStorageMethod(Base):
    """A verified external storage method (S3/GCS/Azure) for Cloud Import. Credentials live
    encrypted in config_encrypted and are NEVER returned to the client (privacy rule SM7)."""

    __tablename__ = "imagehub_storage_methods"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    owner_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    provider: Mapped[str] = mapped_column(Text, nullable=False)
    access_mode: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'read'"))
    bucket: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    region: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    config_encrypted: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    verification_status: Mapped[str] = mapped_column(
        Text, nullable=False, server_default=text("'pending'")
    )
    verification_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    verification_checked_at: Mapped[Optional[datetime]] = mapped_column(
        DateTime(timezone=True), nullable=True
    )
    created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubDatasetFile(Base):
    """Current working file set on a dataset default branch (one row per folder_path + logical_path)."""

    __tablename__ = "imagehub_dataset_files"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    logical_path: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    # Folder path (migration 026): the relative directory of the file inside the dataset so an
    # uploaded tree (e.g. nnU-Net imagesTr/labelsTr) is preserved. '' = dataset root.
    folder_path: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    blob_sha256: Mapped[Optional[str]] = mapped_column(
        Text, ForeignKey("imagehub_blobs.sha256", ondelete="RESTRICT"), nullable=True
    )
    size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False, server_default="0")
    media_type: Mapped[str] = mapped_column(
        Text, nullable=False, server_default=text("'application/octet-stream'")
    )
    imaging_meta: Mapped[Any] = mapped_column(JSONB, nullable=False, server_default=text("'{}'::jsonb"))
    # Segmentation linking (migration 018): a mask row (file_kind='segmentation') points at the
    # image it segments via parent_file_id; organ_label names the organ. Regular files are 'image'.
    file_kind: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'image'"))
    parent_file_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("imagehub_dataset_files.id", ondelete="CASCADE"),
        nullable=True,
    )
    organ_label: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    # Cloud Import (migration 019): a file is EITHER a local content-addressed blob (blob_sha256)
    # OR an external reference (storage_method_id + external_path) that streams from a verified
    # storage method and is never copied to our servers (privacy rule C4).
    storage_method_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("imagehub_storage_methods.id", ondelete="RESTRICT"),
        nullable=True,
    )
    external_path: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
    uploaded_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubVersion(Base):
    """A frozen manifest snapshot of a dataset working files (DAG-ready via parent_version_id)."""

    __tablename__ = "imagehub_versions"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    seq: Mapped[int] = mapped_column(Integer, nullable=False, server_default="1")
    message: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    manifest: Mapped[Any] = mapped_column(JSONB, nullable=False, server_default=text("'[]'::jsonb"))
    parent_version_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_versions.id", ondelete="SET NULL"), nullable=True
    )
    author_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubDatasetStage(Base):
    """A labeling-pipeline stage on a dataset (Label -> Review_1 -> Review_2 ...). `auto_assign`
    is the 'Automatic Task Assignment' toggle; `review_percent` applies to review stages."""

    __tablename__ = "imagehub_dataset_stages"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    kind: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'label'"))
    seq: Mapped[int] = mapped_column(Integer, nullable=False, server_default="0")
    review_percent: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
    auto_assign: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("true"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubDatasetAudit(Base):
    """Append-only audit trail for an ImageHub dataset."""

    __tablename__ = "imagehub_dataset_audit"

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    occurred_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), server_default=text("now()"), nullable=False
    )
    actor_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    actor_name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    role_label: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    action: Mapped[str] = mapped_column(Text, nullable=False)
    subject: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    detail: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))


class ImagehubTask(Base):
    """A unit of labeling work: one dataset file moving through the dataset's pipeline stages.

    A NEW join row (not the file mutated in place): it carries the file's pipeline position
    (``current_stage_id`` + ``pipeline_state``), the per-user queue ``queue_status``, the
    ``assignee``, a ``priority`` float (0..1), and the Ground-Truth ``is_reference_standard`` flag.
    Single-user MVP: task access reuses the dataset owner-or-admin gate; multi-labeler membership is
    a later phase. ``UNIQUE(dataset_file_id)`` enforces one task per file (droppable later).
    """

    __tablename__ = "imagehub_tasks"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    dataset_file_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_dataset_files.id", ondelete="CASCADE"), nullable=False
    )
    name: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    current_stage_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_dataset_stages.id", ondelete="SET NULL"), nullable=True
    )
    # §3 pipeline state: inLabel -> inReview -> groundTruth (terminal); issue diverts (later phase).
    pipeline_state: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'inLabel'"))
    # §4 per-user queue status within a stage.
    queue_status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'assigned'"))
    assignee_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    # A2 manual assignment is sticky (never auto-reassigned); 'auto' is first-come-first-serve.
    assignment_mode: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'auto'"))
    priority: Mapped[float] = mapped_column(Float, nullable=False, server_default=text("0"))
    is_reference_standard: Mapped[bool] = mapped_column(
        Boolean, nullable=False, server_default=text("false")
    )
    # The labeler's vector annotations (bbox/points/pen/brush/polygon), normalized [0..1] geometry
    # per slice — persisted as JSON so the AnnotationTool can load + save a task's work (migr 022).
    annotations: Mapped[Any] = mapped_column(JSONB, nullable=False, server_default=text("'[]'::jsonb"))
    # Q2: a skipped task goes to the end of the queue, ordered by this monotonic seq.
    skipped_seq: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
    updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubDatasetMember(Base):
    """A user (other than the owner) granted access to a dataset's labeling work (multi-labeler).

    MVP: all members are labelers — they view the dataset and work tasks assigned to them; dataset /
    stage / settings management stays with the owner + platform admins. ``role`` is reserved for a
    future project-admin tier. UNIQUE(dataset_id, user_id): one membership per user per dataset.
    """

    __tablename__ = "imagehub_dataset_members"

    id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    user_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
    )
    role: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'member'"))
    added_by: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))


class ImagehubTaskReviewEvent(Base):
    """An append-only record of a task review decision (accept / acceptWithCorrections / reject),
    capturing the reviewer, the stage reviewed, and an optional note (e.g. a reject reason).
    Powers review history + per-reviewer accept/reject counters (migration 025)."""

    __tablename__ = "imagehub_task_review_events"

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
    dataset_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_datasets.id", ondelete="CASCADE"), nullable=False
    )
    task_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_tasks.id", ondelete="CASCADE"), nullable=False
    )
    stage_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("imagehub_dataset_stages.id", ondelete="SET NULL"), nullable=True
    )
    reviewer_user_id: Mapped[Optional[uuid.UUID]] = mapped_column(
        UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
    )
    decision: Mapped[str] = mapped_column(Text, nullable=False)
    note: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=text("now()"))
