// SPDX-FileCopyrightText: 2023 g10 code GmbH
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "webserver.h"

#include <QDebug>
#include <QFile>
#include <QHttpServer>
#include <QHttpServerResponse>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QStandardPaths>
#include <QWebSocket>
#include <QWebSocketServer>

#include "controllers/emailcontroller.h"
#include "controllers/registrationcontroller.h"
#include "controllers/staticcontroller.h"
#include "http_debug.h"
#include "websocket_debug.h"

using namespace Qt::Literals::StringLiterals;

WebServer WebServer::s_instance = WebServer();

WebServer &WebServer::self()
{
    return s_instance;
}

WebServer::WebServer()
    : QObject(nullptr)
    , m_httpServer(new QHttpServer(this))
    , m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, this))
{
}

WebServer::~WebServer() = default;

bool WebServer::run()
{
    auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem"));
    auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
    Q_ASSERT(!keyPath.isEmpty());
    Q_ASSERT(!certPath.isEmpty());

    QFile privateKeyFile(keyPath);
    if (!privateKeyFile.open(QIODevice::ReadOnly)) {
        qCFatal(HTTP_LOG) << "Couldn't open file" << keyPath << "for reading:" << privateKeyFile.errorString();
        return false;
    }
    const QSslKey sslKey(&privateKeyFile, QSsl::Rsa);
    privateKeyFile.close();

    const auto sslCertificateChain = QSslCertificate::fromPath(certPath);
    if (sslCertificateChain.isEmpty()) {
        qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file:"_s << certPath;
        return false;
    }

    // Static assets controller
    m_httpServer->route(u"/home"_s, &StaticController::homeAction);
    m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction);

    // Registration controller
    m_httpServer->route(u"/register"_s, &RegistrationController::registerAction);

    // Email controller
    m_httpServer->route(u"/view"_s, &EmailController::viewEmailAction);
    m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction);
    m_httpServer->route(u"/reply"_s, &EmailController::replyEmailAction);
    m_httpServer->route(u"/forward"_s, &EmailController::forwardEmailAction);
    m_httpServer->route(u"/new"_s, &EmailController::newEmailAction);
    m_httpServer->route(u"/socket-web"_s, &EmailController::socketWebAction);

    m_httpServer->route(u"/draft/<arg>"_s, &EmailController::draftAction);

    m_httpServer->sslSetup(sslCertificateChain.front(), sslKey);

    const auto port = m_httpServer->listen(QHostAddress::Any, WebServer::Port);
    if (!port) {
        qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
        return false;
    }
    qInfo(HTTP_LOG) << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port);

    QSslConfiguration sslConfiguration;
    sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
    sslConfiguration.setLocalCertificate(sslCertificateChain.front());
    sslConfiguration.setPrivateKey(sslKey);
    m_webSocketServer->setSslConfiguration(sslConfiguration);

    if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) {
        qCInfo(WEBSOCKET_LOG) << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1);
        connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection);
    }

    return true;
}

void WebServer::onNewConnection()
{
    auto pSocket = m_webSocketServer->nextPendingConnection();
    if (!pSocket) {
        return;
    }

    qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort();

    connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage);
    connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage);
    connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected);

    m_clients << pSocket;
}

void WebServer::processTextMessage(QString message)
{
    auto webClient = qobject_cast<QWebSocket *>(sender());
    if (webClient) {
        QJsonParseError error;
        const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error);
        if (error.error != QJsonParseError::NoError) {
            qCWarning(WEBSOCKET_LOG) << "Error parsing json" << error.errorString();
            return;
        }

        if (!doc.isObject()) {
            qCWarning(WEBSOCKET_LOG) << "Invalid json received";
            return;
        }

        const auto object = doc.object();
        if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) {
            qCWarning(WEBSOCKET_LOG) << "Invalid json received: no command or arguments set";
            return;
        }

        static QHash<QString, WebServer::Command> commandMapping{
            {"register"_L1, WebServer::Command::Register},
            {"email-sent"_L1, WebServer::Command::EmailSent},
        };

        const auto command = commandMapping[doc["command"_L1].toString()];

        processCommand(command, object["arguments"_L1].toObject(), webClient);
    }
}

void WebServer::processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket)
{
    switch (command) {
    case Command::Register: {
        const auto type = arguments["type"_L1].toString();
        qCWarning(WEBSOCKET_LOG) << "Register" << arguments;
        if (type.isEmpty()) {
            qCWarning(WEBSOCKET_LOG) << "empty client type given when registering";
            return;
        }

        const auto emails = arguments["emails"_L1].toArray();
        if (type == "webclient"_L1) {
            if (emails.isEmpty()) {
                qCWarning(WEBSOCKET_LOG) << "empty email given";
            }
            for (const auto &email : emails) {
                m_webClientsMappingToEmail[email.toString()] = socket;
                qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a web client";

                const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()];
                if (nativeClient) {
                    QJsonDocument doc(QJsonObject{{"type"_L1, "connection"_L1}, {"payload"_L1, QJsonObject{{"client_type"_L1, "web_client"_L1}}}});
                    nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
                }
            }
        } else {
            if (emails.isEmpty()) {
                qCWarning(WEBSOCKET_LOG) << "empty email given";
            }
            for (const auto &email : emails) {
                m_nativeClientsMappingToEmail[email.toString()] = socket;
                qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a native client";

                const auto webClient = m_webClientsMappingToEmail[email.toString()];
                if (webClient) {
                    QJsonDocument doc(QJsonObject{{"type"_L1, "connection"_L1}, {"payload"_L1, QJsonObject{{"client_type"_L1, "native_client"_L1}}}});
                    webClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
                }
            }
        }
        return;
    }
    case Command::EmailSent: {
        const auto email = arguments["email"_L1].toString();
        const auto socket = m_nativeClientsMappingToEmail[email];
        if (!socket) {
            return;
        }
        QJsonDocument doc(QJsonObject{
            {"type"_L1, "email-sent"_L1},
            {"arguments"_L1, arguments},
        });
        socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
        return;
    }
    case Command::Undefined:
        qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command";
        return;
    }
}

bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload)
{
    auto socket = m_webClientsMappingToEmail[email];
    if (!socket) {
        return false;
    }

    socket->sendTextMessage(QString::fromUtf8(payload));
    return true;
}

void WebServer::processBinaryMessage(QByteArray message)
{
    qCWarning(WEBSOCKET_LOG) << "got binary message" << message;
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient) {
        pClient->sendBinaryMessage(message);
    }
}

void WebServer::socketDisconnected()
{
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient) {
        qCWarning(WEBSOCKET_LOG) << "Client disconnected" << pClient;
        // Web client was disconnected
        {
            const auto it = std::find_if(m_webClientsMappingToEmail.cbegin(), m_webClientsMappingToEmail.cend(), [pClient](QWebSocket *webSocket) {
                return pClient == webSocket;
            });

            if (it != m_webClientsMappingToEmail.cend()) {
                const auto email = it.key();
                const auto nativeClient = m_nativeClientsMappingToEmail[email];
                qCInfo(WEBSOCKET_LOG) << "webclient with email disconnected" << email << nativeClient;
                if (nativeClient) {
                    QJsonDocument doc(QJsonObject{
                        {"type"_L1, "disconnection"_L1},
                    });
                    nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
                }

                m_webClientsMappingToEmail.removeIf([pClient](auto socket) {
                    return pClient == socket.value();
                });
            }
        }

        // Native client was disconnected
        const auto emails = m_nativeClientsMappingToEmail.keys();
        for (const auto &email : emails) {
            const auto webSocket = m_nativeClientsMappingToEmail[email];
            if (webSocket != pClient) {
                qCWarning(WEBSOCKET_LOG) << "webSocket not equal" << email << webSocket << pClient;
                continue;
            }

            qCInfo(WEBSOCKET_LOG) << "native client for" << email << "was disconnected.";

            QJsonDocument doc(QJsonObject{
                {"type"_L1, "disconnection"_L1},
            });
            sendMessageToWebClient(email, doc.toJson());
        }

        m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) {
            return pClient == socket.value();
        });

        m_clients.removeAll(pClient);
    }
}
