""" Card Mirror Service Manages mirrored card data in mtgo_platform for fast deckbuilding queries. Syncs with mtg_cards (mtg_data) when the card database is refreshed. """ from typing import List, Dict, Any, Optional, Tuple from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, update, delete from sqlalchemy.orm import selectinload from app.models.mtg_models import MtgCard, MtgSet from app.models.mirror_models import MtgCardMirror, DeckCardLink from app.core.database import mirror_get_db async def upsert_card_mirror( db: AsyncSession, card_data: Dict[str, Any], source_id: Optional[int] = None ) -> MtgCardMirror: """ Upsert a card into the mirror table. Args: db: Database session card_data: Card data dictionary source_id: Optional reference to mtg_cards.id Returns: The upserted MtgCardMirror instance """ # Check if card already exists by name existing = await db.execute( select(MtgCardMirror).where(MtgCardMirror.name == card_data.get("name")) ) existing_card = existing.scalar_one_or_none() if existing_card: # Update existing mirror for key, value in card_data.items(): if hasattr(existing_card, key): setattr(existing_card, key, value) if source_id: existing_card.source_id = source_id else: # Create new mirror mirror_data = { "name": card_data.get("name"), "mana_cost": card_data.get("mana_cost"), "type_line": card_data.get("type_line"), "oracle_text": card_data.get("oracle_text"), "power": card_data.get("power"), "toughness": card_data.get("toughness"), "rarity": card_data.get("rarity"), "layout": card_data.get("layout"), "artist": card_data.get("artist"), "flavor_text": card_data.get("flavor_text"), "numbers": card_data.get("numbers"), "identifiers": card_data.get("identifiers"), "images": card_data.get("images"), "image": card_data.get("image"), "card_parts": card_data.get("card_parts"), "keywords": card_data.get("keywords"), "legalities": card_data.get("legalities"), "set_code": card_data.get("set_code"), "set_name": card_data.get("set_name"), "source_id": source_id, } mirror_card = MtgCardMirror(**mirror_data) db.add(mirror_card) await db.flush() return mirror_card return existing_card async def get_card_mirror_by_name( db: AsyncSession, name: str ) -> Optional[MtgCardMirror]: """ Get a mirrored card by name. Args: db: Database session name: Card name Returns: MtgCardMirror instance or None """ result = await db.execute( select(MtgCardMirror).where(MtgCardMirror.name == name) ) return result.scalar_one_or_none() async def search_card_mirrors( db: AsyncSession, query: str, limit: int = 100, offset: int = 0 ) -> Tuple[List[MtgCardMirror], int]: """ Search mirrored cards by name. Args: db: Database session query: Search query limit: Maximum results offset: Pagination offset Returns: Tuple of (list of mirrors, total count) """ search_term = f"%{query.lower()}%" # Search query stmt = ( select(MtgCardMirror) .where(MtgCardMirror.name.ilike(search_term)) .offset(offset) .limit(limit) ) result = await db.execute(stmt) mirrors = result.scalars().all() # Total count count_stmt = select(func.count()).select_from(MtgCardMirror).where( MtgCardMirror.name.ilike(search_term) ) count_result = await db.execute(count_stmt) total = count_result.scalar() return mirrors, total async def add_card_to_deck( db: AsyncSession, deck_id: int, card_id: int, quantity: int = 1, zone: str = "main" ) -> DeckCardLink: """ Add a card to a deck. Args: db: Database session deck_id: Deck ID card_id: Mirrored card ID quantity: Number of copies zone: 'main' or 'sideboard' Returns: DeckCardLink instance """ # Check if link already exists existing = await db.execute( select(DeckCardLink).where( DeckCardLink.deck_id == deck_id, DeckCardLink.card_id == card_id, DeckCardLink.zone == zone ) ) existing_link = existing.scalar_one_or_none() if existing_link: # Update quantity existing_link.quantity = quantity await db.flush() return existing_link else: # Create new link link = DeckCardLink( deck_id=deck_id, card_id=card_id, quantity=quantity, zone=zone ) db.add(link) await db.flush() return link async def remove_card_from_deck( db: AsyncSession, deck_id: int, card_id: int, zone: str = "main" ) -> bool: """ Remove a card from a deck. Args: db: Database session deck_id: Deck ID card_id: Mirrored card ID zone: 'main' or 'sideboard' Returns: True if removed, False if not found """ result = await db.execute( delete(DeckCardLink).where( DeckCardLink.deck_id == deck_id, DeckCardLink.card_id == card_id, DeckCardLink.zone == zone ) ) return result.rowcount > 0 async def get_deck_cards( db: AsyncSession, deck_id: int ) -> List[DeckCardLink]: """ Get all cards in a deck with their mirrored data. Args: db: Database session deck_id: Deck ID Returns: List of DeckCardLink instances with joined card data """ stmt = ( select(DeckCardLink, MtgCardMirror) .join(MtgCardMirror, DeckCardLink.card_id == MtgCardMirror.id) .where(DeckCardLink.deck_id == deck_id) .order_by(DeckCardLink.id) ) result = await db.execute(stmt) rows = result.all() links = [] for link, card in rows: link.card = card links.append(link) return links async def get_user_deck_summaries( db: AsyncSession, user_id: int ) -> List[Dict[str, Any]]: """ Get all decks for a user with card counts. Args: db: Database session user_id: User ID Returns: List of deck summaries with card counts """ from app.models.models import DecklistFile stmt = ( select(DecklistFile) .where(DecklistFile.owner_id == user_id) .order_by(DecklistFile.creation_date.desc()) ) result = await db.execute(stmt) decks = result.scalars().all() summaries = [] for deck in decks: # Get card count count_stmt = ( select(func.count()) .select_from(DeckCardLink) .where(DeckCardLink.deck_id == deck.id) ) count_result = await db.execute(count_stmt) card_count = count_result.scalar() summaries.append({ "id": deck.id, "name": deck.name, "format": deck.format, "status": deck.status, "folder_id": deck.folder_id, "owner_id": deck.owner_id, "creation_date": deck.creation_date, "card_count": card_count, }) return summaries async def sync_mirrors_from_mtg_cards( db: AsyncSession, mtg_db: Optional[AsyncSession] = None ) -> int: """ Sync all mirrored cards from the mtg_cards table. This is called when the card database is refreshed. Args: db: Mirror database session mtg_db: Optional MTG database session for cross-DB queries Returns: Number of cards synced """ if not mtg_db: # Use the same session if no MTG session provided # Note: In production, you'd need a cross-DB connection # For now, we'll just refresh the mirror from existing data pass # For now, this is a no-op. In a full implementation, # you'd query mtg_cards and upsert into mtg_cards_mirror. # This requires cross-database connections which SQLAlchemy # can handle with proper configuration. return 0 async def get_card_statistics(db: AsyncSession) -> Dict[str, Any]: """ Get statistics about mirrored cards. Args: db: Database session Returns: Dictionary with statistics """ # Total mirrored cards total_stmt = select(func.count()).select_from(MtgCardMirror) total = (await db.execute(total_stmt)).scalar() # Cards by rarity rarity_stmt = select(MtgCardMirror.rarity, func.count()).group_by( MtgCardMirror.rarity ) rarity_result = await db.execute(rarity_stmt) rarities = {row[0]: row[1] for row in rarity_result if row[0]} # Cards by type type_stmt = select(MtgCardMirror.type_line, func.count()).group_by( MtgCardMirror.type_line ) type_result = await db.execute(type_stmt) types = {row[0]: row[1] for row in type_result if row[0]} return { "total_mirrored_cards": total, "cards_by_rarity": rarities, "cards_by_type": types, }