#include "cluster.h"
#include <saas/library/searchmap/parsers/json/json.h>
#include <library/cpp/cache/thread_safe_cache.h>
#include <library/cpp/digest/md5/md5.h>
#include <google/protobuf/text_format.h>

using namespace NSaas;

TString TCluster::Serialize() const {
    NSaasProto::TCluster root = SerializeToProto();
    TString result;
    if (!::google::protobuf::TextFormat::PrintToString(root, &result))
        ythrow yexception() << "cannot serialize cluster";

    DEBUG_LOG << "Store cluster meta: " << result << Endl;

    return result;
}

bool TCluster::Deserialize(const TString& string) {
    NSaasProto::TCluster root;
    ::NProtoBuf::TextFormat::Parser parser;
    parser.AllowUnknownField(true);
    if (!parser.ParseFromString(string, &root))
        ythrow yexception() << "cannot deserialize cluster from string:" << string;
    if (!root.IsInitialized())
        ythrow yexception() << "Broken cluster.meta " << root.InitializationErrorString();
    return Deserialize(root);
}

NSaasProto::TCluster TCluster::SerializeToProto() const {
    NSaasProto::TCluster result;

    for (auto& i : MetaServices) {
        *result.AddMetaServices() = i.second.SerializeToProto();
    }

    for (auto& i : Services) {
        *result.MutableService()->AddServices() = i.second.SerializeToProto();
    }

    for (auto& i : MetaInt) {
        *result.MutableIntMeta()->AddServices() = i.second.SerializeToProto();
    }

    return result;
}
namespace {
    bool CheckSearchMap(bool checkDoubles, bool checkIntervals, const TMap<TString, NSearchMapParser::TSearchMapService>& services, TString& error) {
        TSet<TString> slots;
        bool result = true;
        for (auto& service : services) {
            TVector<NSearchMapParser::TShardsInterval> intervalsFilter;
            TVector<NSearchMapParser::TShardsInterval> intervalsFetch;
            TVector<NSearchMapParser::TShardsInterval> intervalsIndex;
            if (!service.second.Name) {
                error += "\nIncorrect service name";
                result = false;
            }
            if (service.second.Replicas.empty())
                continue;
            for (auto& replica : service.second.Replicas) {
                for (auto& host : replica.Hosts) {
                    if (checkDoubles && slots.contains(host.GetSlotName())) {
                        error += "\nslot " + host.GetSlotName() + " used twice";
                        result = false;
                    }
                    if (!host.DisableSearchFiltration && !host.DisableSearch)
                        intervalsFilter.push_back(host.Shards);
                    if (!host.DisableFetch && !host.DisableSearch)
                        intervalsFetch.push_back(host.Shards);
                    if (!host.DisableIndexing)
                        intervalsIndex.push_back(host.Shards);
                    slots.insert(host.GetSlotName());
                }
            }
            TVector<NSearchMapParser::TShardsInterval> filterArea = NSearchMapParser::TShardsInterval::Merge(intervalsFilter);
            TVector<NSearchMapParser::TShardsInterval> fetchArea = NSearchMapParser::TShardsInterval::Merge(intervalsFetch);
            TVector<NSearchMapParser::TShardsInterval> indexArea = NSearchMapParser::TShardsInterval::Merge(intervalsIndex);
            if (checkIntervals && service.second.ShardsDispatcher->NeedFullCoverage()) {
                if (filterArea.size() != 1 || filterArea[0].GetMin() != 0 || filterArea[0].GetMax() != service.second.GetShardsMax()) {
                    error += "\nfilter intervals don't cover all: service=" + service.second.Name + ";intervals=";
                    for (const auto& interval : filterArea)
                        error += interval.ToString() + ",";
                    result = false;
                }
                if (fetchArea.size() != 1 || fetchArea[0].GetMin() != 0 || fetchArea[0].GetMax() != service.second.GetShardsMax()) {
                    error += "\nfetch intervals don't cover all: service=" + service.second.Name + ";intervals=";
                    for (const auto& interval : fetchArea)
                        error += interval.ToString() + ",";
                    result = false;
                }
                if (indexArea.size() != 1 || indexArea[0].GetMin() != 0 || indexArea[0].GetMax() != service.second.GetShardsMax()) {
                    error += "\nindex intervals don't cover all: service=" + service.second.Name + ";intervals=";
                    for (const auto& interval : indexArea)
                        error += interval.ToString() + ",";
                    result = false;
                }
            }
        }
        return result;
    }
}

bool TCluster::Validate(TString& error) const {
    bool result = true;

    result &= ::CheckSearchMap(true, true, Services, error);
    result &= ::CheckSearchMap(false, true, MetaInt, error);

    TSearchMaps searchMaps;
    BuildSearchMaps(searchMaps, false);
    try {
        searchMaps.SP.Compile(true, true);
    } catch (...) {
        error += "\nSPSearchMap : " + CurrentExceptionMessage();
        result = false;
    }
    try {
        searchMaps.IP.Compile(true, true);
    } catch (...) {
        error += "\nIPSearchMap : " + CurrentExceptionMessage();
        result = false;
    }
    try {
        searchMaps.IntMeta.Compile(true, true);
    } catch (...) {
        error += "\nIntMetaSearchMap : " + CurrentExceptionMessage();
        result = false;
    }
    try {
        searchMaps.Meta.Compile(true, true);
    } catch (...) {
        error += "\nMetaServiceSearchMap : " + CurrentExceptionMessage();
        result = false;
    }
    try {
        searchMaps.RTY.Compile(true, true);
    } catch (...) {
        error += "\nRTYSearchMap : " + CurrentExceptionMessage();
        result = false;
    }

    return result;
}

void NSaas::TCluster::BuildSearchMaps(TSearchMaps& searchMaps, bool compile) const {
    for (const auto& i : Services) {
        searchMaps.RTY.AddService(i.first) = i.second;
        searchMaps.IP.AddService(i.first) = i.second;
        NSearchMapParser::TSearchMapService& spService = searchMaps.SP.AddService(i.first) = i.second;
        TServices::const_iterator metaInt = MetaInt.find(i.first);
        if (metaInt != MetaInt.end())
            spService.Replicas = metaInt->second.Replicas;
    }

    for (const auto& i : MetaInt) {
        searchMaps.IntMeta.AddService(i.first) = i.second;
    }

    for (const auto& i : MetaServices) {
        searchMaps.IP.GetMetaServices().push_back(i.second);
        NSearchMapParser::TServiceSpecificOptions* sso = &searchMaps.Meta.AddService(i.first);
        *sso = i.second;
        searchMaps.Meta.GetMetaServices().push_back(i.second);
        searchMaps.SP.GetMetaServices().push_back(i.second);
    }

    if (compile) {
        searchMaps.RTY.Compile(false, false);
        searchMaps.IP.Compile(false, false);
        searchMaps.SP.Compile(false, false);
        searchMaps.Meta.Compile(false, false);
        searchMaps.IntMeta.Compile(false, false);
    }
}

bool TCluster::Deserialize(const NSaasProto::TCluster& cluster) {
    for (ui32 service = 0; service < cluster.GetService().ServicesSize(); ++service) {
        NSearchMapParser::TSearchMapService sms;
        if (!sms.Deserialize(cluster.GetService().GetServices(service)))
            return false;

        Services[cluster.GetService().GetServices(service).GetName()] = sms;
    }

    for (ui32 service = 0; service < cluster.MetaServicesSize(); ++service) {
        NSearchMapParser::TMetaService ms;
        if (!ms.Deserialize(cluster.GetMetaServices(service)))
            return false;

        MetaServices[ms.Name] = ms;
    }

    for (ui32 service = 0; service < cluster.GetIntMeta().ServicesSize(); ++service) {
        NSearchMapParser::TSearchMapService sms;
        if (!sms.Deserialize(cluster.GetIntMeta().GetServices(service)))
            return false;

        MetaInt[cluster.GetIntMeta().GetServices(service).GetName()] = sms;
    }

    return true;
}

bool TClusterConst::Deserialize(const NSaasProto::TCluster& cluster) {
    if (!Cluster.Deserialize(cluster))
        return false;
    Cluster.BuildSearchMaps(SearchMapsCache, true);
    return true;
}
bool TClusterConst::Deserialize(const TString& cluster) {
    if (!Cluster.Deserialize(cluster))
        return false;
    Cluster.BuildSearchMaps(SearchMapsCache, true);
    return true;
}

class NSaas::TClusterLocker::TCacheCallbacks : public TThreadSafeCache<TString, TClusterConst, const NRTYDeploy::ICommonData&, const TString&, i64>::ICallbacks {
    TKey GetKey(const NRTYDeploy::ICommonData& common, const TString& ctype, i64 version) const override {
        TString storageStr = common.GetConfig().GetStorageOptions().SerializeToJson().GetStringRobust();
        MD5 calc;
        calc.Update(ctype.data(), ctype.size());
        calc.Update(&version, sizeof(version));
        calc.Update(storageStr.data(), storageStr.size());
        char buf[33];
        return calc.End(buf);
    }

    TClusterConst* CreateObject(const NRTYDeploy::ICommonData& common, const TString& ctype, i64 version) const override {
        CHECK_WITH_LOG(version >= 0);
        TString content;
        if (common.GetStorage().GetValue(GetFilePath(ctype), content, version, false)) {
            THolder<TClusterConst> result = MakeHolder<TClusterConst>();
            if (result->Deserialize(content))
                return result.Release();
        }
        ERROR_LOG << "Cannot create cluster cache object, ctype= " << ctype << " version=" << version << " content_len=" << content.length() << Endl;
        return nullptr;
    }
};


const TClusterConst* NSaas::TClusterLocker::DefineConst(const NRTYDeploy::ICommonData& common, const TString& ctype, i64 version /*= -1*/) {
    if (version < 0 && !GetVersion(common, ctype, version))
        ythrow yexception() << "Incorrect ctype: " << ctype << ": Cluster request incorrect path: " << GetFilePath(ctype);
    auto ptr = TCacheCallbacks::TOwner::Get<TCacheCallbacks>(common, ctype, version);
    if (!ptr)
        ythrow yexception() << "No data for ctype: " << ctype << ": path: " << GetFilePath(ctype);
    return ptr.Get();
}

void NSaas::TClusterLocker::ClearCaches() {
    TCacheCallbacks::TOwner::Clear<TCacheCallbacks>();
}

NSaas::TClusterLocker NSaas::TClusterLocker::DefineMutable(const NRTYDeploy::ICommonData& common, const TString& ctype) {
    TClusterLocker result;
    TString path = GetFilePath(ctype);
    result.Lock = common.GetStorage().WriteLockNode(path);
    CHECK_WITH_LOG(!!result.Lock && result.Lock->IsLocked());
    result.Cluster = MakeAtomicShared<TCluster>();
    result.CType = ctype;
    result.CommonData = &common;
    if (const TClusterConst* clusterConst = DefineConst(common, ctype, -1))
        *result.Cluster = clusterConst->GetCluster();
    result.InitialContent = result.Cluster->Serialize();
    return result;
}
