-- UI/HelpPages/Overview/BagWorth/BagWorth.lua
-- What Farm? :: Overview Bag Worth section

local ADDON_NAME, ns = ...
local ADDON_WF = ns.WF

ADDON_WF.UI = ADDON_WF.UI or {}
ADDON_WF.UI.HelpPages = ADDON_WF.UI.HelpPages or {}
ADDON_WF.UI.HelpPages.OverviewSections = ADDON_WF.UI.HelpPages.OverviewSections or {}

local Sections = ADDON_WF.UI.HelpPages.OverviewSections

function Sections.BuildBagWorth(ctx, env)
  env = env or {}

  local UI = env.UI
  local WF = env.WF or ADDON_WF
  local Widgets = env.Widgets
  local Theme = env.Theme
  local Util = env.Util

  local Money = env.Money
  local C4 = env.C4
  local TEXT = env.TEXT
  local MUTED = env.MUTED
  local PANEL = env.PANEL
  local BORDER = env.BORDER or (Theme and Theme.border) or { 1, 1, 1, 0.12 }
  local ACCENT = env.ACCENT
  local rSC = env.rSC
  local tokDiv = env.tokDiv

  local MakeSectionTitle = env.MakeSectionTitle
  local MakeBodyText = env.MakeBodyText
  local MakeMutedText = env.MakeMutedText
  local MakeDivider = env.MakeDivider
  local EnsureSettingsDefaults = env.EnsureSettingsDefaults
  local TSMStringToLabel = env.TSMStringToLabel

  local _WF_ApplyGearOptionButton = env.ApplyGearOptionButton
  local _WF_GearOpt_SetHover = env.GearOptionSetHover
  local WF_ApplyBlueCheck = env.ApplyBlueCheck
  local _WF_TooltipFollow_Stop = env.TooltipFollowStop or function() end
  local WF_FontTemplate = env.WF_FontTemplate or function(wfName, blizzardName) return wfName or blizzardName end

  if not (UI and rSC and tokDiv and Widgets and MakeSectionTitle and MakeBodyText and MakeMutedText and C4 and TEXT and MUTED and ACCENT) then
    return { bBW = tokDiv, bottom = tokDiv }
  end

    -- Bag Worth (manual only)

    -- Bag Worth (manual only)
    local bwHeader = CreateFrame("Frame", nil, rSC)
    bwHeader:SetPoint("TOPLEFT", tokDiv, "BOTTOMLEFT", 0, -18)
    bwHeader:SetPoint("TOPRIGHT", tokDiv, "BOTTOMRIGHT", 0, -18)
    bwHeader:SetHeight(20)

    local bwTitle = MakeSectionTitle(bwHeader, "Bag Worth")
    bwTitle:SetPoint("LEFT", 0, 0)

    local bwOptBtn = CreateFrame("Button", nil, bwHeader)
    bwOptBtn:SetSize(64, 18)
    bwOptBtn:SetPoint("RIGHT", 0, 0)

    bwOptBtn.bg = bwOptBtn:CreateTexture(nil, "BACKGROUND")
    bwOptBtn.bg:SetAllPoints(bwOptBtn)
    bwOptBtn.bg:SetTexture("Interface\\Buttons\\WHITE8x8")
    bwOptBtn.bg:SetVertexColor(1, 1, 1, 1)
    bwOptBtn.bg:SetAlpha(0)

    bwOptBtn.txt = bwOptBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    bwOptBtn.txt:SetPoint("CENTER", 0, 0)
    bwOptBtn.txt:SetText("Options")
    do
      local mr, mg, mb = C4(MUTED, MUTED)
      if bwOptBtn.txt then bwOptBtn.txt:SetTextColor(mr, mg, mb, 1) end
    end


    _WF_ApplyGearOptionButton(bwOptBtn)
    bwOptBtn:SetScript("OnEnter", function()
      _WF_GearOpt_SetHover(bwOptBtn, true)
      if GameTooltip then
        GameTooltip:SetOwner(bwOptBtn, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine("Bag Worth Options", ar, ag, ab)
        GameTooltip:AddLine("Choose what Bag Worth counts when you press Refresh Worth.", TEXT[1], TEXT[2], TEXT[3], true)
        GameTooltip:Show()
      end
    end)
    bwOptBtn:SetScript("OnLeave", function()
      _WF_GearOpt_SetHover(bwOptBtn, false)
      if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
    end)

    local function _BW_Ensure()
      if EnsureSettingsDefaults then EnsureSettingsDefaults() end
    end

    local function _BW_Get(key, default)
      _BW_Ensure()
      if WF and WF.DB and WF.DB.GetSetting then
        local v = WF.DB:GetSetting(key)
        if v == nil then return default end
        return v
      end
      return default
    end

    local function _BW_Set(key, v)
      _BW_Ensure()
      if WF and WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting(key, v)
      end
    end

    local function _BW_ClearRemembered()
      if WF and WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting("bwLastPayload", nil)
        WF.DB:SetSetting("bwLastProvider", nil)
        WF.DB:SetSetting("bwLastAt", nil)
      end
      UI._bwLastPayload = nil
      if UI and UI.RefreshBagWorth then
        UI:RefreshBagWorth({ force = false })
      end
    end

    local MakeCheck = ctx.MakeCheck

    local bwOptPanel = CreateFrame("Frame", nil, rSC, "BackdropTemplate")
    bwOptPanel:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -10)
    bwOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)
    bwOptPanel:Hide()
    bwOptPanel:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 10,
      insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    bwOptPanel:SetBackdropColor(0, 0, 0, 0.36)
    bwOptPanel:SetBackdropBorderColor(1, 1, 1, 0.14)

    local chkMats = MakeCheck(
      bwOptPanel,
      "Incl. Mats",
      function() return _BW_Get("bwIncludeMats", true) end,
      function(v) _BW_Set("bwIncludeMats", v) end
    )
    local chkGear = MakeCheck(
      bwOptPanel,
      "Incl. Gear",
      function() return _BW_Get("bwIncludeGear", true) end,
      function(v) _BW_Set("bwIncludeGear", v) end
    )
    local chkPatterns = MakeCheck(
      bwOptPanel,
      "Incl. Patterns",
      function() return _BW_Get("bwIncludePatterns", true) end,
      function(v) _BW_Set("bwIncludePatterns", v) end
    )
    local chkPets = MakeCheck(
      bwOptPanel,
      "Incl. Pets",
      function() return _BW_Get("bwIncludePets", true) end,
      function(v) _BW_Set("bwIncludePets", v) end
    )
    local chkToys = MakeCheck(
      bwOptPanel,
      "Incl. Toys",
      function() return _BW_Get("bwIncludeToys", true) end,
      function(v) _BW_Set("bwIncludeToys", v) end
    )
    local chkConsumables = MakeCheck(
      bwOptPanel,
      "Incl. Consumables",
      function() return _BW_Get("bwIncludeConsumables", true) end,
      function(v) _BW_Set("bwIncludeConsumables", v) end
    )
    local chkGems = MakeCheck(
      bwOptPanel,
      "Incl. Gems",
      function() return _BW_Get("bwIncludeGems", true) end,
      function(v) _BW_Set("bwIncludeGems", v) end
    )

    local chkGreys = MakeCheck(
      bwOptPanel,
      "Incl. Greys",
      function() return _BW_Get("bwIncludeGreys", false) end,
      function(v) _BW_Set("bwIncludeGreys", v) end
    )
    local chkWhites = MakeCheck(
      bwOptPanel,
      "Incl. Whites",
      function() return _BW_Get("bwIncludeWhites", false) end,
      function(v) _BW_Set("bwIncludeWhites", v) end
    )
    local chkGreens = MakeCheck(
      bwOptPanel,
      "Incl. Greens",
      function() return _BW_Get("bwIncludeGreens", true) end,
      function(v) _BW_Set("bwIncludeGreens", v) end
    )
    local chkBlues = MakeCheck(
      bwOptPanel,
      "Incl. Blues",
      function() return _BW_Get("bwIncludeBlues", true) end,
      function(v) _BW_Set("bwIncludeBlues", v) end
    )
    local chkEpics = MakeCheck(
      bwOptPanel,
      "Incl. Epics",
      function() return _BW_Get("bwIncludeEpics", true) end,
      function(v) _BW_Set("bwIncludeEpics", v) end
    )
    local chkRemember = MakeCheck(
      bwOptPanel,
      "Remember Worth",
      function() return _BW_Get("bwRememberWorth", true) end,
      function(v)
        _BW_Set("bwRememberWorth", v)
        if not v then
          _BW_ClearRemembered()
        end
      end
    )

    local bwTypeHead = MakeMutedText(bwOptPanel, "ITEM TYPES")
    bwTypeHead:SetJustifyH("LEFT")
    local bwQualityHead = MakeMutedText(bwOptPanel, "QUALITY")
    bwQualityHead:SetJustifyH("LEFT")
    local bwDisplayHead = MakeMutedText(bwOptPanel, "DISPLAY")
    bwDisplayHead:SetJustifyH("LEFT")
    do
      local ar, ag, ab = C4(ACCENT, ACCENT)
      bwTypeHead:SetTextColor(ar, ag, ab)
      bwQualityHead:SetTextColor(ar, ag, ab)
      bwDisplayHead:SetTextColor(ar, ag, ab)
    end

    local function _BW_AttachTip(frame, title, body)
      if not (frame and frame.HookScript) then return end

      -- Layout controls the hover/click area via explicit widths so tooltips don't overlap columns.

      frame:HookScript("OnEnter", function(selfTip)
        if not GameTooltip then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(selfTip, "ANCHOR_RIGHT")
        if GameTooltip.ClearLines then GameTooltip:ClearLines() end
        local ar, ag, ab = C4(ACCENT, ACCENT)
        GameTooltip:AddLine(tostring(title or ""), ACCENT[1], ACCENT[2], ACCENT[3], true)
        body = tostring(body or "")
        if body ~= "" then
          GameTooltip:AddLine(body, TEXT[1], TEXT[2], TEXT[3], true)
        end
        GameTooltip:Show()
      end)

      frame:HookScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
        _WF_TooltipFollow_Stop()
      end)
    end

    _BW_AttachTip(chkMats, "Incl. Mats", "Counts trade goods and crafting reagents (cloth, ore, herbs, leather, etc.).")
    _BW_AttachTip(chkGear, "Incl. Gear", "Counts weapons, armor, and trinkets. Quality toggles apply here too.")
    _BW_AttachTip(chkPatterns, "Incl. Patterns", "Counts recipes, patterns, plans, schematics, and similar learnables. Quality toggles apply here too.")
    _BW_AttachTip(chkPets, "Incl. Pets", "Counts caged battle pets and pet-related items. Quality toggles apply here too.")
    _BW_AttachTip(chkToys, "Incl. Toys", "Counts toy items when the WoW toy API identifies the item as a toy. Quality toggles apply here too.")
    _BW_AttachTip(chkConsumables, "Incl. Consumables", "Counts potions, flasks, food, elixirs, scrolls, and other consumables. Quality toggles apply here too.")
    _BW_AttachTip(chkGems, "Incl. Gems", "Counts gems and gem-like items. Quality toggles apply here too.")
    _BW_AttachTip(chkGreys, "Incl. Greys", "Counts grey (poor quality) items for every enabled Bag Worth type. Useful for vendor-value scans that include junk.")
    _BW_AttachTip(chkWhites, "Incl. Whites", "Counts white (common quality) items for every enabled Bag Worth type. Useful when you farm cloth, reagents, or other white-quality stacks.")
    _BW_AttachTip(chkGreens, "Incl. Greens", "Counts uncommon (green) items for every enabled Bag Worth type.")
    _BW_AttachTip(chkBlues, "Incl. Blues", "Counts rare (blue) items for every enabled Bag Worth type.")
    _BW_AttachTip(chkEpics, "Incl. Epics", "Counts epic (purple) and better items for every enabled Bag Worth type.")
    _BW_AttachTip(chkRemember, "Remember Worth", "Show the last scanned totals after reloads. Disable to clear the saved worth display.")


    local function _BW_LayoutOptions()
      -- Re-anchor so the backdrop redraws with the correct width after rSC is sized.
      bwOptPanel:ClearAllPoints()
      bwOptPanel:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -10)
      bwOptPanel:SetPoint("RIGHT", rSC, "RIGHT", -12, 0)

      local w = (bwOptPanel and bwOptPanel.GetWidth and bwOptPanel:GetWidth()) or 0
      if not w or w <= 1 then w = 300 end

      local col1X = 12
      local col2X = math.floor(w * 0.52)
      local minRight = 118
      local minLeft = 112
      if (w - col2X) - 12 < minRight then col2X = w - minRight - 12 end
      if (col2X - col1X) - 8 < minLeft then col2X = col1X + minLeft + 8 end
      if col2X < 116 then col2X = 116 end
      if col2X > math.floor(w * 0.62) then col2X = math.floor(w * 0.62) end

      local y0 = -10
      local gap = 8
      local headGap = 5
      local rowGap = 6
      local sectionGap = 10
      local leftColW = (col2X - col1X) - gap
      if leftColW < 86 then leftColW = 86 end
      local rightColW = (w - col2X) - 12
      if rightColW < 86 then rightColW = 86 end

      local function _BW_SetColumnHitRect(f, colW)
        if not (f and f.SetHitRectInsets and f.GetWidth) then return end
        local fw = f:GetWidth() or 0
        if not fw or fw <= 1 then fw = 26 end
        local delta = colW - fw
        f:SetHitRectInsets(0, -delta, 0, 0)
      end

      bwTypeHead:ClearAllPoints()
      bwQualityHead:ClearAllPoints()
      bwDisplayHead:ClearAllPoints()
      chkMats:ClearAllPoints()
      chkGear:ClearAllPoints()
      chkPatterns:ClearAllPoints()
      chkPets:ClearAllPoints()
      chkToys:ClearAllPoints()
      chkConsumables:ClearAllPoints()
      chkGems:ClearAllPoints()
      chkGreys:ClearAllPoints()
      chkWhites:ClearAllPoints()
      chkGreens:ClearAllPoints()
      chkBlues:ClearAllPoints()
      chkEpics:ClearAllPoints()
      chkRemember:ClearAllPoints()

      bwTypeHead:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", col1X, y0)
      bwTypeHead:SetWidth(leftColW)
      bwQualityHead:SetPoint("TOPLEFT", bwOptPanel, "TOPLEFT", col2X, y0)
      bwQualityHead:SetWidth(rightColW)

      chkMats:SetPoint("TOPLEFT", bwTypeHead, "BOTTOMLEFT", 0, -headGap)
      chkGear:SetPoint("TOPLEFT", chkMats, "BOTTOMLEFT", 0, -rowGap)
      chkPatterns:SetPoint("TOPLEFT", chkGear, "BOTTOMLEFT", 0, -rowGap)
      chkPets:SetPoint("TOPLEFT", chkPatterns, "BOTTOMLEFT", 0, -rowGap)
      chkToys:SetPoint("TOPLEFT", chkPets, "BOTTOMLEFT", 0, -rowGap)
      chkConsumables:SetPoint("TOPLEFT", chkToys, "BOTTOMLEFT", 0, -rowGap)
      chkGems:SetPoint("TOPLEFT", chkConsumables, "BOTTOMLEFT", 0, -rowGap)

      chkGreys:SetPoint("TOPLEFT", bwQualityHead, "BOTTOMLEFT", 0, -headGap)
      chkWhites:SetPoint("TOPLEFT", chkGreys, "BOTTOMLEFT", 0, -rowGap)
      chkGreens:SetPoint("TOPLEFT", chkWhites, "BOTTOMLEFT", 0, -rowGap)
      chkBlues:SetPoint("TOPLEFT", chkGreens, "BOTTOMLEFT", 0, -rowGap)
      chkEpics:SetPoint("TOPLEFT", chkBlues, "BOTTOMLEFT", 0, -rowGap)
      bwDisplayHead:SetPoint("TOPLEFT", chkEpics, "BOTTOMLEFT", 0, -sectionGap)
      bwDisplayHead:SetWidth(rightColW)
      chkRemember:SetPoint("TOPLEFT", bwDisplayHead, "BOTTOMLEFT", 0, -headGap)

      _BW_SetColumnHitRect(chkMats, leftColW)
      _BW_SetColumnHitRect(chkGear, leftColW)
      _BW_SetColumnHitRect(chkPatterns, leftColW)
      _BW_SetColumnHitRect(chkPets, leftColW)
      _BW_SetColumnHitRect(chkToys, leftColW)
      _BW_SetColumnHitRect(chkConsumables, leftColW)
      _BW_SetColumnHitRect(chkGems, leftColW)

      _BW_SetColumnHitRect(chkGreys, rightColW)
      _BW_SetColumnHitRect(chkWhites, rightColW)
      _BW_SetColumnHitRect(chkGreens, rightColW)
      _BW_SetColumnHitRect(chkBlues, rightColW)
      _BW_SetColumnHitRect(chkEpics, rightColW)
      _BW_SetColumnHitRect(chkRemember, rightColW)

      local checkW = 26
      local leftLabelW = math.max(44, leftColW - checkW)
      local rightLabelW = math.max(44, rightColW - checkW)
      local function _BW_SetLabelWidth(chk, lw)
        if chk and chk.Text and chk.Text.SetWidth then
          chk.Text:SetWidth(lw)
        end
      end
      _BW_SetLabelWidth(chkMats, leftLabelW)
      _BW_SetLabelWidth(chkGear, leftLabelW)
      _BW_SetLabelWidth(chkPatterns, leftLabelW)
      _BW_SetLabelWidth(chkPets, leftLabelW)
      _BW_SetLabelWidth(chkToys, leftLabelW)
      _BW_SetLabelWidth(chkConsumables, leftLabelW)
      _BW_SetLabelWidth(chkGems, leftLabelW)
      _BW_SetLabelWidth(chkGreys, rightLabelW)
      _BW_SetLabelWidth(chkWhites, rightLabelW)
      _BW_SetLabelWidth(chkGreens, rightLabelW)
      _BW_SetLabelWidth(chkBlues, rightLabelW)
      _BW_SetLabelWidth(chkEpics, rightLabelW)
      _BW_SetLabelWidth(chkRemember, rightLabelW)

      bwOptPanel:SetHeight(226)
    end

    bwOptPanel:SetScript("OnShow", function()
      -- Defer one frame so rSC has its real width before the backdrop renders,
      -- preventing the clipped/missing top-left corner visual glitch.
      C_Timer.After(0, function()
        if bwOptPanel:IsShown() then
          _BW_LayoutOptions()
        end
      end)
    end)

    bwOptPanel:SetScript("OnSizeChanged", function()
      if bwOptPanel:IsShown() then
        _BW_LayoutOptions()
      end
    end)

    -- Display
    local bwTotal = MakeBodyText(rSC, "Total - —")
    bwTotal:SetJustifyH("LEFT")
    bwTotal:SetTextColor(C4(TEXT, TEXT))
    bwTotal:SetWordWrap(true)

    local bwBags = MakeMutedText(rSC, "Bags - —")
    bwBags:SetJustifyH("LEFT")
    bwBags:SetWordWrap(true)

    local bwPB = MakeMutedText(rSC, "Personal Bank - open bank + refresh")
    bwPB:SetJustifyH("LEFT")
    bwPB:SetWordWrap(true)

    local bwGB = MakeMutedText(rSC, "Guild Bank - open guild bank + refresh")
    bwGB:SetJustifyH("LEFT")
    bwGB:SetWordWrap(true)

    local bwSource = MakeMutedText(rSC, "Source - —")
    bwSource:SetJustifyH("LEFT")
    bwSource:SetWordWrap(true)

    local bwProgress = MakeMutedText(rSC, "")
    bwProgress:SetJustifyH("LEFT")
    bwProgress:SetWordWrap(true)
    bwProgress:Hide()

    local function _BW_Reflow()
      -- Right margin: 12px from rSC right edge
      local rightInset = -12

      bwTotal:ClearAllPoints()
      if bwOptPanel:IsShown() then
        bwTotal:SetPoint("TOPLEFT", bwOptPanel, "BOTTOMLEFT", 0, -10)
      else
        bwTotal:SetPoint("TOPLEFT", bwHeader, "BOTTOMLEFT", 0, -10)
      end
      bwTotal:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwBags:ClearAllPoints()
      bwBags:SetPoint("TOPLEFT", bwTotal, "BOTTOMLEFT", 0, -8)
      bwBags:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwPB:ClearAllPoints()
      bwPB:SetPoint("TOPLEFT", bwBags, "BOTTOMLEFT", 0, -6)
      bwPB:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwGB:ClearAllPoints()
      bwGB:SetPoint("TOPLEFT", bwPB, "BOTTOMLEFT", 0, -6)
      bwGB:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwSource:ClearAllPoints()
      bwSource:SetPoint("TOPLEFT", bwGB, "BOTTOMLEFT", 0, -6)
      bwSource:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      bwProgress:ClearAllPoints()
      bwProgress:SetPoint("TOPLEFT", bwSource, "BOTTOMLEFT", 0, -6)
      bwProgress:SetPoint("RIGHT", rSC, "RIGHT", rightInset, 0)

      if UI and UI._UpdateOverviewRightScroll then
        UI:_UpdateOverviewRightScroll()
      end
    end

    bwOptBtn:SetScript("OnClick", function()
      if bwOptPanel:IsShown() then
        bwOptPanel:Hide()
      else
        bwOptPanel:Show()
      end
      _BW_Reflow()
    end)

    _BW_Reflow()

    local bBW = Widgets:Button(rSC, "Refresh Worth", 160, 26)
bBW:SetPoint("TOPLEFT", bwSource, "BOTTOMLEFT", 0, -12)

-- Combined Refresh/Details action:
--   Click         = Refresh Worth
--   Shift + Click = Details breakdown (shows what's being counted)
UI._bwDetailsReady = UI._bwDetailsReady or false

local function BW_ShowActionTooltip()
  if not (bBW and GameTooltip) then return end

  local isShift = false
  if IsShiftKeyDown then
    isShift = (IsShiftKeyDown() == true)
  end

  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(bBW, "ANCHOR_RIGHT")
  if GameTooltip.ClearLines then GameTooltip:ClearLines() end

  local ar, ag, ab = C4(ACCENT, ACCENT)
  if isShift then
    GameTooltip:AddLine("Bag Worth Details", ar, ag, ab)
    GameTooltip:AddLine("Shows which items are being counted, their quantities, and how much they contribute to the total.", TEXT[1], TEXT[2], TEXT[3], true)
    if not UI._bwDetailsReady then
      GameTooltip:AddLine(" ")
      GameTooltip:AddLine("Press Refresh Worth first to generate a breakdown.", 0.35, 0.78, 1.00, true)
    end
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("Tip: release Shift to refresh worth.", 0.75, 0.75, 0.75, true)
  else
    GameTooltip:AddLine("Refresh Worth", ar, ag, ab)
    GameTooltip:AddLine("Runs a manual Bag Worth scan using your selected filters.", TEXT[1], TEXT[2], TEXT[3], true)
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine("Tip: hold Shift to open Details instead.", 0.75, 0.75, 0.75, true)
  end

  GameTooltip:Show()
end

local function BW_UpdateActionButton()
  if not bBW then return end

  local isShift = false
  if IsShiftKeyDown then
    isShift = (IsShiftKeyDown() == true)
  end

  local label = nil
  if UI._bwScanRunning then
    label = "Scanning…"
  else
    label = isShift and "Details" or "Refresh Worth"
  end
  if bBW.label and bBW.label.SetText then
    bBW.label:SetText(label)
  elseif bBW.SetText then
    bBW:SetText(label)
  end

  -- Subtle cue: if you're holding Shift but no breakdown exists yet, mute the label.
  if bBW.label and bBW.label.SetTextColor and Theme then
    if isShift and not UI._bwDetailsReady then
      local mr, mg, mb = C4(MUTED, MUTED)
      bBW.label:SetTextColor(mr, mg, mb, 1)
    elseif Theme.text then
      bBW.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
    end
  end

  -- If tooltip is showing, refresh it so the text matches the current modifier state.
  if GameTooltip and GameTooltip.IsOwned and GameTooltip:IsOwned(bBW) then
    BW_ShowActionTooltip()
  end
end

local function BW_SetDetailsEnabled(on)
  UI._bwDetailsReady = (on == true)
  BW_UpdateActionButton()
end

local function BW_SetScanButtonState(scanning)
  UI._bwScanRunning = (scanning == true)
  if bBW then
    if UI._bwScanRunning then
      if bBW.Disable then bBW:Disable() end
    else
      if bBW.Enable then bBW:Enable() end
    end
  end
  BW_UpdateActionButton()
end

local function BW_FinishScanState(message)
  BW_SetScanButtonState(false)
  if type(message) == "string" then
    bwProgress:SetText(message)
  end
end

-- Update label when Shift is pressed/released (even without hovering).
if bBW and bBW.RegisterEvent and bBW.SetScript then
  bBW:RegisterEvent("MODIFIER_STATE_CHANGED")
  bBW:SetScript("OnEvent", function()
    BW_UpdateActionButton()
  end)
end

if bBW and bBW.HookScript then
  bBW:HookScript("OnShow", BW_UpdateActionButton)
  bBW:HookScript("OnEnter", function()
    BW_UpdateActionButton()
    BW_ShowActionTooltip()
  end)
  bBW:HookScript("OnLeave", function()
    if GameTooltip then GameTooltip:Hide() end
    _WF_TooltipFollow_Stop()
  end)
end

BW_UpdateActionButton()

-- -----------------------------------------------------
-- Bag Worth breakdown popup (shows what is being counted)
-- -----------------------------------------------------
local function BW_NormalizeCategoryKey(category)
      category = tostring(category or ""):lower()
      if category == "gear" then return "gear" end
      if category == "mats" or category == "material" or category == "materials" then return "mats" end
      if category == "patterns" or category == "pattern" then return "patterns" end
      if category == "pets" or category == "pet" then return "pets" end
      if category == "toys" or category == "toy" then return "toys" end
      if category == "consumables" or category == "consumable" then return "consumables" end
      if category == "gems" or category == "gem" then return "gems" end
      if category == "other" then return "other" end
      return nil
    end

    local function BW_DeriveCategoryKeyFromLink(itemLink)
      local classID
      local subClassID
      local equipLoc
      local isCraftingReagent

      if itemLink then
        local _, _, _, instantEquipLoc, _, instantClassID, instantSubClassID = GetItemInfoInstant(itemLink)
        if instantEquipLoc and instantEquipLoc ~= "" then
          equipLoc = instantEquipLoc
        end
        if instantClassID ~= nil then
          classID = tonumber(instantClassID) or classID
        end
        if instantSubClassID ~= nil then
          subClassID = tonumber(instantSubClassID) or subClassID
        end

        local itemName, _, _, _, _, _, _, _, fullEquipLoc, _, _, fullClassID, fullSubClassID, _, _, _, craftingReagent = GetItemInfo(itemLink)
        if itemName then
          if fullEquipLoc and fullEquipLoc ~= "" then
            equipLoc = fullEquipLoc
          end
          if fullClassID ~= nil then
            classID = tonumber(fullClassID) or classID
          end
          if fullSubClassID ~= nil then
            subClassID = tonumber(fullSubClassID) or subClassID
          end
          if craftingReagent ~= nil then
            isCraftingReagent = craftingReagent and true or false
          end
        end
      end

      if classID == 2 or classID == 4 then
        return "gear"
      end

      if equipLoc and equipLoc ~= "" and equipLoc ~= "INVTYPE_BAG" and equipLoc ~= "INVTYPE_QUIVER" then
        if tostring(equipLoc):find("PROFESSION") then
          return "gear"
        end
      end

      if isCraftingReagent == true or classID == 7 then
        return "mats"
      end

      if classID == 9 then
        return "patterns"
      end

      if classID == 17 or (classID == 15 and subClassID == 2) or (type(itemLink) == "string" and itemLink:find("battlepet:")) then
        return "pets"
      end

      local itemID = nil
      if itemLink and GetItemInfoInstant then
        itemID = tonumber(select(1, GetItemInfoInstant(itemLink)))
      end
      if itemID and C_ToyBox and C_ToyBox.GetToyInfo then
        local ok, toyItemID = pcall(C_ToyBox.GetToyInfo, itemID)
        if ok and toyItemID then
          return "toys"
        end
      end

      if classID == 0 then
        return "consumables"
      end

      if classID == 3 then
        return "gems"
      end

      return "other"
    end

    local function BW_ResolveCategoryKey(category, itemLink)
      return BW_NormalizeCategoryKey(category) or BW_DeriveCategoryKeyFromLink(itemLink) or "other"
    end

    local function BW_FormatCategoryLabel(category, itemLink)
      local key = BW_ResolveCategoryKey(category, itemLink)
      if key == "gear" then return "Gear" end
      if key == "mats" then return "Materials" end
      if key == "patterns" then return "Patterns" end
      if key == "pets" then return "Pets" end
      if key == "toys" then return "Toys" end
      if key == "consumables" then return "Consumables" end
      if key == "gems" then return "Gems" end
      return "Other"
    end

    local function BW_BuildBreakdownFromByItem(byItem, limit)
      local out = {}
      if type(byItem) ~= "table" then return out end
      for key, e in pairs(byItem) do
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local v = tonumber(e.value) or 0
          local link = e.link or (type(key) == "string" and key or nil)
          if c > 0 and v > 0 then
            local each = math.floor((v / c) + 0.5)
            out[#out + 1] = {
              link = link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = c,
              each = each,
              value = v,
              category = BW_FormatCategoryLabel(e.category, link),
            }
          end
        end
      end
      table.sort(out, function(a, b) return (a.value or 0) > (b.value or 0) end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildTopForSave(byItem, limit)
      local out = {}
      if type(byItem) ~= "table" then return out end
      for key, e in pairs(byItem) do
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local v = tonumber(e.value) or 0
          local link = e.link or (type(key) == "string" and key or nil)
          if c > 0 and v > 0 then
            out[#out + 1] = {
              link = link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = c,
              value = v,
              category = BW_ResolveCategoryKey(e.category, link),
            }
          end
        end
      end
      table.sort(out, function(a, b)
        local av = tonumber(a.value) or 0
        local bv = tonumber(b.value) or 0
        if av == bv then
          local an = tostring(a.link or "")
          local bn = tostring(b.link or "")
          return an < bn
        end
        return av > bv
      end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildSkippedBreakdown(skippedItems, limit)
      local out = {}
      if type(skippedItems) ~= "table" then return out end
      for i = 1, #skippedItems do
        local e = skippedItems[i]
        if type(e) == "table" then
          local c = tonumber(e.count) or 0
          local reason = tostring(e.reason or "Skipped")
          if c > 0 then
            out[#out + 1] = {
              itemID = tonumber(e.itemID),
              link = e.link,
              name = e.name,
              count = c,
              reason = reason,
              source = e.source,
            }
          end
        end
      end
      table.sort(out, function(a, b)
        local ar = tostring(a.reason or "")
        local br = tostring(b.reason or "")
        if ar == br then
          local ac = tonumber(a.count) or 0
          local bc = tonumber(b.count) or 0
          if ac == bc then
            local an = BW_LinkName and BW_LinkName(a.link) or tostring(a.name or a.link or "")
            local bn = BW_LinkName and BW_LinkName(b.link) or tostring(b.name or b.link or "")
            return an < bn
          end
          return ac > bc
        end
        return ar < br
      end)
      if limit and #out > limit then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end
      return out
    end

    local function BW_BuildSkippedForSave(skippedItems, limit)
      local tmp = BW_BuildSkippedBreakdown(skippedItems, limit)
      local out = {}
      for i = 1, #tmp do
        local d = tmp[i]
        out[#out + 1] = {
          itemID = tonumber(d.itemID),
          link = d.link,
          name = d.name,
          count = d.count,
          reason = d.reason,
          source = d.source,
        }
      end
      return out
    end

    local function BW_MakeCompactPayload(payload)
      payload = payload or {}

      local top = nil
      if type(payload.top) == "table" then
        top = {}
        for i = 1, #payload.top do
          local e = payload.top[i]
          if type(e) == "table" then
            top[#top + 1] = {
              link = e.link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = tonumber(e.count) or 0,
              value = tonumber(e.value) or 0,
              category = BW_ResolveCategoryKey(e.category, e.link),
            }
          end
        end
      else
        top = BW_BuildTopForSave(payload.byItem, 1000)
      end

      local skippedTop = nil
      if type(payload.skippedTop) == "table" then
        skippedTop = {}
        for i = 1, #payload.skippedTop do
          local e = payload.skippedTop[i]
          if type(e) == "table" then
            skippedTop[#skippedTop + 1] = {
              itemID = tonumber(e.itemID),
              link = e.link,
              name = e.name,
              count = tonumber(e.count) or 0,
              reason = e.reason,
              source = e.source,
            }
          end
        end
      else
        skippedTop = BW_BuildSkippedForSave(payload.skippedItems, 1000)
      end

      local breakdown = nil
      if type(payload.breakdown) == "table" then
        breakdown = {}
        for i = 1, #payload.breakdown do
          local e = payload.breakdown[i]
          if type(e) == "table" then
            breakdown[#breakdown + 1] = {
              link = e.link,
              name = e.name,
              itemID = tonumber(e.itemID),
              count = tonumber(e.count) or 0,
              each = tonumber(e.each) or 0,
              value = tonumber(e.value) or 0,
              category = BW_ResolveCategoryKey(e.category, e.link),
            }
          end
        end
      else
        breakdown = BW_BuildBreakdownFromByItem(payload.byItem, 1000)
      end

      local skippedBreakdown = nil
      if type(payload.skippedBreakdown) == "table" then
        skippedBreakdown = BW_BuildSkippedBreakdown(payload.skippedBreakdown, 1000)
      else
        skippedBreakdown = BW_BuildSkippedBreakdown(payload.skippedItems or payload.skippedTop, 1000)
      end

      return {
        scope = payload.scope,
        source = payload.source,
        total = payload.total,
        bagsTotal = payload.bagsTotal,
        bankTotal = payload.bankTotal,
        bankScanned = payload.bankScanned,
        gbTotal = payload.gbTotal,
        pricedStacks = payload.pricedStacks,
        pricedQty = payload.pricedQty,
        skippedStacks = payload.skippedStacks,
        skippedQty = payload.skippedQty,
        note = payload.note,
        pending = payload.pending,
        top = top,
        skippedTop = skippedTop,
        breakdown = breakdown,
        skippedBreakdown = skippedBreakdown,
        bagsBreakdown = type(payload.bagsByItem) == "table" and BW_BuildBreakdownFromByItem(payload.bagsByItem, 1000) or nil,
        bankBreakdown = type(payload.bankByItem) == "table" and BW_BuildBreakdownFromByItem(payload.bankByItem, 1000) or nil,
        gbBreakdown   = type(payload.gbByItem)   == "table" and BW_BuildBreakdownFromByItem(payload.gbByItem,   1000) or nil,
        bagsSkippedItems = type(payload.bagsSkippedItems) == "table" and payload.bagsSkippedItems or nil,
        bankSkippedItems = type(payload.bankSkippedItems) == "table" and payload.bankSkippedItems or nil,
        gbSkippedItems   = type(payload.gbSkippedItems)   == "table" and payload.gbSkippedItems   or nil,
      }
    end

    local function BW_ApplyDetailsTheme(frame, role, bgAlpha, borderAlpha)
      if not frame then return end

      local fallback = { 0.05, 0.05, 0.05, 0.98 }
      local src = Theme and Theme.bg
      if role == "header" then
        -- Match the Help/Main header darkness. Theme.panel2 is intentionally
        -- brighter and made the Bag Breakdown title bar look detached.
        src = Theme and Theme.bg
        fallback = { 0.05, 0.05, 0.05, 0.98 }
      elseif role == "panel" then
        src = Theme and Theme.panel
        fallback = { 0.07, 0.07, 0.07, 0.98 }
      end

      local pr, pg, pb = C4(src, fallback)
      pr = pr or fallback[1]; pg = pg or fallback[2]; pb = pb or fallback[3]

      local br, bg, bb = C4(BORDER, BORDER)
      br = br or 0.18; bg = bg or 0.18; bb = bb or 0.18

      if frame.SetBackdrop then
        frame:SetBackdrop({
          bgFile = "Interface/Buttons/WHITE8X8",
          edgeFile = "Interface/Buttons/WHITE8X8",
          edgeSize = 1,
          insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        frame:SetBackdropColor(pr, pg, pb, tonumber(bgAlpha) or 1)
        frame:SetBackdropBorderColor(br, bg, bb, tonumber(borderAlpha) or 1)
      elseif frame.SetBackdropColor then
        frame:SetBackdropColor(pr, pg, pb, tonumber(bgAlpha) or 1)
      end
    end

    local function BW_RegisterEscapeClose(frame)
      if not frame then return end

      local specialName = "WF_BagWorthBreakdownFrame"
      if _G[specialName] ~= frame then
        _G[specialName] = frame
      end

      for i = 1, #UISpecialFrames do
        if UISpecialFrames[i] == specialName then
          return
        end
      end

      UISpecialFrames[#UISpecialFrames + 1] = specialName
    end

    local function BW_ClaimUtilityWindowFocus(frame)
      local focus = WF and WF.UI and WF.UI.UtilityWindowFocus or nil
      if focus and focus.Claim then
        focus:Claim("bagWorthBreakdown", frame)
        return
      end

      if frame and frame.Raise then
        frame:Raise()
      end
    end

    local function BW_EnsureDetailsFrame()
      if UI._bwDetailsFrame then return UI._bwDetailsFrame end

      local f = Widgets:Panel(UIParent)
      f:SetFrameStrata("DIALOG")
      f:SetSize(820, 560)
      f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
      if f.SetClampedToScreen then f:SetClampedToScreen(true) end
      f:Hide()
      BW_RegisterEscapeClose(f)

      if f.HookScript then
        f:HookScript("OnShow", function(frame)
          BW_ClaimUtilityWindowFocus(frame)
        end)
        f:HookScript("OnMouseDown", function(frame)
          BW_ClaimUtilityWindowFocus(frame)
        end)
      end

      if f.EnableMouse then f:EnableMouse(true) end
      if f.EnableKeyboard then f:EnableKeyboard(true) end
      if f.SetPropagateKeyboardInput then f:SetPropagateKeyboardInput(true) end
      if f.SetScript then
        f:SetScript("OnKeyDown", function(self, key)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(key ~= "ESCAPE")
          end

          if key == "ESCAPE" and self and self.IsShown and self:IsShown() then
            self:Hide()
            if self and self.SetPropagateKeyboardInput then
              self:SetPropagateKeyboardInput(true)
            end
          end
        end)
        f:SetScript("OnKeyUp", function(self)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(true)
          end
        end)
        f:SetScript("OnHide", function(self)
          if self and self.SetPropagateKeyboardInput then
            self:SetPropagateKeyboardInput(true)
          end
        end)
      end

      BW_ApplyDetailsTheme(f, "bg", 1, 1)

      if f.bg and f.bg.SetAlpha then f.bg:SetAlpha(1) end
      if f.Bg and f.Bg.SetAlpha then f.Bg:SetAlpha(1) end
      if f.background and f.background.SetAlpha then f.background:SetAlpha(1) end

      do
        if not f._wfSolidBG then
          local bg = f:CreateTexture(nil, "BACKGROUND")
          bg:SetAllPoints(f)
          local sr, sg, sb = C4((Theme and Theme.bg) or { 0.05, 0.05, 0.05, 0.98 }, { 0.05, 0.05, 0.05, 0.98 })
          bg:SetColorTexture(sr or 0.05, sg or 0.05, sb or 0.05, 1)
          f._wfSolidBG = bg
        else
          local sr, sg, sb = C4((Theme and Theme.bg) or { 0.05, 0.05, 0.05, 0.98 }, { 0.05, 0.05, 0.05, 0.98 })
          f._wfSolidBG:SetColorTexture(sr or 0.05, sg or 0.05, sb or 0.05, 1)
          if f._wfSolidBG.SetAlpha then f._wfSolidBG:SetAlpha(1) end
        end
      end

      local header = Widgets:Header(f, "WHAT FARM?", "Bag Breakdown")
      BW_ApplyDetailsTheme(header, "header", 0.98, 1)
      header:SetPoint("TOPLEFT", 0, 0)
      header:SetPoint("TOPRIGHT", 0, 0)
      header:SetHeight(34)

      if header and header.close then
        header.close:SetScript("OnClick", function()
          if f and f.Hide then f:Hide() end
        end)
      end

      if f.SetMovable then f:SetMovable(true) end
      if header and header.EnableMouse then header:EnableMouse(true) end
      if header and header.RegisterForDrag then header:RegisterForDrag("LeftButton") end
      if header and header.SetScript then
        header:SetScript("OnDragStart", function()
          BW_ClaimUtilityWindowFocus(f)
          if f and f.StartMoving then f:StartMoving() end
        end)
        header:SetScript("OnDragStop", function()
          if f and f.StopMovingOrSizing then f:StopMovingOrSizing() end
        end)
      end

      if f.SetResizable then f:SetResizable(true) end
      if f.SetMinResize then f:SetMinResize(740, 460) end

      local grip = CreateFrame("Button", nil, f)
      grip:SetSize(16, 16)
      grip:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -6, 6)
      grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
      grip:SetHighlightTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
      grip:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
      grip._wfActive = false
      grip._wfSizingStarted = false
      grip._wfDownW = nil
      grip._wfDownH = nil
      grip._wfDownX = nil
      grip._wfDownY = nil
      grip._wfMinW = nil
      grip._wfMinH = nil
      grip._wfMaxW = nil
      grip._wfMaxH = nil

      local BW_DRAG_THRESHOLD = 3

      local function BW_GetScaledCursorPosition()
        local x, y = GetCursorPosition()
        local scale = 1
        if UIParent and UIParent.GetEffectiveScale then
          scale = UIParent:GetEffectiveScale() or 1
        end
        if scale == 0 then
          scale = 1
        end
        return (x or 0) / scale, (y or 0) / scale
      end

      local function BW_Clamp(v, minV, maxV)
        v = tonumber(v) or 0
        if minV and v < minV then v = minV end
        if maxV and maxV > 0 and v > maxV then v = maxV end
        return v
      end

      local function BW_ReadResizeBounds()
        local minW, minH = 740, 460
        local maxW, maxH = nil, nil

        if f and f.GetResizeBounds then
          local ok, a, b, c, d = pcall(f.GetResizeBounds, f)
          if ok then
            minW = tonumber(a) or minW
            minH = tonumber(b) or minH
            maxW = tonumber(c) or maxW
            maxH = tonumber(d) or maxH
          end
        elseif f and f.GetMinResize then
          local ok, a, b = pcall(f.GetMinResize, f)
          if ok then
            minW = tonumber(a) or minW
            minH = tonumber(b) or minH
          end
          if f.GetMaxResize then
            local okMax, c, d = pcall(f.GetMaxResize, f)
            if okMax then
              maxW = tonumber(c) or maxW
              maxH = tonumber(d) or maxH
            end
          end
        end

        if not maxW and UIParent and UIParent.GetWidth then
          maxW = math.max(minW, math.floor((UIParent:GetWidth() or 0) - 20))
        end
        if not maxH and UIParent and UIParent.GetHeight then
          maxH = math.max(minH, math.floor((UIParent:GetHeight() or 0) - 20))
        end

        if maxW and maxW > 0 and maxW < minW then maxW = minW end
        if maxH and maxH > 0 and maxH < minH then maxH = minH end

        return minW, minH, maxW, maxH
      end

      local function BW_ClearGripState()
        if grip and grip.SetScript then
          grip:SetScript("OnUpdate", nil)
        end
        grip._wfActive = false
        grip._wfSizingStarted = false
        grip._wfDownW = nil
        grip._wfDownH = nil
        grip._wfDownX = nil
        grip._wfDownY = nil
        grip._wfMinW = nil
        grip._wfMinH = nil
        grip._wfMaxW = nil
        grip._wfMaxH = nil
      end

      local function BW_BeginGripResize(selfBtn)
        selfBtn = selfBtn or grip
        if not selfBtn then return end
        if selfBtn._wfSizingStarted then return end
        if not (f and f.SetSize) then return end

        if f and f.StopMovingOrSizing then
          f:StopMovingOrSizing()
        end

        selfBtn._wfSizingStarted = true
        selfBtn._wfMinW, selfBtn._wfMinH, selfBtn._wfMaxW, selfBtn._wfMaxH = BW_ReadResizeBounds()
      end

      local function BW_ApplyGripResize(selfBtn)
        selfBtn = selfBtn or grip
        if not (selfBtn and selfBtn._wfActive) then return end
        if not (f and f.SetSize) then return end

        local xNow, yNow = BW_GetScaledCursorPosition()
        local dx = (xNow or 0) - (selfBtn._wfDownX or 0)
        local dy = (yNow or 0) - (selfBtn._wfDownY or 0)

        if not selfBtn._wfSizingStarted then
          if math.abs(dx) < BW_DRAG_THRESHOLD and math.abs(dy) < BW_DRAG_THRESHOLD then
            return
          end
          BW_BeginGripResize(selfBtn)
        end

        if not selfBtn._wfSizingStarted then return end

        local nextW = BW_Clamp((selfBtn._wfDownW or f:GetWidth() or 740) + dx, selfBtn._wfMinW or 740, selfBtn._wfMaxW)
        local nextH = BW_Clamp((selfBtn._wfDownH or f:GetHeight() or 460) - dy, selfBtn._wfMinH or 460, selfBtn._wfMaxH)
        nextW = math.floor(nextW + 0.5)
        nextH = math.floor(nextH + 0.5)

        local curW, curH = f:GetSize()
        if math.abs((curW or 0) - nextW) >= 1 or math.abs((curH or 0) - nextH) >= 1 then
          f:SetSize(nextW, nextH)
        end
      end

      local function BW_FinishGripResize(selfBtn)
        selfBtn = selfBtn or grip
        if not selfBtn then return end
        if not selfBtn._wfActive then return end

        if selfBtn.SetScript then
          selfBtn:SetScript("OnUpdate", nil)
        end

        if f and f.StopMovingOrSizing then
          f:StopMovingOrSizing()
        end

        BW_ClearGripState()
      end

      grip:SetScript("OnMouseDown", function(selfBtn, button)
        if button ~= "LeftButton" then return end
        if not (f and f.SetSize) then return end

        if f and f.StopMovingOrSizing then
          f:StopMovingOrSizing()
        end

        selfBtn._wfActive = true
        selfBtn._wfSizingStarted = false
        selfBtn._wfDownW, selfBtn._wfDownH = f:GetSize()
        selfBtn._wfDownX, selfBtn._wfDownY = BW_GetScaledCursorPosition()
        selfBtn._wfMinW, selfBtn._wfMinH, selfBtn._wfMaxW, selfBtn._wfMaxH = BW_ReadResizeBounds()

        selfBtn:SetScript("OnUpdate", function(btn)
          if not (btn and btn._wfActive) then return end

          if IsMouseButtonDown and not IsMouseButtonDown("LeftButton") then
            BW_FinishGripResize(btn)
            return
          end

          BW_ApplyGripResize(btn)
        end)
      end)

      grip:SetScript("OnMouseUp", function(selfBtn, button)
        if button ~= "LeftButton" then return end
        BW_FinishGripResize(selfBtn)
      end)

      grip:SetScript("OnHide", function()
        if f and f.StopMovingOrSizing then
          f:StopMovingOrSizing()
        end
        BW_ClearGripState()
      end)

      local meta = CreateFrame("Frame", nil, f)
      meta:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 14, -10)
      meta:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", -14, -10)
      meta:SetHeight(206)

      local ar, ag, ab = C4(ACCENT, ACCENT)
      local tr, tg, tb = C4(TEXT, TEXT)
      local mr, mg, mb = C4(MUTED, MUTED)

      local metaLabel = MakeMutedText(meta, "TOTAL WORTH")
      metaLabel:SetPoint("TOPLEFT", meta, "TOPLEFT", 0, 0)
      metaLabel:SetJustifyH("LEFT")
      metaLabel:SetTextColor(1, 1, 1)

      local totalBox = CreateFrame("Frame", nil, meta, (BackdropTemplateMixin and "BackdropTemplate") or nil)
      totalBox:SetPoint("TOPLEFT", metaLabel, "BOTTOMLEFT", 0, -4)
      totalBox:SetSize(238, 34)
      if totalBox.SetBackdrop then
        totalBox:SetBackdrop({
          bgFile = "Interface/Buttons/WHITE8X8",
          edgeFile = "Interface/Buttons/WHITE8X8",
          edgeSize = 1,
          insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        local pr, pg, pb = C4((Theme and Theme.panel) or { 0.07, 0.07, 0.07, 0.98 }, { 0.07, 0.07, 0.07, 0.98 })
        totalBox:SetBackdropColor(pr or 0.07, pg or 0.07, pb or 0.07, 0.98)
        totalBox:SetBackdropBorderColor(ar or 0.18, ag or 0.62, ab or 1.00, 0.65)
      end

      local metaTotal = totalBox:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
      metaTotal:SetPoint("LEFT", totalBox, "LEFT", 10, 0)
      metaTotal:SetPoint("RIGHT", totalBox, "RIGHT", -10, 0)
      metaTotal:SetJustifyH("LEFT")
      metaTotal:SetTextColor(1, 1, 1)

      local function MakeMetaField(anchor, labelText)
        local label = MakeMutedText(meta, labelText)
        label:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -8)
        label:SetWidth(112)
        label:SetJustifyH("LEFT")
        label:SetTextColor(ar, ag, ab)

        local value = meta:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        value:SetPoint("LEFT", label, "RIGHT", 8, 0)
        value:SetPoint("RIGHT", meta, "RIGHT", -4, 0)
        value:SetJustifyH("LEFT")
        value:SetTextColor(tr, tg, tb)

        return label, value
      end

      local scannedLabel, scannedValue = MakeMetaField(totalBox, "Scanned:")
      local sourceLabel, sourceValue = MakeMetaField(scannedLabel, "Price Source:")
      local bagsLabel, bagsValue = MakeMetaField(sourceLabel, "Bags:")
      local pbLabel, pbValue = MakeMetaField(bagsLabel, "Personal Bank:")
      local gbLabel, gbValue = MakeMetaField(pbLabel, "Guild Bank:")
      local pricedLabel, pricedValue = MakeMetaField(gbLabel, "Priced Stacks:")
      local skippedLabel, skippedValue = MakeMetaField(pricedLabel, "Skipped Stacks:")

      local tabBar = CreateFrame("Frame", nil, f)
      tabBar:SetPoint("TOPLEFT", meta, "BOTTOMLEFT", 0, -18)
      tabBar:SetPoint("TOPRIGHT", meta, "BOTTOMRIGHT", 0, -18)
      tabBar:SetHeight(24)

      local function MakeTabButton(parent, width)
        local b = CreateFrame("Button", nil, parent)
        b:SetSize(width, 22)

        b.bg = b:CreateTexture(nil, "BACKGROUND")
        b.bg:SetAllPoints(b)
        b.bg:SetColorTexture(0, 0, 0, 0.35)

        b.borderTop = b:CreateTexture(nil, "BORDER")
        b.borderTop:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
        b.borderTop:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
        b.borderTop:SetHeight(1)

        b.borderBottom = b:CreateTexture(nil, "BORDER")
        b.borderBottom:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
        b.borderBottom:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
        b.borderBottom:SetHeight(1)

        b.borderLeft = b:CreateTexture(nil, "BORDER")
        b.borderLeft:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
        b.borderLeft:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
        b.borderLeft:SetWidth(1)

        b.borderRight = b:CreateTexture(nil, "BORDER")
        b.borderRight:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
        b.borderRight:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
        b.borderRight:SetWidth(1)

        b.text = b:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        b.text:SetPoint("CENTER")

        return b
      end

      local tabCounted = MakeTabButton(tabBar, 136)
      tabCounted:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)

      local tabSkipped = MakeTabButton(tabBar, 136)
      tabSkipped:SetPoint("LEFT", tabCounted, "RIGHT", 8, 0)

      local PAD_R = 18
      local COL_GAP = 12
      local COL_TOTAL_W = 132
      local COL_EACH_W = 108
      local COL_QTY_W = 54
      local COL_CATEGORY_W = 102
      local COL_REASON_W = 248
      local SKIPPED_QTY_SHIFT = 30
      local SKIPPED_COLUMNS_LEFT_SHIFT = 112

      local cols = CreateFrame("Frame", nil, f)
      cols:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 0, -8)
      cols:SetPoint("TOPRIGHT", tabBar, "BOTTOMRIGHT", 0, -8)
      cols:SetHeight(18)

      local cItem = MakeMutedText(cols, "Item")
      cItem:SetJustifyH("LEFT")

      local cCategory = MakeMutedText(cols, "Category")
      cCategory:SetJustifyH("LEFT")
      cCategory:SetWidth(COL_CATEGORY_W)

      local cQty = MakeMutedText(cols, "Qty")
      cQty:SetJustifyH("RIGHT")
      cQty:SetWidth(COL_QTY_W)

      local cEach = MakeMutedText(cols, "Each")
      cEach:SetJustifyH("RIGHT")
      cEach:SetWidth(COL_EACH_W)

      local cTotal = MakeMutedText(cols, "Total")
      cTotal:SetJustifyH("RIGHT")
      cTotal:SetWidth(COL_TOTAL_W)

      local div = MakeDivider(f)
      div:SetPoint("TOPLEFT", cols, "BOTTOMLEFT", 0, -6)
      div:SetPoint("TOPRIGHT", cols, "BOTTOMRIGHT", 0, -6)

      local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
      if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
      scroll:SetPoint("TOPLEFT", div, "BOTTOMLEFT", 0, -6)
      scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -28, 18)

      local sc = CreateFrame("Frame", nil, scroll)
      sc:SetSize(1, 1)
      scroll:SetScrollChild(sc)

      local emptyText = MakeMutedText(sc, "No items to show.")
      emptyText:SetPoint("TOP", sc, "TOP", 0, -18)
      emptyText:SetJustifyH("CENTER")
      emptyText:Hide()

      f._bwMeta = meta
      f._bwMetaTotal = metaTotal
      f._bwMetaScanned = scannedValue
      f._bwMetaSource = sourceValue
      f._bwMetaBags = bagsValue
      f._bwMetaPB = pbValue
      f._bwMetaGB = gbValue
      f._bwMetaPriced = pricedValue
      f._bwMetaSkipped = skippedValue
      f._bwScroll = scroll
      f._bwScrollChild = sc
      f._bwRows = {}
      f._bwCountedData = {}
      f._bwSkippedData = {}
      f._bwData = {}
      f._bwCollapsedSources = {}
      f._bwView = "counted"
      f._bwSort = {
        counted = { key = "value", asc = false },
        skipped = { key = "reason", asc = true },
      }
      f._bwEmptyText = emptyText
      f._bwTabCounted = tabCounted
      f._bwTabSkipped = tabSkipped

      local ROW_H = 18

      local function Row_OnEnter(self)
        if self and self._hl then self._hl:SetAlpha(0.10) end
      end

      local function Row_OnLeave(self)
        if self and self._hl then self._hl:SetAlpha(0) end
      end

      local function Row_OnClick(self)
        if not self then return end
        if self._isDivider and self._sourceKey then
          f._bwCollapsedSources = f._bwCollapsedSources or {}
          f._bwCollapsedSources[self._sourceKey] = not f._bwCollapsedSources[self._sourceKey]
          if f._BW_Rerender then
            f:_BW_Rerender()
          end
          return
        end
        if self._link and ChatEdit_InsertLink then
          ChatEdit_InsertLink(self._link)
        end
      end

      local function BW_LinkName(link)
        if type(link) ~= "string" then return "" end
        local n = link:match("%[(.-)%]")
        if n and n ~= "" then return n end
        return link
      end

      local function BW_DisplayName(d)
        d = d or {}
        if type(d.link) == "string" and d.link ~= "" then
          -- Prefer the original hyperlink. It already contains the quality color,
          -- even when item info has not finished loading yet.
          if d.link:find("|H", 1, true) then
            return d.link, true
          end

          local itemName, itemLink = nil, nil
          if GetItemInfo then
            itemName, itemLink = GetItemInfo(d.link)
          end
          if itemLink and itemLink ~= "" then
            return itemLink, true
          end
          if itemName and itemName ~= "" then
            return itemName, false
          end
          return BW_LinkName(d.link), false
        end
        if d.name and d.name ~= "" then
          return tostring(d.name), false
        end
        return "—", false
      end

      local SORT_ICON_UP = "Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up"
      local SORT_ICON_DOWN = "Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up"

      local function BW_EnsureSortIcon(btn, anchorRegion)
        if not btn then return nil end
        if btn._bwSortIcon then return btn._bwSortIcon end

        local icon = btn:CreateTexture(nil, "OVERLAY")
        icon:SetSize(12, 12)
        if anchorRegion then
          icon:SetPoint("LEFT", anchorRegion, "RIGHT", 2, 0)
        else
          icon:SetPoint("RIGHT", btn, "RIGHT", 0, 0)
        end
        icon:SetAlpha(0.92)
        if icon.SetDesaturation then
          icon:SetDesaturation(1)
        elseif icon.SetDesaturated then
          icon:SetDesaturated(true)
        end
        icon:Hide()

        btn._bwSortIcon = icon
        return icon
      end

      local function BW_SetSortIcon(btn, anchorRegion, active, asc)
        local icon = BW_EnsureSortIcon(btn, anchorRegion)
        if not icon then return end

        if not active then
          icon:Hide()
          return
        end

        if anchorRegion and icon.ClearAllPoints then
          icon:ClearAllPoints()
          local justify = anchorRegion.GetJustifyH and anchorRegion:GetJustifyH()
          if justify == "LEFT" and anchorRegion.GetStringWidth and anchorRegion.GetWidth then
            local textW = tonumber(anchorRegion:GetStringWidth()) or 0
            local regionW = tonumber(anchorRegion:GetWidth()) or 0
            local offset = textW + 4
            if regionW > 12 and offset > (regionW - 12) then
              offset = regionW - 12
            end
            if offset < 2 then offset = 2 end
            icon:SetPoint("LEFT", anchorRegion, "LEFT", offset, 0)
          else
            icon:SetPoint("LEFT", anchorRegion, "RIGHT", 2, 0)
          end
        end

        icon:SetTexture(asc and SORT_ICON_UP or SORT_ICON_DOWN)
        local ir, ig, ib = C4(ACCENT, ACCENT)
        icon:SetVertexColor(ir or 0.18, ig or 0.62, ib or 1.00, 1)
        icon:Show()
      end

      local function BW_GetActiveSortState(self)
        local view = self._bwView or "counted"
        self._bwSort = self._bwSort or {}
        if not self._bwSort[view] then
          if view == "skipped" then
            self._bwSort[view] = { key = "reason", asc = true }
          else
            self._bwSort[view] = { key = "value", asc = false }
          end
        end
        return self._bwSort[view]
      end

      local function MakeHeaderButton(parent)
        local b = CreateFrame("Button", nil, cols)
        b:SetPoint("TOPLEFT", parent, "TOPLEFT", -8, 8)
        b:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 8, -8)
        b:EnableMouse(true)
        return b
      end

      local bItem = MakeHeaderButton(cItem)
      local bCategory = MakeHeaderButton(cCategory)
      local bQty = MakeHeaderButton(cQty)
      local bEach = MakeHeaderButton(cEach)
      local bTotal = MakeHeaderButton(cTotal)

      BW_EnsureSortIcon(bItem, cItem)
      BW_EnsureSortIcon(bCategory, cCategory)
      BW_EnsureSortIcon(bQty, cQty)
      BW_EnsureSortIcon(bEach, cEach)
      BW_EnsureSortIcon(bTotal, cTotal)

      function f:_BW_UpdateTabVisuals()
        local activeView = self._bwView or "counted"
        local activeR, activeG, activeB = C4(ACCENT, ACCENT)
        local textR, textG, textB = C4(TEXT, TEXT)
        local mutR, mutG, mutB = C4(MUTED, MUTED)
        local borderR, borderG, borderB = C4(BORDER, BORDER)

        local function PaintTab(btn, active)
          if not btn then return end
          if btn.bg then
            if active then
              btn.bg:SetColorTexture(activeR or 0.18, activeG or 0.62, activeB or 1.00, 0.18)
            else
              btn.bg:SetColorTexture(0, 0, 0, 0.28)
            end
          end
          if btn.text then
            if active then
              btn.text:SetTextColor(textR, textG, textB)
            else
              btn.text:SetTextColor(mutR, mutG, mutB)
            end
          end
          local lineR, lineG, lineB = borderR, borderG, borderB
          if active then
            lineR, lineG, lineB = activeR, activeG, activeB
          end
          if btn.borderTop then btn.borderTop:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderBottom then btn.borderBottom:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderLeft then btn.borderLeft:SetColorTexture(lineR, lineG, lineB, 1) end
          if btn.borderRight then btn.borderRight:SetColorTexture(lineR, lineG, lineB, 1) end
        end

        PaintTab(tabCounted, activeView == "counted")
        PaintTab(tabSkipped, activeView == "skipped")
      end

      function f:_BW_UpdateTabCounts()
        local countedN = 0
        for _, e in ipairs(self._bwCountedData or {}) do
          if type(e) == "table" and e.kind ~= "divider" and e.kind ~= "section" and e.kind ~= "spacer" then countedN = countedN + 1 end
        end
        local skippedN = 0
        for _, e in ipairs(self._bwSkippedData or {}) do
          if type(e) == "table" and e.kind ~= "divider" and e.kind ~= "section" and e.kind ~= "spacer" then skippedN = skippedN + 1 end
        end
        if tabCounted and tabCounted.text then
          tabCounted.text:SetText("Counted (" .. tostring(countedN) .. ")")
        end
        if tabSkipped and tabSkipped.text then
          tabSkipped.text:SetText("Skipped (" .. tostring(skippedN) .. ")")
        end
      end

      function f:_BW_UpdateSortHeader()
        local state = BW_GetActiveSortState(self)
        local key = state.key or "value"
        local asc = (state.asc == true)
        local skippedView = (self._bwView == "skipped")
        local ar, ag, ab = C4(ACCENT, ACCENT)
        local mr, mg, mb = C4(MUTED, MUTED)

        local function SetHeader(fs, btn, anchorRegion, text, active)
          if not fs then return end
          fs:SetText(text)
          if active then
            fs:SetTextColor(ar, ag, ab)
          else
            fs:SetTextColor(mr, mg, mb)
          end
          BW_SetSortIcon(btn, anchorRegion, active, asc)
        end

        SetHeader(cItem, bItem, cItem, "Item", key == "name")
        SetHeader(cQty, bQty, cQty, "Qty", key == "count")

        if skippedView then
          SetHeader(cCategory, bCategory, cCategory, "Category", false)
          SetHeader(cEach, bEach, cEach, "Why", key == "reason")
        else
          SetHeader(cCategory, bCategory, cCategory, "Category", key == "category")
          SetHeader(cEach, bEach, cEach, "Each", key == "each")
        end

        SetHeader(cTotal, bTotal, cTotal, "Total", key == "value")
      end

      local function _BW_AlignHeaderColumns()
        local sbw = (scroll and scroll.ScrollBar and scroll.ScrollBar.GetWidth and scroll.ScrollBar:GetWidth()) or 16
        local pad = (sbw or 16) + 6
        local skippedView = (f._bwView == "skipped")

        if skippedView then
          cTotal:Hide()
          bTotal:Hide()
          cCategory:Hide()
          bCategory:Hide()

          cEach:Show()
          bEach:Show()
          cEach:SetJustifyH("LEFT")
          cEach:ClearAllPoints()
          cEach:SetPoint("RIGHT", cols, "RIGHT", -(PAD_R + pad + SKIPPED_COLUMNS_LEFT_SHIFT), 0)
          cEach:SetWidth(COL_REASON_W)

          cQty:ClearAllPoints()
          cQty:SetPoint("RIGHT", cEach, "LEFT", -(COL_GAP + SKIPPED_QTY_SHIFT), 0)
          cQty:SetWidth(COL_QTY_W)
        else
          cTotal:Show()
          bTotal:Show()
          cCategory:Show()
          bCategory:Show()

          cTotal:SetJustifyH("RIGHT")
          cTotal:ClearAllPoints()
          cTotal:SetPoint("RIGHT", cols, "RIGHT", -(PAD_R + pad), 0)
          cTotal:SetWidth(COL_TOTAL_W)

          cEach:Show()
          bEach:Show()
          cEach:SetJustifyH("RIGHT")
          cEach:ClearAllPoints()
          cEach:SetPoint("RIGHT", cTotal, "LEFT", -COL_GAP, 0)
          cEach:SetWidth(COL_EACH_W)

          cQty:ClearAllPoints()
          cQty:SetPoint("RIGHT", cEach, "LEFT", -(COL_GAP + SKIPPED_QTY_SHIFT), 0)
          cQty:SetWidth(COL_QTY_W)

          cCategory:ClearAllPoints()
          cCategory:SetPoint("RIGHT", cQty, "LEFT", -COL_GAP, 0)
          cCategory:SetWidth(COL_CATEGORY_W)
        end

        cItem:ClearAllPoints()
        cItem:SetPoint("LEFT", cols, "LEFT", 28, 0)
        if skippedView then
          cItem:SetPoint("RIGHT", cQty, "LEFT", -14, 0)
        else
          cItem:SetPoint("RIGHT", cCategory, "LEFT", -12, 0)
        end
        cItem:SetJustifyH("LEFT")

        bItem:ClearAllPoints()
        bItem:SetPoint("TOPLEFT", cols, "TOPLEFT", 6, 8)
        if skippedView then
          bItem:SetPoint("BOTTOMRIGHT", cQty, "BOTTOMLEFT", -8, -8)
        else
          bItem:SetPoint("BOTTOMRIGHT", cCategory, "BOTTOMLEFT", -8, -8)
        end
      end

      local function _BW_SizeScrollChild()
        local w = (scroll and scroll.GetWidth and scroll:GetWidth()) or 0
        local sbw = (scroll and scroll.ScrollBar and scroll.ScrollBar.GetWidth and scroll.ScrollBar:GetWidth()) or 16
        w = (w or 0) - (sbw or 16) - 6
        if w < 1 then w = 1 end
        sc:SetWidth(w)
        _BW_AlignHeaderColumns()
      end

      _BW_SizeScrollChild()
      if scroll and scroll.HookScript then
        scroll:HookScript("OnShow", _BW_SizeScrollChild)
        scroll:HookScript("OnSizeChanged", _BW_SizeScrollChild)
      end
      if f and f.HookScript then
        f:HookScript("OnShow", _BW_SizeScrollChild)
        f:HookScript("OnSizeChanged", _BW_SizeScrollChild)
      end

      local function EnsureRow(i)
        local row = f._bwRows[i]
        if row then return row end

        row = CreateFrame("Button", nil, sc)
        row:SetHeight(ROW_H)
        row:SetPoint("TOPLEFT", sc, "TOPLEFT", 0, -((i - 1) * ROW_H))
        row:SetPoint("TOPRIGHT", sc, "TOPRIGHT", 0, -((i - 1) * ROW_H))

        row._bg = row:CreateTexture(nil, "BACKGROUND")
        row._bg:SetAllPoints(row)
        row._bg:SetColorTexture(0.18, 0.62, 1.00, 0.0)

        row._hl = row:CreateTexture(nil, "HIGHLIGHT")
        row._hl:SetAllPoints(row)
        row._hl:SetColorTexture(0.18, 0.62, 1.00, 0.0)

        row.icon = row:CreateTexture(nil, "ARTWORK")
        row.icon:SetSize(16, 16)
        row.icon:SetPoint("LEFT", 6, 0)

        row.name = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
        row.name:SetJustifyH("LEFT")

        row.category = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.category:SetJustifyH("LEFT")
        row.category:SetWidth(COL_CATEGORY_W)

        row.qty = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.qty:SetJustifyH("RIGHT")
        row.qty:SetWidth(COL_QTY_W)

        row.each = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.each:SetJustifyH("RIGHT")
        row.each:SetWidth(COL_EACH_W)

        row.total = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontHighlightSmall", "GameFontHighlightSmall"))
        row.total:SetJustifyH("RIGHT")
        row.total:SetWidth(COL_TOTAL_W)
        do
          local ar, ag, ab = C4(ACCENT, ACCENT)
          row.total:SetTextColor(ar, ag, ab)
        end

        row.reason = row:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontDisableSmall", "GameFontDisableSmall"))
        row.reason:SetJustifyH("LEFT")
        row.reason:SetWidth(COL_REASON_W)

        row:SetScript("OnEnter", Row_OnEnter)
        row:SetScript("OnLeave", Row_OnLeave)
        row:SetScript("OnClick", Row_OnClick)

        f._bwRows[i] = row
        return row
      end

      local function _BW_GetNum(d, key)
        d = d or {}
        if key == "count" then
          return tonumber(d.count) or 0
        end
        if key == "each" then
          return tonumber(d.each) or 0
        end
        if key == "value" then
          return tonumber(d.value) or 0
        end
        return 0
      end

      function f:_BW_ApplySort(nameFunc)
        local data = self._bwData or {}
        if #data < 2 and not (type(self._bwRawSections) == "table" and #self._bwRawSections > 0) then return end

        local state = BW_GetActiveSortState(self)
        local key = state.key or "value"
        local asc = (state.asc == true)
        local getName = nameFunc or function(link)
          if type(link) ~= "string" then return "" end
          local n = link:match("%[(.-)%]")
          if n and n ~= "" then return n end
          return link
        end

        local function RowName(row)
          row = row or {}
          if row.link then
            return tostring(getName(row.link) or "")
          end
          return tostring(row.name or "")
        end

        local function RowLess(a, b)
          a = a or {}
          b = b or {}

          if key == "name" then
            local an = RowName(a)
            local bn = RowName(b)
            if an == bn then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if ac == bc then
                local av = tonumber(a.value) or 0
                local bv = tonumber(b.value) or 0
                if asc then return av < bv end
                return av > bv
              end
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return an < bn end
            return an > bn
          end

          if key == "category" then
            local acat = tostring(a.category or "")
            local bcat = tostring(b.category or "")
            if acat == bcat then
              local av = tonumber(a.value) or 0
              local bv = tonumber(b.value) or 0
              if av == bv then
                local an = RowName(a)
                local bn = RowName(b)
                if asc then return an < bn end
                return an > bn
              end
              if asc then return av < bv end
              return av > bv
            end
            if asc then return acat < bcat end
            return acat > bcat
          end

          if key == "reason" then
            local ar = tostring(a.reason or "")
            local br = tostring(b.reason or "")
            if ar == br then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if ac == bc then
                local an = RowName(a)
                local bn = RowName(b)
                if asc then return an < bn end
                return an > bn
              end
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return ar < br end
            return ar > br
          end

          local av = _BW_GetNum(a, key)
          local bv = _BW_GetNum(b, key)
          if av == bv then
            local an = RowName(a)
            local bn = RowName(b)
            if an == bn then
              local ac = tonumber(a.count) or 0
              local bc = tonumber(b.count) or 0
              if asc then return ac < bc end
              return ac > bc
            end
            if asc then return an < bn end
            return an > bn
          end
          if asc then return av < bv end
          return av > bv
        end

        local function SortRows(rows)
          if type(rows) ~= "table" or #rows < 2 then return end
          table.sort(rows, RowLess)
        end

        -- New section model: keep the Bags / Personal Bank / Guild Bank headers in place,
        -- but sort the items inside each section. Only add a blank spacer after a section
        -- that is actually open and showing items, so collapsed headers do not look huge.
        if self._bwView ~= "skipped" and type(self._bwRawSections) == "table" and #self._bwRawSections > 0 then
          local flat = {}
          local previousSectionHadVisibleItems = false
          for i = 1, #self._bwRawSections do
            local sec = self._bwRawSections[i]
            if type(sec) == "table" then
              local items = sec.items or {}
              SortRows(items)
              if #flat > 0 and previousSectionHadVisibleItems then
                flat[#flat + 1] = { kind = "spacer" }
              end
              flat[#flat + 1] = sec
              previousSectionHadVisibleItems = false
              if not sec.collapsed then
                for j = 1, #items do
                  flat[#flat + 1] = items[j]
                end
                previousSectionHadVisibleItems = (#items > 0)
              end
            end
          end
          self._bwCountedData = flat
          self._bwData = flat
          return
        end

        local function IsSectionRow(row)
          if type(row) ~= "table" then return false end
          local kind = row.kind
          return kind == "divider" or kind == "section" or kind == "spacer"
        end

        local hasSections = false
        for i = 1, #data do
          if IsSectionRow(data[i]) then
            hasSections = true
            break
          end
        end

        if not hasSections then
          SortRows(data)
          return
        end

        -- Legacy section model: preserve each source header/spacer and sort only the
        -- item rows between headers. This keeps bank items inside bank, guild bank
        -- items inside guild bank, etc.
        local sorted = {}
        local chunk = {}

        local function FlushChunk()
          SortRows(chunk)
          for i = 1, #chunk do
            sorted[#sorted + 1] = chunk[i]
          end
          chunk = {}
        end

        for i = 1, #data do
          local row = data[i]
          if IsSectionRow(row) then
            FlushChunk()
            sorted[#sorted + 1] = row
          else
            chunk[#chunk + 1] = row
          end
        end
        FlushChunk()

        for i = 1, #sorted do
          data[i] = sorted[i]
        end
        for i = #sorted + 1, #data do
          data[i] = nil
        end
      end

      local function BW_RequestSort(key)
        local data = f._bwData or {}
        if #data < 1 then return end

        local state = BW_GetActiveSortState(f)
        if state.key ~= key then
          state.key = key
          state.asc = (key == "name" or key == "reason" or key == "category")
        else
          state.asc = not state.asc
        end

        f:_BW_ApplySort(BW_LinkName)
        f:_BW_UpdateSortHeader()
        if f._BW_Rerender then
          f:_BW_Rerender()
        end
      end

      bItem:SetScript("OnClick", function() BW_RequestSort("name") end)
      bCategory:SetScript("OnClick", function()
        if f._bwView ~= "skipped" then
          BW_RequestSort("category")
        end
      end)
      bQty:SetScript("OnClick", function() BW_RequestSort("count") end)
      bEach:SetScript("OnClick", function()
        if f._bwView == "skipped" then
          BW_RequestSort("reason")
        else
          BW_RequestSort("each")
        end
      end)
      bTotal:SetScript("OnClick", function() BW_RequestSort("value") end)

      function f:_BW_GetRenderData()
        local src = self._bwData or {}
        local out = {}
        local currentSource = nil
        local currentCollapsed = false
        local collapsed = self._bwCollapsedSources or {}
        local previousSourceHadVisibleItems = false

        for i = 1, #src do
          local d = src[i]
          if type(d) == "table" and d.kind == "spacer" then
            -- Spacing is generated here so collapsed sections do not leave huge gaps.
          elseif type(d) == "table" and d.kind == "divider" then
            if #out > 0 and previousSourceHadVisibleItems then
              out[#out + 1] = { kind = "spacer" }
            end
            currentSource = tostring(d.sourceKey or d.label or i)
            currentCollapsed = (collapsed[currentSource] == true)
            previousSourceHadVisibleItems = false
            out[#out + 1] = d
          elseif not currentSource or not currentCollapsed then
            out[#out + 1] = d
            if currentSource then
              previousSourceHadVisibleItems = true
            end
          end
        end

        return out
      end

      local function BW_RenderRows()
        local data = f:_BW_GetRenderData()
        local n = #data
        local skippedView = (f._bwView == "skipped")

        if n < 1 then
          f._bwScrollChild:SetHeight(1)
          if f._bwEmptyText then
            if skippedView then
              f._bwEmptyText:SetText("No skipped items to show.")
            else
              f._bwEmptyText:SetText("No counted items to show.")
            end
            f._bwEmptyText:Show()
          end
        else
          f._bwScrollChild:SetHeight(n * ROW_H)
          if f._bwEmptyText then
            f._bwEmptyText:Hide()
          end
        end

        local ar, ag, ab = C4(ACCENT, ACCENT)
        local mr, mg, mb = C4(MUTED, MUTED)

        for i = 1, n do
          local d = data[i]
          local row = EnsureRow(i)

          if d.kind == "spacer" then
            row._link = nil
            row._isDivider = false
            row._sourceKey = nil
            row._bg:SetAlpha(0)
            row._hl:SetAlpha(0)
            row:EnableMouse(false)

            row.icon:SetTexture(nil)
            row.icon:Hide()
            row.name:SetText("")
            row.name:Hide()
            row.qty:Hide()
            row.each:Hide()
            row.total:Hide()
            row.category:Hide()
            row.reason:Hide()
            row:Show()

          -- Divider row: section header between sources
          elseif d.kind == "divider" then
            row._link = nil
            row._isDivider = true
            row._sourceKey = tostring(d.sourceKey or d.label or i)
            row._bg:SetAlpha(0.16)
            row._hl:SetAlpha(0)
            row:EnableMouse(true)

            row.icon:SetTexture(nil)
            row.icon:Hide()

            row.name:Show()
            row.name:ClearAllPoints()
            row.name:SetPoint("LEFT", row, "LEFT", 6, 0)
            row.name:SetPoint("RIGHT", row, "RIGHT", -6, 0)
            row.name:SetJustifyH("LEFT")
            row.name:SetTextColor(ar, ag, ab)
            local label = tostring(d.label or "")
            local sourceKey = tostring(d.sourceKey or d.label or i)
            local collapsed = f._bwCollapsedSources and f._bwCollapsedSources[sourceKey]
            local prefix = collapsed and "+  " or "-  "
            local total = tonumber(d.total) or 0
            local count = tonumber(d.count) or 0
            label = prefix .. label
            if count > 0 then
              label = label .. "  •  " .. tostring(count) .. " items"
            end
            if total > 0 then
              label = label .. "  •  " .. Money(Util:Round(total))
            end
            row.name:SetText(label)

            row.qty:Hide()
            row.each:Hide()
            row.total:Hide()
            row.category:Hide()
            row.reason:Hide()
            row:Show()

          else
            row._isDivider = false
            row._sourceKey = nil
            row:EnableMouse(true)
            if row._bg and row._bg.SetAlpha then
              row._bg:SetAlpha((i % 2) == 0 and 0.03 or 0.0)
            end

            row._link = d.link

            row.name:Show()
            row.qty:Show()

            local text, isLink = BW_DisplayName(d)
            row.name:SetTextColor(C4(TEXT, TEXT))
            row.name:SetText(text)

            row.qty:SetText(tostring(d.count or 0))

            if skippedView then
              row.total:Hide()
              row.each:Hide()
              row.category:Hide()
              row.reason:Show()

              row.reason:ClearAllPoints()
              row.reason:SetPoint("RIGHT", row, "RIGHT", -(PAD_R + SKIPPED_COLUMNS_LEFT_SHIFT), 0)
              row.reason:SetWidth(COL_REASON_W)
              row.reason:SetText(tostring(d.reason or "—"))

              row.qty:ClearAllPoints()
              row.qty:SetPoint("RIGHT", row.reason, "LEFT", -(COL_GAP + SKIPPED_QTY_SHIFT), 0)
              row.qty:SetWidth(COL_QTY_W)

              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
              row.name:SetPoint("RIGHT", row.qty, "LEFT", -14, 0)
            else
              row.reason:Hide()
              row.each:Show()
              row.total:Show()
              row.category:Show()

              row.total:ClearAllPoints()
              row.total:SetPoint("RIGHT", row, "RIGHT", -PAD_R, 0)
              row.total:SetWidth(COL_TOTAL_W)
              row.total:SetText(Money(Util:Round(d.value or 0)))

              row.each:ClearAllPoints()
              row.each:SetPoint("RIGHT", row.total, "LEFT", -COL_GAP, 0)
              row.each:SetWidth(COL_EACH_W)
              row.each:SetText(Money(Util:Round(d.each or 0)))

              row.qty:ClearAllPoints()
              row.qty:SetPoint("RIGHT", row.each, "LEFT", -COL_GAP, 0)
              row.qty:SetWidth(COL_QTY_W)

              row.category:ClearAllPoints()
              row.category:SetPoint("RIGHT", row.qty, "LEFT", -COL_GAP, 0)
              row.category:SetWidth(COL_CATEGORY_W)
              row.category:SetText(tostring(d.category or "Other"))

              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row.icon, "RIGHT", 6, 0)
              row.name:SetPoint("RIGHT", row.category, "LEFT", -12, 0)
            end

            local icon = nil
            if type(d.link) == "string" and d.link ~= "" then
              if C_Item and C_Item.GetItemIconByID and GetItemInfoInstant then
                local itemID = select(1, GetItemInfoInstant(d.link))
                if itemID then
                  icon = C_Item.GetItemIconByID(itemID)
                end
              end
              if not icon and GetItemIcon and GetItemInfoInstant then
                local itemID = select(1, GetItemInfoInstant(d.link))
                if itemID then icon = GetItemIcon(itemID) end
              end
            end

            if icon then
              row.icon:SetTexture(icon)
              row.icon:Show()
            else
              row.icon:SetTexture(nil)
              row.icon:Hide()
              row.name:ClearAllPoints()
              row.name:SetPoint("LEFT", row, "LEFT", 6, 0)
              if skippedView then
                row.name:SetPoint("RIGHT", row.qty, "LEFT", -14, 0)
              else
                row.name:SetPoint("RIGHT", row.category, "LEFT", -12, 0)
              end
            end

            row:Show()
          end
        end

        for i = n + 1, #f._bwRows do
          f._bwRows[i]:Hide()
        end
      end

      function f:_BW_Rerender()
        _BW_SizeScrollChild()
        BW_RenderRows()
      end

      function f:_BW_SetView(view)
        if view ~= "skipped" then
          view = "counted"
        end
        self._bwView = view
        if view == "skipped" then
          self._bwData = self._bwSkippedData or {}
        else
          self._bwData = self._bwCountedData or {}
        end
        self:_BW_ApplySort(BW_LinkName)
        self:_BW_UpdateTabVisuals()
        self:_BW_UpdateSortHeader()
        self:_BW_Rerender()
        if self._bwScroll and self._bwScroll.SetVerticalScroll then
          self._bwScroll:SetVerticalScroll(0)
        end
      end

      function f:_BW_SetData(payload)
        payload = payload or {}
        local total = tonumber(payload.total) or 0
        local bags = tonumber(payload.bagsTotal) or 0
        local bank = tonumber(payload.bankTotal) or 0
        local gb = tonumber(payload.gbTotal) or 0
        local scope = tostring(payload.scope or "bags")

        local function BW_FormatScopeLabel(scopeValue)
          scopeValue = tostring(scopeValue or "bags")
          if scopeValue == "bank" then
            return "Personal Bank"
          end
          if scopeValue == "bagsbank" then
            return "Bags + Personal Bank"
          end
          if scopeValue == "gb" then
            return "Guild Bank"
          end
          if scopeValue == "all" then
            return "Bags + Personal Bank + Guild Bank"
          end
          return "Bags"
        end

        local function BW_FormatSourceLabel(providerStr)
          providerStr = tostring(providerStr or "")
          providerStr = providerStr:gsub("^%s+", ""):gsub("%s+$", "")
          if providerStr == "" then
            return "—"
          end

          local prov, extra = providerStr:match("^([%a_]+)%:(.+)$")
          if not prov then
            prov = providerStr
          end

          prov = tostring(prov or ""):lower()
          extra = tostring(extra or ""):gsub("^%s+", ""):gsub("%s+$", "")

          if prov == "tsm" then
            if extra ~= "" then
              return "TSM (" .. extra .. ")"
            end
            return "TSM"
          end

          if prov == "auctionator" then
            if extra ~= "" then
              return "Auctionator (" .. extra .. ")"
            end
            return "Auctionator"
          end

          if prov == "vendor" then
            return "Vendor"
          end

          if extra ~= "" then
            return tostring(prov) .. " (" .. extra .. ")"
          end

          return providerStr
        end

        local countedList = nil

        -- Build a per-source sectioned list if we have per-scope breakdowns.
        -- Each section gets a divider entry (kind="divider") followed by its items.
        local function BuildSectionedList(bagsBreakdown, bankBreakdown, gbBreakdown, scope)
          local out = {}
          local function AddSection(sourceKey, label, total, items)
            items = items or {}
            if #items > 1 then
              table.sort(items, function(a, b) return (a.value or 0) > (b.value or 0) end)
            end
            out[#out + 1] = { kind = "divider", sourceKey = sourceKey, label = label, total = total, count = #items }
            for i = 1, #items do
              if type(items[i]) == "table" then
                items[i]._sourceKey = sourceKey
                out[#out + 1] = items[i]
              end
            end
          end
          local bags = bagsBreakdown or {}
          local bank = bankBreakdown or {}
          local gb   = gbBreakdown   or {}
          -- Order: Bags, Personal Bank, Guild Bank. Dividers are clickable collapsers.
          if #bags > 0 or #bank > 0 or #gb > 0 then
            local scanScope = tostring(scope or "bags")
            local includeBags = (scanScope == "bags" or scanScope == "bagsbank" or scanScope == "all")
            local includeBank = (scanScope == "bank" or scanScope == "bagsbank" or (scanScope == "all" and payload.bankScanned == true))
            local includeGB = (scanScope == "gb" or scanScope == "all")

            if includeBags then AddSection("bags", "Bags", tonumber(payload.bagsTotal) or 0, bags) end
            if includeBank then AddSection("bank", "Personal Bank", tonumber(payload.bankTotal) or 0, bank) end
            if includeGB then AddSection("gb", "Guild Bank", tonumber(payload.gbTotal) or 0, gb) end
            return out
          end
          return nil -- fall back to flat breakdown
        end

        if type(payload.bagsBreakdown) == "table" or type(payload.bankBreakdown) == "table" or type(payload.gbBreakdown) == "table" then
          local scope = tostring(payload.scope or "bags")
          -- Always section source-aware payloads, even when only one source has
          -- counted items. Bank-only, guild-bank-only, and bags-only scans should
          -- still show the source divider/header instead of flattening the list.
          countedList = BuildSectionedList(payload.bagsBreakdown, payload.bankBreakdown, payload.gbBreakdown, scope)
        end

        if not countedList then
          if type(payload.breakdown) == "table" then
            countedList = {}
            for i = 1, #payload.breakdown do
              local e = payload.breakdown[i]
              if type(e) == "table" and (tonumber(e.value) or 0) > 0 then
                countedList[#countedList + 1] = {
                  link = e.link,
                  name = e.name,
                  itemID = tonumber(e.itemID),
                  count = tonumber(e.count) or 0,
                  each = tonumber(e.each) or 0,
                  value = tonumber(e.value) or 0,
                  category = BW_FormatCategoryLabel(e.category, e.link),
                }
              end
            end
          elseif type(payload.byItem) == "table" then
            countedList = BW_BuildBreakdownFromByItem(payload.byItem, 1000)
          elseif type(payload.top) == "table" then
            countedList = {}
            for _, e in ipairs(payload.top) do
              if type(e) == "table" then
                local link = e.link
                local c = tonumber(e.count) or 0
                local v = tonumber(e.value) or 0
                if type(link) == "string" and c > 0 and v > 0 then
                  local each = math.floor((v / c) + 0.5)
                  countedList[#countedList + 1] = {
                    link = link,
                    count = c,
                    each = each,
                    value = v,
                    category = BW_FormatCategoryLabel(e.category, link),
                  }
                end
              end
            end
          else
            countedList = {}
          end
        end

        local skippedList = {}
        local function AddSkippedSection(sourceKey, label, sourceItems)
          if type(sourceItems) ~= "table" or #sourceItems == 0 then return end
          local items = BW_BuildSkippedBreakdown(sourceItems, 1000)
          if #items == 0 then return end
          local sectionLabel = "Skipped from " .. tostring(label or "Unknown")
          skippedList[#skippedList + 1] = {
            kind = "divider",
            sourceKey = sourceKey,
            label = sectionLabel,
            count = #items,
            total = 0,
          }
          for i = 1, #items do
            items[i].source = label
            skippedList[#skippedList + 1] = items[i]
          end
        end

        local hasSourceSkippedPayload = type(payload.bagsSkippedItems) == "table" or type(payload.bankSkippedItems) == "table" or type(payload.gbSkippedItems) == "table"
        if hasSourceSkippedPayload then
          AddSkippedSection("skipped_bags", "Bags", payload.bagsSkippedItems)
          AddSkippedSection("skipped_bank", "Personal Bank", payload.bankSkippedItems)
          AddSkippedSection("skipped_guildbank", "Guild Bank", payload.gbSkippedItems)
        else
          local skippedSrc = payload.skippedBreakdown or payload.skippedItems or payload.skippedTop
          if type(skippedSrc) == "table" then
            local items = BW_BuildSkippedBreakdown(skippedSrc, 1000)
            if #items > 0 then
              local fallbackLabel = "Skipped Items"
              if scope == "bags" then
                fallbackLabel = "Skipped from Bags"
              elseif scope == "bank" then
                fallbackLabel = "Skipped from Personal Bank"
              elseif scope == "gb" then
                fallbackLabel = "Skipped from Guild Bank"
              elseif scope == "bagsbank" then
                fallbackLabel = "Skipped from Bags + Personal Bank"
              elseif scope == "all" then
                fallbackLabel = "Skipped from Selected Sources"
              end
              skippedList[#skippedList + 1] = { kind = "divider", sourceKey = "skipped", label = fallbackLabel, count = #items, total = 0 }
              for i = 1, #items do
                skippedList[#skippedList + 1] = items[i]
              end
            end
          end
        end

        self._bwCountedData = countedList or {}
        self._bwSkippedData = skippedList or {}
        self._bwCollapsedSources = self._bwCollapsedSources or {}
        self:_BW_UpdateTabCounts()

        local priced = tonumber(payload.pricedStacks) or 0
        local skipped = tonumber(payload.skippedStacks) or 0
        local scopeLabel = BW_FormatScopeLabel(scope)
        local sourceLabel = BW_FormatSourceLabel(payload.source)
        local bankScanned = (payload.bankScanned == true) or scope == "bank" or scope == "bagsbank" or scope == "all"
        local gbScanned = (scope == "gb" or scope == "all")
        local pbText = bankScanned and Money(Util:Round(bank)) or "Not scanned"
        local gbText = gbScanned and Money(Util:Round(gb)) or "Not scanned"

        if self._bwMetaTotal and self._bwMetaTotal.SetText then
          self._bwMetaTotal:SetText(Money(Util:Round(total)))
        end
        if self._bwMetaScanned and self._bwMetaScanned.SetText then
          self._bwMetaScanned:SetText(scopeLabel)
        end
        if self._bwMetaSource and self._bwMetaSource.SetText then
          self._bwMetaSource:SetText(sourceLabel)
        end
        if self._bwMetaBags and self._bwMetaBags.SetText then
          self._bwMetaBags:SetText(Money(Util:Round(bags)))
        end
        if self._bwMetaPB and self._bwMetaPB.SetText then
          self._bwMetaPB:SetText(pbText)
        end
        if self._bwMetaGB and self._bwMetaGB.SetText then
          self._bwMetaGB:SetText(gbText)
        end
        if self._bwMetaPriced and self._bwMetaPriced.SetText then
          local pricedQty = tonumber(payload.pricedQty) or 0
          self._bwMetaPriced:SetText(tostring(priced) .. " stacks / " .. tostring(pricedQty) .. " qty")
        end
        if self._bwMetaSkipped and self._bwMetaSkipped.SetText then
          local skippedQty = tonumber(payload.skippedQty) or 0
          self._bwMetaSkipped:SetText(tostring(skipped) .. " stacks / " .. tostring(skippedQty) .. " qty")
        end

        if self._bwView ~= "skipped" and self._bwView ~= "counted" then
          self._bwView = "counted"
        end
        self:_BW_SetView(self._bwView or "counted")
      end

      tabCounted:SetScript("OnClick", function()
        f:_BW_SetView("counted")
      end)
      tabSkipped:SetScript("OnClick", function()
        f:_BW_SetView("skipped")
      end)

      scroll:EnableMouseWheel(true)
      scroll:SetScript("OnMouseWheel", function(_, delta)
        local cur = scroll:GetVerticalScroll() or 0
        local step = 36
        local nextV = cur - (delta * step)
        if nextV < 0 then nextV = 0 end
        local range = scroll:GetVerticalScrollRange() or 0
        if nextV > range then nextV = range end
        scroll:SetVerticalScroll(nextV)
      end)

      f:_BW_UpdateTabCounts()
      f:_BW_UpdateTabVisuals()
      f:_BW_UpdateSortHeader()

      UI._bwDetailsFrame = f
      return f
    end
    local function BW_OpenDetails()
      local payload = UI._bwLastPayload
      if type(payload) ~= "table" then
        if UIErrorsFrame and UIErrorsFrame.AddMessage then
          UIErrorsFrame:AddMessage("WhatFarm: Press 'Refresh Worth' first to generate a breakdown.", 0.35, 0.78, 1.00)
        end
        return
      end

      local f = BW_EnsureDetailsFrame()
      if f and f._BW_SetData then
        f:_BW_SetData(payload)
      end
      f:Show()
      BW_ClaimUtilityWindowFocus(f)
    end

    BW_SetDetailsEnabled(false)





    local function IsGBOpen()
      if IsGuildBankFrameOpen then
        return IsGuildBankFrameOpen()
      end
      if GuildBankFrame and GuildBankFrame.IsShown and GuildBankFrame:IsShown() then
        return true
      end
      return false
    end

    local function BagWorthSourceLabel(providerStr)
      providerStr = tostring(providerStr or "")
      local prov, extra = providerStr:match("^([%a_]+)%:(.+)$")
      if not prov then prov = providerStr end
      prov = tostring(prov or ""):lower()
      extra = tostring(extra or "")

      if prov == "tsm" then
        local lbl = extra
        if TSMStringToLabel then
          lbl = TSMStringToLabel(extra) or extra
        end
        if lbl == "" then lbl = "Default" end
        return "TSM • " .. lbl
      end

      if prov == "auctionator" then
        return "Auctionator"
      end

      if prov == "vendor" or prov == "" then
        return "Vendor"
      end

      return providerStr
    end

    function UI:RefreshBagWorth(opt)
      opt = opt or {}

      -- Bag Worth saved options (and optional remembered worth)
      local bwRemember = true
      if _BW_Get then
        bwRemember = (_BW_Get("bwRememberWorth", true) ~= false)
      elseif WF and WF.DB and WF.DB.GetSetting then
        local v = WF.DB:GetSetting("bwRememberWorth")
        if v == false then bwRemember = false end
      end

      if bwRemember and not UI._bwLastPayload and WF and WF.DB and WF.DB.GetSetting then
        local saved = WF.DB:GetSetting("bwLastPayload")
        if type(saved) == "table" then
          UI._bwLastPayload = saved
        end
      end

      local providerStr = (WF.Price and WF.Price.GetProvider) and WF.Price:GetProvider() or "vendor"
      bwSource:SetText("Source: " .. BagWorthSourceLabel(providerStr))
      bwPB:SetText("Personal Bank - open bank + refresh")

      -- Manual-only display (no scan)
      if not opt.force then
        local last = UI._bwLastPayload
        BW_SetDetailsEnabled(last and (type(last.byItem) == "table" or type(last.top) == "table" or type(last.skippedTop) == "table"))
        if last then
          local total = tonumber(last.total) or 0
          local bagsTotal = tonumber(last.bagsTotal) or 0
          local bankTotal = tonumber(last.bankTotal) or 0
          local bankScanned = (last.bankScanned == true)
          local gbTotal = tonumber(last.gbTotal) or 0

          bwTotal:SetText("Total - " .. Money(Util:Round(total)))
          bwBags:SetText("Bags - " .. Money(Util:Round(bagsTotal)))

          if bankScanned then
            bwPB:SetText("Personal Bank - " .. Money(Util:Round(bankTotal)))
          else
            bwPB:SetText("Personal Bank - open bank + refresh")
          end

          if gbTotal > 0 then
            bwGB:SetText("Guild Bank - " .. Money(Util:Round(gbTotal)))
          else
            bwGB:SetText("Guild Bank - open guild bank + refresh")
          end

          bwProgress:SetText("")
        else
          bwTotal:SetText("Total - —")
          bwBags:SetText("Bags - —")
          bwPB:SetText("Personal Bank - —")
          bwGB:SetText("Guild Bank - —")
          bwProgress:SetText("") -- removed "Press Refresh Worth"
        end
        return
      end

      -- Forced scan (button)
      if not (WF and WF.BagWorth and WF.BagWorth.Init and WF.BagWorth.Scan) then
        bwTotal:SetText("Total - —")
        bwBags:SetText("Bags - —")
        bwGB:SetText("Guild Bank - —")
        bwProgress:SetText("Bag Worth module not loaded")
        return
      end

      WF.BagWorth:Init()

      local function IsBankOpen()
        if C_PlayerInteractionManager and Enum and Enum.PlayerInteractionType and Enum.PlayerInteractionType.Banker and C_PlayerInteractionManager.IsInteractingWithNpcOfType then
          local ok, isBanker = pcall(C_PlayerInteractionManager.IsInteractingWithNpcOfType, Enum.PlayerInteractionType.Banker)
          if ok and isBanker then return true end
        end
        if BankFrame and BankFrame.IsShown and BankFrame:IsShown() then return true end
        return false
      end

      local gbOpen = IsGBOpen()
      local bankOpen = IsBankOpen()

      local scope = "bags"
      if gbOpen then
        scope = "all"
      elseif bankOpen then
        scope = "bagsbank"
      end

      bwProgress:SetText("Scanning…")
      BW_SetDetailsEnabled(false)
      BW_SetScanButtonState(true)
      do
        local df = UI._bwDetailsFrame
        if df and df.IsShown and df:IsShown() then
          if df._bwMetaTotal and df._bwMetaTotal.SetText then df._bwMetaTotal:SetText("—") end
          if df._bwMetaScanned and df._bwMetaScanned.SetText then df._bwMetaScanned:SetText("Scanning…") end
          if df._bwMetaSource and df._bwMetaSource.SetText then df._bwMetaSource:SetText("—") end
          if df._bwMetaBags and df._bwMetaBags.SetText then df._bwMetaBags:SetText("—") end
          if df._bwMetaPB and df._bwMetaPB.SetText then df._bwMetaPB:SetText("Not scanned") end
          if df._bwMetaGB and df._bwMetaGB.SetText then df._bwMetaGB:SetText("Not scanned") end
          if df._bwMetaPriced and df._bwMetaPriced.SetText then df._bwMetaPriced:SetText("—") end
          if df._bwMetaSkipped and df._bwMetaSkipped.SetText then df._bwMetaSkipped:SetText("—") end
        end
      end

      if bankOpen then
        bwPB:SetText("Personal Bank - scanning…")
      else
        bwPB:SetText("Personal Bank - open bank + refresh")
      end

      local scanOpt = { force = true }
      do
        local function getBW(key, default)
          if _BW_Get then return _BW_Get(key, default) end
          if WF and WF.DB and WF.DB.GetSetting then
            local v = WF.DB:GetSetting(key)
            if v ~= nil then return v end
          end
          return default
        end
        scanOpt.filters = {
          greys        = getBW("bwIncludeGreys",        false),
          whites       = getBW("bwIncludeWhites",       false),
          greens       = getBW("bwIncludeGreens",       true),
          blues        = getBW("bwIncludeBlues",        true),
          epics        = getBW("bwIncludeEpics",        true),
          mats         = getBW("bwIncludeMats",         true),
          gear         = getBW("bwIncludeGear",         true),
          patterns     = getBW("bwIncludePatterns",     true),
          pets         = getBW("bwIncludePets",         true),
          toys         = getBW("bwIncludeToys",         true),
          consumables  = getBW("bwIncludeConsumables",  true),
          gems         = getBW("bwIncludeGems",         true),
        }
      end

      local function BW_HandleScanEvent(event, payload)
        if type(event) ~= "string" then
          BW_FinishScanState("Bag Worth scan failed")
          return
        end

        if type(payload) ~= "table" then
          if event == "error" then
            BW_FinishScanState("Bag Worth scan failed")
          end
          return
        end

        if event == "progress" then
          local tab = tonumber(payload.tab) or 0
          local tabs = tonumber(payload.tabs) or 0
          if tabs > 0 and tab > 0 then
            bwProgress:SetText("Scanning guild bank " .. tab .. "/" .. tabs .. "…")
          else
            bwProgress:SetText("Scanning…")
          end
          return
        end

        if event == "error" then
          BW_FinishScanState(tostring(payload.message or "Bag Worth scan failed"))
          return
        end

        if event == "done" then
          local livePayload = BW_MakeCompactPayload(payload)

          UI._bwLastPayload = livePayload

          if bwRemember and WF and WF.DB and WF.DB.SetSetting then
            WF.DB:SetSetting("bwLastPayload", livePayload)
            WF.DB:SetSetting("bwLastProvider", providerStr)
            if time then WF.DB:SetSetting("bwLastAt", time()) end
          end

          BW_SetDetailsEnabled(true)
          do
            local df = UI._bwDetailsFrame
            if df and df.IsShown and df:IsShown() and df._BW_SetData then
              df:_BW_SetData(livePayload)
            end
          end

          local total = tonumber(payload.total) or 0
          local bagsTotal = tonumber(payload.bagsTotal) or 0
          local bankTotal = tonumber(payload.bankTotal) or 0
          local bankScanned = (payload.bankScanned == true)
          local gbTotal = tonumber(payload.gbTotal) or 0

          bwTotal:SetText("Total - " .. Money(Util:Round(total)))
          bwBags:SetText("Bags - " .. Money(Util:Round(bagsTotal)))

          if bankScanned then
            bwPB:SetText("Personal Bank - " .. Money(Util:Round(bankTotal)))
          else
            bwPB:SetText("Personal Bank - open bank + refresh")
          end

          if gbTotal > 0 then
            bwGB:SetText("Guild Bank - " .. Money(Util:Round(gbTotal)))
          else
            bwGB:SetText("Guild Bank - open guild bank + refresh")
          end

          if payload.pending == true then
            bwProgress:SetText("Scanning guild bank…")
          else
            BW_FinishScanState("")
          end
          return
        end

        BW_FinishScanState("Bag Worth scan failed")
      end

      local ok, err = xpcall(function()
        WF.BagWorth:Scan(scope, providerStr, BW_HandleScanEvent, scanOpt)
      end, geterrorhandler())

      if not ok then
        BW_FinishScanState("Bag Worth scan failed")
        return
      end

      -- Safety net: if the scan never fires done/error (e.g. stalled GB scan),
      -- release the button after 30 seconds so it doesn't stay stuck forever.
      local scanToken = (UI._bwScanToken or 0) + 1
      UI._bwScanToken = scanToken
      C_Timer.After(30, function()
        if UI._bwScanToken == scanToken and UI._bwScanRunning then
          BW_FinishScanState("Scan timed out")
        end
      end)
    end

  if bBW and bBW.SetScript then
    bBW:SetScript("OnClick", function()
      if IsShiftKeyDown and IsShiftKeyDown() then
        BW_OpenDetails()
      else
        UI:RefreshBagWorth({ force = true })
      end
      BW_UpdateActionButton()
    end)
  end

  return { bBW = bBW, bottom = bBW }
end
