use axum::{extract::State, http::HeaderMap, response::Json}; use chrono::TimeZone; use std::sync::Arc; use crate::calendar::CalDAVClient; use crate::{ models::{ ApiError, CreateEventSeriesRequest, CreateEventSeriesResponse, DeleteEventSeriesRequest, DeleteEventSeriesResponse, UpdateEventSeriesRequest, UpdateEventSeriesResponse, }, AppState, }; use calendar_models::{EventClass, EventStatus, VEvent}; use super::auth::{extract_bearer_token, extract_password_header}; /// Create a new recurring event series pub async fn create_event_series( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!( "📝 Create event series request received: title='{}', recurrence='{}', all_day={}", request.title, request.recurrence, request.all_day ); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.title.trim().is_empty() { return Err(ApiError::BadRequest("Event title is required".to_string())); } if request.title.len() > 200 { return Err(ApiError::BadRequest( "Event title too long (max 200 characters)".to_string(), )); } if request.recurrence == "none" { return Err(ApiError::BadRequest( "Use regular create endpoint for non-recurring events".to_string(), )); } // Validate recurrence type - handle both simple strings and RRULE strings let recurrence_freq = if request.recurrence.contains("FREQ=") { // Parse RRULE to extract frequency if request.recurrence.contains("FREQ=DAILY") { "daily" } else if request.recurrence.contains("FREQ=WEEKLY") { "weekly" } else if request.recurrence.contains("FREQ=MONTHLY") { "monthly" } else if request.recurrence.contains("FREQ=YEARLY") { "yearly" } else { return Err(ApiError::BadRequest( "Invalid RRULE frequency. Must be DAILY, WEEKLY, MONTHLY, or YEARLY".to_string(), )); } } else { // Handle simple strings let lower = request.recurrence.to_lowercase(); match lower.as_str() { "daily" => "daily", "weekly" => "weekly", "monthly" => "monthly", "yearly" => "yearly", _ => { return Err(ApiError::BadRequest( "Invalid recurrence type. Must be daily, weekly, monthly, or yearly" .to_string(), )) } } }; // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Determine which calendar to use let calendar_path = if let Some(ref path) = request.calendar_path { path.clone() } else { // Use the first available calendar let calendar_paths = client .discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; if calendar_paths.is_empty() { return Err(ApiError::BadRequest( "No calendars available for event creation".to_string(), )); } calendar_paths[0].clone() }; println!("📅 Using calendar path: {}", calendar_path); // Parse datetime components let start_date = chrono::NaiveDate::parse_from_str(&request.start_date, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid start_date format. Expected YYYY-MM-DD".to_string()) })?; let (start_datetime, end_datetime) = if request.all_day { // For all-day events, use the dates as-is let start_dt = start_date .and_hms_opt(0, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?; let end_date = if !request.end_date.is_empty() { chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string()) })? } else { start_date }; let end_dt = end_date .and_hms_opt(23, 59, 59) .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; // Convert from local time to UTC let start_local = chrono::Local.from_local_datetime(&start_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?; let end_local = chrono::Local.from_local_datetime(&end_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?; ( start_local.with_timezone(&chrono::Utc), end_local.with_timezone(&chrono::Utc), ) } else { // Parse times for timed events let start_time = if !request.start_time.is_empty() { chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| { ApiError::BadRequest("Invalid start_time format. Expected HH:MM".to_string()) })? } else { chrono::NaiveTime::from_hms_opt(9, 0, 0).unwrap() // Default to 9 AM }; let end_time = if !request.end_time.is_empty() { chrono::NaiveTime::parse_from_str(&request.end_time, "%H:%M").map_err(|_| { ApiError::BadRequest("Invalid end_time format. Expected HH:MM".to_string()) })? } else { chrono::NaiveTime::from_hms_opt(10, 0, 0).unwrap() // Default to 1 hour duration }; let start_dt = start_date.and_time(start_time); let end_dt = if !request.end_date.is_empty() { let end_date = chrono::NaiveDate::parse_from_str(&request.end_date, "%Y-%m-%d") .map_err(|_| { ApiError::BadRequest("Invalid end_date format. Expected YYYY-MM-DD".to_string()) })?; end_date.and_time(end_time) } else { start_date.and_time(end_time) }; // Convert from local time to UTC let start_local = chrono::Local.from_local_datetime(&start_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?; let end_local = chrono::Local.from_local_datetime(&end_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?; ( start_local.with_timezone(&chrono::Utc), end_local.with_timezone(&chrono::Utc), ) }; // Generate a unique UID for the series let uid = format!("series-{}", uuid::Uuid::new_v4().to_string()); // Create the VEvent for the series let mut event = VEvent::new(uid.clone(), start_datetime); event.dtend = Some(end_datetime); event.all_day = request.all_day; // Set the all_day flag properly event.summary = if request.title.trim().is_empty() { None } else { Some(request.title.clone()) }; event.description = if request.description.trim().is_empty() { None } else { Some(request.description.clone()) }; event.location = if request.location.trim().is_empty() { None } else { Some(request.location.clone()) }; // Set event status event.status = Some(match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }); // Set event class event.class = Some(match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }); // Set priority event.priority = request.priority; // Check if recurrence is already a full RRULE or just a simple type let rrule = if request.recurrence.starts_with("FREQ=") { // Frontend sent a complete RRULE string, use it directly request.recurrence.clone() } else { // Legacy path: Generate the RRULE for recurrence build_series_rrule_with_freq(&request, recurrence_freq)? }; event.rrule = Some(rrule); // Create the event on the CalDAV server let event_href = client .create_event(&calendar_path, &event) .await .map_err(|e| ApiError::Internal(format!("Failed to create event series: {}", e)))?; println!( "✅ Event series created successfully with UID: {}, href: {}", uid, event_href ); Ok(Json(CreateEventSeriesResponse { success: true, message: "Event series created successfully".to_string(), series_uid: Some(uid), occurrences_created: Some(1), // Series created as a single repeating event event_href: Some(event_href), })) } /// Update a recurring event series with different scope options pub async fn update_event_series( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!( "🔄 Update event series request received: series_uid='{}', update_scope='{}', recurrence_count={:?}, recurrence_end_date={:?}", request.series_uid, request.update_scope, request.recurrence_count, request.recurrence_end_date ); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.series_uid.trim().is_empty() { return Err(ApiError::BadRequest("Series UID is required".to_string())); } if request.title.trim().is_empty() { return Err(ApiError::BadRequest("Event title is required".to_string())); } if request.title.len() > 200 { return Err(ApiError::BadRequest( "Event title too long (max 200 characters)".to_string(), )); } // Validate update scope match request.update_scope.as_str() { "this_only" | "this_and_future" | "all_in_series" => {} _ => { return Err(ApiError::BadRequest( "Invalid update_scope. Must be: this_only, this_and_future, or all_in_series" .to_string(), )) } } // Validate recurrence type - handle both simple strings and RRULE strings let recurrence_freq = if request.recurrence.contains("FREQ=") { // Parse RRULE to extract frequency if request.recurrence.contains("FREQ=DAILY") { "daily" } else if request.recurrence.contains("FREQ=WEEKLY") { "weekly" } else if request.recurrence.contains("FREQ=MONTHLY") { "monthly" } else if request.recurrence.contains("FREQ=YEARLY") { "yearly" } else { return Err(ApiError::BadRequest( "Invalid RRULE frequency. Must be DAILY, WEEKLY, MONTHLY, or YEARLY".to_string(), )); } } else { // Handle simple strings let lower = request.recurrence.to_lowercase(); match lower.as_str() { "daily" => "daily", "weekly" => "weekly", "monthly" => "monthly", "yearly" => "yearly", _ => { return Err(ApiError::BadRequest( "Invalid recurrence type. Must be daily, weekly, monthly, or yearly" .to_string(), )) } } }; // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Use the parsed frequency for further processing (avoiding unused variable warning) let _freq_for_processing = recurrence_freq; // Determine which calendar to search (or search all calendars) let calendar_paths = if let Some(ref path) = request.calendar_path { vec![path.clone()] } else { client .discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))? }; if calendar_paths.is_empty() { return Err(ApiError::BadRequest( "No calendars available for event update".to_string(), )); } // Find the series event across all specified calendars let mut existing_event = None; let mut calendar_path = String::new(); for path in &calendar_paths { if let Ok(Some(event)) = client.fetch_event_by_uid(path, &request.series_uid).await { existing_event = Some(event); calendar_path = path.clone(); break; } } let mut existing_event = existing_event.ok_or_else(|| { ApiError::NotFound(format!( "Event series with UID '{}' not found", request.series_uid )) })?; println!("📅 Found series event in calendar: {}", calendar_path); println!( "📅 Event details: UID={}, summary={:?}, dtstart={}", existing_event.uid, existing_event.summary, existing_event.dtstart ); // Parse datetime components for the update let original_start_date = existing_event.dtstart.date_naive(); // For "this_and_future" and "this_only" updates, use the occurrence date for the modified event // For "all_in_series" updates, preserve the original series start date let start_date = if (request.update_scope == "this_and_future" || request.update_scope == "this_only") && request.occurrence_date.is_some() { let occurrence_date_str = request.occurrence_date.as_ref().unwrap(); chrono::NaiveDate::parse_from_str(occurrence_date_str, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid occurrence_date format. Expected YYYY-MM-DD".to_string()) })? } else { original_start_date }; let (start_datetime, end_datetime) = if request.all_day { let start_dt = start_date .and_hms_opt(0, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid start date".to_string()))?; // For all-day events, also preserve the original date pattern let end_date = if !request.end_date.is_empty() { // Calculate the duration from the original event let original_duration_days = existing_event .dtend .map(|end| (end.date_naive() - existing_event.dtstart.date_naive()).num_days()) .unwrap_or(0); start_date + chrono::Duration::days(original_duration_days) } else { start_date }; let end_dt = end_date .and_hms_opt(23, 59, 59) .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; // Convert from local time to UTC let start_local = chrono::Local.from_local_datetime(&start_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?; let end_local = chrono::Local.from_local_datetime(&end_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?; ( start_local.with_timezone(&chrono::Utc), end_local.with_timezone(&chrono::Utc), ) } else { let start_time = if !request.start_time.is_empty() { chrono::NaiveTime::parse_from_str(&request.start_time, "%H:%M").map_err(|_| { ApiError::BadRequest("Invalid start_time format. Expected HH:MM".to_string()) })? } else { existing_event.dtstart.time() }; let end_time = if !request.end_time.is_empty() { chrono::NaiveTime::parse_from_str(&request.end_time, "%H:%M").map_err(|_| { ApiError::BadRequest("Invalid end_time format. Expected HH:MM".to_string()) })? } else { existing_event .dtend .map(|dt| dt.time()) .unwrap_or_else(|| existing_event.dtstart.time() + chrono::Duration::hours(1)) }; let start_dt = start_date.and_time(start_time); let end_dt = if !request.end_time.is_empty() { // Use the new end time with the preserved date start_date.and_time(end_time) } else { // Calculate end time based on original duration let original_duration = existing_event .dtend .map(|end| end - existing_event.dtstart) .unwrap_or_else(|| chrono::Duration::hours(1)); (chrono::Utc.from_utc_datetime(&start_dt) + original_duration).naive_utc() }; // Convert from local time to UTC let start_local = chrono::Local.from_local_datetime(&start_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous start datetime".to_string()))?; let end_local = chrono::Local.from_local_datetime(&end_dt) .single() .ok_or_else(|| ApiError::BadRequest("Ambiguous end datetime".to_string()))?; ( start_local.with_timezone(&chrono::Utc), end_local.with_timezone(&chrono::Utc), ) }; // Handle different update scopes let (updated_event, occurrences_affected) = match request.update_scope.as_str() { "all_in_series" => { // Update the entire series - modify the master event update_entire_series(&mut existing_event, &request, start_datetime, end_datetime)? } "this_and_future" => { // Split the series: keep past occurrences, create new series from occurrence date update_this_and_future( &mut existing_event, &request, start_datetime, end_datetime, &client, &calendar_path, ) .await? } "this_only" => { // Create exception for single occurrence, keep original series let event_href = existing_event .href .as_ref() .ok_or_else(|| { ApiError::Internal( "Event missing href for single occurrence update".to_string(), ) })? .clone(); update_single_occurrence( &mut existing_event, &request, start_datetime, end_datetime, &client, &calendar_path, &event_href, ) .await? } _ => unreachable!(), // Already validated above }; // Update the event on the CalDAV server using the original event's href println!("📤 Updating event on CalDAV server..."); let event_href = existing_event .href .as_ref() .ok_or_else(|| ApiError::Internal("Event missing href for update".to_string()))?; println!("📤 Using event href: {}", event_href); println!("📤 Calendar path: {}", calendar_path); match client .update_event(&calendar_path, &updated_event, event_href) .await { Ok(_) => { println!("✅ CalDAV update completed successfully"); } Err(e) => { println!("❌ CalDAV update failed: {}", e); return Err(ApiError::Internal(format!( "Failed to update event series: {}", e ))); } } println!( "✅ Event series updated successfully with UID: {}", request.series_uid ); Ok(Json(UpdateEventSeriesResponse { success: true, message: "Event series updated successfully".to_string(), series_uid: Some(request.series_uid), occurrences_affected: Some(occurrences_affected), })) } /// Delete a recurring event series or specific occurrences pub async fn delete_event_series( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!( "🗑️ Delete event series request received: series_uid='{}', delete_scope='{}'", request.series_uid, request.delete_scope ); // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.series_uid.trim().is_empty() { return Err(ApiError::BadRequest("Series UID is required".to_string())); } if request.calendar_path.trim().is_empty() { return Err(ApiError::BadRequest( "Calendar path is required".to_string(), )); } if request.event_href.trim().is_empty() { return Err(ApiError::BadRequest("Event href is required".to_string())); } // Validate delete scope match request.delete_scope.as_str() { "this_only" | "this_and_future" | "all_in_series" => {} _ => { return Err(ApiError::BadRequest( "Invalid delete_scope. Must be: this_only, this_and_future, or all_in_series" .to_string(), )) } } // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Handle different deletion scopes let occurrences_affected = match request.delete_scope.as_str() { "all_in_series" => { // Delete the entire series - simply delete the event delete_entire_series(&client, &request).await? } "this_and_future" => { // Modify RRULE to end before this occurrence delete_this_and_future(&client, &request).await? } "this_only" => { // Add EXDATE for single occurrence delete_single_occurrence(&client, &request).await? } _ => unreachable!(), // Already validated above }; println!( "✅ Event series deletion completed with {} occurrences affected", occurrences_affected ); Ok(Json(DeleteEventSeriesResponse { success: true, message: "Event series deletion completed successfully".to_string(), occurrences_affected: Some(occurrences_affected), })) } // Helper functions fn build_series_rrule_with_freq( request: &CreateEventSeriesRequest, freq: &str, ) -> Result { let mut rrule_parts = Vec::new(); // Add frequency match freq { "daily" => rrule_parts.push("FREQ=DAILY".to_string()), "weekly" => rrule_parts.push("FREQ=WEEKLY".to_string()), "monthly" => rrule_parts.push("FREQ=MONTHLY".to_string()), "yearly" => rrule_parts.push("FREQ=YEARLY".to_string()), _ => { return Err(ApiError::BadRequest( "Invalid recurrence frequency".to_string(), )) } } // Add interval if specified and greater than 1 if let Some(interval) = request.recurrence_interval { if interval > 1 { rrule_parts.push(format!("INTERVAL={}", interval)); } } // Handle weekly recurrence with specific days (BYDAY) if freq == "weekly" && request.recurrence_days.len() == 7 { let selected_days: Vec<&str> = request .recurrence_days .iter() .enumerate() .filter_map(|(i, &selected)| { if selected { Some(match i { 0 => "SU", // Sunday 1 => "MO", // Monday 2 => "TU", // Tuesday 3 => "WE", // Wednesday 4 => "TH", // Thursday 5 => "FR", // Friday 6 => "SA", // Saturday _ => return None, }) } else { None } }) .collect(); if !selected_days.is_empty() { rrule_parts.push(format!("BYDAY={}", selected_days.join(","))); } } // Add end date if specified (UNTIL takes precedence over COUNT) if let Some(end_date) = &request.recurrence_end_date { // Parse the end date and convert to RRULE format (YYYYMMDDTHHMMSSZ) match chrono::NaiveDate::parse_from_str(end_date, "%Y-%m-%d") { Ok(date) => { let end_datetime = date .and_hms_opt(23, 59, 59) .ok_or_else(|| ApiError::BadRequest("Invalid end date".to_string()))?; let utc_end = chrono::Utc.from_utc_datetime(&end_datetime); rrule_parts.push(format!("UNTIL={}", utc_end.format("%Y%m%dT%H%M%SZ"))); } Err(_) => { return Err(ApiError::BadRequest( "Invalid recurrence_end_date format. Expected YYYY-MM-DD".to_string(), )) } } } else if let Some(count) = request.recurrence_count { if count > 0 { rrule_parts.push(format!("COUNT={}", count)); } } Ok(rrule_parts.join(";")) } /// Update the entire series - modify the master event fn update_entire_series( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, start_datetime: chrono::DateTime, end_datetime: chrono::DateTime, ) -> Result<(VEvent, u32), ApiError> { // Clone the existing event to preserve all metadata let mut updated_event = existing_event.clone(); // Update only the modified properties from the request updated_event.dtstart = start_datetime; updated_event.dtend = Some(end_datetime); updated_event.summary = if request.title.trim().is_empty() { existing_event.summary.clone() // Keep original if empty } else { Some(request.title.clone()) }; updated_event.description = if request.description.trim().is_empty() { existing_event.description.clone() // Keep original if empty } else { Some(request.description.clone()) }; updated_event.location = if request.location.trim().is_empty() { existing_event.location.clone() // Keep original if empty } else { Some(request.location.clone()) }; updated_event.status = Some(match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }); updated_event.class = Some(match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }); updated_event.priority = request.priority; // Update timestamps let now = chrono::Utc::now(); updated_event.dtstamp = now; updated_event.last_modified = Some(now); // Keep original created timestamp to preserve event history // Update RRULE if recurrence parameters are provided if let Some(ref existing_rrule) = updated_event.rrule { let mut new_rrule = existing_rrule.clone(); println!("🔄 Original RRULE: {}", existing_rrule); // Update COUNT if provided if let Some(count) = request.recurrence_count { println!("🔄 Updating RRULE with new COUNT: {}", count); // Remove old COUNT or UNTIL parameters new_rrule = new_rrule.split(';') .filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL=")) .collect::>() .join(";"); // Add new COUNT new_rrule = format!("{};COUNT={}", new_rrule, count); } else if let Some(ref end_date) = request.recurrence_end_date { println!("🔄 Updating RRULE with new UNTIL: {}", end_date); // Remove old COUNT or UNTIL parameters new_rrule = new_rrule.split(';') .filter(|part| !part.starts_with("COUNT=") && !part.starts_with("UNTIL=")) .collect::>() .join(";"); // Add new UNTIL (convert YYYY-MM-DD to YYYYMMDD format) let until_date = end_date.replace("-", ""); new_rrule = format!("{};UNTIL={}", new_rrule, until_date); } println!("🔄 Updated RRULE: {}", new_rrule); updated_event.rrule = Some(new_rrule); } // Copy the updated event back to existing_event for the main handler *existing_event = updated_event.clone(); Ok((updated_event, 1)) // 1 series updated (affects all occurrences) } /// Update this occurrence and all future occurrences (RFC 5545 compliant series splitting) /// /// This function implements the "this and future events" modification pattern for recurring /// event series by splitting the original series into two parts: /// /// ## Operation Overview: /// 1. **Terminate Original Series**: Adds an UNTIL clause to the original recurring event /// to stop generating occurrences before the target occurrence date. /// 2. **Create New Series**: Creates a completely new recurring series starting from the /// target occurrence date with the updated properties (new times, title, etc.). /// /// ## Example Scenario: /// - Original series: "Daily meeting 9:00-10:00 AM" (Aug 15 onwards, no end date) /// - User drags Aug 22 occurrence to 2:00-3:00 PM /// - Result: /// - Original series: "Daily meeting 9:00-10:00 AM" with UNTIL=Aug 22 midnight (covers Aug 15-21) /// - New series: "Daily meeting 2:00-3:00 PM" starting Aug 22 (covers Aug 22 onwards) /// /// ## RFC 5545 Compliance: /// - Uses UNTIL property in RRULE to cleanly terminate the original series /// - Preserves original event UIDs and CalDAV metadata /// - Maintains proper DTSTAMP and LAST-MODIFIED timestamps /// - New series gets fresh UID to avoid conflicts /// /// ## CalDAV Operations: /// This function performs two sequential CalDAV operations: /// 1. CREATE new series on the CalDAV server /// 2. UPDATE original series (handled by caller) with UNTIL clause /// /// Operations are serialized using a global mutex to prevent race conditions. /// /// ## Parameters: /// - `existing_event`: The original recurring event to be split /// - `request`: Update request containing new properties and occurrence_date /// - `start_datetime`/`end_datetime`: New times for the future occurrences /// - `client`: CalDAV client for server operations /// - `calendar_path`: CalDAV calendar path where events are stored /// /// ## Returns: /// - `(VEvent, u32)`: Updated original event with UNTIL clause, and count of operations (2) /// /// ## Error Handling: /// - Validates occurrence_date format and presence /// - Handles CalDAV server communication errors /// - Ensures atomic operations (both succeed or both fail) async fn update_this_and_future( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, start_datetime: chrono::DateTime, end_datetime: chrono::DateTime, client: &CalDAVClient, calendar_path: &str, ) -> Result<(VEvent, u32), ApiError> { // Clone the existing event to create the new series before modifying the RRULE of the // original, because we'd like to preserve the original UNTIL logic let mut new_series = existing_event.clone(); let occurrence_date = request.occurrence_date.as_ref().ok_or_else(|| { ApiError::BadRequest("occurrence_date is required for this_and_future updates".to_string()) })?; // Parse occurrence date let occurrence_date_parsed = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") .map_err(|_| ApiError::BadRequest("Invalid occurrence date format".to_string()))?; // Step 1: Add UNTIL to the original series to stop before the occurrence date let until_datetime = occurrence_date_parsed .and_hms_opt(0, 0, 0) .ok_or_else(|| ApiError::BadRequest("Invalid occurrence date".to_string()))?; let utc_until = chrono::Utc.from_utc_datetime(&until_datetime); // Create modified RRULE with UNTIL clause for the original series let original_rrule = existing_event .rrule .clone() .unwrap_or_else(|| "FREQ=WEEKLY".to_string()); let parts: Vec<&str> = original_rrule .split(';') .filter(|part| !part.starts_with("UNTIL=") && !part.starts_with("COUNT=")) .collect(); existing_event.rrule = Some(format!( "{};UNTIL={}", parts.join(";"), utc_until.format("%Y%m%dT%H%M%SZ") )); println!( "🔄 this_and_future: Updated original series RRULE: {:?}", existing_event.rrule ); // Step 2: Create a new series starting from the occurrence date with updated properties let new_series_uid = format!("series-{}", uuid::Uuid::new_v4()); // Update the new series with new properties new_series.uid = new_series_uid.clone(); new_series.dtstart = start_datetime; new_series.dtend = Some(end_datetime); new_series.summary = if request.title.trim().is_empty() { None } else { Some(request.title.clone()) }; new_series.description = if request.description.trim().is_empty() { None } else { Some(request.description.clone()) }; new_series.location = if request.location.trim().is_empty() { None } else { Some(request.location.clone()) }; new_series.status = Some(match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }); new_series.class = Some(match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }); new_series.priority = request.priority; // Update timestamps let now = chrono::Utc::now(); new_series.dtstamp = now; new_series.created = Some(now); new_series.last_modified = Some(now); new_series.href = None; // Will be set when created println!( "🔄 this_and_future: Creating new series with UID: {}", new_series_uid ); println!( "🔄 this_and_future: New series RRULE: {:?}", new_series.rrule ); // Create the new series on CalDAV server client .create_event(calendar_path, &new_series) .await .map_err(|e| ApiError::Internal(format!("Failed to create new series: {}", e)))?; println!("✅ this_and_future: Created new series successfully"); // Return the original event (with UNTIL added) - it will be updated by the main handler Ok((existing_event.clone(), 2)) // 2 operations: updated original + created new series } /// Update only a single occurrence (create an exception) async fn update_single_occurrence( existing_event: &mut VEvent, request: &UpdateEventSeriesRequest, start_datetime: chrono::DateTime, end_datetime: chrono::DateTime, client: &CalDAVClient, calendar_path: &str, _original_event_href: &str, ) -> Result<(VEvent, u32), ApiError> { // For RFC 5545 compliant single occurrence updates, we need to: // 1. Add EXDATE to the original series to exclude this occurrence // 2. Create a new exception event with RECURRENCE-ID pointing to the original occurrence // First, add EXDATE to the original series let occurrence_date = request.occurrence_date.as_ref().ok_or_else(|| { ApiError::BadRequest( "occurrence_date is required for single occurrence updates".to_string(), ) })?; // Parse the occurrence date let exception_date = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid occurrence date format. Expected YYYY-MM-DD".to_string()) })?; // Create the EXDATE datetime using the original event's time let original_time = existing_event.dtstart.time(); let exception_datetime = exception_date.and_time(original_time); let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime); // Add the exception date to the original series println!( "📝 BEFORE adding EXDATE: existing_event.exdate = {:?}", existing_event.exdate ); existing_event.exdate.push(exception_utc); println!( "📝 AFTER adding EXDATE: existing_event.exdate = {:?}", existing_event.exdate ); println!( "🚫 Added EXDATE for single occurrence modification: {}", exception_utc.format("%Y-%m-%d %H:%M:%S") ); // Create exception event by cloning the existing event to preserve all metadata let mut exception_event = existing_event.clone(); // Give the exception event a unique UID (required for CalDAV) exception_event.uid = format!("exception-{}", uuid::Uuid::new_v4()); // Update the modified properties from the request exception_event.dtstart = start_datetime; exception_event.dtend = Some(end_datetime); exception_event.summary = if request.title.trim().is_empty() { existing_event.summary.clone() // Keep original if empty } else { Some(request.title.clone()) }; exception_event.description = if request.description.trim().is_empty() { existing_event.description.clone() // Keep original if empty } else { Some(request.description.clone()) }; exception_event.location = if request.location.trim().is_empty() { existing_event.location.clone() // Keep original if empty } else { Some(request.location.clone()) }; exception_event.status = Some(match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }); exception_event.class = Some(match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }); exception_event.priority = request.priority; // Update timestamps for the exception event let now = chrono::Utc::now(); exception_event.dtstamp = now; exception_event.last_modified = Some(now); // Keep original created timestamp to preserve event history // Set RECURRENCE-ID to point to the original occurrence // exception_event.recurrence_id = Some(exception_utc); // Remove any recurrence rules from the exception (it's a single event) exception_event.rrule = None; exception_event.rdate.clear(); exception_event.exdate.clear(); // Set calendar path for the exception event exception_event.calendar_path = Some(calendar_path.to_string()); println!( "✨ Created exception event with RECURRENCE-ID: {}", exception_utc.format("%Y-%m-%d %H:%M:%S") ); // Create the exception event as a new event (original series will be updated by main handler) client .create_event(calendar_path, &exception_event) .await .map_err(|e| ApiError::Internal(format!("Failed to create exception event: {}", e)))?; println!("✅ Created exception event successfully"); // Return the original series (now with EXDATE) - main handler will update it on CalDAV Ok((existing_event.clone(), 1)) // 1 occurrence modified (via exception) } /// Delete the entire series async fn delete_entire_series( client: &CalDAVClient, request: &DeleteEventSeriesRequest, ) -> Result { // Simply delete the entire event from the CalDAV server client .delete_event(&request.calendar_path, &request.event_href) .await .map_err(|e| ApiError::Internal(format!("Failed to delete event series: {}", e)))?; println!("🗑️ Entire series deleted: {}", request.series_uid); Ok(1) // 1 series deleted (affects all occurrences) } /// Delete this occurrence and all future occurrences (modify RRULE with UNTIL) async fn delete_this_and_future( client: &CalDAVClient, request: &DeleteEventSeriesRequest, ) -> Result { // Fetch the existing event to modify its RRULE let event_uid = request.series_uid.clone(); let existing_event = client .fetch_event_by_uid(&request.calendar_path, &event_uid) .await .map_err(|e| ApiError::Internal(format!("Failed to fetch event for modification: {}", e)))? .ok_or_else(|| { ApiError::NotFound(format!("Event series with UID '{}' not found", event_uid)) })?; // If no occurrence_date is provided, delete the entire series let Some(occurrence_date) = &request.occurrence_date else { return delete_entire_series(client, request).await; }; // Parse occurrence date to set as UNTIL for the RRULE let until_date = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid occurrence date format. Expected YYYY-MM-DD".to_string()) })?; // Set UNTIL to the day before the occurrence to exclude it and all future occurrences let until_datetime = until_date .pred_opt() .ok_or_else(|| { ApiError::BadRequest("Cannot delete from the first possible date".to_string()) })? .and_hms_opt(23, 59, 59) .ok_or_else(|| ApiError::BadRequest("Invalid date calculation".to_string()))?; let utc_until = chrono::Utc.from_utc_datetime(&until_datetime); // Modify the existing event's RRULE let mut updated_event = existing_event; if let Some(rrule) = &updated_event.rrule { // Remove existing UNTIL or COUNT if present and add new UNTIL let parts: Vec<&str> = rrule .split(';') .filter(|part| !part.starts_with("UNTIL=") && !part.starts_with("COUNT=")) .collect(); updated_event.rrule = Some(format!( "{};UNTIL={}", parts.join(";"), utc_until.format("%Y%m%dT%H%M%SZ") )); } // Update the event on the CalDAV server client .update_event(&request.calendar_path, &updated_event, &request.event_href) .await .map_err(|e| { ApiError::Internal(format!("Failed to update event series for deletion: {}", e)) })?; println!( "🗑️ Series modified with UNTIL for this_and_future deletion: {}", utc_until.format("%Y-%m-%d") ); Ok(1) // 1 series modified } /// Delete only a single occurrence (add EXDATE) async fn delete_single_occurrence( client: &CalDAVClient, request: &DeleteEventSeriesRequest, ) -> Result { // Fetch the existing event to add EXDATE let event_uid = request.series_uid.clone(); let existing_event = client .fetch_event_by_uid(&request.calendar_path, &event_uid) .await .map_err(|e| ApiError::Internal(format!("Failed to fetch event for modification: {}", e)))? .ok_or_else(|| { ApiError::NotFound(format!("Event series with UID '{}' not found", event_uid)) })?; // If no occurrence_date is provided, cannot delete single occurrence let Some(occurrence_date) = &request.occurrence_date else { return Err(ApiError::BadRequest( "occurrence_date is required for single occurrence deletion".to_string(), )); }; // Parse occurrence date let exception_date = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d").map_err(|_| { ApiError::BadRequest("Invalid occurrence date format. Expected YYYY-MM-DD".to_string()) })?; // Create the EXDATE datetime (use the same time as the original event) let original_time = existing_event.dtstart.time(); let exception_datetime = exception_date.and_time(original_time); let exception_utc = chrono::Utc.from_utc_datetime(&exception_datetime); // Add the exception date to the event's EXDATE list let mut updated_event = existing_event; updated_event.exdate.push(exception_utc); println!( "🗑️ Added EXDATE for single occurrence deletion: {}", exception_utc.format("%Y%m%dT%H%M%SZ") ); // Update the event on the CalDAV server client .update_event(&request.calendar_path, &updated_event, &request.event_href) .await .map_err(|e| { ApiError::Internal(format!( "Failed to update event series for single deletion: {}", e )) })?; Ok(1) // 1 occurrence excluded }