#include "spoofing.h"
#include "utils.h"

#include <string>
#include <fstream>
#include <random>
#include <vector>
#include <regex>
#include <thread>

#include <time.h>

#include <nerutils/log.h>

#include <nlohmann/json.hpp>

using json = nlohmann::json;

static std::random_device rd;
static std::mt19937 gen(rd());

/*
    utils
*/
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;
}

/*
    setup / init
*/
void Spoofer::init(Proxy* proxy)
{
    registerListeners(proxy);
    loadData();
    loadConfig();
}

void Spoofer::registerListeners(Proxy* proxy)
{
    Log::info("Registering Proxy event listeners for spoofer");
    proxy->OnServerResponse.addListener([this](const std::string& url, std::string& body, std::string& respHeaders) {
        this->serverResponseHandler(url, body, respHeaders);
    });

    proxy->OnClientRequest.addListener([this](std::string& url, const std::string& body, std::string& reqHeaders) {
        this->clientRequestHandler(url, body, reqHeaders);
    });
}

#define LOADDATA(fileName, type, camperSet, slasherSet, errorMsg)                                                      \
    std::string path##type = utils::getExePath() + fileName;                                                           \
    std::ifstream file##type(path##type);                                                                              \
    if (file##type.is_open())                                                                                          \
    {                                                                                                                  \
        std::stringstream buff;                                                                                        \
        buff << file##type.rdbuf();                                                                                    \
        if (!parseStackable(buff.str(), camperSet, slasherSet))                                                        \
            Log::error("Failed to parse {} - {}", fileName, errorMsg);                                                 \
        else                                                                                                           \
            Log::verbose("Loaded {} camper ids and {} slasher ids from {}", camperSet.size(), slasherSet.size(),       \
                         fileName);                                                                                    \
    }                                                                                                                  \
    else                                                                                                               \
        Log::warning("Missing {} - {}", fileName, errorMsg);

void Spoofer::loadData()
{
    Log::info("Loading data");

    std::string catalogPath = utils::getExePath() + "catalog.json";
    std::ifstream catalogFile(catalogPath);
    if (catalogFile.is_open())
    {
        std::stringstream buff;
        buff << catalogFile.rdbuf();
        if (!parseCatalog(buff.str())) Log::warning("Failed to parse catalog.json - Customizations won't be unlocked");
    }
    else
        Log::warning("Missing catalog.json - Customizations won't be unlocked");

    LOADDATA("items.json", Items, _camperItemIds, _slasherPowerIds, "Items won't be added");
    LOADDATA("offerings.json", Offerings, _camperOfferingIds, _slasherOfferingIds, "Offerings won't be added");
    LOADDATA("addons.json", Addons, _camperAddonIds, _slasherAddonIds, "Addons won't be added");
    LOADDATA("perks.json", Perks, _camperPerkIds, _slasherPerkIds, "Perks won't be added");
}

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

    if (configFile.is_open())
    {
        try
        {
            json configJson = json::parse(configFile);
            _config.spoofCharacterOwnership = configJson.value("spoofCharacterOwnership", false);
            _config.spoofInventory = configJson.value("spoofInventory", true);
            _config.spoofCustomization = configJson.value("spoofCustomization", true);
            Log::info("Loaded config: Ownership={}, Inventory={}, Customization={}", _config.spoofCharacterOwnership,
                      _config.spoofInventory, _config.spoofCustomization);
        }
        catch (...)
        {
            Log::error("Failed to parse config.json, using defaults");
        }
    }
    else
    {
        Log::info("config.json not found, using default settings");
        json defaultConfig = {
            {"spoofCharacterOwnership", true}, {"spoofInventory", true}, {"spoofCustomization", true}};
        std::ofstream out(configPath);
        out << defaultConfig.dump(4);
    }
}

/*
    data parsing
*/
bool Spoofer::parseCatalog(std::string data)
{
    json doc = json::parse(data, nullptr, false);

    if (doc.is_discarded())
    {
        Log::error("Failed to parse catalog");
        return false;
    }

    try
    {
        const auto& catalogData = doc.at("data");

        auto extractIds = [&](const std::string& key, std::unordered_set<std::string>& targetSet) {
            if (catalogData.contains(key) && catalogData[key].contains("items"))
                for (const auto& id : catalogData[key]["items"])
                {
                    if (id.is_string()) targetSet.insert(id.get<std::string>());
                }
            else
                Log::warning("Catalog missing or invalid category: {}", key);
        };

        extractIds("item", _catalogItemIds);
        extractIds("outfit", _catalogOutfitIds);
    }
    catch (const json::exception& e)
    {
        Log::error("Invalid catalog format: {}", e.what());
        return false;
    }

    Log::info("Parsed {} items and {} outfits from catalog", _catalogItemIds.size(), _catalogOutfitIds.size());

    return true;
}

bool Spoofer::parseStackable(std::string data, std::unordered_set<std::string>& camperSet,
                             std::unordered_set<std::string>& slasherSet)
{
    json doc = json::parse(data, nullptr, false);
    if (doc.is_discarded())
    {
        Log::error("JSON parse error @ parseStackable");
        return false;
    }

    auto populate = [&](const std::string& key, std::unordered_set<std::string>& targetSet) {
        if (doc.contains(key) && doc[key].is_array())
            for (const auto& item : doc[key])
            {
                if (item.is_string()) targetSet.insert(item.get<std::string>());
            }
        else
            Log::warning("Missing stackables array ({})", key);
    };

    populate("Slashers", slasherSet);
    populate("Campers", camperSet);

    return true;
}

/*
    misc functions
*/
std::string Spoofer::getRandomItem()
{
    std::vector<const std::unordered_set<std::string>*> allSets = {
        &_camperItemIds,  &_slasherPowerIds, &_camperOfferingIds, &_slasherOfferingIds,
        &_camperAddonIds, &_slasherAddonIds, &_camperPerkIds,     &_slasherPerkIds};

    std::vector<const std::unordered_set<std::string>*> validSets;
    for (auto* s : allSets)
        if (!s->empty()) validSets.push_back(s);

    if (validSets.empty()) return "Spring2024Offering";

    std::uniform_int_distribution<> setDist(0, static_cast<int>(validSets.size()) - 1);
    const auto& selectedSet = *validSets[setDist(gen)];

    std::uniform_int_distribution<> itemDist(0, static_cast<int>(selectedSet.size()) - 1);
    auto it = selectedSet.begin();
    std::advance(it, itemDist(gen));

    return *it;
}

int Spoofer::getRandomQuantity()
{
    std::uniform_int_distribution<> dist(64, 97);
    return dist(gen);
}

void Spoofer::generateBloodweb(nlohmann::json& js)
{
    if (!js.is_object()) js = json::object();

    std::vector<std::string> paths;
    json ringDataArray = json::array();

    ringDataArray.push_back({{"nodeData", {{{"nodeId", "0"}, {"state", "Collected"}}}}});

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

    for (int ring = 1; ring <= 3; ++ring)
    {
        json nodeDataArray = json::array();
        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 = getRandomItem();
            if (item.empty()) item = "Spring2024Offering";

            nodeDataArray.push_back({{"nodeId", childId}, {"state", "Collected"}, {"contentId", item}});
        }
        ringDataArray.push_back({{"nodeData", nodeDataArray}});
        prevRingNodes = std::move(currentRingNodes);
    }

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

/*
    data modification
*/
void Spoofer::modifyCharacterData(json& js)
{
    if (!js.contains("characterName") || !js["characterName"].is_string())
    {
        Log::verbose("attempted to modify invalid char");
        return;
    }

    std::string name = js["characterName"];

    if (_config.spoofCharacterOwnership)
    {
        bool needsSpoofing = false;

        if (js.value("isEntitled", false) == false)
        {
            _unownedCharacters.insert(name);
            js["isEntitled"] = true;
            js["purchaseInfo"] = {{"quantity", 1},
                                  {"origin", "PlayerInventory"},
                                  {"reason", "Item(s) added via Purchase"},
                                  {"lastUpdateAt", std::time(nullptr)},
                                  {"objectId", name}};
            needsSpoofing = true;
        }
        else if (_unownedCharacters.contains(name))
            needsSpoofing = true;

        /*
            modifications for unowned characters (spoof level and fake bloodweb)
        */
        if (needsSpoofing)
        {
            if (js.contains("bloodWebLevel") && js["bloodWebLevel"].is_number() && js["bloodWebLevel"] <= 15)
                if (!js.contains("prestigeLevel") || (js["prestigeLevel"].is_number() && js["prestigeLevel"] <= 0))
                    js["bloodWebLevel"] = 16;

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

    {
        if (js.value("isEntitled", false) == false) return;
    }

    /*
        item spoofing
    */
    if (_config.spoofInventory)
    {
        bool slasher = isSlasher(js["characterName"]);

        if (js.contains("characterItems") && js["characterItems"].is_array())
        {
            std::unordered_set<std::string> existingItemIds;

            std::unordered_set<std::string> stackableIds;
            stackableIds.insert(_camperItemIds.begin(), _camperItemIds.end());
            stackableIds.insert(_camperOfferingIds.begin(), _camperOfferingIds.end());
            stackableIds.insert(_camperAddonIds.begin(), _camperAddonIds.end());
            stackableIds.insert(_slasherAddonIds.begin(), _slasherAddonIds.end());
            stackableIds.insert(_slasherOfferingIds.begin(), _slasherOfferingIds.end());

            for (auto& item : js["characterItems"])
            {
                /*
                set existing items to rnd number
            */
                if (item.contains("itemId") && item["itemId"].is_string())
                {
                    std::string itemId = item["itemId"];
                    existingItemIds.insert(itemId);
                    if (stackableIds.contains(itemId)) item["quantity"] = getRandomQuantity();
                }
            }

            auto appendItems = [&](const std::unordered_set<std::string>& idList, bool isPerk) {
                for (const auto& itemId : idList)
                    if (existingItemIds.find(itemId) == existingItemIds.end())
                        js["characterItems"].push_back(
                            {{"itemId", itemId}, {"quantity", isPerk ? 3 : getRandomQuantity()}});
            };

            if (!slasher)
            {
                appendItems(_camperItemIds, false);
                appendItems(_camperAddonIds, false);
                appendItems(_camperOfferingIds, false);
                appendItems(_camperPerkIds, true);
            }
            else
            {
                appendItems(_slasherAddonIds, false);
                appendItems(_slasherOfferingIds, false);
                appendItems(_slasherPerkIds, true);
            }
        }
    }

#ifdef _DEBUG
    Log::verbose("Spoofed data for character {}", name);
#endif
}

/*
    endpoint handlers
*/

void Spoofer::onGetCatalogItems(std::string& body)
{
    std::thread t([body]() {
        std::string outPath = utils::getExePath() + "catalog.json";
        std::ofstream file(outPath);
        if (file.is_open())
        {
            file << body;
            file.close();
            Log::verbose("Raw catalog saved to {}", outPath);
        }
        else
            Log::error("Unable to write to catalog.json");
    });
    t.detach();

    parseCatalog(body);
}

void Spoofer::onGetAll(std::string& body)
{
    json doc = json::parse(body, nullptr, false);
    if (doc.is_discarded()) return Log::error("JSON parse error for dbd-character-data/get-all");
    if (!doc.contains("list") || !doc["list"].is_array())
        return Log::error("Invalid json for dbd-character-data/get-all");

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

    body = doc.dump();
}

void Spoofer::onInventoryAll(std::string& body)
{
    json doc = json::parse(body, nullptr, false);
    if (doc.is_discarded()) return Log::error("JSON parse error for dbd-inventories/all");
    if (!doc.contains("inventoryItems") || !doc["inventoryItems"].is_array())
        return Log::error("Invalid json for JSON parse error for dbd-inventories/all");

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

    /*
        item updates
    */
    struct Category
    {
            const std::unordered_set<std::string>& source;
            int quantity;
    };

    std::vector<Category> categories;
    if (_config.spoofInventory)
    {
        categories.push_back({_camperPerkIds, 3});
        categories.push_back({_slasherPerkIds, 3});
        categories.push_back({_camperOfferingIds, -1});
        categories.push_back({_slasherOfferingIds, -1});

        categories.push_back({_camperItemIds, -1});
        categories.push_back({_camperAddonIds, -1});
        categories.push_back({_slasherAddonIds, -1});
    }

    if (_config.spoofCustomization)
    {
        categories.push_back({_catalogOutfitIds, 1});
        categories.push_back({_catalogItemIds, 1});
    }

    for (auto& item : itemsArr)
    {
        std::string id = item.value("objectId", "");
        if (id.empty()) continue;

        existingIds.insert(id);

        for (const auto& cat : categories)
        {
            if (cat.source.contains(id))
            {
                item["quantity"] = (cat.quantity == -1) ? getRandomQuantity() : cat.quantity;
                break;
            }
        }
    }

    /*
        item inserts
    */
    for (const auto& cat : categories)
    {
        for (const std::string& id : cat.source)
        {
            if (!existingIds.contains(id))
            {
                int qty = (cat.quantity == -1) ? getRandomQuantity() : cat.quantity;
                itemsArr.push_back({{"objectId", id}, {"quantity", qty}, {"lastUpdateAt", now}});
                existingIds.insert(id);
            }
        }
    }

    body = doc.dump();
    Log::verbose("Inventory spoofed");
}

void Spoofer::onMessageList(std::string& body)
{
    json doc = json::parse(body, nullptr, false);
    if (doc.is_discarded()) return Log::error("JSON parse error for dbd-messages/listV2");

    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();

    json msg;

    /*
        msg base
    */
    msg["allowedPlatforms"] = json::array({"egs", "grdk", "ps4", "ps5", "steam", "xbox", "xsx"});
    msg["flag"] = "READ";
    msg["gameSpecificData"] = {};
    msg["read"] = false;
    msg["tag"] = json::array({"inbox"});
    msg["expireAt"] = now_seconds + (1337 * 24 * 60 * 60);
    msg["received"] = now_milliseconds;
    msg["recipientId"] = "system";

    /*
        msg content
    */
    json bodyContent;
    bodyContent["sections"] = json::array();
    bodyContent["sections"].push_back(
        {{"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."}});

    bodyContent["image"] = {
        {"packagedPath", "/Game/UI/UMGAssets/Icons/ItemAddons/iconAddon_powerBulb.iconAddon_powerBulb"},
        {"contentVersion", "ccc3f02b0a671fe19a0017d6a69293876a465fd9"},
        {"uri", ""}};

    bodyContent["sections"].push_back(
        {{"type", "itemshowcase"},
         {"rewards", json::array({{{"type", "inventory"}, {"id", "ADDON_flashlight_oddbulb"}, {"amount", 1}}})}});

    msg["message"] = {};
    msg["message"]["title"] = "Japan is turning footsteps into electricity";
    msg["message"]["body"] = bodyContent.dump();

    doc["messages"].push_back(msg);

    body = doc.dump();
#ifdef _DEBUG
    Log::verbose("Spoofed message list");
#endif
}

void Spoofer::onBloodweb(std::string& body, std::string& respHeaders)
{
    json doc = json::parse(body, nullptr, false);
    if (doc.is_discarded()) return Log::error("JSON parse error for bloodweb response");

    if (_config.spoofCharacterOwnership)
    {
        if (body.find("NotAllowedException") != std::string::npos && body.find("not owned") != std::string::npos)
        {
            Log::info("Spoofing bloodweb error for unowned character");
            json mock;
            mock["bloodWebLevelChanged"] = false;
            mock["updatedWallets"] = json::array();
            mock["bloodWebLevel"] = 16;
            mock["prestigeLevel"] = 0;
            mock["bloodWebData"] = json::object();
            mock["characterItems"] = json::array();
            mock["characterName"] = this->_lastBloodWebChar;
            mock["isEntitled"] = true;
            mock["purchaseInfo"] = {{"quantity", 1},
                                    {"origin", "PlayerInventory"},
                                    {"reason", "Item(s) added via Purchase"},
                                    {"lastUpdateAt", std::time(nullptr)},
                                    {"objectId", this->_lastBloodWebChar}};

            _unownedCharacters.insert(this->_lastBloodWebChar); // probably not needed but just in case

            modifyCharacterData(mock);

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

            body = mock.dump();
#ifdef _DEBUG
            Log::verbose("Spoofed bloodweb request for unowned character.");
#endif
            return;
        }
    }

    modifyCharacterData(doc);
    body = doc.dump();
#ifdef _DEBUG
    Log::verbose("Spoofed bloodweb items for owned character");
#endif
}

/*
    event handlers
*/
void Spoofer::serverResponseHandler(const std::string& url, std::string& body, std::string& respHeaders)
{
    if (url.find("bhvrdbd.com") == std::string::npos) return;
    std::lock_guard<std::mutex> lock(_mtx);

#ifdef _DEBUG
    Log::verbose("BHVR api res @ {}", url);
#endif

    if (url.find("api/v1/extensions/store/getCatalogItems") != std::string::npos) return onGetCatalogItems(body);
    if (url.find("api/v1/dbd-character-data/get-all") != std::string::npos) return onGetAll(body);
    if (url.find("api/v1/dbd-character-data/get-all") != std::string::npos) return onGetAll(body);
    if (url.find("api/v1/dbd-inventories/all") != std::string::npos) return onInventoryAll(body);
    if (url.find("/api/v1/messages/listV2") != std::string::npos) return onMessageList(body);
    if (url.find("api/v1/dbd-character-data/bloodweb") != std::string::npos ||
        url.find("api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
        return onBloodweb(body, respHeaders);
}

void Spoofer::clientRequestHandler(std::string& url, const std::string& body, std::string& /*reqHeaders*/)
{
    if (url.find("bhvrdbd.com") == std::string::npos) return;
    std::lock_guard<std::mutex> lock(_mtx);

    if (url.find("api/v1/dbd-character-data/bloodweb") != std::string::npos ||
        url.find("api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
    {
        json req = json::parse(body, nullptr, false);
        if (req.is_discarded()) return Log::error("JSON parse error for bloodweb request handler");
        if (req.contains("characterName")) this->_lastBloodWebChar = req["characterName"];
    }
}
