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

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

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc/gateway.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/chrono/include/time_point.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.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/libs/tile/include/geometry.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/infra/yacare/include/params/tile.h>

#include <util/string/split.h>

#include <unordered_map>

namespace maps {
namespace mrc {
namespace tasks_planner {

namespace {

auto toTracksJson(const db::ugc::AssignmentReview& review)
{
    return [&](json::ObjectBuilder obj) {
        const geolib3::PolylinesVector EMPTY_POLYLINES;
        obj["coveredTargets"] =
            geolib3::geojson(
                review.coveredGeodeticGeom().value_or(EMPTY_POLYLINES)
            );

        obj["uncoveredTargets"] =
            geolib3::geojson(
                review.uncoveredGeodeticGeom().value_or(EMPTY_POLYLINES)
            );

        obj["track"] =
            geolib3::geojson(
                review.trackGeodeticGeom().value_or(EMPTY_POLYLINES)
            );
    };

}

db::ugc::AssignmentReviews
loadAssignmentReviews(pqxx::transaction_base& txn, db::TId assignmenId)
{
    return db::ugc::AssignmentReviewGateway(txn)
        .load(db::ugc::table::AssignmentReview::assignmentId == assignmenId);
}

} // namespace

YCR_RESPOND_TO("GET /assignments/", locale = RU, results = 0, skip = 0)
{
    sql_chemistry::FiltersCollection filter{sql_chemistry::op::Logical::And};

    const std::string TASKS_GROUP_PARAM = "tasks_group_id";
    const std::string TASK_PARAM = "task_id";
    sql_chemistry::FiltersCollection allOf(sql_chemistry::op::Logical::And);
    if (request.input().has(TASKS_GROUP_PARAM)) {
        auto tasksGroupIds = vectorQueryParam<db::TId>(request, TASKS_GROUP_PARAM);
        allOf.add(db::ugc::table::Assignment::taskId == db::ugc::table::Task::id);
        allOf.add(db::ugc::table::Task::tasksGroupId.in(tasksGroupIds));
    } else if (request.input().has(TASK_PARAM)) {
        auto taskIds = vectorQueryParam<db::TId>(request, TASK_PARAM);
        allOf.add(db::ugc::table::Assignment::taskId.in(taskIds));
    }

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

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

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

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

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

    auto txn = Globals::pool().slaveTransaction();
    const auto objects = db::ugc::AssignmentGateway(*txn).load(allOf, order);
    auto assignmentsReviewMap = loadAssignmentsReviewsMap(*txn, ids(objects));
    const auto totalObjectsCount = db::ugc::AssignmentGateway(*txn).count(allOf);
    setTotalCountHeader(response, totalObjectsCount);

    response << [&](json::ArrayBuilder builder) {
        for (const auto& object : objects) {
            auto reviews = assignmentsReviewMap[object.id()];
            builder << [&](json::ObjectBuilder builder) {
                tasks_planner::toJson(builder, object, reviews, locale);
            };
        }
    };
}

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

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

    auto reviews = loadAssignmentReviews(*txn, object->id());
    response << YCR_JSON(obj) {
        tasks_planner::toJson(obj, *object, reviews, locale);
    };
}

YCR_RESPOND_TO("PATCH /assignments/$/", locale = RU)
{
    auto id = pathnameParam<int64_t>(0);
    auto newStatusStr = parseJsonFromRequestBodyElse400(request).as<std::string>();

    auto txn = Globals::pool().masterWriteableTransaction();
    db::ugc::AssignmentGateway gtw(*txn);

    auto object = gtw.tryLoadById(id);

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

    auto reviews = loadAssignmentReviews(*txn, object->id());

    auto newStatus = tryCallElse400(tasks_planner::fromString<db::ugc::AssignmentStatus>,
                                    newStatusStr);

    if (newStatus != object->status()) {

        if (object->status() != db::ugc::AssignmentStatus::Active) {
            throw yacare::errors::Forbidden() << "Can't modify object status";
        }

        if (newStatus == db::ugc::AssignmentStatus::Completed) {
            object->markAsCompleted();
        } else if (newStatus == db::ugc::AssignmentStatus::Revoked) {
            object->markAsRevoked();
            db::ugc::TaskGateway taskGtw(*txn);
            auto task = taskGtw.loadById(object->taskId());
            if (task.status() == db::ugc::TaskStatus::Acquired) {
                task.setStatus(db::ugc::TaskStatus::New);
                taskGtw.update(task);
            }
        } else {
            throw yacare::errors::Forbidden()
                << "Transition to status " << newStatusStr << " is forbidden";
        }

        gtw.update(*object);
        txn->commit();
    }

    response << YCR_JSON(obj) {
        tasks_planner::toJson(obj, *object, reviews, locale);
    };
}

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

    db::CameraDeviation cameraDeviation(db::CameraDeviation::Front);

    const std::string CAMERA_DIRECTION_PARAM = "camera_direction";
    if (request.input().has(CAMERA_DIRECTION_PARAM)) {
        cameraDeviation = fromString<db::CameraDeviation>(request.input()[CAMERA_DIRECTION_PARAM]);
    }

    auto txn = Globals::pool().slaveTransaction();
    const auto object = db::ugc::AssignmentGateway(*txn).tryLoadById(id);

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

    auto reviews = loadAssignmentReviews(*txn, object->id());

    auto review = db::ugc::AssignmentReviewGateway(*txn)
        .tryLoadOne(db::ugc::table::AssignmentReview::assignmentId == id &&
            db::ugc::table::AssignmentReview::cameraDeviation ==
                static_cast<std::underlying_type_t<db::CameraDeviation>>(cameraDeviation));

    if (!review) {
        review = db::ugc::AssignmentReview(id);
        review->setCameraDeviation(db::CameraDeviation::Front);
        db::ugc::TaskGateway gtw(*txn);
        auto task = gtw.loadById(object->taskId());
        gtw.loadTargets(task);
        review->setUncoveredGeodeticGeom(makeVisualizationPolylines(task.targets()));
    }

    response << toTracksJson(*review);
}


std::vector<std::underlying_type_t<db::CameraDeviation>>
parseCameraDeviations(const yacare::Request& request)
{
    std::vector<std::underlying_type_t<db::CameraDeviation>> cameraDeviations;

    const std::string CAMERA_DIRECTIONS_PARAM = "camera_directions";
    if (request.input().has(CAMERA_DIRECTIONS_PARAM)) {
        try {
            std::vector<std::string> values =
                StringSplitter(request.input()[CAMERA_DIRECTIONS_PARAM])
                .Split(',')
                .SkipEmpty();
            cameraDeviations.reserve(values.size());
            for(const auto& value : values) {
                cameraDeviations.push_back(
                    static_cast<std::underlying_type_t<db::CameraDeviation>>(
                        fromString<db::CameraDeviation>(value)
                    )
                );
            }
        } catch (const std::exception& ex) {
            WARN() << "Failed to parse '" << CAMERA_DIRECTIONS_PARAM << "' param value '"
                << request.input()[CAMERA_DIRECTIONS_PARAM] << "'";
            throw yacare::errors::BadRequest() << CAMERA_DIRECTIONS_PARAM
                << " param value is invalid";
        }
    }
    return cameraDeviations;
}


YCR_RESPOND_TO("GET /assignments/$/photos/", tile = tile::Tile(0, 0, 0),
    bbox = geolib3::BoundingBox(), results = 0, skip = 0)
{
    sql_chemistry::FiltersCollection sqlFilter{sql_chemistry::op::Logical::And};

    auto assignmentId = pathnameParam<db::TId>(0);
    auto cameraDeviations = parseCameraDeviations(request);

    sqlFilter.add(db::table::Feature::assignmentId == assignmentId);

    if (!cameraDeviations.empty()) {
        sqlFilter.add(db::table::Feature::cameraDeviation.in(cameraDeviations));
    }

    if (auto optDateBefore = parseOptionalDateParam(request, "taken_at_before")) {
        sqlFilter.add(db::table::Feature::date < optDateBefore.value());
    }

    if (auto optDateAfter = parseOptionalDateParam(request, "taken_at_after")) {
        sqlFilter.add(db::table::Feature::date >= optDateAfter.value());
    }

    if (has(tile)) {
        sqlFilter.add(
            db::table::Feature::pos.intersects(
                tile::mercatorBBox(tile)
            )
        );
    }

    if (has(bbox)) {
        sqlFilter.add(
            db::table::Feature::pos.intersects(
                geolib3::convertGeodeticToMercator(bbox)
            )
        );
    }

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

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

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

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

    auto txn = Globals::pool().slaveTransaction();

    const auto objects =
        db::FeatureGateway(*txn).load(sqlFilter, order);

    const auto totalObjectsCount =
        db::FeatureGateway(*txn).count(sqlFilter);
    setTotalCountHeader(response, totalObjectsCount);

    auto baseUrlStr = baseUrl(request);
    response << YCR_JSON_ARRAY(arr){
        for(const auto& object : objects) {
            arr << [&](json::ObjectBuilder builder) {
                toJson(builder, baseUrlStr, object);
            };
        }
    };
}

YCR_RESPOND_TO("GET /assignments/$/objects/")
{
    auto assignmentId = pathnameParam<db::TId>(0);

    const auto objects =
        db::ugc::AssignmentObjectGateway(*Globals::pool().slaveTransaction())
            .load(db::ugc::table::AssignmentObject::assignmentId == assignmentId);

    response << YCR_JSON_ARRAY(arr){
        for(const auto& object : objects) {
            arr << [&](json::ObjectBuilder builder) {
                toJson(builder, object);
            };
        }
    };
}


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