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};
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    /// Computes the forwarding probability based on visible peer count.
487    ///
488    /// Uses the formula: min(1.0, max(min_probability, target_fanout / peer_count))
489    /// This ensures constant expected message overhead regardless of network size.
490    pub fn compute_forwarding_probability(&self, visible_peer_count: usize) -> f32 {
491        let gossip = &self.config.gossip;
492
493        if !gossip.enabled || visible_peer_count <= gossip.small_network_threshold {
494            return 1.0;
495        }
496
497        let raw_probability = gossip.target_fanout as f32 / visible_peer_count as f32;
498        raw_probability.max(gossip.min_probability).min(1.0)
499    }
500
501    /// Determines if a message should be forwarded to a specific peer using
502    /// deterministic pseudo-random selection based on message ID and peer ID.
503    ///
504    /// This ensures that the same message-peer pair always produces the same
505    /// decision across all nodes, preventing duplicate forwarding.
506    fn should_forward_to_peer(&self, message_id: &str, peer_id: &str, probability: f32) -> bool {
507        if probability >= 1.0 {
508            return true;
509        }
510        if probability <= 0.0 {
511            return false;
512        }
513
514        // Create deterministic hash from message ID, peer ID, and local device ID
515        let mut hasher = DefaultHasher::new();
516        message_id.hash(&mut hasher);
517        peer_id.hash(&mut hasher);
518        self.local_device_id.hash(&mut hasher);
519        let hash = hasher.finish();
520
521        // Convert hash to probability (0.0 to 1.0)
522        let hash_probability = (hash as f64 / u64::MAX as f64) as f32;
523
524        hash_probability < probability
525    }
526
527    /// Selects the best path(s) for message delivery with gossip-based filtering.
528    ///
529    /// # Arguments
530    ///
531    /// * `message` - The message to route
532    /// * `neighbors` - Available neighboring devices
533    /// * `total_visible_peers` - Total number of peers visible in the network
534    ///   (may be larger than neighbors if some peers are not connected)
535    ///
536    /// # Returns
537    ///
538    /// Returns a forwarding decision with selected peers and metadata.
539    pub fn select_paths_with_gossip(
540        &self,
541        message: &Message,
542        neighbors: &[NeighborInfo],
543        total_visible_peers: usize,
544    ) -> ForwardingDecision {
545        if neighbors.is_empty() {
546            return ForwardingDecision {
547                peers: Vec::new(),
548                gossip_applied: false,
549                probability: 1.0,
550                dropped_count: 0,
551            };
552        }
553
554        // Check if high-priority message should bypass gossip
555        let bypass_gossip = self.config.gossip.priority_bypass
556            && matches!(
557                message.priority,
558                offline_protocol_core::MessagePriority::High
559                    | offline_protocol_core::MessagePriority::Critical
560            );
561
562        // Compute forwarding probability
563        let probability = if bypass_gossip {
564            1.0
565        } else {
566            self.compute_forwarding_probability(total_visible_peers.max(neighbors.len()))
567        };
568
569        let gossip_applied = probability < 1.0;
570
571        // Calculate scores for each neighbor
572        let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
573            .iter()
574            .map(|neighbor| {
575                let score = self.calculate_path_score(message, neighbor);
576                (neighbor.peer_id.clone(), score)
577            })
578            .collect();
579
580        // Filter out neighbors with relay congestion > max threshold
581        scored_neighbors.retain(|(peer_id, _)| {
582            if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
583                if let Some(relay_info) = &neighbor.relay_info {
584                    return relay_info.congestion_level <= self.config.max_congestion_level;
585                }
586            }
587            true
588        });
589
590        // Sort by total score (descending)
591        scored_neighbors.sort_by(|a, b| {
592            b.1.total
593                .partial_cmp(&a.1.total)
594                .unwrap_or(std::cmp::Ordering::Equal)
595        });
596
597        // Take top K candidates
598        let candidates: Vec<String> = scored_neighbors
599            .into_iter()
600            .take(self.config.forward_to_top_k)
601            .map(|(peer_id, _)| peer_id)
602            .collect();
603
604        // Apply probabilistic filtering if gossip is active
605        let initial_count = candidates.len();
606        let message_id = message.id.as_str();
607        let peers: Vec<String> = if gossip_applied {
608            candidates
609                .into_iter()
610                .filter(|peer_id| {
611                    self.should_forward_to_peer(&message_id, peer_id.as_str(), probability)
612                })
613                .collect()
614        } else {
615            candidates
616        };
617
618        let dropped_count = initial_count.saturating_sub(peers.len());
619
620        ForwardingDecision {
621            peers,
622            gossip_applied,
623            probability,
624            dropped_count,
625        }
626    }
627
628    /// Selects the best path(s) for message delivery.
629    ///
630    /// # Arguments
631    ///
632    /// * `message` - The message to route
633    /// * `neighbors` - Available neighboring devices
634    ///
635    /// # Returns
636    ///
637    /// Returns a list of neighbors to forward the message to, ordered by preference.
638    ///
639    /// Note: This method does not apply gossip filtering. Use `select_paths_with_gossip`
640    /// for scalable routing in large networks.
641    pub fn select_paths(&self, message: &Message, neighbors: &[NeighborInfo]) -> Vec<String> {
642        if neighbors.is_empty() {
643            return Vec::new();
644        }
645
646        // Calculate scores for each neighbor
647        let mut scored_neighbors: Vec<(String, PathScore)> = neighbors
648            .iter()
649            .map(|neighbor| {
650                let score = self.calculate_path_score(message, neighbor);
651                (neighbor.peer_id.clone(), score)
652            })
653            .collect();
654
655        // Filter out neighbors with relay congestion > max threshold
656        scored_neighbors.retain(|(peer_id, _)| {
657            if let Some(neighbor) = neighbors.iter().find(|n| n.peer_id == *peer_id) {
658                if let Some(relay_info) = &neighbor.relay_info {
659                    return relay_info.congestion_level <= self.config.max_congestion_level;
660                }
661            }
662            true
663        });
664
665        // Sort by total score (descending)
666        scored_neighbors.sort_by(|a, b| {
667            b.1.total
668                .partial_cmp(&a.1.total)
669                .unwrap_or(std::cmp::Ordering::Equal)
670        });
671
672        // Select top K neighbors for redundancy
673        scored_neighbors
674            .into_iter()
675            .take(self.config.forward_to_top_k)
676            .map(|(peer_id, _)| peer_id)
677            .collect()
678    }
679
680    /// Calculates the path score for a neighbor.
681    fn calculate_path_score(&self, message: &Message, neighbor: &NeighborInfo) -> PathScore {
682        let signal_score = self.calculate_signal_score(neighbor);
683        let proximity_score = self.calculate_proximity_score(message, neighbor);
684        let capacity_score = self.calculate_capacity_score(neighbor);
685        let energy_score = self.calculate_energy_score(neighbor);
686
687        let total = (signal_score * SIGNAL_WEIGHT)
688            + (proximity_score * PROXIMITY_WEIGHT)
689            + (capacity_score * CAPACITY_WEIGHT)
690            + (energy_score * ENERGY_WEIGHT);
691
692        PathScore {
693            signal: signal_score,
694            proximity: proximity_score,
695            capacity: capacity_score,
696            energy: energy_score,
697            total,
698        }
699    }
700
701    /// Calculates signal strength score from RSSI.
702    fn calculate_signal_score(&self, neighbor: &NeighborInfo) -> f32 {
703        let rssi = neighbor.rssi;
704
705        if rssi >= EXCELLENT_RSSI_THRESHOLD {
706            EXCELLENT_SIGNAL_SCORE
707        } else if rssi >= GOOD_RSSI_THRESHOLD {
708            GOOD_SIGNAL_BASE
709                + ((rssi - GOOD_RSSI_THRESHOLD) as f32 * 30.0
710                    / (EXCELLENT_RSSI_THRESHOLD - GOOD_RSSI_THRESHOLD) as f32)
711        } else if rssi >= FAIR_RSSI_THRESHOLD {
712            FAIR_SIGNAL_BASE
713                + ((rssi - FAIR_RSSI_THRESHOLD) as f32 * 30.0
714                    / (GOOD_RSSI_THRESHOLD - FAIR_RSSI_THRESHOLD) as f32)
715        } else {
716            ((rssi - POOR_RSSI_MAX).max(0) as f32 * FAIR_SIGNAL_BASE
717                / (FAIR_RSSI_THRESHOLD - POOR_RSSI_MAX) as f32)
718                .max(0.0)
719        }
720    }
721
722    /// Calculates proximity score based on hop distance.
723    fn calculate_proximity_score(&self, message: &Message, neighbor: &NeighborInfo) -> f32 {
724        if let Some(hops) = neighbor.hops_to_destination {
725            // We know the distance to destination through this neighbor
726            let remaining_ttl = message.ttl.value();
727
728            if hops == 0 {
729                // Direct connection to destination
730                100.0
731            } else {
732                // Score based on whether we have enough TTL
733                let score = if hops < remaining_ttl {
734                    100.0 - (hops as f32 / remaining_ttl as f32 * 50.0)
735                } else {
736                    // Not enough TTL, but still possible
737                    20.0
738                };
739                score.max(0.0)
740            }
741        } else {
742            // Unknown distance, use link quality as proxy
743            neighbor.link_quality as f32
744        }
745    }
746
747    /// Calculates capacity score based on relay status and congestion.
748    fn calculate_capacity_score(&self, neighbor: &NeighborInfo) -> f32 {
749        if let Some(relay_info) = &neighbor.relay_info {
750            let relay_score = self.relay_manager.calculate_relay_score(relay_info);
751            relay_score.min(EXCELLENT_SIGNAL_SCORE)
752        } else {
753            NON_RELAY_BASIC_CAPACITY
754        }
755    }
756
757    /// Calculates energy score based on battery level.
758    fn calculate_energy_score(&self, neighbor: &NeighborInfo) -> f32 {
759        if let Some(relay_info) = &neighbor.relay_info {
760            if relay_info.is_charging {
761                CHARGING_BATTERY_SCORE
762            } else {
763                relay_info.battery_level as f32
764            }
765        } else {
766            NON_RELAY_ASSUMED_BATTERY
767        }
768    }
769
770    /// Finds the best single path (for unicast).
771    pub fn select_best_path(
772        &self,
773        message: &Message,
774        neighbors: &[NeighborInfo],
775    ) -> Option<String> {
776        self.select_paths(message, neighbors).into_iter().next()
777    }
778
779    /// Checks if routing around congestion is needed.
780    pub fn should_route_around_congestion(&self, neighbor: &NeighborInfo) -> bool {
781        if let Some(relay_info) = &neighbor.relay_info {
782            relay_info.congestion_level > self.config.max_congestion_level
783        } else {
784            false
785        }
786    }
787
788    /// Gets the path configuration.
789    pub fn config(&self) -> &PathConfig {
790        &self.config
791    }
792
793    // MARK: - Gradient Routing Methods
794
795    /// Learns a route from an incoming message.
796    /// Call this when receiving a message from a neighbor to learn
797    /// that the neighbor can reach the message's sender.
798    pub fn learn_route_from_message(
799        &mut self,
800        message: &Message,
801        from_neighbor: &str,
802        quality: f32,
803    ) {
804        let sender = message.sender.as_str();
805        let hop_count = message.hop_count.value();
806        self.routing_table.learn_route(
807            sender,
808            from_neighbor,
809            hop_count,
810            quality,
811            0, // No destination sequence in message yet; use 0 for backward compatibility
812        );
813    }
814
815    /// Gets the best route to a destination, if known.
816    pub fn get_route_to(&self, destination: &str) -> Option<&RouteEntry> {
817        self.routing_table.get_route(destination)
818    }
819
820    /// Checks if we have a known route to a destination.
821    pub fn has_route_to(&self, destination: &str) -> bool {
822        self.routing_table.has_route(destination)
823    }
824
825    /// Selects the best path for a message using gradient routing when available.
826    /// Falls back to flooding if no route is known.
827    pub fn select_directed_path(
828        &self,
829        message: &Message,
830        neighbors: &[NeighborInfo],
831    ) -> Option<String> {
832        let recipient = message.recipient.as_str();
833
834        // Check if any neighbor IS the recipient
835        for neighbor in neighbors {
836            if neighbor.peer_id == recipient {
837                return Some(neighbor.peer_id.clone());
838            }
839        }
840
841        // Check if we have a learned route
842        if let Some(route) = self.routing_table.get_route(recipient) {
843            // Verify the next hop is in our current neighbors
844            if neighbors.iter().any(|n| n.peer_id == route.next_hop) {
845                return Some(route.next_hop.clone());
846            }
847        }
848
849        // No direct route known, fall back to best path selection
850        self.select_best_path(message, neighbors)
851    }
852
853    /// Removes a neighbor from the routing table (e.g., on disconnect).
854    pub fn remove_neighbor_routes(&mut self, neighbor: &str) {
855        self.routing_table.remove_neighbor(neighbor);
856    }
857
858    /// Cleans up expired routes.
859    pub fn cleanup_routes(&mut self) {
860        self.routing_table.cleanup_expired();
861    }
862
863    /// Returns routing table statistics.
864    pub fn routing_stats(&self) -> (usize, usize) {
865        (
866            self.routing_table.destination_count(),
867            self.routing_table.route_count(),
868        )
869    }
870
871    /// Gets a mutable reference to the routing table for advanced operations.
872    pub fn routing_table_mut(&mut self) -> &mut GradientRoutingTable {
873        &mut self.routing_table
874    }
875}
876
877impl Default for PathSelector {
878    fn default() -> Self {
879        Self::new()
880    }
881}
882
883#[cfg(test)]
884mod tests {
885    use super::*;
886    use crate::relay::{RelayInfo, RelayRole};
887    use offline_protocol_core::{AppId, UserId};
888
889    fn create_test_message() -> Message {
890        Message::new(
891            UserId::new("alice").unwrap(),
892            UserId::new("bob").unwrap(),
893            AppId::new("test").unwrap(),
894            "Test message",
895        )
896    }
897
898    fn create_neighbor(id: &str, rssi: i16, hops: Option<u8>, congestion: f32) -> NeighborInfo {
899        NeighborInfo {
900            peer_id: id.to_string(),
901            rssi,
902            hops_to_destination: hops,
903            link_quality: 80,
904            relay_info: Some(RelayInfo {
905                connection_count: 5,
906                battery_level: 70,
907                is_charging: false,
908                role: RelayRole::Relay,
909                link_quality: 80,
910                queue_depth: 10,
911                congestion_level: congestion,
912            }),
913        }
914    }
915
916    #[test]
917    fn test_path_selection_basic() {
918        let selector = PathSelector::new();
919        let message = create_test_message();
920
921        let neighbors = vec![
922            create_neighbor("peer1", -60, Some(2), 0.2),
923            create_neighbor("peer2", -70, Some(3), 0.3),
924            create_neighbor("peer3", -55, Some(1), 0.1),
925        ];
926
927        let paths = selector.select_paths(&message, &neighbors);
928
929        // Should select top K paths
930        assert!(!paths.is_empty());
931        assert!(paths.len() <= 3);
932
933        // Best path should be peer3 (best RSSI, lowest hops, lowest congestion)
934        assert_eq!(paths[0], "peer3");
935    }
936
937    #[test]
938    fn test_congestion_filtering() {
939        let selector = PathSelector::new();
940        let message = create_test_message();
941
942        let neighbors = vec![
943            create_neighbor("peer1", -60, Some(2), 0.9), // High congestion
944            create_neighbor("peer2", -65, Some(3), 0.3),
945        ];
946
947        let paths = selector.select_paths(&message, &neighbors);
948
949        // Should filter out peer1 due to high congestion
950        assert!(!paths.contains(&"peer1".to_string()));
951    }
952
953    #[test]
954    fn test_direct_destination_preferred() {
955        let selector = PathSelector::new();
956        let message = create_test_message();
957
958        let neighbors = vec![
959            create_neighbor("peer1", -70, Some(5), 0.3),
960            create_neighbor("peer2", -70, Some(0), 0.1), // Direct to destination
961        ];
962
963        let paths = selector.select_paths(&message, &neighbors);
964
965        // Should prefer peer2 (direct destination) with same signal
966        assert_eq!(paths[0], "peer2");
967    }
968
969    #[test]
970    fn test_signal_score_calculation() {
971        let selector = PathSelector::new();
972
973        let excellent = create_neighbor("peer1", -40, None, 0.1);
974        let score = selector.calculate_signal_score(&excellent);
975        assert_eq!(score, 100.0);
976
977        let poor = create_neighbor("peer2", -90, None, 0.1);
978        let score = selector.calculate_signal_score(&poor);
979        assert!(score < 40.0);
980    }
981
982    #[test]
983    fn test_empty_neighbors() {
984        let selector = PathSelector::new();
985        let message = create_test_message();
986
987        let paths = selector.select_paths(&message, &[]);
988        assert!(paths.is_empty());
989    }
990
991    #[test]
992    fn test_select_best_path() {
993        let selector = PathSelector::new();
994        let message = create_test_message();
995
996        let neighbors = vec![
997            create_neighbor("peer1", -60, Some(2), 0.2),
998            create_neighbor("peer2", -55, Some(1), 0.1),
999        ];
1000
1001        let best = selector.select_best_path(&message, &neighbors);
1002        assert_eq!(best, Some("peer2".to_string()));
1003    }
1004
1005    #[test]
1006    fn test_route_around_congestion() {
1007        let selector = PathSelector::new();
1008
1009        let congested = create_neighbor("peer1", -60, Some(2), 0.9);
1010        assert!(selector.should_route_around_congestion(&congested));
1011
1012        let normal = create_neighbor("peer2", -60, Some(2), 0.3);
1013        assert!(!selector.should_route_around_congestion(&normal));
1014    }
1015
1016    #[test]
1017    fn test_gossip_probability_small_network() {
1018        let selector = PathSelector::new();
1019
1020        // Below threshold (10), probability should be 1.0
1021        assert_eq!(selector.compute_forwarding_probability(5), 1.0);
1022        assert_eq!(selector.compute_forwarding_probability(10), 1.0);
1023    }
1024
1025    #[test]
1026    fn test_gossip_probability_large_network() {
1027        let selector = PathSelector::new();
1028
1029        // Above threshold, probability should scale down
1030        // target_fanout = 4, so at 40 peers: probability = 4/40 = 0.1
1031        // But min_probability = 0.15, so it should be 0.15
1032        let prob = selector.compute_forwarding_probability(40);
1033        assert!((prob - 0.15).abs() < 0.01);
1034
1035        // At 20 peers: probability = 4/20 = 0.2
1036        let prob = selector.compute_forwarding_probability(20);
1037        assert!((prob - 0.2).abs() < 0.01);
1038
1039        // At 100 peers: probability = 4/100 = 0.04, but min is 0.15
1040        let prob = selector.compute_forwarding_probability(100);
1041        assert!((prob - 0.15).abs() < 0.01);
1042    }
1043
1044    #[test]
1045    fn test_gossip_disabled() {
1046        let config = PathConfig {
1047            gossip: GossipConfig {
1048                enabled: false,
1049                ..Default::default()
1050            },
1051            ..Default::default()
1052        };
1053        let selector = PathSelector::with_config(config, RelayManager::new());
1054
1055        // Even with 1000 peers, probability should be 1.0 when disabled
1056        assert_eq!(selector.compute_forwarding_probability(1000), 1.0);
1057    }
1058
1059    #[test]
1060    fn test_gossip_forwarding_deterministic() {
1061        let mut selector = PathSelector::new();
1062        selector.set_local_device_id("device1");
1063
1064        let message = create_test_message();
1065        let neighbors = vec![
1066            create_neighbor("peer1", -60, Some(2), 0.2),
1067            create_neighbor("peer2", -60, Some(2), 0.2),
1068            create_neighbor("peer3", -60, Some(2), 0.2),
1069        ];
1070
1071        // Same input should produce same output
1072        let decision1 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1073        let decision2 = selector.select_paths_with_gossip(&message, &neighbors, 50);
1074
1075        assert_eq!(decision1.peers, decision2.peers);
1076        assert!(decision1.gossip_applied);
1077    }
1078
1079    #[test]
1080    fn test_gossip_priority_bypass() {
1081        use offline_protocol_core::MessagePriority;
1082
1083        let mut selector = PathSelector::new();
1084        selector.set_local_device_id("device1");
1085
1086        let high_priority_message = Message::builder(
1087            UserId::new("alice").unwrap(),
1088            UserId::new("bob").unwrap(),
1089            AppId::new("test").unwrap(),
1090        )
1091        .content("Test")
1092        .priority(MessagePriority::High)
1093        .build();
1094
1095        let neighbors = vec![
1096            create_neighbor("peer1", -60, Some(2), 0.2),
1097            create_neighbor("peer2", -60, Some(2), 0.2),
1098        ];
1099
1100        // High priority should bypass gossip
1101        let decision = selector.select_paths_with_gossip(&high_priority_message, &neighbors, 100);
1102        assert!(!decision.gossip_applied);
1103        assert_eq!(decision.probability, 1.0);
1104    }
1105
1106    #[test]
1107    fn test_gossip_reduces_forwarding_count() {
1108        let mut selector = PathSelector::new();
1109        selector.set_local_device_id("test-device");
1110
1111        let neighbors: Vec<NeighborInfo> = (0..20)
1112            .map(|i| create_neighbor(&format!("peer{}", i), -60, Some(2), 0.2))
1113            .collect();
1114
1115        let message = create_test_message();
1116
1117        // With 100 visible peers and target_fanout=4, probability ≈ 0.15
1118        // So we should forward to fewer peers on average
1119        let decision = selector.select_paths_with_gossip(&message, &neighbors, 100);
1120
1121        // Gossip should be applied
1122        assert!(decision.gossip_applied);
1123        // Probability should be at minimum (0.15)
1124        assert!((decision.probability - 0.15).abs() < 0.01);
1125    }
1126
1127    // ---------- DSDV sequence number tests (loop-free routing) ----------
1128
1129    #[test]
1130    fn test_dsdv_prefer_higher_sequence_number() {
1131        let mut table = GradientRoutingTable::new();
1132        let dest = "alice";
1133
1134        table.learn_route(dest, "peer_a", 2, 0.9, 1);
1135        table.learn_route(dest, "peer_b", 3, 0.8, 2);
1136
1137        let route = table.get_route(dest).expect("should have route");
1138        assert_eq!(route.next_hop, "peer_b");
1139        assert_eq!(route.sequence_number, 2);
1140    }
1141
1142    #[test]
1143    fn test_dsdv_reject_stale_update_same_next_hop() {
1144        let mut table = GradientRoutingTable::new();
1145        let dest = "bob";
1146
1147        table.learn_route(dest, "peer_a", 2, 0.9, 2);
1148        table.learn_route(dest, "peer_a", 5, 0.5, 1); // stale: lower sequence
1149
1150        let route = table.get_route(dest).expect("should have route");
1151        assert_eq!(route.next_hop, "peer_a");
1152        assert_eq!(route.hop_count, 2);
1153        assert_eq!(route.sequence_number, 2);
1154    }
1155
1156    #[test]
1157    fn test_dsdv_accept_same_sequence_better_hop_count() {
1158        let mut table = GradientRoutingTable::new();
1159        let dest = "carol";
1160
1161        table.learn_route(dest, "peer_a", 4, 0.7, 2);
1162        table.learn_route(dest, "peer_a", 2, 0.8, 2); // same seq, better hop count
1163
1164        let route = table.get_route(dest).expect("should have route");
1165        assert_eq!(route.hop_count, 2);
1166        assert_eq!(route.sequence_number, 2);
1167    }
1168
1169    #[test]
1170    fn test_global_eviction_prefers_low_quality_routes() {
1171        let config = GradientRoutingConfig {
1172            max_routing_table_size: 3,
1173            max_routes_per_destination: 3,
1174            route_ttl_secs: 300,
1175            ..Default::default()
1176        };
1177        let mut table = GradientRoutingTable::with_config(config);
1178
1179        // Insert a high-quality recent route
1180        table.learn_route("alice", "hop_good", 1, 0.9, 0);
1181        // Insert two low-quality routes
1182        table.learn_route("bob", "hop_low1", 3, 0.1, 0);
1183        table.learn_route("carol", "hop_low2", 4, 0.1, 0);
1184
1185        assert_eq!(table.route_count(), 3);
1186
1187        // Adding a 4th route should evict the lowest composite score route
1188        table.learn_route("dave", "hop_new", 2, 0.5, 0);
1189        assert_eq!(table.route_count(), 3);
1190
1191        // The high-quality route to alice should survive
1192        assert!(
1193            table.has_route("alice"),
1194            "High-quality route to alice should survive eviction"
1195        );
1196        // dave was just inserted, should exist
1197        assert!(table.has_route("dave"), "Newly inserted route should exist");
1198    }
1199
1200    #[test]
1201    fn test_dsdv_eviction_removes_worst_route() {
1202        let config = GradientRoutingConfig {
1203            max_routes_per_destination: 2,
1204            ..Default::default()
1205        };
1206        let mut table = GradientRoutingTable::with_config(config);
1207        let dest = "dest";
1208
1209        table.learn_route(dest, "hop1", 1, 0.9, 10);
1210        table.learn_route(dest, "hop2", 2, 0.8, 10);
1211        // At capacity: evict worst of current two (hop2), then add hop3
1212        table.learn_route(dest, "hop3", 3, 0.7, 5);
1213
1214        let route = table.get_route(dest).expect("should have route");
1215        assert_eq!(route.next_hop, "hop1");
1216        let routes = table.get_routes(dest);
1217        assert_eq!(routes.len(), 2);
1218        assert!(routes.iter().any(|r| r.next_hop == "hop1"));
1219        assert!(routes.iter().any(|r| r.next_hop == "hop3"));
1220        assert!(
1221            !routes.iter().any(|r| r.next_hop == "hop2"),
1222            "hop2 (worst of original two) should be evicted"
1223        );
1224    }
1225
1226    // ---------- RFC 1982 sequence number wrapping tests ----------
1227
1228    #[test]
1229    fn test_seq_is_newer_basic() {
1230        assert!(seq_is_newer(2, 1));
1231        assert!(seq_is_newer(100, 50));
1232        assert!(!seq_is_newer(1, 2));
1233        assert!(!seq_is_newer(5, 5)); // equal is not newer
1234    }
1235
1236    #[test]
1237    fn test_seq_is_newer_wrapping() {
1238        // Near u32::MAX wrapping to 0
1239        assert!(seq_is_newer(0, u32::MAX));
1240        assert!(seq_is_newer(1, u32::MAX));
1241        assert!(seq_is_newer(5, u32::MAX - 2));
1242        // Going backwards across the wrap should not be considered newer
1243        assert!(!seq_is_newer(u32::MAX, 0));
1244        assert!(!seq_is_newer(u32::MAX - 2, 5));
1245    }
1246
1247    #[test]
1248    fn test_dsdv_wrapping_sequence_update() {
1249        let mut table = GradientRoutingTable::new();
1250        let dest = "alice";
1251
1252        // Learn a route with seq near MAX
1253        table.learn_route(dest, "peer_a", 2, 0.9, u32::MAX - 1);
1254
1255        // Update with wrapped sequence (0 is newer than MAX-1)
1256        table.learn_route(dest, "peer_a", 2, 0.9, 0);
1257
1258        let route = table.get_route(dest).expect("should have route");
1259        assert_eq!(
1260            route.sequence_number, 0,
1261            "Wrapped sequence should be accepted"
1262        );
1263    }
1264
1265    #[test]
1266    fn test_seq_cmp_basic_and_wrapping() {
1267        use std::cmp::Ordering;
1268        assert_eq!(seq_cmp(5, 5), Ordering::Equal);
1269        assert_eq!(seq_cmp(10, 5), Ordering::Greater);
1270        assert_eq!(seq_cmp(5, 10), Ordering::Less);
1271        // Wrapping: 0 is newer than MAX
1272        assert_eq!(seq_cmp(0, u32::MAX), Ordering::Greater);
1273        assert_eq!(seq_cmp(u32::MAX, 0), Ordering::Less);
1274    }
1275
1276    #[test]
1277    fn test_get_route_prefers_wrapped_sequence() {
1278        let mut table = GradientRoutingTable::new();
1279        let dest = "alice";
1280
1281        // Route A has seq near MAX, route B wrapped to 1
1282        table.learn_route(dest, "peer_old", 2, 0.9, u32::MAX - 1);
1283        table.learn_route(dest, "peer_new", 3, 0.7, 1);
1284
1285        let route = table.get_route(dest).expect("should have route");
1286        assert_eq!(
1287            route.next_hop, "peer_new",
1288            "get_route must prefer wrapped (newer) sequence number"
1289        );
1290        assert_eq!(route.sequence_number, 1);
1291    }
1292
1293    #[test]
1294    fn test_per_dest_eviction_respects_wrapping() {
1295        let config = GradientRoutingConfig {
1296            max_routes_per_destination: 2,
1297            ..Default::default()
1298        };
1299        let mut table = GradientRoutingTable::with_config(config);
1300        let dest = "bob";
1301
1302        // Two routes with pre-wrap sequences
1303        table.learn_route(dest, "hop_old", 2, 0.8, u32::MAX - 5);
1304        table.learn_route(dest, "hop_mid", 2, 0.8, u32::MAX);
1305
1306        // Third route with wrapped sequence — should evict the oldest (MAX-5)
1307        table.learn_route(dest, "hop_new", 2, 0.8, 2);
1308
1309        let routes = table.get_routes(dest);
1310        assert_eq!(routes.len(), 2);
1311        assert!(
1312            routes.iter().any(|r| r.next_hop == "hop_new"),
1313            "Wrapped-sequence route must survive"
1314        );
1315        assert!(
1316            routes.iter().any(|r| r.next_hop == "hop_mid"),
1317            "Second-best sequence route must survive"
1318        );
1319        assert!(
1320            !routes.iter().any(|r| r.next_hop == "hop_old"),
1321            "Oldest sequence route should be evicted"
1322        );
1323    }
1324
1325    #[test]
1326    fn test_seq_is_newer_half_space_boundary() {
1327        // At exactly half the sequence space, RFC 1982 says the result is
1328        // undefined. Our implementation treats it as "not newer".
1329        let half = 1u32 << 31;
1330        assert!(
1331            !seq_is_newer(half, 0),
1332            "Half-space distance is ambiguous, should not be newer"
1333        );
1334        assert!(!seq_is_newer(0, half), "Reverse half-space also not newer");
1335    }
1336}