using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using SqlKata; using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Services.Extensions; namespace Wino.Services; public class CalendarServiceEx : BaseDatabaseService, ICalendarServiceEx { public CalendarServiceEx(IDatabaseService databaseService) : base(databaseService) { } public async Task> GetAllEventsAsync() { return await Connection.Table() .Where(e => !e.IsDeleted) .ToListAsync(); } /// /// Gets all events from the database including soft-deleted ones /// /// List of all events including deleted ones public async Task> GetAllEventsIncludingDeletedAsync() { return await Connection.Table().ToListAsync(); } public async Task GetEventByRemoteIdAsync(string remoteEventId) { return await Connection.Table() .Where(e => e.RemoteEventId == remoteEventId && !e.IsDeleted) .FirstOrDefaultAsync(); } public async Task> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate) { return await Connection.Table() .Where(e => e.StartDateTime >= startDate && e.StartDateTime <= endDate && !e.IsDeleted) .ToListAsync(); } public async Task> GetRecurringEventsAsync() { return await Connection.Table() .Where(e => !string.IsNullOrEmpty(e.RecurrenceRules) && !e.IsDeleted) .ToListAsync(); } public async Task InsertEventAsync(CalendarItem calendarItem) { calendarItem.Id = Guid.NewGuid(); calendarItem.CreatedDate = DateTime.UtcNow; calendarItem.LastModified = DateTime.UtcNow; return await Connection.InsertAsync(calendarItem); } public async Task UpdateEventAsync(CalendarItem calendarItem) { calendarItem.LastModified = DateTime.UtcNow; return await Connection.UpdateAsync(calendarItem); } public async Task UpsertEventAsync(CalendarItem calendarItem) { var existingEvent = await GetEventByRemoteIdAsync(calendarItem.RemoteEventId); if (existingEvent != null) { calendarItem.Id = existingEvent.Id; calendarItem.CreatedDate = existingEvent.CreatedDate; return await UpdateEventAsync(calendarItem); } else { return await InsertEventAsync(calendarItem); } } public async Task DeleteEventAsync(string remoteEventId) { var existingEvent = await GetEventByRemoteIdAsync(remoteEventId); if (existingEvent != null) { existingEvent.IsDeleted = true; existingEvent.LastModified = DateTime.UtcNow; return await UpdateEventAsync(existingEvent); } return 0; } public async Task HardDeleteEventAsync(string remoteEventId) { return await Connection.Table() .DeleteAsync(e => e.RemoteEventId == remoteEventId); } public async Task> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime) { if (lastSyncTime == null) { return await GetAllEventsAsync(); } return await Connection.Table() .Where(e => e.LastModified > lastSyncTime && !e.IsDeleted) .ToListAsync(); } public async Task ClearAllEventsAsync() { return await Connection.DeleteAllAsync(); } // Calendar management methods public async Task> GetAllCalendarsAsync() { return await Connection.Table() .Where(c => !c.IsDeleted) .ToListAsync(); } public async Task GetCalendarByRemoteIdAsync(string remoteCalendarId) { return await Connection.Table() .Where(c => c.RemoteCalendarId == remoteCalendarId && !c.IsDeleted) .FirstOrDefaultAsync(); } public async Task InsertCalendarAsync(AccountCalendar calendar) { calendar.Id = Guid.NewGuid(); calendar.CreatedDate = DateTime.UtcNow; calendar.LastModified = DateTime.UtcNow; return await Connection.InsertAsync(calendar); } public async Task UpdateCalendarAsync(AccountCalendar calendar) { calendar.LastModified = DateTime.UtcNow; return await Connection.UpdateAsync(calendar); } public async Task UpsertCalendarAsync(AccountCalendar calendar) { var existingCalendar = await GetCalendarByRemoteIdAsync(calendar.RemoteCalendarId); if (existingCalendar != null) { calendar.Id = existingCalendar.Id; calendar.CreatedDate = existingCalendar.CreatedDate; return await UpdateCalendarAsync(calendar); } else { return await InsertCalendarAsync(calendar); } } public async Task DeleteCalendarAsync(string remoteCalendarId) { var existingCalendar = await GetCalendarByRemoteIdAsync(remoteCalendarId); if (existingCalendar != null) { existingCalendar.IsDeleted = true; existingCalendar.LastModified = DateTime.UtcNow; return await UpdateCalendarAsync(existingCalendar); } return 0; } /// /// Gets events for a specific calendar by internal Guid /// /// The internal Guid of the calendar /// List of events for the specified calendar public async Task> GetEventsForCalendarAsync(Guid calendarId) { return await Connection.Table() .Where(e => e.CalendarId == calendarId && !e.IsDeleted) .ToListAsync(); } /// /// Gets events for a specific calendar by Remote Calendar ID /// /// The Remote Calendar ID /// List of events for the specified calendar public async Task> GetEventsByremoteCalendarIdAsync(string remoteCalendarId) { // First get the calendar to find its internal Guid var calendar = await GetCalendarByRemoteIdAsync(remoteCalendarId); if (calendar == null) { return new List(); } return await GetEventsForCalendarAsync(calendar.Id); } public async Task ClearAllCalendarsAsync() { return await Connection.DeleteAllAsync(); } public async Task ClearAllDataAsync() { var calendarCount = await Connection.DeleteAllAsync(); var eventCount = await Connection.DeleteAllAsync(); var calendareventattendeeCount = await Connection.DeleteAllAsync(); return calendarCount + eventCount + calendareventattendeeCount; } /// /// Gets all events (including expanded recurring event instances) within a date range /// /// Start date of the range /// End date of the range /// List of events including expanded recurring instances public async Task> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate) { var allEvents = new List(); // Get all non-recurring events in the date range var oneTimeEvents = await Connection.Table() .Where(e => !e.IsDeleted && (string.IsNullOrEmpty(e.RecurrenceRules) || e.RecurrenceRules == "") && e.StartDateTime >= startDate && e.StartDateTime <= endDate) .ToListAsync(); allEvents.AddRange(oneTimeEvents); // Get all recurring events var recurringEvents = await Connection.Table() .Where(e => !e.IsDeleted && !string.IsNullOrEmpty(e.RecurrenceRules) && e.RecurrenceRules != "") .ToListAsync(); // Expand recurring events foreach (var recurringEvent in recurringEvents) { var expandedInstances = ExpandRecurringEvent(recurringEvent, startDate, endDate); allEvents.AddRange(expandedInstances); } // Sort by start date and return return allEvents.OrderBy(e => e.StartDateTime).ToList(); } /// /// Expands a recurring event into individual instances within the specified date range /// /// The recurring event to expand /// Start of the date range /// End of the date range /// List of event instances private List ExpandRecurringEvent(CalendarItem recurringEvent, DateTime rangeStart, DateTime rangeEnd) { var instances = new List(); if (string.IsNullOrEmpty(recurringEvent.RecurrenceRules)) return instances; try { var recurrenceRules = recurringEvent.RecurrenceRules.Split(';'); var rrule = recurrenceRules.FirstOrDefault(r => r.StartsWith("RRULE:")); if (string.IsNullOrEmpty(rrule)) return instances; // Parse RRULE var ruleData = ParseRRule(rrule.Substring(6)); // Remove "RRULE:" prefix if (ruleData == null || !ruleData.ContainsKey("FREQ")) return instances; var frequency = ruleData["FREQ"]; var interval = ruleData.ContainsKey("INTERVAL") ? int.Parse(ruleData["INTERVAL"]) : 1; var count = ruleData.ContainsKey("COUNT") ? int.Parse(ruleData["COUNT"]) : (int?)null; var until = ruleData.ContainsKey("UNTIL") ? ParseUntilDate(ruleData["UNTIL"]) : (DateTime?)null; // Calculate event duration var duration = recurringEvent.EndDateTime - recurringEvent.StartDateTime; // Start from the original event date var currentDate = recurringEvent.StartDateTime; var instanceCount = 0; var maxInstances = count ?? 1000; // Limit to prevent infinite loops // Generate instances while (instanceCount < maxInstances && currentDate <= rangeEnd && (until == null || currentDate <= until)) { // Check if this instance falls within our range if (currentDate >= rangeStart && currentDate <= rangeEnd) { var instance = CreateEventInstance(recurringEvent, currentDate, duration, instanceCount); instances.Add(instance); } // Move to next occurrence based on frequency currentDate = GetNextOccurrence(currentDate, frequency, interval, ruleData); instanceCount++; // Safety check to prevent infinite loops if (instanceCount > 10000) break; } } catch (Exception ex) { // Log error but don't crash - return empty list Console.WriteLine($"Error expanding recurring event {recurringEvent.RemoteEventId}: {ex.Message}"); } return instances; } /// /// Parses an RRULE string into a dictionary of key-value pairs /// /// The RRULE string (without RRULE: prefix) /// Dictionary of rule parameters private Dictionary ParseRRule(string rrule) { try { var ruleData = new Dictionary(); var parts = rrule.Split(';'); foreach (var part in parts) { var keyValue = part.Split('='); if (keyValue.Length == 2) { ruleData[keyValue[0].Trim()] = keyValue[1].Trim(); } } return ruleData; } catch { return null; } } /// /// Parses UNTIL date from RRULE /// /// UNTIL date string /// Parsed DateTime or null private DateTime? ParseUntilDate(string untilString) { try { // Handle different UNTIL formats if (untilString.Length == 8) // YYYYMMDD { return DateTime.ParseExact(untilString, "yyyyMMdd", null); } else if (untilString.Length == 15 && untilString.EndsWith("Z")) // YYYYMMDDTHHMMSSZ { return DateTime.ParseExact(untilString, "yyyyMMddTHHmmssZ", null); } else if (untilString.Length == 16) // YYYYMMDDTHHMMSSZ without Z { return DateTime.ParseExact(untilString.Substring(0, 15), "yyyyMMddTHHmmss", null); } } catch { // Return null if parsing fails } return null; } /// /// Creates an instance of a recurring event /// /// The original recurring event /// Start time for this instance /// Duration of the event /// Instance number /// Event instance private CalendarItem CreateEventInstance(CalendarItem originalEvent, DateTime instanceStart, TimeSpan duration, int instanceNumber) { return new CalendarItem { Id = Guid.NewGuid(), RemoteEventId = $"{originalEvent.RemoteEventId}_instance_{instanceNumber}", CalendarId = originalEvent.CalendarId, Title = originalEvent.Title, Description = originalEvent.Description, Location = originalEvent.Location, StartDateTime = instanceStart, EndDateTime = instanceStart + duration, IsAllDay = originalEvent.IsAllDay, TimeZone = originalEvent.TimeZone, RecurrenceRules = "", // Instances don't have recurrence rules Status = originalEvent.Status, OrganizerDisplayName = originalEvent.OrganizerDisplayName, OrganizerEmail = originalEvent.OrganizerEmail, CreatedDate = originalEvent.CreatedDate, LastModified = originalEvent.LastModified, IsDeleted = false, RecurringEventId = originalEvent.RemoteEventId, OriginalStartTime = instanceStart.ToString("O"), HtmlLink = originalEvent.HtmlLink, ItemType = originalEvent.ItemType }; } /// /// Calculates the next occurrence based on frequency and interval /// /// Current occurrence date /// Frequency (DAILY, WEEKLY, MONTHLY, YEARLY) /// Interval between occurrences /// Additional rule data /// Next occurrence date private DateTime GetNextOccurrence(DateTime currentDate, string frequency, int interval, Dictionary ruleData) { switch (frequency.ToUpperInvariant()) { case "DAILY": return currentDate.AddDays(interval); case "WEEKLY": // Handle BYDAY for weekly recurrence if (ruleData.ContainsKey("BYDAY")) { var byDays = ruleData["BYDAY"].Split(','); return GetNextWeeklyOccurrence(currentDate, interval, byDays); } return currentDate.AddDays(7 * interval); case "MONTHLY": // Handle BYMONTHDAY and BYDAY for monthly recurrence if (ruleData.ContainsKey("BYMONTHDAY")) { var monthDay = int.Parse(ruleData["BYMONTHDAY"]); return GetNextMonthlyByMonthDay(currentDate, interval, monthDay); } else if (ruleData.ContainsKey("BYDAY")) { return GetNextMonthlyByDay(currentDate, interval, ruleData["BYDAY"]); } return currentDate.AddMonths(interval); case "YEARLY": return currentDate.AddYears(interval); default: return currentDate.AddDays(interval); // Default to daily } } /// /// Gets next weekly occurrence considering BYDAY rule /// private DateTime GetNextWeeklyOccurrence(DateTime currentDate, int interval, string[] byDays) { var dayMap = new Dictionary { {"SU", DayOfWeek.Sunday}, {"MO", DayOfWeek.Monday}, {"TU", DayOfWeek.Tuesday}, {"WE", DayOfWeek.Wednesday}, {"TH", DayOfWeek.Thursday}, {"FR", DayOfWeek.Friday}, {"SA", DayOfWeek.Saturday} }; var targetDays = byDays.Where(d => dayMap.ContainsKey(d)).Select(d => dayMap[d]).OrderBy(d => d).ToList(); if (!targetDays.Any()) return currentDate.AddDays(7 * interval); var currentDayOfWeek = currentDate.DayOfWeek; var nextDay = targetDays.FirstOrDefault(d => d > currentDayOfWeek); if (nextDay != default(DayOfWeek)) { // Next occurrence is later this week var daysToAdd = (int)nextDay - (int)currentDayOfWeek; return currentDate.AddDays(daysToAdd); } else { // Next occurrence is next week var daysToAdd = (7 * interval) - (int)currentDayOfWeek + (int)targetDays.First(); return currentDate.AddDays(daysToAdd); } } /// /// Gets next monthly occurrence by month day /// private DateTime GetNextMonthlyByMonthDay(DateTime currentDate, int interval, int monthDay) { var nextMonth = currentDate.AddMonths(interval); var daysInMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month); var targetDay = Math.Min(monthDay, daysInMonth); return new DateTime(nextMonth.Year, nextMonth.Month, targetDay, currentDate.Hour, currentDate.Minute, currentDate.Second); } /// /// Gets next monthly occurrence by day (e.g., first Monday, last Friday) /// private DateTime GetNextMonthlyByDay(DateTime currentDate, int interval, string byDay) { // This is a simplified implementation // Full implementation would handle patterns like "1MO" (first Monday), "-1FR" (last Friday) return currentDate.AddMonths(interval); } /// /// Gets all events (including expanded recurring event instances) within a date range with proper exception handling /// /// Start date of the range /// End date of the range /// List of events including expanded recurring instances, excluding canceled exceptions public async Task> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar) { var allEvents = new List(); var gg = Guid.Parse("c10b83b0-9423-4d26-82d3-34b63d2e1297"); var tt = await Connection.Table().Where(a => a.Id == gg).FirstOrDefaultAsync(); var type = tt.ItemType; // Get all non-recurring events in the date range var oneTimeEventsQuery = new Query() .From(nameof(CalendarItem)) .Where(nameof(CalendarItem.IsDeleted), false) .Where(q => q.WhereNull(nameof(CalendarItem.RecurrenceRules)).OrWhere(nameof(CalendarItem.RecurrenceRules), "")) .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), "")) .Where(nameof(CalendarItem.StartDateTime), ">=", startDate) .Where(nameof(CalendarItem.StartDateTime), "<=", endDate) .Where(nameof(CalendarItem.CalendarId), calendar.Id); var oneTimeEvents = await Connection.QueryAsync(oneTimeEventsQuery.GetRawQuery()); allEvents.AddRange(oneTimeEvents); // Get all recurring events (master events only) var recurringEventsQuery = new Query() .From(nameof(CalendarItem)) .Where(nameof(CalendarItem.IsDeleted), false) .WhereNotNull(nameof(CalendarItem.RecurrenceRules)) .Where(nameof(CalendarItem.RecurrenceRules), "!=", "") .Where(q => q.WhereNull(nameof(CalendarItem.RecurringEventId)).OrWhere(nameof(CalendarItem.RecurringEventId), "")) .Where(nameof(CalendarItem.CalendarId), calendar.Id); var recurringEvents = await Connection.QueryAsync(recurringEventsQuery.GetRawQuery()); // Get all exception instances (modified or moved instances) var exceptionInstancesQuery = new Query() .From(nameof(CalendarItem)) .Where(nameof(CalendarItem.IsDeleted), false) .Where(nameof(CalendarItem.CalendarId), calendar.Id) .WhereNotNull(nameof(CalendarItem.RecurringEventId)) .Where(nameof(CalendarItem.RecurringEventId), "!=", "") .Where(nameof(CalendarItem.StartDateTime), ">=", startDate) .Where(nameof(CalendarItem.StartDateTime), "<=", endDate); var exceptionInstances = await Connection.QueryAsync(exceptionInstancesQuery.GetRawQuery()); // Get all canceled instances (marked as deleted but with RecurringEventId) var canceledInstancesQuery = new Query() .From(nameof(CalendarItem)) .Where(nameof(CalendarItem.IsDeleted), true) .Where(nameof(CalendarItem.CalendarId), calendar.Id) .WhereNotNull(nameof(CalendarItem.RecurringEventId)) .Where(nameof(CalendarItem.RecurringEventId), "!=", "") .WhereNotNull(nameof(CalendarItem.OriginalStartTime)) .Where(nameof(CalendarItem.OriginalStartTime), "!=", ""); var canceledInstances = await Connection.QueryAsync(canceledInstancesQuery.GetRawQuery()); // Group exceptions and cancellations by their parent recurring event var exceptionsByParent = exceptionInstances .Where(e => !string.IsNullOrEmpty(e.RecurringEventId)) .GroupBy(e => e.RecurringEventId!) .ToDictionary(g => g.Key, g => g.ToList()); var canceledInstancesByParent = canceledInstances .Where(e => !string.IsNullOrEmpty(e.RecurringEventId)) .GroupBy(e => e.RecurringEventId!) .ToDictionary(g => g.Key, g => g.ToList()); // Expand recurring events with exception handling foreach (var recurringEvent in recurringEvents) { var exceptions = exceptionsByParent.GetValueOrDefault(recurringEvent.RemoteEventId, new List()); var canceled = canceledInstancesByParent.GetValueOrDefault(recurringEvent.RemoteEventId, new List()); var expandedInstances = ExpandRecurringEventWithExceptions(recurringEvent, startDate, endDate, exceptions, canceled); allEvents.AddRange(expandedInstances); } // Add the exception instances (modified/moved instances) to the final list allEvents.AddRange(exceptionInstances); // allEvents[0].DetermineItemType(); // Sort by start date and return return allEvents.OrderBy(e => e.StartDateTime).ToList(); } /// /// Expands a recurring event into individual instances within the specified date range, with handling for exceptions and cancellations /// /// The recurring event to expand /// Start of the date range /// End of the date range /// List of modified instances for this recurring event /// List of canceled instances for this recurring event /// List of event instances excluding canceled ones private List ExpandRecurringEventWithExceptions( CalendarItem recurringEvent, DateTime rangeStart, DateTime rangeEnd, List exceptions, List canceled) { var instances = new List(); if (string.IsNullOrEmpty(recurringEvent.RecurrenceRules)) return instances; try { // Parse EXDATE (exception dates) from recurrence rules var exceptionDates = ParseExceptionDates(recurringEvent.RecurrenceRules); // Create sets of canceled and modified dates for quick lookup var canceledDates = new HashSet(); var modifiedDates = new HashSet(); // Add canceled instances to the set foreach (var canceledInstance in canceled) { if (!string.IsNullOrEmpty(canceledInstance.OriginalStartTime)) { if (DateTime.TryParse(canceledInstance.OriginalStartTime, out var originalDate)) { canceledDates.Add(originalDate.Date); } } } // Add modified instances to the set foreach (var exception in exceptions) { if (!string.IsNullOrEmpty(exception.OriginalStartTime)) { if (DateTime.TryParse(exception.OriginalStartTime, out var originalDate)) { modifiedDates.Add(originalDate.Date); } } } // Generate base instances using existing logic var baseInstances = ExpandRecurringEvent(recurringEvent, rangeStart, rangeEnd); // Filter out canceled, modified, and EXDATE instances foreach (var instance in baseInstances) { var instanceDate = instance.StartDateTime.Date; // Skip if this instance is canceled if (canceledDates.Contains(instanceDate)) { Console.WriteLine($"Skipping canceled instance: {instance.Title} on {instanceDate:MM/dd/yyyy}"); continue; } // Skip if this instance has been modified (the modified version will be added separately) if (modifiedDates.Contains(instanceDate)) { Console.WriteLine($"Skipping modified instance: {instance.Title} on {instanceDate:MM/dd/yyyy} (modified version exists)"); continue; } // Skip if this date is in EXDATE list if (exceptionDates.Contains(instanceDate)) { Console.WriteLine($"Skipping EXDATE instance: {instance.Title} on {instanceDate:MM/dd/yyyy}"); continue; } instances.Add(instance); } } catch (Exception ex) { Console.WriteLine($"Error expanding recurring event with exceptions {recurringEvent.RemoteEventId}: {ex.Message}"); // Fall back to basic expansion without exception handling return ExpandRecurringEvent(recurringEvent, rangeStart, rangeEnd); } return instances; } /// /// Parses EXDATE (exception dates) from recurrence rules /// /// The full recurrence rules string /// Set of exception dates private HashSet ParseExceptionDates(string recurrenceRules) { var exceptionDates = new HashSet(); if (string.IsNullOrEmpty(recurrenceRules)) return exceptionDates; try { var rules = recurrenceRules.Split(';'); foreach (var rule in rules) { if (rule.StartsWith("EXDATE")) { // Handle different EXDATE formats // EXDATE:20250711T100000Z // EXDATE;TZID=America/New_York:20250711T100000 var exdateValue = rule.Contains(':') ? rule.Split(':')[1] : rule; var dates = exdateValue.Split(','); foreach (var dateStr in dates) { var cleanDateStr = dateStr.Trim(); DateTime exceptionDate; // Try different date formats if (cleanDateStr.Length == 8) // YYYYMMDD { if (DateTime.TryParseExact(cleanDateStr, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out exceptionDate)) { exceptionDates.Add(exceptionDate.Date); } } else if (cleanDateStr.Length >= 15) // YYYYMMDDTHHMMSS or YYYYMMDDTHHMMSSZ { var dateOnly = cleanDateStr.Substring(0, 8); if (DateTime.TryParseExact(dateOnly, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out exceptionDate)) { exceptionDates.Add(exceptionDate.Date); } } } } } } catch (Exception ex) { Console.WriteLine($"Error parsing EXDATE: {ex.Message}"); } return exceptionDates; } /// /// Stores sync token for a calendar to enable delta synchronization /// /// The calendar ID /// The sync token from Remote Calendar API public async Task UpdateCalendarSyncTokenAsync(string calendarId, string syncToken) { var calendar = await GetCalendarByRemoteIdAsync(calendarId); if (calendar != null) { calendar.SynchronizationDeltaToken = syncToken; calendar.LastSyncTime = DateTime.UtcNow; return await UpdateCalendarAsync(calendar); } return 0; } /// /// Gets the sync token for a calendar /// /// The calendar ID /// The sync token or null if not found public async Task GetCalendarSyncTokenAsync(string calendarId) { var calendar = await GetCalendarByRemoteIdAsync(calendarId); return calendar?.SynchronizationDeltaToken; } /// /// Marks an event as deleted (soft delete) for delta sync /// /// The Remote event ID /// The Remote calendar ID public async Task MarkEventAsDeletedAsync(string remoteEventId, string remoteCalendarId) { var existingEvent = await GetEventByRemoteIdAsync(remoteEventId); if (existingEvent != null) { existingEvent.IsDeleted = true; existingEvent.Status = "cancelled"; existingEvent.LastModified = DateTime.UtcNow; return await UpdateEventAsync(existingEvent); } // If event doesn't exist locally, create a placeholder for the cancellation // First get the calendar to find its internal Guid var calendar = await GetCalendarByRemoteIdAsync(remoteCalendarId); if (calendar == null) { throw new InvalidOperationException($"Calendar not found for Remote Calendar ID: {remoteCalendarId}"); } var canceledEvent = new CalendarItem { Id = Guid.NewGuid(), RemoteEventId = remoteEventId, CalendarId = calendar.Id, Title = "[Canceled Event]", Status = "cancelled", IsDeleted = true, CreatedDate = DateTime.UtcNow, LastModified = DateTime.UtcNow, StartDateTime = DateTime.MinValue, EndDateTime = DateTime.MinValue }; return await Connection.InsertAsync(canceledEvent); } /// /// Gets the last synchronization time for a calendar /// /// The calendar ID /// Last sync time or null if never synced public async Task GetLastSyncTimeAsync(string calendarId) { var calendar = await GetCalendarByRemoteIdAsync(calendarId); return calendar?.LastSyncTime; } // CalendarEventAttendee management methods /// /// Gets all calendareventattendees for a specific event /// /// The internal event Guid /// List of calendareventattendees for the event public async Task> GetCalendarEventAttendeesForEventAsync(Guid eventId) { return await Connection.Table() .Where(a => a.EventId == eventId) .OrderBy(a => a.DisplayName ?? a.Email) .ToListAsync(); } /// /// Gets all calendareventattendees for a specific event by Remote Event ID /// /// The Remote Event ID /// List of calendareventattendees for the event public async Task> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId) { var calendarItem = await GetEventByRemoteIdAsync(remoteEventId); if (calendarItem == null) { return new List(); } return await GetCalendarEventAttendeesForEventAsync(calendarItem.Id); } /// /// Inserts a new calendareventattendee /// /// The calendareventattendee to insert /// Number of rows affected public async Task InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee) { calendareventattendee.Id = Guid.NewGuid(); calendareventattendee.CreatedDate = DateTime.UtcNow; calendareventattendee.LastModified = DateTime.UtcNow; return await Connection.InsertAsync(calendareventattendee); } /// /// Updates an existing calendareventattendee /// /// The calendareventattendee to update /// Number of rows affected public async Task UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee) { calendareventattendee.LastModified = DateTime.UtcNow; return await Connection.UpdateAsync(calendareventattendee); } /// /// Syncs calendareventattendees for an event (replaces all existing calendareventattendees) /// /// The internal event Guid /// List of calendareventattendees to sync /// Number of calendareventattendees synced public async Task SyncCalendarEventAttendeesForEventAsync(Guid eventId, List calendareventattendees) { // Delete existing calendareventattendees for this event await Connection.Table() .Where(a => a.EventId == eventId) .DeleteAsync(); // Insert new calendareventattendees int syncedCount = 0; foreach (var calendareventattendee in calendareventattendees) { calendareventattendee.EventId = eventId; await InsertCalendarEventAttendeeAsync(calendareventattendee); syncedCount++; } return syncedCount; } /// /// Deletes all calendareventattendees for a specific event /// /// The internal event Guid /// Number of calendareventattendees deleted public async Task DeleteCalendarEventAttendeesForEventAsync(Guid eventId) { return await Connection.Table() .Where(a => a.EventId == eventId) .DeleteAsync(); } /// /// Gets calendareventattendee count by response status /// /// The internal event Guid /// Dictionary with response status counts public async Task> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId) { var calendareventattendees = await GetCalendarEventAttendeesForEventAsync(eventId); return calendareventattendees .GroupBy(a => a.ResponseStatus) .ToDictionary(g => g.Key, g => g.Count()); } /// /// Clears all calendareventattendees from the database /// /// Number of calendareventattendees deleted public async Task ClearAllCalendarEventAttendeesAsync() { return await Connection.DeleteAllAsync(); } /// /// Gets all calendareventattendees from the database /// /// List of all calendareventattendees public async Task> GetAllCalendarEventAttendeesAsync() { return await Connection.Table().ToListAsync(); } /// /// Gets events by calendar item type /// /// The calendar item type to filter by /// List of events matching the item type public async Task> GetEventsByItemTypeAsync(CalendarItemType itemType) { return await Connection.Table() .Where(e => !e.IsDeleted && e.ItemType == itemType) .OrderBy(e => e.StartDateTime) .ToListAsync(); } /// /// Gets events by multiple calendar item types /// /// The calendar item types to filter by /// List of events matching any of the item types public async Task> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes) { var events = await Connection.Table() .Where(e => !e.IsDeleted) .ToListAsync(); return events .Where(e => itemTypes.Contains(e.ItemType)) .OrderBy(e => e.StartDateTime) .ToList(); } /// /// Gets all-day events (all types of all-day events) /// /// List of all-day events public async Task> GetAllDayEventsAsync() { return await GetEventsByItemTypesAsync( CalendarItemType.AllDay, CalendarItemType.MultiDayAllDay, CalendarItemType.RecurringAllDay); } /// /// Gets all recurring events by item type (all types of recurring events) /// /// List of recurring events public async Task> GetAllRecurringEventsByTypeAsync() { return await GetEventsByItemTypesAsync( CalendarItemType.Recurring, CalendarItemType.RecurringAllDay, CalendarItemType.RecurringException); } /// /// Gets multi-day events (all types of multi-day events) /// /// List of multi-day events public async Task> GetMultiDayEventsAsync() { return await GetEventsByItemTypesAsync( CalendarItemType.MultiDay, CalendarItemType.MultiDayAllDay); } /// /// Gets event statistics grouped by item type /// /// Dictionary with item type counts public async Task> GetEventStatsByItemTypeAsync() { var allEvents = await Connection.Table() .Where(e => !e.IsDeleted) .ToListAsync(); return allEvents .GroupBy(e => e.ItemType) .ToDictionary(g => g.Key, g => g.Count()); } /// /// Updates all existing events to determine their item types /// This is useful for migrating existing data to use the new ItemType property /// /// Number of events updated public async Task UpdateAllEventItemTypesAsync() { var allEvents = await Connection.Table() .ToListAsync(); int updatedCount = 0; foreach (var calendarItem in allEvents) { var oldItemType = calendarItem.ItemType; calendarItem.DetermineItemType(); if (oldItemType != calendarItem.ItemType) { await Connection.UpdateAsync(calendarItem); updatedCount++; } } return updatedCount; } /// /// Inserts a new attendee /// /// The attendee to insert /// Number of rows affected public async Task InsertAttendeeAsync(CalendarEventAttendee attendee) { attendee.Id = Guid.NewGuid(); attendee.CreatedDate = DateTime.UtcNow; attendee.LastModified = DateTime.UtcNow; return await Connection.InsertAsync(attendee); } /// /// Updates an existing attendee /// /// The attendee to update /// Number of rows affected public async Task UpdateAttendeeAsync(CalendarEventAttendee attendee) { attendee.LastModified = DateTime.UtcNow; return await Connection.UpdateAsync(attendee); } /// /// Syncs attendees for an event (replaces all existing attendees) /// /// The internal event Guid /// List of attendees to sync /// Number of attendees synced public async Task SyncAttendeesForEventAsync(Guid eventId, List attendees) { // Delete existing attendees for this event await Connection.Table() .Where(a => a.EventId == eventId) .DeleteAsync(); // Insert new attendees int syncedCount = 0; foreach (var attendee in attendees) { attendee.EventId = eventId; await InsertAttendeeAsync(attendee); syncedCount++; } return syncedCount; } }