#include "diff_loaders.h"
#include "extractors.h"

#include <yandex/maps/wiki/diffalert/revision/editor_config.h>

#include <maps/libs/common/include/exception.h>
#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <maps/libs/log8/include/log8.h>

#include <algorithm>
#include <memory>
#include <vector>

namespace mwr = maps::wiki::revision;
namespace rf = maps::wiki::revision::filters;

namespace maps {
namespace wiki {
namespace diffalert {

enum class RevisionsType
{
    Objects,
    Relations
};

struct IdsBatch
{
    explicit IdsBatch(RevisionsType type_)
        : type(type_)
        , oldRevIds(std::make_shared<std::vector<mwr::RevisionID>>())
        , oldOids(std::make_shared<std::vector<mwr::DBID>>())
        , newRevIds(std::make_shared<std::vector<mwr::RevisionID>>())
        , newOids(std::make_shared<std::vector<mwr::DBID>>())
    {}

    RevisionsType type;
    std::shared_ptr<std::vector<mwr::RevisionID>> oldRevIds;
    std::shared_ptr<std::vector<mwr::DBID>> oldOids;
    std::shared_ptr<std::vector<mwr::RevisionID>> newRevIds;
    std::shared_ptr<std::vector<mwr::DBID>> newOids;
};

namespace {

const size_t OBJECTS_BATCH_SIZE = 10000;

// Calls executor with IdsBatch closed by object ids.
// That is, for any object in batch there is either a revision id
// or an object id in both old and new id sets.
// Each batch corresponds to at most OBJECTS_BATCH_SIZE objects.
template<typename Func>
void executeInBatchesByOids(
    RevisionsType type,
    const mwr::RevisionIds& sortedOldRevIds,
    const mwr::RevisionIds& sortedNewRevIds,
    Func executor)
{
    auto oldRevIdsIt = std::begin(sortedOldRevIds);
    auto newRevIdsIt = std::begin(sortedNewRevIds);

    IdsBatch idsBatch{type};
    size_t batchLeft = OBJECTS_BATCH_SIZE;

    while (oldRevIdsIt != std::end(sortedOldRevIds) ||
            newRevIdsIt != std::end(sortedNewRevIds)) {
        if (!batchLeft) {
            executor(std::move(idsBatch));
            idsBatch = IdsBatch(type);
            batchLeft = OBJECTS_BATCH_SIZE;
        }

        auto oldObjectId = (oldRevIdsIt != std::end(sortedOldRevIds))
            ? oldRevIdsIt->objectId()
            : 0;
        auto newObjectId = (newRevIdsIt != std::end(sortedNewRevIds))
            ? newRevIdsIt->objectId()
            : 0;

        if (oldObjectId && (!newObjectId || oldObjectId <= newObjectId)) {
            idsBatch.oldRevIds->push_back(*oldRevIdsIt++);
        } else {
            idsBatch.oldOids->push_back(newObjectId);
        }
        if (newObjectId && (!oldObjectId || newObjectId <= oldObjectId)) {
            idsBatch.newRevIds->push_back(*newRevIdsIt++);
        } else {
            idsBatch.newOids->push_back(oldObjectId);
        }
        --batchLeft;
    }
    executor(std::move(idsBatch));
}

struct RevisionsBatch
{
    std::map<TId, mwr::ObjectRevision> oldRevsByOid;
    std::map<TId, mwr::ObjectRevision> newRevsByOid;
};

RevisionsBatch loadExistingRevisionsBatch(
        const IdsBatch& idsBatch,
        RevSnapshotFactory& oldRevSnapshotFct,
        RevSnapshotFactory& newRevSnapshotFct)
{
    auto fetchRevsByOid = [](
            RevSnapshotFactory& revSnapshotFct,
            RevisionsType revsType,
            const std::vector<mwr::RevisionID>& revIds,
            const std::vector<mwr::DBID>& oids)
    {
        auto typeFilter = (revsType == RevisionsType::Relations)
            ? rf::ObjRevAttr::isRelation()
            : rf::ObjRevAttr::isNotRelation();

        return common::retryDuration([&] {
            auto revSnapshot = revSnapshotFct.get();

            std::map<TId, mwr::ObjectRevision> revsByOid;
            if (!revIds.empty()) {
                for (auto&& rev :
                        revSnapshot->reader().loadRevisions(
                            revIds,
                            rf::ObjRevAttr::isNotDeleted() && typeFilter)) {
                    auto objectId = rev.id().objectId();
                    revsByOid.emplace(objectId, std::move(rev));
                }
            }
            if (!oids.empty()) {
                for (auto&& [objectId, rev] : revSnapshot->objectRevisions(oids)) {
                    if (rev.data().deleted) {
                        continue;
                    }
                    revsByOid.emplace(objectId, std::move(rev));
                }
            }
            return revsByOid;
        });
    };
    return {
        fetchRevsByOid(
            oldRevSnapshotFct,
            idsBatch.type,
            *idsBatch.oldRevIds,
            *idsBatch.oldOids),
        fetchRevsByOid(
            newRevSnapshotFct,
            idsBatch.type,
            *idsBatch.newRevIds,
            *idsBatch.newOids)
    };
}

struct ParsedRelations
{
    ParsedRelations(const std::map<TId, mwr::ObjectRevision>& revsByOid)
    {
        for (const auto& oidRevPair : revsByOid) {
            const auto& rev = oidRevPair.second;
            try {
                ASSERT(!rev.data().deleted);
                auto& dest = isTableAttrRevision(rev) ? tableAttrs : relations;
                dest.insert(extractRelation(rev));
            } catch (const maps::Exception& ex) {
                badRelations.push_back(oidRevPair.first);
                ERROR() << "Error while parsing relation id = '"
                    << oidRevPair.first << "' exception: " << ex;
            }
        }
    }

    Relations relations;
    Relations tableAttrs;

    std::list<TId> badRelations;
};

std::map<TId, LongtaskRelationsDiff> calcDiffsByOid(
        ParsedRelations&& lhs,
        ParsedRelations&& rhs)
{
    std::map<TId, LongtaskRelationsDiff> diffsByOid;

    for (auto&& rel : lhs.relations) {
        if (!rhs.relations.erase(rel)) {
            diffsByOid[rel.masterId].relationsDeleted.insert(rel);
            diffsByOid[rel.slaveId].relationsDeleted.insert(std::move(rel));
        }
    }
    for (auto&& rel : rhs.relations) {
        diffsByOid[rel.masterId].relationsAdded.insert(rel);
        diffsByOid[rel.slaveId].relationsAdded.insert(std::move(rel));
    }
    for (auto&& ta : lhs.tableAttrs) {
        if (!rhs.tableAttrs.erase(ta)) {
            diffsByOid[ta.masterId].tableAttrsDeleted.insert(std::move(ta));
        }
    }
    for (auto&& ta : rhs.tableAttrs) {
        diffsByOid[ta.masterId].tableAttrsAdded.insert(std::move(ta));
    }
    return diffsByOid;
}

template<typename Map>
const typename Map::mapped_type* ptrMapAt(
        const Map& map,
        const typename Map::key_type& key)
{
    auto it = map.find(key);
    if (it != std::end(map)) {
        return &it->second;
    }
    return nullptr;
}

} // namespace

LongtaskObjectDiffsLoader::LongtaskObjectDiffsLoader(
        RevSnapshotFactory oldRevSnapshotFct,
        RevSnapshotFactory newRevSnapshotFct,
        const EditorConfig& config,
        size_t numThreads)
    : oldRevSnapshotFct_(std::move(oldRevSnapshotFct))
    , newRevSnapshotFct_(std::move(newRevSnapshotFct))
    , config_(config)
    , threadPool_(numThreads)
{ }

void LongtaskObjectDiffsLoader::loadChangedObjects(
        const mwr::RevisionIds& sortedOldRevIds,
        const mwr::RevisionIds& sortedNewRevIds)
{
    executeInBatchesByOids(
        RevisionsType::Objects,
        sortedOldRevIds,
        sortedNewRevIds,
        [this](const IdsBatch& idsBatch) {
            threadPool_.push([=]
            {
                exceptionTrap_.try_([&]{ doLoadChangedObjects(idsBatch); });
            });
        });
}

void LongtaskObjectDiffsLoader::loadUnchangedObjects(const std::vector<TId>& oids)
{
    for (size_t iBegin = 0; iBegin < oids.size(); iBegin += OBJECTS_BATCH_SIZE) {
        auto iEnd = std::min(iBegin + OBJECTS_BATCH_SIZE, oids.size());
        auto batchOids = std::make_shared<std::vector<TId>>(
            oids.begin() + iBegin, oids.begin() + iEnd);
        threadPool_.push([=]
        {
            exceptionTrap_.try_([&]{ doLoadUnchangedObjects(*batchOids); });
        });
    }
}

LongtaskObjectDiffsLoader::LoadLongtaskObjectDiffResult
LongtaskObjectDiffsLoader::getAll()
{
    threadPool_.shutdown();
    exceptionTrap_.release();
    objDiffQueue_.finish();
    std::list<LongtaskObjectDiff> diffs;
    objDiffQueue_.popAll(diffs);
    badObjectQueue_.finish();
    std::list<TId> badObjects;
    badObjectQueue_.popAll(badObjects);
    return {std::move(diffs), std::move(badObjects)};
}

void LongtaskObjectDiffsLoader::doLoadChangedObjects(const IdsBatch& idsBatch)
{
    auto revBatch = loadExistingRevisionsBatch(
        idsBatch,
        oldRevSnapshotFct_,
        newRevSnapshotFct_);

    std::list<LongtaskObjectDiff> batchDiffs;
    std::list<TId> badObjects;
    for (const auto& oidOldRevPair : revBatch.oldRevsByOid) {
        try {
            if (isSystemObjectRevision(oidOldRevPair.second)) {
                continue;
            }
            batchDiffs.emplace_back(
                &oidOldRevPair.second,
                oldRevSnapshotFct_,
                ptrMapAt(revBatch.newRevsByOid, oidOldRevPair.first),
                newRevSnapshotFct_,
                config_);
        } catch (const maps::Exception& ex) {
            badObjects.push_back(oidOldRevPair.first);
            ERROR() << "Error while computing object diff id = '"
                << oidOldRevPair.first << "' exception: " << ex;
        }
    }
    for (const auto& oidNewRevPair : revBatch.newRevsByOid) {
        try {
            if (isSystemObjectRevision(oidNewRevPair.second)) {
                continue;
            }
            if (!revBatch.oldRevsByOid.count(oidNewRevPair.first)) {
                batchDiffs.emplace_back(
                    nullptr,
                    oldRevSnapshotFct_,
                    &oidNewRevPair.second,
                    newRevSnapshotFct_,
                    config_);
            }
        } catch (const maps::Exception& ex) {
            badObjects.push_back(oidNewRevPair.first);
            ERROR() << "Error while computing object diff id = '"
                << oidNewRevPair.first << "' exception: " << ex;
        }
    }
    badObjectQueue_.pushAll(std::move(badObjects));
    objDiffQueue_.pushAll(std::move(batchDiffs));
}

void LongtaskObjectDiffsLoader::doLoadUnchangedObjects(
        const std::vector<mwr::DBID>& batchOids)
{
    auto revisions = common::retryDuration([&] {
        auto snapshot = oldRevSnapshotFct_.get();
        return snapshot->objectRevisions(batchOids);
    });

    std::list<LongtaskObjectDiff> batchDiffs;
    std::list<TId> badObjects;
    for (const auto& [objectId, rev] : revisions) {
        if (rev.data().deleted) {
            continue;
        }
        try {
            batchDiffs.emplace_back(
                &rev,
                oldRevSnapshotFct_,
                &rev,
                newRevSnapshotFct_,
                config_);
        } catch (const maps::Exception& ex) {
            badObjects.push_back(objectId);
            ERROR() << "Error while computing object diff id = '"
                << objectId << "' exception: " << ex;
        }
    }
    badObjectQueue_.pushAll(std::move(badObjects));
    objDiffQueue_.pushAll(std::move(batchDiffs));
}

mwr::RevisionIds loadSortedRevisionIds(
        RevSnapshotFactory& revSnapshotFct,
        const mwr::filters::ProxyFilterExpr& filter)
{
    auto revIds = common::retryDuration([&] {
        auto snapshot = revSnapshotFct.get();
        return snapshot->revisionIdsByFilter(filter);
    });
    std::sort(std::begin(revIds), std::end(revIds));
    return revIds;
}

LoadLongtaskRelationDiffResult loadLongtaskRelationDiffs(
        const revision::RevisionIds& sortedOldRevIds,
        const revision::RevisionIds& sortedNewRevIds,
        RevSnapshotFactory& oldRevSnapshotFct,
        RevSnapshotFactory& newRevSnapshotFct,
        size_t numThreads)
{
    common::ExceptionTrap exceptionTrap;
    ThreadPool threadPool(numThreads);
    ThreadedQueue<std::map<TId, LongtaskRelationsDiff>> relDiffQueue;
    ThreadedQueue<TId> badRelationQueue;

    executeInBatchesByOids(
        RevisionsType::Relations,
        sortedOldRevIds,
        sortedNewRevIds,
        [&](const IdsBatch& idsBatch) {
            threadPool.push([&, idsBatch]
            {
                exceptionTrap.try_([&]
                {
                    auto revBatch = loadExistingRevisionsBatch(
                        idsBatch,
                        oldRevSnapshotFct,
                        newRevSnapshotFct);

                    auto oldParsedRelations = ParsedRelations(revBatch.oldRevsByOid);
                    auto newParsedRelations = ParsedRelations(revBatch.newRevsByOid);

                    badRelationQueue.pushAll(std::move(oldParsedRelations.badRelations));
                    badRelationQueue.pushAll(std::move(newParsedRelations.badRelations));

                    relDiffQueue.push(
                        calcDiffsByOid(
                            std::move(oldParsedRelations),
                            std::move(newParsedRelations)));
                });
            });
        });

    threadPool.shutdown();
    exceptionTrap.release();
    relDiffQueue.finish();
    std::list<std::map<TId, LongtaskRelationsDiff>> diffParts;
    relDiffQueue.popAll(diffParts);

    std::map<TId, LongtaskRelationsDiff> relDiffsByOid;
    for (auto&& diffPart : diffParts) {
        for (auto&& oidRelDiffPair : diffPart) {
            relDiffsByOid[oidRelDiffPair.first] +=
                std::move(oidRelDiffPair.second);
        }
    }

    badRelationQueue.finish();
    std::list<TId> badRelations;
    badRelationQueue.popAll(badRelations);

    return {std::move(relDiffsByOid), std::move(badRelations)};
}

} // namespace diffalert
} // namespace wiki
} // namespace maps
