from typing import List, Optional from sqlmodel import Field, Relationship, SQLModel from datetime import datetime # --- Join Tables --- class Performance(SQLModel, table=True): """Link table between Show and Song (Many-to-Many with extra data)""" id: Optional[int] = Field(default=None, primary_key=True) slug: Optional[str] = Field(default=None, unique=True, index=True, description="songslug-YYYY-MM-DD") show_id: int = Field(foreign_key="show.id") song_id: int = Field(foreign_key="song.id") position: int = Field(description="Order in the setlist") set_name: Optional[str] = Field(default=None, description="e.g., Set 1, Encore") segue: bool = Field(default=False, description="Transition to next song >") notes: Optional[str] = Field(default=None) track_url: Optional[str] = Field(default=None, description="Deep link to track audio") youtube_link: Optional[str] = Field(default=None, description="YouTube video URL") bandcamp_link: Optional[str] = Field(default=None, description="Bandcamp track URL") nugs_link: Optional[str] = Field(default=None, description="Nugs.net track URL") nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance") show: "Show" = Relationship(back_populates="performances") song: "Song" = Relationship() class ShowArtist(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) show_id: int = Field(foreign_key="show.id") artist_id: int = Field(foreign_key="artist.id") notes: Optional[str] = Field(default=None, description="Role e.g. Guest") class PerformanceArtist(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) performance_id: int = Field(foreign_key="performance.id") artist_id: int = Field(foreign_key="artist.id") notes: Optional[str] = Field(default=None, description="Role e.g. Guest") class PerformanceNickname(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) performance_id: int = Field(foreign_key="performance.id") nickname: str = Field(index=True) description: Optional[str] = Field(default=None) status: str = Field(default="pending", index=True) # pending, approved, rejected suggested_by: int = Field(foreign_key="user.id") created_at: datetime = Field(default_factory=datetime.utcnow) performance: "Performance" = Relationship(back_populates="nicknames") user: "User" = Relationship() class EntityTag(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) tag_id: int = Field(foreign_key="tag.id") entity_type: str = Field(index=True) # "show", "song", "venue" entity_id: int = Field(index=True) # --- Core Entities --- class Vertical(SQLModel, table=True): """Represents a Fandom Vertical (e.g., 'Phish', 'Goose', 'Star Wars')""" id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) slug: str = Field(unique=True, index=True) description: Optional[str] = Field(default=None) shows: List["Show"] = Relationship(back_populates="vertical") songs: List["Song"] = Relationship(back_populates="vertical") class Venue(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) slug: Optional[str] = Field(default=None, unique=True, index=True) city: str state: Optional[str] = Field(default=None) country: str capacity: Optional[int] = Field(default=None) notes: Optional[str] = Field(default=None) shows: List["Show"] = Relationship(back_populates="venue") class Tour(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) slug: Optional[str] = Field(default=None, unique=True, index=True) start_date: Optional[datetime] = None end_date: Optional[datetime] = None notes: Optional[str] = Field(default=None) shows: List["Show"] = Relationship(back_populates="tour") class Artist(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) instrument: Optional[str] = Field(default=None) notes: Optional[str] = Field(default=None) class Show(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) date: datetime = Field(index=True) slug: Optional[str] = Field(default=None, unique=True, index=True) vertical_id: int = Field(foreign_key="vertical.id") venue_id: Optional[int] = Field(default=None, foreign_key="venue.id") tour_id: Optional[int] = Field(default=None, foreign_key="tour.id") notes: Optional[str] = Field(default=None) # External Links bandcamp_link: Optional[str] = Field(default=None) nugs_link: Optional[str] = Field(default=None) youtube_link: Optional[str] = Field(default=None) vertical: Vertical = Relationship(back_populates="shows") venue: Optional[Venue] = Relationship(back_populates="shows") tour: Optional[Tour] = Relationship(back_populates="shows") attendances: List["Attendance"] = Relationship(back_populates="show") performances: List["Performance"] = Relationship(back_populates="show") class Song(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) title: str = Field(index=True) slug: Optional[str] = Field(default=None, unique=True, index=True) original_artist: Optional[str] = Field(default=None) vertical_id: int = Field(foreign_key="vertical.id") notes: Optional[str] = Field(default=None) youtube_link: Optional[str] = Field(default=None) vertical: Vertical = Relationship(back_populates="songs") class Tag(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(unique=True, index=True) slug: str = Field(unique=True, index=True) class Attendance(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") show_id: int = Field(foreign_key="show.id") notes: Optional[str] = Field(default=None) created_at: datetime = Field(default_factory=datetime.utcnow) user: "User" = Relationship(back_populates="attendances") show: "Show" = Relationship(back_populates="attendances") class Comment(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") content: str created_at: datetime = Field(default_factory=datetime.utcnow) # Polymorphic-ish associations (nullable FKs) show_id: Optional[int] = Field(default=None, foreign_key="show.id") venue_id: Optional[int] = Field(default=None, foreign_key="venue.id") song_id: Optional[int] = Field(default=None, foreign_key="song.id") parent_id: Optional[int] = Field(default=None, foreign_key="comment.id") user: "User" = Relationship(back_populates="comments") class Rating(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") score: float = Field(ge=1.0, le=10.0, description="Rating from 1.0 to 10.0") created_at: datetime = Field(default_factory=datetime.utcnow) show_id: Optional[int] = Field(default=None, foreign_key="show.id") song_id: Optional[int] = Field(default=None, foreign_key="song.id") performance_id: Optional[int] = Field(default=None, foreign_key="performance.id") venue_id: Optional[int] = Field(default=None, foreign_key="venue.id") tour_id: Optional[int] = Field(default=None, foreign_key="tour.id") user: "User" = Relationship(back_populates="ratings") class User(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) email: str = Field(unique=True, index=True) hashed_password: str is_active: bool = Field(default=True) is_superuser: bool = Field(default=False) role: str = Field(default="user") # user, moderator, admin bio: Optional[str] = Field(default=None) avatar: Optional[str] = Field(default=None) avatar_bg_color: Optional[str] = Field(default="#3B82F6", description="Hex color for avatar background") avatar_text: Optional[str] = Field(default=None, description="1-3 character text overlay on avatar") # Privacy settings profile_public: bool = Field(default=True, description="Allow others to view profile") show_attendance_public: bool = Field(default=True, description="Show attended shows on profile") appear_in_leaderboards: bool = Field(default=True, description="Appear in community leaderboards") # Gamification xp: int = Field(default=0, description="Experience points") level: int = Field(default=1, description="User level based on XP") streak_days: int = Field(default=0, description="Consecutive days active") last_activity: Optional[datetime] = Field(default=None) # Custom Titles & Flair (tracker forum style) custom_title: Optional[str] = Field(default=None, description="Custom title chosen by user") title_color: Optional[str] = Field(default=None, description="Hex color for username display") flair: Optional[str] = Field(default=None, description="Small text/emoji beside name") is_early_adopter: bool = Field(default=False, description="First 100 users get special perks") is_supporter: bool = Field(default=False, description="Donated/supported the platform") joined_at: datetime = Field(default_factory=datetime.utcnow) # Email verification email_verified: bool = Field(default=False) verification_token: Optional[str] = Field(default=None) verification_token_expires: Optional[datetime] = Field(default=None) # Password reset reset_token: Optional[str] = Field(default=None) reset_token_expires: Optional[datetime] = Field(default=None) # Multi-identity support: A user can have multiple Profiles profiles: List["Profile"] = Relationship(back_populates="user") comments: List["Comment"] = Relationship(back_populates="user") ratings: List["Rating"] = Relationship(back_populates="user") reviews: List["Review"] = Relationship(back_populates="user") attendances: List["Attendance"] = Relationship(back_populates="user") badges: List["UserBadge"] = Relationship(back_populates="user") preferences: Optional["UserPreferences"] = Relationship(back_populates="user", sa_relationship_kwargs={"uselist": False}) reports: List["Report"] = Relationship(back_populates="user") notifications: List["Notification"] = Relationship(back_populates="user") class Report(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") entity_type: str = Field(index=True) # comment, review, nickname entity_id: int = Field(index=True) reason: str details: str = Field(default="") status: str = Field(default="pending", index=True) # pending, resolved, dismissed created_at: datetime = Field(default_factory=datetime.utcnow) user: "User" = Relationship(back_populates="reports") class Badge(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(unique=True, index=True) description: str icon: str = Field(description="Lucide icon name or image URL") slug: str = Field(unique=True, index=True) tier: str = Field(default="bronze", description="bronze, silver, gold, platinum, diamond") category: str = Field(default="general", description="attendance, ratings, social, milestones") xp_reward: int = Field(default=50, description="XP awarded when badge is earned") class UserBadge(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") badge_id: int = Field(foreign_key="badge.id") awarded_at: datetime = Field(default_factory=datetime.utcnow) user: "User" = Relationship(back_populates="badges") badge: "Badge" = Relationship() class Review(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") blurb: str = Field(description="One-liner/pullquote") content: str = Field(description="Full review text") score: float = Field(ge=1.0, le=10.0) show_id: Optional[int] = Field(default=None, foreign_key="show.id") venue_id: Optional[int] = Field(default=None, foreign_key="venue.id") song_id: Optional[int] = Field(default=None, foreign_key="song.id") performance_id: Optional[int] = Field(default=None, foreign_key="performance.id") tour_id: Optional[int] = Field(default=None, foreign_key="tour.id") year: Optional[int] = Field(default=None, description="For reviewing a specific year") created_at: datetime = Field(default_factory=datetime.utcnow) user: "User" = Relationship(back_populates="reviews") class UserPreferences(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id", unique=True) wiki_mode: bool = Field(default=False, description="Disable social features") show_ratings: bool = Field(default=True) show_comments: bool = Field(default=True) user: User = Relationship(back_populates="preferences") class Profile(SQLModel, table=True): """A user's identity within a specific context or global""" id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") username: str = Field(index=True) display_name: Optional[str] = Field(default=None) user: User = Relationship(back_populates="profiles") # --- Groups --- class Group(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True, unique=True) description: Optional[str] = None privacy: str = Field(default="public") # public, private created_by: int = Field(foreign_key="user.id") created_at: datetime = Field(default_factory=datetime.utcnow) members: List["GroupMember"] = Relationship(back_populates="group") posts: List["GroupPost"] = Relationship(back_populates="group") class GroupMember(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) group_id: int = Field(foreign_key="group.id") user_id: int = Field(foreign_key="user.id") role: str = Field(default="member") # member, admin joined_at: datetime = Field(default_factory=datetime.utcnow) group: Group = Relationship(back_populates="members") user: User = Relationship() class GroupPost(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) group_id: int = Field(foreign_key="group.id") user_id: int = Field(foreign_key="user.id") content: str created_at: datetime = Field(default_factory=datetime.utcnow) group: Group = Relationship(back_populates="posts") user: User = Relationship() class Notification(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id", index=True) type: str = Field(description="reply, mention, system") title: str message: str link: Optional[str] = None is_read: bool = Field(default=False) created_at: datetime = Field(default_factory=datetime.utcnow) user: User = Relationship(back_populates="notifications") class Reaction(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") entity_type: str = Field(index=True) # "review", "comment" entity_id: int = Field(index=True) emoji: str # "❤️", "🔥", etc. created_at: datetime = Field(default_factory=datetime.utcnow) user: User = Relationship() class ChaseSong(SQLModel, table=True): """Songs a user wants to see live (hasn't seen performed yet or wants to see again)""" id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id", index=True) song_id: int = Field(foreign_key="song.id", index=True) priority: int = Field(default=1, description="1=high, 2=medium, 3=low") notes: Optional[str] = Field(default=None) created_at: datetime = Field(default_factory=datetime.utcnow) caught_at: Optional[datetime] = Field(default=None, description="When they finally saw it") caught_show_id: Optional[int] = Field(default=None, foreign_key="show.id") user: User = Relationship() song: "Song" = Relationship()