// 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 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;
}
}

void WebsocketClient::registerServer()
{
    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, m_port},
                                                      {"emails"_L1, QJsonArray::fromStringList(m_emails)},
                                                      {"clientId"_L1, m_clientId},
                                                  })
                                        .toJson());

    connect(registerReply, &QNetworkReply::finished, qnam, [this, registerReply]() {
        if (registerReply->error() != QNetworkReply::NoError) {
            QTimer::singleShot(m_delay, this, [this]() {
                m_delay += m_delay / 2;
                registerServer();
            });
            qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << m_delay;
        } else {
            qWarning() << "Register";
            m_delay = 2000ms;
        }

        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)
    , m_state(NotConnected)
    , m_stateDisplay(i18nc("@info", "Loading..."))
{
    auto globalKeyCache = Kleo::KeyCache::instance();
    m_emails = trustedEmails(globalKeyCache);
    if (m_emails.isEmpty()) {
        qWarning() << "No ultimately keys found in keychain";
        return;
    }

    connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected);
    connect(&m_webSocket, &QWebSocket::disconnected, this, [this] {
        m_state = NotConnected;
        m_stateDisplay = i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.");
        Q_EMIT stateChanged();
    });
    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);

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

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_state = NotConnected; /// We still need to connect to the web client
    m_stateDisplay = i18nc("@info", "Waiting for web client.");
    Q_EMIT stateChanged();
}

void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error)
{
    qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString();
    m_state = NotConnected;
    m_stateDisplay = i18nc("@info", "Could not reach the Outlook extension.");
    Q_EMIT stateChanged();
    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
        m_state = NotConnected;
        m_stateDisplay = i18nc("@info", "Connection to the Outlook extension lost. Make sure the extension pane is open.");
        Q_EMIT stateChanged();
    } else if (object["type"_L1] == "connection"_L1) {
        // reconnection of the web client
        m_state = Connected;
        m_stateDisplay = i18nc("@info", "Connected.");
        Q_EMIT stateChanged();
    } 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);
    });
}

WebsocketClient::State WebsocketClient::state() const
{
    return m_state;
}

QString WebsocketClient::stateDisplay() const
{
    return m_stateDisplay;
}
