#include "ad_finder.h"

#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <maps/libs/log8/include/log8.h>

#include <geos/geom/Envelope.h>
#include <geos/geom/GeometryFactory.h>

namespace maps {
namespace wiki {
namespace importer {

namespace {

constexpr uint64_t COUNTRY_LEVEL_KIND = 1;
constexpr uint64_t LOCALITY_LEVEL_KIND = 4;

struct ObjectWithEnvelope
{
    ObjectPtr object;
    geos::geom::Envelope envelope;
    LevelKind maxLevelKind; //parent must be in [1; maxLevelKind]
};

class AdFinder
{
public:
    AdFinder(TaskParams& params)
        : txn_(params.viewPool.masterReadOnlyTransaction())
        , adCategory_(params.editorConfig.category(cat::AD))
    {
        txn_->exec("SET search_path=vrevisions_trunk,public");

        //from country (level = 1) to locality (level = 4)
        levelKindCondition_ = common::join(std::vector<int>{1, 2, 3, 4},
            [](const int& level)
            {
                return "(domain_attrs @> hstore('ad:level_kind', '" + std::to_string(level) + "'))";
            },
            " OR ");
    }

    const cfg::Category& adCategory() const { return adCategory_; }

    TObjectId requestAd(const geos::geom::Envelope& envelope, LevelKind maxLevelKind)
    {
        common::Geom envelopeGeom(geos::geom::GeometryFactory::getDefaultInstance()->toGeometry(&envelope));

        std::ostringstream query;
        query << "SELECT object_id, ST_SetSrid(ST_Envelope(ST_Extent(the_geom)), 3395), MAX(domain_attrs -> 'ad:level_kind') "
              << "FROM contour_objects_geom "
              << "WHERE ST_Intersects("
              << "   the_geom, "
              << "   ST_GeomFromWKB('" << txn_->esc_raw(envelopeGeom.wkb()) << "', 3395)) "
              << "AND (" << levelKindCondition_ << ") "
              << "AND (NOT domain_attrs @> hstore('ad:disp_class', '10')) "
              << "AND domain_attrs ? 'cat:ad' "
              << "GROUP BY 1 ORDER BY 3 DESC "
              << "LIMIT 4";

        auto rows = txn_->exec(query.str());
        for (const auto& row : rows) {
            auto lk = row[2].as<LevelKind>();
            if (lk > maxLevelKind) {
                continue;
            }
            common::Geom adEnvelope(row[1]);
            if (adEnvelope->getEnvelopeInternal()->contains(&envelope)) {
                return row[0].as<TObjectId>();
            }
        }

        return EMPTY_DB_ID;
    }

private:
    pgpool3::TransactionHandle txn_;
    const cfg::Category& adCategory_;
    std::string levelKindCondition_;
};

} // namespace

void findContainingAdForRoadsWithoutGeometry(
    const ObjectsCache& cache,
    TaskParams& params,
    MessageReporter& messageReporter)
{
    std::map<ID, ObjectWithEnvelope> roadsWithoutAd;
    for (const auto& object : cache.objects()) {
        if (object->state() != ObjectState::New) {
            continue;
        }
        if (object->category().id() == cat::RD && object->masterRelations().empty()) {
            roadsWithoutAd.emplace(object->id(),
                ObjectWithEnvelope{object, geos::geom::Envelope(), LOCALITY_LEVEL_KIND});
        }
    }

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

    for (const auto& object : cache.objects()) {
        if (object->category().id() != cat::ADDR) {
            continue;
        }
        for (const auto& relation : object->futureMasters()) {
            auto it = roadsWithoutAd.find(relation.relatedId);
            if (it == roadsWithoutAd.end()) {
                continue;
            }
            common::Geom pointGeom(object->wkb());
            geos::geom::Coordinate c;
            ASSERT(pointGeom->getCentroid(c));
            auto mercPoint = common::geodeticTomercator(c.x, c.y);
            it->second.envelope.expandToInclude(mercPoint.x(), mercPoint.y());
        }
    }

    AdFinder finder(params);

    for (const auto& kv : roadsWithoutAd) {
        const auto& id = kv.first;
        const auto& object = kv.second.object;
        const auto& envelope = kv.second.envelope;

        if (envelope.isNull()) {
            messageReporter.warning(id) << "Failed to find ad: road has no address points";
            continue;
        }
        auto adId = finder.requestAd(envelope, kv.second.maxLevelKind);
        if (adId) {
            object->addMaster(role::ASSOCIATED_WITH, adId, finder.adCategory());
        } else {
            messageReporter.warning(id) << "Failed to find ad: no containing ad";
        }
    }
}

void findParentAdForAd(
    const ObjectsCache& cache,
    TaskParams& params,
    MessageReporter& messageReporter)
{
    std::map<ID, ObjectWithEnvelope> adWithoutParentAd;

    for (const auto& object : cache.objects()) {
        if (object->state() != ObjectState::New) {
            continue;
        }

        if (object->category().id() != cat::AD || !object->masterRelations().empty()) {
            continue;
        }

        auto levelKind = extractLevelKind(object->attributesToAdd());
        if (!levelKind) {
            messageReporter.warning(object->id()) << "Ad has no level_kind attribute";
            continue;
        }
        if (levelKind == COUNTRY_LEVEL_KIND) {
            continue; //no parents for countries
        }

        geos::geom::Envelope envelope;

        for (const auto& relation : object->slaveRelations()) {
            const auto& slave = relation.relatedObject;

            if (slave->category().id() != cat::AD_CNT) {
                continue;
            }
            common::Geom pointGeom(slave->wkb());
            geos::geom::Coordinate c;
            ASSERT(pointGeom->getCentroid(c));
            auto mercPoint = common::geodeticTomercator(c.x, c.y);
            envelope.expandToInclude(mercPoint.x(), mercPoint.y());
            break;
        }

        adWithoutParentAd.emplace(object->id(), ObjectWithEnvelope{object, envelope, *levelKind - 1});
    }

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

    AdFinder finder(params);

    for (const auto& kv : adWithoutParentAd) {
        const auto& id = kv.first;
        const auto& object = kv.second.object;
        const auto& envelope = kv.second.envelope;

        if (envelope.isNull()) {
            messageReporter.warning(id) << "Failed to find parent ad: ad has no center";
            continue;
        }
        auto adId = finder.requestAd(envelope, kv.second.maxLevelKind);
        if (adId) {
            object->addMaster(role::CHILD, adId, finder.adCategory());
        } else {
            messageReporter.warning(id) << "Failed to find parent ad: no parent ad";
        }
    }
}

} // importer
} // wiki
} // maps
