"""Deck manager service.""" from typing import List, Dict, Any, Optional from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update, delete, func, text from sqlalchemy.orm import selectinload from app.models.models import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, MtgonlineCard class DeckManager: """Deck manager service.""" @staticmethod async def create_deck( db: AsyncSession, user_id: int, name: str, folder_id: Optional[int] = None, format: str = "standard", notes: Optional[str] = None, is_precedent: bool = False, precedent_name: Optional[str] = None, ) -> UserDeck: """Create a new deck using ORM approach. Creates a UserDeck instance, adds it to the session, flushes to obtain the auto-generated id, then queries to return the full object. """ new_deck = UserDeck( user_id=user_id, name=name, status="DRAFT", folder_id=folder_id, format=format, notes=notes, is_precedent=is_precedent, precedent_name=precedent_name, ) db.add(new_deck) await db.flush() # Query to get the fully loaded object with its auto-generated id # (SQLite doesn't support RETURNING, so new_deck.id may be None after flush) stmt = select(UserDeck).where( UserDeck.user_id == user_id, UserDeck.name == name, ) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def get_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]: """Get a deck by ID.""" stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id) result = await db.execute(stmt) return result.scalar_one_or_none() @staticmethod async def list_decks( db: AsyncSession, user_id: int, status_filter: Optional[str] = None, folder_id: Optional[int] = None, is_precedent: Optional[bool] = None, page: int = 1, page_size: int = 50, ) -> List[UserDeck]: """List user's decks with filtering.""" conditions = [UserDeck.user_id == user_id] if status_filter: conditions.append(UserDeck.status == status_filter) if folder_id: conditions.append(UserDeck.folder_id == folder_id) if is_precedent is not None: conditions.append(UserDeck.is_precedent == is_precedent) offset = (page - 1) * page_size stmt = select(UserDeck).where(*conditions).order_by(UserDeck.updated_at.desc()).offset(offset).limit(page_size) result = await db.execute(stmt) return result.scalars().all() @staticmethod async def update_deck( db: AsyncSession, deck_id: int, user_id: int, name: Optional[str] = None, folder_id: Optional[int] = None, format: Optional[str] = None, notes: Optional[str] = None, ) -> Optional[UserDeck]: """Update a deck.""" deck = await DeckManager.get_deck(db, deck_id, user_id) if not deck: return None if deck.status == "FINAL": raise ValueError("Cannot modify a finalized deck") if name: deck.name = name if folder_id is not None: deck.folder_id = folder_id if format: deck.format = format if notes is not None: deck.notes = notes await db.flush() return deck @staticmethod async def delete_deck(db: AsyncSession, deck_id: int, user_id: int) -> bool: """Delete a deck.""" deck = await DeckManager.get_deck(db, deck_id, user_id) if not deck: return False await db.execute(delete(UserDeck).where(UserDeck.id == deck_id)) await db.flush() return True @staticmethod async def finalize_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]: """Transition a deck from DRAFT to FINAL status.""" deck = await DeckManager.get_deck(db, deck_id, user_id) if not deck: return None if deck.status == "FINAL": raise ValueError("Deck is already finalized") # Check deck has cards card_count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id) card_count_result = await db.execute(card_count_stmt) card_count = card_count_result.scalar() or 0 if card_count == 0: raise ValueError("Cannot finalize an empty deck") deck.status = "FINAL" await db.flush() return deck @staticmethod async def add_card_to_deck( db: AsyncSession, deck_id: int, card_id: int, quantity: int = 1, zone: str = "main", position: Optional[int] = None, ) -> UserDeckCard: """Add a card to a deck.""" deck_card = UserDeckCard( deck_id=deck_id, card_id=card_id, quantity=quantity, zone=zone, position=position, ) db.add(deck_card) await db.flush() return deck_card @staticmethod async def get_deck_cards(db: AsyncSession, deck_id: int, zone: Optional[str] = None) -> List[UserDeckCard]: """Get cards in a deck.""" conditions = [UserDeckCard.deck_id == deck_id] if zone: conditions.append(UserDeckCard.zone == zone) stmt = select(UserDeckCard).where(*conditions).order_by(UserDeckCard.id) result = await db.execute(stmt) return result.scalars().all() @staticmethod async def update_deck_card( db: AsyncSession, deck_card_id: int, quantity: Optional[int] = None, zone: Optional[str] = None, position: Optional[int] = None, ) -> Optional[UserDeckCard]: """Update a card in a deck.""" stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id) result = await db.execute(stmt) deck_card = result.scalar_one_or_none() if not deck_card: return None if quantity is not None: deck_card.quantity = quantity if zone: deck_card.zone = zone if position is not None: deck_card.position = position await db.flush() return deck_card @staticmethod async def remove_card_from_deck(db: AsyncSession, deck_card_id: int) -> bool: """Remove a card from a deck.""" stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id) result = await db.execute(stmt) deck_card = result.scalar_one_or_none() if not deck_card: return False await db.execute(delete(UserDeckCard).where(UserDeckCard.id == deck_card_id)) await db.flush() return True @staticmethod async def clone_precedent( db: AsyncSession, precedent_id: int, user_id: int, name: Optional[str] = None, ) -> UserDeck: """Clone a precedent into a new deck. Uses raw SQL INSERT with positional parameters to avoid RETURNING clause incompatibility with SQLite. """ # Get precedent stmt = select(DeckPrecedent).where(DeckPrecedent.id == precedent_id) result = await db.execute(stmt) precedent = result.scalar_one_or_none() if not precedent: raise ValueError(f"Precedent {precedent_id} not found") # Create new deck using raw SQL INSERT with positional parameters # Note: We don't call db.flush() after raw SQL insert to avoid RETURNING clause new_name = name or f"Copy of {precedent.name}" insert_sql = text( "INSERT INTO user_decks (user_id, name, format, is_precedent) " "VALUES (?, ?, ?, ?)" ) await db.execute(insert_sql, (user_id, new_name, precedent.format, False)) await db.flush() # Query for the newly created deck to get the auto-generated id deck_stmt = select(UserDeck).where( UserDeck.user_id == user_id, UserDeck.name == new_name, ) result = await db.execute(deck_stmt) new_deck = result.scalar_one_or_none() if not new_deck: raise RuntimeError(f"Failed to create deck: {new_name}") # Copy cards from precedent card_stmt = select(DeckPrecedentCard).where(DeckPrecedentCard.precedent_id == precedent_id) card_result = await db.execute(card_stmt) precedent_cards = card_result.scalars().all() for pc in precedent_cards: new_dc = UserDeckCard( deck_id=new_deck.id, card_id=pc.card_id, quantity=pc.quantity, zone=pc.zone, ) db.add(new_dc) await db.flush() return new_deck