from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.core.database import get_session from app.models.game import Game from app.models.route import Route from app.schemas.game import ( GameCreate, GameDetailResponse, GameResponse, GameUpdate, RouteCreate, RouteReorderRequest, RouteResponse, RouteUpdate, ) router = APIRouter() @router.get("", response_model=list[GameResponse]) async def list_games(session: AsyncSession = Depends(get_session)): result = await session.execute(select(Game).order_by(Game.id)) return result.scalars().all() @router.get("/{game_id}", response_model=GameDetailResponse) async def get_game(game_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute( select(Game) .where(Game.id == game_id) .options(selectinload(Game.routes)) ) game = result.scalar_one_or_none() if game is None: raise HTTPException(status_code=404, detail="Game not found") # Sort routes by order for the response game.routes.sort(key=lambda r: r.order) return game @router.get("/{game_id}/routes", response_model=list[RouteResponse]) async def list_game_routes( game_id: int, session: AsyncSession = Depends(get_session) ): # Verify game exists game = await session.get(Game, game_id) if game is None: raise HTTPException(status_code=404, detail="Game not found") result = await session.execute( select(Route) .where(Route.game_id == game_id) .order_by(Route.order) ) return result.scalars().all() # --- Admin endpoints --- @router.post("", response_model=GameResponse, status_code=201) async def create_game( data: GameCreate, session: AsyncSession = Depends(get_session) ): existing = await session.execute( select(Game).where(Game.slug == data.slug) ) if existing.scalar_one_or_none() is not None: raise HTTPException(status_code=409, detail="Game with this slug already exists") game = Game(**data.model_dump()) session.add(game) await session.commit() await session.refresh(game) return game @router.put("/{game_id}", response_model=GameResponse) async def update_game( game_id: int, data: GameUpdate, session: AsyncSession = Depends(get_session) ): game = await session.get(Game, game_id) if game is None: raise HTTPException(status_code=404, detail="Game not found") update_data = data.model_dump(exclude_unset=True) if "slug" in update_data: existing = await session.execute( select(Game).where(Game.slug == update_data["slug"], Game.id != game_id) ) if existing.scalar_one_or_none() is not None: raise HTTPException(status_code=409, detail="Game with this slug already exists") for field, value in update_data.items(): setattr(game, field, value) await session.commit() await session.refresh(game) return game @router.delete("/{game_id}", status_code=204) async def delete_game( game_id: int, session: AsyncSession = Depends(get_session) ): result = await session.execute( select(Game).where(Game.id == game_id).options(selectinload(Game.runs)) ) game = result.scalar_one_or_none() if game is None: raise HTTPException(status_code=404, detail="Game not found") if game.runs: raise HTTPException( status_code=409, detail="Cannot delete game with existing runs. Delete the runs first.", ) # Delete routes (and their route_encounters via cascade) routes = await session.execute( select(Route).where(Route.game_id == game_id) ) for route in routes.scalars().all(): await session.delete(route) await session.delete(game) await session.commit() @router.post("/{game_id}/routes", response_model=RouteResponse, status_code=201) async def create_route( game_id: int, data: RouteCreate, session: AsyncSession = Depends(get_session) ): game = await session.get(Game, game_id) if game is None: raise HTTPException(status_code=404, detail="Game not found") route = Route(game_id=game_id, **data.model_dump()) session.add(route) await session.commit() await session.refresh(route) return route @router.put("/{game_id}/routes/reorder", response_model=list[RouteResponse]) async def reorder_routes( game_id: int, data: RouteReorderRequest, session: AsyncSession = Depends(get_session), ): game = await session.get(Game, game_id) if game is None: raise HTTPException(status_code=404, detail="Game not found") for item in data.routes: route = await session.get(Route, item.id) if route is None or route.game_id != game_id: raise HTTPException( status_code=400, detail=f"Route {item.id} not found in this game", ) route.order = item.order await session.commit() result = await session.execute( select(Route).where(Route.game_id == game_id).order_by(Route.order) ) return result.scalars().all() @router.put("/{game_id}/routes/{route_id}", response_model=RouteResponse) async def update_route( game_id: int, route_id: int, data: RouteUpdate, session: AsyncSession = Depends(get_session), ): route = await session.get(Route, route_id) if route is None or route.game_id != game_id: raise HTTPException(status_code=404, detail="Route not found in this game") for field, value in data.model_dump(exclude_unset=True).items(): setattr(route, field, value) await session.commit() await session.refresh(route) return route @router.delete("/{game_id}/routes/{route_id}", status_code=204) async def delete_route( game_id: int, route_id: int, session: AsyncSession = Depends(get_session), ): result = await session.execute( select(Route) .where(Route.id == route_id, Route.game_id == game_id) .options(selectinload(Route.encounters)) ) route = result.scalar_one_or_none() if route is None: raise HTTPException(status_code=404, detail="Route not found in this game") if route.encounters: raise HTTPException( status_code=409, detail="Cannot delete route with existing encounters. Delete the encounters first.", ) await session.delete(route) await session.commit()