#include "hyp_poster.h"
#include "hyp_serialize.h"

#include <maps/wikimap/mapspro/libs/http/http_utils.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/json/include/value.h>
#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/social/feedback/attributes.h>
#include <yandex/maps/wiki/social/feedback/attribute_names.h>

#include <algorithm>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/optional.hpp>

namespace bg = boost::gregorian;
namespace mwsfa = maps::wiki::social::feedback::attrs;

namespace maps::wiki::traffic_analyzer {

namespace urlids {
const std::string UID = "uid";
}

namespace {

const std::string ROAD_DIR_HANDLE = "/feedback/tasks/road-direction";
const std::string RESPONSE_DUPLICATE = "ERR_DUPLICATE";

std::string postRequestBody(const OnewayHypothesisRevision& hypRev)
{
    json::Builder bodyBuilder;

    bodyBuilder << [&](json::ObjectBuilder bodyBuilder) {
        bodyBuilder["source"] = "gps";
        bodyBuilder["workflow"] = "task";
        bodyBuilder["hidden"] = true;

        bodyBuilder["objectId"] = std::to_string(hypRev.revId.objectId());
        bodyBuilder[mwsfa::COMMIT_ID] = std::to_string(hypRev.revId.commitId());

        bodyBuilder["userAttrs"] = [&](json::ObjectBuilder contextBuilder) {
            contextBuilder["roadTraffic"] = [&](json::ObjectBuilder trafficBuilder) {
                json(hypRev.roadTraffic, trafficBuilder);
            };
        };

        bodyBuilder[mwsfa::SUPPOSED_DIRECTION] =
            boost::lexical_cast<std::string>(hypRev.direction);

        bodyBuilder["position"] =
            geolib3::geojson(geolib3::mercator2GeoPoint(hypRev.positionMerc));
    };

    return bodyBuilder.str();
}

boost::optional<std::string> extractRealHttpError(const json::Value& body)
{
    try {
        return body["error"]["status"].as<std::string>();
    } catch (const Exception&) {
        return boost::none;
    }
}

} // namespace anonymous


HypothesisRevisionPoster::HypothesisRevisionPoster(
    const std::string& socialServiceUrl) :
        socialServiceUrl_(socialServiceUrl)
{
    socialServiceUrl_.setPath(ROAD_DIR_HANDLE);
    socialServiceUrl_.addParam(urlids::UID, std::to_string(common::ROBOT_UID));
}

/* Returns 'true' : if hypothesis was succesfully posted.
   Returns 'false': if this hypothesis is duplicate of already existing one.
   Else throws in case of any error during http communication
*/
bool HypothesisRevisionPoster::postHypothesis(
    const OnewayHypothesisRevision& hypRev) const
{
    const size_t maxAttempts = 6;
    const std::chrono::seconds initialTimeout{1};
    const double timeoutBackoff = 2;

    auto retryPolicy = maps::common::RetryPolicy()
        .setTryNumber(maxAttempts)
        .setInitialCooldown(initialTimeout)
        .setCooldownBackoff(timeoutBackoff);

    http::Client httpClient;
    auto [response, status] = httpClient.post(
        socialServiceUrl_,
        postRequestBody(hypRev),
        retryPolicy);

    REQUIRE(status == http_status::OK,
        "Unable to post hypothesis; Response status: " << status);

    /* If social service is available, it always returns code 200,
       even if there were real mistakes. Such limitation is imposed
       by frontend.
       In order to get real errors we should parse response body.
       Forwarded errors are described here:
       "arcadia/maps/wikimap/mapspro/services/social/src/error.h"
    */
    auto realHttpError = extractRealHttpError(
        json::Value::fromString(response));

    if (realHttpError == boost::none) {
        return true;
    } else if (*realHttpError == RESPONSE_DUPLICATE) {
        return false;
    } else {
        throw Exception() << "Unable to post hypothesis. "
                          << "Response status: " << *realHttpError;
    }
}

/* Return number of hypothese which were published
*/
int postHypothesesLimited(
    const HypothesisRevisionPoster& poster,
    std::vector<OnewayHypothesisRevision> hyps,
    int hypsMaxNumber)
{
    auto greaterByRatio =
        [](const OnewayHypothesisRevision& lhs,
           const OnewayHypothesisRevision& rhs) {
            return lhs.roadTraffic.matchRatio > rhs.roadTraffic.matchRatio;
        };

    std::sort(hyps.begin(), hyps.end(), greaterByRatio);

    int postedHyps = 0;
    for (const auto& hyp : hyps) {
        if (postedHyps >= hypsMaxNumber) {
            break;
        }
        if (poster.postHypothesis(hyp)) {
            postedHyps++;
        }
    }

    return postedHyps;
}

} // namespace maps::wiki::traffic_analyzer
