#include "summarize_streams.h"
#include "log_parsers.h"
#include "aux_utils.h"

#include <util/string/builder.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;
        }

        struct TErrorInfo {
            TString Data;
            EContType ContType = EContType::Unknown;
        };

        struct TTimingInfo {
            TInstant Start;
            TInstant End;

            TInstant NextStart;
            TInstant NextEnd;

            bool Healthy = true;

            TDuration Duration() const {
                return Healthy && Start && End && End > Start ? End - Start : TDuration();
            }

            void SelectMaxDuration() {
                TTimingInfo next;
                next.Start = NextStart;
                next.End = NextEnd;
                if (next.Duration() > Duration()) {
                    *this = next;
                }
            }
        };

        struct TStreamHealthySession : public TSession {
            TMap<ui32, TTimingInfo> RunTiming;
            TMaybe<TString> UserAgent;
        };

        struct TStreamInfo {
            TMaybe<TReqInfo> ReqInfo;
            TTimingInfo Time;
            TMaybe<TErrorInfo> Exception;
            TMaybe<TString> ReqContentLength;
            TMaybe<TString> ReqRecvLength;
            TMaybe<TString> RespContentLength;
        };

        struct TStreamErrorSession : public TSession {
            THashMap<ui32, TStreamInfo> Streams;
            TMap<TError, TSet<ui32>> StreamErrors;
            TMaybe<TString> UserAgent;
        };

        void PrintHealthyInfoSummary(IOutputStream& out, TStringBuf addr, ui32 streamId, const TTimingInfo& summary, bool humanReadable) {
            if (humanReadable) {
                out << addr << "\tStreamId=" << streamId
                    << "\tRunTime=" << summary.Duration().MilliSeconds() << "ms"
                    << "\tStartTime=" << summary.Start.MilliSeconds() << "ms"
                    << "\tEndTime=" << summary.End.MilliSeconds() << "ms\n";
            } else {
                out << addr << "\t" << streamId
                    << "\t" << summary.Duration().MilliSeconds()
                    << "\t" << summary.Start.MilliSeconds()
                    << "\t" << summary.End.MilliSeconds() << "\n";
            }
        }
    }

    void SummarizeErrorStreams(TFileManager& fileManager) {
        Y_H2_LOG_FUNC(fileManager);
        THashMap<TString, TStreamErrorSession> active;
        TStreamErrorSession* sess = nullptr;
        TString line;
        while (fileManager.GetInput().ReadLine(line)) {
            if (!(sess = ParseNextLogLine(sess, active, line))) {
                continue;
            }

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

            if (IsStreamOpen(item)) {
                auto& streamInfo = sess->Streams[*ParseStreamId(item)];

                if (!sess->UserAgent) {
                    sess->UserAgent = ParseUserAgent(item);
                }

                streamInfo.ReqInfo = ParseReqInfo(item);
                streamInfo.Time.Start = item.TimeParsed;
            } else if (IsRespHeaders(item)) {
                auto streamId = ParseStreamId(item);
                if (streamId && sess->Streams.contains(*streamId)) {
                    auto& streamInfo = sess->Streams[*ParseStreamId(item)];
                    streamInfo.RespContentLength = ParseRespContentLength(item);
                }
            } else if (IsStreamFinish(item)) {
                auto streamId = ParseStreamId(item);
                if (streamId && sess->Streams.contains(*streamId)) {
                    auto& streamInfo = sess->Streams[*ParseStreamId(item)];
                    streamInfo.ReqContentLength = ParseReqContentLength(item);
                    streamInfo.ReqRecvLength = ParseReqRecvLength(item);
                }
            } else if (IsRecvRstStream(item) || IsSendRstStream(item)) {
                auto streamId = ParseStreamId(item);
                Y_ENSURE_EX(streamId, yexception() << item.RawPrefix << '\t' << item.Event.RawEvent);
                sess->StreamErrors[*ParseError(item)].insert(*streamId);
            } else if (IsException(item) || IsCatch(item)) {
                auto streamId = ParseStreamId(item);
                if (streamId && sess->Streams.contains(*streamId)) {
                    auto& streamInfo = sess->Streams[*streamId];
                    streamInfo.Exception = {item.Event.RawEvent, item.Cont.ContType};
                    streamInfo.Time.End = item.TimeParsed;
                }
            } else if (IsH2SessionEnd(item)) {
                for (const auto& error : sess->StreamErrors) {
                    {
                        auto& streamsOut = fileManager.GetFileOutput(GetFileName(error.first, "streams"));
                        for (const auto& past : sess->Log) {
                            auto streamId = ParseStreamId(past);
                            if (streamId && !error.second.contains(*streamId)) {
                                continue;
                            }
                            streamsOut << past.RawPrefix << '\t' << past.Event.RawEvent << '\n';
                            for (const auto& trace : past.Trace) {
                                streamsOut << trace << '\n';
                            }
                        }
                    }
                    {
                        auto& summaryOut = fileManager.GetFileOutput(GetFileName(error.first, "streams.summary"));
                        summaryOut << item.Addr << "\tuser-agent\t" << sess->UserAgent << "\n";

                        for (auto streamId : error.second) {
                            summaryOut << item.Addr << '\t' << streamId << '\t' << error.first << '\n';
                            if (sess->Streams.contains(streamId)) {
                                const auto& streamInfo = sess->Streams.at(streamId);
                                if (const auto& reqInfo = streamInfo.ReqInfo) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << '\t' << reqInfo->ToString() << '\n';
                                }

                                if (const auto& exc = streamInfo.Exception) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << "\tException\t" << exc->Data << '\n';
                                }

                                if (streamInfo.Time.Duration()) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << "\tDuration\t" << streamInfo.Time.Duration() << '\n';
                                }

                                if (streamInfo.ReqContentLength) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << "\tReq-Content-Length\t" << streamInfo.ReqContentLength << '\n';
                                }

                                if (streamInfo.ReqRecvLength) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << "\tReq-Recv-Length\t" << streamInfo.ReqRecvLength << '\n';
                                }

                                if (streamInfo.RespContentLength) {
                                    summaryOut << item.Addr << '\t' << streamId
                                        << "\tResp-Content-Length\t" << streamInfo.RespContentLength << '\n';
                                }
                            }
                        }
                    }
                }
                active.erase(item.Addr);
            }
        }
    }

    void SummarizeSlowStreams(TFileManager& fileManager, TDuration minDuration, bool humanReadable) {
        Y_H2_LOG_FUNC(fileManager);
        THashMap<TString, TStreamHealthySession> active;
        TStreamHealthySession* sess = nullptr;
        TString line;
        while (fileManager.GetInput().ReadLine(line)) {
            if (!(sess = ParseNextLogLine(sess, active, line))) {
                continue;
            }

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

            if (IsStreamRunStart(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    sess->RunTiming[*streamId].Start = item.TimeParsed;
                }
            } else if (IsStreamRunEnd(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    sess->RunTiming[*streamId].End = item.TimeParsed;
                }
            } else if (IsRecvRstStream(item) || IsSendRstStream(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    sess->RunTiming[*streamId].Healthy = false;
                }
            } else if (IsH2SessionEnd(item)) {
                auto& summaryOut = fileManager.GetFileOutput("slow.streams.summary");
                bool hasSlow = false;
                for (const auto& stream : sess->RunTiming) {
                    if (stream.second.Duration() > minDuration) {
                        hasSlow = true;
                        PrintHealthyInfoSummary(summaryOut, item.Addr, stream.first, stream.second, humanReadable);
                    }
                }
                if (hasSlow) {
                    auto& streamsOut = fileManager.GetFileOutput("slow.streams");
                    DumpSession(sess->Log, streamsOut);
                }
                active.erase(item.Addr);
            }
        }
    }

    void SummarizeSlowStreamSends(TFileManager& fileManager, TDuration minDuration, bool humanReadable) {
        Y_H2_LOG_FUNC(fileManager);
        THashMap<TString, TStreamHealthySession> active;
        TStreamHealthySession* sess = nullptr;
        TString line;
        while (fileManager.GetInput().ReadLine(line)) {
            if (!(sess = ParseNextLogLine(sess, active, line))) {
                continue;
            }

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

            if (IsStreamSendStart(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    sess->RunTiming[*streamId].NextStart = item.TimeParsed;
                }
            } else if (IsStreamSendEnd(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    auto& timing = sess->RunTiming[*streamId];
                    timing.NextEnd = item.TimeParsed;
                    timing.SelectMaxDuration();
                }
            } else if (IsRecvRstStream(item) || IsSendRstStream(item)) {
                if (auto streamId = ParseStreamId(item)) {
                    sess->RunTiming[*streamId].Healthy = false;
                }
            } else if (IsH2SessionEnd(item)) {
                auto& summaryOut = fileManager.GetFileOutput("slow.stream.sends.summary");
                bool hasSlow = false;
                for (const auto& stream : sess->RunTiming) {
                    if (stream.second.Duration() > minDuration) {
                        hasSlow = true;
                        PrintHealthyInfoSummary(summaryOut, item.Addr, stream.first, stream.second, humanReadable);
                    }
                }
                if (hasSlow) {
                    auto& streamsOut = fileManager.GetFileOutput("slow.stream.sends");
                    DumpSession(sess->Log, streamsOut);
                }
                active.erase(item.Addr);
            }
        }
    }
}
