from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import BigInteger, Boolean, DateTime, Float, ForeignKey, Integer, String, Text, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base from app.models.base import TimestampMixin, UUIDPrimaryKeyMixin class Document(Base, UUIDPrimaryKeyMixin, TimestampMixin): """Uploaded document record.""" __tablename__ = "documents" filename: Mapped[str] = mapped_column(String(500), nullable=False) original_filename: Mapped[str] = mapped_column(String(500), nullable=False) content_type: Mapped[str] = mapped_column(String(100), nullable=False) file_size: Mapped[int] = mapped_column(BigInteger, nullable=False) checksum: Mapped[str] = mapped_column(String(128), nullable=False, index=True) storage_path: Mapped[str] = mapped_column(String(1024), nullable=False) status: Mapped[str] = mapped_column( String(50), default="pending", nullable=False, index=True, ) page_count: Mapped[int | None] = mapped_column(Integer, nullable=True) is_scanned: Mapped[bool | None] = mapped_column(Boolean, nullable=True) document_metadata: Mapped[dict | None] = mapped_column(JSONB, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) uploaded_by: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True, ) pages: Mapped[list[DocumentPage]] = relationship( "DocumentPage", back_populates="document", cascade="all, delete-orphan", order_by="DocumentPage.page_number", lazy="selectin", ) template_matches: Mapped[list[TemplateMatch]] = relationship( "TemplateMatch", back_populates="document", cascade="all, delete-orphan", lazy="dynamic", ) def __repr__(self) -> str: return f"" class DocumentPage(Base, UUIDPrimaryKeyMixin): """Individual page within a document.""" __tablename__ = "document_pages" document_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int] = mapped_column(Integer, nullable=False) width: Mapped[float] = mapped_column(Float, nullable=False) height: Mapped[float] = mapped_column(Float, nullable=False) image_path: Mapped[str | None] = mapped_column(String(1024), nullable=True) text_content: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) document: Mapped[Document] = relationship("Document", back_populates="pages") text_blocks: Mapped[list[DocumentTextBlock]] = relationship( "DocumentTextBlock", back_populates="page", cascade="all, delete-orphan", order_by="DocumentTextBlock.sequence", lazy="selectin", ) images: Mapped[list[DocumentImage]] = relationship( "DocumentImage", back_populates="page", cascade="all, delete-orphan", lazy="selectin", ) tables: Mapped[list[DocumentTable]] = relationship( "DocumentTable", back_populates="page", cascade="all, delete-orphan", lazy="selectin", ) def __repr__(self) -> str: return f"" class DocumentTextBlock(Base, UUIDPrimaryKeyMixin): """Extracted text block from a document page.""" __tablename__ = "document_text_blocks" page_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_pages.id", ondelete="CASCADE"), nullable=False, index=True, ) text: Mapped[str] = mapped_column(Text, nullable=False) x: Mapped[float] = mapped_column(Float, nullable=False) y: Mapped[float] = mapped_column(Float, nullable=False) width: Mapped[float] = mapped_column(Float, nullable=False) height: Mapped[float] = mapped_column(Float, nullable=False) confidence: Mapped[float | None] = mapped_column(Float, nullable=True) font_family: Mapped[str | None] = mapped_column(String(255), nullable=True) font_size: Mapped[float | None] = mapped_column(Float, nullable=True) font_color: Mapped[str | None] = mapped_column(String(50), nullable=True) font_style: Mapped[str | None] = mapped_column(String(50), nullable=True) block_type: Mapped[str] = mapped_column( String(50), default="text", nullable=False, ) sequence: Mapped[int] = mapped_column(Integer, default=0, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) page: Mapped[DocumentPage] = relationship("DocumentPage", back_populates="text_blocks") def __repr__(self) -> str: return f"" class DocumentImage(Base, UUIDPrimaryKeyMixin): """Extracted image from a document page.""" __tablename__ = "document_images" page_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_pages.id", ondelete="CASCADE"), nullable=False, index=True, ) x: Mapped[float] = mapped_column(Float, nullable=False) y: Mapped[float] = mapped_column(Float, nullable=False) width: Mapped[float] = mapped_column(Float, nullable=False) height: Mapped[float] = mapped_column(Float, nullable=False) image_path: Mapped[str] = mapped_column(String(1024), nullable=False) image_type: Mapped[str] = mapped_column( String(50), default="figure", nullable=False, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) page: Mapped[DocumentPage] = relationship("DocumentPage", back_populates="images") def __repr__(self) -> str: return f"" class DocumentTable(Base, UUIDPrimaryKeyMixin): """Extracted table from a document page.""" __tablename__ = "document_tables" page_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_pages.id", ondelete="CASCADE"), nullable=False, index=True, ) x: Mapped[float] = mapped_column(Float, nullable=False) y: Mapped[float] = mapped_column(Float, nullable=False) width: Mapped[float] = mapped_column(Float, nullable=False) height: Mapped[float] = mapped_column(Float, nullable=False) rows: Mapped[int] = mapped_column(Integer, nullable=False) columns: Mapped[int] = mapped_column(Integer, nullable=False) data: Mapped[dict | None] = mapped_column(JSONB, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) page: Mapped[DocumentPage] = relationship("DocumentPage", back_populates="tables") def __repr__(self) -> str: return f"" class TemplateMatch(Base, UUIDPrimaryKeyMixin): """Template matching result for a document.""" __tablename__ = "template_matches" document_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("documents.id", ondelete="CASCADE"), nullable=False, index=True, ) format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) confidence_score: Mapped[float] = mapped_column(Float, nullable=False) match_details: Mapped[dict | None] = mapped_column(JSONB, nullable=True) selected: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) document: Mapped[Document] = relationship("Document", back_populates="template_matches") template: Mapped[DocumentFormat] = relationship("DocumentFormat") def __repr__(self) -> str: return f""