use crate::domain::auth::Authenticator; use crate::error::{AppError, AppResult}; use chrono::{DateTime, Duration, Utc}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalendarEvent { pub id: Option, pub name: String, pub from: DateTime, pub to: DateTime, pub user_sub: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreateEventRequest { pub name: String, pub from: String, pub to: String, pub user_sub: Option, } struct TokenState { access_token: String, expires_at: DateTime, } pub struct CalendarClient { base_url: String, client: Client, authenticator: Arc, token_state: RwLock>, } impl CalendarClient { pub fn new(base_url: String, authenticator: Arc) -> Self { let client = Client::builder() .timeout(std::time::Duration::from_secs(30)) .connect_timeout(std::time::Duration::from_secs(10)) .build() .unwrap_or_else(|_| Client::new()); Self { base_url: base_url.trim_end_matches('/').to_string(), client, authenticator, token_state: RwLock::new(None), } } async fn get_token(&self) -> AppResult { { let state = self.token_state.read().await; if let Some(token) = &*state { if token.expires_at > Utc::now() + Duration::seconds(30) { tracing::debug!("Using cached Calendar API token"); return Ok(token.access_token.clone()); } } } let mut state = self.token_state.write().await; // Double check after acquiring write lock if let Some(token) = &*state { if token.expires_at > Utc::now() + Duration::seconds(30) { return Ok(token.access_token.clone()); } } tracing::info!("Refreshing Calendar API token via Client Credentials flow"); let token_data = self .authenticator .client_credentials("profile") .await .map_err(|e| AppError::Internal(format!("Failed to get client credentials: {}", e)))?; let access_token = token_data["access_token"] .as_str() .ok_or_else(|| AppError::Internal("Missing access_token in response".into()))? .to_string(); let expires_in = token_data["expires_in"].as_i64().unwrap_or(3600); let expires_at = Utc::now() + Duration::seconds(expires_in); *state = Some(TokenState { access_token: access_token.clone(), expires_at, }); Ok(access_token) } pub async fn list_events( &self, user_sub: Option, upcoming: Option, ) -> AppResult> { let token = self.get_token().await?; let mut url = format!("{}/service/v1/events", self.base_url); let mut params = Vec::new(); if let Some(uid) = &user_sub { params.push(format!("user_sub={}", uid)); } if let Some(u) = upcoming { params.push(format!("upcoming={}", u)); } if !params.is_empty() { url.push_str("?"); url.push_str(¶ms.join("&")); } tracing::info!(method = "GET", %url, "Sending Calendar API request"); let res = self .client .get(&url) .bearer_auth(token) .send() .await .map_err(AppError::Network)?; let status = res.status(); tracing::info!(%status, %url, "Received Calendar API response"); if !status.is_success() { let error_body = res.text().await.unwrap_or_default(); tracing::error!(%status, %url, body = %error_body, "Calendar API request failed"); return Err(AppError::Internal(format!( "Failed to list events: {} - {}", status, error_body ))); } let body = res .json() .await .map_err(|e| AppError::Internal(e.to_string())); tracing::info!(%status, %url, body = ?body, "Received Calendar API response"); body } pub async fn create_event( &self, user_sub: Option, name: &str, from: &str, to: &str, ) -> AppResult { let token = self.get_token().await?; let url = format!("{}/service/v1/events", self.base_url); let request = CreateEventRequest { name: name.to_string(), from: from.to_string(), to: to.to_string(), user_sub, }; tracing::info!(method = "POST", %url, "Sending Calendar API request"); let res = self .client .post(&url) .bearer_auth(token) .json(&request) .send() .await .map_err(AppError::Network)?; let status = res.status(); tracing::info!(%status, %url, "Received Calendar API response"); if !status.is_success() { let error_body = res.text().await.unwrap_or_default(); tracing::error!(%status, %url, body = %error_body, "Calendar API request failed"); return Err(AppError::Internal(format!( "Failed to create event: {} - {}", status, error_body ))); } res.json() .await .map_err(|e| AppError::Internal(e.to_string())) } #[allow(dead_code)] pub async fn get_event(&self, id: i32) -> AppResult { let token = self.get_token().await?; let url = format!("{}/service/v1/events/{}", self.base_url, id); tracing::info!(method = "GET", %url, "Sending Calendar API request"); let res = self .client .get(&url) .bearer_auth(token) .send() .await .map_err(AppError::Network)?; let status = res.status(); tracing::info!(%status, %url, "Received Calendar API response"); if !status.is_success() { let error_body = res.text().await.unwrap_or_default(); tracing::error!(%status, %url, body = %error_body, "Calendar API request failed"); return Err(AppError::Internal(format!( "Failed to get event: {} - {}", status, error_body ))); } res.json() .await .map_err(|e| AppError::Internal(e.to_string())) } #[allow(dead_code)] pub async fn update_event( &self, id: i32, user_sub: Option, name: &str, from: &str, to: &str, ) -> AppResult { let token = self.get_token().await?; let url = format!("{}/service/v1/events/{}", self.base_url, id); let request = CreateEventRequest { name: name.to_string(), from: from.to_string(), to: to.to_string(), user_sub, }; tracing::info!(method = "PUT", %url, "Sending Calendar API request"); let res = self .client .put(&url) .bearer_auth(token) .json(&request) .send() .await .map_err(AppError::Network)?; let status = res.status(); tracing::info!(%status, %url, "Received Calendar API response"); if !status.is_success() { let error_body = res.text().await.unwrap_or_default(); tracing::error!(%status, %url, body = %error_body, "Calendar API request failed"); return Err(AppError::Internal(format!( "Failed to update event: {} - {}", status, error_body ))); } res.json() .await .map_err(|e| AppError::Internal(e.to_string())) } #[allow(dead_code)] pub async fn delete_event(&self, id: i32) -> AppResult<()> { let token = self.get_token().await?; let url = format!("{}/service/v1/events/{}", self.base_url, id); tracing::info!(method = "DELETE", %url, "Sending Calendar API request"); let res = self .client .delete(&url) .bearer_auth(token) .send() .await .map_err(AppError::Network)?; let status = res.status(); tracing::info!(%status, %url, "Received Calendar API response"); if !status.is_success() { let error_body = res.text().await.unwrap_or_default(); tracing::error!(%status, %url, body = %error_body, "Calendar API request failed"); return Err(AppError::Internal(format!( "Failed to delete event: {} - {}", status, error_body ))); } Ok(()) } }