#include "worker.h"
#include "utils.h"

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/pg_locks.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/utility.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/geom_tools/object_diff_builder.h>

#include <boost/algorithm/cxx11/all_of.hpp>
#include <boost/geometry/geometry.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/algorithm/count_if.hpp>
#include <boost/range/algorithm_ext/erase.hpp>

using namespace std::chrono_literals;

namespace maps::mrc::onfoot {

namespace {

constexpr size_t THUMBNAIL_HEIGHT_PX = 160;
constexpr std::string_view FEEDBACK_WORKFLOW_TYPE = "feedback";
constexpr std::string_view ONFOOT_FEEDBACK_SOURCE_WALKS = "experiment-onfoot";
constexpr std::string_view ONFOOT_FEEDBACK_SOURCE_BACKOFFICE_OBJECT = "sprav-pedestrian-onfoot";
constexpr std::string_view ONFOOT_FEEDBACK_SOURCE_PARTNER = "partner-pedestrian-onfoot";
constexpr std::string_view INDOOR_FEEDBACK_SOURCE_PARTNER = "partner-pedestrian-indoor";
constexpr std::string_view TOLOKA_FEEDBACK_SOURCE_PARTNER = "toloka-pedestrian-onfoot";

common::Size thumbnailSize(const common::Size& size)
{
    return {size.width * THUMBNAIL_HEIGHT_PX / size.height,
            THUMBNAIL_HEIGHT_PX};
}

common::Size normThumbnailSizeOf(const db::Feature& feature)
{
    const auto size
        = transformByImageOrientation(feature.size(), feature.orientation());
    return thumbnailSize(size);
}

bool isPublishingDone(const db::Features& walkPhotos)
{
    // Check that all photos that should be published are published
    return std::all_of(
        walkPhotos.begin(), walkPhotos.end(),
        [](const auto& feature) {
            return feature.shouldBePublished() == feature.isPublished();
        }
    );
}

db::FeaturePrivacy evalMaxPrivacy(const db::Features& walkPhotos)
{
    auto maxPrivacy = db::FeaturePrivacy::Public;
    for (const auto& feature : walkPhotos) {
        maxPrivacy = std::max(maxPrivacy, feature.privacy());
        if (maxPrivacy == db::FeaturePrivacy::Max) {
            break;
        }
    }
    return maxPrivacy;
}

bool hasGdprDeletedPhotos(const db::Features& walkPhotos)
{
    return std::any_of(
        walkPhotos.begin(), walkPhotos.end(),
        [](const auto& photo) {
            return photo.gdprDeleted().value_or(false);
        }
    );
}

bool arePhotosOptional(const db::WalkObject& walkObject)
{
    return walkObject.dataset() == db::Dataset::PedestrianTask;
}

http::URL makeBaseImageUrl(const common::Config& cfg, db::FeaturePrivacy privacy)
{
    if (privacy == db::FeaturePrivacy::Secret) {
        return cfg.externals().mrcBrowserProUrl();
    }
    return cfg.externals().mrcBrowserUrl();
}


http::URL makeFullImageUrl(const common::Config& cfg, const db::Feature& feature)
{
    http::URL result = makeBaseImageUrl(cfg, feature.privacy());
    result.setPath("/feature/" + std::to_string(feature.id()) + "/image");
    return boost::lexical_cast<std::string>(result);
}

http::URL makeImagePreviewUrl(const common::Config& cfg, const db::Feature& feature)
{
    http::URL result = makeBaseImageUrl(cfg, feature.privacy());
    result.setPath("/feature/" + std::to_string(feature.id()) + "/thumbnail");
    return boost::lexical_cast<std::string>(result);
}

std::string decodeFeedbackType(db::WalkFeedbackType type)
{
    switch (type) {
        case db::WalkFeedbackType::None:
            break;
        case db::WalkFeedbackType::Barrier:
            return "barrier";
        case db::WalkFeedbackType::Other:
            return "other";
        case db::WalkFeedbackType::BuildingEntrance:
            return "entrance";
        case db::WalkFeedbackType::AddressPlate:
            return "address";
        case db::WalkFeedbackType::EntrancePlate:
            return "entrance";
        case db::WalkFeedbackType::BusinessSign:
            return "poi";
        case db::WalkFeedbackType::BusinessWorkingHours:
            return "poi";
        case db::WalkFeedbackType::FootPath:
            return "pedestrian-route";
        case db::WalkFeedbackType::CyclePath:
            return "bicycle-route";
        case db::WalkFeedbackType::Stairs:
            return "pedestrian-route";
        case db::WalkFeedbackType::Ramp:
            return "other";
        case db::WalkFeedbackType::Room:
            return "indoor";
        case db::WalkFeedbackType::Wall:
            return "indoor-barrier";
        case db::WalkFeedbackType::Organization:
            return "poi";
        case db::WalkFeedbackType::Fence:
            return "fence";
        case db::WalkFeedbackType::Building:
            return "building";
        case db::WalkFeedbackType::Parking:
            return "parking";

        default:
            return "other";
    }
    throw maps::LogicError{} << "Unexpected walk feedback type: " << type;
}

std::optional<std::string_view> evalFeedbackSource(const db::WalkObject& object)
{
    switch(object.dataset()) {
        case db::Dataset::Walks:
            return ONFOOT_FEEDBACK_SOURCE_WALKS;
        case db::Dataset::BackofficeObject:
        case db::Dataset::AltayPedestrians:
        case db::Dataset::YangPedestrians:
            return ONFOOT_FEEDBACK_SOURCE_BACKOFFICE_OBJECT;
        case db::Dataset::TolokaPedestrians:
            return TOLOKA_FEEDBACK_SOURCE_PARTNER;
        case db::Dataset::PedestrianTask:
            if (object.hasIndoorLevelId()) {
                return INDOOR_FEEDBACK_SOURCE_PARTNER;
            } else {
                return ONFOOT_FEEDBACK_SOURCE_PARTNER;
            }

        default: return std::nullopt;
    }
}

using TPoint =
    boost::geometry::model::point<double, 2, boost::geometry::cs::cartesian>;
using TMultiPoint = boost::geometry::model::multi_point<TPoint>;
using TPolygon = boost::geometry::model::polygon<TPoint, false, true>;
using TRing = TPolygon::ring_type;

TPoint toBoost(const geolib3::Point2& point)
{
    return TPoint(point.x(), point.y());
}

TMultiPoint toBoost(const geolib3::PointsVector& points)
{
    auto result = TMultiPoint{};
    result.reserve(points.size());
    for (const auto& point : points) {
        result.push_back(toBoost(point));
    }
    return result;
}

TRing toBoost(const geolib3::LinearRing2& ring)
{
    auto result = TRing{};
    result.reserve(ring.pointsNumber() + 1);
    for (size_t i = 0; i < ring.pointsNumber(); ++i) {
        result.push_back(toBoost(ring.pointAt(i)));
    }
    result.push_back(toBoost(ring.pointAt(0)));
    return result;
}

TPolygon toBoost(const geolib3::Polygon2& polygon)
{
    auto result = TPolygon{};
    result.outer() = toBoost(polygon.exteriorRing());
    result.inners().reserve(polygon.interiorRingsNumber());
    for (size_t i = 0; i < polygon.interiorRingsNumber(); ++i) {
        result.inners().push_back(toBoost(polygon.interiorRingAt(i)));
    }
    return result;
}

template <class Geometry>
geolib3::Point2 returnCentroid(const Geometry& geom)
{
    TPoint result = boost::geometry::return_centroid<TPoint>(toBoost(geom));
    return geolib3::Point2(result.get<0>(), result.get<1>());
}

geolib3::Point2 getPosition(const db::WalkObject& walkObject)
{
    auto geometryVariant = walkObject.geodeticGeometry();
    auto geometryType = walkObject.geometryType();

    switch (geometryType) {
        case geolib3::GeometryType::Point:
            return std::get<geolib3::Point2>(geometryVariant);
        case geolib3::GeometryType::LineString:
            return returnCentroid(std::get<geolib3::Polyline2>(geometryVariant).points());
        case geolib3::GeometryType::Polygon:
            return returnCentroid(std::get<geolib3::Polygon2>(geometryVariant));
        default:
            break;
    }
    REQUIRE(false, "Unsupported geometry type " << geometryType);
}

geolib3::Point2 getPosition(db::WalkObjects::iterator first,
                            db::WalkObjects::iterator last)
{
    ASSERT(first != last);
    auto points = geolib3::PointsVector{};
    std::for_each(first, last, [&](const db::WalkObject& obj) {
        points.push_back(getPosition(obj));
    });
    return returnCentroid(points);
}

template <typename Geometry>
void writeGeometryDiff(
    db::WalkObjects::iterator first,
    db::WalkObjects::iterator last,
    json::ObjectBuilder builder)
{
    ASSERT(first != last);
    auto diffBuilder = wiki::geom_tools::ObjectDiffBuilder<Geometry>{};
    auto geomsBefore = std::vector<Geometry>{};
    auto geomsAfter = std::vector<Geometry>{};
    std::for_each(first, last, [&](const db::WalkObject& obj) {
        auto actionType =
            obj.hasActionType() ? obj.actionType() : db::ObjectActionType::Add;
        auto geom = std::get<Geometry>(obj.geodeticGeometry());
        switch (actionType) {
            case db::ObjectActionType::Remove:
                geomsBefore.push_back(geom);
                break;
            case db::ObjectActionType::Add:
                geomsAfter.push_back(geom);
                break;
            default:
                break;
        }
    });
    if (!geomsBefore.empty()) {
        diffBuilder.setBefore(geomsBefore);
    }
    if (!geomsAfter.empty()) {
        diffBuilder.setAfter(geomsAfter);
    }
    builder["objectDiff"] = [&](json::ObjectBuilder builder) {
        diffBuilder.json(builder);
    };
}

void addGeometryDiff(db::WalkObjects::iterator first,
                     db::WalkObjects::iterator last,
                     json::ObjectBuilder builder)
{
    ASSERT(first != last);
    switch (first->geometryType()) {
        case geolib3::GeometryType::Point:
            break;
        case geolib3::GeometryType::LineString:
            return writeGeometryDiff<geolib3::Polyline2>(first, last, builder);
        case geolib3::GeometryType::Polygon:
            return writeGeometryDiff<geolib3::Polygon2>(first, last, builder);
        default:
            break;
    }
}

std::string makeUserComment(db::WalkObjects::iterator first,
                            db::WalkObjects::iterator last)
{
    ASSERT(first != last);
    auto objComments =
        boost::make_iterator_range(first, last) |
        boost::adaptors::transformed(boost::mem_fn(&db::WalkObject::comment)) |
        boost::adaptors::filtered(std::not_fn(&std::string::empty));
    if (auto result = wiki::common::join(objComments, ", "); !result.empty()) {
        return result;
    }
    return humanReadableFeedbackType(first->feedbackType());
}

const auto emptyString = std::string{};

const std::string& getUserId(const db::WalkObject& obj)
{
    return obj.hasUserId() ? obj.userId() : emptyString;
}

const std::string& getIndoorLevelId(const db::WalkObject& obj)
{
    return obj.hasIndoorLevelId() ? obj.indoorLevelId() : emptyString;
}

auto sliceWalkObject = common::makeTupleFn(getUserId,
                                           &db::WalkObject::deviceId,
                                           &db::WalkObject::taskId,
                                           getIndoorLevelId,
                                           &db::WalkObject::dataset,
                                           &db::WalkObject::feedbackType,
                                           &db::WalkObject::geometryType,
                                           &db::WalkObject::status,
                                           &db::WalkObject::nmapsObjectId);

auto lessWalkObjectGroup = common::lessFn(sliceWalkObject);

auto equalWalkObjectGroup = [](const db::WalkObject& lhs,
                               const db::WalkObject& rhs) {
    return isGroupHypothesisGeneration(lhs) && isGroupHypothesisGeneration(rhs)
               ? common::equalFn(sliceWalkObject)(lhs, rhs)
               : lhs.id() == rhs.id();
};

} // namespace

std::string Worker::makeRequestBody(
    db::WalkObjects::iterator first,
    db::WalkObjects::iterator last,
    const db::Features& walkPhotos,
    std::string_view feedbackSource)
{
    ASSERT(first != last);
    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        auto objectPos = getPosition(first, last);

        builder["workflow"] << FEEDBACK_WORKFLOW_TYPE;
        builder["type"] << decodeFeedbackType(first->feedbackType());
        builder["source"] << feedbackSource;
        builder["position"] << geolib3::geojson(objectPos);
        auto maxPhotosPrivacy =
            db::selectStricterPrivacy(evalMaxPrivacy(walkPhotos),
                db::evalPrivacy(first->dataset()));
        builder["hidden"] = (db::FeaturePrivacy::Public != maxPhotosPrivacy);
        if (db::FeaturePrivacy::Secret == maxPhotosPrivacy) {
            builder["internalContent"] = true;
        }
        auto comment = makeUserComment(first, last);
        if (!comment.empty()) {
            builder["userComment"] = comment;
        }
        if (first->hasIndoorLevelId()) {
            builder["indoorLevel"] = first->indoorLevelId();
        }
        if (first->nmapsObjectId().has_value()) {
            builder["objectId"] << first->nmapsObjectId().value();
        }

        if (!walkPhotos.empty()) {
            builder["sourceContext"] = [&](json::ObjectBuilder builder) {
                builder["type"] = "images";
                builder["content"] = [&](json::ObjectBuilder builder) {

                    builder["imageFeatures"] = [&](json::ArrayBuilder builder) {
                        for (const auto& feature : walkPhotos) {
                            builder << [&](json::ObjectBuilder imageFeature) {
                                imageFeature["id"] = std::to_string(feature.id());

                                imageFeature["heading"] = feature.heading().value();
                                imageFeature["geometry"] = geolib3::geojson(feature.geodeticPos());
                                imageFeature["timestamp"] = chrono::formatIsoDateTime(feature.timestamp());

                                imageFeature["targetGeometry"] = geolib3::geojson(objectPos);

                                imageFeature["imageFull"] = [&](json::ObjectBuilder imageFull) {
                                    auto size = transformByImageOrientation(
                                        feature.size(), feature.orientation());

                                    imageFull["url"] = makeFullImageUrl(cfg_, feature).toString();
                                    imageFull["width"] = size.width;
                                    imageFull["height"] = size.height;
                                };

                                imageFeature["imagePreview"] = [&](json::ObjectBuilder imagePreview) {
                                    auto size = normThumbnailSizeOf(feature);

                                    imagePreview["url"] = makeImagePreviewUrl(cfg_, feature).toString();
                                    imagePreview["width"] = size.width;
                                    imagePreview["height"] = size.height;
                                };
                            };
                        }
                    };
                };
            };
        }
        addGeometryDiff(first, last, builder);
    };
    return builder.str();
}

void Worker::sendToSocial(
    db::WalkObjects::iterator first,
    db::WalkObjects::iterator last,
    const db::Features& walkPhotos,
    std::string_view feedbackSource)
{
    static const auto ROBOT_UID = std::to_string(wiki::common::ROBOT_UID);

    ASSERT(!walkPhotos.empty() || arePhotosOptional(*first));

    http::URL url = cfg_.externals().socialBackofficeUrl();
    url.addParam("uid",
                 first->hasUserId() && !hasGdprDeletedPhotos(walkPhotos)
                     ? first->userId()
                     : ROBOT_UID);
    url.setPath("/feedback/tasks/onfoot");

    http::Request request(client_, http::POST, url);
    if (tvmClient_) {
        auto tvmTicket = tvmClient_->GetServiceTicketFor("social-backoffice");
        request.addHeader(auth::SERVICE_TICKET_HEADER, std::move(tvmTicket));
    }

    request.setContent(makeRequestBody(first, last, walkPhotos, feedbackSource));
    auto response = request.perform();
    auto responseJson = json::Value::fromString(response.readBody());

    const auto& error = responseJson["error"];
    REQUIRE(
        !error.exists(),
        "Social status " << error["status"].toString()
                  << ", message: " << error["message"].toString()
    );

    auto feedbackId =
        std::stoull(responseJson["feedbackTask"]["id"].toString());
    std::for_each(first, last, [&](db::WalkObject& obj) {
        obj.setFeedbackId(feedbackId);
    });
}

/**
 * Handle WalkObject in Pending or WaitPublishing state.
 * - If the object state is Pending, its associated photo is inspected.
 *   If the photo should be published, the object enters WaitPublishing state.
 *   Otherwise the object is discarded.
 * - If the object state is WaitPublishing AND the photo is published
 *   then feedback is sent to Social.
 */
void Worker::processWalkObjectGroup(db::WalkObjects::iterator first,
                                    db::WalkObjects::iterator last)
{
    ASSERT(first != last);
    auto objIds =
        boost::make_iterator_range(first, last) |
        boost::adaptors::transformed(boost::mem_fn(&db::WalkObject::id));
    auto objDescription = "objects{" + wiki::common::join(objIds, ',') + "}";
    auto oldObjectStatus = first->status();
    auto setObjectStatus = [&](db::ObjectStatus newObjectStatus) {
        std::for_each(first, last, [&](db::WalkObject& obj) {
            obj.setObjectStatus(newObjectStatus);
            if (db::ObjectStatus::Published == newObjectStatus) {
                obj.setPublishedAt(chrono::TimePoint::clock::now());
            }
        });
        INFO() << newObjectStatus << " " << objDescription;
    };

    try {
        auto txn = pool_.masterWriteableTransaction();
        auto photos =
            db::FeatureGateway(*txn).load(db::table::Feature::walkObjectId.in(
                {objIds.begin(), objIds.end()}));
        ASSERT(!photos.empty() || arePhotosOptional(*first));
        if (db::ObjectStatus::Pending == oldObjectStatus) {
            if (!boost::algorithm::all_of(
                    photos, boost::mem_fn(&db::Feature::processedAt))) {
                INFO() << "wait till photos are processed " << objDescription;
                return;
            }
            auto numShouldBePublished = boost::count_if(
                photos, boost::mem_fn(&db::Feature::shouldBePublished));
            if (numShouldBePublished > 0 ||
                photos.empty() && arePhotosOptional(*first)) {
                setObjectStatus(db::ObjectStatus::WaitPublishing);
            }
            else {
                setObjectStatus(db::ObjectStatus::Discarded);
            }
        }
        else if (db::ObjectStatus::WaitPublishing == oldObjectStatus) {
            if (!isPublishingDone(photos)) {
                INFO() << "wait till photos are published " << objDescription;
                return;
            }
            boost::remove_erase_if(
                photos, [](const auto& photo) { return !photo.isPublished(); });
            if (!photos.empty() || arePhotosOptional(*first)) {
                setObjectStatus(db::ObjectStatus::Published);
            }
            else {
                setObjectStatus(db::ObjectStatus::Discarded);
            }
        }

        if (dryRun_ == DryRun::No) {
            if (first->status() == db::ObjectStatus::Published) {
                if (first->feedbackType() != db::WalkFeedbackType::None) {
                    if (auto feedbackSource = evalFeedbackSource(*first)) {
                        sendToSocial(first, last, photos, *feedbackSource);
                    }
                    else {
                        INFO() << "no feedback for " << first->dataset();
                    }
                }
                else {
                    INFO() << "no feedback type";
                }
            }

            db::WalkObjectGateway{*txn}.updatex(
                TArrayRef<db::WalkObject>{first, last});
            txn->commit();
        }
    }
    catch (const maps::Exception& e) {
        setObjectStatus(oldObjectStatus);
        ERROR() << "failed " + objDescription + ": " << e;
    }
    catch (const std::exception& e) {
        setObjectStatus(oldObjectStatus);
        ERROR() << "failed " + objDescription + ": " << e.what();
    }
}

bool Worker::runOnce(pqxx::transaction_base& lockTxn)
{
    INFO() << "Start processing";

    auto walkObjects =
        db::WalkObjectGateway{lockTxn}.load(db::table::WalkObject::status.in(
            {db::ObjectStatus::Pending, db::ObjectStatus::WaitPublishing}));

    if (walkObjects.empty()) {
        INFO() << "No objects to process";
        return false;
    }

    auto pendingObjectsCount =
        boost::count_if(walkObjects, [](const db::WalkObject& obj) {
            return obj.status() == db::ObjectStatus::Pending;
        });

    std::sort(walkObjects.begin(), walkObjects.end(), lessWalkObjectGroup);

    common::forEachEqualRange(
        walkObjects.begin(),
        walkObjects.end(),
        equalWalkObjectGroup,
        [this](auto first, auto last) { processWalkObjectGroup(first, last); });

    size_t publishedCount = 0;
    size_t discardedCount = 0;
    size_t pendingCount = 0;
    size_t waitPublishingCount = 0;

    for (auto&& walkObject : walkObjects) {
        switch (walkObject.status()) {
            case db::ObjectStatus::Published:
                ++publishedCount;
                break;
            case db::ObjectStatus::Discarded:
                ++discardedCount;
                break;
            case db::ObjectStatus::Pending:
                ++pendingCount;
                break;
            case db::ObjectStatus::WaitPublishing:
                ++waitPublishingCount;
                break;
            default:
                // Other states are not used
                break;
        }
    }

    INFO() << "Finished processing " << walkObjects.size() << " objects. "
        << "published: " << publishedCount << ", "
        << "discarded: " << discardedCount << ", "
        << "pending: " << pendingCount << ", "
        << "wait publishing: " << waitPublishingCount;

    return pendingObjectsCount > 0;
}

void Worker::run()
{
    constexpr auto PROCESS_WAIT_TIME = 60s;

    while (true) {
        try {
            pgp3utils::PgAdvisoryXactMutex locker(
                pool_,
                static_cast<int64_t>(common::LockId::OnfootProcessor));
            if (locker.try_lock()) {
                runOnce(locker.writableTxn());
            }
        } catch (const maps::Exception& e) {
            ERROR() << "Caught exception while processing objects: " << e;
        } catch (const std::exception& e) {
            ERROR() << "Caught exception while processing objects: " << e.what();
        }

        std::this_thread::sleep_for(PROCESS_WAIT_TIME);
    }
}

void Worker::initTvmClient(const auth::TvmtoolSettings& tvmToolSettings)
{
    tvmClient_ = std::make_unique<NTvmAuth::TTvmClient>(tvmToolSettings.makeTvmClient());
}

} // namespace maps::mrc::onfoot
