[border-routing] fix stale prefixes checking timer (#7241)

Current implementation will reschedule the Stale Timer when discovered
on-link or OMR prefixes or RA messages are updated, but it has a few
issues:

1. It's counting the maximum stale time of all on-link prefixes so
    that we will schedule Router Solicitation when all on-link
    prefixes become stale. But it failed to filter out OMR prefixes in
    the `mDiscoveredPrefixes` list. This results in incorrect stale
    time calculation.

2. A deprecated on-link-prefix is not removed from
    `mDiscoveredPrefixes` directly but the preferred lifetime is set
    to zero. We are not filtering out deprecated on-link prefixes when
    calculating stale time and this results in zero stale timer delay
    if there are no non-deprecated on-link prefixes.

This commit fixes those issues by having a dedicated
`ResetDiscoveredPrefixStaleTimer` to reschedule the stale timer with
following rules whenever discovered prefixes or learnt RA messages are
updated:

1. If BR learns RA header from Host daemons, it should send RS when
   the RA header is stale.

2. If BR discovered any on-link prefix, it should send RS when all
   on-link prefixes are stale.

3. If BR discovered any OMR prefix, it should send RS when the first
   OMR prefix is stale.

`ResetDiscoveredPrefixStaleTimer` is supposed to correctly calculate
stale timer timeout whenever it's called.
diff --git a/src/core/border_router/routing_manager.cpp b/src/core/border_router/routing_manager.cpp
index 28ee8ae..5128b2e 100644
--- a/src/core/border_router/routing_manager.cpp
+++ b/src/core/border_router/routing_manager.cpp
@@ -65,6 +65,7 @@
     , mIsAdvertisingLocalOnLinkPrefix(false)
     , mOnLinkPrefixDeprecateTimer(aInstance, HandleOnLinkPrefixDeprecateTimer)
     , mTimeRouterAdvMessageLastUpdate(TimerMilli::GetNow())
+    , mLearntRouterAdvMessageFromHost(false)
     , mDiscoveredPrefixInvalidTimer(aInstance, HandleDiscoveredPrefixInvalidTimer)
     , mDiscoveredPrefixStaleTimer(aInstance, HandleDiscoveredPrefixStaleTimer)
     , mRouterAdvertisementTimer(aInstance, HandleRouterAdvertisementTimer)
@@ -917,6 +918,9 @@
 
         // Re-evaluate our routing policy and send Router Advertisement if necessary.
         EvaluateRoutingPolicy();
+
+        // Reset prefix stale timer because `mDiscoveredPrefixes` may change.
+        ResetDiscoveredPrefixStaleTimer();
     }
 }
 
@@ -927,7 +931,7 @@
 
 void RoutingManager::HandleDiscoveredPrefixStaleTimer(void)
 {
-    otLogInfoBr("All on-link prefixes are stale");
+    otLogInfoBr("Stale On-Link or OMR Prefixes or RA messages are detected");
     StartRouterSolicitationDelay();
 }
 
@@ -1072,7 +1076,6 @@
 {
     Ip6::Prefix     prefix         = aPio.GetPrefix();
     bool            needReevaluate = false;
-    TimeMilli       staleTime;
     ExternalPrefix  onLinkPrefix;
     ExternalPrefix *existingPrefix = nullptr;
 
@@ -1093,16 +1096,12 @@
     onLinkPrefix.mPreferredLifetime = aPio.GetPreferredLifetime();
     onLinkPrefix.mTimeLastUpdate    = TimerMilli::GetNow();
 
-    staleTime = TimerMilli::GetNow();
-
     for (ExternalPrefix &externalPrefix : mDiscoveredPrefixes)
     {
         if (externalPrefix == onLinkPrefix)
         {
             existingPrefix = &externalPrefix;
         }
-
-        staleTime = OT_MAX(staleTime, externalPrefix.GetStaleTime());
     }
 
     if (existingPrefix == nullptr)
@@ -1158,8 +1157,7 @@
     }
 
     mDiscoveredPrefixInvalidTimer.FireAtIfEarlier(existingPrefix->GetExpireTime());
-    staleTime = OT_MAX(staleTime, existingPrefix->GetStaleTime());
-    mDiscoveredPrefixStaleTimer.FireAtIfEarlier(staleTime);
+    ResetDiscoveredPrefixStaleTimer();
 
 exit:
     return needReevaluate;
@@ -1213,8 +1211,6 @@
         {
             existingPrefix = &externalPrefix;
         }
-
-        mDiscoveredPrefixStaleTimer.FireAtIfEarlier(externalPrefix.GetStaleTime());
     }
 
     if (existingPrefix == nullptr)
@@ -1239,7 +1235,7 @@
     *existingPrefix = omrPrefix;
 
     mDiscoveredPrefixInvalidTimer.FireAtIfEarlier(existingPrefix->GetExpireTime());
-    mDiscoveredPrefixStaleTimer.FireAtIfEarlier(existingPrefix->GetStaleTime());
+    ResetDiscoveredPrefixStaleTimer();
 
 exit:
     return;
@@ -1325,17 +1321,82 @@
     if (aRouterAdvMessage == nullptr || aRouterAdvMessage->GetRouterLifetime() == 0)
     {
         mRouterAdvMessage.SetToDefault();
+        mLearntRouterAdvMessageFromHost = false;
     }
     else
     {
-        mRouterAdvMessage = *aRouterAdvMessage;
-        mDiscoveredPrefixStaleTimer.FireAtIfEarlier(mTimeRouterAdvMessageLastUpdate +
-                                                    Time::SecToMsec(kRtrAdvStaleTime));
+        mRouterAdvMessage               = *aRouterAdvMessage;
+        mLearntRouterAdvMessageFromHost = true;
     }
 
+    ResetDiscoveredPrefixStaleTimer();
+
     return (mRouterAdvMessage != oldRouterAdvMessage);
 }
 
+void RoutingManager::ResetDiscoveredPrefixStaleTimer(void)
+{
+    TimeMilli now                           = TimerMilli::GetNow();
+    TimeMilli nextStaleTime                 = now.GetDistantFuture();
+    TimeMilli maxOnlinkPrefixStaleTime      = now;
+    bool      requireCheckStaleOnlinkPrefix = false;
+
+    OT_ASSERT(mIsRunning);
+
+    // The stale timer triggers sending RS to check the state of On-Link/OMR prefixes and host RA messages.
+    // The rules for calculating the next stale time:
+    // 1. If BR learns RA header from Host daemons, it should send RS when the RA header is stale.
+    // 2. If BR discovered any on-link prefix, it should send RS when all on-link prefixes are stale.
+    // 3. If BR discovered any OMR prefix, it should send RS when the first OMR prefix is stale.
+
+    // Check for stale Router Advertisement Message if learnt from Host.
+    if (mLearntRouterAdvMessageFromHost)
+    {
+        TimeMilli routerAdvMessageStaleTime = mTimeRouterAdvMessageLastUpdate + Time::SecToMsec(kRtrAdvStaleTime);
+
+        nextStaleTime = OT_MIN(nextStaleTime, routerAdvMessageStaleTime);
+    }
+
+    for (ExternalPrefix &externalPrefix : mDiscoveredPrefixes)
+    {
+        TimeMilli prefixStaleTime = externalPrefix.GetStaleTime();
+
+        if (externalPrefix.mIsOnLinkPrefix)
+        {
+            if (!externalPrefix.IsDeprecated())
+            {
+                // Check for least recent stale On-Link Prefixes if BR is not advertising local On-Link Prefix.
+                maxOnlinkPrefixStaleTime      = OT_MAX(maxOnlinkPrefixStaleTime, prefixStaleTime);
+                requireCheckStaleOnlinkPrefix = true;
+            }
+        }
+        else
+        {
+            // Check for most recent stale OMR Prefixes
+            nextStaleTime = OT_MIN(nextStaleTime, prefixStaleTime);
+        }
+    }
+
+    if (requireCheckStaleOnlinkPrefix)
+    {
+        nextStaleTime = OT_MIN(nextStaleTime, maxOnlinkPrefixStaleTime);
+    }
+
+    if (nextStaleTime == now.GetDistantFuture())
+    {
+        if (mDiscoveredPrefixStaleTimer.IsRunning())
+        {
+            otLogDebgBr("Prefix stale timer stopped");
+        }
+        mDiscoveredPrefixStaleTimer.Stop();
+    }
+    else
+    {
+        mDiscoveredPrefixStaleTimer.FireAt(nextStaleTime);
+        otLogDebgBr("Prefix stale timer scheduled in %lu ms", nextStaleTime - now);
+    }
+}
+
 } // namespace BorderRouter
 
 } // namespace ot
diff --git a/src/core/border_router/routing_manager.hpp b/src/core/border_router/routing_manager.hpp
index 4e4354e..37c89b4 100644
--- a/src/core/border_router/routing_manager.hpp
+++ b/src/core/border_router/routing_manager.hpp
@@ -315,6 +315,7 @@
     void InvalidateAllDiscoveredPrefixes(void);
     bool NetworkDataContainsOmrPrefix(const Ip6::Prefix &aPrefix) const;
     bool UpdateRouterAdvMessage(const RouterAdv::RouterAdvMessage *aRouterAdvMessage);
+    void ResetDiscoveredPrefixStaleTimer(void);
 
     static bool IsValidOmrPrefix(const NetworkData::OnMeshPrefixConfig &aOnMeshPrefixConfig);
     static bool IsValidOmrPrefix(const Ip6::Prefix &aOmrPrefix);
@@ -370,6 +371,7 @@
     // and updated with RA messages initiated from infra interface.
     RouterAdv::RouterAdvMessage mRouterAdvMessage;
     TimeMilli                   mTimeRouterAdvMessageLastUpdate;
+    bool                        mLearntRouterAdvMessageFromHost;
 
     TimerMilli mDiscoveredPrefixInvalidTimer;
     TimerMilli mDiscoveredPrefixStaleTimer;