import random from datetime import datetime, timezone from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.dependencies import get_db, get_current_user from app.models.child import Child from app.models.strike import StrikeEvent from app.models.user import User from app.schemas.child import ChildCreate, ChildOut, ChildUpdate from app.websocket.manager import manager router = APIRouter(prefix="/api/children", tags=["children"]) def _today_in_tz(tz_name: str): """Return today's date in the given IANA timezone, falling back to UTC.""" try: tz = ZoneInfo(tz_name) except (ZoneInfoNotFoundError, Exception): tz = timezone.utc return datetime.now(tz).date() @router.get("", response_model=list[ChildOut]) async def list_children( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Child).where(Child.user_id == current_user.id).order_by(Child.name) ) children = result.scalars().all() today = _today_in_tz(current_user.timezone) needs_commit = False for child in children: if child.strikes != 0 and child.strikes_last_reset != today: child.strikes = 0 child.strikes_last_reset = today needs_commit = True await manager.broadcast(child.id, {"event": "strikes_update", "strikes": 0}) if needs_commit: await db.commit() return children async def _generate_tv_token(db: AsyncSession) -> int: while True: token = random.randint(1000, 9999) result = await db.execute(select(Child).where(Child.tv_token == token)) if not result.scalar_one_or_none(): return token @router.post("", response_model=ChildOut, status_code=status.HTTP_201_CREATED) async def create_child( body: ChildCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): tv_token = await _generate_tv_token(db) child = Child(**body.model_dump(), user_id=current_user.id, tv_token=tv_token) db.add(child) await db.commit() await db.refresh(child) return child @router.get("/{child_id}", response_model=ChildOut) async def get_child( child_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Child).where(Child.id == child_id, Child.user_id == current_user.id) ) child = result.scalar_one_or_none() if not child: raise HTTPException(status_code=404, detail="Child not found") return child @router.patch("/{child_id}", response_model=ChildOut) async def update_child( child_id: int, body: ChildUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Child).where(Child.id == child_id, Child.user_id == current_user.id) ) child = result.scalar_one_or_none() if not child: raise HTTPException(status_code=404, detail="Child not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(child, field, value) await db.commit() await db.refresh(child) return child class StrikesBody(BaseModel): strikes: int = Field(..., ge=0, le=3) @router.patch("/{child_id}/strikes", response_model=ChildOut) async def update_strikes( child_id: int, body: StrikesBody, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Child).where(Child.id == child_id, Child.user_id == current_user.id) ) child = result.scalar_one_or_none() if not child: raise HTTPException(status_code=404, detail="Child not found") prev = child.strikes child.strikes = body.strikes db.add(StrikeEvent(child_id=child.id, prev_strikes=prev, new_strikes=body.strikes)) await db.commit() await db.refresh(child) await manager.broadcast(child_id, {"event": "strikes_update", "strikes": child.strikes}) return child @router.delete("/{child_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_child( child_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(Child).where(Child.id == child_id, Child.user_id == current_user.id) ) child = result.scalar_one_or_none() if not child: raise HTTPException(status_code=404, detail="Child not found") await db.delete(child) await db.commit()