#include "feed_component.h"

#include <yandex/maps/wiki/common/name_type.h>
#include <yandex/maps/wiki/revision/snapshot.h>
#include <yandex/maps/wiki/revision/diff.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/variant.h>

#include <boost/algorithm/string.hpp>

using namespace std::string_literals;

namespace maps::wiki::poi {
namespace {
namespace json {
const auto LON = "lon"s;
const auto LAT = "lat"s;
const auto NAMES = "names"s;
const auto LANG = "lang"s;
const auto NAME = "name"s;
const auto SHORT_NAMES = "short_names"s;
const auto SYNONYM_NAMES = "synonym_names"s;
const auto ACTUALIZATION_DATE = "actualization_date"s;
const auto COMMIT_ID = "commit_id"s;
}

const auto REL_ROLE_ATTR = "rel:role"s;
const auto LANG_ATTR_SUFFIX = ":lang"s;
const auto NAME_ATTR_SUFFIX = ":name"s;

geolib3::Point2 center(const geolib3::SimpleGeometryVariant& variant)
{
    return variant.visit(
        [](const auto& geometry) {
            return geometry.boundingBox().center();
        });
}

const std::set<std::string> NAME_ROLES
    {
        boost::lexical_cast<std::string>(common::NameType::Official),
        boost::lexical_cast<std::string>(common::NameType::RenderLabel),
        boost::lexical_cast<std::string>(common::NameType::AddressLabel),
        boost::lexical_cast<std::string>(common::NameType::Short),
        boost::lexical_cast<std::string>(common::NameType::Synonym),
        boost::lexical_cast<std::string>(common::NameType::Old),
    };

bool
hasNamesDiff(const CommitDiff& commitDiff, DBID objectId)
{
    for (const auto& [_, diff] : commitDiff) {
        const auto& diffData = diff.data();
        if (!diffData.newRelationData && !diffData.oldRelationData) {
            DEBUG() << "No relation data";
            continue;
        }
        if (!diffData.attributes) {
            DEBUG() << "No relation attributes";
            continue;
        }
        if (
            (diffData.newRelationData &&
                diffData.newRelationData->masterObjectId() == objectId) ||
            (diffData.oldRelationData &&
                diffData.oldRelationData->masterObjectId() == objectId))
        {
            DEBUG() << "Relation changed for:" << objectId;
            for (const auto& [_, valueDiff] : *diffData.attributes) {
                if (NAME_ROLES.count(valueDiff.after) || NAME_ROLES.count(valueDiff.before)) {
                    return true;
                }
            }
        }
    }
    return false;
}

bool
isDeletedByDiff(const CommitDiff& commitDiff, DBID objectId)
{
    auto it = commitDiff.find(objectId);
    if (it == commitDiff.end()) {
        return false;
    }
    const auto& diff = it->second;
    return diff.data().deleted && diff.data().deleted->after;
}

std::set<common::LocalizedString>
readNames(const maps::json::Value& array)
{
    std::set<common::LocalizedString> result;
    ASSERT(array.isArray());
    for (const auto& obj : array) {
        result.insert({obj[json::LANG].toString(), obj[json::NAME].toString()});
    }
    return result;
}

void
writeNames(maps::json::ArrayBuilder& namesBuilder, const std::set<common::LocalizedString>& names)
{
    for (const auto& name : names) {
        namesBuilder << [&](maps::json::ObjectBuilder nameBuilder) {
            nameBuilder[json::LANG] = name.lang;
            nameBuilder[json::NAME] = name.value;
        };
    }
}

common::LocalizedString
readLocalizedName(const ObjectRevision& objectRev)
{
    if (!objectRev.data().attributes) {
        return {};
    }
    std::string lang;
    std::string name;
    for (const auto& [attrId, value] : *objectRev.data().attributes) {
        if (boost::algorithm::ends_with(attrId, LANG_ATTR_SUFFIX)) {
            lang = value;
        }
        if (boost::algorithm::ends_with(attrId, NAME_ATTR_SUFFIX)) {
            name = value;
        }
    }
    return {lang, name};
}

} // namespace

bool
FeedComponentBase::updateFromCommit(
        const Branch& branch,
        pqxx::transaction_base& coreTxn,
        const revision::Commit& commit,
        DBID objectId)
{
    if (commit.id() <= commitId_) {
        return true;
    }
    bool hasUpdate = processCommit(branch, coreTxn, commit, objectId);
    if (hasUpdate) {
        commitId_ = commit.id();
        actualizationDate_ = commit.createdAtTimePoint();
    }
    return hasUpdate;
}

void
FeedComponentBase::readJson(const maps::json::Value& value)
{
    fromJson(value);
    commitId_ = boost::lexical_cast<DBID>(value[json::COMMIT_ID].toString());
    actualizationDate_ = chrono::parseIsoDateTime(value[json::ACTUALIZATION_DATE].toString());
}

void
FeedComponentBase::updateJson(maps::json::ObjectBuilder& object) const
{
    toJson(object);
    object[json::COMMIT_ID] = std::to_string(commitId_);
    object[json::ACTUALIZATION_DATE] = chrono::formatIsoDateTime(actualizationDate_);
}

bool
LonLatFeedComponent::processCommit(
        const Branch&,
        pqxx::transaction_base& coreTxn,
        const revision::Commit& commit,
        DBID objectId)
{
    const auto diff = revision::commitDiff(coreTxn, commit.id(), objectId);
    if (isDeletedByDiff(diff, objectId)) {
        return false;
    }
    auto objectDiffIt = diff.find(objectId);
    if (objectDiffIt == diff.end() ||
        !objectDiffIt->second.data().geometry) {
        return false;
    }
    try {
        const auto& geomDiff = *objectDiffIt->second.data().geometry;
        if (geomDiff.after.empty()) {
            return false;
        }
        const auto geometryVariant = geolib3::WKB::read<geolib3::SimpleGeometryVariant>(geomDiff.after);
        geolib3::Point2 lonLat = center(geolib3::convertMercatorToGeodetic(geometryVariant));
        lon_ = lonLat.x();
        lat_ = lonLat.y();
    } catch (...) {
       ERROR() << "LonLatFeedComponent::processCommit  failed  oid " << objectId << " commitId " << commit.id();
       throw;
    }
    return true;
}

void
LonLatFeedComponent::fromJson(const maps::json::Value& lonlat)
{
    lon_ = lonlat[json::LON].as<double>();
    lat_ = lonlat[json::LAT].as<double>();
}

void
LonLatFeedComponent::toJson(maps::json::ObjectBuilder& lonlat) const
{
    lonlat[json::LON] = lon_;
    lonlat[json::LAT] = lat_;
}

namespace {
typedef std::map<DBID, revision::ObjectRevision> RevisionsMap;

RevisionsMap
loadNamesRevisions(revision::Snapshot& snapshot, const DBIDSet& ids)
{
    auto nameRevs = snapshot.objectRevisions(ids);
    if (ids.size() == nameRevs.size()) {
        return nameRevs;
    }
    DBIDSet lostIds;
    for (auto id : ids) {
        if (!nameRevs.count(id)) {
            lostIds.insert(id);
        }
    }
    if (lostIds.empty()) {
        return nameRevs;
    }
    auto reader = snapshot.reader();
    auto revsFilter =
        revision::filters::ObjRevAttr::objectId().in(lostIds) &&
        revision::filters::ObjRevAttr::isNotRelation() &&
        revision::filters::ObjRevAttr::isNotDeleted() &&
        !revision::filters::Geom::defined();
    const auto lostRevisions = reader.loadRevisions(revsFilter);
    for (const auto& rev : lostRevisions) {
        nameRevs.emplace(rev.id().objectId(), rev);
    }
    return nameRevs;
}
} // namespace

bool
NamesFeedComponent::processCommit(
        const Branch& branch,
        pqxx::transaction_base& coreTxn,
        const revision::Commit& commit,
        DBID objectId)
{
    const auto diff = revision::commitDiff(coreTxn, commit.id());
    if (isDeletedByDiff(diff, objectId)) {
        DEBUG() << "NamesFeedComponent::processCommit deleted object commit:" << commit.id();
        return false;
    }
    if (!hasNamesDiff(diff, objectId)) {
        DEBUG() << "NamesFeedComponent::processCommit no name diff commit:" << commit.id();
        return false;
    }
    names_.clear();
    synonymNames_.clear();
    shortNames_.clear();

    revision::RevisionsGateway gateway(coreTxn, branch);
    auto snapshot = gateway.stableSnapshot(commit.id());
    auto slaveRelations = snapshot.loadSlaveRelations(objectId);
    std::multimap<DBID, common::NameType> namesIdsWithType;
    DBIDSet nameIds;
    for (const auto& slaveRelation : slaveRelations) {
        const auto& data = slaveRelation.data();
        ASSERT(data.relationData);
        ASSERT(data.attributes);
        auto it = data.attributes->find(REL_ROLE_ATTR);
        if (it == data.attributes->end() || !NAME_ROLES.count(it->second)) {
            continue;
        }
        const auto nameId = data.relationData->slaveObjectId();
        namesIdsWithType.emplace(nameId, boost::lexical_cast<common::NameType>(it->second));
        nameIds.insert(nameId);
    }
    if (nameIds.empty()) {
        return true;
    }
    const auto nameRevisions = loadNamesRevisions(snapshot, nameIds);
    for (const auto& [nameId, nameRevision] : nameRevisions) {
        const auto itRange = namesIdsWithType.equal_range(nameId);
        for (auto it = itRange.first; it != itRange.second; ++it) {
            switch (it->second) {
                case common::NameType::Official:
                    names_.insert(readLocalizedName(nameRevision));
                    break;
                case common::NameType::RenderLabel:
                    shortNames_.insert(readLocalizedName(nameRevision));
                    break;
                default:
                    synonymNames_.insert(readLocalizedName(nameRevision));
                    break;
            }
        }
    }
    return true;
}

void
NamesFeedComponent::fromJson(const maps::json::Value& obj)
{
    names_ = readNames(obj[json::NAMES]);
    shortNames_ = readNames(obj[json::SHORT_NAMES]);
    synonymNames_ = readNames(obj[json::SYNONYM_NAMES]);
}

void
NamesFeedComponent::toJson(maps::json::ObjectBuilder& objectBuilder) const
{
    objectBuilder[json::NAMES] = [&](maps::json::ArrayBuilder namesBuilder) {
        writeNames(namesBuilder, names_);
    };
    objectBuilder[json::SHORT_NAMES] = [&](maps::json::ArrayBuilder namesBuilder) {
        writeNames(namesBuilder, shortNames_);
    };
    objectBuilder[json::SYNONYM_NAMES] = [&](maps::json::ArrayBuilder namesBuilder) {
        writeNames(namesBuilder, synonymNames_);
    };
}

} // namespace maps::wiki::poi
