#pragma once

#include "common.h"
#include "dataset.h"
#include "txn_id.h"

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/sql_chemistry/include/gateway_access.h>
#include <maps/libs/enum_io/include/enum_io_fwd.h>

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

namespace maps::mrc::db {

enum class WalkFeedbackType {
    None,
    Barrier,
    Other, // user didn't select feedback type explicitly
    BuildingEntrance,
    AddressPlate,
    EntrancePlate,
    BusinessSign,
    BusinessWorkingHours,
    FootPath,
    CyclePath,
    Stairs,
    Ramp,
    Room,
    Wall,
    Organization,
    Fence,
    Building,
    Parking
};

DECLARE_ENUM_IO(WalkFeedbackType);

enum class ObjectStatus {
    Pending,
    Discarded,
    WaitPublishing,
    Published,
    Failed,
    Delayed,
};

DECLARE_ENUM_IO(ObjectStatus);

enum class ObjectActionType { Add, Remove };

DECLARE_ENUM_IO(ObjectActionType);

using GeometryVariant
    = std::variant<geolib3::Point2, geolib3::Polyline2, geolib3::Polygon2>;

class WalkObject {
public:
    WalkObject(
        Dataset dataset,
        std::string deviceId,
        chrono::TimePoint createdAt,
        WalkFeedbackType feedbackType,
        const GeometryVariant& geodeticGeometry,
        std::string comment = {})
    {
        setDataset(dataset);
        setDeviceId(std::move(deviceId));
        setCreatedAt(createdAt);
        setFeedbackType(feedbackType);
        setGeodeticGeometry(geodeticGeometry);
        setComment(std::move(comment));
    }

    TId id() const { return id_; }

    const std::string& deviceId() const { return deviceId_; }

    chrono::TimePoint createdAt() const { return createdAt_; }

    bool hasPublishedAt() const { return publishedAt_ != std::nullopt; }

    chrono::TimePoint publishedAt() const {
        REQUIRE(hasPublishedAt(), "Should have published at");
        return *publishedAt_;
    }

    WalkFeedbackType feedbackType() const { return feedbackType_; }

    const std::string& comment() const { return comment_; }

    bool hasFeedbackTaskId() const { return feedbackTaskId_ != std::nullopt; }

    TId feedbackTaskId() const {
        REQUIRE(hasFeedbackTaskId(), "Should have feedback task id");
        return *feedbackTaskId_;
    }

    ObjectStatus status() const { return status_; }

    Dataset dataset() const {
        return dataset_;
    }

    bool hasUserId() const { return userId_ != std::nullopt; }

    const std::string& userId() const {
        REQUIRE(hasUserId(), "Should have user id");
        return *userId_;
    }

    bool hasIndoorLevelId() const { return indoorLevelId_ != std::nullopt; }

    const std::string& indoorLevelId() const {
        REQUIRE(hasIndoorLevelId(), "Should have indoor level id");
        return *indoorLevelId_;
    }

    bool hasActionType() const { return actionType_ != std::nullopt; }

    ObjectActionType actionType() const {
        REQUIRE(hasActionType(), "Should have action type");
        return *actionType_;
    }

    geolib3::GeometryType geometryType() const {
        return geolib3::WKB::getGeometryType(wkbGeometry_);
    }

    GeometryVariant mercatorGeometry() const {
        using namespace geolib3;

        const auto type = geometryType();
        const auto validate = Validate::Yes;
        switch (type) {
            case GeometryType::Point:
                return WKB::read<Point2>(wkbGeometry_, validate);
            case GeometryType::LineString:
                return WKB::read<Polyline2>(wkbGeometry_, validate);
            case GeometryType::Polygon:
                return WKB::read<Polygon2>(wkbGeometry_, validate);
            default:
                REQUIRE(false, "Unsupported geometry type " << type);
        }
    }

    GeometryVariant geodeticGeometry() const {
        return std::visit(
            [](const auto& val) -> GeometryVariant {
                return geolib3::convertMercatorToGeodetic(val);
            },
            mercatorGeometry());
    }

    const std::optional<std::string>& taskId() const { return taskId_; }

    const std::optional<std::string>& nmapsObjectId() const
    {
        return nmapsObjectId_;
    }

    TId txnId() const { return txnId_; }

    WalkObject& setDeviceId(std::string id) {
        deviceId_ = std::move(id);
        return *this;
    }

    WalkObject& setCreatedAt(chrono::TimePoint timePoint) {
        createdAt_ = timePoint;
        return *this;
    }

    WalkObject& setPublishedAt(chrono::TimePoint timePoint) {
        publishedAt_ = timePoint;
        return *this;
    }

    WalkObject& setFeedbackType(WalkFeedbackType feedbackType) {
        feedbackType_ = feedbackType;
        return *this;
    }

    WalkObject& setComment(std::string comment) {
        comment_ = std::move(comment);
        return *this;
    }

    WalkObject& setFeedbackId(std::optional<TId> feedbackTaskId) {
        feedbackTaskId_ = feedbackTaskId;
        return *this;
    }

    WalkObject& setObjectStatus(ObjectStatus status) {
        status_ = status;
        return *this;
    }

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

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

    WalkObject& setIndoorLevelId(std::string indoorLevelId) {
        indoorLevelId_ = std::move(indoorLevelId);
        return *this;
    }

    WalkObject& setActionType(ObjectActionType actionType) {
        actionType_ = actionType;
        return *this;
    }

    WalkObject& setMercatorGeometry(const GeometryVariant& geometryVariant) {
        std::visit(
            [this](const auto& val){ setMercatorGeometryImpl(val); },
            geometryVariant);
        return *this;
    }

    WalkObject& setGeodeticGeometry(const GeometryVariant& geometryVariant) {
        std::visit(
            [this](const auto& val){ setGeodeticGeometryImpl(val); },
            geometryVariant);
        return *this;
    }

    WalkObject& setTaskId(std::string taskId) {
        taskId_ = std::move(taskId);
        return *this;
    }

    WalkObject& setNmapsObjectId(std::string nmapsObjectId) {
        nmapsObjectId_ = std::move(nmapsObjectId);
        return *this;
    }

private:
    friend class sql_chemistry::GatewayAccess<WalkObject>;
    friend class TxnIdAccess<WalkObject>;

    WalkObject() = default;

    WalkObject& setTxnId(TId txnId)
    {
        txnId_ = txnId;
        return *this;
    }

    template <typename Geometry>
    WalkObject& setMercatorGeometryImpl(const Geometry& geometry) {
        std::stringstream stream;
        geolib3::WKB::write(geometry, stream);
        wkbGeometry_ = stream.str();
        return *this;
    }

    template <typename Geometry>
    WalkObject& setGeodeticGeometryImpl(const Geometry& geometry) {
        setMercatorGeometryImpl(geolib3::convertGeodeticToMercator(geometry));
        return *this;
    }

    template <typename T>
    static auto introspect(T & t)
    {
        return std::tie(t.id_,
                        t.deviceId_,
                        t.createdAt_,
                        t.publishedAt_,
                        t.feedbackType_,
                        t.comment_,
                        t.feedbackTaskId_,
                        t.status_,
                        t.dataset_,
                        t.userId_,
                        t.wkbGeometry_,
                        t.indoorLevelId_,
                        t.actionType_,
                        t.taskId_,
                        t.nmapsObjectId_,
                        t.txnId_);
    }

    TId id_{0};
    std::string deviceId_{};
    chrono::TimePoint createdAt_{};
    std::optional<chrono::TimePoint> publishedAt_{};
    WalkFeedbackType feedbackType_ = WalkFeedbackType::None;
    std::string comment_{};
    std::optional<TId> feedbackTaskId_{};
    ObjectStatus status_ = ObjectStatus::Pending;
    Dataset dataset_ = Dataset::Walks;
    std::optional<std::string> userId_{};
    std::string wkbGeometry_{};
    std::optional<std::string> indoorLevelId_{};
    std::optional<ObjectActionType> actionType_{};
    std::optional<std::string> taskId_{};
    std::optional<std::string> nmapsObjectId_{};
    TId txnId_;

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

    /// For unit-test only.
    explicit WalkObject(TId id) : id_(id) {}
};

using WalkObjects = std::vector<WalkObject>;

/// @see https://st.yandex-team.ru/MAPSMRC-3868
inline bool isGroupHypothesisGeneration(const WalkObject& obj)
{
    return obj.taskId().has_value() &&
           obj.geometryType() != geolib3::GeometryType::Point &&
           obj.feedbackType() == db::WalkFeedbackType::Wall;
}

} // namespace maps::mrc::db
