#pragma once

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/assignment.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/target.h>

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/impl/utility.h>

#include <maps/libs/sql_chemistry/include/gateway_access.h>
#include <maps/libs/enum_io/include/enum_io_fwd.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/deprecated/localeutils/include/locale.h>

#include <util/generic/array_ref.h>

#include <map>

namespace maps::mrc::db::ugc {

enum class TaskStatus {
    Draft,
    New,
    Acquired,
    Done,
};

DECLARE_ENUM_IO(TaskStatus);

enum class EstimatedPositionSource {
    /// Capture time of the photo is not in gps track range.
        Error,
    /// Photo position was calculated using the track matched to the map.
        Graph,
    /// Photo position was calculated using the source gps track,
    /// because it was not possible to match that part of the track to the map.
        Track,
    /// Photo position was estimated by client.
        Client,
};

DECLARE_ENUM_IO(EstimatedPositionSource);

class TaskName {
public:
    TaskName(TId taskId, const Locale& locale, std::string value)
        : taskId_(taskId)
        , locale_(boost::lexical_cast<std::string>(locale))
        , value_(std::move(value)) {}

    TId taskId() const { return taskId_; }

    Locale locale() const { return boost::lexical_cast<Locale>(locale_); }

    const std::string& value() const { return value_; }

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

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

    TId taskId_;
    std::string locale_;
    std::string value_;

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

using TaskNamesMap = std::map<Locale, std::string>;
using TaskNamePair = TaskNamesMap::value_type;

enum class LoadNames { Yes, No };

enum class LoadTargets { Yes, No };

class Task {
public:
    Task() = default;

    TId id() const { return id_; }

    std::optional<TId> tasksGroupId() const { return tasksGroupId_; }

    TaskStatus status() const { return status_; }

    std::chrono::seconds durationInSeconds() const { return std::chrono::seconds(duration_); }

    double distanceInMeters() const { return distanceInMeters_; }

    const geolib3::MultiPolygon2& geodeticHull() const { return geodeticHull_; }

    const Targets& targets() const {
        REQUIRE(taskIsNew() || targetsWereLoaded_,
            "Targets for task " << id_ << " have not been fetched from database");
        return targets_;
    }

    std::vector<CameraDeviation> cameraDeviations() const
    {
        if (cameraDeviations_.has_value()) {
            return castItemsTo<CameraDeviation>(cameraDeviations_.value());
        }
        else {
            return {CameraDeviation::Front};
        }
    }

    Task& setTasksGroupId(TId tasksGroupId) {
        tasksGroupId_ = tasksGroupId;
        return *this;
    }

    Task& setDistanceInMeters(double distanceInMeters) {
        distanceInMeters_ = distanceInMeters;
        return *this;
    }

    Task& setDuration(std::chrono::seconds duration) {
        duration_ = duration.count();
        return *this;
    }

    Task& setStatus(TaskStatus status) {
        status_ = status;
        return *this;
    }

    Task& setGeodeticHull(geolib3::MultiPolygon2 hull) {
        geodeticHull_ = std::move(hull);
        return *this;
    }

    Task&
    setCameraDeviations(const std::vector<CameraDeviation>& cameraDeviations)
    {
        cameraDeviations_ = castItemsTo<int16_t>(cameraDeviations);
        return *this;
    }

    Task& addTarget(
        geolib3::Polyline2 geodeticGeom,
        Oneway oneway,
        Traffic traffic,
        Direction direction,
        std::optional<uint32_t> forwardPos = {},
        std::optional<uint32_t> backwardPos = {}) {
        targets_.emplace_back(
            std::move(geodeticGeom), oneway, traffic, direction, forwardPos, backwardPos);
        ++newTargetsNumber_;
        return *this;
    }

    const TaskNamesMap& names() const {
        REQUIRE (taskIsNew() || namesWereLoaded_,
            "Names for task " << id_ << " have not been fetched from database");
        return names_;
    }

    /// Will throw if another name with the same locale already exists —
    /// either upon addName call or upon task saving
    Task& addName(const Locale& locale, std::string value) {
        auto insertResult = names_.emplace(locale, std::move(value));
        REQUIRE(insertResult.second,
            "Duplicated locale " << locale << " is not allowed for task names");
        newNames_.emplace_back(insertResult.first);
        return *this;
    }

    /// Will throw RuntimeError if task status is not New
    Assignment assignTo(UserId userId) {
        REQUIRE(!taskIsNew(), "Can't assign task if it is not stored in database");
        REQUIRE(status_ == TaskStatus::New, "Can't assign task which status is " << status_);
        status_ = TaskStatus::Acquired;
        return Assignment{id_, std::move(userId)};
    }

private:
    friend class sql_chemistry::GatewayAccess<Task>;
    friend class TaskGateway;

    template <typename T>
    static auto introspect(T& t) {
        return std::tie(t.id_, t.status_, t.geodeticHull_, t.duration_, t.distanceInMeters_,
            t.tasksGroupId_, t.xMin_, t.cameraDeviations_);
    }

    bool taskIsNew() const { return id_ == 0; }

    void loadTargets(Targets ts) {
        targets_ = std::move(ts);
        targetsWereLoaded_ = true;
    }

    void loadNames(TaskNamesMap ns) {
        names_ = std::move(ns);
        namesWereLoaded_ = true;
    }

    void checkCanUpdate() const {
        REQUIRE(targetsWereLoaded_ || newTargetsNumber_ == 0,
            "Targets should be loaded before update");
        REQUIRE(namesWereLoaded_ || newNames_.size() == 0,
            "Names should be loaded before update");
    }

    TArrayRef<Target> targetsToUpdate() {
        ASSERT(newTargetsNumber_ <= targets_.size());
        for (auto& target: targets_) {
            target.setTaskId(id_);
        }
        auto newTargetsNumber = newTargetsNumber_;
        newTargetsNumber_ = 0;
        targetsWereLoaded_ = true;
        return {targets_.end() - newTargetsNumber, targets_.end()};
    }

    std::vector<TaskName> namesToUpdate() {
        ASSERT(newNames_.size() <= names_.size());
        std::vector<TaskName> names;
        for (auto name: newNames_) {
            names.emplace_back(id_, name->first, name->second);
        }
        newNames_.clear();
        namesWereLoaded_ = true;
        return names;
    }

    // id will be equal to 0 for newly created tasks
    TId id_{0};
    TaskStatus status_{TaskStatus::Draft};
    geolib3::MultiPolygon2 geodeticHull_{};
    int64_t duration_{};
    double distanceInMeters_{};
    std::optional<TId> tasksGroupId_;
    std::optional<std::vector<int16_t>> cameraDeviations_;

    uint64_t xMin_{0};

    Targets targets_{};
    // Newly inserted targets are to be inserted at the end of targets vector
    size_t newTargetsNumber_{};
    TaskNamesMap names_{};
    std::vector<TaskNamesMap::iterator> newNames_{};

    bool targetsWereLoaded_{false};
    bool namesWereLoaded_{false};

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

using Tasks = std::vector<Task>;

} // namespace maps::mrc::db::ugc
