#pragma once

#include "host.h"
#include "slots_pool.h"

#include <saas/protos/cluster.pb.h>
#include <saas/library/sharding/sharding.h>
#include <saas/util/types/coverage.h>

#include <library/cpp/json/writer/json_value.h>
#include <library/cpp/object_factory/object_factory.h>

#include <util/datetime/base.h>
#include <util/generic/map.h>
#include <util/generic/string.h>
#include <utility>

namespace NSearchMapParser {
    struct TReplicaSpecificOptions {
        TString Alias;
        TString Features;
        TDuration UpdateFrequency;
        bool DisableSearch;
        bool DisableIndexing;

        bool Deserialize(const NSaasProto::TConfigType& configType) {
            Alias = configType.GetName();
            Features = configType.GetFeatures();
            UpdateFrequency = TDuration::Seconds(configType.GetUpdateFrequency());
            DisableSearch = configType.GetDisableSearch();
            DisableIndexing = configType.GetDisableIndexing();
            return true;
        }

        NSaasProto::TConfigType SerializeToProto() const {
            NSaasProto::TConfigType result;
            result.SetName(Alias);
            result.SetFeatures(Features);
            result.SetUpdateFrequency(UpdateFrequency.Seconds());
            result.SetDisableSearch(DisableSearch);
            result.SetDisableIndexing(DisableIndexing);
            return result;
        }
    };

    struct TMetaServiceComponent {
        TString Name;
        ui32 Tier;
    };

    struct TMetaServiceSpecificOptions {
        TVector<TMetaServiceComponent> Components;
    };

    struct TServiceSpecificOptions {
    private:
        TString Servers;
    public:
        const TString& GetServers() const {
            return Servers;
        }
        const TString& GetDistributors() const {
            return Servers;
        }
        void SetServers(const TString& servers) {
            Servers = servers;
        }
    public:
        typedef THashMap<TString, ui32> TWeightByAliasTo;
        typedef THashMap<TString, TWeightByAliasTo> TWeightsByAliasFrom;
        bool RequireAuth = false;
        TString SearchProtocol = "proto";
        DispatchTarget IndexingTarget = Backends;
        TString Stream;
        bool PerDcSearch = false;
        bool SkipPing = false;
        NSaas::TShardsDispatcher::TPtr ShardsDispatcher = MakeAtomicShared<NSaas::TShardsDispatcher>(NSaas::TShardsDispatcher::TContext(NSaas::UrlHash));
        ui32 NumTopics = 0;
        TWeightsByAliasFrom WeightsByAliasFrom;

        TShardIndex GetShardsMax() const;

        bool Deserialize(const NSaasProto::TService& service) {
            RequireAuth = service.GetRequireAuth();
            SearchProtocol = service.GetSearchProtocol();
            ShardsDispatcher.Reset(new NSaas::TShardsDispatcher(NSaas::TShardsDispatcher::TContext::FromProto(service)));

            switch (service.GetIndexingTarget()) {
            case NSaasProto::TService::Backends:
                IndexingTarget = NSearchMapParser::Backends;
                break;
            case NSaasProto::TService::Distributor:
                IndexingTarget = NSearchMapParser::Distributor;
                break;
            case NSaasProto::TService::PersQueue:
                IndexingTarget = NSearchMapParser::PersQueue;
                break;
            case NSaasProto::TService::Void:
                IndexingTarget = NSearchMapParser::Void;
                break;
            default:
                ythrow yexception() << "Unknown IndexingTarget type";
            }
            for (const auto& fromTo : service.GetAliasesWeights()) {
                WeightsByAliasFrom[fromTo.GetFrom()][fromTo.GetTo()] = fromTo.GetWeight();
            }
            Servers = service.GetDistributors();
            Stream = service.GetStream();
            NumTopics = service.GetNumTopics();
            PerDcSearch = service.GetPerDcSearch();
            SkipPing = service.GetSkipPing();
            return true;
        }

        NSaasProto::TService SerializeToProto() const {
            NSaasProto::TService result;
            result.SetRequireAuth(RequireAuth);
            result.SetSearchProtocol(SearchProtocol);
            ShardsDispatcher->GetContext().ToProto(result);
            switch (IndexingTarget) {
            case NSearchMapParser::Backends:
                result.SetIndexingTarget(NSaasProto::TService::Backends);
                break;
            case NSearchMapParser::Distributor:
                result.SetIndexingTarget(NSaasProto::TService::Distributor);
                break;
            case NSearchMapParser::PersQueue:
                result.SetIndexingTarget(NSaasProto::TService::PersQueue);
                break;
            case NSearchMapParser::Void:
                result.SetIndexingTarget(NSaasProto::TService::Void);
                break;
            default:
                ythrow yexception() << "Unknown IndexingTarget type";
            }

            result.SetDistributors(Servers);
            result.SetStream(Stream);
            result.SetNumTopics(NumTopics);
            result.SetPerDcSearch(PerDcSearch);
            if (SkipPing) {
                result.SetSkipPing(true);
            } else {
                result.ClearSkipPing();
            }

            for (const auto& from : WeightsByAliasFrom) {
                for (const auto& to : from.second) {
                    auto& weight = *result.AddAliasesWeights();
                    weight.SetFrom(from.first);
                    weight.SetTo(to.first);
                    weight.SetWeight(to.second);
                }
            }

            return result;
        }
    };

    struct TSearchReplica
        : public NUtil::TCoverage<TShardIndex, TSearchInformation>
        , public TReplicaSpecificOptions
    {
        inline TSearchReplica(TShardIndex shards, const TReplicaSpecificOptions& options)
            : NUtil::TCoverage<TShardIndex, TSearchInformation>(0, shards)
            , TReplicaSpecificOptions(options)
        {}
    };

    class ISearchMapProcessor;

    struct TSearchCluster: public TServiceSpecificOptions {
        TVector<TSearchReplica> Replicas;

        inline TSearchCluster(const TServiceSpecificOptions& options)
            : TServiceSpecificOptions(options)
        {}

        TVector<TSearchInformation> GetAllHosts() const;
        void ProcessSearchMap(ISearchMapProcessor& processor) const;

        void NormalizeIntervals();
    };

    class ISearchMapProcessor {
    public:
        virtual ~ISearchMapProcessor() {}
        virtual bool ProcessService(const TServiceSpecificOptions& /* service options */) {
            return true;
        }
        virtual void Do(
            const TServiceSpecificOptions& /* service options */,
            const TReplicaSpecificOptions& /* replica options*/,
            const TInterval<TShardIndex>& /* interval */,
            const TSearchInformation& /* host */) = 0;
    };

    struct TSearchMapReplica: public TReplicaSpecificOptions, public TSlotsPool
    {
        TSearchMapReplica() {
            SetDefaults();
        }
        void SetDefaults();
        bool IsDefault() const;

        bool Deserialize(const NJson::TJsonValue& value);
        bool Deserialize(const NSaasProto::TConfigType& configType);
        NSaasProto::TConfigType SerializeToProto() const;
        NJson::TJsonValue Serialize() const;

        TVector<TSearchMapHost> GetSlotsIntersect(NSearchMapParser::TShardIndex shardMin, NSearchMapParser::TShardIndex shardMax) const;
        TVector<TSearchMapHost> GetSlotsContainedIn(NSearchMapParser::TShardIndex shardMin, NSearchMapParser::TShardIndex shardMax) const;

    };

    class TMetaComponent {
    public:
        typedef TVector<TMetaComponent> TVectorType;
    public:
        TMetaComponent()
            : SearchPriority(0)
            , SearchDisabled(false)
            , IndexDisabled(false)
            , IndexReplyIgnoring(false)
        {}
        TMetaComponent(const TString& name, bool searchDisabled = false, ui32 searchPriority = 0, bool indexDisabled = false, bool indexReplyIgnoring = false)
            : ServiceName(name)
            , SearchPriority(searchPriority)
            , SearchDisabled(searchDisabled)
            , IndexDisabled(indexDisabled)
            , IndexReplyIgnoring(indexReplyIgnoring)
        {}

        TString ServiceName;
        ui32 SearchPriority;
        bool SearchDisabled;
        bool IndexDisabled;
        bool IndexReplyIgnoring;

        bool Deserialize(const NSaasProto::TMetaComponent& component) {
            ServiceName = component.GetServiceName();
            SearchPriority = component.GetSearchPriority();
            SearchDisabled = component.GetSearchDisabled();
            IndexDisabled = component.GetIndexDisabled();
            IndexReplyIgnoring = component.GetIndexReplyIgnoring();
            return true;
        }

        bool Deserialize(const NJson::TJsonValue& info) {
            ServiceName = info["ServiceName"].GetStringRobust();
            SearchPriority = info["SearchPriority"].GetIntegerRobust();
            SearchDisabled = info["SearchDisabled"].GetBooleanRobust();
            IndexDisabled = info["IndexDisabled"].GetBooleanRobust();
            IndexReplyIgnoring = info["IndexReplyIgnoring"].GetBooleanRobust();
            return true;
        }

        NJson::TJsonValue SerializeToJson() const {
            NJson::TJsonValue result;
            result.InsertValue("ServiceName", ServiceName);
            result.InsertValue("SearchPriority", SearchPriority);
            result.InsertValue("SearchDisabled", SearchDisabled);
            result.InsertValue("IndexDisabled", IndexDisabled);
            result.InsertValue("IndexReplyIgnoring", IndexReplyIgnoring);
            return result;
        }

        NSaasProto::TMetaComponent SerializeToProto() const {
            NSaasProto::TMetaComponent result;
            result.SetServiceName(ServiceName);
            result.SetSearchPriority(SearchPriority);
            result.SetSearchDisabled(SearchDisabled);
            result.SetIndexDisabled(IndexDisabled);
            result.SetIndexReplyIgnoring(IndexReplyIgnoring);
            return result;
        }
    };

    class TMetaService: public TServiceSpecificOptions {
    public:
        typedef TVector<TMetaService> TVectorType;
    public:
        TString Name;
        TMetaComponent::TVectorType Components;
        bool Deserialize(const NJson::TJsonValue& info);

        NJson::TJsonValue SerializeToJson() const;

        const TMetaComponent* GetComponent(const TString& compName) const {
            for (auto&& i : Components) {
                if (i.ServiceName == compName)
                    return &i;
            }
            return nullptr;
        }

        bool Deserialize(const NSaasProto::TMetaService& service) {
            if (!TServiceSpecificOptions::Deserialize(service.GetServiceInfo()))
                return false;
            Name = service.GetServiceInfo().GetName();
            for (ui32 i = 0; i < service.ComponentsSize(); ++i) {
                TMetaComponent component;
                if (!component.Deserialize(service.GetComponents(i))) {
                    return false;
                }
                Components.push_back(component);
            }
            return true;
        }

        NSaasProto::TMetaService SerializeToProto() const {
            NSaasProto::TMetaService result;
            *result.MutableServiceInfo() = TServiceSpecificOptions::SerializeToProto();
            result.MutableServiceInfo()->SetName(Name);
            for (auto& i : Components) {
                *result.AddComponents() = i.SerializeToProto();
            }
            return result;
        }
    };

    class TSearchMapService: public TServiceSpecificOptions, public ISlotsPool {
    public:
        typedef TVector<TSearchMapService> TVectorType;
    public:
        TString Name;
        ui16 SearchPort;
        ui16 IndexerPort;
        TVector<TSearchMapReplica> Replicas;

        TSearchMapService() {
            SetDefaults();
        }
        void SetDefaults();

        virtual TVector<TSearchMapHost> GetSlots() const override;

        bool Deserialize(const NJson::TJsonValue& value);
        bool Deserialize(const NSaasProto::TService& service);
        NJson::TJsonValue Serialize() const;
        NSaasProto::TService SerializeToProto() const;

        template<class C>
        void Sort(C sorter) {
            for (auto& r : Replicas) {
                r.Sort(sorter);
            }
        }

        TSearchMapReplica* GetReplica(const TString& configType);
        const TSearchMapReplica* GetReplica(const TString& configType) const;
        TSearchMapReplica* AddReplica(const TString& configType);
    };

    class ISearchMapScannerCallback {
    public:
        virtual ~ISearchMapScannerCallback() {}

        virtual bool OnService(const NSearchMapParser::TSearchMapService& /*info*/) {
            return true;
        }

        virtual void OnMetaService(const NSearchMapParser::TMetaService& /*info*/) {
        }

        virtual void OnConfType(const NSearchMapParser::TSearchMapReplica& /*replica*/, const NSearchMapParser::TSearchMapService& /*service*/) {}
        virtual void OnHost(const NSearchMapParser::TSearchMapHost& host, const NSearchMapParser::TSearchMapReplica& replica, const NSearchMapParser::TSearchMapService& service) = 0;
        virtual void OnEndpointSet(const NSearchMapParser::TSearchMapHost& /*host*/, const NSearchMapParser::TSearchMapReplica& /*replica*/, const NSearchMapParser::TSearchMapService& /*service*/) {};
    };

    class TSearchMap: public ISlotsPool {
    public:
        typedef TMap<TString, TSearchCluster> TServiceMap;

    public:
        TSearchMap() {}
        TSearchMap(const TVector<TSearchMapService>& internalSearchMap, const TVector<TMetaService> metaSearchMap);

        template<class C>
        void Sort(C sorter) {
            for (auto& s : InternalSearchMap) {
                s.Sort(sorter);
            }
        }

        const TMetaService* GetMetaService(const TString& serviceName) const {
            for (auto&& i : MetaServices) {
                if (i.Name == serviceName) {
                    return &i;
                }
            }
            return nullptr;
        }

        bool HasService(const TString& service) const {
            if (GetServiceMap().contains(service))
                return true;
            for (auto&& i : MetaServices) {
                if (i.Name == service)
                    return true;
            }
            return false;
        }

        TString SerializeToJson() const;
        NJson::TJsonValue SerializeToJsonObject() const;

        void Compile(bool checkIndex, bool checkSearch);

        const TSearchMapService* GetService(const TString& name) const {
            for (ui32 i = 0; i < InternalSearchMap.size(); ++i) {
                if (InternalSearchMap[i].Name == name) {
                    return &InternalSearchMap[i];
                }
            }
            return nullptr;
        }

        TSearchMapService* GetService(const TString& name) {
            for (ui32 i = 0; i < InternalSearchMap.size(); ++i) {
                if (InternalSearchMap[i].Name == name) {
                    return &InternalSearchMap[i];
                }
            }
            return nullptr;
        }

        void Scan(ISearchMapScannerCallback& callback) const {
            for (const auto& i : InternalSearchMap) {
                if (callback.OnService(i))
                    for (ui32 r = 0; r < i.Replicas.size(); ++r) {
                        callback.OnConfType(i.Replicas[r], i);
                        for (ui32 h = 0; h < i.Replicas[r].Hosts.size(); ++h) {
                            callback.OnHost(i.Replicas[r].Hosts[h], i.Replicas[r], i);
                        }
                        for (ui32 h = 0; h < i.Replicas[r].EndPointSets.size(); ++h) {
                            callback.OnEndpointSet(i.Replicas[r].EndPointSets[h], i.Replicas[r], i);
                        }
                    }
            }

            for (auto& i : MetaServices) {
                callback.OnMetaService(i);
            }
        }

        TSearchMapService& AddService(const TString& name) {
            TSearchMapService* result = GetService(name);
            if (!result) {
                TSearchMapService newService;
                newService.Name = name;
                InternalSearchMap.push_back(newService);
                result = &InternalSearchMap.back();
            }
            return *result;
        }

        virtual TVector<TSearchMapHost> GetSlots() const {
            TVector<TSearchMapHost> result;
            for (TVector<TSearchMapService>::const_iterator i = InternalSearchMap.begin(); i != InternalSearchMap.end(); ++i) {
                const TVector<TSearchMapHost>& replicaSlots = i->GetSlots();
                Copy(replicaSlots.begin(), replicaSlots.end(), inserter(result, result.end()));
            }
            return result;
        }

        const TServiceMap& GetServiceMap() const {
            return ServiceMap;
        }

        TServiceMap* MutableServiceMap() {
            return &ServiceMap;
        }

        const TSearchMapService::TVectorType& GetInternalSearchMap() const {
            return InternalSearchMap;
        }

        TSearchMapService::TVectorType& GetInternalSearchMap() {
            return InternalSearchMap;
        }

        const TMetaService::TVectorType& GetMetaServices() const {
            return MetaServices;
        }

        TMetaService::TVectorType& GetMetaServices() {
            return MetaServices;
        }

        void Clear() {
            InternalSearchMap.clear();
            MetaServices.clear();
            ServiceMap.clear();
        }

    private:
        TSearchMapService::TVectorType InternalSearchMap;
        TMetaService::TVectorType MetaServices;

        TServiceMap ServiceMap;
    };

    class TSearchMapModifier : public ISearchMapScannerCallback {
    public:
        TSearchMapModifier();
        virtual void OnMetaService(const NSearchMapParser::TMetaService& info) override;
        virtual bool OnService(const TSearchMapService& service) override;
        virtual void OnConfType(const TSearchMapReplica& replica, const TSearchMapService& /*service*/) override;
        virtual void OnHost(const TSearchMapHost& host, const TSearchMapReplica& replica, const TSearchMapService& service) override;
        virtual void OnEndpointSet(const TSearchMapHost& host, const TSearchMapReplica& replica, const TSearchMapService& service) override;

        // return false - do not add service to result searchmap;
        virtual bool ModifyService(TSearchMapService& service, const TSearchMapService& originalService);
        // return false - do not add replica to service;
        virtual bool ModifyReplica(TSearchMapReplica& replica, const TSearchMapReplica& originalReplica, const TSearchMapService& originalService);
        // return false - do not add host to replica;
        virtual bool ModifyHost(TSearchMapHost& host, const TSearchMapReplica& originalReplica, const TSearchMapService& originalService) = 0;
        // return false - do not add host to replica;
        virtual bool ModifyEndpointSet(TSearchMapHost& /*host*/, const TSearchMapReplica& /*originalReplica*/, const TSearchMapService& /*originalService*/) { return true; };

        TSearchMap& Process(TSearchMap sm);

    private:
        void CloseService();
        void CloseReplica();
        TSearchMapService NewService;
        const TSearchMapService* OriginalService;
        TSearchMapReplica NewReplica;
        const TSearchMapReplica* OriginalReplica;
        TSearchMap Result;
    };

}
