// ==========================================================
// AutoPOE2 MemProbe - Cong cu do offset tren client that
// (Giai doan 2 - doc du lieu that tu PathOfExile.exe)
//
// Che do:
//   memprobe.exe map              : ban do module + PE sections
//   memprobe.exe read <addr> <n>  : dump bo nho (addr hex)
//   memprobe.exe aob              : quet cac pattern trong tai lieu 06
//   memprobe.exe scan-hp <value>          : quet toan bo RAM tim u32 == value
//   memprobe.exe scan-hp <value> --rescan : loc danh sach ung vien truoc
//   memprobe.exe probe-pools <lifeHex> --mana 828 --spirit 138 --spirit-cur 8
//
// Tu dong xin quyen Admin (UAC) khi chua elevated.
// Ket qua ghi vao memprobe_report.txt (cung thu muc exe).
// ==========================================================

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <winsock2.h>
#include <windows.h>
#include <tlhelp32.h>
#include <shellapi.h>

#include "memory/rpm_reader.hpp"
#include "memory/aob_scanner.hpp"
#include "memory/terrain_reader.hpp"
#include "memory/offset_registry.hpp"
#include "memory/pe_fingerprint.hpp"
#include "memory/registry_live_label.hpp"
#include "memory/vitals_fingerprint.hpp"
#include "memory/pointer_chain_resolver.hpp"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>

namespace {

bool IsElevated() {
    BOOL elevated = FALSE;
    HANDLE token = nullptr;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
        TOKEN_ELEVATION elev{};
        DWORD size = sizeof(elev);
        if (GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &size)) {
            elevated = elev.TokenIsElevated;
        }
        CloseHandle(token);
    }
    return elevated != FALSE;
}

void RelaunchAsAdmin(int argc, char** argv) {
    wchar_t exePath[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, exePath, MAX_PATH);

    std::wstring params;
    for (int i = 1; i < argc; ++i) {
        if (i > 1) params += L" ";
        params += L"\"" + std::wstring(argv[i], argv[i] + std::strlen(argv[i])) + L"\"";
    }

    SHELLEXECUTEINFOW sei{};
    sei.cbSize = sizeof(sei);
    sei.lpVerb = L"runas";       // Yeu cau UAC
    sei.lpFile = exePath;
    sei.lpParameters = params.c_str();
    sei.nShow = SW_SHOWNORMAL;
    sei.fMask = SEE_MASK_NOCLOSEPROCESS;
    if (!ShellExecuteExW(&sei)) {
        std::cerr << "[MemProbe] Khong the xin thang cap. Loi: " << GetLastError() << std::endl;
        return;
    }
    if (sei.hProcess) {
        WaitForSingleObject(sei.hProcess, INFINITE);
        CloseHandle(sei.hProcess);
    }
}

// Thu muc chua exe (de ghi report - tien trinh elevated co cwd = system32)
std::wstring ExeDir() {
    wchar_t exePath[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, exePath, MAX_PATH);
    std::wstring dir(exePath);
    const auto slash = dir.find_last_of(L"\\/");
    return (slash == std::wstring::npos) ? L"." : dir.substr(0, slash);
}

// Chuyển đường dẫn exe sang narrow AN TOÀN (WideCharToMultiByte).
// KHÔNG dùng std::string(wstr.begin(), wstr.end()) - đã chứng minh bị
// hỏng bộ nhớ trong tiến trình elevated (use-after-free của wstring tạm).
std::string ExeDirNarrow() {
    wchar_t exePath[MAX_PATH] = {};
    GetModuleFileNameW(nullptr, exePath, MAX_PATH);
    char buf[MAX_PATH] = {};
    WideCharToMultiByte(CP_UTF8, 0, exePath, -1, buf, sizeof(buf), nullptr, nullptr);
    std::string s(buf);
    const auto slash = s.find_last_of("\\/");
    return (slash == std::string::npos) ? "." : s.substr(0, slash);
}

std::wstring NarrowToWide(const std::string& s) {
    return std::wstring(s.begin(), s.end());
}

void AppendReport(const std::string& line) {
    std::ofstream f(ExeDir() + L"\\memprobe_report.txt", std::ios::app);
    f << line << "\n";
}

std::string Hex(uint64_t v) {
    char buf[32];
    std::snprintf(buf, sizeof(buf), "%llX", static_cast<unsigned long long>(v));
    return buf;
}

void HexDump(const uint8_t* data, size_t size, uintptr_t baseAddr) {
    for (size_t i = 0; i < size; i += 16) {
        std::printf("%012llX  ", static_cast<unsigned long long>(baseAddr + i));
        for (size_t j = 0; j < 16; ++j) {
            if (i + j < size) std::printf("%02X ", data[i + j]);
            else std::printf("   ");
            if (j == 7) std::printf(" ");
        }
        std::printf(" |");
        for (size_t j = 0; j < 16 && i + j < size; ++j) {
            const uint8_t c = data[i + j];
            std::printf("%c", (c >= 0x20 && c < 0x7F) ? static_cast<char>(c) : '.');
        }
        std::printf("|\n");
    }
}

} // namespace

namespace {

// ---- Che do "map": liet ke module + section cua exe chinh ----
void CmdMap(ReadProcessMemoryReader& reader, uint32_t pid) {
    (void)reader;
    AppendReport("=== MAP (PID " + std::to_string(pid) + ") ===");

    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
    if (snap == INVALID_HANDLE_VALUE) {
        AppendReport("[MAP] CreateToolhelp32Snapshot that bai");
        return;
    }

    MODULEENTRY32W me{};
    me.dwSize = sizeof(me);
    std::wstring exePath;
    if (Module32FirstW(snap, &me)) {
        do {
            const std::string line = "  " + std::string(me.szModule, me.szModule + wcslen(me.szModule))
                + "  base=0x" + Hex(reinterpret_cast<uintptr_t>(me.modBaseAddr))
                + "  size=0x" + Hex(me.modBaseSize);
            std::cout << line << std::endl;
            AppendReport(line);
            if (_wcsicmp(me.szModule, L"PathOfExile.exe") == 0) {
                exePath = me.szExePath;
            }
        } while (Module32NextW(snap, &me));
    }
    CloseHandle(snap);

    // Phan tich PE section tu file tren dia (doc cau truc, khong doc RAM)
    if (exePath.empty()) return;
    HANDLE hFile = CreateFileW(exePath.c_str(), GENERIC_READ, FILE_SHARE_READ,
                               nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (hFile == INVALID_HANDLE_VALUE) return;

    auto sectionLine = [&](const std::string& s) {
        std::cout << s << std::endl;
        AppendReport(s);
    };
    sectionLine("  --- PE Sections (PathOfExile.exe) ---");

    auto readAt = [&](void* buf, DWORD size, DWORD off) {
        SetFilePointer(hFile, off, nullptr, FILE_BEGIN);
        DWORD got = 0;
        return ReadFile(hFile, buf, size, &got, nullptr) && got == size;
    };

    IMAGE_DOS_HEADER dos{};
    if (!readAt(&dos, sizeof(dos), 0) || dos.e_magic != IMAGE_DOS_SIGNATURE) {
        CloseHandle(hFile); return;
    }
    DWORD ntOff = dos.e_lfanew;
    DWORD sig = 0;
    if (!readAt(&sig, sizeof(sig), ntOff) || sig != IMAGE_NT_SIGNATURE) {
        CloseHandle(hFile); return;
    }
    IMAGE_FILE_HEADER ifh{};
    if (!readAt(&ifh, sizeof(ifh), ntOff + 4)) { CloseHandle(hFile); return; }
    const DWORD optSize = ifh.SizeOfOptionalHeader;
    const DWORD secTableOff = ntOff + 4 + sizeof(IMAGE_FILE_HEADER) + optSize;

    for (WORD i = 0; i < ifh.NumberOfSections; ++i) {
        IMAGE_SECTION_HEADER sh{};
        if (!readAt(&sh, sizeof(sh), secTableOff + i * sizeof(sh))) break;
        char name[9] = {};
        std::memcpy(name, sh.Name, 8);
        sectionLine("  .section " + std::string(name)
            + "  VA=0x" + Hex(sh.VirtualAddress)
            + "  VSize=0x" + Hex(sh.Misc.VirtualSize));
    }
    CloseHandle(hFile);
}

// ---- Che do "read": dump bo nho tai dia chi ----
void CmdRead(ReadProcessMemoryReader& reader, uintptr_t addr, size_t len) {
    if (len > 4096) len = 4096;
    std::vector<uint8_t> buf(len, 0);
    AppendReport("=== READ 0x" + Hex(addr) + " (" + std::to_string(len) + " bytes) ===");
    if (reader.Read(addr, buf.data(), len)) {
        // Ghi TOÀN BỘ hexdump vào report (console elevated có thể không nhìn thấy)
        std::string dump;
        for (size_t i = 0; i < len; i += 16) {
            char row[32];
            std::snprintf(row, sizeof(row), "%012llX  ",
                static_cast<unsigned long long>(addr + i));
            dump += row;
            for (size_t j = 0; j < 16; ++j) {
                if (i + j < len) {
                    char b[4];
                    std::snprintf(b, sizeof(b), "%02X ", buf[i + j]);
                    dump += b;
                } else {
                    dump += "   ";
                }
                if (j == 7) dump += " ";
            }
            dump += " |";
            for (size_t j = 0; j < 16 && i + j < len; ++j) {
                const uint8_t c = buf[i + j];
                dump += (c >= 0x20 && c < 0x7F) ? static_cast<char>(c) : '.';
            }
            dump += "|\n";
        }
        AppendReport(dump);
    } else {
        AppendReport("  [FAIL] Khong doc duoc (dia chi khong hop le / khong commit)");
        std::cout << "[FAIL] Khong doc duoc dia chi 0x" << Hex(addr) << std::endl;
    }
}

} // namespace

namespace {

// ---- Che do "aob": quet pattern trong tai lieu 06 tren section .text ----
void CmdAob(ReadProcessMemoryReader& reader, uint32_t pid) {
    AppendReport("=== AOB SCAN (PID " + std::to_string(pid) + ") ===");

    ModuleInfo mainMod;
    if (!AobScanner::FindModule(pid, L"PathOfExile.exe", mainMod)) {
        AppendReport("[AOB] Khong tim thay module PathOfExile.exe");
        return;
    }
    AppendReport("  Module: base=0x" + Hex(mainMod.base) + " size=0x" + Hex(mainMod.size));

    // Cac pattern khuyen nghi trong docs/development/06 muc 3
    const std::vector<std::pair<std::string, std::string>> patterns = {
        {"InGameStateBase", "48 8B 05 ? ? ? ? 48 8B 40 38 48 8B 88 ? ? ? ? 48 85 C9 74"},
        {"VolumetricFog",   "48 8B 0D ? ? ? ? F3 0F 11 05 ? ? ? ? 48 85 C9 74 ? E8"},
        {"EntityListRoot",  "48 8B 0D ? ? ? ? E8 ? ? ? ? 48 8B F8 48 85 C0 74 ? 48 8B 58 08"},
        {"AreaExploration", "48 8B 05 ? ? ? ? 48 8B 40 38 48 8B 88 ? ? ? ? 48 85 C9 74"},
    };

    for (const auto& kv : patterns) {
        const auto p = AobPattern::Parse(kv.second);
        if (!p.Valid()) { AppendReport("  " + kv.first + ": pattern sai cu phap"); continue; }

        const uintptr_t hit = AobScanner::ScanModule(reader, mainMod, p);
        const std::string line = hit != 0
            ? "  [MATCH] " + kv.first + " @ 0x" + Hex(hit)
            : "  [MISS]  " + kv.first + " (pattern khong khop ban build nay)";
        std::cout << line << std::endl;
        AppendReport(line);
    }
}

} // namespace

namespace {

// ---- Che do "scan-hp": differential scan tim dia chi HP ----
// rescanFile       : loc chinh xac theo gia tri moi
// rescanChangedFile: loc kieu "da thay doi" - bo dia chi van giu gia tri cu
void CmdScanHp(ReadProcessMemoryReader& reader, uint32_t value,
               const std::string& rescanFile, const std::string& rescanChangedFile) {
    // Dùng đường dẫn WIDE trực tiếp (tránh hỏng khi narrow hoá)
    const std::wstring candPathW = ExeDir() + L"\\hp_candidates.txt";

    std::vector<uintptr_t> candidates;

    if (rescanFile.empty() && rescanChangedFile.empty()) {
        // Lượt 1: quét toàn bộ bộ nhớ committed readable
        AppendReport("=== SCAN-HP value=" + std::to_string(value) + " (full scan) ===");
        std::cout << "[Scan] Dang quet toan bo bo nho game... co the mat 30-90 giay." << std::endl;

        std::vector<uint8_t> buf(1024 * 1024);
        uintptr_t addr = 0x10000;
        MEMORY_BASIC_INFORMATION mbi{};
        size_t scannedMB = 0;

        while (VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(addr),
                              &mbi, sizeof(mbi)) != 0) {
            const uintptr_t regionBase = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
            const bool readable = (mbi.State == MEM_COMMIT)
                && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))
                && mbi.Protect != 0;
            if (readable && mbi.RegionSize <= (512u << 20)) {
                for (size_t off = 0; off + sizeof(uint32_t) <= mbi.RegionSize;
                     off += buf.size()) {
                    const size_t n = (std::min<size_t>)(buf.size(),
                        static_cast<size_t>(mbi.RegionSize) - off);
                    if (reader.Read(regionBase + off, buf.data(), n)) {
                        for (size_t i = 0; i + sizeof(uint32_t) <= n; i += 4) {
                            uint32_t v;
                            std::memcpy(&v, buf.data() + i, sizeof(v));
                            if (v == value) candidates.push_back(regionBase + off + i);
                        }
                    }
                }
                scannedMB += static_cast<size_t>(mbi.RegionSize) / (1024 * 1024);
                if (scannedMB % 512 == 0) {
                    AppendReport("  [progress] " + std::to_string(scannedMB)
                        + " MB, " + std::to_string(candidates.size()) + " ung vien");
                }
            }
            const uintptr_t next = regionBase + mbi.RegionSize;
            if (next <= regionBase) break;
            addr = next;
        }
    } else if (!rescanChangedFile.empty()) {
        // Lọc kiểu "giá trị ĐÃ THAY ĐỔI": bỏ các địa chỉ vẫn giữ giá trị cũ
        AppendReport("=== SCAN-HP rescan-changed (bo dia chi van == "
            + std::to_string(value) + ") ===");
        std::ifstream f(rescanChangedFile);
        if (!f) {
            AppendReport("[ERROR] Khong doc duoc file ung vien: " + rescanChangedFile);
            return;
        }
        std::string line;
        while (std::getline(f, line)) {
            if (line.size() < 4) continue;
            const uintptr_t addr = std::strtoull(line.c_str(), nullptr, 16);
            uint32_t v = 0;
            if (reader.Read(addr, &v, sizeof(v)) && v != value) candidates.push_back(addr);
        }
    } else {
        // Lọc chính xác theo giá trị mới
        AppendReport("=== SCAN-HP rescan value=" + std::to_string(value) + " ===");
        std::ifstream f(rescanFile);
        if (!f) {
            AppendReport("[ERROR] Khong doc duoc file ung vien: " + rescanFile);
            return;
        }
        std::string line;
        while (std::getline(f, line)) {
            if (line.size() < 4) continue;
            const uintptr_t addr = std::strtoull(line.c_str(), nullptr, 16);
            uint32_t v = 0;
            if (reader.Read(addr, &v, sizeof(v)) && v == value) candidates.push_back(addr);
        }
    }

    // Ghi danh sách ứng viên (path wide + kiểm tra lỗi mở file)
    {
        std::ofstream f(candPathW, std::ios::trunc);
        if (!f.is_open()) {
            AppendReport("[ERROR] Khong ghi duoc hp_candidates.txt (loi "
                + std::to_string(GetLastError()) + ")");
            return;
        }
        for (uintptr_t c : candidates) f << Hex(c) << "\n";
        AppendReport("  [OK] Da ghi " + std::to_string(candidates.size())
            + " ung vien vao hp_candidates.txt");
    }

    const std::string summary = "  Ket qua: " + std::to_string(candidates.size()) + " ung vien";
    AppendReport(summary);
    std::cout << summary << std::endl;

    const size_t show = (std::min<size_t>)(30, candidates.size());
    for (size_t i = 0; i < show; ++i) {
        AppendReport("    0x" + Hex(candidates[i]));
    }
    if (candidates.size() > 30) {
        AppendReport("    ... va " + std::to_string(candidates.size() - 30)
            + " dia chi khac trong hp_candidates.txt");
    }
    AppendReport("  Huong dan: de nhan vat mat/hoi mau roi chay: memprobe.exe scan-hp <soMoi> --rescan hp_candidates.txt");
    AppendReport("[SCAN DONE]");
}

// ---- Che do "ptrscan": tim pointer chain tu static .data den dia chi HP ----
namespace {

// Quet toan bo memory, thu thap (diaChiLuuTru, giaTri) voi giaTri nam trong
// 1 khung [lo, hi) cua danh sach windows.
void ScanPointerPass(ReadProcessMemoryReader& reader,
                     const std::vector<std::pair<uintptr_t, uintptr_t>>& windows,
                     std::vector<std::pair<uintptr_t, uintptr_t>>& out,
                     const std::string& label) {
    std::vector<std::pair<uintptr_t, uintptr_t>> sorted = windows;
    std::sort(sorted.begin(), sorted.end());

    std::vector<uint8_t> buf(1024 * 1024);
    uintptr_t addr = 0x10000;
    MEMORY_BASIC_INFORMATION mbi{};
    size_t scannedMB = 0;

    AppendReport("  [pass " + label + "] bat dau quet...");
    while (VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(addr),
                          &mbi, sizeof(mbi)) != 0) {
        const uintptr_t regionBase = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
        const bool readable = (mbi.State == MEM_COMMIT)
            && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))
            && mbi.Protect != 0;
        if (readable && mbi.RegionSize <= (512u << 20)) {
            for (size_t off = 0; off + sizeof(uint64_t) <= mbi.RegionSize;
                 off += buf.size()) {
                const size_t n = (std::min<size_t>)(buf.size(),
                    static_cast<size_t>(mbi.RegionSize) - off);
                if (reader.Read(regionBase + off, buf.data(), n)) {
                    for (size_t i = 0; i + sizeof(uint64_t) <= n; i += 8) {
                        uint64_t v;
                        std::memcpy(&v, buf.data() + i, sizeof(v));
                        if (v < 0x10000) continue;
                        auto it = std::upper_bound(sorted.begin(), sorted.end(),
                            std::make_pair(v, uintptr_t(0)));
                        if (it == sorted.begin()) continue;
                        --it;
                        if (v >= it->first && v < it->second) {
                            out.push_back({regionBase + off + i, v});
                        }
                    }
                }
            }
            scannedMB += static_cast<size_t>(mbi.RegionSize) / (1024 * 1024);
            if (scannedMB % 1024 == 0) {
                AppendReport("    [progress] " + std::to_string(scannedMB) + " MB, "
                    + std::to_string(out.size()) + " match");
            }
        }
        const uintptr_t next = regionBase + mbi.RegionSize;
        if (next <= regionBase) break;
        addr = next;
    }
    AppendReport("  [pass " + label + "] xong: " + std::to_string(out.size()) + " match");
}

// ---- Che do "entscan": tim Entity Pointer Array tu dia chi PlayerState ----
// Y tuong: player cung la 1 entity -> ton tai 1 entry trong Entity Pointer Array
// tro vao player entity base. Tu entry do, di doc ca mang (stride 8) va doc
// {HP} tai cung offset de dem so entity hop le.
void CmdEntScan(ReadProcessMemoryReader& reader, uintptr_t statAddr) {
    AppendReport("=== ENTSCAN statAddr=0x" + Hex(statAddr) + " ===");

    uint32_t curHP = 0;
    if (!reader.ReadValue<uint32_t>(statAddr, curHP)) {
        AppendReport("[ERROR] Stat block khong con hop le (game relog?) - chay lai findplayer.");
        return;
    }
    AppendReport("  Stat block CON SONG, gia tri = " + std::to_string(curHP));

    // Pass A: tìm mọi ô nhớ lưu con trỏ trỏ vào player entity object
    // (entry của player trong Entity Pointer Array nằm trong số này)
    std::vector<std::pair<uintptr_t, uintptr_t>> ptrs; // (L, V)
    const uintptr_t wLo = statAddr > 0x40000 ? statAddr - 0x40000 : 0;
    ScanPointerPass(reader, {{wLo, statAddr + 16}}, ptrs, "A");
    AppendReport("  Pass A: " + std::to_string(ptrs.size())
        + " o nho chua con tro tro vao player entity object");
    if (ptrs.empty()) {
        AppendReport("[ENTSCAN DONE]");
        return;
    }

    // Ưu tiên V gần statAddr nhất (entity base)
    std::sort(ptrs.begin(), ptrs.end(), [](const auto& a, const auto& b) {
        return a.second > b.second;
    });

    // Pass B: với từng (L, V): statOff = statAddr - V; đi dọc mảng ±128 entry
    // (stride 8), đọc u32 tại (E + statOff) -> đếm HP hợp lệ (0 < hp <= 500)
    struct ArrayHit {
        uintptr_t slotAddr;
        uintptr_t statOff;
        int plausible;
        int totalRead;
        std::vector<std::pair<int, uintptr_t>> slots;   // (index, entity base)
        std::vector<std::pair<int, uint32_t>> hps;      // (index, HP)
    };
    std::vector<ArrayHit> hits;
    int tested = 0;
    uintptr_t lastV = 0;
    for (const auto& pv : ptrs) {
        if (pv.second == 0 || statAddr < pv.second) continue;
        const uintptr_t statOff = statAddr - pv.second;
        if (statOff > 0x40000) continue;
        if (pv.second == lastV) continue; // cùng base, đã thử
        lastV = pv.second;
        if (tested >= 16) break;
        ++tested;

        ArrayHit hit{};
        hit.slotAddr = pv.first;
        hit.statOff = statOff;

        for (int i = -128; i <= 128; ++i) {
            const uintptr_t slot = pv.first + static_cast<int64_t>(8) * i;
            uint64_t ent = 0;
            if (!reader.ReadValue<uint64_t>(slot, ent) || ent < 0x10000
                || ent > 0x7FFFFFFEFFFF) {
                continue;
            }
            ++hit.totalRead;
            uint32_t hp = 0;
            if (reader.ReadValue<uint32_t>(ent + statOff, hp) && hp > 0 && hp <= 500) {
                ++hit.plausible;
                if ((int)hit.slots.size() < 96) {
                    hit.slots.push_back({i, ent});
                    hit.hps.push_back({i, hp});
                }
            }
        }
        hits.push_back(hit);
        const std::string l = "  Array@0x" + Hex(pv.first) + " statOff=0x"
            + Hex(statOff) + ": " + std::to_string(hit.plausible) + "/"
            + std::to_string(hit.totalRead) + " HP hop le";
        AppendReport(l);
        std::cout << l << std::endl;
    }

    // Chọn array tốt nhất (nhiều HP hợp lệ nhất)
    ArrayHit* best = nullptr;
    for (auto& h : hits) {
        if (!best || h.plausible > best->plausible) best = &h;
    }
    if (!best || best->plausible < 8) {
        AppendReport("  Khong tim thay entity array hap dan - can mo rong cua so/offset.");
        AppendReport("[ENTSCAN DONE]");
        return;
    }

    AppendReport("  >>> ENTITY ARRAY TIM THAY! <<<");
    AppendReport("  Array slot (player entry) = 0x" + Hex(best->slotAddr));
    AppendReport("  statOff (HP offset trong entity) = 0x" + Hex(best->statOff));
    AppendReport("  So entry doc duoc: " + std::to_string(best->totalRead)
        + " | HP hop le: " + std::to_string(best->plausible));

    // Dump 30 entity đầu tiên + săn tên string trong object
    int dumped = 0;
    for (size_t i = 0; i < best->slots.size() && dumped < 30; ++i) {
        const int idx = best->slots[i].first;
        const uintptr_t ent = best->slots[i].second;
        const uintptr_t hpAddr = ent + best->statOff;
        uint32_t hp = best->hps[i].second;

        // Săn chuỗi ASCII trong 512 bytes đầu của entity object
        std::string name = "?";
        std::vector<uint8_t> obj(512, 0);
        if (reader.Read(ent, obj.data(), obj.size())) {
            size_t run = 0, bestRun = 0, bestStart = 0;
            for (size_t b = 0; b < obj.size(); ++b) {
                const uint8_t ch = obj[b];
                if (ch >= 0x20 && ch < 0x7F) {
                    if (run == 0) bestStart = b;
                    ++run;
                    if (run > bestRun) { bestRun = run; }
                } else {
                    if (run >= 5 && bestStart + run <= obj.size()) { break; }
                    run = 0;
                }
            }
            if (bestRun >= 5) {
                name = std::string(reinterpret_cast<char*>(obj.data() + bestStart), bestRun);
            }
        }

        const std::string l = "    [" + std::to_string(idx) + "] ent=0x" + Hex(ent)
            + " HP=" + std::to_string(hp) + " name~\"" + name + "\"";
        AppendReport(l);
        std::cout << l << std::endl;
        ++dumped;
    }
    AppendReport("[ENTSCAN DONE]");
}

void CmdPtrScan(ReadProcessMemoryReader& reader, uintptr_t target) {
    AppendReport("=== PTRSCAN target=0x" + Hex(target) + " ===");

    // 0. Xác nhận địa chỉ còn hợp lệ (game chưa thoát/relog)
    uint32_t curVal = 0;
    if (!reader.Read(target, &curVal, sizeof(curVal))) {
        AppendReport("[ERROR] Khong doc duoc target - game da thoat/relog? Chay lai scan-hp.");
        return;
    }
    AppendReport("  Gia tri hien tai tai target = " + std::to_string(curVal));

    // 1. Phạm vi allocation chứa target
    MEMORY_BASIC_INFORMATION mbi{};
    if (!VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(target),
                        &mbi, sizeof(mbi))) {
        AppendReport("[ERROR] VirtualQueryEx that bai");
        return;
    }
    const uintptr_t allocBase = reinterpret_cast<uintptr_t>(mbi.AllocationBase);
    uintptr_t spanHi = allocBase;
    {
        uintptr_t cur = allocBase;
        MEMORY_BASIC_INFORMATION t{};
        while (VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(cur),
                              &t, sizeof(t))) {
            if (t.AllocationBase != mbi.AllocationBase) break;
            spanHi = cur + t.RegionSize;
            cur = spanHi;
            if (spanHi - allocBase > (256u << 20)) break;
        }
    }
    AppendReport("  Allocation: base=0x" + Hex(allocBase) + " span=0x"
        + Hex(spanHi - allocBase));

    ModuleInfo mainMod;
    const bool hasMod = AobScanner::FindModule(
        FindProcessId(L"PathOfExile.exe"), L"PathOfExile.exe", mainMod);

    // 2. Pass 1: mọi con trỏ u64 trỏ vào allocation này
    std::vector<std::pair<uintptr_t, uintptr_t>> level1; // (A1, P1)
    ScanPointerPass(reader, {{allocBase, spanHi}}, level1, "1");

    // 3. Phân loại: static trỏ trực tiếp = chain cấp 1; heap = trung gian
    std::vector<std::pair<uintptr_t, uintptr_t>> heapPtrs;
    for (const auto& ap : level1) {
        const uintptr_t a1 = ap.first;
        const uintptr_t p1 = ap.second;
        if (p1 > target || target - p1 > 0x10000) continue; // HP phải nằm sau block base
        const uintptr_t off1 = target - p1;
        const bool isStatic = hasMod && a1 >= mainMod.base
            && a1 < mainMod.base + mainMod.size;
        if (isStatic) {
            AppendReport("  [CHAIN L1 - STATIC] [0x" + Hex(a1) + "] + 0x"
                + Hex(off1) + " -> HP");
        } else {
            heapPtrs.push_back({a1, p1});
        }
    }
    AppendReport("  Trung gian heap: " + std::to_string(heapPtrs.size())
        + " con tro (dung cho Pass 2)");

    if (heapPtrs.empty()) {
        AppendReport("[PTRSCAN DONE]");
        return;
    }

    // 4. Pass 2: tìm con trỏ trỏ đến (gần) từng A1 trung gian
    std::vector<std::pair<uintptr_t, uintptr_t>> windows;
    for (const auto& hp : heapPtrs) {
        const uintptr_t a1 = hp.first;
        windows.push_back({a1 > 0x10000 ? a1 - 0x10000 : 0, a1 + 8});
    }
    std::sort(windows.begin(), windows.end());
    windows.erase(std::unique(windows.begin(), windows.end()), windows.end());

    std::vector<std::pair<uintptr_t, uintptr_t>> level2; // (A2, Q)
    ScanPointerPass(reader, windows, level2, "2");

    // 5. Ghép chain cấp 2: A2 -> Q gần A1 -> P1 gần HP
    uint32_t chains = 0;
    for (const auto& l2 : level2) {
        const uintptr_t a2 = l2.first;
        const uintptr_t q = l2.second;
        // Q phải trỏ đúng vào (hoặc ngay trước) 1 A1
        for (const auto& hp : heapPtrs) {
            const uintptr_t a1 = hp.first;
            if (q > a1 || a1 - q > 0x10000) continue;
            const uintptr_t p1 = hp.second;
            const uintptr_t off1 = target - p1;
            const uintptr_t off2 = a1 - q;
            const bool a2Static = hasMod && a2 >= mainMod.base
                && a2 < mainMod.base + mainMod.size;
            ++chains;
            if (a2Static) {
                AppendReport("  [CHAIN L2 - STATIC] [[0x" + Hex(a2) + "]+0x"
                    + Hex(off2) + "] + 0x" + Hex(off1) + " -> HP");
            } else if (chains <= 40) {
                AppendReport("  [heap L2] 0x" + Hex(a2) + " +0x" + Hex(off2)
                    + " -> 0x" + Hex(a1) + " +0x" + Hex(off1) + " -> HP");
            }
        }
    }
    if (chains == 0) {
        AppendReport("  Khong tim thay chain cap 2 - chuyen sang pass 3 (them 1 tang heap).");
    }

    // 6. Pass 3: lặp lại với các A2 trung gian — tìm static ở tầng kế
    if (!level2.empty()) {
        // Map: A2 -> [(Q, A1)] ; A1 -> P1 (dùng hash để tránh vòng lặp 3 tầng)
        std::unordered_map<uintptr_t, std::vector<std::pair<uintptr_t, uintptr_t>>> a2Map;
        std::unordered_map<uintptr_t, uintptr_t> a1ToP1;
        for (const auto& hp : heapPtrs) a1ToP1[hp.first] = hp.second;
        for (const auto& l2 : level2) {
            for (const auto& hp : heapPtrs) {
                const uintptr_t a1 = hp.first;
                const uintptr_t q = l2.second;
                if (q <= a1 && a1 - q <= 0x10000) {
                    a2Map[l2.first].push_back({q, a1});
                }
            }
        }
        std::vector<uintptr_t> keys;
        keys.reserve(a2Map.size());
        for (const auto& kv : a2Map) keys.push_back(kv.first);
        std::sort(keys.begin(), keys.end());

        // Windows cho pass 3: R phải thỏa R <= A2 <= R + 0x10000
        std::vector<std::pair<uintptr_t, uintptr_t>> windows3;
        for (uintptr_t k : keys) {
            windows3.push_back({k > 0x10000 ? k - 0x10000 : 0, k + 8});
        }
        std::sort(windows3.begin(), windows3.end());
        windows3.erase(std::unique(windows3.begin(), windows3.end()), windows3.end());

        std::vector<std::pair<uintptr_t, uintptr_t>> level3; // (A3, R)
        ScanPointerPass(reader, windows3, level3, "3");

        uint32_t chains3 = 0;
        for (const auto& l3 : level3) {
            const uintptr_t a3 = l3.first;
            const uintptr_t r = l3.second;
            // A2 ∈ [r, r + 0x10000]
            auto it = std::lower_bound(keys.begin(), keys.end(), r);
            for (; it != keys.end() && *it <= r + 0x10000; ++it) {
                const uintptr_t a2 = *it;
                for (const auto& q1 : a2Map[a2]) {
                    auto pit = a1ToP1.find(q1.second);
                    if (pit == a1ToP1.end()) continue;
                    const uintptr_t off1 = target - pit->second;
                    const uintptr_t off2 = q1.second - q1.first;
                    const uintptr_t off3 = a2 - r;
                    ++chains3;
                    AppendReport("  [CHAIN L3 - STATIC] [[[0x" + Hex(a3) + "]+0x"
                        + Hex(off3) + "]+0x" + Hex(off2) + "] + 0x" + Hex(off1)
                        + " -> HP");
                    if (chains3 >= 25) {
                        AppendReport("  (du 25 chain - ngung liet ke)");
                        AppendReport("[PTRSCAN DONE]");
                        return;
                    }
                }
            }
        }
        AppendReport("  Pass 3: " + std::to_string(chains3) + " static chain tim duoc");
    }
    AppendReport("[PTRSCAN DONE]");
}

} // namespace

// ---- Che do "ptrdeep": BFS tu dong tu HP den static root (nhieu pass) ----
namespace {

struct DeepNode {
    uintptr_t addr;   // dia chi o nhan con tro
    uintptr_t value;  // gia tri con tro tai o do (= *(addr))
    int parent;       // chi so node cha trong vector (-1 = target goc)
    int depth;
};

void CmdPtrDeep(ReadProcessMemoryReader& reader, uintptr_t target,
                size_t maxOff, int maxDepth) {
    AppendReport("=== PTRDEEP target=0x" + Hex(target) + " maxOff=0x" + Hex(maxOff)
        + " maxDepth=" + std::to_string(maxDepth) + " ===");

    uint32_t curVal = 0;
    if (!reader.Read(target, &curVal, sizeof(curVal))) {
        AppendReport("[ERROR] target khong con hop le (game relog?) - chay lai scan-hp truoc.");
        return;
    }
    AppendReport("  Gia tri hien tai = " + std::to_string(curVal));

    // Danh sach TAT CA module (root static co the nam trong DLL cua game)
    std::vector<std::pair<uintptr_t, uintptr_t>> modules;
    {
        const uint32_t pid = FindProcessId(L"PathOfExile.exe");
        HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
        if (snap != INVALID_HANDLE_VALUE) {
            MODULEENTRY32W me{};
            me.dwSize = sizeof(me);
            if (Module32FirstW(snap, &me)) {
                do {
                    modules.push_back({reinterpret_cast<uintptr_t>(me.modBaseAddr),
                                       static_cast<uintptr_t>(me.modBaseSize)});
                } while (Module32NextW(snap, &me));
            }
            CloseHandle(snap);
        }
    }
    AppendReport("  So module: " + std::to_string(modules.size()));
    auto isStatic = [&](uintptr_t a) {
        for (const auto& m : modules)
            if (a >= m.first && a < m.first + m.second) return true;
        return false;
    };

    constexpr size_t kMaxNodesPerDepth = 50000;
    const size_t kMaxOff = maxOff;
    const int kMaxDepth = maxDepth;

    std::vector<DeepNode> nodes;
    nodes.push_back({target, 0, -1, 0});
    std::vector<size_t> frontier = {0};
    std::unordered_set<uintptr_t> seen;
    seen.insert(target);

    for (int depth = 1; depth <= kMaxDepth && !frontier.empty(); ++depth) {
        // Xay windows tu frontier
        std::vector<std::pair<uintptr_t, uintptr_t>> windows;
        for (size_t idx : frontier) {
            const uintptr_t a = nodes[idx].addr;
            windows.push_back({a > kMaxOff ? a - kMaxOff : 0, a + 8});
        }
        std::sort(windows.begin(), windows.end());
        windows.erase(std::unique(windows.begin(), windows.end()), windows.end());

        std::vector<std::pair<uintptr_t, uintptr_t>> matches; // (L, V)
        ScanPointerPass(reader, windows, matches, std::to_string(depth));

        // Sorted frontier addresses de khop cha nhanh
        std::vector<uintptr_t> parentAddrs;
        for (size_t idx : frontier) parentAddrs.push_back(nodes[idx].addr);
        std::sort(parentAddrs.begin(), parentAddrs.end());

        std::vector<size_t> newFrontier;
        int staticFound = 0;
        int staticNodeIdx = -1;
        AppendReport("  [depth " + std::to_string(depth) + "] matches="
            + std::to_string(matches.size()));

        for (const auto& mv : matches) {
            if (newFrontier.size() >= kMaxNodesPerDepth) break;
            const uintptr_t L = mv.first;
            const uintptr_t V = mv.second;
            if (L == target || seen.count(L)) continue;

            // Tim cha: parentAddrs[i] ∈ [V, V + kMaxOff]
            auto it = std::lower_bound(parentAddrs.begin(), parentAddrs.end(), V);
            for (; it != parentAddrs.end() && *it <= V + kMaxOff; ++it) {
                const uintptr_t parentAddr = *it;
                // Tìm index node cha theo addr (frontier duy nhất theo addr)
                size_t pIdx = SIZE_MAX;
                for (size_t idx : frontier) {
                    if (nodes[idx].addr == parentAddr) { pIdx = idx; break; }
                }
                if (pIdx == SIZE_MAX) continue;

                seen.insert(L);
                nodes.push_back({L, V, static_cast<int>(pIdx), depth});
                newFrontier.push_back(nodes.size() - 1);

                if (isStatic(L)) {
                    AppendReport("  [STATIC ROOT] 0x" + Hex(L)
                        + " (depth " + std::to_string(depth) + ")");
                    staticNodeIdx = static_cast<int>(nodes.size() - 1);
                    ++staticFound;
                }
                break; // 1 L chi gan vao 1 cha dau tien tim được
            }
        }
        if (staticNodeIdx >= 0) {
            // Dựng lại chain từ static node: mỗi cạnh có offset = parent.addr - node.value
            // (giá trị tại node trỏ đến parent slot; HP nằm tại parent + off)
            AppendReport("[PTRDEEP DONE] Tim thay " + std::to_string(staticFound)
                + " static chain o depth " + std::to_string(depth));

            int cur = staticNodeIdx;
            std::vector<std::pair<uintptr_t, uintptr_t>> edges; // (node.addr, off)
            while (nodes[cur].parent >= 0) {
                const int p = nodes[cur].parent;
                edges.push_back({nodes[cur].addr, nodes[p].addr - nodes[cur].value});
                cur = p;
            }
            // cur = node gốc (depth 0, addr = target)
            // Chuỗi: HP = *(u32*)( *( *(S) + off1 ) + off2 ... )
            const uintptr_t staticAddr = nodes[staticNodeIdx].addr;
            std::string chain = "  [CHAIN] S=0x" + Hex(staticAddr) + " offsets=";
            for (const auto& e : edges) chain += "+0x" + Hex(e.second) + " ";
            chain += "-> HP (u32) | off_hien_tai: ";
            for (size_t i = 0; i < edges.size(); ++i) {
                chain += "off" + std::to_string(i + 1) + "=0x" + Hex(edges[i].second) + " ";
            }
            AppendReport(chain);
            std::cout << chain << std::endl;

            // Xác định module chứa static + RVA (RVA ổn định qua relog nếu module là DLL của game)
            for (const auto& m : modules) {
                if (staticAddr >= m.first && staticAddr < m.first + m.second) {
                    AppendReport("  Static nam trong module base=0x" + Hex(m.first)
                        + " size=0x" + Hex(m.second) + " RVA=0x"
                        + Hex(staticAddr - m.first));
                    break;
                }
            }
            return;
        }
        frontier = newFrontier;
        AppendReport("  [depth " + std::to_string(depth) + "] frontier="
            + std::to_string(frontier.size()));
    }
    AppendReport("[PTRDEEP DONE] Het " + std::to_string(kMaxDepth)
        + " tang - chua gap static root. Co the can tang kMaxDepth/kMaxOff.");
}

// ---- Che do "findplayer": tim stat block qua pattern (HP, maxHP, maxHP) ----
// Chien luoc chot: maxHP la "dau van tay" on dinh cua nhan vat - khong can
// pointer chain, khong can biet truoc dia chi. Moi session chi can quet lai.
void CmdFindPlayer(ReadProcessMemoryReader& reader, uint32_t maxHP,
                   const std::string& rescanFile, int rescanHP) {
    const std::wstring candPathW = ExeDir() + L"\\player_candidates.txt";
    std::vector<std::pair<uintptr_t, uint32_t>> cands; // (addr, HP hien tai)

    if (rescanFile.empty()) {
        // Lượt 1: quét toàn bộ memory tìm triple {X ≤ maxHP, maxHP, maxHP}
        AppendReport("=== FINDPLAYER maxHP=" + std::to_string(maxHP) + " (full scan) ===");
        std::cout << "[Scan] Dang tim stat block {HP, maxHP, maxHP}..." << std::endl;

        std::vector<uint8_t> buf(1024 * 1024);
        uintptr_t addr = 0x10000;
        MEMORY_BASIC_INFORMATION mbi{};

        while (VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(addr),
                              &mbi, sizeof(mbi)) != 0) {
            const uintptr_t regionBase = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
            const bool readable = (mbi.State == MEM_COMMIT)
                && !(mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD))
                && mbi.Protect != 0;
            if (readable && mbi.RegionSize <= (512u << 20)) {
                for (size_t off = 0; off + 12 <= mbi.RegionSize; off += buf.size()) {
                    const size_t n = (std::min<size_t>)(buf.size(),
                        static_cast<size_t>(mbi.RegionSize) - off);
                    if (reader.Read(regionBase + off, buf.data(), n)) {
                        for (size_t i = 0; i + 12 <= n; i += 4) {
                            uint32_t hp, m1, m2;
                            std::memcpy(&hp, buf.data() + i, 4);
                            std::memcpy(&m1, buf.data() + i + 4, 4);
                            std::memcpy(&m2, buf.data() + i + 8, 4);
                            if (m1 == maxHP && m2 == maxHP && hp <= maxHP) {
                                cands.push_back({regionBase + off + i, hp});
                            }
                        }
                    }
                }
            }
            const uintptr_t next = regionBase + mbi.RegionSize;
            if (next <= regionBase) break;
            addr = next;
        }
    } else {
        // Lượt 2+: lọc theo HP mới (sau khi user mất máu/hồi máu)
        AppendReport("=== FINDPLAYER rescan HP=" + std::to_string(rescanHP) + " ===");
        std::ifstream f(rescanFile);
        if (!f) {
            AppendReport("[ERROR] Khong doc duoc file: " + rescanFile);
            return;
        }
        std::string line;
        while (std::getline(f, line)) {
            if (line.size() < 4) continue;
            const uintptr_t addr = std::strtoull(line.c_str(), nullptr, 16);
            uint32_t v = 0;
            if (reader.Read(addr, &v, sizeof(v)) && v == static_cast<uint32_t>(rescanHP)) {
                cands.push_back({addr, v});
            }
        }
    }

    // Ghi danh sách địa chỉ
    {
        std::ofstream f(candPathW, std::ios::trunc);
        if (!f.is_open()) {
            AppendReport("[ERROR] Khong ghi duoc player_candidates.txt");
            return;
        }
        for (const auto& c : cands) f << Hex(c.first) << "\n";
    }

    AppendReport("  [OK] " + std::to_string(cands.size()) + " stat block (luu player_candidates.txt)");
    std::cout << "[OK] " << cands.size() << " stat block" << std::endl;

    const size_t show = (std::min<size_t>)(30, cands.size());
    for (size_t i = 0; i < show; ++i) {
        const std::string l = "    0x" + Hex(cands[i].first) + " (HP=" + std::to_string(cands[i].second) + ")";
        AppendReport(l);
        std::cout << l << std::endl;
    }
    AppendReport("  Huong dan: mat mau roi chay: findplayer " + std::to_string(maxHP)
        + " --rescan <HPmoi>");
    AppendReport("[FINDPLAYER DONE]");
}

// ---- Che do "bridge": Do chuoi con tro tinh tu .data toi PlayerState bang Bi-Directional BFS ----
void CmdBridge(ReadProcessMemoryReader& reader, uintptr_t target) {
    if (target == 0) {
        std::ifstream f(ExeDirNarrow() + "\\player_candidates.txt");
        if (f) {
            std::string line;
            if (std::getline(f, line)) {
                target = std::strtoull(line.c_str(), nullptr, 16);
            }
        }
    }

    AppendReport("=== BRIDGE SEARCH target=0x" + Hex(target) + " ===");
    std::cout << "[Bridge] Bat dau do chuoi con tro tinh toi target: 0x" << Hex(target) << std::endl;

    if (target == 0) {
        AppendReport("[ERROR] target = 0 va khong co player_candidates.txt");
        std::cerr << "[Bridge] Khong co target hop le." << std::endl;
        return;
    }

    uint32_t hp = 0, maxHP = 0;
    if (!reader.Read(target, &hp, 4) || !reader.Read(target + 4, &maxHP, 4)) {
        AppendReport("[ERROR] Khong doc duoc target (game da doi session?).");
        std::cerr << "[Bridge] Khong doc duoc target - game da doi session?" << std::endl;
        return;
    }
    AppendReport("  Target xac nhan: HP=" + std::to_string(hp) + " / " + std::to_string(maxHP));
    std::cout << "  Target xac nhan: HP=" << hp << " / " << maxHP << std::endl;

    // 1. Thong tin main module
    ModuleInfo mainMod;
    if (!AobScanner::FindModule(FindProcessId(L"PathOfExile.exe"), L"PathOfExile.exe", mainMod)) {
        AppendReport("[ERROR] Khong tim thay module PathOfExile.exe");
        return;
    }
    const uintptr_t mainBase = mainMod.base;
    const uintptr_t mainSize = mainMod.size;
    AppendReport("  PathOfExile.exe: base=0x" + Hex(mainBase) + " size=0x" + Hex(mainSize));

    // 2. Tap 8 static RVAs da biet trong .data
    const std::vector<uintptr_t> kStaticRVAs = {
        0x45CCA20, 0x45CCA28, 0x45CCA30, 0x45CFEF8,
        0x45D3280, 0x45D3288, 0x45D3290, 0x4715740
    };

    struct StaticRoot {
        uintptr_t staticAddr;
        uintptr_t rva;
        uintptr_t heapPtr;
    };
    std::vector<StaticRoot> roots;
    for (uintptr_t rva : kStaticRVAs) {
        const uintptr_t sAddr = mainBase + rva;
        uintptr_t hPtr = 0;
        if (reader.Read(sAddr, &hPtr, 8) && hPtr > 0x10000) {
            roots.push_back({sAddr, rva, hPtr});
            AppendReport("  [Root] RVA 0x" + Hex(rva) + " -> *(0x" + Hex(sAddr) + ") = 0x" + Hex(hPtr));
        }
    }
    AppendReport("  So static root doc duoc: " + std::to_string(roots.size()));

    auto mergeIntervals = [](std::vector<std::pair<uintptr_t, uintptr_t>>& intervals) {
        if (intervals.empty()) return;
        std::sort(intervals.begin(), intervals.end());
        std::vector<std::pair<uintptr_t, uintptr_t>> merged;
        merged.push_back(intervals[0]);
        for (size_t i = 1; i < intervals.size(); ++i) {
            if (intervals[i].first <= merged.back().second) {
                merged.back().second = (std::max)(merged.back().second, intervals[i].second);
            } else {
                merged.push_back(intervals[i]);
            }
        }
        intervals = std::move(merged);
    };

    auto checkChain = [&](uintptr_t finalAddr, const std::string& chainDesc, const std::vector<uintptr_t>& offsets) {
        AppendReport("  [STATIC CHAIN FOUND] " + chainDesc);
        std::cout << "  [STATIC CHAIN FOUND] " << chainDesc << std::endl;

        std::string tomlPath = ExeDirNarrow() + "\\offsets.toml";
        std::ofstream toml(tomlPath);
        if (toml) {
            toml << "# Auto-generated by AutoPOE2 MemProbe Bridge\n";
            toml << "[player_stats]\n";
            toml << "player_addr = 0x" << std::hex << target << "\n";
            toml << "static_rva = 0x" << std::hex << (finalAddr - mainBase) << "\n";
            toml << "num_offsets = " << std::dec << offsets.size() << "\n";
            for (size_t i = 0; i < offsets.size(); ++i) {
                toml << "offset_" << (i + 1) << " = 0x" << std::hex << offsets[i] << "\n";
            }
            AppendReport("  Da ghi chuoi offset vao: " + tomlPath);
            std::cout << "  Da ghi chuoi offset vao: " << tomlPath << std::endl;
        }
    };

    // 3. Backward Pass 1: Tim moi pointer tro vao [target - 0x400, target + 0x8]
    std::vector<std::pair<uintptr_t, uintptr_t>> b1;
    ScanPointerPass(reader, {{target > 0x400 ? target - 0x400 : 0, target + 8}}, b1, "B1");

    for (const auto& p : b1) {
        const uintptr_t L = p.first;
        const uintptr_t V = p.second;
        const uintptr_t off = target - V;

        if (L >= mainBase && L < mainBase + mainSize) {
            checkChain(L, "[[0x" + Hex(L) + " (RVA 0x" + Hex(L - mainBase) + ")] + 0x" + Hex(off) + "] -> HP", {off});
            return;
        }
        for (const auto& r : roots) {
            if (L >= r.heapPtr && L < r.heapPtr + 0x10000) {
                const uintptr_t off1 = L - r.heapPtr;
                checkChain(r.staticAddr, "[[[0x" + Hex(r.staticAddr) + "]+0x" + Hex(off1) + "]+0x" + Hex(off) + "] -> HP", {off1, off});
                return;
            }
        }
    }

    // 4. Backward Pass 2: Tim pointer tro vao cac node cua b1
    std::vector<std::pair<uintptr_t, uintptr_t>> windowsB2;
    for (const auto& p : b1) {
        const uintptr_t a = p.first;
        windowsB2.push_back({a > 0x400 ? a - 0x400 : 0, a + 8});
    }
    mergeIntervals(windowsB2);

    std::vector<std::pair<uintptr_t, uintptr_t>> b2;
    ScanPointerPass(reader, windowsB2, b2, "B2");

    for (const auto& p2 : b2) {
        const uintptr_t L2 = p2.first;
        const uintptr_t V2 = p2.second;

        for (const auto& p1 : b1) {
            const uintptr_t L1 = p1.first;
            const uintptr_t V1 = p1.second;
            if (V2 <= L1 && L1 - V2 <= 0x400) {
                const uintptr_t off2 = L1 - V2;
                const uintptr_t off1 = target - V1;

                if (L2 >= mainBase && L2 < mainBase + mainSize) {
                    checkChain(L2, "[[[0x" + Hex(L2) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {off2, off1});
                    return;
                }
                for (const auto& r : roots) {
                    if (L2 >= r.heapPtr && L2 < r.heapPtr + 0x10000) {
                        const uintptr_t offRoot = L2 - r.heapPtr;
                        checkChain(r.staticAddr, "[[[[0x" + Hex(r.staticAddr) + "]+0x" + Hex(offRoot) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {offRoot, off2, off1});
                        return;
                    }
                }
            }
        }
    }

    // 5. Backward Pass 3: Tim pointer tro vao cac node cua b2
    std::vector<std::pair<uintptr_t, uintptr_t>> windowsB3;
    for (const auto& p : b2) {
        const uintptr_t a = p.first;
        windowsB3.push_back({a > 0x400 ? a - 0x400 : 0, a + 8});
    }
    mergeIntervals(windowsB3);

    std::vector<std::pair<uintptr_t, uintptr_t>> b3;
    ScanPointerPass(reader, windowsB3, b3, "B3");

    for (const auto& p3 : b3) {
        const uintptr_t L3 = p3.first;
        const uintptr_t V3 = p3.second;

        for (const auto& p2 : b2) {
            const uintptr_t L2 = p2.first;
            const uintptr_t V2 = p2.second;
            if (V3 <= L2 && L2 - V3 <= 0x400) {
                for (const auto& p1 : b1) {
                    const uintptr_t L1 = p1.first;
                    const uintptr_t V1 = p1.second;
                    if (V2 <= L1 && L1 - V2 <= 0x400) {
                        const uintptr_t off3 = L2 - V3;
                        const uintptr_t off2 = L1 - V2;
                        const uintptr_t off1 = target - V1;

                        if (L3 >= mainBase && L3 < mainBase + mainSize) {
                            checkChain(L3, "[[[[0x" + Hex(L3) + "]+0x" + Hex(off3) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {off3, off2, off1});
                            return;
                        }
                        for (const auto& r : roots) {
                            if (L3 >= r.heapPtr && L3 < r.heapPtr + 0x10000) {
                                const uintptr_t offRoot = L3 - r.heapPtr;
                                checkChain(r.staticAddr, "[[[[[0x" + Hex(r.staticAddr) + "]+0x" + Hex(offRoot) + "]+0x" + Hex(off3) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {offRoot, off3, off2, off1});
                                return;
                            }
                        }
                    }
                }
            }
        }
    }

    // 6. Backward Pass 4: Tim pointer tro vao cac node cua b3
    std::vector<std::pair<uintptr_t, uintptr_t>> windowsB4;
    for (const auto& p : b3) {
        const uintptr_t a = p.first;
        windowsB4.push_back({a > 0x400 ? a - 0x400 : 0, a + 8});
    }
    mergeIntervals(windowsB4);

    std::vector<std::pair<uintptr_t, uintptr_t>> b4;
    ScanPointerPass(reader, windowsB4, b4, "B4");

    for (const auto& p4 : b4) {
        const uintptr_t L4 = p4.first;
        const uintptr_t V4 = p4.second;

        for (const auto& p3 : b3) {
            const uintptr_t L3 = p3.first;
            const uintptr_t V3 = p3.second;
            if (V4 <= L3 && L3 - V4 <= 0x400) {
                for (const auto& p2 : b2) {
                    const uintptr_t L2 = p2.first;
                    const uintptr_t V2 = p2.second;
                    if (V3 <= L2 && L2 - V3 <= 0x400) {
                        for (const auto& p1 : b1) {
                            const uintptr_t L1 = p1.first;
                            const uintptr_t V1 = p1.second;
                            if (V2 <= L1 && L1 - V2 <= 0x400) {
                                const uintptr_t off4 = L3 - V4;
                                const uintptr_t off3 = L2 - V3;
                                const uintptr_t off2 = L1 - V2;
                                const uintptr_t off1 = target - V1;

                                if (L4 >= mainBase && L4 < mainBase + mainSize) {
                                    checkChain(L4, "[[[[[0x" + Hex(L4) + "]+0x" + Hex(off4) + "]+0x" + Hex(off3) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {off4, off3, off2, off1});
                                    return;
                                }
                                for (const auto& r : roots) {
                                    if (L4 >= r.heapPtr && L4 < r.heapPtr + 0x10000) {
                                        const uintptr_t offRoot = L4 - r.heapPtr;
                                        checkChain(r.staticAddr, "[[[[[[0x" + Hex(r.staticAddr) + "]+0x" + Hex(offRoot) + "]+0x" + Hex(off4) + "]+0x" + Hex(off3) + "]+0x" + Hex(off2) + "]+0x" + Hex(off1) + "] -> HP", {offRoot, off4, off3, off2, off1});
                                        return;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    AppendReport("[BRIDGE DONE] Khong tim thay bridge trong 4 pass.");
    std::cout << "[Bridge] Khong tim thay bridge trong 4 pass." << std::endl;
}

// ---- Che do "scan-float": float differential scan tim toa do XYZ ----
namespace {

struct FloatCand { uintptr_t addr; float value; };

std::wstring FloatFile() { return ExeDir() + L"\\float_candidates.txt"; }

bool LoadFloatCands(const std::wstring& path, std::vector<FloatCand>& out) {
    std::ifstream f(path);
    if (!f) return false;
    std::string a, b;
    while (f >> a >> b) {
        FloatCand c{};
        c.addr = std::strtoull(a.c_str(), nullptr, 16);
        const uint32_t bits = static_cast<uint32_t>(std::strtoul(b.c_str(), nullptr, 16));
        std::memcpy(&c.value, &bits, 4);
        out.push_back(c);
    }
    return true;
}

void SaveFloatCands(const std::vector<FloatCand>& cands) {
    std::ofstream f(FloatFile(), std::ios::trunc);
    for (const auto& c : cands) {
        uint32_t bits;
        std::memcpy(&bits, &c.value, 4);
        char line[64];
        std::snprintf(line, sizeof(line), "%llX %08X\n",
            static_cast<unsigned long long>(c.addr), bits);
        f << line;
    }
}

void CmdScanFloat(ReadProcessMemoryReader& reader, const std::string& sub,
                  const std::string& p1, const std::string& p2) {
    if (sub == "init2") {
        // Quét một CỬA SỔ cố định [start, start+size] — dùng khi allocation
        // quá lớn và target nằm sâu giữa allocation (như POE2 này)
        const uintptr_t start = std::strtoull(p1.c_str(), nullptr, 16);
        const size_t size = std::strtoull(p2.c_str(), nullptr, 16);
        AppendReport("=== SCAN-FLOAT INIT2 window=0x" + Hex(start) + " size=0x"
            + Hex(size) + " ===");

        std::vector<FloatCand> cands;
        std::vector<uint8_t> buf(1024 * 1024);
        const float lo = 0.5f, hi = 5000.0f;
        uintptr_t cur = start;
        while (cur < start + size) {
            MEMORY_BASIC_INFORMATION r{};
            if (!VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(cur),
                                &r, sizeof(r))) break;
            const uintptr_t rBase = reinterpret_cast<uintptr_t>(r.BaseAddress);
            const bool readable = (r.State == MEM_COMMIT)
                && !(r.Protect & (PAGE_NOACCESS | PAGE_GUARD)) && r.Protect != 0;
            const uintptr_t rEnd = (std::min)(rBase + r.RegionSize, start + size);
            if (readable && rEnd > rBase) {
                for (size_t off = 0; off + 4 <= rEnd - rBase; off += buf.size()) {
                    const size_t n = (std::min<size_t>)(buf.size(),
                        static_cast<size_t>(rEnd - rBase) - off);
                    if (reader.Read(rBase + off, buf.data(), n)) {
                        for (size_t i = 0; i + 4 <= n; i += 4) {
                            float f;
                            std::memcpy(&f, buf.data() + i, 4);
                            if (std::isfinite(f) && f >= lo && f <= hi) {
                                cands.push_back({rBase + off + i, f});
                            }
                        }
                    }
                }
            }
            const uintptr_t ncur = (rBase + r.RegionSize);
            if (ncur <= cur) break;
            cur = ncur;
        }
        SaveFloatCands(cands);
        AppendReport("  [OK] " + std::to_string(cands.size())
            + " float ung vien (luu float_candidates.txt)");
        std::cout << "[OK] " << cands.size()
            << " ung vien. Di chuyen nhan vat roi chay 'scan-float moved 1.0'." << std::endl;
        AppendReport("[SCAN-FLOAT DONE]");
        return;
    }

    if (sub == "init") {
        // Quét float trong ALLOCATION chứa địa chỉ tham chiếu (32MB thay vì 5GB!)
        const uintptr_t ref = std::strtoull(p1.c_str(), nullptr, 16);
        const float fmin = std::stof(p1.empty() ? "0.5" : "0.5");
        const float fmax = std::stof(p2.empty() ? "5000" : p2);
        (void)fmin;
        AppendReport("=== SCAN-FLOAT INIT ref=0x" + Hex(ref) + " ===");

        MEMORY_BASIC_INFORMATION mbi{};
        if (!VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(ref),
                            &mbi, sizeof(mbi))) {
            AppendReport("[ERROR] VirtualQueryEx that bai");
            return;
        }
        const uintptr_t allocBase = reinterpret_cast<uintptr_t>(mbi.AllocationBase);
        uintptr_t spanHi = allocBase;
        {
            uintptr_t cur = allocBase;
            MEMORY_BASIC_INFORMATION t{};
            while (VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(cur),
                                  &t, sizeof(t))) {
                if (t.AllocationBase != mbi.AllocationBase) break;
                spanHi = cur + t.RegionSize;
                cur = spanHi;
                if (spanHi - allocBase > (256u << 20)) break;
            }
        }
        AppendReport("  Allocation: base=0x" + Hex(allocBase) + " span=0x"
            + Hex(spanHi - allocBase));

        std::vector<FloatCand> cands;
        std::vector<uint8_t> buf(1024 * 1024);
        const float lo = 0.5f, hi = 5000.0f;
        uintptr_t cur = allocBase;
        while (cur < spanHi) {
            MEMORY_BASIC_INFORMATION r{};
            if (!VirtualQueryEx(reader.RawHandle(), reinterpret_cast<LPCVOID>(cur),
                                &r, sizeof(r))) break;
            const uintptr_t rBase = reinterpret_cast<uintptr_t>(r.BaseAddress);
            const bool readable = (r.State == MEM_COMMIT)
                && !(r.Protect & (PAGE_NOACCESS | PAGE_GUARD)) && r.Protect != 0;
            const uintptr_t rEnd = (std::min)(rBase + r.RegionSize, spanHi);
            if (readable && rEnd > rBase) {
                for (size_t off = 0; off + 4 <= rEnd - rBase; off += buf.size()) {
                    const size_t n = (std::min<size_t>)(buf.size(),
                        static_cast<size_t>(rEnd - rBase) - off);
                    if (reader.Read(rBase + off, buf.data(), n)) {
                        for (size_t i = 0; i + 4 <= n; i += 4) {
                            float f;
                            std::memcpy(&f, buf.data() + i, 4);
                            if (std::isfinite(f) && f >= lo && f <= hi) {
                                cands.push_back({rBase + off + i, f});
                            }
                        }
                    }
                }
            }
            const uintptr_t ncur = rEnd;
            if (ncur <= cur) break;
            cur = ncur;
        }
        SaveFloatCands(cands);
        AppendReport("  [OK] " + std::to_string(cands.size())
            + " float ung vien (luu float_candidates.txt)");
        std::cout << "[OK] " << cands.size()
            << " ung vien. Hay DI CHUYEN nhan vat roi chay 'scan-float moved 1.0'." << std::endl;
        AppendReport("[SCAN-FLOAT DONE]");
        return;
    }

    // moved / still: differential theo file
    const float delta = std::stof(p1);
    std::vector<FloatCand> cands;
    if (!LoadFloatCands(FloatFile(), cands)) {
        AppendReport("[ERROR] Khong doc duoc float_candidates.txt");
        return;
    }

    std::vector<FloatCand> kept;
    if (sub == "moved") {
        AppendReport("=== SCAN-FLOAT MOVED delta=" + std::to_string(delta) + " ===");
        for (const auto& c : cands) {
            float now = 0;
            if (reader.Read(c.addr, &now, 4) && std::fabs(now - c.value) >= delta) {
                kept.push_back({c.addr, now});
            }
        }
    } else { // still
        AppendReport("=== SCAN-FLOAT STILL delta=" + std::to_string(delta) + " ===");
        for (const auto& c : cands) {
            float now = 0;
            if (reader.Read(c.addr, &now, 4) && std::fabs(now - c.value) < delta) {
                kept.push_back({c.addr, now});
            }
        }
    }
    SaveFloatCands(kept);
    AppendReport("  [OK] con " + std::to_string(kept.size()) + " ung vien");
    std::cout << "[OK] con " << kept.size() << " ung vien" << std::endl;
    const size_t show = (std::min<size_t>)(20, kept.size());
    for (size_t i = 0; i < show; ++i) {
        char l[96];
        std::snprintf(l, sizeof(l), "    0x%llX = %.3f",
            static_cast<unsigned long long>(kept[i].addr), kept[i].value);
        AppendReport(l);
    }
    AppendReport("[SCAN-FLOAT DONE]");
}

} // namespace

} // namespace (dong namespace ScanHp/PtrScan)

void CmdReadAll(ReadProcessMemoryReader& reader, const std::string& candFile) {
    AppendReport("=== READALL (" + candFile + ") ===");
    std::ifstream f(candFile);
    if (!f) {
        AppendReport("[ERROR] Khong doc duoc file: " + candFile);
        return;
    }
    std::string line;
    while (std::getline(f, line)) {
        if (line.size() < 4) continue;
        const uintptr_t addr = std::strtoull(line.c_str(), nullptr, 16);
        uint32_t v = 0;
        if (reader.Read(addr, &v, sizeof(v))) {
            const std::string l = "  0x" + Hex(addr) + " = " + std::to_string(v);
            std::cout << l << std::endl;
            AppendReport(l);
        } else {
            AppendReport("  0x" + Hex(addr) + " = <khong doc duoc>");
        }
    }
    AppendReport("[READALL DONE]");
}

void LogBoth(const std::string& line) {
    AppendReport(line);
    std::cout << line << std::endl;
}

void RankNearLife(std::vector<vitals_fingerprint::PairHit>& hits, uintptr_t life) {
    std::sort(hits.begin(), hits.end(), [life](const vitals_fingerprint::PairHit& a,
                                               const vitals_fingerprint::PairHit& b) {
        const auto da = (a.addr > life) ? (a.addr - life) : (life - a.addr);
        const auto db = (b.addr > life) ? (b.addr - life) : (life - b.addr);
        return da < db;
    });
}

void DumpU32Around(ReadProcessMemoryReader& reader, uintptr_t addr, int before, int after) {
    if (addr < static_cast<uintptr_t>(before)) {
        return;
    }
    const uintptr_t win = addr - static_cast<uintptr_t>(before);
    const size_t n = static_cast<size_t>(before + after);
    std::vector<uint8_t> buf(n, 0);
    if (!reader.Read(win, buf.data(), n)) {
        LogBoth("    <khong doc duoc xung quanh 0x" + Hex(addr) + ">");
        return;
    }
    std::string line = "    u32:";
    for (int off = -before; off + 4 <= after; off += 4) {
        uint32_t v = 0;
        std::memcpy(&v, buf.data() + static_cast<size_t>(off + before), 4);
        if (v == 0) {
            continue;
        }
        line += " [" + std::string(off >= 0 ? "+" : "") + std::to_string(off) + "]=" + std::to_string(v);
    }
    LogBoth(line);
}

void ScanLocalPairs(ReadProcessMemoryReader& reader, uintptr_t life,
                    uint32_t expectCur, uint32_t expectMax, const char* label) {
    constexpr uintptr_t kRadius = 0x200000;
    const uintptr_t start = (life > kRadius) ? (life - kRadius) : 0x10000ULL;
    constexpr size_t kChunk = 0x10000;
    constexpr size_t kSpan = 0x400000;
    std::vector<vitals_fingerprint::PairHit> hits;
    std::vector<uint8_t> buf(kChunk);
    const bool anyCur = (expectCur == 0xFFFFFFFFu);

    for (size_t off = 0; off + 8 <= kSpan; off += (kChunk - 8)) {
        const size_t n = (std::min)(kChunk, kSpan - off);
        if (!reader.Read(start + off, buf.data(), n)) {
            continue;
        }
        for (size_t i = 0; i + 8 <= n; i += 4) {
            uint32_t cur = 0;
            uint32_t mx = 0;
            std::memcpy(&cur, buf.data() + i, 4);
            std::memcpy(&mx, buf.data() + i + 4, 4);
            if (mx != expectMax) {
                continue;
            }
            if (!anyCur && cur != expectCur) {
                continue;
            }
            if (anyCur && cur > mx * 2) {
                continue;
            }
            const uintptr_t addr = start + off + i;
            bool dup = false;
            for (const auto& prev : hits) {
                if (prev.addr == addr) {
                    dup = true;
                    break;
                }
            }
            if (!dup) {
                hits.push_back(vitals_fingerprint::PairHit{addr, cur, mx});
            }
        }
    }
    RankNearLife(hits, life);
    LogBoth(std::string("[POOLS] Local±2MB ") + label + " hits=" + std::to_string(hits.size()));
    const size_t show = (std::min<size_t>)(8, hits.size());
    for (size_t i = 0; i < show; ++i) {
        const auto d = (hits[i].addr > life) ? (hits[i].addr - life) : (life - hits[i].addr);
        const std::string sign = (hits[i].addr >= life) ? "+" : "-";
        LogBoth("  " + std::string(label) + " 0x" + Hex(hits[i].addr)
                + " " + std::to_string(hits[i].cur) + "/" + std::to_string(hits[i].max)
                + " dist=" + sign + std::to_string(d));
        DumpU32Around(reader, hits[i].addr, 64, 96);
    }
}

void DumpFollowedPointers(ReadProcessMemoryReader& reader, uintptr_t life,
                          uint32_t manaMax, uint32_t spiritCur, uint32_t spiritMax) {
    constexpr uintptr_t kBefore = 128;
    constexpr uintptr_t kAfter = 384;
    if (life < kBefore) {
        LogBoth("[POOLS] Life addr qua thap");
        return;
    }
    const uintptr_t win = life - kBefore;
    const size_t n = static_cast<size_t>(kBefore + kAfter);
    std::vector<uint8_t> buf(n, 0);
    if (!reader.Read(win, buf.data(), n)) {
        LogBoth("[POOLS] Khong doc duoc cua so Life-128/+384");
        return;
    }

    LogBoth("[POOLS] Life window u32 != 0:");
    for (int off = -static_cast<int>(kBefore); off + 4 <= static_cast<int>(kAfter); off += 4) {
        uint32_t v = 0;
        std::memcpy(&v, buf.data() + static_cast<size_t>(off + static_cast<int>(kBefore)), 4);
        if (v == 0) {
            continue;
        }
        LogBoth("  life" + std::string(off >= 0 ? "+" : "") + std::to_string(off)
                + " = " + std::to_string(v) + " (0x" + Hex(v) + ")");
    }

    LogBoth("[POOLS] Follow heap pointers trong cua so:");
    for (int off = -static_cast<int>(kBefore); off + 8 <= static_cast<int>(kAfter); off += 8) {
        uintptr_t ptr = 0;
        std::memcpy(&ptr, buf.data() + static_cast<size_t>(off + static_cast<int>(kBefore)), 8);
        if (!PointerChainResolver::IsValidAddress(ptr)) {
            continue;
        }
        uint32_t block[24] = {};
        if (!reader.Read(ptr, block, sizeof(block))) {
            continue;
        }
        std::string found;
        for (int i = 0; i + 1 < 24; ++i) {
            if (manaMax && block[i] == manaMax && block[i + 1] == manaMax) {
                found += " MANA@" + std::to_string(i * 4);
            }
            if (spiritMax && block[i] == spiritCur && block[i + 1] == spiritMax) {
                found += " SPIRIT@" + std::to_string(i * 4);
            }
        }
        LogBoth("  life" + std::string(off >= 0 ? "+" : "") + std::to_string(off)
                + " -> 0x" + Hex(ptr)
                + " [0]=" + std::to_string(block[0])
                + " [1]=" + std::to_string(block[1])
                + " [2]=" + std::to_string(block[2])
                + found);
    }
}

} // namespace

void CmdProbePools(ReadProcessMemoryReader& reader, uintptr_t life,
                   uint32_t manaMax, uint32_t spiritCur, uint32_t spiritMax,
                   uint32_t rageMax, bool skipFullRam) {
    LogBoth("=== PROBE-POOLS life=0x" + Hex(life)
            + " mana=" + std::to_string(manaMax)
            + " spirit=" + std::to_string(spiritCur) + "/" + std::to_string(spiritMax)
            + " rageMax=" + std::to_string(rageMax)
            + (skipFullRam ? " fast=1" : " fast=0")
            + " ===");

    uint32_t hp = 0, maxHP = 0, es = 0, esMax = 0, ward = 0, wardMax = 0;
    reader.ReadValue<uint32_t>(life, hp);
    reader.ReadValue<uint32_t>(life + 4, maxHP);
    reader.ReadValue<uint32_t>(life + 12, es);
    reader.ReadValue<uint32_t>(life + 16, esMax);
    reader.ReadValue<uint32_t>(life + 32, ward);
    reader.ReadValue<uint32_t>(life + 36, wardMax);
    LogBoth("[POOLS] Life HP=" + std::to_string(hp) + "/" + std::to_string(maxHP)
            + " ES=" + std::to_string(es) + "/" + std::to_string(esMax)
            + " Ward=" + std::to_string(ward) + "/" + std::to_string(wardMax));
    if (maxHP != 1 || esMax == 0) {
        LogBoth("[POOLS] Life khong con khop CI — dung scan-hp/label-registry lai");
    }

    DumpFollowedPointers(reader, life, manaMax, spiritCur, spiritMax);

    if (spiritMax != 0) {
        ScanLocalPairs(reader, life, spiritCur, spiritMax, "spirit");
    }
    if (manaMax != 0) {
        ScanLocalPairs(reader, life, manaMax, manaMax, "mana");
    }
    if (rageMax != 0) {
        ScanLocalPairs(reader, life, 0, rageMax, "rage");
    }

    auto reportFull = [&](const char* label, uint32_t cur, uint32_t mx) {
        LogBoth(std::string("[POOLS] Full-RAM ") + label + " ...");
        vitals_fingerprint::ScanStats st{};
        auto hits = vitals_fingerprint::ScanCurMaxPair(reader, cur, mx, 64, &st);
        RankNearLife(hits, life);
        LogBoth(std::string("[POOLS] Full-RAM ") + label + " hits=" + std::to_string(hits.size())
                + " regions=" + std::to_string(st.regionsVisited)
                + " chunk_ok=" + std::to_string(st.chunkReadsOk)
                + " chunk_fail=" + std::to_string(st.chunkReadsFail));
        const size_t show = (std::min<size_t>)(12, hits.size());
        for (size_t i = 0; i < show; ++i) {
            const auto d = (hits[i].addr > life) ? (hits[i].addr - life) : (life - hits[i].addr);
            const std::string sign = (hits[i].addr >= life) ? "+" : "-";
            LogBoth("  " + std::string(label) + " 0x" + Hex(hits[i].addr)
                    + " " + std::to_string(hits[i].cur) + "/" + std::to_string(hits[i].max)
                    + " dist=" + sign + std::to_string(d));
            DumpU32Around(reader, hits[i].addr, 64, 96);
        }
    };

    if (!skipFullRam) {
        if (spiritMax != 0) {
            reportFull("spirit", spiritCur, spiritMax);
        }
        if (manaMax != 0) {
            reportFull("mana", manaMax, manaMax);
        }
        if (rageMax != 0) {
            reportFull("rage", 0, rageMax);
        }
    } else {
        LogBoth("[POOLS] Bo qua Full-RAM (--fast)");
    }
    LogBoth("[POOLS DONE]");
}

void CmdLabelRegistry(ReadProcessMemoryReader& reader, uint32_t pid,
                      const vitals_fingerprint::Expected& expected) {
    AppendReport("=== LABEL-REGISTRY (PID " + std::to_string(pid) + ") ===");
    if (expected.Enabled()) {
        AppendReport("[LABEL] fingerprint HP=" + std::to_string(expected.maxHP) +
                     " ES=" + std::to_string(expected.maxES) +
                     " Ward=" + std::to_string(expected.maxWard) +
                     " Mana=" + std::to_string(expected.maxMana) +
                     " Spirit=" + std::to_string(expected.curSpirit) + "/" +
                     std::to_string(expected.maxSpirit));
    }

    ModuleInfo mainMod;
    if (!AobScanner::FindModule(pid, L"PathOfExile.exe", mainMod) &&
        !AobScanner::FindModule(pid, L"PathOfExileSteam.exe", mainMod)) {
        AppendReport("[LABEL] Khong tim thay module PathOfExile.exe");
        return;
    }

    std::string exePath;
    if (!pe_fingerprint::LocatePoe2Executable(exePath)) {
        AppendReport("[LABEL] Khong dinh vi PathOfExile.exe tren dia");
        return;
    }
    pe_fingerprint::ClientBuildId live;
    std::string fpError;
    if (!pe_fingerprint::LoadPeHeaders(exePath, live, fpError)) {
        AppendReport("[LABEL] Fingerprint that bai: " + fpError);
        return;
    }

    const std::string baseDir = ExeDirNarrow();
    OffsetRegistry registry;
    std::string tomlPath = baseDir + "\\offsets_" + live.ShortId() + ".toml";
    if (!registry.Load(tomlPath)) {
        tomlPath = baseDir + "\\offsets.toml";
        if (!registry.Load(tomlPath)) {
            AppendReport("[LABEL] Khong nap duoc offsets_" + live.ShortId() + ".toml / offsets.toml");
            return;
        }
    }
    const auto match = registry.CheckBuild(live);
    AppendReport("[LABEL] build=" + live.ShortId() + " toml=" + tomlPath +
                 " match=" + OffsetRegistry::ToString(match));

    registry_live_label::Report report;
    registry_live_label::Probe(reader, registry, mainMod, false, report,
                              expected.Enabled() ? &expected : nullptr);
    report.buildId = live.ShortId();
    report.buildMatch = OffsetRegistry::ToString(match);

    const std::string jsonPath = baseDir + "\\live_label_" + live.ShortId() + ".json";
    if (!registry_live_label::WriteJson(report, jsonPath)) {
        AppendReport("[LABEL] Khong ghi duoc " + jsonPath);
        return;
    }
    AppendReport("[LABEL] hits=" + std::to_string(report.hits.size()) +
                 " labels=" + std::to_string(report.labels.size()) +
                 " json=" + jsonPath);
    std::cout << "[LABEL] hits=" << report.hits.size()
              << " labels=" << report.labels.size()
              << " -> " << jsonPath << std::endl;
    for (const auto& label : report.labels) {
        const std::string line = "  target=" + label.target +
            " kind=" + label.kind +
            " rva=" + registry_live_label::FormatHex(label.rva) +
            " evidence=" + label.evidence;
        std::cout << line << std::endl;
        AppendReport(line);
    }
    if (report.labels.empty()) {
        AppendReport("[LABEL] Khong gan duoc target — fingerprint CI+ES+Ward chua khop heap/static");
        std::cout << "[LABEL] Khong gan duoc target (fingerprint chua khop)." << std::endl;
        for (const auto& hit : report.hits) {
            if (hit.kind != "heap_fingerprint") {
                continue;
            }
            const std::string diag = "  heap_fp es_dwords=" + std::to_string(hit.esDwordHits) +
                " chunk_fail=" + std::to_string(hit.chunkReadsFail) +
                " life=" + hit.lifeDescribe;
            AppendReport(diag);
            std::cout << diag << std::endl;
        }
    }
}

int main(int argc, char** argv) {
    SetConsoleOutputCP(CP_UTF8);

    // Chưa có quyền Admin -> tự xin thăng cấp (UAC sẽ hiện trên màn hình)
    if (!IsElevated()) {
        std::cout << "[MemProbe] Chua co quyen Admin - xin thang cap qua UAC..." << std::endl;
        RelaunchAsAdmin(argc, argv);
        return 0;
    }

    const std::string mode = argc > 1 ? argv[1] : "map";

    const uint32_t pid = FindProcessId(L"PathOfExile.exe");
    if (pid == 0) {
        AppendReport("[FATAL] Khong thay tien trinh PathOfExile.exe");
        std::cerr << "[MemProbe] Khong thay tien trinh PathOfExile.exe" << std::endl;
        return 1;
    }

    ReadProcessMemoryReader reader;
    if (!reader.Attach(pid)) {
        AppendReport("[FATAL] Attach that bai du da elevated (loi "
            + std::to_string(GetLastError()) + ")");
        std::cerr << "[MemProbe] Attach that bai du da elevated!" << std::endl;
        return 1;
    }
    AppendReport("[OK] Da gan vao PID " + std::to_string(pid) + " (elevated)");
    std::cout << "[MemProbe] Da gan vao PathOfExile.exe (PID " << pid << ")" << std::endl;

    if (mode == "map") {
        CmdMap(reader, pid);
    } else if (mode == "entscan" && argc >= 3) {
        CmdEntScan(reader, std::strtoull(argv[2], nullptr, 16));
    } else if (mode == "readall") {
        const std::string f = argc >= 3 ? argv[2]
            : ExeDirNarrow() + "\\hp_candidates.txt";
        CmdReadAll(reader, f);
    } else if (mode == "ptrscan" && argc >= 3) {
        CmdPtrScan(reader, std::strtoull(argv[2], nullptr, 16));
    } else if (mode == "ptrdeep" && argc >= 3) {
        const size_t maxOff = argc >= 4 ? std::strtoull(argv[3], nullptr, 16) : 0x2000;
        const int maxDepth = argc >= 5 ? std::atoi(argv[4]) : 6;
        CmdPtrDeep(reader, std::strtoull(argv[2], nullptr, 16), maxOff, maxDepth);
    } else if (mode == "findplayer" && argc >= 3) {
        const uint32_t maxHP = static_cast<uint32_t>(std::strtoul(argv[2], nullptr, 10));
        std::string rescanFile;
        int rescanHP = -1;
        if (argc >= 5 && std::string(argv[3]) == "--rescan") {
            rescanHP = std::atoi(argv[4]);
            rescanFile = ExeDirNarrow() + "\\player_candidates.txt";
        }
        CmdFindPlayer(reader, maxHP, rescanFile, rescanHP);
    } else if (mode == "scan-float" && argc >= 4) {
        // scan-float <init <refHex>> | <moved|still <delta>>
        CmdScanFloat(reader, argv[2], argc >= 4 ? argv[3] : "", argc >= 5 ? argv[4] : "");
    } else if (mode == "read" && argc >= 4) {
        CmdRead(reader, std::strtoull(argv[2], nullptr, 16),
                static_cast<size_t>(std::atoi(argv[3])));
    } else if (mode == "aob") {
        CmdAob(reader, pid);
    } else if (mode == "label-registry") {
        vitals_fingerprint::Expected expected;
        expected.maxHP = 1;
        for (int i = 2; i + 1 < argc; ++i) {
            const std::string flag = argv[i];
            const uint32_t val = static_cast<uint32_t>(std::strtoul(argv[i + 1], nullptr, 10));
            if (flag == "--hp") { expected.maxHP = val; ++i; }
            else if (flag == "--es") { expected.maxES = val; ++i; }
            else if (flag == "--ward") { expected.maxWard = val; ++i; }
            else if (flag == "--mana") { expected.maxMana = val; ++i; }
            else if (flag == "--spirit") { expected.maxSpirit = val; ++i; }
            else if (flag == "--spirit-cur") { expected.curSpirit = val; ++i; }
        }
        CmdLabelRegistry(reader, pid, expected);
    } else if (mode == "probe-pools" && argc >= 3) {
        const uintptr_t life = static_cast<uintptr_t>(std::strtoull(argv[2], nullptr, 16));
        uint32_t manaMax = 0;
        uint32_t spiritCur = 0;
        uint32_t spiritMax = 0;
        uint32_t rageMax = 0;
        bool skipFullRam = false;
        for (int i = 3; i < argc; ++i) {
            const std::string flag = argv[i];
            if (flag == "--fast") {
                skipFullRam = true;
                continue;
            }
            if (i + 1 >= argc) {
                break;
            }
            const uint32_t val = static_cast<uint32_t>(std::strtoul(argv[i + 1], nullptr, 10));
            if (flag == "--mana") { manaMax = val; ++i; }
            else if (flag == "--spirit") { spiritMax = val; ++i; }
            else if (flag == "--spirit-cur") { spiritCur = val; ++i; }
            else if (flag == "--rage") { rageMax = val; ++i; }
        }
        CmdProbePools(reader, life, manaMax, spiritCur, spiritMax, rageMax, skipFullRam);
    } else if (mode == "scan-hp" && argc >= 3) {
        const uint32_t value = static_cast<uint32_t>(std::strtoul(argv[2], nullptr, 10));
        std::string rescan, rescanChanged;
        if (argc >= 5 && std::string(argv[3]) == "--rescan") rescan = argv[4];
        if (argc >= 5 && std::string(argv[3]) == "--rescan-changed") rescanChanged = argv[4];
        CmdScanHp(reader, value, rescan, rescanChanged);
    } else if (mode == "probe-terrain" && argc >= 3) {
        const uintptr_t baseAddr = std::strtoull(argv[2], nullptr, 16);
        AppendReport("=== PROBE-TERRAIN base=0x" + Hex(baseAddr) + " ===");
        std::cout << "[TerrainProbe] Dang quet cau truc TerrainData quanh 0x" << Hex(baseAddr) << "..." << std::endl;
        memory::NativeTerrainData terrain;
        uintptr_t scanStart = (baseAddr > 0x10000) ? (baseAddr - 0x10000) : 0x10000;
        if (memory::TerrainReader::ScanTerrainCandidates(reader, scanStart, 0x20000, terrain)) {
            std::string res = std::string("[MATCH] Tim thay TerrainData!\n")
                            + "  Kich thuoc: " + std::to_string(terrain.cols) + " x " + std::to_string(terrain.rows) + "\n"
                            + "  Ti le di duoc: " + std::to_string(static_cast<int>(terrain.WalkablePercentage())) + "%\n"
                            + "  Tong so o: " + std::to_string(terrain.walkability.size());
            std::cout << res << std::endl;
            AppendReport(res);
        } else {
            std::string miss = "  [MISS] Khong tim thay cau truc TerrainData trong pham vi quet.";
            std::cout << miss << std::endl;
            AppendReport(miss);
        }
    } else if (mode == "bridge") {
        uintptr_t target = argc >= 3 ? std::strtoull(argv[2], nullptr, 16) : 0;
        CmdBridge(reader, target);
    } else {
        std::cout << "Cach dung: memprobe.exe <map|read|aob|label-registry|probe-pools|scan-hp|findplayer|bridge|ptrscan|ptrdeep|probe-terrain>\n"
                  << "  label-registry --hp 1 --es 2416 --ward 826 --mana 828 --spirit 138 --spirit-cur 8\n"
                  << "  probe-pools <lifeHex> --mana 828 --spirit 138 --spirit-cur 8 [--rage 30]" << std::endl;
        AppendReport("[USAGE] memprobe.exe probe-pools <lifeHex> --mana .. --spirit .. --spirit-cur ..");
    }
    return 0;
}