#include "queue.h"

#include "common.h"
#include "config.h"
#include "diff_alert.h"
#include "feedback.h"
#include "helpers.h"
#include "message.h"
#include "names.h"
#include "object_helpers.h"
#include "recent_patches.h"
#include "stat.h"
#include "patches_context.h"

#include <maps/wikimap/mapspro/libs/poi_feed/include/magic_strings.h>
#include <maps/wikimap/mapspro/libs/editor_client/include/instance.h>
#include <maps/wikimap/mapspro/libs/editor_client/include/exception.h>

#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/revision/commit.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/threadutils/threadpool.h>
#include <yandex/maps/wiki/threadutils/executor.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/log8/include/log8.h>


namespace maps::wiki::merge_poi {
namespace {

const DispClass maxImportantDispClass = 4;

const size_t THREADS_COUNT = 6;
const size_t PATCHES_BATCH_SIZE = 1000;
const double INDOOR_MAX_DISTANCE = 50.0;

double distance(
    const geolib3::Point2& pt1,
    const poi_feed::FeedObjectData::Position& p2)
{
    const geolib3::Point2 pt2 {p2.lon, p2.lat};
    return geolib3::geoDistance(pt1, pt2);
}

PatchResult
deleteEditorObject(
    StatData& statistics,
    editor_client::Instance& editorInstance,
    editor_client::Instance& editorModeratedInstance,
    FeedbackClient &feedbackClient,
    editor_client::BasicEditorObject& object,
    const poi_feed::FeedObjectData& patch,
    const std::unordered_set<poi_feed::ObjectId>& objectIdsForActiveTasks,
    const std::unordered_set<poi_feed::ObjectId>& notYetToDelete,
    const poi_feed::FeedSettings& feedSettings,
    const Config& cfg)
{
    try {
        if (isProtected(object)) {
            return { Resolution::Rejected, {message::DELETE_PROTECTED_BY_ATTRIBUTE} };
        }
        if (patch.permalink() != permalink(object)) {
            if (!rubricId(object)) {
                WARN() << "Update permalink instead of delete can't update object without rubric " << patch.nmapsId();
                return { Resolution::Failed, {}};
            }
            setPermalink(object, patch.permalink());
            editorInstance.saveObject(object);
            INFO() << "Updated permalink instead of delete: " << patch.nmapsId();
            return { Resolution::Applied, {message::UPDATED_PERMALINK}};
        }
        if (notYetToDelete.count(patch.nmapsId())) {
            return { Resolution::Skipped, {message::NOT_YET_TO_DELETE}};
        }
        if (objectIdsForActiveTasks.count(patch.nmapsId())) {
            return { Resolution::Rejected, {message::HAS_ACTIVE_SPRAV_TASK}};
        }
        const auto objectDispClass = dispClass(object);
        if (objectDispClass && maxImportantDispClass >= *objectDispClass) {
            ++statistics.feedback;
            return feedbackClient.createDeleteTask(patch, object);
        }
        ++statistics.deleted;
        PatchResult patchResult = { Resolution::Applied, {message::DELETED}};
        if (!patch.indoorLevelUniversal().empty()) {
            INFO() << "Indoor object deletion moderated: " << object.id;
            patchResult.messages.insert(message::DELETE_INDOOR);
            editorModeratedInstance.deleteObject(object.id);
            const auto deleted = editorModeratedInstance.getObjectById(object.id);
            ASSERT(deleted.recentCommit);
            createDiffAlertMessages(object.id, deleted.recentCommit->id, patchResult.messages, cfg);
        } else {
            if (feedSettings.moderateAllChanges == poi_feed::ModerateAllChanges::Yes) {
                editorModeratedInstance.deleteObject(object.id);
            } else {
                editorInstance.deleteObject(object.id);
            }
        }
        return patchResult;
    } catch (const editor_client::ServerException& ex) {
        if (ex.status() == "ERR_VERSION_CONFLICT") {
            ++statistics.outdated;
            return { Resolution::Outdated, {} };
        }
        WARN() << "Editor backend reported error while deleteing object:"
            << object.id << " " << ex.status() << "\n" << ex;
    } catch (const maps::Exception& ex) {
        ERROR() << "Exception while deleteing object: "
            << object.id << "\n" << ex;
    } catch (const std::exception& ex) {
        ERROR() << "Exception while deleteing object: "
            << object.id << "\n" << ex.what();
    }
    return { Resolution::Failed, {}};
}

PatchResult
updateEditorObject(
    StatData& statistics,
    editor_client::Instance& editorInstance,
    editor_client::Instance& editorModeratedInstance,
    const editor_client::Instance& editorReadOnlyInstance,
    editor_client::BasicEditorObject& object,
    const poi_feed::FeedObjectData& patch,
    const std::unordered_set<poi_feed::ObjectId>& movingToSamePositionIds,
    const poi_feed::FeedSettings& feedSettings,
    const RecentPatches& recentPatches,
    const Config& cfg)
{
    if (!object.getGeometryInGeodetic()
        || object.getGeometryInGeodetic()->geometryType() != geolib3::GeometryType::Point) {
        return { Resolution::Rejected, {message::NOT_POINT} };
    }
    const auto& editorCfg = cfg.editorCfg();
    bool hasIssuesToModerate = false;
    PatchResult patchResult(Resolution::Skipped, {});
    double moveDistance = distance(
        object.getGeometryInGeodetic()->get<geolib3::Point2>(), *patch.position());
    if (
        (feedSettings.feedbackDistance && moveDistance > *feedSettings.feedbackDistance) ||
        (feedSettings.spravFeedbackDistance && moveDistance > *feedSettings.spravFeedbackDistance))
    {
        patchResult.messages.insert(message::TOO_BIG_DISTANCE);
        hasIssuesToModerate = true;
    }

    const auto isByRobot = isRecentCommitByRobot(object);
    const auto ageDays = recentCommitAgeDays(object);
    const auto areCoordinatesVerified = hasVerifiedCoordinates(object);

    try {

        // Edit geometry
        if (moveDistance > feedSettings.minDistance) {
            auto relocationResult = checkRelocationValidity(editorReadOnlyInstance, object, patch);
            patchResult.messages = createMessages(relocationResult);
            if (movingToSamePositionIds.count(patch.nmapsId())) {
                hasIssuesToModerate = true;
                patchResult.messages.insert(message::MOVE_POI_COLLISION);
            }
            if (relocationResult.poiConflict == PoiConflict::High) {
                // TODO: Implement smart behaviour here, don't just reject.
                return { Resolution::Rejected, {message::SEVERE_POI_CONFLICT} };
            }
            if (!valid(relocationResult)) {
                hasIssuesToModerate = true;
            }
            if (!isFullMergeImplemented(patch)) {
                ++statistics.rejected;
                patchResult.resolution = Resolution::Rejected;
            } else {
                if (patch.indoorLevelUniversal().empty() && !areCoordinatesVerified) {
                    object.setGeometryInGeodetic(
                        geolib3::Point2(patch.position()->lon, patch.position()->lat));
                } else if (moveDistance > INDOOR_MAX_DISTANCE) {
                    hasIssuesToModerate = true;
                    patchResult.messages.insert(message::INDOOR_POI_SPRAV_TOO_FAR);
                    WARN() << "Moving indoor object more than " <<
                        INDOOR_MAX_DISTANCE << " m " << patch.nmapsId();
                }
                ++statistics.moved;
                if (!isByRobot && ageDays) {
                    statistics.increaseMovedAge(*ageDays);
                }
                patchResult.messages.insert(message::MOVED);
                patchResult.resolution = Resolution::Applied;
                hasIssuesToModerate = hasIssuesToModerate || !valid(relocationResult);
            }
            if (patchResult.resolution != Resolution::Applied) {
                return patchResult;
            }
        }

        // Edit permalink
        const auto curPermalink = permalink(object);
        if (patch.permalink() != curPermalink) {
            if (curPermalink && areCoordinatesVerified) {
                hasIssuesToModerate = true;
            }
            patchResult.resolution = Resolution::Applied;
            patchResult.messages.insert(message::UPDATED_PERMALINK);
            setPermalink(object, patch.permalink());
        }

        // Edit names
        {
            const auto nameAttr = guessNameAttr(object, editorCfg);
            if (nameAttr.empty()) {
                return {Resolution::Failed, {message::NAME_ATTRIBUTE_NOT_FOUND}};
            }
            Names nmapsNames(object, editorCfg);
            const auto updateResult = nmapsNames.update(Names(patch), editorCfg);
            if (!updateResult.empty()) {
                hasIssuesToModerate = hasIssuesToModerate || areCoordinatesVerified;
                nmapsNames.writeTo(object, editorCfg);
                patchResult.resolution = Resolution::Applied;
                patchResult.messages.insert(message::RENAMED);
                if (updateResult.count(Names::UpdateResult::LangLost)) {
                    patchResult.messages.insert(message::RENAMED_LANG_LOST);
                    hasIssuesToModerate = true;
                }
                if (updateResult.count(Names::UpdateResult::RubricName)) {
                    patchResult.messages.insert(message::RENAMED_WITH_RUBRIC);
                    hasIssuesToModerate = true;
                }
                ++statistics.renamed;
            }
        }
        if (Resolution::Applied == patchResult.resolution) {
            if (isProtected(object)) {
                return { Resolution::Rejected, {message::MOVE_PROTECTED_BY_ATTRIBUTE} };
            }
            if ((patch.isAdvert() || patch.hasOwner()) &&
                recentPatches.compareToStored(patch).none())
            {
                return { Resolution::Rejected, {message::USER_ROBOT_FIGHT} };
            }
            if (hasIssuesToModerate) {
                patchResult.messages.insert(message::NEED_MODERATION);
            }
            if (feedSettings.moderateAllChanges == poi_feed::ModerateAllChanges::Yes ||
                (hasIssuesToModerate && !(patch.isAdvert() || patch.hasOwner())))
            {
                INFO() << "Object change moderated: " << object.id;
                const auto saved = editorModeratedInstance.saveObject(object);
                ASSERT(saved.recentCommit);
                createDiffAlertMessages(saved.id, saved.recentCommit->id, patchResult.messages, cfg);
            } else {
                INFO() << "Object change: " << object.id;
                editorInstance.saveObject(object);
            }
            return patchResult;
        } else {
            return { Resolution::Skipped, {message::NOTHING_TO_DO} };
        }
    } catch (const editor_client::ServerException& ex) {
        if (ex.status() == "ERR_VERSION_CONFLICT") {
            ++statistics.outdated;
            return { Resolution::Outdated, {} };
        }
        ERROR() << "Editor backend reported error while updating object: "
            << object.id << "\n" << ex.status() << "\n" << ex.serverResponse();
        return { Resolution::Failed, {ex.status()} };
    } catch (const maps::Exception& ex) {
        ERROR() << "Exception while updating object: "
            << object.id << "\n" << ex;
    } catch (const std::exception& ex) {
        ERROR() << "Exception while updating object: "
            << object.id << "\n" << ex.what();
    }
    return { Resolution::Failed, {} };
}

struct Results
{
public:
    const std::unordered_map<poi_feed::ObjectId, PatchResult>& data() const
    {
        return resultsMap;
    }

    // Only write is thread safe, since read is never performed by threads.

    void add(poi_feed::ObjectId oid, PatchResult result)
    {
        std::lock_guard<std::mutex> lock(resultsMapMutex);
        resultsMap.emplace(oid, std::move(result));
    }

    std::atomic<size_t> processedPatches = 0;

    const chrono::TimePoint startTime = chrono::TimePoint::clock::now();

private:
    std::mutex resultsMapMutex;
    std::unordered_map<poi_feed::ObjectId, PatchResult> resultsMap;
};

void processPatchesBatch(
    Results& results,
    StatData& statistics,
    const maps::common::Batch<poi_feed::FeedObjectDataVector>& patches,
    const PatchesContext& patchesContext,
    const Config& cfg,
    const RecentPatches& recentPatches)
{
    editor_client::Instance editorInstance(cfg.editorWriterUrl(), common::WIKIMAPS_SPRAV_UID);
    editor_client::Instance editorModeratedInstance(cfg.editorWriterUrl(), common::WIKIMAPS_SPRAV_COMMON_UID);
    const editor_client::Instance editorReadOnlyInstance(cfg.editorUrl(), common::WIKIMAPS_SPRAV_UID);
    FeedbackClient feedbackClient(cfg.socialBackofficeUrl());
    feedbackClient.setTvmTicketProvider(cfg.socialBackofficeTvmTicketProvider());
    const auto& feedSettingsCfg = cfg.feedSettingsConfig();
    for (const auto& patch : patches) {
        if (results.processedPatches >= MAX_PROCESSING_PATCHES) {
            break;
        }
        if (chrono::TimePoint::clock::now() - results.startTime > MAX_PROCESSING_TIME_HOURS) {
            break;
        }
        const auto* feedSettings = feedSettingsCfg.feedSettings(*patch.ftTypeId());
        PatchResult patchResult;
        std::optional<editor_client::BasicEditorObject> editorObject;
        if (!feedSettings) {
            patchResult.messages.insert(message::NO_SETTINGS);
            ++statistics.noSettings;
            results.add(patch.nmapsId(), patchResult);
            continue;
        }
        editorObject = getEditorObject(editorReadOnlyInstance, patch.nmapsId());
        const auto lastUpdateDate = getEditorObjectModificationDate(
            editorReadOnlyInstance,
            patch.nmapsId());
        if (!editorObject ||
            editorObject->recentCommit ||
            !lastUpdateDate)
        {
            patchResult.resolution = Resolution::Failed;
        }  else if (editorObject->deleted ||
            editorObject->recentCommit->date > patch.actualizationDate() ||
            *lastUpdateDate > patch.actualizationDate()) {
            patchResult.resolution = Resolution::Outdated;
            ++statistics.outdated;
        } else if (patch.toDelete()) {
            patchResult = deleteEditorObject(
                statistics,
                editorInstance,
                editorModeratedInstance,
                feedbackClient,
                *editorObject,
                patch,
                patchesContext.objectIdsForActiveTasks,
                patchesContext.notYetToDelete,
                *feedSettings,
                cfg);

            if (patchResult.resolution == Resolution::Applied) {
                ++results.processedPatches;
            }

        } else {
            if (!rubricId(*editorObject)) {
                WARN() << "Can't update object without rubric " << patch.nmapsId();
                continue;
            }
            patchResult = updateEditorObject(
                statistics,
                editorInstance,
                editorModeratedInstance,
                editorReadOnlyInstance,
                *editorObject,
                patch,
                patchesContext.movingToSamePositionIds,
                *feedSettings,
                recentPatches,
                cfg);
            if (patchResult.resolution == Resolution::Applied) {
                ++results.processedPatches;
            }
        }
        results.add(patch.nmapsId(), patchResult);
    }
}
} // namespace

void processPatches(
    StatData& statistics,
    const PatchesContext& patchesContext,
    MergeTaskId taskId,
    const Config& cfg)
{
    RecentPatches recentPatches(cfg);
    auto batches = maps::common::makeBatches(patchesContext.patches, PATCHES_BATCH_SIZE);
    Results results;
    ThreadPool threadPool(THREADS_COUNT);
    Executor threadedExecutor;
    for (const auto& batch : batches) {
        threadedExecutor.addTask([&, batch] {
            processPatchesBatch(
                results,
                statistics,
                batch,
                patchesContext,
                cfg,
                recentPatches);
        });
    }
    threadedExecutor.executeAllInThreads(threadPool);
    if (results.processedPatches >= MAX_PROCESSING_PATCHES) {
        INFO() << "Maximum number of processed patches reached: " << results.processedPatches;
    } else if (chrono::TimePoint::clock::now() - results.startTime > MAX_PROCESSING_TIME_HOURS) {
        INFO() << "Time to process patches is over.";
    }
    INFO() << "Processing finished. Writing results.";
    statistics.skipped += (patchesContext.patches.size() - results.processedPatches);
    PatchResult workLimitExceeded(Resolution::Skipped, {message::WORK_LIMIT_EXCEEDED});
    writeResultsToArchive(results.data(), patchesContext.patches, taskId, cfg, workLimitExceeded);
    std::unordered_set<poi_feed::ObjectId> idsOfModified;
    for (const auto& [objectId, result] : results.data()) {
        if (result.resolution == Resolution::Applied) {
            idsOfModified.insert(objectId);
        }
    }
    recentPatches.updateStorage(patchesContext.patches, idsOfModified);
}

void
writeResultsToArchive(
    const ResultsMap& resultsMap,
    const poi_feed::FeedObjectDataVector& candidates,
    MergeTaskId taskId,
    const Config& cfg,
    const PatchResult& defaultPatchResult)
{
    auto socialTxn = cfg.socialPool().masterWriteableTransaction();
    auto batches = maps::common::makeBatches(candidates, QUERY_BATCH_SIZE);
    for (const auto& batch : batches) {
        std::stringstream query;
        query << "INSERT INTO " + MERGE_POI_PATCH_QUEUE_ARCHIVE +
                " (task_id, object_id, data_json, processed_at, resolution, messages) VALUES ";
        query << common::join(batch,
            [&](const auto& candidate) {
                auto it = resultsMap.find(candidate.nmapsId());
                const auto& result =
                    it != resultsMap.end()
                    ? it->second
                    : defaultPatchResult;
                std::stringstream subQuery;
                subQuery
                    << "(" << taskId << "," << candidate.nmapsId() << "," << socialTxn->quote(candidate.toJson())
                    << "," << socialTxn->quote(chrono::formatSqlDateTime(result.processedAt))
                    << ",'" << result.resolution << "',"
                    << (result.messages.empty() ? "NULL" : socialTxn->quote(toJson(result.messages))) << ")";
                return subQuery.str();
            },
            ',');
        socialTxn->exec(query.str());
    }
    socialTxn->commit();
}

void
truncateResultsArchive(const Config& cfg)
{
    auto socialTxn = cfg.socialPool().masterWriteableTransaction();
    socialTxn->exec("TRUNCATE " + MERGE_POI_PATCH_QUEUE_ARCHIVE);
    socialTxn->commit();
}
} // namespace maps::wiki::merge_poi
