/*
 *  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[])
{
    /**
     * @cli dns compression
     * @code
     * dns compression
     * Enabled
     * @endcode
     * @code
     * dns compression enable
     * Enabled
     * @endcode
     * @code
     * dns compression disable
     * Done
     * dns compression
     * Disabled
     * Done
     * @endcode
     * @cparam dns compression [@ca{enable|disable}]
     * @par
     * Gets or sets the "DNS name compression" mode.
     * @par
     * This is intended for testing only and requires `OPENTHREAD_CONFIG_REFERENCE_DEVICE_ENABLE`.
     * @par
     * By default, DNS name compression is enabled. When disabled, DNS names are appended in full and are never
     * compressed. This applies to OpenThread's DNS and SRP client/server modules.
     * @par
     * DNS name compression cannot be disabled if the OpenThread mDNS module is enabled. Enabling the mDNS module will
     * automatically enable name compression if it was previously disabled. Attempting to disable compression while the
     * mDNS module is active will return an error.
     * @sa otDnsIsNameCompressionEnabled
     * @sa otDnsSetNameCompressionEnabled
     */
    return ProcessEnableDisable(aArgs, otDnsIsNameCompressionEnabled, otDnsSetNameCompressionEnabled);
}

#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", ToYesNo(defaultConfig->mRecursionFlag == OT_DNS_FLAG_RECURSION_DESIRED));
        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

#if OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_ENABLE

/**
 * @cli dns query
 * @cparam dns query @ca{record-type} @ca{first-label} @ca{next-labels} <!--
 * -->                 [@ca{DNS-server-IP}] [@ca{DNS-server-port}] <!--
 * -->                 [@ca{response-timeout-ms}] [@ca{max-tx-attempts}] <!--
 * -->                 [@ca{recursion-desired-boolean}]
 * @code
 * dns query 25 myhost default.service.arpa.
 * DNS query response for myhost.default.service.arpa.
 * 0)
 *     RecordType:25, RecordLength: 32, TTL:7108, Section:answer
 *     Name:myhost.default.service.arpa.
 *     RecordData:[001900010000e02d00440201030d4983605c0406803deb2d672cc42224773977]
 * Done
 * @endcode
 * @par
 * Send a DNS query for a given record type and DNS name.
 * DNS name is provided as a first label, followed by the next labels
 * which are dot '.' separated. Note that the first label can itself
 * contain the dot '.' character.
 * @par
 * The parameters after `next-labels` are optional. Any unspecified (or zero)
 * value for these optional parameters is replaced by the value from the
 * current default config (`dns config`).
 * @par
 * `OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_ENABLE` is required.
 */
template <> otError Dns::Process<Cmd("query")>(Arg aArgs[])
{
    otError           error = OT_ERROR_NONE;
    otDnsQueryConfig  queryConfig;
    otDnsQueryConfig *config = &queryConfig;
    uint16_t          recordType;

    SuccessOrExit(error = aArgs[0].ParseAsUint16(recordType));
    VerifyOrExit(!aArgs[2].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    SuccessOrExit(error = GetDnsConfig(aArgs + 3, config));
    SuccessOrExit(error = otDnsClientQueryRecord(GetInstancePtr(), recordType, aArgs[1].GetCString(),
                                                 aArgs[2].GetCString(), &HandleDnsRecordResponse, this, config));
    error = OT_ERROR_PENDING;

exit:
    return error;
}

#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_ENABLE

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

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)
{
    otError      error;
    char         hostName[OT_DNS_MAX_NAME_SIZE];
    otIp6Address address;
    uint32_t     ttl;
    uint16_t     index = 0;

    SuccessOrExit(error = otDnsAddressResponseGetHostName(aResponse, hostName, sizeof(hostName)));
    OutputFormat("DNS response for %s - ", hostName);

    error = aError;
    SuccessOrExit(error);

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

    OutputNewLine();

exit:
    OutputResult(error);
}

#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)
{
    Error            error;
    char             name[OT_DNS_MAX_NAME_SIZE];
    char             label[OT_DNS_MAX_LABEL_SIZE];
    uint8_t          txtBuffer[kMaxTxtDataSize];
    otDnsServiceInfo serviceInfo;
    uint16_t         index = 0;

    SuccessOrExit(error = otDnsBrowseResponseGetServiceName(aResponse, name, sizeof(name)));
    OutputLine("DNS browse response for %s", name);

    error = aError;
    SuccessOrExit(error);

    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();
    }

exit:
    OutputResult(error);
}

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)
{
    otError          error;
    char             name[OT_DNS_MAX_NAME_SIZE];
    char             label[OT_DNS_MAX_LABEL_SIZE];
    uint8_t          txtBuffer[kMaxTxtDataSize];
    otDnsServiceInfo serviceInfo;

    SuccessOrExit(error = otDnsServiceResponseGetServiceName(aResponse, label, sizeof(label), name, sizeof(name)));
    OutputLine("DNS service resolution response for %s for service %s", label, name);

    error = aError;
    SuccessOrExit(error);

    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();
    }

exit:
    OutputResult(error);
}

#endif // OPENTHREAD_CONFIG_DNS_CLIENT_SERVICE_DISCOVERY_ENABLE

#if OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_ENABLE
void Dns::HandleDnsRecordResponse(otError aError, const otDnsRecordResponse *aResponse, void *aContext)
{
    static_cast<Dns *>(aContext)->HandleDnsRecordResponse(aError, aResponse);
}

void Dns::HandleDnsRecordResponse(otError aError, const otDnsRecordResponse *aResponse)
{
    otError         error;
    char            name[OT_DNS_MAX_NAME_SIZE];
    uint8_t         data[kMaxRrDataSize];
    otDnsRecordInfo recordInfo;

    SuccessOrExit(error = otDnsRecordResponseGetQueryName(aResponse, name, sizeof(name)));
    OutputLine("DNS query response for %s ", name);

    error = aError;
    SuccessOrExit(error);

    for (uint16_t index = 0;; index++)
    {
        ClearAllBytes(recordInfo);
        recordInfo.mNameBuffer     = name;
        recordInfo.mNameBufferSize = sizeof(name);
        recordInfo.mDataBuffer     = data;
        recordInfo.mDataBufferSize = sizeof(data);

        error = otDnsRecordResponseGetRecordInfo(aResponse, index, &recordInfo);

        if (error == OT_ERROR_NOT_FOUND)
        {
            error = OT_ERROR_NONE;
            ExitNow();
        }

        SuccessOrExit(error);

        OutputLine("%u)", index);
        OutputLine(kIndentSize, "RecordType:%u, RecordLength:%u, TTL:%lu, Section:%s", recordInfo.mRecordType,
                   recordInfo.mRecordLength, ToUlong(recordInfo.mTtl), RecordSectionToString(recordInfo.mSection));
        OutputLine(kIndentSize, "Name:%s", recordInfo.mNameBuffer);
        OutputFormat(kIndentSize, "RecordData:[");
        OutputBytes(recordInfo.mDataBuffer, recordInfo.mDataBufferSize);

        if (recordInfo.mDataBufferSize != recordInfo.mRecordLength)
        {
            OutputFormat("...");
        }

        OutputLine("]");
    }

exit:
    OutputResult(error);
}

const char *Dns::RecordSectionToString(otDnsRecordSection aSection)
{
    const char *const kSectionString[] = {
        "answer",     // (0) OT_DNS_SECTION_ANSWER
        "authority",  // (1) OT_DNS_SECTION_AUTHORITY
        "additional", // (2) OT_DNS_SECTION_ADDITIONAL
    };

    static_assert(0 == OT_DNS_SECTION_ANSWER, "OT_DNS_SECTION_ANSWER value is incorrect");
    static_assert(1 == OT_DNS_SECTION_AUTHORITY, "OT_DNS_SECTION_AUTHORITY value is incorrect");
    static_assert(2 == OT_DNS_SECTION_ADDITIONAL, "OT_DNS_SECTION_ADDITIONALATA value is incorrect");

    return Stringify(aSection, kSectionString);
}
#endif // OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_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"),
#if OPENTHREAD_CONFIG_DNS_CLIENT_ARBITRARY_RECORD_QUERY_ENABLE
        CmdEntry("query"),
#endif
        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
