/*
 *  Copyright (c) 2021, 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 the Network Data Publisher.
 *
 */

#include "network_data_publisher.hpp"

#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE

#include "common/array.hpp"
#include "common/code_utils.hpp"
#include "common/const_cast.hpp"
#include "common/instance.hpp"
#include "common/locator_getters.hpp"
#include "common/log.hpp"
#include "common/random.hpp"
#include "thread/network_data_local.hpp"
#include "thread/network_data_service.hpp"

namespace ot {
namespace NetworkData {

RegisterLogModule("NetDataPublshr");

//---------------------------------------------------------------------------------------------------------------------
// Publisher

Publisher::Publisher(Instance &aInstance)
    : InstanceLocator(aInstance)
#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    , mDnsSrpServiceEntry(aInstance)
#endif
#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    , mPrefixCallback(nullptr)
    , mPrefixCallbackContext(nullptr)
#endif
    , mTimer(aInstance, Publisher::HandleTimer)
{
#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    // Since the `PrefixEntry` type is used in an array,
    // we cannot use a constructor with an argument (e.g.,
    // we cannot use `InstacneLocator`) so we use
    // `IntanceLocatorInit`  and `Init()` the entries one
    // by one.

    for (PrefixEntry &entry : mPrefixEntries)
    {
        entry.Init(aInstance);
    }
#endif
}

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE

void Publisher::SetPrefixCallback(PrefixCallback aCallback, void *aContext)
{
    mPrefixCallback        = aCallback;
    mPrefixCallbackContext = aContext;
}

Error Publisher::PublishOnMeshPrefix(const OnMeshPrefixConfig &aConfig)
{
    Error        error = kErrorNone;
    PrefixEntry *entry;

    VerifyOrExit(aConfig.IsValid(GetInstance()), error = kErrorInvalidArgs);
    VerifyOrExit(aConfig.mStable, error = kErrorInvalidArgs);

    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix());
    VerifyOrExit(entry != nullptr, error = kErrorNoBufs);

    entry->Publish(aConfig);

exit:
    return error;
}

Error Publisher::PublishExternalRoute(const ExternalRouteConfig &aConfig)
{
    Error        error = kErrorNone;
    PrefixEntry *entry;

    VerifyOrExit(aConfig.IsValid(GetInstance()), error = kErrorInvalidArgs);
    VerifyOrExit(aConfig.mStable, error = kErrorInvalidArgs);

    entry = FindOrAllocatePrefixEntry(aConfig.GetPrefix());
    VerifyOrExit(entry != nullptr, error = kErrorNoBufs);

    entry->Publish(aConfig);

exit:
    return error;
}

bool Publisher::IsPrefixAdded(const Ip6::Prefix &aPrefix) const
{
    bool               isAdded = false;
    const PrefixEntry *entry;

    entry = FindMatchingPrefixEntry(aPrefix);
    VerifyOrExit(entry != nullptr);

    isAdded = entry->IsAdded();

exit:
    return isAdded;
}

Error Publisher::UnpublishPrefix(const Ip6::Prefix &aPrefix)
{
    Error        error = kErrorNone;
    PrefixEntry *entry;

    entry = FindMatchingPrefixEntry(aPrefix);
    VerifyOrExit(entry != nullptr, error = kErrorNotFound);

    entry->Unpublish();

exit:
    return error;
}

Publisher::PrefixEntry *Publisher::FindOrAllocatePrefixEntry(const Ip6::Prefix &aPrefix)
{
    // Returns a matching prefix entry if found, otherwise tries
    // to allocate a new entry.

    PrefixEntry *prefixEntry = FindMatchingPrefixEntry(aPrefix);

    VerifyOrExit(prefixEntry == nullptr);

    for (PrefixEntry &entry : mPrefixEntries)
    {
        if (!entry.IsInUse())
        {
            prefixEntry = &entry;
            ExitNow();
        }
    }

exit:
    return prefixEntry;
}

Publisher::PrefixEntry *Publisher::FindMatchingPrefixEntry(const Ip6::Prefix &aPrefix)
{
    return AsNonConst(AsConst(this)->FindMatchingPrefixEntry(aPrefix));
}

const Publisher::PrefixEntry *Publisher::FindMatchingPrefixEntry(const Ip6::Prefix &aPrefix) const
{
    const PrefixEntry *prefixEntry = nullptr;

    for (const PrefixEntry &entry : mPrefixEntries)
    {
        if (entry.IsInUse() && entry.Matches(aPrefix))
        {
            prefixEntry = &entry;
            break;
        }
    }

    return prefixEntry;
}

bool Publisher::IsAPrefixEntry(const Entry &aEntry) const
{
    return (&mPrefixEntries[0] <= &aEntry) && (&aEntry < GetArrayEnd(mPrefixEntries));
}

void Publisher::NotifyPrefixEntryChange(Event aEvent, const Ip6::Prefix &aPrefix) const
{
    if (mPrefixCallback != nullptr)
    {
        mPrefixCallback(static_cast<otNetDataPublisherEvent>(aEvent), &aPrefix, mPrefixCallbackContext);
    }
}

#endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE

void Publisher::HandleNotifierEvents(Events aEvents)
{
#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    mDnsSrpServiceEntry.HandleNotifierEvents(aEvents);
#endif

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    for (PrefixEntry &entry : mPrefixEntries)
    {
        entry.HandleNotifierEvents(aEvents);
    }
#endif
}

void Publisher::HandleTimer(Timer &aTimer)
{
    aTimer.Get<Publisher>().HandleTimer();
}

void Publisher::HandleTimer(void)
{
#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    mDnsSrpServiceEntry.HandleTimer();
#endif

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    for (PrefixEntry &entry : mPrefixEntries)
    {
        entry.HandleTimer();
    }
#endif
}

//---------------------------------------------------------------------------------------------------------------------
// Publisher::Entry

void Publisher::Entry::SetState(State aState)
{
    VerifyOrExit(mState != aState);

    LogInfo("%s - State: %s -> %s", ToString(/* aIncludeState */ false).AsCString(), StateToString(mState),
            StateToString(aState));
    mState = aState;

exit:
    return;
}

bool Publisher::Entry::IsPreferred(uint16_t aRloc16) const
{
    // Indicates whether or not an entry from `aRloc16` is preferred
    // over our entry (based on our RLOC). We prefer an entry from a
    // router over an entry from an end-device (e.g., a REED). If both
    // are the same type, then the one with smaller RLOC16 is preferred.

    bool isOtherRouter = Mle::Mle::IsActiveRouter(aRloc16);

    return (Get<Mle::Mle>().IsRouterOrLeader() == isOtherRouter) ? (aRloc16 < Get<Mle::Mle>().GetRloc16())
                                                                 : isOtherRouter;
}

void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEntries, uint8_t aDesiredNumEntries)
{
    // This method uses the info about number existing entries (total
    // and preferred) in Network Data along with the desired number of
    // entries we aim to have in the Network Data to decide whether or
    // not to take any action (add or remove our entry).

    LogInfo("%s in netdata - total:%d, preferred:%d, desired:%d", ToString().AsCString(), aNumEntries,
            aNumPreferredEntries, aDesiredNumEntries);

    switch (GetState())
    {
    case kNoEntry:
        break;

    case kToAdd:
        // Our entry is ready to be added. If there are too few existing
        // entries, we start adding our entry (start the timer with a
        // random delay before adding the entry).

        if (aNumEntries < aDesiredNumEntries)
        {
            mUpdateTime = TimerMilli::GetNow() + Random::NonCrypto::GetUint32InRange(1, kMaxDelayToAdd);
            SetState(kAdding);
            Get<Publisher>().GetTimer().FireAtIfEarlier(mUpdateTime);
            LogUpdateTime();
        }
        break;

    case kAdding:
        // Our entry is being added (waiting time before we add). If we
        // now see that there are enough entries, we stop adding the
        // entry.

        if (aNumEntries >= aDesiredNumEntries)
        {
            SetState(kToAdd);
        }
        break;

    case kAdded:
        // Our entry is already added in the Network Data. If there are
        // enough entries, do nothing and keep monitoring. If we see now
        // that there are too many entries, we start removing our entry
        // after a random delay time. If our entry itself is preferred
        // over other entries (indicated by `aNumPreferredEntries <
        // aDesiredNumEntries`) we add an extra delay before removing
        // the entry. This gives higher chance for a non-preferred
        // entry from another device to be removed before our entry.

        if (aNumEntries > aDesiredNumEntries)
        {
            mUpdateTime = TimerMilli::GetNow() + Random::NonCrypto::GetUint32InRange(1, kMaxDelayToRemove);

            if (aNumPreferredEntries < aDesiredNumEntries)
            {
                mUpdateTime += kExtraDelayToRemovePeferred;
            }

            SetState(kRemoving);
            Get<Publisher>().GetTimer().FireAtIfEarlier(mUpdateTime);
            LogUpdateTime();
        }
        break;

    case kRemoving:
        // Our entry is being removed (wait time before remove). If we
        // now see that there are enough or too few entries, we stop
        // removing our entry.

        if (aNumEntries <= aDesiredNumEntries)
        {
            SetState(kAdded);
        }
        break;
    }
}

void Publisher::Entry::HandleTimer(void)
{
    // Timer is used to delay adding/removing the entry. If we have
    // reached `mUpdateTime` add or remove the entry. Otherwise,
    // restart the timer (note that timer can be shared between
    // different published entries).

    VerifyOrExit((GetState() == kAdding) || (GetState() == kRemoving));

    if (mUpdateTime <= TimerMilli::GetNow())
    {
        if (GetState() == kAdding)
        {
            Add();
        }
        else
        {
            Remove(/* aNextState */ kToAdd);
        }
    }
    else
    {
        Get<Publisher>().GetTimer().FireAtIfEarlier(mUpdateTime);
    }

exit:
    return;
}

void Publisher::Entry::Add(void)
{
#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    if (Get<Publisher>().IsADnsSrpServiceEntry(*this))
    {
        static_cast<DnsSrpServiceEntry *>(this)->Add();
    }
#endif

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    if (Get<Publisher>().IsAPrefixEntry(*this))
    {
        static_cast<PrefixEntry *>(this)->Add();
    }
#endif
}

void Publisher::Entry::Remove(State aNextState)
{
#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    if (Get<Publisher>().IsADnsSrpServiceEntry(*this))
    {
        static_cast<DnsSrpServiceEntry *>(this)->Remove(aNextState);
    }
#endif

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    if (Get<Publisher>().IsAPrefixEntry(*this))
    {
        static_cast<PrefixEntry *>(this)->Remove(aNextState);
    }
#endif
}

Publisher::Entry::InfoString Publisher::Entry::ToString(bool aIncludeState) const
{
    InfoString string;

#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE
    if (Get<Publisher>().IsADnsSrpServiceEntry(*this))
    {
        string.Append("DNS/SRP service");
        ExitNow();
    }
#endif

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE
    if (Get<Publisher>().IsAPrefixEntry(*this))
    {
        const PrefixEntry &prefixEntry = *static_cast<const PrefixEntry *>(this);

        switch (prefixEntry.mType)
        {
        case PrefixEntry::kTypeOnMeshPrefix:
            string.Append("OnMeshPrefix ");
            break;

        case PrefixEntry::kTypeExternalRoute:
            string.Append("ExternalRoute ");
            break;
        }

        string.Append(prefixEntry.mPrefix.ToString().AsCString());
        ExitNow();
    }
#endif

exit:
    if (aIncludeState)
    {
        string.Append(" (state:%s)", StateToString(GetState()));
    }

    return string;
}

void Publisher::Entry::LogUpdateTime(void) const
{
    LogInfo("%s - update in %u msec", ToString().AsCString(), mUpdateTime - TimerMilli::GetNow());
}

const char *Publisher::Entry::StateToString(State aState)
{
    static const char *const kStateStrings[] = {
        "NoEntry",  // (0) kNoEntry
        "ToAdd",    // (1) kToAdd
        "Adding",   // (2) kAdding
        "Added",    // (3) kAdded
        "Removing", // (4) kRemoving
    };

    static_assert(0 == kNoEntry, "kNoEntry value is not correct");
    static_assert(1 == kToAdd, "kToAdd value is not correct");
    static_assert(2 == kAdding, "kAdding value is not correct");
    static_assert(3 == kAdded, "kAdded value is not correct");
    static_assert(4 == kRemoving, "kRemoving value is not correct");

    return kStateStrings[aState];
}

#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE

//---------------------------------------------------------------------------------------------------------------------
// Publisher::DnsSrpServiceEntry

Publisher::DnsSrpServiceEntry::DnsSrpServiceEntry(Instance &aInstance)
    : mCallback(nullptr)
    , mCallbackContext(nullptr)
{
    Init(aInstance);
}

void Publisher::DnsSrpServiceEntry::SetCallback(DnsSrpServiceCallback aCallback, void *aContext)
{
    mCallback        = aCallback;
    mCallbackContext = aContext;
}

void Publisher::DnsSrpServiceEntry::PublishAnycast(uint8_t aSequenceNumber)
{
    LogInfo("Publishing DNS/SRP service anycast (seq-num:%d)", aSequenceNumber);
    Publish(Info::InfoAnycast(aSequenceNumber));
}

void Publisher::DnsSrpServiceEntry::PublishUnicast(const Ip6::Address &aAddress, uint16_t aPort)
{
    LogInfo("Publishing DNS/SRP service unicast (%s, port:%d)", aAddress.ToString().AsCString(), aPort);
    Publish(Info::InfoUnicast(kTypeUnicast, aAddress, aPort));
}

void Publisher::DnsSrpServiceEntry::PublishUnicast(uint16_t aPort)
{
    LogInfo("Publishing DNS/SRP service unicast (ml-eid, port:%d)", aPort);
    Publish(Info::InfoUnicast(kTypeUnicastMeshLocalEid, Get<Mle::Mle>().GetMeshLocal64(), aPort));
}

void Publisher::DnsSrpServiceEntry::Publish(const Info &aInfo)
{
    if (GetState() != kNoEntry)
    {
        if (aInfo == mInfo)
        {
            LogInfo("%s is already being published", ToString().AsCString());
            ExitNow();
        }

        Remove(/* aNextState */ kNoEntry);
    }

    mInfo = aInfo;
    SetState(kToAdd);

    Process();

exit:
    return;
}

void Publisher::DnsSrpServiceEntry::Unpublish(void)
{
    LogInfo("Unpublishing DNS/SRP service");

    Remove(/* aNextState */ kNoEntry);
}

void Publisher::DnsSrpServiceEntry::HandleNotifierEvents(Events aEvents)
{
    if ((GetType() == kTypeUnicastMeshLocalEid) && aEvents.Contains(kEventThreadMeshLocalAddrChanged))
    {
        mInfo.SetAddress(Get<Mle::Mle>().GetMeshLocal64());

        if (GetState() == kAdded)
        {
            // If the entry is already added, we need to update it
            // so we remove it and add it back immediately with
            // the new mesh-local address.

            Remove(/* aNextState */ kAdding);
            Add();
            Get<Notifier>().HandleServerDataUpdated();
        }
    }

    if (aEvents.ContainsAny(kEventThreadNetdataChanged | kEventThreadRoleChanged))
    {
        Process();
    }
}

void Publisher::DnsSrpServiceEntry::Add(void)
{
    // Adds the service entry to the network data.

    switch (GetType())
    {
    case kTypeAnycast:
        SuccessOrExit(Get<Service::Manager>().Add<Service::DnsSrpAnycast>(
            Service::DnsSrpAnycast::ServiceData(mInfo.GetSequenceNumber())));
        break;

    case kTypeUnicast:
        SuccessOrExit(Get<Service::Manager>().Add<Service::DnsSrpUnicast>(
            Service::DnsSrpUnicast::ServiceData(mInfo.GetAddress(), mInfo.GetPort())));
        break;

    case kTypeUnicastMeshLocalEid:
        SuccessOrExit(Get<Service::Manager>().Add<Service::DnsSrpUnicast>(
            Service::DnsSrpUnicast::ServerData(mInfo.GetAddress(), mInfo.GetPort())));
        break;
    }

    Get<Notifier>().HandleServerDataUpdated();
    SetState(kAdded);
    Notify(kEventEntryAdded);

exit:
    return;
}

void Publisher::DnsSrpServiceEntry::Remove(State aNextState)
{
    // Removes the service entry from network data (if it was added).

    VerifyOrExit((GetState() == kAdded) || (GetState() == kRemoving));

    switch (GetType())
    {
    case kTypeAnycast:
        SuccessOrExit(Get<Service::Manager>().Remove<Service::DnsSrpAnycast>(
            Service::DnsSrpAnycast::ServiceData(mInfo.GetSequenceNumber())));
        break;

    case kTypeUnicast:
        SuccessOrExit(Get<Service::Manager>().Remove<Service::DnsSrpUnicast>(
            Service::DnsSrpUnicast::ServiceData(mInfo.GetAddress(), mInfo.GetPort())));
        break;

    case kTypeUnicastMeshLocalEid:
        SuccessOrExit(Get<Service::Manager>().Remove<Service::DnsSrpUnicast>());
        break;
    }

    Get<Notifier>().HandleServerDataUpdated();
    Notify(kEventEntryRemoved);

exit:
    SetState(aNextState);
}

void Publisher::DnsSrpServiceEntry::Notify(Event aEvent) const
{
#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
    Get<Srp::Server>().HandleNetDataPublisherEvent(aEvent);
#endif

    if (mCallback != nullptr)
    {
        mCallback(static_cast<otNetDataPublisherEvent>(aEvent), mCallbackContext);
    }
}

void Publisher::DnsSrpServiceEntry::Process(void)
{
    // This method checks the entries currently present in Network Data
    // based on which it then decides whether or not take action
    // (add/remove or keep monitoring).

    uint8_t numEntries          = 0;
    uint8_t numPreferredEntries = 0;
    uint8_t desiredNumEntries   = 0;

    // Do not make any changes if device is not attached, and wait
    // for role change event.
    VerifyOrExit(Get<Mle::Mle>().IsAttached());

    VerifyOrExit(GetState() != kNoEntry);

    switch (GetType())
    {
    case kTypeAnycast:
        CountAnycastEntries(numEntries, numPreferredEntries);
        desiredNumEntries = kDesiredNumAnycast;
        break;

    case kTypeUnicast:
    case kTypeUnicastMeshLocalEid:
        CountUnicastEntries(numEntries, numPreferredEntries);
        desiredNumEntries = kDesiredNumUnicast;
        break;
    }

    UpdateState(numEntries, numPreferredEntries, desiredNumEntries);

exit:
    return;
}

void Publisher::DnsSrpServiceEntry::CountAnycastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
{
    // Count the number of matching "DNS/SRP Anycast" service entries
    // in the Network Data (the match requires the entry to use same
    // "sequence number" value). We prefer the entries associated with
    // smaller RLCO16.

    Service::DnsSrpAnycast::ServiceData serviceData(mInfo.GetSequenceNumber());
    const ServiceTlv *                  serviceTlv = nullptr;
    ServiceData                         data;

    data.Init(&serviceData, serviceData.GetLength());

    while ((serviceTlv = Get<Leader>().FindNextThreadService(serviceTlv, data, NetworkData::kServicePrefixMatch)) !=
           nullptr)
    {
        TlvIterator      subTlvIterator(*serviceTlv);
        const ServerTlv *serverSubTlv;

        while ((serverSubTlv = subTlvIterator.Iterate<ServerTlv>()) != nullptr)
        {
            aNumEntries++;

            if (IsPreferred(serverSubTlv->GetServer16()))
            {
                aNumPreferredEntries++;
            }
        }
    }
}

void Publisher::DnsSrpServiceEntry::CountUnicastEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
{
    // Count the number of "DNS/SRP Unicast" service entries in
    // the Network Data.

    const ServiceTlv *serviceTlv = nullptr;
    ServiceData       data;

    data.InitFrom(Service::DnsSrpUnicast::kServiceData);

    while ((serviceTlv = Get<Leader>().FindNextThreadService(serviceTlv, data, NetworkData::kServicePrefixMatch)) !=
           nullptr)
    {
        TlvIterator      subTlvIterator(*serviceTlv);
        const ServerTlv *serverSubTlv;

        while (((serverSubTlv = subTlvIterator.Iterate<ServerTlv>())) != nullptr)
        {
            if (serviceTlv->GetServiceDataLength() >= sizeof(Service::DnsSrpUnicast::ServiceData))
            {
                aNumEntries++;

                // Generally, we prefer entries where the SRP/DNS server
                // address/port info is included in the service TLV data
                // over the ones where the info is included in the
                // server TLV data (i.e., we prefer infra-provided
                // SRP/DNS entry over a BR local one using ML-EID). If
                // our entry itself uses the service TLV data, then we
                // prefer based on the associated RLOC16.

                if (GetType() == kTypeUnicast)
                {
                    if (IsPreferred(serverSubTlv->GetServer16()))
                    {
                        aNumPreferredEntries++;
                    }
                }
                else
                {
                    aNumPreferredEntries++;
                }
            }

            if (serverSubTlv->GetServerDataLength() >= sizeof(Service::DnsSrpUnicast::ServerData))
            {
                aNumEntries++;

                // If our entry also uses the server TLV data (with
                // ML-EID address), then the we prefer based on the
                // associated RLOC16.

                if ((GetType() == kTypeUnicastMeshLocalEid) && IsPreferred(serverSubTlv->GetServer16()))
                {
                    aNumPreferredEntries++;
                }
            }
        }
    }
}

//---------------------------------------------------------------------------------------------------------------------
// Publisher::DnsSrpServiceEntry::Info

Publisher::DnsSrpServiceEntry::Info::Info(Type aType, uint16_t aPortOrSeqNumber, const Ip6::Address *aAddress)
    : mPortOrSeqNumber(aPortOrSeqNumber)
    , mType(aType)
{
    // It is important to `Clear()` the object since we compare all
    // bytes using overload of operator `==`.

    Clear();

    mType            = aType;
    mPortOrSeqNumber = aPortOrSeqNumber;

    if (aAddress != nullptr)
    {
        mAddress = *aAddress;
    }
}

#endif // OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE

#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE

//---------------------------------------------------------------------------------------------------------------------
// Publisher::PrefixEntry

void Publisher::PrefixEntry::Publish(const OnMeshPrefixConfig &aConfig)
{
    LogInfo("Publishing OnMeshPrefix %s", aConfig.GetPrefix().ToString().AsCString());

    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeOnMeshPrefix);
}

void Publisher::PrefixEntry::Publish(const ExternalRouteConfig &aConfig)
{
    LogInfo("Publishing ExternalRoute %s", aConfig.GetPrefix().ToString().AsCString());

    Publish(aConfig.GetPrefix(), aConfig.ConvertToTlvFlags(), kTypeExternalRoute);
}

void Publisher::PrefixEntry::Publish(const Ip6::Prefix &aPrefix, uint16_t aNewFlags, Type aNewType)
{
    if (GetState() != kNoEntry)
    {
        // If this is an existing entry, first we check that there is
        // a change in either type or flags. We remove the old entry
        // from Network Data if it was added. If the only change is
        // to flags (e.g., change to the preference level) and the
        // entry was previously added in Network Data, we re-add it
        // with the new flags. This ensures that changes to flags are
        // immediately reflected in the Network Data.

        State oldState = GetState();

        VerifyOrExit((mType != aNewType) || (mFlags != aNewFlags));

        Remove(/* aNextState */ kNoEntry);

        if ((mType == aNewType) && ((oldState == kAdded) || (oldState == kRemoving)))
        {
            mFlags = aNewFlags;
            Add();
        }
    }

    VerifyOrExit(GetState() == kNoEntry);

    mType   = aNewType;
    mPrefix = aPrefix;
    mFlags  = aNewFlags;

    SetState(kToAdd);

exit:
    Process();
}

void Publisher::PrefixEntry::Unpublish(void)
{
    LogInfo("Unpublishing %s", mPrefix.ToString().AsCString());

    Remove(/* aNextState */ kNoEntry);
}

void Publisher::PrefixEntry::HandleNotifierEvents(Events aEvents)
{
    if (aEvents.ContainsAny(kEventThreadNetdataChanged | kEventThreadRoleChanged))
    {
        Process();
    }
}

void Publisher::PrefixEntry::Add(void)
{
    // Adds the prefix entry to the network data.

    switch (mType)
    {
    case kTypeOnMeshPrefix:
        SuccessOrExit(AddOnMeshPrefix());
        break;

    case kTypeExternalRoute:
        SuccessOrExit(AddExternalRoute());
        break;
    }

    Get<Notifier>().HandleServerDataUpdated();
    SetState(kAdded);
    Get<Publisher>().NotifyPrefixEntryChange(kEventEntryAdded, mPrefix);

exit:
    return;
}

Error Publisher::PrefixEntry::AddOnMeshPrefix(void)
{
    OnMeshPrefixConfig config;

    config.mPrefix = mPrefix;
    config.mStable = true;
    config.SetFromTlvFlags(mFlags);

    return Get<Local>().AddOnMeshPrefix(config);
}

Error Publisher::PrefixEntry::AddExternalRoute(void)
{
    ExternalRouteConfig config;

    config.mPrefix = mPrefix;
    config.mStable = true;
    config.SetFromTlvFlags(static_cast<uint8_t>(mFlags));

    return Get<Local>().AddHasRoutePrefix(config);
}

void Publisher::PrefixEntry::Remove(State aNextState)
{
    // Remove the prefix entry from the network data.

    VerifyOrExit((GetState() == kAdded) || (GetState() == kRemoving));

    switch (mType)
    {
    case kTypeOnMeshPrefix:
        IgnoreError(Get<Local>().RemoveOnMeshPrefix(mPrefix));
        break;

    case kTypeExternalRoute:
        IgnoreError(Get<Local>().RemoveHasRoutePrefix(mPrefix));
        break;
    }

    Get<Notifier>().HandleServerDataUpdated();
    Get<Publisher>().NotifyPrefixEntryChange(kEventEntryRemoved, mPrefix);

exit:
    SetState(aNextState);
}

void Publisher::PrefixEntry::Process(void)
{
    // This method checks the entries currently present in Network Data
    // based on which it then decides whether or not take action
    // (add/remove or keep monitoring).

    uint8_t numEntries          = 0;
    uint8_t numPreferredEntries = 0;
    uint8_t desiredNumEntries   = 0;

    // Do not make any changes if device is not attached, and wait
    // for role change event.
    VerifyOrExit(Get<Mle::Mle>().IsAttached());

    VerifyOrExit(GetState() != kNoEntry);

    switch (mType)
    {
    case kTypeOnMeshPrefix:
        CountOnMeshPrefixEntries(numEntries, numPreferredEntries);
        desiredNumEntries = kDesiredNumOnMeshPrefix;
        break;
    case kTypeExternalRoute:
        CountExternalRouteEntries(numEntries, numPreferredEntries);
        desiredNumEntries = kDesiredNumExternalRoute;
        break;
    }

    UpdateState(numEntries, numPreferredEntries, desiredNumEntries);

exit:
    return;
}

void Publisher::PrefixEntry::CountOnMeshPrefixEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
{
    const PrefixTlv *      prefixTlv;
    const BorderRouterTlv *brSubTlv;
    int8_t                 preference             = BorderRouterEntry::PreferenceFromFlags(mFlags);
    uint16_t               flagsWithoutPreference = BorderRouterEntry::FlagsWithoutPreference(mFlags);

    prefixTlv = Get<Leader>().FindPrefix(mPrefix);
    VerifyOrExit(prefixTlv != nullptr);

    brSubTlv = prefixTlv->FindSubTlv<BorderRouterTlv>(/* aStable */ true);
    VerifyOrExit(brSubTlv != nullptr);

    for (const BorderRouterEntry *entry = brSubTlv->GetFirstEntry(); entry <= brSubTlv->GetLastEntry();
         entry                          = entry->GetNext())
    {
        uint16_t entryFlags      = entry->GetFlags();
        int8_t   entryPreference = BorderRouterEntry::PreferenceFromFlags(entryFlags);

        // Count an existing entry in the network data if its flags
        // match ours and and its preference is same or higher than our
        // preference. We do not count matching entries at a lower
        // preference than ours. This ensures that a device with higher
        // preference entry publishes its entry even when there are many
        // lower preference similar entries in the network data
        // (potentially causing a lower preference entry to be removed).

        if ((BorderRouterEntry::FlagsWithoutPreference(entryFlags) == flagsWithoutPreference) &&
            (entryPreference >= preference))
        {
            aNumEntries++;

            // We prefer an entry if it has strictly higher preference
            // than ours or if it has same preference we use the associated
            // RLOC16.

            if ((entryPreference > preference) || IsPreferred(entry->GetRloc()))
            {
                aNumPreferredEntries++;
            }
        }
    }

exit:
    return;
}

void Publisher::PrefixEntry::CountExternalRouteEntries(uint8_t &aNumEntries, uint8_t &aNumPreferredEntries) const
{
    const PrefixTlv *  prefixTlv;
    const HasRouteTlv *hrSubTlv;
    int8_t             preference             = HasRouteEntry::PreferenceFromFlags(static_cast<uint8_t>(mFlags));
    uint8_t            flagsWithoutPreference = HasRouteEntry::FlagsWithoutPreference(static_cast<uint8_t>(mFlags));

    prefixTlv = Get<Leader>().FindPrefix(mPrefix);
    VerifyOrExit(prefixTlv != nullptr);

    hrSubTlv = prefixTlv->FindSubTlv<HasRouteTlv>(/* aStable */ true);
    VerifyOrExit(hrSubTlv != nullptr);

    for (const HasRouteEntry *entry = hrSubTlv->GetFirstEntry(); entry <= hrSubTlv->GetLastEntry();
         entry                      = entry->GetNext())
    {
        uint8_t entryFlags      = entry->GetFlags();
        int8_t  entryPreference = HasRouteEntry::PreferenceFromFlags(entryFlags);

        // Count an existing entry in the network data if its flags
        // match ours and and its preference is same or higher than our
        // preference. We do not count matching entries at a lower
        // preference than ours. This ensures that a device with higher
        // preference entry publishes its entry even when there are many
        // lower preference similar entries in the network data
        // (potentially causing a lower preference entry to be removed).

        if ((HasRouteEntry::FlagsWithoutPreference(entryFlags) == flagsWithoutPreference) &&
            (entryPreference >= preference))
        {
            aNumEntries++;

            // We prefer an entry if it has strictly higher preference
            // than ours or if it has same preference with a smaller
            // RLOC16.

            if ((entryPreference > preference) || IsPreferred(entry->GetRloc()))
            {
                aNumPreferredEntries++;
            }
        }
    }

exit:
    return;
}

#endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE

} // namespace NetworkData
} // namespace ot

#endif // OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE
