#include <saas/tools/parse_int_eventlog/params.pb.h>
#include <saas/tools/parse_int_eventlog/parsed_row.pb.h>

#include <mapreduce/yt/interface/client.h>

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

#include <contrib/libs/re2/re2/re2.h>

#include <util/digest/murmur.h>
#include <util/generic/algorithm.h>
#include <util/generic/string.h>
#include <util/stream/file.h>
#include <util/stream/null.h>
#include <util/string/join.h>
#include <util/string/split.h>
#include <util/string/type.h>

#include <utility>

using NEventLogParser::TParsedRow;

namespace {
    using namespace NYT;

    const TStringBuf EVLOG_EVENT_TYPE = "event_type";
    const TStringBuf EVLOG_EVENT_DATA = "event_data";
    const TStringBuf EVLOG_INSTANCE = "instance";
    const TStringBuf EVLOG_FRAME_ID = "frame_id";
    const TStringBuf EVLOG_TIMESTAMP = "ts";

    const TStringBuf EVLOG_PARAM_REQID = "reqid=";
    const TStringBuf EVLOG_PARAM_DH = "dh=";
    const TStringBuf EVLOG_PARAM_DF = "DF=";

    TStringBuf ExtractBySplit(TStringBuf s, char delimiter, size_t n) {
        size_t i = 0;
        for (auto it : StringSplitter(s).Split(delimiter)) {
            if (i++ == n) {
                return it.Token();
            }
        }
        return {};
    }

    std::pair<TStringBuf, TStringBuf> ExtractSubSourceRequestWithId(TStringBuf eventData) {
        std::pair<TStringBuf, TStringBuf> result;
        size_t i = 0;
        for (auto it : StringSplitter(eventData).Split('\t')) {
            switch (i) {
                case 4:
                    result.first = it.Token();
                    break;
                case 5:
                    result.second = it.Token();
                    return result;
            }
            ++i;
        }
        return result;
    }

    template <typename T>
    T TryParse(TStringBuf s, const T& defaultValue) {
        T t;
        if (!TryFromString<T>(s, t)) {
            return defaultValue;
        }
        return t;
    }

    struct TParsedRequestInfo {
        TStringBuf ReqId;
        ui32 DocHandlesCount = 0;
        bool IsSnippet = false;
    };

    TParsedRequestInfo ParseRequestInfo(TStringBuf request) {
        TParsedRequestInfo info;
        for (auto& item : StringSplitter(request.After('?')).Split('&')) {
            if (item.Token().StartsWith(EVLOG_PARAM_DH)) {
                ++info.DocHandlesCount;
            } else if (item.Token().StartsWith(EVLOG_PARAM_REQID)) {
                info.ReqId = item.Token().substr(EVLOG_PARAM_REQID.size());
            } else if (item.Token().StartsWith(EVLOG_PARAM_DF)) {
                info.IsSnippet = IsTrue(item.Token().substr(EVLOG_PARAM_DF.size()));
            }
        }
        return info;
    }

    ui32 CalcThreshold(double sample) {
        return static_cast<ui32>(static_cast<double>(Max<ui32>()) * sample);
    }

    ui32 CalcHash(TStringBuf request) {
        return MurmurHash<ui32>(request.data(), request.size());
    }

    double CalculateAutoSample(NYT::ICypressClient& cypress, const TString& inputTable, ui32 limit) {
        ui64 inputRows = cypress.Get(inputTable + "/@row_count").IntCast<ui64>();
        ui64 needRows = limit * 2000;
        DEBUG_LOG << "input rows = " << inputRows << Endl;
        DEBUG_LOG << "need rows = " << needRows << Endl;
        if (needRows > inputRows) {
            WARNING_LOG << "input table has less than " << needRows << ", fallback to 1.0 sample factor" << Endl;
            return 1.0;
        }
        return static_cast<double>(needRows) / inputRows;
    }

    TVector<std::pair<TString, TString>> ParseMarkers(const NUtils::TParams& params) {
        TVector<std::pair<TString, TString>> result;
        for (auto& markerString : params.GetMarker()) {
            auto pos = markerString.find('/');
            if (pos == TStringBuf::npos) {
                WARNING_LOG << "Skip invalid marker: " << markerString << Endl;
            }
            result.emplace_back(markerString.substr(0, pos), markerString.substr(pos + 1));
            DEBUG_LOG << "Add new marker: " << result.back().first << '/' << result.back().second << Endl;
        }
        return result;
    }

    struct TEventlogParserParams {
        TVector<TString> IncludeFilter;
        TVector<TString> ExcludeFilter;
        TVector<TString> InstanceIncludeFilter;
        TVector<TString> InstanceExcludeFilter;
        TVector<std::pair<TString, TString>> Markers;
        float Sample = 1.0f;
        TInstant MinTimestamp;
        TInstant MaxTimestamp;
        bool CollectTimings = false;
        bool AllowEmptyReqids = false;
        bool SyncRequests = false;

        Y_SAVELOAD_DEFINE(IncludeFilter, ExcludeFilter, Sample, MinTimestamp, MaxTimestamp, Markers, CollectTimings, AllowEmptyReqids, SyncRequests, InstanceIncludeFilter, InstanceExcludeFilter)

        void InitFromProto(const NUtils::TParams& params, ICypressClient& cypress) {
            IncludeFilter.assign(params.GetInclude().begin(), params.GetInclude().end());
            ExcludeFilter.assign(params.GetExclude().begin(), params.GetExclude().end());
            InstanceIncludeFilter.assign(params.GetInstanceInclude().begin(), params.GetInstanceInclude().end());
            InstanceExcludeFilter.assign(params.GetInstanceExclude().begin(), params.GetInstanceExclude().end());

            Markers = ParseMarkers(params);

            Sample = ClampVal<double>(params.GetSample(), 0.0, 1.0);
            if (params.GetAutoSample()) {
                Sample = CalculateAutoSample(cypress, params.GetEventlog(), params.GetLimit());
            }

            MinTimestamp = TInstant::Seconds(params.GetMinTimestamp());
            MaxTimestamp = TInstant::Seconds(params.GetMaxTimestamp());

            CollectTimings = params.GetCollectTimings();
            AllowEmptyReqids = params.GetAllowEmptyReqids();
            SyncRequests = params.GetSyncRequests();

            DEBUG_LOG << "sample = " << Sample << Endl;
            DEBUG_LOG << "request include filter = " << JoinSeq(" ", IncludeFilter) << Endl;
            DEBUG_LOG << "request exclude filter = " << JoinSeq(" ", ExcludeFilter) << Endl;
            DEBUG_LOG << "instance include filter = " << JoinSeq(" ", InstanceIncludeFilter) << Endl;
            DEBUG_LOG << "instance exclude filter = " << JoinSeq(" ", InstanceExcludeFilter) << Endl;
            DEBUG_LOG << "min timestamp = " << ToString(MinTimestamp) << Endl;
            DEBUG_LOG << "max timestamp = " << ToString(MaxTimestamp) << Endl;

            DEBUG_LOG << "CollectTimings = " << params.GetCollectTimings() << Endl;
            DEBUG_LOG << "AllowEmptyReqids = " << params.GetAllowEmptyReqids() << Endl;
            DEBUG_LOG << "SyncRequests = " << params.GetSyncRequests() << Endl;
        }
    };

    struct TReFilter {
        void Init(const TVector<TString>& include, const TVector<TString>& exclude) {
            IncludeRe.clear();
            ExcludeRe.clear();
            for (auto& includeStr : include) {
                IncludeRe.emplace_back(includeStr);
            }
            for (auto& excludeStr : exclude) {
                ExcludeRe.emplace_back(excludeStr);
            }
        }

        bool IsAllowed(const TString& s) const {
            const auto matchString = [&s](const re2::RE2& re) {
                return re2::RE2::PartialMatch(s, re);
            };
            return AllOf(IncludeRe, matchString) && !AnyOf(ExcludeRe, matchString);
        }

    private:
        TList<re2::RE2> IncludeRe;
        TList<re2::RE2> ExcludeRe;
    };

    struct TEventlogRowParser : IMapper<TTableReader<TNode>, TTableWriter<TParsedRow>> {
        TEventlogRowParser() = default;
        TEventlogRowParser(const TEventlogParserParams& params)
            : P(params)
        {
        }

        Y_SAVELOAD_JOB(P)

        void Start(TTableWriter<TParsedRow>*) final {
            RequestFilter.Init(P.IncludeFilter, P.ExcludeFilter);
            InstanceFilter.Init(P.InstanceIncludeFilter, P.InstanceExcludeFilter);
            for (auto& marker : P.Markers) {
                MarkerMatchers.emplace_back(marker.first, marker.second);
            }
        }

        void Do(TTableReader<TNode>* input, TTableWriter<TParsedRow>* output) final {
            auto threshold = CalcThreshold(P.Sample);
            TParsedRow outRow;
            for (; input->IsValid(); input->Next()) {
                const auto& inRow = input->GetRow();
                const auto& eventType = inRow[EVLOG_EVENT_TYPE];
                if (!eventType.IsString()) {
                    continue;
                }
                const auto& eventDataNode = inRow[EVLOG_EVENT_DATA];
                if (!eventDataNode.IsString()) {
                    continue;
                }
                const TInstant timestamp = TInstant::MicroSeconds(inRow[EVLOG_TIMESTAMP].IntCast<ui64>());
                if (P.MinTimestamp != TInstant::Zero() && timestamp < P.MinTimestamp) {
                    continue;
                }
                if (P.MaxTimestamp != TInstant::Zero() && timestamp >= P.MaxTimestamp) {
                    continue;
                }
                if (!InstanceFilter.IsAllowed(inRow[EVLOG_INSTANCE].AsString())) {
                    continue;
                }
                const auto& eventData = eventDataNode.AsString();
                outRow.Clear();
                if (P.CollectTimings && eventType.AsString() == "SubSourceOk"sv) {
                    auto ssrStr = ExtractBySplit(eventData, '\t', 6);
                    if (ssrStr.empty()) {
                        continue;
                    }
                    auto ssrId = FromString<ui64>(ssrStr);
                    outRow.SetInstance(inRow[EVLOG_INSTANCE].AsString());
                    outRow.SetFrameId(inRow[EVLOG_FRAME_ID].IntCast<ui64>());
                    outRow.SetTimestamp(timestamp.MicroSeconds());
                    outRow.SetSubSourceOk(true);
                    outRow.SetSubSourceRequestId(ssrId);
                    output->AddRow(outRow);
                    continue;
                }
                if (eventType.AsString() != "SubSourceRequest"sv) {
                    continue;
                }
                if (!RequestFilter.IsAllowed(eventData)) {
                    continue;
                }
                const auto [request, ssrId] = ExtractSubSourceRequestWithId(eventData);
                if (P.CollectTimings && ssrId.empty()) {
                    continue;
                }
                const auto requestInfo = ParseRequestInfo(request);
                if (!P.AllowEmptyReqids && requestInfo.ReqId.empty()) {
                    continue;
                }
                const auto hash = !P.SyncRequests ? CalcHash(request) : CalcHash(requestInfo.ReqId);
                if (threshold != Max<ui32>() && hash >= threshold) {
                    continue;
                }
                TVector<TString> matchedMarkers;
                for (auto& markerMatcher : MarkerMatchers) {
                    const re2::RE2& re = markerMatcher.second;
                    if (re2::RE2::PartialMatch(eventData, re)) {
                        if (matchedMarkers.empty() || matchedMarkers.back() != markerMatcher.first) {
                            matchedMarkers.push_back(markerMatcher.first);
                        }
                    }
                }
                if (requestInfo.DocHandlesCount > 0 && requestInfo.IsSnippet) {
                    outRow.SetSubSourceRequestType(TParsedRow::SNIPPET);
                } else if (requestInfo.DocHandlesCount > 0) {
                    outRow.SetSubSourceRequestType(TParsedRow::FACTOR);
                } else {
                    outRow.SetSubSourceRequestType(TParsedRow::SEARCH);
                }
                outRow.SetInstance(inRow[EVLOG_INSTANCE].AsString());
                outRow.SetFrameId(inRow[EVLOG_FRAME_ID].IntCast<ui64>());
                outRow.SetTimestamp(timestamp.MicroSeconds());
                outRow.SetSubSourceRequest(TString{request});
                outRow.SetDocHandleCount(requestInfo.DocHandlesCount);
                outRow.SetRequestHash(hash);
                outRow.SetReqId(TString{requestInfo.ReqId});
                outRow.SetMarker(JoinSeq("/", matchedMarkers));
                outRow.SetSubSourceRequestId(TryParse<ui64>(ssrId, 0));
                outRow.SetBasesearchHost(TString{request.Before('?')});
                output->AddRow(outRow);
            }
        }

    private:
        TEventlogParserParams P;
        TReFilter RequestFilter;
        TReFilter InstanceFilter;
        TList<std::pair<TString, re2::RE2>> MarkerMatchers;
    };

    REGISTER_MAPPER(TEventlogRowParser);

    struct TSubSourceRequestTimingsReducer : IReducer<TTableReader<TParsedRow>, TTableWriter<TParsedRow>> {
        void Do(TTableReader<TParsedRow>* input, TTableWriter<TParsedRow>* output) final {
            TMaybe<TParsedRow> mainRow;
            TMaybe<TParsedRow> okRow;
            for (; input->IsValid(); input->Next()) {
                auto& row = input->GetRow();
                if (row.GetSubSourceOk()) {
                    if (!okRow.Defined()) {
                        okRow = row;
                    } else if (okRow->GetTimestamp() < row.GetTimestamp()) {
                        okRow = row;
                    }
                } else {
                    if (!mainRow.Defined()) {
                        mainRow = row;
                    } else if (mainRow->GetTimestamp() > row.GetTimestamp()) {
                        mainRow = row;
                    }
                }
            }
            if (okRow.Defined() && mainRow.Defined()) {
                TDuration duration = TInstant::MicroSeconds(okRow->GetTimestamp()) - TInstant::MicroSeconds(mainRow->GetTimestamp());
                mainRow->SetResponseTime(duration.MicroSeconds());
                output->AddRow(*mainRow);
            }
        }
    };
    REGISTER_REDUCER(TSubSourceRequestTimingsReducer);

    struct TSyncRequestsReducer : IReducer<TTableReader<TParsedRow>, TTableWriter<TParsedRow>> {
        void Do(TTableReader<TParsedRow>* input, TTableWriter<TParsedRow>* output) final {
            TMaybe<TParsedRow> searchRequest;
            TMaybe<TParsedRow> factorRequest;
            TMaybe<TParsedRow> snippetRequest;
            for (; input->IsValid(); input->Next()) {
                auto& row = input->GetRow();
                if (row.GetSubSourceRequestType() == TParsedRow::SEARCH) {
                    if (!searchRequest.Defined()) {
                        searchRequest = row;
                    }
                } else if (row.GetSubSourceRequestType() == TParsedRow::FACTOR) {
                    if (!factorRequest.Defined() || factorRequest->GetDocHandleCount() < row.GetDocHandleCount()) {
                        factorRequest = row;
                    }
                } else if (row.GetSubSourceRequestType() == TParsedRow::SNIPPET) {
                    if (!snippetRequest.Defined() || snippetRequest->GetDocHandleCount() < row.GetDocHandleCount()) {
                        snippetRequest = row;
                    }
                }
            }
            if (searchRequest.Defined() && factorRequest.Defined() && snippetRequest.Defined()) {
                output->AddRow(*searchRequest);
                output->AddRow(*factorRequest);
                output->AddRow(*snippetRequest);
            }
        }
    };
    REGISTER_REDUCER(TSyncRequestsReducer);

    template <typename TRow>
    TString CreateTempTable(ICypressClient& cypress, TStringBuf parentPath, size_t attempts = 5) {
        bool created = false;
        for (size_t i = 0; !created && i < attempts; ++i) {
            const TString path = TString::Join(parentPath, '/', CreateGuidAsString());
            try {
                cypress.CreateTable<TRow>(path, TSortColumns{}, TCreateOptions().Recursive(true));
                DEBUG_LOG << "Create temporary table " << path << Endl;
                return path;
            } catch (...) {
                ERROR_LOG << "Failed to create a temporary table " << path << ". Exception: " << CurrentExceptionMessage() << Endl;
                Sleep(TDuration::Seconds(i + 1));
            }
        }
        ythrow yexception() << "Cannot create temporary table with prefix " << parentPath;
    }

    template <typename TRow>
    struct TTempTable {
        TTempTable(ICypressClient& cypress, TStringBuf parentPath)
            : Cypress(cypress)
            , Path(CreateTempTable<TRow>(cypress, parentPath))
        {
        }

        const TString& GetPath() const {
            return Path;
        }

        ~TTempTable() {
            try {
                Cypress.Remove(Path, TRemoveOptions().Recursive(true));
                DEBUG_LOG << "Removed temporary table " << Path << Endl;
            } catch (...) {
                ERROR_LOG << "Failed to remove a temporary table " << Path << ". Exception: " << CurrentExceptionMessage() << Endl;
            }
        }

    private:
        ICypressClient& Cypress;
        const TString Path;
    };

    void WriteRequestParams(IOutputStream& output, TStringBuf reqParams) {
        output.Write('?');
        output.Write(reqParams);
        output.Write('\n');
    }

    void ReadTableRangeToFile(TStringBuf title, IClientBase& yt, const TString& resultTable, const TString& filePath, ui32 limit, const NYT::TReadRange& range) {
        if (filePath.empty()) {
            return;
        }
        DEBUG_LOG << "Dumping " << title << " to " << filePath << Endl;
        TFileOutput output(filePath);

        NYT::TRichYPath path;
        path.Path(resultTable);
        path.AddRange(range);
        auto reader = yt.CreateTableReader<TParsedRow>(path);

        ui32 count = 0;
        for (; count < limit && reader->IsValid(); reader->Next()) {
            auto& row = reader->GetRow();
            auto reqParams = ExtractBySplit(row.GetSubSourceRequest(), '?', 1);
            if (!reqParams.empty()) {
                WriteRequestParams(output, reqParams);
                ++count;
            }
        }
        if (count < limit) {
            WARNING_LOG << "Number of lines written(" << count << ") to " << filePath << " is less than the limit(" << limit << ")" << Endl;
        }
        DEBUG_LOG << "Finished dumping " << title << Endl;
    }

    void DumpResultsToFiles(IClientBase& yt, const TString& resultTable, const NUtils::TParams& params) {
        ReadTableRangeToFile("factor queries", yt, resultTable, params.GetFactorQueries(), params.GetLimit(),
            TReadRange().LowerLimit(TReadLimit().Key("FACTOR")).UpperLimit(TReadLimit().Key("SEARCH")));

        ReadTableRangeToFile("search queries", yt, resultTable, params.GetSearchQueries(), params.GetLimit(),
            TReadRange().LowerLimit(TReadLimit().Key("SEARCH")).UpperLimit(TReadLimit().Key("SNIPPET")));

        ReadTableRangeToFile("snippet queries", yt, resultTable, params.GetSnippetQueries(), params.GetLimit(),
            TReadRange().LowerLimit(TReadLimit().Key("SNIPPET")));
    }

    THolder<IOutputStream> CreateFileOrNull(const TString& path) {
        if (path.empty()) {
            return MakeHolder<TNullOutput>();
        }
        return MakeHolder<TFileOutput>(path);
    }

    void DumpSyncRequestsToFiles(IClientBase& yt, const TString& resultTable, const NUtils::TParams& params) {
        auto searchOutput = CreateFileOrNull(params.GetSearchQueries());
        auto factorOutput = CreateFileOrNull(params.GetFactorQueries());
        auto snippetOutput = CreateFileOrNull(params.GetSnippetQueries());
        auto reader = yt.CreateTableReader<TParsedRow>(resultTable);
        ui32 count = 0;
        for (; count < params.GetLimit() && reader->IsValid(); reader->Next()) {
            auto searchReq = reader->GetRow();
            Y_ENSURE(searchReq.GetSubSourceRequestType() == TParsedRow::SEARCH);
            reader->Next();
            if (!reader->IsValid()) {
                break;
            }
            auto factorReq = reader->GetRow();
            Y_ENSURE(factorReq.GetSubSourceRequestType() == TParsedRow::FACTOR);
            reader->Next();
            if (!reader->IsValid()) {
                break;
            }
            auto snippetReq = reader->GetRow();
            Y_ENSURE(snippetReq.GetSubSourceRequestType() == TParsedRow::SNIPPET);

            auto searchParams = ExtractBySplit(searchReq.GetSubSourceRequest(), '?', 1);
            auto factorParams = ExtractBySplit(factorReq.GetSubSourceRequest(), '?', 1);
            auto snippetParams = ExtractBySplit(snippetReq.GetSubSourceRequest(), '?', 1);
            if (!searchParams.empty() && !factorParams.empty() && !snippetParams.empty()) {
                WriteRequestParams(*searchOutput, searchParams);
                WriteRequestParams(*factorOutput, factorParams);
                WriteRequestParams(*snippetOutput, snippetParams);
                ++count;
            }
        }
    }
}

int main(int argc, const char* argv[]) {
    NYT::Initialize(argc, argv);

    NGetoptPb::TGetoptPbSettings getoptSettings;
    getoptSettings.DumpConfig = false;
    const NUtils::TParams params = NGetoptPb::GetoptPbOrAbort(argc, argv, getoptSettings);
    if (params.GetVerbose()) {
        InitGlobalLog2Console(TLOG_DEBUG);
    }
    try {
        Y_ENSURE(!params.GetAllowEmptyReqids() || !params.GetSyncRequests(), "--allow-empty-reqids and --sync-requests cannot by used at the same time");

        DEBUG_LOG << "Create YT client..." << Endl;
        auto yt = NYT::CreateClient(params.GetYt());
        auto tx = yt->StartTransaction();
        DEBUG_LOG << "Started transaction " << GetGuidAsString(tx->GetId()) << Endl;

        {
            TMaybe<TTempTable<TParsedRow>> tempTable;
            const TString* outputPath = nullptr;
            if (!params.GetSaveYtOutput().empty()) {
                tx->CreateTable<TParsedRow>(params.GetSaveYtOutput(), {}, NYT::TCreateOptions().Force(true).Recursive(true));
                outputPath = &params.GetSaveYtOutput();
            } else {
                tempTable.ConstructInPlace(*tx, "//tmp");
                outputPath = &tempTable->GetPath();
            }

            if (params.GetLimit() > 0) {
                DEBUG_LOG << "output limit = " << params.GetLimit() << Endl;
            } else {
                DEBUG_LOG << "output limit is not set, results will be saved to a YT table only" << Endl;
            }

            TEventlogParserParams parserParams;
            parserParams.InitFromProto(params, *tx);

            DEBUG_LOG << "Run TEventlogRowParser" << Endl;

            if (params.GetSyncRequests()) {
                NYT::TMapReduceOperationSpec mrSpec;
                mrSpec.AddInput<NYT::TNode>(params.GetEventlog());
                mrSpec.AddOutput<TParsedRow>(*outputPath);
                mrSpec.ReduceBy({"ReqId", "BasesearchHost"});
                auto parser = MakeIntrusive<TEventlogRowParser>(parserParams);
                tx->MapReduce(mrSpec, parser, new TSyncRequestsReducer);
            } else if (!params.GetCollectTimings()) {
                NYT::TMapOperationSpec mapSpec;
                mapSpec.AddInput<NYT::TNode>(params.GetEventlog());
                mapSpec.AddOutput<TParsedRow>(*outputPath);
                auto parser = MakeIntrusive<TEventlogRowParser>(parserParams);
                tx->Map(mapSpec, parser);
            } else {
                NYT::TMapReduceOperationSpec mrSpec;
                mrSpec.AddInput<NYT::TNode>(params.GetEventlog());
                mrSpec.AddOutput<TParsedRow>(*outputPath);
                mrSpec.ReduceBy({"Instance", "FrameId", "SubSourceRequestId"});
                auto parser = MakeIntrusive<TEventlogRowParser>(parserParams);
                tx->MapReduce(mrSpec, parser, new TSubSourceRequestTimingsReducer);
            }

            bool needToDumpResultsToFiles =
                params.GetLimit() > 0 && (
                    !params.GetSearchQueries().empty() ||
                    !params.GetFactorQueries().empty() ||
                    !params.GetSnippetQueries().empty()
                );
            if (!needToDumpResultsToFiles) {
                DEBUG_LOG << "Skip dumping queries to files" << Endl;
            } else if (params.GetSyncRequests()) {
                DumpSyncRequestsToFiles(*tx, *outputPath, params);
            } else {
                NYT::TSortOperationSpec sortSpec;
                sortSpec.AddInput(*outputPath);
                sortSpec.Output(*outputPath);
                sortSpec.SortBy({"SubSourceRequestType", "RequestHash"});
                DEBUG_LOG << "Run sort" << Endl;
                tx->Sort(sortSpec);
                DEBUG_LOG << "Sort finished" << Endl;

                auto outputSize = tx->Get(*outputPath + "/@row_count").IntCast<ui64>();
                DEBUG_LOG << "Saved " << outputSize << " requests from the input eventlog." << Endl;

                DumpResultsToFiles(*tx, *outputPath, params);
            }
        }

        tx->Commit();
        tx.Drop();

        DEBUG_LOG << "Transaction committed." << Endl;
        return 0;

    } catch (...) {
        ERROR_LOG << CurrentExceptionMessage() << Endl;
        return 1;
    }
}
