#include <maps/wikimap/mapspro/services/social/src/api/globals.h>
#include <maps/wikimap/mapspro/services/social/src/libs/yacare/bbox.h>
#include <maps/wikimap/mapspro/services/social/src/libs/yacare/helpers.h>
#include <maps/wikimap/mapspro/services/social/src/libs/common/common.h>
#include <yandex/maps/wiki/social/involvement.h>
#include <yandex/maps/wiki/social/involvement_filter.h>
#include <yandex/maps/wiki/social/involvement_stat.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>

#include <yandex/maps/wiki/social/involvement.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/json/include/std/set.h>
#include <maps/libs/json/include/std/vector.h>
#include <maps/libs/json/include/std/set.h>

#include <functional>
#include <unordered_map>

namespace srv = maps::wiki::socialsrv;
namespace social = maps::wiki::social;

namespace maps::wiki::social {

namespace {
const std::string UNITS_OBJECTS = "objects";
const std::string UNITS_KILOMETERS = "kilometers";
const std::string UNITS_METERS = "meters";

const std::set<std::string> TYPES_UNITS_KILOMETERS {
    involvement_name::UNPAVED_ROADS,
    involvement_name::SPEED_LIMITS,
    involvement_name::PARKING_LOTS_LINEAR,
    involvement_name::MRC_FEEDBACK_MIRRORS
};

const std::set<std::string> TYPES_UNITS_METERS {
    involvement_name::CREATED_FENCES_LENGTH
};


const std::string TOKEN = "token";

} //anonymous namespace

void json(const social::Involvement& involvement, json::ObjectBuilder obj)
{
    obj["id"] = involvement.id();
    obj["title"] = involvement.title();
    obj["url"] = involvement.url();
    obj["enabled"] = involvement.enabled();
    obj["types"] = involvement.types();
    obj["start"] = chrono::formatIsoDateTime(involvement.start());
    if (involvement.finish()) {
        obj["finish"] = chrono::formatIsoDateTime(involvement.finish().value());
    }
    if (involvement.polygons().polygonsNumber()) {
        obj["polygons"] = geolib3::geojson(
            geolib3::convertMercatorToGeodetic(involvement.polygons())
        );
    }
}

void json(const social::InvolvementStat& counter, json::ObjectBuilder obj)
{
    const auto& type = counter.type();
    obj["type"] = type;

    if (TYPES_UNITS_KILOMETERS.count(type)) {
        // counter values are stored in meters,
        // while client expects them to be returned in kilometers
        //
        obj["value"] = (counter.value() / 1000);
        obj["units"] = UNITS_KILOMETERS;
    } else if (TYPES_UNITS_METERS.count(type)) {
        obj["value"] = counter.value();
        obj["units"] = UNITS_METERS;
    } else {
        obj["value"] = counter.value();
        obj["units"] = UNITS_OBJECTS;
    }
}

} //namespace maps::wiki::social

namespace maps::wiki::socialsrv {

namespace {

social::Involvement involvementFromJson(const json::Value& value)
{
    REQUIRE(
        value.isObject(),
        yacare::errors::BadRequest()
            << "json object expected as involvement representation"
    );

    social::TId id = value["id"].as<social::TId>(0);
    std::string title = value["title"].as<std::string>();
    std::string url = value["url"].as<std::string>();
    social::Enabled enabled = value["enabled"].as<bool>() ?
        social::Enabled::Yes :
        social::Enabled::No;
    auto types = value["types"].as<std::set<std::string>>();
    auto start = chrono::parseIsoDateTime(value["start"].as<std::string>());

    social::Involvement inv(
        id,
        std::move(title),
        std::move(url),
        std::move(types),
        enabled,
        start
    );

    if (value.hasField("finish")) {
        inv.setFinish(chrono::parseIsoDateTime(value["finish"].as<std::string>()));
    }

    if (value.hasField("polygons")) {
        inv.setPolygons(
            geolib3::convertGeodeticToMercator(
                geolib3::readGeojson<geolib3::MultiPolygon2>(value["polygons"])
            )
        );
    }

    return inv;
}

yacare::ThreadPool involvementsThreadPool("involvementsThreadPool", 2, 2048);

} //anonymous namespace

YCR_USE(involvementsThreadPool) {

YCR_RESPOND_TO("GET /involvements", uid = 0, token = "")
{
    social::InvolvementFilter filter;

    auto enabledFilter = socialsrv::optionalQueryParam<bool>(request, "enabled");
    auto activeFilter = socialsrv::optionalQueryParam<bool>(request, "active");
    auto startBeforeFilter = socialsrv::optionalQueryParam<std::string>(request, "start-before");
    auto finishAfterFilter = socialsrv::optionalQueryParam<std::string>(request, "finish-after");

    if (enabledFilter) {
        filter.enabled(enabledFilter.value()
            ? social::Enabled::Yes
            : social::Enabled::No);
    }

    if (startBeforeFilter) {
        auto startBefore = chrono::parseIsoDateTime(startBeforeFilter.value());
        filter.startedBefore(startBefore);
    }

    if (finishAfterFilter) {
        auto finishAfter = chrono::parseIsoDateTime(finishAfterFilter.value());
        filter.finishedAfter(finishAfter);
    }

    if (activeFilter) {
        filter.active(activeFilter.value()
            ? social::Active::Yes
            : social::Active::No);
    }

    if (request.input().has("bbox")) {
        auto bbox = bboxGeoFromRequest(request.input()["bbox"]);
        REQUIRE(
            !bbox.isDegenerate(),
            yacare::errors::BadRequest() << "Invalid bbox"
        );
        filter.boundedBy(geolib3::convertGeodeticToMercator(bbox));
    }

    auto socialTxnHandle = srv::Globals::dbPools().socialReadTxn(token);
    auto& socialTxn = socialTxnHandle.get();
    auto involvements = social::Involvement::byFilter(socialTxn, filter);

    response << YCR_JSON_ARRAY(array)
    {
        for (const auto& involvement: involvements) {
            array << involvement;
        }
    };
}

YCR_RESPOND_TO("POST /involvements", uid)
{
    //TODO: check uid permissions via libacl here
    checkUid(uid);

    auto writeContext = srv::Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();

    auto newInvolvement = involvementFromJson(json::Value::fromString(request.body()));
    newInvolvement.writeToDatabase(socialTxn);
    auto token = writeContext.commit();

    response << YCR_JSON(obj)
    {
        social::json(newInvolvement, obj);
        obj[social::TOKEN] = token;
    };
}

YCR_RESPOND_TO("POST /involvements/$", uid)
{
    //TODO: check uid permissions via libacl here
    checkUid(uid);

    social::TId involvementId = positionalParam<social::TId>(argv, 0);

    auto writeContext = srv::Globals::dbPools().writeContext();
    auto& socialTxn = writeContext.socialTxn();

    auto existingInvolvement = involvementFromJson(json::Value::fromString(request.body()));
    REQUIRE(
        involvementId == existingInvolvement.id(),
        yacare::errors::BadRequest() << "Involvement id mismatch"
    );

    existingInvolvement.writeToDatabase(socialTxn);
    auto token = writeContext.commit();
    response << YCR_JSON(obj)
    {
        social::json(existingInvolvement, obj);
        obj[social::TOKEN] = token;
    };
}

YCR_RESPOND_TO("GET /involvements/$/stats", uid = 0, token = "")
{
    social::TId involvementId = positionalParam<social::TId>(argv, 0);
    auto socialTxnHandle = srv::Globals::dbPools().socialReadTxn(token);
    auto& socialTxn = socialTxnHandle.get();

    auto involvement = social::Involvement::byId(socialTxn, involvementId);
    auto involvementStatMap = social::loadInvolvementStatMap(
        socialTxn,
        {std::move(involvement)});
    ASSERT(involvementStatMap.size() == 1);
    response << YCR_JSON_ARRAY(array)
    {
        for (const auto& stat: involvementStatMap.begin()->second) {
            array << stat;
        }
    };
}

} // YCR_USE

} //namespace maps::wiki::socialsrv
