/*
 *  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.
 */

/**
 * @file
 *   This file implements a simple CLI for the SRP Client.
 */

#include "cli_srp_client.hpp"

#if OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE

#include <string.h>

#include "cli/cli.hpp"

namespace ot {
namespace Cli {

static otError CopyString(char *aDest, uint16_t aDestSize, const char *aSource)
{
    // Copies a string from `aSource` to `aDestination` (char array),
    // verifying that the string fits in the destination array.

    otError error = OT_ERROR_NONE;
    size_t  len   = strlen(aSource);

    VerifyOrExit(len + 1 <= aDestSize, error = OT_ERROR_INVALID_ARGS);
    memcpy(aDest, aSource, len + 1);

exit:
    return error;
}

SrpClient::SrpClient(otInstance *aInstance, OutputImplementer &aOutputImplementer)
    : Utils(aInstance, aOutputImplementer)
    , mCallbackEnabled(false)
{
    otSrpClientSetCallback(GetInstancePtr(), SrpClient::HandleCallback, this);
}

#if OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

template <> otError SrpClient::Process<Cmd("autostart")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;
    bool    enable;

    /**
     * @cli srp client autostart (get)
     * @code
     * srp client autostart
     * Disabled
     * Done
     * @endcode
     * @par
     * Indicates the current state of auto-start mode (enabled or disabled).
     * @moreinfo{@srp}.
     * @sa otSrpClientIsAutoStartModeEnabled
     */
    if (aArgs[0].IsEmpty())
    {
        OutputEnabledDisabledStatus(otSrpClientIsAutoStartModeEnabled(GetInstancePtr()));
        ExitNow();
    }

    SuccessOrExit(error = ParseEnableOrDisable(aArgs[0], enable));

    /**
     * @cli srp client autostart enable
     * @code
     * srp client autostart enable
     * Done
     * @endcode
     * @par
     * Enables auto-start mode.
     * @par
     * When auto-start is enabled, the SRP client monitors Thread
     * network data to discover SRP servers, to select the preferred
     * server, and to automatically start and stop the client when
     * an SRP server is detected.
     * @par
     * Three categories of network data entries indicate the presence of an SRP sever,
     * and are preferred in the following order:
     *  -# Unicast entries in which the server address is included in the service
     *  data. If there are multiple options, the option with the lowest numerical
     *  IPv6 address is preferred.
     *  -# Anycast entries that each have a sequence number. The largest sequence
     *  number as specified by Serial Number Arithmetic Logic
     *  in RFC-1982 is preferred.
     *  -# Unicast entries in which the server address information is included
     *  with the server data. If there are multiple options, the option with the
     *  lowest numerical IPv6 address is preferred.
     * @sa otSrpClientEnableAutoStartMode
     */
    if (enable)
    {
        otSrpClientEnableAutoStartMode(GetInstancePtr(), /* aCallback */ nullptr, /* aContext */ nullptr);
    }
    /**
     * @cli srp client autostart disable
     * @code
     * srp client autostart disable
     * Done
     * @endcode
     * @par
     * Disables the auto-start mode.
     * @par
     * Disabling auto-start mode does not stop a running client.
     * However, the SRP client stops monitoring Thread network data.
     * @sa otSrpClientDisableAutoStartMode
     */
    else
    {
        otSrpClientDisableAutoStartMode(GetInstancePtr());
    }

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_SRP_CLIENT_AUTO_START_API_ENABLE

/**
 * @cli srp client callback (get,enable,disable)
 * @code
 * srp client callback enable
 * Done
 * @endcode
 * @code
 * srp client callback
 * Enabled
 * Done
 * @endcode
 * @cparam srp client callback [@ca{enable}|@ca{disable}]
 * @par
 * Gets or enables/disables printing callback events from the SRP client.
 * @moreinfo{@srp}.
 * @sa otSrpClientSetCallback
 */
template <> otError SrpClient::Process<Cmd("callback")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    if (aArgs[0].IsEmpty())
    {
        OutputEnabledDisabledStatus(mCallbackEnabled);
        ExitNow();
    }

    error = ParseEnableOrDisable(aArgs[0], mCallbackEnabled);

exit:
    return error;
}

template <> otError SrpClient::Process<Cmd("host")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    /**
     * @cli srp client host
     * @code
     * srp client host
     * name:"dev4312", state:Registered, addrs:[fd00:0:0:0:0:0:0:1234, fd00:0:0:0:0:0:0:beef]
     * Done
     * @endcode
     * @par api_copy
     * #otSrpClientGetHostInfo
     */
    if (aArgs[0].IsEmpty())
    {
        OutputHostInfo(0, *otSrpClientGetHostInfo(GetInstancePtr()));
    }
    /**
     * @cli srp client host name (get,set)
     * @code
     * srp client host name dev4312
     * Done
     * @endcode
     * @code
     * srp client host name
     * dev4312
     * Done
     * @endcode
     * @cparam srp client host name [@ca{name}]
     * To set the client host name when the host has either been removed or not yet
     * registered with the server, use the `name` parameter.
     * @par
     * Gets or sets the host name of the SRP client. @moreinfo{@srp}.
     * @sa otSrpClientSetHostName
     */
    else if (aArgs[0] == "name")
    {
        if (aArgs[1].IsEmpty())
        {
            const char *name = otSrpClientGetHostInfo(GetInstancePtr())->mName;
            OutputLine("%s", (name != nullptr) ? name : "(null)");
        }
        else
        {
            uint16_t len;
            uint16_t size;
            char    *hostName;

            VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
            hostName = otSrpClientBuffersGetHostNameString(GetInstancePtr(), &size);

            len = aArgs[1].GetLength();
            VerifyOrExit(len + 1 <= size, error = OT_ERROR_INVALID_ARGS);

            // We first make sure we can set the name, and if so
            // we copy it to the persisted string buffer and set
            // the host name again now with the persisted buffer.
            // This ensures that we do not overwrite a previous
            // buffer with a host name that cannot be set.

            SuccessOrExit(error = otSrpClientSetHostName(GetInstancePtr(), aArgs[1].GetCString()));
            memcpy(hostName, aArgs[1].GetCString(), len + 1);

            IgnoreError(otSrpClientSetHostName(GetInstancePtr(), hostName));
        }
    }
    /**
     * @cli srp client host state
     * @code
     * srp client host state
     * Registered
     * Done
     * @endcode
     * @par
     * Returns the state of the SRP client host. Possible states:
     *   * `ToAdd`: Item to be added/registered.
     *	 * `Adding`: Item is being added/registered.
     *   * `ToRefresh`: Item to be refreshed for lease renewal.
     *   * `Refreshing`: Item is beig refreshed.
     *   * `ToRemove`: Item to be removed.
     *   * `Removing`: Item is being removed.
     *   * `Registered`: Item is registered with server.
     *   * `Removed`: Item has been removed.
     */
    else if (aArgs[0] == "state")
    {
        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
        OutputLine("%s", otSrpClientItemStateToString(otSrpClientGetHostInfo(GetInstancePtr())->mState));
    }
    else if (aArgs[0] == "address")
    {
        /**
         * @cli srp client host address (get)
         * @code
         * srp client host address
         * auto
         * Done
         * @endcode
         * @code
         * srp client host address
         * fd00:0:0:0:0:0:0:1234
         * fd00:0:0:0:0:0:0:beef
         * Done
         * @endcode
         * @par
         * Indicates whether auto address mode is enabled. If auto address mode is not
         * enabled, then the list of SRP client host addresses is returned.
         * @moreinfo{@srp}.
         * @sa otSrpClientGetHostInfo
         */
        if (aArgs[1].IsEmpty())
        {
            const otSrpClientHostInfo *hostInfo = otSrpClientGetHostInfo(GetInstancePtr());

            if (hostInfo->mAutoAddress)
            {
                OutputLine("auto");
            }
            else
            {
                for (uint8_t index = 0; index < hostInfo->mNumAddresses; index++)
                {
                    OutputIp6AddressLine(hostInfo->mAddresses[index]);
                }
            }
        }
        /**
         * @cli srp client host address (set)
         * @code
         * srp client host address auto
         * Done
         * @endcode
         * @code
         * srp client host address fd00::cafe
         * Done
         * @endcode
         * @cparam srp client host address [auto|@ca{address...}]
         *   * Use the `auto` parameter to enable auto host address mode.
         *     When enabled, the client automatically uses all preferred Thread
         *     `netif` unicast addresses except for link-local and mesh-local
         *     addresses. If there is no valid address, the mesh local
         *     EID address gets added. The SRP client automatically
         *     re-registers if addresses on the Thread `netif` are
         *     added or removed or marked as non-preferred.
         *   * Explicitly specify the list of host addresses, separating
         *     each address by a space. You can set this list while the client is
         *     running. This will also disable auto host address mode.
         * @par
         * Enable auto host address mode or explicitly set the list of host
         * addresses. @moreinfo{@srp}.
         * @sa otSrpClientEnableAutoHostAddress
         * @sa otSrpClientSetHostAddresses
         */
        else if (aArgs[1] == "auto")
        {
            error = otSrpClientEnableAutoHostAddress(GetInstancePtr());
        }
        else
        {
            uint8_t       numAddresses = 0;
            otIp6Address  addresses[kMaxHostAddresses];
            uint8_t       arrayLength;
            otIp6Address *hostAddressArray;

            hostAddressArray = otSrpClientBuffersGetHostAddressesArray(GetInstancePtr(), &arrayLength);

            // We first make sure we can set the addresses, and if so
            // we copy the address list into the persisted address array
            // and set it again. This ensures that we do not overwrite
            // a previous list before we know it is safe to set/change
            // the address list.

            if (arrayLength > kMaxHostAddresses)
            {
                arrayLength = kMaxHostAddresses;
            }

            for (Arg *arg = &aArgs[1]; !arg->IsEmpty(); arg++)
            {
                VerifyOrExit(numAddresses < arrayLength, error = OT_ERROR_NO_BUFS);
                SuccessOrExit(error = arg->ParseAsIp6Address(addresses[numAddresses]));
                numAddresses++;
            }

            SuccessOrExit(error = otSrpClientSetHostAddresses(GetInstancePtr(), addresses, numAddresses));

            memcpy(hostAddressArray, addresses, numAddresses * sizeof(hostAddressArray[0]));
            IgnoreError(otSrpClientSetHostAddresses(GetInstancePtr(), hostAddressArray, numAddresses));
        }
    }
    /**
     * @cli srp client host remove
     * @code
     * srp client host remove 1
     * Done
     * @endcode
     * @cparam srp client host remove [@ca{removekeylease}] [@ca{sendunregtoserver}]
     *   * The parameter `removekeylease` is an optional boolean value that indicates
     *     whether the host key lease should also be removed (default is `false`).
     *   * The parameter `sendunregtoserver` is an optional boolean value that indicates
     *     whether the client host should send an "update" message to the server
     *     even when the client host information has not yet been registered with the
     *     server (default is `false`). This parameter can be specified only if the
     *     `removekeylease` parameter is specified first in the command.
     * @par
     * Removes SRP client host information and all services from the SRP server.
     * @moreinfo{@srp}.
     * @sa otSrpClientRemoveHostAndServices
     * @sa otSrpClientSetHostName
     */
    else if (aArgs[0] == "remove")
    {
        bool removeKeyLease    = false;
        bool sendUnregToServer = false;

        if (!aArgs[1].IsEmpty())
        {
            SuccessOrExit(error = aArgs[1].ParseAsBool(removeKeyLease));

            if (!aArgs[2].IsEmpty())
            {
                SuccessOrExit(error = aArgs[2].ParseAsBool(sendUnregToServer));
                VerifyOrExit(aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
            }
        }

        error = otSrpClientRemoveHostAndServices(GetInstancePtr(), removeKeyLease, sendUnregToServer);
    }
    /**
     * @cli srp client host clear
     * @code
     * srp client host clear
     * Done
     * @endcode
     * @par
     * Clears all host information and all services.
     * @sa otSrpClientBuffersFreeAllServices
     * @sa otSrpClientClearHostAndServices
     */
    else if (aArgs[0] == "clear")
    {
        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
        otSrpClientClearHostAndServices(GetInstancePtr());
        otSrpClientBuffersFreeAllServices(GetInstancePtr());
    }
    else
    {
        error = OT_ERROR_INVALID_COMMAND;
    }

exit:
    return error;
}

/**
 * @cli srp client leaseinterval (get,set)
 * @code
 * srp client leaseinterval 3600
 * Done
 * @endcode
 * @code
 * srp client leaseinterval
 * 3600
 * Done
 * @endcode
 * @cparam srp client leaseinterval [@ca{interval}]
 * @par
 * Gets or sets the lease interval in seconds.
 * @sa otSrpClientGetLeaseInterval
 * @sa otSrpClientSetLeaseInterval
 */
template <> otError SrpClient::Process<Cmd("leaseinterval")>(Arg aArgs[])
{
    return ProcessGetSet(aArgs, otSrpClientGetLeaseInterval, otSrpClientSetLeaseInterval);
}

/**
 * @cli srp client keyleaseinterval (get,set)
 * @code
 * srp client keyleaseinterval 864000
 * Done
 * @endcode
 * @code
 * srp client keyleaseinterval
 * 864000
 * Done
 * @endcode
 * @cparam srp client keyleaseinterval [@ca{interval}]
 * @par
 * Gets or sets the key lease interval in seconds.
 * @sa otSrpClientGetKeyLeaseInterval
 * @sa otSrpClientSetKeyLeaseInterval
 */
template <> otError SrpClient::Process<Cmd("keyleaseinterval")>(Arg aArgs[])
{
    return ProcessGetSet(aArgs, otSrpClientGetKeyLeaseInterval, otSrpClientSetKeyLeaseInterval);
}

template <> otError SrpClient::Process<Cmd("server")>(Arg aArgs[])
{
    otError           error          = OT_ERROR_NONE;
    const otSockAddr *serverSockAddr = otSrpClientGetServerAddress(GetInstancePtr());

    /**
     * @cli srp client server
     * @code
     * srp client server
     * &lsqb;fd00:0:0:0:d88a:618b:384d:e760&rsqb;:4724
     * Done
     * @endcode
     * @par
     * Gets the socket address (IPv6 address and port number) of the SRP server
     * that is being used by the SRP client. If the client is not running, the address
     * is unspecified (all zeros) with a port number of 0. @moreinfo{@srp}.
     * @sa otSrpClientGetServerAddress
     */
    if (aArgs[0].IsEmpty())
    {
        OutputSockAddrLine(*serverSockAddr);
        ExitNow();
    }

    VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    /**
     * @cli srp client server address
     * @code
     * srp client server address
     * fd00:0:0:0:d88a:618b:384d:e760
     * Done
     * @endcode
     * @par
     * Returns the server's IPv6 address.
     */
    if (aArgs[0] == "address")
    {
        OutputIp6AddressLine(serverSockAddr->mAddress);
    }
    /**
     * @cli srp client server port
     * @code
     * srp client server port
     * 4724
     * Done
     * @endcode
     * @par
     * Returns the server's port number.
     */
    else if (aArgs[0] == "port")
    {
        OutputLine("%u", serverSockAddr->mPort);
    }
    else
    {
        error = OT_ERROR_INVALID_COMMAND;
    }

exit:
    return error;
}

template <> otError SrpClient::Process<Cmd("service")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;
    bool    isRemove;

    /**
     * @cli srp client service
     * @code
     * srp client service
     * instance:"ins2", name:"_test2._udp,_sub1,_sub2", state:Registered, port:111, priority:1, weight:1
     * instance:"ins1", name:"_test1._udp", state:Registered, port:777, priority:0, weight:0
     * Done
     * @endcode
     * @par api_copy
     * #otSrpClientGetServices
     */
    if (aArgs[0].IsEmpty())
    {
        OutputServiceList(0, otSrpClientGetServices(GetInstancePtr()));
    }
    /**
     * @cli srp client service add
     * @code
     * srp client service add ins1 _test1._udp 777
     * Done
     * @endcode
     * @code
     * srp client service add ins2 _test2._udp,_sub1,_sub2 111 1 1
     * Done
     * @endcode
     * @cparam srp client service add @ca{instancename} @ca{servicename} <!--
     * * -->                          @ca{port} [@ca{priority}] <!--
     * * -->                          [@ca{weight}] [@ca{txt}]
     * The `servicename` parameter can optionally include a list of service subtype labels that are
     * separated by commas. The examples here use generic naming. The `priority` and `weight` (both are `uint16_t`
     * values) parameters are optional, and if not provided zero is used. The optional `txt` parameter sets the TXT
     * data associated with the service. The `txt` value must be in hex-string format and is treated as an already
     * encoded TXT data byte sequence.
     * @par
     * Adds a service with a given instance name, service name, and port number.
     * @moreinfo{@srp}.
     * @sa otSrpClientAddService
     */
    else if (aArgs[0] == "add")
    {
        error = ProcessServiceAdd(aArgs);
    }
    /**
     * @cli srp client service remove
     * @code
     * srp client service remove ins2 _test2._udp
     * Done
     * @endcode
     * @cparam srp client service remove @ca{instancename} @ca{servicename}
     * @par
     * Requests a service to be unregistered with the SRP server.
     * @sa otSrpClientRemoveService
     */
    /**
     * @cli srp client service name clear
     * @code
     * srp client service clear ins2 _test2._udp
     * Done
     * @endcode
     * @cparam srp client service clear @ca{instancename} @ca{servicename}
     * @par
     * Clears a service, immediately removing it from the client service list,
     * with no interaction with the SRP server.
     * @sa otSrpClientClearService
     */
    else if ((isRemove = (aArgs[0] == "remove")) || (aArgs[0] == "clear"))
    {
        // `remove`|`clear` <instance-name> <service-name>

        const otSrpClientService *service;

        VerifyOrExit(!aArgs[2].IsEmpty() && aArgs[3].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

        for (service = otSrpClientGetServices(GetInstancePtr()); service != nullptr; service = service->mNext)
        {
            if ((aArgs[1] == service->mInstanceName) && (aArgs[2] == service->mName))
            {
                break;
            }
        }

        VerifyOrExit(service != nullptr, error = OT_ERROR_NOT_FOUND);

        if (isRemove)
        {
            error = otSrpClientRemoveService(GetInstancePtr(), const_cast<otSrpClientService *>(service));
        }
        else
        {
            SuccessOrExit(error = otSrpClientClearService(GetInstancePtr(), const_cast<otSrpClientService *>(service)));

            otSrpClientBuffersFreeService(GetInstancePtr(), reinterpret_cast<otSrpClientBuffersServiceEntry *>(
                                                                const_cast<otSrpClientService *>(service)));
        }
    }
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    /**
     * @cli srp client service key (get,set)
     * @code
     * srp client service key enable
     * Done
     * @endcode
     * @code
     * srp client service key
     * Enabled
     * Done
     * @endcode
     * @par
     * Gets or sets the service key record inclusion mode in the SRP client.
     * This command is intended for testing only, and requires that
     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` be enabled. @moreinfo{@srp}.
     * @sa otSrpClientIsServiceKeyRecordEnabled
     */
    else if (aArgs[0] == "key")
    {
        // `key [enable/disable]`

        error = ProcessEnableDisable(aArgs + 1, otSrpClientIsServiceKeyRecordEnabled,
                                     otSrpClientSetServiceKeyRecordEnabled);
    }
#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
    else
    {
        error = OT_ERROR_INVALID_COMMAND;
    }

exit:
    return error;
}

otError SrpClient::ProcessServiceAdd(Arg aArgs[])
{
    // `add` <instance-name> <service-name> <port> [priority] [weight] [txt] [lease] [key-lease]

    otSrpClientBuffersServiceEntry *entry = nullptr;
    uint16_t                        size;
    char                           *string;
    otError                         error;
    char                           *label;

    entry = otSrpClientBuffersAllocateService(GetInstancePtr());

    VerifyOrExit(entry != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = aArgs[3].ParseAsUint16(entry->mService.mPort));

    // Successfully parsing aArgs[3] indicates that aArgs[1] and
    // aArgs[2] are also non-empty.

    string = otSrpClientBuffersGetServiceEntryInstanceNameString(entry, &size);
    SuccessOrExit(error = CopyString(string, size, aArgs[1].GetCString()));

    string = otSrpClientBuffersGetServiceEntryServiceNameString(entry, &size);
    SuccessOrExit(error = CopyString(string, size, aArgs[2].GetCString()));

    // Service subtypes are added as part of service name as a comma separated list
    // e.g., "_service._udp,_sub1,_sub2"

    label = strchr(string, ',');

    if (label != nullptr)
    {
        uint16_t     arrayLength;
        const char **subTypeLabels = otSrpClientBuffersGetSubTypeLabelsArray(entry, &arrayLength);

        // Leave the last array element as `nullptr` to indicate end of array.
        for (uint16_t index = 0; index + 1 < arrayLength; index++)
        {
            *label++             = '\0';
            subTypeLabels[index] = label;

            label = strchr(label, ',');

            if (label == nullptr)
            {
                break;
            }
        }

        VerifyOrExit(label == nullptr, error = OT_ERROR_NO_BUFS);
    }

    SuccessOrExit(error = aArgs[3].ParseAsUint16(entry->mService.mPort));

    if (!aArgs[4].IsEmpty())
    {
        SuccessOrExit(error = aArgs[4].ParseAsUint16(entry->mService.mPriority));
    }

    if (!aArgs[5].IsEmpty())
    {
        SuccessOrExit(error = aArgs[5].ParseAsUint16(entry->mService.mWeight));
    }

    if (!aArgs[6].IsEmpty() && (aArgs[6] != "-"))
    {
        uint8_t *txtBuffer;

        txtBuffer                     = otSrpClientBuffersGetServiceEntryTxtBuffer(entry, &size);
        entry->mTxtEntry.mValueLength = size;

        SuccessOrExit(error = aArgs[6].ParseAsHexString(entry->mTxtEntry.mValueLength, txtBuffer));
    }
    else
    {
        entry->mService.mNumTxtEntries = 0;
    }

    if (!aArgs[7].IsEmpty())
    {
        SuccessOrExit(error = aArgs[7].ParseAsUint32(entry->mService.mLease));
    }

    if (!aArgs[8].IsEmpty())
    {
        SuccessOrExit(error = aArgs[8].ParseAsUint32(entry->mService.mKeyLease));
        VerifyOrExit(aArgs[9].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    }

    SuccessOrExit(error = otSrpClientAddService(GetInstancePtr(), &entry->mService));

    entry = nullptr;

exit:
    if (entry != nullptr)
    {
        otSrpClientBuffersFreeService(GetInstancePtr(), entry);
    }

    return error;
}

void SrpClient::OutputHostInfo(uint8_t aIndentSize, const otSrpClientHostInfo &aHostInfo)
{
    OutputFormat(aIndentSize, "name:");

    if (aHostInfo.mName != nullptr)
    {
        OutputFormat("\"%s\"", aHostInfo.mName);
    }
    else
    {
        OutputFormat("(null)");
    }

    OutputFormat(", state:%s, addrs:", otSrpClientItemStateToString(aHostInfo.mState));

    if (aHostInfo.mAutoAddress)
    {
        OutputLine("auto");
    }
    else
    {
        OutputFormat("[");

        for (uint8_t index = 0; index < aHostInfo.mNumAddresses; index++)
        {
            if (index > 0)
            {
                OutputFormat(", ");
            }

            OutputIp6Address(aHostInfo.mAddresses[index]);
        }

        OutputLine("]");
    }
}

void SrpClient::OutputServiceList(uint8_t aIndentSize, const otSrpClientService *aServices)
{
    while (aServices != nullptr)
    {
        OutputService(aIndentSize, *aServices);
        aServices = aServices->mNext;
    }
}

void SrpClient::OutputService(uint8_t aIndentSize, const otSrpClientService &aService)
{
    OutputFormat(aIndentSize, "instance:\"%s\", name:\"%s", aService.mInstanceName, aService.mName);

    if (aService.mSubTypeLabels != nullptr)
    {
        for (uint16_t index = 0; aService.mSubTypeLabels[index] != nullptr; index++)
        {
            OutputFormat(",%s", aService.mSubTypeLabels[index]);
        }
    }

    OutputLine("\", state:%s, port:%d, priority:%d, weight:%d", otSrpClientItemStateToString(aService.mState),
               aService.mPort, aService.mPriority, aService.mWeight);
}

/**
 * @cli srp client start
 * @code
 * srp client start fd00::d88a:618b:384d:e760 4724
 * Done
 * @endcode
 * @cparam srp client start @ca{serveraddr} @ca{serverport}
 * @par
 * Starts the SRP client operation. @moreinfo{@srp}.
 * @sa otSrpClientStart
 */
template <> otError SrpClient::Process<Cmd("start")>(Arg aArgs[])
{
    otError    error = OT_ERROR_NONE;
    otSockAddr serverSockAddr;

    SuccessOrExit(error = aArgs[0].ParseAsIp6Address(serverSockAddr.mAddress));
    SuccessOrExit(error = aArgs[1].ParseAsUint16(serverSockAddr.mPort));
    VerifyOrExit(aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    error = otSrpClientStart(GetInstancePtr(), &serverSockAddr);

exit:
    return error;
}

/**
 * @cli srp client state
 * @code
 * srp client state
 * Enabled
 * Done
 * @endcode
 * @par api_copy
 * #otSrpClientIsRunning
 * @moreinfo{@srp}.
 */
template <> otError SrpClient::Process<Cmd("state")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    OutputEnabledDisabledStatus(otSrpClientIsRunning(GetInstancePtr()));

exit:
    return error;
}

/**
 * @cli srp client stop
 * @code
 * srp client stop
 * Done
 * @endcode
 * @par api_copy
 * #otSrpClientStop
 */
template <> otError SrpClient::Process<Cmd("stop")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    VerifyOrExit(aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    otSrpClientStop(GetInstancePtr());

exit:
    return error;
}

/**
 * @cli srp client ttl (get,set)
 * @code
 * srp client ttl 3600
 * Done
 * @endcode
 * @code
 * srp client ttl
 * 3600
 * Done
 * @endcode
 * @cparam srp client ttl [@ca{value}]
 * @par
 * Gets or sets the `ttl`(time to live) value in seconds.
 * @sa otSrpClientGetTtl
 * @sa otSrpClientSetTtl
 */
template <> otError SrpClient::Process<Cmd("ttl")>(Arg aArgs[])
{
    return ProcessGetSet(aArgs, otSrpClientGetTtl, otSrpClientSetTtl);
}

void SrpClient::HandleCallback(otError                    aError,
                               const otSrpClientHostInfo *aHostInfo,
                               const otSrpClientService  *aServices,
                               const otSrpClientService  *aRemovedServices,
                               void                      *aContext)
{
    static_cast<SrpClient *>(aContext)->HandleCallback(aError, aHostInfo, aServices, aRemovedServices);
}

void SrpClient::HandleCallback(otError                    aError,
                               const otSrpClientHostInfo *aHostInfo,
                               const otSrpClientService  *aServices,
                               const otSrpClientService  *aRemovedServices)
{
    otSrpClientService *next;

    if (mCallbackEnabled)
    {
        OutputLine("SRP client callback - error:%s", otThreadErrorToString(aError));
        OutputLine("Host info:");
        OutputHostInfo(kIndentSize, *aHostInfo);

        OutputLine("Service list:");
        OutputServiceList(kIndentSize, aServices);

        if (aRemovedServices != nullptr)
        {
            OutputLine("Removed service list:");
            OutputServiceList(kIndentSize, aRemovedServices);
        }
    }

    // Go through removed services and free all removed services

    for (const otSrpClientService *service = aRemovedServices; service != nullptr; service = next)
    {
        next = service->mNext;

        otSrpClientBuffersFreeService(GetInstancePtr(), reinterpret_cast<otSrpClientBuffersServiceEntry *>(
                                                            const_cast<otSrpClientService *>(service)));
    }
}

otError SrpClient::Process(Arg aArgs[])
{
#define CmdEntry(aCommandString)                                 \
    {                                                            \
        aCommandString, &SrpClient::Process<Cmd(aCommandString)> \
    }

    static constexpr Command kCommands[] = {
        CmdEntry("autostart"),     CmdEntry("callback"), CmdEntry("host"),    CmdEntry("keyleaseinterval"),
        CmdEntry("leaseinterval"), CmdEntry("server"),   CmdEntry("service"), CmdEntry("start"),
        CmdEntry("state"),         CmdEntry("stop"),     CmdEntry("ttl"),
    };

    static_assert(BinarySearch::IsSorted(kCommands), "kCommands is not sorted");

    otError        error = OT_ERROR_INVALID_COMMAND;
    const Command *command;

    if (aArgs[0].IsEmpty() || (aArgs[0] == "help"))
    {
        OutputCommandTable(kCommands);
        ExitNow(error = aArgs[0].IsEmpty() ? error : OT_ERROR_NONE);
    }

    command = BinarySearch::Find(aArgs[0].GetCString(), kCommands);
    VerifyOrExit(command != nullptr);

    error = (this->*command->mHandler)(aArgs + 1);

exit:
    return error;
}

} // namespace Cli
} // namespace ot

#endif // OPENTHREAD_CONFIG_SRP_CLIENT_ENABLE
