// 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>
#include <MimeTreeParserCore/ObjectTreeParser>

// gpgme headers
#include <gpgme++/key.h>
#include <kmime/message.h>
#include <quuid.h>

#include "draft/draftmanager.h"
#include "editor/composerwindow.h"
#include "editor/composerwindowfactory.h"
#include "emailviewer.h"
#include "ews/ewstypes.h"
#include "gpgol_client_debug.h"
#include "gpgoljs_version.h"
#include "protocol.h"
#include "qnam.h"
#include "websocket_debug.h"

#include "ews/ewsitem.h"
#include "ews/ewspropertyfield.h"
#include "ews/ewsserverversion.h"
#include "ews/ewsxml.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;
}
}

WebsocketClient &WebsocketClient::self(const QUrl &url, 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, clientId);
    }
    return *client;
};

WebsocketClient::WebsocketClient(const QUrl &url, const QString &clientId)
    : QObject(nullptr)
    , m_webSocket(QWebSocket(QStringLiteral("Client")))
    , m_url(url)
    , 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, Protocol::commandToString(Protocol::Register)},
        {
            "arguments"_L1,
            QJsonObject{{"emails"_L1, QJsonArray::fromStringList(m_emails)}, {"type"_L1, "nativeclient"_L1}},
        },
    });
    m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson()));

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

bool WebsocketClient::sendEWSRequest(const QString &fromEmail, const QString &requestId, const QString &requestBody)
{
    KMime::Types::Mailbox mailbox;
    mailbox.fromUnicodeString(fromEmail);

    const QJsonObject json{
        {"command"_L1, Protocol::commandToString(Protocol::Ews)},
        {"arguments"_L1,
         QJsonObject{
             {"body"_L1, requestBody},
             {"email"_L1, QString::fromUtf8(mailbox.address())},
             {"requestId"_L1, requestId},
         }},
    };

    m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
    return true;
}

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();
    const auto command = Protocol::commandFromString(object["command"_L1].toString());
    const auto args = object["arguments"_L1].toObject();

    switch (command) {
    case Protocol::Disconnection:
        // 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();
        return;
    case Protocol::Connection:
        // reconnection of the web client
        m_state = Connected;
        m_stateDisplay = i18nc("@info", "Connected.");
        Q_EMIT stateChanged();
        return;
    case Protocol::View: {
        const auto email = args["email"_L1].toString();
        const auto displayName = args["displayName"_L1].toString();
        const auto content = args["body"_L1].toString();
        const auto token = args["token"_L1].toString();

        if (!m_emailViewer) {
            m_emailViewer = new EmailViewer(content, email, displayName, token);
            m_emailViewer->setAttribute(Qt::WA_DeleteOnClose);
        } else {
            m_emailViewer->view(content, email, displayName, token);
        }

        m_emailViewer->show();
        m_emailViewer->activateWindow();
        m_emailViewer->raise();
        return;
    }
    case Protocol::RestoreAutosave: {
        const auto email = args["email"_L1].toString();
        const auto displayName = args["displayName"_L1].toString();
        const auto bearerToken = args["token"_L1].toString().toUtf8();
        ComposerWindowFactory::self().restoreAutosave(email, displayName, bearerToken);
        return;
    }
    case Protocol::EwsResponse: {
        // confirmation that the email was sent
        const auto args = object["arguments"_L1].toObject();
        Q_EMIT ewsResponseReceived(args["requestId"_L1].toString(), args["body"_L1].toString());
        return;
    }
    case Protocol::Composer:
    case Protocol::Reply:
    case Protocol::Forward:
    case Protocol::OpenDraft: {
        const auto email = args["email"_L1].toString();
        const auto displayName = args["displayName"_L1].toString();
        const auto bearerToken = args["token"_L1].toString().toUtf8();

        auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken);

        if (command == Protocol::Reply || command == Protocol::Forward) {
            const auto content = args["body"_L1].toString();

            KMime::Message::Ptr message(new KMime::Message());
            message->setContent(KMime::CRLFtoLF(content.toUtf8()));
            message->parse();
            if (command == Protocol::Reply) {
                dialog->reply(message);
            } else {
                dialog->forward(message);
            }
        } else if (command == Protocol::OpenDraft) {
            const auto draftId = args["id"_L1].toString();
            if (draftId.isEmpty()) {
                return;
            }
            const auto draft = DraftManager::self().draftById(draftId.toUtf8());
            dialog->setMessage(draft.mime());
        }
        dialog->show();
        dialog->activateWindow();
        dialog->raise();
        return;
    }
    case Protocol::DeleteDraft: {
        const auto draftId = args["id"_L1].toString();
        if (draftId.isEmpty()) {
            qWarning() << "Draft not valid";
            return;
        }
        const auto draft = DraftManager::self().draftById(draftId.toUtf8());

        if (!draft.isValid()) {
            qWarning() << "Draft not valid";
            return;
        }

        if (!DraftManager::self().remove(draft)) {
            qCWarning(GPGOL_CLIENT_LOG) << "Could not delete draft";
            return;
        }
        return;
    }
    case Protocol::Info: {
        const auto content = args["body"_L1].toString();
        KMime::Message::Ptr message(new KMime::Message());
        message->setContent(KMime::CRLFtoLF(content.toUtf8()));
        message->parse();

        MimeTreeParser::ObjectTreeParser treeParser;
        treeParser.parseObjectTree(message.get());

        const QJsonObject json{{"command"_L1, Protocol::commandToString(Protocol::InfoFetched)},
                               {"arguments"_L1,
                                QJsonObject{
                                    {"itemId"_L1, args["itemId"_L1]},
                                    {"email"_L1, args["email"_L1]},
                                    {"encrypted"_L1, treeParser.hasEncryptedParts()},
                                    {"signed"_L1, treeParser.hasSignedParts()},
                                    {"drafts"_L1, DraftManager::self().toJson()},
                                    {"viewerOpen"_L1, !m_emailViewer.isNull()},
                                    {"version"_L1, QStringLiteral(GPGOLJS_VERSION_STRING)},
                                }}};

        m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
        return;
    }
    default:
        qCWarning(WEBSOCKET_LOG) << "Unhandled command" << command;
    }
}

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

void WebsocketClient::sendViewerClosed(const QString &email)
{
    const QJsonObject json{
        {"command"_L1, Protocol::commandToString(Protocol::ViewerClosed)},
        {"arguments"_L1, QJsonObject{{"email"_L1, email}}},
    };

    m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
}

void WebsocketClient::sendViewerOpened(const QString &email)
{
    const QJsonObject json{
        {"command"_L1, Protocol::commandToString(Protocol::ViewerOpened)},
        {"arguments"_L1, QJsonObject{{"email"_L1, email}}},
    };

    m_webSocket.sendTextMessage(QString::fromUtf8(QJsonDocument(json).toJson()));
}
