import { NextRequest, NextResponse } from 'next/server'; export const dynamic = 'force-dynamic'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/lib/auth"; import { prisma } from '@/lib/prisma'; import { readCachedEvents, isCacheStale, refreshConnectionCache, RefreshableConnection } from '@/lib/calendar-cache'; import { runSyncRules } from '@/lib/calendar-cross-sync'; import { safeErrorMessage } from '@/lib/api-error'; export async function POST(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); const { timeMin, timeMax, connectionId, forceRefresh } = body; const timeMinDate = new Date(timeMin ?? Date.now()); const timeMaxDate = new Date(timeMax ?? Date.now() + 7 * 24 * 60 * 60 * 1000); // Look up by email — session ID can be stale after DB restore/migration const user = await prisma.user.findUnique({ where: { email: session.user.email }, include: { calendarConnections: true }, }); if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } let connections = user.calendarConnections; if (connectionId) { connections = connections.filter(c => c.id === connectionId); } if (connections.length === 0) { return NextResponse.json({ success: true, events: [], fromCache: true }); } // FAST PATH: read from DB cache const cachedEvents = await readCachedEvents(user.id, timeMinDate, timeMaxDate); // Check which connections are stale const staleChecks = await Promise.all( connections.map(async conn => ({ conn, stale: forceRefresh || await isCacheStale(conn.id, timeMinDate), })) ); const staleConnections = staleChecks.filter(c => c.stale).map(c => c.conn); // Refresh stale connections if (staleConnections.length > 0) { const doRefresh = () => Promise.allSettled( staleConnections.map(conn => { const rc: RefreshableConnection = { dbId: conn.id, userId: user.id, id: conn.id, provider: conn.provider as 'google' | 'apple' | 'outlook' | 'synology', accessToken: conn.accessToken, refreshToken: conn.refreshToken ?? undefined, expiresAt: conn.expiresAt ?? undefined, calendars: conn.calendars as any, }; return refreshConnectionCache(rc, timeMinDate, timeMaxDate); }) ); if (forceRefresh) { // BLOCKING: wait for fresh data when user explicitly requests refresh await doRefresh(); // Run cross-sync rules after cache refresh (fire-and-forget, don't block response) runSyncRules(user.id).catch(e => console.error('[cross-sync] Error running sync rules:', e)); const freshEvents = await readCachedEvents(user.id, timeMinDate, timeMaxDate); return NextResponse.json({ success: true, events: freshEvents, count: freshEvents.length, fromCache: false, staleConnectionCount: 0, }); } else { // BACKGROUND: try to refresh within a short window; fall back to stale cache const refreshWork = doRefresh().catch(e => console.error('[CACHE] Background refresh error:', e)); const timeout = new Promise(resolve => setTimeout(resolve, 800)); await Promise.race([refreshWork, timeout]); const maybeRefreshed = await readCachedEvents(user.id, timeMinDate, timeMaxDate); return NextResponse.json({ success: true, events: maybeRefreshed, count: maybeRefreshed.length, fromCache: true, staleConnectionCount: staleConnections.length, }); } } return NextResponse.json({ success: true, events: cachedEvents, count: cachedEvents.length, fromCache: true, staleConnectionCount: 0, }); } catch (error) { console.error('[CALENDAR SYNC] Sync request failed:', error); return NextResponse.json({ error: 'Sync failed', details: safeErrorMessage(error, 'Unknown error') }, { status: 500 }); } }