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

local FEATURE_ID = "inventory_tracker"
local MODE_DETAIL = "detail"
local MODE_COMPACT = "compact"
local MODE_TOTAL = "total"
local WARBAND_ON = "on"
local WARBAND_OFF = "off"
local DB_VERSION = 1

local GOLD = { r = 1.00, g = 0.82, b = 0.24 }
local TEXT = { r = 0.88, g = 0.86, b = 0.82 }
local MUTED = { r = 0.66, g = 0.68, b = 0.72 }

local SOURCE_ORDER = {
    "bags",
    "bank",
    "reagents",
    "equipment",
}

local SOURCE_META = {
    bags = {
        icon = "Interface\\Icons\\INV_Misc_Bag_08",
        label = function() return L.FEATURE_INVENTORY_TRACKER_SOURCE_BAGS or "Bags" end,
    },
    bank = {
        icon = "Interface\\Icons\\INV_Misc_Coin_02",
        label = function() return L.FEATURE_INVENTORY_TRACKER_SOURCE_BANK or "Bank" end,
    },
    reagents = {
        icon = "Interface\\Icons\\INV_Enchant_DustArcane",
        label = function() return L.FEATURE_INVENTORY_TRACKER_SOURCE_REAGENTS or "Reagents" end,
    },
    equipment = {
        icon = "Interface\\Icons\\INV_Chest_Chain",
        label = function() return L.FEATURE_INVENTORY_TRACKER_SOURCE_EQUIPPED or "Equipped" end,
    },
    warband = {
        icon = "Interface\\Icons\\Achievement_GuildPerk_EverybodysFriend",
        label = function() return L.FEATURE_INVENTORY_TRACKER_SOURCE_WARBAND or "Warband Bank" end,
    },
}

local Runtime = {
    enabled = false,
    hooksReady = false,
    eventsReady = false,
    frame = nil,
    bankOpen = false,
    bagScanQueued = false,
    bankScanQueued = false,
}

ns.InventoryTrackerRuntime = Runtime

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

local function colorToHex(color)
    color = color or GOLD
    local r = math.floor(((tonumber(color.r) or 1) * 255) + 0.5)
    local g = math.floor(((tonumber(color.g) or 1) * 255) + 0.5)
    local b = math.floor(((tonumber(color.b) or 1) * 255) + 0.5)
    return string.format("|cff%02x%02x%02x", r, g, b)
end

local function colorText(text, color)
    return colorToHex(color) .. tostring(text or "") .. "|r"
end

local function iconText(path)
    return string.format("|T%s:11:11:0:0|t", tostring(path or "Interface\\Icons\\INV_Misc_QuestionMark"))
end

local function getClassColor(classFile)
    local color = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] or nil
    if color then
        return { r = color.r or 1, g = color.g or 1, b = color.b or 1 }
    end
    return TEXT
end

local function parseItemID(value)
    local ok, itemID = pcall(function()
        if type(value) == "number" then
            return value
        end
        if type(value) ~= "string" then
            return nil
        end

        if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
            local resolvedID = C_Item.GetItemInfoInstant(value)
            if tonumber(resolvedID) then
                return tonumber(resolvedID)
            end
        end

        return tonumber(value:match("item:(%d+)"))
    end)
    if ok and tonumber(itemID) and tonumber(itemID) > 0 then
        return math.floor(tonumber(itemID))
    end
    return nil
end

local function getContainerInfo(bagID, slotID)
    local itemID, count, link
    if C_Container and type(C_Container.GetContainerItemInfo) == "function" then
        local okInfo, info = pcall(C_Container.GetContainerItemInfo, bagID, slotID)
        if okInfo and type(info) == "table" then
            itemID = tonumber(info.itemID)
            count = tonumber(info.stackCount or info.quantity or info.count)
            link = info.hyperlink or info.itemLink
        end
    elseif type(GetContainerItemInfo) == "function" then
        local okInfo, _, itemCount, _, _, _, itemLink = pcall(GetContainerItemInfo, bagID, slotID)
        if okInfo then
            count = tonumber(itemCount)
            link = itemLink
        end
    end

    if not itemID then
        if not link then
            link = ns.ContainerItems and ns.ContainerItems.GetItemLink and ns.ContainerItems:GetItemLink(bagID, slotID) or nil
        end
        itemID = parseItemID(link)
    end

    count = tonumber(count) or 1
    if itemID and count > 0 then
        return itemID, math.floor(count + 0.5)
    end
    return nil, nil
end

local function getNumContainerSlots(bagID)
    if C_Container and type(C_Container.GetContainerNumSlots) == "function" then
        local okSlots, slots = pcall(C_Container.GetContainerNumSlots, bagID)
        if okSlots then
            return tonumber(slots) or 0
        end
    elseif type(GetContainerNumSlots) == "function" then
        local okSlots, slots = pcall(GetContainerNumSlots, bagID)
        if okSlots then
            return tonumber(slots) or 0
        end
    end
    return 0
end

local function addBagID(list, seen, bagID)
    bagID = tonumber(bagID)
    if bagID ~= nil and not seen[bagID] then
        list[#list + 1] = bagID
        seen[bagID] = true
    end
end

local function getBagIndex()
    return Enum and Enum.BagIndex or {}
end

local function getCharacterBagIDs()
    local bagIndex = getBagIndex()
    local list, seen = {}, {}
    addBagID(list, seen, bagIndex.Backpack or BACKPACK_CONTAINER or 0)
    addBagID(list, seen, bagIndex.Bag_1 or 1)
    addBagID(list, seen, bagIndex.Bag_2 or 2)
    addBagID(list, seen, bagIndex.Bag_3 or 3)
    addBagID(list, seen, bagIndex.Bag_4 or 4)

    local reagentBagSlots = Constants and Constants.InventoryConstants and tonumber(Constants.InventoryConstants.NumReagentBagSlots) or 0
    if reagentBagSlots > 0 then
        addBagID(list, seen, bagIndex.ReagentBag)
    end
    return list
end

local function getBankBagIDs()
    local bagIndex = getBagIndex()
    local list, seen = {}, {}
    addBagID(list, seen, bagIndex.Bank or BANK_CONTAINER or -1)

    if bagIndex.CharacterBankTab_1 then
        addBagID(list, seen, bagIndex.CharacterBankTab_1)
        addBagID(list, seen, bagIndex.CharacterBankTab_2)
        addBagID(list, seen, bagIndex.CharacterBankTab_3)
        addBagID(list, seen, bagIndex.CharacterBankTab_4)
        addBagID(list, seen, bagIndex.CharacterBankTab_5)
        addBagID(list, seen, bagIndex.CharacterBankTab_6)
        return list
    end

    addBagID(list, seen, bagIndex.BankBag_1)
    addBagID(list, seen, bagIndex.BankBag_2)
    addBagID(list, seen, bagIndex.BankBag_3)
    addBagID(list, seen, bagIndex.BankBag_4)
    addBagID(list, seen, bagIndex.BankBag_5)
    addBagID(list, seen, bagIndex.BankBag_6)
    addBagID(list, seen, bagIndex.BankBag_7)
    return list
end

local function getReagentBankBagID()
    local bagIndex = getBagIndex()
    return bagIndex.Reagentbank or bagIndex.ReagentBank or REAGENTBANK_CONTAINER
end

local function getAccountBankBagIDs()
    local bagIndex = getBagIndex()
    local list, seen = {}, {}
    addBagID(list, seen, bagIndex.AccountBankTab_1)
    addBagID(list, seen, bagIndex.AccountBankTab_2)
    addBagID(list, seen, bagIndex.AccountBankTab_3)
    addBagID(list, seen, bagIndex.AccountBankTab_4)
    addBagID(list, seen, bagIndex.AccountBankTab_5)
    return list
end

local function scanContainerInto(bucket, bagID)
    local numSlots = getNumContainerSlots(bagID)
    if not numSlots or numSlots <= 0 then
        return false
    end

    for slotID = 1, numSlots do
        local itemID, count = getContainerInfo(bagID, slotID)
        if itemID and count and count > 0 then
            bucket[itemID] = (tonumber(bucket[itemID]) or 0) + count
        end
    end
    return true
end

local function ensureInventoryDB()
    if not ns.db and ns.EnsureDB then
        ns:EnsureDB()
    end
    local db = ns.db
    if type(db) ~= "table" then
        return nil
    end

    db.inventoryTracker = type(db.inventoryTracker) == "table" and db.inventoryTracker or {}
    db.inventoryTracker.version = DB_VERSION
    db.inventoryTracker.characters = type(db.inventoryTracker.characters) == "table" and db.inventoryTracker.characters or {}
    db.inventoryTracker.warband = type(db.inventoryTracker.warband) == "table" and db.inventoryTracker.warband or {}
    db.inventoryTracker.warband.items = type(db.inventoryTracker.warband.items) == "table" and db.inventoryTracker.warband.items or {}
    return db.inventoryTracker
end

local function ensureCurrentCharacterInventory()
    local inventoryDB = ensureInventoryDB()
    if type(inventoryDB) ~= "table" then
        return nil
    end

    local info = ns.GetCurrentCharacterInfo and ns:GetCurrentCharacterInfo() or {}
    local key = type(info.key) == "string" and info.key ~= "" and info.key or "Unknown"
    local record = inventoryDB.characters[key]
    if type(record) ~= "table" then
        record = {}
        inventoryDB.characters[key] = record
    end

    record.key = key
    record.name = info.name or UnitName("player") or key
    record.realm = info.realm or (ns.TrimRealm and ns:TrimRealm(GetRealmName())) or GetRealmName()
    record.className = info.className or (select(1, UnitClass("player")))
    record.classFile = select(2, UnitClass("player"))
    record.level = tonumber(info.level) or UnitLevel("player") or 0
    record.lastSeen = getNow()
    record.bags = type(record.bags) == "table" and record.bags or {}
    record.bank = type(record.bank) == "table" and record.bank or {}
    record.reagents = type(record.reagents) == "table" and record.reagents or {}
    record.equipment = type(record.equipment) == "table" and record.equipment or {}

    if ns.RefreshCurrentCharacterRecord then
        ns:RefreshCurrentCharacterRecord()
    end

    return record
end

local function sumBucket(bucket, itemID)
    if type(bucket) ~= "table" or not itemID then
        return 0
    end
    return tonumber(bucket[itemID]) or tonumber(bucket[tostring(itemID)]) or 0
end

local function sumAll(bucket)
    local total = 0
    if type(bucket) ~= "table" then
        return 0
    end
    for _, count in pairs(bucket) do
        total = total + (tonumber(count) or 0)
    end
    return total
end

local function buildSourceText(sources)
    local parts = {}
    for _, sourceKey in ipairs(SOURCE_ORDER) do
        local count = tonumber(sources and sources[sourceKey]) or 0
        if count > 0 then
            local meta = SOURCE_META[sourceKey] or {}
            parts[#parts + 1] = string.format("%s %s %d", iconText(meta.icon), meta.label(), count)
        end
    end
    return table.concat(parts, "   ")
end

local function getTooltipMode()
    return (ns.GetFeatureOptionValue and ns:GetFeatureOptionValue(FEATURE_ID, "tooltip_mode")) or MODE_DETAIL
end

local function shouldIncludeWarband()
    return ((ns.GetFeatureOptionValue and ns:GetFeatureOptionValue(FEATURE_ID, "warband_bank")) or WARBAND_ON) == WARBAND_ON
end

local function isWarbandBankSupported()
    return C_Container
        and C_Bank
        and C_Bank.FetchPurchasedBankTabData
        and Enum
        and Enum.BagIndex
        and Enum.BagIndex.AccountBankTab_1
        and Enum.BankType
        and Enum.BankType.Account
end

function Runtime:ScanBags()
    local record = ensureCurrentCharacterInventory()
    if not record then
        return
    end

    local nextBags = {}
    local scannedAnyContainer = false
    for _, bagID in ipairs(getCharacterBagIDs()) do
        scannedAnyContainer = scanContainerInto(nextBags, bagID) or scannedAnyContainer
    end

    if scannedAnyContainer then
        record.bags = nextBags
        record.bagsUpdatedAt = getNow()
    end
end

function Runtime:ScanEquipment()
    local record = ensureCurrentCharacterInventory()
    if not record then
        return
    end

    local nextEquipment = {}
    local scannedAnySlot = false
    local slotNames = ns.GetCharacterGearSlotNames and ns:GetCharacterGearSlotNames() or {}
    for _, slotName in ipairs(slotNames) do
        local slotID = type(GetInventorySlotInfo) == "function" and GetInventorySlotInfo(slotName) or nil
        if slotID then
            scannedAnySlot = true
            local link = type(GetInventoryItemLink) == "function" and GetInventoryItemLink("player", slotID) or nil
            local itemID = parseItemID(link)
            local count = type(GetInventoryItemCount) == "function" and tonumber(GetInventoryItemCount("player", slotID)) or 1
            if itemID then
                nextEquipment[itemID] = (tonumber(nextEquipment[itemID]) or 0) + math.max(1, count or 1)
            end
        end
    end

    if scannedAnySlot then
        record.equipment = nextEquipment
        record.equipmentUpdatedAt = getNow()
    end
end

function Runtime:ScanBank()
    local bankVisible = (BankFrame and BankFrame.IsShown and BankFrame:IsShown()) == true
    if self.bankOpen ~= true and bankVisible ~= true then
        return
    end

    local record = ensureCurrentCharacterInventory()
    if not record then
        return
    end

    local nextBank = {}
    local scannedBank = false
    for _, bagID in ipairs(getBankBagIDs()) do
        scannedBank = scanContainerInto(nextBank, bagID) or scannedBank
    end

    if scannedBank then
        record.bank = nextBank
    end

    local nextReagents = {}
    local scannedReagents = false
    local reagentBankID = getReagentBankBagID()
    if reagentBankID ~= nil then
        scannedReagents = scanContainerInto(nextReagents, reagentBankID)
    end

    if scannedReagents then
        record.reagents = nextReagents
    end

    if scannedBank or scannedReagents then
        record.bankUpdatedAt = getNow()
    end
end

function Runtime:ScanWarbandBank()
    if not isWarbandBankSupported() or not shouldIncludeWarband() then
        return
    end

    local inventoryDB = ensureInventoryDB()
    if type(inventoryDB) ~= "table" then
        return
    end

    local okTabs, tabs = pcall(C_Bank.FetchPurchasedBankTabData, Enum.BankType.Account)
    if not okTabs or type(tabs) ~= "table" then
        return
    end

    local accountBagIDs = getAccountBankBagIDs()
    local items = {}
    local scannedAnyTab = false
    for tabIndex in ipairs(tabs) do
        local bagID = accountBagIDs[tabIndex]
        if bagID ~= nil then
            scannedAnyTab = scanContainerInto(items, bagID) or scannedAnyTab
        end
    end

    if scannedAnyTab then
        inventoryDB.warband.items = items
        inventoryDB.warband.updatedAt = getNow()
    end
end

function Runtime:ScheduleBagScan(delay)
    if self.bagScanQueued then
        return
    end
    self.bagScanQueued = true

    local function run()
        Runtime.bagScanQueued = false
        if Runtime.enabled ~= true then
            return
        end
        Runtime:ScanBags()
        Runtime:ScanEquipment()
    end

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

function Runtime:ScheduleBankScan(delay)
    if self.bankScanQueued then
        return
    end
    self.bankScanQueued = true

    local function run()
        Runtime.bankScanQueued = false
        if Runtime.enabled ~= true then
            return
        end
        Runtime:ScanBank()
        Runtime:ScanWarbandBank()
    end

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

function Runtime:GetItemSummary(itemID)
    itemID = tonumber(itemID)
    if not itemID or itemID <= 0 then
        return nil
    end

    local inventoryDB = ensureInventoryDB()
    if type(inventoryDB) ~= "table" then
        return nil
    end

    local rows = {}
    local total = 0
    local currentKey = ns.GetCurrentCharacterInfo and (ns:GetCurrentCharacterInfo().key) or nil

    for key, record in pairs(inventoryDB.characters or {}) do
        if type(record) == "table" then
            local sources = {
                bags = sumBucket(record.bags, itemID),
                bank = sumBucket(record.bank, itemID),
                reagents = sumBucket(record.reagents, itemID),
                equipment = sumBucket(record.equipment, itemID),
            }
            local characterTotal = sources.bags + sources.bank + sources.reagents + sources.equipment
            if characterTotal > 0 then
                rows[#rows + 1] = {
                    key = key,
                    name = record.name or key,
                    realm = record.realm,
                    classFile = record.classFile,
                    total = characterTotal,
                    sources = sources,
                    current = key == currentKey,
                    main = ns.IsLiveMainCharacter and ns:IsLiveMainCharacter(key) or false,
                    lastSeen = tonumber(record.lastSeen) or 0,
                }
                total = total + characterTotal
            end
        end
    end

    table.sort(rows, function(a, b)
        if a.current ~= b.current then
            return a.current
        end
        if a.main ~= b.main then
            return a.main
        end
        if a.total ~= b.total then
            return a.total > b.total
        end
        if a.lastSeen ~= b.lastSeen then
            return a.lastSeen > b.lastSeen
        end
        return tostring(a.name or "") < tostring(b.name or "")
    end)

    local warbandTotal = 0
    if shouldIncludeWarband() then
        warbandTotal = sumBucket(inventoryDB.warband and inventoryDB.warband.items, itemID)
        total = total + warbandTotal
    end

    if total <= 0 then
        return nil
    end

    return {
        total = total,
        rows = rows,
        warband = warbandTotal,
    }
end

function Runtime:AddDetailTooltip(tooltip, summary)
    tooltip:AddLine(" ")
    tooltip:AddDoubleLine(
        L.FEATURE_INVENTORY_TRACKER_TOOLTIP_HEADER or "Vaultloom Inventory",
        string.format(L.FEATURE_INVENTORY_TRACKER_TOOLTIP_TOTAL_FORMAT or "Total: %d", summary.total or 0),
        GOLD.r, GOLD.g, GOLD.b,
        GOLD.r, GOLD.g, GOLD.b
    )

    local shown = 0
    local maxRows = 8
    for _, row in ipairs(summary.rows or {}) do
        if shown >= maxRows then
            break
        end
        shown = shown + 1
        local name = colorText(row.name or row.key or "?", getClassColor(row.classFile))
        tooltip:AddDoubleLine(name, tostring(row.total or 0), TEXT.r, TEXT.g, TEXT.b, 1, 1, 1)
        local sourceText = buildSourceText(row.sources)
        if sourceText ~= "" then
            tooltip:AddLine(sourceText, MUTED.r, MUTED.g, MUTED.b, true)
        end
    end

    if #(summary.rows or {}) > maxRows then
        tooltip:AddLine(string.format(L.FEATURE_INVENTORY_TRACKER_TOOLTIP_MORE_FORMAT or "+%d more characters", #summary.rows - maxRows), MUTED.r, MUTED.g, MUTED.b, true)
    end

    if tonumber(summary.warband) and summary.warband > 0 then
        local meta = SOURCE_META.warband
        tooltip:AddDoubleLine(iconText(meta.icon) .. " " .. meta.label(), tostring(summary.warband), TEXT.r, TEXT.g, TEXT.b, 1, 1, 1)
    end
end

function Runtime:AddCompactTooltip(tooltip, summary)
    tooltip:AddLine(" ")
    tooltip:AddLine(
        string.format(
            L.FEATURE_INVENTORY_TRACKER_TOOLTIP_COMPACT_TOTAL_FORMAT or "Vaultloom Inventory: %d total",
            summary.total or 0
        ),
        GOLD.r, GOLD.g, GOLD.b,
        true
    )

    local parts = {}
    local maxParts = 4
    for _, row in ipairs(summary.rows or {}) do
        if #parts >= maxParts then
            break
        end
        parts[#parts + 1] = string.format("%s %d", row.name or row.key or "?", row.total or 0)
    end
    if tonumber(summary.warband) and summary.warband > 0 and #parts < maxParts then
        parts[#parts + 1] = string.format("%s %d", SOURCE_META.warband.label(), summary.warband)
    end
    if #parts > 0 then
        tooltip:AddLine(table.concat(parts, "  |  "), TEXT.r, TEXT.g, TEXT.b, true)
    end
end

function Runtime:AddTotalTooltip(tooltip, summary)
    tooltip:AddLine(" ")
    tooltip:AddLine(
        string.format(
            L.FEATURE_INVENTORY_TRACKER_TOOLTIP_COMPACT_TOTAL_FORMAT or "Vaultloom Inventory: %d total",
            summary.total or 0
        ),
        GOLD.r, GOLD.g, GOLD.b,
        true
    )
end

local function ensureTooltipResetHook(tooltip)
    if not tooltip or tooltip.VaultloomInventoryTrackerClearHook then
        return
    end
    if tooltip.HookScript then
        tooltip:HookScript("OnTooltipCleared", function(selfTooltip)
            selfTooltip.VaultloomInventoryTrackerItemID = nil
        end)
        tooltip.VaultloomInventoryTrackerClearHook = true
    end
end

local function getTooltipItemID(tooltip, data)
    if type(data) == "table" then
        local itemID = parseItemID(data.id or data.hyperlink or data.guid)
        if itemID then
            return itemID
        end
    end

    if tooltip and type(tooltip.GetItem) == "function" then
        local okItem, _, itemLink = pcall(tooltip.GetItem, tooltip)
        if okItem then
            return parseItemID(itemLink)
        end
    end
    return nil
end

function Runtime:ApplyTooltip(tooltip, data)
    if self.enabled ~= true or not tooltip then
        return
    end

    local itemID = getTooltipItemID(tooltip, data)
    if not itemID then
        return
    end
    if tooltip.VaultloomInventoryTrackerItemID == itemID then
        return
    end

    local summary = self:GetItemSummary(itemID)
    if not summary then
        return
    end

    ensureTooltipResetHook(tooltip)
    tooltip.VaultloomInventoryTrackerItemID = itemID

    local mode = getTooltipMode()
    if mode == MODE_TOTAL then
        self:AddTotalTooltip(tooltip, summary)
    elseif mode == MODE_COMPACT then
        self:AddCompactTooltip(tooltip, summary)
    else
        self:AddDetailTooltip(tooltip, summary)
    end

    if tooltip.Show then
        tooltip:Show()
    end
end

function Runtime:EnsureHooks()
    if self.hooksReady then
        return
    end
    self.hooksReady = true

    if TooltipDataProcessor and TooltipDataProcessor.AddTooltipPostCall and Enum and Enum.TooltipDataType then
        TooltipDataProcessor.AddTooltipPostCall(Enum.TooltipDataType.Item, function(tooltip, data)
            Runtime:ApplyTooltip(tooltip, data)
        end)
    elseif GameTooltip and GameTooltip.HookScript then
        GameTooltip:HookScript("OnTooltipSetItem", function(tooltip)
            Runtime:ApplyTooltip(tooltip, nil)
        end)
    end
end

local function registerEvent(frame, eventName)
    if frame and type(eventName) == "string" and eventName ~= "" then
        pcall(frame.RegisterEvent, frame, eventName)
    end
end

function Runtime:EnsureEventFrame()
    if self.frame then
        return self.frame
    end

    local frame = CreateFrame("Frame")
    frame:SetScript("OnEvent", function(_, event)
        if Runtime.enabled ~= true then
            return
        end

        if event == "PLAYER_ENTERING_WORLD" then
            Runtime:ScheduleBagScan(1.0)
        elseif event == "BAG_UPDATE_DELAYED" then
            Runtime:ScheduleBagScan(0.05)
            if Runtime.bankOpen then
                Runtime:ScheduleBankScan(0.15)
            end
        elseif event == "PLAYER_EQUIPMENT_CHANGED" then
            Runtime:ScheduleBagScan(0.10)
        elseif event == "BANKFRAME_OPENED" then
            Runtime.bankOpen = true
            Runtime:ScheduleBankScan(0.35)
        elseif event == "BANKFRAME_CLOSED" then
            Runtime.bankOpen = false
        elseif event == "PLAYERBANKSLOTS_CHANGED"
            or event == "PLAYERREAGENTBANKSLOTS_CHANGED"
            or event == "REAGENTBANK_PURCHASED"
            or event == "PLAYER_ACCOUNT_BANK_TAB_SLOTS_CHANGED"
        then
            Runtime:ScheduleBankScan(0.20)
        end
    end)

    self.frame = frame
    return frame
end

function Runtime:RegisterEvents()
    local frame = self:EnsureEventFrame()
    if self.eventsReady then
        return
    end

    self.eventsReady = true
    registerEvent(frame, "PLAYER_ENTERING_WORLD")
    registerEvent(frame, "BAG_UPDATE_DELAYED")
    registerEvent(frame, "PLAYER_EQUIPMENT_CHANGED")
    registerEvent(frame, "BANKFRAME_OPENED")
    registerEvent(frame, "BANKFRAME_CLOSED")
    registerEvent(frame, "PLAYERBANKSLOTS_CHANGED")
    registerEvent(frame, "PLAYERREAGENTBANKSLOTS_CHANGED")
    registerEvent(frame, "REAGENTBANK_PURCHASED")
    registerEvent(frame, "PLAYER_ACCOUNT_BANK_TAB_SLOTS_CHANGED")
end

function Runtime:UnregisterEvents()
    if self.frame and self.eventsReady then
        pcall(self.frame.UnregisterAllEvents, self.frame)
    end
    self.eventsReady = false
    self.bankOpen = false
    self.bagScanQueued = false
    self.bankScanQueued = false
end

function Runtime:SetEnabled(enabled)
    self.enabled = enabled == true
    self:EnsureHooks()

    if self.enabled then
        self:RegisterEvents()
        self:ScheduleBagScan(0.25)
    else
        self:UnregisterEvents()
    end
end

function ns:GetInventoryTrackerItemSummary(itemID)
    return Runtime:GetItemSummary(itemID)
end

ns:RegisterFeatureDefinition(FEATURE_ID, {
    id = FEATURE_ID,
    order = 21,
    category = "inventory",
    title = function() return L.FEATURE_INVENTORY_TRACKER_TITLE or "Vaultloom Inventory" end,
    description = function() return L.FEATURE_INVENTORY_TRACKER_DESCRIPTION or "Shows saved item counts from your characters, bank, reagent bank, equipped gear, and Warband bank in item tooltips." end,
    note = function() return L.FEATURE_INVENTORY_TRACKER_NOTE or "Scans are event-based: bags after bag updates, bank data only while the bank is open, and tooltips read cached totals." end,
    previewLabel = function() return L.FEATURE_INVENTORY_TRACKER_PREVIEW_LABEL or "Item Tooltip" end,
    previewNote = function() return L.FEATURE_INVENTORY_TRACKER_PREVIEW_NOTE or "Character totals with source details, compact summaries, or total-only mode." end,
    optionToggle = {
        key = "tooltip_mode",
        label = function() return L.FEATURE_INVENTORY_TRACKER_MODE_LABEL or "Tooltip" end,
        values = { MODE_DETAIL, MODE_COMPACT, MODE_TOTAL },
        defaultValue = MODE_DETAIL,
        valueLabels = {
            detail = function() return L.FEATURE_INVENTORY_TRACKER_MODE_DETAIL or "Detail" end,
            compact = function() return L.FEATURE_INVENTORY_TRACKER_MODE_COMPACT or "Compact" end,
            total = function() return L.FEATURE_INVENTORY_TRACKER_MODE_TOTAL or "Total" end,
        },
    },
    secondaryOptionToggle = {
        key = "warband_bank",
        label = function() return L.FEATURE_INVENTORY_TRACKER_WARBAND_LABEL or "Warband" end,
        values = { WARBAND_ON, WARBAND_OFF },
        defaultValue = WARBAND_ON,
        valueLabels = {
            on = function() return L.FEATURE_INVENTORY_TRACKER_WARBAND_ON or "Warband On" end,
            off = function() return L.FEATURE_INVENTORY_TRACKER_WARBAND_OFF or "Warband Off" end,
        },
    },
    requiresReload = false,
    apply = function(isEnabled)
        Runtime:SetEnabled(isEnabled == true)
    end,
    onOptionChanged = function(value, optionKey)
        if optionKey == "warband_bank" and Runtime.enabled == true and value == WARBAND_ON and Runtime.bankOpen == true then
            Runtime:ScheduleBankScan(0.10)
        end
    end,
})
