// 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 <QSslServer>
#include <QStandardPaths>
#include <QWebSocket>
#include <QWebSocketCorsAuthenticator>
#include <QWebSocketServer>
#include <protocol.h>

#include "controllers/staticcontroller.h"
#include "http_debug.h"
#include "model/serverstate.h"
#include "websocket_debug.h"

#include <KLocalizedString>

using namespace Qt::Literals::StringLiterals;
using namespace Protocol;

WebServer::WebServer(QObject *parent)
    : QObject(parent)
    , 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);

    QSslConfiguration sslConfiguration;
    sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone);
    sslConfiguration.setLocalCertificate(sslCertificateChain.front());
    sslConfiguration.setPrivateKey(sslKey);

#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
    m_tcpserver = std::make_unique<QSslServer>();
    m_tcpserver->setSslConfiguration(sslConfiguration);
    if (!m_tcpserver->listen(QHostAddress::LocalHost, WebServer::Port) || !m_httpServer->bind(m_tcpserver.get())) {
        qCFatal(HTTP_LOG) << "Server failed to listen on a port.";
        return false;
    }
    quint16 port = m_tcpserver->serverPort();
#else
    m_httpServer->sslSetup(sslCertificateChain.front(), sslKey);
    quint16 port = m_httpServer->listen(QHostAddress::Any, WebServer::Port);
#endif
    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);

    m_webSocketServer->setSslConfiguration(sslConfiguration);

    connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection);
    connect(m_webSocketServer, &QWebSocketServer::originAuthenticationRequired, this, [](QWebSocketCorsAuthenticator *authenticator) {
        const auto origin = authenticator->origin();
        // Only allow the gpgol-client and localhost:5656 to connect to this host
        // Otherwise any tab from the browser is able to access the websockets
        authenticator->setAllowed(origin == u"Client"_s || origin == u"https://localhost:5656"_s);
    });

    if (m_webSocketServer->listen(QHostAddress::LocalHost, 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);
    }

    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();

        processCommand(object, webClient);
    }
}

void WebServer::processCommand(const QJsonObject &object, QWebSocket *socket)
{
    if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) {
        qCWarning(WEBSOCKET_LOG) << "Invalid json received: no type or arguments set" << object;
        return;
    }

    const auto arguments = object["arguments"_L1].toObject();
    const auto command = commandFromString(object["command"_L1].toString());

    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{{"command"_L1, Protocol::commandToString(Protocol::Connection)},
                                                  {"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()];
                bool hasWebClient = false;
                if (webClient) {
                    hasWebClient = true;
                    const QJsonDocument doc(QJsonObject{{"command"_L1, Protocol::commandToString(Protocol::Connection)},
                                                        {
                                                            "payload"_L1,
                                                            QJsonObject{{"client_type"_L1, "native_client"_L1}},
                                                        }});
                    webClient->sendTextMessage(QString::fromUtf8(doc.toJson()));
                }

                if (hasWebClient) {
                    const QJsonDocument doc(QJsonObject{
                        {"command"_L1, Protocol::commandToString(Protocol::Connection)},
                        {"payload"_L1, QJsonObject{{"client_type"_L1, "web_client"_L1}}},
                    });
                    socket->sendTextMessage(QString::fromUtf8(doc.toJson()));
                }
            }
        }
        return;
    }
    case Command::EwsResponse: {
        const auto email = arguments["email"_L1].toString();
        QJsonDocument doc(QJsonObject{
            {"command"_L1, Protocol::commandToString(Protocol::EwsResponse)},
            {"arguments"_L1, arguments},
        });
        sendMessageToNativeClient(email, doc.toJson());
        return;
    }
    case Command::View:
    case Command::Reply:
    case Command::Forward:
    case Command::Composer:
    case Command::OpenDraft:
    case Command::RestoreAutosave:
    case Command::Info:
    case Command::DeleteDraft: {
        const auto email = arguments["email"_L1].toString();
        const auto token = QUuid::createUuid().toString(QUuid::WithoutBraces);
        object["token"_L1] = token;

        auto &serverState = ServerState::instance();
        serverState.composerRequest[token.toUtf8()] = email;

        const QJsonDocument doc(object); // simple forwarding to the native client

        const auto ok = sendMessageToNativeClient(email, doc.toJson());
        if (!ok) {
            socket->sendTextMessage(i18n("Unable to found corresponding native client."));
        }
        return;
    }
    case Command::InfoFetched: {
        const auto email = arguments["email"_L1].toString();
        QJsonDocument doc(QJsonObject{
            {"command"_L1, Protocol::commandToString(Protocol::InfoFetched)},
            {"arguments"_L1, arguments},
        });
        const auto ok = sendMessageToWebClient(email, doc.toJson());
        if (!ok) {
            qCWarning(WEBSOCKET_LOG) << "Unable to send info fetched to web client";
        }
        return;
    }
    case Command::Ews: {
        const auto email = arguments["email"_L1].toString();
        QJsonDocument doc(QJsonObject{
            {"command"_L1, Protocol::commandToString(Protocol::Ews)},
            {"arguments"_L1, arguments},
        });
        const auto ok = sendMessageToWebClient(email, doc.toJson());
        if (!ok) {
            qCWarning(WEBSOCKET_LOG) << "Unable to send ews request to web client";
        }
        return;
    }
    case Command::ViewerClosed:
    case Command::ViewerOpened: {
        const auto email = arguments["email"_L1].toString();
        QJsonDocument doc(QJsonObject{
            {"command"_L1, Protocol::commandToString(command)},
            {"arguments"_L1, arguments},
        });
        const auto ok = sendMessageToWebClient(email, doc.toJson());
        if (!ok) {
            qCWarning(WEBSOCKET_LOG) << "Unable to send viewer close/open request to web client";
        }
        return;
    }
    case Command::Log:
        qWarning(WEBSOCKET_LOG) << arguments["message"_L1].toString() << arguments["args"_L1].toString();
        return;
    default:
        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;
}

bool WebServer::sendMessageToNativeClient(const QString &email, const QByteArray &payload)
{
    auto socket = m_nativeClientsMappingToEmail[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()
{
    auto pClient = qobject_cast<QWebSocket *>(sender());
    if (!pClient) {
        return;
    }
    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{
                    {"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
                });
                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{
            {"command"_L1, Protocol::commandToString(Protocol::Disconnection)},
        });
        sendMessageToWebClient(email, doc.toJson());
    }

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

    m_clients.removeAll(pClient);
}
