Skip to main content

eventkit/
lib.rs

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