/*
 * SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez <aleixpol@kde.org>
 * SPDX-FileCopyrightText: 2025 David Redondo <kde@david-redondo.de>
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "globalshortcuts.h"
#include "quickdialog.h"
#include "request.h"
#include "session.h"
#include "utils.h"
#include "xdgshortcut.h"

#include <KGlobalAccel>
#include <KIO/OpenUrlJob>
#include <KLocalizedString>
#include <KStandardShortcut>

#include "kglobalaccel_component_interface.h"
#include "kglobalaccel_interface.h"

#include <QAbstractListModel>
#include <QAction>
#include <QDBusMetaType>
#include <QDataStream>
#include <QLoggingCategory>
#include <QUrl>
#include <QWindow>

#include <QQmlEngine>

#include <ranges>

using namespace Qt::StringLiterals;

Q_LOGGING_CATEGORY(XdgDesktopPortalKdeGlobalShortcuts, "xdp-kde-GlobalShortcuts")

using namespace Qt::StringLiterals;

class ShortcutsModel : public QAbstractListModel
{
    Q_OBJECT
    Q_PROPERTY(bool hasGlobalConflict READ hasGlobalConflict NOTIFY hasGlobalConflictChanged)
    Q_PROPERTY(bool hasInternalConflict READ hasInternalConflict NOTIFY hasInternalConflictChanged)
public:
    enum Roles {
        KeySequence = Qt::UserRole,
        GlobalConflict,
        StandardConflict,
        InternalConflict,
        ConflictText,
    };
    explicit ShortcutsModel(const QList<ShortcutInfo> &shortcuts, QObject *parent = nullptr)
        : QAbstractListModel(parent)
        , m_shortcuts(shortcuts)
    {
        m_standardConflicts.reserve(shortcuts.size());
        m_globalConflicts.reserve(shortcuts.size());
        for (const auto &shortcut : m_shortcuts) {
            m_standardConflicts.push_back(KStandardShortcut::find(shortcut.keySequence));
            m_globalConflicts.push_back(!KGlobalAccel::isGlobalShortcutAvailable(shortcut.keySequence));
            m_internalConflictDetector.insert(shortcut.keySequence, m_internalConflictDetector.size());
        }
    }
    const QList<ShortcutInfo> &shortcuts()
    {
        return m_shortcuts;
    }
    QHash<int, QByteArray> roleNames() const override
    {
        return {{Qt::DisplayRole, "display"},
                {Roles::KeySequence, "keySequence"},
                {Roles::GlobalConflict, "globalConflict"},
                {Roles::StandardConflict, "standardConflict"},
                {Roles::InternalConflict, "internalConflict"},
                {Roles::ConflictText, "conflictText"}};
    }
    int rowCount([[maybe_unused]] const QModelIndex &parent) const override
    {
        return m_shortcuts.size();
    }
    QVariant data(const QModelIndex &index, int role) const override
    {
        if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) {
            return QVariant();
        }
        auto &shortcut = m_shortcuts.at(index.row());
        switch (role) {
        case Qt::DisplayRole:
            return shortcut.description;
        case Roles::KeySequence:
            return shortcut.keySequence;
        case Roles::GlobalConflict:
            return m_globalConflicts.at(index.row());
        case Roles::StandardConflict:
            return m_standardConflicts.at(index.row()) != KStandardShortcut::AccelNone;
        case Roles::InternalConflict:
            return !shortcut.keySequence.isEmpty() && m_internalConflictDetector.count(shortcut.keySequence) > 1;
        case Roles::ConflictText:
            if (m_globalConflicts.at(index.row())) {
                auto takenBy = KGlobalAccel::globalShortcutsByKey(shortcut.keySequence);
                if (takenBy.empty()) {
                    return QString();
                }
                return xi18nc("@info:tooltip",
                              "The shortcut is already assigned to the action <interface>%1</interface> of <application>%2</application>. "
                              "Re-enter it here to override that action.",
                              takenBy[0].friendlyName(),
                              takenBy[0].componentFriendlyName());
            }
            if (m_standardConflicts.at(index.row()) != KStandardShortcut::AccelNone) {
                return xi18nc("@info:tooltip",
                              "The shortcut is already assigned to the <interface>%1</interface> standard action.",
                              KStandardShortcut::name(m_standardConflicts.at(index.row())));
            }
            if (shortcut.keySequence == QKeySequence()) {
                break;
            }
            if (auto conflicts = m_internalConflictDetector.values(shortcut.keySequence); conflicts.size() != 1) {
                QStringList descriptions;
                std::ranges::sort(conflicts); // Want same order as the main view
                for (const auto conflict : conflicts) {
                    // cppcheck-suppress useStlAlgorithm
                    descriptions.push_back(m_shortcuts.at(conflict).description);
                }
                return xi18nc("@info:tooltip %1 is a list of actions",
                              "The shortcut is assigned to multiple actions (<interface>%1</interface>)",
                              QLocale().createSeparatedList(descriptions));
            }
            break;
        }
        return QVariant();
    }
    bool setData(const QModelIndex &index, const QVariant &value, int role) override
    {
        if (checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) {
            if (role == Roles::KeySequence) {
                m_internalConflictDetector.remove(m_shortcuts[index.row()].keySequence, index.row());
                const auto sequence = value.value<QKeySequence>();
                m_shortcuts[index.row()].keySequence = sequence;
                m_globalConflicts[index.row()] = !KGlobalAccel::isGlobalShortcutAvailable(sequence);
                m_standardConflicts[index.row()] = KStandardShortcut::find(sequence);
                m_internalConflictDetector.insert(sequence, index.row());
                Q_EMIT dataChanged(index, index, {Roles::KeySequence, Roles::GlobalConflict, Roles::StandardConflict, Roles::ConflictText});
                Q_EMIT dataChanged(this->index(0, 0), this->index(0, m_shortcuts.size() - 1), {Roles::InternalConflict});
                Q_EMIT hasGlobalConflictChanged();
                Q_EMIT hasInternalConflictChanged();
                return true;
            }
        }
        return false;
    }
    bool hasGlobalConflict()
    {
        return std::ranges::any_of(m_globalConflicts, std::identity());
    }
    bool hasInternalConflict()
    {
        const auto sequences = m_internalConflictDetector.uniqueKeys();
        if (sequences.contains(QKeySequence())) {
            // Allow multiple shortcuts having no sequence
            return (sequences.size() - 1 + m_internalConflictDetector.count(QKeySequence())) < m_shortcuts.size();
        }
        return sequences.size() < m_shortcuts.size();
    }
Q_SIGNALS:
    void hasGlobalConflictChanged();
    void hasInternalConflictChanged();

private:
    QList<ShortcutInfo> m_shortcuts;
    QList<bool> m_globalConflicts;
    QList<KStandardShortcut::StandardShortcut> m_standardConflicts;
    QMultiHash<QKeySequence, int> m_internalConflictDetector;
};

GlobalShortcutsPortal::GlobalShortcutsPortal(QObject *parent)
    : QDBusAbstractAdaptor(parent)
{
    qDBusRegisterMetaType<Shortcut>();
    qDBusRegisterMetaType<Shortcuts>();

    Q_ASSERT(QLatin1String(QDBusMetaType::typeToSignature(QMetaType(qMetaTypeId<Shortcuts>()))) == QLatin1String("a(sa{sv})"));
}

GlobalShortcutsPortal::~GlobalShortcutsPortal() = default;

uint GlobalShortcutsPortal::version() const
{
    return 2;
}

uint GlobalShortcutsPortal::CreateSession(const QDBusObjectPath &handle,
                                          const QDBusObjectPath &session_handle,
                                          const QString &app_id,
                                          const QVariantMap &options,
                                          QVariantMap &results)
{
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "CreateSession called with parameters:";
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    handle: " << handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    session_handle: " << session_handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    app_id: " << app_id;
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    options: " << options;

    auto session = new GlobalShortcutsSession(this, app_id, session_handle.path());
    if (!session->isValid()) {
        return PortalResponse::OtherError;
    }

    session->loadActions();

    connect(session, &GlobalShortcutsSession::shortcutsChanged, this, [this, session, session_handle] {
        Q_EMIT ShortcutsChanged(session_handle, session->shortcutDescriptions());
    });

    connect(session, &GlobalShortcutsSession::shortcutActivated, this, [this, session](const QString &shortcutName, qlonglong timestamp) {
        Q_EMIT Activated(QDBusObjectPath(session->handle()), shortcutName, timestamp);
    });
    connect(session, &GlobalShortcutsSession::shortcutDeactivated, this, [this, session](const QString &shortcutName, qlonglong timestamp) {
        Q_EMIT Deactivated(QDBusObjectPath(session->handle()), shortcutName, timestamp);
    });

    Q_UNUSED(results);
    return PortalResponse::Success;
}

uint GlobalShortcutsPortal::ListShortcuts(const QDBusObjectPath &handle, const QDBusObjectPath &session_handle, QVariantMap &results)
{
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "ListShortcuts called with parameters:";
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    handle: " << handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    session_handle: " << session_handle.path();

    auto session = Session::getSession<GlobalShortcutsSession>(session_handle.path());
    if (!session) {
        return PortalResponse::OtherError;
    }
    results = {
        {u"shortcuts"_s, session->shortcutDescriptionsVariant()},
    };
    return PortalResponse::Success;
}

void GlobalShortcutsPortal::BindShortcuts(const QDBusObjectPath &handle,
                                          const QDBusObjectPath &session_handle,
                                          const Shortcuts &shortcuts,
                                          const QString &parent_window,
                                          const QVariantMap &options,
                                          const QDBusMessage &message,
                                          uint &replyResponse,
                                          QVariantMap &replyResults)
{
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "BindShortcuts called with parameters:";
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    handle: " << handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    session_handle: " << session_handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    shortcuts: " << shortcuts;
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    parent_window: " << parent_window;
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    options: " << options;

    auto session = Session::getSession<GlobalShortcutsSession>(session_handle.path());
    if (!session) {
        replyResponse = PortalResponse::OtherError;
        return;
    }

    auto previousShortcuts = session->shortcutDescriptions();
    auto currentShortcuts = shortcuts;
    std::ranges::sort(currentShortcuts, {}, &Shortcut::first);
    std::ranges::sort(previousShortcuts, {}, &Shortcut::first);

    // Some applications (like chrome) requests contain the same shortcut id multiple times
    // This makes no sense remove the duplicates to not prompt for those as if they were new
    const auto duplicates = std::ranges::unique(currentShortcuts, {}, &Shortcut::first);
    currentShortcuts.erase(duplicates.begin(), duplicates.end());

    Shortcuts newShortcuts;
    std::ranges::set_difference(currentShortcuts, previousShortcuts, std::back_inserter(newShortcuts), {}, &Shortcut::first, &Shortcut::first);
    Shortcuts returningShortcuts;
    std::ranges::set_intersection(currentShortcuts, previousShortcuts, std::back_inserter(returningShortcuts), {}, &Shortcut::first, &Shortcut::first);

    auto toShortcutInfo = [](const Shortcut &shortcut) {
        const QKeySequence preferredKeySequence = XdgShortcut::parse(shortcut.second.value(u"preferred_trigger"_s).toString()).value_or(QKeySequence());
        return ShortcutInfo{.id = shortcut.first,
                            .description = shortcut.second.value(u"description"_s).toString(),
                            .keySequence = preferredKeySequence,
                            .preferredKeySequence = preferredKeySequence};
    };

    QList<ShortcutInfo> newShortcutInfos;
    newShortcutInfos.reserve(newShortcuts.size());
    std::ranges::transform(newShortcuts, std::back_inserter(newShortcutInfos), toShortcutInfo);

    QList<ShortcutInfo> returningShortcutInfos;
    returningShortcutInfos.reserve(returningShortcuts.size());
    std::ranges::transform(returningShortcuts, std::back_inserter(returningShortcutInfos), toShortcutInfo);

    if (newShortcutInfos.empty()) {
        replyResponse = PortalResponse::Success;
        session->setActions(returningShortcutInfos);
        replyResults = {{u"shortcuts"_s, session->shortcutDescriptionsVariant()}};
        return;
    }

    auto dialog = new QuickDialog();
    Request::makeClosableDialogRequestWithSession(handle, dialog, session);
    auto model = new ShortcutsModel(newShortcutInfos, dialog);
    dialog->create(u"GlobalShortcutsDialog"_s,
                   {{u"app"_s, Utils::applicationName(session->appId())},
                    {u"returningShortcuts"_s, QVariant::fromValue(returningShortcutInfos)},
                    {u"newShortcuts"_s, QVariant::fromValue(model)},
                    {u"component"_s, session->componentName()}});
    Utils::setParentWindow(dialog->windowHandle(), parent_window);
    delayReply(message, dialog, session, [model, returningShortcutInfos, session](DialogResult result) -> QVariantList {
        // The dialog asks the user if they want to add the new bindings. If denied we still allow
        // binding the returning shortcuts if there are any.
        // We also register the denied shortcuts (with no key combinations) so we don't prompt on next
        // application start. The user can assign bindings later in systemsettings if they change their mind.
        // Still return a cancelled result if there are no active shortcuts.
        auto newShortcutInfos = model->shortcuts();
        if (result == DialogResult::Rejected) {
            for (auto &newShortcut : newShortcutInfos) {
                newShortcut.keySequence = {};
            }
        }
        QList<ShortcutInfo> currentShortcutInfos;
        currentShortcutInfos.reserve(newShortcutInfos.size() + returningShortcutInfos.size());
        std::ranges::set_union(newShortcutInfos, returningShortcutInfos, std::back_inserter(currentShortcutInfos), {}, &ShortcutInfo::id, &ShortcutInfo::id);
        session->setActions(currentShortcutInfos);
        if (result == DialogResult::Rejected && returningShortcutInfos.empty()) {
            return QVariantList{PortalResponse::Cancelled, QVariantMap{}};
        }
        return {PortalResponse::Success, QVariantMap{{u"shortcuts"_s, session->shortcutDescriptionsVariant()}}};
    });
}

void GlobalShortcutsPortal::ConfigureShortcuts(const QDBusObjectPath &session_handle, const QString &parent_window, const QVariantMap &options)
{
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "ConfigureShortcuts called with parameters:";
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    parent_window: " << parent_window;
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    session_handle: " << session_handle.path();
    qCDebug(XdgDesktopPortalKdeGlobalShortcuts) << "    options: " << options;

    auto session = Session::getSession<GlobalShortcutsSession>(session_handle.path());
    if (!session) {
        return;
    }
    auto job = new KIO::OpenUrlJob(QUrl(u"systemsettings://kcm_keys/"_s + session->componentName()));
    job->setStartupId(options.value(u"activation_token"_s).value<QByteArray>());
    job->start();
}


GlobalShortcutsSession::GlobalShortcutsSession(QObject *parent, const QString &appId, const QString &path)
    : Session(parent, appId, path)
    , m_token(path.mid(path.lastIndexOf(u'/') + 1))
    , m_globalAccelInterface(
          new KGlobalAccelInterface(QStringLiteral("org.kde.kglobalaccel"), QStringLiteral("/kglobalaccel"), QDBusConnection::sessionBus(), this))
    , m_component(new KGlobalAccelComponentInterface(m_globalAccelInterface->service(),
                                                     m_globalAccelInterface->getComponent(componentName()).value().path(),
                                                     m_globalAccelInterface->connection(),
                                                     this))
{
    qDBusRegisterMetaType<KGlobalShortcutInfo>();
    qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
    qDBusRegisterMetaType<QKeySequence>();
    qDBusRegisterMetaType<QList<QKeySequence>>();

    connect(m_globalAccelInterface,
            &KGlobalAccelInterface::yourShortcutsChanged,
            this,
            [this](const QStringList &actionId, const QList<QKeySequence> &newKeys) {
                Q_UNUSED(newKeys);
                if (actionId[KGlobalAccel::ComponentUnique] == componentName()) {
                    if (auto it = m_shortcuts.find(actionId[KGlobalAccel::ActionUnique]); it != m_shortcuts.end()) {
                        it->second->setShortcuts(newKeys);
                        Q_EMIT shortcutsChanged();
                    }
                }
            });
    connect(m_component,
            &KGlobalAccelComponentInterface::globalShortcutPressed,
            this,
            [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
                if (componentUnique != componentName()) {
                    return;
                }

                Q_EMIT shortcutActivated(actionUnique, timestamp);
            });
    connect(m_component,
            &KGlobalAccelComponentInterface::globalShortcutReleased,
            this,
            [this](const QString &componentUnique, const QString &actionUnique, qlonglong timestamp) {
                if (componentUnique != componentName()) {
                    return;
                }

                Q_EMIT shortcutDeactivated(actionUnique, timestamp);
            });
}

GlobalShortcutsSession::~GlobalShortcutsSession() = default;

void GlobalShortcutsSession::setActions(const QList<ShortcutInfo> &shortcuts)
{
    const QList<KGlobalShortcutInfo> shortcutInfos = m_component->allShortcutInfos();
    QHash<QString, KGlobalShortcutInfo> shortcutInfosByName;
    shortcutInfosByName.reserve(shortcutInfos.size());
    for (const auto &shortcutInfo : shortcutInfos) {
        shortcutInfosByName[shortcutInfo.uniqueName()] = shortcutInfo;
    }

    for (const auto &shortcut : shortcuts) {
        if (shortcut.id.isEmpty()) {
            qCWarning(XdgDesktopPortalKdeGlobalShortcuts) << "Shortcut without id" << "for" << componentName();
            continue;
        }
        if (shortcut.description.isEmpty()) {
            qCWarning(XdgDesktopPortalKdeGlobalShortcuts) << "Shortcut without name or description" << shortcut.id << "for" << componentName();
            continue;
        }

        std::unique_ptr<QAction> &action = m_shortcuts[shortcut.id];
        if (!action) {
            action = std::make_unique<QAction>();
        }
        const QString app = Utils::applicationName(m_appId);
        action->setProperty("componentName", componentName());
        action->setProperty("componentDisplayName", app.isEmpty() ? componentName() : app);
        action->setObjectName(shortcut.id);
        action->setText(shortcut.description);
        const auto itShortcut = shortcutInfosByName.constFind(shortcut.id);
        if (itShortcut != shortcutInfosByName.constEnd()) {
            action->setShortcuts(itShortcut->keys());
        } else {
            action->setShortcut(shortcut.keySequence);
            if (!shortcut.preferredKeySequence.isEmpty()) {
                KGlobalAccel::self()->setDefaultShortcut(action.get(), {shortcut.preferredKeySequence});
            }
        }
        // If it's a new shortcut assign keySequence otherwise the old ones will be loaded.
        KGlobalAccel::self()->setShortcut(action.get(), {shortcut.keySequence}, KGlobalAccel::Autoloading);

        shortcutInfosByName.remove(shortcut.id);
    }

           // We can forget the shortcuts that aren't around anymore
    while (!shortcutInfosByName.isEmpty()) {
        const QString shortcutName = shortcutInfosByName.begin().key();
        auto it = m_shortcuts.find(shortcutName);
        if (it != m_shortcuts.end()) {
            KGlobalAccel::self()->removeAllShortcuts(it->second.get());
            m_shortcuts.erase(it);
        }
        shortcutInfosByName.erase(shortcutInfosByName.begin());
    }

    Q_ASSERT(static_cast<qsizetype>(m_shortcuts.size()) == shortcuts.size());
}

void GlobalShortcutsSession::loadActions()
{
    m_shortcuts.clear();

    const QList<KGlobalShortcutInfo> shortcutInfos = m_component->allShortcutInfos();
    QHash<QString, KGlobalShortcutInfo> shortcutInfosByName;
    shortcutInfosByName.reserve(shortcutInfos.size());
    for (const auto &shortcutInfo : shortcutInfos) {
        shortcutInfosByName[shortcutInfo.uniqueName()] = shortcutInfo;
    }

    for (const auto &[name, info] : shortcutInfosByName.asKeyValueRange()) {
        std::unique_ptr<QAction> &action = m_shortcuts[name];
        if (!action) {
            action = std::make_unique<QAction>();
        }
        action->setProperty("componentName", componentName());
        action->setProperty("componentDisplayName", Utils::applicationName(m_appId));
        action->setObjectName(name);
        action->setText(info.friendlyName());
        action->setShortcuts(info.keys());
        // Explicitly load existing global shortcut setting
        KGlobalAccel::self()->setShortcut(action.get(), action->shortcuts(), KGlobalAccel::Autoloading);
    }
}

QVariant GlobalShortcutsSession::shortcutDescriptionsVariant() const
{
    QDBusArgument retVar;
    retVar << shortcutDescriptions();
    return QVariant::fromValue(retVar);
}

Shortcuts GlobalShortcutsSession::shortcutDescriptions() const
{
    Shortcuts ret;
    for (const auto &[key, action] : m_shortcuts) {
        QStringList triggers;
        triggers.reserve(action->shortcuts().size());
        const auto shortcuts = action->shortcuts();
        for (const auto &shortcut : shortcuts) {
            triggers += shortcut.toString(QKeySequence::NativeText);
        }

        ret.append({key,
                    QVariantMap{
                                {QStringLiteral("description"), action->text()},
                                {QStringLiteral("trigger_description"), triggers.join(i18n(", "))},
                                }});
    }
    Q_ASSERT(ret.size() == static_cast<qsizetype>(m_shortcuts.size()));
    return ret;
}

#include "globalshortcuts.moc"
#include "moc_globalshortcuts.cpp"
