#include "hypothesis.h"
#include "poi.h"
#include "sprav.h"

#include <yandex/maps/geolib3/proto.h>

#include <maps/infra/yacare/include/yacare.h>

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/json/include/builder.h>

#include <maps/wikimap/mapspro/libs/geom_tools/include/yandex/maps/wiki/geom_tools/object_diff_builder.h>

#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/attribute_names.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/description_producers.h>

#include <util/string/cast.h>
#include <cmath>

using namespace std::string_literals;

namespace maps::wiki::socialsrv {

using social::TId;

namespace mwsf = social::feedback;
namespace mwsfa = social::feedback::attrs;

namespace {

const auto FIND_POI_DISTANCE_METERS = 5000; // 5 km

const auto AFTER = "after"s;
const auto BEFORE = "before"s;
const auto BOUNDS = "bounds"s;
const auto CONTENT = "content"s;
const auto GEOMETRY = "geometry"s;
const auto MODIFIED = "modified"s;
const auto PERMALINK = "permalink"s;
const auto SPRAV_FEED = "sprav-feed"s;
const auto TYPE = "type"s;

json::Value makeSourceContext(TId companyId)
{
    json::Builder b;
    b << [&](json::ObjectBuilder b) {
        b[TYPE] = SPRAV_FEED;
        b[CONTENT] << [&](json::ObjectBuilder content) {
            content[PERMALINK] = std::to_string(companyId);
        };
    };
    return json::Value::fromString(b.str());
}

geolib3::Point2 roundPoint(const geolib3::Point2& point)
{
    constexpr auto PRECISION = 6;
    static const auto FACTOR = std::pow(10, PRECISION);

    return geolib3::Point2(
        round(point.x() * FACTOR) / FACTOR,
        round(point.y() * FACTOR) / FACTOR
    );
}

geolib3::PointsVector roundPoints(const geolib3::PointsVector& points)
{
    geolib3::PointsVector result;
    result.reserve(points.size());
    for (const auto& point : points) {
        result.push_back(roundPoint(point));
    }
    return result;
}

json::Value makeObjectDiff(
    const geolib3::Point2& oldPosition,
    const geolib3::PointsVector& oldEntrances,
    const geolib3::Point2& newPosition,
    const geolib3::PointsVector& newEntrances)
{
    geom_tools::ObjectDiffBuilder<geolib3::Point2> diff;
    {
        geolib3::PointsVector before{ oldPosition };
        before.insert(before.end(), oldEntrances.cbegin(), oldEntrances.cend());
        diff.setBefore(std::move(before));
    }
    {
        geolib3::PointsVector after{ newPosition };
        after.insert(after.end(), newEntrances.cbegin(), newEntrances.cend());
        diff.setAfter(std::move(after));
    }

    json::Builder b;
    b << [&](json::ObjectBuilder builder) {
        diff.json(builder);
    };
    return json::Value::fromString(b.str());
}

mwsf::TaskNew makePoiTask(
    DbPoolsWithViewTrunk& dbPools,
    const SpravData& spravData,
    const std::string& source,
    const geolib3::Point2& positionGeo,
    const geolib3::PointsVector& entrancesGeo)
{
    auto companyData = findCompany(dbPools, spravData, FIND_POI_DISTANCE_METERS);

    auto suggestedAction = mwsf::SuggestedAction::Modify;
    mwsf::TaskNew task(
        geolib3::geoPoint2Mercator(positionGeo),
        companyData ? companyData->business.feedbackType : mwsf::Type::Poi,
        source,
        mwsf::SuggestedActionDescr{suggestedAction}.toDescription());

    task.attrs.addCustom(mwsfa::SUGGESTED_ACTION, std::string(toString(suggestedAction)));

    task.internalContent = true;
    task.hidden = true;

    task.attrs.add(mwsf::AttrType::SourceContext, makeSourceContext(spravData.businessId));

    if (companyData) {
        task.objectId = companyData->business.objectId;
        auto oldPositionGeo = geolib3::mercator2GeoPoint(companyData->business.positionMercator);
        geolib3::PointsVector oldEntrancesGeo;
        for (const auto& entrance : companyData->entrances) {
            oldEntrancesGeo.emplace_back(
                geolib3::mercator2GeoPoint(entrance.positionMercator));
        }
        task.attrs.add(
            mwsf::AttrType::ObjectDiff,
            makeObjectDiff(
                roundPoint(oldPositionGeo),
                roundPoints(oldEntrancesGeo),
                roundPoint(positionGeo),
                roundPoints(entrancesGeo))
        );
    }
    return task;
}

api::HypothesisResult processVerdictAccepted(
    DbPoolsWithViewTrunk& dbPools,
    const social::feedback::Task& task)
{
    const auto& objectId = task.objectId();
    REQUIRE(objectId && *objectId,
            "Can not find object id, feedback id" << task.id());

    api::HypothesisResult message;
    auto* changeBusinessResult = message.mutable_change_business_result();
    auto* status = changeBusinessResult->mutable_status();

    auto companyData = findCompanyByObjectId(dbPools, *objectId);
    if (companyData) {
        auto* accepted = status->mutable_accepted();
        auto* company = accepted->mutable_company();
        *(company->mutable_point()) = geolib3::proto::encode(
            geolib3::mercator2GeoPoint(companyData->business.positionMercator));

        for (const auto& entranceData : companyData->entrances) {
            auto* entrance = company->add_entrances();
            *(entrance->mutable_point()) = geolib3::proto::encode(
                geolib3::mercator2GeoPoint(entranceData.positionMercator));
        }
    } else {
        // object deleted ?
        status->mutable_unknown();
        WARN() << "Can not find object id: " << *objectId
               << ", feedback id" << task.id();
    }
    return message;
}

api::HypothesisResult processVerdictRejected()
{
    api::HypothesisResult message;
    auto* changeBusinessResult = message.mutable_change_business_result();
    changeBusinessResult->mutable_status()->mutable_rejected();
    return message;
}

} // namespace

mwsf::TaskNew poiNewTaskFromProto(
    DbPoolsWithViewTrunk& dbPools,
    geosearch_client::Client& geosearchClient,
    const std::string& source,
    const std::string& body)
{
    api::Hypothesis message;
    REQUIRE(message.ParseFromString(TString{body}),
            yacare::errors::BadRequest() << "Invalid proto message");

    REQUIRE(message.has_change_business(),
            yacare::errors::BadRequest() << "Missing change business");
    const auto& changeBusiness = message.change_business();
    REQUIRE(changeBusiness.has_company(),
            yacare::errors::BadRequest() << "Missing company");

    REQUIRE(changeBusiness.has_company_id(),
            yacare::errors::BadRequest() << "Missing company id");
    const auto& companyId = changeBusiness.company_id();
    REQUIRE(!companyId.empty(),
            yacare::errors::BadRequest() << "Empty company id");

    TId companyIdValue;
    REQUIRE(TryFromString(companyId, companyIdValue),
            yacare::errors::BadRequest() << "Invalid company id: " << companyId);

    const auto& company = changeBusiness.company();
    REQUIRE(company.has_point(),
            yacare::errors::BadRequest() << "Missing point");

    geolib3::PointsVector entrances;
    for (const auto& entrance : company.entrances()) {
        entrances.emplace_back(geolib3::proto::decode(entrance.point()));
    }

    auto spravData = loadSpravData(geosearchClient, companyIdValue);
    REQUIRE(spravData,
            yacare::errors::NotFound() << "Can not load data from geosearch: " << companyId);

    auto task = makePoiTask(
        dbPools, *spravData, source,
        geolib3::proto::decode(company.point()), entrances);
    REQUIRE(task.objectId && *task.objectId,
            yacare::errors::NotFound() << "Can not find company: " << companyId);

    if (!entrances.empty()) {
        json::Builder b;
        b << geolib3::geojson(entrances);
        task.attrs.add(mwsf::AttrType::PoiEntrances, json::Value::fromString(b.str()));
    }

    if (!changeBusiness.has_context()) {
        return task;
    }

    const auto& context = changeBusiness.context();
    if (context.has_comment()) {
        const auto& comment = context.comment();
        if (!comment.empty()) {
            task.attrs.addCustom(mwsfa::USER_DATA_COMMENT, comment);
        }
    }

    std::vector<json::Value> urls;
    for (const auto& file : context.files()) {
        urls.emplace_back(file.url());
    }
    if (!urls.empty()) {
        task.attrs.add(mwsf::AttrType::UserDataPhotoUrls, json::Value(std::move(urls)));
    }
    return task;
}

api::Hypothesis makeHypothesisProto(const mwsf::Task& task)
{
    api::Hypothesis message;
    auto* changeBusiness = message.mutable_change_business();
    auto* company = changeBusiness->mutable_company();

    auto positionGeo = geolib3::mercator2GeoPoint(task.position());
    *(company->mutable_point()) = geolib3::proto::encode(positionGeo);

    const auto& attrs = task.attrs();
    if (attrs.exist(mwsf::AttrType::SourceContext)) {
        const auto& sourceContext = attrs.get(mwsf::AttrType::SourceContext);
        if (sourceContext.hasField(TYPE) && sourceContext[TYPE].as<std::string>() == SPRAV_FEED) {
            const auto& permalink = sourceContext[CONTENT][PERMALINK];
            if (permalink.exists() && !permalink.isNull()) {
                changeBusiness->set_company_id(TString(permalink.toString()));
            }
        }
    }

    if (attrs.exist(mwsf::AttrType::PoiEntrances)) {
        auto points = geolib3::readGeojson<geolib3::PointsVector>(
            attrs.get(mwsf::AttrType::PoiEntrances));
        for (const auto& point : points) {
            auto* entrance = company->add_entrances();
            *(entrance->mutable_point()) = geolib3::proto::encode(point);
        }
    }

    std::string comment;
    if (attrs.existCustom(mwsfa::USER_DATA_COMMENT)) {
        comment = attrs.getCustom(mwsfa::USER_DATA_COMMENT);
    }
    std::vector<std::string> urls;
    if (attrs.exist(mwsf::AttrType::UserDataPhotoUrls)) {
        for (const auto& value : attrs.get(mwsf::AttrType::UserDataPhotoUrls)) {
            urls.emplace_back(value.as<std::string>());
        }
    }

    if (!comment.empty() || !urls.empty()) {
        auto* context = changeBusiness->mutable_context();
        if (!comment.empty()) {
            context->set_comment(TString(comment));
        }
        for (const auto& url : urls) {
            context->add_files()->set_url(TString(url));
        }
    }
    return message;
}

api::HypothesisResult makeHypothesisResultProto(
    DbPoolsWithViewTrunk& dbPools,
    const social::feedback::Task& task)
{
    const auto& resolved = task.resolved();
    ASSERT(resolved);

    switch (resolved->resolution.verdict()) {
        case mwsf::Verdict::Accepted:
            return processVerdictAccepted(dbPools, task);
        case mwsf::Verdict::Rejected:
            return processVerdictRejected();
    }
}

} // namespace maps::wiki::socialsrv
