#include "actions.h"

#include <yandex/maps/wiki/groupedit/actions/delete.h>
#include <yandex/maps/wiki/groupedit/actions/update_attrs.h>
#include <yandex/maps/wiki/filters/stored_expression.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/make_batches.h>

namespace rf = maps::wiki::revision::filters;

namespace maps::wiki::groupedit {
namespace {
const size_t MAX_OIDS_PER_BATCH = 10000;
const std::string CANONICAL_CAT_PREFFIX = "cat:";

std::vector<revision::DBID>
loadObjectIds(
    pgpool3::TransactionHandle& viewTxn,
    const std::string& aoiWkb,
    const std::string& viewFilterClause,
    const StringSet& categoryIds)
{
    std::string query = "SELECT id FROM objects_g WHERE (" + viewFilterClause + ")";

    if (!categoryIds.empty()) {
        query += " AND domain_attrs ?| ARRAY[" + common::join(categoryIds,
            [&](const std::string& categoryId) {
                return viewTxn->quote(categoryId);
            }, ",") + "]";
    }
    if (!aoiWkb.empty()) {
        query += " AND ST_Intersects(ST_GeomFromWkb('" + viewTxn->esc_raw(aoiWkb) + "',3395), the_geom)";
    }
    auto rows = viewTxn->exec(query);
    INFO() << "ViewFilterClause used to fetch ids returned rows: " << rows.size();
    std::vector<revision::DBID> oids;
    oids.reserve(rows.size());
    for (const auto& row : rows) {
        oids.emplace_back(row[0].as<revision::DBID>());
    }
    return oids;
}

std::vector<rf::ProxyFilterExpr>
makeRevisionsFilter(
    pgpool3::TransactionHandle& txn,
    pgpool3::TransactionHandle& viewTxn,
    filters::TExpressionId filterExprId,
    const std::string& aoiWkb,
    const StringSet& categoryIds)
{
    auto expr = filters::StoredExpression::load(*txn, filterExprId);
    auto storedRevFilter = expr.parsed().revisionFilter();
    if (storedRevFilter) {
        return {*storedRevFilter};
    }
    const auto oids = loadObjectIds(viewTxn, aoiWkb, expr.parsed().viewFilterClause(*viewTxn), categoryIds);
    const auto oidsBatches = maps::common::makeBatches(oids, MAX_OIDS_PER_BATCH);
    std::vector<rf::ProxyFilterExpr> filters;
    filters.reserve(oidsBatches.size());
    for (const auto& oidsBatch : oidsBatches) {
        filters.emplace_back(rf::ObjRevAttr::objectId().in(oidsBatch));
    }
    return filters;
}
} // namespace

std::vector<TCommitId> move(
        pgpool3::TransactionHandle& txn,
        pgpool3::TransactionHandle& viewTxn,
        revision::DBID branchId,
        TUserId author,
        const std::optional<filters::TExpressionId>& filterExprId,
        const std::string& aoiWkb,
        double dx, double dy,
        const StringSet& categoryIds,
        groupedit::actions::PolygonMode polygonMode)
{
    Session session(*txn, branchId);

    auto filters = filterExprId
        ? makeRevisionsFilter(txn, viewTxn, *filterExprId, aoiWkb, categoryIds)
        : std::vector<rf::ProxyFilterExpr>{rf::True()};

    std::optional<rf::ProxyFilterExpr> catFilter;
    if (!categoryIds.empty()) {
        std::vector<std::string> canonicalCategoryId;
        canonicalCategoryId.reserve(categoryIds.size());
        for (const auto& categoryId : categoryIds) {
            INFO() << "Moved cat: " << categoryId;
            canonicalCategoryId.emplace_back(CANONICAL_CAT_PREFFIX + categoryId);
        }
        catFilter = rf::Attr::definedAny(canonicalCategoryId);
    }

    std::vector<TCommitId> resultCommits;
    for (auto filter : filters) {
        if (catFilter) {
            filter = filter && *catFilter;
        }
        auto commits = perform(Action::Move, txn, [&](){
            return actions::moveObjects(session, filter, aoiWkb, dx, dy, author, polygonMode);
        });
        resultCommits.insert(resultCommits.end(), commits.begin(), commits.end());
    }
    return resultCommits;
}

std::vector<TCommitId> updateAttrs(
        pgpool3::TransactionHandle& txn,
        pgpool3::TransactionHandle& viewTxn,
        revision::DBID branchId,
        groupedit::TUserId author,
        filters::TExpressionId filterExprId,
        const std::optional<std::string>& aoiWkb,
        const json::Value& attrs)
{
    actions::UpdateAttrsAction action;

    for (auto key : attrs.fields()) {
        auto values = attrs[key];
        if (values.empty()) {
            action.removeAttribute(key);
        } else {
            for (auto val: values) {
                action.addAttributeValue(key, val.as<std::string>());
            }
        }
    }

    Session session(*txn, branchId);
    auto filters = makeRevisionsFilter(txn, viewTxn, filterExprId, aoiWkb ? *aoiWkb : std::string(), {});
    std::vector<TCommitId> resultCommits;
    for (auto filter : filters) {
        auto commits = perform(Action::UpdateAttrs, txn, [&](){
                return action.perform(
                    session,
                    aoiWkb
                        ? RevGeomFilter{filter, GeomPredicate::Within, *aoiWkb}
                        : RevGeomFilter{filter},
                    author);
            });
        resultCommits.insert(resultCommits.end(), commits.begin(), commits.end());
    }
    return resultCommits;
}

std::vector<TCommitId> deleteObjects(
        pgpool3::TransactionHandle& txn,
        pgpool3::TransactionHandle& viewTxn,
        revision::DBID branchId,
        groupedit::TUserId author,
        filters::TExpressionId filterExprId,
        const std::optional<std::string>& aoiWkb)
{
    auto filters = makeRevisionsFilter(txn, viewTxn, filterExprId,  aoiWkb ? *aoiWkb : std::string(), {});

    Session session(*txn, branchId);
    std::vector<TCommitId> resultCommits;
    for (auto filter : filters) {
        std::vector<RevGeomFilter> filtersWithGeom;
        if (aoiWkb) {
            filtersWithGeom.emplace_back(filter, GeomPredicate::Within, *aoiWkb);
        } else {
            filtersWithGeom.emplace_back(filter);
        }
        auto commits =
            perform(Action::SetState, txn, [&](){
                return actions::deleteObjects(
                    session,
                    filtersWithGeom,
                    author);
            });
        resultCommits.insert(resultCommits.end(), commits.begin(), commits.end());
    }

    return resultCommits;
}

Action actionFromString(const std::string& value)
{
    static const std::map<std::string, Action> nameToAction = {
        {"move", Action::Move},
        {"attributes", Action::UpdateAttrs},
        {"state", Action::SetState}
    };
    const auto itr = nameToAction.find(value);
    REQUIRE(itr != nameToAction.end(), "Invalid groupedit action: " + value);
    return itr->second;
}

std::string actionToString(Action action)
{
    switch (action) {
        case Action::Move: return "move";
        case Action::UpdateAttrs: return "attributes";
        case Action::SetState: return "state";
    }
    throw maps::RuntimeError()
            << "Invalid groupedit action value: " << static_cast<int>(action);
}

std::ostream& operator<<(std::ostream& os, Action action)
{
    return os << actionToString(action);
}

std::istream& operator>>(std::istream& is, Action& action)
{
    std::string str;
    is >> str;
    action = actionFromString(str);
    return is;
}

} // namespace maps::wiki::groupedit
