""" Normalization API router for dynamic normalization management. Endpoints: - GET /tryout/{tryout_id}/normalization: Get normalization configuration - PUT /tryout/{tryout_id}/normalization: Update normalization settings - POST /tryout/{tryout_id}/normalization/reset: Reset normalization stats - GET /tryout/{tryout_id}/normalization/validate: Validate dynamic normalization """ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Header, status from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.services.config_management import ( get_normalization_config, reset_normalization_stats, toggle_normalization_mode, update_config, ) from app.services.normalization import ( validate_dynamic_normalization, ) router = APIRouter(prefix="/tryout", tags=["normalization"]) def get_website_id_from_header( x_website_id: Optional[str] = Header(None, alias="X-Website-ID"), ) -> int: """ Extract and validate website_id from request header. Args: x_website_id: Website ID from header Returns: Validated website ID as integer Raises: HTTPException: If header is missing or invalid """ if x_website_id is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="X-Website-ID header is required", ) try: return int(x_website_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="X-Website-ID must be a valid integer", ) @router.get( "/{tryout_id}/normalization", summary="Get normalization configuration", description="Retrieve current normalization configuration including mode, static values, dynamic values, and threshold status.", ) async def get_normalization_endpoint( tryout_id: str, db: AsyncSession = Depends(get_db), website_id: int = Depends(get_website_id_from_header), ): """ Get normalization configuration for a tryout. Returns: Normalization configuration with: - mode (static/dynamic/hybrid) - current rataan, sb (from TryoutStats) - static_rataan, static_sb (from Tryout config) - participant_count - threshold_status (ready for dynamic or not) Raises: HTTPException: If tryout not found """ try: config = await get_normalization_config(db, website_id, tryout_id) return config except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) @router.put( "/{tryout_id}/normalization", summary="Update normalization settings", description="Update normalization mode and static values for a tryout.", ) async def update_normalization_endpoint( tryout_id: str, normalization_mode: Optional[str] = None, static_rataan: Optional[float] = None, static_sb: Optional[float] = None, db: AsyncSession = Depends(get_db), website_id: int = Depends(get_website_id_from_header), ): """ Update normalization settings for a tryout. Args: tryout_id: Tryout identifier normalization_mode: New normalization mode (static/dynamic/hybrid) static_rataan: New static mean value static_sb: New static standard deviation db: Database session website_id: Website ID from header Returns: Updated normalization configuration Raises: HTTPException: If tryout not found or validation fails """ # Build updates dictionary updates = {} if normalization_mode is not None: if normalization_mode not in ["static", "dynamic", "hybrid"]: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid normalization_mode: {normalization_mode}. Must be 'static', 'dynamic', or 'hybrid'", ) updates["normalization_mode"] = normalization_mode if static_rataan is not None: if static_rataan <= 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="static_rataan must be greater than 0", ) updates["static_rataan"] = static_rataan if static_sb is not None: if static_sb <= 0: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="static_sb must be greater than 0", ) updates["static_sb"] = static_sb if not updates: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="No updates provided", ) try: # Update configuration await update_config(db, website_id, tryout_id, updates) # Get updated configuration config = await get_normalization_config(db, website_id, tryout_id) return config except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) @router.post( "/{tryout_id}/normalization/reset", summary="Reset normalization stats", description="Reset TryoutStats to initial values and switch to static normalization mode.", ) async def reset_normalization_endpoint( tryout_id: str, db: AsyncSession = Depends(get_db), website_id: int = Depends(get_website_id_from_header), ): """ Reset normalization stats for a tryout. Resets TryoutStats to initial values (participant_count=0, sums cleared) and temporarily switches normalization_mode to "static". Args: tryout_id: Tryout identifier db: Database session website_id: Website ID from header Returns: Success message with updated configuration Raises: HTTPException: If tryout not found """ try: stats = await reset_normalization_stats(db, website_id, tryout_id) config = await get_normalization_config(db, website_id, tryout_id) return { "message": "Normalization stats reset successfully", "tryout_id": tryout_id, "participant_count": stats.participant_count, "normalization_mode": config["normalization_mode"], } except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), ) @router.get( "/{tryout_id}/normalization/validate", summary="Validate dynamic normalization", description="Validate that dynamic normalization produces expected distribution (mean≈500±5, SD≈100±5).", ) async def validate_normalization_endpoint( tryout_id: str, target_mean: float = 500.0, target_sd: float = 100.0, mean_tolerance: float = 5.0, sd_tolerance: float = 5.0, db: AsyncSession = Depends(get_db), website_id: int = Depends(get_website_id_from_header), ): """ Validate dynamic normalization for a tryout. Checks if calculated rataan and sb are close to target values. Returns validation status, deviations, warnings, and suggestions. Args: tryout_id: Tryout identifier target_mean: Target mean (default: 500) target_sd: Target standard deviation (default: 100) mean_tolerance: Allowed deviation from target mean (default: 5) sd_tolerance: Allowed deviation from target SD (default: 5) db: Database session website_id: Website ID from header Returns: Validation result with: - is_valid: True if within tolerance - details: Full validation details Raises: HTTPException: If tryout not found """ try: is_valid, details = await validate_dynamic_normalization( db=db, website_id=website_id, tryout_id=tryout_id, target_mean=target_mean, target_sd=target_sd, mean_tolerance=mean_tolerance, sd_tolerance=sd_tolerance, ) return { "tryout_id": tryout_id, "is_valid": is_valid, "target_mean": target_mean, "target_sd": target_sd, "mean_tolerance": mean_tolerance, "sd_tolerance": sd_tolerance, "details": details, } except ValueError as e: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e), )