from typing import Optional, List, Dict from datetime import date import random from sqlalchemy import select, and_, func from sqlalchemy.ext.asyncio import AsyncSession from app.models.question import Question from app.models.category import Category # Number of categories per game CATEGORIES_PER_GAME = 5 class QuestionService: async def get_daily_questions( self, db: AsyncSession, target_date: Optional[date] = None ) -> Dict[int, List[dict]]: """ Obtiene preguntas del día organizadas por categoría. Args: db: Sesión de BD target_date: Fecha (default: hoy) Returns: Dict[category_id, List[question_dict]] para el tablero """ if target_date is None: target_date = date.today() # Buscar preguntas aprobadas para la fecha query = select(Question).where( and_( Question.date_active == target_date, Question.status == "approved" ) ).order_by(Question.category_id, Question.difficulty) result = await db.execute(query) questions = result.scalars().all() # Organizar por categoría board: Dict[int, List[dict]] = {} for q in questions: if q.category_id not in board: board[q.category_id] = [] board[q.category_id].append({ "id": q.id, "category_id": q.category_id, "question_text": q.question_text, "correct_answer": q.correct_answer, "alt_answers": q.alt_answers or [], "difficulty": q.difficulty, "points": q.points, "time_seconds": q.time_seconds, "fun_fact": q.fun_fact, "answered": False, "selected": False }) return board async def get_board_for_game( self, db: AsyncSession, target_date: Optional[date] = None ) -> Dict[str, List[dict]]: """ Genera el tablero 5×5 para el juego. Selecciona 5 categorías aleatorias y 1 pregunta por dificultad. Returns: Dict con category_id como string (para JSON) -> lista de preguntas """ full_board = await self.get_daily_questions(db, target_date) if not full_board: return {} # Get available category IDs that have questions available_categories = list(full_board.keys()) # Select random categories (up to CATEGORIES_PER_GAME) num_categories = min(CATEGORIES_PER_GAME, len(available_categories)) selected_categories = random.sample(available_categories, num_categories) # Build the game board with selected categories game_board: Dict[str, List[dict]] = {} for cat_id in selected_categories: questions_by_difficulty: Dict[int, List[dict]] = {} # Group questions by difficulty for q in full_board[cat_id]: diff = q["difficulty"] if diff not in questions_by_difficulty: questions_by_difficulty[diff] = [] questions_by_difficulty[diff].append(q) # Select one random question per difficulty selected_questions = [] for difficulty in range(1, 6): # 1-5 if difficulty in questions_by_difficulty: questions = questions_by_difficulty[difficulty] selected_q = random.choice(questions) selected_questions.append(selected_q) if selected_questions: game_board[str(cat_id)] = selected_questions return game_board async def get_question_by_id( self, db: AsyncSession, question_id: int ) -> Optional[Question]: """Obtiene una pregunta por ID.""" result = await db.execute( select(Question).where(Question.id == question_id) ) return result.scalar_one_or_none() async def mark_question_used( self, db: AsyncSession, question_id: int ) -> bool: """Marca una pregunta como usada.""" question = await self.get_question_by_id(db, question_id) if question: question.status = "used" await db.commit() return True return False async def get_categories_with_questions( self, db: AsyncSession, target_date: Optional[date] = None ) -> List[dict]: """ Obtiene categorías que tienen preguntas para la fecha. Útil para mostrar solo categorías disponibles. """ board = await self.get_daily_questions(db, target_date) # Obtener info de categorías cat_ids = list(board.keys()) if not cat_ids: return [] result = await db.execute( select(Category).where(Category.id.in_(cat_ids)) ) categories = result.scalars().all() return [ { "id": c.id, "name": c.name, "icon": c.icon, "color": c.color, "question_count": len(board.get(c.id, [])) } for c in categories ] # Singleton question_service = QuestionService()