from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import 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 DocumentFormat(Base, UUIDPrimaryKeyMixin, TimestampMixin): """Reusable document template format.""" __tablename__ = "document_formats" name: Mapped[str] = mapped_column(String(255), nullable=False, index=True) description: Mapped[str | None] = mapped_column(Text, nullable=True) page_width: Mapped[float] = mapped_column(Float, nullable=False) page_height: Mapped[float] = mapped_column(Float, nullable=False) page_count: Mapped[int] = mapped_column(Integer, default=1, nullable=False) margin_top: Mapped[float] = mapped_column(Float, default=72.0, nullable=False) margin_right: Mapped[float] = mapped_column(Float, default=72.0, nullable=False) margin_bottom: Mapped[float] = mapped_column(Float, default=72.0, nullable=False) margin_left: Mapped[float] = mapped_column(Float, default=72.0, nullable=False) fingerprint: Mapped[dict | None] = mapped_column(JSONB, nullable=True) source_document_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("documents.id", ondelete="SET NULL"), nullable=True, index=True, ) version: Mapped[int] = mapped_column(Integer, default=1, nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, index=True) created_by: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) cells: Mapped[list[DocumentCell]] = relationship( "DocumentCell", back_populates="format", cascade="all, delete-orphan", order_by="DocumentCell.sequence", lazy="selectin", ) regions: Mapped[list[DocumentRegion]] = relationship( "DocumentRegion", back_populates="format", cascade="all, delete-orphan", order_by="DocumentRegion.sequence", lazy="selectin", ) table_formats: Mapped[list[TableFormat]] = relationship( "TableFormat", back_populates="format", cascade="all, delete-orphan", lazy="selectin", ) watermarks: Mapped[list[Watermark]] = relationship( "Watermark", back_populates="format", cascade="all, delete-orphan", lazy="selectin", ) image_regions: Mapped[list[ImageRegion]] = relationship( "ImageRegion", back_populates="format", cascade="all, delete-orphan", lazy="selectin", ) fingerprint_record: Mapped[TemplateFingerprint | None] = relationship( "TemplateFingerprint", back_populates="format", uselist=False, cascade="all, delete-orphan", lazy="selectin", ) def __repr__(self) -> str: return f"" class DocumentCell(Base, UUIDPrimaryKeyMixin): """Cell definition within a document template.""" __tablename__ = "document_cells" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int] = mapped_column(Integer, 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) row_no: Mapped[int] = mapped_column(Integer, default=0, nullable=False) column_no: Mapped[int] = mapped_column(Integer, default=0, nullable=False) data_type: Mapped[str] = mapped_column(String(50), default="text", nullable=False) font_family: Mapped[str | None] = mapped_column(String(255), nullable=True) font_size: Mapped[float | None] = mapped_column(Float, nullable=True) font_style: Mapped[str | None] = mapped_column(String(50), nullable=True) font_color: Mapped[str | None] = mapped_column(String(50), nullable=True) background_color: Mapped[str | None] = mapped_column(String(50), nullable=True) border_top: Mapped[str | None] = mapped_column(String(100), nullable=True) border_right: Mapped[str | None] = mapped_column(String(100), nullable=True) border_bottom: Mapped[str | None] = mapped_column(String(100), nullable=True) border_left: Mapped[str | None] = mapped_column(String(100), nullable=True) padding_top: Mapped[float] = mapped_column(Float, default=0.0, nullable=False) padding_right: Mapped[float] = mapped_column(Float, default=0.0, nullable=False) padding_bottom: Mapped[float] = mapped_column(Float, default=0.0, nullable=False) padding_left: Mapped[float] = mapped_column(Float, default=0.0, nullable=False) alignment: Mapped[str] = mapped_column(String(20), default="left", nullable=False) vertical_alignment: Mapped[str] = mapped_column(String(20), default="top", nullable=False) rowspan: Mapped[int] = mapped_column(Integer, default=1, nullable=False) colspan: Mapped[int] = mapped_column(Integer, default=1, nullable=False) static_text: Mapped[str | None] = mapped_column(Text, nullable=True) field_name: Mapped[str | None] = mapped_column(String(255), nullable=True) sequence: Mapped[int] = mapped_column(Integer, default=0, nullable=False) is_dynamic: 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, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="cells") def __repr__(self) -> str: return f"" class DocumentRegion(Base, UUIDPrimaryKeyMixin): """Region definition within a document template.""" __tablename__ = "document_regions" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int] = mapped_column(Integer, nullable=False) region_type: Mapped[str] = mapped_column(String(50), 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) content: Mapped[dict | None] = mapped_column(JSONB, nullable=True) 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, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="regions") def __repr__(self) -> str: return f"" class TableFormat(Base, UUIDPrimaryKeyMixin): """Table definition within a document template.""" __tablename__ = "table_formats" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int] = mapped_column(Integer, 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) rows: Mapped[int] = mapped_column(Integer, nullable=False) columns: Mapped[int] = mapped_column(Integer, nullable=False) border_style: Mapped[str] = mapped_column(String(50), default="solid", nullable=False) border_width: Mapped[float] = mapped_column(Float, default=1.0, nullable=False) border_color: Mapped[str] = mapped_column(String(50), default="#000000", nullable=False) header_rows: Mapped[int] = mapped_column(Integer, default=1, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="table_formats") table_columns: Mapped[list[TableColumn]] = relationship( "TableColumn", back_populates="table_format", cascade="all, delete-orphan", order_by="TableColumn.column_index", lazy="selectin", ) table_rows: Mapped[list[TableRow]] = relationship( "TableRow", back_populates="table_format", cascade="all, delete-orphan", order_by="TableRow.row_index", lazy="selectin", ) def __repr__(self) -> str: return f"" class TableColumn(Base, UUIDPrimaryKeyMixin): """Column definition within a table format.""" __tablename__ = "table_columns" table_format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("table_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) column_index: Mapped[int] = mapped_column(Integer, nullable=False) width: Mapped[float] = mapped_column(Float, nullable=False) header_text: Mapped[str | None] = mapped_column(String(500), nullable=True) data_type: Mapped[str] = mapped_column(String(50), default="text", nullable=False) alignment: Mapped[str] = mapped_column(String(20), default="left", nullable=False) font_family: Mapped[str | None] = mapped_column(String(255), nullable=True) font_size: Mapped[float | None] = mapped_column(Float, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) table_format: Mapped[TableFormat] = relationship("TableFormat", back_populates="table_columns") def __repr__(self) -> str: return f"" class TableRow(Base, UUIDPrimaryKeyMixin): """Row definition within a table format.""" __tablename__ = "table_rows" table_format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("table_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) row_index: Mapped[int] = mapped_column(Integer, nullable=False) height: Mapped[float] = mapped_column(Float, default=20.0, nullable=False) is_header: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) background_color: Mapped[str | None] = mapped_column(String(50), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) table_format: Mapped[TableFormat] = relationship("TableFormat", back_populates="table_rows") def __repr__(self) -> str: return f"" class Watermark(Base, UUIDPrimaryKeyMixin): """Watermark definition within a document template.""" __tablename__ = "watermarks" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int | None] = mapped_column(Integer, nullable=True) text: Mapped[str | None] = mapped_column(String(500), nullable=True) image_path: Mapped[str | None] = mapped_column(String(1024), nullable=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) opacity: Mapped[float] = mapped_column(Float, default=0.3, nullable=False) rotation: Mapped[float] = mapped_column(Float, default=0.0, nullable=False) 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) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="watermarks") def __repr__(self) -> str: return f"" class ImageRegion(Base, UUIDPrimaryKeyMixin): """Image region within a document template.""" __tablename__ = "image_regions" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, index=True, ) page_number: Mapped[int] = mapped_column(Integer, 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) image_path: Mapped[str | None] = mapped_column(String(1024), nullable=True) image_type: Mapped[str] = mapped_column(String(50), default="figure", nullable=False) is_static: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) field_name: Mapped[str | None] = mapped_column(String(255), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="image_regions") def __repr__(self) -> str: return f"" class TemplateFingerprint(Base, UUIDPrimaryKeyMixin): """Layout fingerprint for template matching.""" __tablename__ = "template_fingerprints" format_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("document_formats.id", ondelete="CASCADE"), nullable=False, unique=True, index=True, ) page_dimensions: Mapped[dict | None] = mapped_column(JSONB, nullable=True) logo_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True) header_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True) footer_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True) table_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True) cell_coordinates: Mapped[dict | None] = mapped_column(JSONB, nullable=True) fingerprint_hash: Mapped[str] = mapped_column(String(256), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), server_default=func.now(), nullable=False, ) format: Mapped[DocumentFormat] = relationship("DocumentFormat", back_populates="fingerprint_record") def __repr__(self) -> str: return f""