"""Calendar API router. BE-CAL-004: MinimumWorkload CRUD endpoints. BE-CAL-API-001: Single-slot creation endpoint. """ from datetime import date as date_type from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.api.deps import get_current_user from app.core.config import get_db from app.models.calendar import SlotStatus, TimeSlot from app.models.models import User from app.schemas.calendar import ( MinimumWorkloadConfig, MinimumWorkloadResponse, MinimumWorkloadUpdate, SlotConflictItem, TimeSlotCreate, TimeSlotCreateResponse, TimeSlotResponse, ) from app.services.minimum_workload import ( get_workload_config, get_workload_warnings_for_date, replace_workload_config, upsert_workload_config, ) from app.services.overlap import check_overlap_for_create router = APIRouter(prefix="/calendar", tags=["Calendar"]) # --------------------------------------------------------------------------- # TimeSlot creation (BE-CAL-API-001) # --------------------------------------------------------------------------- def _slot_to_response(slot: TimeSlot) -> TimeSlotResponse: """Convert a TimeSlot ORM object to a response schema.""" return TimeSlotResponse( id=slot.id, user_id=slot.user_id, date=slot.date, slot_type=slot.slot_type.value if hasattr(slot.slot_type, "value") else str(slot.slot_type), estimated_duration=slot.estimated_duration, scheduled_at=slot.scheduled_at.isoformat() if slot.scheduled_at else "", started_at=slot.started_at.isoformat() if slot.started_at else None, attended=slot.attended, actual_duration=slot.actual_duration, event_type=slot.event_type.value if slot.event_type and hasattr(slot.event_type, "value") else (str(slot.event_type) if slot.event_type else None), event_data=slot.event_data, priority=slot.priority, status=slot.status.value if hasattr(slot.status, "value") else str(slot.status), plan_id=slot.plan_id, created_at=slot.created_at, updated_at=slot.updated_at, ) @router.post( "/slots", response_model=TimeSlotCreateResponse, status_code=201, summary="Create a single calendar slot", ) def create_slot( payload: TimeSlotCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Create a one-off calendar slot. - **Overlap detection**: rejects the request if the proposed slot overlaps with existing real or virtual slots on the same day. - **Workload warnings**: after successful creation, returns any minimum-workload warnings (advisory only, does not block creation). """ target_date = payload.date or date_type.today() # --- Overlap check (hard reject) --- conflicts = check_overlap_for_create( db, user_id=current_user.id, target_date=target_date, scheduled_at=payload.scheduled_at, estimated_duration=payload.estimated_duration, ) if conflicts: raise HTTPException( status_code=409, detail={ "message": "Slot overlaps with existing schedule", "conflicts": [c.to_dict() for c in conflicts], }, ) # --- Create the slot --- slot = TimeSlot( user_id=current_user.id, date=target_date, slot_type=payload.slot_type.value, estimated_duration=payload.estimated_duration, scheduled_at=payload.scheduled_at, event_type=payload.event_type.value if payload.event_type else None, event_data=payload.event_data, priority=payload.priority, status=SlotStatus.NOT_STARTED, ) db.add(slot) db.commit() db.refresh(slot) # --- Workload warnings (advisory) --- warnings = get_workload_warnings_for_date(db, current_user.id, target_date) return TimeSlotCreateResponse( slot=_slot_to_response(slot), warnings=warnings, ) # --------------------------------------------------------------------------- # MinimumWorkload # --------------------------------------------------------------------------- @router.get( "/workload-config", response_model=MinimumWorkloadResponse, summary="Get current user's minimum workload configuration", ) def get_my_workload_config( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return the workload thresholds for the authenticated user. If no configuration has been saved yet, returns default (all-zero) thresholds. """ cfg = get_workload_config(db, current_user.id) return MinimumWorkloadResponse(user_id=current_user.id, config=cfg) @router.put( "/workload-config", response_model=MinimumWorkloadResponse, summary="Replace the current user's minimum workload configuration", ) def put_my_workload_config( payload: MinimumWorkloadConfig, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Full replacement of the workload configuration.""" row = replace_workload_config(db, current_user.id, payload) db.commit() db.refresh(row) return MinimumWorkloadResponse(user_id=current_user.id, config=row.config) @router.patch( "/workload-config", response_model=MinimumWorkloadResponse, summary="Partially update the current user's minimum workload configuration", ) def patch_my_workload_config( payload: MinimumWorkloadUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """Partial update — only the provided periods are overwritten.""" row = upsert_workload_config(db, current_user.id, payload) db.commit() db.refresh(row) return MinimumWorkloadResponse(user_id=current_user.id, config=row.config) # --------------------------------------------------------------------------- # Admin: manage another user's workload config # --------------------------------------------------------------------------- def _require_admin(current_user: User = Depends(get_current_user)): if not current_user.is_admin: raise HTTPException(status_code=403, detail="Admin required") return current_user @router.get( "/workload-config/{user_id}", response_model=MinimumWorkloadResponse, summary="[Admin] Get a specific user's minimum workload configuration", ) def get_user_workload_config( user_id: int, db: Session = Depends(get_db), _admin: User = Depends(_require_admin), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") cfg = get_workload_config(db, user_id) return MinimumWorkloadResponse(user_id=user_id, config=cfg) @router.put( "/workload-config/{user_id}", response_model=MinimumWorkloadResponse, summary="[Admin] Replace a specific user's minimum workload configuration", ) def put_user_workload_config( user_id: int, payload: MinimumWorkloadConfig, db: Session = Depends(get_db), _admin: User = Depends(_require_admin), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") row = replace_workload_config(db, user_id, payload) db.commit() db.refresh(row) return MinimumWorkloadResponse(user_id=user_id, config=row.config) @router.patch( "/workload-config/{user_id}", response_model=MinimumWorkloadResponse, summary="[Admin] Partially update a specific user's minimum workload configuration", ) def patch_user_workload_config( user_id: int, payload: MinimumWorkloadUpdate, db: Session = Depends(get_db), _admin: User = Depends(_require_admin), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="User not found") row = upsert_workload_config(db, user_id, payload) db.commit() db.refresh(row) return MinimumWorkloadResponse(user_id=user_id, config=row.config)