#include <maps/wikimap/mapspro/services/social/src/api/globals.h>
#include <maps/wikimap/mapspro/services/social/src/libs/yacare/helpers.h>
#include <maps/wikimap/mapspro/services/social/src/libs/common/common.h>

#include <boost/algorithm/string/split.hpp>

using TBadgeId = uint64_t;

namespace {
const std::string STR_BADGES_PERMISSIONS = "mpro/social/badges";
const std::string STR_DELETABLE = "deletable";
const std::string STR_ICON = "icon";
const std::string STR_ID = "id";
const std::string STR_BADGE_ID = "badge_id";
const std::string STR_JSON_BADGE_ID = "badgeId";
const std::string STR_BADGE = "badge";
const std::string STR_BADGES = "badges";
const std::string STR_TOKEN = "token";
const std::string STR_LEVEL = "level";
const std::string STR_UID = "uid";
const std::string STR_BADGE_ID_LEVEL_DELIMITER = "__";

bool isBadgeAssigned(pqxx::transaction_base& txn, const std::string& badgeId)
{
    ASSERT(!badgeId.empty());
    auto levelPos = badgeId.find(STR_BADGE_ID_LEVEL_DELIMITER);
    if (levelPos == std::string::npos) {
        return txn.exec(
            "SELECT EXISTS (SELECT badge_id FROM social.badge WHERE badge_id=" +
            txn.quote(badgeId) +
            ");")[0][0].as<bool>();
    } else {
        auto profileBadgeId = badgeId.substr(0, levelPos);
        auto level = badgeId.substr(levelPos + 2);
        return txn.exec(
            "SELECT EXISTS (SELECT badge_id FROM social.badge WHERE badge_id=" +
            txn.quote(profileBadgeId) + " AND level=" + txn.esc(level) +
            ");")[0][0].as<bool>();
    }

}

yacare::ThreadPool badgesThreadPool("badgesThreadPool", 1, 1024);

}//namespace

namespace maps::wiki::socialsrv {

YCR_USE(badgesThreadPool) {

YCR_RESPOND_TO("GET /profile/$/badges", token = "")
{
    auto userId = positionalParam<UserId>(argv, 0);
    auto socialTxn = Globals::dbPools().socialReadTxn(token);
    auto query =
        "WITH current_badges AS ("
            "SELECT DISTINCT ON (badge_id) * "
                "FROM social.badge WHERE uid = " + std::to_string(userId) + " "
                "ORDER BY badge_id, level DESC) "
        "SELECT c.badge_id, level, icon FROM current_badges c "
        " LEFT JOIN social.badge_meta m ON "
        " c.badge_id=m.badge_id OR c.badge_id || '" +
        STR_BADGE_ID_LEVEL_DELIMITER + "' || level=m.badge_id "
            "ORDER BY awarded_at DESC";

    auto badgeRows = socialTxn->exec(query);

    response << YCR_JSON(respBuilder) {
        respBuilder[STR_UID] = std::to_string(userId);
        respBuilder[STR_BADGES] = YCR_JSON_ARRAY(badgesBuilder) {
            for (const auto& badgeRow : badgeRows) {
                badgesBuilder << YCR_JSON(badgeBuilder) {
                    auto badgeId = badgeRow[STR_BADGE_ID].as<std::string>();
                    const auto& levelField = badgeRow[STR_LEVEL];
                    if (!levelField.is_null()) {
                        badgeId += STR_BADGE_ID_LEVEL_DELIMITER + levelField.as<std::string>();
                    }
                    badgeBuilder[STR_JSON_BADGE_ID] = badgeId;
                    const auto& iconField = badgeRow[STR_ICON];
                    if (!iconField.is_null()) {
                        badgeBuilder[STR_ICON] = iconField.as<std::string>();
                    }
                };
            }
        };
    };
}

YCR_RESPOND_TO("GET /badges", token = "")
{
    auto badgesIds = optionalQueryParam<std::string>(request, "ids");
    auto socialTxn = Globals::dbPools().socialReadTxn(token);
    pqxx::result badgeRows;
    if (!badgesIds) {
        badgeRows = socialTxn->exec(
            "SELECT id, badge_id, icon "
            "FROM social.badge_meta a ORDER BY id DESC");
    } else {
        std::vector<std::string> ids;
        boost::split(ids, *badgesIds, [](const char a) { return a == ',';});
        for (auto& id : ids) {
            boost::trim(id);
        }
        ids.erase(
            std::remove_if(ids.begin(), ids.end(),
                [](const std::string& id) {return id.empty();}), ids.end());

        REQUIRE(!ids.empty(), yacare::errors::BadRequest() << "Empty ids array: " << *badgesIds);

        std::string idsStr;
        for (const auto& id : ids) {
            idsStr += (idsStr.empty() ? "" : ",") + socialTxn->quote(id);
        }

        badgeRows = socialTxn->exec(
            "SELECT id, badge_id, icon "
            "FROM social.badge_meta a "
            "WHERE badge_id IN(" + idsStr + ") "
            "ORDER BY id DESC");
    }
    response << YCR_JSON(respBuilder) {
        respBuilder[STR_BADGES] = YCR_JSON_ARRAY(badgesBuilder) {
            for (const auto& badgeRow : badgeRows) {
                badgesBuilder << YCR_JSON(badgeBuilder) {
                    badgeBuilder[STR_ID] = badgeRow[STR_ID].as<std::string>();
                    const auto& badgeId = badgeRow[STR_BADGE_ID].as<std::string>();
                    badgeBuilder[STR_JSON_BADGE_ID] = badgeId;
                    badgeBuilder[STR_ICON] = badgeRow[STR_ICON].is_null() ? "" : badgeRow[STR_ICON].as<std::string>();
                    badgeBuilder[STR_DELETABLE] = !isBadgeAssigned(*socialTxn, badgeId);
                };
            }
        };
    };
}

YCR_RESPOND_TO("DELETE /badges/$", uid)
{
    Globals::aclChecker().checkPermission(uid, STR_BADGES_PERMISSIONS);

    auto badgeId = boost::trim_copy(positionalParam<std::string>(argv, 0));
    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();

    SOCIAL_REQUIRE(
        !isBadgeAssigned(socialTxn, badgeId),
        InvalidOperation,
        "Badge " << badgeId << " is assigned to profiles."
    );

    auto query =
        "DELETE FROM social.badge_meta "
        "WHERE badge_id=" + socialTxn.quote(badgeId) + " "
        "RETURNING id";

    SOCIAL_REQUIRE(
        !socialTxn.exec(query).empty(),
        NotFound,
        "Badge " << badgeId << " does not exist."
    );

    auto token = writeContext.commit();
    response << YCR_JSON(obj) {
        obj[STR_TOKEN] = token;
    };
}

YCR_RESPOND_TO("POST /badges", uid)
{
    Globals::aclChecker().checkPermission(uid, STR_BADGES_PERMISSIONS);

    std::unique_ptr<maps::json::Value> postBodyValueParsed;
    try {
        postBodyValueParsed.reset(new maps::json::Value(maps::json::Value::fromString(request.body())));
    } catch (...) {
        throw yacare::errors::BadRequest()
            << "Failed to parse request json: " << request.body();
    }

    const auto& postBodyValue = *postBodyValueParsed;
    REQUIRE(
        postBodyValue.hasField(STR_ICON) && postBodyValue.hasField(STR_JSON_BADGE_ID),
        yacare::errors::BadRequest()
            << "Malformed badge save request: " << request.body()
    );

    const auto& iconValue = postBodyValue[STR_ICON];
    const auto& badgeIdValue = postBodyValue[STR_JSON_BADGE_ID];

    REQUIRE(
        iconValue.isString() && badgeIdValue.isString(),
        yacare::errors::BadRequest()
            << "Wrong format of fields for badge save request: " << request.body()
    );

    auto id = postBodyValue.hasField(STR_ID)
        ? boost::lexical_cast<TBadgeId>(postBodyValue[STR_ID].as<std::string>())
        : 0;
    auto icon = iconValue.as<std::string>();
    auto badgeId = boost::trim_copy(badgeIdValue.as<std::string>());

    auto writeContext = Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();
    bool isAssigned = false;
    if (!id) {
        auto res = socialTxn.exec(
            "INSERT INTO social.badge_meta(badge_id, icon) "
            "VALUES (" + socialTxn.quote(badgeId) + "," + socialTxn.quote(icon) + ") "
            "ON CONFLICT DO NOTHING "
            "RETURNING id");
        ASSERT(res.size() <= 1);
        SOCIAL_REQUIRE(!res.empty(), Duplicate, "Badge: " << badgeId << " already exists.");
        id = res[0][0].as<TBadgeId>();
    } else {
        auto res = socialTxn.exec(
            "UPDATE social.badge_meta "
            "SET icon=" + socialTxn.quote(icon) + " "
            "WHERE badge_id=" + socialTxn.quote(badgeId) + " "
            "RETURNING id");
        SOCIAL_REQUIRE(
            !res.empty() && res[0][0].as<TBadgeId>() == id,
            NotFound,
            "Badge " << badgeId << " does not exist."
        );

        isAssigned = isBadgeAssigned(socialTxn, badgeId);
    }
    auto token = writeContext.commit();
    response << YCR_JSON(obj) {
        obj[STR_TOKEN] = token;
        obj[STR_BADGE] = YCR_JSON(badgeBuilder) {
            badgeBuilder[STR_ID] = std::to_string(id);
            badgeBuilder[STR_JSON_BADGE_ID] = badgeId;
            badgeBuilder[STR_ICON] = icon;
            badgeBuilder[STR_DELETABLE] = !isAssigned;
        };
    };
}

} // YCR_USE

} // namespace maps::wiki::socialsrv
