#include "configuration.h"
#include "utility.h"
#include "yacare_params.h"

#include <maps/infra/yacare/include/limit_rate.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/json/include/value.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/yacare_helpers.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_utility.h>
#include <maps/wikimap/mapspro/services/mrc/libs/proto/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/ugc_event_logger/include/logger.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/async_ride_correction/lib/utility.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/ride.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ugc_uploader/lib/ugc.h>
#include <maps/wikimap/mapspro/services/mrc/ugc_back/proto/chunk_descriptor_impl.pb.h>
#include <yandex/maps/geolib3/proto.h>
#include <yandex/maps/i18n.h>
#include <yandex/maps/proto/mrcugc/ride.pb.h>

#include <google/protobuf/text_format.h>

namespace maps::mrc::ugc_back {

namespace {

const std::string CONTENT_TYPE_HEADER = "Content-Type";
const std::string CONTENT_TYPE_JPEG = "image/jpeg";
const std::string CONTENT_TYPE_JSON = "application/json";
const std::string CONTENT_TYPE_PROTOBUF = "application/protobuf";
const std::string CONTENT_TYPE_TEXT = "text/plain; charset=utf-8";
const std::string FORMAT_JSON = "json";
const std::string FORMAT_PROTOBUF = "protobuf";
const std::string FORMAT_TEXT = "text";
const std::string RATELIMITER_SERVICE = "maps-core-nmaps-mrc-ugc-back";
const std::string RU_LANG = "ru_RU";
const std::string SELF_TVM_ALIAS = "maps-core-nmaps-mrc-ugc-back";

std::locale getLocale(const std::string& lang)
try {
    return i18n::bestLocale(boost::lexical_cast<Locale>(lang));
}
catch (const std::exception&) {
    throw yacare::errors::BadRequest() << "unsupported lang: " << lang;
}

auto makeMyPhotoProto(const db::Feature& photo,
                      const http::URL& ugcBack,
                      const std::locale& locale)
    -> yandex::maps::proto::mrcphoto::ugc::ride::MyPhoto
{
    auto result = yandex::maps::proto::mrcphoto::ugc::ride::MyPhoto{};
    result.set_id(TString{std::to_string(photo.id())});
    *result.mutable_taken_at() = proto::makeTime(getTimestamp(photo), locale);
    auto geoPhoto = result.mutable_geo_photo();
    if (photo.hasSize()) {
        auto url = ugcBack;
        url.setPath("/v1/rides/my/photo_image")
            .addParam("photo_id", std::to_string(photo.id()));
        *geoPhoto->mutable_image() =
            proto::makeImage(url, "size_name", proto::prepareNamedSizes(photo));
    }
    if (hasPos(photo)) {
        auto shootingPoint = geoPhoto->mutable_shooting_point();
        auto point3d = shootingPoint->mutable_point();
        *point3d->mutable_point() =
            geolib3::proto::encode(getGeodeticPos(photo));
        if (photo.hasHeading()) {
            *shootingPoint->mutable_direction() =
                proto::makeDirection(photo.heading());
        }
    }
    geoPhoto->set_taken_at(chrono::convertToUnixTime(getTimestamp(photo)));
    return result;
}

auto makeTrackPreviewProto(const geolib3::Polyline2& track,
                           const PhotoStream& photos,
                           size_t previewSize,
                           size_t chunkSize,
                           const std::optional<std::string>& clientRideId)
    -> yandex::maps::proto::mrcphoto::ugc::ride::TrackPreview
{
    ASSERT(!photos.items.empty());

    auto result = yandex::maps::proto::mrcphoto::ugc::ride::TrackPreview{};

    auto preview = PhotoStream{};
    auto previewStep = std::max<float>(1.0f,
        photos.items.size() / static_cast<float>(previewSize));
    for (float i = 0; std::round(i) < photos.items.size(); i += previewStep) {
        preview.items.push_back(photos.items[std::round(i)]);
    }
    if (preview.items.back().photo.get().id() !=
        photos.items.back().photo.get().id()) {
        preview.items.push_back(photos.items.back());
    }
    *result.mutable_preview() = encode(preview);

    auto chunkList = result.mutable_chunk_list();
    auto maxPayloadBytesNumber = size_t{0};
    for (auto& batch : maps::common::makeBatches(photos.items, chunkSize)) {
        auto& front = *batch.begin();
        auto& back = *std::prev(batch.end());
        auto subpolyline =
            Subpolyline{.begin = front.position, .end = back.position};
        auto chunk = chunkList->add_chunks();
        *chunk->mutable_subpolyline() = encode(subpolyline);
        chunk->set_photos_count(std::distance(batch.begin(), batch.end()));

        auto chunkDescriptorImpl = yandex::maps::proto::mrcphoto::ugc::
            chunk_descriptor_impl::ChunkDescriptorImpl{};
        chunkDescriptorImpl.set_source_id(
            TString{front.photo.get().sourceId()});
        chunkDescriptorImpl.set_started_at(
            std::chrono::floor<std::chrono::milliseconds>(
                getTimestamp(front.photo.get()).time_since_epoch())
                .count());
        chunkDescriptorImpl.set_finished_at(
            std::chrono::ceil<std::chrono::milliseconds>(
                getTimestamp(back.photo.get()).time_since_epoch())
                .count());
        *chunkDescriptorImpl.mutable_subpolyline() = encode(subpolyline);
        *chunkDescriptorImpl.mutable_geometry() =
            geolib3::proto::encode(makePartition(track, subpolyline));
        if (clientRideId) {
            chunkDescriptorImpl.set_client_ride_id(TString(*clientRideId));
        }
        ASSERT(chunkDescriptorImpl.IsInitialized());

        auto buf = TString{};
        REQUIRE(chunkDescriptorImpl.SerializeToString(&buf),
                "failed to serialize protobuf");
        chunk->mutable_chunk_descriptor()->set_payload(buf);
        maxPayloadBytesNumber = std::max(maxPayloadBytesNumber, buf.size());
    }
    INFO() << chunkList->chunks_size() << " chunks (maximum "
           << maxPayloadBytesNumber << " bytes)";
    return result;
}

void setRideStatus(
    const db::Ride& ride,
    yandex::maps::proto::mrcphoto::ugc::ride::MyRide* result)
{
    auto* status = result->mutable_ride_status();
    switch (ride.status()) {
        case db::RideStatus::Pending:
            status->mutable_pending();
            break;
        case db::RideStatus::Processed:
            status->mutable_processed();
            break;
    }
}

auto makeMyRideProto(const db::Ride& ride,
                     db::Features photos,  //< sink
                     size_t previewSize,
                     size_t chunkSize,
                     const std::locale& locale,
                     const db::FeedbackIdToHypothesisTypeMap& hypotheses)
    -> yandex::maps::proto::mrcphoto::ugc::ride::MyRide
{
    auto result = yandex::maps::proto::mrcphoto::ugc::ride::MyRide{};
    result.set_id(TString{std::to_string(ride.rideId())});
    *result.mutable_started_at() = proto::makeTime(ride.startTime(), locale);
    *result.mutable_finished_at() = proto::makeTime(ride.endTime(), locale);
    *result.mutable_duration() =
        proto::makeDuration(ride.endTime() - ride.startTime(), locale);
    *result.mutable_distance() =
        proto::makeDistance(ride.distanceInMeters(), locale);
    result.set_photos_count(ride.photos());
    std::erase_if(photos, std::not_fn(hasPos));
    auto [track, photoStream] = makeTrack(photos);
    if (track.pointsNumber()) {
        *result.mutable_track() = geolib3::proto::encode(track);
        *result.mutable_bbox() = geolib3::proto::encode(track.boundingBox());
    }
    result.set_show_authorship(ride.showAuthorship());
    if (!photoStream.items.empty()) {
        *result.mutable_track_preview() = makeTrackPreviewProto(
            track, photoStream, previewSize, chunkSize, ride.clientId());
    }
    if (ride.clientId()) {
        result.set_client_ride_id(TString(*ride.clientId()));
    }
    if (ride.publishedPhotos()) {
        result.set_published_photos_count(*ride.publishedPhotos());
    }
    db::copyRideHypotheses(hypotheses, result);
    setRideStatus(ride, &result);
    ASSERT(result.IsInitialized());
    return result;
}

auto makeMyStatProto(const db::RideLiteViews& rides,
                     const std::locale& locale)
    -> yandex::maps::proto::mrcphoto::ugc::ride::MyStat
{
    auto result = yandex::maps::proto::mrcphoto::ugc::ride::MyStat{};
    result.set_rides_count(rides.size());

    std::chrono::seconds duration{0};
    double distanceInMeters{0};
    size_t photosCount{0};
    size_t publishedPhotosCount{0};

    for (const auto& ride: rides) {
        duration += std::chrono::duration_cast<std::chrono::seconds>(
            ride.endTime() - ride.startTime());
        distanceInMeters += ride.distanceInMeters();
        photosCount += ride.photos();
        publishedPhotosCount += ride.publishedPhotos().value_or(0);
    }

    *result.mutable_total_duration() = proto::makeDuration(duration, locale);
    *result.mutable_total_distance() = proto::makeDistance(distanceInMeters, locale);
    result.set_total_photos_count(photosCount);
    result.set_total_published_photos_count(publishedPhotosCount);
    return result;
}

auto makeTrackChunkProto(const db::Features& photos,
                         const Subpolyline& subpolyline,
                         const geolib3::Polyline2& geometry)
    -> yandex::maps::proto::mrcphoto::ugc::ride::TrackChunk
{
    auto result = yandex::maps::proto::mrcphoto::ugc::ride::TrackChunk{};
    auto [track, photoStream] = makeTrack(photos);

    // clang-format off
    // Проблема: "в превью и в чанках у одних и тех же фоток разные положения на полилинии"
    //   preview
    //     0, 0
    //     0, 1
    //     1, 1
    //     2, 1
    //     3, 1
    //     4, 1
    //   chunk
    //     0, 0
    //     1, 0.000241252
    //     2, 0.000969173
    //     2, 0.99657
    //     3, 0.9979
    //     4, 1
    // Причина: проецирование снимков с линии на саму себя, но пропущенную через protobuf
    // Решение: перед проецированием приводим линию "к общему знаменателю"
    // clang-format on
    track = geolib3::proto::decode(geolib3::proto::encode(track));

    transformPhotoStream(track, geometry, photoStream);
    for (auto& item : photoStream.items) {
        item.position.translateTo(subpolyline);
    }
    *result.mutable_photos() = encode(photoStream);
    return result;
}

auto makeGrinderArgs(const db::Ride& ride,
                     std::optional<bool> showAuthorship,
                     const yacare::Request& request)
    -> json::Value
{
    using Value = json::Value;
    auto result = std::map<std::string, Value, std::less<>>{
        {std::string{"type"}, Value(ride_correction::GRINDER_WORKER_TYPE)},
        {ride_correction::USER_ID_FIELD, Value(ride.userId())},
        {ride_correction::USER_IP_FIELD, Value(std::string(request.getClientIpAddress()))},
        {ride_correction::SOURCE_ID_FIELD, Value(ride.sourceId())},
        {ride_correction::START_TIME_FIELD,
         Value(std::chrono::floor<std::chrono::seconds>(
                   ride.startTime().time_since_epoch())
                   .count())},
        {ride_correction::END_TIME_FIELD,
         Value(std::chrono::ceil<std::chrono::seconds>(
                   ride.endTime().time_since_epoch())
                   .count())},
        {std::string{"__idempotent"}, Value(true)}};
    if (ride.clientId()) {
        result.insert(
            {ride_correction::CLIENT_RIDE_ID_FIELD, Value(*ride.clientId())});
    }
    if (showAuthorship.has_value()) {
        result.insert({ride_correction::SHOW_AUTHORSHIP_FIELD,
                       Value(showAuthorship.value())});
    }
    auto maybePort = request.getClientPort();
    if (maybePort.has_value()) {
        result.insert({ride_correction::USER_PORT_FIELD, Value(maybePort.value())});
    }
    return Value{std::move(result)};
}

std::optional<db::Ride> tryLoadRide(sql_chemistry::Transaction& txn,
                                    db::TId userId,
                                    db::TId rideId)
{
    return db::RideGateway{txn}.tryLoadOne(
        db::table::Ride::userId == std::to_string(userId) &&
        db::table::Ride::rideId == rideId &&
        !db::table::Ride::isDeleted.is(true));
}

db::Feature loadPhoto(sql_chemistry::Transaction& txn,
                      db::TId userId,
                      db::TId photoId,
                      db::Dataset dataset)
{
    auto photo = db::FeatureGateway{txn}.tryLoadOne(
        db::table::Feature::dataset == dataset &&
        db::table::Feature::userId == std::to_string(userId) &&
        db::table::Feature::id == photoId &&
        !db::table::Feature::deletedByUser.is(true) &&
        !db::table::Feature::gdprDeleted.is(true));
    if (!photo) {
        throw yacare::errors::NotFound() << "photo " << photoId << " not found";
    }
    return *photo;
}

chrono::TimePoint loadRidePhotoTime(
    sql_chemistry::Transaction& txn,
    const std::string& userId,
    const std::string& photoId)
{
    auto photo = db::FeatureGateway{txn}.tryLoadOne(
        db::table::Feature::dataset == db::Dataset::Rides &&
        db::table::Feature::userId == userId &&
        db::table::Feature::id == boost::lexical_cast<db::TId>(photoId));
    if (!photo) {
        throw yacare::errors::NotFound() << "photo " << photoId << " not found";
    }
    return getTimestamp(*photo);
}

template <class... Filters>
db::Features loadPhotos(sql_chemistry::Transaction& txn,
                        const std::string& userId,
                        const std::string& sourceId,
                        const std::optional<std::string>& clientRideId,
                        Filters&&... filters)
{
    auto allOf = makeFilter<db::table::Feature>(userId, sourceId, clientRideId);
    allOf.add(db::table::Feature::dataset == db::Dataset::Rides);
    allOf.add(!db::table::Feature::deletedByUser.is(true));
    allOf.add(!db::table::Feature::gdprDeleted.is(true));
    (allOf.add(std::forward<Filters>(filters)), ...);
    auto photos = db::FeatureGateway{txn}.load(allOf);
    std::sort(
        photos.begin(), photos.end(), [](const auto& lhs, const auto& rhs) {
            return std::make_pair(getTimestamp(lhs), lhs.id()) <
                   std::make_pair(getTimestamp(rhs), rhs.id());
        });
    if (photos.empty()) {
        throw yacare::errors::BadRequest() << "no photos";
    }
    return photos;
}

db::Features loadRidePhotosExcludingInterval(
    sql_chemistry::Transaction& txn,
    const db::Ride& ride,
    chrono::TimePoint excludeFrom,
    chrono::TimePoint excludeTo)
{
    sql_chemistry::FiltersCollection filter(sql_chemistry::op::Logical::Or);
    filter.add(db::table::Feature::date.between(ride.startTime(), excludeFrom));
    filter.add(db::table::Feature::date.between(excludeTo, ride.endTime()));

    return loadPhotos(
        txn,
        ride.userId(),
        ride.sourceId(),
        ride.clientId(),
        filter);
}

db::DeletedInterval makeDeletedInterval(
    const db::Ride& ride,
    chrono::TimePoint startedAt,
    chrono::TimePoint endedAt)
{
    return db::DeletedInterval(
        ride.userId(),
        ride.sourceId(),
        ride.clientId(),
        startedAt,
        endedAt);
}

std::optional<db::WalkObject> tryLoadWalkObject(
    sql_chemistry::Transaction& txn,
    db::TId userId,
    db::TId walkObjectId)
{
    return db::WalkObjectGateway{txn}.tryLoadOne(
        db::table::WalkObject::userId == std::to_string(userId) &&
        db::table::WalkObject::id == walkObjectId);
}

void applyUpdateRideRequest(
    const yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest& update,
    sql_chemistry::Transaction& txn,
    db::Ride& ride,
    db::Features& photos,
    db::DeletedIntervals& deletedIntervals)
{
    constexpr auto POSTGRES_TIMESTAMP_RESOLUTION = std::chrono::microseconds{1};
    auto showAuthorship = ride.showAuthorship();
    if (update.has_show_authorship()) {
        INFO() << "show_authorship = " << update.show_authorship();
        showAuthorship = update.show_authorship();
    }
    auto afterTime = std::optional<chrono::TimePoint>{};
    auto beforeTime = std::optional<chrono::TimePoint>{};
    if (update.has_delete_photos_after_photo_id()) {
        auto afterId = update.delete_photos_after_photo_id();
        INFO() << "delete_photos_after_photo_id = " << afterId;
        afterTime = loadRidePhotoTime(txn, ride.userId(), afterId);
    }
    if (update.has_delete_photos_before_photo_id()) {
        auto beforeId = update.delete_photos_before_photo_id();
        INFO() << "delete_photos_before_photo_id = " << beforeId;
        beforeTime = loadRidePhotoTime(txn, ride.userId(), beforeId);
    }

    if (afterTime && beforeTime && *afterTime <= *beforeTime) {
        auto after = *afterTime + POSTGRES_TIMESTAMP_RESOLUTION;
        auto before = *beforeTime - POSTGRES_TIMESTAMP_RESOLUTION;
        deletedIntervals.emplace_back(makeDeletedInterval(ride, after, before));
        photos = loadRidePhotosExcludingInterval(txn, ride, after, before);
    } else {
        if (afterTime && *afterTime + POSTGRES_TIMESTAMP_RESOLUTION <= ride.endTime()) {
            deletedIntervals.emplace_back(makeDeletedInterval(
                ride, *afterTime + POSTGRES_TIMESTAMP_RESOLUTION, ride.endTime()));
        }
        if (beforeTime && ride.startTime() <= *beforeTime - POSTGRES_TIMESTAMP_RESOLUTION) {
            deletedIntervals.emplace_back(makeDeletedInterval(
                ride, ride.startTime(), *beforeTime - POSTGRES_TIMESTAMP_RESOLUTION));
        }
        photos = loadPhotos(
            txn,
            ride.userId(),
            ride.sourceId(),
            ride.clientId(),
            db::table::Feature::date.between(
                beforeTime.value_or(ride.startTime()),
                afterTime.value_or(ride.endTime())));
    }
    ride.copyContentFrom(
        ride_inspector::makeRide(photos.begin(), photos.end()));
    ride.setShowAuthorship(showAuthorship);
}

void write(const google::protobuf::Message& message, const std::string& format)
{
    ASSERT(message.IsInitialized());
    yacare::Response& response = yacare::response();
    TString buf;
    bool success = true;
    if (format == FORMAT_PROTOBUF) {
        success = message.SerializeToString(&buf);
        response.setHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_PROTOBUF);
    }
    else if (format == FORMAT_TEXT) {
        success = google::protobuf::TextFormat::PrintToString(message, &buf);
        response.setHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_TEXT);
    }
    else if (format == FORMAT_JSON) {
        TStringOutput output(buf);
        message.PrintJSON(output);
        response.setHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON);
    }
    else {
        throw yacare::errors::BadRequest() << "unsupported format: " << format;
    }
    REQUIRE(success, "failed to serialize protobuf");
    response.write(buf.data(), buf.size());
}


void logUgcPhotoViewEvent(
    uint64_t userId,
    const db::TId photoId,
    const yacare::Request& request,
    ugc_event_logger::Action action)
{
    ugc_event_logger::UserInfo userInfo{.ip = std::string(request.getClientIpAddress())};
    auto maybePort = request.getClientPort();
    if (maybePort.has_value()) {
        userInfo.port = maybePort.value();
    }
    userInfo.uid = std::to_string(userId);
    Configuration::instance()->ugcEventLogger().logEvent(
        userInfo,
        action,
        ugc_event_logger::Photo{.id = std::to_string(photoId)}
    );
}

void writeJpeg(uint64_t userId,
               const db::Feature& photo,
               const std::string& sizeName,
               const yacare::Request& request)
{
    auto cfg = Configuration::instance();
    auto sizes = proto::prepareNamedSizes(photo);
    auto it = sizes.find(sizeName);
    if (it == sizes.end()) {
        throw yacare::errors::BadRequest() << "invalid size: " << sizeName;
    }
    auto size = it->second;
    auto img = common::decodeImage(cfg->mds().get(photo.mdsKey()));
    if (!getOrientation(photo).isNormal()) {
        img = transformByImageOrientation(img, getOrientation(photo));
    }
    if (size != common::Size(img.cols, img.rows)) {
        cv::Mat resized(static_cast<int>(size.height),
                        static_cast<int>(size.width),
                        CV_8UC3);
        cv::resize(img, resized, resized.size(), 0, 0);
        std::swap(img, resized);
    }
    auto jpg = mrc::common::encodeImage(img);
    yacare::Response& response = yacare::response();
    common::handleYandexOrigin(request, response);
    response.setHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_JPEG);
    response.write(reinterpret_cast<const char*>(jpg.data()), jpg.size());
    logUgcPhotoViewEvent(userId, photo.id(), request,
        ugc_event_logger::Action::View);
}

void tryPushContribution(const db::Ride& ride,
                         const std::optional<db::Feature>& albumImage,
                         const db::FeedbackIdToHypothesisTypeMap& hypothesis)
try {
    auto cfg = Configuration::instance();
    ugc_uploader::pushContribution(cfg->tvmClient(),
                                   cfg->contributionsModifyUrl(),
                                   cfg->mapsCoreNmapsMrcUgcBackUrl(),
                                   ride,
                                   albumImage,
                                   hypothesis);
}
catch (const std::exception& e) {
    WARN() << "unable to push ride " << ride.rideId()
           << " contribution: " << e.what();
}

void tryRejectSocialFeedbackTask(const db::WalkObject& walkObject)
{
    if (walkObject.status() != db::ObjectStatus::Published
        || !walkObject.hasFeedbackTaskId()
        || !walkObject.hasUserId()) {
        return;
    }

    auto& feedbackClient = Configuration::instance()->feedbackClient();
    auto taskId = walkObject.feedbackTaskId();

    try {
        auto task = feedbackClient.getById(taskId);
        if (!task.resolution) {
            feedbackClient.resolve(
                taskId,
                feedback_client::TaskResolution::Rejected);
        }
    } catch (const std::exception& e) {
        // There can be various reasons for the failure,
        // for example, task may be already acquired by another user.
        WARN() << "unable to reject feedback task " << taskId
               << ": " << e.what();
    }
}

}  // namespace

YCR_RESPOND_TO("GET /v1/rides/my/get",
               userId,
               token,
               ride_id,
               lang = RU_LANG,
               format = FORMAT_PROTOBUF,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto cfg = Configuration::instance();
    auto ride =
        tryLoadRide(*cfg->pool().slaveTransaction(token), userId, ride_id);
    if (!ride) {
        throw yacare::errors::NotFound() << "ride " << ride_id << " not found";
    }
    auto hypotheses =
        db::loadRideHypotheses(*cfg->pool().slaveTransaction(token), *ride);
    auto photos = loadPhotos(
        *cfg->pool().slaveTransaction(token),
        ride->userId(),
        ride->sourceId(),
        ride->clientId(),
        db::table::Feature::date.between(ride->startTime(), ride->endTime()));
    auto showAuthorship = ride->showAuthorship();
    ride->copyContentFrom(
        ride_inspector::makeRide(photos.begin(), photos.end()));
    ride->setShowAuthorship(showAuthorship);
    write(makeMyRideProto(*ride,
                          std::move(photos),
                          cfg->previewSize(),
                          cfg->chunkSize(),
                          getLocale(lang),
                          hypotheses),
          format);
}

YCR_RESPOND_TO("GET /v1/rides/my/photo",
               userId,
               token,
               id,
               lang = RU_LANG,
               format = FORMAT_PROTOBUF,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto cfg = Configuration::instance();
    auto photo = loadPhoto(
        *cfg->pool().slaveTransaction(token), userId, id, db::Dataset::Rides);
    const auto& ugcBack = makeBaseUrl(request);
    write(makeMyPhotoProto(photo, ugcBack, getLocale(lang)), format);
}

YCR_RESPOND_TO("OPTIONS /v1/rides/my/photo_image")
{
    if (common::handleYandexOrigin(request, response)) {
        response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
    }
}

YCR_RESPOND_TO("GET /v1/rides/my/photo_image",
               userId,
               token,
               photo_id,
               size_name = proto::SIZE_NAME_ORIGINAL)
{
    auto cfg = Configuration::instance();
    auto photo = loadPhoto(*cfg->pool().slaveTransaction(token),
        userId, photo_id, db::Dataset::Rides);
    writeJpeg(userId, photo, size_name, request);
}

YCR_RESPOND_TO("OPTIONS /v1/walk_objects/my/photos")
{
    if (common::handleYandexOrigin(request, response)) {
        response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
    }
}

YCR_RESPOND_TO("GET /v1/walk_objects/my/photos",
               userId,
               token,
               photo_id,
               size_name = proto::SIZE_NAME_ORIGINAL)
{
    auto cfg = Configuration::instance();
    auto photo = loadPhoto(*cfg->pool().slaveTransaction(token),
        userId, photo_id, db::Dataset::Walks);
    writeJpeg(userId, photo, size_name, request);
}

YCR_RESPOND_TO("PUT /v1/rides/my/update",
               userId,
               token,
               ride_id,
               lang = RU_LANG,
               format = FORMAT_PROTOBUF,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto update = yandex::maps::proto::mrcphoto::ugc::ride::UpdateRideRequest{};
    if (!update.ParseFromString(TString{request.body()})) {
        throw yacare::errors::BadRequest()
            << "invalid UpdateRideRequest (" << request.body().size()
            << " bytes)";
    }
    auto cfg = Configuration::instance();
    auto ride =
        tryLoadRide(*cfg->pool().slaveTransaction(token), userId, ride_id);
    if (!ride) {
        throw yacare::errors::NotFound() << "ride " << ride_id << " not found";
    }
    auto hypotheses =
        db::loadRideHypotheses(*cfg->pool().slaveTransaction(token), *ride);
    auto grinderArgs = makeGrinderArgs(
        *ride,
        update.has_show_authorship() ? std::optional{update.show_authorship()}
                                     : std::nullopt,
        request);
    auto photos = db::Features{};
    auto deletedIntervals = db::DeletedIntervals{};
    applyUpdateRideRequest(update,
                           *cfg->pool().slaveTransaction(token),
                           *ride,
                           photos,
                           deletedIntervals);
    auto txn = cfg->pool().masterWriteableTransaction();
    if (!deletedIntervals.empty()) {
        db::DeletedIntervalGateway{*txn}.insert(deletedIntervals);
    }
    db::RideGateway{*txn}.updatex(*ride);
    auto token = pgpool3::generateToken(*txn);
    txn->commit();
    setTokenHeader(token);
    cfg->grinderClient().submit(grinderArgs);
    write(makeMyRideProto(*ride,
                          std::move(photos),
                          cfg->previewSize(),
                          cfg->chunkSize(),
                          getLocale(lang),
                          hypotheses),
          format);
}

YCR_RESPOND_TO("DELETE /v1/rides/my/delete",
               userId,
               token,
               ride_id,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto cfg = Configuration::instance();
    auto ride =
        tryLoadRide(*cfg->pool().slaveTransaction(token), userId, ride_id);
    if (!ride) {
        return;
    }
    auto grinderArgs = makeGrinderArgs(*ride, std::nullopt, request);

    /// if ride with client_ride_id is deleted,
    /// then new photos outside its interval should also be deleted
    auto margin = std::chrono::years{ride->clientId() ? 100 : 0};
    auto deletedInterval = db::DeletedInterval(ride->userId(),
                                               ride->sourceId(),
                                               ride->clientId(),
                                               ride->startTime() - margin,
                                               ride->endTime() + margin);
    ride->setIsDeleted(true);
    auto txn = cfg->pool().masterWriteableTransaction();
    db::DeletedIntervalGateway{*txn}.insert(deletedInterval);
    db::RideGateway{*txn}.updatex(*ride);
    auto token = pgpool3::generateToken(*txn);
    txn->commit();
    setTokenHeader(token);
    cfg->grinderClient().submit(grinderArgs);

    tryPushContribution(*ride,
                        {},  //< album image, not used when deleting
                        {}   //< hypothesis, not used when deleting
    );
}

YCR_RESPOND_TO("POST /v1/rides/my/chunk",
               userId,
               token,
               format = FORMAT_PROTOBUF,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    using namespace std::chrono_literals;

    auto chunkDescriptor = yandex::maps::proto::mrcphoto::ugc::ride::
        TrackPreview::ChunkDescriptor{};
    if (!chunkDescriptor.ParseFromString(TString{request.body()})) {
        throw yacare::errors::BadRequest()
            << "invalid ChunkDescriptor (" << request.body().size()
            << " bytes)";
    }
    if (!chunkDescriptor.has_payload()) {
        throw yacare::errors::BadRequest() << "no ChunkDescriptor.payload";
    }
    auto payload = TString{chunkDescriptor.payload()};
    auto chunkDescriptorImpl = yandex::maps::proto::mrcphoto::ugc::
        chunk_descriptor_impl::ChunkDescriptorImpl{};
    if (!chunkDescriptorImpl.ParseFromString(payload)) {
        throw yacare::errors::BadRequest()
            << "invalid ChunkDescriptor.payload (" << payload.size()
            << " bytes)";
    }
    auto startTime = chrono::TimePoint{};
    startTime += std::chrono::milliseconds{chunkDescriptorImpl.started_at()};
    auto endTime = chrono::TimePoint{};
    endTime += std::chrono::milliseconds{chunkDescriptorImpl.finished_at()};
    auto cfg = Configuration::instance();
    auto photos =
        loadPhotos(*cfg->pool().slaveTransaction(token),
                   std::to_string(userId),
                   chunkDescriptorImpl.source_id(),
                   getClientRideId(chunkDescriptorImpl),
                   db::table::Feature::date.between(startTime, endTime),
                   db::table::Feature::pos.isNotNull());
    auto subpolyline = decode(chunkDescriptorImpl.subpolyline());
    auto geometry = geolib3::proto::decode(chunkDescriptorImpl.geometry());
    if (!photos.empty()) {
        auto frontTime = getTimestamp(photos.front());
        auto backTime = getTimestamp(photos.back());
        if (frontTime - startTime > 1ms || endTime - backTime > 1ms) {
            auto period = endTime - startTime - 2ms;
            auto startPos = ratio(frontTime - startTime, period);
            auto endPos = ratio(backTime - startTime, period);
            auto narrowSubpolyline =
                makeSubpolyline(geometry, startPos, endPos);
            auto narrowGeometry = makePartition(geometry, narrowSubpolyline);
            narrowSubpolyline.begin.translateTo(subpolyline);
            narrowSubpolyline.end.translateTo(subpolyline);

            subpolyline = std::move(narrowSubpolyline);
            geometry = std::move(narrowGeometry);
        }
    }
    write(makeTrackChunkProto(photos, subpolyline, geometry), format);
}

YCR_RESPOND_TO("GET /v1/rides/my/stat",
               userId,
               token,
               lang = RU_LANG,
               format = FORMAT_PROTOBUF,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto cfg = Configuration::instance();
    auto txn = cfg->pool().slaveTransaction(token);
    auto rides = db::RideLiteViewGateway{*txn}.load(
        db::table::RideLiteView::userId == std::to_string(userId) &&
        !db::table::RideLiteView::isDeleted.is(true));

    write(makeMyStatProto(rides, getLocale(lang)), format);
}

YCR_RESPOND_TO("DELETE /v1/walk_objects/my/delete",
               userId,
               token,
               id,
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource(RATELIMITER_SERVICE)))
{
    auto cfg = Configuration::instance();
    auto txn = cfg->pool().slaveTransaction(token);

    auto walkObject = tryLoadWalkObject(*txn, userId, id);
    if (!walkObject) {
        return;
    }

    auto photos = db::FeatureGateway{*txn}.load(
        db::table::Feature::walkObjectId == id);
    for (auto& photo : photos) {
        photo.setDeletedByUser(true);
        logUgcPhotoViewEvent(userId, photo.id(), request,
            ugc_event_logger::Action::Delete);
    }

    if (!photos.empty()) {
        auto txn = cfg->pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.update(photos, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    tryRejectSocialFeedbackTask(*walkObject);

    ugc_uploader::delWalkObjectContribution(
        cfg->tvmClient(),
        cfg->contributionsModifyUrl(),
        *walkObject);
}

}  // namespace maps::mrc::ugc_back
