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

local BagItemLevelRuntime = {
    enabled = false,
    hooksReady = false,
    refreshQueued = false,
    trackedButtons = setmetatable({}, { __mode = "k" }),
}
ns.BagItemLevelRuntime = BagItemLevelRuntime

local function ensureBagItemLevelFont()
    local existing = _G.VaultloomBagItemLevelFont
    if existing then
        return existing
    end

    local font = CreateFont("VaultloomBagItemLevelFont")
    font:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
    font:SetShadowOffset(1, -1)
    font:SetShadowColor(0, 0, 0, 0.95)
    return font
end

local function ensureBagItemLevelOverlay(button)
    if not button then
        return nil
    end

    local text = button.VaultloomBagItemLevelText
    if text then
        return text
    end

    text = button:CreateFontString(nil, "OVERLAY")
    text:SetFontObject(ensureBagItemLevelFont())
    text:SetJustifyH("CENTER")
    text:SetPoint("BOTTOM", button, "BOTTOM", 0, 3)
    text:SetText("")
    text:Hide()

    button.VaultloomBagItemLevelText = text
    return text
end

local function hideBagItemLevelOverlay(button)
    local text = button and button.VaultloomBagItemLevelText or nil
    if text then
        text:Hide()
    end
end

local function isQuestItemContext(button)
    local frame = button
    local guard = 0
    while frame and guard < 8 do
        local name = frame.GetName and frame:GetName() or nil
        if type(name) == "string" and (name:find("^Quest") or name:find("^GossipFrame")) then
            return true
        end

        frame = frame.GetParent and frame:GetParent() or nil
        guard = guard + 1
    end

    return false
end

local function getItemLevelFromItemLink(itemLink)
    if type(itemLink) ~= "string" or itemLink == "" then
        return nil, nil
    end

    local isEquippable = false
    if type(IsEquippableItem) == "function" then
        local okEquippable, resolvedEquippable = pcall(IsEquippableItem, itemLink)
        if okEquippable and resolvedEquippable then
            isEquippable = true
        end
    end

    if isEquippable ~= true then
        return nil, itemLink
    end

    local itemEquipLoc, itemClassID = nil, nil
    local weaponClassID = (Enum and Enum.ItemClass and Enum.ItemClass.Weapon) or 2
    local armorClassID = (Enum and Enum.ItemClass and Enum.ItemClass.Armor) or 4
    if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
        local _, _, _, equipLoc, _, classID = C_Item.GetItemInfoInstant(itemLink)
        itemEquipLoc = equipLoc
        itemClassID = classID
    end

    if (type(itemEquipLoc) ~= "string" or itemEquipLoc == "") and type(GetItemInfo) == "function" then
        local _, _, _, _, _, _, _, _, equipLoc, _, _, classID = GetItemInfo(itemLink)
        itemEquipLoc = equipLoc
        itemClassID = itemClassID or classID
    end

    if type(itemEquipLoc) ~= "string" or itemEquipLoc == "" or itemEquipLoc == "INVTYPE_BAG" then
        return nil, itemLink
    end

    if itemClassID ~= weaponClassID and itemClassID ~= armorClassID then
        return nil, itemLink
    end

    local itemLevel = nil
    if C_Item and type(C_Item.GetDetailedItemLevelInfo) == "function" then
        local okLevel, resolvedLevel = pcall(C_Item.GetDetailedItemLevelInfo, itemLink)
        if okLevel then
            itemLevel = tonumber(resolvedLevel)
        end
    end

    if not itemLevel and type(GetDetailedItemLevelInfo) == "function" then
        local okLevel, resolvedLevel = pcall(GetDetailedItemLevelInfo, itemLink)
        if okLevel then
            itemLevel = tonumber(resolvedLevel)
        end
    end

    if not itemLevel or itemLevel <= 0 then
        return nil, itemLink
    end

    return math.floor(itemLevel + 0.5), itemLink
end

local function getBagItemLevelColor(bagID, slotID, itemLink)
    local textStyle = ns.GetFeatureOptionValue and ns:GetFeatureOptionValue("bag_item_level", "text_style") or "white"
    if textStyle ~= "quality" then
        return 1, 1, 1
    end

    local quality = ContainerItems:GetQuality(bagID, slotID, itemLink)
    local color = quality ~= nil and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] or nil
    if color then
        return color.r, color.g, color.b
    end

    return 1, 1, 1
end

local function getContainerGearItemLevel(bagID, slotID)
    bagID = tonumber(bagID)
    slotID = tonumber(slotID)
    if bagID == nil or slotID == nil or slotID < 1 then
        return nil, nil
    end

    local itemLink = ContainerItems:GetItemLink(bagID, slotID)
    if type(itemLink) ~= "string" or itemLink == "" then
        return nil, nil
    end

    local isEquippable = false
    if type(IsEquippableItem) == "function" then
        local okEquippable, resolvedEquippable = pcall(IsEquippableItem, itemLink)
        if okEquippable and resolvedEquippable then
            isEquippable = true
        end
    end

    if isEquippable ~= true then
        return nil, itemLink
    end

    local itemEquipLoc, itemClassID = nil, nil
    local weaponClassID = (Enum and Enum.ItemClass and Enum.ItemClass.Weapon) or 2
    local armorClassID = (Enum and Enum.ItemClass and Enum.ItemClass.Armor) or 4
    if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
        local _, _, _, equipLoc, _, classID = C_Item.GetItemInfoInstant(itemLink)
        itemEquipLoc = equipLoc
        itemClassID = classID
    end

    if (type(itemEquipLoc) ~= "string" or itemEquipLoc == "") and type(GetItemInfo) == "function" then
        local _, _, _, _, _, _, _, _, equipLoc, _, _, classID = GetItemInfo(itemLink)
        itemEquipLoc = equipLoc
        itemClassID = itemClassID or classID
    end

    if type(itemEquipLoc) ~= "string" or itemEquipLoc == "" or itemEquipLoc == "INVTYPE_BAG" then
        return nil, itemLink
    end

    if itemClassID ~= weaponClassID and itemClassID ~= armorClassID then
        return nil, itemLink
    end

    local itemLevel = nil
    if C_Item and ItemLocation and type(ItemLocation.CreateFromBagAndSlot) == "function" then
        local itemLocation = ItemLocation:CreateFromBagAndSlot(bagID, slotID)
        if itemLocation and C_Item.DoesItemExist and C_Item.DoesItemExist(itemLocation) and type(C_Item.GetCurrentItemLevel) == "function" then
            local okCurrent, currentLevel = pcall(C_Item.GetCurrentItemLevel, itemLocation)
            if okCurrent then
                itemLevel = tonumber(currentLevel)
            end
        end
    end

    if not itemLevel and type(GetDetailedItemLevelInfo) == "function" then
        local okLevel, resolvedLevel = pcall(GetDetailedItemLevelInfo, itemLink)
        if okLevel then
            itemLevel = tonumber(resolvedLevel)
        end
    end

    if not itemLevel or itemLevel <= 0 then
        return getItemLevelFromItemLink(itemLink)
    end

    return math.floor(itemLevel + 0.5), itemLink
end

function BagItemLevelRuntime:TrackButton(button)
    if not button or (button.IsForbidden and button:IsForbidden()) then
        return
    end

    if isQuestItemContext(button) then
        hideBagItemLevelOverlay(button)
        self.trackedButtons[button] = nil
        return
    end

    local name = button.GetName and button:GetName() or nil
    local hasItemContext = type(button.GetSlotAndBagID) == "function"
        or type(button.GetBagID) == "function"
        or type(button.GetItem) == "function"
        or type(button.GetItemLocation) == "function"
        or button.itemLocation ~= nil
        or button.itemLink ~= nil
        or button.hyperlink ~= nil
        or (type(name) == "string" and name:find("Item", 1, true) and ContainerItems:GetButtonIcon(button))
    if not hasItemContext then
        return
    end

    local bagID, slotID = ContainerItems:GetButtonBagAndSlot(button)
    if (bagID == nil or slotID == nil) and not ContainerItems:GetButtonItemLink(button, bagID, slotID) then
        return
    end

    self.trackedButtons[button] = true

    if button.VaultloomBagItemLevelHooked ~= true and button.HookScript then
        button.VaultloomBagItemLevelHooked = true
        button:HookScript("OnShow", function(selfButton)
            BagItemLevelRuntime:RefreshButton(selfButton)
        end)
        button:HookScript("OnHide", function(selfButton)
            local text = selfButton and selfButton.VaultloomBagItemLevelText or nil
            if text then
                text:Hide()
            end
        end)
    end
end

local function forEachDescendantButton(rootFrame, callback)
    if not rootFrame or type(callback) ~= "function" then
        return
    end

    local stack = { rootFrame }
    local visited = {}
    while #stack > 0 do
        local frame = table.remove(stack)
        if not visited[frame] then
            visited[frame] = true

            if frame ~= rootFrame and frame.IsObjectType and frame:IsObjectType("Button") then
                callback(frame)
            end

            if frame.GetChildren then
                for _, child in ipairs({ frame:GetChildren() }) do
                    stack[#stack + 1] = child
                end
            end
        end
    end
end

function BagItemLevelRuntime:RefreshContainer(frame)
    if not frame or (frame.IsShown and not frame:IsShown()) or type(frame.EnumerateValidItems) ~= "function" then
        return
    end

    for _, button in frame:EnumerateValidItems() do
        self:TrackButton(button)
        self:RefreshButton(button)
    end
end

function BagItemLevelRuntime:RefreshBankButtons()
    for _, rootFrame in ipairs({
        _G.PlayerBankPanel,
        _G.BankFrame,
        _G.BankPanel,
        _G.ReagentBankFrame,
        _G.AccountBankPanel,
        _G.AccountBankFrame,
        _G.WarbandBankFrame,
    }) do
        self:RefreshContainer(rootFrame)
        forEachDescendantButton(rootFrame, function(button)
            self:TrackButton(button)
            self:RefreshButton(button)
        end)
    end

    for _, entry in ipairs({
        { prefix = "BankFrameItem", maxCount = tonumber(NUM_BANKGENERIC_SLOTS) or 28 },
        { prefix = "BankPanelItem", maxCount = tonumber(NUM_BANKGENERIC_SLOTS) or 28 },
        { prefix = "ReagentBankFrameItem", maxCount = tonumber(NUM_REAGENTBANKGENERIC_SLOTS) or 98 },
    }) do
        for index = 1, entry.maxCount do
            local button = _G[entry.prefix .. index]
            if button then
                self:TrackButton(button)
                self:RefreshButton(button)
            end
        end
    end
end

function BagItemLevelRuntime:RefreshButton(button)
    if not button then
        return
    end

    if isQuestItemContext(button) then
        hideBagItemLevelOverlay(button)
        self.trackedButtons[button] = nil
        return
    end

    local text = ensureBagItemLevelOverlay(button)
    if not text then
        return
    end

    if self.enabled ~= true or (button.IsShown and not button:IsShown()) then
        text:Hide()
        return
    end

    local bagID, slotID = ContainerItems:GetButtonBagAndSlot(button)
    local itemLevel, itemLink
    if bagID ~= nil and slotID ~= nil then
        itemLevel, itemLink = getContainerGearItemLevel(bagID, slotID)
    else
        itemLink = ContainerItems:GetButtonItemLink(button, bagID, slotID)
        itemLevel, itemLink = getItemLevelFromItemLink(itemLink)
    end

    if not itemLevel then
        text:Hide()
        return
    end

    local r, g, b = getBagItemLevelColor(bagID, slotID, itemLink)
    text:SetText(itemLevel)
    text:SetTextColor(r, g, b, 1)
    text:Show()
end

function BagItemLevelRuntime:RefreshAll()
    local combinedBags = _G.ContainerFrameCombinedBags
    if combinedBags then
        self:RefreshContainer(combinedBags)
    end

    local containerCount = tonumber(NUM_TOTAL_BAG_FRAMES or NUM_CONTAINER_FRAMES) or 0
    for index = 1, containerCount do
        local frame = _G["ContainerFrame" .. index]
        if frame and frame ~= combinedBags then
            self:RefreshContainer(frame)
        end
    end

    self:RefreshBankButtons()

    for button in pairs(self.trackedButtons or {}) do
        self:RefreshButton(button)
    end
end

function BagItemLevelRuntime:HideAll()
    for button in pairs(self.trackedButtons or {}) do
        local text = button and button.VaultloomBagItemLevelText or nil
        if text then
            text:Hide()
        end
    end
end

function BagItemLevelRuntime:QueueRefresh(delaySeconds)
    if self.refreshQueued then
        return
    end

    self.refreshQueued = true
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(delaySeconds or 0, function()
            BagItemLevelRuntime.refreshQueued = false
            if BagItemLevelRuntime.enabled == true then
                BagItemLevelRuntime:RefreshAll()
            end
        end)
        return
    end

    self.refreshQueued = false
    self:RefreshAll()
end

function BagItemLevelRuntime:EnsureHooks()
    if self.hooksReady then
        return
    end

    self.hooksReady = true

    if type(hooksecurefunc) ~= "function" then
        return
    end

    if type(ContainerFrameMixin) == "table" and type(ContainerFrameMixin.UpdateItems) == "function" then
        hooksecurefunc(ContainerFrameMixin, "UpdateItems", function(frame)
            BagItemLevelRuntime:RefreshContainer(frame)
        end)
    end

    if type(ContainerFrameItemButtonMixin) == "table" and type(ContainerFrameItemButtonMixin.OnLoad) == "function" then
        hooksecurefunc(ContainerFrameItemButtonMixin, "OnLoad", function(button)
            BagItemLevelRuntime:TrackButton(button)
            BagItemLevelRuntime:RefreshButton(button)
        end)
    end

    if type(SetItemButtonQuality) == "function" then
        hooksecurefunc("SetItemButtonQuality", function(button)
            BagItemLevelRuntime:TrackButton(button)
            BagItemLevelRuntime:RefreshButton(button)
        end)
    end

    local combinedBags = _G.ContainerFrameCombinedBags
    if combinedBags and type(combinedBags.UpdateItems) == "function" then
        hooksecurefunc(combinedBags, "UpdateItems", function(frame)
            BagItemLevelRuntime:RefreshContainer(frame)
        end)
    end

    local containerCount = tonumber(NUM_TOTAL_BAG_FRAMES or NUM_CONTAINER_FRAMES) or 0
    for index = 1, containerCount do
        local frame = _G["ContainerFrame" .. index]
        if frame and type(frame.UpdateItems) == "function" then
            hooksecurefunc(frame, "UpdateItems", function(containerFrame)
                BagItemLevelRuntime:RefreshContainer(containerFrame)
            end)
        end
    end

    for _, frame in ipairs({
        _G.PlayerBankPanel,
        _G.BankFrame,
        _G.ReagentBankFrame,
        _G.BankPanel,
        _G.AccountBankPanel,
        _G.AccountBankFrame,
        _G.WarbandBankFrame,
    }) do
        if frame and frame.HookScript then
            frame:HookScript("OnShow", function()
                BagItemLevelRuntime:RefreshAll()
                BagItemLevelRuntime:QueueRefresh(0)
                BagItemLevelRuntime:QueueRefresh(0.1)
            end)
        end
    end
end

local function isBagItemLevelDelayedRefreshEvent(event)
    return event == "BANKFRAME_OPENED"
        or event == "PLAYERBANKSLOTS_CHANGED"
        or event == "PLAYERREAGENTBANKSLOTS_CHANGED"
        or event == "PLAYER_ACCOUNT_BANK_TAB_SLOTS_CHANGED"
        or event == "PLAYER_INTERACTION_MANAGER_FRAME_SHOW"
        or event == "PLAYER_ENTERING_WORLD"
end

function BagItemLevelRuntime:HandleEvent(event)
    self:RefreshAll()

    if isBagItemLevelDelayedRefreshEvent(event) then
        self:QueueRefresh(0)
        self:QueueRefresh(0.1)
        self:QueueRefresh(0.25)
    end
end

function BagItemLevelRuntime:SetEnabled(enabled)
    enabled = enabled == true
    self.enabled = enabled

    if not self.eventFrame then
        self.eventFrame = CreateFrame("Frame")
        self.eventFrame:SetScript("OnEvent", function(_, event)
            BagItemLevelRuntime:HandleEvent(event)
        end)
    end

    self.eventFrame:UnregisterAllEvents()

    if not enabled then
        self.refreshQueued = false
        self:HideAll()
        return
    end

    self:EnsureHooks()
    self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    self.eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
    self.eventFrame:RegisterEvent("BAG_OPEN")
    self.eventFrame:RegisterEvent("BAG_CLOSED")
    self.eventFrame:RegisterEvent("BANKFRAME_OPENED")
    self.eventFrame:RegisterEvent("BANKFRAME_CLOSED")
    self.eventFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
    if self.eventFrame.RegisterEvent then
        pcall(self.eventFrame.RegisterEvent, self.eventFrame, "PLAYERREAGENTBANKSLOTS_CHANGED")
        pcall(self.eventFrame.RegisterEvent, self.eventFrame, "PLAYER_ACCOUNT_BANK_TAB_SLOTS_CHANGED")
        pcall(self.eventFrame.RegisterEvent, self.eventFrame, "PLAYER_INTERACTION_MANAGER_FRAME_SHOW")
    end
    self:RefreshAll()
    self:QueueRefresh(0)
    self:QueueRefresh(0.1)
end

ns:RegisterFeatureDefinition("bag_item_level", {
    id = "bag_item_level",
    order = 22,
    category = "inventory",
    title = function() return L.FEATURE_BAG_ITEM_LEVEL_TITLE or "Bag Item Level" end,
    description = function() return L.FEATURE_BAG_ITEM_LEVEL_DESCRIPTION or "Shows item level on equippable gear in bags and bank." end,
    note = function() return L.FEATURE_BAG_ITEM_LEVEL_NOTE or "Visible on bag and bank item buttons." end,
    previewLabel = function() return L.FEATURE_BAG_ITEM_LEVEL_PREVIEW_LABEL or "Bag & Bank Overlay" end,
    previewNote = function() return L.FEATURE_BAG_ITEM_LEVEL_PREVIEW_NOTE or "Small item level numbers on visible gear icons in your bags and bank." end,
    optionToggle = {
        key = "text_style",
        label = function() return L.FEATURE_BAG_ITEM_LEVEL_STYLE_LABEL or "Text Style" end,
        values = { "white", "quality" },
        defaultValue = "white",
        valueLabels = {
            white = function() return L.FEATURE_BAG_ITEM_LEVEL_STYLE_WHITE or "White" end,
            quality = function() return L.FEATURE_BAG_ITEM_LEVEL_STYLE_COLORED or "Colored" end,
        },
    },
    requiresReload = false,
    apply = function(isEnabled)
        BagItemLevelRuntime:SetEnabled(isEnabled == true)
    end,
    onOptionChanged = function()
        BagItemLevelRuntime:RefreshAll()
    end,
})
