#include "pagecallback.h"
#include "httpstatusmanager.h"
#include "misspellcorrector.h"

#include <saas/searchproxy/configs/searchserverconfig.h>
#include <saas/library/daemon_base/metrics/servicemetrics.h>
#include <saas/library/report_builder/abstract.h>
#include <saas/searchproxy/common/abstract.h>
#include <saas/searchproxy/common/nameextractors.h>
#include <saas/library/searchserver/client.h>
#include <saas/library/searchserver/delay.h>
#include <saas/library/searchserver/exception.h>
#include <saas/searchproxy/unistat_signals/signals.h>
#include <saas/searchproxy/logging/common.h>
#include <saas/searchproxy/logging/incoming_log.h>
#include <saas/searchproxy/logging/error_log.h>
#include <saas/searchproxy/logging/fetch_log.h>
#include <saas/searchproxy/logging/access_log.h>
#include <saas/searchproxy/logging/reqans_log.h>
#include <saas/searchproxy/report/abstract/reply_data.h>
#include <saas/searchproxy/report/new/converter.h>

#include <search/config/virthost.h>
#include <search/reqparam/treat_cgi_request.h>
#include <search/session/reqenv.h>
#include <search/request/data/reqdata.h>

#include <library/cpp/http/misc/httpcodes.h>

#include <util/string/cast.h>
#include <library/cpp/deprecated/atomic/atomic.h>
#include <util/datetime/base.h>

namespace {
    static TAtomic CallbackRequestId;

    class TAutoExtendedSearchContext: public TExtendedSearchContext {
    private:
        IRequestContext* const RequestContext;
        ISearchContext* SearchContext;

    public:
        TAutoExtendedSearchContext(IRequestContext* requestContext)
            : TExtendedSearchContext(&SearchContext)
            , RequestContext(requestContext)
            , SearchContext(RequestContext->CreateContext())
        {
            Y_ASSERT(SC());
        }

        ~TAutoExtendedSearchContext()
        {
            RequestContext->DestroyContext(SearchContext);
        }
    };

    class TSearchReplyData: public ISearchReplyData {
    public:
        TSearchReplyData(ISearchContext& searchContext)
            : SearchContext(searchContext)
            , ReqEnv(dynamic_cast<TReqEnv&>(*searchContext.ReqEnv()))
        {}

        void FillArchiveInfo(int group, int doc, const TGroupingIndex& gi, bool passages, TMainDocArchiveInfo& info) const override
        {
            const IArchiveDocInfo* aa = ReqEnv.GetArchiveAccessor(group, doc, gi);

            info.DocId =  aa->DocHandle();
            info.Url = aa->DocUrl(0);
            aa->ReadDocProperty(DP_URL, DP_URL_HREF, &info.UrlProperty);
            info.Relevance = SearchContext.Cluster()->DocRelevance(group, doc, gi);
            info.HeadLine = aa->DocHeadline();
            info.Title = aa->DocTitle();

            if (passages) {
                for (int i = 0; i < aa->DocPassageCount(); ++i)
                    info.Passages.push_back(aa->DocPassage(i));
            }
        }

        void SerializeAttributes(int group, int doc, const TGroupingIndex& gi, TArrayRef<const char * const> attrNames, IAttributeWriter& writer, bool firstStage) const override {
            const IArchiveDocInfo* aa = ReqEnv.GetArchiveAccessor(group, doc, gi);
            aa->SerializeAttributes(attrNames, writer);
            if (firstStage) {
                aa->SerializeFirstStageAttributes(attrNames, writer);
            }
        }
        TSearcherPropsRef GetSearchProperties() const override {
            return ReqEnv.GetProperties();
        }

        void FillReportStat(TFullReportInfo& info) const override {
            ICluster& cluster =  *SearchContext.Cluster();
            info.Stat[0] = cluster.TotalDocCount(0);
            info.Stat[1] = cluster.TotalDocCount(1);
            info.Stat[2] = cluster.TotalDocCount(2);
        }

        void FillGroupingInfo(const TGroupingIndex& gi, TGroupingInfo& info) const override {
            ICluster& cluster =  *SearchContext.Cluster();
            info.Stat[0] = cluster.GroupingGroupCount(0, gi);
            info.Stat[1] = cluster.GroupingGroupCount(1, gi);
            info.Stat[2] = cluster.GroupingGroupCount(2, gi);
            info.GroupsCount = cluster.GroupingSize(gi);
        }

        void FillGroupInfo(int group, const TGroupingIndex& gi, TGroupInfo& info) const override {
            ICluster& cluster =  *SearchContext.Cluster();
            info.Stat[0] = cluster.GroupDocCount(0, group, gi);
            info.Stat[1] = cluster.GroupDocCount(1, group, gi);
            info.Stat[2] = cluster.GroupDocCount(2, group, gi);
            info.Relevance = cluster.GroupRelevance(group, gi);
            info.DocsCount = cluster.GroupSize(group, gi);
            info.CategString = cluster.GroupCategStr(group, gi);
        }

        void FillDocCategInfo(int group, int doc, const TGroupingIndex& gi, const TString& attrName, int docInCateg, TDocCategInfo& info) const override {
            const IIndexProperty* indexProperty = SearchContext.IndexProperty();
            Y_ASSERT(indexProperty);
            ICluster& cluster =  *SearchContext.Cluster();

            TCateg categ = cluster.DocCategId(group, doc, gi, attrName.data(), docInCateg);
            while (categ != END_CATEG) {
                info.Categs.push_back(categ);
                info.CategsNames.push_back(indexProperty->CategName(attrName.data(), categ));
                categ = indexProperty->CategParent(attrName.data(), categ);
            }
        }

        void FillIndexAttrsInfo(int group, int doc, const TGroupingIndex& gi, TAttrsInfo& info) const override {
            const IIndexProperty* indexProperty = SearchContext.IndexProperty();
            if (!indexProperty) {
                return;
            }
            ICluster& cluster =  *SearchContext.Cluster();
            for (int i = 0; i < indexProperty->AttrCount(); ++i) {
                const char* attrName = indexProperty->Attr(i);
                info.Attrs.push_back(attrName);
                info.DocsCount.push_back(cluster.DocCategCount(group, doc, gi, attrName));
            }
        }

        void FillErrors(TVector<TString>&) const override {
        }

        void FillPagingInfo(const TGroupingIndex& gi, const int groups,
                const long int requestedPage, TPagingInfo& paging) const override
        {
            ICluster& cluster =  *SearchContext.Cluster();
            const ui32 groupCount = cluster.GroupingSize(gi);
            paging.FirstGroup = (gi.Mode == GM_WIDE ? 0 : requestedPage) * groups;
            paging.LastGroup = paging.FirstGroup + groups;
            if (paging.LastGroup > groupCount) {
                paging.LastGroup = groupCount;
            }
        }

    private:
        ISearchContext& SearchContext;
        TReqEnv& ReqEnv;
    };

    class TReportBuilderContext : public IReportBuilderContext {
    public:
        TReportBuilderContext(TReqEnv* reqEnv, IRequestContext* requestContext = nullptr)
            : ReqEnv(reqEnv)
            , RequestContext(requestContext)
        {}

        const TCgiParameters& GetCgiParameters() const override {
            return ReqEnv->CgiParam;
        }

        const TSearchRequestData& GetRequestData() const override {
            Y_VERIFY(ReqEnv->RequestData);
            return *ReqEnv->RequestData;
        }

        const TRequestParams& GetRP() const override {
            return ReqEnv->RP();
        }

        TInstant GetRequestStartTime() const override {
            return TInstant::MicroSeconds(ReqEnv->RequestBeginTime());
        }

        long GetRequestedPage() const override {
            return ReqEnv->GetRequestedPage();
        }

        void MakeSimpleReply(const TBuffer& data, int) override {
            ReqEnv->PrintS(data.data(), data.size());
        }

        void AddReplyInfo(const TString& key, const TString& value) override {
            Y_VERIFY(RequestContext);
            RequestContext->HeaderOut(key.data(), value.data());
        }

        void Print(const TStringBuf& data) override {
            ReqEnv->PrintS(data.data(), data.size());
        }

    private:
        TReqEnv* ReqEnv;
        IRequestContext* RequestContext;
    };
}

int TSearchProxyPageCallback::HTTPHead(IRequestContext* requestContext,
    ISearchContext* searchContext)
{
    const TString origin{requestContext->HeaderInOrEmpty("Origin")};
    if (!!origin) {
        requestContext->HeaderOut("Access-Control-Allow-Origin", origin.data());
        requestContext->HeaderOut("Access-Control-Allow-Credentials", "true");
    }

    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());

    const TString& format = reqEnv->CgiParam.Get("report_format", 0);
    if (IReportFormatter::TPtr formatter = IReportFormatter::TFactory::Construct(format, reqEnv->CgiParam)) {
        TReportBuilderContext context(reqEnv, requestContext);
        formatter->FillHeaders(context);
        requestContext->EndOfHeader();
        return 0;
    } else {
        if (!format.empty()) {
            TAutoExtendedSearchContext extendedContext(requestContext);
            const int status = HttpStatusManager.CalculateHttpStatus(extendedContext);
            throw TSearchException(status, yxSYNTAX_ERROR) << "Unknown report format " << format;
        }
        return TBaseBuiltinPageCallback::HTTPHead(requestContext,
            searchContext);
    }
}

void TSearchProxyPageCallback::ParseRequest(ISearchContext* searchContext)
{
    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());
    if (!reqEnv->RP().GetParsedRequest()) {
        NCgiRequest::TreatParsedRequestParams(reqEnv->MutableRP(), reqEnv->CgiParam);
    }
}

class TSearchProxyExternalCgi: public ICustomCgi {
private:
    TInstant StartWizardTime;
public:
    void TreatCgiParams(TRequestParams& /*RP*/, const TCgiParameters& /*cgiParams*/) override {
        StartWizardTime = Now();
    }

    void TreatCgiParamsAfter(TRequestParams& RP, const TCgiParameters& /*cgiParams*/) override {
        TDuration durWiz = Now() - StartWizardTime;
        if (durWiz.MilliSeconds() > 10) {
            if (RP.SearchTimeBoundSoft.IsSet()) {
                if (RP.SearchTimeBoundSoft.Get() > durWiz.MicroSeconds())
                    RP.SearchTimeBoundSoft.Set(RP.SearchTimeBoundSoft.Get() - durWiz.MicroSeconds());
                else
                    RP.SearchTimeBoundSoft.Set(0);
            }
            if (RP.SearchTimeBound.IsSet()) {
                if (RP.SearchTimeBound.Get() > durWiz.MicroSeconds())
                    RP.SearchTimeBound.Set(RP.SearchTimeBound.Get() - durWiz.MicroSeconds());
                else
                    RP.SearchTimeBound.Set(0);
            }
        }
    }

};

int TSearchProxyPageCallback::MakePage(IRequestContext* requestContext)
{
    TAtomic id = AtomicIncrement(CallbackRequestId);

    TAutoExtendedSearchContext searchContext(requestContext);
    try {
        TAutoMetricGuard guard(Metric, TInstant::MicroSeconds(searchContext->ReqEnv()->RequestBeginTime()));
        NSearchProxy::NLogging::TIncomingLog::Log(id, searchContext);

        TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());
        const TSearchRequestData* rd = reqEnv->RequestData;

        reqEnv->MutableRP().ExternalCgi.Reset(new TSearchProxyExternalCgi);
        searchContext.SetRequestType(reqEnv->Owner.RequestType(rd->CgiParam));
        searchContext.SetProcessingStartTime(Now());

        CgiCorrector.FormContextCgi(requestContext, searchContext);

        const TInstant deadline = GetDeadline(searchContext);
        CheckDeadline(searchContext, deadline, true);
        {
            PreSearchDbgCall(*rd);
            requestContext->LoadOrSearch(searchContext);
            PostSearchDbgCall(*rd);
        }
        CheckDeadline(searchContext, deadline, false);

        const int status = HttpStatusManager.CalculateHttpStatus(searchContext);
        requestContext->SetHttpStatus(status);
        searchContext.SetHttpStatus(status);
        searchContext.SetReportStartTime(Now());

        int httpHead = HTTPHead(requestContext, searchContext);
        if (httpHead) {
            throw TSearchException(status, yxUNKNOWN_ERROR) << "HTTPHead: error code " << httpHead;
        }

        int beforeReport = requestContext->BeforeReport(searchContext);
        if (beforeReport) {
            throw TSearchException(status, yxUNKNOWN_ERROR) << "BeforeReport: error code " << beforeReport;
        }

        int report = Report(searchContext);
        if (report) {
            throw TSearchException(status, yxUNKNOWN_ERROR) << "Report: error code " << report;
        }

        auto stats = reqEnv->GetReportStats();
        searchContext.SetReportByteSize(stats.ReportByteSize);
        searchContext.SetReportDocsCount(stats.ReportDocsCount);
        TSaasSearchProxySignals::DoUnistatRecord(searchContext);
        NSearchProxy::NLogging::TInfoLog::Log(id, searchContext);
        if (ServiceConfig.IsReqAnsLogEnabled()) {
            auto loggingResult = NSearchProxy::NLogging::TReqAnsLog::Log(id, searchContext, ServiceConfig.GetFactorsLogAggregationStrategy());
            if (loggingResult.Error || loggingResult.Incosistency) {
                const TString serviceName = TServiceNameExtractor::ExtractServiceName(reqEnv->CgiParam);
                if (loggingResult.Error) {
                    TSaasSearchProxySignals::DoUnistatReqansLoggingError(serviceName);
                }
                if (loggingResult.Incosistency) {
                    TSaasSearchProxySignals::DoUnistatReqansLoggingIncosistency(serviceName);
                }
            }
        }
    } catch (...) {
        NSearchProxy::NLogging::TErrorLog::Log(id, searchContext.GetRequestData(), CurrentExceptionMessage());
        const TString service = searchContext.GetRequestData().CgiParam.Get(NSearchProxyCgi::service);
        TSaasSearchProxySignals::DoUnistatExceptionRecord(service);
        throw;
    }
    return yxOK;
}

int TSearchProxyPageCallback::Report(ISearchContext* searchContext)
{
    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());

    if (reqEnv->Owner.Config->TwoStepQuery) {
        const TString attr = reqEnv->Get("ag", 0);
        reqEnv->GetPage(reqEnv->GetRequestedPage(), TGroupingIndex(!attr ? GM_FLAT : GM_DEEP, attr));
    }

    const TString& format = reqEnv->CgiParam.Get("report_format", 0);
    if (IReportFormatter::TPtr formatter = IReportFormatter::TFactory::Construct(format, reqEnv->CgiParam)) {
        TSearchReplyData replyData(*searchContext);
        TReportBuilderContext context(reqEnv);
        TReportConstructor constructor(replyData, context, SafeReport);
        constructor.BuildReport(*formatter);
        context.MakeSimpleReply(formatter->GetResult(), HTTP_OK);
        return 0;
    } else {
        return TBaseBuiltinPageCallback::Report(searchContext);
    }
}

int TSearchProxyPageCallback::ReportSnippets(IPassageContext* passageContext, NSearch::IOutputContext* out, const TSearchRequestData& rd) {
    auto ctx = CheckedCast<TPassageContext*>(passageContext);
    Y_ASSERT(ctx);

    TAtomic id = AtomicIncrement(CallbackRequestId);
    TAutoMetricGuard guard(Metric, TInstant::MicroSeconds(ctx->StartExecTime()));
    TPassageRequestInfo passageInfo = {
        *ctx,
        rd
    };

    NSearchProxy::NLogging::TIncomingLog::Log(id, passageInfo);
    int res = TBaseBuiltinPageCallback::ReportSnippets(passageContext, out, rd);
    NSearchProxy::NLogging::TFetchLog::Log(id, passageInfo);

    return res;
}

void TSearchProxyPageCallback::CheckDeadline(TExtendedSearchContext& searchContext, TInstant deadline, bool critical) const
{
    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());
    const TInstant now = Now();
    if (deadline > now) {
        if (critical && !IsTrue(reqEnv->CgiParam.Get("no_global_timeout"))) {
            reqEnv->RemoveField("timeout");
            reqEnv->FormFieldInsert("timeout", ToString((deadline - now).MicroSeconds()).data());
        }

        return;
    }

    if (critical || reqEnv->ErrorCode() == yxOK) {
        searchContext.MarkTimeouted();
    }
    if (critical) {
        ythrow TSearchException(HttpStatusManager.CalculateHttpStatus(searchContext), yxTIMEOUT) << "Request timeout";
    }
}

TInstant TSearchProxyPageCallback::GetDeadline(const TExtendedSearchContext& searchContext) const
{
    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());
    ui64 timeout = reqEnv->Owner.Config->DefaultScatterOptions.TimeoutTable.front().MicroSeconds();
    if (reqEnv->GetFieldCount("timeout") && !TryFromString(reqEnv->Get("timeout", 0), timeout)) {
        ythrow TSearchException(HTTP_BAD_REQUEST, yxINCOMPATIBLE_REQ_PARAMS) << "Incorrect parameter: timeout";
    }

    return timeout ? TInstant::MicroSeconds(reqEnv->RequestBeginTime() + timeout) : TInstant::Max();
}

TSearchProxyPageCallback::TSearchProxyPageCallback(
    IPageCallback* backupCallback,
    const TServiceConfig& serviceConfig,
    const IHttpStatusManager& httpStatusManager,
    ICgiCorrector& cgiCorrector,
    TServiceMetricPtr metric
)
    : TBaseBuiltinPageCallback(backupCallback)
    , ServiceConfig(serviceConfig)
    , HttpStatusManager(httpStatusManager)
    , CgiCorrector(cgiCorrector)
    , Metric(metric)
{
    SafeReport = ServiceConfig.GetMetaSearchConfig()->SafeReport;
}

TSearchProxyPageCallback::~TSearchProxyPageCallback() {
}

void TSearchProxyPageCallback::PreSearchDbgCall(const TSearchRequestData& rd)
{
    TSearchRequestDelay::Process(rd, "sp_presearch");
}

void TSearchProxyPageCallback::PostSearchDbgCall(const TSearchRequestData& rd)
{
    TSearchRequestDelay::Process(rd, "sp_postsearch");
}
