#include <smoke_api/smoke_api.hpp>
#include <steam_impl/steam_impl.hpp>

#include <koalabox/io.hpp>

#include <cpr/cpr.h>

using namespace smoke_api;

constexpr auto max_dlc = 64;

Vector<AppId_t> cached_dlcs;
int original_dlc_count = 0;

Path get_cache_path() {
    static const auto path = self_directory / "SmokeAPI.cache.json";
    return path;
}

void read_from_cache(const String& app_id_str) {
    try {
        const auto text = io::read_file(get_cache_path());
        if (text.empty()) {
            return;
        }

        auto json = nlohmann::json::parse(text);

        cached_dlcs = json[app_id_str]["dlc"].get<decltype(cached_dlcs)>();

        logger->debug("Read {} DLCs from cache", cached_dlcs.size());
    } catch (const Exception& ex) {
        logger->error("Error reading DLCs from cache: {}", ex.what());
    }

}

void save_to_cache(const String& app_id_str) {
    try {
        logger->debug("Saving {} DLCs to cache", cached_dlcs.size());

        const nlohmann::json json = {
            {app_id_str, {
                {"dlc", cached_dlcs}
            }}
        };

        io::write_file(get_cache_path(), json.dump(2));
    } catch (const Exception& ex) {
        logger->error("Error saving DLCs to cache: {}", ex.what());
    }
}

void fetch_and_cache_dlcs() {
    uint32_t app_id;
    try {
        app_id = steam_functions::get_app_id_or_throw();
        logger->info("Detected App ID: {}", app_id);
    } catch (const Exception& ex) {
        logger->error("Failed to get app ID: {}", ex.what());
        return;
    }

    const auto app_id_str = std::to_string(app_id);

    const auto fetch_from_steam = [&]() {
        const auto url = fmt::format("https://store.steampowered.com/dlc/{}/ajaxgetdlclist", app_id_str);
        const auto res = cpr::Get(cpr::Url{url});

        if (res.status_code != cpr::status::HTTP_OK) {
            throw util::exception(
                "Steam Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n"
                "Headers:\n{}\nBody:\n{}",
                res.status_code, (int) res.error.code, res.raw_header, res.text
            );
        }

        const auto json = nlohmann::json::parse(res.text);


        if (json["success"] != 1) {
            throw util::exception("Web API responded with 'success': 1.");
        }

        Vector<AppId_t> dlcs;

        for (const auto& dlc: json["dlcs"]) {
            const auto app_id = dlc["appid"].get<String>();
            dlcs.emplace_back(std::stoi(app_id));
        }

        return dlcs;
    };

    const auto fetch_from_github = [&]() {
        const String url = "https://raw.githubusercontent.com/acidicoala/public-entitlements/main/steam/v1/dlc.json";
        const auto res = cpr::Get(cpr::Url{url});

        if (res.status_code != cpr::status::HTTP_OK) {
            throw util::exception(
                "Github Web API didn't responded with HTTP_OK result. Code: {}, Error: {},\n"
                "Headers:\n{}\nBody:\n{}",
                res.status_code, (int) res.error.code, res.raw_header, res.text
            );
        }

        const auto json = nlohmann::json::parse(res.text);

        if (json.contains(app_id_str)) {
            return json[app_id_str].get<decltype(cached_dlcs)>();
        }

        return Vector<AppId_t>{};
    };

    try {
        read_from_cache(app_id_str);

        auto list1 = fetch_from_steam();
        auto list2 = fetch_from_github();
        list1.insert(list1.end(), list2.begin(), list2.end());
        Set<AppId_t> fetched_dlcs(list1.begin(), list1.end());

        if (fetched_dlcs.size() > cached_dlcs.size()) {
            cached_dlcs = Vector<AppId_t>(fetched_dlcs.begin(), fetched_dlcs.end());
        }

        save_to_cache(app_id_str);
    } catch (const Exception& ex) {
        logger->error("Failed to fetch DLC: {}", ex.what());
    }
}


namespace steam_apps{

    bool IsSubscribedApp(const String& function_name, AppId_t appID) {
        const auto subscribed = should_unlock(appID);

        logger->info("{} -> App ID: {}, Subscribed: {}", function_name, appID, subscribed);

        return subscribed;
    }

    bool IsDlcInstalled(const String& function_name, AppId_t appID) {
        const auto installed = should_unlock(appID);

        logger->info("{} -> App ID: {}, Installed: {}", function_name, appID, installed);

        return installed;
    }

    int GetDLCCount(const String& function_name, const std::function<int()>& original_function) {
        static std::mutex section;
        std::lock_guard<std::mutex> guard(section);

        // Compute count only once
        static int total_count = [&]() {
            original_dlc_count = original_function();
            logger->debug("{} -> Original DLC count: {}", function_name, original_dlc_count);

            const auto injected_count = static_cast<int>(config.dlc_ids.size());
            logger->debug("{} -> Injected DLC count: {}", function_name, injected_count);

            if (original_dlc_count < max_dlc) {
                // Steamworks may max out this value at 64, depending on how much unowned DLCs the user has.
                // Despite this limit, some games with more than 64 DLCs still keep using this method.
                // This means we have to fetch full list of IDs from web api.

                return original_dlc_count + injected_count;
            }

            logger->debug("Game has {} or more DLCs. Fetching DLCs from a web API.", max_dlc);
            fetch_and_cache_dlcs();

            const auto fetched_count = static_cast<int>(cached_dlcs.size());
            logger->debug("{} -> Fetched/cached DLC count: {}", function_name, fetched_count);

            return fetched_count + injected_count;
        }();

        logger->info("{} -> Responding with DLC count: {}", function_name, total_count);

        return total_count;
    }

    bool GetDLCDataByIndex(
        const String& function_name,
        int iDLC,
        AppId_t* pAppID,
        bool* pbAvailable,
        char* pchName,
        int cchNameBufferSize,
        const std::function<bool()>& original_function
    ) {
        const auto print_dlc_info = [&](const String& tag) {
            logger->info(
                "{} -> [{}] index: {}, App ID: {}, available: {}, name: '{}'",
                function_name, tag, iDLC, *pAppID, *pbAvailable, pchName
            );
        };

        const auto fill_dlc_info = [&](const AppId_t id) {
            *pAppID = id;
            *pbAvailable = should_unlock(id);

            auto name = fmt::format("DLC #{} with ID: {} ", iDLC, id);
            name = name.substr(0, cchNameBufferSize);
            *name.rbegin() = '\0';
            memcpy_s(pchName, cchNameBufferSize, name.c_str(), name.size());
        };

        const auto inject_dlc = [&](const int index) {
            if (index >= config.dlc_ids.size()) {
                logger->error("{} -> Out of bounds injected index: {}", function_name, index);
                return false;
            }

            const auto app_id = config.dlc_ids[index];
            fill_dlc_info(app_id);
            print_dlc_info("injected");
            return true;
        };

        // Original response
        if (cached_dlcs.empty()) {
            // Original DLC index
            if (iDLC < original_dlc_count) {
                const auto success = original_function();

                if (success) {
                    *pbAvailable = should_unlock(*pAppID);
                    print_dlc_info("original");
                } else {
                    logger->warn("{} -> original function failed for index: {}", function_name, iDLC);
                }
                return success;
            }

            // Injected DLC index (after original)
            const auto index = iDLC - original_dlc_count;
            return inject_dlc(index);
        }

        // Cached response
        const auto total_size = cached_dlcs.size() + config.dlc_ids.size();
        if (iDLC < 0 or iDLC >= total_size) {
            logger->error(
                "{} -> Game accessed out of bounds DLC index: {}. Total size: {}",
                function_name, iDLC, total_size
            );
            return false;
        }

        // Cached index
        if (iDLC < cached_dlcs.size()) {
            const auto app_id = cached_dlcs[iDLC];
            fill_dlc_info(app_id);
            print_dlc_info("cached");
            return true;
        }

        // Injected DLC index (after cached)

        const auto index = iDLC - static_cast<int>(cached_dlcs.size());
        return inject_dlc(index);
    }
}
