// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "neochatconnection.h"

#include "general_logging.h"

#include <QImageReader>
#include <QJsonDocument>

#include "jobs/neochatauthmetadatajob.h"
#include "jobs/neochatreportuserjob.h"
#include "jobs/neochatprofilefieldjobs.h"
#include "neochatroom.h"
#include "spacehierarchycache.h"

#include <Quotient/jobs/basejob.h>
#include <Quotient/quotient_common.h>
#include <qt6keychain/keychain.h>

#include <KLocalizedString>
#include <QGuiApplication>

#include <Quotient/csapi/capabilities.h>
#include <Quotient/csapi/content-repo.h>
#include <Quotient/csapi/profile.h>
#include <Quotient/csapi/registration.h>
#include <Quotient/csapi/versions.h>
#include <Quotient/e2ee/sssshandler.h>
#include <Quotient/jobs/downloadfilejob.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/room.h>
#include <Quotient/settings.h>
#include <Quotient/user.h>

#ifdef HAVE_KUNIFIEDPUSH
#include <QCoroNetwork>
#include <Quotient/csapi/pusher.h>
#include <Quotient/networkaccessmanager.h>
#endif

using namespace Quotient;
using namespace Qt::StringLiterals;

bool NeoChatConnection::m_globalUrlPreviewDefault = true;
PushRuleAction::Action NeoChatConnection::m_defaultAction = PushRuleAction::Unknown;

NeoChatConnection::NeoChatConnection(QObject *parent)
    : Connection(parent)
{
    connectSignals();
}

NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
    : Connection(server, parent)
{
    connectSignals();
}

void NeoChatConnection::connectSignals()
{
    connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
        if (type == u"m.identity_server"_s) {
            Q_EMIT identityServerChanged();
        }
        if (type == u"m.invite_permission_config"_s) {
            Q_EMIT blockAllInvitesChanged();
        }
    });
    connect(this, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
        if (job->error() == BaseJob::UserConsentRequired) {
            Q_EMIT userConsentRequired(job->errorUrl());
        }
    });
    connect(this, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
        if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData().value("errcode"_L1).toString() == "M_TOO_LARGE"_L1) {
            Q_EMIT showMessage(MessageType::Warning, i18n("File too large to download.<br />Contact your matrix server administrator for support."));
        }
    });
    connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
        Q_EMIT directChatInvitesChanged();
        for (const auto &chatId : additions) {
            if (const auto chat = room(chatId)) {
                connect(chat, &Room::changed, this, [this](Room::Changes changes) {
                    if (changes & (Room::Change::UnreadStats | Room::Change::Highlights)) {
                        refreshBadgeNotificationCount();
                        Q_EMIT directChatNotificationsChanged();
                        Q_EMIT directChatsHaveHighlightNotificationsChanged();
                    }
                });
            }
        }
        for (const auto &chatId : removals) {
            if (const auto chat = room(chatId)) {
                disconnect(chat, &Room::changed, this, nullptr);
            }
        }
    });
    connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
        if (room->isDirectChat()) {
            connect(room, &Room::changed, this, [this](Room::Changes changes) {
                if (changes & (Room::Change::UnreadStats | Room::Change::Highlights)) {
                    Q_EMIT directChatNotificationsChanged();
                    Q_EMIT directChatsHaveHighlightNotificationsChanged();
                }
            });
        }
        Q_EMIT roomInvitesChanged();
        connect(room, &Room::changed, this, [this](Room::Changes changes) {
            if (changes & (Room::Change::UnreadStats | Room::Change::Highlights)) {
                refreshBadgeNotificationCount();
                Q_EMIT homeNotificationsChanged();
                Q_EMIT homeHaveHighlightNotificationsChanged();
            }
        });
    });
    connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
        Q_UNUSED(room)
        if (prev && prev->isDirectChat()) {
            Q_EMIT directChatInvitesChanged();
            Q_EMIT directChatNotificationsChanged();
            Q_EMIT directChatsHaveHighlightNotificationsChanged();
        }
        refreshBadgeNotificationCount();
        Q_EMIT homeNotificationsChanged();
        Q_EMIT homeHaveHighlightNotificationsChanged();
    });

    connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
        refreshBadgeNotificationCount();
        Q_EMIT homeNotificationsChanged();
        Q_EMIT homeHaveHighlightNotificationsChanged();
    });

    connect(this, &NeoChatConnection::globalUrlPreviewEnabledChanged, this, [this]() {
        if (!m_globalUrlPreviewDefault) {
            m_linkPreviewers.clear();
        }
    });

    // Fetch unstable features
    // TODO: Expose unstableFeatures() in libQuotient
    connect(
        this,
        &Connection::connected,
        this,
        [this] {
            callApi<GetVersionsJob>(BackgroundRequest).onResult([this](const auto &job) {
                m_canCheckMutualRooms =
                    job->unstableFeatures().value("uk.half-shot.msc2666.query_mutual_rooms.stable"_L1, false) || job->versions().count("v1.19"_L1);
                Q_EMIT canCheckMutualRoomsChanged();
                m_canEraseData = job->unstableFeatures().value("org.matrix.msc4025"_L1, false) || job->versions().count("v1.10"_L1);
                Q_EMIT canEraseDataChanged();
                m_supportsProfileFields = job->unstableFeatures().value("uk.tcpip.msc4133.stable"_L1, false) || job->versions().count("v1.16"_L1);
                Q_EMIT supportsProfileFieldsChanged();

                if (m_supportsProfileFields) {
                    callApi<NeoChatGetProfileFieldJob>(BackgroundRequest, userId(), QStringLiteral("m.tz")).then([this](const auto &job) {
                        m_profileFields[QStringLiteral("m.tz")] = job->value();
                    });
                }
            });
            callApi<GetCapabilitiesJob>(BackgroundRequest).onResult([this](const auto &job) {
                const auto profileFields = job->capabilities().additionalProperties["m.profile_fields"_L1].toMap();

                m_profileFieldsWriteable = profileFields["enabled"_L1].toBool();
                m_allowedFields = profileFields["allowed"_L1].toStringList();
                m_disallowedFields = profileFields["disallowed"_L1].toStringList();
            });

            callApi<NeoChatAuthMetadataJob>().then([this](const auto &job) {
                setAccountManagementUri(job->jsonData()[u"account_management_uri"_s].toString());
            });
        },
        Qt::SingleShotConnection);

    connect(
        this,
        &Connection::syncDone,
        this,
        [this] {
            m_syncDone = true;
            Q_EMIT ownSessionVerified();
        },
        Qt::SingleShotConnection);
}

int NeoChatConnection::badgeNotificationCount() const
{
    return m_badgeNotificationCount;
}

void NeoChatConnection::refreshBadgeNotificationCount()
{
    int count = 0;
    const auto rooms = allRooms();
    for (const auto &r : rooms) {
        if (const auto room = static_cast<NeoChatRoom *>(r)) {
            count += room->contextAwareNotificationCount();

            if (room->joinState() == JoinState::Invite) {
                count++;
            }
        }
    }

    if (count != m_badgeNotificationCount) {
        m_badgeNotificationCount = count;
        Q_EMIT badgeNotificationCountChanged(m_badgeNotificationCount);
    }
}

bool NeoChatConnection::globalUrlPreviewEnabled()
{
    return m_globalUrlPreviewDefault;
}

void NeoChatConnection::setGlobalUrlPreviewDefault(bool useByDefault)
{
    NeoChatConnection::m_globalUrlPreviewDefault = useByDefault;
}

PushRuleAction::Action NeoChatConnection::keywordPushRuleDefault() const
{
    return m_defaultAction;
}

void NeoChatConnection::setKeywordPushRuleDefault(PushRuleAction::Action defaultAction)
{
    NeoChatConnection::m_defaultAction = defaultAction;
}

void NeoChatConnection::logout(bool serverSideLogout)
{
    SettingsGroup(u"Accounts"_s).remove(userId());

    QKeychain::DeletePasswordJob job(qAppName());
    job.setAutoDelete(true);
    job.setKey(userId());
    QEventLoop loop;
    QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
    job.start();
    loop.exec();

    if (!serverSideLogout) {
        return;
    }
    Connection::logout();
}

bool NeoChatConnection::setAvatar(const QUrl &avatarSource)
{
    QString decoded = avatarSource.path();
    if (decoded.isEmpty()) {
        callApi<SetAvatarUrlJob>(user()->id(), avatarSource);
        return true;
    }
    if (QImageReader(decoded).read().isNull()) {
        return false;
    } else {
        return user()->setAvatar(decoded);
    }
}

QVariantList NeoChatConnection::getSupportedRoomVersions() const
{
    const auto &roomVersions = availableRoomVersions();
    QVariantList supportedRoomVersions;
    for (const auto &v : roomVersions) {
        QVariantMap roomVersionMap;
        roomVersionMap.insert("id"_L1, v.id);
        roomVersionMap.insert("status"_L1, v.status);
        roomVersionMap.insert("isStable"_L1, v.isStable());
        supportedRoomVersions.append(roomVersionMap);
    }
    return supportedRoomVersions;
}

bool NeoChatConnection::canCheckMutualRooms() const
{
    return m_canCheckMutualRooms;
}

void NeoChatConnection::changePassword(const QString &currentPassword, const QString &newPassword)
{
    callApi<ChangePasswordJob>(newPassword, false).onFailure([this, currentPassword, newPassword](const auto &job) {
        QJsonObject replyData = job->jsonData();
        AuthenticationData authData;
        authData.session = replyData["session"_L1].toString();
        authData.type = "m.login.password"_L1;
        authData.authInfo["password"_L1] = currentPassword;
        authData.authInfo["user"_L1] = user()->id();
        authData.authInfo["identifier"_L1] = QJsonObject{{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
        auto innerJob = callApi<ChangePasswordJob>(newPassword, false, authData)
                            .then(
                                [this]() {
                                    Q_EMIT passwordStatus(PasswordStatus::Success);
                                },
                                [this](const auto &job) {
                                    Q_EMIT passwordStatus(job->jsonData()["errcode"_L1] == "M_FORBIDDEN"_L1 ? PasswordStatus::Wrong : PasswordStatus::Other);
                                });
    });
}

void NeoChatConnection::deactivateAccount(const QString &password, const bool erase)
{
    callApi<DeactivateAccountJob>().onFailure([password, erase, this](const auto &job) {
        QJsonObject replyData = job->jsonData();
        AuthenticationData authData;
        authData.session = replyData["session"_L1].toString();
        authData.authInfo["password"_L1] = password;
        authData.type = "m.login.password"_L1;
        authData.authInfo["user"_L1] = user()->id();
        QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
        authData.authInfo["identifier"_L1] = identifier;
        callApi<DeactivateAccountJob>(authData, QString{}, erase).onResult([this]() {
            logout(false);
        });
    });
}

bool NeoChatConnection::hasIdentityServer() const
{
    if (!hasAccountData(u"m.identity_server"_s)) {
        return false;
    }

    const auto url = accountData(u"m.identity_server"_s)->contentPart<QUrl>("base_url"_L1);
    if (!url.isEmpty()) {
        return true;
    }
    return false;
}

QUrl NeoChatConnection::identityServer() const
{
    if (!hasAccountData(u"m.identity_server"_s)) {
        return {};
    }

    const auto url = accountData(u"m.identity_server"_s)->contentPart<QUrl>("base_url"_L1);
    if (!url.isEmpty()) {
        return url;
    }
    return {};
}

QString NeoChatConnection::identityServerUIString() const
{
    if (!hasIdentityServer()) {
        return i18nc("@info", "No identity server configured");
    }

    return identityServer().toString();
}

void NeoChatConnection::createRoom(const QString &name, const QString &topic, const QString &parent, bool isPrivate, const QString &address)
{
    QList<CreateRoomJob::StateEvent> initialStateEvents;
    if (!parent.isEmpty()) {
        initialStateEvents.append(CreateRoomJob::StateEvent{
            "m.space.parent"_L1,
            QJsonObject{
                {"canonical"_L1, true},
                {"via"_L1, QJsonArray{domain()}},
            },
            parent,
        });
    }

    Connection::createRoom(isPrivate ? Connection::UnpublishRoom : Connection::PublishRoom,
                           isPrivate ? QString() : address,
                           name,
                           topic,
                           QStringList(),
                           {},
                           {},
                           {},
                           initialStateEvents)
        .then(
            [parent, this](const auto &job) {
                if (parent.isEmpty()) {
                    return;
                }
                if (auto parentRoom = room(parent)) {
                    parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
                }
            },
            [this](const auto &job) {
                Q_EMIT errorOccured(i18n("Room creation failed: %1", job->errorString()));
            });
}

void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent)
{
    QList<CreateRoomJob::StateEvent> initialStateEvents;
    if (!parent.isEmpty()) {
        initialStateEvents.append(CreateRoomJob::StateEvent{
            "m.space.parent"_L1,
            QJsonObject{
                {"canonical"_L1, true},
                {"via"_L1, QJsonArray{domain()}},
            },
            parent,
        });
    }

    Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_L1, "m.space"_L1}})
        .then(
            [parent, this](const auto &job) {
                if (parent.isEmpty()) {
                    return;
                }
                if (auto parentRoom = room(parent)) {
                    parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
                }
            },
            [this](const auto &job) {
                Q_EMIT errorOccured(i18n("Space creation failed: %1", job->errorString()));
            });
}

Quotient::ForgetRoomJob *NeoChatConnection::forgetRoom(const QString &id)
{
    Q_EMIT roomAboutToBeLeft(id);

    return Connection::forgetRoom(id);
}

bool NeoChatConnection::directChatExists(Quotient::User *user)
{
    return directChats().contains(user);
}

qsizetype NeoChatConnection::directChatNotifications() const
{
    qsizetype notifications = 0;
    QStringList added; // The same ID can be in the list multiple times.
    const auto roomIds = directChats();
    for (const auto &chatId : roomIds) {
        if (!added.contains(chatId)) {
            if (const auto chat = room(chatId)) {
                notifications += dynamic_cast<NeoChatRoom *>(chat)->contextAwareNotificationCount();
                added += chatId;
            }
        }
    }
    return notifications;
}

bool NeoChatConnection::directChatsHaveHighlightNotifications() const
{
    const auto roomIds = directChats();
    for (const auto &childId : roomIds) {
        if (const auto child = static_cast<NeoChatRoom *>(room(childId))) {
            if (child->highlightCount() > 0) {
                return true;
            }
        }
    }
    return false;
}

qsizetype NeoChatConnection::homeNotifications() const
{
    qsizetype notifications = 0;
    QStringList added;
    const auto &spaceHierarchyCache = SpaceHierarchyCache::instance();
    const auto rooms = allRooms();
    for (const auto &r : rooms) {
        if (const auto room = static_cast<NeoChatRoom *>(r)) {
            if (!added.contains(room->id()) && !room->isDirectChat() && !spaceHierarchyCache.isChild(room->id())) {
                notifications += dynamic_cast<NeoChatRoom *>(room)->contextAwareNotificationCount();
                added += room->id();
            }
        }
    }
    return notifications;
}

bool NeoChatConnection::homeHaveHighlightNotifications() const
{
    const auto &spaceHierarchyCache = SpaceHierarchyCache::instance();
    const auto rooms = allRooms();
    for (const auto &r : rooms) {
        if (const auto room = static_cast<NeoChatRoom *>(r)) {
            if (!room->isDirectChat() && !spaceHierarchyCache.isChild(room->id()) && room->highlightCount() > 0) {
                return true;
            }
        }
    }
    return false;
}

qsizetype NeoChatConnection::directChatInvites() const
{
    const auto inviteRooms = rooms(JoinState::Invite);
    return std::ranges::count_if(inviteRooms, [](const auto room) {
        return room->isDirectChat();
    });
}

qsizetype NeoChatConnection::roomInvites() const
{
    const auto inviteRooms = rooms(JoinState::Invite);
    return std::ranges::count_if(inviteRooms, [](const auto room) {
        return !room->isDirectChat();
    });
}

QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
{
#ifdef HAVE_KUNIFIEDPUSH
    QUrl gatewayEndpoint(endpoint);
    gatewayEndpoint.setPath(u"/_matrix/push/v1/notify"_s);

    QNetworkRequest checkGateway(gatewayEndpoint);
    auto reply = co_await NetworkAccessManager::instance()->get(checkGateway);

    // We want to check if this UnifiedPush server has a Matrix gateway
    // This is because Matrix does not natively support UnifiedPush
    const auto &replyJson = QJsonDocument::fromJson(reply->readAll()).object();

    if (replyJson["unifiedpush"_L1]["gateway"_L1].toString() == u"matrix"_s) {
        callApi<PostPusherJob>(endpoint,
                               u"http"_s,
                               QGuiApplication::desktopFileName(),
                               QCoreApplication::applicationName(),
                               deviceId(),
                               QString(), // profileTag is intentionally left empty for now, it's optional
                               u"en-US"_s,
                               PostPusherJob::PusherData{gatewayEndpoint},
                               false);

        qCInfo(GENERAL) << "Successfully registered for push notifications.";
        m_pushNotificationsEnabled = true;
    } else {
        qCWarning(GENERAL) << "There's no gateway, not setting up push notifications.";
        m_pushNotificationsEnabled = false;
    }
    Q_EMIT enablePushNotificationsChanged();
#else
    Q_UNUSED(endpoint)
    co_return;
#endif
}

QString NeoChatConnection::accountDataJsonString(const QString &type) const
{
    return QString::fromUtf8(QJsonDocument(accountDataJson(type)).toJson());
}

LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
{
    if (!m_globalUrlPreviewDefault) {
        return nullptr;
    }

    auto previewer = m_linkPreviewers.value(link);
    if (previewer != nullptr) {
        return previewer;
    }

    previewer = new LinkPreviewer(link, this);
    m_linkPreviewers.insert(link, previewer);
    return previewer;
}

KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path)
{
    KeyImport keyImport;
    auto result = keyImport.exportKeys(passphrase, this);
    if (!result.has_value()) {
        return result.error();
    }
    QUrl url(path);
    QFile file(url.toLocalFile());
    auto ok = file.open(QFile::WriteOnly);
    if (!ok) {
        qWarning() << "Failed to open" << file.fileName() << file.errorString();
        return KeyImport::OtherError;
    }
    file.write(result.value());
    file.close();
    return KeyImport::Success;
}

bool NeoChatConnection::canEraseData() const
{
    return m_canEraseData;
}

bool NeoChatConnection::pushNotificationsAvailable() const
{
#ifdef HAVE_KUNIFIEDPUSH
    return true;
#else
    return false;
#endif
}

bool NeoChatConnection::enablePushNotifications() const
{
    return m_pushNotificationsEnabled;
}

bool NeoChatConnection::isVerifiedSession() const
{
    // HACK: Wait to hide the "Verify This Device" warning until sync is done, which can happen quite late in the process.
    if (!m_syncDone) {
        return true;
    }

    return isUserVerified(userId());
}

void NeoChatConnection::unlockSSSS(const QString &secret)
{
    auto handler = new SSSSHandler();
    handler->setConnection(this);
    connect(handler, &SSSSHandler::error, this, [secret, handler, this]() {
        disconnect(handler, &SSSSHandler::error, this, nullptr);
        if (!secret.isEmpty()) {
            connect(handler, &SSSSHandler::error, this, [handler, this]() {
                Q_EMIT keyBackupError();
                handler->deleteLater();
            });
            handler->unlockSSSSWithPassphrase(secret);
        } else {
            Q_EMIT keyBackupError();
        }
    });
    connect(handler, &SSSSHandler::keyBackupUnlocked, this, [handler, this]() {
        Q_EMIT keyBackupUnlocked();
        connect(handler, &SSSSHandler::finished, handler, &SSSSHandler::deleteLater);
    });
    if (secret.isEmpty()) {
        handler->unlockSSSSFromCrossSigning();
    } else {
        handler->unlockSSSSFromSecurityKey(secret);
    }
}

void NeoChatConnection::reportUser(const QString &userId, const QString &reason)
{
    callApi<NeochatReportUserJob>(userId, reason);
}

bool NeoChatConnection::supportsMatrixSpecVersion(const QString &version)
{
    return supportedMatrixSpecVersions().contains(version);
}

QString NeoChatConnection::noteForUser(const QString &userId)
{
    const auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
    return object[userId].toString();
}

void NeoChatConnection::setNoteForUser(const QString &userId, const QString &note)
{
    auto object = accountDataJson(QStringLiteral("org.kde.neochat.user_note"));
    if (note.isEmpty()) {
        object.remove(userId);
    } else {
        object[userId] = note;
    }
    setAccountData(QStringLiteral("org.kde.neochat.user_note"), object);
}

bool NeoChatConnection::blockAllInvites() const
{
    return accountDataJson("m.invite_permission_config"_L1).value("default_action"_L1).toString() == "block"_L1;
}

void NeoChatConnection::setBlockAllInvites(bool block)
{
    auto object = accountDataJson(QStringLiteral("m.invite_permission_config"));
    if (block) {
        object["default_action"_L1] = "block"_L1;
    } else {
        object.remove("default_action"_L1);
    }
    setAccountData(QStringLiteral("m.invite_permission_config"), object);
}

QString NeoChatConnection::accountManagementUri() const
{
    return m_accountManagementUri;
}

void NeoChatConnection::setAccountManagementUri(const QString &uri)
{
    m_accountManagementUri = uri;
    Q_EMIT accountManagementUriChanged();
}

bool NeoChatConnection::profileFieldWritable(const QString &key) const
{
    // No profile fields are writeable in this case
    if (!m_profileFieldsWriteable) {
        return false;
    }

    // If it isn't in the allow list...
    if (!m_allowedFields.isEmpty() && !m_allowedFields.contains(key)) {
        return false;
    }

    // If it isn't in the disallow list...
    if (!m_disallowedFields.isEmpty() && m_disallowedFields.contains(key)) {
        return false;
    }

    return true;
}

QString NeoChatConnection::profileField(const QString &key)
{
    return m_profileFields[key];
}

void NeoChatConnection::setProfileField(const QString &key, const QString &value)
{
    // This should never happen!
    Q_ASSERT(profileFieldWritable(key));

    callApi<NeoChatSetProfileFieldJob>(BackgroundRequest, userId(), key, value);
    m_profileFields[key] = value;
}

bool NeoChatConnection::supportsProfileFields() const
{
    return m_supportsProfileFields;
}

#include "moc_neochatconnection.cpp"
