#include <maps/wikimap/mapspro/services/renderer/src/data_sets/include/data_set.h>
#include "feature_iterator.h"

#include <maps/renderer/libs/base/include/string_util.h>
#include <maps/renderer/libs/base/include/wallclock_timer.h>
#include <maps/renderer/libs/data_sets/data_set/include/special_attributes.h>
#include <maps/renderer/libs/feature/include/yandex/maps/renderer/feature/attributes.h>
#include <maps/renderer/libs/postgres/include/postgres.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/infra/yacare/include/error.h>

#include <contrib/libs/yaml-cpp/include/yaml-cpp/yaml.h>
#include <util/string/subst.h>
#include <util/string/join.h>
#include <util/generic/algorithm.h>

#include <pqxx/pipeline>

#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>

#include <iostream>
#include <optional>

using namespace maps::renderer;

namespace maps::wiki::renderer {

namespace {

void addColumns(const SourceLayer& layer, std::stringstream& query)
{
    query << "SELECT ST_AsBinary(" << layer.geometryExpression << ") as "
          << layer.geometryColumn;

    if (!layer.schema.properties.contains(layer.featureId))
        query << ", " << layer.featureId;

    if (layer.zoomRange) {
        query << ", " << layer.zoomRange->first << ", "
              << layer.zoomRange->second;
    }

    if (layer.zlevel) {
        query << ", " << layer.zlevel->first << ", "
              << layer.zlevel->second;
    }

    for (const auto& [name, expr] : layer.propertyExpressions) {
        if (expr) {
            query << ", " << expr.value() << " as " << postgres::quoteIdentifier(name);
        } else {
            query << ", " << postgres::quoteIdentifier(name);
        }
    }
}

void substitute(
    std::string* text,
    const std::string& sourceLayerId,
    const SubstitutionData& data)
{
    auto substitute = [](std::string* text, const auto& substitutions) {
        for (const auto& [placeholder, value] : substitutions) {
            SubstGlobal(*text, placeholder, value);
        }
    };
    auto it = data.sources.find(sourceLayerId);
    if (it != data.sources.end()) {
        substitute(text, it->second);
    }
    substitute(text, data.global);
}

void addTable(
    const SourceLayer& layer,
    std::stringstream& query)
{
    std::string table = layer.table;
    query << " FROM " << table << " as SOME_TMP_TABLE";
}

std::string makeQuery(
    const SourceLayer& layer,
    const base::ZoomRange& zoomRange,
    const SubstitutionData& substitutionData,
    const base::BoxD& bbox,
    const std::optional<size_t>& objectsQuantityLimit)
{
    std::stringstream query;

    addColumns(layer, query);
    addTable(layer, query);

    query << " WHERE (" << layer.geometryColumn;
    query << std::fixed << " && ST_MakeEnvelope("
          << bbox.x1 << ", " <<  bbox.y1 << ", " << bbox.x2 << ", " << bbox.y2 << ", 3395))";

    if (layer.zoomExpression && zoomRange.min() == zoomRange.max()) {
        query << " AND " << layer.zoomExpression.value();
    }

    if (layer.zoomRange) {
        query << " AND "
              << layer.zoomRange->first
              << " <= " << zoomRange.max()
              << " AND "
              << layer.zoomRange->second
              << " >= " << zoomRange.min();
    }

    if (layer.filterClause) {
        query << " AND (" << layer.filterClause.value() << ")";
    }

    if (objectsQuantityLimit) {
        query << " LIMIT " << objectsQuantityLimit.value();
    }

    auto queryStr = query.str();
    substitute(&queryStr, layer.id, substitutionData);

    return queryStr;
}

std::vector<const SourceLayer*> filterSourceLayers(
    const std::unordered_map<std::string, SourceLayer>& sourceLayers,
    const std::unordered_set<std::string>& filter)
{
    std::vector<const SourceLayer*> result;
    for (const auto& [id, layer] : sourceLayers) {
        if (filter.contains(layer.id)) {
            result.push_back(&layer);
        }
    }
    return result;
}

std::string stripSpaces(const std::string& str)
{
    std::string result;
    for (auto ch : str) {
        if (ch != ' ') {
            result += ch;
        }
    }
    return result;
}

pgpool3::TransactionHandle getTransaction(const AuxData& auxData)
{
    static const std::string SEARCH_PATH = "search_path";
    auto transaction = auxData.getTransaction();
    REQUIRE(transaction, "transaction handle is not valid");
    if (!auxData.searchPath.empty()) {
        auto&& txn = transaction.get();
        auto oldSearchPath = txn.get_variable(SEARCH_PATH);
        if (stripSpaces(oldSearchPath) != stripSpaces(auxData.searchPath)) {
            if (!oldSearchPath.empty()) {
                INFO() << "Overwriting search path. Old: " << oldSearchPath << "; New: " << auxData.searchPath;
            }
            transaction.get().set_variable(SEARCH_PATH, auxData.searchPath);
        }
    }
    return transaction;
}

data_set::LayerView makeLayerView(
    std::shared_ptr<pqxx::result> data,
    const SourceLayer& layer,
    const Transform& vertexTransform,
    base::Zoom zoom)
{
    if (data->empty()) {
        return data_set::LayerView{layer.info};
    }

    return data_set::LayerView{
        layer.info,
        std::make_shared<PostgisFeatureIterable>(
            std::move(data), layer, vertexTransform, zoom)
    };
}

template <typename Strings>
Strings splitWords(const std::string& str, const std::string& delimiter)
{
    Strings result;
    base::splitWords<char>(str, delimiter, std::inserter(result, result.end()));
    return result;
}

typedef std::vector<std::string> SubLayerParams;
typedef std::map<std::string, SubLayerParams> ParsedSubLayers;

ParsedSubLayers parseSubLayers(const std::string& subLayers)
{
    ParsedSubLayers result;

    static const std::string DELIMITER_LAYERS = ",";
    static const std::string DELIMITER_PARAMS = ":";

    auto splittedSubLayers = splitWords<std::vector<std::string>>(subLayers, DELIMITER_LAYERS);

    for (const auto& subLayer : splittedSubLayers) {
        auto subLayerWithParam = splitWords<std::vector<std::string>>(subLayer, DELIMITER_PARAMS);
        ASSERT(!subLayerWithParam.empty());

        const auto layerId = subLayerWithParam.front();
        if (layerId.empty()) {
            continue;
        }
        subLayerWithParam.erase(subLayerWithParam.begin());
        result.emplace(layerId, std::move(subLayerWithParam));
    }
    return result;
}

} // namespace

DataSet::DataSet(const YAML::Node& layersNode, const YAML::Node& groupsNode)
{
    auto parseMapTypes = [] (const YAML::Node& mapTypesNode) {
        std::unordered_set<std::string> result;
        if (mapTypesNode.IsDefined()) {
            for (auto& mapTypeNode : mapTypesNode) {
                result.insert(mapTypeNode.as<std::string>());
            }
        }
        return result;
    };

    auto parseSourceLayers = [&] (const YAML::Node& node) {
        Sources sources;
        for (const auto& sourceLayerNode : node) {
            auto id = sourceLayerNode["id"].as<std::string>();
            auto& source = sources[id];
            source.id = id;
            source.mapTypes = parseMapTypes(node["map_type"]);
        }
        return sources;
    };

    for (const auto& groupNode: groupsNode) {
        auto groupId = groupNode["id"].as<std::string>();
        for (const auto& layerNode : groupNode["layers"]) {
            auto layerId = layerNode["id"].as<std::string>();
            auto layerType = layerNode["type"].as<std::string>("base");
            auto layerSources = parseSourceLayers(layerNode["source_layer"]);
            auto presentationsNode = layerNode["presentations"];
            if (presentationsNode.IsDefined() && presentationsNode.size() > 0) {
                for (const auto& presentationNode : layerNode["presentations"]) {
                    auto id = presentationNode["id"].as<std::string>();
                    auto& subLayer = subLayers_[id];
                    subLayer.id = id;
                    subLayer.layerId = layerId;
                    subLayer.groupId = groupId;
                    subLayer.sources = parseSourceLayers(presentationNode["source_layer"]);
                    subLayer.sources.insert(layerSources.begin(), layerSources.end());
                    subLayer.type = "presentation";
                }
            } else {
                auto& subLayer = subLayers_[layerId];
                subLayer.id = layerId;
                subLayer.layerId = layerId;
                subLayer.groupId = groupId;
                subLayer.sources = layerSources;
                subLayer.type = layerType;
            }
        }
    }

    sourceLayers_ = loadSourceLayers(layersNode);

    data_set::schema::Schema schema;
    for (const auto& [sourceId, layer] : sourceLayers_) {
        info_.layers.push_back(layer.info);
        schema.layers.insert({sourceId, layer.schema});
    }
    schema_ = schema.toString();
}

DataSet::~DataSet() = default;

std::vector<data_set::LayerView> DataSet::queryView(
    data_set::ViewQueryContext& ctx) const
{
    const auto& params = ctx.params;

    const auto& auxParams = params.auxParams;
    ASSERT(auxParams.contains(SUBLAYERS));

    ASSERT(params.auxData.has_value());
    const auto& auxData = std::any_cast<AuxData>(params.auxData);

    auto parsedSubLayers = parseSubLayers(auxParams.at(SUBLAYERS));

    // remove missing or unknown sub layers
    std::erase_if(parsedSubLayers, [&] (const auto& layer) {
        return !subLayers_.contains(layer.first);
    });

    data_set::ViewQueryParams::AcceptedLayers result;
    for (const auto& [layerId, _] : parsedSubLayers) {
        const auto& subLayer = subLayers_.at(layerId);
        for (const auto& [sourceId, _] : subLayer.sources) {
            result.insert(sourceId);
        }
    }

    SubstitutionData substitutionData;

    REQUIRE(params.zoomRange.min() == params.zoomRange.max(), "multizoom tiles are not supported");

    substitutionData.global.insert({
        ZOOM_PLACEHOLDER, std::to_string(params.zoomRange.min())
    });

    std::unordered_set<std::string> layersIds;
    for (const auto& [layerId, _] : parsedSubLayers) {
        layersIds.insert("'" + layerId + "'");
    }

    substitutionData.global.insert({
        LAYERS_PLACEHOLDER, base::join(layersIds, ",")
    });

    for (const auto& [layerId, params] : parsedSubLayers) {
        const auto& subLayer = subLayers_.at(layerId);
        if (subLayer.type == "filter" && !params.empty()) {
            auto filter = auxData.getFilter(params.front());
            for (const auto& [sourceId, _] : subLayer.sources) {
                substitutionData.sources[sourceId].insert({
                    FILTER_PLACEHOLDER, filter
                });
            }
        }
    }

    for (const auto& [layerId, params] : parsedSubLayers) {
        const auto& subLayer = subLayers_.at(layerId);
        if (subLayer.type == "selection" && !params.empty()) {
            for (const auto& param : params) {
                REQUIRE(
                    std::all_of(param.begin(), param.end(), ::isdigit),
                    yacare::errors::BadRequest() <<
                        "SubLayer parameter is not integer: " << param);
            }
            for (const auto& [sourceId,_] : subLayer.sources) {
                const auto& sourceLayer = sourceLayers_.at(sourceId);

                auto nParams = params.size();
                auto needSelectionFilterClause2 =  (nParams > 1) && sourceLayer.selectionFilterClause2;
                REQUIRE(sourceLayer.selectionFilterClause1 || needSelectionFilterClause2,
                        yacare::errors::BadRequest() <<
                        "selection layer filter clause not found for source layer: " << sourceId);

                boost::format pfcFormatter(
                    needSelectionFilterClause2
                ? *sourceLayer.selectionFilterClause2
                : *sourceLayer.selectionFilterClause1);

                if (needSelectionFilterClause2){
                    pfcFormatter
                        % params[0]
                        % std::string(
                             boost::algorithm::join(
                                 boost::make_iterator_range(
                                     std::next(params.begin()),params.end()), ",")
                             );
                } else if (nParams == 1) {
                    pfcFormatter % params[0];
                } else {
                    std::string arraySql = " ANY (ARRAY[" + boost::algorithm::join(params, ",") + "])";
                    pfcFormatter % arraySql;
                }

                const auto filter = pfcFormatter.str();

                substitutionData.sources[sourceId].insert({
                    FILTER_PLACEHOLDER, filter
                });
            }
        }
    }

    for (const auto& [_, source] : sourceLayers_) {
        if (result.contains(source.id) && !substitutionData.sources.contains(source.id)) {
            if (source.table.find(FILTER_PLACEHOLDER) != std::string::npos) {
                substitutionData.sources[source.id].insert({
                    FILTER_PLACEHOLDER, "FALSE"
                });
            }
        }
    }

    return queryViewInternal(ctx, substitutionData, result);
}

std::vector<data_set::LayerView> DataSet::queryViewInternal(
    data_set::ViewQueryContext& ctx,
    const SubstitutionData& substitutionData,
    const data_set::ViewQueryParams::AcceptedLayers& layers) const
{
    const auto& params = ctx.params;

    ASSERT(params.zoomRange.isValid());
    ASSERT(params.zoomRange.max() <= base::MAX_ZOOM);

    const auto& auxData = std::any_cast<AuxData>(params.auxData);

    auto trHandle = getTransaction(auxData);

    std::vector<data_set::LayerView> result;

    base::WallclockTimer pipelineTimer;

    pqxx::transaction_base& transaction = *trHandle;
    pqxx::pipeline pipeline(transaction);

    auto vertexTransform = [shiftVertex = auxData.shiftGeometry](base::PointD& vtx) {
        vtx += shiftVertex;
    };

    std::unordered_map<pqxx::pipeline::query_id, const SourceLayer*> queryLayers;
    for (const auto* layer : filterSourceLayers(sourceLayers_, layers)) {
        if (layer->minzoom > params.zoomRange.max()) {
            continue;
        }
        auto bbox = params.layerBBox(layer->id);
        bbox -= auxData.shiftGeometry;

        auto query = makeQuery(
            *layer, params.zoomRange, substitutionData, bbox, auxData.objectsQuantityLimit);

        auto queryId = pipeline.insert(query);
        queryLayers.insert({queryId, layer});
    }

    while (!pipeline.empty()) {
        const auto& [queryId, pqxxResult] = pipeline.retrieve();
        auto data = std::make_shared<pqxx::result>(pqxxResult);

        auto it = queryLayers.find(queryId);
        if (it != queryLayers.end()) {
            const auto& layer = *(it->second);
            auto view = makeLayerView(
                std::move(data), layer, vertexTransform,
                params.zoomRange.min());
            result.push_back(std::move(view));
        }
    }
    INFO() << "queryView took: " << pipelineTimer.elapsedMs() << " ms";

    return result;
}

const data_set::DataSetInfo& DataSet::info() const
{
    return info_;
}

std::optional<std::string> DataSet::schema() const
{
    return schema_;
}

} // namespace maps::wiki::renderer
