#include <maps/wikimap/mapspro/services/renderer_overlay/lib/config.h>

#include <maps/wikimap/mapspro/services/renderer_overlay/lib/image.h>
#include <maps/wikimap/mapspro/services/renderer_overlay/lib/object_helpers.h>
#include <maps/wikimap/mapspro/services/renderer_overlay/lib/overlay_image_info.h>
#include <maps/wikimap/mapspro/services/renderer_overlay/lib/render.h>
#include <maps/wikimap/mapspro/services/renderer_overlay/lib/storage.h>
#include <maps/wikimap/mapspro/services/renderer_overlay/lib/transformation.h>

#include <yandex/maps/wiki/revision/common.h>

#include <maps/wikimap/mapspro/libs/acl/include/check_context.h>
#include <maps/wikimap/mapspro/libs/acl/include/subject_path.h>
#include <maps/wikimap/mapspro/libs/acl_utils/include/caching_acl.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/infra/yacare/include/yacare.h>

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

#include <maps/libs/common/include/base64.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/profiletimer.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/tile/include/utils.h>
#include <maps/infra/yacare/include/params/tvm.h>
#include <maps/infra/yacare/include/tvm.h>
#include <maps/libs/auth/include/blackbox.h>

using namespace maps;
using namespace maps::geolib3;
using namespace maps::tile;
using namespace maps::wiki;
using namespace maps::wiki::acl;
using namespace maps::wiki::renderer_overlay;

using TvmAlias = NTvmAuth::TClientSettings::TAlias;


namespace {

const size_t BACKLOG = 500;
const size_t THREADS = 24;

const size_t ACL_CACHE_MAX_SIZE = 1000;
const std::chrono::seconds ACL_CACHE_EXPIRATION_PERIOD(600);

const std::string CONTENT_TYPE_HEADER = "Content-Type";
const std::string CONTENT_TYPE_PNG = "image/png";
const std::string CONTENT_TYPE_JSON = "application/json";
const std::string DEFAULT_CONFIG_PATH = "/etc/yandex/maps/wiki/renderer_overlay/config.xml";
const std::string DEFAULT_SERVICES_CONFIG_PATH = "/etc/yandex/maps/wiki/services/services.xml";
const std::string REQUEST_ENV_HTTP_HOST = "HTTP_HOST";
const std::string HTTPS = "https://";
const std::string READ_ACCESS_PATH = "mpro/editor/image_overlay/view";
const std::string CREATE_ACCESS_PATH = "mpro/editor/image_overlay/create";
const std::string READ_ACCESS_PATH_PUBLIC = "mpro/editor/image_overlay_public/view";
const std::string CREATE_ACCESS_PATH_PUBLIC = "mpro/editor/image_overlay_public/create";

const size_t MAX_IMAGE_SIZE_X_Y = 2000;


yacare::ThreadPool rendererOverlayThreadPool("rendererOverlayThreadPool", (THREADS * 3) / 2, BACKLOG);
std::optional<NTvmAuth::TTvmClient> g_tvmClient;
std::unique_ptr<acl_utils::CachingAclChecker> g_acl;

std::string metadataJson(const Image& image, const std::string uuid, const yacare::Request& request)
{
   json::Builder builder;
    builder << [&](json::ObjectBuilder object) {
        object["uuid"] = uuid;
        object["height"] = image.height();
        object["width"] = image.width();
        object["url"] = HTTPS + request.env(REQUEST_ENV_HTTP_HOST) + "/source-image/" + uuid;
    };
    return builder.str();
}

void
checkReadAccess(UID uid)
{
    static const SubjectPath path(READ_ACCESS_PATH);
    static const SubjectPath pathPublic(READ_ACCESS_PATH_PUBLIC);
    auto transaction = cfg()->aclPool().slaveTransaction();
    CheckContext globalContext(uid, {}, *transaction, {User::Status::Active});
    if (!path.isAllowed(globalContext) && !pathPublic.isAllowed(globalContext)) {
        WARN() << "User " << uid << " access is forbidden to view image_overlays data.";
        throw yacare::errors::Forbidden();
    }
}

void
checkCreateAccess(UID uid)
{
    static const SubjectPath path(CREATE_ACCESS_PATH);
    static const SubjectPath pathPublic(CREATE_ACCESS_PATH_PUBLIC);
    auto transaction = cfg()->aclPool().slaveTransaction();
    CheckContext globalContext(uid, {}, *transaction, {User::Status::Active});
    if (!path.isAllowed(globalContext) && !pathPublic.isAllowed(globalContext)) {
        WARN() << "User " << uid << " access is forbidden to create any image_overlays.";
        throw yacare::errors::Forbidden();
    }
}

void checkCachedAclReadAccess(UID uid)
{
    if (!g_acl->userHasPartOfPermission(uid, READ_ACCESS_PATH) &&
        !g_acl->userHasPartOfPermission(uid, READ_ACCESS_PATH_PUBLIC))
    {
        WARN() << "User " << uid << " access is forbidden to view any image_overlays tiles.";
        throw yacare::errors::Forbidden();
    }
}
} // namespace


yacare::VirtualHost vhost {
    // for future nginx config
    yacare::VirtualHost::SLB { "core-nmaps-renderer-overlay" },
    yacare::VirtualHost::FQDN { "core-nmaps-renderer-overlay.crowdtest.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "core-nmaps-renderer-overlay.testing.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "01.core-nmaps-renderer-overlay.testing.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "02.core-nmaps-renderer-overlay.testing.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "03.core-nmaps-renderer-overlay.testing.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "04.core-nmaps-renderer-overlay.testing.maps.yandex.ru" },
    yacare::VirtualHost::FQDN { "core-nmaps-renderer-overlay-unstable.maps.n.yandex.ru" },
    yacare::VirtualHost::FQDN { "01.core-nmaps-renderer-overlay-unstable.maps.n.yandex.ru" },
    yacare::VirtualHost::FQDN { "02.core-nmaps-renderer-overlay-unstable.maps.n.yandex.ru" },
    yacare::VirtualHost::FQDN { "03.core-nmaps-renderer-overlay-unstable.maps.n.yandex.ru" },
    yacare::VirtualHost::FQDN { "04.core-nmaps-renderer-overlay-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;

YCR_QUERY_PARAM(images, std::string);
YCR_QUERY_PARAM(branch, revision::DBID);
YCR_QUERY_PARAM(token, std::string);

YCR_QUERY_CUSTOM_PARAM((), objectId, revision::DBID, YCR_DEFAULT(0))
{
    return yacare::impl::parseArg(dest, request, "object-id", true);
}


YCR_RESPOND_TO("POST /source-image",
    userId,
    YCR_IN_POOL(rendererOverlayThreadPool),
    YCR_USING(yacare::NginxLimitBody<yacare::ConfigScope::Endpoint>{}.maxSize(5_MB).bufferSize(5_MB)))
{
    response[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON;
    checkCreateAccess(userId);
    try {
        const auto fileData = base64Decode(request.body());
        Image image(fileData);
        image.fitTo(MAX_IMAGE_SIZE_X_Y);
        const auto uuid = storage()->store(image);
        response << metadataJson(image, uuid, request);
    } catch (const std::exception& ex) {
        ERROR() << ex.what();
        throw yacare::Error(400);
    }
}

YCR_RESPOND_TO("GET /source-image/$/meta",
    YCR_IN_POOL(rendererOverlayThreadPool),
    userId)
{
    response[CONTENT_TYPE_HEADER] = CONTENT_TYPE_JSON;
    checkReadAccess(userId);
    const auto& uuid = pathnameParam<std::string>(0);
    DEBUG() << "User " << userId << " accessing " << uuid  << " meta";
    response << metadataJson(*storage()->read(pathnameParam<std::string>(0)), uuid, request);
}

YCR_RESPOND_TO("GET /source-image/$",
    YCR_IN_POOL(rendererOverlayThreadPool),
    userId)
{
    response[CONTENT_TYPE_HEADER] = CONTENT_TYPE_PNG;
    auto imgId = pathnameParam<std::string>(0);
    if (imgId.empty()) {
        throw yacare::errors::BadRequest() << "Missing image id parameter";
    }
    checkReadAccess(userId);
    response << storage()->read(imgId)->pngData();
}

namespace {

void handleTileRequest(
    const yacare::Request& request,
    revision::DBID branchId,
    const std::string& token,
    revision::DBID objectId,
    const maps::tile::Tile& tile,
    const std::string& images,
    uint64_t userId,
    yacare::Response& response)
{
    response[CONTENT_TYPE_HEADER] = CONTENT_TYPE_PNG;
    checkCachedAclReadAccess(userId);
    std::optional<Polygon2> cutOffPolygon;
    if (!request.input().at("geometry").empty()) {
        const auto geomJson = json::Value::fromString(request.input().at("geometry"));
        cutOffPolygon =
            geolib3::convertGeodeticToMercator(
                geolib3::readGeojson<Polygon2>(geomJson));
    }
    if (objectId) {
        cutOffPolygon = getObjectGeometry(
            cfg()->viewPool(branchId),
            branchId,
            token,
            objectId);
    }
    Tile mapTile(
        TileCoord(tile.x(), tile.y()), tile.z());
    if (cutOffPolygon) {
        const auto tileMercatorBox = mapTile.mercatorBox();
        geolib3::BoundingBox tileBoundingBox(
            {tileMercatorBox.lt().x(), tileMercatorBox.lt().y()},
            {tileMercatorBox.rb().x(), tileMercatorBox.rb().y()});
        if (!spatialRelation(tileBoundingBox, *cutOffPolygon, SpatialRelation:: Intersects)) {
            return;
        }
    }
    ProfileTimer setupTimer;
    const auto overlayImageInfoByZOrder = readFromImagesArgByZOrder(
            images,
            tile.z()
        );
    const auto setupElapsed = setupTimer.getElapsedTime();
    ProfileTimer renderTimer;
    response << renderTile(mapTile, overlayImageInfoByZOrder, cutOffPolygon).pngData();
    const auto renderElapsed = renderTimer.getElapsedTime();
    DEBUG() << "ProfileTimer setup: " << setupElapsed << " render: " << renderElapsed;
}

} // namespace

YCR_RESPOND_TO("GET /tile", YCR_IN_POOL(rendererOverlayThreadPool),
    tile,
    images,
    userId,
    branch = revision::TRUNK_BRANCH_ID,
    token = std::string(),
    objectId = 0)
{
    handleTileRequest(request, branch, token, objectId, tile, images, userId, response);
}

YCR_MAIN(argc, argv)
{
    try {
        maps::cmdline::Parser parser;
        auto config = parser.string("config")
            .help("path to configuration");
        auto services = parser.string("services")
            .help("path to services configuration");
        parser.parse(argc, argv);
        const std::string cfgPath =
            config.defined()
                ? config
                : DEFAULT_CONFIG_PATH;
        const std::string servicesCfgPath =
            services.defined()
                ? services
                : DEFAULT_SERVICES_CONFIG_PATH;

        INFO() << "USING CONFIG: " << cfgPath;
        INFO() << "USING SERVICES CONFIG: " << servicesCfgPath;
        ConfigScope cfgScope(cfgPath, servicesCfgPath);
        StorageScope storageScope(*cfg());
        g_acl = std::make_unique<acl_utils::CachingAclChecker>(
            cfg()->aclPool(),
            ACL_CACHE_MAX_SIZE,
            ACL_CACHE_EXPIRATION_PERIOD);
        maps::log8::setLevel(cfg()->logLevel());
        INFO() << "Enable TVM suppport";
        auto tvmtoolSettings = maps::auth::TvmtoolSettings();
        tvmtoolSettings.selectClientAlias("renderer-overlay");
        g_tvmClient = tvmtoolSettings.makeTvmClient();
        yacare::tvm::configureUserAuth(maps::auth::BlackboxApi(tvmtoolSettings));
        yacare::run(yacare::RunSettings{.useSystemDefaultLocale = true});
        return EXIT_SUCCESS;
    } catch (maps::Exception& e) {
        std::cerr << e << std::endl;
    } catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return EXIT_FAILURE;
}
