#include "db_helpers.h"

#include <maps/wikimap/mapspro/services/editor/src/branch_helpers.h>
#include <maps/wikimap/mapspro/services/editor/src/magic_strings.h>
#include <maps/wikimap/mapspro/services/editor/src/commit.h>

#include <maps/libs/pgpool/include/pool_configuration.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/category_template.h>
#include <maps/wikimap/mapspro/libs/revision_meta/include/branch_lock_ids.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

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

namespace maps {
namespace wiki {
namespace sync {

namespace {

std::string
toString(const StringSet& values)
{
    std::string res;
    bool first = true;
    for (const auto& val : values) {
        (((res += (first ? "" : ",")) += "'") += val) += "'";
        first = false;
    }
    return res;
}

template <typename Revisions>
TOIds
revisionsToOids(const Revisions& revisions)
{
    TOIds oids;
    for (const auto& rev : revisions) {
        oids.insert(rev.id().objectId());
    }
    return oids;
}

template <typename RevisionIds>
TOIds
revisionIdsToOids(const RevisionIds& revisionIds)
{
    TOIds oids;
    for (const auto& revId : revisionIds) {
        oids.insert(revId.objectId());
    }
    return oids;
}

} // namespace

std::string masterConnectionString(const pgpool3::PoolState& state)
{
    REQUIRE(state.configuration.master(), "no master configured");
    pgpool3::InstanceId master = *state.configuration.master();

    return
        "host=" + master.host()
        + " port=" + std::to_string(master.port())
        + " " + state.connParams;
}

bool isSplittedViewLabels(TBranchId branchId)
{
    auto& poolView = cfg()->poolView(branchId);
    auto& poolLabels = cfg()->poolLabels(branchId);

    return masterConnectionString(poolView.state()) !=
           masterConnectionString(poolLabels.state());
}

BranchLocker::BranchLocker(pgpool3::TransactionHandle& txn)
    : pooledTxn_(&txn)
{}

BranchLocker::BranchLocker(const std::string& connectionString)
    : pooledTxn_(0)
{
    conn_ = make_unique<pqxx::connection>(connectionString);
    txn_  = make_unique<pqxx::work>(*conn_);
}

BranchLocker::~BranchLocker()
{}

pqxx::transaction_base&
BranchLocker::work() const
{
    if (pooledTxn_) {
        return *(*pooledTxn_);
    }
    return *txn_;
}

void
BranchLocker::lockBranchPersistently(
    const revision::Branch& branch,
    revision::Branch::LockType lockType) const
{
    // separate shared and exclusive mode execution
    branch.lock(
        work(),
        revision_meta::EDITOR_TOOL_LOCK_ID,
        revision::Branch::LockMode::Wait,
        lockType);

    // wait fast processes
    branch.lock(
        work(),
        revision_meta::GLOBAL_LOCK_ID,
        revision::Branch::LockMode::Wait,
        revision::Branch::LockType::Shared);
}

void
BranchLocker::lockWaitExclusive(
    const revision::Branch& branch,
    revision::Branch::LockId lockId) const
{
    branch.lock(
        work(),
        lockId,
        revision::Branch::LockMode::Wait,
        revision::Branch::LockType::Exclusive);
}

bool
BranchLocker::tryLockExclusive(
    const revision::Branch& branch,
    revision::Branch::LockId lockId) const
{
    return branch.tryLock(
        work(),
        lockId,
        revision::Branch::LockType::Exclusive);
}

RelationsCategories
relationsCategories(
    const StringSet& objectsCategories,
    const StringSet& availableCategories)
{
    StringSet masterCategories, slaveCategories;
    for (const auto& catId : objectsCategories) {
        const auto& category = cfg()->editor()->categories()[catId];
        auto masterCatIds =  category.masterCategoryIds(roles::filters::All);
        masterCategories.insert(masterCatIds.begin(), masterCatIds.end());
        auto slaveCatIds =  category.slaveCategoryIds(roles::filters::All);
        slaveCategories.insert(slaveCatIds.begin(), slaveCatIds.end());
    }
    RelationsCategories result;
    auto masterInserter = std::inserter(result.master, result.master.end());
    auto slaveInserter = std::inserter(result.slave, result.slave.end());
    std::set_intersection(
        masterCategories.begin(), masterCategories.end(),
        availableCategories.begin(), availableCategories.end(),
        masterInserter);
    std::set_intersection(
        slaveCategories.begin(), slaveCategories.end(),
        availableCategories.begin(), availableCategories.end(),
        slaveInserter);
    return result;
}

revision::filters::ProxyFilterExpr
headObjectIdsFilter(const StringSet& categories)
{
    if (categories.empty()) {
        return rf::False();
    }

    return rf::ProxyFilterExpr(
        rf::Attr::definedAny(plainCategoryIdsToCanonical(categories)) &&
        rf::ObjRevAttr::isNotDeleted() &&
        rf::ObjRevAttr::isNotRelation());
}

revision::filters::ProxyFilterExpr
headObjectIdsFilter(const std::string& categoryId)
{
    const auto& category = cfg()->editor()->categories()[categoryId];
    const auto& catTemplate = category.categoryTemplate();

    rf::ProxyFilterExpr filter(
        rf::Attr(plainCategoryIdToCanonical(categoryId)).defined() &&
        rf::ObjRevAttr::isNotDeleted() &&
        rf::ObjRevAttr::isNotRelation());

    if (catTemplate.hasGeometryType()) {
        filter &= revision::filters::Geom::defined();
    } else {
        filter &= !revision::filters::Geom::defined();
    }
    return filter;
}

std::string
objectsVRevisionsFilter(const StringSet& categories)
{
    return categories.empty()
        ? STR_FALSE
        : STR_DOMAIN_ATTRS + " ?| ARRAY["
            + toString(plainCategoryIdsToCanonical(categories)) + "]";
}

std::string
suggestVRevisionsFilter(const StringSet& categories)
{
    return categories.empty()
        ? STR_FALSE
        : STR_CATEGORIES + " ?| ARRAY["
            + toString(plainCategoryIdsToCanonical(categories)) + "]";
}

revision::filters::ProxyFilterExpr
headRelationIdsFilter(const StringSet& objectsCategories,
    const RelationsCategories& relCategories, const StringVec& forbiddenRoles)
{
    if (objectsCategories.empty() ||
        (relCategories.master.empty() && relCategories.slave.empty()))
    {
        return rf::False();
    }

    auto commonFilter = rf::ProxyFilterExpr(
        rf::ObjRevAttr::isNotDeleted() && rf::ObjRevAttr::isRelation());

    if (!forbiddenRoles.empty()) {
        commonFilter &= !rf::Attr(ATTR_REL_ROLE).in(forbiddenRoles);
    }

    if (relCategories.master == relCategories.slave &&
        relCategories.master == objectsCategories)
    {
        commonFilter &=
            rf::Attr(ATTR_REL_MASTER).in(objectsCategories) &&
            rf::Attr(ATTR_REL_SLAVE).in(objectsCategories);
        return commonFilter;
    }

    rf::ProxyFilterExpr categoriesFilter = rf::False();
    if (!relCategories.master.empty()) {
        categoriesFilter |=
            rf::Attr(ATTR_REL_MASTER).in(relCategories.master) &&
            rf::Attr(ATTR_REL_SLAVE).in(objectsCategories);
    }
    if (!relCategories.slave.empty()) {
        categoriesFilter |=
            rf::Attr(ATTR_REL_MASTER).in(objectsCategories) &&
            rf::Attr(ATTR_REL_SLAVE).in(relCategories.slave);
    }
    return std::move(categoriesFilter) && std::move(commonFilter);
}

namespace {

std::string
hstoreValueInFilter(const std::string& hstoreName, const std::string& key,
    const StringSet& values)
{
    if (values.empty()) {
        return STR_FALSE;
    }
    return "("
        + hstoreName + "->'" + key + "'" + (values.size() == 1
            ? "='" + *values.begin() + "'"
            : " IN (" + toString(values) + ")")
        + ")";
}

} // namespace

std::string
masterCoreConnectionString()
{
    return masterConnectionString(cfg()->poolCore().state());
}

std::string
relationsVRevisionsFilter(
    const StringSet& objectsCategories,
    const RelationsCategories& relCategories)
{
    if (objectsCategories.empty() ||
        (relCategories.master.empty() && relCategories.slave.empty()))
    {
        return STR_FALSE;
    }

    if (relCategories.master == relCategories.slave &&
        relCategories.master == objectsCategories)
    {
        return hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_MASTER, objectsCategories)
            + " AND "
            + hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_SLAVE, objectsCategories);
    }

    std::string masterFilter =
        hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_MASTER, relCategories.master);
    std::string slaveFilter =
        hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_SLAVE, relCategories.slave);

    std::string result;
    if (!masterFilter.empty()) {
        result += masterFilter + " AND "
            + hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_SLAVE, objectsCategories);
    }
    if (!slaveFilter.empty()) {
        if (!result.empty()) {
            result += " OR ";
        }
        result += hstoreValueInFilter(STR_DOMAIN_ATTRS, ATTR_REL_MASTER, objectsCategories)
            + " AND " + slaveFilter;
    }
    return result;
}

TOIds
headObjectIds(
    pqxx::transaction_base& work,
    TCommitId commitId,
    const revision::Branch& branch,
    const revision::filters::ProxyFilterExpr& filter)
{
    if (!commitId) {
        return {};
    }

    auto snapshot = revision::RevisionsGateway(work, branch).snapshot(commitId);
    return revisionIdsToOids(snapshot.revisionIdsByFilter(filter));
}

boost::optional<TOIds>
tryLoadHeadObjectIds(
    pqxx::transaction_base& work,
    TCommitId commitId, const revision::Branch& branch,
    const revision::filters::ProxyFilterExpr& filter, size_t limit)
{
    if (!commitId) {
        return {};
    }

    auto snapshot = revision::RevisionsGateway(work, branch).snapshot(commitId);
    auto revIds = snapshot.tryLoadRevisionIdsByFilter(filter, limit);
    if (!revIds) {
        return boost::none;
    }
    return revisionIdsToOids(*revIds);
}

TCommitId
findNonServiceStartCommitId(
    pqxx::transaction_base& work, const TCommitIds& commitIds)
{
    const size_t BULK_COMMITS_SIZE = 1000;

    auto searcher = [&](const TCommitIds& bulkCommitIds)
    {
        rf::CommitAttribute attrAction(common::COMMIT_PROPKEY_ACTION);
        auto filter = rf::CommitAttr::id().in(bulkCommitIds) &&
            !(attrAction.defined() && attrAction == common::COMMIT_PROPVAL_ACTION_SERVICE);

        TCommitId minCommitId = 0;
        for (const auto& commit : revision::Commit::load(work, filter)) {
            minCommitId = minCommitId
                ? std::min(minCommitId, commit.id())
                : commit.id();
        }
        return minCommitId;
    };

    TCommitIds bulkCommitIds;
    for (auto commitId : commitIds) {
        bulkCommitIds.insert(commitId);
        if (bulkCommitIds.size() >= BULK_COMMITS_SIZE) {
            auto commitId = searcher(bulkCommitIds);
            if (commitId) {
                return commitId;
            }
            bulkCommitIds.clear();
        }
    }
    return bulkCommitIds.empty() ? 0 : searcher(bulkCommitIds);
}

size_t
maxThreadsCount(TBranchId branchId)
{
    auto poolStateCore = cfg()->poolCore().state();
    auto poolStateView = cfg()->poolView(branchId).state();
    auto poolStateLabels = cfg()->poolLabels(branchId).state();

    return std::min(
        std::min(
            poolStateCore.constants.masterMaxSize,
            poolStateView.constants.masterMaxSize),
        poolStateLabels.constants.masterMaxSize);
}

std::set<std::string>
findBranchViews(pqxx::transaction_base& work, TBranchId branchId)
{
    auto schemaName = vrevisionsSchemaName(branchId);

    auto checkBranchMaskViewQuery = "SELECT viewname "
        "FROM pg_views "
        "WHERE schemaname=" + work.quote(schemaName) +
        "AND viewname ~ '^objects_[cplar]$'";

    std::set<std::string> views;
    for (const auto& row : work.exec(checkBranchMaskViewQuery)) {
        views.emplace(row[0].as<std::string>());
    }
    return views;
}

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