Files
Wino-Mail/Wino.Services/CalendarServiceEx.cs
2025-07-07 21:03:07 +02:00

1125 lines
43 KiB
C#

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<List<CalendarItem>> GetAllEventsAsync()
{
return await Connection.Table<CalendarItem>()
.Where(e => !e.IsDeleted)
.ToListAsync();
}
/// <summary>
/// Gets all events from the database including soft-deleted ones
/// </summary>
/// <returns>List of all events including deleted ones</returns>
public async Task<List<CalendarItem>> GetAllEventsIncludingDeletedAsync()
{
return await Connection.Table<CalendarItem>().ToListAsync();
}
public async Task<CalendarItem> GetEventByRemoteIdAsync(string remoteEventId)
{
return await Connection.Table<CalendarItem>()
.Where(e => e.RemoteEventId == remoteEventId && !e.IsDeleted)
.FirstOrDefaultAsync();
}
public async Task<List<CalendarItem>> GetEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
return await Connection.Table<CalendarItem>()
.Where(e => e.StartDateTime >= startDate && e.StartDateTime <= endDate && !e.IsDeleted)
.ToListAsync();
}
public async Task<List<CalendarItem>> GetRecurringEventsAsync()
{
return await Connection.Table<CalendarItem>()
.Where(e => !string.IsNullOrEmpty(e.RecurrenceRules) && !e.IsDeleted)
.ToListAsync();
}
public async Task<int> InsertEventAsync(CalendarItem calendarItem)
{
calendarItem.Id = Guid.NewGuid();
calendarItem.CreatedDate = DateTime.UtcNow;
calendarItem.LastModified = DateTime.UtcNow;
return await Connection.InsertAsync(calendarItem);
}
public async Task<int> UpdateEventAsync(CalendarItem calendarItem)
{
calendarItem.LastModified = DateTime.UtcNow;
return await Connection.UpdateAsync(calendarItem);
}
public async Task<int> 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<int> 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<int> HardDeleteEventAsync(string remoteEventId)
{
return await Connection.Table<CalendarItem>()
.DeleteAsync(e => e.RemoteEventId == remoteEventId);
}
public async Task<List<CalendarItem>> GetEventsSinceLastSyncAsync(DateTime? lastSyncTime)
{
if (lastSyncTime == null)
{
return await GetAllEventsAsync();
}
return await Connection.Table<CalendarItem>()
.Where(e => e.LastModified > lastSyncTime && !e.IsDeleted)
.ToListAsync();
}
public async Task<int> ClearAllEventsAsync()
{
return await Connection.DeleteAllAsync<CalendarItem>();
}
// Calendar management methods
public async Task<List<AccountCalendar>> GetAllCalendarsAsync()
{
return await Connection.Table<AccountCalendar>()
.Where(c => !c.IsDeleted)
.ToListAsync();
}
public async Task<AccountCalendar> GetCalendarByRemoteIdAsync(string remoteCalendarId)
{
return await Connection.Table<AccountCalendar>()
.Where(c => c.RemoteCalendarId == remoteCalendarId && !c.IsDeleted)
.FirstOrDefaultAsync();
}
public async Task<int> InsertCalendarAsync(AccountCalendar calendar)
{
calendar.Id = Guid.NewGuid();
calendar.CreatedDate = DateTime.UtcNow;
calendar.LastModified = DateTime.UtcNow;
return await Connection.InsertAsync(calendar);
}
public async Task<int> UpdateCalendarAsync(AccountCalendar calendar)
{
calendar.LastModified = DateTime.UtcNow;
return await Connection.UpdateAsync(calendar);
}
public async Task<int> 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<int> DeleteCalendarAsync(string remoteCalendarId)
{
var existingCalendar = await GetCalendarByRemoteIdAsync(remoteCalendarId);
if (existingCalendar != null)
{
existingCalendar.IsDeleted = true;
existingCalendar.LastModified = DateTime.UtcNow;
return await UpdateCalendarAsync(existingCalendar);
}
return 0;
}
/// <summary>
/// Gets events for a specific calendar by internal Guid
/// </summary>
/// <param name="calendarId">The internal Guid of the calendar</param>
/// <returns>List of events for the specified calendar</returns>
public async Task<List<CalendarItem>> GetEventsForCalendarAsync(Guid calendarId)
{
return await Connection.Table<CalendarItem>()
.Where(e => e.CalendarId == calendarId && !e.IsDeleted)
.ToListAsync();
}
/// <summary>
/// Gets events for a specific calendar by Remote Calendar ID
/// </summary>
/// <param name="remoteCalendarId">The Remote Calendar ID</param>
/// <returns>List of events for the specified calendar</returns>
public async Task<List<CalendarItem>> GetEventsByremoteCalendarIdAsync(string remoteCalendarId)
{
// First get the calendar to find its internal Guid
var calendar = await GetCalendarByRemoteIdAsync(remoteCalendarId);
if (calendar == null)
{
return new List<CalendarItem>();
}
return await GetEventsForCalendarAsync(calendar.Id);
}
public async Task<int> ClearAllCalendarsAsync()
{
return await Connection.DeleteAllAsync<AccountCalendar>();
}
public async Task<int> ClearAllDataAsync()
{
var calendarCount = await Connection.DeleteAllAsync<AccountCalendar>();
var eventCount = await Connection.DeleteAllAsync<CalendarItem>();
var calendareventattendeeCount = await Connection.DeleteAllAsync<CalendarEventAttendee>();
return calendarCount + eventCount + calendareventattendeeCount;
}
/// <summary>
/// Gets all events (including expanded recurring event instances) within a date range
/// </summary>
/// <param name="startDate">Start date of the range</param>
/// <param name="endDate">End date of the range</param>
/// <returns>List of events including expanded recurring instances</returns>
public async Task<List<CalendarItem>> GetExpandedEventsInDateRangeAsync(DateTime startDate, DateTime endDate)
{
var allEvents = new List<CalendarItem>();
// Get all non-recurring events in the date range
var oneTimeEvents = await Connection.Table<CalendarItem>()
.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<CalendarItem>()
.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();
}
/// <summary>
/// Expands a recurring event into individual instances within the specified date range
/// </summary>
/// <param name="recurringEvent">The recurring event to expand</param>
/// <param name="rangeStart">Start of the date range</param>
/// <param name="rangeEnd">End of the date range</param>
/// <returns>List of event instances</returns>
private List<CalendarItem> ExpandRecurringEvent(CalendarItem recurringEvent, DateTime rangeStart, DateTime rangeEnd)
{
var instances = new List<CalendarItem>();
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;
}
/// <summary>
/// Parses an RRULE string into a dictionary of key-value pairs
/// </summary>
/// <param name="rrule">The RRULE string (without RRULE: prefix)</param>
/// <returns>Dictionary of rule parameters</returns>
private Dictionary<string, string> ParseRRule(string rrule)
{
try
{
var ruleData = new Dictionary<string, string>();
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;
}
}
/// <summary>
/// Parses UNTIL date from RRULE
/// </summary>
/// <param name="untilString">UNTIL date string</param>
/// <returns>Parsed DateTime or null</returns>
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;
}
/// <summary>
/// Creates an instance of a recurring event
/// </summary>
/// <param name="originalEvent">The original recurring event</param>
/// <param name="instanceStart">Start time for this instance</param>
/// <param name="duration">Duration of the event</param>
/// <param name="instanceNumber">Instance number</param>
/// <returns>Event instance</returns>
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
};
}
/// <summary>
/// Calculates the next occurrence based on frequency and interval
/// </summary>
/// <param name="currentDate">Current occurrence date</param>
/// <param name="frequency">Frequency (DAILY, WEEKLY, MONTHLY, YEARLY)</param>
/// <param name="interval">Interval between occurrences</param>
/// <param name="ruleData">Additional rule data</param>
/// <returns>Next occurrence date</returns>
private DateTime GetNextOccurrence(DateTime currentDate, string frequency, int interval, Dictionary<string, string> 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
}
}
/// <summary>
/// Gets next weekly occurrence considering BYDAY rule
/// </summary>
private DateTime GetNextWeeklyOccurrence(DateTime currentDate, int interval, string[] byDays)
{
var dayMap = new Dictionary<string, DayOfWeek>
{
{"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);
}
}
/// <summary>
/// Gets next monthly occurrence by month day
/// </summary>
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);
}
/// <summary>
/// Gets next monthly occurrence by day (e.g., first Monday, last Friday)
/// </summary>
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);
}
/// <summary>
/// Gets all events (including expanded recurring event instances) within a date range with proper exception handling
/// </summary>
/// <param name="startDate">Start date of the range</param>
/// <param name="endDate">End date of the range</param>
/// <returns>List of events including expanded recurring instances, excluding canceled exceptions</returns>
public async Task<List<CalendarItem>> GetExpandedEventsInDateRangeWithExceptionsAsync(DateTime startDate, DateTime endDate, AccountCalendar calendar)
{
var allEvents = new List<CalendarItem>();
var gg = Guid.Parse("c10b83b0-9423-4d26-82d3-34b63d2e1297");
var tt = await Connection.Table<CalendarItem>().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<CalendarItem>(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<CalendarItem>(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<CalendarItem>(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<CalendarItem>(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<CalendarItem>());
var canceled = canceledInstancesByParent.GetValueOrDefault(recurringEvent.RemoteEventId, new List<CalendarItem>());
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();
}
/// <summary>
/// Expands a recurring event into individual instances within the specified date range, with handling for exceptions and cancellations
/// </summary>
/// <param name="recurringEvent">The recurring event to expand</param>
/// <param name="rangeStart">Start of the date range</param>
/// <param name="rangeEnd">End of the date range</param>
/// <param name="exceptions">List of modified instances for this recurring event</param>
/// <param name="canceled">List of canceled instances for this recurring event</param>
/// <returns>List of event instances excluding canceled ones</returns>
private List<CalendarItem> ExpandRecurringEventWithExceptions(
CalendarItem recurringEvent,
DateTime rangeStart,
DateTime rangeEnd,
List<CalendarItem> exceptions,
List<CalendarItem> canceled)
{
var instances = new List<CalendarItem>();
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<DateTime>();
var modifiedDates = new HashSet<DateTime>();
// 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;
}
/// <summary>
/// Parses EXDATE (exception dates) from recurrence rules
/// </summary>
/// <param name="recurrenceRules">The full recurrence rules string</param>
/// <returns>Set of exception dates</returns>
private HashSet<DateTime> ParseExceptionDates(string recurrenceRules)
{
var exceptionDates = new HashSet<DateTime>();
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;
}
/// <summary>
/// Stores sync token for a calendar to enable delta synchronization
/// </summary>
/// <param name="calendarId">The calendar ID</param>
/// <param name="syncToken">The sync token from Remote Calendar API</param>
public async Task<int> 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;
}
/// <summary>
/// Gets the sync token for a calendar
/// </summary>
/// <param name="calendarId">The calendar ID</param>
/// <returns>The sync token or null if not found</returns>
public async Task<string> GetCalendarSyncTokenAsync(string calendarId)
{
var calendar = await GetCalendarByRemoteIdAsync(calendarId);
return calendar?.SynchronizationDeltaToken;
}
/// <summary>
/// Marks an event as deleted (soft delete) for delta sync
/// </summary>
/// <param name="remoteEventId">The Remote event ID</param>
/// <param name="remoteCalendarId">The Remote calendar ID</param>
public async Task<int> 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);
}
/// <summary>
/// Gets the last synchronization time for a calendar
/// </summary>
/// <param name="calendarId">The calendar ID</param>
/// <returns>Last sync time or null if never synced</returns>
public async Task<DateTime?> GetLastSyncTimeAsync(string calendarId)
{
var calendar = await GetCalendarByRemoteIdAsync(calendarId);
return calendar?.LastSyncTime;
}
// CalendarEventAttendee management methods
/// <summary>
/// Gets all calendareventattendees for a specific event
/// </summary>
/// <param name="eventId">The internal event Guid</param>
/// <returns>List of calendareventattendees for the event</returns>
public async Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventAsync(Guid eventId)
{
return await Connection.Table<CalendarEventAttendee>()
.Where(a => a.EventId == eventId)
.OrderBy(a => a.DisplayName ?? a.Email)
.ToListAsync();
}
/// <summary>
/// Gets all calendareventattendees for a specific event by Remote Event ID
/// </summary>
/// <param name="remoteEventId">The Remote Event ID</param>
/// <returns>List of calendareventattendees for the event</returns>
public async Task<List<CalendarEventAttendee>> GetCalendarEventAttendeesForEventByRemoteIdAsync(string remoteEventId)
{
var calendarItem = await GetEventByRemoteIdAsync(remoteEventId);
if (calendarItem == null)
{
return new List<CalendarEventAttendee>();
}
return await GetCalendarEventAttendeesForEventAsync(calendarItem.Id);
}
/// <summary>
/// Inserts a new calendareventattendee
/// </summary>
/// <param name="calendareventattendee">The calendareventattendee to insert</param>
/// <returns>Number of rows affected</returns>
public async Task<int> InsertCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
calendareventattendee.Id = Guid.NewGuid();
calendareventattendee.CreatedDate = DateTime.UtcNow;
calendareventattendee.LastModified = DateTime.UtcNow;
return await Connection.InsertAsync(calendareventattendee);
}
/// <summary>
/// Updates an existing calendareventattendee
/// </summary>
/// <param name="calendareventattendee">The calendareventattendee to update</param>
/// <returns>Number of rows affected</returns>
public async Task<int> UpdateCalendarEventAttendeeAsync(CalendarEventAttendee calendareventattendee)
{
calendareventattendee.LastModified = DateTime.UtcNow;
return await Connection.UpdateAsync(calendareventattendee);
}
/// <summary>
/// Syncs calendareventattendees for an event (replaces all existing calendareventattendees)
/// </summary>
/// <param name="eventId">The internal event Guid</param>
/// <param name="calendareventattendees">List of calendareventattendees to sync</param>
/// <returns>Number of calendareventattendees synced</returns>
public async Task<int> SyncCalendarEventAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> calendareventattendees)
{
// Delete existing calendareventattendees for this event
await Connection.Table<CalendarEventAttendee>()
.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;
}
/// <summary>
/// Deletes all calendareventattendees for a specific event
/// </summary>
/// <param name="eventId">The internal event Guid</param>
/// <returns>Number of calendareventattendees deleted</returns>
public async Task<int> DeleteCalendarEventAttendeesForEventAsync(Guid eventId)
{
return await Connection.Table<CalendarEventAttendee>()
.Where(a => a.EventId == eventId)
.DeleteAsync();
}
/// <summary>
/// Gets calendareventattendee count by response status
/// </summary>
/// <param name="eventId">The internal event Guid</param>
/// <returns>Dictionary with response status counts</returns>
public async Task<Dictionary<AttendeeResponseStatus, int>> GetCalendarEventAttendeeResponseCountsAsync(Guid eventId)
{
var calendareventattendees = await GetCalendarEventAttendeesForEventAsync(eventId);
return calendareventattendees
.GroupBy(a => a.ResponseStatus)
.ToDictionary(g => g.Key, g => g.Count());
}
/// <summary>
/// Clears all calendareventattendees from the database
/// </summary>
/// <returns>Number of calendareventattendees deleted</returns>
public async Task<int> ClearAllCalendarEventAttendeesAsync()
{
return await Connection.DeleteAllAsync<CalendarEventAttendee>();
}
/// <summary>
/// Gets all calendareventattendees from the database
/// </summary>
/// <returns>List of all calendareventattendees</returns>
public async Task<List<CalendarEventAttendee>> GetAllCalendarEventAttendeesAsync()
{
return await Connection.Table<CalendarEventAttendee>().ToListAsync();
}
/// <summary>
/// Gets events by calendar item type
/// </summary>
/// <param name="itemType">The calendar item type to filter by</param>
/// <returns>List of events matching the item type</returns>
public async Task<List<CalendarItem>> GetEventsByItemTypeAsync(CalendarItemType itemType)
{
return await Connection.Table<CalendarItem>()
.Where(e => !e.IsDeleted && e.ItemType == itemType)
.OrderBy(e => e.StartDateTime)
.ToListAsync();
}
/// <summary>
/// Gets events by multiple calendar item types
/// </summary>
/// <param name="itemTypes">The calendar item types to filter by</param>
/// <returns>List of events matching any of the item types</returns>
public async Task<List<CalendarItem>> GetEventsByItemTypesAsync(params CalendarItemType[] itemTypes)
{
var events = await Connection.Table<CalendarItem>()
.Where(e => !e.IsDeleted)
.ToListAsync();
return events
.Where(e => itemTypes.Contains(e.ItemType))
.OrderBy(e => e.StartDateTime)
.ToList();
}
/// <summary>
/// Gets all-day events (all types of all-day events)
/// </summary>
/// <returns>List of all-day events</returns>
public async Task<List<CalendarItem>> GetAllDayEventsAsync()
{
return await GetEventsByItemTypesAsync(
CalendarItemType.AllDay,
CalendarItemType.MultiDayAllDay,
CalendarItemType.RecurringAllDay);
}
/// <summary>
/// Gets all recurring events by item type (all types of recurring events)
/// </summary>
/// <returns>List of recurring events</returns>
public async Task<List<CalendarItem>> GetAllRecurringEventsByTypeAsync()
{
return await GetEventsByItemTypesAsync(
CalendarItemType.Recurring,
CalendarItemType.RecurringAllDay,
CalendarItemType.RecurringException);
}
/// <summary>
/// Gets multi-day events (all types of multi-day events)
/// </summary>
/// <returns>List of multi-day events</returns>
public async Task<List<CalendarItem>> GetMultiDayEventsAsync()
{
return await GetEventsByItemTypesAsync(
CalendarItemType.MultiDay,
CalendarItemType.MultiDayAllDay);
}
/// <summary>
/// Gets event statistics grouped by item type
/// </summary>
/// <returns>Dictionary with item type counts</returns>
public async Task<Dictionary<CalendarItemType, int>> GetEventStatsByItemTypeAsync()
{
var allEvents = await Connection.Table<CalendarItem>()
.Where(e => !e.IsDeleted)
.ToListAsync();
return allEvents
.GroupBy(e => e.ItemType)
.ToDictionary(g => g.Key, g => g.Count());
}
/// <summary>
/// Updates all existing events to determine their item types
/// This is useful for migrating existing data to use the new ItemType property
/// </summary>
/// <returns>Number of events updated</returns>
public async Task<int> UpdateAllEventItemTypesAsync()
{
var allEvents = await Connection.Table<CalendarItem>()
.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;
}
/// <summary>
/// Inserts a new attendee
/// </summary>
/// <param name="attendee">The attendee to insert</param>
/// <returns>Number of rows affected</returns>
public async Task<int> InsertAttendeeAsync(CalendarEventAttendee attendee)
{
attendee.Id = Guid.NewGuid();
attendee.CreatedDate = DateTime.UtcNow;
attendee.LastModified = DateTime.UtcNow;
return await Connection.InsertAsync(attendee);
}
/// <summary>
/// Updates an existing attendee
/// </summary>
/// <param name="attendee">The attendee to update</param>
/// <returns>Number of rows affected</returns>
public async Task<int> UpdateAttendeeAsync(CalendarEventAttendee attendee)
{
attendee.LastModified = DateTime.UtcNow;
return await Connection.UpdateAsync(attendee);
}
/// <summary>
/// Syncs attendees for an event (replaces all existing attendees)
/// </summary>
/// <param name="eventId">The internal event Guid</param>
/// <param name="attendees">List of attendees to sync</param>
/// <returns>Number of attendees synced</returns>
public async Task<int> SyncAttendeesForEventAsync(Guid eventId, List<CalendarEventAttendee> attendees)
{
// Delete existing attendees for this event
await Connection.Table<CalendarEventAttendee>()
.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;
}
}