#include <maps/wikimap/mapspro/services/tasks_realtime/src/acl_permissions_synchronizer/lib/staff.h>
#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/moderation.h>

#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/secrets.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/status_writer.h>

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>

#include <boost/algorithm/string.hpp>

#include <algorithm>
#include <exception>
#include <functional>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace common = maps::wiki::common;

namespace maps::wiki {

namespace {

enum class Mode {
    Dry,
    Real
};

constexpr enum_io::Representations<Mode>
MODE_STRINGS {
    {Mode::Dry, "dry"},
    {Mode::Real, "real"}
};

DEFINE_ENUM_IO(Mode, MODE_STRINGS);

const std::string MAX_OUTSOURCE_INACTIVITY_TIME = "60 days";
const std::string YANDEX_LOGIN_PREFIX = "yndx";

const std::string OUTSOURCE_ROLE = "outsource-role";
const std::string ROBOT_ROLE = "robot";
const std::string QA_GROUP = "nmaps-qa";

const std::unordered_set<std::string> ADVANCED_GROUPS = {
    "mpro"
};

acl::ID setUserStaffLoginReturnId(
    acl::ACLGateway& aclGw,
    const std::string& nmapsLogin,
    const std::string& staffLogin)
{
    const auto loginPart = nmapsLogin.substr(0, nmapsLogin.find('@'));
    const auto loginPartSize = loginPart.size();
    auto users = aclGw.users(loginPart, 0);
    for (const auto& user : users) {
        if (user.login().size() == loginPartSize) {
            user.setStaffLogin(staffLogin);
            return user.id();
        }
    }
    WARN()
        << "Staff user: " << staffLogin
        << " with nmaps login: " << nmapsLogin
        << " not found in acl.";
    return 0;
}

std::unordered_map<acl::ID, std::string>
syncAndGetIdToStaffMap(acl::ACLGateway& aclGw, const Staff& staff)
{
    const auto allUsersIDMRoles = aclGw.allUsersIDMRoles();
    std::set<std::string> staffLogins;
    for (const auto& [login, _] : allUsersIDMRoles) {
        staffLogins.insert(login);
    }
    const auto staffUsers = staff.getUsersByLogins(staffLogins);
    aclGw.clearUidToStaff();
    std::unordered_map<acl::ID, std::string> idToStaffLogin;
    for (const auto& staffUser : staffUsers) {
        for (const auto& nmapsLogin : staffUser.nmapsLogins) {
            auto userId = setUserStaffLoginReturnId(aclGw, nmapsLogin, staffUser.login);
            if (userId) {
                idToStaffLogin.emplace(userId, staffUser.login);
            }
        }
    }
    return idToStaffLogin;
}

std::set<acl::ID>
getAdvancedRoleIds(acl::ACLGateway& aclGw)
{
    std::set<acl::ID> result;
    for (const auto& role : aclGw.roles()) {
        if (!role.isPublic())
        {
            result.insert(role.id());
        }
    }
    return result;
}

std::unordered_set<std::string>
getAdvancedGroupNamesByRoleIds(
    acl::ACLGateway& aclGw,
    const std::set<acl::ID>& advancedRoleIds)
{
    std::unordered_set<std::string> result = ADVANCED_GROUPS;

    auto allGroups = aclGw.groups();
    for (const auto& group : allGroups) {
        auto policies = group.policies();
        if (std::any_of(
                policies.begin(),
                policies.end(),
                [&](const acl::Policy& policy) {
                    return advancedRoleIds.contains(policy.roleId());
                }))
        {
            result.insert(group.name());
        }
    }
    return result;
}

std::unordered_set<acl::ID>
getUsersWithAdvancedRolesFromAcl(
    acl::ACLGateway& aclGw,
    const std::set<acl::ID>& advancedRoleIds,
    const std::unordered_set<std::string>& advancedGroupNames)
{
    std::unordered_set<acl::ID> result;

    for (const auto& groupName : advancedGroupNames) {
        for (const auto& user : aclGw.group(groupName).users()) {
            result.insert(user.id());
        }
    }

    auto userIdsByRoles = aclGw.userIdsByRoles(advancedRoleIds);
    result.insert(userIdsByRoles.begin(), userIdsByRoles.end());

    return result;
}

std::unordered_set<std::string>
getStaffLoginsWithAdvancedIdmRoles(acl::ACLGateway& aclGw)
{
    const auto allUsersIDMRoles = aclGw.allUsersIDMRoles("editor");
    std::unordered_set<std::string> result;
    for (const auto& [staffLogin, idmRoles] : allUsersIDMRoles) {
        if (idmRoles.count("advanced-user")) {
            result.insert(staffLogin);
        }
    }
    return result;
}

std::unordered_set<std::string>
getGroupNames(acl::User& user)
{
    std::unordered_set<std::string> result;
    for (const auto& group : user.groups()) {
        result.insert(group.name());
    }
    return result;
}

bool
isActiveUser(
    pqxx::transaction_base& socialTxn,
    acl::UID uid)
{
    std::string query =
        "SELECT FROM social.commit_event "
        "WHERE created_by = " + std::to_string(uid) +
        "  AND age(created_at) < interval '" +
        MAX_OUTSOURCE_INACTIVITY_TIME + "' " +
        "LIMIT 1";
    return !socialTxn.exec(query).empty();
}

bool
isUserChangeOldEnough(
    pqxx::transaction_base& coreTxn,
    acl::UID uid)
{
    std::string query =
        "SELECT FROM acl.user "
        "WHERE uid = " + std::to_string(uid) +
        "  AND age(modified) > interval '" +
        MAX_OUTSOURCE_INACTIVITY_TIME + "'";
    return !coreTxn.exec(query).empty();
}

void
removeAdvancedPermissions(
    acl::ACLGateway& aclGw,
    acl::User& user,
    const std::unordered_set<std::string>& userGroupNames,
    const std::set<acl::ID>& advancedRoleIds,
    const std::unordered_set<std::string>& advancedGroupNames,
    Mode mode)
{
    std::set<std::string> removedRoleNames;
    for (const auto& policy : user.policies()) {
        if (advancedRoleIds.contains(policy.roleId()))
        {
            removedRoleNames.insert(policy.role().name());
            if (mode == Mode::Real) {
                user.removePolicy(policy.roleId(), policy.aoiId());
            }
        }
    }

    std::set<std::string> removedGroupNames;
    for (const auto& groupName : advancedGroupNames) {
        if (userGroupNames.contains(groupName)) {
            removedGroupNames.insert(groupName);
            if (mode == Mode::Real) {
                aclGw.group(groupName).remove(user);
            }
        }
    }

    INFO() << "User: " << user.login() << " has no advanced access."
           << "Remove advanced roles: {" << common::join(removedRoleNames, ',') << "}. "
           << "Remove advanced groups: {" << common::join(removedGroupNames, ',') << "}.";
}

void
updateStatusFile(std::optional<std::string> filePath, std::function<void()> func)
{
    tasks::StatusWriter statusWriter(std::move(filePath));

    try {
        func();
        statusWriter.flush();
    } catch (const std::exception& e) {
        statusWriter.err(e.what());
        statusWriter.flush();
        throw;
    }
}

void run(
    pqxx::transaction_base& coreWriteTxn,
    pqxx::transaction_base& socialReadTxn,
    const Staff& staff,
    Mode mode)
{
    acl::ACLGateway aclGw(coreWriteTxn);
    aclGw.synchronizeSchedules();
    const auto idToStaffLogin = syncAndGetIdToStaffMap(aclGw, staff);

    const auto advancedRoleIds = getAdvancedRoleIds(aclGw);
    const auto advancedGroupNames = getAdvancedGroupNamesByRoleIds(
        aclGw, advancedRoleIds);

    const auto advancedUserIdsAcl = getUsersWithAdvancedRolesFromAcl(
        aclGw, advancedRoleIds, advancedGroupNames);
    const auto advancedUsersStaffLogins = getStaffLoginsWithAdvancedIdmRoles(
        aclGw);

    for (auto& userId : advancedUserIdsAcl) {
        auto user = aclGw.userById(userId);
        auto userGroupNames = getGroupNames(user);

        if (user.status() == acl::User::Status::Deleted) {
            continue;
        }
        if (acl_utils::hasRole(user, ROBOT_ROLE)) {
            continue;
        }
        if (userGroupNames.contains(QA_GROUP)) {
            continue;
        }

        if (acl_utils::hasRole(user, OUTSOURCE_ROLE)) {
            if (isUserChangeOldEnough(coreWriteTxn, user.uid()) &&
                !isActiveUser(socialReadTxn, user.uid()))
            {
                INFO() << "Inactive outsourcer: " << user.login() << ". Remove login";
                if (mode == Mode::Real) {
                    user.setDeleted(acl::User::DeleteReason::InactiveOutsourcer, common::ROBOT_UID);
                }
            }
            continue;
        }

        if (!idToStaffLogin.contains(userId) ||
            !advancedUsersStaffLogins.contains(idToStaffLogin.at(userId)))
        {
            if (user.login().starts_with(YANDEX_LOGIN_PREFIX)) {
                INFO() << "User: " << user.login() << " has no advanced access. Remove login";
                if (mode == Mode::Real) {
                    user.setDeleted(acl::User::DeleteReason::YndxRegistration, common::ROBOT_UID);
                }
            } else {
                removeAdvancedPermissions(
                    aclGw, user, userGroupNames,
                    advancedRoleIds, advancedGroupNames, mode);
            }
            continue;
        }
    }
    auto allYndxUsers = aclGw.users(YANDEX_LOGIN_PREFIX);
    for (auto& user : allYndxUsers) {
        if (user.status() != acl::User::Status::Deleted) {
            continue;
        }
        const auto userId = user.id();
        if (idToStaffLogin.contains(userId) &&
            advancedUsersStaffLogins.contains(idToStaffLogin.at(userId)))
        {
            INFO() << "User: " << user.login() << " restored.";
            if (mode == Mode::Real) {
                user.setActive(common::ROBOT_UID);
            }
        }
    }

    if (mode == Mode::Real) {
        coreWriteTxn.commit();
    }
}

} // namespace

} // namespace maps::wiki

int main(int argc, char* argv[])
try {
    maps::cmdline::Parser parser;
    auto configPath = parser
        .file("config")
        .help("Path to services.xml");
    auto syslogTag = parser
        .string("syslog-tag")
        .help("Redirect log output to syslog with given tag");
    auto modeStr = parser
        .string("mode")
        .help("Run mode: dry (only write messages to log) or real");
    auto statusDir = parser.string("status-dir")
        .help("Path to status dir");

    parser.parse(argc, argv);

    if (syslogTag.defined()) {
        maps::log8::setBackend(maps::log8::toSyslog(syslogTag));
    }

    auto mode = maps::wiki::Mode::Dry;
    if (modeStr.defined()) {
        mode = maps::enum_io::fromString<maps::wiki::Mode>(modeStr);
    }
    INFO() << "Run mode: " << mode;

    auto configDocPtr =
        configPath.defined()
        ? std::make_unique<common::ExtendedXmlDoc>(configPath)
        : common::loadDefaultConfig();

    common::PoolHolder coreDbHolder(*configDocPtr, "core", "grinder");

    maps::pgp3utils::PgAdvisoryXactMutex locker(
        coreDbHolder.pool(),
        static_cast<int64_t>(common::AdvisoryLockIds::PERMISSIONS_SYNCHRONIZER));
    if (!locker.try_lock()) {
        INFO() << "Core database is already locked. Stop working";
        return EXIT_SUCCESS;
    }

    common::PoolHolder socialDbHolder(*configDocPtr, "social", "grinder");

    auto staffUrlBase = configDocPtr->getAttr<std::string>(
        "/config/common/staff", "api-base-url");
    auto staffToken = maps::wiki::common::secrets::tokenByKey(
        maps::wiki::common::secrets::Key::RobotWikimapStaffToken);
    maps::wiki::Staff staff(staffUrlBase, staffToken);

    std::optional<std::string> statusFileName;
    if (statusDir.defined()) {
        statusFileName = statusDir + "/wiki-acl-permissions-synchronizer.status";
    }

    INFO() << "Permissions synchronization started";
    maps::wiki::updateStatusFile(
        statusFileName,
        [&]() {
            auto socialReadTxn = socialDbHolder.pool().slaveTransaction();
            maps::wiki::run(
                locker.writableTxn(),
                *socialReadTxn,
                staff,
                mode);
        });
    INFO() << "Permissions synchronization finished";

    return EXIT_SUCCESS;
} catch (const maps::Exception& e) {
    ERROR() << "Worker failed: " << e;
    return EXIT_FAILURE;
} catch (const std::exception& e) {
    ERROR() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
