#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/tnc2ymapsdf/builder.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/tnc2ymapsdf/common.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/tnc2ymapsdf/names.h>
#include <maps/wikimap/mapspro/tools/ymapsdf-conversion/shp2ymapsdf/database.h>

#include <maps/libs/ymapsdf/include/ad.h>
#include <maps/libs/ymapsdf/include/ft.h>

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

#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/serialization.h>

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

#include <yandex/maps/wiki/common/pg_utils.h>
#include <yandex/maps/wiki/common/rd/speed_cat.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <vector>
#include <unordered_map>
#include <unordered_set>


namespace maps {
namespace wiki {
namespace tnc2ymapsdf {

namespace {

const std::string ID = "id";
const std::string IDS = "ids";
const std::string CITY_ID = "city_id";
const std::string F_NODE_ID = "f_node_id";
const std::string T_NODE_ID = "t_node_id";
const std::string KIND = "kind";
const std::string STREET_ID = "street_id";
const std::string SHAPE = "shape";
const std::string CENTER = "center";


const std::unordered_map<int, int> RSHAPE_TO_FOW {
    {0,0}, {1,4}, {2,16}, {3,2}
};

inline auto getFow(int rshape)
{
    ASSERT(rshape <= 3);
    return RSHAPE_TO_FOW.at(rshape);
}

inline int getFc(int rclass)
{
    switch (rclass) {
        case 1: return 1;
        case 2: return 2;
        case 3: return 3;
        case 4: return 4;
        case 5: return 4;
        case 6: return 5;
        case 7: return 6;
        case 8: return 7;
        case 9: return 9;
        case 10: return 9;
        default: return 10;
    }
}

inline auto getSpeedCat(int fc, int paved)
{
    return common::predictSpeedCategory(fc, common::FOW::None, paved > 0);
}


class Builder
{
public:
    Builder(const std::string& connStr,
            const std::string& srcSchema,
            const std::string& dstSchema)
        : db_(connStr, srcSchema, dstSchema)
    {
        adId_ = db_.maxId("ad");
        nodeId_ = db_.maxId("node");
        faceId_ = db_.maxId("face");
        edgeId_ = db_.maxId("edge");
        bldId_ = db_.maxId("bld");
        ftId_ = db_.maxId("ft");

        std::vector<Id> ids {
            db_.maxId("ad_nm", "nm_id"),
            db_.maxId("rd_nm", "nm_id"),
            db_.maxId("ft_nm", "nm_id"),
            db_.maxId("addr_nm", "nm_id") };
        nmId_ = *(std::max_element(ids.begin(), ids.end()));
    }

    void run(const std::unordered_set<std::string>& stages)
    {

        if (stages.count(STAGE_ALL) || stages.count(STAGE_ATD)) {
            processAd();
        }
        if (stages.count(STAGE_ALL) || stages.count(STAGE_ROADS)) {
            processStreets();
            processRdJc();
            processRoads();
            processRoutes();
            processConds();
        }
        if (stages.count(STAGE_ALL) || stages.count(STAGE_ADDRS)) {
            processAddrs();
        }
        if (stages.count(STAGE_ALL) || stages.count(STAGE_HYDRO)) {
            processHydro();
        }
        if (stages.count(STAGE_ALL) || stages.count(STAGE_VEGETATION)) {
            processVegetation();
        }
        if (stages.count(STAGE_ALL) || stages.count(STAGE_BUILDINGS)) {
            processBldValid();
            processBldInvalid();
        }
        db_.commit();
    }

private:
    void insertNames(
        const std::string& table,
        const std::string& key,
        Id id,
        const NamesBase& names)
    {
        auto query = Query()
            .insertInto(table)
            .columns("nm_id, " + key + ", lang, is_local, is_auto, name");
        bool first = true;
        for (const auto& [lang, name] : names.langToName()) {
            auto record = Query()
                << ++nmId_ << ','
                << id << ','
                << db_.quote(lang) << ','
                << (lang == LOCAL_LANG ? "TRUE" : "FALSE") << ','
                << "false" << ','
                << db_.quote(name);
            if (first) {
                query.values(record);
                first = false;
            } else {
                appendValues(query, record);
            }
        }
        db_.write(query);
    }

    void insertFtEdge(Id ftId, Id edgeId)
    {
        auto query = Query()
            .insertInto("ft_edge")
            .columns("ft_id, edge_id")
            .values(Query() << ftId << ',' << edgeId);
        db_.write(query);
    }

    void insertFaceEdge(Id faceId, Id edgeId)
    {
        auto query = Query()
            .insertInto("face_edge")
            .columns("face_id, edge_id")
            .values(Query() << faceId << ',' << edgeId);
        db_.write(query);
    }

    Id insertFace(const geolib3::LinearRing2& linearRing)
    {
        auto faceId = ++faceId_;

        auto query = Query()
            .insertInto("face")
            .columns("face_id")
            .values(Query() << faceId);
        db_.write(query);

        geolib3::PointsVector points;
        for (size_t i = 0; i < linearRing.pointsNumber(); ++i) {
            points.push_back(linearRing.pointAt(i));
        }
        points.push_back(linearRing.pointAt(0));

        auto edgeId = insertRingEdge(geolib3::Polyline2(points));
        insertFaceEdge(faceId, edgeId);
        return faceId;
    }

    Id insertRingEdge(const geolib3::Polyline2& polyline)
    {
        auto wkb = geolib3::WKB::toString(polyline);
        auto edgeId = ++edgeId_;
        auto nodeId = insertNode(polyline.pointAt(0));

        auto query = Query()
            .insertInto("edge")
            .columns("edge_id, f_node_id, t_node_id, shape")
            .values(Query()
                << edgeId << ','
                << nodeId << ','
                << nodeId << ','
                << db_.toGeom(wkb));
        db_.write(query);
        return edgeId;
    }

    Id insertEdge(const std::string& wkb)
    {
        auto polyline = geolib3::WKB::read<geolib3::Polyline2>(wkb);
        const auto& points = polyline.points();
        ASSERT(points.size() >= 2);
        auto fNodeId = insertNode(points.front());
        auto tNodeId = insertNode(points.back());

        auto edgeId = ++edgeId_;

        auto query = Query()
            .insertInto("edge")
            .columns("edge_id, f_node_id, t_node_id, shape")
            .values(Query()
                << edgeId << ','
                << fNodeId << ','
                << tNodeId << ','
                << db_.toGeom(wkb));
        db_.write(query);
        return edgeId;
    }

    Id insertNode(const geolib3::Point2& point)
    {
        auto wkb = geolib3::WKB::toString(point);
        return insertNode(wkb);
    }

    Id insertNode(const std::string& wkb)
    {
        auto nodeId = ++nodeId_;
        auto query = Query()
            .insertInto("node")
            .columns("node_id, shape")
            .values(Query()
                << nodeId << ','
                << db_.toGeom(wkb));
        db_.write(query);
        return nodeId;
    }

    void insertFace(
        const std::string& parentTableName,
        Id id,
        const geolib3::LinearRing2& linearRing,
        bool isInterior)
    {
        auto faceId = insertFace(linearRing);
        auto query = Query()
            .insertInto(parentTableName + "_face")
            .columns(parentTableName + "_id, face_id, is_interior")
            .values(Query()
                << id << ','
                << faceId << ','
                << (isInterior ? "TRUE" : "FALSE"));
        db_.write(query);
    }

    void insertPolygon(const std::string& parentTableName, Id id, const geolib3::Polygon2& polygon)
    {
        insertFace(parentTableName, id, polygon.exteriorRing(), false);
        for (size_t i = 0; i < polygon.interiorRingsNumber(); ++i) {
            insertFace(parentTableName, id, polygon.interiorRingAt(i), true);
        }
    }

    void insertPolygons(const std::string& parentTableName, Id id, const geolib3::MultiPolygon2& polygons)
    {
        auto size = polygons.polygonsNumber();
        for (size_t idx = 0; idx < size; ++idx) {
            insertPolygon(parentTableName, id, polygons.polygonAt(idx));
        }
    }

    void insertAd(
        const pqxx::row& row,
        Id id,
        Id parentAdId,
        ymapsdf::ad::LevelKind levelKind,
        const std::string& isocode = EMPTY)
    {
        ASSERT(!parentAdId || adNames_.count(parentAdId));

        auto wkbCenter = pqxx::binarystring(row[CENTER]).str();
        auto idCenter = insertNode(wkbCenter);
        auto wkb = pqxx::binarystring(row[SHAPE]).str();
        auto polygons = geolib3::WKB::read<geolib3::MultiPolygon2>(wkb);

        const auto& names = adNames_.emplace(id, row).first->second;
        names.check(id);

        auto fields = Query() << "ad_id, level_kind";
        auto values = Query() << id << ',' << static_cast<uint32_t>(levelKind);
        if (parentAdId) {
            fields << ", p_ad_id";
            values << ',' << parentAdId;
        }
        if (!isocode.empty()) {
            fields << ", isocode";
            values << ',' << db_.quote(isocode);
        }

        db_.write(Query().insertInto("ad").columns(fields).values(values));

        insertCenter("ad", id, idCenter);
        insertPolygons("ad", id, polygons);
    }

    void insertAdTown(Id id)
    {
        auto query = Query()
            .insertInto("locality")
            .columns("ad_id, town")
            .values(Query() << id << ',' << "TRUE");
        db_.write(query);
    }

    void insertCenter(const std::string& parentTableName, Id id, Id nodeId)
    {
        auto query = Query()
            .insertInto(parentTableName + "_center")
            .columns(parentTableName + "_id, node_id")
            .values(Query() << id << ',' << nodeId);
        db_.write(query);
    }

    void insertRd(Id rdId, int rdType, const NamesBase& names)
    {
        auto query = Query()
            .insertInto("rd")
            .columns("rd_id, rd_type")
            .values(Query() << rdId << ',' << rdType);
        db_.write(query);

        insertNames("rd_nm", "rd_id", rdId, names);
    }

    void insertRdAd(Id rdId, Id adId)
    {
        auto query = Query()
            .insertInto("rd_ad")
            .columns("rd_id, ad_id")
            .values(Query() << rdId << ',' << adId);
        db_.write(query);
    }

    void insertRdRdEl(Id rdId, const std::vector<std::string>& rdElIds)
    {
        auto query = Query()
            .insertInto("rd_rd_el")
            .columns("rd_id, rd_el_id");
        bool first = true;
        for (auto rdElId : rdElIds) {
            auto subQuery = Query() << rdId << ',' << rdElId;
            if (first) {
                query.values(subQuery);
                first = false;
            } else {
                appendValues(query, subQuery);
            }
        }
        db_.write(query);
    }

    void insertBld(const pqxx::row& row, ymapsdf::ft::Type ftTypeId)
    {
        auto wkb = pqxx::binarystring(row[SHAPE]).str();
        auto polygons = geolib3::WKB::read<geolib3::MultiPolygon2>(wkb);
        auto num = polygons.polygonsNumber();
        if (num != 1) {
            WARN() << "Invalid bld, id: " << row[ID].as<Id>(0) << ", polygons: " << num;
        }

        for (size_t idx = 0; idx < num; ++idx) {
            auto id = ++bldId_;
            auto query = Query()
                .insertInto("bld")
                .columns("bld_id, ft_type_id")
                .values(Query() << id << ',' << static_cast<uint32_t>(ftTypeId));
            db_.write(query);

            insertPolygon("bld", id, polygons.polygonAt(idx));
        }
    }

    void insertFt(Id id, ymapsdf::ft::Type ftTypeId)
    {
        auto query = Query()
            .insertInto("ft")
            .columns("ft_id, ft_type_id")
            .values(Query() << id << ',' << static_cast<uint32_t>(ftTypeId));
        db_.write(query);
    }

    void insertPolygonalFt(Id id, const pqxx::row& row, ymapsdf::ft::Type ftTypeId)
    {
        auto wkb = pqxx::binarystring(row[SHAPE]).str();
        auto polygons = geolib3::WKB::read<geolib3::MultiPolygon2>(wkb);

        insertFt(id, ftTypeId);
        insertPolygons("ft", id, polygons);
    }

    void insertNamedPolygonalFt(Id id, const pqxx::row& row, ymapsdf::ft::Type ftTypeId)
    {
        insertPolygonalFt(id, row, ftTypeId);

        Names names(row);
        if (!names.empty()) {
            insertNames("ft_nm", "ft_id", id, names);
        }
    }

    Id getAtdId(const std::string& key, Id id) const
    {
        if (!id) {
            return id;
        }

        auto atdKey = key + std::to_string(id);
        auto it = atd2Id_.find(atdKey);
        REQUIRE(it != atd2Id_.end(), "Can not find ATD (" << key << "): " << id);
        return it->second;
    }

    Id newAtdId(const std::string& key, Id id)
    {
        auto atdKey = key + std::to_string(id);
        REQUIRE(!atd2Id_.count(atdKey), "Already exists ATD (" << key << "): " << id);
        atd2Id_.emplace(atdKey, ++adId_);
        return adId_;
    }

    Id getCountryId()
    {
        if (!countryId_) {
            countryId_ = db_.countryId();
        }
        return countryId_;
    }

    void processAd();

    void processStreets();
    void processRdJc();
    void processRoads();
    void processRoutes();
    void processConds();

    void processAddrs();

    void processBldValid();
    void processBldInvalid();

    void processVegetation();
    void processHydro();

    Database db_;
    Id countryId_ = 0;
    Id adId_ = 0;
    Id nmId_ = 0;
    Id nodeId_ = 0;
    Id faceId_ = 0;
    Id edgeId_ = 0;
    Id bldId_ = 0;
    Id ftId_ = 0;

    std::unordered_map<std::string, Id> atd2Id_;
    std::unordered_map<Id, Names> adNames_;
    std::unordered_map<Id, StreetNames> rdNames_;
    std::unordered_set<Id> citiesIds_;
    std::unordered_set<Id> streetIds_;
};

void Builder::processRdJc()
{
    auto query = Query()
        .insertInto("rd_jc")
        .columns("rd_jc_id, shape")
        .append(" ")
        .append(Query()
            .select("id, ST_Setsrid(geom,4326)")
            .from(db_.srcSchema() + ".road_nodes_point"));
    db_.write(query);
}

void Builder::processAd()
{
    auto fields =
        "id::bigint,name_nat,name_nat,name_rus,name_eng,name_arb,"
        "ST_AsBinary(ST_Multi(ST_Buffer(ST_Setsrid(geom, 4326), 0))) AS " + SHAPE + ","
        "ST_AsBinary(ST_Setsrid(ST_ClosestPoint(geom,ST_Centroid(geom)),4326)) AS " + CENTER;
    {
        auto query = Query().select(fields).from("country_region");
        auto rows = db_.read(query, "countries");
        ASSERT(rows.size() == 1);
        const auto& row = rows[0];
        countryId_ = newAtdId("_", row[ID].as<Id>(0));
        insertAd(row, countryId_, 0, ymapsdf::ad::LevelKind::Country, LOCAL_ISOCODE);
    }
    {
        auto query = Query().select(fields + ",country_id").from("districts_region");
        for (const auto& row : db_.read(query, "districts")) {
            auto id = newAtdId("d", row[ID].as<Id>(0));
            insertAd(row, id, getAtdId("_", row["country_id"].as<Id>(0)), ymapsdf::ad::LevelKind::Province);
        }
    }
    {
        auto query = Query().select(fields + ",dist_id").from("sub_districts_region");
        for (const auto& row : db_.read(query, "sub districts")) {
            auto id = newAtdId("s", row[ID].as<Id>(0));
            insertAd(row, id, getAtdId("d", row["dist_id"].as<Id>(0)), ymapsdf::ad::LevelKind::Area);
        }
    }

    std::unordered_map<Id,Id> natToSubIds;
    {
        auto query = Query().select(fields + ",subdist_id").from("natural_regions_region");
        for (const auto& row : db_.read(query, "natural regions")) {
            auto id = newAtdId("n", row[ID].as<Id>(0));
            auto subId = getAtdId("s", row["subdist_id"].as<Id>(0));
            if (subId) {
                natToSubIds.emplace(id, subId);
            }
            insertAd(row, id, subId, ymapsdf::ad::LevelKind::Other);
        }
    }
    {
        auto query = Query().select(fields + ",nat_reg_id,class").from("cities_region");
        for (const auto& row : db_.read(query, "cities")) {
            auto cityClass = row["class"].as<int>(0);
            auto id = newAtdId("c", row[ID].as<Id>(0));
            auto natId = getAtdId("n", row["nat_reg_id"].as<Id>(0));
            auto it = natToSubIds.find(natId);
            ASSERT(it != natToSubIds.end());
            auto subId = it->second;
            insertAd(row, id, subId, ymapsdf::ad::LevelKind::Locality);
            citiesIds_.insert(id);
            if (cityClass <= 2) {
                insertAdTown(id);
            }
        }
    }
    {
        auto query = Query().select(fields + ",cit_id::bigint AS " + CITY_ID).from("ad7");
        for (const auto& row : db_.read(query, "ad7")) {
            auto id = newAtdId("a", row["id"].as<Id>(0));
            auto cityId = getAtdId("c", row[CITY_ID].as<Id>(0));
            insertAd(row, id, cityId, ymapsdf::ad::LevelKind::Block);
        }
    }

    for (const auto& [adId, names] : adNames_) {
        insertNames("ad_nm", "ad_id", adId, names);
    }
}

void Builder::processBldValid()
{
    auto fields =
        ID + ", ST_AsBinary(ST_Multi(ST_Setsrid(geom, 4326))) AS " + SHAPE;
    auto query = Query()
        .select(fields)
        .from("buildings_region")
        .where("ST_Isvalid(geom) AND ST_Isvalid(ST_Transform(ST_Setsrid(geom, 4326),3395))");

    for (const auto& row : db_.read(query, "buildings (valid)")) {
        insertBld(row, ymapsdf::ft::Type::UrbanResidential);
    }
}

void Builder::processBldInvalid()
{
    auto fields =
        ID + ", ST_AsBinary(ST_Multi(ST_Buffer(ST_Transform(ST_Transform(ST_Setsrid(geom, 4326),3395),4326),0))) AS " + SHAPE;
    auto query = Query()
        .select(fields)
        .from("buildings_region")
        .where("NOT ST_Isvalid(geom) OR NOT ST_Isvalid(ST_Transform(ST_Setsrid(geom, 4326),3395))");

    for (const auto& row : db_.read(query, "buildings (invalid)")) {
        insertBld(row, ymapsdf::ft::Type::UrbanResidential);
    }
}

void Builder::processVegetation()
{
    auto fields = "ST_AsBinary(ST_Multi(ST_Buffer(ST_Setsrid(geom, 4326), 0))) AS " + SHAPE;
    {
        auto query = Query().select(fields).from("vegetation_region");
        for (const auto& row : db_.read(query, "vegetation")) {
            insertPolygonalFt(++ftId_, row, ymapsdf::ft::Type::Vegetation);
        }
    }
    fields += ",name_nat,name_nat,name_rus,name_eng,name_arb";
    {
        auto query = Query().select(fields).from("cemetery_region");
        for (const auto& row : db_.read(query, "cemetery")) {
            insertNamedPolygonalFt(++ftId_, row, ymapsdf::ft::Type::UrbanCemetery);
        }
    }
    {
        auto query = Query().select(fields).from("park_region");
        for (const auto& row : db_.read(query, "park")) {
            insertNamedPolygonalFt(++ftId_, row, ymapsdf::ft::Type::VegetationPark);
        }
    }
}

void Builder::processHydro()
{
    auto getRiverFtTypeId = [](int riverClass) {
        ASSERT(riverClass >= 1 and riverClass <= 3);
        switch (riverClass) {
            case 1:     return ymapsdf::ft::Type::HydroRiverLarge;
            case 3:     return ymapsdf::ft::Type::HydroRiverSmall;
            default:    return ymapsdf::ft::Type::HydroRiver;
        }
    };

    std::unordered_map<int, std::unordered_map<std::string, Id>> rclassToNameFtId;

    auto fields =
        ID + ",class,"
        "ST_AsBinary(ST_Setsrid(ST_GeometryN(geom,1), 4326)) AS " + SHAPE + ","
        "ST_NumGeometries(geom) numgeom,"
        "name_nat,name_nat,name_rus,name_eng,name_arb";

    auto query = Query().select(fields).from("hydro_polyline");

    for (const auto& row : db_.read(query, "hydro rivers")) {
        auto id = row[ID].as<Id>();
        auto numGeom = row["numgeom"].as<int>();
        REQUIRE(numGeom == 1, "Invalid hydro geometry, id:" << id << " num:" << numGeom);

        auto rclass = row["class"].as<int>(0);
        auto edgeId = insertEdge(pqxx::binarystring(row[SHAPE]).str());

        Names names(row);

        const auto& langToName = names.langToName();
        if (langToName.empty()) {
            auto ftId = ++ftId_;
            insertFt(ftId, getRiverFtTypeId(rclass));
            insertFtEdge(ftId, edgeId);
            continue;
        }

        ASSERT(langToName.size() == 1);
        ASSERT(langToName.begin()->first == LOCAL_LANG);
        const auto& name = langToName.begin()->second;

        auto& ftId = rclassToNameFtId[rclass][name];
        if (!ftId) {
            ftId = ++ftId_;
            insertFt(ftId, getRiverFtTypeId(rclass));
            insertNames("ft_nm", "ft_id", ftId, names);
        }
        insertFtEdge(ftId, edgeId);
    }
}

void Builder::processStreets()
{
    auto query = Query().select("*").from("strt_none");

    for (const auto& row : db_.read(query, "streets")) {
        auto id = row[ID].as<Id>(0);
        const auto& names = rdNames_.emplace(id, row).first->second;
        names.check(id);
        auto cityId = getAtdId("c", row[CITY_ID].as<Id>(0));
        if (cityId) {
            ASSERT(citiesIds_.empty() || citiesIds_.count(cityId));
            insertRdAd(id, cityId);
        }
    }

    for (const auto& [rdId, names] : rdNames_) {
        insertRd(rdId, 1, names);
    }
}

void Builder::processRoads()
{
    //    "route_nat,route_rus,route_eng,route_arb,"
    //    "street_nat,street_rus,street_eng,street_arb,"
    //    "taxi,emergency,delivery,residents,"
    auto fields =
        ID + "," + F_NODE_ID + "," + T_NODE_ID + ",class,"
        "street_id,speedlimit,oneway,kind,rshape,pav,toll,"
        "car,bus,truck,bicycle,pedestrian,"
        "ST_AsBinary(ST_Setsrid(ST_GeometryN(geom,1), 4326)) AS " + SHAPE + ","
        "ST_NumGeometries(geom) numgeom";

    auto query = Query().select(fields).from("roads_polyline").where("kind<5");
    auto rows = db_.read(query, "roads");

    std::unordered_map<Id, int> nodeZLevel;
    auto getZLevel = [&](Id nodeId, int kind) -> int {
        switch (kind) {
            case 1: return 1;
            case 2: return -1;
            default:
                break;
        }
        auto it = nodeZLevel.find(nodeId);
        return it == nodeZLevel.end() ? 0 : it->second;
    };

    {
        std::unordered_map<Id, size_t> nodeCounts;
        for (const auto& row : rows) {
            auto fNodeId = row[F_NODE_ID].as<Id>();
            auto tNodeId = row[T_NODE_ID].as<Id>();
            ++(nodeCounts[fNodeId]);
            ++(nodeCounts[tNodeId]);

            auto kind = row[KIND].as<int>(0);
            if (kind == 1 || kind == 2) {
                int zlevel = kind == 1 ? 1 : -1;
                nodeZLevel.emplace(fNodeId, zlevel);
                nodeZLevel.emplace(tNodeId, zlevel);
            }
        }
        for (auto [nodeId, valency] : nodeCounts) {
            if (valency != 2) {
                nodeZLevel.erase(nodeId);
            }
        }
    }

    for (const auto& row : rows) {
        auto id = row[ID].as<Id>();
        auto numGeom = row["numgeom"].as<int>();
        REQUIRE(numGeom == 1, "Invalid roads geometry, id:" << id << " num:" << numGeom);

        auto fNodeId = row[F_NODE_ID].as<Id>();
        auto tNodeId = row[T_NODE_ID].as<Id>();

        auto kind = row[KIND].as<int>(0);
        int structType = kind <= 2 ? kind : 0;
        int ferry = kind == 3 ? 1 : kind == 4 ? 2 : 0;

        auto fZLevel = getZLevel(fNodeId, kind);
        auto tZLevel = getZLevel(tNodeId, kind);

        auto fow = getFow(row["rshape"].as<int>(0));
        auto oneway = row["oneway"].as<int>(0) > 0 ? "'F'" : "'B'";

        int accessId =
            (row["pedestrian"].as<int>(0) > 0 ? 0x01 : 0) +
            (row["bus"].as<int>(0) > 0 ? 0x02 : 0) +
            (row["truck"].as<int>(0) == 0 ? 0x04 : 0) +
            (row["car"].as<int>(0) > 0 ? 0x08 : 0) +
            (row["bicycle"].as<int>(0) > 0 ? 0x10 : 0);

        auto streetId = row[STREET_ID].as<Id>(0);
        auto fc = getFc(row["class"].as<int>(0));

        int paved = row["pav"].as<int>(0) == 0 ? 1 : 0; // rclass == 9 ? 0 : 1;
        auto toll = row["toll"].as<int>(0);
        auto speedLimit = row["speedlimit"].as<int>(0);
        std::string speedLimitStr = fc < 10 && speedLimit > 0
            ? std::to_string(speedLimit)
            : "NULL";
        auto wkb = pqxx::binarystring(row[SHAPE]).str();

        auto speedCat = getSpeedCat(fc, paved);

        auto query = Query()
            .insertInto("rd_el")
            .columns(
                "rd_el_id, f_rd_jc_id, t_rd_jc_id,"
                "fc, fow, speed_cat, paved,"
                "struct_type, ferry, toll,"
                "speed_limit, oneway, access_id,"
                "f_zlev, t_zlev, shape")
            .values(Query()
                << id << ',' << fNodeId << ',' << tNodeId << ','
                << fc << ',' << fow << ',' << speedCat << ',' << paved << ','
                << structType << ',' << ferry << ',' << toll << ','
                << speedLimitStr << ',' << oneway << ',' << accessId << ','
                << fZLevel << ',' << tZLevel << ',' << db_.toGeom(wkb));
        db_.write(query);

        if (streetId) {
            ASSERT(rdNames_.count(streetId));
            streetIds_.insert(streetId);
            auto query = Query()
                .insertInto("rd_rd_el")
                .columns("rd_id, rd_el_id")
                .values(Query() << streetId << ',' << id);
            db_.write(query);
        }
    }
}

void Builder::processRoutes()
{
    auto rdId = db_.maxId("rd");
    auto countryId = getCountryId();

    {
        std::string fields = "label_nat, label_rus, label_eng, label_arb";
        auto query = Query()
            .select(fields + ", array_agg(id) " + IDS)
            .from("roads_polyline")
            .where(
                "route_nat IS NOT NULL AND "
                "label_nat IS NOT NULL AND "
                "street_id=0")
            << " GROUP BY route_nat, " << fields;

        for (const auto& row : db_.read(query, "roads (named routes)", "")) {
            ++rdId;
            LabelNames names(row);
            names.check(rdId);
            insertRd(rdId, 5, names);
            insertRdRdEl(rdId, common::parseSqlArray(row[IDS].as<std::string>()));
            insertRdAd(rdId, countryId);
        }
    }
    {
        auto fields = "route_nat, array_agg(id) " + IDS;

        auto query = Query()
            .select(fields)
            .from("roads_polyline")
            .where("route_nat IS NOT NULL")
            << " GROUP BY route_nat";

        for (const auto& row : db_.read(query, "roads (numeric routes)", "")) {
            ++rdId;
            Names names;
            names.addLocal(row[0].c_str());
            names.check(rdId);
            insertRd(rdId, 2, names);
            insertRdRdEl(rdId, common::parseSqlArray(row[IDS].as<std::string>()));
            insertRdAd(rdId, countryId);
        }
    }
}

void Builder::processConds()
{
    auto fields = "node_id,f_road_id,t_road_id";
    auto query = Query().select(fields).from("road_restricts_none");

    Id condId = 0;
    for (const auto& row : db_.read(query, "road restricts")) {
        auto nodeId = row[0].as<Id>();
        auto fRoadId = row[1].as<Id>();
        auto tRoadId = row[2].as<Id>();

        ++condId;
        auto query = Query()
            .insertInto("cond")
            .columns("cond_id, cond_type, cond_seq_id, access_id")
            .values(Query() << condId << ",1," << condId << ",30");
        db_.write(query);

        query = Query()
            .insertInto("cond_rd_seq")
            .columns("cond_seq_id, rd_jc_id, rd_el_id, seq_num")
            .values(Query() << condId << ',' << nodeId << ',' << fRoadId << ",0");

        //TODO: query.appendValues(...)
        appendValues(query, Query() << condId << ",NULL," << tRoadId << ",1");

        db_.write(query);
    }
}

void Builder::processAddrs()
{
    // "postcode,"
    // "name_nat,name_rus,name_eng,name_arb,"
    auto fields =
        "id::bigint,street_id::bigint,build,"
        "ST_AsBinary(ST_Setsrid(geom, 4326)) AS " + SHAPE;
    auto query = Query()
        .select(fields)
        .from("address_point")
        .where(STREET_ID + " > 0");

    for (const auto& row : db_.read(query, "address")) {
        auto addrId = row[ID].as<Id>();
        auto streetId = row[STREET_ID].as<Id>(0);
        if (!rdNames_.empty()) {
            ASSERT(rdNames_.count(streetId));
            if (!streetIds_.count(streetId)) {
                WARN() << "Address: " << addrId << " street (without rd_el): " << streetId;
            }
        }

        Names names;
        names.addLocal(row["build"].c_str());
        names.check(addrId);

        auto wkb = pqxx::binarystring(row[SHAPE]).str();
        auto addrNodeId = insertNode(wkb);

        auto query = Query()
            .insertInto("addr")
            .columns("addr_id, node_id, rd_id")
            .values(Query() << addrId << ',' << addrNodeId << ',' << streetId);
        db_.write(query);

        insertNames("addr_nm", "addr_id", addrId, names);
    }
}

} // namespace

void buildYMapsDF(
    const std::string& connStr,
    const std::string& srcSchema,
    const std::string& dstSchema,
    const std::string& stages)
{
    INFO() << "DB: " << connStr;
    INFO() << srcSchema << " --> " << dstSchema << " : " << stages;

    auto splitted = common::split(stages, ",");

    Builder builder(connStr, srcSchema, dstSchema);
    builder.run({ splitted.begin(), splitted.end() });

    INFO() << "Done.";
}

} // namespace tnc2ymapsdf
} // namespace wiki
} // namespace maps
