#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
#include <variant>
#include <vector>

struct Credentail {
    int cert_;
};

struct Download {
    enum class ConnectionType { Telnet,
                                Ssh };

    std::string url_;
    Credentail credentail_;
    int maxMbps_;
    bool cache_;
    ConnectionType type_;
};

struct Upload {
    enum class ConnectionType { Telnet,
                                Ssh };

    std::string url_;
    Credentail credentail_;
    int maxMbps_;
    int size_;
    ConnectionType type_;
};

struct RemoveFromCache {
    std::string url_;
};

struct ClearCache {
};

struct Image {
    std::vector<uint8_t> bitmap_;
};

class Server {
public:
    enum class ErrorCode {
        Ok,
        WrongUrl,
        CanNotConnect,
        WrongCredential,
        MaximumSizeExceeded,
        MissingImage,
    };
    using CallbackType = void (*)(ErrorCode, Image);

    void handle(const Download& request, CallbackType callback) {
        if (const auto it = findImage(request.url_); it != std::cend(cache_)) {
            callback(ErrorCode::Ok, it->first);
            return;
        }

        Image img;
        const auto ec = download(request, img);
        if (ec == ErrorCode::Ok) {
            if (request.cache_) {
                cache_.emplace_back(img, request.url_);
            }
        }

        callback(ec, img);
    }

    void handle(const Upload& request, CallbackType callback) const {
        if (request.size_ > 100) {
            callback(ErrorCode::MaximumSizeExceeded, Image{});
            return;
        }

        Image img;
        callback(upload(request, img), img);
    }

    void handle(const RemoveFromCache& request, CallbackType callback) {
        if (const auto it = findImage(request.url_); it != std::cend(cache_)) {
            cache_.erase(it);
            callback(ErrorCode::Ok, Image{});
            return;
        }

        callback(ErrorCode::MissingImage, Image{});
    }

    void handle(const ClearCache& request, CallbackType callback) {
        cache_.clear();
        callback(ErrorCode::Ok, Image{});
    }

private:
    std::vector<std::pair<Image, std::string>>::const_iterator findImage(const std::string& url) const {
        return std::find_if(cbegin(cache_), cend(cache_),
                            [url](const auto& pair) {
                                const auto& [image, this_url] = pair;
                                return this_url == url;
                            });
    }

    ErrorCode download(const Download& request, Image& image) const {
        if (request.type_ == Download::ConnectionType::Ssh && request.credentail_.cert_ != 123) {
            return ErrorCode::WrongCredential;
        }
        if (request.type_ == Download::ConnectionType::Telnet && request.credentail_.cert_ != 231) {
            return ErrorCode::CanNotConnect;
        }

        // Simulate some other error
        if (request.maxMbps_ % 2) {
            return ErrorCode::WrongUrl;
        }
        image.bitmap_ = {97, 98, 99, 100, 101, 102};
        return ErrorCode::Ok;
    }

    ErrorCode upload(const Upload& request, const Image& image) const {
        if (request.type_ == Upload::ConnectionType::Ssh && request.credentail_.cert_ != 1234) {
            return ErrorCode::WrongCredential;
        }
        if (request.type_ == Upload::ConnectionType::Telnet && request.credentail_.cert_ != 5432) {
            return ErrorCode::CanNotConnect;
        }

        // Simulate some other error
        if (!request.maxMbps_ % 2) {
            return ErrorCode::WrongUrl;
        }
        return ErrorCode::Ok;
    }

    std::vector<std::pair<Image, std::string>> cache_;
};

class RequestHandler {
public:
    using RequestType = std::variant<Download, Upload, RemoveFromCache, ClearCache>;

    void start(Server* server) {
        std::thread(&RequestHandler::run, this, server).detach();
    }

    void stop() {
        stop_ = true;
        cv_.notify_one();
    }

    void pushRequest(const RequestType& request, Server::CallbackType callback) {
        {
            std::lock_guard lock(m_);
            requests_.emplace(request, callback);
        }
        cv_.notify_one();
    }

private:
    using QueueType = std::pair<RequestType, Server::CallbackType>;

    void run(Server* server) {
        while (!stop_) {
            const auto res = waitForRequest();
            if (!res) {
                return;
            }

            const auto [request, callback] = *res;
            switch (request.index()) {
            case 0:
                server->handle(std::get<Download>(request), callback);
                break;
            case 1:
                server->handle(std::get<Upload>(request), callback);
                break;
            case 2:
                server->handle(std::get<RemoveFromCache>(request), callback);
                break;
            case 3:
                server->handle(std::get<ClearCache>(request), callback);
                break;
            }
        }
    }

    std::optional<QueueType> waitForRequest() {
        std::unique_lock lk(m_);
        cv_.wait(lk, [&]() { return !requests_.empty() || stop_; });
        if (stop_) {
            return std::nullopt;
        }
        auto pair = requests_.front();
        requests_.pop();

        return pair;
    }

    std::mutex m_;
    std::condition_variable cv_;
    std::atomic<bool> stop_{false};
    std::queue<QueueType> requests_;
};

int main() {
    Server server;
    RequestHandler handler;

    handler.start(&server);

    handler.pushRequest(Download{"sth.png", 123, 100, true, Download::ConnectionType::Ssh},
                        [](Server::ErrorCode ec, Image image) {
                            if (ec == Server::ErrorCode::Ok) {
                                for (auto el : image.bitmap_) {
                                    std::cout << el << ' ';
                                }
                                std::cout << '\n';
                            } else {
                                std::cout << "FAILED!\n";
                            }
                        });

    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    handler.stop();
}