Skip to main content

offline_protocol_router/
router.rs

1//! Path selection and routing for optimal message delivery.
2//!
3//! This module implements gossip-based probabilistic forwarding to prevent
4//! broadcast storms in large networks. The forwarding probability adapts
5//! based on the number of visible peers to maintain constant message overhead.
6
7use crate::constants::*;
8use crate::relay::{RelayInfo, RelayManager, RelayRole, RelayTransition};
9use offline_protocol_core::Message;
10use std::collections::hash_map::DefaultHasher;
11use std::collections::HashMap;
12use std::hash::{Hash, Hasher};
13use std::time::{Duration, Instant};
14
15/// Information about a neighboring device.
16#[derive(Debug, Clone)]
17pub struct NeighborInfo {
18    /// Unique identifier for this neighbor.
19    pub peer_id: String,
20
21    /// RSSI signal strength (dBm).
22    pub rssi: i16,
23
24    /// Number of hops to destination through this neighbor (if known).
25    pub hops_to_destination: Option<u8>,
26
27    /// Link quality to this neighbor (0-100).
28    pub link_quality: u8,
29
30    /// Relay information if this neighbor is a relay.
31    pub relay_info: Option<RelayInfo>,
32}
33
34/// Gossip forwarding configuration for probabilistic message propagation.
35#[derive(Debug, Clone)]
36pub struct GossipConfig {
37    /// Enable probabilistic (gossip) forwarding to prevent broadcast storms.
38    pub enabled: bool,
39
40    /// Target number of peers to forward to in large networks.
41    /// The actual forwarding probability is computed as: min(1.0, target_fanout / visible_peers)
42    pub target_fanout: usize,
43
44    /// Minimum forwarding probability (0.0-1.0). Ensures messages still propagate
45    /// even in very dense networks.
46    pub min_probability: f32,
47
48    /// Peer count threshold below which we always forward (no probabilistic dropping).
49    /// Below this threshold, the network is small enough to handle full flooding.
50    pub small_network_threshold: usize,
51
52    /// High-priority messages bypass probabilistic dropping.
53    pub priority_bypass: bool,
54}
55
56impl Default for GossipConfig {
57    fn default() -> Self {
58        Self {
59            enabled: true,
60            target_fanout: 4,
61            min_probability: 0.15,
62            small_network_threshold: 10,
63            priority_bypass: true,
64        }
65    }
66}
67
68/// Configuration for gradient routing.
69#[derive(Debug, Clone)]
70pub struct GradientRoutingConfig {
71    /// Enable gradient routing for directed message delivery.
72    pub enabled: bool,
73    /// Maximum entries in the routing table per destination.
74    pub max_routes_per_destination: usize,
75    /// Time to live for routing entries (seconds).
76    pub route_ttl_secs: u64,
77    /// Maximum total routing table entries.
78    pub max_routing_table_size: usize,
79}
80
81impl Default for GradientRoutingConfig {
82    fn default() -> Self {
83        Self {
84            enabled: true,
85            max_routes_per_destination: 3,
86            route_ttl_secs: 300, // 5 minutes
87            max_routing_table_size: 1000,
88        }
89    }
90}
91
92/// A routing entry tracking how to reach a destination.
93/// Uses a destination sequence number (DSDV-style) to avoid loops: newer updates
94/// from the destination are preferred; stale routes are ignored.
95#[derive(Debug, Clone)]
96pub struct RouteEntry {
97    /// The next-hop neighbor to reach this destination.
98    pub next_hop: String,
99    /// Hop count to destination through this route.
100    pub hop_count: u8,
101    /// When this route was last confirmed.
102    pub last_seen: Instant,
103    /// Quality score of this route (higher is better).
104    pub quality: f32,
105    /// Destination sequence number from the destination node (DSDV).
106    /// Higher values are fresher; prefer route with higher sequence to avoid count-to-infinity.
107    pub sequence_number: u32,
108}
109
110/// Gradient routing table for directed message delivery.
111/// Learns routes from incoming messages and uses them for replies.
112#[derive(Debug)]
113pub struct GradientRoutingTable {
114    config: GradientRoutingConfig,
115    /// Routes indexed by destination user ID.
116    routes: HashMap<String, Vec<RouteEntry>>,
117    /// Reverse mapping: which destinations can be reached through which neighbor.
118    neighbor_destinations: HashMap<String, Vec<String>>,
119}
120
121impl GradientRoutingTable {
122    /// Creates a new gradient routing table.
123    pub fn new() -> Self {
124        Self::with_config(GradientRoutingConfig::default())
125    }
126
127    /// Creates a new gradient routing table with custom configuration.
128    pub fn with_config(config: GradientRoutingConfig) -> Self {
129        Self {
130            config,
131            routes: HashMap::new(),
132            neighbor_destinations: HashMap::new(),
133        }
134    }
135
136    /// Records a route learned from an incoming message (DSDV-style).
137    /// Prefers higher `sequence_number` (fresher from destination) to avoid loops.
138    /// Pass 0 for sequence when the message does not carry a destination sequence.
139    pub fn learn_route(
140        &mut self,
141        destination: &str,
142        next_hop: &str,
143        hop_count: u8,
144        quality: f32,
145        sequence_number: u32,
146    ) {
147        if !self.config.enabled {
148            return;
149        }
150
151        let now = Instant::now();
152
153        // Get or create route list for this destination
154        let routes = self.routes.entry(destination.to_string()).or_default();
155
156        // Check if we already have a route through this neighbor
157        if let Some(existing) = routes.iter_mut().find(|r| r.next_hop == next_hop) {
158            // Accept update only if fresher (higher seq, with wrapping) or same seq with better hop count
159            let accept = seq_is_newer(sequence_number, existing.sequence_number)
160                || (sequence_number == existing.sequence_number && hop_count <= existing.hop_count);
161            if accept {
162                existing.hop_count = hop_count;
163                existing.last_seen = now;
164                existing.quality = quality;
165                existing.sequence_number = sequence_number;
166            }
167            return;
168        }
169
170        // New route through this neighbor
171        if routes.len() >= self.config.max_routes_per_destination {
172            // Sort best-first so worst is last; pop() removes worst (lowest seq, lowest quality, highest hop_count)
173            routes.sort_by(|a, b| {
174                seq_cmp(b.sequence_number, a.sequence_number)
175                    .then_with(|| {
176                        b.quality
177                            .partial_cmp(&a.quality)
178                            .unwrap_or(std::cmp::Ordering::Equal)
179                    })
180                    .then_with(|| a.hop_count.cmp(&b.hop_count))
181            });
182            if let Some(evicted) = routes.pop() {
183                // Remove evicted route from reverse mapping
184                if let Some(dests) = self.neighbor_destinations.get_mut(&evicted.next_hop) {
185                    dests.retain(|d| d != destination);
186                    if dests.is_empty() {
187                        self.neighbor_destinations.remove(&evicted.next_hop);
188                    }
189                }
190            }
191        }
192
193        routes.push(RouteEntry {
194            next_hop: next_hop.to_string(),
195            hop_count,
196            last_seen: now,
197            quality,
198            sequence_number,
199        });
200
201        // Update reverse mapping (only add if not already present)
202        let dests = self
203            .neighbor_destinations
204            .entry(next_hop.to_string())
205            .or_default();
206        if !dests.contains(&destination.to_string()) {
207            dests.push(destination.to_string());
208        }
209
210        // Enforce table size limit
211        self.enforce_size_limit();
212    }
213
214    /// Gets the best route to a destination, if known.
215    pub fn get_route(&self, destination: &str) -> Option<&RouteEntry> {
216        if !self.config.enabled {
217            return None;
218        }
219
220        let routes = self.routes.get(destination)?;
221        let ttl = Duration::from_secs(self.config.route_ttl_secs);
222        let now = Instant::now();
223
224        // Find best non-expired route: prefer higher sequence (fresher, wrapping-aware),
225        // then quality, then lower hop_count
226        routes
227            .iter()
228            .filter(|r| now.duration_since(r.last_seen) < ttl)
229            .max_by(|a, b| {
230                seq_cmp(a.sequence_number, b.sequence_number)
231                    .then_with(|| {
232                        a.quality
233                            .partial_cmp(&b.quality)
234                            .unwrap_or(std::cmp::Ordering::Equal)
235                    })
236                    .then_with(|| b.hop_count.cmp(&a.hop_count))
237            })
238    }
239
240    /// Gets all valid routes to a destination.
241    pub fn get_routes(&self, destination: &str) -> Vec<&RouteEntry> {
242        if !self.config.enabled {
243            return Vec::new();
244        }
245
246        let Some(routes) = self.routes.get(destination) else {
247            return Vec::new();
248        };
249
250        let ttl = Duration::from_secs(self.config.route_ttl_secs);
251        let now = Instant::now();
252
253        routes
254            .iter()
255            .filter(|r| now.duration_since(r.last_seen) < ttl)
256            .collect()
257    }
258
259    /// Checks if we have a known route to a destination.
260    pub fn has_route(&self, destination: &str) -> bool {
261        self.get_route(destination).is_some()
262    }
263
264    /// Removes a neighbor from the routing table (e.g., on disconnect).
265    pub fn remove_neighbor(&mut self, neighbor: &str) {
266        // Remove all routes through this neighbor
267        for routes in self.routes.values_mut() {
268            routes.retain(|r| r.next_hop != neighbor);
269        }
270
271        // Remove empty entries
272        self.routes.retain(|_, v| !v.is_empty());
273
274        // Update reverse mapping
275        self.neighbor_destinations.remove(neighbor);
276    }
277
278    /// Cleans up expired routes.
279    pub fn cleanup_expired(&mut self) {
280        let ttl = Duration::from_secs(self.config.route_ttl_secs);
281        let now = Instant::now();
282
283        for routes in self.routes.values_mut() {
284            routes.retain(|r| now.duration_since(r.last_seen) < ttl);
285        }
286
287        self.routes.retain(|_, v| !v.is_empty());
288
289        // Rebuild reverse mapping
290        self.neighbor_destinations.clear();
291        for (dest, routes) in &self.routes {
292            for route in routes {
293                self.neighbor_destinations
294                    .entry(route.next_hop.clone())
295                    .or_default()
296                    .push(dest.clone());
297            }
298        }
299    }
300
301    /// Returns the number of known destinations.
302    pub fn destination_count(&self) -> usize {
303        self.routes.len()
304    }
305
306    /// Returns the total number of routes.
307    pub fn route_count(&self) -> usize {
308        self.routes.values().map(|v| v.len()).sum()
309    }
310
311    /// Enforces the maximum routing table size.
312    ///
313    /// Evicts routes with the lowest composite score (quality * recency_factor)
314    /// so that high-quality, recently-seen routes survive over stale/low-quality ones.
315    fn enforce_size_limit(&mut self) {
316        let route_ttl_secs = self.config.route_ttl_secs as f64;
317        while self.route_count() > self.config.max_routing_table_size {
318            // Find route with the lowest composite score
319            let worst = self
320                .routes
321                .iter()
322                .flat_map(|(dest, routes)| routes.iter().map(move |r| (dest.clone(), r)))
323                .min_by(|(_, a), (_, b)| {
324                    let score_a = composite_eviction_score(a, route_ttl_secs);
325                    let score_b = composite_eviction_score(b, route_ttl_secs);
326                    score_a
327                        .partial_cmp(&score_b)
328                        .unwrap_or(std::cmp::Ordering::Equal)
329                });
330
331            if let Some((dest, route)) = worst {
332                let next_hop = route.next_hop.clone();
333                if let Some(routes) = self.routes.get_mut(&dest) {
334                    routes.retain(|r| r.next_hop != next_hop);
335                    if routes.is_empty() {
336                        self.routes.remove(&dest);
337                    }
338                }
339                // Update reverse mapping
340                if let Some(dests) = self.neighbor_destinations.get_mut(&next_hop) {
341                    dests.retain(|d| d != &dest);
342                    if dests.is_empty() {
343                        self.neighbor_destinations.remove(&next_hop);
344                    }
345                }
346            } else {
347                break;
348            }
349        }
350    }
351}
352
353/// RFC 1982 serial number arithmetic for 32-bit sequence numbers.
354/// Returns `true` if `new` is strictly newer than `old`, handling wrapping.
355fn seq_is_newer(new: u32, old: u32) -> bool {
356    // When new == old, not newer. Otherwise, check sign of difference.
357    new != old && new.wrapping_sub(old) < (1u32 << 31)
358}
359
360/// Wrapping-aware comparison of two sequence numbers.
361/// Returns `Ordering::Greater` if `a` is newer, `Less` if `b` is newer,
362/// `Equal` if they are the same.
363///
364/// **Note on total order:** When `a` and `b` are exactly `1 << 31` apart,
365/// RFC 1982 declares the relationship *undefined*. This function maps the
366/// ambiguous case to `Less`, which means `seq_cmp(a, b)` and `seq_cmp(b, a)`
367/// can both return `Less`. In practice the half-space boundary is unreachable
368/// (it requires two live sequence numbers that differ by 2 billion), and the
369/// `then_with` tie-breakers on quality/hop_count ensure stable sort results
370/// even if it were hit.
371fn seq_cmp(a: u32, b: u32) -> std::cmp::Ordering {
372    if a == b {
373        std::cmp::Ordering::Equal
374    } else if seq_is_newer(a, b) {
375        std::cmp::Ordering::Greater
376    } else {
377        std::cmp::Ordering::Less
378    }
379}
380
381/// Computes a composite score for eviction: quality * recency_factor.
382/// Lower score = more likely to be evicted.
383fn composite_eviction_score(route: &RouteEntry, route_ttl_secs: f64) -> f64 {
384    let age_secs = route.last_seen.elapsed().as_secs_f64();
385    let recency_factor = 1.0 - (age_secs / route_ttl_secs).min(1.0);
386    route.quality as f64 * recency_factor
387}
388
389impl Default for GradientRoutingTable {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395/// Path selection configuration.
396#[derive(Debug, Clone)]
397pub struct PathConfig {
398    /// Number of top relays to forward to (for redundancy).
399    pub forward_to_top_k: usize,
400
401    /// Maximum acceptable congestion level (0.0-1.0).
402    pub max_congestion_level: f32,
403
404    /// Gossip forwarding configuration for scalable message propagation.
405    pub gossip: GossipConfig,
406
407    /// Gradient routing configuration for directed message delivery.
408    pub gradient_routing: GradientRoutingConfig,
409}
410
411impl Default for PathConfig {
412    fn default() -> Self {
413        Self {
414            forward_to_top_k: DEFAULT_FORWARD_TO_TOP_K,
415            max_congestion_level: DEFAULT_MAX_CONGESTION_LEVEL,
416            gossip: GossipConfig::default(),
417            gradient_routing: GradientRoutingConfig::default(),
418        }
419    }
420}
421
422/// Path score breakdown for debugging/monitoring.
423#[derive(Debug, Clone)]
424pub struct PathScore {
425    /// Signal strength component (0-100).
426    pub signal: f32,
427
428    /// Proximity/hops component (0-100).
429    pub proximity: f32,
430
431    /// Capacity component (0-100).
432    pub capacity: f32,
433
434    /// Energy component (0-100).
435    pub energy: f32,
436
437    /// Total weighted score.
438    pub total: f32,
439}
440
441/// Forwarding decision result with reasoning.
442#[derive(Debug, Clone)]
443pub struct ForwardingDecision {
444    /// Peers to forward the message to.
445    pub peers: Vec<String>,
446    /// Whether gossip probabilistic forwarding was applied.
447    pub gossip_applied: bool,
448    /// The computed forwarding probability (1.0 if gossip not applied).
449    pub probability: f32,
450    /// Number of peers that were probabilistically dropped.
451    pub dropped_count: usize,
452}
453
454/// Path selector for optimal relay selection.
455pub struct PathSelector {
456    config: PathConfig,
457    relay_manager: RelayManager,
458    /// Local device ID for deterministic randomness.
459    local_device_id: String,
460    /// Gradient routing table for directed message delivery.
461    routing_table: GradientRoutingTable,
462}
463
464impl PathSelector {
465    /// Creates a new path selector with default configuration.
466    pub fn new() -> Self {
467        Self::with_config(PathConfig::default(), RelayManager::new())
468    }
469
470    /// Creates a new path selector with custom configuration.
471    pub fn with_config(config: PathConfig, relay_manager: RelayManager) -> Self {
472        let routing_table = GradientRoutingTable::with_config(config.gradient_routing.clone());
473        Self {
474            config,
475            relay_manager,
476            local_device_id: String::new(),
477            routing_table,
478        }
479    }
480
481    /// Sets the local device ID for deterministic gossip decisions.
482    pub fn set_local_device_id(&mut self, device_id: impl Into<String>) {
483        self.local_device_id = device_id.into();
484    }
485
486    /// Returns the current [`RelayRole`] from the internal [`RelayManager`].
487    ///
488    /// Exposed for read-only callers (e.g., the telemetry aggregator that
489    /// needs to know the current relay role without widening the surface
490    /// to the full [`RelayManager`] API).
491    pub fn current_relay_role(&self) -> RelayRole {
492        self.relay_manager.current_role()
493    }
494
495    /// Re-evaluates the local relay role against current connectivity and
496    /// battery, applying any change to the internal [`RelayManager`].
497    ///
498    /// Returns `Some(transition)` only when the role actually changes, so the
499    /// caller can emit a role-transition event exactly once per transition.
500    pub fn evaluate_relay_transition(
501        &mut self,
502        connection_count: usize,
503        battery_level: u8,
504        is_charging: bool,
505    ) -> Option<RelayTransition> {
506        self.relay_manager
507            .evaluate_transition(connection_count, battery_level, is_charging)
508    }
509
510    /// Computes the forwarding probability based on visible peer count.
511    ///
512    /// Uses the formula: min(1.0, max(min_probability, target_fanout / peer_count))
513    /// This ensures constant expected message overhead regardless of network size.
514    pub fn compute_forwarding_probability(&self, visible_peer_count: usize) -> f32 {
515        let gossip = &self.config.gossip;
516
517        if !gossip.enabled || visible_peer_count <= gossip.small_network_threshold {
518            return 1.0;
519        }
520
521        let raw_probability = gossip.target_fanout as f32 / visible_peer_count as f32;
522        raw_probability.max(gossip.min_probability).min(1.0)
523    }
524
525    /// Determines if a message should be forwarded to a specific peer using
526    /// deterministic pseudo-random selection based on message ID and peer ID.
527    ///
528    /// This ensures that the same message-peer pair always produces the same
529    /// decision across all nodes, preventing duplicate forwarding.
530    fn should_forward_to_peer(&self, message_id: &str, peer_id: &str, probability: f32) -> bool {
531        if probability >= 1.0 {
532            return true;
533        }
534        if probability <= 0.0 {
535            return false;
536        }
537
538        // Create deterministic hash from message ID, peer ID, and local device ID
539        let mut hasher = DefaultHasher::new();
540        message_id.hash(&mut hasher);
541        peer_id.hash(&mut hasher);
542        self.local_device_id.hash(&mut hasher);
543        let hash = hasher.finish();
544
545        // Convert hash to probability (0.0 to 1.0)
546        let hash_probability = (hash as f64 / u64::MAX as f64) as f32;
547
548        hash_probability < probability
549    }
550
551    /// Selects the best path(s) for message delivery with gossip-based filtering.
552    ///
553    /// # Arguments
554    ///
555    /// * `message` - The message to route
556    /// * `neighbors` - Available neighboring devices
557    /// * `total_visible_peers` - Total number of peers visible in the network
558    ///   (may be larger than neighbors if some peers are not connected)
559    ///
560    /// # Returns
561    ///
562    /// Returns a forwarding decision with selected peers and metadata.
563    pub fn select_paths_with_gossip(
564        &self,
565        message: &Message,
566        neighbors: &[NeighborInfo],
567        total_visible_peers: usize,
568    ) -> ForwardingDecision {
569        if neighbors.is_empty() {
570            return ForwardingDecision {
571                peers: Vec::new(),
572                gossip_applied: false,
573                probability: 1.0,
574                dropped_count: 0,
575            };
576        }
577
578        // Check if high-priority message should bypass gossip
579        let bypass_gossip = self.config.gossip.priority_bypass
580            && matches!(
581                message.priority,
582                offline_protocol_core::MessagePriority::High
583                    | offline_protocol_core::MessagePriority::Critical
584            );
585
586        // Compute forwarding probability
587        let probability = if bypass_gossip {
588            1.0
589        } else {
590            self.compute_forwarding_probability(total_visible_peers.max(neighbors.len()))
591        };
592
593        let gossip_applied = probability < 1.0;
594
595        // Calculate scores for each neighbor
596        let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
597            .iter()
598            .map(|neighbor| {
599                let score = self.calculate_path_score(message, neighbor);
600                (neighbor.peer_id.clone(), score)
601            })
602            .collect();
603
604        // Filter out neighbors with relay congestion > max threshold
605        scored_neighbors.retain(|(peer_id, _)| {
606            if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
607                if let Some(relay_info) = &neighbor.relay_info {
608                    return relay_info.congestion_level <= self.config.max_congestion_level;
609                }
610            }
611            true
612        });
613
614        // Sort by total score (descending)
615        scored_neighbors.sort_by(|a, b| {
616            b.1.total
617                .partial_cmp(&a.1.total)
618                .unwrap_or(std::cmp::Ordering::Equal)
619        });
620
621        // Take top K candidates
622        let candidates: Vec<String> = scored_neighbors
623            .into_iter()
624            .take(self.config.forward_to_top_k)
625            .map(|(peer_id, _)| peer_id)
626            .collect();
627
628        // Apply probabilistic filtering if gossip is active
629        let initial_count = candidates.len();
630        let message_id = message.id.as_str();
631        let peers: Vec<String> = if gossip_applied {
632            candidates
633                .into_iter()
634                .filter(|peer_id| {
635                    self.should_forward_to_peer(&message_id, peer_id.as_str(), probability)
636                })
637                .collect()
638        } else {
639            candidates
640        };
641
642        let dropped_count = initial_count.saturating_sub(peers.len());
643
644        ForwardingDecision {
645            peers,
646            gossip_applied,
647            probability,
648            dropped_count,
649        }
650    }
651
652    /// Selects the best path(s) for message delivery.
653    ///
654    /// # Arguments
655    ///
656    /// * `message` - The message to route
657    /// * `neighbors` - Available neighboring devices
658    ///
659    /// # Returns
660    ///
661    /// Returns a list of neighbors to forward the message to, ordered by preference.
662    ///
663    /// Note: This method does not apply gossip filtering. Use `select_paths_with_gossip`
664    /// for scalable routing in large networks.
665    pub fn select_paths(&self, message: &Message, neighbors: &[NeighborInfo]) -> Vec<String> {
666        if neighbors.is_empty() {
667            return Vec::new();
668        }
669
670        // Calculate scores for each neighbor
671        let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
672            .iter()
673            .map(|neighbor| {
674                let score = self.calculate_path_score(message, neighbor);
675                (neighbor.peer_id.clone(), score)
676            })
677            .collect();
678
679        // Filter out neighbors with relay congestion > max threshold
680        scored_neighbors.retain(|(peer_id, _)| {
681            if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
682                if let Some(relay_info) = &neighbor.relay_info {
683                    return relay_info.congestion_level <= self.config.max_congestion_level;
684                }
685            }
686            true
687        });
688
689        // Sort by total score (descending)
690        scored_neighbors.sort_by(|a, b| {
691            b.1.total
692                .partial_cmp(&a.1.total)
693                .unwrap_or(std::cmp::Ordering::Equal)
694        });
695
696        // Select top K neighbors for redundancy
697        scored_neighbors
698            .into_iter()
699            .take(self.config.forward_to_top_k)
700            .map(|(peer_id, _)| peer_id)
701            .collect()
702    }
703
704    /// Calculates the path score for a neighbor.
705    fn calculate_path_score(&self, message: &Message, neighbor: &NeighborInfo) -> PathScore {
706        let signal_score = self.calculate_signal_score(neighbor);
707        let proximity_score = self.calculate_proximity_score(message, neighbor);
708        let capacity_score = self.calculate_capacity_score(neighbor);
709        let energy_score = self.calculate_energy_score(neighbor);
710
711        let total = (signal_score * SIGNAL_WEIGHT)
712            + (proximity_score * PROXIMITY_WEIGHT)
713            + (capacity_score * CAPACITY_WEIGHT)
714            + (energy_score * ENERGY_WEIGHT);
715
716        PathScore {
717            signal: signal_score,
718            proximity: proximity_score,
719            capacity: capacity_score,
720            energy: energy_score,
721            total,
722        }
723    }
724
725    /// Calculates signal strength score from RSSI.
726    fn calculate_signal_score(&self, neighbor: &NeighborInfo) -> f32 {
727        let rssi = neighbor.rssi;
728
729        if rssi >= EXCELLENT_RSSI_THRESHOLD {
730            EXCELLENT_SIGNAL_SCORE
731        } else if rssi >= GOOD_RSSI_THRESHOLD {
732            GOOD_SIGNAL_BASE
733                + ((rssi - GOOD_RSSI_THRESHOLD) as f32 * 30.0
734                    / (EXCELLENT_RSSI_THRESHOLD - GOOD_RSSI_THRESHOLD) as f32)
735        } else if rssi >= FAIR_RSSI_THRESHOLD {
736            FAIR_SIGNAL_BASE
737                + ((rssi - FAIR_RSSI_THRESHOLD) as f32 * 30.0
738                    / (GOOD_RSSI_THRESHOLD - FAIR_RSSI_THRESHOLD) as f32)
739        } else {
740            ((rssi - POOR_RSSI_MAX).max(0) as f32 * FAIR_SIGNAL_BASE
741                / (FAIR_RSSI_THRESHOLD - POOR_RSSI_MAX) as f32)
742                .max(0.0)
743        }
744    }
745
746    /// Calculates proximity score based on hop distance.
747    fn calculate_proximity_score(&self, message: &Message, neighbor: &NeighborInfo) -> f32 {
748        if let Some(hops) = neighbor.hops_to_destination {
749            // We know the distance to destination through this neighbor
750            let remaining_ttl = message.ttl.value();
751
752            if hops == 0 {
753                // Direct connection to destination
754                100.0
755            } else {
756                // Score based on whether we have enough TTL
757                let score = if hops < remaining_ttl {
758                    100.0 - (hops as f32 / remaining_ttl as f32 * 50.0)
759                } else {
760                    // Not enough TTL, but still possible
761                    20.0
762                };
763                score.max(0.0)
764            }
765        } else {
766            // Unknown distance, use link quality as proxy
767            neighbor.link_quality as f32
768        }
769    }
770
771    /// Calculates capacity score based on relay status and congestion.
772    fn calculate_capacity_score(&self, neighbor: &NeighborInfo) -> f32 {
773        if let Some(relay_info) = &neighbor.relay_info {
774            let relay_score = self.relay_manager.calculate_relay_score(relay_info);
775            relay_score.min(EXCELLENT_SIGNAL_SCORE)
776        } else {
777            NON_RELAY_BASIC_CAPACITY
778        }
779    }
780
781    /// Calculates energy score based on battery level.
782    fn calculate_energy_score(&self, neighbor: &NeighborInfo) -> f32 {
783        if let Some(relay_info) = &neighbor.relay_info {
784            if relay_info.is_charging {
785                CHARGING_BATTERY_SCORE
786            } else {
787                relay_info.battery_level as f32
788            }
789        } else {
790            NON_RELAY_ASSUMED_BATTERY
791        }
792    }
793
794    /// Finds the best single path (for unicast).
795    pub fn select_best_path(
796        &self,
797        message: &Message,
798        neighbors: &[NeighborInfo],
799    ) -> Option<String> {
800        self.select_paths(message, neighbors).into_iter().next()
801    }
802
803    /// Checks if routing around congestion is needed.
804    pub fn should_route_around_congestion(&self, neighbor: &NeighborInfo) -> bool {
805        if let Some(relay_info) = &neighbor.relay_info {
806            relay_info.congestion_level > self.config.max_congestion_level
807        } else {
808            false
809        }
810    }
811
812    /// Gets the path configuration.
813    pub fn config(&self) -> &PathConfig {
814        &self.config
815    }
816
817    // MARK: - Gradient Routing Methods
818
819    /// Learns a route from an incoming message.
820    /// Call this when receiving a message from a neighbor to learn
821    /// that the neighbor can reach the message's sender.
822    pub fn learn_route_from_message(
823        &mut self,
824        message: &Message,
825        from_neighbor: &str,
826        quality: f32,
827    ) {
828        let sender = message.sender.as_str();
829        let hop_count = message.hop_count.value();
830        self.routing_table.learn_route(
831            sender,
832            from_neighbor,
833            hop_count,
834            quality,
835            0, // No destination sequence in message yet; use 0 for backward compatibility
836        );
837    }
838
839    /// Gets the best route to a destination, if known.
840    pub fn get_route_to(&self, destination: &str) -> Option<&RouteEntry> {
841        self.routing_table.get_route(destination)
842    }
843
844    /// Checks if we have a known route to a destination.
845    pub fn has_route_to(&self, destination: &str) -> bool {
846        self.routing_table.has_route(destination)
847    }
848
849    /// Selects the best path for a message using gradient routing when available.
850    /// Falls back to flooding if no route is known.
851    pub fn select_directed_path(
852        &self,
853        message: &Message,
854        neighbors: &[NeighborInfo],
855    ) -> Option<String> {
856        let recipient = message.recipient.as_str();
857
858        // Check if any neighbor IS the recipient
859        for neighbor in neighbors {
860            if neighbor.peer_id == recipient {
861                return Some(neighbor.peer_id.clone());
862            }
863        }
864
865        // Check if we have a learned route
866        if let Some(route) = self.routing_table.get_route(recipient) {
867            // Verify the next hop is in our current neighbors
868            if neighbors.iter().any(|n| n.peer_id == route.next_hop) {
869                return Some(route.next_hop.clone());
870            }
871        }
872
873        // No direct route known, fall back to best path selection
874        self.select_best_path(message, neighbors)
875    }
876
877    /// Removes a neighbor from the routing table (e.g., on disconnect).
878    pub fn remove_neighbor_routes(&mut self, neighbor: &str) {
879        self.routing_table.remove_neighbor(neighbor);
880    }
881
882    /// Cleans up expired routes.
883    pub fn cleanup_routes(&mut self) {
884        self.routing_table.cleanup_expired();
885    }
886
887    /// Returns routing table statistics.
888    pub fn routing_stats(&self) -> (usize, usize) {
889        (
890            self.routing_table.destination_count(),
891            self.routing_table.route_count(),
892        )
893    }
894
895    /// Gets a mutable reference to the routing table for advanced operations.
896    pub fn routing_table_mut(&mut self) -> &mut GradientRoutingTable {
897        &mut self.routing_table
898    }
899}
900
901impl Default for PathSelector {
902    fn default() -> Self {
903        Self::new()
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use super::*;
910    use crate::relay::{RelayInfo, RelayRole};
911    use offline_protocol_core::{AppId, UserId};
912
913    fn create_test_message() -> Message {
914        Message::new(
915            UserId::new("alice").unwrap(),
916            UserId::new("bob").unwrap(),
917            AppId::new("test").unwrap(),
918            "Test message",
919        )
920    }
921
922    fn create_neighbor(id: &str, rssi: i16, hops: Option<u8>, congestion: f32) -> NeighborInfo {
923        NeighborInfo {
924            peer_id: id.to_string(),
925            rssi,
926            hops_to_destination: hops,
927            link_quality: 80,
928            relay_info: Some(RelayInfo {
929                connection_count: 5,
930                battery_level: 70,
931                is_charging: false,
932                role: RelayRole::Relay,
933                link_quality: 80,
934                queue_depth: 10,
935                congestion_level: congestion,
936            }),
937        }
938    }
939
940    #[test]
941    fn test_path_selection_basic() {
942        let selector = PathSelector::new();
943        let message = create_test_message();
944
945        let neighbors = vec![
946            create_neighbor("peer1", -60, Some(2), 0.2),
947            create_neighbor("peer2", -70, Some(3), 0.3),
948            create_neighbor("peer3", -55, Some(1), 0.1),
949        ];
950
951        let paths = selector.select_paths(&message, &neighbors);
952
953        // Should select top K paths
954        assert!(!paths.is_empty());
955        assert!(paths.len() <= 3);
956
957        // Best path should be peer3 (best RSSI, lowest hops, lowest congestion)
958        assert_eq!(paths[0], "peer3");
959    }
960
961    #[test]
962    fn test_congestion_filtering() {
963        let selector = PathSelector::new();
964        let message = create_test_message();
965
966        let neighbors = vec![
967            create_neighbor("peer1", -60, Some(2), 0.9), // High congestion
968            create_neighbor("peer2", -65, Some(3), 0.3),
969        ];
970
971        let paths = selector.select_paths(&message, &neighbors);
972
973        // Should filter out peer1 due to high congestion
974        assert!(!paths.contains(&"peer1".to_string()));
975    }
976
977    #[test]
978    fn test_direct_destination_preferred() {
979        let selector = PathSelector::new();
980        let message = create_test_message();
981
982        let neighbors = vec![
983            create_neighbor("peer1", -70, Some(5), 0.3),
984            create_neighbor("peer2", -70, Some(0), 0.1), // Direct to destination
985        ];
986
987        let paths = selector.select_paths(&message, &neighbors);
988
989        // Should prefer peer2 (direct destination) with same signal
990        assert_eq!(paths[0], "peer2");
991    }
992
993    #[test]
994    fn test_signal_score_calculation() {
995        let selector = PathSelector::new();
996
997        let excellent = create_neighbor("peer1", -40, None, 0.1);
998        let score = selector.calculate_signal_score(&excellent);
999        assert_eq!(score, 100.0);
1000
1001        let poor = create_neighbor("peer2", -90, None, 0.1);
1002        let score = selector.calculate_signal_score(&poor);
1003        assert!(score < 40.0);
1004    }
1005
1006    #[test]
1007    fn test_empty_neighbors() {
1008        let selector = PathSelector::new();
1009        let message = create_test_message();
1010
1011        let paths = selector.select_paths(&message, &[]);
1012        assert!(paths.is_empty());
1013    }
1014
1015    #[test]
1016    fn test_select_best_path() {
1017        let selector = PathSelector::new();
1018        let message = create_test_message();
1019
1020        let neighbors = vec![
1021            create_neighbor("peer1", -60, Some(2), 0.2),
1022            create_neighbor("peer2", -55, Some(1), 0.1),
1023        ];
1024
1025        let best = selector.select_best_path(&message, &neighbors);
1026        assert_eq!(best, Some("peer2".to_string()));
1027    }
1028
1029    #[test]
1030    fn test_route_around_congestion() {
1031        let selector = PathSelector::new();
1032
1033        let congested = create_neighbor("peer1", -60, Some(2), 0.9);
1034        assert!(selector.should_route_around_congestion(&congested));
1035
1036        let normal = create_neighbor("peer2", -60, Some(2), 0.3);
1037        assert!(!selector.should_route_around_congestion(&normal));
1038    }
1039
1040    #[test]
1041    fn test_gossip_probability_small_network() {
1042        let selector = PathSelector::new();
1043
1044        // Below threshold (10), probability should be 1.0
1045        assert_eq!(selector.compute_forwarding_probability(5), 1.0);
1046        assert_eq!(selector.compute_forwarding_probability(10), 1.0);
1047    }
1048
1049    #[test]
1050    fn test_gossip_probability_large_network() {
1051        let selector = PathSelector::new();
1052
1053        // Above threshold, probability should scale down
1054        // target_fanout = 4, so at 40 peers: probability = 4/40 = 0.1
1055        // But min_probability = 0.15, so it should be 0.15
1056        let prob = selector.compute_forwarding_probability(40);
1057        assert!((prob - 0.15).abs() < 0.01);
1058
1059        // At 20 peers: probability = 4/20 = 0.2
1060        let prob = selector.compute_forwarding_probability(20);
1061        assert!((prob - 0.2).abs() < 0.01);
1062
1063        // At 100 peers: probability = 4/100 = 0.04, but min is 0.15
1064        let prob = selector.compute_forwarding_probability(100);
1065        assert!((prob - 0.15).abs() < 0.01);
1066    }
1067
1068    #[test]
1069    fn test_gossip_disabled() {
1070        let config = PathConfig {
1071            gossip: GossipConfig {
1072                enabled: false,
1073                ..Default::default()
1074            },
1075            ..Default::default()
1076        };
1077        let selector = PathSelector::with_config(config, RelayManager::new());
1078
1079        // Even with 1000 peers, probability should be 1.0 when disabled
1080        assert_eq!(selector.compute_forwarding_probability(1000), 1.0);
1081    }
1082
1083    #[test]
1084    fn test_gossip_forwarding_deterministic() {
1085        let mut selector = PathSelector::new();
1086        selector.set_local_device_id("device1");
1087
1088        let message = create_test_message();
1089        let neighbors = vec![
1090            create_neighbor("peer1", -60, Some(2), 0.2),
1091            create_neighbor("peer2", -60, Some(2), 0.2),
1092            create_neighbor("peer3", -60, Some(2), 0.2),
1093        ];
1094
1095        // Same input should produce same output
1096        let decision1 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1097        let decision2 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1098
1099        assert_eq!(decision1.peers, decision2.peers);
1100        assert!(decision1.gossip_applied);
1101    }
1102
1103    #[test]
1104    fn test_gossip_priority_bypass() {
1105        use offline_protocol_core::MessagePriority;
1106
1107        let mut selector = PathSelector::new();
1108        selector.set_local_device_id("device1");
1109
1110        let high_priority_message = Message::builder(
1111            UserId::new("alice").unwrap(),
1112            UserId::new("bob").unwrap(),
1113            AppId::new("test").unwrap(),
1114        )
1115        .content("Test")
1116        .priority(MessagePriority::High)
1117        .build();
1118
1119        let neighbors = vec![
1120            create_neighbor("peer1", -60, Some(2), 0.2),
1121            create_neighbor("peer2", -60, Some(2), 0.2),
1122        ];
1123
1124        // High priority should bypass gossip
1125        let decision = selector.select_paths_with_gossip(&high_priority_message, &neighbors, 100);
1126        assert!(!decision.gossip_applied);
1127        assert_eq!(decision.probability, 1.0);
1128    }
1129
1130    #[test]
1131    fn test_gossip_reduces_forwarding_count() {
1132        let mut selector = PathSelector::new();
1133        selector.set_local_device_id("test-device");
1134
1135        let neighbors: Vec<NeighborInfo> = (0..20)
1136            .map(|i| create_neighbor(&format!("peer{}", i), -60, Some(2), 0.2))
1137            .collect();
1138
1139        let message = create_test_message();
1140
1141        // With 100 visible peers and target_fanout=4, probability ≈ 0.15
1142        // So we should forward to fewer peers on average
1143        let decision = selector.select_paths_with_gossip(&message, &neighbors, 100);
1144
1145        // Gossip should be applied
1146        assert!(decision.gossip_applied);
1147        // Probability should be at minimum (0.15)
1148        assert!((decision.probability - 0.15).abs() < 0.01);
1149    }
1150
1151    // ---------- DSDV sequence number tests (loop-free routing) ----------
1152
1153    #[test]
1154    fn test_dsdv_prefer_higher_sequence_number() {
1155        let mut table = GradientRoutingTable::new();
1156        let dest = "alice";
1157
1158        table.learn_route(dest, "peer_a", 2, 0.9, 1);
1159        table.learn_route(dest, "peer_b", 3, 0.8, 2);
1160
1161        let route = table.get_route(dest).expect("should have route");
1162        assert_eq!(route.next_hop, "peer_b");
1163        assert_eq!(route.sequence_number, 2);
1164    }
1165
1166    #[test]
1167    fn test_dsdv_reject_stale_update_same_next_hop() {
1168        let mut table = GradientRoutingTable::new();
1169        let dest = "bob";
1170
1171        table.learn_route(dest, "peer_a", 2, 0.9, 2);
1172        table.learn_route(dest, "peer_a", 5, 0.5, 1); // stale: lower sequence
1173
1174        let route = table.get_route(dest).expect("should have route");
1175        assert_eq!(route.next_hop, "peer_a");
1176        assert_eq!(route.hop_count, 2);
1177        assert_eq!(route.sequence_number, 2);
1178    }
1179
1180    #[test]
1181    fn test_dsdv_accept_same_sequence_better_hop_count() {
1182        let mut table = GradientRoutingTable::new();
1183        let dest = "carol";
1184
1185        table.learn_route(dest, "peer_a", 4, 0.7, 2);
1186        table.learn_route(dest, "peer_a", 2, 0.8, 2); // same seq, better hop count
1187
1188        let route = table.get_route(dest).expect("should have route");
1189        assert_eq!(route.hop_count, 2);
1190        assert_eq!(route.sequence_number, 2);
1191    }
1192
1193    #[test]
1194    fn test_global_eviction_prefers_low_quality_routes() {
1195        let config = GradientRoutingConfig {
1196            max_routing_table_size: 3,
1197            max_routes_per_destination: 3,
1198            route_ttl_secs: 300,
1199            ..Default::default()
1200        };
1201        let mut table = GradientRoutingTable::with_config(config);
1202
1203        // Insert a high-quality recent route
1204        table.learn_route("alice", "hop_good", 1, 0.9, 0);
1205        // Insert two low-quality routes
1206        table.learn_route("bob", "hop_low1", 3, 0.1, 0);
1207        table.learn_route("carol", "hop_low2", 4, 0.1, 0);
1208
1209        assert_eq!(table.route_count(), 3);
1210
1211        // Adding a 4th route should evict the lowest composite score route
1212        table.learn_route("dave", "hop_new", 2, 0.5, 0);
1213        assert_eq!(table.route_count(), 3);
1214
1215        // The high-quality route to alice should survive
1216        assert!(
1217            table.has_route("alice"),
1218            "High-quality route to alice should survive eviction"
1219        );
1220        // dave was just inserted, should exist
1221        assert!(table.has_route("dave"), "Newly inserted route should exist");
1222    }
1223
1224    #[test]
1225    fn test_dsdv_eviction_removes_worst_route() {
1226        let config = GradientRoutingConfig {
1227            max_routes_per_destination: 2,
1228            ..Default::default()
1229        };
1230        let mut table = GradientRoutingTable::with_config(config);
1231        let dest = "dest";
1232
1233        table.learn_route(dest, "hop1", 1, 0.9, 10);
1234        table.learn_route(dest, "hop2", 2, 0.8, 10);
1235        // At capacity: evict worst of current two (hop2), then add hop3
1236        table.learn_route(dest, "hop3", 3, 0.7, 5);
1237
1238        let route = table.get_route(dest).expect("should have route");
1239        assert_eq!(route.next_hop, "hop1");
1240        let routes = table.get_routes(dest);
1241        assert_eq!(routes.len(), 2);
1242        assert!(routes.iter().any(|r| r.next_hop == "hop1"));
1243        assert!(routes.iter().any(|r| r.next_hop == "hop3"));
1244        assert!(
1245            !routes.iter().any(|r| r.next_hop == "hop2"),
1246            "hop2 (worst of original two) should be evicted"
1247        );
1248    }
1249
1250    // ---------- RFC 1982 sequence number wrapping tests ----------
1251
1252    #[test]
1253    fn test_seq_is_newer_basic() {
1254        assert!(seq_is_newer(2, 1));
1255        assert!(seq_is_newer(100, 50));
1256        assert!(!seq_is_newer(1, 2));
1257        assert!(!seq_is_newer(5, 5)); // equal is not newer
1258    }
1259
1260    #[test]
1261    fn test_seq_is_newer_wrapping() {
1262        // Near u32::MAX wrapping to 0
1263        assert!(seq_is_newer(0, u32::MAX));
1264        assert!(seq_is_newer(1, u32::MAX));
1265        assert!(seq_is_newer(5, u32::MAX - 2));
1266        // Going backwards across the wrap should not be considered newer
1267        assert!(!seq_is_newer(u32::MAX, 0));
1268        assert!(!seq_is_newer(u32::MAX - 2, 5));
1269    }
1270
1271    #[test]
1272    fn test_dsdv_wrapping_sequence_update() {
1273        let mut table = GradientRoutingTable::new();
1274        let dest = "alice";
1275
1276        // Learn a route with seq near MAX
1277        table.learn_route(dest, "peer_a", 2, 0.9, u32::MAX - 1);
1278
1279        // Update with wrapped sequence (0 is newer than MAX-1)
1280        table.learn_route(dest, "peer_a", 2, 0.9, 0);
1281
1282        let route = table.get_route(dest).expect("should have route");
1283        assert_eq!(
1284            route.sequence_number, 0,
1285            "Wrapped sequence should be accepted"
1286        );
1287    }
1288
1289    #[test]
1290    fn test_seq_cmp_basic_and_wrapping() {
1291        use std::cmp::Ordering;
1292        assert_eq!(seq_cmp(5, 5), Ordering::Equal);
1293        assert_eq!(seq_cmp(10, 5), Ordering::Greater);
1294        assert_eq!(seq_cmp(5, 10), Ordering::Less);
1295        // Wrapping: 0 is newer than MAX
1296        assert_eq!(seq_cmp(0, u32::MAX), Ordering::Greater);
1297        assert_eq!(seq_cmp(u32::MAX, 0), Ordering::Less);
1298    }
1299
1300    #[test]
1301    fn test_get_route_prefers_wrapped_sequence() {
1302        let mut table = GradientRoutingTable::new();
1303        let dest = "alice";
1304
1305        // Route A has seq near MAX, route B wrapped to 1
1306        table.learn_route(dest, "peer_old", 2, 0.9, u32::MAX - 1);
1307        table.learn_route(dest, "peer_new", 3, 0.7, 1);
1308
1309        let route = table.get_route(dest).expect("should have route");
1310        assert_eq!(
1311            route.next_hop, "peer_new",
1312            "get_route must prefer wrapped (newer) sequence number"
1313        );
1314        assert_eq!(route.sequence_number, 1);
1315    }
1316
1317    #[test]
1318    fn test_per_dest_eviction_respects_wrapping() {
1319        let config = GradientRoutingConfig {
1320            max_routes_per_destination: 2,
1321            ..Default::default()
1322        };
1323        let mut table = GradientRoutingTable::with_config(config);
1324        let dest = "bob";
1325
1326        // Two routes with pre-wrap sequences
1327        table.learn_route(dest, "hop_old", 2, 0.8, u32::MAX - 5);
1328        table.learn_route(dest, "hop_mid", 2, 0.8, u32::MAX);
1329
1330        // Third route with wrapped sequence — should evict the oldest (MAX-5)
1331        table.learn_route(dest, "hop_new", 2, 0.8, 2);
1332
1333        let routes = table.get_routes(dest);
1334        assert_eq!(routes.len(), 2);
1335        assert!(
1336            routes.iter().any(|r| r.next_hop == "hop_new"),
1337            "Wrapped-sequence route must survive"
1338        );
1339        assert!(
1340            routes.iter().any(|r| r.next_hop == "hop_mid"),
1341            "Second-best sequence route must survive"
1342        );
1343        assert!(
1344            !routes.iter().any(|r| r.next_hop == "hop_old"),
1345            "Oldest sequence route should be evicted"
1346        );
1347    }
1348
1349    #[test]
1350    fn test_seq_is_newer_half_space_boundary() {
1351        // At exactly half the sequence space, RFC 1982 says the result is
1352        // undefined. Our implementation treats it as "not newer".
1353        let half = 1u32 << 31;
1354        assert!(
1355            !seq_is_newer(half, 0),
1356            "Half-space distance is ambiguous, should not be newer"
1357        );
1358        assert!(!seq_is_newer(0, half), "Reverse half-space also not newer");
1359    }
1360}