/**
 * 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.
 */
#pragma once

#include <chrono>
#include <mutex>
#include <unordered_map>

#include "ExecutorService.h"
#include "RetryableOperation.h"

namespace pulsar {

template <typename T>
class RetryableOperationCache;

template <typename T>
using RetryableOperationCachePtr = std::shared_ptr<RetryableOperationCache<T>>;

template <typename T>
class RetryableOperationCache : public std::enable_shared_from_this<RetryableOperationCache<T>> {
    friend class LookupServiceTest;
    friend class RetryableOperationCacheTest;
    struct PassKey {
        explicit PassKey() {}
    };

    RetryableOperationCache(ExecutorServiceProviderPtr executorProvider, TimeDuration timeout)
        : executorProvider_(executorProvider), timeout_(timeout) {}

    using Self = RetryableOperationCache<T>;

   public:
    template <typename... Args>
    explicit RetryableOperationCache(PassKey, Args&&... args)
        : RetryableOperationCache(std::forward<Args>(args)...) {}

    template <typename... Args>
    static std::shared_ptr<Self> create(Args&&... args) {
        return std::make_shared<Self>(PassKey{}, std::forward<Args>(args)...);
    }

    Future<Error, T> run(const std::string& key, std::function<Future<Error, T>()>&& func) {
        std::unique_lock<std::mutex> lock{mutex_};
        if (closed_) {
            Promise<Error, T> promise;
            promise.setFailed({ResultAlreadyClosed, ""});
            return promise.getFuture();
        }
        auto it = operations_.find(key);
        if (it == operations_.end()) {
            DeadlineTimerPtr timer;
            try {
                timer = executorProvider_->get()->createDeadlineTimer();
            } catch (const std::runtime_error& e) {
                LOG_ERROR("Failed to retry lookup for " << key << ": " << e.what());
                Promise<Error, T> promise;
                promise.setFailed({ResultConnectError, e.what()});
                return promise.getFuture();
            }

            auto operation = RetryableOperation<T>::create(key, std::move(func), timeout_, timer);
            auto future = operation->run();
            operations_[key] = operation;
            lock.unlock();

            auto weakSelf = this->weak_from_this();
            future.addListener([this, weakSelf, key, operation](const Error&, const T&) {
                auto self = weakSelf.lock();
                if (!self) {
                    return;
                }
                std::lock_guard<std::mutex> lock{mutex_};
                operations_.erase(key);
                operation->cancel();
            });

            return future;
        } else {
            return it->second->run();
        }
    }

    void close() {
        decltype(operations_) operations;
        {
            std::lock_guard<std::mutex> lock{mutex_};
            if (closed_) {
                return;
            }
            operations.swap(operations_);
            closed_ = true;
        }
        // cancel() could trigger the listener to erase the key from operations, so we should use a swap way
        // to release the lock here
        for (auto&& kv : operations) {
            kv.second->cancel();
        }
    }

   private:
    ExecutorServiceProviderPtr executorProvider_;
    const TimeDuration timeout_;

    std::unordered_map<std::string, std::shared_ptr<RetryableOperation<T>>> operations_;
    bool closed_{false};
    mutable std::mutex mutex_;

    DECLARE_LOG_OBJECT()
};

}  // namespace pulsar
