#include "common_task.h"
#include "sender_executor.h"

#include <saas/util/logging/tskv_log.h>
#include <saas/util/destroyer.h>
#include <saas/protos/rtyserver.pb.h>
#include <saas/indexerproxy/unistat_signals/signals.h>
#include <saas/indexerproxy/context/persqueue_context.h>
#include <saas/indexerproxy/context/backends_context.h>
#include <saas/indexerproxy/context/distributor_context.h>
#include <saas/indexerproxy/context/void_context.h>

#include <dict/dictutil/str.h>

#include <library/cpp/string_utils/base64/base64.h>

#include <util/digest/fnv.h>
#include <util/system/env.h>

void TCommonSenderTask::DoLogAndMetric(const TString& serviceName, int replyCode, bool fromMeta, const TString& message, bool logging) const {
    ui64 duration = (TInstant::Now() - Context->GetReceiveTime()).MilliSeconds();

    NUtil::TTSKVRecord entry("saas-ipr-log");
    entry.AddIsoEventTime();
    entry.ForceAdd("origin", Context->GetOrigin());
    entry.ForceAdd("ip", Context->GetRemoteAddr());
    entry.ForceAdd("url", Context->GetScriptName());
    entry.ForceAdd("cgi", Context->GetQueryString());
    entry.ForceAdd("service", serviceName);
    entry.Add("id", GetMessageId());
    entry.ForceAdd("size", Context->GetPost().Length());
    entry.ForceAdd("source", Context->GetRequestOptions().GetSource());
    entry.ForceAdd("status", GetStatusString(replyCode));
    entry.ForceAdd("http_code", replyCode);
    entry.ForceAdd("key", GetDocUrl());
    entry.ForceAdd("keyprefix", GetDocKeyPrefix());
    entry.ForceAdd("dtimestamp", GetDocTimestamp());
    entry.ForceAdd("duration", duration);
    entry.ForceAdd("message", message);
    entry.ForceAdd("kind", "ip");
    entry.ForceAdd("from_meta", fromMeta);
    entry.ForceAdd("deferred", Context->GetRequestOptions().IsDeferredMessage());

    auto document = Context->GetDocument();
    if (!!document) {
        entry.ForceAdd("action", document->GetAction().GetActionType());
        if (document->GetMessage().HasDocument()) {
            const auto& doc = document->GetMessage().GetDocument();
            if (doc.HasDeadlineMinutesUTC()) {
                entry.ForceAdd("doc_deadline", doc.GetDeadlineMinutesUTC());
            }
            if (doc.HasVersion()) {
                entry.ForceAdd("doc_version", doc.GetVersion());
            }
        }

        for (const auto& loggingProperty: document->GetMessage().GetLoggingProperties()) {
            entry.ForceAdd(loggingProperty.GetName(), loggingProperty.GetValue());
        }
    }

    const TString& logRecord = entry.ToString();
    DEBUG_LOG << logRecord << Endl;
    if (logging) {
        IPR_LOG << logRecord << Endl;
    }

    TServiceMetricPtr metric = Metrics.Get(serviceName);
    CHECK_WITH_LOG(!!metric);
    if (replyCode == 200) {
        metric->OnSuccess(TInstant::Now() - Context->GetReceiveTime(), TInstant::Seconds(GetDocTimestamp()));
    } else if (IsServerError(replyCode)) {
        metric->OnFailure();
    } else if (IsUserError(replyCode)) {
        metric->OnUserFailure();
    } else if (replyCode == HTTP_NOT_MODIFIED) {
        metric->OnDeprecatedFailure();
    } else {
        metric->OnUnknownFailure();
    }

    TVector<TString> distrAttrs;
    if (!!document) {
        const NRTYServer::TMessage& msg = document->GetMessage();
        distrAttrs.insert(distrAttrs.end(), msg.GetDistributorAttributes().begin(), msg.GetDistributorAttributes().end());
    }

    TSaasIndexerProxySignals::TUnistatRecordContext assCtx (
        serviceName,
        replyCode,
        duration,
        Seconds() - GetDocTimestamp(),
        fromMeta,
        Context->GetRequestOptions().IsDeferredMessage(),
        Context->GetRequestOptions().GetSource(),
        message.find("DUPLICATED") != TString::npos,
        std::move(distrAttrs)
        );
    CHECK_WITH_LOG(TSaasIndexerProxySignals::DoUnistatRecord(assCtx));
}

void TCommonSenderTask::SendReply() {
    TGuard<TMutex> g(Mutex);
    DEBUG_LOG << "action=client_destructor_start;init=" << Initialized << ";client_id=" << ClientId << Endl;
    if (Initialized) {
        WasReplied = true;
        try {
            const auto* meta = Executor.GetConfig().GetSearchMap().GetMetaService(Context->GetServiceName());
            ui32 dispatchCode = ActionReply.GetHttpCode();
            ui32 smartDispatchCode = 0;
            if (Authorized) {

                if (!!Context->GetDocument()) {
                    DEBUG_LOG << "action=reply;status=bydoc;id=" << GetMessageId() << Endl;
                    for (auto&& i : ServiceContexts) {
                        i->CheckReplies();
                        const TDispStatusBehaviour& dsb = GetBehaviour(i->GetDispStatus());
                        if (!meta || meta->IndexingTarget != NSearchMapParser::Backends) {
                            dispatchCode = Max(dispatchCode, dsb.HttpCode);
                            if (!dsb.IgnoreInMerge)
                                smartDispatchCode = Max(smartDispatchCode, dsb.HttpCode);
                        } else {
                            const auto* componentInfo = meta->GetComponent(i->GetServiceName());
                            CHECK_WITH_LOG(componentInfo);
                            if (!componentInfo->IndexReplyIgnoring) {
                                dispatchCode = Max(dispatchCode, dsb.HttpCode);
                                if (!dsb.IgnoreInMerge)
                                    smartDispatchCode = Max(smartDispatchCode, dsb.HttpCode);
                            }
                        }
                    }
                }
            }
            if (smartDispatchCode) {
                dispatchCode = smartDispatchCode;
            }

            const TString& message = GetReplyMessage(dispatchCode);

            try {
                const TString& clientMessage = (Context->GetRequestOptions().GetVerboseLogging() || dispatchCode != 200) ? message : Default<TString>();
                Context->Reply(dispatchCode, clientMessage, dispatchCode == 200 ? "OK" : " ");
            } catch (...) {
                FATAL_LOG << "Exception while sending reply " << ClientId << ": " << CurrentExceptionMessage() << Endl;
                Context->Reply(500, message, "Internal server error");
            }

            const auto& proxyConfig = Executor.GetConfig().GetServicesConfig();

            DoLogAndMetric(Context->GetServiceName(), dispatchCode, false, message, proxyConfig.GetConfig(Context->GetServiceName()).LoggingEnabled);

            if (meta && meta->IndexingTarget == NSearchMapParser::Backends) {
                for (auto&& i : ServiceContexts) {
                    const auto& dsb = GetBehaviour(i->GetDispStatus());
                    DoLogAndMetric(i->GetServiceName(), dsb.HttpCode, true, message, proxyConfig.GetConfig(i->GetServiceName()).LoggingEnabled);
                }
            }
            DEBUG_LOG << "action=send_reply_finished;client_id=" << ClientId << Endl;

            if (DumpOriginalMessage && Executor.HasDumper() && !Context->GetRequestOptions().QueueDisabled()) {
                TString json(Context->GetPost().AsCharPtr(), Context->GetPost().Size());
                ReplaceAnyOf(json, "\n\t\r", ' ');

                NUtil::TTSKVRecord record("saas-log-json-dump");
                record.Add("unixtime", Seconds());
                record.Add("service", Context->GetServiceName());
                record.Add("message", json);

                DEBUG_LOG << "action=start_dump;client_id=" << ClientId << Endl;
                Executor.GetDumper() << record.ToString() << Endl;
                DEBUG_LOG << "action=finish_dump;client_id=" << ClientId << Endl;
            }
        } catch (...) {
            Context->Reply(500, "Exception while send reply : " + CurrentExceptionMessage(), "Internal server error");
            ERROR_LOG << "While send reply: " << CurrentExceptionMessage() << Endl;
        }
    }
    DEBUG_LOG << "action=client_destructor_finished;init=" << Initialized << ";client_id=" << ClientId << Endl;
}

bool TCommonSenderTask::PrepareDataFromContext(TAdaptersManager& processor) {
    DEBUG_LOG << "action=parse_message;status=start;client_id=" << ClientId << Endl;
    DumpOriginalMessage = processor.ShouldDump(Context->GetServiceName());
    ui32 parseResult = DoFillDocuments(processor);
    if (!!GetContext().GetDocument()) {
        GetContext().GetDocument()->PatchMessageByContext(Executor.GetConfig().GetServicesConfig().GetConfig(Context->GetServiceName()));
    }
    Authorized = processor.Authorize(*this);
    DEBUG_LOG << "action=parse_message;status=finished;docs_exists=" << !!Context->GetDocument() << ";result=" << parseResult << ";authorized=" << Authorized << ";service=" << Context->GetServiceName() << ";client_id=" << ClientId << Endl;
    return !!Context->GetDocument() && Authorized;
}

void TCommonSenderTask::BuildContexts(NRTYServer::TMessage& message) {
    CHECK_WITH_LOG(!ServiceContexts.size());
    TVector<TString> services;
    auto* metaService = Executor.GetConfig().GetSearchMap().GetMetaService(Context->GetServiceName());
    bool isMeta = false;
    if (metaService && metaService->IndexingTarget == NSearchMapParser::Backends) {
        isMeta = true;
        for (auto&& comp : metaService->Components) {
            if (!comp.IndexDisabled)
                services.push_back(comp.ServiceName);
        }
    } else {
        services.push_back(Context->GetServiceName());
    }

    for (auto&& i : services) {
        if (isMeta) {
            Metrics.Get(i)->OnIncoming(TInstant::Seconds(GetDocTimestamp()));
        }
        const NSearchMapParser::TServiceSpecificOptions* service = dynamic_cast<const NSearchMapParser::TServiceSpecificOptions*>(Executor.GetConfig().GetServiceInfo(i));
        VERIFY_WITH_LOG(service, "Incorrect service name : %s", i.data());
        NSearchMapParser::DispatchTarget target = service->IndexingTarget;
        if (target == NSearchMapParser::Distributor) {
            if (GetContext().GetRequestOptions().GetBypassDistributor()) {
                target = NSearchMapParser::Backends;
            }
        }
        switch (target) {
        case NSearchMapParser::PersQueue:
            ServiceContexts.push_back(new TPersQueueReplier(i, *this, message, &Executor.GetConfig().GetDispConfig(), Executor.GetDispatcher().GetStorage()));
            break;
        case NSearchMapParser::Backends:
            ServiceContexts.push_back(new TBackendsReplier(i, *this, message, &Executor.GetConfig().GetDispConfig(), Executor.GetDispatcher().GetStorage()));
            break;
        case NSearchMapParser::Distributor:
            ServiceContexts.push_back(new TDistributorReplier(i, *this, message, &Executor.GetConfig().GetDispConfig(), Executor.GetDispatcher().GetStorage()));
            break;
        case NSearchMapParser::Void:
            ServiceContexts.push_back(new TVoidReplier(i, *this, message, &Executor.GetConfig().GetDispConfig(), Executor.GetDispatcher().GetStorage()));
            break;
        default:
            FAIL_LOG("Incorrect indexing target: %d", (int)service->IndexingTarget);
        }
    }
}

void TCommonSenderTask::Process(void* ThreadSpecificResource) {
    TGuard<TMutex> g(Mutex);
    Initialized = true;
    Metrics.Get(Context->GetServiceName())->OnIncoming(TInstant::Seconds(GetDocTimestamp()));
    DEBUG_LOG << "action=check_expired;client_id=" << ClientId << Endl;

    if (!GetContext().GetRequestOptions().IsTrace() && GetContext().GetRequestOptions().IsInstantReply()) {
        TServiceMetricPtr metric = Executor.GetMetrics().Get(GetContext().GetServiceName());
        CHECK_WITH_LOG(!!metric);
        ui32 limitOnFlight = Executor.GetConfig().GetServicesConfig().GetConfig(GetContext().GetServiceName()).MaxInFlightForInstant;
        if (metric->GetInFlightCount() > limitOnFlight) {
            DEBUG_LOG << "action=check_max_in_flight;status=fail;client_id=" << ClientId << Endl;
            ActionReply.SetInfo(NRTYServer::TReply::dsUSER_ERROR, "max_in_flight_limit");
            Destroyer.Register(this);
            return;
        }
    }

    if (IsExpired()) {
        DEBUG_LOG << "action=check_expired;status=fail;client_id=" << ClientId << Endl;
        ActionReply.SetInfo(NRTYServer::TReply::dsINTERNAL_TIMEOUT, "internal timeout");
        Destroyer.Register(this);
        return;
    }

    if (!Executor.GetConfig().GetServiceInfo(GetContext().GetServiceName())) {
        DEBUG_LOG << "action=on_unknown_service;status=fail;client_id=" << ClientId << Endl;
        ActionReply.SetInfo(NRTYServer::TReply::dsUSER_ERROR, "unknown service");
        Destroyer.Register(this);
        return;
    }

    if (Executor.GetConfig().GetServicesConfig().GetConfig(GetContext().GetServiceName()).IndexationDisabled) {
        DEBUG_LOG << "action=on_disabled_service;status=fail;client_id=" << ClientId << Endl;
        ActionReply.SetInfo(NRTYServer::TReply::dsUSER_ERROR, "indexation disabled for service");
        Destroyer.Register(this);
        return;
    }

    TAdaptersManager* processor = (TAdaptersManager*)(ThreadSpecificResource);
    if (!PrepareDataFromContext(*processor)) {
        Destroyer.Register(this);
        return;
    }

    if (!processor->IsAcceptableByFilter(*this)) {
        DEBUG_LOG << "action=check_filter;status=fail;client_id=" << ClientId << Endl;
        ActionReply.SetInfo(NRTYServer::TReply::dsIGNORED_BY_RANK, "ignored by rank");
        Destroyer.Register(this);
        return;
    }

    DEBUG_LOG << "action=build_dispatch_tasks;status=start;client_id=" << ToString(ClientId) << Endl;

    NRTYServer::TMessage& message = *Context->GetDocument()->MutableMessage();

    BuildContexts(message);

    if (!ServiceContexts.size()) {
        Destroyer.Register(this);
        return;
    }

    DEBUG_LOG << "doc=" << message.GetDocument().GetUrl() << ";kps=" << message.GetDocument().GetKeyPrefix()
        << ";client_id=" << ClientId << ";id=" << message.GetMessageId() << Endl;

    bool verificationFlag = true;
    for (auto&& i : ServiceContexts) {
        if (!i->Verify(Executor.GetConfig())) {
            verificationFlag = false;
        }
    }

    if (verificationFlag) {
        for (auto&& i : ServiceContexts) {
            i->Send(Executor.GetConfig(), Executor.GetDispatcher().GetSelector());
        }
    } else {
        for (auto&& i : ServiceContexts) {
            i->SetBackendCounter(0);
            if (i->Verify(Executor.GetConfig())) {
                i->ForceMessageStatus(NRTYServer::TReply::dsUSER_ERROR, "discarded incorrect service context for other service");
            }
            i->FinishDocument();
        }
    }
    DEBUG_LOG << "action=build_dispatch_tasks;status=finished;client_id=" << ClientId << ";init=" << Initialized << Endl;
}

namespace {
    static TAtomic InitClientId() {
        // some seed (no uuid in Arcadia)
        TString strSeed;
        strSeed = GetEnv("BSCONFIG_INAME");
        if (!strSeed)
            strSeed = GetEnv("PWD");

        ui64 hostHash = FnvHash<ui32>(TString(strSeed)) % 10000;
        time_t time = (Seconds() / 600) % 10000; // value generated from 10-minutes interval (unique within 2 months)
        ui64 seed = (hostHash * 10000 + time) * 10000000;
        return seed;
    }

    static TAtomic GlobalClientId = InitClientId();
}

TString TCommonSenderTask::GetReplyMessage(ui32 httpStatus) const {
    TStringStream ss;
    NJson::TJsonValue dispatching;
    NJson::TJsonValue reply;

    if (Authorized) {
        if (!!Context->GetDocument()) {
            for (auto&& i : ServiceContexts) {
                dispatching.InsertValue(i->GetServiceName(), i->GetReply());
            }
            reply.InsertValue("dispatch", dispatching);
        }
        NRTYServer::TRequestTracer rt;
        if (!!Context->GetDocument()) {
            Context->GetDocument()->AddInTrace(GetTrace(), rt, ServiceContexts);
        }
        if (rt.DocumentsSize())
            reply.InsertValue("async_code", Base64Encode(rt.SerializeAsString()));
    }

    reply.InsertValue("proxy_info", ActionReply.GetReply());
    reply.InsertValue("code", httpStatus);
    NJson::WriteJson(&ss, &reply);
    return ss.Str();
}

ui64 TCommonSenderTask::GetDocKeyPrefix() const {
    if (!Context->GetDocument())
        return 0;
    return Context->GetDocument()->GetMessage().GetDocument().GetKeyPrefix();
}

ui64 TCommonSenderTask::GetDocTimestamp() const {
    if (!Context->GetDocument())
        return 0;
    return Context->GetDocument()->GetMessage().GetDocument().GetModificationTimestamp();
}

TString TCommonSenderTask::GetDocUrl() const {
    if (!Context->GetDocument())
        return "";
    return Context->GetDocument()->GetMessage().GetDocument().GetUrl();
}

ui64 TCommonSenderTask::GetMessageId() const {
    //returns 0 if no document or !Action.HasMessageId(), as in ISrvDispContext::GetMessageId()
    if (!Context->GetDocument())
        return 0;

    return Context->GetDocument()->GetMessage().GetMessageId();
}

TCommonSenderTask::TCommonSenderTask(TTaskExecutor& executor, TFinishExecutor& finishExecutor, IAdapterContext* context, TStaticServiceMetrics& metrics, ISenderTask::TDestroyer& destroyer)
    : Executor(executor)
    , FinishExecutor(finishExecutor)
    , Destroyer(destroyer)
    , RepliesReady(0)
    , ActionReply(NRTYServer::TReply::dsOK)
    , Metrics(metrics)
    , Authorized(true)
    , Initialized(false)
    , DumpOriginalMessage(false)
    , ClientId(AtomicIncrement(::GlobalClientId))
{
    WasReplied = false;
    CHECK_WITH_LOG(!!context);
    DEBUG_LOG << "action=client_added_in_queue;client_id=" << ClientId << Endl;
    Context.Reset(context);
}
