import {
CalendarEvent,
Calendar,
TimeSlot,
CreateEventInput,
FindFreeTimeInput,
ApiResponse,
User,
EventSearchCriteria,
UserPreferences
} from '../types';
import { CalendarProvider, ProviderFactory } from '../providers/CalendarProvider';
import { CacheManager } from '../cache';
import { DatabaseManager } from '../database';
export interface MeetingSuggestionInput {
attendeeEmails: string[];
durationMinutes: number;
dateRangeStart: Date;
dateRangeEnd: Date;
constraints?: {
preferredTimes?: Array<{
startTime: string;
endTime: string;
preference: 'preferred' | 'acceptable' | 'avoid';
}>;
excludeWeekends?: boolean;
minimumNoticePeriod?: number;
};
maxSuggestions?: number;
}
export interface SchedulePattern {
averageEventsPerDay: number;
busiestDayOfWeek: string;
mostActiveHours: string[];
averageMeetingDuration: number;
frequentAttendees: string[];
commonLocations: string[];
peakSchedulingDays: string[];
}
export class CalendarService {
constructor(
private cache: CacheManager,
private database: DatabaseManager
) {}
async getCalendars(userId: string): Promise<ApiResponse<Calendar[]>> {
try {
// Check cache first
const cachedCalendars = await this.cache.getCalendars(userId);
if (cachedCalendars) {
return { success: true, data: cachedCalendars };
}
// Get user and provider
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.getCalendars();
if (response.success && response.data) {
// Cache the result
await this.cache.setCalendars(userId, response.data);
}
return response;
} catch (error) {
return this.handleError('GET_CALENDARS_FAILED', error);
}
}
async getEvents(
userId: string,
calendarId: string,
startDate: Date,
endDate: Date,
query?: string
): Promise<ApiResponse<CalendarEvent[]>> {
try {
// Check cache first (only if no query - queries are too dynamic to cache effectively)
if (!query) {
const cachedEvents = await this.cache.getEvents(userId, calendarId, startDate, endDate);
if (cachedEvents) {
return { success: true, data: cachedEvents };
}
}
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.getEvents(calendarId, startDate, endDate, query);
if (response.success && response.data && !query) {
// Cache the result (only for non-query requests)
await this.cache.setEvents(userId, calendarId, startDate, endDate, response.data);
}
return response;
} catch (error) {
return this.handleError('GET_EVENTS_FAILED', error);
}
}
async createEvent(
userId: string,
eventData: CreateEventInput
): Promise<ApiResponse<CalendarEvent>> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.createEvent(eventData.calendarId, eventData);
if (response.success) {
// Invalidate cached events for this calendar
await this.cache.invalidateEvents(userId, eventData.calendarId);
}
return response;
} catch (error) {
return this.handleError('CREATE_EVENT_FAILED', error);
}
}
async updateEvent(
userId: string,
calendarId: string,
eventId: string,
updates: Partial<CreateEventInput>
): Promise<ApiResponse<CalendarEvent>> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.updateEvent(calendarId, eventId, updates);
if (response.success) {
// Invalidate cached events for this calendar
await this.cache.invalidateEvents(userId, calendarId);
}
return response;
} catch (error) {
return this.handleError('UPDATE_EVENT_FAILED', error);
}
}
async deleteEvent(
userId: string,
calendarId: string,
eventId: string
): Promise<ApiResponse<boolean>> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.deleteEvent(calendarId, eventId);
if (response.success) {
// Invalidate cached events for this calendar
await this.cache.invalidateEvents(userId, calendarId);
}
return response;
} catch (error) {
return this.handleError('DELETE_EVENT_FAILED', error);
}
}
async findFreeTime(
userId: string,
criteria: FindFreeTimeInput
): Promise<ApiResponse<TimeSlot[]>> {
try {
// Create cache key from criteria
const cacheKey = this.generateFreeTimeCacheKey(criteria);
const cachedSlots = await this.cache.getFreeTime(userId, cacheKey);
if (cachedSlots) {
return { success: true, data: cachedSlots };
}
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
const response = await provider.findFreeTime(criteria);
if (response.success && response.data) {
// Cache the result
await this.cache.setFreeTime(userId, cacheKey, response.data);
}
return response;
} catch (error) {
return this.handleError('FIND_FREE_TIME_FAILED', error);
}
}
async checkAvailability(
userId: string,
emailAddresses: string[],
startTime: Date,
endTime: Date
): Promise<ApiResponse<Record<string, boolean>>> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const provider = await this.getProviderForUser(user);
return await provider.checkAvailability(emailAddresses, startTime, endTime);
} catch (error) {
return this.handleError('CHECK_AVAILABILITY_FAILED', error);
}
}
async suggestMeetingTimes(
userId: string,
input: MeetingSuggestionInput
): Promise<ApiResponse<TimeSlot[]>> {
try {
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
// Get all attendee calendars (including organizer)
const allAttendees = [...input.attendeeEmails, user.email];
// Find free time considering all attendees
const freeTimeResponse = await this.findFreeTime(userId, {
calendarIds: [user.preferences.defaultCalendarId || 'primary'],
startDate: input.dateRangeStart.toISOString(),
endDate: input.dateRangeEnd.toISOString(),
durationMinutes: input.durationMinutes,
constraints: input.constraints ? {
excludeWeekends: input.constraints.excludeWeekends,
minimumNoticePeriod: input.constraints.minimumNoticePeriod
} : undefined,
maxResults: (input.maxSuggestions || 5) * 2 // Get more to filter later
});
if (!freeTimeResponse.success || !freeTimeResponse.data) {
return freeTimeResponse;
}
// Check availability for each suggested slot
const availabilityChecks = await Promise.all(
freeTimeResponse.data.map(async (slot) => {
const availabilityResponse = await this.checkAvailability(
userId,
input.attendeeEmails,
slot.start,
slot.end
);
const allAvailable = availabilityResponse.success &&
Object.values(availabilityResponse.data || {}).every(available => available);
return { slot, allAvailable };
})
);
// Filter to only slots where everyone is available
const availableSlots = availabilityChecks
.filter(check => check.allAvailable)
.map(check => check.slot);
// Apply time preferences if specified
let scoredSlots = availableSlots;
if (input.constraints?.preferredTimes) {
scoredSlots = this.scoreSlotsByPreferences(availableSlots, input.constraints.preferredTimes);
}
// Limit to requested number of suggestions
const limitedSlots = scoredSlots.slice(0, input.maxSuggestions || 5);
return {
success: true,
data: limitedSlots
};
} catch (error) {
return this.handleError('SUGGEST_MEETING_TIMES_FAILED', error);
}
}
async analyzeSchedulePatterns(
userId: string,
calendarId: string,
daysBack: number = 30
): Promise<ApiResponse<SchedulePattern>> {
try {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const eventsResponse = await this.getEvents(userId, calendarId, startDate, endDate);
if (!eventsResponse.success || !eventsResponse.data) {
return eventsResponse as ApiResponse<SchedulePattern>;
}
const events = eventsResponse.data;
const analysis = this.calculateSchedulePatterns(events, daysBack);
return {
success: true,
data: analysis
};
} catch (error) {
return this.handleError('ANALYZE_SCHEDULE_PATTERNS_FAILED', error);
}
}
async getWorkingHours(userId: string, calendarId: string): Promise<ApiResponse<any>> {
try {
const preferences = await this.cache.getUserPreferences(userId);
if (preferences?.workingHours) {
return {
success: true,
data: preferences.workingHours
};
}
// Default working hours if not set
const defaultWorkingHours = {
monday: { startTime: '09:00', endTime: '17:00' },
tuesday: { startTime: '09:00', endTime: '17:00' },
wednesday: { startTime: '09:00', endTime: '17:00' },
thursday: { startTime: '09:00', endTime: '17:00' },
friday: { startTime: '09:00', endTime: '17:00' }
};
return {
success: true,
data: defaultWorkingHours
};
} catch (error) {
return this.handleError('GET_WORKING_HOURS_FAILED', error);
}
}
async getRecentSimilarEvents(
userId: string,
query: string,
limit: number = 5,
daysBack: number = 90
): Promise<ApiResponse<CalendarEvent[]>> {
try {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
// Get user's default calendar
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const calendarId = user.preferences.defaultCalendarId || 'primary';
const eventsResponse = await this.getEvents(userId, calendarId, startDate, endDate, query);
if (!eventsResponse.success || !eventsResponse.data) {
return eventsResponse;
}
// Sort by relevance and recency, limit results
const sortedEvents = eventsResponse.data
.sort((a, b) => b.created.getTime() - a.created.getTime())
.slice(0, limit);
return {
success: true,
data: sortedEvents
};
} catch (error) {
return this.handleError('GET_RECENT_SIMILAR_EVENTS_FAILED', error);
}
}
async getContactSuggestions(
userId: string,
partialName: string,
limit: number = 10
): Promise<ApiResponse<string[]>> {
try {
// Check cache first
const cachedSuggestions = await this.cache.getContactSuggestions(userId, partialName);
if (cachedSuggestions) {
return { success: true, data: cachedSuggestions };
}
// Get recent attendees from past events
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 365); // Look back 1 year
const user = await this.database.getUser(userId);
if (!user) {
return { success: false, error: { code: 'USER_NOT_FOUND', message: 'User not found' } };
}
const calendarId = user.preferences.defaultCalendarId || 'primary';
const eventsResponse = await this.getEvents(userId, calendarId, startDate, endDate);
if (!eventsResponse.success || !eventsResponse.data) {
return { success: true, data: [] }; // Return empty if no events
}
// Extract unique attendee emails that match the partial name
const attendeeEmails = new Set<string>();
const searchLower = partialName.toLowerCase();
eventsResponse.data.forEach(event => {
event.attendees?.forEach(attendee => {
const email = attendee.email.toLowerCase();
const displayName = (attendee.displayName || '').toLowerCase();
if (email.includes(searchLower) || displayName.includes(searchLower)) {
attendeeEmails.add(attendee.email);
}
});
});
const suggestions = Array.from(attendeeEmails).slice(0, limit);
// Cache the results
await this.cache.setContactSuggestions(userId, partialName, suggestions);
return {
success: true,
data: suggestions
};
} catch (error) {
return this.handleError('GET_CONTACT_SUGGESTIONS_FAILED', error);
}
}
// Private helper methods
private async getProviderForUser(user: User): Promise<CalendarProvider> {
const primaryCalendar = user.connectedCalendars.find(cal => cal.isDefault);
if (!primaryCalendar) {
throw new Error('No connected calendar found for user');
}
return await ProviderFactory.createProvider(
primaryCalendar.provider,
primaryCalendar.accessToken,
primaryCalendar.refreshToken,
primaryCalendar.tokenExpiry
);
}
private generateFreeTimeCacheKey(criteria: FindFreeTimeInput): string {
return `${criteria.calendarIds.join(',')}_${criteria.startDate}_${criteria.endDate}_${criteria.durationMinutes}_${JSON.stringify(criteria.constraints || {})}`;
}
private scoreSlotsByPreferences(
slots: TimeSlot[],
preferences: Array<{
startTime: string;
endTime: string;
preference: 'preferred' | 'acceptable' | 'avoid';
}>
): TimeSlot[] {
return slots
.map(slot => {
const slotStartTime = slot.start.toTimeString().slice(0, 5);
const slotEndTime = slot.end.toTimeString().slice(0, 5);
let score = 0;
for (const pref of preferences) {
if (slotStartTime >= pref.startTime && slotEndTime <= pref.endTime) {
switch (pref.preference) {
case 'preferred': score += 10; break;
case 'acceptable': score += 5; break;
case 'avoid': score -= 10; break;
}
}
}
return { slot, score };
})
.sort((a, b) => b.score - a.score)
.map(item => item.slot);
}
private calculateSchedulePatterns(events: CalendarEvent[], daysBack: number): SchedulePattern {
const dayOfWeekCounts: Record<string, number> = {
Sunday: 0, Monday: 0, Tuesday: 0, Wednesday: 0,
Thursday: 0, Friday: 0, Saturday: 0
};
const hourCounts: Record<number, number> = {};
const attendeeCounts: Record<string, number> = {};
const locationCounts: Record<string, number> = {};
const durations: number[] = [];
events.forEach(event => {
// Day of week analysis
const dayName = event.startTime.toLocaleDateString('en-US', { weekday: 'long' });
dayOfWeekCounts[dayName]++;
// Hour analysis
const hour = event.startTime.getHours();
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
// Duration analysis
const duration = (event.endTime.getTime() - event.startTime.getTime()) / (1000 * 60);
durations.push(duration);
// Attendee analysis
event.attendees?.forEach(attendee => {
attendeeCounts[attendee.email] = (attendeeCounts[attendee.email] || 0) + 1;
});
// Location analysis
if (event.location) {
locationCounts[event.location] = (locationCounts[event.location] || 0) + 1;
}
});
// Calculate results
const busiestDay = Object.entries(dayOfWeekCounts)
.sort(([,a], [,b]) => b - a)[0][0];
const mostActiveHours = Object.entries(hourCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([hour]) => `${hour}:00`);
const frequentAttendees = Object.entries(attendeeCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([email]) => email);
const commonLocations = Object.entries(locationCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([location]) => location);
return {
averageEventsPerDay: events.length / daysBack,
busiestDayOfWeek: busiestDay,
mostActiveHours,
averageMeetingDuration: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0,
frequentAttendees,
commonLocations,
peakSchedulingDays: Object.entries(dayOfWeekCounts)
.filter(([,count]) => count > events.length / 7) // Above average
.map(([day]) => day)
};
}
private handleError(code: string, error: any): ApiResponse<any> {
console.error(`Calendar Service Error [${code}]:`, error);
return {
success: false,
error: {
code,
message: error instanceof Error ? error.message : 'Unknown error occurred',
details: { originalError: error }
}
};
}
}