#pragma once

#include <string>
#include <chrono>
#include <Definitions.h>
#include <condition_variable>
#include <variant>
#include <deque>
#include <functional>

#define TRANSFER_KEY_LENGTH (32)

namespace ts::server::file {
    enum struct ExecuteStatus {
        UNKNOWN,
        WAITING,
        SUCCESS,
        ERROR
    };

    template<typename VariantType, typename T, std::size_t index = 0>
    constexpr std::size_t variant_index() {
        if constexpr (index == std::variant_size_v<VariantType>) {
            return index;
        } else if constexpr (std::is_same_v<std::variant_alternative_t<index, VariantType>, T>) {
            return index;
        } else {
            return variant_index<VariantType, T, index + 1>();
        }
    }

    struct EmptyExecuteResponse { };
    template <class error_t, class response_t = EmptyExecuteResponse>
    class ExecuteResponse {
            typedef std::variant<EmptyExecuteResponse, error_t, response_t> variant_t;
        public:
            ExecuteStatus status{ExecuteStatus::WAITING};

            [[nodiscard]] inline const auto& response() const { return std::get<response_t>(this->response_); }

            template <typename = std::enable_if_t<!std::is_void<error_t>::value>>
            [[nodiscard]] inline const error_t& error() const { return std::get<error_t>(this->response_); }

            inline void wait() const {
                std::unique_lock nlock{this->notify_mutex};
                this->notify_cv.wait(nlock, [&]{ return this->status != ExecuteStatus::WAITING; });
            }

            template<typename _Rep, typename _Period>
            [[nodiscard]] inline bool wait_for(const std::chrono::duration<_Rep, _Period>& time) const {
                std::unique_lock nlock{this->notify_mutex};
                return this->notify_cv.wait_for(nlock, time, [&]{ return this->status != ExecuteStatus::WAITING; });
            }

            template <typename... Args>
            inline void emplace_success(Args&&... args) {
                constexpr auto success_index = variant_index<variant_t, response_t>();

                std::lock_guard rlock{this->notify_mutex};
                this->response_.template emplace<success_index, Args...>(std::forward<Args>(args)...);
                this->status = ExecuteStatus::SUCCESS;
                this->notify_cv.notify_all();
            }

            template <typename... Args>
            inline void emplace_fail(Args&&... args) {
                constexpr auto error_index = variant_index<variant_t, error_t>();

                std::lock_guard rlock{this->notify_mutex};
                this->response_.template emplace<error_index, Args...>(std::forward<Args>(args)...);
                this->status = ExecuteStatus::ERROR;
                this->notify_cv.notify_all();
            }

            [[nodiscard]] inline bool succeeded() const {
                return this->status == ExecuteStatus::SUCCESS;
            }

            ExecuteResponse(std::mutex& notify_mutex, std::condition_variable& notify_cv)
                : notify_mutex{notify_mutex}, notify_cv{notify_cv} {}
        private:
            variant_t response_{}; /* void* as default value so we don't initialize error_t or response_t */

            std::mutex& notify_mutex;
            std::condition_variable& notify_cv;
    };

    namespace filesystem {
        template <typename ErrorCodes>
        struct DetailedError {
            ErrorCodes error_type{ErrorCodes::UNKNOWN};
            std::string error_message{};

            DetailedError(ErrorCodes type, std::string extraMessage) : error_type{type}, error_message{std::move(extraMessage)} {}
        };

        enum struct DirectoryQueryErrorType {
            UNKNOWN,
            PATH_EXCEEDS_ROOT_PATH,
            PATH_IS_A_FILE,
            PATH_DOES_NOT_EXISTS,
            FAILED_TO_LIST_FILES,

            MAX
        };
        constexpr std::array<std::string_view, (int) DirectoryQueryErrorType::MAX> directory_query_error_messages = {
            "unknown error",
            "path exceeds base path",
            "path is a file",
            "path does not exists",
            "failed to list files"
        };

        typedef DetailedError<DirectoryQueryErrorType> DirectoryQueryError;

        struct DirectoryEntry {
            enum Type {
                UNKNOWN,
                DIRECTORY,
                FILE
            };

            Type type{Type::UNKNOWN};
            std::string name{};
            std::chrono::system_clock::time_point modified_at{};

            size_t size{0};
        };

        enum struct DirectoryModifyErrorType {
            UNKNOWN,
            PATH_EXCEEDS_ROOT_PATH,
            PATH_ALREADY_EXISTS,
            FAILED_TO_CREATE_DIRECTORIES
        };
        typedef DetailedError<DirectoryModifyErrorType> DirectoryModifyError;

        enum struct FileModifyErrorType {
            UNKNOWN,
            PATH_EXCEEDS_ROOT_PATH,
            TARGET_PATH_EXCEEDS_ROOT_PATH,
            PATH_DOES_NOT_EXISTS,
            TARGET_PATH_ALREADY_EXISTS,
            FAILED_TO_DELETE_FILES,
            FAILED_TO_RENAME_FILE,

            SOME_FILES_ARE_LOCKED
        };
        typedef DetailedError<FileModifyErrorType> FileModifyError;

        enum struct ServerCommandErrorType {
            UNKNOWN,
            FAILED_TO_CREATE_DIRECTORIES,
            FAILED_TO_DELETE_DIRECTORIES
        };
        typedef DetailedError<ServerCommandErrorType> ServerCommandError;

        class AbstractProvider {
            public:
                typedef ExecuteResponse<DirectoryQueryError, std::deque<DirectoryEntry>> directory_query_response_t;

                /* server */
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<ServerCommandError>> initialize_server(ServerId /* server */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<ServerCommandError>> delete_server(ServerId /* server */) = 0;

                /* channels */
                [[nodiscard]] virtual std::shared_ptr<directory_query_response_t> query_channel_directory(ServerId /* server */, ChannelId /* channel */, const std::string& /* path */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<DirectoryModifyError>> create_channel_directory(ServerId /* server */, ChannelId /* channel */, const std::string& /* path */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<FileModifyError>> delete_channel_file(ServerId /* server */, ChannelId /* channel */, const std::string& /* path */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<FileModifyError>> rename_channel_file(ServerId /* server */, ChannelId /* channel */, const std::string& /* path */, const std::string& /* target */) = 0;

                /* icons */
                [[nodiscard]] virtual std::shared_ptr<directory_query_response_t> query_icon_directory(ServerId /* server */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<FileModifyError>> delete_icon(ServerId /* server */, const std::string& /* name */) = 0;

                /* avatars */
                [[nodiscard]] virtual std::shared_ptr<directory_query_response_t> query_avatar_directory(ServerId /* server */) = 0;
                [[nodiscard]] virtual std::shared_ptr<ExecuteResponse<FileModifyError>> delete_avatar(ServerId /* server */, const std::string& /* name */) = 0;
            private:
        };
    }

    namespace transfer {
        typedef uint32_t transfer_id;

        struct Transfer {
            transfer_id server_transfer_id{0};
            transfer_id client_transfer_id{0};

            ServerId server_id{0};
            ClientId client_id{0};
            ChannelId channel_id{0};

            char transfer_key[TRANSFER_KEY_LENGTH]{};
            std::chrono::system_clock::time_point initialized_timestamp{};
            enum Direction {
                DIRECTION_UNKNOWN,
                DIRECTION_UPLOAD,
                DIRECTION_DOWNLOAD
            } direction{DIRECTION_UNKNOWN};

            struct Address {
                std::string hostname{};
                uint16_t port{0};
            };
            std::vector<Address> server_addresses{};

            enum TargetType {
                TARGET_TYPE_UNKNOWN,
                TARGET_TYPE_CHANNEL_FILE,
                TARGET_TYPE_ICON,
                TARGET_TYPE_AVATAR
            } target_type{TARGET_TYPE_UNKNOWN};
            std::string target_file_path{};

            int64_t max_bandwidth{-1};
            size_t expected_file_size{0}; /* incl. the offset! */
            size_t file_offset{0};
            bool override_exiting{false};
        };

        struct TransferStatistics {
            uint64_t network_bytes_send{0};
            uint64_t network_bytes_received{0};

            uint64_t delta_network_bytes_send{0};
            uint64_t delta_network_bytes_received{0};

            uint64_t file_bytes_transferred{0};
            uint64_t delta_file_bytes_transferred{0};

            size_t file_start_offset{0};
            size_t file_current_offset{0};
            size_t file_total_size{0};
        };

        struct TransferInitError {
            enum Type {
                UNKNOWN
            } error_type{UNKNOWN};
            std::string error_message{};
        };

        struct TransferActionError {
            enum Type {
                UNKNOWN,

                UNKNOWN_TRANSFER
            } error_type{UNKNOWN};
            std::string error_message{};
        };

        struct TransferError {
            enum Type {
                UNKNOWN,

                TRANSFER_TIMEOUT,

                DISK_IO_ERROR,
                DISK_TIMEOUT,
                DISK_INITIALIZE_ERROR,

                NETWORK_IO_ERROR,

                UNEXPECTED_CLIENT_DISCONNECT,
                UNEXPECTED_DISK_EOF
            } error_type{UNKNOWN};
            std::string error_message{};
        };

        class AbstractProvider {
            public:
                struct TransferInfo {
                    std::string file_path{};

                    bool override_exiting{false}; /* only for upload valid */
                    size_t file_offset{0};
                    size_t expected_file_size{0};
                    int64_t max_bandwidth{-1};
                };

                virtual std::shared_ptr<ExecuteResponse<TransferInitError, std::shared_ptr<Transfer>>> initialize_channel_transfer(Transfer::Direction /* direction */, ServerId /* server */, ChannelId /* channel */, const TransferInfo& /* info */) = 0;
                virtual std::shared_ptr<ExecuteResponse<TransferInitError, std::shared_ptr<Transfer>>> initialize_icon_transfer(Transfer::Direction /* direction */, ServerId /* server */, const TransferInfo& /* info */) = 0;
                virtual std::shared_ptr<ExecuteResponse<TransferInitError, std::shared_ptr<Transfer>>> initialize_avatar_transfer(Transfer::Direction /* direction */, ServerId /* server */, const TransferInfo& /* info */) = 0;

                virtual std::shared_ptr<ExecuteResponse<TransferActionError>> stop_transfer(transfer_id /* id */, bool /* flush */) = 0;

                std::function<void(const std::shared_ptr<Transfer>&)> callback_transfer_registered{}; /* transfer has been registered */
                std::function<void(const std::shared_ptr<Transfer>&)> callback_transfer_started{}; /* transfer has been started */
                std::function<void(const std::shared_ptr<Transfer>&)> callback_transfer_finished{}; /* transfer has been finished */
                std::function<void(const std::shared_ptr<Transfer>&, const TransferError&)> callback_transfer_aborted{}; /* an error happened while transferring the data  */
                std::function<void(const std::shared_ptr<Transfer>&, const TransferStatistics&)> callback_transfer_statistics{};
        };
    }

    class AbstractFileServer {
        public:
            [[nodiscard]] virtual filesystem::AbstractProvider& file_system() = 0;
            [[nodiscard]] virtual transfer::AbstractProvider& file_transfer() = 0;
        private:
    };

    extern bool initialize(std::string& /* error */);
    extern void finalize();

    extern std::shared_ptr<AbstractFileServer> server();
}