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

local BACKDROP_TEMPLATE = BackdropTemplateMixin and "BackdropTemplate" or nil
local addonName = ns.ADDON_NAME or "Vaultloom"
local ASSET_ROOT = "Interface\\AddOns\\" .. tostring(addonName) .. "\\Assets\\"

local COLORS = {
    bg = { 0.035, 0.034, 0.040, 0.96 },
    panel = { 0.055, 0.052, 0.060, 0.94 },
    panelSoft = { 0.075, 0.070, 0.080, 0.90 },
    border = { 0.36, 0.28, 0.14, 0.82 },
    borderSoft = { 0.25, 0.22, 0.18, 0.70 },
    gold = { 0.95, 0.76, 0.28, 1.00 },
    text = { 0.92, 0.90, 0.84, 1.00 },
    muted = { 0.70, 0.70, 0.74, 1.00 },
    good = { 0.34, 0.88, 0.46, 1.00 },
    warn = { 0.95, 0.70, 0.24, 1.00 },
    bad = { 0.95, 0.24, 0.24, 1.00 },
}

local ARMORY_FEATURE_ID = "vaultloom_armory"
local ARMORY_FRAME_NAME = "VaultloomArmoryFrame"
local ARMORY_BUTTON_NAME = "VaultloomArmoryCharacterButton"
local ARMORY_WIDTH = 1110
local ARMORY_HEIGHT = 650
local ARMORY_BG_TEXTURE = ASSET_ROOT .. "armory_bg.tga"
local ARMORY_BG_TEX_RIGHT = ARMORY_WIDTH / 2048
local ARMORY_BG_TEX_BOTTOM = ARMORY_HEIGHT / 1024
local ARMORY_BG_TINT_ALPHA = 0.30

local ENCHANTABLE_SLOT_IDS = {
    [1] = true,
    [3] = true,
    [5] = true,
    [7] = true,
    [8] = true,
    [11] = true,
    [12] = true,
    [16] = true,
    [17] = true,
}

local SOCKETABLE_SLOT_MAX = {
    [1] = 1,
    [2] = 2,
    [6] = 1,
    [9] = 1,
    [11] = 2,
    [12] = 2,
}

local SLOT_COLUMNS = {
    left = {
        { name = "HeadSlot", label = function() return HEADSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_HEAD or "Head" end },
        { name = "NeckSlot", label = function() return NECKSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_NECK or "Neck" end },
        { name = "ShoulderSlot", label = function() return SHOULDERSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_SHOULDER or "Shoulders" end },
        { name = "BackSlot", label = function() return BACKSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_BACK or "Back" end },
        { name = "ChestSlot", label = function() return CHESTSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_CHEST or "Chest" end },
        { name = "WristSlot", label = function() return WRISTSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_WRIST or "Wrist" end },
        { name = "HandsSlot", label = function() return HANDSSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_HANDS or "Hands" end },
        { name = "WaistSlot", label = function() return WAISTSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_WAIST or "Waist" end },
    },
    right = {
        { name = "LegsSlot", label = function() return LEGSSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_LEGS or "Legs" end },
        { name = "FeetSlot", label = function() return FEETSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_FEET or "Feet" end },
        { name = "Finger0Slot", label = function() return FINGER0SLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_FINGER_1 or "Ring 1" end },
        { name = "Finger1Slot", label = function() return FINGER1SLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_FINGER_2 or "Ring 2" end },
        { name = "Trinket0Slot", label = function() return TRINKET0SLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_TRINKET_1 or "Trinket 1" end },
        { name = "Trinket1Slot", label = function() return TRINKET1SLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_TRINKET_2 or "Trinket 2" end },
        { name = "MainHandSlot", label = function() return MAINHANDSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_MAIN_HAND or "Main Hand" end },
        { name = "SecondaryHandSlot", label = function() return SECONDARYHANDSLOT or L.FEATURE_VAULTLOOM_ARMORY_SLOT_OFF_HAND or "Off Hand" end },
    },
}

local TRACK_HINTS = {
    "explorer", "adventurer", "veteran", "champion", "hero", "myth", "mythic",
    "entdecker", "abenteurer", "veteran", "champion", "held", "mythisch",
    "explorateur", "aventurier", "veteran", "champion", "heros", "mythique",
    "explorador", "aventurero", "veterano", "campeon", "heroe", "mitico",
}

local ArmoryRuntime = {
    enabled = false,
    filterMode = "all",
    hooksReady = false,
    refreshQueued = false,
    slotCards = {},
    statRows = {},
}

local cachedEmptySocketLines = nil

local function applyBackdrop(frame, bgColor, borderColor)
    if not frame or not frame.SetBackdrop then
        return
    end

    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = false,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    bgColor = bgColor or COLORS.panel
    borderColor = borderColor or COLORS.border
    frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1)
    frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1)
end

local function createLabel(parent, fontObject, justify)
    local text = parent:CreateFontString(nil, "OVERLAY", fontObject or "GameFontHighlightSmall")
    text:SetJustifyH(justify or "LEFT")
    text:SetJustifyV("MIDDLE")
    return text
end

local function createActionButton(parent, text, width)
    local button = CreateFrame("Button", nil, parent, BACKDROP_TEMPLATE)
    button:SetSize(width or 86, 24)
    applyBackdrop(button, COLORS.panelSoft, COLORS.border)
    button.label = createLabel(button, "GameFontHighlightSmall", "CENTER")
    button.label:SetPoint("CENTER")
    button.label:SetText(text or "")
    button:SetScript("OnEnter", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], 1)
        end
    end)
    button:SetScript("OnLeave", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(COLORS.border[1], COLORS.border[2], COLORS.border[3], COLORS.border[4])
        end
    end)
    return button
end

local function setSegmentButtonSelected(button, selected)
    if not button then
        return
    end

    if button.SetBackdropColor then
        local bg = selected and COLORS.panel or COLORS.panelSoft
        button:SetBackdropColor(bg[1], bg[2], bg[3], selected and 0.98 or 0.86)
    end
    if button.SetBackdropBorderColor then
        local border = selected and COLORS.gold or COLORS.borderSoft
        button:SetBackdropBorderColor(border[1], border[2], border[3], selected and 0.95 or 0.70)
    end
    if button.label then
        local color = selected and COLORS.gold or COLORS.muted
        button.label:SetTextColor(color[1], color[2], color[3], 1)
    end
end

local function trimText(text)
    text = tostring(text or "")
    text = text:gsub("|A:[^|]+|a", "")
    text = text:gsub("|T[^|]+|t", "")
    text = text:gsub("|c%x%x%x%x%x%x%x%x", "")
    text = text:gsub("|r", "")
    text = text:gsub("%s+", " ")
    text = text:gsub("^%s+", ""):gsub("%s+$", "")
    return text
end

local function getEmptySocketLines()
    if cachedEmptySocketLines then
        return cachedEmptySocketLines
    end

    local lines = {}
    for globalName, value in pairs(_G) do
        if type(globalName) == "string"
            and type(value) == "string"
            and globalName:find("EMPTY_SOCKET_", 1, true)
        then
            lines[#lines + 1] = value
        end
    end

    cachedEmptySocketLines = lines
    return lines
end

local function getSlotInfo(slotName)
    if type(GetInventorySlotInfo) ~= "function" then
        return nil, nil
    end

    local ok, slotID, emptyTexture = pcall(GetInventorySlotInfo, slotName)
    if ok then
        return tonumber(slotID), emptyTexture
    end

    return nil, nil
end

local function getItemLinkName(itemLink)
    if type(itemLink) ~= "string" then
        return nil
    end

    return itemLink:match("%[(.-)%]") or itemLink
end

local function getItemIcon(itemLink, slotID, emptyTexture)
    if type(GetInventoryItemTexture) == "function" and slotID then
        local ok, texture = pcall(GetInventoryItemTexture, "player", slotID)
        if ok and texture then
            return texture
        end
    end

    if type(itemLink) == "string" and C_Item and type(C_Item.GetItemInfoInstant) == "function" then
        local ok, _, _, _, _, icon = pcall(C_Item.GetItemInfoInstant, itemLink)
        if ok and icon then
            return icon
        end
    end

    return emptyTexture or "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function getItemQuality(itemLink, slotID)
    if type(GetInventoryItemQuality) == "function" and slotID then
        local ok, quality = pcall(GetInventoryItemQuality, "player", slotID)
        if ok and quality then
            return tonumber(quality)
        end
    end

    if type(itemLink) == "string" and type(GetItemInfo) == "function" then
        local _, _, quality = GetItemInfo(itemLink)
        return tonumber(quality)
    end

    return nil
end

local function getQualityColor(quality)
    local color = quality and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] or nil
    if color then
        return { color.r, color.g, color.b, 1 }
    end
    return COLORS.borderSoft
end

local function getEquippedItemLevel(slotID, itemLink)
    local itemLevel = nil
    if type(itemLink) == "string" and type(GetDetailedItemLevelInfo) == "function" then
        local ok, resolvedLevel = pcall(GetDetailedItemLevelInfo, itemLink)
        if ok then
            itemLevel = tonumber(resolvedLevel)
        end
    end

    if not itemLevel and C_Item and ItemLocation and type(ItemLocation.CreateFromEquipmentSlot) == "function" then
        local itemLocation = ItemLocation:CreateFromEquipmentSlot(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

    return itemLevel and math.floor(itemLevel + 0.5) or nil
end

local function getInventoryTooltipLines(slotID, itemLink)
    local lines = {}

    if C_TooltipInfo and type(C_TooltipInfo.GetInventoryItem) == "function" then
        local ok, tooltipData = pcall(C_TooltipInfo.GetInventoryItem, "player", slotID)
        if ok and type(tooltipData) == "table" and type(tooltipData.lines) == "table" then
            for _, lineData in ipairs(tooltipData.lines) do
                local text = lineData and lineData.leftText
                if type(text) == "string" and text ~= "" then
                    lines[#lines + 1] = text
                end
            end
        end
    end

    if #lines > 0 or type(itemLink) ~= "string" then
        return lines
    end

    local tooltip = _G.VaultloomArmoryScanTooltip
    if not tooltip then
        tooltip = CreateFrame("GameTooltip", "VaultloomArmoryScanTooltip", nil, "GameTooltipTemplate")
        tooltip:SetOwner(WorldFrame or UIParent, "ANCHOR_NONE")
    end

    tooltip:ClearLines()
    tooltip:SetHyperlink(itemLink)
    for index = 1, tooltip:NumLines() do
        local line = _G["VaultloomArmoryScanTooltipTextLeft" .. index]
        local text = line and line:GetText()
        if type(text) == "string" and text ~= "" then
            lines[#lines + 1] = text
        end
    end
    tooltip:ClearLines()

    return lines
end

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

    if C_Item and type(C_Item.GetItemGem) == "function" then
        local ok, _, gemLink = pcall(C_Item.GetItemGem, itemLink, gemIndex)
        if ok and gemLink then
            return gemLink
        end
    end

    if type(GetItemGem) == "function" then
        local ok, _, gemLink = pcall(GetItemGem, itemLink, gemIndex)
        if ok and gemLink then
            return gemLink
        end
    end

    return nil
end

local function getSocketInfo(slotID, itemLink, tooltipLines)
    local sockets = {}
    local filled = 0
    local empty = 0

    for gemIndex = 1, 4 do
        local gemLink = getInventoryItemGemLink(itemLink, gemIndex)
        if gemLink then
            local icon
            if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
                local okGemInfo, _, _, _, _, resolvedIcon = pcall(C_Item.GetItemInfoInstant, gemLink)
                if okGemInfo then
                    icon = resolvedIcon
                end
            end
            sockets[#sockets + 1] = {
                gemLink = gemLink,
                texture = icon or "Interface\\Icons\\INV_Misc_Gem_01",
                empty = false,
            }
            filled = filled + 1
        end
    end

    local emptySocketLines = getEmptySocketLines()
    for _, line in ipairs(tooltipLines or {}) do
        for _, socketLine in ipairs(emptySocketLines) do
            if type(socketLine) == "string" and socketLine ~= "" and line:find(socketLine, 1, true) then
                empty = empty + 1
                sockets[#sockets + 1] = {
                    texture = "Interface\\ItemSocketingFrame\\UI-EmptySocket-Prismatic",
                    empty = true,
                }
                break
            end
        end
    end

    local maxSockets = tonumber(SOCKETABLE_SLOT_MAX[slotID]) or 0
    local total = math.max(filled + empty, maxSockets > 0 and math.min(maxSockets, filled + empty) or 0)
    return {
        sockets = sockets,
        filled = filled,
        empty = empty,
        total = total,
    }
end

local function isOffhandWeapon(slotID, itemLink)
    if tonumber(slotID) ~= 17 or type(itemLink) ~= "string" then
        return false
    end

    local equipLoc, itemClassID, itemSubClassID
    if C_Item and type(C_Item.GetItemInfoInstant) == "function" then
        local _, _, _, resolvedEquipLoc, _, resolvedClassID, resolvedSubClassID = C_Item.GetItemInfoInstant(itemLink)
        equipLoc = resolvedEquipLoc
        itemClassID = resolvedClassID
        itemSubClassID = resolvedSubClassID
    end

    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 itemClassID == weaponClassID then
        return true
    end
    if itemClassID == armorClassID and (itemSubClassID == 6 or equipLoc == "INVTYPE_HOLDABLE" or itemSubClassID == 0) then
        return false
    end

    return equipLoc == "INVTYPE_WEAPON" or equipLoc == "INVTYPE_WEAPONOFFHAND"
end

local function canSlotBeEnchanted(slotID, itemLink)
    slotID = tonumber(slotID)
    if not slotID or ENCHANTABLE_SLOT_IDS[slotID] ~= true or type(itemLink) ~= "string" or itemLink == "" then
        return false
    end

    if slotID == 17 then
        return isOffhandWeapon(slotID, itemLink)
    end

    return true
end

local function getInventoryEnchantStatus(slotID, itemLink, tooltipLines)
    if type(itemLink) ~= "string" or itemLink == "" then
        return false, nil
    end

    local enchantID = tonumber(itemLink:match("item:%-?%d+:(%-?%d+)"))
    if not enchantID or enchantID <= 0 then
        return false, nil
    end

    for _, line in ipairs(tooltipLines or {}) do
        local cleanText = trimText(line)
        local lower = cleanText:lower()
        if cleanText ~= ""
            and not lower:find("socket", 1, true)
            and not lower:find("sockel", 1, true)
            and not lower:find("chasse", 1, true)
            and (line:find("Professions%-ChatIcon%-Quality%-Tier") or line:find("|A:Professions"))
        then
            cleanText = cleanText:gsub("^Enchanted:%s*", "")
            return true, cleanText
        end
    end

    return true, nil
end

local function getUpgradeTrack(tooltipLines)
    for _, rawLine in ipairs(tooltipLines or {}) do
        local line = trimText(rawLine)
        local lower = line:lower()
        if line:match("%d+%s*/%s*%d+") then
            for _, hint in ipairs(TRACK_HINTS) do
                if lower:find(hint, 1, true) then
                    return line
                end
            end
        end
    end

    return nil
end

local function getCompactUpgradeTrack(text)
    if type(text) ~= "string" or text == "" then
        return text
    end

    local compact = trimText(text:gsub("^%s*[^:]+:%s*", ""))
    if compact ~= "" and compact ~= text and compact:match("%d+%s*/%s*%d+") then
        return compact
    end

    return text
end

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

    local current, maximum = text:match("(%d+)%s*/%s*(%d+)")
    return tonumber(current), tonumber(maximum)
end

local function isUpgradeTrackIncomplete(text)
    local current, maximum = getUpgradeTrackProgress(text)
    if current and maximum and maximum > 0 and current < maximum then
        return true
    end
    return false
end

local function getUpgradeTrackColor(text)
    if type(text) ~= "string" or text == "" then
        return COLORS.muted
    end

    if isUpgradeTrackIncomplete(text) then
        return COLORS.warn
    end

    return COLORS.text
end

local function getSlotIssueFlags(data)
    local flags = {
        hasIssue = false,
        emptySlot = false,
        missingEnchant = false,
        emptySockets = 0,
        upgradeable = false,
    }

    if not data or data.empty == true then
        flags.emptySlot = true
        flags.hasIssue = true
        return flags
    end

    flags.missingEnchant = data.enchantable == true and data.enchanted ~= true
    flags.emptySockets = tonumber(data.socketInfo and data.socketInfo.empty) or 0
    flags.upgradeable = isUpgradeTrackIncomplete(data.upgradeTrack)
    flags.hasIssue = flags.missingEnchant or flags.emptySockets > 0 or flags.upgradeable
    return flags
end

local function collectSlotData(slotInfo)
    local slotID, emptyTexture = getSlotInfo(slotInfo.name)
    local itemLink = slotID and type(GetInventoryItemLink) == "function" and GetInventoryItemLink("player", slotID) or nil
    local label = slotInfo.label and slotInfo.label() or slotInfo.name
    if type(itemLink) ~= "string" or itemLink == "" then
        return {
            slotID = slotID,
            slotName = slotInfo.name,
            label = label,
            empty = true,
            icon = getItemIcon(nil, slotID, emptyTexture),
        }
    end

    if C_Item and type(C_Item.RequestLoadItemDataByID) == "function" then
        pcall(C_Item.RequestLoadItemDataByID, itemLink)
    end

    local tooltipLines = getInventoryTooltipLines(slotID, itemLink)
    local itemName = getItemLinkName(itemLink)
    local quality = getItemQuality(itemLink, slotID)
    local itemLevel = getEquippedItemLevel(slotID, itemLink)
    local enchantable = canSlotBeEnchanted(slotID, itemLink)
    local enchanted, enchantName = getInventoryEnchantStatus(slotID, itemLink, tooltipLines)
    local socketInfo = getSocketInfo(slotID, itemLink, tooltipLines)

    return {
        slotID = slotID,
        slotName = slotInfo.name,
        label = label,
        itemLink = itemLink,
        itemName = itemName,
        itemLevel = itemLevel,
        quality = quality,
        icon = getItemIcon(itemLink, slotID, emptyTexture),
        enchantable = enchantable,
        enchanted = enchanted == true,
        enchantName = enchantName,
        socketInfo = socketInfo,
        upgradeTrack = getCompactUpgradeTrack(getUpgradeTrack(tooltipLines)),
        empty = false,
    }
end

local function setTextureDesaturated(texture, desaturated)
    if texture and texture.SetDesaturated then
        texture:SetDesaturated(desaturated == true)
    end
end

local function hideSlotStatus(card)
    if not card then
        return
    end
    if card.enchantStatus then
        card.enchantStatus.vlTitle = nil
        card.enchantStatus.vlText = nil
        card.enchantStatus:Hide()
    end
    for _, socket in ipairs(card.sockets or {}) do
        socket.vlGemLink = nil
        socket.vlEmpty = nil
        socket:Hide()
    end
end

local function layoutSlotSockets(card, enchantShown)
    if not card or not card.statusRail then
        return
    end

    for index, socket in ipairs(card.sockets or {}) do
        socket:ClearAllPoints()
        if enchantShown and card.enchantStatus then
            socket:SetPoint("RIGHT", card.enchantStatus, "LEFT", -4 - ((index - 1) * 15), 0)
        else
            socket:SetPoint("RIGHT", card.statusRail, "RIGHT", -((index - 1) * 15), 0)
        end
    end
end

local function applyEnchantStatus(card, data)
    if not card or not card.enchantStatus then
        return false
    end

    if not data or data.enchantable ~= true then
        card.enchantStatus.vlTitle = nil
        card.enchantStatus.vlText = nil
        card.enchantStatus:Hide()
        return false
    end

    local ready = data.enchanted == true
    card.enchantStatus.icon:SetTexture(ready and "Interface\\RaidFrame\\ReadyCheck-Ready" or "Interface\\RaidFrame\\ReadyCheck-NotReady")
    card.enchantStatus.vlTitle = ready
        and (L.FEATURE_VAULTLOOM_ARMORY_ENCHANT_OK or "Enchant OK")
        or (L.FEATURE_VAULTLOOM_ARMORY_ENCHANT_MISSING or "Missing enchant")
    card.enchantStatus.vlText = data.enchantName
    if card.enchantStatus.SetBackdropBorderColor then
        local color = ready and COLORS.good or COLORS.bad
        card.enchantStatus:SetBackdropBorderColor(color[1], color[2], color[3], 0.95)
    end
    card.enchantStatus:Show()
    return true
end

local function setSlotCardBorder(card, color, alpha)
    if not card or not card.SetBackdropBorderColor then
        return
    end
    color = color or COLORS.borderSoft
    card:SetBackdropBorderColor(color[1], color[2], color[3], alpha or color[4] or 0.70)
end

local function applySlotCard(card, data)
    if not card or type(data) ~= "table" then
        return
    end

    local issueFlags = getSlotIssueFlags(data)
    card.vlIssueFlags = issueFlags
    card.vlHasIssue = issueFlags.hasIssue == true
    card.slotID = data.slotID
    card.vlSlotName = data.slotName
    card.vlItemLevel = data.itemLevel
    card.itemLink = data.itemLink
    card.icon:SetTexture(data.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    card.slot:SetText(data.label or "")

    if data.empty then
        card.name:SetText(L.FEATURE_VAULTLOOM_ARMORY_EMPTY_SLOT or "Empty")
        card.name:SetTextColor(COLORS.muted[1], COLORS.muted[2], COLORS.muted[3], 1)
        card.level:SetText("--")
        card.detail:SetText(L.FEATURE_VAULTLOOM_ARMORY_NO_ITEM or "No item equipped")
        card.detail:SetTextColor(COLORS.muted[1], COLORS.muted[2], COLORS.muted[3], 1)
        setTextureDesaturated(card.icon, true)
        setSlotCardBorder(card, COLORS.borderSoft, 0.55)
        hideSlotStatus(card)
        return
    end

    setTextureDesaturated(card.icon, false)
    local qualityColor = getQualityColor(data.quality)
    card.name:SetText(data.itemName or data.itemLink or "")
    card.name:SetTextColor(qualityColor[1], qualityColor[2], qualityColor[3], 1)
    card.level:SetText(data.itemLevel and tostring(data.itemLevel) or "?")

    local parts = {}
    local socketInfo = data.socketInfo or {}
    if data.upgradeTrack and data.upgradeTrack ~= "" then
        parts[#parts + 1] = data.upgradeTrack
    end

    card.detail:SetText(#parts > 0 and table.concat(parts, "  |  ") or (L.FEATURE_VAULTLOOM_ARMORY_READY or "Ready"))
    local detailColor = getUpgradeTrackColor(data.upgradeTrack)
    card.detail:SetTextColor(detailColor[1], detailColor[2], detailColor[3], 1)
    if issueFlags.missingEnchant then
        setSlotCardBorder(card, COLORS.bad, 0.62)
    elseif issueFlags.emptySockets > 0 or issueFlags.upgradeable then
        setSlotCardBorder(card, COLORS.warn, 0.58)
    else
        setSlotCardBorder(card, COLORS.borderSoft, 0.50)
    end

    local enchantShown = applyEnchantStatus(card, data)
    layoutSlotSockets(card, enchantShown)
    for index, socket in ipairs(card.sockets or {}) do
        local visual = socketInfo.sockets and socketInfo.sockets[index]
        if visual then
            socket.icon:SetTexture(visual.texture or "Interface\\ItemSocketingFrame\\UI-EmptySocket-Prismatic")
            socket:SetShown(true)
            socket.vlGemLink = visual.gemLink
            socket.vlEmpty = visual.empty == true
        else
            socket.vlGemLink = nil
            socket.vlEmpty = nil
            socket:Hide()
        end
    end
end

local function createSlotCard(parent)
    local card = CreateFrame("Button", nil, parent, BACKDROP_TEMPLATE)
    card:SetHeight(58)
    card:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    applyBackdrop(card, COLORS.panelSoft, COLORS.borderSoft)

    card.icon = card:CreateTexture(nil, "ARTWORK")
    card.icon:SetSize(42, 42)
    card.icon:SetPoint("LEFT", 7, 0)
    card.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    card.slot = createLabel(card, "GameFontDisableSmall", "LEFT")
    card.slot:SetPoint("TOPLEFT", card.icon, "TOPRIGHT", 8, -1)
    card.slot:SetPoint("TOPRIGHT", -56, -1)

    card.name = createLabel(card, "GameFontHighlightSmall", "LEFT")
    card.name:SetPoint("TOPLEFT", card.slot, "BOTTOMLEFT", 0, -1)
    card.name:SetPoint("TOPRIGHT", -56, -13)
    if card.name.SetWordWrap then
        card.name:SetWordWrap(false)
    end
    if card.name.SetMaxLines then
        card.name:SetMaxLines(1)
    end

    card.detail = createLabel(card, "GameFontDisableSmall", "LEFT")
    card.detail:SetPoint("TOPLEFT", card.name, "BOTTOMLEFT", 0, -3)
    card.detail:SetPoint("TOPRIGHT", -84, -31)
    if card.detail.SetWordWrap then
        card.detail:SetWordWrap(false)
    end
    if card.detail.SetMaxLines then
        card.detail:SetMaxLines(1)
    end

    card.level = createLabel(card, "GameFontNormal", "CENTER")
    card.level:SetPoint("TOPRIGHT", -8, -8)
    card.level:SetSize(38, 18)
    card.level:SetTextColor(COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], 1)

    card.statusRail = CreateFrame("Frame", nil, card)
    card.statusRail:SetSize(78, 16)
    card.statusRail:SetPoint("BOTTOMRIGHT", -8, 6)

    card.enchantStatus = CreateFrame("Button", nil, card.statusRail, BACKDROP_TEMPLATE)
    card.enchantStatus:SetSize(15, 15)
    card.enchantStatus:SetPoint("RIGHT", card.statusRail, "RIGHT", 0, 0)
    card.enchantStatus:EnableMouse(true)
    applyBackdrop(card.enchantStatus, { 0.02, 0.02, 0.025, 0.92 }, COLORS.borderSoft)
    card.enchantStatus.icon = card.enchantStatus:CreateTexture(nil, "ARTWORK")
    card.enchantStatus.icon:SetPoint("TOPLEFT", 1, -1)
    card.enchantStatus.icon:SetPoint("BOTTOMRIGHT", -1, 1)
    card.enchantStatus:SetScript("OnEnter", function(selfStatus)
        GameTooltip:SetOwner(selfStatus, "ANCHOR_RIGHT")
        GameTooltip:AddLine(selfStatus.vlTitle or (L.FEATURE_VAULTLOOM_ARMORY_ENCHANT_OK or "Enchant OK"), COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], true)
        if selfStatus.vlText and selfStatus.vlText ~= "" then
            GameTooltip:AddLine(selfStatus.vlText, COLORS.text[1], COLORS.text[2], COLORS.text[3], true)
        end
        GameTooltip:Show()
    end)
    card.enchantStatus:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
    card.enchantStatus:Hide()

    card.sockets = {}
    for index = 1, 4 do
        local socket = CreateFrame("Button", nil, card.statusRail, BACKDROP_TEMPLATE)
        socket:SetSize(13, 13)
        socket:SetPoint("RIGHT", card.enchantStatus, "LEFT", -4 - ((index - 1) * 15), 0)
        socket:EnableMouse(true)
        applyBackdrop(socket, { 0.02, 0.02, 0.025, 0.92 }, COLORS.borderSoft)
        socket.icon = socket:CreateTexture(nil, "ARTWORK")
        socket.icon:SetPoint("TOPLEFT", 1, -1)
        socket.icon:SetPoint("BOTTOMRIGHT", -1, 1)
        socket.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        socket:SetScript("OnEnter", function(selfSocket)
            GameTooltip:SetOwner(selfSocket, "ANCHOR_RIGHT")
            if selfSocket.vlGemLink then
                GameTooltip:SetHyperlink(selfSocket.vlGemLink)
            else
                GameTooltip:AddLine(L.FEATURE_VAULTLOOM_ARMORY_EMPTY_SOCKET or "Empty socket", COLORS.warn[1], COLORS.warn[2], COLORS.warn[3], true)
            end
            GameTooltip:Show()
        end)
        socket:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
        socket:Hide()
        card.sockets[index] = socket
    end

    card:SetScript("OnEnter", function(selfCard)
        if selfCard.SetBackdropColor then
            selfCard:SetBackdropColor(0.085, 0.078, 0.085, 0.98)
        end
        if selfCard.slotID and selfCard.itemLink and GameTooltip then
            GameTooltip:SetOwner(selfCard, "ANCHOR_RIGHT")
            GameTooltip:SetInventoryItem("player", selfCard.slotID)
            GameTooltip:Show()
        end
    end)
    card:SetScript("OnLeave", function(selfCard)
        if selfCard.SetBackdropColor then
            selfCard:SetBackdropColor(COLORS.panelSoft[1], COLORS.panelSoft[2], COLORS.panelSoft[3], COLORS.panelSoft[4])
        end
        GameTooltip:Hide()
    end)
    card:SetScript("OnClick", function(selfCard)
        if selfCard.itemLink and type(HandleModifiedItemClick) == "function" and HandleModifiedItemClick(selfCard.itemLink) then
            return
        end
        if selfCard.itemLink and IsShiftKeyDown and IsShiftKeyDown() and DressUpItemLink then
            DressUpItemLink(selfCard.itemLink)
        end
    end)

    return card
end

local function createPanel(parent, titleText, unframed)
    local panel = CreateFrame("Frame", nil, parent, BACKDROP_TEMPLATE)
    if unframed ~= true then
        applyBackdrop(panel, COLORS.panel, COLORS.border)
    end
    panel.title = createLabel(panel, "GameFontNormal", "LEFT")
    panel.title:SetPoint("TOPLEFT", 12, -10)
    panel.title:SetPoint("TOPRIGHT", -12, -10)
    panel.title:SetTextColor(COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], 1)
    panel.title:SetText(titleText or "")
    return panel
end

local function createModel(parent)
    local model
    for _, frameType in ipairs({ "DressUpModel", "PlayerModel" }) do
        local ok, created = pcall(CreateFrame, frameType, nil, parent)
        if ok and created then
            model = created
            break
        end
    end
    if model then
        model:SetPoint("TOPLEFT", 10, -34)
        model:SetPoint("BOTTOMRIGHT", -10, 10)
        return model
    end

    local fallback = CreateFrame("Frame", nil, parent, BACKDROP_TEMPLATE)
    fallback:SetPoint("TOPLEFT", 10, -34)
    fallback:SetPoint("BOTTOMRIGHT", -10, 10)
    applyBackdrop(fallback, { 0.02, 0.02, 0.025, 0.75 }, COLORS.borderSoft)
    fallback.text = createLabel(fallback, "GameFontDisableLarge", "CENTER")
    fallback.text:SetPoint("CENTER")
    fallback.text:SetText(L.FEATURE_VAULTLOOM_ARMORY_MODEL_UNAVAILABLE or "3D model unavailable")
    return fallback
end

local function createStatRow(parent)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(18)
    row.label = createLabel(row, "GameFontDisableSmall", "LEFT")
    row.label:SetPoint("LEFT", 0, 0)
    row.label:SetPoint("RIGHT", row, "CENTER", -6, 0)
    row.value = createLabel(row, "GameFontHighlightSmall", "RIGHT")
    row.value:SetPoint("LEFT", row, "CENTER", 6, 0)
    row.value:SetPoint("RIGHT", 0, 0)
    return row
end

local function formatPercent(value)
    value = tonumber(value)
    if not value then
        return "--"
    end
    return string.format("%.1f%%", value)
end

local function getRatingBonus(globalName)
    local rating = _G[globalName]
    if rating and type(GetCombatRatingBonus) == "function" then
        local ok, value = pcall(GetCombatRatingBonus, rating)
        if ok then
            return tonumber(value)
        end
    end
    return nil
end

local function getAttribute(statIndex)
    if type(UnitStat) ~= "function" then
        return nil
    end
    local ok, _, effective = pcall(UnitStat, "player", statIndex)
    if ok then
        return tonumber(effective)
    end
    return nil
end

local function setStatRow(row, label, value, color)
    if not row then
        return
    end
    row.label:SetText(label or "")
    row.value:SetText(value or "--")
    color = color or COLORS.text
    row.value:SetTextColor(color[1], color[2], color[3], color[4] or 1)
end

local function getAverageItemLevelText()
    if type(GetAverageItemLevel) ~= "function" then
        return "--"
    end
    local ok, avg, equipped = pcall(GetAverageItemLevel)
    if not ok then
        return "--"
    end
    local equippedLevel = tonumber(equipped) or tonumber(avg)
    if equippedLevel then
        return string.format("%.1f", equippedLevel)
    end
    return "--"
end

function ArmoryRuntime:RefreshStats(summary)
    local rows = self.statRows or {}
    summary = summary or {}
    local crit = type(GetCritChance) == "function" and GetCritChance() or nil
    local haste = type(GetHaste) == "function" and GetHaste() or nil
    local mastery = type(GetMasteryEffect) == "function" and GetMasteryEffect() or nil
    local versatility = getRatingBonus("CR_VERSATILITY_DAMAGE_DONE")
    local leech = getRatingBonus("CR_LIFESTEAL")
    local avoidance = getRatingBonus("CR_AVOIDANCE")
    local speed = getRatingBonus("CR_SPEED")

    local missingEnchants = tonumber(summary.missingEnchants) or 0
    local emptySockets = tonumber(summary.emptySockets) or 0

    setStatRow(rows[1], L.FEATURE_VAULTLOOM_ARMORY_STAT_EQUIPPED or "Equipped", string.format("%d/%d", tonumber(summary.equipped) or 0, tonumber(summary.totalSlots) or 0), COLORS.text)
    setStatRow(rows[2], L.FEATURE_VAULTLOOM_ARMORY_STAT_AVG_ILVL or "Average ilvl", getAverageItemLevelText(), COLORS.gold)
    setStatRow(rows[3], L.FEATURE_VAULTLOOM_ARMORY_STAT_MISSING_ENCHANTS or "Missing enchants", tostring(missingEnchants), missingEnchants > 0 and COLORS.bad or COLORS.good)
    setStatRow(rows[4], L.FEATURE_VAULTLOOM_ARMORY_STAT_EMPTY_SOCKETS or "Empty sockets", tostring(emptySockets), emptySockets > 0 and COLORS.warn or COLORS.good)
    setStatRow(rows[5], L.FEATURE_VAULTLOOM_ARMORY_STAT_STRENGTH or "Strength", getAttribute(1) or "--", COLORS.text)
    setStatRow(rows[6], L.FEATURE_VAULTLOOM_ARMORY_STAT_AGILITY or "Agility", getAttribute(2) or "--", COLORS.text)
    setStatRow(rows[7], L.FEATURE_VAULTLOOM_ARMORY_STAT_INTELLECT or "Intellect", getAttribute(4) or "--", COLORS.text)
    setStatRow(rows[8], L.FEATURE_VAULTLOOM_ARMORY_STAT_STAMINA or "Stamina", getAttribute(3) or "--", COLORS.text)
    setStatRow(rows[9], STAT_CRITICAL_STRIKE or L.FEATURE_VAULTLOOM_ARMORY_STAT_CRIT or "Crit", formatPercent(crit), COLORS.text)
    setStatRow(rows[10], STAT_HASTE or L.FEATURE_VAULTLOOM_ARMORY_STAT_HASTE or "Haste", formatPercent(haste), COLORS.text)
    setStatRow(rows[11], STAT_MASTERY or L.FEATURE_VAULTLOOM_ARMORY_STAT_MASTERY or "Mastery", formatPercent(mastery), COLORS.text)
    setStatRow(rows[12], STAT_VERSATILITY or L.FEATURE_VAULTLOOM_ARMORY_STAT_VERSATILITY or "Versatility", formatPercent(versatility), COLORS.text)
    setStatRow(rows[13], STAT_LIFESTEAL or L.FEATURE_VAULTLOOM_ARMORY_STAT_LEECH or "Leech", formatPercent(leech), COLORS.text)
    setStatRow(rows[14], STAT_AVOIDANCE or L.FEATURE_VAULTLOOM_ARMORY_STAT_AVOIDANCE or "Avoidance", formatPercent(avoidance), COLORS.text)
    setStatRow(rows[15], STAT_SPEED or L.FEATURE_VAULTLOOM_ARMORY_STAT_SPEED or "Speed", formatPercent(speed), COLORS.text)
    setStatRow(rows[16], L.FEATURE_VAULTLOOM_ARMORY_STAT_REFRESH or "Refresh", date and date("%H:%M:%S") or "--", COLORS.muted)
end

function ArmoryRuntime:RefreshSummaryBar(summary)
    local label = self.frame and self.frame.summaryLabel
    if not label then
        return
    end

    summary = summary or {}
    local equipped = tonumber(summary.equipped) or 0
    local totalSlots = tonumber(summary.totalSlots) or 0
    local missingEnchants = tonumber(summary.missingEnchants) or 0
    local emptySockets = tonumber(summary.emptySockets) or 0
    local upgradeable = tonumber(summary.upgradeable) or 0
    local emptySlots = tonumber(summary.emptySlots) or 0
    label:SetText(string.format(
        L.FEATURE_VAULTLOOM_ARMORY_CHECK_FORMAT or "Gear check: %d/%d equipped · %d enchants missing · %d empty sockets · %d upgradeable · %d empty slots",
        equipped,
        totalSlots,
        missingEnchants,
        emptySockets,
        upgradeable,
        emptySlots
    ))

    local issueCount = missingEnchants + emptySockets + upgradeable + emptySlots
    local color = issueCount > 0 and COLORS.warn or COLORS.good
    label:SetTextColor(color[1], color[2], color[3], 1)
end

function ArmoryRuntime:UpdateFilterButtons()
    if not self.frame then
        return
    end

    local mode = self.filterMode == "issues" and "issues" or "all"
    setSegmentButtonSelected(self.frame.filterAllButton, mode == "all")
    setSegmentButtonSelected(self.frame.filterIssuesButton, mode == "issues")
end

function ArmoryRuntime:ApplyFilter()
    if not self.frame then
        return
    end

    local showIssuesOnly = self.filterMode == "issues"
    local panelBySide = {
        left = self.frame.leftPanel,
        right = self.frame.rightPanel,
    }

    for side, cards in pairs(self.slotCards or {}) do
        local panel = panelBySide[side]
        local previous
        local visibleCount = 0
        for _, card in ipairs(cards or {}) do
            local showCard = not showIssuesOnly or card.vlHasIssue == true
            card:ClearAllPoints()
            card:SetPoint("LEFT", 10, 0)
            card:SetPoint("RIGHT", -10, 0)
            card:SetShown(showCard)
            if showCard then
                visibleCount = visibleCount + 1
                if previous then
                    card:SetPoint("TOP", previous, "BOTTOM", 0, -5)
                elseif panel and panel.title then
                    card:SetPoint("TOP", panel.title, "BOTTOM", 0, -10)
                end
                previous = card
            end
        end

        if panel and panel.emptyText then
            panel.emptyText:SetShown(showIssuesOnly and visibleCount == 0)
        end
    end

    self:UpdateFilterButtons()
end

function ArmoryRuntime:SetFilterMode(mode)
    self.filterMode = mode == "issues" and "issues" or "all"
    self:ApplyFilter()
end

function ArmoryRuntime:RefreshModel()
    local model = self.model
    if not model or not model.SetUnit then
        return
    end
    pcall(model.SetUnit, model, "player")
    if model.SetCamDistanceScale then
        pcall(model.SetCamDistanceScale, model, 1.15)
    end
    if model.SetPortraitZoom then
        pcall(model.SetPortraitZoom, model, 0)
    end
    if model.SetRotation then
        pcall(model.SetRotation, model, 0.08)
    end
end

function ArmoryRuntime:RefreshAll()
    if self.enabled ~= true or not self.frame or not self.frame:IsShown() then
        return
    end

    self:RefreshModel()

    local summary = {
        totalSlots = 0,
        equipped = 0,
        emptySlots = 0,
        missingEnchants = 0,
        emptySockets = 0,
        upgradeable = 0,
        issueSlots = 0,
    }

    for side, slotList in pairs(SLOT_COLUMNS) do
        for index, slotInfo in ipairs(slotList) do
            local card = self.slotCards[side] and self.slotCards[side][index]
            local data = collectSlotData(slotInfo)
            applySlotCard(card, data)
            local issueFlags = card and card.vlIssueFlags or getSlotIssueFlags(data)

            summary.totalSlots = summary.totalSlots + 1
            if issueFlags.hasIssue then
                summary.issueSlots = summary.issueSlots + 1
            end
            if issueFlags.emptySlot then
                summary.emptySlots = summary.emptySlots + 1
            end
            if data and not data.empty then
                summary.equipped = summary.equipped + 1
                if issueFlags.missingEnchant then
                    summary.missingEnchants = summary.missingEnchants + 1
                end
                summary.emptySockets = summary.emptySockets + (tonumber(issueFlags.emptySockets) or 0)
                if issueFlags.upgradeable then
                    summary.upgradeable = summary.upgradeable + 1
                end
            end
        end
    end

    self:RefreshStats(summary)
    self:RefreshSummaryBar(summary)
    self:ApplyFilter()
end

function ArmoryRuntime:QueueRefresh(delay)
    if self.enabled ~= true or self.refreshQueued == true then
        return
    end
    if not self.frame or not self.frame:IsShown() then
        return
    end

    self.refreshQueued = true
    if C_Timer and type(C_Timer.After) == "function" then
        C_Timer.After(delay or 0.18, function()
            ArmoryRuntime.refreshQueued = false
            ArmoryRuntime:RefreshAll()
        end)
    else
        self.refreshQueued = false
        self:RefreshAll()
    end
end

function ArmoryRuntime:EnsureFrame()
    if self.frame then
        return self.frame
    end

    local frame = CreateFrame("Frame", ARMORY_FRAME_NAME, UIParent, BACKDROP_TEMPLATE)
    frame:SetSize(ARMORY_WIDTH, ARMORY_HEIGHT)
    frame:SetPoint("CENTER")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetFrameStrata("DIALOG")
    applyBackdrop(frame, COLORS.bg, COLORS.border)
    if frame.SetBackdropColor then
        frame:SetBackdropColor(0, 0, 0, 0)
    end

    frame.bgTexture = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
    frame.bgTexture:SetAllPoints(frame)
    frame.bgTexture:SetTexture(ARMORY_BG_TEXTURE)
    frame.bgTexture:SetTexCoord(0, ARMORY_BG_TEX_RIGHT, 0, ARMORY_BG_TEX_BOTTOM)
    frame.bgTexture:SetVertexColor(1, 1, 1, 1)

    frame.bgTint = frame:CreateTexture(nil, "BACKGROUND", nil, 2)
    frame.bgTint:SetAllPoints(frame)
    frame.bgTint:SetTexture("Interface\\Buttons\\WHITE8X8")
    frame.bgTint:SetColorTexture(COLORS.bg[1], COLORS.bg[2], COLORS.bg[3], ARMORY_BG_TINT_ALPHA)

    frame:SetScript("OnDragStart", function(selfFrame)
        selfFrame:StartMoving()
    end)
    frame:SetScript("OnDragStop", function(selfFrame)
        selfFrame:StopMovingOrSizing()
    end)
    frame:SetScript("OnShow", function()
        ArmoryRuntime:QueueRefresh(0.05)
    end)
    frame:Hide()

    frame.title = createLabel(frame, "GameFontNormalLarge", "LEFT")
    frame.title:SetPoint("TOPLEFT", 18, -16)
    frame.title:SetPoint("TOPRIGHT", -180, -16)
    frame.title:SetTextColor(COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], 1)
    frame.title:SetText(L.FEATURE_VAULTLOOM_ARMORY_WINDOW_TITLE or "Vaultloom Armory")

    frame.subtitle = createLabel(frame, "GameFontDisableSmall", "LEFT")
    frame.subtitle:SetPoint("TOPLEFT", frame.title, "BOTTOMLEFT", 0, -3)
    frame.subtitle:SetPoint("TOPRIGHT", -180, -36)
    frame.subtitle:SetText(L.FEATURE_VAULTLOOM_ARMORY_WINDOW_SUBTITLE or "Extended character sheet, gear checks, and combat stats.")

    frame.summaryLabel = createLabel(frame, "GameFontDisableSmall", "LEFT")
    frame.summaryLabel:SetPoint("TOPLEFT", 18, -48)
    frame.summaryLabel:SetPoint("TOPRIGHT", -330, -48)
    frame.summaryLabel:SetText(L.FEATURE_VAULTLOOM_ARMORY_CHECK_PENDING or "Gear check pending")

    frame.closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    frame.closeButton:SetPoint("TOPRIGHT", -7, -7)

    frame.refreshButton = createActionButton(frame, L.FEATURE_VAULTLOOM_ARMORY_REFRESH or "Refresh", 112)
    frame.refreshButton:SetPoint("TOPRIGHT", -48, -14)
    frame.refreshButton:SetScript("OnClick", function()
        ArmoryRuntime:RefreshAll()
    end)

    frame.filterIssuesButton = createActionButton(frame, L.FEATURE_VAULTLOOM_ARMORY_FILTER_ISSUES or "Issues", 82)
    frame.filterIssuesButton:SetSize(82, 22)
    frame.filterIssuesButton:SetPoint("TOPRIGHT", -48, -44)
    frame.filterIssuesButton:SetScript("OnClick", function()
        ArmoryRuntime:SetFilterMode("issues")
    end)
    frame.filterIssuesButton:SetScript("OnLeave", function()
        ArmoryRuntime:UpdateFilterButtons()
    end)

    frame.filterAllButton = createActionButton(frame, L.FEATURE_VAULTLOOM_ARMORY_FILTER_ALL or "All", 54)
    frame.filterAllButton:SetSize(54, 22)
    frame.filterAllButton:SetPoint("RIGHT", frame.filterIssuesButton, "LEFT", -6, 0)
    frame.filterAllButton:SetScript("OnClick", function()
        ArmoryRuntime:SetFilterMode("all")
    end)
    frame.filterAllButton:SetScript("OnLeave", function()
        ArmoryRuntime:UpdateFilterButtons()
    end)

    frame.viewLabel = createLabel(frame, "GameFontDisableSmall", "RIGHT")
    frame.viewLabel:SetPoint("RIGHT", frame.filterAllButton, "LEFT", -8, 0)
    frame.viewLabel:SetSize(62, 18)
    frame.viewLabel:SetText(L.FEATURE_VAULTLOOM_ARMORY_VIEW_LABEL or "View:")
    frame.viewLabel:SetTextColor(COLORS.muted[1], COLORS.muted[2], COLORS.muted[3], 1)

    frame.leftPanel = createPanel(frame, L.FEATURE_VAULTLOOM_ARMORY_GEAR_LEFT or "Gear", true)
    frame.leftPanel:SetPoint("TOPLEFT", 16, -70)
    frame.leftPanel:SetSize(300, 540)

    frame.modelPanel = createPanel(frame, L.FEATURE_VAULTLOOM_ARMORY_MODEL_TITLE or "Character")
    frame.modelPanel:SetPoint("TOPLEFT", frame.leftPanel, "TOPRIGHT", 14, 0)
    frame.modelPanel:SetSize(430, 356)
    frame.model = createModel(frame.modelPanel)
    self.model = frame.model

    frame.statsPanel = createPanel(frame, L.FEATURE_VAULTLOOM_ARMORY_STATS_TITLE or "Stats")
    frame.statsPanel:SetPoint("TOPLEFT", frame.modelPanel, "BOTTOMLEFT", 0, -14)
    frame.statsPanel:SetSize(430, 170)

    frame.rightPanel = createPanel(frame, L.FEATURE_VAULTLOOM_ARMORY_GEAR_RIGHT or "Gear", true)
    frame.rightPanel:SetPoint("TOPLEFT", frame.modelPanel, "TOPRIGHT", 14, 0)
    frame.rightPanel:SetSize(300, 540)

    self.slotCards.left = {}
    self.slotCards.right = {}
    local function buildColumn(panel, side)
        panel.emptyText = createLabel(panel, "GameFontDisableSmall", "CENTER")
        panel.emptyText:SetPoint("TOPLEFT", panel.title, "BOTTOMLEFT", 10, -44)
        panel.emptyText:SetPoint("RIGHT", -10, 0)
        panel.emptyText:SetText(L.FEATURE_VAULTLOOM_ARMORY_FILTER_EMPTY or "No gear issues")
        panel.emptyText:Hide()

        local previous
        for index in ipairs(SLOT_COLUMNS[side]) do
            local card = createSlotCard(panel)
            card:SetPoint("LEFT", 10, 0)
            card:SetPoint("RIGHT", -10, 0)
            if index == 1 then
                card:SetPoint("TOP", panel.title, "BOTTOM", 0, -10)
            else
                card:SetPoint("TOP", previous, "BOTTOM", 0, -5)
            end
            self.slotCards[side][index] = card
            previous = card
        end
    end
    buildColumn(frame.leftPanel, "left")
    buildColumn(frame.rightPanel, "right")
    self:UpdateFilterButtons()

    self.statRows = {}
    for index = 1, 16 do
        local row = createStatRow(frame.statsPanel)
        local column = index <= 8 and 1 or 2
        local rowIndex = ((index - 1) % 8) + 1
        if column == 1 then
            row:SetPoint("TOPLEFT", 14, -36 - ((rowIndex - 1) * 16))
            row:SetPoint("RIGHT", frame.statsPanel, "CENTER", -8, 0)
        else
            row:SetPoint("TOPLEFT", frame.statsPanel, "TOP", 8, -36 - ((rowIndex - 1) * 16))
            row:SetPoint("RIGHT", -14, 0)
        end
        self.statRows[index] = row
    end

    self.frame = frame
    return frame
end

function ArmoryRuntime:ShowFrame()
    if self.enabled ~= true then
        return
    end
    local frame = self:EnsureFrame()
    frame:Show()
    self:RefreshAll()
end

function ArmoryRuntime:ToggleFrame()
    if self.enabled ~= true then
        return
    end
    local frame = self:EnsureFrame()
    if frame:IsShown() then
        frame:Hide()
    else
        self:ShowFrame()
    end
end

function ArmoryRuntime:PositionCharacterButton()
    local characterFrame = _G.CharacterFrame
    if not self.button or not characterFrame then
        return
    end

    self.button:ClearAllPoints()
    self.button:SetPoint("TOPLEFT", characterFrame, "TOPRIGHT", 6, -74)
    if self.button.SetFrameStrata and characterFrame.GetFrameStrata then
        self.button:SetFrameStrata(characterFrame:GetFrameStrata())
    end
    if self.button.SetFrameLevel and characterFrame.GetFrameLevel then
        self.button:SetFrameLevel((characterFrame:GetFrameLevel() or 1) + 35)
    end
end

function ArmoryRuntime:EnsureCharacterButton()
    local characterFrame = _G.CharacterFrame
    if not characterFrame or type(CreateFrame) ~= "function" then
        return nil
    end

    if self.button then
        self:PositionCharacterButton()
        self.button:SetShown(self.enabled == true and (not characterFrame.IsShown or characterFrame:IsShown()))
        return self.button
    end

    local button = CreateFrame("Button", ARMORY_BUTTON_NAME, UIParent, BACKDROP_TEMPLATE)
    button:SetSize(76, 24)
    button:SetClampedToScreen(true)
    applyBackdrop(button, COLORS.panelSoft, COLORS.border)
    button.label = createLabel(button, "GameFontHighlightSmall", "CENTER")
    button.label:SetPoint("CENTER")
    button.label:SetText(L.FEATURE_VAULTLOOM_ARMORY_BUTTON or "Vaultloom")
    button:SetScript("OnClick", function()
        ArmoryRuntime:ToggleFrame()
    end)
    button:SetScript("OnEnter", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], 1)
        end
        GameTooltip:SetOwner(selfButton, "ANCHOR_RIGHT")
        GameTooltip:AddLine(L.FEATURE_VAULTLOOM_ARMORY_TITLE or "Vaultloom Armory", COLORS.gold[1], COLORS.gold[2], COLORS.gold[3], true)
        GameTooltip:AddLine(L.FEATURE_VAULTLOOM_ARMORY_BUTTON_TOOLTIP or "Open the extended Vaultloom character sheet.", 0.88, 0.86, 0.82, true)
        GameTooltip:Show()
    end)
    button:SetScript("OnLeave", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(COLORS.border[1], COLORS.border[2], COLORS.border[3], COLORS.border[4])
        end
        GameTooltip:Hide()
    end)

    self.button = button
    self:PositionCharacterButton()
    button:SetShown(self.enabled == true and (not characterFrame.IsShown or characterFrame:IsShown()))
    return button
end

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

    local characterFrame = _G.CharacterFrame
    if not characterFrame or not characterFrame.HookScript then
        return
    end

    self.hooksReady = true
    characterFrame:HookScript("OnShow", function()
        ArmoryRuntime:EnsureCharacterButton()
        if ArmoryRuntime.button then
            ArmoryRuntime:PositionCharacterButton()
            ArmoryRuntime.button:SetShown(ArmoryRuntime.enabled == true)
        end
    end)
    characterFrame:HookScript("OnHide", function()
        if ArmoryRuntime.button then
            ArmoryRuntime.button:Hide()
        end
    end)
end

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

    if not self.eventFrame then
        self.eventFrame = CreateFrame("Frame")
        self.eventFrame:SetScript("OnEvent", function(_, event, ...)
            if event == "ADDON_LOADED" then
                local loadedName = ...
                if loadedName == "Blizzard_CharacterUI" then
                    ArmoryRuntime:EnsureHooks()
                    ArmoryRuntime:EnsureCharacterButton()
                end
                return
            end

            if event == "UNIT_INVENTORY_CHANGED" or event == "UNIT_STATS" then
                local unit = ...
                if unit ~= "player" then
                    return
                end
            end

            if event == "PLAYER_ENTERING_WORLD" then
                ArmoryRuntime:EnsureHooks()
                ArmoryRuntime:EnsureCharacterButton()
            end

            ArmoryRuntime:QueueRefresh(0.18)
        end)
    end

    self.eventFrame:UnregisterAllEvents()

    if not self.enabled then
        if self.button then
            self.button:Hide()
        end
        if self.frame then
            self.frame:Hide()
        end
        return
    end

    self:EnsureHooks()
    self:EnsureCharacterButton()
    self.eventFrame:RegisterEvent("ADDON_LOADED")
    self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    self.eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
    self.eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED")
    self.eventFrame:RegisterEvent("UNIT_STATS")
    self.eventFrame:RegisterEvent("COMBAT_RATING_UPDATE")
    self.eventFrame:RegisterEvent("ACTIVE_PLAYER_SPECIALIZATION_CHANGED")
end

ns:RegisterFeatureDefinition(ARMORY_FEATURE_ID, {
    id = ARMORY_FEATURE_ID,
    order = 10,
    category = "character",
    title = function() return L.FEATURE_VAULTLOOM_ARMORY_TITLE or "Vaultloom Armory" end,
    description = function() return L.FEATURE_VAULTLOOM_ARMORY_DESCRIPTION or "Adds a Vaultloom button to the Blizzard character frame and opens an extended character sheet with gear cards, item level, sockets, enchants, and a compact stat overview." end,
    note = function() return L.FEATURE_VAULTLOOM_ARMORY_NOTE or "Opens on demand and refreshes from equipment/stat events only." end,
    previewLabel = function() return L.FEATURE_VAULTLOOM_ARMORY_PREVIEW_LABEL or "Extended Character Sheet" end,
    previewNote = function() return L.FEATURE_VAULTLOOM_ARMORY_PREVIEW_NOTE or "Keeps the 3D model central, rebuilds gear and stat panels in Vaultloom style, and avoids permanent scanning." end,
    requiresReload = false,
    apply = function(isEnabled)
        ArmoryRuntime:SetEnabled(isEnabled == true)
    end,
})
