import base64 from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.api.deps import get_current_active_user from app.models import User from app.models.session import Session from app.models.supporting_data import SessionSupportingData from app.schemas.supporting_data import ( SupportingDataCreate, SupportingDataUpdate, SupportingDataResponse, ) router = APIRouter(prefix="/sessions", tags=["supporting-data"]) MAX_ITEMS_PER_SESSION = 20 MAX_TEXT_SNIPPET_CHARS = 50_000 MAX_SCREENSHOT_RAW_BYTES = 2 * 1024 * 1024 # 2MB async def _check_session_access(user: User, session: Session, db: AsyncSession) -> None: """Verify user has access to the session (owner, team admin, or super admin).""" if user.is_super_admin: return if session.user_id == user.id: return # Team admins can only access sessions from their own team members if user.is_team_admin and user.team_id is not None: session_owner = await db.get(User, session.user_id) if session_owner and session_owner.team_id == user.team_id: return raise HTTPException(status_code=403, detail="Access denied") async def _get_session_or_404(session_id: UUID, db: AsyncSession) -> Session: """Fetch session by ID or raise 404.""" result = await db.execute(select(Session).where(Session.id == session_id)) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") return session @router.post( "/{session_id}/supporting-data", response_model=SupportingDataResponse, status_code=status.HTTP_201_CREATED, ) async def create_supporting_data( session_id: UUID, data: SupportingDataCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_active_user), ): """Add a supporting data item (text snippet or screenshot) to a session.""" session = await _get_session_or_404(session_id, db) await _check_session_access(current_user, session, db) # Check item limit count_result = await db.execute( select(func.count()).select_from(SessionSupportingData).where( SessionSupportingData.session_id == session_id ) ) current_count = count_result.scalar() or 0 if current_count >= MAX_ITEMS_PER_SESSION: raise HTTPException( status_code=400, detail=f"Maximum {MAX_ITEMS_PER_SESSION} supporting data items per session", ) # Validate content size based on type if data.data_type == "text_snippet": if len(data.content) > MAX_TEXT_SNIPPET_CHARS: raise HTTPException( status_code=400, detail=f"Text snippet exceeds maximum {MAX_TEXT_SNIPPET_CHARS} characters", ) elif data.data_type == "screenshot": try: raw_bytes = base64.b64decode(data.content) except Exception: raise HTTPException(status_code=400, detail="Invalid base64 content for screenshot") if len(raw_bytes) > MAX_SCREENSHOT_RAW_BYTES: raise HTTPException( status_code=400, detail=f"Screenshot exceeds maximum {MAX_SCREENSHOT_RAW_BYTES // (1024 * 1024)}MB raw size", ) # Auto-increment sort_order max_order_result = await db.execute( select(func.max(SessionSupportingData.sort_order)).where( SessionSupportingData.session_id == session_id ) ) max_order = max_order_result.scalar() next_order = (max_order or 0) + 1 item = SessionSupportingData( session_id=session_id, label=data.label, data_type=data.data_type, content=data.content, content_type=data.content_type, sort_order=next_order, ) db.add(item) await db.commit() await db.refresh(item) return item @router.get( "/{session_id}/supporting-data", response_model=list[SupportingDataResponse], ) async def list_supporting_data( session_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_active_user), ): """List all supporting data items for a session, ordered by sort_order.""" session = await _get_session_or_404(session_id, db) await _check_session_access(current_user, session, db) result = await db.execute( select(SessionSupportingData) .where(SessionSupportingData.session_id == session_id) .order_by(SessionSupportingData.sort_order) ) return result.scalars().all() @router.patch( "/{session_id}/supporting-data/{item_id}", response_model=SupportingDataResponse, ) async def update_supporting_data( session_id: UUID, item_id: UUID, data: SupportingDataUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_active_user), ): """Update a supporting data item's label or content.""" session = await _get_session_or_404(session_id, db) await _check_session_access(current_user, session, db) result = await db.execute( select(SessionSupportingData).where( SessionSupportingData.id == item_id, SessionSupportingData.session_id == session_id, ) ) item = result.scalar_one_or_none() if not item: raise HTTPException(status_code=404, detail="Supporting data item not found") if data.label is not None: item.label = data.label if data.content is not None: item.content = data.content await db.commit() await db.refresh(item) return item @router.delete( "/{session_id}/supporting-data/{item_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_supporting_data( session_id: UUID, item_id: UUID, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_active_user), ): """Remove a supporting data item from a session.""" session = await _get_session_or_404(session_id, db) await _check_session_access(current_user, session, db) result = await db.execute( select(SessionSupportingData).where( SessionSupportingData.id == item_id, SessionSupportingData.session_id == session_id, ) ) item = result.scalar_one_or_none() if not item: raise HTTPException(status_code=404, detail="Supporting data item not found") await db.delete(item) await db.commit()