#include "region_builder.h"

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

#include <maps/libs/common/include/environment.h>
#include <maps/libs/log8/include/log8.h>

#include <yandex/maps/wiki/common/string_utils.h>
#include <boost/algorithm/string/split.hpp>

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

namespace {

bool inUnittest()
{
    return maps::common::getYandexEnvironment() == maps::common::Environment::Unittest;
}

std::vector<std::string>
splitByComma(const std::string& values)
{
    std::vector<std::string> splitValues;
    boost::split(
        splitValues,
        values,
        std::bind(std::equal_to<char>(), std::placeholders::_1, ','));

    return splitValues;
}


void
updateRegionStatus(
    pgpool3::Pool& pool,
    const std::string& schema,
    const Region& region)
{
    std::stringstream query;
    query << "SET search_path TO " << schema << ", public; "
        << "UPDATE meta_param AS status_param "
        << "SET value = '" << toString(region.status) << "' "
        << "FROM meta_param AS id_param "
        << "WHERE id_param.meta_id = status_param.meta_id "
        << "AND status_param.key = 'status' "
        << "AND status_param.value != '" << toString(region.status) << "' "
        << "AND id_param.key = 'id' "
        << "AND id_param.value = '" << region.id << "';";

    execCommitWithRetries(pool, "updateRegionStatus: " + region.id, query.str());
}


Regions
loadAllRegions(
    pgpool3::Pool& pool,
    const std::string& schema)
{
    std::stringstream query;
    query << "SET search_path TO " << schema << ", public; "
        << "SELECT "
            << "id_param.value AS id, "
            << "isocodes_param.value AS isocodes, "
            << "( "
                << "SELECT string_agg(ad.isocode, ',') "
                << "FROM ad_isocode "
                << "JOIN ad_excl ON(ad_isocode.ad_id = e_ad_id) "
                << "JOIN ad USING(ad_id) "
                << "WHERE ad_isocode.isocode BETWEEN '500' AND '999' "
                << "AND ad_excl.t_ad_id = meta.meta_id "
                << "AND ad.level_kind = 1 "
                << "GROUP BY id_param.value "
            << ") AS ad_neutral_codes, "
            << "status_param.value AS status "
        << "FROM meta "
        << "JOIN meta_param AS id_param "
            << "ON(id_param.meta_id = meta.meta_id AND id_param.key = 'id') "
        << "JOIN meta_param AS isocodes_param "
            << "ON(isocodes_param.meta_id = meta.meta_id AND isocodes_param.key = 'isocodes') "
        << "JOIN meta_param AS status_param "
            << "ON(status_param.meta_id = meta.meta_id AND status_param.key = 'status') ";

    const auto rows = readWithRetries(pool, query.str());

    Regions regions;
    for(const auto& row: rows) {
        auto isocodes = splitByComma(row["isocodes"].as<std::string>());
        auto adNeutralCodes = splitByComma(row["ad_neutral_codes"].as<std::string>(""));
        isocodes.insert(isocodes.end(), adNeutralCodes.begin(), adNeutralCodes.end());
        regions.emplace_back(Region{
            row["id"].as<std::string>(),
            isocodes,
            toRegionStatus(row["status"].as<std::string>())
        });
    }
    return regions;
}

void
buildRegionsInternal(pqxx::transaction_base& work)
{
    auto execQuery = [&](const auto& query, const auto& context) {
        work.exec(query.str());
        INFO() << "Pass buildRegions (" << context << ")";
    };

    std::stringstream queryMetaParamNames;
    queryMetaParamNames
        << "INSERT INTO meta_param (meta_param_id, meta_id, key, value) "
        << "SELECT nm_id, ad_id, 'id', name "
        << "FROM ad_nm "
        << "JOIN ad AS region USING(ad_id) "
        << "WHERE region.level_kind = 0 "
        << "AND name_type = 0 "
        << "AND lang = 'en';";
    execQuery(queryMetaParamNames, "fill meta_param, names");

    std::stringstream queryCountries;
    queryCountries
        << "CREATE TEMPORARY TABLE countries AS ("
            << "SELECT meta_id AS region_id, ad_id, isocode "
            << "FROM ad "
            << "JOIN ad_excl ON (ad_id = e_ad_id) "
            << "JOIN meta ON (meta_id = t_ad_id) "
            << "WHERE level_kind = 1 "
            << "AND g_ad_id IS NULL "
            << "UNION "
            << "SELECT meta_id, slave_subst.ad_id, slave_subst.isocode "
            << "FROM ad master_ad "
            << "JOIN ad slave_ad ON (slave_ad.p_ad_id = master_ad.ad_id) "
            << "JOIN ad slave_subst ON (slave_subst.g_ad_id = slave_ad.ad_id AND slave_subst.level_kind = 1) "
            << "JOIN ad_excl ON (master_ad.ad_id = e_ad_id) "
            << "JOIN meta ON (meta_id = t_ad_id) "
            << "WHERE master_ad.level_kind = 1 "
            << "AND master_ad.g_ad_id IS NULL"
        << ")";
    execQuery(queryCountries, "create countries");

    std::stringstream queryMetaParamIsocodes;
    queryMetaParamIsocodes
        << "INSERT INTO meta_param (meta_param_id, meta_id, key, value) "
        << "SELECT "
            << idManager().uniqueDBID() << ", "
            << "region_id, "
            << "'isocodes', "
            << "'001,' || string_agg(isocode, ',') "
        << "FROM countries "
        << "WHERE (isocode < '500' OR isocode > '999') AND isocode != 'XZ' AND isocode != '001' "
        << "GROUP BY region_id;"

        << "DROP TABLE countries;";
    execQuery(queryMetaParamIsocodes, "fill meta_param, isocodes");


    std::stringstream queryLandGeom;
    queryLandGeom
        << "CREATE TEMPORARY TABLE land_geom AS ("
            << "SELECT ST_Union(ad_geom.shape) AS shape "
            << "FROM ad "
            << "JOIN ad_geom USING (ad_id)"
            << "WHERE (isocode < '500' OR isocode > '999') AND isocode != 'XZ' AND isocode != '001' "
        << ");";
    execQuery(queryLandGeom, "create land_geom");

    std::stringstream queryAdGeom;
    queryAdGeom
        << "UPDATE ad_geom "
        << "SET shape = ST_Difference(ad_geom.shape, land_geom.shape) "
        << "FROM land_geom "
        << "WHERE ad_id IN ("
            << "SELECT ad_id FROM ad WHERE isocode = 'XZ' OR isocode BETWEEN '500' AND '999'"
        << ");"

        << "DROP TABLE land_geom;";
    execQuery(queryAdGeom, "update ad_geom, water_geom - land_geom");

    std::stringstream queryRegionGeom;
    queryRegionGeom
        << "CREATE TEMPORARY TABLE region_geom AS ("
            << "SELECT "
                << "region.ad_id AS region_id, "
                << "ST_Union(ad_geom.shape) AS shape "
            << "FROM ad region "
            << "LEFT JOIN ad_excl ON (region.ad_id = t_ad_id)"
            << "LEFT JOIN ad_geom ON (ad_geom.ad_id = e_ad_id)"
            << "WHERE region.level_kind = 0 "
            << "GROUP BY 1 "
        << ");";
    execQuery(queryRegionGeom, "create region_geom");

    std::stringstream queryMeta;
    queryMeta
        << "UPDATE meta SET shape = region_geom.shape "
        << "FROM region_geom "
        << "WHERE region_geom.region_id = meta.meta_id; "

        << "DROP TABLE region_geom;";
    execQuery(queryMeta, "update meta, fill geometry for regions");
}

} // namespace


void
buildRegions(
    pgpool3::Pool& pool,
    const std::string& schema)
{
    INFO() << "Start buildRegions";

    execCommitWithRetries(
        pool,
        "buildRegions",
        "SET search_path TO " + schema + ", public",
        buildRegionsInternal);

    INFO() << "Finished buildRegions";
}


void
checkRegions(
    pgpool3::Pool& pool,
    const std::string& schema)
{
    std::vector<DBID> invalidRegions;
    {
        const std::string regionWithoutNameQuery =
            "SET search_path TO " + schema + ", public; "
            "SELECT status_param.meta_param_id "
            "FROM meta_param AS status_param "
            "LEFT JOIN meta_param AS id_param "
                "ON (id_param.meta_id = status_param.meta_id AND id_param.key = 'id') "
            "WHERE status_param.key = 'status' "
            "AND id_param.value IS NULL;";
        const auto rows = readWithRetries(pool, regionWithoutNameQuery);
        for (const auto& row: rows) {
            invalidRegions.push_back(row[0].as<DBID>() );
            DATA_ERROR() << "Region oid = " << row[0].as<std::string>() << " has no official en name";
        }
    }
    {
        const std::string regionWithoutCountriesQuery =
            "SET search_path TO " + schema + ", public; "
            "SELECT status_param.meta_param_id "
            "FROM meta_param AS status_param "
            "LEFT JOIN meta_param AS isocodes_param "
                "ON (isocodes_param.meta_id = status_param.meta_id AND isocodes_param.key = 'isocodes') "
            "WHERE status_param.key = 'status' "
            "AND isocodes_param.value IS NULL;";
        const auto rows = readWithRetries(pool, regionWithoutCountriesQuery);
        for (const auto& row: rows) {
            invalidRegions.push_back(row[0].as<DBID>() );
            DATA_ERROR() << "Region oid = " << row[0].as<std::string>() << " has no assigned countries";
        }
    }

    if (invalidRegions.empty()) {
        return;
    }

    std::stringstream query;
    query << "SET search_path TO " << schema << ", public; "
        << "UPDATE meta_param "
        << "SET value = '" << toString(Region::Status::Failed) << "' "
        << "WHERE meta_param.meta_param_id IN ("
        << common::join(invalidRegions, ",")
        << ");";
    execCommitWithRetries(pool, "checkRegions", query.str());
}


Regions
loadRegions(
    pgpool3::Pool& pool,
    const std::string& schema,
    const RegionIds& regionIds)
{
    auto regions = loadAllRegions(pool, schema);
    if (!regionIds.empty()) {
        std::map<RegionId, Region> regionMap;
        for (const auto& region: regions) {
            regionMap[region.id] = region;
        }

        regions.clear(); // will be ordered by regionIds

        auto needOverwriteExportStatus = inUnittest();

        for (const auto& regionId: regionIds) {
            auto it = regionMap.find(regionId);
            if (it != regionMap.end()) {
                auto& region = it->second;
                if (needOverwriteExportStatus) {
                    region.status = Region::Status::Export;
                }
                regions.push_back(std::move(region));
                regionMap.erase(it);
            }
        }

        for (auto& [_, region] : regionMap) {
            region.status = Region::Status::Skip;
            regions.push_back(std::move(region));
        }
    }

    for (const auto& region : regions) {
        INFO() << region.info();
    }
    return regions;
}


void
updateRegionsStatus(
    pgpool3::Pool& pool,
    const std::vector<std::string>& regionSchemas,
    const Regions& regions)
{
    for (const auto& schema: regionSchemas) {
        for (const auto& region: regions) {
            updateRegionStatus(pool, schema, region);
        }
    }
}

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