#include "geo.h"
#include <geobase/include/structs.hpp>
#include <library/cpp/geo/point.h>
#include <library/cpp/geo/util.h>
#include <library/cpp/geo/window.h>
#include <search/web/util/common/common.h>

namespace {
    static const TString TVM_SECRET{"TVM_SECRET"};
    static const TString GEOCODER_SOURCE{"GEOCODER"};
    constexpr auto DEFAULT_GEOCODER_TVM_ID = 2001886;
    constexpr TStringBuf TVM_HEADER = "X-Ya-Service-Ticket";
    constexpr TStringBuf GEOCODER_WAIT_STAGE = "geocoder_wait_stage";
    constexpr TStringBuf GEOCODER_REQUIRED_CGI = "&correct_misspell=0&middle_noqtree=1&ms=pb&lang=ru&origin=saas";
    constexpr TStringBuf SCHEME_HYPERGEO_DIAGONAL_THRESHOLD = "HypergeoDiagonalThreshold";
    constexpr TStringBuf DEFAULT_REQUEST_TYPE = "geo";

    using namespace yandex::maps::proto;
    using TGeoKind = search::kind::Kind;

    static const THashMap<TString, TGeoKind> STRING_TO_KIND = {
        {"unknown", TGeoKind::UNKNOWN},
        {"country", TGeoKind::COUNTRY},
        {"region", TGeoKind::REGION},
        {"province", TGeoKind::PROVINCE},
        {"area", TGeoKind::AREA},
        {"locality", TGeoKind::LOCALITY},
        {"district", TGeoKind::DISTRICT},
        {"street", TGeoKind::STREET},
        {"house", TGeoKind::HOUSE},
        {"route", TGeoKind::ROUTE},
        {"station", TGeoKind::STATION},
        {"metro_station", TGeoKind::METRO_STATION},
        {"railway_station", TGeoKind::RAILWAY_STATION},
        {"vegetation", TGeoKind::VEGETATION},
        {"hydro", TGeoKind::HYDRO},
        {"airport", TGeoKind::AIRPORT},
        {"other", TGeoKind::OTHER},
        {"entrance", TGeoKind::ENTRANCE},
        {"level", TGeoKind::LEVEL},
        {"apartment", TGeoKind::APARTMENT}};

    struct TToponymDescription {
        NGeo::TGeoPoint Point;
        NGeo::TGeoWindow BoundedBy;
        i32 Geoid = NGeobase::UNKNOWN_REGION;
        bool IsHyperGeo = false;
        TString DisplayedText;
        TGeoKind Kind = yandex::maps::proto::search::kind::UNKNOWN;
    };

    TSimpleAuxRequestRef SendGeoCoderRequest(const TMaybe<NTvmAuth::TTvmClient>& tvmClient,
                                             NRearr::IRearrangeEnvironment& rearrangeEnv,
                                             const TString& requestCgi) {
        TString geoCoderHeaders;
        if (tvmClient) {
            geoCoderHeaders = TString::Join(TVM_HEADER, ": ", tvmClient->GetServiceTicketFor(GEOCODER_SOURCE));
        }
        auto request = MakeIntrusive<TSimpleAuxRequest>(GEOCODER_SOURCE, requestCgi, geoCoderHeaders);
        request->SetStage(GEOCODER_WAIT_STAGE);
        rearrangeEnv.SendAuxRequest(request);
        return request;
    }

    const common2::geo_object::GeoObject& FindMostAppropriateResult(const common2::geo_object::GeoObject& reply, const TVector<TGeoKind>& preferredKinds) {
        int bestResPos = 0;
        size_t bestPrio = preferredKinds.size();
        for (int i = 0; i < reply.geo_object().size(); ++i) {
            if (bestPrio == 0) {
                break;
            }
            const auto& geoObject = reply.geo_object()[i];
            for (const auto& metadata : geoObject.metadata()) {
                if (!metadata.HasExtension(search::geocoder::GEO_OBJECT_METADATA)) {
                    continue;
                }
                const auto& geoObjectMetadata = metadata.GetExtension(search::geocoder::GEO_OBJECT_METADATA);
                if (!geoObjectMetadata.HasExtension(search::geocoder_internal::TOPONYM_INFO)) {
                    continue;
                }
                const auto& toponymInfo = geoObjectMetadata.GetExtension(search::geocoder_internal::TOPONYM_INFO);
                for (const auto& matched : toponymInfo.matched_component()) {
                    size_t prio = FindIndex(preferredKinds, matched.kind());
                    if (prio != NPOS && prio < bestPrio) {
                        bestPrio = prio;
                        bestResPos = i;
                    }
                }
                break;
            }
        }
        return reply.geo_object()[bestResPos];
    }

    TMaybe<TToponymDescription> ParseGeoCoderResponse(const TSimpleAuxRequest* request, float diagonalThreshold, const TVector<TGeoKind>& preferredKinds) {
        using namespace NGeo;

        auto getPoint = [](const auto& geometryVector) -> TMaybe<TGeoPoint> {
            for (auto&& geometry : geometryVector) {
                if (geometry.Haspoint()) {
                    const auto& point = geometry.Getpoint();
                    return TGeoPoint{point.Getlon(), point.Getlat()};
                }
            }
            return {};
        };

        if (!!request) {
            if (common2::response::Response answer; answer.ParseFromString(request->GetResult())) {
                if (answer.has_reply() && !answer.reply().geo_object().empty()) {
                    const auto& geoObject = FindMostAppropriateResult(answer.reply(), preferredKinds);
                    TToponymDescription td;
                    if (auto point = getPoint(geoObject.geometry())) {
                        td.Point = *point;
                    }

                    if (geoObject.has_bounded_by()) {
                        const auto& bb = geoObject.bounded_by();
                        TGeoWindow window{TGeoPoint{bb.lower_corner().lon(), bb.lower_corner().lat()},
                                          TGeoPoint{bb.upper_corner().lon(), bb.upper_corner().lat()}};
                        double diameter = window.Diameter();
                        td.IsHyperGeo = NGeo::GetMetersFromDeg(diameter) <= diagonalThreshold;
                        td.BoundedBy = window;
                    }

                    if (geoObject.has_name()) {
                        td.DisplayedText = geoObject.name();
                    }

                    for (const auto& metadata : geoObject.metadata()) {
                        if (!metadata.HasExtension(search::geocoder::GEO_OBJECT_METADATA)) {
                            continue;
                        }

                        const auto& geoObjectMetadata = metadata.GetExtension(search::geocoder::GEO_OBJECT_METADATA);
                        if (!geoObjectMetadata.HasExtension(search::geocoder_internal::TOPONYM_INFO)) {
                            continue;
                        }

                        const auto& toponymInfo = geoObjectMetadata.GetExtension(
                            search::geocoder_internal::TOPONYM_INFO);
                        if (toponymInfo.has_geoid()) {
                            td.Geoid = toponymInfo.geoid();
                        }

                        for (const auto& matched : toponymInfo.matched_component()) {
                            if (matched.kind() == search::kind::HOUSE ||
                                matched.kind() == search::kind::METRO_STATION) {
                                td.IsHyperGeo = true;
                                td.Kind = matched.kind();
                                break;
                            }
                        }

                        if (td.IsHyperGeo && td.Geoid != NGeobase::UNKNOWN_REGION) {
                            break;
                        }
                    }
                    return td;
                }
            }
        }
        return Nothing();
    }

    TString ReadTvmKey() {
        TString geoTvmKey = GetEnv(TVM_SECRET);
        if (!geoTvmKey) {
            throw yexception() << TVM_SECRET << " environment parameter was not found";
        }

        return geoTvmKey;
    }

    using TDstMap = NTvmAuth::NTvmApi::TClientSettings::TDstMap;

    NTvmAuth::NTvmApi::TClientSettings MakeTvmConfig(TDstMap&& destinations, ui32 selfTvmId) {
        NTvmAuth::NTvmApi::TClientSettings setts;
        setts.SetSelfTvmId(selfTvmId);
        setts.EnableServiceTicketChecking();
        setts.EnableUserTicketChecking(NTvmAuth::EBlackboxEnv::ProdYateam);
        setts.EnableServiceTicketsFetchOptions(ReadTvmKey(), std::move(destinations));
        return setts;
    }

    TMaybe<NTvmAuth::TTvmClient> MakeTvmClient(const NRearrConf::TRearrangeRuleParams& params) {
        auto selfTvmId = NRearrConf::InitValue<ui32>(params, "SelfTvmId", 0);
        auto geocoderTvmId = NRearrConf::InitValue<ui32>(params, "GeocoderTvmId", DEFAULT_GEOCODER_TVM_ID);
        if (!selfTvmId) {
            return Nothing();
        }
        return NTvmAuth::TTvmClient(
            MakeTvmConfig({{GEOCODER_SOURCE, geocoderTvmId}}, selfTvmId),
            NTvmAuth::TDevNullLogger::IAmBrave());
    }
}

class TRTYGeoRuleContext: public IRearrangeRuleContext {
private:
    bool IsEnabled() {
        return LocalScheme().Get("Enable").IsTrue();
    }

public:
    TRTYGeoRuleContext(const TRTYGeoRule& rule)
        : Rule_(rule)
    {
    }

    // before search
    void DoAdjustClientParams(const TAdjustParams& ap) override {
        if (!IsEnabled()) {
            return;
        }

        const TStringBuf name = LocalScheme().Get("ToponymName").GetString();
        const i64 region = LocalScheme().Get("Region").GetIntNumber(0);
        const TStringBuf type = LocalScheme().Get("Type").GetString(DEFAULT_REQUEST_TYPE);
        TVector<TGeoKind> preferredKinds;
        preferredKinds.reserve(LocalScheme().Get("ResultKinds").GetArray().size());
        for (const auto& kindStr : LocalScheme().Get("ResultKinds").GetArray()) {
            if (const auto* kind = STRING_TO_KIND.FindPtr(kindStr)) {
                preferredKinds.push_back(*kind);
            }
        }
        if (!name || !region) {
            // TODO: log this case
            return;
        }

        PrepareToponymDescription(name, region, type, preferredKinds);
        PatchRequest(ToponymDescription_, ap, LocalScheme());
    }

    // after snippets
    void DoRearrangeAfterFetch(TRearrangeParams& rp) override {
        if (!IsEnabled()) {
            return;
        }
        if (!!GeoFilter_) {
            AddSearchProperty(rp, "Filter.debug", GeoFilter_);
        }
        if (!!DisplayedText_) {
            AddSearchProperty(rp, "DisplayedText.debug", DisplayedText_);
        }
    }

private:
    void PrepareToponymDescription(const TStringBuf name, const i64 region, const TStringBuf type, const TVector<TGeoKind>& preferredKinds) {
        if (ToponymDescriptionPrepared_) {
            return;
        }

        ToponymDescriptionPrepared_ = true;

        TStringStream forwardRequestCgi;
        forwardRequestCgi << "text=" << CGIEscapeRet(name)
                          << GEOCODER_REQUIRED_CGI
                          << "&lr=" << ToString(region)
                          << "&type=" << type;
        auto request = SendGeoCoderRequest(Rule_.TvmClient, GetRearrangeEnv(), forwardRequestCgi.Str());
        GetRearrangeEnv().WaitAuxRequests({request.Get()}, "Forward geocoder request");

        float diagonalThreshold = LocalScheme().Get(SCHEME_HYPERGEO_DIAGONAL_THRESHOLD).GetNumber();
        TMaybe<TToponymDescription> descr = ParseGeoCoderResponse(request.Get(), diagonalThreshold, preferredKinds);
        ToponymDescription_ = std::move(descr);
    }

    void PatchRequest(const TMaybe<TToponymDescription>& descr, const IMetaRearrangeContext::TAdjustParams& ap, const NSc::TValue& scheme) {
        if (descr.Defined()) {
            if (scheme.Get("FilterByWindow").IsTrue()) {
                TStringStream geoFilter;
                double width = Max(descr->BoundedBy.GetSize().GetWidth(), scheme.Get("SpnWidthMin").GetNumber(0.0));
                double height = Max(descr->BoundedBy.GetSize().GetHeight(), scheme.Get("SpnHeightMin").GetNumber(0.0));

                geoFilter << "comp:geo;"
                          << "lonlat:" << descr->BoundedBy.GetCenter().Lon() << ","
                          << descr->BoundedBy.GetCenter().Lat() << ";"
                          << "spn:" << width << "," << height;

                // for instance you can pass prne and maxspn (https://wiki.yandex-team.ru/users/yrum/mingeo/#avtozumiprjuning)
                for (const auto& extra : scheme.Get("CompSearchExtra").GetArray()) {
                    geoFilter << ";" << extra.GetString();
                }

                const TCgiParameters result = {
                    {"comp_search", geoFilter.Str()}};
                ap.ClientRequestAdjuster->ClientCgiInsert(result);
                GeoFilter_ = geoFilter.Str();
            }

            for (const auto& item : scheme.Get("RelevCalcTemplates").GetArray()) {
                TString expression{item.GetString()};
                SubstGlobal(expression, "#geocoder_center_lon", ToString(descr->BoundedBy.GetCenter().Lon()));
                SubstGlobal(expression, "#geocoder_center_lat", ToString(descr->BoundedBy.GetCenter().Lat()));
                ap.ClientRequestAdjuster->ClientAppendRelev("calc", expression);
            }

            DisplayedText_ = descr->DisplayedText;
        } else {
            TVector<TStringBuf> compSearchParts;
            for (const auto& item : scheme.Get("DefaultCompSearch").GetArray()) {
                compSearchParts.push_back(item.GetString());
            }
            TString defaultCompSearch = JoinSeq(';', compSearchParts);
            if (!!defaultCompSearch && scheme.Get("FilterByWindow").IsTrue()) {
                const TCgiParameters result = {
                    {"comp_search", defaultCompSearch}};
                ap.ClientRequestAdjuster->ClientCgiInsert(result);
                GeoFilter_ = defaultCompSearch;
            }

            auto defaultDisplayedText = TString{LocalScheme().Get("DefaultDisplayedText").GetString()};
            if (!!defaultDisplayedText) {
                DisplayedText_ = defaultDisplayedText;
            }
        }
    }

private:
    const TRTYGeoRule& Rule_;
    TString GeoFilter_;
    TString DisplayedText_;
    TMaybe<TToponymDescription> ToponymDescription_ = {};
    bool ToponymDescriptionPrepared_ = false;
};

TRTYGeoRule::TRTYGeoRule(const NRearrConf::TRearrangeRuleParams& params)
    : TvmClient(MakeTvmClient(params))
{
}

IRearrangeRuleContext* TRTYGeoRule::DoConstructContext() const {
    return new TRTYGeoRuleContext(*this);
}

IRearrangeRule* CreateRTYGeoRule(const TString& options, const TSearchConfig&) {
    const NRearrConf::TRearrangeRuleParams params(options);
    return new TRTYGeoRule(params);
}

REGISTER_REARRANGE_RULE(RTYGeo, CreateRTYGeoRule);
