#include "get_branch_diff.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h"
#include "maps/wikimap/mapspro/services/editor/src/views/objects_query.h"
#include "maps/wikimap/mapspro/services/editor/src/sync/db_helpers.h"

#include <maps/wikimap/mapspro/libs/views/include/query_builder.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/string_utils.h>

namespace maps {
namespace wiki {

namespace {

const std::string ID_COL = "id";
const std::string COMMIT_ID_COL = "commit_id";
const std::string THE_GEOM_COL = "the_geom";

const CachePolicy CACHE_POLICY {
    TableAttributesLoadPolicy::Load,
    ServiceAttributesLoadPolicy::Skip, // default: Load
    DanglingRelationsPolicy::Ignore // default: Check
};

void
fillAttributesDiff(DiffDetails& details)
{
    for (const auto& attr : details.anyObject()->attributes()) {
        if (attr.def().system()) {
            continue;
        }
        auto extractVal = [&](const ObjectPtr& obj) -> std::string
        {
            if (!obj) {
                return {};
            }
            auto it = obj->attributes().find(attr.id());
            if (it != obj->attributes().end()) {
                return it->packedValues();
            }
            return {};
        };
        auto fromVal = extractVal(details.fromObjectVersion);
        auto toVal = extractVal(details.toObjectVersion);
        if (fromVal != toVal) {
            details.attributesDiff.emplace(
                attr.id(), revision::StringDiff(fromVal, toVal));
        }
    }
}

void
fillRelativesDiff(DiffDetails& details, ObjectsCache& fromCache, ObjectsCache& toCache)
{
    const auto& fromSnapshot = fromCache.revisionsFacade().snapshot();
    const auto& toSnapshot = toCache.revisionsFacade().snapshot();

    const auto& category = details.anyObject()->category();
    auto objectId = details.anyObject()->id();

    TOIds addedRelativeIds;
    TOIds removedRelativeIds;
    auto fromMasters = fromSnapshot.loadMasterRelations(objectId);
    auto toMasters  = toSnapshot.loadMasterRelations(objectId);
    details.mastersDiff = RevisionsFacade::relationsRevisionsToRoleRelDiff(
            RelationType::Master, toMasters, fromMasters);
    for (const auto& pair : details.mastersDiff) {
        const auto& roleDiff = pair.second;
        addedRelativeIds.insert(roleDiff.added.begin(), roleDiff.added.end());
        removedRelativeIds.insert(roleDiff.deleted.begin(), roleDiff.deleted.end());
    }

    if (category.showSlavesDiff()) {
        auto fromSlaves = fromSnapshot.loadSlaveRelations(objectId);
        auto toSlaves  = toSnapshot.loadSlaveRelations(objectId);
        details.slavesDiff = RevisionsFacade::relationsRevisionsToRoleRelDiff(
                RelationType::Slave, toSlaves, fromSlaves);
        for (const auto& pair : details.slavesDiff) {
            const auto& roleDiff = pair.second;
            addedRelativeIds.insert(roleDiff.added.begin(), roleDiff.added.end());
            removedRelativeIds.insert(
                    roleDiff.deleted.begin(), roleDiff.deleted.end());
        }
    }
    const auto& slaveRoles = category.slaveRoleIds(roles::filters::IsNotTable);
    for (auto it = details.slavesDiff.begin(); it != details.slavesDiff.end();) {
        if (!slaveRoles.count(it->first)) {
            it = details.slavesDiff.erase(it);
        } else {
            ++it;
        }
    }

    auto loadRelativesFromRevision = [&](
        const TOIds& relativesIds,
        ObjectsCache& cache,
        std::map<TOid, views::ViewObject>& relatives)
    {
        if (relativesIds.empty()) {
            return;
        }

        auto notInViewObjects = cache.get(relativesIds);
        for (const auto& obj : notInViewObjects) {
            relatives.insert({obj->id(), views::ViewObject(obj)});
        }
    };

    auto loadRelatives = [&](
            TOIds relativesIds,
            ObjectsCache& cache,
            std::map<TOid, views::ViewObject>& relatives)
    {
        if (relativesIds.empty()) {
            return;
        }

        if (cache.branchContext().branch.state() == revision::BranchState::Unavailable) {
            loadRelativesFromRevision(relativesIds, cache, relatives);
            return;
        }

        views::ObjectsQuery objectsQuery;
        objectsQuery.addCondition(views::OidsCondition(relativesIds));
        auto objectsInView = objectsQuery.exec(
            cache.workView(), cache.branchContext().branch.id());
        for (const auto& obj : objectsInView) {
            relatives.insert({obj.id(), obj});
            relativesIds.erase(obj.id());
        }
        loadRelativesFromRevision(relativesIds, cache, relatives);
    };
    loadRelatives(std::move(addedRelativeIds), toCache, details.addedRelatives);
    loadRelatives(std::move(removedRelativeIds), fromCache, details.removedRelatives);
}

class GeomPartsProvider
{
public:
    explicit GeomPartsProvider(ObjectsCache& cache)
        : cache_(cache)
        , branchId_(cache_.branchContext().branch.id())
    {
    }

    virtual ~GeomPartsProvider() = default;

    virtual RevisionIds geomPartIds(const ObjectPtr& obj) = 0;
    virtual std::map<TOid, Geom> geomPartGeoms(const std::vector<TRevisionId>& revIds) = 0;

protected:
    ObjectsCache& cache_;
    TBranchId branchId_;
};

class GeomPartsProviderFromRevision : public GeomPartsProvider
{
public:
    explicit GeomPartsProviderFromRevision(ObjectsCache& cache)
        : GeomPartsProvider(cache)
    {
    }

    RevisionIds geomPartIds(const ObjectPtr& obj) override;
    std::map<TOid, Geom> geomPartGeoms(const std::vector<TRevisionId>& revIds) override;
};

class GeomPartsProviderFromView : public GeomPartsProvider
{
public:
    explicit GeomPartsProviderFromView(ObjectsCache& cache)
        : GeomPartsProvider(cache)
    {
    }

    RevisionIds geomPartIds(const ObjectPtr& obj) override;
    std::map<TOid, Geom> geomPartGeoms(const std::vector<TRevisionId>& revIds) override;
};

RevisionIds GeomPartsProviderFromRevision::geomPartIds(const ObjectPtr& obj)
{
    RevisionIds ret;
    if (!obj || obj->isDeleted()) {
        return ret;
    }

    const auto& category = obj->category();
    if (isDirectGeomMasterCategory(category)) {
        auto geomPartRoles = obj->category().slaveRoleIds(roles::filters::IsGeom);
        if (geomPartRoles.empty()) {
            return ret;
        }
        for (const auto& rel : obj->slaveRelations().range(geomPartRoles)) {
            ret.insert(rel.relative()->revision());
        }
    }

    if (isIndirectGeomMasterCategory(category)) {
        const auto& contourObjectDef = cfg()->editor()->contourObjectsDefs()
            .contourDef(category.id());
        const auto& contourRole = contourObjectDef.contour.roleId;
        const auto& elementRole = contourObjectDef.contour.linearElement.roleId;

        for (const auto& rel : obj->slaveRelations().range(contourRole)) {
            const auto* contour = rel.relative();
            for (const auto& rel : contour->slaveRelations().range(elementRole)) {
                ret.insert(rel.relative()->revision());
            }
        }
    }

    return ret;
}

std::map<TOid, Geom> GeomPartsProviderFromRevision::geomPartGeoms(const std::vector<TRevisionId>& revIds)
{
    std::map<TOid, Geom> geoms;
    if (revIds.empty()) {
        return geoms;
    }

    TOIds oids;
    for (const auto& revId : revIds) {
        oids.insert(revId.objectId());
    }

    for (const auto& object : cache_.get(oids)) {
        geoms.emplace(object->id(), object->geom());
    }
    return geoms;
}

RevisionIds GeomPartsProviderFromView::geomPartIds(const ObjectPtr& obj)
{
    RevisionIds ret;
    if (!obj || obj->isDeleted()) {
        return ret;
    }

    auto& txn = cache_.workView();
    auto loadRevisionIds = [&](const std::string& query)
    {
        for (const auto& row : txn.exec(query)) {
            ret.insert(TRevisionId(
                    row[ID_COL].as<revision::DBID>(),
                    row[COMMIT_ID_COL].as<revision::DBID>()));
        }
    };

    const auto& category = obj->category();
    if (isDirectGeomMasterCategory(category)) {
        auto geomPartRoles = category.slaveRoleIds(roles::filters::IsGeom);
        if (geomPartRoles.empty()) {
            return ret;
        }

        const auto& categories = cfg()->editor()->categories();

        StringSet slaveCategories;
        StringSet geomRoles;
        for (const auto& roleId : geomPartRoles) {
            const auto& categoryId = category.slaveRole(roleId).categoryId();
            if (!categories[categoryId].complex()) {
                slaveCategories.insert(categoryId);
                geomRoles.insert(roleId);
            }
        }

        if (!slaveCategories.empty()) {
            views::QueryBuilder qb(branchId_);
            qb.selectFields("g.id, g.commit_id");
            qb.fromTable(views::TABLE_OBJECTS_R, "r");
            qb.fromTable(bestViewTable(slaveCategories), "g");
            qb.whereClause(
                " r.slave_id = g.id "
                " AND r.domain_attrs->'rel:role' IN (" +
                    common::join(
                        geomRoles, [&](const std::string& role) { return txn.quote(role); }, ',') +
                    ") "
                " AND r.master_id = " + std::to_string(obj->id()));
            loadRevisionIds(qb.query());
        }
    }

    if (isIndirectGeomMasterCategory(category)) {
        const auto& contourObjectDef = cfg()->editor()->contourObjectsDefs()
            .contourDef(category.id());
        const auto contourRole = contourObjectDef.contour.roleId;
        const auto elementRole = contourObjectDef.contour.linearElement.roleId;

        views::QueryBuilder qb(branchId_);
        qb.selectFields("l.id, l.commit_id");
        qb.fromTable(views::TABLE_OBJECTS_R, "r", "r_next");
        qb.fromTable(views::TABLE_OBJECTS_L, "l");
        qb.whereClause(
            " r.slave_id = r_next.master_id AND r_next.slave_id = l.id "
            " AND r.domain_attrs->'rel:role'=" + txn.quote(contourRole) +
            " AND r_next.domain_attrs->'rel:role'=" + txn.quote(elementRole) +
            " AND r.master_id = " + std::to_string(obj->id()));
        loadRevisionIds(qb.query());
    }

    return ret;
}

std::map<TOid, Geom> GeomPartsProviderFromView::geomPartGeoms(const std::vector<TRevisionId>& revIds)
{
    std::map<TOid, Geom> geoms;
    if (revIds.empty()) {
        return geoms;
    }

    TOIds oids;
    for (const auto& revId : revIds) {
        oids.insert(revId.objectId());
    }

    views::QueryBuilder qb(branchId_);
    qb.selectFields(ID_COL + ", ST_ASBinary(the_geom) as " + THE_GEOM_COL);
    qb.fromTable(views::TABLE_OBJECTS_G, "g");
    qb.whereClause(common::whereClause(ID_COL, oids));

    for (const auto& row : cache_.workView().exec(qb.query())) {
        Geom geom(pqxx::binarystring(row[THE_GEOM_COL]).str());
        geoms.emplace(row[ID_COL].as<TOid>(), std::move(geom));
    }
    return geoms;
}

std::unique_ptr<GeomPartsProvider> makeGeomPartsProvider(ObjectsCache& cache)
{
    if (cache.branchContext().branch.state() == revision::BranchState::Unavailable) {
        return std::unique_ptr<GeomPartsProvider>(new GeomPartsProviderFromRevision(cache));
    }
    return std::unique_ptr<GeomPartsProvider>(new GeomPartsProviderFromView(cache));
}

void
fillGeomDiff(DiffDetails& details, ObjectsCache& fromCache, ObjectsCache& toCache)
{
    const auto& fromObj = details.fromObjectVersion;
    const auto& toObj = details.toObjectVersion;

    struct PartDiff {
        Geom before;
        Geom after;
    };
    std::map<TId, PartDiff> partDiffsById;

    if (!details.anyObject()->geom().isNull()) {
        auto& partDiff = partDiffsById[details.anyObject()->id()];
        if (fromObj && !fromObj->isDeleted()) {
            partDiff.before = fromObj->geom();
        }
        if (toObj && !toObj->isDeleted()) {
            partDiff.after = toObj->geom();
        }
    } else {
        auto fromProvider = makeGeomPartsProvider(fromCache);
        auto toProvider = makeGeomPartsProvider(toCache);

        auto fromParts = fromProvider->geomPartIds(fromObj);
        auto toParts = toProvider->geomPartIds(toObj);

        std::vector<TRevisionId> removedParts;
        std::set_difference(
                fromParts.begin(), fromParts.end(), toParts.begin(), toParts.end(),
                std::back_inserter(removedParts));
        std::vector<TRevisionId> addedParts;
        std::set_difference(
                toParts.begin(), toParts.end(), fromParts.begin(), fromParts.end(),
                std::back_inserter(addedParts));

        for (auto& idGeomPair : fromProvider->geomPartGeoms(removedParts)) {
            partDiffsById[idGeomPair.first].before = std::move(idGeomPair.second);
        }
        for (auto& idGeomPair : toProvider->geomPartGeoms(addedParts)) {
            partDiffsById[idGeomPair.first].after = std::move(idGeomPair.second);
        }
    }

    try {
        for (auto& pair : partDiffsById) {
            auto& partDiff = pair.second;
            if (!partDiff.before.equal(partDiff.after, CALCULATION_TOLERANCE)) {
                details.geomDiff.tryAddBefore(std::move(partDiff.before));
                details.geomDiff.tryAddAfter(std::move(partDiff.after));
            }
        }
    } catch (const DiffLimitExceeded& ex) {
        details.geomDiff.setLimitExceeded();
    }
    details.envelope = changesEnvelope(details.geomDiff);
    if (details.envelope.isNull()) {
        details.envelope = changesEnvelope(
            details.fromObjectVersion.get(),
            details.toObjectVersion.get());
    }
}

void
fillThreadStopsDiff(
        DiffDetails& details, ObjectsCache& fromCache, ObjectsCache& toCache)
{
    if (isTransportThread(details.anyObject()->categoryId())) {
        details.threadStopDiff = threadsDiff(
                details.toObjectVersion, details.fromObjectVersion,
                &toCache, &fromCache);
    }
}
}// namespace

GetBranchDiff::GetBranchDiff(const Request& request)
    : controller::BaseController<GetBranchDiff>(BOOST_CURRENT_FUNCTION)
    , request_(request)
{
    CHECK_REQUEST_PARAM(request.objectId);
    result_->fromBranchId = request.fromBranchId;
    result_->toBranchId = request.toBranchId;
    result_->objectId = request.objectId;
}

GetBranchDiff::~GetBranchDiff()
{}

std::string
GetBranchDiff::printRequest() const
{
    std::stringstream ss;
    ss << " fromBranchId:" << request_.fromBranchId;
    ss << " toBranchId:" << request_.toBranchId;
    ss << " objectId:" << request_.objectId;
    ss << " user: " << request_.user;
    ss << " token: " << request_.dbToken;
    return ss.str();
}

void
GetBranchDiff::control()
{
    auto fromBranchCtx = BranchContextFacade(request_.fromBranchId).acquireReadCoreView(request_.dbToken);
    ObjectsCache fromCache(fromBranchCtx, boost::none, CACHE_POLICY);

    auto toBranchCtx = BranchContextFacade(request_.toBranchId).acquireReadCoreView(request_.dbToken);
    ObjectsCache toCache(toBranchCtx, boost::none, CACHE_POLICY);

    auto& details = result_->details;

    details.fromObjectVersion =
        fromCache.get(request_.objectId).get_value_or(ObjectPtr());
    details.toObjectVersion =
        toCache.get(request_.objectId).get_value_or(ObjectPtr());
    WIKI_REQUIRE(
            details.fromObjectVersion || details.toObjectVersion, ERR_BAD_REQUEST,
            "object id: " << request_.objectId << " doesn't exist in either branch");

    if (request_.fromBranchId == request_.toBranchId) {
        return;
    }
    fillCategoryDiff(details);

    fillAttributesDiff(details);

    fillRelativesDiff(details, fromCache, toCache);

    fillGeomDiff(details, fromCache, toCache);

    fillThreadStopsDiff(details, fromCache, toCache);
}

} // namespace wiki
} // namespace maps
