#include "spoofer.h"
#include "utils.h"
#include "log-sink-cout.h"
#include "log-sink-file.h"

#include <regex>
#include <map>
#include <fstream>

#include <tinymitm/proxy.h>

#include <seallib/assert.h>
#include <seallib/log.h>

#include <ixwebsocket/IXWebSocketServer.h>

#include <glaze/glaze.hpp>

/*
    misc helper functions
*/
std::unordered_set<std::string> slasherNames = {
    "Chuckles",  "Bob", "HillBilly", "Nurse",  "Shape",  "Witch",     "Killer07",   "Cannibal", "Bear",
    "Nightmare", "Pig", "Clown",     "Spirit", "Plague", "Ghostface", "Demogorgon", "Oni",      "Gunslinger"};

bool isSlasher(std::string name)
{
    if (slasherNames.contains(name) || (name.length() == 3 && name[0] == 'K')) return true;
    return false;
}

/*
    spoofer impl
*/
Spoofer::Spoofer()
{
    _log = new seallib::Logger("Spoofer");
    _log->addSink(std::make_shared<ConOutSink>());
    _log->addSink(FileSink::getSharedInstance());

    _log->verbose("Spoofer init");

    loadConfig();

    _log->verbose("Starting WebSocket server");
    initServer();
}

Spoofer::~Spoofer()
{
    _log->verbose("Stopping WebSocket server");
    stopServer();

    delete _log;
}

void Spoofer::registerListeners(TinyMITMProxy* proxy)
{
    proxy->onClientRequest.addListener(
        [this](const std::string& url, std::string& body, std::string& headers, bool& blockOutgoing) {
            this->clientRequestHandler(url, body, headers, blockOutgoing);
        });

    proxy->onServerResponse.addListener(
        [this](const std::string& url, std::string& body, std::string& headers, bool wasBlocked) {
            this->serverResponseHandler(url, body, headers, wasBlocked);
        });
}

/*
    config
*/
struct FileConfig
{
        SpooferConfig profile;
        WSMessages::Toggles toggles;
};

void Spoofer::loadConfig()
{
    FileConfig conf;
    std::string buffer;
    std::string configPath = utils::getExePath() + "config.json";
    auto errCtx = glz::read_file_json(conf, configPath, buffer);
    if (errCtx.ec == glz::error_code::none)
    {
        _camperItems = conf.profile.camperItems;
        _camperAddons = conf.profile.camperAddons;
        _slasherAddons = conf.profile.slasherAddons;
        _camperOfferings = conf.profile.camperOfferings;
        _slasherOfferings = conf.profile.slasherOfferings;
        _globalOfferings = conf.profile.globalOfferings;
        _camperPerks = conf.profile.camperPerks;
        _slasherPerks = conf.profile.slasherPerks;
        _catalogItemIds = conf.profile.catalogItemIds;
        _dlcListGRDK = conf.profile.dlcListGRDK;
        _dlcListEGS = conf.profile.dlcListEGS;
        _dlcListSteam = conf.profile.dlcListSteam;
        _unlockedCharacters = conf.profile.unlockedCharacters;
        _spoofItems = conf.toggles.spoofItems;
        _spoofPerks = conf.toggles.spoofPerks;
        _spoofCatalog = conf.toggles.spoofCatalog;
        _spoofDLCs = conf.toggles.spoofDLCs;
        _log->info("Config loaded successfully from {}", configPath);
    }
    else
    {
        _log->info("Config not found or invalid at {}, creating default.", configPath);
        saveConfig();
    }
}

void Spoofer::saveConfig()
{
    std::string configPath = utils::getExePath() + "config.json";

    FileConfig conf;
    conf.profile.camperItems = _camperItems;
    conf.profile.camperAddons = _camperAddons;
    conf.profile.slasherAddons = _slasherAddons;
    conf.profile.camperOfferings = _camperOfferings;
    conf.profile.slasherOfferings = _slasherOfferings;
    conf.profile.globalOfferings = _globalOfferings;
    conf.profile.camperPerks = _camperPerks;
    conf.profile.slasherPerks = _slasherPerks;
    conf.profile.catalogItemIds = _catalogItemIds;
    conf.profile.dlcListGRDK = _dlcListGRDK;
    conf.profile.dlcListEGS = _dlcListEGS;
    conf.profile.dlcListSteam = _dlcListSteam;
    conf.profile.unlockedCharacters = _unlockedCharacters;
    conf.toggles.spoofItems = _spoofItems;
    conf.toggles.spoofPerks = _spoofPerks;
    conf.toggles.spoofCatalog = _spoofCatalog;
    conf.toggles.spoofDLCs = _spoofDLCs;
    std::string buffer;
    auto errCtx = glz::write_file_json(conf, configPath, buffer);
    if (errCtx.ec != glz::error_code::none) _log->error("Failed to save config to {}", configPath);
    _log->verbose("Saved config @ config.json");
}

/*
    websocket
*/
void Spoofer::initServer()
{
    if (_wsServer) PANIC("Attempted to init websocket server from invalid state");
    _wsServer = new ix::WebSocketServer(WS_PORT, WS_ADDR);

    _wsServer->setOnClientMessageCallback([this](std::shared_ptr<ix::ConnectionState> connectionState,
                                                 ix::WebSocket& webSocket,
                                                 const std::unique_ptr<ix::WebSocketMessage>& msg) {
        this->wsMessageCallback(connectionState, webSocket, msg);
    });

    std::pair<bool, std::string> res = _wsServer->listen();
    if (!res.first) PANIC("Websocket server failed to start");

    _log->verbose("WebSocket server running @ {}:{}", WS_ADDR, WS_PORT);

    _wsServer->start();
}

void Spoofer::stopServer()
{
    if (_wsServer)
    {
        _wsServer->stop();
        delete _wsServer;
        _wsServer = nullptr;
    }
}

void Spoofer::wsMessageCallback(std::shared_ptr<ix::ConnectionState> /*connectionState*/, ix::WebSocket& webSocket,
                                const std::unique_ptr<ix::WebSocketMessage>& msg)
{
    std::lock_guard<std::mutex> lock(_mutex);
    switch (msg->type)
    {
        case ix::WebSocketMessageType::Open:
            {
                _log->verbose("WebSocket connection open, URI: {}", msg->openInfo.uri);
                WSMessages::Init initMsg;
                initMsg.profile.camperItems = _camperItems;
                initMsg.profile.camperAddons = _camperAddons;
                initMsg.profile.slasherAddons = _slasherAddons;
                initMsg.profile.camperOfferings = _camperOfferings;
                initMsg.profile.slasherOfferings = _slasherOfferings;
                initMsg.profile.globalOfferings = _globalOfferings;
                initMsg.profile.camperPerks = _camperPerks;
                initMsg.profile.slasherPerks = _slasherPerks;
                initMsg.profile.catalogItemIds = _catalogItemIds;
                initMsg.profile.dlcListGRDK = _dlcListGRDK;
                initMsg.profile.dlcListEGS = _dlcListEGS;
                initMsg.profile.dlcListSteam = _dlcListSteam;
                initMsg.profile.unlockedCharacters = _unlockedCharacters;
                initMsg.toggles.spoofItems = _spoofItems;
                initMsg.toggles.spoofPerks = _spoofPerks;
                initMsg.toggles.spoofCatalog = _spoofCatalog;
                initMsg.toggles.spoofDLCs = _spoofDLCs;
                std::string out;
                auto errCtx = glz::write_json(initMsg, out);
                if (errCtx && errCtx.ec != glz::error_code::none)
                    _log->error("Error occurred while writing init msg");
                else
                    webSocket.send(out);
                break;
            }
        case ix::WebSocketMessageType::Close:
            _log->verbose("WebSocket connection close");
            break;
        case ix::WebSocketMessageType::Message:
            {
                WSMessages::Request req;
                auto err = glz::read_json(req, msg->str);
                if (err.ec != glz::error_code::none)
                {
                    _log->error("Failed to parse WebSocket message");
                    break;
                }
                switch (req.action)
                {
                    case WSMessages::SYNC_STATE:
                        {
                            _camperItems = std::move(req.profile.camperItems);
                            _camperAddons = std::move(req.profile.camperAddons);
                            _slasherAddons = std::move(req.profile.slasherAddons);
                            _camperOfferings = std::move(req.profile.camperOfferings);
                            _slasherOfferings = std::move(req.profile.slasherOfferings);
                            _globalOfferings = std::move(req.profile.globalOfferings);
                            _camperPerks = std::move(req.profile.camperPerks);
                            _slasherPerks = std::move(req.profile.slasherPerks);
                            _catalogItemIds = std::move(req.profile.catalogItemIds);
                            _dlcListGRDK = std::move(req.profile.dlcListGRDK);
                            _dlcListEGS = std::move(req.profile.dlcListEGS);
                            _dlcListSteam = std::move(req.profile.dlcListSteam);
                            _unlockedCharacters = std::move(req.profile.unlockedCharacters);
                            saveConfig();
                            _log->verbose("Profile state synchronized.");
                            break;
                        }
                    case WSMessages::SYNC_TOGGLES:
                        {
                            _spoofItems = req.toggles.spoofItems;
                            _spoofPerks = req.toggles.spoofPerks;
                            _spoofCatalog = req.toggles.spoofCatalog;
                            _spoofDLCs = req.toggles.spoofDLCs;
                            saveConfig();
                            _log->verbose("Toggles synchronized.");
                            break;
                        }
                    default:
                        _log->warning("Unknown action: {}", static_cast<int>(req.action));
                        break;
                }
                break;
            }
        default:
            break;
    }
}

/*
    endpoint related helper functions
*/
void Spoofer::modifyCharacterInventory(glz::generic& js)
{
    bool slasher = isSlasher(js["characterName"].get<std::string>());

    bool hasItems = js.is_object() && js.get_object().contains("characterItems") && js["characterItems"].is_array();
    if (!hasItems) return;

    std::unordered_set<std::string> existingItemIds;
    std::unordered_map<std::string, int> stackableQty;
    stackableQty.insert(_camperItems.begin(), _camperItems.end());
    stackableQty.insert(_camperOfferings.begin(), _camperOfferings.end());
    stackableQty.insert(_globalOfferings.begin(), _globalOfferings.end());
    stackableQty.insert(_camperAddons.begin(), _camperAddons.end());
    stackableQty.insert(_slasherAddons.begin(), _slasherAddons.end());
    stackableQty.insert(_slasherOfferings.begin(), _slasherOfferings.end());

    auto& itemsArr = js["characterItems"].get_array();

    /*
        existing items
    */
    for (auto& item : itemsArr)
    {
        if (item.is_object() && item.get_object().contains("itemId") && item["itemId"].is_string())
        {
            std::string itemId = item["itemId"].get<std::string>();
            existingItemIds.insert(itemId);
            auto it = stackableQty.find(itemId);
            if (it != stackableQty.end()) item["quantity"] = it->second;
        }
    }

    /*
        item injection
    */
    auto appendItems = [&](const std::unordered_map<std::string, int>& idMap) {
        for (const auto& [itemId, qty] : idMap)
            if (existingItemIds.find(itemId) == existingItemIds.end())
            {
                glz::json_t::object_t newItem;
                newItem["itemId"] = itemId;
                newItem["quantity"] = qty;
                itemsArr.push_back(newItem);
            }
    };

    auto appendPerks = [&](const std::unordered_set<std::string>& idSet) {
        for (const auto& itemId : idSet)
            if (existingItemIds.find(itemId) == existingItemIds.end())
            {
                glz::json_t::object_t newItem;
                newItem["itemId"] = itemId;
                newItem["quantity"] = 3;
                itemsArr.push_back(newItem);
            }
    };

    if (!slasher)
    {
        appendItems(_camperItems);
        appendItems(_camperAddons);
        appendItems(_camperOfferings);
        appendPerks(_camperPerks);
    }
    else
    {
        appendItems(_slasherAddons);
        appendItems(_slasherOfferings);
        appendPerks(_slasherPerks);
    }
    appendItems(_globalOfferings);

    _log->verbose("Modified inventory for character {}", js["characterName"].get_string());
}

void Spoofer::modifyCharacterData(glz::generic& js)
{
    if (!js.contains("characterName") || !js["characterName"].is_string())
    {
        _log->warning("Attempted to modify invalid character");
        return;
    }

    std::string name = js["characterName"].get_string();

    if (_spoofCharacters)
    {
        bool needsLvlSpoofing = false;
        if (js.contains("isEntitled") && !js["isEntitled"].get_boolean() && js.contains("characterName") &&
            _unlockedCharacters.contains(js["characterName"].get_string()))
        {
            js["isEntitled"] = true;
            js["purchaseInfo"] = glz::json_t::object_t{{"quantity", 1},
                                                       {"origin", "PlayerInventory"},
                                                       {"reason", "Item(s) added via Purchase"},
                                                       {"lastUpdateAt", static_cast<uint64_t>(std::time(nullptr))},
                                                       {"objectId", name}};
            needsLvlSpoofing = true;
        }

        if (needsLvlSpoofing)
        {

            if (js.contains("bloodWebLevel") && js["bloodWebLevel"].is_number() &&
                js["bloodWebLevel"].get_number() <= 15)
                if (!js.contains("prestigeLevel") ||
                    (js["prestigeLevel"].is_number() && js["prestigeLevel"].get_number() <= 0))
                    js["bloodWebLevel"] = 16;

            if (js.contains("bloodWebData")) generateBloodweb(js["bloodWebData"]);
        }
    }

    if (_spoofItems || _spoofPerks) modifyCharacterInventory(js);

    _log->verbose("Modified data for character {}", js["characterName"].get_string());
}

void Spoofer::generateBloodweb(glz::generic& js)
{
    if (!js.is_object()) js = glz::json_t::object_t{};

    std::vector<std::string> paths;
    glz::json_t::array_t ringDataArray;

    glz::json_t::object_t rootRing;
    glz::json_t::array_t rootNodeData;
    glz::json_t::object_t rootNode;
    rootNode["nodeId"] = "0";
    rootNode["state"] = "Collected";
    rootNodeData.push_back(rootNode);
    rootRing["nodeData"] = rootNodeData;
    ringDataArray.push_back(rootRing);

    int nodesPerRing[] = {6, 12, 12};
    std::vector<std::string> prevRingNodes = {"0"};

    for (int ring = 1; ring <= 3; ++ring)
    {
        glz::json_t::array_t nodeDataArray;
        std::vector<std::string> currentRingNodes;
        int numNodes = nodesPerRing[ring - 1];

        for (int i = 1; i <= numNodes; ++i)
        {
            std::string childId = std::to_string((ring * 100) + i);
            currentRingNodes.push_back(childId);

            int parentIndex = (i - 1) / (numNodes / static_cast<int>(prevRingNodes.size()));
            std::string parentId = prevRingNodes[(std::min)(parentIndex, (int)prevRingNodes.size() - 1)];
            paths.push_back(parentId + "_" + childId);

            std::string item = PLACEHOLDER_ITEM_ID;

            glz::json_t::object_t node;
            node["nodeId"] = childId;
            node["state"] = "Collected";
            node["contentId"] = item;
            nodeDataArray.push_back(node);
        }

        glz::json_t::object_t ringEntry;
        ringEntry["nodeData"] = nodeDataArray;
        ringDataArray.push_back(ringEntry);

        prevRingNodes = std::move(currentRingNodes);
    }

    glz::json_t::array_t pathsArr;
    for (auto& p : paths)
        pathsArr.push_back(p);

    js["paths"] = pathsArr;
    js["ringData"] = ringDataArray;
}

/*
    api handlers
*/
void Spoofer::onServerGetAll(std::string& body)
{
    glz::generic doc{};
    auto ec = glz::read_json(doc, body);
    if (ec) return _log->error("JSON parse error for dbd-character-data/get-all");

    if (!doc.is_object() || !doc.get_object().contains("list") || !doc["list"].is_array())
        return _log->error("Invalid json for dbd-character-data/get-all");

    auto& list = doc["list"].get_array();
    for (auto& charData : list)
        modifyCharacterData(charData);

    auto written = glz::write_json(doc);
    if (written)
        body = written.value();
    else
        _log->error("JSON dump error for dbd-character-data/get-all");
}

void Spoofer::onServerInventoryAll(std::string& body)
{
    glz::generic doc{};
    auto ec = glz::read_json(doc, body);
    if (ec) return _log->error("JSON parse error for dbd-inventories/all");

    if (!doc.is_object() || !doc.get_object().contains("inventoryItems") || !doc["inventoryItems"].is_array())
        return _log->error("Invalid json for JSON parse error for dbd-inventories/all");

    auto& itemsArr = doc["inventoryItems"].get_array();
    std::unordered_set<std::string> existingIds;
    int64_t now = std::time(nullptr);

    std::unordered_map<std::string, int> spoofMap;

    if (_spoofPerks)
    {
        for (const auto& id : _camperPerks)
            spoofMap[id] = 3;
        for (const auto& id : _slasherPerks)
            spoofMap[id] = 3;
    }
    if (_spoofItems)
    {
        for (const auto& [id, qty] : _camperOfferings)
            spoofMap[id] = qty;
        for (const auto& [id, qty] : _slasherOfferings)
            spoofMap[id] = qty;
        for (const auto& [id, qty] : _globalOfferings)
            spoofMap[id] = qty;
        for (const auto& [id, qty] : _camperItems)
            spoofMap[id] = qty;
        for (const auto& [id, qty] : _camperAddons)
            spoofMap[id] = qty;
        for (const auto& [id, qty] : _slasherAddons)
            spoofMap[id] = qty;
    }
    if (_spoofCatalog)
    {
        //for (const auto& id : _catalogOutfitIds)
        //spoofMap[id] = 1;
        for (const auto& id : _catalogItemIds)
            spoofMap[id] = 1;
    }

    /*
        item updates
    */
    for (auto& item : itemsArr)
    {
        std::string id;
        if (item.is_object() && item.get_object().contains("objectId") && item["objectId"].is_string())
            id = item["objectId"].get<std::string>();

        if (id.empty()) continue;
        existingIds.insert(id);

        auto it = spoofMap.find(id);
        if (it != spoofMap.end()) item["quantity"] = it->second;
    }

    /*
        item inserts
    */
    for (const auto& [id, qty] : spoofMap)
    {
        if (!existingIds.contains(id))
        {
            glz::json_t::object_t newItem;
            newItem["objectId"] = id;
            newItem["quantity"] = qty;
            newItem["lastUpdateAt"] = now;
            itemsArr.push_back(newItem);
        }
    }

    auto written = glz::write_json(doc);
    if (written)
        body = written.value();
    else
        _log->error("JSON dump error for dbd-inventories/all");

    _log->verbose("Inventory spoofed");
}

void Spoofer::onServerMessageList(std::string& body)
{
    glz::generic doc;
    auto ec = glz::read_json(doc, body);
    if (ec)
    {
        _log->error("JSON parse error for onServerMessageList");
        return;
    }

    auto now = std::chrono::system_clock::now();
    auto now_seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
    auto now_milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();

    /*
        msg construction
    */
    glz::generic bodyContent = glz::generic::object_t{
        {"sections",
         glz::generic::array_t{
             glz::generic::object_t{
                 {"type", "text"},
                 {"text", "Japan is turning footsteps into electricity!<br><br>Using piezoelectric tiles, every step "
                          "you take generates a small amount of energy. Millions of steps together can power LED "
                          "lights and displays in busy places like Shibuya Station.<br><br>A brilliant way to create a "
                          "sustainable and smart city - turning movement into clean, renewable energy."}},
             glz::generic::object_t{
                 {"type", "itemshowcase"},
                 {"rewards", glz::generic::array_t{glz::generic::object_t{
                                 {"type", "inventory"}, {"id", "ADDON_flashlight_oddbulb"}, {"amount", 1.0}}}}}}},
        {"image", glz::generic::object_t{
                      {"packagedPath", "/Game/UI/UMGAssets/Icons/ItemAddons/iconAddon_powerBulb.iconAddon_powerBulb"},
                      {"contentVersion", "ccc3f02b0a671fe19a0017d6a69293876a465fd9"},
                      {"uri", ""}}}};

    std::string bodyContentStr;
    ec = glz::write_json(bodyContent, bodyContentStr);
    if (ec)
    {
        _log->error("JSON write error for onServerMessageList");
        return;
    }

    glz::generic msg = glz::generic::object_t{
        {"allowedPlatforms", glz::generic::array_t{"egs", "grdk", "ps4", "ps5", "steam", "xbox", "xsx"}},
        {"flag", "READ"},
        {"gameSpecificData", glz::generic::object_t{}},
        {"read", false},
        {"tag", glz::generic::array_t{"inbox"}},
        {"expireAt", static_cast<double>(now_seconds + (1337 * 24 * 60 * 60))},
        {"received", static_cast<double>(now_milliseconds)},
        {"recipientId", "system"},
        {"message",
         glz::generic::object_t{{"title", "Japan is turning footsteps into electricity"}, {"body", bodyContentStr}}}};

    doc["messages"].get_array().push_back(msg);
    ec = glz::write_json(doc, body);
    if (ec)
    {
        _log->error("JSON write error for onServerMessageList");
        return;
    }

    _log->verbose("Spoofed message list");
}

void Spoofer::onServerBloodweb(std::string& body, std::string& respHeaders)
{
    glz::generic doc;
    auto ec = glz::read_json(doc, body);
    if (ec)
    {
        _log->error("JSON parse error for onServerBloodweb");
        return;
    }

    /*
        false char response
    */
    if (body.find("NotAllowedException") != std::string::npos && body.find("not owned") != std::string::npos)
    {
        glz::generic mock = glz::generic::object_t{};
        mock["bloodWebLevelChanged"] = false;
        mock["updatedWallets"] = glz::generic::array_t{};
        mock["bloodWebLevel"] = 16;
        mock["prestigeLevel"] = 0;
        mock["bloodWebData"] = glz::generic::object_t{};
        mock["characterItems"] = glz::generic::array_t{};
        mock["characterName"] = this->_lastBwCharacter;
        mock["isEntitled"] = true;

        glz::generic::object_t purchaseInfo;
        purchaseInfo["quantity"] = 1;
        purchaseInfo["origin"] = "PlayerInventory";
        purchaseInfo["reason"] = "Item(s) added via Purchase";
        purchaseInfo["lastUpdateAt"] = static_cast<uint64_t>(std::time(nullptr));
        purchaseInfo["objectId"] = this->_lastBwCharacter;

        mock["purchaseInfo"] = purchaseInfo;

        if (_spoofCharacters) generateBloodweb(mock["bloodWebData"]);

        if (_spoofItems || _spoofPerks) modifyCharacterInventory(mock);

        std::string new_body;
        ec = glz::write_json(mock, new_body);
        if (ec)
        {
            _log->error("JSON write error for onServerBloodweb");
            return;
        }
        body = new_body;

        std::regex statusRegex(R"(HTTP\/\d\.\d\s+403)");
        respHeaders = std::regex_replace(respHeaders, statusRegex, "HTTP/1.1 200");

        _log->verbose("Spoofed bloodweb request for unowned character.");
    }

    /*
        bloodweb fixup for already owned perks
    */
    if (_spoofPerks)
    {
        if (doc.contains("bloodWebData") && doc["bloodWebData"].is_object())
        {
            auto& bloodWebData = doc["bloodWebData"];

            if (!bloodWebData.contains("paths") || !bloodWebData.contains("ringData")) return;

            for (auto& ring : bloodWebData["ringData"].get_array())
            {
                if (!ring.contains("nodeData") || !ring["nodeData"].is_array()) continue;

                for (auto& node : ring["nodeData"].get_array())
                {
                    if (!node.contains("contentId") || !node["contentId"].is_string()) continue;

                    std::string contentId = node["contentId"].get_string();
                    if (_camperPerks.contains(contentId) || _slasherPerks.contains(contentId))
                        node["contentId"] = PLACEHOLDER_ITEM_ID;
                }
            }
            _log->verbose("Fixed bloodweb request");
        }
    }

    modifyCharacterData(doc);

    auto written = glz::write_json(doc);
    if (written)
        body = written.value();
    else
        _log->error("JSON write error for onServerBloodweb");

    _log->verbose("Spoofed bloodweb items for owned character");
}

void Spoofer::onServerUpdateEntitlements(const std::string& url, std::string& body)
{
    if (!_spoofDLCs) return;

    glz::generic doc;
    auto ec = glz::read_json(doc, body);
    if (ec)
    {
        _log->error("JSON parse error for onClientUpdateEntitlements");
        return;
    }

    if (doc.contains("entitlements") && doc["entitlements"].is_array())
    {
        auto& entitlements = doc["entitlements"].get_array();

        std::unordered_set<std::string>* dlcList = nullptr;

        if (url.starts_with("https://grdk.live.bhvrdbd.com/"))
            dlcList = &_dlcListGRDK;
        else if (url.starts_with("https://egs.live.bhvrdbd.com/"))
            dlcList = &_dlcListEGS;
        else
            return _log->error("Entitlement spoofing only works with Epic / Xbox");

        for (const std::string& dlcId : *dlcList)
        {
            auto it = std::ranges::find_if(
                entitlements, [&](const auto& item) { return item.is_string() && item.get_string() == dlcId; });
            if (it == entitlements.end()) entitlements.emplace_back(dlcId);
        }
    }

    ec = glz::write_json(doc, body);
    if (ec)
    {
        _log->error("JSON write error for onClientUpdateEntitlements");
        return;
    }

    _log->verbose("Spoofed entitlements (response)");
}

void Spoofer::onClientBloodweb(std::string& body)
{
    glz::generic doc;
    auto ec = glz::read_json(doc, body);
    if (ec)
    {
        _log->error("JSON parse error for onClientBloodweb");
        return;
    }

    if (doc.contains("characterName")) _lastBwCharacter = doc["characterName"].get_string();
}

void Spoofer::onClientUpdateEntitlements(const std::string& url, std::string& body)
{
    if (!_spoofDLCs) return;

    glz::generic doc;
    auto ec = glz::read_json(doc, body);
    if (ec)
    {
        _log->error("JSON parse error for onClientUpdateEntitlements");
        return;
    }

    if (doc.contains("clientEntitlementIds") && doc["clientEntitlementIds"].is_array())
    {
        auto& entitlements = doc["clientEntitlementIds"].get_array();

        std::unordered_set<std::string>* dlcList = nullptr;

        if (url.starts_with("https://grdk.live.bhvrdbd.com/"))
            dlcList = &_dlcListGRDK;
        else if (url.starts_with("https://egs.live.bhvrdbd.com/"))
            dlcList = &_dlcListEGS;
        else
            return _log->error("Entitlement spoofing only works with Epic / Xbox");

        for (const std::string& dlcId : *dlcList)
        {
            auto it = std::ranges::find_if(
                entitlements, [&](const auto& item) { return item.is_string() && item.get_string() == dlcId; });
            if (it == entitlements.end()) entitlements.emplace_back(dlcId);
        }
    }

    ec = glz::write_json(doc, body);
    if (ec)
    {
        _log->error("JSON write error for onClientUpdateEntitlements");
        return;
    }

    _log->verbose("Spoofed entitlements (client)");
}

/*
    proxy handlers
*/
void Spoofer::serverResponseHandler(const std::string& url, std::string& body, std::string& headers,
                                    bool& /*blockOutgoing*/)
{
    if (url.find("bhvrdbd.com") == std::string::npos) return;

    std::lock_guard<std::mutex> lock(_mutex);

    if (url.find("/api/v1/messages/listV2") != std::string::npos) return onServerMessageList(body);
    if (url.find("/api/v1/dbd-inventories/all") != std::string::npos) return onServerInventoryAll(body);
    if (url.find("/api/v1/dbd-character-data/get-all") != std::string::npos) return onServerGetAll(body);
    if (url.find("/api/v1/owned-products/get-update-entitlements") != std::string::npos)
        return onServerUpdateEntitlements(url, body);

    if (url.find("/api/v1/dbd-character-data/bloodweb") != std::string::npos ||
        url.find("/api/v1/dbd-character-data/bloodweb/v2") != std::string::npos ||
        url.find("/api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
        return onServerBloodweb(body, headers);
}

void Spoofer::clientRequestHandler(const std::string& url, std::string& body, std::string& /*headers*/,
                                   bool /*wasBlocked*/)
{
    if (url.find("bhvrdbd.com") == std::string::npos) return;

    std::lock_guard<std::mutex> lock(_mutex);

    if (url.find("/api/v1/owned-products/get-update-entitlements") != std::string::npos)
        return onClientUpdateEntitlements(url, body);

    if (url.find("/api/v1/dbd-character-data/bloodweb") != std::string::npos ||
        url.find("/api/v1/dbd-character-data/bloodweb/v2") != std::string::npos ||
        url.find("/api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
        return onClientBloodweb(body);
}
