local _, privateDB = ...

-- ============================================================================
-- CRAFTING QUEUE PANEL (middle column of the Recipes tab)
-- Owns the queue ScrollBox, char-grouping, expand/collapse subsidiary recipes,
-- and the embedded Lumber widget at the bottom of the queue panel.
-- Extracted from HDG_RecipesTab.lua during the v2.42.x code-smell sweep.
-- ============================================================================

local CONFIG = privateDB.CONFIG
local L = HDG_L or {}
local HDG = _G.HousingDecorGuide

local HDG_QueuePanel = {}
privateDB.HDG_QueuePanel = HDG_QueuePanel

local function GetScheme() return HDG.Theme.GetCurrentScheme() end
local function RegisterWidget(w, t) HDG.Theme.RegisterWidget(w, t) end
local function ApplyFont(fs, s, ft, sz) HDG.Theme.ApplyFont(fs, s, ft, sz) end
local function Data() return HDG_UIHelpers.Data() end

local function IsPSLAvailable()
    return C_AddOns and C_AddOns.IsAddOnLoaded("ProfessionShoppingList")
        and SlashCmdList["PROFESSIONSHOPPINGLIST"]
end

local mainFrame = nil

-- Async tooltip rebuild registry: when a tooltip is opened on an item whose
-- C_Item.GetItemInfo isn't yet cached, register a rebuilder keyed by itemID.
-- The shared listener fires on GET_ITEM_INFO_RECEIVED and replays the build
-- if the same tooltip is still hovering the original owner.
local pendingTooltipRebuilds = {}
local itemInfoListener = CreateFrame("Frame")
itemInfoListener:RegisterEvent("GET_ITEM_INFO_RECEIVED")
itemInfoListener:SetScript("OnEvent", function(_, _, receivedID, success)
    local pending = pendingTooltipRebuilds[receivedID]
    if not pending then return end
    pendingTooltipRebuilds[receivedID] = nil
    if not success then return end
    if GameTooltip:IsShown() and GameTooltip:GetOwner() == pending.owner then
        pending.rebuildFn()
    end
end)

-- Open a profession recipe and run a callback once the profession UI is fully
-- populated. Synchronous if the trade-skill window is already open; otherwise
-- waits for TRADE_SKILL_LIST_UPDATE with a 2s fallback timer.
local function OpenRecipeAndThen(spellID, callback)
    if not (C_TradeSkillUI and C_TradeSkillUI.OpenRecipe) then return end

    local ready = C_TradeSkillUI.IsTradeSkillReady and C_TradeSkillUI.IsTradeSkillReady()
    if ready then
        C_TradeSkillUI.OpenRecipe(spellID)
        if callback then callback() end
        return
    end

    C_TradeSkillUI.OpenRecipe(spellID)
    local readyFrame = CreateFrame("Frame")
    local fallbackTimer
    local done = false
    local function finish()
        if done then return end
        done = true
        readyFrame:UnregisterAllEvents()
        readyFrame:SetScript("OnEvent", nil)
        if fallbackTimer then fallbackTimer:Cancel(); fallbackTimer = nil end
        if C_TradeSkillUI and C_TradeSkillUI.OpenRecipe then
            C_TradeSkillUI.OpenRecipe(spellID)
        end
        if callback then callback() end
    end
    readyFrame:RegisterEvent("TRADE_SKILL_LIST_UPDATE")
    readyFrame:SetScript("OnEvent", finish)
    fallbackTimer = C_Timer.NewTimer(2, finish)
end

local function GetQueueItemColor(item)
    if item.expansion then
        local expColor = HDG_ExpansionData.GetColor(item.expansion)
        return { expColor[1] * 0.5, expColor[2] * 0.5, expColor[3] * 0.5, 0.4 }
    end
    return { 0.08, 0.08, 0.08, 0.85 }
end

-- Lazy-built reverse index for legacy queue items missing a profession field
local itemToProfession = nil
local function GetProfessionForItem(itemID)
    if not itemID or not HDG_DecorDB then return nil end
    if not itemToProfession then
        itemToProfession = {}
        for _, recipe in pairs(HDG_DecorDB) do
            if recipe.itemID then itemToProfession[recipe.itemID] = recipe.profession end
        end
    end
    return itemToProfession[itemID]
end

-- Track which queue items are expanded to show subsidiary recipes
local expandedQueueItems = {}

local SUB_INDENT = 16
local SUB_HEIGHT = 18

-- ============================================================================
-- ScrollBox helpers: extent calculator, initializer, per-kind binders
-- Element kinds (one Button frame template, kind-dispatched on bind):
--   charHeader      - "Crafted by <character>" section divider
--   queueRow        - main queued item with optional [Craft] button
--   queueSubLabel   - "Craft Order:" label inside an expanded queue item
--   queueSubStep    - one indented intermediate-craft step
-- ============================================================================

local function GetQueueElementExtent(_, data)
    local kind = data.kind
    if kind == "charHeader" then
        return 18
    elseif kind == "queueRow" then
        return CONFIG.QUEUE_ITEM_HEIGHT + 2
    elseif kind == "queueSubLabel" or kind == "queueSubStep" then
        return SUB_HEIGHT + 1
    end
    return 20
end

local function HideAllQueueKindWidgets(frame)
    if frame.charHeaderText then frame.charHeaderText:Hide() end
    if frame.expandIcon then frame.expandIcon:Hide() end
    if frame.profIcon then frame.profIcon:Hide() end
    if frame.nameText then frame.nameText:Hide() end
    if frame.craftBtn then frame.craftBtn:Hide() end
    if frame.subProfIcon then frame.subProfIcon:Hide() end
    if frame.subNameText then frame.subNameText:Hide() end
    if frame.subQtyText then frame.subQtyText:Hide() end
end

local function InitializeQueueFrame(frame)
    if frame._hdgInit then return end
    frame._hdgInit = true

    frame.bg = frame:CreateTexture(nil, "BACKGROUND")
    frame.bg:SetAllPoints()

    frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")

    -- charHeader widgets
    frame.charHeaderText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.charHeaderText:SetPoint("LEFT", 2, 0)
    frame.charHeaderText:SetJustifyH("LEFT")

    -- queueRow widgets
    frame.expandIcon = CreateFrame("Button", nil, frame)
    frame.expandIcon:SetSize(14, 14)
    frame.expandIcon:SetPoint("LEFT", 2, 0)
    frame.expandIcon:SetHighlightAtlas("common-button-collapseExpand-hover")
    frame.expandIcon:SetScript("OnClick", function()
        local item = frame._item
        if item then
            expandedQueueItems[item.itemID] = not expandedQueueItems[item.itemID]
            HDG_UI.RefreshUI()
        end
    end)

    frame.profIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.profIcon:SetSize(16, 16)
    frame.nameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.nameText:SetJustifyH("LEFT")

    -- Craft button (sub-Button with manual 3D bevel textures)
    frame.craftBtn = CreateFrame("Button", nil, frame, "BackdropTemplate")
    frame.craftBtn:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    frame.craftBtn.highlight = frame.craftBtn:CreateTexture(nil, "ARTWORK")
    frame.craftBtn.highlight:SetTexture("Interface\\Buttons\\WHITE8x8")
    frame.craftBtn.highlight:SetPoint("TOPLEFT", 1, -1)
    frame.craftBtn.highlight:SetPoint("TOPRIGHT", -1, -1)
    frame.craftBtn.highlight:SetHeight(6)
    frame.craftBtn.shadow = frame.craftBtn:CreateTexture(nil, "ARTWORK")
    frame.craftBtn.shadow:SetTexture("Interface\\Buttons\\WHITE8x8")
    frame.craftBtn.shadow:SetPoint("BOTTOMLEFT", 1, 1)
    frame.craftBtn.shadow:SetPoint("BOTTOMRIGHT", -1, 1)
    frame.craftBtn.shadow:SetHeight(4)
    frame.craftBtn.glow = frame.craftBtn:CreateTexture(nil, "BACKGROUND", nil, -1)
    frame.craftBtn.glow:SetTexture("Interface\\Buttons\\WHITE8x8")
    frame.craftBtn.glow:SetPoint("TOPLEFT", -2, 2)
    frame.craftBtn.glow:SetPoint("BOTTOMRIGHT", 2, -2)
    frame.craftBtn.btnText = frame.craftBtn:CreateFontString(nil, "OVERLAY", "GameFontNormal")

    frame.craftBtn:SetScript("OnEnter", function(self)
        local item = frame._item
        if not item then return end
        self:SetBackdropColor(0.2, 0.85, 0.2, 1)
        self:SetBackdropBorderColor(0.15, 0.65, 0.15, 1)
        self.glow:SetVertexColor(0.4, 1, 0.4, 0.5)
        GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
        GameTooltip:SetText(HDG.Theme.C_Success() .. L["RECIPE_TIP_CRAFT_NOW"] .. "|r", 1, 1, 1)
        GameTooltip:AddLine(string.format("%s", frame._localizedName or ""), 1, 1, 1)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(HDG.Theme.C_Warning() .. string.format(L["RECIPE_TIP_QUEUED"], item.quantity) .. "|r", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(HDG.Theme.C_Success() .. string.format(L["RECIPE_TIP_CAN_CRAFT"], frame._maxCraftable or 0) .. "|r", 0.7, 0.7, 0.7)
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine(L["RECIPE_TIP_CLICK_CONFIRM"], 0.7, 0.7, 0.7)
        GameTooltip:AddLine(L["RECIPE_TIP_SHIFT_OPEN_PROF"], 0.7, 0.7, 0.7)
        GameTooltip:Show()
    end)
    frame.craftBtn:SetScript("OnLeave", function(self)
        self:SetBackdropColor(0.15, 0.7, 0.15, 1)
        self:SetBackdropBorderColor(0.1, 0.5, 0.1, 1)
        self.glow:SetVertexColor(0.3, 1, 0.3, 0.3)
        GameTooltip:Hide()
    end)
    frame.craftBtn:SetScript("OnMouseDown", function(self)
        self:SetBackdropColor(0.1, 0.5, 0.1, 1)
        self.highlight:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.4), CreateColor(0.1, 0.5, 0.1, 0))
        self.shadow:SetGradient("VERTICAL", CreateColor(0.3, 0.8, 0.3, 0.3), CreateColor(0.1, 0.5, 0.1, 0))
        self.btnText:SetPoint("CENTER", 0, -1)
    end)
    frame.craftBtn:SetScript("OnMouseUp", function(self)
        self:SetBackdropColor(0.2, 0.85, 0.2, 1)
        self.highlight:SetGradient("VERTICAL", CreateColor(0.5, 1, 0.5, 0.4), CreateColor(0.2, 0.8, 0.2, 0))
        self.shadow:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.6), CreateColor(0.1, 0.5, 0.1, 0))
        self.btnText:SetPoint("CENTER", 0, 0)
    end)
    frame.craftBtn:SetScript("OnClick", function()
        local item = frame._item
        local idx = frame._itemIndex
        if not item or not item.spellID then return end
        if IsShiftKeyDown() then
            OpenRecipeAndThen(item.spellID)
        else
            OpenRecipeAndThen(item.spellID, function()
                privateDB.HDG_CraftDialog.Show(item, idx)
            end)
        end
    end)

    -- queueSub widgets (shared between subLabel and subStep)
    frame.subProfIcon = frame:CreateTexture(nil, "ARTWORK")
    frame.subProfIcon:SetSize(14, 14)
    frame.subProfIcon:SetPoint("LEFT", 4 + SUB_INDENT, 0)
    frame.subNameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.subNameText:SetPoint("LEFT", 22 + SUB_INDENT, 0)
    frame.subNameText:SetJustifyH("LEFT")
    frame.subQtyText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    frame.subQtyText:SetPoint("RIGHT", -4, 0)
    frame.subQtyText:SetJustifyH("RIGHT")

    frame:SetScript("OnClick", function(self, button)
        if self._kind ~= "queueRow" then return end
        local item = self._item
        if not item then return end
        if button == "RightButton" then
            if IsShiftKeyDown() then
                expandedQueueItems[item.itemID] = nil
                HDG.Store:Dispatch("REMOVE_FROM_CRAFTING_QUEUE", { itemID = item.itemID })
            elseif self._hasSubsidiaries then
                expandedQueueItems[item.itemID] = not expandedQueueItems[item.itemID]
                HDG_UI.RefreshUI()
            else
                HDG.Store:Dispatch("REMOVE_FROM_CRAFTING_QUEUE", { itemID = item.itemID })
            end
        elseif IsShiftKeyDown() then
            local newQty = item.quantity - 1
            if newQty <= 0 then expandedQueueItems[item.itemID] = nil end
            HDG.Store:Dispatch("UPDATE_CRAFTING_QUEUE_ITEM_QTY", { itemID = item.itemID, quantity = newQty })
        elseif item.spellID then
            OpenRecipeAndThen(item.spellID)
        end
    end)

    frame:SetScript("OnEnter", function(self)
        if self._kind == "queueRow" then
            local item = self._item
            if not item then return end
            self.bg:SetColorTexture(0.3, 0.3, 0.35, 0.8)
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)

            local function BuildTooltipContent()
                if item.reagents and #item.reagents > 0 then
                    GameTooltip:AddLine(HDG.Theme.C_Text() .. string.format(L["RECIPE_TIP_MATERIALS_N"], item.quantity) .. "|r")
                    for _, reagent in ipairs(item.reagents) do
                        local have = HDG_Data.GetItemCountWithVariants(reagent.id)
                        local need = (reagent.qty or 1) * item.quantity
                        local reagentName = reagent.name or Data().GetCachedItemName(reagent.id) or ("Item " .. reagent.id)
                        local color = have >= need and HDG.Theme.C_Success() or (have > 0 and HDG.Theme.C_Warning() or HDG.Theme.C_Error())
                        GameTooltip:AddDoubleLine("  " .. reagentName, string.format("%s%d/%d|r", color, have, need), 1, 1, 1, 1, 1, 1)
                    end
                    GameTooltip:AddLine(" ")
                end
                if item.spellID then
                    GameTooltip:AddLine(L["RECIPE_TIP_LEFT_OPEN_PROF"], 0.7, 0.7, 0.7)
                end
                GameTooltip:AddLine(L["RECIPE_TIP_SHIFT_REMOVE_ONE"], 0.7, 0.7, 0.7)
                if self._hasSubsidiaries then
                    GameTooltip:AddLine(HDG.Theme.C_Warning() .. "Right-click:|r " .. (self._isExpanded and "Collapse" or "Show craft order"), 0.7, 0.7, 0.7)
                    GameTooltip:AddLine(L["RECIPE_TIP_SHIFT_RIGHT_REMOVE_ALL"], 0.7, 0.7, 0.7)
                else
                    GameTooltip:AddLine(L["RECIPE_TIP_RIGHT_REMOVE_ALL"], 0.7, 0.7, 0.7)
                end
                GameTooltip:Show()
            end

            if item.itemID then
                local _, itemLink = C_Item.GetItemInfo(item.itemID)
                if itemLink then
                    GameTooltip:SetHyperlink(itemLink)
                    GameTooltip:AddLine(" ")
                    BuildTooltipContent()
                else
                    GameTooltip:SetItemByID(item.itemID)
                    C_Item.RequestLoadItemDataByID(item.itemID)
                    local owner = self
                    local itemID = item.itemID
                    pendingTooltipRebuilds[itemID] = {
                        owner = owner,
                        rebuildFn = function()
                            local _, link = C_Item.GetItemInfo(itemID)
                            if link then GameTooltip:SetHyperlink(link) end
                            GameTooltip:AddLine(" ")
                            BuildTooltipContent()
                            GameTooltip:Show()
                        end,
                    }
                end
            else
                GameTooltip:SetText(HDG.Theme.C_Accent() .. (item.name or "Unknown") .. "|r")
                BuildTooltipContent()
            end
        elseif self._kind == "queueSubStep" then
            local step = self._subStep
            local recipe = self._subRecipe
            self.bg:SetColorTexture(0.2, 0.2, 0.25, 0.9)
            GameTooltip:SetOwner(self, HDG_UIHelpers.TOOLTIP_ANCHOR)
            GameTooltip:SetText(step and step.name or "", 1, 1, 1)
            if recipe then
                local profLine = recipe.profession or ""
                if recipe.expansion then profLine = profLine .. " - " .. recipe.expansion end
                GameTooltip:AddLine(profLine, 0.7, 0.7, 0.7)
                if recipe.slots and #recipe.slots > 0 then
                    GameTooltip:AddLine(" ")
                    GameTooltip:AddLine(L["RECIPE_TIP_REAGENTS_PER_CRAFT"], 0.98, 0.84, 0.34)
                    for _, slot in ipairs(recipe.slots) do
                        if slot.type == "basic" and slot.itemID then
                            local reagentName = slot.name or HDG_PowerCrafting.GetItemName(slot.itemID) or ("Item " .. slot.itemID)
                            GameTooltip:AddDoubleLine("  " .. reagentName, "x" .. (slot.qty or 1), 0.8, 0.8, 0.8, 0.98, 0.84, 0.34)
                        end
                    end
                end
                if recipe.recipeID then
                    local knowers = HDG_Data.GetCharactersWhoKnowRecipe(recipe.recipeID, recipe.profession)
                    if knowers and #knowers > 0 then
                        local names = {}
                        for _, k in ipairs(knowers) do table.insert(names, k.name) end
                        GameTooltip:AddLine(" ")
                        GameTooltip:AddLine("Known by: " .. table.concat(names, ", "), 0.4, 0.8, 0.4)
                    end
                end
            end
            GameTooltip:Show()
        end
    end)

    frame:SetScript("OnLeave", function(self)
        if self._kind == "queueRow" and self._item then
            local c = GetQueueItemColor(self._item)
            self.bg:SetColorTexture(c[1], c[2], c[3], c[4])
        elseif self._kind == "queueSubStep" then
            local cPR = CONFIG.COLORS.PANEL_ROW
            self.bg:SetColorTexture(cPR[1], cPR[2], cPR[3], 0.7)
        end
        GameTooltip:Hide()
    end)
end

local function BindCharHeaderKind(frame, data, scheme)
    frame._kind = "charHeader"
    frame._item = nil
    frame.bg:SetColorTexture(0, 0, 0, 0)
    frame.charHeaderText:Show()
    ApplyFont(frame.charHeaderText, scheme, "small")
    frame.charHeaderText:SetText(data.text or "")
end

local function BindQueueRowKind(frame, data, scheme)
    local item = data.item
    frame._kind = "queueRow"
    frame._item = item
    frame._itemIndex = data.index
    frame._hasSubsidiaries = data.hasSubsidiaries
    frame._isExpanded = data.isExpanded
    frame._maxCraftable = data.maxCraftable
    frame._localizedName = data.localizedName

    local c = GetQueueItemColor(item)
    frame.bg:SetColorTexture(c[1], c[2], c[3], c[4])

    local CRAFT_BTN_WIDTH = 44
    local PROF_ICON_SIZE = 16
    local EXPAND_ICON_WIDTH = data.hasSubsidiaries and 14 or 0

    -- Layout order: profession icon -> expand arrow (if any) -> name
    local x = 4
    frame.profIcon:ClearAllPoints()
    frame.profIcon:SetPoint("LEFT", x, 0)
    HDG_ExpansionData.ApplyProfessionIcon(frame.profIcon, data.profession)
    frame.profIcon:Show()
    x = x + PROF_ICON_SIZE + 2

    if data.hasSubsidiaries then
        frame.expandIcon:ClearAllPoints()
        frame.expandIcon:SetPoint("LEFT", x, 0)
        frame.expandIcon:Show()
        frame.expandIcon:SetNormalAtlas("common-icon-rotateright")
        frame.expandIcon:SetPushedAtlas("common-icon-rotateright")
        if data.isExpanded then
            frame.expandIcon:GetNormalTexture():SetVertexColor(0.6, 1, 0.6, 1)
        else
            frame.expandIcon:GetNormalTexture():SetVertexColor(1, 1, 1, 1)
        end
        x = x + EXPAND_ICON_WIDTH + 2
    else
        frame.expandIcon:Hide()
    end

    frame.nameText:ClearAllPoints()
    frame.nameText:SetPoint("LEFT", x, 0)
    frame.nameText:SetPoint("RIGHT", -CRAFT_BTN_WIDTH, 0)
    ApplyFont(frame.nameText, scheme, "small")
    if scheme then
        frame.nameText:SetTextColor(scheme.text.r, scheme.text.g, scheme.text.b, scheme.text.a)
    end
    frame.nameText:SetText(string.format("%dx %s", item.quantity, data.localizedName))
    frame.nameText:Show()

    if data.isKnown and data.canCraft then
        local maxCraftable = data.maxCraftable or 0
        local craftText = maxCraftable > 99 and "99+" or tostring(maxCraftable)
        local btnWidth = 34 + (string.len(craftText) > 1 and 6 or 0) + (string.len(craftText) > 2 and 4 or 0)

        local craftBtn = frame.craftBtn
        craftBtn:Show()
        craftBtn:ClearAllPoints()
        craftBtn:SetSize(btnWidth, 16)
        craftBtn:SetPoint("RIGHT", -2, 0)

        if scheme then
            craftBtn:SetBackdropColor(scheme.success.r, scheme.success.g, scheme.success.b, 1)
            craftBtn:SetBackdropBorderColor(scheme.success.r * 0.7, scheme.success.g * 0.7, scheme.success.b * 0.7, 1)
        else
            craftBtn:SetBackdropColor(0.15, 0.7, 0.15, 1)
            craftBtn:SetBackdropBorderColor(0.1, 0.5, 0.1, 1)
        end
        craftBtn.highlight:SetGradient("VERTICAL", CreateColor(0.5, 1, 0.5, 0.4), CreateColor(0.2, 0.8, 0.2, 0))
        craftBtn.shadow:SetGradient("VERTICAL", CreateColor(0, 0.3, 0, 0.6), CreateColor(0.1, 0.5, 0.1, 0))
        craftBtn.glow:SetVertexColor(0.3, 1, 0.3, 0.3)

        local btnText = craftBtn.btnText
        btnText:ClearAllPoints()
        ApplyFont(btnText, scheme, "small", 9)
        btnText:SetPoint("CENTER", 0, 0)
        btnText:SetText(string.format(HDG.Theme.C_Text() .. "%s|r", craftText))
    else
        frame.craftBtn:Hide()
    end
end

local function BindQueueSubLabelKind(frame, data, scheme)
    frame._kind = "queueSubLabel"
    frame._item = nil
    frame.bg:SetColorTexture(0, 0, 0, 0)
    frame.subNameText:Show()
    ApplyFont(frame.subNameText, scheme, "small")
    frame.subNameText:SetText(HDG.Theme.C_Accent() .. L["RECIPE_CRAFT_ORDER"] .. "|r")
    frame.subNameText:SetTextColor(1, 1, 1, 1)
end

local function BindQueueSubStepKind(frame, data, scheme)
    local step = data.step
    local recipe = data.recipe
    frame._kind = "queueSubStep"
    frame._item = nil
    frame._subStep = step
    frame._subRecipe = recipe

    local cPR = CONFIG.COLORS.PANEL_ROW
    frame.bg:SetColorTexture(cPR[1], cPR[2], cPR[3], 0.7)

    frame.subProfIcon:Show()
    HDG_ExpansionData.ApplyProfessionIcon(frame.subProfIcon, recipe and recipe.profession)

    ApplyFont(frame.subNameText, scheme, "small")
    frame.subNameText:SetText((step.step or "?") .. ". " .. (step.name or ("Item " .. step.itemID)))
    if data.isStepKnown and scheme then
        frame.subNameText:SetTextColor(scheme.success.r, scheme.success.g, scheme.success.b, 1)
    elseif scheme then
        frame.subNameText:SetTextColor(scheme.text_dim.r, scheme.text_dim.g, scheme.text_dim.b, scheme.text_dim.a)
    end
    frame.subNameText:Show()

    ApplyFont(frame.subQtyText, scheme, "small")
    frame.subQtyText:SetText("x" .. (step.qty or 0))
    if scheme then
        frame.subQtyText:SetTextColor(scheme.warning.r, scheme.warning.g, scheme.warning.b, scheme.warning.a)
    end
    frame.subQtyText:Show()
end

local function BindQueueFrame(frame, data)
    HideAllQueueKindWidgets(frame)
    local scheme = GetScheme()
    local kind = data.kind
    if kind == "charHeader" then
        BindCharHeaderKind(frame, data, scheme)
    elseif kind == "queueRow" then
        BindQueueRowKind(frame, data, scheme)
    elseif kind == "queueSubLabel" then
        BindQueueSubLabelKind(frame, data, scheme)
    elseif kind == "queueSubStep" then
        BindQueueSubStepKind(frame, data, scheme)
    end
end

-- ============================================================================
-- DataProvider push helpers
-- ============================================================================

local function PushQueueItem(dp, itemData)
    local item = itemData.item
    local i = itemData.index
    local hasSubsidiaries = HDG_PowerCrafting.HasSubsidiaryRecipes(item)
    local isExpanded = hasSubsidiaries and expandedQueueItems[item.itemID] or false

    local isKnown = item.spellID and Data().PlayerKnowsRecipeBySpellID and Data().PlayerKnowsRecipeBySpellID(item.spellID) or false
    local canCraft, maxCraftable = HDG_Data.CanCraftRecipe(item)
    maxCraftable = maxCraftable or 0
    local profession = item.profession or GetProfessionForItem(item.itemID)
    local localizedName = Data().GetLocalizedName(item.itemID, item.name)

    dp:Insert({
        kind = "queueRow",
        item = item,
        index = i,
        hasSubsidiaries = hasSubsidiaries,
        isExpanded = isExpanded,
        isKnown = isKnown,
        canCraft = canCraft,
        maxCraftable = maxCraftable,
        profession = profession,
        localizedName = localizedName,
    })

    if isExpanded then
        local decorRecipe = { reagents = {} }
        for _, r in ipairs(item.reagents or {}) do
            decorRecipe.reagents[r.id] = { name = r.name, qty = (r.qty or 1) * item.quantity }
        end
        local craftingSteps = HDG_PowerCrafting.GetCraftingOrder(decorRecipe)

        if #craftingSteps > 0 then
            dp:Insert({ kind = "queueSubLabel" })
            for _, step in ipairs(craftingSteps) do
                local recipe = HDG_PowerCrafting.GetRecipeByItemID(step.itemID)
                local isStepKnown = recipe and HDG_Data.PlayerKnowsRecipeBySpellID(recipe.recipeID) or false
                dp:Insert({
                    kind = "queueSubStep",
                    step = step,
                    recipe = recipe,
                    isStepKnown = isStepKnown,
                })
            end
        end
    end
end

local function GroupQueueByCharacter(craftingQueue)
    local currentCharKey = HDG_Data.GetCharacterKey()
    local currentCharShort = currentCharKey:match("^([^-]+)") or currentCharKey
    local itemsByCharacter = {}
    local characterOrder = {}

    for i, item in ipairs(craftingQueue) do
        local crafters = {}
        if item.spellID then
            crafters = HDG_Data.GetCharactersWhoKnowRecipe(item.spellID, item.profession)
        end

        local primaryCrafter = "Unknown"
        if #crafters > 0 then
            for _, c in ipairs(crafters) do
                if c.isCurrent then
                    primaryCrafter = currentCharKey
                    break
                end
            end
            if primaryCrafter == "Unknown" then
                primaryCrafter = crafters[1].name
            end
        end

        if not itemsByCharacter[primaryCrafter] then
            itemsByCharacter[primaryCrafter] = {}
            table.insert(characterOrder, primaryCrafter)
        end
        table.insert(itemsByCharacter[primaryCrafter], { item = item, index = i })
    end

    -- Current char first, then alphabetical, "Unknown" last
    table.sort(characterOrder, function(a, b)
        if a == currentCharKey then return true end
        if b == currentCharKey then return false end
        if a == "Unknown" then return false end
        if b == "Unknown" then return true end
        return a < b
    end)

    return currentCharKey, currentCharShort, itemsByCharacter, characterOrder
end

-- ============================================================================
-- Public API
-- ============================================================================

function HDG_QueuePanel.Init(frame)
    mainFrame = frame
end

function HDG_QueuePanel.Create()
    local scheme = GetScheme()

    local layout = HDG.UI:TabPanel(mainFrame, {
        hasHeaderBar = false,
        hasScroll = false,
    })

    local queueFrame = layout.frame
    queueFrame:ClearAllPoints()
    queueFrame:SetPoint("TOPLEFT", mainFrame.recipeFrame, "TOPRIGHT", CONFIG.SPACING.PANEL_GAP, 0)
    queueFrame:SetSize(CONFIG.MIDDLE_PANEL_WIDTH, CONFIG.PANEL_HEIGHT)
    mainFrame.queueFrame = queueFrame

    local craftingQueueLabel = L["HEADER_CRAFTING_QUEUE"] or "Crafting Queue"
    local queueHeaderBg, queueTitle = HDG_UIHelpers.CreatePanelHeader(queueFrame, craftingQueueLabel, { center = true })
    mainFrame.queueHeaderBg = queueHeaderBg
    mainFrame.queueTitle = queueTitle
    mainFrame.craftingQueueLabel = craftingQueueLabel

    queueHeaderBg:EnableMouse(true)
    HDG_UIHelpers.SetupTooltip(queueHeaderBg, {
        title = HDG.Theme.C_Text() .. craftingQueueLabel .. "|r",
        desc  = {
            L["RECIPE_TIP_QUEUE_DESC"],
            " ",
            HDG.Theme.C_Warning() .. L["RECIPE_TIP_QUEUE_HOW"] .. "|r",
            L["RECIPE_TIP_QUEUE_ADD"],
            L["RECIPE_TIP_QUEUE_REMOVE"],
        },
    })

    -- Clear button
    local clearBtn = CreateFrame("Button", nil, queueFrame)
    clearBtn:SetSize(20, 20)
    clearBtn:SetPoint("TOPRIGHT", -4, -2)
    clearBtn:SetFrameLevel(queueFrame:GetFrameLevel() + 10)
    clearBtn:RegisterForClicks("AnyUp")
    local clearIcon = clearBtn:CreateTexture(nil, "ARTWORK")
    clearIcon:SetAllPoints()
    clearIcon:SetAtlas("common-icon-delete")
    clearBtn:SetScript("OnClick", function()
        HDG.Store:Dispatch("CLEAR_CRAFTING_QUEUE", {})
    end)
    HDG_UIHelpers.SetupTooltip(clearBtn, {
        title = HDG.Theme.C_Warning() .. (L["BTN_CLEAR"] or "Clear") .. " " .. craftingQueueLabel .. "|r",
        desc  = "Remove all items from crafting queue",
    })

    -- Send to PSL button (Profession Shopping List integration)
    local pslBtn = CreateFrame("Button", nil, queueFrame)
    pslBtn:SetSize(20, 20)
    pslBtn:SetPoint("TOPRIGHT", -28, -2)
    pslBtn:SetFrameLevel(queueFrame:GetFrameLevel() + 10)
    pslBtn:RegisterForClicks("AnyUp")
    local pslIcon = pslBtn:CreateTexture(nil, "ARTWORK")
    pslIcon:SetAllPoints()
    pslIcon:SetAtlas("Professions-Icon-Queue")
    pslBtn:SetScript("OnClick", function()
        local list = HDG.Store:GetCraftingQueue()
        local sent, skipped = 0, 0
        for _, item in ipairs(list) do
            if item.spellID then
                SlashCmdList["PROFESSIONSHOPPINGLIST"]("track " .. item.spellID .. " " .. (item.quantity or 1))
                sent = sent + 1
            else
                skipped = skipped + 1
            end
        end
        local msg = HDG.Theme.C_Accent() .. "[HDG]|r Sent " .. sent .. " recipe(s) to Profession Shopping List"
        if skipped > 0 then msg = msg .. " (" .. skipped .. " skipped)" end
        print(msg)
    end)
    HDG_UIHelpers.SetupTooltip(pslBtn, {
        title = HDG.Theme.C_Warning() .. L["RECIPE_TIP_SEND_PSL"] .. "|r",
        desc  = L["RECIPE_TIP_SEND_PSL_DESC"],
    })
    pslBtn:SetShown(IsPSLAvailable() and #HDG.Store:GetCraftingQueue() > 0)
    mainFrame.pslBtn = pslBtn

    -- Queue list scroll. Bottom adjusts based on lumber frame visibility (see
    -- HDG_UI.UpdateLumberLayout which repositions queueScrollBox by SetPoint).
    local lumberReservedHeight = 24 + (#HDG_ExpansionData.LUMBER_ORDER * 18) + 8 + 8

    local queueScrollBox = CreateFrame("Frame", nil, queueFrame, "WowScrollBoxList")
    queueScrollBox:SetPoint("TOPLEFT", 8, -28)
    queueScrollBox:SetPoint("BOTTOMRIGHT", -20, lumberReservedHeight)

    local queueScrollBar = CreateFrame("EventFrame", nil, queueFrame, "MinimalScrollBar")
    queueScrollBar:SetPoint("TOPLEFT", queueScrollBox, "TOPRIGHT", 2, 0)
    queueScrollBar:SetPoint("BOTTOMLEFT", queueScrollBox, "BOTTOMRIGHT", 2, 0)

    local queueView = CreateScrollBoxListLinearView(0, 0, 0, 0, 0)
    queueView:SetElementExtentCalculator(GetQueueElementExtent)
    queueView:SetElementInitializer("Button", function(frame, elementData)
        InitializeQueueFrame(frame)
        frame:SetWidth(queueScrollBox:GetWidth())
        BindQueueFrame(frame, elementData)
    end)

    ScrollUtil.InitScrollBoxListWithScrollBar(queueScrollBox, queueScrollBar, queueView)

    mainFrame.queueScrollBox = queueScrollBox
    mainFrame.queueScrollBar = queueScrollBar
    mainFrame.queueScrollView = queueView

    -- Embedded Lumber Frame (shown when NOT pinned)
    local lumberFrame = CreateFrame("Frame", nil, queueFrame, "BackdropTemplate")
    lumberFrame:SetPoint("BOTTOMLEFT", 8, 8)
    lumberFrame:SetPoint("BOTTOMRIGHT", -8, 8)
    local lumberCount = #HDG_ExpansionData.LUMBER_ORDER
    local embeddedLumberHeight = 24 + (lumberCount * 18) + 8
    lumberFrame:SetHeight(embeddedLumberHeight)
    HDG_UIHelpers.ApplyBackdrop(lumberFrame, "panel",
        {scheme.panel.r, scheme.panel.g, scheme.panel.b, 0.8},
        {scheme.border.r, scheme.border.g, scheme.border.b, scheme.border.a})
    RegisterWidget(lumberFrame, "Panel")
    mainFrame.lumberFrame = lumberFrame

    -- Pin button (top-right) - expand atlas; click to pin/expand the Lumber widget out
    local pinBtn = CreateFrame("Button", nil, lumberFrame)
    pinBtn:SetSize(20, 20)
    pinBtn:SetPoint("TOPRIGHT", -4, -4)

    local pinNormal = pinBtn:CreateTexture(nil, "ARTWORK")
    pinNormal:SetAllPoints()
    pinNormal:SetAtlas("RedButton-Expand")
    local pinPushed = pinBtn:CreateTexture(nil, "ARTWORK")
    pinPushed:SetAllPoints()
    pinPushed:SetAtlas("RedButton-Expand-Pressed")
    local pinHighlight = pinBtn:CreateTexture(nil, "HIGHLIGHT")
    pinHighlight:SetAllPoints()
    pinHighlight:SetAtlas("RedButton-Expand")
    pinHighlight:SetAlpha(0.3)
    pinBtn:SetNormalTexture(pinNormal)
    pinBtn:SetPushedTexture(pinPushed)
    pinBtn:SetHighlightTexture(pinHighlight)

    pinBtn:SetScript("OnClick", function()
        HDG.Store:Dispatch("SET_LUMBER_PINNED", { pinned = true })
        if HDG_LumberTracker then
            HDG_LumberTracker.ShowPinned()
        end
        if HDG_UI and HDG_UI.UpdateLumberLayout then
            HDG_UI.UpdateLumberLayout()
        end
        if HDG_ConfigTab and HDG_ConfigTab.UpdateLumberPinCheckbox then
            HDG_ConfigTab.UpdateLumberPinCheckbox()
        end
        print(HDG.Theme.C_Warning() .. "[Housing Decor Guide]|r " .. L["RECIPE_MSG_LUMBER_PINNED"])
    end)

    HDG_UIHelpers.SetupTooltip(pinBtn, {
        title = HDG.Theme.C_Warning() .. L["RECIPE_TIP_PIN_LUMBER"] .. "|r",
        desc  = {
            { text = L["RECIPE_TIP_PIN_LUMBER_DESC"], color = {0.7, 0.7, 0.7} },
            { text = L["RECIPE_TIP_PIN_LUMBER_NOTE"], color = {0.5, 0.8, 0.5} },
        },
    })

    lumberFrame.pinBtn = pinBtn

    -- Lumber column headers
    local lumberHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    lumberHeader:SetPoint("TOP", 0, -6)
    lumberHeader:SetPoint("LEFT", 4, 0)
    lumberHeader:SetWidth(CONFIG.LUMBER_COL_1)
    lumberHeader:SetJustifyH("LEFT")
    lumberHeader:SetText(HDG.Theme.C_Text() .. (L["COL_TYPE"] or "Type") .. "|r")
    ApplyFont(lumberHeader, scheme, "small")

    local queueHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    queueHeader:SetPoint("TOP", 0, -6)
    queueHeader:SetPoint("LEFT", 4 + CONFIG.LUMBER_COL_1, 0)
    queueHeader:SetWidth(CONFIG.LUMBER_COL_2)
    queueHeader:SetJustifyH("CENTER")
    queueHeader:SetText(HDG.Theme.C_Text() .. (L["COL_NEEDED"] or "Queue") .. "|r")
    ApplyFont(queueHeader, scheme, "small")

    local allHeader = lumberFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    allHeader:SetPoint("TOP", 0, -6)
    allHeader:SetPoint("LEFT", 4 + CONFIG.LUMBER_COL_1 + CONFIG.LUMBER_COL_2, 0)
    allHeader:SetWidth(CONFIG.LUMBER_COL_3)
    allHeader:SetJustifyH("CENTER")
    allHeader:SetText(HDG.Theme.C_Text() .. (L["COL_IN_STOCK"] or "Uncollected") .. "|r")
    ApplyFont(allHeader, scheme, "small")

    local lumberContent = CreateFrame("Frame", nil, lumberFrame)
    lumberContent:SetPoint("TOPLEFT", 4, -24)
    lumberContent:SetPoint("BOTTOMRIGHT", -4, 4)
    mainFrame.lumberContent = lumberContent

    return queueFrame
end

function HDG_QueuePanel.Refresh()
    if not mainFrame or not mainFrame.queueScrollBox then return end

    local queue = HDG.Store:GetCraftingQueue()
    if mainFrame.queueTitle and mainFrame.craftingQueueLabel then
        local count = #queue
        if count > 0 then
            mainFrame.queueTitle:SetText(mainFrame.craftingQueueLabel .. " " .. HDG.Theme.C_Accent() .. "(" .. count .. ")|r")
        else
            mainFrame.queueTitle:SetText(mainFrame.craftingQueueLabel)
        end
    end

    if mainFrame.pslBtn then
        mainFrame.pslBtn:SetShown(IsPSLAvailable() and #queue > 0)
    end

    local dp = CreateDataProvider()
    if #queue == 0 then
        mainFrame.queueScrollBox:SetDataProvider(dp, ScrollBoxConstants.RetainScrollPosition)
        return
    end

    local currentCharKey, _, itemsByCharacter, characterOrder = GroupQueueByCharacter(queue)

    for _, charKey in ipairs(characterOrder) do
        local items = itemsByCharacter[charKey]
        local charName = charKey:match("^([^-]+)") or charKey
        local realmName = charKey:match("-(.+)$") or ""
        local headerText
        if charKey == currentCharKey then
            headerText = HDG.Theme.C_Success() .. charName .. "|r " .. HDG.Theme.C_Dim() .. "- " .. realmName .. "|r " .. HDG.Theme.C_Warning() .. (L["QUEUE_YOU"] or "(you)") .. "|r"
        elseif charKey == "Unknown" then
            headerText = HDG.Theme.C_Error() .. (L["QUEUE_NOT_KNOWN"] or "Not Known") .. "|r"
        else
            headerText = HDG.Theme.C_Warning() .. charName .. "|r " .. HDG.Theme.C_Dim() .. "- " .. realmName .. "|r"
        end
        dp:Insert({ kind = "charHeader", text = headerText })
        for _, itemData in ipairs(items) do
            PushQueueItem(dp, itemData)
        end
    end

    mainFrame.queueScrollBox:SetDataProvider(dp, ScrollBoxConstants.RetainScrollPosition)
end
