#include "commit.h"

#include "collection.h"
#include "objects/object.h"
#include "objects/junction.h"
#include "objects_cache.h"
#include "edit_notes.h"
#include "magic_strings.h"
#include "revisions_facade.h"
#include "last_commits.h"

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

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/revision/commit.h>
#include <yandex/maps/wiki/social/feedback/gateway_ro.h>

#include <boost/lexical_cast.hpp>

#include <map>

namespace maps::wiki {

namespace {

const std::map<std::string, StringSet> ACTION_TO_ACTION_NOTES {
    {common::COMMIT_PROPVAL_GROUP_MOVED, StringSet{
        common::COMMIT_PROPKEY_GROUP_MOVED_DLON,
        common::COMMIT_PROPKEY_GROUP_MOVED_DLAT,
        common::COMMIT_PROPKEY_GROUP_MOVED_OBJECTS_COUNT
    }},
    {common::COMMIT_PROPVAL_COMMIT_REVERTED, StringSet{
        common::COMMIT_PROPKEY_REVERT_REASON
    }}
};


GeoObjectCollection
geomMastersCollection(const GeoObjectCollection& objects, ObjectsCache& cache,
    TOIds& modifiedGeomElementsObjects)
{
    GeoObjectCollection masters;
    for (auto objectPtr : objects) {
        auto masterCatIds = objectPtr->category().masterCategoryIds(roles::filters::IsGeom);
        auto slaveGeomPartRoles = objectPtr->category().slaveRoleIds(roles::filters::IsGeom);
        bool objGeomUpdated =
            objectPtr->isModifiedGeom()
            || modifiedGeomElementsObjects.count(objectPtr->id())
            || !objectPtr->slaveRelations().diff(slaveGeomPartRoles).empty();
        for (const auto& masterRel : objectPtr->masterRelations().range()) {
            if (!masterCatIds.count(masterRel.categoryId())) {
                continue;
            }
            auto masterId = masterRel.id();
            masters.add(cache.getExisting(masterId));
            if (objectPtr->isCreated() || is<Junction>(objectPtr.get()))
                continue;
            if (objGeomUpdated) {
                modifiedGeomElementsObjects.insert(masterId);
            }
        }
    }
    return masters;
}

const std::map<RevertReason, std::string> REVERT_REASON_TO_STR = {
    {RevertReason::DuplicateCreation, "duplicate-creation"},
    {RevertReason::UnrealObjectCreation, "unreal-object-creation"},
    {RevertReason::UnfinishedObjectCreation, "unfinished-object-creation"},
    {RevertReason::ForbiddenObjectCreation, "forbidden-object-creation"},
    {RevertReason::IncorrectObjectCategory, "incorrect-object-category"},
    {RevertReason::IncorrectObjectAttr, "incorrect-object-attr"},
    {RevertReason::IncorrectObjectGeometry, "incorrect-object-geometry"},
    {RevertReason::UnchartedObjectCreation, "uncharted-object-creation"},
    {RevertReason::Vandalism, "vandalism"},
    {RevertReason::Self, "self"},
    {RevertReason::Other, "other"}
};

TCommitIds loadDraftApprovingCommitIds(
    const BranchContext& branchCtx,
    const std::list<revision::Commit>& commits,
    revision_meta::ApprovedQueueMode mode)
{
    TCommitIds draftCommitIds;
    for (const auto& commit : commits) {
        if (commit.state() == revision::CommitState::Draft) {
            draftCommitIds.insert(commit.id());
        }
    }

    if (draftCommitIds.empty()) {
        return {};
    }

    revision_meta::ApprovedQueue queue(branchCtx.txnCore(), mode);
    return queue.readCommitIds(draftCommitIds);
}

std::string commitState(
    const revision::Commit& commit,
    const TCommitIds& preApprovingCommitIds,
    const TCommitIds& approvingCommitIds)
{
    if (commit.state() == revision::CommitState::Approved) {
        return commit_state::APPROVED;
    }
    if (preApprovingCommitIds.count(commit.id())) {
        return commit_state::PREAPPROVING;
    }
    return approvingCommitIds.count(commit.id())
        ? commit_state::APPROVING
        : commit_state::DRAFT;
}

} //namespace

std::ostream&
operator << (std::ostream& os, const RevertReason& reason)
{
    os << REVERT_REASON_TO_STR.at(reason);
    return os;
}

std::istream&
operator >> (std::istream& is, RevertReason& reason)
{
    std::string s;
    is >> s;
    for (const auto& p: REVERT_REASON_TO_STR) {
        if (p.second == s) {
            reason = p.first;
            return is;
        }
    }
    THROW_WIKI_INTERNAL_ERROR("Unrecognized revert reason: " << s);
}

StringMap
createCommitNotes(const std::string& action, const StringMap& actionNotes,
    const GeoObjectCollection& objects, ObjectsCache& cache)
{
    StringMap ret;
    ret[common::COMMIT_PROPKEY_ACTION] = action;
    ret.insert(actionNotes.begin(), actionNotes.end());

    TOIds modifiedGeomElementsObjects;
    GeoObjectCollection geomMasters = geomMastersCollection(objects, cache, modifiedGeomElementsObjects);
    GeoObjectCollection notesObjects = objects;
    notesObjects.append(geomMasters);
    notesObjects.append(geomMastersCollection(geomMasters, cache, modifiedGeomElementsObjects));

    for (auto objectPtr : notesObjects) {
        const auto& editNotes = edit_notes::editNotes(objectPtr.get(), modifiedGeomElementsObjects);
        if (!editNotes.empty()) {
            ret[objectEditNotesKey(objectPtr->id())] =
                boost::join(editNotes, common::COMMIT_PROPKEY_ARRAYS_DELIMITER);
        }
    }

    ObjectPtr primaryObj = objects.primaryObject();
    if (primaryObj) {
        ret[primaryObjectKey(primaryObj->id())] = common::COMMIT_ATTR_TRUE;
    }
    return ret;
}

std::string
objectEditNotesKey(TOid objectId)
{
    return common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX + std::to_string(objectId);
}

std::string
primaryObjectKey(TOid objectId)
{
    return common::COMMIT_PROPKEY_PRIMARY_OBJECT_PREFIX + std::to_string(objectId);
}

TOid
objectIdFromEditNotesKey(const std::string& editNotesKey)
{
    ASSERT(editNotesKey.starts_with(common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX));
    return boost::lexical_cast<TOid>(editNotesKey.substr(
        common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX.length()));
}

TOid
primaryObjectIdFromEditNotesKey(const std::string& editNotesKey)
{
    ASSERT(editNotesKey.starts_with(common::COMMIT_PROPKEY_PRIMARY_OBJECT_PREFIX));
    return boost::lexical_cast<TOid>(editNotesKey.substr(
        common::COMMIT_PROPKEY_PRIMARY_OBJECT_PREFIX.length()));
}

std::string
commitAttributesObjectEditNotes(const StringMap& attributes, TOid objectId)
{
    auto it = attributes.find(objectEditNotesKey(objectId));
    return it == attributes.end() ? std::string() : it->second;
}

bool
hasEditNotes(const revision::Commit& commit)
{
    for (const auto& attrPair : commit.attributes()) {
        if (attrPair.first.starts_with(common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX)) {
            return true;
        }
    }
    return false;
}

TOIds
oidsWithNotes(const revision::Commit& commit)
{
    TOIds withNotes;
    for (const auto& attrPair : commit.attributes()) {
        if (attrPair.first.starts_with(common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX)) {
            withNotes.insert(objectIdFromEditNotesKey(attrPair.first));
        }
    }
    return withNotes;
}

boost::optional<RevertReason>
revertReason(const revision::Commit& commit)
{
    const auto& attrs = commit.attributes();
    auto it = attrs.find(common::COMMIT_PROPKEY_REVERT_REASON);
    if (it != attrs.end()) {
        return boost::lexical_cast<RevertReason>(it->second);
    }
    return boost::none;
}

edit_notes::EditNotesTree
objectEditNotesTree(const std::string& notesString)
{
    if (!notesString.empty()) {
        const auto& notesSet = split<StringSet>(notesString, common::COMMIT_PROPKEY_ARRAYS_DELIMITER[0]);
        return edit_notes::treeRepresentation(notesSet);
    }
    return edit_notes::EditNotesTree();
}

edit_notes::EditNotesTree
objectEditNotesTree(const revision::Commit& commit, TOid objectId)
{
    auto notesString = commitAttributesObjectEditNotes(commit.attributes(), objectId);
    return objectEditNotesTree(notesString);
}

StringMap
actionNotes(const revision::Commit& commit)
{
    const auto& action = commit.action();
    auto actionIt = ACTION_TO_ACTION_NOTES.find(action);
    if (actionIt == ACTION_TO_ACTION_NOTES.end()) {
        return {};
    }
    const auto& notesAttrs = actionIt->second;

    auto commitAttrs = commit.attributes();
    StringMap ret;
    for (auto it : commitAttrs) {
        if (notesAttrs.count(it.first)) {
            ret[it.first] = it.second;
        }
    }
    return ret;
}

boost::optional<TOid>
primaryObjectId(const revision::Commit& commit)
{
    const auto& attrs = commit.attributes();
    for (const auto& attrPair : attrs) {
        if (attrPair.first.starts_with(common::COMMIT_PROPKEY_PRIMARY_OBJECT_PREFIX)) {
            return primaryObjectIdFromEditNotesKey(attrPair.first);
        }
    }
    return boost::none;
}

TOIds
affectedObjectIds(const revision::Commit& commit)
{
    TOIds objectIds;
    const auto& attrs = commit.attributes();
    for (const auto& attrPair : attrs) {
        if (attrPair.first.starts_with(common::COMMIT_PROPKEY_EDIT_NOTES_PREFIX)) {
            objectIds.insert(objectIdFromEditNotesKey(attrPair.first));
        }
    }
    return objectIds;
}

revision::Commit
recentAffectingCommit(ObjectsCache& cache, const TRevisionId& objRevId)
{
    auto objectId = objRevId.objectId();
    auto commitId = objRevId.commitId();

    StringVec commitAttrs { objectEditNotesKey(objectId), primaryObjectKey(objectId) };
    auto commitsFilter =
        revision::filters::CommitAttribute::definedAny(commitAttrs)
        && revision::filters::CommitAttr::isVisible(cache.branchContext().branch);

    auto commits = revision::Commit::load(cache.workCore(), commitsFilter);
    auto it = std::find_if(
        commits.begin(),
        commits.end(),
        [&](const revision::Commit& commit) { return commit.id() == commitId; }
    );

    auto maxCommit = it == commits.end()
        ? revision::Commit::load(cache.workCore(), commitId)
        : *it;
    for (const auto& commit : commits) {
        if (commit.id() > maxCommit.id()) {
            maxCommit = commit;
        }
    }
    return maxCommit;
}

revision::Commit
firstCommit(ObjectsCache& cache, TOid objectId)
{
    auto revFilter =
        revision::filters::ObjRevAttr::objectId() == objectId
        && revision::filters::ObjRevAttr::prevCommitId().isZero();
    auto relRevs = cache.revisionsFacade().gateway().historicalSnapshot(
        0, cache.headCommitId()).revisionIdsByFilter(revFilter);
    REQUIRE(relRevs.size() == 1,
        "Invalid first revisions count for oid: "
            << objectId << "(" << relRevs.size() << ").");

    return revision::Commit::load(cache.workCore(), relRevs[0].commitId());
}

TBranchId
sourceBranchId(const revision::Commit& commit)
{
    return commit.inTrunk() ? revision::TRUNK_BRANCH_ID : commit.stableBranchId();
}

CommitModel::CommitModel(revision::Commit commit)
    : commit_(std::move(commit))
    , primaryObjectId_(primaryObjectId(commit_))
    , contextPrimaryObjectId_(primaryObjectId_)
{}

const revision::Commit&
CommitModel::commit() const
{
    return commit_;
}

const boost::optional<TOid>&
CommitModel::contextPrimaryObjectId() const
{
    return contextPrimaryObjectId_;
}

bool
CommitModel::primary() const
{
    return (!primaryObjectId_ || primaryObjectId_ == contextPrimaryObjectId_);
}

boost::optional<edit_notes::EditNotesTree>
CommitModel::editNotesTree() const
{
    if (!contextPrimaryObjectId_) {
        return boost::none;
    }
    return objectEditNotesTree(commit_, *contextPrimaryObjectId_);
}

const boost::optional<std::string>&
CommitModel::state() const
{
    return state_;
}

const boost::optional<approve_status::ApproveStatus>&
CommitModel::approveStatus() const
{
    return approveStatus_;
}

const boost::optional<bool>&
CommitModel::last() const
{
    return last_;
}

const boost::optional<TId>&
CommitModel::feedbackTaskId() const
{
    return feedbackTaskId_;
}

const boost::optional<bool>&
CommitModel::isRevertible() const
{
    return isRevertible_;
}

void
CommitModel::setCustomContextObjectId(TOid customContextObjectId)
{
    contextPrimaryObjectId_ = customContextObjectId;
}

void
CommitModel::setupState(const BranchContext& branchCtx)
{
    auto preApprovingCommitIds = loadDraftApprovingCommitIds(
        branchCtx,
        std::list<revision::Commit>{commit_},
        revision_meta::ApprovedQueueMode::PreApprove);

    auto approvingCommitIds = loadDraftApprovingCommitIds(
        branchCtx,
        std::list<revision::Commit>{commit_},
        revision_meta::ApprovedQueueMode::ViewAttrs);

    state_ = commitState(
        commit_,
        preApprovingCommitIds,
        approvingCommitIds);
}

void
CommitModel::setupFeedbackTaskId(const BranchContext& branchCtx)
{
    auto feedbackTask = social::feedback::GatewayRO(branchCtx.txnSocial())
        .taskByCommitId(commit_.id());
    if (feedbackTask) {
        feedbackTaskId_ = feedbackTask->id();
    }
}

void
CommitModel::setApproveStatus(approve_status::ApproveStatus approveStatus)
{
    approveStatus_ = approveStatus;
}

void
CommitModel::setupApproveStatus(const BranchContext& branchCtx)
{
    if (branchCtx.branch.id() == revision::TRUNK_BRANCH_ID) {
        approveStatus_ = approve_status::evalApproveStatus(branchCtx, commit_);
    }
}

void
CommitModel::setIsRevertible(bool isRevertible)
{
    isRevertible_ = isRevertible;
}

void
BatchCommitPreparedFields::prepareStates(
    const BranchContext& branchCtx,
    const std::list<revision::Commit>& commits)
{
    auto preApprovingCommitIds = loadDraftApprovingCommitIds(
        branchCtx,
        commits,
        revision_meta::ApprovedQueueMode::PreApprove);

    auto approvingCommitIds = loadDraftApprovingCommitIds(
        branchCtx,
        commits,
        revision_meta::ApprovedQueueMode::ViewAttrs);

    for (const auto& commit: commits) {
        commitIdToState_.emplace(
            commit.id(),
            commitState(
                commit,
                preApprovingCommitIds,
                approvingCommitIds));
    }
}

void
BatchCommitPreparedFields::prepareApproveStatuses(
    const BranchContext& branchCtx,
    const std::list<revision::Commit>& commits)
{
    commitIdToApproveStatus_ =
        approve_status::evalApproveStatuses(branchCtx, commits);
}

void
BatchCommitPreparedFields::prepareLastFlags(
    const BranchContext& branchCtx,
    const std::list<revision::Commit>& commits,
    TBranchId branchId)
{
    lastCommitIds_ = evalLastCommitIds(branchCtx.txnCore(), commits, branchId);
    lastCommitsWasPrepared_ = true;
}


void
BatchCommitPreparedFields::fillCommitModel(CommitModel& commitModel)
{
    const auto& commit = commitModel.commit();
    if (commitIdToState_.count(commit.id())) {
        commitModel.state_ = commitIdToState_.at(commit.id());
    }
    if (commitIdToApproveStatus_.count(commit.id())) {
        commitModel.approveStatus_ = commitIdToApproveStatus_.at(commit.id());
    }
    if (lastCommitsWasPrepared_) {
        commitModel.last_ = lastCommitIds_.count(commit.id()) > 0;
    }
}

} // namespace maps::wiki
