from datetime import date, datetime, time from uuid import uuid4 from sqlalchemy import ( JSON, Boolean, Date, DateTime, Float, ForeignKey, Integer, String, Text, Time, UniqueConstraint, func, ) from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): """Declarative base.""" class User(Base): """User entity.""" __tablename__ = "users" id: Mapped[str] = mapped_column(String(255), primary_key=True) name: Mapped[str] = mapped_column(String(255), nullable=False) avatar_url: Mapped[str | None] = mapped_column(String(500)) provider: Mapped[str] = mapped_column(String(50), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) stories: Mapped[list["Story"]] = relationship( "Story", back_populates="user", cascade="all, delete-orphan" ) child_profiles: Mapped[list["ChildProfile"]] = relationship( "ChildProfile", back_populates="user", cascade="all, delete-orphan" ) class Story(Base): """Story entity.""" __tablename__ = "stories" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) user_id: Mapped[str] = mapped_column( String(255), ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) child_profile_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="SET NULL"), nullable=True ) universe_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("story_universes.id", ondelete="SET NULL"), nullable=True ) title: Mapped[str] = mapped_column(String(255), nullable=False) story_text: Mapped[str | None] = mapped_column(Text, nullable=True) pages: Mapped[list[dict] | None] = mapped_column(JSON, default=list) cover_prompt: Mapped[str | None] = mapped_column(Text) image_url: Mapped[str | None] = mapped_column(String(500)) mode: Mapped[str] = mapped_column(String(20), nullable=False, default="generated") generation_status: Mapped[str] = mapped_column( String(32), nullable=False, default="narrative_ready" ) image_status: Mapped[str] = mapped_column( String(32), nullable=False, default="not_requested" ) audio_status: Mapped[str] = mapped_column( String(32), nullable=False, default="not_requested" ) audio_path: Mapped[str | None] = mapped_column(String(500)) last_error: Mapped[str | None] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) user: Mapped["User"] = relationship("User", back_populates="stories") child_profile: Mapped["ChildProfile | None"] = relationship("ChildProfile") story_universe: Mapped["StoryUniverse | None"] = relationship("StoryUniverse") def _uuid() -> str: return str(uuid4()) class ChildProfile(Base): """Child profile entity.""" __tablename__ = "child_profiles" __table_args__ = (UniqueConstraint("user_id", "name", name="uq_child_profile_user_name"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( String(255), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) name: Mapped[str] = mapped_column(String(50), nullable=False) avatar_url: Mapped[str | None] = mapped_column(String(500)) birth_date: Mapped[date | None] = mapped_column(Date) gender: Mapped[str | None] = mapped_column(String(10)) interests: Mapped[list[str]] = mapped_column(JSON, default=list) growth_themes: Mapped[list[str]] = mapped_column(JSON, default=list) reading_preferences: Mapped[dict] = mapped_column(JSON, default=dict) stories_count: Mapped[int] = mapped_column(Integer, default=0) total_reading_time: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) user: Mapped["User"] = relationship("User", back_populates="child_profiles") story_universes: Mapped[list["StoryUniverse"]] = relationship( "StoryUniverse", back_populates="child_profile", cascade="all, delete-orphan" ) @property def age(self) -> int | None: if not self.birth_date: return None today = date.today() return today.year - self.birth_date.year - ( (today.month, today.day) < (self.birth_date.month, self.birth_date.day) ) class StoryUniverse(Base): """Story universe entity.""" __tablename__ = "story_universes" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) child_profile_id: Mapped[str] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) name: Mapped[str] = mapped_column(String(100), nullable=False) protagonist: Mapped[dict] = mapped_column(JSON, nullable=False) recurring_characters: Mapped[list] = mapped_column(JSON, default=list) world_settings: Mapped[dict] = mapped_column(JSON, default=dict) achievements: Mapped[list] = mapped_column(JSON, default=list) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) child_profile: Mapped["ChildProfile"] = relationship( "ChildProfile", back_populates="story_universes" ) class ReadingEvent(Base): """Reading event entity.""" __tablename__ = "reading_events" id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) child_profile_id: Mapped[str] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) story_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("stories.id", ondelete="SET NULL"), nullable=True, index=True ) event_type: Mapped[str] = mapped_column(String(20), nullable=False) reading_time: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), index=True ) class PushConfig(Base): """Push configuration entity.""" __tablename__ = "push_configs" __table_args__ = ( UniqueConstraint("child_profile_id", name="uq_push_config_child"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( String(255), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) child_profile_id: Mapped[str] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) push_time: Mapped[time | None] = mapped_column(Time) push_days: Mapped[list[int]] = mapped_column(JSON, default=list) enabled: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class PushEvent(Base): """Push event entity.""" __tablename__ = "push_events" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) user_id: Mapped[str] = mapped_column( String(255), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) child_profile_id: Mapped[str] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) trigger_type: Mapped[str] = mapped_column(String(20), nullable=False) status: Mapped[str] = mapped_column(String(20), nullable=False) reason: Mapped[str | None] = mapped_column(Text) sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) class MemoryItem(Base): """Memory item entity with time decay metadata.""" __tablename__ = "memory_items" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) child_profile_id: Mapped[str] = mapped_column( String(36), ForeignKey("child_profiles.id", ondelete="CASCADE"), nullable=False, index=True ) universe_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("story_universes.id", ondelete="SET NULL"), nullable=True, index=True ) type: Mapped[str] = mapped_column(String(50), nullable=False) value: Mapped[dict] = mapped_column(JSON, nullable=False) base_weight: Mapped[float] = mapped_column(Float, default=1.0) last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) ttl_days: Mapped[int | None] = mapped_column(Integer)