from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy import func, 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.game import Game from app.models.genlocke import Genlocke, GenlockeLeg from app.models.nuzlocke_run import NuzlockeRun from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate router = APIRouter() @router.post("", response_model=RunResponse, status_code=201) async def create_run( data: RunCreate, session: AsyncSession = Depends(get_session) ): # Validate game exists game = await session.get(Game, data.game_id) if game is None: raise HTTPException(status_code=404, detail="Game not found") run = NuzlockeRun( game_id=data.game_id, name=data.name, status="active", rules=data.rules, ) session.add(run) await session.commit() await session.refresh(run) return run @router.get("", response_model=list[RunResponse]) async def list_runs(session: AsyncSession = Depends(get_session)): result = await session.execute( select(NuzlockeRun).order_by(NuzlockeRun.started_at.desc()) ) return result.scalars().all() @router.get("/{run_id}", response_model=RunDetailResponse) async def get_run(run_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute( select(NuzlockeRun) .where(NuzlockeRun.id == run_id) .options( joinedload(NuzlockeRun.game), selectinload(NuzlockeRun.encounters) .joinedload(Encounter.pokemon), selectinload(NuzlockeRun.encounters) .joinedload(Encounter.current_pokemon), selectinload(NuzlockeRun.encounters) .joinedload(Encounter.route), ) ) run = result.scalar_one_or_none() if run is None: raise HTTPException(status_code=404, detail="Run not found") # Check if this run belongs to a genlocke genlocke_context = None leg_result = await session.execute( select(GenlockeLeg) .where(GenlockeLeg.run_id == run_id) .options(joinedload(GenlockeLeg.genlocke)) ) leg = leg_result.scalar_one_or_none() if leg: total_legs_result = await session.execute( select(func.count()) .select_from(GenlockeLeg) .where(GenlockeLeg.genlocke_id == leg.genlocke_id) ) total_legs = total_legs_result.scalar_one() genlocke_context = RunGenlockeContext( genlocke_id=leg.genlocke_id, genlocke_name=leg.genlocke.name, leg_order=leg.leg_order, total_legs=total_legs, is_final_leg=leg.leg_order == total_legs, ) response = RunDetailResponse.model_validate(run) response.genlocke = genlocke_context return response @router.patch("/{run_id}", response_model=RunResponse) async def update_run( run_id: int, data: RunUpdate, session: AsyncSession = Depends(get_session), ): run = await session.get(NuzlockeRun, run_id) if run is None: raise HTTPException(status_code=404, detail="Run not found") update_data = data.model_dump(exclude_unset=True) # Auto-set completed_at when ending a run if "status" in update_data and update_data["status"] in ("completed", "failed"): if run.status != "active": raise HTTPException( status_code=400, detail="Only active runs can be ended" ) update_data["completed_at"] = datetime.now(timezone.utc) for field, value in update_data.items(): setattr(run, field, value) # Genlocke side effects when run status changes if "status" in update_data and update_data["status"] in ("completed", "failed"): leg_result = await session.execute( select(GenlockeLeg) .where(GenlockeLeg.run_id == run_id) .options(joinedload(GenlockeLeg.genlocke)) ) leg = leg_result.scalar_one_or_none() if leg: genlocke = leg.genlocke if update_data["status"] == "failed": genlocke.status = "failed" elif update_data["status"] == "completed": total_legs_result = await session.execute( select(func.count()) .select_from(GenlockeLeg) .where(GenlockeLeg.genlocke_id == genlocke.id) ) total_legs = total_legs_result.scalar_one() if leg.leg_order == total_legs: genlocke.status = "completed" await session.commit() await session.refresh(run) return run @router.delete("/{run_id}", status_code=204) async def delete_run( run_id: int, session: AsyncSession = Depends(get_session) ): run = await session.get(NuzlockeRun, run_id) if run is None: raise HTTPException(status_code=404, detail="Run not found") # Delete associated encounters first encounters = await session.execute( select(Encounter).where(Encounter.run_id == run_id) ) for enc in encounters.scalars(): await session.delete(enc) await session.delete(run) await session.commit() return Response(status_code=204)