/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
#ifndef LIB_CLIENTIMPL_H_
#define LIB_CLIENTIMPL_H_

#include <pulsar/Client.h>
#include <pulsar/ServiceInfo.h>
#include <pulsar/ServiceInfoProvider.h>

#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <shared_mutex>

#include "AtomicSharedPtr.h"
#include "ConnectionPool.h"
#include "Future.h"
#include "LookupDataResult.h"
#include "LookupService.h"
#include "MemoryLimitController.h"
#include "ProtoApiEnums.h"
#include "SynchronizedHashMap.h"

namespace pulsar {

class PulsarFriend;
class ClientImpl;
typedef std::shared_ptr<ClientImpl> ClientImplPtr;
typedef std::weak_ptr<ClientImpl> ClientImplWeakPtr;

class ReaderImpl;
typedef std::shared_ptr<ReaderImpl> ReaderImplPtr;
typedef std::weak_ptr<ReaderImpl> ReaderImplWeakPtr;

class TableViewImpl;
typedef std::shared_ptr<TableViewImpl> TableViewImplPtr;

class ConsumerImplBase;
typedef std::weak_ptr<ConsumerImplBase> ConsumerImplBaseWeakPtr;

class ClientConnection;
using ClientConnectionPtr = std::shared_ptr<ClientConnection>;

using LookupServiceFactory = std::function<LookupServicePtr(
    ServiceInfo&& serviceInfo, const ClientConfiguration&, ConnectionPool& pool)>;

class ProducerImplBase;
using ProducerImplBaseWeakPtr = std::weak_ptr<ProducerImplBase>;
class ConsumerImplBase;
using ConsumerImplBaseWeakPtr = std::weak_ptr<ConsumerImplBase>;
class TopicName;
using TopicNamePtr = std::shared_ptr<TopicName>;

using NamespaceTopicsPtr = std::shared_ptr<std::vector<std::string>>;
using GetConnectionFuture = Future<Result, ClientConnectionPtr>;

std::string generateRandomName();

class ClientImpl : public std::enable_shared_from_this<ClientImpl> {
   public:
    ClientImpl(std::unique_ptr<ServiceInfoProvider> serviceInfoProvider,
               const ClientConfiguration& clientConfiguration);
    ClientImpl(std::unique_ptr<ServiceInfoProvider> serviceInfoProvider,
               const ClientConfiguration& clientConfiguration, LookupServiceFactory&& lookupServiceFactory);
    ClientImpl(const std::string& serviceUrl, const ClientConfiguration& clientConfiguration);

    // only for tests
    ClientImpl(const std::string& serviceUrl, const ClientConfiguration& clientConfiguration,
               LookupServiceFactory&& lookupServiceFactory);

    void initialize();
    virtual ~ClientImpl();

    /**
     * @param autoDownloadSchema When it is true, Before creating a producer, it will try to get the schema
     * that exists for the topic.
     */
    void createProducerAsync(const std::string& topic, const ProducerConfiguration& conf,
                             CreateProducerV2Callback callback, bool autoDownloadSchema = false);

    void subscribeAsync(const std::string& topic, const std::string& subscriptionName,
                        const ConsumerConfiguration& conf, const SubscribeCallback& callback);

    void subscribeAsyncV2(const SubscribeTopics& topics, const std::string& subscriptionName,
                          const ConsumerConfiguration& conf, SubscribeV2Callback callback);

    void subscribeAsync(const std::vector<std::string>& topics, const std::string& subscriptionName,
                        const ConsumerConfiguration& conf, const SubscribeCallback& callback);

    void subscribeWithRegexAsync(const std::string& regexPattern, const std::string& subscriptionName,
                                 const ConsumerConfiguration& conf, const SubscribeCallback& callback);

    void createReaderAsync(const std::string& topic, const MessageId& startMessageId,
                           const ReaderConfiguration& conf, const ReaderCallback& callback);

    void createReaderAsyncV2(const std::string& topic, const MessageId& startMessageId,
                             const ReaderConfiguration& conf, ReaderV2Callback callback);

    void createTableViewAsync(const std::string& topic, const TableViewConfiguration& conf,
                              const TableViewCallback& callback);

    void createTableViewAsyncV2(const std::string& topic, const TableViewConfiguration& conf,
                                TableViewV2Callback callback);

    void getPartitionsForTopicAsync(const std::string& topic, const GetPartitionsCallback& callback);

    // Use virtual method to test
    virtual GetConnectionFuture getConnection(const std::string& redirectedClusterURI,
                                              const std::string& topic, size_t key);

    GetConnectionFuture connect(const std::string& redirectedClusterURI, const std::string& logicalAddress,
                                size_t key);

    void closeAsync(const CloseCallback& callback);
    void shutdown();

    MemoryLimitController& getMemoryLimitController();

    uint64_t newProducerId();
    uint64_t newConsumerId();
    uint64_t newRequestId();

    uint64_t getNumberOfProducers();
    uint64_t getNumberOfConsumers();

    const ClientConfiguration& getClientConfig() const;

    const ClientConfiguration& conf() const;
    ExecutorServiceProviderPtr getIOExecutorProvider();
    ExecutorServiceProviderPtr getListenerExecutorProvider();
    ExecutorServiceProviderPtr getPartitionListenerExecutorProvider();

    void cleanupProducer(ProducerImplBase* address) { producers_.remove(address); }

    void cleanupConsumer(ConsumerImplBase* address) { consumers_.remove(address); }

    std::shared_ptr<std::atomic<uint64_t>> getRequestIdGenerator() const { return requestIdGenerator_; }

    ConnectionPool& getConnectionPool() noexcept { return pool_; }
    uint64_t getLookupCount() { return lookupCount_; }

    void updateServiceInfo(ServiceInfo&& serviceInfo);
    ServiceInfo getServiceInfo() const;

    // Since the underlying `lookupServicePtr_` can be modified by `updateServiceInfo`, we should not expose
    // it to other classes, otherwise the update might not be visible.
    auto getPartitionMetadataAsync(const TopicNamePtr& topicName) {
        std::shared_lock lock(mutex_);
        return lookupServicePtr_->getPartitionMetadataAsync(topicName);
    }

    auto getTopicsOfNamespaceAsync(const NamespaceNamePtr& nsName, CommandGetTopicsOfNamespace_Mode mode) {
        std::shared_lock lock(mutex_);
        return lookupServicePtr_->getTopicsOfNamespaceAsync(nsName, mode);
    }
    auto getSchema(const TopicNamePtr& topicName, const std::string& version = "") {
        std::shared_lock lock(mutex_);
        return lookupServicePtr_->getSchema(topicName, version);
    }

    static std::chrono::nanoseconds getOperationTimeout(const ClientConfiguration& clientConfiguration);

    friend class PulsarFriend;

   private:
    void handleCreateProducer(const Error& error, const LookupDataResultPtr& partitionMetadata,
                              const TopicNamePtr& topicName, const ProducerConfiguration& conf,
                              CreateProducerV2Callback callback);

    void handleSubscribe(const Error& error, const LookupDataResultPtr& partitionMetadata,
                         const TopicNamePtr& topicName, const std::string& consumerName,
                         ConsumerConfiguration conf, SubscribeV2Callback callback);

    void subscribeToTopicsAsyncV2(const std::string& topic, const std::string& subscriptionName,
                                  const ConsumerConfiguration& conf, SubscribeV2Callback callback);

    void subscribeToTopicsAsyncV2(const std::vector<std::string>& topics, const std::string& subscriptionName,
                                  const ConsumerConfiguration& conf, SubscribeV2Callback callback);

    void subscribeToTopicsAsyncV2(const TopicRegex& topicRegex, const std::string& subscriptionName,
                                  const ConsumerConfiguration& conf, SubscribeV2Callback callback);

    void handleReaderMetadataLookup(const Error& error, const LookupDataResultPtr& partitionMetadata,
                                    const TopicNamePtr& topicName, const MessageId& startMessageId,
                                    const ReaderConfiguration& conf, const ReaderV2Callback& callback);

    void handleGetPartitions(Result result, const LookupDataResultPtr& partitionMetadata,
                             const TopicNamePtr& topicName, const GetPartitionsCallback& callback);

    void handleProducerCreated(const Error& error, const ProducerImplBaseWeakPtr& producerWeakPtr,
                               const CreateProducerV2Callback& callback, const ProducerImplBasePtr& producer);
    void handleConsumerCreated(Result result, const ConsumerImplBaseWeakPtr& consumerWeakPtr,
                               const SubscribeV2Callback& callback, const ConsumerImplBasePtr& consumer);

    typedef std::shared_ptr<int> SharedInt;

    void handleClose(Result result, const SharedInt& remaining, const ResultCallback& callback);

    void createPatternMultiTopicsConsumer(Result result, const NamespaceTopicsPtr& topics,
                                          const std::string& regexPattern,
                                          CommandGetTopicsOfNamespace_Mode mode,
                                          const std::string& consumerName, const ConsumerConfiguration& conf,
                                          SubscribeV2Callback callback);

    const std::string& getPhysicalAddress(const std::string& redirectedClusterURI,
                                          const std::string& logicalAddress);

    // This overload is only used for blue-green migration, where only the service URL is modified, the other
    // parameters remain the same
    LookupServicePtr createRedirectedLookup(const std::string& redirectedUrl) {
        auto serviceInfo = serviceInfo_.load();
        return createLookup(
            ServiceInfo{redirectedUrl, serviceInfo->authentication(), serviceInfo->tlsTrustCertsFilePath()});
    }

    LookupServicePtr createLookup(ServiceInfo serviceInfo);

    LookupServicePtr getLookup(const std::string& redirectedClusterURI);

    static std::string getClientVersion(const ClientConfiguration& clientConfiguration);

    enum State : uint8_t
    {
        Open,
        Closing,
        Closed
    };

    std::unique_ptr<ServiceInfoProvider> serviceInfoProvider_;
    mutable std::shared_mutex mutex_;

    State state_;
    ClientConfiguration clientConfiguration_;
    AtomicSharedPtr<ServiceInfo> serviceInfo_;
    MemoryLimitController memoryLimitController_;

    ExecutorServiceProviderPtr ioExecutorProvider_;
    ExecutorServiceProviderPtr listenerExecutorProvider_;
    ExecutorServiceProviderPtr partitionListenerExecutorProvider_;

    LookupServicePtr lookupServicePtr_;
    std::unordered_map<std::string, LookupServicePtr> redirectedClusterLookupServicePtrs_;
    ConnectionPool pool_;

    std::atomic_uint64_t producerIdGenerator_;
    std::atomic_uint64_t consumerIdGenerator_;
    std::shared_ptr<std::atomic<uint64_t>> requestIdGenerator_{std::make_shared<std::atomic<uint64_t>>(0)};

    SynchronizedHashMap<ProducerImplBase*, ProducerImplBaseWeakPtr> producers_;
    SynchronizedHashMap<ConsumerImplBase*, ConsumerImplBaseWeakPtr> consumers_;

    std::atomic<Result> closingError;
    std::atomic<bool> useProxy_;
    std::atomic<uint64_t> lookupCount_;
    LookupServiceFactory lookupServiceFactory_;

    friend class Client;
};
} /* namespace pulsar */

#endif /* LIB_CLIENTIMPL_H_ */
