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

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

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/sql_chemistry/include/exceptions.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/i18n.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/std/vector.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/deprecated/localeutils/include/locale.h>
#include <maps/libs/deprecated/localeutils/include/localemapper.h>
#include <maps/infra/yacare/include/yacare.h>

#include <iterator>
#include <string>
#include <vector>
#include <unordered_map>

namespace maps {
namespace mrc {
namespace tasks_planner {


namespace {

bool assignmentRequested(const yacare::Request& request)
{
    const std::string& PARAM = "with_assignment";
    return request.input().has(PARAM) &&
        request.input()[PARAM] == "true";
}

std::pair<TaskAssignmentMap, AssignmentReviewsMap>
loadTasksAssignments(pqxx::transaction_base& txn,
                     const db::TIds& taskIds)
{
    TaskAssignmentMap  result;

    auto assignments = db::ugc::AssignmentGateway(txn)
        .load(db::ugc::table::Assignment::taskId.in(taskIds),
              sql_chemistry::orderBy(db::ugc::table::Assignment::id).desc());

    auto reviewsMap = loadAssignmentsReviewsMap(txn, ids(assignments));

    for (const auto& a : assignments) {
        result.insert(std::make_pair(a.taskId(), std::move(a)));
    }
    return std::make_pair(result, reviewsMap);
}

void changeActiveAssignmentStatuseTo(pqxx::transaction_base& txn,
                                     db::TId taskId,
                                     db::ugc::AssignmentStatus status)
{
    db::ugc::AssignmentGateway gtw(txn);
    auto assignments = gtw.load(db::ugc::table::Assignment::taskId == taskId &&
        db::ugc::table::Assignment::status == db::ugc::AssignmentStatus::Active);
    for (auto& assignment : assignments) {
        if (status == db::ugc::AssignmentStatus::Completed) {
            assignment.markAsCompleted();
        } else if (status == db::ugc::AssignmentStatus::Revoked) {
            assignment.markAsRevoked();
        }
    }
    gtw.update(assignments);
}


} // namespace


YCR_RESPOND_TO("GET /tasks/", locale = RU, results = 0, skip = 0)
{
    sql_chemistry::FiltersCollection allOf(sql_chemistry::op::Logical::And);
    const std::string TASKS_GROUP_PARAM = "tasks_group_id";
    if (request.input().has(TASKS_GROUP_PARAM)) {
        auto tasksGroupIds = vectorQueryParam<db::TId>(request, TASKS_GROUP_PARAM);;
        allOf.add(db::ugc::table::Task::tasksGroupId.in(tasksGroupIds));
    }

    const std::string STATUS_PARAM = "status";
    if (request.input().has(STATUS_PARAM)) {
        auto status = tryCallElse400(tasks_planner::fromString<db::ugc::TaskStatus>,
                                     request.input()[STATUS_PARAM]);
        allOf.add(db::ugc::table::Task::status == status);
    }

    auto order = sql_chemistry::orderBy(db::ugc::table::Task::id).asc();

    if (shouldReverseOrder(request)) {
        order.desc();
    }

    if (has(results)) {
        order.limit(results);
    }

    if (has(skip)) {
        order.offset(skip);
    }

    auto txn = Globals::pool().slaveTransaction();
    auto objects = db::ugc::TaskGateway{*txn}.load(allOf, order);
    const auto totalObjectsCount = db::ugc::TaskGateway(*txn).count(allOf);
    setTotalCountHeader(response, totalObjectsCount);

    db::ugc::TaskGateway{*txn}.loadNames(objects);

    std::pair<TaskAssignmentMap, AssignmentReviewsMap> taskAssignmentMap;
    if (assignmentRequested(request)) {
        taskAssignmentMap = loadTasksAssignments(*txn, ids(objects));
    }

    response << [&](json::ArrayBuilder builder) {
        for (const auto& object : objects) {
            auto assignment = taskAssignmentMap.first.count(object.id()) ?
                std::make_optional(taskAssignmentMap.first.at(object.id())) :
                std::nullopt;
            auto reviews = assignment.has_value()
                ? taskAssignmentMap.second[assignment->id()]
                : AssignmentReviewsMap::mapped_type{};
            builder << [&](json::ObjectBuilder builder) {
                toJson(builder, object, assignment, reviews, locale);
            };
        }
    };
}

YCR_RESPOND_TO("GET /tasks/$/", locale = RU)
{
    auto id = pathnameParam<int64_t>(0);
    auto txn = Globals::pool().slaveTransaction();
    auto object = db::ugc::TaskGateway{*txn}.tryLoadById(id);

    if (!object) {
        throw yacare::errors::NotFound();
    }

    db::ugc::TaskGateway{*txn}.loadNames(*object);

    std::pair<TaskAssignmentMap, AssignmentReviewsMap> taskAssignmentMap;
    if (assignmentRequested(request)) {
        taskAssignmentMap = loadTasksAssignments(*txn, {id});
    }

    response << YCR_JSON(obj) {
        auto assignment = taskAssignmentMap.first.count(object->id()) ?
                std::make_optional(taskAssignmentMap.first.at(object->id())) :
                std::nullopt;
        auto reviews = assignment.has_value()
                ? taskAssignmentMap.second[assignment->id()]
                : AssignmentReviewsMap::mapped_type{};
        toJson(obj, *object, assignment, reviews, locale);
    };
}


YCR_RESPOND_TO("PATCH /tasks/$/", locale = RU)
{
    auto id = pathnameParam<int64_t>(0);
    auto newStatus = tryCallElse400(
        [&]() {
            return fromString<db::ugc::TaskStatus>(
                json::Value::fromString(request.body())["status"].as<std::string>()
            );
        });


    auto txn = Globals::pool().masterWriteableTransaction();
    db::ugc::TaskGateway gtw{*txn};

    auto object = gtw.tryLoadById(id);

    if (!object) {
        throw yacare::errors::NotFound();
    }

    db::ugc::TaskGateway{*txn}.loadNames(*object);

    if (newStatus != object->status() && object->tasksGroupId()) {
        auto tasksGroup = db::ugc::TasksGroupGateway{*txn}.loadById(*object->tasksGroupId());
        if (tasksGroup.status() != db::ugc::TasksGroupStatus::InProgress) {
            throw yacare::errors::Forbidden() << "Can't change task status";
        }
    }

    if (newStatus == db::ugc::TaskStatus::New)
    {
        object->setStatus(newStatus);
        changeActiveAssignmentStatuseTo(*txn, object->id(), db::ugc::AssignmentStatus::Revoked);
    } else if (newStatus == db::ugc::TaskStatus::Done) {
        object->setStatus(newStatus);
        changeActiveAssignmentStatuseTo(*txn, object->id(), db::ugc::AssignmentStatus::Completed);
    } else {
        yacare::errors::Forbidden() << "Transition to state is not supported";
    }
    gtw.update(*object);
    txn->commit();

    response << YCR_JSON(obj) {
        toJson(obj, *object, std::nullopt, {}, locale);
    };
}

YCR_RESPOND_TO("GET /tasks/$/targets/")
{
    auto id = pathnameParam<int64_t>(0);

    auto txn = Globals::pool().slaveTransaction();
    db::ugc::TaskGateway gtw{*txn};
    auto object = gtw.tryLoadById(id);

    if (!object) {
        throw yacare::errors::NotFound();
    }

    auto& task = *object;
    gtw.loadTargets(task);

    geolib3::PolylinesVector visualizedTargets =
        makeVisualizationPolylines(task.targets());

    response << YCR_JSON(obj) {
        obj["track"] = geolib3::geojson(visualizedTargets);
    };
}

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