#pragma once

#include "common.h"
#include "dataset.h"
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/img/include/common.h>
#include <maps/libs/mds-client/include/yandex/maps/mds/mds.h>
#include <maps/libs/sql_chemistry/include/gateway_access.h>

#include <maps/libs/geolib/include/heading.h>
#include <maps/libs/geolib/include/point.h>


#include <optional>
#include <string>
#include <vector>

namespace maps::mrc::db {

namespace feature {
const std::string YANDEX_DRIVE_UID = "0";
const std::string NO_SOURCE_ID = "";


} // namespace feature



class Feature {
public:
    Feature(
        std::string sourceId,
        geolib3::Point2 geodeticPos,
        geolib3::Heading heading,
        const std::string& timestamp,
        mds::Key mdsKey,
        Dataset dataset
    );

    // Note! This constructor should only be used to reconstruct a feature
    // from a serialized representation. Do not use it when working with DB.
    explicit Feature(TId id) : id_(id) {}

    TId id() const { return id_; }

    bool hasPos() const { return mercatorPos_ != std::nullopt; }

    geolib3::Point2 mercatorPos() const {
        REQUIRE(hasPos(), "Position should not be empty");
        return *mercatorPos_;
    }

    geolib3::Point2 geodeticPos() const {
        return geolib3::convertMercatorToGeodetic(mercatorPos());
    }

    bool hasOdometerPos() const { return odometerMercatorPos_ != std::nullopt; }

    // this position is not correct, but a shift between neighboring
    // features is very precise.
    geolib3::Point2 odometerMercatorPos() const {
        REQUIRE(hasPos(), "Odometer position should not be empty");
        return *odometerMercatorPos_;
    }

    bool hasHeading() const { return heading_ != std::nullopt; }

    geolib3::Heading heading() const {
        REQUIRE(hasHeading(), "Heading should not be empty");
        return geolib3::Heading{*heading_};
    }

    bool hasCameraRodrigues() const { return cameraRodrigues_ != std::nullopt; }

    // Phone orientation in rodrigues rotation. Uses this coordinate
    // system: {x -> East, y -> North, z -> Up}.
    // Initial phone orientation: phone is in portrait mode, camera is directed to north,
    std::vector<double> cameraRodrigues() const {
        REQUIRE(hasCameraRodrigues(), "camera rodrigues should not be empty");
        return *cameraRodrigues_;
    }

    mds::Key mdsKey() const { return {mdsGroupId_, mdsPath_}; }
    const std::string& mdsGroupId() const { return mdsGroupId_; }
    const std::string& mdsPath() const { return mdsPath_; }

    chrono::TimePoint timestamp() const { return date_; }

    Dataset dataset() const { return dataset_; }

    const std::string& sourceId() const { return sourceId_; }

    bool hasSize() const {
        REQUIRE((width_ && height_) || (!width_ && !height_),
            "Width and height should be both empty or not empty.");
        return width_ && *width_ > 0 && *height_ > 0;
    }

    common::Size size() const {
        REQUIRE(hasSize(), "Width and height should not be empty");
        return {*width_, *height_};
    }

    bool hasQuality() const { return quality_ != std::nullopt; }

    double quality() const {
        REQUIRE(hasQuality(), "Quality should not be empty");
        return *quality_;
    }

    bool hasRoadProbability() const { return roadProbability_ != std::nullopt; }

    double roadProbability() const {
        REQUIRE(hasRoadProbability(), "Road Probability should not be empty");
        return *roadProbability_;
    }

    bool hasForbiddenProbability() const { return forbiddenProbability_ != std::nullopt; }

    double forbiddenProbability() const {
        REQUIRE(hasForbiddenProbability(), "Forbidden Probability should not be empty");
        return *forbiddenProbability_;
    }

    bool isPublished() const { return isPublished_; }

    bool hasShouldBePublished() const { return shouldBePublished_.has_value(); }

    bool shouldBePublished() const { return shouldBePublished_.value_or(false); }

    bool hasOrientation() const { return orientation_ != std::nullopt; }

    // Return default value if orientation is missing!
    common::ImageOrientation orientation() const {
        return hasOrientation()
            ? common::ImageOrientation::fromExif(*orientation_)
            : common::ImageOrientation();
    }

    bool hasCameraDeviation() const { return cameraDeviation_.has_value(); }

    CameraDeviation cameraDeviation() const
    {
        return static_cast<CameraDeviation>(cameraDeviation_.value_or(0));
    }

    bool hasPrivacy() const { return privacy_.has_value(); }

    FeaturePrivacy privacy() const
    {
        return privacy_.value_or(FeaturePrivacy::Public);
    }

    bool hasGraph() const { return graph_.has_value(); }

    GraphType graph() const
    {
        return graph_.value_or(GraphType::Road);
    }

    bool hasUploadedAt() const { return uploadedAt_.has_value(); }

    chrono::TimePoint uploadedAt() const { return uploadedAt_.value_or(date_); }

    std::optional<bool> moderatorsShouldBePublished() const
    {
        return moderatorsShouldBePublished_;
    }

    const std::optional<TId>& assignmentId() const { return assignmentId_; }

    const std::optional<std::string>& userId() const { return userId_; }

    const std::optional<bool>& gdprDeleted() const { return gdprDeleted_; }

    const std::optional<bool>& showAuthorship() const { return showAuthorship_; }

    const std::optional<bool>& deletedByUser() const { return deletedByUser_; }

    const std::optional<TId>& walkObjectId() const { return walkObjectId_; }

    const std::optional<bool>& automaticShouldBePublished() const
    {
        return automaticShouldBePublished_;
    }

    const std::optional<chrono::TimePoint>& processedAt() const
    {
        return processedAt_;
    }

    const std::optional<std::string>& clientRideId() const
    {
        return clientRideId_;
    }

    const std::optional<bool>& deletedFromMds() const { return deletedFromMds_; }

    Feature& setGeodeticPos(geolib3::Point2 pos) {
        return setMercatorPos(geolib3::convertGeodeticToMercator(pos));
    }

    Feature& setMercatorPos(geolib3::Point2 pos) {
        mercatorPos_ = pos;
        return *this;
    }

    Feature& resetPos() {
        mercatorPos_ = std::nullopt;
        updatePublished();
        return *this;
    }

    // this position is not correct, but a shift between neighboring
    // features is very precise.
    Feature& setOdometerMercatorPos(geolib3::Point2 pos) {
        odometerMercatorPos_ = pos;
        return *this;
    }

    Feature& setHeading(geolib3::Heading heading) {
        heading_ = geolib3::normalize(heading).value();
        return *this;
    }

    Feature& resetHeading() {
        heading_ = std::nullopt;
        updatePublished();
        return *this;
    }

    // Phone orientation in rodrigues rotation. Uses this coordinate
    // system: {x -> East, y -> North, z -> Up}.
    // Initial phone orientation: phone is in portrait mode, camera is directed to north,
    Feature& setCameraRodrigues(std::vector<double> rodrigues) {
        REQUIRE(rodrigues.size() == 3, "number of Rodrigues values should be 3");
        cameraRodrigues_ = rodrigues;
        return *this;
    }

    Feature& setMdsKey(mds::Key key) {
        mdsGroupId_ = std::move(key.groupId);
        mdsPath_ = std::move(key.path);
        return *this;
    }

    Feature& setTimestamp(const std::string& sqlDateTime) {
        date_ = chrono::parseSqlDateTime(sqlDateTime);
        return *this;
    }

    Feature& setTimestamp(chrono::TimePoint timePoint) {
        date_ = timePoint;
        return *this;
    }

    Feature& setDataset(Dataset dataset) {
        dataset_ = dataset;
        return *this;
    }

    Feature& setSourceId(std::string id) {
        sourceId_ = std::move(id);
        return *this;
    }

    Feature& setSize(size_t width, size_t height) {
        width_ = width;
        height_ = height;
        return *this;
    }

    Feature& setSize(common::Size size) {
        return setSize(size.width, size.height);
    }

    Feature& setQuality(double quality) {
        quality_ = quality;
        return *this;
    }

    Feature& setRoadProbability(double roadProbability) {
        roadProbability_ = roadProbability;
        return *this;
    }

    Feature& setForbiddenProbability(double forbiddenProbability) {
        forbiddenProbability_ = forbiddenProbability;
        return *this;
    }

    Feature& setIsPublished(bool published) {
        updatePublished();
        REQUIRE(published <= shouldBePublished_.value_or(false),
                "it can't be published");
        isPublished_ = published;
        return *this;
    }

    Feature& setOrientation(const common::ImageOrientation& orientation) {
        orientation_ = static_cast<int>(orientation);
        return *this;
    }

    Feature& setCameraDeviation(CameraDeviation cameraDeviation)
    {
        cameraDeviation_ = toIntegral(cameraDeviation);
        return *this;
    }

    Feature& setPrivacy(FeaturePrivacy privacy)
    {
        privacy_ = privacy;
        return *this;
    }

    Feature& setGraph(GraphType graph)
    {
        graph_ = graph;
        return *this;
    }

    Feature& resetGraph() {
        graph_ = std::nullopt;
        return *this;
    }

    Feature& setUploadedAt(chrono::TimePoint uploadedAt)
    {
        uploadedAt_ = uploadedAt;
        return *this;
    }

    Feature& resetUploadedAt() {
        uploadedAt_ = std::nullopt;
        return *this;
    }

    Feature& setModeratorsShouldBePublished(bool moderatorsShouldBePublished)
    {
        moderatorsShouldBePublished_ = moderatorsShouldBePublished;
        updatePublished();
        return *this;
    }

    Feature& setAssignmentId(TId assignmentId) {
        assignmentId_ = assignmentId;
        return *this;
    }

    Feature& setUserId(std::string userId) {
        userId_ = std::move(userId);
        return *this;
    }

    Feature& setGdprDeleted(bool gdprDeleted) {
        gdprDeleted_ = gdprDeleted;
        return *this;
    }

    Feature& setShowAuthorship(bool showAuthorship) {
        showAuthorship_ = showAuthorship;
        return *this;
    }

    Feature& setDeletedByUser(bool deletedByUser) {
        deletedByUser_ = deletedByUser;
        updatePublished();
        return *this;
    }

    Feature& setWalkObjectId(TId walkObjectId) {
        walkObjectId_ = walkObjectId;
        return *this;
    }

    Feature& setAutomaticShouldBePublished(bool automaticShouldBePublished) {
        automaticShouldBePublished_ = automaticShouldBePublished;
        updatePublished();
        return *this;
    }

    Feature& setProcessedAt(chrono::TimePoint processedAt)
    {
        processedAt_ = processedAt;
        return *this;
    }

    Feature& resetProcessedAt()
    {
        processedAt_ = std::nullopt;
        return *this;
    }

    Feature& setClientRideId(std::string clientRideId)
    {
        clientRideId_ = std::move(clientRideId);
        return *this;
    }

    Feature& resetClientRideId()
    {
        clientRideId_ = std::nullopt;
        return *this;
    }

    Feature& setDeletedFromMds(bool deletedFromMds) {
        deletedFromMds_ = deletedFromMds;
        updatePublished();
        return *this;
    }

private:
    friend class sql_chemistry::GatewayAccess<Feature>;

    Feature() = default;

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(t.id_,
                        t.sourceId_,
                        t.mercatorPos_,
                        t.heading_,
                        t.date_,
                        t.mdsGroupId_,
                        t.mdsPath_,
                        t.width_,
                        t.height_,
                        t.quality_,
                        t.roadProbability_,
                        t.forbiddenProbability_,
                        t.dataset_,
                        t.isPublished_,
                        t.shouldBePublished_,
                        t.orientation_,
                        t.cameraDeviation_,
                        t.privacy_,
                        t.odometerMercatorPos_,
                        t.cameraRodrigues_,
                        t.graph_,
                        t.uploadedAt_,
                        t.moderatorsShouldBePublished_,
                        t.assignmentId_,
                        t.userId_,
                        t.gdprDeleted_,
                        t.showAuthorship_,
                        t.deletedByUser_,
                        t.walkObjectId_,
                        t.automaticShouldBePublished_,
                        t.processedAt_,
                        t.clientRideId_,
                        t.deletedFromMds_,
                        t.xmin_);
    }

    TId id_{0};
    std::string sourceId_{};
    std::optional<geolib3::Point2> mercatorPos_{};
    std::optional<double> heading_{};
    chrono::TimePoint date_{};
    std::string mdsGroupId_{};
    std::string mdsPath_{};
    std::optional<size_t> width_{};
    std::optional<size_t> height_{};
    std::optional<double> quality_{};
    std::optional<double> roadProbability_{};
    std::optional<double> forbiddenProbability_{};
    Dataset dataset_ = Dataset::Agents;
    bool isPublished_{false};
    std::optional<bool> shouldBePublished_{};
    std::optional<int16_t> orientation_{};
    std::optional<int16_t> cameraDeviation_{};
    std::optional<FeaturePrivacy> privacy_{};
    std::optional<geolib3::Point2> odometerMercatorPos_{};
    std::optional<std::vector<double>> cameraRodrigues_{};
    std::optional<GraphType> graph_{};
    std::optional<chrono::TimePoint> uploadedAt_{};
    std::optional<bool> moderatorsShouldBePublished_{};
    std::optional<TId> assignmentId_{};
    std::optional<std::string> userId_{};
    std::optional<bool> gdprDeleted_{};
    std::optional<bool> showAuthorship_{};
    std::optional<bool> deletedByUser_{};
    std::optional<TId> walkObjectId_{};
    std::optional<bool> automaticShouldBePublished_{false};
    std::optional<chrono::TimePoint> processedAt_{};
    std::optional<std::string> clientRideId_{};
    std::optional<bool> deletedFromMds_{};
    uint64_t xmin_{};

    void updatePublished();

    bool isSynchronouslyPublishable() {
        return privacy_ == FeaturePrivacy::Secret;
    }

public:
    auto introspect() const { return introspect(*this); }
};


using Features = std::vector<Feature>;

class FeatureQaTask {
public:
    FeatureQaTask(TId featureId, TId tolokaId)
        : featureId_(featureId)
        , tolokaId_(tolokaId) {}

    TId featureId() const { return featureId_; }

    TId tolokaId() const { return tolokaId_; }

private:
    friend class sql_chemistry::GatewayAccess<FeatureQaTask>;
    FeatureQaTask() = default;

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(t.featureId_, t.tolokaId_);
    }

    TId featureId_{};
    TId tolokaId_{};

public:
    auto introspect() const { return introspect(*this); }
};

using FeatureQaTasks = std::vector<FeatureQaTask>;

} // namespace maps::mrc::db
