use anyhow::{Context, Result}; use reqwest::{blocking::{Client, Response}, header}; use serde_json::json; use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; use std::time::Duration; use crate::models::*; /// API Client for BeepZone backend #[derive(Clone)] pub struct ApiClient { client: Client, base_url: String, token: Option, db_timeout_flag: Arc, } impl ApiClient { /// Create a new API client pub fn new(base_url: String) -> Result { let client = Client::builder() .timeout(Duration::from_secs(30)) .build() .context("Failed to create HTTP client")?; Ok(Self { client, base_url: base_url.trim_end_matches('/').to_string(), token: None, db_timeout_flag: Arc::new(AtomicBool::new(false)), }) } fn flag_timeout_signal(&self) { self.db_timeout_flag.store(true, Ordering::SeqCst); } fn observe_response_error(&self, error: &Option) { if Self::is_database_timeout_error(error) { self.flag_timeout_signal(); } } fn send_request( &self, builder: reqwest::blocking::RequestBuilder, context_msg: &'static str, ) -> Result { builder .send() .map_err(|err| { self.flag_timeout_signal(); err }) .context(context_msg) } /// Returns true if a timeout signal was previously raised (and clears it) pub fn take_timeout_signal(&self) -> bool { self.db_timeout_flag.swap(false, Ordering::SeqCst) } /// Set the authentication token pub fn set_token(&mut self, token: String) { self.token = Some(token); } /// Clear the authentication token #[allow(dead_code)] pub fn clear_token(&mut self) { self.token = None; } /// Check if server is reachable pub fn health_check(&self) -> Result { let url = format!("{}/health", self.base_url); let response = self .send_request(self.client.get(&url), "Failed to perform health check")?; Ok(response.status().is_success()) } /// Get health details (tries to parse JSON; returns None if non-JSON) pub fn health_info(&self) -> Result> { let url = format!("{}/health", self.base_url); let response = self .send_request(self.client.get(&url), "Failed to fetch health info")?; if !response.status().is_success() { return Ok(None); } // Try to parse as JSON; if it fails, just return None (some servers return plain text) let text = response.text()?; match serde_json::from_str::(&text) { Ok(v) => Ok(Some(v)), Err(_) => Ok(None), } } /// Check if the error message indicates a database timeout pub fn is_database_timeout_error(error: &Option) -> bool { if let Some(err) = error { err.contains("Database temporarily unavailable") } else { false } } // Authentication Methods /// Login with username and password pub fn login_password(&self, username: &str, password: &str) -> Result { let url = format!("{}/auth/login", self.base_url); let body = LoginRequest { method: "password".to_string(), username: Some(username.to_string()), password: Some(password.to_string()), pin: None, login_string: None, }; let response = self.send_request( self.client.post(&url).json(&body), "Failed to send login request", )?; let result: LoginResponse = response.json().context("Failed to parse login response")?; Ok(result) } /// Login with PIN #[allow(dead_code)] pub fn login_pin(&self, username: &str, pin: &str) -> Result> { let url = format!("{}/auth/login", self.base_url); let body = LoginRequest { method: "pin".to_string(), username: Some(username.to_string()), password: None, pin: Some(pin.to_string()), login_string: None, }; let response = self.send_request( self.client.post(&url).json(&body), "Failed to send PIN login request", )?; let result: ApiResponse = response.json().context("Failed to parse login response")?; self.observe_response_error(&result.error); Ok(result) } /// Login with token/RFID string #[allow(dead_code)] pub fn login_token(&self, login_string: &str) -> Result> { let url = format!("{}/auth/login", self.base_url); let body = LoginRequest { method: "token".to_string(), username: None, password: None, pin: None, login_string: Some(login_string.to_string()), }; let response = self.send_request( self.client.post(&url).json(&body), "Failed to send token login request", )?; let result: ApiResponse = response.json().context("Failed to parse login response")?; self.observe_response_error(&result.error); Ok(result) } /// Logout current session pub fn logout(&self) -> Result> { let url = format!("{}/auth/logout", self.base_url); let response = self.send_request( self.make_authorized_request(reqwest::Method::POST, &url)?, "Failed to send logout request", )?; let result: ApiResponse<()> = response.json()?; self.observe_response_error(&result.error); Ok(result) } /// Check session status #[allow(dead_code)] pub fn check_session(&self) -> Result> { let url = format!("{}/auth/status", self.base_url); let response = self.send_request( self.make_authorized_request(reqwest::Method::GET, &url)?, "Failed to check session status", )?; let result: ApiResponse = response.json()?; self.observe_response_error(&result.error); Ok(result) } /// Best-effort session validity check. /// Returns Ok(true) when the session appears valid, Ok(false) when clearly invalid (401/403 or explicit valid=false). /// Be tolerant of different backend response shapes and assume valid on ambiguous 2xx responses. pub fn check_session_valid(&self) -> Result { let url = format!("{}/auth/status", self.base_url); let response = self.send_request( self.make_authorized_request(reqwest::Method::GET, &url)?, "Failed to check session status", )?; let status = response.status(); let text = response.text()?; // Explicitly invalid if unauthorized/forbidden if status.as_u16() == 401 || status.as_u16() == 403 { return Ok(false); } // Parse generic JSON and look for common shapes first if let Ok(val) = serde_json::from_str::(&text) { // data.valid if let Some(valid) = val .get("data") .and_then(|d| d.get("valid")) .and_then(|v| v.as_bool()) { return Ok(valid); } // top-level valid if let Some(valid) = val.get("valid").and_then(|v| v.as_bool()) { return Ok(valid); } // success=true is generally a good sign if val.get("success").and_then(|v| v.as_bool()) == Some(true) { return Ok(true); } } // As a last attempt, try strict ApiResponse if let Ok(parsed) = serde_json::from_str::>(&text) { if let Some(data) = parsed.data { return Ok(data.valid); } // If no data provided, treat success=true as valid by default return Ok(parsed.success || status.is_success()); } // Last resort: if response was 2xx and not explicitly invalid, assume valid Ok(status.is_success()) } // Permissions & Preferences /// Get current user's permissions #[allow(dead_code)] pub fn get_permissions(&self) -> Result> { let url = format!("{}/permissions", self.base_url); let response = self.send_request( self.make_authorized_request(reqwest::Method::GET, &url)?, "Failed to get permissions", )?; let result: ApiResponse = response.json()?; self.observe_response_error(&result.error); Ok(result) } /// Get user preferences #[allow(dead_code)] pub fn get_preferences( &self, user_id: Option, ) -> Result> { let url = format!("{}/preferences", self.base_url); let body = PreferencesRequest { action: "get".to_string(), user_id, preferences: None, }; let response = self.send_request( self.make_authorized_request(reqwest::Method::POST, &url)? .json(&body), "Failed to get preferences", )?; let result: ApiResponse = response.json()?; self.observe_response_error(&result.error); Ok(result) } /// Set user preferences #[allow(dead_code)] pub fn set_preferences( &self, values: serde_json::Value, user_id: Option, ) -> Result> { let url = format!("{}/preferences", self.base_url); let body = PreferencesRequest { action: "set".to_string(), user_id, preferences: Some(values), }; let response = self.send_request( self.make_authorized_request(reqwest::Method::POST, &url)? .json(&body), "Failed to set preferences", )?; let result: ApiResponse = response.json()?; self.observe_response_error(&result.error); Ok(result) } // Query Methods /// Execute a generic query pub fn query(&self, request: &QueryRequest) -> Result> { let url = format!("{}/query", self.base_url); // Log the serialized request for debugging let body = serde_json::to_value(request)?; log::debug!("Query request JSON: {}", serde_json::to_string(&body)?); // Log the request for debugging JOINs if request.joins.is_some() { log::debug!( "Query with JOINs: table={}, columns={:?}, joins={:?}", request.table, request.columns.as_ref().map(|c| c.len()), request.joins.as_ref().map(|j| j.len()) ); } let response = self.send_request( self.make_authorized_request(reqwest::Method::POST, &url)? .json(&body), "Failed to execute query", )?; // Try to get the response text for debugging let status = response.status(); let response_text = response.text()?; // Log the raw response for debugging if !status.is_success() { log::error!("API error ({}): {}", status, response_text); } else { log::debug!( "API response (first 500 chars): {}", if response_text.len() > 500 { &response_text[..500] } else { &response_text } ); } // Now try to parse it let result: ApiResponse = serde_json::from_str(&response_text) .with_context(|| { format!( "Failed to parse API response. Status: {}, Body: {}", status, if response_text.len() > 200 { &response_text[..200] } else { &response_text } ) })?; self.observe_response_error(&result.error); Ok(result) } /// Select records from a table pub fn select( &self, table: &str, columns: Option>, where_clause: Option, order_by: Option>, limit: Option, ) -> Result>> { let request = QueryRequest { action: "select".to_string(), table: table.to_string(), columns, data: None, r#where: where_clause, filter: None, order_by, limit, offset: None, joins: None, }; let response = self.query(&request)?; if response.success { let data = response.data.unwrap_or(json!([])); let records: Vec = serde_json::from_value(data)?; Ok(ApiResponse { success: true, data: Some(records), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } /// Select records from a table with JOINs pub fn select_with_joins( &self, table: &str, columns: Option>, where_clause: Option, filter: Option, order_by: Option>, limit: Option, joins: Option>, ) -> Result>> { let request = QueryRequest { action: "select".to_string(), table: table.to_string(), columns, data: None, r#where: where_clause, filter, order_by, limit, offset: None, joins, }; let response = self.query(&request)?; if response.success { let data = response.data.unwrap_or(json!([])); let records: Vec = serde_json::from_value(data)?; Ok(ApiResponse { success: true, data: Some(records), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } /// Insert a record pub fn insert(&self, table: &str, values: serde_json::Value) -> Result> { let request = QueryRequest { action: "insert".to_string(), table: table.to_string(), columns: None, data: Some(values), r#where: None, filter: None, order_by: None, limit: None, offset: None, joins: None, }; let response = self.query(&request)?; if response.success { let id: i32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; Ok(ApiResponse { success: true, data: Some(id), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } /// Update records pub fn update( &self, table: &str, values: serde_json::Value, where_clause: serde_json::Value, ) -> Result> { let request = QueryRequest { action: "update".to_string(), table: table.to_string(), columns: None, data: Some(values), r#where: Some(where_clause), filter: None, order_by: None, limit: None, offset: None, joins: None, }; let response = self.query(&request)?; if response.success { let count: u32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; Ok(ApiResponse { success: true, data: Some(count), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } /// Delete records pub fn delete(&self, table: &str, where_clause: serde_json::Value) -> Result> { let request = QueryRequest { action: "delete".to_string(), table: table.to_string(), columns: None, data: None, r#where: Some(where_clause), filter: None, order_by: None, limit: None, offset: None, joins: None, }; let response = self.query(&request)?; if response.success { let count: u32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; Ok(ApiResponse { success: true, data: Some(count), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } /// Cunt records pub fn count( &self, table: &str, where_clause: Option, ) -> Result> { let request = QueryRequest { action: "count".to_string(), table: table.to_string(), columns: None, data: None, r#where: where_clause, filter: None, order_by: None, limit: None, offset: None, joins: None, }; let response = self.query(&request)?; if response.success { let count: i32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; Ok(ApiResponse { success: true, data: Some(count), error: None, message: response.message, }) } else { Ok(ApiResponse { success: false, data: None, error: response.error, message: response.message, }) } } // Helper Methods /// Create an authorized request with proper headers fn make_authorized_request( &self, method: reqwest::Method, url: &str, ) -> Result { let token = self.token.as_ref().context("No authentication token set")?; let builder = self .client .request(method, url) .header(header::AUTHORIZATION, format!("Bearer {}", token)); Ok(builder) } /// Get the based URL pub fn base_url(&self) -> &str { &self.base_url } }