#include "common.h"
#include "reqans_log.h"
#include "fields.h"

#include <saas/library/report_builder/abstract.h>
#include <saas/factor_slices/factors_info.h>
#include <saas/protos/logging.pb.h>
#include <saas/util/json/json.h>
#include <saas/searchproxy/search_meta/context.h>

#include <search/session/web_reqans_logger.h>
#include <search/session/reqenv.h>

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

#include <util/stream/zlib.h>

using namespace NSearchProxy::NLogging;
using namespace NJson;

class TMetaReqansLogFrame: public TWebReqansLogFrame {
public:
    TMetaReqansLogFrame() { }
    TMetaReqansLogFrame(ISearchContext* searchContext, EFactorsAggregationStrategy aggregationStrategy, TReqansLogResult* logResult)
        : LogResult(logResult)
    {
        TReqEnv* reqenv = dynamic_cast<TReqEnv*>(searchContext->ReqEnv());
        if (reqenv == nullptr) {
            return;
        }

        const TSearcherPropsRef propertiesRef = reqenv->GetProperties();
        const TString configFactors { propertiesRef->GetOr("rty_factors") };
        if (!configFactors.empty()) {
            if (ProcessRtyConfigFactorsProp(configFactors, aggregationStrategy)) {
                for (const auto& factorName: LoggedFactors) {
                    Record.AddFactorNames(factorName);
                }
            }
        }
    }

    void FillSearchContextFields(ISearchContext* sc) override {
        TReqEnv* reqenv = dynamic_cast<TReqEnv*>(sc->ReqEnv());
        ICluster* cluster = sc->Cluster();

        if (!reqenv)
            return;

        const TSearcherPropsRef propertiesRef = reqenv->GetProperties();
        if (!!propertiesRef) {
            const auto& properties = *propertiesRef;
            TString searchProps;
            for (const auto& prop : properties) {
                const TString& name = prop.first;
                if (name.EndsWith(".nodump") || name.EndsWith(".debug"))
                    continue;
                if (!!searchProps)
                    searchProps.append(',');
                searchProps.append(name);
                searchProps.append('=');
                searchProps.append(prop.second);
            }
            Record.SetSearchProps(searchProps);
        }

        if (cluster) {
            if (reqenv->RP().GroupingParams.size()) {
                FillDocuments(reqenv, cluster, reqenv->RP().GroupingParams[0]);
            }
        }
    }

private:
    void FillDocumentProps(NEvClass::ReqansRecord::Doc* doc, const IArchiveDocInfo* aa) override {
        doc->ClearRegion();
        const int markersCount = aa->DocPropertyCount(DP_MARKERS);
        TString propertyValue;
        for (int i = 0; i < markersCount; ++i) {
            aa->ReadDocProperty(DP_MARKERS, i, &propertyValue);
            doc->AddMarkers(propertyValue);
        }

        if (IsRelevConfFactorLogging()) {
            LogBasedOnRelevConf(doc, aa);
        } else {
            LogBasedOnJsonFactors(doc, aa);
        }
    }

    bool ProcessRtyConfigFactorsProp(const TString& propValue, EFactorsAggregationStrategy aggregationStrategy) {
        try {
            const TJsonValue configs = NUtil::JsonFromString(propValue);
            const auto& configsMap = configs.GetMapSafe();
            Y_ENSURE(!configsMap.empty());
            if (configsMap.size() > 1) {
                OnLoggingIncosistency();
            }

            THashSet<TString> resultFactors;
            for (const auto& [version, factors]: configsMap) {
                const TJsonValue factorsJson = NUtil::JsonFromString(factors.GetString());
                const auto& factorToIndex = factorsJson.GetMapSafe();

                if (resultFactors.empty()) {
                    for (const auto& [factorName, _]: factorToIndex) {
                        resultFactors.insert(factorName);
                    }
                } else {
                    switch (aggregationStrategy)
                    {
                    case EFactorsAggregationStrategy::Intersect:
                    {
                        for (auto it = resultFactors.begin(); it != resultFactors.end(); ) {
                            if (!factorToIndex.contains(*it)) {
                                resultFactors.erase(it++);
                            } else {
                                it++;
                            }
                        }
                    }
                    break;

                    case EFactorsAggregationStrategy::Union:
                    {
                        for (const auto& [factorName, _]: factorToIndex) {
                            resultFactors.insert(factorName);
                        }
                    }
                    break;

                    default:
                        Y_ASSERT(false);
                    }
                }

                THashMap<TString, ui32> resFactorToIndex;
                for (const auto& [factorName, index]: factorToIndex) {
                    resFactorToIndex[factorName] = static_cast<ui32>(index.GetIntegerSafe());
                }
                ConfigVersionToFactors[version] = std::move(resFactorToIndex);
            }
            LoggedFactors.assign(resultFactors.begin(), resultFactors.end());
        } catch (...) {
            OnLoggingError();
            DEBUG_LOG << "Failed to parse rty config factors: " << propValue << Endl;
            return false;
        }
        return true;
    }

    void LogBasedOnJsonFactors(NEvClass::ReqansRecord::Doc* doc, const IArchiveDocInfo* aa) {
        TString jsonFactorsString;
        if (!aa->ReadDocProperty(RelevFactorsName.data(), 0, &jsonFactorsString)) {
            return;
        }

        bool factorNamesFilled = Record.FactorNamesSize();
        try {
            const TJsonValue& factors = NUtil::JsonFromString(jsonFactorsString);
            const TJsonValue::TArray& arr = factors.GetArray();
            for (TJsonValue::TArray::const_iterator i = arr.begin(); i != arr.end(); ++i) {
                const TJsonValue::TMapType& factor = NUtil::GetMap(*i);
                if (factor.size() != 1)
                    ythrow yexception() << "incorrect factor json description " << NUtil::JsonToString(factors);
                const TString& name = factor.begin()->first;
                const float value = factor.begin()->second.GetDoubleRobust();
                doc->AddFactorValues(value);
                if (!factorNamesFilled)
                    Record.AddFactorNames(name);
            }
        } catch (const yexception& e) {
            DEBUG_LOG << "failed to process reqans factors based on json, url: " << doc->GetUrl() << " error: " << e.what() << Endl;
        }
    }

    void LogBasedOnRelevConf(NEvClass::ReqansRecord::Doc* doc, const IArchiveDocInfo* aa) {
        TString configVersion;
        if (!aa->ReadDocProperty(RelevConfigVersion.data(), 0, &configVersion)) {
            return;
        }

        try {
            const auto& configFactorsIter = ConfigVersionToFactors.find(configVersion);
            Y_ENSURE(configFactorsIter != ConfigVersionToFactors.end());
            const auto& factorToIndex = configFactorsIter->second;

            TVector<float> allFactors(NSaasFactors::GetSaasFactorsInfo()->GetFactorCount(), 0.0f);
            const size_t actualFactorsCount = aa->GetAllFactors(allFactors.data(), allFactors.size());

            for (const auto& factorName: LoggedFactors) {
                auto it = factorToIndex.find(factorName);
                if (it == factorToIndex.end()) {
                    continue;
                }
                Y_ENSURE(it->second < actualFactorsCount, "factor index value exceeds the saas bounds");
                doc->AddFactorValues(allFactors[it->second]);
            }
        } catch (const yexception& e) {
            OnLoggingError();
            DEBUG_LOG << "failed to process reqans factors based on relev conf, url: " << doc->GetUrl() << " error: " << e.what() << Endl;
        }
    }

    bool IsRelevConfFactorLogging() const {
        return !LoggedFactors.empty();
    }

    void OnLoggingError() {
        if (LogResult != nullptr) {
            LogResult->Error = true;
        }
    }

    void OnLoggingIncosistency() {
        if (LogResult != nullptr) {
            LogResult->Incosistency = true;
        }
    }

private:
    TReqansLogResult* LogResult {nullptr};
    TVector<TString> LoggedFactors;
    THashMap<TString, THashMap<TString, ui32> > ConfigVersionToFactors;
};

class TProxyReqansLogFrame {
private:
    class TScanner: public ICustomReportBuilder::IScanner {
    public:
        TScanner(NSaas::NProto::TReqAnsRecord& record)
            : Record(record)
        {
        }

        void operator()(const NMetaProtocol::TDocument& doc, const TString& grouping, const TString& category) override {
            auto d = Record.AddDocument();
            if (!d) {
                return;
            }

            d->SetUrl(doc.GetUrl());
            if (grouping) {
                d->SetGrouping(grouping);
            }
            if (category) {
                d->SetCategory(category);
            }
            if (auto properties = d->MutableProperties()) {
                properties->MergeFrom(doc.GetFirstStageAttribute());
                properties->MergeFrom(doc.GetArchiveInfo().GetGtaRelatedAttribute());
            }
        }
        void operator()(const TString& key, const TString& value) override {
            auto p = Record.AddMeta();
            if (!p) {
                return;
            }

            p->SetKey(key);
            p->SetValue(value);
        }

    private:
        NSaas::NProto::TReqAnsRecord& Record;
    };

public:
    const NSaas::NProto::TReqAnsRecord& GetRecord() const {
        return Record;
    }
    NSaas::NProto::TReqAnsRecord& MutableRecord() {
        return Record;
    }

    void FillSearchContextFields(const ICustomReportBuilder& report) {
        TScanner scanner(Record);
        report.ScanReport(scanner);
    }
    void SetErrorCode(i32 errorCode) {
        Record.SetErrorCode(errorCode);
    }

private:
    NSaas::NProto::TReqAnsRecord Record;
};

TString SerializeEvent(const NEvClass::ReqansRecord& ev) {
    return Base64Encode(ev.SerializeAsString());
}

TString SerializeEvent(const NSaas::NProto::TReqAnsRecord& ev) {
    TStringStream ss;
    TZLibCompress zlib(&ss, ZLib::ZLib);
    Y_ENSURE(ev.SerializeToArcadiaStream(&zlib), "cannot serialize NSaas::NProto::TReqAnsRecord to zlib stream");
    zlib.Finish();
    return Base64Encode(ss.Str());
}

template <class TReqansLogFrame, class TContext>
void ReqAnsLog(ui32 id, const TContext& context, const TCgiParameters& cgi, const TSearchRequestData& rd,
    TInstant requestBeginTime, ui32 docCount, const std::function<TReqansLogFrame ()> logFrameFactory = std::function<TReqansLogFrame ()>()
) {
    if (!TLoggerOperator<TReqAnsLog>::Usage())
        return;
    NUtil::TTSKVRecord record(TLoggerOperator<TReqAnsLog>::Get()->GetName());

    record.Add(IdLable, id);
    record.Add(StatTimestampLable, requestBeginTime.Seconds());
    record.Add(UnixTimestampLable, requestBeginTime.Seconds());
    InsertClientIp(record, rd);
    InsertQueryInfo(record, cgi, rd);
    record.Add(HowLable, cgi.Get("how"));
    const TString separator = ";";
    record.Add(RelevLable, EscapeStat(GetFullCgiParameter(cgi, "relev", separator.data())));
    record.Add(RearrLable, EscapeStat(GetFullCgiParameter(cgi, "rearr", separator.data())));
    record.Add(PronLable, EscapeStat(GetFullCgiParameter(cgi, "pron", separator.data())));
    record.ForceAdd(DocCountLable, docCount);

    TReqansLogFrame reqansLogFrame = !!logFrameFactory ? logFrameFactory() : TReqansLogFrame();
    reqansLogFrame.SetErrorCode(yxOK);
    reqansLogFrame.FillSearchContextFields(context);
    auto& proto = reqansLogFrame.MutableRecord();
    proto.SetQuery(GetUserRequest(cgi));
    proto.SetTimestamp(requestBeginTime.TimeT());
    record.Add(ReqAnsProtoLable, SerializeEvent(reqansLogFrame.GetRecord()));

    TLoggerOperator<TReqAnsLog>::Log() << record.ToString() << Endl;
}

TReqansLogResult TReqAnsLog::Log(ui32 id, ISearchContext* searchContext, EFactorsAggregationStrategy logsAggregation) {
    TReqEnv* reqEnv = static_cast<TReqEnv*>(searchContext->ReqEnv());
    const TCgiParameters& cgi = reqEnv->CgiParam;
    const TSearchRequestData& rd = *reqEnv->RequestData;
    const TInstant requestBeginTime = TInstant::MicroSeconds(reqEnv->RequestBeginTime());
    const ui32 docCount = searchContext->Cluster()->TotalDocCount(0);

    TReqansLogResult result;
    ReqAnsLog<TMetaReqansLogFrame>(id, searchContext, cgi, rd, requestBeginTime, docCount, [searchContext, logsAggregation, &result]() { return TMetaReqansLogFrame(searchContext, logsAggregation, &result); });
    return result;
}

void TReqAnsLog::Log(ui32 id, const ICustomReportBuilder& report) {
    const IReportBuilderContext& context = report.GetContext();
    ReqAnsLog<TProxyReqansLogFrame>(id, report, context.GetCgiParameters(), context.GetRequestData(), context.GetRequestStartTime(), report.GetDocumentsCount());
}
