use axum::{ extract::{Path, Query, State}, http::HeaderMap, response::Json, }; use chrono::Datelike; use serde::Deserialize; use std::sync::Arc; use crate::calendar::{CalDAVClient, CalendarEvent}; use crate::{ models::{ ApiError, CreateEventRequest, CreateEventResponse, DeleteEventRequest, DeleteEventResponse, UpdateEventRequest, UpdateEventResponse, }, AppState, }; use calendar_models::{ AlarmAction, AlarmTrigger, Attendee, CalendarUser, EventClass, EventStatus, VAlarm, VEvent, }; use super::auth::{extract_bearer_token, extract_password_header}; #[derive(Deserialize)] pub struct CalendarQuery { pub year: Option, pub month: Option, } pub async fn get_calendar_events( State(state): State>, Query(params): Query, headers: HeaderMap, ) -> Result>, ApiError> { // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; println!("🔑 API call with password length: {}", password.len()); // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Discover calendars if needed let calendar_paths = client .discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; if calendar_paths.is_empty() { return Ok(Json(vec![])); // No calendars found } // Fetch events from all calendars let mut all_events = Vec::new(); for calendar_path in &calendar_paths { match client.fetch_events(calendar_path).await { Ok(mut events) => { // Set calendar_path for each event to identify which calendar it belongs to for event in &mut events { event.calendar_path = Some(calendar_path.clone()); } all_events.extend(events); } Err(e) => { eprintln!( "Failed to fetch events from calendar {}: {}", calendar_path, e ); // Continue with other calendars instead of failing completely } } } // If year and month are specified, filter events if let (Some(year), Some(month)) = (params.year, params.month) { let target_date = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let month_start = target_date; let month_end = if month == 12 { chrono::NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() } else { chrono::NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() } - chrono::Duration::days(1); all_events.retain(|event| { let event_date = event.dtstart.date_naive(); // For non-recurring events, check if the event date is within the month if event.rrule.is_none() || event.rrule.as_ref().unwrap().is_empty() { let event_year = event.dtstart.year(); let event_month = event.dtstart.month(); return event_year == year && event_month == month; } // For recurring events, check if they could have instances in this month // Include if: // 1. The event starts before or during the requested month // 2. The event doesn't have an UNTIL date, OR the UNTIL date is after the month start if event_date > month_end { // Event starts after the requested month return false; } // Check UNTIL date in RRULE if present if let Some(ref rrule) = event.rrule { if let Some(until_pos) = rrule.find("UNTIL=") { let until_part = &rrule[until_pos + 6..]; let until_end = until_part.find(';').unwrap_or(until_part.len()); let until_str = &until_part[..until_end]; // Try to parse UNTIL date (format: YYYYMMDDTHHMMSSZ or YYYYMMDD) if until_str.len() >= 8 { if let Ok(until_date) = chrono::NaiveDate::parse_from_str(&until_str[..8], "%Y%m%d") { if until_date < month_start { // Recurring event ended before the requested month return false; } } } } } // Include the recurring event - the frontend will do proper expansion true }); } println!("📅 Returning {} events", all_events.len()); Ok(Json(all_events)) } pub async fn refresh_event( State(state): State>, Path(uid): Path, headers: HeaderMap, ) -> Result>, ApiError> { let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Discover calendars let calendar_paths = client .discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))?; // Search for the event by UID across all calendars for calendar_path in &calendar_paths { if let Ok(Some(mut event)) = client.fetch_event_by_uid(calendar_path, &uid).await { event.calendar_path = Some(calendar_path.clone()); return Ok(Json(Some(event))); } } Ok(Json(None)) } async fn fetch_event_by_href( client: &CalDAVClient, calendar_path: &str, event_href: &str, ) -> Result, crate::calendar::CalDAVError> { // This is a simplified implementation - in a real scenario, you'd want to fetch the specific event by href // For now, we'll fetch all events and find the matching one by href (inefficient but functional) let events = client.fetch_events(calendar_path).await?; println!("🔍 fetch_event_by_href: looking for href='{}'", event_href); println!( "🔍 Available events with hrefs: {:?}", events.iter().map(|e| (&e.uid, &e.href)).collect::>() ); // First try to match by exact href for event in &events { if let Some(stored_href) = &event.href { if stored_href == event_href { println!("✅ Found matching event by exact href: {}", event.uid); return Ok(Some(event.clone())); } } } // Fallback: try to match by UID extracted from href filename let filename = event_href.split('/').last().unwrap_or(event_href); let uid_from_href = filename.trim_end_matches(".ics"); println!( "🔍 Fallback: trying UID match. filename='{}', uid='{}'", filename, uid_from_href ); for event in events { if event.uid == uid_from_href { println!("✅ Found matching event by UID: {}", event.uid); return Ok(Some(event)); } } println!("❌ No matching event found for href: {}", event_href); Ok(None) } pub async fn delete_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // 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 delete actions for recurring events match request.delete_action.as_str() { "delete_this" => { if let Some(event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href) .await .map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? { // Check if this is a recurring event if event.rrule.is_some() && !event.rrule.as_ref().unwrap().is_empty() { // Recurring event - add EXDATE for this occurrence if let Some(occurrence_date) = &request.occurrence_date { let exception_utc = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) { // RFC3339 format (with time and timezone) date.with_timezone(&chrono::Utc) } else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") { // Simple date format (YYYY-MM-DD) naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc() } else { return Err(ApiError::BadRequest(format!("Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date))); }; let mut updated_event = event; updated_event.exdate.push(exception_utc); println!( "🔄 Adding EXDATE {} to recurring event {}", exception_utc.format("%Y%m%dT%H%M%SZ"), updated_event.uid ); // Update the event with the new EXDATE client .update_event( &request.calendar_path, &updated_event, &request.event_href, ) .await .map_err(|e| { ApiError::Internal(format!( "Failed to update event with EXDATE: {}", e )) })?; println!("✅ Successfully updated recurring event with EXDATE"); Ok(Json(DeleteEventResponse { success: true, message: "Single occurrence deleted successfully".to_string(), })) } else { Err(ApiError::BadRequest("Occurrence date is required for single occurrence deletion of recurring events".to_string())) } } else { // Non-recurring event - delete the entire event println!("🗑️ Deleting non-recurring event: {}", event.uid); client .delete_event(&request.calendar_path, &request.event_href) .await .map_err(|e| { ApiError::Internal(format!("Failed to delete event: {}", e)) })?; println!("✅ Successfully deleted non-recurring event"); Ok(Json(DeleteEventResponse { success: true, message: "Event deleted successfully".to_string(), })) } } else { Err(ApiError::NotFound("Event not found".to_string())) } } "delete_following" => { // For "this and following" deletion, we need to: // 1. Fetch the recurring event // 2. Modify the RRULE to end before this occurrence // 3. Update the event if let Some(mut event) = fetch_event_by_href(&client, &request.calendar_path, &request.event_href) .await .map_err(|e| ApiError::Internal(format!("Failed to fetch event: {}", e)))? { if let Some(occurrence_date) = &request.occurrence_date { let until_date = if let Ok(date) = chrono::DateTime::parse_from_rfc3339(occurrence_date) { // RFC3339 format (with time and timezone) date.with_timezone(&chrono::Utc) } else if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(occurrence_date, "%Y-%m-%d") { // Simple date format (YYYY-MM-DD) naive_date.and_hms_opt(0, 0, 0).unwrap().and_utc() } else { return Err(ApiError::BadRequest(format!( "Invalid occurrence date format: {}. Expected RFC3339 or YYYY-MM-DD", occurrence_date ))); }; // Modify the RRULE to add an UNTIL clause if let Some(rrule) = &event.rrule { // Remove existing UNTIL if present and add new one let parts: Vec<&str> = rrule .split(';') .filter(|part| { !part.starts_with("UNTIL=") && !part.starts_with("COUNT=") }) .collect(); let new_rrule = format!( "{};UNTIL={}", parts.join(";"), until_date.format("%Y%m%dT%H%M%SZ") ); event.rrule = Some(new_rrule); // Update the event with the modified RRULE client .update_event(&request.calendar_path, &event, &request.event_href) .await .map_err(|e| { ApiError::Internal(format!( "Failed to update event with modified RRULE: {}", e )) })?; Ok(Json(DeleteEventResponse { success: true, message: "This and following occurrences deleted successfully" .to_string(), })) } else { // No RRULE, just delete the single event client .delete_event(&request.calendar_path, &request.event_href) .await .map_err(|e| { ApiError::Internal(format!("Failed to delete event: {}", e)) })?; Ok(Json(DeleteEventResponse { success: true, message: "Event deleted successfully".to_string(), })) } } else { Err(ApiError::BadRequest( "Occurrence date is required for following deletion".to_string(), )) } } else { Err(ApiError::NotFound("Event not found".to_string())) } } "delete_series" | _ => { // Delete the entire event/series client .delete_event(&request.calendar_path, &request.event_href) .await .map_err(|e| ApiError::Internal(format!("Failed to delete event: {}", e)))?; Ok(Json(DeleteEventResponse { success: true, message: "Event deleted successfully".to_string(), })) } } } pub async fn create_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { println!( "📝 Create event request received: title='{}', all_day={}, calendar_path={:?}", request.title, request.all_day, request.calendar_path ); // 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(), )); } // 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(path) = request.calendar_path { path } 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() }; // Parse dates and times let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; // For all-day events, add one day to end date for RFC-5545 compliance // RFC-5545 uses exclusive end dates for all-day events if request.all_day { end_datetime = end_datetime + chrono::Duration::days(1); } // Validate that end is after start (allow equal times for all-day events) if request.all_day { if end_datetime < start_datetime { return Err(ApiError::BadRequest( "End date must be on or after start date for all-day events".to_string(), )); } } else { if end_datetime <= start_datetime { return Err(ApiError::BadRequest( "End date/time must be after start date/time".to_string(), )); } } // Generate a unique UID for the event let uid = format!( "{}-{}", uuid::Uuid::new_v4(), chrono::Utc::now().timestamp() ); // Parse status let status = match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }; // Parse class let class = match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }; // Parse attendees (comma-separated email list) let attendees: Vec = if request.attendees.trim().is_empty() { Vec::new() } else { request .attendees .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse categories (comma-separated list) let categories: Vec = if request.categories.trim().is_empty() { Vec::new() } else { request .categories .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect() }; // Parse alarms - convert from minutes string to EventReminder structs let alarms: Vec = if request.reminder.trim().is_empty() { Vec::new() } else { match request.reminder.parse::() { Ok(minutes) => vec![crate::calendar::EventReminder { minutes_before: minutes, action: crate::calendar::ReminderAction::Display, description: None, }], Err(_) => Vec::new(), } }; // 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 if request.recurrence.is_empty() { None } else { Some(request.recurrence.clone()) } } else { // Legacy path: Parse recurrence with BYDAY support for weekly recurrence match request.recurrence.to_uppercase().as_str() { "DAILY" => Some("FREQ=DAILY".to_string()), "WEEKLY" => { // Handle weekly recurrence with optional BYDAY parameter let mut rrule = "FREQ=WEEKLY".to_string(); // Check if specific days are selected (recurrence_days has 7 elements: [Sun, Mon, Tue, Wed, Thu, Fri, Sat]) if 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 = format!("{};BYDAY={}", rrule, selected_days.join(",")); } } Some(rrule) } "MONTHLY" => Some("FREQ=MONTHLY".to_string()), "YEARLY" => Some("FREQ=YEARLY".to_string()), _ => None, } }; // Create the VEvent struct (RFC 5545 compliant) let mut event = VEvent::new(uid, start_datetime); event.dtend = Some(end_datetime); 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) }; event.location = if request.location.trim().is_empty() { None } else { Some(request.location) }; event.status = Some(status); event.class = Some(class); event.priority = request.priority; event.organizer = if request.organizer.trim().is_empty() { None } else { Some(CalendarUser { cal_address: request.organizer, common_name: None, dir_entry_ref: None, sent_by: None, language: None, }) }; event.attendees = attendees .into_iter() .map(|email| Attendee { cal_address: email, common_name: None, role: None, part_stat: None, rsvp: None, cu_type: None, member: Vec::new(), delegated_to: Vec::new(), delegated_from: Vec::new(), sent_by: None, dir_entry_ref: None, language: None, }) .collect(); event.categories = categories; event.rrule = rrule; event.all_day = request.all_day; event.alarms = alarms .into_iter() .map(|reminder| VAlarm { action: AlarmAction::Display, trigger: AlarmTrigger::Duration(chrono::Duration::minutes( -reminder.minutes_before as i64, )), duration: None, repeat: None, description: reminder.description, summary: None, attendees: Vec::new(), attach: Vec::new(), }) .collect(); event.calendar_path = Some(calendar_path.clone()); // 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: {}", e)))?; println!( "✅ Event created successfully with UID: {} at href: {}", event.uid, event_href ); Ok(Json(CreateEventResponse { success: true, message: "Event created successfully".to_string(), event_href: Some(event_href), })) } pub async fn update_event( State(state): State>, headers: HeaderMap, Json(request): Json, ) -> Result, ApiError> { // Handle update request // Extract and verify token let token = extract_bearer_token(&headers)?; let password = extract_password_header(&headers)?; // Validate request if request.uid.trim().is_empty() { return Err(ApiError::BadRequest("Event 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(), )); } // Create CalDAV config from token and password let config = state .auth_service .caldav_config_from_token(&token, &password)?; let client = CalDAVClient::new(config); // Find the event across all calendars (or in the specified calendar) let calendar_paths = if let Some(path) = &request.calendar_path { vec![path.clone()] } else { client .discover_calendars() .await .map_err(|e| ApiError::Internal(format!("Failed to discover calendars: {}", e)))? }; let mut found_event: Option<(CalendarEvent, String, String)> = None; // (event, calendar_path, event_href) for calendar_path in &calendar_paths { match client.fetch_events(calendar_path).await { Ok(events) => { for event in events { if event.uid == request.uid { // Use the actual href from the event, or generate one if missing let event_href = event .href .clone() .unwrap_or_else(|| format!("{}.ics", event.uid)); println!("🔍 Found event {} with href: {}", event.uid, event_href); found_event = Some((event, calendar_path.clone(), event_href)); break; } } if found_event.is_some() { break; } } Err(e) => { eprintln!( "Failed to fetch events from calendar {}: {}", calendar_path, e ); continue; } } } let (mut event, calendar_path, event_href) = found_event .ok_or_else(|| ApiError::NotFound(format!("Event with UID '{}' not found", request.uid)))?; // Parse dates and times let start_datetime = parse_event_datetime(&request.start_date, &request.start_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid start date/time: {}", e)))?; let mut end_datetime = parse_event_datetime(&request.end_date, &request.end_time, request.all_day) .map_err(|e| ApiError::BadRequest(format!("Invalid end date/time: {}", e)))?; // For all-day events, add one day to end date for RFC-5545 compliance // RFC-5545 uses exclusive end dates for all-day events if request.all_day { end_datetime = end_datetime + chrono::Duration::days(1); } // Validate that end is after start (allow equal times for all-day events) if request.all_day { if end_datetime < start_datetime { return Err(ApiError::BadRequest( "End date must be on or after start date for all-day events".to_string(), )); } } else { if end_datetime <= start_datetime { return Err(ApiError::BadRequest( "End date/time must be after start date/time".to_string(), )); } } // Update event properties event.dtstart = start_datetime; event.dtend = Some(end_datetime); event.summary = if request.title.trim().is_empty() { None } else { Some(request.title) }; event.description = if request.description.trim().is_empty() { None } else { Some(request.description) }; event.location = if request.location.trim().is_empty() { None } else { Some(request.location) }; event.all_day = request.all_day; // Parse and update status event.status = Some(match request.status.to_lowercase().as_str() { "tentative" => EventStatus::Tentative, "cancelled" => EventStatus::Cancelled, _ => EventStatus::Confirmed, }); // Parse and update class event.class = Some(match request.class.to_lowercase().as_str() { "private" => EventClass::Private, "confidential" => EventClass::Confidential, _ => EventClass::Public, }); event.priority = request.priority; // Update the event on the CalDAV server println!( "📝 Updating event {} at calendar_path: {}, event_href: {}", event.uid, calendar_path, event_href ); client .update_event(&calendar_path, &event, &event_href) .await .map_err(|e| ApiError::Internal(format!("Failed to update event: {}", e)))?; println!("✅ Successfully updated event {}", event.uid); Ok(Json(UpdateEventResponse { success: true, message: "Event updated successfully".to_string(), })) } fn parse_event_datetime( date_str: &str, time_str: &str, all_day: bool, ) -> Result, String> { use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}; // Parse the date let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") .map_err(|_| format!("Invalid date format: {}. Expected YYYY-MM-DD", date_str))?; if all_day { // For all-day events, use noon UTC to avoid timezone boundary issues // This ensures the date remains correct when converted to any local timezone let datetime = date .and_hms_opt(12, 0, 0) .ok_or_else(|| "Failed to create noon datetime".to_string())?; Ok(Utc.from_utc_datetime(&datetime)) } else { // Parse the time let time = NaiveTime::parse_from_str(time_str, "%H:%M") .map_err(|_| format!("Invalid time format: {}. Expected HH:MM", time_str))?; // Combine date and time let datetime = NaiveDateTime::new(date, time); // Frontend now sends UTC times, so treat as UTC directly Ok(Utc.from_utc_datetime(&datetime)) } }