#include "sla_processing.h"

#include <google/protobuf/text_format.h>

namespace {

    using namespace google::protobuf;

    NSaasProto::TCtypeSLA* GetCtypeSLA(NSaasProto::TSlaDescription& description, const TString& ctype) {
        for (ui32 i = 0; i < description.CTypeSLASize(); ++i) {
            if (description.GetCTypeSLA(i).GetCtype() == ctype) {
                return description.MutableCTypeSLA(i);
            }
        }
        NSaasProto::TCtypeSLA* res = description.AddCTypeSLA();
        res->SetCtype(ctype);
        return res;
    }

    template<typename T>
    bool TryGetProtoTextFromStorage(const TString& dmPath, const NSaas::IVersionedStorage& storage, T& message) {
        if (!storage.ExistsNode(dmPath)) {
            message.Clear();
            return true;
        }
        TString protoStr;
        storage.GetValue(dmPath, protoStr);
        NProtoBuf::TextFormat::Parser parser;
        return parser.ParseFromString(protoStr, &message);
    }

    struct TProto2JsonMapping {
        TString JsonName;
        TString ProtoName;
    };

    static const std::initializer_list<TProto2JsonMapping> protoToJsonMapping = {
        {"maxdocs", "SoftMaxDocsPerService"},
        {"ticket", "Ticket"},
        {"search_rps", "SearchRPS"},
        {"search_rps_planned", "SearchRPSPlanned"},
        {"index_rps", "IndexRPS"},
        {"total_index_size_bytes", "TotalIndexSizeBytes"},
        {"search_q_99_ms", "SearchQ99Ms"},
        {"search_q_999_ms", "SearchQ999Ms"},
        {"unanswers_5min_perc_warn", "Unanswers5MinPercWarn"},
        {"unanswers_5min_perc_crit", "Unanswers5MinPercCrit"},
        {"service_weight", "ServiceWeight"},
        {"abc_user_service", "ABCUserService"},
        {"abc_quota_service", "ABCQuotaService"},
        {"disaster_alerts", "DisasterAlerts"},
    };

    void SerializeCTypeSLA(const NSaasProto::TCtypeSLA& ctypeSLA, NJson::TJsonValue& result) {
        auto descriptor = ctypeSLA.GetDescriptor();
        auto reflection = ctypeSLA.GetReflection();
        for (const auto& mapping: protoToJsonMapping) {
            const FieldDescriptor* fieldDescriptor = descriptor->FindFieldByName(mapping.ProtoName);
            if (!reflection->HasField(ctypeSLA, fieldDescriptor)) {
                continue;
            }
            switch (fieldDescriptor->cpp_type()) {
                case FieldDescriptor::CPPTYPE_UINT64:
                    result[mapping.JsonName] = reflection->GetUInt64(ctypeSLA, fieldDescriptor);
                    break;
                case FieldDescriptor::CPPTYPE_DOUBLE:
                    result[mapping.JsonName] = reflection->GetDouble(ctypeSLA, fieldDescriptor);
                    break;
                case FieldDescriptor::CPPTYPE_STRING:
                    result[mapping.JsonName] = reflection->GetString(ctypeSLA, fieldDescriptor);
                    break;
                default:
                    ythrow yexception() << "Unknown field type";
            }
        }
        for (ui32 i = 0; i < ctypeSLA.NannyFerrymanSize(); ++i) {
            result["ferrymans"].AppendValue(ctypeSLA.GetNannyFerryman(i));
        }
    }

    void DeserializeCtypeSLA(const NJson::TJsonValue& value, NSaasProto::TCtypeSLA& ctypeSLA) {
        auto descriptor = ctypeSLA.GetDescriptor();
        auto reflection = ctypeSLA.GetReflection();
        for (const auto& mapping: protoToJsonMapping) {
            if (!value.Has(mapping.JsonName)) {
                continue;
            }
            const FieldDescriptor* fieldDescriptor = descriptor->FindFieldByName(mapping.ProtoName);
            const TString valueStr = value[mapping.JsonName].GetStringRobust();
            if (!valueStr) {
                reflection->ClearField(&ctypeSLA, fieldDescriptor);
            } else {
                switch (fieldDescriptor->cpp_type()) {
                    case FieldDescriptor::CPPTYPE_UINT64:
                        reflection->SetUInt64(&ctypeSLA, fieldDescriptor, FromString<ui64>(valueStr));
                        break;
                    case FieldDescriptor::CPPTYPE_DOUBLE:
                        reflection->SetDouble(&ctypeSLA, fieldDescriptor, FromString<double>(valueStr));
                        break;
                    case FieldDescriptor::CPPTYPE_STRING:
                        reflection->SetString(&ctypeSLA, fieldDescriptor, valueStr);
                        break;
                    default:
                        ythrow yexception() << "Unknown field type";
                }

            }
        }
        if (value.Has("ferrymans")) {
            ctypeSLA.ClearNannyFerryman();
            for (const auto& ferryman: value["ferrymans"].GetArray()) {
                ctypeSLA.AddNannyFerryman(ferryman.GetStringRobust());
            }
        }
    }

    const TString FILE_NAME = "sla_description.conf";
    const TString TS_FILE_PATH = "cache_timestamps/sla_last_updated";
}

namespace NRTYDeploy {

    using namespace NSaas;

    const TString& GetSLATimestampPath() {
        return TS_FILE_PATH;
    }

    bool TryGetSLADescription(const TString& serviceName, const IVersionedStorage& storage, NSaasProto::TSlaDescription& description) {
        const TString dmPath(TString::Join("configs/", serviceName, "/" + FILE_NAME));
        return TryGetProtoTextFromStorage(dmPath, storage, description);
    }

    bool TryGetSLADefault(const TString& ctype, const IVersionedStorage& storage, NSaasProto::TSlaDescription& description) {
        const TString dmPath(TString::Join("common/", ctype, "/" + FILE_NAME));
        NSaasProto::TCtypeSLA ctypeSLA;
        if (!TryGetProtoTextFromStorage(dmPath, storage, ctypeSLA)) {
            return false;
        }
        NSaasProto::TCtypeSLA* currentSLAPtr = GetCtypeSLA(description, ctype);
        ctypeSLA.MergeFrom(*currentSLAPtr);
        *currentSLAPtr = std::move(ctypeSLA);
        return true;
    }

    void MergeSLA(const NSaasProto::TSlaDescription& defaultDescr, NSaasProto::TSlaDescription& serviceDescr) {
        for (auto ctypeDefSLA: defaultDescr.GetCTypeSLA()) {
            auto* targetSLA = GetCtypeSLA(serviceDescr, ctypeDefSLA.GetCtype());
            ctypeDefSLA.MergeFrom(*targetSLA);
            *targetSLA = std::move(ctypeDefSLA);
        }
    }

    SLAUpdateStatus StoreSLADescription(const TString& serviceName, NSaas::IVersionedStorage& storage, NSaasProto::TSlaDescription& description) {
        const TString dmPath(TString::Join("configs/", serviceName, "/" + FILE_NAME));

        SLAUpdateStatus res;

        NProtoBuf::TextFormat::Printer printer;
        printer.SetHideUnknownFields(true);

        VERIFY_WITH_LOG(printer.PrintToString(description, &res.NewDataProto),
            "Failed to serialize message : %s", res.NewDataProto.data());

        if (storage.ExistsNode(dmPath)) {
            storage.GetValue(dmPath, res.OldDataProto);
        }
        if (res.OldDataProto == res.NewDataProto) {
            return res;
        }
        if (!storage.SetValue(dmPath, res.NewDataProto, true, true, &res.Version)) {
            ythrow yexception() << "Failed to store value " << dmPath;
        } else if (!storage.SetValue(GetSLATimestampPath(), ToString(Now().MilliSeconds()), false)) {
            ythrow yexception() << "Failed to update timestamp value";
        }
        return res;
    }

    NJson::TJsonValue SerializeDescription(const NSaasProto::TSlaDescription& description, const TString& ctype) {
        NJson::TJsonValue result(NJson::JSON_MAP);
        for (ui32 i = 0; i < description.OwnerLoginSize(); ++i) {
            result["owners"].AppendValue(description.GetOwnerLogin(i));
        }

        for (ui32 i = 0; i < description.ResponsiblesSize(); ++i) {
            result["responsibles"].AppendValue(description.GetResponsibles(i));
        }
        for (ui32 i = 0; i < description.CTypeSLASize(); ++i) {
            const auto& ctypeSLA = description.GetCTypeSLA(i);
            if (ctypeSLA.GetCtype() == ctype) {
                SerializeCTypeSLA(ctypeSLA, result);
                break;
            }
        }
        return result;
    }

    void PatchDescription(NSaasProto::TSlaDescription& description, const NJson::TJsonValue& value, const TString& ctype) {
        if (value.Has("owners")) {
            description.ClearOwnerLogin();
            for (const auto& owner: value["owners"].GetArray()) {
                description.AddOwnerLogin(owner.GetStringRobust());
            }
        }

        if (value.Has("responsibles")) {
            description.ClearResponsibles();
            for (const auto& responsible: value["responsibles"].GetArray()) {
                description.AddResponsibles(responsible.GetStringRobust());
            }
        }

        NSaasProto::TCtypeSLA* ctypeSLAPtr = GetCtypeSLA(description, ctype);
        DeserializeCtypeSLA(value, *ctypeSLAPtr);
    }

}
