// SPDX-FileCopyrightText: 2017 Contributors to Chatterino <https://chatterino.com>
//
// SPDX-License-Identifier: MIT

#include "providers/twitch/TwitchAccountManager.hpp"

#include "Application.hpp"
#include "common/Args.hpp"
#include "common/Common.hpp"
#include "common/Literals.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchUser.hpp"
#include "singletons/Settings.hpp"
#include "util/QCompareTransparent.hpp"
#include "util/SharedPtrElementLess.hpp"

#include <QStringBuilder>

namespace {

using namespace chatterino;
using namespace literals;

QString missingScopes(const QJsonArray &scopesArray)
{
    std::set<QString, QCompareTransparent> scopes;
    for (auto s : scopesArray)
    {
        scopes.emplace(s.toString());
    }

    QString missingList;
    for (auto scope : AUTH_SCOPES)
    {
        if (!scopes.contains(scope))
        {
            if (!missingList.isEmpty())
            {
                missingList.append(u", ");
            }
            missingList.append(scope);
        }
    }

    return missingList;
}

void checkMissingScopes(const std::shared_ptr<TwitchAccount> &account)
{
    NetworkRequest(u"https://id.twitch.tv/oauth2/validate"_s,
                   NetworkRequestType::Get)
        .header("Authorization", u"OAuth " % account->getOAuthToken())
        .timeout(20000)
        .onSuccess([account](const auto &res) {
            auto *app = tryGetApp();
            if (!app)
            {
                return;
            }

            const auto json = res.parseJson();

            const auto login = json["login"_L1].toString();
            if (!login.isEmpty() &&
                login.compare(account->getUserName(), Qt::CaseInsensitive) != 0)
            {
                account->setUserName(login);
                const std::string basePath =
                    "/accounts/uid" + account->getUserId().toStdString();
                pajlada::Settings::Setting<QString>::set(basePath + "/username",
                                                         login);
                auto &manager = app->getAccounts()->twitch;
                auto currentUsername = manager.getCurrent()->getUserName();
                if (currentUsername.compare(manager.currentUsername.getValue(),
                                            Qt::CaseInsensitive) != 0)
                {
                    manager.currentUsername = currentUsername;
                }
                getSettings()->requestSave();
                app->getAccounts()->twitch.currentUserNameChanged.invoke();
            }

            auto missing = missingScopes(json["scopes"_L1].toArray());
            if (missing.isEmpty())
            {
                return;
            }

            auto msg = MessageBuilder::makeMissingScopesMessage(missing);
            app->getTwitch()->forEachChannel([msg](const auto &chan) {
                chan->addMessage(msg, MessageContext::Original);
            });
        })
        .onError([](const auto &res) {
            qCWarning(chatterinoTwitch)
                << "Failed to check for missing scopes:" << res.formatError();
        })
        .execute();
}

}  // namespace

namespace chatterino {

const std::vector<QStringView> AUTH_SCOPES{
    u"channel:moderate",  // for seeing automod & which moderator banned/unbanned a user (felanbird unbanned weeb123)
    u"channel:read:redemptions",  // for getting the list of channel point redemptions (not currently used)
    u"chat:edit",      // for sending messages in chat
    u"chat:read",      // for viewing messages in chat
    u"whispers:read",  // for viewing recieved whispers

    // https://dev.twitch.tv/docs/api/reference#start-commercial
    u"channel:edit:commercial",  // for /commercial api

    // https://dev.twitch.tv/docs/api/reference#create-clip
    u"clips:edit",  // for /clip creation

    // https://dev.twitch.tv/docs/api/reference#create-stream-marker
    // https://dev.twitch.tv/docs/api/reference#modify-channel-information
    u"channel:manage:broadcast",  // for creating stream markers with /marker command, and for the /settitle and /setgame commands

    // https://dev.twitch.tv/docs/api/reference#get-user-block-list
    u"user:read:blocked_users",  // for getting list of blocked users

    // https://dev.twitch.tv/docs/api/reference#block-user
    // https://dev.twitch.tv/docs/api/reference#unblock-user
    u"user:manage:blocked_users",  // for blocking/unblocking other users

    // https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages
    u"moderator:manage:automod",  // for approving/denying automod messages

    // https://dev.twitch.tv/docs/api/reference#start-a-raid
    // https://dev.twitch.tv/docs/api/reference#cancel-a-raid
    u"channel:manage:raids",  // for starting/canceling raids

    // https://dev.twitch.tv/docs/api/reference#create-poll
    // https://dev.twitch.tv/docs/api/reference#end-poll
    u"channel:manage:polls",  // for creating & ending polls (not currently used)

    // https://dev.twitch.tv/docs/api/reference#get-polls
    u"channel:read:polls",  // for reading broadcaster poll status (not currently used)

    // https://dev.twitch.tv/docs/api/reference#create-prediction
    // https://dev.twitch.tv/docs/api/reference#end-prediction
    u"channel:manage:predictions",  // for creating & ending predictions (not currently used)

    // https://dev.twitch.tv/docs/api/reference#get-predictions
    u"channel:read:predictions",  // for reading broadcaster prediction status (not currently used)

    // https://dev.twitch.tv/docs/api/reference#send-chat-announcement
    u"moderator:manage:announcements",  // for /announce api

    // https://dev.twitch.tv/docs/api/reference#send-whisper
    u"user:manage:whispers",  // for whispers api

    // https://dev.twitch.tv/docs/api/reference#ban-user
    // https://dev.twitch.tv/docs/api/reference#unban-user
    u"moderator:manage:banned_users",  // for ban/unban/timeout/untimeout api & channel.moderate eventsub topic

    // https://dev.twitch.tv/docs/api/reference#delete-chat-messages
    u"moderator:manage:chat_messages",  // for delete message api (/delete, /clear) & channel.moderate eventsub topic

    // https://dev.twitch.tv/docs/api/reference#update-user-chat-color
    u"user:manage:chat_color",  // for update user color api (/color coral)

    // https://dev.twitch.tv/docs/api/reference#get-chat-settings
    u"moderator:manage:chat_settings",  // for roomstate api (/followersonly, /uniquechat, /slow) & channel.moderate eventsub topic

    // https://dev.twitch.tv/docs/api/reference#get-moderators
    // https://dev.twitch.tv/docs/api/reference#add-channel-moderator
    // https://dev.twitch.tv/docs/api/reference#remove-channel-vip
    u"channel:manage:moderators",  // for add/remove/view mod api

    // https://dev.twitch.tv/docs/api/reference#add-channel-vip
    // https://dev.twitch.tv/docs/api/reference#remove-channel-vip
    // https://dev.twitch.tv/docs/api/reference#get-vips
    u"channel:manage:vips",  // for add/remove/view vip api

    // https://dev.twitch.tv/docs/api/reference#get-chatters
    u"moderator:read:chatters",  // for get chatters api

    // https://dev.twitch.tv/docs/api/reference#get-shield-mode-status
    // https://dev.twitch.tv/docs/api/reference#update-shield-mode-status
    u"moderator:manage:shield_mode",  // for reading/managing the channel's shield-mode status

    // https://dev.twitch.tv/docs/api/reference/#send-a-shoutout
    u"moderator:manage:shoutouts",  // for reading/managing the channel's shoutouts (not currently used)

    // https://dev.twitch.tv/docs/api/reference/#get-moderated-channels
    u"user:read:moderated_channels",  // for reading where the user is modded (not currently used)

    // https://dev.twitch.tv/docs/eventsub/eventsub-subscription-types/#channelchatmessage
    u"user:read:chat",  // for reading chat via eventsub (in progress)

    // https://dev.twitch.tv/docs/api/reference/#send-chat-message
    u"user:write:chat",  // for sending chat messages via helix (in testing)

    // https://dev.twitch.tv/docs/api/reference/#get-user-emotes
    u"user:read:emotes",  // for fetching emotes that a user can use via helix

    // https://dev.twitch.tv/docs/api/reference/#warn-chat-user
    u"moderator:manage:warnings",  // for /warn api & channel.moderate eventsub topic

    // https://dev.twitch.tv/docs/api/reference/#get-followed-channels
    u"user:read:follows",  // for determining if the current user follows a streamer

    u"moderator:manage:blocked_terms",  // for channel.moderate eventsub topic

    u"moderator:manage:unban_requests",  // for channel.moderate eventsub topic

    u"moderator:read:moderators",  // for channel.moderate eventsub topic

    u"moderator:read:vips",  // for channel.moderate eventsub topic

    u"moderator:read:suspicious_users",  // for channel.suspicious_user.message and channel.suspicious_user.update

    // https://dev.twitch.tv/docs/api/reference#add-suspicious-status-to-chat-user
    // https://dev.twitch.tv/docs/api/reference#remove-suspicious-status-from-chat-user
    u"moderator:manage:suspicious_users",
};

TwitchAccountManager::TwitchAccountManager()
    : accounts(SharedPtrElementLess<TwitchAccount>{})
    , anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
{
    this->currentUserChanged.connect([this] {
        auto currentUser = this->getCurrent();
        currentUser->loadBlocks();
        currentUser->loadSeventvUserID();
        if (!currentUser->isAnon())
        {
            checkMissingScopes(currentUser);
        }
    });

    // We can safely ignore this signal connection since accounts will always be removed
    // before TwitchAccountManager
    std::ignore = this->accounts.itemRemoved.connect([this](const auto &acc) {
        this->removeUser(acc.item.get());
    });
}

std::shared_ptr<TwitchAccount> TwitchAccountManager::getCurrent()
{
    if (!this->currentUser_)
    {
        return this->anonymousUser_;
    }

    return this->currentUser_;
}

std::vector<QString> TwitchAccountManager::getUsernames() const
{
    std::vector<QString> userNames;

    std::lock_guard<std::mutex> lock(this->mutex_);

    for (const auto &user : this->accounts)
    {
        userNames.push_back(user->getUserName());
    }

    return userNames;
}

std::shared_ptr<TwitchAccount> TwitchAccountManager::findUserByUsername(
    const QString &username) const
{
    std::lock_guard<std::mutex> lock(this->mutex_);

    for (const auto &user : this->accounts)
    {
        if (username.compare(user->getUserName(), Qt::CaseInsensitive) == 0)
        {
            return user;
        }
    }

    return nullptr;
}

bool TwitchAccountManager::userExists(const QString &username) const
{
    return this->findUserByUsername(username) != nullptr;
}

void TwitchAccountManager::reloadUsers()
{
    auto keys = pajlada::Settings::SettingManager::getObjectKeys("/accounts");

    UserData userData;

    bool listUpdated = false;

    for (const auto &uid : keys)
    {
        if (uid == "current")
        {
            continue;
        }

        auto username = pajlada::Settings::Setting<QString>::get(
            "/accounts/" + uid + "/username");
        auto userID = pajlada::Settings::Setting<QString>::get("/accounts/" +
                                                               uid + "/userID");
        auto clientID = pajlada::Settings::Setting<QString>::get(
            "/accounts/" + uid + "/clientID");
        auto oauthToken = pajlada::Settings::Setting<QString>::get(
            "/accounts/" + uid + "/oauthToken");

        if (username.isEmpty() || userID.isEmpty() || clientID.isEmpty() ||
            oauthToken.isEmpty())
        {
            continue;
        }

        userData.username = username.trimmed();
        userData.userID = userID.trimmed();
        userData.clientID = clientID.trimmed();
        userData.oauthToken = oauthToken.trimmed();

        switch (this->addUser(userData))
        {
            case AddUserResponse::UserAlreadyExists: {
                qCDebug(chatterinoTwitch)
                    << "User" << userData.username << "already exists";
                // Do nothing
            }
            break;
            case AddUserResponse::UserValuesUpdated: {
                qCDebug(chatterinoTwitch)
                    << "User" << userData.username
                    << "already exists, and values updated!";
                if (userData.username == this->getCurrent()->getUserName())
                {
                    qCDebug(chatterinoTwitch)
                        << "It was the current user, so we need to "
                           "reconnect stuff!";
                    this->currentUserChanged();
                }
            }
            break;
            case AddUserResponse::UserAdded: {
                qCDebug(chatterinoTwitch) << "Added user" << userData.username;
                listUpdated = true;
            }
            break;
        }
    }

    if (listUpdated)
    {
        this->userListUpdated.invoke();
    }
}

void TwitchAccountManager::load()
{
    if (getApp()->getArgs().initialLogin.has_value())
    {
        this->currentUsername = getApp()->getArgs().initialLogin.value();
    }

    this->reloadUsers();

    this->currentUsername.connect([this](const QString &newUsername) {
        auto user = this->findUserByUsername(newUsername);

        this->currentUserAboutToChange.invoke(this->currentUser_, user);

        if (user)
        {
            qCDebug(chatterinoTwitch)
                << "Twitch user updated to" << newUsername;
            getHelix()->update(user->getOAuthClient(), user->getOAuthToken());
            this->currentUser_ = user;
        }
        else
        {
            qCDebug(chatterinoTwitch) << "Twitch user updated to anonymous";
            this->currentUser_ = this->anonymousUser_;
        }

        this->currentUserChanged();
        this->currentUser_->reloadEmotes();
    });
}

bool TwitchAccountManager::isLoggedIn() const
{
    if (!this->currentUser_)
    {
        return false;
    }

    // Once `TwitchAccount` class has a way to check, we should also return
    // false if the credentials are incorrect
    return !this->currentUser_->isAnon();
}

bool TwitchAccountManager::removeUser(TwitchAccount *account)
{
    static const QString accountFormat("/accounts/uid%1");

    auto userID(account->getUserId());
    if (!userID.isEmpty())
    {
        pajlada::Settings::SettingManager::gRemoveSetting(
            accountFormat.arg(userID).toStdString());
    }

    if (account->getUserName() == this->currentUsername)
    {
        // The user that was removed is the current user, log into the anonymous
        // user
        this->currentUsername = "";
    }

    this->userListUpdated.invoke();

    return true;
}

TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser(
    const TwitchAccountManager::UserData &userData)
{
    auto previousUser = this->findUserByUsername(userData.username);
    if (previousUser)
    {
        bool userUpdated = false;

        if (previousUser->setOAuthClient(userData.clientID))
        {
            userUpdated = true;
        }

        if (previousUser->setOAuthToken(userData.oauthToken))
        {
            userUpdated = true;
        }

        if (userUpdated)
        {
            return AddUserResponse::UserValuesUpdated;
        }
        else
        {
            return AddUserResponse::UserAlreadyExists;
        }
    }

    auto newUser =
        std::make_shared<TwitchAccount>(userData.username, userData.oauthToken,
                                        userData.clientID, userData.userID);

    //    std::lock_guard<std::mutex> lock(this->mutex);

    this->accounts.insert(newUser);

    return AddUserResponse::UserAdded;
}

}  // namespace chatterino
