from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload, selectinload from app.core.database import get_session from app.models.encounter import Encounter from app.models.nuzlocke_run import NuzlockeRun from app.models.pokemon import Pokemon from app.models.route import Route from app.schemas.encounter import ( EncounterCreate, EncounterDetailResponse, EncounterResponse, EncounterUpdate, ) router = APIRouter() @router.post( "/runs/{run_id}/encounters", response_model=EncounterResponse, status_code=201, ) async def create_encounter( run_id: int, data: EncounterCreate, session: AsyncSession = Depends(get_session), ): # Validate run exists run = await session.get(NuzlockeRun, run_id) if run is None: raise HTTPException(status_code=404, detail="Run not found") # Validate route exists and load its children result = await session.execute( select(Route) .where(Route.id == data.route_id) .options(selectinload(Route.children)) ) route = result.scalar_one_or_none() if route is None: raise HTTPException(status_code=404, detail="Route not found") # Cannot create encounter on a parent route (routes with children) if route.children: raise HTTPException( status_code=400, detail="Cannot create encounter on a parent route. Use a child route instead.", ) # If this route has a parent, check if any sibling already has an encounter if route.parent_route_id is not None: # Get all sibling route IDs (routes with same parent, including this one) siblings_result = await session.execute( select(Route.id).where(Route.parent_route_id == route.parent_route_id) ) sibling_ids = [r for r in siblings_result.scalars().all()] # Check if any sibling already has an encounter in this run existing_encounter = await session.execute( select(Encounter) .where( Encounter.run_id == run_id, Encounter.route_id.in_(sibling_ids), ) ) if existing_encounter.scalar_one_or_none() is not None: raise HTTPException( status_code=409, detail="This location group already has an encounter. Only one encounter per location group is allowed.", ) # Validate pokemon exists pokemon = await session.get(Pokemon, data.pokemon_id) if pokemon is None: raise HTTPException(status_code=404, detail="Pokemon not found") encounter = Encounter( run_id=run_id, route_id=data.route_id, pokemon_id=data.pokemon_id, nickname=data.nickname, status=data.status, catch_level=data.catch_level, ) session.add(encounter) await session.commit() await session.refresh(encounter) return encounter @router.patch("/encounters/{encounter_id}", response_model=EncounterDetailResponse) async def update_encounter( encounter_id: int, data: EncounterUpdate, session: AsyncSession = Depends(get_session), ): encounter = await session.get(Encounter, encounter_id) if encounter is None: raise HTTPException(status_code=404, detail="Encounter not found") update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(encounter, field, value) await session.commit() # Reload with relationships for detail response result = await session.execute( select(Encounter) .where(Encounter.id == encounter_id) .options( joinedload(Encounter.pokemon), joinedload(Encounter.current_pokemon), joinedload(Encounter.route), ) ) return result.scalar_one() @router.delete("/encounters/{encounter_id}", status_code=204) async def delete_encounter( encounter_id: int, session: AsyncSession = Depends(get_session) ): encounter = await session.get(Encounter, encounter_id) if encounter is None: raise HTTPException(status_code=404, detail="Encounter not found") await session.delete(encounter) await session.commit() return Response(status_code=204)