Skip to main content

eventkit/
imp.rs

1use block2::RcBlock;
2use chrono::{DateTime, Datelike, Duration, Local, TimeZone, Timelike};
3use objc2::Message;
4use objc2::rc::Retained;
5use objc2::runtime::Bool;
6use objc2_event_kit::{
7    EKAlarm, EKAlarmProximity, EKAuthorizationStatus, EKCalendar, EKCalendarItem, EKEntityType,
8    EKEvent, EKEventStore, EKRecurrenceDayOfWeek, EKRecurrenceEnd, EKRecurrenceFrequency,
9    EKRecurrenceRule, EKReminder, EKSource, EKSpan, EKStructuredLocation, EKWeekday,
10};
11use objc2_foundation::{
12    NSArray, NSCalendar, NSDate, NSDateComponents, NSError, NSNumber, NSString,
13};
14use std::sync::{Arc, Condvar, Mutex};
15use thiserror::Error;
16
17#[cfg(feature = "location")]
18#[path = "location.rs"]
19pub mod location;
20
21#[cfg(feature = "mcp")]
22#[path = "mcp.rs"]
23pub mod mcp;
24
25/// Errors that can occur when working with EventKit
26#[derive(Error, Debug)]
27pub enum EventKitError {
28    #[error("Authorization denied")]
29    AuthorizationDenied,
30
31    #[error("Authorization restricted by system policy")]
32    AuthorizationRestricted,
33
34    #[error("Authorization not determined")]
35    AuthorizationNotDetermined,
36
37    #[error("Failed to request authorization: {0}")]
38    AuthorizationRequestFailed(String),
39
40    #[error("No default calendar")]
41    NoDefaultCalendar,
42
43    #[error("Calendar not found: {0}")]
44    CalendarNotFound(String),
45
46    #[error("Item not found: {0}")]
47    ItemNotFound(String),
48
49    #[error("Failed to save: {0}")]
50    SaveFailed(String),
51
52    #[error("Failed to delete: {0}")]
53    DeleteFailed(String),
54
55    #[error("Failed to fetch: {0}")]
56    FetchFailed(String),
57
58    #[error("EventKit error: {0}")]
59    EventKitError(String),
60
61    #[error("Invalid date range")]
62    InvalidDateRange,
63}
64
65/// Backward compatibility alias
66pub type RemindersError = EventKitError;
67
68/// Result type for EventKit operations
69pub type Result<T> = std::result::Result<T, EventKitError>;
70
71/// Represents a reminder item with its properties
72#[derive(Debug, Clone)]
73pub struct ReminderItem {
74    /// Unique identifier for the reminder
75    pub identifier: String,
76    /// Title of the reminder
77    pub title: String,
78    /// Optional notes/description
79    pub notes: Option<String>,
80    /// Whether the reminder is completed
81    pub completed: bool,
82    /// Priority (0 = none, 1-4 = high, 5 = medium, 6-9 = low)
83    pub priority: usize,
84    /// Calendar/list the reminder belongs to
85    pub calendar_title: Option<String>,
86    /// Calendar/list identifier
87    pub calendar_id: Option<String>,
88    /// Due date for the reminder
89    pub due_date: Option<DateTime<Local>>,
90    /// Start date (when to start working on it)
91    pub start_date: Option<DateTime<Local>>,
92    /// Completion date (when it was completed)
93    pub completion_date: Option<DateTime<Local>>,
94    /// External identifier for the reminder (server-provided)
95    pub external_identifier: Option<String>,
96    /// Location associated with the reminder
97    pub location: Option<String>,
98    /// URL associated with the reminder
99    pub url: Option<String>,
100    /// Creation date of the reminder
101    pub creation_date: Option<DateTime<Local>>,
102    /// Last modified date of the reminder
103    pub last_modified_date: Option<DateTime<Local>>,
104    /// Timezone of the reminder
105    pub timezone: Option<String>,
106    /// Whether the reminder has alarms
107    pub has_alarms: bool,
108    /// Whether the reminder has recurrence rules
109    pub has_recurrence_rules: bool,
110    /// Whether the reminder has attendees
111    pub has_attendees: bool,
112    /// Whether the reminder has notes
113    pub has_notes: bool,
114    /// Attendees on this reminder (usually empty, possible on shared lists)
115    pub attendees: Vec<ParticipantInfo>,
116}
117
118/// Type of calendar/source.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub enum CalendarType {
121    Local,
122    CalDAV,
123    Exchange,
124    Subscription,
125    Birthday,
126    Unknown,
127}
128
129/// An account source (iCloud, Local, Exchange, etc.)
130#[derive(Debug, Clone)]
131pub struct SourceInfo {
132    pub identifier: String,
133    pub title: String,
134    pub source_type: String,
135}
136
137/// Represents a calendar (reminder list or event calendar).
138#[derive(Debug, Clone)]
139pub struct CalendarInfo {
140    /// Unique identifier
141    pub identifier: String,
142    /// Title of the calendar
143    pub title: String,
144    /// Source name (e.g., iCloud, Local)
145    pub source: Option<String>,
146    /// Source identifier
147    pub source_id: Option<String>,
148    /// Calendar type
149    pub calendar_type: CalendarType,
150    /// Whether items can be added/modified/deleted
151    pub allows_modifications: bool,
152    /// Whether the calendar itself can be modified (renamed/deleted)
153    pub is_immutable: bool,
154    /// Whether this is a URL-subscribed read-only calendar
155    pub is_subscribed: bool,
156    /// Calendar color as RGBA (0.0-1.0)
157    pub color: Option<(f64, f64, f64, f64)>,
158    /// Entity types this calendar supports ("event", "reminder")
159    pub allowed_entity_types: Vec<String>,
160}
161
162/// Proximity trigger for a location-based alarm.
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum AlarmProximity {
165    /// No proximity trigger.
166    None,
167    /// Trigger when entering the location.
168    Enter,
169    /// Trigger when leaving the location.
170    Leave,
171}
172
173/// A structured location for geofenced alarms.
174#[derive(Debug, Clone)]
175pub struct StructuredLocation {
176    /// Display title for the location.
177    pub title: String,
178    /// Latitude of the location.
179    pub latitude: f64,
180    /// Longitude of the location.
181    pub longitude: f64,
182    /// Geofence radius in meters.
183    pub radius: f64,
184}
185
186/// An alarm attached to a reminder or event.
187#[derive(Debug, Clone)]
188pub struct AlarmInfo {
189    /// Offset in seconds before the due date (negative = before).
190    pub relative_offset: Option<f64>,
191    /// Absolute date for the alarm (ISO 8601 string).
192    pub absolute_date: Option<DateTime<Local>>,
193    /// Proximity trigger (enter/leave geofence).
194    pub proximity: AlarmProximity,
195    /// Location for geofenced alarms.
196    pub location: Option<StructuredLocation>,
197}
198
199/// How often a recurrence repeats.
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum RecurrenceFrequency {
202    Daily,
203    Weekly,
204    Monthly,
205    Yearly,
206}
207
208/// When a recurrence ends.
209#[derive(Debug, Clone)]
210pub enum RecurrenceEndCondition {
211    /// Repeats forever.
212    Never,
213    /// Ends after a number of occurrences.
214    AfterCount(usize),
215    /// Ends on a specific date.
216    OnDate(DateTime<Local>),
217}
218
219/// A recurrence rule describing how a reminder or event repeats.
220#[derive(Debug, Clone)]
221pub struct RecurrenceRule {
222    /// How often it repeats (daily, weekly, monthly, yearly).
223    pub frequency: RecurrenceFrequency,
224    /// Repeat every N intervals (e.g., every 2 weeks).
225    pub interval: usize,
226    /// When the recurrence ends.
227    pub end: RecurrenceEndCondition,
228    /// Days of the week (1=Sun..7=Sat) for weekly/monthly rules.
229    pub days_of_week: Option<Vec<u8>>,
230    /// Days of the month (1-31, negatives count from end) for monthly rules.
231    pub days_of_month: Option<Vec<i32>>,
232}
233
234/// The main reminders manager providing access to EventKit functionality
235pub struct RemindersManager {
236    store: Retained<EKEventStore>,
237}
238
239impl RemindersManager {
240    /// Creates a new RemindersManager instance
241    pub fn new() -> Self {
242        let store = unsafe { EKEventStore::new() };
243        Self { store }
244    }
245
246    /// Gets the current authorization status for reminders
247    pub fn authorization_status() -> AuthorizationStatus {
248        let status =
249            unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Reminder) };
250        status.into()
251    }
252
253    /// Requests full access to reminders (blocking)
254    ///
255    /// Returns Ok(true) if access was granted, Ok(false) if denied
256    pub fn request_access(&self) -> Result<bool> {
257        let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
258        let result_clone = Arc::clone(&result);
259
260        let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
261            let error_msg = if !error.is_null() {
262                let error_ref = unsafe { &*error };
263                Some(format!("{:?}", error_ref))
264            } else {
265                None
266            };
267
268            let (lock, cvar) = &*result_clone;
269            let mut res = lock.lock().unwrap();
270            *res = Some((granted.as_bool(), error_msg));
271            cvar.notify_one();
272        });
273
274        unsafe {
275            // Convert RcBlock to raw pointer for the API
276            let block_ptr = &*completion as *const _ as *mut _;
277            self.store
278                .requestFullAccessToRemindersWithCompletion(block_ptr);
279        }
280
281        let (lock, cvar) = &*result;
282        let mut res = lock.lock().unwrap();
283        while res.is_none() {
284            res = cvar.wait(res).unwrap();
285        }
286
287        match res.take() {
288            Some((granted, None)) => Ok(granted),
289            Some((_, Some(error))) => Err(RemindersError::AuthorizationRequestFailed(error)),
290            None => Err(RemindersError::AuthorizationRequestFailed(
291                "Unknown error".to_string(),
292            )),
293        }
294    }
295
296    /// Ensures we have authorization, requesting if needed
297    pub fn ensure_authorized(&self) -> Result<()> {
298        match Self::authorization_status() {
299            AuthorizationStatus::FullAccess => Ok(()),
300            AuthorizationStatus::NotDetermined => {
301                if self.request_access()? {
302                    Ok(())
303                } else {
304                    Err(RemindersError::AuthorizationDenied)
305                }
306            }
307            AuthorizationStatus::Denied => Err(RemindersError::AuthorizationDenied),
308            AuthorizationStatus::Restricted => Err(RemindersError::AuthorizationRestricted),
309            AuthorizationStatus::WriteOnly => Ok(()), // Can still read with write-only in some cases
310        }
311    }
312
313    /// Lists all reminder calendars (lists)
314    pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
315        self.ensure_authorized()?;
316
317        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
318
319        let mut result = Vec::new();
320        for calendar in calendars.iter() {
321            result.push(calendar_to_info(&calendar));
322        }
323
324        Ok(result)
325    }
326
327    /// Lists all available sources (iCloud, Local, Exchange, etc.)
328    pub fn list_sources(&self) -> Result<Vec<SourceInfo>> {
329        self.ensure_authorized()?;
330        let sources = unsafe { self.store.sources() };
331        let mut result = Vec::new();
332        for source in sources.iter() {
333            result.push(source_to_info(&source));
334        }
335        Ok(result)
336    }
337
338    /// Gets the default calendar for new reminders
339    pub fn default_calendar(&self) -> Result<CalendarInfo> {
340        self.ensure_authorized()?;
341
342        let calendar = unsafe { self.store.defaultCalendarForNewReminders() };
343
344        match calendar {
345            Some(cal) => Ok(calendar_to_info(&cal)),
346            None => Err(RemindersError::NoDefaultCalendar),
347        }
348    }
349
350    /// Fetches all reminders (blocking)
351    pub fn fetch_all_reminders(&self) -> Result<Vec<ReminderItem>> {
352        self.fetch_reminders(None)
353    }
354
355    /// Fetches reminders from specific calendars (blocking)
356    pub fn fetch_reminders(&self, calendar_titles: Option<&[&str]>) -> Result<Vec<ReminderItem>> {
357        self.ensure_authorized()?;
358
359        let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
360            Some(titles) => {
361                let all_calendars =
362                    unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
363                let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
364
365                for cal in all_calendars.iter() {
366                    let title = unsafe { cal.title() };
367                    let title_str = title.to_string();
368                    if titles.iter().any(|t| *t == title_str) {
369                        matching.push(cal.retain());
370                    }
371                }
372
373                if matching.is_empty() {
374                    return Err(RemindersError::CalendarNotFound(titles.join(", ")));
375                }
376
377                Some(NSArray::from_retained_slice(&matching))
378            }
379            None => None,
380        };
381
382        let predicate = unsafe {
383            self.store
384                .predicateForRemindersInCalendars(calendars.as_deref())
385        };
386
387        let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
388        let result_clone = Arc::clone(&result);
389
390        let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
391            let items = if reminders.is_null() {
392                Vec::new()
393            } else {
394                let reminders = unsafe { Retained::retain(reminders).unwrap() };
395                reminders.iter().map(|r| reminder_to_item(&r)).collect()
396            };
397            let (lock, cvar) = &*result_clone;
398            let mut guard = lock.lock().unwrap();
399            *guard = Some(items);
400            cvar.notify_one();
401        });
402
403        unsafe {
404            self.store
405                .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
406        }
407
408        let (lock, cvar) = &*result;
409        let mut guard = lock.lock().unwrap();
410        while guard.is_none() {
411            guard = cvar.wait(guard).unwrap();
412        }
413
414        guard
415            .take()
416            .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
417    }
418
419    /// Fetches incomplete reminders
420    pub fn fetch_incomplete_reminders(&self) -> Result<Vec<ReminderItem>> {
421        self.ensure_authorized()?;
422
423        let predicate = unsafe {
424            self.store
425                .predicateForIncompleteRemindersWithDueDateStarting_ending_calendars(
426                    None, None, None,
427                )
428        };
429
430        let result = Arc::new((Mutex::new(None::<Vec<ReminderItem>>), Condvar::new()));
431        let result_clone = Arc::clone(&result);
432
433        let completion = RcBlock::new(move |reminders: *mut NSArray<EKReminder>| {
434            let items = if reminders.is_null() {
435                Vec::new()
436            } else {
437                let reminders = unsafe { Retained::retain(reminders).unwrap() };
438                reminders.iter().map(|r| reminder_to_item(&r)).collect()
439            };
440            let (lock, cvar) = &*result_clone;
441            let mut guard = lock.lock().unwrap();
442            *guard = Some(items);
443            cvar.notify_one();
444        });
445
446        unsafe {
447            self.store
448                .fetchRemindersMatchingPredicate_completion(&predicate, &completion);
449        }
450
451        let (lock, cvar) = &*result;
452        let mut guard = lock.lock().unwrap();
453        while guard.is_none() {
454            guard = cvar.wait(guard).unwrap();
455        }
456
457        guard
458            .take()
459            .ok_or_else(|| RemindersError::FetchFailed("Unknown error".to_string()))
460    }
461
462    /// Creates a new reminder
463    ///
464    /// # Arguments
465    /// * `title` - The reminder title
466    /// * `notes` - Optional notes/description
467    /// * `calendar_title` - Optional calendar/list name (uses default if None)
468    /// * `priority` - Optional priority (0 = none, 1-4 = high, 5 = medium, 6-9 = low)
469    /// * `due_date` - Optional due date for the reminder
470    /// * `start_date` - Optional start date (when to start working on it)
471    #[allow(clippy::too_many_arguments)]
472    pub fn create_reminder(
473        &self,
474        title: &str,
475        notes: Option<&str>,
476        calendar_title: Option<&str>,
477        priority: Option<usize>,
478        due_date: Option<DateTime<Local>>,
479        start_date: Option<DateTime<Local>>,
480    ) -> Result<ReminderItem> {
481        self.ensure_authorized()?;
482
483        let reminder = unsafe { EKReminder::reminderWithEventStore(&self.store) };
484
485        // Set title
486        let ns_title = NSString::from_str(title);
487        unsafe { reminder.setTitle(Some(&ns_title)) };
488
489        // Set notes if provided
490        if let Some(notes_text) = notes {
491            let ns_notes = NSString::from_str(notes_text);
492            unsafe { reminder.setNotes(Some(&ns_notes)) };
493        }
494
495        // Set priority if provided
496        if let Some(p) = priority {
497            unsafe { reminder.setPriority(p) };
498        }
499
500        // Set due date if provided
501        if let Some(due) = due_date {
502            let components = datetime_to_date_components(due);
503            unsafe { reminder.setDueDateComponents(Some(&components)) };
504        }
505
506        // Set start date if provided
507        if let Some(start) = start_date {
508            let components = datetime_to_date_components(start);
509            unsafe { reminder.setStartDateComponents(Some(&components)) };
510        }
511
512        // Set calendar
513        let calendar = if let Some(cal_title) = calendar_title {
514            self.find_calendar_by_title(cal_title)?
515        } else {
516            unsafe { self.store.defaultCalendarForNewReminders() }
517                .ok_or(RemindersError::NoDefaultCalendar)?
518        };
519        unsafe { reminder.setCalendar(Some(&calendar)) };
520
521        // Save
522        unsafe {
523            self.store
524                .saveReminder_commit_error(&reminder, true)
525                .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
526        }
527
528        Ok(reminder_to_item(&reminder))
529    }
530
531    /// Updates an existing reminder
532    ///
533    /// All fields are optional - only provided fields will be updated.
534    /// Pass `Some(None)` for due_date/start_date to clear them.
535    /// Use `calendar_title` to move the reminder to a different list.
536    #[allow(clippy::too_many_arguments)]
537    pub fn update_reminder(
538        &self,
539        identifier: &str,
540        title: Option<&str>,
541        notes: Option<&str>,
542        completed: Option<bool>,
543        priority: Option<usize>,
544        due_date: Option<Option<DateTime<Local>>>,
545        start_date: Option<Option<DateTime<Local>>>,
546        calendar_title: Option<&str>,
547    ) -> Result<ReminderItem> {
548        self.ensure_authorized()?;
549
550        let reminder = self.find_reminder_by_id(identifier)?;
551
552        if let Some(t) = title {
553            let ns_title = NSString::from_str(t);
554            unsafe { reminder.setTitle(Some(&ns_title)) };
555        }
556
557        if let Some(n) = notes {
558            let ns_notes = NSString::from_str(n);
559            unsafe { reminder.setNotes(Some(&ns_notes)) };
560        }
561
562        if let Some(c) = completed {
563            unsafe { reminder.setCompleted(c) };
564        }
565
566        if let Some(p) = priority {
567            unsafe { reminder.setPriority(p) };
568        }
569
570        // Handle due date: Some(Some(date)) = set, Some(None) = clear, None = no change
571        if let Some(due_opt) = due_date {
572            match due_opt {
573                Some(due) => {
574                    let components = datetime_to_date_components(due);
575                    unsafe { reminder.setDueDateComponents(Some(&components)) };
576                }
577                None => {
578                    unsafe { reminder.setDueDateComponents(None) };
579                }
580            }
581        }
582
583        // Handle start date: Some(Some(date)) = set, Some(None) = clear, None = no change
584        if let Some(start_opt) = start_date {
585            match start_opt {
586                Some(start) => {
587                    let components = datetime_to_date_components(start);
588                    unsafe { reminder.setStartDateComponents(Some(&components)) };
589                }
590                None => {
591                    unsafe { reminder.setStartDateComponents(None) };
592                }
593            }
594        }
595
596        // Move to a different calendar/list if specified
597        if let Some(cal_title) = calendar_title {
598            let calendar = self.find_calendar_by_title(cal_title)?;
599            unsafe { reminder.setCalendar(Some(&calendar)) };
600        }
601
602        unsafe {
603            self.store
604                .saveReminder_commit_error(&reminder, true)
605                .map_err(|e| RemindersError::SaveFailed(format!("{:?}", e)))?;
606        }
607
608        Ok(reminder_to_item(&reminder))
609    }
610
611    /// Marks a reminder as complete
612    pub fn complete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
613        self.update_reminder(identifier, None, None, Some(true), None, None, None, None)
614    }
615
616    /// Marks a reminder as incomplete
617    pub fn uncomplete_reminder(&self, identifier: &str) -> Result<ReminderItem> {
618        self.update_reminder(identifier, None, None, Some(false), None, None, None, None)
619    }
620
621    /// Deletes a reminder
622    pub fn delete_reminder(&self, identifier: &str) -> Result<()> {
623        self.ensure_authorized()?;
624
625        let reminder = self.find_reminder_by_id(identifier)?;
626
627        unsafe {
628            self.store
629                .removeReminder_commit_error(&reminder, true)
630                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
631        }
632
633        Ok(())
634    }
635
636    /// Gets a reminder by its identifier
637    pub fn get_reminder(&self, identifier: &str) -> Result<ReminderItem> {
638        self.ensure_authorized()?;
639        let reminder = self.find_reminder_by_id(identifier)?;
640        Ok(reminder_to_item(&reminder))
641    }
642
643    // ========================================================================
644    // Alarm Management
645    // ========================================================================
646
647    /// Lists all alarms on a reminder.
648    pub fn get_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
649        self.ensure_authorized()?;
650        let reminder = self.find_reminder_by_id(identifier)?;
651        Ok(get_item_alarms(&reminder))
652    }
653
654    /// Adds an alarm to a reminder.
655    pub fn add_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
656        self.ensure_authorized()?;
657        let reminder = self.find_reminder_by_id(identifier)?;
658        add_item_alarm(&reminder, alarm);
659        unsafe {
660            self.store
661                .saveReminder_commit_error(&reminder, true)
662                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
663        }
664        Ok(())
665    }
666
667    /// Removes all alarms from a reminder.
668    pub fn remove_all_alarms(&self, identifier: &str) -> Result<()> {
669        self.ensure_authorized()?;
670        let reminder = self.find_reminder_by_id(identifier)?;
671        clear_item_alarms(&reminder);
672        unsafe {
673            self.store
674                .saveReminder_commit_error(&reminder, true)
675                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
676        }
677        Ok(())
678    }
679
680    /// Removes a specific alarm from a reminder by index.
681    pub fn remove_alarm(&self, identifier: &str, index: usize) -> Result<()> {
682        self.ensure_authorized()?;
683        let reminder = self.find_reminder_by_id(identifier)?;
684        remove_item_alarm(&reminder, index)?;
685        unsafe {
686            self.store
687                .saveReminder_commit_error(&reminder, true)
688                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
689        }
690        Ok(())
691    }
692
693    // ========================================================================
694    // URL Management
695    // ========================================================================
696
697    /// Set or clear the URL on a reminder.
698    pub fn set_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
699        self.ensure_authorized()?;
700        let reminder = self.find_reminder_by_id(identifier)?;
701        set_item_url(&reminder, url);
702        unsafe {
703            self.store
704                .saveReminder_commit_error(&reminder, true)
705                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
706        }
707        Ok(())
708    }
709
710    // ========================================================================
711    // Recurrence Rule Management
712    // ========================================================================
713
714    /// Gets recurrence rules on a reminder.
715    pub fn get_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
716        self.ensure_authorized()?;
717        let reminder = self.find_reminder_by_id(identifier)?;
718        Ok(get_item_recurrence_rules(&reminder))
719    }
720
721    /// Sets a recurrence rule on a reminder (replaces any existing rules).
722    pub fn set_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
723        self.ensure_authorized()?;
724        let reminder = self.find_reminder_by_id(identifier)?;
725        set_item_recurrence_rule(&reminder, rule);
726        unsafe {
727            self.store
728                .saveReminder_commit_error(&reminder, true)
729                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
730        }
731        Ok(())
732    }
733
734    /// Removes all recurrence rules from a reminder.
735    pub fn remove_recurrence_rules(&self, identifier: &str) -> Result<()> {
736        self.ensure_authorized()?;
737        let reminder = self.find_reminder_by_id(identifier)?;
738        clear_item_recurrence_rules(&reminder);
739        unsafe {
740            self.store
741                .saveReminder_commit_error(&reminder, true)
742                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
743        }
744        Ok(())
745    }
746
747    // ========================================================================
748    // Calendar (Reminder List) Management
749    // ========================================================================
750
751    /// Creates a new reminder list (calendar)
752    ///
753    /// The list will be created in the default source (usually iCloud or Local).
754    pub fn create_calendar(&self, title: &str) -> Result<CalendarInfo> {
755        self.ensure_authorized()?;
756
757        // Create a new calendar for reminders
758        let calendar = unsafe {
759            EKCalendar::calendarForEntityType_eventStore(EKEntityType::Reminder, &self.store)
760        };
761
762        // Set the title
763        let ns_title = NSString::from_str(title);
764        unsafe { calendar.setTitle(&ns_title) };
765
766        // Find a suitable source (prefer iCloud, fall back to local)
767        let source = self.find_best_source_for_reminders()?;
768        unsafe { calendar.setSource(Some(&source)) };
769
770        // Save the calendar
771        unsafe {
772            self.store
773                .saveCalendar_commit_error(&calendar, true)
774                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
775        }
776
777        Ok(calendar_to_info(&calendar))
778    }
779
780    /// Renames an existing reminder list (calendar)
781    /// Rename a reminder list (backward compat wrapper).
782    pub fn rename_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
783        self.update_calendar(identifier, Some(new_title), None)
784    }
785
786    /// Update a reminder list — name, color, or both.
787    pub fn update_calendar(
788        &self,
789        identifier: &str,
790        new_title: Option<&str>,
791        color_rgba: Option<(f64, f64, f64, f64)>,
792    ) -> Result<CalendarInfo> {
793        self.ensure_authorized()?;
794        let calendar = self.find_calendar_by_id(identifier)?;
795
796        if !unsafe { calendar.allowsContentModifications() } {
797            return Err(EventKitError::SaveFailed(
798                "Calendar does not allow modifications".to_string(),
799            ));
800        }
801
802        if let Some(title) = new_title {
803            let ns_title = NSString::from_str(title);
804            unsafe { calendar.setTitle(&ns_title) };
805        }
806
807        if let Some((r, g, b, a)) = color_rgba {
808            let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
809            unsafe { calendar.setCGColor(Some(&cg)) };
810        }
811
812        unsafe {
813            self.store
814                .saveCalendar_commit_error(&calendar, true)
815                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
816        }
817
818        Ok(calendar_to_info(&calendar))
819    }
820
821    /// Deletes a reminder list (calendar)
822    ///
823    /// Warning: This will delete all reminders in the list!
824    pub fn delete_calendar(&self, identifier: &str) -> Result<()> {
825        self.ensure_authorized()?;
826
827        let calendar = self.find_calendar_by_id(identifier)?;
828
829        // Check if modifications are allowed
830        if !unsafe { calendar.allowsContentModifications() } {
831            return Err(EventKitError::DeleteFailed(
832                "Calendar does not allow modifications".to_string(),
833            ));
834        }
835
836        unsafe {
837            self.store
838                .removeCalendar_commit_error(&calendar, true)
839                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
840        }
841
842        Ok(())
843    }
844
845    /// Gets a calendar by its identifier
846    pub fn get_calendar(&self, identifier: &str) -> Result<CalendarInfo> {
847        self.ensure_authorized()?;
848        let calendar = self.find_calendar_by_id(identifier)?;
849        Ok(calendar_to_info(&calendar))
850    }
851
852    // Helper to find the best source for creating new reminder calendars
853    fn find_best_source_for_reminders(&self) -> Result<Retained<objc2_event_kit::EKSource>> {
854        // Try to get the source from the default calendar first
855        if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewReminders() }
856            && let Some(source) = unsafe { default_cal.source() }
857        {
858            return Ok(source);
859        }
860
861        // Fall back to finding any source that supports reminders
862        let sources = unsafe { self.store.sources() };
863        for source in sources.iter() {
864            // Check if this source supports reminder calendars
865            let calendars = unsafe { source.calendarsForEntityType(EKEntityType::Reminder) };
866            if !calendars.is_empty() {
867                return Ok(source.retain());
868            }
869        }
870
871        Err(EventKitError::SaveFailed(
872            "No suitable source found for creating reminder calendar".to_string(),
873        ))
874    }
875
876    // Helper to find a calendar by identifier
877    fn find_calendar_by_id(&self, identifier: &str) -> Result<Retained<EKCalendar>> {
878        let ns_id = NSString::from_str(identifier);
879        let calendar = unsafe { self.store.calendarWithIdentifier(&ns_id) };
880
881        match calendar {
882            Some(cal) => Ok(cal),
883            None => Err(EventKitError::CalendarNotFound(identifier.to_string())),
884        }
885    }
886
887    // Helper to find a calendar by title
888    fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
889        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Reminder) };
890
891        for cal in calendars.iter() {
892            let cal_title = unsafe { cal.title() };
893            if cal_title.to_string() == title {
894                return Ok(cal.retain());
895            }
896        }
897
898        Err(RemindersError::CalendarNotFound(title.to_string()))
899    }
900
901    // Helper to find a reminder by identifier
902    fn find_reminder_by_id(&self, identifier: &str) -> Result<Retained<EKReminder>> {
903        let ns_id = NSString::from_str(identifier);
904        let item = unsafe { self.store.calendarItemWithIdentifier(&ns_id) };
905
906        match item {
907            Some(item) => {
908                // Try to downcast to EKReminder
909                if let Some(reminder) = item.downcast_ref::<EKReminder>() {
910                    Ok(reminder.retain())
911                } else {
912                    Err(EventKitError::ItemNotFound(identifier.to_string()))
913                }
914            }
915            None => Err(EventKitError::ItemNotFound(identifier.to_string())),
916        }
917    }
918}
919
920impl Default for RemindersManager {
921    fn default() -> Self {
922        Self::new()
923    }
924}
925
926/// Authorization status for reminders access
927#[derive(Debug, Clone, Copy, PartialEq, Eq)]
928pub enum AuthorizationStatus {
929    /// User has not yet made a choice
930    NotDetermined,
931    /// Access restricted by system policy
932    Restricted,
933    /// User explicitly denied access
934    Denied,
935    /// Full access granted
936    FullAccess,
937    /// Write-only access granted
938    WriteOnly,
939}
940
941impl From<EKAuthorizationStatus> for AuthorizationStatus {
942    fn from(status: EKAuthorizationStatus) -> Self {
943        if status == EKAuthorizationStatus::NotDetermined {
944            AuthorizationStatus::NotDetermined
945        } else if status == EKAuthorizationStatus::Restricted {
946            AuthorizationStatus::Restricted
947        } else if status == EKAuthorizationStatus::Denied {
948            AuthorizationStatus::Denied
949        } else if status == EKAuthorizationStatus::FullAccess {
950            AuthorizationStatus::FullAccess
951        } else if status == EKAuthorizationStatus::WriteOnly {
952            AuthorizationStatus::WriteOnly
953        } else {
954            AuthorizationStatus::NotDetermined
955        }
956    }
957}
958
959impl std::fmt::Display for AuthorizationStatus {
960    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
961        match self {
962            AuthorizationStatus::NotDetermined => write!(f, "Not Determined"),
963            AuthorizationStatus::Restricted => write!(f, "Restricted"),
964            AuthorizationStatus::Denied => write!(f, "Denied"),
965            AuthorizationStatus::FullAccess => write!(f, "Full Access"),
966            AuthorizationStatus::WriteOnly => write!(f, "Write Only"),
967        }
968    }
969}
970
971// Helper function to convert EKReminder to ReminderItem
972fn reminder_to_item(reminder: &EKReminder) -> ReminderItem {
973    let identifier = unsafe { reminder.calendarItemIdentifier() }.to_string();
974    let title = unsafe { reminder.title() }.to_string();
975    let notes = unsafe { reminder.notes() }.map(|n| n.to_string());
976    let completed = unsafe { reminder.isCompleted() };
977    let priority = unsafe { reminder.priority() };
978    let cal = unsafe { reminder.calendar() };
979    let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
980    let calendar_id = cal
981        .as_ref()
982        .map(|c| unsafe { c.calendarIdentifier() }.to_string());
983
984    // Extract due date from dueDateComponents
985    let due_date = unsafe { reminder.dueDateComponents() }
986        .and_then(|components| date_components_to_datetime(&components));
987
988    // Extract start date from startDateComponents
989    let start_date = unsafe { reminder.startDateComponents() }
990        .and_then(|components| date_components_to_datetime(&components));
991
992    // Extract completion date
993    let completion_date =
994        unsafe { reminder.completionDate() }.map(|date| nsdate_to_datetime(&date));
995
996    // Extract additional fields from EKCalendarItem parent class
997    let external_identifier =
998        unsafe { reminder.calendarItemExternalIdentifier() }.map(|id| id.to_string());
999    let location = unsafe { reminder.location() }.map(|loc| loc.to_string());
1000    let url = unsafe { reminder.URL() }
1001        .as_ref()
1002        .and_then(|url_ref| url_ref.absoluteString())
1003        .map(|abs_str| abs_str.to_string());
1004    let creation_date = unsafe { reminder.creationDate() }.map(|date| nsdate_to_datetime(&date));
1005    let last_modified_date =
1006        unsafe { reminder.lastModifiedDate() }.map(|date| nsdate_to_datetime(&date));
1007    let timezone = unsafe { reminder.timeZone() }.map(|tz| tz.name().to_string());
1008    let has_alarms = unsafe { reminder.hasAlarms() };
1009    let has_recurrence_rules = unsafe { reminder.hasRecurrenceRules() };
1010    let has_attendees = unsafe { reminder.hasAttendees() };
1011    let has_notes = unsafe { reminder.hasNotes() };
1012
1013    ReminderItem {
1014        identifier,
1015        title,
1016        notes,
1017        completed,
1018        priority,
1019        calendar_title,
1020        calendar_id,
1021        due_date,
1022        start_date,
1023        completion_date,
1024        external_identifier,
1025        location,
1026        url,
1027        creation_date,
1028        last_modified_date,
1029        timezone,
1030        has_alarms,
1031        has_recurrence_rules,
1032        has_attendees,
1033        has_notes,
1034        attendees: get_item_attendees(reminder),
1035    }
1036}
1037
1038// Helper function to convert EKCalendar to CalendarInfo
1039fn source_to_info(source: &EKSource) -> SourceInfo {
1040    let identifier = unsafe { source.sourceIdentifier() }.to_string();
1041    let title = unsafe { source.title() }.to_string();
1042    // EKSourceType: 0=Local, 1=Exchange, 2=CalDAV, 3=MobileMe, 4=Subscribed, 5=Birthdays
1043    let source_type = unsafe { source.sourceType() };
1044    let source_type = match source_type.0 {
1045        0 => "local",
1046        1 => "exchange",
1047        2 => "caldav",
1048        3 => "mobileme",
1049        4 => "subscribed",
1050        5 => "birthdays",
1051        _ => "unknown",
1052    }
1053    .to_string();
1054
1055    SourceInfo {
1056        identifier,
1057        title,
1058        source_type,
1059    }
1060}
1061
1062fn calendar_to_info(calendar: &EKCalendar) -> CalendarInfo {
1063    let identifier = unsafe { calendar.calendarIdentifier() }.to_string();
1064    let title = unsafe { calendar.title() }.to_string();
1065    let source = unsafe { calendar.source() }.map(|s| unsafe { s.title() }.to_string());
1066    let source_id =
1067        unsafe { calendar.source() }.map(|s| unsafe { s.sourceIdentifier() }.to_string());
1068    let allows_modifications = unsafe { calendar.allowsContentModifications() };
1069    let is_immutable = unsafe { calendar.isImmutable() };
1070    let is_subscribed = unsafe { calendar.isSubscribed() };
1071
1072    // Calendar type: Local=0, CalDAV=1, Exchange=2, Subscription=3, Birthday=4
1073    let cal_type = unsafe { calendar.r#type() };
1074    let calendar_type = match cal_type.0 {
1075        0 => CalendarType::Local,
1076        1 => CalendarType::CalDAV,
1077        2 => CalendarType::Exchange,
1078        3 => CalendarType::Subscription,
1079        4 => CalendarType::Birthday,
1080        _ => CalendarType::Unknown,
1081    };
1082
1083    // Read RGBA from CGColor
1084    let color: Option<(f64, f64, f64, f64)> = unsafe {
1085        calendar.CGColor().and_then(|cg| {
1086            use objc2_core_graphics::CGColor as CG;
1087            let n = CG::number_of_components(Some(&cg));
1088            if n >= 3 {
1089                let ptr = CG::components(Some(&cg));
1090                let r = *ptr;
1091                let g = *ptr.add(1);
1092                let b = *ptr.add(2);
1093                let a = if n >= 4 { *ptr.add(3) } else { 1.0 };
1094                Some((r, g, b, a))
1095            } else {
1096                None
1097            }
1098        })
1099    };
1100
1101    // Allowed entity types
1102    let entity_mask = unsafe { calendar.allowedEntityTypes() };
1103    let mut allowed_entity_types = Vec::new();
1104    if entity_mask.0 & 1 != 0 {
1105        allowed_entity_types.push("event".to_string());
1106    }
1107    if entity_mask.0 & 2 != 0 {
1108        allowed_entity_types.push("reminder".to_string());
1109    }
1110
1111    CalendarInfo {
1112        identifier,
1113        title,
1114        source,
1115        source_id,
1116        calendar_type,
1117        allows_modifications,
1118        is_immutable,
1119        is_subscribed,
1120        color,
1121        allowed_entity_types,
1122    }
1123}
1124
1125// ============================================================================
1126// Calendar Events Support
1127// ============================================================================
1128
1129/// Event availability for scheduling.
1130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1131pub enum EventAvailability {
1132    NotSupported,
1133    Busy,
1134    Free,
1135    Tentative,
1136    Unavailable,
1137}
1138
1139/// Event status.
1140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1141pub enum EventStatus {
1142    None,
1143    Confirmed,
1144    Tentative,
1145    Canceled,
1146}
1147
1148/// Participant role in an event.
1149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1150pub enum ParticipantRole {
1151    Unknown,
1152    Required,
1153    Optional,
1154    Chair,
1155    NonParticipant,
1156}
1157
1158/// Participant RSVP status.
1159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1160pub enum ParticipantStatus {
1161    Unknown,
1162    Pending,
1163    Accepted,
1164    Declined,
1165    Tentative,
1166    Delegated,
1167    Completed,
1168    InProcess,
1169}
1170
1171/// A participant (attendee) on an event or reminder.
1172#[derive(Debug, Clone)]
1173pub struct ParticipantInfo {
1174    pub name: Option<String>,
1175    pub url: Option<String>,
1176    pub role: ParticipantRole,
1177    pub status: ParticipantStatus,
1178    pub is_current_user: bool,
1179}
1180
1181/// Represents a calendar event with its properties.
1182#[derive(Debug, Clone)]
1183pub struct EventItem {
1184    /// Unique identifier for the event
1185    pub identifier: String,
1186    /// Title of the event
1187    pub title: String,
1188    /// Optional notes/description
1189    pub notes: Option<String>,
1190    /// Optional location (string)
1191    pub location: Option<String>,
1192    /// Start date/time
1193    pub start_date: DateTime<Local>,
1194    /// End date/time
1195    pub end_date: DateTime<Local>,
1196    /// Whether this is an all-day event
1197    pub all_day: bool,
1198    /// Calendar the event belongs to
1199    pub calendar_title: Option<String>,
1200    /// Calendar identifier
1201    pub calendar_id: Option<String>,
1202    /// URL associated with the event
1203    pub url: Option<String>,
1204    /// Availability for scheduling
1205    pub availability: EventAvailability,
1206    /// Event status (read-only)
1207    pub status: EventStatus,
1208    /// Whether this occurrence was modified from its recurring series
1209    pub is_detached: bool,
1210    /// Original date in a recurring series
1211    pub occurrence_date: Option<DateTime<Local>>,
1212    /// Geo-coordinate location
1213    pub structured_location: Option<StructuredLocation>,
1214    /// Attendees
1215    pub attendees: Vec<ParticipantInfo>,
1216    /// Event organizer
1217    pub organizer: Option<ParticipantInfo>,
1218}
1219
1220/// The events manager providing access to Calendar events via EventKit
1221pub struct EventsManager {
1222    store: Retained<EKEventStore>,
1223}
1224
1225impl EventsManager {
1226    /// Creates a new EventsManager instance
1227    pub fn new() -> Self {
1228        let store = unsafe { EKEventStore::new() };
1229        Self { store }
1230    }
1231
1232    /// Gets the current authorization status for calendar events
1233    pub fn authorization_status() -> AuthorizationStatus {
1234        let status = unsafe { EKEventStore::authorizationStatusForEntityType(EKEntityType::Event) };
1235        status.into()
1236    }
1237
1238    /// Requests full access to calendar events (blocking)
1239    ///
1240    /// Returns Ok(true) if access was granted, Ok(false) if denied
1241    pub fn request_access(&self) -> Result<bool> {
1242        let result = Arc::new((Mutex::new(None::<(bool, Option<String>)>), Condvar::new()));
1243        let result_clone = Arc::clone(&result);
1244
1245        let completion = RcBlock::new(move |granted: Bool, error: *mut NSError| {
1246            let error_msg = if !error.is_null() {
1247                let error_ref = unsafe { &*error };
1248                Some(format!("{:?}", error_ref))
1249            } else {
1250                None
1251            };
1252
1253            let (lock, cvar) = &*result_clone;
1254            let mut res = lock.lock().unwrap();
1255            *res = Some((granted.as_bool(), error_msg));
1256            cvar.notify_one();
1257        });
1258
1259        unsafe {
1260            let block_ptr = &*completion as *const _ as *mut _;
1261            self.store
1262                .requestFullAccessToEventsWithCompletion(block_ptr);
1263        }
1264
1265        let (lock, cvar) = &*result;
1266        let mut res = lock.lock().unwrap();
1267        while res.is_none() {
1268            res = cvar.wait(res).unwrap();
1269        }
1270
1271        match res.take() {
1272            Some((granted, None)) => Ok(granted),
1273            Some((_, Some(error))) => Err(EventKitError::AuthorizationRequestFailed(error)),
1274            None => Err(EventKitError::AuthorizationRequestFailed(
1275                "Unknown error".to_string(),
1276            )),
1277        }
1278    }
1279
1280    /// Ensures we have authorization, requesting if needed
1281    pub fn ensure_authorized(&self) -> Result<()> {
1282        match Self::authorization_status() {
1283            AuthorizationStatus::FullAccess => Ok(()),
1284            AuthorizationStatus::NotDetermined => {
1285                if self.request_access()? {
1286                    Ok(())
1287                } else {
1288                    Err(EventKitError::AuthorizationDenied)
1289                }
1290            }
1291            AuthorizationStatus::Denied => Err(EventKitError::AuthorizationDenied),
1292            AuthorizationStatus::Restricted => Err(EventKitError::AuthorizationRestricted),
1293            AuthorizationStatus::WriteOnly => Ok(()),
1294        }
1295    }
1296
1297    /// Lists all event calendars
1298    pub fn list_calendars(&self) -> Result<Vec<CalendarInfo>> {
1299        self.ensure_authorized()?;
1300
1301        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1302
1303        let mut result = Vec::new();
1304        for calendar in calendars.iter() {
1305            result.push(calendar_to_info(&calendar));
1306        }
1307
1308        Ok(result)
1309    }
1310
1311    /// Gets the default calendar for new events
1312    pub fn default_calendar(&self) -> Result<CalendarInfo> {
1313        self.ensure_authorized()?;
1314
1315        let calendar = unsafe { self.store.defaultCalendarForNewEvents() };
1316
1317        match calendar {
1318            Some(cal) => Ok(calendar_to_info(&cal)),
1319            None => Err(EventKitError::NoDefaultCalendar),
1320        }
1321    }
1322
1323    /// Fetches events for today
1324    pub fn fetch_today_events(&self) -> Result<Vec<EventItem>> {
1325        let now = Local::now();
1326        let start = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
1327        let end = now.date_naive().and_hms_opt(23, 59, 59).unwrap();
1328
1329        self.fetch_events(
1330            Local.from_local_datetime(&start).unwrap(),
1331            Local.from_local_datetime(&end).unwrap(),
1332            None,
1333        )
1334    }
1335
1336    /// Fetches events for the next N days
1337    pub fn fetch_upcoming_events(&self, days: i64) -> Result<Vec<EventItem>> {
1338        let now = Local::now();
1339        let end = now + Duration::days(days);
1340        self.fetch_events(now, end, None)
1341    }
1342
1343    /// Fetches events in a date range
1344    pub fn fetch_events(
1345        &self,
1346        start: DateTime<Local>,
1347        end: DateTime<Local>,
1348        calendar_titles: Option<&[&str]>,
1349    ) -> Result<Vec<EventItem>> {
1350        self.ensure_authorized()?;
1351
1352        if start >= end {
1353            return Err(EventKitError::InvalidDateRange);
1354        }
1355
1356        let calendars: Option<Retained<NSArray<EKCalendar>>> = match calendar_titles {
1357            Some(titles) => {
1358                let all_calendars =
1359                    unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1360                let mut matching: Vec<Retained<EKCalendar>> = Vec::new();
1361
1362                for cal in all_calendars.iter() {
1363                    let title = unsafe { cal.title() };
1364                    let title_str = title.to_string();
1365                    if titles.iter().any(|t| *t == title_str) {
1366                        matching.push(cal.retain());
1367                    }
1368                }
1369
1370                if matching.is_empty() {
1371                    return Err(EventKitError::CalendarNotFound(titles.join(", ")));
1372                }
1373
1374                Some(NSArray::from_retained_slice(&matching))
1375            }
1376            None => None,
1377        };
1378
1379        let start_date = datetime_to_nsdate(start);
1380        let end_date = datetime_to_nsdate(end);
1381
1382        let predicate = unsafe {
1383            self.store
1384                .predicateForEventsWithStartDate_endDate_calendars(
1385                    &start_date,
1386                    &end_date,
1387                    calendars.as_deref(),
1388                )
1389        };
1390
1391        let events = unsafe { self.store.eventsMatchingPredicate(&predicate) };
1392
1393        let mut items = Vec::new();
1394        for event in events.iter() {
1395            items.push(event_to_item(&event));
1396        }
1397
1398        // Sort by start date
1399        items.sort_by(|a, b| a.start_date.cmp(&b.start_date));
1400
1401        Ok(items)
1402    }
1403
1404    /// Creates a new event
1405    #[allow(clippy::too_many_arguments)]
1406    pub fn create_event(
1407        &self,
1408        title: &str,
1409        start: DateTime<Local>,
1410        end: DateTime<Local>,
1411        notes: Option<&str>,
1412        location: Option<&str>,
1413        calendar_title: Option<&str>,
1414        all_day: bool,
1415    ) -> Result<EventItem> {
1416        self.ensure_authorized()?;
1417
1418        let event = unsafe { EKEvent::eventWithEventStore(&self.store) };
1419
1420        // Set title
1421        let ns_title = NSString::from_str(title);
1422        unsafe { event.setTitle(Some(&ns_title)) };
1423
1424        // Set dates
1425        let start_date = datetime_to_nsdate(start);
1426        let end_date = datetime_to_nsdate(end);
1427        unsafe {
1428            event.setStartDate(Some(&start_date));
1429            event.setEndDate(Some(&end_date));
1430            event.setAllDay(all_day);
1431        }
1432
1433        // Set notes if provided
1434        if let Some(notes_text) = notes {
1435            let ns_notes = NSString::from_str(notes_text);
1436            unsafe { event.setNotes(Some(&ns_notes)) };
1437        }
1438
1439        // Set location if provided
1440        if let Some(loc) = location {
1441            let ns_location = NSString::from_str(loc);
1442            unsafe { event.setLocation(Some(&ns_location)) };
1443        }
1444
1445        // Set calendar
1446        let calendar = if let Some(cal_title) = calendar_title {
1447            self.find_calendar_by_title(cal_title)?
1448        } else {
1449            unsafe { self.store.defaultCalendarForNewEvents() }
1450                .ok_or(EventKitError::NoDefaultCalendar)?
1451        };
1452        unsafe { event.setCalendar(Some(&calendar)) };
1453
1454        // Save
1455        unsafe {
1456            self.store
1457                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1458                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1459        }
1460
1461        Ok(event_to_item(&event))
1462    }
1463
1464    /// Updates an existing event
1465    pub fn update_event(
1466        &self,
1467        identifier: &str,
1468        title: Option<&str>,
1469        notes: Option<&str>,
1470        location: Option<&str>,
1471        start: Option<DateTime<Local>>,
1472        end: Option<DateTime<Local>>,
1473    ) -> Result<EventItem> {
1474        self.ensure_authorized()?;
1475
1476        let event = self.find_event_by_id(identifier)?;
1477
1478        if let Some(t) = title {
1479            let ns_title = NSString::from_str(t);
1480            unsafe { event.setTitle(Some(&ns_title)) };
1481        }
1482
1483        if let Some(n) = notes {
1484            let ns_notes = NSString::from_str(n);
1485            unsafe { event.setNotes(Some(&ns_notes)) };
1486        }
1487
1488        if let Some(l) = location {
1489            let ns_location = NSString::from_str(l);
1490            unsafe { event.setLocation(Some(&ns_location)) };
1491        }
1492
1493        if let Some(s) = start {
1494            let start_date = datetime_to_nsdate(s);
1495            unsafe { event.setStartDate(Some(&start_date)) };
1496        }
1497
1498        if let Some(e) = end {
1499            let end_date = datetime_to_nsdate(e);
1500            unsafe { event.setEndDate(Some(&end_date)) };
1501        }
1502
1503        unsafe {
1504            self.store
1505                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1506                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1507        }
1508
1509        Ok(event_to_item(&event))
1510    }
1511
1512    /// Deletes an event
1513    pub fn delete_event(&self, identifier: &str, affect_future: bool) -> Result<()> {
1514        self.ensure_authorized()?;
1515
1516        let event = self.find_event_by_id(identifier)?;
1517        let span = if affect_future {
1518            EKSpan::FutureEvents
1519        } else {
1520            EKSpan::ThisEvent
1521        };
1522
1523        unsafe {
1524            self.store
1525                .removeEvent_span_commit_error(&event, span, true)
1526                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1527        }
1528
1529        Ok(())
1530    }
1531
1532    /// Gets an event by its identifier
1533    pub fn get_event(&self, identifier: &str) -> Result<EventItem> {
1534        self.ensure_authorized()?;
1535        let event = self.find_event_by_id(identifier)?;
1536        Ok(event_to_item(&event))
1537    }
1538
1539    // ========================================================================
1540    // Event Calendar Management
1541    // ========================================================================
1542
1543    /// Creates a new event calendar.
1544    pub fn create_event_calendar(&self, title: &str) -> Result<CalendarInfo> {
1545        self.ensure_authorized()?;
1546        let calendar = unsafe {
1547            EKCalendar::calendarForEntityType_eventStore(EKEntityType::Event, &self.store)
1548        };
1549        let ns_title = NSString::from_str(title);
1550        unsafe { calendar.setTitle(&ns_title) };
1551
1552        // Use the default source
1553        if let Some(default_cal) = unsafe { self.store.defaultCalendarForNewEvents() }
1554            && let Some(source) = unsafe { default_cal.source() }
1555        {
1556            unsafe { calendar.setSource(Some(&source)) };
1557        }
1558
1559        unsafe {
1560            self.store
1561                .saveCalendar_commit_error(&calendar, true)
1562                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1563        }
1564        Ok(calendar_to_info(&calendar))
1565    }
1566
1567    /// Renames an event calendar.
1568    /// Rename an event calendar (backward compat wrapper).
1569    pub fn rename_event_calendar(&self, identifier: &str, new_title: &str) -> Result<CalendarInfo> {
1570        self.update_event_calendar(identifier, Some(new_title), None)
1571    }
1572
1573    /// Update an event calendar — name, color, or both.
1574    pub fn update_event_calendar(
1575        &self,
1576        identifier: &str,
1577        new_title: Option<&str>,
1578        color_rgba: Option<(f64, f64, f64, f64)>,
1579    ) -> Result<CalendarInfo> {
1580        self.ensure_authorized()?;
1581        let calendar = unsafe {
1582            self.store
1583                .calendarWithIdentifier(&NSString::from_str(identifier))
1584        }
1585        .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1586
1587        if let Some(title) = new_title {
1588            let ns_title = NSString::from_str(title);
1589            unsafe { calendar.setTitle(&ns_title) };
1590        }
1591
1592        if let Some((r, g, b, a)) = color_rgba {
1593            let cg = objc2_core_graphics::CGColor::new_srgb(r, g, b, a);
1594            unsafe { calendar.setCGColor(Some(&cg)) };
1595        }
1596
1597        unsafe {
1598            self.store
1599                .saveCalendar_commit_error(&calendar, true)
1600                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1601        }
1602        Ok(calendar_to_info(&calendar))
1603    }
1604
1605    /// Deletes an event calendar.
1606    pub fn delete_event_calendar(&self, identifier: &str) -> Result<()> {
1607        self.ensure_authorized()?;
1608        let calendar = unsafe {
1609            self.store
1610                .calendarWithIdentifier(&NSString::from_str(identifier))
1611        }
1612        .ok_or_else(|| EventKitError::CalendarNotFound(identifier.to_string()))?;
1613
1614        unsafe {
1615            self.store
1616                .removeCalendar_commit_error(&calendar, true)
1617                .map_err(|e| EventKitError::DeleteFailed(format!("{:?}", e)))?;
1618        }
1619        Ok(())
1620    }
1621
1622    // ========================================================================
1623    // Event Alarm Management (shared via EKCalendarItem)
1624    // ========================================================================
1625
1626    /// Lists all alarms on an event.
1627    pub fn get_event_alarms(&self, identifier: &str) -> Result<Vec<AlarmInfo>> {
1628        self.ensure_authorized()?;
1629        let event = self.find_event_by_id(identifier)?;
1630        Ok(get_item_alarms(&event))
1631    }
1632
1633    /// Adds an alarm to an event.
1634    pub fn add_event_alarm(&self, identifier: &str, alarm: &AlarmInfo) -> Result<()> {
1635        self.ensure_authorized()?;
1636        let event = self.find_event_by_id(identifier)?;
1637        add_item_alarm(&event, alarm);
1638        unsafe {
1639            self.store
1640                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1641                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1642        }
1643        Ok(())
1644    }
1645
1646    // ========================================================================
1647    // Event Recurrence Management (shared via EKCalendarItem)
1648    // ========================================================================
1649
1650    /// Gets recurrence rules on an event.
1651    pub fn get_event_recurrence_rules(&self, identifier: &str) -> Result<Vec<RecurrenceRule>> {
1652        self.ensure_authorized()?;
1653        let event = self.find_event_by_id(identifier)?;
1654        Ok(get_item_recurrence_rules(&event))
1655    }
1656
1657    /// Sets a recurrence rule on an event (replaces any existing rules).
1658    pub fn set_event_recurrence_rule(&self, identifier: &str, rule: &RecurrenceRule) -> Result<()> {
1659        self.ensure_authorized()?;
1660        let event = self.find_event_by_id(identifier)?;
1661        set_item_recurrence_rule(&event, rule);
1662        unsafe {
1663            self.store
1664                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1665                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1666        }
1667        Ok(())
1668    }
1669
1670    /// Removes all recurrence rules from an event.
1671    pub fn remove_event_recurrence_rules(&self, identifier: &str) -> Result<()> {
1672        self.ensure_authorized()?;
1673        let event = self.find_event_by_id(identifier)?;
1674        clear_item_recurrence_rules(&event);
1675        unsafe {
1676            self.store
1677                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1678                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1679        }
1680        Ok(())
1681    }
1682
1683    /// Removes a specific alarm from an event by index.
1684    pub fn remove_event_alarm(&self, identifier: &str, index: usize) -> Result<()> {
1685        self.ensure_authorized()?;
1686        let event = self.find_event_by_id(identifier)?;
1687        remove_item_alarm(&event, index)?;
1688        unsafe {
1689            self.store
1690                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1691                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1692        }
1693        Ok(())
1694    }
1695
1696    /// Set or clear the URL on an event.
1697    pub fn set_event_url(&self, identifier: &str, url: Option<&str>) -> Result<()> {
1698        self.ensure_authorized()?;
1699        let event = self.find_event_by_id(identifier)?;
1700        set_item_url(&event, url);
1701        unsafe {
1702            self.store
1703                .saveEvent_span_commit_error(&event, EKSpan::ThisEvent, true)
1704                .map_err(|e| EventKitError::SaveFailed(format!("{:?}", e)))?;
1705        }
1706        Ok(())
1707    }
1708
1709    // Helper to find a calendar by title
1710    fn find_calendar_by_title(&self, title: &str) -> Result<Retained<EKCalendar>> {
1711        let calendars = unsafe { self.store.calendarsForEntityType(EKEntityType::Event) };
1712
1713        for cal in calendars.iter() {
1714            let cal_title = unsafe { cal.title() };
1715            if cal_title.to_string() == title {
1716                return Ok(cal.retain());
1717            }
1718        }
1719
1720        Err(EventKitError::CalendarNotFound(title.to_string()))
1721    }
1722
1723    // Helper to find an event by identifier
1724    fn find_event_by_id(&self, identifier: &str) -> Result<Retained<EKEvent>> {
1725        let ns_id = NSString::from_str(identifier);
1726        let event = unsafe { self.store.eventWithIdentifier(&ns_id) };
1727
1728        match event {
1729            Some(e) => Ok(e),
1730            None => Err(EventKitError::ItemNotFound(identifier.to_string())),
1731        }
1732    }
1733}
1734
1735impl Default for EventsManager {
1736    fn default() -> Self {
1737        Self::new()
1738    }
1739}
1740
1741// Helper function to convert EKEvent to EventItem
1742fn event_to_item(event: &EKEvent) -> EventItem {
1743    let identifier = unsafe { event.eventIdentifier() }
1744        .map(|s| s.to_string())
1745        .unwrap_or_default();
1746    let title = unsafe { event.title() }.to_string();
1747    let notes = unsafe { event.notes() }.map(|n| n.to_string());
1748    let location = unsafe { event.location() }.map(|l| l.to_string());
1749    let all_day = unsafe { event.isAllDay() };
1750    let cal = unsafe { event.calendar() };
1751    let calendar_title = cal.as_ref().map(|c| unsafe { c.title() }.to_string());
1752    let calendar_id = cal
1753        .as_ref()
1754        .map(|c| unsafe { c.calendarIdentifier() }.to_string());
1755
1756    let start_ns: Retained<NSDate> = unsafe { event.startDate() };
1757    let end_ns: Retained<NSDate> = unsafe { event.endDate() };
1758    let start_date = nsdate_to_datetime(&start_ns);
1759    let end_date = nsdate_to_datetime(&end_ns);
1760
1761    let url = get_item_url(event);
1762
1763    // Availability: -1=NotSupported, 0=Busy, 1=Free, 2=Tentative, 3=Unavailable
1764    let avail = unsafe { event.availability() };
1765    let availability = match avail.0 {
1766        0 => EventAvailability::Busy,
1767        1 => EventAvailability::Free,
1768        2 => EventAvailability::Tentative,
1769        3 => EventAvailability::Unavailable,
1770        _ => EventAvailability::NotSupported,
1771    };
1772
1773    // Status: 0=None, 1=Confirmed, 2=Tentative, 3=Canceled
1774    let stat = unsafe { event.status() };
1775    let status = match stat.0 {
1776        1 => EventStatus::Confirmed,
1777        2 => EventStatus::Tentative,
1778        3 => EventStatus::Canceled,
1779        _ => EventStatus::None,
1780    };
1781
1782    let is_detached = unsafe { event.isDetached() };
1783    let occurrence_date = unsafe { event.occurrenceDate() }.map(|d| nsdate_to_datetime(&d));
1784
1785    // Structured location
1786    let structured_location = unsafe { event.structuredLocation() }.map(|loc| {
1787        let title = unsafe { loc.title() }
1788            .map(|t| t.to_string())
1789            .unwrap_or_default();
1790        let radius = unsafe { loc.radius() };
1791        let (latitude, longitude) = unsafe { loc.geoLocation() }
1792            .map(|geo| {
1793                let coord = unsafe { geo.coordinate() };
1794                (coord.latitude, coord.longitude)
1795            })
1796            .unwrap_or((0.0, 0.0));
1797        StructuredLocation {
1798            title,
1799            latitude,
1800            longitude,
1801            radius,
1802        }
1803    });
1804
1805    // Attendees (shared via EKCalendarItem)
1806    let attendees = get_item_attendees(event);
1807
1808    // Organizer (event-only)
1809    let organizer = unsafe { event.organizer() }.map(|p| participant_to_info(&p));
1810
1811    EventItem {
1812        identifier,
1813        title,
1814        notes,
1815        location,
1816        start_date,
1817        end_date,
1818        all_day,
1819        calendar_title,
1820        calendar_id,
1821        url,
1822        availability,
1823        status,
1824        is_detached,
1825        occurrence_date,
1826        structured_location,
1827        attendees,
1828        organizer,
1829    }
1830}
1831
1832// Read attendees from an EKCalendarItem (shared by events and reminders)
1833fn get_item_attendees(item: &EKCalendarItem) -> Vec<ParticipantInfo> {
1834    let attendees = unsafe { item.attendees() };
1835    let Some(attendees) = attendees else {
1836        return Vec::new();
1837    };
1838    let mut result = Vec::new();
1839    for i in 0..attendees.len() {
1840        let p = attendees.objectAtIndex(i);
1841        result.push(participant_to_info(&p));
1842    }
1843    result
1844}
1845
1846// Convert an EKParticipant to ParticipantInfo
1847fn participant_to_info(p: &objc2_event_kit::EKParticipant) -> ParticipantInfo {
1848    let name = unsafe { p.name() }.map(|n| n.to_string());
1849    let url = unsafe { p.URL() }.absoluteString().map(|s| s.to_string());
1850
1851    // Role: 0=Unknown, 1=Required, 2=Optional, 3=Chair, 4=NonParticipant
1852    let role = unsafe { p.participantRole() };
1853    let role = match role.0 {
1854        1 => ParticipantRole::Required,
1855        2 => ParticipantRole::Optional,
1856        3 => ParticipantRole::Chair,
1857        4 => ParticipantRole::NonParticipant,
1858        _ => ParticipantRole::Unknown,
1859    };
1860
1861    // Status: 0=Unknown, 1=Pending, 2=Accepted, 3=Declined, 4=Tentative,
1862    //         5=Delegated, 6=Completed, 7=InProcess
1863    let status = unsafe { p.participantStatus() };
1864    let status = match status.0 {
1865        1 => ParticipantStatus::Pending,
1866        2 => ParticipantStatus::Accepted,
1867        3 => ParticipantStatus::Declined,
1868        4 => ParticipantStatus::Tentative,
1869        5 => ParticipantStatus::Delegated,
1870        6 => ParticipantStatus::Completed,
1871        7 => ParticipantStatus::InProcess,
1872        _ => ParticipantStatus::Unknown,
1873    };
1874
1875    let is_current_user = unsafe { p.isCurrentUser() };
1876
1877    ParticipantInfo {
1878        name,
1879        url,
1880        role,
1881        status,
1882        is_current_user,
1883    }
1884}
1885
1886// Helper to convert chrono DateTime to NSDate
1887fn datetime_to_nsdate(dt: DateTime<Local>) -> Retained<NSDate> {
1888    let timestamp = dt.timestamp() as f64;
1889    NSDate::dateWithTimeIntervalSince1970(timestamp)
1890}
1891
1892// Helper to convert NSDate to chrono DateTime
1893fn nsdate_to_datetime(date: &NSDate) -> DateTime<Local> {
1894    let timestamp = date.timeIntervalSince1970();
1895    Local.timestamp_opt(timestamp as i64, 0).unwrap()
1896}
1897
1898// Helper to convert NSDateComponents to chrono DateTime
1899fn date_components_to_datetime(components: &NSDateComponents) -> Option<DateTime<Local>> {
1900    // Get a calendar to convert components to a date
1901    let calendar = NSCalendar::currentCalendar();
1902
1903    // Convert components to NSDate using the calendar
1904    let date = calendar.dateFromComponents(components)?;
1905
1906    Some(nsdate_to_datetime(&date))
1907}
1908
1909// Helper to convert chrono DateTime to NSDateComponents
1910fn datetime_to_date_components(dt: DateTime<Local>) -> Retained<NSDateComponents> {
1911    let components = NSDateComponents::new();
1912
1913    components.setYear(dt.year() as isize);
1914    components.setMonth(dt.month() as isize);
1915    components.setDay(dt.day() as isize);
1916    components.setHour(dt.hour() as isize);
1917    components.setMinute(dt.minute() as isize);
1918    components.setSecond(dt.second() as isize);
1919
1920    components
1921}
1922
1923// ============================================================================
1924// Shared EKCalendarItem operations
1925// ============================================================================
1926// EKCalendarItem is the base class for both EKReminder and EKEvent.
1927// These functions operate on the shared interface — both types auto-deref to it.
1928
1929/// Read all alarms from a calendar item.
1930fn get_item_alarms(item: &EKCalendarItem) -> Vec<AlarmInfo> {
1931    let alarms = unsafe { item.alarms() };
1932    let Some(alarms) = alarms else {
1933        return Vec::new();
1934    };
1935    let mut result = Vec::new();
1936    for i in 0..alarms.len() {
1937        let alarm = alarms.objectAtIndex(i);
1938        result.push(alarm_to_info(&alarm));
1939    }
1940    result
1941}
1942
1943/// Add an alarm to a calendar item.
1944fn add_item_alarm(item: &EKCalendarItem, alarm: &AlarmInfo) {
1945    let ek_alarm = create_ek_alarm(alarm);
1946    unsafe { item.addAlarm(&ek_alarm) };
1947}
1948
1949/// Remove an alarm from a calendar item by index.
1950fn remove_item_alarm(item: &EKCalendarItem, index: usize) -> Result<()> {
1951    let alarms = unsafe { item.alarms() };
1952    let Some(alarms) = alarms else {
1953        return Err(EventKitError::ItemNotFound("No alarms on this item".into()));
1954    };
1955    if index >= alarms.len() {
1956        return Err(EventKitError::ItemNotFound(format!(
1957            "Alarm index {} out of range ({})",
1958            index,
1959            alarms.len()
1960        )));
1961    }
1962    let alarm = alarms.objectAtIndex(index);
1963    unsafe { item.removeAlarm(&alarm) };
1964    Ok(())
1965}
1966
1967/// Clear all alarms from a calendar item.
1968fn clear_item_alarms(item: &EKCalendarItem) {
1969    unsafe { item.setAlarms(None) };
1970}
1971
1972/// Read all recurrence rules from a calendar item.
1973fn get_item_recurrence_rules(item: &EKCalendarItem) -> Vec<RecurrenceRule> {
1974    let rules = unsafe { item.recurrenceRules() };
1975    let Some(rules) = rules else {
1976        return Vec::new();
1977    };
1978    let mut result = Vec::new();
1979    for i in 0..rules.len() {
1980        let rule = rules.objectAtIndex(i);
1981        result.push(recurrence_rule_to_info(&rule));
1982    }
1983    result
1984}
1985
1986/// Set a single recurrence rule on a calendar item (replaces any existing).
1987fn set_item_recurrence_rule(item: &EKCalendarItem, rule: &RecurrenceRule) {
1988    let ek_rule = create_ek_recurrence_rule(rule);
1989    unsafe {
1990        let rules = NSArray::from_retained_slice(&[ek_rule]);
1991        item.setRecurrenceRules(Some(&rules));
1992    }
1993}
1994
1995/// Clear all recurrence rules from a calendar item.
1996fn clear_item_recurrence_rules(item: &EKCalendarItem) {
1997    unsafe { item.setRecurrenceRules(None) };
1998}
1999
2000/// Set URL on a calendar item.
2001fn set_item_url(item: &EKCalendarItem, url: Option<&str>) {
2002    unsafe {
2003        let ns_url = url.map(|u| {
2004            let ns_str = NSString::from_str(u);
2005            objc2_foundation::NSURL::URLWithString(&ns_str).unwrap()
2006        });
2007        item.setURL(ns_url.as_deref());
2008    }
2009}
2010
2011/// Read URL from a calendar item.
2012fn get_item_url(item: &EKCalendarItem) -> Option<String> {
2013    unsafe { item.URL() }.map(|u| u.absoluteString().unwrap().to_string())
2014}
2015
2016// ============================================================================
2017// Type conversion helpers
2018// ============================================================================
2019
2020// Helper to convert an EKRecurrenceRule to a RecurrenceRule
2021fn recurrence_rule_to_info(rule: &EKRecurrenceRule) -> RecurrenceRule {
2022    let frequency = unsafe { rule.frequency() };
2023    let frequency = match frequency {
2024        EKRecurrenceFrequency::Daily => RecurrenceFrequency::Daily,
2025        EKRecurrenceFrequency::Weekly => RecurrenceFrequency::Weekly,
2026        EKRecurrenceFrequency::Monthly => RecurrenceFrequency::Monthly,
2027        EKRecurrenceFrequency::Yearly => RecurrenceFrequency::Yearly,
2028        _ => RecurrenceFrequency::Daily,
2029    };
2030
2031    let interval = unsafe { rule.interval() } as usize;
2032
2033    let end = unsafe { rule.recurrenceEnd() }
2034        .map(|end| {
2035            let count = unsafe { end.occurrenceCount() };
2036            if count > 0 {
2037                RecurrenceEndCondition::AfterCount(count)
2038            } else if let Some(date) = unsafe { end.endDate() } {
2039                RecurrenceEndCondition::OnDate(nsdate_to_datetime(&date))
2040            } else {
2041                RecurrenceEndCondition::Never
2042            }
2043        })
2044        .unwrap_or(RecurrenceEndCondition::Never);
2045
2046    let days_of_week = unsafe { rule.daysOfTheWeek() }.map(|days| {
2047        let mut result = Vec::new();
2048        for i in 0..days.len() {
2049            let day = days.objectAtIndex(i);
2050            let weekday = unsafe { day.dayOfTheWeek() };
2051            result.push(weekday.0 as u8);
2052        }
2053        result
2054    });
2055
2056    let days_of_month = unsafe { rule.daysOfTheMonth() }.map(|days| {
2057        let mut result = Vec::new();
2058        for i in 0..days.len() {
2059            let num = days.objectAtIndex(i);
2060            result.push(num.intValue());
2061        }
2062        result
2063    });
2064
2065    RecurrenceRule {
2066        frequency,
2067        interval,
2068        end,
2069        days_of_week,
2070        days_of_month,
2071    }
2072}
2073
2074// Helper to create an EKRecurrenceRule from a RecurrenceRule
2075fn create_ek_recurrence_rule(rule: &RecurrenceRule) -> Retained<EKRecurrenceRule> {
2076    let frequency = match rule.frequency {
2077        RecurrenceFrequency::Daily => EKRecurrenceFrequency::Daily,
2078        RecurrenceFrequency::Weekly => EKRecurrenceFrequency::Weekly,
2079        RecurrenceFrequency::Monthly => EKRecurrenceFrequency::Monthly,
2080        RecurrenceFrequency::Yearly => EKRecurrenceFrequency::Yearly,
2081    };
2082
2083    let end = match &rule.end {
2084        RecurrenceEndCondition::Never => None,
2085        RecurrenceEndCondition::AfterCount(count) => {
2086            Some(unsafe { EKRecurrenceEnd::recurrenceEndWithOccurrenceCount(*count) })
2087        }
2088        RecurrenceEndCondition::OnDate(date) => {
2089            let nsdate = datetime_to_nsdate(*date);
2090            Some(unsafe { EKRecurrenceEnd::recurrenceEndWithEndDate(&nsdate) })
2091        }
2092    };
2093
2094    let days_of_week: Option<Vec<Retained<EKRecurrenceDayOfWeek>>> =
2095        rule.days_of_week.as_ref().map(|days| {
2096            days.iter()
2097                .map(|&d| {
2098                    let weekday = EKWeekday(d as isize);
2099                    unsafe { EKRecurrenceDayOfWeek::dayOfWeek(weekday) }
2100                })
2101                .collect()
2102        });
2103
2104    let days_of_month: Option<Vec<Retained<NSNumber>>> = rule
2105        .days_of_month
2106        .as_ref()
2107        .map(|days| days.iter().map(|&d| NSNumber::new_i32(d)).collect());
2108
2109    let days_of_week_arr = days_of_week
2110        .as_ref()
2111        .map(|v| NSArray::from_retained_slice(v));
2112    let days_of_month_arr = days_of_month
2113        .as_ref()
2114        .map(|v| NSArray::from_retained_slice(v));
2115
2116    unsafe {
2117        use objc2::AnyThread;
2118        EKRecurrenceRule::initRecurrenceWithFrequency_interval_daysOfTheWeek_daysOfTheMonth_monthsOfTheYear_weeksOfTheYear_daysOfTheYear_setPositions_end(
2119            EKRecurrenceRule::alloc(),
2120            frequency,
2121            rule.interval as isize,
2122            days_of_week_arr.as_deref(),
2123            days_of_month_arr.as_deref(),
2124            None, // months of year
2125            None, // weeks of year
2126            None, // days of year
2127            None, // set positions
2128            end.as_deref(),
2129        )
2130    }
2131}
2132
2133// Helper to convert an EKAlarm to an AlarmInfo
2134fn alarm_to_info(alarm: &EKAlarm) -> AlarmInfo {
2135    let relative_offset = unsafe { alarm.relativeOffset() };
2136    let absolute_date = unsafe { alarm.absoluteDate() }.map(|d| nsdate_to_datetime(&d));
2137
2138    let proximity = unsafe { alarm.proximity() };
2139    let proximity = match proximity {
2140        EKAlarmProximity::Enter => AlarmProximity::Enter,
2141        EKAlarmProximity::Leave => AlarmProximity::Leave,
2142        _ => AlarmProximity::None,
2143    };
2144
2145    let location = unsafe { alarm.structuredLocation() }.map(|loc| {
2146        let title = unsafe { loc.title() }
2147            .map(|t| t.to_string())
2148            .unwrap_or_default();
2149        let radius = unsafe { loc.radius() };
2150        let (latitude, longitude) = unsafe { loc.geoLocation() }
2151            .map(|geo| {
2152                let coord = unsafe { geo.coordinate() };
2153                (coord.latitude, coord.longitude)
2154            })
2155            .unwrap_or((0.0, 0.0));
2156
2157        StructuredLocation {
2158            title,
2159            latitude,
2160            longitude,
2161            radius,
2162        }
2163    });
2164
2165    AlarmInfo {
2166        // relativeOffset of 0 means "at time of event" — it's always set
2167        relative_offset: Some(relative_offset),
2168        absolute_date,
2169        proximity,
2170        location,
2171    }
2172}
2173
2174// Helper to create an EKAlarm from an AlarmInfo
2175fn create_ek_alarm(info: &AlarmInfo) -> Retained<EKAlarm> {
2176    let alarm = if let Some(date) = &info.absolute_date {
2177        let nsdate = datetime_to_nsdate(*date);
2178        unsafe { EKAlarm::alarmWithAbsoluteDate(&nsdate) }
2179    } else {
2180        let offset = info.relative_offset.unwrap_or(0.0);
2181        unsafe { EKAlarm::alarmWithRelativeOffset(offset) }
2182    };
2183
2184    // Set proximity
2185    let prox = match info.proximity {
2186        AlarmProximity::Enter => EKAlarmProximity::Enter,
2187        AlarmProximity::Leave => EKAlarmProximity::Leave,
2188        AlarmProximity::None => EKAlarmProximity::None,
2189    };
2190    unsafe { alarm.setProximity(prox) };
2191
2192    // Set structured location if provided
2193    if let Some(loc) = &info.location {
2194        let title = NSString::from_str(&loc.title);
2195        let structured = unsafe { EKStructuredLocation::locationWithTitle(&title) };
2196        unsafe { structured.setRadius(loc.radius) };
2197
2198        // Create CLLocation for the geo coordinates
2199        #[cfg(feature = "location")]
2200        {
2201            use objc2::AnyThread;
2202            use objc2_core_location::CLLocation;
2203            let cl_location = unsafe {
2204                CLLocation::initWithLatitude_longitude(
2205                    CLLocation::alloc(),
2206                    loc.latitude,
2207                    loc.longitude,
2208                )
2209            };
2210            unsafe { structured.setGeoLocation(Some(&cl_location)) };
2211        }
2212
2213        unsafe { alarm.setStructuredLocation(Some(&structured)) };
2214    }
2215
2216    alarm
2217}
2218
2219#[cfg(test)]
2220mod tests {
2221    use super::*;
2222
2223    #[test]
2224    fn test_authorization_status_display() {
2225        assert_eq!(
2226            format!("{}", AuthorizationStatus::NotDetermined),
2227            "Not Determined"
2228        );
2229        assert_eq!(
2230            format!("{}", AuthorizationStatus::FullAccess),
2231            "Full Access"
2232        );
2233    }
2234
2235    #[test]
2236    fn test_event_item_debug() {
2237        let event = EventItem {
2238            identifier: "test".to_string(),
2239            title: "Test Event".to_string(),
2240            notes: None,
2241            location: None,
2242            start_date: Local::now(),
2243            end_date: Local::now(),
2244            all_day: false,
2245            calendar_title: None,
2246            calendar_id: None,
2247            url: None,
2248            availability: EventAvailability::Busy,
2249            status: EventStatus::None,
2250            is_detached: false,
2251            occurrence_date: None,
2252            structured_location: None,
2253            attendees: Vec::new(),
2254            organizer: None,
2255        };
2256        assert!(format!("{:?}", event).contains("Test Event"));
2257    }
2258}