#include "tasks.h"
#include "dependent_info.h"
#include "sync_params.h"
#include "sync_view.h"
#include "sync_attrs.h"
#include "target_tables.h"
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/collection.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/junction.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/linear_element.h>
#include <maps/wikimap/mapspro/services/editor/src/srv_attrs/registry.h>
#include <maps/wikimap/mapspro/services/editor/src/observers/renderer.h>
#include <maps/wikimap/mapspro/services/editor/src/observers/view_syncronizer.h>
#include <maps/wikimap/mapspro/services/editor/src/observers/observer.h>

#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/branch.h>

#include <sstream>
#include <cstdlib>
#include <unistd.h>

namespace maps {
namespace wiki {
namespace sync {

/* Tasks priority
 * 1) WRITE_VIEW
 * 2) UPDATE_SRV_ATTRS
 * 3) UPDATE_LABELS
 */

namespace {

const CachePolicy EDITOR_TOOL_BUILD_VIEW_CACHE_POLICY =
{
    TableAttributesLoadPolicy::Skip,
    ServiceAttributesLoadPolicy::Skip,
    DanglingRelationsPolicy::Ignore
};

const CachePolicy EDITOR_TOOL_DEPENDENT_INFO_CACHE_POLICY =
{
    TableAttributesLoadPolicy::Skip,
    ServiceAttributesLoadPolicy::Skip,
    DanglingRelationsPolicy::Ignore
};

const CachePolicy EDITOR_TOOL_BUILD_ATTRS_CACHE_POLICY =
{
    TableAttributesLoadPolicy::Load,
    ServiceAttributesLoadPolicy::Skip,
    DanglingRelationsPolicy::Check
};

const size_t MAX_BATCH_VIEW = 1000;
const size_t MAX_BATCH_ATTRS = 500;
const size_t MAX_BATCH_LABELS = 100;
const size_t MAX_BATCH_DEPENDENT_INFO = 500;

const size_t SLEEP_TIMEOUT = 1; // 1 second

} // namespace

struct RetryParams
{
    std::string name;
    size_t maxAttempts;
    size_t maxTimeout;
};

namespace {
const RetryParams BUILD_VIEW_PARAMS {"build view", 5, 3};
const RetryParams UPDATE_ATTRS_PARAMS {"update srv attrs", 5, 3};
const RetryParams UPDATE_SUGGEST_PARAMS {"update suggest", 5, 3};
const RetryParams PLACE_LABELS_PARAMS {"place labels", 15, 10};
} // namespace

Tasks::TaskInfo
Tasks::passDependentInfo(DependentInfo& dependentInfo)
{
    auto functor = [&] (const Tasks& tasks)
    {
        tasks.updateDependentInfo(dependentInfo);
    };
    return {functor, "UPDATE_DEPENDENT_INFO", MAX_BATCH_DEPENDENT_INFO};
}

boost::optional<Tasks::TaskInfo>
Tasks::passView(const SyncFlags& flags)
{
    if (flags.view()) {
        return TaskInfo{&Tasks::writeObjectView, "WRITE_VIEW", MAX_BATCH_VIEW};
    }
    return boost::optional<TaskInfo>();
}

boost::optional<Tasks::TaskInfo>
Tasks::passAttrs(const SyncFlags& flags)
{
    if (flags.attrs()) {
        return TaskInfo{&Tasks::updateViewSrvAttrs, "UPDATE_SRV_ATTRS", MAX_BATCH_ATTRS};
    }
    return boost::optional<TaskInfo>();
}

boost::optional<Tasks::TaskInfo>
Tasks::passSuggest(const SyncFlags& flags)
{
    if (flags.suggest()) {
        return TaskInfo{&Tasks::updateSuggest, "UPDATE_SUGGEST", MAX_BATCH_ATTRS};
    }
    return boost::optional<TaskInfo>();
}

boost::optional<Tasks::TaskInfo>
Tasks::passLabels(const SyncFlags& flags)
{
    if (flags.labels()) {
        return TaskInfo{&Tasks::updateLabels, "UPDATE_LABELS", MAX_BATCH_LABELS};
    }
    return boost::optional<TaskInfo>();
}

namespace {

ObjectPredicate
filterByIds(const TOIds& ids)
{
    return [&] (const GeoObject* object) -> bool
        {
            return ids.find(object->id()) != ids.end();
        };
}

} // namespace

Tasks::Tasks(
        const TargetTables& targetTables,
        const TOIds& oids,
        std::string logContext)
    : targetTables_(targetTables)
    , oids_(oids)
    , logContext_(std::move(logContext))
{}

void
Tasks::updateDependentInfo(DependentInfo& dependentInfo) const
{
    auto branchId = targetTables_.branch().id();
    auto headCommitId = targetTables_.syncParams().headCommitId();
    auto branchCtx = BranchContextFacade(branchId).acquireWrite();
    ObjectsCache cache(
        branchCtx,
        headCommitId,
        EDITOR_TOOL_DEPENDENT_INFO_CACHE_POLICY);

    dependentInfo.updateData(oids_, cache);
}

void
Tasks::writeObjectView() const
{
    execWithRetry(
        [&]() -> std::string
        {
            auto branchId = targetTables_.branch().id();
            auto headCommitId = targetTables_.syncParams().headCommitId();
            auto branchCtx = BranchContextFacade(branchId).acquireWrite();
            ObjectsCache cache(
                branchCtx,
                headCommitId,
                EDITOR_TOOL_BUILD_VIEW_CACHE_POLICY);
            cache.load(oids_);

            bool checkViewExists = !targetTables_.isViewEmpty();
            updateViews(cache, filterByIds(oids_), checkViewExists);

            auto executionState = targetTables_.syncParams().executionState();
            if (executionState->isOk()) {
                branchCtx.txnView().commit();
            }
            return {};
        },
        BUILD_VIEW_PARAMS);
}

void
Tasks::updateViewSrvAttrs() const
{
    execWithRetry(
        [&]() -> std::string
        {
            auto branchId = targetTables_.branch().id();
            auto headCommitId = targetTables_.syncParams().headCommitId();
            auto branchCtx = BranchContextFacade(branchId).acquireWrite();
            ObjectsCache cache(
                branchCtx, headCommitId, EDITOR_TOOL_BUILD_ATTRS_CACHE_POLICY);

            srv_attrs::preloadRequiredObjects(oids_, cache);
            bool checkSuggestExists = !targetTables_.isSuggestEmpty();

            updateAttrs(cache, filterByIds(oids_));

            ViewSyncronizer viewSync;
            viewSync.updateObjectsSuggests(cache, filterByIds(oids_), checkSuggestExists);

            auto executionState = targetTables_.syncParams().executionState();
            if (executionState->isOk()) {
                branchCtx.txnView().commit();
            }
            return {};
        },
        UPDATE_ATTRS_PARAMS);
}

void
Tasks::updateSuggest() const
{
    execWithRetry(
        [&]() -> std::string
        {
            auto branchId = targetTables_.branch().id();
            auto headCommitId = targetTables_.syncParams().headCommitId();
            auto branchCtx = BranchContextFacade(branchId).acquireWrite();
            ObjectsCache cache(
                branchCtx, headCommitId, EDITOR_TOOL_BUILD_ATTRS_CACHE_POLICY);
            cache.load(oids_);

            bool checkSuggestExists = !targetTables_.isSuggestEmpty();
            ViewSyncronizer().updateObjectsSuggests(cache, filterByIds(oids_), checkSuggestExists);

            auto executionState = targetTables_.syncParams().executionState();
            if (executionState->isOk()) {
                branchCtx.txnView().commit();
            }
            return {};
        },
        UPDATE_SUGGEST_PARAMS);
}

void
Tasks::updateLabels() const
{
    execWithRetry(
        [&]() -> std::string
        {
            auto branchId = targetTables_.branch().id();
            auto workLabels = BranchContextFacade::acquireWorkWriteLabelsOnly(branchId);
            if (!targetTables_.areLabelsEmpty()) {
                RendererObserver::deleteLabels(oids_, *workLabels);
            }
            auto executionState = targetTables_.syncParams().executionState();
            if (!executionState->isOk()) {
                return {};
            }
            auto workView = BranchContextFacade::acquireWorkWriteViewOnly(branchId);
            if (!RendererObserver::placeLabels(oids_, *workView, *workLabels, branchId)) {
                return "error detected on place labels";
            }
            if (executionState->isOk()) {
                workLabels->commit();
            }
            return {};
        },
        PLACE_LABELS_PARAMS);
}

void
Tasks::execWithRetry(
    std::function<std::string(void)> function, const RetryParams& params) const
{
    auto runMethod = [&]() -> std::string
    {
        try {
            return function();
        } catch (const maps::Exception& e) {
            std::ostringstream os;
            os << e;
            return os.str();
        } catch (const std::exception& e) {
            return e.what();
        }
    };

    auto executionState = targetTables_.syncParams().executionState();
    size_t attempt = 0;
    try {
        while (executionState->isOk() && attempt < params.maxAttempts) {
            ++attempt;
            if (attempt > 1) {
                INFO() << "Attempt: " << attempt << " " << logContext_
                       << " : " << params.name << " continue";
            }
            auto errorMessage = runMethod();
            if (!executionState->isOk()) {
                break;
            }
            if (errorMessage.empty()) {
                return; // ok
            }

            auto timeout = (rand() % params.maxTimeout) + 1;
            WARN() << "Attempt: " << attempt << " " << logContext_
                   << " sleeping " << timeout << " seconds on " << params.name <<  ","
                   << " reason: " << errorMessage;
            while (timeout > 0 && executionState->isOk()) {
                ::sleep(SLEEP_TIMEOUT);
                timeout -= SLEEP_TIMEOUT;
            }
        }
    } catch (const std::exception& e) {
        ERROR() << "Attempt: " << attempt << " " << logContext_ << " error: " << e.what();
    } catch (...) {
        ERROR() << "Attempt: " << attempt << " " << logContext_ << " unknown error";
    }

    ERROR() << params.name << " batch failed, attempts: " << attempt << " " << logContext_
            << " execution state canceled: " << executionState->cancel
                              << " failed: " << executionState->fail
            << " oids: " << common::join(oids_, ',');
}

} // namespace sync
} // namespace wiki
} // namespace maps
