// SPDX-License-Identifier: GPL-3.0-or-later

#define ACLK_LWS_HTTPS_CLIENT_INTERNAL
#include "aclk_lws_https_client.h"

#include "aclk_common.h"

#include "aclk_lws_wss_client.h"

#define SMALL_BUFFER 16

struct simple_hcc_data {
    char *data;
    size_t data_size;
    size_t written;
    char lws_work_buffer[1024 + LWS_PRE];
    char *payload;
    int response_code;
    int done;
};

static int simple_https_client_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len)
{
    UNUSED(user);
    int n;
    char *ptr;
    char buffer[SMALL_BUFFER];
    struct simple_hcc_data *perconn_data = lws_get_opaque_user_data(wsi);

    switch (reason) {
    case LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ:
        debug(D_ACLK, "LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ");
        if (perconn_data->data_size - 1 - perconn_data->written < len)
            return 1;
        memcpy(&perconn_data->data[perconn_data->written], in, len);
        perconn_data->written += len;
        return 0;
    case LWS_CALLBACK_RECEIVE_CLIENT_HTTP:
        debug(D_ACLK, "LWS_CALLBACK_RECEIVE_CLIENT_HTTP");
        if(!perconn_data) {
            error("Missing Per Connect Data");
            return -1;
        }
        n = sizeof(perconn_data->lws_work_buffer) - LWS_PRE;
        ptr = perconn_data->lws_work_buffer + LWS_PRE;
        if (lws_http_client_read(wsi, &ptr, &n) < 0)
            return -1;
        perconn_data->data[perconn_data->written] = '\0';
        return 0;
    case LWS_CALLBACK_WSI_DESTROY:
        debug(D_ACLK, "LWS_CALLBACK_WSI_DESTROY");
        if(perconn_data)
            perconn_data->done = 1;
        return 0;
    case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP:
        debug(D_ACLK, "LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP");
        if(perconn_data)
            perconn_data->response_code = lws_http_client_http_response(wsi);
        return 0;
    case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
        debug(D_ACLK, "LWS_CALLBACK_CLOSED_CLIENT_HTTP");
        return 0;
    case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS:
        debug(D_ACLK, "LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS");
        return 0;
    case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER:
        debug(D_ACLK, "LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER");
        if(perconn_data && perconn_data->payload) {
            unsigned char **p = (unsigned char **)in, *end = (*p) + len;
            snprintfz(buffer, SMALL_BUFFER, "%zu", strlen(perconn_data->payload));
            if (lws_add_http_header_by_token(wsi,
                    WSI_TOKEN_HTTP_CONTENT_LENGTH,
                    (unsigned char *)buffer, strlen(buffer), p, end))
                return -1;
            if (lws_add_http_header_by_token(wsi,
                    WSI_TOKEN_HTTP_CONTENT_TYPE,
                    (unsigned char *)ACLK_CONTENT_TYPE_JSON,
                    strlen(ACLK_CONTENT_TYPE_JSON), p, end))
                return -1;
            lws_client_http_body_pending(wsi, 1);
            lws_callback_on_writable(wsi);
        }
        return 0;
    case LWS_CALLBACK_CLIENT_HTTP_WRITEABLE:
        debug(D_ACLK, "LWS_CALLBACK_CLIENT_HTTP_WRITEABLE");
        if(perconn_data && perconn_data->payload) {
            n = strlen(perconn_data->payload);
            if(perconn_data->data_size < (size_t)LWS_PRE + n + 1) {
                error("Buffer given is not big enough");
                return 1;
            }

            memcpy(&perconn_data->data[LWS_PRE], perconn_data->payload, n);
            if(n != lws_write(wsi, (unsigned char*)&perconn_data->data[LWS_PRE], n, LWS_WRITE_HTTP)) {
                error("lws_write error");
                perconn_data->data[0] = 0;
                return 1;
            }
            lws_client_http_body_pending(wsi, 0);
            // clean for subsequent reply read
            perconn_data->data[0] = 0;
        }
        return 0;
    case LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL:
        debug(D_ACLK, "LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL");
        return 0;
    case LWS_CALLBACK_WSI_CREATE:
        debug(D_ACLK, "LWS_CALLBACK_WSI_CREATE");
        return 0;
    case LWS_CALLBACK_PROTOCOL_INIT:
        debug(D_ACLK, "LWS_CALLBACK_PROTOCOL_INIT");
        return 0;
    case LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL:
        debug(D_ACLK, "LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL");
        return 0;
    case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED:
        debug(D_ACLK, "LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED");
        return 0;
    case LWS_CALLBACK_GET_THREAD_ID:
        debug(D_ACLK, "LWS_CALLBACK_GET_THREAD_ID");
        return 0;
    case LWS_CALLBACK_EVENT_WAIT_CANCELLED:
        debug(D_ACLK, "LWS_CALLBACK_EVENT_WAIT_CANCELLED");
        return 0;
    case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
        debug(D_ACLK, "LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION");
        return 0;
    case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH:
        debug(D_ACLK, "LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH");
        return 0;
    default:
        debug(D_ACLK, "Unknown callback %d", (int)reason);
        return 0;
    }
}

static const struct lws_protocols protocols[] = {
    {
        "http",
        simple_https_client_callback,
        0,
        0,
        0,
        0,
        0
    },
    { NULL, NULL, 0, 0, 0, 0, 0 }
};

static void simple_hcc_log_divert(int level, const char *line)
{
    UNUSED(level);
    error("Libwebsockets: %s", line);
}

int aclk_send_https_request(char *method, char *host, int port, char *url, char *b, size_t b_size, char *payload)
{
    info("%s %s", __func__, method);

    struct lws_context_creation_info info;
    struct lws_client_connect_info i;
    struct lws_context *context;

    struct simple_hcc_data *data = callocz(1, sizeof(struct simple_hcc_data));
    data->data = b;
    data->data[0] = 0;
    data->data_size = b_size;
    data->payload = payload;

    int n = 0;
    time_t timestamp;

    struct lws_vhost *vhost;

    memset(&info, 0, sizeof info);

    info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
    info.port = CONTEXT_PORT_NO_LISTEN;
    info.protocols = protocols;


    context = lws_create_context(&info);
    if (!context) {
        error("Error creating LWS context");
        freez(data);
        return 1;
    }

    lws_set_log_level(LLL_ERR | LLL_WARN, simple_hcc_log_divert);

    lws_service(context, 0);

    memset(&i, 0, sizeof i); /* otherwise uninitialized garbage */
    i.context = context;

#ifdef ACLK_SSL_ALLOW_SELF_SIGNED
    i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK | LCCSCF_ALLOW_INSECURE;
    info("Disabling SSL certificate checks");
#else
    i.ssl_connection = LCCSCF_USE_SSL;
#endif
#if defined(HAVE_X509_VERIFY_PARAM_set1_host) && HAVE_X509_VERIFY_PARAM_set1_host == 0
#warning DISABLING SSL HOSTNAME VALIDATION BECAUSE IT IS NOT AVAILABLE ON THIS SYSTEM.
    i.ssl_connection |= LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
#endif

    i.port = port;
    i.address = host;
    i.path = url;

    i.host = i.address;
    i.origin = i.address;
    i.method = method;
    i.opaque_user_data = data;
    i.alpn = "http/1.1";

    i.protocol = protocols[0].name;

    vhost = lws_get_vhost_by_name(context, "default");
    if(!vhost)
        fatal("Could not find the default LWS vhost.");

    //set up proxy
    aclk_wss_set_proxy(vhost);

    lws_client_connect_via_info(&i);

    // libwebsockets handle connection timeouts already
    // this adds additional safety in case of bug in LWS
    timestamp = now_monotonic_sec();
    while( n >= 0 && !data->done && !netdata_exit) {
        n = lws_service(context, 0);
        if( now_monotonic_sec() - timestamp > SEND_HTTPS_REQUEST_TIMEOUT ) {
            data->data[0] = 0;
            data->done = 1;
            error("Servicing LWS took too long.");
        }
    }

    lws_context_destroy(context);

    n = data->response_code;

    freez(data);
    return (n < 200 || n >= 300);
}
