-- UI/HelpWindow.lua
-- What Farm? :: Settings & Intelligence Panel
-- Premium UI: custom combobox, pooled rows, responsive layout, zero junk globals.

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

local Widgets = WF.UI.Widgets
local Theme   = WF.UI.Theme

local BODY_FONT = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.body) or ("Interface\\AddOns\\" .. tostring(ADDON_NAME) .. "\\Media\\Fonts\\PTSansNarrow.ttf")
local DISPLAY_FONT = (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.display) or ("Interface\\AddOns\\" .. tostring(ADDON_NAME) .. "\\Media\\Fonts\\Expressway.ttf")


-- -------------------------------------------------------
-- Checkboxes: force WhatFarm accent blue checks (not Blizzard green)
-- -------------------------------------------------------
local function WF_ApplyBlueCheck(btn)
  if not btn or btn._wfBlueCheck then return end
  btn._wfBlueCheck = true

  local function tint(tex, a)
    if not tex or not tex.SetVertexColor then return end
    if tex.SetDesaturated then
      tex:SetDesaturated(true) -- Blizzard check texture is green; desaturate first so tint works
    end
    tex:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], a or 1)
  end

  local function apply()
    tint(btn.GetCheckedTexture and btn:GetCheckedTexture(), 1)
    tint(btn.GetDisabledCheckedTexture and btn:GetDisabledCheckedTexture(), 0.65)
    if btn.Check then tint(btn.Check, 1) end
  end

  apply()
  if btn.HookScript then
    btn:HookScript("OnShow", apply)
  end
end
local Util    = WF.Util

WF.UI.Help = WF.UI.Help or {}
local UI = WF.UI.Help

local function WF_TrySetPropagateKeyboardInput(frame, propagate)
  if not (frame and frame.SetPropagateKeyboardInput) then return end
  if InCombatLockdown and InCombatLockdown() then return end
  pcall(frame.SetPropagateKeyboardInput, frame, propagate and true or false)
end

local function DisplayProfileName(name)
  name = tostring(name or "")
  if name == "" then
    return "Default"
  end
  return name
end


-- -------------------------------------------------------
-- -------------------------------------------------------
-- Quality label constants (kept local; no globals)
-- -------------------------------------------------------
local QUALITY_LABEL = {
  [0] = "Poor+",
  [1] = "Common+",
  [2] = "Uncommon+",
  [3] = "Rare+",
  [4] = "Epic+",
  [5] = "Legendary+",
}
local QUALITY_ITEMS = { "Poor+","Common+","Uncommon+","Rare+","Epic+","Legendary+" }
local QUALITY_VALUE = {
  ["Poor+"] = 0,
  ["Common+"] = 1,
  ["Uncommon+"] = 2,
  ["Rare+"] = 3,
  ["Epic+"] = 4,
  ["Legendary+"] = 5,
}

-- Constants / Lists
-- -------------------------------------------------------
local EXPANSIONS = {
  "Any","Classic","TBC","Wrath","Cata","MoP","WoD","Legion","BfA","Shadowlands",
  "Dragonflight","The War Within","Midnight","Other",
}

local FARM_TYPES = {
  "Any","Instance","OpenWorld","Gathering","Other",
}


local FARM_AIMS = {
  "Unknown",
  "Raw Gold",
  "Materials",
  "Transmog",
  "Mounts",
  "Pets",
  "Reputation",
  "Achievements",
  "Other",
}

local RANDOM_MODES  = { "filtered", "any" }
local PRICE_SOURCES = { "vendor", "tsm", "auctionator" }

local TSM_SOURCE_ROWS = {
  { "Market Value", "dbmarket" },
  { "Historical Price", "dbhistorical" },
  { "Recent Value", "dbrecent" },
  { "Min Buyout", "dbminbuyout" },
  { "Region Market Value Average", "dbregionmarketavg" },
  { "Region Historical Price", "dbregionhistorical" },
  { "Region Sale Average", "dbregionsaleavg" },
  { "Region Sale Rate", "dbregionsalerate" },
  { "Region Avg Daily Sold", "dbregionavgdailysold" },
  { "Region Global Sale Average", "dbregionglobalsaleavg" },
}

local TSM_LABELS = {}
local TSM_BY_LABEL = {}
local TSM_BY_STR = {}
do
  for _, row in ipairs(TSM_SOURCE_ROWS) do
    local label, str = row[1], row[2]
    TSM_LABELS[#TSM_LABELS + 1] = label
    TSM_BY_LABEL[label] = str
    TSM_BY_STR[str] = label
  end
end

local function TSMStringToLabel(str)
  str = tostring(str or "")
  str = str:gsub("^%s+", ""):gsub("%s+$", "")
  if str == "" or str == "vendorsell" then
    return TSM_LABELS[1] or "dbmarket"
  end
  return TSM_BY_STR[str] or str
end

-- -------------------------------------------------------
-- Theme Helpers
-- -------------------------------------------------------
local function C4(t, fallback)
  if type(t) == "table" then return t[1], t[2], t[3], t[4] end
  if type(fallback) == "table" then return fallback[1], fallback[2], fallback[3], fallback[4] end
  return 1, 1, 1, 1
end

local ACCENT = Theme.accent or { 0.35, 0.78, 1.00, 1.00 }
local TEXT   = Theme.text   or { 0.92, 0.92, 0.92, 1.00 }
local MUTED  = Theme.muted  or { 0.65, 0.65, 0.65, 1.00 }
local BG     = Theme.bg     or { 0.06, 0.06, 0.06, 0.96 }
local PANEL  = Theme.panel  or { 0.09, 0.09, 0.09, 0.94 }
local BORDER = Theme.border or { 0.22, 0.22, 0.22, 0.90 }

local MENU_BG = {
  math.max(0, math.min(1, (BG[1] * 0.75) + (PANEL[1] * 0.25) - 0.012)),
  math.max(0, math.min(1, (BG[2] * 0.75) + (PANEL[2] * 0.25) - 0.012)),
  math.max(0, math.min(1, (BG[3] * 0.75) + (PANEL[3] * 0.25) - 0.012)),
  1.00,
}
local MENU_BORDER = {
  math.min(1, (BORDER[1] * 0.90) + (ACCENT[1] * 0.10)),
  math.min(1, (BORDER[2] * 0.90) + (ACCENT[2] * 0.10)),
  math.min(1, (BORDER[3] * 0.90) + (ACCENT[3] * 0.10)),
  0.92,
}
local MENU_HOVER = { ACCENT[1], ACCENT[2], ACCENT[3], 0.08 }
local MENU_SELECTED = { ACCENT[1], ACCENT[2], ACCENT[3], 0.12 }

local function WF_HideTexture(tex)
  if not tex then return end
  if tex.SetTexture then pcall(tex.SetTexture, tex, nil) end
  if tex.SetAlpha then tex:SetAlpha(0) end
  if tex.Hide then tex:Hide() end
end

local function WF_IsElvUIEnabled()
  if _G.ElvUI then return true end
  if IsAddOnLoaded and IsAddOnLoaded("ElvUI") then return true end
  return false
end

local function WF_ApplyStaticPopupButtonText(btn, enabled)
  if not btn then return end

  if btn.SetNormalFontObject then btn:SetNormalFontObject("WF_GameFontNormal") end
  if btn.SetHighlightFontObject then btn:SetHighlightFontObject("WF_GameFontNormal") end
  if btn.SetDisabledFontObject then btn:SetDisabledFontObject("WF_GameFontNormal") end

  local fs = btn.GetFontString and btn:GetFontString() or nil
  if fs then
    fs:SetFontObject("WF_GameFontNormal")
    if fs.SetShadowOffset then fs:SetShadowOffset(0, 0) end
    if fs.SetShadowColor then fs:SetShadowColor(0, 0, 0, 0) end
    if fs.SetTextColor then
      if enabled == false then
        fs:SetTextColor(MUTED[1], MUTED[2], MUTED[3], 0.85)
      else
        fs:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1.00)
      end
    end
  end
end

local function WF_StyleStaticPopupButton(btn)
  if not btn then return end

  local usingElvUI = WF_IsElvUIEnabled()
  local function restyleLater(self)
    if not self or not self:IsShown() then return end
    if C_Timer and C_Timer.After then
      C_Timer.After(0, function()
        if self and self:IsShown() then
          WF_ApplyStaticPopupButtonText(self, (not self.IsEnabled) or self:IsEnabled())
        end
      end)
    end
  end

  if not btn._wfElvTextStyled then
    btn._wfElvTextStyled = true
    btn:HookScript("OnShow", function(self)
      WF_ApplyStaticPopupButtonText(self, (not self.IsEnabled) or self:IsEnabled())
      restyleLater(self)
    end)
  end

  WF_ApplyStaticPopupButtonText(btn, (not btn.IsEnabled) or btn:IsEnabled())

  if usingElvUI then
    return
  end

  if not btn._wfElvSkin then
    btn._wfElvSkin = true

    local regions = { btn:GetRegions() }
    for i = 1, #regions do
      local r = regions[i]
      if r and r.GetObjectType and r:GetObjectType() == "Texture" then
        WF_HideTexture(r)
      end
    end

    local bg = CreateFrame("Frame", nil, btn, "BackdropTemplate")
    bg:SetPoint("TOPLEFT", 0, 0)
    bg:SetPoint("BOTTOMRIGHT", 0, 0)
    bg:SetFrameLevel(math.max(0, (btn:GetFrameLevel() or 1) - 1))
    bg:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
    })
    btn._wfElvBg = bg

    local glow = bg:CreateTexture(nil, "ARTWORK")
    glow:SetAllPoints()
    glow:SetTexture("Interface/Buttons/WHITE8x8")
    glow:SetBlendMode("ADD")
    btn._wfElvGlow = glow

    local function apply(self, state)
      local enabled = (not self.IsEnabled) or self:IsEnabled()
      local hover = self.IsMouseOver and self:IsMouseOver()
      local down = (state == "down")

      if not enabled then
        if self._wfElvBg then
          self._wfElvBg:SetBackdropColor(0.06, 0.06, 0.06, 0.92)
          self._wfElvBg:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], 0.55)
        end
        if self._wfElvGlow then
          self._wfElvGlow:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], 0.00)
        end
        WF_ApplyStaticPopupButtonText(self, false)
        return
      end

      local bgA = 0.96
      local brA = 0.92
      local glowA = 0.00
      if down then
        bgA = 0.98
        brA = 1.00
        glowA = 0.08
      elseif hover then
        bgA = 0.98
        brA = 0.98
        glowA = 0.05
      end

      if self._wfElvBg then
        self._wfElvBg:SetBackdropColor(0.05, 0.05, 0.05, bgA)
        self._wfElvBg:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], brA)
      end
      if self._wfElvGlow then
        self._wfElvGlow:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], glowA)
      end
      WF_ApplyStaticPopupButtonText(self, true)
    end

    btn:HookScript("OnEnter", function(self) apply(self, "hover") end)
    btn:HookScript("OnLeave", function(self) apply(self) end)
    btn:HookScript("OnMouseDown", function(self) apply(self, "down") end)
    btn:HookScript("OnMouseUp", function(self) apply(self) end)
    btn:HookScript("OnEnable", function(self) apply(self) restyleLater(self) end)
    btn:HookScript("OnDisable", function(self) apply(self) restyleLater(self) end)
  end

  if btn._wfElvBg then
    btn._wfElvBg:SetBackdropColor(0.05, 0.05, 0.05, 0.96)
    btn._wfElvBg:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], 0.92)
  end
  if btn._wfElvGlow then
    btn._wfElvGlow:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], 0.00)
  end
end

local function WF_StyleStaticPopupEditBox(eb)
  if not eb then return end

  local usingElvUI = WF_IsElvUIEnabled()

  if usingElvUI then
    if not eb._wfElvTextStyled then
      eb._wfElvTextStyled = true
      if eb.SetTextInsets then eb:SetTextInsets(8, 8, 3, 3) end
      if eb.SetFontObject then eb:SetFontObject("WF_GameFontHighlight") end
    end
    if eb.SetTextColor then eb:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1.00) end
    return
  end

  if not eb._wfElvSkin then
    eb._wfElvSkin = true

    local regions = { eb:GetRegions() }
    for i = 1, #regions do
      local r = regions[i]
      if r and r.GetObjectType and r:GetObjectType() == "Texture" then
        WF_HideTexture(r)
      end
    end

    local bg = CreateFrame("Frame", nil, eb, "BackdropTemplate")
    bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -6, 0)
    bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 6, 0)
    bg:SetFrameLevel(math.max(0, (eb:GetFrameLevel() or 1) - 1))
    bg:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
    })
    eb._wfElvBg = bg

    local fill = bg:CreateTexture(nil, "ARTWORK")
    fill:SetAllPoints()
    fill:SetTexture("Interface/Buttons/WHITE8x8")
    fill:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], 0.015)
    eb._wfElvFill = fill

    if eb.SetTextInsets then eb:SetTextInsets(8, 8, 3, 3) end
    if eb.SetFontObject then eb:SetFontObject("WF_GameFontHighlight") end
  end

  if eb._wfElvBg then
    eb._wfElvBg:SetBackdropColor(0.05, 0.05, 0.05, 0.98)
    eb._wfElvBg:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], 0.95)
  end
  if eb.SetTextColor then eb:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1.00) end
end

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

  local usingElvUI = WF_IsElvUIEnabled()

  if not usingElvUI and not frame._wfElvSkin then
    frame._wfElvSkin = true

    local regions = { frame:GetRegions() }
    for i = 1, #regions do
      local r = regions[i]
      if r and r.GetObjectType and r:GetObjectType() == "Texture" then
        WF_HideTexture(r)
      end
    end

    local bg = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    bg:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -8)
    bg:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -8, 8)
    bg:SetFrameLevel(math.max(0, (frame:GetFrameLevel() or 1) - 1))
    bg:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
    })
    bg:SetBackdropColor(0.04, 0.04, 0.04, 0.97)
    bg:SetBackdropBorderColor(BORDER[1], BORDER[2], BORDER[3], 0.96)
    frame._wfElvBg = bg

    local glow = bg:CreateTexture(nil, "ARTWORK")
    glow:SetAllPoints()
    glow:SetTexture("Interface/Buttons/WHITE8x8")
    glow:SetVertexColor(ACCENT[1], ACCENT[2], ACCENT[3], 0.02)
    frame._wfElvGlow = glow
  end

  local txt = frame.text or (frame.GetName and _G[frame:GetName() .. "Text"])
  if txt then
    txt:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1.00)
    txt:SetJustifyH("CENTER")
  end

  WF_StyleStaticPopupEditBox(frame.editBox or frame.wideEditBox)

  local btn1 = frame.button1 or (frame.GetName and _G[frame:GetName() .. "Button1"])
  local btn2 = frame.button2 or (frame.GetName and _G[frame:GetName() .. "Button2"])
  local btn3 = frame.button3 or (frame.GetName and _G[frame:GetName() .. "Button3"])
  local btn4 = frame.button4 or (frame.GetName and _G[frame:GetName() .. "Button4"])
  WF_StyleStaticPopupButton(btn1)
  WF_StyleStaticPopupButton(btn2)
  WF_StyleStaticPopupButton(btn3)
  WF_StyleStaticPopupButton(btn4)
end
local MENU_SEPARATOR = {
  math.min(1, (BORDER[1] * 0.96) + (ACCENT[1] * 0.04)),
  math.min(1, (BORDER[2] * 0.96) + (ACCENT[2] * 0.04)),
  math.min(1, (BORDER[3] * 0.96) + (ACCENT[3] * 0.04)),
  0.45,
}

local function Money(c)
  c = tonumber(c) or 0
  local neg = c < 0
  if neg then c = -c end

  local g = math.floor(c / 10000)
  local s = math.floor((c % 10000) / 100)
  local cp = math.floor(c % 100)

  local gs = tostring(g)
  if BreakUpLargeNumbers then
    gs = BreakUpLargeNumbers(g)
  else
    gs = gs:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
  end

  if neg then gs = "-" .. gs end
  return string.format("%sg %ds %dc", gs, s, cp)
end

-- -------------------------------------------------------
-- Shared popups + helpers: rename farm / session notes
-- (Used by Overview + History so functionality works no matter which tab is opened first.)
-- -------------------------------------------------------
local POP_RENAME_FARM = "WF_RENAME_FARM"
local POP_RUN_NOTE    = "WF_RUN_NOTE"

local RebuildHistoryAggregates
local _WFSyncSessionFarmLinkByName

local function _WFTrim(s)
  s = tostring(s or "")
  s = s:gsub("^%s+", ""):gsub("%s+$", "")
  return s
end


-- -------------------------------------------------------
-- Rename debugging (enable Settings -> Debug to see these)
-- -------------------------------------------------------
local function _WFRenameDbg(msg)
  if not WF or not WF.DB or not WF.DB.GetSetting then return end
  if not WF.DB:GetSetting("debug") then return end

  local t = ""
  if date then
    t = tostring(date("%H:%M:%S") or "")
  end

  local line = "WF-Rename: " .. tostring(msg)
  if t ~= "" then
    line = "[" .. t .. "] " .. line
  end

  if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
    DEFAULT_CHAT_FRAME:AddMessage("|cff66c2ff" .. line .. "|r")
  else
    print(line)
  end
end

local function _WFRenameSessBrief(sess)
  if not sess then return "sess=nil" end
  return "startedAt=" .. tostring(sess.startedAt)
      .. " endedAt=" .. tostring(sess.endedAt)
      .. " elapsed=" .. tostring(sess.elapsed)
      .. " farmId=" .. tostring(sess.farmId)
      .. " mapId=" .. tostring(sess.mapId)
      .. " zone=" .. tostring(sess.zone)
end

local function _WFNormLooseName(s)
  s = _WFTrim(s)
  if s == "" then return "" end
  return string.lower(s)
end

local function _WFRenameGetPopupText(dialog)
  local eb = nil
  local src = "none"

  if dialog and dialog.editBox and dialog.editBox.IsShown and dialog.editBox:IsShown() then
    eb = dialog.editBox
    src = "editBox"
  elseif dialog and dialog.wideEditBox and dialog.wideEditBox.IsShown and dialog.wideEditBox:IsShown() then
    eb = dialog.wideEditBox
    src = "wideEditBox"
  end

  local focus = (GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus()) or nil
  if (not eb) and focus and focus.GetParent and focus:GetParent() == dialog then
    eb = focus
    src = "focus"
  end

  local txt = ""
  if eb and eb.GetText then
    txt = tostring(eb:GetText() or "")
  end

  -- Extra fallbacks: on some client builds the shown editbox isn't the one we expect.
  if txt == "" and dialog then
    if dialog.editBox and dialog.editBox.GetText then
      local t1 = tostring(dialog.editBox:GetText() or "")
      if t1 ~= "" then
        txt = t1
        src = src .. "+editBox"
      end
    end
    if txt == "" and dialog.wideEditBox and dialog.wideEditBox.GetText then
      local t2 = tostring(dialog.wideEditBox:GetText() or "")
      if t2 ~= "" then
        txt = t2
        src = src .. "+wideEditBox"
      end
    end
    if txt == "" and focus and focus.GetText then
      local tf = tostring(focus:GetText() or "")
      if tf ~= "" then
        txt = tf
        src = src .. "+focus"
      end
    end
  end

  return txt, src, eb, focus
end

function UI:_EnsureHistoryPopups()
  if self._wfHistoryPopupsReady then return end
  self._wfHistoryPopupsReady = true
  if not StaticPopupDialogs then return end

  if not StaticPopupDialogs[POP_RENAME_FARM] then
    StaticPopupDialogs[POP_RENAME_FARM] = {
      text = "Rename farm",
      button1 = "Save",
      button2 = "Cancel",
      hasEditBox = true,
      editBoxWidth = 240,
      maxLetters = 64,
      timeout = 0,
      whileDead = true,
      hideOnEscape = true,
      preferredIndex = 3,
      OnShow = function(self, data)
        if self.text then self.text:SetText("Rename farm") end
        WF_StyleStaticPopupFrame(self)
        local eb = self.editBox or self.wideEditBox
        local v = (data and data.initial) or ""
        if eb then
          eb:SetText(v)
          eb:HighlightText()
          eb:SetFocus()
        end

        self._wfRenameLastText = v
        self._wfRenamePrinted = nil
        _WFRenameDbg("Popup OnShow fired. which=" .. tostring(self.which) .. " data=" .. tostring(data) .. " eb=" .. tostring(eb) .. " initial='" .. tostring(v) .. "'")
      end,
      OnAccept = function(self, data)
        local d = data or self.data or (self.GetParent and self:GetParent() and self:GetParent().data)
        local txt, src = _WFRenameGetPopupText(self)

        -- Some client builds lose the editbox value when clicking the Save button.
        -- Fall back to the last text we observed via EditBoxOnTextChanged.
        if _WFTrim(txt) == "" then
          local lt = _WFTrim(self._wfRenameLastText)
          if lt ~= "" then
            txt = lt
            src = tostring(src) .. "+lastText"
          end
        end

        _WFRenameDbg("Popup OnAccept fired. which=" .. tostring(self.which)
          .. " data=" .. tostring(data)
          .. " d=" .. tostring(d)
          .. " src=" .. tostring(src)
          .. " text='" .. tostring(txt) .. "'"
          .. " lastText='" .. tostring(self._wfRenameLastText) .. "'")

        if not d or not d.ui or not d.sess then return end
        _WFRenameDbg("RenameFarmFromSession called. " .. _WFRenameSessBrief(d.sess))
        d.ui:RenameFarmFromSession(d.sess, txt)
      end,
      EditBoxOnTextChanged = function(self)
        local parent = self:GetParent()
        if parent then
          parent._wfRenameLastText = tostring(self:GetText() or "")
        end
      end,
      EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local data = parent and parent.data
        local txt, src = _WFRenameGetPopupText(parent)

        if _WFTrim(txt) == "" and parent then
          local lt = _WFTrim(parent._wfRenameLastText)
          if lt ~= "" then
            txt = lt
            src = tostring(src) .. "+lastText"
          end
        end

        _WFRenameDbg("EditBoxOnEnterPressed. which=" .. tostring(parent and parent.which)
          .. " data=" .. tostring(data)
          .. " src=" .. tostring(src)
          .. " text='" .. tostring(txt) .. "'"
          .. " lastText='" .. tostring(parent and parent._wfRenameLastText) .. "'")

        if not data or not data.ui or not data.sess then
          if parent then parent:Hide() end
          return
        end

        data.ui:RenameFarmFromSession(data.sess, txt)
        parent:Hide()
      end,
      EditBoxOnEscapePressed = function(self)
        local parent = self:GetParent()
        if parent then parent:Hide() end
      end,
    }
  end

  if not StaticPopupDialogs[POP_RUN_NOTE] then
    StaticPopupDialogs[POP_RUN_NOTE] = {
      text = "Session note",
      button1 = "Save",
      button2 = "Cancel",
      hasEditBox = true,
      editBoxWidth = 280,
      maxLetters = 160,
      timeout = 0,
      whileDead = true,
      hideOnEscape = true,
      preferredIndex = 3,
      OnShow = function(self, data)
        if self.text then self.text:SetText("Session note") end
        WF_StyleStaticPopupFrame(self)

        -- Some UI stacks pass nil `data` into OnAccept.
        -- Force `self.data` and keep a "last seen" text so the Save button always works.
        if data then self.data = data end

        local eb = self.editBox or self.wideEditBox
        local v = (data and data.initial) or (self.data and self.data.initial) or ""
        self._wfNoteLastText = tostring(v or "")

        if eb then
          eb:SetText(self._wfNoteLastText)
          eb:HighlightText()
          eb:SetFocus()
        end
      end,
      EditBoxOnTextChanged = function(self)
        local parent = self:GetParent()
        if not parent then return end
        parent._wfNoteLastText = tostring(self:GetText() or "")
      end,
      OnAccept = function(self, data)
        local d = data or self.data
        if not d or not d.ui or not d.sess then return end

        local src = nil
        local focus = GetCurrentKeyBoardFocus and GetCurrentKeyBoardFocus() or nil
        if focus and focus.GetParent and focus:GetParent() == self then
          src = focus
        else
          if self.wideEditBox and self.wideEditBox:IsShown() then src = self.wideEditBox end
          if self.editBox and self.editBox:IsShown() then src = self.editBox end
        end

        local v = (src and src:GetText()) or self._wfNoteLastText or ""
        d.ui:SetHistorySessionNote(d.sess, v)
      end,
      EditBoxOnEnterPressed = function(self)
        local parent = self:GetParent()
        local d = parent and parent.data
        if not d or not d.ui or not d.sess then
          if parent then parent:Hide() end
          return
        end

        local v = tostring(self:GetText() or "")
        if v == "" then v = parent._wfNoteLastText or "" end
        d.ui:SetHistorySessionNote(d.sess, v)

        if parent then parent:Hide() end
      end,
      EditBoxOnEscapePressed = function(self)
        local parent = self:GetParent()
        if parent then parent:Hide() end
      end,
    }
  end
end

function UI:GetSessionDisplayName(sess)
  if not sess then return nil end

  local n = _WFTrim(sess._wfCustomFarmName)
  if n ~= "" then return n end

  local farmId = sess.farmId
  if farmId and WF and WF.DB then
    local farm = nil
    if WF.DB.GetFarm then
      farm = WF.DB:GetFarm(farmId)
    elseif WF.DB.Get then
      local db = WF.DB:Get() or {}
      farm = db.farms and db.farms[farmId]
    end
    if farm then
      local fn = _WFTrim(farm.name)
      if fn ~= "" then return fn end
      local fz = _WFTrim(farm.zone)
      if fz ~= "" then return fz end
    end
  end

  local sname = _WFTrim(sess.farmName)
  if sname ~= "" then return sname end

  local zone = _WFTrim(sess.zone)
  if zone ~= "" then return zone end

  return "Farm"
end


-- -------------------------------------------------------
-- Session mode filter (Profile-based)
-- -------------------------------------------------------
function UI:GetModeFilter()
  local f = self._modeFilter
  if type(f) ~= "string" or f == "" then return nil end
  return f
end

function UI:SetModeFilter(name)
  if type(name) ~= "string" or name == "" then
    self._modeFilter = nil
  else
    self._modeFilter = name
  end
  if self.RefreshOverview then self:RefreshOverview() end
  if self.RefreshHistory then self:RefreshHistory() end
end

function UI:OpenModeFilterMenu()
  local names = {}
  if WF.DB and WF.DB.GetProfileNames then
    names = WF.DB:GetProfileNames()
  end

  local cur = self:GetModeFilter()
  local items = {}

  items[#items + 1] = {
    text = "All",
    checked = (cur == nil),
    fn = function() UI:SetModeFilter(nil) end,
  }

  if names and #names > 0 then
    items[#items + 1] = { sep = true }
    for i = 1, #names do
      local n = tostring(names[i] or "")
      if n ~= "" then
        items[#items + 1] = {
          text = DisplayProfileName(n),
          checked = (cur == n),
          fn = function() UI:SetModeFilter(n) end,
        }
      end
    end
  end

  self:OpenContextMenu(items, 240)
end


function UI:PromptRenameFarm(sess)
  if not sess then return end

  local initial = tostring(self:GetSessionDisplayName(sess) or "")
  _WFRenameDbg("PromptRenameFarm. initial='" .. tostring(initial) .. "' " .. _WFRenameSessBrief(sess))

  if Widgets and Widgets.OpenTextPrompt then
    Widgets:OpenTextPrompt(self, "_wfRenameFarmPrompt", {
      title = "Rename farm",
      body = "Set a display name for this farm.",
      defaultText = initial,
      okText = "Save",
      maxLetters = 64,
      onOK = function(txt)
        _WFRenameDbg("Prompt widget commit. text='" .. tostring(txt) .. "'")
        self:RenameFarmFromSession(sess, txt)
      end,
    })
    return
  end

  self:_EnsureHistoryPopups()
  if not StaticPopup_Show then return end

  local data = { ui = self, sess = sess, initial = initial }
  local frame = StaticPopup_Show(POP_RENAME_FARM, nil, nil, data)
  _WFRenameDbg("StaticPopup_Show(POP_RENAME_FARM) returned frame=" .. tostring(frame) .. " which=" .. tostring(frame and frame.which))
  if frame then
    frame.data = data
    WF_StyleStaticPopupFrame(frame)
  end
end

-- -------------------------------------------------------
-- Session notes (used by Overview + History)
-- -------------------------------------------------------
function UI:PromptHistorySessionNote(sess)
  if not sess then return end

  local initial = tostring(sess.note or "")
  if Widgets and Widgets.OpenTextPrompt then
    Widgets:OpenTextPrompt(self, "_wfHistoryNotePrompt", {
      title = "Session note",
      body = "Add a short note for this run.",
      defaultText = initial,
      okText = "Save",
      maxLetters = 160,
      onOK = function(txt)
        self:SetHistorySessionNote(sess, txt)
      end,
    })
    return
  end

  self:_EnsureHistoryPopups()
  if not StaticPopup_Show then return end

  local data = {
    ui = self,
    sess = sess,
    initial = initial,
  }

  local frame = StaticPopup_Show(POP_RUN_NOTE, nil, nil, data)
  if frame then
    frame.data = data
    WF_StyleStaticPopupFrame(frame)
  end
end

function UI:SetHistorySessionNote(sess, note)
  if not sess then return end
  local v = _WFTrim(note)
  if v == "" then
    sess.note = nil
  else
    sess.note = v
  end

  if self.RefreshOverview then self:RefreshOverview() end
  if self.RefreshHistory then self:RefreshHistory() end
end


-- Back-compat: History page used to call this.
function UI:PromptRenameHistorySession(sess)
  self:PromptRenameFarm(sess)
end

function UI:RenameFarmFromSession(sess, newName)
  if not sess then return end

  _WFRenameDbg("RenameFarmFromSession entry. newName='" .. tostring(newName) .. "' " .. _WFRenameSessBrief(sess))

  local v = _WFTrim(newName)
  _WFRenameDbg("RenameFarmFromSession after trim. v='" .. tostring(v) .. "' len=" .. tostring(string.len(v)))
  if v == "" then
    _WFRenameDbg("RenameFarmFromSession aborted: empty name after trim")
    return
  end

  -- -------------------------------------------------------
  -- IMPORTANT: Session-scoped rename (do NOT rename the saved farm record)
  -- This prevents multiple history rows that share farmId (same zone/coords)
  -- from all changing when you rename just one run.
  -- -------------------------------------------------------
  if sess._wfOrigFarmName == nil then
    local orig = _WFTrim(sess._wfCustomFarmName or sess.farmName or "")
    if orig == "" then orig = _WFTrim(sess.zone or "") end
    if orig ~= "" then
      sess._wfOrigFarmName = orig
    end
  end

  sess._wfCustomFarmName = v
  sess.farmName = v

  if _WFSyncSessionFarmLinkByName then
    _WFSyncSessionFarmLinkByName(self, sess)
  end

  _WFRenameDbg("RenameFarmFromSession done. (session-scoped) farmId=" .. tostring(sess.farmId)
    .. " farmName='" .. tostring(sess.farmName) .. "' custom='" .. tostring(sess._wfCustomFarmName) .. "'")
end

function UI:ResetFarmNameFromSession(sess)
  if not sess then return end

  -- Session-scoped reset: drop the custom name and restore the original name for THIS run only.
  local restore = _WFTrim(sess._wfOrigFarmName or "")
  if restore == "" then
    -- Fallbacks if we never captured an original
    restore = _WFTrim(sess.zone or "")
    if restore == "" and WF and WF.DB and WF.DB.GetFarm and sess.farmId then
      local farm = WF.DB:GetFarm(sess.farmId)
      if farm then
        restore = _WFTrim(farm.name or farm.zone or "")
      end
    end
  end

  sess._wfCustomFarmName = nil
  sess._wfOrigFarmName = nil
  if restore ~= "" then
    sess.farmName = restore
  end

  if _WFSyncSessionFarmLinkByName then
    _WFSyncSessionFarmLinkByName(self, sess)
  end

end

function UI:ResetHistorySessionName(sess)
  if not sess then return end

  local orig = _WFTrim(sess._wfOrigFarmName)
  if orig ~= "" then
    sess.farmName = orig
  end

  sess._wfOrigFarmName = nil
  sess._wfCustomFarmName = nil

  if _WFSyncSessionFarmLinkByName then
    _WFSyncSessionFarmLinkByName(self, sess)
  end

end

function UI:ToggleHistorySessionPinned(sess)
  if not sess then return end
  if sess.pinned then
    sess.pinned = nil
  else
    sess.pinned = true
  end

  if self.RefreshOverview then self:RefreshOverview() end
end

local function SessionGPH(sess)
  if not sess then return 0 end
  local e = tonumber(sess.elapsed) or 0
  if e <= 0 then return 0 end
  return (tonumber(sess.totalValue) or 0) / e * 3600
end

local function SessionLIV(sess)
  if not sess then return 0 end
  local e = tonumber(sess.elapsed) or 0
  if e <= 0 then return 0 end

  -- Prefer explicit item-value fields if your session stores them.
  local v =
      tonumber(sess.itemsValue)
      or tonumber(sess.itemValue)
      or tonumber(sess.lootedItemValue)
      or tonumber(sess.lootItemValue)
      or tonumber(sess.itemsTotalValue)
      or tonumber(sess.itemTotalValue)
      or tonumber(sess.itemsValueTotal)
      or tonumber(sess.itemValueTotal)

  -- If we only have total + gold, infer item-value.
  if v == nil then
    local total = tonumber(sess.totalValue)
    local gold  =
        tonumber(sess.moneyGained)
        or tonumber(sess.lootedGoldValue)
        or tonumber(sess.goldValue)
        or tonumber(sess.rawGoldValue)
        or tonumber(sess.goldLooted)
        or tonumber(sess.lootedGold)

    if total ~= nil and gold ~= nil then
      v = total - gold
    end
  end

  -- Fallback: if we can't differentiate, treat total as item-value.
  if v == nil then v = tonumber(sess.totalValue) or 0 end
  if v < 0 then v = 0 end
  return v / e * 3600
end


RebuildHistoryAggregates = function(db)
  if not db then return end
  db.history = db.history or {}
  db.settings = db.settings or {}
  local h = db.history
  h.sessions = h.sessions or {}

  -- All-modes totals (some pages still use this)
  h.totalsByFarm = {}
  -- Mode-aware totals (used by Farms averages so profiles don't mix)
  h.totalsByFarmMode = {}

  h.lastRunByFarm = {}

  local minAvgSeconds = tonumber(db.settings.historyAvgMinSeconds) or 0
  if minAvgSeconds < 0 then minAvgSeconds = 0 end

  local function GetSessionGold(sess)
    if not sess then return 0 end
    return
        tonumber(sess.money)
        or tonumber(sess.moneyGained)
        or tonumber(sess.lootedGoldValue)
        or tonumber(sess.goldValue)
        or tonumber(sess.rawGoldValue)
        or tonumber(sess.goldLooted)
        or tonumber(sess.lootedGold)
        or 0
  end

  local function GetSessionMode(sess)
    if not sess then return "Default" end
    return tostring(sess.mode or sess.profile or sess.farmMode or "Default")
  end

  local function EnsureTotals(tbl, key)
    local t = tbl[key]
    if not t then
      t = { time = 0, value = 0, gold = 0, kills = 0, sessions = 0 }
      tbl[key] = t
    end
    return t
  end

  for i = 1, #h.sessions do
    local sess = h.sessions[i]
    if sess then
      local farmId = sess.farmId
      if farmId ~= nil then
        local elapsed = tonumber(sess.elapsedSec or sess.elapsed or sess.durationSec or sess.duration) or 0
        local value = tonumber(sess.totalValue or sess.valueCopper or sess.value) or 0
        local gold = GetSessionGold(sess)
        local kills = tonumber(sess.kills) or 0

        local startedAt = tonumber(sess.startedAt) or 0
        if startedAt > 0 then
          local prev = h.lastRunByFarm[farmId] or 0
          if startedAt > prev then
            h.lastRunByFarm[farmId] = startedAt
          end
        end

        if elapsed >= minAvgSeconds then
          -- all-modes
          local tAll = EnsureTotals(h.totalsByFarm, farmId)
          tAll.time = (tAll.time or 0) + elapsed
          tAll.value = (tAll.value or 0) + value
          tAll.gold = (tAll.gold or 0) + gold
          tAll.kills = (tAll.kills or 0) + kills
          tAll.sessions = (tAll.sessions or 0) + 1

          -- per-mode
          local mode = GetSessionMode(sess)
          local byMode = h.totalsByFarmMode[farmId]
          if not byMode then
            byMode = {}
            h.totalsByFarmMode[farmId] = byMode
          end
          local tMode = EnsureTotals(byMode, mode)
          tMode.time = (tMode.time or 0) + elapsed
          tMode.value = (tMode.value or 0) + value
          tMode.gold = (tMode.gold or 0) + gold
          tMode.kills = (tMode.kills or 0) + kills
          tMode.sessions = (tMode.sessions or 0) + 1
        end
      end
    end
  end
end

_WFSyncSessionFarmLinkByName = function(self, sess)
  if not sess or not WF or not WF.DB then return false end

  local db = nil
  if WF.DB.Get then
    db = WF.DB:Get() or {}
  end

  local prevFarmId = tonumber(sess.farmId) or sess.farmId
  local prevFarmName = ""

  if prevFarmId and WF.DB.GetFarm then
    local prevFarm = WF.DB:GetFarm(prevFarmId)
    if prevFarm then
      prevFarmName = _WFNormLooseName(prevFarm.name or prevFarm.zone or "")
    end
  end

  local currentName = _WFNormLooseName(sess._wfCustomFarmName or sess.farmName or "")
  local keepPreviousLink = false

  if prevFarmId and prevFarmName ~= "" and currentName ~= "" and prevFarmName == currentName then
    keepPreviousLink = true
  end

  if prevFarmId and not keepPreviousLink and WF.DB.RemoveSessionFromFarmStats then
    WF.DB:RemoveSessionFromFarmStats(sess)
    sess.farmId = nil
  end

  local linked = false
  if WF.DB.TryLinkSessionToFarmByName then
    linked = WF.DB:TryLinkSessionToFarmByName(sess) and true or false
  end

  if db and RebuildHistoryAggregates then
    RebuildHistoryAggregates(db)
  end

  if self and self.MarkFarmAveragesDirty then self:MarkFarmAveragesDirty() end
  if self and self.RefreshOverview then self:RefreshOverview() end
  if self and self.RefreshHistory then self:RefreshHistory() end
  if self and self.RefreshFarms then self:RefreshFarms() end

  return linked
end

local function _SessionsMatch(a, b)
  if a == b then return true end
  if not a or not b then return false end
  -- fallback: match by stable-ish fields
  if a.startedAt and b.startedAt and a.startedAt == b.startedAt then return true end
  if a.farmId and b.farmId and a.farmId == b.farmId and a.elapsed and b.elapsed and a.elapsed == b.elapsed then
    return true
  end
  return false
end

function UI:RemoveHistorySession(sess)
  if not sess or not WF.DB or not WF.DB.Get then return end
  local db = WF.DB:Get() or {}
  db.history = db.history or {}
  local h = db.history
  h.sessions = h.sessions or {}

  local removed = false
  for i = #h.sessions, 1, -1 do
    if _SessionsMatch(h.sessions[i], sess) then
      table.remove(h.sessions, i)
      removed = true
      break
    end
  end

  if removed then
    RebuildHistoryAggregates(db)
    if self.RefreshOverview then self:RefreshOverview() end
    if self.RefreshHistory then self:RefreshHistory() end
  end
end

local function FormatTime(ts)
  if not ts then return "—" end
  return date("%m/%d %H:%M", ts)
end

local function SafeNow()
  if GetServerTime then
    local ok, t = pcall(GetServerTime)
    if ok and type(t) == "number" then return t end
  end
  return time()
end


local function EnsureSettingsDefaults()
  if not WF.DB or not WF.DB.GetSetting then return end

  if WF.DB:GetSetting("randomMode") == nil then WF.DB:SetSetting("randomMode", "filtered") end
  if WF.DB:GetSetting("randomExpansion") == nil then WF.DB:SetSetting("randomExpansion", "Any") end
  if WF.DB:GetSetting("randomFarmType") == nil then WF.DB:SetSetting("randomFarmType", "Any") end
  if WF.DB:GetSetting("randomAim") == nil then WF.DB:SetSetting("randomAim", "Any") end
  if WF.DB:GetSetting("randomCurrentZoneOnly") == nil then WF.DB:SetSetting("randomCurrentZoneOnly", false) end
  if WF.DB:GetSetting("randomIncludeUnknown") == nil then WF.DB:SetSetting("randomIncludeUnknown", false) end
  if WF.DB:GetSetting("randomFarmToastStyle") == nil then WF.DB:SetSetting("randomFarmToastStyle", "Banner") end
  if WF.DB:GetSetting("randomFarmToastSound") == nil then WF.DB:SetSetting("randomFarmToastSound", "None") end

  -- farms page filters
  if WF.DB:GetSetting("farmFilterExpansion") == nil then WF.DB:SetSetting("farmFilterExpansion", "Any") end
  if WF.DB:GetSetting("farmFilterType") == nil then WF.DB:SetSetting("farmFilterType", "Any") end
  if WF.DB:GetSetting("farmFilterCurrentZoneOnly") == nil then WF.DB:SetSetting("farmFilterCurrentZoneOnly", false) end

  if WF.DB:GetSetting("farmGroup") == nil then WF.DB:SetSetting("farmGroup", "None") end
  if WF.DB:GetSetting("farmSort") == nil then WF.DB:SetSetting("farmSort", "Name (A-Z)") end

  if WF.DB:GetSetting("allowInstanceFarms") == nil then WF.DB:SetSetting("allowInstanceFarms", true) end
  -- Color rows by quality is always enabled (option removed).
  if WF.DB:GetSetting("colorByQuality") ~= true then WF.DB:SetSetting("colorByQuality", true) end

  -- Deprecated toggle (removed from UI): always off.
  if WF.DB:GetSetting("showColumnDividers") ~= false then WF.DB:SetSetting("showColumnDividers", false) end
  -- Tooltip "Dropped by" is user-facing again (default on).
  if WF.DB:GetSetting("tooltipShowSource") == nil then WF.DB:SetSetting("tooltipShowSource", true) end
  if WF.DB:GetSetting("priceSource") == nil then WF.DB:SetSetting("priceSource", "vendor") end
  if WF.DB:GetSetting("tsmPriceString") == nil or WF.DB:GetSetting("tsmPriceString") == "vendorsell" then WF.DB:SetSetting("tsmPriceString", "dbregionmarketavg") end

  -- window / misc
  if WF.DB:GetSetting("uiLocked") == nil then WF.DB:SetSetting("uiLocked", false) end
  if WF.DB:GetSetting("uiScale") == nil then WF.DB:SetSetting("uiScale", 1.0) end
  if WF.DB:GetSetting("showMinimapButton") == nil then WF.DB:SetSetting("showMinimapButton", true) end


  if WF.DB:GetSetting("autoStartSessionOnOpen") == nil then WF.DB:SetSetting("autoStartSessionOnOpen", false) end
  if WF.DB:GetSetting("autoResumeOnLootWhilePaused") == nil then WF.DB:SetSetting("autoResumeOnLootWhilePaused", false) end
  if WF.DB:GetSetting("mainWindowLayout") == nil then WF.DB:SetSetting("mainWindowLayout", "Normal") end
  if WF.DB:GetSetting("mainWindowPill1") == nil then WF.DB:SetSetting("mainWindowPill1", "Time") end
  if WF.DB:GetSetting("mainWindowPill2") == nil then WF.DB:SetSetting("mainWindowPill2", "Looted Gold") end
  if WF.DB:GetSetting("mainWindowPill3") == nil then WF.DB:SetSetting("mainWindowPill3", "Looted Items") end
  if WF.DB:GetSetting("mainWindowPill4") == nil then WF.DB:SetSetting("mainWindowPill4", "Hidden") end
  if WF.DB:GetSetting("mainWindowRaceTimerSeconds") == nil then WF.DB:SetSetting("mainWindowRaceTimerSeconds", 0) end
  for slot = 1, 4 do
    if WF.DB:GetSetting("mainWindowPill" .. tostring(slot)) == "Kills" then
      WF.DB:SetSetting("mainWindowPill" .. tostring(slot), "Hidden")
    end
  end
  if WF.DB:GetSetting("compareTarget") == nil then WF.DB:SetSetting("compareTarget", "Last Run") end
  if WF.DB:GetSetting("historyKeep") == nil then WF.DB:SetSetting("historyKeep", 100) end
  if WF.DB:GetSetting("historyMinSeconds") == nil then WF.DB:SetSetting("historyMinSeconds", 60) end
  if WF.DB:GetSetting("historyMinGPH") == nil then WF.DB:SetSetting("historyMinGPH", 0) end
  if WF.DB:GetSetting("historyAvgMinSeconds") == nil then WF.DB:SetSetting("historyAvgMinSeconds", 0) end
  if WF.DB:GetSetting("confirmDestructive") == nil then WF.DB:SetSetting("confirmDestructive", true) end
  
    -- loot capture filters
  if WF.DB:GetSetting("lootMinQty") == nil then WF.DB:SetSetting("lootMinQty", 1) end
  if WF.DB:GetSetting("lootMinGold") == nil then WF.DB:SetSetting("lootMinGold", 0) end
  if WF.DB:GetSetting("lootMinQuality") == nil then WF.DB:SetSetting("lootMinQuality", 0) end
  -- normalize legacy string quality values (e.g. "Uncommon+") into numeric thresholds
  local _q = WF.DB:GetSetting("lootMinQuality")
  if type(_q) == "string" then
    WF.DB:SetSetting("lootMinQuality", QUALITY_VALUE[_q] or tonumber(_q) or 0)
  end
  if WF.DB:GetSetting("lootIncludeMaterials") == nil then WF.DB:SetSetting("lootIncludeMaterials", true) end
  if WF.DB:GetSetting("lootOnlyMaterials") == nil then WF.DB:SetSetting("lootOnlyMaterials", false) end
  if WF.DB:GetSetting("lootOnlyGear") == nil then WF.DB:SetSetting("lootOnlyGear", false) end
  if WF.DB:GetSetting("lootIgnoreSoulbound") == nil then WF.DB:SetSetting("lootIgnoreSoulbound", true) end
  -- notifications (valuable loot pings)
  if WF.DB:GetSetting("notifyLootEnabled") == nil then WF.DB:SetSetting("notifyLootEnabled", true) end
  if WF.DB:GetSetting("notifyLootMinGold") == nil then WF.DB:SetSetting("notifyLootMinGold", 10000) end -- 1g
  if WF.DB:GetSetting("notifyLootChat") == nil then WF.DB:SetSetting("notifyLootChat", true) end
  if WF.DB:GetSetting("notifyLootShowValue") == nil then WF.DB:SetSetting("notifyLootShowValue", true) end
  if WF.DB:GetSetting("notifyLootRaidWarning") == nil then WF.DB:SetSetting("notifyLootRaidWarning", false) end
  if WF.DB:GetSetting("notifyLootSound") == nil then WF.DB:SetSetting("notifyLootSound", "coins") end

  

  -- notepad
  if WF.DB:GetSetting("notepadText") == nil then WF.DB:SetSetting("notepadText", "") end
  if WF.DB:GetSetting("notepadData") == nil then
    WF.DB:SetSetting("notepadData", { v = 2, activeId = nil, notes = {} })
  end

  -- Bag Worth options
  if WF.DB:GetSetting("bwIncludeToys") == nil then WF.DB:SetSetting("bwIncludeToys", true) end
  if WF.DB:GetSetting("bwIncludeMats") == nil then WF.DB:SetSetting("bwIncludeMats", true) end
  if WF.DB:GetSetting("bwIncludeGear") == nil then WF.DB:SetSetting("bwIncludeGear", true) end
  if WF.DB:GetSetting("bwIncludePatterns") == nil then WF.DB:SetSetting("bwIncludePatterns", true) end
  if WF.DB:GetSetting("bwIncludePets") == nil then WF.DB:SetSetting("bwIncludePets", true) end
  if WF.DB:GetSetting("bwIncludeConsumables") == nil then WF.DB:SetSetting("bwIncludeConsumables", true) end
  if WF.DB:GetSetting("bwIncludeGems") == nil then WF.DB:SetSetting("bwIncludeGems", true) end
  if WF.DB:GetSetting("bwIncludeGreys") == nil then WF.DB:SetSetting("bwIncludeGreys", false) end
  if WF.DB:GetSetting("bwIncludeWhites") == nil then WF.DB:SetSetting("bwIncludeWhites", false) end
  if WF.DB:GetSetting("bwIncludeGreens") == nil then WF.DB:SetSetting("bwIncludeGreens", true) end
  if WF.DB:GetSetting("bwIncludeBlues") == nil then WF.DB:SetSetting("bwIncludeBlues", true) end
  if WF.DB:GetSetting("bwIncludeEpics") == nil then WF.DB:SetSetting("bwIncludeEpics", true) end
  if WF.DB:GetSetting("bwRememberWorth") == nil then WF.DB:SetSetting("bwRememberWorth", true) end
end


-- -------------------------------------------------------
-- Loot Notifications (value pings)
-- -------------------------------------------------------
do
  WF.Notify = WF.Notify or {}
  local N = WF.Notify

  local f
  local lastByItem = {}

  local function Now()
    if GetTimePreciseSec then return GetTimePreciseSec() end
    return GetTime()
  end

  local function SBool(k, def)
    if not WF or not WF.DB or not WF.DB.GetSetting then return def end
    local v = WF.DB:GetSetting(k)
    if v == nil then return def end
    return v and true or false
  end

  local function SNum(k, def)
    if not WF or not WF.DB or not WF.DB.GetSetting then return def end
    local v = tonumber(WF.DB:GetSetting(k))
    if v == nil then return def end
    return v
  end

  local function SStr(k, def)
    if not WF or not WF.DB or not WF.DB.GetSetting then return def end
    local v = WF.DB:GetSetting(k)
    if v == nil then return def end
    return tostring(v)
  end

  local function Chat(msg)
    if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
      DEFAULT_CHAT_FRAME:AddMessage(msg)
    else
      print(msg)
    end
  end

  local raidWarnLootHoverFrame

  local function HideRaidWarnLootTooltip(frame)
    if GameTooltip and frame and frame._wfLootTooltipShown then
      GameTooltip:Hide()
    end
    if frame then
      frame._wfLootTooltipShown = nil
    end
  end

  local function GetRaidWarnLootHoverFrame()
    if raidWarnLootHoverFrame then return raidWarnLootHoverFrame end

    local frame = CreateFrame("Frame", nil, UIParent)
    frame:SetFrameStrata("TOOLTIP")
    frame:SetSize(720, 96)
    frame:SetPoint("CENTER", UIParent, "CENTER", 0, 120)
    frame:Hide()

    if frame.SetMouseMotionEnabled then
      frame:SetMouseMotionEnabled(true)
    end
    if frame.SetMouseClickEnabled then
      frame:SetMouseClickEnabled(false)
    end
    if not frame.SetMouseMotionEnabled and frame.EnableMouse then
      frame:EnableMouse(true)
    end

    frame:SetScript("OnEnter", function(self)
      local link = self._wfLootItemLink
      if not link or link == "" or not GameTooltip then return end

      GameTooltip:SetOwner(self, "ANCHOR_CURSOR_RIGHT")
      GameTooltip:SetHyperlink(link)
      GameTooltip:Show()
      self._wfLootTooltipShown = true
    end)

    frame:SetScript("OnLeave", function(self)
      HideRaidWarnLootTooltip(self)
    end)

    raidWarnLootHoverFrame = frame
    return raidWarnLootHoverFrame
  end

  local function ShowRaidWarnLootHover(link, holdSeconds)
    if link == nil then return end

    local ok, safeLink = pcall(function()
      return tostring(link)
    end)
    if not ok or type(safeLink) ~= "string" or safeLink == "" then return end

    local frame = GetRaidWarnLootHoverFrame()
    if not frame then return end

    frame._wfLootItemLink = safeLink
    frame._wfLootHoverSerial = (frame._wfLootHoverSerial or 0) + 1
    local serial = frame._wfLootHoverSerial

    frame:ClearAllPoints()
    if RaidWarningFrame then
      frame:SetPoint("CENTER", RaidWarningFrame, "CENTER", 0, 0)
    else
      frame:SetPoint("CENTER", UIParent, "CENTER", 0, 120)
    end
    frame:SetSize(720, 96)
    frame:Show()

    if C_Timer and C_Timer.After then
      C_Timer.After((tonumber(holdSeconds) or 1.6) + 0.55, function()
        if not frame or frame._wfLootHoverSerial ~= serial then return end
        HideRaidWarnLootTooltip(frame)
        frame._wfLootItemLink = nil
        frame:Hide()
      end)
    end
  end

  local function RaidWarn(msg, itemLink)
    if not msg or msg == "" then return end

    local base = { r = 1.00, g = 1.00, b = 1.00 }
    local hold = 1.6
    local fade = 0.35

    if RaidNotice_AddMessage and RaidWarningFrame then
      if RaidWarningFrame.SetTimeVisible then RaidWarningFrame:SetTimeVisible(hold) end
      if RaidWarningFrame.SetFadeOutTime then RaidWarningFrame:SetFadeOutTime(fade) end
      RaidNotice_AddMessage(RaidWarningFrame, msg, base, hold)
      ShowRaidWarnLootHover(itemLink, hold)
      return
    end

    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage(msg, base.r, base.g, base.b, 1.0, hold)
      ShowRaidWarnLootHover(itemLink, hold)
      return
    end

    Chat(msg)
  end

  local function WF_PlainString(v)
    if v == nil then return nil end
    local ok, s = pcall(function()
      return tostring(v)
    end)
    if not ok or type(s) ~= "string" then
      return nil
    end
    return s
  end

  local function BuildRaidWarnItemText(link)
    local rawLink = WF_PlainString(link) or ""
    local itemName, _, quality = GetItemInfo(rawLink)

    local plainName = itemName
    if not plainName or plainName == "" then
      plainName = string.match(rawLink, "|h%[(.-)%]|h") or string.match(rawLink, "%[(.-)%]") or rawLink
    end

    if string.match(plainName, "^%[.*%]$") == nil then
      plainName = "[" .. plainName .. "]"
    end

    local hex
    if quality ~= nil and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] and ITEM_QUALITY_COLORS[quality].hex then
      hex = tostring(ITEM_QUALITY_COLORS[quality].hex)
      hex = string.gsub(hex, "^|c", "")
    end
    if not hex or hex == "" then
      hex = string.match(rawLink, "^|c(%x%x%x%x%x%x%x%x)") or "ffffffff"
    end

    return "|c" .. hex .. plainName .. "|r"
  end

  local function BuildRaidWarnLootMessage(link, count, totalCopper)
    local msg = "|cff33aaffWhatFarm:|r " .. BuildRaidWarnItemText(link)
    if count and count > 1 then
      msg = msg .. " |cffb8b8b8x" .. tostring(count) .. "|r"
    end
    if SBool("notifyLootShowValue", true) then
      msg = msg .. " |cffaaaaaa(" .. Money(totalCopper or 0) .. ")|r"
    end
    return msg
  end

  local SOUND_PRESETS = {
    none  = {},
    coins = { "LOOT_WINDOW_COIN_SOUND", "LOOT_WINDOW_OPEN", "IG_MAINMENU_OPTION_CHECKBOX_ON" },
    loot  = { "LOOT_WINDOW_OPEN" },
    epic  = { "UI_EPICLOOT_TOAST", "UI_LEGENDARY_LOOT_TOAST" },
    alert = { "RAID_WARNING", "READY_CHECK", "PVP_THROUGH_QUEUE" },
  }

  local function PlayPreset(key)
    key = tostring(key or "none"):lower()
    if key == "none" then return end
    if not PlaySound then return end

    local list = SOUND_PRESETS[key]
    if type(list) ~= "table" then return end

    for i = 1, #list do
      local nm = list[i]
      local kit = SOUNDKIT and SOUNDKIT[nm]
      if kit then
        pcall(PlaySound, kit, "Master")
        return
      end
    end
  end

  local function TryGetValueCopper(itemLink, itemID)
    -- Prefer addon price pipeline if available.
    if WF and WF.Price then
      local fn = WF.Price.GetValue or WF.Price.GetItemValue or WF.Price.Get or WF.Price.Value
      if type(fn) == "function" then
        local ok, v = pcall(fn, WF.Price, itemID or itemLink)
        if ok and tonumber(v) then return tonumber(v) end
      end
    end
    if WF and WF.Tracker then
      local fn = WF.Tracker.GetItemValue or WF.Tracker.GetValue
      if type(fn) == "function" then
        local ok, v = pcall(fn, WF.Tracker, itemID or itemLink)
        if ok and tonumber(v) then return tonumber(v) end
      end
    end

    -- Vendor sell price fallback.
    local _, _, _, _, _, _, _, _, _, _, sellPrice = GetItemInfo(itemLink or itemID or 0)
    return tonumber(sellPrice) or 0
  end

  local function ExtractItemLink(msg)
    msg = WF_PlainString(msg)
    if not msg or msg == "" then return nil end

    local link = string.match(msg, "(|c%x+|Hitem:%d+:[^|]+|h%[[^%]]+%]|h|r)")
    if link then return link end

    return string.match(msg, "(|Hitem:%d+:[^|]+|h%[[^%]]+%]|h)")
  end

  local function ExtractCount(msg)
    msg = WF_PlainString(msg)
    if not msg or msg == "" then return 1 end

    local c = string.match(msg, "x(%d+)")
    if c then return tonumber(c) or 1 end
    return 1
  end

  local function ShouldHandleMsg(msg)
    msg = WF_PlainString(msg)
    if not msg or msg == "" then return false end

    -- Try to avoid notifying for other players' loot spam.
    -- (LOOT_ITEM_SELF strings are localized, so we keep it simple.)
    return string.match(msg, "^You ") ~= nil
  end

  local function Notify(link, count, totalCopper)
    if not SBool("notifyLootEnabled", true) then return end

    local safeLink = WF_PlainString(link)
    if not safeLink or safeLink == "" then return end

    local minC = SNum("notifyLootMinGold", 0) or 0
    if minC < 0 then minC = 0 end
    if (tonumber(totalCopper) or 0) < minC then return end

    local itemID = tonumber(string.match(safeLink, "Hitem:(%d+):")) or 0
    local now = Now()
    local last = lastByItem[itemID] or 0
    if (now - last) < 0.12 then return end -- spam guard
    lastByItem[itemID] = now

    -- Ensure the item link has its quality color prefix.
    -- Tainted loot messages sometimes strip the |c prefix.
    if not safeLink:find("^|c") then
      local _, _, quality = GetItemInfo(safeLink)
      if not quality and itemID > 0 then
        _, _, quality = GetItemInfo(itemID)
      end
      if quality then
        local r, g, b = GetItemQualityColor(quality)
        if r and g and b then
          local hex = string.format("%02x%02x%02x", math.floor(r*255), math.floor(g*255), math.floor(b*255))
          safeLink = "|cff" .. hex .. safeLink .. "|r"
        end
      end
    end

    local plainMsg = BuildRaidWarnLootMessage(safeLink, count, totalCopper)

    if SBool("notifyLootChat", true) then
      local msg = "|cff33aaffWhatFarm|r  " .. safeLink
      if count and count > 1 then msg = msg .. " x" .. tostring(count) end
      if SBool("notifyLootShowValue", true) then
        msg = msg .. "  |cffaaaaaa(" .. Money(totalCopper or 0) .. ")|r"
      end
      Chat(msg)
    end

    if SBool("notifyLootRaidWarning", false) then
      RaidWarn(plainMsg, safeLink)
    end

    PlayPreset(SStr("notifyLootSound", "coins"))
  end

  -- Respect active loot-tracking filters so pings match what you actually record.
  local _bindCacheByID = {}

  local function _GetInstantInfo(linkOrID)
    if GetItemInfoInstant then
      local itemID, _, _, equipLoc, _, classID, subClassID = GetItemInfoInstant(linkOrID)
      return tonumber(itemID) or 0, tostring(equipLoc or ""), tonumber(classID), tonumber(subClassID)
    end
    return 0, "", nil, nil
  end

  local function _IsMaterial(link, itemID, classID)
    if classID == nil then
      local _, _, c = _GetInstantInfo(itemID > 0 and itemID or link)
      classID = c
    end
    if classID == 7 then return true end -- Trade Goods
    if C_Item and C_Item.IsCraftingReagent then
      local ok, v = pcall(C_Item.IsCraftingReagent, link)
      if ok and v then return true end
    end
    return false
  end

  local function _IsGear(link, itemID, classID, equipLoc)
    if equipLoc == nil or equipLoc == "" or classID == nil then
      local _, e, c = _GetInstantInfo(itemID > 0 and itemID or link)
      equipLoc, classID = equipLoc or e, classID or c
    end
    if equipLoc and equipLoc ~= "" then return true end
    if classID == 2 or classID == 4 then return true end -- Weapon / Armor
    if IsEquippableItem then
      local ok, v = pcall(IsEquippableItem, link)
      if ok and v then return true end
    end
    return false
  end

  local function _IsSoulbound(link, itemID)
    itemID = tonumber(itemID) or 0
    local cached = _bindCacheByID[itemID]
    if cached ~= nil then return cached end

    local isBound = false

    -- Tooltip scan (localized-safe via global strings).
    local tip = _G.WF_NotifyBindTip
    if not tip then
      tip = CreateFrame("GameTooltip", "WF_NotifyBindTip", UIParent, "GameTooltipTemplate")
      tip:SetOwner(UIParent, "ANCHOR_NONE")
    end
    tip:ClearLines()
    pcall(tip.SetHyperlink, tip, link)

    local needle1 = ITEM_SOULBOUND
    local needle2 = ITEM_BIND_ON_PICKUP
    local needle3 = ITEM_BIND_TO_ACCOUNT
    local needle4 = ITEM_BIND_TO_BNETACCOUNT

    for i = 1, (tip:NumLines() or 0) do
      local line = _G["WF_NotifyBindTipTextLeft" .. i]
      local t = line and line.GetText and line:GetText()
      if t and (t == needle1 or t == needle2 or t == needle3 or t == needle4) then
        isBound = true
        break
      end
    end

    _bindCacheByID[itemID] = isBound
    return isBound
  end

  local LOOT_CATEGORY_KEYS = {
    "lootRecordMaterials",
    "lootRecordGear",
    "lootRecordRecipes",
    "lootRecordPets",
    "lootRecordToys",
    "lootRecordMounts",
    "lootRecordConsumables",
    "lootRecordMisc",
  }

  local LOOT_NON_MATERIAL_CATEGORY_KEYS = {
    "lootRecordGear",
    "lootRecordRecipes",
    "lootRecordPets",
    "lootRecordToys",
    "lootRecordMounts",
    "lootRecordConsumables",
    "lootRecordMisc",
  }

  local function _GetLootCategorySelection()
    local out = {}
    local hasData = false

    for i = 1, #LOOT_CATEGORY_KEYS do
      local key = LOOT_CATEGORY_KEYS[i]
      local v = WF.DB:GetSetting(key)
      if v ~= nil then
        out[key] = v and true or false
        hasData = true
      else
        out[key] = false
      end
    end

    if hasData then
      return out
    end

    local includeMats = WF.DB:GetSetting("lootIncludeMaterials")
    if includeMats == nil then includeMats = true end
    includeMats = includeMats and true or false

    local onlyMats = WF.DB:GetSetting("lootOnlyMaterials") and true or false
    local onlyGear = WF.DB:GetSetting("lootOnlyGear") and true or false

    if onlyMats and onlyGear then
      out.lootRecordMaterials = true
      out.lootRecordGear = true
      return out
    end

    if onlyMats then
      out.lootRecordMaterials = true
      return out
    end

    if onlyGear then
      out.lootRecordGear = true
      return out
    end

    if includeMats == false then
      for i = 1, #LOOT_NON_MATERIAL_CATEGORY_KEYS do
        out[LOOT_NON_MATERIAL_CATEGORY_KEYS[i]] = true
      end
    end

    return out
  end

  local function _HasLootCategorySelection(categories)
    for i = 1, #LOOT_CATEGORY_KEYS do
      if categories[LOOT_CATEGORY_KEYS[i]] then
        return true
      end
    end
    return false
  end

  local function _SafeCallSimple(fn, ...)
    if type(fn) ~= "function" then return nil end
    local ok, a = pcall(fn, ...)
    if ok then return a end
    return nil
  end

  local function _IsToyItem(itemID)
    if not itemID then return false end
    if C_ToyBox and C_ToyBox.GetToyInfo then
      local toyName = _SafeCallSimple(C_ToyBox.GetToyInfo, itemID)
      if toyName then return true end
    end
    if PlayerHasToy then
      return _SafeCallSimple(PlayerHasToy, itemID) and true or false
    end
    return false
  end

  local function _IsMountItem(itemID)
    if not itemID then return false end
    if C_MountJournal and C_MountJournal.GetMountFromItem then
      local mountID = _SafeCallSimple(C_MountJournal.GetMountFromItem, itemID)
      if mountID and mountID ~= 0 then return true end
    end
    return false
  end

  local function _GetRecordCategory(link, itemID)
    local _, equipLoc, classID = _GetInstantInfo(itemID > 0 and itemID or link)
    local isMat = _IsMaterial(link, itemID, classID)
    local isGear = _IsGear(link, itemID, classID, equipLoc)

    if _IsToyItem(itemID) then return "lootRecordToys" end
    if _IsMountItem(itemID) then return "lootRecordMounts" end
    if classID == 17 then return "lootRecordPets" end
    if classID == 9 then return "lootRecordRecipes" end
    if isMat then return "lootRecordMaterials" end
    if isGear then return "lootRecordGear" end
    if classID == 0 then return "lootRecordConsumables" end
    return "lootRecordMisc"
  end

  local function _PassesLootTrackingFilters(link, count, totalCopper, itemID)
    if not (WF and WF.DB and WF.DB.GetSetting) then return true end

    count = tonumber(count) or 1
    totalCopper = tonumber(totalCopper) or 0
    itemID = tonumber(itemID) or 0

    local minQty = tonumber(WF.DB:GetSetting("lootMinQty")) or 1
    if minQty < 1 then minQty = 1 end
    if count < minQty then return false end

    local minQ = tonumber(WF.DB:GetSetting("lootMinQuality")) or 0
    if minQ < 0 then minQ = 0 end

    local quality = select(3, GetItemInfo(link)) or 0
    if quality < minQ then return false end

    local minV = tonumber(WF.DB:GetSetting("lootMinGold")) or 0
    if minV < 0 then minV = 0 end
    if totalCopper < minV then return false end

    local ignoreSoulbound = WF.DB:GetSetting("lootIgnoreSoulbound")
    if ignoreSoulbound then
      -- Most BOP items show as "Soulbound" in tooltip after pickup.
      if _IsSoulbound(link, itemID) then return false end
    end

    local categories = _GetLootCategorySelection()
    if _HasLootCategorySelection(categories) then
      local categoryKey = _GetRecordCategory(link, itemID)
      if not categories[categoryKey] then
        return false
      end
    end

    return true
  end



  local function Process(link, count)
    local itemID = tonumber(string.match(WF_PlainString(link) or "", "Hitem:(%d+):")) or 0
    local v = TryGetValueCopper(link, itemID)
    local total = (tonumber(v) or 0) * (count or 1)

    -- Match your active loot-tracking filters (mats-only, gear-only, soulbound ignore, min quality/value/qty).
    if not _PassesLootTrackingFilters(link, count or 1, total, itemID) then return end

    Notify(link, count or 1, total)
  end

  local function _IsRunActive()
    local tr = WF and WF.Tracker
    if tr and type(tr.IsActive) == "function" then
      return tr:IsActive() and true or false
    end
    return false
  end

  local function OnLootMsg(msg)
    if not SBool("notifyLootEnabled", true) then return end
    if not _IsRunActive() then return end

    -- CHAT_MSG_LOOT messages are tainted in Midnight; string ops on them can error.
    -- Guard every tainted-string operation with pcall.
    local ok, shouldHandle = pcall(ShouldHandleMsg, msg)
    if not ok or not shouldHandle then return end

    local link
    ok, link = pcall(ExtractItemLink, msg)
    if not ok or not link then return end

    local count = 1
    ok, count = pcall(ExtractCount, msg)
    if not ok then count = 1 end
    count = count or 1

    -- Ensure item info is loaded before we price it (for vendor sell price, etc.)
    if Item and Item.CreateFromItemLink then
      local it = Item:CreateFromItemLink(link)
      it:ContinueOnItemLoad(function()
        if not _IsRunActive() then return end
        Process(link, count)
      end)
      return
    end

    -- Fallback path (no Item API): best-effort immediate pricing.
    Process(link, count)
  end

  function N:Init()
    if f then return end
    f = CreateFrame("Frame", nil, UIParent)
    f:RegisterEvent("CHAT_MSG_LOOT")
    f:SetScript("OnEvent", function(_, _, msg)
      OnLootMsg(msg)
    end)
  end

  function N:Test()
    EnsureSettingsDefaults()
    local sampleLink = "|cffffffff|Hitem:6948:::::::::::::|h[Hearthstone]|h|r"
    local sample = "|cff33aaffWhatFarm|r  " .. sampleLink
    local value = SNum("notifyLootMinGold", 10000) or 0

    if SBool("notifyLootChat", true) then
      local msg = sample
      if SBool("notifyLootShowValue", true) then
        msg = msg .. "  |cffaaaaaa(" .. Money(value) .. ")|r"
      end
      Chat(msg)
    end

    if SBool("notifyLootRaidWarning", false) then
      RaidWarn(BuildRaidWarnLootMessage(sampleLink, 1, value), sampleLink)
    end

    PlayPreset(SStr("notifyLootSound", "coins"))
  end

  -- Initialize once per session.
  if not N._wfInit then
    N._wfInit = true
    local init = CreateFrame("Frame")
    init:RegisterEvent("PLAYER_LOGIN")
    init:SetScript("OnEvent", function()
      EnsureSettingsDefaults()
      if WF and WF.Notify and WF.Notify.Init then
        WF.Notify:Init()
      end
      init:UnregisterAllEvents()
    end)
  end
end


-- -------------------------------------------------------
-- Waypoint
-- -------------------------------------------------------
local function SetWaypoint(mapId, x, y)
  if not mapId or x == nil or y == nil then return false end

  x = tonumber(x)
  y = tonumber(y)
  if not x or not y then return false end

  -- Farms may store coords as 0-1 OR 0-100. Normalize for UiMapPoint.
  if x > 1 then x = x / 100 end
  if y > 1 then y = y / 100 end
  if x < 0 then x = 0 elseif x > 1 then x = 1 end
  if y < 0 then y = 0 elseif y > 1 then y = 1 end

  if C_Map and C_Map.SetUserWaypoint and UiMapPoint and UiMapPoint.CreateFromCoordinates then
    local point = UiMapPoint.CreateFromCoordinates(mapId, x, y)
    C_Map.SetUserWaypoint(point)
    if C_SuperTrack and C_SuperTrack.SetSuperTrackedUserWaypoint then
      C_SuperTrack.SetSuperTrackedUserWaypoint(true)
    end
    return true
  end
  return false
end


local function ApplyUIScale(s)
  s = tonumber(s) or 1.0
  if WF and WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("uiScale", s)
  end

  local scaledFrames = {}

  local function ApplyToFrame(fr)
    if fr and fr.SetScale then
      fr:SetScale(s)
      scaledFrames[#scaledFrames + 1] = fr
    end
  end

  ApplyToFrame(UI and UI.frame)
  ApplyToFrame(UI and UI._ctx)
  ApplyToFrame(UI and UI._confirm)
  ApplyToFrame(UI and UI._renameModal)
  ApplyToFrame(UI and UI._noteModal)
  ApplyToFrame(UI and UI._farmNoteModal)
  ApplyToFrame(UI and UI._compareFrame)

  local share = WF and WF.Share
  if share then
    ApplyToFrame(share._shareHub and share._shareHub.box)
    ApplyToFrame(share._viewer and share._viewer.frame)
    ApplyToFrame(share._exportDialog and share._exportDialog.box)
    ApplyToFrame(share._importDialog and share._importDialog.box)
  end

  ApplyToFrame(UI and UI._wfSettingsProfilesShare and UI._wfSettingsProfilesShare.box)
  ApplyToFrame(UI and UI._wfSettingsProfilesImportPreview and UI._wfSettingsProfilesImportPreview.box)

  -- Best-effort: keep the main tracker window in sync if it's already created.
  local main = WF and WF.UI and WF.UI.Main
  if main then
    local frames = {
      main.frame,
      main.Frame,
      main.mainFrame,
      main.compareFrame,
      main.compFrame,
      main._comp and main._comp.settingsWindow or nil,
    }

    for i = 1, #frames do
      ApplyToFrame(frames[i])
    end

    if main.ApplyScale then
      pcall(main.ApplyScale, main, s)
    end
  end

  if Widgets and Widgets.RefreshPixelPerfect then
    for i = 1, #scaledFrames do
      Widgets:RefreshPixelPerfect(scaledFrames[i])
    end
  end
end

-- -------------------------------------------------------
-- Docking / Non-overlap (Settings vs Main)
-- -------------------------------------------------------
local function GetMainFrameForDock()
  local main = WF and WF.UI and WF.UI.Main
  if not main then return nil end

  if type(main.GetFrame) == "function" then
    local ok, fr = pcall(main.GetFrame, main)
    if ok and fr then return fr end
  end

  return main.frame or main.Frame or main.mainFrame or main.MainFrame or main.window
end

local function FramesOverlap(a, b)
  if not a or not b then return false end
  local al, ar, at, ab = a:GetLeft(), a:GetRight(), a:GetTop(), a:GetBottom()
  local bl, br, bt, bb = b:GetLeft(), b:GetRight(), b:GetTop(), b:GetBottom()
  if not (al and ar and at and ab and bl and br and bt and bb) then return false end
  return (al < br and ar > bl and ab < bt and at > bb)
end

local function FitsOnScreen(f)
  if not f then return false end
  local l, r, t, b = f:GetLeft(), f:GetRight(), f:GetTop(), f:GetBottom()
  if not (l and r and t and b) then return false end
  local w, h = UIParent:GetWidth(), UIParent:GetHeight()
  return l >= 0 and b >= 0 and r <= w and t <= h
end

function UI:EnsureNoOverlap()
  -- NOTE:
  -- This keeps the Help window from spawning on top of the Main window,
  -- but does NOT anchor Help to Main. We place relative to UIParent using
  -- Main's screen coords so the two windows remain independent.
  if not self.frame or not self.frame:IsShown() then return end
  local mainF = GetMainFrameForDock()
  if not mainF or not mainF:IsShown() then return end
  if not FramesOverlap(self.frame, mainF) then return end

  local f = self.frame
  local ml, mr, mt, mb = mainF:GetLeft(), mainF:GetRight(), mainF:GetTop(), mainF:GetBottom()
  if not (ml and mr and mt and mb) then return end

  local fw, fh = f:GetWidth() or 0, f:GetHeight() or 0
  local pad = 16

  local function setTopLeft(x, y)
    f:ClearAllPoints()
    f:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)
  end

  local function placeRight()
    setTopLeft(mr + pad, mt)
  end
  local function placeLeft()
    setTopLeft(ml - pad - fw, mt)
  end
  local function placeAbove()
    -- place f bottom-left above main top-left + pad
    setTopLeft(ml, mt + pad + fh)
  end
  local function placeBelow()
    setTopLeft(ml, mb - pad)
  end

  placeRight()
  if FitsOnScreen(f) and not FramesOverlap(f, mainF) then return end

  placeLeft()
  if FitsOnScreen(f) and not FramesOverlap(f, mainF) then return end

  placeAbove()
  if FitsOnScreen(f) and not FramesOverlap(f, mainF) then return end

  placeBelow()
  if FitsOnScreen(f) and not FramesOverlap(f, mainF) then return end

  f:ClearAllPoints()
  f:SetPoint("CENTER", UIParent, "CENTER", 80, 0)
end

function UI:_HookDocking()
  -- Docking hooks removed: Help and Main should NOT move together.
  -- (We still keep EnsureNoOverlap on open, but it places relative to UIParent.)
  return
end



-- -------------------------------------------------------
-- UI Primitives
-- -------------------------------------------------------
local function MakePanel(parent, bg)
  local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  f:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  f:SetBackdropColor(C4(bg or PANEL, PANEL))
  f:SetBackdropBorderColor(C4(BORDER, BORDER))
  return f
end

local function MakeDivider(parent)
  local t = parent:CreateTexture(nil, "BORDER")
  t:SetTexture("Interface/Buttons/WHITE8x8")
  t:SetVertexColor(BORDER[1], BORDER[2], BORDER[3], 0.85)

  local function SyncDividerGeometry()
    local px = 1
    if Widgets and Widgets.PixelSize then
      px = Widgets:PixelSize(parent)
    end

    if t.SetHeight then
      t:SetHeight(px)
    end
  end

  t._wfSyncDividerGeometry = SyncDividerGeometry
  SyncDividerGeometry()

  if Widgets and Widgets.MakePixelPerfect then
    Widgets:MakePixelPerfect(t, parent, { mode = "h" })
  end

  t:SetAlpha(0.24)
  return t
end

local function MakeSectionTitle(parent, text)
  local fs = parent:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")

  -- Slightly bolder/crisper section titles (better hierarchy on dark panels).
  local fnt, sz = fs:GetFont()
  if not fnt then
    fnt = BODY_FONT
  end
  sz = tonumber(sz) or 12
  fs:SetFont(fnt, sz + 1, "OUTLINE")

  fs:SetTextColor(C4(ACCENT, ACCENT))
  fs:SetText(text or "")
  return fs
end

local function MakeHeaderText(parent, text)
  local fs = parent:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")

  -- Crisper page header text without changing the overall font choice.
  local fnt, sz = fs:GetFont()
  if not fnt then
    fnt = DISPLAY_FONT
  end
  sz = tonumber(sz) or 16
  fs:SetFont(fnt, sz + 1, "OUTLINE")

  fs:SetTextColor(C4(ACCENT, ACCENT))
  fs:SetText(text or "")
  return fs
end

local function MakeBodyText(parent, text)
  local fs = parent:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
  fs:SetTextColor(C4(TEXT, TEXT))
  fs:SetText(text or "")
  return fs
end

local function MakeMutedText(parent, text)
  local fs = parent:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  fs:SetTextColor(C4(MUTED, MUTED))
  fs:SetText(text or "")
  return fs
end

local function MakeCard(parent, titleText)
  local c = MakePanel(parent, BG)
  c:SetBackdropColor(C4(BG, BG))

  local t = c:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  t:SetTextColor(C4(MUTED, MUTED))
  t:SetText(titleText or "")
  t:SetPoint("TOPLEFT", 12, -10)

  local v = c:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  v:SetTextColor(C4(ACCENT, ACCENT))
  v:SetText("—")
  v:SetPoint("TOPLEFT", t, "BOTTOMLEFT", 0, -6)

  c._title = t
  c._value = v
  return c
end

local function MakeInput(parent, w, h)
  local eb = CreateFrame("EditBox", nil, parent, "BackdropTemplate")
  eb:SetSize(w or 240, h or 24)
  eb:SetAutoFocus(false)
  eb:SetFontObject("WF_GameFontHighlight")

  eb:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 10,
    insets = { left = 6, right = 6, top = 4, bottom = 4 },
  })
  eb:SetBackdropColor(0.04, 0.04, 0.04, 0.90)
  eb:SetBackdropBorderColor(C4(BORDER, BORDER))
  eb:SetTextInsets(6, 6, 2, 2)

  eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
  return eb
end

-- NOTE:
-- We intentionally do NOT resize the CheckButton to include its label.
-- Doing so makes the button's hit-rect overlap nearby controls and breaks
-- side-by-side layout in tight toolbars. Instead, callers should anchor the
-- *next* checkbox to the previous checkbox's Text (label) width.
local function _SizeCheckButton(_b)
  return
end

-- Make the checkbox label clickable without expanding the CheckButton's hit-rect.
-- Expanding the hit-rect/width causes overlap with nearby controls in tight toolbars.
local function _AttachCheckLabelHit(b)
  if not b or b._wfLabelHit then return end
  if not (b.Text and b.Text.GetStringWidth) then return end

  local hit = CreateFrame("Button", nil, b)
  hit:EnableMouse(true)
  hit:RegisterForClicks("LeftButtonUp")
  hit:SetFrameLevel((b:GetFrameLevel() or 0) + 2)

  local function SizeHit()
    if not (b and b.Text) then return end
    local w = b.Text:GetStringWidth() or 0
    if w < 1 then w = 1 end
    hit:SetSize(w + 8, 18)
  end

  hit:SetPoint("LEFT", b.Text, "LEFT", -4, 0)
  hit:SetPoint("CENTER", b.Text, "CENTER", 0, 0)
  SizeHit()

  hit:SetScript("OnClick", function()
    if b and b.Click then b:Click() end
  end)

  -- Provide a small hover affordance by using the checkbox highlight.
  hit:SetScript("OnEnter", function()
    if b and b.LockHighlight then b:LockHighlight() end
  end)
  hit:SetScript("OnLeave", function()
    if b and b.UnlockHighlight then b:UnlockHighlight() end
  end)

  -- Keep sizing correct when the control is shown (font/string widths can resolve late).
  if b.HookScript then
    b:HookScript("OnShow", SizeHit)
  end

  b._wfLabelHit = hit
end

local function MakeCheck(parent, label, getFn, setFn)
  local b = CreateFrame("CheckButton", nil, parent, "ChatConfigCheckButtonTemplate")

  WF_ApplyBlueCheck(b)
  b.Text:SetText(label)
  b.Text:SetTextColor(C4(TEXT, TEXT))

  -- Keep template defaults (small checkbox frame). Layout code handles spacing
  -- using the label width when needed.

  b:SetScript("OnShow", function(self)
    self:SetChecked(getFn() and true or false)
  end)
  b:SetScript("OnClick", function(self)
    setFn(self:GetChecked() and true or false)
  end)

  -- Allow clicking the label text without inflating the checkbox hitbox.
  _AttachCheckLabelHit(b)
  return b
end

-- -------------------------------------------------------
-- Premium Dropdown (no UIDropDownMenuTemplate)
-- -------------------------------------------------------
function UI:_EnsureDropdown()
  if self._dd then return end

  local blocker = CreateFrame("Frame", nil, UIParent)
  blocker:SetAllPoints(UIParent)
  blocker:EnableMouse(true)
  blocker:Hide()
  blocker:SetFrameStrata("FULLSCREEN_DIALOG")
  blocker:SetFrameLevel(198)
  blocker:SetScript("OnMouseDown", function() UI:CloseDropdown() end)

  local dd = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  dd:SetFrameStrata("FULLSCREEN_DIALOG")
  dd:SetFrameLevel(199)
  dd:SetSize(240, 220)
  dd:EnableMouse(true)
  dd:Hide()
  dd:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  dd:SetBackdropColor(C4(MENU_BG, MENU_BG))
  dd:SetBackdropBorderColor(C4(MENU_BORDER, MENU_BORDER))

  local ddBG = dd:CreateTexture(nil, "ARTWORK", nil, 0)
  ddBG:SetAllPoints(dd)
  ddBG:SetTexture("Interface/Buttons/WHITE8x8")
  ddBG:SetVertexColor(C4(MENU_BG, MENU_BG))
  if ddBG.SetDrawLayer then ddBG:SetDrawLayer("ARTWORK", 0) end
  dd._wfSolidBG = ddBG

  local scroll = CreateFrame("ScrollFrame", nil, dd, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
  if scroll.ScrollBar then
    scroll.ScrollBar:Hide()
    if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Hide then scroll.ScrollBar._wfBG:Hide() end
  end
  scroll:SetPoint("TOPLEFT", 6, -6)
  scroll:SetPoint("BOTTOMRIGHT", -28, 6)

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

  dd._scroll  = scroll
  dd._content = content
  dd._rows    = {}
  dd._onPick  = nil
  dd._value   = nil
  dd._items   = nil

  self._dd = dd
  self._ddBlocker = blocker
end

function UI:CloseDropdown()
  if self._dd then self._dd:Hide() end
  if self._ddBlocker then self._ddBlocker:Hide() end
end

function UI:OpenDropdown(anchor, width, items, currentValue, onPick)
  self:_EnsureDropdown()

  local dd = self._dd
  local blocker = self._ddBlocker
  dd._items = items or {}
  dd._value = currentValue
  dd._onPick = onPick

  dd:ClearAllPoints()
  dd:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -4)
  dd:SetWidth(width or anchor:GetWidth())

  local rows = dd._rows
  local content = dd._content
  local rowH = 22

  local function EnsureRow(i)
    local r = rows[i]
    if r then return r end

    r = CreateFrame("Button", nil, content)
    r:SetHeight(rowH)
    r:SetPoint("LEFT", 0, 0)
    r:SetPoint("RIGHT", 0, 0)

    r.bg = r:CreateTexture(nil, "BACKGROUND")
    r.bg:SetAllPoints(r)
    r.bg:SetTexture("Interface/Buttons/WHITE8x8")
    r.bg:SetVertexColor(C4(MENU_HOVER, MENU_HOVER))
    r.bg:SetAlpha(0)

    r.txt = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    r.txt:SetPoint("LEFT", 10, 0)
    r.txt:SetTextColor(C4(TEXT, TEXT))

    local tooltipPalette = (Widgets and Widgets.GetTooltipPalette and Widgets:GetTooltipPalette()) or nil
    local DROPDOWN_TOOLTIP_MUTED = tooltipPalette and tooltipPalette.muted or { MUTED[1], MUTED[2], MUTED[3] }
    local DROPDOWN_TOOLTIP_TITLE = tooltipPalette and tooltipPalette.title or { TEXT[1], TEXT[2], TEXT[3] }
    local DROPDOWN_TOOLTIP_BODY = tooltipPalette and tooltipPalette.body or { 0.86, 0.86, 0.86, 1.00 }

    local function AddDropdownTooltipTextLine(text, style)
      text = tostring(text or "")
      if text == "" then
        GameTooltip:AddLine(" ", 1, 1, 1)
        return
      end

      if style == "accent" or style == "blue" or style == "title" or style == "header" then
        GameTooltip:AddLine(text, DROPDOWN_TOOLTIP_TITLE[1], DROPDOWN_TOOLTIP_TITLE[2], DROPDOWN_TOOLTIP_TITLE[3], true)
      elseif style == "muted" or style == "gray" or style == "grey" or style == "hint" then
        GameTooltip:AddLine(text, DROPDOWN_TOOLTIP_MUTED[1], DROPDOWN_TOOLTIP_MUTED[2], DROPDOWN_TOOLTIP_MUTED[3], true)
      else
        GameTooltip:AddLine(text, DROPDOWN_TOOLTIP_BODY[1], DROPDOWN_TOOLTIP_BODY[2], DROPDOWN_TOOLTIP_BODY[3], true)
      end
    end

    local function AddDropdownTooltipLine(line, lineIndex)
      if line == nil then return end

      local text = nil
      local style = nil

      if type(line) == "table" then
        text = tostring(line.text or line[1] or "")
        style = tostring(line.style or line.color or "")
      else
        text = tostring(line)
      end

      if not style or style == "" then
        local lower = text:lower()
        if lineIndex and lineIndex > 1 then
          style = "muted"
        elseif lower:match("^examples?:") or lower:match("^tip:") or lower:match("^note:") or lower:match("^current:") or lower:match("^currently:") then
          style = "muted"
        else
          style = "text"
        end
      end

      local segments = {}
      local start = 1
      while true do
        local newlineStart, newlineEnd = text:find("\n", start, true)
        if newlineStart then
          segments[#segments + 1] = text:sub(start, newlineStart - 1)
          start = newlineEnd + 1
        else
          segments[#segments + 1] = text:sub(start)
          break
        end
      end

      for segmentIndex = 1, #segments do
        local segmentStyle = style
        if (not style or style == "" or style == "text") and segmentIndex > 1 then
          segmentStyle = "muted"
        end
        AddDropdownTooltipTextLine(segments[segmentIndex], segmentStyle)
      end
    end

    r:SetScript("OnEnter", function(self)
      self.bg:SetAlpha(MENU_HOVER[4] or 0.10)

      if self._tooltipTitle or self._tooltipBody then
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:ClearLines()

        if self._tooltipTitle and self._tooltipTitle ~= "" then
          GameTooltip:AddLine(self._tooltipTitle, DROPDOWN_TOOLTIP_TITLE[1], DROPDOWN_TOOLTIP_TITLE[2], DROPDOWN_TOOLTIP_TITLE[3], true)
        end

        if type(self._tooltipBody) == "table" then
          for lineIndex = 1, #self._tooltipBody do
            AddDropdownTooltipLine(self._tooltipBody[lineIndex], lineIndex)
          end
        elseif self._tooltipBody and self._tooltipBody ~= "" then
          AddDropdownTooltipLine(self._tooltipBody, 1)
        end

        GameTooltip:Show()
      end
    end)
    r:SetScript("OnLeave", function(self)
      if self._selected then self.bg:SetAlpha(MENU_SELECTED[4] or 0.16) else self.bg:SetAlpha(0) end
      if GameTooltip and GameTooltip:IsOwned(self) then
        GameTooltip:Hide()
      end
    end)
    r:SetScript("OnClick", function(self)
      if dd._onPick then dd._onPick(self._value) end
      UI:CloseDropdown()
    end)

    rows[i] = r
    return r
  end

  local maxVis = 10
  local count = #dd._items
  local vis = math.min(maxVis, math.max(count, 1))
  local needsScroll = count > maxVis
  dd:SetHeight((vis * rowH) + 12)

  if dd._scroll and dd._scroll.ScrollBar then
    if needsScroll then
      dd._scroll.ScrollBar:Show()
      if dd._scroll.ScrollBar._wfBG and dd._scroll.ScrollBar._wfBG.Show then
        dd._scroll.ScrollBar._wfBG:Show()
      end
    else
      if dd._scroll.SetVerticalScroll then
        pcall(dd._scroll.SetVerticalScroll, dd._scroll, 0)
      end
      if dd._scroll.ScrollBar.SetValue then
        pcall(dd._scroll.ScrollBar.SetValue, dd._scroll.ScrollBar, 0)
      end
      dd._scroll.ScrollBar:Hide()
      if dd._scroll.ScrollBar._wfBG and dd._scroll.ScrollBar._wfBG.Hide then
        dd._scroll.ScrollBar._wfBG:Hide()
      end
    end
  end

  local w = dd._scroll:GetWidth()
  if w and w > 1 then
    if needsScroll then
      content:SetWidth(w - 14)
    else
      content:SetWidth(w)
    end
  end

  for i = 1, math.max(count, #rows) do
    local r = rows[i]
    local entry = dd._items[i]
    if entry then
      r = EnsureRow(i)
      r:Show()
      r:SetPoint("TOPLEFT", 0, -((i - 1) * rowH))
      r:SetPoint("TOPRIGHT", 0, -((i - 1) * rowH))

      local value = entry
      local label = entry
      local tooltipTitle = nil
      local tooltipBody = nil

      if type(entry) == "table" then
        value = entry.value
        if value == nil then value = entry.label end
        if value == nil then value = entry.text end
        label = entry.label
        if label == nil then label = entry.text end
        if label == nil then label = value end
        tooltipTitle = entry.tooltipTitle or entry.tipTitle or entry.title
        tooltipBody = entry.tooltipBody or entry.tipBody or entry.description
      end

      r._value = value
      r._tooltipTitle = tooltipTitle
      r._tooltipBody = tooltipBody

      if Util and Util.PrettyToken then
        r.txt:SetText(Util:PrettyToken(label))
      else
        r.txt:SetText(tostring(label))
      end
      r._selected = (value == dd._value)
      if r._selected then
        r.bg:SetVertexColor(C4(MENU_SELECTED, MENU_SELECTED))
        r.bg:SetAlpha(MENU_SELECTED[4] or 0.16)
      else
        r.bg:SetVertexColor(C4(MENU_HOVER, MENU_HOVER))
        r.bg:SetAlpha(0)
      end
    elseif r then
      r._tooltipTitle = nil
      r._tooltipBody = nil
      r:Hide()
    end
  end

  content:SetHeight(count * rowH)

  blocker:Show()
  dd:Show()
  blocker:SetFrameLevel(998)
  blocker:Raise()
  dd:SetFrameLevel(999)
  dd:Raise()
end


-- -------------------------------------------------------
-- Context Menu (right-click actions)
-- -------------------------------------------------------
function UI:_EnsureContextMenu()
  if self._ctx then return end

  local blocker = CreateFrame("Frame", nil, UIParent)
  blocker:SetAllPoints(UIParent)
  blocker:EnableMouse(true)
  blocker:Hide()
  blocker:SetFrameStrata("FULLSCREEN_DIALOG")
  blocker:SetFrameLevel(198)
  blocker:SetScript("OnMouseDown", function() UI:CloseContextMenu() end)

  local ctx = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  ctx:SetFrameStrata("FULLSCREEN_DIALOG")
  ctx:SetFrameLevel(199)
  ctx:SetSize(240, 220)
  ctx:EnableMouse(true)
  ctx:Hide()
  ctx:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  ctx:SetBackdropColor(C4(MENU_BG, MENU_BG))
  ctx:SetBackdropBorderColor(C4(MENU_BORDER, MENU_BORDER))

  local ctxBG = ctx:CreateTexture(nil, "ARTWORK", nil, 0)
  ctxBG:SetAllPoints(ctx)
  ctxBG:SetTexture("Interface/Buttons/WHITE8x8")
  ctxBG:SetVertexColor(C4(MENU_BG, MENU_BG))
  if ctxBG.SetDrawLayer then ctxBG:SetDrawLayer("ARTWORK", 0) end
  ctx._wfSolidBG = ctxBG

  local scroll = CreateFrame("ScrollFrame", nil, ctx, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", 6, -6)
  scroll:SetPoint("BOTTOMRIGHT", -24, 6)
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then
    Widgets:SkinScrollBar(scroll.ScrollBar)
  end
  if scroll.ScrollBar then
    scroll.ScrollBar:ClearAllPoints()
    scroll.ScrollBar:SetPoint("TOPRIGHT", ctx, "TOPRIGHT", -6, -6)
    scroll.ScrollBar:SetPoint("BOTTOMRIGHT", ctx, "BOTTOMRIGHT", -6, 6)
  end

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

  ctx._scroll = scroll
  ctx._content = content
  ctx._rows = {}

  ctx:SetScript("OnHide", function(self)
    self:SetScript("OnUpdate", nil)
    self._wfContextUpdater = nil
    self._wfMergeMenuState = nil
    self._wfMergeMenuTicker = nil
  end)

  self._ctx = ctx
  self._ctxBlocker = blocker
end

function UI:CloseContextMenu()
  if self._ctx then
    self._ctx:SetScript("OnUpdate", nil)
    self._ctx._wfContextUpdater = nil
    self._ctx._wfMergeMenuState = nil
    self._ctx._wfMergeMenuTicker = nil
    self._ctx:Hide()
  end
  if self._ctxBlocker then self._ctxBlocker:Hide() end
end

function UI:SetContextMenuOnUpdate(fn)
  self:_EnsureContextMenu()
  if self._ctx then
    self._ctx._wfContextUpdater = fn
    if InCombatLockdown and InCombatLockdown() then
      return
    end
    self._ctx:SetScript("OnUpdate", fn)
  end
end


-- -------------------------------------------------------
-- Global ESC handler
-- Ensures any open WhatFarm dropdown/context menus close when ESC is pressed,
-- without consuming the key (so other ESC behavior still runs).
-- -------------------------------------------------------
function UI:_EnsureEscDropdownCloser()
  if self._escDropdownCloser then return end

  local f = CreateFrame("Frame", nil, UIParent)
  f:SetAllPoints(UIParent)
  f:EnableKeyboard(true)
  WF_TrySetPropagateKeyboardInput(f, true)
  f:SetFrameStrata("BACKGROUND")
  f:SetScript("OnKeyDown", function(self, key)
    WF_TrySetPropagateKeyboardInput(self, true)

    if key ~= "ESCAPE" then return end

    -- Close Blizzard dropdowns too (in case a lib menu is open)
    if _G.CloseDropDownMenus then
      pcall(_G.CloseDropDownMenus)
    end

    -- Close WhatFarm menus
    if UI.CloseDropdown then UI:CloseDropdown() end
    if UI.CloseContextMenu then UI:CloseContextMenu() end

    -- Modal stack behavior: farm edit closes before the main WhatFarm window.
    if UI._farmMeta and UI._farmMeta.IsShown and UI._farmMeta:IsShown() then
      if UI.CloseFarmMetaModal then UI:CloseFarmMetaModal() end
      WF_TrySetPropagateKeyboardInput(self, false)
      return
    end
  end)
  f:Show()

  self._escDropdownCloser = f
end

local function CursorPoint()
  local x, y = GetCursorPosition()
  local s = UIParent:GetScale()
  x = (x or 0) / s
  y = (y or 0) / s
  return x, y
end

function UI:OpenContextMenu(items, width, anchorX, anchorY)
  self:_EnsureContextMenu()
  self:CloseDropdown()

  local ctx = self._ctx
  local blocker = self._ctxBlocker
  local content = ctx._content
  local rows = ctx._rows

  ctx:SetScript("OnUpdate", nil)
  ctx._wfContextUpdater = nil
  ctx._wfMergeMenuState = nil
  ctx._wfMergeMenuTicker = nil

  for i = 1, #rows do rows[i]:Hide() end

  local rowH = 22
  local yOff = 0
  local maxW = width or 240
  local count = 0

  -- If any item provides a `checked` field, render a checkbox column.
  local useChecks = false
  for i = 1, #(items or {}) do
    local it = items[i]
    if it and (not it.sep) and it.checked ~= nil then
      useChecks = true
      break
    end
  end

  local function EnsureRow(i)
    local r = rows[i]
    if r then return r end

    r = CreateFrame("Button", nil, content)
    r:SetHeight(rowH)
    r:SetPoint("LEFT", 0, 0)
    r:SetPoint("RIGHT", 0, 0)

    r.bg = r:CreateTexture(nil, "BACKGROUND")
    r.bg:SetAllPoints(r)
    r.bg:SetTexture("Interface/Buttons/WHITE8x8")
    r.bg:SetVertexColor(C4(MENU_HOVER, MENU_HOVER))
    r.bg:SetAlpha(0)

    -- Optional checkbox column (shown when `useChecks` is true)
    r.checkBG = r:CreateTexture(nil, "OVERLAY")
    r.checkBG:SetSize(14, 14)
    r.checkBG:SetPoint("LEFT", 10, 0)
    r.checkBG:SetTexture("Interface/Buttons/UI-CheckBox-Up")
    r.checkBG:SetAlpha(0.75)
    r.checkBG:Hide()

    r.check = r:CreateTexture(nil, "OVERLAY")
    r.check:SetSize(14, 14)
    r.check:SetPoint("CENTER", r.checkBG, "CENTER", 0, 0)
    r.check:SetTexture("Interface/Buttons/UI-CheckBox-Check")
    if r.check.SetDesaturated then r.check:SetDesaturated(true) end -- Blizzard check texture is green; desaturate so Theme tint is true blue
    r.check:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
    r.check:Hide()

    r.txt = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    r.txt:SetPoint("LEFT", 10, 0)
    r.txt:SetPoint("RIGHT", -10, 0)
    r.txt:SetJustifyH("LEFT")
    r.txt:SetTextColor(C4(TEXT, TEXT))

    r:SetScript("OnEnter", function(self)
      if self._disabled or self._passive then return end
      self.bg:SetVertexColor(C4(MENU_HOVER, MENU_HOVER))
      self.bg:SetAlpha(MENU_HOVER[4] or 0.10)
    end)
    r:SetScript("OnLeave", function(self) self.bg:SetAlpha(0) end)

    rows[i] = r
    return r
  end

  local function EnsureSep(i)
    local r = EnsureRow(i)
    r:EnableMouse(false)
    r.txt:SetText("")
    r.bg:SetAlpha(0)
    if r.checkBG then r.checkBG:Hide() end
    if r.check then r.check:Hide() end

    if not r.sep then
      r.sep = r:CreateTexture(nil, "BORDER")
      r.sep:SetTexture("Interface/Buttons/WHITE8x8")
      local px = 1
      local leftX, rightX = 8, -8
      if Widgets and Widgets.PixelSize then
        px = Widgets:PixelSize(r)
      end
      if Widgets and Widgets.Snap then
        leftX = Widgets:Snap(r, leftX)
        rightX = Widgets:Snap(r, rightX)
      end

      r.sep:SetHeight(px)
      if Widgets and Widgets.MakePixelPerfect then
        Widgets:MakePixelPerfect(r.sep, r)
      end
      r.sep:SetPoint("LEFT", leftX, 0)
      r.sep:SetPoint("RIGHT", rightX, 0)
      r.sep:SetVertexColor(C4(MENU_SEPARATOR, MENU_SEPARATOR))
      r.sep:SetAlpha(MENU_SEPARATOR[4] or 0.60)
    end
    r.sep:Show()
    return r
  end

  for i = 1, #(items or {}) do
    local it = items[i]
    if it then
      count = count + 1
      local r
      if it.sep then
        r = EnsureSep(count)
        r:SetHeight(10)
        r:SetPoint("TOPLEFT", 0, -yOff)
        r:SetPoint("TOPRIGHT", 0, -yOff)
        yOff = yOff + 10
        r._disabled = true
        r:SetScript("OnClick", nil)
      else
        r = EnsureRow(count)
        r:SetHeight(rowH)
        r:SetPoint("TOPLEFT", 0, -yOff)
        r:SetPoint("TOPRIGHT", 0, -yOff)
        yOff = yOff + rowH

        r._disabled = it.disabled and true or false
        r._passive = (it.passive or it.nonInteractive) and true or false
        r:EnableMouse(not r._disabled and not r._passive)

        if r.sep then r.sep:Hide() end

        -- Checkbox column
        if useChecks then
          r.checkBG:Show()
          r.checkBG:SetAlpha(r._disabled and 0.25 or 0.75)
          r.check:SetAlpha(r._disabled and 0.25 or 1.00)
          if it.checked then r.check:Show() else r.check:Hide() end

          r.txt:ClearAllPoints()
          r.txt:SetPoint("LEFT", 30, 0)
          r.txt:SetPoint("RIGHT", -10, 0)
        else
          if r.checkBG then r.checkBG:Hide() end
          if r.check then r.check:Hide() end

          r.txt:ClearAllPoints()
          r.txt:SetPoint("LEFT", 10, 0)
          r.txt:SetPoint("RIGHT", -10, 0)
        end

        r.txt:SetText(tostring(it.text or ""))
        if type(it.textColor) == "table" then
          r.txt:SetTextColor(
            it.textColor[1] or 1,
            it.textColor[2] or 1,
            it.textColor[3] or 1,
            it.textColor[4] or 1
          )
        elseif r._disabled then
          r.txt:SetTextColor(C4(MUTED, MUTED))
        else
          r.txt:SetTextColor(C4(TEXT, TEXT))
        end

        if r._disabled or r._passive then
          r:SetScript("OnClick", nil)
        else
          r:SetScript("OnClick", function()
            UI:CloseContextMenu()
            if it.fn then it.fn() end
          end)
        end
      end
      r:Show()
    end
  end

  if count == 0 then return end

  local desiredH = yOff + 12
  local maxMenuH = math.max(140, math.floor((UIParent and UIParent.GetHeight and UIParent:GetHeight() or 900) * 0.55))

  ctx:SetWidth(maxW)
  ctx:SetHeight(math.min(desiredH, maxMenuH))

  if ctx._content and ctx._content.SetWidth then
    local innerW = maxW - ((desiredH > maxMenuH) and 28 or 12)
    if innerW < 1 then innerW = 1 end
    ctx._content:SetWidth(innerW)
    ctx._content:SetHeight(math.max(1, yOff))
  end

  if ctx._scroll and ctx._scroll.SetVerticalScroll then
    ctx._scroll:SetVerticalScroll(0)
  end
  if ctx._scroll and ctx._scroll.ScrollBar then
    if desiredH > maxMenuH then
      ctx._scroll.ScrollBar:Show()
      if ctx._scroll.ScrollBar._wfBG and ctx._scroll.ScrollBar._wfBG.Show then
        ctx._scroll.ScrollBar._wfBG:Show()
      end
    else
      if ctx._scroll.ScrollBar.SetValue then
        pcall(ctx._scroll.ScrollBar.SetValue, ctx._scroll.ScrollBar, 0)
      end
      ctx._scroll.ScrollBar:Hide()
      if ctx._scroll.ScrollBar._wfBG and ctx._scroll.ScrollBar._wfBG.Hide then
        ctx._scroll.ScrollBar._wfBG:Hide()
      end
    end
  end

  local x, y = anchorX, anchorY
  if x == nil or y == nil then
    x, y = CursorPoint()
  end
  ctx._wfAnchorX = x
  ctx._wfAnchorY = y

  ctx:ClearAllPoints()
  ctx:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y)

  blocker:Show()
  ctx:Show()
  blocker:SetFrameLevel(998)
  blocker:Raise()
  ctx:SetFrameLevel(999)
  ctx:Raise()
end


-- -------------------------------------------------------
-- Confirmation (destructive actions)
-- -------------------------------------------------------
function UI:_EnsureConfirm()
  if self._confirm and self._confirm._bYes and self._confirm._bNo and self._confirm._bAlt and self._confirm._bThird then
    return
  end

  if self._confirm then
    self._confirm:Hide()
    self._confirm = nil
  end
  if self._confirmBlocker then
    self._confirmBlocker:Hide()
    self._confirmBlocker = nil
  end

  local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  blocker:SetAllPoints(UIParent)
  blocker:EnableMouse(true)
  blocker:Hide()
  blocker:SetFrameStrata("FULLSCREEN_DIALOG")
  blocker:SetFrameLevel(198)
  blocker:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
  blocker:SetBackdropColor(0, 0, 0, 0.60)
  blocker:SetScript("OnMouseDown", function() UI:CloseConfirm() end)

  local box = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  box:SetFrameStrata("FULLSCREEN_DIALOG")
  box:SetFrameLevel(199)
  box:SetSize(420, 182)
  box:EnableMouse(true)
  box:Hide()
  box:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  box:SetBackdropColor(0, 0, 0, 1)
  box:SetBackdropBorderColor(1, 1, 1, 0.14)

  local boxBG = box:CreateTexture(nil, "ARTWORK", nil, 0)
  boxBG:SetAllPoints(box)
  boxBG:SetTexture("Interface/Buttons/WHITE8x8")
  boxBG:SetVertexColor(0, 0, 0, 1)
  if boxBG.SetDrawLayer then boxBG:SetDrawLayer("ARTWORK", 0) end
  box._wfSolidBG = boxBG

  local title = box:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  title:SetTextColor(C4(ACCENT, ACCENT))
  title:SetText("Confirm")

  local body = box:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
  body:SetPoint("TOPLEFT", 14, -44)
  body:SetPoint("TOPRIGHT", -14, -44)
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")
  if body.SetWordWrap then body:SetWordWrap(true) end
  body:SetTextColor(C4(TEXT, TEXT))
  body:SetText("—")

  local bYes = Widgets:Button(box, "Confirm", 160, 26)
  bYes:SetPoint("BOTTOMRIGHT", -14, 14)

  local bAlt = Widgets:Button(box, "Option", 160, 26)
  bAlt:Hide()

  local bThird = Widgets:Button(box, "Option", 150, 26)
  bThird:Hide()

  local bNo = Widgets:Button(box, "Cancel", 120, 26)

  local function _SetConfirmButtonSize(btn, w, h)
    if not btn then return end
    btn:SetSize(w or btn:GetWidth() or 120, h or 26)
  end

  local function _CenterConfirmButtonRow(buttons, gap, bottomInset)
    gap = gap or 12
    bottomInset = bottomInset or 14

    local totalWidth = 0
    local visible = {}
    for i = 1, #buttons do
      local btn = buttons[i]
      if btn then
        visible[#visible + 1] = btn
        totalWidth = totalWidth + (btn:GetWidth() or 0)
      end
    end
    if #visible <= 0 then return end

    totalWidth = totalWidth + ((#visible - 1) * gap)
    local startX = -(totalWidth * 0.5)
    local prev = nil
    for i = 1, #visible do
      local btn = visible[i]
      btn:ClearAllPoints()
      if prev then
        btn:SetPoint("LEFT", prev, "RIGHT", gap, 0)
      else
        btn:SetPoint("BOTTOMLEFT", box, "BOTTOM", startX, bottomInset)
      end
      prev = btn
    end
  end

  local function _AutoFitConfirmHeight(minHeight, gapAboveButtons, bottomInset)
    local bodyHeight = 0
    if body and body.GetStringHeight then
      bodyHeight = math.ceil((body:GetStringHeight() or 0) + 0.5)
    end

    gapAboveButtons = gapAboveButtons or 18
    bottomInset = bottomInset or 14

    local desiredHeight = 44 + bodyHeight + gapAboveButtons + 26 + bottomInset
    if desiredHeight < (minHeight or 0) then
      desiredHeight = minHeight or desiredHeight
    end

    box:SetHeight(desiredHeight)
  end

  local function _LayoutTwoButtons()
    box:SetSize(420, 182)
    bThird:Hide()
    bThird:ClearAllPoints()
    bAlt:Hide()
    bAlt:ClearAllPoints()

    if bNo.Show then bNo:Show() end
    if bYes.Show then bYes:Show() end

    _SetConfirmButtonSize(bYes, 150, 26)
    _SetConfirmButtonSize(bNo, 120, 26)
    _CenterConfirmButtonRow({ bYes, bNo }, 12, 14)
  end

  local function _LayoutThreeButtons()
    box:SetSize(560, 160)
    bThird:Hide()
    bThird:ClearAllPoints()
    bAlt:Show()
    bAlt:ClearAllPoints()
    if bYes.Show then bYes:Show() end
    if bNo.Show then bNo:Show() end

    _SetConfirmButtonSize(bAlt, 150, 26)
    _SetConfirmButtonSize(bYes, 150, 26)
    _SetConfirmButtonSize(bNo, 150, 26)
    _CenterConfirmButtonRow({ bAlt, bYes, bNo }, 12, 18)
    _AutoFitConfirmHeight(160, 16, 18)
  end

  local function _LayoutFourButtons()
    box:SetSize(736, 170)
    bThird:Show()
    bThird:ClearAllPoints()
    bAlt:Show()
    bAlt:ClearAllPoints()
    if bYes.Show then bYes:Show() end
    if bNo.Show then bNo:Show() end

    _SetConfirmButtonSize(bThird, 155, 26)
    _SetConfirmButtonSize(bAlt, 155, 26)
    _SetConfirmButtonSize(bYes, 155, 26)
    _SetConfirmButtonSize(bNo, 155, 26)
    _CenterConfirmButtonRow({ bThird, bAlt, bYes, bNo }, 12, 18)
    _AutoFitConfirmHeight(170, 16, 18)
  end

  bNo:SetScript("OnClick", function() UI:CloseConfirm() end)
  bThird:SetScript("OnClick", function()
    local fn = box._onThird
    UI:CloseConfirm()
    if fn then fn() end
  end)
  bAlt:SetScript("OnClick", function()
    local fn = box._onAlt
    UI:CloseConfirm()
    if fn then fn() end
  end)
  bYes:SetScript("OnClick", function()
    local fn = box._onYes
    UI:CloseConfirm()
    if fn then fn() end
  end)

  local function _ApplyConfirmTooltip(btn, titleText, bodyText)
    if not btn then return end
    btn._wfTooltipTitle = titleText
    btn._wfTooltipText = bodyText
  end

  local function _ClearConfirmTooltip(btn)
    if not btn then return end
    btn._wfTooltipTitle = nil
    btn._wfTooltipText = nil
  end

  local function _ShowConfirmButtonTooltip(btn)
    if not (btn and GameTooltip) then return end
    local titleText = tostring(btn._wfTooltipTitle or "")
    local bodyText = tostring(btn._wfTooltipText or "")
    if bodyText == "" then return end

    GameTooltip:SetOwner(btn, "ANCHOR_TOP")
    GameTooltip:ClearLines()
    if titleText ~= "" then
      GameTooltip:AddLine(titleText, C4(ACCENT, ACCENT))
    end
    for line in string.gmatch(bodyText, "[^\n]+") do
      GameTooltip:AddLine(line, 0.82, 0.82, 0.82, true)
    end
    GameTooltip:Show()
  end

  local function _HideConfirmButtonTooltip()
    if GameTooltip then GameTooltip:Hide() end
  end

  local function _SetConfirmButtonEnabled(btn, enabled)
    if not btn then return end
    enabled = enabled and true or false
    if btn.SetEnabled then
      btn:SetEnabled(enabled)
    elseif enabled then
      if btn.Enable then btn:Enable() end
    else
      if btn.Disable then btn:Disable() end
    end
    if btn.EnableMouse then
      btn:EnableMouse(enabled or (btn._wfTooltipText ~= nil and btn._wfTooltipText ~= ""))
    end
    if enabled then
      btn:SetAlpha(1)
      if btn.label and btn.label.SetTextColor then
        btn.label:SetTextColor(C4(TEXT, TEXT))
      end
    else
      btn:SetAlpha(0.58)
      if btn.label and btn.label.SetTextColor then
        btn.label:SetTextColor(0.72, 0.72, 0.72, 1)
      end
    end
  end

  local function _AttachConfirmTooltipHandlers(btn)
    if not btn then return end
    btn:SetScript("OnEnter", function(self)
      _ShowConfirmButtonTooltip(self)
    end)
    btn:SetScript("OnLeave", function()
      _HideConfirmButtonTooltip()
    end)
  end

  _AttachConfirmTooltipHandlers(bYes)
  _AttachConfirmTooltipHandlers(bAlt)
  _AttachConfirmTooltipHandlers(bThird)
  _AttachConfirmTooltipHandlers(bNo)

  box._SetConfirmButtonEnabled = _SetConfirmButtonEnabled
  box._SetButtonStates = function(primaryEnabled, secondaryEnabled, tertiaryEnabled, cancelEnabled)
    _SetConfirmButtonEnabled(bYes, primaryEnabled ~= false)
    _SetConfirmButtonEnabled(bAlt, secondaryEnabled ~= false)
    _SetConfirmButtonEnabled(bThird, tertiaryEnabled ~= false)
    _SetConfirmButtonEnabled(bNo, cancelEnabled ~= false)
  end
  box._SetButtonTooltips = function(primaryTitle, primaryText, secondaryTitle, secondaryText, tertiaryTitle, tertiaryText, cancelTitle, cancelText)
    if primaryText and primaryText ~= "" then _ApplyConfirmTooltip(bYes, primaryTitle, primaryText) else _ClearConfirmTooltip(bYes) end
    if secondaryText and secondaryText ~= "" then _ApplyConfirmTooltip(bAlt, secondaryTitle, secondaryText) else _ClearConfirmTooltip(bAlt) end
    if tertiaryText and tertiaryText ~= "" then _ApplyConfirmTooltip(bThird, tertiaryTitle, tertiaryText) else _ClearConfirmTooltip(bThird) end
    if cancelText and cancelText ~= "" then _ApplyConfirmTooltip(bNo, cancelTitle, cancelText) else _ClearConfirmTooltip(bNo) end
    _SetConfirmButtonEnabled(bYes, bYes:IsEnabled())
    _SetConfirmButtonEnabled(bAlt, bAlt:IsEnabled())
    _SetConfirmButtonEnabled(bThird, bThird:IsEnabled())
    _SetConfirmButtonEnabled(bNo, bNo:IsEnabled())
  end

  box._title = title
  box._body = body
  box._bYes = bYes
  box._bAlt = bAlt
  box._bThird = bThird
  box._bNo = bNo
  box._LayoutTwoButtons = _LayoutTwoButtons
  box._LayoutThreeButtons = _LayoutThreeButtons
  box._LayoutFourButtons = _LayoutFourButtons

  self._confirm = box
  self._confirmBlocker = blocker
end

function UI:CloseConfirm()
  if self._confirm then
    self._confirm:Hide()
    self._confirm._onYes = nil
    self._confirm._onAlt = nil
    self._confirm._onThird = nil
    self._confirm._wfConfirmKind = nil
  end
  if self._confirmBlocker then self._confirmBlocker:Hide() end
end

function UI:_PrepareConfirm(kind)
  self:_EnsureConfirm()

  if self._confirmBlocker then
    self._confirmBlocker:Hide()
  end
  if self._confirm then
    self._confirm:Hide()
  end

  local box = self._confirm
  local blocker = self._confirmBlocker
  if not box then
    return nil, blocker
  end

  box._onYes = nil
  box._onAlt = nil
  box._onThird = nil
  box._wfConfirmKind = kind

  if box._title and box._title.SetText then
    box._title:SetText("Confirm")
  end
  if box._body and box._body.SetText then
    box._body:SetText("Are you sure?")
  end

  if box._bYes and box._bYes.SetText then
    box._bYes:SetText("Confirm")
  end
  if box._bAlt and box._bAlt.SetText then
    box._bAlt:SetText("Option")
  end
  if box._bThird and box._bThird.SetText then
    box._bThird:SetText("Option")
  end
  if box._bNo and box._bNo.SetText then
    box._bNo:SetText("Cancel")
  end

  if box._bAlt and box._bAlt.Hide then box._bAlt:Hide() end
  if box._bThird and box._bThird.Hide then box._bThird:Hide() end

  if box._SetButtonStates then
    box._SetButtonStates(true, true, true, true)
  end
  if box._SetButtonTooltips then
    box._SetButtonTooltips(nil, nil, nil, nil, nil, nil, nil, nil)
  end

  box:ClearAllPoints()
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 40)

  return box, blocker
end

function UI:Confirm(title, text, onYes, yesText)
  local box, blocker = self:_PrepareConfirm("single")
  if not box then return end

  box._onYes = onYes
  box._title:SetText(title or "Confirm")
  box._body:SetText(text or "Are you sure?")

  if box._bYes and box._bYes.SetText then
    box._bYes:SetText(yesText or "Confirm")
  end
  if box._bNo and box._bNo.SetText then
    box._bNo:SetText("Cancel")
  end

  if box._LayoutTwoButtons then box._LayoutTwoButtons() end
  if box._SetButtonStates then box._SetButtonStates(true, true, true, true) end

  blocker:Show()
  box:Show()
  blocker:SetFrameLevel(998)
  blocker:Raise()
  box:SetFrameLevel(999)
  box:Raise()
end

-- Confirm with two explicit actions (plus Cancel).
-- Cancel is right-most; primary action sits immediately to its left.
function UI:ConfirmTwoActions(title, text, primaryText, onPrimary, secondaryText, onSecondary, primaryDisabled, secondaryDisabled, primaryTooltipTitle, primaryTooltipText, secondaryTooltipTitle, secondaryTooltipText, cancelTooltipTitle, cancelTooltipText)
  local box, blocker = self:_PrepareConfirm("double")
  if not box then return end

  box._onYes = onPrimary
  box._onAlt = onSecondary

  box._title:SetText(title or "Confirm")
  box._body:SetText(text or "Are you sure?")

  if box._bYes and box._bYes.SetText then
    box._bYes:SetText(primaryText or "Confirm")
  end
  if box._bAlt and box._bAlt.SetText then
    box._bAlt:SetText(secondaryText or "Option")
  end
  if box._bNo and box._bNo.SetText then
    box._bNo:SetText("Cancel")
  end

  if box._LayoutThreeButtons then box._LayoutThreeButtons() end
  if box._SetButtonStates then box._SetButtonStates(not primaryDisabled, not secondaryDisabled, true, true) end
  if box._SetButtonTooltips then
    box._SetButtonTooltips(primaryTooltipTitle, primaryTooltipText, secondaryTooltipTitle, secondaryTooltipText, nil, nil, cancelTooltipTitle, cancelTooltipText)
  end

  blocker:Show()
  box:Show()
  blocker:SetFrameLevel(998)
  blocker:Raise()
  box:SetFrameLevel(999)
  box:Raise()
end

function UI:ConfirmThreeActions(title, text, primaryText, onPrimary, secondaryText, onSecondary, tertiaryText, onTertiary, primaryDisabled, secondaryDisabled, tertiaryDisabled, primaryTooltipTitle, primaryTooltipText, secondaryTooltipTitle, secondaryTooltipText, tertiaryTooltipTitle, tertiaryTooltipText, cancelTooltipTitle, cancelTooltipText)
  local box, blocker = self:_PrepareConfirm("triple")
  if not box then return end

  box._onYes = onPrimary
  box._onAlt = onSecondary
  box._onThird = onTertiary

  box._title:SetText(title or "Confirm")
  box._body:SetText(text or "Are you sure?")

  if box._bYes and box._bYes.SetText then
    box._bYes:SetText(primaryText or "Confirm")
  end
  if box._bAlt and box._bAlt.SetText then
    box._bAlt:SetText(secondaryText or "Option")
  end
  if box._bThird and box._bThird.SetText then
    box._bThird:SetText(tertiaryText or "Option")
  end
  if box._bNo and box._bNo.SetText then
    box._bNo:SetText("Cancel")
  end

  if box._LayoutFourButtons then box._LayoutFourButtons() end
  if box._SetButtonStates then box._SetButtonStates(not primaryDisabled, not secondaryDisabled, not tertiaryDisabled, true) end
  if box._SetButtonTooltips then
    box._SetButtonTooltips(primaryTooltipTitle, primaryTooltipText, secondaryTooltipTitle, secondaryTooltipText, tertiaryTooltipTitle, tertiaryTooltipText, cancelTooltipTitle, cancelTooltipText)
  end

  blocker:Show()
  box:Show()
  blocker:SetFrameLevel(998)
  blocker:Raise()
  box:SetFrameLevel(999)
  box:Raise()
end


local function MakeCombo(parent, labelText, getFn, setFn, itemsProvider)
  local wrap = CreateFrame("Frame", nil, parent)
  wrap:SetSize(240, 44)

  local lbl = MakeMutedText(wrap, labelText or "")
  lbl:SetPoint("TOPLEFT", 0, 0)

  local btn = CreateFrame("Button", nil, wrap, "BackdropTemplate")
  btn:SetPoint("TOPLEFT", 0, -18)
  btn:SetSize(240, 24)
  btn:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 10,
    insets = { left = 6, right = 6, top = 4, bottom = 4 },
  })

  local baseBg = { 0.04, 0.04, 0.04, 0.90 }
  local hoverBg = { 0.055, 0.070, 0.090, 0.96 }
  local activeBg = { 0.060, 0.078, 0.102, 0.98 }

  local baseBorder = { C4(BORDER, BORDER) }
  local hoverBorder = { Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.75 }
  local activeBorder = { Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.95 }

  btn:SetBackdropColor(unpack(baseBg))
  btn:SetBackdropBorderColor(unpack(baseBorder))

  local hoverFill = btn:CreateTexture(nil, "ARTWORK", nil, 1)
  hoverFill:SetAllPoints()
  hoverFill:SetTexture("Interface/Buttons/WHITE8X8")
  hoverFill:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0)
  hoverFill:SetBlendMode("ADD")

  local txt = MakeBodyText(btn, "—")
  txt:SetPoint("LEFT", 10, 0)
  txt:SetJustifyH("LEFT")
  txt:SetPoint("RIGHT", -24, 0)

  local arrow = btn:CreateTexture(nil, "OVERLAY")
  arrow:SetSize(14, 14)
  arrow:SetPoint("RIGHT", -8, 0)
  arrow:SetTexture("Interface/ChatFrame/UI-ChatIcon-ScrollDown-Up")
  if arrow.SetDesaturation then
    arrow:SetDesaturation(1)
  elseif arrow.SetDesaturated then
    arrow:SetDesaturated(true)
  end
  arrow:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.92)
  arrow:SetAlpha(0.90)

  local function ApplyVisualState(state)
    if state == "active" then
      btn:SetBackdropColor(unpack(activeBg))
      btn:SetBackdropBorderColor(unpack(activeBorder))
      hoverFill:SetAlpha(0.12)
      arrow:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1.00)
      arrow:SetAlpha(1.00)
      txt:SetTextColor(1.00, 1.00, 1.00, 1.00)
    elseif state == "hover" then
      btn:SetBackdropColor(unpack(hoverBg))
      btn:SetBackdropBorderColor(unpack(hoverBorder))
      hoverFill:SetAlpha(0.08)
      arrow:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.98)
      arrow:SetAlpha(1.00)
      txt:SetTextColor(1.00, 1.00, 1.00, 1.00)
    else
      btn:SetBackdropColor(unpack(baseBg))
      btn:SetBackdropBorderColor(unpack(baseBorder))
      hoverFill:SetAlpha(0.00)
      arrow:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.92)
      arrow:SetAlpha(0.90)
      txt:SetTextColor(C4(TEXT, TEXT))
    end
  end

  local function Refresh()
    local v = getFn()
    if Util and Util.PrettyToken then
      txt:SetText(Util:PrettyToken(v))
    else
      txt:SetText(tostring(v or "—"))
    end
  end

  btn:SetScript("OnEnter", function(self)
    if self._wfActive then
      ApplyVisualState("active")
      return
    end
    ApplyVisualState("hover")
  end)

  btn:SetScript("OnLeave", function(self)
    self._wfActive = false
    ApplyVisualState("normal")
  end)

  btn:SetScript("OnMouseDown", function(self)
    self._wfActive = true
    ApplyVisualState("active")
  end)

  btn:SetScript("OnMouseUp", function(self)
    if self:IsMouseOver() then
      self._wfActive = false
      ApplyVisualState("hover")
      return
    end
    self._wfActive = false
    ApplyVisualState("normal")
  end)

  btn:SetScript("OnClick", function()
    ApplyVisualState("active")
    UI:OpenDropdown(btn, btn:GetWidth(), itemsProvider(), getFn(), function(v)
      setFn(v)
      Refresh()
      if btn:IsMouseOver() then
        ApplyVisualState("hover")
        return
      end
      ApplyVisualState("normal")
    end)
  end)

  wrap:SetScript("OnShow", function()
    Refresh()
    ApplyVisualState("normal")
  end)

  wrap._refresh = Refresh
  wrap._btn = btn
  wrap._txt = txt
  return wrap
end

-- -------------------------------------------------------
-- Random Farm Selection
-- -------------------------------------------------------
local function _RandomAimValue(v)
  v = tostring(v or "")
  v = v:gsub("^%s+", ""):gsub("%s+$", "")
  if v == "" then return "Unknown" end
  return v
end

local function FilterFarm(farm, exp, ftype, aim, currentZoneOnly, includeUnknown, curMapId)
  if type(farm) ~= "table" then return false end

  -- Keep Random Farm (Pool) and Farms Loaded in sync with the Farms library.
  -- Archived farms and legacy blank records should not count as "loaded" or be rollable.
  if farm.archived then return false end
  if not farm.name or farm.name == "" then return false end

  if currentZoneOnly and curMapId and farm.mapId ~= curMapId then
    return false
  end

  local fx = farm.expansion or "Unknown"
  local ft = farm.farmType or "Any"
  local fa = _RandomAimValue(farm.aim)

  if exp and exp ~= "Any" then
    if exp == "Unknown" then
      if not (fx == "Unknown" or fx == "") then
        return false
      end
    elseif fx == "Unknown" or fx == "" then
      if not includeUnknown then return false end
    elseif fx ~= exp then
      return false
    end
  end

  if ftype and ftype ~= "Any" then
    if ftype == "Unknown" then
      if not (ft == "Unknown" or ft == "") then
        return false
      end
    elseif ft == "Unknown" or ft == "" then
      if not includeUnknown then return false end
    elseif ft ~= ftype then
      return false
    end
  end

  if aim and aim ~= "Any" then
    if aim == "Unknown" then
      if fa ~= "Unknown" then
        return false
      end
    elseif fa == "Unknown" then
      if not includeUnknown then return false end
    elseif fa ~= aim then
      return false
    end
  end

  return true
end

local function _WfShallowCopy(src)
  if type(src) ~= "table" then return src end
  local t = {}
  for k, v in pairs(src) do
    t[k] = v
  end
  return t
end

function UI:GetRandomPool()
  EnsureSettingsDefaults()

  local db = WF.DB:Get()
  local farms = (db and db.farms) or {}

  local curMapId = nil
  if C_Map and C_Map.GetBestMapForUnit then
    curMapId = C_Map.GetBestMapForUnit("player")
  end

  local exp = WF.DB:GetSetting("randomExpansion") or "Any"
  local ftype = WF.DB:GetSetting("randomFarmType") or "Any"
  local aim = WF.DB:GetSetting("randomAim") or "Any"
  local currentZoneOnly = WF.DB:GetSetting("randomCurrentZoneOnly") and true or false
  local includeUnknown = WF.DB:GetSetting("randomIncludeUnknown") and true or false

  local list = {}

  for _, farm in pairs(farms) do
    if FilterFarm(farm, exp, ftype, aim, currentZoneOnly, includeUnknown, curMapId) then
      list[#list + 1] = farm
    end
  end

  return list, {
    aim = aim,
    expansion = exp,
    farmType = ftype,
    currentZoneOnly = currentZoneOnly,
    includeUnknown = includeUnknown,
  }
end

-- -------------------------------------------------------
-- Farms Loaded (unfiltered)
-- -------------------------------------------------------
-- Single source of truth: the DB farm directory.
--
-- * Farms Loaded = total active farms available in the directory.
-- * Random Farm Pool = Farms Loaded filtered by the Random Farm filters.
--
-- NOTE: This intentionally does NOT apply Random Farm filters.
function UI:GetFarmsLoadedCount()
  local db = WF.DB and WF.DB.Get and WF.DB:Get() or nil
  local farms = (db and db.farms) or {}

  local n = 0
  for _, farm in pairs(farms) do
    if type(farm) == "table" and not farm.archived and (farm.name and farm.name ~= "") then
      n = n + 1
    end
  end
  return n
end

function UI:PickRandomFarm()
  local pool = select(1, self:GetRandomPool())
  if not pool or #pool == 0 then return nil end
  return pool[math.random(1, #pool)]
end

-- -------------------------------------------------------
-- Responsive Layout
-- -------------------------------------------------------
local function Clamp(v, lo, hi)
  if v < lo then return lo end
  if v > hi then return hi end
  return v
end

function UI:_Layout(liveFrameW, liveFrameH)
  if not self.frame or not self.frame:IsShown() then return end
  if not self._layout then return end

  local L = self._layout

  local contentW = L.content:GetWidth() or 0
  local contentH = L.content:GetHeight() or 0

  -- During live corner-grip resizing, anchored children can lag behind the parent
  -- frame by a frame. Derive the content size from the actual help-frame size so
  -- the Overview cards reflow immediately instead of keeping stale 4-wide anchors.
  do
    local fw = tonumber(liveFrameW)
    local fh = tonumber(liveFrameH)
    if (not fw or not fh) and self.frame and self.frame.GetSize then
      fw, fh = self.frame:GetSize()
    end

    local sideW = 190
    if L.sidebar and L.sidebar.GetWidth then
      sideW = tonumber(L.sidebar:GetWidth()) or sideW
    end

    local liveContentW = fw and math.floor((fw - 6 - sideW - 8 - 6) + 0.5) or nil
    local liveContentH = fh and math.floor((fh - 6 - 34 - 6 - 6) + 0.5) or nil

    if liveContentW and liveContentW > 1 then contentW = liveContentW end
    if liveContentH and liveContentH > 1 then contentH = liveContentH end
  end

  if contentW <= 1 or contentH <= 1 then return end

  local pad = 18
  local gap = 12

  -- Right rail width is fixed-ish, but clamps for small screens / UI scale.
  local rightW = Clamp(math.floor(contentW * 0.30), 260, 320)
  local leftW  = contentW - (pad * 2) - rightW - gap
  if leftW < 520 then
    leftW = 520
    rightW = contentW - (pad * 2) - leftW - gap
    rightW = Clamp(rightW, 260, 320)
  end

  -- ---- Overview page
  if L.ov then
    local ov = L.ov

    -- Cards: 4-wide if we can, otherwise 2x2.
    local cols = 4
    local minCardW = 210
    local cardGap = 10
    local available = contentW - (pad * 2)

    if available < (minCardW * 4 + cardGap * 3) then
      cols = 2
    end

    local cardW = math.floor((available - (cardGap * (cols - 1))) / cols)
    local cardH = 64
    local rows = math.ceil(4 / cols)

    for i = 1, 4 do
      local c = ov.cards[i]
      c:ClearAllPoints()

      local r = math.floor((i - 1) / cols)
      local col = (i - 1) % cols

      local x = pad + (col * (cardW + cardGap))
      local y = -52 - (r * (cardH + 10))

      c:SetSize(cardW, cardH)
      c:SetPoint("TOPLEFT", ov.page, "TOPLEFT", x, y)
    end

    local cardsBlockH = (rows * cardH) + ((rows - 1) * 10)

    -- Left/Right panels under cards
    ov.left:ClearAllPoints()
    ov.right:ClearAllPoints()

    ov.right:SetPoint("TOPRIGHT", ov.page, "TOPRIGHT", -pad, -52 - cardsBlockH - 12)
    ov.right:SetPoint("BOTTOMRIGHT", ov.page, "BOTTOMRIGHT", -pad, pad)
    ov.right:SetWidth(rightW)

    ov.left:SetPoint("TOPLEFT", ov.page, "TOPLEFT", pad, -52 - cardsBlockH - 12)
    ov.left:SetPoint("BOTTOMLEFT", ov.page, "BOTTOMLEFT", pad, pad)
    ov.left:SetPoint("RIGHT", ov.right, "LEFT", -gap, 0)

    -- Random meta text width (prefer the inner scroll width if present)
    local wrapW = rightW - 24
    if ov.rightScroll then
      local sw = ov.rightScroll:GetWidth() or 0
      if sw > 1 then wrapW = sw - 24 end
    end
	
	

    ov.rMeta:SetWidth(wrapW)
    ov.tList:SetWidth(wrapW)
    if ov.tokPrice then ov.tokPrice:SetWidth(wrapW) end
    if ov.tokUpdated then ov.tokUpdated:SetWidth(wrapW) end
    if ov.tokGoal then ov.tokGoal:SetWidth(wrapW) end
    if ov.tokRemain then ov.tokRemain:SetWidth(wrapW) end

    if self._UpdateOverviewRightScroll then
      self:_UpdateOverviewRightScroll()
    end
  end

  
-- ---- Farms page
  if L.farms then
    local p = L.farms
    local farmsRightW = Clamp(rightW + 48, 320, 380)

    p.right:ClearAllPoints()
    p.listPanel:ClearAllPoints()

    p.right:SetPoint("TOPRIGHT", p.page, "TOPRIGHT", -pad, -52)
    p.right:SetPoint("BOTTOMRIGHT", p.page, "BOTTOMRIGHT", -pad, pad)
    p.right:SetWidth(farmsRightW)

    -- Top row: Search + Reset + Resync
    if p.bResync then
      p.bResync:ClearAllPoints()
      p.bResync:SetPoint("TOPRIGHT", p.right, "TOPLEFT", -gap, -52)
    end

    if p.bReset then
      p.bReset:ClearAllPoints()
      if p.bResync then
        p.bReset:SetPoint("RIGHT", p.bResync, "LEFT", -8, 0)
        p.bReset:SetPoint("TOP", p.bResync, "TOP", 0, 0)
      else
        p.bReset:SetPoint("TOPRIGHT", p.right, "TOPLEFT", -gap, -52)
      end
    end

    p.search:ClearAllPoints()
    p.search:SetPoint("TOPLEFT", p.page, "TOPLEFT", pad, -52)
    if p.bReset then
      p.search:SetPoint("RIGHT", p.bReset, "LEFT", -10, 0)
    elseif p.bResync then
      p.search:SetPoint("RIGHT", p.bResync, "LEFT", -10, 0)
    else
      p.search:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)
    end

    if p.tResync then
      p.tResync:ClearAllPoints()
      p.tResync:SetPoint("TOPLEFT", p.search, "BOTTOMLEFT", 2, -6)
      p.tResync:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)
    end

    if p.filterBar then
      p.filterBar:ClearAllPoints()
      p.filterBar:SetPoint("TOPLEFT", p.search, "BOTTOMLEFT", 0, -26)
      p.filterBar:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)

      -- Responsive filter layout (checkboxes live on the Group row)
      if p.fExp and p.fType and p.fGroup and p.ckZone and p.ckArchived then
        local barW = p.filterBar:GetWidth() or leftW
        local g2 = 12

        local topW = math.floor((barW - g2) / 2)
        topW = Clamp(topW, 160, 280)

        local zoneLabelW = 120
        if p.ckZone.Text and p.ckZone.Text.GetStringWidth then
          zoneLabelW = math.floor(p.ckZone.Text:GetStringWidth() or zoneLabelW)
          if zoneLabelW < 60 then zoneLabelW = 60 end
        end

        local archLabelW = 100
        if p.ckArchived.Text and p.ckArchived.Text.GetStringWidth then
          archLabelW = math.floor(p.ckArchived.Text:GetStringWidth() or archLabelW)
          if archLabelW < 60 then archLabelW = 60 end
        end

        -- Rough checkbox footprint: box + label + gaps
        local needW = 16 + (24 + zoneLabelW) + 18 + (24 + archLabelW)
        local groupW = barW - needW
        groupW = Clamp(groupW, 160, 280)

        p.fExp:SetWidth(topW);  if p.fExp._btn  then p.fExp._btn:SetWidth(topW) end
        p.fType:SetWidth(topW); if p.fType._btn then p.fType._btn:SetWidth(topW) end
        p.fGroup:SetWidth(groupW); if p.fGroup._btn then p.fGroup._btn:SetWidth(groupW) end
        if p.fSort then p.fSort:SetWidth(1) end

        p.fExp:ClearAllPoints()
        p.fType:ClearAllPoints()
        p.fGroup:ClearAllPoints()
        p.ckZone:ClearAllPoints()
        p.ckArchived:ClearAllPoints()
        if p.fSort then p.fSort:ClearAllPoints() end

        p.fExp:SetPoint("TOPLEFT", p.filterBar, "TOPLEFT", 0, 0)
        p.fType:SetPoint("LEFT", p.fExp, "RIGHT", g2, 0)

        p.fGroup:SetPoint("TOPLEFT", p.fExp, "BOTTOMLEFT", 0, -12)
        if p.fSort then
          p.fSort:SetPoint("LEFT", p.fGroup, "RIGHT", g2, 0)
        end

        local groupAnchor = (p.fGroup and p.fGroup._btn) or p.fGroup
        p.ckZone:SetPoint("TOPLEFT", groupAnchor, "TOPRIGHT", 16, -4)

        if p.ckZone.Text then
          p.ckArchived:SetPoint("LEFT", p.ckZone.Text, "RIGHT", 18, 0)
          p.ckArchived:SetPoint("TOP", p.ckZone, "TOP", 0, 0)
        else
          p.ckArchived:SetPoint("LEFT", p.ckZone, "RIGHT", 18, 0)
        end
      end
    end

    p.listPanel:SetPoint("TOPLEFT", (p.filterBar or p.search), "BOTTOMLEFT", 0, -10)
    p.listPanel:SetPoint("BOTTOMLEFT", p.page, "BOTTOMLEFT", pad, pad)
    p.listPanel:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)

    -- right-panel wrapping widths
    if p.hlText then p.hlText:SetWidth(farmsRightW - 24) end
    if p.tagText then p.tagText:SetWidth(farmsRightW - 24) end
    if p.noteText then p.noteText:SetWidth(farmsRightW - 24) end
  end

-- ---- History page
  if L.history then
    local p = L.history
    p.right:ClearAllPoints()
    p.left:ClearAllPoints()

    p.right:SetPoint("TOPRIGHT", p.page, "TOPRIGHT", -pad, -52)
    p.right:SetPoint("BOTTOMRIGHT", p.page, "BOTTOMRIGHT", -pad, pad)
    p.right:SetWidth(rightW)

    p.left:SetPoint("TOPLEFT", p.page, "TOPLEFT", pad, -52)
    p.left:SetPoint("BOTTOMLEFT", p.page, "BOTTOMLEFT", pad, pad)
    p.left:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)

    p.meta:SetWidth(rightW - 24)
  end

  -- ---- Notepad page
  if L.notepad then
    local p = L.notepad
    p.right:ClearAllPoints()
    p.left:ClearAllPoints()

    p.right:SetPoint("TOPRIGHT", p.page, "TOPRIGHT", -pad, -52)
    p.right:SetPoint("BOTTOMRIGHT", p.page, "BOTTOMRIGHT", -pad, pad)
    p.right:SetWidth(rightW)

    p.left:SetPoint("TOPLEFT", p.page, "TOPLEFT", pad, -52)
    p.left:SetPoint("BOTTOMLEFT", p.page, "BOTTOMLEFT", pad, pad)
    p.left:SetPoint("RIGHT", p.right, "LEFT", -gap, 0)

    local innerW = p.left:GetWidth() or leftW
    local listW = Clamp(math.floor(innerW * 0.30), 220, 300)

    if p.listPanel then
      p.listPanel:ClearAllPoints()
      p.listPanel:SetPoint("TOPLEFT", p.left, "TOPLEFT", 12, -78)
      p.listPanel:SetPoint("BOTTOMLEFT", p.left, "BOTTOMLEFT", 12, 12)
      p.listPanel:SetWidth(listW)
    end

    if p.editorPanel and p.listPanel then
      p.editorPanel:ClearAllPoints()
      p.editorPanel:SetPoint("TOPLEFT", p.listPanel, "TOPRIGHT", 12, 0)
      p.editorPanel:SetPoint("BOTTOMRIGHT", p.left, "BOTTOMRIGHT", -12, 12)
    end

    if p.listContent and p.listScroll then
      p.listContent:SetWidth(p.listScroll:GetWidth() or 1)
    end

    local function FitScrollEdit(scroll, edit)
      if not scroll or not edit then return end
      local w = scroll:GetWidth() or 0
      local h = scroll:GetHeight() or 0
      if w <= 8 or h <= 8 then return end

      edit:SetWidth(w)

      local sh = 0
      if edit.GetStringHeight then sh = edit:GetStringHeight() or 0 end
      if sh < h then sh = h end
      edit:SetHeight(sh + 20)

      if scroll.UpdateScrollChildRect then scroll:UpdateScrollChildRect() end
    end

    FitScrollEdit(p.bodyScroll, p.bodyEdit)
    FitScrollEdit(p.exportScroll, p.exportEdit)
    FitScrollEdit(p.importScroll, p.importEdit)
  end


  -- ---- Settings page
  if L.settings then
    local p = L.settings

    if p.panel then
      p.panel:ClearAllPoints()
      p.panel:SetPoint("TOPLEFT", p.page, "TOPLEFT", pad, -52)
      p.panel:SetPoint("BOTTOMRIGHT", p.page, "BOTTOMRIGHT", -pad, pad)
    end

    if p.scroll and p.content then
      local w = (p.scroll:GetWidth() or 0)
      if w < 50 and p.panel then
        w = (p.panel:GetWidth() or 1) - 40
      end
      if w < 1 then w = 1 end

      p.content:SetWidth(w)
      if p.scroll.UpdateScrollChildRect then
        p.scroll:UpdateScrollChildRect()
      end
    end
  end
end

-- -------------------------------------------------------
-- Create / Pages
-- -------------------------------------------------------
function UI:Create()
  if self.frame then return end
  EnsureSettingsDefaults()

  -- Bigger by default (fixes your screenshot immediately).
  local initW, initH = 1180, 720
  local minW,  minH  = 1040, 640
  local maxW,  maxH  = 2200, 1400

  local f = CreateFrame("Frame", "WhatFarmHelpWindow", UIParent, "BackdropTemplate")

  -- Allow ESC to close this window (standard WoW behavior)
  do
    local n = f and f.GetName and f:GetName()
    if n and UISpecialFrames then
      local exists = false
      for j = 1, #UISpecialFrames do
        if UISpecialFrames[j] == n then exists = true break end
      end
      if not exists then tinsert(UISpecialFrames, n) end
    end
  end
  f:SetSize(initW, initH)

  -- Persisted position/size (so switching tabs doesn't snap back)
  local geom = nil
  if WF and WF.DB and WF.DB.GetSetting then
    geom = WF.DB:GetSetting("helpFrame")
  end
  if type(geom) == "table" and geom.point then
    f:ClearAllPoints()
    f:SetPoint(geom.point, UIParent, geom.relPoint or geom.point, geom.x or 0, geom.y or 0)
    if geom.w and geom.h and f.SetSize then
      f:SetSize(geom.w, geom.h)
    end
    f._wfHasSavedGeom = true
  else
    f:SetPoint("CENTER")
  end

  -- Apply user scale (keeps layout math stable, avoids per-widget scaling hacks)
  do
    local s = 1.0
    if WF.DB and WF.DB.GetSetting then
      s = tonumber(WF.DB:GetSetting("uiScale")) or 1.0
    end
    if f.SetScale then f:SetScale(s) end
  end
  f:SetMovable(true)
  f:EnableMouse(true)
  f:SetClampedToScreen(true)

  if f.SetResizable then f:SetResizable(true) end
  if f.SetResizeBounds then
    f:SetResizeBounds(minW, minH, maxW, maxH)
  else
    if f.SetMinResize then f:SetMinResize(minW, minH) end
    if f.SetMaxResize then f:SetMaxResize(maxW, maxH) end
  end

  f:RegisterForDrag("LeftButton")

  local function SaveHelpGeom()
    if not WF or not WF.DB or not WF.DB.SetSetting then return end
    local point, relTo, relPoint, x, y = f:GetPoint(1)
    if not point then return end
    WF.DB:SetSetting("helpFrame", {
      point    = point,
      relPoint = relPoint or point,
      x        = math.floor((x or 0) + 0.5),
      y        = math.floor((y or 0) + 0.5),
      w        = math.floor((f:GetWidth() or 0) + 0.5),
      h        = math.floor((f:GetHeight() or 0) + 0.5),
    })
    f._wfHasSavedGeom = true
  end

  f:SetScript("OnDragStart", function(self)
    if WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("uiLocked_help") then return end
    self:StartMoving()
  end)
  f:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    SaveHelpGeom()
  end)

  f:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 14,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
  })
  f:SetBackdropColor(C4(BG, BG))
  f:SetBackdropBorderColor(C4(BORDER, BORDER))

  -- Resize grip (drag = resize, click = lock)
  local grip = nil
  if Widgets and Widgets.AttachCornerLockGrip then
    grip = Widgets:AttachCornerLockGrip(f, {
      lockKey = "uiLocked_help",
      size = 18,
      x = -6,
      y = 6,
      alpha = 0.60,
      resizeText = "FRAME BEING RESIZED",
      onResizeUpdate = function(frame, w, h)
        UI:_Layout(w, h)
      end,
      onResizeStop = function(frame, w, h)
        SaveHelpGeom()
        UI:_Layout(w, h)
        if Widgets and Widgets.RefreshPixelPerfect then
          Widgets:RefreshPixelPerfect(frame)
        end
      end,
    })
  end

  local header = Widgets:Header(f, "WHAT FARM?", "A Smarter Way to  Farm")
  header:SetPoint("TOPLEFT", 6, -6)
  header:SetPoint("TOPRIGHT", -6, -6)
  header:SetHeight(34)

  header.close:SetScript("OnClick", function()
    UI:CloseDropdown()
    f:Hide()
  end)

  -- Tutorial overlay toggle (modal on top of whatever tab is currently visible)
  local bTutorial = Widgets:Button(header, "Tutorial", 86, 22)
  bTutorial:SetPoint("RIGHT", header.close, "LEFT", -8, 0)
  bTutorial:SetScript("OnClick", function()
    if WF and WF.UI and WF.UI.TutorialOverlay and WF.UI.TutorialOverlay.Toggle then
      WF.UI.TutorialOverlay:Toggle(f, header, UI, UI._tutorialCtx)
    end
  end)
  header.tutorial = bTutorial

  local sidebar = MakePanel(f, PANEL)
  sidebar:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 6, -6)
  sidebar:SetPoint("BOTTOMLEFT", 6, 6)
  sidebar:SetWidth(190)

  local content = MakePanel(f, BG)
  content:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 8, 0)
  content:SetPoint("BOTTOMRIGHT", -6, 6)
  content:SetBackdropColor(C4(BG, BG))
  -- Keep page content from visually spilling outside the help window during live resize.
  if content.SetClipsChildren then content:SetClipsChildren(true) end

  local pages = {}

  local function MakePage(key, title)
    local p = CreateFrame("Frame", nil, content)
    p:SetAllPoints(content)
    if p.SetClipsChildren then p:SetClipsChildren(true) end
    p:Hide()

    local t = p:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
    t:SetPoint("TOPLEFT", 18, -16)
    t:SetTextColor(C4(TEXT, TEXT))
    t:SetText(title)
    p._titleText = t

    pages[key] = p
    return p
  end

  local function MakeNavButton(text, key, y)
    local b = Widgets:Button(sidebar, text, 166, 26)
    b:SetPoint("TOPLEFT", 12, y)
    b:SetScript("OnClick", function() UI:Show(key) end)
    return b
  end

  -- Layout registry
  self._layout = { content = content, header = header, sidebar = sidebar }

    -- -----------------------------------------------------
  -- PAGES (split into UI/HelpPages/*)
  -- -----------------------------------------------------
  do
    local Pages = (WF.UI and WF.UI.HelpPages) or nil
    local ctx = {
      UI = UI,
      WF = WF,
      Widgets = Widgets,
      Theme = Theme,
      Util = Util,
      -- format helpers
      Money = Money,
      FormatTime = FormatTime,
      SessionGPH = SessionGPH,
      SessionLIV = SessionLIV,

      -- history aggregates helper (used by Farms/History pages)
      RebuildHistoryAggregates = RebuildHistoryAggregates,


      -- theme/colors
      C4 = C4,
      ACCENT = ACCENT,
      TEXT = TEXT,
      MUTED = MUTED,
      BG = BG,
      PANEL = PANEL,
      BORDER = BORDER,

      -- shared helpers
      Money = Money,
      Clamp = Clamp,
      EnsureSettingsDefaults = EnsureSettingsDefaults,
      SafeNow = SafeNow,
      FormatTime = FormatTime,
      SetWaypoint = SetWaypoint,

      -- constructors
      MakePage = MakePage,
      MakePanel = MakePanel,
      MakeCard = MakeCard,
      MakeSectionTitle = MakeSectionTitle,
      MakeMutedText = MakeMutedText,
      MakeBodyText = MakeBodyText,
      MakeDivider = MakeDivider,
      MakeHeaderText = MakeHeaderText,
      MakeInput = MakeInput,
      MakeCombo = MakeCombo,
      MakeCheck = MakeCheck,

      -- lists
      EXPANSIONS = EXPANSIONS,
      FARM_TYPES = FARM_TYPES,
      FARM_AIMS = FARM_AIMS,
      RANDOM_MODES = RANDOM_MODES,
      PRICE_SOURCES = PRICE_SOURCES,
      TSM_LABELS = TSM_LABELS,
      TSMStringToLabel = TSMStringToLabel,
      TSM_BY_LABEL = TSM_BY_LABEL,
      TSM_BY_STR = TSM_BY_STR,
    }

    -- Keep this context for the tutorial overlay (so it can reuse the same combo/checkbox widgets).
    UI._tutorialCtx = ctx

    if Pages and Pages.Overview then Pages.Overview(ctx) end
    if Pages and Pages.Farms then Pages.Farms(ctx) end
    if Pages and Pages.History then Pages.History(ctx) end
    if Pages and Pages.Notepad then Pages.Notepad(ctx) end
  end

-- -----------------------------------------------------
  -- SETTINGS
  -- -----------------------------------------------------
  local pSettings = MakePage("Settings", "Settings")
  do
    local panel = MakePanel(pSettings, PANEL)

    -- Settings internal tabs (premium): General | Profiles
    local tabBar = CreateFrame("Frame", nil, panel)
    tabBar:SetPoint("TOPLEFT", 6, -8)
    tabBar:SetPoint("TOPRIGHT", -6, -8)
    tabBar:SetHeight(30)

    local tabHost = CreateFrame("Frame", nil, panel)
    tabHost:SetPoint("TOPLEFT", 6, -44)
    tabHost:SetPoint("BOTTOMRIGHT", -6, 8)

    local tabGeneral = CreateFrame("Frame", nil, tabHost)
    tabGeneral:SetAllPoints(tabHost)

    local tabProfiles = CreateFrame("Frame", nil, tabHost)
    tabProfiles:SetAllPoints(tabHost)
    tabProfiles:Hide()

    local function StyleTabButton(b, isActive)
      if not b then return end
      if isActive then
        local r = math.min(1, (ACCENT[1] or 1) * 1.06)
        local g = math.min(1, (ACCENT[2] or 1) * 1.06)
        local bl = math.min(1, (ACCENT[3] or 1) * 1.10)
        local a = tonumber(ACCENT[4]) or 1
        b:SetBackdropBorderColor(r, g, bl, a)
      else
        b:SetBackdropBorderColor(C4(BORDER, BORDER))
      end
    end

    local activeSettingsTab = "General"
    local bTabGeneral = Widgets:Button(tabBar, "General", 110, 24)
    bTabGeneral:SetPoint("LEFT", 0, 0)

    local bTabProfiles = Widgets:Button(tabBar, "Profiles", 110, 24)
    bTabProfiles:SetPoint("LEFT", bTabGeneral, "RIGHT", 10, 0)

    local activeProfileInfo = CreateFrame("Frame", nil, pSettings)
    if pSettings._titleText then
      activeProfileInfo:SetPoint("LEFT", pSettings._titleText, "RIGHT", 8, -1)
    else
      activeProfileInfo:SetPoint("TOPLEFT", 74, -19)
    end
    activeProfileInfo:SetHeight(14)
    activeProfileInfo:SetWidth(220)

    local activeProfileLabel = MakeMutedText(activeProfileInfo, "Profile:")
    activeProfileLabel:SetFontObject("WF_GameFontDisableSmall")
    activeProfileLabel:SetPoint("LEFT", activeProfileInfo, "LEFT", 0, -1)

    local activeProfileValue = MakeBodyText(activeProfileInfo, "—")
    activeProfileValue:SetFontObject("WF_GameFontDisableSmall")
    activeProfileValue:SetPoint("LEFT", activeProfileLabel, "RIGHT", 4, 0)
    activeProfileValue:SetJustifyH("LEFT")
    activeProfileValue:SetTextColor(C4(MUTED, MUTED))

    local function GetActiveProfileBadgeText()
      local name = "Default"
      if WF and WF.DB and WF.DB.GetActiveProfileName then
        name = WF.DB:GetActiveProfileName() or "Default"
      end
      return DisplayProfileName(name or "Default")
    end

    local function RefreshActiveProfileBadge(force)
      local displayName = GetActiveProfileBadgeText()
      if not force and activeProfileInfo._wfActiveProfileText == displayName then
        return
      end
      activeProfileInfo._wfActiveProfileText = displayName
      activeProfileValue:SetText(displayName)
      if string.lower(tostring(displayName or "")) == "mats" then
        activeProfileValue:SetTextColor(C4(ACCENT, ACCENT))
      else
        activeProfileValue:SetTextColor(C4(MUTED, MUTED))
      end
    end

    activeProfileInfo:SetScript("OnUpdate", function(self, elapsed)
      self._wfBadgeElapsed = (self._wfBadgeElapsed or 0) + (tonumber(elapsed) or 0)
      if self._wfBadgeElapsed < 0.15 then return end
      self._wfBadgeElapsed = 0

      RefreshActiveProfileBadge()
    end)

    function UI:RefreshSettingsProfileBadge()
      RefreshActiveProfileBadge(true)
    end

    local function SyncSettingsTutorialLayouts()
      if not self or not self._layout then return end

      self._layout.settings = self._layout.settings or {}
      self._layout.settings.general = {
        page = pSettings,
        panel = panel,
        tab = tabGeneral,
        tabKey = "General",
      }

      local profilesLayout = tabProfiles and tabProfiles._wfTutorialLayout
      if type(profilesLayout) ~= "table" then
        profilesLayout = {
          page = pSettings,
          panel = tabProfiles,
          tab = tabProfiles,
          tabKey = "Profiles",
        }
      else
        profilesLayout.page = profilesLayout.page or pSettings
        profilesLayout.panel = profilesLayout.panel or tabProfiles
        profilesLayout.tab = profilesLayout.tab or tabProfiles
        profilesLayout.tabKey = "Profiles"
      end

      self._layout.settings.profiles = profilesLayout
      self._layout.profiles = profilesLayout
    end

    local function ShowSettingsTab(keyOrOwner, maybeKey)
      local key = keyOrOwner
      if type(keyOrOwner) == "table" then
        key = maybeKey
      end
      key = tostring(key or "")
      if key ~= "Profiles" then key = "General" end
      activeSettingsTab = key

      tabGeneral:SetShown(key == "General")
      tabProfiles:SetShown(key == "Profiles")

      StyleTabButton(bTabGeneral, key == "General")
      StyleTabButton(bTabProfiles, key == "Profiles")

      SyncSettingsTutorialLayouts()
      RefreshActiveProfileBadge()
      activeProfileInfo:Show()

      -- Ensure Profiles panel paints when opened.
      if key == "Profiles" and tabProfiles._wf_profiles_refresh then
        tabProfiles._wf_profiles_refresh()
        SyncSettingsTutorialLayouts()
      end

      UI:CloseDropdown()
      UI:_Layout()
    end

    bTabGeneral:SetScript("OnClick", function() ShowSettingsTab("General") end)
    bTabProfiles:SetScript("OnClick", function() ShowSettingsTab("Profiles") end)

    -- General settings scroll
    local scroll = CreateFrame("ScrollFrame", nil, tabGeneral, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
    scroll:SetPoint("TOPLEFT", 0, 0)
    scroll:SetPoint("BOTTOMRIGHT", -22, 0)

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

    -- Profiles tab content is built in a separate file for cleanliness.
    if WF.UI and WF.UI.HelpPages and WF.UI.HelpPages.SettingsProfiles then
      WF.UI.HelpPages.SettingsProfiles({
        UI = UI,
        WF = WF,
        Widgets = Widgets,
        Theme = Theme,
        C4 = C4,
        ACCENT = ACCENT,
        TEXT = TEXT,
        MUTED = MUTED,
        BG = BG,
        PANEL = PANEL,
        BORDER = BORDER,
        MakePanel = MakePanel,
        MakeDivider = MakeDivider,
        MakeSectionTitle = MakeSectionTitle,
        MakeHeaderText = MakeHeaderText,
        MakeBodyText = MakeBodyText,
        MakeMutedText = MakeMutedText,
        MakeInput = MakeInput,
        Money = Money,
        Clamp = Clamp,
      }, tabProfiles)
    end

    local xPad = 12
    -- ---------------------------------------------------------
    -- General tab: Settings (clean + collapsible + tooltips)
    -- ---------------------------------------------------------
    local xPad = 12
    local GUTTER = 24

    local function Keyify(s)
      s = tostring(s or "")
      s = s:lower():gsub("[^%w]+", "_"):gsub("^_+", ""):gsub("_+$", "")
      if s == "" then s = "section" end
      return s
    end

    local function CollapseKey(title)
      return "ui_settingsCollapse_" .. Keyify(title)
    end

    local function IsCollapsed(title)
      return WF.DB:GetSetting(CollapseKey(title)) and true or false
    end

    local function SetCollapsed(title, v)
      WF.DB:SetSetting(CollapseKey(title), v and true or false)
    end

    local function HookTooltip(frame, title, body)
      if not frame or frame._wf_tooltip then return end
      frame._wf_tooltip = true

      -- Some templates disable mouse; we need hover on wrappers.
      if frame.EnableMouse then frame:EnableMouse(true) end

      if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
      local tooltipPalette = (Widgets and Widgets.GetTooltipPalette and Widgets:GetTooltipPalette()) or nil
      local TOOLTIP_TITLE = tooltipPalette and tooltipPalette.title or { TEXT[1], TEXT[2], TEXT[3] }
      local TOOLTIP_BODY = tooltipPalette and tooltipPalette.body or { 0.86, 0.86, 0.86, 1.00 }
      local TOOLTIP_MUTED = tooltipPalette and tooltipPalette.muted or { MUTED[1], MUTED[2], MUTED[3] }

      local function AddTooltipTextLine(text, style)
        text = tostring(text or "")
        if text == "" then
          GameTooltip:AddLine(" ", 1, 1, 1)
          return
        end

        if style == "accent" or style == "blue" or style == "title" or style == "header" then
          GameTooltip:AddLine(text, TOOLTIP_TITLE[1], TOOLTIP_TITLE[2], TOOLTIP_TITLE[3], true)
        elseif style == "muted" or style == "gray" or style == "grey" or style == "hint" then
          GameTooltip:AddLine(text, TOOLTIP_MUTED[1], TOOLTIP_MUTED[2], TOOLTIP_MUTED[3], true)
        else
          GameTooltip:AddLine(text, TOOLTIP_BODY[1], TOOLTIP_BODY[2], TOOLTIP_BODY[3], true)
        end
      end

      local function AddStyledTooltipLine(line, lineIndex, totalLines)
        if line == nil then return end

        local text = nil
        local style = nil

        if type(line) == "table" then
          text = tostring(line.text or line[1] or "")
          style = tostring(line.style or line.color or "")
        else
          text = tostring(line)
        end

        if not style or style == "" then
          local lower = text:lower()
          if lower:match("^examples?:") or lower:match("^tip:") or lower:match("^note:") or lower:match("^current:") or lower:match("^currently:") then
            style = "muted"
          elseif lower:find("profiles > edit rules", 1, true) then
            style = "muted"
          elseif totalLines and totalLines > 1 and lineIndex and lineIndex > 1 then
            style = "muted"
          else
            style = "text"
          end
        end

        local segments = {}
        local start = 1
        while true do
          local newlineStart, newlineEnd = text:find("\n", start, true)
          if newlineStart then
            segments[#segments + 1] = text:sub(start, newlineStart - 1)
            start = newlineEnd + 1
          else
            segments[#segments + 1] = text:sub(start)
            break
          end
        end

        for segmentIndex = 1, #segments do
          local segmentStyle = style
          if (not style or style == "" or style == "text") and segmentIndex > 1 then
            segmentStyle = "muted"
          end
          AddTooltipTextLine(segments[segmentIndex], segmentStyle)
        end
      end

      frame:HookScript("OnEnter", function(self)
        if not GameTooltip or (not title and not body) then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:ClearLines()

        if title and title ~= "" then
          GameTooltip:AddLine(title, TOOLTIP_TITLE[1], TOOLTIP_TITLE[2], TOOLTIP_TITLE[3], true)
        end

        if type(body) == "table" then
          local totalLines = #body
          for i = 1, totalLines do
            AddStyledTooltipLine(body[i], i, totalLines)
          end
        elseif body and body ~= "" then
          AddStyledTooltipLine(body, 1, 1)
        end

        GameTooltip:Show()
      end)

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

    local function Tip(frame, title, body)
      if not frame then return end

      -- Tooltips should only be attached to *interactive controls* (not wrappers / muted label text).
      -- Wrappers expose their control as _edit (EditBox), _btn (dropdown button), _check (CheckButton), or _slider.
      local target = frame._edit or frame._btn or frame._check or frame._slider

      if not target and frame.GetObjectType then
        local ot = frame:GetObjectType()
        if ot == "Button" or ot == "CheckButton" or ot == "EditBox" or ot == "Slider" then
          target = frame
        end
      end

      if not target then return end
      HookTooltip(target, title, body)
    end

    local function CalcColW()
      local w = 0
      if scroll and scroll.GetWidth then w = scroll:GetWidth() or 0 end
      if w < 100 and panel and panel.GetWidth then w = panel:GetWidth() or 0 end
      if w < 100 then w = 900 end

      -- Subtract padding + scrollbar gutter.
      local avail = w - (xPad * 2) - 22
      local col = math.floor((avail - GUTTER) / 2)

      if col < 240 then col = 240 end
      if col > 360 then col = 360 end
      return col
    end

    local function FullW(colW)
      return (colW * 2) + GUTTER
    end

    local function SetWrapWidth(w, width)
      if not w or not width then return end
      if w.SetWidth then w:SetWidth(width) end

      -- For combo-style wrappers, stretch the button. For checkbox rows, keep the check box fixed.
      if w._btn and w._btn.SetWidth and not w._isCheckRow then
        w._btn:SetWidth(width)
      end

      -- Stretch visible text region where appropriate.
      if w._text and w._text.SetWidth then
        w._text:SetWidth(math.max(1, width - 34))
      end
    end

    local function SetFullWidth(w, width)
      if not w or not width then return end
      if w.SetWidth then w:SetWidth(width) end
      if w._btn and w._btn.SetWidth then w._btn:SetWidth(width) end
    end

    local function RowHeight(a, b, minH)
      local h1 = 0
      local h2 = 0
      if a and a.IsShown and not a:IsShown() then a = nil end
      if b and b.IsShown and not b:IsShown() then b = nil end
      if a and a.GetHeight then h1 = a:GetHeight() or 0 end
      if b and b.GetHeight then h2 = b:GetHeight() or 0 end
      local h = math.max(h1, h2)
      if h < (minH or 24) then h = (minH or 24) end
      return h
    end

    local function MakeCheckW(parent, label, getFn, setFn)
      -- Wrapper row: keeps checkbox textures fixed while allowing a full-width clickable label.
      local row = CreateFrame("Frame", nil, parent)
      row:SetSize(240, 24)
      row._isCheckRow = true

      local b = CreateFrame("CheckButton", nil, row, "ChatConfigCheckButtonTemplate")

  WF_ApplyBlueCheck(b)
      b:SetPoint("TOPLEFT", 0, 0)
      b:SetSize(24, 24)
      if b.Text then
        b.Text:SetText("")
        b.Text:Hide()
      end

      local t = row:CreateFontString(nil, "ARTWORK", "WF_GameFontHighlight")
      t:SetText(label)
      t:SetTextColor(C4(TEXT, TEXT))
      t:SetPoint("LEFT", b, "RIGHT", 2, 0)
      t:SetPoint("RIGHT", row, "RIGHT", -2, 0)
      t:SetJustifyH("LEFT")

      local function Refresh()
        b:SetChecked(getFn() and true or false)
      end

      row:SetScript("OnMouseDown", function(_, mouseButton)
        if mouseButton ~= "LeftButton" then return end
        b:SetChecked(not b:GetChecked())
        setFn(b:GetChecked() and true or false)
        Refresh()
      end)

      b:SetScript("OnShow", Refresh)
      b:SetScript("OnClick", function()
        setFn(b:GetChecked() and true or false)
        Refresh()
      end)

      row.Refresh = Refresh
      row._check = b
      row._text = t
      return row
    end

    local SECTIONS = {}

    local function NewSection(title, tipBody)
      local sec = {}
      sec.title = title
      sec.rows = {}

      -- Invisible wrapper frame that encompasses the full section (header + content).
      -- Used by the tutorial spotlight so it can highlight the whole section at once.
      local wrap = CreateFrame("Frame", nil, sc)
      wrap:SetPoint("TOPLEFT", sc, "TOPLEFT", 0, 0)
      wrap:SetPoint("TOPRIGHT", sc, "TOPRIGHT", 0, 0)
      wrap:SetHeight(24)
      wrap:EnableMouse(false)
      sec.wrap = wrap

      local hdr = CreateFrame("Button", nil, sc)
      hdr:SetHeight(20)
      hdr:RegisterForClicks("LeftButtonUp")

      local bg = hdr:CreateTexture(nil, "BACKGROUND")
      bg:SetTexture("Interface/Buttons/WHITE8x8")
      bg:SetAllPoints()
      bg:SetAlpha(0.00)

      hdr:HookScript("OnEnter", function()
        bg:SetAlpha(0.06)
      end)
      hdr:HookScript("OnLeave", function()
        bg:SetAlpha(0.00)
      end)

      local arrow = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      arrow:SetPoint("LEFT", 0, 0)
      arrow:SetText("-")
      arrow:SetTextColor(C4(TEXT, TEXT))

      local t = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
      t:SetPoint("LEFT", arrow, "RIGHT", 6, 0)
      t:SetTextColor(C4(ACCENT, ACCENT))
      t:SetText(title or "")

      local d = MakeDivider(sc)

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

      sec.header = hdr
      sec.headerBg = bg
      sec.arrow = arrow
      sec.titleFS = t
      sec.divider = d
      sec.content = content
      sec.tip = tipBody

      hdr:SetScript("OnClick", function()
        local collapsed = IsCollapsed(title)
        SetCollapsed(title, not collapsed)
        if sc and sc._wf_reflow_settings then
          sc._wf_reflow_settings()
        end
      end)

      Tip(hdr, title, tipBody or { "Click to expand/collapse." })

      SECTIONS[#SECTIONS + 1] = sec
      return sec
    end

    local function AddPair(sec, left, right, gap)
      sec.rows[#sec.rows + 1] = { kind = "pair", left = left, right = right, gap = gap or 10 }
    end

    local function AddFull(sec, frame, gap)
      sec.rows[#sec.rows + 1] = { kind = "full", frame = frame, gap = gap or 10 }
    end

    local function ReflowSettingsGeneral()
      local colW = CalcColW()
      local fullW = FullW(colW)

      local y = -10

      for i = 1, #SECTIONS do
        local sec = SECTIONS[i]
        local collapsed = IsCollapsed(sec.title)

        local headerY = y
        local dividerY = y - 22
        if Widgets and Widgets.Snap then
          headerY = Widgets:Snap(sc, headerY)
          dividerY = Widgets:Snap(sc, dividerY)
        end

        -- header
        sec.header:ClearAllPoints()
        sec.header:SetPoint("TOPLEFT", sc, "TOPLEFT", xPad, headerY)
        sec.header:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -xPad, headerY)
        sec.header:SetHeight(20)

        -- sync wrap top to header top
        sec.wrap:ClearAllPoints()
        sec.wrap:SetPoint("TOPLEFT", sc, "TOPLEFT", xPad, headerY)
        sec.wrap:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -xPad, headerY)

        sec.arrow:SetText(collapsed and "+" or "-")

        -- divider
        sec.divider:ClearAllPoints()
        sec.divider:SetPoint("TOPLEFT", sc, "TOPLEFT", xPad, dividerY)
        sec.divider:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -xPad, dividerY)
        if sec.divider._wfSyncDividerGeometry then
          sec.divider:_wfSyncDividerGeometry()
        end

        y = y - 28

        -- content
        sec.content:ClearAllPoints()
        sec.content:SetPoint("TOPLEFT", sc, "TOPLEFT", xPad, y)
        sec.content:SetPoint("TOPRIGHT", sc, "TOPRIGHT", -xPad, y)

        if collapsed then
          sec.content:Hide()
          y = y - 10
          -- wrap covers header only
          sec.wrap:SetHeight(28)
        else
          sec.content:Show()

          local sy = 0

          for r = 1, #sec.rows do
            local row = sec.rows[r]

            if row.kind == "pair" then
              local L = row.left
              local R = row.right
              local gap = row.gap or 10

              if L and L.IsShown and not L:IsShown() then L = nil end
              if R and R.IsShown and not R:IsShown() then R = nil end

              if L then
                L:ClearAllPoints()
                L:SetParent(sec.content)
                L:SetPoint("TOPLEFT", sec.content, "TOPLEFT", 0, -sy)
                SetWrapWidth(L, colW)
              end

              if R then
                R:ClearAllPoints()
                R:SetParent(sec.content)
                R:SetPoint("TOPLEFT", sec.content, "TOPLEFT", colW + GUTTER, -sy)
                SetWrapWidth(R, colW)
              end

              local h = RowHeight(L, R, 24)
              sy = sy + h + gap

            elseif row.kind == "full" then
              local F = row.frame
              local gap = row.gap or 10

              if F and F.IsShown and not F:IsShown() then F = nil end
              if F then
                F:ClearAllPoints()
                F:SetParent(sec.content)
                F:SetPoint("TOPLEFT", sec.content, "TOPLEFT", 0, -sy)
                SetFullWidth(F, fullW)
                if F._wfRefreshLayout then
                  F:_wfRefreshLayout(fullW, colW)
                end

                local h = F:GetHeight() or 24
                if h < 24 then h = 24 end
                sy = sy + h + gap
              end
            end
          end

          sec.content:SetHeight(math.max(1, sy))
          y = y - sy - 10
          -- wrap covers header + content + bottom gap
          sec.wrap:SetHeight(28 + math.max(1, sy) + 10)
        end
      end

      sc:SetHeight((-y) + 20)
      sc:SetWidth(scroll:GetWidth() or 1)
      if scroll.UpdateScrollChildRect then scroll:UpdateScrollChildRect() end
      if Widgets and Widgets.RefreshPixelPerfect then
        Widgets:RefreshPixelPerfect(sc)
      end
    end

    sc._wf_reflow_settings = ReflowSettingsGeneral

    -- -----------------------------
    -- History
    -- -----------------------------
    local secHistory = NewSection("History", {
      "Controls how many sessions are kept and what gets stored.",
      "Use Prune or Clear to manage recorded runs.",
    })

    local keepWrap = CreateFrame("Frame", nil, secHistory.content)
    keepWrap:SetSize(240, 44)
    local keepLab = MakeMutedText(keepWrap, "Sessions to keep:")
    keepLab:SetPoint("TOPLEFT", 0, 0)

    local keepInput = MakeInput(keepWrap, 140, 24)
    keepInput:SetPoint("TOPLEFT", 0, -18)
    keepInput:SetText(tostring(WF.DB:GetSetting("historyKeep") or 100))
    keepInput:SetScript("OnEnterPressed", function(self)
      local n = tonumber(self:GetText() or "")
      n = n and math.floor(n)
      if not n or n < 10 then
        self:SetText(tostring(WF.DB:GetSetting("historyKeep") or 100))
        self:ClearFocus()
        return
      end
      WF.DB:SetSetting("historyKeep", n)
      self:SetText(tostring(n))
      self:ClearFocus()
    end)
    keepInput:SetScript("OnEscapePressed", function(self)
      self:SetText(tostring(WF.DB:GetSetting("historyKeep") or 100))
      self:ClearFocus()
    end)
    keepInput:SetScript("OnEditFocusLost", function(self)
      self:SetText(tostring(WF.DB:GetSetting("historyKeep") or 100))
    end)
    keepWrap._edit = keepInput
    Tip(keepWrap, "Sessions to keep", {
      "Maximum number of sessions stored in History.",
      "Oldest sessions are pruned when you exceed this value.",
    })

    local minDurWrap = CreateFrame("Frame", nil, secHistory.content)
    minDurWrap:SetSize(240, 44)
    local minDurLab = MakeMutedText(minDurWrap, "Min duration to save:")
    minDurLab:SetPoint("TOPLEFT", 0, 0)

    -- Split input: minutes + seconds (stored as total seconds in historyMinSeconds)
    local minsInput = MakeInput(minDurWrap, 56, 24)
    minsInput:SetPoint("TOPLEFT", 0, -18)

    local minsSuffix = MakeMutedText(minDurWrap, "min")
    minsSuffix:SetPoint("LEFT", minsInput, "RIGHT", 6, 0)

    local secsInput = MakeInput(minDurWrap, 56, 24)
    secsInput:SetPoint("LEFT", minsSuffix, "RIGHT", 10, 0)

    local secsSuffix = MakeMutedText(minDurWrap, "sec")
    secsSuffix:SetPoint("LEFT", secsInput, "RIGHT", 6, 0)

    local function _WFClampInt(v, lo, hi)
      v = tonumber(v)
      v = v and math.floor(v) or 0
      if v < lo then v = lo end
      if hi and v > hi then v = hi end
      return v
    end

    local function _WFSetMinDurTotal(total)
      total = _WFClampInt(total, 0, 86400)
      WF.DB:SetSetting("historyMinSeconds", total)

      local m = math.floor(total / 60)
      local s = total % 60
      minsInput:SetText(tostring(m))
      secsInput:SetText(tostring(s))
    end

    local function _WFReadMinDurTotal()
      local m = _WFClampInt(minsInput:GetText() or "", 0, 1440)
      local s = _WFClampInt(secsInput:GetText() or "", 0, 86400)
      _WFSetMinDurTotal((m * 60) + s)
    end

    local function _WFRefreshMinDur()
      local total = tonumber(WF.DB:GetSetting("historyMinSeconds")) or 60
      _WFSetMinDurTotal(total)
    end

    minsInput:SetScript("OnEnterPressed", function(self)
      _WFReadMinDurTotal()
      self:ClearFocus()
    end)
    secsInput:SetScript("OnEnterPressed", function(self)
      _WFReadMinDurTotal()
      self:ClearFocus()
    end)

    minsInput:SetScript("OnEscapePressed", function(self)
      _WFRefreshMinDur()
      self:ClearFocus()
    end)
    secsInput:SetScript("OnEscapePressed", function(self)
      _WFRefreshMinDur()
      self:ClearFocus()
    end)

    minsInput:SetScript("OnEditFocusLost", function() _WFRefreshMinDur() end)
    secsInput:SetScript("OnEditFocusLost", function() _WFRefreshMinDur() end)

    _WFRefreshMinDur()

    minDurWrap._edit = minsInput
    Tip(minDurWrap, "Minimum duration", {
      "Prevents very short runs from being saved to History.",
      { text = "Set minutes and seconds to 0 to disable the minimum.", style = "muted" },
    })

    local avgDurWrap = CreateFrame("Frame", nil, secHistory.content)
    avgDurWrap:SetSize(240, 44)
    local avgDurLab = MakeMutedText(avgDurWrap, "Min duration for averages:")
    avgDurLab:SetPoint("TOPLEFT", 0, 0)

    local avgMinsInput = MakeInput(avgDurWrap, 56, 24)
    avgMinsInput:SetPoint("TOPLEFT", 0, -18)

    local avgMinsSuffix = MakeMutedText(avgDurWrap, "min")
    avgMinsSuffix:SetPoint("LEFT", avgMinsInput, "RIGHT", 6, 0)

    local avgSecsInput = MakeInput(avgDurWrap, 56, 24)
    avgSecsInput:SetPoint("LEFT", avgMinsSuffix, "RIGHT", 10, 0)

    local avgSecsSuffix = MakeMutedText(avgDurWrap, "sec")
    avgSecsSuffix:SetPoint("LEFT", avgSecsInput, "RIGHT", 6, 0)

    local function RefreshAvgMinDur()
      local total = tonumber(WF.DB:GetSetting("historyAvgMinSeconds")) or 0
      total = _WFClampInt(total, 0, 86400)
      avgMinsInput:SetText(tostring(math.floor(total / 60)))
      avgSecsInput:SetText(tostring(total % 60))
    end

    local function CommitAvgMinDur()
      local m = _WFClampInt(avgMinsInput:GetText() or "", 0, 1440)
      local s = _WFClampInt(avgSecsInput:GetText() or "", 0, 86400)
      local total = _WFClampInt((m * 60) + s, 0, 86400)
      WF.DB:SetSetting("historyAvgMinSeconds", total)
      RefreshAvgMinDur()

      local db = WF.DB:Get() or {}
      RebuildHistoryAggregates(db)
      if UI and UI.MarkFarmAveragesDirty then UI:MarkFarmAveragesDirty() end
      if UI and UI.RefreshOverview then UI:RefreshOverview() end
      if UI and UI.RefreshHistory then UI:RefreshHistory() end
      if UI and UI.RefreshFarms then UI:RefreshFarms() end
    end

    avgMinsInput:SetScript("OnEnterPressed", function(self)
      CommitAvgMinDur()
      self:ClearFocus()
    end)
    avgSecsInput:SetScript("OnEnterPressed", function(self)
      CommitAvgMinDur()
      self:ClearFocus()
    end)

    avgMinsInput:SetScript("OnEscapePressed", function(self)
      RefreshAvgMinDur()
      self:ClearFocus()
    end)
    avgSecsInput:SetScript("OnEscapePressed", function(self)
      RefreshAvgMinDur()
      self:ClearFocus()
    end)

    avgMinsInput:SetScript("OnEditFocusLost", function() RefreshAvgMinDur() end)
    avgSecsInput:SetScript("OnEditFocusLost", function() RefreshAvgMinDur() end)

    RefreshAvgMinDur()

    avgDurWrap._edit = avgMinsInput
    Tip(avgDurWrap, "Minimum duration for averages", {
      "Only runs at or above this duration count toward farm average columns and tooltips.",
      "History still keeps shorter runs unless your save minimum blocks them.",
      { text = "Set minutes and seconds to 0 to disable the filter.", style = "muted" },
    })

    local minGPHWrap = CreateFrame("Frame", nil, secHistory.content)
    minGPHWrap:SetSize(240, 44)
    local minGPHLab = MakeMutedText(minGPHWrap, "Min GPH to save:")
    minGPHLab:SetPoint("TOPLEFT", 0, 0)

    local minGPHInput = MakeInput(minGPHWrap, 140, 24)
    minGPHInput:SetPoint("TOPLEFT", 0, -18)

    local function ParseGPHTextToCopper(txt)
      txt = tostring(txt or "")
      local t = txt:lower():gsub(",", ""):gsub("%s+", "")
      if t == "" then return 0 end

      t = t:gsub("/hr$", "")
      t = t:gsub("gph$", "")
      t = t:gsub("gold/hr$", "")
      t = t:gsub("gold$", "")
      t = t:gsub("g$", "")

      local mult = 1
      local suffix = t:sub(-1)
      if suffix == "k" then
        mult = 1000
        t = t:sub(1, -2)
      elseif suffix == "m" then
        mult = 1000000
        t = t:sub(1, -2)
      end

      local n = tonumber(t)
      if not n then return nil end
      if n < 0 then n = 0 end
      return math.floor((n * mult * 10000) + 0.5)
    end

    local function FormatGPHGoldText(copperPerHour)
      local gold = (tonumber(copperPerHour) or 0) / 10000
      local whole = math.floor(gold + 0.0000001)

      if math.abs(gold - whole) < 0.000001 then
        local out
        if BreakUpLargeNumbers then
          out = BreakUpLargeNumbers(whole)
        else
          out = tostring(whole):reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
        end
        return tostring(out or whole) .. "g"
      end

      local txt = string.format("%.2f", gold)
      txt = txt:gsub("(%..-)0+$", "%1")
      txt = txt:gsub("%.$", "")
      return txt .. "g"
    end

    local function RefreshMinGPH()
      local v = tonumber(WF.DB:GetSetting("historyMinGPH")) or 0
      if v < 0 then v = 0 end
      minGPHInput:SetText(FormatGPHGoldText(v))
    end

    RefreshMinGPH()

    minGPHInput:SetScript("OnEnterPressed", function(self)
      local copperPerHour = ParseGPHTextToCopper(self:GetText() or "")
      if not copperPerHour or copperPerHour < 0 then
        RefreshMinGPH()
        self:ClearFocus()
        return
      end
      WF.DB:SetSetting("historyMinGPH", copperPerHour)
      RefreshMinGPH()
      self:ClearFocus()
    end)
    minGPHInput:SetScript("OnEscapePressed", function(self)
      RefreshMinGPH()
      self:ClearFocus()
    end)
    minGPHInput:SetScript("OnEditFocusLost", function(self)
      RefreshMinGPH()
    end)
    minGPHWrap._edit = minGPHInput
    Tip(minGPHWrap, "Minimum GPH", {
      "Only save runs whose total value meets this gold per hour threshold.",
      "Accepts values like: 15000, 15k, 12.5k, 25000g.",
      "Set to 0 to disable the minimum.",
    })

    local cConfirm = MakeCheckW(secHistory.content, "Confirm destructive actions",
      function() return WF.DB:GetSetting("confirmDestructive") end,
      function(v) WF.DB:SetSetting("confirmDestructive", v) end
    )
    Tip(cConfirm, "Confirm destructive actions", "Ask before pruning or clearing history.")

    local bPrune = Widgets:Button(secHistory.content, "Prune now", 240, 24)
    Tip(bPrune, "Prune now", "Immediately prune History to your 'Sessions to keep' limit.")

    local bClear = Widgets:Button(secHistory.content, "Clear history", 240, 24)
    Tip(bClear, "Clear history", "Delete ALL recorded sessions from History.")

    bPrune:SetScript("OnClick", function()
      local function doPrune()
        local keep = tonumber(WF.DB:GetSetting("historyKeep")) or 100
        keep = math.floor(keep)

        local db = WF.DB:Get() or {}
        db.history = db.history or {}
        db.history.sessions = db.history.sessions or {}

        while #db.history.sessions > keep do
          table.remove(db.history.sessions, 1)
        end

        RebuildHistoryAggregates(db)
        if UI and UI.RefreshOverview then UI:RefreshOverview() end
        if UI and UI.RefreshHistory then UI:RefreshHistory() end
      end

      if WF.DB:GetSetting("confirmDestructive") then
        UI:Confirm("Prune history?", "This will remove older sessions above your keep limit.", doPrune)
      else
        doPrune()
      end
    end)

    bClear:SetScript("OnClick", function()
      local function doClear()
        local db = WF.DB:Get() or {}
        db.history = db.history or {}
        db.history.sessions = {}
        db.history.lastRunByFarm = {}
        db.history.totalsByFarm = {}
        RebuildHistoryAggregates(db)
        if UI and UI.RefreshOverview then UI:RefreshOverview() end
        if UI and UI.RefreshHistory then UI:RefreshHistory() end
      end

      if WF.DB:GetSetting("confirmDestructive") then
        UI:Confirm("Clear history?", "This will delete ALL recorded sessions from the History tab.", doClear)
      else
        doClear()
      end
    end)

    AddPair(secHistory, minDurWrap, minGPHWrap, 16)
    AddPair(secHistory, avgDurWrap, keepWrap, 12)
    AddPair(secHistory, cConfirm, nil, 12)
    AddPair(secHistory, bClear, bPrune, 16)

    -- -----------------------------
    -- Loot tracking
    -- -----------------------------
        local secLoot = NewSection("Loot tracking", {
      "Controls what gets recorded during a run.",
      "These settings affect History values and drop capture.",
    })

    local minWrap = CreateFrame("Frame", nil, secLoot.content)
    minWrap:SetSize(240, 44)
    local minLab = MakeMutedText(minWrap, "Min total quantity to record:")
    minLab:SetPoint("TOPLEFT", 0, 0)

    local minInput = MakeInput(minWrap, 140, 24)
    minInput:SetPoint("TOPLEFT", 0, -18)
    minInput:SetText(tostring(WF.DB:GetSetting("lootMinQty") or 1))
    minInput:SetScript("OnEnterPressed", function(self)
      local n = tonumber(self:GetText() or "")
      n = n and math.floor(n)
      if not n or n < 1 then
        self:SetText(tostring(WF.DB:GetSetting("lootMinQty") or 1))
        self:ClearFocus()
        return
      end
      WF.DB:SetSetting("lootMinQty", n)
      self:SetText(tostring(n))
      self:ClearFocus()
    end)
    minInput:SetScript("OnEscapePressed", function(self)
      self:SetText(tostring(WF.DB:GetSetting("lootMinQty") or 1))
      self:ClearFocus()
    end)
    minInput:SetScript("OnEditFocusLost", function(self)
      self:SetText(tostring(WF.DB:GetSetting("lootMinQty") or 1))
    end)
    minWrap._edit = minInput
    Tip(minWrap, "Min quantity", "Requires at least this many of an item to record it.")

    local minGoldWrap = CreateFrame("Frame", nil, secLoot.content)
    minGoldWrap:SetSize(240, 44)
    local minGoldLab = MakeMutedText(minGoldWrap, "Min total value to record:")
    minGoldLab:SetPoint("TOPLEFT", 0, 0)

    local minGoldInput = MakeInput(minGoldWrap, 140, 24)
    minGoldInput:SetPoint("TOPLEFT", 0, -18)

    local function ParseMoneyTextToCopper(txt)
      txt = tostring(txt or "")
      local t = txt:lower()
      local g = t:match("([%d%.]+)%s*g")
      local s = t:match("([%d%.]+)%s*s")
      local c = t:match("([%d%.]+)%s*c")

      local cg = g and tonumber(g) or 0
      local cs = s and tonumber(s) or 0
      local cc = c and tonumber(c) or 0

      if (cg > 0) or (cs > 0) or (cc > 0) then
        return math.floor((cg * 10000) + (cs * 100) + cc + 0.5)
      end

      local n = tonumber((t:gsub(",", "")))
      if not n then return nil end
      return math.floor((n * 10000) + 0.5)
    end

    local function RefreshMinGold()
      local v = tonumber(WF.DB:GetSetting("lootMinGold")) or 0
      if v < 0 then v = 0 end
      minGoldInput:SetText(Money(v))
    end

    RefreshMinGold()

    minGoldInput:SetScript("OnEnterPressed", function(self)
      local copper = ParseMoneyTextToCopper(self:GetText() or "")
      if not copper or copper < 0 then
        RefreshMinGold()
        self:ClearFocus()
        return
      end
      WF.DB:SetSetting("lootMinGold", copper)
      RefreshMinGold()
      self:ClearFocus()
    end)
    minGoldInput:SetScript("OnEscapePressed", function(self)
      RefreshMinGold()
      self:ClearFocus()
    end)
    minGoldInput:SetScript("OnEditFocusLost", function(self)
      RefreshMinGold()
    end)
    minGoldWrap._edit = minGoldInput
    Tip(minGoldWrap, "Min value", {
      "Only record items whose total value meets this threshold.",
      { text = "Examples: 100g, 250g 50s, 100 (treated as gold).", style = "muted" },
    })

    local comboQ = MakeCombo(secLoot.content, "Min item quality",
      function()
        local v = WF.DB:GetSetting("lootMinQuality")
        if type(v) == "string" then v = QUALITY_VALUE[v] or tonumber(v) end
        v = tonumber(v) or 0
        if v < 0 then v = 0 end
        local label = QUALITY_LABEL[v] or "Poor+"
        return (tostring(label):gsub("%+", ""))
      end,
      function(v)
        local key = tostring(v or "")
        if key ~= "" and key:sub(-1) ~= "+" then
          key = key .. "+"
        end
        local n = QUALITY_VALUE[key] or tonumber(key) or 0
        if n < 0 then n = 0 end
        WF.DB:SetSetting("lootMinQuality", n)
      end,
      function()
        local out = {}
        for i = 0, 5 do
          local label = QUALITY_LABEL[i] or "Poor+"
          out[#out+1] = (tostring(label):gsub("%+", ""))
        end
        return out
      end
    )
    Tip(comboQ, "Min quality", "Only record items at or above this quality threshold.")

    local uiLootCategoryKeys = {
      "lootRecordMaterials",
      "lootRecordGear",
      "lootRecordRecipes",
      "lootRecordPets",
      "lootRecordToys",
      "lootRecordMounts",
      "lootRecordConsumables",
      "lootRecordMisc",
    }

    local uiLootNonMaterialCategoryKeys = {
      "lootRecordGear",
      "lootRecordRecipes",
      "lootRecordPets",
      "lootRecordToys",
      "lootRecordMounts",
      "lootRecordConsumables",
      "lootRecordMisc",
    }

    local function _UIGetLootCategorySelection()
      local out = {}
      local hasData = false

      for i = 1, #uiLootCategoryKeys do
        local key = uiLootCategoryKeys[i]
        local v = WF.DB:GetSetting(key)
        if v ~= nil then
          out[key] = v and true or false
          hasData = true
        else
          out[key] = false
        end
      end

      if hasData then
        return out
      end

      local includeMats = WF.DB:GetSetting("lootIncludeMaterials")
      if includeMats == nil then includeMats = true end
      includeMats = includeMats and true or false

      local onlyMats = WF.DB:GetSetting("lootOnlyMaterials") and true or false
      local onlyGear = WF.DB:GetSetting("lootOnlyGear") and true or false

      if onlyMats and onlyGear then
        out.lootRecordMaterials = true
        out.lootRecordGear = true
        return out
      end

      if onlyMats then
        out.lootRecordMaterials = true
        return out
      end

      if onlyGear then
        out.lootRecordGear = true
        return out
      end

      if includeMats == false then
        for i = 1, #uiLootNonMaterialCategoryKeys do
          out[uiLootNonMaterialCategoryKeys[i]] = true
        end
      end

      return out
    end

    local function _SetLootCategories(spec)
      for i = 1, #uiLootCategoryKeys do
        local key = uiLootCategoryKeys[i]
        WF.DB:SetSetting(key, spec and spec[key] and true or false)
      end
    end

    local function GetRecordMode()
      local categories = _UIGetLootCategorySelection()
      local count = 0
      for i = 1, #uiLootCategoryKeys do
        if categories[uiLootCategoryKeys[i]] then
          count = count + 1
        end
      end

      if count == 0 then return "Everything" end
      if count == 1 and categories.lootRecordMaterials then return "Only Materials" end
      if count == 1 and categories.lootRecordGear then return "Only Gear" end

      local nonMatCount = 0
      for i = 1, #uiLootNonMaterialCategoryKeys do
        if categories[uiLootNonMaterialCategoryKeys[i]] then
          nonMatCount = nonMatCount + 1
        end
      end
      if nonMatCount == #uiLootNonMaterialCategoryKeys and not categories.lootRecordMaterials then
        return "Everything (no materials)"
      end

      return "Custom Categories"
    end

    local function SetRecordMode(mode)
      mode = tostring(mode or "Everything")

      if mode == "Only Materials" then
        _SetLootCategories({ lootRecordMaterials = true })
        WF.DB:SetSetting("lootOnlyMaterials", true)
        WF.DB:SetSetting("lootOnlyGear", false)
        WF.DB:SetSetting("lootIncludeMaterials", true)
        return
      end

      if mode == "Only Gear" then
        _SetLootCategories({ lootRecordGear = true })
        WF.DB:SetSetting("lootOnlyGear", true)
        WF.DB:SetSetting("lootOnlyMaterials", false)
        WF.DB:SetSetting("lootIncludeMaterials", false)
        return
      end

      if mode == "Everything (no materials)" then
        local spec = {}
        for i = 1, #uiLootNonMaterialCategoryKeys do
          spec[uiLootNonMaterialCategoryKeys[i]] = true
        end
        _SetLootCategories(spec)
        WF.DB:SetSetting("lootOnlyMaterials", false)
        WF.DB:SetSetting("lootOnlyGear", false)
        WF.DB:SetSetting("lootIncludeMaterials", false)
        return
      end

      if mode == "Custom Categories" then
        return
      end

      -- Everything
      _SetLootCategories(nil)
      WF.DB:SetSetting("lootOnlyMaterials", false)
      WF.DB:SetSetting("lootOnlyGear", false)
      WF.DB:SetSetting("lootIncludeMaterials", true)
    end

    local comboRec = MakeCombo(secLoot.content, "Record",
      function() return GetRecordMode() end,
      function(v) SetRecordMode(v) end,
      function()
        return {
          {
            label = "Everything",
            value = "Everything",
            tooltipTitle = "Everything",
            tooltipBody = {
              "Records all supported loot categories that pass your filters.",
              { text = "Materials, gear, recipes, pets, toys, mounts, consumables, and misc are all included.", style = "muted" },
            },
          },
          {
            label = "Everything (no materials)",
            value = "Everything (no materials)",
            tooltipTitle = "Everything (no materials)",
            tooltipBody = {
              "Records everything except materials.",
              { text = "Good when cloth, ore, herbs, and other raw mats would flood your item history.", style = "muted" },
            },
          },
          {
            label = "Only Materials",
            value = "Only Materials",
            tooltipTitle = "Only Materials",
            tooltipBody = {
              "Only records material drops.",
              { text = "Useful for cloth, ore, herbs, leather, and similar raw-farm sessions.", style = "muted" },
            },
          },
          {
            label = "Only Gear",
            value = "Only Gear",
            tooltipTitle = "Only Gear",
            tooltipBody = {
              "Only records gear items.",
              { text = "Useful for transmog, BOE, or equipment-focused farms.", style = "muted" },
            },
          },
          {
            label = "Custom Categories",
            value = "Custom Categories",
            tooltipTitle = "Custom Categories",
            tooltipBody = {
              "Uses your detailed loot category rules.",
              { text = "Configure exactly what gets recorded in Profiles > Edit Rules.", style = "muted" },
            },
          },
        }
      end
    )
    Tip(comboRec, "Record", {
      "Choose which loot categories this profile records.",
      { text = "Hover each dropdown option to see what it does.", style = "muted" },
      { text = "Custom Categories uses your detailed rules from Profiles > Edit Rules.", style = "muted" },
    })

    local cSB = MakeCheckW(secLoot.content, "Do not record Soulbound or BoP items",
      function() return WF.DB:GetSetting("lootIgnoreSoulbound") end,
      function(v) WF.DB:SetSetting("lootIgnoreSoulbound", v) end
    )
    Tip(cSB, "Soulbound or BoP items", "Skip items that bind on pick up or are soulbound.")

    AddPair(secLoot, minWrap, minGoldWrap, 12)
    AddPair(secLoot, comboQ, comboRec, 12)
    AddPair(secLoot, cSB, nil, 16)

    -- Allow the Main window Profile button to live-update this Settings section.
    local function RefreshLootTrackingUI()
      if minInput and minInput.SetText then
        minInput:SetText(tostring(WF.DB:GetSetting("lootMinQty") or 1))
      end

      if comboQ and comboQ._refresh then
        comboQ._refresh()
      end

      if comboRec and comboRec._refresh then
        comboRec._refresh()
      end

      if RefreshMinGold then
        RefreshMinGold()
      end

      if cSB and cSB.Refresh then cSB.Refresh() end
    end

    function UI:RefreshLootSettings()
      RefreshLootTrackingUI()
    end

-- -----------------------------
    -- Capture & UI
    -- -----------------------------
    
    -- -----------------------------
    -- Notifications
    -- -----------------------------
    local secNotify = NewSection("Notifications", {
      "Controls valuable loot alerts and Random Farm roll pings.",
      "Loot values use your active Price source when available (TSM/Auctionator), otherwise vendor sell price.",
    })

    local function UpdateNotifyEnabled() end -- forward

    local cNotify = MakeCheckW(secNotify.content, "Enable valuable loot notifications",
      function() return WF.DB:GetSetting("notifyLootEnabled") end,
      function(v)
        WF.DB:SetSetting("notifyLootEnabled", v)
        UpdateNotifyEnabled()
      end
    )
    Tip(cNotify, "Enable notifications", "Play a sound, print a chat message, and/or show a raid warning toast when you loot valuable items.")

    local minNotifyWrap = CreateFrame("Frame", nil, secNotify.content)
    minNotifyWrap:SetSize(240, 44)

    local minNotifyLab = MakeMutedText(minNotifyWrap, "Minimum total item value to notify:")
    minNotifyLab:SetPoint("TOPLEFT", 0, 0)

    local minNotifyInput = MakeInput(minNotifyWrap, 140, 24)
    minNotifyInput:SetPoint("TOPLEFT", 0, -18)

    local function ParseMoneyTextToCopperNotify(txt)
      txt = tostring(txt or "")
      local t = txt:lower()
      local g = t:match("([%d%.]+)%s*g")
      local s = t:match("([%d%.]+)%s*s")
      local c = t:match("([%d%.]+)%s*c")

      local cg = g and tonumber(g) or 0
      local cs = s and tonumber(s) or 0
      local cc = c and tonumber(c) or 0

      if (cg > 0) or (cs > 0) or (cc > 0) then
        return math.floor((cg * 10000) + (cs * 100) + cc + 0.5)
      end

      local n = tonumber((t:gsub(",", "")))
      if not n then return nil end
      return math.floor((n * 10000) + 0.5)
    end

    local function RefreshNotifyMin()
      local v = tonumber(WF.DB:GetSetting("notifyLootMinGold")) or 0
      if v < 0 then v = 0 end
      minNotifyInput:SetText(Money(v))
    end

    RefreshNotifyMin()
    minNotifyInput:SetScript("OnEnterPressed", function(self)
      local copper = ParseMoneyTextToCopperNotify(self:GetText() or "")
      if not copper or copper < 0 then
        RefreshNotifyMin()
        self:ClearFocus()
        return
      end
      WF.DB:SetSetting("notifyLootMinGold", math.floor(copper + 0.5))
      RefreshNotifyMin()
      self:ClearFocus()
    end)
    minNotifyInput:SetScript("OnEscapePressed", function(self)
      RefreshNotifyMin()
      self:ClearFocus()
    end)
    minNotifyInput:SetScript("OnEditFocusLost", function(self)
      RefreshNotifyMin()
    end)

    minNotifyWrap._edit = minNotifyInput
    Tip(minNotifyWrap, "Minimum value", {
      "Only notify when the looted stack's total value meets or exceeds this threshold.",
      { text = "Examples: 100g, 250g 50s, 100 (treated as gold).", style = "muted" },
    })

    local SOUND_LABELS = { "None", "Coins", "Loot", "Epic toast", "Alert" }
    local SOUND_BY_LABEL = {
      ["None"] = "none",
      ["Coins"] = "coins",
      ["Loot"] = "loot",
      ["Epic toast"] = "epic",
      ["Alert"] = "alert",
    }
    local SOUND_BY_KEY = {
      none = "None",
      coins = "Coins",
      loot = "Loot",
      epic = "Epic toast",
      alert = "Alert",
    }

    local comboSound = MakeCombo(secNotify.content, "Notification sound",
      function()
        local key = tostring(WF.DB:GetSetting("notifyLootSound") or "coins"):lower()
        return SOUND_BY_KEY[key] or "Coins"
      end,
      function(label)
        local key = SOUND_BY_LABEL[tostring(label or "")] or "coins"
        WF.DB:SetSetting("notifyLootSound", key)
      end,
      function() return SOUND_LABELS end
    )
    Tip(comboSound, "Sound", "Sound to play when a valuable item is looted.")

    local cChat = MakeCheckW(secNotify.content, "Print notification in chat",
      function() return WF.DB:GetSetting("notifyLootChat") end,
      function(v) WF.DB:SetSetting("notifyLootChat", v) end
    )
    Tip(cChat, "Chat message", "Print a one line message in chat when a valuable item is looted.")

    local cRaidWarn = MakeCheckW(secNotify.content, "Show raid warning toast",
      function() return WF.DB:GetSetting("notifyLootRaidWarning") end,
      function(v) WF.DB:SetSetting("notifyLootRaidWarning", v) end
    )
    Tip(cRaidWarn, "Raid warning toast", "Shows a centered raid warning style toast in WhatFarm blue when a valuable item is looted.")

    local cShowVal = MakeCheckW(secNotify.content, "Include value in the chat message",
      function() return WF.DB:GetSetting("notifyLootShowValue") end,
      function(v) WF.DB:SetSetting("notifyLootShowValue", v) end
    )
    Tip(cShowVal, "Include value", "Adds the estimated value to the chat message (based on your Price source).")

    local bTest = Widgets:Button(secNotify.content, "Test Notification", 240, 24)
    Tip(bTest, "Test Notification", "Plays the selected sound and shows a sample notification using your enabled chat and raid warning settings.")
    bTest:SetScript("OnClick", function()
      if WF and WF.Notify and WF.Notify.Test then
        WF.Notify:Test()
      end
    end)


    local rfNotifyHeader = CreateFrame("Frame", nil, secNotify.content)
    rfNotifyHeader:SetSize(240, 58)

    local rfNotifyDivider = MakeDivider(rfNotifyHeader)
    rfNotifyDivider:SetPoint("TOPLEFT", rfNotifyHeader, "TOPLEFT", 0, -2)
    rfNotifyDivider:SetPoint("TOPRIGHT", rfNotifyHeader, "TOPRIGHT", 0, -2)

    local rfNotifyTitle = MakeBodyText(rfNotifyHeader, "Random Farm roll notifications")
    rfNotifyTitle:SetPoint("TOPLEFT", rfNotifyHeader, "TOPLEFT", 0, -14)
    rfNotifyTitle:SetTextColor(C4(ACCENT, ACCENT))

    local rfNotifyBody = MakeMutedText(rfNotifyHeader, "These only affect the Random Farm popup shown when you roll a farm.")
    rfNotifyBody:SetPoint("TOPLEFT", rfNotifyTitle, "BOTTOMLEFT", 0, -3)
    rfNotifyBody:SetPoint("RIGHT", rfNotifyHeader, "RIGHT", 0, 0)
    rfNotifyBody:SetJustifyH("LEFT")
    if rfNotifyBody.SetWordWrap then rfNotifyBody:SetWordWrap(true) end

    rfNotifyHeader._wfRefreshLayout = function(self, fullW)
      fullW = tonumber(fullW) or self:GetWidth() or 240
      if fullW < 240 then fullW = 240 end
      self:SetWidth(fullW)
      if rfNotifyDivider then
        rfNotifyDivider:ClearAllPoints()
        rfNotifyDivider:SetPoint("TOPLEFT", self, "TOPLEFT", 0, -2)
        rfNotifyDivider:SetPoint("TOPRIGHT", self, "TOPRIGHT", 0, -2)
        if rfNotifyDivider._wfSyncDividerGeometry then
          rfNotifyDivider:_wfSyncDividerGeometry()
        end
      end
      if rfNotifyBody then
        rfNotifyBody:SetWidth(fullW)
        local h = 58
        if rfNotifyBody.GetStringHeight then
          h = math.max(58, 14 + 16 + math.ceil((rfNotifyBody:GetStringHeight() or 0) + 0.5) + 10)
        end
        self:SetHeight(h)
      end
    end

    local RF_NOTIFY_STYLE_ITEMS = {
      { value = "Banner", label = "Banner", tooltipTitle = "Banner", tooltipBody = "Centered two-line What Farm text. Cleanest default style." },
      { value = "Card", label = "Card", tooltipTitle = "Card", tooltipBody = "Smaller dark card with subtle What Farm styling." },
      { value = "Compact", label = "Compact", tooltipTitle = "Compact", tooltipBody = "One-line pill style for faster rolls." },
      { value = "Minimal", label = "Minimal", tooltipTitle = "Minimal", tooltipBody = "Small text-only notification with no panel." },
      { value = "Raid Warning", label = "Raid Warning", tooltipTitle = "Raid Warning", tooltipBody = "Default Blizzard-style raid warning text." },
    }

    local RF_NOTIFY_SOUND_ITEMS = {
      { value = "None", label = "None", tooltipTitle = "None", tooltipBody = "Do not play a sound when Random Farm rolls." },
      { value = "Soft", label = "Soft", tooltipTitle = "Soft", tooltipBody = "Light UI confirmation sound." },
      { value = "Click", label = "Click", tooltipTitle = "Click", tooltipBody = "Short button-style sound." },
      { value = "Ready Check", label = "Ready Check", tooltipTitle = "Ready Check", tooltipBody = "A stronger but still clean alert sound." },
      { value = "Raid Warning", label = "Raid Warning", tooltipTitle = "Raid Warning", tooltipBody = "Louder raid warning style alert." },
      { value = "Epic Loot", label = "Epic Loot", tooltipTitle = "Epic Loot", tooltipBody = "Loot-toast style sound when a farm is chosen." },
    }

    local comboRFNotifyStyle = MakeCombo(secNotify.content, "Random Farm style",
      function()
        EnsureSettingsDefaults()
        local v = WF.DB:GetSetting("randomFarmToastStyle") or "Banner"
        if v == "Boxed" then v = "Card" end
        return v
      end,
      function(v)
        EnsureSettingsDefaults()
        if v == "Boxed" then v = "Card" end
        WF.DB:SetSetting("randomFarmToastStyle", v)
      end,
      function() return RF_NOTIFY_STYLE_ITEMS end
    )
    Tip(comboRFNotifyStyle, "Random Farm style", {
      "Controls the popup style used when Random Farm chooses a farm.",
      { text = "This is separate from valuable loot notifications.", style = "muted" },
    })

    local comboRFNotifySound = MakeCombo(secNotify.content, "Random Farm sound",
      function()
        EnsureSettingsDefaults()
        return WF.DB:GetSetting("randomFarmToastSound") or "None"
      end,
      function(v)
        EnsureSettingsDefaults()
        WF.DB:SetSetting("randomFarmToastSound", v)
      end,
      function() return RF_NOTIFY_SOUND_ITEMS end
    )
    Tip(comboRFNotifySound, "Random Farm sound", {
      "Sound to play when Random Farm chooses a farm.",
      { text = "Set this to None when you want the popup without audio.", style = "muted" },
    })

    local bTestRFNotify = Widgets:Button(secNotify.content, "Test Farm Notification", 240, 24)
    Tip(bTestRFNotify, "Test Farm Notification", "Shows a sample Random Farm popup using the selected style and sound.")
    bTestRFNotify:SetScript("OnClick", function()
      EnsureSettingsDefaults()
      if WF and WF.UI and WF.UI.Main and WF.UI.Main._ShowRandomFarmToast then
        WF.UI.Main:_ShowRandomFarmToast("What Farm?: Chose Example Farm", "Example Zone • 51.0, 42.7 • Map pin set", 1.55)
      elseif UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: Chose Example Farm • Example Zone • Map pin set", 0.35, 0.78, 1.00, 1)
      end
    end)

    local function SetControlEnabled(w, enabled)
      enabled = enabled and true or false
      local target = w and (w._edit or w._btn or w._check) or w
      if target and target.SetEnabled then target:SetEnabled(enabled) end
      if target and target.EnableMouse then target:EnableMouse(enabled) end
      if w and w.SetAlpha then w:SetAlpha(enabled and 1 or 0.45) end
    end

    UpdateNotifyEnabled = function()
      local on = WF.DB:GetSetting("notifyLootEnabled") and true or false
      SetControlEnabled(minNotifyWrap, on)
      SetControlEnabled(comboSound, on)
      SetControlEnabled(cChat, on)
      SetControlEnabled(cRaidWarn, on)
      SetControlEnabled(cShowVal, on)
      SetControlEnabled(bTest, on)
    end

    UpdateNotifyEnabled()

    AddPair(secNotify, minNotifyWrap, comboSound, 10)
    AddPair(secNotify, cNotify, cChat, 12)
    AddPair(secNotify, cRaidWarn, cShowVal, 12)
    AddPair(secNotify, bTest, nil, 16)
    AddFull(secNotify, rfNotifyHeader, 8)
    AddPair(secNotify, comboRFNotifyStyle, comboRFNotifySound, 12)
    AddPair(secNotify, bTestRFNotify, nil, 16)

    -- -----------------------------
    -- Window
    -- -----------------------------
    local secWin = NewSection("Window", {
      "Controls layout mode, header metric pills, capture preferences, and UI scale.",
      "Tip: window locking is handled via the corner gripper on the window itself.",
    })

    local RefreshWindowPillWidgets

    local comboLayout = MakeCombo(secWin.content, "Window Layout",
function()
  local mode = "Normal"
  if WF and WF.UI and WF.UI.Main and WF.UI.Main.GetLayoutMode then
    mode = tostring(WF.UI.Main:GetLayoutMode() or "Normal")
  else
    mode = tostring(WF.DB:GetSetting("mainWindowLayout") or "Normal")
  end
  if mode == "Compare" or mode == "Competitive" then
    mode = "Normal"
  end
  return mode
end,
      function(v)
        WF.DB:SetSetting("mainWindowLayout", v)
        if WF.UI.Main and WF.UI.Main.ApplyLayoutMode then
          WF.UI.Main:ApplyLayoutMode(v)
        elseif WF.UI.Main and WF.UI.Main.SetLayoutMode then
          WF.UI.Main:SetLayoutMode(v)
        end
        if RefreshWindowPillWidgets then
          RefreshWindowPillWidgets()
        end
      end,
      function() return { "Normal", "Compact" } end
    )
    Tip(comboLayout, "Window Layout", "Switches the main window layout between Normal and Compact.")

    local function PillSettingDisplayValue(value)
      value = tostring(value or "")
      if value == "Looted Value" or value == "Looted Item Value" then
        return "Looted Item Value"
      end
      return value
    end

    local function PillSettingStoredValue(value)
      value = tostring(value or "Hidden")
      if value == "Looted Item Value" then
        return "Looted Value"
      end
      return value
    end

    local PILL_CHOICE_TOOLTIPS = {
      ["Time"] = {
        title = "Time",
        body = "Shows the current run timer.",
      },
      ["Looted Gold"] = {
        title = "Looted Gold",
        body = "Shows raw coin looted during the current run.",
      },
      ["Looted Items"] = {
        title = "Looted Items",
        body = "Shows the total number of item drops recorded during the current run.",
      },
      ["Looted Item Value"] = {
        title = "Looted Item Value",
        body = "Shows the estimated value of recorded item drops, separate from raw looted gold.",
      },
      ["Estimated GPH"] = {
        title = "Estimated GPH",
        body = "Shows estimated gold per hour based on the current run's total value and elapsed time.",
      },
      ["Total Value"] = {
        title = "Total Value",
        body = "Shows raw looted gold plus the estimated value of recorded item drops.",
      },
      ["Race Timer"] = {
        title = "Race Timer",
        body = "Shows the active race timer value when you are using the run timer challenge tools.",
      },
      ["Hidden"] = {
        title = "Hidden",
        body = "Leaves this header pill slot empty.",
      },
    }

    local function PillChoices()
      local items
      if WF and WF.UI and WF.UI.Main and WF.UI.Main.GetMetricPillOptions then
        items = WF.UI.Main:GetMetricPillOptions()
      else
        items = {
          "Time",
          "Looted Gold",
          "Looted Items",
          "Looted Value",
          "Estimated GPH",
          "Total Value",
          "Race Timer",
          "Hidden",
        }
      end

      local out = {}
      for i = 1, #items do
        local item = tostring(items[i] or "")
        if item == "Looted Value" then
          item = "Looted Item Value"
        end
        local tip = PILL_CHOICE_TOOLTIPS[item]
        if tip then
          out[#out + 1] = {
            label = item,
            value = item,
            tooltipTitle = tip.title,
            tooltipBody = tip.body,
          }
        else
          out[#out + 1] = item
        end
      end
      return out
    end

    local function ApplyMainPillSetting(slot, value)
      value = tostring(value or "Hidden")
      if value == "Kills" then value = "Hidden" end
      value = PillSettingStoredValue(value)
      WF.DB:SetSetting("mainWindowPill" .. tostring(slot), value)
      if WF.UI and WF.UI.Main and WF.UI.Main.ApplyMetricPillConfig then
        WF.UI.Main:ApplyMetricPillConfig(true)
      elseif WF.UI and WF.UI.Main and WF.UI.Main.RefreshMetrics then
        WF.UI.Main:RefreshMetrics()
      end
    end

    local comboPill1 = MakeCombo(secWin.content, "Pill 1",
      function()
        local v = WF.DB:GetSetting("mainWindowPill1") or "Time"
        if v == "Kills" then return "Hidden" end
        return PillSettingDisplayValue(v)
      end,
      function(v) ApplyMainPillSetting(1, v) end,
      PillChoices
    )
    Tip(comboPill1, "Pill 1", "Chooses which metric is shown in the first header pill.")

    local comboPill2 = MakeCombo(secWin.content, "Pill 2",
      function()
        local v = WF.DB:GetSetting("mainWindowPill2") or "Looted Gold"
        if v == "Kills" then return "Hidden" end
        return PillSettingDisplayValue(v)
      end,
      function(v) ApplyMainPillSetting(2, v) end,
      PillChoices
    )
    Tip(comboPill2, "Pill 2", "Chooses which metric is shown in the second header pill.")

    local comboPill3 = MakeCombo(secWin.content, "Pill 3",
      function()
        local v = WF.DB:GetSetting("mainWindowPill3") or "Looted Items"
        if v == "Kills" then return "Hidden" end
        return PillSettingDisplayValue(v)
      end,
      function(v) ApplyMainPillSetting(3, v) end,
      PillChoices
    )
    Tip(comboPill3, "Pill 3", "Chooses which metric is shown in the third header pill.")

    local comboPill4 = MakeCombo(secWin.content, "Pill 4",
      function()
        local v = WF.DB:GetSetting("mainWindowPill4") or "Hidden"
        if v == "Kills" then return "Hidden" end
        return PillSettingDisplayValue(v)
      end,
      function(v) ApplyMainPillSetting(4, v) end,
      PillChoices
    )
    Tip(comboPill4, "Pill 4", "Chooses which metric is shown in the fourth header pill.")

    local cCompactPill4 = MakeCheckW(secWin.content, "Enable 4th Pill",
      function() return WF.DB:GetSetting("compactModeShowFourthPill") end,
      function(v)
        WF.DB:SetSetting("compactModeShowFourthPill", v)
        if RefreshWindowPillWidgets then
          RefreshWindowPillWidgets()
        end
        if WF.UI and WF.UI.Main and WF.UI.Main.ApplyMetricPillConfig then
          WF.UI.Main:ApplyMetricPillConfig(true)
        elseif WF.UI and WF.UI.Main and WF.UI.Main.RefreshMetrics then
          WF.UI.Main:RefreshMetrics()
        end
      end
    )
    Tip(cCompactPill4, "Enable 4th Pill", "Shows the fourth header pill when available. May cause crowding at smaller window sizes.")

    local compactLayoutRow = CreateFrame("Frame", nil, secWin.content)
    compactLayoutRow:SetSize(1, 44)
    compactLayoutRow._wfRefreshLayout = function(self, fullW, colW)
      local comboW = math.max(240, colW or 240)
      local checkW = math.max(180, fullW - comboW - 18)

      comboLayout:ClearAllPoints()
      comboLayout:SetParent(self)
      comboLayout:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
      SetWrapWidth(comboLayout, comboW)

      cCompactPill4:ClearAllPoints()
      cCompactPill4:SetParent(self)
      cCompactPill4:SetPoint("LEFT", comboLayout._btn or comboLayout, "RIGHT", 16, 0)
      cCompactPill4:SetPoint("RIGHT", self, "RIGHT", 0, 0)
      if cCompactPill4.SetWidth then cCompactPill4:SetWidth(checkW) end
      if cCompactPill4._text and cCompactPill4._text.SetWidth then
        cCompactPill4._text:SetWidth(math.max(1, checkW - 30))
      end

      self:SetHeight(math.max(comboLayout:GetHeight() or 44, (cCompactPill4:IsShown() and (cCompactPill4:GetHeight() or 24)) or 24))
    end

    RefreshWindowPillWidgets = function()
      local layoutMode = "Normal"
      if WF and WF.UI and WF.UI.Main and WF.UI.Main.GetLayoutMode then
        layoutMode = tostring(WF.UI.Main:GetLayoutMode() or "Normal")
      else
        layoutMode = tostring(WF.DB:GetSetting("mainWindowLayout") or "Normal")
      end
      if layoutMode ~= "Compact" then
        layoutMode = "Normal"
      end
      local isCompact = (layoutMode == "Compact")
      local allowFourthCompact = WF.DB:GetSetting("compactModeShowFourthPill") and true or false

      cCompactPill4:SetShown(isCompact)
      comboPill4:SetShown((not isCompact) or allowFourthCompact)

      if cCompactPill4.Refresh then
        cCompactPill4:Refresh()
      end
      if comboPill4._refresh then
        comboPill4._refresh()
      end

      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end

    local cAutoStart = MakeCheckW(secWin.content, "Auto start session when opening main window",
      function() return WF.DB:GetSetting("autoStartSessionOnOpen") end,
      function(v) WF.DB:SetSetting("autoStartSessionOnOpen", v) end
    )
    Tip(cAutoStart, "Auto start session", "Automatically starts recording when you open the main window if you are not already recording.")

    local cAutoResumeLoot = MakeCheckW(secWin.content, "Auto resume on loot while paused",
      function() return WF.DB:GetSetting("autoResumeOnLootWhilePaused") end,
      function(v) WF.DB:SetSetting("autoResumeOnLootWhilePaused", v) end
    )
    Tip(cAutoResumeLoot, "Auto resume on loot", "If a run is paused, looting an item automatically resumes the timer before that loot is recorded, so the item is still included without counting paused loot against a frozen timer.")

    local cSplitOnMode = MakeCheckW(secWin.content, "Auto split run when Mode changes while recording",
      function() return WF.DB:GetSetting("splitRunOnProfileChange") end,
      function(v)
        WF.DB:SetSetting("splitRunOnProfileChange", v)
        if v then
          WF.DB:SetSetting("lockProfileWhileRecording", false)
        end
      end
    )
    Tip(cSplitOnMode, "Auto split on Mode change", "If you switch profiles or modes while a run is recording, the addon will save the current run and immediately start a new run under the new Mode.")

    local cLockMode = MakeCheckW(secWin.content, "Lock mode while recording",
      function() return WF.DB:GetSetting("lockProfileWhileRecording") end,
      function(v)
        WF.DB:SetSetting("lockProfileWhileRecording", v)
        if v then
          WF.DB:SetSetting("splitRunOnProfileChange", false)
        end
      end
    )
    Tip(cLockMode, "Lock mode while recording", "Prevents changing profiles or modes while a run is recording. Stop the run or turn this off to switch.")

    -- Capture & UI (merged into Window)
    local cInst = MakeCheckW(secWin.content, "Allow instance farms (dungeons/raids)",
      function() return WF.DB:GetSetting("allowInstanceFarms") end,
      function(v) WF.DB:SetSetting("allowInstanceFarms", v) end
    )
    Tip(cInst, "Instance farms", "Allow recording farms done inside dungeons/raids.")

    local cSource = MakeCheckW(secWin.content, "Show 'Dropped by' in tooltips",
      function() return WF.DB:GetSetting("tooltipShowSource") end,
      function(v) WF.DB:SetSetting("tooltipShowSource", v) end
    )
    Tip(cSource, "Dropped by", "Adds a 'Dropped by' line to item tooltips when the source is known.")

    local cMini = MakeCheckW(secWin.content, "Show minimap button",
      function() return WF.DB:GetSetting("showMinimapButton") end,
      function(v)
        WF.DB:SetSetting("showMinimapButton", v)
        if WF.UI and WF.UI.Minimap and WF.UI.Minimap.Refresh then
          WF.UI.Minimap:Refresh()
        end
      end
    )
    Tip(cMini, "Minimap button", "Show or hide the minimap button.")

    local function FormatDoubleTapKeyLabel()
      local Main = WF and WF.UI and WF.UI.Main
      if Main and Main.FormatDoubleTapToggleKey then
        return Main:FormatDoubleTapToggleKey(WF.DB:GetSetting("doubleTapToggleKey") or "ESCAPE")
      end
      return tostring(WF.DB:GetSetting("doubleTapToggleKey") or "ESCAPE")
    end

    local cDoubleTap
    local RefreshDoubleTapWidgets

    cDoubleTap = MakeCheckW(secWin.content, "Enable double tap keybind to open or close What Farm?",
      function() return WF.DB:GetSetting("doubleTapToggleEnabled") end,
      function(v)
        WF.DB:SetSetting("doubleTapToggleEnabled", v)
        RefreshDoubleTapWidgets()
      end
    )

    local bindRow = CreateFrame("Frame", nil, secWin.content)
    bindRow:SetSize(1, 24)

    local bindLabel = MakeMutedText(bindRow, "Toggle key")
    bindLabel:SetPoint("LEFT", bindRow, "LEFT", 26, 0)

    local bindBtn = CreateFrame("Button", nil, bindRow, "BackdropTemplate")
    bindBtn:SetSize(132, 24)
    bindBtn:SetPoint("LEFT", bindLabel, "RIGHT", 10, 0)
    bindBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    bindBtn:SetBackdrop({
      bgFile = "Interface/Tooltips/UI-Tooltip-Background",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 10,
      insets = { left = 6, right = 6, top = 4, bottom = 4 },
    })
    bindBtn:SetBackdropColor(0.04, 0.04, 0.04, 0.90)
    bindBtn:SetBackdropBorderColor(C4(BORDER, BORDER))

    local bindBtnText = bindBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    bindBtnText:SetPoint("CENTER", 0, 0)
    bindBtn:SetScript("OnEnter", function(self)
      self:SetBackdropBorderColor(C4(ACCENT, ACCENT))
    end)
    bindBtn:SetScript("OnLeave", function(self)
      self:SetBackdropBorderColor(C4(BORDER, BORDER))
    end)

    local bindCapture = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    bindCapture:SetAllPoints(UIParent)
    bindCapture:SetFrameStrata("FULLSCREEN_DIALOG")
    bindCapture:SetFrameLevel(200)
    bindCapture:EnableKeyboard(true)
    bindCapture:EnableMouse(true)
    WF_TrySetPropagateKeyboardInput(bindCapture, false)
    bindCapture:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
    bindCapture:SetBackdropColor(0, 0, 0, 0.35)
    bindCapture:Hide()

    local bindCaptureText = bindCapture:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
    bindCaptureText:SetPoint("CENTER", 0, 12)
    bindCaptureText:SetTextColor(C4(ACCENT, ACCENT))
    bindCaptureText:SetText("Press a key for What Farm?")

    local bindCaptureSub = bindCapture:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    bindCaptureSub:SetPoint("TOP", bindCaptureText, "BOTTOM", 0, -8)
    bindCaptureSub:SetTextColor(C4(TEXT, TEXT))
    bindCaptureSub:SetText("Click anywhere to cancel.")

    RefreshDoubleTapWidgets = function()
      local enabled = WF.DB:GetSetting("doubleTapToggleEnabled") and true or false
      bindRow:SetShown(enabled)
      bindBtnText:SetText(FormatDoubleTapKeyLabel())
      if cDoubleTap and cDoubleTap.Refresh then
        cDoubleTap:Refresh()
      end
      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end

    bindCapture:SetScript("OnHide", function()
      UI._wfCapturingDoubleTapToggleKey = nil
      bindBtnText:SetText(FormatDoubleTapKeyLabel())
      if bindBtn.UnlockHighlight then bindBtn:UnlockHighlight() end
    end)

    bindCapture:SetScript("OnMouseDown", function()
      bindCapture:Hide()
    end)

    bindCapture:SetScript("OnKeyDown", function(_, key)
      local Main = WF and WF.UI and WF.UI.Main
      if not (Main and Main.IsAllowedDoubleTapToggleKey and Main.NormalizeDoubleTapToggleKey) then
        bindCapture:Hide()
        return
      end

      key = Main:NormalizeDoubleTapToggleKey(key)
      if not Main:IsAllowedDoubleTapToggleKey(key) then
        bindCapture:Hide()
        return
      end

      WF.DB:SetSetting("doubleTapToggleKey", key)
      bindCapture:Hide()
      RefreshDoubleTapWidgets()
    end)

    bindBtn:SetScript("OnClick", function(self, button)
      if button == "RightButton" then
        WF.DB:SetSetting("doubleTapToggleKey", "ESCAPE")
        RefreshDoubleTapWidgets()
        return
      end

      UI._wfCapturingDoubleTapToggleKey = true
      bindBtnText:SetText("Press a key...")
      if self.LockHighlight then self:LockHighlight() end
      bindCapture:Show()
    end)

    Tip(cDoubleTap, "Double tap keybind", "When enabled, double tap the selected key to toggle the main What Farm? window. The key selector only appears while this is enabled.")
    Tip(bindBtn, "Toggle key", "Choose which key must be double tapped to open or close the main What Farm? window. Right click resets it to Escape.")

    RefreshDoubleTapWidgets()
    RefreshWindowPillWidgets()

    local toggleOptionsWrap = CreateFrame("Frame", nil, secWin.content)
    toggleOptionsWrap:SetSize(1, 1)

    local toggleOptionsLeft = CreateFrame("Frame", nil, toggleOptionsWrap)
    local toggleOptionsRight = CreateFrame("Frame", nil, toggleOptionsWrap)

    local function LayoutSettingsStack(parentFrame, items, width, gap)
      local sy = 0
      gap = gap or 12

      for i = 1, #items do
        local item = items[i]
        if item and (not item.IsShown or item:IsShown()) then
          item:SetParent(parentFrame)
          item:ClearAllPoints()
          item:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", 0, -sy)
          SetWrapWidth(item, width)

          local h = item:GetHeight() or 24
          if h < 24 then h = 24 end
          sy = sy + h + gap
        end
      end

      if sy > 0 then
        sy = sy - gap
      end

      parentFrame:SetHeight(math.max(1, sy))
      return sy
    end

    toggleOptionsWrap._wfRefreshLayout = function(self, fullWidth, colWOverride)
      local innerColW = colWOverride or math.floor((((fullWidth or self:GetWidth() or 1) - GUTTER) / 2))
      if innerColW < 1 then innerColW = 1 end

      toggleOptionsLeft:ClearAllPoints()
      toggleOptionsLeft:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
      toggleOptionsLeft:SetWidth(innerColW)

      toggleOptionsRight:ClearAllPoints()
      toggleOptionsRight:SetPoint("TOPLEFT", self, "TOPLEFT", innerColW + GUTTER, 0)
      toggleOptionsRight:SetWidth(innerColW)

      local leftH = LayoutSettingsStack(toggleOptionsLeft, {
        cDoubleTap,
        bindRow,
        cSplitOnMode,
      }, innerColW, 12)

      local rightH = LayoutSettingsStack(toggleOptionsRight, {
        cLockMode,
        cAutoResumeLoot,
      }, innerColW, 12)

      self:SetHeight(math.max(leftH, rightH, 24))
    end

    local scaleWrap = CreateFrame("Frame", nil, secWin.content)
    scaleWrap:SetSize(1, 64)

    local scaleLabel = MakeMutedText(scaleWrap, "UI scale")
    scaleLabel:SetPoint("TOPLEFT", 0, 0)

    local scaleValue = MakeBodyText(scaleWrap, "")
    scaleValue:SetPoint("TOPRIGHT", -10, 0)
    scaleValue:SetJustifyH("RIGHT")

    -- Uses the shared ApplyUIScale() helper defined in this file.
    local function ClampScale(v)
      v = tonumber(v) or 1.0
      if v < 0.80 then return 0.80 end
      if v > 1.30 then return 1.30 end
      return v
    end

    local function RoundScale(v)
      v = ClampScale(v)
      return ClampScale(math.floor((v / 0.05) + 0.5) * 0.05)
    end

    local function SyncScaleText(v)
      scaleValue:SetText(string.format("%.0f%%", RoundScale(v) * 100))
    end

    local function SkinWhatFarmScaleSlider(slider)
      if not slider or slider._wfScaleSkinned then return end
      slider._wfScaleSkinned = true

      local accent = ACCENT or { 0.35, 0.78, 1.00, 1.00 }

      slider:SetOrientation("HORIZONTAL")
      slider:SetHeight(22)
      slider:SetThumbTexture("Interface/Buttons/WHITE8x8")

      local thumb = slider:GetThumbTexture()
      if thumb then
        thumb:SetTexture("Interface/Buttons/WHITE8x8")
        if thumb.SetColorTexture then
          thumb:SetColorTexture(1, 1, 1, 0)
        end
        thumb:SetVertexColor(1, 1, 1, 0)
        thumb:SetAlpha(0)
        thumb:SetSize(12, 22)
        if thumb.SetDrawLayer then thumb:SetDrawLayer("BACKGROUND", 0) end
      end

      local function ApplySolidBackdrop(frame, bgR, bgG, bgB, bgA)
        if not frame or not frame.SetBackdrop then return end
        frame:SetBackdrop({
          bgFile = "Interface/Buttons/WHITE8x8",
          insets = { left = 0, right = 0, top = 0, bottom = 0 },
        })
        frame:SetBackdropColor(bgR, bgG, bgB, bgA)
      end

      local function SetPixelBorderColor(frame, r, g, b, a)
        if frame and frame._wfPixelBorder and frame._wfPixelBorder.SetColor then
          frame._wfPixelBorder:SetColor(r, g, b, a)
        end
      end

      local track = CreateFrame("Frame", nil, slider, "BackdropTemplate")
      slider._wfTrack = track
      track:EnableMouse(false)
      track:SetPoint("LEFT", slider, "LEFT", 0, 0)
      track:SetPoint("RIGHT", slider, "RIGHT", 0, 0)
      track:SetHeight(8)
      if track.SetFrameLevel then track:SetFrameLevel(slider:GetFrameLevel() + 1) end
      ApplySolidBackdrop(track, 0.018, 0.021, 0.028, 0.94)
      if Widgets and Widgets.CreatePixelBorder then
        Widgets:CreatePixelBorder(track, track, { color = { 0.13, 0.16, 0.21, 0.90 }, layer = "BORDER", subLevel = 2 })
      end

      local fill = track:CreateTexture(nil, "ARTWORK")
      slider._wfFill = fill
      fill:SetTexture("Interface/Buttons/WHITE8x8")
      fill:SetVertexColor(accent[1], accent[2], accent[3], 0.88)
      fill:SetPoint("TOPLEFT", track, "TOPLEFT", 1, -1)
      fill:SetPoint("BOTTOMLEFT", track, "BOTTOMLEFT", 1, 1)

      local shine = track:CreateTexture(nil, "OVERLAY")
      slider._wfShine = shine
      shine:SetTexture("Interface/Buttons/WHITE8x8")
      shine:SetVertexColor(1, 1, 1, 0.08)
      shine:SetPoint("TOPLEFT", fill, "TOPLEFT", 0, 0)
      shine:SetPoint("TOPRIGHT", fill, "TOPRIGHT", 0, 0)
      shine:SetHeight(1)

      local knob = CreateFrame("Frame", nil, slider, "BackdropTemplate")
      slider._wfKnob = knob
      knob:EnableMouse(false)
      knob:SetSize(10, 18)
      if knob.SetFrameLevel then knob:SetFrameLevel(slider:GetFrameLevel() + 8) end
      ApplySolidBackdrop(knob, 0.035, 0.040, 0.052, 0.98)
      if Widgets and Widgets.CreatePixelBorder then
        Widgets:CreatePixelBorder(knob, knob, { color = { accent[1], accent[2], accent[3], 0.95 }, layer = "BORDER", subLevel = 2 })
      end

      local knobCore = knob:CreateTexture(nil, "ARTWORK")
      slider._wfKnobCore = knobCore
      knobCore:SetTexture("Interface/Buttons/WHITE8x8")
      knobCore:SetPoint("TOPLEFT", knob, "TOPLEFT", 3, -3)
      knobCore:SetPoint("BOTTOMRIGHT", knob, "BOTTOMRIGHT", -3, 3)
      knobCore:SetVertexColor(accent[1], accent[2], accent[3], 0.42)

      local knobLine = knob:CreateTexture(nil, "OVERLAY")
      slider._wfKnobLine = knobLine
      knobLine:SetTexture("Interface/Buttons/WHITE8x8")
      knobLine:SetSize(1, 10)
      knobLine:SetPoint("CENTER", knob, "CENTER", 0, 0)
      knobLine:SetVertexColor(1, 1, 1, 0.22)

      if thumb then
        knob:SetPoint("CENTER", thumb, "CENTER", 0, 0)
      else
        knob:SetPoint("CENTER", slider, "CENTER", 0, 0)
      end

      local function UpdateFill()
        local minV, maxV = slider:GetMinMaxValues()
        local v = tonumber(slider:GetValue()) or minV or 0

        minV = tonumber(minV) or 0
        maxV = tonumber(maxV) or 1

        local pct = 0
        if maxV > minV then
          pct = (v - minV) / (maxV - minV)
        end

        if pct < 0 then pct = 0 end
        if pct > 1 then pct = 1 end

        local px = 1
        if Widgets and Widgets.PixelSize then
          px = Widgets:PixelSize(track)
        end

        fill:ClearAllPoints()
        fill:SetPoint("TOPLEFT", track, "TOPLEFT", px, -px)
        fill:SetPoint("BOTTOMLEFT", track, "BOTTOMLEFT", px, px)

        shine:ClearAllPoints()
        shine:SetPoint("TOPLEFT", fill, "TOPLEFT", 0, 0)
        shine:SetPoint("TOPRIGHT", fill, "TOPRIGHT", 0, 0)
        shine:SetHeight(px)

        local trackW = (track:GetWidth() or 0) - (px * 2)
        if trackW < 0 then trackW = 0 end

        local w = math.floor((trackW * pct) + 0.5)
        if pct > 0 and w < 1 then w = 1 end
        if w > trackW then w = trackW end

        fill:SetWidth(w)
      end

      slider._wfUpdateFill = UpdateFill

      slider:HookScript("OnValueChanged", UpdateFill)
      slider:HookScript("OnSizeChanged", UpdateFill)
      slider:HookScript("OnShow", UpdateFill)

      slider:HookScript("OnEnter", function()
        SetPixelBorderColor(track, accent[1], accent[2], accent[3], 0.50)
        SetPixelBorderColor(knob, accent[1], accent[2], accent[3], 1.00)
        knobCore:SetVertexColor(accent[1], accent[2], accent[3], 0.58)
      end)

      slider:HookScript("OnLeave", function()
        SetPixelBorderColor(track, 0.13, 0.16, 0.21, 0.90)
        SetPixelBorderColor(knob, accent[1], accent[2], accent[3], 0.95)
        knobCore:SetVertexColor(accent[1], accent[2], accent[3], 0.42)
      end)

      UpdateFill()
    end

    local slider = CreateFrame("Slider", "WF_HelpUIScaleSlider", scaleWrap)
    scaleWrap._slider = slider
    slider:SetPoint("TOPLEFT", 0, -22)
    slider:SetPoint("TOPRIGHT", -10, -22)
    slider:SetMinMaxValues(0.80, 1.30)
    slider:SetValueStep(0.05)
    if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end

    SkinWhatFarmScaleSlider(slider)

    local scaleLow = MakeMutedText(scaleWrap, "80%")
    scaleLow:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, -2)

    local scaleHigh = MakeMutedText(scaleWrap, "130%")
    scaleHigh:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, -2)
    scaleHigh:SetJustifyH("RIGHT")

    local lastAppliedScale

    local function CommitScale(v)
      v = RoundScale(v)
      if lastAppliedScale == v then return end
      lastAppliedScale = v
      ApplyUIScale(v)
    end

    slider:SetScript("OnShow", function(self)
      local s = RoundScale(WF.DB:GetSetting("uiScale"))

      self._wfSilent = true
      self:SetValue(s)
      self._wfSilent = false

      SyncScaleText(s)
      if self._wfUpdateFill then self._wfUpdateFill() end
    end)

    slider:SetScript("OnMouseDown", function(self)
      self._wfDragging = true
    end)

    slider:SetScript("OnMouseUp", function(self)
      self._wfDragging = false

      local v = RoundScale(self:GetValue())
      self:SetValue(v)
      SyncScaleText(v)
      CommitScale(v)
    end)

    slider:SetScript("OnValueChanged", function(self, v)
      v = RoundScale(v)
      SyncScaleText(v)
      if self._wfUpdateFill then self._wfUpdateFill() end

      if self._wfSilent then return end
      if self._wfDragging then return end

      CommitScale(v)
    end)

    do
      local s = RoundScale(WF.DB:GetSetting("uiScale"))
      slider._wfSilent = true
      slider:SetValue(s)
      slider._wfSilent = false
      SyncScaleText(s)
      if slider._wfUpdateFill then slider._wfUpdateFill() end
    end

    Tip(scaleWrap, "UI scale", "Adjusts the overall UI scale for the addon windows.")
    -- Layout / options
    -- Put the layout dropdown on a full row, then expose four configurable metric pill slots.
    AddFull(secWin, compactLayoutRow, 12)
    AddPair(secWin, comboPill1, comboPill2, 12)
    AddPair(secWin, comboPill3, comboPill4, 12)
    AddPair(secWin, cAutoStart, cMini, 12)
    AddFull(secWin, toggleOptionsWrap, 12)
    AddFull(secWin, scaleWrap, 16)

    -- -----------------------------
    -- Pricing
    -- -----------------------------
    local secPrice = NewSection("Pricing", {
      "Controls which price source is used for item values.",
      "TSM options appear when you choose TSM.",
    })

    local tsmCombo -- forward

    local priceWarnWrap -- forward
    local priceWarnText -- forward

    local function SettingsAccentHex()
      local a = ACCENT or { 0.35, 0.78, 1.00, 1.00 }
      local function ch(v)
        v = tonumber(v) or 1
        if v <= 1 then v = v * 255 end
        if v < 0 then v = 0 elseif v > 255 then v = 255 end
        return math.floor(v + 0.5)
      end
      return string.format("%02x%02x%02x", ch(a[1]), ch(a[2]), ch(a[3]))
    end

    local function ColorProviderWarningText(text)
      local hex = SettingsAccentHex()
      local accent = "|cff" .. hex
      text = tostring(text or "")
      text = text:gsub("TradeSkillMaster", accent .. "TradeSkillMaster|r")
      text = text:gsub("Auctionator", accent .. "Auctionator|r")
      text = text:gsub("TSM", accent .. "TSM|r")
      text = text:gsub("Vendor", accent .. "Vendor|r")
      return text
    end

    local function UpdatePricingWidgets()
      local ps = WF.DB:GetSetting("priceSource") or "vendor"

      if tsmCombo then
        tsmCombo:SetShown(ps == "tsm")
      end

      -- Provider warning line
      if priceWarnWrap and priceWarnText and WF and WF.Price and WF.Price.GetProviderWarning then
        local warn = WF.Price:GetProviderWarning(ps)
        if warn and warn ~= "" then
          priceWarnText:SetText(ColorProviderWarningText(warn .. " Enable it and /reload, or switch to Vendor."))
          priceWarnWrap:Show()
        else
          priceWarnWrap:Hide()
        end
      end

      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end

    local comboPrice = MakeCombo(secPrice.content, "Price Source",
      function() return WF.DB:GetSetting("priceSource") or "vendor" end,
function(v)
  WF.DB:SetSetting("priceSource", v)

  if WF.Price and WF.Price.WarnIfProviderUnavailable then
    pcall(WF.Price.WarnIfProviderUnavailable, WF.Price, v, true)
  end
  UpdatePricingWidgets()

  if WF.Tracker and WF.Tracker.RepriceCurrent then
    pcall(WF.Tracker.RepriceCurrent, WF.Tracker, true)
  end

  -- Refresh any visible pages so lists + averages update without reopening.
  if WF and WF.UI then
    local Main = WF.UI.Main
    if Main then
      if Main.RefreshMetrics then pcall(Main.RefreshMetrics, Main) end
      if Main.RefreshLoot then pcall(Main.RefreshLoot, Main, true) end
    end
  end

  if UI then
    if UI.RefreshOverview then pcall(UI.RefreshOverview, UI) end
    if UI.RefreshHistory then pcall(UI.RefreshHistory, UI) end
    if UI.RefreshFarms then pcall(UI.RefreshFarms, UI) end
  end

  -- Update the main window header price label immediately (e.g., DBMinBuyout).
  if WF and WF.UI and WF.UI.Main and WF.UI.Main.RefreshHeaderPriceSource then
    pcall(WF.UI.Main.RefreshHeaderPriceSource, WF.UI.Main)
  end

end,


      function() return PRICE_SOURCES end
    )
    Tip(comboPrice, "Price Source", "Selects what price data the addon uses: Vendor, Auctionator, or TSM.")

    tsmCombo = MakeCombo(secPrice.content, "TSM Price",
      function()
        local s = WF.DB:GetSetting("tsmPriceString") or "dbregionmarketavg"
        if s == "vendorsell" then
          s = "dbregionmarketavg"
          WF.DB:SetSetting("tsmPriceString", s)
        end
        return TSMStringToLabel(s)
      end,
function(label)
  local s = TSM_BY_LABEL[label] or tostring(label or "")
  if s == "" then s = "dbregionmarketavg" end
  WF.DB:SetSetting("tsmPriceString", s)

  if WF.Tracker and WF.Tracker.RepriceCurrent then
    pcall(WF.Tracker.RepriceCurrent, WF.Tracker, true)
  end

  -- Refresh any visible pages so lists + averages update without reopening.
  if WF and WF.UI then
    local Main = WF.UI.Main
    if Main then
      if Main.RefreshMetrics then pcall(Main.RefreshMetrics, Main) end
      if Main.RefreshLoot then pcall(Main.RefreshLoot, Main, true) end
    end
  end

  if UI then
    if UI.RefreshOverview then pcall(UI.RefreshOverview, UI) end
    if UI.RefreshHistory then pcall(UI.RefreshHistory, UI) end
    if UI.RefreshFarms then pcall(UI.RefreshFarms, UI) end
  end

  -- Update the main window header price label immediately (e.g., DBMinBuyout).
  if WF and WF.UI and WF.UI.Main and WF.UI.Main.RefreshHeaderPriceSource then
    pcall(WF.UI.Main.RefreshHeaderPriceSource, WF.UI.Main)
  end

end,
      function() return TSM_LABELS end
    )
    Tip(tsmCombo, "TSM Price", "Select which TSM price string is used when the price source is set to TSM")

    -- Inline warning under Pricing (white text with WhatFarm accent highlights)
    do
      priceWarnWrap = CreateFrame("Frame", nil, secPrice.content)
      priceWarnWrap:SetSize(1, 34)

      priceWarnText = MakeBodyText(priceWarnWrap, "")
      priceWarnText:SetPoint("TOPLEFT", 0, 0)
      priceWarnText:SetPoint("TOPRIGHT", -10, 0)
      priceWarnText:SetJustifyH("LEFT")
      priceWarnText:SetJustifyV("TOP")
      priceWarnText:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
      priceWarnText:SetText("")

      priceWarnWrap._text = priceWarnText
      priceWarnWrap:Hide()
    end


    AddPair(secPrice, comboPrice, tsmCombo, 12)

    AddFull(secPrice, priceWarnWrap, 12)


    -- Initial state
    UpdatePricingWidgets()

    -- Reflow on resize/show
    panel:HookScript("OnSizeChanged", function()
      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end)

    tabGeneral:HookScript("OnShow", function()
      RefreshActiveProfileBadge(true)
      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end)

    -- Initial layout
    RefreshActiveProfileBadge(true)
    ReflowSettingsGeneral()

    pSettings:SetScript("OnShow", function()
      UpdatePricingWidgets()
      ShowSettingsTab(activeSettingsTab)
      if sc and sc._wf_reflow_settings then
        sc._wf_reflow_settings()
      end
    end)

    self._layout.settings = {
      page = pSettings,
      panel = panel,
      scroll = scroll,
      content = sc,
      showTab = ShowSettingsTab,
      activeTab = function()
        return activeSettingsTab
      end,
      sections = { secHistory, secLoot, secNotify, secWin, secPrice },
      expandSection = function(title)
        local needle = tostring(title or ""):lower()
        local found = false
        for i = 1, #SECTIONS do
          local sec = SECTIONS[i]
          if type(sec) == "table" then
            if tostring(sec.title or ""):lower() == needle then
              found = true
            end
          end
        end
        if not found then return end

        -- Save the user's original collapse state before the tutorial first touches it.
        if not self._layout.settings._tutorialSavedCollapse then
          self._layout.settings._tutorialSavedCollapse = {}
          for i = 1, #SECTIONS do
            local sec = SECTIONS[i]
            if type(sec) == "table" and sec.title then
              self._layout.settings._tutorialSavedCollapse[sec.title] = IsCollapsed(sec.title)
            end
          end
        end

        local needsReflow = false
        for i = 1, #SECTIONS do
          local sec = SECTIONS[i]
          if type(sec) == "table" then
            local isTarget = tostring(sec.title or ""):lower() == needle
            if isTarget then
              if IsCollapsed(sec.title) then
                SetCollapsed(sec.title, false)
                needsReflow = true
              end
            else
              if not IsCollapsed(sec.title) then
                SetCollapsed(sec.title, true)
                needsReflow = true
              end
            end
          end
        end
        if needsReflow and sc and sc._wf_reflow_settings then
          sc._wf_reflow_settings()
        end
      end,
      restoreSectionCollapse = function()
        local saved = self._layout.settings._tutorialSavedCollapse
        if not saved then return end
        self._layout.settings._tutorialSavedCollapse = nil
        local needsReflow = false
        for i = 1, #SECTIONS do
          local sec = SECTIONS[i]
          if type(sec) == "table" and sec.title then
            local wasCollapsed = saved[sec.title]
            if wasCollapsed == nil then wasCollapsed = false end
            if IsCollapsed(sec.title) ~= wasCollapsed then
              SetCollapsed(sec.title, wasCollapsed)
              needsReflow = true
            end
          end
        end
        if needsReflow and sc and sc._wf_reflow_settings then
          sc._wf_reflow_settings()
        end
      end,
    }
    SyncSettingsTutorialLayouts()
  end

  -- Sidebar nav
  MakeNavButton("Overview", "Overview", -14)
  MakeNavButton("Farms", "Farms", -46)
  MakeNavButton("History", "History", -78)
  MakeNavButton("Notepad", "Notepad", -110)
  MakeNavButton("Settings", "Settings", -142)

  self.frame = f
  self.pages = pages
  self.currentKey = nil

  self._layout.content = content
  f:SetScript("OnHide", function() UI:CloseDropdown(); UI:CloseContextMenu() end)
  f:SetScript("OnSizeChanged", function(frame, w, h)
    -- AttachCornerLockGrip marks the frame as _wfSizing while the user is dragging.
    -- The old early-return skipped the page layout until mouse-up, so the top Overview
    -- cards stayed in their old 4-wide positions and visibly hung outside the frame.
    if frame and frame._wfSizing then
      UI:_Layout(w, h)
      return
    end
    if UI._wfSizing then return end
    if Widgets and Widgets.SnapSize and w and h then
      local sw, sh = Widgets:SnapSize(frame, w, h)
      if sw ~= w or sh ~= h then
        UI._wfSizing = true
        frame:SetSize(sw, sh)
        UI._wfSizing = nil
        w, h = sw, sh
      end
    end
    UI:_Layout()
    if Widgets and Widgets.RefreshPixelPerfect then
      Widgets:RefreshPixelPerfect(frame)
    end
  end)

  -- Install the global ESC closer once
  self:_EnsureEscDropdownCloser()
end


function UI:RefreshNotepad()
  EnsureSettingsDefaults()
  local L = self._layout and self._layout.notepad
  if not L then return end

  local raw = WF.DB:GetSetting("notepadText") or ""

  if L.noteEdit then
    self._npSetting = true
    if L.noteEdit:GetText() ~= raw then
      L.noteEdit:SetText(raw)
    end
    self._npSetting = nil
  end

  if L.exportEdit then
    -- best-effort refresh; export encoding lives in the page code too
    -- (so we only rebuild when the page is shown)
    L.exportEdit:SetText(L.exportEdit:GetText() or "")
  end
end

function UI:Show(key)
  self:Create()
  -- Always close any open menus before switching/showing pages
  self:CloseDropdown()
  self:CloseContextMenu()
  if self.frame and self.frame.SetFrameStrata then self.frame:SetFrameStrata("HIGH") end
  if self.frame and self.frame.Raise then self.frame:Raise() end
  local wasShown = self.frame and self.frame.IsShown and self.frame:IsShown() or false
  self.frame:Show()

  local haveSaved = false
  if WF and WF.DB and WF.DB.GetSetting then
    local g = WF.DB:GetSetting("helpFrame")
    haveSaved = (type(g) == "table" and g.point) and true or false
  end

  if (not wasShown) and (not haveSaved) and self.EnsureNoOverlap then
    self:EnsureNoOverlap()
  end

  for k, p in pairs(self.pages) do
    if k == key then p:Show() else p:Hide() end
  end

  -- Auto-open the tutorial overlay the first time Help is opened, unless the
  -- user already closed/dismissed it. Choosing Fast/Advanced does not mean the
  -- tutorial is finished, but pressing X should still be respected and should
  -- not make the overlay pop back up every time the addon opens.
  if not wasShown then
    local done = false
    local dismissed = false
    if WF and WF.DB and WF.DB.GetSetting then
      done = WF.DB:GetSetting("tutorialDone") and true or false
      dismissed = WF.DB:GetSetting("tutorialAutoDismissed") and true or false
    end
    if not done and not dismissed and WF and WF.UI and WF.UI.TutorialOverlay and WF.UI.TutorialOverlay.Toggle then
      WF.UI.TutorialOverlay:Toggle(self.frame, (self._layout and self._layout.header) or nil, self, self._tutorialCtx)
    end
  end

  self.currentKey = key

  if key == "Overview" and self.RefreshOverview then self:RefreshOverview() end
  if key == "Farms" and self.RefreshFarms then self:RefreshFarms() end
  if key == "History" and self.RefreshHistory then self:RefreshHistory() end
  if key == "Notepad" and self.RefreshNotepad then self:RefreshNotepad() end
end

function UI:Toggle(key)
  self:Create()
  if self.frame:IsShown() then
    self:CloseDropdown()
    self:CloseContextMenu()
    self.frame:Hide()
  else
    self:Show(key or "Overview")
  end
end
