"""Story universe APIs.""" from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import require_user from app.db.database import get_db from app.db.models import ChildProfile, StoryUniverse, User router = APIRouter() class StoryUniverseCreate(BaseModel): """Create universe payload.""" name: str = Field(..., min_length=1, max_length=100) protagonist: dict[str, Any] recurring_characters: list[dict[str, Any]] = Field(default_factory=list) world_settings: dict[str, Any] = Field(default_factory=dict) class StoryUniverseUpdate(BaseModel): """Update universe payload.""" name: str | None = Field(default=None, min_length=1, max_length=100) protagonist: dict[str, Any] | None = None recurring_characters: list[dict[str, Any]] | None = None world_settings: dict[str, Any] | None = None class AchievementCreate(BaseModel): """Achievement payload.""" type: str = Field(..., min_length=1, max_length=50) description: str = Field(..., min_length=1, max_length=200) class StoryUniverseResponse(BaseModel): """Universe response.""" id: str child_profile_id: str name: str protagonist: dict[str, Any] recurring_characters: list[dict[str, Any]] world_settings: dict[str, Any] achievements: list[dict[str, Any]] class Config: from_attributes = True class StoryUniverseListResponse(BaseModel): """Universe list response.""" universes: list[StoryUniverseResponse] total: int async def _get_profile_or_404( profile_id: str, user: User, db: AsyncSession, ) -> ChildProfile: result = await db.execute( select(ChildProfile).where( ChildProfile.id == profile_id, ChildProfile.user_id == user.id, ) ) profile = result.scalar_one_or_none() if not profile: raise HTTPException(status_code=404, detail="档案不存在") return profile async def _get_universe_or_404( universe_id: str, user: User, db: AsyncSession, ) -> StoryUniverse: result = await db.execute( select(StoryUniverse) .join(ChildProfile, StoryUniverse.child_profile_id == ChildProfile.id) .where( StoryUniverse.id == universe_id, ChildProfile.user_id == user.id, ) ) universe = result.scalar_one_or_none() if not universe: raise HTTPException(status_code=404, detail="宇宙不存在") return universe @router.get("/profiles/{profile_id}/universes", response_model=StoryUniverseListResponse) async def list_universes( profile_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """List universes for a child profile.""" await _get_profile_or_404(profile_id, user, db) result = await db.execute( select(StoryUniverse) .where(StoryUniverse.child_profile_id == profile_id) .order_by(StoryUniverse.updated_at.desc()) ) universes = result.scalars().all() return StoryUniverseListResponse(universes=universes, total=len(universes)) @router.post( "/profiles/{profile_id}/universes", response_model=StoryUniverseResponse, status_code=status.HTTP_201_CREATED, ) async def create_universe( profile_id: str, payload: StoryUniverseCreate, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Create a story universe.""" await _get_profile_or_404(profile_id, user, db) universe = StoryUniverse(child_profile_id=profile_id, **payload.model_dump()) db.add(universe) await db.commit() await db.refresh(universe) return universe @router.get("/universes/{universe_id}", response_model=StoryUniverseResponse) async def get_universe( universe_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Get one universe.""" universe = await _get_universe_or_404(universe_id, user, db) return universe @router.put("/universes/{universe_id}", response_model=StoryUniverseResponse) async def update_universe( universe_id: str, payload: StoryUniverseUpdate, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Update a story universe.""" universe = await _get_universe_or_404(universe_id, user, db) updates = payload.model_dump(exclude_unset=True) for key, value in updates.items(): setattr(universe, key, value) await db.commit() await db.refresh(universe) return universe @router.delete("/universes/{universe_id}") async def delete_universe( universe_id: str, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Delete a story universe.""" universe = await _get_universe_or_404(universe_id, user, db) await db.delete(universe) await db.commit() return {"message": "Deleted"} @router.post("/universes/{universe_id}/achievements", response_model=StoryUniverseResponse) async def add_achievement( universe_id: str, payload: AchievementCreate, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Add an achievement to a universe.""" universe = await _get_universe_or_404(universe_id, user, db) achievements = list(universe.achievements or []) key = (payload.type.strip(), payload.description.strip()) existing = { (str(item.get("type", "")).strip(), str(item.get("description", "")).strip()) for item in achievements if isinstance(item, dict) } if key not in existing: achievements.append({"type": key[0], "description": key[1]}) universe.achievements = achievements await db.commit() await db.refresh(universe) return universe