#include "ad_geometry_builder.h"

#include "exit_codes.h"

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

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


namespace maps {
namespace wiki {
namespace ad_geometry_builder {

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 column {
const std::string PERIMETER = "perimeter";
const std::string BORDER_LENGTH = "border_length";
}


AdGeometryBuilder::AdGeometryBuilder(
    const std::string& connectionStr,
    const std::string& inSchema,
    const std::string& outSchema)
try : conn_{connectionStr}
    , 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.");
}
catch (const pqxx::broken_connection& e) {
    FATAL() << e.what();
    exit(EXITCODE_POSTGRESQL_BROKEN_CONNECTION);
}


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

    pqxx::work txn(conn_);
    ProfileTimer buildTimer;
    txn.exec(buildQuery(levelKind));
    txn.commit();
    DEBUG() << "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};
    pqxx::work txn(conn_);

    ProfileTimer checkTimer;
    for (const auto& row: txn.exec(checkQuery(levelKind))) {
        double perimeter = row[column::PERIMETER].as<double>();
        double border_length = 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 < border_length * perimeterTolerance) {
            error = true;
            ERROR() << "The resulting (multi)polygon has much smaller perimeter: " << perimeter
                    << " than sum of the edges lengths: " << border_length << "; "
                    << "possibly, some of the input edges form invalid boundary. "
                    << "Check AD: " << row["ad_id"].as<int64_t>();
        }
    }
    DEBUG() << "Geometry check time: " << checkTimer;

    if (error) {
        throw RuntimeError("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) << ") AS " << table::BORDERS;

    DEBUG() << "Geometry build query:\n"
            << "-8<-----------------------------------------------------------------------------\n"
            << result.str() << "\n"
            << "----------------------------------------------------------------------------->8-";

    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) << ") AS " << table::BORDERS << " USING(ad_id)";

    DEBUG() << "Check perimeter query:\n"
            << "-8<-----------------------------------------------------------------------------\n"
            << result.str() << "\n"
            << "----------------------------------------------------------------------------->8-";

    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\n"
        << "  level_kind = " << levelKind << "\n"
        << "  AND\n"
        << "  g_ad_id IS NULL\n"
        << "GROUP BY ad_id";

    return result.str();
}


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

    return txn.exec(query).empty() ? false : true;
}


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

    return txn.exec(query).empty() ? false : true;
}


} // namespace ad_geometry_builder
} // namespace wiki
} // namespace maps
