#include "meta.h"

#include <saas/library/persqueue/configuration/namespace/namespace.h>

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

#include <util/string/vector.h>

#include <google/protobuf/util/message_differencer.h>

#include <algorithm>

namespace NSaasLB {

    TServiceMetaManager::TServiceMetaManager(const TString& ns, const TString& name, const TString& ctype)
        : Namespace(ns)
        , Name(name)
        , Ctype(ctype)
    {
        auto nsManager = TNamespaceManager(ns);
        auto namespaceConfig = nsManager.GetConfig();

        ServiceConfigPath = namespaceConfig.GetServicesConfigsPath() + "/" + Name + "/" + Ctype;

        NZooKeeper::TZooKeeperOptions zkOptions(MAIN_SAAS_ZK_CLUSTER);
        Storage = MakeHolder<NZooKeeper::TZooKeeper>(zkOptions);

        TryLoadServiceConfig();
    }

    TServiceMetaManager::~TServiceMetaManager() = default;

    void TServiceMetaManager::TryLoadServiceConfig() {
        if (!Storage->Exists(ServiceConfigPath)) {
            return;
        }
        TString serviceConfigData;
        try {
            serviceConfigData = Storage->GetData(ServiceConfigPath);
        } catch (NZooKeeper::TNodeNotExistsError&) {
            ythrow yexception() << "no service config by path=" << ServiceConfigPath;
        }

        TServiceConfig config;
        if (!config.ParseFromString(serviceConfigData)) {
            ythrow yexception() << "incorrect service config by path=" << ServiceConfigPath;
        }
        Config = config;
    }

    const TServiceConfig& TServiceMetaManager::GetConfig() const {
        if (!Config) {
            ythrow yexception() << "service config does not exist";
        }
        return *Config;
    }

    void TServiceMetaManager::SetConfig(const TServiceConfig& config) {
        if (!Config || !google::protobuf::util::MessageDifferencer::Equals(*Config, config)) {
            AddChange(Config ? EChangeAction::MODIFY : EChangeAction::CREATE, config);
            Config = config;
        }
    }

    void TServiceMetaManager::Remove() {
        if (Config) {
            AddChange(EChangeAction::REMOVE, {});
            Config.reset();
        }
    }

    const TServiceMetaModifyRequests& TServiceMetaManager::GetChanges() const {
        return Changes;
    }

    void TServiceMetaManager::Apply(ui32 changesCount) {
        Y_ENSURE(changesCount <= ui32(Changes.GetRequest().size()), yexception() << "Incorrect changes count to apply");
        if (changesCount == 0) {
            changesCount = Changes.GetRequest().size();
        }
        INFO_LOG << "Apply meta service changes " << changesCount << "/" << Changes.GetRequest().size() << Endl;
        for (ui32 i = 0; i < changesCount; ++i) {
            auto& request = Changes.GetRequest(i);
            switch (request.GetAction()) {
                case EChangeAction::CREATE:
                case EChangeAction::MODIFY: {
                    TString serializedConfig;
                    Y_PROTOBUF_SUPPRESS_NODISCARD request.GetConfig().SerializeToString(&serializedConfig);
                    RecursiveCreateNode(request.GetPath(), serializedConfig);
                    break;
                }
                case EChangeAction::REMOVE: {
                    RemoveNode(request.GetPath());
                    break;
                }
                default: {
                    ythrow yexception() << "unsupported action for service meta changes: " << request;
                }
            }
        }
        Changes.MutableRequest()->ExtractSubrange(0, changesCount, nullptr);
    }

    void TServiceMetaManager::AddChange(EChangeAction action, std::optional<TServiceConfig> config) {
        auto request = Changes.AddRequest();
        request->SetAction(action);
        request->SetPath(ServiceConfigPath);
        if (config) {
            auto c = request->MutableConfig();
            c->CopyFrom(*config);
        }
    }

    void TServiceMetaManager::RecursiveCreateNode(const TString& nodePath, const TString& data) {
        auto dirs = SplitString(nodePath, "/");
        TString prefix = "";
        for (auto&& dir : dirs) {
            prefix += "/" + dir;
            if (!Storage->Exists(prefix)) {
                Storage->Create(prefix, "", NZooKeeper::ACLS_ALL, NZooKeeper::CM_PERSISTENT);
            }
        }
        Storage->SetData(nodePath, data);
    }

    void TServiceMetaManager::RemoveNode(const TString& nodePath) {
        Storage->Delete(nodePath, -1);
    }


}
