import asyncio import logging from uuid import UUID from fastapi import Depends, FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.availability_service import calculate_availability from app.database import get_db from app.ics_service import sync_all_calendars, sync_participant_calendar from app.models import Participant from app.schemas import ( AvailabilityRequest, AvailabilityResponse, ParticipantCreate, ParticipantResponse, SyncResponse, ScheduleRequest, ) from app.scheduler import start_scheduler, stop_scheduler from app.email_service import send_meeting_invite from app.zulip_service import send_zulip_notification from contextlib import asynccontextmanager from datetime import datetime, timezone, timedelta logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): start_scheduler() yield stop_scheduler() app = FastAPI(title="Common Availability API", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") async def health_check(): return {"status": "healthy"} @app.post("/api/participants", response_model=ParticipantResponse) async def create_participant( data: ParticipantCreate, db: AsyncSession = Depends(get_db) ): existing = await db.execute( select(Participant).where(Participant.email == data.email) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Email already registered") participant = Participant( name=data.name, email=data.email, ics_url=data.ics_url, ) db.add(participant) await db.commit() await db.refresh(participant) try: if participant.ics_url: await sync_participant_calendar(db, participant) except Exception as e: logger.warning(f"Initial sync failed for {participant.email}: {e}") return participant @app.get("/api/participants", response_model=list[ParticipantResponse]) async def list_participants(db: AsyncSession = Depends(get_db)): result = await db.execute(select(Participant)) return result.scalars().all() @app.get("/api/participants/{participant_id}", response_model=ParticipantResponse) async def get_participant(participant_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute( select(Participant).where(Participant.id == participant_id) ) participant = result.scalar_one_or_none() if not participant: raise HTTPException(status_code=404, detail="Participant not found") return participant @app.delete("/api/participants/{participant_id}") async def delete_participant(participant_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute( select(Participant).where(Participant.id == participant_id) ) participant = result.scalar_one_or_none() if not participant: raise HTTPException(status_code=404, detail="Participant not found") await db.delete(participant) await db.commit() return {"status": "deleted"} @app.post("/api/availability", response_model=AvailabilityResponse) async def get_availability( request: AvailabilityRequest, db: AsyncSession = Depends(get_db) ): slots = await calculate_availability(db, request.participant_ids) return {"slots": slots} @app.post("/api/sync", response_model=SyncResponse) async def sync_calendars(db: AsyncSession = Depends(get_db)): result = await db.execute(select(Participant)) participants = result.scalars().all() results = await sync_all_calendars(db, list(participants)) return {"results": results} @app.post("/api/sync/{participant_id}") async def sync_participant(participant_id: UUID, db: AsyncSession = Depends(get_db)): result = await db.execute( select(Participant).where(Participant.id == participant_id) ) participant = result.scalar_one_or_none() if not participant: raise HTTPException(status_code=404, detail="Participant not found") try: if participant.ics_url: count = await sync_participant_calendar(db, participant) return {"status": "success", "blocks_synced": count} return {"status": "skipped", "message": "No ICS URL provided"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/schedule") async def schedule_meeting( data: ScheduleRequest, db: AsyncSession = Depends(get_db) ): min_start_time = datetime.now(timezone.utc) + timedelta(hours=2) if data.start_time.replace(tzinfo=timezone.utc) < min_start_time: raise HTTPException( status_code=400, detail="Meetings must be scheduled at least 2 hours in advance." ) result = await db.execute( select(Participant).where(Participant.id.in_(data.participant_ids)) ) participants = result.scalars().all() if len(participants) != len(data.participant_ids): raise HTTPException(status_code=400, detail="Some participants not found") participant_dicts = [ {"name": p.name, "email": p.email} for p in participants ] email_success = await send_meeting_invite( participant_dicts, data.title, data.description, data.start_time, data.end_time ) zulip_success = await asyncio.to_thread( send_zulip_notification, data.title, data.start_time, participant_dicts ) return { "status": "success", "email_sent": email_success, "zulip_sent": zulip_success }