#pragma once

#include "../DefaultInclude.hpp"
#include <fs/filesystem.hpp>
#include "Result.hpp"
#include "json.hpp"
#include <mutex>

namespace geode::utils::web {
    using FileProgressCallback = std::function<bool(double, double)>;

    /**
     * Synchronously fetch data from the internet
     * @param url URL to fetch
     * @returns Returned data as bytes, or error on error
     */
    GEODE_DLL Result<byte_array> fetchBytes(std::string const& url);

    /**
     * Synchronously fetch data from the internet
     * @param url URL to fetch
     * @returns Returned data as string, or error on error
     */
    GEODE_DLL Result<std::string> fetch(std::string const& url);

    /**
     * Syncronously download a file from the internet
     * @param url URL to fetch
     * @param into Path to download file into
     * @param prog Progress function; first parameter is bytes downloaded so 
     * far, and second is total bytes to download. Return true to continue 
     * downloading, and false to interrupt. Note that interrupting does not 
     * automatically remove the file that was being downloaded
     * @returns Returned data as JSON, or error on error
     */
    GEODE_DLL Result<> fetchFile(
        std::string const& url,
        ghc::filesystem::path const& into,
        FileProgressCallback prog = nullptr
    );

    /**
     * Synchronously fetch data from the internet and parse it as JSON
     * @param url URL to fetch
     * @returns Returned data as JSON, or error on error
     */
    template<class Json = nlohmann::json>
    Result<Json> fetchJSON(std::string const& url) {
        auto res = fetch(url);
        if (!res) return Err(res.error());
        try {
            return Ok(Json::parse(res.value()));
        } catch(std::exception& e) {
            return Err(e.what());
        }
    }

    class SentAsyncWebRequest;
    template<class T>
    class AsyncWebResult;
    class AsyncWebResponse;
    class AsyncWebRequest;

    using AsyncProgress  = std::function<void(SentAsyncWebRequest&, double, double)>;
    using AsyncExpect    = std::function<void(std::string const&)>;
    using AsyncThen      = std::function<void(SentAsyncWebRequest&, byte_array const&)>;
    using AsyncCancelled = std::function<void(SentAsyncWebRequest&)>;

    class SentAsyncWebRequest {
    private:
        std::string m_id;
        std::string m_url;
        std::vector<AsyncThen> m_thens;
        std::vector<AsyncExpect> m_expects;
        std::vector<AsyncProgress> m_progresses;
        std::vector<AsyncCancelled> m_cancelleds;
        std::atomic<bool> m_paused = true;
        std::atomic<bool> m_cancelled = false;
        std::atomic<bool> m_finished = false;
        std::atomic<bool> m_cleanedUp = false;
        mutable std::mutex m_mutex;
        std::variant<
            std::monostate,
            std::ostream*,
            ghc::filesystem::path
        > m_target = std::monostate();

        template<class T>
        friend class AsyncWebResult;
        friend class AsyncWebRequest;

        void pause();
        void resume();
        void error(std::string const& error);
        void doCancel();

    public:
        SentAsyncWebRequest(AsyncWebRequest const&, std::string const& id);

        void cancel();
        bool finished() const;
    };

    using SentAsyncWebRequestHandle = std::shared_ptr<SentAsyncWebRequest>;

    template<class T>
    using DataConverter = Result<T>(*)(byte_array const&);

    class GEODE_DLL AsyncWebRequest {
    private:
        std::optional<std::string> m_joinID;
        std::string m_url;
        AsyncThen m_then = nullptr;
        AsyncExpect m_expect = nullptr;
        AsyncProgress m_progress = nullptr;
        AsyncCancelled m_cancelled = nullptr;
        bool m_sent = false;
        std::variant<
            std::monostate,
            std::ostream*,
            ghc::filesystem::path
        > m_target;

        template<class T>
        friend class AsyncWebResult;
        friend class SentAsyncWebRequest;
        friend class AsyncWebResponse;

    public:
        AsyncWebRequest& join(std::string const& requestID);
        AsyncWebResponse fetch(std::string const& url);
        AsyncWebRequest& expect(AsyncExpect handler);
        AsyncWebRequest& progress(AsyncProgress progressFunc);
        // Web requests may be cancelled after they are finished (for example, 
        // if downloading files in bulk and one fails). In that case, handle 
        // freeing up the results of `then` here
        AsyncWebRequest& cancelled(AsyncCancelled cancelledFunc);
        SentAsyncWebRequestHandle send();
        ~AsyncWebRequest();
    };

    template<class T>
    class AsyncWebResult {
    private:
        AsyncWebRequest& m_request;
        DataConverter<T> m_converter;

        AsyncWebResult(AsyncWebRequest& request, DataConverter<T> converter)
         : m_request(request), m_converter(converter) {}
        
        friend class AsyncWebResponse;

    public:
        AsyncWebRequest& then(std::function<void(T)> handle);
        AsyncWebRequest& then(std::function<void(SentAsyncWebRequest&, T)> handle);
    };

    class GEODE_DLL AsyncWebResponse {
    private:
        AsyncWebRequest& m_request;

        inline AsyncWebResponse(AsyncWebRequest& request) : m_request(request) {}

        friend class AsyncWebRequest;

    public:
        // Make sure the stream lives for the entire duration of the request.
        AsyncWebResult<std::monostate> into(std::ostream& stream);
        AsyncWebResult<std::monostate> into(ghc::filesystem::path const& path);
        AsyncWebResult<std::string> text();
        AsyncWebResult<byte_array> bytes();
        AsyncWebResult<nlohmann::json> json();

        template<class T>
        AsyncWebResult<T> as(DataConverter<T> converter) {
            return AsyncWebResult(m_request, converter);
        }
    };
    
    template<class T>
    AsyncWebRequest& AsyncWebResult<T>::then(std::function<void(T)> handle) {
        m_request.m_then = [
            converter = m_converter,
            handle
        ](SentAsyncWebRequest& req, byte_array const& arr) {
            auto conv = converter(arr);
            if (conv) {
                handle(conv.value());
            } else {
                req.error("Unable to convert value: " + conv.error());
            }
        };
        return m_request;
    }

    template<class T>
    AsyncWebRequest& AsyncWebResult<T>::then(std::function<void(SentAsyncWebRequest&, T)> handle) {
        m_request.m_then = [
            converter = m_converter,
            handle
        ](SentAsyncWebRequest& req, byte_array const& arr) {
            auto conv = converter(arr);
            if (conv) {
                handle(req, conv.value());
            } else {
                req.error("Unable to convert value: " + conv.error());
            }
        };
        return m_request;
    }
}