#include <maps/wikimap/mapspro/libs/acl/include/user.h>

#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <maps/wikimap/mapspro/libs/acl/include/group.h>
#include <maps/wikimap/mapspro/libs/acl/include/policy.h>

#include "exception_helper.h"
#include "factory.h"

#include <maps/libs/enum_io/include/enum_io.h>

#include <iostream>

namespace maps::wiki::acl {

namespace {

constexpr enum_io::Representations<User::Status> STATUS_ENUM_REPRESENTATION {
    {User::Status::Active, "active"},
    {User::Status::Banned, "banned"},
    {User::Status::Deleted, "deleted"}
};

constexpr enum_io::Representations<User::DeleteReason> DELETE_REASON_ENUM_REPRESENTATION {
    {User::DeleteReason::User, "user"},
    {User::DeleteReason::Spammer, "spammer"},
    {User::DeleteReason::Suspicious, "suspicious"},
    {User::DeleteReason::DDOS, "ddos"},
    {User::DeleteReason::YndxRegistration, "yndx-registration"},
    {User::DeleteReason::InactiveOutsourcer, "inactive-outsourcer"},
};

constexpr enum_io::Representations<User::TrustLevel> TRUST_LEVEL_ENUM_REPRESENTATION {
    {User::TrustLevel::Trusted, "trusted"},
    {User::TrustLevel::Novice, "novice"},
    {User::TrustLevel::AfterBan, "afterBan"}
};

const time_t SECONDS_PER_DAY = 24 * 60 * 60;
const time_t NOVICE_PERIOD = 7 * SECONDS_PER_DAY;
const time_t AFTER_BAN_PERIOD = 7 * SECONDS_PER_DAY;

const std::string QUERY_OWN_POLICIES_BY_USER_ID =
    "SELECT policy.*, role.name, role.description, role.is_public FROM acl.policy"
    " JOIN acl.role ON (policy.role_id = role.id)"
    " WHERE agent_id = ";

const std::string QUERY_GROUPS_POLICIES_BY_USER_ID =
    "SELECT policy.*, role.name, role.description, role.is_public FROM acl.policy"
    " JOIN acl.role ON (policy.role_id = role.id)"
    " JOIN acl.group_user ON (policy.agent_id = group_user.group_id)"
    " WHERE user_id = ";

const std::string QUERY_GROUPS_BY_USER_ID =
    "SELECT acl.group.* FROM acl.group, acl.group_user "
    " WHERE acl.group.id = acl.group_user.group_id "
    "   AND user_id = ";

User::TrustLevel
calcTrustLevel(
    User::Status status,
    time_t createdTs,
    time_t lastBanRecordTs)
{
    if (status == User::Status::Banned) {
        return User::TrustLevel::AfterBan;
    }

    time_t now = std::time(nullptr);
    if (lastBanRecordTs + AFTER_BAN_PERIOD > now) {
        return User::TrustLevel::AfterBan;
    }
    if (createdTs + NOVICE_PERIOD > now) {
        return User::TrustLevel::Novice;
    }
    return User::TrustLevel::Trusted;
}

std::vector<Policy>
loadPolicies(
    pqxx::transaction_base& work,
    const std::string& query)
{
    auto rows = work.exec(query);

    std::vector<Policy> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        auto role = Factory::role(
            row["role_id"].as<ID>(),
            row["name"].as<std::string>(),
            row["description"].as<std::string>(),
            Role::privacy(row["is_public"].as<bool>()),
            work);
        result.push_back(Factory::policy(
            row["agent_id"].as<ID>(),
            std::move(role),
            row["aoi_id"].as<ID>(), work));
    }
    return result;
}

} // namespace

DEFINE_ENUM_IO(User::Status, STATUS_ENUM_REPRESENTATION);
DEFINE_ENUM_IO(User::DeleteReason, DELETE_REASON_ENUM_REPRESENTATION);
DEFINE_ENUM_IO(User::TrustLevel, TRUST_LEVEL_ENUM_REPRESENTATION);

User::User(
    ID id, UID uid, std::string login,
    std::string created, UID createdBy,
    std::string modified, UID modifiedBy,
    Status status, std::optional<DeleteReason> deleteReason,
    const std::optional<BanRecord>& currentBan,
    time_t createdTs, time_t lastBanRecordTs,
    const std::string& displayName,
    Transaction& work)
    : id_(id)
    , uid_(uid)
    , login_(std::move(login))
    , displayName_(displayName)
    , created_(std::move(created))
    , createdBy_(createdBy)
    , modified_(std::move(modified))
    , modifiedBy_(modifiedBy)
    , status_(status)
    , deleteReason_(deleteReason)
    , trustLevel_(calcTrustLevel(status, createdTs, lastBanRecordTs))
    , currentBan_(currentBan)
    , work_(work)
{
    if (lastBanRecordTs > 0) {
        unbannedAt_ = std::chrono::system_clock::from_time_t(lastBanRecordTs);
    }
}

std::vector<Group>
User::groups() const
{
    auto rows = work_.exec(QUERY_GROUPS_BY_USER_ID + std::to_string(id_));

    std::vector<Group> result;
    result.reserve(rows.size());
    for (const auto& row : rows) {
        result.push_back(Factory::group(
            row["id"].as<ID>(),
            row["name"].c_str(),
            row["description"].c_str(),
            work_));
    }
    return result;
}

std::vector<Policy>
User::policies() const
{
    return loadPolicies(work_,
        QUERY_OWN_POLICIES_BY_USER_ID + std::to_string(id_));
}

std::vector<Policy>
User::groupsPolicies() const
{
    return loadPolicies(work_,
        QUERY_GROUPS_POLICIES_BY_USER_ID + std::to_string(id_));
}

std::vector<Policy>
User::allPolicies() const
{
    const auto stringId = std::to_string(id_);
    return loadPolicies(work_,
        QUERY_OWN_POLICIES_BY_USER_ID + stringId +
        " UNION " +
        QUERY_GROUPS_POLICIES_BY_USER_ID + stringId);
}

void
User::removePolicies() const
{
    auto query =
        "DELETE FROM acl.policy WHERE agent_id = " + std::to_string(id_);
    work_.exec(query);
}

void
User::removePolicy(ID roleID, ID aoiID) const
{
    auto query =
        "DELETE FROM acl.policy "
        " WHERE agent_id = " + std::to_string(id_) +
        " AND role_id = " + std::to_string(roleID) +
        " AND aoi_id = " + std::to_string(aoiID);
    work_.exec(query);
}

void
User::setActive(UID authorId)
{
    try {
        auto query =
            "UPDATE acl.user SET "
                "status = " + work_.quote(toString(Status::Active).data()) + ", "
                "modified = Now(), modified_by = " + std::to_string(authorId) +
            " WHERE id = " + std::to_string(id_);
        work_.exec(query);
        status_ = Status::Active;
    } CATCH_CONVERT();
}

void
User::setDeleted(DeleteReason reason, UID authorId)
{
    try {
        auto query =
            "UPDATE acl.user SET "
                "status = " + work_.quote(toString(Status::Deleted).data()) + ", "
                "delete_reason = " + work_.quote(toString(reason).data()) + ", "
                "modified = Now(), modified_by = " + std::to_string(authorId) +
            " WHERE id = " + std::to_string(id_);
        work_.exec(query);
        status_ = Status::Deleted;
    } CATCH_CONVERT();
}

void
User::setLogin(const std::string& newLogin)
{
    try {
        work_.exec(
            "UPDATE acl.user SET login = " +
            work_.quote(newLogin) +
            " WHERE id = " + std::to_string(id_));
        displayName_ = newLogin;
    } CATCH_CONVERT();
}

void
User::setDisplayName(const std::string& newDisplayName)
{
    try {
        work_.exec(
            "UPDATE acl.user SET display_name = " +
            work_.quote(newDisplayName) +
            " WHERE id = " + std::to_string(id_));
        displayName_ = newDisplayName;
    } CATCH_CONVERT();
}

void
User::checkActiveStatus() const
{
    auto st = status();
    if (st != Status::Active) {
        throw AccessDenied(uid()) << " is " << st;
    }
}

void
User::checkNotDeletedStatus() const
{
    auto st = status();
    if (st == Status::Deleted) {
        throw AccessDenied(uid()) << " is " << st;
    }
}

std::string User::staffLogin() const
{
    const auto rows = work_.exec(
        "SELECT staff_login FROM acl.uid_staff_login WHERE uid = " +
        std::to_string(uid_));
    return rows.empty()
        ? std::string()
        : rows[0][0].as<std::string>();
}

void
User::setStaffLogin(const std::string& newStaffLogin) const
{
    const auto curStaffLogin = staffLogin();
    if (curStaffLogin.empty()) {
        work_.exec(
            "INSERT INTO acl.uid_staff_login (uid, staff_login) "
            " VALUES (" + std::to_string(uid_) + "," + work_.quote(newStaffLogin) + ")");
    } else if (newStaffLogin != curStaffLogin) {
        work_.exec(
            "UPDATE acl.uid_staff_login SET staff_login = " + work_.quote(newStaffLogin) +
            " WHERE uid = " + std::to_string(uid_));
    };
}

} // namespace maps::maps::wiki::acl
