#include "ascii_art.h"
#include "human_readable_output.h"

#include <saas/library/persqueue/common/common.h>
#include <saas/library/persqueue/common/installations.h>

#include <library/cpp/colorizer/colors.h>

#include <util/string/vector.h>
#include <util/string/join.h>

#include <algorithm>
#include <cctype>

namespace NSaasLB {
    using TMirrorRuleKey = NLogBroker::RemoteMirrorRuleKey;
    using TMirrorRuleProps = NLogBroker::RemoteMirrorRuleProperties;
    using TMirrorRule = std::tuple<TMirrorRuleKey, std::optional<TMirrorRuleProps>>;

    void DescribeNamespace(const TNamespaceDescription& ns) {
        DescribeNamespaceConfig(ns.GetConfig());

        Cout << Endl;
        auto services = ns.GetServices();
        if (!services.empty()) {
            Cout << "Services:" << Endl;
            for (auto sIt = services.begin(); sIt != services.end(); ++sIt) {
                bool lastService = NAsciiArt::isLastItem(services.end(), sIt);
                Cout << (lastService ? NAsciiArt::LIST_ITEM_LAST : NAsciiArt::LIST_ITEM) << sIt->GetName() << Endl;
                auto& ctypes = sIt->GetCtypes();
                for (auto cIt = ctypes.begin(); cIt != ctypes.end(); ++cIt) {
                    bool lastCtype = NAsciiArt::isLastItem(ctypes.end(), cIt);
                    Cout << (lastService ? NAsciiArt::SPACE : NAsciiArt::LIST)
                        << "\t" << (lastCtype ? NAsciiArt::LIST_ITEM_LAST : NAsciiArt::LIST_ITEM)
                        << *cIt << Endl;
                }
            }
        } else {
            Cout << "No sevices in namespace" << Endl;
        }
    }

    void DescribeNamespaceConfig(const TNamespaceConfig& config) {
        Cout << "Services config path for configuration logbroker: " << config.GetServicesConfigsPath() << Endl;

        Cout << Endl << "Default values for services creation" << Endl;
        Cout << "Logbroker path: " << config.GetLogbrokerServicesPath()  << Endl;
        Cout << "Logbroker path with dev consumers: " << (config.HasDevConsumersPath() ? config.GetDevConsumersPath() : "not set") << Endl;
        if (config.HasDeployManagerHost()) {
            Cout << "Deploy Manager: " << config.GetDeployManagerHost() << Endl;
        }
        Cout << Endl << "Default ZooKeeper settings for lock consumers" << Endl;
        Cout << "Path: " << config.GetLocksPath() << Endl;
        Cout << "Servers:" << Endl;
        for (const auto& s : SplitString(config.GetLocksServer(), ",")) {
            Cout << " - " << s << Endl;
        }
    }

    void DescribeServiceConfig(IOutputStream& out, const TServiceConfig& config, const TString& lineBegin /*= ""*/) {
        out << lineBegin << "Logbroker information" << Endl;
        out << lineBegin << "Installation: " << FormatLogbrokerName(config.GetLogbroker())
            << " (" << GetLogbrokerEndpoint(config.GetLogbroker()) << ")" << Endl;

        TVector<TString> dcs;
        for (auto dc : config.GetDataCenters()) {
            dcs.push_back(ToString(EDataCenter(dc)));
        }
        out << lineBegin << "Read from: " << JoinRange(",", dcs.begin(), dcs.end()) << Endl;
        out << lineBegin << "Topics path: " << config.GetTopicsPath() << Endl;
        out << lineBegin << "Consumers path: " << config.GetConsumersPath() << Endl;

        if (config.HasLogbrokerMirror()) {
            out << lineBegin << Endl
                << lineBegin << "Installation mirror: " << FormatLogbrokerName(config.GetLogbrokerMirror())
                << " (" << GetLogbrokerEndpoint(config.GetLogbrokerMirror()) << ")" << Endl;

            out << lineBegin << "Mirror topics path: " << config.GetMirrorTopicsPath() << Endl;
            out << lineBegin << "Mirror consumers path: " << config.GetMirrorConsumersPath() << Endl;
        }

        out << lineBegin << "YT clusters to delivery: " << JoinRange(",",
            config.GetYtDeliveryCluster().begin(),
            config.GetYtDeliveryCluster().end()) << Endl;

        out << lineBegin << Endl << lineBegin << "ZooKeeper settings for lock consumers" << Endl;
        out << lineBegin << "Path: " << config.GetLocksPath() << Endl;
        out << lineBegin << "Servers:" << Endl;
        for (const auto& s : SplitString(config.GetLocksServer(), ",")) {
            out << lineBegin << " - " << s << Endl;
        }

        TString backendsInfo = config.HasDeployManagerHost() ? "Deploy Manager" : (config.HasGencfg() ? "Gencfg" : " not set (incorrect config)");
        out << lineBegin << Endl << lineBegin << "Getting saas backends information from: " << backendsInfo << Endl;
        if (config.HasDeployManagerHost()) {
            out << lineBegin << "Deploy Manger: " << config.GetDeployManagerHost() << Endl;
        } else if (config.HasGencfg()) {
            for (auto& group : config.GetGencfg().GetGroups()) {
                out << lineBegin << " - " << group << Endl;
            }
        }
    }

    void PrintEvents(const NColorizer::TColors& colors, ui32 startCmdNumber, ui32 endCmdNumber, EChangeAction action, const TString& itemType, const TString& msg, bool printAction = true) {
        startCmdNumber++;
        endCmdNumber++;
        if (startCmdNumber == endCmdNumber) {
            Cout << "Command #" << startCmdNumber << ": ";
        } else {
            Cout << "Commands #" << startCmdNumber << "-" << endCmdNumber << ": ";
        }
        if (action == EChangeAction::CREATE) {
            Cout << colors.Green();
        } else if (action == EChangeAction::MODIFY) {
            Cout << colors.DarkGrayColor();
        } else if (action == EChangeAction::REMOVE) {
            Cout << colors.RedColor();
        } else {
            ythrow yexception() << "unsupported action: " << action;
        }
        TString actionStr = ToString(action);
        std::transform(actionStr.begin(), actionStr.vend(), actionStr.begin(), [](char c) { return std::tolower(c); });
        Cout << (printAction ? actionStr : "") << " " << itemType << colors.OldColor() << " " << msg << Endl;
    }

    void PrintEvent(const NColorizer::TColors& colors, ui32 cmdNumber, EChangeAction action, const TString& itemType, const TString& msg, bool printAction = true) {
        PrintEvents(colors, cmdNumber, cmdNumber, action, itemType, msg, printAction);
    }

    template <class TRequest>
    void PrintCreateLogbrokerRequest(const NColorizer::TColors& colors, ui32 cmdNumber, const TString& itemType, const TRequest& req) {
        PrintEvent(colors, cmdNumber, EChangeAction::CREATE, itemType, req.path().path());
    }

    template <class TRequest>
    void PrintRemoveLogbrokerRequest(const NColorizer::TColors& colors, ui32 cmdNumber, const TString& itemType, const TRequest& req) {
        PrintEvent(colors, cmdNumber, EChangeAction::REMOVE, itemType, req.path().path());
    }

    void PrintReadRulesRequest(
        const NColorizer::TColors& colors,
        ui32 startCmdNumber,
        ui32 endCmdNumber,
        EChangeAction action,
        const TString& topic,
        const TString& consumer,
        const TVector<TString>& clusters
    ) {
        TStringStream ss;
        ss << "\n\t - topic: " << topic << "\n\t - consumer: " << consumer;
        if (!clusters.empty()) {
            ss << "\n\t - cluster: " << JoinRange(",", clusters.begin(), clusters.end());
        }
        PrintEvents(colors, startCmdNumber, endCmdNumber, action, "read_rule", ss.Str());
    }

    void PrintRemoteRulesRequest(
        const NColorizer::TColors& colors,
        ui32 startCmdNumber,
        ui32 endCmdNumber,
        EChangeAction action,
        const TString& topic,
        const TVector<TString>& clusters,
        const TVector<TString>& originConsumers,
        const std::optional<TString>& originTopic
    ) {
        TStringStream ss;
        if (originTopic.has_value()) {
            ss << "\n\t - topic: " << originTopic.value();
        }
        if (!originConsumers.empty()) {
            ss << "\n\t - consumer:"
               << "\n\t\t - " << JoinRange("\n\t\t - ", originConsumers.begin(), originConsumers.end());
        }
        ss << "\n\t - mirror topic: " << topic;
        if (!clusters.empty()) {
            ss << "\n\t - mirror cluster: " << JoinRange(",", clusters.begin(), clusters.end());
        }
        PrintEvents(colors, startCmdNumber, endCmdNumber, action, "remote_mirror_rule", ss.Str());
    }

    template <class TRequest>
    void PrintPermissionLogbrokerRequest(const NColorizer::TColors& colors, ui32 cmdNumber, const TRequest& req, EChangeAction action) {
        TStringStream ss;
        ss << " for " << req.path().path();
        for (auto& p : req.permissions()) {
            Y_VERIFY(p.subject());
            ss << "\n - " << p.subject()<< " : " << JoinRange(",", p.permission_names().begin(), p.permission_names().end());
        }
        TString requestType;
        if (action == EChangeAction::CREATE) {
            requestType = "grant";
        } else if (action == EChangeAction::REMOVE) {
            requestType = "revoke";
        } else {
            ythrow yexception() << "unsuporrted action for permissions: " << action;
        }
        PrintEvent(colors, cmdNumber, action, requestType + " permissions", ss.Str(), false);
    }

    template <class TRequest>
    void PrintYtDeliveryRequest(const NColorizer::TColors& colors, ui32 cmdNumber, const TRequest& req, EChangeAction action) {
        TStringStream ss;
        ss << "\n\t - delivery: " << req.delivery() << "\n\t - topic: " << req.topic();
        PrintEvent(colors, cmdNumber, action, "yt_delivery", ss.Str());
    }

    template <class TRequests, class TFunc>
    ui32 PrintSameReadRules(
        const NColorizer::TColors& colors,
        const TRequests& requests,
        ui32 index,
        EChangeAction action,
        TFunc getReadRuleFunc
    ) {
        ui32 sameReadRulesCount = 0;
        TVector<TString> clusters;
        auto rr = getReadRuleFunc(requests[index]);
        for (i32 i = index; i < requests.size(); ++i) {
            auto nextRR = getReadRuleFunc(requests[i]);

            if (rr.topic().path() == nextRR.topic().path() && rr.consumer().path() == nextRR.consumer().path()) {
                sameReadRulesCount++;
                if (nextRR.has_mirror_to_cluster()) {
                    clusters.push_back(nextRR.mirror_to_cluster().cluster());
                }
            } else {
                break;
            }
        }
        PrintReadRulesRequest(
            colors,
            index,
            index + sameReadRulesCount - 1,
            action,
            rr.topic().path(),
            rr.consumer().path(),
            clusters
        );
        return sameReadRulesCount;
    }

    template <class TRequests, class TFunc>
    ui32 PrintSameRemoteMirrorRules(
        const NColorizer::TColors& colors,
        const TRequests& requests,
        ui32 index,
        EChangeAction action,
        TFunc getRuleFunc
    ) {
        const auto [ruleKey, ruleProps] = getRuleFunc(requests[index]);
        ui32 sameRulesCount = 0;

        TVector<TString> clusters;
        TVector<TString> originConsumers;
        std::optional<TString> originTopic;

        if (ruleProps.has_value()) {
            originTopic = GetPropertyValue<std::optional<TString>>(ruleProps.value().src_topic());
        }

        for (i32 i = index; i < requests.size(); ++i) {
            const auto [nextRuleKey, nextRuleProps] = getRuleFunc(requests[i]);
            if (ruleKey.topic().path() == nextRuleKey.topic().path()) {
                sameRulesCount++;
                clusters.push_back(nextRuleKey.cluster().cluster());

                if (nextRuleProps.has_value()) {
                    originConsumers.push_back(GetPropertyValue<TString>(nextRuleProps.value().src_consumer()));
                }
            } else {
                break;
            }
        }
        PrintRemoteRulesRequest(
            colors,
            index,
            index + sameRulesCount - 1,
            action,
            ruleKey.topic().path(),
            clusters,
            originConsumers,
            originTopic
        );
        return sameRulesCount;
    }

    template <class TRequests>
    void PrintRequests(const NColorizer::TColors& colors, const TRequests& requestsBatch);

    template <>
    void PrintRequests(const NColorizer::TColors& colors, const NSaasLB::TLogbrokerModifyRequests& requestsBatch) {
        auto requests = requestsBatch.GetRequest();
        ui32 requestsCount = requests.size();
        for (ui32 i = 0; i < requestsCount; ++i) {
            if (requests[i].has_create_consumer()) {
                PrintCreateLogbrokerRequest(colors, i, "consumer", requests[i].create_consumer());
            } else if (requests[i].has_create_topic()) {
                PrintCreateLogbrokerRequest(colors, i, "topic", requests[i].create_topic());
            } else if (requests[i].has_create_directory()) {
                PrintCreateLogbrokerRequest(colors, i, "directory", requests[i].create_directory());
            } else if (requests[i].has_remove_consumer()) {
                PrintRemoveLogbrokerRequest(colors, i, "consumer", requests[i].remove_consumer());
            } else if (requests[i].has_remove_topic()) {
                PrintRemoveLogbrokerRequest(colors, i, "topic", requests[i].remove_topic());
            } else if (requests[i].has_remove_directory()) {
                PrintRemoveLogbrokerRequest(colors, i, "directory", requests[i].remove_directory());
            } else if (requests[i].has_create_read_rule()) {
                ui32 cnt = PrintSameReadRules(colors, requests, i, EChangeAction::CREATE,
                    [](const NLogBroker::SingleModifyRequest& req) -> NLogBroker::ReadRuleKey {
                        return req.create_read_rule().read_rule();
                    }
                );
                i += cnt - 1;
            } else if (requests[i].has_remove_read_rule()) {
                ui32 cnt = PrintSameReadRules(colors, requests, i, EChangeAction::REMOVE,
                    [](const NLogBroker::SingleModifyRequest& req) -> NLogBroker::ReadRuleKey {
                        return req.remove_read_rule().read_rule();
                    }
                );
                i += cnt - 1;
            } else if (requests[i].has_create_remote_mirror_rule()) {
                ui32 cnt = PrintSameRemoteMirrorRules(colors, requests, i, EChangeAction::CREATE,
                    [](const NLogBroker::SingleModifyRequest& req) -> TMirrorRule {
                        return std::make_tuple(
                            req.create_remote_mirror_rule().remote_mirror_rule(),
                            req.create_remote_mirror_rule().properties()
                        );
                    }
                );
                i += cnt - 1;
            } else if (requests[i].has_remove_remote_mirror_rule()) {
                ui32 cnt = PrintSameRemoteMirrorRules(colors, requests, i, EChangeAction::REMOVE,
                    [](const NLogBroker::SingleModifyRequest& req) -> TMirrorRule {
                        const auto props = std::optional<TMirrorRuleProps>();
                        return std::make_tuple(req.remove_remote_mirror_rule().remote_mirror_rule(), props);
                    }
                );
                i += cnt - 1;
            } else if (requests[i].has_grant_permissions()) {
                PrintPermissionLogbrokerRequest(colors, i, requests[i].grant_permissions(), EChangeAction::CREATE);
            } else if (requests[i].has_revoke_permissions()) {
                PrintPermissionLogbrokerRequest(colors, i, requests[i].revoke_permissions(), EChangeAction::REMOVE);
            } else if (requests[i].has_add_topic_to_yt_delivery()) {
                PrintYtDeliveryRequest(colors, i, requests[i].add_topic_to_yt_delivery(), EChangeAction::CREATE);
            } else if (requests[i].has_remove_topic_from_yt_delivery()) {
                PrintYtDeliveryRequest(colors, i, requests[i].remove_topic_from_yt_delivery(), EChangeAction::REMOVE);
            } else {
                ythrow yexception() << "Unsupported logbroker command: " << requests[i].DebugString();
            }
        }
    }

    template <>
    void PrintRequests(const NColorizer::TColors& colors, const NSaasLB::TLocksModifyRequests& requestsBatch) {
        auto requests = requestsBatch.GetRequest();
        ui32 requestsCount = requests.size();
        for (ui32 i = 0; i < requestsCount; ++i) {
            TStringStream ss;
            ss << "for consumer: " + requests[i].GetLock().GetResource();
            if (requests[i].GetLock().HasZooKeeperLock()) {
                ss << "\n - zk node path: " << requests[i].GetLock().GetZooKeeperLock().GetNodePath();
            } else {
                ythrow yexception() << "unsupported lock type: " << requests[i].DebugString();
            }
            PrintEvent(colors, i, requests[i].GetAction(), "lock", ss.Str());
        }
    }

    template <>
    void PrintRequests(const NColorizer::TColors& colors, const NSaasLB::TServiceMetaModifyRequests& requestsBatch) {
        auto requests = requestsBatch.GetRequest();
        ui32 requestsCount = requests.size();
        for (ui32 i = 0; i < requestsCount; ++i) {
            TStringStream ss;
            ss << "for service";
            ss << "\n - config path: " << requests[i].GetPath();
            if (requests[i].GetAction() == EChangeAction::CREATE || requests[i].GetAction() == EChangeAction::MODIFY) {
                ss << "\n";
                DescribeServiceConfig(ss, requests[i].GetConfig(), "\t" + TString(NAsciiArt::LIST));
            }
            PrintEvent(colors, i, requests[i].GetAction(), "config", ss.Str());
        }
    }

    template <class T>
    void PrintChangeStage(const NColorizer::TColors& colors, ui32 stageIndex, TStringBuf name, const T& requestsBatch) {
        const auto& requests = requestsBatch.GetRequest();
        ui32 requestsCount = requests.size();
        Cout << colors.UnderlineOn() << "Stage #" << stageIndex + 1 << ": " << name
             << " with " << requestsCount << " command(s)." << colors.UnderlineOff() << Endl;
        PrintRequests(colors, requestsBatch);
    }

    void PrintServiceChanges(const NSaasLB::TServiceChanges& changes) {
        auto colors = NColorizer::AutoColors(Cout);
        bool addingMirrorRules = false;

        Cout << colors.UnderlineOn() << "All changes are divided into " << changes.StagesSize() << " stages." << colors.UnderlineOff() << Endl;
        for (ui32 stageIndex = 0; stageIndex < changes.StagesSize(); ++stageIndex) {
            auto& stage = changes.GetStages(stageIndex);
            if (stage.HasLogbroker()) {
                auto& logbroker = stage.GetLogbroker();
                const TString& name = "logbroker [" + GetLogbrokerEndpoint(logbroker.GetLogbroker()) + "]";
                PrintChangeStage(colors, stageIndex, name, logbroker);
            } else if (stage.HasLogbrokerMirror()) {
                auto& logbroker = stage.GetLogbrokerMirror();
                const TString& name = "logbroker mirror [" + GetLogbrokerEndpoint(logbroker.GetLogbroker()) + "]";
                PrintChangeStage(colors, stageIndex, name, stage.GetLogbrokerMirror());
                addingMirrorRules = addingMirrorRules || logbroker.GetAddingMirrorRules();
            } else if (stage.HasLocks()) {
                PrintChangeStage(colors, stageIndex, "locks", stage.GetLocks());
            } else if (stage.HasServiceMeta()) {
                PrintChangeStage(colors, stageIndex, "service_meta", stage.GetServiceMeta());
            } else {
                ythrow yexception() << "unknown changes stage: " << stage;
            }
        }

        if (addingMirrorRules) {
            Cout << colors.RedColor() << "\n"
                 << "CAUTION! Ensure that there are no other active remote mirror rules for your source topic.\n"
                 << "The tool can not handle this case automatically, you should remove old mirror rules by yourself.\n\n"
                 << colors.OldColor();
        }
    }
}
