#include "raiisearch.h"
#include "search.h"

#include <saas/searchproxy/common/messages.h>
#include <saas/searchproxy/common/serverinfo.h>
#include <saas/searchproxy/search_meta/cgi_corrector.h>
#include <saas/searchproxy/search_meta/httpstatusmanager.h>
#include <saas/searchproxy/search_meta/httpstatusmanagerfactory.h>
#include <saas/searchproxy/search_meta/misspellcorrector.h>
#include <saas/searchproxy/search_meta/misspellcorrectorfactory.h>
#include <saas/searchproxy/search_meta/pagecallback.h>
#include <saas/searchproxy/search_meta/template_corrector.h>

#include <search/common/cphash.h>

#include <saas/searchproxy/configs/searchproxyconfig.h>
#include <saas/searchproxy/configs/serviceconfig.h>
#include <saas/library/daemon_base/metrics/servicemetrics.h>
#include <saas/library/metasearch/helpers/rearrange.h>
#include <saas/library/searchserver/exception.h>
#include <saas/library/searchserver/protocol.h>
#include <saas/library/unistat_signals/factory.h>
#include <saas/util/queue.h>

#include <search/report/builtin/registrar.h>
#include <search/report/manager/factory.h>

#include <search/session/manager.h>

#include <search/wizard/remote/remote_wizard.h>
#include <search/wizard/remote/wizard_params.h>

#include <search/web/core/rearrange.h>

#include <library/cpp/object_factory/object_factory.h>
#include <library/cpp/eventlog/eventlog_int.h>
#include <library/cpp/eventlog/threaded_eventlog.h>

#include <library/cpp/charset/ci_string.h>
#include <util/generic/yexception.h>
#include <util/digest/murmur.h>
#include <library/cpp/string_utils/quote/quote.h>

namespace {
    bool HasRealEventLog(const TServiceConfig& config) {
        return !!config.GetEventLogName();
    }

    TEventLogPtr CreateNullEventLog() {
        return new TEventLog(NEvClass::Factory()->CurrentFormat());
    }

    TEventLogPtr CreateEventLog(const TServiceConfig& config) {
        const TString& name = config.GetEventLogName();
        if (!name) {
            return CreateNullEventLog();
        }

        TMaybe<TEventLogFormat> eventLogFormat = ParseEventLogFormat(config.GetEventLogCompressionFormat());
        TEventLogPtr log = new TEventLog(name, NEvClass::Factory()->CurrentFormat(), {}, eventLogFormat);
        return new TThreadedEventLog(log);
    }
}

TDuration NMetaSearch::TReplier::GetDefaultTimeout() const {
    Y_ASSERT(CommonSearcher);
    const TSearchConfig& config = *CommonSearcher->Config;
    if (config.DefaultScatterOptions.TimeoutTable.size()) {
        return config.DefaultScatterOptions.TimeoutTable.front();
    } else {
        return TDuration::MilliSeconds(100);
    }
}

NMetaSearch::TReplier::TReplier(const TString& service, IReplyContext::TPtr context, const TCommonSearch* commonSearcher, const THttpStatusManagerConfig* httpStatusConfig)
    : TCommonSearchReplier(context, commonSearcher, httpStatusConfig)
{
    StatFrame = NSaas::GetGlobalUnistatFrameFactory().SpawnFrame(service);
}

TRaiiSearch::TReportFactorySetter reportFactorySetter;

NCgiHash::THashRegistrator<TRaiiSearch::GetTextKpsHash> cpTextKps("TextKps");

TRaiiSearch::TRaiiSearch(const TServiceConfig& serviceConfig, const TSourcesConfig& searchConfig
        , TRaiiSearchParams* params, TEventLogPtr eventlog)
    : TMetaSearch(params)
    , ServiceConfig(serviceConfig)
    , SearchConfig(MakeHolder<TSearchConfig>(searchConfig))
    , HttpStatusManager(CreateHttpStatusManager(serviceConfig.GetHttpStatusManagerConfig()))
    , MisspellCorrector(CreateMisspellCorrector(searchConfig))
    , TemplateCorrector(CreateTemplateCorrector(searchConfig))
    , EventLog(!!eventlog ? eventlog : CreateEventLog(serviceConfig))
    , NullEventLog(CreateNullEventLog())
    , CgiCorrector(new TFakeCgiCorrector)
    , EventLogEnabled(serviceConfig.IsEventLogEnabled())
{
}

TRaiiSearch::~TRaiiSearch()
{
    RaiiSearchClose();
}

void TRaiiSearch::ReopenLog() const
{
    SearchConfig->ReopenLog();
    EventLog->ReopenLog();
    NullEventLog->ReopenLog();
}

IPageCallback* TRaiiSearch::CreatePageCallback(
    IPageCallback* backupCallback) const
{
    return new TSearchProxyPageCallback(
        backupCallback,
        ServiceConfig,
        *HttpStatusManager,
        *CgiCorrector,
        Metric
    );
}

IThreadPool* TRaiiSearch::CreateThreadPool(size_t treadCount, size_t queueSize, const TString& threadName)
{
    if (ServiceConfig.UseSmartQueue()) {
        return CreateRTYQueue(treadCount, queueSize).Release();
    }

    return TMetaSearch::CreateThreadPool(treadCount, queueSize, threadName);
}

void TRaiiSearch::RaiiSearchOpen()
{
    if (IsSearching()) {
        return;
    }

    THolder<TSearchConfig> config = MakeHolder<TSearchConfig>(*SearchConfig);
    THolder<IRequestWizard> wizard;
    if (config->WizardConfig) {
        wizard = MakeHolder<TRemoteWizard>();
        wizard->Init(*config->WizardConfig);
    }

    if (TAutoPtr<NCgiHash::IHashFunction> hf = NCgiHash::THashFunctionFactory::Construct(config->ProtoCollection_.GetRouteHashType())) {
        config->HashFunction = hf->Get();
        config->MetaHashFunction = hf->Get();
    }

    if (ServiceConfig.GetName() && !config->ProtoCollection_.GetCollectionName()) {
        config->ProtoCollection_.SetCollectionName(ServiceConfig.GetName());
    }

    if (int res = SearchOpen(config.Release(), wizard.Release())) {
        ythrow yexception() << "SearchOpen failed with status: " << res;
    }
}

void TRaiiSearch::RaiiSearchClose()
{
    if (IsSearching()) {
        SearchClose();
    }
}

void TRaiiSearch::EnableEventLog() {
    if (!HasRealEventLog(ServiceConfig)) {
        throw yexception() << "Real EventLog is not enabled";
    }

    EventLogEnabled = true;
}

void TRaiiSearch::DisableEventLog() {
    EventLogEnabled = false;
}

TRequestHash TRaiiSearch::GetTextKpsHash(const TCgiParameters& cgi)
{
    const TString& text = cgi.Has("text") ? cgi.Get("text") : cgi.Get("qtree");
    const TString& kps  = cgi.Get("kps");
    TRequestHash result = 0;
    result += MurmurHash<ui64>(text.data(), text.size());
    result += MurmurHash<ui64>(kps.data(), kps.size());
    return result;
}

TRaiiSearch::TReportFactorySetter::TReportFactorySetter()
    : ReportFactory(NSearchReport::DefaultFactory())
{
    SetReportFactory(ReportFactory.Get());
    RegisterBuiltinPageCallbackConstructor("searchproxy",
        CreateSearchProxyPageCallback);
}

THolder<IMetaRearrange> TRaiiSearchParams::ConstructRearrange(const TConstructRearrangeEnv& env)
{
    return WebRearrangeCore(env);
}

namespace {
    class TUpperSearch: public NSearchProxy::TCommonMetaSearch
    {
    public:
        TUpperSearch(const TServiceConfig& serviceConfig, const TSourcesConfig& searchConfig, ICgiCorrectorPtr cgiCorrector
                , TServiceMetricPtr metric, bool addServiceShardingRearr, TEventLogPtr eventlog)
            : NSearchProxy::TCommonMetaSearch(serviceConfig, searchConfig, cgiCorrector, metric, eventlog)
        {
            if (addServiceShardingRearr) {
                NUtil::TRearrangeOptionsHelper helper(*SearchConfig->ProtoCollection_.MutableReArrangeOptions());
                helper.AddRule("ServiceSharding");
            }
        }
    };

    class TServiceSearch: public NSearchProxy::TCommonMetaSearch {
    public:
        TServiceSearch(const NSaas::TShardsDispatcher::TPtr sharding, const TServiceConfig& serviceConfig, const TSourcesConfig& searchConfig
                , ICgiCorrectorPtr cgiCorrector, TServiceMetricPtr metric, TEventLogPtr eventlog)
            : NSearchProxy::TCommonMetaSearch(serviceConfig, searchConfig, cgiCorrector, metric, eventlog)
        {
            if (sharding->GetContext().Type == NSaas::KeyPrefix) {
                NUtil::TRearrangeOptionsHelper helper(*SearchConfig->ProtoCollection_.MutableReArrangeOptions());
                helper.AddRule("XSharding");
                helper.RuleScheme("XSharding")["ShardBy"] = sharding->GetContext().ToString();
            }
        }
    };
}

TString NMetaSearch::TProxy::Name() const {
    return "meta";
}

bool NMetaSearch::TProxy::Process(IMessage* message) {
    if (auto info = message->As<TCollectSearchProxyServerInfo>()) {
        for (auto&& i : Searchers) {
            const TCommonSearch* search = i.second.Get();
            const IThreadPool* mainHandler = search ? search->MainHandler.Get() : nullptr;
            const IThreadPool* passagesHandler = search ? search->PassagesHandler.Get() : nullptr;

            auto& status = info->SearchQueues[i.first];
            if (mainHandler) {
                status.MainQueueSize = mainHandler->Size();
            }
            if (passagesHandler && (passagesHandler != mainHandler)) {
                status.FetchQueueSize = passagesHandler->Size();
            }
        }
        return true;
    }
    if (auto eventLogControl = message->As<NSearchProxy::TControlEventLogMessage>()) {
        auto searcher = Searchers.find(eventLogControl->Service);
        if (searcher != Searchers.end()) {
            eventLogControl->TargetServiceFound = true;
            if (eventLogControl->TargetState) {
                searcher->second->EnableEventLog();
            } else {
                searcher->second->DisableEventLog();
            }
            eventLogControl->ResultState = searcher->second->IsEventLogEnabled();
        }
        return true;
    }
    return false;
}

ISearchReplier::TPtr NMetaSearch::TProxy::BuildReplier(const TString& serviceCgi, IReplyContext::TPtr context)
{
    const TString ServiceDelimiter(",");
    if (Y_LIKELY(serviceCgi.find(ServiceDelimiter) == TString::npos)) {
        auto it = Searchers.find(serviceCgi);
        if (it == Searchers.end())
            return nullptr;
        TRaiiSearch::TPtr searcher = it->second;
        return new NMetaSearch::TReplier(serviceCgi, context, searcher.Get(), searcher->GetHttpStatusManagerConfig());
    } else {
        Y_ASSERT(Searchers.contains(UpperSearchName));
        TRaiiSearch::TPtr searcher = Searchers.find(UpperSearchName)->second;
        TStringBuf services(serviceCgi.data(), serviceCgi.size());
        TStringBuf service;
        while (services.NextTok(ServiceDelimiter, service)) {
            if (Y_UNLIKELY(!Searchers.contains(TString{service}))) {
                ythrow TSearchException(searcher->GetHttpStatusManagerConfig()->SyntaxErrorStatus) << "unknown service " << service << " in sequence " << serviceCgi;
            }
        }

        return new NMetaSearch::TReplier(UpperSearchName, context, searcher.Get(), searcher->GetHttpStatusManagerConfig());
    }
}

void NMetaSearch::TProxy::RegisterService(const NSearchMapParser::TSearchMapService& serviceInfo, const TServiceConfig& serviceConfig, const TSourcesConfig& sourcesConfig
    , ICgiCorrectorPtr cc, TServiceMetricPtr metric, TEventLogPtr eventlog)
{
    if (serviceConfig.GetMetaSearchConfig()) {
        Searchers[serviceInfo.Name] = new TServiceSearch(serviceInfo.ShardsDispatcher, serviceConfig, sourcesConfig, cc, metric, eventlog);
        INFO_LOG << "Registered MetaSearch for " << serviceInfo.Name << Endl;
    }
}

void NMetaSearch::TProxy::RegisterMetaService(const NSearchMapParser::TMetaService& serviceInfo, const TServiceConfig& serviceConfig, const TSourcesConfig& sourcesConfig
    , ICgiCorrectorPtr cc, TServiceMetricPtr metric, TEventLogPtr eventlog)
{
    if (serviceConfig.GetMetaSearchConfig()) {
        Searchers[serviceInfo.Name] = new TUpperSearch(serviceConfig, sourcesConfig, cc, metric, false, eventlog);
        INFO_LOG << "Registered MetaSearch for " << serviceInfo.Name << Endl;
    }
}

void NMetaSearch::TProxy::RegisterUpperService(const TServiceConfig& serviceConfig, const TSourcesConfig& sourcesConfig
    , ICgiCorrectorPtr cc, TServiceMetricPtr metric, TEventLogPtr eventlog)
{
    Searchers[UpperSearchName] = new TUpperSearch(serviceConfig, sourcesConfig, cc, metric, true, eventlog);
    INFO_LOG << "Registered MetaSearch for " << UpperSearchName << Endl;
}

NMetaSearch::TProxy::TFactory::TRegistrator<NMetaSearch::TProxy> NMetaSearch::TProxy::Registrator("meta");
