local ADDON_NAME = ...

local frame = CreateFrame("Frame")
local scanner = CreateFrame("GameTooltip", "EatMeScannerTooltip", UIParent, "GameTooltipTemplate")
scanner:SetOwner(UIParent, "ANCHOR_NONE")

local UPDATE_DELAY = 0.5
local DEFAULT_ICON_ITEM = 113509 -- Conjured Mana Bun

local MACROS = {
    food = "Eat Me Food",
    drink = "Eat Me Drink",
    buff = "Eat Me Buff",
    flask = "Eat Me Flask",
}

local OBSOLETE_MACROS = {
    "Eat Me",
    "Eat Me Health",
    "Eat Me Mana",
}

local itemCache = {}
local debugCache = {}
local debugEntries = {}
local bagItems = {}
local updatePending = false
local bagScanNeeded = true
local elapsedSinceRequest = 0
local lastSelected = {}
local currentBuffFood
local buffReminder


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

    -- Midnight can mark aura text as a secret value. Secret strings cannot be
    -- indexed with string methods such as :lower() or :find().
    if canaccessvalue and not canaccessvalue(value) then
        return nil
    end

    if type(value) ~= "string" then return nil end
    return value:lower()
end

local function AuraTooltipContainsWellFed(index)
    if not C_TooltipInfo or not C_TooltipInfo.GetUnitBuff then return false end

    local ok, tooltipData = pcall(C_TooltipInfo.GetUnitBuff, "player", index)
    if not ok or not tooltipData or not tooltipData.lines then return false end

    for _, line in ipairs(tooltipData.lines) do
        local left = GetReadableLowerText(line.leftText)
        local right = GetReadableLowerText(line.rightText)
        if (left and left:find("well fed", 1, true))
            or (right and right:find("well fed", 1, true)) then
            return true
        end
    end

    return false
end

local function PlayerHasFoodBuff()
    if C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
        for index = 1, 40 do
            local aura = C_UnitAuras.GetAuraDataByIndex("player", index, "HELPFUL")
            if not aura then break end

            local name = GetReadableLowerText(aura.name)
            if (name and name:find("well fed", 1, true)) or AuraTooltipContainsWellFed(index) then
                return true
            end
        end
    elseif UnitBuff then
        for index = 1, 40 do
            local name = UnitBuff("player", index)
            if not name then break end

            local readableName = GetReadableLowerText(name)
            if readableName and readableName:find("well fed", 1, true) then return true end
        end
    end

    return false
end

local function CreateBuffReminder()
    if buffReminder then return end

    buffReminder = CreateFrame("Frame", "EatMeBuffReminder", UIParent, "BackdropTemplate")
    buffReminder:SetSize(310, 66)
    buffReminder:SetPoint("TOP", UIParent, "TOP", 0, -145)
    buffReminder:SetFrameStrata("DIALOG")
    buffReminder:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 14,
        insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    buffReminder:SetBackdropColor(0.04, 0.04, 0.04, 0.92)
    buffReminder:SetBackdropBorderColor(1, 0.72, 0.12, 0.95)

    local icon = buffReminder:CreateTexture(nil, "ARTWORK")
    icon:SetSize(48, 48)
    icon:SetPoint("LEFT", 10, 0)
    icon:SetTexture("Interface\\AddOns\\Eat-Me\\Media\\Minimap")

    local title = buffReminder:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -3)
    title:SetText("Food buff missing")
    title:SetTextColor(1, 0.82, 0.15)

    local itemText = buffReminder:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    itemText:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
    itemText:SetPoint("RIGHT", buffReminder, "RIGHT", -10, 0)
    itemText:SetJustifyH("LEFT")
    buffReminder.itemText = itemText

    buffReminder:Hide()
end

local function UpdateBuffReminder()
    CreateBuffReminder()

    if not EatMeDB or EatMeDB.notificationsEnabled == false then
        buffReminder:Hide()
        return
    end

    if InCombatLockdown() or not currentBuffFood or PlayerHasFoodBuff() then
        buffReminder:Hide()
        return
    end

    buffReminder.itemText:SetText(currentBuffFood.name or "Buff food available")
    buffReminder:Show()
end

local function Print(message)
    print("|cff66ccffEat Me|r: " .. message)
end

local function GetItemDisplay(itemID, fallbackName)
    local _, itemLink = GetItemInfo(itemID)
    return itemLink or fallbackName or ("item:" .. tostring(itemID))
end

local function ParseNumber(text)
    if not text then return 0 end
    local value = text:match("([%d,]+)")
    if not value then return 0 end
    return tonumber((value:gsub(",", ""))) or 0
end

local function IsConjured(nameLower, tooltipText)
    return tooltipText:find("conjured", 1, true)
        or nameLower:find("mana bun", 1, true)
        or nameLower:find("mana pudding", 1, true)
        or nameLower:find("mana strudel", 1, true)
        or nameLower:find("mana biscuit", 1, true)
        or nameLower:find("conjured", 1, true)
end

local function GetPrimaryStatName()
    local _, classFile = UnitClass("player")
    local spec = GetSpecialization()

    if classFile == "MAGE" or classFile == "PRIEST" or classFile == "WARLOCK" or classFile == "EVOKER" then
        return "intellect"
    elseif classFile == "ROGUE" or classFile == "HUNTER" or classFile == "DEMONHUNTER" then
        return "agility"
    elseif classFile == "DEATHKNIGHT" or classFile == "WARRIOR" then
        return "strength"
    elseif classFile == "PALADIN" then
        return spec == 1 and "intellect" or "strength"
    elseif classFile == "SHAMAN" then
        return spec == 2 and "agility" or "intellect"
    elseif classFile == "DRUID" then
        return (spec == 2 or spec == 3) and "agility" or "intellect"
    elseif classFile == "MONK" then
        return spec == 2 and "intellect" or "agility"
    end

    return nil
end

local MANA_CLASSES = {
    DRUID = true, EVOKER = true, MAGE = true, MONK = true,
    PALADIN = true, PRIEST = true, SHAMAN = true, WARLOCK = true,
}

local function PlayerUsesMana()
    local _, classFile = UnitClass("player")
    return MANA_CLASSES[classFile] == true
end

local FOOD_DRINK_SUBCLASS = (Enum and Enum.ItemConsumableSubclass and Enum.ItemConsumableSubclass.Fooddrink) or 5

local DRINK_NAME_HINTS = {
    "water", "drink", "tea", "juice", "milk", "coffee", "cider",
    "nectar", "beverage", "ale", "brew", "punch", "smoothie",
}

local function NameLooksLikeDrink(nameLower)
    for _, hint in ipairs(DRINK_NAME_HINTS) do
        if nameLower:find(hint, 1, true) then return true end
    end
    return false
end

local function SetDebug(itemID, name, classification, reason, report)
    debugCache[itemID] = {
        id = itemID,
        name = name or ("item:" .. tostring(itemID)),
        classification = classification,
        reason = reason,
        report = report == true,
    }
end

local function ScanItem(itemID)
    if itemCache[itemID] ~= nil then
        return itemCache[itemID] ~= false and itemCache[itemID] or nil
    end

    local name, _, quality, itemLevel, requiredLevel, itemType, itemSubType, _, _, icon, sellPrice, classID, subclassID = GetItemInfo(itemID)
    if not name then return nil, true end
    if classID ~= 0 then
        itemCache[itemID] = false
        SetDebug(itemID, name, "ignored", "not a consumable", false)
        return nil
    end

    local tooltipLines = {}

    -- C_TooltipInfo is more reliable than a hidden GameTooltip on modern Retail.
    -- Fall back to the hidden tooltip for older clients.
    if C_TooltipInfo and C_TooltipInfo.GetHyperlink then
        local ok, tooltipData = pcall(C_TooltipInfo.GetHyperlink, "item:" .. itemID)
        if ok and tooltipData and tooltipData.lines then
            for _, line in ipairs(tooltipData.lines) do
                if line.leftText and line.leftText ~= "" then table.insert(tooltipLines, line.leftText) end
                if line.rightText and line.rightText ~= "" then table.insert(tooltipLines, line.rightText) end
            end
        end
    end

    if #tooltipLines == 0 then
        scanner:ClearLines()
        scanner:SetHyperlink("item:" .. itemID)
        for lineIndex = 1, scanner:NumLines() do
            local left = _G["EatMeScannerTooltipTextLeft" .. lineIndex]
            local right = _G["EatMeScannerTooltipTextRight" .. lineIndex]
            if left and left:GetText() then table.insert(tooltipLines, left:GetText()) end
            if right and right:GetText() then table.insert(tooltipLines, right:GetText()) end
        end
    end

    if #tooltipLines == 0 then return nil, true end

    local allText = ""
    local health, mana = 0, 0
    local percentHealth, percentMana = false, false
    local requiresSitting, wellFed = false, false
    local buffAmount = 0

    for _, rawText in ipairs(tooltipLines) do
        local text = rawText and rawText:lower() or ""
        if text ~= "" then
            allText = allText .. " " .. text

            if text:find("must remain seated", 1, true) or text:find("remain seated", 1, true) then
                requiresSitting = true
            end

            if text:find("well fed", 1, true)
                or (text:find("spend at least 10 seconds eating", 1, true) and text:find("gain", 1, true)) then
                wellFed = true
            end

            local percent = text:match("restores%s+(%d+)%%")
            if percent then
                if text:find("mana", 1, true) then percentMana = true end
                if text:find("health", 1, true) or not text:find("mana", 1, true) then percentHealth = true end
            end

            local restored = ParseNumber(text:match("restores%s+([%d,]+)"))
            local healed = ParseNumber(text:match("heals%s+([%d,]+)"))
            local value = math.max(restored, healed)
            if value > 0 then
                if text:find("mana", 1, true) then mana = math.max(mana, value) end
                if text:find("health", 1, true) or text:find("heals", 1, true) then health = math.max(health, value) end
            end

            if wellFed
                or text:find("strength", 1, true)
                or text:find("agility", 1, true)
                or text:find("intellect", 1, true)
                or text:find("stamina", 1, true)
                or text:find("critical strike", 1, true)
                or text:find("haste", 1, true)
                or text:find("mastery", 1, true)
                or text:find("versatility", 1, true)
                or text:find("highest secondary stat", 1, true) then
                buffAmount = math.max(buffAmount, ParseNumber(text))
            end
        end
    end

    if percentHealth then health = math.max(health, 1000000) end
    if percentMana then mana = math.max(mana, 1000000) end

    local nameLower = name:lower()
    local isConjured = IsConjured(nameLower, allText)

    -- Midnight can hide or change the normal food tooltip wording for freshly
    -- conjured refreshments.  A conjured consumable is always valid recovery
    -- food, and mage refreshments restore both health and mana.
    if isConjured then
        health = math.max(health, 1000000)
        mana = math.max(mana, 1000000)
    end

    local subTypeLower = itemSubType and itemSubType:lower() or ""
    local isFoodDrinkSubclass = subclassID == FOOD_DRINK_SUBCLASS
        or subTypeLower:find("food", 1, true) ~= nil
        or subTypeLower:find("drink", 1, true) ~= nil

    -- Older expansion food can omit or localize the familiar "restores" wording.
    -- Blizzard's Food & Drink subclass is therefore the authoritative fallback.
    if isFoodDrinkSubclass and not wellFed and not isConjured then
        local tooltipMentionsMana = allText:find("mana", 1, true) ~= nil
        local tooltipMentionsHealth = allText:find("health", 1, true) ~= nil
            or allText:find("heals", 1, true) ~= nil

        if tooltipMentionsMana then
            mana = math.max(mana, 1)
        end
        if tooltipMentionsHealth then
            health = math.max(health, 1)
        end

        -- If the tooltip supplies no usable recovery wording, names that clearly
        -- describe beverages become drinks; everything else is ordinary food.
        if health == 0 and mana == 0 then
            if NameLooksLikeDrink(nameLower) then
                mana = 1
            else
                health = 1
            end
        end
    end

    local isPotion = nameLower:find("potion", 1, true) ~= nil
    local nameSaysHealthPotion = isPotion and (nameLower:find("healing potion", 1, true) or nameLower:find("health potion", 1, true))
    local nameSaysManaPotion = isPotion and nameLower:find("mana potion", 1, true)
    local isHealthPotion = isPotion and not requiresSitting and (health > 0 or nameSaysHealthPotion)
    local isManaPotion = isPotion and not requiresSitting and (mana > 0 or nameSaysManaPotion)
    local isFlask = nameLower:find("flask", 1, true) ~= nil or nameLower:find("phial", 1, true) ~= nil
    local isRecovery = (requiresSitting and (health > 0 or mana > 0)) or isConjured or (isFoodDrinkSubclass and not wellFed)

    if not isRecovery and not wellFed and not isHealthPotion and not isManaPotion and not isFlask then
        itemCache[itemID] = false
        SetDebug(itemID, name, "ignored", "consumable type not used by Eat Me", true)
        return nil
    end

    local data = {
        id = itemID,
        name = name,
        nameLower = nameLower,
        icon = icon,
        quality = quality or 0,
        itemLevel = itemLevel or 0,
        requiredLevel = requiredLevel or 0,
        price = sellPrice or 0,
        health = health,
        mana = mana,
        restoresBoth = health > 0 and mana > 0,
        isPercent = percentHealth or percentMana,
        isConjured = isConjured,
        isRecovery = isRecovery,
        isBuffFood = wellFed,
        isHealthPotion = isHealthPotion,
        isManaPotion = isManaPotion,
        potionPower = math.max(health, mana, (itemLevel or 0) * 1000 + (requiredLevel or 0)),
        isFlask = isFlask,
        buffAmount = buffAmount,
        tooltipText = allText,
    }

    local categories = {}
    if data.isConjured then table.insert(categories, "conjured recovery") end
    if data.isBuffFood then table.insert(categories, "buff food") end
    if data.isRecovery and data.health > 0 and not data.isBuffFood then table.insert(categories, "food") end
    if data.isRecovery and data.mana > 0 and not data.isBuffFood then table.insert(categories, "drink") end
    if data.isHealthPotion then table.insert(categories, "healing potion") end
    if data.isManaPotion then table.insert(categories, "mana potion") end
    if data.isFlask then table.insert(categories, "flask/phial") end

    local reason
    if data.isConjured then
        reason = "conjured refreshment"
    elseif data.isBuffFood then
        reason = "tooltip grants Well Fed/stat buff"
    elseif isFoodDrinkSubclass then
        reason = "Blizzard Food & Drink subclass"
    elseif data.isHealthPotion or data.isManaPotion then
        reason = "potion name/tooltip"
    elseif data.isFlask then
        reason = "flask/phial name"
    else
        reason = "recovery tooltip"
    end

    SetDebug(itemID, name, table.concat(categories, ", "), reason, true)
    itemCache[itemID] = data
    return data
end

local function ScanBags()
    wipe(bagItems)
    wipe(debugEntries)
    local debugSeen = {}
    local playerLevel = UnitLevel("player")
    local retryNeeded = false

    for bag = 0, NUM_BAG_SLOTS do
        local slots = C_Container.GetContainerNumSlots(bag)
        for slot = 1, slots do
            local info = C_Container.GetContainerItemInfo(bag, slot)
            if info and info.itemID then
                local data, retry = ScanItem(info.itemID)
                retryNeeded = retryNeeded or retry
                if not debugSeen[info.itemID] then
                    local debugInfo = debugCache[info.itemID]
                    if debugInfo and debugInfo.report then
                        local entry = CopyTable(debugInfo)
                        entry.stackCount = info.stackCount or 1
                        table.insert(debugEntries, entry)
                    end
                    debugSeen[info.itemID] = true
                end
                if data and data.requiredLevel <= playerLevel then
                    local candidate = CopyTable(data)
                    candidate.stackCount = info.stackCount or 1
                    table.insert(bagItems, candidate)
                end
            end
        end
    end

    bagScanNeeded = false
    return retryNeeded
end

local function BetterByPower(candidate, best, valueField)
    if not best then return true end
    local a = candidate[valueField] or 0
    local b = best[valueField] or 0
    if a ~= b then return a > b end
    if candidate.itemLevel ~= best.itemLevel then return candidate.itemLevel > best.itemLevel end
    if candidate.quality ~= best.quality then return candidate.quality > best.quality end
    if candidate.requiredLevel ~= best.requiredLevel then return candidate.requiredLevel > best.requiredLevel end
    return candidate.stackCount < best.stackCount
end

local function FindBestFood()
    local best

    for _, candidate in ipairs(bagItems) do
        if candidate.isRecovery and candidate.health > 0 and not candidate.isBuffFood then
            if not best then
                best = candidate
            elseif candidate.isConjured ~= best.isConjured then
                if candidate.isConjured then best = candidate end
            elseif candidate.isPercent ~= best.isPercent then
                if candidate.isPercent then best = candidate end
            elseif candidate.health ~= best.health then
                if candidate.health > best.health then best = candidate end
            elseif candidate.isBuffFood ~= best.isBuffFood then
                if not candidate.isBuffFood then best = candidate end
            elseif candidate.price < best.price then
                best = candidate
            end
        end
    end

    return best
end

local function FindBestDrink()
    if not PlayerUsesMana() then return nil end

    local best
    for _, candidate in ipairs(bagItems) do
        if candidate.isRecovery and candidate.mana > 0 and not candidate.isBuffFood then
            if not best then
                best = candidate
            elseif candidate.isConjured ~= best.isConjured then
                if candidate.isConjured then best = candidate end
            elseif candidate.isPercent ~= best.isPercent then
                if candidate.isPercent then best = candidate end
            elseif candidate.mana ~= best.mana then
                if candidate.mana > best.mana then best = candidate end
            elseif candidate.isBuffFood ~= best.isBuffFood then
                if not candidate.isBuffFood then best = candidate end
            elseif candidate.price < best.price then
                best = candidate
            end
        end
    end

    return best
end

local function FindBestBuffFood()
    local primary = GetPrimaryStatName()
    local best, bestScore

    for _, candidate in ipairs(bagItems) do
        if candidate.isBuffFood then
            local text = candidate.tooltipText
            local score = candidate.buffAmount

            if text:find("primary stat", 1, true) then score = score + 100000 end
            if primary and text:find(primary, 1, true) then score = score + 90000 end
            if text:find("highest secondary stat", 1, true) then score = score + 80000 end
            if candidate.nameLower:find("feast", 1, true) then score = score + 70000 end
            score = score + candidate.itemLevel * 10 + candidate.quality

            if not best or score > bestScore then
                best, bestScore = candidate, score
            end
        end
    end

    return best
end

local function FindBestHealthPotion()
    local best
    for _, candidate in ipairs(bagItems) do
        if candidate.isHealthPotion and BetterByPower(candidate, best, candidate.health > 0 and "health" or "potionPower") then best = candidate end
    end
    return best
end

local function FindBestManaPotion()
    if not PlayerUsesMana() then return nil end
    local best
    for _, candidate in ipairs(bagItems) do
        if candidate.isManaPotion and BetterByPower(candidate, best, candidate.mana > 0 and "mana" or "potionPower") then best = candidate end
    end
    return best
end

local function FindBestFlask()
    local primary = GetPrimaryStatName()
    local best, bestScore
    for _, candidate in ipairs(bagItems) do
        if candidate.isFlask then
            local score = candidate.itemLevel * 100 + candidate.quality
            if candidate.tooltipText:find("primary stat", 1, true) then score = score + 100000 end
            if primary and candidate.tooltipText:find(primary, 1, true) then score = score + 90000 end
            if not best or score > bestScore then best, bestScore = candidate, score end
        end
    end
    return best
end

local function BuildMacroBody(key, item, combatItem)
    if key == "food" or key == "drink" then
        local lines = { "#showtooltip" }
        if combatItem then
            table.insert(lines, "/use [combat] item:" .. combatItem.id)
        end
        if item then
            table.insert(lines, "/use [nocombat] item:" .. item.id)
        end
        if not item and not combatItem then
            table.insert(lines, "/run print('|cff66ccffEat Me|r: No suitable item found in your bags.')")
        end
        return table.concat(lines, "\n")
    end

    if item then
        return "#showtooltip item:" .. item.id .. "\n/use item:" .. item.id
    end

    return "#showtooltip\n/run print('|cff66ccffEat Me|r: No suitable item found in your bags.')"
end

local function EnsureMacro(key, item, combatItem)
    local macroName = MACROS[key]
    local selectedID = item and item.id or 0
    local combatID = combatItem and combatItem.id or 0
    local selectionKey = tostring(selectedID) .. ":" .. tostring(combatID)
    if lastSelected[key] == selectionKey then return end

    local icon = (item and item.icon) or (combatItem and combatItem.icon) or C_Item.GetItemIconByID(DEFAULT_ICON_ITEM) or 134062
    local body = BuildMacroBody(key, item, combatItem)

    local macroIndex = GetMacroIndexByName(macroName)
    if macroIndex == 0 then
        local created = CreateMacro(macroName, icon, body, false)
        if not created then
            Print("Could not create " .. macroName .. ". Delete a character macro and type /eatme scan.")
            return
        end
    else
        EditMacro(macroIndex, macroName, icon, body)
    end

    lastSelected[key] = selectionKey
end

local function RemoveObsoleteMacros()
    for _, macroName in ipairs(OBSOLETE_MACROS) do
        local macroIndex = GetMacroIndexByName(macroName)
        if macroIndex > 0 then
            DeleteMacro(macroIndex)
        end
    end
end

local function UpdateMacros(force)
    if InCombatLockdown() then return end

    local retryNeeded = false
    if bagScanNeeded then retryNeeded = ScanBags() end

    local choices = {
        food = FindBestFood(),
        drink = FindBestDrink(),
        buff = FindBestBuffFood(),
        flask = FindBestFlask(),
        healthPotion = FindBestHealthPotion(),
        manaPotion = FindBestManaPotion(),
    }

    currentBuffFood = choices.buff

    RemoveObsoleteMacros()
    if force then wipe(lastSelected) end
    EnsureMacro("food", choices.food, choices.healthPotion)
    EnsureMacro("drink", choices.drink, choices.manaPotion)
    EnsureMacro("buff", choices.buff)
    EnsureMacro("flask", choices.flask)
    UpdateBuffReminder()

    if force then
        Print("Macros updated.")
        for _, key in ipairs({"food", "drink", "buff", "flask"}) do
            local item = choices[key]
            Print(MACROS[key] .. ": " .. (item and GetItemDisplay(item.id, item.name) or "none found"))
        end
        Print("Food combat item: " .. (choices.healthPotion and GetItemDisplay(choices.healthPotion.id, choices.healthPotion.name) or "none found"))
        Print("Drink combat item: " .. (choices.manaPotion and GetItemDisplay(choices.manaPotion.id, choices.manaPotion.name) or "none found"))
    end

    if retryNeeded then
        C_Timer.After(1.5, function()
            bagScanNeeded = true
            updatePending = true
            elapsedSinceRequest = 0
        end)
    end
end

local function RequestUpdate(needsBagScan)
    if needsBagScan then bagScanNeeded = true end
    if InCombatLockdown() then return end
    updatePending = true
    elapsedSinceRequest = 0
end

local function GetCurrentChoices()
    if bagScanNeeded then ScanBags() end
    return {
        food = FindBestFood(),
        drink = FindBestDrink(),
        buff = FindBestBuffFood(),
        flask = FindBestFlask(),
        healthPotion = FindBestHealthPotion(),
        manaPotion = FindBestManaPotion(),
    }
end

local function ShowStatus()
    if InCombatLockdown() then
        Print("Status will update after combat.")
        return
    end
    local choices = GetCurrentChoices()
    for _, key in ipairs({"food", "drink", "buff", "flask"}) do
        local item = choices[key]
        Print(MACROS[key] .. ": " .. (item and GetItemDisplay(item.id, item.name) or "none found"))
    end
    Print("Food combat item: " .. (choices.healthPotion and GetItemDisplay(choices.healthPotion.id, choices.healthPotion.name) or "none found"))
    Print("Drink combat item: " .. (choices.manaPotion and GetItemDisplay(choices.manaPotion.id, choices.manaPotion.name) or "none found"))
end

local function ShowDebug()
    if InCombatLockdown() then
        Print("Debug scan queued until combat ends.")
        bagScanNeeded = true
        return
    end

    wipe(itemCache)
    wipe(debugCache)
    bagScanNeeded = true
    ScanBags()

    Print("Debug classification report:")
    if #debugEntries == 0 then
        Print("No consumables found in equipped bags.")
        return
    end

    table.sort(debugEntries, function(a, b) return a.name < b.name end)
    for _, entry in ipairs(debugEntries) do
        Print(string.format("%s x%d -> %s (%s)",
            GetItemDisplay(entry.id, entry.name),
            entry.stackCount or 1,
            entry.classification or "unknown",
            entry.reason or "no reason"))
    end
end

local function ToggleNotifications()
    EatMeDB = EatMeDB or {}
    if EatMeDB.notificationsEnabled == nil then
        EatMeDB.notificationsEnabled = true
    end

    EatMeDB.notificationsEnabled = not EatMeDB.notificationsEnabled
    if EatMeDB.notificationsEnabled then
        Print("Food buff reminders shown.")
        UpdateBuffReminder()
    else
        if buffReminder then buffReminder:Hide() end
        Print("Food buff reminders hidden.")
    end
end

local function ForceScan()
    if InCombatLockdown() then
        bagScanNeeded = true
        Print("Scan queued until combat ends.")
        return
    end
    wipe(itemCache)
    wipe(debugCache)
    bagScanNeeded = true
    UpdateMacros(true)
end

local minimapIcon

local function CreateMinimapButton()
    if minimapIcon then return end

    EatMeDB = EatMeDB or {}
    EatMeDB.minimap = EatMeDB.minimap or {}
    if EatMeDB.minimap.minimapPos == nil and EatMeDB.minimapAngle ~= nil then
        EatMeDB.minimap.minimapPos = EatMeDB.minimapAngle % 360
    end

    local LDB = LibStub("LibDataBroker-1.1")
    local DBIcon = LibStub("LibDBIcon-1.0")

    minimapIcon = LDB:NewDataObject("Eat Me", {
        type = "launcher",
        text = "Eat Me",
        label = "Eat Me",
        icon = "Interface\\AddOns\\Eat-Me\\Media\\Minimap",
        OnClick = function(_, button)
            if button == "LeftButton" then
                ForceScan()
            elseif button == "RightButton" then
                ToggleNotifications()
            end
        end,
        OnTooltipShow = function(tooltip)
            tooltip:AddLine("Eat Me |cffaaaaaav1.2.2|r", 1, 0.82, 0)
            tooltip:AddLine("Smart Consumable Manager", 1, 1, 1)
            tooltip:AddLine(" ")
            tooltip:AddDoubleLine("Left-click", "Scan bags", 0.4, 0.8, 1, 1, 1, 1)
            tooltip:AddDoubleLine("Right-click", "Toggle reminders", 0.4, 0.8, 1, 1, 1, 1)
            tooltip:AddLine("Drag to move around the minimap", 0.7, 0.7, 0.7)
        end,
    })

    DBIcon:Register("Eat Me", minimapIcon, EatMeDB.minimap)
end

SLASH_EATME1 = "/eatme"
SlashCmdList.EATME = function(input)
    input = (input or ""):lower():match("^%s*(.-)%s*$")
    if input == "scan" or input == "refresh" then
        ForceScan()
    elseif input == "status" then
        ShowStatus()
    elseif input == "debug" then
        ShowDebug()
    else
        Print("Commands: /eatme scan, /eatme status, /eatme debug")
    end
end

frame:SetScript("OnUpdate", function(_, elapsed)
    if not updatePending then return end
    elapsedSinceRequest = elapsedSinceRequest + elapsed
    if elapsedSinceRequest >= UPDATE_DELAY then
        updatePending = false
        UpdateMacros(false)
    end
end)

frame:SetScript("OnEvent", function(_, event, arg1)
    if event == "PLAYER_ENTERING_WORLD" then
        EatMeDB = EatMeDB or {}
        if EatMeDB.notificationsEnabled == nil then EatMeDB.notificationsEnabled = true end
        CreateMinimapButton()
        RequestUpdate(true)
        C_Timer.After(2, function() RequestUpdate(true) end)
    elseif event == "PLAYER_REGEN_ENABLED" then
        RequestUpdate(bagScanNeeded)
        C_Timer.After(0.2, UpdateBuffReminder)
    elseif event == "PLAYER_REGEN_DISABLED" then
        if buffReminder then buffReminder:Hide() end
    elseif event == "UNIT_AURA" then
        if arg1 == "player" then UpdateBuffReminder() end
    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        -- This event can identify a unit. Only rebuild for the player.
        if arg1 == nil or arg1 == "player" then
            RequestUpdate(true)
        end
    elseif event == "BAG_UPDATE_DELAYED" or event == "PLAYER_LEVEL_UP" or event == "GET_ITEM_INFO_RECEIVED" then
        RequestUpdate(true)
    end
end)

frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
frame:RegisterUnitEvent("UNIT_AURA", "player")
frame:RegisterEvent("BAG_UPDATE_DELAYED")
frame:RegisterEvent("PLAYER_LEVEL_UP")
frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
