#include "resources.h"
#include <saas/deploy_manager/modules/nanny/nanny.h>
#include <saas/deploy_manager/modules/resources/common/common.h>
#include <saas/deploy_manager/scripts/common/deploy/deploy_builder.h>
#include <saas/deploy_manager/scripts/common/global_names/global_names.h>
#include <saas/library/searchmap/searchmap.h>
#include <saas/util/external/dc.h>
#include <library/cpp/json/json_reader.h>
#include <library/cpp/json/json_writer.h>
#include <library/cpp/logger/global/global.h>
#include <util/string/split.h>

namespace {
    inline TString BuildCacheKey(const TStringBuf service, const TStringBuf ctype) {
        return TString::Join(service, '-', ctype);
    }
}

namespace NRTYDeploy {

    bool TTagsInfo::TServiceResourcesInfo::ParseList(const NJson::TJsonValue& value, TVector<NRTYCluster::TSlotData>& result, bool isEndpointSet) {
        if (value.GetType() == NJson::JSON_UNDEFINED)
            return true;
        if (!value.IsArray())
            return false;
        result.reserve(result.size() + value.GetArray().size());
        for (NJson::TJsonValue::TArray::const_iterator i = value.GetArray().begin(); i != value.GetArray().end(); ++i) {
            if (!i->IsString())
                return false;
            NRTYCluster::TSlotData sd;
            bool parseRes = isEndpointSet ? sd.ParseAsEndpointSet(i->GetStringRobust(), sd) : sd.Parse(i->GetStringRobust(), sd);
            if (!parseRes)
                return false;
            result.push_back(std::move(sd));
        }
        return true;
    }

    bool TTagsInfo::TServiceResourcesInfo::ParseBool(const NJson::TJsonValue& value, bool& result) {
        if (value.GetType() == NJson::JSON_UNDEFINED)
            return true;
        if (value.IsArray() || value.IsMap())
            return false;
        result = value.GetBooleanRobust();
        return true;
    }

    bool TTagsInfo::TServiceResourcesInfo::ParseTags(const NJson::TJsonValue& value, TTag::TType type, TVector<TTag>& tags) {
        if (value.GetType() == NJson::JSON_UNDEFINED) {
            return true;
        }
        if (!value.IsArray())
            return false;
        tags.reserve(tags.size() + value.GetArray().size());
        for (NJson::TJsonValue::TArray::const_iterator i = value.GetArray().begin(); i != value.GetArray().end(); ++i) {
            if (i->IsMap()) {
                const NJson::TJsonValue::TMapType& mapTagInfo = i->GetMap();
                TTag tag(mapTagInfo, type);
                if (!tag.Name)
                    return false;
                tags.push_back(std::move(tag));
            } else {
                if (!i->IsString())
                    return false;
                tags.push_back(TTag(i->GetStringRobust(), type));
            }
        }
        return true;
    }

    TTagsInfo::TServiceResourcesInfo::TPtr TTagsInfo::TServiceResourcesInfo::Deserialize(const NJson::TJsonValue& json) {
        TVector<TTag> tags;
        bool useContainers = USE_CONTAINERS_DEFAULT_VALUE;
        TVector<NRTYCluster::TSlotData> slots;
        TVector<NRTYCluster::TSlotData> excludedSlots;
        TVector<NRTYCluster::TSlotData> endpointSets;
        switch (json.GetType()) {
        case NJson::JSON_STRING:
            tags.emplace_back(json.GetString());
            return new TServiceResourcesInfo(std::move(tags), useContainers,
                std::move(slots), std::move(excludedSlots), std::move(endpointSets));

        case NJson::JSON_ARRAY:
            for (const auto& val: json.GetArray()) {
                NRTYCluster::TSlotData sd;
                if (!NRTYCluster::TSlotData::Parse(val.GetStringRobust(), sd))
                    return nullptr;
                slots.push_back(std::move(sd));
            }
            return new TServiceResourcesInfo(std::move(tags), useContainers,
                std::move(slots), std::move(excludedSlots), std::move(endpointSets));

        case NJson::JSON_MAP:
            if (ParseTags(json["nanny_services"], TTag::NANNY_SERVICE, tags) &&
                   ParseTags(json["tags"], TTag::NANNY_SERVICE, tags) &&
                   ParseList(json["slots"], slots) &&
                   ParseList(json["exclude"], excludedSlots) &&
                   ParseList(json["endpointsets"], endpointSets, true) &&
                   ParseBool(json["use_container_names"], useContainers)) {
                return new TServiceResourcesInfo(std::move(tags), useContainers,
                    std::move(slots), std::move(excludedSlots), std::move(endpointSets));
            }

        default:
            return nullptr;
        }
    }

    void TTagsInfo::TServiceResourcesInfo::Serialize(NJson::TJsonValue& json) const {
        json = NJson::JSON_MAP;
        for (const auto& tag: Tags) {
            switch(tag.Type) {
            case TTag::NANNY_SERVICE:
                json["nanny_services"].AppendValue(tag.ToString());
                break;
            default:
                FAIL_LOG("invalid tag type");
            }
        }
        if (UseContainers) {
            json["use_container_names"] = 1;
        }
        for (const auto& slot: SlotList)
            json["slots"].AppendValue(slot.FullSlotName());
        for (const auto& slot: ExcludeSlots)
            json["exclude"].AppendValue(slot.FullSlotName());
        for (const auto& slot: EndpointSets)
            json["endpointsets"].AppendValue(slot.FullHost());
    }

    void TTagsInfo::TTag::BuildAliasByTag() {
        if (Name.StartsWith("SAS") || Name.StartsWith("MAN") || Name.StartsWith("MSK")) {
            Alias = Name.substr(0, 3);
        }
    }

    TTagsInfo::TTag::TTag(const NJson::TJsonValue::TMapType& mapInfo, TType type)
        : Type(type)
    {
        {
            const auto it = mapInfo.find("name");
            if (it != mapInfo.end()) {
                Name = it->second.GetString();
            }
        }

        {
            const auto it = mapInfo.find("configuration");
            if (it != mapInfo.end()) {
                Configuration = it->second.GetString();
            } else {
                Configuration = "SAAS";
            }
        }

        {
            const auto it = mapInfo.find("alias");
            if (it != mapInfo.end()) {
                Alias = it->second.GetString();
            } else {
                BuildAliasByTag();
            }
        }
    }

    TTagsInfo::TTag::TTag(TStringBuf info, TType type)
        : Type(type)
    {
        TVector<TString> splitted;
        StringSplitter(info).Split('@').AddTo(&splitted);
        if (splitted.size() == 0) {
            ythrow yexception() << "incorrect tag string " << info;
        }
        Name = std::move(splitted[0]);
        if (splitted.size() > 1) {
            Configuration = std::move(splitted[1]);
        } else {
            Configuration = "SAAS";
        }
        BuildAliasByTag();
    }

    bool TTagsInfo::TTag::operator<(const TTagsInfo::TTag& other) const {
        return Name < other.Name;
    }

    bool TTagsInfo::TTag::operator==(const TTagsInfo::TTag& other) const {
        return Name == other.Name;
    }

    TTagsInfo::TTagsInfo(const IVersionedStorage& storage)
        : Storage(storage)
    {
        NRTYDeploy::GetCTypesFromStorage(storage, CTypes);
    }

    void TTagsInfo::InvalidateServiceTagsInfo(const TString& service, const TString& ctype) {
        TWriteGuard g(MutexCacheServiceInfo);
        CacheServiceInfo.erase(BuildCacheKey(service, ctype));
    }

    void TTagsInfo::SaveServiceTagsInfo(const TString& service, const TString& ctype, const TServiceResourcesInfo& info) const {
        const TString path = "/configs/" + service + "/tags.info";
        TString data;
        NJson::TJsonValue tagsInfo;
        if (Storage.GetValue(path, data))
            NJson::ReadJsonFastTree(data, &tagsInfo);
        info.Serialize(tagsInfo[ctype]);
        Storage.SetValue(path, NJson::WriteJson(tagsInfo, true, true));
    }

    TTagsInfo::TServiceResourcesInfo::TPtr TTagsInfo::GetServiceTagsInfo(const TString& service, const TString& ctype, bool logError) const {

        {
            TReadGuard rg(MutexCacheServiceInfo);
            auto it = CacheServiceInfo.find(BuildCacheKey(service, ctype));
            if (it != CacheServiceInfo.end()) {
                return it->second;
            }
        }
        TString data;
        const TString pathTagsInfo = "/configs/" + service + "/tags.info";
        if (!Storage.ExistsNode(pathTagsInfo) || !Storage.GetValue(pathTagsInfo, data)) {
            ERROR_LOG << "Can't receive tags info from " << service << Endl;
            TWriteGuard rg(MutexCacheServiceInfo);
            for (auto&& ctype : CTypes) {
                CacheServiceInfo[BuildCacheKey(service, ctype)] = nullptr;
            }
            return nullptr;
        }
        NJson::TJsonValue jv;
        NJson::TJsonValue::TMapType mapData;
        if (!NJson::ReadJsonFastTree(data, &jv) || !jv.GetMap(&mapData)) {
            ythrow yexception() << "Incorrect json format: " << data;
        } else {
            bool ctypeExists = false;
            for (auto&& itCType : CTypes) {
                const TString cacheKey(BuildCacheKey(service, itCType));
                NJson::TJsonValue::TMapType::const_iterator it = mapData.find(itCType);
                if (it == mapData.end()) {
                    if (logError && ctype == itCType)
                        ERROR_LOG << "Can't find ctype " << itCType << " from " << service << " tags info" << Endl;
                    TWriteGuard rg(MutexCacheServiceInfo);
                    CacheServiceInfo[cacheKey] = nullptr;
                } else {
                    TTagsInfo::TServiceResourcesInfo::TPtr result = TTagsInfo::TServiceResourcesInfo::Deserialize(it->second);
                    TWriteGuard wg(MutexCacheServiceInfo);
                    if (result) {
                        CacheServiceInfo[cacheKey] = result;
                    } else {
                        CacheServiceInfo[cacheKey] = nullptr;
                    }
                }
                if (itCType == ctype)
                    ctypeExists = true;
            }
            if (!ctypeExists)
                return nullptr;
            return GetServiceTagsInfo(service, ctype, logError);
        }
    }

    void TTagsInfo::InvalidateCTypeTagsInfo(const TString& ctype, const TString& serviceClass) {
        TWriteGuard g(MutexCacheServiceTypeInfo);
        CacheServiceTypeInfo.erase(serviceClass + "-" + ctype);
    }

    void TTagsInfo::SaveCTypeTagsInfo(const TString& ctype, const TString& serviceClass, const TServiceResourcesInfo& info) const {
        TString path = "/common/" + ctype + "/tags.info";
        TString data;
        NJson::TJsonValue tagsInfo;
        if (Storage.GetValue(path, data))
            NJson::ReadJsonFastTree(data, &tagsInfo);
        info.Serialize(tagsInfo[serviceClass]);
        Storage.SetValue(path, NJson::WriteJson(tagsInfo, true, true));
    }

    TTagsInfo::TServiceResourcesInfo::TPtr TTagsInfo::GetCTypeTagsInfo(const TString& ctype, const TString& serviceClass) const {
        const TString cacheKey(TString::Join(serviceClass, '-', ctype));
        {
            TReadGuard rg(MutexCacheServiceTypeInfo);
            auto it = CacheServiceTypeInfo.find(cacheKey);
            if (it != CacheServiceTypeInfo.end()) {
                return it->second;
            }
        }

        TString data;
        if (!Storage.GetValue("/common/" + ctype + "/tags.info", data)) {
            ERROR_LOG << "Can't receive tags info from " << ctype << Endl;
            TWriteGuard wg(MutexCacheServiceTypeInfo);
            CacheServiceTypeInfo[cacheKey] = nullptr;
            return nullptr;
        }
        NJson::TJsonValue jv;
        NJson::TJsonValue::TMapType mapData;
        if (!NJson::ReadJsonFastTree(data, &jv) || !jv.GetMap(&mapData)) {
            ythrow yexception() << "Incorrect json format: " << data;
        } else {
            NJson::TJsonValue::TMapType::const_iterator i = mapData.find(serviceClass);
            if (i == mapData.end()) {
                ERROR_LOG << "Can't find service_class " << serviceClass << " from " << ctype << " tags info" << Endl;
                TWriteGuard wg(MutexCacheServiceTypeInfo);
                CacheServiceTypeInfo[cacheKey] = nullptr;
                return nullptr;
            }
            TTagsInfo::TServiceResourcesInfo::TPtr result = TTagsInfo::TServiceResourcesInfo::Deserialize(i->second);
            if (!result)
                ythrow yexception() << "Incorrect tag data service_class " << serviceClass << " from " << ctype << " tags info: " << i->second.GetStringRobust() << Endl;
            TWriteGuard wg(MutexCacheServiceTypeInfo);
            CacheServiceTypeInfo[cacheKey] = result;
            return result;
        }
    }

    bool TResourcesManager::GetSlotListFromCacheUnsafe(const TTagsInfo::TTag& tag, TVector<NRTYCluster::TSlotData>& slotsResult) const {
        auto it = Cache.find(tag.ToString());
        if (it != Cache.end()) {
            slotsResult = it->second;
            return true;
        }
        return false;
    }

    bool TResourcesManager::GetSlotList(const TTagsInfo::TTag& tag, const TString& ctype,
        const bool useContainerNames, const TVector<NRTYCluster::TSlotData>& excludes,
        NRTYCluster::TCluster& result) const {

        TVector<NRTYCluster::TSlotData> slotsResult;
        bool fromCache = false;
        {
            TReadGuard rg(TagsCacheMutex);
            fromCache = GetSlotListFromCacheUnsafe(tag, slotsResult);
        }
        if (!fromCache) {
            bool loaded = false;

            if (tag.Type & TTagsInfo::TTag::NANNY_SERVICE) {
                if (!(loaded = EnvironmentInformation.GetNannyModule().GetSlots(tag.Name, useContainerNames, slotsResult)))
                    ERROR_LOG << "Can't receive Slots info from nanny for tag: " << tag << Endl;
            }

            if (!loaded) {
                ERROR_LOG << "Can't receive info from tag " << tag << Endl;
                return false;
            }

            TWriteGuard wg(TagsCacheMutex);
            if (!GetSlotListFromCacheUnsafe(tag, slotsResult))
                Cache[tag.ToString()] = slotsResult;
        }

        for (auto&& slot : slotsResult) {
            if (Find(excludes, slot) == excludes.end())
                result.RegisterSlot(ctype, slot);
        }
        return true;
    }


    TTagsInfo::TServiceResourcesInfo::TPtr TResourcesManager::GetServiceResourcesInfo(const TString& ctype, const TString& serviceType, const TString& serviceName) const {
        if (serviceName == "*")
            return TagsInfo.GetCTypeTagsInfo(ctype, serviceType);
        return TagsInfo.GetServiceTagsInfo(serviceName, ctype, true);
    }

    void TResourcesManager::SaveServiceResourcesInfo(const TString& ctype, const TString& serviceType, const TString& serviceName, const TTagsInfo::TServiceResourcesInfo& info) const {
        if (serviceName == "*")
            TagsInfo.SaveCTypeTagsInfo(ctype, serviceType, info);
        else
            TagsInfo.SaveServiceTagsInfo(serviceName, ctype, info);
    }

    void TResourcesManager::InvalidateCaches(const TString& ctype, const TString& serviceType, const TString& serviceName, bool reloadAfter) {
        if (reloadAfter) {
            TWriteGuard rg(AntiDDosMutex);

            const TString key = serviceType + "-" + serviceName;
            const auto& it = UpdateTimes.find(key);
            ui32 now = Now().Seconds();
            if (it != UpdateTimes.end() && (now - it->second < 45) ) {
                INFO_LOG << "Too frequent request: " << key << Endl;
                return;
            }
            UpdateTimes[key] = now;
        }
        TTagsInfo::TServiceResourcesInfo::TPtr res = GetServiceResourcesInfo(ctype, serviceType, serviceName);
        if (!!res) {
            TWriteGuard wg(TagsCacheMutex);
            for(const TTagsInfo::TTag& tag: res->GetTags())
                Cache.erase(tag.ToString());
        }
        if (serviceName == "*")
            TagsInfo.InvalidateCTypeTagsInfo(ctype, serviceType);
        else
            TagsInfo.InvalidateServiceTagsInfo(serviceName, ctype);

        if (reloadAfter && serviceName != "*") {
            TSTSInfo::TPtr newClusterPtr = NewClusterInfo(serviceType, serviceName);
            TWriteGuard rg(ClustersCacheMutex);
            Clusters[serviceType + "-" + serviceName] = newClusterPtr;
        } else {
            TWriteGuard rg(ClustersCacheMutex);
            Clusters.erase(serviceType + "-" + serviceName);
        }
    }

    NRTYCluster::TCluster TResourcesManager::GetCluster(const TString& serviceType, const TString& serviceName) const {
        const TString cacheKey(TString::Join(serviceType, '-', serviceName));
        {
            TReadGuard rg(ClustersCacheMutex);
            auto it = Clusters.find(cacheKey);
            if (it != Clusters.end())
                return it->second->GetCluster();
        }
        TWriteGuard rg(ClustersCacheMutex);
        TSTSInfo::TPtr& stsInfo = Clusters[cacheKey];
        if (!stsInfo)
            stsInfo.Reset(new TSTSInfo(serviceType, serviceName, *this));
        return stsInfo->GetCluster();
    }

    TResourcesManager::TSTSInfo::TPtr TResourcesManager::NewClusterInfo(const TString& serviceType, const TString& serviceName) const {
        TSTSInfo::TPtr stsInfo(new TSTSInfo(serviceType, serviceName, *this));
        stsInfo->GetCluster();
        return stsInfo;
    }

    NRTYCluster::TCluster TResourcesManager::TSTSInfo::GetCluster() const {
        {
            TReadGuard rg(Mutex);
            if (Cluster)
                return *Cluster;
        }
        TWriteGuard rg(Mutex);
        if (!Cluster) {
            Cluster.Reset(new NRTYCluster::TCluster);
            if (!CTypes.size()) {
                CTypes = ResourcesManager.GetCTypes();
            }
            for (auto&& cType : CTypes) {
                ResourcesManager.GetSlotList(cType, ServiceType, ServiceName, *Cluster);
            }
            INFO_LOG << "Cluster for " << ServiceName << " built: " << CTypes.size() << "/" << Cluster->GetSlots().size() << Endl;
        }
        return *Cluster;
    }

    bool TResourcesManager::GetSlotList(const TString& ctype, const TString& serviceType, const TString& serviceName, NRTYCluster::TCluster& result) const {
        DEBUG_LOG << "Request for slots: " << ctype << "/" << serviceType << "/" << serviceName << Endl;
        TTagsInfo::TServiceResourcesInfo::TPtr res;
        if (serviceName != "*")
            res = TagsInfo.GetServiceTagsInfo(serviceName, ctype, true);
        if (!res) // Sensitive logic
            res = TagsInfo.GetCTypeTagsInfo(ctype, serviceType);
        if (!res) {
            return false;
        }

        for (auto&& i : res->GetSlotList()) {
            result.RegisterSlot(ctype, i);
        }

        const auto& excludedSlots = res->GetExcludeSlots();
        const bool useContainers = res->GetUseContainers();
        for (auto&& tag : res->GetTags()) {
            if (!GetSlotList(tag, ctype, useContainers, excludedSlots, result)) {
                return false;
            }
        }
        DEBUG_LOG << "Request for slots: " << ctype << "/" << serviceType << "/" << serviceName << " OK: " << result.GetSlots().size() << Endl;
        return true;
    }

    bool TResourcesManager::GetServices(const IVersionedStorage& storage, TVector<TString>& services) {
        services.clear();
        if (storage.GetNodes("configs/", services, true)) {
            return true;
        }
        return false;
    }

}

template <>
void Out<NRTYDeploy::TTagsInfo::TTag>(IOutputStream& stream, TTypeTraits<NRTYDeploy::TTagsInfo::TTag>::TFuncParam tag) {
    stream << tag.Name << "@" << tag.Configuration;
}
