//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// Disabler is a cleaned up version of PVE BattlEye Bypass by x3e
/// https://www.unknowncheats.me/forum/escape-from-tarkov/728902-pve-battleyent-alternative.html
/// Credit goes to all original contributors.
/// 

#include "disabler.h"

#include "../libs/ntdll.h"
#include "../libs/detours.h"
#include <psapi.h>
#include <regex>
#include <shlwapi.h>
#include <fstream>
#include <iosfwd>
#include <mutex>
#include <string>
#include <cstdio>

#include "../util/globals.h"
#include "../util/logger.h"
#include "../util/Config.h"

#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "ntdll.lib")

static bool eft_started = false;
static bool eft_ready = false;
static DWORD eft_pid = false;

void patch_bytes(const HANDLE hProcess, void* address, const std::vector<std::uint8_t>& data)
{
    const size_t size = data.size();
    DWORD dwOldProtect = 0;
    VirtualProtectEx(hProcess, address, size, PAGE_EXECUTE_READWRITE, &dwOldProtect);
    WriteProcessMemory(hProcess, address, data.data(), size, nullptr);
    FlushInstructionCache(hProcess, address, size);
    VirtualProtectEx(hProcess, address, size, dwOldProtect, &dwOldProtect);
}

// Match pattern at a known RVA (used to reject stale offsets.cfg entries).
static bool offset_matches_pattern(
    const char* module, size_t module_size, UINT64 offset, const std::string& pattern)
{
    if (offset == 0 || offset >= module_size)
        return false;
    // find_pattern only searches whole buffer; check a window at offset via temporary slice
    // by requiring the global scan to return exactly this offset when possible.
    // Cheap local check: compile first few solid bytes of pattern against module[offset].
    size_t string_index = 0;
    size_t byte_index = 0;
    while (string_index < pattern.length())
    {
        if (offset + byte_index >= module_size)
            return false;
        if (pattern[string_index] == '?')
        {
            string_index += 2;
            byte_index += 1;
            continue;
        }
        if (string_index + 1 >= pattern.length())
            break;
        const std::string byte_string = pattern.substr(string_index, 2);
        string_index += 3;
        const auto expected = static_cast<char>(strtoul(byte_string.c_str(), nullptr, 16));
        if (module[offset + byte_index] != expected)
            return false;
        byte_index += 1;
        if (byte_index >= 12) // enough solid prefix
            return true;
    }
    return byte_index > 0;
}

static UINT64 resolve_in_module(
    const char* module,
    size_t module_size,
    Configuration& offset_cfg,
    Configuration& pattern_cfg,
    const char* key,
    const std::string& default_pattern)
{
    std::string pattern = default_pattern;
    if (!pattern_cfg.empty())
    {
        const std::string from_cfg = pattern_cfg.get(key);
        if (!from_cfg.empty())
            pattern = from_cfg;
    }

    UINT64 offset = 0;
    if (!offset_cfg.empty())
    {
        const uintptr_t from_cfg = globals::get_offset(offset_cfg, key);
        if (from_cfg != 0)
        {
            if (offset_matches_pattern(module, module_size, from_cfg, pattern))
            {
                offset = from_cfg;
            }
            else
            {
                logger::warn(
                    std::string("Offset for ") + key +
                    " does not match expected bytes — falling back to pattern scan");
            }
        }
    }

    if (offset == 0)
    {
        for (int i = 0; i < 4; ++i)
        {
            offset = globals::find_pattern(pattern, module, module_size);
            if (offset != 0)
                break;
            logger::warn(
                std::string("Retrying pattern scan for ") + key + " in 125ms");
            Sleep(125);
        }
    }

    if (offset != 0)
    {
        char buf[128];
        sprintf_s(buf, "%s @ 0x%llX", key, static_cast<unsigned long long>(offset));
        logger::info(buf);
    }
    return offset;
}

static HMODULE find_module(const HANDLE hProcess, const char* name)
{
    HMODULE hMods[1024];
    DWORD cbNeeded;
    char szModName[MAX_PATH];

    if (!EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded))
    {
        return FALSE;
    }

    for (unsigned int i = 0; i < cbNeeded / sizeof(HMODULE); i++)
    {
        if (GetModuleFileNameExA(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(char)))
        {
            if (StrStrIA(szModName, name))
            {
                return hMods[i];
            }
        }
    }

    return nullptr;
}

using CreateProcessInternalW_t = BOOL(WINAPI*)(
    HANDLE hUserToken,
    LPCWSTR lpApplicationName,
    LPWSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCWSTR lpCurrentDirectory,
    LPSTARTUPINFOW lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation,
    PHANDLE hRestrictedUserToken
    );

static CreateProcessInternalW_t oCreateProcessInternalW;

BOOL WINAPI hkCreateProcessInternalW(
    HANDLE hUserToken,
    LPCWSTR lpApplicationName,
    LPWSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    LPVOID lpEnvironment,
    LPCWSTR lpCurrentDirectory,
    LPSTARTUPINFOW lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation,
    PHANDLE hRestrictedUserToken
)
{
    if (lpCommandLine && wcsstr(lpCommandLine, L"EscapeFromTarkov_BE"))
    {
        auto commandLine = std::regex_replace(
            lpCommandLine, std::wregex(L"EscapeFromTarkov_BE"), L"EscapeFromTarkov"
        );

        STARTUPINFOW si;
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);

        PROCESS_INFORMATION pi;
        ZeroMemory(&pi, sizeof(pi));

        const auto ret = oCreateProcessInternalW(
            nullptr,
            nullptr,
            const_cast<LPWSTR>(commandLine.data()),
            nullptr,
            nullptr,
            false,
            CREATE_SUSPENDED,
            nullptr,
            lpCurrentDirectory,
            &si,
            &pi,
            nullptr
        );

        HANDLE hProcess = pi.hProcess;
        HANDLE hThread = pi.hThread;

        eft_pid = GetProcessId(hProcess);
        eft_started = true;

        PROCESS_BASIC_INFORMATION pbi;
        if (!NT_SUCCESS(NtQueryInformationProcess(
            hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), nullptr
        )))
        {
            logger::error("Failed to get pbi");
            return FALSE;
        }

        PEB peb = {};
        if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), nullptr))
        {
            return FALSE;
        }

        IMAGE_DOS_HEADER idh = {};
        if (!ReadProcessMemory(hProcess, peb.ImageBaseAddress, &idh, sizeof(idh), nullptr))
        {
            return FALSE;
        }

        IMAGE_NT_HEADERS inh = {};
        if (!ReadProcessMemory(
            hProcess,
            reinterpret_cast<LPVOID>( // NOLINT(performance-no-int-to-ptr)
                reinterpret_cast<ULONG_PTR>(peb.ImageBaseAddress) + idh.e_lfanew
                ),
            &inh,
            sizeof(inh),
            nullptr
        ))
        {
            return FALSE;
        }

        UINT_PTR entry = inh.OptionalHeader.AddressOfEntryPoint + inh.OptionalHeader.ImageBase;

        std::vector<uint8_t> buf(2);
        ReadProcessMemory(hProcess, reinterpret_cast<LPVOID>(entry), buf.data(), 2, nullptr); // NOLINT(performance-no-int-to-ptr, performance-no-int-to-ptr)
        patch_bytes(hProcess, reinterpret_cast<LPVOID>(entry), { 0xEB, 0xFE }); // NOLINT(performance-no-int-to-ptr)
        ResumeThread(hThread);

        CONTEXT context;
        GetThreadContext(hThread, &context);
        context.ContextFlags = CONTEXT_FULL;
        for (int i = 0; context.Rip != entry; Sleep(100))
        {
            if (++i > 50)
            {
                logger::error("Entrypoint timed out");
                return FALSE;
            }
            context.ContextFlags = CONTEXT_CONTROL;
            GetThreadContext(hThread, &context);
        }

        SuspendThread(hThread);

        Configuration offset_cfg;
        offset_cfg.load(globals::DLL_OFFSET_FILE);

        Configuration pattern_cfg;
        pattern_cfg.load(globals::DLL_PATTERN_FILE);

        HMODULE hUnityPlayer = find_module(hProcess, "UnityPlayer");
        if (!hUnityPlayer)
        {
            logger::error("Disabler Failed: UnityPlayer not loaded at entry");
            TerminateProcess(hProcess, 0);
            Sleep(2000);
            exit(0); // NOLINT(concurrency-mt-unsafe)
        }

        MODULEINFO unityInfo = {};
        GetModuleInformation(hProcess, hUnityPlayer, &unityInfo, sizeof(MODULEINFO));
        auto unityModule = new char[unityInfo.SizeOfImage];
        if (!ReadProcessMemory(hProcess, unityInfo.lpBaseOfDll, unityModule, unityInfo.SizeOfImage, nullptr))
        {
            delete[] unityModule;
            logger::error("Disabler Failed: cannot read UnityPlayer");
            TerminateProcess(hProcess, 0);
            Sleep(2000);
            exit(0); // NOLINT(concurrency-mt-unsafe)
        }

        // EFT 1.0.6.x UnityPlayer — BattlEye service check (xref BEService). @ 0x491E60 on 1.0.6.5.46189
        const std::string checkBeServicePattern =
            "48 89 5C 24 ? 48 89 6C 24 ? 56 57 41 56 48 83 EC 60 33 C0 0F 57 C0";

        const UINT64 checkBeServiceOffset = resolve_in_module(
            unityModule, unityInfo.SizeOfImage, offset_cfg, pattern_cfg,
            "be_service_check", checkBeServicePattern);

        if (checkBeServiceOffset == 0)
        {
            delete[] unityModule;
            logger::error("Disabler Failed: Check BE Service Function Missing");
            TerminateProcess(hProcess, 0);
            Sleep(2000);
            exit(0); // NOLINT(concurrency-mt-unsafe)
        }
        auto* beCheckFunc =
            reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(hUnityPlayer) + checkBeServiceOffset); // NOLINT(performance-no-int-to-ptr)
        delete[] unityModule;

        // B0 01 mov al, 1 / C3 ret  — report "service OK"
        patch_bytes(hProcess, beCheckFunc, { 0xB0, 0x01, 0xC3 });
        logger::info("Patched be_service_check (return true)");

        patch_bytes(hProcess, reinterpret_cast<LPVOID>(entry), buf); // NOLINT(performance-no-int-to-ptr)

        ResumeThread(hThread);

        HMODULE hGameAssembly;
        while (!((hGameAssembly = find_module(hProcess, "GameAssembly"))))
        {
            Sleep(10);
        }

        // Let the loader finish mapping sections before we read/patch.
        Sleep(500);

        MODULEINFO gameInfo = {};
        GetModuleInformation(hProcess, hGameAssembly, &gameInfo, sizeof(MODULEINFO));
        auto gameModule = new char[gameInfo.SizeOfImage];
        if (!ReadProcessMemory(hProcess, gameInfo.lpBaseOfDll, gameModule, gameInfo.SizeOfImage, nullptr))
        {
            delete[] gameModule;
            logger::error("Disabler Failed: cannot read GameAssembly");
            TerminateProcess(hProcess, 0);
            Sleep(2000);
            exit(0); // NOLINT(concurrency-mt-unsafe)
        }

        // ---------------------------------------------------------------------------
        // BE disable targets on GameAssembly (1.0.6.5.46189):
        //
        // A) load_be_client (forum / UC-style) @ 0x28545E0
        //    AsyncTaskMethodBuilder.Start<AnticheatValidationOperation.<RunValidation>d__10>
        //    This is the ONLY caller of RunValidation.MoveNext. ret here prevents the
        //    validation async method from ever starting (cleaner than ret on MoveNext).
        //
        //    NOT the same as the old EC 90 signature which hit MoveNext @ 0x9D0020 —
        //    ret on MoveNext itself often crashes StartTask.
        //
        // B) BattlEye.BEClient$$Start / $$Init — stop actual BE client load/start.
        // ---------------------------------------------------------------------------

        // Forum load_be_client — prolog of AsyncTaskMethodBuilder.Start<RunValidation>
        // (many similar Start<> clones exist; prefer offset 0x28545E0 + byte check)
        const std::string loadBeClientPattern =
            "48 89 5C 24 ? 48 89 74 24 ? 57 48 83 EC 60 49 8B D8 48 8B F2 49 83 78 38 00 75 20";

        // BattlEye.BEClient$$Start @ 0x657AF0
        const std::string beClientStartPattern =
            "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 48 89 4C 24 ? 41 54 41 56 41 57 48 83 EC 60 41 0F B7 F1 45 8B F0 4C 8B FA";

        // BattlEye.BEClient$$Init @ 0x655700
        const std::string beClientInitPattern =
            "40 53 55 56 41 56 48 83 EC 38 80 3D ? ? ? ? ? 41 0F B7 F1 41 8B E8";

        // Prefer explicit offset — pattern for AsyncTaskMethodBuilder.Start<> is ambiguous
        // (hits many generic clones like @ 0x26DE680 first).
        UINT64 loadBeOffset = 0;
        {
            const uintptr_t cfg_off = globals::get_offset(offset_cfg, "load_be_client");
            if (cfg_off != 0 &&
                offset_matches_pattern(gameModule, gameInfo.SizeOfImage, cfg_off, loadBeClientPattern))
            {
                loadBeOffset = cfg_off;
                logger::info("load_be_client from offsets.cfg");
            }
            else if (offset_matches_pattern(gameModule, gameInfo.SizeOfImage, 0x28545E0, loadBeClientPattern))
            {
                loadBeOffset = 0x28545E0;
                logger::info("load_be_client @ 0x28545E0 (built-in forum RVA)");
            }
            else
            {
                // Last resort: pattern, then verify call to known MoveNext if possible
                loadBeOffset = resolve_in_module(
                    gameModule, gameInfo.SizeOfImage, offset_cfg, pattern_cfg,
                    "load_be_client", loadBeClientPattern);
                if (loadBeOffset != 0 && loadBeOffset != 0x28545E0)
                {
                    bool ok = false;
                    if (loadBeOffset + 0x120 < gameInfo.SizeOfImage)
                    {
                        for (UINT64 off = 0; off + 5 < 0x120; ++off)
                        {
                            const auto* p = reinterpret_cast<const unsigned char*>(
                                gameModule + loadBeOffset + off);
                            if (p[0] != 0xE8)
                                continue;
                            const auto rel = *reinterpret_cast<const int32_t*>(p + 1);
                            const UINT64 tgt = loadBeOffset + off + 5 + static_cast<int64_t>(rel);
                            if (tgt == 0x9D0020)
                            {
                                ok = true;
                                break;
                            }
                        }
                    }
                    if (!ok)
                    {
                        logger::warn("load_be_client pattern ambiguous — forcing 0x28545E0 if valid");
                        loadBeOffset = offset_matches_pattern(
                                           gameModule, gameInfo.SizeOfImage, 0x28545E0, loadBeClientPattern)
                                           ? 0x28545E0
                                           : 0;
                    }
                }
            }
        }

        UINT64 beStartOffset = resolve_in_module(
            gameModule, gameInfo.SizeOfImage, offset_cfg, pattern_cfg,
            "be_client_start", beClientStartPattern);

        UINT64 beInitOffset = resolve_in_module(
            gameModule, gameInfo.SizeOfImage, offset_cfg, pattern_cfg,
            "be_client_init", beClientInitPattern);

        int patches = 0;
        const auto gaBase = reinterpret_cast<uintptr_t>(hGameAssembly);

        if (loadBeOffset != 0)
        {
            // ret — never call RunValidation.MoveNext
            patch_bytes(
                hProcess,
                reinterpret_cast<void*>(gaBase + loadBeOffset), // NOLINT(performance-no-int-to-ptr)
                { 0xC3 });
            logger::info("Patched load_be_client (RunValidation AsyncTaskMethodBuilder.Start) -> ret");
            ++patches;
        }

        if (beStartOffset != 0)
        {
            patch_bytes(
                hProcess,
                reinterpret_cast<void*>(gaBase + beStartOffset), // NOLINT(performance-no-int-to-ptr)
                { 0xC3 });
            logger::info("Patched BattlEye.BEClient.Start -> ret");
            ++patches;
        }

        if (beInitOffset != 0)
        {
            // xor eax, eax; ret  — Init fails cleanly instead of loading BEClient DLL
            patch_bytes(
                hProcess,
                reinterpret_cast<void*>(gaBase + beInitOffset), // NOLINT(performance-no-int-to-ptr)
                { 0x31, 0xC0, 0xC3 });
            logger::info("Patched BattlEye.BEClient.Init -> return 0");
            ++patches;
        }

        delete[] gameModule;

        if (patches == 0)
        {
            logger::error("Disabler Failed: no BE targets found (load_be_client / BEClient Start/Init)");
            TerminateProcess(hProcess, 0);
            Sleep(2000);
            exit(0); // NOLINT(concurrency-mt-unsafe)
        }

        logger::info("Disabler patches applied");
        eft_ready = true;
        return ret;
    }

    return oCreateProcessInternalW(
        hUserToken,
        lpApplicationName,
        lpCommandLine,
        lpProcessAttributes,
        lpThreadAttributes,
        bInheritHandles,
        dwCreationFlags,
        lpEnvironment,
        lpCurrentDirectory,
        lpStartupInfo,
        lpProcessInformation,
        hRestrictedUserToken
    );
}

namespace disabler
{
    DWORD disable()
    {
        std::ifstream config_file(globals::DLL_CONFIG_FILE);
        if (config_file)
        {
            std::string enabled_string;
            for (int i = 0; i < 4; ++i)
            {
                std::getline(config_file, enabled_string);
            }
            enabled_string = enabled_string.substr(9);
            if (enabled_string != "true") return 0;
        }
        else
        {
            return 0;
        }
        logger::info("Disabler Enabled");
        config_file.close();

        DetourRestoreAfterWith();
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());

        oCreateProcessInternalW = reinterpret_cast<CreateProcessInternalW_t>(
            DetourFindFunction("KernelBase.dll", "CreateProcessInternalW")
            );
        DetourAttach(reinterpret_cast<PVOID*>(&oCreateProcessInternalW), hkCreateProcessInternalW); // NOLINT(clang-diagnostic-microsoft-cast)

        if (DetourTransactionCommit() != NO_ERROR)
        {
            logger::error("Failed to hook CreateProcessInternalW");
            return 0;
        }


        while (!eft_ready)
        {
            Sleep(100);
        }
        return eft_pid;
    }

    void exit()
    {
        DetourTransactionBegin();
        DetourUpdateThread(GetCurrentThread());
        DetourDetach(reinterpret_cast<PVOID*>(&oCreateProcessInternalW), hkCreateProcessInternalW); // NOLINT(clang-diagnostic-microsoft-cast)
        DetourTransactionCommit();
    }
}
