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

#include "websocketclient.h"

// Qt headers
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QStandardPaths>
#include <QTimer>

// KDE headers
#include <KLocalizedString>
#include <Libkleo/KeyCache>

// gpgme headers
#include <gpgme++/key.h>

#include "qnam.h"
#include "websocket_debug.h"

using namespace std::chrono;
using namespace Qt::Literals::StringLiterals;

namespace
{
QStringList trustedEmails(const std::shared_ptr<const Kleo::KeyCache> &keyCache)
{
    QStringList emails;

    const auto keys = keyCache->keys();
    for (const auto &key : keys) {
        for (const auto &userId : key.userIDs()) {
            if (key.ownerTrust() == GpgME::Key::Ultimate) {
                emails << QString::fromLatin1(userId.email()).toLower();
                break;
            }
        }
    }

    return emails;
}

auto delay = 2000ms;

void registerServer(int port, const QStringList &emails, const QString &clientId)
{
    QNetworkRequest registerRequest(QUrl(u"https://127.0.0.1:5656/register"_s));
    registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s);
    auto registerReply = qnam->post(registerRequest,
                                    QJsonDocument(QJsonObject{
                                                      {"port"_L1, port},
                                                      {"emails"_L1, QJsonArray::fromStringList(emails)},
                                                      {"clientId"_L1, clientId},
                                                  })
                                        .toJson());

    QObject::connect(registerReply, &QNetworkReply::finished, qnam, [port, emails, clientId, registerReply]() {
        if (registerReply->error() != QNetworkReply::NoError) {
            QTimer::singleShot(delay, [emails, port, clientId]() {
                delay *= 2;
                registerServer(port, emails, clientId);
            });
            qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << delay;
        } else {
            qWarning() << "Register";
        }

        registerReply->deleteLater();
    });
}
}

WebsocketClient &WebsocketClient::self(const QUrl &url, int port, const QString &clientId)
{
    static WebsocketClient *client = nullptr;
    if (!client && url.isEmpty()) {
        qFatal() << "Unable to create a client without an url";
    } else if (!client) {
        client = new WebsocketClient(url, port, clientId);
    }
    return *client;
};

WebsocketClient::WebsocketClient(const QUrl &url, int port, const QString &clientId)
    : QObject(nullptr)
    , m_webSocket(QWebSocket(QStringLiteral("Client")))
    , m_url(url)
    , m_port(port)
    , m_clientId(clientId)
{
    connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected);
    connect(&m_webSocket, &QWebSocket::disconnected, this, [this] {
        Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a disconnection with the broker."));
    });
    connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred);
    connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived);
    connect(&m_webSocket, QOverload<const QList<QSslError> &>::of(&QWebSocket::sslErrors), this, [this](const QList<QSslError> &errors) {
        // TODO remove
        m_webSocket.ignoreSslErrors(errors);
    });

    QSslConfiguration sslConfiguration;
    auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem"));
    Q_ASSERT(!certPath.isEmpty());

    QFile certFile(certPath);
    if (!certFile.open(QIODevice::ReadOnly)) {
        qFatal() << "Couldn't read certificate" << certPath;
    }
    QSslCertificate certificate(&certFile, QSsl::Pem);
    certFile.close();
    sslConfiguration.addCaCertificate(certificate);
    m_webSocket.setSslConfiguration(sslConfiguration);

    m_webSocket.open(url);

    auto globalKeyCache = Kleo::KeyCache::instance();

    m_emails = trustedEmails(globalKeyCache);
    if (m_emails.isEmpty()) {
        qWarning() << "No ultimately keys found in keychain";
        return;
    }

    // TODO remove me
    QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList<QSslError> &errors) {
        reply->ignoreSslErrors();
    });

    registerServer(m_port, m_emails, m_clientId);
}

void WebsocketClient::slotConnected()
{
    qCInfo(WEBSOCKET_LOG) << "websocket connected";
    QJsonDocument doc(QJsonObject{{"command"_L1, "register"_L1},
                                  {"arguments"_L1, QJsonObject{{"emails"_L1, QJsonArray::fromStringList(m_emails)}, {"type"_L1, "nativeclient"_L1}}}});
    m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson()));

    registerServer(m_port, m_emails, m_clientId);

    Q_EMIT connected();
}

void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
    qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
    Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a connection error."));
    reconnect();
}

void WebsocketClient::slotTextMessageReceived(QString message)
{
    const auto doc = QJsonDocument::fromJson(message.toUtf8());
    if (!doc.isObject()) {
        qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message;
        return;
    }

    const auto object = doc.object();
    qCDebug(WEBSOCKET_LOG) << object;
    if (object["type"_L1] == "disconnection"_L1) {
        // disconnection of the web client
        Q_EMIT closed(i18nc("@info", "Connection to outlook lost. Make sure the extension tab is open."));
    } else if (object["type"_L1] == "connection"_L1) {
        // reconnection of the web client
        Q_EMIT connected();
    } else if (object["type"_L1] == "email-sent"_L1) {
        // confirmation that the email was sent
        const auto args = object["arguments"_L1].toObject();
        Q_EMIT emailSentSuccessfully(args["id"_L1].toString());
    }
}

void WebsocketClient::reconnect()
{
    QTimer::singleShot(1000ms, this, [this]() {
        m_webSocket.open(m_url);
    });
}
