local _, ns = ...
local L = ns.L or {}

local FEATURE_ID = "quiet_loot"
local STYLE_NORMAL = "normal"
local STYLE_CLEAN = "clean"
local DIRECTION_DOWN = "down"
local DIRECTION_UP = "up"

local MAX_LOOT_ROWS = 6
local MAX_TOAST_ROWS = 5
local TOAST_DURATION = 4.5
local TOAST_DEDUPE_SECONDS = 8
local TOAST_DEDUPE_PRUNE_SECONDS = 24
local TOAST_WIDTH = 310
local TOAST_HEIGHT = 30
local LOOT_WINDOW_POSITION_KEY = "quietLootWindowPosition"
local TOAST_POSITION_KEY = "quietLootToastPosition"
local MENU_POSITION_KEY = "quietLootMenuPosition"
local VISUAL_STYLE_KEY = "quietLootVisualStyle"
local LOOT_WINDOW_ENABLED_KEY = "quietLootWindowEnabled"
local REWARD_LOOT_ENABLED_KEY = "quietLootRewardLootEnabled"
local GROWTH_DIRECTION_KEY = "quietLootGrowthDirection"
local LOOT_SCALE_KEY = "quietLootLootScale"
local TOAST_SCALE_KEY = "quietLootToastScale"

local DEFAULT_LOOT_WINDOW_POSITION = { point = "CENTER", relativePoint = "CENTER", x = 0, y = 40 }
local DEFAULT_TOAST_POSITION = { point = "TOP", relativePoint = "TOP", x = 0, y = -185 }
local DEFAULT_MENU_POSITION = { point = "CENTER", relativePoint = "CENTER", x = 0, y = 0 }
local SCALE_VALUES = { 0.75, 0.9, 1, 1.15, 1.3 }

local EVENT_NAMES = {
    "PLAYER_ENTERING_WORLD",
    "LOOT_OPENED",
    "LOOT_CLOSED",
    "LOOT_SLOT_CHANGED",
    "LOOT_SLOT_CLEARED",
    "CHAT_MSG_LOOT",
    "CHAT_MSG_CURRENCY",
    "SHOW_LOOT_TOAST",
    "SHOW_LOOT_TOAST_UPGRADE",
    "SHOW_LOOT_TOAST_LEGENDARY_LOOTED",
    "ENCOUNTER_LOOT_RECEIVED",
    "LOOT_ITEM_ROLL_WON",
    "BONUS_ROLL_RESULT",
}

local ALERT_FRAME_EVENTS = {
    "SHOW_LOOT_TOAST",
    "SHOW_LOOT_TOAST_UPGRADE",
    "SHOW_LOOT_TOAST_LEGENDARY_LOOTED",
}

local Runtime = {
    enabled = false,
    eventFrame = nil,
    lootWindow = nil,
    toastFrame = nil,
    menuFrame = nil,
    toastRows = {},
    toastQueue = {},
    testLootRows = nil,
    recentMessages = {},
    registeredEvents = {},
    alertFrameMuted = false,
    alertFrameEventState = nil,
    nativeLootHooked = false,
    customLootActive = false,
    testMode = false,
    refreshQueued = false,
    bossHooked = false,
    bossHookFunc = nil,
    originalBossBannerOnEvent = nil,
}

local function getNow()
    return type(GetTime) == "function" and GetTime() or time()
end

local function getFeatureDB()
    return ns.EnsureFeatureDB and ns:EnsureFeatureDB() or nil
end

local function getSetting(settingKey, defaultValue)
    local db = getFeatureDB()
    if type(db) == "table" and db[settingKey] ~= nil then
        return db[settingKey]
    end
    return defaultValue
end

local function setSetting(settingKey, value)
    local db = getFeatureDB()
    if type(db) == "table" then
        db[settingKey] = value
    end
    return value
end

local function getBoolSetting(settingKey, defaultValue)
    local value = getSetting(settingKey, defaultValue)
    if value == nil then
        return defaultValue == true
    end
    return value == true
end

local function setBoolSetting(settingKey, value)
    return setSetting(settingKey, value == true) == true
end

local function useSimpleLootWindow()
    return getBoolSetting(LOOT_WINDOW_ENABLED_KEY, true)
end

local function useRewardLoot()
    return getBoolSetting(REWARD_LOOT_ENABLED_KEY, true)
end

local function cycleBoolSetting(settingKey, defaultValue)
    return setBoolSetting(settingKey, not getBoolSetting(settingKey, defaultValue))
end

local function getVisualStyle()
    local value = getSetting(VISUAL_STYLE_KEY, STYLE_NORMAL)
    if value == STYLE_CLEAN then
        return STYLE_CLEAN
    end
    return STYLE_NORMAL
end

local function useCleanStyle()
    return getVisualStyle() == STYLE_CLEAN
end

local function setVisualStyle(style)
    return setSetting(VISUAL_STYLE_KEY, style == STYLE_CLEAN and STYLE_CLEAN or STYLE_NORMAL)
end

local function getGrowthDirection()
    return getSetting(GROWTH_DIRECTION_KEY, DIRECTION_DOWN) == DIRECTION_UP and DIRECTION_UP or DIRECTION_DOWN
end

local function growUp()
    return getGrowthDirection() == DIRECTION_UP
end

local function setGrowthDirection(direction)
    return setSetting(GROWTH_DIRECTION_KEY, direction == DIRECTION_UP and DIRECTION_UP or DIRECTION_DOWN)
end

local function getScaleSetting(settingKey)
    local value = tonumber(getSetting(settingKey, 1)) or 1
    if value < 0.6 then
        return 0.6
    end
    if value > 1.5 then
        return 1.5
    end
    return value
end

local function cycleScaleSetting(settingKey)
    local current = getScaleSetting(settingKey)
    for index, value in ipairs(SCALE_VALUES) do
        if math.abs(current - value) < 0.01 then
            return setSetting(settingKey, SCALE_VALUES[(index % #SCALE_VALUES) + 1])
        end
    end
    return setSetting(settingKey, 1)
end

local function getScalePercent(settingKey)
    return math.floor((getScaleSetting(settingKey) * 100) + 0.5)
end

local function playerMatches(playerName)
    if type(playerName) ~= "string" or playerName == "" then
        return true
    end
    if not UnitName then
        return true
    end

    local currentPlayer = UnitName("player")
    if playerName == currentPlayer then
        return true
    end

    if type(Ambiguate) == "function" then
        local ok, shortName = pcall(Ambiguate, playerName, "none")
        if ok and shortName == currentPlayer then
            return true
        end
    end

    return false
end

local function getQualityColor(quality)
    local color = quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality]
    if color then
        return color.r or 1, color.g or 1, color.b or 1
    end
    return 0.94, 0.94, 0.94
end

local function extractItemLink(text)
    if type(text) ~= "string" then
        return nil
    end
    return text:match("|c%x+|Hitem:.-|h%[.-%]|h|r") or text:match("|Hitem:.-|h%[.-%]|h")
end

local function extractCurrencyLink(text)
    if type(text) ~= "string" then
        return nil
    end
    return text:match("|c%x+|Hcurrency:.-|h%[.-%]|h|r") or text:match("|Hcurrency:.-|h%[.-%]|h")
end

local function getLinkDisplayName(link)
    if type(link) ~= "string" then
        return nil
    end
    return link:match("%[([^%]]+)%]")
end

local function normalizeDedupeText(value)
    if type(value) ~= "string" or value == "" then
        return nil
    end
    local text = value:gsub("|A:[^|]+|a", " ")
    text = text:gsub("|T[^|]+|t", " ")
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")
    text = text:gsub("|H.-|h%[([^%]]+)%]|h", "%1")
    text = text:gsub("[%p%c]", " ")
    text = text:gsub("%s+", " ")
    text = text:gsub("^%s+", ""):gsub("%s+$", "")
    if text == "" then
        return nil
    end
    local ok, lowered = pcall(string.lower, text)
    return ok and lowered or text
end

local function parseItemIDFromLink(link)
    if type(link) ~= "string" then
        return nil
    end
    return tonumber(link:match("|Hitem:(%d+)") or link:match("item:(%d+)"))
end

local function parseCurrencyIDFromLink(link)
    if type(link) ~= "string" then
        return nil
    end
    return tonumber(link:match("|Hcurrency:(%d+)") or link:match("currency:(%d+)"))
end

local function extractQuantity(text, fallback)
    if type(text) == "string" then
        local count = text:match("[xX](%d+)")
        count = tonumber(count)
        if count and count > 0 and count < 100000 then
            return count
        end
    end
    return tonumber(fallback) or 1
end

local function getItemInfoFromLink(link)
    if type(link) ~= "string" then
        return nil
    end

    local itemID, icon, quality = parseItemIDFromLink(link), nil, nil
    if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
        local apiItemID
        apiItemID, _, _, _, icon = C_Item.GetItemInfoInstant(link)
        itemID = tonumber(apiItemID) or itemID
    elseif type(GetItemInfoInstant) == "function" then
        local apiItemID
        apiItemID, _, _, _, icon = GetItemInfoInstant(link)
        itemID = tonumber(apiItemID) or itemID
    end

    if C_Item and type(C_Item.GetItemInfo) == "function" then
        local name, _, itemQuality, _, _, _, _, _, _, itemIcon = C_Item.GetItemInfo(link)
        return itemID, name, itemIcon or icon, itemQuality or quality
    elseif type(GetItemInfo) == "function" then
        local name, _, itemQuality, _, _, _, _, _, _, itemIcon = GetItemInfo(link)
        return itemID, name, itemIcon or icon, itemQuality or quality
    end

    return itemID, nil, icon, quality
end

local function buildCurrencyToastData(link, quantity)
    if type(link) ~= "string" then
        return nil
    end
    local currencyID = parseCurrencyIDFromLink(link)
    local name, icon, quality
    if C_CurrencyInfo and type(C_CurrencyInfo.GetCurrencyInfoFromLink) == "function" then
        local info = C_CurrencyInfo.GetCurrencyInfoFromLink(link)
        if info then
            name = info.name
            icon = info.iconFileID
            quality = info.quality
            currencyID = tonumber(info.currencyID) or currencyID
        end
    end
    local key = "currency:" .. tostring(currencyID or normalizeDedupeText(getLinkDisplayName(link) or link) or link)
    return {
        key = key,
        dedupeKey = key,
        dedupeName = not currencyID and normalizeDedupeText(name or getLinkDisplayName(link)) or nil,
        text = link,
        icon = icon,
        quantity = quantity or 1,
        quality = quality or 1,
        tooltipLink = link,
        fallbackName = name,
    }
end

local function buildItemToastData(link, quantity)
    if type(link) ~= "string" then
        return nil
    end
    local itemID, name, icon, quality = getItemInfoFromLink(link)
    itemID = tonumber(itemID) or parseItemIDFromLink(link)
    local key = "item:" .. tostring(itemID or normalizeDedupeText(name or getLinkDisplayName(link) or link) or link)
    return {
        key = key,
        dedupeKey = key,
        dedupeName = not itemID and normalizeDedupeText(name or getLinkDisplayName(link)) or nil,
        text = link,
        icon = icon,
        quantity = quantity or 1,
        quality = quality or 1,
        tooltipLink = link,
        fallbackName = name,
    }
end

local function showTooltip(owner, data)
    if not owner or not GameTooltip then
        return
    end
    GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
    if data and data.tooltipLink and type(GameTooltip.SetHyperlink) == "function" then
        GameTooltip:SetHyperlink(data.tooltipLink)
    elseif data and data.name then
        GameTooltip:SetText(data.name)
    end
    GameTooltip:Show()
end

local function hideTooltip()
    if GameTooltip then
        GameTooltip:Hide()
    end
end

local function applyPosition(frame, positionKey, defaultPosition)
    if not frame then
        return
    end

    if ns.ApplyFeaturePosition then
        ns:ApplyFeaturePosition(frame, positionKey, defaultPosition)
        return
    end

    frame:ClearAllPoints()
    frame:SetPoint(
        defaultPosition.point or "CENTER",
        UIParent,
        defaultPosition.relativePoint or defaultPosition.point or "CENTER",
        defaultPosition.x or 0,
        defaultPosition.y or 0
    )
end

local function storePosition(frame, positionKey)
    if frame and ns.StoreFeaturePosition then
        ns:StoreFeaturePosition(frame, positionKey)
    end
end

local function clearPosition(positionKey)
    local db = getFeatureDB()
    if type(db) == "table" then
        db[positionKey] = nil
    end
end

local function makeFrameDraggable(frame, positionKey)
    if not frame then
        return
    end

    frame:EnableMouse(true)
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", function(selfFrame)
        selfFrame:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(selfFrame)
        selfFrame:StopMovingOrSizing()
        storePosition(selfFrame, positionKey)
    end)
end

local function setFrameInteraction(frame, enabled)
    if not frame then
        return
    end
    enabled = enabled == true
    if type(frame.EnableMouse) == "function" then
        pcall(frame.EnableMouse, frame, enabled)
    end
    if type(frame.SetMouseClickEnabled) == "function" then
        pcall(frame.SetMouseClickEnabled, frame, enabled)
    end
    if type(frame.SetMouseMotionEnabled) == "function" then
        pcall(frame.SetMouseMotionEnabled, frame, enabled)
    end
end

function Runtime:EnsureEventFrame()
    if self.eventFrame or type(CreateFrame) ~= "function" then
        return
    end

    local frame = CreateFrame("Frame")
    frame:SetScript("OnEvent", function(_, eventName, ...)
        Runtime:HandleEvent(eventName, ...)
    end)
    self.eventFrame = frame
end

function Runtime:RegisterEvents()
    self:EnsureEventFrame()
    local frame = self.eventFrame
    if not frame or type(frame.RegisterEvent) ~= "function" then
        return
    end

    for _, eventName in ipairs(EVENT_NAMES) do
        if not self.registeredEvents[eventName] then
            local ok = pcall(frame.RegisterEvent, frame, eventName)
            if ok then
                self.registeredEvents[eventName] = true
            end
        end
    end
end

function Runtime:UnregisterEvents()
    local frame = self.eventFrame
    if not frame or type(frame.UnregisterEvent) ~= "function" then
        wipe(self.registeredEvents)
        return
    end

    for eventName in pairs(self.registeredEvents) do
        pcall(frame.UnregisterEvent, frame, eventName)
    end
    wipe(self.registeredEvents)
end

function Runtime:UpdateAlertFrameMute()
    local shouldMute = self.enabled == true
    local frame = AlertFrame
    if not frame or type(frame.UnregisterEvent) ~= "function" or type(frame.RegisterEvent) ~= "function" then
        return
    end

    if shouldMute and not self.alertFrameMuted then
        self.alertFrameMuted = true
        self.alertFrameEventState = {}

        for _, eventName in ipairs(ALERT_FRAME_EVENTS) do
            local wasRegistered = true
            if type(frame.IsEventRegistered) == "function" then
                local ok, registered = pcall(frame.IsEventRegistered, frame, eventName)
                wasRegistered = ok and registered == true
            end
            self.alertFrameEventState[eventName] = wasRegistered
            pcall(frame.UnregisterEvent, frame, eventName)
        end
    elseif not shouldMute and self.alertFrameMuted then
        local previous = self.alertFrameEventState or {}
        self.alertFrameMuted = false
        self.alertFrameEventState = nil

        for _, eventName in ipairs(ALERT_FRAME_EVENTS) do
            if previous[eventName] then
                pcall(frame.RegisterEvent, frame, eventName)
            end
        end
    end
end

function Runtime:InstallNativeLootHook()
    if self.nativeLootHooked or not LootFrame or type(LootFrame.HookScript) ~= "function" then
        return
    end

    self.nativeLootHooked = true
    pcall(LootFrame.HookScript, LootFrame, "OnShow", function(frame)
        if Runtime.enabled == true and Runtime.customLootActive == true and useSimpleLootWindow() and type(frame.Hide) == "function" then
            pcall(frame.Hide, frame)
        end
    end)
end

function Runtime:HideNativeLootFrame()
    if LootFrame and type(LootFrame.Hide) == "function" then
        pcall(LootFrame.Hide, LootFrame)
    end
end

function Runtime:CreateLootRow(parent, index)
    local row = CreateFrame("Button", nil, parent)
    row.index = index
    row:SetSize(278, 28)
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints(row)
    row.bg:SetColorTexture(0.02, 0.025, 0.03, 0.62)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("LEFT", row, "LEFT", 4, 0)
    row.icon:SetSize(22, 22)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    row.count = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.count:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
    row.count:SetWidth(34)
    row.count:SetJustifyH("LEFT")

    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.text:SetPoint("LEFT", row.count, "RIGHT", 0, 0)
    row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    row.text:SetJustifyH("LEFT")
    row.text:SetWordWrap(false)

    row:SetScript("OnClick", function(selfRow, button)
        if button == "RightButton" then
            if Runtime.testMode == true then
                Runtime:ToggleMenu()
            end
            return
        end
        if Runtime.testMode == true then
            return
        end
        if selfRow.slotIndex and LootSlot then
            pcall(LootSlot, selfRow.slotIndex)
            Runtime:QueueLootWindowRefresh(0.05)
        end
    end)
    row:SetScript("OnEnter", function(selfRow)
        showTooltip(selfRow, selfRow.data)
    end)
    row:SetScript("OnLeave", hideTooltip)
    row:SetScript("OnDragStart", function()
        if Runtime.testMode == true then
            parent:StartMoving()
        end
    end)
    row:SetScript("OnDragStop", function()
        if Runtime.testMode == true then
            parent:StopMovingOrSizing()
            storePosition(parent, LOOT_WINDOW_POSITION_KEY)
        end
    end)
    row:RegisterForDrag("LeftButton")

    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, -34 - ((index - 1) * 31))
    return row
end

function Runtime:EnsureAnchorMarker(frame)
    if not frame or frame.anchorDot then
        return
    end

    frame.anchorDot = frame:CreateTexture(nil, "OVERLAY")
    frame.anchorDot:SetSize(8, 8)
    frame.anchorDot:SetColorTexture(0.98, 0.82, 0.26, 1)

    frame.anchorText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.anchorText:SetText(L.FEATURE_QUIET_LOOT_ANCHOR_LABEL or "Anchor")
    frame.anchorText:SetTextColor(0.98, 0.82, 0.26, 1)
end

function Runtime:ApplyAnchorMarker(frame)
    if not frame then
        return
    end
    self:EnsureAnchorMarker(frame)

    local showMarker = self.testMode == true
    frame.anchorDot:SetShown(showMarker)
    frame.anchorText:SetShown(showMarker)
    if not showMarker then
        return
    end

    frame.anchorDot:ClearAllPoints()
    frame.anchorText:ClearAllPoints()
    if growUp() then
        frame.anchorDot:SetPoint("CENTER", frame, "BOTTOM", 0, 0)
        frame.anchorText:SetPoint("TOP", frame.anchorDot, "BOTTOM", 0, -3)
    else
        frame.anchorDot:SetPoint("CENTER", frame, "TOP", 0, 0)
        frame.anchorText:SetPoint("BOTTOM", frame.anchorDot, "TOP", 0, 3)
    end
end

function Runtime:UpdateFrameInteraction()
    local enabled = self.testMode == true

    if self.lootWindow then
        setFrameInteraction(self.lootWindow, enabled)
        setFrameInteraction(self.lootWindow.takeAll, enabled)
        setFrameInteraction(self.lootWindow.close, enabled)
        for _, row in ipairs(self.lootWindow.rows or {}) do
            setFrameInteraction(row, enabled)
        end
    end

    if self.toastFrame then
        setFrameInteraction(self.toastFrame, enabled)
    end
    for _, row in ipairs(self.toastRows or {}) do
        setFrameInteraction(row, enabled)
    end
end

function Runtime:ApplyVisualStyle()
    local clean = useCleanStyle()
    local rowsGrowUp = growUp()

    if self.lootWindow then
        local frame = self.lootWindow
        frame:SetScale(getScaleSetting(LOOT_SCALE_KEY))
        if frame.bg then
            frame.bg:SetAlpha(clean and 0 or 1)
        end
        if frame.title then
            frame.title:SetShown(not clean)
            frame.title:ClearAllPoints()
            frame.title:SetPoint(rowsGrowUp and "BOTTOMLEFT" or "TOPLEFT", frame, rowsGrowUp and "BOTTOMLEFT" or "TOPLEFT", 9, rowsGrowUp and 8 or -8)
        end
        if frame.close then
            frame.close:SetAlpha(clean and 0.78 or 1)
            frame.close:ClearAllPoints()
            frame.close:SetPoint(rowsGrowUp and "BOTTOMRIGHT" or "TOPRIGHT", frame, rowsGrowUp and "BOTTOMRIGHT" or "TOPRIGHT", -6, rowsGrowUp and 6 or -6)
        end
        if frame.takeAll then
            frame.takeAll:SetAlpha(clean and 0.86 or 1)
            frame.takeAll:ClearAllPoints()
            frame.takeAll:SetPoint(rowsGrowUp and "BOTTOMRIGHT" or "TOPRIGHT", frame.close or frame, rowsGrowUp and "BOTTOMLEFT" or "TOPLEFT", -4, 0)
        end

        for index, row in ipairs(frame.rows or {}) do
            row:ClearAllPoints()
            if rowsGrowUp then
                row:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 8, (clean and 5 or 34) + ((index - 1) * 31))
            else
                row:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, (clean and -5 or -34) - ((index - 1) * 31))
            end
            if row.bg then
                row.bg:SetAlpha(clean and 0 or 1)
            end
        end
        self:ApplyAnchorMarker(frame)
    end

    if self.toastFrame then
        self.toastFrame:SetScale(getScaleSetting(TOAST_SCALE_KEY))
        self:ApplyAnchorMarker(self.toastFrame)
    end

    for index, row in ipairs(self.toastRows or {}) do
        row:ClearAllPoints()
        if rowsGrowUp then
            row:SetPoint("BOTTOM", self.toastFrame, "BOTTOM", 0, ((index - 1) * (TOAST_HEIGHT + 4)))
        else
            row:SetPoint("TOP", self.toastFrame, "TOP", 0, -((index - 1) * (TOAST_HEIGHT + 4)))
        end
        if row.bg then
            row.bg:SetAlpha(clean and 0 or 1)
        end
    end
    self:UpdateFrameInteraction()
end

function Runtime:EnsureLootWindow()
    if self.lootWindow or type(CreateFrame) ~= "function" or not UIParent then
        return self.lootWindow
    end

    local frame = CreateFrame("Frame", "VaultloomQuietLootWindow", UIParent)
    frame:SetSize(294, 74)
    frame:SetFrameStrata("DIALOG")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    makeFrameDraggable(frame, LOOT_WINDOW_POSITION_KEY)
    frame:Hide()

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints(frame)
    frame.bg:SetColorTexture(0.015, 0.018, 0.022, 0.84)

    frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 9, -8)
    frame.title:SetText(L.FEATURE_QUIET_LOOT_WINDOW_TITLE or "Loot")

    frame.takeAll = CreateFrame("Button", nil, frame)
    frame.takeAll:SetSize(62, 20)
    frame.takeAll:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -28, -5)
    frame.takeAll.text = frame.takeAll:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.takeAll.text:SetAllPoints(frame.takeAll)
    frame.takeAll.text:SetText(L.FEATURE_QUIET_LOOT_TAKE_ALL or "Take All")
    frame.takeAll:SetScript("OnClick", function()
        Runtime:TakeAllLoot()
    end)

    frame.close = CreateFrame("Button", nil, frame)
    frame.close:SetSize(18, 18)
    frame.close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
    frame.close.text = frame.close:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.close.text:SetAllPoints(frame.close)
    frame.close.text:SetText("x")
    frame.close:SetScript("OnClick", function()
        if Runtime.testMode == true then
            Runtime:HideTestMode()
            return
        end
        if CloseLoot then
            pcall(CloseLoot)
        end
        Runtime:HideLootWindow()
    end)
    frame:SetScript("OnMouseUp", function(_, button)
        if button == "RightButton" and Runtime.testMode == true then
            Runtime:ToggleMenu()
        end
    end)

    frame.rows = {}
    for index = 1, MAX_LOOT_ROWS do
        frame.rows[index] = self:CreateLootRow(frame, index)
        frame.rows[index]:Hide()
    end

    self.lootWindow = frame
    applyPosition(frame, LOOT_WINDOW_POSITION_KEY, DEFAULT_LOOT_WINDOW_POSITION)
    self:ApplyVisualStyle()
    return frame
end

function Runtime:AnchorLootWindow()
    local frame = self:EnsureLootWindow()
    if not frame then
        return
    end

    applyPosition(frame, LOOT_WINDOW_POSITION_KEY, DEFAULT_LOOT_WINDOW_POSITION)
end

function Runtime:BuildLootSlotData(slotIndex)
    if not GetLootSlotInfo or not GetLootSlotLink or not GetLootSlotType then
        return nil
    end

    local icon, name, quantity, currencyID, quality, locked, isQuestItem, questID, isActive, isCoin = GetLootSlotInfo(slotIndex)
    local link = GetLootSlotLink(slotIndex)
    local slotType = GetLootSlotType(slotIndex) or 0

    if not name and link then
        name = link
    end

    if not icon and link then
        local _, itemName, itemIcon, itemQuality = getItemInfoFromLink(link)
        name = name or itemName
        icon = itemIcon
        quality = quality or itemQuality
    end

    if not name and not link and not isCoin then
        return nil
    end

    return {
        slotIndex = slotIndex,
        slotType = slotType,
        icon = icon,
        name = name or link or "",
        text = link or name or "",
        quantity = tonumber(quantity) or 1,
        quality = quality or 1,
        locked = locked == true,
        currencyID = currencyID,
        isQuestItem = isQuestItem,
        questID = questID,
        isActive = isActive,
        isCoin = isCoin,
        tooltipLink = link,
    }
end

function Runtime:RefreshLootWindow()
    self.refreshQueued = false
    local isTestMode = self.testMode == true
    if isTestMode and not useSimpleLootWindow() then
        if self.lootWindow then
            self.lootWindow:Hide()
        end
        return
    end
    if not isTestMode and (not self.customLootActive or not useSimpleLootWindow()) then
        self:HideLootWindow()
        return
    end

    local frame = self:EnsureLootWindow()
    if not frame then
        return
    end

    local rows = {}
    if isTestMode then
        for _, data in ipairs(self.testLootRows or {}) do
            rows[#rows + 1] = data
        end
    elseif GetNumLootItems then
        local numLootItems = GetNumLootItems()
        for slotIndex = 1, numLootItems do
            local hasItem = true
            if LootSlotHasItem then
                local ok, result = pcall(LootSlotHasItem, slotIndex)
                hasItem = (not ok) or result == true
            end
            local data = self:BuildLootSlotData(slotIndex)
            if data and (hasItem or data.isCoin or data.currencyID) then
                rows[#rows + 1] = data
            end
        end
    end

    if #rows == 0 then
        self:HideLootWindow()
        return
    end

    self:AnchorLootWindow()
    for index = 1, MAX_LOOT_ROWS do
        local row = frame.rows[index]
        local data = rows[index]
        if row and data then
            row.data = data
            row.slotIndex = data.slotIndex
            row.icon:SetTexture(data.icon or 134400)
            row.count:SetText(data.quantity and data.quantity > 1 and ("+" .. data.quantity) or "")
            local r, g, b = getQualityColor(data.quality)
            row.text:SetText(data.text or data.name or "")
            row.text:SetTextColor(r, g, b, 1)
            row:Show()
        elseif row then
            row.data = nil
            row.slotIndex = nil
            row:Hide()
        end
    end

    local visibleRows = math.min(#rows, MAX_LOOT_ROWS)
    self:ApplyVisualStyle()
    frame:SetHeight((useCleanStyle() and 8 or 42) + (visibleRows * 31))
    frame:Show()
    if not isTestMode then
        self:HideNativeLootFrame()
    end
end

function Runtime:QueueLootWindowRefresh(delay)
    if self.refreshQueued then
        return
    end
    self.refreshQueued = true

    local function refresh()
        Runtime:RefreshLootWindow()
    end

    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(tonumber(delay) or 0, refresh)
    else
        refresh()
    end
end

function Runtime:HideLootWindow()
    if self.testMode == true then
        self.testMode = false
        self.testLootRows = nil
    end
    self.customLootActive = false
    self.refreshQueued = false
    if self.lootWindow then
        self.lootWindow:Hide()
    end
    self:UpdateFrameInteraction()
end

function Runtime:TakeAllLoot()
    if not GetNumLootItems or not LootSlot then
        return
    end

    local gatheringRuntime = ns and ns.GatheringNodesRuntime
    if type(gatheringRuntime) == "table" and type(gatheringRuntime.ScanLootWindow) == "function" then
        pcall(gatheringRuntime.ScanLootWindow, gatheringRuntime)
    end

    for slotIndex = 1, GetNumLootItems() do
        pcall(LootSlot, slotIndex)
    end
    self:QueueLootWindowRefresh(0.08)
end

function Runtime:OnLootOpened()
    if not useSimpleLootWindow() then
        self.customLootActive = false
        return
    end

    self.customLootActive = false
    self:InstallNativeLootHook()
    self:HideNativeLootFrame()
    self:TakeAllLoot()
end

function Runtime:CreateToastRow(parent, index)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(TOAST_WIDTH, TOAST_HEIGHT)
    row:SetPoint("TOP", parent, "TOP", 0, -((index - 1) * (TOAST_HEIGHT + 4)))
    row:EnableMouse(true)

    row.bg = row:CreateTexture(nil, "BACKGROUND")
    row.bg:SetAllPoints(row)
    row.bg:SetColorTexture(0.015, 0.018, 0.022, 0.68)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("LEFT", row, "LEFT", 4, 0)
    row.icon:SetSize(24, 24)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    row.count = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    row.count:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
    row.count:SetWidth(38)
    row.count:SetJustifyH("LEFT")

    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.text:SetPoint("LEFT", row.count, "RIGHT", 0, 0)
    row.text:SetPoint("RIGHT", row, "RIGHT", -8, 0)
    row.text:SetJustifyH("LEFT")
    row.text:SetWordWrap(false)

    row:SetScript("OnEnter", function(selfRow)
        showTooltip(selfRow, selfRow.data)
    end)
    row:SetScript("OnLeave", hideTooltip)
    row:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" and Runtime.testMode == true then
            parent:StartMoving()
        elseif button == "RightButton" and Runtime.testMode == true then
            Runtime:ToggleMenu()
        end
    end)
    row:SetScript("OnMouseUp", function(_, button)
        if button == "LeftButton" and Runtime.testMode == true then
            parent:StopMovingOrSizing()
            storePosition(parent, TOAST_POSITION_KEY)
        end
    end)
    row:Hide()
    return row
end

function Runtime:EnsureToastFrame()
    if self.toastFrame or type(CreateFrame) ~= "function" or not UIParent then
        return self.toastFrame
    end

    local frame = CreateFrame("Frame", "VaultloomQuietLootToasts", UIParent)
    frame:SetSize(TOAST_WIDTH, (TOAST_HEIGHT + 4) * MAX_TOAST_ROWS)
    frame:SetFrameStrata("HIGH")
    makeFrameDraggable(frame, TOAST_POSITION_KEY)
    frame:SetScript("OnMouseUp", function(_, button)
        if button == "RightButton" and Runtime.testMode == true then
            Runtime:ToggleMenu()
        end
    end)
    frame:Hide()

    for index = 1, MAX_TOAST_ROWS do
        self.toastRows[index] = self:CreateToastRow(frame, index)
    end

    self.toastFrame = frame
    applyPosition(frame, TOAST_POSITION_KEY, DEFAULT_TOAST_POSITION)
    self:ApplyVisualStyle()
    return frame
end

function Runtime:RefreshToasts()
    local frame = self:EnsureToastFrame()
    if not frame then
        return
    end

    local now = getNow()
    for index = #self.toastQueue, 1, -1 do
        local data = self.toastQueue[index]
        if type(data) ~= "table" or (data.expiresAt or 0) <= now then
            table.remove(self.toastQueue, index)
        end
    end

    local visibleRows = (self.testMode == true and not useRewardLoot()) and 0 or math.min(#self.toastQueue, MAX_TOAST_ROWS)
    for index = 1, MAX_TOAST_ROWS do
        local row = self.toastRows[index]
        local data = self.toastQueue[index]
        if row and index <= visibleRows and data then
            row.data = data
            row.icon:SetTexture(data.icon or 134400)
            row.count:SetText(data.quantity and data.quantity > 1 and ("+" .. data.quantity) or "")
            local r, g, b = getQualityColor(data.quality)
            row.text:SetText(data.text or data.fallbackName or "")
            row.text:SetTextColor(r, g, b, 1)
            row:Show()
        elseif row then
            row.data = nil
            row:Hide()
        end
    end

    if visibleRows > 0 then
        applyPosition(frame, TOAST_POSITION_KEY, DEFAULT_TOAST_POSITION)
        self:ApplyVisualStyle()
        frame:Show()
    else
        frame:Hide()
    end
end

local function addToastDedupeKey(keys, key)
    if type(key) ~= "string" or key == "" then
        return
    end
    for _, existingKey in ipairs(keys) do
        if existingKey == key then
            return
        end
    end
    keys[#keys + 1] = key
end

local function getToastDedupeKeys(data)
    local keys = {}
    if type(data) ~= "table" then
        return keys
    end

    addToastDedupeKey(keys, data.dedupeKey)
    addToastDedupeKey(keys, data.key)
    if type(data.dedupeName) == "string" and data.dedupeName ~= "" then
        addToastDedupeKey(keys, "name:" .. data.dedupeName)
    end
    return keys
end

local function toastKeysOverlap(firstKeys, data)
    if type(firstKeys) ~= "table" or type(data) ~= "table" then
        return false
    end

    local secondKeys = data.dedupeKeys
    if type(secondKeys) ~= "table" then
        secondKeys = getToastDedupeKeys(data)
        data.dedupeKeys = secondKeys
    end

    for _, firstKey in ipairs(firstKeys) do
        for _, secondKey in ipairs(secondKeys) do
            if firstKey == secondKey then
                return true
            end
        end
    end
    return false
end

function Runtime:AddToast(data, isBoss)
    if self.enabled ~= true or type(data) ~= "table" then
        return
    end

    local now = getNow()
    local dedupeKeys = getToastDedupeKeys(data)
    if #dedupeKeys == 0 then
        return
    end
    data.dedupeKeys = dedupeKeys

    for _, key in ipairs(dedupeKeys) do
        if self.recentMessages[key] and now - self.recentMessages[key] < TOAST_DEDUPE_SECONDS then
            return
        end
    end
    for _, queuedToast in ipairs(self.toastQueue or {}) do
        if type(queuedToast) == "table" and (tonumber(queuedToast.expiresAt) or 0) > now and toastKeysOverlap(dedupeKeys, queuedToast) then
            return
        end
    end
    for _, key in ipairs(dedupeKeys) do
        self.recentMessages[key] = now
    end

    for messageKey, timestamp in pairs(self.recentMessages) do
        if now - timestamp > TOAST_DEDUPE_PRUNE_SECONDS then
            self.recentMessages[messageKey] = nil
        end
    end

    data.expiresAt = now + TOAST_DURATION
    if isBoss and (not data.quality or data.quality < 3) then
        data.quality = 3
    end

    table.insert(self.toastQueue, 1, data)
    while #self.toastQueue > MAX_TOAST_ROWS do
        table.remove(self.toastQueue)
    end

    self:RefreshToasts()
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(TOAST_DURATION + 0.1, function()
            Runtime:RefreshToasts()
        end)
    end
end

function Runtime:AddToastFromChat(message, isCurrency)
    if not useSimpleLootWindow() or self.customLootActive then
        return
    end

    local quantity = extractQuantity(message, 1)
    if isCurrency then
        local currencyLink = extractCurrencyLink(message)
        self:AddToast(buildCurrencyToastData(currencyLink, quantity), false)
    else
        local itemLink = extractItemLink(message)
        self:AddToast(buildItemToastData(itemLink, quantity), false)
    end
end

function Runtime:AddToastFromLootToast(...)
    if not useRewardLoot() then
        return
    end

    local typeIdentifier, itemLink, quantity = ...
    if typeIdentifier == "item" and type(itemLink) == "string" then
        self:AddToast(buildItemToastData(itemLink, tonumber(quantity) or 1), false)
    elseif typeIdentifier == "currency" and type(itemLink) == "string" then
        self:AddToast(buildCurrencyToastData(itemLink, tonumber(quantity) or 1), false)
    end
end

function Runtime:AddToastFromRoll(...)
    if not useRewardLoot() then
        return
    end
    for index = 1, select("#", ...) do
        local value = select(index, ...)
        if type(value) == "string" and value:find("|Hitem:", 1, true) then
            local quantity = select(index + 1, ...)
            self:AddToast(buildItemToastData(value, tonumber(quantity) or 1), false)
            return
        end
    end
end

function Runtime:HandleBossLoot(encounterID, itemID, itemLink, quantity, playerName)
    if self.enabled ~= true then
        return false
    end

    if useRewardLoot() and itemLink and playerMatches(playerName) then
        local data = buildItemToastData(itemLink, tonumber(quantity) or 1)
        if data then
            data.sourceKey = "boss:" .. tostring(encounterID or "") .. ":" .. tostring(itemID or itemLink)
            self:AddToast(data, true)
        end
    end

    return true
end

function Runtime:InstallBossBannerHook()
    if self.bossHooked or not BossBanner or type(BossBanner.SetScript) ~= "function" then
        return
    end

    self.originalBossBannerOnEvent = BossBanner:GetScript("OnEvent") or BossBanner_OnEvent
    self.bossHookFunc = function(frame, eventName, ...)
        if Runtime.enabled == true and eventName == "ENCOUNTER_LOOT_RECEIVED" and Runtime:HandleBossLoot(...) then
            return
        end
        if Runtime.originalBossBannerOnEvent then
            return Runtime.originalBossBannerOnEvent(frame, eventName, ...)
        end
    end

    pcall(BossBanner.SetScript, BossBanner, "OnEvent", self.bossHookFunc)
    self.bossHooked = true
end

function Runtime:RestoreBossBannerHook()
    if not self.bossHooked or not BossBanner or type(BossBanner.SetScript) ~= "function" then
        self.bossHooked = false
        return
    end

    if type(BossBanner.GetScript) == "function" and BossBanner:GetScript("OnEvent") == self.bossHookFunc then
        pcall(BossBanner.SetScript, BossBanner, "OnEvent", self.originalBossBannerOnEvent)
    end

    self.bossHooked = false
    self.bossHookFunc = nil
    self.originalBossBannerOnEvent = nil
end

function Runtime:CreateMenuButton(parent, label, order, onClick)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(196, 24)
    button:SetPoint("TOP", parent, "TOP", 0, -36 - ((order - 1) * 29))

    button.bg = button:CreateTexture(nil, "BACKGROUND")
    button.bg:SetAllPoints(button)
    button.bg:SetColorTexture(0.08, 0.10, 0.12, 0.92)

    button.text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    button.text:SetAllPoints(button)
    button.text:SetText(label)

    button:SetScript("OnEnter", function(selfButton)
        selfButton.bg:SetColorTexture(0.14, 0.17, 0.20, 0.96)
    end)
    button:SetScript("OnLeave", function(selfButton)
        selfButton.bg:SetColorTexture(0.08, 0.10, 0.12, 0.92)
    end)
    button:SetScript("OnClick", onClick)
    return button
end

function Runtime:EnsureMenu()
    if self.menuFrame or type(CreateFrame) ~= "function" or not UIParent then
        return self.menuFrame
    end

    local frame = CreateFrame("Frame", "VaultloomQuietLootMenu", UIParent)
    frame:SetSize(226, 344)
    frame:SetFrameStrata("DIALOG")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    makeFrameDraggable(frame, MENU_POSITION_KEY)
    frame:Hide()

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints(frame)
    frame.bg:SetColorTexture(0.015, 0.018, 0.022, 0.92)

    frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -10)
    frame.title:SetText(L.FEATURE_QUIET_LOOT_MENU_TITLE or "Quiet Loot")

    frame.close = CreateFrame("Button", nil, frame)
    frame.close:SetSize(18, 18)
    frame.close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -8, -8)
    frame.close.text = frame.close:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    frame.close.text:SetAllPoints(frame.close)
    frame.close.text:SetText("x")
    frame.close:SetScript("OnClick", function()
        frame:Hide()
    end)

    self:CreateMenuButton(frame, L.FEATURE_QUIET_LOOT_MENU_TEST or "Toggle Test Mode", 1, function()
        Runtime:ToggleTestMode()
    end)
    frame.lootWindowButton = self:CreateMenuButton(frame, "", 2, function()
        Runtime:ToggleLootWindowSetting()
    end)
    frame.rewardButton = self:CreateMenuButton(frame, "", 3, function()
        Runtime:ToggleRewardLootSetting()
    end)
    frame.styleButton = self:CreateMenuButton(frame, "", 4, function()
        Runtime:ToggleVisualStyle()
    end)
    frame.directionButton = self:CreateMenuButton(frame, "", 5, function()
        Runtime:ToggleGrowthDirection()
    end)
    frame.lootScaleButton = self:CreateMenuButton(frame, "", 6, function()
        Runtime:CycleLootScale()
    end)
    frame.toastScaleButton = self:CreateMenuButton(frame, "", 7, function()
        Runtime:CycleToastScale()
    end)
    self:CreateMenuButton(frame, L.FEATURE_QUIET_LOOT_MENU_RESET_LOOT or "Reset Loot Window", 8, function()
        Runtime:ResetLootWindowPosition()
    end)
    self:CreateMenuButton(frame, L.FEATURE_QUIET_LOOT_MENU_RESET_TOASTS or "Reset Toast Position", 9, function()
        Runtime:ResetToastPosition()
    end)
    self:CreateMenuButton(frame, L.FEATURE_QUIET_LOOT_MENU_HIDE_TEST or "Hide Test", 10, function()
        Runtime:HideTestMode()
    end)

    self.menuFrame = frame
    applyPosition(frame, MENU_POSITION_KEY, DEFAULT_MENU_POSITION)
    self:UpdateMenu()
    return frame
end

function Runtime:UpdateMenu()
    local frame = self.menuFrame
    if not frame then
        return
    end

    local onLabel = L.FEATURE_QUIET_LOOT_SETTING_ON or "On"
    local offLabel = L.FEATURE_QUIET_LOOT_SETTING_OFF or "Off"

    if frame.lootWindowButton and frame.lootWindowButton.text then
        frame.lootWindowButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_LOOT_WINDOW_FORMAT or "Loot Window: %s",
            useSimpleLootWindow() and onLabel or offLabel
        ))
    end

    if frame.rewardButton and frame.rewardButton.text then
        frame.rewardButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_REWARD_FORMAT or "Boss/Chests: %s",
            useRewardLoot() and onLabel or offLabel
        ))
    end

    if frame.styleButton and frame.styleButton.text then
        local styleLabel = useCleanStyle()
            and (L.FEATURE_QUIET_LOOT_STYLE_CLEAN or "Clean")
            or (L.FEATURE_QUIET_LOOT_STYLE_NORMAL or "Normal")
        frame.styleButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_STYLE_FORMAT or "Style: %s",
            styleLabel
        ))
    end

    if frame.directionButton and frame.directionButton.text then
        local directionLabel = growUp()
            and (L.FEATURE_QUIET_LOOT_DIRECTION_UP or "Up")
            or (L.FEATURE_QUIET_LOOT_DIRECTION_DOWN or "Down")
        frame.directionButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_DIRECTION_FORMAT or "Direction: %s",
            directionLabel
        ))
    end

    if frame.lootScaleButton and frame.lootScaleButton.text then
        frame.lootScaleButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_LOOT_SIZE_FORMAT or "Loot Size: %d%%",
            getScalePercent(LOOT_SCALE_KEY)
        ))
    end

    if frame.toastScaleButton and frame.toastScaleButton.text then
        frame.toastScaleButton.text:SetText(string.format(
            L.FEATURE_QUIET_LOOT_MENU_TOAST_SIZE_FORMAT or "Popup Size: %d%%",
            getScalePercent(TOAST_SCALE_KEY)
        ))
    end
end

function Runtime:ToggleMenu()
    local frame = self:EnsureMenu()
    if not frame then
        return
    end
    if frame:IsShown() then
        frame:Hide()
    else
        applyPosition(frame, MENU_POSITION_KEY, DEFAULT_MENU_POSITION)
        self:UpdateMenu()
        frame:Show()
    end
end

function Runtime:ToggleVisualStyle()
    local nextStyle = useCleanStyle() and STYLE_NORMAL or STYLE_CLEAN
    setVisualStyle(nextStyle)
    self:ApplyVisualStyle()
    self:UpdateMenu()
    if self.testMode == true then
        self:RefreshLootWindow()
        self:RefreshToasts()
    end
end

function Runtime:RefreshTestFrames()
    if self.testMode == true then
        self:RefreshLootWindow()
        self:RefreshToasts()
    else
        self:ApplyVisualStyle()
    end
end

function Runtime:ToggleLootWindowSetting()
    cycleBoolSetting(LOOT_WINDOW_ENABLED_KEY, true)
    if not useSimpleLootWindow() then
        self.customLootActive = false
        if self.lootWindow then
            self.lootWindow:Hide()
        end
    end
    self:UpdateMenu()
    self:RefreshTestFrames()
end

function Runtime:ToggleRewardLootSetting()
    cycleBoolSetting(REWARD_LOOT_ENABLED_KEY, true)
    if not useRewardLoot() then
        wipe(self.toastQueue)
    elseif self.testMode == true then
        self.toastQueue = self:BuildTestToastRows()
    end
    self:UpdateMenu()
    self:RefreshTestFrames()
end

function Runtime:ToggleGrowthDirection()
    setGrowthDirection(growUp() and DIRECTION_DOWN or DIRECTION_UP)
    self:UpdateMenu()
    self:RefreshTestFrames()
end

function Runtime:CycleLootScale()
    cycleScaleSetting(LOOT_SCALE_KEY)
    self:UpdateMenu()
    self:RefreshTestFrames()
end

function Runtime:CycleToastScale()
    cycleScaleSetting(TOAST_SCALE_KEY)
    self:UpdateMenu()
    self:RefreshTestFrames()
end

function Runtime:BuildTestLootRows()
    return {
        {
            icon = 134566,
            text = "|cff1eff00|Hitem:2589:::::::::::::|h[Simple Loot Test Item]|h|r",
            name = "Simple Loot Test Item",
            quantity = 7,
            quality = 2,
            tooltipLink = "|Hitem:2589:::::::::::::|h[Simple Loot Test Item]|h",
        },
        {
            icon = 133217,
            text = "|cff0070dd|Hitem:1529:::::::::::::|h[Compact Gem Sample]|h|r",
            name = "Compact Gem Sample",
            quantity = 1,
            quality = 3,
            tooltipLink = "|Hitem:1529:::::::::::::|h[Compact Gem Sample]|h",
        },
        {
            icon = 136243,
            text = "|cffa335ee|Hitem:19019:::::::::::::|h[Boss Loot Sample]|h|r",
            name = "Boss Loot Sample",
            quantity = 1,
            quality = 4,
            tooltipLink = "|Hitem:19019:::::::::::::|h[Boss Loot Sample]|h",
        },
    }
end

function Runtime:BuildTestToastRows()
    local now = getNow()
    return {
        {
            key = "test-toast-1",
            icon = 134566,
            text = "|cff1eff00|Hitem:2589:::::::::::::|h[Simple Loot Test Item]|h|r",
            quantity = 7,
            quality = 2,
            tooltipLink = "|Hitem:2589:::::::::::::|h[Simple Loot Test Item]|h",
            expiresAt = now + 86400,
        },
        {
            key = "test-toast-2",
            icon = 133217,
            text = "|cff0070dd|Hitem:1529:::::::::::::|h[Compact Gem Sample]|h|r",
            quantity = 1,
            quality = 3,
            tooltipLink = "|Hitem:1529:::::::::::::|h[Compact Gem Sample]|h",
            expiresAt = now + 86400,
        },
    }
end

function Runtime:ShowTestMode()
    self.testMode = true
    self.customLootActive = false
    self.testLootRows = self:BuildTestLootRows()
    self.toastQueue = useRewardLoot() and self:BuildTestToastRows() or {}
    self:RefreshLootWindow()
    self:RefreshToasts()
    if ns.Print then
        ns:Print(L.FEATURE_QUIET_LOOT_TEST_HINT or "Quiet Loot test mode: drag the loot window or toast rows to save their positions. Right-click opens this menu.")
    end
end

function Runtime:HideTestMode()
    self.testMode = false
    self.testLootRows = nil
    wipe(self.toastQueue)
    self:UpdateFrameInteraction()
    self:RefreshToasts()
    if self.lootWindow then
        self.lootWindow:Hide()
    end
end

function Runtime:ToggleTestMode()
    if self.testMode == true then
        self:HideTestMode()
    else
        self:ShowTestMode()
    end
end

function Runtime:ResetLootWindowPosition()
    clearPosition(LOOT_WINDOW_POSITION_KEY)
    if self.lootWindow then
        applyPosition(self.lootWindow, LOOT_WINDOW_POSITION_KEY, DEFAULT_LOOT_WINDOW_POSITION)
    end
    if self.testMode == true then
        self:RefreshLootWindow()
    end
end

function Runtime:ResetToastPosition()
    clearPosition(TOAST_POSITION_KEY)
    if self.toastFrame then
        applyPosition(self.toastFrame, TOAST_POSITION_KEY, DEFAULT_TOAST_POSITION)
    end
    if self.testMode == true then
        self:RefreshToasts()
    end
end

function Runtime:HandleSlash(message)
    local command = string.lower(tostring(message or ""):match("^%s*(%S*)") or "")
    if command == "test" or command == "testen" then
        self:ToggleTestMode()
    elseif command == "reset" then
        self:ResetLootWindowPosition()
        self:ResetToastPosition()
    elseif command == "hide" or command == "aus" then
        self:HideTestMode()
        if self.menuFrame then
            self.menuFrame:Hide()
        end
    else
        self:ToggleMenu()
    end
end

function ns:HandleQuietLootSlash(message)
    Runtime:HandleSlash(message)
end

function Runtime:HandleEvent(eventName, ...)
    if eventName == "PLAYER_ENTERING_WORLD" then
        self:InstallNativeLootHook()
        self:InstallBossBannerHook()
        self:UpdateAlertFrameMute()
        return
    end

    if eventName == "LOOT_OPENED" then
        self:OnLootOpened(...)
        return
    end

    if eventName == "LOOT_CLOSED" then
        self:HideLootWindow()
        return
    end

    if eventName == "LOOT_SLOT_CHANGED" or eventName == "LOOT_SLOT_CLEARED" then
        if self.customLootActive then
            self:QueueLootWindowRefresh(0.03)
        end
        return
    end

    if eventName == "CHAT_MSG_LOOT" then
        self:AddToastFromChat((...), false)
        return
    end

    if eventName == "CHAT_MSG_CURRENCY" then
        self:AddToastFromChat((...), true)
        return
    end

    if eventName == "SHOW_LOOT_TOAST" or eventName == "SHOW_LOOT_TOAST_UPGRADE" or eventName == "SHOW_LOOT_TOAST_LEGENDARY_LOOTED" then
        self:AddToastFromLootToast(...)
        return
    end

    if eventName == "LOOT_ITEM_ROLL_WON" or eventName == "BONUS_ROLL_RESULT" then
        self:AddToastFromRoll(...)
        return
    end

    if eventName == "ENCOUNTER_LOOT_RECEIVED" then
        self:HandleBossLoot(...)
    end
end

function Runtime:SetEnabled(enabled)
    self.enabled = enabled == true
    if self.enabled then
        self:RegisterEvents()
        self:InstallNativeLootHook()
        self:InstallBossBannerHook()
        self:UpdateAlertFrameMute()
    else
        self:HideLootWindow()
        self:UnregisterEvents()
        self:UpdateAlertFrameMute()
        self:RestoreBossBannerHook()
        wipe(self.toastQueue)
        wipe(self.recentMessages)
        self:RefreshToasts()
    end
end

function Runtime:RefreshOptions()
    self:UpdateAlertFrameMute()
    self:ApplyVisualStyle()
    self:UpdateMenu()
    if self.enabled then
        self:InstallBossBannerHook()
    end
    if self.customLootActive and not useSimpleLootWindow() then
        self:HideLootWindow()
    elseif self.customLootActive then
        self:QueueLootWindowRefresh(0)
    end
end

ns.QuietLootRuntime = Runtime

ns:RegisterFeatureDefinition(FEATURE_ID, {
    id = FEATURE_ID,
    order = 35,
    category = "blizzard",
    title = function() return L.FEATURE_QUIET_LOOT_TITLE or "Quiet Loot" end,
    description = function() return L.FEATURE_QUIET_LOOT_DESCRIPTION or "Replaces the bulky loot window and loot popups with compact icon-and-text rows." end,
    note = function() return L.FEATURE_QUIET_LOOT_NOTE or "Simple mode uses a small loot window for manual looting and compact rows for automatic loot." end,
    previewLabel = function() return L.FEATURE_QUIET_LOOT_PREVIEW_LABEL or "Loot UI" end,
    previewNote = function() return L.FEATURE_QUIET_LOOT_PREVIEW_NOTE or "Small item icons, stack counts, and item text without the Blizzard reward stack." end,
    requiresReload = false,
    apply = function(isEnabled)
        Runtime:SetEnabled(isEnabled == true)
    end,
    onOptionChanged = function()
        Runtime:RefreshOptions()
    end,
})
