// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "PowerlevelsEditModels.h"

#include <QCoreApplication>
#include <QTimer>

#include <algorithm>
#include <set>
#include <unordered_set>

#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"

PowerlevelsTypeListModel::PowerlevelsTypeListModel(const std::string &rid,
                                                   const mtx::events::state::PowerLevels &pl,
                                                   QObject *parent)
  : QAbstractListModel(parent)
  , room_id(rid)
  , powerLevels_(pl)
{
    std::set<mtx::events::state::power_level_t> seen_levels;
    for (const auto &[type, level] : powerLevels_.events) {
        if (!seen_levels.count(level)) {
            types.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
        types.push_back(Entry{type, level});
    }

    for (const auto &[user, level] : powerLevels_.users) {
        (void)user;
        if (!seen_levels.count(level)) {
            types.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
    }

    for (const auto &level : {
           powerLevels_.events_default,
           powerLevels_.state_default,
           powerLevels_.users_default,
           powerLevels_.ban,
           powerLevels_.kick,
           powerLevels_.invite,
           powerLevels_.redact,
         }) {
        if (!seen_levels.count(level)) {
            types.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
    }

    types.push_back(Entry{"zdefault_states", powerLevels_.state_default});
    types.push_back(Entry{"zdefault_events", powerLevels_.events_default});
    types.push_back(Entry{"ban", powerLevels_.ban});
    types.push_back(Entry{"kick", powerLevels_.kick});
    types.push_back(Entry{"invite", powerLevels_.invite});
    types.push_back(Entry{"redact", powerLevels_.redact});

    std::sort(types.begin(), types.end(), [](const Entry &a, const Entry &b) {
        if (a.pl != b.pl) // sort by PL
            return a.pl > b.pl;
        else if (a.type.empty() != b.type.empty()) // empty types are headers
            return a.type.empty() > b.type.empty();
        else {
            bool a_contains_dot = a.type.find('.') != std::string::npos;
            bool b_contains_dot = b.type.find('.') != std::string::npos;
            if (a_contains_dot != b_contains_dot) // sort stuff like "invite" or "default" last
                return a_contains_dot > b_contains_dot;
            else // rest is sorted alphabetical
                return a.type < b.type;
        }
    });
}

std::map<std::string, mtx::events::state::power_level_t, std::less<>>
PowerlevelsTypeListModel::toEvents() const
{
    std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
    for (const auto &[key, pl] : std::as_const(types))
        if (key.find('.') != std::string::npos)
            m[key] = pl;
    return m;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::kick() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "kick")
            return pl;
    return powerLevels_.users_default;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::invite() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "invite")
            return pl;
    return powerLevels_.users_default;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::ban() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "ban")
            return pl;
    return powerLevels_.users_default;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::redact() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "redact")
            return pl;
    return powerLevels_.users_default;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::eventsDefault() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "zdefault_events")
            return pl;
    return powerLevels_.users_default;
}
mtx::events::state::power_level_t
PowerlevelsTypeListModel::stateDefault() const
{
    for (const auto &[key, pl] : std::as_const(types))
        if (key == "zdefault_states")
            return pl;
    return powerLevels_.users_default;
}

QHash<int, QByteArray>
PowerlevelsTypeListModel::roleNames() const
{
    return {
      {DisplayName, "displayName"},
      {Powerlevel, "powerlevel"},
      {IsType, "isType"},
      {Moveable, "moveable"},
      {Removeable, "removeable"},
    };
}

QVariant
PowerlevelsTypeListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= types.size())
        return {};

    const auto &type = types.at(index.row());

    switch (static_cast<Roles>(role)) {
    case DisplayName:
        if (type.type == "zdefault_events")
            return tr("Other events");
        else if (type.type == "zdefault_states")
            return tr("Other state events");
        else if (type.type == "kick")
            return tr("Remove other users");
        else if (type.type == "ban")
            return tr("Ban other users");
        else if (type.type == "invite")
            return tr("Invite other users");
        else if (type.type == "redact")
            return tr("Redact events sent by others");
        else if (type.type == "m.reaction")
            return tr("Reactions");
        else if (type.type == "m.room.aliases")
            return tr("Deprecated aliases events");
        else if (type.type == "m.room.avatar")
            return tr("Change the room avatar");
        else if (type.type == "m.room.canonical_alias")
            return tr("Change the room addresses");
        else if (type.type == "m.room.encrypted")
            return tr("Send encrypted messages");
        else if (type.type == "m.room.encryption")
            return tr("Enable encryption");
        else if (type.type == "m.room.guest_access")
            return tr("Change guest access");
        else if (type.type == "m.room.history_visibility")
            return tr("Change history visibility");
        else if (type.type == "m.room.join_rules")
            return tr("Change who can join");
        else if (type.type == "m.room.message")
            return tr("Send messages");
        else if (type.type == "m.room.name")
            return tr("Change the room name");
        else if (type.type == "m.room.power_levels")
            return tr("Change the room permissions");
        else if (type.type == "m.room.topic")
            return tr("Change the rooms topic");
        else if (type.type == "m.widget")
            return tr("Change the widgets");
        else if (type.type == "im.vector.modular.widgets")
            return tr("Change the widgets (experimental)");
        else if (type.type == "m.room.redaction")
            return tr("Redact own events");
        else if (type.type == "m.room.pinned_events")
            return tr("Change the pinned events");
        else if (type.type == "m.room.tombstone")
            return tr("Upgrade the room");
        else if (type.type == "m.sticker")
            return tr("Send stickers");

        else if (type.type == "m.policy.rule.user")
            return tr("Ban users using policy rules");
        else if (type.type == "m.policy.rule.room")
            return tr("Ban rooms using policy rules");
        else if (type.type == "m.policy.rule.server")
            return tr("Ban servers using policy rules");

        else if (type.type == "m.space.child")
            return tr("Edit child communities and rooms");
        else if (type.type == "m.space.parent")
            return tr("Change parent communities");

        else if (type.type == "m.call.invite")
            return tr("Start a call");
        else if (type.type == "m.call.candidates")
            return tr("Negotiate a call");
        else if (type.type == "m.call.answer")
            return tr("Answer a call");
        else if (type.type == "m.call.hangup")
            return tr("Hang up a call");
        else if (type.type == "m.call.reject")
            return tr("Reject a call");
        else if (type.type == "im.ponies.room_emotes")
            return tr("Change the room emotes");
        return QString::fromStdString(type.type);
    case Powerlevel:
        return static_cast<qlonglong>(type.pl);
    case IsType:
        return !type.type.empty();
    case Moveable:
        return !type.type.empty();
    case Removeable:
        return !type.type.empty() && type.type.find('.') != std::string::npos;
    }

    return {};
}

bool
PowerlevelsTypeListModel::remove(int row)
{
    if (row < 0 || row >= types.size() || types.at(row).type.empty())
        return false;

    beginRemoveRows(QModelIndex(), row, row);
    types.remove(row);
    endRemoveRows();

    return true;
}

void
PowerlevelsTypeListModel::add(int row, QString type)
{
    if (row < 0 || row > types.size())
        return;

    const auto typeStr = type.toStdString();
    for (int i = 0; i < types.size(); i++) {
        if (types[i].type == typeStr) {
            if (i > row)
                move(i, row + 1);
            else
                move(i, row);
            return;
        }
    }

    beginInsertRows(QModelIndex(), row + 1, row + 1);
    types.insert(row + 1, Entry{type.toStdString(), types.at(row).pl});
    endInsertRows();
}

void
PowerlevelsTypeListModel::addRole(int64_t role)
{
    for (int i = 0; i < types.size(); i++) {
        if (types[i].pl < role) {
            beginInsertRows(QModelIndex(), i, i);
            types.insert(i, Entry{"", role});
            endInsertRows();
            return;
        }
    }

    beginInsertRows(QModelIndex(), types.size(), types.size());
    types.push_back(Entry{"", role});
    endInsertRows();
}

bool
PowerlevelsTypeListModel::move(int from, int to)
{
    if (from == to)
        return false;
    if (from < to)
        to += 1;

    beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
    auto ret = moveRow(QModelIndex(), from, QModelIndex(), to);
    endMoveRows();
    return ret;
}

bool
PowerlevelsTypeListModel::moveRows(const QModelIndex &,
                                   int sourceRow,
                                   int count,
                                   const QModelIndex &,
                                   int destinationChild)
{
    if (sourceRow == destinationChild)
        return true;

    if (count != 1)
        return false;

    if (sourceRow < 0 || sourceRow >= types.size())
        return false;
    if (destinationChild < 0 || destinationChild > types.size())
        return false;

    if (types.at(sourceRow).type.empty())
        return false;

    auto pl         = types.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
    auto sourceItem = types.takeAt(sourceRow);
    sourceItem.pl   = pl;

    auto movedType = sourceItem.type;

    if (destinationChild < sourceRow)
        types.insert(destinationChild, std::move(sourceItem));
    else
        types.insert(destinationChild - 1, std::move(sourceItem));

    if (movedType == "m.room.power_levels")
        emit adminLevelChanged();
    else if (movedType == "redact")
        emit moderatorLevelChanged();

    return true;
}

PowerlevelsUserListModel::PowerlevelsUserListModel(const std::string &rid,
                                                   const mtx::events::state::PowerLevels &pl,
                                                   QObject *parent)
  : QAbstractListModel(parent)
  , room_id(rid)
  , powerLevels_(pl)
{
    std::set<mtx::events::state::power_level_t> seen_levels;
    for (const auto &[user, level] : powerLevels_.users) {
        if (!seen_levels.count(level)) {
            users.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
        users.push_back(Entry{user, level});
    }

    for (const auto &[type, level] : powerLevels_.events) {
        (void)type;
        if (!seen_levels.count(level)) {
            users.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
    }

    for (const auto &level : {
           powerLevels_.events_default,
           powerLevels_.state_default,
           powerLevels_.users_default,
           powerLevels_.ban,
           powerLevels_.kick,
           powerLevels_.invite,
           powerLevels_.redact,
         }) {
        if (!seen_levels.count(level)) {
            users.push_back(Entry{"", level});
            seen_levels.insert(level);
        }
    }

    users.push_back(Entry{"default", powerLevels_.users_default});

    std::sort(users.begin(), users.end(), [](const Entry &a, const Entry &b) {
        if (a.pl != b.pl)
            return a.pl > b.pl;
        else
            return a.mxid < b.mxid;
    });
}

std::map<std::string, mtx::events::state::power_level_t, std::less<>>
PowerlevelsUserListModel::toUsers() const
{
    std::map<std::string, mtx::events::state::power_level_t, std::less<>> m;
    for (const auto &[key, pl] : std::as_const(users))
        if (key.size() > 0 && key.at(0) == '@')
            m[key] = pl;
    return m;
}
mtx::events::state::power_level_t
PowerlevelsUserListModel::usersDefault() const
{
    for (const auto &[key, pl] : std::as_const(users))
        if (key == "default")
            return pl;
    return powerLevels_.users_default;
}

QHash<int, QByteArray>
PowerlevelsUserListModel::roleNames() const
{
    return {
      {Mxid, "mxid"},
      {DisplayName, "displayName"},
      {AvatarUrl, "avatarUrl"},
      {Powerlevel, "powerlevel"},
      {IsUser, "isUser"},
      {Moveable, "moveable"},
      {Removeable, "removeable"},
    };
}

QVariant
PowerlevelsUserListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= users.size())
        return {};

    const auto &user = users.at(index.row());

    switch (static_cast<Roles>(role)) {
    case Mxid:
        if ("default" == user.mxid)
            return QStringLiteral("*");
        return QString::fromStdString(user.mxid);
    case DisplayName:
        if (user.mxid == "default")
            return tr("Other users");
        return QString::fromStdString(cache::displayName(room_id, user.mxid));
    case AvatarUrl:
        return cache::avatarUrl(QString::fromStdString(room_id), QString::fromStdString(user.mxid));
    case Powerlevel:
        return static_cast<qlonglong>(user.pl);
    case IsUser:
        return !user.mxid.empty();
    case Moveable:
        return !user.mxid.empty();
    case Removeable:
        return !user.mxid.empty() && user.mxid.find('.') != std::string::npos;
    }

    return {};
}

bool
PowerlevelsUserListModel::remove(int row)
{
    if (row < 0 || row >= users.size() || users.at(row).mxid.empty())
        return false;

    beginRemoveRows(QModelIndex(), row, row);
    users.remove(row);
    endRemoveRows();

    return true;
}

void
PowerlevelsUserListModel::add(int row, QString user)
{
    if (row < 0 || row > users.size())
        return;

    const auto userStr = user.toStdString();
    for (int i = 0; i < users.size(); i++) {
        if (users[i].mxid == userStr) {
            if (i > row)
                move(i, row + 1);
            else
                move(i, row);
            return;
        }
    }

    beginInsertRows(QModelIndex(), row + 1, row + 1);
    users.insert(row + 1, Entry{user.toStdString(), users.at(row).pl});
    endInsertRows();
}

void
PowerlevelsUserListModel::addRole(int64_t role)
{
    for (int i = 0; i < users.size(); i++) {
        if (users[i].pl < role) {
            beginInsertRows(QModelIndex(), i, i);
            users.insert(i, Entry{"", role});
            endInsertRows();
            return;
        }
    }

    beginInsertRows(QModelIndex(), users.size(), users.size());
    users.push_back(Entry{"", role});
    endInsertRows();
}

bool
PowerlevelsUserListModel::move(int from, int to)
{
    if (from == to)
        return false;
    if (from < to)
        to += 1;

    beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
    auto ret = moveRow(QModelIndex(), from, QModelIndex(), to);
    endMoveRows();
    return ret;
}

bool
PowerlevelsUserListModel::moveRows(const QModelIndex &,
                                   int sourceRow,
                                   int count,
                                   const QModelIndex &,
                                   int destinationChild)
{
    if (sourceRow == destinationChild)
        return true;

    if (count != 1)
        return false;

    if (sourceRow < 0 || sourceRow >= users.size())
        return false;
    if (destinationChild < 0 || destinationChild > users.size())
        return false;

    if (users.at(sourceRow).mxid.empty())
        return false;

    auto pl         = users.at(destinationChild > 0 ? destinationChild - 1 : 0).pl;
    auto sourceItem = users.takeAt(sourceRow);
    sourceItem.pl   = pl;

    auto movedType = sourceItem.mxid;

    if (destinationChild < sourceRow)
        users.insert(destinationChild, std::move(sourceItem));
    else
        users.insert(destinationChild - 1, std::move(sourceItem));

    if (movedType == "default")
        emit defaultUserLevelChanged();

    return true;
}

PowerlevelEditingModels::PowerlevelEditingModels(QString room_id, QObject *parent)
  : QObject(parent)
  , powerLevels_(cache::client()
                   ->getStateEvent<mtx::events::state::PowerLevels>(room_id.toStdString())
                   .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{})
                   .content)
  , types_(room_id.toStdString(), powerLevels_, this)
  , users_(room_id.toStdString(), powerLevels_, this)
  , spaces_(room_id.toStdString(), powerLevels_, this)
  , room_id_(room_id.toStdString())
{
    connect(&types_,
            &PowerlevelsTypeListModel::adminLevelChanged,
            this,
            &PowerlevelEditingModels::adminLevelChanged);
    connect(&types_,
            &PowerlevelsTypeListModel::moderatorLevelChanged,
            this,
            &PowerlevelEditingModels::moderatorLevelChanged);
    connect(&users_,
            &PowerlevelsUserListModel::defaultUserLevelChanged,
            this,
            &PowerlevelEditingModels::defaultUserLevelChanged);
}

bool
PowerlevelEditingModels::isSpace() const
{
    return cache::singleRoomInfo(room_id_).is_space;
}

mtx::events::state::PowerLevels
PowerlevelEditingModels::calculateNewPowerlevel() const
{
    auto newPl           = powerLevels_;
    newPl.events         = types_.toEvents();
    newPl.kick           = types_.kick();
    newPl.invite         = types_.invite();
    newPl.ban            = types_.ban();
    newPl.redact         = types_.redact();
    newPl.events_default = types_.eventsDefault();
    newPl.state_default  = types_.stateDefault();
    newPl.users          = users_.toUsers();
    newPl.users_default  = users_.usersDefault();
    return newPl;
}

void
PowerlevelEditingModels::commit()
{
    powerLevels_ = calculateNewPowerlevel();

    http::client()->send_state_event(
      room_id_, powerLevels_, [](const mtx::responses::EventId &, mtx::http::RequestErr e) {
          if (e) {
              nhlog::net()->error("Failed to send PL event: {}", *e);
              ChatPage::instance()->showNotification(
                tr("Failed to update powerlevel: %1")
                  .arg(QString::fromStdString(e->matrix_error.error)));
          }
      });
}

void
PowerlevelEditingModels::updateSpacesModel()
{
    powerLevels_            = calculateNewPowerlevel();
    spaces_.newPowerlevels_ = powerLevels_;
}

void
PowerlevelEditingModels::addRole(int pl)
{
    for (const auto &e : std::as_const(types_.types))
        if (pl == int(e.pl))
            return;

    types_.addRole(pl);
    users_.addRole(pl);
}

static bool
samePl(const mtx::events::state::PowerLevels &a, const mtx::events::state::PowerLevels &b)
{
    return std::tie(a.events,
                    a.users_default,
                    a.users,
                    a.state_default,
                    a.users_default,
                    a.events_default,
                    a.ban,
                    a.kick,
                    a.invite,
                    a.notifications,
                    a.redact) == std::tie(b.events,
                                          b.users_default,
                                          b.users,
                                          b.state_default,
                                          b.users_default,
                                          b.events_default,
                                          b.ban,
                                          b.kick,
                                          b.invite,
                                          b.notifications,
                                          b.redact);
}

PowerlevelsSpacesListModel::PowerlevelsSpacesListModel(const std::string &room_id_,
                                                       const mtx::events::state::PowerLevels &pl,
                                                       QObject *parent)
  : QAbstractListModel(parent)
  , room_id(std::move(room_id_))
  , oldPowerLevels_(std::move(pl))
{
    beginResetModel();

    spaces.push_back(Entry{room_id, oldPowerLevels_, true});

    std::unordered_set<std::string> visited;

    std::function<void(const std::string &)> addChildren;
    addChildren = [this, &addChildren, &visited](const std::string &space) {
        if (visited.count(space))
            return;
        else
            visited.insert(space);

        for (const auto &s : cache::client()->getChildRoomIds(space)) {
            auto parent =
              cache::client()->getStateEvent<mtx::events::state::space::Parent>(s, space);
            if (parent && parent->content.via && !parent->content.via->empty() &&
                parent->content.canonical) {
                auto parentPl = cache::client()->getStateEvent<mtx::events::state::PowerLevels>(s);

                spaces.push_back(Entry{
                  s, parentPl ? parentPl->content : mtx::events::state::PowerLevels{}, false});
                addChildren(s);
            }
        }
    };

    addChildren(room_id);

    endResetModel();

    updateToDefaults();
}

struct PowerLevelApplier
{
    std::vector<std::string> spaces;
    mtx::events::state::PowerLevels pl;

    void next()
    {
        if (spaces.empty())
            return;

        auto room_id_ = spaces.back();
        http::client()->send_state_event(
          room_id_,
          pl,
          [self = *this](const mtx::responses::EventId &, mtx::http::RequestErr e) mutable {
              if (e) {
                  if (e->status_code == 429 && e->matrix_error.retry_after.count() != 0) {
                      ChatPage::instance()->callFunctionOnGuiThread(
                        [self = std::move(self), interval = e->matrix_error.retry_after]() {
                            QTimer::singleShot(interval,
                                               ChatPage::instance(),
                                               [self = std::move(self)]() mutable { self.next(); });
                        });
                      return;
                  }

                  nhlog::net()->error("Failed to send PL event: {}", *e);
                  ChatPage::instance()->showNotification(
                    QCoreApplication::translate("PowerLevels", "Failed to update powerlevel: %1")
                      .arg(QString::fromStdString(e->matrix_error.error)));
              }
              self.spaces.pop_back();
              self.next();
          });
    }
};

void
PowerlevelsSpacesListModel::commit()
{
    std::vector<std::string> spacesToApplyTo;

    for (const auto &s : std::as_const(spaces))
        if (s.apply)
            spacesToApplyTo.push_back(s.roomid);

    PowerLevelApplier context{std::move(spacesToApplyTo), newPowerlevels_};
    context.next();
}

void
PowerlevelsSpacesListModel::updateToDefaults()
{
    for (int i = 1; i < spaces.size(); i++) {
        spaces[i].apply =
          applyToChildren_ && data(index(i), Roles::IsEditable).toBool() &&
          !data(index(i), Roles::IsAlreadyUpToDate).toBool() &&
          (overwriteDiverged_ || !data(index(i), Roles::IsDifferentFromBase).toBool());
    }

    if (spaces.size() > 1)
        emit dataChanged(index(1), index(spaces.size() - 1), {Roles::ApplyPermissions});
}

bool
PowerlevelsSpacesListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != Roles::ApplyPermissions || index.row() < 0 || index.row() >= spaces.size())
        return false;

    spaces[index.row()].apply = value.toBool();
    return true;
}

QVariant
PowerlevelsSpacesListModel::data(QModelIndex const &index, int role) const
{
    auto row = index.row();
    if (row >= spaces.size() && row < 0)
        return {};

    if (role == Roles::DisplayName || role == Roles::AvatarUrl || role == Roles::IsSpace) {
        auto info = cache::singleRoomInfo(spaces.at(row).roomid);
        if (role == Roles::DisplayName)
            return QString::fromStdString(info.name);
        else if (role == Roles::AvatarUrl)
            return QString::fromStdString(info.avatar_url);
        else
            return info.is_space;
    }

    auto entry = spaces.at(row);
    switch (role) {
    case Roles::IsEditable:
        return entry.pl.user_level(http::client()->user_id().to_string()) >=
               entry.pl.state_level(to_string(mtx::events::EventType::RoomPowerLevels));
    case Roles::IsDifferentFromBase:
        return !samePl(entry.pl, oldPowerLevels_);
    case Roles::IsAlreadyUpToDate:
        return samePl(entry.pl, newPowerlevels_);
    case Roles::ApplyPermissions:
        return entry.apply;
    }
    return {};
}
QHash<int, QByteArray>
PowerlevelsSpacesListModel::roleNames() const
{
    return {
      {DisplayName, "displayName"},
      {AvatarUrl, "avatarUrl"},
      {IsEditable, "isEditable"},
      {IsDifferentFromBase, "isDifferentFromBase"},
      {IsAlreadyUpToDate, "isAlreadyUpToDate"},
      {ApplyPermissions, "applyPermissions"},
    };
}

#include "moc_PowerlevelsEditModels.cpp"
