/*
 *  Copyright (c) 2021, 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 TCP CLI tool.
 */

#include "openthread-core-config.h"

#include "cli_config.h"

#if OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE

#include "cli_tcp.hpp"

#include <openthread/nat64.h>
#include <openthread/tcp.h>

#include "cli/cli.hpp"
#include "common/encoding.hpp"
#include "common/timer.hpp"

#if OPENTHREAD_CONFIG_TLS_ENABLE
#include <mbedtls/debug.h>
#include <mbedtls/ecjpake.h>
#include "crypto/mbedtls.hpp"
#endif

namespace ot {
namespace Cli {

#if OPENTHREAD_CONFIG_TLS_ENABLE
const int TcpExample::sCipherSuites[] = {MBEDTLS_TLS_ECJPAKE_WITH_AES_128_CCM_8,
                                         MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8, 0};
#endif

TcpExample::TcpExample(otInstance *aInstance, OutputImplementer &aOutputImplementer)
    : Utils(aInstance, aOutputImplementer)
    , mInitialized(false)
    , mEndpointConnected(false)
    , mEndpointConnectedFastOpen(false)
    , mSendBusy(false)
    , mUseCircularSendBuffer(true)
    , mUseTls(false)
    , mTlsHandshakeComplete(false)
    , mBenchmarkBytesTotal(0)
    , mBenchmarkBytesUnsent(0)
    , mBenchmarkTimeUsed(0)
{
    mEndpointAndCircularSendBuffer.mEndpoint   = &mEndpoint;
    mEndpointAndCircularSendBuffer.mSendBuffer = &mSendBuffer;
}

#if OPENTHREAD_CONFIG_TLS_ENABLE
void TcpExample::MbedTlsDebugOutput(void *ctx, int level, const char *file, int line, const char *str)
{
    TcpExample &tcpExample = *static_cast<TcpExample *>(ctx);

    tcpExample.OutputLine("%s:%d:%d: %s", file, line, level, str);
}
#endif

/**
 * @cli tcp init
 * @code
 * tcp init tls
 * Done
 * @endcode
 * @cparam tcp init [@ca{mode}] [@ca{size}]
 * * The `mode` has three possible values:
 *   * `tls`: Specifies that the TCP connection between two nodes should also
 *     use the TLS protocol on top of TCP. When two nodes communicate over TCP,
 *     both nodes must either use TLS or neither node should use TLS because
 *     a non-TLS endpoint cannot communicate with a TLS endpoint.
 *   * `linked` or `circular`: Either one of these options means that TLS
 *     is not to be used, and the specified buffering type should be used for TCP
 *     buffering. The behavior of `linked` and `circular` is identical. Examine the code
 *     for the differences between these two buffering types.
 *     Two endpoints of a TCP connection are not required to use the same buffering type.
 * * The `size` parameter sets the size of the receive buffer to associate with the
 *   example TCP endpoint. If left unspecified, the maximum size is used. The
 *   maximum size is set in `OPENTHREAD_CONFIG_CLI_TCP_RECEIVE_BUFFER_SIZE`.
 * @par
 * Initializes the example TCP listener and the example TCP endpoint provided
 * by the `tcp` CLI.
 * @sa otTcpListenerInitialize
 * @sa otTcpEndpointInitialize
 */
template <> otError TcpExample::Process<Cmd("init")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;
    size_t  receiveBufferSize;

    VerifyOrExit(!mInitialized, error = OT_ERROR_ALREADY);

    if (aArgs[0].IsEmpty())
    {
        mUseCircularSendBuffer = true;
        mUseTls                = false;
        receiveBufferSize      = sizeof(mReceiveBufferBytes);
    }
    else
    {
        if (aArgs[0] == "linked")
        {
            mUseCircularSendBuffer = false;
            mUseTls                = false;
        }
        else if (aArgs[0] == "circular")
        {
            mUseCircularSendBuffer = true;
            mUseTls                = false;
        }
#if OPENTHREAD_CONFIG_TLS_ENABLE
        else if (aArgs[0] == "tls")
        {
            mUseCircularSendBuffer = true;
            mUseTls                = true;

            mbedtls_x509_crt_init(&mSrvCert);
            mbedtls_pk_init(&mPKey);

            mbedtls_ssl_init(&mSslContext);
            mbedtls_ssl_config_init(&mSslConfig);
            mbedtls_ssl_conf_rng(&mSslConfig, Crypto::MbedTls::CryptoSecurePrng, nullptr);
            mbedtls_ssl_conf_authmode(&mSslConfig, MBEDTLS_SSL_VERIFY_NONE);
            mbedtls_ssl_conf_ciphersuites(&mSslConfig, sCipherSuites);

#if (MBEDTLS_VERSION_NUMBER >= 0x03020000)
            mbedtls_ssl_conf_min_tls_version(&mSslConfig, MBEDTLS_SSL_VERSION_TLS1_2);
            mbedtls_ssl_conf_max_tls_version(&mSslConfig, MBEDTLS_SSL_VERSION_TLS1_2);
#else
            mbedtls_ssl_conf_min_version(&mSslConfig, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
            mbedtls_ssl_conf_max_version(&mSslConfig, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
#endif

#if (MBEDTLS_VERSION_NUMBER >= 0x03000000)
#include "crypto/mbedtls.hpp"
            int rv = mbedtls_pk_parse_key(&mPKey, reinterpret_cast<const unsigned char *>(sSrvKey), sSrvKeyLength,
                                          nullptr, 0, Crypto::MbedTls::CryptoSecurePrng, nullptr);
#else
            int rv = mbedtls_pk_parse_key(&mPKey, reinterpret_cast<const unsigned char *>(sSrvKey), sSrvKeyLength,
                                          nullptr, 0);
#endif
            if (rv != 0)
            {
                OutputLine("mbedtls_pk_parse_key returned %d", rv);
            }

            rv = mbedtls_x509_crt_parse(&mSrvCert, reinterpret_cast<const unsigned char *>(sSrvPem), sSrvPemLength);
            if (rv != 0)
            {
                OutputLine("mbedtls_x509_crt_parse (1) returned %d", rv);
            }
            rv = mbedtls_x509_crt_parse(&mSrvCert, reinterpret_cast<const unsigned char *>(sCasPem), sCasPemLength);
            if (rv != 0)
            {
                OutputLine("mbedtls_x509_crt_parse (2) returned %d", rv);
            }
            rv = mbedtls_ssl_setup(&mSslContext, &mSslConfig);
            if (rv != 0)
            {
                OutputLine("mbedtls_ssl_setup returned %d", rv);
            }
        }
#endif // OPENTHREAD_CONFIG_TLS_ENABLE
        else
        {
            ExitNow(error = OT_ERROR_INVALID_ARGS);
        }

        if (aArgs[1].IsEmpty())
        {
            receiveBufferSize = sizeof(mReceiveBufferBytes);
        }
        else
        {
            uint32_t windowSize;

            SuccessOrExit(error = aArgs[1].ParseAsUint32(windowSize));

            receiveBufferSize = windowSize + ((windowSize + 7) >> 3);
            VerifyOrExit(receiveBufferSize <= sizeof(mReceiveBufferBytes) && receiveBufferSize != 0,
                         error = OT_ERROR_INVALID_ARGS);
        }
    }

    otTcpCircularSendBufferInitialize(&mSendBuffer, mSendBufferBytes, sizeof(mSendBufferBytes));

    {
        otTcpEndpointInitializeArgs endpointArgs;

        ClearAllBytes(endpointArgs);
        endpointArgs.mEstablishedCallback = HandleTcpEstablishedCallback;

        if (mUseCircularSendBuffer)
        {
            endpointArgs.mForwardProgressCallback = HandleTcpForwardProgressCallback;
        }
        else
        {
            endpointArgs.mSendDoneCallback = HandleTcpSendDoneCallback;
        }

        endpointArgs.mReceiveAvailableCallback = HandleTcpReceiveAvailableCallback;
        endpointArgs.mDisconnectedCallback     = HandleTcpDisconnectedCallback;
        endpointArgs.mContext                  = this;
        endpointArgs.mReceiveBuffer            = mReceiveBufferBytes;
        endpointArgs.mReceiveBufferSize        = receiveBufferSize;

        SuccessOrExit(error = otTcpEndpointInitialize(GetInstancePtr(), &mEndpoint, &endpointArgs));
    }

    {
        otTcpListenerInitializeArgs listenerArgs;

        ClearAllBytes(listenerArgs);
        listenerArgs.mAcceptReadyCallback = HandleTcpAcceptReadyCallback;
        listenerArgs.mAcceptDoneCallback  = HandleTcpAcceptDoneCallback;
        listenerArgs.mContext             = this;

        error = otTcpListenerInitialize(GetInstancePtr(), &mListener, &listenerArgs);

        if (error != OT_ERROR_NONE)
        {
            IgnoreReturnValue(otTcpEndpointDeinitialize(&mEndpoint));
            ExitNow();
        }
    }

    mInitialized = true;

exit:
    if (error != OT_ERROR_NONE)
    {
#if OPENTHREAD_CONFIG_TLS_ENABLE
        if (mUseTls)
        {
            mbedtls_ssl_config_free(&mSslConfig);
            mbedtls_ssl_free(&mSslContext);

            mbedtls_pk_free(&mPKey);
            mbedtls_x509_crt_free(&mSrvCert);
        }
#endif // OPENTHREAD_CONFIG_TLS_ENABLE

        otTcpCircularSendBufferForceDiscardAll(&mSendBuffer);
        OT_UNUSED_VARIABLE(otTcpCircularSendBufferDeinitialize(&mSendBuffer));
    }

    return error;
}

/**
 * @cli tcp deinit
 * @code
 * tcp deinit
 * Done
 * @endcode
 * @par api_copy
 * #otTcpEndpointDeinitialize
 */
template <> otError TcpExample::Process<Cmd("deinit")>(Arg aArgs[])
{
    otError error = OT_ERROR_NONE;
    otError endpointError;
    otError bufferError;
    otError listenerError;

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

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls)
    {
        mbedtls_ssl_config_free(&mSslConfig);
        mbedtls_ssl_free(&mSslContext);

        mbedtls_pk_free(&mPKey);
        mbedtls_x509_crt_free(&mSrvCert);

        mUseTls = false;
    }
#endif

    endpointError = otTcpEndpointDeinitialize(&mEndpoint);
    mSendBusy     = false;

    otTcpCircularSendBufferForceDiscardAll(&mSendBuffer);
    bufferError = otTcpCircularSendBufferDeinitialize(&mSendBuffer);

    listenerError = otTcpListenerDeinitialize(&mListener);
    mInitialized  = false;

    SuccessOrExit(error = endpointError);
    SuccessOrExit(error = bufferError);
    SuccessOrExit(error = listenerError);

exit:
    return error;
}

/**
 * @cli tcp bind
 * @code
 * tcp bind :: 30000
 * Done
 * @endcode
 * @cparam tcp bind @ca{ip} @ca{port}
 * * `ip`: IPv6 address to bind to. If you wish to have the TCP/IPv6 stack assign
 *   the binding IPv6 address, use the unspecified IPv6 address: `::`.
 * * `port`: TCP port number to bind to.
 * @par
 * Associates an IPv6 address and a port to the example TCP endpoint provided by
 * the `tcp` CLI. Associating the TCP endpoint to an IPv6
 * address and port is referred to as "naming the TCP endpoint." This binds the
 * endpoint for communication. @moreinfo{@tcp}.
 * @sa otTcpBind
 */
template <> otError TcpExample::Process<Cmd("bind")>(Arg aArgs[])
{
    otError    error;
    otSockAddr sockaddr;

    VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);

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

    error = otTcpBind(&mEndpoint, &sockaddr);

exit:
    return error;
}

/**
 * @cli tcp connect
 * @code
 * tcp connect fe80:0:0:0:a8df:580a:860:ffa4 30000
 * Done
 * TCP: Connection established
 * @endcode
 * @code
 * tcp connect 172.17.0.1 1234
 * Connecting to synthesized IPv6 address: fdde:ad00:beef:2:0:0:ac11:1
 * Done
 * @endcode
 * @cparam tcp connect @ca{ip} @ca{port} [@ca{fastopen}]
 * * `ip`: IP address of the peer The address can be an IPv4 address,
 *   which gets synthesized to an IPv6 address using the preferred
 *   NAT64 prefix from the network data. The command returns `InvalidState`
 *   when the preferred NAT64 prefix is unavailable.
 * * `port`: TCP port number of the peer.
 * * `fastopen`: This parameter is optional. If set to `fast`, TCP Fast Open is enabled
 *   for this connection. Otherwise, if this parameter is set to `slow` or not used,
 *   TCP Fast Open is disabled.
 * @par
 * Establishes a connection with the specified peer.
 * @par
 * If the connection establishment is successful, the resulting TCP connection
 * is associated with the example TCP endpoint. @moreinfo{@tcp}.
 * @sa otTcpConnect
 */
template <> otError TcpExample::Process<Cmd("connect")>(Arg aArgs[])
{
    otError    error;
    otSockAddr sockaddr;
    bool       nat64Synth;
    uint32_t   flags;

    VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);

    SuccessOrExit(error = ParseToIp6Address(GetInstancePtr(), aArgs[0], sockaddr.mAddress, nat64Synth));

    if (nat64Synth)
    {
        OutputFormat("Connecting to synthesized IPv6 address: ");
        OutputIp6AddressLine(sockaddr.mAddress);
    }

    SuccessOrExit(error = aArgs[1].ParseAsUint16(sockaddr.mPort));

    if (aArgs[2].IsEmpty())
    {
        flags = OT_TCP_CONNECT_NO_FAST_OPEN;
    }
    else
    {
        if (aArgs[2] == "slow")
        {
            flags = OT_TCP_CONNECT_NO_FAST_OPEN;
        }
        else if (aArgs[2] == "fast")
        {
            flags = 0;
        }
        else
        {
            ExitNow(error = OT_ERROR_INVALID_ARGS);
        }

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

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls)
    {
        int rv = mbedtls_ssl_config_defaults(&mSslConfig, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
                                             MBEDTLS_SSL_PRESET_DEFAULT);
        if (rv != 0)
        {
            OutputLine("mbedtls_ssl_config_defaults returned %d", rv);
        }
    }
#endif

    SuccessOrExit(error = otTcpConnect(&mEndpoint, &sockaddr, flags));
    mEndpointConnected         = true;
    mEndpointConnectedFastOpen = ((flags & OT_TCP_CONNECT_NO_FAST_OPEN) == 0);

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls && mEndpointConnectedFastOpen)
    {
        PrepareTlsHandshake();
        ContinueTlsHandshake();
    }
#endif

exit:
    return error;
}

/**
 * @cli tcp send
 * @code
 * tcp send hello
 * Done
 * @endcode
 * @code
 * tcp send -x 68656c6c6f
 * Done
 * @endcode
 * @cparam tcp send [@ca{type}] @ca{message}
 * The `message` parameter contains the message you want to send to the
 * remote TCP endpoint.
 * If `type` is `-x`, then
 * binary data in hexadecimal representation is given in the `message` parameter.
 * @par
 * Sends data over the TCP connection associated with the example TCP endpoint
 * that is provided with the `tcp` CLI. @moreinfo{@tcp}.
 */
template <> otError TcpExample::Process<Cmd("send")>(Arg aArgs[])
{
    static constexpr uint16_t kBufferSizeForHexData = 128;

    otError  error;
    uint16_t dataLen;
    uint8_t *data;
    uint8_t  buf[kBufferSizeForHexData];

    VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);
    VerifyOrExit(mBenchmarkBytesTotal == 0, error = OT_ERROR_BUSY);
    VerifyOrExit(!aArgs[0].IsEmpty(), error = OT_ERROR_INVALID_ARGS);

    if (aArgs[0] == "-x" && !aArgs[1].IsEmpty())
    {
        // Binary hex data payload
        dataLen = sizeof(buf);
        data    = &buf[0];
        SuccessOrExit(error = aArgs[1].ParseAsHexString(dataLen, buf));
    }
    else
    {
        VerifyOrExit(aArgs[1].IsEmpty(), error = OT_ERROR_INVALID_ARGS);
        dataLen = aArgs[0].GetLength();
        data    = reinterpret_cast<uint8_t *>(aArgs[0].GetCString());
    }

    if (mUseCircularSendBuffer)
    {
#if OPENTHREAD_CONFIG_TLS_ENABLE
        if (mUseTls)
        {
            int rv = mbedtls_ssl_write(&mSslContext, data, dataLen);

            if (rv < 0 && rv != MBEDTLS_ERR_SSL_WANT_WRITE && rv != MBEDTLS_ERR_SSL_WANT_READ)
            {
                ExitNow(error = OT_ERROR_FAILED);
            }

            error = OT_ERROR_NONE;
        }
        else
#endif
        {
            size_t written;

            SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, data, dataLen, &written, 0));
        }
    }
    else
    {
        VerifyOrExit(!mSendBusy, error = OT_ERROR_BUSY);

        mSendLink.mNext   = nullptr;
        mSendLink.mData   = mSendBufferBytes;
        mSendLink.mLength = OT_MIN(dataLen, sizeof(mSendBufferBytes));
        memcpy(mSendBufferBytes, data, mSendLink.mLength);

        SuccessOrExit(error = otTcpSendByReference(&mEndpoint, &mSendLink, 0));
        mSendBusy = true;
    }

exit:
    return error;
}

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

    /**
     * @cli tcp benchmark result
     * @code
     * tcp benchmark result
     * TCP Benchmark Status: Ongoing
     * Done
     * @endcode
     * @code
     * tcp benchmark result
     * TCP Benchmark Status: Completed
     * TCP Benchmark Complete: Transferred 73728 bytes in 7056 milliseconds
     * TCP Goodput: 83.592 kb/s
     * @endcode
     * @par
     * Shows the latest result of the TCP benchmark test. Possible status values:
     * * Ongoing
     * * Completed
     * * Untested
     * @par
     * This command is primarily intended for creating scripts that automate
     * the TCP benchmark test.
     */
    if (aArgs[0] == "result")
    {
        OutputFormat("TCP Benchmark Status: ");

        if (mBenchmarkBytesTotal != 0)
        {
            OutputLine("Ongoing");
        }
        else if (mBenchmarkTimeUsed != 0)
        {
            OutputLine("Completed");
            OutputBenchmarkResult();
        }
        else
        {
            OutputLine("Untested");
        }
    }
    /**
     * @cli tcp benchmark run
     * @code
     * tcp benchmark run
     * Done
     * TCP Benchmark Complete: Transferred 73728 bytes in 7233 milliseconds
     * TCP Goodput: 81.546 kb/s
     * @endcode
     * @cparam tcp benchmark run [@ca{size}]
     * Use the `size` parameter to specify the number of bytes to send
     * for the benchmark. If you do not use the `size` parameter, the default
     * value (`OPENTHREAD_CONFIG_CLI_TCP_DEFAULT_BENCHMARK_SIZE`) is used.
     * @par
     * Transfers the specified number of bytes using the TCP connection
     * currently associated with the example TCP endpoint provided by the `tcp` CLI.
     * @note You must establish a TCP connection before you run this command.
     */
    else if (aArgs[0] == "run")
    {
        VerifyOrExit(!mSendBusy, error = OT_ERROR_BUSY);
        VerifyOrExit(mBenchmarkBytesTotal == 0, error = OT_ERROR_BUSY);

        if (aArgs[1].IsEmpty())
        {
            mBenchmarkBytesTotal = OPENTHREAD_CONFIG_CLI_TCP_DEFAULT_BENCHMARK_SIZE;
        }
        else
        {
            SuccessOrExit(error = aArgs[1].ParseAsUint32(mBenchmarkBytesTotal));
            VerifyOrExit(mBenchmarkBytesTotal != 0, error = OT_ERROR_INVALID_ARGS);
        }

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

        mBenchmarkStart       = TimerMilli::GetNow();
        mBenchmarkBytesUnsent = mBenchmarkBytesTotal;

        if (mUseCircularSendBuffer)
        {
            SuccessOrExit(error = ContinueBenchmarkCircularSend());
        }
        else
        {
            uint32_t benchmarkLinksLeft =
                (mBenchmarkBytesTotal + sizeof(mSendBufferBytes) - 1) / sizeof(mSendBufferBytes);
            uint32_t toSendOut = OT_MIN(OT_ARRAY_LENGTH(mBenchmarkLinks), benchmarkLinksLeft);

            /* We could also point the linked buffers directly to sBenchmarkData. */
            memset(mSendBufferBytes, 'a', sizeof(mSendBufferBytes));

            for (uint32_t i = 0; i != toSendOut; i++)
            {
                mBenchmarkLinks[i].mNext   = nullptr;
                mBenchmarkLinks[i].mData   = mSendBufferBytes;
                mBenchmarkLinks[i].mLength = sizeof(mSendBufferBytes);

                if (i == 0 && mBenchmarkBytesTotal % sizeof(mSendBufferBytes) != 0)
                {
                    mBenchmarkLinks[i].mLength = mBenchmarkBytesTotal % sizeof(mSendBufferBytes);
                }

                error = otTcpSendByReference(&mEndpoint, &mBenchmarkLinks[i],
                                             i == toSendOut - 1 ? 0 : OT_TCP_SEND_MORE_TO_COME);
                VerifyOrExit(error == OT_ERROR_NONE, mBenchmarkBytesTotal = 0);
            }
        }
    }
    else
    {
        error = OT_ERROR_INVALID_ARGS;
    }

exit:
    return error;
}

/**
 * @cli tcp sendend
 * @code
 * tcp sendend
 * Done
 * @endcode
 * @par
 * Sends the "end of stream" signal over the TCP connection
 * associated with the example TCP endpoint provided by the `tcp` CLI. This
 * alerts the peer that it will not receive any more data over this TCP connection.
 * @sa otTcpSendEndOfStream
 */
template <> otError TcpExample::Process<Cmd("sendend")>(Arg aArgs[])
{
    otError error;

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

    error = otTcpSendEndOfStream(&mEndpoint);

exit:
    return error;
}

/**
 * @cli tcp abort
 * @code
 * tcp abort
 * TCP: Connection reset
 * Done
 * @endcode
 * @par
 * Unceremoniously ends the TCP connection associated with the
 * example TCP endpoint, transitioning the TCP endpoint to the closed state.
 * @sa otTcpAbort
 */
template <> otError TcpExample::Process<Cmd("abort")>(Arg aArgs[])
{
    otError error;

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

    SuccessOrExit(error = otTcpAbort(&mEndpoint));
    mEndpointConnected         = false;
    mEndpointConnectedFastOpen = false;

exit:
    return error;
}

/**
 * @cli tcp listen
 * @code
 * tcp listen :: 30000
 * Done
 * @endcode
 * @cparam tcp listen @ca{ip} @ca{port}
 * The following parameters are required:
 * * `ip`: IPv6 address or the unspecified IPv6 address (`::`) of the example
 *   TCP listener provided by the `tcp` CLI.
 * * `port`: TCP port of the example TCP listener.
 *   If no TCP connection is associated with the example TCP endpoint, then any
 *   incoming connections matching the specified IPv6 address and port are accepted
 *   and are associated with the example TCP endpoint.
 * @par
 * Uses the example TCP listener to listen for incoming connections on the
 * specified IPv6 address and port. @moreinfo{@tcp}.
 * @sa otTcpListen
 */
template <> otError TcpExample::Process<Cmd("listen")>(Arg aArgs[])
{
    otError    error;
    otSockAddr sockaddr;

    VerifyOrExit(mInitialized, error = OT_ERROR_INVALID_STATE);

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

    SuccessOrExit(error = otTcpStopListening(&mListener));
    error = otTcpListen(&mListener, &sockaddr);

exit:
    return error;
}

/**
 * @cli tcp stoplistening
 * @code
 * tcp stoplistening
 * Done
 * @endcode
 * @par
 * Instructs the example TCP listener to stop listening for incoming TCP connections.
 * @sa otTcpStopListening
 */
template <> otError TcpExample::Process<Cmd("stoplistening")>(Arg aArgs[])
{
    otError error;

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

    error = otTcpStopListening(&mListener);

exit:
    return error;
}

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

    static constexpr Command kCommands[] = {
        CmdEntry("abort"), CmdEntry("benchmark"), CmdEntry("bind"), CmdEntry("connect"), CmdEntry("deinit"),
        CmdEntry("init"),  CmdEntry("listen"),    CmdEntry("send"), CmdEntry("sendend"), CmdEntry("stoplistening"),
    };

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

void TcpExample::HandleTcpEstablishedCallback(otTcpEndpoint *aEndpoint)
{
    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpEstablished(aEndpoint);
}

void TcpExample::HandleTcpSendDoneCallback(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
{
    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpSendDone(aEndpoint, aData);
}

void TcpExample::HandleTcpForwardProgressCallback(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog)
{
    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))
        ->HandleTcpForwardProgress(aEndpoint, aInSendBuffer, aBacklog);
}

void TcpExample::HandleTcpReceiveAvailableCallback(otTcpEndpoint *aEndpoint,
                                                   size_t         aBytesAvailable,
                                                   bool           aEndOfStream,
                                                   size_t         aBytesRemaining)
{
    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))
        ->HandleTcpReceiveAvailable(aEndpoint, aBytesAvailable, aEndOfStream, aBytesRemaining);
}

void TcpExample::HandleTcpDisconnectedCallback(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason)
{
    static_cast<TcpExample *>(otTcpEndpointGetContext(aEndpoint))->HandleTcpDisconnected(aEndpoint, aReason);
}

otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReadyCallback(otTcpListener    *aListener,
                                                                       const otSockAddr *aPeer,
                                                                       otTcpEndpoint   **aAcceptInto)
{
    return static_cast<TcpExample *>(otTcpListenerGetContext(aListener))
        ->HandleTcpAcceptReady(aListener, aPeer, aAcceptInto);
}

void TcpExample::HandleTcpAcceptDoneCallback(otTcpListener    *aListener,
                                             otTcpEndpoint    *aEndpoint,
                                             const otSockAddr *aPeer)
{
    static_cast<TcpExample *>(otTcpListenerGetContext(aListener))->HandleTcpAcceptDone(aListener, aEndpoint, aPeer);
}

void TcpExample::HandleTcpEstablished(otTcpEndpoint *aEndpoint)
{
    OT_UNUSED_VARIABLE(aEndpoint);
    OutputLine("TCP: Connection established");
#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls && !mEndpointConnectedFastOpen)
    {
        PrepareTlsHandshake();
        ContinueTlsHandshake();
    }
#endif
}

void TcpExample::HandleTcpSendDone(otTcpEndpoint *aEndpoint, otLinkedBuffer *aData)
{
    OT_UNUSED_VARIABLE(aEndpoint);
    OT_ASSERT(!mUseCircularSendBuffer); // this callback is not used when using the circular send buffer

    if (mBenchmarkBytesTotal == 0)
    {
        // If the benchmark encountered an error, we might end up here. So,
        // tolerate some benchmark links finishing in this case.
        if (aData == &mSendLink)
        {
            OT_ASSERT(mSendBusy);
            mSendBusy = false;
        }
    }
    else
    {
        OT_ASSERT(aData != &mSendLink);
        OT_ASSERT(mBenchmarkBytesUnsent >= aData->mLength);

        mBenchmarkBytesUnsent -= aData->mLength; // could be less than sizeof(mSendBufferBytes) for the first link

        if (mBenchmarkBytesUnsent >= OT_ARRAY_LENGTH(mBenchmarkLinks) * sizeof(mSendBufferBytes))
        {
            aData->mLength = sizeof(mSendBufferBytes);

            if (otTcpSendByReference(&mEndpoint, aData, 0) != OT_ERROR_NONE)
            {
                OutputLine("TCP Benchmark Failed");
                mBenchmarkBytesTotal = 0;
            }
        }
        else if (mBenchmarkBytesUnsent == 0)
        {
            CompleteBenchmark();
        }
    }
}

void TcpExample::HandleTcpForwardProgress(otTcpEndpoint *aEndpoint, size_t aInSendBuffer, size_t aBacklog)
{
    OT_UNUSED_VARIABLE(aEndpoint);
    OT_UNUSED_VARIABLE(aBacklog);
    OT_ASSERT(mUseCircularSendBuffer); // this callback is only used when using the circular send buffer

    otTcpCircularSendBufferHandleForwardProgress(&mSendBuffer, aInSendBuffer);

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls)
    {
        ContinueTlsHandshake();
    }
#endif

    /* Handle case where we're in a benchmark. */
    if (mBenchmarkBytesTotal != 0)
    {
        if (mBenchmarkBytesUnsent != 0)
        {
            /* Continue sending out data if there's data we haven't sent. */
            IgnoreError(ContinueBenchmarkCircularSend());
        }
        else if (aInSendBuffer == 0)
        {
            /* Handle case where all data is sent out and the send buffer has drained. */
            CompleteBenchmark();
        }
    }
}

void TcpExample::HandleTcpReceiveAvailable(otTcpEndpoint *aEndpoint,
                                           size_t         aBytesAvailable,
                                           bool           aEndOfStream,
                                           size_t         aBytesRemaining)
{
    OT_UNUSED_VARIABLE(aBytesRemaining);
    OT_ASSERT(aEndpoint == &mEndpoint);

    /* If we get data before the handshake completes, then this is a TFO connection. */
    if (!mEndpointConnected)
    {
        mEndpointConnected         = true;
        mEndpointConnectedFastOpen = true;

#if OPENTHREAD_CONFIG_TLS_ENABLE
        if (mUseTls)
        {
            PrepareTlsHandshake();
        }
#endif
    }

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls && ContinueTlsHandshake())
    {
        ExitNow();
    }
#endif

    if ((mTlsHandshakeComplete || !mUseTls) && aBytesAvailable > 0)
    {
#if OPENTHREAD_CONFIG_TLS_ENABLE
        if (mUseTls)
        {
            uint8_t buffer[500];

            for (;;)
            {
                int rv = mbedtls_ssl_read(&mSslContext, buffer, sizeof(buffer));

                if (rv < 0)
                {
                    if (rv == MBEDTLS_ERR_SSL_WANT_READ)
                    {
                        break;
                    }

                    OutputLine("TLS receive failure: %d", rv);
                }
                else
                {
                    OutputLine("TLS: Received %u bytes: %.*s", static_cast<unsigned>(rv), rv,
                               reinterpret_cast<const char *>(buffer));
                }
            }
            OutputLine("(TCP: Received %u bytes)", static_cast<unsigned>(aBytesAvailable));
        }
        else
#endif // OPENTHREAD_CONFIG_TLS_ENABLE
        {
            const otLinkedBuffer *data;
            size_t                totalReceived = 0;

            IgnoreError(otTcpReceiveByReference(aEndpoint, &data));

            for (; data != nullptr; data = data->mNext)
            {
                OutputLine("TCP: Received %u bytes: %.*s", static_cast<unsigned>(data->mLength),
                           static_cast<unsigned>(data->mLength), reinterpret_cast<const char *>(data->mData));
                totalReceived += data->mLength;
            }

            OT_ASSERT(aBytesAvailable == totalReceived);
            IgnoreReturnValue(otTcpCommitReceive(aEndpoint, totalReceived, 0));
        }
    }

    if (aEndOfStream)
    {
        OutputLine("TCP: Reached end of stream");
    }

    ExitNow();

exit:
    return;
}

void TcpExample::HandleTcpDisconnected(otTcpEndpoint *aEndpoint, otTcpDisconnectedReason aReason)
{
    static const char *const kReasonStrings[] = {
        "Disconnected",            // (0) OT_TCP_DISCONNECTED_REASON_NORMAL
        "Connection refused",      // (1) OT_TCP_DISCONNECTED_REASON_REFUSED
        "Connection reset",        // (2) OT_TCP_DISCONNECTED_REASON_RESET
        "Entered TIME-WAIT state", // (3) OT_TCP_DISCONNECTED_REASON_TIME_WAIT
        "Connection timed out",    // (4) OT_TCP_DISCONNECTED_REASON_TIMED_OUT
    };

    OT_UNUSED_VARIABLE(aEndpoint);

    static_assert(0 == OT_TCP_DISCONNECTED_REASON_NORMAL, "OT_TCP_DISCONNECTED_REASON_NORMAL value is incorrect");
    static_assert(1 == OT_TCP_DISCONNECTED_REASON_REFUSED, "OT_TCP_DISCONNECTED_REASON_REFUSED value is incorrect");
    static_assert(2 == OT_TCP_DISCONNECTED_REASON_RESET, "OT_TCP_DISCONNECTED_REASON_RESET value is incorrect");
    static_assert(3 == OT_TCP_DISCONNECTED_REASON_TIME_WAIT, "OT_TCP_DISCONNECTED_REASON_TIME_WAIT value is incorrect");
    static_assert(4 == OT_TCP_DISCONNECTED_REASON_TIMED_OUT, "OT_TCP_DISCONNECTED_REASON_TIMED_OUT value is incorrect");

    OutputLine("TCP: %s", Stringify(aReason, kReasonStrings));

#if OPENTHREAD_CONFIG_TLS_ENABLE
    if (mUseTls)
    {
        mbedtls_ssl_session_reset(&mSslContext);
    }
#endif

    // We set this to false even for the TIME-WAIT state, so that we can reuse
    // the active socket if an incoming connection comes in instead of waiting
    // for the 2MSL timeout.
    mEndpointConnected         = false;
    mEndpointConnectedFastOpen = false;
    mSendBusy                  = false;

    // Mark the benchmark as inactive if the connection was disconnected.
    mBenchmarkBytesTotal  = 0;
    mBenchmarkBytesUnsent = 0;

    otTcpCircularSendBufferForceDiscardAll(&mSendBuffer);
}

otTcpIncomingConnectionAction TcpExample::HandleTcpAcceptReady(otTcpListener    *aListener,
                                                               const otSockAddr *aPeer,
                                                               otTcpEndpoint   **aAcceptInto)
{
    otTcpIncomingConnectionAction action;

    OT_UNUSED_VARIABLE(aListener);

    if (mEndpointConnected)
    {
        OutputFormat("TCP: Ignoring incoming connection request from ");
        OutputSockAddr(*aPeer);
        OutputLine(" (active socket is busy)");

        ExitNow(action = OT_TCP_INCOMING_CONNECTION_ACTION_DEFER);
    }

    *aAcceptInto = &mEndpoint;
    action       = OT_TCP_INCOMING_CONNECTION_ACTION_ACCEPT;

#if OPENTHREAD_CONFIG_TLS_ENABLE
    /*
     * Natural to wait until the AcceptDone callback but with TFO we could get data before that
     * so it doesn't make sense to wait until then.
     */
    if (mUseTls)
    {
        int rv;

        rv = mbedtls_ssl_config_defaults(&mSslConfig, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM,
                                         MBEDTLS_SSL_PRESET_DEFAULT);
        if (rv != 0)
        {
            OutputLine("mbedtls_ssl_config_defaults returned %d", rv);
        }

        mbedtls_ssl_conf_ca_chain(&mSslConfig, mSrvCert.next, nullptr);
        rv = mbedtls_ssl_conf_own_cert(&mSslConfig, &mSrvCert, &mPKey);

        if (rv != 0)
        {
            OutputLine("mbedtls_ssl_conf_own_cert returned %d", rv);
        }
    }
#endif // OPENTHREAD_CONFIG_TLS_ENABLE

exit:
    return action;
}

void TcpExample::HandleTcpAcceptDone(otTcpListener *aListener, otTcpEndpoint *aEndpoint, const otSockAddr *aPeer)
{
    OT_UNUSED_VARIABLE(aListener);
    OT_UNUSED_VARIABLE(aEndpoint);

    mEndpointConnected = true;
    OutputFormat("Accepted connection from ");
    OutputSockAddrLine(*aPeer);
}

otError TcpExample::ContinueBenchmarkCircularSend(void)
{
    otError error = OT_ERROR_NONE;
    size_t  freeSpace;

    while (mBenchmarkBytesUnsent != 0 && (freeSpace = otTcpCircularSendBufferGetFreeSpace(&mSendBuffer)) != 0)
    {
        size_t   toSendThisIteration = OT_MIN(mBenchmarkBytesUnsent, sBenchmarkDataLength);
        uint32_t flag                = (toSendThisIteration < freeSpace && toSendThisIteration < mBenchmarkBytesUnsent)
                                           ? OT_TCP_CIRCULAR_SEND_BUFFER_WRITE_MORE_TO_COME
                                           : 0;
        size_t   written             = 0;

#if OPENTHREAD_CONFIG_TLS_ENABLE
        if (mUseTls)
        {
            int rv = mbedtls_ssl_write(&mSslContext, reinterpret_cast<const unsigned char *>(sBenchmarkData),
                                       toSendThisIteration);

            if (rv > 0)
            {
                written = static_cast<size_t>(rv);
                OT_ASSERT(written <= mBenchmarkBytesUnsent);
            }
            else if (rv != MBEDTLS_ERR_SSL_WANT_WRITE && rv != MBEDTLS_ERR_SSL_WANT_READ)
            {
                ExitNow(error = OT_ERROR_FAILED);
            }

            error = OT_ERROR_NONE;
        }
        else
#endif
        {
            SuccessOrExit(error = otTcpCircularSendBufferWrite(&mEndpoint, &mSendBuffer, sBenchmarkData,
                                                               toSendThisIteration, &written, flag));
        }
        mBenchmarkBytesUnsent -= written;
    }

exit:
    if (error != OT_ERROR_NONE)
    {
        OutputLine("TCP Benchmark Failed");
        mBenchmarkBytesTotal  = 0;
        mBenchmarkBytesUnsent = 0;
    }

    return error;
}

void TcpExample::OutputBenchmarkResult(void)
{
    uint32_t thousandTimesGoodput =
        (1000 * (mBenchmarkLastBytesTotal << 3) + (mBenchmarkTimeUsed >> 1)) / mBenchmarkTimeUsed;

    OutputLine("TCP Benchmark Complete: Transferred %lu bytes in %lu milliseconds", ToUlong(mBenchmarkLastBytesTotal),
               ToUlong(mBenchmarkTimeUsed));
    OutputLine("TCP Goodput: %lu.%03u kb/s", ToUlong(thousandTimesGoodput / 1000),
               static_cast<uint16_t>(thousandTimesGoodput % 1000));
}

void TcpExample::CompleteBenchmark(void)
{
    mBenchmarkTimeUsed       = TimerMilli::GetNow() - mBenchmarkStart;
    mBenchmarkLastBytesTotal = mBenchmarkBytesTotal;

    OutputBenchmarkResult();

    mBenchmarkBytesTotal = 0;
}

#if OPENTHREAD_CONFIG_TLS_ENABLE
void TcpExample::PrepareTlsHandshake(void)
{
    int rv;

    rv = mbedtls_ssl_set_hostname(&mSslContext, "localhost");

    if (rv != 0)
    {
        OutputLine("mbedtls_ssl_set_hostname returned %d", rv);
    }

    rv = mbedtls_ssl_set_hs_ecjpake_password(&mSslContext, reinterpret_cast<const unsigned char *>(sEcjpakePassword),
                                             sEcjpakePasswordLength);
    if (rv != 0)
    {
        OutputLine("mbedtls_ssl_set_hs_ecjpake_password returned %d", rv);
    }

    mbedtls_ssl_set_bio(&mSslContext, &mEndpointAndCircularSendBuffer, otTcpMbedTlsSslSendCallback,
                        otTcpMbedTlsSslRecvCallback, nullptr);
    mTlsHandshakeComplete = false;
}

bool TcpExample::ContinueTlsHandshake(void)
{
    bool wasNotAlreadyDone = false;
    int  rv;

    if (!mTlsHandshakeComplete)
    {
        rv = mbedtls_ssl_handshake(&mSslContext);

        if (rv == 0)
        {
            OutputLine("TLS Handshake Complete");
            mTlsHandshakeComplete = true;
        }
        else if (rv != MBEDTLS_ERR_SSL_WANT_READ && rv != MBEDTLS_ERR_SSL_WANT_WRITE)
        {
            OutputLine("TLS Handshake Failed: %d", rv);
        }

        wasNotAlreadyDone = true;
    }

    return wasNotAlreadyDone;
}
#endif // OPENTHREAD_CONFIG_TLS_ENABLE

} // namespace Cli
} // namespace ot

#endif // OPENTHREAD_CONFIG_TCP_ENABLE && OPENTHREAD_CONFIG_CLI_TCP_ENABLE
