#include <maps/wikimap/mapspro/services/tasks-ng/modules/common.h>
#include <maps/wikimap/mapspro/services/tasks-ng/modules/revision_helpers.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/config.h>
#include <maps/wikimap/mapspro/services/tasks-ng/lib/db_pools.h>
#include <maps/libs/stringutils/include/split.h>

#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

namespace maps {
namespace wiki {
namespace tasks_ng {
namespace modules {
namespace vrevisions_refresh {

const std::string TASK_TYPE = "vrevisions_refresh";
const std::string TABLE_TASK = sql::SCHEMA + "." + TASK_TYPE + "_task";
const std::string FIELDS_TASK = "branch_id, commit_id, action, stages, category_groups, is_exclusive";

const std::string ACTION_REFRESH = "refresh";
const std::string ACTION_CREATE_STABLE = "create-stable";

const std::string XPATH_STAGES = "/config/services/tasks/vrevisions-refresh/stages";
const std::string ID = "id";

const revision::Branch::LockId GLOBAL_BRANCH_LOCK_ID = 0;

const auto ARCHIVE_BRANCH_MAX = 7;

void writeItem(
    XmlWriter& writer,
    const std::string& itemTag,
    const std::string& id)
{
    writer.addTag(itemTag, [&] {
        writer.addAttribute(ID, id);
    });
}

void checkNotProgress(const revision::Branch& branch)
{
    TASKS_REQUIRE(
        branch.state() != revision::BranchState::Progress,
        ERR_BRANCH_IN_PROGRESS,
        "Branch " << branch.id() << " in progress mode");
}

void checkNotUnavailable(const revision::Branch& branch)
{
    TASKS_REQUIRE(
        branch.state() != revision::BranchState::Unavailable,
        ERR_BRANCH_UNAVAILABLE,
        "Branch " << branch.id() << " unavailable");
}

class BranchWrapper
{
public:
    explicit BranchWrapper(revision::Branch branch)
        : branch_(std::move(branch))
    {}

    void checkNormal() const
    {
        checkNotProgress(branch_);
        checkNotUnavailable(branch_);

        auto workCore = DbPools::get().core().masterWriteableTransaction();
        lock(*workCore); // check if not locked
    }

    void setProgress()
    {
        checkNotProgress(branch_);

        auto workCore = DbPools::get().core().masterWriteableTransaction();
        lock(*workCore);

        TASKS_REQUIRE(
            branch_.setState(*workCore, revision::BranchState::Progress),
            ERR_BRANCH_LOCKED,
            "Branch " << branch_.id() << "maybe concurrently locked");
        workCore->commit();
    }

private:
    void lock(pqxx::transaction_base& txn) const
    {
        auto success = branch_.tryLock(
            txn, GLOBAL_BRANCH_LOCK_ID, revision::Branch::LockType::Shared);

        TASKS_REQUIRE(
            success,
            ERR_BRANCH_LOCKED,
            "Branch " << branch_.id() << "already locked");
    }

    revision::Branch branch_;
};


class Context : public TaskContext
{
public:
    explicit Context(const pqxx::row& row)
        : branchId(row[0].as<Id>())
        , commitId(row[1].as<Id>(0))
        , action(row[2].as<std::string>())
        , stages(row[3].as<std::string>())
        , categoryGroups(row[4].as<std::string>())
        , isExclusive(row[5].as<bool>())
    {}

    void write(XmlWriter& writer, TaskWriteMode mode) const override
    {
        writer.addTag("vrevisions-refresh-context", [&] {
            writer.addTagData("action", action);
            writer.addTagData("branch", branchId);
            writer.addTagData("commit-id", commitId);
            if (mode == TaskWriteMode::Full) {
                writeItems(writer, categoryGroups, "category-groups", "category-group");
                writeItems(writer, stages, "stages", "stage");
            }
        });
    }

    const Id branchId;
    const Id commitId;
    const std::string action;
    const std::string stages;
    const std::string categoryGroups;
    const bool isExclusive;

private:
    static void writeItems(
        XmlWriter& writer,
        const std::string& str,
        const std::string& mainTag,
        const std::string& itemTag)
    {
        if (str.empty()) {
            return;
        }
        writer.addTag(mainTag, [&] {
            for (const auto& id : stringutils::split<std::string>(str, ',')) {
                writeItem(writer, itemTag, id);
            }
        });
    }
};


class Module : public TaskModule
{
public:
    std::string name() const override { return TASK_TYPE; }

    void capabilities(XmlWriter& writer) const override
    {
        writer.addTag("vrevisions-refresh-task-type", [&] {
            writer.addTag("actions", [&] {
                writeItem(writer, "action", ACTION_REFRESH);
                writeItem(writer, "action", ACTION_CREATE_STABLE);
            });

            auto node = cfg().doc().node(XPATH_STAGES);
            visitChildren(writer, node);
        });
    }

    void loadContext(DbContext& ctx, Task& task) const override
    {
        task.loadContext<Context>(ctx, TABLE_TASK, FIELDS_TASK);
    }

    void onCreate(
        DbContext& ctx,
        Task& task,
        const RequestParameters& parameters) const override
    {
        Id branchId = 0;
        Id commitId = 0;
        auto action = parameters.getValue<std::string>("action", ACTION_REFRESH);
        auto stages = parameters.getValue<std::string>("stages", "all");
        auto categoryGroups = parameters.getValue<std::string>("categoryGroups", "all");
        bool isExclusive = parameters.getValue<bool>("exclusive", false);

        if (action == ACTION_CREATE_STABLE) {
            revision::BranchManager bm(ctx.txnCore());
            auto getApprovedBranch = [&] {
                auto branches = bm.load(revision::BranchManager::BranchLimits{
                    {revision::BranchType::Approved, 1}
                });
                TASKS_REQUIRE(
                    !branches.empty(),
                    ERR_BRANCH_NOT_EXISTS,
                    "Approved branch not exists");
                return std::move(branches.front());
            };

            auto branches = bm.load(revision::BranchManager::BranchLimits{
                {revision::BranchType::Stable, 1},
                {revision::BranchType::Archive, revision::BranchManager::UNLIMITED}
            });
            TASKS_REQUIRE(
                branches.empty() || branches.front().type() != revision::BranchType::Stable,
                ERR_BRANCH_STABLE_ALREADY_EXISTS,
                "Stable branch already exists");
            TASKS_REQUIRE(
                branches.size() >= ARCHIVE_BRANCH_MAX,
                ERR_BRANCH_ARCHIVE_TOO_MANY,
                "Archive branches too many");

            auto approvedBranch = getApprovedBranch();
            checkNotProgress(approvedBranch);
            checkNotUnavailable(approvedBranch);

            BranchWrapper bw(approvedBranch);
            bw.setProgress();

            approvedBranch.concatenateAttributes(ctx.txnCore(), {{"stable_branch_creation", "1"}});

            branchId = approvedBranch.id();
            commitId = revision::RevisionsGateway(ctx.txnCore()).headCommitId();
            isExclusive = true;
        } else {
            TASKS_REQUIRE(
                action == ACTION_REFRESH, ERR_BAD_REQUEST, "Unknown action: " << action);

            auto branch = getBranch(ctx, parameters);
            BranchWrapper bw(branch);
            if (isExclusive) {
                bw.setProgress();
            } else {
                bw.checkNormal();
            }
            branchId = branch.id();
            commitId = getCommitId(ctx, parameters, branch);
        }

        auto& txnCore = ctx.txnCore();
        auto query = Query()
            .insertInto(TABLE_TASK)
            .columns("id, " + FIELDS_TASK)
            .values(Query()
                << task.id() << ','
                << branchId << ','
                << commitId << ','
                << txnCore.quote(action) << ','
                << txnCore.quote(stages) << ','
                << txnCore.quote(categoryGroups) << ','
                << (isExclusive ? "true" : "false"))
            .returning(FIELDS_TASK);

        auto rows = txnCore.exec(query.str());
        ASSERT(rows.size() == 1);

        task.setContext(std::make_unique<Context>(rows[0]));
    }

    json::Value launchParameters(const Task& task) const override
    {
        const auto& ctx = task.context<Context>();

        return json::Value{
            { TYPE, json::Value{TASK_TYPE} },
            { TASK_ID, json::Value{task.id()} },
            { UID, json::Value{task.createdBy()} },
            { BRANCH_ID, json::Value{ctx.branchId} },
            { "action", json::Value{ctx.action} },
            { "stages", json::Value{ctx.stages} },
            { "categoryGroups", json::Value{ctx.categoryGroups} },
            { "isExclusive", json::Value{ctx.isExclusive} }
        };
    }

private:
    static void visitChildren(XmlWriter& writer, const xml3::Node& parent)
    {
        auto node = parent.firstElementChild();
        for (; !node.isNull(); node = node.nextElementSibling()) {
            writer.addTag(node.name(), [&] {
                writer.addAttribute(ID, node.attr<std::string>(ID));
                visitChildren(writer, node);
            });
        }
    }
};

const auto module = TaskModuleRegistry::get().registerModule<Module>();

} // namespace vrevisions_refresh
} // namespace modules
} // namespace tasks_ng
} // namespace wiki
} // namespace maps
