// SPDX-License-Identifier: GPL-2.0-only
/*
 * Digital Voice Modem - Common Library
 * GPLv2 Open Source. Use is subject to license terms.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 *  Copyright (C) 2024,2026 Bryan Biedenkapp, N2PLL
 *
 */
/**
 * @file SecureClientConnection.h
 * @ingroup http
 */
#if !defined(__REST_HTTP__SECURE_CLIENT_CONNECTION_H__)
#define __REST_HTTP__SECURE_CLIENT_CONNECTION_H__

#if defined(ENABLE_SSL)

#include "common/Defines.h"
#include "common/restapi/http/HTTPLexer.h"
#include "common/restapi/http/HTTPPayload.h"
#include "common/Log.h"

#include <array>
#include <vector>
#include <memory>
#include <utility>
#include <iterator>

#include <asio.hpp>
#include <asio/ssl.hpp>

namespace restapi
{
    namespace http
    {
        // ---------------------------------------------------------------------------
        //  Class Declaration
        // ---------------------------------------------------------------------------

        /**
         * @brief This class represents a single connection from a client.
         * @tparam RequestHandlerType Type representing a request handler.
         * @ingroup http
         */
        template <typename RequestHandlerType>
        class SecureClientConnection {
        public:
            auto operator=(SecureClientConnection&) -> SecureClientConnection& = delete;
            auto operator=(SecureClientConnection&&) -> SecureClientConnection& = delete;
            SecureClientConnection(SecureClientConnection&) = delete;

            /**
             * @brief Initializes a new instance of the SecureClientConnection class.
             * @param socket TCP socket for this connection.
             * @param context SSL context for this connection.
             * @param handler Request handler for this connection.
             */
            explicit SecureClientConnection(asio::ip::tcp::socket socket, asio::ssl::context& context, RequestHandlerType& handler) :
                m_socket(std::move(socket), context),
                m_requestHandler(handler),
                m_sizeToTransfer(0U),
                m_bytesTransferred(0U),
                m_fullBuffer(65536),
                m_lexer(HTTPLexer(true))
            {
                m_socket.set_verify_mode(asio::ssl::verify_none);
                m_socket.set_verify_callback(std::bind(&SecureClientConnection::verify_certificate, this, std::placeholders::_1, std::placeholders::_2));
            }

            /**
             * @brief Start the first asynchronous operation for the connection.
             */
            void start() 
            { 
                m_socket.handshake(asio::ssl::stream_base::client);
                read(); 
            }
            /**
             * @brief Stop all asynchronous operations associated with the connection.
             */
            void stop()
            {
                try
                {
                    ensureNoLinger();
                    if (m_socket.lowest_layer().is_open()) {
                        m_socket.lowest_layer().close();
                    }
                }
                catch(const std::exception&) { /* ignore */ }
            }

            /**
             * @brief Helper to enable the SO_LINGER socket option during shutdown.
             */
            void ensureNoLinger()
            {
                try
                {
                    // enable SO_LINGER timeout 0
                    asio::socket_base::linger linger(true, 0);
                    m_socket.lowest_layer().set_option(linger);
                }
                catch(const asio::system_error& e)
                {
                    asio::error_code ec = e.code();
                    if (ec) {
                        ::LogError(LOG_REST, "SecureClientConnection::ensureNoLinger(), %s, code = %u", ec.message().c_str(), ec.value());
                    }
                }
            }

            /**
             * @brief Perform an synchronous write operation.
             * @param request HTTP request.
             */
            void send(HTTPPayload request)
            {
                request.attachHostHeader(m_socket.lowest_layer().remote_endpoint());
                write(request);
            }
        private:
            /**
             * @brief Perform an SSL certificate verification.
             * @param preverified Flag indicating the SSL certificate was preverified.
             * @param context SSL verification context.
             * @returns True, if SSL certificate is valid, otherwise false.
             */
            bool verify_certificate(bool preverified, asio::ssl::verify_context& context)
            {
                return true; // ignore always valid
            }

            /**
             * @brief Perform an asynchronous read operation.
             */
            void read()
            {
                m_socket.async_read_some(asio::buffer(m_buffer), [=](asio::error_code ec, std::size_t bytes_transferred) {
                    if (!ec) {
                        HTTPLexer::ResultType result;
                        char* content;

                        try
                        {
                            // grow accumulation buffer as needed
                            if (m_bytesTransferred + bytes_transferred > m_fullBuffer.size()) {
                                m_fullBuffer.resize(m_bytesTransferred + bytes_transferred);
                            }

                            // always accumulate into m_fullBuffer
                            ::memcpy(m_fullBuffer.data() + m_bytesTransferred, m_buffer.data(), bytes_transferred);
                            m_bytesTransferred += bytes_transferred;

                            if (m_sizeToTransfer > 0U && m_bytesTransferred < m_sizeToTransfer) {
                                // still waiting for more data
                                read();
                            }
                            else {
                                bool wasMultiRead = (m_sizeToTransfer > 0U);
                                m_sizeToTransfer = 0U;

                                if (wasMultiRead) {
                                    // re-parse the full reassembled response from the beginning
                                    m_lexer.reset();
                                }

                                // always parse from m_fullBuffer so content pointer is always valid
                                std::tie(result, content) = m_lexer.parse(m_request, m_fullBuffer.data(), m_fullBuffer.data() + m_bytesTransferred);

                                // determine content length
                                std::string contentLength = m_request.headers.find("Content-Length");
                                if (contentLength != "") {
                                    size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10);

                                    // body bytes available: distance from parsed body start to end of accumulated data
                                    // content and m_fullBuffer.data() are in the same allocation, so this arithmetic is valid
                                    size_t bodyBytesAvailable = (size_t)((m_fullBuffer.data() + m_bytesTransferred) - content);
                                    if (length > bodyBytesAvailable) {
                                        // total bytes we need = current accumulated + remaining body bytes
                                        m_sizeToTransfer = m_bytesTransferred + (length - bodyBytesAvailable);
                                    }

                                    if (m_sizeToTransfer > 0U) {
                                        result = HTTPLexer::CONTINUE;
                                    } else {
                                        m_request.content = std::string(content, length);
                                    }
                                }

                                m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string());

                                if (result == HTTPLexer::GOOD) {
                                    m_sizeToTransfer = m_bytesTransferred = 0U;
                                    m_requestHandler.handleRequest(m_request, m_reply);
                                }
                                else if (result == HTTPLexer::BAD) {
                                    m_sizeToTransfer = m_bytesTransferred = 0U;
                                    return;
                                }
                                else {
                                    read();
                                }
                            }
                        }
                        catch(const std::exception& e) { ::LogError(LOG_REST, "SecureClientConnection::read(), %s", ec.message().c_str()); }
                    }
                    else if (ec != asio::error::operation_aborted) {
                        if (ec) {
                            ::LogError(LOG_REST, "SecureClientConnection::read(), %s, code = %u", ec.message().c_str(), ec.value());
                        }
                        stop();
                    }
                });
            }

            /**
             * @brief Perform an synchronous write operation.
             * @param request HTTP request.
             */
            void write(HTTPPayload request)
            {
                try
                {
                    m_socket.handshake(asio::ssl::stream_base::client);

                    auto buffers = request.toBuffers();
                    asio::write(m_socket, buffers);
                }
                catch(const asio::system_error& e)
                {
                    asio::error_code ec = e.code();
                    if (ec) {
                        ::LogError(LOG_REST, "SecureClientConnection::write(), %s, code = %u", ec.message().c_str(), ec.value());

                        try
                        {
                            // initiate graceful connection closure
                            asio::error_code ignored_ec;
                            m_socket.lowest_layer().shutdown(asio::ip::tcp::socket::shutdown_both, ignored_ec);
                        }
                        catch(const std::exception& e) { 
                            ::LogError(LOG_REST, "SecureClientConnection::write(), %s, code = %u", ec.message().c_str(), ec.value()); 
                        }
                    }
                }
            }

            asio::ssl::stream<asio::ip::tcp::socket> m_socket;

            RequestHandlerType& m_requestHandler;

            std::size_t m_sizeToTransfer;
            std::size_t m_bytesTransferred;
            std::vector<char> m_fullBuffer;

            std::array<char, 4096> m_buffer;

            HTTPPayload m_request;
            HTTPLexer m_lexer;
            HTTPPayload m_reply;
        };
    } // namespace http
} // namespace restapi

#endif // ENABLE_SSL

#endif // __REST_HTTP__SECURE_CLIENT_CONNECTION_H__
