/*
Adds a WebServer and WebService callbacks to meshtastic via the Portduino/native target (Linux and
macOS). The WebServer & Webservices run in a real host thread beside the Portduino threading
emulation. It replaces the complete ESP32 Webserver libs including generation of SSL certificates,
because those libs use ESP-specific details that can't be emulated.

The WebServices adapt to the two major phoneapi functions "handleAPIv1FromRadio,handleAPIv1ToRadio"
The WebServer just adds basic support to deliver WebContent, so it can be used to
deliver the WebGui defined by the WebClient Project.

Steps to get it running:

Linux (apt):
1.) Add these libs to the compile and target machine:

    sudo apt update && \
        apt -y install openssl libssl-dev libsdl2-dev \
                     libulfius-dev liborcania-dev

macOS (Homebrew):
1.) Install prerequisites via Homebrew:

    brew install ulfius openssl@3

    The PlatformIO env (native-macos) picks up compiler/linker flags via
    `pkg-config`. In particular, OpenSSL needs `pkg-config --cflags --libs openssl@3`
    so both the Homebrew include path and linker flags are provided; ulfius and its
    dependencies (liborcania, libyder) are also resolved via `pkg-config`.

2.) Configure the root directory of the web Content in the config.yaml file.
    The following tags should be included and set at your needs

    Example entry in the config.yaml
    Webserver:
        Port: 9001 # Port for Webserver & Webservices
        RootPath: /home/marc/web # Root Dir of WebServer

3.) Checkout the web project
    https://github.com/meshtastic/web.git

    Build it and copy the content of the folder web/dist/* to the folder you did set as "RootPath"

!!!The WebServer should not be used as production system or exposed to the Internet. Its a raw basic version!!!

Author: Marc Philipp Hammermann
mail:   marchammermann@googlemail.com

*/
// Mirrors the guard in PiWebServer.h - see comment there. macOS Homebrew
// provides ulfius + deps; Linux pulls them via apt. Either way, this
// translation unit only compiles when the headers are present.
#ifdef ARCH_PORTDUINO
#if __has_include(<ulfius.h>)
// First, in its own block so the include sorter keeps it there: configuration.h supplies the
// variant defines mesh-pb-constants.h needs (portduino resolves MAX_NUM_NODES at runtime).
#include "configuration.h"

#include "NodeDB.h"
#include "PhoneAPI.h"
#include "PiWebServer.h"
#include "PowerFSM.h"
#include "RadioLibInterface.h"
#include "airtime.h"
#include "graphics/Screen.h"
#include "main.h"
#include "mesh/wifi/WiFiAPClient.h"
#include "sleep.h"
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/x509.h>
#include <orcania.h>
#include <string.h>
#include <ulfius.h>
#include <yder.h>

#include <cstring>
#include <string>

#include "PortduinoFS.h"
#include "platform/portduino/PortduinoGlue.h"

#define DEFAULT_REALM "default_realm"
#define PREFIX ""

#define KEY_PATH portduino_config.webserver_ssl_key_path.c_str()
#define CERT_PATH portduino_config.webserver_ssl_cert_path.c_str()

struct _file_config configWeb;

// We need to specify some content-type mapping, so the resources get delivered with the
// right content type and are displayed correctly in the browser
char contentTypes[][2][32] = {{".txt", "text/plain"},      {".html", "text/html"},
                              {".js", "text/javascript"},  {".png", "image/png"},
                              {".jpg", "image/jpg"},       {".gz", "application/gzip"},
                              {".gif", "image/gif"},       {".json", "application/json"},
                              {".css", "text/css"},        {".ico", "image/vnd.microsoft.icon"},
                              {".svg", "image/svg+xml"},   {".ts", "text/javascript"},
                              {".tsx", "text/javascript"}, {"", ""}};

#undef str

volatile bool isWebServerReady;
volatile bool isCertReady;

PiWebServerThread *piwebServerThread;

/**
 * Return the filename extension
 */
const char *get_filename_ext(const char *path)
{
    const char *dot = strrchr(path, '.');
    if (!dot || dot == path)
        return "*";
    if (strchr(dot, '?') != NULL) {
        //*strchr(dot, '?') = '\0';
        const char *empty = "\0";
        return empty;
    }
    return dot;
}

/**
 * Streaming callback function to ease sending large files
 */
static ssize_t callback_static_file_stream(void *cls, uint64_t pos, char *buf, size_t max)
{
    (void)(pos);
    if (cls != NULL) {
        return fread(buf, 1, max, (FILE *)cls);
    } else {
        return U_STREAM_END;
    }
}

/**
 * Cleanup FILE* structure when streaming is complete
 */
static void callback_static_file_stream_free(void *cls)
{
    if (cls != NULL) {
        fclose((FILE *)cls);
    }
}

/**
 * static file callback endpoint that delivers the content for WebServer calls
 */
int callback_static_file(const struct _u_request *request, struct _u_response *response, void *user_data)
{
    size_t length;
    FILE *f;
    char *file_requested, *file_path, *url_dup_save, *real_path = NULL;
    const char *content_type;

    /*
     * Comment this if statement if you don't access static files url from root dir, like /app
     */
    if (request->callback_position > 0) {
        return U_CALLBACK_CONTINUE;
    } else if (user_data != NULL && (configWeb.files_path != NULL)) {
        file_requested = o_strdup(request->http_url);
        url_dup_save = file_requested;

        while (file_requested[0] == '/') {
            file_requested++;
        }
        file_requested += o_strlen(configWeb.url_prefix);
        while (file_requested[0] == '/') {
            file_requested++;
        }

        if (strchr(file_requested, '#') != NULL) {
            *strchr(file_requested, '#') = '\0';
        }

        if (strchr(file_requested, '?') != NULL) {
            *strchr(file_requested, '?') = '\0';
        }

        if (file_requested == NULL || o_strlen(file_requested) == 0 || 0 == o_strcmp("/", file_requested)) {
            o_free(url_dup_save);
            url_dup_save = file_requested = o_strdup("index.html");
        }

        file_path = msprintf("%s/%s", configWeb.files_path, file_requested);
        real_path = realpath(file_path, NULL);
        if (0 == o_strncmp(configWeb.files_path, real_path, o_strlen(configWeb.files_path))) {
            if (access(file_path, F_OK) != -1) {
                f = fopen(file_path, "rb");
                if (f) {
                    fseek(f, 0, SEEK_END);
                    length = ftell(f);
                    fseek(f, 0, SEEK_SET);

                    content_type = u_map_get_case(&configWeb.mime_types, get_filename_ext(file_requested));
                    if (content_type == NULL) {
                        content_type = u_map_get(&configWeb.mime_types, "*");
                        LOG_DEBUG("Static File Server - Unknown mime type for ext %s ", get_filename_ext(file_requested));
                    }
                    u_map_put(response->map_header, "Content-Type", content_type);
                    u_map_copy_into(response->map_header, &configWeb.map_header);

                    if (ulfius_set_stream_response(response, 200, callback_static_file_stream, callback_static_file_stream_free,
                                                   length, STATIC_FILE_CHUNK, f) != U_OK) {
                        LOG_DEBUG("callback_static_file - Error ulfius_set_stream_response");
                        // The stream-free callback only runs when the stream was accepted, so the
                        // file must be closed here or the FILE and its fd leak on every failure.
                        fclose(f);
                        ulfius_set_string_body_response(response, 500, "Internal server error");
                    }
                }
            } else {
                if (configWeb.redirect_on_404 == NULL) {
                    ulfius_set_string_body_response(response, 404, "File not found");
                } else {
                    ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404);
                    response->status = 302;
                }
            }
        } else {
            if (configWeb.redirect_on_404 == NULL) {
                ulfius_set_string_body_response(response, 404, "File not found");
            } else {
                ulfius_add_header_to_response(response, "Location", configWeb.redirect_on_404);
                response->status = 302;
            }
        }

        o_free(file_path);
        o_free(url_dup_save);
        free(real_path); // realpath uses malloc
        return U_CALLBACK_CONTINUE;
    } else {
        LOG_DEBUG("Static File Server - user_data NULL or inconsistent");
        return U_CALLBACK_ERROR;
    }
}

/*
 * Adapt the radioapi to the Webservice handleAPIv1ToRadio
 * Trigger : WebGui(SAVE)->WebServcice->phoneApi
 */
int handleAPIv1ToRadio(const struct _u_request *req, struct _u_response *res, void *user_data)
{
    LOG_DEBUG("handleAPIv1ToRadio web -> radio  ");

    ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf");
    ulfius_add_header_to_response(res, "Access-Control-Allow-Headers", "Content-Type");
    ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*");
    ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "PUT, OPTIONS");
    ulfius_add_header_to_response(res, "X-Protobuf-Schema",
                                  "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto");

    if (strcmp(req->http_verb, "OPTIONS") == 0) {
        ulfius_set_response_properties(res, U_OPT_STATUS, 204);
        return U_CALLBACK_COMPLETE;
    }

    byte buffer[MAX_TO_FROM_RADIO_SIZE];
    // ulfius allocates binary_body at exactly binary_body_length bytes (NULL for a body-less
    // PUT), and the framework accepts bodies larger than our buffer, so clamp both directions.
    size_t s = req->binary_body ? req->binary_body_length : 0;
    if (s > sizeof(buffer))
        s = sizeof(buffer);
    if (s > 0)
        memcpy(buffer, req->binary_body, s);

    // FIXME* Problem with portdunio loosing mountpoint maybe because of running in a real sep. thread

    portduinoVFS->mountpoint(configWeb.rootPath);

    LOG_DEBUG("Received %d bytes from PUT request", s);
    static_cast<HttpAPI *>(user_data)->handleToRadio(buffer, s);
    LOG_DEBUG("end web->radio  ");
    return U_CALLBACK_COMPLETE;
}

/*
 * Adapt the radioapi to the Webservice handleAPIv1FromRadio
 * Trigger : WebGui(POLL)->handleAPIv1FromRadio->phoneapi->Meshtastic(Radio) events
 */
int handleAPIv1FromRadio(const struct _u_request *req, struct _u_response *res, void *user_data)
{

    // LOG_DEBUG("handleAPIv1FromRadio radio -> web");
    std::string valueAll;

    // Status code is 200 OK by default.
    ulfius_add_header_to_response(res, "Content-Type", "application/x-protobuf");
    ulfius_add_header_to_response(res, "Access-Control-Allow-Origin", "*");
    ulfius_add_header_to_response(res, "Access-Control-Allow-Methods", "GET");
    ulfius_add_header_to_response(res, "X-Protobuf-Schema",
                                  "https://raw.githubusercontent.com/meshtastic/protobufs/master/meshtastic/mesh.proto");

    if (strcmp(req->http_verb, "OPTIONS") == 0) {
        ulfius_set_response_properties(res, U_OPT_STATUS, 204);
        return U_CALLBACK_COMPLETE;
    }

    uint8_t txBuf[MAX_STREAM_BUF_SIZE];
    uint32_t len = 1;

    if (valueAll == "true") {
        while (len) {
            len = static_cast<HttpAPI *>(user_data)->getFromRadio(txBuf);
            ulfius_set_response_properties(res, U_OPT_STATUS, 200, U_OPT_BINARY_BODY, txBuf, len);
            const char *tmpa = (const char *)txBuf;
            ulfius_set_string_body_response(res, 200, tmpa);
            // LOG_DEBUG("\n----webAPI response all:----");
            // LOG_DEBUG(tmpa);
            // LOG_DEBUG(".");
        }
        // Otherwise, just return one protobuf
    } else {
        len = static_cast<HttpAPI *>(user_data)->getFromRadio(txBuf);
        const char *tmpa = (const char *)txBuf;
        ulfius_set_binary_body_response(res, 200, tmpa, len);
        // LOG_DEBUG("\n----webAPI response:");
        // LOG_DEBUG(tmpa);
        // LOG_DEBUG(".");
    }

    // LOG_DEBUG("end radio->web", len);
    return U_CALLBACK_COMPLETE;
}

/*
OpenSSL RSA Key Gen
*/
int generate_rsa_key(EVP_PKEY **pkey)
{
    EVP_PKEY_CTX *pkey_ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
    if (!pkey_ctx)
        return -1;
    if (EVP_PKEY_keygen_init(pkey_ctx) <= 0 || EVP_PKEY_CTX_set_rsa_keygen_bits(pkey_ctx, 2048) <= 0 ||
        EVP_PKEY_keygen(pkey_ctx, pkey) <= 0) {
        EVP_PKEY_CTX_free(pkey_ctx);
        return -1;
    }
    EVP_PKEY_CTX_free(pkey_ctx);
    return 0; // SUCCESS
}

int generate_self_signed_x509(EVP_PKEY *pkey, X509 **x509)
{
    *x509 = X509_new();
    if (!*x509)
        return -1;
    if (X509_set_version(*x509, 2) != 1)
        return -1;
    ASN1_INTEGER_set(X509_get_serialNumber(*x509), 1);
    X509_gmtime_adj(X509_get_notBefore(*x509), 0);
    X509_gmtime_adj(X509_get_notAfter(*x509), 31536000L); // 1 YEAR ACCESS

    X509_set_pubkey(*x509, pkey);

    // SET Subject Name
    X509_NAME *name = X509_get_subject_name(*x509);
    X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char *)"DE", -1, -1, 0);
    X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char *)"Meshtastic", -1, -1, 0);
    X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"meshtastic.local", -1, -1, 0);
    // Selfsigned,  Issuer = Subject
    X509_set_issuer_name(*x509, name);

    // Certificate signed with our privte key
    if (X509_sign(*x509, pkey, EVP_sha256()) <= 0)
        return -1;

    return 0;
}

char *read_file_into_string(const char *filename)
{
    FILE *file = fopen(filename, "rb");
    if (file == NULL) {
        LOG_ERROR("Error reading File : %s ", filename);
        return NULL;
    }

    // Size of file
    fseek(file, 0, SEEK_END);
    long filesize = ftell(file);
    rewind(file);

    // reserve mem for file + 1 byte
    char *buffer = (char *)malloc(filesize + 1);
    if (buffer == NULL) {
        LOG_ERROR("Malloc failed for file : %s ", filename);
        fclose(file);
        return NULL;
    }

    // read content
    size_t readSize = fread(buffer, 1, filesize, file);
    if (readSize != filesize) {
        LOG_ERROR("Error reading file into buffer");
        free(buffer);
        fclose(file);
        return NULL;
    }

    // add terminator sign at the end
    buffer[filesize] = '\0';
    fclose(file);
    return buffer; // return pointer
}

int PiWebServerThread::CheckSSLandLoad()
{
    // read certificate
    cert_pem = read_file_into_string(CERT_PATH);
    if (cert_pem == NULL) {
        LOG_ERROR("SSL Certificate File can't be loaded or missing");
        return 1;
    }
    // read private key
    key_pem = read_file_into_string(KEY_PATH);
    if (key_pem == NULL) {
        LOG_ERROR("File private_key can't be loaded or missing");
        // The constructor retries CheckSSLandLoad() after regenerating, which would overwrite
        // (and leak) the cert buffer loaded above.
        free(cert_pem);
        cert_pem = NULL;
        return 2;
    }

    return 0;
}

int PiWebServerThread::CreateSSLCertificate()
{

    EVP_PKEY *pkey = NULL;
    X509 *x509 = NULL;

    if (generate_rsa_key(&pkey) != 0) {
        LOG_ERROR("Error generating RSA-Key");
        return 1;
    }

    if (generate_self_signed_x509(pkey, &x509) != 0) {
        LOG_ERROR("Error generating X509-Cert");
        // generate_self_signed_x509 can fail after allocating *x509; X509_free(NULL) is a no-op
        X509_free(x509);
        EVP_PKEY_free(pkey);
        return 2;
    }

    // Open file to write private key file
    FILE *pkey_file = fopen(KEY_PATH, "wb");
    if (!pkey_file) {
        LOG_ERROR("Error opening private key file");
        X509_free(x509);
        EVP_PKEY_free(pkey);
        return 3;
    }
    // write private key file
    PEM_write_PrivateKey(pkey_file, pkey, NULL, NULL, 0, NULL, NULL);
    fclose(pkey_file);

    // open Certificate file
    FILE *x509_file = fopen(CERT_PATH, "wb");
    if (!x509_file) {
        LOG_ERROR("Error opening cert");
        X509_free(x509);
        EVP_PKEY_free(pkey);
        return 4;
    }
    // write certificate
    PEM_write_X509(x509_file, x509);
    fclose(x509_file);

    EVP_PKEY_free(pkey);
    LOG_INFO("Create SSL Key %s successful", KEY_PATH);
    X509_free(x509);
    LOG_INFO("Create SSL Cert %s successful", CERT_PATH);
    return 0;
}

void initWebServer() {}

PiWebServerThread::PiWebServerThread()
{
    int ret, retssl, webservport;

    if (CheckSSLandLoad() != 0) {
        CreateSSLCertificate();
        if (CheckSSLandLoad() != 0) {
            LOG_ERROR("Major Error Gen & Read SSL Certificate");
        }
    }

    if (portduino_config.webserverport != 0) {
        webservport = portduino_config.webserverport;
        LOG_INFO("Use webserver port from yaml config %i ", webservport);
    } else {
        LOG_INFO("Webserver port in yaml config 0, default to 9443");
        webservport = 9443;
    }

    // Web Content Service Instance
    if (ulfius_init_instance(&instanceWeb, webservport, NULL, DEFAULT_REALM) != U_OK) {
        LOG_ERROR("Webserver start failed, abort");
    } else {

        LOG_INFO("Webserver started");
        u_map_init(&configWeb.mime_types);
        u_map_put(&configWeb.mime_types, "*", "application/octet-stream");
        u_map_put(&configWeb.mime_types, ".html", "text/html");
        u_map_put(&configWeb.mime_types, ".htm", "text/html");
        u_map_put(&configWeb.mime_types, ".tsx", "application/javascript");
        u_map_put(&configWeb.mime_types, ".ts", "application/javascript");
        u_map_put(&configWeb.mime_types, ".css", "text/css");
        u_map_put(&configWeb.mime_types, ".js", "application/javascript");
        u_map_put(&configWeb.mime_types, ".json", "application/json");
        u_map_put(&configWeb.mime_types, ".png", "image/png");
        u_map_put(&configWeb.mime_types, ".gif", "image/gif");
        u_map_put(&configWeb.mime_types, ".jpeg", "image/jpeg");
        u_map_put(&configWeb.mime_types, ".jpg", "image/jpeg");
        u_map_put(&configWeb.mime_types, ".ttf", "font/ttf");
        u_map_put(&configWeb.mime_types, ".woff", "font/woff");
        u_map_put(&configWeb.mime_types, ".ico", "image/x-icon");
        u_map_put(&configWeb.mime_types, ".svg", "image/svg+xml");

        webrootpath = portduino_config.webserver_root_path;

        configWeb.files_path = (char *)webrootpath.c_str();
        configWeb.url_prefix = "";
        configWeb.rootPath = strdup(portduinoVFS->mountpoint());

        u_map_put(instanceWeb.default_headers, "Access-Control-Allow-Origin", "*");
        // Maximum body size sent by the client is 1 Kb
        instanceWeb.max_post_body_size = 1024;
        ulfius_add_endpoint_by_val(&instanceWeb, "GET", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI);
        ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/fromradio/*", 1, &handleAPIv1FromRadio, &webAPI);
        ulfius_add_endpoint_by_val(&instanceWeb, "PUT", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI);
        ulfius_add_endpoint_by_val(&instanceWeb, "OPTIONS", PREFIX, "/api/v1/toradio/*", 1, &handleAPIv1ToRadio, &webAPI);

        // Add callback function to all endpoints for the Web Server
        ulfius_add_endpoint_by_val(&instanceWeb, "GET", NULL, "/*", 2, &callback_static_file, &configWeb);

        // thats for serving without SSL
        // retssl = ulfius_start_framework(&instanceWeb);

        // thats for serving with SSL
        retssl = ulfius_start_secure_framework(&instanceWeb, key_pem, cert_pem);

        if (retssl == U_OK) {
            LOG_INFO("Web Server framework started on port: %i ", webservport);
            LOG_INFO("Web Server root %s", (char *)webrootpath.c_str());
        } else {
            LOG_ERROR("Web Server framework start failed, err: %d", retssl);
        }
    }
}

PiWebServerThread::~PiWebServerThread()
{
    u_map_clean(&configWeb.mime_types);

    ulfius_stop_framework(&instanceWeb);
    ulfius_clean_instance(&instanceWeb);
    free(configWeb.rootPath);
    free(key_pem);
    free(cert_pem);
    LOG_INFO("End framework");
}

#endif
#endif