/**
 * 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.
 */
#include "ClientImpl.h"

#include <pulsar/ClientConfiguration.h>
#include <pulsar/Version.h>

#include <algorithm>
#include <chrono>
#include <iterator>
#include <mutex>
#include <random>
#include <shared_mutex>
#include <sstream>
#include <variant>

#include "BinaryProtoLookupService.h"
#include "ClientConfigurationImpl.h"
#include "Commands.h"
#include "ConsumerImpl.h"
#include "ConsumerInterceptors.h"
#include "DefaultServiceInfoProvider.h"
#include "ExecutorService.h"
#include "HTTPLookupService.h"
#include "LogUtils.h"
#include "MultiTopicsConsumerImpl.h"
#include "PartitionedProducerImpl.h"
#include "PatternMultiTopicsConsumerImpl.h"
#include "ProducerImpl.h"
#include "ProducerInterceptors.h"
#include "ReaderImpl.h"
#include "RetryableLookupService.h"
#include "TableViewImpl.h"
#include "TimeUtils.h"
#include "TopicName.h"

#ifdef PULSAR_USE_BOOST_REGEX
#include <boost/regex.hpp>
#define PULSAR_REGEX_NAMESPACE boost
#else
#include <regex>
#define PULSAR_REGEX_NAMESPACE std
#endif

DECLARE_LOG_OBJECT()

namespace pulsar {

namespace {

template <typename T>
void invokeLegacyCallback(const std::function<void(Result, T)>& callback,
                          const std::variant<Error, T>& value) {
    if (const auto* error = std::get_if<Error>(&value)) {
        callback(error->result, T());
    } else {
        callback(ResultOk, std::get<T>(value));
    }
}

}  // namespace

static const char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7',
                                 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
static std::uniform_int_distribution<> hexDigitsDist(0, sizeof(hexDigits) - 1);
static std::mt19937 randomEngine =
    std::mt19937(std::chrono::high_resolution_clock::now().time_since_epoch().count());

std::string generateRandomName() {
    const int randomNameLength = 10;

    std::string randomName;
    for (int i = 0; i < randomNameLength; ++i) {
        randomName += hexDigits[hexDigitsDist(randomEngine)];
    }
    return randomName;
}

typedef std::vector<std::string> StringList;

static LookupServicePtr defaultLookupServiceFactory(const ServiceInfo& serviceInfo,
                                                    const ClientConfiguration& clientConfiguration,
                                                    ConnectionPool& pool) {
    if (ServiceNameResolver::useHttp(ServiceURI(serviceInfo.serviceUrl()))) {
        LOG_DEBUG("Using HTTP Lookup");
        return std::make_shared<HTTPLookupService>(std::cref(serviceInfo), std::cref(clientConfiguration));
    } else {
        LOG_DEBUG("Using Binary Lookup");
        return std::make_shared<BinaryProtoLookupService>(std::cref(serviceInfo), std::ref(pool),
                                                          std::cref(clientConfiguration));
    }
}

ClientImpl::ClientImpl(const std::string& serviceUrl, const ClientConfiguration& clientConfiguration)
    : ClientImpl(serviceUrl, clientConfiguration, &defaultLookupServiceFactory) {}

ClientImpl::ClientImpl(const std::string& serviceUrl, const ClientConfiguration& clientConfiguration,
                       LookupServiceFactory&& lookupServiceFactory)
    : ClientImpl(std::make_unique<DefaultServiceInfoProvider>(std::cref(serviceUrl),
                                                              std::cref(*clientConfiguration.impl_)),
                 clientConfiguration, std::move(lookupServiceFactory)) {}

ClientImpl::ClientImpl(std::unique_ptr<ServiceInfoProvider> serviceInfoProvider,
                       const ClientConfiguration& clientConfiguration)
    : ClientImpl(std::move(serviceInfoProvider), clientConfiguration, &defaultLookupServiceFactory) {}

ClientImpl::ClientImpl(std::unique_ptr<ServiceInfoProvider> serviceInfoProvider,
                       const ClientConfiguration& clientConfiguration,
                       LookupServiceFactory&& lookupServiceFactory)
    : serviceInfoProvider_(std::move(serviceInfoProvider)),
      state_(Open),
      clientConfiguration_(clientConfiguration),
      serviceInfo_(serviceInfoProvider_->initialServiceInfo()),
      memoryLimitController_(clientConfiguration.getMemoryLimit()),
      ioExecutorProvider_(std::make_shared<ExecutorServiceProvider>(clientConfiguration_.getIOThreads())),
      listenerExecutorProvider_(
          std::make_shared<ExecutorServiceProvider>(clientConfiguration_.getMessageListenerThreads())),
      partitionListenerExecutorProvider_(
          std::make_shared<ExecutorServiceProvider>(clientConfiguration_.getMessageListenerThreads())),
      pool_(serviceInfo_, clientConfiguration_, ioExecutorProvider_,
            ClientImpl::getClientVersion(clientConfiguration)),
      producerIdGenerator_(0),
      consumerIdGenerator_(0),
      closingError(ResultOk),
      useProxy_(false),
      lookupCount_(0L),
      lookupServiceFactory_(std::move(lookupServiceFactory)) {
    std::unique_ptr<LoggerFactory> loggerFactory = clientConfiguration_.impl_->takeLogger();
    if (loggerFactory) {
        LogUtils::setLoggerFactory(std::move(loggerFactory));
    }

    lookupServicePtr_ = createLookup(*serviceInfo_.load());
}

ClientImpl::~ClientImpl() { shutdown(); }

void ClientImpl::initialize() {
    auto weakSelf = weak_from_this();
    serviceInfoProvider_->initialize([weakSelf](ServiceInfo serviceInfo) {
        if (auto self = weakSelf.lock()) {
            self->updateServiceInfo(std::move(serviceInfo));
        }
    });
}

LookupServicePtr ClientImpl::createLookup(ServiceInfo serviceInfo) {
    auto lookupServicePtr = RetryableLookupService::create(
        lookupServiceFactory_(std::move(serviceInfo), clientConfiguration_, pool_),
        clientConfiguration_.impl_->operationTimeout, ioExecutorProvider_);
    return lookupServicePtr;
}

const ClientConfiguration& ClientImpl::conf() const { return clientConfiguration_; }

MemoryLimitController& ClientImpl::getMemoryLimitController() { return memoryLimitController_; }

ExecutorServiceProviderPtr ClientImpl::getIOExecutorProvider() { return ioExecutorProvider_; }

ExecutorServiceProviderPtr ClientImpl::getListenerExecutorProvider() { return listenerExecutorProvider_; }

ExecutorServiceProviderPtr ClientImpl::getPartitionListenerExecutorProvider() {
    return partitionListenerExecutorProvider_;
}

LookupServicePtr ClientImpl::getLookup(const std::string& redirectedClusterURI) {
    std::shared_lock readLock(mutex_);
    if (redirectedClusterURI.empty()) {
        return lookupServicePtr_;
    }

    if (auto it = redirectedClusterLookupServicePtrs_.find(redirectedClusterURI);
        it != redirectedClusterLookupServicePtrs_.end()) {
        return it->second;
    }
    readLock.unlock();

    std::unique_lock writeLock(mutex_);
    // Double check in case another thread acquires the lock and inserts a pair first
    if (auto it = redirectedClusterLookupServicePtrs_.find(redirectedClusterURI);
        it != redirectedClusterLookupServicePtrs_.end()) {
        return it->second;
    }
    auto lookup = createRedirectedLookup(redirectedClusterURI);
    redirectedClusterLookupServicePtrs_.emplace(redirectedClusterURI, lookup);
    return lookup;
}

void ClientImpl::createProducerAsync(const std::string& topic, const ProducerConfiguration& conf,
                                     CreateProducerV2Callback callback, bool autoDownloadSchema) {
    if (conf.isChunkingEnabled() && conf.getBatchingEnabled()) {
        throw std::invalid_argument("Batching and chunking of messages can't be enabled together");
    }

    TopicNamePtr topicName;
    {
        std::shared_lock lock(mutex_);
        if (state_ != Open) {
            lock.unlock();
            callback(Error{ResultAlreadyClosed, ""});
            return;
        } else if (!(topicName = TopicName::get(topic))) {
            lock.unlock();
            // TODO: return an error in TopicName
            callback(Error{ResultInvalidTopicName, ""});
            return;
        }
    }

    if (autoDownloadSchema) {
        getSchema(topicName).addListener([self{shared_from_this()}, topicName, callback{std::move(callback)}](
                                             const Error& error, const SchemaInfo& topicSchema) mutable {
            if (error.result != ResultOk) {
                callback(error);
                return;
            }
            ProducerConfiguration conf;
            conf.setSchema(topicSchema);
            self->getPartitionMetadataAsync(topicName).addListener(
                std::bind(&ClientImpl::handleCreateProducer, self, std::placeholders::_1,
                          std::placeholders::_2, topicName, conf, callback));
        });
    } else {
        getPartitionMetadataAsync(topicName).addListener(
            [this, conf, topicName, callback{std::move(callback)}](
                const Error& error, const LookupDataResultPtr& partitionMetadata) {
                handleCreateProducer(error, partitionMetadata, topicName, conf, callback);
            });
    }
}

void ClientImpl::handleCreateProducer(const Error& error, const LookupDataResultPtr& partitionMetadata,
                                      const TopicNamePtr& topicName, const ProducerConfiguration& conf,
                                      CreateProducerV2Callback callback) {
    if (!error.result) {
        ProducerImplBasePtr producer;

        auto interceptors = std::make_shared<ProducerInterceptors>(conf.getInterceptors());

        try {
            if (partitionMetadata->getPartitions() > 0) {
                producer = std::make_shared<PartitionedProducerImpl>(
                    shared_from_this(), topicName, partitionMetadata->getPartitions(), conf, interceptors);
            } else {
                producer = std::make_shared<ProducerImpl>(shared_from_this(), *topicName, conf, interceptors);
            }
        } catch (const std::runtime_error& e) {
            LOG_ERROR("Failed to create producer: " << e.what());
            callback(Error{ResultConnectError, e.what()});
            return;
        }
        producer->getProducerCreatedFuture().addListener(
            [this, self{shared_from_this()}, callback{std::move(callback)}, producer](
                const Error& error, const ProducerImplBaseWeakPtr& producerBaseWeakPtr) {
                handleProducerCreated(error, producerBaseWeakPtr, callback, producer);
            });
        producer->start();
    } else {
        LOG_ERROR("Error Checking/Getting Partition Metadata while creating producer on "
                  << topicName->toString() << " -- " << error.result);
        callback(error);
    }
}

void ClientImpl::handleProducerCreated(const Error& error, const ProducerImplBaseWeakPtr& producerBaseWeakPtr,
                                       const CreateProducerV2Callback& callback,
                                       const ProducerImplBasePtr& producer) {
    if (error.result == ResultOk) {
        auto address = producer.get();
        auto existingProducer = producers_.putIfAbsent(address, producer);
        if (existingProducer) {
            auto producer = existingProducer.value().lock();
            const auto name = producer ? producer->getProducerName() : "(null)";
            LOG_ERROR("Unexpected existing producer at the same address: " << address
                                                                           << ", producer: " << name);
            callback(Error{ResultUnknownError, "Unexpected existing producer for name " + name});
            return;
        }
        callback(Producer(producer));
    } else {
        callback(error);
    }
}

void ClientImpl::createReaderAsync(const std::string& topic, const MessageId& startMessageId,
                                   const ReaderConfiguration& conf, const ReaderCallback& callback) {
    createReaderAsyncV2(topic, startMessageId, conf,
                        [callback](const auto& value) { invokeLegacyCallback<Reader>(callback, value); });
}

void ClientImpl::createReaderAsyncV2(const std::string& topic, const MessageId& startMessageId,
                                     const ReaderConfiguration& conf, ReaderV2Callback callback) {
    TopicNamePtr topicName;
    {
        std::shared_lock lock(mutex_);
        if (state_ != Open) {
            lock.unlock();
            callback(Error{ResultAlreadyClosed, ""});
            return;
        } else if (!(topicName = TopicName::get(topic))) {
            lock.unlock();
            callback(Error{ResultInvalidTopicName, ""});
            return;
        }
    }

    getPartitionMetadataAsync(topicName).addListener(
        [this, self{shared_from_this()}, topicName, startMessageId, conf, callback{std::move(callback)}](
            const auto& error, const auto& metadata) {
            handleReaderMetadataLookup(error, metadata, topicName, startMessageId, conf, callback);
        });
}

void ClientImpl::createTableViewAsync(const std::string& topic, const TableViewConfiguration& conf,
                                      const TableViewCallback& callback) {
    createTableViewAsyncV2(
        topic, conf, [callback](const auto& value) { invokeLegacyCallback<TableView>(callback, value); });
}

void ClientImpl::createTableViewAsyncV2(const std::string& topic, const TableViewConfiguration& conf,
                                        TableViewV2Callback callback) {
    TopicNamePtr topicName;
    {
        std::shared_lock lock(mutex_);
        if (state_ != Open) {
            lock.unlock();
            callback(Error{ResultAlreadyClosed, ""});
            return;
        } else if (!(topicName = TopicName::get(topic))) {
            lock.unlock();
            callback(Error{ResultInvalidTopicName, ""});
            return;
        }
    }

    TableViewImplPtr tableViewPtr =
        std::make_shared<TableViewImpl>(shared_from_this(), topicName->toString(), conf);
    tableViewPtr->start().addListener(
        [callback{std::move(callback)}](const Error& error, const TableViewImplPtr& tableViewImplPtr) {
            if (error.result == ResultOk) {
                callback(TableView{tableViewImplPtr});
            } else {
                callback(error);
            }
        });
}

void ClientImpl::handleReaderMetadataLookup(const Error& error, const LookupDataResultPtr& partitionMetadata,
                                            const TopicNamePtr& topicName, const MessageId& startMessageId,
                                            const ReaderConfiguration& conf,
                                            const ReaderV2Callback& callback) {
    if (error.result != ResultOk) {
        LOG_ERROR("Error Checking/Getting Partition Metadata while creating readeron "
                  << topicName->toString() << " -- " << error.result);
        callback(error);
        return;
    }

    ReaderImplPtr reader;
    try {
        ReaderCallback readerCreatedCallback = [callback](Result result, const Reader& reader) {
            if (result == ResultOk) {
                callback(reader);
            } else {
                callback(Error{result, ""});
            }
        };
        reader.reset(new ReaderImpl(shared_from_this(), topicName->toString(),
                                    partitionMetadata->getPartitions(), conf,
                                    getListenerExecutorProvider()->get(), readerCreatedCallback));
    } catch (const std::runtime_error& e) {
        LOG_ERROR("Failed to create reader: " << e.what());
        callback(Error{ResultConnectError, e.what()});
        return;
    }
    ConsumerImplBasePtr consumer = reader->getConsumer();
    auto self = shared_from_this();
    reader->start(startMessageId, [this, self](const ConsumerImplBaseWeakPtr& weakConsumerPtr) {
        auto consumer = weakConsumerPtr.lock();
        if (consumer) {
            auto address = consumer.get();
            auto existingConsumer = consumers_.putIfAbsent(address, consumer);
            if (existingConsumer) {
                consumer = existingConsumer.value().lock();
                LOG_ERROR("Unexpected existing consumer at the same address: "
                          << address << ", consumer: " << (consumer ? consumer->getName() : "(null)"));
            }
        } else {
            LOG_ERROR("Unexpected case: the consumer is somehow expired");
        }
    });
}

void ClientImpl::subscribeWithRegexAsync(const std::string& regexPattern, const std::string& subscriptionName,
                                         const ConsumerConfiguration& conf,
                                         const SubscribeCallback& callback) {
    subscribeAsyncV2(TopicRegex{regexPattern}, subscriptionName, conf,
                     [callback](const auto& value) { invokeLegacyCallback<Consumer>(callback, value); });
}

void ClientImpl::subscribeAsyncV2(const SubscribeTopics& topics, const std::string& subscriptionName,
                                  const ConsumerConfiguration& conf, SubscribeV2Callback callback) {
    std::visit(
        [this, &subscriptionName, &conf, callback{std::move(callback)}](const auto& topicSpec) mutable {
            subscribeToTopicsAsyncV2(topicSpec, subscriptionName, conf, std::move(callback));
        },
        topics);
}

void ClientImpl::subscribeToTopicsAsyncV2(const TopicRegex& topicRegex, const std::string& subscriptionName,
                                          const ConsumerConfiguration& conf, SubscribeV2Callback callback) {
    const auto regexPattern = topicRegex.pattern;
    TopicNamePtr topicNamePtr = TopicName::get(regexPattern);

    std::shared_lock lock(mutex_);
    if (state_ != Open) {
        lock.unlock();
        callback(Error{ResultAlreadyClosed, ""});
        return;
    } else {
        lock.unlock();
        if (!topicNamePtr) {
            LOG_ERROR("Topic pattern not valid: " << regexPattern);
            callback(Error{ResultInvalidTopicName, ""});
            return;
        }
    }

    if (TopicName::containsDomain(regexPattern)) {
        LOG_WARN("Ignore invalid domain: "
                 << topicNamePtr->getDomain()
                 << ", use the RegexSubscriptionMode parameter to set the topic type");
    }

    CommandGetTopicsOfNamespace_Mode mode;
    auto regexSubscriptionMode = conf.getRegexSubscriptionMode();
    switch (regexSubscriptionMode) {
        case PersistentOnly:
            mode = CommandGetTopicsOfNamespace_Mode_PERSISTENT;
            break;
        case NonPersistentOnly:
            mode = CommandGetTopicsOfNamespace_Mode_NON_PERSISTENT;
            break;
        case AllTopics:
            mode = CommandGetTopicsOfNamespace_Mode_ALL;
            break;
        default:
            LOG_ERROR("RegexSubscriptionMode not valid: " << regexSubscriptionMode);
            callback(Error{ResultInvalidConfiguration, ""});
            return;
    }

    getTopicsOfNamespaceAsync(topicNamePtr->getNamespaceName(), mode)
        .addListener([self{shared_from_this()}, regexPattern, mode, subscriptionName, conf,
                      callback{std::move(callback)}](Result result, const NamespaceTopicsPtr& topics) {
            self->createPatternMultiTopicsConsumer(result, topics, regexPattern, mode, subscriptionName, conf,
                                                   callback);
        });
}

void ClientImpl::createPatternMultiTopicsConsumer(Result result, const NamespaceTopicsPtr& topics,
                                                  const std::string& regexPattern,
                                                  CommandGetTopicsOfNamespace_Mode mode,
                                                  const std::string& subscriptionName,
                                                  const ConsumerConfiguration& conf,
                                                  SubscribeV2Callback callback) {
    if (result == ResultOk) {
        ConsumerImplBasePtr consumer;

        PULSAR_REGEX_NAMESPACE::regex pattern(TopicName::removeDomain(regexPattern));

        NamespaceTopicsPtr matchTopics =
            PatternMultiTopicsConsumerImpl::topicsPatternFilter(*topics, pattern);

        auto interceptors = std::make_shared<ConsumerInterceptors>(conf.getInterceptors());

        consumer = std::make_shared<PatternMultiTopicsConsumerImpl>(
            shared_from_this(), regexPattern, mode, *matchTopics, subscriptionName, conf, interceptors);

        consumer->getConsumerCreatedFuture().addListener(
            [self{shared_from_this()}, callback{std::move(callback)}, consumer](
                Result result, const ConsumerImplBaseWeakPtr& consumerImplBaseWeakPtr) {
                self->handleConsumerCreated(result, consumerImplBaseWeakPtr, callback, consumer);
            });
        consumer->start();
    } else {
        LOG_ERROR("Error Getting topicsOfNameSpace while createPatternMultiTopicsConsumer:  " << result);
        callback(Error{result, ""});
    }
}

void ClientImpl::subscribeAsync(const std::vector<std::string>& originalTopics,
                                const std::string& subscriptionName, const ConsumerConfiguration& conf,
                                const SubscribeCallback& callback) {
    subscribeAsyncV2(originalTopics, subscriptionName, conf,
                     [callback](const auto& value) { invokeLegacyCallback<Consumer>(callback, value); });
}

void ClientImpl::subscribeToTopicsAsyncV2(const std::vector<std::string>& originalTopics,
                                          const std::string& subscriptionName,
                                          const ConsumerConfiguration& conf, SubscribeV2Callback callback) {
    TopicNamePtr topicNamePtr;

    // Remove duplicates from the list of topics
    auto topics = originalTopics;
    std::sort(topics.begin(), topics.end());
    auto it = std::unique(topics.begin(), topics.end());
    auto newSize = std::distance(topics.begin(), it);
    topics.resize(newSize);
    std::shared_lock lock(mutex_);
    if (state_ != Open) {
        lock.unlock();
        callback(Error{ResultAlreadyClosed, ""});
        return;
    } else {
        if (!topics.empty() && !(topicNamePtr = MultiTopicsConsumerImpl::topicNamesValid(topics))) {
            lock.unlock();
            callback(Error{ResultInvalidTopicName, ""});
            return;
        }
    }
    lock.unlock();

    if (topicNamePtr) {
        std::string randomName = generateRandomName();
        std::stringstream consumerTopicNameStream;
        consumerTopicNameStream << topicNamePtr->toString() << "-TopicsConsumerFakeName-" << randomName;
        topicNamePtr = TopicName::get(consumerTopicNameStream.str());
    }

    auto interceptors = std::make_shared<ConsumerInterceptors>(conf.getInterceptors());

    ConsumerImplBasePtr consumer = std::make_shared<MultiTopicsConsumerImpl>(
        shared_from_this(), topics, subscriptionName, topicNamePtr, conf, interceptors);

    consumer->getConsumerCreatedFuture().addListener(
        [self{shared_from_this()}, callback{std::move(callback)}, consumer](
            Result result, const ConsumerImplBaseWeakPtr& consumerImplBaseWeakPtr) {
            self->handleConsumerCreated(result, consumerImplBaseWeakPtr, callback, consumer);
        });
    consumer->start();
}

void ClientImpl::subscribeAsync(const std::string& topic, const std::string& subscriptionName,
                                const ConsumerConfiguration& conf, const SubscribeCallback& callback) {
    subscribeAsyncV2(topic, subscriptionName, conf,
                     [callback](const auto& value) { invokeLegacyCallback<Consumer>(callback, value); });
}

void ClientImpl::subscribeToTopicsAsyncV2(const std::string& topic, const std::string& subscriptionName,
                                          const ConsumerConfiguration& conf, SubscribeV2Callback callback) {
    LOG_INFO("Subscribing on Topic :" << topic);
    TopicNamePtr topicName;
    {
        std::shared_lock lock(mutex_);
        if (state_ != Open) {
            lock.unlock();
            callback(Error{ResultAlreadyClosed, ""});
            return;
        } else if (!(topicName = TopicName::get(topic))) {
            lock.unlock();
            callback(Error{ResultInvalidTopicName, ""});
            return;
        } else if (conf.isReadCompacted() && (topicName->getDomain().compare("persistent") != 0 ||
                                              (conf.getConsumerType() != ConsumerExclusive &&
                                               conf.getConsumerType() != ConsumerFailover))) {
            lock.unlock();
            callback(Error{ResultInvalidConfiguration, ""});
            return;
        }
    }

    getPartitionMetadataAsync(topicName).addListener(
        [this, self{shared_from_this()}, topicName, subscriptionName, conf, callback{std::move(callback)}](
            const auto& error, const auto& metadata) {
            handleSubscribe(error, metadata, topicName, subscriptionName, conf, callback);
        });
}

void ClientImpl::handleSubscribe(const Error& error, const LookupDataResultPtr& partitionMetadata,
                                 const TopicNamePtr& topicName, const std::string& subscriptionName,
                                 ConsumerConfiguration conf, SubscribeV2Callback callback) {
    if (error.result == ResultOk) {
        // generate random name if not supplied by the customer.
        if (conf.getConsumerName().empty()) {
            conf.setConsumerName(generateRandomName());
        }
        ConsumerImplBasePtr consumer;
        auto interceptors = std::make_shared<ConsumerInterceptors>(conf.getInterceptors());

        try {
            if (partitionMetadata->getPartitions() > 0) {
                if (conf.getReceiverQueueSize() == 0) {
                    LOG_ERROR("Can't use partitioned topic if the queue size is 0.");
                    callback(Error{ResultInvalidConfiguration, ""});
                    return;
                }
                consumer = std::make_shared<MultiTopicsConsumerImpl>(shared_from_this(), topicName,
                                                                     partitionMetadata->getPartitions(),
                                                                     subscriptionName, conf, interceptors);
            } else {
                auto consumerImpl = std::make_shared<ConsumerImpl>(shared_from_this(), topicName->toString(),
                                                                   subscriptionName, conf,
                                                                   topicName->isPersistent(), interceptors);
                consumerImpl->setPartitionIndex(topicName->getPartitionIndex());
                consumer = consumerImpl;
            }
        } catch (const std::runtime_error& e) {
            LOG_ERROR("Failed to create consumer: " << e.what());
            callback(Error{ResultConnectError, e.what()});
            return;
        }
        consumer->getConsumerCreatedFuture().addListener(
            [self{shared_from_this()}, callback{std::move(callback)}, consumer](
                Result result, const ConsumerImplBaseWeakPtr& consumerImplBaseWeakPtr) {
                self->handleConsumerCreated(result, consumerImplBaseWeakPtr, callback, consumer);
            });
        consumer->start();
    } else {
        LOG_ERROR("Error Checking/Getting Partition Metadata while Subscribing on "
                  << topicName->toString() << " -- " << error.result);
        callback(error);
    }
}

void ClientImpl::handleConsumerCreated(Result result, const ConsumerImplBaseWeakPtr& consumerImplBaseWeakPtr,
                                       const SubscribeV2Callback& callback,
                                       const ConsumerImplBasePtr& consumer) {
    if (result == ResultOk) {
        auto address = consumer.get();
        auto existingConsumer = consumers_.putIfAbsent(address, consumer);
        if (existingConsumer) {
            auto consumer = existingConsumer.value().lock();
            LOG_ERROR("Unexpected existing consumer at the same address: "
                      << address << ", consumer: " << (consumer ? consumer->getName() : "(null)"));
            callback(Error{ResultUnknownError, ""});
            return;
        }
        callback(Consumer(consumer));
    } else {
        // In order to be compatible with the current broker error code confusion.
        // https://github.com/apache/pulsar/blob/cd2aa550d0fe4e72b5ff88c4f6c1c2795b3ff2cd/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/BrokerServiceException.java#L240-L241
        if (result == ResultProducerBusy) {
            LOG_ERROR("Failed to create consumer: SubscriptionName cannot be empty.");
            callback(Error{ResultInvalidConfiguration, "SubscriptionName cannot be empty."});
        } else {
            callback(Error{result, ""});
        }
    }
}

GetConnectionFuture ClientImpl::getConnection(const std::string& redirectedClusterURI,
                                              const std::string& topic, size_t key) {
    Promise<Result, ClientConnectionPtr> promise;

    const auto topicNamePtr = TopicName::get(topic);
    if (!topicNamePtr) {
        LOG_ERROR("Unable to parse topic - " << topic);
        promise.setFailed(ResultInvalidTopicName);
        return promise.getFuture();
    }

    auto self = shared_from_this();
    getLookup(redirectedClusterURI)
        ->getBroker(*topicNamePtr)
        .addListener([this, self, promise, key](Result result, const LookupService::LookupResult& data) {
            if (result != ResultOk) {
                promise.setFailed(result);
                return;
            }
            useProxy_ = data.proxyThroughServiceUrl;
            lookupCount_++;
            pool_.getConnectionAsync(data.logicalAddress, data.physicalAddress, key)
                .addListener([promise](const auto& error, const ClientConnectionWeakPtr& weakCnx) {
                    if (error.result == ResultOk) {
                        auto cnx = weakCnx.lock();
                        if (cnx) {
                            promise.setValue(cnx);
                        } else {
                            promise.setFailed(ResultConnectError);
                        }
                    } else {
                        promise.setFailed(error.result);
                    }
                });
        });

    return promise.getFuture();
}

const std::string& ClientImpl::getPhysicalAddress(const std::string& redirectedClusterURI,
                                                  const std::string& logicalAddress) {
    if (useProxy_) {
        return getLookup(redirectedClusterURI)->getServiceNameResolver().resolveHost();
    } else {
        return logicalAddress;
    }
}

GetConnectionFuture ClientImpl::connect(const std::string& redirectedClusterURI,
                                        const std::string& logicalAddress, size_t key) {
    const auto& physicalAddress = getPhysicalAddress(redirectedClusterURI, logicalAddress);
    Promise<Result, ClientConnectionPtr> promise;
    pool_.getConnectionAsync(logicalAddress, physicalAddress, key)
        .addListener([promise](const auto& error, const ClientConnectionWeakPtr& weakCnx) {
            if (error.result == ResultOk) {
                auto cnx = weakCnx.lock();
                if (cnx) {
                    promise.setValue(cnx);
                } else {
                    promise.setFailed(ResultConnectError);
                }
            } else {
                promise.setFailed(error.result);
            }
        });
    return promise.getFuture();
}

void ClientImpl::handleGetPartitions(Result result, const LookupDataResultPtr& partitionMetadata,
                                     const TopicNamePtr& topicName, const GetPartitionsCallback& callback) {
    if (result != ResultOk) {
        LOG_ERROR("Error getting topic partitions metadata: " << result);
        callback(result, StringList());
        return;
    }

    StringList partitions;

    if (partitionMetadata->getPartitions() > 0) {
        for (unsigned int i = 0; i < partitionMetadata->getPartitions(); i++) {
            partitions.push_back(topicName->getTopicPartitionName(i));
        }
    } else {
        partitions.push_back(topicName->toString());
    }

    callback(ResultOk, partitions);
}

void ClientImpl::getPartitionsForTopicAsync(const std::string& topic, const GetPartitionsCallback& callback) {
    TopicNamePtr topicName;
    {
        std::shared_lock lock(mutex_);
        if (state_ != Open) {
            lock.unlock();
            callback(ResultAlreadyClosed, StringList());
            return;
        } else if (!(topicName = TopicName::get(topic))) {
            lock.unlock();
            callback(ResultInvalidTopicName, StringList());
            return;
        }
    }
    getPartitionMetadataAsync(topicName).addListener(
        [this, self{shared_from_this()}, topicName, callback](const auto& error, const auto& metadata) {
            handleGetPartitions(error.result, metadata, topicName, callback);
        });
}

void ClientImpl::closeAsync(const CloseCallback& callback) {
    serviceInfoProvider_.reset();
    std::unique_lock lock(mutex_);
    if (state_ != Open) {
        lock.unlock();
        if (callback) {
            callback(ResultAlreadyClosed);
        }
        return;
    }
    // Set the state to Closing so that no producers could get added
    state_ = Closing;

    memoryLimitController_.close();
    lookupServicePtr_->close();
    for (const auto& it : redirectedClusterLookupServicePtrs_) {
        it.second->close();
    }
    redirectedClusterLookupServicePtrs_.clear();
    lock.unlock();

    auto producers = producers_.move();
    auto consumers = consumers_.move();

    SharedInt numberOfOpenHandlers = std::make_shared<int>(producers.size() + consumers.size());
    LOG_INFO("Closing Pulsar client with " << producers.size() << " producers and " << consumers.size()
                                           << " consumers");

    for (auto&& kv : producers) {
        ProducerImplBasePtr producer = kv.second.lock();
        if (producer && !producer->isClosed()) {
            producer->closeAsync(std::bind(&ClientImpl::handleClose, shared_from_this(),
                                           std::placeholders::_1, numberOfOpenHandlers, callback));
        } else {
            // Since the connection is already closed
            (*numberOfOpenHandlers)--;
        }
    }

    for (auto&& kv : consumers) {
        ConsumerImplBasePtr consumer = kv.second.lock();
        if (consumer && !consumer->isClosed()) {
            consumer->closeAsync(std::bind(&ClientImpl::handleClose, shared_from_this(),
                                           std::placeholders::_1, numberOfOpenHandlers, callback));
        } else {
            // Since the connection is already closed
            (*numberOfOpenHandlers)--;
        }
    }

    if (*numberOfOpenHandlers == 0 && callback) {
        handleClose(ResultOk, numberOfOpenHandlers, callback);
    }
    lookupCount_ = 0;
}

void ClientImpl::handleClose(Result result, const SharedInt& numberOfOpenHandlers,
                             const ResultCallback& callback) {
    Result expected = ResultOk;
    if (!closingError.compare_exchange_strong(expected, result)) {
        LOG_DEBUG("Tried to updated closingError, but already set to "
                  << expected << ". This means multiple errors have occurred while closing the client");
    }

    if (*numberOfOpenHandlers > 0) {
        --(*numberOfOpenHandlers);
    }
    if (*numberOfOpenHandlers == 0) {
        std::unique_lock lock(mutex_);
        if (state_ == Closed) {
            LOG_DEBUG("Client is already shutting down, possible race condition in handleClose");
            return;
        } else {
            state_ = Closed;
            lock.unlock();
        }

        LOG_DEBUG("Shutting down producers and consumers for client");
        // handleClose() is called in ExecutorService's event loop, while shutdown() tried to wait the
        // event loop exits. So here we use another thread to call shutdown().
        auto self = shared_from_this();
        std::thread shutdownTask{[this, self, callback] {
            shutdown();
            if (callback) {
                if (closingError != ResultOk) {
                    LOG_DEBUG(
                        "Problem in closing client, could not close one or more consumers or producers");
                }
                callback(closingError);
            }
        }};
        shutdownTask.detach();
    }
}

void ClientImpl::shutdown() {
    auto producers = producers_.move();
    auto consumers = consumers_.move();

    for (auto&& kv : producers) {
        ProducerImplBasePtr producer = kv.second.lock();
        if (producer) {
            producer->shutdown();
        }
    }

    for (auto&& kv : consumers) {
        ConsumerImplBasePtr consumer = kv.second.lock();
        if (consumer) {
            consumer->shutdown();
        }
    }

    if (producers.size() + consumers.size() > 0) {
        LOG_DEBUG(producers.size() << " producers and " << consumers.size()
                                   << " consumers have been shutdown.");
    }

    std::shared_lock lock(mutex_);
    lookupServicePtr_->close();
    lock.unlock();
    if (!pool_.close()) {
        // pool_ has already been closed. It means shutdown() has been called before.
        return;
    }
    LOG_DEBUG("ConnectionPool is closed");

    // 500ms as the timeout is long enough because ExecutorService::close calls io_service::stop()
    // internally and waits until io_service::run() in another thread returns, which should be as soon as
    // possible after stop() is called.
    TimeoutProcessor<std::chrono::milliseconds> timeoutProcessor{500};

    timeoutProcessor.tik();
    ioExecutorProvider_->close(timeoutProcessor.getLeftTimeout());
    timeoutProcessor.tok();
    LOG_DEBUG("ioExecutorProvider_ is closed");

    timeoutProcessor.tik();
    listenerExecutorProvider_->close(timeoutProcessor.getLeftTimeout());
    timeoutProcessor.tok();
    LOG_DEBUG("listenerExecutorProvider_ is closed");

    timeoutProcessor.tik();
    partitionListenerExecutorProvider_->close(timeoutProcessor.getLeftTimeout());
    timeoutProcessor.tok();
    LOG_DEBUG("partitionListenerExecutorProvider_ is closed");
    lookupCount_ = 0;
}

uint64_t ClientImpl::newProducerId() { return producerIdGenerator_++; }

uint64_t ClientImpl::newConsumerId() { return consumerIdGenerator_++; }

uint64_t ClientImpl::newRequestId() { return (*requestIdGenerator_)++; }

uint64_t ClientImpl::getNumberOfProducers() {
    uint64_t numberOfAliveProducers = 0;
    producers_.forEachValue([&numberOfAliveProducers](const ProducerImplBaseWeakPtr& producer) {
        const auto& producerImpl = producer.lock();
        if (producerImpl) {
            numberOfAliveProducers += producerImpl->getNumberOfConnectedProducer();
        }
    });
    return numberOfAliveProducers;
}

uint64_t ClientImpl::getNumberOfConsumers() {
    uint64_t numberOfAliveConsumers = 0;
    consumers_.forEachValue([&numberOfAliveConsumers](const ConsumerImplBaseWeakPtr& consumer) {
        const auto consumerImpl = consumer.lock();
        if (consumerImpl) {
            numberOfAliveConsumers += consumerImpl->getNumberOfConnectedConsumer();
        }
    });
    return numberOfAliveConsumers;
}

const ClientConfiguration& ClientImpl::getClientConfig() const { return clientConfiguration_; }

std::string ClientImpl::getClientVersion(const ClientConfiguration& clientConfiguration) {
    std::ostringstream oss;
    oss << "Pulsar-CPP-v" << PULSAR_VERSION_STR;
    if (!clientConfiguration.getDescription().empty()) {
        oss << "-" << clientConfiguration.getDescription();
    }
    return oss.str();
}

std::chrono::nanoseconds ClientImpl::getOperationTimeout(const ClientConfiguration& clientConfiguration) {
    return clientConfiguration.impl_->operationTimeout;
}

void ClientImpl::updateServiceInfo(ServiceInfo&& serviceInfo) {
    std::unique_lock lock{mutex_};
    if (state_ != Open) {
        LOG_ERROR("Client is not open, cannot update service info");
        return;
    }

    serviceInfo_.store(std::make_shared<const ServiceInfo>(serviceInfo));
    pool_.closeAllConnectionsForNewCluster();
    if (lookupServicePtr_) {
        lookupServicePtr_->close();
    }
    lookupServicePtr_ = createLookup(serviceInfo);

    for (auto&& it : redirectedClusterLookupServicePtrs_) {
        it.second->close();
    }
    redirectedClusterLookupServicePtrs_.clear();
    useProxy_ = false;
    lookupCount_ = 0;
}

ServiceInfo ClientImpl::getServiceInfo() const { return *(serviceInfo_.load()); }

} /* namespace pulsar */
