#include "utils.h"

#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/configs/editor/exception.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/codepage/include/codepage.h>

#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/regex.hpp>

#include <util/system/compiler.h>

#define ATTR_REQUIRE(cond, msg) \
    if (Y_LIKELY(cond)) {} else \
        throw ::maps::wiki::configs::editor::WrongAttributeValueError() << msg;

namespace maps::wiki::configs::editor {

namespace {

const std::string s_emptyString;

const std::map<std::string, ValueType> mapTypeNameToType
{
    { "boolean", ValueType::Boolean },
    { "integer", ValueType::Integer },
    { "float", ValueType::Float },
    { "bitmask", ValueType::Bitmask },
    { "string", ValueType::String },
    { "enum", ValueType::Enum },
    { "table", ValueType::Table },
    { "json", ValueType::Json},
    { "login", ValueType::Login},
};

const std::set<std::string> ALLOWED_TRUE_VALUES
{
    TRUE_VALUE,
    "true",
    "TRUE",
    "True"
};

const std::set<std::string> ALLOWED_FALSE_VALUES
{
    FALSE_VALUE,
    "false",
    "FALSE",
    "False",
    "0"
};

ValueType parseValueType(const std::string& valueTypeStr)
{
    auto it = mapTypeNameToType.find(valueTypeStr);
    REQUIRE(it != mapTypeNameToType.end(), "Invalid attribute type specified.");
    return it->second;
}

} // namespace

const char AttributeDef::MULTIVALUE_DELIMITER = '|';

AttributeDef::Directional::Directional(const maps::xml3::Node& valuesNode)
{
    auto valueNodes = valuesNode.nodes("value", true);
    for (size_t i = 0; i < valueNodes.size(); ++i) {
        const maps::xml3::Node& valNode = valueNodes[i];
        const std::string value = valNode.value<std::string>();
        const std::string direction =
            valNode.attr<std::string>("direction");
        if (direction == "both") {
            bothValue = value;
        } else if (direction == "from") {
            fromValue = value;
        } else if (direction == "to") {
            toValue = value;
        }
    }
    REQUIRE(bothValue != fromValue
        && bothValue != toValue
        && toValue != fromValue, "Directional attribute direction values should be unique");
}

//================================================================

class AttributeDef::Impl
{
public:
    Impl(std::string id
        , std::string defaultValue
        , const StringSet& allowedValues)
        : id_(std::move(id))
        , valueType_(allowedValues.empty() ? ValueType::String : ValueType::Enum)
        , multiValue_(false)
        , objectAccessControl_(false)
        , mergeable_(false)
        , copyable_(true)
        , junctionSpecific_(false)
        , system_(true)
        , minValues_(0)
        , maxValues_(0)
        , uniqueRows_(false)
        , minLen_(0)
        , maxLen_(256)
        , defaultValue_(std::move(defaultValue))
        , required_(false)
    {
        for (const auto& allowedValue : allowedValues) {
            values_.emplace_back(allowedValue, s_emptyString);
        }
        initAutoValue();
    }

    explicit Impl(const maps::xml3::Node& node)
        : id_(node.attr<std::string>("id"))
        , valueType_(parseValueType(node.attr<std::string>("type")))
        , multiValue_(node.attr<bool>("multi-value", false))
        , objectAccessControl_(node.attr<bool>("object-access-control", false))
        , mergeable_(node.attr<bool>("mergeable", false))
        , copyable_(node.attr<bool>("copyable", true))
        , junctionSpecific_(node.attr<bool>("junction-specific", false))
        , junctionSpecificPolicy_(node.attr<std::string>("junction-specific-policy", s_emptyString))
        , junctionSpecificPolicyAttribute_(node.attr<std::string>("junction-specific-policy-attribute", s_emptyString))
        , system_(node.attr<bool>("system", false))
        , minValues_(0)
        , maxValues_(0)
        , uniqueRows_(node.attr<bool>("unique-rows", false))
        , minLen_(node.attr<size_t>("min-len", 0))
        , maxLen_(node.attr<size_t>("max-len", 256))
        , rowObjCategory_(valueType_ == ValueType::Table
                            ? node.attr<std::string>("row-obj-category")
                            : s_emptyString)
        , rowObjRole_(node.attr<std::string>("row-obj-role", s_emptyString))
        , roleAttr_(node.attr<std::string>("role-attribute", s_emptyString))
        , requiredAttributeId_(node.attr<std::string>("requires", s_emptyString))
        , required_(node.attr<bool>("required", false))
    {
        init(node);

        REQUIRE(junctionSpecificPolicyAttribute_.empty() || junctionSpecificPolicy_.empty(),
            "Only one junctionSpecificPolicy source can be set: " << id_);
        const auto regexpStr = node.attr<std::string>("regexp", s_emptyString);
        if (!regexpStr.empty()) {
            regexp_ = boost::regex(regexpStr);
            REQUIRE(defaultValue().empty() || boost::regex_match(defaultValue(), *regexp_),
                "Default value [" << defaultValue() << "] doesn't match regexp ["
                    << regexpStr << "] provided for attribute: " << id_);
        }
        if (!anyValue() && defaultValue().empty() && !multiValue_ &&
            std::find(values_.begin(), values_.end(), s_emptyString) == values_.end()) {

            throw maps::Exception() << "Forbidden empty default value for attribute " << id_;
        }
        initAutoValue();
    }

    void init(const maps::xml3::Node& node)
    {
        maps::xml3::Node tipsNode = node.node("view-hint", true);
        if (!tipsNode.isNull()) {
            tips_ = tipsNode.value<std::string>();
        }
        maps::xml3::Node valuesNode = node.node("values", true);
        if (!valuesNode.isNull()) {
            minValues_ = valuesNode.attr<size_t>("min", 0);
            maxValues_ = valuesNode.attr<size_t>("max", 0);
            if (valuesNode.attr<bool>("directional", false)) {
                directional_ = Directional(valuesNode);
            }
            defaultValue_ = valuesNode.attr<std::string>("default", "");
            maps::xml3::Nodes valueNodes = valuesNode.nodes("value", true);
            for (size_t i = 0; i < valueNodes.size(); ++i) {
                const maps::xml3::Node& valNode = valueNodes[i];
                const std::string value = valNode.value<std::string>();
                const std::string label = valNode.attr<std::string>("label", "");
                values_.emplace_back(value, label);
            }
        }
        maps::xml3::Node labelNode = node.node("label", true);
        if (!labelNode.isNull()) {
            label_ = labelNode.value<std::string>();
        }
        if (junctionSpecific_) {
            idA_ = node.attr<std::string>("id-A");
            idB_ = node.attr<std::string>("id-B");
        }
        if (valueType_ == ValueType::Table) {
            maps::xml3::Nodes columns = node.nodes("values/value/attribute");
            for (size_t i = 0; i < columns.size(); ++i) {
                const auto id = columns[i].attr<std::string>("id");
                bool isKeyColumn = columns[i].attr<bool>("key", true);
                columns_.insert(id);
                if (isKeyColumn) {
                    keyColumns_.insert(id);
                }
            }
        }
    }

    void initAutoValue()
    {
        try {
            autoValue_ = allowedValue({});
        } catch (...) {
            autoValue_ = defaultValue();
        }
    }

    bool anyValue() const
    {
        return valueType_ != ValueType::Enum;
    }

    const std::string& defaultValue() const
    {
        return !defaultValue_.empty()
            ? defaultValue_
            : (!values_.empty() && !multiValue_
                ? values_.front().value
                : s_emptyString);
    }

    std::string allowedValue(const std::string& originalValue) const
    {
        std::string resultValue = boost::trim_copy(originalValue);
        deduplicatePunctuation(resultValue);
        resultValue = trimLeadingUTF8BOM(resultValue);
        if (maxLen_) {
            auto u32value = codepage::convertToUtf32(codepage::u8string_view(resultValue));
            if (u32value.size() > maxLen_) {
                codepage::u8string u8value;
                std::u32string_view u32view(u32value);
                codepage::convert(u32view.substr(0, maxLen_), u8value);
                resultValue = std::move(u8value.value());
            }
        }
        ATTR_REQUIRE(minLen_ <= resultValue.length(),
            "Value :[" << resultValue << "] for attribute: " << id_
            << " should be at least "
            << minLen_ << " characters long.");
        if (valueType_ == ValueType::Enum) {
            if (resultValue.empty() &&
                !multiValue_ &&
                std::find(values_.begin(), values_.end(), s_emptyString) == values_.end())
            {
                resultValue = defaultValue();
            }
            ATTR_REQUIRE(
                (multiValue_ && resultValue.empty() && !required_) ||
                std::find(values_.begin(), values_.end(), resultValue) != values_.end()
                || resultValue == defaultValue(),
                "Value not allowed:[" << resultValue << "] for attribute: " << id_);
        }
        if (multiValue_ && (minValues_ || maxValues_)) {
            const auto numValues = unpackMultiValue(originalValue).size();
            ATTR_REQUIRE(
                (!minValues_ || numValues >= minValues_) &&
                (!maxValues_ || numValues <= maxValues_),
                "Value not allowed:[" << resultValue << "] for attribute: " << id_);
        }
        try {
            if (regexp_ &&
                (resultValue != defaultValue() || required_)) {
                ATTR_REQUIRE(boost::regex_match(resultValue, *regexp_),
                    "Value :[" << resultValue << "] for attribute: " << id_
                    << " didn't match regexp pattern");
            }
            if (valueType_ == ValueType::Float) {
                ATTR_REQUIRE(!required_ || !resultValue.empty(),
                    "Attribute: " << id_ << " value required");
                if (!resultValue.empty()) {
                    auto doubleValue = boost::lexical_cast<double>(resultValue);
                    ATTR_REQUIRE(!required_ || doubleValue != 0.0 || resultValue != "0",
                        "Attribute: " << id_ << " value required");
                }
            }
            if (valueType_ == ValueType::Integer) {
                ATTR_REQUIRE(!required_ || !resultValue.empty(),
                    "Attribute: " << id_ << " value required");
                if (!resultValue.empty()) {
                    auto longintValue = boost::lexical_cast<long int>(resultValue);
                    ATTR_REQUIRE(!required_ || longintValue != 0 || resultValue != "0",
                        "Attribute: " << id_ << " value required");
                }
            }
            if (valueType_ == ValueType::Bitmask && !resultValue.empty()) {
                auto bitmaskValue = boost::lexical_cast<size_t>(resultValue);
                ATTR_REQUIRE(!required_ || bitmaskValue != 0x0,
                    "Attribute: " << id_ << " value required");
            }
            if (valueType_ == ValueType::Boolean) {
                if (ALLOWED_TRUE_VALUES.count(resultValue)) {
                    resultValue = TRUE_VALUE;
                } else if (ALLOWED_FALSE_VALUES.count(resultValue)){
                    resultValue = FALSE_VALUE;
                } else {
                    throw boost::bad_lexical_cast();
                }
            }
        } catch (const boost::bad_lexical_cast& ex) {
            throw WrongAttributeValueError()
                << "Value can't be expressed:["
                << resultValue << "] for attribute: " << id_;
        }
        return resultValue;
    }

    std::string id_;
    std::string label_;
    ValueType valueType_;
    bool multiValue_;
    bool objectAccessControl_;
    bool mergeable_;
    bool copyable_;
    bool junctionSpecific_;
    std::string junctionSpecificPolicy_;
    std::string junctionSpecificPolicyAttribute_;
    bool system_;
    size_t minValues_;
    size_t maxValues_;
    bool  uniqueRows_;
    std::string tips_;
    ValueList values_;
    size_t minLen_;
    size_t maxLen_;
    std::string rowObjCategory_;
    std::string rowObjRole_;
    std::string roleAttr_;
    std::string group_;
    std::string idA_;
    std::string idB_;
    std::string enableCondition_;
    StringSet columns_;
    StringSet keyColumns_;
    std::string defaultValue_;
    std::optional<Directional> directional_;
    std::string requiredAttributeId_;
    bool required_;
    std::string autoValue_;
    std::optional<boost::regex> regexp_;
};

MOVABLE_PIMPL_DEFINITIONS(AttributeDef)

AttributeDef::AttributeDef(std::string id, std::string defaultValue, const StringSet& allowedValues)
    : impl_(new Impl{std::move(id), std::move(defaultValue), allowedValues})
{}

AttributeDef::AttributeDef(const maps::xml3::Node& node)
    : impl_(new Impl{node})
{}

const std::string& AttributeDef::id() const { return impl_->id_; }

const std::string& AttributeDef::idA() const
{
    assert(impl_->idA_.length());
    return impl_->idA_;
}

const std::string& AttributeDef::idB() const
{
    assert(impl_->idB_.length());
    return impl_->idB_;
}

bool AttributeDef::isA() const { return id() == idA(); }

bool AttributeDef::isB() const { return id() == idB(); }

const std::string& AttributeDef::label() const { return impl_->label_; }

ValueType AttributeDef::valueType() const { return impl_->valueType_; }

bool AttributeDef::anyValue() const { return impl_->anyValue(); }

bool AttributeDef::booleanValue() const { return impl_->valueType_ == ValueType::Boolean; }

bool AttributeDef::objectAccessControl() const { return impl_->objectAccessControl_; }

bool AttributeDef::mergeable() const { return impl_->mergeable_; }
bool AttributeDef::copyable() const { return impl_->copyable_; }

bool AttributeDef::multiValue() const { return impl_->multiValue_; }

size_t AttributeDef::minValues() const { return impl_->minValues_; }
size_t AttributeDef::maxValues() const { return impl_->maxValues_; }

StringVector AttributeDef::unpackMultiValue(const std::string& packed)
{ return common::split(packed, s_emptyString + MULTIVALUE_DELIMITER); }

bool AttributeDef::junctionSpecific() const
{ return impl_->junctionSpecific_; }

const std::string& AttributeDef::junctionSpecificPolicy() const
{ return impl_->junctionSpecificPolicy_; }

const std::string& AttributeDef::junctionSpecificPolicyAttribute() const
{ return impl_->junctionSpecificPolicyAttribute_; }

bool AttributeDef::system() const { return impl_->system_; }

const std::string& AttributeDef::tips() const { return impl_->tips_; }

const ValueList& AttributeDef::values() const { return impl_->values_; }

size_t AttributeDef::minLen() const { return impl_->minLen_; }

size_t AttributeDef::maxLen() const { return impl_->maxLen_; }

bool AttributeDef::table() const { return impl_->valueType_ == ValueType::Table; }

bool AttributeDef::uniqueRows() const { return impl_->uniqueRows_; }

const std::string& AttributeDef::rowObjCategory() const { return impl_->rowObjCategory_; }

const std::string& AttributeDef::rowObjRole() const { return impl_->rowObjRole_; }

const std::string& AttributeDef::roleAttr() const { return impl_->roleAttr_; }

const std::string& AttributeDef::defaultValue() const { return impl_->defaultValue(); }

const std::string& AttributeDef::autoValue() const { return impl_->autoValue_; }

const StringSet& AttributeDef::columns() const { return impl_->columns_; }
const StringSet& AttributeDef::keyColumns() const { return impl_->keyColumns_; }

const std::optional<AttributeDef::Directional>& AttributeDef::directional() const
{ return impl_->directional_; }

const std::string& AttributeDef::requiredAttributeId() const { return impl_->requiredAttributeId_; }

bool AttributeDef::required() const { return impl_->required_; }

std::string AttributeDef::allowedValue(const std::string& originalValue) const
{
    return impl_->allowedValue(originalValue);
}

} // namespace maps::wiki::configs::editor
