/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

/**
 * @file MbedTLSConnection.hpp
 * @brief
 *
 */

#include <iostream>
#include <util/memory/stl/Vector.hpp>

#include "MbedTLSConnection.hpp"
#include "util/logging/LogMacros.hpp"

#define MBEDTLS_WRAPPER_LOG_TAG "[MbedTLS Wrapper]"
#define MAX_CHARS_IN_PORT_NUMBER 6

namespace awsiotsdk {
    namespace network {
        MbedTLSConnection::MbedTLSConnection(util::String endpoint,
                                             uint16_t endpoint_port,
                                             util::String root_ca_location,
                                             util::String device_cert_location,
                                             util::String device_private_key_location,
                                             std::chrono::milliseconds tls_handshake_timeout,
                                             std::chrono::milliseconds tls_read_timeout,
                                             std::chrono::milliseconds tls_write_timeout,
                                             bool server_verification_flag) {
            endpoint_ = endpoint;
            endpoint_port_ = endpoint_port;
            root_ca_location_ = root_ca_location;
            device_cert_location_ = device_cert_location;
            device_private_key_location_ = device_private_key_location;
            server_verification_flag_ = server_verification_flag;
            tls_handshake_timeout_ = tls_handshake_timeout;
            tls_read_timeout_ = tls_read_timeout;
            tls_write_timeout_ = tls_write_timeout;
            flags_ = 0;

            is_connected_ = false;
            requires_free_ = false;
        }

        bool MbedTLSConnection::IsPhysicalLayerConnected() {
            // Use this to add implementation which can check for physical layer disconnect
            return true;
        }

        bool MbedTLSConnection::IsConnected() {
            return is_connected_;
        }

        int MbedTLSConnection::VerifyCertificate(void *data, mbedtls_x509_crt *crt, int depth, uint32_t *flags) {
            char buf[1024];
            ((void) data);

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "Verify requested for (Depth %d):", depth);
            mbedtls_x509_crt_info(buf, sizeof(buf) - 1, "", crt);
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "%s", buf);

            if ((*flags) == 0) {
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "This certificate has no flags");
            } else {
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, buf, sizeof(buf), "  ! ", *flags);
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "%s\n", buf);
            }

            return 0;
        }

        ResponseCode MbedTLSConnection::ConnectInternal() {
            ResponseCode rc = ResponseCode::SUCCESS;

            int ret = 0;
            const util::String pers = "aws_iot_tls_wrapper";
            char port_buf[6];
            char vrfy_buf[512];

            mbedtls_net_init(&server_fd_);
            mbedtls_ssl_init(&ssl_);
            mbedtls_ssl_config_init(&conf_);
            mbedtls_ctr_drbg_init(&ctr_drbg_);
            mbedtls_x509_crt_init(&cacert_);
            mbedtls_x509_crt_init(&clicert_);
            mbedtls_pk_init(&pkey_);

            requires_free_ = true;

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "...............................%d", MBEDTLS_SSL_MAX_CONTENT_LEN);

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Seeding the random number generator...");
            mbedtls_entropy_init(&entropy_);
            if ((ret = mbedtls_ctr_drbg_seed(&ctr_drbg_, mbedtls_entropy_func, &entropy_,
                                             (const unsigned char *) (pers.c_str()), pers.length())) != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, " Connect Failed!!! mbedtls_ctr_drbg_seed returned -0x%x", -ret);
                return ResponseCode::NETWORK_SSL_INIT_ERROR;
            }

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Loading the CA root certificate... %s", root_ca_location_.c_str());
            ret = mbedtls_x509_crt_parse_file(&cacert_, root_ca_location_.c_str());
            if (ret < 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG,
                              "Failed!!!  mbedtls_x509_crt_parse returned -0x%x while parsing root cert\n\n",
                              -ret);
                return ResponseCode::NETWORK_SSL_ROOT_CRT_PARSE_ERROR;
            }
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "ok (%d skipped)\n", ret);

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Loading the client cert. and key...");
            ret = mbedtls_x509_crt_parse_file(&clicert_, device_cert_location_.c_str());
            if (ret != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG,
                              "Failed!!!  mbedtls_x509_crt_parse returned -0x%x while parsing device cert\n\n",
                              -ret);
                return ResponseCode::NETWORK_SSL_DEVICE_CRT_PARSE_ERROR;
            }

            ret = mbedtls_pk_parse_keyfile(&pkey_, device_private_key_location_.c_str(), "");
            if (ret != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG,
                              "Failed!!!  mbedtls_pk_parse_key returned -0x%x while parsing private key\n\n",
                              -ret);
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, " path : %s ", device_private_key_location_.c_str());
                return ResponseCode::NETWORK_SSL_KEY_PARSE_ERROR;
            }
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, " ok\n");
            snprintf(port_buf, MAX_CHARS_IN_PORT_NUMBER, "%d", endpoint_port_);
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Connecting to %s/%s...", endpoint_.c_str(), port_buf);
            if ((ret = mbedtls_net_connect(&server_fd_, endpoint_.c_str(), port_buf, MBEDTLS_NET_PROTO_TCP)) != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_net_connect returned -0x%x\n\n", -ret);
                switch (ret) {
                    case MBEDTLS_ERR_NET_SOCKET_FAILED:
                        return ResponseCode::NETWORK_TCP_SETUP_ERROR;
                    case MBEDTLS_ERR_NET_UNKNOWN_HOST:
                        return ResponseCode::NETWORK_TCP_UNKNOWN_HOST;
                    case MBEDTLS_ERR_NET_CONNECT_FAILED:
                    default:
                        return ResponseCode::NETWORK_TCP_CONNECT_ERROR;
                };
            }

            ret = mbedtls_net_set_block(&server_fd_);
            if (ret != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! net_set_(non)block() returned -0x%x\n\n", -ret);
                return ResponseCode::NETWORK_SSL_UNKNOWN_ERROR;
            }
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "Ok!");

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Setting up the SSL/TLS structure...");
            if ((ret = mbedtls_ssl_config_defaults(&conf_, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
                                                   MBEDTLS_SSL_PRESET_DEFAULT)) != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG,
                              "Failed!!! mbedtls_ssl_config_defaults returned -0x%x\n\n",
                              -ret);
                return ResponseCode::NETWORK_SSL_UNKNOWN_ERROR;
            }

            mbedtls_ssl_conf_verify(&conf_, &MbedTLSConnection::VerifyCertificate, NULL);
            if (server_verification_flag_) {
                mbedtls_ssl_conf_authmode(&conf_, MBEDTLS_SSL_VERIFY_REQUIRED);
            } else {
                mbedtls_ssl_conf_authmode(&conf_, MBEDTLS_SSL_VERIFY_OPTIONAL);
            }
            mbedtls_ssl_conf_rng(&conf_, mbedtls_ctr_drbg_random, &ctr_drbg_);

            mbedtls_ssl_conf_ca_chain(&conf_, &cacert_, NULL);
            if ((ret = mbedtls_ssl_conf_own_cert(&conf_, &clicert_, &pkey_)) !=
                0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_ssl_conf_own_cert returned %d\n\n", ret);
                return ResponseCode::NETWORK_SSL_UNKNOWN_ERROR;
            }

            mbedtls_ssl_conf_read_timeout(&conf_, static_cast<uint32_t>(tls_handshake_timeout_.count()));
            if ((ret = mbedtls_ssl_set_hostname(&ssl_, endpoint_.c_str())) != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_ssl_set_hostname returned %d\n\n", ret);
                return ResponseCode::NETWORK_SSL_UNKNOWN_ERROR;
            }
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "\n\nSSL state connect : %d ", ssl_.state);
            mbedtls_ssl_set_bio(&ssl_, &server_fd_, mbedtls_net_send, NULL, mbedtls_net_recv_timeout);
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "Ok!");

            if ((ret = mbedtls_ssl_setup(&ssl_, &conf_)) != 0) {
                AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_ssl_setup returned -0x%x\n\n", -ret);
                return ResponseCode::NETWORK_SSL_UNKNOWN_ERROR;
            }

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "\n\nSSL state connect : %d ", ssl_.state);
            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Performing the SSL/TLS handshake...");
            while ((ret = mbedtls_ssl_handshake(&ssl_)) != 0) {
                if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
                    AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_ssl_handshake returned -0x%x\n", -ret);
                    if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
                        AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "    Unable to verify the server's certificate. "
                            "Either it is invalid,\n"
                            "    or you didn't set ca_file or ca_path "
                            "to an appropriate value.\n"
                            "    Alternatively, you may want to use "
                            "auth_mode=optional for testing purposes.\n");
                    }
                    return ResponseCode::NETWORK_SSL_TLS_HANDSHAKE_ERROR;
                }
            }

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG,
                         " ok\n    [ Protocol is %s ]\n    [ Ciphersuite is %s ]\n",
                         mbedtls_ssl_get_version(&ssl_),
                         mbedtls_ssl_get_ciphersuite(&ssl_));
            if ((ret = mbedtls_ssl_get_record_expansion(&ssl_)) >= 0) {
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "    [ Record expansion is %d ]\n", ret);
            } else {
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "    [ Record expansion is unknown (compression) ]\n");
            }

            AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, "....Verifying peer X.509 certificate...");

            if (server_verification_flag_) {
                if ((flags_ = mbedtls_ssl_get_verify_result(&ssl_)) != 0) {
                    AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, " failed\n");
                    mbedtls_x509_crt_verify_info(vrfy_buf, sizeof(vrfy_buf), "  ! ", flags_);
                    AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "%s\n", vrfy_buf);
                    rc = ResponseCode::NETWORK_SSL_SERVER_VERIFICATION_ERROR;
                } else {
                    AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, " ok\n");
                    rc = ResponseCode::SUCCESS;
                }
            } else {
                AWS_LOG_INFO(MBEDTLS_WRAPPER_LOG_TAG, " Server Verification skipped\n");
                rc = ResponseCode::SUCCESS;
            }

            mbedtls_ssl_conf_read_timeout(&conf_, static_cast<uint32_t>(tls_read_timeout_.count()));
            is_connected_ = true;
            return rc;
        }

        ResponseCode MbedTLSConnection::WriteInternal(const util::String &buf, size_t &size_written_bytes_out) {
            size_t total_written_length = 0;
            ResponseCode rc = ResponseCode::SUCCESS;
            size_t bytes_to_write = buf.length();
            const unsigned char *buf_cstr = (const unsigned char *) (buf.c_str());
            bool isErrorFlag = false;
            int ret;

            auto timeout = std::chrono::system_clock::now() + tls_write_timeout_;
            auto now = std::chrono::system_clock::now();

            do {
                ret = mbedtls_ssl_write(&ssl_, buf_cstr + total_written_length, bytes_to_write - total_written_length);
                if (ret > 0) {
                    total_written_length += ret;
                } else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
                    AWS_LOG_ERROR(MBEDTLS_WRAPPER_LOG_TAG, "Failed!!! mbedtls_ssl_write returned -0x%x\n\n", -ret);
                    /* All other negative return values indicate connection needs to be reset.
                     * Will be caught in ping request so ignored here */
                    isErrorFlag = true;
                    break;
                }
                now = std::chrono::system_clock::now();
            } while (now < timeout && total_written_length < bytes_to_write);

            size_written_bytes_out = total_written_length;

            if (isErrorFlag) {
                rc = ResponseCode::NETWORK_SSL_WRITE_ERROR;
            } else if (now < timeout && total_written_length != bytes_to_write) {
                return ResponseCode::NETWORK_SSL_WRITE_TIMEOUT_ERROR;
            }

            return rc;
        }

        ResponseCode MbedTLSConnection::ReadInternal(util::Vector<unsigned char> &buf, size_t buf_read_offset,
                                                     size_t size_bytes_to_read, size_t &size_read_bytes_out) {
            int ret;
            size_t total_read_length = 0;
            size_t remaining_bytes_to_read = size_bytes_to_read;
            auto timeout = std::chrono::system_clock::now() + tls_read_timeout_;
            do {
                // This read will timeout after IOT_SSL_READ_TIMEOUT if there's no data to be read
                ret = mbedtls_ssl_read(&ssl_, &buf[buf_read_offset], remaining_bytes_to_read);
                if (ret > 0) {
                    buf_read_offset += ret;
                    total_read_length += ret;
                    remaining_bytes_to_read -= ret;
                    size_read_bytes_out = total_read_length;
                } else if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE
                    && ret != MBEDTLS_ERR_SSL_TIMEOUT) {
                    return ResponseCode::NETWORK_SSL_READ_ERROR;
                }
            } while (remaining_bytes_to_read > 0 && timeout < std::chrono::system_clock::now());

            if (0 == total_read_length) {
                return ResponseCode::NETWORK_SSL_NOTHING_TO_READ;
            } else if (size_read_bytes_out == size_bytes_to_read) {
                return ResponseCode::SUCCESS;
            } else if (0 < total_read_length) {
                return ResponseCode::NETWORK_SSL_READ_TIMEOUT_ERROR;
            }

            return ResponseCode::NETWORK_SSL_READ_ERROR;
        }

        ResponseCode MbedTLSConnection::DisconnectInternal() {
            if (is_connected_) {
                int ret = 0;
                do {
                    ret = mbedtls_ssl_close_notify(&ssl_);
                } while (ret == MBEDTLS_ERR_SSL_WANT_WRITE);
            }

            if(requires_free_) {
                mbedtls_net_free(&server_fd_);

                mbedtls_x509_crt_free(&clicert_);
                mbedtls_x509_crt_free(&cacert_);
                mbedtls_pk_free(&pkey_);
                mbedtls_ssl_free(&ssl_);
                mbedtls_ssl_config_free(&conf_);
                mbedtls_ctr_drbg_free(&ctr_drbg_);
                mbedtls_entropy_free(&entropy_);
                requires_free_ = false;
            }

            is_connected_ = false;

            /* All other negative return values indicate connection needs to be reset.
             * No further action required since this is disconnect call */
            return ResponseCode::SUCCESS;
        }

        MbedTLSConnection::~MbedTLSConnection() {
            Disconnect();
        }
    }
}
