import EventKit import Foundation @MainActor final class CalendarService { static let shared = CalendarService() private let eventStore = EKEventStore() private init() {} func requestAccess() async -> Bool { do { return try await eventStore.requestFullAccessToEvents() } catch { CrashlyticsService.record(error, context: "calendar_access") print("Calendar access error: \(error)") return false } } func availableCalendars() -> [EKCalendar] { eventStore.calendars(for: .event) } func createEvent( slot: MealSlot, dishName: String, dishDescription: String?, weekStartDate: Date, settings: AppSettings ) -> String? { guard let calendar = resolvedCalendar(for: settings) else { print("Calendar sync skipped: no writable calendar available") return nil } let event = EKEvent(eventStore: eventStore) let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) " let mealNameKey = slot.mealTypeEnum.localizedKey let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved()) event.title = "\(prefix)\(mealName): \(dishName)" let slotDate = weekStartDate.addingDays(slot.dayOfWeek) let slotTime = settings.time(for: slot.mealTypeEnum) event.startDate = combineDateAndTime(date: slotDate, time: slotTime) event.endDate = event.startDate.addingMinutes(settings.eventDuration) event.notes = dishDescription event.calendar = calendar if let reminder = settings.reminderMinutesBefore { event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60))) } do { try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } catch { CrashlyticsService.record(error, context: "calendar_create") print("Error saving event: \(error)") return nil } } func updateEvent( eventId: String, slot: MealSlot, dishName: String, dishDescription: String?, weekStartDate: Date, settings: AppSettings ) -> String? { guard let event = eventStore.event(withIdentifier: eventId) else { // Event was deleted externally — create a fresh one return createEvent( slot: slot, dishName: dishName, dishDescription: dishDescription, weekStartDate: weekStartDate, settings: settings ) } let prefix = settings.eventPrefix.isEmpty ? "" : "\(settings.eventPrefix) " let mealNameKey = slot.mealTypeEnum.localizedKey let mealName = localizedString(mealNameKey, language: settings.languageEnum.resolved()) event.title = "\(prefix)\(mealName): \(dishName)" let slotDate = weekStartDate.addingDays(slot.dayOfWeek) let slotTime = settings.time(for: slot.mealTypeEnum) event.startDate = combineDateAndTime(date: slotDate, time: slotTime) event.endDate = event.startDate.addingMinutes(settings.eventDuration) event.notes = dishDescription event.alarms?.forEach { event.removeAlarm($0) } if let reminder = settings.reminderMinutesBefore { event.addAlarm(EKAlarm(relativeOffset: -TimeInterval(reminder * 60))) } do { try eventStore.save(event, span: .thisEvent) return event.eventIdentifier } catch { CrashlyticsService.record(error, context: "calendar_update") print("Error updating event: \(error)") return nil } } func deleteEvent(eventId: String) { guard let event = eventStore.event(withIdentifier: eventId) else { return } do { try eventStore.remove(event, span: .thisEvent) } catch { CrashlyticsService.record(error, context: "calendar_delete") print("Error deleting event: \(error)") } } func updateEventsTime(slots: [MealSlot], weekStartDate: Date, settings: AppSettings) { for slot in slots { guard let eventId = slot.calendarEventId, let event = eventStore.event(withIdentifier: eventId), event.startDate >= Date() else { continue } let slotDate = weekStartDate.addingDays(slot.dayOfWeek) let newTime = settings.time(for: slot.mealTypeEnum) event.startDate = combineDateAndTime(date: slotDate, time: newTime) event.endDate = event.startDate.addingMinutes(settings.eventDuration) try? eventStore.save(event, span: .thisEvent) } } private func resolvedCalendar(for settings: AppSettings) -> EKCalendar? { if let calendarId = settings.calendarId, let calendar = eventStore.calendar(withIdentifier: calendarId) { return calendar } if let defaultCalendar = eventStore.defaultCalendarForNewEvents { return defaultCalendar } return eventStore.calendars(for: .event).first } }