import asyncio import uuid from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import asc, case, desc, func from sqlalchemy.orm import Session from game.card import _get_specific_card_async from core.database import get_db from services.database_functions import check_boosters, fill_card_pool, BOOSTER_MAX from core.dependencies import get_current_user, limiter from core.models import Card as CardModel from core.models import Deck as DeckModel from core.models import DeckCard as DeckCardModel from core.models import User as UserModel router = APIRouter() @router.get("/boosters") def get_boosters(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): count, countdown = check_boosters(user, db) return {"count": count, "countdown": countdown, "email_verified": user.email_verified} @router.get("/cards") def get_cards( skip: int = 0, limit: int = 40, search: str = "", rarities: list[str] = Query(default=[]), types: list[str] = Query(default=[]), cost_min: int = 1, cost_max: int = 10, favorites_only: bool = False, wtt_only: bool = False, sort_by: str = "name", sort_dir: str = "asc", user: UserModel = Depends(get_current_user), db: Session = Depends(get_db), ): q = db.query(CardModel).filter(CardModel.user_id == user.id) if search: q = q.filter(CardModel.name.ilike(f"%{search}%")) if rarities: q = q.filter(CardModel.card_rarity.in_(rarities)) if types: q = q.filter(CardModel.card_type.in_(types)) q = q.filter(CardModel.cost >= cost_min, CardModel.cost <= cost_max) if favorites_only: q = q.filter(CardModel.is_favorite == True) if wtt_only: q = q.filter(CardModel.willing_to_trade == True) total = q.count() # case() for rarity ordering matches frontend RARITY_ORDER constant rarity_order_expr = case( (CardModel.card_rarity == 'common', 0), (CardModel.card_rarity == 'uncommon', 1), (CardModel.card_rarity == 'rare', 2), (CardModel.card_rarity == 'super_rare', 3), (CardModel.card_rarity == 'epic', 4), (CardModel.card_rarity == 'legendary', 5), else_=0 ) # coalesce mirrors frontend: received_at ?? generated_at date_received_expr = func.coalesce(CardModel.received_at, CardModel.generated_at) sort_map = { "name": CardModel.name, "cost": CardModel.cost, "attack": CardModel.attack, "defense": CardModel.defense, "rarity": rarity_order_expr, "date_generated": CardModel.generated_at, "date_received": date_received_expr, } sort_col = sort_map.get(sort_by, CardModel.name) order_fn = desc if sort_dir == "desc" else asc # Secondary sort by name keeps pages stable when primary values are tied q = q.order_by(order_fn(sort_col), asc(CardModel.name)) cards = q.offset(skip).limit(limit).all() return { "cards": [ {c.name: getattr(card, c.name) for c in card.__table__.columns} for card in cards ], "total": total, } @router.get("/cards/in-decks") def get_cards_in_decks(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): deck_ids = [d.id for d in db.query(DeckModel).filter(DeckModel.user_id == user.id, DeckModel.deleted == False).all()] if not deck_ids: return [] card_ids = db.query(DeckCardModel.card_id).filter(DeckCardModel.deck_id.in_(deck_ids)).distinct().all() return [str(row.card_id) for row in card_ids] @router.post("/open_pack") @limiter.limit("10/minute") async def open_pack(request: Request, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): if not user.email_verified: raise HTTPException(status_code=403, detail="You must verify your email before opening packs") check_boosters(user, db) if user.boosters == 0: raise HTTPException(status_code=400, detail="No booster packs available") cards = ( db.query(CardModel) .filter(CardModel.user_id == None, CardModel.ai_used == False) .limit(5) .all() ) if len(cards) < 5: asyncio.create_task(fill_card_pool()) raise HTTPException(status_code=503, detail="Card pool is low, please try again shortly") now = datetime.now() for card in cards: card.user_id = user.id card.received_at = now was_full = user.boosters == BOOSTER_MAX user.boosters -= 1 if was_full: user.boosters_countdown = datetime.now() db.commit() asyncio.create_task(fill_card_pool()) return [ {**{c.name: getattr(card, c.name) for c in card.__table__.columns}, "card_rarity": card.card_rarity, "card_type": card.card_type} for card in cards ] @router.post("/cards/{card_id}/report") def report_card(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): card = db.query(CardModel).filter( CardModel.id == uuid.UUID(card_id), CardModel.user_id == user.id ).first() if not card: raise HTTPException(status_code=404, detail="Card not found") card.reported = True db.commit() return {"message": "Card reported"} @router.post("/cards/{card_id}/refresh") @limiter.limit("5/hour") async def refresh_card(request: Request, card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): card = db.query(CardModel).filter( CardModel.id == uuid.UUID(card_id), CardModel.user_id == user.id ).first() if not card: raise HTTPException(status_code=404, detail="Card not found") if user.last_refresh_at and datetime.now() - user.last_refresh_at < timedelta(minutes=10): remaining = (user.last_refresh_at + timedelta(minutes=10)) - datetime.now() minutes = int(remaining.total_seconds() // 60) seconds = int(remaining.total_seconds() % 60) raise HTTPException( status_code=429, detail=f"You can refresh again in {minutes}m {seconds}s" ) new_card = await _get_specific_card_async(card.name) if not new_card: raise HTTPException(status_code=502, detail="Failed to regenerate card from Wikipedia") card.image_link = new_card.image_link card.card_rarity = new_card.card_rarity.name card.card_type = new_card.card_type.name card.text = new_card.text card.attack = new_card.attack card.defense = new_card.defense card.cost = new_card.cost card.reported = False card.generated_at = datetime.now() card.received_at = datetime.now() user.last_refresh_at = datetime.now() db.commit() return { **{c.name: getattr(card, c.name) for c in card.__table__.columns}, "card_rarity": card.card_rarity, "card_type": card.card_type, } @router.post("/cards/{card_id}/favorite") def toggle_favorite(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): card = db.query(CardModel).filter( CardModel.id == uuid.UUID(card_id), CardModel.user_id == user.id ).first() if not card: raise HTTPException(status_code=404, detail="Card not found") card.is_favorite = not card.is_favorite db.commit() return {"is_favorite": card.is_favorite} @router.post("/cards/{card_id}/willing-to-trade") def toggle_willing_to_trade(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)): card = db.query(CardModel).filter( CardModel.id == uuid.UUID(card_id), CardModel.user_id == user.id ).first() if not card: raise HTTPException(status_code=404, detail="Card not found") card.willing_to_trade = not card.willing_to_trade db.commit() return {"willing_to_trade": card.willing_to_trade}