#include "get_error_sessions.h"
#include "log_parsers.h"
#include "file_manager.h"
#include "aux_utils.h"

#include <util/generic/string.h>
#include <util/stream/output.h>
#include <util/string/strip.h>
#include <util/string/builder.h>
#include <util/system/file.h>

namespace NH2Log {

    namespace {
        TString GetFileName(const TError& err, TStringBuf fileTypeInfix) {
            Y_ENSURE(err.Code && err.Type != EErrorType::None && err.Source != EErrorSource::None);
            return TStringBuilder() << err << '.' << fileTypeInfix;
        }

        TError ParseError(TStringBuf fname) {
            TError res;
            res.Source = FromString<EErrorSource>(fname.NextTok('-'));
            res.Type = FromString<EErrorType>(fname.NextTok('-'));
            res.Code = fname.Before('.');
            return res;
        }

        TString GetErrorAddressFileName(const TError& err) {
            return GetFileName(err, "addr");
        }

        TString GetStatsFileName() {
            return "LogStats";
        }

        struct TStats {
            TString FirstTstamp;
            TString FinalTstamp;
            ui64 TotalH1Sessions = 0;
            ui64 TotalH1BrokenSessions = 0;
            ui64 TotalH2Sessions = 0;
            ui64 TotalH2BrokenSessions = 0;
            ui64 TotalStreams = 0;
            ui64 TotalBrokenStreams = 0;
        };

        void PrintStats(const TStats& stats, IOutputStream& out) {
#define Y_H2_LOG_PRINT_FIELD(stats, field, out) out << #field << ": " << stats.field << Endl;
            Y_H2_LOG_PRINT_FIELD(stats, FirstTstamp, out);
            Y_H2_LOG_PRINT_FIELD(stats, FinalTstamp, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalH1Sessions, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalH1BrokenSessions, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalH2Sessions, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalH2BrokenSessions, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalStreams, out);
            Y_H2_LOG_PRINT_FIELD(stats, TotalBrokenStreams, out);
        }
    }


    TString GetErrorSessionFileName(const TError& err) {
        return GetFileName(err, "sess");
    }

    struct TErrorSession : public TSession {
        TSet<ui32> CancelledStreams;
    };

    TSet<TError> GetErrorSessions(TFileManager& fileManager) {
        Y_H2_LOG_FUNC(fileManager);

        TStats stats;
        TSet<TError> result;
        THashMap<TString, TErrorSession> active;
        TString line;

        if (fileManager.HasDoneTag(__func__)) {
            Y_H2_LOG_EVENT << "Found done tag: " << fileManager.GetDoneTag(__func__) << Endl;
            for (const auto& file : fileManager.ListFiles("*-*-*.sess")) {
                Y_H2_LOG_EVENT << "Found result: " << file << Endl;
                result.insert(ParseError(file));
            }
            return result;
        }

        TErrorSession* sess = nullptr;

        // TODO (velavokr): add tag checking
        while (fileManager.GetInput().ReadLine(line)) {
            if (!(sess = ParseNextLogLine(sess, active, line))) {
                continue;
            }

            auto& item = sess->Log.back();

            if (!stats.FirstTstamp) {
                stats.FirstTstamp = item.TimeRaw;
            }

            stats.FinalTstamp = item.TimeRaw;

            if (IsH1SessionStart(item)) {
                stats.TotalH1Sessions += 1;
            } else if (IsH1SessionEnd(item) && item.Event.IsUnwinding) {
                stats.TotalH1BrokenSessions += 1;
            } else if (IsStreamOpen(item)) {
                stats.TotalStreams += 1;
            } else if (IsStreamFinish(item) && !IsFinalStateClosed(item)) {
                stats.TotalBrokenStreams += 1;
            } else if (IsErrorMessage(item)) {
                auto err = ParseError(item);
                Y_ENSURE_EX(err, yexception() << item.RawPrefix << '\t' << item.Event.RawEvent);
                if (err->Type == EErrorType::RST_STREAM && err->Source == EErrorSource::Recv) {
                    if (err->Code == "CANCEL") {
                        sess->CancelledStreams.insert(err->StreamId);
                    } else if (err->Code == "PROTOCOL_ERROR" && sess->CancelledStreams.contains(err->StreamId)) {
                        err->Benigh = true;
                    }
                }
                if (!err->Benigh) {
                    sess->Errors.insert(*err);
                }
            } else if (IsH2SessionStart(item)) {
                stats.TotalH2Sessions += 1;
            } else if (IsH2SessionEnd(item)) {
                if (item.Event.IsUnwinding) {
                    stats.TotalH2BrokenSessions += 1;
                }
                for (const auto& err : sess->Errors) {
                    result.insert(err);
                    fileManager.GetFileOutput(GetErrorAddressFileName(err)) << item.Addr << '\n';
                    DumpSession(sess->Log, fileManager.GetFileOutput(GetErrorSessionFileName(err)));
                }
                active.erase(item.Addr);
            }
        }

        PrintStats(stats, fileManager.GetFileOutput(GetStatsFileName()));
        fileManager.CloseOutput();
        fileManager.SetDoneTag(__func__);

        return result;
    }
}
