#include "outsource_region.h"

#include <yandex/maps/wiki/common/geom_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/format.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/lexical_cast.hpp>

#include <algorithm>
#include <string>
#include <unordered_set>

namespace rev = maps::wiki::revision;

namespace maps {
namespace wiki {
namespace tasks {
namespace outsource_regions {

namespace {

const std::string ATTR_PREFIX = "outsource_region:";

const std::string ATTR_NAME = ATTR_PREFIX + "name";
const std::string ATTR_TASK_TYPE = ATTR_PREFIX + "task_type";
const std::string ATTR_STATUS = ATTR_PREFIX + "status";
const std::string ATTR_COMPANY_ID = ATTR_PREFIX + "company_id";
const std::string ATTR_PARENT_ST_KEY = ATTR_PREFIX + "parent_st_key";
const std::string ATTR_FC_SET = ATTR_PREFIX + "fc_set";
const std::string ATTR_QUALITY = ATTR_PREFIX + "quality";

const std::string LINK_TEMPLATE = "https://%1%/#!/objects/%4%?z=16&ll=%2%%%2C%3%&l=nk%%23sat";

const std::string DEFAULT_NAME_PREFIX = "Зона ";

const std::string ACTIONS_LOG_FORMAT = "nmaps-outsource-regions-actions-log";

const std::unordered_set<std::string> ATTRIBUTES_TO_LOG = {
    "name",
    "task_type",
    "fc_set",
    "complexity_rate",
    "company_id",
    "outsourcer_login",
    "quality",
    "parent_st_key",
    "status"
};

constexpr uint64_t MIN_FC_CLASS = 1;
constexpr uint64_t MAX_FC_CLASS = 10;

/**
Present given bitmask in human readable format.
Example: 163 -> 1,2,5,7
*/
std::string formatFCAttr(const std::string& fcValueStr)
{
    std::vector<uint64_t> classes;

    auto fcBitmask = boost::lexical_cast<uint64_t>(fcValueStr);
    for (size_t i = MIN_FC_CLASS; i <= MAX_FC_CLASS; i++) {
        if (fcBitmask & 1) {
            classes.push_back(i);
        }
        fcBitmask >>= 1;
    }

    return common::join(classes, ',');
}

std::string commentHeader(revision::DBID commitId, const std::string& login)
{
    return "Пользователь: **" + login + "**\n" +
        "Правка: " + std::to_string(commitId) + "\n";
}

std::string commentAttr(
    const std::string& attrName,
    const std::string& value,
    const Translator& translator)
{
    auto translatedName = translator.attribute(attrName);

    if (attrName == ATTR_TASK_TYPE || attrName == ATTR_STATUS || attrName == ATTR_QUALITY) {
        return translatedName + ": " + translator.attributeValue(attrName, value) + "\n";
    } else if (attrName == ATTR_FC_SET) {
        return translatedName + ": " + formatFCAttr(value) + "\n";
    } else {
        return translatedName + ": " + value + "\n";
    }
}

} // anonymous namespace

OutsourceRegion::OutsourceRegion(const revision::ObjectRevision& rev)
    : objectId_(rev.id().objectId())
    , attributes_(*rev.data().attributes)
    , geometry_(common::Geom(*rev.data().geometry))
    , defaultName_(DEFAULT_NAME_PREFIX + std::to_string(objectId_))
{
    auto it = attributes_.find(ATTR_NAME);
    if (it != attributes_.end()) {
        name_ = it->second;
    }

    it = attributes_.find(ATTR_COMPANY_ID);
    if (it != attributes_.end()) {
        companyId_ = it->second;
    }

    it = attributes_.find(ATTR_PARENT_ST_KEY);
    if (it != attributes_.end()) {
        parentStKey_ = it->second;
    }
}

void OutsourceRegion::updateAttributes(const rev::AttributesDiff& attrsDiff)
{
    for (const auto& kvp : attrsDiff) {
        const auto& attr = kvp.first;
        const auto& valueAfter = kvp.second.after;
        attributes_[attr] = valueAfter;

        if (attr == ATTR_NAME) {
            name_ = valueAfter;
        }
        if (attr == ATTR_COMPANY_ID) {
            companyId_ = valueAfter;
        }
        if (attr == ATTR_PARENT_ST_KEY) {
            parentStKey_ = valueAfter;
        }
    }
}

void OutsourceRegion::updateGeometry(const rev::GeometryDiff& geomDiff)
{
    geometry_ = common::Geom(geomDiff.after);
}

std::string OutsourceRegion::description(
    revision::DBID commitId,
    const std::string& login,
    const Translator& translator) const
{
    std::stringstream text;
    text << commentHeader(commitId, login);

    for (const auto& kvp : attributes_) {
        const auto& attrName = kvp.first;
        const auto& value = kvp.second;

        if (!boost::algorithm::starts_with(attrName, "cat:")) {
            text << commentAttr(attrName, value, translator);
        }
    }

    text << nproLink();

    return text.str();
}

std::string OutsourceRegion::comment(
    revision::DBID commitId,
    const revision::AttributesDiff& attrsDiff,
    const std::string& login,
    const Translator& translator) const
{
    std::stringstream text;
    text << commentHeader(commitId, login);

    for (const auto& kvp : attrsDiff) {
        const auto& attrName = kvp.first;
        const auto& valueAfter = kvp.second.after;

        if (!boost::algorithm::starts_with(attrName, "cat:")) {
            text << commentAttr(attrName, valueAfter, translator);
        }
    }

    text << nproLink();

    return text.str();
}

std::string OutsourceRegion::commentAboutDeletion(
    revision::DBID commitId,
    const std::string& login) const
{
    std::stringstream text;
    text << commentHeader(commitId, login)
        << "Зона аутсорса удалена\n"
        << nproLink();

    return text.str();
}

std::string OutsourceRegion::nproLink() const
{
    if (nproHost_.empty()) {
        return {};
    }

    if (geometry_.isNull()) {
        WARN() << "Region " << objectId_ << " has null geometry";
        return {};
    }

    const auto* envelope = geometry_->getEnvelopeInternal();

    geos::geom::Coordinate center;
    if (!envelope->centre(center)) {
        WARN() << "Failed to compute center for region " << objectId_;
        return {};
    }
    auto geoCenter = common::mercatorToGeodetic(center.x, center.y);

    return (boost::format(LINK_TEMPLATE) % nproHost_ % geoCenter.x() % geoCenter.y() % objectId_).str();
}

common::TskvMessage OutsourceRegion::logMessage(
    const revision::Commit& commit,
    rev::UserID userId) const
{
    common::TskvMessage message(ACTIONS_LOG_FORMAT);
    message.setParam("object_id", objectId_);
    message.setParam("commit_id", commit.id());
    message.setParam("puid", userId);
    message.setParam(
        "unixtime",
        chrono::convertToUnixTime(commit.createdAtTimePoint())
    );

    for (const auto& attrName : ATTRIBUTES_TO_LOG) {
        auto it = attributes_.find(ATTR_PREFIX + attrName);
        message.setParam(attrName, it != attributes_.end()
            ? it->second
            : std::string{});
    }
    return message;
}

} // namespace outsource_regions
} // namespace tasks
} // namespace wiki
} // namespace maps
