""" Deck service: orchestrates prompt building, Claude call, and DB persistence. """ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.collection import CollectionCard from app.models.deck import Deck, DeckCard, DeckMode from app.schemas.deck import DeckConstraints, GenerateRequest, CompleteRequest, CullRequest from app.services.ai import prompts from app.services.ai.claude_client import call_claude, DeckPayload from app.services.ai.constraints import build_constraint_context, build_owned_card_list # A full 99-card deck with reasoning needs ~16000 tokens GENERATE_MAX_TOKENS = 16000 COMPLETE_MAX_TOKENS = 16000 CULL_MAX_TOKENS = 16000 async def generate_deck(req: GenerateRequest, user_id: int, db: AsyncSession) -> Deck: owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else [] system, user_msg = prompts.generate_prompt( commander=req.commander, playstyle=req.playstyle, constraint_text=build_constraint_context(req.constraints, owned_names or None), owned_list_text=build_owned_card_list(owned_names), ) payload = await call_claude(system, user_msg, max_tokens=GENERATE_MAX_TOKENS) return await _persist_deck( payload=payload, user_id=user_id, mode=DeckMode.GENERATE, commander=req.commander, name=req.name or payload.deck_name, description=req.description or payload.strategy_summary, playstyle=req.playstyle, constraints=req.constraints, owned_name_set=set(n.lower() for n in owned_names), db=db, ) async def complete_deck(req: CompleteRequest, user_id: int, db: AsyncSession) -> Deck: owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else [] system, user_msg = prompts.complete_prompt( commander=req.commander, playstyle=req.playstyle, existing_cards=req.existing_cards, constraint_text=build_constraint_context(req.constraints, owned_names or None), owned_list_text=build_owned_card_list(owned_names), ) payload = await call_claude(system, user_msg, max_tokens=COMPLETE_MAX_TOKENS) return await _persist_deck( payload=payload, user_id=user_id, mode=DeckMode.COMPLETE, commander=req.commander, name=req.name or payload.deck_name, description=payload.strategy_summary, playstyle=req.playstyle, constraints=req.constraints, owned_name_set=set(n.lower() for n in owned_names), db=db, ) async def cull_deck(req: CullRequest, user_id: int, db: AsyncSession) -> Deck: owned_names = await _owned_names(user_id, db) if req.constraints.prefer_owned else [] owned_set = set(n.lower() for n in owned_names) for card in req.existing_cards: card["is_owned"] = card.get("card_name", "").lower() in owned_set system, user_msg = prompts.cull_prompt( commander=req.commander, existing_cards=req.existing_cards, target_count=req.target_count, constraint_text=build_constraint_context(req.constraints, owned_names or None), owned_list_text=build_owned_card_list(owned_names), prefer_owned=req.constraints.prefer_owned, ) payload = await call_claude(system, user_msg, max_tokens=CULL_MAX_TOKENS) return await _persist_deck( payload=payload, user_id=user_id, mode=DeckMode.CULL, commander=req.commander, name=req.name or payload.deck_name, description=payload.strategy_summary, playstyle=None, constraints=req.constraints, owned_name_set=owned_set, db=db, ) async def _persist_deck( payload: DeckPayload, user_id: int, mode: DeckMode, commander: str, name: str, description: str | None, playstyle: str | None, constraints: DeckConstraints, owned_name_set: set[str], db: AsyncSession, ) -> Deck: deck = Deck( owner_id=user_id, name=name, commander=commander, description=description, mode=mode, playstyle=playstyle, prefer_owned=constraints.prefer_owned, budget_enabled=constraints.budget_enabled, budget_amount=constraints.budget_amount, budget_scope=constraints.budget_scope, ai_reasoning={ "strategy_summary": payload.strategy_summary, "unresolved_cards": payload.unresolved, "cuts": [{"name": c.card_name, "reasoning": c.reasoning} for c in payload.cuts], }, ) db.add(deck) await db.flush() deck_cards = [ DeckCard( deck_id=deck.id, scryfall_id="", card_name=commander, slot="creature", quantity=1, is_owned=commander.lower() in owned_name_set, is_commander=True, ) ] for entry in payload.cards: if not entry.scryfall_id: continue is_owned = ( entry.card_name.lower() in owned_name_set if constraints.prefer_owned else entry.is_owned ) deck_cards.append(DeckCard( deck_id=deck.id, scryfall_id=entry.scryfall_id, card_name=entry.card_name, slot=entry.slot, quantity=entry.quantity, is_owned=is_owned, is_commander=False, ai_reasoning=entry.reasoning, scryfall_data=entry.scryfall_data, )) db.add_all(deck_cards) await db.commit() await db.refresh(deck) return deck async def _owned_names(user_id: int, db: AsyncSession) -> list[str]: result = await db.execute( select(CollectionCard.card_name).where(CollectionCard.owner_id == user_id) ) return [row[0] for row in result.all()]