#include "actions.h"

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/tools/grinder/worker/include/api.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/geolib/include/variant.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/mongo/include/init.h>
#include <yandex/maps/wiki/groupedit/actions/move.h>
#include <yandex/maps/wiki/geolocks/geolocks.h>
#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/task_logger.h>
#include <yandex/maps/wiki/tasks/commit_sync.h>
#include <yandex/maps/wiki/tasks/tasks.h>
#include <yandex/maps/wiki/tasks/tool_commands.h>

#include <maps/wikimap/mapspro/libs/revision_meta/include/utils.h>

#include <functional>
#include <vector>
#include <memory>
#include <sstream>

namespace common = maps::wiki::common;
namespace mlog = maps::log8;
namespace rev = maps::wiki::revision;
namespace worker = maps::grinder::worker;

using CommitIds = std::vector<maps::wiki::groupedit::TCommitId>;
using maps::wiki::geolocks::GeoLock;

namespace {

constexpr size_t WORKER_CONCURRENCY = 3;
const std::string COMMIT_TABLE_NAME = "service.groupedit_commit";

} // anonymous namespace

namespace maps {
namespace wiki {
namespace groupedit {

namespace {

std::optional<std::string>
geoJson2WkbOptional(const json::Value& value, const std::string& field)
{
    if (value.hasField(field)) {
        auto geometry = geolib3::readGeojson<geolib3::SimpleGeometryVariant>(value[field]);
        return geolib3::WKB::toString(geometry);
    }
    return std::nullopt;
}

boost::optional<GeoLock> loadOptionalGeoLock(
        pgpool3::TransactionHandle& lockTxn,
        geolocks::TLockId lockId,
        tasks::TaskPgLogger& logger)
{
    if (!lockId) {
        logger.logWarn() << "Using dummy lock";
        return {};
    }
    return GeoLock::load(*lockTxn, lockId);
}

void unlockOptionalGeolock(
        pgpool3::TransactionHandle& lockTxn,
        boost::optional<GeoLock>& optionalGeoLock)
{
    if (!optionalGeoLock) {
        return;
    }
    optionalGeoLock->unlock(*lockTxn);
}

struct CommonTaskParams
{
    uint64_t taskId;
    revision::DBID branchId;
    TUserId uid;
    std::optional<filters::TExpressionId> filterId;
    std::optional<std::string> aoiWkb;
};

CommonTaskParams
commonTaskParams(const worker::Task& task)
{
    const auto& args = task.args();
    return {
        args["taskId"].as<uint64_t>(),
        args["branchId"].as<rev::DBID>(),
        args["uid"].as<TUserId>(),
        args.hasField("filterId")
            ? std::optional<filters::TExpressionId>(args["filterId"].as<filters::TExpressionId>())
            : std::nullopt,
        geoJson2WkbOptional(args, "aoi")
    };
}

CommitIds groupeditMove(
    pgpool3::TransactionHandle& txn,
    pgpool3::TransactionHandle& viewTxn,
    const worker::Task& task,
    const CommonTaskParams& commonParams,
    const std::string& editorCfgPath)
{
    DEBUG() << "Running move";

    REQUIRE(commonParams.aoiWkb, "Expected aoi in groupedit action 'move'");
    const auto& args = task.args();
    const auto& paramsJson = args["params"];
    const auto dx = paramsJson["dx"].as<double>();
    const auto dy = paramsJson["dy"].as<double>();
    const auto partialPolygons = paramsJson["partialPolygons"].as<bool>();


    StringSet categoryIds;
    if (paramsJson.hasField("categoryIds")) {
        auto categoryIdsJson = paramsJson["categoryIds"];
        ASSERT(categoryIdsJson.isArray());
        for (const auto& categoryIdJson : categoryIdsJson) {
            categoryIds.insert(categoryIdJson.as<std::string>());
        }
    }

    maps::xml3::Doc doc(editorCfgPath);
    auto node = doc.node("/editor");
    auto topoNodes = node.nodes("topology-groups/topology-group", true);
    for (size_t i = 0; i < topoNodes.size(); ++i) {
        const auto& topoNode = topoNodes[i];
        auto linElemCatId = topoNode.firstElementChild("linear-element").attr<std::string>("category-id");
        if (categoryIds.find(linElemCatId) != categoryIds.end()) {
            auto juncCatId = topoNode.firstElementChild("junction").attr<std::string>("category-id");
            categoryIds.insert(juncCatId);
        }
    }

    return groupedit::move(
        txn,
        viewTxn,
        commonParams.branchId,
        commonParams.uid,
        commonParams.filterId,
        *commonParams.aoiWkb,
        dx,
        dy,
        categoryIds,
        partialPolygons
            ? groupedit::actions::PolygonMode::Partial
            : groupedit::actions::PolygonMode::AsWhole);
}

CommitIds groupeditUpdateAttrs(
    pgpool3::TransactionHandle& txn,
    pgpool3::TransactionHandle& viewTxn,
    const worker::Task& task,
    const CommonTaskParams& commonParams,
    const std::string&)
{
    DEBUG() << "Running update attributes";

    REQUIRE(commonParams.filterId,
            "Expected filterId in groupedit action 'update attrs'");
    const auto& args = task.args();
    const auto& paramsJson = args["params"];
    auto attrs = paramsJson["attributes"];

    return groupedit::updateAttrs(
        txn,
        viewTxn,
        commonParams.branchId,
        commonParams.uid,
        *commonParams.filterId,
        commonParams.aoiWkb,
        attrs);
}

CommitIds groupeditSetState(
    pgpool3::TransactionHandle& txn,
    pgpool3::TransactionHandle& viewTxn,
    const worker::Task&,
    const CommonTaskParams& commonParams,
    const std::string&)
{
    DEBUG() << "Running set state";
    // At the moment, state may only be changed to 'deleted'
    REQUIRE(commonParams.filterId,
            "Expected filterId in groupedit action 'set state'");

    return groupedit::deleteObjects(
        txn,
        viewTxn,
        commonParams.branchId,
        commonParams.uid,
        *commonParams.filterId,
        commonParams.aoiWkb);
}

/* TODO: Move stolen from editor code to wiki/common library"*/
const std::string VREVISIONS_PREFIX = "vrevisions_";
const std::string VREVISIONS_TRUNK = VREVISIONS_PREFIX + "trunk";
const std::string VREVISIONS_STABLE_PREFIX = VREVISIONS_PREFIX + "stable_";

std::string
vrevisionsSchemaName(TBranchId branchId)
{
    if (branchId == revision::TRUNK_BRANCH_ID) {
        return VREVISIONS_TRUNK;
    }
    return VREVISIONS_STABLE_PREFIX + std::to_string(branchId);
}
void
setSearchPath(pqxx::transaction_base& work, TBranchId branchId)
{
    work.exec("SET search_path=" + vrevisionsSchemaName(branchId) + ",public");
}


} // anonymous namespace

using GroupeditAction = std::function<
    CommitIds(pgpool3::TransactionHandle&,
              pgpool3::TransactionHandle&,
              const worker::Task&,
              const CommonTaskParams&,
              const std::string&)
>;

const std::map<Action, GroupeditAction> ACTION_MAP {
    { Action::Move, groupeditMove },
    { Action::UpdateAttrs, groupeditUpdateAttrs },
    { Action::SetState, groupeditSetState }
};

// Non-idempotent part of the task
CommitIds updateTds(
        const worker::Task& task,
        const CommonTaskParams& commonParams,
        pgpool3::Pool& pgPool,
        pgpool3::Pool& viewPgPool,
        boost::optional<GeoLock>& optionalGeoLock,
        pgpool3::TransactionHandle& lockTxn,
        tasks::TaskPgLogger& logger,
        const std::string& editorCfgPath)
{
    const auto& args = task.args();
    auto action = actionFromString(args["action"].as<std::string>());

    try {
        auto txn = pgPool.masterWriteableTransaction();
        auto viewReadTxn = viewPgPool.slaveTransaction();
        setSearchPath(*viewReadTxn, commonParams.branchId);
        auto commitIds = ACTION_MAP.at(action)(
            txn, viewReadTxn, task, commonParams, editorCfgPath);

        if (commitIds.empty()) {
            logger.logInfo() << "Nothing to update. Task finished.";
            return {};
        }
        logger.logInfo() << "Modifying tds done";

        tasks::CommitRecordGateway commitGtw(txn, COMMIT_TABLE_NAME);
        commitGtw.addCommits(commonParams.taskId, commitIds, tasks::CommitStatus::NotSynced);
        txn->commit();
        logger.logInfo() << "Groupedit commit done";
        return commitIds;

    } catch (...) {
        unlockOptionalGeolock(lockTxn, optionalGeoLock);
        lockTxn->commit();
        logger.logError() << "Modifying tds failed";

        throw;
    }
}

// Idempotent part of the task
void refreshViews(
        pgpool3::Pool& pgPool,
        const CommitIds& commitIds,
        tasks::TaskPgLogger& logger,
        const std::string& configPath,
        const CommonTaskParams& commonParams)
{
    INFO() << "Refreshing views";

    try {
        tasks::refreshViews(commitIds, commonParams.branchId, configPath);

        auto txn = pgPool.masterWriteableTransaction();
        tasks::CommitRecordGateway geCommitGtw(txn, COMMIT_TABLE_NAME);
        geCommitGtw.updateStatus(commonParams.taskId,
            commitIds, tasks::CommitStatus::Synced);
        txn->commit();
        logger.logInfo() << "Refreshing view done";

    } catch (...) {
        logger.logError() << "Refreshing view failed";
        auto txn = pgPool.masterWriteableTransaction();
        tasks::freezeTask(*txn, commonParams.taskId);
        txn->commit();

        throw;
    }
}

void runGroupedit(const common::ExtendedXmlDoc& configDoc,
                  const worker::Task& task)
{
    const auto commonParams = commonTaskParams(task);
    const std::string configPath = configDoc.sourcePaths()[0];
    common::PoolHolder dbHolder(configDoc, "core", "grinder");
    pgpool3::Pool& pgPool = dbHolder.pool();

    common::PoolHolder viewDbHolder(configDoc,
        commonParams.branchId == revision::TRUNK_BRANCH_ID
            ? "view-trunk"
            : "view-stable"
        , "editor");
    pgpool3::Pool& viewPgPool = viewDbHolder.pool();

    const auto& args = task.args();
    tasks::TaskPgLogger logger(pgPool, commonParams.taskId);
    INFO() << "Received groupedit task: " << commonParams.taskId << ". "
           << "Grinder task id: " << task.id();

    try {
        auto txn = pgPool.masterWriteableTransaction();
        // If the task is 'frozen', it will resume at view refresh stage
        bool frozen = tasks::isFrozen(*txn, commonParams.taskId);
        logger.logInfo() << "Task " << (frozen ? "resumed" : "started")
                << ". Grinder task id: " << task.id();

        auto lockId = args["lockId"].as<geolocks::TLockId>();
        auto optionalGeoLock = loadOptionalGeoLock(txn, lockId, logger);

        CommitIds commitIds;
        if (frozen) {
            tasks::CommitRecordGateway commitGtw(txn, COMMIT_TABLE_NAME);
            commitIds = commitGtw.getCommitIds(commonParams.taskId);
        } else {
            commitIds = updateTds(
                task, commonParams,
                pgPool, viewPgPool,
                optionalGeoLock, txn, logger,
                configDoc.get<std::string>("/config/services/editor/config"));
            if (!commitIds.empty()) {
                INFO() << "Enqueue commits for approved branch: " << commitIds.size();
                auto txn = pgPool.masterWriteableTransaction();
                revision_meta::enqueueServiceTaskCommitsForApprovedBranch(
                    *txn,
                    revision_meta::TCommitIds{commitIds.cbegin(), commitIds.cend()}
                );
                txn->commit();
            }
        }

        if (!commitIds.empty()) {
            refreshViews(pgPool, commitIds, logger, configPath, commonParams);
        }

        unlockOptionalGeolock(txn, optionalGeoLock);
        if (frozen)
            tasks::unfreezeTask(*txn, commonParams.taskId);
        txn->commit();
        logger.logInfo() << "Unlocked";
    } catch (const maps::Exception& e) {
        ERROR() << "Task " << commonParams.taskId << " failed: " << e;
        logger.logError() << "Task failed: " << e.what();
        throw;
    } catch (const std::exception& e) {
        ERROR() << "Task " << commonParams.taskId << " failed: " << e.what();
        logger.logError() << "Task failed: " << e.what();
        throw;
    }
    logger.logInfo() << "Task finished";
}

} // groupedit
} // wiki
} // maps

int main(int argc, char* argv[]) try {
    maps::cmdline::Parser parser;
    auto workerConfig
        = parser.string("config").help("path to worker configuration");
    auto syslogTag = parser.string("syslog-tag").help(
        "redirect log output to syslog with given tag");
    auto grinderConfig = parser.string("grinder-config")
                             .help("path to grinder configuration file");

    parser.parse(argc, argv);
    if (syslogTag.defined()) {
        mlog::setBackend(mlog::toSyslog(syslogTag));
    }

    maps::mongo::init();

    std::unique_ptr<common::ExtendedXmlDoc> configDocPtr;
    if (workerConfig.defined()) {
        configDocPtr.reset(new common::ExtendedXmlDoc(workerConfig));
    } else {
        configDocPtr = common::loadDefaultConfig();
    }

    worker::Options workerOpts;

    if (grinderConfig.defined()) {
        workerOpts.setConfigLocation(grinderConfig);
    }

    workerOpts.on("groupedit",
                [&](const worker::Task& task) {
                    maps::wiki::groupedit::runGroupedit(*configDocPtr, task);
                })
            .setConcurrencyLevel(WORKER_CONCURRENCY);

    worker::run(std::move(workerOpts));

    return EXIT_SUCCESS;
} catch (const maps::Exception& e) {
    ERROR() << "Worker failed: " << e;
    return EXIT_FAILURE;
} catch (const std::exception& e) {
    ERROR() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
