#include "commit_revert.h"
#include "commit_relations.h"
#include "commit_worker.h"
#include "helpers.h"
#include "query_generator.h"
#include "reader_impl.h"
#include "result_interpreter.h"
#include "sql_strings.h"
#include "filter_generators.h"

#include <yandex/maps/wiki/revision/exception.h>
#include <yandex/maps/wiki/revision/objectrevision.h>
#include <yandex/maps/wiki/revision/revisionid.h>

#include <boost/format.hpp>

#include <list>
#include <map>
#include <utility>
#include <vector>

namespace maps::wiki::revision {

using namespace helpers;

namespace {


using NewRevisionData = RevisionsGateway::NewRevisionData;
using NewRevisionDataList = std::list<NewRevisionData>;


struct CommitIdRange {
    DBID toRestore;
    DBID last;
    bool lastDeleted;
};

using ObjectIdToCommitIdRange = std::map<DBID, CommitIdRange>;


struct RevisionPair {
    ObjectRevision toRestore;
    ObjectRevision last;
};

using ObjectIdToRevisionPair = std::map<DBID, RevisionPair>;


const boost::format QUERY_REVISION_DATA (
// %1% is commit id filter.
    "SELECT object_id, commit_id, prev_commit_id, deleted"
    "    FROM revision.object_revision"
    "    WHERE %1%"
);

const boost::format QUERY_REVERTED_COMMIT_IDS (
// %1% is commit id filter.
    "SELECT attributes -> '" + sql::col::ATTR_REVERTED_COMMIT_IDS + "'"
    "   FROM revision.commit"
    "   WHERE attributes ? '" + sql::col::ATTR_REVERTED_COMMIT_IDS + "' AND " +
    "       %1%"
);

const boost::format QUERY_UPDATE_REVERTED_BY (
// %1% is reverted by field
// %2% reverting commit id
// %3% is commit id filter.
    "UPDATE revision.commit SET"
    " attributes = attributes || hstore('%1%', coalesce(attributes->'%1%' || ',', '') || '%2%')"
    " WHERE %3%"
);

const boost::format QUERY_ALREADY_REVERTED_DIRECTLY_COMMIT_IDS (
// %1% is commit id filter.
    "SELECT id"
    "   FROM revision.commit"
    "   WHERE attributes ? '" + sql::col::ATTR_REVERTING_DIRECTLY_COMMIT_ID + "' AND " +
    "       %1%"
);

// https://wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/revert#obrabotkapredydushhixrezultatovrabotyrevert-a
DBIDSet getAlreadyRevertedCommitIds(Transaction& work, const DBIDSet& dependentCommitIds) {
    boost::format query = QUERY_REVERTED_COMMIT_IDS;
    query % QueryGenerator::buildFilterByAttrValues(sql::col::ID, dependentCommitIds);
    DBIDSet revertedCommitIds;
    for (const auto& row: work.exec(query.str())) {
        const DBIDSet ids = stringToDBIDSet(row[0].as<std::string>());
        std::vector<DBID> intersection;
        std::set_intersection(ids.begin(), ids.end(),
            dependentCommitIds.begin(), dependentCommitIds.end(),
            std::back_inserter(intersection));
        if (!intersection.empty()) {
            revertedCommitIds.insert(ids.begin(), ids.end());
        }
    }
    return revertedCommitIds;
}

// https://wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/revert#poiskgranicistoriiobekta
ObjectIdToCommitIdRange getCommitIdRanges(Transaction& work, const DBIDSet& commitIds)
{
    ObjectIdToCommitIdRange ranges;
    boost::format query = QUERY_REVISION_DATA;
    query % QueryGenerator::buildFilterByAttrValues(sql::col::COMMIT_ID, commitIds);
    for (const auto& row : work.exec(query.str())) {
        const DBID objectId = row[sql::col::OBJECT_ID].as<DBID>();
        const DBID commitId = row[sql::col::COMMIT_ID].as<DBID>();
        const DBID prevCommitId = row[sql::col::PREV_COMMIT_ID].as<DBID>();
        const bool deleted = row[sql::col::IS_DELETED].as<bool>();
        auto insertion = ranges.insert({objectId, {prevCommitId, commitId, deleted}});
        if (!insertion.second) {
            CommitIdRange& range = insertion.first->second;
            if (prevCommitId < range.toRestore) {
                range.toRestore = prevCommitId;
            }
            if (range.last < commitId) {
                range.last = commitId;
                range.lastDeleted = deleted;
            }
        }
    }
    return ranges;
}

bool equalContent(const ObjectRevision& lhs, const ObjectRevision& rhs)
{
    return (lhs.data().deleted == rhs.data().deleted) &&

        (lhs.data().relationData == rhs.data().relationData) &&

        (lhs.revisionData().attributesId == rhs.revisionData().attributesId ||
            lhs.data().attributes == rhs.data().attributes) &&

        (lhs.revisionData().descriptionId == rhs.revisionData().descriptionId ||
            lhs.data().description == rhs.data().description) &&

        (lhs.revisionData().geometryId == rhs.revisionData().geometryId ||
            lhs.data().geometry == rhs.data().geometry);
}

ObjectIdToRevisionPair loadRevisionPairs(const ReaderImpl& reader,
    const ObjectIdToCommitIdRange& ranges)
{
    std::vector<RevisionID> toRestoreIds;
    std::vector<RevisionID> lastIds;

    toRestoreIds.reserve(ranges.size());
    lastIds.reserve(ranges.size());

    for (const auto& pair: ranges) {
        const DBID& objectId = pair.first;
        const CommitIdRange& range = pair.second;
        toRestoreIds.emplace_back(objectId, range.toRestore);
        lastIds.emplace_back(objectId, range.last);
    }

    auto toRestoreRevisions = reader.loadRevisions(
        LoadLimitations::None,
        NO_SNAPSHOT_ID,
        NO_LIMIT,
        nullptr, // no basic filter
        CheckResultSize::Yes,
        filters::makeFilterForRevisionIdBatch<maps::common::Batch<RevisionIds>>,
        toRestoreIds
    );

    auto lastRevisions = reader.loadRevisions(
        LoadLimitations::None,
        NO_SNAPSHOT_ID,
        NO_LIMIT,
        nullptr, // no basic filter
        CheckResultSize::Yes,
        filters::makeFilterForRevisionIdBatch<maps::common::Batch<RevisionIds>>,
        lastIds
    );

    ObjectIdToRevisionPair revisionPairs;
    auto toRestoreIt = toRestoreRevisions.begin();
    auto lastIt = lastRevisions.begin();
    while(toRestoreIt != toRestoreRevisions.end() && lastIt != lastRevisions.end()) {
        const DBID& lastObjectId = lastIt->id().objectId();
        REQUIRE(
            toRestoreIt->id().objectId() == lastObjectId,
            "Some error has occurred while loading object " << lastObjectId
        );
        revisionPairs.insert({lastObjectId, {*toRestoreIt, *lastIt}});
        ++toRestoreIt;
        ++lastIt;
    }

    REQUIRE(
        toRestoreIt == toRestoreRevisions.end() && lastIt == lastRevisions.end(),
         "Some error has occurred while loading objects"
    );

    return revisionPairs;
}

// https://beta.wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/revert/#dedublicirovaniesvjazejj
void removeRelationDuplicates(const ReaderImpl& reader, ObjectIdToRevisionPair& revisionPairs, const DBIDSet& commitIdsToRevert)
{
    using Iter = ObjectIdToRevisionPair::iterator;
    using Iters = std::list<Iter>;

    std::map<RelationData, Iters> relationToIters;
    for (auto it = revisionPairs.begin(); it != revisionPairs.end(); ) {
        const ObjectData& data = it->second.toRestore.data();
        if (data.revisionType() == RevisionType::Relation && !data.deleted) {
            Iters& iters = relationToIters[*data.relationData];
            const bool anyHasEqualAttrs = std::any_of(iters.begin(), iters.end(),
                [&](const Iter& iter)
                {
                    return *iter->second.toRestore.data().attributes == *data.attributes;
                }
            );
            if (anyHasEqualAttrs) {
                it = revisionPairs.erase(it);
                continue;
            }
            iters.push_back(it);
        }
        ++it;
    }

    std::vector<RelationData> relations;

    relations.reserve(relationToIters.size());
    for (const auto& pair: relationToIters) {
        relations.push_back(pair.first);
    }

    static const auto basicFilter = filters::ObjRevAttr::isNotDeleted();

    const Revisions potentialDuplicates = reader.loadRevisions(
        LoadLimitations::Snapshot,
        NO_SNAPSHOT_ID,
        NO_LIMIT,
        &basicFilter,
        CheckResultSize::No,
        filters::makeFilterForRelationDataBatch<maps::common::Batch<std::vector<RelationData>>>,
        relations
    );

    for (const auto& revision: potentialDuplicates) {
        if (commitIdsToRevert.count(revision.id().commitId())) {
            continue;
        }

        const DBID& objectId = revision.id().objectId();
        const ObjectData& data = revision.data();
        Iters& iters = relationToIters[*data.relationData];
        auto it = std::find_if(iters.begin(), iters.end(),
            [&](const Iter& iter)
            {
                return iter->second.toRestore.id().objectId() != objectId &&
                    *iter->second.toRestore.data().attributes == *data.attributes;
            }
        );
        if (it != iters.end()) {
            revisionPairs.erase(*it);
            iters.erase(it);
        }
    }
}

// https://wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/revert#sozdanieotkatyvajushhejjpravki
NewRevisionDataList createRevertRevisionsInTrunk(Transaction& work, const DBIDSet& commitIds)
{
    NewRevisionDataList revisions;
    ReaderImpl reader {
        work, BranchType::Trunk, TRUNK_BRANCH_ID, DescriptionLoadingMode::Load
    };
    ObjectIdToCommitIdRange ranges = getCommitIdRanges(work, commitIds);
    for (auto it = ranges.begin(); it != ranges.end(); ) {
        const CommitIdRange& range = it->second;
        if (!range.toRestore) {
            if (!range.lastDeleted) {
                ObjectData data;
                data.deleted = true;
                revisions.emplace_back(RevisionID{it->first, range.last}, data);
            }
            it = ranges.erase(it);
        } else {
            ++it;
        }
    }

    ObjectIdToRevisionPair revisionPairs = loadRevisionPairs(reader, ranges);
    removeRelationDuplicates(reader, revisionPairs, commitIds);

    for (const auto& pair: revisionPairs) {
        const ObjectRevision& toRestore = pair.second.toRestore;
        const ObjectRevision& last = pair.second.last;
        if (!(last.data().deleted && toRestore.data().deleted)) {
            if (!equalContent(toRestore, last)) {
                revisions.emplace_back(last.id(), toRestore.data());
            }
        }
    }
    return revisions;
}

void checkAlreadyRevertedCommits(const DBIDSet& revertedCommitIds,
    const DBIDSet& commitIds)
{
    DBIDSet intersection;
    std::set_intersection(revertedCommitIds.begin(), revertedCommitIds.end(),
        commitIds.begin(), commitIds.end(),
        std::inserter(intersection, intersection.begin()));
    if (!intersection.empty()) {
        throw AlreadyRevertedCommitsException() << "Already reverted: " <<
            valuesToString(intersection);
    }
}

void checkAlreadyRevertedDirectlyCommits(Transaction& work, const DBIDSet& commitIds)
{
    boost::format query = QUERY_ALREADY_REVERTED_DIRECTLY_COMMIT_IDS;
    query % QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIds);
    DBIDSet alreadyRevertedDirectlyIds;
    for (const auto& row: work.exec(query.str())) {
        alreadyRevertedDirectlyIds.insert(row[0].as<DBID>());
    }
    if (!alreadyRevertedDirectlyIds.empty()) {
        throw AlreadyRevertedDirectlyCommitsException() << "Already reverted directly: " <<
            valuesToString(alreadyRevertedDirectlyIds);
    }
}

void addCommitIdsToAttribute(
    Transaction& work,
    const std::string& attrName,
    const DBID& idToAdd,
    const DBIDSet& commitIdsToUpdate)
{
    boost::format query = QUERY_UPDATE_REVERTED_BY;
    query
        % attrName
        % idToAdd
        % QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIdsToUpdate);
    work.exec(query.str());
}

void
lockCommits(pqxx::transaction_base& work, const DBIDSet& commitIds)
{
    if (commitIds.empty()) {
        return;
    }

    work.exec(
        "SELECT " + sql::col::ID +
        " FROM " + sql::table::COMMIT +
        " WHERE " + QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIds) +
        " ORDER BY " + sql::col::ID + " FOR UPDATE");
}

} // end of anonymous namespace

DBIDSet getRevertedCommitIds(Transaction& work, const DBIDSet& commitIds)
{
    DBIDSet revertedCommitIds;
    if (!commitIds.empty()) {
        boost::format query = QUERY_REVERTED_COMMIT_IDS;
        query % QueryGenerator::buildFilterByAttrValues(sql::col::ID, commitIds);
        for (const auto& row: work.exec(query.str())) {
            const DBIDSet ids = stringToDBIDSet(row[0].as<std::string>());
            revertedCommitIds.insert(ids.begin(), ids.end());
        }
    }
    return revertedCommitIds;
}

RevertCommitsResult revertCommitsInTrunk(Transaction& work, const DBIDSet& commitIds,
    UserID userId, Attributes commitAttributes)
{
    checkAlreadyRevertedDirectlyCommits(work, commitIds);
    const DBIDSet dependentCommitIds = findDependentCommitsInTrunk(work, commitIds);
    const DBIDSet revertedCommitIds = getAlreadyRevertedCommitIds(work, dependentCommitIds);
    checkAlreadyRevertedCommits(revertedCommitIds, commitIds);
    const DBIDSet toRevert = join(dependentCommitIds, revertedCommitIds);
    commitAttributes[sql::col::ATTR_REVERTED_COMMIT_IDS] = valuesToString(toRevert);
    commitAttributes[sql::col::ATTR_REVERTED_DIRECTLY_COMMIT_IDS] = valuesToString(commitIds);
    const NewRevisionDataList revisions = createRevertRevisionsInTrunk(work, toRevert);
    const ConstRange<NewRevisionData> range{revisions};

    CommitWorker committer(work,  range, BranchType::Trunk, TRUNK_BRANCH_ID);
    auto commitData = committer.createCommit(userId, PreparedCommitAttributes(work, commitAttributes));

    lockCommits(work, join(toRevert, commitIds));

    addCommitIdsToAttribute(
        work,
        sql::col::ATTR_REVERTING_COMMIT_IDS,
        commitData.id,
        toRevert);
    addCommitIdsToAttribute(
        work,
        sql::col::ATTR_REVERTING_DIRECTLY_COMMIT_ID,
        commitData.id,
        commitIds);

    return {
        std::move(commitData),
        toRevert
    };
}


} // namespace maps::wiki::revision
