#include "deferred_messages_queue.h"

#include <saas/library/daemon_base/daemon/messages.h>
#include <saas/indexerproxy/adapters/proto_adapter/proto_adapter.h>
#include <saas/indexerproxy/context/inproc.h>
#include <saas/indexerproxy/server/sender_executor.h>
#include <saas/util/logging/number_format.h>

void TDeferredMessagesQueue::AddQueue(const TString& queueName, const TString& serviceName, const NSearchMapParser::TShardsInterval& interval, TServiceData::TPtr serviceData) {
    TQueueHeader header;
    header.Service = serviceName;
    header.Shards = interval;

    IQueue::TPtr q = IQueue::TFactory::Construct(Config.GetDeferredMQConfig().Type, Config, queueName, header);

    INFO_LOG << "action=queue_started;name=" << queueName << ";size=" << q->Size() << Endl;

    CHECK_WITH_LOG(!!q);
    serviceData->AddQueue(queueName, q);
    {
        TBackendData& backendData = Queues[queueName];
        backendData.Queue = q;
        backendData.Service = serviceData;
    }
}

TDeferredMessagesQueue::TDeferredMessagesQueue(const TDispatcherConfig& config)
    : Config(config)
    , ThreadPool("DeferredMsgPool")
    , Active(false)
    , StoreQueue(config.GetOwner().GetUsesSmartQueues(), "DeferredMsgStor")
    , MetricQueueSize("RuntimeAsyncQueueSize")
{
    try {
        for (const auto& service : Config.GetServiceMap()) {
            TServiceData::TPtr serviceData(MakeIntrusive<TServiceData>(service.first, Config.GetOwner().GetServicesConfig().GetConfig(service.first)));
            Services[service.first] = serviceData;
            for (const auto& host : service.second.GetAllHosts()) {
                AddQueue(host.Name + ':' + ToString(host.IndexerPort), service.first, host.Shards, serviceData);
            }
            AddQueue(service.first, service.first, NSearchMapParser::FullInterval, serviceData);
        }
        for (const auto& service : Config.GetOwner().GetSearchMap().GetMetaServices()) {
            TServiceData::TPtr serviceData(MakeIntrusive<TServiceData>(service.Name, Config.GetOwner().GetServicesConfig().GetConfig(service.Name)));
            VERIFY_WITH_LOG(!Services.contains(service.Name), "%s", service.Name.data());
            Services[service.Name] = serviceData;
            AddQueue(service.Name, service.Name, NSearchMapParser::FullInterval, serviceData);
        }
    } catch (...) {
        const TString& message = CurrentExceptionMessage();
        AbortFromCorruptedIndex("cannot create deferred messages queue: %s", message.data());
    }
}

TDeferredMessagesQueue::~TDeferredMessagesQueue() {
    CHECK_WITH_LOG(!Active);
}

ui64 TDeferredMessagesQueue::GetSizeInBytes() const {
    ui64 result = 0;
    for (const auto& q : Queues)
        result += q.second.Queue->SizeInBytes();
    return result;
}

ui64 TDeferredMessagesQueue::TServiceData::GetSizeInBytes() const {
    ui64 size = 0;
    for (const auto& q : Queues)
        size += q.second->SizeInBytes();
    return size;
}

void TDeferredMessagesQueue::TServiceData::CheckSizeInBytesLimit() const {
    if (GetSizeInBytes() >= Config.SpaceLimitInMBytes * 1024 * 1024)
        ythrow yexception() << "SpaceLimit expired for service " << Name;
}

namespace {
    class TQueueStore: public IObjectInQueue {
    private:
        TDeferredMessagesQueue::IQueue::TPtr Queue;
        NRTYServer::TMessage Message;
        TOrangeMetric& MetricQueueSize;
        TString FailMessage;
    public:
        TQueueStore(TDeferredMessagesQueue::IQueue::TPtr queue, const NRTYServer::TMessage& message, TOrangeMetric& metricQueueSize)
            : MetricQueueSize(metricQueueSize)
        {
            Queue = queue;
            Message = message;
            MetricQueueSize.Inc();
        }

        ~TQueueStore() {
            MetricQueueSize.Dec();
            DEBUG_LOG << "Stored Service/Url = " << Queue->GetHeader().Service << "/" << Message.GetDocument().GetUrl() << ". Store status = " << (!!FailMessage ? FailMessage : "OK")<< Endl;
        }

        void Process(void* /*ThreadSpecificResource*/) override {
            try {
                Queue->Store(Message);
            } catch (...) {
                FailMessage = CurrentExceptionMessage();
                DEBUG_LOG << "For message url = " << Message.GetDocument().GetUrl() << "/" << Queue->GetHeader().Service << ". Exception on store = " << FailMessage << Endl;
            }
        }

    };
}

void TDeferredMessagesQueue::Store(const TString& clientForSaveName, const NRTYServer::TMessage& message, bool asyncFlag) {
    if (!Active)
        ythrow yexception() << "Queue is disabled";
    if (GetSizeInBytes() >= Config.GetDeferredMQConfig().SpaceLimitInBytes)
        ythrow yexception() << "global SpaceLimit expired";
    TQueues::iterator q = Queues.find(clientForSaveName);
    VERIFY_WITH_LOG(q != Queues.end(), "Incorrect client name: %s", clientForSaveName.data());

    q->second.Service->CheckSizeInBytesLimit();

    TString checkReply;
    if (!q->second.Queue->CheckMessageForStore(message, checkReply)) {
        ythrow yexception() << checkReply;
    }

    if (!asyncFlag) {
        q->second.Queue->Store(message);
    } else {
        CHECK_WITH_LOG(StoreQueue.AddAndOwn(THolder(new TQueueStore(q->second.Queue, message, MetricQueueSize))));
    }
}

void TDeferredMessagesQueue::PurgeQueue(const TString& name) {
    TQueues::iterator q = Queues.find(name);
    if (q != Queues.end()) {
        q->second.Queue->Purge();
    }
}

void TDeferredMessagesQueue::PurgeService(const TString& service) {
    TSerivces::iterator s = Services.find(service);
    if (s == Services.end())
        ythrow yexception() << "unknown service " << service;
    s->second->PurgeQueues();
}

void TDeferredMessagesQueue::GetQueuesInfo(NJson::TJsonValue& result) const {
    ui32 sizeInDocs = 0;
    ui64 sizeInBytes = 0;
    ui64 allInFlight = 0;
    for (const auto& service : Services) {
        for (const auto& q: service.second->GetQueues()) {
            ui32 size = q.second->Size();
            ui64 space = q.second->SizeInBytes();
            ui64 inFlight = q.second->GetActorsCount();
            sizeInDocs += size;
            sizeInBytes += space;
            allInFlight += inFlight;
            if (size || space) {
                NJson::TJsonValue& queue = result[service.first].InsertValue(q.first, NJson::JSON_MAP);
                queue.InsertValue("current_in_flight", inFlight);
                queue.InsertValue("hr", ToString(size) + " (" + GetHumanReadableNumber(space) + ")");
                queue.InsertValue("docs", size);
                queue.InsertValue("space_bytes", space);
            }
        }
    }
    NJson::TJsonValue& common = result["common"];
    common.InsertValue("docs", sizeInDocs);
    common.InsertValue("space_bytes", sizeInBytes);
    common.InsertValue("current_in_flight", allInFlight);
    common.InsertValue("hr", ToString(sizeInDocs) + " (" + GetHumanReadableNumber(sizeInBytes) + ")");
}

void TDeferredMessagesQueue::Start(TTaskExecutor& executor) {
    TGuard<TMutex> g(MutexStartStop);
    if (!Active) {
        Active = true;
        ThreadPool.Start(0);
        ThreadPool.SafeAdd(new TGarbageCollector(*this));
        for (ui32 i = 0; i < Config.GetDeferredMQConfig().Workers; ++i) {
            ThreadPool.SafeAdd(new TSender(*this, executor, "DeferredMsgSend"));
        }
        StoreQueue.Start(Config.GetDeferredMQConfig().Workers);
    }
}

void TDeferredMessagesQueue::Stop() {
    TGuard<TMutex> g(MutexStartStop);
    if (Active) {
        Active = false;
        StoppedEvent.Signal();
        for (auto& i : Queues)
            i.second.Queue->Stop();
        ThreadPool.Stop();
        TInstant startTime = Now();
        while (GetAllRequestersCount() && (Now() - startTime < Config.GetDeferredMQConfig().DeferredMessagesTimeout * 3)) {
            NOTICE_LOG << GetAllRequestersCount() << " listeners destroy waiting for " << (Now() - startTime).Seconds() << " seconds" << Endl;
            sleep(1);
        }
        ui64 lisCounter = GetAllRequestersCount();
        VERIFY_WITH_LOG(!lisCounter, "%lu listeners not destroyed", lisCounter);

        StoreQueue.Stop();

        for (auto&& q : Queues) {
            INFO_LOG << "action=queue_stopped;name=" << q.first << ";size=" << q.second.Queue->Size() << Endl;
        }

        Services.clear();
        Queues.clear();
    }
}

ui64 TDeferredMessagesQueue::GetAllRequestersCount() const {
    ui64 result = 0;
    for (auto& i : Queues)
        result += i.second.Queue->GetActorsCount();
    return result;
}

void TDeferredMessagesQueue::StartStorage() {
    TGuard<TMutex> g(MutexStartStop);
    StoreQueue.Start(Config.GetDeferredMQConfig().Workers);
}

void TDeferredMessagesQueue::StopStorage() {
    TGuard<TMutex> g(MutexStartStop);
    StoreQueue.Stop();
}

//TProcessor
TDeferredMessagesQueue::TProcessor::TProcessor(TDeferredMessagesQueue& queue, const TDuration& period, const TString& name)
    : Queue(queue)
    , Period(period)
    , Name(name)
{}

void TDeferredMessagesQueue::TProcessor::Process(void* /*ThreadSpecificResource*/) {
    THolder<IObjectInQueue> suicide(this);
    if (Name) {
        TThread::SetCurrentThreadName(Name.c_str());
    }
    do {
        bool processed = false;
        for (auto& queue : Queue.Queues) {
            if (!Queue.Active) {
                break;
            }
            try {
                processed |= DoProcess(queue.first, queue.second.Queue);
            } catch (...) {
                if (!Queue.Config.GetLog().IsNullLog()) {
                    ERROR_LOG << queue.first << "fail: " << CurrentExceptionMessage() << Endl;
                }
            }
        }
        if (!processed && Queue.Active)
            Queue.StoppedEvent.WaitT(Period);
    } while (Queue.Active);
}

//TGarbageCollector
TDeferredMessagesQueue::TGarbageCollector::TGarbageCollector(TDeferredMessagesQueue& queue)
    : TProcessor(queue, queue.Config.GetDeferredMQConfig().GarbageCollectionPeriod, "DeferredMsgGC")
{}

bool TDeferredMessagesQueue::TGarbageCollector::DoProcess(const TString& /*queueName*/, IQueue::TPtr oneBackendQueue) {
    oneBackendQueue->CollectGarbage();
    return false;
}

namespace {
    class TDeferredDocument: public TInternalDocument {
    public:
        TDeferredDocument(const TString& service, const TString& backend, const TDispatcherConfig& config, TAtomic& counter, TDeferredMessagesQueue::IQueue::IMessage::TPtr document, TDeferredMessagesQueue::TBackendData& backendData)
            : TInternalDocument(service, NIndexerProxy::TProtoAdapter::AdapterAlias, document->RawData())
            , Config(config)
            , Backend(backend)
            , Url(document->Data().GetDocument().GetUrl())
            , MessageType(document->Data().GetMessageType())
            , Document(document)
            , Counter(counter)
            , FailRate(backendData.SendFailed)
        {
            if (Backend != service)
                AllowedBackends.insert(Backend);
            DeferredMessage = true;
            DisableQueue = true;
            Source = "queue";
            Origin = Backend;
            Timeout = Config.GetDeferredMQConfig().DeferredMessagesTimeout;
            AtomicIncrement(Counter);
            backendData.TrySend.Hit();
        }

        ~TDeferredDocument() {
            AtomicDecrement(Counter);
        }

    protected:
        void DoReply(ui32 codeReply, const TString& /*message*/, const TString& /*statusMessage*/) override {
            if (!IsServerError(codeReply)) {
                Document->Ack();
                Log(Backend, "ACK");
            } else {
                Log(Backend, "NACK");
                FailRate.Hit();
            }
        }

    private:
        template <class T>
        void Log(const TString& info, T msg) const {
            if (!Config.GetLog().IsNullLog()) {
                Config.GetLog() << NLoggingImpl::GetLocalTimeS() << '\t'
                    << msg << '\t'
                    << info << '\t'
                    << NRTYServer::TMessage_TMessageType_Name(MessageType) << '\t'
                    << Url << Endl;
            }
        }

    private:
        const TDispatcherConfig& Config;
        const TString Backend;
        const TString Url;
        const NRTYServer::TMessage::TMessageType MessageType;
        const TDeferredMessagesQueue::IQueue::IMessage::TPtr Document;
        TAtomic& Counter;
        TEventRate<>& FailRate;
    };
}

//TReader
TDeferredMessagesQueue::TSender::TSender(TDeferredMessagesQueue& queue, TTaskExecutor& executor, const TString& name)
    : TProcessor(queue, queue.Config.GetDeferredMQConfig().RereadPeriod, name)
    , Executor(executor)
{}

bool TDeferredMessagesQueue::TSender::DoProcess(const TString& queueName, IQueue::TPtr oneBackendQueue) {
    auto q = Queue.Queues.find(queueName);
    CHECK_WITH_LOG(q != Queue.Queues.end());
    const TDeferredMQConfig::TFailControlConfig& fcConfig = Queue.Config.GetDeferredMQConfig().FailControlConfig;
    float trySend = q->second.TrySend.Get(fcConfig.CheckPeriod);
    float sendFail = q->second.SendFailed.Get(fcConfig.CheckPeriod);
    if (trySend > fcConfig.MinRate && sendFail >= fcConfig.CriticalRatio * trySend) {
        return false;
    }
    IQueue::IMessage::TPtr msg = oneBackendQueue->Get();
    if (!msg) {
        return false;
    }
    auto service = q->second.Service;
    CHECK_WITH_LOG(service);
    if (const TDuration lifetime = service->GetConfig().DeferredMessagesLifetimeDuration) {
        const TInstant timestamp = TInstant::Seconds(msg->Data().GetDocument().GetModificationTimestamp());
        if (timestamp + lifetime < Now()) {
            msg->Ack();
            if (!Queue.Config.GetLog().IsNullLog()) {
                Queue.Config.GetLog() << NLoggingImpl::GetLocalTimeS() << '\t'
                    << "DEPRECATED" << '\t'
                    << queueName << '\t'
                    << NRTYServer::TMessage_TMessageType_Name(msg->Data().GetMessageType()) << '\t'
                    << msg->Data().GetDocument().GetUrl() << Endl;
            }
            return true;
        }
    }
    try {
        const TString& backend = queueName;
        const auto& header = oneBackendQueue->GetHeader();
        auto doc = MakeHolder<TDeferredDocument>(header.Service, backend, Queue.Config, oneBackendQueue->MutableActorsCounter(), msg, q->second);
        Executor.AddContext(doc.Release());
    } catch (const yexception& exc) {
        if (!Queue.Config.GetLog().IsNullLog()) {
            Queue.Config.GetLog() << NLoggingImpl::GetLocalTimeS() << '\t'
                << "PROCESSING_FAILED" << '\t'
                << queueName << '\t'
                << exc.what() << Endl;
        }
    }
    return true;
}

template <>
void Out<TDeferredMessagesQueue::TQueueHeader>(IOutputStream& output, TTypeTraits<TDeferredMessagesQueue::TQueueHeader>::TFuncParam header) {
    output << header.Service << "(" << header.Shards.ToString() << ")";
}

template <>
TDeferredMessagesQueue::TQueueHeader FromStringImpl<TDeferredMessagesQueue::TQueueHeader>(const char* data, size_t length) {
    const TStringBuf s(data, length);
    TDeferredMessagesQueue::TQueueHeader result;
    result.Service = s.Before('(');
    result.Shards = FromString<TInterval<NSearchMapParser::TShardIndex>>(s.After('(').Before(')'));

    return result;
}
