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

#include "cli_coap.hpp"

#if OPENTHREAD_CONFIG_COAP_API_ENABLE

#include <openthread/random_noncrypto.h>

#include <ctype.h>

#include "cli/cli.hpp"

namespace ot {
namespace Cli {

Coap::Coap(otInstance *aInstance, OutputImplementer &aOutputImplementer)
    : Utils(aInstance, aOutputImplementer)
    , mUseDefaultRequestTxParameters(true)
    , mUseDefaultResponseTxParameters(true)
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    , mObserveSerial(0)
    , mSubscriberConfirmableNotifications(false)
    , mValidateObserveClient(false)
    , mNotificationSeriesCount(0)
#endif
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    , mBlockCount(3)
#endif
{
    ClearAllBytes(mResource);
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    ClearAllBytes(mRequestAddr);
    ClearAllBytes(mSubscriberSock);
    ClearAllBytes(mRequestToken);
    ClearAllBytes(mSubscriberToken);
    ClearAllBytes(mRequestUri);
#endif
    ClearAllBytes(mUriPath);
    strncpy(mResourceContent, "0", sizeof(mResourceContent));
    mResourceContent[sizeof(mResourceContent) - 1] = '\0';
}

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
otError Coap::CancelResourceSubscription(bool aSendCancelMessage)
{
    otError       error   = OT_ERROR_NONE;
    otMessage    *message = nullptr;
    otMessageInfo messageInfo;

    ClearAllBytes(messageInfo);
    messageInfo.mPeerAddr = mRequestAddr;
    messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;

    VerifyOrExit(mRequestToken.mLength != 0, error = OT_ERROR_INVALID_STATE);

    if (aSendCancelMessage)
    {
        message = otCoapNewMessage(GetInstancePtr(), nullptr);
        VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);

        SuccessOrExit(error = otCoapMessageInit(message, OT_COAP_TYPE_CONFIRMABLE, OT_COAP_CODE_GET));

        SuccessOrExit(error = otCoapMessageWriteToken(message, &mRequestToken));
        SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 1));
        SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, mRequestUri));
        SuccessOrExit(error = otCoapSendRequest(GetInstancePtr(), message, &messageInfo, &Coap::HandleResponse, this));
    }

    ClearAllBytes(mRequestAddr);
    ClearAllBytes(mRequestUri);
    ClearAllBytes(mRequestToken);

exit:

    if ((error != OT_ERROR_NONE) && (message != nullptr))
    {
        otMessageFree(message);
    }

    return error;
}

void Coap::CancelSubscriber(void)
{
    OutputFormat("Removed subscriber ");
    OutputIp6AddressLine(mSubscriberSock.mAddress);
    ClearAllBytes(mSubscriberSock);
    ClearAllBytes(mSubscriberToken);
}
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE

void Coap::PrintPayload(otMessage *aMessage)
{
    uint8_t  buf[kMaxBufferSize];
    uint16_t bytesToPrint;
    uint16_t bytesPrinted = 0;
    uint16_t length       = otMessageGetLength(aMessage) - otMessageGetOffset(aMessage);

    if (length > 0)
    {
        OutputFormat(" with payload: ");

        while (length > 0)
        {
            bytesToPrint = Min(length, static_cast<uint16_t>(sizeof(buf)));
            otMessageRead(aMessage, otMessageGetOffset(aMessage) + bytesPrinted, buf, bytesToPrint);

            OutputBytes(buf, static_cast<uint8_t>(bytesToPrint));

            length -= bytesToPrint;
            bytesPrinted += bytesToPrint;
        }
    }

    OutputNewLine();
}

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
/**
 * @cli coap cancel
 * @code
 * coap cancel
 * Done
 * @endcode
 * @par
 * Cancels an existing observation subscription to a remote resource on the CoAP server.
 * @note This command is available only when `OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE` is set.
 * @csa{coap observe}
 */
template <> otError Coap::Process<Cmd("cancel")>(Arg aArgs[])
{
    OT_UNUSED_VARIABLE(aArgs);

    return CancelResourceSubscription(true);
}
#endif

/**
 * @cli coap resource (get,set)
 * @code
 * coap resource test-resource
 * Done
 * @endcode
 * @code
 * coap resource
 * test-resource
 * Done
 * @endcode
 * @cparam coap resource [@ca{uri-path}]
 * @par
 * Gets or sets the URI path of the CoAP server resource.
 * @sa otCoapAddResource
 * @sa otCoapAddBlockWiseResource
 */
template <> otError Coap::Process<Cmd("resource")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;

    if (!aArgs[0].IsEmpty())
    {
        VerifyOrExit(aArgs[0].GetLength() < kMaxUriLength, error = OT_ERROR_INVALID_ARGS);

        mResource.mUriPath = mUriPath;
        mResource.mContext = this;
        mResource.mHandler = &Coap::HandleRequest;

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        mResource.mReceiveHook  = &Coap::BlockwiseReceiveHook;
        mResource.mTransmitHook = &Coap::BlockwiseTransmitHook;

        if (!aArgs[1].IsEmpty())
        {
            SuccessOrExit(error = aArgs[1].ParseAsUint32(mBlockCount));
        }
#endif

        strncpy(mUriPath, aArgs[0].GetCString(), sizeof(mUriPath) - 1);

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        otCoapAddBlockWiseResource(GetInstancePtr(), &mResource);
#else
        otCoapAddResource(GetInstancePtr(), &mResource);
#endif
    }
    else
    {
        OutputLine("%s", mResource.mUriPath != nullptr ? mResource.mUriPath : "");
    }

exit:
    return error;
}

/**
 * @cli coap set
 * @code
 * coap set Testing123
 * Done
 * @endcode
 * @cparam coap set @ca{new-content}
 * @par
 * Sets the content sent by the resource on the CoAP server.
 * If a CoAP client is observing the resource, a notification is sent to that client.
 * @csa{coap observe}
 * @sa otCoapMessageInit
 * @sa otCoapNewMessage
 */
template <> otError Coap::Process<Cmd("set")>(Arg aArgs[])
{
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    otMessage    *notificationMessage = nullptr;
    otMessageInfo messageInfo;
#endif
    otError error = OT_ERROR_NONE;

    if (!aArgs[0].IsEmpty())
    {
        VerifyOrExit(aArgs[0].GetLength() < sizeof(mResourceContent), error = OT_ERROR_INVALID_ARGS);
        strncpy(mResourceContent, aArgs[0].GetCString(), sizeof(mResourceContent));
        mResourceContent[sizeof(mResourceContent) - 1] = '\0';

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        if (mSubscriberToken.mLength > 0)
        {
            // determine the type of notification to send
            const bool isConNotification = mSubscriberConfirmableNotifications || mValidateObserveClient;
            mValidateObserveClient       = false;

            // Notify the subscriber
            ClearAllBytes(messageInfo);
            messageInfo.mPeerAddr = mSubscriberSock.mAddress;
            messageInfo.mPeerPort = mSubscriberSock.mPort;

            OutputFormat("sending coap notification to ");
            OutputIp6AddressLine(mSubscriberSock.mAddress);

            notificationMessage = otCoapNewMessage(GetInstancePtr(), nullptr);
            VerifyOrExit(notificationMessage != nullptr, error = OT_ERROR_NO_BUFS);

            SuccessOrExit(
                error = otCoapMessageInit(notificationMessage,
                                          (isConNotification ? OT_COAP_TYPE_CONFIRMABLE : OT_COAP_TYPE_NON_CONFIRMABLE),
                                          OT_COAP_CODE_CONTENT));

            SuccessOrExit(error = otCoapMessageWriteToken(notificationMessage, &mSubscriberToken));
            SuccessOrExit(error = otCoapMessageAppendObserveOption(notificationMessage, mObserveSerial++));
            SuccessOrExit(error = otCoapMessageSetPayloadMarker(notificationMessage));
            SuccessOrExit(error = otMessageAppend(notificationMessage, mResourceContent,
                                                  static_cast<uint16_t>(strlen(mResourceContent))));
            // Send the CoAP notification message, which is a CoAP Response, using the send-request API.
            // If NON, it's fire-and-forget. If CON, supply the handler for receiving the CoAP ACK.
            SuccessOrExit(
                error = otCoapSendRequest(
                    GetInstancePtr(), notificationMessage, &messageInfo,
                    isConNotification ? static_cast<otCoapResponseHandler>(&Coap::HandleNotificationAck) : nullptr,
                    this));

            // after starting a single client validation, don't require validation for the
            // kMaxNonNotificationsBeforeValidation next notifications. Note that if mSubscriberConfirmableNotifications
            // == true, validation is done automatically with every CON notification.
            if (isConNotification)
            {
                mNotificationSeriesCount = 0;
            }
            else
            {
                mNotificationSeriesCount++;
                if (mNotificationSeriesCount >= kMaxNonNotificationsBeforeValidation)
                {
                    mNotificationSeriesCount = 0;
                    // Require client validation using a CON message for the next notification.
                    mValidateObserveClient = true;
                }
            }
        }
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    }
    else
    {
        OutputLine("%s", mResourceContent);
    }

exit:

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    if ((error != OT_ERROR_NONE) && (notificationMessage != nullptr))
    {
        otMessageFree(notificationMessage);
    }
#endif

    return error;
}

/**
 * @cli coap start
 * @code
 * coap start
 * Done
 * @endcode
 * @par
 * Starts the CoAP server. @moreinfo{@coap}.
 * @sa otCoapStart
 */
template <> otError Coap::Process<Cmd("start")>(Arg aArgs[])
{
    OT_UNUSED_VARIABLE(aArgs);

    return otCoapStart(GetInstancePtr(), OT_DEFAULT_COAP_PORT);
}

/**
 * @cli coap stop
 * @code
 * coap stop
 * Done
 * @endcode
 * @par api_copy
 * #otCoapStop
 */
template <> otError Coap::Process<Cmd("stop")>(Arg aArgs[])
{
    OT_UNUSED_VARIABLE(aArgs);

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    otCoapRemoveBlockWiseResource(GetInstancePtr(), &mResource);
#else
    otCoapRemoveResource(GetInstancePtr(), &mResource);
#endif

    return otCoapStop(GetInstancePtr());
}

/**
 * @cli coap parameters(get,set)
 * @code
 * coap parameters request
 * Transmission parameters for request:
 * ACK_TIMEOUT=1000 ms, ACK_RANDOM_FACTOR=255/254, MAX_RETRANSMIT=2
 * Done
 * @endcode
 * @code
 * coap parameters request default
 * Transmission parameters for request:
 * default
 * Done
 * @endcode
 * @code
 * coap parameters request 1000 255 254 2
 * Transmission parameters for request:
 * ACK_TIMEOUT=1000 ms, ACK_RANDOM_FACTOR=255/254, MAX_RETRANSMIT=2
 * Done
 * @endcode
 * @cparam coap parameters @ca{type} [@ca{default} | <!--
 * -->@ca{ack_timeout ack_random_factor_numerator <!--
 * -->ack_random_factor_denominator max_retransmit}]
 *   * `type`: `request` for CoAP requests, or `response` for CoAP responses.
        If no more parameters are given, the command prints the current configuration.
 *   * `default`: Sets the transmission parameters to
        the following default values:
 *       * `ack_timeout`: 2000 milliseconds
 *       * `ack_random_factor_numerator`: 3
 *       * `ack_random_factor_denominator`: 2
 *       * `max_retransmit`: 4
 *   * `ack_timeout`: The `ACK_TIMEOUT` (0-UINT32_MAX) in milliseconds.
       Refer to RFC7252.
 *   * `ack_random_factor_numerator`:
       The `ACK_RANDOM_FACTOR` numerator, with possible values
       of 0-255. Refer to RFC7252.
 *   * `ack_random_factor_denominator`:
 *     The `ACK_RANDOM_FACTOR` denominator, with possible values
 *     of 0-255. Refer to RFC7252.
 *   * `max_retransmit`: The `MAX_RETRANSMIT` (0-255). Refer to RFC7252.
 * @par
 * Gets current CoAP parameter values if the command is run with no optional
 * parameters.
 * @par
 * Sets the CoAP parameters either to their default values or to the values
 * you specify, depending on the syntax chosen.
 */
template <> otError Coap::Process<Cmd("parameters")>(Arg aArgs[])
{
    otError             error = OT_ERROR_NONE;
    bool               *defaultTxParameters;
    otCoapTxParameters *txParameters;

    if (aArgs[0] == "request")
    {
        txParameters        = &mRequestTxParameters;
        defaultTxParameters = &mUseDefaultRequestTxParameters;
    }
    else if (aArgs[0] == "response")
    {
        txParameters        = &mResponseTxParameters;
        defaultTxParameters = &mUseDefaultResponseTxParameters;
    }
    else
    {
        ExitNow(error = OT_ERROR_INVALID_ARGS);
    }

    if (!aArgs[1].IsEmpty())
    {
        if (aArgs[1] == "default")
        {
            *defaultTxParameters = true;
        }
        else
        {
            SuccessOrExit(error = aArgs[1].ParseAsUint32(txParameters->mAckTimeout));
            SuccessOrExit(error = aArgs[2].ParseAsUint8(txParameters->mAckRandomFactorNumerator));
            SuccessOrExit(error = aArgs[3].ParseAsUint8(txParameters->mAckRandomFactorDenominator));
            SuccessOrExit(error = aArgs[4].ParseAsUint8(txParameters->mMaxRetransmit));

            VerifyOrExit(txParameters->mAckRandomFactorNumerator > txParameters->mAckRandomFactorDenominator,
                         error = OT_ERROR_INVALID_ARGS);

            *defaultTxParameters = false;
        }
    }

    OutputLine("Transmission parameters for %s:", aArgs[0].GetCString());

    if (*defaultTxParameters)
    {
        OutputLine("default");
    }
    else
    {
        OutputLine("ACK_TIMEOUT=%lu ms, ACK_RANDOM_FACTOR=%u/%u, MAX_RETRANSMIT=%u", ToUlong(txParameters->mAckTimeout),
                   txParameters->mAckRandomFactorNumerator, txParameters->mAckRandomFactorDenominator,
                   txParameters->mMaxRetransmit);
    }

exit:
    return error;
}

/**
 * @cli coap get
 * @code
 * coap get fdde:ad00:beef:0:2780:9423:166c:1aac test-resource
 * Done
 * @endcode
 * @code
 * coap get fdde:ad00:beef:0:2780:9423:166c:1aac test-resource block-1024
 * Done
 * @endcode
 * @cparam coap get @ca{address} @ca{uri-path} [@ca{type}]
 *   * `address`: IPv6 address of the CoAP server.
 *   * `uri-path`: URI path of the resource.
 *   * `type`:
 *       * `con`: Confirmable
 *       * `non-con`: Non-confirmable (default)
 *       * `block-`: Use this option, followed by the block-wise value,
 *          if the response should be transferred block-wise. Valid
 *          values are: `block-16`, `block-32`, `block-64`, `block-128`,
 *          `block-256`, `block-512`, or `block-1024`.
 * @par
 * Gets information about the specified CoAP resource on the CoAP server.
 */
template <> otError Coap::Process<Cmd("get")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_GET); }

/**
 * @cli coap post
 * @code
 * coap post fdde:ad00:beef:0:2780:9423:166c:1aac test-resource con hellothere
 * Done
 * @endcode
 * @code
 * coap post fdde:ad00:beef:0:2780:9423:166c:1aac test-resource block-1024 10
 * Done
 * @endcode
 * @cparam coap post @ca{address} @ca{uri-path} [@ca{type}] [@ca{payload}]
 *   * `address`: IPv6 address of the CoAP server.
 *   * `uri-path`: URI path of the resource.
 *   * `type`:
 *         * `con`: Confirmable
 *         * `non-con`: Non-confirmable (default)
 *         * `block-`: Use this option, followed by the block-wise value,
 *            to send blocks with a randomly generated number of bytes
 *            for the payload. Valid values are:
 *            `block-16`, `block-32`, `block-64`, `block-128`,
 *            `block-256`, `block-512`, or `block-1024`.
 *   * `payload`: CoAP payload request, which if used is either a string or an
 *     integer, depending on the `type`. If the `type` is `con` or `non-con`,
 *     the `payload` parameter is optional. If you leave out the
 *     `payload` parameter, an empty payload is sent. However, If you use the
 *     `payload` parameter, its value must be a string, such as
 *     `hellothere`.  If the `type` is `block-`,
 *     the value of the`payload` parameter must be an integer that specifies
 *     the number of blocks to send. The `block-` type requires
 *     `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
 * @par
 * Creates the specified CoAP resource. @moreinfo{@coap}.
 */
template <> otError Coap::Process<Cmd("post")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_POST); }

/**
 * @cli coap put
 * @code
 * coap put fdde:ad00:beef:0:2780:9423:166c:1aac test-resource con hellothere
 * Done
 * @endcode
 * @code
 * coap put fdde:ad00:beef:0:2780:9423:166c:1aac test-resource block-1024 10
 * Done
 * @endcode
 * @cparam coap put @ca{address} @ca{uri-path} [@ca{type}] [@ca{payload}]
 *   * `address`: IPv6 address of the CoAP server.
 *   * `uri-path`: URI path of the resource.
 *   * `type`:
 *         * `con`: Confirmable
 *         * `non-con`: Non-confirmable (default)
 *         * `block-`: Use this option, followed by the block-wise value,
 *            to send blocks with a randomly generated number of bytes
 *            for the payload. Valid values are:
 *            `block-16`, `block-32`, `block-64`, `block-128`,
 *            `block-256`, `block-512`, or `block-1024`.
 *   * `payload`: CoAP payload request, which if used is either a string or an
 *     integer, depending on the `type`. If the `type` is `con` or `non-con`,
 *     the `payload` parameter is optional. If you leave out the
 *     `payload` parameter, an empty payload is sent. However, If you use the
 *     `payload` parameter, its value must be a string, such as
 *     `hellothere`. If the `type` is `block-`,
 *     the value of the`payload` parameter must be an integer that specifies
 *     the number of blocks to send. The `block-` type requires
 *     `OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE` to be set.
 * @par
 * Modifies the specified CoAP resource. @moreinfo{@coap}.
 */
template <> otError Coap::Process<Cmd("put")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_PUT); }

/**
 * @cli coap delete
 * @code
 * coap delete fdde:ad00:beef:0:2780:9423:166c:1aac test-resource con hellothere
 * Done
 * @endcode
 * @cparam coap delete @ca{address} @ca{uri-path} [@ca{type}] [@ca{payload}]
 *   * `address`: IPv6 address of the CoAP server.
 *   * `uri-path`: URI path of the resource.
 *   * `type`:
 *       * `con`: Confirmable
 *       * `non-con`: Non-confirmable (default)
 *   * `payload`: The CoAP payload string. For example, `hellothere`.
 *  @par
 *  Deletes the specified CoAP resource.
 */
template <> otError Coap::Process<Cmd("delete")>(Arg aArgs[]) { return ProcessRequest(aArgs, OT_COAP_CODE_DELETE); }

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
/**
 * @cli coap observe
 * @code
 * coap observe fdde:ad00:beef:0:2780:9423:166c:1aac test-resource
 * Done
 * @endcode
 * @cparam coap observe @ca{address} @ca{uri-path} [@ca{type}]
 *   * `address`: IPv6 address of the CoAP server.
 *   * `uri-path`: URI path of the resource.
 *   * `type`:
 *       * `con`: Confirmable
 *       * `non-con`: Non-confirmable (default).
 * @par
 * Triggers a subscription request which allows the CoAP client to
 * observe the specified resource on the CoAP server for possible changes
 * in its state.
 * @note This command is available only when `OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE` is set.
 */
template <> otError Coap::Process<Cmd("observe")>(Arg aArgs[])
{
    return ProcessRequest(aArgs, OT_COAP_CODE_GET, /* aCoapObserve */ true);
}
#endif

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
otError Coap::ProcessRequest(Arg aArgs[], otCoapCode aCoapCode, bool aCoapObserve)
#else
otError Coap::ProcessRequest(Arg aArgs[], otCoapCode aCoapCode)
#endif
{
    otError       error   = OT_ERROR_NONE;
    otMessage    *message = nullptr;
    otMessageInfo messageInfo;
    uint16_t      payloadLength    = 0;
    char         *uriQueryStartPtr = nullptr;

    // Default parameters
    char         coapUri[kMaxUriLength] = "test";
    otCoapType   coapType               = OT_COAP_TYPE_NON_CONFIRMABLE;
    otIp6Address coapDestinationIp;
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    bool           coapBlock     = false;
    otCoapBlockSzx coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_16;
    BlockType      coapBlockType = (aCoapCode == OT_COAP_CODE_GET) ? kBlockType2 : kBlockType1;
#endif

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE && OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    if (aCoapObserve)
    {
        coapBlockType = kBlockType1;
    }
#endif

    SuccessOrExit(error = aArgs[0].ParseAsIp6Address(coapDestinationIp));

    VerifyOrExit(!aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
    VerifyOrExit(aArgs[1].GetLength() < sizeof(coapUri), error = OT_ERROR_INVALID_ARGS);
    strcpy(coapUri, aArgs[1].GetCString());

    // CoAP-Type
    if (!aArgs[2].IsEmpty())
    {
        if (aArgs[2] == "con")
        {
            coapType = OT_COAP_TYPE_CONFIRMABLE;
        }
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        else if (aArgs[2] == "block-16")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_16;
        }
        else if (aArgs[2] == "block-32")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_32;
        }
        else if (aArgs[2] == "block-64")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_64;
        }
        else if (aArgs[2] == "block-128")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_128;
        }
        else if (aArgs[2] == "block-256")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_256;
        }
        else if (aArgs[2] == "block-512")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_512;
        }
        else if (aArgs[2] == "block-1024")
        {
            coapType      = OT_COAP_TYPE_CONFIRMABLE;
            coapBlock     = true;
            coapBlockSize = OT_COAP_OPTION_BLOCK_SZX_1024;
        }
#endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    }

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    if (aCoapObserve && mRequestToken.mLength)
    {
        // New observe request: cancel the existing observation silently, without sending an explicit cancel message.
        // Note: explicit cancellation MAY be sent by the user using 'coap cancel' per Section 3.6 of RFC7641.
        IgnoreError(CancelResourceSubscription(false));
    }
#endif

    message = otCoapNewMessage(GetInstancePtr(), nullptr);
    VerifyOrExit(message != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = otCoapMessageInit(message, coapType, aCoapCode));
    otCoapMessageGenerateToken(message, OT_COAP_DEFAULT_TOKEN_LENGTH);

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    if (aCoapObserve)
    {
        SuccessOrExit(error = otCoapMessageAppendObserveOption(message, 0));
    }
#endif

    uriQueryStartPtr = const_cast<char *>(StringFind(coapUri, '?'));

    if (uriQueryStartPtr == nullptr)
    {
        // "?" doesn't present in URI --> contains only URI path parts
        SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, coapUri));
    }
    else
    {
        // "?" presents in URI --> contains URI path AND URI query parts
        *uriQueryStartPtr++ = '\0';
        SuccessOrExit(error = otCoapMessageAppendUriPathOptions(message, coapUri));
        SuccessOrExit(error = otCoapMessageAppendUriQueryOptions(message, uriQueryStartPtr));
    }

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    if (coapBlock)
    {
        if (coapBlockType == kBlockType1)
        {
            SuccessOrExit(error = otCoapMessageAppendBlock1Option(message, 0, true, coapBlockSize));
        }
        else
        {
            SuccessOrExit(error = otCoapMessageAppendBlock2Option(message, 0, false, coapBlockSize));
        }
    }
#endif

    if (!aArgs[3].IsEmpty())
    {
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        if (coapBlock)
        {
            SuccessOrExit(error = aArgs[3].ParseAsUint32(mBlockCount));
        }
        else
        {
#endif
            payloadLength = aArgs[3].GetLength();

            if (payloadLength > 0)
            {
                SuccessOrExit(error = otCoapMessageSetPayloadMarker(message));
            }
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        }
#endif
    }

    // Embed content into message if given
    if (payloadLength > 0)
    {
        SuccessOrExit(error = otMessageAppend(message, aArgs[3].GetCString(), payloadLength));
    }

    ClearAllBytes(messageInfo);
    messageInfo.mPeerAddr = coapDestinationIp;
    messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    if (aCoapObserve)
    {
        // Make a note of the message details for later so we can cancel it later.
        memcpy(&mRequestAddr, &coapDestinationIp, sizeof(mRequestAddr));
        SuccessOrExit(error = otCoapMessageReadToken(message, &mRequestToken));

        // Use `memcpy` instead of `strncpy` here because GCC will give warnings for `strncpy` when the dest's length is
        // not bigger than the src's length.
        memcpy(mRequestUri, coapUri, sizeof(mRequestUri) - 1);
    }
#endif

    if ((coapType == OT_COAP_TYPE_CONFIRMABLE) || (aCoapCode == OT_COAP_CODE_GET))
    {
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        if (coapBlock)
        {
            if (aCoapCode == OT_COAP_CODE_PUT || aCoapCode == OT_COAP_CODE_POST)
            {
                SuccessOrExit(error = otCoapMessageSetPayloadMarker(message));
            }
            error = otCoapSendRequestBlockWiseWithParameters(GetInstancePtr(), message, &messageInfo,
                                                             &Coap::HandleResponse, this, GetRequestTxParameters(),
                                                             Coap::BlockwiseTransmitHook, Coap::BlockwiseReceiveHook);
        }
        else
        {
#endif
            error = otCoapSendRequestWithParameters(GetInstancePtr(), message, &messageInfo, &Coap::HandleResponse,
                                                    this, GetRequestTxParameters());
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        }
#endif
    }
    else
    {
        error = otCoapSendRequestWithParameters(GetInstancePtr(), message, &messageInfo, nullptr, nullptr,
                                                GetResponseTxParameters());
    }

exit:

    if ((error != OT_ERROR_NONE) && (message != nullptr))
    {
        otMessageFree(message);
    }

    return error;
}

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

    static constexpr Command kCommands[] = {
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        CmdEntry("cancel"),
#endif
        CmdEntry("delete"),     CmdEntry("get"),
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        CmdEntry("observe"),
#endif
        CmdEntry("parameters"), CmdEntry("post"),  CmdEntry("put"),  CmdEntry("resource"),
        CmdEntry("set"),        CmdEntry("start"), CmdEntry("stop"),
    };

#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() ? OT_ERROR_INVALID_ARGS : OT_ERROR_NONE);
    }

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

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

exit:
    return error;
}

void Coap::HandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    static_cast<Coap *>(aContext)->HandleRequest(aMessage, aMessageInfo);
}

void Coap::HandleRequest(otMessage *aMessage, const otMessageInfo *aMessageInfo)
{
    otError    error           = OT_ERROR_NONE;
    otMessage *responseMessage = nullptr;
    otCoapCode responseCode    = OT_COAP_CODE_EMPTY;
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    uint64_t observe        = 0;
    bool     observePresent = false;
#endif
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    bool     checkForBlock1 = false;
    bool     checkForBlock2 = false;
    uint64_t block1Value    = 0;
    bool     block1Present  = false;
    uint64_t block2Value    = 0;
    bool     block2Present  = false;
#endif
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE || OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    otCoapOptionIterator iterator;
#endif

    OutputFormat("coap request from ");
    OutputIp6Address(aMessageInfo->mPeerAddr);
    OutputFormat(" ");

    switch (otCoapMessageGetCode(aMessage))
    {
    case OT_COAP_CODE_GET:
        OutputFormat("GET");
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        checkForBlock2 = true;
#endif
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        SuccessOrExit(error = otCoapOptionIteratorInit(&iterator, aMessage));
        if (otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE) != nullptr)
        {
            SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &observe));
            observePresent = true;

            OutputFormat(" OBS=");
            OutputUint64(observe);
        }
#endif
        break;

    case OT_COAP_CODE_DELETE:
        OutputFormat("DELETE");
        break;

    case OT_COAP_CODE_PUT:
        OutputFormat("PUT");
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        checkForBlock1 = true;
#endif
        break;

    case OT_COAP_CODE_POST:
        OutputFormat("POST");
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        checkForBlock1 = true;
        checkForBlock2 = true;
#endif
        break;

    default:
        OutputLine("Undefined");
        ExitNow(error = OT_ERROR_PARSE);
    }

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    // Init iterator and check for Block1 (only for POST/PUT requests i.e. checkForBlock1 set)
    SuccessOrExit(error = otCoapOptionIteratorInit(&iterator, aMessage));
    if (checkForBlock1 && otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_BLOCK1) != nullptr)
    {
        SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &block1Value));
        block1Present = true;
    }

    // Check for Block2 (only for GET/POST requests  i.e. checkForBlock2 set)
    if (checkForBlock2 && otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_BLOCK2) != nullptr)
    {
        SuccessOrExit(error = otCoapOptionIteratorGetOptionUintValue(&iterator, &block2Value));
        block2Present = true;
    }
#endif

    PrintPayload(aMessage);

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
    if (observePresent && (mSubscriberToken.mLength > 0) && (observe == 0))
    {
        // There is already a subscriber: ignore the Observe option per Section 4.1 of RFC7641.
        observePresent = false;
    }
#endif

    if (otCoapMessageGetCode(aMessage) == OT_COAP_CODE_GET)
    {
        responseCode = OT_COAP_CODE_CONTENT;
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        if (observePresent)
        {
            if (observe == 0)
            {
                // New subscriber
                OutputLine("Subscribing client");
                mSubscriberSock.mAddress = aMessageInfo->mPeerAddr;
                mSubscriberSock.mPort    = aMessageInfo->mPeerPort;
                mValidateObserveClient   = false;
                mNotificationSeriesCount = 0;
                SuccessOrExit(error = otCoapMessageReadToken(aMessage, &mSubscriberToken));

                /*
                 * Implementer note.
                 *
                 * Here, we try to match a confirmable GET request with confirmable
                 * notifications, however this is not a requirement of RFC7641:
                 * the server can send notifications of either type regardless of
                 * what the client used to subscribe initially.
                 */
                mSubscriberConfirmableNotifications = (otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE);
            }
            else if (observe == 1)
            {
                // See if it matches our subscriber token

                otCoapToken token;

                SuccessOrExit(error = otCoapMessageReadToken(aMessage, &token));

                if (otCoapMessageAreTokensEqual(&token, &mSubscriberToken))
                {
                    CancelSubscriber();
                }
            }
        }
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    }
    else if (block1Present && block2Present)
    {
        // POST supports both Block1/Block2, but the current codebase not yet.
        responseCode = OT_COAP_CODE_NOT_IMPLEMENTED;
#endif
    }
    else
    {
        responseCode = OT_COAP_CODE_CHANGED;
    }

    responseMessage = otCoapNewMessage(GetInstancePtr(), nullptr);
    VerifyOrExit(responseMessage != nullptr, error = OT_ERROR_NO_BUFS);

    SuccessOrExit(error = otCoapMessageInitResponse(responseMessage, aMessage,
                                                    otCoapMessageGetType(aMessage) == OT_COAP_TYPE_CONFIRMABLE
                                                        ? OT_COAP_TYPE_ACKNOWLEDGMENT
                                                        : OT_COAP_TYPE_NON_CONFIRMABLE,
                                                    responseCode));

    if (responseCode == OT_COAP_CODE_CONTENT)
    {
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        if (observePresent && (observe == 0))
        {
            SuccessOrExit(error = otCoapMessageAppendObserveOption(responseMessage, mObserveSerial++));
        }
#endif
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        if (!block2Present && !block1Present)
        {
#endif
            SuccessOrExit(error = otCoapMessageSetPayloadMarker(responseMessage));
            SuccessOrExit(error = otMessageAppend(responseMessage, mResourceContent,
                                                  static_cast<uint16_t>(strlen(mResourceContent))));
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
        }
#endif
    }

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    if (block1Present != block2Present) // logical XOR
    {
        if (block1Present)
        {
            // Echo back the Block1 option to acknowledge the final block of the PUT/POST
            SuccessOrExit(error =
                              otCoapMessageAppendBlock1Option(responseMessage, static_cast<uint32_t>(block1Value >> 4),
                                                              false, // M flag is always 0 for final response
                                                              static_cast<otCoapBlockSzx>(block1Value & 0x7)));
            // No payload marker is set: current implementation does not return a payload for POST, but just
            // treats it like a PUT.
        }
        else if (block2Present)
        {
            // Echo back the Block2 option in the first response to the GET, but with M=1
            SuccessOrExit(error =
                              otCoapMessageAppendBlock2Option(responseMessage, static_cast<uint32_t>(block2Value >> 4),
                                                              true, // M flag is always 1 for first response
                                                              static_cast<otCoapBlockSzx>(block2Value & 0x7)));
            SuccessOrExit(error = otCoapMessageSetPayloadMarker(responseMessage));
        }
        SuccessOrExit(error = otCoapSendResponseBlockWiseWithParameters(GetInstancePtr(), responseMessage, aMessageInfo,
                                                                        GetResponseTxParameters(), this,
                                                                        mResource.mTransmitHook));
    }
    else
    {
#endif
        SuccessOrExit(error = otCoapSendResponseWithParameters(GetInstancePtr(), responseMessage, aMessageInfo,
                                                               GetResponseTxParameters()));
#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
    }
#endif

exit:

    if (error != OT_ERROR_NONE)
    {
        if (responseMessage != nullptr)
        {
            OutputLine("coap send response error %d: %s", error, otThreadErrorToString(error));
            otMessageFree(responseMessage);
        }
    }
    else if (responseCode >= OT_COAP_CODE_RESPONSE_MIN)
    {
        OutputLine("coap response sent");
    }
}

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
void Coap::HandleNotificationAck(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
{
    static_cast<Coap *>(aContext)->HandleNotificationAck(aMessage, aMessageInfo, aError);
}

void Coap::HandleNotificationAck(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
{
    OT_UNUSED_VARIABLE(aMessage);

    switch (aError)
    {
    case OT_ERROR_NONE:
        if (aMessageInfo != nullptr)
        {
            OutputFormat("Received coap notification ACK from ");
            OutputIp6AddressLine(aMessageInfo->mPeerAddr);
        }
        break;

    default:
        // A single CON notification delivery failure will cancel the subscription. RFC7641 would also allow
        // retrying CON notification delivery more often - this is implementation-specific.
        OutputLine("coap notification delivery error %d: %s", aError, otThreadErrorToString(aError));
        CancelSubscriber();
        break;
    }
}
#endif // OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE

void Coap::HandleResponse(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
{
    static_cast<Coap *>(aContext)->HandleResponse(aMessage, aMessageInfo, aError);
}

void Coap::HandleResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, otError aError)
{
    if (aError != OT_ERROR_NONE)
    {
        OutputLine("coap receive response error %d: %s", aError, otThreadErrorToString(aError));
    }
    else if ((aMessageInfo != nullptr) && (aMessage != nullptr))
    {
#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        otCoapOptionIterator iterator;
#endif

        OutputFormat("coap response from ");
        OutputIp6Address(aMessageInfo->mPeerAddr);

#if OPENTHREAD_CONFIG_COAP_OBSERVE_API_ENABLE
        if (otCoapOptionIteratorInit(&iterator, aMessage) == OT_ERROR_NONE)
        {
            const otCoapOption *observeOpt =
                otCoapOptionIteratorGetFirstOptionMatching(&iterator, OT_COAP_OPTION_OBSERVE);

            if (observeOpt != nullptr)
            {
                uint64_t observeVal = 0;
                otError  error      = otCoapOptionIteratorGetOptionUintValue(&iterator, &observeVal);

                if (error == OT_ERROR_NONE)
                {
                    OutputFormat(" OBS=");
                    OutputUint64(observeVal);
                }
            }
        }
#endif
        PrintPayload(aMessage);
    }
}

#if OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE
otError Coap::BlockwiseReceiveHook(void          *aContext,
                                   const uint8_t *aBlock,
                                   uint32_t       aPosition,
                                   uint16_t       aBlockLength,
                                   bool           aMore,
                                   uint32_t       aTotalLength)
{
    return static_cast<Coap *>(aContext)->BlockwiseReceiveHook(aBlock, aPosition, aBlockLength, aMore, aTotalLength);
}

otError Coap::BlockwiseReceiveHook(const uint8_t *aBlock,
                                   uint32_t       aPosition,
                                   uint16_t       aBlockLength,
                                   bool           aMore,
                                   uint32_t       aTotalLength)
{
    OT_UNUSED_VARIABLE(aMore);
    OT_UNUSED_VARIABLE(aTotalLength);

    OutputLine("received block: Num %lu Len %u", ToUlong(aPosition / aBlockLength), aBlockLength);

    for (uint16_t i = 0; i < aBlockLength / 16; i++)
    {
        OutputBytesLine(&aBlock[i * 16], 16);
    }

    return OT_ERROR_NONE;
}

otError Coap::BlockwiseTransmitHook(void     *aContext,
                                    uint8_t  *aBlock,
                                    uint32_t  aPosition,
                                    uint16_t *aBlockLength,
                                    bool     *aMore)
{
    return static_cast<Coap *>(aContext)->BlockwiseTransmitHook(aBlock, aPosition, aBlockLength, aMore);
}

otError Coap::BlockwiseTransmitHook(uint8_t *aBlock, uint32_t aPosition, uint16_t *aBlockLength, bool *aMore)
{
    uint32_t blockCount = aPosition / *aBlockLength;

    // Send a random payload
    otRandomNonCryptoFillBuffer(aBlock, *aBlockLength);

    OutputLine("send block: Num %lu Len %u", ToUlong(blockCount), *aBlockLength);

    for (uint16_t i = 0; i < *aBlockLength / 16; i++)
    {
        OutputBytesLine(&aBlock[i * 16], 16);
    }

    if (blockCount >= mBlockCount - 1)
    {
        *aMore = false;
    }
    else
    {
        *aMore = true;
    }

    return OT_ERROR_NONE;
}
#endif // OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE

} // namespace Cli
} // namespace ot

#endif // OPENTHREAD_CONFIG_COAP_API_ENABLE
