text:
/**
 * @file
 *   This file implements MLE functionality required for the Thread Router and Leader roles.
 */

#if OPENTHREAD_FTD

#include "mle_router.hpp"

#include "common/code_utils.hpp"
#include "common/debug.hpp"
#include "common/encoding.hpp"
#include "common/instance.hpp"
#include "common/locator-getters.hpp"
#include "common/logging.hpp"
#include "common/random.hpp"
#include "common/settings.hpp"
#include "mac/mac_types.hpp"
#include "meshcop/meshcop.hpp"
#include "net/icmp6.hpp"
#include "thread/thread_netif.hpp"
#include "thread/thread_tlvs.hpp"
#include "thread/thread_uri_paths.hpp"
#include "thread/time_sync_service.hpp"
#include "utils/otns.hpp"

using ot::Encoding::BigEndian::HostSwap16;

namespace ot {
namespace Mle {

MleRouter::MleRouter(Instance &aInstance)
    : Mle(aInstance)
    , mAdvertiseTimer(aInstance, MleRouter::HandleAdvertiseTimer, nullptr, this)
    , mStateUpdateTimer(aInstance, MleRouter::HandleStateUpdateTimer, this)
    , mAddressSolicit(OT_URI_PATH_ADDRESS_SOLICIT, &MleRouter::HandleAddressSolicit, this)
    , mAddressRelease(OT_URI_PATH_ADDRESS_RELEASE, &MleRouter::HandleAddressRelease, this)
    , mChildTable(aInstance)
    , mRouterTable(aInstance)
    , mNeighborTableChangedCallback(nullptr)
    , mChallengeTimeout(0)
    , mNextChildId(kMaxChildId)
    , mNetworkIdTimeout(kNetworkIdTimeout)
    , mRouterUpgradeThreshold(kRouterUpgradeThreshold)
    , mRouterDowngradeThreshold(kRouterDowngradeThreshold)
    , mLeaderWeight(kLeaderWeight)
    , mFixedLeaderPartitionId(0)
    , mRouterEligible(true)
    , mAddressSolicitPending(false)
    , mAddressSolicitRejected(false)
    , mPreviousPartitionIdRouter(0)
    , mPreviousPartitionId(0)
    , mPreviousPartitionRouterIdSequence(0)
    , mPreviousPartitionIdTimeout(0)
    , mRouterSelectionJitter(kRouterSelectionJitter)
    , mRouterSelectionJitterTimeout(0)
    , mParentPriority(kParentPriorityUnspecified)
#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
    , mBackboneRouterRegistrationDelay(0)
#endif
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    , mMaxChildIpAddresses(0)
#endif
{
    mDeviceMode.Set(mDeviceMode.Get() | DeviceMode::kModeFullThreadDevice | DeviceMode::kModeFullNetworkData);

    SetRouterId(kInvalidRouterId);

#if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
    mSteeringData.Clear();
#endif
}

void MleRouter::HandlePartitionChange(void)
{
    mPreviousPartitionId               = mLeaderData.GetPartitionId();
    mPreviousPartitionRouterIdSequence = mRouterTable.GetRouterIdSequence();
    mPreviousPartitionIdTimeout        = GetNetworkIdTimeout();

    Get<AddressResolver>().Clear();
    IgnoreError(Get<Coap::Coap>().AbortTransaction(&MleRouter::HandleAddressSolicitResponse, this));
    mRouterTable.Clear();
}

bool MleRouter::IsRouterEligible(void) const
{
    return mRouterEligible && IsFullThreadDevice();
}

otError MleRouter::SetRouterEligible(bool aEligible)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(IsFullThreadDevice() || !aEligible, error = OT_ERROR_NOT_CAPABLE);

    mRouterEligible = aEligible;

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        break;

    case kRoleChild:
        Get<Mac::Mac>().SetBeaconEnabled(mRouterEligible);
        break;

    case kRoleRouter:
    case kRoleLeader:
        if (!mRouterEligible)
        {
            IgnoreError(BecomeDetached());
        }

        break;
    }

exit:
    return error;
}

otError MleRouter::BecomeRouter(ThreadStatusTlv::Status aStatus)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(!IsDisabled(), error = OT_ERROR_INVALID_STATE);
    VerifyOrExit(!IsRouter(), error = OT_ERROR_NONE);
    VerifyOrExit(IsRouterEligible(), error = OT_ERROR_NOT_CAPABLE);

    otLogInfoMle("Attempt to become router");

    Get<MeshForwarder>().SetRxOnWhenIdle(true);
    mRouterSelectionJitterTimeout = 0;

    switch (mRole)
    {
    case kRoleDetached:
        SuccessOrExit(error = SendLinkRequest(nullptr));
        mStateUpdateTimer.Start(kStateUpdatePeriod);
        break;

    case kRoleChild:
        SuccessOrExit(error = SendAddressSolicit(aStatus));
        break;

    default:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);
    }

exit:
    return error;
}

otError MleRouter::BecomeLeader(void)
{
    otError  error = OT_ERROR_NONE;
    Router * router;
    uint32_t partitionId;
    uint8_t  leaderId;

    VerifyOrExit(!Get<MeshCoP::ActiveDataset>().IsPartiallyComplete(), error = OT_ERROR_INVALID_STATE);
    VerifyOrExit(!IsDisabled(), error = OT_ERROR_INVALID_STATE);
    VerifyOrExit(!IsLeader(), error = OT_ERROR_NONE);
    VerifyOrExit(IsRouterEligible(), error = OT_ERROR_NOT_CAPABLE);

    mRouterTable.Clear();

    partitionId = mFixedLeaderPartitionId ? mFixedLeaderPartitionId : Random::NonCrypto::GetUint32();

    leaderId = IsRouterIdValid(mPreviousRouterId) ? mPreviousRouterId
                                                  : Random::NonCrypto::GetUint8InRange(0, kMaxRouterId + 1);

    SetLeaderData(partitionId, mLeaderWeight, leaderId);

    router = mRouterTable.Allocate(leaderId);
    OT_ASSERT(router != nullptr);

    SetRouterId(leaderId);
    router->SetExtAddress(Get<Mac::Mac>().GetExtAddress());

    Get<NetworkData::Leader>().Reset();
    Get<MeshCoP::Leader>().SetEmptyCommissionerData();

    SetStateLeader(Rloc16FromRouterId(leaderId));

exit:
    return error;
}

void MleRouter::StopLeader(void)
{
    Get<Coap::Coap>().RemoveResource(mAddressSolicit);
    Get<Coap::Coap>().RemoveResource(mAddressRelease);
    Get<MeshCoP::ActiveDataset>().StopLeader();
    Get<MeshCoP::PendingDataset>().StopLeader();
    StopAdvertiseTimer();
    Get<NetworkData::Leader>().Stop();
    Get<ThreadNetif>().UnsubscribeAllRoutersMulticast();
}

void MleRouter::HandleDetachStart(void)
{
    mRouterTable.ClearNeighbors();
    StopLeader();
    mStateUpdateTimer.Stop();
}

void MleRouter::HandleChildStart(AttachMode aMode)
{
    // reset `rejected` flag whenever REED becomes child.
    mAddressSolicitRejected = false;

    mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);

    StopLeader();
    mStateUpdateTimer.Start(kStateUpdatePeriod);

    if (mRouterEligible)
    {
        Get<Mac::Mac>().SetBeaconEnabled(true);
    }

    Get<ThreadNetif>().SubscribeAllRoutersMulticast();

    VerifyOrExit(IsRouterIdValid(mPreviousRouterId), OT_NOOP);

    switch (aMode)
    {
    case kAttachSameDowngrade:
        SendAddressRelease();

        // reset children info if any
        if (HasChildren())
        {
            RemoveChildren();
        }

        // reset routerId info
        SetRouterId(kInvalidRouterId);
        break;

    case kAttachSame1:
    case kAttachSame2:
        if (HasChildren())
        {
            IgnoreError(BecomeRouter(ThreadStatusTlv::kHaveChildIdRequest));
        }

        break;

    case kAttachAny:
        // If attach was started due to receiving MLE Announce Messages, all rx-on-when-idle devices would
        // start attach immediately when receiving such Announce message as in Thread 1.1 specification,
        // Section 4.8.1,
        // "If the received value is newer and the channel and/or PAN ID in the Announce message differ
        //  from those currently in use, the receiving device attempts to attach using the channel and
        //  PAN ID received from the Announce message."
        //
        // That is, Parent-child relationship is highly unlikely to be kept in the new partition, so here
        // removes all children, leaving whether to become router according to the new partition status.
        if (IsAnnounceAttach() && HasChildren())
        {
            RemoveChildren();
        }

        // fall through
    case kAttachBetter:
        if (HasChildren() && mPreviousPartitionIdRouter != mLeaderData.GetPartitionId())
        {
            IgnoreError(BecomeRouter(ThreadStatusTlv::kParentPartitionChange));
        }

        break;
    }

exit:

    if (mRouterTable.GetActiveRouterCount() >= mRouterUpgradeThreshold &&
        (!IsRouterIdValid(mPreviousRouterId) || !HasChildren()))
    {
        SetRouterId(kInvalidRouterId);
    }
}

void MleRouter::SetStateRouter(uint16_t aRloc16)
{
    SetRloc16(aRloc16);

    SetRole(kRoleRouter);
    SetAttachState(kAttachStateIdle);
    mAttachCounter = 0;
    mAttachTimer.Stop();
    mMessageTransmissionTimer.Stop();
    StopAdvertiseTimer();
    ResetAdvertiseInterval();

    Get<ThreadNetif>().SubscribeAllRoutersMulticast();
    mPreviousPartitionIdRouter = mLeaderData.GetPartitionId();
    Get<NetworkData::Leader>().Stop();
    Get<Ip6::Ip6>().SetForwardingEnabled(true);
    Get<Ip6::Mpl>().SetTimerExpirations(kMplRouterDataMessageTimerExpirations);
    Get<Mac::Mac>().SetBeaconEnabled(true);

    // remove children that do not have matching RLOC16
    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
    {
        if (RouterIdFromRloc16(iter.GetChild()->GetRloc16()) != mRouterId)
        {
            RemoveNeighbor(*iter.GetChild());
        }
    }
}

void MleRouter::SetStateLeader(uint16_t aRloc16)
{
    IgnoreError(Get<MeshCoP::ActiveDataset>().Restore());
    IgnoreError(Get<MeshCoP::PendingDataset>().Restore());
    SetRloc16(aRloc16);

    SetRole(kRoleLeader);
    SetAttachState(kAttachStateIdle);
    mAttachCounter = 0;
    mAttachTimer.Stop();
    mMessageTransmissionTimer.Stop();
    StopAdvertiseTimer();
    ResetAdvertiseInterval();
    IgnoreError(GetLeaderAloc(mLeaderAloc.GetAddress()));
    Get<ThreadNetif>().AddUnicastAddress(mLeaderAloc);

    Get<ThreadNetif>().SubscribeAllRoutersMulticast();
    mPreviousPartitionIdRouter = mLeaderData.GetPartitionId();
    mStateUpdateTimer.Start(kStateUpdatePeriod);

    Get<NetworkData::Leader>().Start();
    Get<MeshCoP::ActiveDataset>().StartLeader();
    Get<MeshCoP::PendingDataset>().StartLeader();
    Get<Coap::Coap>().AddResource(mAddressSolicit);
    Get<Coap::Coap>().AddResource(mAddressRelease);
    Get<Ip6::Ip6>().SetForwardingEnabled(true);
    Get<Ip6::Mpl>().SetTimerExpirations(kMplRouterDataMessageTimerExpirations);
    Get<Mac::Mac>().SetBeaconEnabled(true);
    Get<AddressResolver>().Clear();

    // remove children that do not have matching RLOC16
    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
    {
        if (RouterIdFromRloc16(iter.GetChild()->GetRloc16()) != mRouterId)
        {
            RemoveNeighbor(*iter.GetChild());
        }
    }

    otLogNoteMle("Leader partition id 0x%x", mLeaderData.GetPartitionId());
}

bool MleRouter::HandleAdvertiseTimer(TrickleTimer &aTimer)
{
    return aTimer.GetOwner<MleRouter>().HandleAdvertiseTimer();
}

bool MleRouter::HandleAdvertiseTimer(void)
{
    bool continueTrickle = true;

    VerifyOrExit(IsRouterEligible(), continueTrickle = false);

    SendAdvertisement();

exit:
    return continueTrickle;
}

void MleRouter::StopAdvertiseTimer(void)
{
    mAdvertiseTimer.Stop();
}

void MleRouter::ResetAdvertiseInterval(void)
{
    VerifyOrExit(IsRouterOrLeader(), OT_NOOP);

    if (!mAdvertiseTimer.IsRunning())
    {
        mAdvertiseTimer.Start(Time::SecToMsec(kAdvertiseIntervalMin), Time::SecToMsec(kAdvertiseIntervalMax),
                              TrickleTimer::kModeNormal);
    }

    mAdvertiseTimer.IndicateInconsistent();

exit:
    return;
}

void MleRouter::SendAdvertisement(void)
{
    otError      error = OT_ERROR_NONE;
    Ip6::Address destination;
    Message *    message = nullptr;

    // Suppress MLE Advertisements when trying to attach to a better partition.
    //
    // Without this suppression, a device may send an MLE Advertisement before receiving the MLE Child ID Response.
    // The candidate parent then removes the attaching device because the Source Address TLV includes an RLOC16 that
    // indicates a Router role (i.e. a Child ID equal to zero).
    VerifyOrExit(!IsAttaching(), OT_NOOP);

    // Suppress MLE Advertisements when transitioning to the router role.
    //
    // When trying to attach to a new partition, sending out advertisements as a REED can cause already-attached
    // children to detach.
    VerifyOrExit(!mAddressSolicitPending, OT_NOOP);

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandAdvertisement));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendLeaderData(*message));

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);

    case kRoleChild:
        break;

    case kRoleRouter:
    case kRoleLeader:
        SuccessOrExit(error = AppendRoute(*message));
        break;
    }

    destination.SetToLinkLocalAllNodesMulticast();
    SuccessOrExit(error = SendMessage(*message, destination));

    LogMleMessage("Send Advertisement", destination);

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to send Advertisement: %s", otThreadErrorToString(error));

        if (message != nullptr)
        {
            message->Free();
        }
    }
}

otError MleRouter::SendLinkRequest(Neighbor *aNeighbor)
{
    static const uint8_t detachedTlvs[]      = {Tlv::kAddress16, Tlv::kRoute};
    static const uint8_t routerTlvs[]        = {Tlv::kLinkMargin};
    static const uint8_t validNeighborTlvs[] = {Tlv::kLinkMargin, Tlv::kRoute};
    otError              error               = OT_ERROR_NONE;
    Message *            message;
    Ip6::Address         destination;

    destination.Clear();

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandLinkRequest));
    SuccessOrExit(error = AppendVersion(*message));

    switch (mRole)
    {
    case kRoleDisabled:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);

    case kRoleDetached:
        SuccessOrExit(error = AppendTlvRequest(*message, detachedTlvs, sizeof(detachedTlvs)));
        break;

    case kRoleChild:
        SuccessOrExit(error = AppendSourceAddress(*message));
        SuccessOrExit(error = AppendLeaderData(*message));
        break;

    case kRoleRouter:
    case kRoleLeader:
        if (aNeighbor == nullptr || !aNeighbor->IsStateValid())
        {
            SuccessOrExit(error = AppendTlvRequest(*message, routerTlvs, sizeof(routerTlvs)));
        }
        else
        {
            SuccessOrExit(error = AppendTlvRequest(*message, validNeighborTlvs, sizeof(validNeighborTlvs)));
        }

        SuccessOrExit(error = AppendSourceAddress(*message));
        SuccessOrExit(error = AppendLeaderData(*message));
        break;
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    SuccessOrExit(error = AppendTimeRequest(*message));
#endif

    if (aNeighbor == nullptr)
    {
        mChallenge.GenerateRandom();
        mChallengeTimeout = (((2 * kMaxResponseDelay) + kStateUpdatePeriod - 1) / kStateUpdatePeriod);

        SuccessOrExit(error = AppendChallenge(*message, mChallenge));
        destination.SetToLinkLocalAllRoutersMulticast();
    }
    else
    {
        if (!aNeighbor->IsStateValid())
        {
            aNeighbor->GenerateChallenge();
            SuccessOrExit(error = AppendChallenge(*message, aNeighbor->GetChallenge(), aNeighbor->GetChallengeSize()));
        }
        else
        {
            Challenge challenge;

            challenge.GenerateRandom();
            SuccessOrExit(error = AppendChallenge(*message, challenge));
        }

        destination.SetToLinkLocalAddress(aNeighbor->GetExtAddress());
    }

    SuccessOrExit(error = SendMessage(*message, destination));

    LogMleMessage("Send Link Request", destination);

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}

void MleRouter::HandleLinkRequest(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo, Neighbor *aNeighbor)
{
    otError       error    = OT_ERROR_NONE;
    Neighbor *    neighbor = nullptr;
    Challenge     challenge;
    uint16_t      version;
    LeaderData    leaderData;
    uint16_t      sourceAddress;
    RequestedTlvs requestedTlvs;
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    TimeRequestTlv timeRequest;
#endif

    LogMleMessage("Receive Link Request", aMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterOrLeader(), error = OT_ERROR_INVALID_STATE);

    VerifyOrExit(!IsAttaching(), error = OT_ERROR_INVALID_STATE);

    // Challenge
    SuccessOrExit(error = ReadChallenge(aMessage, challenge));

    // Version
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kVersion, version));
    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = OT_ERROR_PARSE);

    // Leader Data
    switch (ReadLeaderData(aMessage, leaderData))
    {
    case OT_ERROR_NONE:
        VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId(), error = OT_ERROR_INVALID_STATE);
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Source Address
    switch (Tlv::FindUint16Tlv(aMessage, Tlv::kSourceAddress, sourceAddress))
    {
    case OT_ERROR_NONE:
        if (IsActiveRouter(sourceAddress))
        {
            Mac::ExtAddress macAddr;

            aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);

            neighbor = mRouterTable.GetRouter(RouterIdFromRloc16(sourceAddress));
            VerifyOrExit(neighbor != nullptr, error = OT_ERROR_PARSE);
            VerifyOrExit(!neighbor->IsStateLinkRequest(), error = OT_ERROR_ALREADY);

            if (!neighbor->IsStateValid())
            {
                const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());

                neighbor->SetExtAddress(macAddr);
                neighbor->GetLinkInfo().Clear();
                neighbor->GetLinkInfo().AddRss(linkInfo->mRss);
                neighbor->ResetLinkFailures();
                neighbor->SetLastHeard(TimerMilli::GetNow());
                neighbor->SetState(Neighbor::kStateLinkRequest);
            }
            else
            {
                VerifyOrExit(neighbor->GetExtAddress() == macAddr, OT_NOOP);
            }
        }

        break;

    case OT_ERROR_NOT_FOUND:
        // lack of source address indicates router coming out of reset
        VerifyOrExit(aNeighbor && aNeighbor->IsStateValid() && IsActiveRouter(aNeighbor->GetRloc16()),
                     error = OT_ERROR_DROP);
        neighbor = aNeighbor;
        break;

    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // TLV Request
    switch (FindTlvRequest(aMessage, requestedTlvs))
    {
    case OT_ERROR_NONE:
        break;
    case OT_ERROR_NOT_FOUND:
        requestedTlvs.mNumTlvs = 0;
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (neighbor != nullptr)
    {
        if (Tlv::FindTlv(aMessage, Tlv::kTimeRequest, sizeof(timeRequest), timeRequest) == OT_ERROR_NONE)
        {
            neighbor->SetTimeSyncEnabled(true);
        }
        else
        {
            neighbor->SetTimeSyncEnabled(false);
        }
    }
#endif

    SuccessOrExit(error = SendLinkAccept(aMessageInfo, neighbor, requestedTlvs, challenge));

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogNoteMle("Failed to process Link Request: %s", otThreadErrorToString(error));
    }
}

otError MleRouter::SendLinkAccept(const Ip6::MessageInfo &aMessageInfo,
                                  Neighbor *              aNeighbor,
                                  const RequestedTlvs &   aRequestedTlvs,
                                  const Challenge &       aChallenge)
{
    otError                 error        = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo     = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    static const uint8_t    routerTlvs[] = {Tlv::kLinkMargin};
    Message *               message;
    Header::Command         command;
    uint8_t                 linkMargin;

    command = (aNeighbor == nullptr || aNeighbor->IsStateValid()) ? Header::kCommandLinkAccept
                                                                  : Header::kCommandLinkAcceptAndRequest;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, command));
    SuccessOrExit(error = AppendVersion(*message));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendResponse(*message, aChallenge));
    SuccessOrExit(error = AppendLinkFrameCounter(*message));
    SuccessOrExit(error = AppendMleFrameCounter(*message));

    // always append a link margin, regardless of whether or not it was requested
    linkMargin = LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), linkInfo->mRss);

    SuccessOrExit(error = AppendLinkMargin(*message, linkMargin));

    if (aNeighbor != nullptr && IsActiveRouter(aNeighbor->GetRloc16()))
    {
        SuccessOrExit(error = AppendLeaderData(*message));
    }

    for (uint8_t i = 0; i < aRequestedTlvs.mNumTlvs; i++)
    {
        switch (aRequestedTlvs.mTlvs[i])
        {
        case Tlv::kRoute:
            SuccessOrExit(error = AppendRoute(*message));
            break;

        case Tlv::kAddress16:
            VerifyOrExit(aNeighbor != nullptr, error = OT_ERROR_DROP);
            SuccessOrExit(error = AppendAddress16(*message, aNeighbor->GetRloc16()));
            break;

        case Tlv::kLinkMargin:
            break;

        default:
            ExitNow(error = OT_ERROR_DROP);
        }
    }

    if (aNeighbor != nullptr && !aNeighbor->IsStateValid())
    {
        aNeighbor->GenerateChallenge();

        SuccessOrExit(error = AppendChallenge(*message, aNeighbor->GetChallenge(), aNeighbor->GetChallengeSize()));
        SuccessOrExit(error = AppendTlvRequest(*message, routerTlvs, sizeof(routerTlvs)));
        aNeighbor->SetLastHeard(TimerMilli::GetNow());
        aNeighbor->SetState(Neighbor::kStateLinkRequest);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (aNeighbor != nullptr && aNeighbor->IsTimeSyncEnabled())
    {
        message->SetTimeSync(true);
    }
#endif

    if (aMessageInfo.GetSockAddr().IsMulticast())
    {
        SuccessOrExit(error = AddDelayedResponse(*message, aMessageInfo.GetPeerAddr(),
                                                 1 + Random::NonCrypto::GetUint16InRange(0, kMaxResponseDelay)));

        LogMleMessage("Delay Link Accept", aMessageInfo.GetPeerAddr());
    }
    else
    {
        SuccessOrExit(error = SendMessage(*message, aMessageInfo.GetPeerAddr()));

        LogMleMessage("Send Link Accept", aMessageInfo.GetPeerAddr());
    }

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}

void MleRouter::HandleLinkAccept(const Message &         aMessage,
                                 const Ip6::MessageInfo &aMessageInfo,
                                 uint32_t                aKeySequence,
                                 Neighbor *              aNeighbor)
{
    otError error = HandleLinkAccept(aMessage, aMessageInfo, aKeySequence, aNeighbor, false);

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Link Accept: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleLinkAcceptAndRequest(const Message &         aMessage,
                                           const Ip6::MessageInfo &aMessageInfo,
                                           uint32_t                aKeySequence,
                                           Neighbor *              aNeighbor)
{
    otError error = HandleLinkAccept(aMessage, aMessageInfo, aKeySequence, aNeighbor, true);

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Link Accept and Request: %s", otThreadErrorToString(error));
    }
}

otError MleRouter::HandleLinkAccept(const Message &         aMessage,
                                    const Ip6::MessageInfo &aMessageInfo,
                                    uint32_t                aKeySequence,
                                    Neighbor *              aNeighbor,
                                    bool                    aRequest)
{
    static const uint8_t dataRequestTlvs[] = {Tlv::kNetworkData};

    otError                 error    = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    Router *                router;
    Neighbor::State         neighborState;
    Mac::ExtAddress         macAddr;
    uint16_t                version;
    Challenge               response;
    uint16_t                sourceAddress;
    uint32_t                linkFrameCounter;
    uint32_t                mleFrameCounter;
    uint8_t                 routerId;
    uint16_t                address16;
    RouteTlv                route;
    LeaderData              leaderData;
    uint8_t                 linkMargin;

    // Source Address
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kSourceAddress, sourceAddress));

    if (aRequest)
    {
        LogMleMessage("Receive Link Accept and Request", aMessageInfo.GetPeerAddr(), sourceAddress);
    }
    else
    {
        LogMleMessage("Receive Link Accept", aMessageInfo.GetPeerAddr(), sourceAddress);
    }

    VerifyOrExit(IsActiveRouter(sourceAddress), error = OT_ERROR_PARSE);

    routerId      = RouterIdFromRloc16(sourceAddress);
    router        = mRouterTable.GetRouter(routerId);
    neighborState = (router != nullptr) ? router->GetState() : Neighbor::kStateInvalid;

    // Response
    SuccessOrExit(error = ReadResponse(aMessage, response));

    // verify response
    switch (neighborState)
    {
    case Neighbor::kStateLinkRequest:
        VerifyOrExit(response.Matches(router->GetChallenge(), router->GetChallengeSize()), error = OT_ERROR_SECURITY);
        break;

    case Neighbor::kStateInvalid:
        VerifyOrExit((mChallengeTimeout > 0) && (response == mChallenge), error = OT_ERROR_SECURITY);

    case Neighbor::kStateValid:
        break;

    default:
        ExitNow(error = OT_ERROR_SECURITY);
    }

    // Remove stale neighbors
    if (aNeighbor && aNeighbor->GetRloc16() != sourceAddress)
    {
        RemoveNeighbor(*aNeighbor);
    }

    // Version
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kVersion, version));
    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = OT_ERROR_PARSE);

    // Link-Layer Frame Counter
    SuccessOrExit(error = Tlv::FindUint32Tlv(aMessage, Tlv::kLinkFrameCounter, linkFrameCounter));

    // MLE Frame Counter
    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kMleFrameCounter, mleFrameCounter))
    {
    case OT_ERROR_NONE:
        break;
    case OT_ERROR_NOT_FOUND:
        mleFrameCounter = linkFrameCounter;
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Link Margin
    switch (Tlv::FindUint8Tlv(aMessage, Tlv::kLinkMargin, linkMargin))
    {
    case OT_ERROR_NONE:
        break;
    case OT_ERROR_NOT_FOUND:
        // Link Margin TLV may be skipped in Router Synchronization process after Reset
        VerifyOrExit(IsDetached(), error = OT_ERROR_NOT_FOUND);
        // Wait for an MLE Advertisement to establish a routing cost to the neighbor
        linkMargin = 0;
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    switch (mRole)
    {
    case kRoleDisabled:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);

    case kRoleDetached:
        // Address16
        SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kAddress16, address16));
        VerifyOrExit(GetRloc16() == address16, error = OT_ERROR_DROP);

        // Leader Data
        SuccessOrExit(error = ReadLeaderData(aMessage, leaderData));
        SetLeaderData(leaderData.GetPartitionId(), leaderData.GetWeighting(), leaderData.GetLeaderRouterId());

        // Route
        SuccessOrExit(error = Tlv::FindTlv(aMessage, Tlv::kRoute, sizeof(route), route));
        VerifyOrExit(route.IsValid(), error = OT_ERROR_PARSE);
        mRouterTable.Clear();
        SuccessOrExit(error = ProcessRouteTlv(route));
        router = mRouterTable.GetRouter(routerId);
        VerifyOrExit(router != nullptr, OT_NOOP);

        if (mLeaderData.GetLeaderRouterId() == RouterIdFromRloc16(GetRloc16()))
        {
            SetStateLeader(GetRloc16());
        }
        else
        {
            SetStateRouter(GetRloc16());
        }

        mRetrieveNewNetworkData = true;
        IgnoreError(SendDataRequest(aMessageInfo.GetPeerAddr(), dataRequestTlvs, sizeof(dataRequestTlvs), 0));

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
        Get<TimeSync>().HandleTimeSyncMessage(aMessage);
#endif
        break;

    case kRoleChild:
        VerifyOrExit(router != nullptr, OT_NOOP);
        break;

    case kRoleRouter:
    case kRoleLeader:
        VerifyOrExit(router != nullptr, OT_NOOP);

        // Leader Data
        SuccessOrExit(error = ReadLeaderData(aMessage, leaderData));
        VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId(), OT_NOOP);

        if (mRetrieveNewNetworkData ||
            (static_cast<int8_t>(leaderData.GetDataVersion() - Get<NetworkData::Leader>().GetVersion()) > 0))
        {
            IgnoreError(SendDataRequest(aMessageInfo.GetPeerAddr(), dataRequestTlvs, sizeof(dataRequestTlvs), 0));
        }

        // Route (optional)
        if (Tlv::FindTlv(aMessage, Tlv::kRoute, sizeof(route), route) == OT_ERROR_NONE)
        {
            VerifyOrExit(route.IsValid(), error = OT_ERROR_PARSE);
            SuccessOrExit(error = ProcessRouteTlv(route));
            UpdateRoutes(route, routerId);
        }

        // update routing table
        if (routerId != mRouterId && !IsRouterIdValid(router->GetNextHop()))
        {
            ResetAdvertiseInterval();
        }

        break;
    }

    // finish link synchronization
    aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);
    router->SetExtAddress(macAddr);
    router->SetRloc16(sourceAddress);
    router->SetLinkFrameCounter(linkFrameCounter);
    router->SetMleFrameCounter(mleFrameCounter);
    router->SetLastHeard(TimerMilli::GetNow());
    router->SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                     DeviceMode::kModeFullNetworkData));
    router->GetLinkInfo().Clear();
    router->GetLinkInfo().AddRss(linkInfo->mRss);
    router->SetLinkQualityOut(LinkQualityInfo::ConvertLinkMarginToLinkQuality(linkMargin));
    router->ResetLinkFailures();
    router->SetState(Neighbor::kStateValid);
    router->SetKeySequence(aKeySequence);

    Signal(OT_NEIGHBOR_TABLE_EVENT_ROUTER_ADDED, *router);

    if (aRequest)
    {
        Challenge     challenge;
        RequestedTlvs requestedTlvs;

        // Challenge
        SuccessOrExit(error = ReadChallenge(aMessage, challenge));

        // TLV Request
        switch (FindTlvRequest(aMessage, requestedTlvs))
        {
        case OT_ERROR_NONE:
            break;
        case OT_ERROR_NOT_FOUND:
            requestedTlvs.mNumTlvs = 0;
            break;
        default:
            ExitNow(error = OT_ERROR_PARSE);
        }

        SuccessOrExit(error = SendLinkAccept(aMessageInfo, router, requestedTlvs, challenge));
    }

exit:
    return error;
}

uint8_t MleRouter::LinkQualityToCost(uint8_t aLinkQuality)
{
    uint8_t rval;

    switch (aLinkQuality)
    {
    case 1:
        rval = kLinkQuality1LinkCost;
        break;

    case 2:
        rval = kLinkQuality2LinkCost;
        break;

    case 3:
        rval = kLinkQuality3LinkCost;
        break;

    default:
        rval = kLinkQuality0LinkCost;
        break;
    }

    return rval;
}

uint8_t MleRouter::GetLinkCost(uint8_t aRouterId)
{
    uint8_t rval = kMaxRouteCost;
    Router *router;

    router = mRouterTable.GetRouter(aRouterId);

    // nullptr aRouterId indicates non-existing next hop, hence return kMaxRouteCost for it.
    VerifyOrExit(router != nullptr, OT_NOOP);

    rval = mRouterTable.GetLinkCost(*router);

exit:
    return rval;
}

otError MleRouter::SetRouterSelectionJitter(uint8_t aRouterJitter)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aRouterJitter > 0, error = OT_ERROR_INVALID_ARGS);

    mRouterSelectionJitter = aRouterJitter;

exit:
    return error;
}

otError MleRouter::ProcessRouteTlv(const RouteTlv &aRoute)
{
    otError error = OT_ERROR_NONE;

    mRouterTable.UpdateRouterIdSet(aRoute.GetRouterIdSequence(), aRoute.GetRouterIdMask());

    if (IsRouter() && !mRouterTable.IsAllocated(mRouterId))
    {
        IgnoreError(BecomeDetached());
        error = OT_ERROR_NO_ROUTE;
    }

    return error;
}

bool MleRouter::IsSingleton(void)
{
    bool rval = true;

    if (IsAttached() && IsRouterEligible())
    {
        // not a singleton if any other routers exist
        if (mRouterTable.GetActiveRouterCount() > 1)
        {
            ExitNow(rval = false);
        }
    }

exit:
    return rval;
}

int MleRouter::ComparePartitions(bool              aSingletonA,
                                 const LeaderData &aLeaderDataA,
                                 bool              aSingletonB,
                                 const LeaderData &aLeaderDataB)
{
    int rval = 0;

    if (aSingletonA != aSingletonB)
    {
        ExitNow(rval = aSingletonB ? 1 : -1);
    }

    if (aLeaderDataA.GetWeighting() != aLeaderDataB.GetWeighting())
    {
        ExitNow(rval = aLeaderDataA.GetWeighting() > aLeaderDataB.GetWeighting() ? 1 : -1);
    }

    if (aLeaderDataA.GetPartitionId() != aLeaderDataB.GetPartitionId())
    {
        ExitNow(rval = aLeaderDataA.GetPartitionId() > aLeaderDataB.GetPartitionId() ? 1 : -1);
    }

exit:
    return rval;
}

bool MleRouter::IsSingleton(const RouteTlv &aRouteTlv)
{
    bool    rval  = true;
    uint8_t count = 0;

    // REEDs do not include a Route TLV and indicate not a singleton
    if (!aRouteTlv.IsValid())
    {
        ExitNow(rval = false);
    }

    // Check if 2 or more active routers
    for (uint8_t routerId = 0; routerId <= kMaxRouterId; routerId++)
    {
        if (aRouteTlv.IsRouterIdSet(routerId) && (++count >= 2))
        {
            ExitNow(rval = false);
        }
    }

exit:
    return rval;
}

otError MleRouter::HandleAdvertisement(const Message &         aMessage,
                                       const Ip6::MessageInfo &aMessageInfo,
                                       Neighbor *              aNeighbor)
{
    otError                 error    = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    uint8_t linkMargin = LinkQualityInfo::ConvertRssToLinkMargin(Get<Mac::Mac>().GetNoiseFloor(), linkInfo->mRss);
    Mac::ExtAddress macAddr;
    uint16_t        sourceAddress = Mac::kShortAddrInvalid;
    LeaderData      leaderData;
    RouteTlv        route;
    uint32_t        partitionId;
    Router *        router;
    uint8_t         routerId;
    uint8_t         routerCount;

    aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);

    // Source Address
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kSourceAddress, sourceAddress));

    // Leader Data
    SuccessOrExit(error = ReadLeaderData(aMessage, leaderData));

    // Route Data (optional)
    if (Tlv::FindTlv(aMessage, Tlv::kRoute, sizeof(route), route) == OT_ERROR_NONE)
    {
        VerifyOrExit(route.IsValid(), error = OT_ERROR_PARSE);
    }
    else
    {
        // mark that a Route TLV was not included
        route.SetLength(0);
    }

    partitionId = leaderData.GetPartitionId();

    if (partitionId != mLeaderData.GetPartitionId())
    {
        otLogNoteMle("Different partition (peer:%u, local:%u)", partitionId, mLeaderData.GetPartitionId());

        VerifyOrExit(linkMargin >= OPENTHREAD_CONFIG_MLE_PARTITION_MERGE_MARGIN_MIN, error = OT_ERROR_LINK_MARGIN_LOW);

        if (route.IsValid() && IsFullThreadDevice() && (mPreviousPartitionIdTimeout > 0) &&
            (partitionId == mPreviousPartitionId))
        {
            VerifyOrExit((static_cast<int8_t>(route.GetRouterIdSequence() - mPreviousPartitionRouterIdSequence) > 0),
                         error = OT_ERROR_DROP);
        }

        if (IsChild() && (aNeighbor == &mParent || !IsFullThreadDevice()))
        {
            ExitNow();
        }

        if (ComparePartitions(IsSingleton(route), leaderData, IsSingleton(), mLeaderData) > 0
#if OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
            // if time sync is required, it will only migrate to a better network which also enables time sync.
            && aMessage.GetTimeSyncSeq() != OT_TIME_SYNC_INVALID_SEQ
#endif
        )
        {
            IgnoreError(BecomeChild(kAttachBetter));
        }

        ExitNow(error = OT_ERROR_DROP);
    }
    else if (leaderData.GetLeaderRouterId() != GetLeaderId())
    {
        VerifyOrExit(aNeighbor && aNeighbor->IsStateValid(), OT_NOOP);

        if (!IsChild())
        {
            otLogInfoMle("Leader ID mismatch");
            IgnoreError(BecomeDetached());
            error = OT_ERROR_DROP;
        }

        ExitNow();
    }

    VerifyOrExit(IsActiveRouter(sourceAddress) && route.IsValid(), OT_NOOP);
    routerId = RouterIdFromRloc16(sourceAddress);

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    Get<TimeSync>().HandleTimeSyncMessage(aMessage);
#endif

    if (IsFullThreadDevice() && (aNeighbor && aNeighbor->IsStateValid()) &&
        ((mRouterTable.GetActiveRouterCount() == 0) ||
         (static_cast<int8_t>(route.GetRouterIdSequence() - mRouterTable.GetRouterIdSequence()) > 0)))
    {
        bool processRouteTlv = false;

        switch (mRole)
        {
        case kRoleDisabled:
        case kRoleDetached:
            break;

        case kRoleChild:
            if (sourceAddress == mParent.GetRloc16())
            {
                processRouteTlv = true;
            }
            else
            {
                router = mRouterTable.GetRouter(routerId);

                if (router != nullptr && router->IsStateValid())
                {
                    processRouteTlv = true;
                }
            }

            break;

        case kRoleRouter:
        case kRoleLeader:
            processRouteTlv = true;
            break;
        }

        if (processRouteTlv)
        {
            SuccessOrExit(error = ProcessRouteTlv(route));
            if (Get<RouterTable>().Contains(*aNeighbor))
            {
                aNeighbor = nullptr; // aNeighbor is no longer valid after `ProcessRouteTlv`
            }
        }
    }

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        ExitNow();

    case kRoleChild:
        if (aNeighbor == &mParent)
        {
            // MLE Advertisement from parent
            router = &mParent;

            if (mParent.GetRloc16() != sourceAddress)
            {
                IgnoreError(BecomeDetached());
                ExitNow(error = OT_ERROR_NO_ROUTE);
            }

            if (IsFullThreadDevice())
            {
                Router *leader;

                if ((mRouterSelectionJitterTimeout == 0) &&
                    (mRouterTable.GetActiveRouterCount() < mRouterUpgradeThreshold))
                {
                    mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
                    ExitNow();
                }

                leader = mRouterTable.GetLeader();

                if (leader != nullptr)
                {
                    for (uint8_t id = 0, routeCount = 0; id <= kMaxRouterId; id++)
                    {
                        if (!route.IsRouterIdSet(id))
                        {
                            continue;
                        }

                        if (id != GetLeaderId())
                        {
                            routeCount++;
                            continue;
                        }

                        if (route.GetRouteCost(routeCount) > 0)
                        {
                            leader->SetNextHop(id);
                            leader->SetCost(route.GetRouteCost(routeCount));
                        }
                        else
                        {
                            leader->SetNextHop(kInvalidRouterId);
                            leader->SetCost(0);
                        }

                        break;
                    }
                }
            }
        }
        else
        {
            // MLE Advertisement not from parent, but from some other neighboring router
            router = mRouterTable.GetRouter(routerId);
            VerifyOrExit(router != nullptr, OT_NOOP);

            if (IsFullThreadDevice() && !router->IsStateValid() && !router->IsStateLinkRequest() &&
                (mRouterTable.GetActiveLinkCount() < OPENTHREAD_CONFIG_MLE_CHILD_ROUTER_LINKS))
            {
                router->SetExtAddress(macAddr);
                router->GetLinkInfo().Clear();
                router->GetLinkInfo().AddRss(linkInfo->mRss);
                router->ResetLinkFailures();
                router->SetLastHeard(TimerMilli::GetNow());
                router->SetState(Neighbor::kStateLinkRequest);
                IgnoreError(SendLinkRequest(router));
                ExitNow(error = OT_ERROR_NO_ROUTE);
            }
        }

        router->SetLastHeard(TimerMilli::GetNow());

        ExitNow();

    case kRoleRouter:
        router = mRouterTable.GetRouter(routerId);
        VerifyOrExit(router != nullptr, OT_NOOP);

        // check current active router number
        routerCount = 0;

        for (uint8_t id = 0; id <= kMaxRouterId; id++)
        {
            if (route.IsRouterIdSet(id))
            {
                routerCount++;
            }
        }

        if (routerCount > mRouterDowngradeThreshold && mRouterSelectionJitterTimeout == 0 &&
            HasMinDowngradeNeighborRouters() && HasSmallNumberOfChildren() &&
            HasOneNeighborWithComparableConnectivity(route, routerId))
        {
            mRouterSelectionJitterTimeout = 1 + Random::NonCrypto::GetUint8InRange(0, mRouterSelectionJitter);
        }

        // fall through

    case kRoleLeader:
        router = mRouterTable.GetRouter(routerId);
        VerifyOrExit(router != nullptr, OT_NOOP);

        // Send unicast link request if no link to router and no unicast/multicast link request in progress
        if (!router->IsStateValid() && !router->IsStateLinkRequest() && (mChallengeTimeout == 0) &&
            (linkMargin >= OPENTHREAD_CONFIG_MLE_LINK_REQUEST_MARGIN_MIN))
        {
            router->SetExtAddress(macAddr);
            router->GetLinkInfo().Clear();
            router->GetLinkInfo().AddRss(linkInfo->mRss);
            router->ResetLinkFailures();
            router->SetLastHeard(TimerMilli::GetNow());
            router->SetState(Neighbor::kStateLinkRequest);
            IgnoreError(SendLinkRequest(router));
            ExitNow(error = OT_ERROR_NO_ROUTE);
        }

        router->SetLastHeard(TimerMilli::GetNow());
        break;
    }

    UpdateRoutes(route, routerId);

exit:
    if (aNeighbor && aNeighbor->GetRloc16() != sourceAddress)
    {
        // Remove stale neighbors
        RemoveNeighbor(*aNeighbor);
    }

    return error;
}

void MleRouter::UpdateRoutes(const RouteTlv &aRoute, uint8_t aRouterId)
{
    Router *neighbor;
    bool    resetAdvInterval = false;
    bool    changed          = false;

    neighbor = mRouterTable.GetRouter(aRouterId);
    VerifyOrExit(neighbor != nullptr, OT_NOOP);

    // update link quality out to neighbor
    changed = UpdateLinkQualityOut(aRoute, *neighbor, resetAdvInterval);

    // update routes
    for (uint8_t routerId = 0, routeCount = 0; routerId <= kMaxRouterId; routerId++)
    {
        Router *router;
        Router *nextHop;
        uint8_t oldNextHop;
        uint8_t cost;

        if (!aRoute.IsRouterIdSet(routerId))
        {
            continue;
        }

        router = mRouterTable.GetRouter(routerId);

        if (router == nullptr || router->GetRloc16() == GetRloc16() || router == neighbor)
        {
            routeCount++;
            continue;
        }

        oldNextHop = router->GetNextHop();
        nextHop    = mRouterTable.GetRouter(oldNextHop);

        cost = aRoute.GetRouteCost(routeCount);

        if (cost == 0)
        {
            cost = kMaxRouteCost;
        }

        if (nextHop == nullptr || nextHop == neighbor)
        {
            // route has no next hop or next hop is neighbor (sender)

            if (cost + mRouterTable.GetLinkCost(*neighbor) < kMaxRouteCost)
            {
                if (nextHop == nullptr && mRouterTable.GetLinkCost(*router) >= kMaxRouteCost)
                {
                    resetAdvInterval = true;
                }

                router->SetNextHop(aRouterId);
                router->SetCost(cost);
                changed = true;
            }
            else if (nextHop == neighbor)
            {
                if (mRouterTable.GetLinkCost(*router) >= kMaxRouteCost)
                {
                    resetAdvInterval = true;
                }

                router->SetNextHop(kInvalidRouterId);
                router->SetCost(0);
                router->SetLastHeard(TimerMilli::GetNow());
                changed = true;
            }
        }
        else
        {
            uint8_t curCost = router->GetCost() + mRouterTable.GetLinkCost(*nextHop);
            uint8_t newCost = cost + mRouterTable.GetLinkCost(*neighbor);

            if (newCost < curCost)
            {
                router->SetNextHop(aRouterId);
                router->SetCost(cost);
                changed = true;
            }
        }

        routeCount++;
    }

    if (resetAdvInterval)
    {
        ResetAdvertiseInterval();
    }

#if (OPENTHREAD_CONFIG_LOG_MLE && (OPENTHREAD_CONFIG_LOG_LEVEL >= OT_LOG_LEVEL_INFO))

    VerifyOrExit(changed, OT_NOOP);
    otLogInfoMle("Route table updated");

    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Router &router = *iter.GetRouter();

        otLogInfoMle("    %04x -> %04x, cost:%d %d, lqin:%d, lqout:%d, link:%s", router.GetRloc16(),
                     (router.GetNextHop() == kInvalidRouterId) ? 0xffff : Rloc16FromRouterId(router.GetNextHop()),
                     router.GetCost(), mRouterTable.GetLinkCost(router), router.GetLinkInfo().GetLinkQuality(),
                     router.GetLinkQualityOut(),
                     router.GetRloc16() == GetRloc16() ? "device" : (router.IsStateValid() ? "yes" : "no"));
    }

#else
    OT_UNUSED_VARIABLE(changed);
#endif

exit:
    return;
}

bool MleRouter::UpdateLinkQualityOut(const RouteTlv &aRoute, Router &aNeighbor, bool &aResetAdvInterval)
{
    bool    changed = false;
    uint8_t linkQuality;
    uint8_t myRouterId;
    uint8_t myRouteCount;
    uint8_t oldLinkCost;
    Router *nextHop;

    myRouterId = RouterIdFromRloc16(GetRloc16());
    VerifyOrExit(aRoute.IsRouterIdSet(myRouterId), OT_NOOP);

    myRouteCount = 0;
    for (uint8_t routerId = 0; routerId < myRouterId; routerId++)
    {
        myRouteCount += aRoute.IsRouterIdSet(routerId);
    }

    linkQuality = aRoute.GetLinkQualityIn(myRouteCount);
    VerifyOrExit(aNeighbor.GetLinkQualityOut() != linkQuality, OT_NOOP);

    oldLinkCost = mRouterTable.GetLinkCost(aNeighbor);

    aNeighbor.SetLinkQualityOut(linkQuality);
    nextHop = mRouterTable.GetRouter(aNeighbor.GetNextHop());

    // reset MLE advertisement timer if neighbor route cost changed to or from infinite
    if (nextHop == nullptr && (oldLinkCost >= kMaxRouteCost) != (mRouterTable.GetLinkCost(aNeighbor) >= kMaxRouteCost))
    {
        aResetAdvInterval = true;
    }
    changed = true;

exit:
    return changed;
}

void MleRouter::HandleParentRequest(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    otError                 error    = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    Mac::ExtAddress         macAddr;
    uint16_t                version;
    uint8_t                 scanMask;
    Challenge               challenge;
    Router *                leader;
    Child *                 child;
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    TimeRequestTlv timeRequest;
#endif

    LogMleMessage("Receive Parent Request", aMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterEligible(), error = OT_ERROR_INVALID_STATE);

    // A Router/REED MUST NOT send an MLE Parent Response if:

    // 0. It is detached or attempting to another partition
    VerifyOrExit(!IsDetached() && !IsAttaching(), error = OT_ERROR_DROP);

    // 1. It has no available Child capacity (if Max Child Count minus
    // Child Count would be equal to zero)
    // ==> verified below when allocating a child entry

    // 2. It is disconnected from its Partition (that is, it has not
    // received an updated ID sequence number within LEADER_TIMEOUT
    // seconds)
    VerifyOrExit(mRouterTable.GetLeaderAge() < mNetworkIdTimeout, error = OT_ERROR_DROP);

    // 3. Its current routing path cost to the Leader is infinite.
    leader = mRouterTable.GetLeader();
    OT_ASSERT(leader != nullptr);

    VerifyOrExit(IsLeader() || GetLinkCost(GetLeaderId()) < kMaxRouteCost ||
                     (IsChild() && leader->GetCost() + 1 < kMaxRouteCost) ||
                     (leader->GetCost() + GetLinkCost(leader->GetNextHop()) < kMaxRouteCost),
                 error = OT_ERROR_DROP);

    // 4. It is a REED and there are already `kMaxRouters` active routers in
    // the network (because Leader would reject any further address solicit).
    // ==> Verified below when checking the scan mask.

    aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);

    // Version
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kVersion, version));
    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = OT_ERROR_PARSE);

    // Scan Mask
    SuccessOrExit(error = Tlv::FindUint8Tlv(aMessage, Tlv::kScanMask, scanMask));

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        ExitNow();

    case kRoleChild:
        VerifyOrExit(ScanMaskTlv::IsEndDeviceFlagSet(scanMask), OT_NOOP);
        VerifyOrExit(mRouterTable.GetActiveRouterCount() < kMaxRouters, error = OT_ERROR_DROP);
        break;

    case kRoleRouter:
    case kRoleLeader:
        VerifyOrExit(ScanMaskTlv::IsRouterFlagSet(scanMask), OT_NOOP);
        break;
    }

    // Challenge
    SuccessOrExit(error = ReadChallenge(aMessage, challenge));

    child = mChildTable.FindChild(macAddr, Child::kInStateAnyExceptInvalid);

    if (child == nullptr)
    {
        VerifyOrExit((child = mChildTable.GetNewChild()) != nullptr, OT_NOOP);

        // MAC Address
        child->SetExtAddress(macAddr);
        child->GetLinkInfo().Clear();
        child->GetLinkInfo().AddRss(linkInfo->mRss);
        child->ResetLinkFailures();
        child->SetState(Neighbor::kStateParentRequest);
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
        if (Tlv::FindTlv(aMessage, Tlv::kTimeRequest, sizeof(timeRequest), timeRequest) == OT_ERROR_NONE)
        {
            child->SetTimeSyncEnabled(true);
        }
        else
        {
            child->SetTimeSyncEnabled(false);
        }
#endif
    }
    else if (TimerMilli::GetNow() - child->GetLastHeard() < kParentRequestRouterTimeout - kParentRequestDuplicateMargin)
    {
        ExitNow(error = OT_ERROR_DUPLICATED);
    }

    if (!child->IsStateValidOrRestoring())
    {
        child->SetLastHeard(TimerMilli::GetNow());
        child->SetTimeout(Time::MsecToSec(kMaxChildIdRequestTimeout));
    }

    SendParentResponse(child, challenge, !ScanMaskTlv::IsEndDeviceFlagSet(scanMask));

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Parent Request: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleStateUpdateTimer(Timer &aTimer)
{
    aTimer.GetOwner<MleRouter>().HandleStateUpdateTimer();
}

void MleRouter::HandleStateUpdateTimer(void)
{
    bool routerStateUpdate = false;

    VerifyOrExit(IsFullThreadDevice(), OT_NOOP);

    mStateUpdateTimer.Start(kStateUpdatePeriod);

    if (mChallengeTimeout > 0)
    {
        mChallengeTimeout--;
    }

    if (mPreviousPartitionIdTimeout > 0)
    {
        mPreviousPartitionIdTimeout--;
    }

    if (mRouterSelectionJitterTimeout > 0)
    {
        mRouterSelectionJitterTimeout--;

        if (mRouterSelectionJitterTimeout == 0)
        {
            routerStateUpdate = true;
        }
    }
#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
    // Delay register only when `mRouterSelectionJitterTimeout` is 0,
    // that is, when the device has decided to stay as REED or Router.
    else if (mBackboneRouterRegistrationDelay > 0)
    {
        mBackboneRouterRegistrationDelay--;

        if (mBackboneRouterRegistrationDelay == 0)
        {
            // If no Backbone Router service after jitter, try to register its own Backbone Router Service.
            if (!Get<BackboneRouter::Leader>().HasPrimary())
            {
                if (Get<BackboneRouter::Local>().AddService() == OT_ERROR_NONE)
                {
                    Get<NetworkData::Notifier>().HandleServerDataUpdated();
                }
            }
        }
    }
#endif

    switch (mRole)
    {
    case kRoleDisabled:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);

    case kRoleDetached:
        if (mChallengeTimeout == 0)
        {
            IgnoreError(BecomeDetached());
            ExitNow();
        }

        break;

    case kRoleChild:
        if (routerStateUpdate)
        {
            if (mRouterTable.GetActiveRouterCount() < mRouterUpgradeThreshold)
            {
                // upgrade to Router
                IgnoreError(BecomeRouter(ThreadStatusTlv::kTooFewRouters));
            }
            else
            {
                // send announce after decided to stay in REED if needed
                InformPreviousChannel();
            }

            if (!mAdvertiseTimer.IsRunning())
            {
                SendAdvertisement();

                mAdvertiseTimer.Start(Time::SecToMsec(kReedAdvertiseInterval),
                                      Time::SecToMsec(kReedAdvertiseInterval + kReedAdvertiseJitter),
                                      TrickleTimer::kModePlainTimer);
            }

            ExitNow();
        }

        // fall through

    case kRoleRouter:
        // verify path to leader
        otLogDebgMle("network id timeout = %d", mRouterTable.GetLeaderAge());

        if ((mRouterTable.GetActiveRouterCount() > 0) && (mRouterTable.GetLeaderAge() >= mNetworkIdTimeout))
        {
            otLogInfoMle("Router ID Sequence timeout");
            IgnoreError(BecomeChild(kAttachSame1));
        }

        if (routerStateUpdate && mRouterTable.GetActiveRouterCount() > mRouterDowngradeThreshold)
        {
            // downgrade to REED
            otLogNoteMle("Downgrade to REED");
            IgnoreError(BecomeChild(kAttachSameDowngrade));
        }

        break;

    case kRoleLeader:
        break;
    }

    // update children state
    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateAnyExceptInvalid); !iter.IsDone(); iter++)
    {
        Child &  child   = *iter.GetChild();
        uint32_t timeout = 0;

        switch (child.GetState())
        {
        case Neighbor::kStateInvalid:
        case Neighbor::kStateChildIdRequest:
            continue;

        case Neighbor::kStateParentRequest:
        case Neighbor::kStateValid:
        case Neighbor::kStateRestored:
        case Neighbor::kStateChildUpdateRequest:
            timeout = Time::SecToMsec(child.GetTimeout());
            break;

        case Neighbor::kStateParentResponse:
        case Neighbor::kStateLinkRequest:
            OT_ASSERT(false);
            OT_UNREACHABLE_CODE(break);
        }

        if (TimerMilli::GetNow() - child.GetLastHeard() >= timeout)
        {
            otLogInfoMle("Child timeout expired");
            RemoveNeighbor(child);
        }
        else if (IsRouterOrLeader() && child.IsStateRestored())
        {
            IgnoreError(SendChildUpdateRequest(child));
        }
    }

    // update router state
    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Router & router = *iter.GetRouter();
        uint32_t age;

        if (router.GetRloc16() == GetRloc16())
        {
            router.SetLastHeard(TimerMilli::GetNow());
            continue;
        }

        age = TimerMilli::GetNow() - router.GetLastHeard();

        if (router.IsStateValid())
        {
#if OPENTHREAD_CONFIG_MLE_SEND_LINK_REQUEST_ON_ADV_TIMEOUT == 0

            if (age >= Time::SecToMsec(kMaxNeighborAge))
            {
                otLogInfoMle("Router timeout expired");
                RemoveNeighbor(router);
                continue;
            }

#else

            if (age >= Time::SecToMsec(kMaxNeighborAge))
            {
                if (age < Time::SecToMsec(kMaxNeighborAge) + kMaxTransmissionCount * kUnicastRetransmissionDelay)
                {
                    otLogInfoMle("Router timeout expired");
                    IgnoreError(SendLinkRequest(&router));
                }
                else
                {
                    RemoveNeighbor(router);
                    continue;
                }
            }

#endif
        }
        else if (router.IsStateLinkRequest())
        {
            if (age >= kMaxLinkRequestTimeout)
            {
                otLogInfoMle("Link Request timeout expired");
                RemoveNeighbor(router);
                continue;
            }
        }

        if (IsLeader())
        {
            if (mRouterTable.GetRouter(router.GetNextHop()) == nullptr &&
                mRouterTable.GetLinkCost(router) >= kMaxRouteCost && age >= Time::SecToMsec(kMaxLeaderToRouterTimeout))
            {
                otLogInfoMle("Router ID timeout expired (no route)");
                IgnoreError(mRouterTable.Release(router.GetRouterId()));
            }
        }
    }

    mRouterTable.ProcessTimerTick();

    SynchronizeChildNetworkData();

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (IsRouterOrLeader())
    {
        Get<TimeSync>().ProcessTimeSync();
    }
#endif

exit:
    return;
}

void MleRouter::SendParentResponse(Child *aChild, const Challenge &aChallenge, bool aRoutersOnlyRequest)
{
    otError      error = OT_ERROR_NONE;
    Ip6::Address destination;
    Message *    message;
    uint16_t     delay;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    message->SetDirectTransmission();

    SuccessOrExit(error = AppendHeader(*message, Header::kCommandParentResponse));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendLeaderData(*message));
    SuccessOrExit(error = AppendLinkFrameCounter(*message));
    SuccessOrExit(error = AppendMleFrameCounter(*message));
    SuccessOrExit(error = AppendResponse(*message, aChallenge));
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (aChild->IsTimeSyncEnabled())
    {
        SuccessOrExit(error = AppendTimeParameter(*message));
    }
#endif

    aChild->GenerateChallenge();

    SuccessOrExit(error = AppendChallenge(*message, aChild->GetChallenge(), aChild->GetChallengeSize()));
    error = AppendLinkMargin(*message, aChild->GetLinkInfo().GetLinkMargin());
    SuccessOrExit(error);

    SuccessOrExit(error = AppendConnectivity(*message));
    SuccessOrExit(error = AppendVersion(*message));

    destination.SetToLinkLocalAddress(aChild->GetExtAddress());

    if (aRoutersOnlyRequest)
    {
        delay = 1 + Random::NonCrypto::GetUint16InRange(0, kParentResponseMaxDelayRouters);
    }
    else
    {
        delay = 1 + Random::NonCrypto::GetUint16InRange(0, kParentResponseMaxDelayAll);
    }

    SuccessOrExit(error = AddDelayedResponse(*message, destination, delay));

    LogMleMessage("Delay Parent Response", destination);

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }
}

uint8_t MleRouter::GetMaxChildIpAddresses(void) const
{
    uint8_t num = OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD;

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    if (mMaxChildIpAddresses != 0)
    {
        num = mMaxChildIpAddresses;
    }
#endif

    return num;
}

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
otError MleRouter::SetMaxChildIpAddresses(uint8_t aMaxIpAddresses)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aMaxIpAddresses <= OPENTHREAD_CONFIG_MLE_IP_ADDRS_PER_CHILD, error = OT_ERROR_INVALID_ARGS);

    mMaxChildIpAddresses = aMaxIpAddresses;

exit:
    return error;
}
#endif

otError MleRouter::UpdateChildAddresses(const Message &aMessage, uint16_t aOffset, Child &aChild)
{
    otError                  error = OT_ERROR_NONE;
    AddressRegistrationEntry entry;
    Ip6::Address             address;
    Lowpan::Context          context;
    Tlv                      tlv;
    uint8_t                  registeredCount = 0;
    uint8_t                  storedCount     = 0;
    uint16_t                 offset          = 0;
    uint16_t                 end             = 0;

    VerifyOrExit(aMessage.Read(aOffset, sizeof(tlv), &tlv) == sizeof(tlv), error = OT_ERROR_PARSE);
    VerifyOrExit(tlv.GetLength() <= (aMessage.GetLength() - aOffset - sizeof(tlv)), error = OT_ERROR_PARSE);

    offset = aOffset + sizeof(tlv);
    end    = offset + tlv.GetLength();
    aChild.ClearIp6Addresses();

    while (offset < end)
    {
        uint8_t len;

        // read out the control field
        VerifyOrExit(aMessage.Read(offset, 1, &entry) == 1, error = OT_ERROR_PARSE);

        len = entry.GetLength();

        VerifyOrExit(aMessage.Read(offset, len, &entry) == len, error = OT_ERROR_PARSE);

        offset += len;
        registeredCount++;

        if (entry.IsCompressed())
        {
            if (Get<NetworkData::Leader>().GetContext(entry.GetContextId(), context) != OT_ERROR_NONE)
            {
                otLogWarnMle("Failed to get context %d for compressed address from child 0x%04x", entry.GetContextId(),
                             aChild.GetRloc16());
                continue;
            }

            memcpy(&address, context.mPrefix, BitVectorBytes(context.mPrefixLength));
            address.SetIid(entry.GetIid());
        }
        else
        {
            address = *entry.GetIp6Address();
        }

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        if (mMaxChildIpAddresses > 0 && storedCount >= mMaxChildIpAddresses)
        {
            // Skip remaining address registration entries but keep logging skipped addresses.
            error = OT_ERROR_NO_BUFS;
        }
        else
#endif
        {
            // We try to accept/add as many IPv6 addresses as possible.
            // "Child ID/Update Response" will indicate the accepted
            // addresses.
            error = aChild.AddIp6Address(address);
        }

        if (error == OT_ERROR_NONE)
        {
            storedCount++;
            otLogInfoMle("Child 0x%04x IPv6 address[%d]=%s", aChild.GetRloc16(), storedCount,
                         address.ToString().AsCString());
        }
        else
        {
            otLogWarnMle("Error %s adding IPv6 address %s to child 0x%04x", otThreadErrorToString(error),
                         address.ToString().AsCString(), aChild.GetRloc16());
        }

        if (address.IsMulticast())
        {
            continue;
        }

        // We check if the same address is in-use by another child, if so
        // remove it. This implements "last-in wins" duplicate address
        // resolution policy.
        //
        // Duplicate addresses can occur if a previously attached child
        // attaches to same parent again (after a reset, memory wipe) using
        // a new random extended address before the old entry in the child
        // table is timed out and then trying to register its globally unique
        // IPv6 address as the new child.

        for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
        {
            if (iter.GetChild() == &aChild)
            {
                continue;
            }

            IgnoreError(iter.GetChild()->RemoveIp6Address(address));
        }

        // Clear EID-to-RLOC cache for the unicast address registered by the child.
        Get<AddressResolver>().Remove(address);
    }

    if (registeredCount == 0)
    {
        otLogInfoMle("Child 0x%04x has no registered IPv6 address", aChild.GetRloc16());
    }
    else
    {
        otLogInfoMle("Child 0x%04x has %d registered IPv6 address%s, %d address%s stored", aChild.GetRloc16(),
                     registeredCount, (registeredCount == 1) ? "" : "es", storedCount, (storedCount == 1) ? "" : "es");
    }

    error = OT_ERROR_NONE;

exit:
    return error;
}

void MleRouter::HandleChildIdRequest(const Message &         aMessage,
                                     const Ip6::MessageInfo &aMessageInfo,
                                     uint32_t                aKeySequence)
{
    otError                 error    = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    Mac::ExtAddress         macAddr;
    uint16_t                version;
    Challenge               response;
    uint32_t                linkFrameCounter;
    uint32_t                mleFrameCounter;
    uint8_t                 modeBitmask;
    DeviceMode              mode;
    uint32_t                timeout;
    RequestedTlvs           requestedTlvs;
    ActiveTimestampTlv      activeTimestamp;
    PendingTimestampTlv     pendingTimestamp;
    Child *                 child;
    Router *                router;
    uint8_t                 numTlvs;
    uint16_t                addressRegistrationOffset = 0;

    LogMleMessage("Receive Child ID Request", aMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterEligible(), error = OT_ERROR_INVALID_STATE);

    // only process message when operating as a child, router, or leader
    VerifyOrExit(IsAttached(), error = OT_ERROR_INVALID_STATE);

    // Find Child
    aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);

    child = mChildTable.FindChild(macAddr, Child::kInStateAnyExceptInvalid);
    VerifyOrExit(child != nullptr, error = OT_ERROR_ALREADY);

    // Version
    SuccessOrExit(error = Tlv::FindUint16Tlv(aMessage, Tlv::kVersion, version));
    VerifyOrExit(version >= OT_THREAD_VERSION_1_1, error = OT_ERROR_PARSE);

    // Response
    SuccessOrExit(error = ReadResponse(aMessage, response));
    VerifyOrExit(response.Matches(child->GetChallenge(), child->GetChallengeSize()), error = OT_ERROR_SECURITY);

    // Remove existing MLE messages
    Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleGeneral);
    Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleChildIdRequest);
    Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleChildUpdateRequest);
    Get<MeshForwarder>().RemoveMessages(*child, Message::kSubTypeMleDataResponse);

    // Link-Layer Frame Counter
    SuccessOrExit(error = Tlv::FindUint32Tlv(aMessage, Tlv::kLinkFrameCounter, linkFrameCounter));

    // MLE Frame Counter
    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kMleFrameCounter, mleFrameCounter))
    {
    case OT_ERROR_NONE:
        break;
    case OT_ERROR_NOT_FOUND:
        mleFrameCounter = linkFrameCounter;
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Mode
    SuccessOrExit(error = Tlv::FindUint8Tlv(aMessage, Tlv::kMode, modeBitmask));
    mode.Set(modeBitmask);

    // Timeout
    SuccessOrExit(error = Tlv::FindUint32Tlv(aMessage, Tlv::kTimeout, timeout));

    // TLV Request
    SuccessOrExit(error = FindTlvRequest(aMessage, requestedTlvs));
    VerifyOrExit(requestedTlvs.mNumTlvs <= Child::kMaxRequestTlvs, error = OT_ERROR_PARSE);

    // Active Timestamp
    activeTimestamp.SetLength(0);

    if (Tlv::FindTlv(aMessage, Tlv::kActiveTimestamp, sizeof(activeTimestamp), activeTimestamp) == OT_ERROR_NONE)
    {
        VerifyOrExit(activeTimestamp.IsValid(), error = OT_ERROR_PARSE);
    }

    // Pending Timestamp
    pendingTimestamp.SetLength(0);

    if (Tlv::FindTlv(aMessage, Tlv::kPendingTimestamp, sizeof(pendingTimestamp), pendingTimestamp) == OT_ERROR_NONE)
    {
        VerifyOrExit(pendingTimestamp.IsValid(), error = OT_ERROR_PARSE);
    }

    if (!mode.IsFullThreadDevice())
    {
        SuccessOrExit(error = Tlv::FindTlvOffset(aMessage, Tlv::kAddressRegistration, addressRegistrationOffset));
        SuccessOrExit(error = UpdateChildAddresses(aMessage, addressRegistrationOffset, *child));
    }

    // Remove from router table
    router = mRouterTable.GetRouter(macAddr);
    if (router != nullptr)
    {
        // The `router` here can be invalid
        RemoveNeighbor(*router);
    }

    if (!child->IsStateValid())
    {
        child->SetState(Neighbor::kStateChildIdRequest);
    }
    else
    {
        RemoveNeighbor(*child);
    }

    child->SetLastHeard(TimerMilli::GetNow());
    child->SetLinkFrameCounter(linkFrameCounter);
    child->SetMleFrameCounter(mleFrameCounter);
    child->SetKeySequence(aKeySequence);
    child->SetDeviceMode(mode);
    child->SetVersion(static_cast<uint8_t>(version));
    child->GetLinkInfo().AddRss(linkInfo->mRss);
    child->SetTimeout(timeout);

    if (mode.IsFullNetworkData())
    {
        child->SetNetworkDataVersion(mLeaderData.GetDataVersion());
    }
    else
    {
        child->SetNetworkDataVersion(mLeaderData.GetStableDataVersion());
    }

    child->ClearRequestTlvs();

    for (numTlvs = 0; numTlvs < requestedTlvs.mNumTlvs; numTlvs++)
    {
        child->SetRequestTlv(numTlvs, requestedTlvs.mTlvs[numTlvs]);
    }

    if (activeTimestamp.GetLength() == 0 || Get<MeshCoP::ActiveDataset>().Compare(activeTimestamp) != 0)
    {
        child->SetRequestTlv(numTlvs++, Tlv::kActiveDataset);
    }

    if (pendingTimestamp.GetLength() == 0 || Get<MeshCoP::PendingDataset>().Compare(pendingTimestamp) != 0)
    {
        child->SetRequestTlv(numTlvs++, Tlv::kPendingDataset);
    }

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        OT_ASSERT(false);
        OT_UNREACHABLE_CODE(break);

    case kRoleChild:
        child->SetState(Neighbor::kStateChildIdRequest);
        IgnoreError(BecomeRouter(ThreadStatusTlv::kHaveChildIdRequest));
        break;

    case kRoleRouter:
    case kRoleLeader:
        SuccessOrExit(error = SendChildIdResponse(*child));
        break;
    }

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Child ID Request: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleChildUpdateRequest(const Message &         aMessage,
                                         const Ip6::MessageInfo &aMessageInfo,
                                         uint32_t                aKeySequence)
{
    static const uint8_t kMaxResponseTlvs = 10;

    otError         error = OT_ERROR_NONE;
    Mac::ExtAddress macAddr;
    uint8_t         modeBitmask;
    DeviceMode      mode;
    Challenge       challenge;
    LeaderData      leaderData;
    uint32_t        timeout;
    Child *         child;
    DeviceMode      oldMode;
    RequestedTlvs   requestedTlvs;
    uint8_t         tlvs[kMaxResponseTlvs];
    uint8_t         tlvslength                = 0;
    uint16_t        addressRegistrationOffset = 0;
    bool            childDidChange            = false;

    LogMleMessage("Receive Child Update Request from child", aMessageInfo.GetPeerAddr());

    // Mode
    SuccessOrExit(error = Tlv::FindUint8Tlv(aMessage, Tlv::kMode, modeBitmask));
    mode.Set(modeBitmask);

    // Challenge
    switch (ReadChallenge(aMessage, challenge))
    {
    case OT_ERROR_NONE:
        tlvs[tlvslength++] = Tlv::kResponse;
        break;
    case OT_ERROR_NOT_FOUND:
        challenge.mLength = 0;
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Find Child
    aMessageInfo.GetPeerAddr().ToExtAddress(macAddr);
    child = mChildTable.FindChild(macAddr, Child::kInStateAnyExceptInvalid);

    tlvs[tlvslength++] = Tlv::kSourceAddress;

    // Not proceed if the Child Update Request is from the peer which is not the device's child or
    // which was the device's child but becomes invalid.
    if (child == nullptr || child->IsStateInvalid())
    {
        // For invalid non-sleepy child, Send Child Update Response with status TLV (error)
        if (mode.IsRxOnWhenIdle())
        {
            tlvs[tlvslength++] = Tlv::kStatus;
            SendChildUpdateResponse(nullptr, aMessageInfo, tlvs, tlvslength, challenge);
        }

        ExitNow();
    }

    oldMode = child->GetDeviceMode();
    child->SetDeviceMode(mode);

    tlvs[tlvslength++] = Tlv::kMode;

    // Parent MUST include Leader Data TLV in Child Update Response
    tlvs[tlvslength++] = Tlv::kLeaderData;

    if (challenge.mLength != 0)
    {
        tlvs[tlvslength++] = Tlv::kMleFrameCounter;
        tlvs[tlvslength++] = Tlv::kLinkFrameCounter;
    }

    // Ip6 Address TLV
    if (Tlv::FindTlvOffset(aMessage, Tlv::kAddressRegistration, addressRegistrationOffset) == OT_ERROR_NONE)
    {
        SuccessOrExit(error = UpdateChildAddresses(aMessage, addressRegistrationOffset, *child));
        tlvs[tlvslength++] = Tlv::kAddressRegistration;
    }

    // Leader Data
    switch (ReadLeaderData(aMessage, leaderData))
    {
    case OT_ERROR_NONE:
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Timeout
    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kTimeout, timeout))
    {
    case OT_ERROR_NONE:
        if (child->GetTimeout() != timeout)
        {
            child->SetTimeout(timeout);
            childDidChange = true;
        }

        tlvs[tlvslength++] = Tlv::kTimeout;
        break;

    case OT_ERROR_NOT_FOUND:
        break;

    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // TLV Request
    switch (FindTlvRequest(aMessage, requestedTlvs))
    {
    case OT_ERROR_NONE:
        VerifyOrExit(requestedTlvs.mNumTlvs <= (kMaxResponseTlvs - tlvslength), error = OT_ERROR_PARSE);
        for (uint8_t i = 0; i < requestedTlvs.mNumTlvs; i++)
        {
            // Skip LeaderDataTlv since it is already included by default.
            if (requestedTlvs.mTlvs[i] != Tlv::kLeaderData)
            {
                tlvs[tlvslength++] = requestedTlvs.mTlvs[i];
            }
        }
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    child->SetLastHeard(TimerMilli::GetNow());

    if (oldMode != child->GetDeviceMode())
    {
        otLogNoteMle("Child 0x%04x mode change 0x%02x -> 0x%02x [%s]", child->GetRloc16(), oldMode.Get(),
                     child->GetDeviceMode().Get(), child->GetDeviceMode().ToString().AsCString());

        childDidChange = true;

        // The `IndirectSender::HandleChildModeChange()` needs to happen
        // after "Child Update" message is fully parsed to ensure that
        // any registered IPv6 addresses included in the "Child Update"
        // are added to the child.

        Get<IndirectSender>().HandleChildModeChange(*child, oldMode);
    }

    if (child->IsStateRestoring())
    {
        SetChildStateToValid(*child);
        child->SetKeySequence(aKeySequence);
    }
    else if (child->IsStateValid())
    {
        if (childDidChange)
        {
            IgnoreError(StoreChild(*child));
        }
    }

    SendChildUpdateResponse(child, aMessageInfo, tlvs, tlvslength, challenge);

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Child Update Request from child: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleChildUpdateResponse(const Message &         aMessage,
                                          const Ip6::MessageInfo &aMessageInfo,
                                          uint32_t                aKeySequence,
                                          Neighbor *              aNeighbor)
{
    otError                 error    = OT_ERROR_NONE;
    const otThreadLinkInfo *linkInfo = static_cast<const otThreadLinkInfo *>(aMessageInfo.GetLinkInfo());
    uint16_t                sourceAddress;
    uint32_t                timeout;
    Challenge               response;
    uint8_t                 status;
    uint32_t                linkFrameCounter;
    uint32_t                mleFrameCounter;
    LeaderData              leaderData;
    Child *                 child;
    uint16_t                addressRegistrationOffset = 0;

    if ((aNeighbor == nullptr) || IsActiveRouter(aNeighbor->GetRloc16()))
    {
        LogMleMessage("Receive Child Update Response from unknown child", aMessageInfo.GetPeerAddr());
        ExitNow(error = OT_ERROR_NOT_FOUND);
    }

    child = static_cast<Child *>(aNeighbor);

    // Response
    switch (ReadResponse(aMessage, response))
    {
    case OT_ERROR_NONE:
        VerifyOrExit(response.Matches(child->GetChallenge(), child->GetChallengeSize()), error = OT_ERROR_SECURITY);
        break;
    case OT_ERROR_NOT_FOUND:
        VerifyOrExit(child->IsStateValid(), error = OT_ERROR_SECURITY);
        break;
    default:
        ExitNow(error = OT_ERROR_NONE);
    }

    LogMleMessage("Receive Child Update Response from child", aMessageInfo.GetPeerAddr(), child->GetRloc16());

    // Source Address
    switch (Tlv::FindUint16Tlv(aMessage, Tlv::kSourceAddress, sourceAddress))
    {
    case OT_ERROR_NONE:
        if (child->GetRloc16() != sourceAddress)
        {
            RemoveNeighbor(*child);
            ExitNow();
        }

        break;

    case OT_ERROR_NOT_FOUND:
        break;

    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Status
    switch (Tlv::FindUint8Tlv(aMessage, Tlv::kStatus, status))
    {
    case OT_ERROR_NONE:
        VerifyOrExit(status != StatusTlv::kError, RemoveNeighbor(*child));
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Link-Layer Frame Counter

    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kLinkFrameCounter, linkFrameCounter))
    {
    case OT_ERROR_NONE:
        child->SetLinkFrameCounter(linkFrameCounter);
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // MLE Frame Counter
    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kMleFrameCounter, mleFrameCounter))
    {
    case OT_ERROR_NONE:
        child->SetMleFrameCounter(mleFrameCounter);
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_NONE);
    }

    // Timeout
    switch (Tlv::FindUint32Tlv(aMessage, Tlv::kTimeout, timeout))
    {
    case OT_ERROR_NONE:
        child->SetTimeout(timeout);
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // Ip6 Address
    if (Tlv::FindTlvOffset(aMessage, Tlv::kAddressRegistration, addressRegistrationOffset) == OT_ERROR_NONE)
    {
        SuccessOrExit(error = UpdateChildAddresses(aMessage, addressRegistrationOffset, *child));
    }

    // Leader Data
    switch (ReadLeaderData(aMessage, leaderData))
    {
    case OT_ERROR_NONE:
        if (child->IsFullNetworkData())
        {
            child->SetNetworkDataVersion(leaderData.GetDataVersion());
        }
        else
        {
            child->SetNetworkDataVersion(leaderData.GetStableDataVersion());
        }
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    SetChildStateToValid(*child);
    child->SetLastHeard(TimerMilli::GetNow());
    child->SetKeySequence(aKeySequence);
    child->GetLinkInfo().AddRss(linkInfo->mRss);

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Child Update Response from child: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleDataRequest(const Message &         aMessage,
                                  const Ip6::MessageInfo &aMessageInfo,
                                  const Neighbor *        aNeighbor)
{
    otError             error = OT_ERROR_NONE;
    RequestedTlvs       requestedTlvs;
    ActiveTimestampTlv  activeTimestamp;
    PendingTimestampTlv pendingTimestamp;
    uint8_t             tlvs[4];
    uint8_t             numTlvs;

    LogMleMessage("Receive Data Request", aMessageInfo.GetPeerAddr());

    VerifyOrExit(aNeighbor && aNeighbor->IsStateValid(), error = OT_ERROR_SECURITY);

    // TLV Request
    SuccessOrExit(error = FindTlvRequest(aMessage, requestedTlvs));
    VerifyOrExit(requestedTlvs.mNumTlvs <= sizeof(tlvs), error = OT_ERROR_PARSE);

    // Active Timestamp
    activeTimestamp.SetLength(0);

    if (Tlv::FindTlv(aMessage, Tlv::kActiveTimestamp, sizeof(activeTimestamp), activeTimestamp) == OT_ERROR_NONE)
    {
        VerifyOrExit(activeTimestamp.IsValid(), error = OT_ERROR_PARSE);
    }

    // Pending Timestamp
    pendingTimestamp.SetLength(0);

    if (Tlv::FindTlv(aMessage, Tlv::kPendingTimestamp, sizeof(pendingTimestamp), pendingTimestamp) == OT_ERROR_NONE)
    {
        VerifyOrExit(pendingTimestamp.IsValid(), error = OT_ERROR_PARSE);
    }

    memset(tlvs, Tlv::kInvalid, sizeof(tlvs));
    memcpy(tlvs, requestedTlvs.mTlvs, requestedTlvs.mNumTlvs);
    numTlvs = requestedTlvs.mNumTlvs;

    if (activeTimestamp.GetLength() == 0 || Get<MeshCoP::ActiveDataset>().Compare(activeTimestamp))
    {
        tlvs[numTlvs++] = Tlv::kActiveDataset;
    }

    if (pendingTimestamp.GetLength() == 0 || Get<MeshCoP::PendingDataset>().Compare(pendingTimestamp))
    {
        tlvs[numTlvs++] = Tlv::kPendingDataset;
    }

    SendDataResponse(aMessageInfo.GetPeerAddr(), tlvs, numTlvs, 0);

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Data Request: %s", otThreadErrorToString(error));
    }
}

void MleRouter::HandleNetworkDataUpdateRouter(void)
{
    static const uint8_t tlvs[] = {Tlv::kNetworkData};
    Ip6::Address         destination;
    uint16_t             delay;

    VerifyOrExit(IsRouterOrLeader(), OT_NOOP);

    destination.SetToLinkLocalAllNodesMulticast();

    delay = IsLeader() ? 0 : Random::NonCrypto::GetUint16InRange(0, kUnsolicitedDataResponseJitter);
    SendDataResponse(destination, tlvs, sizeof(tlvs), delay);

    SynchronizeChildNetworkData();

exit:
    return;
}

void MleRouter::SynchronizeChildNetworkData(void)
{
    VerifyOrExit(IsRouterOrLeader(), OT_NOOP);

    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValid); !iter.IsDone(); iter++)
    {
        Child & child = *iter.GetChild();
        uint8_t version;

        if (child.IsRxOnWhenIdle())
        {
            continue;
        }

        if (child.IsFullNetworkData())
        {
            version = Get<NetworkData::Leader>().GetVersion();
        }
        else
        {
            version = Get<NetworkData::Leader>().GetStableVersion();
        }

        if (child.GetNetworkDataVersion() == version)
        {
            continue;
        }

        SuccessOrExit(SendChildUpdateRequest(child));
    }

exit:
    return;
}

#if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
void MleRouter::SetSteeringData(const Mac::ExtAddress *aExtAddress)
{
    Mac::ExtAddress nullExtAddr;
    Mac::ExtAddress allowAnyExtAddr;

    nullExtAddr.Clear();
    allowAnyExtAddr.Fill(0xff);

    if ((aExtAddress == nullptr) || (*aExtAddress == nullExtAddr))
    {
        mSteeringData.Clear();
    }
    else if (*aExtAddress == allowAnyExtAddr)
    {
        mSteeringData.SetToPermitAllJoiners();
    }
    else
    {
        Mac::ExtAddress joinerId;

        mSteeringData.Init();
        MeshCoP::ComputeJoinerId(*aExtAddress, joinerId);
        mSteeringData.UpdateBloomFilter(joinerId);
    }
}
#endif // OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE

void MleRouter::HandleDiscoveryRequest(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    otError                      error = OT_ERROR_NONE;
    Tlv                          tlv;
    MeshCoP::Tlv                 meshcopTlv;
    MeshCoP::DiscoveryRequestTlv discoveryRequest;
    Mac::ExtendedPanId           extPanId;
    uint16_t                     offset;
    uint16_t                     end;

    LogMleMessage("Receive Discovery Request", aMessageInfo.GetPeerAddr());

    // only Routers and REEDs respond
    VerifyOrExit(IsRouterEligible(), error = OT_ERROR_INVALID_STATE);

    // find MLE Discovery TLV
    VerifyOrExit(Tlv::FindTlvOffset(aMessage, Tlv::kDiscovery, offset) == OT_ERROR_NONE, error = OT_ERROR_PARSE);
    aMessage.Read(offset, sizeof(tlv), &tlv);

    offset += sizeof(tlv);
    end = offset + sizeof(tlv) + tlv.GetLength();

    while (offset < end)
    {
        aMessage.Read(offset, sizeof(meshcopTlv), &meshcopTlv);

        switch (meshcopTlv.GetType())
        {
        case MeshCoP::Tlv::kDiscoveryRequest:
            aMessage.Read(offset, sizeof(discoveryRequest), &discoveryRequest);
            VerifyOrExit(discoveryRequest.IsValid(), error = OT_ERROR_PARSE);

            if (discoveryRequest.IsJoiner())
            {
#if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
                if (!mSteeringData.IsEmpty())
                {
                    break;
                }
                else // if steering data is not set out of band, fall back to network data
#endif
                {
                    VerifyOrExit(Get<NetworkData::Leader>().IsJoiningEnabled(), error = OT_ERROR_SECURITY);
                }
            }

            break;

        case MeshCoP::Tlv::kExtendedPanId:
            SuccessOrExit(error = Tlv::ReadTlv(aMessage, offset, &extPanId, sizeof(extPanId)));
            VerifyOrExit(Get<Mac::Mac>().GetExtendedPanId() != extPanId, error = OT_ERROR_DROP);

            break;

        default:
            break;
        }

        offset += sizeof(meshcopTlv) + meshcopTlv.GetLength();
    }

    error = SendDiscoveryResponse(aMessageInfo.GetPeerAddr(), aMessage.GetPanId());

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Discovery Request: %s", otThreadErrorToString(error));
    }
}

otError MleRouter::SendDiscoveryResponse(const Ip6::Address &aDestination, uint16_t aPanId)
{
    otError                       error = OT_ERROR_NONE;
    Message *                     message;
    uint16_t                      startOffset;
    Tlv                           tlv;
    MeshCoP::DiscoveryResponseTlv discoveryResponse;
    MeshCoP::NetworkNameTlv       networkName;
    uint16_t                      delay;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    message->SetSubType(Message::kSubTypeMleDiscoverResponse);
    message->SetPanId(aPanId);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandDiscoveryResponse));

    // Discovery TLV
    tlv.SetType(Tlv::kDiscovery);
    SuccessOrExit(error = message->Append(&tlv, sizeof(tlv)));

    startOffset = message->GetLength();

    // Discovery Response TLV
    discoveryResponse.Init();
    discoveryResponse.SetVersion(kThreadVersion);

    if (Get<KeyManager>().IsNativeCommissioningAllowed())
    {
        SuccessOrExit(
            error = Tlv::AppendUint16Tlv(*message, MeshCoP::Tlv::kCommissionerUdpPort, MeshCoP::kBorderAgentUdpPort));

        discoveryResponse.SetNativeCommissioner(true);
    }
    else
    {
        discoveryResponse.SetNativeCommissioner(false);
    }

    SuccessOrExit(error = discoveryResponse.AppendTo(*message));

    // Extended PAN ID TLV
    SuccessOrExit(error = Tlv::AppendTlv(*message, MeshCoP::Tlv::kExtendedPanId, Get<Mac::Mac>().GetExtendedPanId().m8,
                                         sizeof(Mac::ExtendedPanId)));

    // Network Name TLV
    networkName.Init();
    networkName.SetNetworkName(Get<Mac::Mac>().GetNetworkName().GetAsData());
    SuccessOrExit(error = networkName.AppendTo(*message));

#if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
    // If steering data is set out of band, use that value.
    // Otherwise use the one from commissioning data.
    if (!mSteeringData.IsEmpty())
    {
        SuccessOrExit(error = Tlv::AppendTlv(*message, MeshCoP::Tlv::kSteeringData, mSteeringData.GetData(),
                                             mSteeringData.GetLength()));
    }
    else
#endif
    {
        const MeshCoP::Tlv *steeringData;

        steeringData = Get<NetworkData::Leader>().GetCommissioningDataSubTlv(MeshCoP::Tlv::kSteeringData);

        if (steeringData != nullptr)
        {
            SuccessOrExit(error = steeringData->AppendTo(*message));
        }
    }

    // Joiner UDP Port TLV
    SuccessOrExit(error = Tlv::AppendUint16Tlv(*message, MeshCoP::Tlv::kJoinerUdpPort,
                                               Get<MeshCoP::JoinerRouter>().GetJoinerUdpPort()));

    tlv.SetLength(static_cast<uint8_t>(message->GetLength() - startOffset));
    message->Write(startOffset - sizeof(tlv), sizeof(tlv), &tlv);

    delay = Random::NonCrypto::GetUint16InRange(0, kDiscoveryMaxJitter + 1);

    SuccessOrExit(error = AddDelayedResponse(*message, aDestination, delay));

    LogMleMessage("Delay Discovery Response", aDestination);

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to process Discovery Response: %s", otThreadErrorToString(error));

        if (message != nullptr)
        {
            message->Free();
        }
    }

    return error;
}

otError MleRouter::SendChildIdResponse(Child &aChild)
{
    otError      error = OT_ERROR_NONE;
    Ip6::Address destination;
    Message *    message;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandChildIdResponse));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendLeaderData(*message));
    SuccessOrExit(error = AppendActiveTimestamp(*message));
    SuccessOrExit(error = AppendPendingTimestamp(*message));

    if (aChild.GetRloc16() == 0)
    {
        uint16_t rloc16;

        // pick next Child ID that is not being used
        do
        {
            mNextChildId++;

            if (mNextChildId > kMaxChildId)
            {
                mNextChildId = kMinChildId;
            }

            rloc16 = Get<Mac::Mac>().GetShortAddress() | mNextChildId;

        } while (mChildTable.FindChild(rloc16, Child::kInStateAnyExceptInvalid) != nullptr);

        // allocate Child ID
        aChild.SetRloc16(rloc16);
    }

    SuccessOrExit(error = AppendAddress16(*message, aChild.GetRloc16()));

    for (uint8_t i = 0; i < Child::kMaxRequestTlvs; i++)
    {
        switch (aChild.GetRequestTlv(i))
        {
        case Tlv::kNetworkData:
            SuccessOrExit(error = AppendNetworkData(*message, !aChild.IsFullNetworkData()));
            break;

        case Tlv::kRoute:
            SuccessOrExit(error = AppendRoute(*message));
            break;

        case Tlv::kActiveDataset:
            SuccessOrExit(error = AppendActiveDataset(*message));
            break;

        case Tlv::kPendingDataset:
            SuccessOrExit(error = AppendPendingDataset(*message));
            break;

        default:
            break;
        }
    }

    if (!aChild.IsFullThreadDevice())
    {
        SuccessOrExit(error = AppendChildAddresses(*message, aChild));
    }

    SetChildStateToValid(aChild);

    if (!aChild.IsRxOnWhenIdle())
    {
        Get<IndirectSender>().SetChildUseShortAddress(aChild, false);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (aChild.IsTimeSyncEnabled())
    {
        message->SetTimeSync(true);
    }
#endif

    destination.SetToLinkLocalAddress(aChild.GetExtAddress());
    SuccessOrExit(error = SendMessage(*message, destination));

    LogMleMessage("Send Child ID Response", destination, aChild.GetRloc16());

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}

otError MleRouter::SendChildUpdateRequest(Child &aChild)
{
    static const uint8_t tlvs[] = {Tlv::kTimeout, Tlv::kAddressRegistration};
    otError              error  = OT_ERROR_NONE;
    Ip6::Address         destination;
    Message *            message;

    if (!aChild.IsRxOnWhenIdle())
    {
        uint16_t childIndex = Get<ChildTable>().GetChildIndex(aChild);

        for (message = Get<MeshForwarder>().GetSendQueue().GetHead(); message; message = message->GetNext())
        {
            if (message->GetChildMask(childIndex) && message->GetSubType() == Message::kSubTypeMleChildUpdateRequest)
            {
                // No need to send the resync "Child Update Request" to the sleepy child
                // if there is one already queued.
                if (aChild.IsStateRestoring())
                {
                    ExitNow();
                }

                // Remove queued outdated "Child Update Request" when there is newer Network Data is to send.
                Get<MeshForwarder>().RemoveMessages(aChild, Message::kSubTypeMleChildUpdateRequest);
                break;
            }
        }
    }

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    message->SetSubType(Message::kSubTypeMleChildUpdateRequest);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandChildUpdateRequest));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendLeaderData(*message));
    SuccessOrExit(error = AppendNetworkData(*message, !aChild.IsFullNetworkData()));
    SuccessOrExit(error = AppendActiveTimestamp(*message));
    SuccessOrExit(error = AppendPendingTimestamp(*message));

    if (!aChild.IsStateValid())
    {
        SuccessOrExit(error = AppendTlvRequest(*message, tlvs, sizeof(tlvs)));
        aChild.GenerateChallenge();
        SuccessOrExit(error = AppendChallenge(*message, aChild.GetChallenge(), aChild.GetChallengeSize()));
    }

    destination.SetToLinkLocalAddress(aChild.GetExtAddress());
    SuccessOrExit(error = SendMessage(*message, destination));

    if (aChild.IsRxOnWhenIdle())
    {
        // only try to send a single Child Update Request message to an rx-on-when-idle child
        aChild.SetState(Child::kStateChildUpdateRequest);
    }

    LogMleMessage("Send Child Update Request to child", destination, aChild.GetRloc16());

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}

void MleRouter::SendChildUpdateResponse(Child *                 aChild,
                                        const Ip6::MessageInfo &aMessageInfo,
                                        const uint8_t *         aTlvs,
                                        uint8_t                 aTlvsLength,
                                        const Challenge &       aChallenge)
{
    otError  error = OT_ERROR_NONE;
    Message *message;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandChildUpdateResponse));

    for (int i = 0; i < aTlvsLength; i++)
    {
        switch (aTlvs[i])
        {
        case Tlv::kStatus:
            SuccessOrExit(error = AppendStatus(*message, StatusTlv::kError));
            break;

        case Tlv::kAddressRegistration:
            SuccessOrExit(error = AppendChildAddresses(*message, *aChild));
            break;

        case Tlv::kLeaderData:
            SuccessOrExit(error = AppendLeaderData(*message));
            break;

        case Tlv::kMode:
            SuccessOrExit(error = AppendMode(*message, aChild->GetDeviceMode()));
            break;

        case Tlv::kNetworkData:
            SuccessOrExit(error = AppendNetworkData(*message, !aChild->IsFullNetworkData()));
            SuccessOrExit(error = AppendActiveTimestamp(*message));
            SuccessOrExit(error = AppendPendingTimestamp(*message));
            break;

        case Tlv::kResponse:
            SuccessOrExit(error = AppendResponse(*message, aChallenge));
            break;

        case Tlv::kSourceAddress:
            SuccessOrExit(error = AppendSourceAddress(*message));
            break;

        case Tlv::kTimeout:
            SuccessOrExit(error = AppendTimeout(*message, aChild->GetTimeout()));
            break;

        case Tlv::kMleFrameCounter:
            SuccessOrExit(error = AppendMleFrameCounter(*message));
            break;

        case Tlv::kLinkFrameCounter:
            SuccessOrExit(error = AppendLinkFrameCounter(*message));
            break;
        }
    }

    SuccessOrExit(error = SendMessage(*message, aMessageInfo.GetPeerAddr()));

    if (aChild == nullptr)
    {
        LogMleMessage("Send Child Update Response to child", aMessageInfo.GetPeerAddr());
    }
    else
    {
        LogMleMessage("Send Child Update Response to child", aMessageInfo.GetPeerAddr(), aChild->GetRloc16());
    }

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }
}

void MleRouter::SendDataResponse(const Ip6::Address &aDestination,
                                 const uint8_t *     aTlvs,
                                 uint8_t             aTlvsLength,
                                 uint16_t            aDelay)
{
    otError   error   = OT_ERROR_NONE;
    Message * message = nullptr;
    Neighbor *neighbor;
    bool      stableOnly;

    if (mRetrieveNewNetworkData)
    {
        otLogInfoMle("Suppressing Data Response - waiting for new network data");
        ExitNow();
    }

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    message->SetSubType(Message::kSubTypeMleDataResponse);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandDataResponse));
    SuccessOrExit(error = AppendSourceAddress(*message));
    SuccessOrExit(error = AppendLeaderData(*message));
    SuccessOrExit(error = AppendActiveTimestamp(*message));
    SuccessOrExit(error = AppendPendingTimestamp(*message));

    for (int i = 0; i < aTlvsLength; i++)
    {
        switch (aTlvs[i])
        {
        case Tlv::kNetworkData:
            neighbor   = GetNeighbor(aDestination);
            stableOnly = neighbor != nullptr ? !neighbor->IsFullNetworkData() : false;
            SuccessOrExit(error = AppendNetworkData(*message, stableOnly));
            break;

        case Tlv::kActiveDataset:
            SuccessOrExit(error = AppendActiveDataset(*message));
            break;

        case Tlv::kPendingDataset:
            SuccessOrExit(error = AppendPendingDataset(*message));
            break;
        }
    }

    if (aDelay)
    {
        // Remove MLE Data Responses from Send Message Queue.
        Get<MeshForwarder>().RemoveDataResponseMessages();

        // Remove multicast MLE Data Response from Delayed Message Queue.
        RemoveDelayedDataResponseMessage();

        SuccessOrExit(error = AddDelayedResponse(*message, aDestination, aDelay));
        LogMleMessage("Delay Data Response", aDestination);
    }
    else
    {
        SuccessOrExit(error = SendMessage(*message, aDestination));
        LogMleMessage("Send Data Response", aDestination);
    }

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to send Data Response: %s", otThreadErrorToString(error));

        if (message != nullptr)
        {
            message->Free();
        }
    }
}

bool MleRouter::IsMinimalChild(uint16_t aRloc16)
{
    bool rval = false;

    if (RouterIdFromRloc16(aRloc16) == RouterIdFromRloc16(Get<Mac::Mac>().GetShortAddress()))
    {
        Neighbor *neighbor;

        neighbor = GetNeighbor(aRloc16);

        rval = (neighbor != nullptr) && (!neighbor->IsFullThreadDevice());
    }

    return rval;
}

void MleRouter::RemoveRouterLink(Router &aRouter)
{
    switch (mRole)
    {
    case kRoleChild:
        if (&aRouter == &mParent)
        {
            IgnoreError(BecomeDetached());
        }
        break;

#if OPENTHREAD_FTD
    case kRoleRouter:
    case kRoleLeader:
        mRouterTable.RemoveRouterLink(aRouter);
        break;
#endif

    default:
        break;
    }
}

void MleRouter::RemoveNeighbor(Neighbor &aNeighbor)
{
    VerifyOrExit(!aNeighbor.IsStateInvalid(), OT_NOOP);

    if (&aNeighbor == &mParent)
    {
        if (IsChild())
        {
            IgnoreError(BecomeDetached());
        }
    }
    else if (&aNeighbor == &mParentCandidate)
    {
        mParentCandidate.Clear();
    }
    else if (!IsActiveRouter(aNeighbor.GetRloc16()))
    {
        OT_ASSERT(mChildTable.GetChildIndex(static_cast<Child &>(aNeighbor)) < kMaxChildren);

        if (aNeighbor.IsStateValidOrRestoring())
        {
            Signal(OT_NEIGHBOR_TABLE_EVENT_CHILD_REMOVED, aNeighbor);
        }

        Get<IndirectSender>().ClearAllMessagesForSleepyChild(static_cast<Child &>(aNeighbor));

        if (aNeighbor.IsFullThreadDevice())
        {
            // Clear all EID-to-RLOC entries associated with the child.
            Get<AddressResolver>().Remove(aNeighbor.GetRloc16());
        }

        IgnoreError(RemoveStoredChild(aNeighbor.GetRloc16()));
    }
    else if (aNeighbor.IsStateValid())
    {
        OT_ASSERT(mRouterTable.Contains(aNeighbor));

        Signal(OT_NEIGHBOR_TABLE_EVENT_ROUTER_REMOVED, aNeighbor);
        mRouterTable.RemoveRouterLink(static_cast<Router &>(aNeighbor));
    }

    aNeighbor.GetLinkInfo().Clear();
    aNeighbor.SetState(Neighbor::kStateInvalid);

exit:
    return;
}

Neighbor *MleRouter::GetNeighbor(uint16_t aAddress)
{
    Neighbor *rval = nullptr;

    if (aAddress == Mac::kShortAddrBroadcast || aAddress == Mac::kShortAddrInvalid)
    {
        ExitNow();
    }

    switch (mRole)
    {
    case kRoleDisabled:
        break;

    case kRoleDetached:
    case kRoleChild:
        rval = Mle::GetNeighbor(aAddress);
        break;

    case kRoleRouter:
    case kRoleLeader:
        rval = mChildTable.FindChild(aAddress, Child::kInStateValidOrRestoring);
        VerifyOrExit(rval == nullptr, OT_NOOP);

        rval = mRouterTable.GetNeighbor(aAddress);
        break;
    }

exit:
    return rval;
}

Neighbor *MleRouter::GetNeighbor(const Mac::ExtAddress &aAddress)
{
    Neighbor *rval = nullptr;

    switch (mRole)
    {
    case kRoleDisabled:
        break;

    case kRoleDetached:
    case kRoleChild:
        rval = Mle::GetNeighbor(aAddress);
        break;

    case kRoleRouter:
    case kRoleLeader:
        rval = mChildTable.FindChild(aAddress, Child::kInStateValidOrRestoring);
        VerifyOrExit(rval == nullptr, OT_NOOP);

        rval = mRouterTable.GetNeighbor(aAddress);

        if (rval != nullptr)
        {
            ExitNow();
        }

        if (IsAttaching())
        {
            rval = Mle::GetNeighbor(aAddress);
        }

        break;
    }

exit:
    return rval;
}

Neighbor *MleRouter::GetNeighbor(const Mac::Address &aAddress)
{
    Neighbor *rval = nullptr;

    switch (aAddress.GetType())
    {
    case Mac::Address::kTypeShort:
        rval = GetNeighbor(aAddress.GetShort());
        break;

    case Mac::Address::kTypeExtended:
        rval = GetNeighbor(aAddress.GetExtended());
        break;

    default:
        break;
    }

    return rval;
}

Neighbor *MleRouter::GetNeighbor(const Ip6::Address &aAddress)
{
    Mac::Address    macAddr;
    Lowpan::Context context;
    Child *         child;
    Neighbor *      rval = nullptr;

    if (aAddress.IsLinkLocal())
    {
        aAddress.ToExtAddress(macAddr);

        ExitNow(rval = GetNeighbor(macAddr));
    }

    if (Get<NetworkData::Leader>().GetContext(aAddress, context) != OT_ERROR_NONE)
    {
        context.mContextId = 0xff;
    }

    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
    {
        child = iter.GetChild();

        if ((context.mContextId == kMeshLocalPrefixContextId) && aAddress.IsIidLocator() &&
            (aAddress.GetLocator() == child->GetRloc16()))
        {
            ExitNow(rval = child);
        }

        if (child->HasIp6Address(aAddress))
        {
            ExitNow(rval = child);
        }
    }

    VerifyOrExit(context.mContextId == kMeshLocalPrefixContextId, rval = nullptr);

    if (aAddress.IsIidLocator())
    {
        rval = mRouterTable.GetNeighbor(aAddress.GetLocator());
    }

exit:
    return rval;
}

Neighbor *MleRouter::GetRxOnlyNeighborRouter(const Mac::Address &aAddress)
{
    Neighbor *rval = nullptr;

    VerifyOrExit(IsChild(), rval = nullptr);

    switch (aAddress.GetType())
    {
    case Mac::Address::kTypeShort:
        rval = mRouterTable.GetNeighbor(aAddress.GetShort());
        break;

    case Mac::Address::kTypeExtended:
        rval = mRouterTable.GetNeighbor(aAddress.GetExtended());
        break;

    default:
        break;
    }

exit:
    return rval;
}

uint16_t MleRouter::GetNextHop(uint16_t aDestination)
{
    uint8_t       destinationId = RouterIdFromRloc16(aDestination);
    uint8_t       routeCost;
    uint8_t       linkCost;
    uint16_t      rval = Mac::kShortAddrInvalid;
    const Router *router;
    const Router *nextHop;

    if (IsChild())
    {
        ExitNow(rval = Mle::GetNextHop(aDestination));
    }

    // The frame is destined to a child
    if (destinationId == mRouterId)
    {
        ExitNow(rval = aDestination);
    }

    router = mRouterTable.GetRouter(destinationId);
    VerifyOrExit(router != nullptr, OT_NOOP);

    linkCost  = GetLinkCost(destinationId);
    routeCost = GetRouteCost(aDestination);

    if ((routeCost + GetLinkCost(router->GetNextHop())) < linkCost)
    {
        nextHop = mRouterTable.GetRouter(router->GetNextHop());
        VerifyOrExit(nextHop != nullptr && !nextHop->IsStateInvalid(), OT_NOOP);

        rval = Rloc16FromRouterId(router->GetNextHop());
    }
    else if (linkCost < kMaxRouteCost)
    {
        rval = Rloc16FromRouterId(destinationId);
    }

exit:
    return rval;
}

uint8_t MleRouter::GetCost(uint16_t aRloc16)
{
    uint8_t routerId = RouterIdFromRloc16(aRloc16);
    uint8_t cost     = GetLinkCost(routerId);
    Router *router   = mRouterTable.GetRouter(routerId);
    uint8_t routeCost;

    VerifyOrExit(router != nullptr && mRouterTable.GetRouter(router->GetNextHop()) != nullptr, OT_NOOP);

    routeCost = GetRouteCost(aRloc16) + GetLinkCost(router->GetNextHop());

    if (cost > routeCost)
    {
        cost = routeCost;
    }

exit:
    return cost;
}

uint8_t MleRouter::GetRouteCost(uint16_t aRloc16) const
{
    uint8_t       rval = kMaxRouteCost;
    const Router *router;

    router = mRouterTable.GetRouter(RouterIdFromRloc16(aRloc16));
    VerifyOrExit(router != nullptr && mRouterTable.GetRouter(router->GetNextHop()) != nullptr, OT_NOOP);

    rval = router->GetCost();

exit:
    return rval;
}

otError MleRouter::SetPreferredRouterId(uint8_t aRouterId)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(IsDetached() || IsDisabled(), error = OT_ERROR_INVALID_STATE);

    mPreviousRouterId = aRouterId;

exit:
    return error;
}

void MleRouter::SetRouterId(uint8_t aRouterId)
{
    mRouterId         = aRouterId;
    mPreviousRouterId = mRouterId;
}

otError MleRouter::GetChildInfoById(uint16_t aChildId, otChildInfo &aChildInfo)
{
    otError  error = OT_ERROR_NONE;
    Child *  child;
    uint16_t rloc16;

    if ((aChildId & ~kMaxChildId) != 0)
    {
        aChildId = ChildIdFromRloc16(aChildId);
    }

    rloc16 = Get<Mac::Mac>().GetShortAddress() | aChildId;
    child  = mChildTable.FindChild(rloc16, Child::kInStateAnyExceptInvalid);
    VerifyOrExit(child != nullptr, error = OT_ERROR_NOT_FOUND);

    error = GetChildInfo(*child, aChildInfo);

exit:
    return error;
}

otError MleRouter::GetChildInfoByIndex(uint16_t aChildIndex, otChildInfo &aChildInfo)
{
    otError error = OT_ERROR_NONE;
    Child * child = nullptr;

    child = mChildTable.GetChildAtIndex(aChildIndex);
    VerifyOrExit(child != nullptr, error = OT_ERROR_NOT_FOUND);

    error = GetChildInfo(*child, aChildInfo);

exit:
    return error;
}

otError MleRouter::GetChildNextIp6Address(uint16_t                   aChildIndex,
                                          Child::Ip6AddressIterator &aIterator,
                                          Ip6::Address &             aAddress)
{
    otError error = OT_ERROR_NONE;
    Child * child = nullptr;

    child = mChildTable.GetChildAtIndex(aChildIndex);
    VerifyOrExit(child != nullptr, error = OT_ERROR_INVALID_ARGS);
    VerifyOrExit(child->IsStateValidOrRestoring(), error = OT_ERROR_INVALID_ARGS);

    error = child->GetNextIp6Address(aIterator, aAddress);

exit:
    return error;
}

void MleRouter::RestoreChildren(void)
{
    otError  error          = OT_ERROR_NONE;
    bool     foundDuplicate = false;
    uint16_t numChildren    = 0;

    for (Settings::ChildInfoIterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Child *                    child;
        const Settings::ChildInfo &childInfo = iter.GetChildInfo();

        child = mChildTable.FindChild(childInfo.GetExtAddress(), Child::kInStateAnyExceptInvalid);

        if (child == nullptr)
        {
            VerifyOrExit((child = mChildTable.GetNewChild()) != nullptr, error = OT_ERROR_NO_BUFS);
        }
        else
        {
            foundDuplicate = true;
        }

        child->Clear();

        child->SetExtAddress(childInfo.GetExtAddress());
        child->GetLinkInfo().Clear();
        child->SetRloc16(childInfo.GetRloc16());
        child->SetTimeout(childInfo.GetTimeout());
        child->SetDeviceMode(DeviceMode(childInfo.GetMode()));
        child->SetState(Neighbor::kStateRestored);
        child->SetLastHeard(TimerMilli::GetNow());
        child->SetVersion(static_cast<uint8_t>(childInfo.GetVersion()));
        Get<IndirectSender>().SetChildUseShortAddress(*child, true);
        numChildren++;
    }

exit:

    if (foundDuplicate || (numChildren > mChildTable.GetMaxChildren()) || (error != OT_ERROR_NONE))
    {
        // If there is any error, e.g., there are more saved children
        // in non-volatile settings than could be restored or there are
        // duplicate entries with same extended address, refresh the stored
        // children info to ensure that the non-volatile settings remain
        // consistent with the child table.

        RefreshStoredChildren();
    }
}

otError MleRouter::RemoveStoredChild(uint16_t aChildRloc16)
{
    otError error = OT_ERROR_NOT_FOUND;

    for (Settings::ChildInfoIterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        if (iter.GetChildInfo().GetRloc16() == aChildRloc16)
        {
            error = iter.Delete();
            ExitNow();
        }
    }

exit:
    return error;
}

otError MleRouter::StoreChild(const Child &aChild)
{
    Settings::ChildInfo childInfo;

    IgnoreError(RemoveStoredChild(aChild.GetRloc16()));

    childInfo.Init();
    childInfo.SetExtAddress(aChild.GetExtAddress());
    childInfo.SetTimeout(aChild.GetTimeout());
    childInfo.SetRloc16(aChild.GetRloc16());
    childInfo.SetMode(aChild.GetDeviceMode().Get());
    childInfo.SetVersion(aChild.GetVersion());

    return Get<Settings>().AddChildInfo(childInfo);
}

void MleRouter::RefreshStoredChildren(void)
{
    SuccessOrExit(Get<Settings>().DeleteChildInfo());

    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateAnyExceptInvalid); !iter.IsDone(); iter++)
    {
        SuccessOrExit(StoreChild(*iter.GetChild()));
    }

exit:
    return;
}

otError MleRouter::GetChildInfo(Child &aChild, otChildInfo &aChildInfo)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aChild.IsStateValidOrRestoring(), error = OT_ERROR_NOT_FOUND);

    memset(&aChildInfo, 0, sizeof(aChildInfo));
    aChildInfo.mExtAddress         = aChild.GetExtAddress();
    aChildInfo.mTimeout            = aChild.GetTimeout();
    aChildInfo.mRloc16             = aChild.GetRloc16();
    aChildInfo.mChildId            = ChildIdFromRloc16(aChild.GetRloc16());
    aChildInfo.mNetworkDataVersion = aChild.GetNetworkDataVersion();
    aChildInfo.mAge                = Time::MsecToSec(TimerMilli::GetNow() - aChild.GetLastHeard());
    aChildInfo.mLinkQualityIn      = aChild.GetLinkInfo().GetLinkQuality();
    aChildInfo.mAverageRssi        = aChild.GetLinkInfo().GetAverageRss();
    aChildInfo.mLastRssi           = aChild.GetLinkInfo().GetLastRss();
    aChildInfo.mFrameErrorRate     = aChild.GetLinkInfo().GetFrameErrorRate();
    aChildInfo.mMessageErrorRate   = aChild.GetLinkInfo().GetMessageErrorRate();
    aChildInfo.mRxOnWhenIdle       = aChild.IsRxOnWhenIdle();
    aChildInfo.mSecureDataRequest  = aChild.IsSecureDataRequest();
    aChildInfo.mFullThreadDevice   = aChild.IsFullThreadDevice();
    aChildInfo.mFullNetworkData    = aChild.IsFullNetworkData();
    aChildInfo.mIsStateRestoring   = aChild.IsStateRestoring();

exit:
    return error;
}

void MleRouter::GetNeighborInfo(Neighbor &aNeighbor, otNeighborInfo &aNeighInfo)
{
    aNeighInfo.mExtAddress        = aNeighbor.GetExtAddress();
    aNeighInfo.mAge               = Time::MsecToSec(TimerMilli::GetNow() - aNeighbor.GetLastHeard());
    aNeighInfo.mRloc16            = aNeighbor.GetRloc16();
    aNeighInfo.mLinkFrameCounter  = aNeighbor.GetLinkFrameCounter();
    aNeighInfo.mMleFrameCounter   = aNeighbor.GetMleFrameCounter();
    aNeighInfo.mLinkQualityIn     = aNeighbor.GetLinkInfo().GetLinkQuality();
    aNeighInfo.mAverageRssi       = aNeighbor.GetLinkInfo().GetAverageRss();
    aNeighInfo.mLastRssi          = aNeighbor.GetLinkInfo().GetLastRss();
    aNeighInfo.mFrameErrorRate    = aNeighbor.GetLinkInfo().GetFrameErrorRate();
    aNeighInfo.mMessageErrorRate  = aNeighbor.GetLinkInfo().GetMessageErrorRate();
    aNeighInfo.mRxOnWhenIdle      = aNeighbor.IsRxOnWhenIdle();
    aNeighInfo.mSecureDataRequest = aNeighbor.IsSecureDataRequest();
    aNeighInfo.mFullThreadDevice  = aNeighbor.IsFullThreadDevice();
    aNeighInfo.mFullNetworkData   = aNeighbor.IsFullNetworkData();
}

otError MleRouter::GetNextNeighborInfo(otNeighborInfoIterator &aIterator, otNeighborInfo &aNeighInfo)
{
    otError   error    = OT_ERROR_NONE;
    Neighbor *neighbor = nullptr;
    int16_t   index;

    memset(&aNeighInfo, 0, sizeof(aNeighInfo));

    // Non-negative iterator value gives the Child index into child table

    if (aIterator >= 0)
    {
        for (index = aIterator;; index++)
        {
            Child *child = mChildTable.GetChildAtIndex(static_cast<uint16_t>(index));

            if (child == nullptr)
            {
                break;
            }

            if (child->IsStateValid())
            {
                neighbor            = child;
                aNeighInfo.mIsChild = true;
                index++;
                aIterator = index;
                ExitNow();
            }
        }

        aIterator = 0;
    }

    // Negative iterator value gives the current index into mRouters array

    for (index = -aIterator; index <= kMaxRouterId; index++)
    {
        Router *router = mRouterTable.GetRouter(static_cast<uint8_t>(index));

        if (router != nullptr && router->IsStateValid())
        {
            neighbor            = router;
            aNeighInfo.mIsChild = false;
            index++;
            aIterator = -index;
            ExitNow();
        }
    }

    aIterator = -index;
    error     = OT_ERROR_NOT_FOUND;

exit:

    if (neighbor != nullptr)
    {
        GetNeighborInfo(*neighbor, aNeighInfo);
    }

    return error;
}

void MleRouter::ResolveRoutingLoops(uint16_t aSourceMac, uint16_t aDestRloc16)
{
    Router *router;

    if (aSourceMac != GetNextHop(aDestRloc16))
    {
        ExitNow();
    }

    // loop exists
    router = mRouterTable.GetRouter(RouterIdFromRloc16(aDestRloc16));
    VerifyOrExit(router != nullptr, OT_NOOP);

    // invalidate next hop
    router->SetNextHop(kInvalidRouterId);
    ResetAdvertiseInterval();

exit:
    return;
}

otError MleRouter::CheckReachability(uint16_t aMeshDest, Ip6::Header &aIp6Header)
{
    otError error = OT_ERROR_NONE;

    if (IsChild())
    {
        error = Mle::CheckReachability(aMeshDest, aIp6Header);
        ExitNow();
    }

    if (aMeshDest == Get<Mac::Mac>().GetShortAddress())
    {
        // mesh destination is this device
        if (Get<ThreadNetif>().IsUnicastAddress(aIp6Header.GetDestination()))
        {
            // IPv6 destination is this device
            ExitNow();
        }
        else if (GetNeighbor(aIp6Header.GetDestination()) != nullptr)
        {
            // IPv6 destination is an RFD child
            ExitNow();
        }
    }
    else if (RouterIdFromRloc16(aMeshDest) == mRouterId)
    {
        // mesh destination is a child of this device
        if (mChildTable.FindChild(aMeshDest, Child::kInStateValidOrRestoring))
        {
            ExitNow();
        }
    }
    else if (GetNextHop(aMeshDest) != Mac::kShortAddrInvalid)
    {
        // forwarding to another router and route is known
        ExitNow();
    }

    error = OT_ERROR_NO_ROUTE;

exit:
    return error;
}

otError MleRouter::SendAddressSolicit(ThreadStatusTlv::Status aStatus)
{
    otError          error = OT_ERROR_NONE;
    Ip6::MessageInfo messageInfo;
    Coap::Message *  message = nullptr;

    VerifyOrExit(!mAddressSolicitPending, OT_NOOP);

    VerifyOrExit((message = Get<Coap::Coap>().NewPriorityMessage()) != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = message->Init(OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_POST, OT_URI_PATH_ADDRESS_SOLICIT));
    SuccessOrExit(error = message->SetPayloadMarker());

    SuccessOrExit(error = Tlv::AppendTlv(*message, ThreadTlv::kExtMacAddress, Get<Mac::Mac>().GetExtAddress().m8,
                                         sizeof(Mac::ExtAddress)));

    if (IsRouterIdValid(mPreviousRouterId))
    {
        SuccessOrExit(error =
                          Tlv::AppendUint16Tlv(*message, ThreadTlv::kRloc16, Rloc16FromRouterId(mPreviousRouterId)));
    }

    SuccessOrExit(error = Tlv::AppendUint8Tlv(*message, ThreadTlv::kStatus, static_cast<uint8_t>(aStatus)));

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    SuccessOrExit(error = AppendXtalAccuracy(*message));
#endif

    SuccessOrExit(error = GetLeaderAddress(messageInfo.GetPeerAddr()));
    messageInfo.SetSockAddr(GetMeshLocal16());
    messageInfo.SetPeerPort(kCoapUdpPort);

    SuccessOrExit(
        error = Get<Coap::Coap>().SendMessage(*message, messageInfo, &MleRouter::HandleAddressSolicitResponse, this));
    mAddressSolicitPending = true;

    LogMleMessage("Send Address Solicit", messageInfo.GetPeerAddr());

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}

void MleRouter::SendAddressRelease(void)
{
    otError          error = OT_ERROR_NONE;
    Ip6::MessageInfo messageInfo;
    Coap::Message *  message;

    VerifyOrExit((message = Get<Coap::Coap>().NewMessage()) != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = message->Init(OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_POST, OT_URI_PATH_ADDRESS_RELEASE));
    SuccessOrExit(error = message->SetPayloadMarker());

    SuccessOrExit(error = Tlv::AppendUint16Tlv(*message, ThreadTlv::kRloc16, Rloc16FromRouterId(mRouterId)));

    SuccessOrExit(error = Tlv::AppendTlv(*message, ThreadTlv::kExtMacAddress, Get<Mac::Mac>().GetExtAddress().m8,
                                         sizeof(Mac::ExtAddress)));

    messageInfo.SetSockAddr(GetMeshLocal16());
    SuccessOrExit(error = GetLeaderAddress(messageInfo.GetPeerAddr()));
    messageInfo.SetPeerPort(kCoapUdpPort);
    SuccessOrExit(error = Get<Coap::Coap>().SendMessage(*message, messageInfo));

    LogMleMessage("Send Address Release", messageInfo.GetPeerAddr());

exit:

    if (error != OT_ERROR_NONE)
    {
        otLogWarnMle("Failed to send Address Release: %s", otThreadErrorToString(error));

        if (message != nullptr)
        {
            message->Free();
        }
    }
}

void MleRouter::HandleAddressSolicitResponse(void *               aContext,
                                             otMessage *          aMessage,
                                             const otMessageInfo *aMessageInfo,
                                             otError              aResult)
{
    static_cast<MleRouter *>(aContext)->HandleAddressSolicitResponse(
        static_cast<Coap::Message *>(aMessage), static_cast<const Ip6::MessageInfo *>(aMessageInfo), aResult);
}

void MleRouter::HandleAddressSolicitResponse(Coap::Message *         aMessage,
                                             const Ip6::MessageInfo *aMessageInfo,
                                             otError                 aResult)
{
    OT_UNUSED_VARIABLE(aMessageInfo);

    uint8_t             status;
    uint16_t            rloc16;
    ThreadRouterMaskTlv routerMaskTlv;
    uint8_t             routerId;
    Router *            router;
    Router *            leader;

    mAddressSolicitPending = false;

    VerifyOrExit(aResult == OT_ERROR_NONE && aMessage != nullptr && aMessage != nullptr, OT_NOOP);

    VerifyOrExit(aMessage->GetCode() == OT_COAP_CODE_CHANGED, OT_NOOP);

    LogMleMessage("Receive Address Reply", aMessageInfo->GetPeerAddr());

    SuccessOrExit(Tlv::FindUint8Tlv(*aMessage, ThreadTlv::kStatus, status));

    if (status != ThreadStatusTlv::kSuccess)
    {
        mAddressSolicitRejected = true;

        if (IsRouterIdValid(mPreviousRouterId))
        {
            if (HasChildren())
            {
                RemoveChildren();
            }

            SetRouterId(kInvalidRouterId);
        }

        ExitNow();
    }

    SuccessOrExit(Tlv::FindUint16Tlv(*aMessage, ThreadTlv::kRloc16, rloc16));
    routerId = RouterIdFromRloc16(rloc16);

    SuccessOrExit(ThreadTlv::FindTlv(*aMessage, ThreadTlv::kRouterMask, sizeof(routerMaskTlv), routerMaskTlv));
    VerifyOrExit(routerMaskTlv.IsValid(), OT_NOOP);

    // assign short address
    SetRouterId(routerId);

    SetStateRouter(Rloc16FromRouterId(mRouterId));
    mRouterTable.Clear();
    mRouterTable.UpdateRouterIdSet(routerMaskTlv.GetIdSequence(), routerMaskTlv.GetAssignedRouterIdMask());

    router = mRouterTable.GetRouter(routerId);
    VerifyOrExit(router != nullptr, OT_NOOP);

    router->SetExtAddress(Get<Mac::Mac>().GetExtAddress());
    router->SetCost(0);

    router = mRouterTable.GetRouter(mParent.GetRouterId());
    VerifyOrExit(router != nullptr, OT_NOOP);

    // Keep link to the parent in order to respond to Parent Requests before new link is established.
    *router = mParent;
    router->SetState(Neighbor::kStateValid);
    router->SetNextHop(kInvalidRouterId);
    router->SetCost(0);

    leader = mRouterTable.GetLeader();
    OT_ASSERT(leader != nullptr);

    if (leader != router)
    {
        // Keep route path to the Leader reported by the parent before it is updated.
        leader->SetCost(mParentLeaderCost);
        leader->SetNextHop(RouterIdFromRloc16(mParent.GetRloc16()));
    }

    // send link request
    IgnoreError(SendLinkRequest(nullptr));

    // send child id responses
    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateChildIdRequest); !iter.IsDone(); iter++)
    {
        IgnoreError(SendChildIdResponse(*iter.GetChild()));
    }

exit:

    // send announce after received address solicit reply if needed
    InformPreviousChannel();
}

bool MleRouter::IsExpectedToBecomeRouter(void) const
{
    return IsRouterEligible() && !IsRouterOrLeader() &&
           (Get<RouterTable>().GetActiveRouterCount() < GetRouterUpgradeThreshold()) && !mAddressSolicitRejected;
}

void MleRouter::HandleAddressSolicit(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    static_cast<MleRouter *>(aContext)->HandleAddressSolicit(*static_cast<Coap::Message *>(aMessage),
                                                             *static_cast<const Ip6::MessageInfo *>(aMessageInfo));
}

void MleRouter::HandleAddressSolicit(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    otError         error = OT_ERROR_NONE;
    Mac::ExtAddress extAddress;
    uint16_t        rloc16;
    uint8_t         status;
    Router *        router = nullptr;
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    uint16_t xtalAccuracy;
#endif

    VerifyOrExit(aMessage.IsConfirmable() && aMessage.GetCode() == OT_COAP_CODE_POST, error = OT_ERROR_PARSE);

    LogMleMessage("Receive Address Solicit", aMessageInfo.GetPeerAddr());

    SuccessOrExit(error = ThreadTlv::FindTlv(aMessage, ThreadTlv::kExtMacAddress, &extAddress, sizeof(extAddress)));

    SuccessOrExit(error = Tlv::FindUint8Tlv(aMessage, ThreadTlv::kStatus, status));

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    // In a time sync enabled network, all routers' xtal accuracy must be less than the threshold.
    SuccessOrExit(Tlv::FindUint16Tlv(aMessage, Tlv::kXtalAccuracy, xtalAccuracy));
    VerifyOrExit(xtalAccuracy <= Get<TimeSync>().GetXtalThreshold(), OT_NOOP);
#endif

    // see if allocation already exists
    router = mRouterTable.GetRouter(extAddress);

    if (router != nullptr)
    {
        ExitNow();
    }

    // check the request reason
    switch (status)
    {
    case ThreadStatusTlv::kTooFewRouters:
        VerifyOrExit(mRouterTable.GetActiveRouterCount() < mRouterUpgradeThreshold, OT_NOOP);
        break;

    case ThreadStatusTlv::kHaveChildIdRequest:
    case ThreadStatusTlv::kParentPartitionChange:
        break;

    default:
        ExitNow(error = OT_ERROR_PARSE);
        OT_UNREACHABLE_CODE(break);
    }

    switch (Tlv::FindUint16Tlv(aMessage, ThreadTlv::kRloc16, rloc16))
    {
    case OT_ERROR_NONE:
        router = mRouterTable.Allocate(RouterIdFromRloc16(rloc16));
        break;
    case OT_ERROR_NOT_FOUND:
        break;
    default:
        ExitNow(error = OT_ERROR_PARSE);
    }

    // allocate new router id
    if (router == nullptr)
    {
        router = mRouterTable.Allocate();
    }
    else
    {
        otLogInfoMle("router id requested and provided!");
    }

    if (router != nullptr)
    {
        router->SetExtAddress(extAddress);
    }
    else
    {
        otLogInfoMle("router address unavailable!");
    }

exit:

    if (error == OT_ERROR_NONE)
    {
        SendAddressSolicitResponse(aMessage, router, aMessageInfo);
    }
}

void MleRouter::SendAddressSolicitResponse(const Coap::Message &   aRequest,
                                           const Router *          aRouter,
                                           const Ip6::MessageInfo &aMessageInfo)
{
    otError             error = OT_ERROR_NONE;
    ThreadRouterMaskTlv routerMaskTlv;
    Coap::Message *     message;

    VerifyOrExit((message = Get<Coap::Coap>().NewPriorityMessage()) != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = message->SetDefaultResponseHeader(aRequest));
    SuccessOrExit(error = message->SetPayloadMarker());

    SuccessOrExit(error = Tlv::AppendUint8Tlv(*message, ThreadTlv::kStatus,
                                              aRouter == nullptr ? ThreadStatusTlv::kNoAddressAvailable
                                                                 : ThreadStatusTlv::kSuccess));

    if (aRouter != nullptr)
    {
        SuccessOrExit(error = Tlv::AppendUint16Tlv(*message, ThreadTlv::kRloc16, aRouter->GetRloc16()));

        routerMaskTlv.Init();
        routerMaskTlv.SetIdSequence(mRouterTable.GetRouterIdSequence());
        routerMaskTlv.SetAssignedRouterIdMask(mRouterTable.GetRouterIdSet());

        SuccessOrExit(error = routerMaskTlv.AppendTo(*message));
    }

    SuccessOrExit(error = Get<Coap::Coap>().SendMessage(*message, aMessageInfo));

    LogMleMessage("Send Address Reply", aMessageInfo.GetPeerAddr());

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }
}

void MleRouter::HandleAddressRelease(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    static_cast<MleRouter *>(aContext)->HandleAddressRelease(*static_cast<Coap::Message *>(aMessage),
                                                             *static_cast<const Ip6::MessageInfo *>(aMessageInfo));
}

void MleRouter::HandleAddressRelease(Coap::Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
{
    uint16_t        rloc16;
    Mac::ExtAddress extAddress;
    uint8_t         routerId;
    Router *        router;

    VerifyOrExit(aMessage.IsConfirmable() && aMessage.GetCode() == OT_COAP_CODE_POST, OT_NOOP);

    LogMleMessage("Receive Address Release", aMessageInfo.GetPeerAddr());

    SuccessOrExit(Tlv::FindUint16Tlv(aMessage, ThreadTlv::kRloc16, rloc16));

    SuccessOrExit(Tlv::FindTlv(aMessage, ThreadTlv::kExtMacAddress, &extAddress, sizeof(extAddress)));

    routerId = RouterIdFromRloc16(rloc16);
    router   = mRouterTable.GetRouter(routerId);

    VerifyOrExit((router != nullptr) && (router->GetExtAddress() == extAddress), OT_NOOP);

    IgnoreError(mRouterTable.Release(routerId));

    SuccessOrExit(Get<Coap::Coap>().SendEmptyAck(aMessage, aMessageInfo));

    LogMleMessage("Send Address Release Reply", aMessageInfo.GetPeerAddr());

exit:
    return;
}

void MleRouter::FillConnectivityTlv(ConnectivityTlv &aTlv)
{
    Router *leader;
    uint8_t cost;
    int8_t  parentPriority = kParentPriorityMedium;

    if (mParentPriority != kParentPriorityUnspecified)
    {
        parentPriority = mParentPriority;
    }
    else
    {
        uint16_t numChildren = mChildTable.GetNumChildren(Child::kInStateValid);
        uint16_t maxAllowed  = mChildTable.GetMaxChildrenAllowed();

        if ((maxAllowed - numChildren) < (maxAllowed / 3))
        {
            parentPriority = kParentPriorityLow;
        }
        else
        {
            parentPriority = kParentPriorityMedium;
        }
    }

    aTlv.SetParentPriority(parentPriority);

    // compute leader cost and link qualities
    aTlv.SetLinkQuality1(0);
    aTlv.SetLinkQuality2(0);
    aTlv.SetLinkQuality3(0);

    leader = mRouterTable.GetLeader();
    cost   = (leader != nullptr) ? leader->GetCost() : static_cast<uint8_t>(kMaxRouteCost);

    switch (mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        cost = static_cast<uint8_t>(kMaxRouteCost);
        break;

    case kRoleChild:
        switch (mParent.GetLinkInfo().GetLinkQuality())
        {
        case 1:
            aTlv.SetLinkQuality1(aTlv.GetLinkQuality1() + 1);
            break;

        case 2:
            aTlv.SetLinkQuality2(aTlv.GetLinkQuality2() + 1);
            break;

        case 3:
            aTlv.SetLinkQuality3(aTlv.GetLinkQuality3() + 1);
            break;
        }

        cost += LinkQualityToCost(mParent.GetLinkInfo().GetLinkQuality());
        break;

    case kRoleRouter:
        if (leader != nullptr)
        {
            cost += GetLinkCost(leader->GetNextHop());

            if (!IsRouterIdValid(leader->GetNextHop()) || GetLinkCost(GetLeaderId()) < cost)
            {
                cost = GetLinkCost(GetLeaderId());
            }
        }

        break;

    case kRoleLeader:
        cost = 0;
        break;
    }

    aTlv.SetActiveRouters(mRouterTable.GetActiveRouterCount());

    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Router &router = *iter.GetRouter();
        uint8_t linkQuality;

        if (router.GetRloc16() == GetRloc16())
        {
            // skip self
            continue;
        }

        if (!router.IsStateValid())
        {
            // skip non-neighbor routers
            continue;
        }

        linkQuality = router.GetLinkInfo().GetLinkQuality();

        if (linkQuality > router.GetLinkQualityOut())
        {
            linkQuality = router.GetLinkQualityOut();
        }

        switch (linkQuality)
        {
        case 1:
            aTlv.SetLinkQuality1(aTlv.GetLinkQuality1() + 1);
            break;

        case 2:
            aTlv.SetLinkQuality2(aTlv.GetLinkQuality2() + 1);
            break;

        case 3:
            aTlv.SetLinkQuality3(aTlv.GetLinkQuality3() + 1);
            break;
        }
    }

    aTlv.SetLeaderCost((cost < kMaxRouteCost) ? cost : static_cast<uint8_t>(kMaxRouteCost));
    aTlv.SetIdSequence(mRouterTable.GetRouterIdSequence());
    aTlv.SetSedBufferSize(OPENTHREAD_CONFIG_DEFAULT_SED_BUFFER_SIZE);
    aTlv.SetSedDatagramCount(OPENTHREAD_CONFIG_DEFAULT_SED_DATAGRAM_COUNT);
}

otError MleRouter::AppendConnectivity(Message &aMessage)
{
    ConnectivityTlv tlv;

    tlv.Init();
    FillConnectivityTlv(tlv);

    return tlv.AppendTo(aMessage);
}

otError MleRouter::AppendChildAddresses(Message &aMessage, Child &aChild)
{
    otError                   error;
    Ip6::Address              address;
    Child::Ip6AddressIterator iterator;
    Tlv                       tlv;
    AddressRegistrationEntry  entry;
    Lowpan::Context           context;
    uint8_t                   length      = 0;
    uint16_t                  startOffset = aMessage.GetLength();

    tlv.SetType(Tlv::kAddressRegistration);
    SuccessOrExit(error = aMessage.Append(&tlv, sizeof(tlv)));

    while (aChild.GetNextIp6Address(iterator, address) == OT_ERROR_NONE)
    {
        if (address.IsMulticast() || Get<NetworkData::Leader>().GetContext(address, context) != OT_ERROR_NONE)
        {
            // uncompressed entry
            entry.SetUncompressed();
            entry.SetIp6Address(address);
        }
        else if (context.mContextId != kMeshLocalPrefixContextId)
        {
            // compressed entry
            entry.SetContextId(context.mContextId);
            entry.SetIid(address.GetIid());
        }
        else
        {
            continue;
        }

        SuccessOrExit(error = aMessage.Append(&entry, entry.GetLength()));
        length += entry.GetLength();
    }

    tlv.SetLength(length);
    aMessage.Write(startOffset, sizeof(tlv), &tlv);

exit:
    return error;
}

void MleRouter::FillRouteTlv(RouteTlv &aTlv)
{
    uint8_t routerCount = 0;

    aTlv.SetRouterIdSequence(mRouterTable.GetRouterIdSequence());
    aTlv.SetRouterIdMask(mRouterTable.GetRouterIdSet());

    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++, routerCount++)
    {
        Router &router = *iter.GetRouter();

        if (router.GetRloc16() == GetRloc16())
        {
            aTlv.SetLinkQualityIn(routerCount, 0);
            aTlv.SetLinkQualityOut(routerCount, 0);
            aTlv.SetRouteCost(routerCount, 1);
        }
        else
        {
            Router *nextHop;
            uint8_t linkCost;
            uint8_t routeCost;

            linkCost = mRouterTable.GetLinkCost(router);
            nextHop  = mRouterTable.GetRouter(router.GetNextHop());

            if (nextHop == nullptr)
            {
                routeCost = linkCost;
            }
            else
            {
                routeCost = router.GetCost() + mRouterTable.GetLinkCost(*nextHop);

                if (linkCost < routeCost)
                {
                    routeCost = linkCost;
                }
            }

            if (routeCost >= kMaxRouteCost)
            {
                routeCost = 0;
            }

            aTlv.SetRouteCost(routerCount, routeCost);
            aTlv.SetLinkQualityOut(routerCount, router.GetLinkQualityOut());
            aTlv.SetLinkQualityIn(routerCount, router.GetLinkInfo().GetLinkQuality());
        }
    }

    aTlv.SetRouteDataLength(routerCount);
}

otError MleRouter::AppendRoute(Message &aMessage)
{
    RouteTlv tlv;

    tlv.Init();
    FillRouteTlv(tlv);

    return tlv.AppendTo(aMessage);
}

otError MleRouter::AppendActiveDataset(Message &aMessage)
{
    return Get<MeshCoP::ActiveDataset>().AppendMleDatasetTlv(aMessage);
}

otError MleRouter::AppendPendingDataset(Message &aMessage)
{
    return Get<MeshCoP::PendingDataset>().AppendMleDatasetTlv(aMessage);
}

bool MleRouter::HasMinDowngradeNeighborRouters(void)
{
    uint8_t linkQuality;
    uint8_t routerCount = 0;

    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Router &router = *iter.GetRouter();

        if (!router.IsStateValid())
        {
            continue;
        }

        linkQuality = router.GetLinkInfo().GetLinkQuality();

        if (linkQuality > router.GetLinkQualityOut())
        {
            linkQuality = router.GetLinkQualityOut();
        }

        if (linkQuality >= 2)
        {
            routerCount++;
        }
    }

    return routerCount >= kMinDowngradeNeighbors;
}

bool MleRouter::HasOneNeighborWithComparableConnectivity(const RouteTlv &aRoute, uint8_t aRouterId)
{
    bool rval = true;

    // process local neighbor routers
    for (RouterTable::Iterator iter(GetInstance()); !iter.IsDone(); iter++)
    {
        Router &router           = *iter.GetRouter();
        uint8_t localLinkQuality = 0;
        uint8_t peerLinkQuality  = 0;
        uint8_t routerCount      = 0;

        if (router.GetRouterId() == mRouterId)
        {
            routerCount++;
            continue;
        }

        // check if neighbor is valid
        if (router.IsStateValid())
        {
            // if neighbor is just peer
            if (router.GetRouterId() == aRouterId)
            {
                routerCount++;
                continue;
            }

            localLinkQuality = router.GetLinkInfo().GetLinkQuality();

            if (localLinkQuality > router.GetLinkQualityOut())
            {
                localLinkQuality = router.GetLinkQualityOut();
            }

            if (localLinkQuality >= 2)
            {
                // check if this neighbor router is in peer Route64 TLV
                if (!aRoute.IsRouterIdSet(router.GetRouterId()))
                {
                    ExitNow(rval = false);
                }

                // get the peer's two-way link quality to this router
                peerLinkQuality = aRoute.GetLinkQualityIn(routerCount);

                if (peerLinkQuality > aRoute.GetLinkQualityOut(routerCount))
                {
                    peerLinkQuality = aRoute.GetLinkQualityOut(routerCount);
                }

                // compare local link quality to this router with peer's
                if (peerLinkQuality >= localLinkQuality)
                {
                    routerCount++;
                    continue;
                }
                else
                {
                    ExitNow(rval = false);
                }
            }
        }

        routerCount++;
    }

exit:
    return rval;
}

void MleRouter::SetChildStateToValid(Child &aChild)
{
    VerifyOrExit(!aChild.IsStateValid(), OT_NOOP);

    aChild.SetState(Neighbor::kStateValid);
    IgnoreError(StoreChild(aChild));
    Signal(OT_NEIGHBOR_TABLE_EVENT_CHILD_ADDED, aChild);

exit:
    return;
}

bool MleRouter::HasChildren(void)
{
    return mChildTable.HasChildren(Child::kInStateValidOrAttaching);
}

void MleRouter::RemoveChildren(void)
{
    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
    {
        RemoveNeighbor(*iter.GetChild());
    }
}

bool MleRouter::HasSmallNumberOfChildren(void)
{
    uint16_t numChildren = 0;
    uint8_t  routerCount = mRouterTable.GetActiveRouterCount();

    VerifyOrExit(routerCount > mRouterDowngradeThreshold, OT_NOOP);

    numChildren = mChildTable.GetNumChildren(Child::kInStateValid);

    return numChildren < (routerCount - mRouterDowngradeThreshold) * 3;

exit:
    return false;
}

otError MleRouter::SetAssignParentPriority(int8_t aParentPriority)
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aParentPriority <= kParentPriorityHigh && aParentPriority >= kParentPriorityUnspecified,
                 error = OT_ERROR_INVALID_ARGS);

    mParentPriority = aParentPriority;

exit:
    return error;
}

otError MleRouter::GetMaxChildTimeout(uint32_t &aTimeout) const
{
    otError error = OT_ERROR_NOT_FOUND;

    VerifyOrExit(IsRouterOrLeader(), error = OT_ERROR_INVALID_STATE);

    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValid); !iter.IsDone(); iter++)
    {
        Child &child = *iter.GetChild();

        if (child.IsFullThreadDevice())
        {
            continue;
        }

        if (child.GetTimeout() > aTimeout)
        {
            aTimeout = child.GetTimeout();
        }

        error = OT_ERROR_NONE;
    }

exit:
    return error;
}

void MleRouter::Signal(otNeighborTableEvent aEvent, Neighbor &aNeighbor)
{
    if (mNeighborTableChangedCallback != nullptr)
    {
        otNeighborTableEntryInfo info;
        otError                  error;

        OT_UNUSED_VARIABLE(error);

        info.mInstance = &GetInstance();

        switch (aEvent)
        {
        case OT_NEIGHBOR_TABLE_EVENT_CHILD_ADDED:
        case OT_NEIGHBOR_TABLE_EVENT_CHILD_REMOVED:
            error = GetChildInfo(static_cast<Child &>(aNeighbor), info.mInfo.mChild);
            OT_ASSERT(error == OT_ERROR_NONE);
            break;

        case OT_NEIGHBOR_TABLE_EVENT_ROUTER_ADDED:
        case OT_NEIGHBOR_TABLE_EVENT_ROUTER_REMOVED:
            GetNeighborInfo(aNeighbor, info.mInfo.mRouter);
            break;
        }

        mNeighborTableChangedCallback(aEvent, &info);
    }

#if OPENTHREAD_CONFIG_OTNS_ENABLE
    Get<Utils::Otns>().EmitNeighborChange(aEvent, aNeighbor);
#endif

    switch (aEvent)
    {
    case OT_NEIGHBOR_TABLE_EVENT_CHILD_ADDED:
        Get<Notifier>().Signal(kEventThreadChildAdded);
        break;

    case OT_NEIGHBOR_TABLE_EVENT_CHILD_REMOVED:
        Get<Notifier>().Signal(kEventThreadChildRemoved);
        break;

    default:
        break;
    }
}

bool MleRouter::HasSleepyChildrenSubscribed(const Ip6::Address &aAddress)
{
    bool rval = false;

    for (ChildTable::Iterator iter(GetInstance(), Child::kInStateValidOrRestoring); !iter.IsDone(); iter++)
    {
        Child &child = *iter.GetChild();

        if (child.IsRxOnWhenIdle())
        {
            continue;
        }

        if (IsSleepyChildSubscribed(aAddress, child))
        {
            ExitNow(rval = true);
        }
    }

exit:
    return rval;
}

bool MleRouter::IsSleepyChildSubscribed(const Ip6::Address &aAddress, Child &aChild)
{
    return aChild.IsStateValidOrRestoring() && !aChild.IsRxOnWhenIdle() && aChild.HasIp6Address(aAddress);
}

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
void MleRouter::HandleTimeSync(const Message &aMessage, const Ip6::MessageInfo &aMessageInfo, const Neighbor *aNeighbor)
{
    LogMleMessage("Receive Time Sync", aMessageInfo.GetPeerAddr());

    VerifyOrExit(aNeighbor && aNeighbor->IsStateValid(), OT_NOOP);

    Get<TimeSync>().HandleTimeSyncMessage(aMessage);

exit:
    return;
}

otError MleRouter::SendTimeSync(void)
{
    otError      error = OT_ERROR_NONE;
    Ip6::Address destination;
    Message *    message = nullptr;

    VerifyOrExit((message = NewMleMessage()) != nullptr, error = OT_ERROR_NO_BUFS);
    SuccessOrExit(error = AppendHeader(*message, Header::kCommandTimeSync));

    message->SetTimeSync(true);

    destination.SetToLinkLocalAllNodesMulticast();
    SuccessOrExit(error = SendMessage(*message, destination));

    LogMleMessage("Send Time Sync", destination);

exit:

    if (error != OT_ERROR_NONE && message != nullptr)
    {
        message->Free();
    }

    return error;
}
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE

} // namespace Mle
} // namespace ot

#endif // OPENTHREAD_FTD

redpajama_set_name: RedPajamaGithub