"""Decks API router for Mantimon TCG. This module provides REST endpoints for deck management including creating, reading, updating, and deleting decks, as well as validation. The backend is stateless - deck rules come from the request via DeckConfig. Frontend provides the rules appropriate for the game mode (campaign, freeplay, custom). Endpoints: GET /decks - List all user's decks POST /decks - Create a new deck GET /decks/{deck_id} - Get a specific deck PUT /decks/{deck_id} - Update a deck DELETE /decks/{deck_id} - Delete a deck POST /decks/validate - Validate a deck without saving """ from uuid import UUID from fastapi import APIRouter, HTTPException, status from app.api.deps import CardServiceDep, CollectionServiceDep, CurrentUser, DeckServiceDep from app.repositories.protocols import UNSET from app.schemas.deck import ( DeckCreateRequest, DeckListResponse, DeckResponse, DeckUpdateRequest, DeckValidateRequest, DeckValidationResponse, ) from app.services.deck_service import DeckLimitExceededError, DeckNotFoundError from app.services.deck_validator import validate_deck router = APIRouter(prefix="/decks", tags=["decks"]) @router.get("", response_model=DeckListResponse) async def list_decks( current_user: CurrentUser, deck_service: DeckServiceDep, ) -> DeckListResponse: """Get all decks for the authenticated user. Returns a list of all user's decks along with count and limit information. Premium users have unlimited decks (deck_limit=None). Returns: DeckListResponse with decks, count, and limit. """ decks = await deck_service.get_user_decks(current_user.id) # Convert DeckEntry DTOs to DeckResponse deck_responses = [ DeckResponse( id=d.id, name=d.name, description=d.description, cards=d.cards, energy_cards=d.energy_cards, is_valid=d.is_valid, validation_errors=d.validation_errors, is_starter=d.is_starter, starter_type=d.starter_type, created_at=d.created_at, updated_at=d.updated_at, ) for d in decks ] # Premium users have unlimited decks deck_limit = None if current_user.has_active_premium else current_user.max_decks return DeckListResponse( decks=deck_responses, deck_count=len(decks), deck_limit=deck_limit, ) @router.post("", response_model=DeckResponse, status_code=status.HTTP_201_CREATED) async def create_deck( deck_in: DeckCreateRequest, current_user: CurrentUser, deck_service: DeckServiceDep, ) -> DeckResponse: """Create a new deck. Validates the deck composition and stores it. Invalid decks CAN be saved (with validation errors) to support work-in-progress deck building. Args: deck_in: Deck creation request with name, cards, energy, and config. Returns: The created deck with validation state. Raises: 400: If user has reached their deck limit. """ try: deck = await deck_service.create_deck( user_id=current_user.id, name=deck_in.name, cards=deck_in.cards, energy_cards=deck_in.energy_cards, deck_config=deck_in.deck_config, max_decks=current_user.max_decks, validate_ownership=deck_in.validate_ownership, description=deck_in.description, ) except DeckLimitExceededError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e), ) from None return DeckResponse( id=deck.id, name=deck.name, description=deck.description, cards=deck.cards, energy_cards=deck.energy_cards, is_valid=deck.is_valid, validation_errors=deck.validation_errors, is_starter=deck.is_starter, starter_type=deck.starter_type, created_at=deck.created_at, updated_at=deck.updated_at, ) @router.get("/{deck_id}", response_model=DeckResponse) async def get_deck( deck_id: UUID, current_user: CurrentUser, deck_service: DeckServiceDep, ) -> DeckResponse: """Get a specific deck by ID. Only returns decks owned by the authenticated user. Args: deck_id: The deck's UUID. Returns: The deck details. Raises: 404: If deck not found or not owned by user. """ try: deck = await deck_service.get_deck(current_user.id, deck_id) except DeckNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found", ) from None return DeckResponse( id=deck.id, name=deck.name, description=deck.description, cards=deck.cards, energy_cards=deck.energy_cards, is_valid=deck.is_valid, validation_errors=deck.validation_errors, is_starter=deck.is_starter, starter_type=deck.starter_type, created_at=deck.created_at, updated_at=deck.updated_at, ) @router.put("/{deck_id}", response_model=DeckResponse) async def update_deck( deck_id: UUID, deck_in: DeckUpdateRequest, current_user: CurrentUser, deck_service: DeckServiceDep, ) -> DeckResponse: """Update an existing deck. Only provided fields are updated. If cards or energy_cards change, the deck is re-validated with the provided deck_config. To clear the description, explicitly send `"description": null` in the request. Omitting the field keeps the existing description. Args: deck_id: The deck's UUID. deck_in: Update request with optional fields. Returns: The updated deck. Raises: 404: If deck not found or not owned by user. """ # Check if description was explicitly provided (even if null) # UNSET means "keep existing", whereas None means "clear" description = deck_in.description if "description" in deck_in.model_fields_set else UNSET try: deck = await deck_service.update_deck( user_id=current_user.id, deck_id=deck_id, deck_config=deck_in.deck_config, name=deck_in.name, cards=deck_in.cards, energy_cards=deck_in.energy_cards, validate_ownership=deck_in.validate_ownership, description=description, ) except DeckNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found", ) from None return DeckResponse( id=deck.id, name=deck.name, description=deck.description, cards=deck.cards, energy_cards=deck.energy_cards, is_valid=deck.is_valid, validation_errors=deck.validation_errors, is_starter=deck.is_starter, starter_type=deck.starter_type, created_at=deck.created_at, updated_at=deck.updated_at, ) @router.delete("/{deck_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_deck( deck_id: UUID, current_user: CurrentUser, deck_service: DeckServiceDep, ) -> None: """Delete a deck. Only deletes decks owned by the authenticated user. Args: deck_id: The deck's UUID. Raises: 404: If deck not found or not owned by user. """ try: await deck_service.delete_deck(current_user.id, deck_id) except DeckNotFoundError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Deck not found", ) from None @router.post("/validate", response_model=DeckValidationResponse) async def validate_deck_endpoint( request: DeckValidateRequest, current_user: CurrentUser, card_service: CardServiceDep, collection_service: CollectionServiceDep, ) -> DeckValidationResponse: """Validate a deck composition without saving. Useful for checking if a deck is valid before creating it. Uses the provided deck_config rules from the frontend. Args: request: Validation request with cards, energy, and config. Returns: Validation result with is_valid and any errors. """ # Get owned cards if validating ownership (campaign mode) owned_cards: dict[str, int] | None = None if request.validate_ownership: owned_cards = await collection_service.get_owned_cards_dict(current_user.id) # Use the pure validation function with config from request result = validate_deck( cards=request.cards, energy_cards=request.energy_cards, deck_config=request.deck_config, card_lookup=card_service.get_card, owned_cards=owned_cards, ) return DeckValidationResponse( is_valid=result.is_valid, errors=result.errors, )