#include "api.h"

#include <saas/library/persqueue/common/common.h>

#include <library/cpp/retry/retry.h>
#include <library/cpp/logger/global/global.h>

#include <util/datetime/base.h>
#include <util/generic/yexception.h>
#include <util/string/cast.h>
#include <util/string/join.h>

#include <grpc++/grpc++.h>

#define ADD_PROCESS_METHOD(REQUEST_TYPE)                                                                                        \
NLogBroker::REQUEST_TYPE##Result Process(const NLogBroker::REQUEST_TYPE##Request& request) {                                    \
    return Process<NLogBroker::REQUEST_TYPE##Request, NLogBroker::REQUEST_TYPE##Response, NLogBroker::REQUEST_TYPE##Result>(    \
        request,                                                                                                                \
        &TConfigManager::Stub::REQUEST_TYPE                                                                                     \
    );                                                                                                                          \
}

namespace {
    template<class TRequest>
    TString GetPublicRequestString(const TRequest& request) {
        TString token = NSaasLB::MaskToken(request.token());
        auto publicRequest = request;
        publicRequest.set_token(token);
        return ToString(publicRequest);
    }

    template<>
    TString GetPublicRequestString(const NLogBroker::ExecuteModifyCommandsRequest& request) {
        NLogBroker::ExecuteModifyCommandsRequest publicRequest = request;

        TString token = NSaasLB::MaskToken(publicRequest.token());
        publicRequest.set_token(token);

        for (auto& action : *publicRequest.mutable_actions()) {
            if (action.has_create_remote_mirror_rule()) {
                NLogBroker::CreateRemoteMirrorRuleRequest* mirrorRule = action.mutable_create_remote_mirror_rule();
                NLogBroker::Credentials* credentials = mirrorRule->mutable_properties()->mutable_credentials();

                const TString& mirrorToken = NSaasLB::MaskToken(credentials->oauth_token());
                credentials->set_oauth_token(mirrorToken);
            }
        }
        return ToString(publicRequest);
    }

    template<>
    TString GetPublicRequestString(const Ydb::Operations::GetOperationRequest& request) {
        return ToString(request);
    }
}

namespace NSaasLB {
    // Logbroker limit of batch size for ExecuteModifyCommandsRequest
    const ui32 MAX_COMMANDS_IN_BATCH = 100;

    class TRequestProcessor {
    private:
        using TConfigManager = NLogBroker::ConfigurationManagerService;
    public:
        TRequestProcessor(const TString& server) {
            Channel = grpc::CreateChannel(server, grpc::InsecureChannelCredentials());
            Stub = TConfigManager::NewStub(Channel);
        }

        ADD_PROCESS_METHOD(ListAccounts)
        ADD_PROCESS_METHOD(ListYtDeliveries)
        ADD_PROCESS_METHOD(ListDirectory)
        ADD_PROCESS_METHOD(DescribePath)
        ADD_PROCESS_METHOD(ExecuteModifyCommands)

    private:
        template<class TRequest, class TResponse, class TProcFunc>
        void RunRequest(const TRequest& request, TResponse& response, TProcFunc processFunc);

        template<class TRequest, class TResponse, class TProcFunc>
        void RunRequestWithRetries(const TRequest& request, TResponse& response, TProcFunc processFunc);

        template<class TRequest, class TResponce, class TResult, class TProcFunc>
        TResult Process(const TRequest& request, TProcFunc processFunc);

    private:
        std::shared_ptr<grpc::Channel> Channel;
        std::shared_ptr<TConfigManager::Stub> Stub;
    };

    template<class TRequest, class TResponse, class TProcFunc>
    void TRequestProcessor::RunRequest(const TRequest& request, TResponse& response, TProcFunc processFunc) {
        grpc::ClientContext context;
        auto status = (Stub.get()->*processFunc)(&context, request, &response);
        if (!status.ok()) {
            ythrow yexception() << "Error during grpc request to logbroker: " << status.error_message();
        }
    }

    template<class TRequest, class TResponse, class TProcFunc>
    void TRequestProcessor::RunRequestWithRetries(const TRequest& request, TResponse& response, TProcFunc processFunc) {
        INFO_LOG << "Run request to logbroker " << request.GetTypeName() << ": " << GetPublicRequestString(request) << Endl;
        auto f = [&] () { RunRequest(request, response, processFunc); };
        try {
            DoWithRetry<yexception>(f, TRetryOptions(3, TDuration::Minutes(1)), true);
        } catch (...) {
            ERROR_LOG <<  "Failed request to logbroker: " << CurrentExceptionMessage() << Endl;
            throw;
        }
        INFO_LOG << "Request to logbroker finished." << Endl;
    }

    template<class TRequest, class TResponce, class TResult, class TProcFunc>
    TResult TRequestProcessor::Process(const TRequest& request, TProcFunc processFunc) {
        TResponce response;
        RunRequestWithRetries(request, response, processFunc);
        if (!response.operation().ready()) {
            Ydb::Operations::GetOperationRequest getOperationRequest;
            getOperationRequest.set_id(response.operation().id());
            Ydb::Operations::GetOperationResponse getOperationResponse;
            do {
                RunRequestWithRetries(getOperationRequest, getOperationResponse, &TConfigManager::Stub::GetOperation);
            } while(!getOperationResponse.operation().ready());
            *response.mutable_operation() = getOperationResponse.operation();
        }
        if (response.operation().status() != Ydb::StatusIds::SUCCESS) {
            ythrow yexception() << "Request to logbroker failed: " << response;
        }
        TResult result;
        response.operation().result().UnpackTo(&result);
        return result;
    }

    TLogbrokerApi::TLogbrokerApi(const TString& server, const TString& token)
        : RequestProcessor(new TRequestProcessor(server))
        , Token(token)
    {}

    NLogBroker::ListAccountsResult TLogbrokerApi::GetAccountsList() {
        auto request = NLogBroker::ListAccountsRequest();
        request.set_token(Token);
        return RequestProcessor->Process(request);
    }

    NLogBroker::ListYtDeliveriesResult TLogbrokerApi::GetYtDeliveriesList() {
        auto request = NLogBroker::ListYtDeliveriesRequest();
        request.set_token(Token);
        return RequestProcessor->Process(request);
    }

    NLogBroker::ListDirectoryResult TLogbrokerApi::ListDirectory(const TString& path) {
        NLogBroker::ListDirectoryRequest request;
        request.mutable_path()->set_path(path);
        request.set_token(Token);
        return RequestProcessor->Process(request);
    }

    NLogBroker::DescribePathResult TLogbrokerApi::DescribePath(const TString& path) {
        NLogBroker::DescribePathRequest request;
        request.mutable_path()->set_path(path);
        request.set_token(Token);
        return RequestProcessor->Process(request);
    }

    TVector<NLogBroker::ExecuteModifyCommandsResult> TLogbrokerApi::ExecuteModifyCommands(
        const TVector<NLogBroker::SingleModifyRequest>& commands,
        const TString& comment
    ) {
        TVector<NLogBroker::ExecuteModifyCommandsResult> results;
        for (ui32 batchStart = 0; batchStart < commands.size(); batchStart += MAX_COMMANDS_IN_BATCH) {
            ui32 batchEnd = Min<ui32>(batchStart + MAX_COMMANDS_IN_BATCH, commands.size());
            TString batchComment = Join("", "Batch [", batchStart, ":", batchEnd - 1, "] of ",
                                        commands.size(), " commands: ", comment);
            NLogBroker::ExecuteModifyCommandsRequest request;
            request.set_token(Token);
            request.set_comment(batchComment);
            *request.mutable_actions() = {commands.begin() + batchStart, commands.begin() + batchEnd};

            results.push_back(RequestProcessor->Process(request));
        }
        return results;
    }
}
