"""Reading event APIs.""" from datetime import datetime, timezone from typing import Literal 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, MemoryItem, ReadingEvent, Story, User router = APIRouter() EVENT_WEIGHTS: dict[str, float] = { "completed": 1.0, "replayed": 1.5, "started": 0.1, "skipped": -0.5, } class ReadingEventCreate(BaseModel): """Reading event payload.""" child_profile_id: str story_id: int | None = None event_type: Literal["started", "completed", "skipped", "replayed"] reading_time: int = Field(default=0, ge=0) class ReadingEventResponse(BaseModel): """Reading event response.""" id: int child_profile_id: str story_id: int | None event_type: str reading_time: int created_at: datetime class Config: from_attributes = True @router.post("/reading-events", response_model=ReadingEventResponse, status_code=status.HTTP_201_CREATED) async def create_reading_event( payload: ReadingEventCreate, user: User = Depends(require_user), db: AsyncSession = Depends(get_db), ): """Create a reading event and update profile stats/memory.""" result = await db.execute( select(ChildProfile).where( ChildProfile.id == payload.child_profile_id, ChildProfile.user_id == user.id, ) ) profile = result.scalar_one_or_none() if not profile: raise HTTPException(status_code=404, detail="孩子档案不存在") story = None if payload.story_id is not None: result = await db.execute( select(Story).where( Story.id == payload.story_id, Story.user_id == user.id, ) ) story = result.scalar_one_or_none() if not story: raise HTTPException(status_code=404, detail="故事不存在") if payload.reading_time: profile.total_reading_time = (profile.total_reading_time or 0) + payload.reading_time if payload.event_type in {"completed", "replayed"} and payload.story_id is not None: existing = await db.scalar( select(ReadingEvent.id).where( ReadingEvent.child_profile_id == payload.child_profile_id, ReadingEvent.story_id == payload.story_id, ReadingEvent.event_type.in_(["completed", "replayed"]), ) ) if existing is None: profile.stories_count = (profile.stories_count or 0) + 1 event = ReadingEvent( child_profile_id=payload.child_profile_id, story_id=payload.story_id, event_type=payload.event_type, reading_time=payload.reading_time, ) db.add(event) weight = EVENT_WEIGHTS.get(payload.event_type, 0.0) if story and weight > 0: db.add( MemoryItem( child_profile_id=payload.child_profile_id, universe_id=story.universe_id, type="recent_story", value={ "story_id": story.id, "title": story.title, "event_type": payload.event_type, }, base_weight=weight, last_used_at=datetime.now(timezone.utc), ttl_days=90, ) ) await db.commit() await db.refresh(event) return event