#include <maps/wikimap/mapspro/libs/editor_client/include/instance.h>

#include <maps/wikimap/mapspro/libs/editor_client/include/exception.h>
#include <maps/wikimap/mapspro/libs/editor_client/impl/parser.h>
#include <maps/wikimap/mapspro/libs/editor_client/impl/request.h>

#include "magic_strings.h"

#include <maps/wikimap/mapspro/libs/http/http_utils.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/enum_io/include/enum_io.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/point.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <boost/algorithm/string.hpp>

using namespace std::chrono_literals;
using namespace std::string_literals;

namespace maps::wiki::editor_client {

namespace {
namespace param {
const auto CATEGORIES = "categories"s;
const auto D = "d"s;
const auto FORMAT = "format"s;
const auto GEOMETRY = "geometry"s;
const auto JSON = "json"s;
const auto INTERSECTS = "intersects"s;
const auto IS_GEOPRODUCT = "is-geoproduct"s;
const auto LIMIT = "limit"s;
const auto SYNC = "sync"s;
const auto THRESHOLD = "threshold"s;
const auto TOKEN = "token"s;
const auto TRUE = "true"s;
const auto FALSE = "false"s;
const auto UID = "uid"s;
const auto AUTO_IS_LOCAL = "auto-is-local"s;
const auto PAGE = "page"s;
const auto PER_PAGE = "per-page"s;
const auto OBJECT_ID = "object-id"s;
const auto INDOOR_LEVEL_ID = "indoor-level-id"s;
} // param

const auto URL_PATH_DELIMITER = "/"s;

const auto RETRY_POLICY = maps::common::RetryPolicy()
        .setTryNumber(5)
        .setInitialCooldown(1s)
        .setCooldownBackoff(2.0);

const auto DELETE_RETRY_POLICY = maps::common::RetryPolicy()
        .setTryNumber(2)
        .setInitialCooldown(1s)
        .setCooldownBackoff(2.0);


std::string joinWithUrl(const std::string& urlBase, const std::string& path)
{
    bool pathIsOk = path.starts_with(URL_PATH_DELIMITER);
    bool urlBaseIsOk = !urlBase.ends_with(URL_PATH_DELIMITER);
    if (pathIsOk && urlBaseIsOk) {
        return urlBase + path;
    }
    if (!pathIsOk && !urlBaseIsOk) {
        return urlBase + path;
    }
    if (urlBaseIsOk) {
        return urlBase + URL_PATH_DELIMITER + path;
    } else {
        return urlBase + path.substr(1);
    }
}

void checkServerResponseForError(const std::string& responseBody)
{
    auto jsonBody = maps::json::Value::fromString(responseBody);
    if (!jsonBody.hasField(json::ERROR)) {
        return;
    }
    const auto& error = jsonBody[json::ERROR];
    ASSERT(error.isObject());
    ASSERT(error.hasField(json::STATUS));
    const auto& status = error[json::STATUS];
    ASSERT(status.isString());
    auto statusString = status.as<std::string>();
    throw ServerException(
        statusString.empty()
            ? json::ERROR
            : statusString, responseBody);
}

} // namespace

constexpr enum_io::Representations<IsGeoproduct> IS_GEOPRODUCT_ENUM_REPRESENTATION {
    {IsGeoproduct::True, "true"},
    {IsGeoproduct::False, "false"},
};

DEFINE_ENUM_IO(IsGeoproduct, IS_GEOPRODUCT_ENUM_REPRESENTATION);

Instance::Instance(std::string urlBase, revision::UserID uid)
    : urlBase_(std::move(urlBase))
    , uid_(uid)
{
}

BasicEditorObject
Instance::getObjectById(revision::DBID objectId) const
{
    http::URL url = joinWithUrl(urlBase_, "/objects/" + std::to_string(objectId));
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    if (!token_.empty()) {
        url.addParam(param::TOKEN, token_);
    }

    auto [responseBody, status] = client_.get(url, RETRY_POLICY);
    REQUIRE(status == http_status::OK,
        "Failed to make http get: " << url);
    checkServerResponseForError(responseBody);
    return parseJsonResponse(responseBody);
}



std::vector<ObjectIdentity>
Instance::getObjectsByLasso(
    const std::set<std::string>& categories,
    const geolib3::SimpleGeometryVariant& mercGeometry,
    std::optional<double> threshold,
    size_t limit,
    GeomPredicate geomPredicate) const
{
    ASSERT(
        mercGeometry.geometryType() == geolib3::GeometryType::Polygon ||
        (
            (mercGeometry.geometryType() == geolib3::GeometryType::Point ||
             mercGeometry.geometryType() == geolib3::GeometryType::LineString)
            &&
            threshold));
    http::URL url = joinWithUrl(urlBase_, "/objects/query/lasso"s);
    url.addParam(param::CATEGORIES, maps::wiki::common::join(categories, ','));
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    url.addParam(param::LIMIT, std::to_string(limit));
    if (geomPredicate == GeomPredicate::Intersects) {
        url.addParam(param::INTERSECTS, param::TRUE);
    }
    if (!token_.empty()) {
        url.addParam(param::TOKEN, token_);
    }
    if (threshold) {
        url.addParam(param::THRESHOLD, boost::lexical_cast<std::string>(*threshold));
    }

    maps::json::Builder contentBuilder;
    auto geoGeometry = geolib3::convertMercatorToGeodetic(mercGeometry);
    contentBuilder << geolib3::geojson(geoGeometry);

    auto [responseBody, status] = client_.post(
        url,
        contentBuilder.str(),
        RETRY_POLICY);

    REQUIRE(status == http_status::OK,
        "Failed to make http POST: " << url);
    checkServerResponseForError(responseBody);
    return parseObjectIdentities(responseBody);
}

std::vector<ObjectIdentity>
Instance::getObjectsByBusinessId(
    const std::string& businessId,
    std::optional<const geolib3::Point2> mercPointHint,
    std::optional<double> mercDistance) const
{
    ASSERT(!businessId.empty());
    http::URL url = joinWithUrl(urlBase_, "/objects/query/poi/business-id/"s + businessId);
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    if (!token_.empty()) {
        url.addParam(param::TOKEN, token_);
    }
    if (mercPointHint) {
        maps::json::Builder contentBuilder;
        auto geoGeometry = geolib3::convertMercatorToGeodetic(*mercPointHint);
        contentBuilder << geolib3::geojson(geoGeometry);
        url.addParam(param::GEOMETRY, contentBuilder.str());
    }
    if (mercDistance) {
        url.addParam(param::D, boost::lexical_cast<std::string>(*mercDistance));
    }
    auto [responseBody, status] = client_.get(
        url,
        RETRY_POLICY);

    REQUIRE(status == http_status::OK,
        "Failed to make http POST: " << url);
    checkServerResponseForError(responseBody);
    return parseObjectIdentities(responseBody);
}

BasicEditorObject
Instance::saveObject(const BasicEditorObject& object, std::vector<DiffAlertMessage> messages)
{
    http::Request request(client_, http::POST, joinWithUrl(urlBase_, "/objects"s));
    request.addParam(param::UID, std::to_string(uid_));
    request.addParam(param::FORMAT, param::JSON);
    request.addParam(param::SYNC, param::TRUE);
    request.addParam(param::AUTO_IS_LOCAL, param::TRUE);
    const auto body = createJsonUpdateRequest(object, messages);
    request.setContent(body);
    auto response = request.perform();
    REQUIRE(response.status() == http_status::OK,
        "Failed to make http post: " << request.url());
    const auto responseBody = response.readBody();
    try {
        checkServerResponseForError(responseBody);
    } catch (...) {
        ERROR() << "Save request body:" << body;
        throw;
    }
    auto jsonBody = maps::json::Value::fromString(responseBody);
    ASSERT(jsonBody.hasField(json::TOKEN));
    token_ = jsonBody[json::TOKEN].as<std::string>();
    ASSERT(jsonBody.hasField(json::GEO_OBJECTS));
    const auto& geoObjects = jsonBody[json::GEO_OBJECTS];
    ASSERT(geoObjects.isArray() && !geoObjects.empty());
    return parseObject(geoObjects[0]);
}

void
Instance::deleteObject(revision::DBID objectId)
{
    http::URL url = joinWithUrl(urlBase_,
                "/objects/" + std::to_string(objectId)
                + "/state/deleted");
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    url.addParam(param::SYNC, param::TRUE);

    auto [responseBody, status] = client_.post(url, {}, DELETE_RETRY_POLICY);
    REQUIRE(status == http_status::OK,
        "Failed to make http post: " << url);
    checkServerResponseForError(responseBody);
    auto jsonBody = maps::json::Value::fromString(responseBody);
    ASSERT(jsonBody.hasField(json::TOKEN));
    token_ = jsonBody[json::TOKEN].as<std::string>();
}

std::vector<BasicCommitData>
Instance::getHistory(revision::DBID objectId, size_t page, size_t perPage) const
{
    http::URL url = joinWithUrl(urlBase_, "/objects/" + std::to_string(objectId)+ "/history");
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    url.addParam(param::PAGE, std::to_string(page));
    url.addParam(param::PER_PAGE, std::to_string(perPage));
    if (!token_.empty()) {
        url.addParam(param::TOKEN, token_);
    }

    auto [responseBody, status] = client_.get(url, RETRY_POLICY);
    REQUIRE(status == http_status::OK,
        "Failed to make http get: " << url);
    checkServerResponseForError(responseBody);
    return parseHistoryResponseJson(responseBody);
}

PoiConflicts
Instance::getPoiConflicts(
        std::optional<revision::DBID> selfObjectId,
        std::optional<revision::DBID> indoorLevelObjectId,
        const geolib3::Point2& mercPointHint,
        const IsGeoproduct isGeoproduct) const
{
    http::URL url = joinWithUrl(urlBase_, "/objects/query/poi/conflicts");
    url.addParam(param::UID, std::to_string(uid_));
    url.addParam(param::FORMAT, param::JSON);
    url.addParam(param::IS_GEOPRODUCT, toString(isGeoproduct));
    if (!token_.empty()) {
        url.addParam(param::TOKEN, token_);
    }
    maps::json::Builder contentBuilder;
    auto geoGeometry = geolib3::convertMercatorToGeodetic(mercPointHint);
    contentBuilder << geolib3::geojson(geoGeometry);
    url.addParam(param::GEOMETRY, contentBuilder.str());
    if (selfObjectId) {
        url.addParam(param::OBJECT_ID, std::to_string(*selfObjectId));
    }
    if (indoorLevelObjectId) {
        url.addParam(param::INDOOR_LEVEL_ID, std::to_string(*indoorLevelObjectId));
    }
    auto [responseBody, status] = client_.get(url, RETRY_POLICY);
    REQUIRE(status == http_status::OK,
        "Failed to make http get: " << url);
    checkServerResponseForError(responseBody);
    return parsePoiConflictsResponseJson(responseBody);
}


} // namespace maps::wiki::editor_client
