#include "saas_push.h"
#include "signals.h"

#include <saas/library/tvm/logger.h>
#include <saas/library/persqueue/logger/logger.h>
#include <saas/library/searchmap/parsers/parser.h>

#include <saas/util/logging/tskv_log.h>
#include <saas/util/json/json.h>

#include <library/cpp/json/json_writer.h>
#include <util/generic/overloaded.h>
#include <library/cpp/svnversion/svnversion.h>
#include <library/cpp/yconf/patcher/unstrict_config.h>

#include <util/stream/file.h>
#include <util/generic/guid.h>
#include <util/system/hostname.h>

namespace {
    void WriteResponseHeader(IOutputStream& out, HttpCodes code) {
        out << "HTTP/1.1 " << HttpCodeStrEx(code) <<
            "\r\n"
            "Content-Type: application/json\r\n"
            "Access-Control-Allow-Origin: *\r\n"
            "Access-Control-Allow-Credentials: true\r\n"
            "\r\n";
    }

    void ReportData(IOutputStream& out, HttpCodes code, const TString& data) {
        WriteResponseHeader(out, code);
        out << data;
    }

    void ReportTass(IOutputStream& out) {
        WriteResponseHeader(out, HTTP_OK);
        ReportUnistatSignals(out);
    }

    void ReportOk(IOutputStream& out) {
        WriteResponseHeader(out, HTTP_OK);
        out << "{}";
    }

    void ReportJson(IOutputStream& out, const NJson::TJsonValue& json) {
        WriteResponseHeader(out, HTTP_OK);
        NJsonWriter::TBuf jsonWriter(NJsonWriter::HEM_DONT_ESCAPE_HTML, &out);
        jsonWriter.WriteJsonValue(&json);
    }

    void ReportCommandResult(IOutputStream& out, HttpCodes code, const TString& comment) {
        WriteResponseHeader(out, code);
        NJsonWriter::TBuf jsonWriter(NJsonWriter::HEM_DONT_ESCAPE_HTML, &out);
        jsonWriter.BeginObject();
        jsonWriter.WriteKey("result");
        jsonWriter.WriteBool(code == HTTP_OK);
        jsonWriter.WriteKey("comment");
        jsonWriter.WriteString(comment);
        jsonWriter.EndObject();
    }

    void ReportNotFound(IOutputStream& out) {
        WriteResponseHeader(out, HTTP_NOT_FOUND);
    }

    HttpCodes GetHttpCode(const NSaas::TPersQueueWriter::TWriteResult& result) {
        if (result.Written) {
            return HTTP_OK;
        }
        return result.UserError ? HTTP_BAD_REQUEST : HTTP_INTERNAL_SERVER_ERROR;
    }

    TString GetFQDNHostNameOrGuid() {
        try {
            return FQDNHostName();
        } catch (...) {
            ERROR_LOG << "FQDNHostName failed: " << CurrentExceptionMessage() << Endl;
            static const TString guid(CreateGuidAsString());
            return guid;
        }
    }

    ui64 GetSvnRevision() {
        static constexpr TStringBuf lastChangedRevLabel = "Last Changed Rev: ";
        auto lines = SplitString(GetProgramSvnVersion(), "\n");
        for (auto& line : lines) {
            auto pos = line.find(lastChangedRevLabel);
            if (pos != TString::npos) {
                TString revision = Strip(line.substr(pos + lastChangedRevLabel.size()));
                return FromString<ui64>(revision);
            }
        }
        return 0;
    }
}

namespace NSaasPush {
    class TSaasPushTelemetry : public NSaas::ITelemetry {
    public:
        TSaasPushTelemetry(
            TDaemon* daemon,
            const NSaas::TTelemetryConfig& config,
            TPQLibPtr pqLib,
            std::shared_ptr<NPersQueue::ICredentialsProvider> credentialsProvider
        )
            : NSaas::ITelemetry("saas_push", config, pqLib, credentialsProvider)
            , Daemon(daemon)
        {}

        NJson::TJsonValue GetTelemetryData() const override {
            NJson::TJsonValue data;
            data.InsertValue("config", Daemon->GetJsonConfig());
            data.InsertValue("status", Daemon->GetStatus());
            return data;
        }

    private:
        TDaemon* Daemon;
    };

    struct TWriteRequestInfo {
        TWriteRequestInfo(const TServerRequestData& request, const TString& data)
            : Data(data)
            , Url(request.ScriptName())
            , CgiParams(request.Query())
            , RemoteAddr(request.RemoteAddr())
            , StartProcessTime(TInstant::MicroSeconds(request.RequestBeginTime()))
        {}

        TString Data;
        TString Url;
        TString CgiParams;
        TString RemoteAddr;
        TInstant StartProcessTime;
    };

    TServiceWriter::TServiceWriter(
            const TServiceInfo& config,
            NSaas::TPQLibPtr pqLib,
            std::shared_ptr<NPersQueue::ICredentialsProvider> credentialsProvider,
            const NSearchMapParser::TSearchMap& searchMap)
        : Config(config)
        , Parser(CreateDataParser(Config.Format))
    {
        NSaas::TPersQueueWriterSettings writerSettings;
        writerSettings.SetServiceInfo(searchMap, Config.Name);
        writerSettings.SetCheckMessagesSettings(Config.CheckMessageSettings);
        writerSettings.SetTvm(credentialsProvider);
        writerSettings.SetPQLib(pqLib);
        writerSettings.SetPersQueueSettings(Config.Server, Config.TopicsDir);
        writerSettings.SetMaxAttempts(Config.AttemptsCount);
        writerSettings.SetConnectionTimeout(Config.ConnectionTimeout);
        writerSettings.SetSendTimeout(Config.WriteTimeout);
        writerSettings.SetSourceIdPrefix(Config.SourceIdPrefix);
        if (Config.Codec) {
            writerSettings.SetCodec(Config.Codec.value());
        }
        PQWriter.Reset(new NSaas::TPersQueueWriter());
        PQWriter->Init(writerSettings);
    }

    std::pair<NSaas::TPersQueueWriter::TWriteResult, TActionPtr> TServiceWriter::Write(const TString& data) {
        try {
            auto action = Parser(data);
            return {PQWriter->Write(*action).GetValueSync(), std::move(action)};
        } catch (...) {
            return {
                NSaas::TPersQueueWriter::GetWriteErrorInfo("Incorrect data: " + CurrentExceptionMessage(), true),
                {}
            };
        }
    }

    NJson::TJsonValue TServiceWriter::GetStatus() const {
        NJson::TJsonValue status;
        status.InsertValue("writer", PQWriter->GetStatus());
        return status;
    }

    bool TServiceWriter::GetLoggingEnabled() const {
        return Config.LoggingEnabled;
    }

    const TString& TServiceWriter::GetServiceName() const {
        return Config.Name;
    }

    const TString& TServiceWriter::GetCtype() const {
        return Config.Ctype;
    }

    const TString& TServiceWriter::GetAlias() const {
        return Config.Alias;
    }

    void TServiceWriter::UpdateSearchMap(const NSearchMapParser::TSearchMap& searchMap) {
        PQWriter->UpdateSearchMap(searchMap);
    }

    TWriter::TWriter(
        const TWriterConfig& config,
        TPQLibPtr pqLib,
        TSearchMapStorage& searchMapStorage,
        TLog& tvmLog
    )
        : IServer(this, config.HttpOptions)
        , PQLib(pqLib)
        , SearchMapStorage(searchMapStorage)
        , TvmLog(tvmLog)
        , MessagesLog(config.MessagesLog)
        , Config(config)
    {
        InitTvm();
        InitializationThread.Reset(MakeHolder<TThread>([this] { Init(); }));
        InitializationThread->Start();
    }

    TWriter::~TWriter() {
        NeedStop = true;
        InitializationThread->Join();
        for (auto writer : ServiceWriters) {
            SearchMapStorage.RemoveSubscriber(writer.second.Get());
        }
    }

    bool TWriter::IsInitialized() const {
        return Initialized;
    }

    void TWriter::SetInitializationStatus(bool finished, TStringBuf status) {
        INFO_LOG << "Initialization status: " << status << Endl;
        Initialized = finished;
    }

    void TWriter::ReopenLog() {
        MessagesLog.ReopenLog();
    }

    void TWriter::LogAndPushMetrics(
            const TWriteRequestInfo& request,
            const NSaas::TPersQueueWriter::TWriteResult& result,
            const TServiceWriterPtr& service,
            const TActionPtr& action) {
        TDuration processTime = TInstant::Now() - request.StartProcessTime;
        auto httpCode = GetHttpCode(result);

        TSaasPushSignals::ProcessedDoc(httpCode, processTime, service ? service->GetAlias() : "");

        if (service && !service->GetLoggingEnabled()) {
            return;
        }

        NUtil::TTSKVRecord entry("saas-push-log");
        if (service) {
            entry.ForceAdd("service", service->GetServiceName());
            entry.ForceAdd("ctype", service->GetCtype());
            entry.ForceAdd("alias", service->GetAlias());
        }
        if (action) {
            entry.ForceAdd("action", action->GetActionType());
            if (action->HasPrefix()) {
                entry.ForceAdd("keyprefix", action->GetPrefix());
            }
            if (action->HasDocument()) {
                const auto& doc = action->GetDocument();
                entry.ForceAdd("key", doc.GetUrl());
                if (doc.HasDeadlineMinutesUTC()) {
                    entry.ForceAdd("doc_deadline", doc.GetDeadlineMinutesUTC());
                }
                if (doc.HasTimestamp()) {
                    entry.ForceAdd("doc_timestamp", doc.GetTimestamp());
                }
                if (doc.HasVersion()) {
                    entry.ForceAdd("doc_version", doc.GetVersion());
                }
            }
        }
        entry.ForceAdd("size", request.Data.Size());
        entry.ForceAdd("url", request.Url);
        entry.ForceAdd("cgi", request.CgiParams);
        entry.ForceAdd("ip", request.RemoteAddr);

        entry.AddIsoEventTime();
        entry.ForceAdd("process_time_ms", processTime.MilliSeconds());
        entry.ForceAdd("written", result.Written);
        entry.ForceAdd("user_error", result.UserError);
        entry.ForceAdd("http_code", ui32(httpCode));
        entry.ForceAdd("status", result.ToString());

        const TString& logRecord = entry.ToString();
        MessagesLog << logRecord << Endl;
    }

    NSaas::TPersQueueWriter::TWriteResult TWriter::Write(const TString& alias, const TWriteRequestInfo& request) {
        NSaas::TPersQueueWriter::TWriteResult result;
        TServiceWriterPtr service;
        TActionPtr action;
        std::visit(TOverloaded {
            [&](NSaas::TPersQueueWriter::TWriteResult writeResult) {
                result = std::move(writeResult);
            },
            [&](TServiceWriterPtr writer) {
                service = writer;
                std::tie(result, action) = writer->Write(request.Data);
            }
        }, GetWriter(alias));

        LogAndPushMetrics(request, result, service, action);
        return result;
    }

    NSaas::TPersQueueWriter::TWriteResult TWriter::Write(const TString& alias, const TString& data) {
        NSaas::TPersQueueWriter::TWriteResult result;
        std::visit(TOverloaded {
            [&](NSaas::TPersQueueWriter::TWriteResult writeResult) {
                result = std::move(writeResult);
            },
            [&](TServiceWriterPtr writer) {
                TActionPtr action;
                std::tie(result, action) = writer->Write(data);
            }
        }, GetWriter(alias));
        return result;
    }

    std::variant<NSaas::TPersQueueWriter::TWriteResult, TServiceWriterPtr> TWriter::GetWriter(const TString& alias) {
        if (!IsInitialized()) {
            return NSaas::TPersQueueWriter::GetWriteErrorInfo("Uninitialized writer");
        } else {
            const auto writer = ServiceWriters.FindPtr(alias);
            if (!writer) {
                return NSaas::TPersQueueWriter::GetWriteErrorInfo("no service with alias: " + alias, true);
            } else if (!*writer) {
                return NSaas::TPersQueueWriter::GetWriteErrorInfo("Writer for alias " + alias + " has not been initialized yet");
            } else {
                return *writer;
            }
        }
    }

    void TWriter::InitTvm() {
        SetInitializationStatus(false, "creating TVM clients");
        THashMap<NTvmAuth::TTvmId, NSaas::TTvmSettings> tvmSettings;
        for (auto&& service : Config.Services) {
            const NSaas::TTvmSettings& settings = service.TvmConfig.GetSettings();
            if (tvmSettings.contains(settings.ClientId)) {
                tvmSettings[settings.ClientId].Merge(settings);
            } else {
                tvmSettings[settings.ClientId] = settings;
            }
        }

        auto tvmLogger = MakeIntrusive<NSaas::TTvmLogger>(TvmLog);
        THashMap<NTvmAuth::TTvmId, NSaas::TTvmClientPtr> tvmClients;
        for (auto&& settings : tvmSettings) {
            NSaas::TTvmClientPtr tvmClient;
            while (!tvmClient && !NeedStop) {
                try {
                    tvmClient = NSaas::CreateTvmClient(settings.second, tvmLogger);
                } catch (...) {
                    LastError = "Cant create tvm client for source_id=" + ToString(settings.first) + " : " + CurrentExceptionMessage();
                    ERROR_LOG << LastError << Endl;
                    Sleep(Config.SleepOnConnectFailure);
                }
            }
            tvmClients[settings.second.ClientId] = tvmClient;
        }

        SetInitializationStatus(false, "initializing credentials providers");
        for (auto&& service : Config.Services) {
            auto tvmSettings = service.TvmConfig.GetSettings();
            CredentialsProviders[service.Alias] = NPersQueue::CreateTVMCredentialsProvider(
                tvmClients[tvmSettings.ClientId],
                MakeIntrusive<NSaas::TPersQueueLogger<NPersQueue::ILogger>>(TvmLog),
                tvmSettings.DestinationClients.begin()->first
            );
        }
    }

    void TWriter::Init() {
        SetInitializationStatus(false, "init signals");
        TSet<TString> aliases;
        for (auto&& service : Config.Services) {
            ServiceWriters[service.Alias] = {};
            aliases.insert(service.Alias);
        }

        TSaasPushSignals::BuildSignals(aliases);
        TSaasPushSignals::InIncorrectState(true);

        SetInitializationStatus(false, "initializing services");
        for (auto&& service : Config.Services) {
            INFO_LOG << "Create service writer for alias=" << service.Alias << Endl;

            auto& writer = ServiceWriters[service.Alias];
            while (!writer && !NeedStop) {
                try {
                    auto searchMap = SearchMapStorage.GetSearchMap(service.Ctype);
                    if (!searchMap) {
                        ythrow yexception() << "Cannot get SearchMap with ctype=" << service.Ctype << " for alias=" << service.Alias;
                    }
                    writer = MakeAtomicShared<TServiceWriter>(
                        service, PQLib, CredentialsProviders[service.Alias], *searchMap);
                } catch(...) {
                    LastError = "Error creating service writer instance: " + CurrentExceptionMessage();
                    ERROR_LOG << LastError << Endl;
                    Sleep(Config.SleepOnConnectFailure);
                }
            }
            SearchMapStorage.AddSubscriber(writer.Get());
        }

        SetInitializationStatus(false, "strting HTTP server for writer");
        while (!NeedStop && !Start()) {
            LastError = "Error creating service writer instance";
            ERROR_LOG << LastError << Endl;
            Sleep(Config.SleepOnConnectFailure);
        }
        SetInitializationStatus(true, "done");
        TSaasPushSignals::InIncorrectState(false);
    }

    NJson::TJsonValue TWriter::GetStatus() const {
        NJson::TJsonValue services;

        if (IsInitialized()) {
            for (auto&& service : ServiceWriters) {
                services.InsertValue(service.first, service.second->GetStatus());
            }
        }
        NJson::TJsonValue status;
        status.InsertValue("Services", services);
        status.InsertValue("Initializated", IsInitialized());
        status.InsertValue("LastError", LastError);
        return status;
    }

    void TWriter::OnShutdown() {
        Stop();
        IServer::OnShutdown();
    }

    TClientRequest* TWriter::CreateClient() {
        return new TWriteRequest(*this);
    }

    std::shared_ptr<NPersQueue::ICredentialsProvider> TWriter::GetAnyCredentialsProvider() const {
        return CredentialsProviders.begin()->second;
    }

    std::shared_ptr<NPersQueue::ICredentialsProvider> TWriter::GetCredentialsProvider(const TString& alias) const {
        auto it = CredentialsProviders.find(alias);
        Y_VERIFY(it != CredentialsProviders.end(), "No credentials with alias: %s", alias.c_str());        
        return it->second;
    }

    TWriteRequest::TWriteRequest(TWriter& writer) : Writer(writer)
    {
    }

    TControllerRequest::TControllerRequest(IControllerServer& owner) : Owner(owner)
    {
    }

    bool TControllerRequest::Reply(void*) {
        try {
            if (!ProcessHeaders()) {
                return true;
            }
            RD.Scan();

            if (RD.ScriptName() == "/command"sv) {
                NJson::TJsonValue data = NUtil::JsonFromString(GetPostData());
                auto params = NUtil::GetMap(data);
                if (!params.contains("action")) {
                    ReportCommandResult(Output(), HTTP_BAD_REQUEST, "must have action field in post data");
                } else {
                    auto action = params["action"].GetString();
                    if (action == "shutdown") {
                        Owner.Shutdown();
                        ReportCommandResult(Output(), HTTP_OK, "");
                    } else if (action == "reopen-log") {
                        Owner.ReopenLog();
                        ReportOk(Output());
                    } else {
                        ReportCommandResult(Output(), HTTP_BAD_REQUEST, "unknown action");
                    }
                }
            } else if (RD.ScriptName() == "/get-status"sv) {
                ReportJson(Output(), Owner.GetStatus());
            } else if (RD.ScriptName() == "/get-config"sv) {
                TString format = RD.CgiParam.Has("format") ? RD.CgiParam.Get("format") : "text";
                if (format == "json") {
                    ReportJson(Output(), Owner.GetJsonConfig());
                } else if (format == "text") {
                    ReportData(Output(), HTTP_OK, Owner.GetStringConfig());
                } else {
                    ReportCommandResult(Output(), HTTP_BAD_REQUEST, "Unknown config format: " + format);
                }
            } else if (RD.ScriptName() == "/tass"sv) {
                ReportTass(Output());
            } else {
                ReportNotFound(Output());
            }
        } catch (const NJson::TJsonException&) {
            ERROR_LOG << "Can not process JSON: " << CurrentExceptionMessage() << Endl;
            WriteResponseHeader(Output(), HTTP_BAD_REQUEST);
            Output() << CurrentExceptionMessage();
        } catch (...) {
            ERROR_LOG << "Can not process HTTP request: " << CurrentExceptionMessage() << Endl;
            WriteResponseHeader(Output(), HTTP_INTERNAL_SERVER_ERROR);
            Output() << CurrentExceptionMessage();
        }
        Output().Finish();
        return true;
    }

    bool TWriteRequest::Reply(void*) {
        try {
            if (!ProcessHeaders()) {
                return true;
            }
            RD.Scan();

            if (TString(RD.ScriptName()).StartsWith(TStringBuf("/push/"))) {
                TVector<TString> path = SplitString(TString{RD.ScriptName()}, "/");
                if (path.size() != 2) {
                    ReportNotFound(Output());
                } else {
                    auto result = Writer.Write(path[1], TWriteRequestInfo{RD, GetPostData()});
                    ReportData(Output(), GetHttpCode(result), result.ToString());
                }
            } else {
                ReportNotFound(Output());
            }
        } catch (const NJson::TJsonException&) {
            ERROR_LOG << "Can not process JSON: " << CurrentExceptionMessage() << Endl;
            WriteResponseHeader(Output(), HTTP_BAD_REQUEST);
            Output() << CurrentExceptionMessage();
        } catch (...) {
            ERROR_LOG << "Can not process HTTP request: " << CurrentExceptionMessage() << Endl;
            WriteResponseHeader(Output(), HTTP_INTERNAL_SERVER_ERROR);
            Output() << CurrentExceptionMessage();
        }
        Output().Finish();
        return true;
    }

    TString IRequest::GetPostData() const {
        return TString(Buf.AsCharPtr(), Buf.Length());
    }

    void IServer::Shutdown() {
        BeforeShutdown();
        OnShutdown();
        AfterShutdown();
    }

    void IServer::BeforeShutdown() {
        NeedStop = true;
    }

    void IServer::OnShutdown() {
        INFO_LOG << "Shutdown the HTTP server..." << Endl;
        THttpServer::Shutdown();
        Stopped.Signal();
        INFO_LOG << "HTTP server stopped." << Endl;
    }

    void IServer::AfterShutdown() {}

    void IServer::WaitStopped() {
        Stopped.Wait();
    }

    IControllerServer::IControllerServer(const TControllerConfig& config)
        : IServer(this, config.HttpOptions)
    {
        RegisterGlobalMessageProcessor(this);
    }

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

    TClientRequest* IControllerServer::CreateClient() {
        return new TControllerRequest(*this);
    }

    TString IControllerServer::GetStringConfig() const {
        const TConfig& config = GetConfig();
        TStringStream so;
        config.Print(so);
        TUnstrictConfig unstrictConfig;
        if (!unstrictConfig.ParseMemory(so.Str())) {
            ythrow yexception() << "Parsing config error";
        }
        return unstrictConfig.ToString();
    }

    NJson::TJsonValue IControllerServer::GetJsonConfig() const {
        const TString configText = GetStringConfig();
        NJson::TJsonValue result;
        TUnstrictConfig::ToJson(configText, result);
        return result;
    }

    TString IControllerServer::Name() const {
        return "ControllerServer";
    }

    bool IControllerServer::Process(IMessage* message) {
        if (message->As<TMessageReopenLogs>()) {
            ReopenLog();
            return true;
        }
        return false;
    }

    TDaemon::TDaemon(const TConfig& config)
        : IControllerServer(config.ControllerConfig)
        , LogbrokerLog(config.ServerConfig.LogbrokerLog, config.ServerConfig.LogLevel)
        , TvmLog(config.ServerConfig.LogbrokerLog, config.ServerConfig.LogLevel)
        , SearchMapStorage(
            config.ServerConfig.SearchMapSettings,
            config.ServerConfig.WriterConfig.Services,
            config.ServerConfig.UpdateSearchMapPeriod
        )
        , Config(config)
    {
        PQLib = MakeAtomicShared<NPersQueue::TPQLib>(Config.ServerConfig.PQLibSettings);
        PQLib->SetLogger(MakeIntrusive<NSaas::TPersQueueLogger<NPersQueue::ILogger>>(LogbrokerLog));
        Writer = MakeHolder<TWriter>(config.ServerConfig.WriterConfig, PQLib, SearchMapStorage, TvmLog);

        const auto& credAlias = Config.TelemetryConfig.GetAlias();
        Telemetry = MakeHolder<TSaasPushTelemetry>(
            this,
            Config.TelemetryConfig,
            PQLib,
            credAlias ? Writer->GetCredentialsProvider(credAlias) : Writer->GetAnyCredentialsProvider()
        );
    }

    void TDaemon::BeforeShutdown() {
        IServer::BeforeShutdown();
        Writer->Shutdown();
    }

    void TDaemon::ReopenLog() {
        TLogBackend::ReopenAllBackends(false);
        LogbrokerLog.ReopenLog();
        TvmLog.ReopenLog();
        Writer->ReopenLog();
        INFO_LOG << "Logs reopened" << Endl;
    }

    NJson::TJsonValue TDaemon::GetStatus() const {
        NJson::TJsonValue status;
        status.InsertValue("Writer", Writer->GetStatus());
        status.InsertValue("FQDN", GetFQDNHostNameOrGuid());
        status.InsertValue("SvnRevision", GetSvnRevision());
        if (Config.ServerConfig.ParsedFromOldFormat) {
            status.InsertValue("ConfigParsedFromOldFormat", true);
        }
        return status;
    }

    const TConfig& TDaemon::GetConfig() const {
        return Config;
    }

    TConfig ReadConfig(const TString& path, ui16 basePort, ui16 controllerPort) {
        TFileInput in(path);
        TString configText = in.ReadAll();
        TAnyYandexConfig parsedConfig;
        if (!parsedConfig.ParseMemory(configText)) {
            TString errors;
            parsedConfig.PrintErrors(errors);
            ythrow yexception() << "Cant parse config: " << errors.data();
        }

        TConfig config;
        config.Init(parsedConfig);

        if (basePort) {
            config.ServerConfig.WriterConfig.HttpOptions.Port = basePort;
        }
        if (controllerPort) {
            config.ControllerConfig.HttpOptions.Port = controllerPort;
        }
        return config;
    }
}
