#include "indexer_server.h"

#include <saas/rtyserver/indexer_core/messages.h>
#include <saas/rtyserver/common/common_messages.h>
#include <saas/rtyserver/common/message_collect_server_info.h>
#include <saas/rtyserver/components/ddk/ddk_config.h>
#include <saas/rtyserver/components/ddk/ddk_globals.h>
#include <saas/rtyserver/config/common_indexers_config.h>
#include <saas/rtyserver/config/indexer_config.h>
#include <saas/rtyserver/logging/rty_index.h>
#include <saas/library/indexer_protocol/protocol.h>
#include <saas/library/daemon_base/threads/rty_pool.h>
#include <saas/util/logging/exception_process.h>
#include <saas/util/json/json.h>

#include <library/cpp/balloc/optional/operators.h>
#include <library/cpp/neh/factory.h>
#include <library/cpp/neh/location.h>
#include <library/cpp/neh/multi.h>

namespace {
    TMaybe<TString> GetMessageUrlMaybe(const NRTYServer::TMessage& message) {
        if (message.HasDocument()) {
            return message.GetDocument().GetUrl();
        }
        return Nothing();
    }
}

class TRTYNehReplier : public IReplier {
    NNeh::IRequestRef Request;
    bool NeedReply;
    TServiceMetric& RepliesMetric;
    TMaybe<TString> DocumentUrlMaybe;
    const TString& ServiceName;

public:
    TRTYNehReplier(ITransaction& transaction, const NRTYServer::TMessage& message, NNeh::IRequestRef request, TServiceMetric& metric,
        const TInstant& constructionTime, const TString& serviceName
    )
        : IReplier(transaction, message.HasMessageId() ? message.GetMessageId() : 0, constructionTime)
        , Request(request.Release())
        , NeedReply(message.HasMessageId())
        , RepliesMetric(metric)
        , DocumentUrlMaybe(GetMessageUrlMaybe(message))
        , ServiceName(serviceName)
    {
        if (NeedReply) {
            RepliesMetric.OnIncoming();
        }
    }

    bool DoReply() override {
        VERIFY_WITH_LOG(NeedReply, "Incorrect DoReply method usage");
        NRTYServer::TReply reply = BuildRTYReply();
        NNeh::TDataSaver ds;
        reply.SerializeToArcadiaStream(&ds);

        const TDuration duration = Now() - Created;
        DEBUG_LOG << "action=reply;time=" << duration.MilliSeconds() << ";url=" << reply.GetMessageId() << Endl;

        if (Request->Canceled()) {
            RepliesMetric.OnFailure();
            return false;
        }

        Request->SendReply(ds);
        RepliesMetric.OnSuccess(duration);
        return true;
    }

    void LogReply(const ui64 messageId, const TString& status, const ui32 httpCode, const ui64 duration, const TString& errorMessage) override {
        QueueReplyLogWithUnistat(ServiceName, DocumentUrlMaybe, messageId, status, httpCode, duration, errorMessage);
    }

};

TIndexationStopGuard::TIndexationStopGuard() {
    SendGlobalMessage<TMessageIndexingControl>(TMessageIndexingControl::Disable);
}

TIndexationStopGuard::~TIndexationStopGuard() {
    SendGlobalMessage<TMessageIndexingControl>(TMessageIndexingControl::Enable);
}

TIndexerServer::TIndexerServer(TIndexStorage& indexStorage, TDocumentModifier& /*modifier*/,
                               const NRTYServer::TIndexerConfig& config, const TString& serviceName)
    : Config(config)
    , ServiceName(serviceName)
    , DocumentsQueue(*this)
    , IndexStorage(indexStorage)
    , IndexerManager(nullptr)
    , Requests(Singleton<TRTYPools>()->Get(TRTYPools::TLoadtype::Update), "IdxSrvReqs")
    , RepliesMetric("NehIndexingRequests", GetMetricsPrefix())
    , Status(tsStopped)
    , IndexingPaused(Config.Common.PauseOnStart)
{
    ClientId = 0;
    DbgDocumentCounter = 0;
    RegisterGlobalMessageProcessor(this);
}

TIndexerServer::~TIndexerServer() {
    UnregisterGlobalMessageProcessor(this);
}

class TRequestParser: public IObjectInQueue {
private:
    NNeh::IRequestRef Request;
    TIndexerServer& Server;
    const NRTYServer::TIndexerConfig& Config;
    TInstant ConstructionTime;
private:
    void Abort(NRTYServer::TMessage& message, NRTYServer::TReply::TRTYStatus status, const TString& error) {
        THolder<TRTYNehReplier> replier;
        replier.Reset(new TRTYNehReplier(TIndexerServer::RequestsTransaction, message, Request.Release(), Server.GetRepliesMetric(), ConstructionTime,
            Server.GetServiceName()));
        replier->SetStatus(status, error);
        replier->Reply();
    }
public:
    TRequestParser(NNeh::IRequestRef& req, TIndexerServer& server, const NRTYServer::TIndexerConfig& config)
        : Request(req)
        , Server(server)
        , Config(config)
        , ConstructionTime(Now())
    {}

    void Process(void* /*ThreadSpecificResource*/) override {
        ThreadDisableBalloc();
        THolder<TRequestParser> suicide(this);
        TGuardIncompatibleAction gia(TIndexerServer::RequestsTransaction);
        DEBUG_LOG << "action=new_connection;host=" << Request->RemoteHost() << ";id=" << Request->RequestId() << Endl;
        NRTYServer::TMessage message;
        if (!message.ParseFromArray(Request->Data().data(), Request->Data().size())) {
            DEBUG_LOG << "action=new_message;status=failed;host=" << Request->RemoteHost() << ";id_request=" << Request->RequestId() << Endl;
            message.SetMessageId((ui64)-1);
            Abort(message, NRTYServer::TReply::INCORRECT_DOCUMENT, "Can't parse message");
            return;
        }

        if (message.GetDocument().GetKeyPrefix() == -1) {
            DEBUG_LOG << "action=new_message;status=failed;host=" << Request->RemoteHost() << ";id_request=" << Request->RequestId() << ";error=keyprefix==-1" << Endl;
            Abort(message, NRTYServer::TReply::INCORRECT_DOCUMENT, "Incorrect kps == -1");
            return;
        }

        DEBUG_LOG << "action=new_message;status=ok;id=" << message.GetMessageId() << Endl;
        if (!Server.GetIsRunning()) {
            Abort(message, NRTYServer::TReply::NOTNOW, "Server stopping....");
            return;
        }
        if (Server.IsIndexingPaused()) {
            Abort(message, NRTYServer::TReply::NOTNOW, "Indexation paused");
            return;
        }

        QueueMessageReqLog(Server.GetServiceName(), Request, message, Config.GetTypeWithPort(), "IN PROCESS");
        auto replier = CreateNehReplier(message);
        auto serviceLogic = Config.Common.Owner.ExternalServiceLogic;
        if (!serviceLogic) { // Usual RTYServer pipeline
            QueueDocument(message, replier);
        } else {
            bool doIndexMessage = false;
            try {
                replier->SetStatus(NRTYServer::TReply::OK, "");
                doIndexMessage = !serviceLogic->AddMessage(message, replier.Get());
            } catch (...) {
                doIndexMessage = false;
                replier->SetStatus(NRTYServer::TReply::INTERNAL_ERROR, CurrentExceptionMessage());
                ERROR_LOG << "An exception occurred during ExternalServiceLogic: " << CurrentExceptionMessage() << Endl;
            }
            if (doIndexMessage) {
                QueueDocument(message, replier);
            } else {
                replier->Reply();
            }
        }
    }
private:
    void QueueDocument(const NRTYServer::TMessage& message, IReplier::TPtr replier) {
        TQueuedDocument queuedDocument = MakeAtomicShared<TGuardedDocument>(message, replier, Config.Common.Owner);
        if (queuedDocument->IsCorrect()) {
            Server.AddDocument(queuedDocument, message);
        }
    }

    IReplier::TPtr CreateNehReplier(const NRTYServer::TMessage& message) {
        return MakeAtomicShared<TRTYNehReplier>(TIndexerServer::RequestsTransaction, message, Request.Release(), Server.GetRepliesMetric(),
            ConstructionTime, Server.GetServiceName());
    }
    IReplier::TPtr CreateFakeReplier(const ui64 messageId) {
        return MakeAtomicShared<TFakeReplier>(TIndexerServer::RequestsTransaction, messageId);
    }
};

void TIndexerServer::OnRequest(NNeh::IRequestRef req) {
    Requests.SafeAdd(new TRequestParser(req, *this, Config));
}

void TIndexerServer::AddDocument(TQueuedDocument qDoc, const NRTYServer::TMessage& message, bool forcePut) {
    auto messageType = message.GetMessageType();
    NRTYServer::TReply::TRTYStatus errStatus = NRTYServer::TReply::INCORRECT_DOCUMENT;
    if (messageType == NRTYServer::TMessage::DEPRECATED__UPDATE_DOCUMENT) {
        errStatus = NRTYServer::TReply::INCORRECT_UPDATE;
    }
    TRY
        DEBUG_LOG << "action=add_in_queue;type=" << NRTYServer::TMessage::TMessageType_Name(messageType) << ";id=" << message.GetMessageId() << ";url=" << message.GetDocument().GetUrl() << ";kp=" << message.GetDocument().GetKeyPrefix() << Endl;

        // Routines for simulations & tests
        if (Y_UNLIKELY(message.GetIndexSleepMs()))
            Sleep(TDuration::MilliSeconds(message.GetIndexSleepMs()));
        if (Y_UNLIKELY(Config.DbgMaxDocumentsTotal && messageType != NRTYServer::TMessage::REOPEN_INDEXERS)) {
            auto nDocs = AtomicGetAndIncrement(DbgDocumentCounter);
            if (nDocs >= Config.DbgMaxDocumentsTotal) {
                const TString msg("Requested number of documents for the test has been fetched");
                qDoc->SetStatus(NRTYServer::TReply::DEPRECATED, msg);
                if (nDocs == Config.DbgMaxDocumentsTotal) {
                    SendGlobalDebugMessage<TUniversalAsyncMessage>("DbgMaxDocumentsTotal"); //testability hook
                    NOTICE_LOG << msg << Endl;
                }
                return;
            }
        }

        // Handle it
        switch (messageType) {
        case NRTYServer::TMessage::MODIFY_DOCUMENT:
        case NRTYServer::TMessage::ADD_DOCUMENT:
            if (IndexStorage.ExceededNonMergedIndexesCount()) {
                qDoc->SetStatus(NRTYServer::TReply::NOTNOW, "Maximum number of segments exceeded");
                return;
            }
            [[fallthrough]]; // AUTOGENERATED_FALLTHROUGH_FIXME
        case NRTYServer::TMessage::DELETE_DOCUMENT:
        case NRTYServer::TMessage::DEPRECATED__UPDATE_DOCUMENT:
            if (message.HasDocument()) {
                DocumentsQueue.LockAndPut(qDoc, forcePut);
            } else {
                const TString errMess("Incorrect message with no document. Rejected.");
                qDoc->SetStatus(errStatus, errMess);
                WARNING_LOG << errMess << Endl;
            }
            DEBUG_LOG << "action=add_in_queue_done;type=" << NRTYServer::TMessage::TMessageType_Name(messageType) << ";id=" << message.GetMessageId() << ";url=" << message.GetDocument().GetUrl() << ";kp=" << message.GetDocument().GetKeyPrefix() << ";hash=" << qDoc->GetDocumentHash() << Endl;
            return;
        case NRTYServer::TMessage::REOPEN_INDEXERS: {
            TDeferredRepliesProcessor::TDeferredReplyCheck res = DocumentsQueue.CheckDeferredReply(qDoc.Get(), ServiceName);
            if (res == TDeferredRepliesProcessor::drcStartNew) {
                qDoc->UnLock();
                NOTICE_LOG << "Reopening indexers start" << Endl;
                SendGlobalMessage<TMessageReopenIndexes>(qDoc, "");
            } else {
                NOTICE_LOG << "Reopening indexers waiting..." << Endl;
            }
            return;
        }
        case NRTYServer::TMessage::SWITCH_PREFIX: {
            const i64 prefix = qDoc->GetDocument().GetDocSearchInfo().GetKeyPrefix();
            NOTICE_LOG << "Switch default kps to " << prefix << Endl;

            if (!Config.Common.Owner.IsPrefixedIndex) {
                qDoc->SetStatus(NRTYServer::TReply::INCORRECT_DOCUMENT, "Cannot switch kps on not prefixed index");
                break;
            }
            if (!message.HasDocument() || !message.GetDocument().HasKeyPrefix()){
                const TString errMess("Incorrect switch_prefix message: no document or no keyprefix in document. Rejected.");
                qDoc->SetStatus(NRTYServer::TReply::INCORRECT_DOCUMENT, errMess);
                WARNING_LOG << errMess << Endl;
                break;
            }
            SendGlobalMessage<TMessageSwitchKps>(prefix);
            qDoc->SetStatus(NRTYServer::TReply::OK, "");
            return;
        }
        default:
            qDoc->SetStatus(NRTYServer::TReply::INCORRECT_DOCUMENT, "Incorrect message type");
            return;
        }
    CATCH("on AddMessageToQueue")
    qDoc->SetStatus(errStatus, "Error on AddMessageToQueue");
}

void TIndexerServer::RegisterManager(IIndexerManager* manager) {
    IndexerManager.Reset(manager);
}

bool TIndexerServer::GetIsRunning() const {
    return Status == tsActive;
}

bool TIndexerServer::WaitReady() const {
    while (Status != tsActive && Status != tsFailOnStart) {
        Sleep(TDuration::MilliSeconds(200));
    }
    return Status == tsActive;
}

TSynchronizedDocumentsQueue& TIndexerServer::GetDocumentsQueue() {
    return DocumentsQueue;
}

bool TIndexerServer::StartIndexerServer() {
    TGuardTransaction gt(TransactionServer);
    VERIFY_WITH_LOG(Status == tsStopped, "Try to start indexer server twice");
    Status = tsStarting;
    Requests.Start(Config.Common.ServerOptions.nThreads);
    TRY
        IndexerManager->OpenIndexers();
        INFO_LOG << "Requester construction..." << Endl;
        Addrs.clear();
        Addrs.push_back(NRTYServer::IndexingNehScheme + "://localhost:" + ToString(Config.Common.ServerOptions.Port) + "/");
        Addrs.push_back(NRTYServer::IndexingInternalScheme + "://localhost:" + ToString(Config.Common.ServerOptions.Port) + "/");
        Requester = NNeh::MultiRequester(Addrs, this);
        INFO_LOG << "Requester construction... OK" << Endl;
        ThreadManager = SystemThreadFactory()->Run(IndexerManager.Get());
        Status = tsActive;
        return true;
    CATCH("Cannot start indexers server");
    Status = tsFailOnStart;
    return false;
}

bool TIndexerServer::Process(IMessage* message) {
    TMessageCollectServerInfo* messInfo = dynamic_cast<TMessageCollectServerInfo*>(message);
    if (messInfo) {
        messInfo->SetIndexerRequestsCount(Config.GetType() + "-" + ToString(Config.Common.ServerOptions.Port), Requests.Size());
        messInfo->SetIndexingEnabled(!IndexingPaused);
        return true;
    }
    TMessageIndexingControl* mic = dynamic_cast<TMessageIndexingControl*>(message);
    if (mic) {
        TMessageIndexingControl::EControlAction action = mic->GetAction();
        if (action == TMessageIndexingControl::GetStatus) {
            mic->SetRunning(!IndexingPaused);
            return true;
        }

        TGuard<TMutex> guard(PauseMutex);
        if (!IndexingPaused && action == TMessageIndexingControl::Disable)
            IndexingPaused = true;
        else if (IndexingPaused && action == TMessageIndexingControl::Enable)
            IndexingPaused = false;
        mic->SetRunning(!IndexingPaused);
        return true;
    }
    return false;
}

void TIndexerServer::Wait(bool rigidStop) {
    DEBUG_LOG << "Start to wait indexer server stop" << Endl;
    if (!rigidStop)
        TGuardTransaction gt(TIndexerServer::RequestsTransaction);
    Requester.Reset(nullptr);
    Requests.Stop();
    DEBUG_LOG << "indexer server stopped" << Endl;
}

void TIndexerServer::StopIndexerServer(bool rigidStop) {
    TGuardTransaction gt(TransactionServer);
    try {
        VERIFY_WITH_LOG(Status == tsActive, "Try to stop stopped indexer server");
        NOTICE_LOG << "Closing indexer server on port " << Config.Common.ServerOptions.Port << Endl;
        Status = tsStopping;
        DocumentsQueue.Stop();
        IndexerManager->CloseIndexers(rigidStop);
        IndexerManager->StopActions();
        ThreadManager->Join();
        Status = tsStopped;
        NOTICE_LOG << "Closing indexer server on port " << Config.Common.ServerOptions.Port << " finished" << Endl;
        return;
    } catch (...) {
        ERROR_LOG << "Could not close indexer server " << CurrentExceptionMessage() << Endl;
    }
    Status = tsFailOnStop;
    ERROR_LOG << "Closing indexer server on port " << Config.Common.ServerOptions.Port << " failed" << Endl;
}

ITransaction TIndexerServer::RequestsTransaction;
TMutex TIndexerServer::RequestMutex;
