/*
 *  Copyright (c) 2023, 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 CLI for DNS (client and server/resolver).
 */

#include "cli_dns.hpp"

#include "cli/cli.hpp"

#if OPENTHREAD_CLI_DNS_ENABLE

namespace ot {
namespace Cli {

#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE

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

    /**
     * @cli dns compression
     * @code
     * dns compression
     * Enabled
     * @endcode
     * @cparam dns compression [@ca{enable|disable}]
     * @par api_copy
     * #otDnsIsNameCompressionEnabled
     * @par
     * By default DNS name compression is enabled. When disabled,
     * DNS names are appended as full and never compressed. This
     * is applicable to OpenThread's DNS and SRP client/server
     * modules."
     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is required.
     */
    if (aArgs[0].IsEmpty())
    {
        OutputEnabledDisabledStatus(otDnsIsNameCompressionEnabled());
    }
    /**
     * @cli dns compression (enable,disable)
     * @code
     * dns compression enable
     * Enabled
     * @endcode
     * @code
     * dns compression disable
     * Done
     * dns compression
     * Disabled
     * Done
     * @endcode
     * @cparam dns compression [@ca{enable|disable}]
     * @par
     * Set the "DNS name compression" mode.
     * @par
     * By default DNS name compression is enabled. When disabled,
     * DNS names are appended as full and never compressed. This
     * is applicable to OpenThread's DNS and SRP client/server
     * modules."
     * `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE` is required.
     * @sa otDnsSetNameCompressionEnabled
     */
    else
    {
        bool enable;

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

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE

#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE

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

    /**
     * @cli dns config
     * @code
     * dns config
     * Server: [fd00:0:0:0:0:0:0:1]:1234
     * ResponseTimeout: 5000 ms
     * MaxTxAttempts: 2
     * RecursionDesired: no
     * ServiceMode: srv
     * Nat64Mode: allow
     * Done
     * @endcode
     * @par api_copy
     * #otDnsClientGetDefaultConfig
     * @par
     * The config includes the server IPv6 address and port, response
     * timeout in msec (wait time to rx response), maximum tx attempts
     * before reporting failure, boolean flag to indicate whether the server
     * can resolve the query recursively or not.
     * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
     */
    if (aArgs[0].IsEmpty())
    {
        const otDnsQueryConfig *defaultConfig = otDnsClientGetDefaultConfig(GetInstancePtr());

        OutputFormat("Server: ");
        OutputSockAddrLine(defaultConfig->mServerSockAddr);
        OutputLine("ResponseTimeout: %lu ms", ToUlong(defaultConfig->mResponseTimeout));
        OutputLine("MaxTxAttempts: %u", defaultConfig->mMaxTxAttempts);
        OutputLine("RecursionDesired: %s",
                   (defaultConfig->mRecursionFlag == OT_DNS_FLAG_RECURSION_DESIRED) ? "yes" : "no");
        OutputLine("ServiceMode: %s", DnsConfigServiceModeToString(defaultConfig->mServiceMode));
#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
        OutputLine("Nat64Mode: %s", (defaultConfig->mNat64Mode == OT_DNS_NAT64_ALLOW) ? "allow" : "disallow");
#endif
#if OPENTHREAD_CONFIG_DNS_CLIENT_OVER_TCP_ENABLE
        OutputLine("TransportProtocol: %s", (defaultConfig->mTransportProto == OT_DNS_TRANSPORT_UDP) ? "udp" : "tcp");
#endif
    }
    /**
     * @cli dns config (set)
     * @code
     * dns config fd00::1 1234 5000 2 0
     * Done
     * @endcode
     * @code
     * dns config
     * Server: [fd00:0:0:0:0:0:0:1]:1234
     * ResponseTimeout: 5000 ms
     * MaxTxAttempts: 2
     * RecursionDesired: no
     * ServiceMode: srv_txt_opt
     * Nat64Mode: allow
     * TransportProtocol: udp
     * Done
     * @endcode
     * @code
     * dns config fd00::2
     * Done
     * @endcode
     * @code
     * dns config
     * Server: [fd00:0:0:0:0:0:0:2]:53
     * ResponseTimeout: 6000 ms
     * MaxTxAttempts: 3
     * RecursionDesired: yes
     * ServiceMode: srv_txt_opt
     * Nat64Mode: allow
     * TransportProtocol: udp
     * Done
     * @endcode
     * @par api_copy
     * #otDnsClientSetDefaultConfig
     * @cparam dns config [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
     * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
     * -->                [@ca{recursion-desired-boolean}] [@ca{service-mode}] <!--
     * -->                [@ca{protocol}]
     * @par
     * We can leave some of the fields as unspecified (or use value zero). The
     * unspecified fields are replaced by the corresponding OT config option
     * definitions `OPENTHREAD_CONFIG_DNS_CLIENT_DEFAULT` to form the default
     * query config.
     * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
     */
    else
    {
        otDnsQueryConfig  queryConfig;
        otDnsQueryConfig *config = &queryConfig;

        SuccessOrExit(error = GetDnsConfig(aArgs, config));
        otDnsClientSetDefaultConfig(GetInstancePtr(), config);
    }

exit:
    return error;
}

/**
 * @cli dns resolve
 * @code
 * dns resolve ipv6.google.com
 * DNS response for ipv6.google.com - 2a00:1450:401b:801:0:0:0:200e TTL: 300
 * @endcode
 * @code
 * dns resolve example.com 8.8.8.8
 * Synthesized IPv6 DNS server address: fdde:ad00:beef:2:0:0:808:808
 * DNS response for example.com. - fd4c:9574:3720:2:0:0:5db8:d822 TTL:20456
 * Done
 * @endcode
 * @cparam dns resolve @ca{hostname} [@ca{dns-server-IP}] <!--
 * -->                 [@ca{dns-server-port}] [@ca{response-timeout-ms}] <!--
 * -->                 [@ca{max-tx-attempts}] [@ca{recursion-desired-boolean}]
 * @par api_copy
 * #otDnsClientResolveAddress
 * @par
 * Send DNS Query to obtain IPv6 address for given hostname.
 * @par
 * The parameters after hostname are optional. Any unspecified (or zero) value
 * for these optional parameters is replaced by the value from the current default
 * config (dns config).
 * @par
 * The DNS server IP can be an IPv4 address, which will be synthesized to an
 * IPv6 address using the preferred NAT64 prefix from the network data.
 * @par
 * Note: The command will return InvalidState when the DNS server IP is an IPv4
 * address but the preferred NAT64 prefix is unavailable.
 * `OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE` is required.
 */
template <> otError Dns::Process<Cmd("resolve")>(Arg aArgs[])
{
    otError           error = OT_ERROR_NONE;
    otDnsQueryConfig  queryConfig;
    otDnsQueryConfig *config = &queryConfig;

    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
    SuccessOrExit(error = otDnsClientResolveAddress(GetInstancePtr(), aArgs[0].GetCString(), &HandleDnsAddressResponse,
                                                    this, config));
    error = OT_ERROR_PENDING;

exit:
    return error;
}

#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
template <> otError Dns::Process<Cmd("resolve4")>(Arg aArgs[])
{
    otError           error = OT_ERROR_NONE;
    otDnsQueryConfig  queryConfig;
    otDnsQueryConfig *config = &queryConfig;

    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
    SuccessOrExit(error = otDnsClientResolveIp4Address(GetInstancePtr(), aArgs[0].GetCString(),
                                                       &HandleDnsAddressResponse, this, config));
    error = OT_ERROR_PENDING;

exit:
    return error;
}
#endif

#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE

/**
 * @cli dns browse
 * @code
 * dns browse _service._udp.example.com
 * DNS browse response for _service._udp.example.com.
 * inst1
 *     Port:1234, Priority:1, Weight:2, TTL:7200
 *     Host:host.example.com.
 *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
 *     TXT:[a=6531, b=6c12] TTL:7300
 * instance2
 *     Port:1234, Priority:1, Weight:2, TTL:7200
 *     Host:host.example.com.
 *     HostAddress:fd00:0:0:0:0:0:0:abcd TTL:7200
 *     TXT:[a=1234] TTL:7300
 * Done
 * @endcode
 * @code
 * dns browse _airplay._tcp.default.service.arpa
 * DNS browse response for _airplay._tcp.default.service.arpa.
 * Mac mini
 *     Port:7000, Priority:0, Weight:0, TTL:10
 *     Host:Mac-mini.default.service.arpa.
 *     HostAddress:fd97:739d:386a:1:1c2e:d83c:fcbe:9cf4 TTL:10
 * Done
 * @endcode
 * @cparam dns browse @ca{service-name} [@ca{dns-server-IP}] [@ca{dns-server-port}] <!--
 * -->                [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
 * -->                [@ca{recursion-desired-boolean}]
 * @sa otDnsClientBrowse
 * @par
 * Send a browse (service instance enumeration) DNS query to get the list of services for
 * given service-name
 * @par
 * The parameters after `service-name` are optional. Any unspecified (or zero) value
 * for these optional parameters is replaced by the value from the current default
 * config (`dns config`).
 * @par
 * Note: The DNS server IP can be an IPv4 address, which will be synthesized to an IPv6
 * address using the preferred NAT64 prefix from the network data. The command will return
 * `InvalidState` when the DNS server IP is an IPv4 address but the preferred NAT64 prefix
 * is unavailable. When testing DNS-SD discovery proxy, the zone is not `local` and
 * instead should be `default.service.arpa`.
 * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
 */
template <> otError Dns::Process<Cmd("browse")>(Arg aArgs[])
{
    otError           error = OT_ERROR_NONE;
    otDnsQueryConfig  queryConfig;
    otDnsQueryConfig *config = &queryConfig;

    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = GetDnsConfig(aArgs + 1, config));
    SuccessOrExit(
        error = otDnsClientBrowse(GetInstancePtr(), aArgs[0].GetCString(), &HandleDnsBrowseResponse, this, config));
    error = OT_ERROR_PENDING;

exit:
    return error;
}

/**
 * @cli dns service
 * @cparam dns service @ca{service-instance-label} @ca{service-name} <!--
 * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
 * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
 * -->                 [@ca{recursion-desired-boolean}]
 * @par api_copy
 * #otDnsClientResolveService
 * @par
 * Send a service instance resolution DNS query for a given service instance.
 * Service instance label is provided first, followed by the service name
 * (note that service instance label can contain dot '.' character).
 * @par
 * The parameters after `service-name` are optional. Any unspecified (or zero)
 * value for these optional parameters is replaced by the value from the
 * current default config (`dns config`).
 * @par
 * Note: The DNS server IP can be an IPv4 address, which will be synthesized
 * to an IPv6 address using the preferred NAT64 prefix from the network data.
 * The command will return `InvalidState` when the DNS server IP is an IPv4
 * address but the preferred NAT64 prefix is unavailable.
 * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
 */
template <> otError Dns::Process<Cmd("service")>(Arg aArgs[])
{
    return ProcessService(aArgs, otDnsClientResolveService);
}

/**
 * @cli dns servicehost
 * @cparam dns servicehost @ca{service-instance-label} @ca{service-name} <!--
 * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
 * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
 * -->                 [@ca{recursion-desired-boolean}]
 * @par api_copy
 * #otDnsClientResolveServiceAndHostAddress
 * @par
 * Send a service instance resolution DNS query for a given service instance
 * with potential follow-up host name resolution.
 * Service instance label is provided first, followed by the service name
 * (note that service instance label can contain dot '.' character).
 * @par
 * The parameters after `service-name` are optional. Any unspecified (or zero)
 * value for these optional parameters is replaced by the value from the
 * current default config (`dns config`).
 * @par
 * Note: The DNS server IP can be an IPv4 address, which will be synthesized
 * to an IPv6 address using the preferred NAT64 prefix from the network data.
 * The command will return `InvalidState` when the DNS server IP is an IPv4
 * address but the preferred NAT64 prefix is unavailable.
 * `OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE` is required.
 */
template <> otError Dns::Process<Cmd("servicehost")>(Arg aArgs[])
{
    return ProcessService(aArgs, otDnsClientResolveServiceAndHostAddress);
}

otError Dns::ProcessService(Arg aArgs[], ResolveServiceFn aResolveServiceFn)
{
    otError           error = OT_ERROR_NONE;
    otDnsQueryConfig  queryConfig;
    otDnsQueryConfig *config = &queryConfig;

    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = GetDnsConfig(aArgs + 2, config));
    SuccessOrExit(error = aResolveServiceFn(GetInstancePtr(), aArgs[0].GetCString(), aArgs[1].GetCString(),
                                            &HandleDnsServiceResponse, this, config));
    error = OT_ERROR_PENDING;

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE

//----------------------------------------------------------------------------------------------------------------------

void Dns::OutputResult(otError aError) { Interpreter::GetInterpreter().OutputResult(aError); }

otError Dns::GetDnsConfig(Arg aArgs[], otDnsQueryConfig *&aConfig)
{
    // This method gets the optional DNS config from `aArgs[]`.
    // The format: `[server IP address] [server port] [timeout]
    // [max tx attempt] [recursion desired] [service mode]
    // [transport]`

    otError error = OT_ERROR_NONE;
    bool    recursionDesired;
    bool    nat64Synth;

    ClearAllBytes(*aConfig);

    VerifyOrExit(!aArgs[0].IsEmpty(), aConfig = nullptr);

    SuccessOrExit(error = ParseToIp6Address(GetInstancePtr(), aArgs[0], aConfig->mServerSockAddr.mAddress, nat64Synth));
    if (nat64Synth)
    {
        OutputFormat("Synthesized IPv6 DNS server address: ");
        OutputIp6AddressLine(aConfig->mServerSockAddr.mAddress);
    }

    VerifyOrExit(!aArgs[1].IsEmpty());
    SuccessOrExit(error = aArgs[1].ParseAsUint16(aConfig->mServerSockAddr.mPort));

    VerifyOrExit(!aArgs[2].IsEmpty());
    SuccessOrExit(error = aArgs[2].ParseAsUint32(aConfig->mResponseTimeout));

    VerifyOrExit(!aArgs[3].IsEmpty());
    SuccessOrExit(error = aArgs[3].ParseAsUint8(aConfig->mMaxTxAttempts));

    VerifyOrExit(!aArgs[4].IsEmpty());
    SuccessOrExit(error = aArgs[4].ParseAsBool(recursionDesired));
    aConfig->mRecursionFlag = recursionDesired ? OT_DNS_FLAG_RECURSION_DESIRED : OT_DNS_FLAG_NO_RECURSION;

    VerifyOrExit(!aArgs[5].IsEmpty());
    SuccessOrExit(error = ParseDnsServiceMode(aArgs[5], aConfig->mServiceMode));

    VerifyOrExit(!aArgs[6].IsEmpty());

    if (aArgs[6] == "tcp")
    {
        aConfig->mTransportProto = OT_DNS_TRANSPORT_TCP;
    }
    else if (aArgs[6] == "udp")
    {
        aConfig->mTransportProto = OT_DNS_TRANSPORT_UDP;
    }
    else
    {
        error = OT_ERROR_INVALID_ARGS;
    }

exit:
    return error;
}

const char *const Dns::kServiceModeStrings[] = {
    "unspec",      // OT_DNS_SERVICE_MODE_UNSPECIFIED      (0)
    "srv",         // OT_DNS_SERVICE_MODE_SRV              (1)
    "txt",         // OT_DNS_SERVICE_MODE_TXT              (2)
    "srv_txt",     // OT_DNS_SERVICE_MODE_SRV_TXT          (3)
    "srv_txt_sep", // OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE (4)
    "srv_txt_opt", // OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE (5)
};

static_assert(OT_DNS_SERVICE_MODE_UNSPECIFIED == 0, "OT_DNS_SERVICE_MODE_UNSPECIFIED value is incorrect");
static_assert(OT_DNS_SERVICE_MODE_SRV == 1, "OT_DNS_SERVICE_MODE_SRV value is incorrect");
static_assert(OT_DNS_SERVICE_MODE_TXT == 2, "OT_DNS_SERVICE_MODE_TXT value is incorrect");
static_assert(OT_DNS_SERVICE_MODE_SRV_TXT == 3, "OT_DNS_SERVICE_MODE_SRV_TXT value is incorrect");
static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE == 4, "OT_DNS_SERVICE_MODE_SRV_TXT_SEPARATE value is incorrect");
static_assert(OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE == 5, "OT_DNS_SERVICE_MODE_SRV_TXT_OPTIMIZE value is incorrect");

const char *Dns::DnsConfigServiceModeToString(otDnsServiceMode aMode) const
{
    return Stringify(aMode, kServiceModeStrings);
}

otError Dns::ParseDnsServiceMode(const Arg &aArg, otDnsServiceMode &aMode) const
{
    otError error = OT_ERROR_NONE;

    if (aArg == "def")
    {
        aMode = OT_DNS_SERVICE_MODE_UNSPECIFIED;
        ExitNow();
    }

    for (size_t index = 0; index < OT_ARRAY_LENGTH(kServiceModeStrings); index++)
    {
        if (aArg == kServiceModeStrings[index])
        {
            aMode = static_cast<otDnsServiceMode>(index);
            ExitNow();
        }
    }

    error = OT_ERROR_INVALID_ARGS;

exit:
    return error;
}

void Dns::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse, void *aContext)
{
    static_cast<Dns *>(aContext)->HandleDnsAddressResponse(aError, aResponse);
}

void Dns::HandleDnsAddressResponse(otError aError, const otDnsAddressResponse *aResponse)
{
    char         hostName[OT_DNS_MAX_NAME_SIZE];
    otIp6Address address;
    uint32_t     ttl;

    IgnoreError(otDnsAddressResponseGetHostName(aResponse, hostName, sizeof(hostName)));

    OutputFormat("DNS response for %s - ", hostName);

    if (aError == OT_ERROR_NONE)
    {
        uint16_t index = 0;

        while (otDnsAddressResponseGetAddress(aResponse, index, &address, &ttl) == OT_ERROR_NONE)
        {
            OutputIp6Address(address);
            OutputFormat(" TTL:%lu ", ToUlong(ttl));
            index++;
        }
    }

    OutputNewLine();
    OutputResult(aError);
}

#if OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE

void Dns::OutputDnsServiceInfo(uint8_t aIndentSize, const otDnsServiceInfo &aServiceInfo)
{
    OutputLine(aIndentSize, "Port:%d, Priority:%d, Weight:%d, TTL:%lu", aServiceInfo.mPort, aServiceInfo.mPriority,
               aServiceInfo.mWeight, ToUlong(aServiceInfo.mTtl));
    OutputLine(aIndentSize, "Host:%s", aServiceInfo.mHostNameBuffer);
    OutputFormat(aIndentSize, "HostAddress:");
    OutputIp6Address(aServiceInfo.mHostAddress);
    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mHostAddressTtl));
    OutputFormat(aIndentSize, "TXT:");

    if (!aServiceInfo.mTxtDataTruncated)
    {
        OutputDnsTxtData(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
    }
    else
    {
        OutputFormat("[");
        OutputBytes(aServiceInfo.mTxtData, aServiceInfo.mTxtDataSize);
        OutputFormat("...]");
    }

    OutputLine(" TTL:%lu", ToUlong(aServiceInfo.mTxtDataTtl));
}

void Dns::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse, void *aContext)
{
    static_cast<Dns *>(aContext)->HandleDnsBrowseResponse(aError, aResponse);
}

void Dns::HandleDnsBrowseResponse(otError aError, const otDnsBrowseResponse *aResponse)
{
    char             name[OT_DNS_MAX_NAME_SIZE];
    char             label[OT_DNS_MAX_LABEL_SIZE];
    uint8_t          txtBuffer[kMaxTxtDataSize];
    otDnsServiceInfo serviceInfo;

    IgnoreError(otDnsBrowseResponseGetServiceName(aResponse, name, sizeof(name)));

    OutputLine("DNS browse response for %s", name);

    if (aError == OT_ERROR_NONE)
    {
        uint16_t index = 0;

        while (otDnsBrowseResponseGetServiceInstance(aResponse, index, label, sizeof(label)) == OT_ERROR_NONE)
        {
            OutputLine("%s", label);
            index++;

            serviceInfo.mHostNameBuffer     = name;
            serviceInfo.mHostNameBufferSize = sizeof(name);
            serviceInfo.mTxtData            = txtBuffer;
            serviceInfo.mTxtDataSize        = sizeof(txtBuffer);

            if (otDnsBrowseResponseGetServiceInfo(aResponse, label, &serviceInfo) == OT_ERROR_NONE)
            {
                OutputDnsServiceInfo(kIndentSize, serviceInfo);
            }

            OutputNewLine();
        }
    }

    OutputResult(aError);
}

void Dns::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse, void *aContext)
{
    static_cast<Dns *>(aContext)->HandleDnsServiceResponse(aError, aResponse);
}

void Dns::HandleDnsServiceResponse(otError aError, const otDnsServiceResponse *aResponse)
{
    char             name[OT_DNS_MAX_NAME_SIZE];
    char             label[OT_DNS_MAX_LABEL_SIZE];
    uint8_t          txtBuffer[kMaxTxtDataSize];
    otDnsServiceInfo serviceInfo;

    IgnoreError(otDnsServiceResponseGetServiceName(aResponse, label, sizeof(label), name, sizeof(name)));

    OutputLine("DNS service resolution response for %s for service %s", label, name);

    if (aError == OT_ERROR_NONE)
    {
        serviceInfo.mHostNameBuffer     = name;
        serviceInfo.mHostNameBufferSize = sizeof(name);
        serviceInfo.mTxtData            = txtBuffer;
        serviceInfo.mTxtDataSize        = sizeof(txtBuffer);

        if (otDnsServiceResponseGetServiceInfo(aResponse, &serviceInfo) == OT_ERROR_NONE)
        {
            OutputDnsServiceInfo(/* aIndentSize */ 0, serviceInfo);
            OutputNewLine();
        }
    }

    OutputResult(aError);
}

#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE

#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE

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

    if (aArgs[0].IsEmpty())
    {
        error = OT_ERROR_INVALID_ARGS;
    }
#if OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
    /**
     * @cli dns server upstream
     * @code
     * dns server upstream
     * Enabled
     * Done
     * @endcode
     * @par api_copy
     * #otDnssdUpstreamQueryIsEnabled
     */
    else if (aArgs[0] == "upstream")
    {
        /**
         * @cli dns server upstream {enable|disable}
         * @code
         * dns server upstream enable
         * Done
         * @endcode
         * @cparam dns server upstream @ca{enable|disable}
         * @par api_copy
         * #otDnssdUpstreamQuerySetEnabled
         */
        error = ProcessEnableDisable(aArgs + 1, otDnssdUpstreamQueryIsEnabled, otDnssdUpstreamQuerySetEnabled);
    }
#endif // OPENTHREAD_CONFIG_DNS_UPSTREAM_QUERY_ENABLE
    else
    {
        ExitNow(error = OT_ERROR_INVALID_COMMAND);
    }

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE

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

    static constexpr Command kCommands[] = {

#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
        CmdEntry("browse"),
#endif
#if OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE
        CmdEntry("compression"),
#endif
#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
        CmdEntry("config"),
        CmdEntry("resolve"),
#if OPENTHREAD_CONFIG_DNS_CLIENT_NAT64_ENABLE
        CmdEntry("resolve4"),
#endif
#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE
#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
        CmdEntry("server"),
#endif
#if OPENTHREAD_CONFIG_DNS_CLIENT_ENABLE && OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE
        CmdEntry("service"),
        CmdEntry("servicehost"),
#endif
    };

#undef CmdEntry

    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_CLI_DNS_ENABLE
