from fastapi import APIRouter, HTTPException from sqlalchemy import select from sqlalchemy.orm import selectinload from app.api.deps import DbSession, CurrentUser from app.models import Marathon, MarathonStatus, Game, Challenge, Participant from app.schemas import ( ChallengeCreate, ChallengeUpdate, ChallengeResponse, MessageResponse, GameShort, ) from app.services.gpt import GPTService router = APIRouter(tags=["challenges"]) gpt_service = GPTService() async def get_challenge_or_404(db, challenge_id: int) -> Challenge: result = await db.execute( select(Challenge) .options(selectinload(Challenge.game)) .where(Challenge.id == challenge_id) ) challenge = result.scalar_one_or_none() if not challenge: raise HTTPException(status_code=404, detail="Challenge not found") return challenge 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("/games/{game_id}/challenges", response_model=list[ChallengeResponse]) async def list_challenges(game_id: int, current_user: CurrentUser, db: DbSession): # Get game and check access result = await db.execute( select(Game).where(Game.id == game_id) ) game = result.scalar_one_or_none() if not game: raise HTTPException(status_code=404, detail="Game not found") await check_participant(db, current_user.id, game.marathon_id) result = await db.execute( select(Challenge) .where(Challenge.game_id == game_id) .order_by(Challenge.difficulty, Challenge.created_at) ) challenges = result.scalars().all() return [ ChallengeResponse( id=c.id, title=c.title, description=c.description, type=c.type, difficulty=c.difficulty, points=c.points, estimated_time=c.estimated_time, proof_type=c.proof_type, proof_hint=c.proof_hint, game=GameShort(id=game.id, title=game.title, cover_url=None), is_generated=c.is_generated, created_at=c.created_at, ) for c in challenges ] @router.post("/games/{game_id}/challenges", response_model=ChallengeResponse) async def create_challenge( game_id: int, data: ChallengeCreate, current_user: CurrentUser, db: DbSession, ): # Get game and check access result = await db.execute( select(Game).where(Game.id == game_id) ) game = result.scalar_one_or_none() if not game: raise HTTPException(status_code=404, detail="Game not found") # Check 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 add challenges to active or finished marathon") await check_participant(db, current_user.id, game.marathon_id) challenge = Challenge( game_id=game_id, title=data.title, description=data.description, type=data.type.value, difficulty=data.difficulty.value, points=data.points, estimated_time=data.estimated_time, proof_type=data.proof_type.value, proof_hint=data.proof_hint, is_generated=False, ) db.add(challenge) await db.commit() await db.refresh(challenge) return ChallengeResponse( id=challenge.id, title=challenge.title, description=challenge.description, type=challenge.type, difficulty=challenge.difficulty, points=challenge.points, estimated_time=challenge.estimated_time, proof_type=challenge.proof_type, proof_hint=challenge.proof_hint, game=GameShort(id=game.id, title=game.title, cover_url=None), is_generated=challenge.is_generated, created_at=challenge.created_at, ) @router.post("/marathons/{marathon_id}/generate-challenges", response_model=MessageResponse) async def generate_challenges(marathon_id: int, current_user: CurrentUser, db: DbSession): """Generate challenges for all games in marathon using GPT""" # Check marathon 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 generate challenges for active or finished marathon") await check_participant(db, current_user.id, marathon_id) # Get all games result = await db.execute( select(Game).where(Game.marathon_id == marathon_id) ) games = result.scalars().all() if not games: raise HTTPException(status_code=400, detail="No games in marathon") generated_count = 0 for game in games: # Check if game already has challenges existing = await db.scalar( select(Challenge.id).where(Challenge.game_id == game.id).limit(1) ) if existing: continue # Skip if already has challenges try: challenges_data = await gpt_service.generate_challenges(game.title, game.genre) for ch_data in challenges_data: challenge = Challenge( game_id=game.id, title=ch_data.title, description=ch_data.description, type=ch_data.type, difficulty=ch_data.difficulty, points=ch_data.points, estimated_time=ch_data.estimated_time, proof_type=ch_data.proof_type, proof_hint=ch_data.proof_hint, is_generated=True, ) db.add(challenge) generated_count += 1 except Exception as e: # Log error but continue with other games print(f"Error generating challenges for {game.title}: {e}") await db.commit() return MessageResponse(message=f"Generated {generated_count} challenges") @router.patch("/challenges/{challenge_id}", response_model=ChallengeResponse) async def update_challenge( challenge_id: int, data: ChallengeUpdate, current_user: CurrentUser, db: DbSession, ): challenge = await get_challenge_or_404(db, challenge_id) # Check marathon is in preparing state result = await db.execute(select(Marathon).where(Marathon.id == challenge.game.marathon_id)) marathon = result.scalar_one() if marathon.status != MarathonStatus.PREPARING.value: raise HTTPException(status_code=400, detail="Cannot update challenges in active or finished marathon") await check_participant(db, current_user.id, challenge.game.marathon_id) if data.title is not None: challenge.title = data.title if data.description is not None: challenge.description = data.description if data.type is not None: challenge.type = data.type.value if data.difficulty is not None: challenge.difficulty = data.difficulty.value if data.points is not None: challenge.points = data.points if data.estimated_time is not None: challenge.estimated_time = data.estimated_time if data.proof_type is not None: challenge.proof_type = data.proof_type.value if data.proof_hint is not None: challenge.proof_hint = data.proof_hint await db.commit() await db.refresh(challenge) game = challenge.game return ChallengeResponse( id=challenge.id, title=challenge.title, description=challenge.description, type=challenge.type, difficulty=challenge.difficulty, points=challenge.points, estimated_time=challenge.estimated_time, proof_type=challenge.proof_type, proof_hint=challenge.proof_hint, game=GameShort(id=game.id, title=game.title, cover_url=None), is_generated=challenge.is_generated, created_at=challenge.created_at, ) @router.delete("/challenges/{challenge_id}", response_model=MessageResponse) async def delete_challenge(challenge_id: int, current_user: CurrentUser, db: DbSession): challenge = await get_challenge_or_404(db, challenge_id) # Check marathon is in preparing state result = await db.execute(select(Marathon).where(Marathon.id == challenge.game.marathon_id)) marathon = result.scalar_one() if marathon.status != MarathonStatus.PREPARING.value: raise HTTPException(status_code=400, detail="Cannot delete challenges from active or finished marathon") await check_participant(db, current_user.id, challenge.game.marathon_id) await db.delete(challenge) await db.commit() return MessageResponse(message="Challenge deleted")