#include "telemetry.h"

#include <library/cpp/balloc/optional/operators.h>
#include <library/cpp/logger/global/global.h>
#include <library/cpp/json/writer/json.h>

#include <util/generic/guid.h>
#include <util/random/random.h>

namespace NSaas {
    void TTelemetryConfig::SetServer(const TString& value) {
        Server = value;
    }

    void TTelemetryConfig::SetTopic(const TString& value) {
        Topic = value;
    }

    void TTelemetryConfig::SetInterval(TDuration value) {
        Interval = value;
    }

    void TTelemetryConfig::SetMaxRandomInterval(TDuration value) {
        MaxRandomInterval = value;
    }

    void TTelemetryConfig::SetConnectionTimeout(TDuration value) {
        ConnectionTimeout = value;
    }

    void TTelemetryConfig::SetWriteTimeout(TDuration value) {
        WriteTimeout = value;
    }

    TString TTelemetryConfig::GetServer() const {
        return Server;
    }

    TString TTelemetryConfig::GetTopic() const {
        if (Topic) {
            return Topic;
        }
        TString topic = "saas/saas_push/telemetry";
        // logfeller cannot use two topics with the same name from different LB clusters (16.06.2020)
        if (Server == "lbkxt.logbroker.yandex.net") {
            topic += "-prestable";
        }
        return topic;
    }

    TDuration TTelemetryConfig::GetInterval() const {
        return Interval;
    }

    TDuration TTelemetryConfig::GetMaxRandomInterval() const {
        return MaxRandomInterval;
    }

    TDuration TTelemetryConfig::GetConnectionTimeout() const {
        return ConnectionTimeout;
    }

    TDuration TTelemetryConfig::GetWriteTimeout() const {
        return WriteTimeout;
    }

    void TTelemetryConfig::Init(const TYandexConfig::Section* section) {
        const TYandexConfig::Directives& directives = section->GetDirectives();

        directives.GetValue("Server", Server);
        directives.GetValue("Topic", Topic);
        directives.GetValue("Interval", Interval);
        directives.GetValue("MaxRandomInterval", MaxRandomInterval);
        directives.GetValue("ConnectionTimeout", ConnectionTimeout);
        directives.GetValue("WriteTimeout", WriteTimeout);
    }

    void TTelemetryConfig::Print(IOutputStream& so) const {
        so << "Server: " << Server << Endl;
        so << "Topic : " << Topic << Endl;
        so << "Interval : " << Interval << Endl;
        so << "MaxRandomInterval : " << MaxRandomInterval << Endl;
        so << "ConnectionTimeout : " << ConnectionTimeout << Endl;
        so << "WriteTimeout : " << WriteTimeout << Endl;
    }

    ITelemetry::ITelemetry(
        const TString& applicationName,
        const TTelemetryConfig& config,
        TPQLibPtr pqLib,
        std::shared_ptr<NPersQueue::ICredentialsProvider> credentialsProvider
    )
        : ApplicationName(applicationName)
        , Config(config)
        , PQLib(pqLib)
    {
        if (Config.GetInterval() == TDuration::Zero()) {
            return;
        }

        ProducerSettings.Server = NPersQueue::TServerSetting{ Config.GetServer() };
        ProducerSettings.Topic = Config.GetTopic();
        ProducerSettings.SourceId = CreateGuidAsString();
        ProducerSettings.ReconnectOnFailure = true;
        ProducerSettings.CredentialsProvider = credentialsProvider;

        TThread::TParams processThreadParams([](void* object) -> void* {
            ThreadDisableBalloc();
            auto this_ = reinterpret_cast<ITelemetry*>(object);
            this_->Process();
            return nullptr;
        }, this);
        ProcessThread.Reset(new TThread(processThreadParams));
        ProcessThread->Start();
    }

    ITelemetry::~ITelemetry() {
        StopFlag = true;
        StopSignal.Signal();
        if (ProcessThread) {
            ProcessThread.Destroy();
        }
    }

    void ITelemetry::FillCommonData(NJson::TJsonValue& data) const {
        data.InsertValue("application", ApplicationName);
        data.InsertValue("timestamp", TInstant::Now().Seconds());
    }

    void ITelemetry::Process() {
        THolder<NPersQueue::IProducer> producer;
        while(!StopFlag) {
            try {
                producer = PQLib->CreateProducer(ProducerSettings);
                auto future = producer->Start();
                if (!future.Wait(Config.GetConnectionTimeout())) {
                    ythrow yexception() << "connection timeout expired";
                }
                if (future.GetValue().Response.HasError()) {
                   ythrow yexception() << future.GetValue().Response;
                }
            } catch (...) {
                ERROR_LOG << "Cannot create producer: " << CurrentExceptionMessage() << Endl;
                producer = nullptr;
                continue;
            }
            break;
        }

        while(!StopFlag) {
            auto data = GetTelemetryData();
            FillCommonData(data);

            TStringStream stream;
            NJsonWriter::TBuf jsonWriter(NJsonWriter::HEM_DONT_ESCAPE_HTML, &stream);
            jsonWriter.WriteJsonValue(&data);

            auto future = producer->Write(stream.Str());
            future.Wait(Config.GetWriteTimeout());
            if (future.HasValue()) {
                INFO_LOG << "telemetry sended: " << future.GetValue().Response << Endl;
            } else {
                ERROR_LOG << "telemetry failed: write timeout" << Endl;
            }
            TDuration randomInterval = TDuration::Seconds(RandomNumber<ui64>(Config.GetMaxRandomInterval().Seconds()));
            StopSignal.WaitT(Config.GetInterval() + randomInterval);
        }
    }
}
