/*
 *  Copyright (c) 2020, 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.
 */

#include "srp_client.hpp"

#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE

#include "common/as_core_type.hpp"
#include "common/code_utils.hpp"
#include "common/debug.hpp"
#include "common/instance.hpp"
#include "common/locator_getters.hpp"
#include "common/random.hpp"
#include "common/settings.hpp"
#include "common/string.hpp"

/**
 * @file
 *   This file implements the SRP client.
 */

namespace ot {
namespace Srp {

RegisterLogModule("SrpClient");

//---------------------------------------------------------------------
// Client::HostInfo

void Client::HostInfo::Init(void)
{
    Clearable<HostInfo>::Clear();

    // State is directly set on `mState` instead of using `SetState()`
    // to avoid logging.
    mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED;
}

void Client::HostInfo::Clear(void)
{
    Clearable<HostInfo>::Clear();
    SetState(kRemoved);
}

void Client::HostInfo::SetState(ItemState aState)
{
    if (aState != GetState())
    {
        LogInfo("HostInfo %s -> %s", ItemStateToString(GetState()), ItemStateToString(aState));
        mState = MapEnum(aState);
    }
}

void Client::HostInfo::EnableAutoAddress(void)
{
    mAddresses    = nullptr;
    mNumAddresses = 0;
    mAutoAddress  = true;

    LogInfo("HostInfo enabled auto address", GetNumAddresses());
}

void Client::HostInfo::SetAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses)
{
    mAddresses    = aAddresses;
    mNumAddresses = aNumAddresses;
    mAutoAddress  = false;

    LogInfo("HostInfo set %d addrs", GetNumAddresses());

    for (uint8_t index = 0; index < GetNumAddresses(); index++)
    {
        LogInfo("%s", GetAddress(index).ToString().AsCString());
    }
}

//---------------------------------------------------------------------
// Client::Service

Error Client::Service::Init(void)
{
    Error error = kErrorNone;

    VerifyOrExit((GetName() != nullptr) && (GetInstanceName() != nullptr), error = kErrorInvalidArgs);
    VerifyOrExit((GetTxtEntries() != nullptr) || (GetNumTxtEntries() == 0), error = kErrorInvalidArgs);

    // State is directly set on `mState` instead of using `SetState()`
    // to avoid logging.
    mState = OT_SRP_CLIENT_ITEM_STATE_REMOVED;

exit:
    return error;
}

void Client::Service::SetState(ItemState aState)
{
    VerifyOrExit(GetState() != aState);

    LogInfo("Service %s -> %s, \"%s\" \"%s\"", ItemStateToString(GetState()), ItemStateToString(aState),
            GetInstanceName(), GetName());

    if (aState == kToAdd)
    {
        constexpr uint16_t kSubTypeLabelStringSize = 80;

        String<kSubTypeLabelStringSize> string;

        // Log more details only when entering `kToAdd` state.

        if (HasSubType())
        {
            const char *label;

            for (uint16_t index = 0; (label = GetSubTypeLabelAt(index)) != nullptr; index++)
            {
                string.Append("%s\"%s\"", (index != 0) ? ", " : "", label);
            }
        }

        LogInfo("subtypes:[%s] port:%d weight:%d prio:%d txts:%d", string.AsCString(), GetPort(), GetWeight(),
                GetPriority(), GetNumTxtEntries());
    }

    mState = MapEnum(aState);

exit:
    return;
}

bool Client::Service::Matches(const Service &aOther) const
{
    // This method indicates whether or not two service entries match,
    // i.e., have the same service and instance names. This is intended
    // for use by `LinkedList::FindMatching()` to search within the
    // `mServices` list.

    return (strcmp(GetName(), aOther.GetName()) == 0) && (strcmp(GetInstanceName(), aOther.GetInstanceName()) == 0);
}

//---------------------------------------------------------------------
// Client::AutoStart

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

Client::AutoStart::AutoStart(void)
{
    Clear();
    mState = kDefaultMode ? kSelectedNone : kDisabled;
}

bool Client::AutoStart::HasSelectedServer(void) const
{
    bool hasSelected = false;

    switch (mState)
    {
    case kDisabled:
    case kSelectedNone:
        break;

    case kSelectedUnicastPreferred:
    case kSelectedUnicast:
    case kSelectedAnycast:
        hasSelected = true;
        break;
    }

    return hasSelected;
}

void Client::AutoStart::SetState(State aState)
{
    if (mState != aState)
    {
        LogInfo("AutoStartState %s -> %s", StateToString(mState), StateToString(aState));
        mState = aState;
    }
}

void Client::AutoStart::SetCallback(AutoStartCallback aCallback, void *aContext)
{
    mCallback = aCallback;
    mContext  = aContext;
}

void Client::AutoStart::InvokeCallback(const Ip6::SockAddr *aServerSockAddr) const
{
    if (mCallback != nullptr)
    {
        mCallback(aServerSockAddr, mContext);
    }
}

#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
const char *Client::AutoStart::StateToString(State aState)
{
    static const char *const kStateStrings[] = {
        "Disabled",    // (0) kDisabled
        "Idle",        // (1) kSelectedNone
        "Unicast-prf", // (2) kSelectedUnicastPreferred
        "Anycast",     // (3) kSelectedAnycast
        "Unicast",     // (4) kSelectedUnicast
    };

    static_assert(0 == kDisabled, "kDisabled value is incorrect");
    static_assert(1 == kSelectedNone, "kSelectedNone value is incorrect");
    static_assert(2 == kSelectedUnicastPreferred, "kSelectedUnicastPreferred value is incorrect");
    static_assert(3 == kSelectedAnycast, "kSelectedAnycast value is incorrect");
    static_assert(4 == kSelectedUnicast, "kSelectedUnicast value is incorrect");

    return kStateStrings[aState];
}
#endif

#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

//---------------------------------------------------------------------
// Client

const char Client::kDefaultDomainName[] = "default.service.arpa";

Client::Client(Instance &aInstance)
    : InstanceLocator(aInstance)
    , mState(kStateStopped)
    , mTxFailureRetryCount(0)
    , mShouldRemoveKeyLease(false)
    , mAutoHostAddressAddedMeshLocal(false)
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    , mServiceKeyRecordEnabled(false)
#endif
    , mUpdateMessageId(0)
    , mRetryWaitInterval(kMinRetryWaitInterval)
    , mAcceptedLeaseInterval(0)
    , mTtl(0)
    , mLeaseInterval(kDefaultLease)
    , mKeyLeaseInterval(kDefaultKeyLease)
    , mSocket(aInstance)
    , mCallback(nullptr)
    , mCallbackContext(nullptr)
    , mDomainName(kDefaultDomainName)
    , mTimer(aInstance, Client::HandleTimer)
{
    mHostInfo.Init();

    // The `Client` implementation uses different constant array of
    // `ItemState` to define transitions between states in `Pause()`,
    // `Stop()`, `SendUpdate`, and `ProcessResponse()`, or to convert
    // an `ItemState` to string. Here, we assert that the enumeration
    // values are correct.

    static_assert(kToAdd == 0, "kToAdd value is not correct");
    static_assert(kAdding == 1, "kAdding value is not correct");
    static_assert(kToRefresh == 2, "kToRefresh value is not correct");
    static_assert(kRefreshing == 3, "kRefreshing value is not correct");
    static_assert(kToRemove == 4, "kToRemove value is not correct");
    static_assert(kRemoving == 5, "kRemoving value is not correct");
    static_assert(kRegistered == 6, "kRegistered value is not correct");
    static_assert(kRemoved == 7, "kRemoved value is not correct");
}

Error Client::Start(const Ip6::SockAddr &aServerSockAddr, Requester aRequester)
{
    Error error;

    VerifyOrExit(GetState() == kStateStopped,
                 error = (aServerSockAddr == GetServerAddress()) ? kErrorNone : kErrorBusy);

    SuccessOrExit(error = mSocket.Open(Client::HandleUdpReceive, this));
    SuccessOrExit(error = mSocket.Connect(aServerSockAddr));

    LogInfo("%starting, server %s", (aRequester == kRequesterUser) ? "S" : "Auto-s",
            aServerSockAddr.ToString().AsCString());

    Resume();

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
    if (aRequester == kRequesterAuto)
    {
#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT_SERVER_ADDRESS_AUTO_SET_ENABLE
        Get<Dns::Client>().UpdateDefaultConfigAddress();
#endif
        mAutoStart.InvokeCallback(&aServerSockAddr);
    }
#endif

exit:
    return error;
}

void Client::Stop(Requester aRequester, StopMode aMode)
{
    // Change the state of host info and services so that they are
    // added/removed again once the client is started back. In the
    // case of `kAdding`, we intentionally move to `kToRefresh`
    // instead of `kToAdd` since the server may receive our add
    // request and the item may be registered on the server. This
    // ensures that if we are later asked to remove the item, we do
    // notify server.

    static const ItemState kNewStateOnStop[]{
        /* (0) kToAdd      -> */ kToAdd,
        /* (1) kAdding     -> */ kToRefresh,
        /* (2) kToRefresh  -> */ kToRefresh,
        /* (3) kRefreshing -> */ kToRefresh,
        /* (4) kToRemove   -> */ kToRemove,
        /* (5) kRemoving   -> */ kToRemove,
        /* (6) kRegistered -> */ kToRefresh,
        /* (7) kRemoved    -> */ kRemoved,
    };

    VerifyOrExit(GetState() != kStateStopped);

    mSingleServiceMode.Disable();

    // State changes:
    //   kAdding     -> kToRefresh
    //   kRefreshing -> kToRefresh
    //   kRemoving   -> kToRemove
    //   kRegistered -> kToRefresh

    ChangeHostAndServiceStates(kNewStateOnStop);

    IgnoreError(mSocket.Close());

    mShouldRemoveKeyLease = false;
    mTxFailureRetryCount  = 0;

    if (aMode == kResetRetryInterval)
    {
        ResetRetryWaitInterval();
    }

    SetState(kStateStopped);

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
#if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
    mAutoStart.ResetTimoutFailureCount();
#endif
    if (aRequester == kRequesterAuto)
    {
        mAutoStart.InvokeCallback(nullptr);
    }
#endif

exit:
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
    if (aRequester == kRequesterUser)
    {
        DisableAutoStartMode();
    }
#endif
}

void Client::SetCallback(Callback aCallback, void *aContext)
{
    mCallback        = aCallback;
    mCallbackContext = aContext;
}

void Client::Resume(void)
{
    SetState(kStateUpdated);
    UpdateState();
}

void Client::Pause(void)
{
    // Change the state of host info and services that are are being
    // added or removed so that they are added/removed again once the
    // client is resumed or started back.

    static const ItemState kNewStateOnPause[]{
        /* (0) kToAdd      -> */ kToAdd,
        /* (1) kAdding     -> */ kToRefresh,
        /* (2) kToRefresh  -> */ kToRefresh,
        /* (3) kRefreshing -> */ kToRefresh,
        /* (4) kToRemove   -> */ kToRemove,
        /* (5) kRemoving   -> */ kToRemove,
        /* (6) kRegistered -> */ kRegistered,
        /* (7) kRemoved    -> */ kRemoved,
    };

    mSingleServiceMode.Disable();

    // State changes:
    //   kAdding     -> kToRefresh
    //   kRefreshing -> kToRefresh
    //   kRemoving   -> kToRemove

    ChangeHostAndServiceStates(kNewStateOnPause);

    SetState(kStatePaused);
}

void Client::HandleNotifierEvents(Events aEvents)
{
    if (aEvents.Contains(kEventThreadRoleChanged))
    {
        HandleRoleChanged();
    }

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE
    if (aEvents.ContainsAny(kEventThreadNetdataChanged | kEventThreadMeshLocalAddrChanged))
    {
        ProcessAutoStart();
    }
#endif

    if (mHostInfo.IsAutoAddressEnabled())
    {
        Events::Flags eventFlags = (kEventIp6AddressAdded | kEventIp6AddressRemoved);

        if (mAutoHostAddressAddedMeshLocal)
        {
            eventFlags |= kEventThreadMeshLocalAddrChanged;
        }

        if (aEvents.ContainsAny(eventFlags))
        {
            IgnoreError(UpdateHostInfoStateOnAddressChange());
            UpdateState();
        }
    }
}

void Client::HandleRoleChanged(void)
{
    if (Get<Mle::Mle>().IsAttached())
    {
        VerifyOrExit(GetState() == kStatePaused);
        Resume();
    }
    else
    {
        VerifyOrExit(GetState() != kStateStopped);
        Pause();
    }

exit:
    return;
}

#if OPENTHREAD_CONFIG_SRP_CLIENT_DOMAIN_NAME_API_ENABLE
Error Client::SetDomainName(const char *aName)
{
    Error error = kErrorNone;

    VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), error = kErrorInvalidState);

    mDomainName = (aName != nullptr) ? aName : kDefaultDomainName;
    LogInfo("Domain name \"%s\"", mDomainName);

exit:
    return error;
}
#endif

Error Client::SetHostName(const char *aName)
{
    Error error = kErrorNone;

    VerifyOrExit(aName != nullptr, error = kErrorInvalidArgs);

    VerifyOrExit((mHostInfo.GetState() == kToAdd) || (mHostInfo.GetState() == kRemoved), error = kErrorInvalidState);

    LogInfo("Host name \"%s\"", aName);
    mHostInfo.SetName(aName);
    mHostInfo.SetState(kToAdd);
    UpdateState();

exit:
    return error;
}

Error Client::EnableAutoHostAddress(void)
{
    Error error = kErrorNone;

    VerifyOrExit(!mHostInfo.IsAutoAddressEnabled());
    SuccessOrExit(error = UpdateHostInfoStateOnAddressChange());

    mHostInfo.EnableAutoAddress();
    UpdateState();

exit:
    return error;
}

Error Client::SetHostAddresses(const Ip6::Address *aAddresses, uint8_t aNumAddresses)
{
    Error error = kErrorNone;

    VerifyOrExit((aAddresses != nullptr) && (aNumAddresses > 0), error = kErrorInvalidArgs);
    SuccessOrExit(error = UpdateHostInfoStateOnAddressChange());

    mHostInfo.SetAddresses(aAddresses, aNumAddresses);
    UpdateState();

exit:
    return error;
}

Error Client::UpdateHostInfoStateOnAddressChange(void)
{
    Error error = kErrorNone;

    VerifyOrExit((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving),
                 error = kErrorInvalidState);

    if (mHostInfo.GetState() == kRemoved)
    {
        mHostInfo.SetState(kToAdd);
    }
    else if (mHostInfo.GetState() != kToAdd)
    {
        mHostInfo.SetState(kToRefresh);
    }

exit:
    return error;
}

Error Client::AddService(Service &aService)
{
    Error error;

    VerifyOrExit(mServices.FindMatching(aService) == nullptr, error = kErrorAlready);

    SuccessOrExit(error = aService.Init());
    mServices.Push(aService);

    aService.SetState(kToAdd);
    UpdateState();

exit:
    return error;
}

Error Client::RemoveService(Service &aService)
{
    Error               error = kErrorNone;
    LinkedList<Service> removedServices;

    VerifyOrExit(mServices.Contains(aService), error = kErrorNotFound);

    UpdateServiceStateToRemove(aService);

    // Check if the service was removed immediately, if so
    // invoke the callback to report the removed service.
    GetRemovedServices(removedServices);

    if (!removedServices.IsEmpty())
    {
        InvokeCallback(kErrorNone, mHostInfo, removedServices.GetHead());
    }

    UpdateState();

exit:
    return error;
}

void Client::UpdateServiceStateToRemove(Service &aService)
{
    if (aService.GetState() == kToAdd)
    {
        // If the service has not been added yet, we can remove it immediately.
        aService.SetState(kRemoved);
    }
    else if (aService.GetState() != kRemoving)
    {
        aService.SetState(kToRemove);
    }
}

Error Client::ClearService(Service &aService)
{
    Error error;

    SuccessOrExit(error = mServices.Remove(aService));
    aService.SetNext(nullptr);
    aService.SetState(kRemoved);
    UpdateState();

exit:
    return error;
}

Error Client::RemoveHostAndServices(bool aShouldRemoveKeyLease, bool aSendUnregToServer)
{
    Error error = kErrorNone;

    LogInfo("Remove host & services");

    VerifyOrExit(mHostInfo.GetState() != kRemoved, error = kErrorAlready);

    if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
    {
        // Host info remove is already ongoing, if "key lease" remove mode is
        // the same, there is no need to send a new update message.
        VerifyOrExit(mShouldRemoveKeyLease != aShouldRemoveKeyLease);
    }

    mShouldRemoveKeyLease = aShouldRemoveKeyLease;

    for (Service &service : mServices)
    {
        UpdateServiceStateToRemove(service);
    }

    if ((mHostInfo.GetState() == kToAdd) && !aSendUnregToServer)
    {
        // Host info is not added yet (not yet registered with
        // server), so we can remove it and all services immediately.
        mHostInfo.SetState(kRemoved);
        HandleUpdateDone();
        ExitNow();
    }

    mHostInfo.SetState(kToRemove);
    UpdateState();

exit:
    return error;
}

void Client::ClearHostAndServices(void)
{
    LogInfo("Clear host & services");

    switch (GetState())
    {
    case kStateStopped:
    case kStatePaused:
        break;

    case kStateToUpdate:
    case kStateUpdating:
    case kStateUpdated:
    case kStateToRetry:
        SetState(kStateUpdated);
        break;
    }

    mTxFailureRetryCount = 0;
    ResetRetryWaitInterval();

    mServices.Clear();
    mHostInfo.Clear();
}

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

    LogInfo("State %s -> %s", StateToString(mState), StateToString(aState));
    mState = aState;

    switch (mState)
    {
    case kStateStopped:
    case kStatePaused:
    case kStateUpdated:
        mTimer.Stop();
        break;

    case kStateToUpdate:
        mTimer.Start(Random::NonCrypto::GetUint32InRange(kUpdateTxMinDelay, kUpdateTxMaxDelay));
        break;

    case kStateUpdating:
        mTimer.Start(GetRetryWaitInterval());
        break;

    case kStateToRetry:
        break;
    }
exit:
    return;
}

void Client::ChangeHostAndServiceStates(const ItemState *aNewStates)
{
#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
    ItemState oldHostState = mHostInfo.GetState();
#endif

    mHostInfo.SetState(aNewStates[mHostInfo.GetState()]);

    for (Service &service : mServices)
    {
        if (mSingleServiceMode.IsEnabled() && mSingleServiceMode.GetService() != &service)
        {
            continue;
        }

        service.SetState(aNewStates[service.GetState()]);
    }

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
    if ((oldHostState != kRegistered) && (mHostInfo.GetState() == kRegistered))
    {
        Settings::SrpClientInfo info;

        switch (mAutoStart.GetState())
        {
        case AutoStart::kDisabled:
        case AutoStart::kSelectedNone:
            break;

        case AutoStart::kSelectedUnicastPreferred:
        case AutoStart::kSelectedUnicast:
            info.SetServerAddress(GetServerAddress().GetAddress());
            info.SetServerPort(GetServerAddress().GetPort());
            IgnoreError(Get<Settings>().Save(info));
            break;

        case AutoStart::kSelectedAnycast:
            IgnoreError(Get<Settings>().Delete<Settings::SrpClientInfo>());
            break;
        }
    }
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
}

void Client::InvokeCallback(Error aError) const
{
    InvokeCallback(aError, mHostInfo, nullptr);
}

void Client::InvokeCallback(Error aError, const HostInfo &aHostInfo, const Service *aRemovedServices) const
{
    VerifyOrExit(mCallback != nullptr);
    mCallback(aError, &aHostInfo, mServices.GetHead(), aRemovedServices, mCallbackContext);

exit:
    return;
}

void Client::SendUpdate(void)
{
    static const ItemState kNewStateOnMessageTx[]{
        /* (0) kToAdd      -> */ kAdding,
        /* (1) kAdding     -> */ kAdding,
        /* (2) kToRefresh  -> */ kRefreshing,
        /* (3) kRefreshing -> */ kRefreshing,
        /* (4) kToRemove   -> */ kRemoving,
        /* (5) kRemoving   -> */ kRemoving,
        /* (6) kRegistered -> */ kRegistered,
        /* (7) kRemoved    -> */ kRemoved,
    };

    Error    error   = kErrorNone;
    Message *message = mSocket.NewMessage(0);
    uint32_t length;

    VerifyOrExit(message != nullptr, error = kErrorNoBufs);
    SuccessOrExit(error = PrepareUpdateMessage(*message));

    length = message->GetLength() + sizeof(Ip6::Udp::Header) + sizeof(Ip6::Header);

    if (length >= Ip6::kMaxDatagramLength)
    {
        LogInfo("Msg len %u is larger than MTU, enabling single service mode", length);
        mSingleServiceMode.Enable();
        IgnoreError(message->SetLength(0));
        SuccessOrExit(error = PrepareUpdateMessage(*message));
    }

    SuccessOrExit(error = mSocket.SendTo(*message, Ip6::MessageInfo()));

    LogInfo("Send update");

    // State changes:
    //   kToAdd     -> kAdding
    //   kToRefresh -> kRefreshing
    //   kToRemove  -> kRemoving

    ChangeHostAndServiceStates(kNewStateOnMessageTx);

    // Remember the update message tx time to use later to determine the
    // lease renew time.
    mLeaseRenewTime      = TimerMilli::GetNow();
    mTxFailureRetryCount = 0;

    SetState(kStateUpdating);

    if (!Get<Mle::Mle>().IsRxOnWhenIdle())
    {
        // If device is sleepy send fast polls while waiting for
        // the response from server.
        Get<DataPollSender>().SendFastPolls(kFastPollsAfterUpdateTx);
    }

exit:
    if (error != kErrorNone)
    {
        // If there is an error in preparation or transmission of the
        // update message (e.g., no buffer to allocate message), up to
        // `kMaxTxFailureRetries` times, we wait for a short interval
        // `kTxFailureRetryInterval` and try again. After this, we
        // continue to retry using the `mRetryWaitInterval` (which keeps
        // growing on each failure).

        LogInfo("Failed to send update: %s", ErrorToString(error));

        mSingleServiceMode.Disable();
        FreeMessage(message);

        SetState(kStateToRetry);

        if (mTxFailureRetryCount < kMaxTxFailureRetries)
        {
            uint32_t interval;

            mTxFailureRetryCount++;
            interval = Random::NonCrypto::AddJitter(kTxFailureRetryInterval, kTxFailureRetryJitter);
            mTimer.Start(interval);

            LogInfo("Quick retry %d in %u msec", mTxFailureRetryCount, interval);

            // Do not report message preparation errors to user
            // until `kMaxTxFailureRetries` are exhausted.
        }
        else
        {
            LogRetryWaitInterval();
            mTimer.Start(Random::NonCrypto::AddJitter(GetRetryWaitInterval(), kRetryIntervalJitter));
            GrowRetryWaitInterval();
            InvokeCallback(error);
        }
    }
}

Error Client::PrepareUpdateMessage(Message &aMessage)
{
    constexpr uint16_t kHeaderOffset = 0;

    Error             error = kErrorNone;
    Dns::UpdateHeader header;
    Info              info;

    info.Clear();

    SuccessOrExit(error = ReadOrGenerateKey(info.mKeyPair));

    // Generate random Message ID and ensure it is different from last one
    do
    {
        SuccessOrExit(error = header.SetRandomMessageId());
    } while (header.GetMessageId() == mUpdateMessageId);

    mUpdateMessageId = header.GetMessageId();

    // SRP Update (DNS Update) message must have exactly one record in
    // Zone section, no records in Prerequisite Section, can have
    // multiple records in Update Section (tracked as they are added),
    // and two records in Additional Data Section (OPT and SIG records).
    // The SIG record itself should not be included in calculation of
    // SIG(0) signature, so the addition record count is set to one
    // here. After signature calculation and appending of SIG record,
    // the additional record count is updated to two and the header is
    // rewritten in the message.

    header.SetZoneRecordCount(1);
    header.SetAdditionalRecordCount(1);
    SuccessOrExit(error = aMessage.Append(header));

    // Prepare Zone section

    info.mDomainNameOffset = aMessage.GetLength();
    SuccessOrExit(error = Dns::Name::AppendName(mDomainName, aMessage));
    SuccessOrExit(error = aMessage.Append(Dns::Zone()));

    // Prepare Update section

    if ((mHostInfo.GetState() != kToRemove) && (mHostInfo.GetState() != kRemoving))
    {
        for (Service &service : mServices)
        {
            SuccessOrExit(error = AppendServiceInstructions(service, aMessage, info));

            if (mSingleServiceMode.IsEnabled() && (mSingleServiceMode.GetService() != nullptr))
            {
                break;
            }
        }
    }

    SuccessOrExit(error = AppendHostDescriptionInstruction(aMessage, info));

    header.SetUpdateRecordCount(info.mRecordCount);
    aMessage.Write(kHeaderOffset, header);

    // Prepare Additional Data section

    SuccessOrExit(error = AppendUpdateLeaseOptRecord(aMessage));
    SuccessOrExit(error = AppendSignature(aMessage, info));

    header.SetAdditionalRecordCount(2); // Lease OPT and SIG RRs
    aMessage.Write(kHeaderOffset, header);

exit:
    return error;
}

Error Client::ReadOrGenerateKey(Crypto::Ecdsa::P256::KeyPair &aKeyPair)
{
    Error error;

    error = Get<Settings>().Read<Settings::SrpEcdsaKey>(aKeyPair);

    if (error == kErrorNone)
    {
        Crypto::Ecdsa::P256::PublicKey publicKey;

        if (aKeyPair.GetPublicKey(publicKey) == kErrorNone)
        {
            ExitNow();
        }
    }

    SuccessOrExit(error = aKeyPair.Generate());
    IgnoreError(Get<Settings>().Save<Settings::SrpEcdsaKey>(aKeyPair));

exit:
    return error;
}

Error Client::AppendServiceInstructions(Service &aService, Message &aMessage, Info &aInfo)
{
    Error               error = kErrorNone;
    Dns::ResourceRecord rr;
    Dns::SrvRecord      srv;
    bool                removing;
    uint16_t            serviceNameOffset;
    uint16_t            instanceNameOffset;
    uint16_t            offset;

    if (aService.GetState() == kRegistered)
    {
        // If the lease needs to be renewed or if we are close to the
        // renewal time of a registered service, we refresh the service
        // early and include it in this update. This helps put more
        // services on the same lease refresh schedule.

        VerifyOrExit(ShouldRenewEarly(aService));
        aService.SetState(kToRefresh);
    }

    removing = ((aService.GetState() == kToRemove) || (aService.GetState() == kRemoving));

    //----------------------------------
    // Service Discovery Instruction

    // PTR record

    // "service name labels" + (pointer to) domain name.
    serviceNameOffset = aMessage.GetLength();
    SuccessOrExit(error = Dns::Name::AppendMultipleLabels(aService.GetName(), aMessage));
    SuccessOrExit(error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage));

    // On remove, we use "Delete an RR from an RRSet" where class is set
    // to NONE and TTL to zero (RFC 2136 - section 2.5.4).

    rr.Init(Dns::ResourceRecord::kTypePtr, removing ? Dns::PtrRecord::kClassNone : Dns::PtrRecord::kClassInternet);
    rr.SetTtl(removing ? 0 : GetTtl());
    offset = aMessage.GetLength();
    SuccessOrExit(error = aMessage.Append(rr));

    // "Instance name" + (pointer to) service name.
    instanceNameOffset = aMessage.GetLength();
    SuccessOrExit(error = Dns::Name::AppendLabel(aService.GetInstanceName(), aMessage));
    SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, aMessage));

    UpdateRecordLengthInMessage(rr, offset, aMessage);
    aInfo.mRecordCount++;

    if (aService.HasSubType() && !removing)
    {
        const char *subTypeLabel;
        uint16_t    subServiceNameOffset = 0;

        for (uint16_t index = 0; (subTypeLabel = aService.GetSubTypeLabelAt(index)) != nullptr; ++index)
        {
            // subtype label + "_sub" label + (pointer to) service name.

            SuccessOrExit(error = Dns::Name::AppendLabel(subTypeLabel, aMessage));

            if (index == 0)
            {
                subServiceNameOffset = aMessage.GetLength();
                SuccessOrExit(error = Dns::Name::AppendLabel("_sub", aMessage));
                SuccessOrExit(error = Dns::Name::AppendPointerLabel(serviceNameOffset, aMessage));
            }
            else
            {
                SuccessOrExit(error = Dns::Name::AppendPointerLabel(subServiceNameOffset, aMessage));
            }

            // `rr` is already initialized as PTR.
            offset = aMessage.GetLength();
            SuccessOrExit(error = aMessage.Append(rr));

            SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
            UpdateRecordLengthInMessage(rr, offset, aMessage);
            aInfo.mRecordCount++;
        }
    }

    //----------------------------------
    // Service Description Instruction

    // "Delete all RRsets from a name" for Instance Name.

    SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
    SuccessOrExit(error = AppendDeleteAllRrsets(aMessage));
    aInfo.mRecordCount++;

    VerifyOrExit(!removing);

    // SRV RR

    SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
    srv.Init();
    srv.SetTtl(GetTtl());
    srv.SetPriority(aService.GetPriority());
    srv.SetWeight(aService.GetWeight());
    srv.SetPort(aService.GetPort());
    offset = aMessage.GetLength();
    SuccessOrExit(error = aMessage.Append(srv));
    SuccessOrExit(error = AppendHostName(aMessage, aInfo));
    UpdateRecordLengthInMessage(srv, offset, aMessage);
    aInfo.mRecordCount++;

    // TXT RR

    SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
    rr.Init(Dns::ResourceRecord::kTypeTxt);
    offset = aMessage.GetLength();
    SuccessOrExit(error = aMessage.Append(rr));
    SuccessOrExit(error =
                      Dns::TxtEntry::AppendEntries(aService.GetTxtEntries(), aService.GetNumTxtEntries(), aMessage));
    UpdateRecordLengthInMessage(rr, offset, aMessage);
    aInfo.mRecordCount++;

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    if (mServiceKeyRecordEnabled)
    {
        // KEY RR is optional in "Service Description Instruction". It
        // is added here under `REFERENCE_DEVICE` config and is intended
        // for testing only.

        SuccessOrExit(error = Dns::Name::AppendPointerLabel(instanceNameOffset, aMessage));
        SuccessOrExit(error = AppendKeyRecord(aMessage, aInfo));
    }
#endif

    if (mSingleServiceMode.IsEnabled())
    {
        mSingleServiceMode.SetService(aService);
    }

exit:
    return error;
}

Error Client::AppendHostDescriptionInstruction(Message &aMessage, Info &aInfo)
{
    Error error = kErrorNone;

    //----------------------------------
    // Host Description Instruction

    // "Delete all RRsets from a name" for Host Name.

    SuccessOrExit(error = AppendHostName(aMessage, aInfo));
    SuccessOrExit(error = AppendDeleteAllRrsets(aMessage));
    aInfo.mRecordCount++;

    // AAAA RRs

    if (mHostInfo.IsAutoAddressEnabled())
    {
        // Append all addresses on Thread netif excluding link-local and
        // mesh-local addresses. If no address is appended, we include
        // the mesh local address.

        mAutoHostAddressAddedMeshLocal = true;

        for (const Ip6::Netif::UnicastAddress &unicastAddress : Get<ThreadNetif>().GetUnicastAddresses())
        {
            if (unicastAddress.GetAddress().IsLinkLocal() ||
                Get<Mle::Mle>().IsMeshLocalAddress(unicastAddress.GetAddress()))
            {
                continue;
            }

            SuccessOrExit(error = AppendAaaaRecord(unicastAddress.GetAddress(), aMessage, aInfo));
            mAutoHostAddressAddedMeshLocal = false;
        }

        if (mAutoHostAddressAddedMeshLocal)
        {
            SuccessOrExit(error = AppendAaaaRecord(Get<Mle::Mle>().GetMeshLocal64(), aMessage, aInfo));
        }
    }
    else
    {
        for (uint8_t index = 0; index < mHostInfo.GetNumAddresses(); index++)
        {
            SuccessOrExit(error = AppendAaaaRecord(mHostInfo.GetAddress(index), aMessage, aInfo));
        }
    }

    // KEY RR

    SuccessOrExit(error = AppendHostName(aMessage, aInfo));
    SuccessOrExit(error = AppendKeyRecord(aMessage, aInfo));

exit:
    return error;
}

Error Client::AppendAaaaRecord(const Ip6::Address &aAddress, Message &aMessage, Info &aInfo) const
{
    Error               error;
    Dns::ResourceRecord rr;

    rr.Init(Dns::ResourceRecord::kTypeAaaa);
    rr.SetTtl(GetTtl());
    rr.SetLength(sizeof(Ip6::Address));

    SuccessOrExit(error = AppendHostName(aMessage, aInfo));
    SuccessOrExit(error = aMessage.Append(rr));
    SuccessOrExit(error = aMessage.Append(aAddress));
    aInfo.mRecordCount++;

exit:
    return error;
}

Error Client::AppendKeyRecord(Message &aMessage, Info &aInfo) const
{
    Error                          error;
    Dns::KeyRecord                 key;
    Crypto::Ecdsa::P256::PublicKey publicKey;

    key.Init();
    key.SetTtl(GetTtl());
    key.SetFlags(Dns::KeyRecord::kAuthConfidPermitted, Dns::KeyRecord::kOwnerNonZone,
                 Dns::KeyRecord::kSignatoryFlagGeneral);
    key.SetProtocol(Dns::KeyRecord::kProtocolDnsSec);
    key.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256);
    key.SetLength(sizeof(Dns::KeyRecord) - sizeof(Dns::ResourceRecord) + sizeof(Crypto::Ecdsa::P256::PublicKey));
    SuccessOrExit(error = aMessage.Append(key));
    SuccessOrExit(error = aInfo.mKeyPair.GetPublicKey(publicKey));
    SuccessOrExit(error = aMessage.Append(publicKey));
    aInfo.mRecordCount++;

exit:
    return error;
}

Error Client::AppendDeleteAllRrsets(Message &aMessage) const
{
    // "Delete all RRsets from a name" (RFC 2136 - 2.5.3)
    // Name should be already appended in the message.

    Dns::ResourceRecord rr;

    rr.Init(Dns::ResourceRecord::kTypeAny, Dns::ResourceRecord::kClassAny);
    rr.SetTtl(0);
    rr.SetLength(0);

    return aMessage.Append(rr);
}

Error Client::AppendHostName(Message &aMessage, Info &aInfo, bool aDoNotCompress) const
{
    Error error;

    if (aDoNotCompress)
    {
        // Uncompressed (canonical form) of host name is used for SIG(0)
        // calculation.
        SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), aMessage));
        error = Dns::Name::AppendName(mDomainName, aMessage);
        ExitNow();
    }

    // If host name was previously added in the message, add it
    // compressed as pointer to the previous one. Otherwise,
    // append it and remember the offset.

    if (aInfo.mHostNameOffset != Info::kUnknownOffset)
    {
        ExitNow(error = Dns::Name::AppendPointerLabel(aInfo.mHostNameOffset, aMessage));
    }

    aInfo.mHostNameOffset = aMessage.GetLength();
    SuccessOrExit(error = Dns::Name::AppendMultipleLabels(mHostInfo.GetName(), aMessage));
    error = Dns::Name::AppendPointerLabel(aInfo.mDomainNameOffset, aMessage);

exit:
    return error;
}

Error Client::AppendUpdateLeaseOptRecord(Message &aMessage) const
{
    Error            error;
    Dns::OptRecord   optRecord;
    Dns::LeaseOption leaseOption;

    // Append empty (root domain) as OPT RR name.
    SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage));

    // `Init()` sets the type and clears (set to zero) the extended
    // Response Code, version and all flags.
    optRecord.Init();
    optRecord.SetUdpPayloadSize(kUdpPayloadSize);
    optRecord.SetDnsSecurityFlag();
    optRecord.SetLength(sizeof(Dns::LeaseOption));

    SuccessOrExit(error = aMessage.Append(optRecord));

    leaseOption.Init();

    if ((mHostInfo.GetState() == kToRemove) || (mHostInfo.GetState() == kRemoving))
    {
        leaseOption.SetLeaseInterval(0);
        leaseOption.SetKeyLeaseInterval(mShouldRemoveKeyLease ? 0 : mKeyLeaseInterval);
    }
    else
    {
        leaseOption.SetLeaseInterval(mLeaseInterval);
        leaseOption.SetKeyLeaseInterval(mKeyLeaseInterval);
    }

    error = aMessage.Append(leaseOption);

exit:
    return error;
}

Error Client::AppendSignature(Message &aMessage, Info &aInfo)
{
    Error                          error;
    Dns::SigRecord                 sig;
    Crypto::Sha256                 sha256;
    Crypto::Sha256::Hash           hash;
    Crypto::Ecdsa::P256::Signature signature;
    uint16_t                       offset;
    uint16_t                       len;

    // Prepare SIG RR: TTL, type covered, labels count should be set
    // to zero. Since we have no clock, inception and expiration time
    // are also set to zero. The RDATA length will be set later (not
    // yet known due to variably (and possible compression) of signer's
    // name.

    sig.Clear();
    sig.Init(Dns::ResourceRecord::kClassAny);
    sig.SetAlgorithm(Dns::KeyRecord::kAlgorithmEcdsaP256Sha256);

    // Append the SIG RR with full uncompressed form of the host name
    // as the signer's name. This is used for SIG(0) calculation only.
    // It will be overwritten with host name compressed.

    offset = aMessage.GetLength();
    SuccessOrExit(error = aMessage.Append(sig));
    SuccessOrExit(error = AppendHostName(aMessage, aInfo, /* aDoNotCompress */ true));

    // Calculate signature (RFC 2931): Calculated over "data" which is
    // concatenation of (1) the SIG RR RDATA wire format (including
    // the canonical form of the signer's name), entirely omitting the
    // signature subfield, (2) DNS query message, including DNS header
    // but not UDP/IP header before the header RR counts have been
    // adjusted for the inclusion of SIG(0).

    sha256.Start();

    // (1) SIG RR RDATA wire format
    len = aMessage.GetLength() - offset - sizeof(Dns::ResourceRecord);
    sha256.Update(aMessage, offset + sizeof(Dns::ResourceRecord), len);

    // (2) Message from DNS header before SIG
    sha256.Update(aMessage, 0, offset);

    sha256.Finish(hash);
    SuccessOrExit(error = aInfo.mKeyPair.Sign(hash, signature));

    // Move back in message and append SIG RR now with compressed host
    // name (as signer's name) along with the calculated signature.

    IgnoreError(aMessage.SetLength(offset));

    // SIG(0) uses owner name of root (single zero byte).
    SuccessOrExit(error = Dns::Name::AppendTerminator(aMessage));

    offset = aMessage.GetLength();
    SuccessOrExit(error = aMessage.Append(sig));
    SuccessOrExit(error = AppendHostName(aMessage, aInfo));
    SuccessOrExit(error = aMessage.Append(signature));
    UpdateRecordLengthInMessage(sig, offset, aMessage);

exit:
    return error;
}

void Client::UpdateRecordLengthInMessage(Dns::ResourceRecord &aRecord, uint16_t aOffset, Message &aMessage) const
{
    // This method is used to calculate an RR DATA length and update
    // (rewrite) it in a message. This should be called immediately
    // after all the fields in the record are written in the message.
    // `aOffset` gives the offset in the message to the start of the
    // record.

    aRecord.SetLength(aMessage.GetLength() - aOffset - sizeof(Dns::ResourceRecord));
    aMessage.Write(aOffset, aRecord);
}

void Client::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    OT_UNUSED_VARIABLE(aMessageInfo);

    static_cast<Client *>(aContext)->ProcessResponse(AsCoreType(aMessage));
}

void Client::ProcessResponse(Message &aMessage)
{
    static const ItemState kNewStateOnUpdateDone[]{
        /* (0) kToAdd      -> */ kToAdd,
        /* (1) kAdding     -> */ kRegistered,
        /* (2) kToRefresh  -> */ kToRefresh,
        /* (3) kRefreshing -> */ kRegistered,
        /* (4) kToRemove   -> */ kToRemove,
        /* (5) kRemoving   -> */ kRemoved,
        /* (6) kRegistered -> */ kRegistered,
        /* (7) kRemoved    -> */ kRemoved,
    };

    Error               error = kErrorNone;
    Dns::UpdateHeader   header;
    uint16_t            offset = aMessage.GetOffset();
    uint16_t            recordCount;
    LinkedList<Service> removedServices;

    VerifyOrExit(GetState() == kStateUpdating);

    SuccessOrExit(error = aMessage.Read(offset, header));

    VerifyOrExit(header.GetType() == Dns::Header::kTypeResponse, error = kErrorParse);
    VerifyOrExit(header.GetQueryType() == Dns::Header::kQueryTypeUpdate, error = kErrorParse);
    VerifyOrExit(header.GetMessageId() == mUpdateMessageId, error = kErrorDrop);

    if (!Get<Mle::Mle>().IsRxOnWhenIdle())
    {
        Get<DataPollSender>().StopFastPolls();
    }

    // Response is for the earlier request message.

    LogInfo("Received response");

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
    mAutoStart.ResetTimoutFailureCount();
#endif

    error = Dns::Header::ResponseCodeToError(header.GetResponseCode());

    if (error != kErrorNone)
    {
        LogInfo("Server rejected %s code:%d", ErrorToString(error), header.GetResponseCode());

        if (mHostInfo.GetState() == kAdding)
        {
            // Since server rejected the update message, we go back to
            // `kToAdd` state to allow user to give a new name using
            // `SetHostName()`.
            mHostInfo.SetState(kToAdd);
        }

        // Wait for the timer to expire to retry. Note that timer is
        // already scheduled for the current wait interval when state
        // was changed to `kStateUpdating`.

        LogRetryWaitInterval();
        GrowRetryWaitInterval();
        SetState(kStateToRetry);
        InvokeCallback(error);

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
        if ((error == kErrorDuplicated) || (error == kErrorSecurity))
        {
            // If the server rejects the update with specific errors
            // (indicating duplicate name and/or security error), we
            // try to switch the server (we check if another can be
            // found in the Network Data).
            //
            // Note that this is done after invoking the callback and
            // notifying the user of the error from server. This works
            // correctly even if user makes changes from callback
            // (e.g., calls SRP client APIs like `Stop` or disables
            // auto-start), since we have a guard check at the top of
            // `SelectNextServer()` to verify that client is still
            // running and auto-start is enabled and selected the
            // server.

            SelectNextServer(/* aDisallowSwitchOnRegisteredHost */ true);
        }
#endif
        ExitNow(error = kErrorNone);
    }

    offset += sizeof(header);

    // Skip over all sections till Additional Data section
    // SPEC ENHANCEMENT: Server can echo the request back or not
    // include any of RRs. Would be good to explicitly require SRP server
    // to not echo back RRs.

    if (header.GetZoneRecordCount() != 0)
    {
        VerifyOrExit(header.GetZoneRecordCount() == 1, error = kErrorParse);
        SuccessOrExit(error = Dns::Name::ParseName(aMessage, offset));
        VerifyOrExit(offset + sizeof(Dns::Zone) <= aMessage.GetLength(), error = kErrorParse);
        offset += sizeof(Dns::Zone);
    }

    // Check for Update Lease OPT RR. This determines the lease
    // interval accepted by server. If not present, then use the
    // transmitted lease interval from the update request message.

    mAcceptedLeaseInterval = mLeaseInterval;
    recordCount =
        header.GetPrerequisiteRecordCount() + header.GetUpdateRecordCount() + header.GetAdditionalRecordCount();

    while (recordCount > 0)
    {
        uint16_t            startOffset = offset;
        Dns::ResourceRecord rr;

        SuccessOrExit(error = ReadResourceRecord(aMessage, offset, rr));
        recordCount--;

        if (rr.GetType() == Dns::ResourceRecord::kTypeOpt)
        {
            SuccessOrExit(error = ProcessOptRecord(aMessage, startOffset, static_cast<Dns::OptRecord &>(rr)));
        }
    }

    // Calculate the lease renew time based on update message tx time
    // and the lease time. `kLeaseRenewGuardInterval` is used to
    // ensure that we renew the lease before server expires it. In the
    // unlikely (but maybe useful for testing) case where the accepted
    // lease interval is too short (shorter than the guard time) we
    // just use half of the accepted lease interval.

    if (mAcceptedLeaseInterval > kLeaseRenewGuardInterval)
    {
        mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval - kLeaseRenewGuardInterval);
    }
    else
    {
        mLeaseRenewTime += Time::SecToMsec(mAcceptedLeaseInterval) / 2;
    }

    for (Service &service : mServices)
    {
        if ((service.GetState() == kAdding) || (service.GetState() == kRefreshing))
        {
            service.SetLeaseRenewTime(mLeaseRenewTime);
        }
    }

    // State changes:
    //   kAdding     -> kRegistered
    //   kRefreshing -> kRegistered
    //   kRemoving   -> kRemoved

    ChangeHostAndServiceStates(kNewStateOnUpdateDone);
    mSingleServiceMode.Disable();

    HandleUpdateDone();
    UpdateState();

exit:
    if (error != kErrorNone)
    {
        LogInfo("Failed to process response %s", ErrorToString(error));
    }
}

void Client::HandleUpdateDone(void)
{
    HostInfo            hostInfoCopy = mHostInfo;
    LinkedList<Service> removedServices;

    if (mHostInfo.GetState() == kRemoved)
    {
        mHostInfo.Clear();
    }

    ResetRetryWaitInterval();
    SetState(kStateUpdated);

    GetRemovedServices(removedServices);
    InvokeCallback(kErrorNone, hostInfoCopy, removedServices.GetHead());
}

void Client::GetRemovedServices(LinkedList<Service> &aRemovedServices)
{
    mServices.RemoveAllMatching(kRemoved, aRemovedServices);
}

Error Client::ReadResourceRecord(const Message &aMessage, uint16_t &aOffset, Dns::ResourceRecord &aRecord)
{
    // Reads and skips over a Resource Record (RR) from message at
    // given offset. On success, `aOffset` is updated to point to end
    // of RR.

    Error error;

    SuccessOrExit(error = Dns::Name::ParseName(aMessage, aOffset));
    SuccessOrExit(error = aMessage.Read(aOffset, aRecord));
    VerifyOrExit(aOffset + aRecord.GetSize() <= aMessage.GetLength(), error = kErrorParse);
    aOffset += static_cast<uint16_t>(aRecord.GetSize());

exit:
    return error;
}

Error Client::ProcessOptRecord(const Message &aMessage, uint16_t aOffset, const Dns::OptRecord &aOptRecord)
{
    // Read and process all options (in an OPT RR) from a message.
    // The `aOffset` points to beginning of record in `aMessage`.

    Error    error = kErrorNone;
    uint16_t len;

    IgnoreError(Dns::Name::ParseName(aMessage, aOffset));
    aOffset += sizeof(Dns::OptRecord);

    len = aOptRecord.GetLength();

    while (len > 0)
    {
        Dns::LeaseOption leaseOption;
        Dns::Option &    option = leaseOption;
        uint16_t         size;

        SuccessOrExit(error = aMessage.Read(aOffset, option));

        VerifyOrExit(aOffset + option.GetSize() <= aMessage.GetLength(), error = kErrorParse);

        if ((option.GetOptionCode() == Dns::Option::kUpdateLease) &&
            (option.GetOptionLength() >= Dns::LeaseOption::kOptionLength))
        {
            SuccessOrExit(error = aMessage.Read(aOffset, leaseOption));

            mAcceptedLeaseInterval = leaseOption.GetLeaseInterval();

            if (mAcceptedLeaseInterval > kMaxLease)
            {
                mAcceptedLeaseInterval = kMaxLease;
            }
        }

        size = static_cast<uint16_t>(option.GetSize());
        aOffset += size;
        len -= size;
    }

exit:
    return error;
}

void Client::UpdateState(void)
{
    TimeMilli now               = TimerMilli::GetNow();
    TimeMilli earliestRenewTime = now.GetDistantFuture();
    bool      shouldUpdate      = false;

    VerifyOrExit((GetState() != kStateStopped) && (GetState() != kStatePaused));
    VerifyOrExit(mHostInfo.GetName() != nullptr);

    // Go through the host info and all the services to check if there
    // are any new changes (i.e., anything new to add or remove). This
    // is used to determine whether to send an SRP update message or
    // not. Also keep track of the earliest renew time among the
    // previously registered services. This is used to schedule the
    // timer for next refresh.

    switch (mHostInfo.GetState())
    {
    case kAdding:
    case kRefreshing:
    case kRemoving:
        break;

    case kRegistered:
        if (now < mLeaseRenewTime)
        {
            break;
        }

        mHostInfo.SetState(kToRefresh);

        // Fall through

    case kToAdd:
    case kToRefresh:
        // Make sure we have at least one service and at least one
        // host address, otherwise no need to send SRP update message.
        // The exception is when removing host info where we allow
        // for empty service list.
        VerifyOrExit(!mServices.IsEmpty() && (mHostInfo.IsAutoAddressEnabled() || (mHostInfo.GetNumAddresses() > 0)));

        // Fall through

    case kToRemove:
        shouldUpdate = true;
        break;

    case kRemoved:
        ExitNow();
    }

    // If host info is being removed, we skip over checking service list
    // for new adds (or removes). This handles the situation where while
    // remove is ongoing and before we get a response from the server,
    // user adds a new service to be registered. We wait for remove to
    // finish (receive response from server) before starting with a new
    // service adds.

    if (mHostInfo.GetState() != kRemoving)
    {
        for (Service &service : mServices)
        {
            switch (service.GetState())
            {
            case kToAdd:
            case kToRefresh:
            case kToRemove:
                shouldUpdate = true;
                break;

            case kRegistered:
                if (service.GetLeaseRenewTime() <= now)
                {
                    service.SetState(kToRefresh);
                    shouldUpdate = true;
                }
                else if (service.GetLeaseRenewTime() < earliestRenewTime)
                {
                    earliestRenewTime = service.GetLeaseRenewTime();
                }

                break;

            case kAdding:
            case kRefreshing:
            case kRemoving:
            case kRemoved:
                break;
            }
        }
    }

    if (shouldUpdate)
    {
        SetState(kStateToUpdate);
        ExitNow();
    }

    if ((GetState() == kStateUpdated) && (earliestRenewTime != now.GetDistantFuture()))
    {
        mTimer.FireAt(earliestRenewTime);
    }

exit:
    return;
}

void Client::GrowRetryWaitInterval(void)
{
    mRetryWaitInterval =
        mRetryWaitInterval / kRetryIntervalGrowthFactorDenominator * kRetryIntervalGrowthFactorNumerator;

    if (mRetryWaitInterval > kMaxRetryWaitInterval)
    {
        mRetryWaitInterval = kMaxRetryWaitInterval;
    }
}

uint32_t Client::GetBoundedLeaseInterval(uint32_t aInterval, uint32_t aDefaultInterval) const
{
    uint32_t boundedInterval = aDefaultInterval;

    if (aInterval != 0)
    {
        boundedInterval = OT_MIN(aInterval, static_cast<uint32_t>(kMaxLease));
    }

    return boundedInterval;
}

bool Client::ShouldRenewEarly(const Service &aService) const
{
    // Check if we reached the service renew time or close to it. The
    // "early renew interval" is used to allow early refresh. It is
    // calculated as a factor of the `mAcceptedLeaseInterval`. The
    // "early lease renew factor" is given as a fraction (numerator and
    // denominator). If the denominator is set to zero (i.e., factor is
    // set to infinity), then service is always included in all SRP
    // update messages.

    bool shouldRenew;

#if OPENTHREAD_CONFIG_SRP_CLIENT_EARLY_LEASE_RENEW_FACTOR_DENOMINATOR != 0
    uint32_t earlyRenewInterval =
        Time::SecToMsec(mAcceptedLeaseInterval) / kEarlyLeaseRenewFactorDenominator * kEarlyLeaseRenewFactorNumerator;

    shouldRenew = (aService.GetLeaseRenewTime() <= TimerMilli::GetNow() + earlyRenewInterval);
#else
    OT_UNUSED_VARIABLE(aService);
    shouldRenew = true;
#endif

    return shouldRenew;
}

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

void Client::HandleTimer(void)
{
    switch (GetState())
    {
    case kStateStopped:
    case kStatePaused:
        break;

    case kStateToUpdate:
    case kStateToRetry:
        SendUpdate();
        break;

    case kStateUpdating:
        mSingleServiceMode.Disable();
        LogRetryWaitInterval();
        LogInfo("Timed out, no response");
        GrowRetryWaitInterval();
        SetState(kStateToUpdate);
        InvokeCallback(kErrorResponseTimeout);

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE && OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE

        // After certain number of back-to-back timeout failures, we try
        // to switch the server. This is again done after invoking the
        // callback. It works correctly due to the guard check at the
        // top of `SelectNextServer()`.

        mAutoStart.IncrementTimoutFailureCount();

        if (mAutoStart.GetTimoutFailureCount() >= kMaxTimeoutFailuresToSwitchServer)
        {
            SelectNextServer(kDisallowSwitchOnRegisteredHost);
        }
#endif
        break;

    case kStateUpdated:
        UpdateState();
        break;
    }
}

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

void Client::EnableAutoStartMode(AutoStartCallback aCallback, void *aContext)
{
    mAutoStart.SetCallback(aCallback, aContext);

    VerifyOrExit(mAutoStart.GetState() == AutoStart::kDisabled);

    mAutoStart.SetState(AutoStart::kSelectedNone);
    ProcessAutoStart();

exit:
    return;
}

void Client::ProcessAutoStart(void)
{
    Ip6::SockAddr       serverSockAddr;
    DnsSrpAnycast::Info anycastInfo;
    DnsSrpUnicast::Info unicastInfo;
    bool                shouldRestart = false;

    // If auto start mode is enabled, we check the Network Data entries
    // to discover and select the preferred SRP server to register with.
    // If we currently have a selected server, we ensure that it is
    // still present in the Network Data and is still the preferred one.

    VerifyOrExit(mAutoStart.GetState() != AutoStart::kDisabled);

    // If SRP client is running, we check to make sure that auto-start
    // did select the current server, and server was not specified by
    // user directly.

    if (IsRunning())
    {
        VerifyOrExit(mAutoStart.GetState() != AutoStart::kSelectedNone);
    }

    // There are three types of entries in Network Data:
    //
    // 1) Preferred unicast entries with address included in service data.
    // 2) Anycast entries (each having a seq number).
    // 3) Unicast entries with address info included in server data.

    serverSockAddr.Clear();

    if (SelectUnicastEntry(DnsSrpUnicast::kFromServiceData, unicastInfo) == kErrorNone)
    {
        mAutoStart.SetState(AutoStart::kSelectedUnicastPreferred);
        serverSockAddr = unicastInfo.mSockAddr;
    }
    else if (Get<NetworkData::Service::Manager>().FindPreferredDnsSrpAnycastInfo(anycastInfo) == kErrorNone)
    {
        serverSockAddr.SetAddress(anycastInfo.mAnycastAddress);
        serverSockAddr.SetPort(kAnycastServerPort);

        // We check if we are selecting an anycast entry for first
        // time, or if the seq number has changed. Even if the
        // anycast address remains the same as before, on a seq
        // number change, the client still needs to restart to
        // re-register its info.

        if ((mAutoStart.GetState() != AutoStart::kSelectedAnycast) ||
            (mAutoStart.GetAnycastSeqNum() != anycastInfo.mSequenceNumber))
        {
            shouldRestart = true;
            mAutoStart.SetAnycastSeqNum(anycastInfo.mSequenceNumber);
        }

        mAutoStart.SetState(AutoStart::kSelectedAnycast);
    }
    else if (SelectUnicastEntry(DnsSrpUnicast::kFromServerData, unicastInfo) == kErrorNone)
    {
        mAutoStart.SetState(AutoStart::kSelectedUnicast);
        serverSockAddr = unicastInfo.mSockAddr;
    }

    if (IsRunning())
    {
        VerifyOrExit((GetServerAddress() != serverSockAddr) || shouldRestart);
        Stop(kRequesterAuto, kResetRetryInterval);
    }

    if (!serverSockAddr.GetAddress().IsUnspecified())
    {
        IgnoreError(Start(serverSockAddr, kRequesterAuto));
    }
    else
    {
        mAutoStart.SetState(AutoStart::kSelectedNone);
    }

exit:
    return;
}

Error Client::SelectUnicastEntry(DnsSrpUnicast::Origin aOrigin, DnsSrpUnicast::Info &aInfo) const
{
    Error                                   error = kErrorNotFound;
    DnsSrpUnicast::Info                     unicastInfo;
    NetworkData::Service::Manager::Iterator iterator;
#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
    Settings::SrpClientInfo savedInfo;
    bool                    hasSavedServerInfo = false;

    if (!IsRunning())
    {
        hasSavedServerInfo = (Get<Settings>().Read(savedInfo) == kErrorNone);
    }
#endif

    while (Get<NetworkData::Service::Manager>().GetNextDnsSrpUnicastInfo(iterator, unicastInfo) == kErrorNone)
    {
        if (unicastInfo.mOrigin != aOrigin)
        {
            continue;
        }

        if (mAutoStart.HasSelectedServer() && (GetServerAddress() == unicastInfo.mSockAddr))
        {
            aInfo = unicastInfo;
            error = kErrorNone;
            ExitNow();
        }

#if OPENTHREAD_CONFIG_SRP_CLIENT_SAVE_SELECTED_SERVER_ENABLE
        if (hasSavedServerInfo && (unicastInfo.mSockAddr.GetAddress() == savedInfo.GetServerAddress()) &&
            (unicastInfo.mSockAddr.GetPort() == savedInfo.GetServerPort()))
        {
            // Stop the search if we see a match for the previously
            // saved server info in the network data entries.

            aInfo = unicastInfo;
            error = kErrorNone;
            ExitNow();
        }
#endif

        // Prefer the numerically lowest server address

        if ((error == kErrorNotFound) || (unicastInfo.mSockAddr.GetAddress() < aInfo.mSockAddr.GetAddress()))
        {
            aInfo = unicastInfo;
            error = kErrorNone;
        }
    }

exit:
    return error;
}

#if OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE
void Client::SelectNextServer(bool aDisallowSwitchOnRegisteredHost)
{
    // This method tries to find the next unicast server info entry in the
    // Network Data after the current one selected. If found, it
    // restarts the client with the new server (keeping the retry wait
    // interval as before).

    Ip6::SockAddr         serverSockAddr;
    bool                  selectNext = false;
    DnsSrpUnicast::Origin origin     = DnsSrpUnicast::kFromServiceData;

    serverSockAddr.Clear();

    // Ensure that client is running, auto-start is enabled and
    // auto-start selected the server and it is a unicast entry.

    VerifyOrExit(IsRunning());

    switch (mAutoStart.GetState())
    {
    case AutoStart::kSelectedUnicastPreferred:
        origin = DnsSrpUnicast::kFromServiceData;
        break;

    case AutoStart::kSelectedUnicast:
        origin = DnsSrpUnicast::kFromServerData;
        break;

    case AutoStart::kSelectedAnycast:
    case AutoStart::kDisabled:
    case AutoStart::kSelectedNone:
        ExitNow();
    }

    if (aDisallowSwitchOnRegisteredHost)
    {
        // Ensure that host info is not yet registered (indicating that no
        // service has yet been registered either).
        VerifyOrExit((mHostInfo.GetState() == kAdding) || (mHostInfo.GetState() == kToAdd));
    }

    // We go through all entries to find the one matching the currently
    // selected one, then set `selectNext` to `true` so to select the
    // next one.

    do
    {
        DnsSrpUnicast::Info                     unicastInfo;
        NetworkData::Service::Manager::Iterator iterator;

        while (Get<NetworkData::Service::Manager>().GetNextDnsSrpUnicastInfo(iterator, unicastInfo) == kErrorNone)
        {
            if (unicastInfo.mOrigin != origin)
            {
                continue;
            }

            if (selectNext)
            {
                serverSockAddr = unicastInfo.mSockAddr;
                ExitNow();
            }

            if (GetServerAddress() == unicastInfo.mSockAddr)
            {
                selectNext = true;
            }
        }

        // We loop back to handle the case where the current entry
        // is the last one.

    } while (selectNext);

    // If we reach here it indicates we could not find the entry
    // associated with currently selected server in the list. This
    // situation is rather unlikely but can still happen if Network
    // Data happens to be changed and the entry removed but
    // the "changed" event from `Notifier` may have not yet been
    // processed (note that events are emitted from their own
    // tasklet). In such a case we keep `serverSockAddr` as empty.

exit:
    if (!serverSockAddr.GetAddress().IsUnspecified() && (GetServerAddress() != serverSockAddr))
    {
        // We specifically update `mHostInfo` to `kToAdd` state. This
        // ensures that `Stop()` will keep it as kToAdd` and we detect
        // that the host info has not been registered yet and allow the
        // `SelectNextServer()` to happen again if the timeouts/failures
        // continue to happen with the new server.

        mHostInfo.SetState(kToAdd);
        Stop(kRequesterAuto, kKeepRetryInterval);
        IgnoreError(Start(serverSockAddr, kRequesterAuto));
    }
}
#endif // OPENTHREAD_CONFIG_SRP_CLIENT_SWITCH_SERVER_ON_FAILURE

#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

const char *Client::ItemStateToString(ItemState aState)
{
    static const char *const kItemStateStrings[] = {
        "ToAdd",      // kToAdd      (0)
        "Adding",     // kAdding     (1)
        "ToRefresh",  // kToRefresh  (2)
        "Refreshing", // kRefreshing (3)
        "ToRemove",   // kToRemove   (4)
        "Removing",   // kRemoving   (5)
        "Registered", // kRegistered (6)
        "Removed",    // kRemoved    (7)
    };

    return kItemStateStrings[aState];
}

#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)

const char *Client::StateToString(State aState)
{
    static const char *const kStateStrings[] = {
        "Stopped",  // kStateStopped  (0)
        "Paused",   // kStatePaused   (1)
        "ToUpdate", // kStateToUpdate (2)
        "Updating", // kStateUpdating (3)
        "Updated",  // kStateUpdated  (4)
        "ToRetry",  // kStateToRetry  (5)
    };

    static_assert(kStateStopped == 0, "kStateStopped value is not correct");
    static_assert(kStatePaused == 1, "kStatePaused value is not correct");
    static_assert(kStateToUpdate == 2, "kStateToUpdate value is not correct");
    static_assert(kStateUpdating == 3, "kStateUpdating value is not correct");
    static_assert(kStateUpdated == 4, "kStateUpdated value is not correct");
    static_assert(kStateToRetry == 5, "kStateToRetry value is not correct");

    return kStateStrings[aState];
}

void Client::LogRetryWaitInterval(void) const
{
    constexpr uint16_t kLogInMsecLimit = 5000; // Max interval (in msec) to log the value in msec unit

    uint32_t interval = GetRetryWaitInterval();

    LogInfo("Retry interval %u %s", (interval < kLogInMsecLimit) ? interval : Time::MsecToSec(interval),
            (interval < kLogInMsecLimit) ? "ms" : "sec");
}

#endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)

} // namespace Srp
} // namespace ot

#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
