#pragma once
#include "error.h"

#include <maps/wikimap/mapspro/libs/acl/include/common.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/date_time_condition.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/feedback/task_feed.h>
#include <maps/libs/chrono/include/time_point.h>
#include <yandex/maps/wiki/common/compound_token.h>
#include <maps/infra/yacare/include/yacare.h>

#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/constants.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/lexical_cast.hpp>
#include <optional>

YCR_QUERY_PARAM(uid, maps::wiki::acl::UID);
YCR_QUERY_CUSTOM_PARAM((), token, std::string)
{
    using namespace maps::wiki;

    auto token = request.input()["token"];
    REQUIRE(common::CompoundToken::isValid(token),
            yacare::errors::BadRequest() << "Invalid token");
    dest = token;
    return true;
}


namespace maps::wiki::socialsrv {

void checkUid(acl::UID uid);

template<typename T>
std::optional<T> optionalQueryParam(
    const yacare::Request& request,
    const std::string& name)
{
    const auto& valueStr = request.input()[name];

    if (valueStr.empty()) {
        return std::nullopt;
    }

    try {
        return boost::lexical_cast<T>(valueStr);
    } catch (const boost::bad_lexical_cast&) {
        throw yacare::errors::BadRequest() << "Invalid parameter value";
    }
}

// explicit instantination of boolean values parsing is required
// since boost::lexical_cast doesn't process "true" and "false"
// values properly by default
//
template<>
inline std::optional<bool> optionalQueryParam(
    const yacare::Request& request,
    const std::string& name)
{
    const auto& valueStr = request.input()[name];

    if (valueStr.empty()) {
        return std::nullopt;
    } else if (valueStr == "true") {
        return true;
    } else if (valueStr == "false") {
        return false;
    } else {
        throw yacare::errors::BadRequest() <<
            "Invalue value " << valueStr << " " <<
            "for boolean parameter " << name;
    }
}


template<typename T>
T queryParam(
    const yacare::Request& request,
    const std::string& name,
    const T& defaultValue)
{
    return optionalQueryParam<T>(request, name).value_or(defaultValue);
}


template <class T>
T queryParam(
    const yacare::Request& request,
    const std::string& name)
{
    auto value = optionalQueryParam<T>(request, name);
    REQUIRE(value,
            yacare::errors::BadRequest() << "Missing parameter: " << name);
    return *value;
}

template<typename T>
T positionalParam(const std::vector<std::string>& argv, size_t index)
{
    REQUIRE(index < argv.size(),
            yacare::errors::BadRequest() << "Bad parameters count");
    try {
        return boost::lexical_cast<T>(argv[index]);
    } catch (const boost::bad_lexical_cast&) {
        throw yacare::errors::BadRequest() << "Invalid parameter value";
    }
}

template <typename T>
std::vector<T> vectorQueryParam(
    const yacare::Request& request,
    const std::string& name)
{
    std::vector<T> retVal;

    auto commaSeparatedList = queryParam<std::string>(request, name, "");
    if (commaSeparatedList.empty()) {
        return retVal;
    }
    std::vector<std::string> strings;
    boost::split(strings, commaSeparatedList,
        boost::is_any_of(","), boost::token_compress_on);

    std::for_each(strings.begin(), strings.end(), [](std::string& str) {
        boost::trim(str);
    });

    for (const auto& valueStr : strings) try {
        retVal.push_back(boost::lexical_cast<T>(valueStr));
    } catch (const boost::bad_lexical_cast&) {
        throw yacare::errors::BadRequest()
            << "Invalid value: '" << valueStr << "'"
            << " in list '" << commaSeparatedList << "'";
    }

    return retVal;
}

template <typename T>
std::optional<std::vector<T>> optionalVectorQueryParam(
    const yacare::Request& request,
    const std::string& name)
{
    if (!request.input().has(name)) {
        return std::nullopt;
    }
    return vectorQueryParam<T>(request, name);
}

std::optional<chrono::TimePoint> parseOptionalTimePoint(
    const yacare::Request& request,
    std::string_view name);

std::optional<social::DateTimeCondition> parseOptionalDateTimeCondition(
    const yacare::Request& request,
    std::string_view nameBefore, std::string_view nameAfter);

void makeJsonResponse(
    yacare::Response& response,
    const std::string& jsonString);

std::string jsonToStringNoThrow(const json::Value& json);

geolib3::Point2 pointFromGeojson(const json::Value& geojson);

template<typename T>
std::optional<T> parseOptional(
    const json::Value& jsonValue)
{
    if (!jsonValue.exists()) {
        return std::nullopt;
    }
    REQUIRE(jsonValue.isString(),
            yacare::errors::BadRequest()
                << "string expected in json: '"
                << jsonValue << "'");
    std::istringstream ss(jsonValue.as<std::string>());
    T retVal;
    ss >> retVal;
    return retVal;
}

template<>
inline std::optional<bool> parseOptional(
    const json::Value& jsonValue)
{
    if (!jsonValue.exists()) {
        return std::nullopt;
    }
    if (jsonValue.isBool()) {
        return jsonValue.as<bool>();
    } else {
        throw yacare::errors::BadRequest()
            << "boolean expected in json: '"
            << jsonValue << "'";
    }
}

template<>
inline std::optional<chrono::TimePoint> parseOptional(
    const json::Value& jsonValue)
{
    if (!jsonValue.exists()) {
        return std::nullopt;
    }
    REQUIRE(jsonValue.isString(),
            yacare::errors::BadRequest()
                << "string expected in json: '" << jsonValue << "'");
    try {
        return chrono::parseIsoDateTime(jsonValue.as<std::string>());
    }
    catch (const std::exception&) {
        throw yacare::errors::BadRequest()
            << "string with TimePoint expected in json: '"
            << jsonValue << "'";
    }
}

template <typename T>
std::optional<std::vector<T>> parseOptionalVector(
    const json::Value& jsonValue)
{
    if (!jsonValue.exists()) {
        return std::nullopt;
    }
    REQUIRE(jsonValue.isArray(),
            yacare::errors::BadRequest()
                << "Array expected in json: '"<< jsonValue << "'");
    std::vector<T> retVal;
    retVal.reserve(jsonValue.size());
    for (const auto& item : jsonValue) {
        REQUIRE(item.isString(),
                yacare::errors::BadRequest()
                    << "array of strings expected in json: '"
                    << jsonValue << "'");
        std::istringstream ss(item.as<std::string>());
        T itemValue;
        ss >> itemValue;
        retVal.template emplace_back(std::move(itemValue));
    }

    return retVal;
}

template<typename T>
T parse(
    const json::Value& jsonValue,
    const T& defaultValue)
{
    return parseOptional<T>(jsonValue).value_or(defaultValue);
}

std::optional<social::DateTimeCondition> parseOptionalDateTimeCondition(
    const json::Value& jsonBefore, const json::Value& jsonAfter);

social::feedback::TaskFeedParamsId getFeedParams(const yacare::Request& request);

} // namespace maps::wiki::socialsrv
