1use crate::constants::{HISTORY_CLEANUP_BATCH_SIZE, MAX_MESSAGE_HISTORY};
7use crate::{Error, Result};
8use offline_protocol_transport::TransportType;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::SystemTime;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct NetworkNode {
16 pub user_id: String,
18 pub role: NodeRole,
20 pub connection_count: usize,
22 pub battery_level: Option<u8>,
24 pub last_seen: i64,
26 pub transports: Vec<TransportType>,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum NodeRole {
34 Normal,
36 Relay,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct NetworkLink {
43 pub from: String,
45 pub to: String,
47 pub quality: f32,
49 pub transport: TransportType,
51 pub rssi: Option<i16>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct NetworkTopology {
58 pub timestamp: i64,
60 pub local_user_id: String,
62 pub nodes: Vec<NetworkNode>,
64 pub links: Vec<NetworkLink>,
66 pub stats: NetworkStats,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct NetworkStats {
73 pub total_nodes: usize,
75 pub relay_nodes: usize,
77 pub total_connections: usize,
79 pub avg_link_quality: f32,
81 pub network_diameter: Option<u8>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct MessageStats {
88 pub message_id: String,
90 pub sender: String,
92 pub recipient: String,
94 pub sent_at: i64,
96 pub delivered_at: Option<i64>,
98 pub hop_count: u8,
100 pub transport: Option<TransportType>,
102 pub retry_count: u32,
104 pub latency_ms: Option<u64>,
106}
107
108pub struct NetworkVisualizer {
110 nodes: HashMap<String, NetworkNode>,
112 links: Vec<NetworkLink>,
114 message_history: Vec<MessageStats>,
116 local_user_id: String,
118}
119
120impl NetworkVisualizer {
121 pub fn new(local_user_id: impl Into<String>) -> Self {
123 Self {
124 nodes: HashMap::new(),
125 links: Vec::new(),
126 message_history: Vec::new(),
127 local_user_id: local_user_id.into(),
128 }
129 }
130
131 pub fn set_local_user_id(&mut self, local_user_id: impl Into<String>) {
140 self.local_user_id = local_user_id.into();
141 }
142
143 pub fn update_node(&mut self, node: NetworkNode) {
145 self.nodes.insert(node.user_id.clone(), node);
146 }
147
148 pub fn add_link(&mut self, link: NetworkLink) {
150 self.links
152 .retain(|l| !(l.from == link.from && l.to == link.to));
153 self.links.push(link);
154 }
155
156 pub fn remove_link(&mut self, from: &str, to: &str) {
158 self.links.retain(|l| !(l.from == from && l.to == to));
159 }
160
161 pub fn record_message(&mut self, stats: MessageStats) {
163 self.message_history.push(stats);
164
165 if self.message_history.len() > MAX_MESSAGE_HISTORY {
166 self.message_history.drain(0..HISTORY_CLEANUP_BATCH_SIZE);
167 }
168 }
169
170 pub fn get_topology(&self) -> NetworkTopology {
172 let stats = self.calculate_stats();
173
174 NetworkTopology {
175 timestamp: SystemTime::now()
176 .duration_since(SystemTime::UNIX_EPOCH)
177 .map(|d| d.as_secs() as i64)
178 .unwrap_or(0),
179 local_user_id: self.local_user_id.clone(),
180 nodes: self.nodes.values().cloned().collect(),
181 links: self.links.clone(),
182 stats,
183 }
184 }
185
186 fn calculate_stats(&self) -> NetworkStats {
188 let total_nodes = self.nodes.len();
189 let relay_nodes = self
190 .nodes
191 .values()
192 .filter(|n| n.role == NodeRole::Relay)
193 .count();
194 let total_connections = self.links.len();
195
196 let avg_link_quality = if total_connections > 0 {
197 self.links.iter().map(|l| l.quality).sum::<f32>() / total_connections as f32
198 } else {
199 0.0
200 };
201
202 NetworkStats {
203 total_nodes,
204 relay_nodes,
205 total_connections,
206 avg_link_quality,
207 network_diameter: self.calculate_diameter(),
208 }
209 }
210
211 fn calculate_diameter(&self) -> Option<u8> {
215 if self.nodes.is_empty() {
216 return None;
217 }
218
219 let nodes: Vec<_> = self.nodes.keys().cloned().collect();
221 let n = nodes.len();
222
223 if n == 1 {
224 return Some(0);
225 }
226
227 let mut dist = vec![vec![u8::MAX; n]; n];
229
230 for (i, row) in dist.iter_mut().enumerate() {
232 row[i] = 0;
233 }
234
235 for link in &self.links {
237 if let (Some(from_idx), Some(to_idx)) = (
238 nodes.iter().position(|id| id == &link.from),
239 nodes.iter().position(|id| id == &link.to),
240 ) {
241 dist[from_idx][to_idx] = 1;
242 dist[to_idx][from_idx] = 1; }
244 }
245
246 for k in 0..n {
248 for i in 0..n {
249 for j in 0..n {
250 if dist[i][k] != u8::MAX && dist[k][j] != u8::MAX {
251 dist[i][j] = dist[i][j].min(dist[i][k].saturating_add(dist[k][j]));
252 }
253 }
254 }
255 }
256
257 let max_dist = dist
259 .iter()
260 .flat_map(|row| row.iter())
261 .filter(|&&d| d != u8::MAX)
262 .max()
263 .copied();
264
265 max_dist
266 }
267
268 pub fn export_json(&self) -> Result<String> {
270 let topology = self.get_topology();
271 serde_json::to_string_pretty(&topology)
272 .map_err(|e| Error::Other(format!("Failed to serialize topology: {}", e)))
273 }
274
275 pub fn get_message_stats(&self) -> Vec<MessageStats> {
277 self.message_history.clone()
278 }
279
280 pub fn delivery_success_rate(&self) -> f32 {
282 if self.message_history.is_empty() {
283 return 0.0;
284 }
285
286 let delivered = self
287 .message_history
288 .iter()
289 .filter(|m| m.delivered_at.is_some())
290 .count();
291
292 delivered as f32 / self.message_history.len() as f32
293 }
294
295 pub fn median_latency(&self) -> Option<u64> {
297 let mut latencies: Vec<_> = self
298 .message_history
299 .iter()
300 .filter_map(|m| m.latency_ms)
301 .collect();
302
303 if latencies.is_empty() {
304 return None;
305 }
306
307 latencies.sort_unstable();
308 Some(latencies[latencies.len() / 2])
309 }
310
311 pub fn median_hops(&self) -> Option<u8> {
313 let mut hops: Vec<_> = self
314 .message_history
315 .iter()
316 .filter(|m| m.delivered_at.is_some())
317 .map(|m| m.hop_count)
318 .collect();
319
320 if hops.is_empty() {
321 return None;
322 }
323
324 hops.sort_unstable();
325 Some(hops[hops.len() / 2])
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_visualizer_creation() {
335 let viz = NetworkVisualizer::new("alice");
336 let topology = viz.get_topology();
337 assert_eq!(topology.local_user_id, "alice");
338 assert_eq!(topology.nodes.len(), 0);
339 }
340
341 #[test]
342 fn test_add_node() {
343 let mut viz = NetworkVisualizer::new("alice");
344
345 let node = NetworkNode {
346 user_id: "bob".to_string(),
347 role: NodeRole::Normal,
348 connection_count: 1,
349 battery_level: Some(80),
350 last_seen: 0,
351 transports: vec![TransportType::BLE],
352 };
353
354 viz.update_node(node);
355
356 let topology = viz.get_topology();
357 assert_eq!(topology.nodes.len(), 1);
358 assert_eq!(topology.nodes[0].user_id, "bob");
359 }
360
361 #[test]
362 fn test_add_link() {
363 let mut viz = NetworkVisualizer::new("alice");
364
365 let link = NetworkLink {
366 from: "alice".to_string(),
367 to: "bob".to_string(),
368 quality: 0.9,
369 transport: TransportType::BLE,
370 rssi: Some(-50),
371 };
372
373 viz.add_link(link);
374
375 let topology = viz.get_topology();
376 assert_eq!(topology.links.len(), 1);
377 }
378
379 #[test]
380 fn test_network_stats() {
381 let mut viz = NetworkVisualizer::new("alice");
382
383 for i in 0..5 {
385 viz.update_node(NetworkNode {
386 user_id: format!("node{}", i),
387 role: if i < 2 {
388 NodeRole::Relay
389 } else {
390 NodeRole::Normal
391 },
392 connection_count: 2,
393 battery_level: Some(75),
394 last_seen: 0,
395 transports: vec![TransportType::BLE],
396 });
397 }
398
399 viz.add_link(NetworkLink {
401 from: "node0".to_string(),
402 to: "node1".to_string(),
403 quality: 0.8,
404 transport: TransportType::BLE,
405 rssi: Some(-60),
406 });
407
408 let topology = viz.get_topology();
409 assert_eq!(topology.stats.total_nodes, 5);
410 assert_eq!(topology.stats.relay_nodes, 2);
411 assert_eq!(topology.stats.total_connections, 1);
412 }
413
414 #[test]
415 fn test_message_stats() {
416 let mut viz = NetworkVisualizer::new("alice");
417
418 viz.record_message(MessageStats {
419 message_id: "msg1".to_string(),
420 sender: "alice".to_string(),
421 recipient: "bob".to_string(),
422 sent_at: 0,
423 delivered_at: Some(100),
424 hop_count: 3,
425 transport: Some(TransportType::BLE),
426 retry_count: 0,
427 latency_ms: Some(100),
428 });
429
430 viz.record_message(MessageStats {
431 message_id: "msg2".to_string(),
432 sender: "alice".to_string(),
433 recipient: "charlie".to_string(),
434 sent_at: 100,
435 delivered_at: None,
436 hop_count: 0,
437 transport: None,
438 retry_count: 3,
439 latency_ms: None,
440 });
441
442 assert_eq!(viz.delivery_success_rate(), 0.5);
443 assert_eq!(viz.median_latency(), Some(100));
444 assert_eq!(viz.median_hops(), Some(3));
445 }
446
447 #[test]
448 fn test_export_json() {
449 let mut viz = NetworkVisualizer::new("alice");
450
451 viz.update_node(NetworkNode {
452 user_id: "bob".to_string(),
453 role: NodeRole::Relay,
454 connection_count: 3,
455 battery_level: Some(90),
456 last_seen: 0,
457 transports: vec![TransportType::BLE, TransportType::WiFiDirect],
458 });
459
460 let json = viz.export_json().unwrap();
461 assert!(json.contains("bob"));
462 assert!(json.contains("relay"));
463 }
464}