#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/tools/easyview/lib/io/include/format/yson.h>
#include <maps/tools/easyview/lib/io/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_recording_report_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/db/include/visibility.h>
#include <maps/wikimap/mapspro/services/mrc/tasks-planner/lib/serialization.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/pb_stream2/reader.h>
#include <yandex/maps/proto/offline_recording/log_event.pb.h>
#include <yandex/maps/proto/offline_recording/mapkit2/location.pb.h>
#include <yandex/maps/proto/offline_recording/record.pb.h>

#include <library/cpp/yson/node/node_io.h>

/// use boost::geometry for the intersection algorithm
#include <boost/geometry.hpp>
#include <boost/geometry/algorithms/intersection.hpp>
#include <boost/geometry/algorithms/length.hpp>
#include <boost/geometry/geometries/linestring.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/geometries/polygon.hpp>

#include <chrono>
#include <filesystem>
#include <list>
#include <memory>
#include <ostream>
#include <set>
#include <sstream>
#include <variant>
#include <vector>

namespace mrc = maps::mrc;
namespace precording = yandex::maps::proto::offline::recording;
namespace plog_event = precording::log_event;
namespace precord = precording::record;
namespace pmapkit2 = precording::mapkit2;
namespace ev = maps::tools::easyview;

namespace {

using TimePoint = maps::chrono::TimePoint;
using BPoint = boost::geometry::model::d2::point_xy<double>;
using BLinestring = boost::geometry::model::linestring<BPoint>;
using BPolygon = boost::geometry::model::polygon<BPoint>;
using BRing = BPolygon::ring_type;

enum class CaptureState { UNKNOWN, OFF, ON };

enum class StorageState { UNKNOWN, MEMORY, BUFFERED, STORED };

enum class LocationState {
    UNKNOWN,
    REQUESTED,
    REQUEST_COMPLETED,
    REQUEST_FAILED,
    LOCATED
};

struct DeviceLocationPoint {
    maps::geolib3::Point2 position; // lon, lat
    TimePoint::duration relativeTs;
    TimePoint absoluteTs;

    // Device states
    StorageState storageState;
    CaptureState captureState;
};

struct DeviceImage {
    // Absolute time stamp is nullopt if there the server side feature is absent
    // TODO: compute the device position using quadratic Bezier Curve as on device
    // Device position is nullopt if it this image position wasn't evalated
    std::optional<maps::geolib3::Point2> devicePosition{
        std::nullopt}; // lon, lat
    TimePoint::duration relativeTs;

    // Device states
    LocationState locationState{LocationState::UNKNOWN};
    StorageState storageState{StorageState::UNKNOWN};
    CaptureState captureState{CaptureState::UNKNOWN};
};

using DeviceObject = std::variant<DeviceLocationPoint, DeviceImage>;
using DeviceObjects = std::list<DeviceObject>;

const std::string EXECUTION_SESSION_COMPONENT = "execution_session";
const std::string LOCATION_EVALUATOR_COMPONENT;
const std::string NEW_IMAGE_EVENT = "new image";
const std::string GOT_LOCATED_IMAGE_EVENT = "got located photo";
const std::string BUFFER_IMAGE_EVENT = "put image to buffer for storing";
const std::string SAVE_TO_STORAGE_PREFIX = "saved ";
const std::string SAVE_IMAGES_TO_STORAGE_POSTFIX = " images to storage";
const std::string SAVE_LOCATIONS_TO_STORAGE_POSTFIX =
    " location points to storage";
const std::string CAPTURE_OFF_EVENT = "capture off";
const std::string CAPTURE_PREFIX = "capture";
const std::string REQUEST_IMAGE_LOCATION_EVENT = "evaluated image location";
const std::string IMAGE_LOCATION_EVALUATION_FAILED_EVENT =
    "could not evaluate image location";
const std::string IMAGE_RELATIVE_TS = "image_relts";

constexpr TimePoint::duration TRACK_POINTS_MARGIN = std::chrono::minutes{1};
constexpr TimePoint::duration ACCEPTIBLE_IMAGES_MATCH_TIME_DIFF =
    std::chrono::seconds{1};

const ev::Color EV_RED_COLOR = ev::Color::rgba(0.75, 0, 0, 1);
const ev::Color EV_GREEN_COLOR = ev::Color::rgba(0, 0.75, 0, 1);
const ev::Color EV_BLUE_COLOR = ev::Color::rgba(0, 0, 0.75, 1);
const ev::Color EV_YELLOW_COLOR = ev::Color::rgba(0.75, 0.75, 0, 1);
const ev::Color EV_GREY_COLOR = ev::Color::rgba(0.5, 0.5, 0.5, 1);

constexpr double UNCOVERED_TARGETS_WIDTH = 10.0;
constexpr double COVERED_TARGETS_WIDTH = 10.0;
constexpr double MUSTBE_TARGETS_WIDTH = 10.0;
constexpr double COULDBE_TARGETS_WIDTH = 10.0;
constexpr double TRACK_POINT_RADIUS = 3.0;
constexpr double IMAGE_RADIUS = 4.0;

const ev::LineStyle UNCOVERED_TARGETS_STYLE{EV_BLUE_COLOR,
                                            UNCOVERED_TARGETS_WIDTH};
const ev::LineStyle COVERED_TARGETS_STYLE{EV_GREEN_COLOR,
                                          COVERED_TARGETS_WIDTH};
const ev::LineStyle COULDBE_COVERED_TARGETS_STYLE{EV_YELLOW_COLOR,
                                                  COULDBE_TARGETS_WIDTH};
const ev::LineStyle MUSTBE_COVERED_TARGETS_STYLE{EV_RED_COLOR,
                                                 MUSTBE_TARGETS_WIDTH};

// const ev::LineStyle DEVICE_TRACK_STYLE{EV_GREY_COLOR, TRACK_LINE_WIDTH};
const ev::PointStyle TRACK_POINT_STYLE{
    EV_GREY_COLOR, EV_BLUE_COLOR, TRACK_POINT_RADIUS};
const ev::PointStyle SAVED_IMAGE_POINT_STYLE{
    EV_GREY_COLOR, EV_GREEN_COLOR, IMAGE_RADIUS};
const ev::PointStyle UNSAVED_IMAGE_POINT_STYLE{
    EV_GREY_COLOR, EV_RED_COLOR, IMAGE_RADIUS};

TimePoint::duration abs(TimePoint::duration dur)
{
    return dur < TimePoint::duration{0} ? -dur : dur;
}

template<typename T, typename A>
void extend(std::vector<T, A>& dst, std::vector<T, A>&& src)
{
    dst.insert(dst.end(), src.begin(), src.end());
}

bool startsWith(const std::string& str, const std::string& prefix)
{
    return str.substr(0, prefix.size()) == prefix;
}

bool endsWith(const std::string& str, const std::string& postfix)
{
    return str.substr(str.size() - postfix.size(), postfix.size()) == postfix;
}

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

BLinestring toBoost(const maps::geolib3::Segment2& segment)
{
    auto list = {toBoost(segment.start()), toBoost(segment.end())};
    return {list.begin(), list.end()};
}

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

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

maps::geolib3::Point2 fromBoost(const BPoint& point)
{
    return maps::geolib3::Point2(point.x(), point.y());
}

maps::geolib3::Segment2 fromBoost(const BLinestring& line)
{
    return maps::geolib3::Segment2(
        fromBoost(line.front()), fromBoost(line.back()));
}

template<typename ValueType>
std::optional<ValueType> getParamValueAs(
    const plog_event::EventRecord& eventRecord, const std::string& name)
{
    for (int i = 0; i < eventRecord.params_size(); ++i) {
        const auto& param = eventRecord.params(i);
        if (std::string(param.event()) == name) {
            return boost::lexical_cast<ValueType>(param.value());
        }
    }
    return std::nullopt;
}

template<typename ValueType>
ValueType getCheckedParamValueAs(
    const plog_event::EventRecord& eventRecord, const std::string& name)
{
    const auto value = getParamValueAs<ValueType>(eventRecord, name);
    REQUIRE(value, "Failed to find parameter " << name);
    return *value;
}

template<typename Duration>
Duration getCheckedParamValueAsDuration(
    const plog_event::EventRecord& eventRecord, const std::string& name)
{
    const auto ts = getCheckedParamValueAs<std::uint64_t>(eventRecord, name);
    return Duration{ts};
}

auto getRecordTimestamp(const precord::Record& record)
{
    const std::chrono::seconds secondsFromEpoch{record.timestamp()};
    return TimePoint{secondsFromEpoch};
}

std::optional<std::size_t> readSavedImagesNumber(const std::string& event)
{
    if (startsWith(event, SAVE_TO_STORAGE_PREFIX) &&
        endsWith(event, SAVE_IMAGES_TO_STORAGE_POSTFIX)) {
        return boost::lexical_cast<std::size_t>(event.substr(
            SAVE_TO_STORAGE_PREFIX.size(),
            event.size() - SAVE_TO_STORAGE_PREFIX.size() -
                SAVE_IMAGES_TO_STORAGE_POSTFIX.size()));
    }
    return std::nullopt;
}

std::optional<std::size_t> readSavedLocationsNumber(const std::string& event)
{
    if (startsWith(event, SAVE_TO_STORAGE_PREFIX) &&
        endsWith(event, SAVE_LOCATIONS_TO_STORAGE_POSTFIX)) {
        return boost::lexical_cast<std::size_t>(event.substr(
            SAVE_TO_STORAGE_PREFIX.size(),
            event.size() - SAVE_TO_STORAGE_PREFIX.size() -
                SAVE_LOCATIONS_TO_STORAGE_POSTFIX.size()));
    }
    return std::nullopt;
}

auto getFirstRecordTimestamp(std::istream& stream)
{
    maps::pb_stream2::Reader reader(&stream);
    auto it = reader.begin();
    ASSERT(it != reader.end());
    return getRecordTimestamp(it->as<precord::Record>());
}

// Note: all timestamps are converted to global UTC time
std::tuple<mrc::db::Feature, TimePoint> getStartingCriteria(
    maps::pgpool3::TransactionHandle& txn,
    const maps::cmdline::Option<std::string>& sourceIdOpt,
    const maps::cmdline::Option<std::string>& fromFeatureIdOpt,
    const maps::cmdline::Option<std::string>& fromTimestampOpt)
{
    std::optional<mrc::db::Feature> feature;
    if (fromFeatureIdOpt.defined()) {
        feature = mrc::db::FeatureGateway(*txn).loadById(
            boost::lexical_cast<mrc::db::TId>(fromFeatureIdOpt));
    } else if (fromTimestampOpt.defined() && sourceIdOpt.defined()) {
        feature = mrc::db::FeatureGateway(*txn).loadOne(
            mrc::db::table::Feature::sourceId == sourceIdOpt &&
                mrc::db::table::Feature::date >=
                    maps::chrono::parseSqlDateTime(fromTimestampOpt),
            maps::sql_chemistry::orderBy(
                mrc::db::table::Feature::date)
                .asc()
                .limit(1));
    }
    REQUIRE(
        feature,
        "Either --from-feature or pair --from-timestamp and --source-id must "
        "be set");

    if (fromTimestampOpt.defined()) {
        return {*feature, maps::chrono::parseSqlDateTime(fromTimestampOpt)};
    }

    return {*feature, feature->timestamp()};
}

std::tuple<mrc::db::Feature, TimePoint> getFinishingCriteria(
    maps::pgpool3::TransactionHandle& txn,
    const mrc::db::Feature& fromFeature,
    const maps::cmdline::Option<std::string>& toFeatureIdOpt,
    const maps::cmdline::Option<std::string>& toTimestampOpt)
{
    std::optional<mrc::db::Feature> feature;
    if (toFeatureIdOpt.defined()) {
        feature = mrc::db::FeatureGateway(*txn).loadById(
            boost::lexical_cast<mrc::db::TId>(toFeatureIdOpt));
    } else if (toTimestampOpt.defined()) {
        REQUIRE(fromFeature.assignmentId().has_value(), "empty assignmentId");
        feature = mrc::db::FeatureGateway(*txn).loadOne(
            mrc::db::table::Feature::sourceId ==
                    fromFeature.sourceId() &&
                mrc::db::table::Feature::assignmentId ==
                    fromFeature.assignmentId().value() &&
                mrc::db::table::Feature::date <=
                    maps::chrono::parseSqlDateTime(toTimestampOpt),
            maps::sql_chemistry::orderBy(
                mrc::db::table::Feature::date)
                .desc()
                .limit(1));
    } else {
        REQUIRE(fromFeature.assignmentId().has_value(), "empty assignmentId");
        feature = mrc::db::FeatureGateway(*txn).loadOne(
            mrc::db::table::Feature::sourceId ==
                    fromFeature.sourceId() &&
                mrc::db::table::Feature::assignmentId ==
                    fromFeature.assignmentId().value(),
            maps::sql_chemistry::orderBy(
                mrc::db::table::Feature::date)
                .desc()
                .limit(1));
    }

    REQUIRE(
        fromFeature.assignmentId() == feature->assignmentId(),
        "Erroneous --to-feature-id, its assignment ID doesn't match");

    if (toTimestampOpt.defined()) {
        return {*feature, maps::chrono::parseSqlDateTime(toTimestampOpt)};
    }

    return {*feature, feature->timestamp()};
}

mrc::db::TrackPoints loadTrackPoints(
    maps::pgpool3::TransactionHandle& txn,
    const std::string& sourceId,
    TimePoint fromTimestamp,
    TimePoint toTimestamp,
    TimePoint::duration margin)
{
    return mrc::db::TrackPointGateway{*txn}.load(
        mrc::db::table::TrackPoint::timestamp.between(
            fromTimestamp - margin, toTimestamp + margin) &&
            mrc::db::table::TrackPoint::sourceId == sourceId,
        maps::sql_chemistry::orderBy(
            mrc::db::table::TrackPoint::timestamp)
            .asc());
}

mrc::db::Features loadFeatures(
    maps::pgpool3::TransactionHandle& txn,
    const std::string& sourceId,
    const mrc::db::TId fromFeatureId,
    const mrc::db::TId toFeatureId)
{
    return mrc::db::FeatureGateway{*txn}.load(
        mrc::db::table::Feature::id.between(
            fromFeatureId, toFeatureId) &&
            mrc::db::table::Feature::sourceId == sourceId,
        maps::sql_chemistry::orderBy(mrc::db::table::Feature::date)
            .asc());
}

mrc::db::ugc::Targets loadTargets(
    maps::pgpool3::TransactionHandle& txn, mrc::db::TId assignmentId)
{
    const auto assignment = mrc::db::ugc::AssignmentGateway{*txn}.loadOne(
        mrc::db::ugc::table::Assignment::id == assignmentId);
    return mrc::db::ugc::TargetGateway{*txn}.load(
        mrc::db::ugc::table::Target::taskId == assignment.taskId());
}

std::tuple<
    mrc::db::TrackPoints,
    mrc::db::Features,
    mrc::db::ugc::Targets,
    mrc::db::CameraDeviation>
loadMrcDBData(
    maps::pgpool3::TransactionHandle& txn,
    const std::string& sourceId,
    mrc::db::TId fromFeatureId,
    mrc::db::TId toFeatureId,
    TimePoint fromTimestamp,
    TimePoint toTimestamp)
{
    const auto trackPoints = loadTrackPoints(
        txn, sourceId, fromTimestamp, toTimestamp, TRACK_POINTS_MARGIN);

    const auto features =
        loadFeatures(txn, sourceId, fromFeatureId, toFeatureId);

    if (features.empty()) {
        WARN() << "No features found in DB for the given criteria.";
        return {};
    }

    REQUIRE(features.front().assignmentId().has_value(), "empty assignmentId");
    const auto targets = loadTargets(txn, features.front().assignmentId().value());

    mrc::db::CameraDeviation cameraDeviation{mrc::db::CameraDeviation::Front};
    if (!features.empty()) {
        // It is assumed the camera deviation is always the same for a a pair
        // (source_id, assignment_id). Usually it is os but beware it might
        // not be true.
        cameraDeviation = features.front().cameraDeviation();
    }

    return {trackPoints, features, targets, cameraDeviation};
}

std::set<maps::mds::Key> getMdsKeys(
    maps::pgpool3::TransactionHandle& txn,
    const std::string& sourceId,
    mrc::db::TId assignmentId)
{
    auto recordings =
        mrc::db::ugc::AssignmentRecordingReportGateway(*txn).load(
            mrc::db::ugc::table::AssignmentRecordingReport::assignmentId
                .equals(assignmentId) &&
            mrc::db::ugc::table::AssignmentRecordingReport::sourceId.equals(
                sourceId));

    std::set<maps::mds::Key> mdsKeys;
    for (const auto& recording: recordings) {
        mdsKeys.insert(recording.mdsKey());
    }

    INFO() << "There are " << mdsKeys.size() << " files to download";
    return mdsKeys;
}

std::stringstream downloadRecordings(
    maps::mds::Mds& mdsClient,
    const std::set<maps::mds::Key>& mdsKeys,
    TimePoint toTimestamp)
{
    // Note that a record timestamp is global UTC in seconds
    using TimestampStreamPair = std::pair<TimePoint, std::stringstream>;
    std::vector<TimestampStreamPair> reportsStr;
    reportsStr.reserve(mdsKeys.size());

    std::set<maps::chrono::TimePoint> uniqueRecordings;
    std::size_t oobCount = 0;
    std::size_t dupCount = 0;
    for (const auto& mdsKey: mdsKeys) {
        std::stringstream sstr;
        mdsClient.get(mdsKey, sstr);
        maps::chrono::TimePoint recTimestamp = getFirstRecordTimestamp(sstr);
        if (toTimestamp + std::chrono::minutes{1} < recTimestamp) {
            // Skip recordings that are out of time bounds
            ++oobCount;
            continue;
        }
        if (!uniqueRecordings.insert(recTimestamp).second) {
            // Skip recordings duplicated recordings
            ++dupCount;
            continue;
        }
        reportsStr.emplace_back(recTimestamp, std::move(sstr));
    }
    INFO() << "Downloaded " << mdsKeys.size() << " report parts";
    INFO() << "Skipped OOB recordings " << oobCount;
    INFO() << "Skipped duplicated recordings " << dupCount;

    std::sort(
        std::begin(reportsStr),
        std::end(reportsStr),
        [](const TimestampStreamPair& lhs, const TimestampStreamPair& rhs) {
            return lhs.first < rhs.first;
        });

    std::stringstream result(
        std::ios_base::in | std::ios_base::out | std::ios_base::binary);
    for (auto& tsRecPair: reportsStr) {
        result << tsRecPair.second.str();
    }
    return result;
}

template<typename T>
void applyToLastNObjects(
    DeviceObjects& objects, std::size_t n, std::function<bool(T& obj)> func)
{
    auto rit = objects.rbegin();
    while (n > 0 && rit != objects.rend()) {
        if (T* objPtr = std::get_if<T>(&*rit)) {
            n -= func(*objPtr) ? 1 : 0;
        }
        ++rit;
    }
}

DeviceObjects parseDeviceObjectsFromRecoding(
    std::istream& recStream, TimePoint fromTimestamp, TimePoint toTimestamp)
{
    DeviceObjects result;
    std::map<TimePoint, DeviceLocationPoint*> atsToLocation;
    std::map<TimePoint::duration, DeviceImage*> rtsToImage;
    std::map<TimePoint::duration, TimePoint> rtsToTimestamp;

    CaptureState captureState = CaptureState::UNKNOWN;
    std::size_t skippedUnmatchedRecords = 0;
    std::size_t skippedOOBRecords = 0;

    maps::pb_stream2::Reader pbReader(&recStream);
    for (auto proxy: pbReader)
        try {
            auto record = proxy.as<precord::Record>();

            const auto recTimestamp = getRecordTimestamp(record);
            if (recTimestamp < fromTimestamp || toTimestamp < recTimestamp) {
                ++skippedOOBRecords;
                continue;
            }

            if (record.HasExtension(pmapkit2::location::LOCATION_RECORD)) {
                const auto locationRecord =
                    record.GetExtension(pmapkit2::location::LOCATION_RECORD);

                if (locationRecord.has_location() &&
                    locationRecord.location().has_position() &&
                    locationRecord.location().has_absolute_timestamp()) {
                    const auto& pos = locationRecord.location().position();
                    const TimePoint absTs{std::chrono::milliseconds{
                        locationRecord.location().absolute_timestamp()}};
                    const TimePoint::duration relTs{std::chrono::milliseconds{
                        locationRecord.location().relative_timestamp()}};

                    DeviceLocationPoint newLocation{{pos.lon(), pos.lat()},
                                                    relTs,
                                                    absTs,
                                                    StorageState::MEMORY,
                                                    captureState};

                    if (atsToLocation.count(absTs) == 0) {
                        result.push_back(newLocation);
                        atsToLocation[absTs] =
                            &std::get<DeviceLocationPoint>(result.back());
                    } else {
                        // Just update the location pointj
                        *atsToLocation[absTs] = newLocation;
                    }
                }

            } else if (record.HasExtension(plog_event::EVENT_RECORD)) {
                const auto& eventRecord =
                    record.GetExtension(plog_event::EVENT_RECORD);
                const std::string& component = eventRecord.component();
                const std::string& event = eventRecord.event();

                if (component == EXECUTION_SESSION_COMPONENT) {
                    if (event == NEW_IMAGE_EVENT) {
                        const auto relativeTs =
                            getCheckedParamValueAsDuration<
                                std::chrono::milliseconds>(
                                eventRecord, IMAGE_RELATIVE_TS);

                        DeviceImage newImage{std::nullopt,
                                             relativeTs,
                                             LocationState::UNKNOWN,
                                             StorageState::MEMORY,
                                             captureState};

                        if (rtsToImage.count(relativeTs) == 0) {
                            result.push_back(newImage);
                            rtsToImage.emplace(
                                relativeTs,
                                &std::get<DeviceImage>(result.back()));

                            rtsToTimestamp[relativeTs] = recTimestamp;
                        } else {
                            WARN() << "Duplicated new image event with rts = "
                                   << relativeTs.count() << " ("
                                   << rtsToTimestamp[relativeTs]
                                          .time_since_epoch()
                                          .count()
                                   << ", "
                                   << recTimestamp.time_since_epoch().count()
                                   << ")";
                            // TODO: take into account that images with the
                            //       same relative timestamp are replaces old
                            //       images in the device storage becasue they
                            //       are saved by key which is their absolute
                            //       timestamp inferred from the relative timestamp.
                            *rtsToImage[relativeTs] = newImage;
                        }

                    } else if (event == GOT_LOCATED_IMAGE_EVENT) {
                        const auto relativeTs =
                            getCheckedParamValueAsDuration<
                                std::chrono::milliseconds>(
                                eventRecord, IMAGE_RELATIVE_TS);
                        // TODO: Check state transition
                        rtsToImage.at(relativeTs)->locationState =
                            LocationState::LOCATED;

                    } else if (event == BUFFER_IMAGE_EVENT) {
                        const auto relativeTs =
                            getCheckedParamValueAsDuration<
                                std::chrono::milliseconds>(
                                eventRecord, IMAGE_RELATIVE_TS);
                        // TODO: Check state transition
                        rtsToImage.at(relativeTs)->storageState =
                            StorageState::BUFFERED;

                    } else if (
                        auto savedImagesNum = readSavedImagesNumber(event)) {
                        applyToLastNObjects<DeviceImage>(
                            result, *savedImagesNum, [](auto& img) {
                                if (img.storageState ==
                                    StorageState::BUFFERED) {
                                    img.storageState = StorageState::STORED;
                                    return true;
                                }
                                return false;
                            });

                    } else if (
                        auto savedLocationsNum =
                            readSavedLocationsNumber(event)) {
                        applyToLastNObjects<DeviceLocationPoint>(
                            result, *savedLocationsNum, [](auto& location) {
                                location.storageState = StorageState::STORED;
                                return true;
                            });

                    } else if (event == CAPTURE_OFF_EVENT) {
                        captureState = CaptureState::OFF;
                    } else if (startsWith(event, CAPTURE_PREFIX)) {
                        captureState = CaptureState::ON;
                    }

                } else if (component == LOCATION_EVALUATOR_COMPONENT) {
                    if (event == REQUEST_IMAGE_LOCATION_EVENT) {
                        const auto relativeTs =
                            getCheckedParamValueAsDuration<
                                std::chrono::milliseconds>(
                                eventRecord, IMAGE_RELATIVE_TS);
                        rtsToImage.at(relativeTs)->locationState =
                            LocationState::REQUEST_COMPLETED;
                    } else if (event == IMAGE_LOCATION_EVALUATION_FAILED_EVENT) {
                        const auto relativeTs =
                            getCheckedParamValueAsDuration<
                                std::chrono::milliseconds>(
                                eventRecord, IMAGE_RELATIVE_TS);
                        rtsToImage.at(relativeTs)->locationState =
                            LocationState::REQUEST_FAILED;
                    }
                }
            }
        } catch (std::out_of_range) {
            ++skippedUnmatchedRecords;
        }

    if (skippedUnmatchedRecords > 0) {
        WARN() << "There are " << skippedUnmatchedRecords
               << " skipped unmatched records";
    }
    INFO() << "Skipped OOB records " << skippedOOBRecords;

    return result;
}

DeviceObjects getDeviceObjectsFromRecoding(
    maps::mds::Mds mdsClient,
    maps::pgpool3::TransactionHandle& txn,
    const std::string& sourceId,
    mrc::db::TId assignmentId,
    TimePoint fromTimestamp,
    TimePoint toTimestamp)
{
    const auto mdsKeys = getMdsKeys(txn, sourceId, assignmentId);
    auto recStream = downloadRecordings(mdsClient, mdsKeys, toTimestamp);
    return parseDeviceObjectsFromRecoding(
        recStream, fromTimestamp, toTimestamp);
}

using TrackPointMatches =
    std::map<const DeviceObject*, const mrc::db::TrackPoint*>;

TrackPointMatches matchTrackPoints(
    const DeviceObjects& deviceObjects,
    const mrc::db::TrackPoints& trackPoints)
{
    TrackPointMatches trackPointMatches;
    for (const auto& object: deviceObjects) {
        if (const auto* locPtr = std::get_if<DeviceLocationPoint>(&object)) {
            // NOTE: the device location point absolute timestamp matches the
            //       server side track point timestamp.
            const auto trackPointIt = std::lower_bound(
                trackPoints.begin(),
                trackPoints.end(),
                *locPtr,
                [](const mrc::db::TrackPoint& trackPoint,
                   const DeviceLocationPoint& loc) {
                    return trackPoint.timestamp() <
                           loc.absoluteTs;
                });
            if (trackPointIt != trackPoints.end() &&
                trackPointIt->timestamp() ==
                    locPtr->absoluteTs) {
                trackPointMatches.emplace(&object, &*trackPointIt);
            }
        }
    }
    return trackPointMatches;
}

using ClosestImageLocation =
    std::map<const DeviceImage*, const DeviceObject*>;

ClosestImageLocation findClosestImageLocation(
    const DeviceObjects& deviceObjects,
    const TrackPointMatches& trackPointMatches)
{
    ClosestImageLocation closestLocations;

    const DeviceObject* lastVisitedLocationPtr = nullptr;
    const DeviceImage* lastVisitedImagePtr = nullptr;
    for (const auto& object: deviceObjects) {
        if (std::get_if<DeviceLocationPoint>(&object) &&
            trackPointMatches.count(&object)) {
            // NOTE: Unmatched location points are skipped here.
            lastVisitedLocationPtr = &object;
        } else if (const auto* imgPtr = std::get_if<DeviceImage>(&object)) {
            lastVisitedImagePtr = imgPtr;
        }

        if (lastVisitedLocationPtr && lastVisitedImagePtr) {
            const auto& closestLocationPtr =
                closestLocations[lastVisitedImagePtr];

            if (!closestLocationPtr) {
                closestLocations[lastVisitedImagePtr] =
                    lastVisitedLocationPtr;
            } else {
                const auto& closestLocation =
                    std::get<DeviceLocationPoint>(*closestLocationPtr);
                const auto& lastVisitedLocation =
                    std::get<DeviceLocationPoint>(*lastVisitedLocationPtr);

                if (abs(lastVisitedImagePtr->relativeTs -
                        lastVisitedLocation.relativeTs) <
                    abs(lastVisitedImagePtr->relativeTs -
                        closestLocation.relativeTs)) {
                    closestLocations[lastVisitedImagePtr] =
                        lastVisitedLocationPtr;
                }
            }
        }
    }

    return closestLocations;
}

using ImageMatch =
    std::pair<const DeviceObject*, const mrc::db::Feature*>;
using ImageMatches =
    std::map<ImageMatch::first_type, ImageMatch::second_type>;

ImageMatches matchImages(
    const DeviceObjects& deviceObjects,
    const TrackPointMatches& trackPointMatches,
    const mrc::db::Features& features)
{
    ImageMatches imageMatches;
    const auto closestLocations =
        findClosestImageLocation(deviceObjects, trackPointMatches);

    std::map<const mrc::db::Feature*, const DeviceObject*>
        reverseMatch;

    for (const auto& object: deviceObjects) {
        if (const auto* imgPtr = std::get_if<DeviceImage>(&object)) {
            const auto closestLocationPtr = closestLocations.at(imgPtr);
            if (closestLocationPtr) {
                const auto closeTrackPointPtr =
                    trackPointMatches.at(closestLocationPtr);
                const auto& loc =
                    std::get<DeviceLocationPoint>(*closestLocationPtr);

                REQUIRE(
                    closeTrackPointPtr,
                    "There must be a matched track point");

                const auto relativeTsDiff =
                    imgPtr->relativeTs - loc.relativeTs;
                const auto absoluteTs =
                    closeTrackPointPtr->timestamp() +
                    relativeTsDiff;

                // This solution only may work with complete recordings
                // so it expects if an image is in the recorindgs then it
                // must be in MRC DB.
                auto matchedFeatureIt = std::lower_bound(
                    features.begin(),
                    features.end(),
                    absoluteTs,
                    [](const mrc::db::Feature& feature,
                       TimePoint ts) {
                        return feature.timestamp() <
                               ts - ACCEPTIBLE_IMAGES_MATCH_TIME_DIFF;
                    });

                if (matchedFeatureIt == features.end()) {
                    continue;
                }

                const auto closer = [absoluteTs](
                                        const auto& lhsFeature,
                                        const auto& rhsFeature) {
                    const auto lhsDist =
                        abs(lhsFeature.timestamp() - absoluteTs);
                    const auto rhsDist =
                        abs(rhsFeature.timestamp() - absoluteTs);
                    return lhsDist < rhsDist;
                };

                // Look for the closest feature
                while (
                    std::next(matchedFeatureIt) != features.end() &&
                    closer(*std::next(matchedFeatureIt), *matchedFeatureIt)) {
                    ++matchedFeatureIt;
                }

                const auto absoluteTsDiff =
                    matchedFeatureIt->timestamp() -
                    closeTrackPointPtr->timestamp();
                const auto crossTimeDiff =
                    abs(absoluteTsDiff - relativeTsDiff);

                if (ACCEPTIBLE_IMAGES_MATCH_TIME_DIFF < crossTimeDiff) {
                    continue;
                }

                const auto exisitingImgObjPtr =
                    reverseMatch[&*matchedFeatureIt];

                if (!exisitingImgObjPtr) {
                    imageMatches[&object] = &*matchedFeatureIt;
                    reverseMatch[&*matchedFeatureIt] = &object;

                } else {
                    const auto exisitingImgPtr =
                        std::get_if<DeviceImage>(exisitingImgObjPtr);
                    REQUIRE(
                        exisitingImgPtr,
                        "There must be an exiting device image");

                    if (abs(relativeTsDiff) <
                        abs(exisitingImgPtr->relativeTs - loc.relativeTs)) {
                        imageMatches.erase(exisitingImgObjPtr);
                        imageMatches[&object] = &*matchedFeatureIt;
                        reverseMatch[&*matchedFeatureIt] = &object;
                    }
                }
            }
        }
    }
    return imageMatches;
}

std::tuple<TrackPointMatches, ImageMatches>
matchDeviceObjectsAgainstServerObjects(
    const DeviceObjects& deviceObjects,
    const mrc::db::TrackPoints& trackPoints,
    const mrc::db::Features& features)
{
    TrackPointMatches trackPointMatches =
        matchTrackPoints(deviceObjects, trackPoints);
    ImageMatches imageMatches =
        matchImages(deviceObjects, trackPointMatches, features);
    return {trackPointMatches, imageMatches};
}

void printBasicStatistics(
    const DeviceObjects& deviceObjects,
    const TrackPointMatches& trackPointMatches,
    const ImageMatches& imageMatches)
{
    std::size_t totalLocationPoints = 0;
    std::size_t matchedLocationPoints = 0;

    std::size_t totalImages = 0;
    std::size_t matchedImages = 0;

    std::map<CaptureState, std::size_t> locationCaptureStatesCount;
    std::map<StorageState, std::size_t> locationStorageStatesCount;
    std::map<CaptureState, std::size_t> imageCaptureStatesCount;
    std::map<StorageState, std::size_t> imageStorageStatesCount;
    std::map<LocationState, std::size_t> imageLocationStatesCount;

    for (const auto& object: deviceObjects) {
        if (const auto locPtr = std::get_if<DeviceLocationPoint>(&object)) {
            ++totalLocationPoints;
            matchedLocationPoints += trackPointMatches.count(&object);
            ++locationCaptureStatesCount[locPtr->captureState];
            ++locationStorageStatesCount[locPtr->storageState];

        } else if (const auto imgPtr = std::get_if<DeviceImage>(&object)) {
            ++totalImages;
            matchedImages += imageMatches.count(&object);
            ++imageCaptureStatesCount[imgPtr->captureState];
            ++imageStorageStatesCount[imgPtr->storageState];
            ++imageLocationStatesCount[imgPtr->locationState];
        }
    }

    INFO() << "RECORDING REPORT BASIC STATISTICS";
    INFO() << "                                 ";
    INFO() << "======== LOCAITON POINTS ========";
    INFO() << " Total location points     " << totalLocationPoints;
    INFO() << " Matched location points   " << matchedLocationPoints;
    INFO() << " Capture state - UNKNOWN   "
           << locationCaptureStatesCount[CaptureState::UNKNOWN];
    INFO() << " Capture state - OFF       "
           << locationCaptureStatesCount[CaptureState::OFF];
    INFO() << " Capture state - ON        "
           << locationCaptureStatesCount[CaptureState::ON];
    INFO() << " Storage state - UNKNOWN   "
           << locationStorageStatesCount[StorageState::UNKNOWN];
    INFO() << " Storage state - MEMORY    "
           << locationStorageStatesCount[StorageState::MEMORY];
    INFO() << " Storage state - BUFFERED  "
           << locationStorageStatesCount[StorageState::BUFFERED];
    INFO() << " Storage state - STORED    "
           << locationStorageStatesCount[StorageState::STORED];
    INFO() << "                                 ";
    INFO() << "============ IMAGES =============";
    INFO() << " Total images taken        " << totalImages;
    INFO() << " Matched images            " << matchedImages;
    INFO() << " Capture state - UNKNOWN   "
           << imageCaptureStatesCount[CaptureState::UNKNOWN];
    INFO() << " Capture state - OFF       "
           << imageCaptureStatesCount[CaptureState::OFF];
    INFO() << " Capture state - ON        "
           << imageCaptureStatesCount[CaptureState::ON];
    INFO() << " Storage state - UNKNOWN   "
           << imageStorageStatesCount[StorageState::UNKNOWN];
    INFO() << " Storage state - MEMORY    "
           << imageStorageStatesCount[StorageState::MEMORY];
    INFO() << " Storage state - BUFFERED  "
           << imageStorageStatesCount[StorageState::BUFFERED];
    INFO() << " Storage state - STORED    "
           << imageStorageStatesCount[StorageState::STORED];
    INFO() << "Location state - UNKNOWN   "
           << imageLocationStatesCount[LocationState::UNKNOWN];
    INFO() << "Location state - REQUESTED "
           << imageLocationStatesCount[LocationState::REQUESTED];
    INFO() << "Location state - COMPLETED "
           << imageLocationStatesCount[LocationState::REQUEST_COMPLETED];
    INFO() << "Location state - FAILED    "
           << imageLocationStatesCount[LocationState::REQUEST_FAILED];
    INFO() << "Location state - LOCATED   "
           << imageLocationStatesCount[LocationState::LOCATED];
}

using OptionalPoint = std::optional<maps::geolib3::Point2>;
using OptionalDirection = std::optional<maps::geolib3::Direction2>;
using OptionalOrientedPosition = std::tuple<OptionalPoint, OptionalDirection>;

OptionalOrientedPosition getOrientedPosition(
    const std::optional<maps::geolib3::Point2>& prevLocationPos,
    const DeviceObject& object,
    const ImageMatches& imageMatches)
{
    OptionalOrientedPosition result{std::nullopt, std::nullopt};
    if (const auto* locPtr = std::get_if<DeviceLocationPoint>(&object)) {
        // Just dont compute direction if two points too far away from each other
        if (prevLocationPos &&
            maps::geolib3::geoDistance(*prevLocationPos, locPtr->position) <
                30.0) {
            std::get<OptionalDirection>(result) = maps::geolib3::Direction2{
                maps::geolib3::Segment2{*prevLocationPos, locPtr->position}};
        }
        std::get<OptionalPoint>(result) = locPtr->position;
    } else if (const auto* imgPtr = std::get_if<DeviceImage>(&object)) {
        if (imageMatches.count(&object)) {
            const auto& feature = *imageMatches.at(&object);
            std::get<OptionalPoint>(result) = feature.geodeticPos();
            std::get<OptionalDirection>(result) = mrc::db::direction(feature);
        }
    } else {
        REQUIRE(false, "Did you forget to hande some new object type?");
    }
    return result;
}

using TargetsSpatialTree = maps::geolib3::
    StaticGeometrySearcher<maps::geolib3::Polyline2, mrc::db::Segments*>;

mrc::db::Segments getAffectedTargetSegments(
    TargetsSpatialTree& targetsSpatialTree,
    const maps::geolib3::Point2& position,
    const maps::geolib3::Direction2& direction,
    mrc::db::CameraDeviation cameraDeviation)
{
    mrc::db::Segments affected;
    auto searchResult = targetsSpatialTree.find(
        mrc::db::addVisibilityMargins(position.boundingBox()));

    for (; searchResult.first != searchResult.second; ++searchResult.first) {
        mrc::db::Segments* unaffected = searchResult.first->value();

        if (!mrc::db::isVisible(
                position, direction, cameraDeviation, *unaffected)) {
            continue;
        }

        const auto fov = mrc::db::fieldOfView(
            mrc::db::getRay(position, direction, cameraDeviation));

        std::vector<BLinestring> intersections;
        for (const auto& segment: *unaffected) {
            boost::geometry::intersection(
                toBoost(segment), toBoost(fov), intersections);
        }

        for (const auto& intersection: intersections) {
            if (boost::geometry::length(intersection) > maps::geolib3::EPS) {
                affected.push_back(fromBoost(intersection));
            }
        }

        std::vector<BLinestring> differences;
        for (const auto& segment: *unaffected) {
            boost::geometry::difference(
                toBoost(segment), toBoost(fov), differences);
        }

        mrc::db::Segments newUnaffected;
        for (const auto& difference: differences) {
            if (boost::geometry::length(difference) > maps::geolib3::EPS) {
                newUnaffected.push_back(fromBoost(difference));
            }
        }
        unaffected->swap(newUnaffected);
    }

    return affected;
}

maps::geolib3::Segment2 tryShiftGeom(
    const maps::geolib3::Segment2& segment, double shiftMeters = 10.0)
{
    if (maps::geolib3::sign(shiftMeters) == 0) {
        return segment;
    }
    try {
        maps::geolib3::Polyline2 pLine{segment};
        auto mercPLine = maps::geolib3::convertGeodeticToMercator(pLine);
        auto shiftedMercLine = maps::geolib3::equidistant(
            mercPLine, shiftMeters, maps::geolib3::Clockwise);
        auto shifterGeolPLine =
            maps::geolib3::convertMercatorToGeodetic(shiftedMercLine);
        return shifterGeolPLine.segments().front();
    } catch (const maps::Exception& ex) {
        ERROR() << "Failed to shift geom " << ex;
        return segment;
    }
}

mrc::db::Segments tryShiftGeom(
    const mrc::db::Segments& segments, double shiftMeters = 10.0)
{
    mrc::db::Segments result;
    for (const auto& segment: segments) {
        result.push_back(tryShiftGeom(segment, shiftMeters));
    }
    return result;
}

NYT::TNode pointToYsonEV(
    const maps::geolib3::Point2& pt,
    const ev::PointStyle& ptStyle,
    const std::string& timestamp)
{
    // clang-format off
    return NYT::TNode::CreateMap()
        (ev::yson::POINTSTYLE, ev::yson::pointStyle(
            ptStyle.fill, ptStyle.outline, ptStyle.radius))
        (ev::yson::LON, pt.x())
        (ev::yson::LAT, pt.y())
        ("value", TString{timestamp});
    // clang-format on
}

std::vector<NYT::TNode> orientedPositionToYsonEV(
    const maps::geolib3::Point2& position,
    const maps::geolib3::Direction2& /*direction*/,
    const ev::PointStyle& ptStyle,
    const std::string& timestamp)
{
    return {pointToYsonEV(position, ptStyle, timestamp)};
    // TODO: draw a line sticking from a point in the specified direction
}

NYT::TNode segmentToYsonEV(
    const maps::geolib3::Segment2& segment, const ev::LineStyle& lineStyle)
{
    // clang-format off
    return NYT::TNode::CreateMap()
        (ev::yson::LINESTYLE, ev::yson::lineStyle(lineStyle.color, lineStyle.width))
        (ev::yson::START_LON, segment.start().x())
        (ev::yson::START_LAT, segment.start().y())
        (ev::yson::END_LON, segment.end().x())
        (ev::yson::END_LAT, segment.end().y());
    // clang-format on
}

std::vector<NYT::TNode> segmentsToYsonEV(
    const mrc::db::Segments& segments, const ev::LineStyle& lineStyle)
{
    std::vector<NYT::TNode> result;
    for (const auto& segment: segments) {
        result.push_back(segmentToYsonEV(segment, lineStyle));
    }
    return result;
}

// Compute covered and uncovered segments for targets with the specified
// capture mode.
// FIXME: cameraDeviation may be different for different features for the same
//        pair <source_id, assignment_id>
std::vector<NYT::TNode> toEasyViewYson(
    const mrc::db::ugc::Targets& targets,
    const DeviceObjects& deviceObjects,
    const ImageMatches& imageMatches,
    mrc::db::CameraDeviation cameraDeviation,
    bool drawPoints)
{
    INFO() << "Targets legend";
    INFO() << "      red - uncovered (capture mode on)";
    INFO() << "     blue - uncovered (capture mode off or unknown)";
    INFO() << "    green - covered (capture mode on)";
    INFO() << "   yellow - could be covered (capture mode off)";
    if (drawPoints) {
        INFO() << "Points legend";
        INFO() << "       grey:red - unsaved images";
        INFO() << "      grey:blue - device location points and track line";
        INFO() << "     grey:green - saved images";
    }

    std::list<mrc::db::Segments> unaffectedCollections;
    TargetsSpatialTree targetsSpatialTree;
    for (const auto& target: targets) {
        auto segmentsRage = target.geodeticGeom().segments();
        unaffectedCollections.push_back(
            {segmentsRage.begin(), segmentsRage.end()});
        targetsSpatialTree.insert(
            &target.geodeticGeom(), &unaffectedCollections.back());
    }
    targetsSpatialTree.build();

    OptionalPoint prevLocationPos = std::nullopt;
    const auto forAllDeviceObjects =
        [&](std::function<void(
                const DeviceObject&,
                const maps::geolib3::Point2&,
                const maps::geolib3::Direction2&)> func) {
            for (const auto& object: deviceObjects) {
                const auto [position, direction] = getOrientedPosition(
                    prevLocationPos, object, imageMatches);

                prevLocationPos = position;

                if (!position || !direction) {
                    continue;
                }

                func(object, *position, *direction);
            }
        };

    std::vector<NYT::TNode> imageCapturesEV;
    std::vector<NYT::TNode> imagePointsEV;

    forAllDeviceObjects([&](const DeviceObject& object,
                            const maps::geolib3::Point2& position,
                            const maps::geolib3::Direction2& direction) {
        const auto* imgPtr = std::get_if<DeviceImage>(&object);
        if (!imgPtr)
            return;

        ev::PointStyle ptStyle = imgPtr->storageState == StorageState::STORED
                                     ? SAVED_IMAGE_POINT_STYLE
                                     : UNSAVED_IMAGE_POINT_STYLE;

        const auto affectedSegments = getAffectedTargetSegments(
            targetsSpatialTree, position, direction, cameraDeviation);

        extend(
            imageCapturesEV,
            segmentsToYsonEV(
                tryShiftGeom(affectedSegments), COVERED_TARGETS_STYLE));

        extend(
            imagePointsEV,
            orientedPositionToYsonEV(
                position,
                direction,
                ptStyle,
                std::to_string(imgPtr->relativeTs.count())));
    });

    prevLocationPos = std::nullopt;
    std::vector<NYT::TNode> locationCapturesEV;
    std::vector<NYT::TNode> locationPointsEV;

    forAllDeviceObjects([&](const DeviceObject& object,
                            const maps::geolib3::Point2& position,
                            const maps::geolib3::Direction2& direction) {
        const auto* locPtr = std::get_if<DeviceLocationPoint>(&object);
        if (!locPtr) {
            return;
        }

        const ev::LineStyle segStyle =
            locPtr->captureState == CaptureState::ON
                ? MUSTBE_COVERED_TARGETS_STYLE
                : COULDBE_COVERED_TARGETS_STYLE;

        const auto affectedSegments = getAffectedTargetSegments(
            targetsSpatialTree, position, direction, cameraDeviation);

        extend(
            locationCapturesEV,
            segmentsToYsonEV(tryShiftGeom(affectedSegments), segStyle));

        extend(
            locationPointsEV,
            orientedPositionToYsonEV(
                position,
                direction,
                TRACK_POINT_STYLE,
                std::to_string(
                    locPtr->absoluteTs.time_since_epoch().count())));
    });

    std::vector<NYT::TNode> ysonEasyView;
    for (const auto& unaffected: unaffectedCollections) {
        extend(
            ysonEasyView,
            segmentsToYsonEV(
                tryShiftGeom(unaffected), UNCOVERED_TARGETS_STYLE));
    };

    if (drawPoints) {
        extend(ysonEasyView, std::move(locationPointsEV));
        extend(ysonEasyView, std::move(imagePointsEV));
    }
    extend(ysonEasyView, std::move(imageCapturesEV));
    extend(ysonEasyView, std::move(locationCapturesEV));

    return ysonEasyView;
}

void dumpYson(const std::vector<NYT::TNode>& ysonLines, std::ostream& out)
{
    for (const auto& ysonLine: ysonLines) {
        const TString line = NYT::NodeToCanonicalYsonString(ysonLine);
        out << std::string(line) << "\n";
    }
}

void run(
    const mrc::common::Config& config,
    const maps::cmdline::Option<std::string>& sourceIdOpt,
    const maps::cmdline::Option<std::string>& fromFeatureIdOpt,
    const maps::cmdline::Option<std::string>& toFeatureIdOpt,
    const maps::cmdline::Option<std::string>& fromTimestampOpt,
    const maps::cmdline::Option<std::string>& toTimestampOpt,
    const maps::cmdline::Option<bool>& drawPointsOpt,
    const maps::cmdline::Option<std::string>& outputFileName)
{
    auto poolHolder = config.makePoolHolder();

    auto txn = poolHolder.pool().slaveTransaction();

    const auto [fromFeature, fromTimestamp] = getStartingCriteria(
        txn, sourceIdOpt, fromFeatureIdOpt, fromTimestampOpt);

    const auto [toFeature, toTimestamp] = getFinishingCriteria(
        txn, fromFeature, toFeatureIdOpt, toTimestampOpt);

    INFO() << "Gathering data for features [" << fromFeature.id() << ", "
           << toFeature.id() << "] within the following time span ["
           << maps::chrono::formatSqlDateTime(fromTimestamp) << ", "
           << maps::chrono::formatSqlDateTime(toTimestamp) << "]";

    const auto [trackPoints, features, targets, cameraDeviation] =
        loadMrcDBData(
            txn,
            fromFeature.sourceId(),
            fromFeature.id(),
            toFeature.id(),
            fromTimestamp,
            toTimestamp);

    INFO() << "Camera deviation is "
           << mrc::tasks_planner::toString(cameraDeviation);

    REQUIRE(fromFeature.assignmentId().has_value(), "empty assignmentId");
    const auto deviceObjects = getDeviceObjectsFromRecoding(
        config.makeMdsClient(),
        txn,
        fromFeature.sourceId(),
        fromFeature.assignmentId().value(),
        fromTimestamp,
        toTimestamp);

    const auto [trackPointMatches, imageMatches] =
        matchDeviceObjectsAgainstServerObjects(
            deviceObjects, trackPoints, features);

    printBasicStatistics(deviceObjects, trackPointMatches, imageMatches);

    const auto easyViewYson = toEasyViewYson(
        targets, deviceObjects, imageMatches, cameraDeviation, drawPointsOpt);

    if (outputFileName.defined()) {
        std::ofstream out(outputFileName);
        dumpYson(easyViewYson, out);
    } else {
        dumpYson(easyViewYson, std::cout);
    }

    // TODO:
    //  1. Load features, track points and targets from DB
    //  [DONE]
    //  2. Load recordings from MDS [DONE]
    //  3. Parse recordings into consequtive events [DONE]
    //  4. Group events (location updates, image events). But don't
    //  regroup initial sequence of events [DONE]
    //  5. Find best suited track points for location events
    //  6. Match image event groups and features (to obtain server side
    //     timestamp and direction) [DONE]
    //  7. Gather basic statistics [DONE]
    //  8. Use EasyView to visualize the sever side track in different modes
    //     a) Capture off
    //     b) Capture on with feature coverage
    //     c) Capture on without feature coverage
    //     [DONE]
    //  9. Use cubic Bezier curve (as on device) to obtain device side
    //     position and direction of image event groups using matched
    //     location updates.
    //     [LATER]
    // 10. Use EasyView to visualize the device side track in different modes
    //     [LATER]
    //     a) Capture off
    //     b) Capture on with feature coverage
    //     c) Capture on without feature coverage
}

} // namespace

int main(int argc, char** argv) try {
    maps::cmdline::Parser parser("Tool for recordings analysis");
    auto fromFeatureIdOpt =
        parser.string("from-feature").help("Feature ID to start from");
    auto toFeatureIdOpt =
        parser.string("to-feature").help("Feature ID to stop upon");
    auto sourceIdOpt = parser.string("source-id")
                           .help("Source ID to use with --from-timestamp");
    auto fromTimestampOpt =
        parser.string("from-timestamp").help("Local timestamp");
    auto toTimestampOpt =
        parser.string("to-timestamp").help("Local timestamp");
    auto mrcConfigPath =
        parser.string("mrc-config").help("Path to MRC config");
    auto secretVersion =
        parser.string("secret-version")
            .required()
            .help("Version for secrets from yav.yandex-team.ru");
    auto drawPointsOpt =
        parser.flag("draw-points")
            .help("Render location and image points to easy view")
            .defaultValue(false);
    auto outputFileOpt =
        parser.string("output").help("Output easy view filename");

    parser.parse(argc, argv);

    const auto config =
        maps::mrc::common::templateConfigFromCmdPath(secretVersion, mrcConfigPath);

    run(config,
        sourceIdOpt,
        fromFeatureIdOpt,
        toFeatureIdOpt,
        fromTimestampOpt,
        toTimestampOpt,
        drawPointsOpt,
        outputFileOpt);

    return EXIT_SUCCESS;
} catch (const maps::Exception& e) {
    ERROR() << e;
    return EXIT_FAILURE;
} catch (const std::exception& e) {
    ERROR() << e.what();
    return EXIT_FAILURE;
} catch (...) {
    ERROR() << "Caught unknown exception";
    return EXIT_FAILURE;
}
