#include "export_params_impl.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/json/include/value.h>

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

namespace maps::wiki::revisionapi {

namespace {

const size_t CATEGORIES_FOR_FILTER_LIMIT = 31;

const std::string ATTRIBUTES_KEY = "attributes";
const std::string CATEGORIES_KEY = "categories";
const std::string CATEGORY_KEY = "category";
const std::string CATEGORY_PREFIX = "cat:";
const std::string INVERT_DIRECTION_FOR_ROLES_KEY
    = "invert_direction_for_roles";
const std::string RELATION_EXPORT_MODE_KEY = "relations_export_mode";
const std::string MASTER_TO_SLAVE_VALUE = "Master ==> Slave";
const std::string SLAVE_TO_MASTER_VALUE = "Master <== Slave";

Strings getCategories(const json::Value& cfg)
{
    Strings result;
    for (const auto& val : cfg[CATEGORIES_KEY]) {
        result.push_back(val[CATEGORY_KEY].toString());
    }
    return result;
}

RelationsExportFlags getRelationsExportFlags(const json::Value& cfg)
{
    auto mode = cfg[ATTRIBUTES_KEY][RELATION_EXPORT_MODE_KEY].toString();
    if (mode == MASTER_TO_SLAVE_VALUE) {
        return RelationsExportFlags::MasterToSlave;
    } else if (mode == SLAVE_TO_MASTER_VALUE) {
        return RelationsExportFlags::SlaveToMaster;
    } else {
        throw maps::Exception{} << RELATION_EXPORT_MODE_KEY
                                << " error: " << mode;
    }
}

Strings getInvertDirectionRoles(const json::Value& cfg)
{
    Strings result;
    auto roles
        = cfg[ATTRIBUTES_KEY][INVERT_DIRECTION_FOR_ROLES_KEY].toString();
    boost::split(result, roles, boost::is_any_of(","));
    return result;
}

} // anonymous namespace

std::ostream& operator << (std::ostream& os, RelationsExportFlags flags)
{
    if (flags == RelationsExportFlags::None) {
        os << "None";
        return os;
    }

    std::string result;
    if (!!(flags & RelationsExportFlags::MasterToSlave) ||
            !!(flags & RelationsExportFlags::SlaveToMaster)) {
        result += "Master ";
        if (!!(flags & RelationsExportFlags::SlaveToMaster)) {
            result += "<";
        }
        result += "==";
        if (!!(flags & RelationsExportFlags::MasterToSlave)) {
            result += ">";
        }
        result += " Slave";
    }

    if (!!(flags & RelationsExportFlags::SkipDangling)) {
        result += (result.empty() ? "" : ", ");
        result += "SkipDangling";
    }
    os << result;
    return os;
}


ExportParams::ExportParams(
        const revision::Branch& branch,
        const revision::DBID commitId,
        RelationsExportFlags flags,
        const Strings& invertDirectionRoles)
    : impl_(new Impl(branch, commitId, flags))
{
    if (!invertDirectionRoles.empty()) {
        auto directionFlags =
            impl_->flags & (RelationsExportFlags::MasterToSlave |
                            RelationsExportFlags::SlaveToMaster);

        REQUIRE(directionFlags == RelationsExportFlags::MasterToSlave ||
                    directionFlags == RelationsExportFlags::SlaveToMaster,
                "Cannot set invert direction roles for export mode: "
                    << impl_->flags);
        impl_->invertDirectionRoles = invertDirectionRoles;
    }
}

std::unique_ptr<ExportParams> ExportParams::loadFromFile(
        const revision::Branch& branch,
        const revision::DBID commitId,
        const std::string& jsonConfigPath)
{
    std::unique_ptr<ExportParams> result{new ExportParams{branch, commitId, {}}};
    auto cfg = json::Value::fromFile(jsonConfigPath);
    result->impl_->setCategoriesForFilter(getCategories(cfg));
    result->impl_->flags = getRelationsExportFlags(cfg);
    result->impl_->invertDirectionRoles = getInvertDirectionRoles(cfg);
    return result;
}

ExportParams::ExportParams(
        const revision::Branch& branch,
        const revision::DBID commitId,
        const Strings& categoriesForFilter,
        const Strings& allowedRelativesCategories)
    : impl_(new Impl(
        branch,
        commitId,
        RelationsExportFlags::MasterToSlave | RelationsExportFlags::SlaveToMaster))
{
    impl_->setCategoriesForFilter(categoriesForFilter);
    impl_->allowedRelativesCategories.insert(
        allowedRelativesCategories.begin(),
        allowedRelativesCategories.end());
}


ExportParams::~ExportParams() = default;

void ExportParams::setWriteBatchSize(size_t size)
{ impl_->writeBatchSize = size; }

void ExportParams::setEmptyJsonPolicy(EmptyJsonPolicy policy)
{
    impl_->emptyJsonPolicy = policy;
}

const revision::Branch& ExportParams::branch() const
{ return impl_->branch; }

revision::DBID ExportParams::commitId() const
{ return impl_->commitId; }

RelationsExportFlags ExportParams::flags() const
{ return impl_->flags; }

size_t ExportParams::writeBatchSize() const
{ return impl_->writeBatchSize; }

const FilterPtr& ExportParams::filter() const
{ return impl_->filter; }

const std::set<std::string>&
ExportParams::categories() const
{ return impl_->categories; }

const std::set<std::string>&
ExportParams::categoriesForFilter() const
{ return impl_->categoriesForFilter; }

const std::set<std::string>&
ExportParams::allowedRelativesCategories() const
{ return impl_->allowedRelativesCategories; }

const Strings&
ExportParams::invertDirectionRoles() const
{ return impl_->invertDirectionRoles; }

EmptyJsonPolicy
ExportParams::emptyJsonPolicy() const
{ return impl_->emptyJsonPolicy; }


ExportParams::Impl::Impl(
        revision::Branch branch,
        revision::DBID commitId,
        RelationsExportFlags flags)
    : branch(std::move(branch))
    , commitId(commitId)
    , flags(flags)
    , writeBatchSize(0)
    , emptyJsonPolicy(EmptyJsonPolicy::Export)
{ }

void ExportParams::Impl::setCategoriesForFilter(const Strings& inputCategories)
{
    categories.clear();
    categoriesForFilter.clear();

    for (const auto& category : inputCategories) {
        auto categoryName = (category.find(CATEGORY_PREFIX) == 0)
            ? category.substr(CATEGORY_PREFIX.length())
            : category;
        categoriesForFilter.insert(CATEGORY_PREFIX + categoryName);
        categories.insert(categoryName);
        allowedRelativesCategories.insert(categoryName);
    }

    if (categories.empty() || categories.size() > CATEGORIES_FOR_FILTER_LIMIT) {
        filter = FilterPtr{};
    } else {
        filter = std::make_shared<revision::filters::ProxyFilterExpr>(
            revision::filters::Attr::definedAny(categoriesForFilter));
    }
}

} // namespace maps::wiki::revisionapi
