#include "manager.h"

#include <saas/library/persqueue/common/common.h>
#include <saas/library/persqueue/common/installations.h>
#include <saas/library/persqueue/configuration/service/modify_manager/locks/locks.h>
#include <saas/library/persqueue/configuration/service/modify_manager/logbroker/configuration.h>

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

namespace {
    struct TLogbrokerDescription {
        NSaasLB::ELogbrokerKind kind;
        TString topicsPath;
        TString consumersPath;
    };

    template <class TRequests, class TFunc>
    void UpdateStage(
        ui32& knownChangesCount,
        const TRequests& requests,
        NSaasLB::TStage& stage,
        TFunc selectTypeFunc
    ) {
        if (knownChangesCount != ui32(requests.size())) {
            auto stageRequests = (stage.*selectTypeFunc)()->MutableRequest();
            *stageRequests = {requests.begin() + knownChangesCount, requests.end()};
            knownChangesCount = requests.size();
        }
    }

    bool StagesWithSameType(const NSaasLB::TStage& a, const NSaasLB::TStage& b) {
        return (a.HasLogbroker() && b.HasLogbroker())
            || (a.HasLogbrokerMirror() && b.HasLogbrokerMirror())
            || (a.HasLocks() && b.HasLocks())
            || (a.HasServiceMeta() && b.HasServiceMeta());
    }

    bool CanMergeStages(const NSaasLB::TStage& currStage, const NSaasLB::TStage& prevStage) {
        if (!StagesWithSameType(currStage, prevStage)) {
            return false;
        }

        if (currStage.HasLogbrokerMirror()) {
            const auto& curr = currStage.GetLogbrokerMirror();
            const auto& prev = prevStage.GetLogbrokerMirror();

            if (
                prev.GetAddingMirrorRules() && curr.GetRemovingMirrorRules()
                || prev.GetRemovingMirrorRules() && curr.GetAddingMirrorRules()
            ) {
                return false;
            }
        }

        return true;
    }

    template <class TRequests>
    void CopyRequests(const TRequests& from, TRequests& to) {
        Copy(from.GetRequest().begin(), from.GetRequest().end(), RepeatedFieldBackInserter(to.MutableRequest()));
    }

    void UpdateMergedMirrorLogbroker(
        const NSaasLB::TLogbrokerModifyRequests& currLogbroker,
        NSaasLB::TLogbrokerModifyRequests& prevLogbroker
    ) {
        prevLogbroker.SetAddingMirrorRules(
            prevLogbroker.GetAddingMirrorRules()
            || currLogbroker.GetAddingMirrorRules()
        );
        prevLogbroker.SetRemovingMirrorRules(
            prevLogbroker.GetRemovingMirrorRules()
            || currLogbroker.GetRemovingMirrorRules()
        );
    }
}

namespace NSaasLB {

    TModifyManager::TModifyManager(
        const TString& ns,
        const TString& name,
        const TString& ctype,
        const TString& lbToken,
        const std::optional<TString>& lbRemoteMirrorRulesToken,
        std::optional<TServiceConfig> serviceConfig
    )
        : ServiceMeta(new TServiceMetaManager(ns, name, ctype))
    {
        if (!serviceConfig) {
            serviceConfig = GetServiceConfig();
        }

        Logbroker.Reset(
            new TLogbrokerCluster(
                GetCMLogbrokerEndpointWithPort(serviceConfig->GetLogbroker()),
                lbToken
            )
        );
        Locks.Reset(new TZooKeeperLocks(serviceConfig->GetLocksServer()));

        if (serviceConfig->HasLogbrokerMirror()) {
            LogbrokerMirror.Reset(
                new TLogbrokerCluster(
                    GetCMLogbrokerEndpointWithPort(serviceConfig->GetLogbrokerMirror()),
                    lbToken,
                    lbRemoteMirrorRulesToken
                )
            );
        }

        SetServiceConfig(*serviceConfig);
    }

    TModifyManager::~TModifyManager() = default;

    THolder<TLogbrokerCluster>& TModifyManager::GetLogbrokerCluster(const ELogbrokerKind kind) {
        if (kind == ELogbrokerKind::origin) {
            return Logbroker;
        } else {
            Y_VERIFY(LogbrokerMirror.Get() != nullptr, "LogbrokerMirror was not initialized");
            return LogbrokerMirror;
        }
    }

    TServiceDescription TModifyManager::Describe() {
        TServiceDescription description;
        description.MutableConfig()->CopyFrom(GetServiceConfig());

        TVector<::TLogbrokerDescription> logbrokerDescriptions = {
            {
                ELogbrokerKind::origin,
                description.GetConfig().GetTopicsPath(),
                description.GetConfig().GetConsumersPath()
            },
        };

        if (description.GetConfig().HasLogbrokerMirror()) {
            logbrokerDescriptions.push_back({
                ELogbrokerKind::mirror,
                description.GetConfig().GetMirrorTopicsPath(),
                description.GetConfig().GetMirrorConsumersPath()
            });
        }

        for (auto&& desc : logbrokerDescriptions) {
            auto topics = GetTopics(desc.topicsPath, desc.kind);
            for (const auto& topic : topics) {
                const auto& pathDescription = DescribePath(topic, desc.kind);
                Y_VERIFY(pathDescription.has_topic());
                auto* t = desc.kind == ELogbrokerKind::origin
                        ? description.AddTopics()
                        : description.AddMirrorTopics();
                t->CopyFrom(pathDescription.topic());
            }
            auto consumers = GetConsumers(desc.consumersPath, desc.kind);
            for (const auto& consumer : consumers) {
                const auto& pathDescription = DescribePath(consumer, desc.kind);
                Y_VERIFY(pathDescription.has_consumer());
                auto* c = desc.kind == ELogbrokerKind::origin
                        ? description.AddConsumers()
                        : description.AddMirrorConsumers();
                c->CopyFrom(pathDescription.consumer());
            }
        }

        auto consumersForLock = GetLocks(description.GetConfig().GetLocksPath());
        Sort(consumersForLock.begin(), consumersForLock.end(), [](const TString& a, const TString& b) {
            return (a.size() == b.size()) ? (a < b) : (a.size() < b.size());
        });
        for (auto& consumer : consumersForLock) {
            auto lockDescription = DescribeLock(description.GetConfig().GetLocksPath(), consumer);
            auto l = description.AddLocks();
            l->CopyFrom(lockDescription);
        }
        return description;
    }

    NLogBroker::DescribePathResult TModifyManager::DescribePath(const TString& path, ELogbrokerKind kind) {
        return GetLogbrokerCluster(kind)->GetDescribed(path);
    }

    TLockDescription TModifyManager::DescribeLock(const TString& path, const TString& consumer) {
        return Locks->GetDescribed(path, consumer);
    }

    const TServiceConfig& TModifyManager::GetServiceConfig() const {
        return ServiceMeta->GetConfig();
    }

    void TModifyManager::SetServiceConfig(const TServiceConfig& config) {
        TStageMonitor monitor(this);
        return ServiceMeta->SetConfig(config);
    }

    void TModifyManager::RemoveServiceConfig() {
        TStageMonitor monitor(this);
        ServiceMeta->Remove();
    }

    TVector<TString> TModifyManager::GetConsumers(const TString& path, const ELogbrokerKind kind) {
        return GetLogbrokerCluster(kind)->GetConsumers(path);
    }

    TVector<TString> TModifyManager::GetTopics(const TString& path, const ELogbrokerKind kind) {
        return GetLogbrokerCluster(kind)->GetTopics(path);
    }

    TVector<TReadRule> TModifyManager::GetReadRules(const TString& entity, const ELogbrokerKind kind) {
        return GetLogbrokerCluster(kind)->GetReadRules(entity);
    }

    TVector<TRemoteMirrorRule> TModifyManager::GetRemoteMirrorRules(const TString& topic) {
        return GetLogbrokerCluster(ELogbrokerKind::mirror)->GetRemoteMirrorRules(topic);
    }

    TVector<TString> TModifyManager::GetYtDeliveries(const TString& topicPath, const ELogbrokerKind kind) {
        return GetLogbrokerCluster(kind)->GetYtDeliveries(topicPath);
    }

    TVector<TString> TModifyManager::GetSaasYtDeliveries(const TString& topicPath, const ELogbrokerKind kind) {
        auto deliveries = GetYtDeliveries(topicPath, kind);
        TVector<TString> saasDeliveries;
        for (auto& delivery : deliveries) {
            if (delivery.StartsWith("shared/saas-")) {
                saasDeliveries.push_back(delivery);
            }
        }
        return saasDeliveries;
    }

    TVector<TString> TModifyManager::GetConsumersFor(const TString& topicPath, const ELogbrokerKind kind) {
        auto readRules = GetLogbrokerCluster(kind)->GetReadRules(topicPath);
        THashSet<TString> consumers;
        for (auto&& rr : readRules) {
            consumers.insert(rr.GetConsumerPath());
        }
        return {consumers.begin(), consumers.end()};
    }

    void TModifyManager::CreateConsumers(const TVector<TString>& consumers, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        for (auto&& consumer : consumers) {
            GetLogbrokerCluster(kind)->AddOrUpdateEntity(TConsumer(consumer));
        }
    }

    void TModifyManager::CreateTopics(const TVector<TString>& topics, const ELogbrokerKind kind, const bool authRequired) {
        TStageMonitor monitor(this);
        for (auto&& topic : topics) {
            TTopic currentTopic = TTopic(topic);
            if (authRequired) {
                currentTopic.SetAuthRequired(authRequired);
            }
            GetLogbrokerCluster(kind)->AddOrUpdateEntity(currentTopic);
        }
    }

    void TModifyManager::AddYtDelivery(const TString& topicPath, const TString& ytDelivery, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        return GetLogbrokerCluster(kind)->AddYtDelivery(topicPath, ytDelivery);
    }

    void TModifyManager::RemoveConsumers(const TVector<TString>& consumers, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        for (const auto& consumer : consumers) {
            GetLogbrokerCluster(kind)->RemoveEntity(TConsumer(consumer));
        }
    }

    void TModifyManager::RemoveTopics(const TVector<TString>& topics, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        for (auto&& topic : topics) {
            GetLogbrokerCluster(kind)->RemoveEntity(TTopic(topic));
        }
    }

    void TModifyManager::RemoveYtDelivery(const TString& topicPath, const TString& ytDelivery, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        return GetLogbrokerCluster(kind)->RemoveYtDelivery(topicPath, ytDelivery);
    }

    void TModifyManager::CreateReadRules(const TVector<TReadRule>& readRules, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        GetLogbrokerCluster(kind)->AddReadRules(readRules);
    }

    void TModifyManager::RemoveReadRules(const TVector<TReadRule>& readRules, const ELogbrokerKind kind) {
        TStageMonitor monitor(this);
        GetLogbrokerCluster(kind)->RemoveReadRules(readRules);
    }

    void TModifyManager::CreateRemoteMirrorRules(const TVector<TRemoteMirrorRule>& rules) {
        TStageMonitor monitor(this);
        GetLogbrokerCluster(ELogbrokerKind::mirror)->AddRemoteMirrorRules(rules);
    }

    void TModifyManager::RemoveRemoteMirrorRules(const TVector<TRemoteMirrorRule>& rules) {
        TStageMonitor monitor(this);
        GetLogbrokerCluster(ELogbrokerKind::mirror)->RemoveRemoteMirrorRules(rules);
    }

    void TModifyManager::GrantPermissions(
        const TString& path,
        const TVector<EPermission>& permissions,
        const TVector<TString>& subjects,
        const ELogbrokerKind kind
    ) {
        TStageMonitor monitor(this);
        for (auto& subject : subjects) {
            GetLogbrokerCluster(kind)->GrantPermissions(path, permissions, subject);
        }
    }

    void TModifyManager::RevokePermissions(
        const TString& path,
        const TVector<EPermission>& permissions,
        const TVector<TString>& subjects,
        const ELogbrokerKind kind
    ) {
        TStageMonitor monitor(this);
        for (auto& subject : subjects) {
            GetLogbrokerCluster(kind)->RevokePermissions(path, permissions, subject);
        }
    }

    TVector<TString> TModifyManager::GetLocks(const TString& path) {
        return Locks->GetLocks(path);
    }

    void TModifyManager::AddLocks(const TString& path, const TVector<TString>& consumers) {
        TStageMonitor monitor(this);
        for (auto&& consumer : consumers) {
            Locks->Add(path, consumer);
        }
    }
    void TModifyManager::RemoveLocks(const TString& path, const TVector<TString>& consumers) {
        TStageMonitor monitor(this);
        for (auto&& consumer : consumers) {
            Locks->Remove(path, consumer);
        }
    }

    TServiceChanges TModifyManager::GetChanges() const {
        TServiceChanges changes = Changes;
        for (auto& stage : *changes.MutableStages()) {
            if(!stage.HasLogbrokerMirror()) {
                continue;
            }
            auto* requests = stage.MutableLogbrokerMirror()->MutableRequest();
            for (auto& request: *requests) {
                if (request.has_create_remote_mirror_rule()) {
                    NLogBroker::CreateRemoteMirrorRuleRequest* mirrorRule = request.mutable_create_remote_mirror_rule();
                    NLogBroker::Credentials* credentials = mirrorRule->mutable_properties()->mutable_credentials();

                    const TString& mirrorToken = MaskToken(credentials->oauth_token());
                    credentials->set_oauth_token(mirrorToken);
                }
            }
        }
        return changes;
    }

    void TModifyManager::Apply() {
        INFO_LOG << "Changes: " << Endl << GetChanges().DebugString() << Endl;
        INFO_LOG << "Apply service changes..." << Endl;
        for (auto&& stage : Changes.GetStages()) {
            if (stage.HasServiceMeta()) {
                ServiceMeta->Apply(stage.GetServiceMeta().GetRequest().size());
            } else if (stage.HasLogbroker()) {
                Logbroker->Apply(stage.GetLogbroker().GetRequest().size());
            } else if (stage.HasLogbrokerMirror()) {
                LogbrokerMirror->Apply(stage.GetLogbrokerMirror().GetRequest().size());
            } else if (stage.HasLocks()) {
                Locks->Apply(stage.GetLocks().GetRequest().size());
            } else {
                ythrow yexception() << "unknown stage type to apply: " << stage;
            }
        }

        Changes.Clear();
        ServiceMetaChangesCount = 0;
        LogbrokerChangesCount = 0;
        LogbrokerMirrorChangesCount = 0;
        LocksChangesCount = 0;

        INFO_LOG << "Changes applied." << Endl;
    }

    void TModifyManager::AddStage(const TStage& stage) {
        ui32 stagesCount = Changes.GetStages().size();
        if (stagesCount) {
            auto& prevStage = *Changes.MutableStages(stagesCount - 1);
            if (CanMergeStages(stage, prevStage)) {
                if (stage.HasServiceMeta()) {
                    CopyRequests(stage.GetServiceMeta(), *prevStage.MutableServiceMeta());
                }
                if (stage.HasLogbroker()) {
                    CopyRequests(stage.GetLogbroker(), *prevStage.MutableLogbroker());
                }
                if (stage.HasLogbrokerMirror()) {
                    CopyRequests(stage.GetLogbrokerMirror(), *prevStage.MutableLogbrokerMirror());
                    UpdateMergedMirrorLogbroker(stage.GetLogbrokerMirror(), *prevStage.MutableLogbrokerMirror());
                }
                if (stage.HasLocks()) {
                    CopyRequests(stage.GetLocks(), *prevStage.MutableLocks());
                }
                return;
            }
        }
        Changes.MutableStages()->Add()->CopyFrom(stage);
    }

    void TModifyManager::StartNextStage() {
        TStage stage;

        {
            const auto& changes = ServiceMeta->GetChanges();
            UpdateStage(ServiceMetaChangesCount, changes.GetRequest(), stage, &TStage::MutableServiceMeta);
        }
        {
            const auto& changes = Logbroker->GetChanges();
            UpdateStage(LogbrokerChangesCount, changes.GetRequest(), stage, &TStage::MutableLogbroker);
        }
        {
            const auto& changes = Locks->GetChanges();
            UpdateStage(LocksChangesCount, changes.GetRequest(), stage, &TStage::MutableLocks);
        }

        if (LogbrokerMirror.Get() != nullptr) {
            const auto& changes = LogbrokerMirror->GetChanges();
            UpdateStage(LogbrokerMirrorChangesCount, changes.GetRequest(), stage, &TStage::MutableLogbrokerMirror);
        }

        if (stage.HasLogbroker() || stage.HasLogbrokerMirror() || stage.HasLocks() || stage.HasServiceMeta()) {

            if (stage.HasLogbroker()) {
                stage.MutableLogbroker()->SetLogbroker(GetServiceConfig().GetLogbroker());
            }

            if (stage.HasLogbrokerMirror()) {
                stage.MutableLogbrokerMirror()->SetLogbroker(GetServiceConfig().GetLogbrokerMirror());

                const auto& changes = LogbrokerMirror->GetChanges();
                stage.MutableLogbrokerMirror()->SetAddingMirrorRules(changes.GetAddingMirrorRules());
                stage.MutableLogbrokerMirror()->SetRemovingMirrorRules(changes.GetRemovingMirrorRules());
            }

            AddStage(stage);
        }
    }

}
