/**
 * 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 <initializer_list>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>

#include "ClientConnection.h"
#include "ConsumerImpl.h"
#include "ExecutorService.h"
#include "LogUtils.h"
#include "PulsarApi.pb.h"

namespace pulsar {

class MockServer : public std::enable_shared_from_this<MockServer> {
   public:
    using RequestDelayType = std::unordered_map<std::string, long /* delay in milliseconds */>;

    MockServer(const ClientConnectionPtr& connection) : connection_(connection) {
        requestDelays_["CLOSE_CONSUMER"] = 1;
    }

    void setRequestDelay(std::initializer_list<typename RequestDelayType::value_type> delays) {
        std::lock_guard<std::mutex> lock(mutex_);
        for (auto&& delay : delays) {
            requestDelays_[delay.first] = delay.second;
        }
    }

    bool sendRequest(const std::string& request, uint64_t requestId) {
        auto connection = connection_.lock();
        if (!connection) {
            return false;
        }
        std::lock_guard<std::mutex> lock(mutex_);
        if (auto iter = requestDelays_.find(request); iter != requestDelays_.end()) {
            // Mock the `CLOSE_CONSUMER` command sent by broker, for simplicity, disconnect all consumers
            if (request == "SEEK") {
                schedule(connection, "CLOSE_CONSUMER" + std::to_string(requestId),
                         requestDelays_["CLOSE_CONSUMER"], [connection] {
                             std::vector<uint64_t> consumerIds;
                             {
                                 std::lock_guard<std::mutex> lock{connection->mutex_};
                                 for (auto&& kv : connection->consumers_) {
                                     if (auto consumer = kv.second.lock()) {
                                         consumerIds.push_back(consumer->getConsumerId());
                                     }
                                 }
                             }
                             for (auto consumerId : consumerIds) {
                                 proto::CommandCloseConsumer closeConsumerCmd;
                                 closeConsumerCmd.set_consumer_id(consumerId);
                                 connection->handleCloseConsumer(closeConsumerCmd);
                             }
                         });
            }
            schedule(connection, request + std::to_string(requestId), iter->second,
                     [connection, request, requestId] {
                         if (request == "CONSUMER_STATS") {
                             proto::CommandConsumerStatsResponse response;
                             response.set_request_id(requestId);
                             connection->handleConsumerStatsResponse(response);
                         } else if (request == "LOOKUP") {
                             proto::CommandLookupTopicResponse response;
                             response.set_request_id(requestId);
                             response.set_response(proto::CommandLookupTopicResponse_LookupType_Connect);
                             response.set_brokerserviceurl("pulsar://localhost:6650");
                             connection->handleLookupTopicRespose(response);
                         } else if (request == "GET_LAST_MESSAGE_ID") {
                             proto::CommandGetLastMessageIdResponse response;
                             response.set_request_id(requestId);
                             response.mutable_last_message_id();
                             connection->handleGetLastMessageIdResponse(response);
                         } else if (request == "GET_TOPICS_OF_NAMESPACE") {
                             proto::CommandGetTopicsOfNamespaceResponse response;
                             response.set_request_id(requestId);
                             connection->handleGetTopicOfNamespaceResponse(response);
                         } else if (request == "GET_SCHEMA") {
                             proto::CommandGetSchemaResponse response;
                             response.set_request_id(requestId);
                             response.mutable_schema()->set_type(proto::Schema_Type_String);
                             connection->handleGetSchemaResponse(response);
                         } else {
                             proto::CommandSuccess success;
                             success.set_request_id(requestId);
                             connection->handleSuccess(success);
                         }
                     });
            return true;
        } else {
            return false;
        }
    }

    // Return the number of pending timers cancelled
    auto close() {
        std::lock_guard<std::mutex> lock(mutex_);
        auto result = pendingTimers_.size();
        for (auto&& kv : pendingTimers_) {
            try {
                LOG_INFO("Cancelling timer for " << kv.first);
                kv.second->cancel();
            } catch (...) {
                LOG_WARN("Failed to cancel timer for " << kv.first);
            }
        }
        pendingTimers_.clear();
        return result;
    }

   private:
    mutable std::mutex mutex_;
    std::unordered_map<std::string, long> requestDelays_;
    std::unordered_map<std::string, DeadlineTimerPtr> pendingTimers_;
    ClientConnectionWeakPtr connection_;

    void schedule(ClientConnectionPtr& connection, const std::string& key, long delayMs,
                  std::function<void()>&& task) {
        auto timer = connection->executor_->createDeadlineTimer();
        pendingTimers_[key] = timer;
        timer->expires_after(std::chrono::milliseconds(delayMs));
        LOG_INFO("Mock scheduling " << key << " with delay " << delayMs << " ms");
        auto self = shared_from_this();
        timer->async_wait([this, self, key, connection, task{std::move(task)}](const auto& ec) {
            {
                std::lock_guard<std::mutex> lock(mutex_);
                pendingTimers_.erase(key);
            }
            if (ec) {
                LOG_INFO("Timer cancelled for " << key);
                return;
            }
            if (connection->isClosed()) {
                LOG_INFO("Connection is closed, not completing request " << key);
                return;
            }
            LOG_INFO("Completing delayed request " << key);
            task();
        });
    }

    DECLARE_LOG_OBJECT()
};

}  // namespace pulsar
