import uuid from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from ..models.account import Account from ..models.learnable_language import LearnableLanguage from ...auth import hash_password from ...outbound.postgres.entities.user_entity import User as UserEntity from ...outbound.postgres.repositories import learnable_language_repository, user_repository class AccountService: """Handles account-level operations: registration, profile retrieval, and managing the set of languages a user is learning. All methods operate on behalf of a single authenticated user (or, for ``create_account``, the user being created). Usage:: service = AccountService(db) # Registration account = await service.create_account("alice@example.com", "s3cr3t") # Profile retrieval account = await service.get_account(user_id) print(account.learnable_languages) # [LearnableLanguage(...), ...] # Add French (B1) to the account lang = await service.add_learnable_language( user_id, source_language="en", target_language="fr", proficiencies=["B1"] ) # Remove it again await service.remove_learnable_language(user_id, lang.id) """ def __init__(self, db: AsyncSession) -> None: self.db = db async def create_account(self, email: str, password: str) -> Account: """Create a new user account, hashing the plain-text password before storage. Raises ``ValueError`` if the email address is already registered, so the caller does not need to catch SQLAlchemy exceptions directly. Usage:: try: account = await service.create_account("alice@example.com", "s3cr3t") except ValueError: # email already taken ... """ try: user = await user_repository.create( self.db, email=email, hashed_password=hash_password(password), ) except IntegrityError: await self.db.rollback() raise ValueError("Email already registered") return Account( id=str(user.id), email=user.email, is_active=user.is_active, is_email_verified=user.is_email_verified, created_at=user.created_at, ) async def get_account(self, user_id: uuid.UUID) -> Account: """Retrieve a user's account profile including all their learnable languages. Raises ``ValueError`` if no user exists for the given ``user_id``. Usage:: account = await service.get_account(user_id) for lang in account.learnable_languages: print(lang.target_language, lang.proficiencies) """ # user_repository only exposes get_by_email; query by id directly result = await self.db.execute( select(UserEntity).where(UserEntity.id == user_id) ) user = result.scalar_one_or_none() if user is None: raise ValueError(f"User {user_id} not found") languages = await learnable_language_repository.list_for_user(self.db, user_id) return Account( id=str(user.id), email=user.email, is_active=user.is_active, is_email_verified=user.is_email_verified, created_at=user.created_at, learnable_languages=languages, ) async def add_learnable_language( self, user_id: uuid.UUID, source_language: str, target_language: str, proficiencies: list[str], ) -> LearnableLanguage: """Add a language pair to the user's account, or update proficiency levels if the pair already exists (upsert semantics). Usage:: lang = await service.add_learnable_language( user_id, source_language="en", target_language="fr", proficiencies=["B1", "B2"], ) print(lang.id) # UUID of the learnable_language row """ return await learnable_language_repository.upsert( self.db, user_id=user_id, source_language=source_language, target_language=target_language, proficiencies=proficiencies, ) async def remove_learnable_language( self, user_id: uuid.UUID, language_id: uuid.UUID ) -> None: """Remove a learnable language from the user's account by its row ID. Raises ``ValueError`` if the language entry does not exist or does not belong to ``user_id``. Usage:: await service.remove_learnable_language(user_id, lang.id) """ deleted = await learnable_language_repository.delete( self.db, user_id=user_id, language_id=language_id ) if not deleted: raise ValueError(f"Learnable language {language_id} not found for this user")