/*
 *  Copyright (c) 2016, The OpenThread Authors.  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the copyright holder nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

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

#include "mle.hpp"

#if OPENTHREAD_FTD

#include "instance/instance.hpp"

namespace ot {
namespace Mle {

RegisterLogModule("Mle");

void Mle::SetAlternateRloc16(uint16_t aRloc16)
{
    VerifyOrExit(aRloc16 != Mac::kShortAddrInvalid);

    LogInfo("Setting alternate RLOC16 0x%04x", aRloc16);

    Get<Mac::Mac>().SetAlternateShortAddress(aRloc16);
    mAlternateRloc16Timeout = kAlternateRloc16Timeout;

exit:
    return;
}

void Mle::ClearAlternateRloc16(void)
{
    VerifyOrExit(Get<Mac::Mac>().GetAlternateShortAddress() != Mac::kShortAddrInvalid);

    LogInfo("Clearing alternate RLOC16");
    Get<Mac::Mac>().SetAlternateShortAddress(Mac::kShortAddrInvalid);

exit:
    mAlternateRloc16Timeout = 0;
}

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

    Get<AddressResolver>().Clear();
    IgnoreError(Get<Tmf::Agent>().AbortTransaction(HandleAddressSolicitResponse, this));
    mRouterTable.Clear();
}

bool Mle::RoleTransitioner::DetermineIfRouterRoleAllowed(void) const
{
    bool                  allowed   = false;
    const SecurityPolicy &secPolicy = Get<KeyManager>().GetSecurityPolicy();

    VerifyOrExit(mRouterEligible && Get<Mle>().IsFullThreadDevice());

#if OPENTHREAD_CONFIG_THREAD_VERSION == OT_THREAD_VERSION_1_1
    VerifyOrExit(secPolicy.mRoutersEnabled);
#else

    if (secPolicy.mCommercialCommissioningEnabled)
    {
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        VerifyOrExit(mCcmEnabled || secPolicy.mNonCcmRoutersEnabled);
#else
        VerifyOrExit(secPolicy.mNonCcmRoutersEnabled);
#endif
    }

    if (!secPolicy.mRoutersEnabled)
    {
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        VerifyOrExit(!mThreadVersionCheckEnabled ||
                     secPolicy.mVersionThresholdForRouting + SecurityPolicy::kVersionThresholdOffsetVersion <=
                         kThreadVersion);
#else
        VerifyOrExit(secPolicy.mVersionThresholdForRouting + SecurityPolicy::kVersionThresholdOffsetVersion <=
                     kThreadVersion);
#endif
    }

#endif // #if OPENTHREAD_CONFIG_THREAD_VERSION == OT_THREAD_VERSION_1_1

    allowed = true;

exit:
    return allowed;
}

void Mle::RoleTransitioner::UpdateRouterRoleAllowed(UpdateRouterRoleAllowedReason aReason)
{
    bool allowed = DetermineIfRouterRoleAllowed();

    VerifyOrExit(allowed != mRouterRoleAllowed);
    mRouterRoleAllowed = allowed;

    if (Get<Mle>().IsAttached())
    {
        Get<Mac::Mac>().SetBeaconEnabled(mRouterRoleAllowed);
    }

    // Take action based on the current role, the new `mRouterRoleAllowed`,
    // and the reason for the change.

    if (Get<Mle>().IsChild() && mRouterRoleAllowed && (aReason == kReasonConfigParameterChanged))
    {
        StartTimeout();
    }

    if (Get<Mle>().IsRouterOrLeader())
    {
        // If currently acting as router or leader, but the config or
        // security policy changes such that the router role is no
        // longer allowed, we take action based on the reason. If the
        // change is due to a security policy update, we start a jitter
        // timeout to downgrade (per Section 5.9.9 in the Thread spec),
        // adding an extra delay if acting as leader. If this is
        // triggered due to parameter change (`SetRouterEligible(false)`
        // was called by the user), we take immediate action and become
        // detached.

        VerifyOrExit(!mRouterRoleAllowed);

        switch (aReason)
        {
        case kReasonMleInit:
        case kReasonDeviceModeChanged:
            break;

        case kReasonConfigParameterChanged:
            IgnoreError(Get<Mle>().BecomeDetached());
            break;

        case kReasonSecurityPolicyChanged:
            VerifyOrExit(!IsTransitionPending());
            StartTimeout();

            if (Get<Mle>().IsLeader())
            {
                IncreaseTimeout(kLeaderDowngradeExtraDelay);
            }
            break;
        }
    }

exit:
    return;
}

Error Mle::RoleTransitioner::SetRouterEligible(bool aEligible)
{
    Error error = kErrorNone;

    if (!Get<Mle>().IsFullThreadDevice())
    {
        VerifyOrExit(!aEligible, error = kErrorNotCapable);
    }

    VerifyOrExit(aEligible != mRouterEligible);

    mRouterEligible = aEligible;
    UpdateRouterRoleAllowed(kReasonConfigParameterChanged);

exit:
    return error;
}

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
void Mle::RoleTransitioner::SetCcmEnabled(bool aEnabled)
{
    mCcmEnabled = aEnabled;
    UpdateRouterRoleAllowed(kReasonConfigParameterChanged);
}

void Mle::RoleTransitioner::SetThreadVersionCheckEnabled(bool aEnabled)
{
    mThreadVersionCheckEnabled = aEnabled;
    UpdateRouterRoleAllowed(kReasonConfigParameterChanged);
}
#endif

#if OPENTHREAD_CONFIG_MLE_DEVICE_PROPERTY_LEADER_WEIGHT_ENABLE
void Mle::SetDeviceProperties(const DeviceProperties &aDeviceProperties)
{
    mDeviceProperties = aDeviceProperties;
    mDeviceProperties.ClampWeightAdjustment();
    SetLeaderWeight(mDeviceProperties.CalculateLeaderWeight());
}
#endif

Error Mle::BecomeRouter(RouterUpgradeReason aReason)
{
    Error error = kErrorNone;

    switch (mRole)
    {
    case kRoleChild:
        break;

    case kRoleDisabled:
    case kRoleDetached:
        error = kErrorInvalidState;
        OT_FALL_THROUGH;

    case kRoleRouter:
    case kRoleLeader:
        ExitNow();
    }

    VerifyOrExit(IsRouterRoleAllowed(), error = kErrorNotCapable);

    LogInfo("Attempt to become router, reason:%s", RouterUpgradeReasonToString(aReason));

    Get<MeshForwarder>().SetRxOnWhenIdle(true);
    mRoleTransitioner.StopTimeout();

    error = SendAddressSolicit(aReason);

exit:
    return error;
}

Error Mle::BecomeLeader(LeaderWeightCheck aMode)
{
    Error   error = kErrorNone;
    Router *router;
    uint8_t leaderId;

#if OPENTHREAD_CONFIG_OPERATIONAL_DATASET_AUTO_INIT
    VerifyOrExit(!Get<MeshCoP::ActiveDatasetManager>().IsPartiallyComplete(), error = kErrorInvalidState);
#else
    VerifyOrExit(Get<MeshCoP::ActiveDatasetManager>().IsComplete(), error = kErrorInvalidState);
#endif
    VerifyOrExit(!IsDisabled(), error = kErrorInvalidState);
    VerifyOrExit(!IsLeader(), error = kErrorNone);
    VerifyOrExit(IsRouterRoleAllowed(), error = kErrorNotCapable);

    if ((aMode == kCheckLeaderWeight) && IsAttached())
    {
        VerifyOrExit(mLeaderWeight > mLeaderData.GetWeighting(), error = kErrorNotCapable);
    }

    mRouterTable.Clear();

    leaderId = SelectLeaderId();

    SetLeaderData(SelectPartitionId(), 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), kStartingAsLeader);

exit:
    return error;
}

uint8_t Mle::SelectLeaderId(void) const
{
    uint8_t minId;
    uint8_t maxId;

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    mRouterTable.GetRouterIdRange(minId, maxId);
#else
    minId = 0;
    maxId = kMaxRouterId;
#endif

    return IsValueInRange(mPreviousRouterId, minId, maxId) ? mPreviousRouterId
                                                           : Random::NonCrypto::GenerateInClosedRange(minId, maxId);
}

uint32_t Mle::SelectPartitionId(void) const
{
    uint32_t partitionId;

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    if (mPreferredLeaderPartitionId != 0)
    {
        partitionId = mPreferredLeaderPartitionId;
    }
    else
#endif
    {
        partitionId = Random::NonCrypto::Generate<uint32_t>();
    }

    return partitionId;
}

void Mle::StopLeader(void)
{
    StopAdvertiseTrickleTimer();
    Get<ThreadNetif>().UnsubscribeAllRoutersMulticast();
}

void Mle::HandleDetachStart(void)
{
    mRouterTable.ClearNeighbors();
    StopLeader();
    Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMle);
}

void Mle::HandleChildStart(void)
{
    mAddressSolicitRejected = false;

    mRoleTransitioner.StartTimeout();

    StopLeader();
    Get<TimeTicker>().RegisterReceiver(TimeTicker::kMle);

    Get<Mac::Mac>().SetBeaconEnabled(IsRouterRoleAllowed());

    Get<ThreadNetif>().SubscribeAllRoutersMulticast();

    VerifyOrExit(IsRouterIdValid(mPreviousRouterId));

    switch (mAttacher.GetAttachMode())
    {
    case kDowngradeToReed:
        SendAddressRelease();

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

        SetRouterId(kInvalidRouterId);
        break;

    case kSamePartition:
        if (HasChildren())
        {
            IgnoreError(BecomeRouter(kReasonHaveChildIdRequest));
        }

        break;

    case kAnyPartition:
    case kBetterParent:
    case kSelectedParent:
        // If attach was initiated due to receiving an MLE Announce
        // message, all rx-on-when-idle devices will immediately
        // attempt to attach as well. This aligns with the 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."
        //
        // Since parent-child relationships are unlikely to persist in
        // the new partition, we remove all children here. The
        // decision to become router is determined based on the new
        // partition's status.

        if (mAnnounceHandler.IsAnnounceAttaching() && HasChildren())
        {
            RemoveChildren();
        }

        OT_FALL_THROUGH;

    case kBetterPartition:
        if (HasChildren() && mPreviousPartitionIdRouter != mLeaderData.GetPartitionId())
        {
            IgnoreError(BecomeRouter(kReasonParentPartitionChange));
        }

        break;
    }

exit:

    if (!mRoleTransitioner.IsRouterCountBelowUpgradeThreshold() &&
        (!IsRouterIdValid(mPreviousRouterId) || !HasChildren()))
    {
        SetRouterId(kInvalidRouterId);
    }
}

void Mle::SetStateRouter(uint16_t aRloc16)
{
    // The `aStartMode` is ignored when used with `kRoleRouter`
    SetStateRouterOrLeader(kRoleRouter, aRloc16, /* aStartMode */ kStartingAsLeader);
}

void Mle::SetStateLeader(uint16_t aRloc16, LeaderStartMode aStartMode)
{
    SetStateRouterOrLeader(kRoleLeader, aRloc16, aStartMode);
}

void Mle::SetStateRouterOrLeader(DeviceRole aRole, uint16_t aRloc16, LeaderStartMode aStartMode)
{
    if (aRole == kRoleLeader)
    {
        IgnoreError(Get<MeshCoP::ActiveDatasetManager>().Restore());
        IgnoreError(Get<MeshCoP::PendingDatasetManager>().Restore());
    }

    SetRloc16(aRloc16);

    SetRole(aRole);

    mPrevRoleRestorer.Stop();
    mAttacher.CancelAttachOnRoleChange();

    mRetxTracker.Stop();
    StopAdvertiseTrickleTimer();
    ResetAdvertiseInterval();

    Get<ThreadNetif>().SubscribeAllRoutersMulticast();
    mPreviousPartitionIdRouter = mLeaderData.GetPartitionId();
    Get<Mac::Mac>().SetBeaconEnabled(true);
    Get<TimeTicker>().RegisterReceiver(TimeTicker::kMle);

    // Avoid informing an old parent when attaching next as a child after becoming an active router
    mPreviousParentRloc = kInvalidRloc16;

    if (aRole == kRoleLeader)
    {
        GetLeaderAloc(mLeaderAloc.GetAddress());
        Get<ThreadNetif>().AddUnicastAddress(mLeaderAloc);
        Get<NetworkData::Leader>().Start(aStartMode);
        Get<MeshCoP::ActiveDatasetManager>().StartLeader();
        Get<MeshCoP::PendingDatasetManager>().StartLeader();
        Get<AddressResolver>().Clear();
    }

    // Remove children that do not have a matching RLOC16
    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
    {
        if (RouterIdFromRloc16(child.GetRloc16()) != mRouterId)
        {
            RemoveNeighbor(child);
        }
    }

    LogNote("Partition ID 0x%lx", ToUlong(mLeaderData.GetPartitionId()));
}

void Mle::HandleAdvertiseTrickleTimer(TrickleTimer &aTimer) { aTimer.Get<Mle>().HandleAdvertiseTrickleTimer(); }

void Mle::HandleAdvertiseTrickleTimer(void)
{
    if (!IsRouterRoleAllowed())
    {
        mAdvertiseTrickleTimer.Stop();
    }
    else
    {
        SendMulticastAdvertisement();
    }
}

void Mle::StopAdvertiseTrickleTimer(void) { mAdvertiseTrickleTimer.Stop(); }

uint32_t Mle::DetermineAdvertiseIntervalMax(void) const
{
    uint32_t interval;

#if OPENTHREAD_CONFIG_MLE_LONG_ROUTES_ENABLE
    interval = kAdvIntervalMaxLogRoutes;
#else
    // Determine the interval based on the number of router neighbors
    // with link quality 2 or higher.

    interval = (Get<RouterTable>().GetNeighborCount(kLinkQuality2) + 1) * kAdvIntervalNeighborMultiplier;
    interval = Clamp(interval, kAdvIntervalMaxLowerBound, kAdvIntervalMaxUpperBound);
#endif

    return interval;
}

void Mle::HandleRouterTableEvent(RouterTable::Events aEvents)
{
    // Callback from `RouterTable` when there is a change.

    if (aEvents & RouterTable::kEventRouterAdded)
    {
        mRoleTransitioner.SetDowngradeBlocked(false);
    }

    if (IsRouterOrLeader() && mAdvertiseTrickleTimer.IsRunning())
    {
        mAdvertiseTrickleTimer.SetIntervalMax(DetermineAdvertiseIntervalMax());
    }
}

void Mle::ResetAdvertiseInterval(void)
{
    VerifyOrExit(IsRouterOrLeader());

    if (!mAdvertiseTrickleTimer.IsRunning())
    {
        mAdvertiseTrickleTimer.Start(TrickleTimer::kModeTrickle, kAdvIntervalMin, DetermineAdvertiseIntervalMax());
    }

    mAdvertiseTrickleTimer.IndicateInconsistent();

exit:
    return;
}

void Mle::SendMulticastAdvertisement(void)
{
    VerifyOrExit(IsRouterRoleAllowed());
    SendAdvertisement(Ip6::Address::GetLinkLocalAllNodesMulticast());

exit:
    return;
}

void Mle::ScheduleUnicastAdvertisementTo(const Router &aRouter)
{
    Ip6::Address destination;

    destination.InitAsLinkLocalAddress(aRouter.GetExtAddress());
    mDelayedSender.ScheduleAdvertisement(destination, GenerateRandomDelay(kMaxUnicastAdvertisementDelay));
}

void Mle::SendAdvertisement(const Ip6::Address &aDestination)
{
    Error      error   = kErrorNone;
    TxMessage *message = nullptr;

    // Suppress MLE Advertisements when trying to attach to a better
    // partition. Without this, a candidate parent might incorrectly
    // interpret this advertisement (Source Address TLV containing an
    // RLOC16 indicating device is acting as router) and reject the
    // attaching device.

    VerifyOrExit(!IsAttaching());

    // Suppress MLE Advertisements when attempting to transition to
    // router role. Advertisements as a REED while attaching to a new
    // partition can cause existing children to detach
    // unnecessarily.

    VerifyOrExit(!mAddressSolicitPending);

    VerifyOrExit((message = NewMleMessage(kCommandAdvertisement)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());

    switch (mRole)
    {
    case kRoleChild:
        break;

    case kRoleRouter:
    case kRoleLeader:
        SuccessOrExit(error = message->AppendRouteTlv());
        break;

    case kRoleDisabled:
    case kRoleDetached:
        OT_ASSERT(false);
    }

    SuccessOrExit(error = message->SendTo(aDestination));

    Log(kMessageSend, kTypeAdvertisement, aDestination);

exit:
    FreeMessageOnError(message, error);
    LogSendError(kTypeAdvertisement, error);
}

void Mle::SendLinkRequest(Router *aRouter)
{
    static const uint8_t kDetachedTlvs[]      = {Tlv::kAddress16, Tlv::kRoute};
    static const uint8_t kRouterTlvs[]        = {Tlv::kLinkMargin};
    static const uint8_t kValidNeighborTlvs[] = {Tlv::kLinkMargin, Tlv::kRoute};

    Error        error   = kErrorNone;
    TxMessage   *message = nullptr;
    Ip6::Address destination;

    destination.Clear();

    VerifyOrExit((message = NewMleMessage(kCommandLinkRequest)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendVersionTlv());

    switch (mRole)
    {
    case kRoleDetached:
        SuccessOrExit(error = message->AppendTlvRequestTlv(kDetachedTlvs));
        break;

    case kRoleChild:
        SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
        break;

    case kRoleRouter:
    case kRoleLeader:
        if (aRouter == nullptr || !aRouter->IsStateValid())
        {
            SuccessOrExit(error = message->AppendTlvRequestTlv(kRouterTlvs));
        }
        else
        {
            SuccessOrExit(error = message->AppendTlvRequestTlv(kValidNeighborTlvs));
        }

        SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
        break;

    case kRoleDisabled:
        OT_ASSERT(false);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    SuccessOrExit(error = message->AppendTimeRequestTlv());
#endif

    if (aRouter == nullptr)
    {
        mPrevRoleRestorer.GenerateRandomChallenge();
        SuccessOrExit(error = message->AppendChallengeTlv(mPrevRoleRestorer.GetChallenge()));
        destination = Ip6::Address::GetLinkLocalAllRoutersMulticast();
    }
    else
    {
        if (!aRouter->IsStateValid())
        {
            aRouter->GenerateChallenge();
            SuccessOrExit(error = message->AppendChallengeTlv(aRouter->GetChallenge()));
        }
        else
        {
            TxChallenge challenge;

            challenge.GenerateRandom();
            SuccessOrExit(error = message->AppendChallengeTlv(challenge));
        }

        destination.InitAsLinkLocalAddress(aRouter->GetExtAddress());
        aRouter->RestartLinkAcceptTimeout();
    }

    SuccessOrExit(error = message->SendTo(destination));

    Log(kMessageSend, kTypeLinkRequest, destination);

exit:
    FreeMessageOnError(message, error);
}

void Mle::HandleLinkRequest(RxInfo &aRxInfo)
{
    Error          error    = kErrorNone;
    Neighbor      *neighbor = nullptr;
    uint16_t       version;
    LeaderData     leaderData;
    LinkAcceptInfo info;

    Log(kMessageReceive, kTypeLinkRequest, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterOrLeader(), error = kErrorInvalidState);

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

    SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(info.mRxChallenge));

    SuccessOrExit(error = aRxInfo.mMessage.ReadVersionTlv(version));

    switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
    {
    case kErrorNone:
        VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId(), error = kErrorInvalidState);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    info.mExtAddress.SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid());

    info.mLinkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessage.GetAverageRss());

    switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, info.mRloc16))
    {
    case kErrorNone:
        if (IsRouterRloc16(info.mRloc16))
        {
            Router *router = mRouterTable.FindRouterByRloc16(info.mRloc16);

            VerifyOrExit(router != nullptr, error = kErrorParse);

            if (!router->IsStateValid())
            {
                InitNeighbor(*router, aRxInfo);
                router->SetState(Neighbor::kStateLinkRequest);
                router->ClearLinkAcceptTimeout();
            }
            else
            {
                VerifyOrExit(router->GetExtAddress() == info.mExtAddress);
            }

            neighbor = router;
        }

        break;

    case kErrorNotFound:
        // A missing source address indicates that the router was
        // recently reset.
        VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorDrop);
        info.mRloc16 = aRxInfo.mNeighbor->GetRloc16();
        VerifyOrExit(IsRouterRloc16(info.mRloc16), error = kErrorDrop);
        neighbor = aRxInfo.mNeighbor;
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    switch (aRxInfo.mMessage.ReadTlvRequestTlv(info.mRequestedTlvList))
    {
    case kErrorNone:
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (neighbor != nullptr)
    {
        neighbor->SetTimeSyncEnabled(Tlv::Find<TimeRequestTlv>(aRxInfo.mMessage, nullptr, 0) == kErrorNone);
    }
#endif

#if OPENTHREAD_CONFIG_MULTI_RADIO
    if (neighbor != nullptr)
    {
        neighbor->ClearLastRxFragmentTag();
    }
#endif

    aRxInfo.mClass = RxInfo::kPeerMessage;
    ProcessKeySequence(aRxInfo);

    if (aRxInfo.mMessageInfo.GetSockAddr().IsMulticast())
    {
        mDelayedSender.ScheduleLinkAccept(info, GenerateRandomDelay(kMaxLinkAcceptDelay));
    }
    else
    {
        error = SendLinkAccept(info);
    }

exit:
    LogProcessError(kTypeLinkRequest, error);
    OT_UNUSED_VARIABLE(neighbor);
}

Error Mle::SendLinkAccept(const LinkAcceptInfo &aInfo)
{
    static const uint8_t kRouterTlvs[] = {Tlv::kLinkMargin};

    Error        error   = kErrorNone;
    TxMessage   *message = nullptr;
    Command      command = kCommandLinkAccept;
    Router      *router;
    Ip6::Address destination;

    VerifyOrExit(IsAttached());

    router = mRouterTable.FindRouter(aInfo.mExtAddress);

    if (router != nullptr)
    {
        if (router->IsStateLinkRequest())
        {
            command = kCommandLinkAcceptAndRequest;
            router->SetLastHeard(TimerMilli::GetNow());
        }
        else
        {
            VerifyOrExit(router->IsStateValid());
        }
    }

    VerifyOrExit((message = NewMleMessage(command)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendVersionTlv());
    SuccessOrExit(error = message->AppendSourceAddressTlv());
    SuccessOrExit(error = message->AppendResponseTlv(aInfo.mRxChallenge));
    SuccessOrExit(error = message->AppendLinkAndMleFrameCounterTlvs());

    SuccessOrExit(error = message->AppendLinkMarginTlv(aInfo.mLinkMargin));

    if (router != nullptr)
    {
        SuccessOrExit(error = message->AppendLeaderDataTlv());
    }

    for (uint8_t tlvType : aInfo.mRequestedTlvList)
    {
        switch (tlvType)
        {
        case Tlv::kRoute:
            SuccessOrExit(error = message->AppendCompactRouteTlv(aInfo.mRloc16));
            break;

        case Tlv::kAddress16:
            VerifyOrExit(router != nullptr, error = kErrorDrop);
            SuccessOrExit(error = message->AppendAddress16Tlv(router->GetRloc16()));
            break;

        case Tlv::kLinkMargin:
            break;

        default:
            ExitNow(error = kErrorDrop);
        }
    }

    if (command == kCommandLinkAcceptAndRequest)
    {
        router->GenerateChallenge();

        SuccessOrExit(error = message->AppendChallengeTlv(router->GetChallenge()));
        SuccessOrExit(error = message->AppendTlvRequestTlv(kRouterTlvs));
    }

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

    destination.InitAsLinkLocalAddress(aInfo.mExtAddress);

    SuccessOrExit(error = message->SendTo(destination));

    Log(kMessageSend, (command == kCommandLinkAccept) ? kTypeLinkAccept : kTypeLinkAcceptAndRequest, destination);

exit:
    FreeMessageOnError(message, error);
    return error;
}

void Mle::HandleLinkAccept(RxInfo &aRxInfo) { HandleLinkAcceptVariant(aRxInfo, kTypeLinkAccept); }

void Mle::HandleLinkAcceptAndRequest(RxInfo &aRxInfo) { HandleLinkAcceptVariant(aRxInfo, kTypeLinkAcceptAndRequest); }

void Mle::HandleLinkAcceptVariant(RxInfo &aRxInfo, MessageType aMessageType)
{
    // Handles "Link Accept" or "Link Accept And Request".

    Error           error = kErrorNone;
    Router         *router;
    Neighbor::State neighborState;
    uint16_t        version;
    RxChallenge     response;
    uint16_t        sourceAddress;
    uint32_t        linkFrameCounter;
    uint32_t        mleFrameCounter;
    uint8_t         routerId;
    uint16_t        address16;
    RouteTlv::Data  routeTlvData;
    LeaderData      leaderData;
    uint8_t         linkMargin;
    bool            shouldUpdateRoutes = false;

    SuccessOrExit(error = Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress));

    Log(kMessageReceive, aMessageType, aRxInfo.mMessageInfo.GetPeerAddr(), sourceAddress);

    VerifyOrExit(IsRouterRloc16(sourceAddress), error = kErrorParse);

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

    SuccessOrExit(error = aRxInfo.mMessage.ReadResponseTlv(response));

    switch (neighborState)
    {
    case Neighbor::kStateLinkRequest:
        VerifyOrExit(response == router->GetChallenge(), error = kErrorSecurity);
        break;

    case Neighbor::kStateInvalid:
        VerifyOrExit(mPrevRoleRestorer.IsRestoringRouterOrLeaderRole(), error = kErrorSecurity);
        VerifyOrExit(response == mPrevRoleRestorer.GetChallenge(), error = kErrorSecurity);
        break;

    case Neighbor::kStateValid:
        break;

    default:
        ExitNow(error = kErrorSecurity);
    }

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

    SuccessOrExit(error = aRxInfo.mMessage.ReadVersionTlv(version));

    SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));

    switch (Tlv::Find<LinkMarginTlv>(aRxInfo.mMessage, linkMargin))
    {
    case kErrorNone:
        break;
    case kErrorNotFound:
        // The Link Margin TLV may be omitted after a reset. We wait
        // for MLE Advertisements to establish the routing cost to
        // the neighbor
        VerifyOrExit(IsDetached(), error = kErrorNotFound);
        linkMargin = 0;
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (mRole)
    {
    case kRoleDetached:
        SuccessOrExit(error = Tlv::Find<Address16Tlv>(aRxInfo.mMessage, address16));
        VerifyOrExit(GetRloc16() == address16, error = kErrorDrop);

        SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
        SetLeaderData(leaderData);

        mRouterTable.Clear();
        SuccessOrExit(error = aRxInfo.mMessage.ReadRouteTlv(routeTlvData));
        SuccessOrExit(error = ProcessRouteTlv(routeTlvData, aRxInfo));
        router = mRouterTable.FindRouterById(routerId);
        VerifyOrExit(router != nullptr);

        if (GetLeaderRloc16() == GetRloc16())
        {
            SetStateLeader(GetRloc16(), kRestoringLeaderRoleAfterReset);
        }
        else
        {
            SetStateRouter(GetRloc16());
        }

        mRetrieveNewNetworkData = true;
        IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
        shouldUpdateRoutes = true;

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

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

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

        SuccessOrExit(error = aRxInfo.mMessage.ReadLeaderDataTlv(leaderData));
        VerifyOrExit(leaderData.GetPartitionId() == mLeaderData.GetPartitionId());

        if (mRetrieveNewNetworkData ||
            SerialNumber::IsGreater(leaderData.GetDataVersion(NetworkData::kFullSet),
                                    Get<NetworkData::Leader>().GetVersion(NetworkData::kFullSet)))
        {
            IgnoreError(SendDataRequest(aRxInfo.mMessageInfo.GetPeerAddr()));
        }

        switch (aRxInfo.mMessage.ReadRouteTlv(routeTlvData))
        {
        case kErrorNone:
            VerifyOrExit(routeTlvData.IsAllocated(routerId), error = kErrorParse);

            if (mRouterTable.IsRouteTlvIdSequenceMoreRecent(routeTlvData))
            {
                SuccessOrExit(error = ProcessRouteTlv(routeTlvData, aRxInfo));
                router = mRouterTable.FindRouterById(routerId);
                OT_ASSERT(router != nullptr);
            }
            shouldUpdateRoutes = true;
            break;

        case kErrorNotFound:
            break;

        default:
            ExitNow(error = kErrorParse);
        }

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

        break;

    case kRoleDisabled:
        OT_ASSERT(false);
    }

    InitNeighbor(*router, aRxInfo);
    router->SetRloc16(sourceAddress);
    router->GetLinkFrameCounters().SetAll(linkFrameCounter);
    router->SetLinkAckFrameCounter(linkFrameCounter);
    router->SetMleFrameCounter(mleFrameCounter);
    router->SetVersion(version);
    router->SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                     DeviceMode::kModeFullNetworkData));
    router->SetLinkQualityOut(LinkQualityForLinkMargin(linkMargin));
    router->SetState(Neighbor::kStateValid);
    router->SetKeySequence(aRxInfo.mKeySequence);
    router->ClearLinkAcceptTimeout();

    mNeighborTable.Signal(NeighborTable::kRouterAdded, *router);

    mDelayedSender.RemoveScheduledLinkRequest(*router);

    if (shouldUpdateRoutes)
    {
        mRouterTable.UpdateRoutes(routeTlvData, routerId);
    }

    aRxInfo.mClass = RxInfo::kAuthoritativeMessage;
    ProcessKeySequence(aRxInfo);

    if (aMessageType == kTypeLinkAcceptAndRequest)
    {
        LinkAcceptInfo info;

        SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(info.mRxChallenge));

        switch (aRxInfo.mMessage.ReadTlvRequestTlv(info.mRequestedTlvList))
        {
        case kErrorNone:
        case kErrorNotFound:
            break;
        default:
            ExitNow(error = kErrorParse);
        }

        info.mExtAddress = router->GetExtAddress();
        info.mRloc16     = router->GetRloc16();
        info.mLinkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessage.GetAverageRss());

        SuccessOrExit(error = SendLinkAccept(info));
    }

exit:
    LogProcessError(aMessageType, error);
}

Error Mle::ProcessRouteTlv(const RouteTlv::Data &aRouteTlvData, RxInfo &aRxInfo)
{
    // This method processes `aRouteTlvData` read from an MLE message.
    //
    // During processing of Route TLV, the entries in the router table
    // may shuffle. This method ensures that the `aRxInfo.mNeighbor`
    // (which indicates the neighbor from which the MLE message was
    // received) is correctly updated to point to the same neighbor
    // (in case `mNeighbor` was pointing to a router entry from the
    // `RouterTable`).

    Error    error          = kErrorNone;
    uint16_t neighborRloc16 = kInvalidRloc16;

    if ((aRxInfo.mNeighbor != nullptr) && Get<RouterTable>().Contains(*aRxInfo.mNeighbor))
    {
        neighborRloc16 = aRxInfo.mNeighbor->GetRloc16();
    }

    mRouterTable.UpdateRouterIdMask(aRouteTlvData);

    if (IsAttached() && !mRouterTable.IsAllocated(RouterIdFromRloc16(GetRloc16())))
    {
        // Either we're a router and our own router ID has been removed from the router table,
        // or we're a child and our parent's router ID has been removed from the router table.
        // Detach immediately to minimize time where we're not connected.
        IgnoreError(BecomeDetached());
        error = kErrorNoRoute;
    }

    if (neighborRloc16 != kInvalidRloc16)
    {
        aRxInfo.mNeighbor = Get<NeighborTable>().FindNeighbor(neighborRloc16);
    }

    return error;
}

Error Mle::ReadAndProcessRouteTlvOnFtdChild(RxInfo &aRxInfo, uint8_t aParentId)
{
    // This method reads and processes Route TLV from message on an
    // FTD child if message contains one. It returns `kErrorNone`
    // when successfully processed or if there is no Route TLV in
    // the message.
    //
    // It MUST be used only when device is acting as a child and
    // for a message received from device's current parent.

    Error          error = kErrorNone;
    RouteTlv::Data routeTlvData;

    VerifyOrExit(IsFullThreadDevice());

    switch (aRxInfo.mMessage.ReadRouteTlv(routeTlvData))
    {
    case kErrorNone:
        SuccessOrExit(error = ProcessRouteTlv(routeTlvData, aRxInfo));
        mRouterTable.UpdateRouterOnFtdChild(routeTlvData, aParentId);
        mRequestRouteTlv = false;
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

exit:
    return error;
}

bool Mle::IsSingleton(void) const
{
    bool isSingleton = true;

    VerifyOrExit(IsAttached() && IsRouterRoleAllowed());
    isSingleton = (mRouterTable.GetActiveRouterCount() <= 1);

exit:
    return isSingleton;
}

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

    rval = ThreeWayCompare(aLeaderDataA.GetWeighting(), aLeaderDataB.GetWeighting());
    VerifyOrExit(rval == 0);

    // Not being a singleton is better.
    rval = ThreeWayCompare(!aSingletonA, !aSingletonB);
    VerifyOrExit(rval == 0);

    rval = ThreeWayCompare(aLeaderDataA.GetPartitionId(), aLeaderDataB.GetPartitionId());

exit:
    return rval;
}

Error Mle::HandleAdvertisementOnFtd(RxInfo &aRxInfo, uint16_t aSourceAddress, const LeaderData &aLeaderData)
{
    // This method processes a received MLE Advertisement message on
    // an FTD device. It is called from `Mle::HandleAdvertisement()`
    // only when device is attached (in child, router, or leader roles)
    // and `IsFullThreadDevice()`.
    //
    // - `aSourceAddress` is the read value from `SourceAddressTlv`.
    // - `aLeaderData` is the read value from `LeaderDataTlv`.

    Error          error      = kErrorNone;
    uint8_t        linkMargin = Get<Mac::Mac>().ComputeLinkMargin(aRxInfo.mMessage.GetAverageRss());
    RouteTlv::Data routeTlvData;
    bool           hasRouteTlv = false;
    Router        *router;
    uint8_t        routerId;
    uint32_t       delay;

    switch (aRxInfo.mMessage.ReadRouteTlv(routeTlvData))
    {
    case kErrorNone:
        hasRouteTlv = true;
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Handle Partition ID mismatch

    if (aLeaderData.GetPartitionId() != mLeaderData.GetPartitionId())
    {
        LogNote("Different partition (peer:%lu, local:%lu)", ToUlong(aLeaderData.GetPartitionId()),
                ToUlong(mLeaderData.GetPartitionId()));

        VerifyOrExit(linkMargin >= kPartitionMergeMinMargin, error = kErrorLinkMarginLow);

        if (hasRouteTlv && (mPreviousPartitionIdTimeout > 0) && (aLeaderData.GetPartitionId() == mPreviousPartitionId))
        {
            VerifyOrExit(
                SerialNumber::IsGreater(routeTlvData.GetRouterIdSequence(), mPreviousPartitionRouterIdSequence),
                error = kErrorDrop);
        }

        if (IsChild() && (aRxInfo.mNeighbor == &mParent))
        {
            ExitNow();
        }

        if (hasRouteTlv &&
            ComparePartitions(routeTlvData.IsSingleton(), aLeaderData, IsSingleton(), mLeaderData) > 0
#if OPENTHREAD_CONFIG_TIME_SYNC_REQUIRED
            // Allow a better partition if it also enables time sync.
            && aRxInfo.mMessage.GetTimeSyncSeq() != OT_TIME_SYNC_INVALID_SEQ
#endif
        )
        {
            mAttacher.Attach(kBetterPartition);
        }

        ExitNow(error = kErrorDrop);
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Handle Leader Router ID mismatch

    if (aLeaderData.GetLeaderRouterId() != GetLeaderId())
    {
        VerifyOrExit(aRxInfo.IsNeighborStateValid());

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

        ExitNow();
    }

    VerifyOrExit(IsRouterRloc16(aSourceAddress) && hasRouteTlv);
    routerId = RouterIdFromRloc16(aSourceAddress);

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

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Process `RouteTlv`

    if (aRxInfo.IsNeighborStateValid() && mRouterTable.IsRouteTlvIdSequenceMoreRecent(routeTlvData))
    {
        SuccessOrExit(error = ProcessRouteTlv(routeTlvData, aRxInfo));
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Update routers as a child

    if (IsChild())
    {
        if (aRxInfo.mNeighbor == &mParent)
        {
            // MLE Advertisement from parent
            router = &mParent;

            if (mParent.GetRloc16() != aSourceAddress)
            {
                IgnoreError(BecomeDetached());
                ExitNow(error = kErrorDetached);
            }

            mRouterTable.UpdateRouterOnFtdChild(routeTlvData, routerId);

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

            EstablishRouterLinkOnFtdChild(*router, aRxInfo, linkMargin);
        }

#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
        router->SetSelectableAsParent(true);
#endif

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

        ExitNow();
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Inform `RoleTransitioner` to decide whether we need to downgrade

    mRoleTransitioner.DecideWhetherToDowngrade(routerId, routeTlvData);

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Update routers as a router or leader.

    router = mRouterTable.FindRouterById(routerId);
    VerifyOrExit(router != nullptr);

    if (!router->IsStateValid() && aRxInfo.IsNeighborStateValid() && Get<ChildTable>().Contains(*aRxInfo.mNeighbor))
    {
        // The Adv is from a former child that is now acting as a router,
        // we copy the info from child entry and update the RLOC16.

        *static_cast<Neighbor *>(router) = *aRxInfo.mNeighbor;
        router->SetRloc16(Rloc16FromRouterId(routerId));
        router->SetDeviceMode(DeviceMode(DeviceMode::kModeFullThreadDevice | DeviceMode::kModeRxOnWhenIdle |
                                         DeviceMode::kModeFullNetworkData));

        mNeighborTable.Signal(NeighborTable::kRouterAdded, *router);

        // Change the cache entries associated with the former child
        // from using the old RLOC16 to its new RLOC16.
        Get<AddressResolver>().ReplaceEntriesForRloc16(aRxInfo.mNeighbor->GetRloc16(), router->GetRloc16());
    }

    // Send unicast link request if no link to router and no
    // unicast/multicast link request in progress

    if (!router->IsStateValid() && !router->IsStateLinkRequest() && (linkMargin >= kLinkRequestMinMargin) &&
        routeTlvData.IsAllocated(mRouterId))
    {
        InitNeighbor(*router, aRxInfo);
        router->SetState(Neighbor::kStateLinkRequest);
        router->ClearLinkAcceptTimeout();
        delay = GenerateRandomDelay(kMaxLinkRequestDelayOnRouter);
        mDelayedSender.ScheduleLinkRequest(*router, delay);
        ExitNow(error = kErrorNoRoute);
    }

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

    mRouterTable.UpdateRoutes(routeTlvData, routerId);

exit:
    if (aRxInfo.mNeighbor && aRxInfo.mNeighbor->GetRloc16() != aSourceAddress)
    {
        RemoveNeighbor(*aRxInfo.mNeighbor);
    }

    return error;
}

void Mle::EstablishRouterLinkOnFtdChild(Router &aRouter, RxInfo &aRxInfo, uint8_t aLinkMargin)
{
    // Decide on an FTD child whether to establish a link with a
    // router upon receiving an advertisement from it.

    uint8_t  neighborCount;
    uint32_t minDelay;
    uint32_t maxDelay;

    VerifyOrExit(!aRouter.IsStateValid() && !aRouter.IsStateLinkRequest());

    // The first `mChildRouterLinks` are established quickly. After that,
    // the "gradual router link establishment" mechanism is used, which
    // allows `kExtraChildRouterLinks` additional router links to be
    // established, but it is done slowly and over a longer span of time.
    //
    // Gradual router link establishment conditions:
    // - The maximum `neighborCount` limit is not yet reached.
    // - We see Link Quality 2 or better.
    // - Always skipped in the first 5 minutes
    //   (`kWaitDurationAfterAttach`) after the device attaches.
    // - The child randomly decides whether to perform/skip this (with a 5%
    //   probability, `kProbabilityPercentage`).
    // - If the child decides to send Link Request, a longer random delay
    //   window is used, [1.5-10] seconds.
    //
    // Even in a dense network, if the advertisement is received by 500 FTD
    // children with a 5% selection probability, on average, 25 nodes will
    // try to send a Link Request, which will be randomly spread over a
    // [1.5-10] second window.
    //
    // With a 5% probability, on average, it takes 20 trials (20 advertisement
    // receptions for an FTD child to send a Link Request). Advertisements
    // are, on average, ~32 seconds apart, so, on average, a child will try
    // to establish a link in `20 * 32 = 640` seconds (~10 minutes).

    neighborCount = mRouterTable.GetNeighborCount(kLinkQuality1);

    if (neighborCount < mChildRouterLinks)
    {
        minDelay = kMinLinkRequestDelayOnChild;
        maxDelay = kMaxLinkRequestDelayOnChild;
    }
    else
    {
        VerifyOrExit(neighborCount < mChildRouterLinks + GradualChildRouterLink::kExtraChildRouterLinks);
        VerifyOrExit(LinkQualityForLinkMargin(aLinkMargin) >= kLinkQuality2);
        VerifyOrExit(GetCurrentAttachDuration() > GradualChildRouterLink::kWaitDurationAfterAttach);
        VerifyOrExit(Random::NonCrypto::GenerateUpToExcluding<uint8_t>(100) <
                     GradualChildRouterLink::kProbabilityPercentage);

        minDelay = GradualChildRouterLink::kMinLinkRequestDelay;
        maxDelay = GradualChildRouterLink::kMaxLinkRequestDelay;
    }

    InitNeighbor(aRouter, aRxInfo);
    aRouter.SetState(Neighbor::kStateLinkRequest);
    aRouter.ClearLinkAcceptTimeout();
    mDelayedSender.ScheduleLinkRequest(aRouter, Random::NonCrypto::GenerateInClosedRange(minDelay, maxDelay));

exit:
    return;
}

void Mle::HandleParentRequest(RxInfo &aRxInfo)
{
    Error              error = kErrorNone;
    uint16_t           version;
    uint8_t            scanMask;
    Child             *child;
    DeviceMode         mode;
    uint32_t           delay;
    ParentResponseInfo info;

    Log(kMessageReceive, kTypeParentRequest, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterRoleAllowed());
    VerifyOrExit(!IsDetached() && !IsAttaching());

    VerifyOrExit(!mDetacher.IsDetaching());

    VerifyOrExit(mRouterTable.GetLeaderAge() < mNetworkIdTimeout, error = kErrorDrop);
    VerifyOrExit(mRouterTable.GetPathCostToLeader() < kMaxRouteCost, error = kErrorDrop);

    info.mChildExtAddress.SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid());

    SuccessOrExit(error = aRxInfo.mMessage.ReadVersionTlv(version));

    SuccessOrExit(error = Tlv::Find<ScanMaskTlv>(aRxInfo.mMessage, scanMask));

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

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

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

    SuccessOrExit(error = aRxInfo.mMessage.ReadChallengeTlv(info.mRxChallenge));

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

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

        InitNeighbor(*child, aRxInfo);
        child->SetState(Neighbor::kStateParentRequest);
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
        child->SetTimeSyncEnabled(Tlv::Find<TimeRequestTlv>(aRxInfo.mMessage, nullptr, 0) == kErrorNone);
#endif
        if (aRxInfo.mMessage.ReadModeTlv(mode) == kErrorNone)
        {
            child->SetDeviceMode(mode);
            child->SetVersion(version);
        }
    }
    else
    {
        uint32_t durSinceLastHeard = TimerMilli::GetNow() - child->GetLastHeard();

        VerifyOrExit(durSinceLastHeard >= kParentRequestDuplicateTimeout, error = kErrorDuplicated);
    }

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

    aRxInfo.mClass = RxInfo::kPeerMessage;
    ProcessKeySequence(aRxInfo);

    delay = GenerateRandomDelay(!ScanMaskTlv::IsEndDeviceFlagSet(scanMask) ? kParentResponseMaxDelayRouters
                                                                           : kParentResponseMaxDelayAll);
    mDelayedSender.ScheduleParentResponse(info, delay);

exit:
    LogProcessError(kTypeParentRequest, error);
}

bool Mle::HasNeighborWithGoodLinkQuality(void) const
{
    bool    haveNeighbor = true;
    uint8_t linkMargin;

    linkMargin = Get<Mac::Mac>().ComputeLinkMargin(mParent.GetLinkInfo().GetLastRss());

    if (linkMargin >= kLinkRequestMinMargin)
    {
        ExitNow();
    }

    for (const Router &router : Get<RouterTable>())
    {
        if (!router.IsStateValid())
        {
            continue;
        }

        linkMargin = Get<Mac::Mac>().ComputeLinkMargin(router.GetLinkInfo().GetLastRss());

        if (linkMargin >= kLinkRequestMinMargin)
        {
            ExitNow();
        }
    }

    haveNeighbor = false;

exit:
    return haveNeighbor;
}

void Mle::HandleTimeTick(void)
{
    VerifyOrExit(IsFullThreadDevice(), Get<TimeTicker>().UnregisterReceiver(TimeTicker::kMle));

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

    if (mAlternateRloc16Timeout > 0)
    {
        mAlternateRloc16Timeout--;

        if (mAlternateRloc16Timeout == 0)
        {
            ClearAlternateRloc16();
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Role transitions

    mRoleTransitioner.HandleTimeTick();

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Check Leader's age

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

    case kRoleChild:
        if (!IsRouterRoleAllowed())
        {
            break;
        }

        OT_FALL_THROUGH;

    case kRoleRouter:
        LogDebg("Leader age %lu", ToUlong(mRouterTable.GetLeaderAge()));

        if ((mRouterTable.GetActiveRouterCount() > 0) && (mRouterTable.GetLeaderAge() >= mNetworkIdTimeout))
        {
            LogInfo("Leader age timeout");
            mAttacher.Attach(kSamePartition);
        }

        break;
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Update `ChildTable`

    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateAnyExceptInvalid))
    {
        uint32_t timeout = 0;

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

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

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

#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
        if (child.IsCslSynchronized() &&
            TimerMilli::GetNow() - child.GetCslLastHeard() >= Time::SecToMsec(child.GetCslTimeout()))
        {
            LogInfo("Child 0x%04x CSL synchronization expired", child.GetRloc16());
            child.SetCslSynchronized(false);
            Get<CslTxScheduler>().Update();
        }
#endif

        if (TimerMilli::GetNow() - child.GetLastHeard() >= timeout)
        {
            LogInfo("Child 0x%04x timeout expired", child.GetRloc16());
            RemoveNeighbor(child);
        }
        else if (IsRouterOrLeader() && child.IsStateRestored())
        {
            IgnoreError(SendChildUpdateRequestToChild(child));
        }
    }

    //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // Update `RouterTable`

    for (Router &router : Get<RouterTable>())
    {
        uint32_t age;

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

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

#if OPENTHREAD_CONFIG_PARENT_SEARCH_ENABLE
        router.DecrementParentReselectTimeout();

        if (age >= kMaxNeighborAge)
        {
            router.SetSelectableAsParent(false);
        }
#endif
        if (router.IsStateValid())
        {
            // Neighbor router age and link recovery
            //
            // If the device is an FTD child and has more than
            // `mChildRouterLinks` neighbors, it uses a longer age,
            // `kMaxNeighborAgeOnChild`, and removes the neighboring
            // router upon expiration without trying to re-establish
            // its link with it.
            //
            // Otherwise, if the device itself is a router, or it is an
            // FTD child with `mChildRouterLinks` or fewer neighbors,
            // it uses a shorter `kMaxNeighborAge`. Upon expiration, it
            // tries to re-establish its link with the neighboring router.

            if (IsChild() && (mRouterTable.GetNeighborCount(kLinkQuality1) > mChildRouterLinks))
            {
                if (age >= kMaxNeighborAgeOnChild)
                {
                    LogInfo("No Adv from router 0x%04x - removing router", router.GetRloc16());
                    mDelayedSender.RemoveScheduledLinkRequest(router);
                    RemoveNeighbor(router);
                    continue;
                }
            }
            else if (age >= kMaxNeighborAge)
            {
                // We send a Link Request every time tick (second), up to
                // max attempts. Each transmission is randomly delayed
                // (one-second window). After the last attempt, we wait for
                // the "Link Accept" timeout (~3 seconds) before removing the
                // neighboring router.

                if (!mDelayedSender.HasAnyScheduledLinkRequest(router) && !router.IsWaitingForLinkAccept())
                {
                    LogInfo("No Adv from router 0x%04x - sending Link Request", router.GetRloc16());
                    router.SetLinkRequestAttemptsToMax();
                }

                if (router.HasRemainingLinkRequestAttempts())
                {
                    router.DecrementLinkRequestAttempts();
                    mDelayedSender.ScheduleLinkRequest(router, GenerateRandomDelay(kMaxLinkRequestDelayOnRouter));
                }
            }
        }

        if (router.IsStateLinkRequest() && !mDelayedSender.HasAnyScheduledLinkRequest(router) &&
            !router.IsWaitingForLinkAccept())
        {
            LogInfo("Router 0x%04x - Failed to schedule/send Link Request", router.GetRloc16());
            RemoveNeighbor(router);
            continue;
        }

        if (router.IsWaitingForLinkAccept() && (router.DecrementLinkAcceptTimeout() == 0))
        {
            LogInfo("Router 0x%04x - Link Accept timeout expired", router.GetRloc16());
            RemoveNeighbor(router);
            continue;
        }

        if (IsLeader() && (mRouterTable.FindNextHopTowards(router) == nullptr) &&
            (mRouterTable.GetLinkCost(router) >= kMaxRouteCost) && (age >= kMaxLeaderToRouterTimeout))
        {
            LogInfo("Router 0x%04x ID timeout expired (no route)", router.GetRloc16());
            IgnoreError(mRouterTable.Release(router.GetRouterId()));
        }
    }

    mRouterTable.HandleTimeTick();

    SynchronizeChildNetworkData();

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

exit:
    return;
}

void Mle::SendParentResponse(const ParentResponseInfo &aInfo)
{
    Error        error   = kErrorNone;
    TxMessage   *message = nullptr;
    Child       *child;
    Ip6::Address destination;

    child = mChildTable.FindChild(aInfo.mChildExtAddress, Child::kInStateAnyExceptInvalid);
    VerifyOrExit(child != nullptr);

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

    SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
    SuccessOrExit(error = message->AppendLinkAndMleFrameCounterTlvs());
    SuccessOrExit(error = message->AppendResponseTlv(aInfo.mRxChallenge));
#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    if (child->IsTimeSyncEnabled())
    {
        SuccessOrExit(error = message->AppendTimeParameterTlv());
    }
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
    if (child->IsThreadVersionCslCapable())
    {
        SuccessOrExit(error = message->AppendCslClockAccuracyTlv());
    }
#endif
    child->GenerateChallenge();
    SuccessOrExit(error = message->AppendChallengeTlv(child->GetChallenge()));
    SuccessOrExit(error = message->AppendLinkMarginTlv(child->GetLinkInfo().GetLinkMargin()));
    SuccessOrExit(error = message->AppendConnectivityTlv());
    SuccessOrExit(error = message->AppendVersionTlv());

    destination.InitAsLinkLocalAddress(aInfo.mChildExtAddress);

    SuccessOrExit(error = message->SendTo(destination));

    Log(kMessageSend, kTypeParentResponse, destination);

exit:
    FreeMessageOnError(message, error);
    LogSendError(kTypeParentResponse, error);
}

Error Mle::ProcessAddressRegistrationTlv(RxInfo &aRxInfo, Child &aChild)
{
    Error       error;
    OffsetRange offsetRange;
    uint8_t     count       = 0;
    uint8_t     storedCount = 0;
#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
    Ip6::Address oldDua;
#endif
#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
    Child::Ip6AddressArray oldMlrRegisteredAddresses;
#endif

    OT_UNUSED_VARIABLE(storedCount);

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aRxInfo.mMessage, Tlv::kAddressRegistration, offsetRange));

#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
    if (aChild.GetDomainUnicastAddress(oldDua) != kErrorNone)
    {
        oldDua.Clear();
    }
#endif

#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
    aChild.GetAllMlrRegisteredAddresses(oldMlrRegisteredAddresses);
#endif

    aChild.ClearIp6Addresses();

    while (!offsetRange.IsEmpty())
    {
        uint8_t      controlByte;
        Ip6::Address address;

        // Read out the control byte (first byte in entry)
        SuccessOrExit(error = aRxInfo.mMessage.ReadAndAdvance(offsetRange, controlByte));
        count++;

        address.Clear();

        if (AddressRegistrationTlv::IsEntryCompressed(controlByte))
        {
            // Compressed entry contains IID with the 64-bit prefix
            // determined from 6LoWPAN context identifier (from
            // the control byte).

            uint8_t         contextId = AddressRegistrationTlv::GetContextId(controlByte);
            Lowpan::Context context;

            IgnoreError(aRxInfo.mMessage.ReadAndAdvance(offsetRange, address.GetIid()));

            Get<NetworkData::Leader>().FindContextForId(contextId, context);

            if (!context.IsValid())
            {
                LogWarn("Failed to get context %u for compressed address from child 0x%04x", contextId,
                        aChild.GetRloc16());
                continue;
            }

            address.SetPrefix(context.GetPrefix());
        }
        else
        {
            // Uncompressed entry contains the full IPv6 address.

            IgnoreError(aRxInfo.mMessage.ReadAndAdvance(offsetRange, address));
        }

        error = aChild.AddIp6Address(address);

        if (error == kErrorNone)
        {
            storedCount++;
            LogInfo("Child 0x%04x IPv6 address[%u]=%s", aChild.GetRloc16(), storedCount,
                    address.ToString().AsCString());
        }
        else
        {
            LogWarnOnError(error, "add IPv6 address %s to child 0x%04x", 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 (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
        {
            if (&child == &aChild)
            {
                continue;
            }

            IgnoreError(child.RemoveIp6Address(address));
        }

        // Clear EID-to-RLOC cache for the unicast address registered by the child.
        Get<AddressResolver>().RemoveEntryForAddress(address);
    }
#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
    SignalDuaAddressEvent(aChild, oldDua);
#endif

#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
    Get<Mlr::Manager>().UpdateChildRegistrations(aChild, oldMlrRegisteredAddresses);
#endif

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

    error = kErrorNone;

exit:
    return error;
}

#if OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE
void Mle::SignalDuaAddressEvent(const Child &aChild, const Ip6::Address &aOldDua) const
{
    DuaManager::ChildDuaAddressEvent event = DuaManager::kAddressUnchanged;
    Ip6::Address                     newDua;

    if (aChild.GetDomainUnicastAddress(newDua) == kErrorNone)
    {
        if (aOldDua.IsUnspecified())
        {
            event = DuaManager::kAddressAdded;
        }
        else if (aOldDua != newDua)
        {
            event = DuaManager::kAddressChanged;
        }
    }
    else
    {
        // Child has no DUA address. If there was no old DUA, no need
        // to signal.

        VerifyOrExit(!aOldDua.IsUnspecified());

        event = DuaManager::kAddressRemoved;
    }

    Get<DuaManager>().HandleChildDuaAddressEvent(aChild, event);

exit:
    return;
}
#endif // OPENTHREAD_CONFIG_TMF_PROXY_DUA_ENABLE

bool Mle::IsMessageMleSubType(const Message &aMessage) { return aMessage.IsSubTypeMle(); }

bool Mle::IsMessageChildUpdateRequest(const Message &aMessage)
{
    return aMessage.IsMleCommand(kCommandChildUpdateRequest);
}

void Mle::HandleChildIdRequest(RxInfo &aRxInfo)
{
    Error              error = kErrorNone;
    Mac::ExtAddress    extAddr;
    uint16_t           version;
    uint32_t           linkFrameCounter;
    uint32_t           mleFrameCounter;
    DeviceMode         mode;
    uint32_t           timeout;
    TlvList            tlvList;
    MeshCoP::Timestamp timestamp;
    Child             *child;
    Router            *router;
    uint16_t           supervisionInterval;

    Log(kMessageReceive, kTypeChildIdRequest, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterRoleAllowed(), error = kErrorInvalidState);

    VerifyOrExit(IsAttached(), error = kErrorInvalidState);

    extAddr.SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid());

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

    SuccessOrExit(error = aRxInfo.mMessage.ReadVersionTlv(version));

    SuccessOrExit(error = aRxInfo.mMessage.ReadAndMatchResponseTlvWith(child->GetChallenge()));

    Get<MeshForwarder>().RemoveMessagesForChild(*child, IsMessageMleSubType);

    SuccessOrExit(error = aRxInfo.mMessage.ReadFrameCounterTlvs(linkFrameCounter, mleFrameCounter));

    SuccessOrExit(error = aRxInfo.mMessage.ReadModeTlv(mode));

    SuccessOrExit(error = Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout));

    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(tlvList));

    switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
    {
    case kErrorNone:
        tlvList.Add(Tlv::kSupervisionInterval);
        break;
    case kErrorNotFound:
        supervisionInterval = (version <= kThreadVersion1p3) ? kChildSupervisionDefaultIntervalForOlderVersion : 0;
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
    {
    case kErrorNone:
        if (timestamp == Get<MeshCoP::ActiveDatasetManager>().GetTimestamp())
        {
            break;
        }

        OT_FALL_THROUGH;

    case kErrorNotFound:
        tlvList.Add(Tlv::kActiveDataset);
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
    {
    case kErrorNone:
        if (timestamp == Get<MeshCoP::PendingDatasetManager>().GetTimestamp())
        {
            break;
        }

        OT_FALL_THROUGH;

    case kErrorNotFound:
        tlvList.Add(Tlv::kPendingDataset);
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    VerifyOrExit(tlvList.GetLength() <= Child::kMaxRequestTlvs, error = kErrorParse);

    if (!mode.IsFullThreadDevice())
    {
        SuccessOrExit(error = ProcessAddressRegistrationTlv(aRxInfo, *child));
    }

    router = mRouterTable.FindRouter(extAddr);

    if (router != nullptr)
    {
        RemoveNeighbor(*router);
    }

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

    child->SetLastHeard(TimerMilli::GetNow());
    child->GetLinkFrameCounters().SetAll(linkFrameCounter);
    child->SetLinkAckFrameCounter(linkFrameCounter);
    child->SetMleFrameCounter(mleFrameCounter);
    child->SetKeySequence(aRxInfo.mKeySequence);
    child->SetDeviceMode(mode);
    child->SetVersion(version);
    child->GetLinkInfo().AddRss(aRxInfo.mMessage.GetAverageRss());
    child->SetTimeout(timeout);
    child->SetSupervisionInterval(supervisionInterval);
#if OPENTHREAD_CONFIG_MULTI_RADIO
    child->ClearLastRxFragmentTag();
#endif

    child->SetNetworkDataVersion(mLeaderData.GetDataVersion(mode.GetNetworkDataType()));

    // We already checked above that `tlvList` will fit in
    // `child` entry (with `Child::kMaxRequestTlvs` TLVs).

    child->ClearRequestTlvs();

    for (uint8_t index = 0; index < tlvList.GetLength(); index++)
    {
        child->SetRequestTlv(index, tlvList[index]);
    }

    aRxInfo.mClass = RxInfo::kAuthoritativeMessage;
    ProcessKeySequence(aRxInfo);

    switch (mRole)
    {
    case kRoleChild:
        child->SetState(Neighbor::kStateChildIdRequest);
        IgnoreError(BecomeRouter(kReasonHaveChildIdRequest));
        break;

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

    case kRoleDisabled:
    case kRoleDetached:
        OT_ASSERT(false);
    }

exit:
    LogProcessError(kTypeChildIdRequest, error);
}

void Mle::HandleChildUpdateRequestOnParent(RxInfo &aRxInfo)
{
    Error                   error = kErrorNone;
    Mac::ExtAddress         extAddr;
    DeviceMode              mode;
    LeaderData              leaderData;
    uint32_t                timeout;
    uint16_t                supervisionInterval;
    Child                  *child;
    DeviceMode              oldMode;
    TlvList                 requestedTlvList;
    ChildUpdateResponseInfo info;
    bool                    childDidChange = false;

    Log(kMessageReceive, kTypeChildUpdateRequestOfChild, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(!mDetacher.IsDetaching());

    info.mDestination = aRxInfo.mMessageInfo.GetPeerAddr();

    SuccessOrExit(error = aRxInfo.mMessage.ReadModeTlv(mode));

    switch (aRxInfo.mMessage.ReadChallengeTlv(info.mChallenge))
    {
    case kErrorNone:
        info.mTlvList.Add(Tlv::kResponse);
        break;
    case kErrorNotFound:
        info.mChallenge.Clear();
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    info.mTlvList.Add(Tlv::kSourceAddress);

    extAddr.SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid());
    child = mChildTable.FindChild(extAddr, Child::kInStateAnyExceptInvalid);

    if (child == nullptr)
    {
        // For invalid non-sleepy child, send Child Update Response with
        // Status TLV (error).
        if (mode.IsRxOnWhenIdle())
        {
            IgnoreError(SendChildUpdateRejectResponse(info));
        }

        ExitNow();
    }

    // Ignore "Child Update Request" from a child that is present in the
    // child table but it is not yet in valid state. For example, a
    // child which is being restored (due to parent reset) or is in the
    // middle of the attach process (in `kStateParentRequest` or
    // `kStateChildIdRequest`).

    VerifyOrExit(child->IsStateValid());

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

    info.mTlvList.Add(Tlv::kMode);
    info.mTlvList.Add(Tlv::kLinkMargin);

    // Parent MUST include Leader Data TLV in Child Update Response
    info.mTlvList.Add(Tlv::kLeaderData);

    if (!info.mChallenge.IsEmpty())
    {
        info.mTlvList.Add(Tlv::kMleFrameCounter);
        info.mTlvList.Add(Tlv::kLinkFrameCounter);
    }

    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
    {
    case kErrorNone:
        info.mTlvList.Add(Tlv::kAddressRegistration);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
    {
    case kErrorNone:
        child->SetNetworkDataVersion(leaderData.GetDataVersion(child->GetNetworkDataType()));
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
    {
    case kErrorNone:
        if (child->GetTimeout() != timeout)
        {
            child->SetTimeout(timeout);
            childDidChange = true;
        }

        info.mTlvList.Add(Tlv::kTimeout);
        break;

    case kErrorNotFound:
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
    {
    case kErrorNone:
        info.mTlvList.Add(Tlv::kSupervisionInterval);
        break;

    case kErrorNotFound:
        supervisionInterval =
            (child->GetVersion() <= kThreadVersion1p3) ? kChildSupervisionDefaultIntervalForOlderVersion : 0;
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    child->SetSupervisionInterval(supervisionInterval);

    switch (aRxInfo.mMessage.ReadTlvRequestTlv(requestedTlvList))
    {
    case kErrorNone:
        info.mTlvList.AddElementsFrom(requestedTlvList);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
    if (child->IsCslSynchronized())
    {
        ChannelTlvValue cslChannelTlvValue;
        uint32_t        cslTimeout;

        switch (Tlv::Find<CslTimeoutTlv>(aRxInfo.mMessage, cslTimeout))
        {
        case kErrorNone:
            child->SetCslTimeout(cslTimeout);
            // MUST include CSL accuracy TLV when request includes CSL timeout
            info.mTlvList.Add(Tlv::kCslClockAccuracy);
            break;
        case kErrorNotFound:
            break;
        default:
            ExitNow(error = kErrorNone);
        }

        if (Tlv::Find<CslChannelTlv>(aRxInfo.mMessage, cslChannelTlvValue) == kErrorNone)
        {
            // Special value of zero is used to indicate that
            // CSL channel is not specified.
            child->SetCslChannel(static_cast<uint8_t>(cslChannelTlvValue.GetChannel()));
        }
    }
#endif // OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE

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

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

        childDidChange = true;

#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
        if (child->IsRxOnWhenIdle())
        {
            // Clear CSL synchronization state
            child->SetCslSynchronized(false);
        }
#endif

        // 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 (childDidChange)
    {
        IgnoreError(mChildTable.StoreChild(*child));
    }

#if OPENTHREAD_CONFIG_MULTI_RADIO
    // We clear the fragment tag only if the "Child Update Request" is
    // from a detached child trying to restore its link with its
    // parent which is indicated by the presence of Challenge TLV in
    // the message.
    if (!info.mChallenge.IsEmpty())
    {
        child->ClearLastRxFragmentTag();
    }
#endif

    SendChildUpdateResponseToChild(child, info);

    aRxInfo.mClass = RxInfo::kPeerMessage;

exit:
    LogProcessError(kTypeChildUpdateRequestOfChild, error);
}

void Mle::HandleChildUpdateResponseOnParent(RxInfo &aRxInfo)
{
    Error       error = kErrorNone;
    uint16_t    sourceAddress;
    uint32_t    timeout;
    RxChallenge response;
    uint8_t     status;
    uint32_t    linkFrameCounter;
    uint32_t    mleFrameCounter;
    LeaderData  leaderData;
    Child      *child;

    if ((aRxInfo.mNeighbor == nullptr) || IsRouterRloc16(aRxInfo.mNeighbor->GetRloc16()) ||
        !Get<ChildTable>().Contains(*aRxInfo.mNeighbor))
    {
        Log(kMessageReceive, kTypeChildUpdateResponseOfUnknownChild, aRxInfo.mMessageInfo.GetPeerAddr());
        ExitNow(error = kErrorNotFound);
    }

    child = static_cast<Child *>(aRxInfo.mNeighbor);

    switch (aRxInfo.mMessage.ReadResponseTlv(response))
    {
    case kErrorNone:
        VerifyOrExit(response == child->GetChallenge(), error = kErrorSecurity);
        break;
    case kErrorNotFound:
        VerifyOrExit(child->IsStateValid(), error = kErrorSecurity);
        response.Clear();
        break;
    default:
        ExitNow(error = kErrorNone);
    }

    Log(kMessageReceive, kTypeChildUpdateResponseOfChild, aRxInfo.mMessageInfo.GetPeerAddr(), child->GetRloc16());

    switch (Tlv::Find<SourceAddressTlv>(aRxInfo.mMessage, sourceAddress))
    {
    case kErrorNone:
        if (child->GetRloc16() != sourceAddress)
        {
            RemoveNeighbor(*child);
            ExitNow();
        }

        break;

    case kErrorNotFound:
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<StatusTlv>(aRxInfo.mMessage, status))
    {
    case kErrorNone:
        VerifyOrExit(status != kStatusError, RemoveNeighbor(*child));
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<LinkFrameCounterTlv>(aRxInfo.mMessage, linkFrameCounter))
    {
    case kErrorNone:
        child->GetLinkFrameCounters().SetAll(linkFrameCounter);
        child->SetLinkAckFrameCounter(linkFrameCounter);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<MleFrameCounterTlv>(aRxInfo.mMessage, mleFrameCounter))
    {
    case kErrorNone:
        child->SetMleFrameCounter(mleFrameCounter);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorNone);
    }

    switch (Tlv::Find<TimeoutTlv>(aRxInfo.mMessage, timeout))
    {
    case kErrorNone:
        child->SetTimeout(timeout);
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    {
        uint16_t supervisionInterval;

        switch (Tlv::Find<SupervisionIntervalTlv>(aRxInfo.mMessage, supervisionInterval))
        {
        case kErrorNone:
            child->SetSupervisionInterval(supervisionInterval);
            break;
        case kErrorNotFound:
            break;
        default:
            ExitNow(error = kErrorParse);
        }
    }

    switch (ProcessAddressRegistrationTlv(aRxInfo, *child))
    {
    case kErrorNone:
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    switch (aRxInfo.mMessage.ReadLeaderDataTlv(leaderData))
    {
    case kErrorNone:
        child->SetNetworkDataVersion(leaderData.GetDataVersion(child->GetNetworkDataType()));
        break;
    case kErrorNotFound:
        break;
    default:
        ExitNow(error = kErrorParse);
    }

    SetChildStateToValid(*child);
    child->SetLastHeard(TimerMilli::GetNow());
    child->SetKeySequence(aRxInfo.mKeySequence);
    child->GetLinkInfo().AddRss(aRxInfo.mMessage.GetAverageRss());

    aRxInfo.mClass = response.IsEmpty() ? RxInfo::kPeerMessage : RxInfo::kAuthoritativeMessage;

exit:
    LogProcessError(kTypeChildUpdateResponseOfChild, error);
}

void Mle::HandleDataRequest(RxInfo &aRxInfo)
{
    Error              error = kErrorNone;
    TlvList            tlvList;
    MeshCoP::Timestamp timestamp;

    Log(kMessageReceive, kTypeDataRequest, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(aRxInfo.IsNeighborStateValid(), error = kErrorSecurity);

    SuccessOrExit(error = aRxInfo.mMessage.ReadTlvRequestTlv(tlvList));

    switch (Tlv::Find<ActiveTimestampTlv>(aRxInfo.mMessage, timestamp))
    {
    case kErrorNone:
        if (timestamp == Get<MeshCoP::ActiveDatasetManager>().GetTimestamp())
        {
            break;
        }

        OT_FALL_THROUGH;

    case kErrorNotFound:
        tlvList.Add(Tlv::kActiveDataset);
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    switch (Tlv::Find<PendingTimestampTlv>(aRxInfo.mMessage, timestamp))
    {
    case kErrorNone:
        if (timestamp == Get<MeshCoP::PendingDatasetManager>().GetTimestamp())
        {
            break;
        }

        OT_FALL_THROUGH;

    case kErrorNotFound:
        tlvList.Add(Tlv::kPendingDataset);
        break;

    default:
        ExitNow(error = kErrorParse);
    }

    aRxInfo.mClass = RxInfo::kPeerMessage;
    ProcessKeySequence(aRxInfo);

    SendDataResponse(aRxInfo.mMessageInfo.GetPeerAddr(), tlvList, &aRxInfo.mMessage);

exit:
    LogProcessError(kTypeDataRequest, error);
}

void Mle::HandleNetworkDataUpdateRouter(void)
{
    VerifyOrExit(IsRouterOrLeader());

    if (IsLeader())
    {
        SendMulticastDataResponse();
    }
    else
    {
        mDelayedSender.ScheduleMulticastDataResponse(GenerateRandomDelay(kUnsolicitedDataResponseJitter));
    }

    SynchronizeChildNetworkData();

exit:
    return;
}

void Mle::SynchronizeChildNetworkData(void)
{
    VerifyOrExit(IsRouterOrLeader());

    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValid))
    {
        if (child.IsRxOnWhenIdle())
        {
            continue;
        }

        if (child.GetNetworkDataVersion() == Get<NetworkData::Leader>().GetVersion(child.GetNetworkDataType()))
        {
            continue;
        }

        SuccessOrExit(SendChildUpdateRequestToChild(child));
    }

exit:
    return;
}

#if OPENTHREAD_CONFIG_MLE_STEERING_DATA_SET_OOB_ENABLE
void Mle::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;

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

void Mle::HandleDiscoveryRequest(RxInfo &aRxInfo)
{
    Error                             error                     = kErrorNone;
    bool                              parsedDiscoveryRequestTlv = false;
    Tlv::Info                         tlvInfo;
    MeshCoP::DiscoveryRequestTlvValue discoveryRequestTlvValue;
    MeshCoP::ExtendedPanId            extPanId;
    OffsetRange                       offsetRange;
    DiscoveryResponseInfo             responseInfo;

    Log(kMessageReceive, kTypeDiscoveryRequest, aRxInfo.mMessageInfo.GetPeerAddr());

    VerifyOrExit(IsRouterRoleAllowed(), error = kErrorInvalidState);

    SuccessOrExit(error = Tlv::FindTlvValueOffsetRange(aRxInfo.mMessage, Tlv::kDiscovery, offsetRange));

    for (; !offsetRange.IsEmpty(); offsetRange.AdvanceOffset(tlvInfo.GetSize()))
    {
        SuccessOrExit(error = tlvInfo.ParseFrom(aRxInfo.mMessage, offsetRange));

        if (tlvInfo.IsExtended())
        {
            continue;
        }

        switch (tlvInfo.GetType())
        {
        case MeshCoP::Tlv::kDiscoveryRequest:
            SuccessOrExit(error =
                              tlvInfo.Read<MeshCoP::DiscoveryRequestTlv>(aRxInfo.mMessage, discoveryRequestTlvValue));
            parsedDiscoveryRequestTlv = true;
            break;

        case MeshCoP::Tlv::kExtendedPanId:
            SuccessOrExit(error = tlvInfo.Read<MeshCoP::ExtendedPanIdTlv>(aRxInfo.mMessage, extPanId));
            VerifyOrExit(Get<MeshCoP::NetworkIdentity>().GetExtPanId() != extPanId, error = kErrorDrop);

            break;

        default:
            break;
        }
    }

    if (parsedDiscoveryRequestTlv)
    {
#if OPENTHREAD_CONFIG_MLE_DISCOVERY_SCAN_REQUEST_CALLBACK_ENABLE
        if (mDiscoveryRequestCallback.IsSet())
        {
            DiscoveryRequestInfo info;

            AsCoreType(&info.mExtAddress).SetFromIid(aRxInfo.mMessageInfo.GetPeerAddr().GetIid());
            info.mVersion  = discoveryRequestTlvValue.GetVersion();
            info.mIsJoiner = discoveryRequestTlvValue.GetJoinerFlag();

            mDiscoveryRequestCallback.Invoke(&info);
        }
#endif

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

    responseInfo.mPanId = aRxInfo.mMessage.GetPanId();

#if OPENTHREAD_CONFIG_MULTI_RADIO
    // Send the MLE Discovery Response message on same radio link
    // from which the "MLE Discover Request" message was received.
    responseInfo.mRadioType = aRxInfo.mMessage.GetRadioType();
#endif

    mDelayedSender.ScheduleDiscoveryResponse(aRxInfo.mMessageInfo.GetPeerAddr(), responseInfo,
                                             GenerateRandomDelay(kDiscoveryMaxJitter));

exit:
    LogProcessError(kTypeDiscoveryRequest, error);
}

Error Mle::SendDiscoveryResponse(const Ip6::Address &aDestination, const DiscoveryResponseInfo &aInfo)
{
    Error                              error = kErrorNone;
    TxMessage                         *message;
    Tlv::Bookmark                      tlvBookmark;
    MeshCoP::DiscoveryResponseTlvValue discoveryResponseTlvValue;

    VerifyOrExit((message = NewMleMessage(kCommandDiscoveryResponse)) != nullptr, error = kErrorNoBufs);
    message->SetDirectTransmission();
    message->SetPanId(aInfo.mPanId);
#if OPENTHREAD_CONFIG_MULTI_RADIO
    message->SetRadioType(aInfo.mRadioType);
#endif

    SuccessOrExit(error = Tlv::StartTlv(*message, Tlv::kDiscovery, tlvBookmark));

    discoveryResponseTlvValue.Clear();
    discoveryResponseTlvValue.SetVersion(kThreadVersion);

#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE
    if (Get<KeyManager>().GetSecurityPolicy().mNativeCommissioningEnabled)
    {
        SuccessOrExit(error = Tlv::Append<MeshCoP::CommissionerUdpPortTlv>(
                          *message, Get<MeshCoP::BorderAgent::Manager>().GetUdpPort()));

        discoveryResponseTlvValue.SetNativeCommissionerFlag();
    }
#endif

    if (Get<KeyManager>().GetSecurityPolicy().mCommercialCommissioningEnabled)
    {
        discoveryResponseTlvValue.SetCcmFlag();
    }

    SuccessOrExit(error = Tlv::Append<MeshCoP::DiscoveryResponseTlv>(*message, discoveryResponseTlvValue));

    SuccessOrExit(error =
                      Tlv::Append<MeshCoP::ExtendedPanIdTlv>(*message, Get<MeshCoP::NetworkIdentity>().GetExtPanId()));

    SuccessOrExit(error = Tlv::Append<MeshCoP::NetworkNameTlv>(
                      *message, Get<MeshCoP::NetworkIdentity>().GetNetworkName().GetAsCString()));

    SuccessOrExit(error = message->AppendSteeringDataTlv());

    SuccessOrExit(
        error = Tlv::Append<MeshCoP::JoinerUdpPortTlv>(*message, Get<MeshCoP::JoinerRouter>().GetJoinerUdpPort()));

#if (OPENTHREAD_CONFIG_THREAD_VERSION >= OT_THREAD_VERSION_1_4)
    if (!Get<MeshCoP::NetworkIdentity>().IsDefaultDomainNameSet())
    {
        SuccessOrExit(error = Tlv::Append<MeshCoP::ThreadDomainNameTlv>(
                          *message, Get<MeshCoP::NetworkIdentity>().GetDomainName().GetAsCString()));
    }
#endif

    SuccessOrExit(error = Tlv::EndTlv(*message, tlvBookmark));

    SuccessOrExit(error = message->SendTo(aDestination));

    Log(kMessageSend, kTypeDiscoveryResponse, aDestination);

exit:
    FreeMessageOnError(message, error);
    LogProcessError(kTypeDiscoveryResponse, error);
    return error;
}

Error Mle::SendChildIdResponse(Child &aChild)
{
    Error        error = kErrorNone;
    Ip6::Address destination;
    TxMessage   *message;

    VerifyOrExit((message = NewMleMessage(kCommandChildIdResponse)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
    SuccessOrExit(error = message->AppendActiveAndPendingTimestampTlvs());

    if ((aChild.GetRloc16() == 0) || !HasMatchingRouterIdWith(aChild.GetRloc16()))
    {
        aChild.SetRloc16(mChildTable.AllocateNewChildRloc16());
    }

    SuccessOrExit(error = message->AppendAddress16Tlv(aChild.GetRloc16()));

    for (uint8_t i = 0; i < Child::kMaxRequestTlvs; i++)
    {
        switch (aChild.GetRequestTlv(i))
        {
        case Tlv::kNetworkData:
            SuccessOrExit(error = message->AppendNetworkDataTlv(aChild.GetNetworkDataType()));
            break;

        case Tlv::kRoute:
            SuccessOrExit(error = message->AppendRouteTlv());
            break;

        case Tlv::kActiveDataset:
            SuccessOrExit(error = message->AppendActiveDatasetTlv());
            break;

        case Tlv::kPendingDataset:
            SuccessOrExit(error = message->AppendPendingDatasetTlv());
            break;

        case Tlv::kSupervisionInterval:
            SuccessOrExit(error = message->AppendSupervisionIntervalTlv(aChild.GetSupervisionInterval()));
            break;

        default:
            break;
        }
    }

    if (!aChild.IsFullThreadDevice())
    {
        SuccessOrExit(error = message->AppendAddressRegistrationTlv(aChild));
    }

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

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

    destination.InitAsLinkLocalAddress(aChild.GetExtAddress());
    SuccessOrExit(error = message->SendTo(destination));

    SetChildStateToValid(aChild);

    Log(kMessageSend, kTypeChildIdResponse, destination, aChild.GetRloc16());

exit:
    FreeMessageOnError(message, error);
    return error;
}

Error Mle::SendChildUpdateRequestToChild(Child &aChild)
{
    static const uint8_t kTlvs[] = {Tlv::kTimeout, Tlv::kAddressRegistration};

    Error        error = kErrorNone;
    Ip6::Address destination;
    TxMessage   *message = nullptr;

    if (!aChild.IsRxOnWhenIdle() && aChild.IsStateRestoring())
    {
        // No need to send the resync "Child Update Request"
        // to the sleepy child if there is one already
        // queued.

        VerifyOrExit(!Get<IndirectSender>().HasQueuedMessageForSleepyChild(aChild, IsMessageChildUpdateRequest));
    }

    Get<MeshForwarder>().RemoveMessagesForChild(aChild, IsMessageChildUpdateRequest);

    VerifyOrExit((message = NewMleMessage(kCommandChildUpdateRequest)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
    SuccessOrExit(error = message->AppendNetworkDataTlv(aChild.GetNetworkDataType()));
    SuccessOrExit(error = message->AppendActiveAndPendingTimestampTlvs());

    if (aChild.IsStateValid())
    {
        SuccessOrExit(error = message->AppendLinkMarginTlv(aChild.GetLinkInfo().GetLinkMargin()));
    }
    else
    {
        SuccessOrExit(error = message->AppendTlvRequestTlv(kTlvs));

        if (!aChild.IsStateRestored())
        {
            // A random challenge is generated and saved when `aChild`
            // is first initialized in `kStateRestored`. We will use
            // the saved challenge here. This prevents overwriting
            // the saved challenge when the child is also detached
            // and happens to send a "Parent Request" in the window
            // where the parent transitions to the router/leader role
            // and before the parent sends the "Child Update Request".
            // This ensures that the same random challenge is
            // included in both "Parent Response" and "Child Update
            // Response," guaranteeing proper acceptance of the
            // child's "Child ID request".

            aChild.GenerateChallenge();
        }

        SuccessOrExit(error = message->AppendChallengeTlv(aChild.GetChallenge()));
    }

    destination.InitAsLinkLocalAddress(aChild.GetExtAddress());
    SuccessOrExit(error = message->SendTo(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);
    }

    Log(kMessageSend, kTypeChildUpdateRequestOfChild, destination, aChild.GetRloc16());

exit:
    FreeMessageOnError(message, error);
    return error;
}

void Mle::SendChildUpdateResponseToChild(Child *aChild, const ChildUpdateResponseInfo &aInfo)
{
    Error      error = kErrorNone;
    TxMessage *message;

    VerifyOrExit((message = NewMleMessage(kCommandChildUpdateResponse)) != nullptr, error = kErrorNoBufs);

    for (uint8_t tlvType : aInfo.mTlvList)
    {
        // Add all TLV types that do not depend on `child`

        switch (tlvType)
        {
        case Tlv::kStatus:
            SuccessOrExit(error = message->AppendStatusTlv(kStatusError));
            break;

        case Tlv::kLeaderData:
            SuccessOrExit(error = message->AppendLeaderDataTlv());
            break;

        case Tlv::kResponse:
            SuccessOrExit(error = message->AppendResponseTlv(aInfo.mChallenge));
            break;

        case Tlv::kSourceAddress:
            SuccessOrExit(error = message->AppendSourceAddressTlv());
            break;

        case Tlv::kMleFrameCounter:
            SuccessOrExit(error = message->AppendMleFrameCounterTlv());
            break;

        case Tlv::kLinkFrameCounter:
            SuccessOrExit(error = message->AppendLinkFrameCounterTlv());
            break;
        }

        // Make sure `child` is not null before adding TLV types
        // that can depend on it.

        if (aChild == nullptr)
        {
            continue;
        }

        switch (tlvType)
        {
        case Tlv::kAddressRegistration:
            SuccessOrExit(error = message->AppendAddressRegistrationTlv(*aChild));
            break;

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

        case Tlv::kNetworkData:
            SuccessOrExit(error = message->AppendNetworkDataTlv(aChild->GetNetworkDataType()));
            SuccessOrExit(error = message->AppendActiveAndPendingTimestampTlvs());
            break;

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

        case Tlv::kLinkMargin:
            SuccessOrExit(error = message->AppendLinkMarginTlv(aChild->GetLinkInfo().GetLinkMargin()));
            break;

        case Tlv::kSupervisionInterval:
            SuccessOrExit(error = message->AppendSupervisionIntervalTlv(aChild->GetSupervisionInterval()));
            break;

#if OPENTHREAD_CONFIG_MAC_CSL_TRANSMITTER_ENABLE
        case Tlv::kCslClockAccuracy:
            if (!aChild->IsRxOnWhenIdle())
            {
                SuccessOrExit(error = message->AppendCslClockAccuracyTlv());
            }
            break;
#endif
        }
    }

    SuccessOrExit(error = message->SendTo(aInfo.mDestination));

    if (aChild == nullptr)
    {
        Log(kMessageSend, kTypeChildUpdateResponseOfChild, aInfo.mDestination);
    }
    else
    {
        Log(kMessageSend, kTypeChildUpdateResponseOfChild, aInfo.mDestination, aChild->GetRloc16());
    }

exit:
    FreeMessageOnError(message, error);
}

void Mle::SendMulticastDataResponse(void)
{
    TlvList tlvList;

    tlvList.Add(Tlv::kNetworkData);
    SendDataResponse(Ip6::Address::GetLinkLocalAllNodesMulticast(), tlvList);
}

void Mle::SendDataResponse(const Ip6::Address &aDestination, const TlvList &aTlvList, const Message *aRequestMessage)
{
    OT_UNUSED_VARIABLE(aRequestMessage);

    Error      error   = kErrorNone;
    TxMessage *message = nullptr;
    Neighbor  *neighbor;

    VerifyOrExit(IsAttached());

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

    VerifyOrExit((message = NewMleMessage(kCommandDataResponse)) != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = message->AppendSourceAddressAndLeaderDataTlvs());
    SuccessOrExit(error = message->AppendActiveAndPendingTimestampTlvs());

    for (uint8_t tlvType : aTlvList)
    {
        switch (tlvType)
        {
        case Tlv::kNetworkData:
            neighbor = mNeighborTable.FindNeighbor(aDestination);
            SuccessOrExit(error = message->AppendNetworkDataTlv((neighbor != nullptr) ? neighbor->GetNetworkDataType()
                                                                                      : NetworkData::kFullSet));
            break;

        case Tlv::kActiveDataset:
            SuccessOrExit(error = message->AppendActiveDatasetTlv());
            break;

        case Tlv::kPendingDataset:
            SuccessOrExit(error = message->AppendPendingDatasetTlv());
            break;

        case Tlv::kRoute:
            SuccessOrExit(error = message->AppendRouteTlv());
            break;

#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
        case Tlv::kLinkMetricsReport:
            OT_ASSERT(aRequestMessage != nullptr);
            neighbor = mNeighborTable.FindNeighbor(aDestination);
            VerifyOrExit(neighbor != nullptr, error = kErrorInvalidState);
            SuccessOrExit(error = Get<LinkMetrics::Subject>().AppendReport(*message, *aRequestMessage, *neighbor));
            break;
#endif
        }
    }

    SuccessOrExit(error = message->SendTo(aDestination));
    Log(kMessageSend, kTypeDataResponse, aDestination);

exit:
    FreeMessageOnError(message, error);
    LogSendError(kTypeDataResponse, error);
}

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

    case kRoleRouter:
    case kRoleLeader:
        mRouterTable.RemoveRouterLink(aRouter);
        break;

    default:
        break;
    }
}

void Mle::RemoveNeighbor(Neighbor &aNeighbor)
{
    VerifyOrExit(!aNeighbor.IsStateInvalid());

    if (&aNeighbor == &mParent)
    {
        if (IsChild())
        {
            IgnoreError(BecomeDetached());
        }
    }
    else if (&aNeighbor == &GetParentCandidate())
    {
        mAttacher.ClearParentCandidate();
    }
    else if (IsChildRloc16(aNeighbor.GetRloc16()))
    {
        OT_ASSERT(mChildTable.Contains(aNeighbor));

        if (aNeighbor.IsStateValidOrRestoring())
        {
            mNeighborTable.Signal(NeighborTable::kChildRemoved, aNeighbor);
        }

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

        if (aNeighbor.IsFullThreadDevice())
        {
            Get<AddressResolver>().RemoveEntriesForRloc16(aNeighbor.GetRloc16());
        }

        mChildTable.RemoveStoredChild(static_cast<Child &>(aNeighbor));
    }
    else if (aNeighbor.IsStateValid())
    {
        OT_ASSERT(mRouterTable.Contains(aNeighbor));

        mNeighborTable.Signal(NeighborTable::kRouterRemoved, aNeighbor);
        mRouterTable.RemoveRouterLink(static_cast<Router &>(aNeighbor));
    }

    aNeighbor.GetLinkInfo().Clear();
    aNeighbor.SetState(Neighbor::kStateInvalid);
#if OPENTHREAD_CONFIG_MLE_LINK_METRICS_SUBJECT_ENABLE
    aNeighbor.RemoveAllForwardTrackingSeriesInfo();
#endif

exit:
    return;
}

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
Error Mle::SetPreferredRouterId(uint8_t aRouterId)
{
    Error error = kErrorNone;

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

    mPreviousRouterId = aRouterId;

exit:
    return error;
}
#endif

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

Error Mle::SendAddressSolicit(RouterUpgradeReason aReason)
{
    Error          error   = kErrorNone;
    Coap::Message *message = nullptr;
    Ip6::Address   leaderRloc;

    VerifyOrExit(!mAddressSolicitPending);

    message = Get<Tmf::Agent>().AllocateAndInitPriorityConfirmablePostMessage(kUriAddressSolicit);
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = Tlv::Append<ThreadExtMacAddressTlv>(*message, Get<Mac::Mac>().GetExtAddress()));

    if (IsRouterIdValid(mPreviousRouterId))
    {
        SuccessOrExit(error = Tlv::Append<ThreadRloc16Tlv>(*message, Rloc16FromRouterId(mPreviousRouterId)));
    }

    SuccessOrExit(error = Tlv::Append<ThreadStatusTlv>(*message, aReason));

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    SuccessOrExit(error = Tlv::Append<XtalAccuracyTlv>(*message, otPlatTimeGetXtalAccuracy()));
#endif

    GetLeaderRloc(leaderRloc);

    SuccessOrExit(error = Get<Tmf::Agent>().SendMessageTo(*message, leaderRloc, HandleAddressSolicitResponse, this));
    mAddressSolicitPending = true;

    Log(kMessageSend, kTypeAddressSolicit, leaderRloc);

exit:
    FreeMessageOnError(message, error);
    return error;
}

void Mle::SendAddressRelease(void)
{
    Error          error = kErrorNone;
    Coap::Message *message;
    Ip6::Address   leaderRloc;

    message = Get<Tmf::Agent>().AllocateAndInitPriorityConfirmablePostMessage(kUriAddressRelease);
    VerifyOrExit(message != nullptr, error = kErrorNoBufs);

    SuccessOrExit(error = Tlv::Append<ThreadRloc16Tlv>(*message, Rloc16FromRouterId(mRouterId)));
    SuccessOrExit(error = Tlv::Append<ThreadExtMacAddressTlv>(*message, Get<Mac::Mac>().GetExtAddress()));

    GetLeaderRloc(leaderRloc);

    SuccessOrExit(error = Get<Tmf::Agent>().SendMessageTo(*message, leaderRloc));

    Log(kMessageSend, kTypeAddressRelease, leaderRloc);

exit:
    FreeMessageOnError(message, error);
    LogSendError(kTypeAddressRelease, error);
}

void Mle::HandleAddressSolicitResponse(Coap::Msg *aMsg, Error aResult)
{
    uint8_t      status;
    uint16_t     rloc16;
    RouterIdMask routerIdMask;
    uint8_t      routerId;
    Router      *router;

    mAddressSolicitPending = false;

    VerifyOrExit(aResult == kErrorNone && aMsg != nullptr);

    VerifyOrExit(aMsg->GetCode() == Coap::kCodeChanged);

    Log(kMessageReceive, kTypeAddressReply, aMsg->mMessageInfo.GetPeerAddr());

    SuccessOrExit(Tlv::Find<ThreadStatusTlv>(aMsg->mMessage, status));

    if (status != kAddrSolicitSuccess)
    {
        mAddressSolicitRejected = true;

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

            SetRouterId(kInvalidRouterId);
        }

        ExitNow();
    }

    SuccessOrExit(Tlv::Find<ThreadRloc16Tlv>(aMsg->mMessage, rloc16));
    routerId = RouterIdFromRloc16(rloc16);

    SuccessOrExit(Tlv::Find<ThreadRouterMaskTlv>(aMsg->mMessage, routerIdMask));
    VerifyOrExit(routerIdMask.IsValid());
    VerifyOrExit(routerIdMask.IsAllocated(GetLeaderId()));

    SetAlternateRloc16(GetRloc16());

    SetRouterId(routerId);

    SetStateRouter(Rloc16FromRouterId(mRouterId));

    // We keep the router table next hop and cost as what we had as a
    // REED, i.e., our parent was the next hop towards all other
    // routers and we tracked its cost towards them. As an FTD child,
    // we may have established links with a subset of neighboring routers.
    // We ensure to clear these links to avoid using them (since will
    // be rejected by the neighbor).

    mRouterTable.ClearNeighbors();

    mRouterTable.UpdateRouterIdMask(routerIdMask);

    router = mRouterTable.FindRouterById(routerId);
    VerifyOrExit(router != nullptr);
    router->SetExtAddress(Get<Mac::Mac>().GetExtAddress());
    router->SetNextHopToInvalid();

    // Ensure we have our parent as a neighboring router, copying the
    // `mParent` entry.

    router = mRouterTable.FindRouterById(mParent.GetRouterId());
    VerifyOrExit(router != nullptr);
    router->SetFrom(mParent);
    router->SetState(Neighbor::kStateValid);
    router->SetNextHopToInvalid();

    // Ensure we have a next hop and cost towards leader.
    if (mRouterTable.GetPathCostToLeader() >= kMaxRouteCost)
    {
        Router *leader = mRouterTable.GetLeader();

        OT_ASSERT(leader != nullptr);
        leader->SetNextHopAndCost(RouterIdFromRloc16(mParent.GetRloc16()), mParent.GetLeaderCost());
    }

    // We send a unicast Link Request to our former parent if its
    // version is earlier than 1.3. This is to address a potential
    // compatibility issue with some non-OpenThread stacks which may
    // ignore MLE Advertisements from a former/existing child.

    if (mParent.GetVersion() < kThreadVersion1p3)
    {
        SendLinkRequest(&mParent);
    }

    // We send an Advertisement to inform our former parent of our
    // newly allocated Router ID. This will cause the parent to
    // reset its advertisement trickle timer which can help speed
    // up the dissemination of the new Router ID to other routers.
    // This can also help with quicker link establishment with our
    // former parent and other routers.
    SendMulticastAdvertisement();

    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateChildIdRequest))
    {
        IgnoreError(SendChildIdResponse(child));

        // The transition to the router role was triggered by a Child
        // ID Request. This indicates that the child has no other
        // parent option. We set the flags to prevent the parent
        // router from downgrading back to a REED to ensure this
        // child remains connected.
        //
        // The `DowngradeBlocked` is cleared in various situations:
        // - From `SetStateDetached()` (e.g. partition change).
        // - If a new router is added (new possible parent).
        // - If all children blocking downgrade are disconnected.

        child.SetBlockParentDowngrade(true);
        mRoleTransitioner.SetDowngradeBlocked(true);
    }

exit:
    mAnnounceHandler.HandleRouterRoleTransitionAttemptDone();
}

Error Mle::SetChildRouterLinks(uint8_t aChildRouterLinks)
{
    Error error = kErrorNone;

    VerifyOrExit(IsDisabled(), error = kErrorInvalidState);
    mChildRouterLinks = aChildRouterLinks;
exit:
    return error;
}

bool Mle::RoleTransitioner::WillBecomeRouterSoon(void) const
{
    static constexpr uint8_t kMaxDelay = 10;

    bool willBecomeRouter = false;

    VerifyOrExit(IsRouterRoleAllowed() && Get<Mle>().IsChild());
    VerifyOrExit(!Get<Mle>().mAddressSolicitRejected);

    if (!Get<Mle>().mAddressSolicitPending)
    {
        VerifyOrExit(IsTransitionPending() && GetTimeout() <= kMaxDelay);
    }

    willBecomeRouter = true;

exit:
    return willBecomeRouter;
}

Error Mle::AddrSolicitInfo::ParseFrom(const Coap::Msg &aMsg)
{
    // Parses a `kUriAddressSolicit` request message.

    Error error;

    VerifyOrExit(aMsg.IsConfirmable(), error = kErrorParse);

    SuccessOrExit(error = Tlv::Find<ThreadExtMacAddressTlv>(aMsg.mMessage, mExtAddress));
    SuccessOrExit(error = Tlv::Find<ThreadStatusTlv>(aMsg.mMessage, mReason));

    switch (Tlv::Find<ThreadRloc16Tlv>(aMsg.mMessage, mRequestedRloc16))
    {
    case kErrorNone:
        break;
    case kErrorNotFound:
        mRequestedRloc16 = kInvalidRloc16;
        break;
    default:
        ExitNow(error = kErrorParse);
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    switch (Tlv::Find<XtalAccuracyTlv>(aMsg.mMessage, mXtalAccuracy))
    {
    case kErrorNone:
        break;
    case kErrorNotFound:
        SetToUintMax(mXtalAccuracy);
        break;
    default:
        ExitNow(error = kErrorParse);
    }
#endif

exit:
    return error;
}

void Mle::ProcessAddressSolicit(AddrSolicitInfo &aInfo)
{
    // Processes a previously parsed Address Solicit request and
    // determines whether to accept or reject the request.

    aInfo.mResponse = kAddrSolicitNoAddressAvailable;
    aInfo.mRouter   = nullptr;

    LogInfo("AddrSolicit Reason: %s", RouterUpgradeReasonToString(aInfo.mReason));

    if (aInfo.mRequestedRloc16 != kInvalidRloc16)
    {
        LogInfo("AddrSolicit Requested ID: %u", RouterIdFromRloc16(aInfo.mRequestedRloc16));
    }

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
    VerifyOrExit(aInfo.mXtalAccuracy <= Get<TimeSync>().GetXtalThreshold());
#endif

    aInfo.mRouter = mRouterTable.FindRouter(aInfo.mExtAddress);

    if (aInfo.mRouter != nullptr)
    {
        aInfo.mResponse = kAddrSolicitSuccess;
        ExitNow();
    }

    switch (aInfo.mReason)
    {
    case kReasonTooFewRouters:
        VerifyOrExit(mRoleTransitioner.IsRouterCountBelowUpgradeThreshold());
        break;

    case kReasonHaveChildIdRequest:
    case kReasonParentPartitionChange:
        break;

    case kReasonBorderRouterRequest:
        if (!mRoleTransitioner.IsRouterCountBelowUpgradeThreshold() &&
            (Get<NetworkData::Leader>().CountBorderRouters(NetworkData::kRouterRoleOnly) >=
             kRouterUpgradeBorderRouterRequestThreshold))
        {
            ExitNow();
        }
        break;

    default:
        aInfo.mResponse = kAddrSolicitUnrecognizedReason;
        ExitNow();
    }

    if (aInfo.mRequestedRloc16 != kInvalidRloc16)
    {
        aInfo.mRouter = mRouterTable.Allocate(RouterIdFromRloc16(aInfo.mRequestedRloc16));
    }

    if (aInfo.mRouter == nullptr)
    {
        aInfo.mRouter = mRouterTable.Allocate();
        VerifyOrExit(aInfo.mRouter != nullptr);
    }

    aInfo.mRouter->SetExtAddress(aInfo.mExtAddress);
    aInfo.mResponse = kAddrSolicitSuccess;

exit:
    return;
}

template <> void Mle::HandleTmf<kUriAddressSolicit>(Coap::Msg &aMsg)
{
    Coap::Message  *response = nullptr;
    AddrSolicitInfo info;

    VerifyOrExit(IsLeader() && !IsAttaching());

    Log(kMessageReceive, kTypeAddressSolicit, aMsg.mMessageInfo.GetPeerAddr());

    SuccessOrExit(info.ParseFrom(aMsg));

    ProcessAddressSolicit(info);

    // Prepare and send response

    response = Get<Tmf::Agent>().AllocateAndInitPriorityResponseFor(aMsg.mMessage);
    VerifyOrExit(response != nullptr);

    SuccessOrExit(Tlv::Append<ThreadStatusTlv>(*response, info.mResponse));

    if (info.mRouter != nullptr)
    {
        RouterIdMask routerIdMask;

        SuccessOrExit(Tlv::Append<ThreadRloc16Tlv>(*response, info.mRouter->GetRloc16()));

        mRouterTable.GetRouterIdMask(routerIdMask);
        SuccessOrExit(Tlv::Append<ThreadRouterMaskTlv>(*response, routerIdMask));
    }

    SuccessOrExit(Get<Tmf::Agent>().SendMessage(*response, aMsg.mMessageInfo));
    response = nullptr;

    Log(kMessageSend, kTypeAddressReply, aMsg.mMessageInfo.GetPeerAddr());

    // If assigning a new RLOC16 (e.g., on promotion of a child to
    // router role) we clear any address cache entries associated
    // with the old RLOC16 unless the sender is a direct child. For
    // direct children, we retain the cache entries to allow
    // association with the promoted router's new RLOC16 upon
    // receiving its Link Advertisement.

    if ((info.mResponse == kAddrSolicitSuccess) && (info.mRouter != nullptr))
    {
        uint16_t oldRloc16;

        VerifyOrExit(IsRoutingLocator(aMsg.mMessageInfo.GetPeerAddr()));
        oldRloc16 = aMsg.mMessageInfo.GetPeerAddr().GetIid().GetLocator();

        VerifyOrExit(oldRloc16 != info.mRouter->GetRloc16());
        VerifyOrExit(!RouterIdMatch(oldRloc16, GetRloc16()));
        Get<AddressResolver>().RemoveEntriesForRloc16(oldRloc16);
    }

exit:
    FreeMessage(response);
}

template <> void Mle::HandleTmf<kUriAddressRelease>(Coap::Msg &aMsg)
{
    uint16_t        rloc16;
    Mac::ExtAddress extAddress;
    uint8_t         routerId;
    Router         *router;

    VerifyOrExit(mRole == kRoleLeader);

    VerifyOrExit(aMsg.IsConfirmable());

    Log(kMessageReceive, kTypeAddressRelease, aMsg.mMessageInfo.GetPeerAddr());

    SuccessOrExit(Tlv::Find<ThreadRloc16Tlv>(aMsg.mMessage, rloc16));
    SuccessOrExit(Tlv::Find<ThreadExtMacAddressTlv>(aMsg.mMessage, extAddress));

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

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

    IgnoreError(mRouterTable.Release(routerId));

    SuccessOrExit(Get<Tmf::Agent>().SendAckResponse(aMsg));

    Log(kMessageSend, kTypeAddressReleaseReply, aMsg.mMessageInfo.GetPeerAddr());

exit:
    return;
}

void Mle::DetermineConnectivity(Connectivity &aConnectivity) const
{
    aConnectivity.Clear();

    aConnectivity.mParentPriority = GetAssignParentPriority();

    if (aConnectivity.mParentPriority == kParentPriorityUnspecified)
    {
        uint16_t numChildren = mChildTable.GetNumChildren(Child::kInStateValid);
        uint16_t maxAllowed  = mChildTable.GetMaxChildrenAllowed();

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

    if (IsChild())
    {
        aConnectivity.IncrementNumForLinkQuality(mParent.GetLinkQualityIn());
    }

    for (const Router &router : Get<RouterTable>())
    {
        if (router.GetRloc16() == GetRloc16())
        {
            continue;
        }

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

        aConnectivity.IncrementNumForLinkQuality(router.GetTwoWayLinkQuality());
    }

    aConnectivity.mLeaderCost       = Min(mRouterTable.GetPathCostToLeader(), kMaxRouteCost);
    aConnectivity.mIdSequence       = mRouterTable.GetRouterIdSequence();
    aConnectivity.mActiveRouters    = mRouterTable.GetActiveRouterCount();
    aConnectivity.mSedBufferSize    = Connectivity::kDefaultSedBufferSize;
    aConnectivity.mSedDatagramCount = Connectivity::kDefaultSedDatagramCount;
}

void Mle::FillConnectivityTlvValue(ConnectivityTlvValue &aTlvValue) const
{
    Connectivity connectivity;

    DetermineConnectivity(connectivity);
    aTlvValue.InitFrom(connectivity);
}

void Mle::RoleTransitioner::DecideWhetherToDowngrade(uint8_t aNeighborId, const RouteTlv::Data &aRouteTlvData)
{
    // Determine whether all conditions are satisfied for the router
    // to downgrade after receiving info for a neighboring router
    // with Router ID `aNeighborId` along with its `aRouteTlvData`.

    uint8_t activeRouterCount = Get<RouterTable>().GetActiveRouterCount();
    uint8_t count;

    VerifyOrExit(Get<Mle>().IsRouter());
    VerifyOrExit(Get<RouterTable>().IsAllocated(aNeighborId));
    VerifyOrExit(!mDowngradeBlocked);

    VerifyOrExit(!IsTransitionPending());

    VerifyOrExit(activeRouterCount > mDowngradeThreshold);

    // Check that we have at least `kMinDowngradeNeighbors`
    // neighboring routers with two-way link quality of 2 or better.

    count = 0;

    for (const Router &router : Get<RouterTable>())
    {
        if (!router.IsStateValid() || (router.GetTwoWayLinkQuality() < kLinkQuality2))
        {
            continue;
        }

        count++;

        if (count >= kMinDowngradeNeighbors)
        {
            break;
        }
    }

    VerifyOrExit(count >= kMinDowngradeNeighbors);

    // Check that we have fewer children than three times the number
    // of excess routers (defined as the difference between number of
    // active routers and `mDowngradeThreshold`).

    count = activeRouterCount - mDowngradeThreshold;
    VerifyOrExit(Get<ChildTable>().GetNumChildren(Child::kInStateValid) < count * 3);

    // Check that the neighbor has as good or better-quality links to
    // same routers.

    VerifyOrExit(NeighborHasComparableConnectivity(aNeighborId, aRouteTlvData));

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE && OPENTHREAD_CONFIG_BORDER_ROUTER_REQUEST_ROUTER_ROLE
    // Check if we are eligible to be router due to being a BR.
    VerifyOrExit(!Get<NetworkData::Notifier>().IsEligibleForRouterRoleUpgradeAsBorderRouter());
#endif

    // All conditions are satisfied to start downgrade.
    StartTimeout();

exit:
    return;
}

bool Mle::RoleTransitioner::NeighborHasComparableConnectivity(uint8_t               aNeighborId,
                                                              const RouteTlv::Data &aRouteTlvData) const
{
    // Check whether the neighboring router with Router ID `aNeighborId`
    // (along with its `aRouteTlvData`) has as good or better-quality
    // links to all our neighboring routers which have a two-way link
    // quality of two or better.

    bool isComparable = true;

    for (uint8_t routerId = 0; routerId <= kMaxRouterId; routerId++)
    {
        const Router                *router;
        LinkQuality                  localLinkQuality;
        LinkQuality                  peerLinkQuality;
        const RouteTlv::Data::Entry *entry;

        if ((routerId == Get<Mle>().mRouterId) || (routerId == aNeighborId))
        {
            continue;
        }

        router = Get<RouterTable>().FindRouterById(routerId);

        if ((router == nullptr) || !router->IsStateValid())
        {
            continue;
        }

        localLinkQuality = router->GetTwoWayLinkQuality();

        if (localLinkQuality < kLinkQuality2)
        {
            continue;
        }

        // `router` is our neighbor with two-way link quality of
        // at least two. Check that `aRouteTlvData` has as good or
        // better-quality link to it as well.

        entry = aRouteTlvData.GetEntries().FindMatching(routerId);

        if (entry == nullptr)
        {
            ExitNow(isComparable = false);
        }

        peerLinkQuality = Min(entry->GetLinkQualityIn(), entry->GetLinkQualityOut());

        if (peerLinkQuality < localLinkQuality)
        {
            ExitNow(isComparable = false);
        }
    }

exit:
    return isComparable;
}

void Mle::SetChildStateToValid(Child &aChild)
{
    VerifyOrExit(!aChild.IsStateValid());

    aChild.SetState(Neighbor::kStateValid);
    IgnoreError(mChildTable.StoreChild(aChild));

#if OPENTHREAD_CONFIG_TMF_PROXY_MLR_ENABLE
    Get<Mlr::Manager>().UpdateChildRegistrations(aChild);
#endif

    mNeighborTable.Signal(NeighborTable::kChildAdded, aChild);

exit:
    return;
}

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

void Mle::RemoveChildren(void)
{
    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValidOrRestoring))
    {
        RemoveNeighbor(child);
    }
}

Error Mle::SetAssignParentPriority(int8_t aParentPriority)
{
    Error error = kErrorNone;

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

    mParentPriority = aParentPriority;

exit:
    return error;
}

Error Mle::GetMaxChildTimeout(uint32_t &aTimeout) const
{
    Error error = kErrorNotFound;

    aTimeout = 0;

    VerifyOrExit(IsRouterOrLeader(), error = kErrorInvalidState);

    for (Child &child : Get<ChildTable>().Iterate(Child::kInStateValid))
    {
        if (child.IsFullThreadDevice())
        {
            continue;
        }

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

        error = kErrorNone;
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_TIME_SYNC_ENABLE
Error Mle::SendTimeSync(void)
{
    Error        error = kErrorNone;
    Ip6::Address destination;
    TxMessage   *message = nullptr;

    VerifyOrExit((message = NewMleMessage(kCommandTimeSync)) != nullptr, error = kErrorNoBufs);

    message->SetTimeSync(true);

    destination = Ip6::Address::GetLinkLocalAllNodesMulticast();
    SuccessOrExit(error = message->SendTo(destination));

    Log(kMessageSend, kTypeTimeSync, destination);

exit:
    FreeMessageOnError(message, error);
    return error;
}
#endif // OPENTHREAD_CONFIG_TIME_SYNC_ENABLE

#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)

const char *Mle::RouterUpgradeReasonToString(uint8_t aReason)
{
    const char *str = "Unknown";

    switch (aReason)
    {
    case kReasonTooFewRouters:
        str = "TooFewRouters";
        break;
    case kReasonHaveChildIdRequest:
        str = "HaveChildIdRequest";
        break;
    case kReasonParentPartitionChange:
        str = "ParentPartitionChange";
        break;
    case kReasonBorderRouterRequest:
        str = "BorderRouterRequest";
        break;
    }

    return str;
}

#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)

//----------------------------------------------------------------------------------------------------------------------
// RoleTransitioner

Mle::RoleTransitioner::RoleTransitioner(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mRouterEligible(true)
    , mRouterRoleAllowed(true)
    , mDowngradeBlocked(false)
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    , mCcmEnabled(false)
    , mThreadVersionCheckEnabled(true)
#endif
    , mTimeout(0)
    , mJitter(kRouterSelectionJitter)
    , mUpgradeThreshold(kRouterUpgradeThreshold)
    , mDowngradeThreshold(kRouterDowngradeThreshold)

{
}

void Mle::RoleTransitioner::StartTimeout(void) { mTimeout = 1 + Random::NonCrypto::GenerateUpToExcluding(mJitter); }

bool Mle::RoleTransitioner::IsRouterCountBelowUpgradeThreshold(void) const
{
    return Get<RouterTable>().GetActiveRouterCount() < mUpgradeThreshold;
}

void Mle::RoleTransitioner::DecideWhetherToUpgrade(void)
{
    VerifyOrExit(Get<Mle>().IsChild());

    VerifyOrExit(IsRouterRoleAllowed());

    VerifyOrExit(!IsTransitionPending());
    VerifyOrExit(IsRouterCountBelowUpgradeThreshold());

    StartTimeout();

exit:
    return;
}

void Mle::RoleTransitioner::HandleTimeTick(void)
{
    VerifyOrExit(mTimeout > 0);
    mTimeout--;

    VerifyOrExit(mTimeout == 0);

    // Timeout expired

    switch (Get<Mle>().mRole)
    {
    case kRoleDisabled:
    case kRoleDetached:
        break;

    case kRoleChild:
        if (IsRouterCountBelowUpgradeThreshold() && Get<Mle>().HasNeighborWithGoodLinkQuality())
        {
            IgnoreError(Get<Mle>().BecomeRouter(kReasonTooFewRouters));
        }
        else
        {
            Get<Mle>().mAnnounceHandler.HandleRouterRoleTransitionAttemptDone();
        }

        if (!Get<Mle>().mAdvertiseTrickleTimer.IsRunning())
        {
            Get<Mle>().SendMulticastAdvertisement();
            Get<Mle>().mAdvertiseTrickleTimer.Start(TrickleTimer::kModePlainTimer, kReedAdvIntervalMin,
                                                    kReedAdvIntervalMax);
        }

        break;

    case kRoleRouter:
        if (Get<RouterTable>().GetActiveRouterCount() > mDowngradeThreshold)
        {
            LogNote("Downgrade to REED");
            Get<Mle>().mAttacher.Attach(kDowngradeToReed);
        }

        OT_FALL_THROUGH;

    case kRoleLeader:
        if (!IsRouterRoleAllowed())
        {
            LogInfo("Router role no longer allowed");
            IgnoreError(Get<Mle>().BecomeDetached());
        }

        break;
    }

exit:
    return;
}

} // namespace Mle
} // namespace ot

#endif // OPENTHREAD_FTD
