from fastapi import APIRouter, HTTPException, status, UploadFile, File from sqlalchemy import select, func from sqlalchemy.orm import selectinload import uuid from pathlib import Path from app.api.deps import DbSession, CurrentUser from app.core.config import settings from app.models import Marathon, MarathonStatus, Game, Challenge, Participant from app.schemas import GameCreate, GameUpdate, GameResponse, MessageResponse, UserPublic router = APIRouter(tags=["games"]) async def get_game_or_404(db, game_id: int) -> Game: result = await db.execute( select(Game) .options(selectinload(Game.added_by_user)) .where(Game.id == game_id) ) game = result.scalar_one_or_none() if not game: raise HTTPException(status_code=404, detail="Game not found") return game async def check_participant(db, user_id: int, marathon_id: int) -> Participant: result = await db.execute( select(Participant).where( Participant.user_id == user_id, Participant.marathon_id == marathon_id, ) ) participant = result.scalar_one_or_none() if not participant: raise HTTPException(status_code=403, detail="You are not a participant of this marathon") return participant @router.get("/marathons/{marathon_id}/games", response_model=list[GameResponse]) async def list_games(marathon_id: int, current_user: CurrentUser, db: DbSession): await check_participant(db, current_user.id, marathon_id) result = await db.execute( select(Game, func.count(Challenge.id).label("challenges_count")) .outerjoin(Challenge) .options(selectinload(Game.added_by_user)) .where(Game.marathon_id == marathon_id) .group_by(Game.id) .order_by(Game.created_at.desc()) ) games = [] for row in result.all(): game = row[0] games.append(GameResponse( id=game.id, title=game.title, cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None, download_url=game.download_url, genre=game.genre, added_by=UserPublic.model_validate(game.added_by_user) if game.added_by_user else None, challenges_count=row[1], created_at=game.created_at, )) return games @router.post("/marathons/{marathon_id}/games", response_model=GameResponse) async def add_game( marathon_id: int, data: GameCreate, current_user: CurrentUser, db: DbSession, ): # Check marathon exists and is preparing result = await db.execute(select(Marathon).where(Marathon.id == marathon_id)) marathon = result.scalar_one_or_none() if not marathon: raise HTTPException(status_code=404, detail="Marathon not found") if marathon.status != MarathonStatus.PREPARING.value: raise HTTPException(status_code=400, detail="Cannot add games to active or finished marathon") await check_participant(db, current_user.id, marathon_id) game = Game( marathon_id=marathon_id, title=data.title, download_url=data.download_url, genre=data.genre, added_by_id=current_user.id, ) db.add(game) await db.commit() await db.refresh(game) return GameResponse( id=game.id, title=game.title, cover_url=None, download_url=game.download_url, genre=game.genre, added_by=UserPublic.model_validate(current_user), challenges_count=0, created_at=game.created_at, ) @router.get("/games/{game_id}", response_model=GameResponse) async def get_game(game_id: int, current_user: CurrentUser, db: DbSession): game = await get_game_or_404(db, game_id) await check_participant(db, current_user.id, game.marathon_id) challenges_count = await db.scalar( select(func.count()).select_from(Challenge).where(Challenge.game_id == game_id) ) return GameResponse( id=game.id, title=game.title, cover_url=f"/uploads/covers/{game.cover_path.split('/')[-1]}" if game.cover_path else None, download_url=game.download_url, genre=game.genre, added_by=UserPublic.model_validate(game.added_by_user) if game.added_by_user else None, challenges_count=challenges_count, created_at=game.created_at, ) @router.patch("/games/{game_id}", response_model=GameResponse) async def update_game( game_id: int, data: GameUpdate, current_user: CurrentUser, db: DbSession, ): game = await get_game_or_404(db, game_id) # Check if marathon is in preparing state result = await db.execute(select(Marathon).where(Marathon.id == game.marathon_id)) marathon = result.scalar_one() if marathon.status != MarathonStatus.PREPARING.value: raise HTTPException(status_code=400, detail="Cannot update games in active or finished marathon") # Only the one who added or organizer can update if game.added_by_id != current_user.id and marathon.organizer_id != current_user.id: raise HTTPException(status_code=403, detail="Only the one who added the game or organizer can update it") if data.title is not None: game.title = data.title if data.download_url is not None: game.download_url = data.download_url if data.genre is not None: game.genre = data.genre await db.commit() return await get_game(game_id, current_user, db) @router.delete("/games/{game_id}", response_model=MessageResponse) async def delete_game(game_id: int, current_user: CurrentUser, db: DbSession): game = await get_game_or_404(db, game_id) # Check if marathon is in preparing state result = await db.execute(select(Marathon).where(Marathon.id == game.marathon_id)) marathon = result.scalar_one() if marathon.status != MarathonStatus.PREPARING.value: raise HTTPException(status_code=400, detail="Cannot delete games from active or finished marathon") # Only the one who added or organizer can delete if game.added_by_id != current_user.id and marathon.organizer_id != current_user.id: raise HTTPException(status_code=403, detail="Only the one who added the game or organizer can delete it") await db.delete(game) await db.commit() return MessageResponse(message="Game deleted") @router.post("/games/{game_id}/cover", response_model=GameResponse) async def upload_cover( game_id: int, current_user: CurrentUser, db: DbSession, file: UploadFile = File(...), ): game = await get_game_or_404(db, game_id) await check_participant(db, current_user.id, game.marathon_id) # Validate file if not file.content_type.startswith("image/"): raise HTTPException(status_code=400, detail="File must be an image") contents = await file.read() if len(contents) > settings.MAX_UPLOAD_SIZE: raise HTTPException( status_code=400, detail=f"File too large. Maximum size is {settings.MAX_UPLOAD_SIZE // 1024 // 1024} MB", ) ext = file.filename.split(".")[-1].lower() if file.filename else "jpg" if ext not in settings.ALLOWED_IMAGE_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Invalid file type. Allowed: {settings.ALLOWED_IMAGE_EXTENSIONS}", ) # Save file filename = f"{game_id}_{uuid.uuid4().hex}.{ext}" filepath = Path(settings.UPLOAD_DIR) / "covers" / filename filepath.parent.mkdir(parents=True, exist_ok=True) with open(filepath, "wb") as f: f.write(contents) game.cover_path = str(filepath) await db.commit() return await get_game(game_id, current_user, db)