use crate::shared::models::UserSession; use crate::shared::state::AppState; use chrono::{DateTime, Duration, Timelike, Utc}; use diesel::prelude::*; use log::{error, info, trace}; use rhai::{Dynamic, Engine}; // use serde_json::json; // Commented out - unused import use std::sync::Arc; use uuid::Uuid; // Calendar types - would be from crate::calendar when feature is enabled #[derive(Debug)] pub struct CalendarEngine { _db: crate::shared::utils::DbPool, } #[derive(Debug)] pub struct CalendarEvent { pub id: uuid::Uuid, pub title: String, pub description: Option, pub start_time: DateTime, pub end_time: DateTime, pub location: Option, pub organizer: String, pub attendees: Vec, pub reminder_minutes: Option, pub recurrence_rule: Option, pub status: EventStatus, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug)] pub enum EventStatus { Confirmed, Tentative, Cancelled, } #[derive(Debug)] pub struct RecurrenceRule { pub frequency: String, pub interval: i32, pub count: Option, pub until: Option>, pub by_day: Option>, } impl CalendarEngine { #[must_use] pub fn new(db: crate::shared::utils::DbPool) -> Self { Self { _db: db } } pub async fn create_event( &self, event: CalendarEvent, ) -> Result> { Ok(event) } pub async fn check_conflicts( &self, _start: DateTime, _end: DateTime, _user: &str, ) -> Result, Box> { Ok(vec![]) } pub async fn get_events_range( &self, _start: DateTime, _end: DateTime, ) -> Result, Box> { Ok(vec![]) } } /// Register BOOK keyword in BASIC for calendar appointments pub fn book_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); engine .register_custom_syntax( &[ "BOOK", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ",", "$expr$", ], false, move |context, inputs| { let title = context.eval_expression_tree(&inputs[0])?.to_string(); let description = context.eval_expression_tree(&inputs[1])?.to_string(); let start_time_str = context.eval_expression_tree(&inputs[2])?.to_string(); let duration_minutes = context .eval_expression_tree(&inputs[3])? .as_int() .unwrap_or(30) as i64; let location = context.eval_expression_tree(&inputs[4])?.to_string(); trace!( "BOOK: title={}, start={}, duration={} min for user={}", title, start_time_str, duration_minutes, user_clone.user_id ); let state_for_task = Arc::clone(&state_clone); let user_for_task = user_clone.clone(); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build(); let send_err = if let Ok(rt) = rt { let result = rt.block_on(async move { execute_book( &state_for_task, &user_for_task, &title, &description, &start_time_str, duration_minutes, &location, ) .await }); tx.send(result).err() } else { tx.send(Err("Failed to build tokio runtime".to_string())) .err() }; if send_err.is_some() { error!("Failed to send BOOK result from thread"); } }); match rx.recv_timeout(std::time::Duration::from_secs(10)) { Ok(Ok(event_id)) => Ok(Dynamic::from(event_id)), Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("BOOK failed: {}", e).into(), rhai::Position::NONE, ))), Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( "BOOK timed out".into(), rhai::Position::NONE, ))), } }, ) .unwrap(); // Register BOOK MEETING for more complex meetings let state_clone2 = Arc::clone(&state); let user_clone2 = user.clone(); engine .register_custom_syntax( &["BOOK_MEETING", "$expr$", ",", "$expr$"], false, move |context, inputs| { let meeting_details = context.eval_expression_tree(&inputs[0])?; let attendees_input = context.eval_expression_tree(&inputs[1])?; let mut attendees = Vec::new(); if attendees_input.is_array() { let arr = attendees_input.cast::(); for item in arr.iter() { attendees.push(item.to_string()); } } trace!( "BOOK_MEETING with {} attendees for user={}", attendees.len(), user_clone2.user_id ); let state_for_task = Arc::clone(&state_clone2); let user_for_task = user_clone2.clone(); // Use tokio's block_in_place to run async code in sync context let result = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { execute_book_meeting( &state_for_task, &user_for_task, meeting_details.to_string(), attendees, ) .await }) }); match result { Ok(event_id) => Ok(Dynamic::from(event_id)), Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("BOOK_MEETING failed: {}", e).into(), rhai::Position::NONE, ))), } }, ) .unwrap(); // Register CHECK_AVAILABILITY keyword let state_clone3 = Arc::clone(&state); let user_clone3 = user.clone(); engine .register_custom_syntax( &["CHECK_AVAILABILITY", "$expr$", ",", "$expr$"], false, move |context, inputs| { let date_str = context.eval_expression_tree(&inputs[0])?.to_string(); let duration_minutes = context .eval_expression_tree(&inputs[1])? .as_int() .unwrap_or(30) as i64; trace!( "CHECK_AVAILABILITY for {} on {} for user={}", duration_minutes, date_str, user_clone3.user_id ); let state_for_task = Arc::clone(&state_clone3); let user_for_task = user_clone3.clone(); let date_str = date_str.clone(); // Use tokio's block_in_place to run async code in sync context let result = tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { check_availability( &state_for_task, &user_for_task, &date_str, duration_minutes, ) .await }) }); match result { Ok(slots) => Ok(Dynamic::from(slots)), Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( format!("CHECK_AVAILABILITY failed: {}", e).into(), rhai::Position::NONE, ))), } }, ) .unwrap(); } async fn execute_book( state: &AppState, user: &UserSession, title: &str, description: &str, start_time_str: &str, duration_minutes: i64, location: &str, ) -> Result { // Parse start time let start_time = parse_time_string(start_time_str)?; let end_time = start_time + Duration::minutes(duration_minutes); // Get or create calendar engine let calendar_engine = get_calendar_engine(state).await?; // Check for conflicts let conflicts = calendar_engine .check_conflicts(start_time, end_time, &user.user_id.to_string()) .await .map_err(|e| format!("Failed to check conflicts: {}", e))?; if !conflicts.is_empty() { return Err(format!( "Time slot conflicts with existing appointment: {}", conflicts[0].title )); } // Create calendar event let event = CalendarEvent { id: Uuid::new_v4(), title: title.to_string(), description: Some(description.to_string()), start_time, end_time, location: if location.is_empty() { None } else { Some(location.to_string()) }, organizer: user.user_id.to_string(), attendees: vec![user.user_id.to_string()], reminder_minutes: Some(15), // Default 15-minute reminder recurrence_rule: None, status: EventStatus::Confirmed, created_at: Utc::now(), updated_at: Utc::now(), }; // Create the event let created_event = calendar_engine .create_event(event) .await .map_err(|e| format!("Failed to create appointment: {}", e))?; // Log the booking log_booking(state, user, &created_event.id.to_string(), title).await?; info!( "Appointment booked: {} at {} for user {}", title, start_time, user.user_id ); Ok(format!( "Appointment '{}' booked for {} (ID: {})", title, start_time.format("%Y-%m-%d %H:%M"), created_event.id )) } async fn execute_book_meeting( state: &AppState, user: &UserSession, meeting_json: String, attendees: Vec, ) -> Result { // Parse meeting details from JSON let meeting_data: serde_json::Value = serde_json::from_str(&meeting_json) .map_err(|e| format!("Invalid meeting details: {}", e))?; let title = meeting_data["title"] .as_str() .ok_or("Missing meeting title")?; let start_time_str = meeting_data["start_time"] .as_str() .ok_or("Missing start time")?; let duration_minutes = meeting_data["duration"].as_i64().unwrap_or(60); let description = meeting_data["description"].as_str().unwrap_or(""); let location = meeting_data["location"].as_str().unwrap_or(""); let recurring = meeting_data["recurring"].as_bool().unwrap_or(false); let start_time = parse_time_string(start_time_str)?; let end_time = start_time + Duration::minutes(duration_minutes); // Get or create calendar engine let calendar_engine = get_calendar_engine(state).await?; // Check conflicts for all attendees for attendee in &attendees { let conflicts = calendar_engine .check_conflicts(start_time, end_time, attendee) .await .map_err(|e| format!("Failed to check conflicts: {}", e))?; if !conflicts.is_empty() { return Err(format!("Attendee {} has a conflict at this time", attendee)); } } // Create recurrence rule if needed let recurrence_rule = if recurring { Some(RecurrenceRule { frequency: "WEEKLY".to_string(), interval: 1, count: Some(10), // Default to 10 occurrences until: None, by_day: None, }) } else { None }; // Create calendar event let event = CalendarEvent { id: Uuid::new_v4(), title: title.to_string(), description: Some(description.to_string()), start_time, end_time, location: if location.is_empty() { None } else { Some(location.to_string()) }, organizer: user.user_id.to_string(), attendees: attendees.clone(), reminder_minutes: Some(30), // 30-minute reminder for meetings recurrence_rule, status: EventStatus::Confirmed, created_at: Utc::now(), updated_at: Utc::now(), }; // Create the meeting let created_event = calendar_engine .create_event(event) .await .map_err(|e| format!("Failed to create meeting: {}", e))?; // Send invites to attendees (would integrate with email system) for attendee in &attendees { send_meeting_invite(state, &created_event, attendee).await?; } info!( "Meeting booked: {} at {} with {} attendees", title, start_time, attendees.len() ); Ok(format!( "Meeting '{}' scheduled for {} with {} attendees (ID: {})", title, start_time.format("%Y-%m-%d %H:%M"), attendees.len(), created_event.id )) } async fn check_availability( state: &AppState, _user: &UserSession, date_str: &str, duration_minutes: i64, ) -> Result { let date = parse_date_string(date_str)?; let calendar_engine = get_calendar_engine(state).await?; // Define business hours (9 AM to 5 PM) let business_start = date.with_hour(9).unwrap().with_minute(0).unwrap(); let business_end = date.with_hour(17).unwrap().with_minute(0).unwrap(); // Get all events for the day let events = calendar_engine .get_events_range(business_start, business_end) .await .map_err(|e| format!("Failed to get events: {}", e))?; // Find available slots let mut available_slots = Vec::new(); let mut current_time = business_start; let slot_duration = Duration::minutes(duration_minutes); for event in &events { // Check if there's a gap before this event if current_time + slot_duration <= event.start_time { available_slots.push(format!( "{} - {}", current_time.format("%H:%M"), (current_time + slot_duration).format("%H:%M") )); } current_time = event.end_time; } // Check if there's time after the last event if current_time + slot_duration <= business_end { available_slots.push(format!( "{} - {}", current_time.format("%H:%M"), (current_time + slot_duration).format("%H:%M") )); } if available_slots.is_empty() { Ok("No available slots on this date".to_string()) } else { Ok(format!( "Available slots on {}: {}", date.format("%Y-%m-%d"), available_slots.join(", ") )) } } fn parse_time_string(time_str: &str) -> Result, String> { // Try different date formats let formats = vec![ "%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M", "%d/%m/%Y %H:%M", "%Y-%m-%dT%H:%M:%S", ]; for format in formats { if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(time_str, format) { return Ok(DateTime::from_naive_utc_and_offset(dt, Utc)); } } // Try parsing relative times like "tomorrow at 3pm" if time_str.contains("tomorrow") { let tomorrow = Utc::now() + Duration::days(1); if let Some(hour) = extract_hour_from_string(time_str) { return Ok(tomorrow .with_hour(hour) .unwrap() .with_minute(0) .unwrap() .with_second(0) .unwrap()); } } // Try parsing relative times like "in 2 hours" if time_str.starts_with("in ") { if let Ok(hours) = time_str .trim_start_matches("in ") .trim_end_matches(" hours") .trim_end_matches(" hour") .parse::() { return Ok(Utc::now() + Duration::hours(hours)); } } Err(format!("Could not parse time: {}", time_str)) } fn parse_date_string(date_str: &str) -> Result, String> { // Handle special cases if date_str == "today" { return Ok(Utc::now()); } else if date_str == "tomorrow" { return Ok(Utc::now() + Duration::days(1)); } // Try standard date formats let formats = vec!["%Y-%m-%d", "%Y/%m/%d", "%d/%m/%Y"]; for format in formats { if let Ok(dt) = chrono::NaiveDate::parse_from_str(date_str, format) { return Ok(dt.and_hms_opt(0, 0, 0).unwrap().and_utc()); } } Err(format!("Could not parse date: {}", date_str)) } fn extract_hour_from_string(s: &str) -> Option { // Extract hour from strings like "3pm", "15:00", "3 PM" let s = s.to_lowercase(); if s.contains("pm") { if let Some(hour_str) = s.split("pm").next() { if let Ok(hour) = hour_str.trim().replace(":", "").parse::() { return Some(if hour < 12 { hour + 12 } else { hour }); } } } else if s.contains("am") { if let Some(hour_str) = s.split("am").next() { if let Ok(hour) = hour_str.trim().replace(":", "").parse::() { return Some(if hour == 12 { 0 } else { hour }); } } } None } async fn log_booking( state: &AppState, user: &UserSession, event_id: &str, title: &str, ) -> Result<(), String> { let mut conn = state.conn.get().map_err(|e| format!("DB error: {}", e))?; diesel::sql_query( "INSERT INTO booking_logs (id, user_id, bot_id, event_id, event_title, booked_at) VALUES (gen_random_uuid(), $1, $2, $3, $4, NOW())", ) .bind::(&user.user_id) .bind::(&user.bot_id) .bind::(event_id) .bind::(title) .execute(&mut *conn) .map_err(|e| format!("Failed to log booking: {}", e))?; Ok(()) } async fn get_calendar_engine(state: &AppState) -> Result, String> { // Get or create calendar engine from app state // This would normally be initialized at startup let calendar_engine = Arc::new(CalendarEngine::new(state.conn.clone())); Ok(calendar_engine) } async fn send_meeting_invite( _state: &AppState, event: &CalendarEvent, attendee: &str, ) -> Result<(), String> { // This would integrate with the email system to send calendar invites info!( "Would send meeting invite for '{}' to {}", event.title, attendee ); Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_time_string() { let result = parse_time_string("2024-01-15 14:30"); assert!(result.is_ok()); let result = parse_time_string("tomorrow at 3pm"); assert!(result.is_ok()); let result = parse_time_string("in 2 hours"); assert!(result.is_ok()); } #[test] fn test_parse_date_string() { let result = parse_date_string("today"); assert!(result.is_ok()); let result = parse_date_string("2024-01-15"); assert!(result.is_ok()); let result = parse_date_string("tomorrow"); assert!(result.is_ok()); } #[test] fn test_extract_hour() { assert_eq!(extract_hour_from_string("3pm"), Some(15)); assert_eq!(extract_hour_from_string("3 PM"), Some(15)); assert_eq!(extract_hour_from_string("10am"), Some(10)); assert_eq!(extract_hour_from_string("12am"), Some(0)); assert_eq!(extract_hour_from_string("12pm"), Some(12)); } }