#include "bbox_update_task.h"
#include "bbox_helpers.h"
#include "complex_object_bbox_computer.h"
#include "complex_objects_loader.h"
#include "bbox_provider.h"
#include <maps/wikimap/mapspro/services/editor/src/sync/db_helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/relations_manager.h>

#include <yandex/maps/wiki/common/batch.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/threadutils/executor.h>
#include <yandex/maps/wiki/threadutils/thread_pools.h>

#include <maps/libs/common/include/profiletimer.h>

namespace maps {
namespace wiki {

namespace {

const size_t COMPLEX_OBJECT_BATCH_COUNT = 200;
const size_t UPDATE_VIEW_OBJECTS_BULK = 100;
const double CUSTOM_COMMIT_RATIO = 0.2;
const size_t VIEW_OBJECTS_IDS_FILTER_THRESHOLD = 10000;

TOIds
suggestDataObjectIds(TBranchId branchId, const StringSet& categoryIds)
{
    auto work = BranchContextFacade::acquireWorkWriteViewOnly(branchId);

    auto r = work->exec(
        "SELECT object_id"
        " FROM suggest_data"
        " WHERE categories ?| " + categoryIdsToArray(categoryIds));

    TOIds result;
    for (const auto& row : r) {
        result.insert(row[0].as<TOid>());
    }
    return result;
}

}

ComputeBBoxTask::ComputeBBoxTask(const ComputeBBoxesParams& params)
    : params_(params)
    , categoriesHolder_(params_.categoryIds)
    , failed_(false)
{
    if (!params_.customCommitIds.empty()) {
        ASSERT(*params_.customCommitIds.begin() >= params_.startCommitId);
        ASSERT(*params_.customCommitIds.rbegin() <= params_.lastCommitId);

        if (params_.customCommitIds.size() >
                (params_.lastCommitId - params_.startCommitId) * CUSTOM_COMMIT_RATIO) {
            params_.customCommitIds.clear(); //compute bboxes for all commit range
        }
    }
}

void ComputeBBoxTask::exec(Transaction& mainTxn)
{
    if (categoriesHolder_.complexCategoryIds().empty()) {
        return;
    }

    if (!params_.executionState->checkOk(mainTxn)) {
        return;
    }

    INFO() << "BBOX started. Commit range [" << params_.startCommitId << "; " << params_.lastCommitId << "]. "
        << "Custom commit count " << params_.customCommitIds.size();

    ProfileTimer pt;

    if (isHistoryUpdateRequested(params_.action)) {
        auto startCommitId = params_.startCommitId;
        while (startCommitId <= params_.lastCommitId) {
            auto endCommitId = std::min(params_.lastCommitId, startCommitId + params_.commitRangeBatchSize);
            computeHistoryForCommitRange(mainTxn, startCommitId, endCommitId);
            startCommitId = endCommitId + 1;
            ASSERT(!failed_);
        }
    }

    if (isViewUpdateRequested(params_.action)) {
        updateViews(mainTxn);
    }

    INFO() << "BBOX finished in " << pt.getElapsedTime() << "s.";
}

void ComputeBBoxTask::loadComplexObjectIds(
    const BBoxContext& context,
    size_t threadPoolSize)
{
    if (!params_.customCommitIds.empty() && context.customCommitIds().empty()) {
        INFO() << "Skip complex object ids loading due to absens of custom commit ids";
        return;
    }

    ProfileTimer pt;

    ComplexObjectsLoader loader(
        context,
        categoriesHolder_,
        threadPoolSize);

    loader.run();

    auto currentComplexObjectIdsByCategory = loader.topLevelComplexObjectIds();

    size_t newComplexObjectCount = 0;
    for (const auto& pair : currentComplexObjectIdsByCategory) {
        newComplexObjectCount += pair.second.size();
    }

    if (!params_.customCommitIds.empty()) {
        //Merge complex object ids from all commit ranges
        //Complex object changed by custom commit can also be changed later in the next commit ranges
        for (const auto& pair : currentComplexObjectIdsByCategory) {
            const auto& categoryId = pair.first;
            const auto& objectIds = pair.second;

            complexObjectIdsByCategory_[categoryId].insert(objectIds.begin(), objectIds.end());
        }
    } else {
        complexObjectIdsByCategory_ = std::move(currentComplexObjectIdsByCategory);
    }

    size_t totalComplexObjectCount = 0;
    for (const auto& pair : complexObjectIdsByCategory_) {
        totalComplexObjectCount += pair.second.size();
    }

    INFO() << common::join(
        complexObjectIdsByCategory_,
        [](const std::pair<std::string, TOIds>& pair) {
            return "Category " + pair.first + " count " + std::to_string(pair.second.size());
        },
        '\n');

    INFO() << "Complex object ids loading finished in " << pt.getElapsedTime() << "s. "
        << "Total complex objects " << totalComplexObjectCount << ". "
        << "New complex objects " << newComplexObjectCount << ". "
        << "Branch " << params_.branchId;
}

void ComputeBBoxTask::computeHistoryForCommitRange(
    Transaction& mainTxn,
    TCommitId startCommitId,
    TCommitId endCommitId)
{
    if (!params_.executionState->checkOk(mainTxn)) {
        return;
    }

    ProfileTimer pt;

    TCommitIds currentCustomCommitIds;
    if (!params_.customCommitIds.empty()) {
        for (const auto commitId : params_.customCommitIds) {
            if (commitId >= startCommitId && commitId <= endCommitId) {
                currentCustomCommitIds.insert(commitId);
            }
        }
    }

    INFO() << "Start processing commit range [" << startCommitId << "; " << endCommitId << "]. "
        << "Custom commit count " << currentCustomCommitIds.size() << ". "
        << "Branch " << params_.branchId;

    BBoxContext context(
        params_.executionState,
        params_.branchId,
        startCommitId,
        endCommitId,
        params_.lastCommitId,
        std::move(currentCustomCommitIds));

    loadComplexObjectIds(context, params_.threadPoolSize);

    if (complexObjectIdsByCategory_.empty()) {
        return;
    }

    if (!params_.executionState->checkOk(mainTxn)) {
        return;
    }

    Executor executor;
    for (const auto& pair : complexObjectIdsByCategory_) {
        const auto& categoryId = pair.first;
        const auto& objectIds = pair.second;

        common::applyBatchOp<TOIds>(
            objectIds,
            COMPLEX_OBJECT_BATCH_COUNT,
            [&, this](const TOIds& batch) {
                executor.addTask([=]() {
                    try {
                        if (!failed_) {
                            computeHistoryForComplexObjects(batch, categoryId, context);
                        }
                        return;
                    } catch (const maps::Exception& e) {
                        ERROR() << "ComputeBBoxTask: " << e;
                    } catch (const std::exception& e) {
                        ERROR() << "ComputeBBoxTask: " << e.what();
                    } catch (...) {
                        ERROR() << "ComputeBBoxTask: unknown error";
                    }
                    failed_ = true;
                });
            });
    }

    auto poolHolder = cfg()->threadPools().pool(params_.threadPoolSize);
    executor.executeAllInThreads(*poolHolder);

    INFO() << "Commit range processing finished in " << pt.getElapsedTime() << "s. "
        << "Branch " << params_.branchId;
}

void ComputeBBoxTask::computeHistoryForComplexObjects(
    TOIds complexObjectIds,
    std::string topLevelCategoryId,
    BBoxContext context)
{
    ComplexObjectBBoxComputer computer(
        std::move(complexObjectIds),
        std::move(topLevelCategoryId),
        std::move(context));

    computer.preload();
    computer.run();

    auto geoms = computer.finalGeometries();

    std::lock_guard<std::mutex> lock(globalGeomCacheMutex_);
    for (auto&& pair : geoms) {
        globalGeomCache_[pair.first] = std::move(pair.second);
    }
}

TOIds ComputeBBoxTask::prepareObjectIdsForUpdateViews() const
{
    const auto& categoryIds = categoriesHolder_.complexCategoryIds();

    if (!isHistoryUpdateRequested(params_.action)) {
        return suggestDataObjectIds(params_.branchId, categoryIds);
    }

    TOIds cachedIds;
    for (const auto& pair : globalGeomCache_) {
        cachedIds.insert(pair.first);
    }

    if (cachedIds.size() < VIEW_OBJECTS_IDS_FILTER_THRESHOLD) {
        return cachedIds;
    }

    auto suggestIds = suggestDataObjectIds(params_.branchId, categoryIds);

    TOIds resultIds;
    std::set_intersection(
        suggestIds.begin(), suggestIds.end(),
        cachedIds.begin(), cachedIds.end(),
        std::inserter(resultIds, resultIds.begin()));
    return resultIds;
}

void ComputeBBoxTask::updateViews(Transaction& mainTxn) const
{
    if (!params_.executionState->checkOk(mainTxn)) {
        return;
    }

    ProfileTimer pt;

    auto objectIds = prepareObjectIdsForUpdateViews();
    if (objectIds.empty()) {
        return;
    }

    INFO() << "Update bbox views for " << objectIds.size() << " objects. "
        << "Branch " << params_.branchId;

    Executor executor;
    common::applyBatchOp<TOIds>(
        objectIds,
        UPDATE_VIEW_OBJECTS_BULK,
        [&, this](const TOIds& batch) {
            executor.addTask([=]() {
                try {
                    updateViewForObjects(batch);
                } catch (const maps::Exception& e) {
                    ERROR() << "ComputeBBoxTask: " << e;
                } catch (const std::exception& e) {
                    ERROR() << "ComputeBBoxTask: " << e.what();
                } catch (...) {
                    ERROR() << "ComputeBBoxTask: unknown error";
                }
            });
        });

    auto poolHolder = cfg()->threadPools().pool(params_.threadPoolSize);
    executor.executeAllInThreads(*poolHolder);

    INFO() << "Bbox views update finished in " << pt.getElapsedTime() << "s. "
        << "Branch " << params_.branchId;
}

void ComputeBBoxTask::preloadNonCachedObjects(ObjectsCache& cache) const
{
    auto filter = [&](const GeoObject* object) {
        return !globalGeomCache_.count(object->id());
    };

    std::map<TOid, StringSet> oidsToSlaveRoles;
    for (const auto& object : cache.find(filter)) {
        oidsToSlaveRoles[object->id()] = object->category().slaveRoleIds(roles::filters::IsGeom);
    }
    cache.relationsManager().loadRelations(RelationType::Slave, oidsToSlaveRoles);
}

void ComputeBBoxTask::updateViewForObjects(TOIds objectIds) const
{
    if (!params_.executionState->isOk()) {
        return;
    }

    BranchContextFacade bcFacade(params_.branchId);
    auto branchContextCore =
        cfg()->tryLongReadAccess()
            ? bcFacade.acquireLongReadCoreOnly(params_.lastCommitId)
            : bcFacade.acquireWrite();

    if (!params_.executionState->checkOk(branchContextCore.txnCore())) {
        return;
    }

    ObjectsCache cache(branchContextCore, params_.lastCommitId, REVISION_META_CACHE_POLICY);
    BBoxProvider bboxProvider(cache, SlavesLoadingPolicy::All);
    cache.get(objectIds);
    preloadNonCachedObjects(cache);

    auto txnView = BranchContextFacade::acquireWorkWriteViewOnly(params_.branchId);

    for (auto id : objectIds) {
        ObjectPtr object = cache.getExisting(id);
        if (object->isDeleted()) {
            continue;
        }

        Geom geom;

        auto it = globalGeomCache_.find(id);
        if (it != globalGeomCache_.end()) {
            geom = it->second;
        } else {
            geom = bboxProvider.geometryForObject(*object);
        }

        if (geom.isNull()) {
            WARN() << "Null geom: " << object->revision() << " " << object->categoryId();
            continue;
        }

        auto query = "UPDATE suggest_data"
            "  SET the_geom=" + geomToSqlString(*txnView, geom) +
            "  WHERE object_id=" + std::to_string(id);
        txnView->exec(query);
    }

    txnView->commit();
}

} // namespace wiki
} // namespace maps
