#include "common.h"
#include "globals.h"
#include "serialization.h"

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/object_in_photo_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/graph_matcher_adapter/include/feature_positioner.h>

#include <maps/libs/sql_chemistry/include/exists.h>

#include <boost/lexical_cast.hpp>
#include <boost/range/algorithm.hpp>
#include <boost/range/algorithm_ext/erase.hpp>

namespace rng = boost::range;
namespace maps {
namespace mrc {
namespace tasks_planner {
namespace {

const std::string CAMERA_DIRECTION_PARAM = "cameraDirection";
const std::string DRY_RUN_PARAM = "dryRun";
const std::string GRAPH_TYPE_PARAM = "graphType";
const std::string HEADING_PARAM = "heading";
const std::string FILTER_PARAM = "filter";
const std::string IDS_PARAM = "ids";
const std::string IS_PUBLISHED_PARAM = "isPublished";
const std::string LOGIN_PARAM = "login";
const std::string PATCH_PARAM = "patch";
const std::string PHOTO_ID_PARAM = "photoId";
const std::string POSITION_PARAM = "position";
const std::string PRIVACY_PARAM = "privacy";
const std::string SNAP_TO_PARAM = "snapTo";
const std::string SOURCE_ID_PARAM = "sourceId";
const std::string TAKEN_AT_AFTER_PARAM = "takenAtAfter";
const std::string TAKEN_AT_BEFORE_PARAM = "takenAtBefore";
const std::string UID_PARAM = "uid";

bool patchIsPublished(db::Feature& feature, bool isPublished)
{
    if (feature.moderatorsShouldBePublished().has_value() &&
        feature.moderatorsShouldBePublished().value() == isPublished) {
        return false;
    }
    feature.setModeratorsShouldBePublished(isPublished);
    return true;
}

mrc::common::ImageOrientation rotateCW(
    const mrc::common::ImageOrientation& orientation,
    mrc::common::Rotation rotateBy)
{
    // In case the image orientation is to be flipped then the rotation angle
    // must be inverted.
    if (orientation.horizontalFlip()) {
       rotateBy = rotateBy + mrc::common::Rotation::CW_180;
    }
    return {orientation.horizontalFlip(), orientation.rotation() + rotateBy};
}

bool isValidRotation(mrc::common::Rotation rotateBy)
{
    return rotateBy == mrc::common::Rotation::CW_90 ||
           rotateBy == mrc::common::Rotation::CW_180 ||
           rotateBy == mrc::common::Rotation::CW_270;
}

struct PhotoFilter {

    std::optional<std::string> sourceId;
    db::TIds ids;
    std::optional<std::string> uid;
    std::optional<chrono::TimePoint> takenAtAfter;
    std::optional<chrono::TimePoint> takenAtBefore;
};


PhotoFilter parsePhotoFilter(const json::Value& filter)
try {
    PhotoFilter result;
    if (filter.hasField(IDS_PARAM)) {
        auto items = filter[IDS_PARAM];
        for (const auto& item : items) {
            auto id = boost::lexical_cast<db::TId>(item.as<std::string>());
            result.ids.push_back(id);
        }
    }
    else if (filter.hasField(SOURCE_ID_PARAM)) {
        result.sourceId = filter[SOURCE_ID_PARAM].as<std::string>();
    }

    if (filter.hasField(TAKEN_AT_AFTER_PARAM)) {
        result.takenAtAfter = chrono::parseIsoDateTime(filter[TAKEN_AT_AFTER_PARAM].as<std::string>());
    }

    if (filter.hasField(TAKEN_AT_BEFORE_PARAM)) {
        result.takenAtBefore = chrono::parseIsoDateTime(filter[TAKEN_AT_BEFORE_PARAM].as<std::string>());
    }

    if (filter.hasField(LOGIN_PARAM)) {
        std::string login = filter[LOGIN_PARAM].as<std::string>();
        std::optional<blackbox_client::Uid> optUid = Globals::blackbox().uidByLogin(login);
        if (!optUid.has_value()) {
            throw maps::Exception() << "wrong login";
        }
        result.uid = std::to_string(optUid.value());
    }

    if (filter.hasField(UID_PARAM)) {
        result.uid = filter[UID_PARAM].as<std::string>();
    }

    return result;
} catch (const yacare::Error&) {
    throw;
}
catch(const Exception& ex) {
    WARN() << "Could not parse PhotoFilter '" << filter << "': "<< ex;
    throw yacare::errors::BadRequest();
}

// the service is not intended to deal with Features without image size
auto makeFeaturesWithSizeFilter()
{
    return db::table::Feature::width.isNotNull() &&
        db::table::Feature::height.isNotNull();
}

sql_chemistry::FiltersCollection
makeFilterClause(const PhotoFilter& filter)
{
    sql_chemistry::FiltersCollection where{sql_chemistry::op::Logical::And};

    if (!filter.ids.empty()) {
        where.add(db::table::Feature::id.in(filter.ids));
    }

    if (filter.sourceId.has_value()) {
        where.add(db::table::Feature::sourceId == filter.sourceId.value());
    }

    if (filter.uid.has_value()) {
        where.add(db::table::Feature::userId == filter.uid.value());
    }

    if (filter.takenAtAfter.has_value()) {
        where.add(db::table::Feature::date >= filter.takenAtAfter.value());
    }

    if (filter.takenAtBefore.has_value()) {
        where.add(db::table::Feature::date <= filter.takenAtBefore.value());
    }

    return where;
}

/**
 * @see
 * https://a.yandex-team.ru/arc/trunk/arcadia/maps/wikimap/mapspro/schemas/mrc/tasks-planner/definitions.schema.json?rev=6354869#L171
 */
void patchFeatures(db::Features& photos, const json::Value& patch)
{
    if (patch.hasField(CAMERA_DIRECTION_PARAM)) {
        auto side = fromString<db::CameraDeviation>(
            patch[CAMERA_DIRECTION_PARAM].as<std::string>());
        for (auto& photo : photos) {
            photo.setCameraDeviation(side);
        }
    }

    if (patch.hasField(IS_PUBLISHED_PARAM)) {
        if (patch[IS_PUBLISHED_PARAM].as<bool>()) {
            for (auto& photo : photos) {
                patchIsPublished(photo, true);
            }
        }
        else {
            for (auto& photo : photos) {
                patchIsPublished(photo, false);
            }
        }
    }

    if (patch.hasField(PRIVACY_PARAM)) {
        auto privacy = fromString<db::FeaturePrivacy>(
            patch[PRIVACY_PARAM].as<std::string>());
        for (auto& photo : photos) {
            photo.setPrivacy(privacy);
        }
    }
}

auto lessId = [](auto& l, auto& r) { return l.id() < r.id(); };
auto lessTime = [](auto& l, auto& r) { return l.timestamp() < r.timestamp(); };

db::TrackPoint toAugmentedTrackPoint(
    const db::Feature& photo,
    const geolib3::Point2& geodeticPos,
    const std::optional<geolib3::Heading>& heading)
{
    db::TrackPoint result;
    result.setSourceId(photo.sourceId())
        .setTimestamp(photo.timestamp())
        .setGeodeticPos(geodeticPos)
        .setAccuracyMeters(0)
        .setIsAugmented(true);
    if (heading.has_value()) {
        result.setHeading(heading.value());
    }
    return result;
}

void match(db::GraphType graphType,
           const adapters::CompactGraphMatcherAdapter& matcher,
           sql_chemistry::Transaction& txn,
           const db::TrackPoints& snapTo,
           db::Features& photos)
{
    auto trackPointsProvider = [&](const std::string& sourceId,
                                   chrono::TimePoint startTime,
                                   chrono::TimePoint endTime) {
        auto result = db::TrackPointGateway{txn}.load(
            db::table::TrackPoint::sourceId.equals(sourceId) &&
            db::table::TrackPoint::timestamp.between(startTime, endTime) &&
            (db::table::TrackPoint::isAugmented.isNull() ||
             db::table::TrackPoint::isAugmented.is(false)));
        result.insert(result.end(), snapTo.begin(), snapTo.end());
        return result;
    };
    auto trackType = db::GraphType::Road == graphType
                         ? track_classifier::TrackType::Vehicle
                         : track_classifier::TrackType::Pedestrian;
    adapters::FeaturePositioner positioner{{{graphType, &matcher}},
                                           trackPointsProvider,
                                           adapters::classifyAs(trackType)};
    positioner(photos);
}

struct AdjustPositionsParams {

    struct FeaturePositionParam {
        FeaturePositionParam(const json::Value& jsonValue)
            : featureId(boost::lexical_cast<db::TId>(jsonValue[PHOTO_ID_PARAM].as<std::string>()))
            , geodeticPos(jsonValue[POSITION_PARAM][0].as<double>(), jsonValue[POSITION_PARAM][1].as<double>())
        {
            if (jsonValue.hasField(HEADING_PARAM)) {
                heading = geolib3::Heading{jsonValue[HEADING_PARAM].as<double>()};
            }
        }

        db::TId featureId;
        geolib3::Point2 geodeticPos;
        std::optional<geolib3::Heading> heading;
    };


    AdjustPositionsParams(const json::Value& jsonValue)
    {
        auto filterJson = jsonValue[FILTER_PARAM];
        sourceId = filterJson[SOURCE_ID_PARAM].as<std::string>();
        takenAtAfter = chrono::parseIsoDateTime(filterJson[TAKEN_AT_AFTER_PARAM].as<std::string>());
        takenAtBefore = chrono::parseIsoDateTime(filterJson[TAKEN_AT_BEFORE_PARAM].as<std::string>());

        for (const auto& snapValue : jsonValue[SNAP_TO_PARAM]) {
            snappedFeatures.emplace_back(snapValue);
        }

        if (jsonValue.hasField(DRY_RUN_PARAM)) {
            dryRun = jsonValue[DRY_RUN_PARAM].as<bool>();
        }

        if (jsonValue.hasField(GRAPH_TYPE_PARAM)) {
            const std::string str =
                jsonValue[GRAPH_TYPE_PARAM].as<std::string>();
            graphType = FromString<db::GraphType>(str.c_str());
        }
    }

    std::string sourceId;
    chrono::TimePoint takenAtAfter;
    chrono::TimePoint takenAtBefore;
    std::vector<FeaturePositionParam> snappedFeatures;
    bool dryRun = false;
    db::GraphType graphType = db::GraphType::Road;
};


AdjustPositionsParams parseAdjustPositionsParams(const std::string& body)
try {
    return AdjustPositionsParams(json::Value::fromString(body));
} catch (const yacare::Error&) {
    throw;
} catch(const Exception& ex) {
    WARN() << "Could not parse AdjustPositionsParams '" << body << "': "<< ex;
    throw yacare::errors::BadRequest();
}


db::TrackPoints extractSnapped(db::Features& photos,
                               const AdjustPositionsParams& params,
                               sql_chemistry::Transaction& txn)
{
    db::TrackPoints result;
    rng::sort(photos, lessId);
    for (const auto& snappedFeature : params.snappedFeatures) {
        auto [first, last] = rng::equal_range(
            photos, db::Feature{snappedFeature.featureId}, lessId);
        std::for_each(first, last, [&](db::Feature& feature) {
            result.push_back(toAugmentedTrackPoint(
                feature, snappedFeature.geodeticPos, snappedFeature.heading));
        });
    }
    rng::sort(result, lessTime);

    auto previous = db::TrackPointGateway{txn}.load(
        db::table::TrackPoint::sourceId.equals(params.sourceId) &&
        db::table::TrackPoint::timestamp.between(params.takenAtAfter,
                                                 params.takenAtBefore) &&
        db::table::TrackPoint::isAugmented.is(true));
    for (auto& trackPoint : previous) {
        auto it = std::lower_bound(
            result.begin(), result.end(), trackPoint, lessTime);
        if (it == result.end() || it->timestamp() != trackPoint.timestamp()) {
            result.insert(it, trackPoint);
        }
        else {
            trackPoint.setGeodeticPos(it->geodeticPos());
            if (auto heading = it->heading(); heading.has_value()) {
                trackPoint.setHeading(heading.value());
            }
            else {
                trackPoint.resetHeading();
            }
            *it = trackPoint;
        }
    }
    return result;
}


/**
 * @return positioned photos and manual coordinates
 */
std::pair<db::Features, db::TrackPoints> adjustPositions(
    db::GraphType graphType,
    adapters::CompactGraphMatcherAdapter& matcher,
    sql_chemistry::Transaction& txn,
    const AdjustPositionsParams& params)
{
    db::Features photos = db::FeatureGateway{txn}
        .load(
            makeFeaturesWithSizeFilter() &&
            db::table::Feature::sourceId == params.sourceId &&
            db::table::Feature::date >= params.takenAtAfter &&
            db::table::Feature::date <= params.takenAtBefore);

    db::TrackPoints snapped = extractSnapped(photos, params, txn);

    match(graphType, matcher, txn, snapped, photos);
    return {std::move(photos), std::move(snapped)};
}

} // anonymous namespace

YCR_RESPOND_TO("GET /photos/$")
{
    auto id = pathnameParam<int64_t>(0);
    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
        .tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    response << YCR_JSON(obj) {
        toJson(obj, baseUrl(request), photo.value());
    };
}

YCR_RESPOND_TO("PATCH /photos/$")
{
    auto id = pathnameParam<int64_t>(0);

    auto txn = Globals::pool().masterWriteableTransaction();
    db::FeatureGateway gtw{*txn};
    auto photo = gtw.tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    bool patched = false;

    const auto isPublished = request.optParam<bool>("is_published");
    if (isPublished.has_value()) {
        patched = patchIsPublished(photo.value(), isPublished.value());
    }

    const auto rotateBy = request.optParam<mrc::common::Rotation>("rotate_by");
    if (rotateBy) {
        if (!isValidRotation(rotateBy.value())) {
            throw yacare::errors::BadRequest();
        }

        photo->setOrientation(
            rotateCW(photo->orientation(), rotateBy.value()));
        patched = true;
    }

    if (patched) {
        gtw.update(photo.value(), db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    response << YCR_JSON(obj) {
        toJson(obj, baseUrl(request), photo.value());
    };
}

YCR_RESPOND_TO("GET /photos/$/image")
{
    auto id = pathnameParam<int64_t>(0);
    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
        .tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    auto image = common::decodeImage(Globals::mds().get(photo->mdsKey()));
    if (photo->hasOrientation()) {
        image = transformByImageOrientation(image, photo->orientation());
    }

    auto encodedImage = maps::mrc::common::encodeImage(image);

    response.setHeader("Content-Type", "image/jpeg");
    response.write(reinterpret_cast<const char*>(encodedImage.data()),
                   encodedImage.size());
}

YCR_RESPOND_TO("GET /photos/$/thumbnail")
{
    auto id = pathnameParam<int64_t>(0);
    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
        .tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    response.setHeader("Content-Type", "image/jpeg");

    auto image = common::decodeImage(Globals::mds().get(photo->mdsKey()));
    if (photo->hasOrientation()) {
        image = transformByImageOrientation(image, photo->orientation());
    }
    const auto size = common::getThumbnailSize(image.cols, image.rows);
    cv::Mat thumbnail(static_cast<int>(size.height), static_cast<int>(size.width), CV_8UC3);
    cv::resize(image, thumbnail, thumbnail.size(), 0, 0);
    auto encodedImage = maps::mrc::common::encodeImage(thumbnail);

    response.write(reinterpret_cast<const char*>(encodedImage.data()),
                   encodedImage.size());
}

YCR_RESPOND_TO("GET /photos/$/objects")
{
    auto id = pathnameParam<db::TId>(0);

    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
        .tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    const auto objects =
        db::ObjectInPhotoGateway(*Globals::pool().slaveTransaction())
            .load(db::table::ObjectInPhotoTable::featureId == id);

    response << YCR_JSON_ARRAY(arr){
        for(const auto& object : objects) {
            arr << [&](json::ObjectBuilder builder) {
                toJson(builder, photo.value(), object);
            };
        }
    };
}

YCR_RESPOND_TO("POST /photos/$/objects")
{
    auto id = pathnameParam<db::TId>(0);

    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
        .tryLoadOne(makeFeaturesWithSizeFilter() && db::table::Feature::id == id);

    if (!photo) {
        throw yacare::errors::NotFound();
    }

    auto paramsJson = parseJsonFromRequestBodyElse400(request);
    auto object = tryCallElse400(createObjectInPhoto, photo.value(), paramsJson);

    {
        auto txn = Globals::pool().masterWriteableTransaction();
        db::ObjectInPhotoGateway gtw(*txn);
        gtw.insert(object);
        db::updateFeatureTransaction(*photo, *txn);
        txn->commit();
    }

    response.setStatus(yacare::HTTPStatus::Created);
    response << YCR_JSON(obj) {
        toJson(obj, photo.value(), object);
    };
}

YCR_RESPOND_TO("DELETE /photos/$/objects/$")
{
    auto photoId = pathnameParam<int64_t>(0);
    auto photo = db::FeatureGateway(*Globals::pool().slaveTransaction())
                     .tryLoadOne(makeFeaturesWithSizeFilter() &&
                                 db::table::Feature::id == photoId);
    if (!photo) {
        throw yacare::errors::NotFound();
    }

    auto objectId = pathnameParam<int64_t>(1);
    auto txn = Globals::pool().masterWriteableTransaction();
    db::ObjectInPhotoGateway gtw(*txn);
    auto object = gtw.tryLoadById(objectId);
    if (!object.has_value() || object->featureId() != photoId) {
        throw yacare::errors::NotFound();
    }

    gtw.removeById(objectId);
    db::updateFeatureTransaction(*photo, *txn);
    txn->commit();
}

YCR_RESPOND_TO("GET /photos", uid = std::string(), login = std::string(), results = 0, skip = 0)
{
    db::Features photos;
    unsigned totalCount = 0;

    PhotoFilter filter;
    if (has(uid)) {
        filter.uid = uid;
    } else if (has(login)) {
        std::optional<blackbox_client::Uid> optUid = Globals::blackbox().uidByLogin(login);
        if (!optUid.has_value()) {
            throw yacare::errors::BadRequest() << "unknown login";
        }
        filter.uid = std::to_string(optUid.value());
    }

    filter.sourceId = request.optParam("source_id");
    filter.takenAtAfter = parseOptionalDateParam(request, "taken_at_after");
    filter.takenAtBefore = parseOptionalDateParam(request, "taken_at_before");

    if (auto photoId = request.optParam<db::TId>("photo_id")) {
        filter.ids.push_back(photoId.value());
    }

    bool sortDesc = false;
    if (std::optional<std::string> optSortOrder = request.optParam("sort")) {
        if (optSortOrder.value() == "asc") {
            sortDesc = false;
        } else if (optSortOrder.value() == "desc") {
            sortDesc = true;
        } else {
            throw yacare::errors::BadRequest() << "wrong value of 'sort' param";
        }
    }

    auto filterClause = makeFeaturesWithSizeFilter() && makeFilterClause(filter);
    auto orderBy = sql_chemistry::orderBy(db::table::Feature::date);

    if (sortDesc) {
        orderBy.desc();
    }

    orderBy.orderBy(db::table::Feature::id);
    if (sortDesc) {
        orderBy.desc();
    }

    if (has(skip)) {
        orderBy.offset(skip);
    }

    if (has(results)) {
        orderBy.limit(results);
    }

    auto txn = Globals::pool().slaveTransaction();
    photos = db::FeatureGateway{*txn}.load(filterClause, orderBy);
    totalCount = db::FeatureGateway{*txn}.count(filterClause);

    setTotalCountHeader(response, totalCount);
    auto baseUrlStr = baseUrl(request);
    response << YCR_JSON_ARRAY(arr)
    {
        for (const auto& photo : photos) {
            arr << [&](json::ObjectBuilder builder) {
                toJson(builder, baseUrlStr, photo);
            };
        }
    };
}

YCR_RESPOND_TO("PATCH /photos_is_published_batch_update")
{
    static const auto PUBLISH_PARAM = "publish";
    static const auto UNPUBLISH_PARAM = "unpublish";

    auto txn = Globals::pool().masterWriteableTransaction();
    auto gtw = db::FeatureGateway{*txn};
    auto body = request.body().empty()
                    ? json::Value::nonexistent()
                    : json::Value::fromString(request.body());

    auto parseIds = [&](const std::string& param) {
        std::vector<db::TId> result;
        if (request.input().has(param)) {
            result = vectorQueryParam<db::TId>(request, param);
        }
        if (body.exists() && body.hasField(param)) {
            auto items = body[param];
            for (auto item: items) {
                auto id = boost::lexical_cast<db::TId>(item.as<std::string>());
                result.push_back(id);
            }
        }
        return result;
    };

    if (auto ids = parseIds(PUBLISH_PARAM); !ids.empty()) {
        auto photos = gtw.load(makeFeaturesWithSizeFilter() && db::table::Feature::id.in(ids));
        for (auto& photo : photos) {
            patchIsPublished(photo, true);
        }
        gtw.update(photos, db::UpdateFeatureTxn::Yes);
    }

    if (auto ids = parseIds(UNPUBLISH_PARAM); !ids.empty()) {
        auto photos = gtw.load(makeFeaturesWithSizeFilter() && db::table::Feature::id.in(ids));
        for (auto& photo : photos) {
            patchIsPublished(photo, false);
        }
        gtw.update(photos, db::UpdateFeatureTxn::Yes);
    }

    txn->commit();
}

/**
 * body example:
 * @code
 * [
 *   {
 *     "filter":
 *       {
 *         "sourceId":"1234",
 *         "takenAtAfter":"2020-02-14T13:33:48.153731000Z",
 *         "takenAtBefore":"2020-02-14T13:33:48.153731000Z"
 *       },
 *     "patch":{"cameraDirection":"Right"}
 *   },
 *   {
 *     "filter":{"ids":["2"]},
 *     "patch":{"isPublished":true}
 *   },
 *   {
 *     "filter":{"ids":["3"]},
 *     "patch":{"privacy":"Restricted"}
 *   }
 * ]
 * @endcode
 */
YCR_RESPOND_TO("PATCH /photos_batch_edit")
{
    auto txn = Globals::pool().masterWriteableTransaction();
    auto body = json::Value::fromString(request.body());

    for (const auto& item : body) {
        PhotoFilter filter = parsePhotoFilter(item[FILTER_PARAM]);
        bool filterIsCorrectlyDefined = !filter.ids.empty() ||
            ((filter.sourceId.has_value() || filter.uid.has_value()) &&
                filter.takenAtAfter.has_value() && filter.takenAtBefore.has_value());
        if (!filterIsCorrectlyDefined) {
            throw yacare::errors::BadRequest() << "Malformed PhotoFilter";
        }

        auto photos = db::FeatureGateway{*txn}.load(makeFeaturesWithSizeFilter() && makeFilterClause(filter));
        patchFeatures(photos, item[PATCH_PARAM]);
        db::FeatureGateway{*txn}.update(photos, db::UpdateFeatureTxn::Yes);
    }
    txn->commit();
}

YCR_RESPOND_TO("POST /photos_adjust_positions")
{
    auto params = parseAdjustPositionsParams(request.body());
    INFO() << "dryRun = " << params.dryRun;
    auto txn = params.dryRun ? Globals::pool().masterReadOnlyTransaction()
                             : Globals::pool().masterWriteableTransaction();

    /**
     * @see
     * https://stackoverflow.com/questions/46114214/lambda-implicit-capture-fails-with-variable-declared-from-structured-binding
     */
    auto pair = adjustPositions(params.graphType,
                                params.graphType == db::GraphType::Road
                                    ? Globals::roadMatcher()
                                    : Globals::pedestrianMatcher(),
                                *txn,
                                params);
    auto& photos = pair.first;
    auto& snapped = pair.second;

    auto baseUrlStr = baseUrl(request);
    response << YCR_JSON_ARRAY(arr)
    {
        for (const auto& photo: photos) {
            arr << [&](json::ObjectBuilder builder) {
                toJson(builder, baseUrlStr, photo);
            };
        }
    };

    if (! params.dryRun) {
        db::FeatureGateway{*txn}.update(photos, db::UpdateFeatureTxn::Yes);
        db::TrackPointGateway{*txn}.upsert(snapped);
        txn->commit();
    }
}

} // namespace tasks_planner
} // namespace mrc
} // namespace maps
