""" Games Router - Consolidated This module consolidates room management, game operations, MTGJSON data refresh, and WebSocket connections into a unified router structure. Endpoints: /rooms/* - Room CRUD operations (Admin/Judge only for write ops) /games/* - Game management stubs /mtgjson/* - MTGJSON dataset refresh and verification (Admin only) /ws/game/{game_id} - WebSocket connection for game sessions Architecture: - Uses FastAPI sub-routers with different prefixes for logical grouping - WebSocket endpoint for real-time game communication - All endpoints require authentication via get_current_user dependency - Admin/Judge privilege checks on sensitive operations """ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, WebSocket, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, insert, update, delete from typing import Optional, List from datetime import datetime from app.core.database import get_db from app.core.security import get_current_user from app.models.models import Room, User from app.schemas.schemas import RoomResponse, GameCreate, GameResponse from app.services.mtgjson_manager import MTGJSONManager from app.services.game_server import game_websocket_endpoint # Main router router = APIRouter() # ============================================================================= # Room Management Sub-Router (prefix: /rooms) # ============================================================================= rooms_router = APIRouter(prefix="/rooms", tags=["Rooms"]) @rooms_router.get("/", response_model=List[RoomResponse]) async def list_rooms( db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> List[RoomResponse]: """List all available rooms. Returns a list of all rooms in the system, ordered by ID. Requires authentication. """ stmt = select(Room).order_by(Room.id) result = await db.execute(stmt) rooms = result.scalars().all() return [RoomResponse.model_validate(room) for room in rooms] @rooms_router.get("/{room_id}", response_model=RoomResponse) async def get_room( room_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> RoomResponse: """Get room by ID. Returns detailed information about a specific room. Raises 404 if room not found. """ stmt = select(Room).where(Room.id == room_id) result = await db.execute(stmt) room = result.scalar_one_or_none() if not room: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found", ) return RoomResponse.model_validate(room) @rooms_router.post("/", status_code=status.HTTP_201_CREATED) async def create_room( name: str, description: Optional[str] = None, is_password_protected: bool = False, password_hash: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Create a new room (Admin/Judge only). Creates a new game room with the specified configuration. Only users with Admin or Judge privileges can create rooms. Raises 409 if room name already exists. """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) # Check if room name already exists stmt = select(Room).where(Room.name == name) result = await db.execute(stmt) if result.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Room name already exists", ) new_room = Room( name=name, description=description, is_password_protected=is_password_protected, password_hash=password_hash, ) db.add(new_room) await db.flush() return {"message": f"Room '{name}' created successfully", "room_id": new_room.id} @rooms_router.patch("/{room_id}") async def update_room( room_id: int, name: Optional[str] = None, description: Optional[str] = None, is_password_protected: Optional[bool] = None, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Update room (Admin/Judge only). Updates the specified fields of a room. Only users with Admin or Judge privileges can update rooms. """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(Room).where(Room.id == room_id) result = await db.execute(stmt) room = result.scalar_one_or_none() if not room: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found", ) # Update fields update_data = {} if name: update_data["name"] = name if description is not None: update_data["description"] = description if is_password_protected is not None: update_data["is_password_protected"] = is_password_protected stmt = ( update(Room) .where(Room.id == room_id) .values(**update_data) ) await db.execute(stmt) return {"message": f"Room '{name or room.name}' updated successfully"} @rooms_router.delete("/{room_id}") async def delete_room( room_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Delete room (Admin/Judge only). Permanently deletes a room from the system. Only users with Admin or Judge privileges can delete rooms. """ # Check if current user is admin if current_user.get("privlevel") not in ["Admin", "Judge"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) stmt = select(Room).where(Room.id == room_id) result = await db.execute(stmt) room = result.scalar_one_or_none() if not room: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Room not found", ) await db.execute(delete(Room).where(Room.id == room_id)) return {"message": f"Room '{room.name}' deleted successfully"} # ============================================================================= # Game Management Sub-Router (prefix: /games) # ============================================================================= games_router = APIRouter(prefix="/games", tags=["Games"]) @games_router.get("/", response_model=List[GameResponse]) async def list_games( room_id: Optional[int] = None, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> List[GameResponse]: """List active games. Returns a list of currently active games. Note: This is a stub implementation - returns empty list. """ # This would query a games table - simplified for now # In production, you'd have a MTG OnlineGames model return [] @games_router.post("/", response_model=GameResponse, status_code=status.HTTP_201_CREATED) async def create_game( request: GameCreate, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> GameResponse: """Create a new game (mock implementation). Creates a new game session. Note: This is a stub - returns mock response. """ user_id = int(current_user["user_id"]) # Verify user exists stmt = select(User).where(User.id == user_id) result = await db.execute(stmt) user = result.scalar_one_or_none() if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found", ) # In production, you'd create a game in the game database # For now, return a mock response game_data = { "id": 1, # Mock ID "room_id": request.room_id, "game_type": request.game_type, "description": request.description, "with_password": bool(request.password), "max_players": 4, "player_count": 1, "started": False, "creation_date": datetime.now(), } return GameResponse(**game_data) @games_router.get("/{game_id}", response_model=GameResponse) async def get_game( game_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> GameResponse: """Get game by ID (mock implementation). Returns details about a specific game. Note: This is a stub - returns mock response. """ # In production, query the games table return GameResponse( id=game_id, room_id=1, game_type="Casual", description="Test game", with_password=False, max_players=4, player_count=0, started=False, creation_date=datetime.now(), ) @games_router.post("/{game_id}/join") async def join_game( game_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Join a game (stub implementation). Adds the current user to a game session. Note: This is a stub - returns mock response. """ user_id = int(current_user["user_id"]) # In production, add user to game players table return {"message": f"User {user_id} joined game {game_id}"} @games_router.post("/{game_id}/leave") async def leave_game( game_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Leave a game (stub implementation). Removes the current user from a game session. Note: This is a stub - returns mock response. """ user_id = int(current_user["user_id"]) # In production, remove user from game players table return {"message": f"User {user_id} left game {game_id}"} @games_router.post("/{game_id}/start") async def start_game( game_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """Start a game (host only, stub implementation). Initiates a game session. Note: This is a stub - returns mock response. """ user_id = int(current_user["user_id"]) # In production, update game started status return {"message": f"Game {game_id} started"} @games_router.post("/{game_id}/end") async def end_game( game_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ) -> dict: """End a game (stub implementation). Terminates a game session. Note: This is a stub - returns mock response. """ user_id = int(current_user["user_id"]) # In production, update game closed status return {"message": f"Game {game_id} ended"} # ============================================================================= # MTGJSON Data Refresh Sub-Router (prefix: /mtgjson) # ============================================================================= mtgjson_router = APIRouter(prefix="/mtgjson", tags=["MTGJSON Data"]) @mtgjson_router.post("/refresh") async def trigger_refresh( background_tasks: BackgroundTasks, current_user: dict = Depends(get_current_user), ) -> dict: """Trigger a refresh of MTGJSON datasets (Admin only). Downloads files from MTGJSON API and upserts data into PostgreSQL. The refresh runs in the background and may take several minutes. Only Admin users can trigger refreshes. """ # Check if current user is admin if current_user.get("privlevel") != "Admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) # Create manager and trigger refresh in background manager = MTGJSONManager() background_tasks.add_task(manager.download_and_refresh, force=False) return { "message": "Refresh initiated in background", "status": "started", } @mtgjson_router.get("/status") async def get_refresh_status( current_user: dict = Depends(get_current_user), ) -> dict: """Get current refresh status and data health (Admin only). Returns information about: - Last successful refresh timestamp - Current data counts - File status - Required files status Only Admin users can access this endpoint. """ # Check if current user is admin if current_user.get("privlevel") != "Admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) manager = MTGJSONManager() return await manager.get_health_status() @mtgjson_router.post("/verify") async def verify_files( current_user: dict = Depends(get_current_user), ) -> dict: """Verify the integrity of downloaded MTGJSON files (Admin only). Checks that all required files exist and are valid. Only Admin users can verify files. """ # Check if current user is admin if current_user.get("privlevel") != "Admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required", ) manager = MTGJSONManager() all_valid, errors = await manager.verify_files() if not all_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={ "valid": False, "errors": errors, "message": "Some files failed verification", }, ) return { "valid": True, "message": "All files verified successfully", } # ============================================================================= # WebSocket Endpoint # ============================================================================= @router.websocket("/ws/game/{game_id}") async def game_websocket( websocket: WebSocket, game_id: int, player_id: Optional[int] = Query(None), ) -> None: """WebSocket endpoint for game connections. Handles real-time game state updates for connected players. Establishes a persistent connection for live game communication. """ await websocket.accept() await game_websocket_endpoint(websocket, game_id) # ============================================================================= # Include Sub-Routers # ============================================================================= router.include_router(rooms_router) router.include_router(games_router) router.include_router(mtgjson_router)