#include "feature_author.h"
#include "graph_coverage_explorer.h"
#include "tile_renderer.h"
#include "tools.h"
#include "yacare_params.h"

#include <maps/analyzer/libs/graphmatching/include/match_geometry.h>
#include <maps/analyzer/libs/shortest_path/include/convert.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/proto/track_chunk_descriptor.pb.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/geometry.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/yacare_helpers.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/fbs/storage.fbs64.h>
#include <maps/wikimap/mapspro/services/mrc/libs/proto/include/common.h>
#include <maps/libs/geolib/include/closest_point.h>
#include <maps/libs/geolib/include/projection.h>
#include <yandex/maps/geolib3/proto.h>
#include <yandex/maps/geolib3/sproto.h>
#include <yandex/maps/proto/common2/image.sproto.h>
#include <yandex/maps/proto/mrcphoto/photo.sproto.h>

#include <maps/infra/yacare/include/params/tvm.h>

#include <boost/lexical_cast.hpp>
#include <boost/range/adaptor/filtered.hpp>

#include <ctype.h>
#include <stdlib.h>

#include <chrono>

namespace maps::mrc::browser {

namespace {

namespace sproto = yandex::maps::sproto;
namespace proto = yandex::maps::proto;

constexpr db::CameraDeviation PHOTO_API_CAMERA_DEVIATION{
    db::CameraDeviation::Front};

constexpr db::FeaturePrivacy PHOTO_API_FEATURE_PRIVACY{
    db::FeaturePrivacy::Public};

const FeatureFilter PHOTO_API_FEATURE_FILTER{
    .cameraDeviation = PHOTO_API_CAMERA_DEVIATION};

const Meters MAX_VISIBILITY_DISTANCE{export_gen::COVERAGE_METERS_MAX_PATH};

constexpr Meters NEGLIGIBLE_COVERAGE_GAP{1};
constexpr Meters TRACK_PREVIEW_SPARSE_DISTANCE{100};
constexpr std::size_t PHOTOS_PER_TRACK_CHUNK_LIMIT{10000};

template<typename T>
std::vector<T> join(const std::vector<T>& lhs, const std::vector<T>& rhs)
{
    std::vector<T> result = lhs;
    result.insert(result.end(), rhs.begin(), rhs.end());
    return result;
}

template<typename T>
std::vector<T> join(std::vector<T>&& lhs, std::vector<T>&& rhs)
{
    lhs.insert(
        lhs.end(),
        std::make_move_iterator(rhs.begin()),
        std::make_move_iterator(rhs.end()));
    return lhs;
}

std::string getMrcPhotoLayer(const auto l)
{
    REQUIRE(!l.empty(), yacare::errors::BadRequest("No 'l' parameter set"));

    const auto layer = l.at(0);

    REQUIRE(
        layer == LAYER_PHOTO_AUTOMOTIVE || layer == LAYER_PHOTO_PEDESTRIAN,
        yacare::errors::BadRequest("Unexpected photo layer"));

    return layer;
}

const fb::TEdgeCoverage* photoApiCoverageSelector(const fb::TEdge& coveredEdge)
{
    for (const auto& coverage: coveredEdge.coverages) {
        if (coverage.cameraDeviation == PHOTO_API_CAMERA_DEVIATION &&
            coverage.privacy <= PHOTO_API_FEATURE_PRIVACY) {
            return &coverage;
        }
    }
    return nullptr;
}

db::Features reduceHistoricalFeatures(db::Features historicalFeatures)
{
    constexpr std::size_t HISTORICAL_PHOTOS_LIMIT = 10;
    std::sort(
        historicalFeatures.begin(),
        historicalFeatures.end(),
        [](const auto& lhs, const auto& rhs) {
            return lhs.timestamp() > rhs.timestamp();
        });

    // TODO: partition features which are not older than one year by
    //       months and older features by seasons. Notice that features older
    //       than the base feature by more than 10 years are thrown away. But
    //       for now just limit total historical features quantity.

    if (HISTORICAL_PHOTOS_LIMIT < historicalFeatures.size()) {
        db::Features reduced;
        const float bound = float(historicalFeatures.size()) - 0.5f;
        const float stride =
            historicalFeatures.size() / float(HISTORICAL_PHOTOS_LIMIT);

        for (float point = 0.0; point < bound; point += stride) {
            reduced.push_back(historicalFeatures[std::lround(point)]);
        }
        return reduced;
    }

    return historicalFeatures;
}

db::Features getHistoricalFeaturesByBbox(
    IDataAccess& data,
    const IGraph& graph,
    const geolib3::BoundingBox& bbox)
{
    std::set<db::TId> coverageFeatureIds;
    for (auto edgeId: graph.getCoveredEdgeIdsByBbox(bbox, MAX_FC)) {
        if (const auto coveredEdge =
                graph.getCoveredEdgeById(graph.graph().base(edgeId))) {
            const auto edgeCoverage = photoApiCoverageSelector(*coveredEdge);

            if (!edgeCoverage) {
                continue;
            }

            for (const auto& coveredSubpolyline:
                 edgeCoverage->coveredSubpolylines) {
                coverageFeatureIds.insert(coveredSubpolyline.featureId());
            }
        }
    }

    auto nearbyFeatures = data.getFeaturesByBbox(bbox);
    nearbyFeatures.erase(
        std::remove_if(
            nearbyFeatures.begin(),
            nearbyFeatures.end(),
            [&](const auto& feature) {
                return coverageFeatureIds.count(feature.id()) > 0;
            }),
        nearbyFeatures.end());

    return nearbyFeatures;
}

db::Features evalHistoricalFeatures(
    IDataAccess& data,
    const IGraph& graph,
    const db::Feature& feature,
    const GraphCoverageExplorer::Node& node,
    const GraphCoverageExplorer::Nodes& connections)
{
    // Collect all features spread among a node and its connections and keep
    // only those which a closer to the node itself.
    //
    // Note: a feaure is considered historical if its heading is ±45°
    //       comparing to the node heading.
    const auto nearbyFeatures = getHistoricalFeaturesByBbox(
        data,
        graph,
        db::expandBbox(node.ray.pos.boundingBox(), MAX_VISIBILITY_DISTANCE.value()));

    const auto isInHistoricalCluster = [&](const auto& nearbyFeature) {
        if (feature.id() == nearbyFeature.id() ||
            nearbyFeature.hasCameraDeviation() &&
            nearbyFeature.cameraDeviation() != feature.cameraDeviation()) {
            return false;
        }

        if (PI / 4 <
            geolib3::angleBetween(geolib3::Direction2{nearbyFeature.heading()},
                                  node.ray.direction)) {
            return false;
        }

        const auto nearbyFeaturePos = nearbyFeature.geodeticPos();
        const auto distanceToNode =
            geolib3::fastGeoDistance(nearbyFeaturePos, node.ray.pos);

        if (MAX_VISIBILITY_DISTANCE.value() < distanceToNode) {
            return false;
        }

        for (const auto& connection: connections) {
            if (geolib3::fastGeoDistance(nearbyFeaturePos, connection.ray.pos) <
                distanceToNode)
                return false;
        }

        return true;
    };

    db::Features historicalCluster;

    if (node.featureId != feature.id() && !data.isUnpublished(node.featureId)) {
        // This means the historical features are collected for a historical
        // feature so it needs to add the actual feature from the coverage too.
        auto feature = data.tryGetFeatureById(node.featureId);
        ASSERT(feature);
        historicalCluster.push_back(std::move(*feature));
    }

    for (auto& nearbyFeature: nearbyFeatures) {
        if (isInHistoricalCluster(nearbyFeature)) {
            historicalCluster.push_back(std::move(nearbyFeature));
        }
    }

    return reduceHistoricalFeatures(std::move(historicalCluster));
}

template <class Result, class Proto>
Result protoTo(const Proto& msg)
{
    auto str = TString{};
    REQUIRE(msg.SerializeToString(&str), "failed to serialize proto");
    auto is = std::istringstream{str};
    auto result = Result{};
    is >> result;
    return result;
}

sproto::common2::Image makeGeoPhotoImage(
    const db::Feature& feature,
    const std::string& baseUrl)
{
    auto url = http::URL{baseUrl}.setPath(
        "/feature/" + std::to_string(feature.id()) + "/image");
    auto image = maps::mrc::proto::makeImage(
        url, "size_name", maps::mrc::proto::prepareNamedSizes(feature));
    return protoTo<sproto::common2::Image>(image);
}

sproto::common2::GeoPhoto::ShootingPoint makeGeoPhotoShootingPoint(
    const geolib3::Point2& geodeticPos, geolib3::Heading heading)
{
    sproto::common2::geometry::Point point;
    point.lat() = geodeticPos.y();
    point.lon() = geodeticPos.x();

    sproto::common2::GeoPhoto::Point3D point3d;
    point3d.point() = point;
    // Note: altitude value is not available

    sproto::common2::GeoPhoto::ShootingPoint shootingPoint;
    shootingPoint.point() = point3d;

    sproto::common2::geometry::Direction direction;
    direction.azimuth() = heading.value();
    direction.tilt() = 0.0; // Note: tilt is not provided

    shootingPoint.direction() = direction;

    return shootingPoint;
}

auto getAvatarImage(std::string_view avatarId)
    -> yandex::maps::proto::common2::Image
{
    /// @see https://docs.yandex-team.ru/blackbox/methods/userinfo
    static const auto namedSizes = maps::mrc::proto::TNamedSizes{
        {"islands-small", {28, 28}},
        {"islands-34", {34, 34}},
        {"islands-middle", {42, 42}},
        {"islands-50", {50, 50}},
        {"islands-retina-small", {56, 56}},
        {"islands-68", {68, 68}},
        {"islands-75", {75, 75}},
        {"islands-retina-middle", {84, 84}},
        {"islands-retina-50", {100, 100}},
        {"islands-200", {200, 200}},
    };
    auto result = yandex::maps::proto::common2::Image{};
    result.set_url_template(getAvatarUrl(avatarId, "%s"));
    for (const auto& [name, size] : namedSizes) {
        auto item = result.add_size();
        item->set_size(TString{name});
        item->set_width(size.width);
        item->set_height(size.height);
    }
    return result;
}

sproto::common2::attribution::Attribution makeGeoPhotoAttribution(
    const db::Feature& feature)
{
    const auto userInfo = getBlackboxUserInfo(feature);
    const auto displayName = getDisplayName(userInfo);

    sproto::common2::attribution::Author author;
    author.name() = displayName.name;
    author.uri() =
        "urn:yandex-puid:" +
        userInfo.publicId().value_or(YANDEX_USER_INFO.publicId().value());

    sproto::common2::attribution::Attribution attribution;
    attribution.author() = author;

    auto image = getAvatarImage(*displayName.avatarId);
    attribution.avatar_image() = protoTo<sproto::common2::Image>(image);

    return attribution;
}

sproto::common2::GeoPhoto makeGeoPhoto(
    const db::Feature& feature,
    const std::string& baseUrl)
{
    sproto::common2::GeoPhoto geoPhoto;

    geoPhoto.image() = makeGeoPhotoImage(feature, baseUrl);
    geoPhoto.shooting_point() =
        makeGeoPhotoShootingPoint(feature.geodeticPos(), feature.heading());

    geoPhoto.taken_at() = chrono::convertToUnixTime(feature.timestamp());

    geoPhoto.attribution() = makeGeoPhotoAttribution(feature);

    return geoPhoto;
}

sproto::mrcphoto::Photo::SpatialConnection makeMrcPhotoSpatialConnection(
    const GraphCoverageExplorer::Node& connection,
    sproto::mrcphoto::Photo::SpatialConnection::Type connectionType)
{
    sproto::mrcphoto::Photo::SpatialConnection spatialConnection;

    spatialConnection.photo_id() = std::to_string(connection.featureId);
    spatialConnection.shooting_point() = makeGeoPhotoShootingPoint(
        connection.ray.pos, connection.ray.direction.heading());
    spatialConnection.connection_type() = connectionType;

    return spatialConnection;
}

sproto::mrcphoto::Photo::HistoricalConnection makeMrcPhotoHistoricalConnection(
    const db::Feature& feature)
{
    sproto::mrcphoto::Photo::HistoricalConnection historicalConnection;

    historicalConnection.photo_id() = std::to_string(feature.id());
    historicalConnection.taken_at() =
        chrono::convertToUnixTime(feature.timestamp());

    return historicalConnection;
}

sproto::mrcphoto::Photo::Annotation makeMrcPhotoAnnotation(
    const std::string& layer,
    const GraphCoverageExplorer::OptNode& uturn,
    const GraphCoverageExplorer::Nodes& backward,
    const GraphCoverageExplorer::Nodes& forward,
    const db::Features& historicalFeatures)
{
    using namespace sproto::mrcphoto;

    Photo::Annotation annotation;

    annotation.layer() = layer;

    if (uturn) {
        annotation.spatial_connection().push_back(
            makeMrcPhotoSpatialConnection(
                *uturn, Photo::SpatialConnection::UTURN));
    }

    for (const auto& connection: backward) {
        annotation.spatial_connection().push_back(
            makeMrcPhotoSpatialConnection(
                connection, Photo::SpatialConnection::BACKWARD));
    }

    for (const auto& connection: forward) {
        annotation.spatial_connection().push_back(
            makeMrcPhotoSpatialConnection(
                connection, Photo::SpatialConnection::FORWARD));
    }

    for (const auto& feature: historicalFeatures) {
        annotation.historical_connection().push_back(
            makeMrcPhotoHistoricalConnection(feature));
    }

    return annotation;
}

using GetNextNodesF = std::function<GraphCoverageExplorer::Nodes(
    const GraphCoverageExplorer::Node&)>;

GraphCoverageExplorer::Nodes resolveToPublishedConnections(
    const IDataAccess& data,
    GraphCoverageExplorer::Node node,
    GetNextNodesF getNextNodes)
{
    using Key = std::tuple<road_graph::EdgeId, db::TId>;
    const auto makeKey = [](const auto& node) -> Key {
        return {node.edgeId, node.featureId};
    };

    GraphCoverageExplorer::Nodes result;

    std::set<Key> visited{};
    GraphCoverageExplorer::Nodes connections{node};

    while (!connections.empty()) {
        auto connection = std::move(connections.back());
        connections.pop_back();

        if (!visited.insert(makeKey(connection)).second) {
            continue;
        }

        if (data.isUnpublished(connection.featureId)) {
            connections =
                join(std::move(connections), getNextNodes(connection));
        } else {
            result.push_back(std::move(connection));
        }
    }

    return result;
}

GetNextNodesF makeConnectionsCrawler(
    const geolib3::Point2& fromPoint,
    Meters distanceLimit,
    GetNextNodesF getNextNodes)
{
    return [=](const GraphCoverageExplorer::Node& node) {
        auto nextNodes = getNextNodes(node);

        GraphCoverageExplorer::Nodes reachableNodes;

        for (auto& connection: nextNodes) {
            if (geolib3::fastGeoDistance(fromPoint, connection.ray.pos) <=
                distanceLimit.value()) {
                reachableNodes.push_back(std::move(connection));
            }
        }

        return reachableNodes;
    };
}

GraphCoverageExplorer::Nodes evalPublishedConnections(
    const IDataAccess& data,
    const GraphCoverageExplorer::Node& node,
    const GraphCoverageExplorer::Nodes& connections,
    GetNextNodesF getNextNodes)
{
    GraphCoverageExplorer::Nodes result;

    for (const auto& connection: connections) {
        const auto published = resolveToPublishedConnections(
            data,
            connection,
            makeConnectionsCrawler(
                node.ray.pos, MAX_CONNECTIVITY_DISTANCE, getNextNodes));

        result = join(std::move(result), std::move(published));
    }

    std::sort(
        result.begin(), result.end(), [](const auto& lhs, const auto& rhs) {
            return lhs.featureId < rhs.featureId;
        });

    const auto last = std::unique(
        result.begin(), result.end(), [](const auto& lhs, const auto& rhs) {
            return lhs.featureId == rhs.featureId;
        });

    result.erase(last, result.end());

    return result;
}

GraphCoverageExplorer::OptNode resolveToPublishedNode(
    const IDataAccess& data,
    const GraphCoverageExplorer& explorer,
    const geolib3::Point2& point,
    const GraphCoverageExplorer::Node& node)
{
    if (!data.isUnpublished(node.featureId)) {
        return node;
    }

    const auto backward = resolveToPublishedConnections(
        data,
        node,
        makeConnectionsCrawler(
            point, MAX_CONNECTIVITY_DISTANCE, [&](const auto& node) {
                return explorer.backward(node);
            }));

    const auto forward = resolveToPublishedConnections(
        data,
        node,
        makeConnectionsCrawler(
            point, MAX_CONNECTIVITY_DISTANCE, [&](const auto& node) {
                return explorer.forward(node);
            }));

    GraphCoverageExplorer::OptNode nearest;

    for (auto& connection: join(std::move(backward), std::move(forward))) {
        const auto distance = geolib3::fastGeoDistance(point, connection.ray.pos);

        if (!nearest ||
            distance < geolib3::fastGeoDistance(point, nearest->ray.pos)) {
            nearest = std::move(connection);
        }
    }

    return nearest;
}

// Historical and actual features are distinguished by comparing a node.featureId
// and feature.id(). If these are different then the feature is historical (a node
// cannot be historical by definition).
sproto::mrcphoto::Photo makeMrcPhoto(
    IDataAccess& data,
    const IGraph& graph,
    const GraphCoverageExplorer& explorer,
    const std::string& layer,
    const db::Feature& feature,
    const GraphCoverageExplorer::Node& node,
    const std::string& baseUrl)
{
    auto backward = explorer.backward(node);
    auto forward = explorer.forward(node);

    // Evaluate historial features without discrimination between published
    // and unpublished in order to make stable clusters of historical photos.
    const db::Features historical = evalHistoricalFeatures(
        data, graph, feature, node, join(backward, forward));

    auto uturn = explorer.uturn(node);

    if (uturn) {
        uturn = resolveToPublishedNode(data, explorer, node.ray.pos, *uturn);
    }

    backward = evalPublishedConnections(data, node, backward, [&](const auto& node) {
        return explorer.backward(node);
    });

    forward = evalPublishedConnections(data, node, forward, [&](const auto& node) {
        return explorer.forward(node);
    });

    sproto::mrcphoto::Photo photo;

    photo.id() = std::to_string(feature.id());
    photo.geo_photo() = makeGeoPhoto(feature, baseUrl);
    photo.annotation() = makeMrcPhotoAnnotation(
        layer, uturn, backward, forward, historical);
    photo.source_id() = feature.sourceId();

    return photo;
}

std::string bestResponseType(const std::string& contentType)
{
    static const std::vector<std::string> SUPPORTED_CONTENT_TYPES = {
        CONTENT_TYPE_PROTOBUF,
        CONTENT_TYPE_TEXT_PROTOBUF
    };

    return yacare::bestContentType(contentType, SUPPORTED_CONTENT_TYPES);
}

template<class SprotoType>
void fillPhotoApiResponse(
    yacare::Response& response,
    const std::string& responseType,
    const yacare::Request& request,
    const SprotoType& sprotoObj)
{
    common::handleYandexOrigin(request, response);
    response[CONTENT_TYPE_HEADER] = responseType;
    if (responseType == CONTENT_TYPE_PROTOBUF) {
        response << sprotoObj;
    } else if (responseType == CONTENT_TYPE_TEXT_PROTOBUF) {
        response << textDump(sprotoObj);
    } else {
        REQUIRE(false, "Unsupported response type " << responseType);
    }
}

template<class To>
To deserialize(const std::string& data)
{
    try {
        return boost::lexical_cast<To>(data);
    } catch (const std::exception& e) {
        ERROR() << "Unable to parse protobuf data: " << e.what();
        throw yacare::errors::BadRequest("Invalid data format");
    }
}

template<>
proto::mrcphoto::TrackChunkDescriptor
deserialize<proto::mrcphoto::TrackChunkDescriptor>(const std::string& data)
{
    proto::mrcphoto::TrackChunkDescriptor protoDescriptor;
    Y_PROTOBUF_SUPPRESS_NODISCARD protoDescriptor.ParseFromString(TString(data));
    return protoDescriptor;
}

template<class SprotoType>
auto parse(const std::string& data)
{
    return geolib3::sproto::decode(deserialize<SprotoType>(data));
}

geolib3::Polyline2 parseTrackFromPreviewRequest(const std::string& data)
{
    const auto previewRequest =
        deserialize<sproto::mrcphoto::PreviewRequest>(data);

    REQUIRE(
        previewRequest.polyline(),
        yacare::errors::BadRequest(
            "No track polyline in the preview request"));

    return geolib3::sproto::decode(*previewRequest.polyline());
}

struct CoveredEdgeSegmentPart {
    road_graph::SegmentPart segmentPart;
    std::vector<fb::CoveredSubPolyline> coveredSubpolylines;
};

// Collect covered subpolylines which beginning is within specified part of an
// edge segment.
std::optional<CoveredEdgeSegmentPart> makeCoveredEdgeSegmentPart(
    const IDataAccess& data,
    const IGraph& graph,
    const analyzer::shortest_path::LongSegmentPart& longSegmentPart,
    db::CameraDeviation cameraDeviation,
    db::FeaturePrivacy privacy)
{
    const auto segmentPart = analyzer::shortest_path::convertToShort(
                                 longSegmentPart, graph.persistentIndex())
                                 .get();
    const auto coveredEdge =
        graph.getCoveredEdgeById(segmentPart.segmentId.edgeId);
    if (!coveredEdge) {
        return std::nullopt;
    }

    const auto isWithinEdgeSegment = [&](const fb::PolylinePosition& pos) {
        return pos.segmentIdx() ==
                   segmentPart.segmentId.segmentIndex.value() &&
               segmentPart.start <= pos.segmentRelPosition() &&
               pos.segmentRelPosition() <= segmentPart.finish;
    };

    std::vector<fb::CoveredSubPolyline> coveredSubpolylines;
    for (const auto& coverage: coveredEdge->coverages) {
        if (coverage.cameraDeviation != cameraDeviation ||
            privacy < coverage.privacy) {
            continue;
        }

        for (const auto& coveredSubpolyline: coverage.coveredSubpolylines) {
            if (isWithinEdgeSegment(coveredSubpolyline.begin()) ||
                isWithinEdgeSegment(coveredSubpolyline.end())) {
                coveredSubpolylines.push_back(coveredSubpolyline);
            }
        }
    }

    data.removeUnpublishedFeatures(coveredSubpolylines);

    if (coveredSubpolylines.empty()) {
        return std::nullopt;
    }

    return CoveredEdgeSegmentPart{
        segmentPart, std::move(coveredSubpolylines)};
}

struct TrackPhoto {
    common::geometry::SubPolyline coveredSubpolyline;
    std::string photoId;
};
using TrackPhotos = std::vector<TrackPhoto>;

// Match graph edge segment to a given polyline segment and get all covering
// photos on it.
TrackPhotos makeTrackPhotosFromEdgeSegment(
    const IGraph& graph,
    const CoveredEdgeSegmentPart& coveredEdgeSegmentPart,
    const geolib3::Polyline2& polyline,
    std::size_t polylineSegmentIdx)
{
    using common::geometry::PolylinePosition;
    using common::geometry::SubPolyline;

    REQUIRE(
        polylineSegmentIdx < polyline.segmentsNumber(),
        "Segment index in of of track segments bounds");

    TrackPhotos photos;

    const auto polylineSegment = polyline.segmentAt(polylineSegmentIdx);

    const auto edgeSegmentPart = coveredEdgeSegmentPart.segmentPart;
    const auto edgeSegmentId = edgeSegmentPart.segmentId;
    const auto edgeId = edgeSegmentId.edgeId;
    const auto edgeGeometry = graph.graph().edgeData(edgeId).geometry();

    ASSERT(
        edgeSegmentId.segmentIndex.value() < edgeGeometry.segmentsNumber());
    const auto edgeSegment =
        edgeGeometry.segmentAt(edgeSegmentId.segmentIndex.value());

    const auto matchEdgeRelativePosition = [&](double edgeRelativePosition) {
        const geolib3::Point2 edgePosition =
            edgeSegment.pointByPosition(edgeRelativePosition);

        const geolib3::Point2 matchedPosition{
            geolib3::closestPoint(polylineSegment, edgePosition)};

        const double polylineRelativePosition =
            geolib3::geoSegmentPosition(polylineSegment, matchedPosition);

        return PolylinePosition{polylineSegmentIdx, polylineRelativePosition};
    };

    const auto matchToTackSubpolyline =
        [&](double start, double finish) -> std::optional<SubPolyline> {
        try {
            const SubPolyline coveredSubpolyline {
                matchEdgeRelativePosition(start),
                matchEdgeRelativePosition(finish)};
            // Sanity check for empty coverage (happens from time to time)
            if (coveredSubpolyline.begin() == coveredSubpolyline.end()) {
                return std::nullopt;
            }
            return coveredSubpolyline;

        } catch (const std::exception&) {
            // Handle theoretical bad matching
            return std::nullopt;
        }
    };

    const auto trackSubpolyline =
        matchToTackSubpolyline(edgeSegmentPart.start, edgeSegmentPart.finish);
    if (!trackSubpolyline) {
        return photos;
    }

    for (const auto& coveredSubpolyline:
         coveredEdgeSegmentPart.coveredSubpolylines) {
        const auto trackCoveredSubpolyline = matchToTackSubpolyline(
            coveredSubpolyline.begin().segmentRelPosition(),
            coveredSubpolyline.end().segmentRelPosition());

        if (!trackCoveredSubpolyline) {
            WARN() << "Cannot match photo " << coveredSubpolyline.featureId()
                   << " coverage to track polyline";
            continue;
        }

        if (trackCoveredSubpolyline->end() < trackSubpolyline->begin() ||
            trackSubpolyline->end() < trackCoveredSubpolyline->begin()) {
            // Skip photos which coverage is matched behind covered edge part
            // matched on track subpolyline.
            continue;
        }

        photos.push_back(
            {*trackCoveredSubpolyline,
             std::to_string(coveredSubpolyline.featureId())});
    }

    return photos;
}

// Coverage of the the same photo can match on multiple segments of a track
// polyline. So, coalesce adjacent coverages of the same photo into single
// continuous subpolyline.
// WARN: this function expects incoming photos are sorted by their starting
//       polyline position - coveredSubpolyline.begin() in ascending order.
TrackPhotos coalesceTrackPhotoCoverage(TrackPhotos photos)
{
    if (photos.empty()) {
        return photos;
    }

    auto mergeIt = photos.begin();
    for (auto photoIt = std::next(photos.begin());
         photoIt != photos.end();
         ++photoIt) {

        if (mergeIt->photoId == photoIt->photoId) {
            if (photoIt->coveredSubpolyline.begin() <
                mergeIt->coveredSubpolyline.begin()) {
                mergeIt->coveredSubpolyline.setBegin(
                    photoIt->coveredSubpolyline.begin());
            }

            if (mergeIt->coveredSubpolyline.end() <
                photoIt->coveredSubpolyline.end()) {
                mergeIt->coveredSubpolyline.setEnd(
                    photoIt->coveredSubpolyline.end());
            }
        } else if (++mergeIt != photoIt) {
            *mergeIt = *photoIt;
        }
    }
    photos.erase(std::next(mergeIt), photos.end());

    return photos;
}

// Collect all photos from graph coverage which match on a given track.
TrackPhotos collectTrackPhotos(
    const IDataAccess& data,
    const IGraph& graph,
    const geolib3::Polyline2& track,
    db::CameraDeviation cameraDeviation,
    db::FeaturePrivacy privacy,
    bool debug = false)
{
    using namespace ::maps::analyzer;

    // Minimal start/end segment part to accept it
    constexpr double MIN_SEGMENT_PART_TO_ACCEPT = 0.2;
    constexpr double MAX_RADIUS_METERS = 50.;
    graphmatching::MatchedGeometry matchedGeometry =
        graphmatching::matchGeometry(
            track,
            graph.graph(),
            graph.rtree(),
            graph.persistentIndex(),
            MIN_SEGMENT_PART_TO_ACCEPT,
            MAX_RADIUS_METERS,
            debug,
            true /* fillSegmentsMapping */);

    std::size_t trackSegmentIdx = 0;
    TrackPhotos photos;

    for (const auto& longPathOpt: matchedGeometry.segmentsMapping) {
        if (!longPathOpt) {
            ++trackSegmentIdx;
            continue;
        }

        // One track segment can contain multiple road graph edges and vice versa
        const shortest_path::LongSegmentPartIterator longSegmentPartEnd;
        for (auto longSegmentPartIt = shortest_path::iteratePath(
                 *longPathOpt, graph.graph(), graph.persistentIndex());
             longSegmentPartIt != longSegmentPartEnd;
             ++longSegmentPartIt) {
            const auto coveredEdgeSegmentPart = makeCoveredEdgeSegmentPart(
                data, graph, *longSegmentPartIt, cameraDeviation, privacy);

            if (!coveredEdgeSegmentPart) {
                continue;
            }

            auto edgePartPhotos = makeTrackPhotosFromEdgeSegment(
                graph, *coveredEdgeSegmentPart, track, trackSegmentIdx);

            photos.insert(
                photos.end(),
                edgePartPhotos.begin(),
                edgePartPhotos.end());
        }
        ++trackSegmentIdx;
    }

    return coalesceTrackPhotoCoverage(std::move(photos));
}

// Adjust photo position in a chunk to the original track. Photos within
// chunks must be sorted along the track.
std::vector<TrackPhotos> adjustChunksToGeometry(
    std::vector<TrackPhotos>&& chunks,
    const common::geometry::SubPolyline& chunkGeometry)
{
    // Adjust photos position to the original track from preview request
    const auto adjustPosition =
        [&](const common::geometry::PolylinePosition& pos) {
            return common::geometry::PolylinePosition{
                chunkGeometry.begin().segmentIdx() + pos.segmentIdx(),
                pos.segmentRelPosition()};
        };

    for (auto& chunk: chunks) {
        for (auto& photo: chunk) {
            photo.coveredSubpolyline = {
                adjustPosition(photo.coveredSubpolyline.begin()),
                adjustPosition(photo.coveredSubpolyline.end())};
        }

        // Remove photos outside chunk geometry boundaries
        auto end = std::remove_if(
            chunk.begin(), chunk.end(), [&](const auto& photo) {
                return photo.coveredSubpolyline.begin() <
                       chunkGeometry.begin();
            });
        end =
            std::remove_if(chunk.begin(), end, [&](const auto& photo) {
                return chunkGeometry.end() <=
                       photo.coveredSubpolyline.begin();
            });
        chunk.erase(end, chunk.end());
    }
    return chunks;
}

// Put spatially consecutive photos into the same chunk so they form a
// contiguous coverage on a given polyline. The photos to split must be ordered
// along the track.
std::vector<TrackPhotos> makeCoveredTrackChunks(
    const geolib3::Polyline2& track,
    const TrackPhotos& photos,
    Meters neglibleCoverageGap,
    const std::optional<common::geometry::SubPolyline>& adjustToGeomety)
{
    if (photos.empty()) {
        return {};
    }

    // Split photos into contigues chunks where distance between a previous
    // photo coverage ending and a point the next photo is taken at is less
    // then neglibleCoverageGap.
    std::optional<common::geometry::PolylinePosition> coverageEnding;
    const auto isGap = [&](const TrackPhoto& photo) {
        return !coverageEnding ||
               neglibleCoverageGap < fastGeoDistanceAlong(
                                         track,
                                         *coverageEnding,
                                         photo.coveredSubpolyline.begin());
    };

    std::vector<TrackPhotos> chunks;
    for (const auto& photo: photos) {
        if (isGap(photo)) {
            chunks.push_back({photo});
        } else {
            chunks.back().push_back(photo);
        }
        coverageEnding = photo.coveredSubpolyline.end();
    }

    if (adjustToGeomety) {
        return adjustChunksToGeometry(std::move(chunks), *adjustToGeomety);
    }

    return chunks;
}

// This function expects that chunks and photos within them are sorted along
// the track.
std::vector<common::geometry::SubPolyline> evaluateTrackChunksGeometry(
    const TrackPhotos& trackPhotos, std::size_t chunkPhotosLimit)
{
    std::vector<common::geometry::SubPolyline> result;

    if (trackPhotos.empty()) {
        return result;
    }

    if (chunkPhotosLimit == 0) {
        result.emplace_back(
            trackPhotos.front().coveredSubpolyline.begin(),
            trackPhotos.back().coveredSubpolyline.end());
        return result;
    }

    for (std::size_t beginIdx = 0; beginIdx < trackPhotos.size();
         beginIdx += chunkPhotosLimit) {
        const std::size_t lastIdx =
            beginIdx + chunkPhotosLimit - 1 < trackPhotos.size()
                ? beginIdx + chunkPhotosLimit - 1
                : trackPhotos.size() - 1;

        result.emplace_back(
            trackPhotos[beginIdx].coveredSubpolyline.begin(),
            trackPhotos[lastIdx].coveredSubpolyline.end());
    }

    return result;
}

// Make the given contigues chunks of track photos sparse.
std::vector<TrackPhotos> sparseTrackChunks(
    const geolib3::Polyline2& track,
    const std::vector<TrackPhotos>& chunks,
    Meters photoSparseDistance)
{
    ASSERT(Meters{0} < photoSparseDistance);

    // Sparce photos if necessary to reduce them
    std::vector<TrackPhotos> result;
    for (const auto& toSparse: chunks) {
        ASSERT(!toSparse.empty());

        Meters distanceFromStart{0.0};
        std::optional<TrackPhoto> prevPhoto;

        TrackPhotos sparsePhotos;
        for (const auto& photo: toSparse) {
            distanceFromStart +=
                prevPhoto ? fastGeoDistanceAlong(
                                track,
                                prevPhoto->coveredSubpolyline.begin(),
                                photo.coveredSubpolyline.begin())
                          : Meters{0};

            if (sparsePhotos.size() * photoSparseDistance <=
                distanceFromStart) {
                sparsePhotos.push_back(photo);
            }

            prevPhoto = photo;
        }

        // Always keep the last photo in the chunk to correctly mark its ending.
        if (sparsePhotos.size() == 1 && 1 < toSparse.size()) {
            sparsePhotos.push_back(toSparse.back());
        } else {
            sparsePhotos.back() = toSparse.back();
        }

        result.emplace_back(std::move(sparsePhotos));
    }

    return result;
}

common::geometry::PolylinePosition decode(const proto::common2::geometry::PolylinePosition& polylinePosition) {
    REQUIRE(
        polylinePosition.has_segment_index() &&
            polylinePosition.has_segment_position(),
        yacare::errors::BadRequest("Invalid data format"));

    return {
        polylinePosition.segment_index(),
        polylinePosition.segment_position()};
}

common::geometry::SubPolyline decode(
    const proto::common2::geometry::Subpolyline& subpolyline)
{
    REQUIRE(
        subpolyline.has_begin() && subpolyline.has_end(),
        yacare::errors::BadRequest("Invalid data format"));

    return { decode(subpolyline.begin()), decode(subpolyline.end()) };
}

proto::common2::geometry::PolylinePosition makeProtoPolylinePosition(
    const common::geometry::PolylinePosition& pos)
{
    proto::common2::geometry::PolylinePosition result;
    result.set_segment_index(pos.segmentIdx());
    result.set_segment_position(pos.segmentRelPosition());
    return result;
}

proto::common2::geometry::Subpolyline makeProtoSubpolyline(
    const common::geometry::SubPolyline& subpolyline)
{
    proto::common2::geometry::Subpolyline result;
    *result.mutable_begin() = makeProtoPolylinePosition(subpolyline.begin());
    *result.mutable_end() = makeProtoPolylinePosition(subpolyline.end());
    return result;
}

sproto::mrcphoto::TrackPreview::ChunkDescriptor makeChunkDescriptor(
    const std::string& payload)
{
    sproto::mrcphoto::TrackPreview::ChunkDescriptor result;
    result.payload() = payload;
    return result;
}

sproto::common2::geometry::PolylinePosition makeSprotoPolylinePosition(
    const common::geometry::PolylinePosition& pos)
{
    sproto::common2::geometry::PolylinePosition result;
    result.segment_index() = pos.segmentIdx();
    result.segment_position() = pos.segmentRelPosition();
    return result;
}

sproto::common2::geometry::Subpolyline makeSprotoSubpolyline(
    const common::geometry::SubPolyline& subpolyline)
{
    sproto::common2::geometry::Subpolyline result;
    result.begin() = makeSprotoPolylinePosition(subpolyline.begin());
    result.end() = makeSprotoPolylinePosition(subpolyline.end());
    return result;
}

using ChunkDescriptorPayloadF =
    std::function<std::string(const common::geometry::SubPolyline&)>;

sproto::mrcphoto::TrackPreview::ChunkList::Chunk makeSprotoChunk(
    const common::geometry::SubPolyline& chunkGeometry,
    ChunkDescriptorPayloadF makeChunkDescriptorPayload)
{
    sproto::mrcphoto::TrackPreview::ChunkList::Chunk result;

    result.chunk_descriptor() =
        makeChunkDescriptor(makeChunkDescriptorPayload(chunkGeometry));
    result.subpolyline() = makeSprotoSubpolyline(chunkGeometry);

    return result;
}

sproto::mrcphoto::TrackPreview::ChunkList makeSprotoChunkList(
    const TrackPhotos& trackPhotos,
    ChunkDescriptorPayloadF makeChunkDescriptorPayload,
    std::size_t chunkPhotosLimit)
{
    const auto trackChunksGeometry =
        evaluateTrackChunksGeometry(trackPhotos, chunkPhotosLimit);

    sproto::mrcphoto::TrackPreview::ChunkList result;
    for (const auto& chunkGeometry: trackChunksGeometry) {
        result.chunks().push_back(
            makeSprotoChunk(chunkGeometry, makeChunkDescriptorPayload));
    }

    return result;
}

sproto::mrcphoto::PhotoStream::Item makeSprotoPhotoSteamItem(
    const common::geometry::PolylinePosition& position,
    const std::optional<std::string>& photoId)
{
    sproto::mrcphoto::PhotoStream::Item result;
    result.position() = makeSprotoPolylinePosition(position);
    if (photoId) {
        result.photo_id() = *photoId;
    }
    return result;
}

sproto::mrcphoto::PhotoStream makeSprotoPhotoStream(
    const geolib3::Polyline2& track,
    const TrackPhotos& trackPhotos,
    const std::optional<Meters>& sparseDistance,
    const std::optional<common::geometry::SubPolyline>& adjustToGeomety)
{
    const auto coveredChunks = makeCoveredTrackChunks(
        track, trackPhotos, NEGLIGIBLE_COVERAGE_GAP, adjustToGeomety);

    sproto::mrcphoto::PhotoStream result;
    for (const auto& chunk:
         sparseDistance ? sparseTrackChunks(track, coveredChunks, *sparseDistance)
                        : coveredChunks) {
        ASSERT(!chunk.empty());

        for (const auto& photo: chunk) {
            result.items().push_back(makeSprotoPhotoSteamItem(
                photo.coveredSubpolyline.begin(), photo.photoId));
        }
        // Mark the last item in contiguous coverage with absense of optional
        // photo id so the client can correctly render the coverage on the track.
        result.items().push_back(makeSprotoPhotoSteamItem(
            chunk.back().coveredSubpolyline.end(), std::nullopt));
    }

    return result;
}

geolib3::Polyline2 trimPolyline(
    const geolib3::Polyline2& polyline,
    const common::geometry::SubPolyline& subpolyline)
{
    ASSERT(subpolyline.end().segmentIdx() < polyline.segmentsNumber());

    geolib3::PointsVector points;
    for (std::size_t pointIdx = subpolyline.begin().segmentIdx();
         pointIdx <= subpolyline.end().segmentIdx() + 1;
         ++pointIdx) {
        points.push_back(polyline.pointAt(pointIdx));
    }

    return geolib3::Polyline2{points};
}

sproto::mrcphoto::TrackPreview makeSprotoTrackPreview(
    db::GraphType graphType,
    const geolib3::Polyline2& track,
    const TrackPhotos& trackPhotos,
    const std::string& responseType,
    std::size_t chunkPhotosLimit)
{
    const ChunkDescriptorPayloadF makeChunkDescriptorPayload =
        [&](const common::geometry::SubPolyline& chunkGeometry)
        -> std::string {
        // FIXME: currently local protobuf defintions aren't compiliable
        //        by sprotoc so protoc is used instead. Move to sprotoc once
        //        ya make is able to do its job.
        proto::mrcphoto::TrackChunkDescriptor protoDescriptor;

        protoDescriptor.set_graph_type(ToString(graphType));
        // IMPROVEMENT:
        //     Make a polyline which begining and ending are beyond the
        //     chunk boundaries to collect photos what make maneuvers on
        //     crossroads clearer to see on chunk junctions.

        *protoDescriptor.mutable_polyline() =
            geolib3::proto::encode(trimPolyline(track, chunkGeometry));

        *protoDescriptor.mutable_chunk() =
            makeProtoSubpolyline(chunkGeometry);

        if (responseType == CONTENT_TYPE_TEXT_PROTOBUF) {
            return protoDescriptor.DebugString();
        } else {
            TString opaqueDescriptor;
            Y_PROTOBUF_SUPPRESS_NODISCARD protoDescriptor.SerializeToString(&opaqueDescriptor);
            return opaqueDescriptor;
        }
    };

    sproto::mrcphoto::TrackPreview result;

    result.preview() = makeSprotoPhotoStream(
        track,
        trackPhotos,
        TRACK_PREVIEW_SPARSE_DISTANCE,
        std::nullopt /* adjustToGeomety */);

    result.chunk_list() = makeSprotoChunkList(
        trackPhotos, makeChunkDescriptorPayload, chunkPhotosLimit);

    return result;
}

std::tuple<db::GraphType, geolib3::Polyline2, common::geometry::SubPolyline>
parseTrackChunkRequest(const std::string& data)
{
    const auto chunkDescriptor =
        deserialize<sproto::mrcphoto::TrackPreview::ChunkDescriptor>(data);

    REQUIRE(
        chunkDescriptor.payload(),
        yacare::errors::BadRequest("Invalid descriptor data"));

    const auto protoDescriptor =
        deserialize<proto::mrcphoto::TrackChunkDescriptor>(
            *chunkDescriptor.payload());

    REQUIRE(
        protoDescriptor.has_graph_type() && protoDescriptor.has_polyline() &&
            protoDescriptor.has_chunk(),
        yacare::errors::BadRequest("Invalid descriptor data"));

    const auto graphType = parseGraphType(protoDescriptor.graph_type());

    const geolib3::Polyline2 polyline =
        geolib3::proto::decode(protoDescriptor.polyline());

    const common::geometry::SubPolyline chunk =
        decode(protoDescriptor.chunk());

    return {graphType, std::move(polyline), chunk};
}

sproto::mrcphoto::TrackChunk makeSprotoTrackChunk(
    const geolib3::Polyline2& track,
    const TrackPhotos& trackPhotos,
    const common::geometry::SubPolyline& chunkGeometry)
{
    sproto::mrcphoto::TrackChunk result;

    result.photos() = makeSprotoPhotoStream(
        track, trackPhotos, std::nullopt /* sparseDistance */, chunkGeometry);

    return result;
}

GraphCoverageExplorer::Node findNode(const GraphCoverageExplorer& explorer,
                                     const db::Feature& feature)
{
    auto result = explorer.find(feature);
    if (!result) {
        // This feature cannot be found in the graph coverage so it is assumed
        // to be a historical one.
        auto ray = db::Ray{feature.geodeticPos(), direction(feature)};
        result = explorer.find(ray);
        ASSERT(result);
    }
    return *result;
}

} // namespace

YCR_RESPOND_TO("GET /v2/photo/hotspot", ll, l, z, contentType)
{
    Configuration::logDatasetMetrics();

    const auto layer = getMrcPhotoLayer(l);

    const auto graphPtr = Configuration::instance()->graphByTileLayer(layer);
    const GraphCoverageExplorer explorer{*graphPtr, photoApiCoverageSelector};

    const auto dataPtr = Configuration::instance()->dataAccess();

    auto node = explorer.find(ll, maxDrawableFcByZoom(z, layer));

    if (node) {
        node = resolveToPublishedNode(*dataPtr, explorer, ll, *node);

        auto feature = dataPtr->tryGetFeatureById(node->featureId);
        ASSERT(feature);

        feature->setGeodeticPos(node->ray.pos);
        feature->setHeading(node->ray.direction.heading());

        const auto photo = makeMrcPhoto(
            *dataPtr,
            *graphPtr,
            explorer,
            layer,
            *feature,
            *node,
            baseUrl(request));

        fillPhotoApiResponse(
            response, bestResponseType(contentType), request, photo);

    } else {
        response.setStatus(yacare::HTTPStatus::NoContent);
    }
}

YCR_RESPOND_TO("OPTIONS /v2/photo/hotspot")
{
    if (common::handleYandexOrigin(request, response)) {
        response.setHeader(ALLOW_METHODS_HEADER, METHODS_GET_OPTIONS);
    }
}

YCR_RESPOND_TO("GET /v2/photo/get", l, id, contentType, maybeUserId)
{
    Configuration::logDatasetMetrics();

    const auto layer = getMrcPhotoLayer(l);

    const auto dataPtr = Configuration::instance()->dataAccess();
    REQUIRE(!dataPtr->isUnpublished(id),
            yacare::errors::UnprocessableEntity("Photo is unpublished"));

    auto feature = dataPtr->tryGetFeatureById(id);
    REQUIRE(feature, yacare::errors::NotFound("No photo with such ID"));

    const auto graphPtr = Configuration::instance()->graphByTileLayer(layer);
    const GraphCoverageExplorer explorer{*graphPtr, photoApiCoverageSelector};

    auto node = findNode(explorer, *feature);
    feature->setGeodeticPos(node.ray.pos);
    feature->setHeading(node.ray.direction.heading());

    const auto photo = makeMrcPhoto(
        *dataPtr,
        *graphPtr,
        explorer,
        layer,
        *feature,
        node,
        baseUrl(request));

    fillPhotoApiResponse(response, bestResponseType(contentType), request, photo);

    logUgcPhotoViewEvent(maybeUserId, {feature->id()}, request);
}

YCR_RESPOND_TO("OPTIONS /v2/photo/get")
{
    if (common::handleYandexOrigin(request, response)) {
        response.setHeader(ALLOW_METHODS_HEADER, METHODS_GET_OPTIONS);
    }
}

YCR_RESPOND_TO("GET /v2/photos/get", l, ids, contentType, maybeUserId)
{
    Configuration::logDatasetMetrics();
    auto layer = getMrcPhotoLayer(l);
    auto dataPtr = Configuration::instance()->dataAccess();
    auto graphPtr = Configuration::instance()->graphByTileLayer(layer);
    auto explorer = GraphCoverageExplorer{*graphPtr, photoApiCoverageSelector};
    auto url = baseUrl(request);
    auto photoList = sproto::mrcphoto::PhotoList{};
    for (auto id : ids) {
        REQUIRE(!dataPtr->isUnpublished(id),
                yacare::errors::UnprocessableEntity{} << "unpublished photo "
                                                      << id);
        auto feature = dataPtr->tryGetFeatureById(id);
        REQUIRE(feature, yacare::errors::NotFound{} << "no photo " << id);
        auto node = findNode(explorer, *feature);
        feature->setGeodeticPos(node.ray.pos);
        feature->setHeading(node.ray.direction.heading());
        photoList.photos().push_back(makeMrcPhoto(
            *dataPtr, *graphPtr, explorer, layer, *feature, node, url));
    }
    fillPhotoApiResponse(
        response, bestResponseType(contentType), request, photoList);
    logUgcPhotoViewEvent(maybeUserId, ids, request);
}

YCR_RESPOND_TO("OPTIONS /v2/photos/get")
{
    if (common::handleYandexOrigin(request, response)) {
        response.setHeader(ALLOW_METHODS_HEADER, METHODS_GET_OPTIONS);
    }
}

YCR_RESPOND_TO("GET /v2/photo/strip", id,
                                      contentType,
                                      maybeUserId)
{
    Configuration::logDatasetMetrics();

    const auto dataPtr = Configuration::instance()->dataAccess();
    REQUIRE(!dataPtr->isUnpublished(id),
            yacare::errors::UnprocessableEntity("Photo is unpublished"));

    auto feature = dataPtr->tryGetFeatureById(id);
    REQUIRE(feature, yacare::errors::NotFound("No photo with such ID"));

    sproto::mrcphoto::Photo photo;
    photo.id() = std::to_string(feature->id());
    photo.geo_photo() = makeGeoPhoto(*feature, baseUrl(request));

    fillPhotoApiResponse(response, bestResponseType(contentType), request, photo);

    logUgcPhotoViewEvent(maybeUserId, {feature->id()}, request);
}

YCR_RESPOND_TO("POST /v2/track/preview", contentType,
                                         graphType = db::GraphType::Road,
                                         limit = PHOTOS_PER_TRACK_CHUNK_LIMIT,
                                         debug = false)
{
    Configuration::logDatasetMetrics();

    const auto responseType = bestResponseType(contentType);

    const auto dataPtr = Configuration::instance()->dataAccess();
    const auto graphPtr = Configuration::instance()->graphByType(graphType);

    const auto track = parseTrackFromPreviewRequest(request.body());

    auto trackPhotos = collectTrackPhotos(
        *dataPtr,
        *graphPtr,
        track,
        PHOTO_API_CAMERA_DEVIATION,
        PHOTO_API_FEATURE_PRIVACY,
        debug);

    if (trackPhotos.empty()) {
        response.setStatus(yacare::HTTPStatus::NoContent);

    } else {
        const auto protoTrackPreview = makeSprotoTrackPreview(
            graphType, track, trackPhotos, responseType, limit);

        fillPhotoApiResponse(
            response, responseType, request, protoTrackPreview);
    }
}

YCR_RESPOND_TO("POST /v2/track/chunk", contentType, debug = false)
{
    Configuration::logDatasetMetrics();

    const auto responseType = bestResponseType(contentType);

    const auto [graphType, track, chunkSubpolyline] =
        parseTrackChunkRequest(request.body());

    const auto dataPtr = Configuration::instance()->dataAccess();
    const auto graphPtr = Configuration::instance()->graphByType(graphType);

    auto trackPhotos = collectTrackPhotos(
        *dataPtr,
        *graphPtr,
        track,
        PHOTO_API_CAMERA_DEVIATION,
        PHOTO_API_FEATURE_PRIVACY,
        debug);

    if (trackPhotos.empty()) {
        response.setStatus(yacare::HTTPStatus::NoContent);

    } else {
        const auto sprotoTrackChunk =
            makeSprotoTrackChunk(track, trackPhotos, chunkSubpolyline);

        fillPhotoApiResponse(
            response, responseType, request, sprotoTrackChunk);
    }
}

} // namespace maps::mrc::browser
