from datetime import date from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from sqlalchemy.orm import selectinload from app.dependencies import get_db, get_current_user from app.models.child import Child from app.models.schedule import ScheduleBlock, ScheduleTemplate from app.models.subject import Subject # noqa: F401 — needed for selectinload chain from app.models.session import DailySession, TimerEvent from app.models.user import User from app.schemas.session import DailySessionOut, SessionStart, TimerAction from app.websocket.manager import manager router = APIRouter(prefix="/api/sessions", tags=["sessions"]) async def _broadcast_session(db: AsyncSession, session: DailySession) -> None: """Build a snapshot dict and broadcast it to all connected TVs for this child.""" blocks = [] day_start_time = None day_end_time = None if session.template_id: blocks_result = await db.execute( select(ScheduleBlock) .where(ScheduleBlock.template_id == session.template_id) .options(selectinload(ScheduleBlock.subject).selectinload(Subject.options)) .order_by(ScheduleBlock.order_index) ) blocks = [ { "id": b.id, "subject_id": b.subject_id, "subject": { "id": b.subject.id, "name": b.subject.name, "color": b.subject.color, "icon": b.subject.icon, "options": [{"id": o.id, "text": o.text, "order_index": o.order_index} for o in b.subject.options], } if b.subject else None, "time_start": str(b.time_start), "time_end": str(b.time_end), "duration_minutes": b.duration_minutes, "label": b.label, "order_index": b.order_index, } for b in blocks_result.scalars().all() ] template_result = await db.execute( select(ScheduleTemplate).where(ScheduleTemplate.id == session.template_id) ) template = template_result.scalar_one_or_none() if template: day_start_time = str(template.day_start_time) if template.day_start_time else None day_end_time = str(template.day_end_time) if template.day_end_time else None # Gather completed block IDs from timer events events_result = await db.execute( select(TimerEvent).where( TimerEvent.session_id == session.id, TimerEvent.event_type == "complete", ) ) completed_ids = [e.block_id for e in events_result.scalars().all() if e.block_id] payload = { "event": "session_update", "session": { "id": session.id, "child_id": session.child_id, "session_date": str(session.session_date), "is_active": session.is_active, "current_block_id": session.current_block_id, }, "blocks": blocks, "completed_block_ids": completed_ids, "day_start_time": day_start_time, "day_end_time": day_end_time, } await manager.broadcast(session.child_id, payload) @router.post("", response_model=DailySessionOut, status_code=status.HTTP_201_CREATED) async def start_session( body: SessionStart, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): # Verify child belongs to user child_result = await db.execute( select(Child).where(Child.id == body.child_id, Child.user_id == current_user.id) ) if not child_result.scalar_one_or_none(): raise HTTPException(status_code=404, detail="Child not found") session_date = body.session_date or date.today() # Deactivate any existing active session for this child today existing = await db.execute( select(DailySession).where( DailySession.child_id == body.child_id, DailySession.session_date == session_date, DailySession.is_active == True, ) ) for old in existing.scalars().all(): old.is_active = False session = DailySession( child_id=body.child_id, template_id=body.template_id, session_date=session_date, is_active=True, ) db.add(session) await db.commit() await db.refresh(session) await _broadcast_session(db, session) return session @router.get("/{session_id}", response_model=DailySessionOut) async def get_session( session_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(DailySession) .join(Child) .where(DailySession.id == session_id, Child.user_id == current_user.id) .options(selectinload(DailySession.current_block)) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") return session @router.post("/{session_id}/timer", response_model=DailySessionOut) async def timer_action( session_id: int, body: TimerAction, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): result = await db.execute( select(DailySession) .join(Child) .where(DailySession.id == session_id, Child.user_id == current_user.id) .options(selectinload(DailySession.current_block)) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") # Update current block if provided if body.block_id is not None: session.current_block_id = body.block_id # Record the timer event event = TimerEvent( session_id=session.id, block_id=body.block_id or session.current_block_id, event_type=body.event_type, ) db.add(event) # Mark session complete if event is session-level complete if body.event_type == "complete" and body.block_id is None: session.is_active = False await db.commit() await db.refresh(session) # Broadcast the timer event to all TV clients ws_payload = { "event": body.event_type, "session_id": session.id, "block_id": event.block_id, "current_block_id": session.current_block_id, "is_active": session.is_active, } await manager.broadcast(session.child_id, ws_payload) return session