#include "auth.h"
#include "replier.h"
#include "exception.h"
#include "client.h"

#include <search/config/virthost.h>
#include <search/session/comsearch.h>
#include <search/session/reqenv.h>

TAtomic IReplyContext::GlobalRequestsCounter = 0;
TAtomic IReplyContext::GlobalActiveObjects = 0;

EDeadlineCorrectionResult IReplyContext::DeadlineCorrection(const TDuration& scatterTimeout) {
    TCgiParameters& cgi = MutableRequestData().CgiParam;
    TCgiParameters::iterator iter = cgi.find(TStringBuf("timeout"));
    if (iter == cgi.end() && !IsTrue(cgi.Get("no_global_timeout")) && scatterTimeout.MicroSeconds()) {
        cgi.InsertUnescaped("timeout", ToString(scatterTimeout.MicroSeconds()));
        iter = cgi.find(TStringBuf("timeout"));
    }
    if (iter != cgi.end()) {
        const ui64 now = MicroSeconds();
        const ui64 elapsed = now < GetRequestData().RequestBeginTime()
            ? 0
            : now - GetRequestData().RequestBeginTime();
        ui64 timeout;
        if (!TryFromString<ui64>(iter->second, timeout) || timeout == 0) {
            return EDeadlineCorrectionResult::dcrIncorrectDeadline;
        }
        if (elapsed >= timeout * 0.9) {
            return EDeadlineCorrectionResult::dcrRequestExpired;
        }
        RequestDeadline = TInstant::MicroSeconds(GetRequestData().RequestBeginTime() + timeout);
        iter->second = ToString(timeout - elapsed);
        return EDeadlineCorrectionResult::dcrOK;
    } else {
        return EDeadlineCorrectionResult::dcrNoDeadline;
    }
}

TAtomic ISearchReplier::GlobalActiveObjects = 0;

ISearchReplier::ISearchReplier(IReplyContext::TPtr context, const THttpStatusManagerConfig* config)
    : Context(context)
{
    if (config) {
        HttpStatusConfig = *config;
    }
    AtomicIncrement(GlobalActiveObjects);
}

ISearchReplier::~ISearchReplier() {
    AtomicDecrement(GlobalActiveObjects);
}

void ISearchReplier::Process(void* /*ThreadSpecificResource*/) {
    THolder<ISearchReplier> this_(this);
    SearchAndReply();
}

void ISearchReplier::Reply() {
    Context->SetCommonQueueTime(TInstant::Now());
    THolder<ISearchReplier> cleanup(this);
    IThreadPool* queue = DoSelectHandler();
    if (!queue) {
        SearchAndReply();
        return;
    }

    if (queue->Add(cleanup.Get())) {
        Y_UNUSED(cleanup.Release());
    } else {
        OnQueueFailure();
    }
}

void ISearchReplier::OnQueueFailure() {
    Y_ASSERT(Context);
    MakeErrorPage(Context, HTTP_SERVICE_UNAVAILABLE, "Cannot queue request");
}

IThreadPool* ISearchReplier::GetHandler(ERequestType requestType) {
    const TSearchHandlers* handlers = GetSearchHandlers();
    if (!handlers) {
        return nullptr;
    }

    switch (requestType) {
    case RT_Fetch:
        return handlers->PassagesHandler.Get();
    case RT_Factors:
        return handlers->FactorsHandler.Get();
    case RT_Info:
        return handlers->InfoHandler.Get();
    case RT_DbgSearch:
        return handlers->DbgReqsHandler.Get();
    case RT_Reask:
        return handlers->ReAskHandler.Get();
    case RT_LongSearch:
        return handlers->LongReqsHandler.Get();
    case RT_PreSearch:
        return handlers->PreSearchReqsHandler.Get();
    default:
        break;
    }
    return handlers->MainHandler.Get();
}

TCommonSearchReplier::TCommonSearchReplier(IReplyContext::TPtr context, const TCommonSearch* commonSearcher, const THttpStatusManagerConfig* httpStatusConfig)
    : ISearchReplier(context, httpStatusConfig)
    , CommonSearcher(commonSearcher)
    , LogFrame(nullptr)
    , StatFrame(nullptr)
    , RequestType(RT_Unknown)
{
    VERIFY_WITH_LOG(!!CommonSearcher, "CommonSearcher can't be nullptr");
    RequestType = CommonSearcher->RequestType(Context->GetRequestData().CgiParam);
}

void TCommonSearchReplier::CreateLogFrame() {
    VERIFY_WITH_LOG(!!CommonSearcher, "CommonSearcher can't be nullptr");
    if (!LogFrame) {
        LogFrame = TSelfFlushLogFramePtr(MakeHolder<TSelfFlushLogFrame>(*CommonSearcher->GetEventLog()).Release());
    }
    const size_t queueSize = 0;
    const NEvClass::TEnqueueYSRequest::QType qtype = NEvClass::TEnqueueYSRequest::SEARCH;
    TStringBuf addr = Context->GetRequestData().RemoteAddr();

    LogFrame->LogEvent(NEvClass::TEnqueueYSRequest(Context->GetRequestId(), qtype, queueSize, TString{Context->GetRequestData().ScriptName()}));
    LogFrame->LogEvent(NEvClass::TPeerName(!addr.empty() ? TString{addr} : "no addr"));
}

IThreadPool* TCommonSearchReplier::DoSelectHandler() {
    CreateLogFrame();
    return GetHandler(RequestType);
}

const TSearchHandlers* TCommonSearchReplier::GetSearchHandlers() const {
    return CommonSearcher;
}

void ISearchReplier::SearchAndReply() {
    try {
        auto deadlineCheckResult = Context->DeadlineCorrection(GetDefaultTimeout());
        if (deadlineCheckResult == EDeadlineCorrectionResult::dcrRequestExpired) {
            TString additionalMessage = "no info";
            try {
                OnRequestExpired(HttpStatusConfig.TimeoutStatus);
            } catch (...) {
                additionalMessage = CurrentExceptionMessage();
            }
            ythrow TSearchException(HttpStatusConfig.TimeoutStatus, yxTIMEOUT) << "request timeout: " << additionalMessage;
        } else if (deadlineCheckResult == EDeadlineCorrectionResult::dcrIncorrectDeadline) {
            ythrow TSearchException(HttpStatusConfig.SyntaxErrorStatus, yxSYNTAX_ERROR) << "incorrect &timeout=<value>";
        }
        Context->MakeAuth();
        DoSearchAndReply();
    } catch (const NSaas::TNotAuthorizedException& e) {
        OnAccessDenied(e.GetInfo());
        MakeErrorPage(Context, e.GetHttpCode(), e.what());
    } catch (const TSearchException& se) {
        MakeErrorPage(Context, se.GetHttpCode(), se.what());
    } catch (const yexception& e) {
        MakeErrorPage(Context, HttpStatusConfig.UnknownErrorStatus, e.what());
        WARNING_LOG << "Unexpected exception in SearchAndReply [" << Context->GetRequestId() << "]: " << e.what() << Endl;
    }
}

void TCommonSearchReplier::DoSearchAndReply() {
    switch (RequestType) {
    case RT_Fetch:
        ProcessFetch();
        break;
    case RT_Info:
        if (!CommonSearcher->InfoRequestRequiresPseudoSearch(Context->MutableRequestData())) {
            ProcessInfo();
            break;
        } else {
            [[fallthrough]];
        }
    default:
        ProcessRequest();
        break;
    }
}

TMakePageContext TCommonSearchReplier::CreateMakePageContext() {
    VERIFY_WITH_LOG(!!CommonSearcher, "CommonSearcher can't be nullptr");
    if (!LogFrame) {
        LogFrame = TSelfFlushLogFramePtr(new TSelfFlushLogFrame(*CommonSearcher->GetEventLog()));
    }

    if (!StatFrame) {
        StatFrame = TUnistatFramePtr(new TUnistatFrameWithStat(Stat));
    }

    TMakePageContext context = {
        CommonSearcher,
        &Context->MutableRequestData(),
        nullptr,
        {
            &Context->Output(),
            nullptr,
            LogFrame.Get(),
            StatFrame.Get()
        },
        Context->GetBuf(),
        (ui32)Context->GetRequestId(),
        RequestType,
        !Context->IsHttp(),
        this,
        GetRUsageCheckPoint(),
    };
    return context;
}

bool TCommonSearchReplier::ProcessRequest()
{
    return (MakePage(CreateMakePageContext()) == YXOK);
}

bool TCommonSearchReplier::ProcessFetch()
{
    return (MakePassages(CreateMakePageContext()) == YXOK);
}

bool TCommonSearchReplier::ProcessInfo()
{
    return (MakeInfo(CreateMakePageContext()) == YXOK);
}

int TCommonSearchReplier::BeforeReport(ISearchContext* ctx) const {
    Stat.TotalDocsCount = ctx->Cluster()->TotalDocCount(0);
    Stat.UnanswerCount = ctx->ReqEnv()->BaseSearchNotRespondCount();
    Stat.CacheHit = static_cast<TReqEnv*>(ctx->ReqEnv())->CacheHit();
    Stat.AnswerIsComplete = static_cast<TReqEnv*>(ctx->ReqEnv())->AnswerIsComplete() ?
            TStatistics::AS_COMPLETE : TStatistics::AS_INCOMPLETE;

    Stat.SearchErrors = "";
    if(static_cast<TReqEnv*>(ctx->ReqEnv())->GetProperties()->Has("SearchErrors.debug")) {
        Stat.SearchErrors = static_cast<TReqEnv*>(ctx->ReqEnv())->GetProperties()->at("SearchErrors.debug");
    }

    return 0;
}

void TCommonSearchReplier::AfterReport(ISearchContext* ctx) const {
    auto stats = ctx->ReqEnv()->GetReportStats();
    Stat.ReportByteSize = stats.ReportByteSize;
    Stat.ReportDocsCount = stats.ReportDocsCount;
}

void TCommonSearchReplier::AfterReport(IPassageContext* ctx) const {
    auto stats = ctx->GetReportStats();
    Stat.ReportByteSize = stats.ReportByteSize;
    Stat.ReportDocsCount = stats.ReportDocsCount;
    Stat.TotalDocsCount = stats.TotalDocsCount;
}
