#include "configuration.h"
#include "yt_delivery.h"

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

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

#include <util/folder/path.h>

namespace NSaasLB {

    class TLogbrokerCluster::TImpl
        : public IComponent<TLogbrokerModifyRequests>
        , protected TLogbrokerApi
    {
    private:
        struct TMapNode {
            using TPtr = TAtomicSharedPtr<TMapNode>;

            IEntity::TPtr Entity;
            bool Described;
            std::optional<THashMap<TString, TMapNode::TPtr>> Childs;

            TMapNode() = default;
            TMapNode(IEntity::TPtr entity, bool described = false, bool emptyChildsList = false)
                : Entity(entity)
                , Described(described)
            {
                if (emptyChildsList) {
                    Childs.emplace();
                }
            }
        };

    public:
        TImpl(const TString& server, const TString& token, const std::optional<TString>& remoteMirrorRulesToken);

        void AddOrModify(const IEntity& entity);
        void Remove(const IEntity& entity);

        void AddReadRule(const TReadRule& readRule);
        void RemoveReadRule(const TReadRule& readRule);
        void AddRemoteMirrorRule(const TRemoteMirrorRule& rule);
        void RemoveRemoteMirrorRule(const TRemoteMirrorRule& rule);
        void AddYtDelivery(const TString& topicPath, const TString& ytDelivery);
        void RemoveYtDelivery(const TString& topicPath, const TString& ytDelivery);

        bool Exist(const TString& path);
        TVector<std::pair<EEntityType, TString>> List(const TString& path);
        NLogBroker::DescribePathResult GetDescribed(const TString& path);
        TVector<TReadRule> GetReadRules(const TString& path);
        TVector<TRemoteMirrorRule> GetRemoteMirrorRules(const TString& path);
        TVector<TString> GetYtDeliveries(const TString& topicPath);

        TTopic GetTopic(const TString& path);
        TConsumer GetConsumer(const TString& path);

        void GrantPermissions(const TString& path, const TString& subject, const TVector<EPermission>& permissions);
        void RevokePermissions(const TString& path, const TString& subject, const TVector<EPermission>& permissions);

        void Apply(ui32 changesCount = 0) override;
        const TLogbrokerModifyRequests& GetChanges() const override;

    private:
        IEntity::TPtr Get(const TString& path);
        TMapNode::TPtr GetNode(const TString& path);
        TMapNode::TPtr GetNode(
            TMapNode::TPtr node,
            TVector<TString>::const_iterator begin,
            TVector<TString>::const_iterator end
        );

        void LoadChilds(TMapNode::TPtr node);

        void LoadAccounts();
        void LoadYtDeliveries();
        void Describe(TMapNode::TPtr node);

        void Modify(TMapNode::TPtr node, const IEntity& entity);
        void Add(
            TMapNode::TPtr node,
            TVector<TString>::const_iterator begin,
            TVector<TString>::const_iterator end,
            const IEntity& entity
        );

        TMapNode::TPtr AddNewEntity(TMapNode::TPtr parent, const TString& nodeName, IEntity::TPtr entity);
        void RemoveEntity(TMapNode::TPtr parent, const TString& nodeName, IEntity::TPtr entity);

        template <class TDescribeEntityResult>
        void FillReadRules(TDescribeEntityResult* protoEntity, const TVector<TReadRule>& entityReadRules);

        IEntity::TPtr Transform(const NLogBroker::Entry& entry) const;

        void AddCommand(const NLogBroker::SingleModifyRequest& command);

    private:
        TMapNode::TPtr Root;
        TReadRuleStorage ReadRules;
        TPermissionStorage Permissions;
        TYtDeliveryStorage YtDeliveries;

        const std::optional<TString> RemoteMirrorRulesToken;
        TRemoteMirrorRuleStorage RemoteMirrorRules;

        TLogbrokerModifyRequests Changes;
    };

    TLogbrokerCluster::TLogbrokerCluster(
        const TString& server,
        const TString& token,
        const std::optional<TString>& remoteMirrorRulesToken
    )
        : Impl(MakeHolder<TImpl>(server, token, remoteMirrorRulesToken))
    {}

    TLogbrokerCluster::~TLogbrokerCluster() = default;

    void TLogbrokerCluster::AddOrUpdateEntity(const IEntity& entity) {
        Impl->AddOrModify(entity);
    }

    void TLogbrokerCluster::RemoveEntity(const IEntity& entity) {
        Impl->Remove(entity);
    }

    void TLogbrokerCluster::AddReadRules(const TVector<TReadRule>& readRules) {
        for (auto&& rr : readRules) {
            Impl->AddReadRule(rr);
        }
    }

    void TLogbrokerCluster::RemoveReadRules(const TVector<TReadRule>& readRules) {
        for (auto&& rr : readRules) {
            Impl->RemoveReadRule(rr);
        }
    }

    void TLogbrokerCluster::AddRemoteMirrorRules(const TVector<TRemoteMirrorRule>& rules) {
        for (auto&& rule : rules) {
            Impl->AddRemoteMirrorRule(rule);
        }
    }

    void TLogbrokerCluster::RemoveRemoteMirrorRules(const TVector<TRemoteMirrorRule>& rules) {
        for (auto&& rule : rules) {
            Impl->RemoveRemoteMirrorRule(rule);
        }
    }

    void TLogbrokerCluster::AddYtDelivery(const TString& topicPath, const TString& ytDelivery) {
        Impl->AddYtDelivery(GetCorrectEntityPath(topicPath), ytDelivery);
    }

    void TLogbrokerCluster::RemoveYtDelivery(const TString& topicPath, const TString& ytDelivery) {
        Impl->RemoveYtDelivery(GetCorrectEntityPath(topicPath), ytDelivery);
    }

    NLogBroker::DescribePathResult TLogbrokerCluster::GetDescribed(const TString& path) {
        return Impl->GetDescribed(GetCorrectEntityPath(path));
    }

    TVector<TReadRule> TLogbrokerCluster::GetReadRules(const TString& path) {
        return Impl->GetReadRules(GetCorrectEntityPath(path));
    }

    TVector<TRemoteMirrorRule> TLogbrokerCluster::GetRemoteMirrorRules(const TString &path) {
        return Impl->GetRemoteMirrorRules(GetCorrectEntityPath(path));
    }

    TVector<TString> TLogbrokerCluster::GetYtDeliveries(const TString& topicPath) {
        return Impl->GetYtDeliveries(GetCorrectEntityPath(topicPath));
    }

    TTopic TLogbrokerCluster::GetTopic(const TString& path) {
        return Impl->GetTopic(GetCorrectEntityPath(path));
    }

    TConsumer TLogbrokerCluster::GetConsumer(const TString& path) {
        return Impl->GetConsumer(GetCorrectEntityPath(path));
    }

    TVector<TString> TLogbrokerCluster::GetTopics(const TString& path) {
        return GetEntityList(GetCorrectEntityPath(path), EEntityType::topic);
    }

    TVector<TString> TLogbrokerCluster::GetConsumers(const TString& path) {
        return GetEntityList(GetCorrectEntityPath(path), EEntityType::consumer);
    }

    void TLogbrokerCluster::GrantPermissions(
        const TString& path,
        const TVector<EPermission>& permissions,
        const TString& subject
    ) {
        Impl->GrantPermissions(GetCorrectEntityPath(path), subject, permissions);
    }

    void TLogbrokerCluster::RevokePermissions(
        const TString& path,
        const TVector<EPermission>& permissions,
        const TString& subject
    ) {
        Impl->RevokePermissions(GetCorrectEntityPath(path), subject, permissions);
    }

    const TLogbrokerModifyRequests& TLogbrokerCluster::GetChanges() const {
        return Impl->GetChanges();
    }

    void TLogbrokerCluster::Apply(ui32 changesCount) {
        Impl->Apply(changesCount);
    }

    TVector<TString> TLogbrokerCluster::GetEntityList(const TString& path, const EEntityType entityType) {
        auto entityList = Impl->List(path);
        TVector<TString> requestedEntityList;
        for (auto&& entity : entityList) {
            if (entity.first == entityType) {
                requestedEntityList.push_back(entity.second);
            }
        }
        return requestedEntityList;
    }

    TLogbrokerCluster::TImpl::TImpl(
        const TString& server,
        const TString& token,
        const std::optional<TString>& remoteMirrorRulesToken
    )
        : TLogbrokerApi(server, token)
        , Root(new TMapNode(MakeAtomicShared<TDirectory>("")))
        , RemoteMirrorRulesToken(remoteMirrorRulesToken)
    {
        LoadAccounts();
        LoadYtDeliveries();
    }

    void TLogbrokerCluster::TImpl::AddOrModify(const IEntity& entity) {
        auto node = GetNode(entity.GetPath());
        if (node) {
            Describe(node);
            Modify(node, entity);
            return;
        }
        auto dirs = SplitString(entity.GetPath(), "/");
        //Get(*dirs.begin());
        Add(Root, dirs.begin(), dirs.end(), entity);
    }

    void TLogbrokerCluster::TImpl::Remove(const IEntity& entity) {
        auto node = GetNode(entity.GetPath());
        if (!node) {
            return;
        }
        if (node->Entity->GetType() != entity.GetType()) {
            ythrow yexception() << "Cannot remove entity: incorrect type for path=" << entity.GetPath();
        }
        TString parentPath = TFsPath(entity.GetPath()).Parent();
        TString nodeName = TFsPath(entity.GetPath()).Basename();
        auto parent = GetNode(parentPath);
        RemoveEntity(parent, nodeName, node->Entity);
    }

    bool TLogbrokerCluster::TImpl::Exist(const TString& path) {
        return bool(GetNode(path));
    }

    TVector<std::pair<EEntityType, TString>> TLogbrokerCluster::TImpl::List(const TString& path) {
        auto node = GetNode(path);
        if (!node) {
            return {};
        }
        if (!node->Childs) {
            LoadChilds(node);
        }
        TVector<std::pair<EEntityType, TString>> childsList;
        for (auto&& child : *node->Childs) {
            childsList.push_back({child.second->Entity->GetType(), child.second->Entity->GetPath()});
        }
        return childsList;
    }

    void TLogbrokerCluster::TImpl::AddReadRule(const TReadRule& readRule) {
        auto topic = Get(readRule.GetTopicPath());
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Not exist topic for read rule creation, path=" << readRule.GetTopicPath();
        }
        auto consumer = Get(readRule.GetConsumerPath());
        if (!consumer || consumer->GetType() != EEntityType::consumer) {
            ythrow yexception() << "Not exist consumer for read rule creation. Path=" << readRule.GetConsumerPath();
        }
        auto command = ReadRules.Add(readRule);
        if (command) {
            AddCommand(*command);
        }
    }

    void TLogbrokerCluster::TImpl::RemoveReadRule(const TReadRule& readRule) {
        Get(readRule.GetConsumerPath());
        auto command = ReadRules.Remove(readRule);
        if (command) {
            AddCommand(*command);
        }
    }

    void TLogbrokerCluster::TImpl::AddRemoteMirrorRule(const TRemoteMirrorRule& rule) {
        Y_ENSURE(
            RemoteMirrorRulesToken.has_value(),
            "RemoteMirrorRulesToken should be set in order to add a new mirror rule"
        );

        auto topic = Get(rule.GetTopicPath());
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Topic for a mirror rule creation does not exist, path = " << rule.GetTopicPath();
        }

        auto command = RemoteMirrorRules.Add(rule, RemoteMirrorRulesToken.value());
        if (command) {
            AddCommand(*command);
            Changes.SetAddingMirrorRules(true);
        }
    }

    void TLogbrokerCluster::TImpl::RemoveRemoteMirrorRule(const TRemoteMirrorRule& rule) {
        Get(rule.GetTopicPath());
        auto command = RemoteMirrorRules.Remove(rule);
        if (command) {
            AddCommand(*command);
            Changes.SetRemovingMirrorRules(true);
        }
    }

    void TLogbrokerCluster::TImpl::AddYtDelivery(const TString& topicPath, const TString& ytDelivery) {
        auto topic = Get(topicPath);
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Not exist topic to add yt delivery '" << ytDelivery << "', path=" << topicPath;
        }
        auto command = YtDeliveries.Add(topicPath, ytDelivery);
        if (command) {
            AddCommand(*command);
        }
    }

    void TLogbrokerCluster::TImpl::RemoveYtDelivery(const TString& topicPath, const TString& ytDelivery) {
        auto topic = Get(topicPath);
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Not exist topic to remove yt delivery '" << ytDelivery << "', path=" << topicPath;
        }
        auto command = YtDeliveries.Remove(topicPath, ytDelivery);
        if (command) {
            AddCommand(*command);
        }
    }

    template <class TDescribeEntityResult>
    void TLogbrokerCluster::TImpl::FillReadRules(
        TDescribeEntityResult* protoEntity,
        const TVector<TReadRule>& entityReadRules
    ) {
        for (auto&& readRule : entityReadRules) {
            auto proto = protoEntity->add_read_rules();
            proto->CopyFrom(readRule.GetProto());
        }
    }

    NLogBroker::DescribePathResult TLogbrokerCluster::TImpl::GetDescribed(const TString& path) {
        auto entity = Get(path);
        if (!entity) {
            ythrow yexception() << "can not get information of a non-existent path=" << path;
        }
        NLogBroker::DescribePathResult described = entity->GetDescribed();

        auto entityReadRules = ReadRules.GetFor(*entity);
        if (entity->GetType() == EEntityType::consumer) {
            FillReadRules(described.mutable_consumer(), entityReadRules);
        } else if (entity->GetType() == EEntityType::topic) {
            FillReadRules(described.mutable_topic(), entityReadRules);
            TVector<TString> ytDeliveries = YtDeliveries.GetFor(path);
            *described.mutable_topic()->mutable_yt_deliveries() = {ytDeliveries.begin(), ytDeliveries.end()};
        }
        return described;
    }

    TVector<TReadRule> TLogbrokerCluster::TImpl::GetReadRules(const TString& path) {
        auto entity = Get(path);
        if (!entity) {
            ythrow yexception() << "can not get read rules of a non-existent path=" << path;
        }
        return ReadRules.GetFor(*entity);
    }

    TVector<TRemoteMirrorRule> TLogbrokerCluster::TImpl::GetRemoteMirrorRules(const TString& path) {
        auto topic = Get(path);
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Topic for mirror rules does not exist, given path = " << path;
        }
        return RemoteMirrorRules.GetForTopic(path);
    }

    TVector<TString> TLogbrokerCluster::TImpl::GetYtDeliveries(const TString& topicPath) {
        auto topic = Get(topicPath);
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Not exist topic to get yt delivery list, path=" << topicPath;
        }
        return YtDeliveries.GetFor(topicPath);
    }

    TTopic TLogbrokerCluster::TImpl::GetTopic(const TString& path) {
        auto topic = Get(path);
        if (!topic || topic->GetType() != EEntityType::topic) {
            ythrow yexception() << "Not exist topic for path=" << path;
        }
        TTopic* topicPtr = dynamic_cast<TTopic*>(topic.Get());
        Y_VERIFY(topicPtr);
        return *topicPtr;
    }

    TConsumer TLogbrokerCluster::TImpl::GetConsumer(const TString& path) {
        auto consumer = Get(path);
        if (!consumer || consumer->GetType() != EEntityType::consumer) {
            ythrow yexception() << "Not exist consumer for path=" << path;
        }
        TConsumer* consumerPtr = dynamic_cast<TConsumer*>(consumer.Get());
        return *consumerPtr;
    }

    void TLogbrokerCluster::TImpl::GrantPermissions(
        const TString& path,
        const TString& subject,
        const TVector<EPermission>& permissions
    ) {
        if (!Get(path)) {
            ythrow yexception() << "grant permissions on a non-existent path=" << path;
        }
        auto command = Permissions.Grant(path, subject, permissions);
        if (command) {
            AddCommand(*command);
        }
    }

    void TLogbrokerCluster::TImpl::RevokePermissions(
        const TString& path,
        const TString& subject,
        const TVector<EPermission>& permissions
    ) {
        if (!Get(path)) {
            ythrow yexception() << "revoke permissions on a non-existent path=" << path;
        }
        auto command = Permissions.Revoke(path, subject, permissions);
        if (command) {
            AddCommand(*command);
        }
    }

    void TLogbrokerCluster::TImpl::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 logbroker changes " << changesCount << "/" << Changes.GetRequest().size() << Endl;
        TLogbrokerApi::ExecuteModifyCommands({Changes.GetRequest().begin(), Changes.GetRequest().begin() + changesCount}, "");
        Changes.MutableRequest()->ExtractSubrange(0, changesCount, nullptr);
    }

    const TLogbrokerModifyRequests& TLogbrokerCluster::TImpl::GetChanges() const {
        return Changes;
    }

    IEntity::TPtr TLogbrokerCluster::TImpl::Get(const TString& path) {
        auto node = GetNode(path);
        if (!node) {
            return nullptr;
        }
        Describe(node);
        return node->Entity;
    }

    void TLogbrokerCluster::TImpl::LoadAccounts() {
        auto accounts = TLogbrokerApi::GetAccountsList();
        Root->Childs.emplace();
        for (auto account : accounts.names()) {
            TString path = account;
            IEntity::TPtr accountEntity = new TAccount(path);
            Root->Childs->insert({account, new TMapNode(accountEntity)});
        }
    }

    void TLogbrokerCluster::TImpl::LoadYtDeliveries() {
        auto deliveriesDescription = TLogbrokerApi::GetYtDeliveriesList();
        TVector<TString> deliveries;
        for (auto& delivery : deliveriesDescription.deliveries()) {
            deliveries.push_back(delivery.path());
        }
        YtDeliveries.InitDeliveries(deliveries);
    }

    void TLogbrokerCluster::TImpl::AddCommand(const NLogBroker::SingleModifyRequest& command) {
        auto request = Changes.AddRequest();
        request->CopyFrom(command);
    }

    void TLogbrokerCluster::TImpl::Describe(TMapNode::TPtr node) {
        if (node->Described) {
            return;
        }
        auto entry = TLogbrokerApi::DescribePath(node->Entity->GetPath());
        if (entry.has_directory()) {
            node->Entity = new TDirectory(entry);
            Permissions.Load(entry.path().path(), entry.directory());
        } else if (entry.has_consumer()) {
            node->Entity = new TConsumer(entry);
            Permissions.Load(entry.path().path(), entry.consumer());
            ReadRules.Load(entry.consumer());
        } else if (entry.has_topic()) {
            node->Entity = new TTopic(entry);
            Permissions.Load(entry.path().path(), entry.topic());
            ReadRules.Load(entry.topic());
            RemoteMirrorRules.Load(entry.topic());
            YtDeliveries.Load(entry.path().path(), entry.topic());
        } else if (entry.has_account()) {
            node->Entity = new TAccount(entry);
            Permissions.Load(entry.path().path(), entry.account());
        } else {
            ythrow yexception() << "unknown entry type: " << entry;
        }
        node->Described = true;
    }

    void TLogbrokerCluster::TImpl::Modify(TMapNode::TPtr node, const IEntity& entity) {
        Y_ENSURE(node->Described);
        if (!node->Entity->Equal(entity)) {
            node->Entity = entity.Clone();
            AddCommand(node->Entity->GetModifyCommand());
        }
    }

    void TLogbrokerCluster::TImpl::Add(
        TMapNode::TPtr node,
        TVector<TString>::const_iterator begin,
        TVector<TString>::const_iterator end,
        const IEntity& entity
    ) {
        if (begin == end) {
            ythrow yexception() << "Incorrect usage, item already exist: " << entity.GetPath();
        }
        if (!node->Childs) {
            LoadChilds(node);
        }

        TMapNode::TPtr nextNode;
        TString nextNodeName = *begin;
        auto it = node->Childs->find(nextNodeName);
        if (it != node->Childs->end()) {
            nextNode = it->second;
        } else {
            TString childPath = (node->Entity->GetPath() ? node->Entity->GetPath() + "/" : "") + (*begin);
            if (childPath == entity.GetPath()) {
                AddNewEntity(node, nextNodeName, entity.Clone());
                return;
            }
            IEntity::TPtr newDirectory = MakeAtomicShared<TDirectory>(childPath);
            nextNode = AddNewEntity(node, nextNodeName, newDirectory);
        }
        Add(nextNode, begin + 1, end, entity);
    }

    TLogbrokerCluster::TImpl::TMapNode::TPtr TLogbrokerCluster::TImpl::AddNewEntity(
        TMapNode::TPtr parent,
        const TString& nodeName,
        IEntity::TPtr entity
    ) {
        TMapNode::TPtr newNode = MakeAtomicShared<TMapNode>(entity, true, true);
        parent->Childs->insert({nodeName, newNode});
        AddCommand(entity->GetAddCommand());
        return newNode;
    }

    void TLogbrokerCluster::TImpl::RemoveEntity(TMapNode::TPtr parent, const TString& nodeName, IEntity::TPtr entity) {
        parent->Childs->erase(nodeName);
        AddCommand(entity->GetRemoveCommand());
    }

    TLogbrokerCluster::TImpl::TMapNode::TPtr TLogbrokerCluster::TImpl::GetNode(const TString& path) {
        auto dirs = SplitString(path, "/");
        return GetNode(Root, dirs.begin(), dirs.end());
    }

    TLogbrokerCluster::TImpl::TMapNode::TPtr TLogbrokerCluster::TImpl::GetNode(
        TMapNode::TPtr node,
        TVector<TString>::const_iterator begin,
        TVector<TString>::const_iterator end
    ) {
        if (begin == end) {
            return node;
        }
        if (!node->Childs) {
            LoadChilds(node);
        }
        auto it = node->Childs->find(*begin);
        if (it == node->Childs->end()) {
            return nullptr;
        }
        return GetNode(it->second, begin + 1, end);
    }

    void TLogbrokerCluster::TImpl::LoadChilds(TMapNode::TPtr node) {
        NLogBroker::ListDirectoryResult result = TLogbrokerApi::ListDirectory(node->Entity->GetPath());
        node->Childs.emplace();
        for (auto&& child : result.children()) {
            auto childEntity = Transform(child);
            auto dirs = SplitString(childEntity->GetPath(), "/");
            TString childName = dirs.back();
            node->Childs->insert({childName, new TMapNode(childEntity, false)});
        }
    }

    IEntity::TPtr TLogbrokerCluster::TImpl::Transform(const NLogBroker::Entry& entry) const {
        IEntity::TPtr ptr;
        if (entry.directory()) {
            ptr = new TDirectory(entry.directory());
        } else if (entry.consumer()) {
            ptr = new TConsumer(entry.consumer());
        } else if (entry.topic()) {
            ptr = new TTopic(entry.topic());
        } else {
            ythrow yexception() << "unknown entry type: " << entry;
        }
        return ptr;
    }

}
