"""User service for Mantimon TCG. This module provides async CRUD operations for user accounts, including OAuth-based user creation and premium status management. All database operations use async SQLAlchemy sessions. Example: from app.services.user_service import user_service # Get user by ID user = await user_service.get_by_id(db, user_id) # Create from OAuth user = await user_service.create_from_oauth(db, oauth_info) # Update premium status user = await user_service.update_premium(db, user_id, premium_until) """ from datetime import UTC, datetime from uuid import UUID from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models.user import User from app.schemas.user import OAuthUserInfo, UserCreate, UserUpdate class UserService: """Service for user account operations. Provides async methods for user CRUD, OAuth-based creation, and premium subscription management. """ async def get_by_id(self, db: AsyncSession, user_id: UUID) -> User | None: """Get a user by their ID. Args: db: Async database session. user_id: The user's UUID. Returns: User if found, None otherwise. Example: user = await user_service.get_by_id(db, user_id) if user: print(f"Found user: {user.display_name}") """ result = await db.execute(select(User).where(User.id == user_id)) return result.scalar_one_or_none() async def get_by_email(self, db: AsyncSession, email: str) -> User | None: """Get a user by their email address. Args: db: Async database session. email: The user's email address. Returns: User if found, None otherwise. Example: user = await user_service.get_by_email(db, "player@example.com") """ result = await db.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() async def get_by_oauth( self, db: AsyncSession, provider: str, oauth_id: str, ) -> User | None: """Get a user by their OAuth provider and ID. Args: db: Async database session. provider: OAuth provider name (google, discord). oauth_id: Unique ID from the OAuth provider. Returns: User if found, None otherwise. Example: user = await user_service.get_by_oauth(db, "google", "123456789") """ result = await db.execute( select(User).where( User.oauth_provider == provider, User.oauth_id == oauth_id, ) ) return result.scalar_one_or_none() async def create(self, db: AsyncSession, user_data: UserCreate) -> User: """Create a new user. Args: db: Async database session. user_data: User creation data. Returns: The created User instance. Example: user_data = UserCreate( email="player@example.com", display_name="Player1", oauth_provider="google", oauth_id="123456789" ) user = await user_service.create(db, user_data) """ user = User( email=user_data.email, display_name=user_data.display_name, avatar_url=user_data.avatar_url, oauth_provider=user_data.oauth_provider, oauth_id=user_data.oauth_id, ) db.add(user) await db.commit() await db.refresh(user) return user async def create_from_oauth( self, db: AsyncSession, oauth_info: OAuthUserInfo, ) -> User: """Create a new user from OAuth provider info. Convenience method that converts OAuthUserInfo to UserCreate. Args: db: Async database session. oauth_info: Normalized OAuth user information. Returns: The created User instance. Example: oauth_info = OAuthUserInfo( provider="google", oauth_id="123456789", email="player@example.com", name="Player One", avatar_url="https://..." ) user = await user_service.create_from_oauth(db, oauth_info) """ user_data = oauth_info.to_user_create() return await self.create(db, user_data) async def get_or_create_from_oauth( self, db: AsyncSession, oauth_info: OAuthUserInfo, ) -> tuple[User, bool]: """Get existing user or create new one from OAuth info. First checks for existing user by OAuth provider+ID, then by email (for account linking), and finally creates a new user if not found. Args: db: Async database session. oauth_info: Normalized OAuth user information. Returns: Tuple of (User, created) where created is True if new user. Example: user, created = await user_service.get_or_create_from_oauth(db, oauth_info) if created: print("Welcome, new user!") else: print("Welcome back!") """ # First, check by OAuth provider + ID (exact match) user = await self.get_by_oauth(db, oauth_info.provider, oauth_info.oauth_id) if user: return user, False # Check by email for potential account linking # If user exists with same email but different OAuth, update their OAuth user = await self.get_by_email(db, oauth_info.email) if user: # Update OAuth credentials for existing user # This links the new OAuth provider to the existing account user.oauth_provider = oauth_info.provider user.oauth_id = oauth_info.oauth_id # Optionally update avatar if not set if not user.avatar_url and oauth_info.avatar_url: user.avatar_url = oauth_info.avatar_url await db.commit() await db.refresh(user) return user, False # Create new user user = await self.create_from_oauth(db, oauth_info) return user, True async def update( self, db: AsyncSession, user: User, update_data: UserUpdate, ) -> User: """Update user profile fields. Only updates fields that are provided (not None). Args: db: Async database session. user: The user to update. update_data: Fields to update. Returns: The updated User instance. Example: update_data = UserUpdate(display_name="New Name") user = await user_service.update(db, user, update_data) """ if update_data.display_name is not None: user.display_name = update_data.display_name if update_data.avatar_url is not None: user.avatar_url = update_data.avatar_url await db.commit() await db.refresh(user) return user async def update_last_login(self, db: AsyncSession, user: User) -> User: """Update the user's last login timestamp. Args: db: Async database session. user: The user to update. Returns: The updated User instance. Example: user = await user_service.update_last_login(db, user) """ user.last_login = datetime.now(UTC) await db.commit() await db.refresh(user) return user async def update_premium( self, db: AsyncSession, user: User, premium_until: datetime | None, ) -> User: """Update user's premium subscription status. Args: db: Async database session. user: The user to update. premium_until: When premium expires, or None to remove premium. Returns: The updated User instance. Example: # Grant 30 days of premium expires = datetime.now(UTC) + timedelta(days=30) user = await user_service.update_premium(db, user, expires) # Remove premium user = await user_service.update_premium(db, user, None) """ if premium_until is not None: user.is_premium = True user.premium_until = premium_until else: user.is_premium = False user.premium_until = None await db.commit() await db.refresh(user) return user async def delete(self, db: AsyncSession, user: User) -> None: """Delete a user account. This will cascade delete all related data (decks, collection, etc.) based on the model relationships. Args: db: Async database session. user: The user to delete. Example: await user_service.delete(db, user) """ await db.delete(user) await db.commit() # Global service instance user_service = UserService()