--[[
    efg2g loader  +  Junkie Key System
    ------------------------------------------------------------------
    Flow:
      1. Kunin ang supported games list  (/scripts/supported.lua)
      2. Key system (Junkie SDK)  ->  saved key / keyless / key UI
      3. getgenv().SCRIPT_KEY = validated key
      4. I-load ang game script:
           - self-hosted /games/<file>.lua  -> POST request, key nasa
             x-script-key header + body (hindi sa URL). Ang GET sa
             /games/ ay laging parang homepage lang ang sagot ng worker.
           - external URL (Junkie CDN)      -> GET na may ?key= param

    Junkie docs: https://docs.jnkie.com/roblox-sdk/external-loader
]]

repeat task.wait() until game:IsLoaded()

local Players     = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer

-- =====================================================================
--  CONFIG  (ito lang ang kailangan mong i-edit)
-- =====================================================================
local BRAND  = "efg2g"
local DOMAIN = "efg2g.pages.dev"

local SUPPORTED_URL  = "https://" .. DOMAIN .. "/scripts/supported.lua"
local GAMES_BASE_URL = "https://" .. DOMAIN .. "/games/"

local JUNKIE = {
    ENABLED    = true,                               -- false = walang key system (dev/testing lang)
    SDK_URL    = "https://jnkie.com/sdk/library.lua",

    SERVICE    = "efg2g-free",                            -- Dashboard -> Services   (service name)
    IDENTIFIER = "1136363",                     -- Dashboard -> your user ID (string, e.g. "12345")
    PROVIDER   = "Free",                             -- Dashboard -> Providers  (provider name)

    SAVE_KEY   = true,                               -- i-save ang valid key sa file para hindi na mag-type ulit
    KEY_FOLDER = "efg2g",
    KEY_FILE   = "efg2g/key.txt",

    -- Kapag TRUE, susubukan muna ang "KEYLESS" bago ipakita ang UI.
    -- Dahil dito gumagana ang "Keyless mode" / "Keyless weekdays" toggle
    -- sa dashboard nang hindi na kailangang i-redeploy ang loader.
    KEYLESS_PROBE = true,
}
-- =====================================================================

local function kick(msg)
    pcall(function()
        LocalPlayer:Kick("[" .. BRAND .. "] " .. msg)
    end)
end

local function notify(title, text, duration)
    pcall(function()
        game:GetService("StarterGui"):SetCore("SendNotification", {
            Title    = title,
            Text     = text,
            Duration = duration or 4,
        })
    end)
end

local function trim(s)
    return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

-- Iba-iba ang casing ng response fields depende sa executor: Body/body at
-- StatusCode/status_code/status. I-normalize dito para hindi na ulit-ulitin.
local function parseResponse(resp)
    local code = tonumber(resp.StatusCode or resp.status_code
        or resp.Status or resp.status) or 0
    local body = resp.Body or resp.body or resp.ResponseBody or resp.responseBody
    if body ~= nil then
        body = tostring(body)
    end
    return code, body
end

local function httpGet(url)
    -- Try request() first (available sa karamihan ng modern executors) para makuha ang status code.
    -- Kapag 200 pero walang body, huwag agad mag-fail dahil may executors na
    -- nagbabalik ng empty Body kahit successful; gamitin ang game:HttpGet fallback.
    local requestError
    if type(request) == "function" then
        local ok, resp = pcall(function()
            return request({
                Url = url,
                Method = "GET",
                Headers = {
                    ["Accept"] = "text/plain,application/octet-stream,*/*",
                },
            })
        end)

        if ok and type(resp) == "table" then
            local code, body = parseResponse(resp)

            if body and #body > 0 then
                if code == 0 or (code >= 200 and code < 300) then
                    return body
                end
                return nil, "HTTP " .. tostring(code)
            end

            if code >= 400 then
                requestError = "HTTP " .. tostring(code)
            else
                requestError = "empty response from request()"
            end
        elseif ok and type(resp) == "string" and #resp > 0 then
            -- A few executors return the body directly instead of a response table.
            return resp
        elseif not ok then
            requestError = tostring(resp)
        end
        -- Fall through sa game:HttpGet kapag nag-fail o empty ang request().
    end

    local ok, res = pcall(function()
        return game:HttpGet(url, true)
    end)
    if ok and type(res) == "string" and #res > 0 then
        return res
    end
    if not ok then
        return nil, requestError or tostring(res)
    end
    return nil, requestError or "empty response from game:HttpGet"
end

-- POST fetch para sa self-hosted /games/*.lua (Layer 2 gate sa _worker.js).
--   * ang key ay nasa `x-script-key` header AT sa body — hindi lumalabas sa URL,
--     sa CDN access logs, o sa mga nag-scan ng GET requests
--   * ang GET sa /games/ ay laging "homepage" lang ang sagot ng worker, kaya
--     WALANG game:HttpGet fallback dito (ang ma-fefetch lang nito ay ang loader
--     mismo -> magre-recurse); kailangan talaga ang request() na may POST
--   * isang retry kapag empty ang body (common executor quirk)
--   * kapag may laman pero error status (hal. 403 gate body), ibinabalik ang body
--     para ma-detect sa ibaba at maipakita ang TOTOONG reason imbes na generic
--     "empty response"
local function fetchGameScript(url, key)
    if type(request) ~= "function" then
        return nil, "walang request() function ang executor mo (kailangan ang POST support)"
    end

    key = type(key) == "string" and trim(key) or ""
    local headers = {
        ["Accept"] = "text/plain,application/octet-stream,*/*",
    }
    if #key > 0 then
        headers["x-script-key"] = key
    end

    local lastErr = "empty response from request()"
    for attempt = 1, 2 do
        local ok, resp = pcall(function()
            return request({
                Url     = url,
                Method  = "POST",
                Headers = headers,
                Body    = key,
            })
        end)

        if ok and type(resp) == "table" then
            local code, body = parseResponse(resp)
            if body and #body > 0 then
                if code == 0 or (code >= 200 and code < 300) then
                    return body
                end
                return body, "HTTP " .. tostring(code)
            end
            lastErr = code >= 400 and ("HTTP " .. tostring(code)) or "empty response from request()"
        elseif ok and type(resp) == "string" and #resp > 0 then
            return resp
        elseif not ok then
            lastErr = tostring(resp)
        end

        if attempt < 2 then
            task.wait(0.3)
        end
    end

    return nil, lastErr
end

-- =====================================================================
--  1) SUPPORTED GAMES
-- =====================================================================
local okList, SUPPORTED_GAMES = pcall(function()
    local src, fetchErr = httpGet(SUPPORTED_URL)
    if not src then
        error(fetchErr or "empty response")
    end
    local chunk, compileErr = loadstring(src)
    if not chunk then
        error(compileErr or "invalid supported-games response")
    end
    return chunk()
end)

if not okList or type(SUPPORTED_GAMES) ~= "table" then
    kick("Failed to load game list.")
    return
end

local entry = SUPPORTED_GAMES[game.PlaceId]

if not entry then
    kick("does not support this game.\nPlaceId: " .. tostring(game.PlaceId))
    return
end

-- entry can be:  "142823291.lua"  (file sa /games/)   o   "https://..." (full URL, e.g. Junkie CDN)
local scriptUrl
if type(entry) == "table" then
    scriptUrl = entry.url or entry.file
else
    scriptUrl = entry
end
if type(scriptUrl) ~= "string" or #scriptUrl == 0 then
    kick("Invalid entry in game list for PlaceId " .. tostring(game.PlaceId))
    return
end
-- Self-hosted sa /games/ ba (POST gate) o external URL (hal. Junkie CDN, GET)?
local isSelfHosted = false
if not scriptUrl:match("^https?://") then
    scriptUrl = GAMES_BASE_URL .. scriptUrl
    isSelfHosted = true
elseif scriptUrl:sub(1, #GAMES_BASE_URL) == GAMES_BASE_URL then
    isSelfHosted = true
end

-- =====================================================================
--  2) KEY SYSTEM  (Junkie)
-- =====================================================================
local ERROR_TEXT = {
    KEY_INVALID       = "Invalid key. Please check and try again.",
    KEY_EXPIRED       = "Your key has expired. Click 'Get Key' for a new one.",
    HWID_BANNED       = "You are HWID banned from this script.",
    KEY_INVALIDATED   = "This key has been disabled.",
    ALREADY_USED      = "This one-time key was already used.",
    HWID_MISMATCH     = "This key is bound to another device (HWID limit reached).",
    SERVICE_NOT_FOUND = "Service not found. (Loader config error - contact the owner.)",
    SERVICE_MISMATCH  = "This key is for a different script.",
    PREMIUM_REQUIRED  = "A premium key is required for this script.",
    RATE_LIMITED      = "Rate limited. Please wait ~5 minutes and try again.",
    ERROR             = "Network error. Please try again.",
}

local function describe(code)
    code = tostring(code or "ERROR")
    if ERROR_TEXT[code] then
        return ERROR_TEXT[code]
    end
    if code:match("^http %d+") then
        return "Server error (" .. code .. "). Try again in a moment."
    end
    return code
end

local function loadJunkie()
    local ok, lib = pcall(function()
        local src, fetchErr = httpGet(JUNKIE.SDK_URL)
        if not src then
            error(fetchErr or "empty response")
        end
        local chunk, compileErr = loadstring(src)
        if not chunk then
            error(compileErr or "invalid SDK response")
        end
        return chunk()
    end)
    if not ok or type(lib) ~= "table" then
        return nil, tostring(lib)
    end
    lib.service    = JUNKIE.SERVICE
    lib.identifier = tostring(JUNKIE.IDENTIFIER)
    lib.provider   = JUNKIE.PROVIDER
    return lib
end

-- returns: valid(boolean), code(string)   code = "KEY_VALID" | "KEYLESS" | <error code>
local function checkKey(Junkie, key)
    local ok, res = pcall(Junkie.check_key, key)
    if not ok or type(res) ~= "table" then
        return false, "ERROR"
    end
    if res.valid or res.success then
        return true, res.message or "KEY_VALID"
    end
    return false, res.error or res.message or "KEY_INVALID"
end

-- returns: link(string|nil), err(string|nil)
local function getKeyLink(Junkie)
    local ok, link, err = pcall(Junkie.get_key_link)
    if not ok then
        return nil, "ERROR"
    end
    if type(link) == "string" and #link > 0 then
        return link, nil
    end
    return nil, tostring(err or "ERROR")
end

-- ---------- saved key (file) ----------
local function fsAvailable()
    return JUNKIE.SAVE_KEY
        and type(readfile) == "function"
        and type(writefile) == "function"
        and type(isfile) == "function"
end

local function loadSavedKey()
    if not fsAvailable() then return nil end
    local ok, key = pcall(function()
        if isfile(JUNKIE.KEY_FILE) then
            return readfile(JUNKIE.KEY_FILE)
        end
        return nil
    end)
    if ok and type(key) == "string" then
        key = trim(key)
        if #key > 0 then return key end
    end
    return nil
end

local function saveKey(key)
    if not fsAvailable() then return end
    pcall(function()
        if type(makefolder) == "function" and type(isfolder) == "function" then
            if not isfolder(JUNKIE.KEY_FOLDER) then
                makefolder(JUNKIE.KEY_FOLDER)
            end
        end
        writefile(JUNKIE.KEY_FILE, key)
    end)
end

local function clearSavedKey()
    if not fsAvailable() then return end
    pcall(function()
        if isfile(JUNKIE.KEY_FILE) then
            if type(delfile) == "function" then
                delfile(JUNKIE.KEY_FILE)
            else
                writefile(JUNKIE.KEY_FILE, "")
            end
        end
    end)
end

local function copyToClipboard(text)
    local fn = setclipboard or toclipboard or set_clipboard
        or (Clipboard and Clipboard.set)
    if type(fn) ~= "function" then return false end
    return pcall(fn, text)
end

-- ---------- Key UI ----------
local THEME = {
    Background = Color3.fromRGB(12, 12, 15),
    Panel      = Color3.fromRGB(21, 21, 27),
    Stroke     = Color3.fromRGB(42, 42, 54),
    Accent     = Color3.fromRGB(0, 212, 255),
    AccentText = Color3.fromRGB(5, 5, 8),
    Text       = Color3.fromRGB(240, 240, 245),
    Muted      = Color3.fromRGB(140, 140, 158),
    Success    = Color3.fromRGB(80, 220, 140),
    Error      = Color3.fromRGB(255, 92, 92),
    Warning    = Color3.fromRGB(255, 190, 70),
}

local function new(class, props, parent)
    local inst = Instance.new(class)
    for k, v in pairs(props or {}) do
        inst[k] = v
    end
    if parent then
        inst.Parent = parent
    end
    return inst
end

local function getGuiParent(gui)
    if type(protect_gui) == "function" then
        pcall(protect_gui, gui)
    end
    if type(gethui) == "function" then
        local ok, hui = pcall(gethui)
        if ok and hui then return hui end
    end
    local okCore = pcall(function()
        return game:GetService("CoreGui"):GetChildren()
    end)
    if okCore then
        return game:GetService("CoreGui")
    end
    return LocalPlayer:WaitForChild("PlayerGui")
end

local function makeDraggable(frame, handle)
    local UIS = game:GetService("UserInputService")
    local dragging, dragInput, dragStart, startPos

    handle.InputBegan:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1
            or input.UserInputType == Enum.UserInputType.Touch then
            dragging  = true
            dragStart = input.Position
            startPos  = frame.Position
            input.Changed:Connect(function()
                if input.UserInputState == Enum.UserInputState.End then
                    dragging = false
                end
            end)
        end
    end)

    handle.InputChanged:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseMovement
            or input.UserInputType == Enum.UserInputType.Touch then
            dragInput = input
        end
    end)

    UIS.InputChanged:Connect(function(input)
        if dragging and input == dragInput then
            local delta = input.Position - dragStart
            frame.Position = UDim2.new(
                startPos.X.Scale, startPos.X.Offset + delta.X,
                startPos.Y.Scale, startPos.Y.Offset + delta.Y
            )
        end
    end)
end

local function createKeyUI(handlers)
    local gui = new("ScreenGui", {
        Name           = BRAND .. "_KeySystem",
        ResetOnSpawn   = false,
        ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
        DisplayOrder   = 999,
    })
    gui.Parent = getGuiParent(gui)

    local window = new("Frame", {
        Name             = "Window",
        AnchorPoint      = Vector2.new(0.5, 0.5),
        Position         = UDim2.new(0.5, 0, 0.5, 0),
        Size             = UDim2.new(0, 380, 0, 262),
        BackgroundColor3 = THEME.Background,
        BorderSizePixel  = 0,
    }, gui)
    new("UICorner", { CornerRadius = UDim.new(0, 12) }, window)
    new("UIStroke", { Color = THEME.Stroke, Thickness = 1 }, window)

    -- header
    local header = new("Frame", {
        Name                   = "Header",
        Size                   = UDim2.new(1, 0, 0, 52),
        BackgroundTransparency = 1,
    }, window)

    new("TextLabel", {
        Name                   = "Title",
        Position               = UDim2.new(0, 16, 0, 10),
        Size                   = UDim2.new(1, -70, 0, 20),
        BackgroundTransparency = 1,
        Text                   = BRAND,
        TextColor3             = THEME.Text,
        Font                   = Enum.Font.GothamBold,
        TextSize               = 17,
        TextXAlignment         = Enum.TextXAlignment.Left,
    }, header)

    new("TextLabel", {
        Name                   = "Subtitle",
        Position               = UDim2.new(0, 16, 0, 30),
        Size                   = UDim2.new(1, -70, 0, 16),
        BackgroundTransparency = 1,
        Text                   = "Key System  •  complete the checkpoint to get your key",
        TextColor3             = THEME.Muted,
        Font                   = Enum.Font.Gotham,
        TextSize               = 11,
        TextXAlignment         = Enum.TextXAlignment.Left,
        TextTruncate           = Enum.TextTruncate.AtEnd,
    }, header)

    local closeBtn = new("TextButton", {
        Name                   = "Close",
        AnchorPoint            = Vector2.new(1, 0),
        Position               = UDim2.new(1, -12, 0, 12),
        Size                   = UDim2.new(0, 28, 0, 28),
        BackgroundColor3       = THEME.Panel,
        Text                   = "×",
        TextColor3             = THEME.Muted,
        Font                   = Enum.Font.GothamBold,
        TextSize               = 18,
        AutoButtonColor        = false,
    }, header)
    new("UICorner", { CornerRadius = UDim.new(0, 8) }, closeBtn)

    -- key input
    local keyBox = new("TextBox", {
        Name                   = "KeyInput",
        Position               = UDim2.new(0, 16, 0, 62),
        Size                   = UDim2.new(1, -32, 0, 38),
        BackgroundColor3       = THEME.Panel,
        BorderSizePixel        = 0,
        Text                   = "",
        PlaceholderText        = "Paste your key here...",
        PlaceholderColor3      = THEME.Muted,
        TextColor3             = THEME.Text,
        Font                   = Enum.Font.Gotham,
        TextSize               = 13,
        ClearTextOnFocus       = false,
        TextXAlignment         = Enum.TextXAlignment.Left,
    }, window)
    new("UICorner", { CornerRadius = UDim.new(0, 8) }, keyBox)
    new("UIStroke", { Color = THEME.Stroke, Thickness = 1 }, keyBox)
    new("UIPadding", { PaddingLeft = UDim.new(0, 12), PaddingRight = UDim.new(0, 12) }, keyBox)

    -- buttons
    local getKeyBtn = new("TextButton", {
        Name             = "GetKey",
        Position         = UDim2.new(0, 16, 0, 110),
        Size             = UDim2.new(0.5, -20, 0, 36),
        BackgroundColor3 = THEME.Accent,
        BorderSizePixel  = 0,
        Text             = "Get Key",
        TextColor3       = THEME.AccentText,
        Font             = Enum.Font.GothamBold,
        TextSize         = 13,
        AutoButtonColor  = false,
    }, window)
    new("UICorner", { CornerRadius = UDim.new(0, 8) }, getKeyBtn)

    local checkBtn = new("TextButton", {
        Name             = "CheckKey",
        AnchorPoint      = Vector2.new(1, 0),
        Position         = UDim2.new(1, -16, 0, 110),
        Size             = UDim2.new(0.5, -20, 0, 36),
        BackgroundColor3 = THEME.Panel,
        BorderSizePixel  = 0,
        Text             = "Check Key",
        TextColor3       = THEME.Text,
        Font             = Enum.Font.GothamBold,
        TextSize         = 13,
        AutoButtonColor  = false,
    }, window)
    new("UICorner", { CornerRadius = UDim.new(0, 8) }, checkBtn)
    new("UIStroke", { Color = THEME.Stroke, Thickness = 1 }, checkBtn)

    -- link box (read-only; para makopya sa mobile kung walang setclipboard)
    local linkBox = new("TextBox", {
        Name                   = "KeyLink",
        Position               = UDim2.new(0, 16, 0, 156),
        Size                   = UDim2.new(1, -32, 0, 30),
        BackgroundColor3       = THEME.Panel,
        BorderSizePixel        = 0,
        Text                   = "",
        TextColor3             = THEME.Accent,
        Font                   = Enum.Font.Code,
        TextSize               = 11,
        ClearTextOnFocus       = false,
        TextEditable           = false,
        TextXAlignment         = Enum.TextXAlignment.Left,
        Visible                = false,
    }, window)
    new("UICorner", { CornerRadius = UDim.new(0, 8) }, linkBox)
    new("UIPadding", { PaddingLeft = UDim.new(0, 10), PaddingRight = UDim.new(0, 10) }, linkBox)

    -- status
    local status = new("TextLabel", {
        Name                   = "Status",
        Position               = UDim2.new(0, 16, 0, 192),
        Size                   = UDim2.new(1, -32, 0, 40),
        BackgroundTransparency = 1,
        Text                   = "Click 'Get Key' to receive your key link, then paste the key above.",
        TextColor3             = THEME.Muted,
        Font                   = Enum.Font.Gotham,
        TextSize               = 12,
        TextWrapped            = true,
        TextXAlignment         = Enum.TextXAlignment.Left,
        TextYAlignment         = Enum.TextYAlignment.Top,
    }, window)

    new("TextLabel", {
        Name                   = "Footer",
        Position               = UDim2.new(0, 16, 1, -22),
        Size                   = UDim2.new(1, -32, 0, 14),
        BackgroundTransparency = 1,
        Text                   = BRAND .. "  •  secured by Junkie",
        TextColor3             = THEME.Stroke,
        Font                   = Enum.Font.Gotham,
        TextSize               = 10,
        TextXAlignment         = Enum.TextXAlignment.Right,
    }, window)

    makeDraggable(window, header)

    -- hover effects
    local function hover(btn, normal, hovered)
        btn.MouseEnter:Connect(function() btn.BackgroundColor3 = hovered end)
        btn.MouseLeave:Connect(function() btn.BackgroundColor3 = normal end)
    end
    hover(getKeyBtn, THEME.Accent, Color3.fromRGB(80, 226, 255))
    hover(checkBtn,  THEME.Panel,  Color3.fromRGB(32, 32, 41))
    hover(closeBtn,  THEME.Panel,  Color3.fromRGB(60, 30, 34))

    -- wiring
    getKeyBtn.MouseButton1Click:Connect(function()
        handlers.onGetKey()
    end)
    checkBtn.MouseButton1Click:Connect(function()
        handlers.onCheck(keyBox.Text)
    end)
    keyBox.FocusLost:Connect(function(enterPressed)
        if enterPressed then
            handlers.onCheck(keyBox.Text)
        end
    end)
    closeBtn.MouseButton1Click:Connect(function()
        handlers.onClose()
    end)

    local ui = {}

    function ui:SetStatus(text, kind)
        status.Text = text
        if kind == "ok" then
            status.TextColor3 = THEME.Success
        elseif kind == "err" then
            status.TextColor3 = THEME.Error
        elseif kind == "warn" then
            status.TextColor3 = THEME.Warning
        else
            status.TextColor3 = THEME.Muted
        end
    end

    function ui:ShowLink(link)
        linkBox.Text    = link
        linkBox.Visible = true
        getKeyBtn.Text  = "Copy Link"
    end

    function ui:SetBusy(busy)
        getKeyBtn.Active = not busy
        checkBtn.Active  = not busy
        checkBtn.Text    = busy and "Checking..." or "Check Key"
    end

    function ui:Destroy()
        pcall(function() gui:Destroy() end)
    end

    return ui
end

-- returns validated key (string) or nil kapag hindi nag-proceed
local function runKeySystem()
    local Junkie = loadJunkie()
    if not Junkie then
        kick("Failed to load key system. Please re-execute.")
        return nil
    end

    -- (a) silent checks -> skip UI kung may valid na
    --     1. getgenv().SCRIPT_KEY na pre-set (e.g. galing sa "ready-to-copy loadstring" ng Junkie flow)
    --     2. saved key sa file
    --     3. "KEYLESS" probe (kung walang 1 at 2)
    local candidates = {}
    local preset = getgenv().SCRIPT_KEY
    if type(preset) == "string" and #trim(preset) > 0 then
        table.insert(candidates, { key = trim(preset), source = "preset" })
    end
    local saved = loadSavedKey()
    if saved and saved ~= (candidates[1] and candidates[1].key) then
        table.insert(candidates, { key = saved, source = "saved" })
    end
    if #candidates == 0 and JUNKIE.KEYLESS_PROBE then
        table.insert(candidates, { key = "KEYLESS", source = "probe" })
    end

    for _, c in ipairs(candidates) do
        local valid, code = checkKey(Junkie, c.key)
        if valid then
            if c.source == "preset" and code ~= "KEYLESS" then
                saveKey(c.key)
            end
            return c.key, code
        end
        if code == "HWID_BANNED" then
            kick(ERROR_TEXT.HWID_BANNED)
            return nil
        end
        if c.source == "saved" and (code == "KEY_INVALID" or code == "KEY_EXPIRED"
            or code == "KEY_INVALIDATED" or code == "ALREADY_USED"
            or code == "SERVICE_MISMATCH") then
            clearSavedKey()
        end
    end

    -- (b) UI
    local result     = nil   -- string = key, false = closed
    local resultCode = nil
    local busy       = false
    local cachedLink = nil
    local ui

    ui = createKeyUI({
        onGetKey = function()
            if busy then return end
            if cachedLink then
                local copied = copyToClipboard(cachedLink)
                ui:SetStatus(copied and "Link copied to clipboard again!"
                    or "Copy the link above, finish the checkpoint, then paste your key.", "ok")
                return
            end
            busy = true
            ui:SetBusy(true)
            ui:SetStatus("Generating your key link...", "info")
            task.spawn(function()
                local link, err = getKeyLink(Junkie)
                if link then
                    cachedLink = link
                    ui:ShowLink(link)
                    local copied = copyToClipboard(link)
                    if copied then
                        ui:SetStatus("Link copied to clipboard! Open it in your browser, finish the checkpoint, then paste the key here.", "ok")
                    else
                        ui:SetStatus("Copy the link above, open it in your browser, finish the checkpoint, then paste the key here.", "ok")
                    end
                else
                    ui:SetStatus(describe(err), err == "RATE_LIMITED" and "warn" or "err")
                end
                busy = false
                ui:SetBusy(false)
            end)
        end,

        onCheck = function(text)
            if busy then return end
            local key = trim(text)
            if #key == 0 then
                ui:SetStatus("Please paste your key first.", "err")
                return
            end
            busy = true
            ui:SetBusy(true)
            ui:SetStatus("Checking key...", "info")
            task.spawn(function()
                local valid, code = checkKey(Junkie, key)
                if valid then
                    ui:SetStatus("Key accepted by SDK. Verifying script access...", "info")
                    saveKey(key)
                    resultCode = code
                    result = key
                else
                    ui:SetStatus(describe(code), "err")
                    if code == "HWID_BANNED" then
                        result = false
                        kick(ERROR_TEXT.HWID_BANNED)
                    end
                end
                busy = false
                ui:SetBusy(false)
            end)
        end,

        onClose = function()
            result = false
        end,
    })

    repeat task.wait(0.1) until result ~= nil

    task.wait(0.4)
    ui:Destroy()

    if result == false then
        return nil
    end
    return result, resultCode
end

if JUNKIE.ENABLED then
    local key, code = runKeySystem()
    if not key then
        return
    end
    getgenv().SCRIPT_KEY = key
    notify(BRAND, code == "KEYLESS" and "Keyless mode active. Loading..." or "Key accepted by SDK. Verifying script access...", 3)
end

-- =====================================================================
--  3) LOAD GAME SCRIPT
--     * Self-hosted /games/*.lua  -> POST (Layer 2): key nasa header/body,
--       hindi sa URL. Ang GET sa /games/ ay parang homepage lang sa worker.
--     * External URL (Junkie CDN) -> GET na may ?key= param (si Junkie mismo
--       ang nagve-verify nito server-side).
-- =====================================================================
local function urlEncode(str)
    return tostring(str):gsub("([^%w%-%_%.%~])", function(c)
        return string.format("%%%02X", string.byte(c))
    end)
end

local finalUrl = scriptUrl
local content, httpErr

if isSelfHosted then
    content, httpErr = fetchGameScript(finalUrl, getgenv().SCRIPT_KEY)
else
    do
        local k = getgenv().SCRIPT_KEY
        if type(k) == "string" and #trim(k) > 0 then
            local sep = finalUrl:find("?", 1, true) and "&" or "?"
            finalUrl = finalUrl .. sep .. "key=" .. urlEncode(k)
        end
    end
    content, httpErr = httpGet(finalUrl)
end

if not content or #content == 0 then
    kick("Failed to load script (empty response). Please re-execute." .. (httpErr and ("\n" .. httpErr) or ""))
    return
end

-- Detect HTML error pages (Cloudflare 404/500/1020/WAF)
if content:sub(1, 1) == "<" then
    kick("Failed to load script (server returned HTML - baka 404 o na-block).\nURL: " .. tostring(finalUrl):gsub("key=[^&]+", "key=***") .. (httpErr and ("\n" .. httpErr) or ""))
    return
end

-- Detect Layer 2 gate error responses from _worker.js (nagsisimula sa "-- [BRAND]" at may error() sa dulo)
local gateReason = content:match("^%-%- %[" .. BRAND .. "%] Access Denied: ([^\n]+)")
    or content:match("^%-%- %[" .. BRAND .. "%] Key verification failed: ([^\n]+)")
if gateReason then
    -- Only discard keys with a definitive lifecycle failure. A missing REST
    -- record after SDK success can be an account/config mismatch, not a bad key.
    local needsNewKey = gateReason:find("KEY_EXPIRED", 1, true)
        or gateReason:find("KEY_INVALIDATED", 1, true)
        or gateReason:find("ALREADY_USED", 1, true)
    local hint = "Please retry later or contact the script owner."
    if needsNewKey then
        clearSavedKey()
        hint = "Please re-execute and enter a new key."
    elseif gateReason:find("KEY_INVALID", 1, true) and JUNKIE.ENABLED then
        hint = "The SDK accepted your key, but the server could not find it. Contact the owner to check the worker Junkie account/API key and deployment."
    elseif gateReason:find("SERVICE_MISMATCH", 1, true) then
        hint = "Contact the owner to match JUNKIE_SERVICE with the loader service."
    end
    kick("Key verification failed: " .. gateReason .. "\n" .. hint)
    return
end

-- Safety net: kung ang nakuha ay ang LOADER MISMO (ibig sabihin naka-GET sa
-- /games/ o naka-fall back sa homepage response ng worker), huwag itong i-run
-- para hindi mag-recurse ang loader.
if content:find("efg2g loader", 1, true) then
    kick("Failed to load script (unexpected response - homepage ang nakuha).\nPlease re-execute (" .. BRAND .. ").")
    return
end

local fn, err = loadstring(content)
if not fn then
    -- Ipakita ang unang linya para ma-debug agad (hal. "-- [efg2g] ..." error message)
    local firstLine = content:match("^([^\n]*)")
    kick("Compile error: " .. tostring(err) .. "\nFirst line: " .. tostring(firstLine))
    return
end

local okRun, runErr = pcall(fn)
if not okRun then
    -- Runtime error sa loob ng game script; huwag mag-kick, mag-warn lang
    -- (ang kick ay maka-kick sa player kahit successful naman ang load)
    warn("[" .. BRAND .. "] Script error: " .. tostring(runErr))
end
