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

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/gateway.h>
#include <maps/wikimap/mapspro/services/mrc/tasks-planner/lib/disjoint_polygons.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/test_tools/comparison.h>
#include <maps/libs/deprecated/localeutils/include/locale.h>
#include <maps/libs/deprecated/localeutils/include/localemapper.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/std/vector.h>
#include <maps/libs/json/include/value.h>

#include <boost/optional.hpp>

#include <algorithm>
#include <locale>
#include <string>

namespace maps {
namespace mrc {
namespace tasks_planner {

namespace str{

static constexpr enum_io::Representations<db::ugc::AssignmentObjectType>
ASSIGNMENT_OBJECT_TYPE_STRINGS{
    {db::ugc::AssignmentObjectType::Barrier, "Barrier"},
    {db::ugc::AssignmentObjectType::Deadend, "Deadend"},
    {db::ugc::AssignmentObjectType::BadConditions, "BadConditions"},
    {db::ugc::AssignmentObjectType::NoEntry, "NoEntry"},
};

static constexpr enum_io::Representations<db::CameraDeviation>
CAMERA_DEVIATIONS_STRINGS{
    {db::CameraDeviation::Front, "Front"},
    {db::CameraDeviation::Right, "Right"},
    {db::CameraDeviation::Back, "Back"},
    {db::CameraDeviation::Left, "Left"}
};

static constexpr enum_io::Representations<db::FeaturePrivacy>
FEATURE_PRIVACY_STRINGS{
    {db::FeaturePrivacy::Public, "Public"},
    {db::FeaturePrivacy::Restricted, "Restricted"},
    {db::FeaturePrivacy::Secret, "Secret"}
};

static constexpr enum_io::Representations<db::ugc::TasksGroupStatus>
TASKS_GROUP_STATUS_STRINGS{
    {db::ugc::TasksGroupStatus::Draft, "Draft"},
    {db::ugc::TasksGroupStatus::Generating, "Generating"},
    {db::ugc::TasksGroupStatus::Open, "Open"},
    {db::ugc::TasksGroupStatus::InProgress, "InProgress"},
    {db::ugc::TasksGroupStatus::Closed, "Closed"},
    {db::ugc::TasksGroupStatus::Failed, "Failed"}
};

static constexpr enum_io::Representations<db::ugc::TaskStatus>
TASK_STATUS_STRINGS{
    {db::ugc::TaskStatus::Draft, "Draft"},
    {db::ugc::TaskStatus::New, "Open"},
    {db::ugc::TaskStatus::Acquired, "InProgress"},
    {db::ugc::TaskStatus::Done, "Closed"},
};

static constexpr enum_io::Representations<db::ugc::AssignmentStatus>
ASSIGNMENT_STATUS_STRINGS{
    {db::ugc::AssignmentStatus::Active, "InProgress"},
    {db::ugc::AssignmentStatus::Abandoned, "Abandoned"},
    {db::ugc::AssignmentStatus::Completed, "Resolved"},
    {db::ugc::AssignmentStatus::Accepted, "Closed"},
    {db::ugc::AssignmentStatus::Revoked, "Revoked"}
};

static constexpr enum_io::Representations<db::ObjectInPhotoType>
OBJECT_IN_PHOTO_TYPE_STRINGS{
    {db::ObjectInPhotoType::Face, "Face"},
    {db::ObjectInPhotoType::LicensePlate, "LicensePlate"},
};


const std::string ACTUALIZED_BEFORE = "actualizedBefore";
const std::string CAMERA_DIRECTIONS = "cameraDirections";
const std::string EMAILS = "emails";
const std::string EXCLUDE_DEADENDS = "excludeDeadends";
const std::string FCS = "fcs";
const std::string GEOMETRY = "geometry";
const std::string ID = "id";
const std::string IGNORE_PRIVATE_AREA = "ignorePrivateArea";
const std::string MIN_EDGE_COVERAGE_RATIO = "minEdgeCoverageRatio";
const std::string NAME = "name";
const std::string PARAMS = "params";
const std::string RECOMMENDED_TASK_LENGTH = "recommendedTaskLengthMeters";
const std::string ROUTING = "routing";
const std::string STATUS = "status";
const std::string TOLL = "toll";
const std::string ALLOWED_ASSIGNEES_LOGINS = "allowedAssigneesLogins";

} // namespace str


std::string toString(db::ugc::TasksGroupStatus status)
{
    return std::string(toString(status, str::TASKS_GROUP_STATUS_STRINGS));
}

template<>
db::CameraDeviation fromString(const std::string& value)
{
    db::CameraDeviation cameraDeviation;
    fromString(value, cameraDeviation, str::CAMERA_DEVIATIONS_STRINGS);
    return cameraDeviation;
}

std::string toString(db::CameraDeviation cameraDeviation)
{
    return std::string(toString(cameraDeviation, str::CAMERA_DEVIATIONS_STRINGS));
}

template<>
db::FeaturePrivacy fromString(const std::string& value)
{
    db::FeaturePrivacy privacy;
    fromString(value, privacy, str::FEATURE_PRIVACY_STRINGS);
    return privacy;
}

std::string toString(db::FeaturePrivacy privacy)
{
    return std::string(toString(privacy, str::FEATURE_PRIVACY_STRINGS));
}


template<>
db::ugc::TasksGroupStatus fromString(const std::string& value)
{
    db::ugc::TasksGroupStatus status;
    fromString(value, status, str::TASKS_GROUP_STATUS_STRINGS);
    return status;
}


std::string toString(db::ugc::TaskStatus status)
{
    return std::string(toString(status, str::TASK_STATUS_STRINGS));
}

template<>
db::ugc::TaskStatus fromString(const std::string& value)
{
    db::ugc::TaskStatus status;
    fromString(value, status, str::TASK_STATUS_STRINGS);
    return status;
}


std::string toString(db::ugc::AssignmentStatus status)
{
    return std::string(toString(status, str::ASSIGNMENT_STATUS_STRINGS));
}

template<>
db::ugc::AssignmentStatus fromString(const std::string& value)
{
    db::ugc::AssignmentStatus status;
    fromString(value, status, str::ASSIGNMENT_STATUS_STRINGS);
    return status;
}

template<>
db::ObjectInPhotoType fromString(const std::string& value)
{
    db::ObjectInPhotoType result;
    fromString(value, result, str::OBJECT_IN_PHOTO_TYPE_STRINGS);
    return result;
}


void toJson(json::ObjectBuilder& builder,
            const TasksGroupKit& tasksGroupKit,
            const TaskStatusesMap& tasksStatuses,
            const maps::Locale& locale)
{
    const auto& obj = tasksGroupKit.obj;
    std::locale stdLocale = i18n::bestLocale(locale);
    builder[str::ID] = std::to_string(obj.id());
    builder[str::PARAMS] =
        [&](json::ObjectBuilder builder){
            builder[str::NAME] = obj.name();
            builder[str::GEOMETRY] =
                geolib3::geojson(
                    geolib3::convertMercatorToGeodetic(obj.mercatorGeom())
                );
            builder[str::ROUTING] = static_cast<bool>(obj.useRouting());
            builder[str::FCS] = obj.fcs();
            builder[str::TOLL] = static_cast<bool>(obj.useToll());
            builder[str::RECOMMENDED_TASK_LENGTH] = obj.recommendedTaskLengthMeters();
            builder[str::CAMERA_DIRECTIONS] <<
                [&](json::ArrayBuilder builder) {
                    for(auto cameraDeviation : obj.cameraDeviations()) {
                        builder << toString(cameraDeviation);
                    }
                };

            if (obj.actualizedBefore()) {
                builder[str::ACTUALIZED_BEFORE] =
                        chrono::formatIsoDateTime(*obj.actualizedBefore());
            }

            builder[str::EXCLUDE_DEADENDS] = obj.excludeDeadends();

            if (obj.minEdgeCoverageRatio().has_value()) {
                builder[str::MIN_EDGE_COVERAGE_RATIO] = obj.minEdgeCoverageRatio().value();
            }

            builder[str::IGNORE_PRIVATE_AREA] = obj.ignorePrivateArea();
            if (obj.allowedAssigneesLogins().has_value()) {
                builder[str::ALLOWED_ASSIGNEES_LOGINS] = obj.allowedAssigneesLogins().value();
            }

            builder[str::EMAILS] << [&](json::ArrayBuilder builder) {
                for (const auto& item : tasksGroupKit.emails) {
                    REQUIRE(obj.id() == item.tasksGroupId(), "invalid tasksGroupId");
                    builder << item.email();
                }
            };
        };

    builder[str::STATUS] = tasks_planner::toString(obj.status());

    if (obj.hasCreatedBy()) {
        builder["createdBy"] = obj.createdBy();
    }

    builder["creationStats"] =
        [&](json::ObjectBuilder builder) {
            if(obj.totalLengthMeters()) {
                builder["totalLength"] =
                    toJsonLocalizedValue(
                        i18n::units::Distance(*obj.totalLengthMeters()),
                        stdLocale
                    );
            }

            if(obj.uniqueLengthMeters()) {
                builder["uniqueLength"] =
                    toJsonLocalizedValue(
                        i18n::units::Distance(*obj.uniqueLengthMeters()),
                        stdLocale
                    );
            }

            if(obj.graphCoverageRatio()) {
                builder["targetRoadGraphCoverageRatio"] = *obj.graphCoverageRatio();
            }
        };
    if(tasksStatuses.count(obj.id())) {
        builder["tasksStatusesStats"] <<
            [&](json::ArrayBuilder builder) {
                for(const auto& statusCount : tasksStatuses.at(obj.id())) {
                    builder << [&](json::ObjectBuilder builder) {
                        builder["status"] = tasks_planner::toString(statusCount.first);
                        builder["count"] = statusCount.second;
                    };
                }
            };
    }
}

boost::optional<geolib3::MultiPolygon2> readMultiPolygonFromGeojson(const json::Value& value)
{
    auto typeStr = value["type"].as<std::string>();
    if (typeStr == "MultiPolygon") {
        return geolib3::readGeojson<geolib3::MultiPolygon2>(value);
    } else if (typeStr == "Polygon") {
        auto polygon = geolib3::readGeojson<geolib3::Polygon2>(value);
        return geolib3::MultiPolygon2({polygon});
    }
    return boost::none;
}

//supports FeatureCollection, MultiPolygon, Polygon
geolib3::MultiPolygon2 parseGeometry(const json::Value& geometryJson) {
    auto typeStr = geometryJson["type"].as<std::string>();

    if (typeStr == "FeatureCollection") {
        std::vector<geolib3::Polygon2> polygons;
        for (auto featureJson : geometryJson["features"]) {
            auto multiPolygon = readMultiPolygonFromGeojson(featureJson["geometry"]);
            if (!multiPolygon) {
                continue;
            }
            for(size_t i = 0; i < multiPolygon->polygonsNumber(); ++i) {
                polygons.push_back(multiPolygon->polygonAt(i));
            }
        }
        return makeMultiPolygon(std::move(polygons));

    }
    auto geom = readMultiPolygonFromGeojson(geometryJson);
    REQUIRE(geom, "Can't read geoemtry");
    return *geom;
}

std::vector<db::CameraDeviation> parseCameraDeviations(const json::Value& value)
{
    std::vector<db::CameraDeviation> cameraDirections;
    cameraDirections.reserve(value.size());
    for(auto cameraDirectionStr : value) {
        cameraDirections.push_back(
            tasks_planner::fromString<db::CameraDeviation>(
                cameraDirectionStr.as<std::string>()
            )
        );
    }
    return cameraDirections;
}

static db::ugc::TasksGroupEmails parseTasksGroupEmails(const json::Value& value)
{
    auto result = db::ugc::TasksGroupEmails{};
    for (const auto& item : value) {
        result.push_back({{}, item.as<std::string>()});
    }
    return result;
}

TasksGroupKit createTasksGroup(const json::Value& value)
{
    auto result = TasksGroupKit{.obj = db::ugc::TasksGroup{
        db::ugc::TasksGroupStatus::Draft,
        value[str::NAME].as<std::string>(),
        geolib3::convertGeodeticToMercator(parseGeometry(value[str::GEOMETRY])),
        static_cast<db::ugc::UseRouting>(value[str::ROUTING].as<bool>()),
        value[str::FCS].as<std::vector<int>>(),
        static_cast<db::ugc::UseToll>(value[str::TOLL].as<bool>()),
        value[str::RECOMMENDED_TASK_LENGTH].as<uint32_t>()}};
    auto& tasksGroup = result.obj;

    auto actualizedBefore = value[str::ACTUALIZED_BEFORE];
    if (actualizedBefore.exists()) {
        tasksGroup.setActualizedBefore(
            chrono::parseIsoDateTime(actualizedBefore.as<std::string>())
        );
    }

    auto cameraDirectionsJson = value[str::CAMERA_DIRECTIONS];
    if (cameraDirectionsJson.exists()) {
        tasksGroup.setCameraDeviations(parseCameraDeviations(cameraDirectionsJson));
    }

    auto minEdgeCoverageRatio = value[str::MIN_EDGE_COVERAGE_RATIO];
    if (minEdgeCoverageRatio.exists()) {
        tasksGroup.setMinEdgeCoverageRatio(minEdgeCoverageRatio.as<float>());
    }

    auto excludeDeadEnds = value[str::EXCLUDE_DEADENDS];
    if (excludeDeadEnds.exists()) {
        tasksGroup.setExcludeDeadends(excludeDeadEnds.as<bool>());
    }

    auto ignorePrivateArea = value[str::IGNORE_PRIVATE_AREA];
    if (ignorePrivateArea.exists()) {
        tasksGroup.setIgnorePrivateArea(ignorePrivateArea.as<bool>());
    }

    auto allowedAssigneesLogins = value[str::ALLOWED_ASSIGNEES_LOGINS];
    if (allowedAssigneesLogins.exists()) {
        tasksGroup.setAllowedAssigneesLogins(allowedAssigneesLogins.as<std::vector<std::string>>());
    }

    auto emails = value[str::EMAILS];
    if (emails.exists()) {
        result.emails = parseTasksGroupEmails(emails);
    }

    return result;
}

void patchByJson(TasksGroupKit& tasksGroupKit, const json::Value& value)
{
    auto& obj = tasksGroupKit.obj;
    auto params = value[str::PARAMS];
    if (params.exists()) {
        const bool isInDraft = obj.status() == db::ugc::TasksGroupStatus::Draft;
        auto throwBadRequestIfNotEditable = [isInDraft](){
            if (!isInDraft) {
                throw yacare::errors::BadRequest()
                    << "Only objects in status '"
                    <<  tasks_planner::toString(db::ugc::TasksGroupStatus::Draft)
                    << "' can be modified";
            }
        };

        auto name = params[str::NAME];
        if (name.exists()) {
            auto newValue = name.as<std::string>();
            if (obj.name() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setName(newValue);
            }
        }

        auto geometry = params[str::GEOMETRY];
        if (geometry.exists()) {
            constexpr double GEOM_EPS = 1e-9;
            auto newValue = geolib3::convertGeodeticToMercator(parseGeometry(geometry));
            bool geometriesEqual = geolib3::test_tools::approximateEqual(
                    obj.mercatorGeom(), newValue, GEOM_EPS);
            if (!geometriesEqual) {
                throwBadRequestIfNotEditable();
                obj.setMercatorGeom(newValue);
            }
        }

        auto routing = params[str::ROUTING];
        if (routing.exists()) {
            auto newValue = static_cast<db::ugc::UseRouting>(routing.as<bool>());
            if (obj.useRouting() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setUseRouting(newValue);
            }
        }

        auto fcs = params[str::FCS];
        if (fcs.exists()) {
            auto newValue = fcs.as<std::vector<int>>();
            if (obj.fcs() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setFcs(newValue);
            }
        }

        auto toll = params[str::TOLL];
        if (toll.exists()) {
            auto newValue = static_cast<db::ugc::UseToll>(toll.as<bool>());
            if (obj.useToll() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setUseToll(newValue);
            }
        }

        auto recomendedTaskLength = params[str::RECOMMENDED_TASK_LENGTH];
        if (recomendedTaskLength.exists()) {
            auto newValue = recomendedTaskLength.as<uint32_t>();
            if (obj.recommendedTaskLengthMeters() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setRecommendedTaskLengthMeters(newValue);
            }
        }

        auto actualizedBefore = params[str::ACTUALIZED_BEFORE];
        if (actualizedBefore.exists()) {
            auto newValue = chrono::parseIsoDateTime(actualizedBefore.as<std::string>());
            if (obj.actualizedBefore() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setActualizedBefore(newValue);
            }

        }

        auto cameraDirectionsJson = value[str::CAMERA_DIRECTIONS];
        if (cameraDirectionsJson.exists()) {
            auto newValue = parseCameraDeviations(cameraDirectionsJson);
            if (obj.cameraDeviations() != newValue) {
                throwBadRequestIfNotEditable();
                obj.setCameraDeviations(newValue);
            }
        }


        /// assigneesLogins are alowed to change despite of status value
        auto allowedAssigneesLogins = params[str::ALLOWED_ASSIGNEES_LOGINS];
        if (allowedAssigneesLogins.exists()) {
            obj.setAllowedAssigneesLogins(allowedAssigneesLogins.as<std::vector<std::string>>());
        } else {
            obj.setAllowedAssigneesLogins(std::nullopt);
        }

        auto emails = params[str::EMAILS];
        if (emails.exists()) {
            tasksGroupKit.emails = parseTasksGroupEmails(emails);
            setTasksGroupId(obj.id(), tasksGroupKit.emails);
        }
    }

    auto status = value[str::STATUS];
    if (status.exists()) {
        obj.setStatus(
            tasks_planner::fromString<db::ugc::TasksGroupStatus>(status.as<std::string>())
        );
    }
}

const std::string& findBestTaskName(const db::ugc::TaskNamesMap& names, const Locale& locale)
{
    auto bestNameIt = findBestLocaleTransform(names.begin(), names.end(), locale,
        std::mem_fn(&db::ugc::TaskNamesMap::value_type::first));
    REQUIRE(bestNameIt != names.end(), "Can't find appropriate name");
    return bestNameIt->second;
}

void toJson(json::ObjectBuilder obj,
            const db::ugc::Task& task,
            const std::optional<db::ugc::Assignment>& assignment,
            const db::ugc::AssignmentReviews& reviews,
            const maps::Locale& locale)
{
    std::locale stdLocale = i18n::bestLocale(locale);
    obj["id"] = std::to_string(task.id());
    if (task.tasksGroupId()) {
        obj["tasksGroupId"] = std::to_string(*task.tasksGroupId());
    }

    obj["name"] = findBestTaskName(task.names(), locale);
    obj["status"] = tasks_planner::toString(task.status());
    obj["convexHull"] = geolib3::geojson(task.geodeticHull());
    obj["length"] =
        toJsonLocalizedValue(
            i18n::units::Distance(task.distanceInMeters()),
            stdLocale
        );
    obj[str::CAMERA_DIRECTIONS] <<
        [&](json::ArrayBuilder builder) {
            for(auto cameraDeviation : task.cameraDeviations()) {
                builder << toString(cameraDeviation);
            }
        };

    bool shouldShowAssignment = task.status() == db::ugc::TaskStatus::Acquired ||
            task.status() == db::ugc::TaskStatus::Done;
    if (shouldShowAssignment && assignment.has_value()) {
        obj["assignment"] = [&](json::ObjectBuilder builder) {
            toJson(builder, *assignment, reviews, locale);
        };
    }
}

void toJson(json::ObjectBuilder obj,
            const db::ugc::Assignment& asgnmt,
            const db::ugc::AssignmentReviews& reviews,
            const maps::Locale& locale)
{
    std::locale stdLocale = i18n::bestLocale(locale);
    obj["id"] = std::to_string(asgnmt.id());
    obj["taskId"] = std::to_string(asgnmt.taskId());
    obj["status"] = tasks_planner::toString(asgnmt.status());
    obj["assignee"] = asgnmt.assignedTo();
    obj["acquiredAt"] = chrono::formatIsoDateTime(asgnmt.acquiredAt());

    if (asgnmt.submittedAt()) {
        obj["resolvedAt"] = chrono::formatIsoDateTime(*asgnmt.submittedAt());
    }

    if (asgnmt.evaluatedAt()) {
        obj["closedAt"] = chrono::formatIsoDateTime(*asgnmt.evaluatedAt());
    }

    if (!reviews.empty()) {
        auto sortedReviews = reviews;
        std::sort(sortedReviews.begin(), sortedReviews.end(),
            [](const db::ugc::AssignmentReview& one,
               const db::ugc::AssignmentReview& other)
            {
                return one.cameraDeviation() < other.cameraDeviation();
            }
        );
        std::optional<db::ugc::AssignmentReview> frontAssignmentReview;
        obj["fulfillmentInfoByDirections"] <<
            [&](json::ArrayBuilder arrayBuilder) {
                for(const auto& review : sortedReviews) {
                    arrayBuilder << [&](json::ObjectBuilder builder){
                        builder["cameraDirection"] = toString(review.cameraDeviation());
                        builder["fulfillmentInfo"] =
                            [&](json::ObjectBuilder builder){
                                toJson(builder, review, locale);
                            };
                    };
                    if (review.cameraDeviation() == db::CameraDeviation::Front) {
                        frontAssignmentReview = review;
                    }
                }
            };
        if(frontAssignmentReview.has_value()) {
            obj["fulfillmentInfo"] = [&](json::ObjectBuilder builder){
                        toJson(builder, frontAssignmentReview.value(), locale);
                    };
        }
    }
}

void toJson(json::ObjectBuilder obj,
            const db::ugc::AssignmentReview& review,
            const maps::Locale& locale)
{
    std::locale stdLocale = i18n::bestLocale(locale);
    obj["coverageRatio"] = review.coverageFraction();
    ASSERT(review.goodCoverageFraction());
    obj["goodCoverageRatio"] = *review.goodCoverageFraction();
    ASSERT(review.trackDistanceInMeters());
    obj["distance"] =
        toJsonLocalizedValue(
            i18n::units::Distance(*review.trackDistanceInMeters()),
            stdLocale
        );
    ASSERT(review.trackDuration());
    obj["duration"] =
        toJsonLocalizedValue(
            i18n::units::Duration(review.trackDuration()->count()),
            stdLocale
        );
    if (review.actualizationDate()) {
        obj["updatedAt"] = chrono::formatIsoDateTime(*review.actualizationDate());
    } else {
        obj["updatedAt"] = chrono::formatIsoDateTime(chrono::TimePoint());
    }

    obj["photosNumber"] = review.processedPhotos().value_or(0);
    obj["goodPhotosNumber"] = review.goodPhotos().value_or(0);
}

void toJson(json::ObjectBuilder obj,
            const std::string& baseUrl,
            const db::Feature& feature)
{
    obj["id"] = std::to_string(feature.id());
    obj["sourceId"] = feature.sourceId();
    obj["createdAt"] = chrono::formatIsoDateTime(feature.timestamp());
    if (feature.hasPos()) {
        obj["position"] = asGeojsonPosition(feature.geodeticPos());
    }

    if (feature.hasHeading()) {
        obj["heading"] = feature.heading().value();
    }

    if (feature.hasQuality()) {
        obj["autoQuality"] = feature.quality();
    } else {
        obj["autoQuality"] = 0.;
    }

    obj["isPublished"] = feature.isPublished() || feature.shouldBePublished();
    obj["image"] = [&](json::ObjectBuilder obj) {
        obj["url"] = makeFeatureImageUrl(baseUrl, feature.id());
        auto imageSize = transformByImageOrientation(feature.size(),
                                                     feature.orientation());
        obj["width"] = imageSize.width;
        obj["height"] = imageSize.height;
    };
    obj["thumbnailImage"] = [&](json::ObjectBuilder obj) {
        obj["url"] = makeFeatureThumbnailUrl(baseUrl, feature.id());
        auto imageSize = common::getThumbnailSize(
            transformByImageOrientation(feature.size(), feature.orientation()));
        obj["width"] = imageSize.width;
        obj["height"] = imageSize.height;
    };
    obj["cameraDirection"] = tasks_planner::toString(feature.cameraDeviation());
    obj["privacy"] = tasks_planner::toString(feature.privacy());
}

void toJson(json::ObjectBuilder obj, const db::ugc::AssignmentObject& aobj)
{
    obj["id"] = std::to_string(aobj.objectId());
    obj["type"] = std::string(toString(aobj.objectType(), str::ASSIGNMENT_OBJECT_TYPE_STRINGS));
    obj["createdAt"] = chrono::formatIsoDateTime(aobj.created());
    obj["position"] = asGeojsonPosition(aobj.geodeticPos());
    if (aobj.comment()) {
        obj["comment"] = *aobj.comment();
    }
}

void toJson(json::ObjectBuilder builder, const db::Feature& photo, const db::ObjectInPhoto& obj)
{
    builder["id"] = std::to_string(obj.id());
    builder["type"] = std::string(toString(obj.type(), str::OBJECT_IN_PHOTO_TYPE_STRINGS));
    auto imageBox = common::transformByImageOrientation(obj.imageBox(), photo.size(), photo.orientation());
    builder["bbox"] = [&](json::ArrayBuilder array) {
        array << imageBox.minX() << imageBox.minY() << imageBox.maxX() << imageBox.maxY();
    };
}

db::ObjectInPhoto createObjectInPhoto(const db::Feature& photo, const json::Value& value)
{
    auto bboxValues = value["bbox"];
    auto imageBox = common::ImageBox(bboxValues[0].as<int32_t>(),
                                     bboxValues[1].as<int32_t>(),
                                     bboxValues[2].as<int32_t>(),
                                     bboxValues[3].as<int32_t>());

    if (photo.hasOrientation()) {
        imageBox = common::revertByImageOrientation(std::move(imageBox), photo.size(), photo.orientation());
    }
    return db::ObjectInPhoto(
        photo.id(),
        tasks_planner::fromString<db::ObjectInPhotoType>(value["type"].as<std::string>()),
        imageBox,
        1. // confidence
    );
}

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