#include "dispatch.h"
#include "edit.h"
#include "add.h"
#include "delete.h"
#include "dbhelpers.h"
#include "ad_finder.h"

#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/libs/common/include/map_at.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/algorithm/string/predicate.hpp>

namespace rev = maps::wiki::revision;

namespace maps {
namespace wiki {
namespace importer {

namespace {

std::vector<DbObject> findSlavesByRoleAndLang(
    const DbObject& masterObject,
    const std::string& roleIdToFind,
    const std::string& langToFind)
{
    std::vector<DbObject> slaves;

    for (const auto& relation : masterObject.slaves()) {
        const auto& roleId = relation.roleId();
        if (roleId != roleIdToFind) {
            continue;
        }
        const auto& slave = relation.relatedObject();
        auto lang = extractLang(slave.attributes());
        if (lang == langToFind) {
            slaves.push_back(slave); //copy!
        }
    }
    return slaves;
}

void propagateSkipness(
    const ObjectsCache& cache,
    MessageReporter& messageReporter)
{
    Objects skippedObjects;

    for (const auto& object : cache.objects()) {
        if (object->isSkipped()) {
            skippedObjects.push_back(object);
        }
    }

    while (!skippedObjects.empty()) {
        Objects newSkippedObjects;

        auto checkDirectRelations = [&](const auto& relations) {
            for (const auto& relation : relations) {
                if (!relation.relatedObject->isSkipped()) {
                    relation.relatedObject->setSkipped(true);
                    newSkippedObjects.push_back(relation.relatedObject);
                }
            }
        };

        for (const auto& object : skippedObjects) {
            checkDirectRelations(object->slaveRelations());
            checkDirectRelations(object->masterRelations());
        }

        auto checkInverseRelations = [&](const auto& object, const auto& relations) {
            if (object->isSkipped()) {
                return;
            }
            for (const auto& relation : relations) {
                if (relation.relatedObject->isSkipped()) {
                    object->setSkipped(true);
                    newSkippedObjects.push_back(object);
                    break;
                }
            }
        };

        for (const auto& object : cache.objects()) {
            checkInverseRelations(object, object->slaveRelations());
            checkInverseRelations(object, object->masterRelations());
        }

        skippedObjects = std::move(newSkippedObjects);
    }

    std::set<ID> skippedObjectIds;
    for (const auto& object : cache.objects()) {
        if (object->isSkipped()) {
            skippedObjectIds.insert(object->id());
        }
    }

    for (const auto& id : skippedObjectIds) {
        messageReporter.warning(id) << "Skip object";
    }
}

class ImportData
{
public:
    ImportData(
        TaskParams& params,
        tasks::TaskPgLogger& logger,
        MessageReporter& messageReporter);

    void processObjects(
        const ObjectsCache& cache);

    CommitIds executeOperations();

private:
    void processObject(
        const ObjectPtr& objectInFile);

    void addSlavesOfExistingObject(
        const ObjectPtr& objectInFile,
        const DbObject& objectInDb);

    void deleteSlavesOfExistingObject(
        const ObjectPtr& objectInFile,
        const DbObject& objectInDb);

    void processMastersOfExistingObject(
        const ObjectPtr& objectInFile,
        const DbObject& objectInDb);

    void deleteMastersOfExistingObject(
        const ObjectPtr& objectInFile,
        const DbObject& objectInDb);

    void processDeleteObject(
        const ObjectPtr& objectInFile);

    void addToIdMap(const ObjectPtr& object)
    {
        idMap_[std::to_string(object->dbId())] = object->id();
    }

    void addObject(const ObjectPtr& object)
    {
        objectsToAdd_.push_back(object);
    }

    void deleteObject(TObjectId id)
    {
        objectsToDelete_.insert(id);
    }

    void addRelation(RelationToAdd&& relation)
    {
        relationsToAdd_.push_back(std::move(relation));
    }

    void deleteRelation(rev::RevisionID&& revisionId)
    {
        relationsToDelete_.push_back(std::move(revisionId));
    }

    void editAttributes(const ObjectPtr& object)
    {
        objectsToEditAttributes_.push_back(object);
    }

    TaskParams& params_;
    tasks::TaskPgLogger& logger_;
    MessageReporter& messageReporter_;

    RevisionsFacade facade_;
    DbIdToFeatureIdMap idMap_;

    Objects objectsToAdd_;
    ObjectIds objectsToDelete_;
    std::vector<RelationToAdd> relationsToAdd_;
    rev::RevisionIds relationsToDelete_;
    Objects objectsToEditAttributes_;
};

ImportData::ImportData(
        TaskParams& params,
        tasks::TaskPgLogger& logger,
        MessageReporter& messageReporter)
    : params_(params)
    , logger_(logger)
    , messageReporter_(messageReporter)
    , facade_(params)
{
}

void ImportData::processObjects(const ObjectsCache& cache)
{
    runLoggable([&]{ facade_.loadObjectFromDb(cache, messageReporter_); },
        logger_, "Load objects from db");

    runLoggable([&]{ propagateSkipness(cache, messageReporter_); },
            logger_, "Propagate skipness");

    if (messageReporter_.hasErrors()) {
        return;
    }

    switch (params_.action) {
    case Action::Add:
    case Action::Edit:
        for (const auto& object : cache.objects()) {
            processObject(object);
        }
        break;
    case Action::Delete:
        for (const auto& object : cache.objects()) {
            processDeleteObject(object);
        }
        break;
    }
}

void ImportData::processObject(const ObjectPtr& object)
{
    REQUIRE(object->dbId() != EMPTY_DB_ID, "Empty object db id " << object->category().id());

    if (object->isSkipped()) {
        return;
    }

    addToIdMap(object);
    if (object->state() == ObjectState::New) {
        addObject(object);
    } else if (object->isModified()) {
        editAttributes(object);

        const auto& objectInDb = facade_.object(object->dbId());
        addSlavesOfExistingObject(object, objectInDb);
        deleteSlavesOfExistingObject(object, objectInDb);
        processMastersOfExistingObject(object, objectInDb);
        deleteMastersOfExistingObject(object, objectInDb);
    }
}

void ImportData::addSlavesOfExistingObject(
    const ObjectPtr& objectInFile,
    const DbObject& objectInDb)
{
    for (const auto& relation : objectInFile->slaveRelations()) {
        const auto& slaveInFile = relation.relatedObject;
        const auto& roleIdInFile = relation.roleId;

        auto langInFile = extractLang(slaveInFile->attributesToAdd());
        if (!langInFile.empty()) {
            auto slavesInDb = findSlavesByRoleAndLang(objectInDb, roleIdInFile, langInFile);
            if (!slavesInDb.empty()) {
                bool isNewSlave = true;
                for (const auto& slaveInDb : slavesInDb) {
                    if (slaveInFile->attributesToAdd() == slaveInDb.attributes()) {
                        isNewSlave = false; //The slave in the file is identical to the existent slave in the DB
                        break;
                    }
                }
                if (!isNewSlave) {
                    continue;
                }

                if (roleIdInFile != role::SYNONYM) {
                    //If there is a single non-synonym name, then delete it and create a new one
                    //If there are more than one non-synonym name, then we can't decide whether we should delete all of them or no one
                    REQUIRE(slavesInDb.size() == 1,
                        "Can't decide which name of role " << roleIdInFile << " to delete. Object id " << objectInDb.dbId());
                    deleteObject(slavesInDb[0].dbId());
                }
            }
        }

        //Avoid circural reference by creating duplicate master
        slaveInFile->addMaster(roleIdInFile, objectInFile->dbId(), objectInFile->category());
    }
}

void ImportData::deleteSlavesOfExistingObject(
    const ObjectPtr& objectInFile,
    const DbObject& objectInDb)
{
    for (const auto& relation : objectInFile->deleteSlaveRelations()) {
        REQUIRE(!relation.lang.empty(), "Can't delete relation with empty lang");

        auto slavesInDb = findSlavesByRoleAndLang(objectInDb, relation.roleId, relation.lang);
        if (slavesInDb.empty()) {
            messageReporter_.warning(objectInFile->id())
                << "Object has no " << relation.roleId << " names with lang " << relation.lang;
            continue;
        }

        for (const auto& slaveInDb : slavesInDb) {
            deleteObject(slaveInDb.dbId());
        }
    }
}

void ImportData::processMastersOfExistingObject(
    const ObjectPtr& objectInFile,
    const DbObject& objectInDb)
{
    REQUIRE(objectInFile->masterRelations().size() <= 1, "Object in file has more than one master");
    if (objectInFile->masterRelations().empty()) {
        return;
    }

    const auto& relationInFile = objectInFile->masterRelations().front();
    const auto& masterInFile = relationInFile.relatedObject;
    const auto& roleId = relationInFile.roleId;

    for (const auto& relationInDb : objectInDb.masters()) {
        const auto& masterInDb = relationInDb.relatedObject();
        if (relationInDb.roleId() == roleId
            && masterInDb.dbId() == masterInFile->dbId()) {
            //master hasn't changed - do nothing
            return;
        }
    }

    const auto& role = masterInFile->category().slaveRole(roleId);
    if (role.masterMaxOccurs()) {
        size_t count = 0;
        for (const auto& relationInDb : objectInDb.masters()) {
            if (relationInDb.roleId() == roleId) {
                count++;
            }
        }

        if (count >= *role.masterMaxOccurs()) {
            REQUIRE(count == 1, "We can't decide which master to delete");
            for (const auto& relationInDb : objectInDb.masters()) {
                if (relationInDb.roleId() == roleId) {
                    deleteRelation(relationInDb.revisionId());
                    break;
                }
            }
        }
    }
    addRelation(RelationToAdd{
        masterInFile->dbId(),
        objectInFile->dbId(),
        StringMap{
            { rel::ROLE, roleId },
            { rel::MASTER, masterInFile->category().id() },
            { rel::SLAVE, objectInFile->category().id() }
        }});
}

void ImportData::deleteMastersOfExistingObject(
    const ObjectPtr& objectInFile,
    const DbObject& objectInDb)
{
    const auto& mastersToDelete = objectInFile->mastersToDelete();
    if (mastersToDelete.empty()) {
        return;
    }
    for (const auto& relationInDb : objectInDb.masters()) {
        if (std::any_of(mastersToDelete.begin(), mastersToDelete.end(),
            [&](const auto& toDelete) {
                return toDelete.roleId == relationInDb.roleId() &&
                    (toDelete.masterDbId == 0 ||
                     toDelete.masterDbId == relationInDb.relatedObject().dbId());
            }))
        {
            deleteRelation(relationInDb.revisionId());
        }
    }
}

void ImportData::processDeleteObject(
    const ObjectPtr& objectInFile)
{
    if (objectInFile->isSkipped()) {
        return;
    }

    if (objectInFile->category().id() == cat::AD) {
        std::set<std::string> allowedRoleIds;
        allowedRoleIds.insert(NAME_ROLE_IDS.begin(), NAME_ROLE_IDS.end());
        allowedRoleIds.insert(role::CENTER);
        allowedRoleIds.insert(role::EXCLUSION);

        TObjectId centerObjectId = 0;

        const auto& objectInDb = facade_.object(objectInFile->dbId());
        for (const auto& rel : objectInDb.slaves()) {
            if (!allowedRoleIds.count(rel.roleId())) {
                objectInFile->setSkipped(true);
                messageReporter_.warning(objectInFile->id())
                    << "Skip: ad has role '" << rel.roleId() << "' that prevents deletion";
                return;
            }

            if (rel.roleId() == role::CENTER) {
                centerObjectId = rel.relatedObjectId();
            }
        }

        if (centerObjectId) {
            deleteObject(centerObjectId);
        }

    } else if (objectInFile->category().id() == cat::RD) {
        const auto& objectInDb = facade_.object(objectInFile->dbId());
        for (const auto& rel : objectInDb.slaves()) {
            if (rel.roleId() == role::ASSOCIATED_WITH) {
                objectInFile->setSkipped(true);
                messageReporter_.warning(objectInFile->id()) << "Skip: road has addr points";
                return;
            }
        }
    }
    deleteObject(objectInFile->dbId());
}

CommitIds ImportData::executeOperations()
{
    CommitIds commits;

    if (!objectsToAdd_.empty()) {
        runLoggable([&]{ commits.splice(commits.end(),
                addObjects(objectsToAdd_, idMap_, params_, logger_, messageReporter_)); },
            logger_, "Add objects");
    }

    if (messageReporter_.hasErrors()) {
        return {};
    }

    if (!relationsToDelete_.empty()) {
        runLoggable([&]{ commits.splice(commits.end(),
                deleteRelations(relationsToDelete_, params_)); },
            logger_, "Delete relations");
    }

    if (!relationsToAdd_.empty()) {
        runLoggable([&]{ commits.splice(commits.end(),
                addRelations(relationsToAdd_, params_)); },
            logger_, "Add relations");
    }

    if (!objectsToDelete_.empty()) {
        runLoggable([&]{
                auto resultCommits = deleteObjectsWithTableAttributes(objectsToDelete_, params_, messageReporter_);
                commits.splice(commits.end(), resultCommits);
            },
            logger_, "Delete objects");
    }

    if (messageReporter_.hasErrors()) {
        return {};
    }

    if (!objectsToEditAttributes_.empty()) {
        runLoggable([&]{ commits.splice(commits.end(),
                editObjectAttributes(objectsToEditAttributes_, params_, messageReporter_)); },
            logger_, "Edit attributes");
    }

    if (messageReporter_.hasErrors()) {
        return {};
    }

    return commits;
}

void fulfillFutureMasters(
    const ObjectsCache& cache,
    MessageReporter& messageReporter,
    const EditorConfig& editorConfig)
{
    for (const auto& object : cache.objects()) {
        for (const auto& relation : object->futureMasters()) {
            //Assume that for the most cases categoryId equals layerName
            auto masterCategoryId = relation.relatedId.layerName;
            const auto& masterCategory = editorConfig.category(masterCategoryId);

            auto master = cache.getObject(relation.relatedId, masterCategory);
            if (!master) {
                messageReporter.error(object->id())
                    << "Failed to find related object " << relation.relatedId;
                continue;
            }
            object->addMaster(relation.roleId, master);
        }
    }
}

void generateDbIds(
    const ObjectsCache& cache,
    TaskParams& params)
{
    rev::RevisionsGateway gateway(*params.mainTxn);

    for (const auto& object : cache.objects()) {
        if (object->isSkipped() || object->state() != ObjectState::New) {
            continue;
        }
        REQUIRE(object->dbId() == EMPTY_DB_ID, "Attempt to change existing db id " << object->dbId());
        object->setDbId(gateway.acquireObjectId().objectId(), ObjectState::New);
    }
}

} // namespace

DispatchResult dispatchObjects(
    const ObjectsCache& cache,
    TaskParams& params,
    tasks::TaskPgLogger& logger,
    MessageReporter& messageReporter)
{
    if (cache.objects().empty()) {
        return {};
    }

    if (params.action == Action::Add || params.action == Action::Edit) {
        runLoggable([&]{ fulfillFutureMasters(cache, messageReporter, params.editorConfig); },
            logger, "Fulfill future masters");

        if (messageReporter.hasErrors()) {
            return DispatchResult();
        }
    }

    if (params.action == Action::Add) {
        runLoggable([&]{ findContainingAdForRoadsWithoutGeometry(cache, params, messageReporter); },
            logger, "Find containing ad for roads without geometry");

        runLoggable([&]{ findParentAdForAd(cache, params, messageReporter); },
            logger, "Find containing ad for ad");

        if (messageReporter.hasErrors()) {
            return DispatchResult();
        }
    }

    if (params.action == Action::Add || params.action == Action::Edit) {
        runLoggable([&]{ generateDbIds(cache, params); },
            logger, "Generate object ids in db");
    }

    if (messageReporter.hasErrors()) {
        return DispatchResult();
    }

    ImportData importData(params, logger, messageReporter);
    importData.processObjects(cache);

    if (messageReporter.hasErrors()) {
        return DispatchResult();
    }

    auto commits = importData.executeOperations();

    if (messageReporter.hasErrors()) {
        return DispatchResult();
    }

    ObjectIds skippedObjectIds;
    for (const auto& object : cache.objects()) {
        if (object->isSkipped() && object->state() == ObjectState::Existing) {
            skippedObjectIds.insert(object->dbId());
        }
    }

    return {std::move(commits), std::move(skippedObjectIds)};
}

} // importer
} // wiki
} // maps
