#include "searchproxyserver.h"
#include "searchproxyclient.h"

#include <saas/searchproxy/unistat_signals/factory.h>
#include <saas/searchproxy/unistat_signals/signals.h>
#include <saas/searchproxy/search_meta/raiisearch.h>
#include <saas/library/searchserver/exception.h>
#include <saas/library/searchserver/neh.h>
#include <saas/library/searchserver/protocol.h>
#include <saas/library/searchserver/server.h>
#include <saas/library/searchmap/searchmap.h>
#include <saas/util/network/address.h>
#include <saas/util/network/neh_server.h>
#include <saas/util/logging/messages.h>

#include <saas/searchproxy/logging/tvm_log.h>

#include <library/cpp/eventlog/eventlog_int.h>
#include <library/cpp/eventlog/threaded_eventlog.h>
#include <library/cpp/http/misc/httpcodes.h>
#include <library/cpp/logger/global/global.h>
#include <library/cpp/tvmauth/client/facade.h>

#include <util/datetime/base.h>
#include <util/generic/singleton.h>
#include <util/generic/ptr.h>
#include <util/generic/strbuf.h>
#include <util/generic/string.h>
#include <util/generic/vector.h>
#include <util/string/cast.h>
#include <util/system/fs.h>
#include <util/system/env.h>
#include <util/string/join.h>

using namespace NSearchMapParser;
using namespace NSearchProxy;
using namespace NUnistat;

ICgiCorrectorPtr TSearchProxyServer::CreateServiceCgiCorrector(const TString& service) {
    return new TServiceSearchCgiCorrector(service, Config.GetServiceConfig(service));
}

ICgiCorrectorPtr TSearchProxyServer::CreateGlobalCgiCorrector(const TString& service) {
    return new TServiceGlobalCgiCorrector(Config.GetServiceConfig(service));
}

ICgiCorrectorPtr TSearchProxyServer::CreateMetaCgiCorrector(const TString& service) {
    return new TMetaSearchCgiCorrector(service, Config.GetServiceConfig(service));
}

ICgiCorrectorPtr TSearchProxyServer::CreateUpperCgiCorrector() {
    return new TUpperSearchCgiCorrector(Config.GetServiceConfig(NMetaSearch::UpperSearchName));
}

TEventLogPtr OpenEventLog(const TString& eventLogPath, const TString& eventLogCompressionFormat) {
    if (!!eventLogPath) {
        TMaybe<TEventLogFormat> eventLogFormat = ParseEventLogFormat(eventLogCompressionFormat);
        TEventLogPtr log = new TEventLog(eventLogPath, NEvClass::Factory()->CurrentFormat(), {}, eventLogFormat);
        return new TThreadedEventLog(log);
    } else {
        return nullptr;
    }
}

namespace {

    inline TString WeightFromGroup(const TString& g) {
        TVector<TString> params = SplitString(g, "-", 0, KEEP_EMPTY_TOKENS);
        TString pInfo;
        if (params.size() > 1) {
            TVector<TString> p = SplitString(params[1], "@");
            pInfo = "@" + JoinStrings(p, "@");
        } else {
            pInfo += "@1";
        }
        return pInfo;
    }

    struct TSearchGroup {
        TMap<ui32, TVector<TSearchInformation>> SearchByWeight;
    };

    class TSearchScript {
    private:
        TMap<TString, TSearchGroup> Script;
        TVector<TSearchInformation> ScriptSD;
        TString MetaSearchNehProtocol;
        TString Suffix;
        bool Grouping = true;
        bool IsSd = false;
    public:

        TSearchScript(const TString& metaSearchNehProtocol, const TString& suffix, bool grouping) {
            MetaSearchNehProtocol = metaSearchNehProtocol;
            Suffix = suffix;
            Grouping = grouping;
        }

        void Add(const TSearchInformation& info) {
            if (info.IsSd) {
                IsSd = true;
                ScriptSD.push_back(info);
            } else
                Script[info.Group].SearchByWeight[info.GetWeight()].push_back(info);
        }

        TString Build() const {
            TStringStream result;
            if (IsSd) {
                for (auto&& s : ScriptSD) {
                    result << "sd://" << s.Name << WeightFromGroup(s.Group) << " ";
                }
                return result.Str();
            }
            for (auto&& g : Script) {
                TString pInfo = WeightFromGroup(g.first);

                if (Grouping) {
                    result << "(";
                }

                for (auto&& w : g.second.SearchByWeight) {
                    for (auto&& s : w.second) {
                        result << MetaSearchNehProtocol << "://";
                        result << s.GetDescriptionForNeh();
                        result << Suffix;
                        if (!Grouping) {
                            result << pInfo;
                        }
                        result << ' ';
                    }
                }

                if (Grouping) {
                    result << ")" << pInfo << " ";
                }

            }
            return result.Str();
        }
    };

    class TSearchSourceBuilder {
    private:
        TSearchSource SearchSource;
        TSearchScript SearchScript;
        TSearchScript FetchScript;
    public:
        TSearchSourceBuilder(size_t id, const TString& metaSearchProtocol, const TString& suffix, bool grouping)
            : SearchSource(id)
            , SearchScript(metaSearchProtocol, suffix, grouping)
            , FetchScript(metaSearchProtocol, suffix, grouping)
        {

        }

        TSearchSource& GetSearchSource() {
            return SearchSource;
        }

        TSearchSource Build() const {
            TSearchSource result = SearchSource;
            result.ProtoConfig_.SetSearchScript(SearchScript.Build());
            result.ProtoConfig_.SetSnippetScript(FetchScript.Build());
            return result;
        }

        TSearchScript& GetSearchScript() {
            return SearchScript;
        }

        TSearchScript& GetFetchScript() {
            return FetchScript;
        }

    };

    class TRealTvmClientWrapper: public NSearchProxy::ITvmClient {
    public:
        TRealTvmClientWrapper(TAtomicSharedPtr<NTvmAuth::TTvmClient> backend)
            : Backend(std::move(backend))
        {
        }

        TMaybe<ui32> CheckServiceTicket(const TString& ticketStr) const override {
            auto ticket = Backend->CheckServiceTicket(ticketStr);
            return ticket ? MakeMaybe<ui32>(ticket.GetSrc()) : Nothing();
        }

        TMaybe<ui64> CheckUserTicket(const TString& ticketStr) const override {
            auto ticket = Backend->CheckUserTicket(ticketStr);
{
    INFO_LOG << "CheckUserTicket: Uids: " << JoinSeq(", ", ticket.GetUids())
        << ", DefaulUid: " << ticket.GetDefaultUid() << Endl;
}
            return ticket ? MakeMaybe<ui64>(ticket.GetDefaultUid()) : Nothing();
        }

    private:
       TAtomicSharedPtr<NTvmAuth::TTvmClient> Backend;
    };

    auto MakeTvmClientSettings(ui32 selfId, const TFsPath& cacheDir) {
        NTvmAuth::NTvmApi::TClientSettings opts;
        opts.SetSelfTvmId(selfId);
        opts.EnableServiceTicketChecking();
        if (cacheDir) {
            const auto clientDir = cacheDir / ToString(selfId);
            clientDir.MkDir();
            opts.SetDiskCacheDir(clientDir.GetPath());
        }
        return opts;
    }

}

class TSearchProxySearchMapProcessor : public ISearchMapProcessor {
public:
    TSearchProxySearchMapProcessor(const TString& name, TSearchConfig& searchConfig, IHostResolver& hostResolver,
            THostSet& unresolved, const TString& suffix, TServiceConfig::TConsistencyPolicy consistencePolicy, bool grouping)
        : Name(name)
        , Suffix(suffix)
        , SearchConfig(searchConfig)
        , HostResolver(hostResolver)
        , Unresolved(unresolved)
        , ConsistencyPolicy(consistencePolicy)
        , Grouping(grouping)
    {
        SearcherIndex = 0;
    }

    void Finalize() {
        for (auto&& i : Sources) {
            TSearchSource source = i.second.Build();
            INFO_LOG << "Adding search source for " << source.ProtoConfig_.GetServerDescr().Quote() << " / " << source.ProtoConfig_.GetServerGroup() << ": " << source.ProtoConfig_.GetSearchScript() << " / size = " << SearchConfig.SearchSources.size() << Endl;
            SearchConfig.SearchSources.push_back(source);
        }
    }

    void Do(const TServiceSpecificOptions& /*sso*/, const TReplicaSpecificOptions& rso,
        const TInterval<TShardIndex>& /*interval*/, const TSearchInformation& host) override
    {
        if (rso.DisableSearch || host.DisableSearch) {
            INFO_LOG << "Shard " << Name << ':' << rso.Alias << ':' << host.Name << ':' << host.SearchPort << " is disabled" << Endl;
            return;
        }

        if (!host.IsSd && !HostResolver.CanBeResolved(host.Name, host.SearchPort)) {
            Unresolved.insert(host.Name);
            ERROR_LOG << "Shard " << Name << ':' << rso.Alias << ':' << host.Name << ':' << host.SearchPort << " skipped" << Endl;
            return;
        }

        const TString& sourceDescription = GetSourceDescription(host, rso.Alias);
        TSources::iterator source = Sources.find(sourceDescription);
        bool isNew = false;
        if (source == Sources.end()) {
            source = Sources.insert(std::make_pair(sourceDescription, TSearchSourceBuilder(++SearchConfig.RealSearchSourcesCount, MetaSearchNehProtocol, Suffix, Grouping))).first;
            isNew = true;
        }
        TSearchSourceBuilder& builder = source->second;

        builder.GetSearchSource().ProtoConfig_.SetServerGroup(host.Shards.ToString());
        builder.GetSearchSource().ProtoConfig_.SetServerDescr(Name + '#' + host.Shards.ToString());
        if (!host.DisableSearchFiltration)
            builder.GetSearchScript().Add(host);
        if (!host.DisableFetch)
            builder.GetFetchScript().Add(host);

        if (isNew) {
            builder.GetSearchSource().Remote = SearchConfig.CommonSourceOptions;
            ++builder.GetSearchSource().Remote.MaxAttempts;
        }
        if (host.IsSd) {
            builder.GetSearchSource().Remote.SearcherProtocol = "tcp2";
            builder.GetSearchSource().Remote.SearcherDeltaPort = 1;
        }
    }
private:
    inline TString GetSourceDescription(const TSearchInformation& host, const TString& config) {
        switch (ConsistencyPolicy) {
        case TServiceConfig::cpAllSources:
            return host.Shards.ToString() + "$" + ToString(++SearcherIndex);
        case TServiceConfig::cpLocalDC:
            return host.Shards.ToString() + "$" + ToString(++SourceByGroup[host.Group]);
        case TServiceConfig::cpNo:
            return host.Shards.ToString() + '$' + config;
        default:
            FAIL_LOG("incorrect consistency policy: %s", ToString(ConsistencyPolicy).data());
        }
    }

private:
    typedef TMap<TString, TSearchSourceBuilder> TSources;

private:
    TSources Sources;

    const TString Name;
    const TString Suffix;
    TSearchConfig& SearchConfig;
    IHostResolver& HostResolver;
    NSearchProxy::THostSet& Unresolved;
    TServiceConfig::TConsistencyPolicy ConsistencyPolicy;
    ui32 SearcherIndex;
    TMap<TString, ui32> SourceByGroup;
    bool Grouping = true;
};

IHostResolver::~IHostResolver() noexcept {
}

bool TDefaultHostResolver::CanBeResolved(const TString& host, ui16 port) noexcept {
    return NUtil::CanBeResolved(host, port);
}

TSearchSourcesFiller::TSearchSourcesFiller(const NSearchMapParser::TSearchMap& searchMap, IHostResolver& hostResolver)
    : SearchMap(searchMap)
    , HostResolver(hostResolver)
{
}

void TSearchSourcesFiller::FillSearchSources(const TString& service, TSearchConfig& config, const TString& suffix,
        TServiceConfig::TConsistencyPolicy consistencyPolicy, bool grouping, bool normalizeReplicas)
{
    auto serviceIterator = SearchMap.GetServiceMap().find(service);
    if (serviceIterator == SearchMap.GetServiceMap().end())
        ythrow yexception() << "cannot find service " << service;

    TSearchProxySearchMapProcessor processor(service, config, HostResolver, Unresolved, suffix, consistencyPolicy, grouping);
    if (!normalizeReplicas) {
        serviceIterator->second.ProcessSearchMap(processor);
    } else {
        auto serviceMapCopy = serviceIterator->second;
        serviceMapCopy.NormalizeIntervals();
        serviceMapCopy.ProcessSearchMap(processor);
    }
    processor.Finalize();
}

const NSearchProxy::THostSet& TSearchSourcesFiller::GetUnresolvedHosts() const {
    return Unresolved;
}

class TSearchProxyHttpServer: public TSearchServerBase {
private:
    TSearchProxyServer& Owner;

public:
    TSearchProxyHttpServer(TSearchProxyServer& owner)
        : TSearchServerBase(owner.GetConfig().GetHttpServerOptions(), owner.GetConfig().GetHttpServerInstances())
        , Owner(owner)
    {}

    TClientRequest* CreateClient() override {
        return new TSearchProxyClient(&Owner);
    }
};

class TSearchProxyNehServer: public TSearchNehServer {
private:
    TSearchProxyServer& Owner;

public:
    TSearchProxyNehServer(TSearchProxyServer& owner)
        : TSearchNehServer(NUtil::TAbstractNehServer::TOptions(owner.GetConfig().GetNehServerOptions(),
                                                              owner.GetConfig().GetNehServerSchemes()))
        , Owner(owner)
    {}

    TAutoPtr<IObjectInQueue> DoCreateClientRequest(ui64 /*id*/, NNeh::IRequestRef req) final {
        return new TSearchProxyNehRequest(&YServer, &Owner, req);
    }

    YandexHttpServer& GetYServer() {
        return YServer;
    }
};

void TSearchProxyServer::CreateSearchers() {
    const NSearchMapParser::TSearchMap& searchMap = Config.GetSearchMap();
    CHECK_WITH_LOG(searchMap.GetServiceMap());
    TDefaultHostResolver hostResolver;
    TSearchSourcesFiller ssb(searchMap, hostResolver);
    for (auto& iter : searchMap.GetServiceMap()) {
        const TServiceConfig& serviceConfig = Config.GetServiceConfig(iter.first);
        TSourcesConfig searchConfig = serviceConfig.GetSourcesConfig(/*metaservice=*/false);
        searchConfig.SearchScriptName = iter.first;
        // There always will present at least one server for each interval and
        // at least one interval for each service. Parser enforces that single
        // service has single protocol.
        searchConfig.CommonSourceOptions.ProtocolType = iter.second.SearchProtocol;
        if (!searchConfig.ServiceDiscoveryOptions.Enabled) {
            searchConfig.ServiceDiscoveryOptions = Config.GetServiceDiscoveryOptions();
        }
        TString suffix;
        if (searchConfig.CommonSourceOptions.ProtocolType != "proto") {
            suffix = "/search";
            searchConfig.TwoStepQuery = false;
            searchConfig.CommonSourceOptions.TwoStepQuery = false;
        }

        ssb.FillSearchSources(iter.first, searchConfig, suffix, serviceConfig.GetConsistencyPolicy(),
                serviceConfig.UseGroupingByDC(), serviceConfig.GetNormalizeReplicas());

        ICgiCorrectorPtr cc = CreateServiceCgiCorrector(iter.first);
        TServiceMetricPtr metric = ServiceMetrics.AddStaticMetric(iter.first);
        TEventLogPtr eventlog = OpenEventLog(serviceConfig.GetEventLogName(), serviceConfig.GetEventLogCompressionFormat());
        try {
            for (auto&& proxy : Proxies) {
                proxy.second->RegisterService(*searchMap.GetService(iter.first), serviceConfig, searchConfig,
                    cc, metric, eventlog);
            }
        } catch (const yexception& e) {
            IncorrectServices[iter.first] = e.what();
        }
        GlobalCgiCorrectors[iter.first] = CreateGlobalCgiCorrector(iter.first);
        Services.insert(iter.first);
    }
    UnresolvedHosts = ssb.GetUnresolvedHosts();

    for (auto& i : searchMap.GetMetaServices()) {
        const TServiceConfig& serviceConfig = Config.GetServiceConfig(i.Name);
        TSourcesConfig searchConfig = serviceConfig.GetSourcesConfig(/*metaservice=*/true);
        ui32 tierMax = 0;
        INFO_LOG << "Meta service " << i.Name << " building..." << Endl;
        for (auto& src : i.Components) {
            if (!src.SearchDisabled) {
                INFO_LOG << "Meta service " << i.Name << " component " << src.ServiceName << " construction..." << Endl;
                TSearchSource searchSource(++searchConfig.RealSearchSourcesCount);
                const THttpServerOptions& options = Config.GetNehServerOptions();
                searchSource.ProtoConfig_.SetServerGroup(src.ServiceName);
                searchSource.ProtoConfig_.SetServerDescr("#" + src.ServiceName);

                searchSource.Remote = searchConfig.CommonSourceOptions;
                searchSource.Remote.Tier = src.SearchPriority;
                tierMax = Max(src.SearchPriority, tierMax);

                searchSource.ProtoConfig_.SetSearchScript(InprocNehProtocol + "://localhost:" + ToString(options.Port) + '/' + src.ServiceName);

                searchConfig.SearchSources.push_back(searchSource);
                INFO_LOG << "Meta service " << i.Name << " component " << searchSource.ProtoConfig_.GetSearchScript() << " construction...OK" << Endl;
            }
        }
        searchConfig.ProtoCollection_.SetNTiers(tierMax + 1);
        searchConfig.TierSignificantTable.resize(searchConfig.ProtoCollection_.GetNTiers(), true);

        ICgiCorrectorPtr cc = CreateMetaCgiCorrector(i.Name);
        TServiceMetricPtr metric = ServiceMetrics.AddStaticMetric(i.Name);
        TEventLogPtr eventlog = OpenEventLog(serviceConfig.GetEventLogName(), serviceConfig.GetEventLogCompressionFormat());
        for (auto&& proxy : Proxies) {
            proxy.second->RegisterMetaService(*searchMap.GetMetaService(i.Name), serviceConfig, searchConfig, cc, metric, eventlog);
        }
        GlobalCgiCorrectors[i.Name] = CreateGlobalCgiCorrector(i.Name);
        Services.insert(i.Name);
        INFO_LOG << "Meta service " << i.Name << " building... OK" << Endl;
    }

    {
        const TServiceConfig& serviceConfig = Config.GetServiceConfig(NMetaSearch::UpperSearchName);
        TSourcesConfig sourcesConfig = serviceConfig.GetSourcesConfig(/*metaservice=*/true);

        TVector<TString> services;
        for (auto&& i : searchMap.GetServiceMap()) {
            services.push_back(i.first);
        }
        for (auto&& i : searchMap.GetMetaServices()) {
            services.push_back(i.Name);
        }
        for (auto&& service : services) {
            if (!service) {
                ERROR_LOG << "Incorrect empty service name" << Endl;
                continue;
            }
            TSearchSource searchSource(++sourcesConfig.RealSearchSourcesCount);
            const THttpServerOptions& options = Config.GetNehServerOptions();
            searchSource.ProtoConfig_.SetServerGroup(service);
            searchSource.ProtoConfig_.SetServerDescr("#" + service);
            searchSource.ProtoConfig_.SetSearchScript(InprocNehProtocol + "://localhost:" + ToString(options.Port) + '/' + service);
            searchSource.Remote = sourcesConfig.CommonSourceOptions;
            INFO_LOG << "Adding upper search source: " << searchSource.ProtoConfig_.GetSearchScript() << Endl;
            sourcesConfig.SearchSources.push_back(searchSource);
        }

        ICgiCorrectorPtr cc = CreateUpperCgiCorrector();
        TServiceMetricPtr metric = ServiceMetrics.AddStaticMetric(NMetaSearch::UpperSearchName);
        for (auto &&proxy : Proxies) {
            proxy.second->RegisterUpperService(serviceConfig, sourcesConfig, cc, metric, nullptr);
        }
    }

    CHECK_WITH_LOG(Proxies.contains("meta"));
    Proxies[""] = Proxies["meta"];
    CHECK_WITH_LOG(Proxies.contains("proxy"));
    Proxies["multi_proxy"] = Proxies["proxy"];
}

TSearchProxyServer::TSearchProxyServer(const TSearchProxyConfig& config, const TTestTvmSuite* testSuite)
    : Config(config)
    , HttpServer(new TSearchProxyHttpServer(*this))
    , NehServer(new TSearchProxyNehServer(*this))
    , Authorizer(config.GetAuthKeyWord())
    , ServiceMetrics(&GetGlobalMetrics(), "SearchProxy_")
    , FlowMirror(config.GetFlowMirrorConfig())
    , Broadcaster(config.GetSearchMap())
{
    InitTvmClient(testSuite);

    TSet<TString> keys;
    IProxy::TFactory::GetRegisteredKeys(keys);
    for (auto&& i : keys) {
        INFO_LOG << "Registered " << i << " proxy" << Endl;
        Proxies[i] = IProxy::TFactory::Construct(i);
    }

    CreateSearchers();

    TSaasSearchProxySignals::BuildSignals(Config);

    NSaas::SetGlobalUnistatFrameFactory(THolder(new TSaasProxyUnistatFrameFactory()));

    FlowMirror.RegisterMetrics(GetGlobalMetrics());

    if (Config.GetAppHostConfig().Enabled) {
        InitAppHost();
    }

    RegisterGlobalMessageProcessor(this);
}

TSearchProxyServer::~TSearchProxyServer() {
    UnregisterGlobalMessageProcessor(this);
    FlowMirror.UnregisterMetrics(GetGlobalMetrics());
}

void TSearchProxyServer::InitAppHost() {
    const auto& conf = Config.GetAppHostConfig();

    auto appService = [this](NAppHost::TServiceContextPtr ctx) {
        THolder<TAppHostNehRequest> request = MakeHolder<TAppHostNehRequest>(ctx);
        NThreading::TFuture<void> result = request->GetFuture();
        THolder<TSearchProxyNehRequest> requester = MakeHolder<TSearchProxyNehRequest>(&NehServer->GetYServer(), this, request.Release());
        if (const TString& ticket = ctx->GetTvmTicket()) {
            requester->RequestData().AddHeader("X-Ya-Service-Ticket", ticket);
        }
        requester.Release()->Process(nullptr);
        return result;
    };

    AppHostLoop = MakeHolder<NAppHost::TLoop>(NehServer->GetRequester());
    AppHostLoop->ApplyNehInputConnectionsLimits();

    if (conf.Tvm.Enabled) {
        CHECK_WITH_LOG(conf.Grpc.Enabled);
        AppHostLoop->EnableTvmServiceTickets(conf.Tvm.TvmId, conf.Tvm.AllowedSourceTvmIds);
    }

    if (conf.Grpc.Enabled) {
        CHECK_WITH_LOG(conf.Grpc.Port);
        AppHostLoop->EnableGrpc(conf.Grpc.Port, {
            .ReusePort = true,
            .Threads = static_cast<int>(conf.Grpc.Threads),
        });
    }

    AppHostLoop->Add(Config.GetAppHostConfig().Port, appService);
}

void TSearchProxyServer::BuildTvmClients(const TSearchProxyConfig::TTvmParams& params) {
    auto create = [](const auto& opts) {
        return MakeAtomicShared<NTvmAuth::TTvmClient>(opts, MakeIntrusive<NLogging::TTicketParser2TvmLog>());
    };

    const TFsPath cacheDir = params.DiskCacheDir ? params.DiskCacheDir + "/keys" : "";
    if (cacheDir) {
        cacheDir.MkDirs();
    }

    {
        auto opts = MakeTvmClientSettings(params.TvmId, cacheDir);
        if (params.EnableUserTickets) {
            opts.EnableUserTicketChecking(NTvmAuth::EBlackboxEnv::ProdYateam); // FIXME

            const TString tvmSecret = GetEnv("TVM_SECRET");
            Y_ENSURE(tvmSecret, "Tvm.EnableUserTickets is true but no TVM_SECRET environment variable is set");
            Y_ENSURE(params.AbcApiTvmId, "Tvm.EnableUserTickets is true but Tvm.AbcApiTvmId variable is not set");
            opts.EnableServiceTicketsFetchOptions(tvmSecret, {params.AbcApiTvmId});
        }
        MainTvmClientImpl = create(opts);
        TvmClients.Main = new TRealTvmClientWrapper(MainTvmClientImpl);
    }

    if (params.FlowMirrorTvmId) {
        auto opts = MakeTvmClientSettings(params.FlowMirrorTvmId, cacheDir);
        TvmClients.FlowMirror = new TRealTvmClientWrapper(create(opts));
    }
}

void TSearchProxyServer::InitTvmClient(const TTestTvmSuite* testSuite) {
    const auto& params = Config.GetTvmParams();
    if (!params.Enabled) {
        return;
    }

    if (testSuite) {
        TvmClients = testSuite->TvmClients;
        AbcResolver = testSuite->AbcResolver;
    } else {
        BuildTvmClients(params);
        if (params.EnableUserTickets) {
            AbcResolver = CreateAbcResolver();
        }
    }
}

void TSearchProxyServer::ReopenLog() const {
    GetConfig().GetDaemonConfig().ReopenLog();
    TSet<TString> keys;
    IProxy::TFactory::GetRegisteredKeys(keys);
    for (auto&& key : keys) {
        Proxies.at(key)->ReopenLogs();
    }
}

bool TSearchProxyServer::Process(IMessage* message) {
    if (auto info = message->As<TCollectSearchProxyServerInfo>()) {
        info->UnresolvedHosts = UnresolvedHosts;
        info->IncorrectServices = IncorrectServices;
        return true;
    }
    if (message->As<TMessageReopenLogs>()) {
        ReopenLog();
        return true;
    }
    return false;
}

namespace {
    TVector<ui32> CollectAbcGroups(const TSearchProxyConfig& config) {
        THashSet<ui32> groups;
        {
            const auto& tvm = config.GetTvmParams();
            groups.insert(tvm.AllowedUserAbcGroups.begin(), tvm.AllowedUserAbcGroups.end());
        }
        for (const auto& [_, service] : config.GetServicesConfigs()) {
            const auto& tvm = service.GetTvmParams();
            if (tvm.Enabled) {
                groups.insert(tvm.AllowedUserAbcGroups.begin(), tvm.AllowedUserAbcGroups.end());
            }
        }
        return {groups.begin(), groups.end()};
    }
}

TAtomicSharedPtr<NSearchProxy::IAbcResolver> TSearchProxyServer::CreateAbcResolver() const {
    Y_ENSURE(MainTvmClientImpl);
    const auto& tvmConfig = Config.GetTvmParams();
    const TFsPath cacheDir = tvmConfig.DiskCacheDir ? tvmConfig.DiskCacheDir + "/abc" : "";
    if (cacheDir) {
        cacheDir.MkDirs();
    }
    return new NSearchProxy::TAbcResolver({
        .ApiTvmId = tvmConfig.AbcApiTvmId,
        .GroupsToResolve = CollectAbcGroups(Config),
        .TvmClient = MainTvmClientImpl,
        .CacheDir = cacheDir,
    });
}

void TSearchProxyServer::Start() {
    if (AbcResolver) {
        INFO_LOG << "Starting AbcResolver..." << Endl;
        AbcResolver->Start();
    }

    TSet<TString> keys;
    IProxy::TFactory::GetRegisteredKeys(keys);
    for (auto&& key : keys) {
        if (IncorrectServices.contains(key)) {
            continue;
        }
        Proxies.at(key)->Start();
        for (auto& i : Proxies.at(key)->GetFailedSearchers()) {
            IncorrectServices.insert(i);
        }
    }
    if (!IncorrectServices.empty()) {
        SendGlobalMessage<TSystemStatusMessage>("Has incorrect services", TSystemStatusMessage::ESystemStatus::ssSemiOK);
        if (!Config.GetDaemonConfig().GetController().DMOptions.Enabled) {
            AbortFromCorruptedConfig("Has incorrect services");
        }
    }

    Sleep(Config.GetSleepBeforeListen());

    CHECK_WITH_LOG(HttpServer);
    CHECK_WITH_LOG(NehServer);
    HttpServer->Start();
    NehServer->Start();
    if (Config.GetAppHostConfig().Grpc.Enabled) {
        AppHostLoop->StartGrpc();
    }
}

void TSearchProxyServer::Stop() {
    CHECK_WITH_LOG(HttpServer);
    CHECK_WITH_LOG(NehServer);
    HttpServer->Stop();
    if (Config.GetAppHostConfig().Grpc.Enabled) {
        AppHostLoop->Stop();
    }
    NehServer->Stop();

    TSet<TString> keys;
    IProxy::TFactory::GetRegisteredKeys(keys);
    for (auto&& key : keys) {
        Proxies.at(key)->Stop();
    }

    if (AbcResolver) {
        AbcResolver->Stop();
    }
}

bool TSearchProxyServer::HasService(const TString& service) const {
    if (service.find(',') == TString::npos) {
        return Services.contains(service);
    } else {
        return true;
    }
}

bool TSearchProxyServer::IsGoodService(const TString& service) const {
    return !IncorrectServices.contains(service);
}

IProxy::TPtr TSearchProxyServer::SelectProxy(const TCgiParameters& cgi, const TString& defaultProxy) {
    TString proxyType = cgi.Get("sp_meta_search");
    if (!proxyType) {
        proxyType = defaultProxy;
    }
    auto proxy = Proxies.find(proxyType);
    if (proxy == Proxies.end())
        return nullptr;
    return proxy->second;
}

ISearchReplier::TPtr TSearchProxyServer::BuildReplier(const TString& service, IReplyContext::TPtr context) {
    IProxy::TPtr proxy = SelectProxy(context->GetRequestData().CgiParam, Config.GetServiceConfig(service).GetDefaultMetaSearch());
    if (!proxy) {
        ythrow TSearchException(HTTP_BAD_REQUEST) << "unknown ProxyType/sp_meta_search " << context->GetRequestData().CgiParam.Get("sp_meta_search");
    }
    ISearchReplier::TPtr result = proxy->BuildReplier(service, context);
    if (!result) {
        ythrow TSearchException(HTTP_BAD_REQUEST) << "service " << service << " is not registered in ProxyType/sp_meta_search " << context->GetRequestData().CgiParam.Get("sp_meta_search");
    }
    return result;
}

void TSearchProxyServer::ApplyGlobalCorrections(const TString& serviceName, TCgiParameters& cgi) {
    auto it = GlobalCgiCorrectors.find(serviceName);
    if (it != GlobalCgiCorrectors.end()) {
        it->second->FormCgi(cgi, nullptr);
    }
}
