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 bagItems = {}
local updatePending = false
local bagScanNeeded = true
local elapsedSinceRequest = 0
local lastSelected = {}

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 function ScanItem(itemID)
    if itemCache[itemID] ~= nil then
        return itemCache[itemID] ~= false and itemCache[itemID] or nil
    end

    local name, _, quality, itemLevel, requiredLevel, _, _, _, _, icon, sellPrice, classID = GetItemInfo(itemID)
    if not name then return nil, true end
    if classID ~= 0 then
        itemCache[itemID] = false
        return nil
    end

    scanner:ClearLines()
    scanner:SetHyperlink("item:" .. itemID)
    if scanner:NumLines() == 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 lineIndex = 1, scanner:NumLines() do
        local line = _G["EatMeScannerTooltipTextLeft" .. lineIndex]
        local text = line and line:GetText()
        if text then
            text = text:lower()
            allText = allText .. " " .. text

            if text:find("must remain seated", 1, true) then requiresSitting = true end
            if text:find("well fed", 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("increase", 1, true) or text:find("gain", 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 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

    if not isRecovery and not wellFed and not isHealthPotion and not isManaPotion and not isFlask then
        itemCache[itemID] = false
        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,
    }

    itemCache[itemID] = data
    return data
end

local function ScanBags()
    wipe(bagItems)
    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 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(),
    }

    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)

    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 ForceScan()
    if InCombatLockdown() then
        bagScanNeeded = true
        Print("Scan queued until combat ends.")
        return
    end
    wipe(itemCache)
    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
                ShowStatus()
            end
        end,
        OnTooltipShow = function(tooltip)
            tooltip:AddLine("Eat Me |cffaaaaaav1.1.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", "Show selections", 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()
    else
        Print("Commands: /eatme scan, /eatme status")
    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 {}
        CreateMinimapButton()
        RequestUpdate(true)
        C_Timer.After(2, function() RequestUpdate(true) end)
    elseif event == "PLAYER_REGEN_ENABLED" then
        RequestUpdate(bagScanNeeded)
    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("BAG_UPDATE_DELAYED")
frame:RegisterEvent("PLAYER_LEVEL_UP")
frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
frame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
