#include "global.h"
#include "config.h"
#include "renderer.h"
#include "requestparams.h"
#include "style2_renderer.h"

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/environment.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/infra/yacare/include/yacare.h>
#include <yandex/maps/wiki/common/rate_counter.h>

#include <chrono>
#include <map>
#include <mutex>
#include <string>
#include <sstream>
#include <vector>

namespace rdr = maps::wiki::renderer;

namespace {

const size_t BACKLOG = 500;

yacare::ThreadPool rendererThreadPool("rendererThreadPool", (THREADS * 3) / 2, BACKLOG);

const auto RATE_LIMITER_CACHE_SIZE = 1000;
const std::chrono::seconds RPS_LIMITER_CACHE_TIME{60}; // 1 min
const auto RATE_LIMIT = 1800; // 30 rps per host

class RateCounter
{
public:
    explicit RateCounter(size_t cacheSize)
        : rateCounter_(cacheSize, RPS_LIMITER_CACHE_TIME)
    {}

    size_t add(std::string_view key)
    {
        if (key.empty()) {
            return 0;
        }

        std::string keyStr(key);
        std::lock_guard<std::mutex> lock(mutex_);
        return rateCounter_.add(keyStr);
    }

private:
    std::mutex mutex_;
    maps::wiki::common::RateCounter<std::string> rateCounter_;
};
RateCounter g_rateCounter(RATE_LIMITER_CACHE_SIZE);


bool isRequestLegit(const yacare::Request& request)
{
    if (maps::common::getYandexEnvironment() != maps::common::Environment::Stable) {
        return true;
    }

    // http-header X-Yandex-Internal-Request added by antirobot
    // https://a.yandex-team.ru/arc_vcs/commit/r8942926
    if (request.env("HTTP_X_YANDEX_INTERNAL_REQUEST") == "1") {
        return true;
    }

    // https://st.yandex-team.ru/CAPTCHA-2652
    // all checks by referer moved to antirobot
    auto referer = request.env("HTTP_REFERER");
    if (request.env("HTTP_X_ANTIROBOT_SUSPICIOUSNESS_Y").starts_with("1")) {
        WARN() << "Block by antirobot, referer: " << referer
               << " ip: " << request.getClientIpAddress()
               << " request: " << request;
        return false;
    }

    auto ja3 = request.env("HTTP_X_YANDEX_JA3");
    auto rateValue = g_rateCounter.add(ja3);
    if (rateValue > RATE_LIMIT) {
        WARN() << "Block by rate: " << rateValue
               << " ja3: " << ja3
               << " referer: " << referer
               << " ip: " << request.getClientIpAddress()
               << " request: " << request;
        return false;
    }
    return true;
}

#ifdef WIKIMAPS_RANDOM_TILES_FOR_ROBOTS
template <typename Data>
class LastTile
{
public:
    Data get(const std::string& layer)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        return data_[layer];
    }

    void set(const std::string& layer, Data data)
    {
        std::lock_guard<std::mutex> lock(mutex_);
        std::swap(data_[layer], data);
    }

private:
    std::mutex mutex_;
    std::map<std::string, Data> data_;
};

#else // WIKIMAPS_RANDOM_TILES_FOR_ROBOTS

template <typename Data>
class LastTile
{
public:
    Data get(const std::string&) { return {}; }

    void set(const std::string& , Data) {}
};
#endif // WIKIMAPS_RANDOM_TILES_FOR_ROBOTS


std::string
toString(const yacare::Request& request)
{
    std::ostringstream os;
    os << request;
    auto str = os.str();
    auto pos = str.find(' ');
    return pos == std::string::npos ? str : str.substr(pos + 1);
}

} // namespace


yacare::VirtualHost vhost {
    yacare::VirtualHost::SLB { "wiki-renderer" }, // for monrun checks

    // for RTC nginx config
    yacare::VirtualHost::SLB { "core-nmaps-renderer-nmaps" },

    yacare::VirtualHost::FQDN { "core-nmaps-renderer-nmaps-unstable.maps.n.yandex.ru" },
};

YCR_SET_DEFAULT(vhost);

// The number of seconds before the shutdown to serve requests while not
// responding to /ping.
YCR_OPTIONS.shutdown().grace_period() = 10;

// @warning Renderer parameters are filtered by MRC proxy. Therefore, in case of
//   any update corresponding list must be updated in the
//   `maps/mobile/server/proxy/config/mrc/library.conf` file.
YCR_QUERY_PARAM(token, std::string);
YCR_QUERY_PARAM(sl, std::string);
YCR_QUERY_PARAM(dx, double, YCR_DEFAULT(0.0));
YCR_QUERY_PARAM(dy, double, YCR_DEFAULT(0.0));
YCR_QUERY_PARAM(scale, double);
YCR_QUERY_PARAM(id, std::string);

YCR_QUERY_CUSTOM_PARAM((), layer, std::string, YCR_DEFAULT({}))
{ return yacare::impl::parseArg(dest, request, "l"); }

YCR_QUERY_CUSTOM_PARAM((), xx, long)
{ return yacare::impl::parseArg(dest, request, "x"); }

YCR_QUERY_CUSTOM_PARAM((), yy, long)
{ return yacare::impl::parseArg(dest, request, "y"); }

YCR_QUERY_CUSTOM_PARAM((), zz, long)
{ return yacare::impl::parseArg(dest, request, "z"); }

YCR_QUERY_CUSTOM_PARAM(("branch"), branch, rdr::TBranchId, YCR_DEFAULT(0))
{
    rdr::TBranchId branch;
    if (yacare::impl::parseArg(branch, request, "branch")){
        REQUIRE(branch >= 0,
            yacare::errors::BadRequest() << "Invalid branch: " << branch);
        dest = branch;
        return true;
    } else {
        return false;
    }
}

YCR_QUERY_CUSTOM_PARAM((), plan_id, rdr::TIndoorPlanId)
{ return yacare::impl::parseArg(dest, request, "id"); }

namespace {

std::string expiresDateTime(std::chrono::seconds expires)
{
    return maps::chrono::formatHttpDateTime(
        std::chrono::system_clock::now() + expires);
}

void handleTileRequest(
    rdr::TBranchId branch,
    std::string layer,
    const std::string& sl,
    const std::string& token,
    double dx,
    double dy,
    long xx,
    long yy,
    long zz,
    const yacare::Request& request,
    yacare::Response& response)
{
    auto& global = rdr::GlobalScope::instance();

    if (layer.empty()) {
        layer = global.config().defaultLayerName();
    }

    const auto& renderers = global.renderers();
    auto rendererIt = renderers.find(layer);
    REQUIRE(rendererIt != renderers.end(),
            yacare::errors::NotFound() <<
                "Renderer for layer " << layer << " not found");
    auto& renderer = *(rendererIt->second);

    rdr::RequestParams reqParams{
        toString(request), xx, yy, zz, token, layer, sl, dx, dy, branch};

    static LastTile<rdr::RenderedData> lastTile;

    bool isLegit = isRequestLegit(request);
    auto renderedData = isLegit
        ? renderer.render(reqParams)
        : lastTile.get(layer);

    response.setHeader("Content-Type", "image/png");
    response.setHeader("Expires", expiresDateTime(renderer.expires()));
    response.write(renderedData.data.get(), renderedData.size);
    if (isLegit && renderedData.size) {
        lastTile.set(layer, std::move(renderedData));
    }
}

const rdr::Style2Renderer& style2renderer(std::string layer = "")
{
    auto& global = rdr::GlobalScope::instance();

    if (layer.empty()) {
        layer = global.config().defaultLayerName();
    }
    const auto& renderers = global.style2Renderers();
    auto rendererIt = renderers.find(layer);
    REQUIRE(rendererIt != renderers.end(),
            yacare::errors::NotFound() <<
                "Style2 renderer for layer " << layer << " not found");

    const auto& renderer = *(rendererIt->second);
    return renderer;
}

} // namespace

YCR_RESPOND_TO("GET /tile", YCR_IN_POOL(rendererThreadPool),
    branch,
    layer = "",
    sl = "",
    token = "",
    dx, dy,
    xx, yy, zz)
{
    handleTileRequest(
        branch,
        layer,
        sl,
        token,
        dx, dy,
        xx, yy, zz,
        request,
        response);
}

YCR_RESPOND_TO("GET /tile2", YCR_IN_POOL(rendererThreadPool),
    branch,
    layer = "",
    sl = "",
    token = "",
    dx, dy,
    scale = 1.0,
    xx, yy, zz)
{
    const auto& renderer = style2renderer(layer);

    rdr::RequestParams reqParams{
        toString(request), xx, yy, zz,
        token, layer, sl, dx, dy, branch, scale};

    static LastTile<std::vector<char>> lastTile;

    bool isLegit = isRequestLegit(request);
    auto result = isLegit
        ? renderer.renderTile(reqParams)
        : lastTile.get(layer);

    response.write(result.data(), result.size());
    response.setHeader("Content-Type", "image/png");
    response.setHeader("Expires", expiresDateTime(renderer.expires()));
    response.setHeader("Access-Control-Allow-Origin", "*");
    if (isLegit && !result.empty()) {
        lastTile.set(layer, std::move(result));
    }
}

#ifdef WIKIMAPS_DESIGNSTAND

namespace {

std::optional<maps::renderer::base::ZoomRange> getZoomRange(
    const yacare::Request& request)
{
    using namespace maps::renderer;

    base::Zoom z = 0;
    base::Zoom zmin = 0;
    base::Zoom zmax = 0;
    constexpr unsigned int ZOOM_SHIFT = 1;

    if (yacare::impl::parseArg(z, request, "z")) {
        bool hasZMin = yacare::impl::parseArg(zmin, request, "zmin");
        bool hasZMax = yacare::impl::parseArg(zmax, request, "zmax");

        if (hasZMin != hasZMax) {
            throw yacare::errors::BadRequest()
                    << "both zmin and zmax parameters are required";
        }

        if (hasZMin && hasZMax) {
            if (!(z <= zmax && zmax <= z + ZOOM_SHIFT && zmin == zmax)) {
                throw yacare::errors::BadRequest() << " Wrong zoom range";
            }
            return base::ZoomRange{zmin, zmax};
        }
    }
    return std::nullopt;
}

} // namespace

YCR_RESPOND_TO("GET /vec3_tile", YCR_IN_POOL(rendererThreadPool),
    branch,
    layer = "",
    sl = "",
    token = "",
    dx, dy,
    xx, yy, zz,
    format = "")
{
    const auto& renderer = style2renderer(layer);

    rdr::RequestParams reqParams{
        toString(request), xx, yy, zz,
        token, layer, sl, dx, dy, branch};
    reqParams.zoomRange = getZoomRange(request);

    if (format == "text") {
        auto result = renderer.renderVec3Tile(
            reqParams, rdr::Vec3ResultFormat::Text);
        response.write(result.data(), result.size());
        response.setHeader("Content-Type", "text/plain");
    } else {
        auto result = renderer.renderVec3Tile(
            reqParams, rdr::Vec3ResultFormat::Proto);
        response.write(result.data(), result.size());
        response.setHeader("Content-Type", "application/x-protobuf");
    }
    response.setHeader("Access-Control-Allow-Origin", "*");
}

YCR_RESPOND_TO("GET /indoor_plan", YCR_IN_POOL(rendererThreadPool),
    branch,
    token = "",
    plan_id)
{
    const auto& renderer = style2renderer();
    auto result = renderer.indoorPlan(plan_id, branch, token);
    if (result.empty()) {
        response.setStatus(yacare::HTTPStatus::NoContent);
    } else {
        response.write(result.data(), result.size());
    }
    response.setHeader("Content-Type", "application/x-protobuf");
    response.setHeader("Access-Control-Allow-Origin", "*");
}

YCR_RESPOND_TO("GET /data_set_schema")
{
    const auto& renderer = style2renderer();
    auto schema = renderer.dataSetSchema();

    response.write(schema.data(), schema.size());
    response.setHeader("Content-Type", "application/json");
    response.setHeader("Access-Control-Allow-Origin", "*");
}

YCR_RESPOND_TO("POST /reload_design", YCR_RESTRICT_TO_LOCAL) {
    auto& global = rdr::GlobalScope::instance();
    const auto& renderers = global.style2Renderers();
    for (auto& [_, renderer]: renderers) {
        renderer->reloadDesign();
    }
}

#endif // WIKIMAPS_DESIGNSTAND

YCR_MAIN(argc, argv)
{
    try {
        rdr::GlobalScope globalScope(argv[0], argc >= 2 ? argv[1] : "");
        yacare::run(yacare::RunSettings{.useSystemDefaultLocale = true});
        return 0;
    } catch (maps::Exception& e) {
        std::cerr << e << std::endl;
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 1;
}
