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

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

struct Credentail {
    int cert_;
};

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

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

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

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

class Command {
public:
    enum class ErrorCode {
        Ok,
        WrongUrl,
        CanNotConnect,
        WrongCredential,
        MaximumSizeExceeded,
        MissingImage,
    };

    class Delegate {
    public:
        virtual ~Delegate() = default;
        virtual ErrorCode removeFromCache(const std::string& url) = 0;
        virtual void clearCache() = 0;
        virtual void addToCache(const std::vector<uint8_t>& data, const std::string& url) = 0;
        virtual ErrorCode download(std::vector<uint8_t>& data, const DownloadRequest& request) const = 0;
        virtual ErrorCode upload(const std::vector<uint8_t>& data, const UploadRequest& request) const = 0;
    };

    explicit Command(Delegate* delegate)
        : delegate_(delegate) {}

    // Rule of 5!
    virtual ~Command() = default;
    Command(const Command&) = default;
    Command(Command&&) = default;
    Command& operator=(const Command&) = default;
    Command& operator=(Command&&) = default;

    virtual void operator()() const = 0;

protected:
    Delegate* delegate_;
};

class DownloadImageCommand : public Command {
public:
    using CallbackType = void (*)(ErrorCode, Image);

    ~DownloadImageCommand() override = default;

    DownloadImageCommand(Delegate* delegate, CallbackType callback, const DownloadRequest& request)
        : Command(delegate), callback_(callback), request_(request) {}

    void operator()() const override {
        std::vector<uint8_t> data;
        if (auto ec = delegate_->download(data, request_); ec == Command::ErrorCode::Ok) {
            // Do some conversion on vector
            if (request_.cache_) {
                delegate_->addToCache(data, request_.url_);
            }
            Image image{data};
            callback_(ec, std::move(image));
        } else {
            callback_(ec, Image{});
        }
    };

private:
    CallbackType callback_;
    DownloadRequest request_;
};

class UploadImageCommand : public Command {
public:
    using CallbackType = void (*)(ErrorCode);

    ~UploadImageCommand() override = default;

    UploadImageCommand(Delegate* delegate, const Image& image, CallbackType callback, const UploadRequest& request)
        : Command(delegate), image_(image), callback_(callback), request_(request) {}

    void operator()() const override {
        callback_(delegate_->upload(image_.bitmap_, request_));
    };

private:
    Image image_;
    CallbackType callback_;
    UploadRequest request_;
};

class Server : public Command::Delegate {
public:
    ~Server() override = default;

    Command::ErrorCode removeFromCache(const std::string& url) override {
        if (const auto it = findFile(url); it != std::cend(cache_)) {
            cache_.erase(it);
            return Command::ErrorCode::Ok;
        }

        return Command::ErrorCode::MissingImage;
    }

    void clearCache() override {
        cache_.clear();
    }

    void addToCache(const std::vector<uint8_t>& data, const std::string& url) override {
        cache_.emplace_back(data, url);
    }

    Command::ErrorCode download(std::vector<uint8_t>& data, const DownloadRequest& request) const override {
        if (request.type_ == DownloadRequest::ConnectionType::Ssh && request.credentail_.cert_ != 123) {
            return Command::ErrorCode::WrongCredential;
        }
        if (request.type_ == DownloadRequest::ConnectionType::Telnet && request.credentail_.cert_ != 231) {
            return Command::ErrorCode::CanNotConnect;
        }

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

        data = {97, 98, 99, 100, 101, 102};
        return Command::ErrorCode::Ok;
    }

    Command::ErrorCode upload(const std::vector<uint8_t>& data, const UploadRequest& request) const override {
        if (request.type_ == UploadRequest::ConnectionType::Ssh && request.credentail_.cert_ != 1234) {
            return Command::ErrorCode::WrongCredential;
        }
        if (request.type_ == UploadRequest::ConnectionType::Telnet && request.credentail_.cert_ != 5432) {
            return Command::ErrorCode::CanNotConnect;
        }

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

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

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

class RequestHandler {
public:
    void start() {
        std::thread(&RequestHandler::run, this).detach();
    }

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

    void pushRequest(std::unique_ptr<Command> command) {
        {
            std::lock_guard lock(m_);
            requests_.push(std::move(command));
        }
        cv_.notify_one();
    }

private:
    void run() {
        while (!stop_) {
            const auto request = waitForRequest();
            if (!request || !*request) {
                return;
            }

            (**request)();
        }
    }

    std::optional<std::unique_ptr<Command>> waitForRequest() {
        std::unique_lock lk(m_);
        cv_.wait(lk, [&]() { return !requests_.empty() || stop_; });
        if (stop_) {
            return std::nullopt;
        }

        std::unique_ptr<Command> request = std::move(requests_.front());
        requests_.pop();

        return std::move(request);
    }

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

int main() {
    Server server;
    RequestHandler handler;

    handler.start();
    handler.pushRequest(std::make_unique<DownloadImageCommand>(
        &server,
        [](Command::ErrorCode ec, Image img) {
            if (ec == Command::ErrorCode::Ok) {
                std::copy(cbegin(img.bitmap_), cend(img.bitmap_), std::ostream_iterator<uint8_t>(std::cout, " "));
                std::cout << '\n';
            } else {
                std::cout << "Sth went wrong!\n";
            }
        },
        DownloadRequest{"Sth123", Credentail{123}, 200, true, DownloadRequest::ConnectionType::Ssh}));

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