import uuid from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from ...auth import verify_token from ...config import settings from ...outbound.postgres.database import get_db from ...outbound.postgres.repositories.adventure_repository import ( PostgresAdventureEntryAudioRepository, PostgresAdventureEntryChoiceRepository, PostgresAdventureEntryRepository, PostgresAdventureEntryTranslationRepository, PostgresAdventureRepository, ) router = APIRouter(prefix="/adventure", tags=["bff", "adventures"]) class AdventureChoiceItem(BaseModel): id: str index: int label: str text: str class AdventureEntryItem(BaseModel): id: str adventure_id: str possible_choices: list[AdventureChoiceItem] | None generated_from_choice_id: str | None status: str entry_index: int story_text: str | None translation: str | None audio_url: str | None created_at: str class AdventureDetailResponse(BaseModel): id: str user_id: str status: str language: str source_language: str competencies: list[str] max_entry_count: int title: str description: str | None genres: list[str] setting: list[str] vibes: list[str] protagonist: list[str] created_at: str entries: list[AdventureEntryItem] current_entry_choices: list[AdventureChoiceItem] def _audio_url(key: str | None) -> str | None: if key is None: return None return f"{settings.api_base_url}/media/{key}" @router.get("/{adventure_id}", response_model=AdventureDetailResponse, status_code=200) async def get_adventure( adventure_id: str, db: AsyncSession = Depends(get_db), token_data: dict = Depends(verify_token), ) -> AdventureDetailResponse: user_id = uuid.UUID(token_data["sub"]) try: adv_id = uuid.UUID(adventure_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid adventure_id") adventure = await PostgresAdventureRepository(db).get_by_id(adv_id) if adventure is None or adventure.user_id != str(user_id): raise HTTPException(status_code=404, detail="Adventure not found") entries = await PostgresAdventureEntryRepository(db).list_for_adventure(adv_id) choices_repo = PostgresAdventureEntryChoiceRepository(db) translation_repo = PostgresAdventureEntryTranslationRepository(db) audio_repo = PostgresAdventureEntryAudioRepository(db) entry_items = [] for entry in entries: eid = uuid.UUID(entry.id) translation = await translation_repo.get_for_entry( entry_id=eid, component_type="story_text", target_language=adventure.source_language, ) audio = await audio_repo.get_for_entry(entry_id=eid, component_type="story_text") choices = await choices_repo.list_for_entry(eid) entry_items.append( AdventureEntryItem( id=entry.id, adventure_id=entry.adventure_id, possible_choices=[ AdventureChoiceItem(id=c.id, index=c.index, label=c.label, text=c.text) for c in choices ] if choices else None, generated_from_choice_id=entry.generated_from_choice_id, status=entry.status, entry_index=entry.entry_index, story_text=entry.story_text, translation=translation.translated_text if translation else None, audio_url=_audio_url(audio.file_name if audio else None), created_at=entry.created_at.isoformat(), ) ) # Choices for the most recent entry, if it's complete (entries are ordered entry_index asc) current_entry_choices: list[AdventureChoiceItem] = [] if entries and entries[-1].status == "complete": choices = await PostgresAdventureEntryChoiceRepository(db).list_for_entry( uuid.UUID(entries[-1].id) ) current_entry_choices = [ AdventureChoiceItem(id=c.id, index=c.index, label=c.label, text=c.text) for c in choices ] return AdventureDetailResponse( id=adventure.id, user_id=adventure.user_id, status=adventure.status, language=adventure.language, source_language=adventure.source_language, competencies=adventure.competencies, max_entry_count=adventure.max_entry_count, title=adventure.title, description=adventure.description, genres=adventure.genres, setting=adventure.setting, vibes=adventure.vibes, protagonist=adventure.protagonist, created_at=adventure.created_at.isoformat(), entries=entry_items, current_entry_choices=current_entry_choices, )