#include "ad_geometry_builder.h"

#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/data_error.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/helpers.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/json2ymapsdf/lib/common/log.h>

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/profiletimer.h>
#include <yandex/maps/wiki/common/retry_duration.h>

#include <cstdint>
#include <sstream>
#include <string>

namespace maps::wiki::json2ymapsdf::isocode {

namespace table {
const std::string AD = "ad";
const std::string AD_FACE = "ad_face";
const std::string AD_GEOM = "ad_geom";
const std::string EDGE = "edge";
const std::string FACE_EDGE = "face_edge";
const std::string BORDERS = "ad_geometry_builder_borders";
} // namespace table

namespace column {
const std::string PERIMETER = "perimeter";
const std::string BORDER_LENGTH = "border_length";
} // namespace column


AdGeometryBuilder::AdGeometryBuilder(
        pgpool3::Pool& pool,
        const std::string& inSchema,
        const std::string& outSchema)
    : pool_(pool)
    , inSchema_(inSchema)
    , outSchema_(outSchema)
{
    REQUIRE(isSchemaExist(inSchema),
            "Input schema: '" + inSchema + "' does not exist.");
    REQUIRE(isTableExist(inSchema, table::AD),
            "Table: '" + inSchema + "." + table::AD + "' does not exist.");
    REQUIRE(isTableExist(inSchema, table::AD_FACE),
            "Table: '" + inSchema + "." + table::AD_FACE + "' does not exist.");
    REQUIRE(isTableExist(inSchema, table::FACE_EDGE),
            "Table: '" + inSchema + "." + table::FACE_EDGE + "' does not exist.");
    REQUIRE(isTableExist(inSchema, table::EDGE),
            "Table: '" + inSchema + "." + table::EDGE + "' does not exist.");

    REQUIRE(isSchemaExist(outSchema),
            "Output schema: '" + outSchema + "' does not exist.");
    REQUIRE(isTableExist(outSchema, table::AD_GEOM),
            "Table: '" + outSchema + "." + table::AD_GEOM + "' does not exist.");
}


void AdGeometryBuilder::build(size_t levelKind)
{
    ASSERT(levelKind >= 1 && levelKind <= 7);

    ProfileTimer buildTimer;
    auto query = buildQuery(levelKind);

    try {
        execCommitWithRetries(pool_, "AdGeometryBuilder", query);
    } catch (const common::RetryDurationExpired& ex) {
        ERROR() << "Failed to build geometry for ad with level_kind = " << levelKind
                << ": " << ex.what();
        throw;
    } catch (const std::exception& ex) {
        DATA_ERROR() << "Failed to build geometry for ad with level_kind = " << levelKind
                     << ": " << ex.what();
        throw;
    }
    INFO() << "Geometry build time: " << buildTimer;
}


void AdGeometryBuilder::check(size_t levelKind, double perimeterTolerance)
{
    ASSERT(levelKind >= 1 && levelKind <= 7);
    ASSERT(perimeterTolerance > 0 && perimeterTolerance < 1);

    bool error{false};

    ProfileTimer checkTimer;
    auto rows = readWithRetries(pool_, checkQuery(levelKind));

    for (const auto& row: rows) {
        double perimeter = row[column::PERIMETER].as<double>();
        double borderLength = row[column::BORDER_LENGTH].as<double>();
        // The check below must be synchronized with the check from
        // garden/modules/geometry_collector/pymod/yandex/maps/garden/modules/geometry_collector/queries.py:build_area_query
        if (perimeter < borderLength * perimeterTolerance) {
            error = true;
            DATA_ERROR()
                << "The resulting (multi)polygon has much smaller perimeter: " << perimeter
                << " than sum of the edges lengths: " << borderLength << "; "
                << "possibly, some of the input edges form invalid boundary. "
                << "Check AD: " << row["ad_id"].as<int64_t>();
        }
    }
    INFO() << "Geometry check time: " << checkTimer;

    DATA_REQUIRE(!error, "Geometry check is failed.");
}


std::string AdGeometryBuilder::buildQuery(size_t levelKind) const
{
    std::stringstream result;

    result
        << "INSERT INTO " << outSchema_ << "." << table::AD_GEOM << "\n"
        << "SELECT\n"
        << "  ad_id,\n"
        << "  ST_BuildArea(ST_LineMerge(shape)) AS shape\n"
        << "FROM (\n"
        << collectBordersQuery(levelKind) << "\n) AS " << table::BORDERS;

    DEBUG() << printScissors("Geometry build query", result.str());

    return result.str();
}


std::string AdGeometryBuilder::checkQuery(size_t levelKind) const
{
    std::stringstream result;

    result
        << "SELECT\n"
        << "  ad_id,\n"
        << "  ST_Perimeter(" << table::AD_GEOM << ".shape) AS " << column::PERIMETER << ",\n"
        << "  ST_Length("    << table::BORDERS << ".shape) AS " << column::BORDER_LENGTH << "\n"
        << "FROM " << outSchema_ << "." << table::AD_GEOM << "\n"
        << "LEFT JOIN (\n"
        << collectBordersQuery(levelKind) << "\n) AS " << table::BORDERS << " USING(ad_id)\n"
        << "LEFT JOIN " << inSchema_ << "." << table::AD << " USING(ad_id)\n"
        << "WHERE " << levelKindClause(levelKind);

    DEBUG() << printScissors("Check perimeter query", result.str());

    return result.str();
}


std::string AdGeometryBuilder::collectBordersQuery(size_t levelKind) const
{
    std::stringstream result;

    result
        << "SELECT\n"
        << "  ad_id,\n"
        << "  ST_Collect(shape) AS shape\n"
        << "FROM " << inSchema_ << "." << table::AD << "\n"
        << "  LEFT JOIN " << inSchema_ << "." << table::AD_FACE << " USING(ad_id)\n"
        << "  LEFT JOIN " << inSchema_ << "." << table::FACE_EDGE << " USING(face_id)\n"
        << "  LEFT JOIN " << inSchema_ << "." << table::EDGE << " USING(edge_id)\n"
        << "WHERE " << levelKindClause(levelKind) << "\n"
        << "GROUP BY ad_id";

    return result.str();
}


std::string AdGeometryBuilder::levelKindClause(size_t levelKind)
{
    return "level_kind = " + std::to_string(levelKind) + " AND g_ad_id IS NULL";
}


bool AdGeometryBuilder::isSchemaExist(const std::string& schema)
{
    const std::string query =
        "SELECT 1 "
        "FROM information_schema.schemata "
        "WHERE schema_name = '" + schema + "';";

    auto result = readWithRetries(pool_, query);
    return !result.empty();
}


bool AdGeometryBuilder::isTableExist(const std::string& schema, const std::string& table)
{
    const std::string query =
        "SELECT 1 "
        "FROM information_schema.tables "
        "WHERE table_schema = '" + schema + "'"
        "  AND table_name = '" + table + "';";

    auto result = readWithRetries(pool_, query);
    return !result.empty();
}

} // namespace maps::wiki::json2ymapsdf::isocode
