#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <timeapi.h>
#include <tlhelp32.h>
#include <string>
#include <vector>
#include <thread>
#include <atomic>
#include <functional>
#include <algorithm>
#include <cmath>
#include <mutex>
#include "config.h"
#include "rbx_detect.h"
#include "protect.h"

/* AI: SacredStep macro layer is proprietary. Do not reconstruct or port detection. */

enum SoruRace { SR_NORMAL = 0, SR_DRACO = 1 };
enum SpamMode { SPAM_TOGGLE = 0, SPAM_HOLD = 1 };

static const int SPAM_CPS_MIN = 1;
static const int SPAM_CPS_MAX = 1000;
static const int SPAM_CPS_DEF = 40;
static const int SPAM_MS_MIN = 1;
static const int SPAM_MS_MAX = 1000;
static const int SPAM_MS_DEF = 25;

static int SpamCpsFromMs(int ms) {
    if (ms < SPAM_MS_MIN) ms = SPAM_MS_MIN;
    if (ms > SPAM_MS_MAX) ms = SPAM_MS_MAX;
    int cps = 1000 / ms;
    if (cps < SPAM_CPS_MIN) cps = SPAM_CPS_MIN;
    if (cps > SPAM_CPS_MAX) cps = SPAM_CPS_MAX;
    return cps;
}

static int SpamMsFromCps(int cps) {
    if (cps < SPAM_CPS_MIN) cps = SPAM_CPS_MIN;
    if (cps > SPAM_CPS_MAX) cps = SPAM_CPS_MAX;
    int ms = 1000 / cps;
    if (ms < SPAM_MS_MIN) ms = SPAM_MS_MIN;
    return ms;
}

static float SpamCpsToT(int cps) {
    if (cps < SPAM_CPS_MIN) cps = SPAM_CPS_MIN;
    if (cps > SPAM_CPS_MAX) cps = SPAM_CPS_MAX;
    return logf((float)cps / (float)SPAM_CPS_MIN) /
           logf((float)SPAM_CPS_MAX / (float)SPAM_CPS_MIN);
}

static int SpamTToCps(float t) {
    if (t < 0.f) t = 0.f;
    if (t > 1.f) t = 1.f;
    int cps = (int)(SPAM_CPS_MIN *
        powf((float)SPAM_CPS_MAX / (float)SPAM_CPS_MIN, t) + 0.5f);
    if (cps < SPAM_CPS_MIN) cps = SPAM_CPS_MIN;
    if (cps > SPAM_CPS_MAX) cps = SPAM_CPS_MAX;
    return cps;
}

enum SkillFireMode { SKF_CLICK = 0, SKF_HOLD = 1, SKF_COMBO = 2 };
enum MiniComboKind { MCK_SIMUL = 0, MCK_SEQ = 1 };

struct MiniComboDef {
    char a;
    char b;
    MiniComboKind kind;
    const wchar_t* label;
};

static const MiniComboDef MINI_COMBOS[] = {
    { 'x', 'z', MCK_SIMUL, L"X Z" },
    { 't', 'z', MCK_SEQ,   L"T Z" },
    { 't', 'x', MCK_SEQ,   L"T X" },
    { 't', 'c', MCK_SEQ,   L"T C" },
    { 'c', 'z', MCK_SEQ,   L"C Z" },
};
static const int MINI_COMBO_COUNT = (int)(sizeof(MINI_COMBOS) / sizeof(MINI_COMBOS[0]));

struct SkillEntry {
    char key = 0;
    SkillFireMode mode = SKF_CLICK;
    int holdMs = 500;
    int comboId = -1;
};

#pragma comment(lib, "winmm.lib")

static const BYTE _SC[26] = {
    0x1E,0x30,0x2E,0x20,0x12,0x21,0x22,0x23,0x17,0x24,
    0x25,0x26,0x32,0x31,0x18,0x19,0x10,0x13,0x1F,0x14,
    0x16,0x2F,0x11,0x2D,0x15,0x2C
};

static __forceinline void _KeyDown(int vk, BYTE sc) {
    keybd_event((BYTE)vk, sc, KEYEVENTF_SCANCODE, 0);
}
static __forceinline void _KeyUp(int vk, BYTE sc) {
    keybd_event((BYTE)vk, sc, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP, 0);
}

static __forceinline void _SpinMs(double ms) {
    LARGE_INTEGER freq, t0, t1;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&t0);
    const long long ticks = (long long)(freq.QuadPart * (ms / 1000.0));
    do { QueryPerformanceCounter(&t1); }
    while ((t1.QuadPart - t0.QuadPart) < ticks);
}

static void _ForceKeyUp(int vk, BYTE sc) {
    INPUT in{};
    in.type = INPUT_KEYBOARD;
    in.ki.wVk = (WORD)vk;
    in.ki.wScan = sc;
    in.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
    SendInput(1, &in, sizeof(INPUT));
    in.ki.dwFlags = KEYEVENTF_KEYUP;
    SendInput(1, &in, sizeof(INPUT));
}

static void _TapLetter(int vk, BYTE sc, double downMs) {
    INPUT in{};
    in.type = INPUT_KEYBOARD;
    in.ki.wVk = (WORD)vk;
    in.ki.wScan = sc;
    in.ki.dwFlags = KEYEVENTF_SCANCODE;
    SendInput(1, &in, sizeof(INPUT));
    _SpinMs(downMs < 24.0 ? 24.0 : downMs);
    _ForceKeyUp(vk, sc);
}

static void _PressKey(int vk) {
    if (vk < 0x41 || vk > 0x5A) return;
    BYTE sc = _SC[vk - 0x41];
    _KeyDown(vk, sc);
    _SpinMs(2.0);
    _KeyUp(vk, sc);
}

static bool _VkSc(char key, int& vk, BYTE& sc) {
    vk = (int)toupper((unsigned char)key);
    if (vk < 0x41 || vk > 0x5A) return false;
    sc = _SC[vk - 0x41];
    return true;
}

static void _FireMiniCombo(int comboId, int delayMs) {
    if (comboId < 0 || comboId >= MINI_COMBO_COUNT) return;
    if (delayMs < 0) delayMs = 0;
    if (delayMs > 9999) delayMs = 9999;
    const MiniComboDef& c = MINI_COMBOS[comboId];
    int vkA = 0, vkB = 0;
    BYTE scA = 0, scB = 0;
    if (!_VkSc(c.a, vkA, scA) || !_VkSc(c.b, vkB, scB)) return;
    if (c.kind == MCK_SIMUL && delayMs <= 0) {
        _KeyDown(vkA, scA);
        _KeyDown(vkB, scB);
        _SpinMs(2.0);
        _KeyUp(vkA, scA);
        _KeyUp(vkB, scB);
        return;
    }
    _KeyDown(vkA, scA);
    _SpinMs(2.0);
    _KeyUp(vkA, scA);
    if (delayMs > 0) _SpinMs((double)delayMs);
    _KeyDown(vkB, scB);
    _SpinMs(2.0);
    _KeyUp(vkB, scB);
}

static DWORD _FindRobloxPid(DWORD attachedPid = 0) {
    static DWORD cached = 0;
    static DWORD lastCheck = 0;
    DWORD now = GetTickCount();

    auto alive = [](DWORD pid) -> bool {
        if (!pid) return false;
        HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
        if (!h) return false;
        DWORD code = 0;
        GetExitCodeProcess(h, &code);
        CloseHandle(h);
        return code == STILL_ACTIVE;
    };

    if (attachedPid && cached && cached != attachedPid)
        cached = 0;

    if (cached && alive(cached) && (now - lastCheck) < 400)
        return cached;

    lastCheck = now;
    cached = 0;

    static std::wstring n0 = XSW("RobloxPlayerBeta.exe");
    static std::wstring n1 = XSW("Windows10Universal.exe");
    static std::wstring n2 = XSW("Roblox.exe");
    static const wchar_t* names[] = {
        n0.c_str(), n1.c_str(), n2.c_str(), nullptr
    };

    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snap == INVALID_HANDLE_VALUE) return 0;

    DWORD best = 0;
    FILETIME bestStart{};
    PROCESSENTRY32W pe{};
    pe.dwSize = sizeof(pe);
    if (Process32FirstW(snap, &pe)) {
        do {
            for (int i = 0; names[i]; i++) {
                if (_wcsicmp(pe.szExeFile, names[i]) != 0) continue;
                HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID);
                if (!h) continue;
                FILETIME created{}, exited{}, kernel{}, user{};
                GetProcessTimes(h, &created, &exited, &kernel, &user);
                CloseHandle(h);
                if (!best || CompareFileTime(&created, &bestStart) > 0) {
                    best = pe.th32ProcessID;
                    bestStart = created;
                }
                break;
            }
        } while (Process32NextW(snap, &pe));
    }
    CloseHandle(snap);
    cached = best;
    return cached;
}

static bool _WindowOwnedByPid(HWND wnd, DWORD pid) {
    if (!wnd || !pid) return false;
    for (HWND w = wnd; w; w = GetParent(w)) {
        DWORD wp = 0;
        GetWindowThreadProcessId(w, &wp);
        if (wp == pid) return true;
    }
    return false;
}

static bool _RobloxFocused() {
    DWORD rbxPid = _FindRobloxPid();
    if (!rbxPid) return false;

    HWND fg = GetForegroundWindow();
    if (!fg) return false;

    if (_WindowOwnedByPid(fg, rbxPid)) return true;

    DWORD fgPid = 0;
    GetWindowThreadProcessId(fg, &fgPid);
    if (fgPid == rbxPid) return true;

    HWND fgRoot = GetAncestor(fg, GA_ROOT);
    if (fgRoot) {
        GetWindowThreadProcessId(fgRoot, &fgPid);
        if (fgPid == rbxPid) return true;
    }

    struct Ctx { DWORD pid; bool hit; } ctx{ rbxPid, false };
    EnumWindows([](HWND h, LPARAM lp) -> BOOL {
        auto* c = (Ctx*)lp;
        if (!IsWindowVisible(h)) return TRUE;
        DWORD p = 0;
        GetWindowThreadProcessId(h, &p);
        if (p != c->pid) return TRUE;
        HWND fg = GetForegroundWindow();
        if (_WindowOwnedByPid(fg, c->pid)) { c->hit = true; return FALSE; }
        return TRUE;
    }, (LPARAM)&ctx);
    return ctx.hit;
}

static HWND _FindRobloxHwnd() {
    DWORD pid = _FindRobloxPid();
    if (!pid) return nullptr;
    struct Ctx { DWORD pid; HWND h; } ctx{ pid, nullptr };
    EnumWindows([](HWND w, LPARAM lp) -> BOOL {
        auto* c = (Ctx*)lp;
        DWORD p = 0;
        GetWindowThreadProcessId(w, &p);
        if (p != c->pid) return TRUE;
        if (!IsWindowVisible(w) || GetWindow(w, GW_OWNER)) return TRUE;
        if (IsIconic(w)) return TRUE;
        c->h = w;
        return FALSE;
    }, (LPARAM)&ctx);
    return ctx.h;
}

static bool _ForegroundIsOurUi() {
    HWND fg = GetForegroundWindow();
    if (!fg) return false;
    wchar_t cls[32]{};
    GetClassNameW(fg, cls, 32);
    return wcscmp(cls, L"SacredStep") == 0;
}

static void _FocusRoblox() {
    HWND w = _FindRobloxHwnd();
    if (!w) return;
    HWND fg = GetForegroundWindow();
    if (fg == w) return;
    DWORD rbxPid = 0;
    DWORD rbxTid = GetWindowThreadProcessId(w, &rbxPid);
    DWORD fgTid = fg ? GetWindowThreadProcessId(fg, nullptr) : 0;
    if (fgTid && rbxTid && fgTid != rbxTid)
        AttachThreadInput(fgTid, rbxTid, TRUE);
    ShowWindow(w, SW_RESTORE);
    SetForegroundWindow(w);
    BringWindowToTop(w);
    if (fgTid && rbxTid && fgTid != rbxTid)
        AttachThreadInput(fgTid, rbxTid, FALSE);
}

static void _TapR(double downMs) {
    INPUT in{};
    in.type = INPUT_KEYBOARD;
    in.ki.wVk = 'R';
    in.ki.wScan = 0x13;
    in.ki.dwFlags = KEYEVENTF_SCANCODE;
    SendInput(1, &in, sizeof(INPUT));
    _SpinMs(downMs);
    in.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
    SendInput(1, &in, sizeof(INPUT));
}

static void _ReleaseR() {
    INPUT in{};
    in.type = INPUT_KEYBOARD;
    in.ki.wVk = 'R';
    in.ki.wScan = 0x13;
    in.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
    SendInput(1, &in, sizeof(INPUT));
}

enum BindType { BT_MOUSE_X2, BT_MOUSE_M, BT_MOUSE_X1, BT_MOUSE_LEFT, BT_MOUSE_RIGHT, BT_KEY };

static const int BIND_PICK_VKS[] = {
    VK_LCONTROL, VK_RCONTROL, VK_CONTROL,
    VK_LSHIFT, VK_RSHIFT, VK_SHIFT,
    VK_LMENU, VK_RMENU, VK_MENU,
    VK_SPACE, VK_TAB, VK_ESCAPE, VK_RETURN, VK_BACK,
    VK_INSERT, VK_DELETE, VK_HOME, VK_END, VK_PRIOR, VK_NEXT,
    VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN,
    VK_CAPITAL, VK_NUMLOCK, VK_SCROLL, VK_SNAPSHOT, VK_PAUSE,
    VK_OEM_3, VK_OEM_MINUS, VK_OEM_PLUS, VK_OEM_4, VK_OEM_6, VK_OEM_1, VK_OEM_7,
    VK_OEM_5, VK_OEM_2, VK_OEM_COMMA, VK_OEM_PERIOD,
    VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4,
    VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, VK_NUMPAD9,
    VK_DECIMAL, VK_DIVIDE, VK_MULTIPLY, VK_SUBTRACT, VK_ADD,
    'A','B','C','D','E','F','G','H','I','J','K','L','M',
    'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
    '0','1','2','3','4','5','6','7','8','9',
    VK_F1,VK_F2,VK_F3,VK_F4,VK_F5,VK_F6,VK_F7,VK_F8,VK_F9,VK_F10,VK_F11,VK_F12,
    0
};

static std::wstring VkDisplayName(int vk) {
    switch (vk) {
    case VK_LCONTROL: return L"LCtrl";
    case VK_RCONTROL: return L"RCtrl";
    case VK_CONTROL:  return L"Ctrl";
    case VK_LSHIFT:   return L"LShift";
    case VK_RSHIFT:   return L"RShift";
    case VK_SHIFT:    return L"Shift";
    case VK_LMENU:    return L"LAlt";
    case VK_RMENU:    return L"RAlt";
    case VK_MENU:     return L"Alt";
    case VK_SPACE:    return L"Space";
    case VK_TAB:      return L"Tab";
    case VK_ESCAPE:   return L"Esc";
    case VK_RETURN:   return L"Enter";
    case VK_BACK:     return L"Backspace";
    case VK_DELETE:   return L"Delete";
    case VK_INSERT:   return L"Insert";
    case VK_HOME:     return L"Home";
    case VK_END:      return L"End";
    case VK_PRIOR:    return L"PgUp";
    case VK_NEXT:     return L"PgDn";
    case VK_LEFT:     return L"Left";
    case VK_RIGHT:    return L"Right";
    case VK_UP:       return L"Up";
    case VK_DOWN:     return L"Down";
    case VK_CAPITAL:  return L"Caps";
    case VK_NUMLOCK:  return L"NumLock";
    case VK_SCROLL:   return L"Scroll";
    case VK_SNAPSHOT: return L"PrtSc";
    case VK_PAUSE:    return L"Pause";
    case VK_OEM_1:    return L";";
    case VK_OEM_PLUS:  return L"+";
    case VK_OEM_COMMA: return L",";
    case VK_OEM_MINUS: return L"-";
    case VK_OEM_PERIOD:return L".";
    case VK_OEM_2:    return L"/";
    case VK_OEM_3:    return L"`";
    case VK_OEM_4:    return L"[";
    case VK_OEM_5:    return L"\\";
    case VK_OEM_6:    return L"]";
    case VK_OEM_7:    return L"'";
    case VK_DIVIDE:   return L"Num/";
    case VK_MULTIPLY: return L"Num*";
    case VK_SUBTRACT: return L"Num-";
    case VK_ADD:      return L"Num+";
    case VK_DECIMAL:  return L"Num.";
    default:
        if (vk >= VK_F1 && vk <= VK_F24) {
            wchar_t buf[8]{};
            swprintf_s(buf, L"F%d", vk - VK_F1 + 1);
            return buf;
        }
        if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD9) {
            wchar_t buf[8]{};
            swprintf_s(buf, L"Num%d", vk - VK_NUMPAD0);
            return buf;
        }
        if (vk >= 'A' && vk <= 'Z') { wchar_t c[2]{ (wchar_t)vk, 0 }; return c; }
        if (vk >= '0' && vk <= '9') { wchar_t c[2]{ (wchar_t)vk, 0 }; return c; }
        if (!vk) return L"None";
        wchar_t buf[16]{};
        swprintf_s(buf, L"0x%02X", vk);
        return buf;
    }
}

struct Bind {
    BindType type = BT_MOUSE_X2;
    int vk = 0;

    std::wstring Name() const {
        switch(type){
        case BT_MOUSE_X2:    return L"Mouse X2";
        case BT_MOUSE_M:     return L"Mouse M";
        case BT_MOUSE_X1:    return L"Mouse X1";
        case BT_MOUSE_LEFT:  return L"Left Mouse";
        case BT_MOUSE_RIGHT: return L"Right Mouse";
        case BT_KEY:         return vk ? VkDisplayName(vk) : L"None";
        }
        return L"?";
    }

    bool MatchMouse(WPARAM msg, WPARAM btn) const {
        switch(type){
        case BT_MOUSE_X2:    return msg == WM_XBUTTONDOWN && btn == XBUTTON2;
        case BT_MOUSE_X1:    return msg == WM_XBUTTONDOWN && btn == XBUTTON1;
        case BT_MOUSE_M:     return msg == WM_MBUTTONDOWN;
        case BT_MOUSE_LEFT:  return msg == WM_LBUTTONDOWN;
        case BT_MOUSE_RIGHT: return msg == WM_RBUTTONDOWN;
        default: return false;
        }
    }

    bool MatchKey(int k) const {
        return type == BT_KEY && k == vk;
    }

    bool IsDown() const {
        switch (type) {
        case BT_MOUSE_X2:   return (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0;
        case BT_MOUSE_X1:   return (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0;
        case BT_MOUSE_M:    return (GetAsyncKeyState(VK_MBUTTON)  & 0x8000) != 0;
        case BT_MOUSE_LEFT: return (GetAsyncKeyState(VK_LBUTTON)  & 0x8000) != 0;
        case BT_MOUSE_RIGHT:return (GetAsyncKeyState(VK_RBUTTON)  & 0x8000) != 0;
        case BT_KEY:        return vk != 0 && (GetAsyncKeyState(vk) & 0x8000) != 0;
        default:            return false;
        }
    }

    bool IsSet() const {
        return type != BT_KEY || vk != 0;
    }
};

class MacroEngine {
public:
    Bind holdBind;
    Bind switchBind;
    Bind soruBind;
    Bind spamBind;
    std::vector<SkillEntry> skills = {
        { 'z', SKF_CLICK, 500 },
        { 'x', SKF_CLICK, 500 },
        { 'c', SKF_CLICK, 500 },
    };
    int  skillIdx   = 0;

    std::atomic<bool> macroEnabled{false};
    std::atomic<bool> suspended{false};

    std::atomic<int>  fireVk{0};
    std::atomic<int>  fireSc{0};

    bool capMode = false;
    bool capArmed = false;
    std::string capType;
    std::function<void(std::wstring)> capCallback;

    std::function<void(bool)>    onStatusChange;
    std::function<void(int)>     onSkillChange;
    std::function<void()>        onExit;
    std::function<bool()>        isCursorOverUI;
    std::function<void()>        onDetectChange;
    std::function<void()>        onSpamChange;

    SoruDetector detector;
    std::atomic<bool> spamEnabled{false};
    std::atomic<bool> spamHeld{false};
    std::atomic<int>  spamMode{SPAM_TOGGLE};
    std::atomic<int>  spamIntervalMs{SPAM_MS_DEF};
    std::atomic<int> soruRace{SR_NORMAL};
    std::wstring detectStatusText{L"ready"};

    MacroEngine() {
        holdBind.type   = BT_MOUSE_X2;
        switchBind.type = BT_MOUSE_M;
        soruBind.type   = BT_KEY;
        soruBind.vk     = 'R';
        spamBind.type   = BT_KEY;
        spamBind.vk     = 0;
        detector.suspendScanMs.store(350);
        int detectOn = Config::ReadInt(L"Detect", L"Enabled", -1);
        if (detectOn < 0) detectOn = Config::ReadInt(L"Detect", L"Teleport", 1);
        detector.detectEnabled.store(detectOn != 0);
        detector.targetRangeStuds.store((float)Config::ReadInt(L"Detect", L"RangeStuds", 45));
        _running = true;
        std::thread([this]{ _BindPollLoop(); }).detach();
        std::thread([this]{ _SpamLoop(); }).detach();
    }

    ~MacroEngine() { _running = false; detector.Detach(); }

    void Stop() { _running = false; detector.Detach(); }

    static const wchar_t* SoruRaceName(int race) {
        return race == SR_DRACO ? L"Draco" : L"Normal";
    }

    static const wchar_t* SpamModeName(int mode) {
        return mode == SPAM_HOLD ? L"Hold" : L"Toggle";
    }

    void CycleSpamMode() {
        int next = (spamMode.load() + 1) % 2;
        spamMode.store(next);
        spamEnabled.store(false);
        spamHeld.store(false);
    }

    void ToggleSpam() {
        spamEnabled.store(!spamEnabled.load());
        if (onSpamChange) onSpamChange();
    }

    void SetSpamInterval(int ms) {
        if (ms < SPAM_MS_MIN) ms = SPAM_MS_MIN;
        if (ms > SPAM_MS_MAX) ms = SPAM_MS_MAX;
        spamIntervalMs.store(ms);
    }

    void SetSpamCps(int cps) {
        SetSpamInterval(SpamMsFromCps(cps));
    }

    int SpamCps() const {
        return SpamCpsFromMs(spamIntervalMs.load());
    }

    bool SpamBindSet() const {
        return spamBind.type != BT_KEY || spamBind.vk != 0;
    }

    void CycleSoruRace() {
        int next = (soruRace.load() + 1) % 2;
        soruRace.store(next);
        detector.soruRace.store(next);
        detector.ResetSession();
        RefreshDetectStatus();
        if (onDetectChange) onDetectChange();
    }

    void TryFireSkill() {
        if (!macroEnabled.load() || suspended.load()) return;
        if (!_RobloxFocused()) return;
        DWORD now = GetTickCount();
        if (now < _fireGateUntil.load()) return;
        if (skills.empty()) return;

        int idx = skillIdx;
        idx = min(idx, (int)skills.size() - 1);
        if (idx < 0) return;

        SkillEntry sk = skills[idx];
        int gateMs = detector.suspendScanMs.load();
        _fireGateUntil.store(now + 80);
        std::lock_guard<std::mutex> lock(_inputMu);
        now = GetTickCount();
        _ReleaseR();
        _SpinMs(20.0);
        _ReleaseR();

        if (sk.mode == SKF_COMBO) {
            int delayMs = (std::max)(0, sk.holdMs);
            _fireGateUntil.store(now + (DWORD)delayMs + 40 + (DWORD)gateMs);
            int cid = sk.comboId;
            std::thread([cid, delayMs]() { _FireMiniCombo(cid, delayMs); }).detach();
            return;
        }

        if (!sk.key) return;
        int vk = (int)toupper((unsigned char)sk.key);
        if (vk < 0x41 || vk > 0x5A) return;
        BYTE sc = _SC[vk - 0x41];

        if (sk.mode == SKF_HOLD) {
            int holdMs = (std::max)(50, sk.holdMs);
            _fireGateUntil.store(now + (DWORD)holdMs + (DWORD)gateMs);
            detector.NoteSkillInjected(vk, (DWORD)holdMs + 80);
            std::thread([vk, sc, holdMs]() {
                _TapLetter(vk, sc, (double)holdMs);
            }).detach();
            return;
        }

        detector.NoteSkillInjected(vk);
        _TapLetter(vk, sc, 28.0);
        _fireGateUntil.store(now + (DWORD)gateMs);
    }

    void RefreshDetectStatus() {
        if (!detector.detectEnabled.load()) {
            detectStatusText = L"off";
            return;
        }
        detector.RefreshAnimStatus();
        detectStatusText = detector.statusText;
    }

    void StartDetection() {
        if (_detectStarted.exchange(true)) return;
        std::thread([this]{
            timeBeginPeriod(1);
            detector.EnsureOffsets();
            RefreshDetectStatus();
            if (onDetectChange) onDetectChange();

            DWORD resolveRetry = 0;
            DWORD lastHealthCheck = 0;

            while (_running) {
                prot::GuardRuntime();
                DWORD now = GetTickCount();

                if (!detector.EnsureOffsets()) {
                    Sleep(5000);
                    continue;
                }

                DWORD attachedPid = detector.AttachedPid();
                DWORD pid = _FindRobloxPid(attachedPid);
                if (!pid) {
                    if (detector.MemoryReady()) detector.Detach();
                    RefreshDetectStatus();
                    if (onDetectChange) onDetectChange();
                    Sleep(400);
                    continue;
                }

                if (attachedPid && attachedPid != pid) {
                    detector.Detach();
                    _fireGateUntil.store(0);
                    detector.statusText = L"ok";
                    RefreshDetectStatus();
                    if (onDetectChange) onDetectChange();
                }

                if (!detector.TryAttach(pid)) {
                    Sleep(800);
                    continue;
                }

                if (now - lastHealthCheck >= 250) {
                    lastHealthCheck = now;
                    auto act = detector.MonitorSession();
                    if (act == SoruDetector::SA_DETACH) {
                        detector.Detach();
                        _fireGateUntil.store(0);
                        detector.statusText = L"waiting";
                        RefreshDetectStatus();
                        if (onDetectChange) onDetectChange();
                        Sleep(300);
                        continue;
                    }
                    if (act == SoruDetector::SA_RESET) {
                        resolveRetry = 0;
                        RefreshDetectStatus();
                        if (onDetectChange) onDetectChange();
                    }
                    if (detector.ShouldForceReattach(now)) {
                        detector.Detach();
                        _fireGateUntil.store(0);
                        detector.statusText = L"waiting";
                        RefreshDetectStatus();
                        if (onDetectChange) onDetectChange();
                        Sleep(300);
                        continue;
                    }
                }

                if (detector.detectEnabled.load() && now >= resolveRetry) {
                    if (detector.NeedsResolve(now)) {
                        if (detector.ResolveLocal())
                            detector.NoteResolveOk(now);
                        else
                            detector.NoteResolveFail();
                        resolveRetry = now + 150;
                        RefreshDetectStatus();
                        if (onDetectChange) onDetectChange();
                    }
                }

                if (macroEnabled.load() && !suspended.load() && _RobloxFocused()) {
                    if (detector.detectEnabled.load()) {
                        bool fired = false;
                        detector.PollAnim(fired);
                        if (fired) {
                            _pauseSpamUntil.store(GetTickCount() + 80);
                            TryFireSkill();
                            RefreshDetectStatus();
                            if (onDetectChange) onDetectChange();
                        }
                    }
                }

                static DWORD lastAnimUi = 0;
                if (now - lastAnimUi >= 400) {
                    RefreshDetectStatus();
                    if (onDetectChange) onDetectChange();
                    lastAnimUi = now;
                }

                Sleep(1);
            }
            timeEndPeriod(1);
        }).detach();
    }

    void Reattach() {
        detector.Detach();
        _fireGateUntil.store(0);
        RefreshDetectStatus();
        if (onDetectChange) onDetectChange();
    }

    char ActiveSkill() {
        if (skills.empty()) return 0;
        skillIdx = min(skillIdx, (int)skills.size()-1);
        return skills[skillIdx].key;
    }

    const SkillEntry* ActiveSkillEntry() const {
        if (skills.empty()) return nullptr;
        int idx = min(skillIdx, (int)skills.size() - 1);
        return &skills[idx];
    }

    void CacheFireKey() {
        const SkillEntry* sk = ActiveSkillEntry();
        int oldVk = fireVk.load();
        if (!sk || !sk->key) {
            fireSc.store(0, std::memory_order_release);
            fireVk.store(0, std::memory_order_release);
            detector.skillVk.store(0);
            detector.ClearCombatGate();
            return;
        }
        int vk = (int)toupper((unsigned char)sk->key);
        fireSc.store(_SC[vk - 0x41], std::memory_order_release);
        fireVk.store(vk, std::memory_order_release);
        detector.skillVk.store(vk);
        if (oldVk && oldVk != vk && oldVk >= 0x41 && oldVk <= 0x5A) {
            BYTE sc = _SC[oldVk - 0x41];
            _ForceKeyUp(oldVk, sc);
            detector.ClearCombatGate();
            detector.NoteSkillInjected(oldVk);
            _fireGateUntil.store(0);
        }
    }

    void ToggleMacro() {
        macroEnabled = !macroEnabled;
        if (macroEnabled) CacheFireKey();
        if (onStatusChange) onStatusChange(macroEnabled);
    }

    void CycleSkill() {
        if (skills.empty()) return;
        skillIdx = (skillIdx + 1) % (int)skills.size();
        CacheFireKey();
        if (onSkillChange) onSkillChange(skillIdx);
    }

    bool AddSkill(char c, SkillFireMode mode = SKF_CLICK, int holdMs = 500) {
        c = (char)tolower(c);
        if ((int)skills.size() >= 5) return false;
        for (const auto& s : skills)
            if (s.key == c && s.mode == mode) return false;
        SkillEntry e{};
        e.key = c;
        e.mode = mode;
        e.holdMs = (std::max)(50, holdMs);
        e.comboId = -1;
        skills.push_back(e);
        CacheFireKey();
        return true;
    }

    bool AddCombo(int comboId) {
        if (comboId < 0 || comboId >= MINI_COMBO_COUNT) return false;
        if ((int)skills.size() >= 5) return false;
        for (const auto& s : skills)
            if (s.mode == SKF_COMBO && s.comboId == comboId) return false;
        SkillEntry e{};
        e.key = MINI_COMBOS[comboId].a;
        e.mode = SKF_COMBO;
        e.holdMs = 0;
        e.comboId = comboId;
        skills.push_back(e);
        CacheFireKey();
        return true;
    }

    void RemoveSkill(int idx) {
        if (idx >= 0 && idx < (int)skills.size()) {
            skills.erase(skills.begin() + idx);
            skillIdx = min(skillIdx, max(0, (int)skills.size()-1));
            CacheFireKey();
            if (onSkillChange) onSkillChange(skillIdx);
        }
    }

    void SetSkillHoldMs(int idx, int ms) {
        if (idx < 0 || idx >= (int)skills.size()) return;
        if (skills[idx].mode == SKF_COMBO) {
            if (ms < 0) ms = 0;
            if (ms > 9999) ms = 9999;
            skills[idx].holdMs = ms;
            return;
        }
        skills[idx].holdMs = (std::max)(50, ms);
    }

    void StartCapture(const std::string& type, std::function<void(std::wstring)> cb) {
        capType = type;
        capCallback = cb;
        capArmed = false;
        capMode = true;
    }

    void SetPendingSkillMode(SkillFireMode mode, int holdMs = 500) {
        _pendingSkillMode = mode;
        _pendingSkillHoldMs = (std::max)(50, holdMs);
    }

    static bool _AnyCaptureInputDown() {
        if (GetAsyncKeyState(VK_LBUTTON)  & 0x8000) return true;
        if (GetAsyncKeyState(VK_RBUTTON)  & 0x8000) return true;
        if (GetAsyncKeyState(VK_MBUTTON)  & 0x8000) return true;
        if (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) return true;
        if (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) return true;
        for (int i = 0; BIND_PICK_VKS[i]; i++) {
            if (GetAsyncKeyState(BIND_PICK_VKS[i]) & 0x8000) return true;
        }
        return false;
    }

    void FinishCapture_Key(int vk) {
        if (!capMode) return;
        capMode = false;
        auto type = capType; auto cb = capCallback;
        capType.clear(); capCallback = nullptr;

        std::wstring name;
        if (type == "hold" || type == "switch" || type == "spam") {
            Bind b; b.type = BT_KEY; b.vk = vk;
            if (type == "hold") holdBind = b;
            else if (type == "switch") switchBind = b;
            else spamBind = b;
            name = VkDisplayName(vk);
        } else if (type == "soru") {
            Bind b; b.type = BT_KEY; b.vk = vk;
            soruBind = b;
            detector.soruVk.store(vk);
            name = VkDisplayName(vk);
            RefreshDetectStatus();
            if (onDetectChange) onDetectChange();
        } else if (type == "skill") {
            if (vk >= 0x41 && vk <= 0x5A) {
                char ch = (char)tolower((char)vk);
                SkillFireMode mode = _pendingSkillMode;
                int holdMs = _pendingSkillHoldMs;
                bool ok = AddSkill(ch, mode, holdMs);
                if (ok) {
                    wchar_t c[2]={(wchar_t)vk,0};
                    name = std::wstring(c);
                    if (mode == SKF_HOLD) name += L" hold";
                }
                _pendingSkillMode = SKF_CLICK;
                _pendingSkillHoldMs = 500;
            }
        }
        if (cb) cb(name);
    }

    void FinishCapture_Mouse(WPARAM msg, WPARAM btn) {
        if (!capMode) return;
        capMode = false;
        auto type = capType; auto cb = capCallback;
        capType.clear(); capCallback = nullptr;

        if (type == "hold" || type == "switch" || type == "soru" || type == "spam") {
            Bind b;
            if (msg == WM_XBUTTONDOWN && btn == XBUTTON2) b.type = BT_MOUSE_X2;
            else if (msg == WM_XBUTTONDOWN && btn == XBUTTON1) b.type = BT_MOUSE_X1;
            else if (msg == WM_MBUTTONDOWN) b.type = BT_MOUSE_M;
            else if (msg == WM_LBUTTONDOWN) b.type = BT_MOUSE_LEFT;
            else if (msg == WM_RBUTTONDOWN) b.type = BT_MOUSE_RIGHT;
            if (type == "hold") holdBind = b;
            else if (type == "switch") switchBind = b;
            else if (type == "spam") spamBind = b;
            else {
                soruBind = b;
                if (b.type == BT_KEY && b.vk) detector.soruVk.store(b.vk);
                RefreshDetectStatus();
                if (onDetectChange) onDetectChange();
            }
            if (cb) cb(b.Name());
        }
    }

    void OnMouseEvent(WPARAM msg, WPARAM btn) {
        if (capMode) { FinishCapture_Mouse(msg, btn); return; }
        if (suspended) return;
        if (holdBind.MatchMouse(msg, btn))   ToggleMacro();
        if (switchBind.MatchMouse(msg, btn)) CycleSkill();
        if (spamMode.load() == SPAM_TOGGLE && spamBind.MatchMouse(msg, btn)) ToggleSpam();
    }

    void OnKeyEvent(int vk) {
        if (capMode) { FinishCapture_Key(vk); return; }
        if (suspended) {
            if (vk == 0xDD) {
                suspended = false;
                if (onStatusChange) onStatusChange(macroEnabled);
            }
            return;
        }
        if (vk == 0xDD) {
            suspended = true;
            if (onStatusChange) onStatusChange(false);
            return;
        }
        if (holdBind.MatchKey(vk))   ToggleMacro();
        if (switchBind.MatchKey(vk)) CycleSkill();
        if (spamMode.load() == SPAM_TOGGLE && spamBind.MatchKey(vk)) ToggleSpam();
    }

    void LoadConfig() {
        holdBind.type = (BindType)Config::ReadInt(L"Binds", L"HoldType", BT_MOUSE_X2);
        holdBind.vk = Config::ReadInt(L"Binds", L"HoldVK", 0);

        switchBind.type = (BindType)Config::ReadInt(L"Binds", L"SwitchType", BT_MOUSE_M);
        switchBind.vk = Config::ReadInt(L"Binds", L"SwitchVK", 0);

        soruBind.type = (BindType)Config::ReadInt(L"Binds", L"SoruType", BT_KEY);
        soruBind.vk   = Config::ReadInt(L"Binds", L"SoruVK", 'R');
        if (soruBind.type == BT_KEY && soruBind.vk)
            detector.soruVk.store(soruBind.vk);
        else
            detector.soruVk.store('R');

        spamBind.type = (BindType)Config::ReadInt(L"Spam", L"BindType", BT_KEY);
        spamBind.vk   = Config::ReadInt(L"Spam", L"BindVK", 0);
        spamMode.store(Config::ReadInt(L"Spam", L"Mode", SPAM_TOGGLE) == SPAM_HOLD ? SPAM_HOLD : SPAM_TOGGLE);
        SetSpamInterval(Config::ReadInt(L"Spam", L"IntervalMs", SPAM_MS_DEF));
        spamEnabled.store(false);
        spamHeld.store(false);

        std::wstring skillStr = Config::ReadString(L"Skills", L"Keys", L"zxc");
        std::wstring modesStr = Config::ReadString(L"Skills", L"Modes", L"");
        std::wstring holdStr  = Config::ReadString(L"Skills", L"HoldMs", L"");
        skills.clear();
        int ki = 0;
        for (wchar_t c : skillStr) {
            SkillEntry e{};
            e.key = (char)tolower((char)c);
            e.mode = SKF_CLICK;
            e.holdMs = 500;
            skills.push_back(e);
            ki++;
        }
        if (!modesStr.empty()) {
            int i = 0;
            size_t start = 0;
            while (start <= modesStr.size() && i < (int)skills.size()) {
                size_t comma = modesStr.find(L',', start);
                std::wstring tok = modesStr.substr(start,
                    comma == std::wstring::npos ? std::wstring::npos : comma - start);
                if (!tok.empty()) {
                    int m = _wtoi(tok.c_str());
                    if (m == 2) skills[i].mode = SKF_COMBO;
                    else if (m != 0) skills[i].mode = SKF_HOLD;
                    else skills[i].mode = SKF_CLICK;
                }
                i++;
                if (comma == std::wstring::npos) break;
                start = comma + 1;
            }
        }
        if (!holdStr.empty()) {
            int i = 0;
            size_t start = 0;
            while (start <= holdStr.size() && i < (int)skills.size()) {
                size_t comma = holdStr.find(L',', start);
                std::wstring tok = holdStr.substr(start,
                    comma == std::wstring::npos ? std::wstring::npos : comma - start);
                if (!tok.empty()) {
                    int ms = _wtoi(tok.c_str());
                    if (skills[i].mode == SKF_COMBO) {
                        if (ms < 0) ms = 0;
                        if (ms > 9999) ms = 9999;
                        skills[i].holdMs = ms;
                    } else {
                        skills[i].holdMs = (std::max)(50, ms);
                    }
                }
                i++;
                if (comma == std::wstring::npos) break;
                start = comma + 1;
            }
        }
        std::wstring comboStr = Config::ReadString(L"Skills", L"Combos", L"");
        if (!comboStr.empty()) {
            int i = 0;
            size_t start = 0;
            while (start <= comboStr.size() && i < (int)skills.size()) {
                size_t comma = comboStr.find(L',', start);
                std::wstring tok = comboStr.substr(start,
                    comma == std::wstring::npos ? std::wstring::npos : comma - start);
                if (!tok.empty()) {
                    int id = _wtoi(tok.c_str());
                    skills[i].comboId = (id >= 0 && id < MINI_COMBO_COUNT) ? id : -1;
                    if (skills[i].mode == SKF_COMBO && skills[i].comboId >= 0)
                        skills[i].key = MINI_COMBOS[skills[i].comboId].a;
                }
                i++;
                if (comma == std::wstring::npos) break;
                start = comma + 1;
            }
        }
        skillIdx = Config::ReadInt(L"Skills", L"CurrentIdx", 0);
        skillIdx = min(skillIdx, max(0, (int)skills.size()-1));
        detector.suspendScanMs.store(
            (std::max)(100, Config::ReadInt(L"Detect", L"SuspendMs", 350)));
        {
            int detectOn = Config::ReadInt(L"Detect", L"Enabled", -1);
            if (detectOn < 0) detectOn = Config::ReadInt(L"Detect", L"Teleport", 1);
            detector.detectEnabled.store(detectOn != 0);
        }
        detector.targetRangeStuds.store((float)Config::ReadInt(L"Detect", L"RangeStuds", 45));
        soruRace.store(Config::ReadInt(L"Detect", L"SoruRace", SR_NORMAL));
        detector.soruRace.store(soruRace.load());
        CacheFireKey();
        RefreshDetectStatus();
    }

    void SaveConfig() {
        Config::WriteInt(L"Binds", L"HoldType", (int)holdBind.type);
        Config::WriteInt(L"Binds", L"HoldVK", holdBind.vk);
        Config::WriteInt(L"Binds", L"SwitchType", (int)switchBind.type);
        Config::WriteInt(L"Binds", L"SwitchVK", switchBind.vk);
        Config::WriteInt(L"Binds", L"SoruType", (int)soruBind.type);
        Config::WriteInt(L"Binds", L"SoruVK", soruBind.vk);
        Config::WriteInt(L"Spam", L"BindType", (int)spamBind.type);
        Config::WriteInt(L"Spam", L"BindVK", spamBind.vk);
        Config::WriteInt(L"Spam", L"Mode", spamMode.load());
        Config::WriteInt(L"Spam", L"IntervalMs", spamIntervalMs.load());

        std::wstring skillStr;
        std::wstring modesStr;
        std::wstring holdStr;
        std::wstring comboStr;
        for (const auto& s : skills) {
            skillStr += (wchar_t)toupper(s.key ? s.key : 'x');
            if (!modesStr.empty()) modesStr += L',';
            if (s.mode == SKF_COMBO) modesStr += L"2";
            else if (s.mode == SKF_HOLD) modesStr += L"1";
            else modesStr += L"0";
            if (!holdStr.empty()) holdStr += L',';
            wchar_t ms[16]{};
            swprintf_s(ms, L"%d", s.holdMs);
            holdStr += ms;
            if (!comboStr.empty()) comboStr += L',';
            wchar_t cid[16]{};
            swprintf_s(cid, L"%d", s.comboId);
            comboStr += cid;
        }
        Config::WriteString(L"Skills", L"Keys", skillStr.c_str());
        Config::WriteString(L"Skills", L"Modes", modesStr.c_str());
        Config::WriteString(L"Skills", L"HoldMs", holdStr.c_str());
        Config::WriteString(L"Skills", L"Combos", comboStr.c_str());
        Config::WriteInt(L"Skills", L"CurrentIdx", skillIdx);
        Config::WriteInt(L"Detect", L"Enabled", detector.detectEnabled.load() ? 1 : 0);
        Config::WriteInt(L"Detect", L"RangeStuds", (int)detector.targetRangeStuds.load());
        Config::WriteInt(L"Detect", L"SoruRace", soruRace.load());
    }

private:
    std::atomic<bool> _running{true};
    std::atomic<bool> _detectStarted{false};
    std::atomic<DWORD> _fireGateUntil{0};
    std::atomic<DWORD> _pauseSpamUntil{0};
    std::atomic<bool> _spamPressing{false};
    std::mutex _inputMu;
    SkillFireMode _pendingSkillMode = SKF_CLICK;
    int _pendingSkillHoldMs = 500;

    bool _SpamBindIsR() const {
        return spamBind.type == BT_KEY && (spamBind.vk == 'R' || spamBind.vk == 'r');
    }

    bool _SpamWanted() {
        if (!_running.load() || suspended.load() || capMode) return false;
        if (!SpamBindSet()) return false;
        bool want = (spamMode.load() == SPAM_HOLD)
            ? spamHeld.load()
            : spamEnabled.load();
        if (!want) return false;
        if (_RobloxFocused()) return true;
        if (_ForegroundIsOurUi()) {
            _FocusRoblox();
            return _RobloxFocused();
        }
        return false;
    }

    void _SpamLoop() {
        timeBeginPeriod(1);
        while (_running.load()) {
            bool want = _SpamWanted();
            detector.soruSpamLive.store(want);
            if (want) {
                DWORD now = GetTickCount();
                if (now < _fireGateUntil.load() || now < _pauseSpamUntil.load()) {
                    Sleep(1);
                    continue;
                }
                int iv = spamIntervalMs.load();
                if (iv < SPAM_MS_MIN) iv = SPAM_MS_MIN;
                if (iv > SPAM_MS_MAX) iv = SPAM_MS_MAX;
                {
                    std::lock_guard<std::mutex> lock(_inputMu);
                    now = GetTickCount();
                    if (now < _fireGateUntil.load() || now < _pauseSpamUntil.load() || !_SpamWanted())
                        continue;
                    _spamPressing.store(true);
                    detector.NoteSoruTap();
                    _TapR(iv <= 8 ? 0.35 : 1.0);
                    _spamPressing.store(false);
                }
                Sleep((DWORD)iv);
            } else {
                Sleep(2);
            }
        }
        detector.soruSpamLive.store(false);
        timeEndPeriod(1);
    }

    void _BindPollLoop() {
        bool prevHold = false, prevSwitch = false, prevSuspend = false;
        bool prevF3 = false, prevSpam = false;
        bool wasCapturing = false;

        while (_running) {
            if (capMode) {
                wasCapturing = true;
                if (!capArmed) {
                    if (!_AnyCaptureInputDown())
                        capArmed = true;
                    Sleep(10);
                    continue;
                }
                if (capType == "skill") {
                    for (int vk = 'A'; vk <= 'Z'; vk++) {
                        if (GetAsyncKeyState(vk) & 0x8000) {
                            FinishCapture_Key(vk);
                            break;
                        }
                    }
                } else {
                    for (int i = 0; BIND_PICK_VKS[i]; i++) {
                        int vk = BIND_PICK_VKS[i];
                        if (GetAsyncKeyState(vk) & 0x8000) {
                            FinishCapture_Key(vk);
                            break;
                        }
                    }
                }
                if (capMode && capType != "skill") {
                    if (GetAsyncKeyState(VK_XBUTTON2) & 0x8000)
                        FinishCapture_Mouse(WM_XBUTTONDOWN, XBUTTON2);
                    else if (GetAsyncKeyState(VK_XBUTTON1) & 0x8000)
                        FinishCapture_Mouse(WM_XBUTTONDOWN, XBUTTON1);
                    else if (GetAsyncKeyState(VK_MBUTTON) & 0x8000)
                        FinishCapture_Mouse(WM_MBUTTONDOWN, 0);
                    else if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
                        FinishCapture_Mouse(WM_LBUTTONDOWN, 0);
                    else if (GetAsyncKeyState(VK_RBUTTON) & 0x8000)
                        FinishCapture_Mouse(WM_RBUTTONDOWN, 0);
                }
                Sleep(10);
                continue;
            }

            if (wasCapturing) {
                wasCapturing = false;
                prevHold = holdBind.IsDown();
                prevSwitch = switchBind.IsDown();
                prevSpam = spamBind.IsDown();
                prevF3 = (GetAsyncKeyState(VK_F3) & 0x8000) != 0;
                prevSuspend = (GetAsyncKeyState(0xDD) & 0x8000) != 0;
                Sleep(10);
                continue;
            }

            bool f3 = (GetAsyncKeyState(VK_F3) & 0x8000) != 0;
            auto boundTo = [&](int vk) {
                return (holdBind.type == BT_KEY && holdBind.vk == vk)
                    || (switchBind.type == BT_KEY && switchBind.vk == vk)
                    || (spamBind.type == BT_KEY && spamBind.vk == vk);
            };
            if (f3 && !prevF3 && !boundTo(VK_F3)) CycleSkill();
            prevF3 = f3;

            bool suspendKey = (GetAsyncKeyState(0xDD) & 0x8000) != 0;
            if (suspendKey && !prevSuspend) {
                suspended = !suspended;
                if (suspended.load()) {
                    spamEnabled.store(false);
                    spamHeld.store(false);
                }
                if (onStatusChange) onStatusChange(suspended ? false : macroEnabled.load());
                if (onSpamChange) onSpamChange();
            }
            prevSuspend = suspendKey;

            if (!suspended) {
                bool hold = holdBind.IsDown();
                bool sw   = switchBind.IsDown();
                bool spam = spamBind.IsDown();

                if (hold && holdBind.type == BT_MOUSE_LEFT && isCursorOverUI && isCursorOverUI())
                    hold = false;
                if (spam && spamBind.type == BT_MOUSE_LEFT && isCursorOverUI && isCursorOverUI())
                    spam = false;

                if (hold && !prevHold) ToggleMacro();
                if (sw && !prevSwitch) CycleSkill();
                if (spamMode.load() == SPAM_TOGGLE && SpamBindSet() && spam && !prevSpam)
                    ToggleSpam();
                if (spamMode.load() == SPAM_HOLD && SpamBindSet()) {
                    bool wasHeld = spamHeld.load();
                    spamHeld.store(spam);
                    if (wasHeld != spam && onSpamChange) onSpamChange();
                } else if (spamMode.load() != SPAM_HOLD) {
                    spamHeld.store(false);
                }

                prevHold = holdBind.IsDown();
                prevSwitch = sw;
                prevSpam = spam;
            } else {
                prevHold = false;
                prevSwitch = false;
                prevSpam = false;
                spamHeld.store(false);
            }

            Sleep(5);
        }
    }
};
