local http = require "gamesense/http"

local curwe_loader = {}; do
    local ffi = require("ffi")

    -- ── Config (must match loader_ws on VDS) ────────────────────────────────
    local LOADER_VERSION = "2.0.5"
    local LOADER_API_BASE = "https://loader.curwe-code.ru"
    local LOADER_REGISTER_SECRET = "0f60e6586d632d359b7372fb848f8bbd83cf5879458f3554be026bbe3c5716ed"
    local REGISTER_API = LOADER_API_BASE .. "/api/register"
    local KEY_INIT_API = LOADER_API_BASE .. "/api/key/init"
    local ACCESS_API = LOADER_API_BASE .. "/api/access/"
    local DB_KEY = "curwe::loader::reg_v2"

    local REPO_OWNER = "empieza"
    local REPO_NAME = "curwe-gs-loader"
    local BRANCH = "main"
    local MANIFEST = "manifest.json"
    local ALLOWED_HWIDS = "allowed_hwids.txt"
    local LOADER_FILE = "loader.lua"

    local TAB = "LUA"
    local GROUP = "A"

    local find_signature = client.find_signature or function() return nil end

    local pGetModuleHandle_sig = find_signature("engine.dll", "\xFF\x15\xCC\xCC\xCC\xCC\x85\xC0\x74\x0B") or error("signature #1 not found")
    local pGetProcAddress_sig = find_signature("engine.dll", "\xFF\x15\xCC\xCC\xCC\xCC\xA3\xCC\xCC\xCC\xCC\xEB\x05") or error("signature #2 not found")
    local jmp_ecx = find_signature("engine.dll", "\xFF\xE1") or error("signature #3 not found")

    local pGetProcAddress = ffi.cast("uint32_t**", ffi.cast("uint32_t", pGetProcAddress_sig) + 2)[0][0]
    local fnGetProcAddress = ffi.cast("uint32_t(__fastcall*)(unsigned int, unsigned int, uint32_t, const char*)", jmp_ecx)
    local pGetModuleHandle = ffi.cast("uint32_t**", ffi.cast("uint32_t", pGetModuleHandle_sig) + 2)[0][0]
    local fnGetModuleHandle = ffi.cast("uint32_t(__fastcall*)(unsigned int, unsigned int, const char*)", jmp_ecx)

    local function proc_bind(module_name, function_name, typedef)
        local ctype = ffi.typeof(typedef)
        local module_handle = fnGetModuleHandle(pGetModuleHandle, 0, module_name)
        local proc_address = fnGetProcAddress(pGetProcAddress, 0, module_handle, function_name)
        local call_fn = ffi.cast(ctype, jmp_ecx)
        return function(...) return call_fn(proc_address, 0, ...) end
    end

    ffi.cdef[[
        typedef void VOID;
        typedef VOID* LPVOID;
        typedef const char* LPCSTR;
        typedef char* LPSTR;
        typedef unsigned long DWORD;
        typedef int BOOL;
        typedef unsigned __int64 ULONGLONG;
        typedef struct { DWORD dwLength; DWORD dwMemoryLoad; ULONGLONG ullTotalPhys; ULONGLONG ullAvailPhys; ULONGLONG ullTotalPageFile; ULONGLONG ullAvailPageFile; ULONGLONG ullTotalVirtual; ULONGLONG ullAvailVirtual; ULONGLONG ullAvailExtendedVirtual; } MEMORYSTATUSEX, *LPMEMORYSTATUSEX;
        typedef struct { union { DWORD dwOemId; struct { uint16_t wProcessorArchitecture; uint16_t wReserved; } DUMMY; } DUMMYUNION; DWORD dwPageSize; LPVOID lpMinAddr; LPVOID lpMaxAddr; uintptr_t dwActiveMask; DWORD dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocGran; uint16_t wProcessorLevel; uint16_t wProcessorRevision; } SYSTEM_INFO, *LPSYSTEM_INFO;
        typedef struct { char m_pDriverName[512]; unsigned int m_VendorID; unsigned int m_DeviceID; unsigned int m_SubSysID; unsigned int m_Revision; int m_nDXSupportLevel; int m_nMinDXSupportLevel; int m_nMaxDXSupportLevel; unsigned int m_nDriverVersionHigh; unsigned int m_nDriverVersionLow; } MaterialAdapterInfo_t;
    ]]

    local GlobalMemoryStatusEx = proc_bind("kernel32.dll", "GlobalMemoryStatusEx", "BOOL(__fastcall*)(unsigned int, unsigned int, LPMEMORYSTATUSEX)")
    local GetSystemInfo = proc_bind("kernel32.dll", "GetSystemInfo", "void(__fastcall*)(unsigned int, unsigned int, LPSYSTEM_INFO)")
    local GetVolumeInformationA = proc_bind("kernel32.dll", "GetVolumeInformationA", "BOOL(__fastcall*)(unsigned int, unsigned int, LPCSTR, LPSTR, DWORD, DWORD*, DWORD*, DWORD*, LPSTR, DWORD)")

    local function GetRAM()
        local ms = ffi.new("MEMORYSTATUSEX")
        ms.dwLength = ffi.sizeof(ms)
        if GlobalMemoryStatusEx and GlobalMemoryStatusEx(ms) ~= 0 then
            return math.floor(tonumber(ms.ullTotalPhys / (1024 * 1024)))
        end
        return 0
    end

    local function GetDisk()
        local serial = ffi.new("DWORD[1]")
        if GetVolumeInformationA and GetVolumeInformationA("C:\\", nil, 0, serial, nil, nil, nil, 0) ~= 0 then
            return serial[0]
        end
        return 0
    end

    local function GetCPU()
        local si = ffi.new("SYSTEM_INFO")
        if GetSystemInfo then GetSystemInfo(si) end
        return { level = si.wProcessorLevel or 0, cores = si.dwNumberOfProcessors or 1 }
    end

    local function GetGPU()
        local get_current = vtable_bind("materialsystem.dll", "VMaterialSystem080", 25, "int(__thiscall*)(void*)")
        local get_info = vtable_bind("materialsystem.dll", "VMaterialSystem080", 26, "void(__thiscall*)(void*, int, void*)")
        local info = ffi.new("MaterialAdapterInfo_t")
        if get_current and get_info then
            get_info(get_current(), info)
            return info.m_VendorID, info.m_DeviceID
        end
        return 0, 0
    end

    function curwe_loader.gethwid()
        local ram = GetRAM()
        local disk = GetDisk()
        local cpu = GetCPU()
        local g_ven, g_dev = GetGPU()

        local hwid = string.format("%02X%02X%04X%04X%04X%08X",
            math.min(cpu.level, 0xFF),
            math.min(cpu.cores, 0xFF),
            math.min(ram, 0xFFFF),
            math.min(g_ven, 0xFFFF),
            math.min(g_dev, 0xFFFF),
            disk
        )
        return string.upper(hwid)
    end

    local function normalize_hwid(value)
        if value == nil then
            return nil
        end
        local text = tostring(value):upper():gsub("%s+", "")
        if text == "" then
            return nil
        end
        return text
    end

    local function normalize_steam64(value)
        if value == nil then
            return nil
        end

        local text = tostring(value):gsub("%s+", "")
        text = text:match("^(%d+)") or text

        if text == "" or text == "0" then
            return nil
        end

        return text
    end

    local function get_local_steam64()
        local tries = {
            function()
                return panorama.open().MyPersonaAPI.GetXuid()
            end,
            function()
                return panorama.open("CSGOHud").MyPersonaAPI.GetXuid()
            end,
        }

        for i = 1, #tries do
            local ok, xuid = pcall(tries[i])
            local steam64 = normalize_steam64(xuid)
            if ok and steam64 ~= nil then
                return steam64
            end
        end

        local lp = entity.get_local_player()
        if lp ~= nil then
            return normalize_steam64(entity.get_steam64(lp))
        end
    end

    local function get_local_name()
        local lp = entity.get_local_player()
        if lp ~= nil then
            local name = entity.get_player_name(lp)
            if name ~= nil and name ~= "" then
                return name
            end
        end

        local ok, name = pcall(function()
            return panorama.open().MyPersonaAPI.GetName()
        end)

        if ok and name ~= nil and name ~= "" then
            return name
        end

        return "Unknown"
    end

    local function log_ok(msg)
        client.color_log(144, 238, 144, "[curwe] \0")
        client.color_log(255, 255, 255, tostring(msg))
    end

    local function log_warn(msg)
        client.color_log(255, 200, 96, "[curwe] \0")
        client.color_log(255, 255, 255, tostring(msg))
    end

    local function log_err(msg)
        client.color_log(255, 96, 96, "[curwe] \0")
        client.color_log(255, 255, 255, tostring(msg))
    end

    local function parse_json_body(body)
        if type(body) ~= "string" or body == "" then
            return nil
        end

        local ok, data = pcall(json.parse, body)
        if ok and type(data) == "table" then
            return data
        end

        return nil
    end

    local function http_status_text(response)
        if response == nil then
            return "no response"
        end
        return tostring(response.status or "error")
    end

    local function register_secret_configured()
        return LOADER_REGISTER_SECRET ~= nil
            and LOADER_REGISTER_SECRET ~= ""
            and LOADER_REGISTER_SECRET ~= "CHANGE_ME_SAME_AS_SERVER"
    end

    local function normalize_reg_key(value)
        if value == nil then
            return nil
        end

        local text = tostring(value):upper():gsub("[^A-Z0-9]", "")
        if #text < 8 then
            return nil
        end

        return text
    end

    local function generate_reg_key()
        local chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
        local out = {}

        for i = 1, 16 do
            local idx = client.random_int(1, #chars)
            out[i] = chars:sub(idx, idx)
        end

        return table.concat(out)
    end

    local function get_or_create_reg_key()
        local saved = database.read(DB_KEY)
        if type(saved) == "table" then
            local key = normalize_reg_key(saved.reg_key)
            if key ~= nil then
                return key, false
            end
        end

        local key = generate_reg_key()
        database.write(DB_KEY, { reg_key = key })
        database.flush()
        return key, true
    end

    local state = {
        hwid = curwe_loader.gethwid(),
        reg_key = nil,
        discord_linked = false,
        discord_ready = false,
        ui_built = false,
        authorized = false,
        registered = false,
        register_pending = false,
        key_check_pending = false,
        hwid_mismatched = false,
        busy = false,
        scripts = {},
        active = nil,
    }

    local function print_reg_key_instructions(key, is_new)
        log_ok(string.format("Curwe Loader v%s", LOADER_VERSION))
        log_ok(string.format("HWID: %s", state.hwid))

        if is_new then
            log_warn("Сгенерирован ключ регистрации (сохранён локально):")
        else
            log_warn("Ключ регистрации (привяжи Discord, если ещё не сделал):")
        end

        client.color_log(255, 220, 120, "[curwe] \0")
        client.color_log(255, 255, 255, key)
        log_warn("В Discord: /reg_key " .. key)
        log_warn("Скачать loader: /profile в Discord или loader.curwe-code.ru/download/loader.lua")
        log_warn("Пока ключ не привязан — меню лоадера скрыто")
    end

    local refs = {}

    local function set_register_ui(text)
        if refs.register_info ~= nil then
            ui.set(refs.register_info, text or "Register: ...")
        end
    end

    local function set_status(text)
        if refs.status ~= nil then
            ui.set(refs.status, text)
        end
    end

    local function set_access_ui()
        if state.hwid_mismatched then
            if refs.list ~= nil then
                ui.set_enabled(refs.list, false)
                ui.set_enabled(refs.load, false)
                ui.set_enabled(refs.unload, false)
                ui.set_enabled(refs.refresh, false)
            end
            return
        end

        local enabled = state.authorized and not state.busy

        ui.set_enabled(refs.list, enabled)
        ui.set_enabled(refs.load, enabled)
        ui.set_enabled(refs.unload, state.active ~= nil and not state.busy)

        if state.authorized then
            ui.set(refs.hwid_info, string.format("HWID: %s (authorized)", state.hwid))
        else
            ui.set(refs.hwid_info, string.format("HWID: %s (denied)", state.hwid))
        end
    end

    local function apply_hwid_mismatch()
        state.hwid_mismatched = true
        state.authorized = false
        state.busy = false
        state.scripts = {}

        log_err("HWID mismatched")
        log_warn("Ask admin for HWID reset in loader panel")
        log_warn("Then use /reg_key with your key and reload this script")

        if state.active ~= nil and type(unload_active) == "function" then
            pcall(unload_active)
        end

        if state.ui_built and refs.list ~= nil then
            ui.update(refs.list, { "HWID mismatched" })
            set_register_ui("Register: HWID mismatched")
            set_status("HWID mismatched")
            set_access_ui()
        end
    end

    local function register_with_admin(on_complete)
        if state.register_pending then
            return
        end

        if state.hwid_mismatched then
            if on_complete then
                on_complete(false)
            end
            return
        end

        if not register_secret_configured() then
            state.registered = false
            set_register_ui("Register: set LOADER_REGISTER_SECRET in lua")
            log_warn("LOADER_REGISTER_SECRET is not configured")
            if on_complete then
                on_complete(false)
            end
            return
        end

        state.hwid = curwe_loader.gethwid()
        state.register_pending = true
        set_register_ui("Register: contacting server...")

        local payload = json.stringify({
            reg_key = state.reg_key,
            hwid = state.hwid,
            steam64 = get_local_steam64() or "",
            steam_name = get_local_name(),
        })

        http.post(REGISTER_API, {
            headers = {
                ["Content-Type"] = "application/json",
                ["X-Register-Secret"] = LOADER_REGISTER_SECRET,
            },
            body = payload,
        }, function(ok, response)
            state.register_pending = false

            if not ok then
                state.registered = false
                set_register_ui("Register: server unreachable")
                log_err("Register failed — check DNS/HTTPS for loader.curwe-code.ru")
                if on_complete then
                    on_complete(false)
                end
                return
            end

            local status = response.status or 0
            local data = parse_json_body(response.body)

            if status >= 200 and status < 300 and data ~= nil and data.ok == true then
                state.registered = true
                set_register_ui("Register: ok — wait for admin Allow")
                log_ok("Registered on loader server")
                if on_complete then
                    on_complete(true)
                end
                return
            end

            state.registered = false

            if status == 401 or status == 403 then
                if data and data.error == "discord_not_linked" then
                    set_register_ui("Register: link Discord /reg_key")
                    log_warn("Discord not linked — use /reg_key in Discord")
                else
                    set_register_ui("Register: invalid secret")
                    log_err("Register failed — wrong LOADER_REGISTER_SECRET")
                end
            elseif status == 400 then
                set_register_ui("Register: invalid HWID")
                log_err("Register failed — invalid HWID")
            elseif status == 409 then
                apply_hwid_mismatch()
            else
                local err = data and data.error or http_status_text(response)
                set_register_ui(string.format("Register: failed (%s)", err))
                log_err(string.format("Register failed — HTTP %s", http_status_text(response)))
            end

            if on_complete then
                on_complete(false)
            end
        end)
    end

    local function no_cache_headers()
        return {
            ["Cache-Control"] = "no-cache",
            ["Pragma"] = "no-cache",
        }
    end

    local function raw_url(path, bust_cache)
        local url = string.format(
            "https://raw.githubusercontent.com/%s/%s/%s/%s",
            REPO_OWNER,
            REPO_NAME,
            BRANCH,
            path
        )

        if bust_cache then
            url = url .. "?t=" .. tostring(client.unix_time())
        end

        return url
    end

    local function normalize_http_body(body)
        if type(body) ~= "string" then
            return ""
        end

        return body:gsub("^\239\187\191", "")
    end

    local function is_loader_file(name)
        return name:lower() == LOADER_FILE:lower()
    end

    local function script_label(entry)
        return entry.label or entry.file
    end

    local function list_labels()
        if not state.authorized then
            return { "HWID not in allowed list" }
        end

        local out = {}
        for i = 1, #state.scripts do
            out[i] = script_label(state.scripts[i])
        end

        if #out == 0 then
            out[1] = "No scripts in manifest"
        end

        return out
    end

    local function find_script(label)
        for i = 1, #state.scripts do
            if script_label(state.scripts[i]) == label then
                return state.scripts[i]
            end
        end
    end

    local function get_selected_script()
        local index = ui.get(refs.list)
        if type(index) == "number" then
            return state.scripts[index + 1]
        end
        return find_script(index)
    end

    local function parse_hwid_line(line)
        if type(line) ~= "string" then
            return nil
        end

        line = line:gsub("%s+", ""):upper()
        if line == "" then
            return nil
        end

        if line:match("^#[^0-9A-F]") then
            return nil
        end

        line = line:gsub("^#", "")
        if line:match("^[%dA-F]+$") and #line >= 8 then
            return line
        end

        return nil
    end

    local function parse_allowed_hwids(body)
        local allowed = {}
        local normalized = {}

        body = normalize_http_body(body)

        if body == "" then
            return allowed, normalized
        end

        for line in body:gmatch("[^\r\n]+") do
            local hwid = parse_hwid_line(line)
            if hwid ~= nil and normalized[hwid] == nil then
                normalized[hwid] = true
                allowed[#allowed + 1] = hwid
            end
        end

        return allowed, normalized
    end

    local function hwid_allowed(allowed_map, hwid)
        if type(allowed_map) ~= "table" then
            return false
        end

        local normalized = normalize_hwid(hwid)
        return normalized ~= nil and allowed_map[normalized] == true
    end

    local function apply_manifest(data)
        local entries = {}
        local list = data.scripts or data

        if type(list) ~= "table" then
            return entries
        end

        for i = 1, #list do
            local item = list[i]

            if type(item) == "string" then
                if item:match("%.lua$") and not is_loader_file(item) then
                    entries[#entries + 1] = { file = item, label = item }
                end
            elseif type(item) == "table" and type(item.file) == "string" then
                if item.file:match("%.lua$") and not is_loader_file(item.file) then
                    entries[#entries + 1] = {
                        file = item.file,
                        label = item.name or item.label or item.file,
                    }
                end
            end
        end

        return entries
    end

    local function finish_manifest(data)
        state.scripts = apply_manifest(data)
        state.busy = false

        ui.update(refs.list, list_labels())
        if #state.scripts > 0 then
            ui.set(refs.list, 0)
        end

        set_access_ui()

        if #state.scripts == 0 then
            set_status("Authorized, but no scripts in manifest.json")
            log_err("manifest.json has no scripts")
        else
            set_status(string.format("%d script(s) available", #state.scripts))
            log_ok(string.format("Authorized · %d script(s)", #state.scripts))
        end
    end

    local function deny_access(reason)
        state.busy = false
        state.authorized = false
        state.scripts = {}
        ui.update(refs.list, { "HWID not in allowed list" })
        set_access_ui()
        set_status(reason or "Access denied")
        log_err(string.format("HWID %s is not allowed", state.hwid))
        log_warn("Ask admin to Allow your HWID, then press Refresh")
    end

    local function fetch_manifest()
        http.get(raw_url(MANIFEST, true), {
            headers = no_cache_headers(),
        }, function(ok, response)
            ui.set_enabled(refs.refresh, true)

            if not ok or response.status ~= 200 then
                state.busy = false
                state.authorized = false
                state.scripts = {}
                ui.update(refs.list, { "manifest.json not found" })
                set_access_ui()
                set_status("Could not fetch manifest.json")
                log_err(string.format("manifest HTTP %s", http_status_text(response)))
                return
            end

            local parsed_ok, data = pcall(json.parse, response.body)
            if not parsed_ok or type(data) ~= "table" then
                state.busy = false
                state.authorized = false
                state.scripts = {}
                ui.update(refs.list, { "Invalid manifest.json" })
                set_access_ui()
                set_status("Invalid manifest.json")
                log_err("Could not parse manifest.json")
                return
            end

            finish_manifest(data)
        end)
    end

    local function grant_access(source)
        if state.hwid_mismatched or not state.registered then
            deny_access("Access denied — register HWID first")
            return
        end

        state.authorized = true
        set_access_ui()
        set_status(string.format("HWID authorized (%s), fetching scripts...", source))
        fetch_manifest()
    end

    local function check_access_github(on_done)
        http.get(raw_url(ALLOWED_HWIDS, true), {
            headers = no_cache_headers(),
        }, function(ok, response)
            if not ok or response.status ~= 200 then
                on_done(false, false, 0)
                log_err(string.format("allowed_hwids HTTP %s", http_status_text(response)))
                return
            end

            local allowed_list, allowed_map = parse_allowed_hwids(response.body)
            local allowed = hwid_allowed(allowed_map, state.hwid)
            on_done(true, allowed, #allowed_list)
        end)
    end

    local function check_access_api(on_done)
        local url = ACCESS_API .. state.hwid
        if state.reg_key ~= nil and state.reg_key ~= "" then
            url = url .. "?reg_key=" .. state.reg_key
        end

        http.get(url, {
            headers = no_cache_headers(),
        }, function(ok, response)
            if not ok or response.status ~= 200 then
                on_done(false, nil)
                return
            end

            local data = parse_json_body(response.body)
            if data == nil or data.ok ~= true then
                on_done(false, nil)
                return
            end

            on_done(true, data.allowed == true)
        end)
    end

    local function run_access_check()
        if state.hwid_mismatched or not state.registered then
            state.busy = false
            if state.hwid_mismatched then
                apply_hwid_mismatch()
            end
            return
        end

        set_status("Checking HWID...")
        ui.update(refs.list, { "Loading..." })
        ui.set_enabled(refs.list, false)
        ui.set_enabled(refs.load, false)
        ui.set_enabled(refs.refresh, false)
        ui.set_enabled(refs.unload, false)

        check_access_api(function(api_ok, api_allowed)
            if state.hwid_mismatched or not state.registered then
                state.busy = false
                if state.hwid_mismatched then
                    apply_hwid_mismatch()
                end
                return
            end

            if api_ok then
                if api_allowed then
                    grant_access("server")
                    return
                end

                deny_access("Denied — admin must Allow HWID on loader panel")
                ui.set_enabled(refs.refresh, true)
                return
            end

            state.busy = false
            deny_access("Server access check failed — try Refresh")
            ui.set_enabled(refs.refresh, true)
        end)
    end

    local function fetch_access_list()
        if state.busy or state.hwid_mismatched then
            return
        end

        if not state.ui_built or refs.list == nil then
            return
        end

        state.hwid = curwe_loader.gethwid()
        state.busy = true
        state.authorized = false
        state.scripts = {}

        set_status("Registering HWID...")
        ui.update(refs.list, { "Registering..." })
        ui.set_enabled(refs.list, false)
        ui.set_enabled(refs.load, false)
        ui.set_enabled(refs.refresh, false)
        ui.set_enabled(refs.unload, false)

        register_with_admin(function(registered)
            if not registered or state.hwid_mismatched then
                state.busy = false
                if state.hwid_mismatched then
                    apply_hwid_mismatch()
                end
                return
            end

            run_access_check()
        end)
    end

    local function create_sandbox()
        local ui_refs = {}
        local callbacks = {}

        local ui_proxy = setmetatable({}, {
            __index = function(_, key)
                local fn = ui[key]
                if type(fn) == "function" and key:sub(1, 4) == "new_" then
                    return function(...)
                        local a, b, c, d = fn(...)
                        if a ~= nil then ui_refs[#ui_refs + 1] = a end
                        if b ~= nil then ui_refs[#ui_refs + 1] = b end
                        if c ~= nil then ui_refs[#ui_refs + 1] = c end
                        if d ~= nil then ui_refs[#ui_refs + 1] = d end
                        return a, b, c, d
                    end
                end
                return fn
            end,
        })

        local client_proxy = setmetatable({}, {
            __index = function(_, key)
                if key == "set_event_callback" then
                    return function(event, callback)
                        callbacks[#callbacks + 1] = { event, callback }
                        client.set_event_callback(event, callback)
                    end
                end
                if key == "unset_event_callback" then
                    return function(event, callback)
                        if callback ~= nil then
                            client.unset_event_callback(event, callback)
                        else
                            client.unset_event_callback(event)
                        end
                    end
                end
                return client[key]
            end,
        })

        local env = setmetatable({
            ui = ui_proxy,
            client = client_proxy,
        }, { __index = _G })

        return env, ui_refs, callbacks
    end

    local function cleanup_sandbox(ui_refs, callbacks)
        if callbacks then
            for i = 1, #callbacks do
                local entry = callbacks[i]
                pcall(client.unset_event_callback, entry[1], entry[2])
            end
        end

        if ui_refs then
            for i = 1, #ui_refs do
                pcall(ui.set_visible, ui_refs[i], false)
            end
        end
    end

    local function unload_active()
        if state.active == nil then
            return
        end

        local label = state.active.label

        if type(state.active.unload) == "function" then
            pcall(state.active.unload)
        end

        cleanup_sandbox(state.active.ui_refs, state.active.callbacks)
        state.active = nil
        set_access_ui()

        log_ok(string.format('Unloaded "%s"', label))
        set_status(state.authorized and "Script unloaded" or "Access denied")
    end

    local function load_selected()
        if state.busy or not state.authorized then
            return
        end

        local entry = get_selected_script()
        if entry == nil then
            log_err("Select a script from the list")
            return
        end

        state.busy = true
        set_status("Downloading...")
        ui.set_enabled(refs.load, false)
        ui.set_enabled(refs.unload, false)
        log_ok(string.format('Loading "%s"...', entry.label))

        http.get(raw_url(entry.file, true), {
            headers = no_cache_headers(),
        }, function(ok, response)
            state.busy = false
            set_access_ui()

            if not ok or response.status ~= 200 then
                set_status("Download failed")
                log_err(string.format('Failed to download "%s" (HTTP %s)', entry.file, http_status_text(response)))
                return
            end

            unload_active()

            local env, ui_refs, callbacks = create_sandbox()
            local chunk, err = load(response.body, "@" .. entry.file, "t", env)

            if chunk == nil and loadstring then
                chunk, err = loadstring(response.body, "@" .. entry.file)
                if chunk ~= nil and setfenv then
                    setfenv(chunk, env)
                end
            end

            if chunk == nil then
                set_status("Compile error")
                log_err(tostring(err))
                return
            end

            local run_ok, result = pcall(chunk)
            if not run_ok then
                cleanup_sandbox(ui_refs, callbacks)
                set_status("Runtime error")
                log_err(tostring(result))
                return
            end

            local unload_fn = nil
            if type(result) == "table" and type(result.unload) == "function" then
                unload_fn = result.unload
            elseif type(result) == "function" then
                unload_fn = result
            end

            state.active = {
                label = entry.label,
                file = entry.file,
                ui_refs = ui_refs,
                callbacks = callbacks,
                unload = unload_fn,
            }

            set_access_ui()
            set_status(string.format("Running: %s", entry.label))
            log_ok(string.format('"%s" is running', entry.label))
        end)
    end

    local function build_main_ui()
        if state.ui_built then
            return
        end

        state.ui_built = true

        refs.version = ui.new_label(TAB, GROUP, string.format("Curwe Loader v%s", LOADER_VERSION))
        refs.hwid_info = ui.new_label(TAB, GROUP, "HWID: ...")
        refs.register_info = ui.new_label(TAB, GROUP, "Register: ...")
        refs.status = ui.new_label(TAB, GROUP, "Starting...")
        refs.refresh = ui.new_button(TAB, GROUP, "Refresh list", fetch_access_list)
        refs.list = ui.new_listbox(TAB, GROUP, "Scripts", { "Loading..." })
        refs.load = ui.new_button(TAB, GROUP, "Load", load_selected)
        refs.unload = ui.new_button(TAB, GROUP, "Unload", unload_active)

        set_access_ui()
        set_register_ui("Register: pending")

        if state.hwid_mismatched then
            apply_hwid_mismatch()
            return
        end

        log_ok(string.format("Discord linked · HWID: %s", state.hwid))

        fetch_access_list()

        client.set_event_callback("player_connect_full", function(event)
            if client.userid_to_entindex(event.userid) == entity.get_local_player() then
                register_with_admin()
            end
        end)

        client.set_event_callback("shutdown", function()
            unload_active()
        end)
    end

    local function activate_loader()
        if not state.discord_linked then
            return
        end

        build_main_ui()
    end

    local function schedule_key_recheck(delay)
        if state.hwid_mismatched then
            return
        end

        client.delay_call(delay or 15, function()
            if state.ui_built or state.hwid_mismatched then
                return
            end
            key_init_flow()
        end)
    end

    local function key_init_flow()
        if state.key_check_pending then
            return
        end

        if not register_secret_configured() then
            log_warn("LOADER_REGISTER_SECRET is not configured")
            return
        end

        state.reg_key, state.key_is_new = get_or_create_reg_key()
        state.hwid = curwe_loader.gethwid()
        state.key_check_pending = true

        local payload = json.stringify({
            reg_key = state.reg_key,
            hwid = state.hwid,
        })

        http.post(KEY_INIT_API, {
            headers = {
                ["Content-Type"] = "application/json",
                ["X-Register-Secret"] = LOADER_REGISTER_SECRET,
            },
            body = payload,
        }, function(ok, response)
            state.key_check_pending = false

            if not ok then
                log_err("Key check failed — server unreachable")
                schedule_key_recheck(20)
                return
            end

            local status = response.status or 0
            local data = parse_json_body(response.body)

            if status < 200 or status >= 300 or data == nil or data.ok ~= true then
                log_err(string.format("Key check failed — HTTP %s", http_status_text(response)))
                schedule_key_recheck(20)
                return
            end

            state.discord_linked = data.discord_linked == true
            state.discord_ready = data.discord_ready == true

            if data.hwid_mismatched == true then
                state.hwid_mismatched = true
                if state.discord_linked then
                    activate_loader()
                end
                apply_hwid_mismatch()
                return
            end

            if data.require_reg_key == true or not state.discord_ready then
                print_reg_key_instructions(state.reg_key, state.key_is_new)
                schedule_key_recheck(15)
                return
            end

            activate_loader()
        end)
    end

    key_init_flow()
end
