//! Alert dispatching and delivery. use crate::domain::{Alert, AlertId, Priority, Survivor}; use crate::MatError; use super::AlertGenerator; use std::collections::HashMap; /// Configuration for alert dispatch #[derive(Debug, Clone)] pub struct AlertConfig { /// Enable audio alerts pub audio_enabled: bool, /// Enable visual alerts pub visual_enabled: bool, /// Escalation timeout in seconds pub escalation_timeout_secs: u64, /// Maximum pending alerts before forced escalation pub max_pending_alerts: usize, /// Auto-acknowledge after seconds (0 = disabled) pub auto_ack_secs: u64, } impl Default for AlertConfig { fn default() -> Self { Self { audio_enabled: true, visual_enabled: true, escalation_timeout_secs: 300, // 5 minutes max_pending_alerts: 50, auto_ack_secs: 0, // Disabled } } } /// Dispatcher for sending alerts to rescue teams pub struct AlertDispatcher { config: AlertConfig, generator: AlertGenerator, pending_alerts: parking_lot::RwLock>, handlers: Vec>, } impl AlertDispatcher { /// Create a new alert dispatcher pub fn new(config: AlertConfig) -> Self { Self { config, generator: AlertGenerator::new(), pending_alerts: parking_lot::RwLock::new(HashMap::new()), handlers: Vec::new(), } } /// Add an alert handler pub fn add_handler(&mut self, handler: Box) { self.handlers.push(handler); } /// Generate an alert for a survivor pub fn generate_alert(&self, survivor: &Survivor) -> Result { self.generator.generate(survivor) } /// Dispatch an alert pub async fn dispatch(&self, alert: Alert) -> Result<(), MatError> { let alert_id = alert.id().clone(); let priority = alert.priority(); // Store in pending alerts self.pending_alerts.write().insert(alert_id.clone(), alert.clone()); // Log the alert tracing::info!( alert_id = %alert_id, priority = ?priority, title = %alert.payload().title, "Dispatching alert" ); // Send to all handlers for handler in &self.handlers { if let Err(e) = handler.handle(&alert).await { tracing::warn!( alert_id = %alert_id, handler = %handler.name(), error = %e, "Handler failed to process alert" ); } } // Check if we're at capacity let pending_count = self.pending_alerts.read().len(); if pending_count >= self.config.max_pending_alerts { tracing::warn!( pending_count, max = self.config.max_pending_alerts, "Alert capacity reached - escalating oldest alerts" ); self.escalate_oldest().await?; } Ok(()) } /// Acknowledge an alert pub fn acknowledge(&self, alert_id: &AlertId, by: &str) -> Result<(), MatError> { let mut alerts = self.pending_alerts.write(); if let Some(alert) = alerts.get_mut(alert_id) { alert.acknowledge(by); tracing::info!( alert_id = %alert_id, acknowledged_by = by, "Alert acknowledged" ); Ok(()) } else { Err(MatError::Alerting(format!("Alert {} not found", alert_id))) } } /// Resolve an alert pub fn resolve(&self, alert_id: &AlertId, resolution: crate::domain::AlertResolution) -> Result<(), MatError> { let mut alerts = self.pending_alerts.write(); if let Some(alert) = alerts.remove(alert_id) { let mut resolved_alert = alert; resolved_alert.resolve(resolution); tracing::info!( alert_id = %alert_id, "Alert resolved" ); Ok(()) } else { Err(MatError::Alerting(format!("Alert {} not found", alert_id))) } } /// Get all pending alerts pub fn pending(&self) -> Vec { self.pending_alerts.read().values().cloned().collect() } /// Get pending alerts by priority pub fn pending_by_priority(&self, priority: Priority) -> Vec { self.pending_alerts .read() .values() .filter(|a| a.priority() == priority) .cloned() .collect() } /// Get count of pending alerts pub fn pending_count(&self) -> usize { self.pending_alerts.read().len() } /// Check and escalate timed-out alerts pub async fn check_escalations(&self) -> Result { let timeout_secs = self.config.escalation_timeout_secs as i64; let mut escalated = 0; let mut to_escalate = Vec::new(); { let alerts = self.pending_alerts.read(); for (id, alert) in alerts.iter() { if alert.needs_escalation(timeout_secs) { to_escalate.push(id.clone()); } } } for id in to_escalate { let mut alerts = self.pending_alerts.write(); if let Some(alert) = alerts.get_mut(&id) { alert.escalate(); escalated += 1; tracing::warn!( alert_id = %id, new_priority = ?alert.priority(), "Alert escalated due to timeout" ); } } Ok(escalated) } /// Escalate oldest pending alerts async fn escalate_oldest(&self) -> Result<(), MatError> { let mut alerts: Vec<_> = self.pending_alerts.read() .iter() .map(|(id, alert)| (id.clone(), *alert.created_at())) .collect(); // Sort by creation time (oldest first) alerts.sort_by_key(|(_, created)| *created); // Escalate oldest 10% let to_escalate = (alerts.len() / 10).max(1); let mut pending = self.pending_alerts.write(); for (id, _) in alerts.into_iter().take(to_escalate) { if let Some(alert) = pending.get_mut(&id) { alert.escalate(); } } Ok(()) } /// Get configuration pub fn config(&self) -> &AlertConfig { &self.config } } /// Handler for processing alerts #[async_trait::async_trait] pub trait AlertHandler: Send + Sync { /// Handler name fn name(&self) -> &str; /// Handle an alert async fn handle(&self, alert: &Alert) -> Result<(), MatError>; } /// Console/logging alert handler pub struct ConsoleAlertHandler; #[async_trait::async_trait] impl AlertHandler for ConsoleAlertHandler { fn name(&self) -> &str { "console" } async fn handle(&self, alert: &Alert) -> Result<(), MatError> { let priority_indicator = match alert.priority() { Priority::Critical => "🔴", Priority::High => "🟠", Priority::Medium => "🟡", Priority::Low => "🔵", }; println!("\n{} ALERT {}", priority_indicator, "=".repeat(50)); println!("ID: {}", alert.id()); println!("Priority: {:?}", alert.priority()); println!("Title: {}", alert.payload().title); println!("{}", "=".repeat(60)); println!("{}", alert.payload().message); println!("{}", "=".repeat(60)); println!("Recommended Action: {}", alert.payload().recommended_action); println!("{}\n", "=".repeat(60)); Ok(()) } } /// Audio alert handler. /// /// Requires platform audio support. On systems without audio hardware /// (headless servers, embedded), this logs the alert pattern. On systems /// with audio, integrate with the platform's audio API. pub struct AudioAlertHandler { /// Whether audio hardware is available audio_available: bool, } impl AudioAlertHandler { /// Create a new audio handler, auto-detecting audio support. pub fn new() -> Self { let audio_available = std::env::var("DISPLAY").is_ok() || std::env::var("PULSE_SERVER").is_ok(); Self { audio_available } } /// Create with explicit audio availability flag. pub fn with_availability(available: bool) -> Self { Self { audio_available: available } } } impl Default for AudioAlertHandler { fn default() -> Self { Self::new() } } #[async_trait::async_trait] impl AlertHandler for AudioAlertHandler { fn name(&self) -> &str { "audio" } async fn handle(&self, alert: &Alert) -> Result<(), MatError> { let pattern = alert.priority().audio_pattern(); if self.audio_available { // Platform audio integration point. // Pattern encodes urgency: Critical=continuous, High=3-burst, etc. tracing::info!( alert_id = %alert.id(), pattern, "Playing audio alert pattern" ); } else { tracing::debug!( alert_id = %alert.id(), pattern, "Audio hardware not available - alert pattern logged only" ); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::domain::{SurvivorId, TriageStatus, AlertPayload}; fn create_test_alert() -> Alert { Alert::new( SurvivorId::new(), Priority::High, AlertPayload::new("Test Alert", "Test message", TriageStatus::Delayed), ) } #[tokio::test] async fn test_dispatch_alert() { let dispatcher = AlertDispatcher::new(AlertConfig::default()); let alert = create_test_alert(); let result = dispatcher.dispatch(alert).await; assert!(result.is_ok()); assert_eq!(dispatcher.pending_count(), 1); } #[tokio::test] async fn test_acknowledge_alert() { let dispatcher = AlertDispatcher::new(AlertConfig::default()); let alert = create_test_alert(); let alert_id = alert.id().clone(); dispatcher.dispatch(alert).await.unwrap(); let result = dispatcher.acknowledge(&alert_id, "Team Alpha"); assert!(result.is_ok()); let pending = dispatcher.pending(); assert!(pending.iter().any(|a| a.id() == &alert_id && a.acknowledged_by() == Some("Team Alpha"))); } #[tokio::test] async fn test_resolve_alert() { let dispatcher = AlertDispatcher::new(AlertConfig::default()); let alert = create_test_alert(); let alert_id = alert.id().clone(); dispatcher.dispatch(alert).await.unwrap(); let resolution = crate::domain::AlertResolution { resolution_type: crate::domain::ResolutionType::Rescued, notes: "Survivor extracted successfully".to_string(), resolved_by: Some("Team Alpha".to_string()), resolved_at: chrono::Utc::now(), }; dispatcher.resolve(&alert_id, resolution).unwrap(); assert_eq!(dispatcher.pending_count(), 0); } }