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


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


-- -------------------------------------------------------
-- Money formatting for header metric chips (always shows g/s/c)
-- (Some formatters drop the 'g' at very large values; we don't.)
-- ------------------------------------------------------- (always shows g/s/c)
-- (Some formatters drop the 'g' at very large values; we don't.)
-- -------------------------------------------------------
local function WF_FormatMoneyLetters(copper)
  copper = tonumber(copper) or 0
  if copper < 0 then copper = 0 end

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

  local function _FmtGold(n)
    n = tonumber(n) or 0
    n = math.floor(n + 0.00001)
    if BreakUpLargeNumbers then
      return BreakUpLargeNumbers(n)
    end

    local str = tostring(n)
    local left, num, right = str:match("^([^%d]*)(%d+)(.-)$")
    num = num or str
    local rev = num:reverse():gsub("(%d%d%d)", "%1,"):reverse()
    rev = rev:gsub("^,", "")
    return (left or "") .. rev .. (right or "")
  end

  return string.format("%sg %ss %sc", _FmtGold(g), tostring(s), tostring(c))
end

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

local function WF_MainColorHex(r, g, b)
  r = math.floor(math.max(0, math.min(1, tonumber(r) or 1)) * 255 + 0.5)
  g = math.floor(math.max(0, math.min(1, tonumber(g) or 1)) * 255 + 0.5)
  b = math.floor(math.max(0, math.min(1, tonumber(b) or 1)) * 255 + 0.5)
  return string.format("%02x%02x%02x", r, g, b)
end

local function WF_MainColorizeNotice(msg)
  msg = tostring(msg or "")
  local accent = Theme and Theme.accent or { 0.35, 0.78, 1.00, 1.00 }
  local blue = WF_MainColorHex(accent[1], accent[2], accent[3])
  local prefix = "What Farm?:"

  if msg:sub(1, #prefix) == prefix then
    local body = msg:sub(#prefix + 1):gsub("^%s+", "")
    if body ~= "" then
      return "|cff" .. blue .. prefix .. "|r |cffffffff" .. body .. "|r"
    end
    return "|cff" .. blue .. prefix .. "|r"
  end

  return "|cff" .. blue .. "What Farm?:|r |cffffffff" .. msg .. "|r"
end

local function WF_RandomFarmToastText(msg)
  return WF_MainColorizeNotice(msg)
end

local function WF_ShowRandomFarmRaidWarning(titleText, detailText)
  local msg = tostring(titleText or "")
  local detail = tostring(detailText or "")
  if detail ~= "" then
    msg = msg .. " • " .. detail
  end

  msg = WF_RandomFarmToastText(msg)

  if RaidNotice_AddMessage and RaidWarningFrame then
    local info = ChatTypeInfo and ChatTypeInfo.RAID_WARNING or nil
    RaidNotice_AddMessage(RaidWarningFrame, msg, info)
    return
  end

  if UIErrorsFrame and UIErrorsFrame.AddMessage then
    UIErrorsFrame:AddMessage(msg, 1, 1, 1)
    return
  end

  print(msg)
end


local function WF_RandomFarmSetting(key, fallback)
  if WF and WF.DB and WF.DB.GetSetting then
    local v = WF.DB:GetSetting(key)
    if v ~= nil then
      return v
    end
  end
  return fallback
end

local function WF_RandomFarmToastStyle()
  local v = tostring(WF_RandomFarmSetting("randomFarmToastStyle", "Banner") or "Banner")
  if v == "Boxed" then
    return "Card"
  end
  if v == "Banner" or v == "Card" or v == "Compact" or v == "Minimal" or v == "Raid Warning" then
    return v
  end
  return "Banner"
end

local function WF_RandomFarmToastSound()
  local v = tostring(WF_RandomFarmSetting("randomFarmToastSound", "None") or "None")
  if v == "None" or v == "Soft" or v == "Click" or v == "Ready Check" or v == "Raid Warning" or v == "Epic Loot" then
    return v
  end
  return "None"
end

local WF_RANDOM_FARM_SOUND_PRESETS = {
  ["Soft"] = { "IG_MAINMENU_OPTION_CHECKBOX_ON", "UI_CHECKBOX_ON" },
  ["Click"] = { "U_CHAT_SCROLL_BUTTON", "IG_MAINMENU_OPTION" },
  ["Ready Check"] = { "READY_CHECK", "PVP_THROUGH_QUEUE" },
  ["Raid Warning"] = { "RAID_WARNING", "ALARM_CLOCK_WARNING_3" },
  ["Epic Loot"] = { "UI_EPICLOOT_TOAST", "UI_LEGENDARY_LOOT_TOAST" },
}

local function WF_PlayRandomFarmToastSound(soundKey)
  soundKey = tostring(soundKey or "None")
  if soundKey == "None" or not PlaySound then
    return
  end

  local list = WF_RANDOM_FARM_SOUND_PRESETS[soundKey]
  if type(list) ~= "table" then
    return
  end

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

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")

local function WF_IsElvUIEnabled()
  if _G and _G.ElvUI then
    return true
  end
  if type(IsAddOnLoaded) == "function" then
    local ok, loaded = pcall(IsAddOnLoaded, "ElvUI")
    if ok and loaded then
      return true
    end
  end
  return false
end

local function WF_FontTemplate(wfName, blizzardName)
  if (not WF_IsElvUIEnabled()) and wfName and _G[wfName] then
    return wfName
  end
  if blizzardName and _G[blizzardName] then
    return blizzardName
  end
  return wfName or blizzardName
end

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

function UI:_RandomFarmToastEllipsize(text, maxChars)
  text = tostring(text or "")
  text = text:gsub("^%s+", ""):gsub("%s+$", "")
  maxChars = tonumber(maxChars) or 80
  if maxChars < 8 then
    maxChars = 8
  end
  if #text <= maxChars then
    return text
  end
  return text:sub(1, maxChars - 3):gsub("%s+$", "") .. "..."
end

function UI:_GetRandomFarmToast()
  if self._randomFarmToast and self._randomFarmToast.frame and self._randomFarmToast.title and self._randomFarmToast.detail then
    return self._randomFarmToast
  end

  if not UIParent or not CreateFrame then
    return nil
  end

  local frame = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  frame:SetSize(720, 52)
  frame:SetPoint("TOP", UIParent, "TOP", 0, -96)
  frame:SetFrameStrata("TOOLTIP")
  frame:SetFrameLevel(9000)
  frame:SetAlpha(0)
  frame:Hide()

  if frame.SetBackdrop then
    frame:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
      insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    frame:SetBackdropColor(0, 0, 0, 0)
    frame:SetBackdropBorderColor(0, 0, 0, 0)
  end

  if frame.SetClampedToScreen then
    frame:SetClampedToScreen(true)
  end

  local title = frame:CreateFontString(nil, "OVERLAY")
  title:SetPoint("TOP", frame, "TOP", 0, -1)
  title:SetWidth(720)
  title:SetJustifyH("CENTER")
  title:SetJustifyV("MIDDLE")
  title:SetFont(DISPLAY_FONT, 17, "OUTLINE")
  if title.SetWordWrap then title:SetWordWrap(false) end
  if title.SetNonSpaceWrap then title:SetNonSpaceWrap(false) end
  if title.SetMaxLines then title:SetMaxLines(1) end

  local detail = frame:CreateFontString(nil, "OVERLAY")
  detail:SetPoint("TOP", title, "BOTTOM", 0, -2)
  detail:SetWidth(720)
  detail:SetJustifyH("CENTER")
  detail:SetJustifyV("MIDDLE")
  detail:SetFont(BODY_FONT, 14, "OUTLINE")
  if detail.SetWordWrap then detail:SetWordWrap(false) end
  if detail.SetNonSpaceWrap then detail:SetNonSpaceWrap(false) end
  if detail.SetMaxLines then detail:SetMaxLines(1) end

  local fadeIn = frame:CreateAnimationGroup()
  local fadeInAlpha = fadeIn:CreateAnimation("Alpha")
  fadeInAlpha:SetFromAlpha(0)
  fadeInAlpha:SetToAlpha(1)
  fadeInAlpha:SetDuration(0.12)
  if fadeInAlpha.SetSmoothing then fadeInAlpha:SetSmoothing("OUT") end
  fadeIn:SetScript("OnFinished", function()
    frame:SetAlpha(1)
  end)

  local fadeOut = frame:CreateAnimationGroup()
  local fadeOutAlpha = fadeOut:CreateAnimation("Alpha")
  fadeOutAlpha:SetFromAlpha(1)
  fadeOutAlpha:SetToAlpha(0)
  fadeOutAlpha:SetDuration(0.28)
  if fadeOutAlpha.SetSmoothing then fadeOutAlpha:SetSmoothing("IN") end
  fadeOut:SetScript("OnFinished", function()
    frame:SetAlpha(0)
    frame:Hide()
  end)

  self._randomFarmToast = {
    frame = frame,
    title = title,
    detail = detail,
    fadeIn = fadeIn,
    fadeOut = fadeOut,
  }

  return self._randomFarmToast
end

function UI:_ShowRandomFarmToast(titleText, detailText, holdSeconds)
  local style = WF_RandomFarmToastStyle()
  local sound = WF_RandomFarmToastSound()
  WF_PlayRandomFarmToastSound(sound)

  if style == "Raid Warning" then
    WF_ShowRandomFarmRaidWarning(titleText, detailText)
    return
  end

  local toast = self:_GetRandomFarmToast()
  if not toast then
    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      local msg = tostring(titleText or "")
      local details = tostring(detailText or "")
      if details ~= "" then
        msg = msg .. " • " .. details
      end
      UIErrorsFrame:AddMessage(WF_MainColorizeNotice(msg), 1, 1, 1)
    end
    return
  end

  local frame = toast.frame
  local title = toast.title
  local detail = toast.detail
  local width = 720

  if GetScreenWidth then
    local screenWidth = tonumber(GetScreenWidth()) or 0
    if screenWidth > 0 then
      width = math.min(720, math.max(420, screenWidth - 80))
    end
  end

  local accent = Theme and Theme.accent or { 0.35, 0.78, 1.00, 1.00 }
  local titleTextClean = tostring(titleText or "")
  local detailTextClean = tostring(detailText or "")
  local combinedText = titleTextClean
  if detailTextClean ~= "" then
    combinedText = combinedText .. " • " .. detailTextClean
  end

  if style == "Compact" then
    width = math.min(width, 620)
    frame:SetSize(width, 34)
    title:SetWidth(width - 24)
    detail:SetWidth(width - 24)
    title:SetFont(DISPLAY_FONT, 15, "OUTLINE")
    title:SetJustifyH("CENTER")
    title:SetTextColor(1, 1, 1, 1)
    title:SetText(WF_RandomFarmToastText(self:_RandomFarmToastEllipsize(combinedText, 86)))
    title:ClearAllPoints()
    title:SetPoint("CENTER", frame, "CENTER", 0, 0)
    detail:SetText("")
    detail:Hide()
    if frame.SetBackdropColor then frame:SetBackdropColor(0.02, 0.025, 0.032, 0.88) end
    if frame.SetBackdropBorderColor then frame:SetBackdropBorderColor(accent[1] or 0.35, accent[2] or 0.78, accent[3] or 1.00, 0.55) end
  elseif style == "Card" then
    width = math.min(width, 520)
    frame:SetSize(width, 48)
    title:SetWidth(width - 32)
    detail:SetWidth(width - 32)
    title:SetFont(DISPLAY_FONT, 15, "OUTLINE")
    detail:SetFont(BODY_FONT, 12, "OUTLINE")
    title:SetJustifyH("CENTER")
    detail:SetJustifyH("CENTER")
    title:SetTextColor(1, 1, 1, 1)
    detail:SetTextColor(0.92, 0.92, 0.92, 1)
    title:SetText(WF_RandomFarmToastText(self:_RandomFarmToastEllipsize(titleTextClean, 54)))
    detail:SetText(self:_RandomFarmToastEllipsize(detailTextClean, 72))
    title:ClearAllPoints()
    title:SetPoint("TOP", frame, "TOP", 0, -6)
    detail:ClearAllPoints()
    detail:SetPoint("TOP", title, "BOTTOM", 0, -2)
    if detailTextClean == "" then
      frame:SetHeight(34)
      title:ClearAllPoints()
      title:SetPoint("CENTER", frame, "CENTER", 0, 0)
      detail:Hide()
    else
      detail:Show()
    end
    if frame.SetBackdropColor then frame:SetBackdropColor(0.015, 0.018, 0.026, 0.78) end
    if frame.SetBackdropBorderColor then frame:SetBackdropBorderColor(accent[1] or 0.35, accent[2] or 0.78, accent[3] or 1.00, 0.22) end
  elseif style == "Minimal" then
    width = math.min(width, 680)
    frame:SetSize(width, 28)
    title:SetWidth(width)
    detail:SetWidth(width)
    title:SetFont(BODY_FONT, 15, "OUTLINE")
    title:SetJustifyH("CENTER")
    title:SetTextColor(1, 1, 1, 1)
    title:SetText(WF_RandomFarmToastText(self:_RandomFarmToastEllipsize(combinedText, 98)))
    title:ClearAllPoints()
    title:SetPoint("CENTER", frame, "CENTER", 0, 0)
    detail:SetText("")
    detail:Hide()
    if frame.SetBackdropColor then frame:SetBackdropColor(0, 0, 0, 0) end
    if frame.SetBackdropBorderColor then frame:SetBackdropBorderColor(0, 0, 0, 0) end
  else
    frame:SetSize(width, 52)
    title:SetWidth(width)
    detail:SetWidth(width)
    title:SetFont(DISPLAY_FONT, 17, "OUTLINE")
    detail:SetFont(BODY_FONT, 14, "OUTLINE")
    title:SetJustifyH("CENTER")
    detail:SetJustifyH("CENTER")
    title:SetTextColor(1, 1, 1, 1)
    detail:SetTextColor(0.92, 0.92, 0.92, 1)
    title:SetText(WF_RandomFarmToastText(self:_RandomFarmToastEllipsize(titleTextClean, 62)))
    detailTextClean = self:_RandomFarmToastEllipsize(detailTextClean, 92)
    detail:SetText(detailTextClean)

    if detailTextClean == "" then
      frame:SetHeight(30)
      title:ClearAllPoints()
      title:SetPoint("CENTER", frame, "CENTER", 0, 0)
      detail:Hide()
    else
      frame:SetHeight(52)
      title:ClearAllPoints()
      title:SetPoint("TOP", frame, "TOP", 0, -1)
      detail:ClearAllPoints()
      detail:SetPoint("TOP", title, "BOTTOM", 0, -2)
      detail:Show()
    end
    if frame.SetBackdropColor then frame:SetBackdropColor(0, 0, 0, 0) end
    if frame.SetBackdropBorderColor then frame:SetBackdropBorderColor(0, 0, 0, 0) end
  end

  frame:ClearAllPoints()
  frame:SetPoint("TOP", UIParent, "TOP", 0, -96)

  frame._wfToastSerial = (frame._wfToastSerial or 0) + 1
  local serial = frame._wfToastSerial

  if toast.fadeIn and toast.fadeIn.Stop then toast.fadeIn:Stop() end
  if toast.fadeOut and toast.fadeOut.Stop then toast.fadeOut:Stop() end

  frame:SetAlpha(0)
  frame:Show()

  if toast.fadeIn and toast.fadeIn.Play then
    toast.fadeIn:Play()
  else
    frame:SetAlpha(1)
  end

  holdSeconds = tonumber(holdSeconds) or 1.55
  if holdSeconds < 0.8 then
    holdSeconds = 0.8
  end

  if C_Timer and C_Timer.After then
    C_Timer.After(holdSeconds, function()
      if not frame or frame._wfToastSerial ~= serial then
        return
      end
      if toast.fadeOut and toast.fadeOut.Play then
        toast.fadeOut:Play()
      else
        frame:SetAlpha(0)
        frame:Hide()
      end
    end)
  end
end

-- ---------------------------------------------------------
-- Layout modes
-- ---------------------------------------------------------
local MAIN_LAYOUTS = { "Normal", "Compact" }
-- Anchor validation (keeps saved points sane; avoids bad saved data breaking layout)
local VALID_ANCHOR_POINTS = {
  TOPLEFT = true, TOP = true, TOPRIGHT = true,
  LEFT = true, CENTER = true, RIGHT = true,
  BOTTOMLEFT = true, BOTTOM = true, BOTTOMRIGHT = true,
}

local function NormalizeLayout(v)
  if not v then return "Normal" end
  v = tostring(v)
  local lv = string.lower(v)
  for i = 1, #MAIN_LAYOUTS do
    if string.lower(MAIN_LAYOUTS[i]) == lv then
      return MAIN_LAYOUTS[i]
    end
  end
  -- allow legacy values like "normal"
  if lv == "normal" then return "Normal" end
  if lv == "compact" then return "Compact" end
  if lv == "compare" then return "Compare" end
  if lv == "competitive" then return "Competitive" end
  return "Normal"
end

-- ---------------------------------------------------------
-- Per-mode min size (allows ultra-tight Compact)
-- ---------------------------------------------------------
local function ModeMin(mode)
  mode = NormalizeLayout(mode)

  -- Per-layout minimums (prevents "dumb" shrink states where UI overlaps).
  -- Tune these first before changing any anchors/padding.
  if mode == "Compact" then
    return 520, 230  -- tight Compact; always opens at min size
  elseif mode == "Normal" then
    return 820, 420  -- prevents bottom bar overlap (Start/Pause/Reset/Random/Save/ResetDungeon + Help)
  elseif mode == "Compare" then
    return 960, 520
  elseif mode == "Competitive" then
    return 960, 520
  end

  return 820, 420
end


-- ---------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------
local function SecondsToClock(sec)
  sec = tonumber(sec) or 0
  local h = math.floor(sec / 3600)
  local m = math.floor((sec % 3600) / 60)
  local s = math.floor(sec % 60)
  if h > 0 then
    return string.format("%dh %02dm %02ds", h, m, s)
  end
  return string.format("%dm %02ds", m, s)
end

local function RaceTimerClock(sec)
  sec = tonumber(sec) or 0
  if sec < 0 then sec = 0 end
  sec = math.floor(sec + 0.0001)

  local h = math.floor(sec / 3600)
  local m = math.floor((sec % 3600) / 60)
  local s = math.floor(sec % 60)

  if h > 0 then
    return string.format("%dh %02dm", h, m)
  end
  return string.format("%dm %02ds", m, s)
end

local function RaceTimerPromptText(sec)
  sec = tonumber(sec) or 0
  if sec <= 0 then
    return "30m"
  end

  local h = math.floor(sec / 3600)
  local m = math.floor((sec % 3600) / 60)
  local s = math.floor(sec % 60)

  local parts = {}
  if h > 0 then parts[#parts + 1] = tostring(h) .. "h" end
  if m > 0 then parts[#parts + 1] = tostring(m) .. "m" end
  if s > 0 and h == 0 then parts[#parts + 1] = tostring(s) .. "s" end
  if #parts == 0 then return "30m" end
  return table.concat(parts, " ")
end

local function ParseRaceTimerText(text)
  text = tostring(text or ""):lower()
  text = text:gsub(",", " "):gsub("^%s+", ""):gsub("%s+$", "")

  if text == "" then
    return nil, "Enter a time like 30m, 45, 1h 30m, or 01:30."
  end

  if text == "off" or text == "none" or text == "clear" or text == "disable" or text == "0" or text == "0m" then
    return 0
  end

  local colonParts = {}
  for part in text:gmatch("[^:]+") do
    part = part:gsub("^%s+", ""):gsub("%s+$", "")
    if part == "" or not tonumber(part) then
      colonParts = nil
      break
    end
    colonParts[#colonParts + 1] = tonumber(part)
  end
  if colonParts and #colonParts >= 2 and #colonParts <= 3 then
    local seconds = 0
    if #colonParts == 2 then
      seconds = (colonParts[1] * 3600) + (colonParts[2] * 60)
    else
      seconds = (colonParts[1] * 3600) + (colonParts[2] * 60) + colonParts[3]
    end
    seconds = math.floor(seconds + 0.0001)
    if seconds <= 0 then return 0 end
    if seconds > 86400 then
      return nil, "Race timer max is 24 hours."
    end
    return seconds
  end

  local total = 0
  local matched = false
  for num, unit in text:gmatch("([%d%.]+)%s*([hms])") do
    local n = tonumber(num)
    if not n then
      return nil, "Could not read that time."
    end
    matched = true
    if unit == "h" then
      total = total + (n * 3600)
    elseif unit == "m" then
      total = total + (n * 60)
    elseif unit == "s" then
      total = total + n
    end
  end

  if matched then
    local stripped = text:gsub("[%d%.]+%s*[hms]", ""):gsub("%s+", "")
    if stripped ~= "" then
      return nil, "Use a time like 30m, 45, 1h 30m, or 01:30."
    end
    total = math.floor(total + 0.0001)
    if total <= 0 then return 0 end
    if total > 86400 then
      return nil, "Race timer max is 24 hours."
    end
    return total
  end

  local plain = tonumber(text)
  if plain then
    local seconds = math.floor((plain * 60) + 0.0001)
    if seconds <= 0 then return 0 end
    if seconds > 86400 then
      return nil, "Race timer max is 24 hours."
    end
    return seconds
  end

  return nil, "Use a time like 30m, 45, 1h 30m, or 01:30."
end

local function SessionGPH(elapsed, totalValue)
  elapsed = tonumber(elapsed) or 0
  if elapsed <= 0 then return 0 end
  totalValue = tonumber(totalValue) or 0
  return totalValue / elapsed * 3600
end

local METRIC_PILL_OPTIONS = {
  "Time",
  "Looted Gold",
  "Looted Items",
  "Looted Value",
  "Estimated GPH",
  "Total Value",
  "Race Timer",
  "Hidden",
}

local METRIC_PILL_DEFAULTS = {
  [1] = "Time",
  [2] = "Looted Gold",
  [3] = "Looted Items",
  [4] = "Hidden",
}

local METRIC_PILL_LABELS = {
  ["Time"] = "Time:",
  ["Looted Gold"] = "Looted Gold:",
  ["Looted Items"] = "Looted Items:",
  ["Looted Value"] = "Looted Value:",
  ["Estimated GPH"] = "Est. GPH:",
  ["Total Value"] = "Total Value:",
  ["Race Timer"] = "Race:",
  ["Hidden"] = "",
}

local METRIC_PILL_WIDTHS = {
  ["Time"] = { min = 92, max = 126 },
  ["Looted Gold"] = { min = 132, max = 250 },
  ["Looted Items"] = { min = 120, max = 250 },
  ["Looted Value"] = { min = 120, max = 250 },
  ["Estimated GPH"] = { min = 138, max = 260 },
  ["Total Value"] = { min = 128, max = 250 },
  ["Race Timer"] = { min = 116, max = 174 },
  ["Hidden"] = { min = 0, max = 0 },
}

local function NormalizeMetricPill(value, slot)
  local fallback = METRIC_PILL_DEFAULTS[tonumber(slot) or 0] or "Hidden"
  if value == nil then return fallback end

  local raw = tostring(value)
  if raw == "" then return fallback end

  for i = 1, #METRIC_PILL_OPTIONS do
    if METRIC_PILL_OPTIONS[i] == raw then
      return METRIC_PILL_OPTIONS[i]
    end
  end

  local key = string.lower(raw):gsub("[%s_%-]", "")
  if key == "time" then return "Time" end
  if key == "lootedgold" or key == "gold" then return "Looted Gold" end
  if key == "looteditems" or key == "items" or key == "itemcount" or key == "count" then return "Looted Items" end
  if key == "lootedvalue" or key == "looteditemvalue" or key == "itemvalue" or key == "liv" or key == "livvalue" or key == "itemliv" then return "Looted Value" end
  if key == "estimatedgph" or key == "gph" then return "Estimated GPH" end
  if key == "totalvalue" or key == "value" then return "Total Value" end
  if key == "racetimer" or key == "race" or key == "countdown" or key == "farmtimer" then return "Race Timer" end
  if key == "kills" then return "Hidden" end
  if key == "hidden" or key == "off" or key == "none" then return "Hidden" end

  return fallback
end

local function GetMetricPillWidth(metric)
  return METRIC_PILL_WIDTHS[metric] or { min = 96, max = 220 }
end

local function GetMetricPillSetting(slot)
  if WF and WF.DB and WF.DB.GetSetting then
    return NormalizeMetricPill(WF.DB:GetSetting("mainWindowPill" .. tostring(slot)), slot)
  end
  return METRIC_PILL_DEFAULTS[tonumber(slot) or 0] or "Hidden"
end

local function GetMetricPillLabel(metric)
  return METRIC_PILL_LABELS[metric] or (tostring(metric or "Metric") .. ":")
end

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

local function SafeNow()
  if GetServerTime then
    return GetServerTime()
  end
  return time()
end


-- ---------------------------------------------------------
-- Tooltip "secret value" safety
-- Some WoW builds can return protected/tainted numeric proxy values (shows as <no value> in errors).
-- Any arithmetic/compare on those can explode with: "attempt to compare ... (a secret value)".
-- This coerces to a plain Lua number (or nil if it can't).
-- ---------------------------------------------------------
local function WF_SafePlainNumber(v)
  if v == nil then return nil end

  local t = type(v)
  if t == "number" then
    return v
  end

  if t == "string" then
    if v == "" then return nil end
    local n = tonumber(v)
    if type(n) == "number" then
      return n
    end
  end

  return nil
end


-- ---------------------------------------------------------
-- Blizzard MoneyFrame "secret value" crash guard
--
-- In some UI states (and especially when multiple tooltip addons are enabled),
-- Blizzard_MoneyFrame can throw errors like:
--   "attempt to perform arithmetic/compare on a secret value"
-- inside MoneyFrame_Update / SetTooltipMoney. When that happens, tooltip building
-- aborts and you lose comparison tooltips.
--
-- We defensively wrap those globals in pcall (and coerce the money argument back
-- to a plain number when possible) so the rest of the tooltip can still render.
-- NOTE: We intentionally do NOT monkeypatch Blizzard money/tooltip globals.
-- Doing so can taint tooltip money frames and cause "secret value" errors or broken sell-price layout.

-- Safe wrapper: SetHyperlink can sometimes explode via Blizzard tooltip money ("secret value") paths.
-- We treat that as "tooltip unavailable" instead of hard-erroring the addon.
local function WF_Tip_TrySetHyperlink(link)
  if not (GameTooltip and link) then return false end
  if Util and Util.TrySetHyperlinkCached then
    local ok = Util:TrySetHyperlinkCached(GameTooltip, link)
    return ok and true or false
  end
  if GameTooltip.SetHyperlink then
    local ok = pcall(GameTooltip.SetHyperlink, GameTooltip, link)
    if ok then
      return true
    end
  end
  return false
end



-- ---------------------------------------------------------
-- Tooltip anchoring helpers (smart above/below; avoids cursor clamping)
-- ---------------------------------------------------------
local function WF_Tip_AnchorSide(owner, preferLeft, xOff, yOff)
  if not (GameTooltip and owner) then return end

  xOff = tonumber(xOff) or 12
  yOff = tonumber(yOff) or 0

  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(owner, "ANCHOR_NONE")
  GameTooltip:ClearAllPoints()

  -- Seed a sane default (ABOVE). We'll finalize after content is added
  -- so we can measure tooltip size and pick ABOVE vs BELOW cleanly.
  local yAbove = 8 + yOff
  if preferLeft then
    GameTooltip:SetPoint("BOTTOMRIGHT", owner, "TOPLEFT", -xOff, yAbove)
  else
    GameTooltip:SetPoint("BOTTOMLEFT", owner, "TOPRIGHT", xOff, yAbove)
  end
end

local function WF_Tip_FinalizeSide(owner, preferLeft)
  if not (GameTooltip and owner) then return end

  local xOff = 12
  local yAbove = 8
  local yBelow = -10

  GameTooltip:ClearAllPoints()

  if preferLeft then
    GameTooltip:SetPoint("BOTTOMRIGHT", owner, "TOPLEFT", -xOff, yAbove)
  else
    GameTooltip:SetPoint("BOTTOMLEFT", owner, "TOPRIGHT", xOff, yAbove)
  end

  GameTooltip:Show()
end


-- -------------------------------------------------------
-- Tooltip anchoring helpers (centered; keeps tooltip near the control)
-- -------------------------------------------------------
local function WF_Tip_AnchorCentered(owner)
  if not (GameTooltip and owner) then return end
  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(owner, "ANCHOR_NONE")
  GameTooltip:ClearAllPoints()
end
local function WF_Tip_FinalizeCentered(owner, anchor, preferBelow)
  if not (GameTooltip and owner) then return end

  anchor = anchor or owner
  if not anchor then return end

  local yAbove = 10
  local yBelow = -12

  GameTooltip:ClearAllPoints()
  if preferBelow then
    GameTooltip:SetPoint("TOP", anchor, "BOTTOM", 0, yBelow)
  else
    GameTooltip:SetPoint("BOTTOM", anchor, "TOP", 0, yAbove)
  end
  GameTooltip:Show()
end


-- ---------------------------------------------------------
-- Pause support (UI-driven; no Tracker file edits required)
-- --------------------------------------------------------- (UI-driven; no Tracker file edits required)
-- ---------------------------------------------------------
local function RealTrackerIsActive()
  if not Tracker or not Tracker.IsActive then return false end
  if Tracker._wfOrigIsActive then
    return Tracker._wfOrigIsActive(Tracker)
  end
  return Tracker:IsActive()
end

local function PauseClockNow()
  return GetTime()
end

function UI:IsPaused()
  return self._paused and true or false
end

function UI:_SyncPauseStateToSession()
  local sess = Tracker and Tracker.current
  if not (sess and sess.active) then
    return
  end

  sess._wfPausedTotal = tonumber(self._pausedTotal) or 0
  sess._wfPaused = self:IsPaused() and true or false

  if sess._wfPaused then
    local frozen = tonumber(self._pausedElapsed)
    if frozen and frozen >= 0 then
      sess._wfPausedElapsed = frozen
    else
      sess._wfPausedElapsed = nil
    end
  else
    sess._wfPausedElapsed = nil
  end
end

function UI:_RestorePauseStateFromSession()
  local sess = Tracker and Tracker.current
  if not (sess and sess.active) then
    self._paused = false
    self._pauseStartedAt = nil
    self._pausedElapsed = nil
    self._pausedTotal = 0
    self:StopPauseFlash()
    self:_ApplyTimerStateVisuals()
    return
  end

  self._pausedTotal = tonumber(sess._wfPausedTotal) or 0
  self._paused = sess._wfPaused and true or false

  if self._paused then
    local frozen = tonumber(sess._wfPausedElapsed)
    if not frozen or frozen < 0 then
      local now = PauseClockNow()
      frozen = now - (tonumber(sess.startTime) or now) - self._pausedTotal
      if frozen < 0 then frozen = 0 end
    end
    self._pausedElapsed = frozen
    self._pauseStartedAt = PauseClockNow()
  else
    self._pauseStartedAt = nil
    self._pausedElapsed = nil
  end

  self:StopPauseFlash()
  self:_ApplyTimerStateVisuals()
end

function UI:_EnsurePauseHooks()
  if self._pauseHooksReady then return end
  self._pauseHooksReady = true

  if Tracker and type(Tracker.IsActive) == "function" and not Tracker._wfOrigIsActive then
    Tracker._wfOrigIsActive = Tracker.IsActive
    Tracker.IsActive = function(t, ...)
      if UI and UI.IsPaused and UI:IsPaused() then
        return false
      end
      return Tracker._wfOrigIsActive(t, ...)
    end
  end

  if Tracker and type(Tracker.GetElapsed) == "function" and not Tracker._wfOrigGetElapsed then
    Tracker._wfOrigGetElapsed = Tracker.GetElapsed
    Tracker.GetElapsed = function(t, ...)
      local e = Tracker._wfOrigGetElapsed(t, ...)
      e = tonumber(e) or 0

      local pausedTotal = tonumber(UI and UI._pausedTotal) or 0
      e = e - pausedTotal
      if e < 0 then e = 0 end

      if UI and UI.IsPaused and UI:IsPaused() then
        return tonumber(UI._pausedElapsed) or e
      end
      return e
    end
  end
end

function UI:_GetTimeMetricChips()
  return self._timeMetricChips or {}
end

function UI:_ResetMetricChipVisuals()
  local chips = self.metricChips or {}
  for i = 1, #chips do
    local chip = chips[i]
    if chip and chip.value then
      chip.value:SetAlpha(1)
      chip.value:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
    end
  end
end

function UI:StartPauseFlash()
  if self._pauseFlash then return end

  local timeChips = self:_GetTimeMetricChips()
  if #timeChips == 0 then return end

  local pause = Theme.accent or Theme.pause
  local dim = {
    pause[1] * 0.70,
    pause[2] * 0.70,
    pause[3] * 0.70,
    1.00,
  }

  local function ApplyColor(color)
    for i = 1, #timeChips do
      local chip = timeChips[i]
      if chip and chip.value then
        chip.value:SetAlpha(1)
        chip.value:SetTextColor(color[1], color[2], color[3], color[4])
      end
    end
  end

  local on = true
  ApplyColor(pause)

  self._pauseFlash = C_Timer.NewTicker(0.5, function()
    if not UI.frame or not UI.frame:IsShown() then
      return
    end
    if not UI:IsPaused() then
      UI:StopPauseFlash()
      return
    end

    local currentTimeChips = UI:_GetTimeMetricChips()
    if #currentTimeChips == 0 then
      UI:StopPauseFlash()
      return
    end

    on = not on
    ApplyColor(on and pause or dim)
  end)
end

-- Timer visuals. Called by RefreshMetrics / Pause / Resume.

function UI:_ApplyTimerStateVisuals()
  local timeChips = self:_GetTimeMetricChips()
  if #timeChips == 0 then
    self:StopPauseFlash()
    self._wfTimerVisualKey = "none"
    return
  end

  local isPaused = self.IsPaused and self:IsPaused()
  local isActive = RealTrackerIsActive()

  -- Paused: pulse color (no alpha flicker).
  if isPaused then
    self._wfTimerVisualKey = "paused"
    for i = 1, #timeChips do
      local chip = timeChips[i]
      if chip and chip.value then
        chip.value:SetAlpha(1)
      end
    end
    self:StartPauseFlash()
    return
  end

  -- Not paused: ensure pulse is stopped and apply stable state colors.
  self:StopPauseFlash()

  local key = isActive and "active" or "idle"
  if self._wfTimerVisualKey == key then
    return
  end
  self._wfTimerVisualKey = key

  local color = Theme.muted
  if key == "active" then
    color = Theme.accent
  end

  for i = 1, #timeChips do
    local chip = timeChips[i]
    if chip and chip.value then
      chip.value:SetAlpha(1)
      chip.value:SetTextColor(color[1], color[2], color[3], color[4])
    end
  end
end

function UI:StopPauseFlash()
  if self._pauseFlash then
    self._pauseFlash:Cancel()
    self._pauseFlash = nil
  end

  local chips = self.metricChips or self:_GetTimeMetricChips()
  for i = 1, #chips do
    local chip = chips[i]
    if chip and chip.value then
      chip.value:SetAlpha(1)
    end
  end
end

function UI:_ClampRaceTimerSeconds(seconds)
  seconds = tonumber(seconds) or 0
  if seconds < 0 then seconds = 0 end
  if seconds > 86400 then seconds = 86400 end
  return math.floor(seconds + 0.0001)
end

function UI:_GetStoredRaceTimerSeconds()
  local v = 0
  if WF and WF.DB and WF.DB.GetSetting then
    v = tonumber(WF.DB:GetSetting("mainWindowRaceTimerSeconds")) or 0
  end
  return self:_ClampRaceTimerSeconds(v)
end

function UI:_GetRaceTimerCompetitiveContext()
  local sid = tostring(self._compSessionId or "")
  if sid == "" then
    return false, "idle", false, ""
  end

  local sessState = tostring((self._compSession and self._compSession.state) or "idle")
  if sessState == "ended" then
    return false, sessState, false, sid
  end

  local leader = tostring(self._compLeader or "")
  if sessState == "idle" and leader == "" then
    return false, sessState, false, sid
  end

  local isLeader = false
  if self._IsCompetitiveLeader then
    isLeader = self:_IsCompetitiveLeader() and true or false
  end

  return true, sessState, isLeader, sid
end

function UI:GetRaceTimerSeconds()
  local inComp, _, isLeader = self:_GetRaceTimerCompetitiveContext()
  if inComp and not isLeader then
    return self:_ClampRaceTimerSeconds(self._compRaceTimerSeconds)
  end

  return self:_GetStoredRaceTimerSeconds()
end

function UI:_BroadcastCompetitiveRaceTimer(seconds, force)
  local inComp, _, isLeader, sid = self:_GetRaceTimerCompetitiveContext()
  if not inComp or not isLeader or sid == "" then
    return
  end
  if not self._SendCompetitivePeerMessage then
    return
  end

  seconds = self:_ClampRaceTimerSeconds(seconds ~= nil and seconds or self:_GetStoredRaceTimerSeconds())
  self._compRaceTimerSeconds = seconds

  local now = SafeNow()
  if not force then
    local lastSeconds = tonumber(self._compLastRaceTimerSentSeconds)
    local lastAt = tonumber(self._compLastRaceTimerSentAt) or 0
    if lastSeconds == seconds and (now - lastAt) < 5 then
      return
    end
  end

  self._compLastRaceTimerSentSeconds = seconds
  self._compLastRaceTimerSentAt = now
  self:_SendCompetitivePeerMessage("RTIMER|" .. sid .. "|" .. tostring(seconds) .. "||")
end

function UI:_ApplyCompetitiveRaceTimerSync(seconds)
  local inComp, _, isLeader = self:_GetRaceTimerCompetitiveContext()
  if not inComp or isLeader then
    return
  end

  self._compRaceTimerSeconds = self:_ClampRaceTimerSeconds(seconds)
  self._raceTimerStopQueued = false
  self._raceTimerStopNotified = false

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

function UI:SetRaceTimerSeconds(seconds)
  seconds = self:_ClampRaceTimerSeconds(seconds)

  if WF and WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("mainWindowRaceTimerSeconds", seconds)
  end

  self._raceTimerStopQueued = false
  self._raceTimerStopNotified = false

  if self._BroadcastCompetitiveRaceTimer then
    self:_BroadcastCompetitiveRaceTimer(seconds, true)
  end

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

function UI:_RaceTimerElapsedSeconds()
  local inComp, sessState = self:_GetRaceTimerCompetitiveContext()
  if inComp and (sessState == "countdown" or sessState == "live" or sessState == "paused") and self._GetCompetitiveSessionElapsed then
    local compElapsed = tonumber(self:_GetCompetitiveSessionElapsed()) or 0
    if compElapsed < 0 then compElapsed = 0 end
    return math.floor(compElapsed + 0.0001)
  end

  local active = RealTrackerIsActive()
  local paused = self.IsPaused and self:IsPaused()
  if not active and not paused then
    return 0
  end

  local elapsed = 0
  if Tracker and Tracker.GetElapsed then
    elapsed = tonumber(Tracker:GetElapsed()) or 0
  end
  if elapsed < 0 then elapsed = 0 end
  return math.floor(elapsed + 0.0001)
end

function UI:_RaceTimerRemainingSeconds()
  local duration = self:GetRaceTimerSeconds()
  if duration <= 0 then
    return nil
  end

  local remaining = duration - self:_RaceTimerElapsedSeconds()
  if remaining < 0 then remaining = 0 end
  return math.floor(remaining + 0.0001)
end

function UI:_RaceTimerValueText()
  local duration = self:GetRaceTimerSeconds()
  if duration <= 0 then
    return "Set Time"
  end

  local remaining = self:_RaceTimerRemainingSeconds()
  if remaining == nil then
    return RaceTimerClock(duration)
  end
  return RaceTimerClock(remaining)
end

function UI:_HasVisibleRaceTimerPill()
  local chips = self._raceTimerMetricChips or {}
  for i = 1, #chips do
    local chip = chips[i]
    if chip and chip.IsShown and chip:IsShown() then
      return true
    end
  end
  return false
end

function UI:_ShowRaceTimerMessage(msg, r, g, b)
  msg = tostring(msg or "")
  if msg == "" then return end

  if UIErrorsFrame and UIErrorsFrame.AddMessage then
    UIErrorsFrame:AddMessage(WF_MainColorizeNotice("What Farm?: " .. msg), r or 1, g or 1, b or 1, 1)
  else
    print("What Farm?: " .. msg)
  end
end

function UI:OpenRaceTimerPrompt()
  local inComp, _, isLeader = self:_GetRaceTimerCompetitiveContext()
  if inComp and not isLeader then
    self:_ShowRaceTimerMessage("Only the host can set the Race Timer during Competitive.")
    return
  end

  if not (Widgets and Widgets.OpenTextPrompt) then
    self:_ShowRaceTimerMessage("Race timer prompt unavailable.")
    return
  end

  local current = self:GetRaceTimerSeconds()
  Widgets:OpenTextPrompt(self, "_raceTimerPrompt", {
    title = "Set Race Timer",
    body = "Enter a farm length. Plain numbers are minutes. Use 0, off, or clear to disable. Examples: 30, 45m, 1h 30m, 01:30.",
    defaultText = RaceTimerPromptText(current),
    okText = "Set Timer",
    cancelText = "Cancel",
    width = 460,
    height = 186,
    inputY = -94,
    maxLetters = 24,
    onOK = function(text)
      local seconds, err = ParseRaceTimerText(text)
      if not seconds then
        UI:_ShowRaceTimerMessage(err or "Could not read that race timer.")
        return
      end

      local active = RealTrackerIsActive() or (UI.IsPaused and UI:IsPaused())
      if active and seconds > 0 then
        local elapsed = UI:_RaceTimerElapsedSeconds()
        if seconds <= elapsed then
          UI:_ShowRaceTimerMessage("Race timer must be longer than the current run time.")
          return
        end
      end

      UI:SetRaceTimerSeconds(seconds)
      if seconds <= 0 then
        UI:_ShowRaceTimerMessage("Race timer disabled.", 0.35, 0.78, 1.00)
      else
        UI:_ShowRaceTimerMessage("Race timer set to " .. RaceTimerClock(seconds) .. ".", 0.35, 0.78, 1.00)
      end
    end,
  })
end

function UI:_EndFarmFromRaceTimer()
  local inComp, sessState, isLeader = self:_GetRaceTimerCompetitiveContext()
  if inComp then
    if not isLeader then
      self._raceTimerStopQueued = false
      return
    end

    if sessState == "live" or sessState == "paused" then
      self._raceTimerStopQueued = false
      self._raceTimerStopNotified = true
      if self.QuitCompetitiveMatch then
        self:QuitCompetitiveMatch()
        self:_ShowRaceTimerMessage("Race timer finished. Competitive match ended for everyone.", 0.35, 0.78, 1.00)
      end
      return
    end

    self._raceTimerStopQueued = false
    return
  end

  if not (RealTrackerIsActive() or (self.IsPaused and self:IsPaused())) then
    self._raceTimerStopQueued = false
    return
  end

  if self._DoStopHard then
    self._DoStopHard()
  elseif Tracker and Tracker.Stop then
    Tracker:Stop(true)
    if self.RefreshTitle then self:RefreshTitle() end
    if self.RefreshMetrics then self:RefreshMetrics() end
    if self.RefreshLoot then self:RefreshLoot(true) end
  end

  self._raceTimerStopQueued = false
  self._raceTimerStopNotified = true
  self:_ShowRaceTimerMessage("Race timer finished. Farm ended.", 0.35, 0.78, 1.00)
end

function UI:_CheckRaceTimerAutoStop()
  if self._raceTimerStopQueued then
    return
  end

  local inComp, sessState, isLeader = self:_GetRaceTimerCompetitiveContext()
  if inComp then
    if not isLeader then
      return
    end
    if sessState ~= "live" and sessState ~= "paused" then
      return
    end
  end

  local duration = self:GetRaceTimerSeconds()
  if duration <= 0 then
    return
  end

  if not inComp and not (RealTrackerIsActive() or (self.IsPaused and self:IsPaused())) then
    return
  end

  if not (self._HasVisibleRaceTimerPill and self:_HasVisibleRaceTimerPill()) then
    return
  end

  local remaining = self:_RaceTimerRemainingSeconds()
  if remaining == nil or remaining > 0 then
    return
  end

  self._raceTimerStopQueued = true
  if C_Timer and C_Timer.After then
    C_Timer.After(0, function()
      if UI and UI._EndFarmFromRaceTimer then
        UI:_EndFarmFromRaceTimer()
      end
    end)
  else
    self:_EndFarmFromRaceTimer()
  end
end

function UI:ClearPauseState(resetTotals)
  self._paused = false
  self._pauseStartedAt = nil
  self._pausedElapsed = nil
  if resetTotals then
    self._pausedTotal = 0
    self._raceTimerStopQueued = false
    self._raceTimerStopNotified = false
  end
  self:_SyncPauseStateToSession()
  self:StopPauseFlash()
end

function UI:FinalizePauseAccounting()
  if self._paused and self._pauseStartedAt then
    local now = PauseClockNow()
    local started = tonumber(self._pauseStartedAt) or now
    local delta = now - started
    if delta > 0 then
      self._pausedTotal = (tonumber(self._pausedTotal) or 0) + delta
    end
  end
  self._paused = false
  self._pauseStartedAt = nil
  self._pausedElapsed = nil
  self:_SyncPauseStateToSession()
  self:StopPauseFlash()
end

function UI:Pause()
  if self:IsPaused() then return end
  if not RealTrackerIsActive() then return end

  self._pausedTotal = tonumber(self._pausedTotal) or 0
  self._pausedElapsed = Tracker:GetElapsed() -- already pause-adjusted pre-pause
  self._pauseStartedAt = PauseClockNow()
  self._paused = true
  self:_SyncPauseStateToSession()

  -- No flashing; timer color indicates paused.
  self:StopPauseFlash()
  self:_ApplyTimerStateVisuals()
  self:RefreshMetrics()
end

function UI:Resume()
  if not self:IsPaused() then return end

  local now = PauseClockNow()
  local started = tonumber(self._pauseStartedAt) or now
  local delta = now - started
  if delta > 0 then
    self._pausedTotal = (tonumber(self._pausedTotal) or 0) + delta
  end

  self._paused = false
  self._pauseStartedAt = nil
  self._pausedElapsed = nil
  self:_SyncPauseStateToSession()

  self:StopPauseFlash()
  self:_ApplyTimerStateVisuals()
  self:RefreshMetrics()
  self:RefreshLoot(true)
end

local function BuildRows(parent, count)
  local rows = {}
  for i = 1, count do
    local r = CreateFrame("Button", nil, parent)
    r:SetHeight(22)
    r:Hide()

    r.icon = r:CreateTexture(nil, "ARTWORK")
    r.icon:SetSize(18, 18)
    r.icon:SetPoint("LEFT", 8, 0)

    r.name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    r.name:SetPoint("LEFT", r.icon, "RIGHT", 8, 0)
    r.name:SetJustifyH("LEFT")
    r.name:SetWordWrap(false)
    if r.name.SetMaxLines then r.name:SetMaxLines(1) end

    -- Right-aligned numeric columns (fixed widths to prevent overlap)
    r.value = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    r.value:SetJustifyH("RIGHT")
    r.value:SetWordWrap(false)
    if r.value.SetMaxLines then r.value:SetMaxLines(1) end
    r.value:SetWidth(118)
    r.value:SetPoint("RIGHT", -12, 0)

    r.price = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    r.price:SetJustifyH("RIGHT")
    r.price:SetWordWrap(false)
    if r.price.SetMaxLines then r.price:SetMaxLines(1) end
    r.price:SetWidth(110)
    r.price:SetPoint("RIGHT", r.value, "LEFT", -12, 0)

    r.count = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    r.count:SetJustifyH("RIGHT")
    r.count:SetWordWrap(false)
    if r.count.SetMaxLines then r.count:SetMaxLines(1) end
    -- Count is realistically never going to exceed ~99k; tighten the column and
    -- reduce the Count→Price gutter so long item names have more breathing room.
    r.count:SetWidth(46)
    -- Nudge Count slightly right (closer to Price) to reclaim space for long item names.
    r.count:SetPoint("RIGHT", r.price, "LEFT", -4, 0)

    -- Name fills remaining space (prevents long names from pushing into numbers)
    r.name:SetPoint("RIGHT", r.count, "LEFT", -2, 0)

    rows[i] = r
  end
  return rows
end

local function NormalizeWaypointCoord(v)
  local n = tonumber(v)
  if not n then
    return nil
  end

  -- Farms can come from saved fields, manual edits, or older data. Support both
  -- 0-1 map coords and 0-100 percent coords before creating a waypoint.
  if n > 1 then
    n = n / 100
  end

  if n < 0 then
    n = 0
  elseif n > 1 then
    n = 1
  end

  return n
end

local function IsInstanceStyleMapId(mapId)
  local mid = tonumber(mapId)
  if not mid or not C_Map or not C_Map.GetMapInfo then
    return false
  end

  local info = C_Map.GetMapInfo(mid)
  local mapType = info and info.mapType
  if mapType == nil then
    return false
  end

  local uiMapType = Enum and Enum.UIMapType
  local dungeonType = (uiMapType and uiMapType.Dungeon) or 4
  local microType = (uiMapType and uiMapType.Micro) or 5
  local orphanType = (uiMapType and uiMapType.Orphan) or 6

  return mapType == dungeonType or mapType == microType or mapType == orphanType
end

local function SetWaypoint(mapId, x, y, allowFallback)
  if not mapId then
    return false, false
  end

  -- Dungeon/raid floor maps do not make reliable user waypoints. Do not fake a
  -- 50/50 pin there, because it can show as a completely unrelated location.
  if IsInstanceStyleMapId(mapId) then
    return false, false
  end

  local fx = NormalizeWaypointCoord(x)
  local fy = NormalizeWaypointCoord(y)
  local approx = false

  -- Open-world farms with a zone map but no coords can still get a clear
  -- "zone center" placeholder. Instance maps are blocked above.
  if (not fx or not fy) and allowFallback then
    fx, fy = 0.5, 0.5
    approx = true
  end

  if not fx or not fy then
    return false, false
  end

  if C_Map and C_Map.SetUserWaypoint and UiMapPoint and UiMapPoint.CreateFromCoordinates then
    local point = UiMapPoint.CreateFromCoordinates(mapId, fx, fy)
    C_Map.SetUserWaypoint(point)
    if C_SuperTrack and C_SuperTrack.SetSuperTrackedUserWaypoint then
      C_SuperTrack.SetSuperTrackedUserWaypoint(true)
    end
    return true, approx
  end

  return false, false
end


local function FarmKey(f)
  if type(f) ~= "table" then return "" end
  if f.id then
    return "id:" .. tostring(f.id)
  end
  local n = tostring(f.name or "")
  local z = tostring(f.zone or "")
  return "nz:" .. n .. "@" .. z
end

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

local function FarmLooksInstanced(farm)
  if type(farm) ~= "table" then
    return false
  end

  local farmType = _Norm(farm.farmType or farm.type or farm.category or farm.kind or "")
  if farmType:find("instance", 1, true) or farmType:find("dungeon", 1, true) or farmType:find("raid", 1, true) then
    return true
  end

  if IsInstanceStyleMapId(farm.mapId) then
    return true
  end

  return false
end

function UI:_GetPlayerMapId()
  if C_Map and C_Map.GetBestMapForUnit then
    return C_Map.GetBestMapForUnit("player")
  end
  return nil
end

function UI:_GetPlayerZoneName()
  local z = nil
  if GetRealZoneText then z = GetRealZoneText() end
  if (not z or z == "") and GetZoneText then z = GetZoneText() end
  return z
end

function UI:_IsPlayerInFarmArea(farm)
  if type(farm) ~= "table" then return false end

  local pMap = self:_GetPlayerMapId()
  if farm.mapId and pMap and tonumber(farm.mapId) == tonumber(pMap) then
    return true
  end

  local fz = farm.zone or farm.zoneName
  local pz = self:_GetPlayerZoneName()
  if fz and pz and _Norm(fz) ~= "" and _Norm(fz) == _Norm(pz) then
    return true
  end

  return false
end

-- ---------------------------------------------------------
-- Random farm pool (existing behavior)
-- ---------------------------------------------------------
function UI:_BuildFarmPool()
  local pool = {}
  local seenNZ = {}

  -- Ensure seed farms are materialized in the DB (but respect deletions via seed keys).
  if WF.DB and WF.DB.ApplySeedFarms then
    pcall(function() WF.DB:ApplySeedFarms() end)
  end

  -- 1) DB farms (includes manual farms and any farms that have been seeded into the DB)
  local db = WF.DB and WF.DB.Get and WF.DB:Get()
  local farms = db and db.farms
  if type(farms) == "table" then
    for _, f in pairs(farms) do
      if type(f) == "table" and not f.archived and (f.source == "manual" or f.source == "seed" or f.source == nil) and (f.name and f.name ~= "") then
        pool[#pool + 1] = f
        seenNZ[tostring(f.name or "") .. "@" .. tostring(f.zone or "")] = true
      end
    end
  end

  return pool
end

function UI:_PickRandomFarm(preferCoords)
  local pool = self:_BuildFarmPool()
  if #pool == 0 then
    return nil
  end

  local list = pool
  if preferCoords then
    local withCoords = {}
    for i = 1, #pool do
      local f = pool[i]
      if f and f.mapId and f.x and f.y then
        withCoords[#withCoords + 1] = f
      end
    end
    if #withCoords > 0 then
      list = withCoords
    end
  end

  local n = #list
  if n == 1 then
    return list[1]
  end

  self._randCounter = (self._randCounter or 0) + 1
  local t = SafeNow()
  local gt = 0
  if GetTime then
    gt = math.floor((GetTime() or 0) * 1000)
  end
  local seed = t + gt + (self._randCounter * 7919)

  local idx = (seed % n) + 1
  local pick = list[idx]

  -- avoid immediate repeats when possible
  local lastKey = self._lastRandomKey
  if lastKey and FarmKey(pick) == lastKey then
    idx = (idx % n) + 1
    pick = list[idx]
  end

  self._lastRandomKey = FarmKey(pick)
  return pick
end

function UI:_ApplyFarmToSession(farm)
  if not farm or not Tracker or not Tracker.current then
    return
  end

  Tracker.current.farmId = farm.id or Tracker.current.farmId
  Tracker.current.farmName = farm.name or Tracker.current.farmName
  Tracker.current.zone = farm.zone or Tracker.current.zone
  Tracker.current.mapId = farm.mapId or Tracker.current.mapId
  Tracker.current.x = farm.x or Tracker.current.x
  Tracker.current.y = farm.y or Tracker.current.y

  -- Optional metadata
  Tracker.current.expansion = farm.expansion or Tracker.current.expansion
  Tracker.current.farmType  = farm.farmType  or Tracker.current.farmType
  Tracker.current.focus     = farm.focus     or Tracker.current.focus
  Tracker.current.tags      = farm.tags      or Tracker.current.tags
  Tracker.current.notes     = farm.notes     or Tracker.current.notes
end

-- ---------------------------------------------------------
-- Instance reset counter (10/hr)
-- ---------------------------------------------------------
function UI:_LoadInstanceResetLog()
  local t = WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("instanceResetLog")
  if type(t) ~= "table" then
    t = {}
  end
  self._instanceResets = t
  self:_PruneInstanceResets()
end

function UI:_SaveInstanceResetLog()
  if not (WF.DB and WF.DB.SetSetting) then return end
  if type(self._instanceResets) ~= "table" then return end
  -- keep it small
  while #self._instanceResets > 20 do
    table.remove(self._instanceResets, 1)
  end
  WF.DB:SetSetting("instanceResetLog", self._instanceResets)
end

function UI:_PruneInstanceResets()
  local t = self._instanceResets
  if type(t) ~= "table" then return end
  local now = SafeNow()
  local i = 1
  while i <= #t do
    local ts = tonumber(t[i]) or 0
    if (now - ts) > 3600 then
      table.remove(t, i)
    else
      i = i + 1
    end
  end
end

function UI:GetInstanceResetCounter()
  local cap = 10
  if type(self._instanceResets) ~= "table" then
    return 0, cap, nil
  end

  self:_PruneInstanceResets()

  local count = #self._instanceResets
  local remaining = nil
  if count > 0 then
    local oldest = tonumber(self._instanceResets[1]) or SafeNow()
    remaining = (oldest + 3600) - SafeNow()
    if remaining < 0 then remaining = 0 end
  end

  return count, cap, remaining
end

function UI:RecordInstanceReset()
  self._instanceResets = self._instanceResets or {}
  self._instanceResets[#self._instanceResets + 1] = SafeNow()
  self:_SaveInstanceResetLog()
end

function UI:RefreshInstanceResetButton()
  local b = self.bResetDungeon
  if not b or not b.label then
    return
  end

  -- Compact mode keeps the label short; details live in the tooltip.
  if self.GetLayoutMode and self:GetLayoutMode() == "Compact" then
    b.label:SetText("Reset Dungeon")
    return
  end

  local count, cap = self:GetInstanceResetCounter()
  b.label:SetText(string.format("Reset Dungeon (%d/%d)", count, cap))
end

-- ---------------------------------------------------------
-- ---------------------------------------------------------
-- Compare window code moved to UI/CompareWindow.lua
-- Competitive comms/mode code moved to UI/Competitive.lua

-- Layout mode API
-- ---------------------------------------------------------
function UI:GetLayoutMode()
  local v = nil
  if WF.DB and WF.DB.GetSetting then
    v = WF.DB:GetSetting("mainWindowLayout")
  end

  -- Default to Compact when unset (common on first install / fresh SavedVariables).
  -- We persist it once so subsequent loads are stable and user toggles still save.
  if v == nil or v == "" then
    if WF.DB and WF.DB.SetSetting then
      WF.DB:SetSetting("mainWindowLayout", "Compact")
    end
    return "Compact"
  end

  return NormalizeLayout(v)
end

function UI:SetLayoutMode(mode)
  mode = NormalizeLayout(mode)

  -- Compare / Competitive are now separate auxiliary windows, not main-window layouts.
  if mode == "Compare" then
    if self.ToggleCompareWindow then self:ToggleCompareWindow() end
    return
  elseif mode == "Competitive" then
    if self.ToggleCompetitiveWindow then self:ToggleCompetitiveWindow() end
    return
  end

  -- persist current mode state before switching
  if self.PersistLayoutPosition then
    self:PersistLayoutPosition()
  end
  -- save current mode size before switching
  if self.PersistLayoutSize then
    self:PersistLayoutSize()
  end
  if WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("mainWindowLayout", mode)
  end
  self:ApplyLayoutMode(mode)
end

-- ---------------------------------------------------------

function UI:IsCompetitiveOpen()
  return self.compFrame and self.compFrame:IsShown() or false
end


function UI:ToggleCompetitiveWindow()
  if not self.compFrame then
    if self.Create then self:Create() end
  end
  local f = self.compFrame
  if not f then return end

  if f:IsShown() then
    f:Hide()
  else
    f:Show()

    -- Ensure competitive comms are ready and (when not grouped) auto-join the shared challenge channel.
    self:_EnsureCompetitiveComm()
    if not self:_HasCompetitivePeers() then
      self:_GetCompetitiveDistribution()
    end

    self:RefreshCompetitivePanel(true)
  end
end


-- Farming profile API (separate from Layout)
-- ---------------------------------------------------------
function UI:GetActiveProfileName()
  if WF and WF.DB and WF.DB.GetActiveProfileName then
    return WF.DB:GetActiveProfileName()
  end
  return "Default"
end

function UI:RefreshProfileUI()
  if not self.bProfile then
    if self.RefreshHeaderPriceSource then
      pcall(self.RefreshHeaderPriceSource, self)
    end
    return
  end

  local name = self:GetActiveProfileName()
  if name == nil or name == "" then name = "Profile" end

  local labelText = "|cff9aa0a6Mode:|r " .. DisplayProfileName(name)
  if self.bProfile.SetText then
    self.bProfile:SetText(labelText)
  elseif self.bProfile.label and self.bProfile.label.SetText then
    self.bProfile.label:SetText(labelText)
  end

  if self.RefreshHeaderPriceSource then
    pcall(self.RefreshHeaderPriceSource, self)
  end
end

function UI:EnsureActiveProfileApplied(force)
  if not (WF and WF.DB and WF.DB.ApplyProfile and WF.DB.GetActiveProfileName) then
    return
  end

  if self._activeProfileApplied and not force then
    return
  end

  local name = WF.DB:GetActiveProfileName()
  if not name or name == "" then
    name = "Default"
  end

  -- Important: ApplyProfile is what pushes profile settings into the live config the tracker reads.
  -- If we don't do this on boot, the tracker may keep default/cached rules until you click Profile.
  WF.DB:ApplyProfile(name, true)
  self._activeProfileApplied = true
end

local WF_DOUBLE_TAP_TOGGLE_WINDOW = 0.35
local WF_DOUBLE_TAP_BLOCKED_KEYS = {
  LSHIFT = true,
  RSHIFT = true,
  LCTRL = true,
  RCTRL = true,
  LALT = true,
  RALT = true,
  UNKNOWN = true,
}

local function WF_DoubleTapNow()
  if GetTimePreciseSec then
    return GetTimePreciseSec()
  end
  if GetTime then
    return GetTime()
  end
  return 0
end

function UI:NormalizeDoubleTapToggleKey(key)
  key = tostring(key or ""):upper()
  key = key:gsub("^KEY_", "")
  return key
end

function UI:IsAllowedDoubleTapToggleKey(key)
  key = self:NormalizeDoubleTapToggleKey(key)
  if key == "" then return false end
  if WF_DOUBLE_TAP_BLOCKED_KEYS[key] then return false end
  return true
end

function UI:FormatDoubleTapToggleKey(key)
  key = self:NormalizeDoubleTapToggleKey(key)
  if key == "" then
    return "Unbound"
  end

  if GetBindingText then
    local pretty = GetBindingText(key, "KEY_")
    if pretty and pretty ~= "" then
      return pretty
    end
  end

  return key
end

function UI:GetDoubleTapToggleKey()
  local key = "ESCAPE"
  if WF and WF.DB and WF.DB.GetSetting then
    key = WF.DB:GetSetting("doubleTapToggleKey") or key
  end
  key = self:NormalizeDoubleTapToggleKey(key)
  if not self:IsAllowedDoubleTapToggleKey(key) then
    key = "ESCAPE"
  end
  return key
end

local function WF_SafeGetObjectType(obj)
  if obj == nil then
    return nil
  end

  local objType = type(obj)
  if objType ~= "table" and objType ~= "userdata" then
    return nil
  end

  local getter = obj.GetObjectType
  if type(getter) ~= "function" then
    return nil
  end

  local ok, objectType = pcall(getter, obj)
  if not ok then
    return nil
  end

  return objectType
end

function UI:_CanProcessDoubleTapToggleKey()
  if not (WF and WF.DB and WF.DB.GetSetting) then
    return false
  end
  if not WF.DB:GetSetting("doubleTapToggleEnabled") then
    return false
  end

  local Help = WF and WF.UI and WF.UI.Help
  if Help and Help._wfCapturingDoubleTapToggleKey then
    return false
  end

  if GetCurrentKeyBoardFocus then
    local focus = GetCurrentKeyBoardFocus()
    if WF_SafeGetObjectType(focus) == "EditBox" then
      return false
    end
  end

  return true
end

function UI:_IsDoubleTapToggleKeyDown(key)
  key = self:NormalizeDoubleTapToggleKey(key)
  if key == "" then
    return false
  end

  if not IsKeyDown then
    return false
  end

  local ok, down = pcall(IsKeyDown, key)
  if not ok then
    return false
  end

  return down and true or false
end

function UI:_ResetDoubleTapToggleState(key)
  local frame = self._doubleTapToggleListener
  if not frame then
    return
  end

  key = self:NormalizeDoubleTapToggleKey(key or self:GetDoubleTapToggleKey())
  frame._trackedKey = key
  frame._wasDown = self:_IsDoubleTapToggleKeyDown(key)
  frame._lastAt = 0
end

function UI:_EnsureDoubleTapToggleListener()
  if self._doubleTapToggleListener then
    return
  end

  local f = CreateFrame("Frame", nil, UIParent)
  f._pollElapsed = 0
  f._trackedKey = nil
  f._wasDown = false
  f._lastAt = 0

  f:SetScript("OnUpdate", function(frame, elapsed)
    frame._pollElapsed = (frame._pollElapsed or 0) + (tonumber(elapsed) or 0)
    if frame._pollElapsed < 0.01 then
      return
    end
    frame._pollElapsed = 0

    local bind = UI:GetDoubleTapToggleKey()

    if bind ~= frame._trackedKey then
      UI:_ResetDoubleTapToggleState(bind)
      return
    end

    if not UI:_CanProcessDoubleTapToggleKey() then
      frame._wasDown = UI:_IsDoubleTapToggleKeyDown(bind)
      frame._lastAt = 0
      return
    end

    local down = UI:_IsDoubleTapToggleKeyDown(bind)
    if down == frame._wasDown then
      return
    end

    frame._wasDown = down
    if not down then
      return
    end

    local now = WF_DoubleTapNow()
    if (now - (frame._lastAt or 0)) <= WF_DOUBLE_TAP_TOGGLE_WINDOW then
      frame._lastAt = 0
      if UI.Toggle then
        UI:Toggle()
      end
      frame._wasDown = UI:_IsDoubleTapToggleKeyDown(bind)
      return
    end

    frame._lastAt = now
  end)

  f:Show()
  self._doubleTapToggleListener = f
  self:_ResetDoubleTapToggleState()
end

function UI:CycleProfile(backwards)
  if not (WF and WF.DB and WF.DB.GetProfileNames and WF.DB.ApplyProfile) then
    return
  end

  local names = WF.DB:GetProfileNames()
  if not names or #names == 0 then
    return
  end

  local cur = self:GetActiveProfileName()
  local idx = 1
  for i = 1, #names do
    if names[i] == cur then
      idx = i
      break
    end
  end

  if backwards then
    idx = idx - 1
    if idx < 1 then idx = #names end
  else
    idx = idx + 1
    if idx > #names then idx = 1 end
  end

  local nextName = names[idx]
  local ok = WF.DB:ApplyProfile(nextName)

  if ok then
    self:RefreshProfileUI()
    self:RefreshTitle()
    self:RefreshMetrics()
    self:RefreshLoot(true)

    if WF and WF.UI and WF.UI.Help and WF.UI.Help.RefreshLootSettings then
      WF.UI.Help:RefreshLootSettings()
    end

    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage("What Farm?: Profile set to " .. tostring(nextName) .. ".", 0.92, 0.92, 0.92, 1)
    end
  end
end

function UI:PersistLayoutSize()
  if not self.frame then return end
  if not (WF and WF.DB and WF.DB.SetSetting and WF.DB.GetSetting) then return end
  if WF.DB:GetSetting("uiLocked_main") then return end

  local modeNow = NormalizeLayout(self:GetLayoutMode())
  local kW = "mainW_" .. modeNow
  local kH = "mainH_" .. modeNow

  local w = self.frame:GetWidth() or 0
  local h = self.frame:GetHeight() or 0

  local minW, minH = ModeMin(modeNow)
  local maxW, maxH = 1600, 1000
  if UIParent and UIParent.GetWidth then
    local pw = UIParent:GetWidth() or maxW
    local ph = UIParent:GetHeight() or maxH
    maxW = math.max(minW, math.floor(pw - 40))
    maxH = math.max(minH, math.floor(ph - 40))
  end

  w = math.min(math.max(w, minW), maxW)
  h = math.min(math.max(h, minH), maxH)

  WF.DB:SetSetting(kW, math.floor(w + 0.5))
  WF.DB:SetSetting(kH, math.floor(h + 0.5))
end


function UI:PersistLayoutPosition()
  if not self.frame then return end
  if not (WF and WF.DB and WF.DB.SetSetting and WF.DB.GetSetting) then return end
  if WF.DB:GetSetting("uiLocked_main") then return end

  local modeNow = NormalizeLayout(self:GetLayoutMode())
  local point, _, relPoint, x, y = self.frame:GetPoint(1)
  if not point then return end

  local pU = string.upper(tostring(point))
  local rpU = string.upper(tostring(relPoint or point))
  if not VALID_ANCHOR_POINTS[pU] then pU = "CENTER" end
  if not VALID_ANCHOR_POINTS[rpU] then rpU = pU end

  x = tonumber(x) or 0
  y = tonumber(y) or 0

  WF.DB:SetSetting("mainP_" .. modeNow, pU)
  WF.DB:SetSetting("mainRP_" .. modeNow, rpU)
  WF.DB:SetSetting("mainX_" .. modeNow, math.floor(x + 0.5))
  WF.DB:SetSetting("mainY_" .. modeNow, math.floor(y + 0.5))
end

function UI:RestoreLayoutPosition(mode)
  if not self.frame then return false end
  if not (WF and WF.DB and WF.DB.GetSetting) then return false end

  mode = NormalizeLayout(mode or self:GetLayoutMode())
  if mode == "Compare" or mode == "Competitive" then
    mode = "Normal"
    if WF and WF.DB and WF.DB.SetSetting then
      WF.DB:SetSetting("mainWindowLayout", mode)
    end
  end

  local point = WF.DB:GetSetting("mainP_" .. mode)
  local relPoint = WF.DB:GetSetting("mainRP_" .. mode)
  local x = tonumber(WF.DB:GetSetting("mainX_" .. mode))
  local y = tonumber(WF.DB:GetSetting("mainY_" .. mode))

  if type(point) ~= "string" or type(relPoint) ~= "string" or x == nil or y == nil then
    return false
  end

  point = string.upper(point)
  relPoint = string.upper(relPoint)
  if not VALID_ANCHOR_POINTS[point] then point = "CENTER" end
  if not VALID_ANCHOR_POINTS[relPoint] then relPoint = point end

  self.frame:ClearAllPoints()
  self.frame:SetPoint(point, UIParent, relPoint, x, y)
  return true
end

local function WFCompForceLine(fs)
  if not fs then return end
  if fs.SetMaxLines then fs:SetMaxLines(1) end
  if fs.SetWordWrap then fs:SetWordWrap(false) end
  if fs.SetNonSpaceWrap then fs:SetNonSpaceWrap(false) end
end

local function WFCompTip(widget, title, body)
  if not widget or not widget.HookScript then return end
  widget:HookScript("OnEnter", function(self)
    if not GameTooltip 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, Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
    end
    if body and body ~= "" then
      GameTooltip:AddLine(body, Theme.text[1], Theme.text[2], Theme.text[3], true)
    end
    GameTooltip:Show()
  end)
  widget:HookScript("OnLeave", function()
    if GameTooltip then GameTooltip:Hide() end
  end)
end

local function WFCompGetAllowInvites()
  if WF and WF.DB and WF.DB.GetSetting then
    return not not WF.DB:GetSetting("compAllowInvitesDuringSession")
  end
  return false
end

local function WFCompSetAllowInvites(state)
  if WF and WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("compAllowInvitesDuringSession", state and true or false)
  end
end

local function WFCompGetOpenJoin()
  if WF and WF.DB and WF.DB.GetSetting then
    return not not WF.DB:GetSetting("compAllowJoinWithoutInvite")
  end
  return false
end

local function WFCompSetOpenJoin(state)
  if WF and WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("compAllowJoinWithoutInvite", state and true or false)
  end
end

local function WFCompGetChannel()
  if WF and WF.DB and WF.DB.GetSetting then
    return tostring(WF.DB:GetSetting("compChannelName") or "WFARMCOMP")
  end
  return "WFARMCOMP"
end

local function WFCompSetChannel(name)
  name = tostring(name or "")
  name = name:gsub("^%s+", ""):gsub("%s+$", "")
  if name == "" then
    name = "WFARMCOMP"
  end
  if WF and WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("compChannelName", name)
  end
  return name
end

local function WFCompCreateSettingsWindow(ownerUI, compWin, CreateAuxWindow, CreateHeaderGearIconButton)
  local compSettingsWin, compSettingsBody = CreateAuxWindow("compcfg", "WHAT FARM?", "Competitive Settings", 520, 430, compWin)
  local compSettingsPanel = Widgets:Panel(compSettingsBody)
  compSettingsPanel:SetPoint("TOPLEFT", 0, 0)
  compSettingsPanel:SetPoint("BOTTOMRIGHT", 0, 0)
  if compSettingsPanel.SetClipsChildren then compSettingsPanel:SetClipsChildren(true) end

  local hero = Widgets:Panel(compSettingsPanel)
  hero:SetPoint("TOPLEFT", 12, -12)
  hero:SetPoint("TOPRIGHT", -12, -12)
  hero:SetHeight(98)

  local heroGlow = hero:CreateTexture(nil, "BACKGROUND")
  heroGlow:SetAllPoints()
  if Theme and Theme.accent then
    heroGlow:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.08)
  else
    heroGlow:SetColorTexture(1, 1, 1, 0.05)
  end

  local heroTopRule = hero:CreateTexture(nil, "ARTWORK")
  heroTopRule:SetPoint("TOPLEFT", hero, "TOPLEFT", 0, 0)
  heroTopRule:SetPoint("TOPRIGHT", hero, "TOPRIGHT", 0, 0)
  heroTopRule:SetHeight(2)
  if Theme and Theme.accent then
    heroTopRule:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.95)
  else
    heroTopRule:SetColorTexture(1, 1, 1, 0.35)
  end

  local compSettingsKicker = hero:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compSettingsKicker:SetPoint("TOPLEFT", 14, -10)
  compSettingsKicker:SetText("COMPETITIVE CONTROL CENTER")
  if Theme and Theme.muted then
    compSettingsKicker:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], 1)
  end
  WFCompForceLine(compSettingsKicker)

  local compSettingsTitle = hero:CreateFontString(nil, "OVERLAY", WF_FontTemplate("WF_GameFontNormalLarge", "GameFontNormalLarge"))
  compSettingsTitle:SetPoint("TOPLEFT", compSettingsKicker, "BOTTOMLEFT", 0, -4)
  compSettingsTitle:SetText("Competitive Settings")
  if Theme and Theme.accent then
    compSettingsTitle:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  end

  local compSettingsHint = hero:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compSettingsHint:SetPoint("TOPLEFT", compSettingsTitle, "BOTTOMLEFT", 0, -3)
  compSettingsHint:SetPoint("TOPRIGHT", hero, "TOPRIGHT", -14, -34)
  compSettingsHint:SetJustifyH("LEFT")
  compSettingsHint:SetText("Premium access controls for live invites, join policy, and fallback shared-channel sessions.")
  if Theme and Theme.muted then
    compSettingsHint:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end

  local heroChips = CreateFrame("Frame", nil, hero)
  heroChips:SetPoint("BOTTOMLEFT", hero, "BOTTOMLEFT", 12, 10)
  heroChips:SetPoint("BOTTOMRIGHT", hero, "BOTTOMRIGHT", -12, 10)
  heroChips:SetHeight(26)

  local chipInviteState = Widgets:MetricChip(heroChips, "LIVE INVITES")
  chipInviteState:SetPoint("LEFT", heroChips, "LEFT", 0, 0)
  chipInviteState:SetWidth(150)
  chipInviteState:SetHeight(24)

  local chipJoinState = Widgets:MetricChip(heroChips, "JOIN POLICY")
  chipJoinState:SetPoint("LEFT", chipInviteState, "RIGHT", 10, 0)
  chipJoinState:SetWidth(150)
  chipJoinState:SetHeight(24)

  local chipChannelState = Widgets:MetricChip(heroChips, "CHANNEL")
  chipChannelState:SetPoint("LEFT", chipJoinState, "RIGHT", 10, 0)
  chipChannelState:SetPoint("RIGHT", heroChips, "RIGHT", 0, 0)
  chipChannelState:SetHeight(24)

  local function SetCompHeroChipValue(chip, value, useAccent)
    if not chip then return end
    if chip.value and chip.value.SetText then
      chip.value:SetText(value or "--")
      if useAccent and Theme and Theme.accent then
        chip.value:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
      elseif Theme and Theme.text then
        chip.value:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
      end
    end
  end

  local function MakeCompSettingsCard(parent, anchor, titleText, helpText, height)
    local card = Widgets:Panel(parent)
    card:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -10)
    card:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -10)
    card:SetHeight(height or 100)

    card.accent = card:CreateTexture(nil, "ARTWORK")
    card.accent:SetPoint("TOPLEFT", card, "TOPLEFT", 0, 0)
    card.accent:SetPoint("BOTTOMLEFT", card, "BOTTOMLEFT", 0, 0)
    card.accent:SetWidth(2)
    if Theme and Theme.accent then
      card.accent:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.75)
    else
      card.accent:SetColorTexture(1, 1, 1, 0.25)
    end

    card.title = card:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    card.title:SetPoint("TOPLEFT", 14, -10)
    card.title:SetText(titleText or "Section")
    if Theme and Theme.accent then
      card.title:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
    end

    card.help = card:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    card.help:SetPoint("TOPLEFT", card.title, "BOTTOMLEFT", 0, -3)
    card.help:SetPoint("TOPRIGHT", card, "TOPRIGHT", -14, -13)
    card.help:SetJustifyH("LEFT")
    card.help:SetText(helpText or "")
    if Theme and Theme.muted then
      card.help:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
    end

    card.rule = card:CreateTexture(nil, "ARTWORK")
    card.rule:SetPoint("TOPLEFT", card, "TOPLEFT", 14, -38)
    card.rule:SetPoint("TOPRIGHT", card, "TOPRIGHT", -14, -38)
    card.rule:SetHeight(1)
    card.rule:SetColorTexture(1, 1, 1, 0.06)

    return card
  end

  local function MakeCompToggleRow(parent, anchor, titleText, helpText, buttonWidth)
    local row = Widgets:Panel(parent)
    row:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 14, -10)
    row:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", -14, -10)
    row:SetHeight(54)

    local glow = row:CreateTexture(nil, "BACKGROUND")
    glow:SetAllPoints()
    if Theme and Theme.accent then
      glow:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.035)
    else
      glow:SetColorTexture(1, 1, 1, 0.02)
    end

    row.title = row:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    row.title:SetPoint("TOPLEFT", 10, -8)
    row.title:SetPoint("TOPRIGHT", row, "TOPRIGHT", -126, -8)
    row.title:SetJustifyH("LEFT")
    row.title:SetText(titleText or "Setting")
    WFCompForceLine(row.title)

    row.help = row:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    row.help:SetPoint("TOPLEFT", row.title, "BOTTOMLEFT", 0, -3)
    row.help:SetPoint("TOPRIGHT", row, "TOPRIGHT", -126, -25)
    row.help:SetJustifyH("LEFT")
    row.help:SetText(helpText or "")
    if Theme and Theme.muted then
      row.help:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
    end

    row.action = Widgets:Button(row, "Off", buttonWidth or 98, 22)
    row.action:SetPoint("RIGHT", row, "RIGHT", -10, 0)

    return row
  end

  local accessCard = MakeCompSettingsCard(compSettingsPanel, hero, "Session Access", "Tune how strict or flexible Competitive sessions feel for hosts and participants.", 164)
  local rowLiveInvites = MakeCompToggleRow(accessCard, accessCard.rule, "Allow invites during an active match", "Invite late joiners while the session is live or paused.", 108)
  local rowOpenJoin = MakeCompToggleRow(accessCard, rowLiveInvites, "Allow join requests without an invite", "Open the door for direct session joins when someone knows the session details.", 108)

  local routingCard = MakeCompSettingsCard(compSettingsPanel, accessCard, "Session Routing", "Fallback behavior used when you are not grouped and still want to share a Competitive session.", 138)

  local channelLabel = routingCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  channelLabel:SetPoint("TOPLEFT", routingCard.rule, "BOTTOMLEFT", 0, -10)
  channelLabel:SetText("Shared channel name")
  if Theme and Theme.muted then
    channelLabel:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end

  local channelShell = Widgets:Panel(routingCard)
  channelShell:SetPoint("TOPLEFT", channelLabel, "BOTTOMLEFT", 0, -6)
  channelShell:SetPoint("RIGHT", routingCard, "RIGHT", -112, 0)
  channelShell:SetHeight(32)

  local channelShellGlow = channelShell:CreateTexture(nil, "BACKGROUND")
  channelShellGlow:SetAllPoints()
  if Theme and Theme.accent then
    channelShellGlow:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.05)
  else
    channelShellGlow:SetColorTexture(1, 1, 1, 0.03)
  end

  local channelBox = CreateFrame("EditBox", nil, channelShell, "InputBoxTemplate")
  channelBox:SetPoint("TOPLEFT", channelShell, "TOPLEFT", 10, -5)
  channelBox:SetPoint("BOTTOMRIGHT", channelShell, "BOTTOMRIGHT", -10, 5)
  channelBox:SetAutoFocus(false)
  if channelBox.SetTextInsets then
    channelBox:SetTextInsets(2, 2, 0, 0)
  end
  if channelBox.SetMaxLetters then
    channelBox:SetMaxLetters(32)
  end
  channelBox:SetScript("OnEscapePressed", function(selfBox)
    selfBox:SetText(WFCompGetChannel())
    selfBox:ClearFocus()
  end)

  local bChannelReset = Widgets:Button(routingCard, "Default", 88, 22)
  bChannelReset:SetPoint("TOPRIGHT", channelShell, "TOPRIGHT", 98, -5)

  local channelHint = routingCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  channelHint:SetPoint("TOPLEFT", channelShell, "BOTTOMLEFT", 0, -7)
  channelHint:SetPoint("TOPRIGHT", routingCard, "TOPRIGHT", -14, -94)
  channelHint:SetJustifyH("LEFT")
  channelHint:SetText("Used for ungrouped Competitive discovery. Keep it short, clean, and easy for your circle to remember.")
  if Theme and Theme.muted then
    channelHint:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end

  local roadmapCard = MakeCompSettingsCard(compSettingsPanel, routingCard, "Roadmap", "The shell is in place for deeper match administration without cluttering the main window.", 92)

  local roadmapPills = CreateFrame("Frame", nil, roadmapCard)
  roadmapPills:SetPoint("TOPLEFT", roadmapCard.rule, "BOTTOMLEFT", 0, -10)
  roadmapPills:SetPoint("TOPRIGHT", roadmapCard.rule, "BOTTOMRIGHT", 0, -10)
  roadmapPills:SetHeight(24)

  local function MakeRoadmapPill(parent, anchor, text)
    local pill = Widgets:Button(parent, text, 120, 22)
    if anchor then
      pill:SetPoint("LEFT", anchor, "RIGHT", 8, 0)
    else
      pill:SetPoint("LEFT", parent, "LEFT", 0, 0)
    end
    if pill.Disable then pill:Disable() end
    if pill.label and Theme and Theme.muted then
      pill.label:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], 1)
    end
    return pill
  end

  local roadmapPill1 = MakeRoadmapPill(roadmapPills, nil, "Moderation")
  local roadmapPill2 = MakeRoadmapPill(roadmapPills, roadmapPill1, "Invite Codes")
  MakeRoadmapPill(roadmapPills, roadmapPill2, "Mode Presets")

  local compSettingsFooter = compSettingsPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compSettingsFooter:SetPoint("BOTTOMLEFT", 12, 12)
  compSettingsFooter:SetPoint("BOTTOMRIGHT", -12, 12)
  compSettingsFooter:SetJustifyH("LEFT")
  compSettingsFooter:SetText("Built to feel like a control room: quick reads at the top, polished actions in the middle, and room to grow without bloating the Competitive window.")
  if Theme and Theme.muted then
    compSettingsFooter:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], 0.95)
  end

  local function RefreshCompSettingsWindow()
    local liveInvites = WFCompGetAllowInvites()
    local openJoin = WFCompGetOpenJoin()
    local channelName = WFCompGetChannel()

    SetCompHeroChipValue(chipInviteState, liveInvites and "ON" or "OFF", liveInvites)
    SetCompHeroChipValue(chipJoinState, openJoin and "OPEN" or "INVITE ONLY", openJoin)
    SetCompHeroChipValue(chipChannelState, channelName, false)

    if rowLiveInvites and rowLiveInvites.action and rowLiveInvites.action.label then
      rowLiveInvites.action.label:SetText(liveInvites and "Enabled" or "Disabled")
      if liveInvites and Theme and Theme.accent then
        rowLiveInvites.action.label:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
      elseif Theme and Theme.text then
        rowLiveInvites.action.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
      end
    end

    if rowOpenJoin and rowOpenJoin.action and rowOpenJoin.action.label then
      rowOpenJoin.action.label:SetText(openJoin and "Open" or "Invite Only")
      if openJoin and Theme and Theme.accent then
        rowOpenJoin.action.label:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
      elseif Theme and Theme.text then
        rowOpenJoin.action.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
      end
    end

    if channelBox and channelBox.SetText then
      channelBox:SetText(channelName)
    end
  end

  rowLiveInvites.action:SetScript("OnClick", function()
    WFCompSetAllowInvites(not WFCompGetAllowInvites())
    if ownerUI and ownerUI._comp and ownerUI._comp.refreshButtons then
      ownerUI._comp.refreshButtons()
    end
    RefreshCompSettingsWindow()
  end)

  rowOpenJoin.action:SetScript("OnClick", function()
    WFCompSetOpenJoin(not WFCompGetOpenJoin())
    RefreshCompSettingsWindow()
  end)

  local function CommitCompChannelName()
    local val = WFCompSetChannel(channelBox and channelBox.GetText and channelBox:GetText() or "")
    if channelBox and channelBox.SetText then
      channelBox:SetText(val)
    end
    RefreshCompSettingsWindow()
  end

  channelBox:SetScript("OnEnterPressed", function(selfBox)
    CommitCompChannelName()
    selfBox:ClearFocus()
  end)
  channelBox:SetScript("OnEditFocusLost", function()
    CommitCompChannelName()
  end)

  bChannelReset:SetScript("OnClick", function()
    WFCompSetChannel("WFARMCOMP")
    RefreshCompSettingsWindow()
  end)

  if compSettingsWin and compSettingsWin.HookScript then
    compSettingsWin:HookScript("OnShow", RefreshCompSettingsWindow)
  end

  local bCompSettings = nil
  if compWin and compWin._wfHeader and CreateHeaderGearIconButton then
    bCompSettings = CreateHeaderGearIconButton(compWin._wfHeader)
    bCompSettings:SetPoint("RIGHT", compWin._wfHeader.close, "LEFT", -4, 0)
    bCompSettings:SetScript("OnClick", function()
      if compSettingsWin:IsShown() then
        compSettingsWin:Hide()
      else
        RefreshCompSettingsWindow()
        compSettingsWin:Show()
      end
    end)
    if Widgets and Widgets.SkinCloseButton then
      local ht = bCompSettings.GetHighlightTexture and bCompSettings:GetHighlightTexture()
      if ht then ht:SetTexture(nil) end
    end
    WFCompTip(bCompSettings, "Competitive Settings", "Configure live invites, invite-only joining, and the shared channel name.")
  end

  return compSettingsWin, bCompSettings
end

-- ---------------------------------------------------------
-- Main UI
-- ---------------------------------------------------------
local function WF_NormalizeUIScale(scale)
  scale = tonumber(scale) or 1.0
  if scale < 0.80 then return 0.80 end
  if scale > 1.30 then return 1.30 end
  return math.floor((scale / 0.05) + 0.5) * 0.05
end

function UI:ApplyScale(scale)
  scale = WF_NormalizeUIScale(scale or (WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("uiScale")) or 1.0)

  local refreshed = {}
  local function ApplyToFrame(frame)
    if frame and frame.SetScale then
      frame:SetScale(scale)
      refreshed[#refreshed + 1] = frame
    end
  end

  ApplyToFrame(self.frame)
  ApplyToFrame(self.compareFrame)
  ApplyToFrame(self.compFrame)
  ApplyToFrame(self._comp and self._comp.settingsWindow)

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

function UI:Create()
  self:_EnsureSceneRecoveryHooks()

  if self.frame then
    self:_EnsureDoubleTapToggleListener()
    return
  end

  local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  local UIRef = self
  f:SetSize(820, 520)
  f:SetPoint("CENTER")
  f:SetMovable(true)
  if f.SetClampedToScreen then f:SetClampedToScreen(true) end
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", function(self)
    if WF.DB:GetSetting("uiLocked_main") then
      return
    end
    self:StartMoving()
  end)
  f:SetScript("OnDragStop", function(frame)
    frame:StopMovingOrSizing()
    if UIRef and UIRef.PersistLayoutPosition then
      UIRef:PersistLayoutPosition()
    end
  end)

  f:HookScript("OnHide", function()
    if UIRef and UIRef.PersistLayoutSize then
      UIRef:PersistLayoutSize()
    end
    if UIRef and UIRef.PersistLayoutPosition then
      UIRef:PersistLayoutPosition()
    end
  
    -- Ensure background tickers are stopped when the frame is hidden by any means
    if UIRef and UIRef.StopTicker then
      UIRef:StopTicker()
    end
    if UIRef and UIRef.StopPauseFlash then
      UIRef:StopPauseFlash()
    end
  end)


  f:SetResizable(true)
  do
    local modeNow = NormalizeLayout(self:GetLayoutMode())
    local minW, minH = ModeMin(modeNow)
    local maxW, maxH = 1600, 1000
    if UIParent and UIParent.GetWidth then
      local pw = UIParent:GetWidth() or maxW
      local ph = UIParent:GetHeight() or maxH
      maxW = math.max(minW, math.floor(pw - 40))
      maxH = math.max(minH, math.floor(ph - 40))
    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
  end

  -- UIRef already set above
  f:SetScript("OnSizeChanged", function(frame, w, h)
    if frame and frame._wfSizing then return end
    if UIRef._applyingLayout then return end
    if not (WF and WF.DB and WF.DB.SetSetting and WF.DB.GetSetting) then return end
    if WF.DB:GetSetting("uiLocked_main") then return end

    local modeNow = NormalizeLayout(UIRef:GetLayoutMode())
    local kW = "mainW_" .. modeNow
    local kH = "mainH_" .. modeNow

    local minW, minH = ModeMin(modeNow)
    local maxW, maxH = 1600, 1000
    if UIParent and UIParent.GetWidth then
      local pw = UIParent:GetWidth() or maxW
      local ph = UIParent:GetHeight() or maxH
      maxW = math.max(minW, math.floor(pw - 40))
      maxH = math.max(minH, math.floor(ph - 40))
    end

    local ow, oh = w, h
    w = math.min(math.max(w, minW), maxW)
    h = math.min(math.max(h, minH), maxH)

    if Widgets and Widgets.SnapSize then
      local sw, sh = Widgets:SnapSize(frame, w, h)
      w, h = sw, sh
      -- snapping can nudge us by a pixel; clamp again
      w = math.min(math.max(w, minW), maxW)
      h = math.min(math.max(h, minH), maxH)
    end

    if w ~= ow or h ~= oh then
      UIRef._applyingLayout = true
      frame:SetSize(w, h)
      UIRef._applyingLayout = false
    end

    WF.DB:SetSetting(kW, math.floor(w + 0.5))
    WF.DB:SetSetting(kH, math.floor(h + 0.5))

    -- Keep metric chips packed to the current window width.
    if UIRef._UpdateChipsWidth then
      UIRef:_UpdateChipsWidth()
    end
    if UIRef._FitMetricChips then
      UIRef:_FitMetricChips()
    end

    -- Mark this mode as user-resized (any user-driven size change).
    if modeNow == "Compact" then
      WF.DB:SetSetting("mainSizeTouchedV2_Compact", true)
    else
      WF.DB:SetSetting("mainSizeTouched_" .. modeNow, true)
    end
  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(Theme.bg[1], Theme.bg[2], Theme.bg[3], Theme.bg[4])
  f:SetBackdropBorderColor(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4])

  -- -------------------------------------------------------
  -- -------------------------------------------------------
  -- Corner grip (drag = resize, click = lock)
  -- -------------------------------------------------------
  local grip = nil
  if Widgets and Widgets.AttachCornerLockGrip then
    grip = Widgets:AttachCornerLockGrip(f, {
      lockKey = "uiLocked_main",
      size = 16,
      x = -6,
      y = 6,
      alpha = 0.60,
      resizeText = "FRAME BEING RESIZED",
      onResizeStop = function(frame)
        if UIRef and UIRef.PersistLayoutSize then
          UIRef:PersistLayoutSize()
        end
        if UIRef and UIRef._UpdateChipsWidth then
          UIRef:_UpdateChipsWidth()
        end
        if UIRef and UIRef._FitMetricChips then
          UIRef:_FitMetricChips()
        end
      end,
    })
  end

  self.resizeGrip = grip

  local header = Widgets:Header(f, "WHAT FARM?", "")
  header:SetPoint("TOPLEFT", 6, -6)
  header:SetPoint("TOPRIGHT", -6, -6)
  header:SetHeight(34)
  header.close:SetScript("OnClick", function()
    if UIRef and UIRef.Hide then
      UIRef:Hide()
    else
      f:Hide()
    end
  end)

-- Price source is displayed as a header "chip" button next to Profile + Layout.
-- Keep a hidden legacy header.price FontString for backwards compatibility, but don't show it.
if header and header.title and not header.price then
  header.price = header:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  header.price:SetPoint("LEFT", header.title, "RIGHT", 10, -1)
  header.price:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
  header.price:SetText("")
  if header.price.Hide then header.price:Hide() end
  if header.price.SetAlpha then header.price:SetAlpha(0) end
end

-- header.sub is reserved for compact/meta hints; keep it anchored after the title.
if header and header.title and header.sub and header.sub.ClearAllPoints then
  header.sub:ClearAllPoints()
  header.sub:SetPoint("LEFT", header.title, "RIGHT", 10, -1)
end

-- Header window controls: consistent sizing/spacing + clean hover/press chrome.
do
  local c = header.close
  if c then
    -- Hard anchor so the icon cluster always aligns (regardless of header template changes).
    if c.ClearAllPoints then
      c:ClearAllPoints()
      c:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -6)
    end

    local btnSize = 22
    c:SetSize(btnSize, btnSize)
    if header and header.GetFrameLevel and c.SetFrameLevel then c:SetFrameLevel(header:GetFrameLevel() + 10) end
    c:SetHitRectInsets(-2, -2, -2, -2)

    -- Strip default textures (we render our own).
    local nt = c.GetNormalTexture and c:GetNormalTexture()
    if nt then nt:SetTexture(nil); nt:SetAlpha(0); nt:Hide() end
    local pt = c.GetPushedTexture and c:GetPushedTexture()
    if pt then pt:SetTexture(nil); pt:SetAlpha(0); pt:Hide() end
    local ht = c.GetHighlightTexture and c:GetHighlightTexture()
    if ht then ht:SetTexture(nil); ht:SetAlpha(0); ht:Hide() end
    local dt = c.GetDisabledTexture and c:GetDisabledTexture()
    if dt then dt:SetTexture(nil); dt:SetAlpha(0); dt:Hide() end

    -- Hover/press background (subtle, purposeful).
    if not c._wfBg then
      c._wfBg = c:CreateTexture(nil, "BACKGROUND")
      c._wfBg:SetAllPoints()
      c._wfBg:SetTexture("Interface/Buttons/WHITE8x8")
    end

    -- Glyph
    if not c._wfX then
      c._wfX = c:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
      c._wfX:SetPoint("CENTER", 0, 0)

      -- Make the close glyph slightly bigger (reliably). Do NOT depend on template internals.
      local fnt, sz, fl = c._wfX:GetFont()
      if not fnt then
        fnt = DISPLAY_FONT
      end
      sz = tonumber(sz) or 16
      c._wfX:SetFont(fnt, sz + 1, fl)

      c._wfX:SetText("×")
    end

    local function _SetCloseState(state)
      -- state: "idle" | "hover" | "down"
      local ax, ay = 0, -1
      local br, bg, bb, ba = 1, 1, 1, 0
      local xr, xg, xb, xa = 1, 1, 1, 0.90

      if state == "hover" then
        -- Neutral hover chrome (no blue box). Accent only on the glyph.
        br, bg, bb, ba = 1, 1, 1, 0.06
        xr, xg, xb, xa = Theme.accent[1], Theme.accent[2], Theme.accent[3], 1
      elseif state == "down" then
        ax, ay = 1, -2
        br, bg, bb, ba = 1, 1, 1, 0.10
        xr, xg, xb, xa = Theme.accent[1], Theme.accent[2], Theme.accent[3], 1
      end

      if c._wfBg then
        c._wfBg:SetVertexColor(1, 1, 1, 0)
      end
      if c._wfX then
        c._wfX:ClearAllPoints()
        c._wfX:SetPoint("CENTER", ax, ay)
        c._wfX:SetTextColor(xr, xg, xb, xa)
      end
    end

    _SetCloseState("idle")

    c:HookScript("OnEnter", function() _SetCloseState("hover") end)
    c:HookScript("OnLeave", function() _SetCloseState("idle") end)
    c:HookScript("OnMouseDown", function() _SetCloseState("down") end)
    c:HookScript("OnMouseUp", function()
      if c.IsMouseOver and c:IsMouseOver() then
        _SetCloseState("hover")
      else
        _SetCloseState("idle")
      end
    end)
  end
end


  local function CreateHeaderTextButton(parent, text)
    local b = CreateFrame("Button", nil, parent)
    b:SetHeight(20)

    b.label = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    b.label:SetPoint("CENTER", 0, 0)
    b.label:SetText(text or "")

    b.underline = b:CreateTexture(nil, "ARTWORK")
    b.underline:SetColorTexture(0.35, 0.7, 1, 0.9)
    b.underline:SetHeight((Widgets and Widgets.PixelSize) and Widgets:PixelSize(b) or 1)
    if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(b.underline, b) end
    b.underline:SetPoint("BOTTOMLEFT", (Widgets and Widgets.Snap) and Widgets:Snap(b, 4) or 4, (Widgets and Widgets.Snap) and Widgets:Snap(b, 4) or 4)
    b.underline:SetPoint("BOTTOMRIGHT", (Widgets and Widgets.Snap) and Widgets:Snap(b, -4) or -4, (Widgets and Widgets.Snap) and Widgets:Snap(b, 4) or 4)
    b.underline:Hide()

    function b:SetText(t)
      self.label:SetText(t or "")
      local w = self.label:GetStringWidth() or 0
      w = math.max(24, math.min(160, w + 18))
      self:SetWidth(w)
    end

    function b:SetHot(on)
      if on then
        self.label:SetTextColor(0.35, 0.7, 1)
        self.underline:Show()
      else
        self.label:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
        self.underline:Hide()
      end
    end

    b:SetText(text)
    b:SetHot(false)

    b:SetHitRectInsets(-6, -6, -6, -6)

    b:SetScript("OnMouseDown", function(self)
      self.label:ClearAllPoints()
      self.label:SetPoint("CENTER", 1, -1)
    end)
    b:SetScript("OnMouseUp", function(self)
      self.label:ClearAllPoints()
      self.label:SetPoint("CENTER", 0, 0)
    end)

    return b
  end



  local function CreateHeaderIconButton(parent)
  -- Compact toggle button: crisp "window" glyph + subtle hover chrome.
  local b = CreateFrame("Button", nil, parent)
  b:SetSize(22, 22)
  if parent and parent.GetFrameLevel and b.SetFrameLevel then b:SetFrameLevel(parent:GetFrameLevel() + 10) end
  b:SetHitRectInsets(-2, -2, -2, -2)

  -- Background hover/press (keeps the control cluster feeling "intentional").
  b.bg = b:CreateTexture(nil, "BACKGROUND")
  b.bg:SetAllPoints()
  b.bg:SetTexture("Interface/Buttons/WHITE8x8")
  b.bg:SetVertexColor(1, 1, 1, 0)

  b.icon = CreateFrame("Frame", nil, b)
  b.icon:SetSize(14, 14)
  b.icon:SetPoint("CENTER", 0, 1)

  local THICK = 2

  local function _MakeWindow(ox, oy)
    local fBox = CreateFrame("Frame", nil, b.icon)
    fBox:SetSize(10, 10)
    fBox:SetPoint("BOTTOMLEFT", b.icon, "BOTTOMLEFT", ox or 0, oy or 0)

    local function Line()
      local t = fBox:CreateTexture(nil, "ARTWORK")
      t:SetTexture("Interface/Buttons/WHITE8x8")
      t:SetVertexColor(1, 1, 1, 1)
      return t
    end

    fBox.lines = {}

    fBox.lines.top = Line()
    fBox.lines.top:SetPoint("TOPLEFT", 0, 0)
    fBox.lines.top:SetPoint("TOPRIGHT", 0, 0)
    fBox.lines.top:SetHeight(THICK)

    fBox.lines.bottom = Line()
    fBox.lines.bottom:SetPoint("BOTTOMLEFT", 0, 0)
    fBox.lines.bottom:SetPoint("BOTTOMRIGHT", 0, 0)
    fBox.lines.bottom:SetHeight(THICK)

    fBox.lines.left = Line()
    fBox.lines.left:SetPoint("TOPLEFT", 0, 0)
    fBox.lines.left:SetPoint("BOTTOMLEFT", 0, 0)
    fBox.lines.left:SetWidth(THICK)

    fBox.lines.right = Line()
    fBox.lines.right:SetPoint("TOPRIGHT", 0, 0)
    fBox.lines.right:SetPoint("BOTTOMRIGHT", 0, 0)
    fBox.lines.right:SetWidth(THICK)

    function fBox:SetColor(r, g, bb, a)
      for _, t in pairs(self.lines) do
        if t and t.SetVertexColor then
          t:SetVertexColor(r or 1, g or 1, bb or 1, a or 1)
        end
      end
    end

    return fBox
  end

  -- Back window (offset up/right) + front window.
  b._wfBack  = _MakeWindow(3, 3)
  b._wfFront = _MakeWindow(0, 0)

  function b:_RefreshIcon()
    local mode = self._wfMode or ""
    local hot = self._wfHot and true or false
    local down = self._wfDown and true or false
    local isCompact = (mode == "Compact")

    local fr, fg, fb, fa = 1, 1, 1, 0.92
    local br, bg, bb, ba = 1, 1, 1, 0.28

    -- Keep the glyph monochrome (gray/white) in all states.
    -- Compact/hover/press can adjust *alpha* only (no blue accent fill).
    if isCompact then
      ba = 0.45
    end

    if hot or down then
      fa = 1
      local targetBackAlpha = 0.54
      if down then targetBackAlpha = 0.62 end
      ba = math.max(ba, targetBackAlpha)
    end

    if self._wfFront and self._wfFront.SetColor then
      self._wfFront:SetColor(fr, fg, fb, fa)
    end
    if self._wfBack and self._wfBack.SetColor then
      self._wfBack:SetColor(br, bg, bb, ba)
    end

    -- background chrome
    -- Neutral chrome (no blue hover box)
    local aa = 0
    if down then
      aa = 0.10
    elseif hot then
      aa = 0.06
    end
    if self.bg then
      self.bg:SetVertexColor(1, 1, 1, 0)
    end
  end

  function b:SetMode(mode)
    self._wfMode = mode or ""
    self:_RefreshIcon()
  end

  function b:SetHot(on)
    self._wfHot = on and true or false
    self:_RefreshIcon()
  end

  function b:SetDown(on)
    self._wfDown = on and true or false
    -- tiny press offset for "button" feel
    if self.icon and self.icon.ClearAllPoints then
      self.icon:ClearAllPoints()
      if on then
        self.icon:SetPoint("CENTER", 1, -1)
      else
        self.icon:SetPoint("CENTER", 0, 0)
      end
    end
    self:_RefreshIcon()
  end

  b:SetScript("OnEnter", function(selfBtn)
    selfBtn:SetHot(true)
    if GameTooltip then
      WF_Tip_AnchorCentered(selfBtn)
      GameTooltip:ClearLines()
      GameTooltip:SetText("Toggle Compact Mode", 1, 1, 1, 1)
      WF_Tip_FinalizeCentered(selfBtn)
    end
  end)
  b:SetScript("OnLeave", function(selfBtn)
    selfBtn:SetHot(false)
    selfBtn:SetDown(false)
    if GameTooltip then GameTooltip:Hide() end
  end)

  b:SetScript("OnMouseDown", function(selfBtn)
    selfBtn:SetDown(true)
  end)
  b:SetScript("OnMouseUp", function(selfBtn)
    selfBtn:SetDown(false)
  end)

  b:SetMode("Normal")
  b:SetHot(false)
  b:SetDown(false)

  return b
end

local function CreateHeaderGearIconButton(parent)
  -- Settings icon button: custom gear glyph with the same hover/press chrome as other header controls.
  local b = CreateFrame("Button", nil, parent)
  b:SetSize(22, 22)
  if parent and parent.GetFrameLevel and b.SetFrameLevel then b:SetFrameLevel(parent:GetFrameLevel() + 10) end
  b:SetHitRectInsets(-2, -2, -2, -2)

  b.bg = b:CreateTexture(nil, "BACKGROUND")
  b.bg:SetAllPoints()
  b.bg:SetTexture("Interface/Buttons/WHITE8x8")
  b.bg:SetVertexColor(1, 1, 1, 0)

  local texPath = "Interface/AddOns/WhatFarm/Media/Icons/WF_Gear_White2"

  b.shadow = b:CreateTexture(nil, "BACKGROUND")
  b.shadow:SetTexture(texPath)
  b.shadow:SetSize(13, 13)
  b.shadow:SetPoint("CENTER", 1, -2)
  b.shadow:SetVertexColor(0, 0, 0, 0.55)

  b.tex = b:CreateTexture(nil, "ARTWORK")
  b.tex:SetTexture(texPath)
  b.tex:SetSize(13, 13)
  b.tex:SetPoint("CENTER", 0, -1)
  b.tex:SetVertexColor(1, 1, 1, 0.94)

  function b:_ApplyState(state)
    local ar, ag, ab = Theme.accent[1], Theme.accent[2], Theme.accent[3]
    local hot = (state == "hover" or state == "down")
    local down = (state == "down")

    if self.bg then
      local a = 0
      if down then a = 0.10 elseif hot then a = 0.06 end
      self.bg:SetVertexColor(1, 1, 1, 0)
    end

    if self.tex then
      if hot then
        self.tex:SetVertexColor(ar, ag, ab, 1)
      else
        self.tex:SetVertexColor(1, 1, 1, 0.94)
      end
    end

    if self.tex and self.tex.ClearAllPoints then
      self.tex:ClearAllPoints()
      self.tex:SetPoint("CENTER", down and 1 or 0, down and -2 or -1)
    end
    if self.shadow and self.shadow.ClearAllPoints then
      self.shadow:ClearAllPoints()
      self.shadow:SetPoint("CENTER", down and 2 or 1, down and -3 or -2)
    end
  end

  b:SetScript("OnEnter", function(selfBtn)
    selfBtn:_ApplyState("hover")
    if GameTooltip then
      WF_Tip_AnchorCentered(selfBtn)
      GameTooltip:ClearLines()
      GameTooltip:SetText("Settings", 1, 1, 1, 1)
      WF_Tip_FinalizeCentered(selfBtn)
    end
  end)

  b:SetScript("OnLeave", function(selfBtn)
    selfBtn:_ApplyState("idle")
    if GameTooltip then GameTooltip:Hide() end
  end)

  b:SetScript("OnMouseDown", function(selfBtn)
    selfBtn:_ApplyState("down")
  end)

  b:SetScript("OnMouseUp", function(selfBtn)
    if selfBtn.IsMouseOver and selfBtn:IsMouseOver() then
      selfBtn:_ApplyState("hover")
    else
      selfBtn:_ApplyState("idle")
    end
  end)

  b:_ApplyState("idle")
  return b
end

  -- Settings (header)
  local bSettings = CreateHeaderGearIconButton(header)
  bSettings:SetPoint("RIGHT", header.close, "LEFT", -4, 0)

  -- Layout (header icon) - click toggles Compact/Regular. Shift-click opens Compare/Competitive.
  local bLayoutBar = CreateHeaderIconButton(header)
  bLayoutBar:SetPoint("RIGHT", bSettings, "LEFT", -4, 0)


  -- Remove default hover highlight borders (keep blue icon "glow" only).
  local function _WF_KillButtonHighlight(btn)
    if not btn then return end
    local ht = btn.GetHighlightTexture and btn:GetHighlightTexture()
    if ht then ht:SetTexture(nil); ht:SetAlpha(0); ht:Hide() end
  end
  _WF_KillButtonHighlight(header.close)
  _WF_KillButtonHighlight(bLayoutBar)
  _WF_KillButtonHighlight(bSettings)

  -- Subtle dividers between header controls (purposeful segmentation).
  if not header._wfCtrlDiv1 then
    header._wfCtrlDiv1 = header:CreateTexture(nil, "BORDER")
    header._wfCtrlDiv1:SetTexture("Interface/Buttons/WHITE8x8")
    header._wfCtrlDiv1:SetWidth((Widgets and Widgets.PixelSize) and Widgets:PixelSize(header) or 1)
    if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(header._wfCtrlDiv1, header) end
    header._wfCtrlDiv1:SetVertexColor(1, 1, 1, 0.10)
  end
  if not header._wfCtrlDiv2 then
    header._wfCtrlDiv2 = header:CreateTexture(nil, "BORDER")
    header._wfCtrlDiv2:SetTexture("Interface/Buttons/WHITE8x8")
    header._wfCtrlDiv2:SetWidth((Widgets and Widgets.PixelSize) and Widgets:PixelSize(header) or 1)
    if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(header._wfCtrlDiv2, header) end
    header._wfCtrlDiv2:SetVertexColor(1, 1, 1, 0.10)
  end
  -- Position dividers centered between buttons.
  header._wfCtrlDiv1:ClearAllPoints()
  header._wfCtrlDiv1:SetPoint("TOP", header.close, "TOPLEFT", 0, -3)
  header._wfCtrlDiv1:SetPoint("BOTTOM", header.close, "BOTTOMLEFT", 0, 3)

  header._wfCtrlDiv2:ClearAllPoints()
  header._wfCtrlDiv2:SetPoint("TOP", bSettings, "TOPLEFT", 0, -3)
  header._wfCtrlDiv2:SetPoint("BOTTOM", bSettings, "BOTTOMLEFT", 0, 3)
  bSettings:SetScript("OnClick", function()
    if WF.UI.Help and WF.UI.Help.Show then
      local ok = pcall(function()
        WF.UI.Help:Show("Settings")
      end)
      if not ok then
        WF.UI.Help:Show("Overview")
      end
    end
  end)
  -- Profile (header)
  local bProfileBar = CreateHeaderTextButton(header, "Profile")
  bProfileBar:SetPoint("RIGHT", bLayoutBar, "LEFT", -12, 0)
  if bProfileBar.RegisterForClicks then
    bProfileBar:RegisterForClicks("LeftButtonUp", "RightButtonUp")
  end

-- Price Source (header)
  local bPriceBar = CreateHeaderTextButton(header, "Source")
  bPriceBar:SetPoint("RIGHT", bProfileBar, "LEFT", -12, 0)



  local title = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 12, -10)
  title:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
  title:SetText("No farm selected")

  local sub = f:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  sub:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
  sub:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
  sub:SetText("Start anywhere. What Farm? will capture your session automatically.")

  local chips = CreateFrame("Frame", nil, f)
  chips:SetPoint("TOPLEFT", sub, "BOTTOMLEFT", 0, -10)
  chips:SetSize(780, 30)

  local metricChips = {}
  local function CreateMetricChip(index)
    local chip = Widgets:MetricChip(chips, "Metric")
    chip._wfSlot = index
    chip:EnableMouse(true)
    chip:SetScript("OnMouseUp", function(selfChip, button)
      if button == "LeftButton" and selfChip._wfMetric == "Race Timer" and UI.OpenRaceTimerPrompt then
        UI:OpenRaceTimerPrompt()
      end
    end)
    chip:SetScript("OnEnter", function(selfChip)
      if selfChip._wfMetric ~= "Race Timer" then
        return
      end
      WF_Tip_AnchorCentered(selfChip)
      GameTooltip:ClearLines()
      GameTooltip:AddLine("Race Timer", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
      local inComp, _, isLeader = UI:_GetRaceTimerCompetitiveContext()
      if inComp and isLeader then
        GameTooltip:AddLine("Host-controlled during Competitive. When your timer hits zero, the match ends for everyone.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        GameTooltip:AddLine("Left-click to set the shared time.", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
      elseif inComp then
        GameTooltip:AddLine("Synced from the Competitive host.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        GameTooltip:AddLine("Only the host can change it during the match.", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
      else
        GameTooltip:AddLine("Counts down during the farm and ends the run when it hits zero.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        GameTooltip:AddLine("Left-click to set the time.", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
      end
      WF_Tip_FinalizeCentered(selfChip, nil, true)
    end)
    chip:SetScript("OnLeave", function(selfChip)
      if selfChip._wfMetric == "Race Timer" then
        GameTooltip:Hide()
      end
    end)
    if index == 1 then
      chip:SetPoint("LEFT", 0, 0)
    else
      chip:SetPoint("LEFT", metricChips[index - 1], "RIGHT", 10, 0)
    end
    metricChips[index] = chip
    return chip
  end

  local chip1 = CreateMetricChip(1)
  local chip2 = CreateMetricChip(2)
  local chip3 = CreateMetricChip(3)
  local chip4 = CreateMetricChip(4)

  local function _WF_TightenMetricChip(chip)
    if not chip or not chip.label or not chip.value then return end

    if chip.label.SetMaxLines then
      chip.label:SetMaxLines(1)
    end
    if chip.label.SetWordWrap then
      chip.label:SetWordWrap(false)
    end

    chip.value:ClearAllPoints()
    chip.value:SetJustifyH("LEFT")
    chip.value:SetJustifyV("MIDDLE")
    chip.value:SetPoint("LEFT", chip.label, "RIGHT", 6, 0)
    chip.value:SetPoint("RIGHT", chip, "RIGHT", -10, 0)

    if chip.value.SetMaxLines then
      chip.value:SetMaxLines(1)
    end
    if chip.value.SetWordWrap then
      chip.value:SetWordWrap(false)
    end
    if chip.value.SetNonSpaceWrap then
      chip.value:SetNonSpaceWrap(false)
    end
  end
  _WF_TightenMetricChip(chip1)
  _WF_TightenMetricChip(chip2)
  _WF_TightenMetricChip(chip3)
  _WF_TightenMetricChip(chip4)


  local tablePanel = Widgets:Panel(f)
  tablePanel:SetPoint("TOPLEFT", chips, "BOTTOMLEFT", 0, -12)
  tablePanel:SetPoint("BOTTOMRIGHT", -10, 48)

  local hdr = CreateFrame("Frame", nil, tablePanel)
  hdr:SetPoint("TOPLEFT", 6, -6)
  hdr:SetPoint("TOPRIGHT", -28, -6)  -- match scroll width so columns align
  hdr:SetHeight(22)

  local hItem = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  hItem:SetPoint("LEFT", 0, 0)
  hItem:SetText("Item")

  -- Keep headers aligned to the same column geometry as rows
  local hValue = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  hValue:SetJustifyH("RIGHT")
  hValue:SetWidth(118)
  hValue:SetPoint("RIGHT", -12, 0)
  hValue:SetText("Value")

  local hPrice = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  hPrice:SetJustifyH("RIGHT")
  hPrice:SetWidth(110)
  hPrice:SetPoint("RIGHT", hValue, "LEFT", -12, 0)
  hPrice:SetText("Price")

  local hCount = hdr:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  hCount:SetJustifyH("RIGHT")
  -- Match row geometry: tighter Count column + smaller gutter for more Item space.
  hCount:SetWidth(46)
  -- Keep header aligned with row geometry (Count nudged slightly right).
  hCount:SetPoint("RIGHT", hPrice, "LEFT", -4, 0)
  hCount:SetText("Count")

  -- Keep references so RefreshLoot can highlight the active sort column
  self._lootHdr = hdr
  hdr._wfLabelItem = hItem
  hdr._wfLabelCount = hCount
  hdr._wfLabelPrice = hPrice
  hdr._wfLabelValue = hValue

  -- -------------------------------------------------------
  -- Header click targets (main loot table)
  -- Left-click: sort by that column (toggles desc/asc)
  -- Right-click: add item… (only when session is Active or Paused)
  --
  -- IMPORTANT: These buttons must be sized/aligned to the *column geometry*,
  -- not the label text bounds, otherwise the hover highlight looks like
  -- floating/misaligned boxes.
  -- -------------------------------------------------------
  local hbItem, hbCount, hbPrice, hbValue

  local function _WF_CanManualAddNow()
    if not Tracker or not Tracker.current then return false end
    if RealTrackerIsActive and RealTrackerIsActive() then return true end
    if UI and UI.IsPaused and UI:IsPaused() then return true end
    return false
  end

  local function _WF_OpenAddItemMenu()
    if not _WF_CanManualAddNow() then return end

    local help = WF and WF.UI and WF.UI.Help
    if help and help.OpenContextMenu then
      help:OpenContextMenu({
        {
          text = "Add item...",
          fn = function()
            if UI and UI.PromptAddItemToRun then
              UI:PromptAddItemToRun()
            end
          end
        },
        { sep = true },
        { text = (CANCEL or "Cancel"), fn = function() end },
      }, 200)
    else
      -- Fallback: just open the prompt
      if UI and UI.PromptAddItemToRun then
        UI:PromptAddItemToRun()
      end
    end
  end

  local function _WF_SetLootSortKey(key)
    if Tracker and Tracker.SetLootSortKey then
      Tracker:SetLootSortKey(key)
    else
      Tracker._lootSortKey = key
      Tracker._sortedCache = nil
      if Tracker and Tracker.MarkDirty then Tracker:MarkDirty() end
    end
    if UI and UI.RefreshLoot then
      UI:RefreshLoot(true)
    end
  end

  local function _WF_HeaderClick(key)
    return function(_, button)
      if button == "RightButton" then
        _WF_OpenAddItemMenu()
        return
      end
      if button ~= "LeftButton" then return end
      _WF_SetLootSortKey(key)
    end
  end

  local function _WF_HeaderHover(btn, on)
    if not btn or not btn._hl then return end
    btn._hl:SetAlpha(on and 0.08 or 0)
  end

  local function _WF_InitHdrBtn(b, key)
    if not b then return end

    -- Keep the click region aligned to the header bar (not just the fontstring height).
    b:SetPoint("TOP", hdr, "TOP", 0, 0)
    b:SetPoint("BOTTOM", hdr, "BOTTOM", 0, 0)

    b._hl = b:CreateTexture(nil, "BACKGROUND")
    b._hl:SetAllPoints(b)
    b._hl:SetTexture("Interface/Buttons/WHITE8x8")
    b._hl:SetVertexColor(1, 1, 1, 1)
    b._hl:SetAlpha(0)

    b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    b:EnableMouse(true)

    -- Ensure the buttons sit above the header background and don't get buried.
    if hdr and hdr.GetFrameLevel and b.SetFrameLevel then
      b:SetFrameLevel(hdr:GetFrameLevel() + 2)
    end

    b:SetScript("OnEnter", function(self) _WF_HeaderHover(self, true) end)
    b:SetScript("OnLeave", function(self) _WF_HeaderHover(self, false) end)
    b:SetScript("OnClick", _WF_HeaderClick(key))
  end

  -- Define header click regions by *column edges* (no weird floating boxes, no overlaps).
  hbValue = CreateFrame("Button", nil, hdr)
  hbValue:SetPoint("LEFT", hValue, "LEFT", 0, 0)
  hbValue:SetPoint("RIGHT", hdr, "RIGHT", 0, 0)
  _WF_InitHdrBtn(hbValue, "value")

  hbPrice = CreateFrame("Button", nil, hdr)
  hbPrice:SetPoint("LEFT", hPrice, "LEFT", 0, 0)
  hbPrice:SetPoint("RIGHT", hValue, "LEFT", 0, 0) -- includes the Price→Value gutter
  _WF_InitHdrBtn(hbPrice, "price")

  hbCount = CreateFrame("Button", nil, hdr)
  hbCount:SetPoint("LEFT", hCount, "LEFT", 0, 0)
  hbCount:SetPoint("RIGHT", hPrice, "LEFT", 0, 0) -- includes the Count→Price gutter
  _WF_InitHdrBtn(hbCount, "count")

  hbItem = CreateFrame("Button", nil, hdr)
  hbItem:SetPoint("LEFT", hdr, "LEFT", 0, 0)
  hbItem:SetPoint("RIGHT", hCount, "LEFT", 0, 0)
  _WF_InitHdrBtn(hbItem, "name")

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

  local content = CreateFrame("Frame", nil, scroll)
  content:SetSize(1, 1)
  scroll:SetScrollChild(content)
  -- Empty-state message (when session not active)
  -- User request: keep it extremely simple (just plain text, no blue highlights, no card UI).
  local empty = CreateFrame("Frame", nil, scroll)
  empty:SetAllPoints(scroll)
  empty:SetFrameLevel(scroll:GetFrameLevel() + 20)
  empty:EnableMouse(false)
  empty:Hide()

  local emptyText = empty:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  emptyText:SetPoint("CENTER", empty, "CENTER", 0, 0)
  emptyText:SetJustifyH("CENTER")
  emptyText:SetJustifyV("MIDDLE")
  do
    local r, g, b = Theme.accent[1] or 0.35, Theme.accent[2] or 0.78, Theme.accent[3] or 1.0
    local hex = string.format("%02x%02x%02x",
      math.floor(r * 255 + 0.5),
      math.floor(g * 255 + 0.5),
      math.floor(b * 255 + 0.5)
    )
    emptyText:SetText("Session not active. |cff" .. hex .. "Press Start|r to begin tracking.")
  end
  emptyText:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], 0.85)

  self._lootEmptyFrame = empty
  self._lootEmptyText = emptyText
  -- Backwards compat with earlier patch fields (some code may reference these)
  self._lootEmptyTitle = emptyText
  self._lootEmptyBody = emptyText
  self._lootEmptyHint = emptyText
  local rows = BuildRows(content, 200)
  for i = 1, #rows do
    local r = rows[i]
    r:SetPoint("TOPLEFT", 0, -((i - 1) * 24))
    r:SetPoint("TOPRIGHT", 0, -((i - 1) * 24))
  end
  content:SetHeight(1) -- dynamic height; prevents scrolling when empty

  -- column dividers (optional)
  local function AddDivider(parent, rightOffset)
    local t = parent:CreateTexture(nil, "BORDER")
    t:SetTexture("Interface/Buttons/WHITE8x8")

    local px = (Widgets and Widgets.PixelSize) and Widgets:PixelSize(parent) or 1
    local x = rightOffset
    local topY, botY = -32, 36
    if Widgets and Widgets.Snap then
      x = Widgets:Snap(parent, x)
      topY = Widgets:Snap(parent, topY)
      botY = Widgets:Snap(parent, botY)
    end

    t:SetWidth(px)
    if Widgets and Widgets.MakePixelPerfect then
      Widgets:MakePixelPerfect(t, parent)
    end

    t:SetPoint("TOP", parent, "TOPRIGHT", x, topY)
    t:SetPoint("BOTTOM", parent, "BOTTOMRIGHT", x, botY)
    t:SetAlpha(0.22)
    return t
  end


  self._dividers = self._dividers or {}
  if WF.DB:GetSetting("showColumnDividers") then
    -- Divider offsets are tuned to the column geometry above.
    self._dividers[1] = AddDivider(tablePanel, -204) -- between Item and Count
    self._dividers[2] = AddDivider(tablePanel, -128) -- between Count and Price
    self._dividers[3] = AddDivider(tablePanel, -87)  -- between Price and Value
  end

  -- -------------------------------------------------------
  -- Separate windows: Compare + Competitive
  -- (keeps the main farm window compact; these open as their own frames)
  -- -------------------------------------------------------
  local function CreateAuxWindow(prefix, titleText, subtitleText, defW, defH, anchorTo)
    local af = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")

    local minW, minH = defW, defH
    if prefix == "comp" then
      minW, minH = 380, 360
    elseif prefix == "cmp" then
      minW, minH = 460, 420
    end

    local w = tonumber(WF.DB:GetSetting(prefix .. "W")) or defW
    local h = tonumber(WF.DB:GetSetting(prefix .. "H")) or defH
    w = math.max(tonumber(w) or defW, minW)
    h = math.max(tonumber(h) or defH, minH)
    af:SetSize(w, h)
    do
      local s = tonumber(WF.DB:GetSetting("uiScale")) or 1.0
      if af.SetScale then af:SetScale(s) end
    end
    af:SetMovable(true)
    if af.SetClampedToScreen then af:SetClampedToScreen(true) end
    af:EnableMouse(true)
    af:RegisterForDrag("LeftButton")
    local lockKey = "uiLocked_" .. tostring(prefix)
    af:SetScript("OnDragStart", function(self)
      if WF.DB:GetSetting(lockKey) then return end
      self:StartMoving()
    end)
    af:SetScript("OnDragStop", function(self)
      self:StopMovingOrSizing()
      local point, _, relPoint, x, y = self:GetPoint(1)
      if not point then return end
      WF.DB:SetSetting(prefix .. "P", string.upper(tostring(point)))
      WF.DB:SetSetting(prefix .. "RP", string.upper(tostring(relPoint or point)))
      WF.DB:SetSetting(prefix .. "X", math.floor((tonumber(x) or 0) + 0.5))
      WF.DB:SetSetting(prefix .. "Y", math.floor((tonumber(y) or 0) + 0.5))
    end)

    af:SetBackdrop({
      bgFile = "Interface/Tooltips/UI-Tooltip-Background",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 14,
      insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    af:SetBackdropColor(Theme.bg[1], Theme.bg[2], Theme.bg[3], Theme.bg[4])
    af:SetBackdropBorderColor(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4])

    local header2 = Widgets:Header(af, titleText, subtitleText or "")
    header2:SetPoint("TOPLEFT", 6, -6)
    header2:SetPoint("TOPRIGHT", -6, -6)
    header2:SetHeight(32)
    af._wfHeader = header2

    if header2.close then
      header2.close:SetScript("OnClick", function() af:Hide() end)
    end

    -- Resize grip (drag = resize, click = lock)
    local grip2 = nil
    if af.SetResizable then af:SetResizable(true) end

    do
      local maxW, maxH = 1600, 1000
      if UIParent and UIParent.GetWidth then
        local pw = UIParent:GetWidth() or maxW
        local ph = UIParent:GetHeight() or maxH
        maxW = math.max(minW, math.floor(pw - 40))
        maxH = math.max(minH, math.floor(ph - 40))
      end
      if af.SetResizeBounds then
        af:SetResizeBounds(minW, minH, maxW, maxH)
      else
        if af.SetMinResize then af:SetMinResize(minW, minH) end
        if af.SetMaxResize then af:SetMaxResize(maxW, maxH) end
      end
    end

    if Widgets and Widgets.AttachCornerLockGrip then
      grip2 = Widgets:AttachCornerLockGrip(af, {
        lockKey = lockKey,
        size = 16,
        x = -6,
        y = 6,
        alpha = 0.60,
        onResizeStop = function()
          local nw, nh = af:GetSize()
          WF.DB:SetSetting(prefix .. "W", math.floor((tonumber(nw) or defW) + 0.5))
          WF.DB:SetSetting(prefix .. "H", math.floor((tonumber(nh) or defH) + 0.5))
        end,
      })
    end

    -- Restore saved pos; otherwise dock near main window.
    local p = WF.DB:GetSetting(prefix .. "P")
    local rp = WF.DB:GetSetting(prefix .. "RP")
    local sx = tonumber(WF.DB:GetSetting(prefix .. "X"))
    local sy = tonumber(WF.DB:GetSetting(prefix .. "Y"))
    if type(p) == "string" and type(rp) == "string" and sx ~= nil and sy ~= nil then
      af:ClearAllPoints()
      af:SetPoint(string.upper(p), UIParent, string.upper(rp), sx, sy)
    else
      af:ClearAllPoints()
      if anchorTo and anchorTo.GetPoint then
        af:SetPoint("TOPLEFT", anchorTo, "TOPRIGHT", 12, 0)
      else
        af:SetPoint("CENTER", UIParent, "CENTER", 240, 0)
      end
    end

    local body = CreateFrame("Frame", nil, af)
    body:SetPoint("TOPLEFT", header2, "BOTTOMLEFT", 0, -8)
    body:SetPoint("BOTTOMRIGHT", -8, 8)

    af:Hide()
    return af, body
  end

  local compareWin, compareBody = CreateAuxWindow("cmp", "WHAT FARM?", "Compare Window", 460, 520, f)
  local compWin, compBody = CreateAuxWindow("comp", "WHAT FARM?", "Competitive Window", 560, 520, f)

  self.compareFrame = compareWin
  self.compFrame = compWin

  if UIRef.InitCompareWindow then
    UIRef:InitCompareWindow(compareWin, compareBody)
  end
  if compWin and compWin.HookScript then
    compWin:HookScript("OnShow", function()
      UIRef:RefreshCompetitivePanel(true)
      if UIRef.RefreshCompetitiveLootPanel then UIRef:RefreshCompetitiveLootPanel(true) end
    end)
  end

  local function ApplyCompetitiveWindowHeader()
    if not (compWin and compWin._wfHeader) then return end
    local header = compWin._wfHeader
    if header.title and header.title.SetText then
      header.title:SetText("WHAT FARM?")
      if Theme and Theme.text and header.title.SetTextColor then
        header.title:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
      end
    end
    if header.sub then
      if header.sub.SetText then
        header.sub:SetText("Competitive Window")
      end
      if Theme and Theme.muted and header.sub.SetTextColor then
        header.sub:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
      end
      if header.sub.Show then
        header.sub:Show()
      end
    end
  end

  if compWin and compWin.HookScript then
    compWin:HookScript("OnShow", ApplyCompetitiveWindowHeader)
    compWin:HookScript("OnSizeChanged", ApplyCompetitiveWindowHeader)
  end
  ApplyCompetitiveWindowHeader()


  -- -------------------------------------------------------
  -- Side: Competitive panel (separate window)
  -- -------------------------------------------------------
  local compPanel = Widgets:Panel(compBody)
  compPanel:SetPoint("TOPLEFT", 0, 0)
  compPanel:SetPoint("BOTTOMRIGHT", 0, 0)
  if compPanel.SetClipsChildren then compPanel:SetClipsChildren(true) end

  local compTitle = compPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  compTitle:SetPoint("TOPLEFT", 12, -10)
  compTitle:SetText("")
  compTitle:Hide()

  local compHint = compPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compHint:SetPoint("TOPLEFT", 12, -10)
  compHint:SetPoint("TOPRIGHT", -12, -10)
  compHint:SetJustifyH("LEFT")
  compHint:SetText("")
  compHint:Hide()

  local compSettingsWin, bCompSettings = WFCompCreateSettingsWindow(self, compWin, CreateAuxWindow, CreateHeaderGearIconButton)


  local compTabs = CreateFrame("Frame", nil, compPanel)
  compTabs:SetPoint("TOPLEFT", 12, -10)
  compTabs:SetPoint("TOPRIGHT", -12, -10)
  compTabs:SetHeight(24)

  local bTabLB = Widgets:Button(compTabs, "Leaderboard", 120, 22)
  bTabLB:SetPoint("LEFT", 0, 0)

  local bTabLoot = Widgets:Button(compTabs, "Loot", 80, 22)
  bTabLoot:SetPoint("LEFT", bTabLB, "RIGHT", 8, 0)
  WFCompTip(bTabLB, "Leaderboard", "View competitors, session details, and live match controls.")
  WFCompTip(bTabLoot, "Loot", "View merged loot across all competitors.")

  local function _CompTabColor(btn, selected)
    if not (btn and btn.label) then return end
    local c = Theme and Theme.text or { 1, 1, 1, 1 }
    if selected and Theme and Theme.accent then
      c = Theme.accent
    end
    btn.label:SetTextColor(c[1], c[2], c[3], c[4] or 1)
  end

  local function _LayoutCompTabs()
    if not compTabs or not compTabs.GetWidth then return end
    local w = compTabs:GetWidth() or 0
    if w <= 50 then return end
    local gap = 8
    local bw = math.floor((w - gap) / 2)
    if bTabLB and bTabLB.SetWidth then bTabLB:SetWidth(bw) end
    if bTabLoot and bTabLoot.SetWidth then bTabLoot:SetWidth(bw) end
  end
  compTabs:HookScript("OnShow", _LayoutCompTabs)
  compTabs:HookScript("OnSizeChanged", _LayoutCompTabs)

  local function UpdateCompTopLayout()
    if not compTabs then return end
    local w = compPanel and compPanel.GetWidth and (compPanel:GetWidth() or 0) or 0
    compTabs:ClearAllPoints()
    if w > 0 and w < 470 then
      compHint:Hide()
      compTabs:SetPoint("TOPLEFT", compTitle, "BOTTOMLEFT", 0, -8)
    else
      compHint:Show()
      compTabs:SetPoint("TOPLEFT", compHint, "BOTTOMLEFT", 0, -8)
    end
    compTabs:SetPoint("TOPRIGHT", -12, 0)
  end
  if compPanel and compPanel.HookScript then
    compPanel:HookScript("OnShow", UpdateCompTopLayout)
    compPanel:HookScript("OnSizeChanged", UpdateCompTopLayout)
  end

  local lbPanel = CreateFrame("Frame", nil, compPanel)
  lbPanel:SetPoint("TOPLEFT", compTabs, "BOTTOMLEFT", 0, -8)
  lbPanel:SetPoint("BOTTOMRIGHT", -12, 12)
  if lbPanel.SetClipsChildren then lbPanel:SetClipsChildren(true) end
  if lbPanel.EnableMouseWheel then lbPanel:EnableMouseWheel(true) end

  local lbScroll = CreateFrame("ScrollFrame", nil, lbPanel, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and lbScroll.ScrollBar then Widgets:SkinScrollBar(lbScroll.ScrollBar) end
  lbScroll:SetPoint("TOPLEFT", lbPanel, "TOPLEFT", 0, 0)
  lbScroll:SetPoint("BOTTOMRIGHT", lbPanel, "BOTTOMRIGHT", 0, 0)
  if lbScroll.EnableMouseWheel then lbScroll:EnableMouseWheel(true) end

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

  local function ScrollCompLeaderboard(delta)
    if not lbScroll then return end
    local step = 36
    local cur = lbScroll.GetVerticalScroll and (lbScroll:GetVerticalScroll() or 0) or 0
    local max = 0
    if lbScroll.GetVerticalScrollRange then
      max = lbScroll:GetVerticalScrollRange() or 0
    elseif lbContent and lbContent.GetHeight and lbScroll.GetHeight then
      max = math.max(0, (lbContent:GetHeight() or 0) - (lbScroll:GetHeight() or 0))
    end
    cur = cur - ((delta or 0) * step)
    if cur < 0 then cur = 0 end
    if cur > max then cur = max end
    if lbScroll.SetVerticalScroll then
      lbScroll:SetVerticalScroll(cur)
    end
  end

  lbScroll:SetScript("OnMouseWheel", function(_, delta)
    ScrollCompLeaderboard(delta)
  end)
  lbPanel:SetScript("OnMouseWheel", function(_, delta)
    ScrollCompLeaderboard(delta)
  end)

  local lootPanel = CreateFrame("Frame", nil, compPanel)
  lootPanel:SetPoint("TOPLEFT", compTabs, "BOTTOMLEFT", 0, -8)
  lootPanel:SetPoint("BOTTOMRIGHT", -12, 12)
  lootPanel:Hide()

  local function ShowCompTab(which)
    if which == "loot" then
      lbPanel:Hide()
      lootPanel:Show()
      _CompTabColor(bTabLB, false)
      _CompTabColor(bTabLoot, true)
      if UI and UI.RefreshCompetitiveLootPanel then
        UI:RefreshCompetitiveLootPanel(true)
      end
    else
      lootPanel:Hide()
      lbPanel:Show()
      _CompTabColor(bTabLB, true)
      _CompTabColor(bTabLoot, false)
      if UI and UI.RefreshCompetitivePanel then
        UI:RefreshCompetitivePanel(true)
      end
    end
  end

  bTabLB:SetScript("OnClick", function() ShowCompTab("lb") end)
  bTabLoot:SetScript("OnClick", function() ShowCompTab("loot") end)

  _CompTabColor(bTabLB, true)
  _CompTabColor(bTabLoot, false)
  _LayoutCompTabs()
  UpdateCompTopLayout()

  -- -------------------------------------------------------
  -- Leaderboard tab
  -- -------------------------------------------------------
  local invLabel, invHelp, invBox, bInvite
  local compMeta, compMeta2
  local UpdateCompLeaderboardScrollLayout = function() end
  local UpdateCompLeaderboardViewport = function() end

  local function GetCompInviteCollapsed()
    if WF and WF.DB and WF.DB.GetSetting then
      return not not WF.DB:GetSetting("compInviteCollapsed")
    end
    return false
  end

  local function SetCompInviteCollapsed(state)
    if WF and WF.DB and WF.DB.SetSetting then
      WF.DB:SetSetting("compInviteCollapsed", state and true or false)
    end
  end

  local function GetCompSessionDetailsCollapsed()
    if WF and WF.DB and WF.DB.GetSetting then
      return not not WF.DB:GetSetting("compSessionDetailsCollapsed")
    end
    return false
  end

  local function SetCompSessionDetailsCollapsed(state)
    if WF and WF.DB and WF.DB.SetSetting then
      WF.DB:SetSetting("compSessionDetailsCollapsed", state and true or false)
    end
  end

  local actionBar = Widgets:Panel(lbContent)
  actionBar:SetPoint("TOPLEFT", 0, 0)
  actionBar:SetPoint("TOPRIGHT", 0, 0)
  actionBar:SetHeight(60)
  if actionBar.SetClipsChildren then actionBar:SetClipsChildren(true) end

  local actionTitle = actionBar:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
  actionTitle:SetPoint("TOPLEFT", 10, -7)
  actionTitle:SetText("Match Controls")
  actionTitle:SetJustifyH("LEFT")
  if Theme and Theme.accent then
    actionTitle:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  end
  WFCompForceLine(actionTitle)
  WFCompTip(actionTitle, "Match Controls", "Start, pause, resume, or end the current competitive match.")

  local actionRule = actionBar:CreateTexture(nil, "ARTWORK")
  actionRule:SetHeight(1)
  actionRule:SetPoint("TOPLEFT", actionBar, "TOPLEFT", 10, -26)
  actionRule:SetPoint("TOPRIGHT", actionBar, "TOPRIGHT", -10, -26)
  if Theme and Theme.border then
    actionRule:SetColorTexture(Theme.border[1], Theme.border[2], Theme.border[3], 0.45)
  else
    actionRule:SetColorTexture(1, 1, 1, 0.06)
  end

  local bStartChallenge = Widgets:Button(actionBar, "Start Match", 126, 22)
  bStartChallenge:SetPoint("LEFT", 8, -10)
  WFCompTip(bStartChallenge, "Start Match", "Start the current competitive session.")
  bStartChallenge:SetScript("OnClick", function()
    if UI and UI.StartChallenge then UI:StartChallenge() end
    if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
  end)

  local bPauseMatch = Widgets:Button(actionBar, "Pause Match", 116, 22)
  bPauseMatch:SetPoint("LEFT", bStartChallenge, "RIGHT", 6, 0)
  WFCompTip(bPauseMatch, "Pause / Resume", "Pause or resume the active competitive session.")
  bPauseMatch:SetScript("OnClick", function()
    if UI and UI.ToggleCompetitivePause then UI:ToggleCompetitivePause() end
    if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
  end)

  local bQuitMatch = Widgets:Button(actionBar, "Quit Match", 106, 22)
  bQuitMatch:SetPoint("LEFT", bPauseMatch, "RIGHT", 6, 0)
  WFCompTip(bQuitMatch, "Quit / End", "Hosts can end the match. Participants can leave it.")
  bQuitMatch:SetScript("OnClick", function()
    if UI and UI.QuitCompetitiveMatch then UI:QuitCompetitiveMatch() end
    if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
  end)

  local function LayoutCompActionButtons()
    local visible = {}
    local buttons = { bStartChallenge, bPauseMatch, bQuitMatch }
    for _, btn in ipairs(buttons) do
      if btn and btn.IsShown and btn:IsShown() then
        visible[#visible + 1] = btn
      end
    end

    local count = #visible
    local barW = actionBar and actionBar.GetWidth and (actionBar:GetWidth() or 0) or 0
    local gap = 6
    local rowY = -7

    for _, btn in ipairs(buttons) do
      if btn and btn.ClearAllPoints then btn:ClearAllPoints() end
    end

    if count <= 0 then return end

    -- Give each button an equal share of the bar width then center the row
    local eachW = math.floor((barW - 20 - ((count - 1) * gap)) / count)
    if eachW < 96  then eachW = 96  end
    if eachW > 170 then eachW = 170 end

    local totalW = (count * eachW) + ((count - 1) * gap)
    local startX = math.floor((barW - totalW) / 2)

    for i, btn in ipairs(visible) do
      if btn.SetWidth then btn:SetWidth(eachW) end
      if i == 1 then
        btn:SetPoint("TOPLEFT", actionBar, "TOPLEFT", startX, rowY - 26)
      else
        btn:SetPoint("LEFT", visible[i - 1], "RIGHT", gap, 0)
      end
    end
  end

  if actionBar and actionBar.HookScript then
    actionBar:HookScript("OnSizeChanged", LayoutCompActionButtons)
  end

  local inviteCard = Widgets:Panel(lbContent)
  inviteCard:SetPoint("TOPLEFT", actionBar, "BOTTOMLEFT", 0, -8)
  inviteCard:SetPoint("TOPRIGHT", actionBar, "BOTTOMRIGHT", 0, -8)
  inviteCard:SetHeight(82)
  if inviteCard.SetClipsChildren then inviteCard:SetClipsChildren(true) end

  local inviteTitle = inviteCard:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
  inviteTitle:SetPoint("TOPLEFT", 10, -7)
  inviteTitle:SetPoint("RIGHT", inviteCard, "RIGHT", -72, 0)
  inviteTitle:SetJustifyH("LEFT")
  inviteTitle:SetText("Invite")
  if Theme and Theme.accent then
    inviteTitle:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  end
  WFCompForceLine(inviteTitle)
  WFCompTip(inviteTitle, "Invite", "Invite another player, or hold Shift while the field is focused to switch into join mode when you are not already hosting or in a session.")

  local bInviteToggle = Widgets:Button(inviteCard, "Hide", 54, 18)
  bInviteToggle:SetPoint("TOPRIGHT", -10, -5)
  WFCompTip(bInviteToggle, "Invite Section", "Hide or show the invite controls.")

  local inviteHeaderRule = inviteCard:CreateTexture(nil, "ARTWORK")
  inviteHeaderRule:SetHeight(1)
  inviteHeaderRule:SetPoint("TOPLEFT", inviteCard, "TOPLEFT", 10, -26)
  inviteHeaderRule:SetPoint("TOPRIGHT", inviteCard, "TOPRIGHT", -10, -26)
  inviteHeaderRule:SetColorTexture(1, 1, 1, 0.06)

  invLabel = inviteCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  invLabel:SetPoint("TOPLEFT", inviteHeaderRule, "BOTTOMLEFT", 0, -5)
  invLabel:SetPoint("TOPRIGHT", inviteHeaderRule, "BOTTOMRIGHT", 0, -5)
  invLabel:SetJustifyH("LEFT")
  invLabel:SetText("Invite a player to this session.")
  if Theme and Theme.muted then
    invLabel:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end
  WFCompForceLine(invLabel)

  invHelp = inviteCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  invHelp:SetPoint("TOPLEFT", invLabel, "BOTTOMLEFT", 0, -2)
  invHelp:SetPoint("TOPRIGHT", inviteHeaderRule, "BOTTOMRIGHT", 0, -2)
  invHelp:SetJustifyH("LEFT")
  invHelp:SetText("Player, Name-Realm, or BattleTag. Shift = Join.")
  if Theme and Theme.muted then
    invHelp:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end
  WFCompForceLine(invHelp)

  invBox = CreateFrame("EditBox", nil, inviteCard, "InputBoxTemplate")
  invBox:SetSize(292, 20)
  invBox:SetAutoFocus(false)
  invBox:SetScript("OnEscapePressed", function() invBox:ClearFocus() end)
  invBox:SetText("")
  WFCompTip(invBox, "Invite Target", "Enter a player name, Name-Realm, or BattleTag. When Join mode is active, enter a session ID or SessionID + host.")

  local inviteShiftJoin = false
  local function ParseCompetitiveJoinInput(raw)
    raw = tostring(raw or "")
    local sid, host = raw:match("^(%d+)%s+(.+)$")
    if sid then
      return sid, host
    end
    sid, host = raw:match("^(%d+)%s*$")
    if sid then
      return sid, ""
    end
    sid, host = raw:match("^[Jj][Oo][Ii][Nn]%s+(%S+)%s*(.-)%s*$")
    if sid then
      return sid, host
    end
    sid, host = raw:match("^(%d+)%s*@%s*(.+)$")
    if sid then
      return sid, host
    end
    return nil, nil
  end

  bInvite = Widgets:Button(inviteCard, "Invite", 74, 22)
  WFCompTip(bInvite, "Invite / Join", "Normal mode sends an invite. Hold Shift when you are not in a session to switch this button into Join mode.")
  bInvite:SetScript("OnClick", function()
    local raw = tostring(invBox:GetText() or "")
    if inviteShiftJoin then
      local sid, host = ParseCompetitiveJoinInput(raw)
      if sid and UI and UI.JoinCompetitiveSession then
        UI:JoinCompetitiveSession(sid, host)
      elseif UI and UI.Notify then
        UI:Notify("What Farm?: Enter a session ID to join.")
      elseif UI and UI.Print then
        UI:Print("What Farm?: Enter a session ID to join.")
      elseif Notify then
        Notify("What Farm?: Enter a session ID to join.")
      end
    elseif UI and UI.InviteChallenge then
      UI:InviteChallenge(raw)
    end
    invBox:ClearFocus()
  end)

  local function ApplyCompInviteCardLayout()
    local collapsed = GetCompInviteCollapsed()
    local cardW = inviteCard and inviteCard.GetWidth and (inviteCard:GetWidth() or 0) or 0
    local stacked = cardW > 0 and cardW < 560
    local compact = cardW > 0 and cardW < 460
    local showInviteControls = true
    if UI and UI._comp and UI._comp._showInviteControls ~= nil then
      showInviteControls = UI._comp._showInviteControls and true or false
    end

    if bInviteToggle and bInviteToggle.label then
      bInviteToggle.label:SetText(collapsed and "Show" or "Hide")
    end

    invLabel:ClearAllPoints()
    invHelp:ClearAllPoints()
    invBox:ClearAllPoints()
    bInvite:ClearAllPoints()

    if collapsed then
      invLabel:Hide()
      invHelp:Hide()
      invBox:Hide()
      bInvite:Hide()
      inviteCard:SetHeight(30)
      UpdateCompLeaderboardScrollLayout()
      return
    end

    invLabel:Show()
    invLabel:SetPoint("TOPLEFT", inviteHeaderRule, "BOTTOMLEFT", 0, -5)
    invLabel:SetPoint("TOPRIGHT", inviteHeaderRule, "BOTTOMRIGHT", 0, -5)

    if not showInviteControls then
      invHelp:Show()
      invBox:Hide()
      bInvite:Hide()
      invHelp:SetPoint("TOPLEFT", invLabel, "BOTTOMLEFT", 0, -3)
      invHelp:SetPoint("TOPRIGHT", inviteHeaderRule, "BOTTOMRIGHT", 0, -3)
      inviteCard:SetHeight(68)
      UpdateCompLeaderboardScrollLayout()
      return
    end

    invBox:Show()
    bInvite:Show()

    if stacked then
      invHelp:Hide()
      invBox:SetPoint("TOPLEFT", invLabel, "BOTTOMLEFT", 0, -6)
      invBox:SetPoint("TOPRIGHT", inviteCard, "TOPRIGHT", -10, -36)
      bInvite:SetPoint("TOPLEFT", invBox, "BOTTOMLEFT", 0, -6)
      bInvite:SetPoint("TOPRIGHT", inviteCard, "TOPRIGHT", -10, -62)
      inviteCard:SetHeight(compact and 102 or 96)
    else
      invHelp:Show()
      invHelp:SetPoint("TOPLEFT", invLabel, "BOTTOMLEFT", 0, -2)
      invHelp:SetPoint("TOPRIGHT", inviteHeaderRule, "BOTTOMRIGHT", 0, -2)
      bInvite:SetPoint("TOPRIGHT", inviteCard, "TOPRIGHT", -10, -56)
      invBox:SetPoint("TOPLEFT", invHelp, "BOTTOMLEFT", 0, -6)
      invBox:SetPoint("TOPRIGHT", bInvite, "TOPLEFT", -8, 2)
      inviteCard:SetHeight(86)
    end

    UpdateCompLeaderboardScrollLayout()
  end

  bInviteToggle:SetScript("OnClick", function()
    SetCompInviteCollapsed(not GetCompInviteCollapsed())
    ApplyCompInviteCardLayout()
  end)

  inviteCard:HookScript("OnShow", ApplyCompInviteCardLayout)
  inviteCard:HookScript("OnSizeChanged", ApplyCompInviteCardLayout)

  local sessionCard = Widgets:Panel(lbContent)
  sessionCard:SetPoint("TOPLEFT", inviteCard, "BOTTOMLEFT", 0, -10)
  sessionCard:SetPoint("TOPRIGHT", inviteCard, "BOTTOMRIGHT", 0, -10)
  sessionCard:SetHeight(108)
  if sessionCard.SetClipsChildren then sessionCard:SetClipsChildren(true) end

  local sessionTitle = sessionCard:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
  sessionTitle:SetPoint("TOPLEFT", 10, -7)
  sessionTitle:SetPoint("RIGHT", sessionCard, "RIGHT", -72, 0)
  sessionTitle:SetJustifyH("LEFT")
  sessionTitle:SetText("Session Details")
  if Theme and Theme.accent then
    sessionTitle:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  end
  WFCompForceLine(sessionTitle)
  WFCompTip(sessionTitle, "Session Details", "Review the active session ID, state, host, profile, and scoring mode.")

  local bSessionToggle = Widgets:Button(sessionCard, "Hide", 54, 18)
  bSessionToggle:SetPoint("TOPRIGHT", -10, -5)
  WFCompTip(bSessionToggle, "Session Details", "Hide or show the session details box.")

  local sessionHeaderRule = sessionCard:CreateTexture(nil, "ARTWORK")
  sessionHeaderRule:SetHeight(1)
  sessionHeaderRule:SetPoint("TOPLEFT", sessionCard, "TOPLEFT", 10, -26)
  sessionHeaderRule:SetPoint("TOPRIGHT", sessionCard, "TOPRIGHT", -10, -26)
  sessionHeaderRule:SetColorTexture(1, 1, 1, 0.06)

  local summaryWrap = CreateFrame("Frame", nil, sessionCard)
  summaryWrap:SetPoint("TOPLEFT", 10, -32)
  summaryWrap:SetPoint("TOPRIGHT", -10, -32)
  summaryWrap:SetHeight(52)

  local chipSession = Widgets:MetricChip(summaryWrap, "Session")
  local chipStatus  = Widgets:MetricChip(summaryWrap, "Status")
  local chipMode    = Widgets:MetricChip(summaryWrap, "Mode")
  local chipHost    = Widgets:MetricChip(summaryWrap, "Host")
  local chipProfile = Widgets:MetricChip(summaryWrap, "Profile")
  local chipScoring = Widgets:MetricChip(summaryWrap, "Scoring")
  local summaryChips = { chipSession, chipStatus, chipMode, chipHost, chipProfile, chipScoring }

  local rosterWrap = Widgets:Panel(lbContent)
  if rosterWrap.SetClipsChildren then rosterWrap:SetClipsChildren(true) end

  local function LayoutCompSummary()
    if not (summaryWrap and summaryWrap.GetWidth) then return end
    local w = summaryWrap:GetWidth() or 0
    if w <= 50 then return end

    local cols = 3
    if w < 500 then cols = 2 end
    if w < 300 then cols = 1 end
    local gap = 6
    local chipH = 24
    local chipW = math.floor((w - ((cols - 1) * gap)) / cols)
    if chipW < 126 then chipW = 126 end

    for i, chip in ipairs(summaryChips) do
      local row = math.floor((i - 1) / cols)
      local col = (i - 1) % cols
      chip:ClearAllPoints()
      chip:SetPoint("TOPLEFT", summaryWrap, "TOPLEFT", col * (chipW + gap), -(row * (chipH + 6)))
      chip:SetSize(chipW, chipH)
    end

    local rowCount = math.ceil(#summaryChips / cols)
    summaryWrap:SetHeight((rowCount * chipH) + ((rowCount - 1) * 6))
  end

  compMeta = sessionCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compMeta:SetPoint("TOPLEFT", summaryWrap, "BOTTOMLEFT", 2, -6)
  compMeta:SetPoint("TOPRIGHT", summaryWrap, "BOTTOMRIGHT", -2, -6)
  compMeta:SetJustifyH("LEFT")
  compMeta:SetText("Session details will appear here.")
  if Theme and Theme.muted then
    compMeta:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end

  compMeta2 = sessionCard:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  compMeta2:SetPoint("TOPLEFT", compMeta, "BOTTOMLEFT", 0, -2)
  compMeta2:SetPoint("TOPRIGHT", compMeta, "BOTTOMRIGHT", 0, -2)
  compMeta2:SetJustifyH("LEFT")
  compMeta2:SetText("Hover players for details. Host rows keep right-click actions.")
  if Theme and Theme.muted then
    compMeta2:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], 0.95)
  end

  local function UpdateCompSessionTextDensity()
    local w = sessionCard and sessionCard.GetWidth and (sessionCard:GetWidth() or 0) or 0
    if w > 0 and w < 500 then
      compMeta2:Hide()
    else
      compMeta2:Show()
    end
  end

  local function ApplyCompSessionCardLayout()
    local collapsed = GetCompSessionDetailsCollapsed()
    if bSessionToggle and bSessionToggle.label then
      bSessionToggle.label:SetText(collapsed and "Show" or "Hide")
    end

    UpdateCompSessionTextDensity()

    if collapsed then
      summaryWrap:Hide()
      compMeta:Hide()
      compMeta2:Hide()
      sessionCard:SetHeight(30)
    else
      summaryWrap:Show()
      compMeta:Show()
      UpdateCompSessionTextDensity()
      local metaH = (compMeta.GetStringHeight and compMeta:GetStringHeight() or 12)
      if compMeta2 and compMeta2.IsShown and compMeta2:IsShown() then
        metaH = metaH + 2 + (compMeta2.GetStringHeight and compMeta2:GetStringHeight() or 12)
      end
      local expandedH = 44 + (summaryWrap:GetHeight() or 54) + metaH + 14
      sessionCard:SetHeight(math.max(102, expandedH))
    end
    UpdateCompLeaderboardScrollLayout()
  end

  bSessionToggle:SetScript("OnClick", function()
    SetCompSessionDetailsCollapsed(not GetCompSessionDetailsCollapsed())
    ApplyCompSessionCardLayout()
  end)

  summaryWrap:HookScript("OnShow", function()
    LayoutCompSummary()
    ApplyCompSessionCardLayout()
  end)
  summaryWrap:HookScript("OnSizeChanged", function()
    LayoutCompSummary()
    ApplyCompSessionCardLayout()
  end)
  sessionCard:HookScript("OnSizeChanged", function()
    UpdateCompSessionTextDensity()
    UpdateCompLeaderboardScrollLayout()
  end)
  LayoutCompSummary()

  rosterWrap:SetPoint("TOPLEFT", sessionCard, "BOTTOMLEFT", 0, -10)
  rosterWrap:SetPoint("TOPRIGHT", sessionCard, "BOTTOMRIGHT", 0, -10)
  rosterWrap:SetHeight(260)

  local rosterTitle = rosterWrap:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
  rosterTitle:SetPoint("TOPLEFT", 10, -7)
  rosterTitle:SetText("Roster")
  if Theme and Theme.accent then
    rosterTitle:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  end
  WFCompTip(rosterTitle, "Roster", "Hover a player for details. If you are the host, right-click a row for invite and removal controls.")

  local rosterHint = rosterWrap:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  rosterHint:SetPoint("LEFT", rosterTitle, "RIGHT", 8, 0)
  rosterHint:SetPoint("RIGHT", rosterWrap, "RIGHT", -10, 0)
  rosterHint:SetJustifyH("RIGHT")
  rosterHint:SetText("Right-click a row for competitor controls.")
  if Theme and Theme.muted then
    rosterHint:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  end
  WFCompForceLine(rosterHint)

  local rosterHeader = CreateFrame("Frame", nil, rosterWrap, "BackdropTemplate")
  rosterHeader:SetPoint("TOPLEFT", 10, -26)
  rosterHeader:SetPoint("TOPRIGHT", -10, -26)
  rosterHeader:SetHeight(20)
  rosterHeader:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
  rosterHeader:SetBackdropColor(1, 1, 1, 0.035)

  local function MakeRosterHeader(text, anchor, rel, x, y, width, justify)
    local fs = rosterHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    fs:SetPoint(anchor, rel or rosterHeader, anchor, x, y or 0)
    if width then fs:SetWidth(width) end
    fs:SetJustifyH(justify or "LEFT")
    fs:SetText(text)
    if Theme and Theme.muted then
      fs:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], 0.95)
    end
    return fs
  end

  local hdrRank = MakeRosterHeader("#", "LEFT", rosterHeader, 10, 0, 18, "LEFT")
  local hdrPlayer = MakeRosterHeader("Player", "LEFT", rosterHeader, 36, 0, nil, "LEFT")
  local hdrStatus = MakeRosterHeader("Status", "RIGHT", rosterHeader, -176, 0, 72, "LEFT")
  local hdrGold = MakeRosterHeader("Gold", "RIGHT", rosterHeader, -76, 0, 88, "RIGHT")
  local hdrGPH = MakeRosterHeader("GPH", "RIGHT", rosterHeader, -10, 0, 60, "RIGHT")
  WFCompForceLine(hdrRank)
  WFCompForceLine(hdrPlayer)
  WFCompForceLine(hdrStatus)
  WFCompForceLine(hdrGold)
  WFCompForceLine(hdrGPH)

  local compListScroll = CreateFrame("ScrollFrame", nil, rosterWrap, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and compListScroll.ScrollBar then Widgets:SkinScrollBar(compListScroll.ScrollBar) end
  compListScroll:SetPoint("TOPLEFT", rosterHeader, "BOTTOMLEFT", 0, -4)
  compListScroll:SetPoint("TOPRIGHT", rosterHeader, "BOTTOMRIGHT", 0, -4)
  compListScroll:SetPoint("BOTTOMLEFT", rosterWrap, "BOTTOMLEFT", 10, 8)
  compListScroll:SetPoint("BOTTOMRIGHT", rosterWrap, "BOTTOMRIGHT", -10, 8)
  if compListScroll.EnableMouseWheel then compListScroll:EnableMouseWheel(true) end

  local compList = CreateFrame("Frame", nil, compListScroll)
  compList:SetSize(1, 1)
  if compList.SetClipsChildren then compList:SetClipsChildren(true) end
  compListScroll:SetScrollChild(compList)
  compListScroll:SetScript("OnMouseWheel", function(self, delta)
    local step = 22
    local cur = self:GetVerticalScroll() or 0
    local max = 0
    if self.GetVerticalScrollRange then
      max = self:GetVerticalScrollRange() or 0
    end
    cur = cur - ((delta or 0) * step)
    if cur < 0 then cur = 0 end
    if cur > max then cur = max end
    self:SetVerticalScroll(cur)
  end)

  local compRows = {}
  local compVisibleRowCount = 0
  for i = 1, 24 do
    local r = CreateFrame("Frame", nil, compList)
    r:SetHeight(20)
    r:SetPoint("TOPLEFT", 0, -((i - 1) * 22))
    r:SetPoint("TOPRIGHT", 0, -((i - 1) * 22))
    if r.SetClipsChildren then r:SetClipsChildren(true) end

    local bg = r:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    if (i % 2) == 1 then
      bg:SetColorTexture(1, 1, 1, 0.02)
    else
      bg:SetColorTexture(1, 1, 1, 0.00)
    end

    local hl = r:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints()
    if Theme and Theme.accent then
      hl:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.08)
    else
      hl:SetColorTexture(1, 1, 1, 0.08)
    end

    local rank = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    rank:SetPoint("LEFT", 10, 0)
    rank:SetWidth(18)
    rank:SetJustifyH("LEFT")
    rank:SetText(tostring(i) .. ".")

    local status = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    status:SetPoint("RIGHT", r, "RIGHT", -176, 0)
    status:SetWidth(72)
    status:SetJustifyH("LEFT")

    local name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    name:SetPoint("LEFT", 36, 0)
    name:SetPoint("RIGHT", status, "LEFT", -10, 0)
    name:SetJustifyH("LEFT")

    local val = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    val:SetPoint("RIGHT", r, "RIGHT", -76, 0)
    val:SetWidth(88)
    val:SetJustifyH("RIGHT")

    local gph = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    gph:SetPoint("RIGHT", r, "RIGHT", -10, 0)
    gph:SetWidth(60)
    gph:SetJustifyH("RIGHT")

    r:EnableMouse(true)
    r:SetScript("OnEnter", function()
      if UI and UI.ShowCompetitiveTooltip then
        UI:ShowCompetitiveTooltip(r)
      else
        if GameTooltip then
          if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
          GameTooltip:SetOwner(r, "ANCHOR_RIGHT")
          GameTooltip:ClearLines()
          GameTooltip:AddLine(name:GetText() or "Competitor", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
          GameTooltip:AddLine((status:GetText() or "") .. "    Gold: " .. (val:GetText() or "--") .. "    GPH: " .. (gph:GetText() or "--"), 0.82, 0.82, 0.82, true)
          GameTooltip:Show()
        end
      end
    end)
    r:SetScript("OnLeave", function()
      if GameTooltip then GameTooltip:Hide() end
    end)

    WFCompForceLine(rank)
    WFCompForceLine(status)
    WFCompForceLine(name)
    WFCompForceLine(val)
    WFCompForceLine(gph)

    compRows[i] = {
      frame = r,
      rank = rank,
      name = name,
      status = status,
      value = val,
      gph = gph,
      bg = bg,
    }
  end

  local function LayoutCompRoster()
    local rowW = rosterHeader and rosterHeader.GetWidth and (rosterHeader:GetWidth() or 0) or 0
    if rowW <= 50 then return end

    if rosterHint then
      if rowW < 560 then
        rosterHint:Hide()
      else
        rosterHint:Show()
      end
    end

    local scrollReserve = (compListScroll and compListScroll.ScrollBar and compListScroll.ScrollBar:IsShown()) and 10 or 0
    local gphW = (rowW < 470) and 76 or 92
    local goldW = (rowW < 520) and 84 or 98
    local statusW = (rowW < 500) and 68 or 78
    local clusterPad = (rowW < 520) and 14 or 24
    local rowRightInset = 4
    local headerRightInset = rowRightInset + scrollReserve
    local gphRight = rowRightInset + clusterPad
    local goldRight = gphRight + gphW + 12
    local statusRight = goldRight + goldW + 16
    local hdrGphRight = headerRightInset + clusterPad
    local hdrGoldRight = hdrGphRight + gphW + 12
    local hdrStatusRight = hdrGoldRight + goldW + 16

    hdrRank:ClearAllPoints()
    hdrRank:SetPoint("LEFT", rosterHeader, "LEFT", 10, 0)
    hdrRank:SetWidth(18)

    hdrGPH:ClearAllPoints()
    hdrGPH:SetPoint("RIGHT", rosterHeader, "RIGHT", -hdrGphRight, 0)
    hdrGPH:SetWidth(gphW)

    hdrGold:ClearAllPoints()
    hdrGold:SetPoint("RIGHT", rosterHeader, "RIGHT", -hdrGoldRight, 0)
    hdrGold:SetWidth(goldW)

    hdrStatus:ClearAllPoints()
    hdrStatus:SetPoint("RIGHT", rosterHeader, "RIGHT", -hdrStatusRight, 0)
    hdrStatus:SetWidth(statusW)

    hdrPlayer:ClearAllPoints()
    hdrPlayer:SetPoint("LEFT", rosterHeader, "LEFT", 36, 0)
    hdrPlayer:SetPoint("RIGHT", hdrStatus, "LEFT", -8, 0)

    for _, row in ipairs(compRows) do
      if row and row.frame then
        row.rank:ClearAllPoints()
        row.rank:SetPoint("LEFT", row.frame, "LEFT", 10, 0)
        row.rank:SetWidth(18)

        row.gph:ClearAllPoints()
        row.gph:SetPoint("RIGHT", row.frame, "RIGHT", -gphRight, 0)
        row.gph:SetWidth(gphW)

        row.value:ClearAllPoints()
        row.value:SetPoint("RIGHT", row.frame, "RIGHT", -goldRight, 0)
        row.value:SetWidth(goldW)

        row.status:ClearAllPoints()
        row.status:SetPoint("RIGHT", row.frame, "RIGHT", -statusRight, 0)
        row.status:SetWidth(statusW)

        row.name:ClearAllPoints()
        row.name:SetPoint("LEFT", row.frame, "LEFT", 36, 0)
        row.name:SetPoint("RIGHT", row.status, "LEFT", -8, 0)
      end
    end
  end

  local function LayoutCompRosterScroll()
    if not (compListScroll and compList) then return end
    local viewW = compListScroll.GetWidth and (compListScroll:GetWidth() or 0) or 0
    local viewH = compListScroll.GetHeight and (compListScroll:GetHeight() or 0) or 0
    local rowCount = math.max(0, tonumber((UI and UI._comp and UI._comp.visibleRowCount) or compVisibleRowCount) or 0)
    local contentH = math.max(1, (rowCount * 22) - 2)

    local maxScroll = 0
    if contentH > viewH then
      maxScroll = contentH - viewH
    end

    if compListScroll.ScrollBar then
      if maxScroll > 0 then
        compListScroll.ScrollBar:Show()
        if compListScroll.ScrollBar.Enable then compListScroll.ScrollBar:Enable() end
      else
        compListScroll.ScrollBar:Hide()
        if compListScroll.ScrollBar.Disable then compListScroll.ScrollBar:Disable() end
      end
    end

    local contentW = math.max(1, viewW - ((compListScroll.ScrollBar and compListScroll.ScrollBar:IsShown()) and 10 or 0))
    compList:SetSize(contentW, contentH)

    local cur = compListScroll:GetVerticalScroll() or 0
    if maxScroll <= 0 then
      compListScroll:SetVerticalScroll(0)
    elseif cur > maxScroll then
      compListScroll:SetVerticalScroll(maxScroll)
    end
  end

  rosterHeader:HookScript("OnShow", LayoutCompRoster)
  rosterHeader:HookScript("OnSizeChanged", LayoutCompRoster)
  compList:HookScript("OnShow", LayoutCompRoster)
  compList:HookScript("OnSizeChanged", LayoutCompRoster)
  compListScroll:HookScript("OnShow", LayoutCompRosterScroll)
  compListScroll:HookScript("OnSizeChanged", LayoutCompRosterScroll)

  UpdateCompLeaderboardViewport = function()
    if not (lbScroll and lbContent) then return end

    local viewW = lbScroll.GetWidth and (lbScroll:GetWidth() or 0) or 0
    local viewH = lbScroll.GetHeight and (lbScroll:GetHeight() or 0) or 0
    local contentH = lbContent.GetHeight and (lbContent:GetHeight() or 0) or 0
    if viewW <= 0 or viewH <= 0 then return end

    local showOuterScroll = contentH > (viewH + 0.5)
    if lbScroll.ScrollBar then
      if showOuterScroll then
        lbScroll.ScrollBar:Show()
        if lbScroll.ScrollBar.Enable then lbScroll.ScrollBar:Enable() end
      else
        lbScroll.ScrollBar:Hide()
        if lbScroll.ScrollBar.Disable then lbScroll.ScrollBar:Disable() end
      end
    end

    local reserve = (lbScroll.ScrollBar and lbScroll.ScrollBar:IsShown()) and 18 or 0
    if lbContent.SetWidth then
      lbContent:SetWidth(math.max(1, viewW - reserve))
    end

    local maxScroll = math.max(0, contentH - viewH)
    local cur = lbScroll.GetVerticalScroll and (lbScroll:GetVerticalScroll() or 0) or 0
    if maxScroll <= 0 then
      if lbScroll.SetVerticalScroll then lbScroll:SetVerticalScroll(0) end
    elseif cur > maxScroll then
      if lbScroll.SetVerticalScroll then lbScroll:SetVerticalScroll(maxScroll) end
    end
  end

  UpdateCompLeaderboardScrollLayout = function()
    if not rosterWrap then return end

    local viewH = lbScroll and lbScroll.GetHeight and (lbScroll:GetHeight() or 0) or 0
    local actionShown = actionBar and actionBar.IsShown and actionBar:IsShown()
    local actionH = actionShown and actionBar.GetHeight and (actionBar:GetHeight() or 0) or 0
    local inviteH = inviteCard and inviteCard.GetHeight and (inviteCard:GetHeight() or 0) or 0
    local sessionH = sessionCard and sessionCard.GetHeight and (sessionCard:GetHeight() or 0) or 0
    local totalTop
    if actionShown then
      -- Actual anchor order: action controls -> session details -> invite -> roster.
      totalTop = actionH + 8 + sessionH + 8 + inviteH + 10
    else
      -- Actual anchor order: session details -> invite -> roster.
      totalTop = sessionH + 8 + inviteH + 10
    end
    local rowCount = math.max(0, tonumber((UI and UI._comp and UI._comp.visibleRowCount) or compVisibleRowCount) or 0)
    local rosterChromeH = 56
    local rosterBodyMinH = 96
    local rosterBodyIdealH = math.max(rosterBodyMinH, math.min(220, math.max(5, rowCount) * 22))
    local rosterAvailBodyH = viewH - totalTop - rosterChromeH - 12
    local rosterBodyH = rosterBodyIdealH
    if rosterAvailBodyH >= rosterBodyMinH then
      rosterBodyH = math.min(rosterBodyIdealH, rosterAvailBodyH)
    else
      rosterBodyH = rosterBodyMinH
    end

    local rosterH = rosterChromeH + rosterBodyH
    rosterWrap:SetHeight(rosterH)

    local contentH = totalTop + rosterH + 12
    if lbContent and lbContent.SetHeight then
      lbContent:SetHeight(math.max(contentH, viewH))
    end

    LayoutCompRoster()
    LayoutCompRosterScroll()
    UpdateCompLeaderboardViewport()
  end

  if lbPanel and lbPanel.HookScript then
    lbPanel:HookScript("OnShow", UpdateCompLeaderboardScrollLayout)
    lbPanel:HookScript("OnSizeChanged", UpdateCompLeaderboardScrollLayout)
  end
  if lbScroll and lbScroll.HookScript then
    lbScroll:HookScript("OnShow", UpdateCompLeaderboardScrollLayout)
    lbScroll:HookScript("OnSizeChanged", UpdateCompLeaderboardScrollLayout)
  end

  local function RefreshCompButtons()
    local sid = tostring(UI and UI._compSessionId or "")
    local sessState = tostring(UI and UI._compSession and UI._compSession.state or "idle")
    local isLeader = UI and UI._IsCompetitiveLeader and UI:_IsCompetitiveLeader()
    local hasSession = (sid ~= "")
    local allowLiveInvites = WFCompGetAllowInvites()
    local showStart = false
    local showPause = false
    local showQuit = false
    local showInvite = false

    if hasSession then
      if isLeader then
        showStart = (sessState == "idle" or sessState == "ended")
        showPause = (sessState == "live" or sessState == "paused")
        showQuit = (sessState == "countdown" or sessState == "live" or sessState == "paused")
        showInvite = (sessState == "idle" or sessState == "ended" or sessState == "countdown" or ((sessState == "live" or sessState == "paused") and allowLiveInvites))
      else
        showStart = false
        showPause = false
        showQuit = true
        showInvite = false
      end
    else
      showStart = true
      showPause = false
      showQuit = false
      showInvite = true
    end

    inviteShiftJoin = showInvite and (not hasSession) and IsShiftKeyDown and IsShiftKeyDown() or false

    if inviteTitle then
      inviteTitle:SetText(inviteShiftJoin and "Join Session" or "Invite")
    end
    if invLabel then
      if inviteShiftJoin then
        invLabel:SetText("Join a session by ID.")
      elseif showInvite then
        invLabel:SetText("Invite a player to this session.")
      elseif isLeader and (sessState == "live" or sessState == "paused") then
        invLabel:SetText("Session active, please wait until it's over to invite someone.")
      else
        invLabel:SetText("Invite access is unavailable for this session.")
      end
    end
    if bInvite and bInvite.label then
      bInvite.label:SetText(inviteShiftJoin and "Join" or "Invite")
    end

    if bPauseMatch and bPauseMatch.label then
      if sessState == "paused" then
        bPauseMatch.label:SetText("Resume Match")
      else
        bPauseMatch.label:SetText("Pause Match")
      end
    end

    if bQuitMatch and bQuitMatch.label then
      if isLeader then
        bQuitMatch.label:SetText("End Match")
      else
        if sessState == "live" or sessState == "paused" then
          bQuitMatch.label:SetText("Quit Match")
        else
          bQuitMatch.label:SetText("Leave Match")
        end
      end
    end

    -- Reanchor the leaderboard sections based on host vs participant.
    -- Host (actionBar visible):   actionBar → inviteCard → sessionCard → rosterWrap  (original order)
    -- Participant (no actionBar): sessionCard → inviteCard → rosterWrap  (details first, no gap)
  local function ReanchorCompSections()
    local showAction = actionBar and actionBar.IsShown and actionBar:IsShown()
    if showAction then
      -- Host: actionBar → sessionCard → inviteCard → rosterWrap
      sessionCard:ClearAllPoints()
      sessionCard:SetPoint("TOPLEFT",  actionBar, "BOTTOMLEFT",  0, -8)
      sessionCard:SetPoint("TOPRIGHT", actionBar, "BOTTOMRIGHT", 0, -8)

      inviteCard:ClearAllPoints()
      inviteCard:SetPoint("TOPLEFT",  sessionCard, "BOTTOMLEFT",  0, -8)
      inviteCard:SetPoint("TOPRIGHT", sessionCard, "BOTTOMRIGHT", 0, -8)

      rosterWrap:ClearAllPoints()
      rosterWrap:SetPoint("TOPLEFT",  inviteCard, "BOTTOMLEFT",  0, -10)
      rosterWrap:SetPoint("TOPRIGHT", inviteCard, "BOTTOMRIGHT", 0, -10)
    else
      -- Participant: session details on top, invite below, roster at bottom
      sessionCard:ClearAllPoints()
      sessionCard:SetPoint("TOPLEFT",  lbContent, "TOPLEFT",  0, 0)
      sessionCard:SetPoint("TOPRIGHT", lbContent, "TOPRIGHT", 0, 0)

      inviteCard:ClearAllPoints()
      inviteCard:SetPoint("TOPLEFT",  sessionCard, "BOTTOMLEFT",  0, -8)
      inviteCard:SetPoint("TOPRIGHT", sessionCard, "BOTTOMRIGHT", 0, -8)

      rosterWrap:ClearAllPoints()
      rosterWrap:SetPoint("TOPLEFT",  inviteCard, "BOTTOMLEFT",  0, -10)
      rosterWrap:SetPoint("TOPRIGHT", inviteCard, "BOTTOMRIGHT", 0, -10)
    end
  end

    if bStartChallenge then
      if showStart then bStartChallenge:Show() else bStartChallenge:Hide() end
    end
    if bPauseMatch then
      if showPause then bPauseMatch:Show() else bPauseMatch:Hide() end
    end
    if bQuitMatch then
      if showQuit then bQuitMatch:Show() else bQuitMatch:Hide() end
    end

    if UI and UI._comp then
      UI._comp._showInviteControls = showInvite and true or false
    end
    if invBox then
      if showInvite then invBox:Show() else invBox:Hide() end
      invBox:EnableMouse(showInvite)
      if invBox.SetEnabled then invBox:SetEnabled(showInvite) end
    end
    if bInvite then
      if showInvite then bInvite:Show() else bInvite:Hide() end
    end
    if invHelp then
      if showInvite then
        if inviteShiftJoin then
          invHelp:SetText("SessionID or: SessionID Host")
        elseif hasSession and isLeader and (sessState == "live" or sessState == "paused") and allowLiveInvites then
          invHelp:SetText("Live invites are enabled in Competitive Settings.")
        else
          invHelp:SetText("Player, Name-Realm, or BattleTag. Shift = Join.")
        end
      elseif isLeader and (sessState == "live" or sessState == "paused") then
        invHelp:SetText("Enable live invites in settings for mid-match joins.")
      else
        invHelp:SetText("Only the host can manage invites for this session.")
      end
    end
    LayoutCompActionButtons()
    if UI and UI._comp and UI._comp.applyInviteCardLayout then
      UI._comp.applyInviteCardLayout()
    end
    -- Show the controls row whenever there is a usable control.
    -- Non-host players still need this row for Leave Match, otherwise they can get stuck.
    if actionBar then
      actionBar:SetShown((showStart or showPause or showQuit) and true or false)
    end
    ReanchorCompSections()
    UpdateCompLeaderboardScrollLayout()
  end

  do
    local shiftTicker = 0
    local lastRefreshKey = nil
    compWin:HookScript("OnUpdate", function(_, elapsed)
      shiftTicker = shiftTicker + (elapsed or 0)
      if shiftTicker < 0.20 then return end
      shiftTicker = 0

      local shiftDown = (IsShiftKeyDown and IsShiftKeyDown()) and "1" or "0"
      local sid = tostring(UI and UI._compSessionId or "")
      local sessState = tostring(UI and UI._compSession and UI._compSession.state or "idle")
      local isLeader = (UI and UI._IsCompetitiveLeader and UI:_IsCompetitiveLeader()) and "1" or "0"
      local allowLiveInvites = WFCompGetAllowInvites() and "1" or "0"
      local refreshKey = sid .. "|" .. sessState .. "|" .. isLeader .. "|" .. allowLiveInvites .. "|" .. shiftDown

      if refreshKey ~= lastRefreshKey then
        lastRefreshKey = refreshKey
        if UI and UI._comp and UI._comp.refreshButtons then
          UI._comp.refreshButtons()
        end
      end

      if collectgarbage and UI and (GetTime() - (UI._wfCompWinGcAt or 0)) >= 2.5 then
        UI._wfCompWinGcAt = GetTime()
        pcall(collectgarbage, "step", 64)
      end
    end)
  end

  ApplyCompInviteCardLayout()
  ApplyCompSessionCardLayout()
  LayoutCompActionButtons()
  LayoutCompRoster()
  UpdateCompLeaderboardScrollLayout()
  -- -------------------------------------------------------
  -- Loot tab (overall)
  -- -------------------------------------------------------

-- Filter bar (clean + consistent with the rest of the addon)
local lootBar = CreateFrame("Frame", nil, lootPanel)
lootBar:SetPoint("TOPLEFT", lootPanel, "TOPLEFT", 0, 0)
lootBar:SetPoint("TOPRIGHT", lootPanel, "TOPRIGHT", 0, 0)
lootBar:SetHeight(24)

local lootFilterLabel = lootBar:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
lootFilterLabel:SetPoint("LEFT", 0, 0)
lootFilterLabel:SetText("Filter")
if Theme and Theme.accent then
  lootFilterLabel:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
end

local lootFilterBox = CreateFrame("EditBox", nil, lootBar, "InputBoxTemplate")
lootFilterBox:SetAutoFocus(false)
lootFilterBox:SetHeight(22)
lootFilterBox:SetPoint("LEFT", lootFilterLabel, "RIGHT", 8, 0)
lootFilterBox:SetPoint("RIGHT", lootBar, "RIGHT", 0, 0)
lootFilterBox:SetScript("OnEscapePressed", function() lootFilterBox:ClearFocus() end)
lootFilterBox:SetScript("OnTextChanged", function(selfBox, userInput)
  if not userInput then return end
  UI._comp = UI._comp or {}
  UI._comp.lootFilter = selfBox:GetText() or ""
  if UI and UI.RefreshCompetitiveLootPanel then
    UI:RefreshCompetitiveLootPanel(true)
  end
end)

local function SetCompLootSortMode(mode)
  UI._comp = UI._comp or {}
  mode = tostring(mode or "value")
  if mode ~= "name" and mode ~= "owner" and mode ~= "count" and mode ~= "each" and mode ~= "value" then
    mode = "value"
  end

  if UI._comp.lootSortMode == mode then
    local dir = tostring(UI._comp.lootSortDir or "desc")
    UI._comp.lootSortDir = (dir == "asc") and "desc" or "asc"
  else
    UI._comp.lootSortMode = mode
    UI._comp.lootSortDir = "desc"
  end

  UI._compLootDirty = true
  if UI and UI.RefreshCompetitiveLootPanel then
    UI:RefreshCompetitiveLootPanel(true)
  end
end

local lootCols = CreateFrame("Frame", nil, lootPanel)
lootCols:SetPoint("TOPLEFT", lootBar, "BOTTOMLEFT", 0, -8)
lootCols:SetPoint("TOPRIGHT", lootBar, "BOTTOMRIGHT", 0, -8)
lootCols:SetHeight(18)

local lootColsLine = lootCols:CreateTexture(nil, "ARTWORK")
lootColsLine:SetPoint("BOTTOMLEFT", 0, -2)
lootColsLine:SetPoint("BOTTOMRIGHT", 0, -2)
lootColsLine:SetHeight(1)
if Theme and Theme.border then
  lootColsLine:SetColorTexture(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4] or 1)
else
  lootColsLine:SetColorTexture(1, 1, 1, 0.08)
end

local LOOT_TOTAL_W = 88
local LOOT_EACH_W = 76
local LOOT_COUNT_W = 42
local LOOT_WHO_W = 108
local LOOT_ICON_W = 18
local LOOT_COL_GAP = 10

local lootColTotal = lootCols:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
lootColTotal:SetWidth(LOOT_TOTAL_W)
lootColTotal:SetPoint("RIGHT", -2, 0)
lootColTotal:SetJustifyH("RIGHT")
lootColTotal:SetText("Total")
WFCompForceLine(lootColTotal)

local lootColEach = lootCols:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
lootColEach:SetWidth(LOOT_EACH_W)
lootColEach:SetPoint("RIGHT", lootColTotal, "LEFT", -LOOT_COL_GAP, 0)
lootColEach:SetJustifyH("RIGHT")
lootColEach:SetText("Each")
WFCompForceLine(lootColEach)

local lootColCount = lootCols:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
lootColCount:SetWidth(LOOT_COUNT_W)
lootColCount:SetPoint("RIGHT", lootColEach, "LEFT", -LOOT_COL_GAP, 0)
lootColCount:SetJustifyH("RIGHT")
lootColCount:SetText("Qty")
WFCompForceLine(lootColCount)

local lootColWho = lootCols:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
lootColWho:SetWidth(LOOT_WHO_W)
lootColWho:SetPoint("RIGHT", lootColCount, "LEFT", -LOOT_COL_GAP, 0)
lootColWho:SetJustifyH("LEFT")
lootColWho:SetText("Player")
WFCompForceLine(lootColWho)

local lootColItem = lootCols:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
lootColItem:SetPoint("LEFT", LOOT_ICON_W + 6, 0)
lootColItem:SetPoint("RIGHT", lootColWho, "LEFT", -LOOT_COL_GAP, 0)
lootColItem:SetJustifyH("LEFT")
lootColItem:SetText("Item")
WFCompForceLine(lootColItem)

local function RefreshCompLootHeaderState()
  local mode = tostring(UI and UI._comp and UI._comp.lootSortMode or "value")
  local active = (Theme and Theme.text) or { 1, 1, 1, 1 }
  local inactive = (Theme and Theme.accent) or { 0.35, 0.78, 1.00, 1.00 }

  local function SetHeader(fs, key)
    if not (fs and fs.SetTextColor) then return end
    local c = (mode == key) and active or inactive
    local a = (mode == key) and (c[4] or 1) or 0.88
    fs:SetTextColor(c[1], c[2], c[3], a)
  end

  SetHeader(lootColItem, "name")
  SetHeader(lootColWho, "owner")
  SetHeader(lootColCount, "count")
  SetHeader(lootColEach, "each")
  SetHeader(lootColTotal, "value")
end

local function InitCompLootHeaderButton(leftObj, rightObj, sortKey)
  local b = CreateFrame("Button", nil, lootCols)
  b:SetPoint("TOP", lootCols, "TOP", 0, 0)
  b:SetPoint("BOTTOM", lootCols, "BOTTOM", 0, 0)
  b:SetPoint("LEFT", leftObj, "LEFT", 0, 0)
  b:SetPoint("RIGHT", rightObj, "RIGHT", 0, 0)

  local hl = b:CreateTexture(nil, "BACKGROUND")
  hl:SetAllPoints(b)
  hl:SetTexture("Interface/Buttons/WHITE8x8")
  hl:SetVertexColor(1, 1, 1, 1)
  hl:SetAlpha(0)
  b._hl = hl

  b:EnableMouse(true)
  if b.RegisterForClicks then
    b:RegisterForClicks("LeftButtonUp")
  end
  if lootCols and lootCols.GetFrameLevel and b.SetFrameLevel then
    b:SetFrameLevel(lootCols:GetFrameLevel() + 2)
  end
  b:SetScript("OnEnter", function(selfBtn)
    if selfBtn._hl then selfBtn._hl:SetAlpha(0.08) end
  end)
  b:SetScript("OnLeave", function(selfBtn)
    if selfBtn._hl then selfBtn._hl:SetAlpha(0) end
  end)
  b:SetScript("OnClick", function(_, button)
    if button ~= "LeftButton" then return end
    SetCompLootSortMode(sortKey)
    RefreshCompLootHeaderState()
  end)
  return b
end

InitCompLootHeaderButton(lootCols, lootColItem, "name")
InitCompLootHeaderButton(lootColWho, lootColWho, "owner")
InitCompLootHeaderButton(lootColCount, lootColCount, "count")
InitCompLootHeaderButton(lootColEach, lootColEach, "each")
InitCompLootHeaderButton(lootColTotal, lootCols, "value")
RefreshCompLootHeaderState()

if lootPanel and lootPanel.HookScript then
  lootPanel:HookScript("OnShow", RefreshCompLootHeaderState)
end

local lootScroll = CreateFrame("ScrollFrame", nil, lootPanel, "UIPanelScrollFrameTemplate")
if Widgets and Widgets.SkinScrollBar and lootScroll.ScrollBar then Widgets:SkinScrollBar(lootScroll.ScrollBar) end
lootScroll:SetPoint("TOPLEFT", lootCols, "BOTTOMLEFT", 0, -6)
lootScroll:SetPoint("BOTTOMRIGHT", 0, 0)
lootScroll._wfLastLootScroll = 0

local function MarkCompetitiveLootScrolled()
  if lootScroll then
    lootScroll._wfLastLootScroll = GetTime and GetTime() or 0
  end
end

if lootScroll.HookScript then
  lootScroll:HookScript("OnMouseWheel", MarkCompetitiveLootScrolled)
end

local lootScrollBar = lootScroll.ScrollBar
if (not lootScrollBar) and lootScroll.GetName then
  local lootScrollName = lootScroll:GetName()
  if lootScrollName then
    lootScrollBar = _G and _G[lootScrollName .. "ScrollBar"]
  end
end
if lootScrollBar and lootScrollBar.HookScript then
  lootScrollBar:HookScript("OnValueChanged", MarkCompetitiveLootScrolled)
end

local function CompetitiveLootRecentlyScrolled()
  if not (lootScroll and lootScroll._wfLastLootScroll and GetTime) then
    return false
  end
  return (GetTime() - lootScroll._wfLastLootScroll) < 0.20
end

local lootChild = CreateFrame("Frame", nil, lootScroll)
lootChild:SetSize(1, 1)
lootScroll:SetScrollChild(lootChild)

local function SyncCompetitiveLootScrollLayout()
  local width = tonumber(lootScroll:GetWidth()) or 0
  local scrollBar = lootScroll.ScrollBar
  if scrollBar and scrollBar.IsShown and scrollBar:IsShown() then
    width = width - ((tonumber(scrollBar:GetWidth()) or 0) + 6)
  end
  if width < 120 then
    width = 120
  end
  lootChild:SetWidth(width)
end

if lootScroll.HookScript then
  lootScroll:HookScript("OnSizeChanged", SyncCompetitiveLootScrollLayout)
end
if lootPanel and lootPanel.HookScript then
  lootPanel:HookScript("OnShow", SyncCompetitiveLootScrollLayout)
end

local lootRows = {}
local ROW_H = 22
local ROW_GAP = 2
local MIN_LOOT_ROWS = 40
local COMP_LOOT_TOOLTIP_DELAY = 0.15

local function CreateCompetitiveLootRow(index)
  local r = CreateFrame("Button", nil, lootChild)
  r:SetHeight(ROW_H)
  r:SetPoint("TOPLEFT", 0, -((index - 1) * (ROW_H + ROW_GAP)))
  r:SetPoint("TOPRIGHT", -2, -((index - 1) * (ROW_H + ROW_GAP)))

  local bg = r:CreateTexture(nil, "BACKGROUND")
  bg:SetAllPoints()
  if Theme and Theme.panel2 then
    local baseA = (index % 2 == 0) and 0.40 or 0.22
    bg:SetColorTexture(Theme.panel2[1], Theme.panel2[2], Theme.panel2[3], baseA)
  else
    bg:SetColorTexture(1, 1, 1, (index % 2 == 0) and 0.06 or 0.03)
  end

  local hover = r:CreateTexture(nil, "HIGHLIGHT")
  hover:SetAllPoints()
  if Theme and Theme.accent then
    hover:SetColorTexture(Theme.accent[1], Theme.accent[2], Theme.accent[3], 0.08)
  else
    hover:SetColorTexture(1, 1, 1, 0.06)
  end

  local sep = r:CreateTexture(nil, "BORDER")
  sep:SetPoint("BOTTOMLEFT", 0, 0)
  sep:SetPoint("BOTTOMRIGHT", 0, 0)
  sep:SetHeight(1)
  if Theme and Theme.border then
    sep:SetColorTexture(Theme.border[1], Theme.border[2], Theme.border[3], 0.55)
  else
    sep:SetColorTexture(1, 1, 1, 0.06)
  end

  local total = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
  total:SetWidth(LOOT_TOTAL_W)
  total:SetPoint("RIGHT", -2, 0)
  total:SetJustifyH("RIGHT")
  WFCompForceLine(total)

  local each = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  each:SetWidth(LOOT_EACH_W)
  each:SetPoint("RIGHT", total, "LEFT", -LOOT_COL_GAP, 0)
  each:SetJustifyH("RIGHT")
  WFCompForceLine(each)

  local count = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
  count:SetWidth(LOOT_COUNT_W)
  count:SetPoint("RIGHT", each, "LEFT", -LOOT_COL_GAP, 0)
  count:SetJustifyH("RIGHT")
  WFCompForceLine(count)

  local who = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  who:SetWidth(LOOT_WHO_W)
  who:SetPoint("RIGHT", count, "LEFT", -LOOT_COL_GAP, 0)
  who:SetJustifyH("LEFT")
  WFCompForceLine(who)

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

  local name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
  name:SetPoint("LEFT", icon, "RIGHT", 8, 0)
  name:SetPoint("RIGHT", who, "LEFT", -LOOT_COL_GAP, 0)
  name:SetJustifyH("LEFT")
  WFCompForceLine(name)

  r:EnableMouse(true)
  if r.RegisterForClicks then
    r:RegisterForClicks("LeftButtonUp")
  end
  r:SetScript("OnClick", function(selfRow)
    if not selfRow._wfLootExpandable then
      return
    end
    if UI and UI.ToggleCompetitiveLootItemExpansion then
      UI:ToggleCompetitiveLootItemExpansion(selfRow._wfLootItemID)
    end
  end)
  local function ShowCompetitiveLootTooltip(selfRow, token)
    if not (selfRow and selfRow:IsShown() and GameTooltip) then return end
    if token ~= selfRow._wfLootTipToken then return end
    if CompetitiveLootRecentlyScrolled() then return end
    if selfRow.IsMouseOver and not selfRow:IsMouseOver() then return end
    if not selfRow._wfLootItemID then return end

    WF_Tip_AnchorSide(selfRow, false, 12, 0)
    GameTooltip:ClearLines()

    local link = "item:" .. tostring(selfRow._wfLootItemID)
    local okLink = WF_Tip_TrySetHyperlink(link)
    if not okLink then
      GameTooltip:ClearLines()
      GameTooltip:SetText("Item ID: " .. tostring(selfRow._wfLootItemID), 1, 1, 1, true)
      GameTooltip:AddLine("Tooltip unavailable", 0.70, 0.70, 0.70, true)
    end

    if selfRow._wfLootOwnersText and selfRow._wfLootOwnersText ~= "" then
      GameTooltip:AddLine(" ")
      GameTooltip:AddDoubleLine("Looted By", tostring(selfRow._wfLootOwnersText), 0.75, 0.75, 0.75, 1, 1, 1)
    end

    WF_Tip_FinalizeSide(selfRow, false)
  end

  r:SetScript("OnEnter", function(selfRow)
    selfRow._wfLootTipToken = (selfRow._wfLootTipToken or 0) + 1
    local token = selfRow._wfLootTipToken

    if not selfRow._wfLootItemID then return end
    if not (C_Timer and C_Timer.After) then
      ShowCompetitiveLootTooltip(selfRow, token)
      return
    end

    C_Timer.After(COMP_LOOT_TOOLTIP_DELAY, function()
      ShowCompetitiveLootTooltip(selfRow, token)
    end)
  end)
  r:SetScript("OnLeave", function(selfRow)
    selfRow._wfLootTipToken = (selfRow._wfLootTipToken or 0) + 1
    if GameTooltip then GameTooltip:Hide() end
  end)

  return {
    frame = r,
    bg = bg,
    icon = icon,
    name = name,
    who = who,
    count = count,
    each = each,
    total = total,
  }
end

local function EnsureCompetitiveLootRows(want)
  want = tonumber(want) or MIN_LOOT_ROWS
  if want < MIN_LOOT_ROWS then
    want = MIN_LOOT_ROWS
  end
  if want > 250 then
    want = 250
  end

  for i = #lootRows + 1, want do
    lootRows[i] = CreateCompetitiveLootRow(i)
  end
end

EnsureCompetitiveLootRows(MIN_LOOT_ROWS)
SyncCompetitiveLootScrollLayout()
self._compEnsureLootRows = EnsureCompetitiveLootRows
self._compSyncLootLayout = SyncCompetitiveLootScrollLayout

  RefreshCompButtons()
-- -------------------------------------------------------
  -- Button bar (can "wrap" in Compact)
  -- -------------------------------------------------------
  local btnBar = CreateFrame("Frame", nil, f)
  btnBar:SetPoint("BOTTOMLEFT", 10, 10)
  btnBar:SetPoint("BOTTOMRIGHT", -10, 10)
  btnBar:SetHeight(28)

  local bStart = Widgets:Button(btnBar, "Start", 90, 24)
  bStart:SetPoint("LEFT", 0, 0)

  local bStop = Widgets:Button(btnBar, "Pause", 90, 24)
  bStop:SetPoint("LEFT", bStart, "RIGHT", 10, 0)

  local bReset = Widgets:Button(btnBar, "Reset", 90, 24)
  bReset:SetPoint("LEFT", bStop, "RIGHT", 10, 0)

  local bRandom = Widgets:Button(btnBar, "Random", 90, 24)
  bRandom:SetPoint("LEFT", bReset, "RIGHT", 10, 0)

  local bSave = Widgets:Button(btnBar, "Save Farm", 110, 24)
  bSave:SetPoint("LEFT", bRandom, "RIGHT", 10, 0)

  local bResetDungeon = Widgets:Button(btnBar, "Reset Dungeon", 150, 24)
  bResetDungeon:SetPoint("LEFT", bSave, "RIGHT", 10, 0)

  -- -------------------------------------------------------
  -- Scroll sizing
  -- -------------------------------------------------------
  local function UpdateScrollWidth()
    local w = scroll:GetWidth()
    if not w or w <= 1 then
      return
    end
    content:SetWidth(w)
  end

  scroll:HookScript("OnSizeChanged", function()
    UpdateScrollWidth()
    UI:RefreshLoot(true)
  end)

  f:HookScript("OnShow", function()
    C_Timer.After(0, function()
      if UI.frame and UI.frame:IsShown() then
        UpdateScrollWidth()
        UI:ApplyLayoutMode() -- in case settings changed while hidden
        if UI.RefreshProfileUI then UI:RefreshProfileUI() end
        UI:RefreshTitle()
        UI:RefreshMetrics()
        UI:RefreshLoot(true)
      end
    end)
  end)

  f:HookScript("OnHide", function()
    if UI and UI.PersistLayoutSize then
      UI:PersistLayoutSize()
    end
  end)



  -- -------------------------------------------------------
  -- Row interaction: right-click removes an item from the CURRENT run
  -- -------------------------------------------------------
  local function _WF_EntryItemID(e)
    if type(e) ~= "table" then return nil end
    local id = tonumber(e.itemID or e.id or e.baseItemID or e.itemId)
    if (not id) and type(e.link) == "string" then
      id = tonumber(e.link:match("item:(%d+)"))
    end
    return id
  end

  local function _WF_TryTracker(fn, ...)
    local f = Tracker and Tracker[fn]
    if type(f) ~= "function" then return false end
    local ok, ret = pcall(f, Tracker, ...)
    if not ok then return false end
    if ret == false then return false end
    return true
  end

  local function _WF_RemoveFromTable(t, keys, entry)
    if type(t) ~= "table" then return false end
    local removed = false

    for i = 1, #keys do
      local k = keys[i]
      if k ~= nil and rawget(t, k) ~= nil then
        t[k] = nil
        removed = true
      end
    end

    local n = #t
    if n and n > 0 then
      for i = n, 1, -1 do
        local v = t[i]
        if v == entry or (type(v) == "table" and (v.entry == entry or v._entry == entry)) then
          table.remove(t, i)
          removed = true
        end
      end
    end

    for k, v in pairs(t) do
      if v == entry or (type(v) == "table" and (v.entry == entry or v._entry == entry)) then
        t[k] = nil
        removed = true
      end
    end

    return removed
  end

  local function _WF_MarkTrackerDirty()
    if not Tracker then return end
    if Tracker.SetDirty then pcall(Tracker.SetDirty, Tracker, true) end
    if Tracker.MarkDirty then pcall(Tracker.MarkDirty, Tracker) end
    if Tracker._dirty ~= nil then Tracker._dirty = true end
  end

  local function _WF_RemoveEntryFromRun(entry)
    if type(entry) ~= "table" then return false end

    local id = _WF_EntryItemID(entry)
    local link = entry.link

    -- Preferred: Tracker exposes an API
    if _WF_TryTracker("RemoveEntry", entry) then return true end
    if _WF_TryTracker("DeleteEntry", entry) then return true end
    if id and _WF_TryTracker("RemoveItem", id) then return true end
    if link and _WF_TryTracker("RemoveItem", link) then return true end
    if id and _WF_TryTracker("RemoveItemByID", id) then return true end
    if link and _WF_TryTracker("RemoveItemByLink", link) then return true end
    if id and _WF_TryTracker("DeleteItem", id) then return true end

    -- Fallback: delete from common containers
    local keys = {}
    if id then
      keys[#keys + 1] = id
      keys[#keys + 1] = tostring(id)
    end
    if link and link ~= "" then
      keys[#keys + 1] = link
    end

    local removed = false
    local function try(t)
      if _WF_RemoveFromTable(t, keys, entry) then
        removed = true
      end
    end

    -- Tracker-level
    try(Tracker and Tracker.items)
    try(Tracker and Tracker._items)
    try(Tracker and Tracker.itemMap)
    try(Tracker and Tracker._itemMap)
    try(Tracker and Tracker.entries)
    try(Tracker and Tracker._entries)
    try(Tracker and Tracker.loot)
    try(Tracker and Tracker._loot)

    -- Session-level
    local sess = Tracker and Tracker.current
    if type(sess) == "table" then
      try(sess.items)
      try(sess._items)
      try(sess.itemMap)
      try(sess._itemMap)
      try(sess.entries)
      try(sess._entries)
      try(sess.loot)
      try(sess._loot)

      -- Best-effort adjust cached totals so the UI updates immediately.
      if removed and sess.totalValue ~= nil then
        local dv = tonumber(entry.value) or 0
        local tv = (tonumber(sess.totalValue) or 0) - dv
        if tv < 0 then tv = 0 end
        sess.totalValue = tv
      end
    end

    if removed then
      _WF_MarkTrackerDirty()
      return true
    end

    return false
  end

  -- -------------------------------------------------------
  -- Row context menu (right-click)
  -- WhatFarm-styled mini menu (no Blizzard dropdown skin)
  -- -------------------------------------------------------
  local WF_RunRowMenuFrame
  local WF_RunRowMenuCatcher
  local WF_RunRowMenuButtons = {}

  local function WF_HideRowMenu()
    if WF_RunRowMenuFrame then WF_RunRowMenuFrame:Hide() end
    if WF_RunRowMenuCatcher then WF_RunRowMenuCatcher:Hide() end
  end

  local function WF_EnsureRowMenu()
    if WF_RunRowMenuFrame and WF_RunRowMenuCatcher then
      return WF_RunRowMenuFrame
    end

    -- Click-catcher to close the menu safely (prevents accidental clicks through).
    WF_RunRowMenuCatcher = CreateFrame("Button", nil, UIParent)
    WF_RunRowMenuCatcher:SetAllPoints(UIParent)
    WF_RunRowMenuCatcher:SetFrameStrata("TOOLTIP")
    WF_RunRowMenuCatcher:SetFrameLevel(998)
    WF_RunRowMenuCatcher:EnableMouse(true)
    WF_RunRowMenuCatcher:Hide()
    WF_RunRowMenuCatcher:SetScript("OnMouseDown", function()
      WF_HideRowMenu()
    end)
    if WF_RunRowMenuCatcher.EnableKeyboard then WF_RunRowMenuCatcher:EnableKeyboard(true) end
    WF_RunRowMenuCatcher:SetScript("OnKeyDown", function(_, key)
      if key == "ESCAPE" then
        WF_HideRowMenu()
      end
    end)

    WF_RunRowMenuFrame = CreateFrame("Frame", "WF_RunRowContextMenu", UIParent, "BackdropTemplate")
    WF_RunRowMenuFrame:SetFrameStrata("TOOLTIP")
    WF_RunRowMenuFrame:SetFrameLevel(999)
    WF_RunRowMenuFrame:EnableMouse(true)
    if WF_RunRowMenuFrame.SetClampedToScreen then WF_RunRowMenuFrame:SetClampedToScreen(true) end
    WF_RunRowMenuFrame:Hide()

    WF_RunRowMenuFrame:SetBackdrop({
      bgFile = "Interface/Tooltips/UI-Tooltip-Background",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 14,
      insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    WF_RunRowMenuFrame:SetBackdropColor(Theme.bg[1], Theme.bg[2], Theme.bg[3], Theme.bg[4])
    WF_RunRowMenuFrame:SetBackdropBorderColor(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4])

    WF_RunRowMenuFrame.title = WF_RunRowMenuFrame:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
    WF_RunRowMenuFrame.title:SetPoint("TOPLEFT", WF_RunRowMenuFrame, "TOPLEFT", 10, -8)
    WF_RunRowMenuFrame.title:SetJustifyH("LEFT")
    WF_RunRowMenuFrame.title:SetJustifyV("TOP")
    WF_RunRowMenuFrame.title:SetWordWrap(true)

    WF_RunRowMenuFrame.div = WF_RunRowMenuFrame:CreateTexture(nil, "BORDER")
    WF_RunRowMenuFrame.div:SetHeight((Widgets and Widgets.PixelSize) and Widgets:PixelSize(WF_RunRowMenuFrame) or 1)
    if Widgets and Widgets.MakePixelPerfect then Widgets:MakePixelPerfect(WF_RunRowMenuFrame.div, WF_RunRowMenuFrame) end

    local function GetBtn(i)
      local b = WF_RunRowMenuButtons[i]
      if b then return b end
      b = CreateFrame("Button", nil, WF_RunRowMenuFrame)
      b:SetHeight(18)
      b:SetScript("OnClick", function(self)
        local fn = self._wf_onclick
        WF_HideRowMenu()
        if fn then fn() end
      end)

      b.text = b:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      b.text:SetPoint("LEFT", b, "LEFT", 10, 0)
      b.text:SetJustifyH("LEFT")

      local ar, ag, ab = 0.30, 0.70, 1.00
      if Theme and Theme.accent then
        ar, ag, ab = Theme.accent[1], Theme.accent[2], Theme.accent[3]
      end

      b.hi = b:CreateTexture(nil, "HIGHLIGHT")
      b.hi:SetAllPoints(b)
      b.hi:SetColorTexture(ar, ag, ab, 0.18)

      local function ApplyTextColor(self, isHover)
        if not (self and self.text) then return end
        if isHover and self._wf_textColorHover then
          local c = self._wf_textColorHover
          self.text:SetTextColor(c[1], c[2], c[3], c[4] or 1)
          return
        end
        if self._wf_textColor then
          local c = self._wf_textColor
          self.text:SetTextColor(c[1], c[2], c[3], c[4] or 1)
          return
        end
        if self._wf_isDanger then
          self.text:SetTextColor(ar, ag, ab, 1)
        elseif Theme and Theme.text then
          self.text:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
        end
      end

      b:SetScript("OnEnter", function(self)
        ApplyTextColor(self, true)
      end)
      b:SetScript("OnLeave", function(self)
        ApplyTextColor(self, false)
      end)

      WF_RunRowMenuButtons[i] = b
      return b
    end

    WF_RunRowMenuFrame._wf_getbtn = GetBtn
    return WF_RunRowMenuFrame
  end

  local function WF_PositionRowMenu(frame)
    if not (frame and UIParent and GetCursorPosition) then return end
    local x, y = GetCursorPosition()
    local s = UIParent:GetEffectiveScale() or 1
    x = (x or 0) / s
    y = (y or 0) / s

    local uiW = UIParent:GetWidth() or 0
    local uiH = UIParent:GetHeight() or 0
    local w = frame:GetWidth() or 0
    local h = frame:GetHeight() or 0

    local pad = 10
    local left = x + 10
    local top = y - 10

    if left + w > uiW - pad then
      left = (uiW - pad) - w
    end
    if left < pad then left = pad end

    if top > uiH - pad then top = uiH - pad end
    if top - h < pad then
      top = pad + h
    end

    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
  end

  local function WF_ShowRowMenu(row, entry)
    if not (row and entry) then return end

    local menuFrame = WF_EnsureRowMenu()

    local title = entry.link or entry.name or "Item"
    menuFrame.title:SetText(title)

    local maxW = 0
    local function w(fs)
      if not fs then return 0 end
      local v = fs:GetStringWidth() or 0
      if v > maxW then maxW = v end
      return v
    end
    -- Buttons
    local ar, ag, ab = 0.30, 0.70, 1.00
    if Theme and Theme.accent then
      ar, ag, ab = Theme.accent[1], Theme.accent[2], Theme.accent[3]
    end

    local b1 = menuFrame._wf_getbtn(1)
    b1._wf_isDanger = false
    b1.text:SetText("Remove from run")
    b1._wf_textColor = { ar, ag, ab, 1 }
    b1._wf_textColorHover = { ar, ag, ab, 1 }
    b1.text:SetTextColor(ar, ag, ab, 1)
    b1._wf_onclick = function()
      local ok = _WF_RemoveEntryFromRun(entry)
      if ok then
        if UI and UI.RefreshLoot then UI:RefreshLoot(true) end
        if UI and UI.RefreshMetrics then UI:RefreshMetrics() end
      else
        if UIErrorsFrame and UIErrorsFrame.AddMessage then
          UIErrorsFrame:AddMessage("What Farm?: Couldn't remove that item from this run.", 0.35, 0.78, 1.00, 1)
        end
      end
    end

    local b2 = menuFrame._wf_getbtn(2)
    b2._wf_isDanger = false
    b2._wf_textColor = nil
    b2._wf_textColorHover = nil
    b2.text:SetText(CANCEL or "Cancel")
    if Theme and Theme.text then
      b2.text:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4] or 1)
    end
    b2._wf_onclick = function()
      -- just close
    end

    -- Hide any extra buttons
    for i = 3, #WF_RunRowMenuButtons do
      if WF_RunRowMenuButtons[i] then
        WF_RunRowMenuButtons[i]:Hide()
      end
    end

    -- Compute width (cap, wrap title)
    local MAX_W = 300
    menuFrame.title:SetWidth(MAX_W - 20)
    w(menuFrame.title)
    w(b1.text)
    w(b2.text)

    local width = math.ceil(math.min(MAX_W, maxW + 34))

    -- Layout
    local xPad = 8
    local yPadTop = 8
    local yGap = 6

    menuFrame:SetWidth(width)
    -- divider spans
    menuFrame.div:ClearAllPoints()
    menuFrame.div:SetColorTexture(Theme.border[1], Theme.border[2], Theme.border[3], 0.35)
    menuFrame.div:SetPoint("TOPLEFT", menuFrame, "TOPLEFT", 8, - (yPadTop + (menuFrame.title:GetStringHeight() or 0) + 10))
    menuFrame.div:SetPoint("TOPRIGHT", menuFrame, "TOPRIGHT", -8, - (yPadTop + (menuFrame.title:GetStringHeight() or 0) + 10))

    local btnW = width - (xPad * 2)

    b1:ClearAllPoints()
    b1:SetPoint("TOPLEFT", menuFrame, "TOPLEFT", xPad, - (yPadTop + (menuFrame.title:GetStringHeight() or 0) + 12))
    b1:SetSize(btnW, 18)
    b1:Show()

    b2:ClearAllPoints()
    b2:SetPoint("TOPLEFT", b1, "BOTTOMLEFT", 0, -yGap)
    b2:SetSize(btnW, 18)
    b2:Show()

    local totalH = (yPadTop + (menuFrame.title:GetStringHeight() or 0) + 12) + 18 + yGap + 18 + 10
    menuFrame:SetHeight(totalH)

    GameTooltip:Hide()

    WF_RunRowMenuCatcher:Show()
    menuFrame:Show()
    WF_PositionRowMenu(menuFrame)
  end

  -- row tooltip
  for i = 1, #rows do
    local row = rows[i]

    row:SetScript("OnEnter", function(self)
      self._hoverToken = (self._hoverToken or 0) + 1
      local tok = self._hoverToken

      local e = self._entry
      if not e then
        return
      end

      -- Lightweight tooltip immediately (doesn't force item info loads).
      local title = e.link or e.name or "Item"

      WF_Tip_AnchorSide(self, false, 12, 0)
      GameTooltip:ClearLines()
      GameTooltip:AddLine(title, 1, 1, 1, true)

      if WF.DB:GetSetting("tooltipShowSource") then
        local src = e.lastSource
        if src and src ~= "" then
          GameTooltip:AddLine("Dropped by: " .. src, 0.70, 0.70, 0.70, true)
        end
      end

      GameTooltip:AddLine(" ")
      GameTooltip:AddLine("Hover to load full tooltip", 0.70, 0.70, 0.70, true)
      GameTooltip:AddLine("Right-click: actions", 0.70, 0.70, 0.70, true)
      WF_Tip_FinalizeSide(self, false)

      -- Upgrade to full item tooltip only after a brief hover AND no recent scroll.
      if C_Timer and C_Timer.After then
        C_Timer.After(0.15, function()
          if not self or not self:IsShown() then return end
          if tok ~= self._hoverToken then return end

          local sf = UI and UI.scroll
          if sf and sf._wfLastScroll and (GetTime() - sf._wfLastScroll) < 0.20 then return end

          local e2 = self._entry
          if not e2 or not e2.link then return end

          WF_Tip_AnchorSide(self, false, 12, 0)
          GameTooltip:ClearLines()
          local okLink = WF_Tip_TrySetHyperlink(e2.link)
          if not okLink then
            -- Fallback: show a lightweight tooltip without hyperlink (prevents MoneyFrame secret-value crashes).
            GameTooltip:ClearLines()
            local title = (e2 and (e2.name or e2.itemName)) or "Item"
            GameTooltip:SetText(title, 1, 1, 1, true)
            GameTooltip:AddLine("Full tooltip unavailable", 0.70, 0.70, 0.70, true)
          end

          if WF.DB:GetSetting("tooltipShowSource") then
            local src = e2.lastSource
            if src and src ~= "" then
              GameTooltip:AddLine(" ")
              GameTooltip:AddLine("Dropped by: " .. src, 0.70, 0.70, 0.70, true)
            end
          end

          GameTooltip:AddLine(" ")
          GameTooltip:AddLine("Right-click: actions", 0.70, 0.70, 0.70, true)
          WF_Tip_FinalizeSide(self, false)
        end)
      end
    end)

    row:SetScript("OnLeave", function(self)
      self._hoverToken = (self._hoverToken or 0) + 1
      GameTooltip:Hide()
    end)

    if row.RegisterForClicks then
      row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    end

    row:SetScript("OnClick", function(self, btn)
      if btn ~= "RightButton" then return end
      local e = self._entry
      if not e then return end
      WF_ShowRowMenu(self, e)
    end)
  end

  -- -------------------------------------------------------
  -- Button logic (Start / Pause / Stop)
  -- -------------------------------------------------------
  UI:_EnsurePauseHooks()
  if UI._RestorePauseStateFromSession then
    UI:_RestorePauseStateFromSession()
  end

  local function AttachMainControlTooltip(button, titleText, normalText, actionLabel)
    if not button then return end

    button:SetScript("OnEnter", function(selfBtn)
      WF_Tip_AnchorCentered(selfBtn)
      GameTooltip:ClearLines()
      GameTooltip:AddLine(titleText, Theme.accent[1], Theme.accent[2], Theme.accent[3], true)

      local locked, sessState, isLeader = UI:_IsCompetitiveMainControlLocked()
      if locked then
        GameTooltip:AddLine("Competitive session active. Main controls locked.", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
        if isLeader then
          if sessState == "countdown" then
            GameTooltip:AddLine("The match is already starting from the Competitive window. Use that window for match controls.", Theme.text[1], Theme.text[2], Theme.text[3], true)
          else
            GameTooltip:AddLine("Use the Competitive window to pause, resume, or end the match. Main Reset will not silently end it.", Theme.text[1], Theme.text[2], Theme.text[3], true)
          end
        elseif sessState == "countdown" then
          GameTooltip:AddLine("The host already started this match. Wait for the countdown to finish, or use the Competitive window.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        elseif sessState == "idle" then
          GameTooltip:AddLine("You are already in a Competitive lobby. Only the host can start the shared match.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        else
          GameTooltip:AddLine("Only the host can " .. tostring(actionLabel or "control the main session") .. " while this Competitive session is active.", Theme.text[1], Theme.text[2], Theme.text[3], true)
        end
      else
        GameTooltip:AddLine(normalText, Theme.text[1], Theme.text[2], Theme.text[3], true)
      end

      WF_Tip_FinalizeCentered(selfBtn, nil, true)
    end)

    button:SetScript("OnLeave", function()
      GameTooltip:Hide()
    end)
  end

  local function DoStartOrResume()
    -- resume
    if UI:IsPaused() then
      UI:Resume()
      return
    end

    if RealTrackerIsActive() then
      return
    end

    -- fresh run: reset pause accounting
    UI:ClearPauseState(true)

    -- Make sure the tracker is using the currently selected Profile rules (min value, filters, etc)
    UI:EnsureActiveProfileApplied(true)

    Tracker:Start()

    -- apply pending farm selection (from Random)
    if UI._pendingFarm then
      -- Only lock the session to the rolled farm when you're actually in that farm's area.
      -- If you rolled Sethekk Halls then decided to start somewhere else, we treat it as "nothing preloaded".
      if UI._IsPlayerInFarmArea and UI:_IsPlayerInFarmArea(UI._pendingFarm) then
        UI:_ApplyFarmToSession(UI._pendingFarm)
      else
        UI._pendingFarm = nil
      end
    end

    -- Competitive is started explicitly from the Competitive window.

    UI:RefreshTitle()
    UI:RefreshMetrics()
    UI:RefreshLoot(true)

    if UI._RefreshCompactControls then
      UI:_RefreshCompactControls()
    end
  end

  local function DoPauseOrResume()
    if UI:IsPaused() then
      UI:Resume()
      return
    end
    if not RealTrackerIsActive() then
      return
    end
    UI:Pause()
    if UI._RefreshCompactControls then
      UI:_RefreshCompactControls()
    end
  end

  local function DoStopHard()
    if not RealTrackerIsActive() and not UI:IsPaused() then
      return
    end

    -- If we are stopping from a paused state, lock the saved elapsed to the
    -- exact frozen timer value the user sees instead of recomputing it after
    -- pause finalization. This keeps History aligned with the paused UI timer.
    UI._wfStopElapsedOverride = nil
    if UI:IsPaused() then
      local frozen = tonumber(UI._pausedElapsed)
      if not frozen or frozen < 0 then
        frozen = tonumber(Tracker and Tracker.GetElapsed and Tracker:GetElapsed()) or 0
      end
      if frozen < 0 then frozen = 0 end
      UI._wfStopElapsedOverride = math.floor(frozen + 0.0001)
    end

    -- ensure elapsed excludes paused time
    UI:FinalizePauseAccounting()

    -- snapshot session before stop (Stop() may clear Tracker.current)
    UI._lastStoppedSession = ShallowCopy(Tracker.current)

    Tracker:Stop(true)

    -- If the Help window is open, refresh Overview/History so Recent Runs and Mode reflect the new session.
    -- (Do NOT spam updates during the run; only on stop.)
    local function _RefreshHelp()
      local Help = (WF and WF.UI and WF.UI.Help) or nil
      if not Help or not Help.frame or not Help.frame.IsShown or not Help.frame:IsShown() then
        return
      end
      if Help.RefreshOverview then Help:RefreshOverview() end
      if Help.RefreshHistory then Help:RefreshHistory() end
      if Help.MarkFarmAveragesDirty then Help:MarkFarmAveragesDirty() end
    end
    if C_Timer and C_Timer.After then
      C_Timer.After(0, _RefreshHelp)
    else
      _RefreshHelp()
    end

    UI:RefreshTitle()
    UI:RefreshMetrics()
    UI:RefreshLoot(true)

    if UI:IsCompareOpen() then
      UI:RefreshComparePanel()
    end

    if UI:IsCompetitiveOpen() then
      UI:_SendCompetitiveUpdate()
      UI:RefreshCompetitivePanel(true)
    end

    if UI._RefreshCompactControls then
      UI:_RefreshCompactControls()
    end

    UI._raceTimerStopQueued = false
    UI._raceTimerStopNotified = false
  end

  UI._DoStopHard = DoStopHard


  function UI:RollRandomFarmAndWaypoint()
  local pick = nil

  -- Prefer the Settings > Random Farm pool.
  if WF and WF.UI and WF.UI.Help and WF.UI.Help.GetRandomPool then
    local pool = WF.UI.Help:GetRandomPool()
    if pool and #pool > 0 then
      pick = pool[math.random(1, #pool)]
    else
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: No farms match your Random Farm filters.", 0.35, 0.78, 1.00, 1)
      end
      return nil
    end
  end

  -- Fallback only when the Random Farm pool helpers are unavailable.
  if not pick then
    pick = self:_PickRandomFarm(false)
  end
  if not pick then
    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage("What Farm?: No farms available to roll.", 0.35, 0.78, 1.00, 1)
    end
    return nil
  end

  self._pendingFarm = pick

  local function CoordText(x, y)
    local nx = NormalizeWaypointCoord(x)
    local ny = NormalizeWaypointCoord(y)
    if not nx or not ny then
      return "—"
    end
    return string.format("%.1f, %.1f", nx * 100, ny * 100)
  end

  local function MapName(mapId)
    if not mapId or not C_Map or not C_Map.GetMapInfo then return nil end
    local info = C_Map.GetMapInfo(mapId)
    if info and info.name then return info.name end
    return nil
  end

  local function FarmLocationName()
    local zone = tostring(pick.zone or pick.zoneName or "")
    if zone ~= "" and FarmLooksInstanced(pick) then
      return zone
    end
    return MapName(pick.mapId) or (zone ~= "" and zone or nil)
  end

  local function NotifyChosen(pinnedOk, approx, pinHint)
    local function CleanText(v)
      v = tostring(v or "")
      v = v:gsub("^%s+", ""):gsub("%s+$", "")
      return v
    end

    local function SameText(a, b)
      a = CleanText(a):lower()
      b = CleanText(b):lower()
      return a ~= "" and b ~= "" and a == b
    end

    local function AddPart(parts, text)
      text = CleanText(text)
      if text ~= "" and text ~= "—" then
        parts[#parts + 1] = text
      end
    end

    local name = CleanText(pick.name)
    if name == "" then name = "Farm" end

    local mapName = FarmLocationName()
    local coords = CoordText(pick._wx, pick._wy)
    if approx and coords ~= "—" then
      coords = coords .. " zone center"
    end

    local title = "What Farm?: Chose " .. name
    local details = {}

    if mapName and not SameText(mapName, name) then
      AddPart(details, mapName)
    end
    AddPart(details, coords)

    if pinnedOk then
      AddPart(details, "Map pin set")
    else
      AddPart(details, "No map pin")
      if pinHint and pinHint ~= "" then
        AddPart(details, pinHint)
      end
    end

    if UI and UI._ShowRandomFarmToast then
      UI:_ShowRandomFarmToast(title, table.concat(details, " • "), 1.55)
      return
    end

    local fallback = title
    local detailText = table.concat(details, " • ")
    if detailText ~= "" then
      fallback = fallback .. " • " .. detailText
    end

    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage(WF_MainColorizeNotice(fallback), 1, 1, 1)
    else
      print(fallback)
    end
  end

  -- Normalize coords for waypoint + for display. Instance/dungeon floor maps must
  -- not fake/display a 50/50 waypoint because WoW can resolve that to a random-looking map.
  local wx = NormalizeWaypointCoord(pick.x)
  local wy = NormalizeWaypointCoord(pick.y)
  local approx = false
  local instancedFarm = FarmLooksInstanced(pick)
  local instancedMap = (pick.mapId and IsInstanceStyleMapId(pick.mapId)) and true or false

  if instancedMap then
    wx, wy = nil, nil
  elseif pick.mapId and (not wx or not wy) and not instancedFarm then
    wx, wy = 0.5, 0.5
    approx = true
  end

  pick._wx, pick._wy = wx, wy

  local function MissingPinHint()
    if pick.mapId and not instancedMap and type(wx) == "number" and type(wy) == "number" then
      return nil
    end
    if instancedFarm then
      return "Set entrance pin in Farms"
    end
    return "Set one in Farms"
  end

  -- If a session is running, treat this as "queue next farm".
  if Tracker and Tracker.IsActive and Tracker:IsActive() then
    NotifyChosen(false, approx, MissingPinHint())
    return pick
  end

  self:RefreshTitle()

  local ok = false
  if pick.mapId and not instancedMap and type(wx) == "number" and type(wy) == "number" then
    local pinnedOk = false
    local pinnedApprox = false
    pinnedOk, pinnedApprox = SetWaypoint(pick.mapId, wx, wy, not instancedFarm)
    ok = pinnedOk and true or false
    -- Prefer our "no coords yet" approx flag, but keep SetWaypoint's approx if it did fallback internally.
    if pinnedApprox then
      approx = true
    end
  end

  NotifyChosen(ok, approx, MissingPinHint())
  return pick
end

bRandom:SetScript("OnClick", function()
  UI:RollRandomFarmAndWaypoint()
end)

  AttachMainControlTooltip(bStart, "Start", "Start the current farm run. In Compact mode this button also pauses or resumes the run.", "control the main session")
  AttachMainControlTooltip(bStop, "Pause / Resume", "Pause the current farm, or resume it if it is already paused. Shift-click ends the run.", "pause, resume, or stop the session")
  AttachMainControlTooltip(bReset, "Reset", "Stop the current run if needed, then clear the session back to zero.", "reset the session")

  local function GetHostedCompetitiveMainSession()
    local sid = tostring(UI and UI._compSessionId or "")
    if sid == "" then
      return nil, nil
    end
    if not (UI and UI._IsCompetitiveLeader and UI:_IsCompetitiveLeader()) then
      return nil, nil
    end
    local state = tostring((UI._compSession and UI._compSession.state) or "idle")
    return state, sid
  end

  local function HostedCompetitiveHasRoster()
    if not UI then
      return false
    end
    if UI._HasCompetitivePeers and UI:_HasCompetitivePeers() then
      return true
    end
    if UI._HasCompetitiveInvited and UI:_HasCompetitiveInvited() then
      return true
    end
    return false
  end

  local function NotifyCompetitiveMain(msg)
    if UIErrorsFrame and UIErrorsFrame.AddMessage then
      UIErrorsFrame:AddMessage(WF_MainColorizeNotice(msg), 1, 1, 1)
    end
  end

  local function HandleHostedCompetitiveMainStart()
    local state = GetHostedCompetitiveMainSession()
    if not state then
      return false
    end

    if IsShiftKeyDown and IsShiftKeyDown() then
      if UI and UI.QuitCompetitiveMatch then
        UI:QuitCompetitiveMatch()
      end
      if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
      return true
    end

    if state == "live" or state == "paused" then
      if UI and UI.ToggleCompetitivePause then
        UI:ToggleCompetitivePause()
      end
      if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
      return true
    end

    if state == "countdown" then
      NotifyCompetitiveMain("What Farm?: Competitive match is already starting.")
      return true
    end

    if UI and UI.StartChallenge and (HostedCompetitiveHasRoster() or (UI.IsCompetitiveOpen and UI:IsCompetitiveOpen())) then
      UI:StartChallenge()
      if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
      return true
    end

    return false
  end

  local function HandleHostedCompetitiveMainPauseOrEnd()
    local state = GetHostedCompetitiveMainSession()
    if not state then
      return false
    end

    if IsShiftKeyDown and IsShiftKeyDown() then
      if UI and UI.QuitCompetitiveMatch then
        UI:QuitCompetitiveMatch()
      end
      if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
      return true
    end

    if state == "live" or state == "paused" then
      if UI and UI.ToggleCompetitivePause then
        UI:ToggleCompetitivePause()
      end
      if UI and UI._comp and UI._comp.refreshButtons then UI._comp.refreshButtons() end
      return true
    end

    if state == "countdown" then
      NotifyCompetitiveMain("What Farm?: Competitive match is already starting.")
      return true
    end

    return false
  end

  local function RefreshAfterMainReset()
    UI._pendingFarm = nil
    UI._lastStoppedSession = nil

    if UI.ClearPauseState then
      UI:ClearPauseState(true)
    end
    if Tracker and Tracker.Reset then
      Tracker:Reset()
    end
    if UI.RefreshTitle then
      UI:RefreshTitle()
    end
    if UI.RefreshMetrics then
      UI:RefreshMetrics()
    end
    if UI.RefreshLoot then
      UI:RefreshLoot(true)
    end
    if UI.IsCompareOpen and UI:IsCompareOpen() and UI.RefreshComparePanel then
      UI:RefreshComparePanel()
    end
    if UI.IsCompetitiveOpen and UI:IsCompetitiveOpen() and UI.RefreshCompetitivePanel then
      UI:RefreshCompetitivePanel(true)
    end
    if UI._comp and UI._comp.refreshButtons then
      UI._comp.refreshButtons()
    end
  end

  local function HandleHostedCompetitiveMainReset()
    local state = GetHostedCompetitiveMainSession()
    if not state then
      return false
    end

    if state == "countdown" or state == "live" or state == "paused" or state == "ended" or state == "idle" then
      if UI and UI.QuitCompetitiveMatch then
        UI:QuitCompetitiveMatch()
      end
      RefreshAfterMainReset()
      return true
    end

    return false
  end

  bStart:SetScript("OnClick", function()
    if UI and UI._NotifyCompetitiveMainControlLocked and UI:_NotifyCompetitiveMainControlLocked("control the main session") then
      return
    end

    -- When hosting Competitive, the Farm Window Start button owns the match again.
    -- Start starts the match for everyone; Pause/Resume affects everyone; Shift-click ends everyone.
    if HandleHostedCompetitiveMainStart() then
      return
    end

    if UI:GetLayoutMode() == "Compact" then
      -- Shift-click ends the run (saves history). Normal click toggles Start/Pause/Resume.
      if IsShiftKeyDown and IsShiftKeyDown() then
        DoStopHard()
        return
      end

      if UI:IsPaused() then
        DoStartOrResume()
        return
      end

      if RealTrackerIsActive() then
        DoPauseOrResume()
      else
        DoStartOrResume()
      end
      return
    end

    DoStartOrResume()
  end)

  bStop:SetScript("OnClick", function()
    -- Non-leaders are blocked entirely during active matches
    if UI and UI._NotifyCompetitiveMainControlLocked and UI:_NotifyCompetitiveMainControlLocked("pause, resume, or stop the session") then
      return
    end
    -- If we are the host of an active match, main window Pause/Resume drives the match
    if HandleHostedCompetitiveMainPauseOrEnd() then
      return
    end
    -- Shift-click ends the run (saves history)
    if IsShiftKeyDown and IsShiftKeyDown() then
      DoStopHard()
      return
    end
    DoPauseOrResume()
  end)

  bReset:SetScript("OnClick", function()
    -- Non-leaders are blocked entirely during active matches
    if UI and UI._NotifyCompetitiveMainControlLocked and UI:_NotifyCompetitiveMainControlLocked("reset the session") then
      return
    end

    -- When hosting Competitive, Reset must end the shared match, not just reset locally.
    if HandleHostedCompetitiveMainReset() then
      return
    end

    -- Reset acts as Stop + Reset:
    -- - Ends the current run (saving history) even if you're paused
    -- - Then clears the session back to zero
    if RealTrackerIsActive() or UI:IsPaused() then
      DoStopHard()
    end

    RefreshAfterMainReset()
  end)


  bSave:SetScript("OnClick", function()
    UI:SaveCurrentFarm()
  end)

  bResetDungeon:SetScript("OnClick", function()
    if InCombatLockdown and InCombatLockdown() then
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: Can't reset instances in combat.", 0.35, 0.78, 1.00, 1)
      end
      return
    end

    if ResetInstances then
      ResetInstances()
      UI:RecordInstanceReset()
      UI:RefreshInstanceResetButton()

      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: Instance reset requested (party leader + everyone outside).", 0.92, 0.92, 0.92, 1)
      end
    else
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: ResetInstances() not available.", 0.35, 0.78, 1.00, 1)
      end
    end
  end)

  bResetDungeon:SetScript("OnEnter", function()
    WF_Tip_AnchorCentered(bResetDungeon)
    GameTooltip:ClearLines()

    local count, cap, remaining = UI:GetInstanceResetCounter()
    GameTooltip:AddLine("Reset Dungeon", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
    GameTooltip:AddLine(string.format("Resets (last hour): %d/%d", count, cap), Theme.text[1], Theme.text[2], Theme.text[3], true)

    if remaining and remaining > 0 then
      GameTooltip:AddLine(string.format("Oldest reset expires in: %s", SecondsToClock(remaining)), Theme.text[1], Theme.text[2], Theme.text[3], true)
    end

    if GetInstanceLockTimeRemaining then
      local apiRemain = GetInstanceLockTimeRemaining()
      if type(apiRemain) == "number" and apiRemain > 0 then
        GameTooltip:AddLine(string.format("Lock time remaining: %s", SecondsToClock(apiRemain)), Theme.text[1], Theme.text[2], Theme.text[3], true)
      end
    end

    WF_Tip_FinalizeCentered(bResetDungeon, nil, true)
  end)

  bResetDungeon:SetScript("OnLeave", function()
    GameTooltip:Hide()
  end)

  local function GetPriceSourceDisplay()
    if not (WF and WF.DB and WF.DB.GetSetting) then return nil end
    local src = tostring(WF.DB:GetSetting("priceSource") or "vendor"):lower()

    -- TSM: show the chosen TSM price string (DBMinBuyout, DBRegionMarketAvg, etc.)
    if src == "tsm" then
      local raw = tostring(WF.DB:GetSetting("tsmPriceString") or "dbregionmarketavg"):lower()
      local MAP = {
        dbmarket = "DBMarket",
        dbhistorical = "DBHistorical",
        dbrecent = "DBRecent",
        dbminbuyout = "DBMinBuyout",
        dbregionmarketavg = "DBRegionMarketAvg",
        dbregionhistorical = "DBRegionHistorical",
        dbregionsaleavg = "DBRegionSaleAvg",
        dbregionsalerate = "DBRegionSaleRate",
        dbregionavgdailysold = "DBRegionAvgDailySold",
      }
      local priceMeta = MAP[raw]
      if not priceMeta then
        if raw:match("^[%w_]+$") then
          if raw:sub(1,2) == "db" and raw:len() > 2 then
            priceMeta = "DB" .. raw:sub(3,3):upper() .. raw:sub(4)
          else
            priceMeta = Util:PrettyToken(raw)
          end
        else
          priceMeta = "TSM"
        end
      end
      return priceMeta
    end

    -- Non-TSM: just show the selected source name (Vendor, Auctionator, etc.)
    return Util:PrettyToken(src)
  end
local function RefreshHeaderPriceSource()
  local ps = GetPriceSourceDisplay()

  -- Modern builds: show it in the header button cluster next to Profile + Layout.
  if bPriceBar and bPriceBar.SetText then
    if ps and ps ~= "" then
      bPriceBar:SetText("|cff9aa0a6Source:|r " .. ps)
    else
      bPriceBar:SetText("|cff9aa0a6Source:|r ?")
    end
    return
  end

  -- Legacy fallback (older builds)
  local tgt = nil
  if header and header.price and header.price.SetText then
    tgt = header.price
  elseif header and header.sub and header.sub.SetText then
    tgt = header.sub
  end
  if not tgt then return end

  if ps and ps ~= "" then
    tgt:SetText("|cff9aa0a6" .. ps .. "|r")
  else
    tgt:SetText("")
  end
end


  -- Expose a safe public refresh so Settings can update the header immediately.
  function UI:RefreshHeaderPriceSource()
    -- Refreshing the header is cheap; protect against any bad state.
    pcall(RefreshHeaderPriceSource)
  end


  local function RefreshProfileButtonText()
    local pn = nil
    if WF and WF.DB and WF.DB.GetActiveProfileName then
      pn = WF.DB:GetActiveProfileName()
    end
    if not pn or pn == "" then pn = "Profile" end
    local labelText = "|cff9aa0a6Mode:|r " .. DisplayProfileName(pn)


    if bProfileBar then
      if bProfileBar.SetText then
        bProfileBar:SetText(labelText)
      elseif bProfileBar.label and bProfileBar.label.SetText then
        bProfileBar.label:SetText(labelText)
      end
    end

    -- Keep the price source next to the top header title (not next to the profile).
    if UI and UI.RefreshHeaderPriceSource then
      UI:RefreshHeaderPriceSource()
    else
      pcall(RefreshHeaderPriceSource)
    end
  end


  RefreshProfileButtonText()

  bProfileBar:SetScript("OnClick", function(_, btn)
    local backwards = false
    if btn == "RightButton" then
      backwards = true
    end
    if UI and UI.CycleProfile then
      UI:CycleProfile(backwards)
    elseif WF and WF.DB and WF.DB.ApplyProfile then
      -- ultra-safe fallback: do nothing if CycleProfile is missing
    end
    RefreshProfileButtonText()
  end)


-- -------------------------------------------------------
-- Header tooltip anchoring (centered; prefer ABOVE only if it fully fits)
-- -------------------------------------------------------
local function _WF_HeaderTipAnchorRegion(btn)
  if btn and btn.label and btn.label.GetTop then
    return btn.label
  end
  return btn
end

local function _WF_AnchorHeaderTooltip(btn)
  if not (GameTooltip and btn) then return end
  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(btn, "ANCHOR_NONE")
  GameTooltip:ClearAllPoints()
end

local function _WF_FinalizeHeaderTooltip(btn)
  if not (GameTooltip and btn) then return end

  local anchor = _WF_HeaderTipAnchorRegion(btn)
  if not anchor then anchor = btn end

  local yBelow = -10

  GameTooltip:ClearAllPoints()
  GameTooltip:SetPoint("TOP", anchor, "BOTTOM", 0, yBelow)
  GameTooltip:Show()
end

  bProfileBar:SetScript("OnEnter", function()
    if bProfileBar.SetHot then bProfileBar:SetHot(true) end
    _WF_AnchorHeaderTooltip(bProfileBar)
    GameTooltip:ClearLines()
    GameTooltip:AddLine("Farming Mode", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
    GameTooltip:AddLine("Left click: next mode", Theme.text[1], Theme.text[2], Theme.text[3], true)
    GameTooltip:AddLine("Right click: previous mode", Theme.text[1], Theme.text[2], Theme.text[3], true)
    GameTooltip:AddLine("Modes switch your capture intent (mats vs transmog) without touching Layout.", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
    _WF_FinalizeHeaderTooltip(bProfileBar)
  end)

  bProfileBar:SetScript("OnLeave", function()
    if bProfileBar.SetHot then bProfileBar:SetHot(false) end
    GameTooltip:Hide()
  end)



  -- Price Source (header)
  bPriceBar:SetScript("OnClick", function()
    -- Keep it simple: open Settings (pricing lives there).
    if WF.UI.Help and WF.UI.Help.Show then
      local ok = pcall(function()
        WF.UI.Help:Show("Settings")
      end)
      if not ok then
        WF.UI.Help:Show("Overview")
      end
    end
  end)

  bPriceBar:SetScript("OnEnter", function()
    if bPriceBar.SetHot then bPriceBar:SetHot(true) end
    _WF_AnchorHeaderTooltip(bPriceBar)
    GameTooltip:ClearLines()
    GameTooltip:AddLine("Source", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)

    local ps = GetPriceSourceDisplay()
    if ps and ps ~= "" then
      GameTooltip:AddLine("Current: " .. ps, Theme.text[1], Theme.text[2], Theme.text[3], true)
    end
    GameTooltip:AddLine("Set this in Settings.", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
    _WF_FinalizeHeaderTooltip(bPriceBar)
  end)

  bPriceBar:SetScript("OnLeave", function()
    if bPriceBar.SetHot then bPriceBar:SetHot(false) end
    GameTooltip:Hide()
  end)
  if bLayoutBar.RegisterForClicks then
    bLayoutBar:RegisterForClicks("LeftButtonUp", "RightButtonUp")
  end

  bLayoutBar:SetScript("OnClick", function(_, btn)
    -- Shift-click opens auxiliary Compare / Competitive windows (separate from the main farm window).
    if IsShiftKeyDown and IsShiftKeyDown() then
      if btn == "RightButton" then
        if UI.ToggleCompetitiveWindow then UI:ToggleCompetitiveWindow() end
      else
        if UI.ToggleCompareWindow then UI:ToggleCompareWindow() end
      end
      return
    end

    -- Regular click only cycles main layouts (keeps the farm window from ballooning).
    local cur = UI:GetLayoutMode()
    if cur ~= "Compact" then
      UI:SetLayoutMode("Compact")
    else
      UI:SetLayoutMode("Normal")
    end
  end)

  bLayoutBar:SetScript("OnEnter", function()
    if bLayoutBar.SetHot then bLayoutBar:SetHot(true) end
    _WF_AnchorHeaderTooltip(bLayoutBar)
    GameTooltip:ClearLines()
    GameTooltip:AddLine("Layout", Theme.accent[1], Theme.accent[2], Theme.accent[3], true)
    GameTooltip:AddLine("Cycles UI layouts: Normal and Compact.", Theme.text[1], Theme.text[2], Theme.text[3], true)
    GameTooltip:AddLine("Hold Shift and click to open Compare or Competitive.", Theme.text[1], Theme.text[2], Theme.text[3], true)
    GameTooltip:AddLine("Layout changes your window layout only (not your loot capture rules).", Theme.muted[1], Theme.muted[2], Theme.muted[3], true)
    _WF_FinalizeHeaderTooltip(bLayoutBar)
  end)

  bLayoutBar:SetScript("OnLeave", function()
    if bLayoutBar.SetHot then bLayoutBar:SetHot(false) end
    GameTooltip:Hide()
  end)


  -- -------------------------------------------------------
  -- Store refs
  -- -------------------------------------------------------
  self.frame = f
  self.header = header
  self.titleText = title
  self.subText = sub

  self.layoutBtn = bLayoutBar
  self.profileBtn = bProfileBar
  self.priceBtn = bPriceBar
  self.settingsBtn = bSettings

  self.chipsFrame = chips
  self.metricChips = metricChips
  self.chipTime = nil
  self.chipGold = nil
  self.chipItems = nil

  self.tablePanel = tablePanel
  self.scroll = scroll
  self.content = content
  self.rows = rows

  self:_EnsureDoubleTapToggleListener()

  self.btnBar = btnBar
  self.bStart = bStart
  self.bStop = bStop
  self.bReset = bReset
  self.bRandom = bRandom
  self.bSave = bSave
  self.bResetDungeon = bResetDungeon
  self.bLayout = bLayoutBar
  self.bProfile = bProfileBar
  self.bPrice = bPriceBar

  self.compPanel = compPanel
  self.compLootPanel = lootPanel
  self._comp = self._comp or {}
  self._comp.meta = compMeta
  self._comp.meta2 = compMeta2
  self._comp.rows = compRows
  self._comp.summarySession = chipSession
  self._comp.summaryStatus = chipStatus
  self._comp.summaryMode = chipMode
  self._comp.summaryHost = chipHost
  self._comp.summaryProfile = chipProfile
  self._comp.summaryScoring = chipScoring
  self._comp.sessionCard = sessionCard
  self._comp.sessionToggle = bSessionToggle
  self._comp.applySessionCardLayout = ApplyCompSessionCardLayout
  self._comp.lbScroll = lbScroll
  self._comp.lbContent = lbContent
  self._comp.rosterScroll = compListScroll
  self._comp.updateLeaderboardScrollLayout = UpdateCompLeaderboardScrollLayout
  self._comp.inviteCard = inviteCard
  self._comp.inviteTitle = inviteTitle
  self._comp.inviteToggle = bInviteToggle
  self._comp.applyInviteCardLayout = ApplyCompInviteCardLayout
  self._comp.lootRows = lootRows
  self._comp.lootChild = lootChild
  self._comp.lootScroll = lootScroll
  self._comp.lootRowPitch = ROW_H + ROW_GAP
  self._comp.lootMinRows = MIN_LOOT_ROWS
  self._comp.invBox = invBox
  self._comp.invLabel = invLabel
  self._comp.invHelp = invHelp
  self._comp.bInvite = bInvite
  self._comp.bStartChallenge = bStartChallenge
  self._comp.bPauseMatch = bPauseMatch
  self._comp.bQuitMatch = bQuitMatch
  self._comp.settingsButton = bCompSettings
  self._comp.settingsWindow = compSettingsWin
  self._comp.showTab = ShowCompTab
  self._comp.refreshButtons = RefreshCompButtons
  self._comp.lootFilterBox = lootFilterBox
  self._comp.refreshLootHeaderState = RefreshCompLootHeaderState

  self:_LoadInstanceResetLog()
  self:RefreshInstanceResetButton()
  self:ApplyMetricPillConfig(false)
  if self.RefreshProfileUI then self:RefreshProfileUI() end

  if self.ApplyScale then
    self:ApplyScale(WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("uiScale") or 1.0)
  end

  if self._RefreshCompactControls then
    self:_RefreshCompactControls()
  end

  self:ApplyLayoutMode()
end

function UI:ApplyLayoutMode(mode)
  if not self.frame then return end

  mode = NormalizeLayout(mode or self:GetLayoutMode())
  if mode == "Compare" or mode == "Competitive" then
    mode = "Normal"
    if WF and WF.DB and WF.DB.SetSetting then
      WF.DB:SetSetting("mainWindowLayout", mode)
    end
  end
  self._layoutMode = mode

  -- always keep the header updated
  if self.settingsBtn and self.settingsBtn.label then
    self.settingsBtn.label:SetText("Settings")
  end
  if self.layoutBtn then
    if self.layoutBtn.SetMode then
      self.layoutBtn:SetMode(mode)
    elseif self.layoutBtn.label then
      local t = mode
      if t == "Normal" then t = "Regular" end
      self.layoutBtn.label:SetText(t)
    end
  end

  -- base sizes (defaults) + per-mode saved sizes
  local w, h = 820, 520
  if mode == "Compact" then
    w, h = 640, 360
  end

  -- restore saved size per mode (mainW_<mode>, mainH_<mode>)
  local kW = "mainW_" .. mode
  local kH = "mainH_" .. mode
  local sw, sh
  if WF and WF.DB and WF.DB.GetSetting then
    sw = tonumber(WF.DB:GetSetting(kW))
    sh = tonumber(WF.DB:GetSetting(kH))
  end

  local hasSaved = (sw and sh and sw > 0 and sh > 0)

  -- Compact defaults: open at the safe minimum until the user manually resizes this mode.
  -- This prevents legacy/bad saved values from making Compact open huge.
  local touched = false
  if WF and WF.DB and WF.DB.GetSetting then
    if mode == "Compact" then
      touched = WF.DB:GetSetting("mainSizeTouchedV2_Compact") and true or false
      if not touched then
        touched = WF.DB:GetSetting("mainSizeTouched_Compact") and true or false
      end
    else
      touched = WF.DB:GetSetting("mainSizeTouched_" .. mode) and true or false
    end
  end

  -- If Compact accidentally got a legacy "big" saved size (e.g. 820x520) from earlier builds,
  -- treat it as untouced so Compact opens at the true compact minimum again.
  if mode == "Compact" and touched and hasSaved then
    if (sw and sw >= 800) or (sh and sh >= 500) then
      touched = false
    end
  end

  if mode == "Compact" and (not touched) then
    local ow, oh = ModeMin("Compact")
    w, h = ow, oh
    sw, sh = nil, nil
  end

  -- clamp to sane range + screen
  local minW, minH = ModeMin(mode)
  local maxW, maxH = 1600, 1000
  if UIParent and UIParent.GetWidth then
    local pw = UIParent:GetWidth() or maxW
    local ph = UIParent:GetHeight() or maxH
    maxW = math.max(minW, math.floor(pw - 40))
    maxH = math.max(minH, math.floor(ph - 40))
  end

  -- enforce per-mode resize bounds (prevents shrinking into broken overlap states)
  if self.frame.SetResizeBounds then
    self.frame:SetResizeBounds(minW, minH, maxW, maxH)
  else
    if self.frame.SetMinResize then
      self.frame:SetMinResize(minW, minH)
    end
    if self.frame.SetMaxResize then
      self.frame:SetMaxResize(maxW, maxH)
    end
  end

  if sw then w = math.min(math.max(sw, minW), maxW) end
  if sh then h = math.min(math.max(sh, minH), maxH) end


  self._applyingLayout = true
  self.frame:SetSize(w, h)
  self._applyingLayout = false

  if self.RestoreLayoutPosition then
    self:RestoreLayoutPosition(mode)
  end

  if Widgets and Widgets.UpdateLockGrips then
    Widgets:UpdateLockGrips()
  end

  -- title / subtitle visibility
  local showTitle = (mode == "Normal")
  local showSub = (mode == "Normal")
  if mode == "Compact" then
    showTitle = false
    showSub = false
  end

  self.titleText:SetShown(showTitle)
  self.subText:SetShown(showSub)

  -- compact mode tightens the header to remove dead space
  if self.header and self.header.SetHeight then
    if mode == "Compact" then
      self.header:SetHeight(28)
    else
      self.header:SetHeight(34)
    end
  end

  -- chips placement: under sub when visible, otherwise under header
  self.chipsFrame:ClearAllPoints()
  if showSub then
    self.chipsFrame:SetPoint("TOPLEFT", self.subText, "BOTTOMLEFT", 0, -10)
  else
    local y = -10
    if mode == "Compact" then y = -2 end
    self.chipsFrame:SetPoint("TOPLEFT", self.header, "BOTTOMLEFT", 12, y)
  end

  if self.chipsFrame and self.chipsFrame.SetHeight then
    if mode == "Compact" then
      self.chipsFrame:SetHeight(24)
    else
      self.chipsFrame:SetHeight(30)
    end
  end

  self:ApplyMetricPillConfig(false)

  if self._LayoutMetricChips then
    self:_LayoutMetricChips()
  elseif self._UpdateChipsWidth then
    self:_UpdateChipsWidth()
  end

  -- button bar: single row (Compact trims buttons instead of wrapping)
  local barH = 28
  self.btnBar:SetHeight(barH)

  -- reset button positions
  self.bStart:ClearAllPoints()
  self.bStop:ClearAllPoints()
  self.bReset:ClearAllPoints()
  self.bRandom:ClearAllPoints()
  self.bSave:ClearAllPoints()
  self.bResetDungeon:ClearAllPoints()

  if mode == "Compact" then
    -- Compact: Start/Stop is one button; hide Random/Save and place Help on the far right.
    self.bStop:SetShown(false)
    self.bRandom:SetShown(false)
    self.bSave:SetShown(false)

    self.bStart:SetPoint("LEFT", 0, 0)
    self.bReset:SetPoint("LEFT", self.bStart, "RIGHT", 10, 0)
    self.bResetDungeon:SetPoint("LEFT", self.bReset, "RIGHT", 10, 0)
  else
    self.bStop:SetShown(true)
    self.bRandom:SetShown(true)
    self.bSave:SetShown(true)

    self.bStart:SetPoint("LEFT", 0, 0)
    self.bStop:SetPoint("LEFT", self.bStart, "RIGHT", 10, 0)
    self.bReset:SetPoint("LEFT", self.bStop, "RIGHT", 10, 0)
    self.bRandom:SetPoint("LEFT", self.bReset, "RIGHT", 10, 0)
    self.bSave:SetPoint("LEFT", self.bRandom, "RIGHT", 10, 0)
    self.bResetDungeon:SetPoint("LEFT", self.bSave, "RIGHT", 10, 0)
  end

  -- table panel anchors
  -- (Compare / Competitive are separate windows now)
  self.tablePanel:ClearAllPoints()

  local bottomGap = 10
  if mode == "Compact" then bottomGap = 6 end
  local bottomPad = 10 + barH + bottomGap  -- bar bottom padding + bar height + gap
  local topAnchor = self.chipsFrame
  local tableGap = -10
  if mode == "Compact" then tableGap = -4 end

  self.tablePanel:SetPoint("TOPLEFT", topAnchor, "BOTTOMLEFT", 0, tableGap)
  self.tablePanel:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -10, bottomPad)

  if self._RefreshCompactControls then
    self:_RefreshCompactControls()
  end
  self:RefreshInstanceResetButton()
end

function UI:_GetCompetitiveMainControlLockInfo()
  local sid = tostring(self._compSessionId or "")
  if sid == "" then
    return false, "idle", false
  end

  local sessState = tostring((self._compSession and self._compSession.state) or "idle")
  if self._compStarted and sessState == "idle" then
    sessState = "live"
  end

  local isLeader = false
  if self._IsCompetitiveLeader then
    isLeader = self:_IsCompetitiveLeader() and true or false
  end

  -- Hosts are never locked — they drive the match/session from the main window.
  if isLeader then
    return false, sessState, true
  end

  -- Non-hosts are locked as soon as they are attached to a Competitive session,
  -- including the lobby/waiting state before the host presses Start Match.
  return true, sessState, false
end

function UI:_IsCompetitiveMainControlLocked()
  return self:_GetCompetitiveMainControlLockInfo()
end

function UI:_NotifyCompetitiveMainControlLocked(actionLabel)
  local locked, sessState, isLeader = self:_GetCompetitiveMainControlLockInfo()
  if not locked then
    return false
  end

  actionLabel = tostring(actionLabel or "control the session")
  local msg
  if isLeader then
    msg = "What Farm?: Main window controls are locked during an active Competitive match. Use the Competitive window to pause, resume, or end it."
    if sessState == "countdown" then
      msg = "What Farm?: This Competitive match is already starting. Use the Competitive window controls once it begins."
    end
  else
    msg = "What Farm?: Only the host can " .. actionLabel .. " during this Competitive session. Main window controls are locked."
    if sessState == "countdown" then
      msg = "What Farm?: The host already started this Competitive match. Wait for the countdown to finish."
    elseif sessState == "idle" then
      msg = "What Farm?: You are already in a Competitive lobby. Only the host can start the shared match."
    end
  end

  if UIErrorsFrame and UIErrorsFrame.AddMessage then
    UIErrorsFrame:AddMessage(WF_MainColorizeNotice(msg), 1, 1, 1)
  end
  return true
end

function UI:_RefreshMainSessionControlLocks()
  if not self.frame then
    return
  end

  local locked = self:_IsCompetitiveMainControlLocked()

  local function Apply(button)
    if not button then return end
    if button.Enable then
      button:Enable()
    end
    if button.EnableMouse then
      button:EnableMouse(true)
    end
    if button.SetAlpha then
      if locked then
        button:SetAlpha(0.82)
      else
        button:SetAlpha(1)
      end
    end
  end

  Apply(self.bStart)
  Apply(self.bStop)
  Apply(self.bReset)
end

function UI:_RefreshCompactControls()
  if not self.frame or not self.bStart or not self.bStart.label then
    return
  end

  local mode = self:GetLayoutMode()
  local paused = self:IsPaused()
  local active = RealTrackerIsActive()

  if mode == "Compact" then
    if paused then
      self.bStart.label:SetText("Resume")
    elseif active then
      self.bStart.label:SetText("Pause")
    else
      self.bStart.label:SetText("Start")
    end
  else
    self.bStart.label:SetText("Start")
    if self.bStop and self.bStop.label then
      if paused then
        self.bStop.label:SetText("Resume")
      else
        self.bStop.label:SetText("Pause")
      end
    end
  end

  self:_RefreshMainSessionControlLocks()
end


-- ---------------------------------------------------------
-- Competitive UI refresh
-- ---------------------------------------------------------

-- (moved to UI/Competitive.lua)




-- ---------------------------------------------------------
-- Manual add-to-run (for missed loot/events/testing)
-- ---------------------------------------------------------
local WF_AddRunItemFrame
local WF_AddRunItemCatcher
local WF_AddRunItemHookedInsertLink = false
local WF_AddRunItemHookedModifiedClick = false

local function WF_HideAddRunItem()
  if WF_AddRunItemFrame then WF_AddRunItemFrame:Hide() end
  if WF_AddRunItemCatcher then WF_AddRunItemCatcher:Hide() end
end

local function WF_ParseItemLink(s)
  if type(s) ~= "string" then return nil end

  local link = s:match("(\124c%x%x%x%x%x%x%x%x\124Hitem:.-\124h%[.-%]\124h\124r)")
  if link then return link end

  link = s:match("(\124Hitem:.-\124h%[.-%]\124h)")
  if link then return link end

  local id = s:match("item:(%d+)")
  if id then
    return "item:" .. tostring(id)
  end

  return nil
end

-- Backwards-compat: some code paths call WF_ParseItemLink as a global.
-- Keep it pointing at the local implementation to avoid nil errors.
_G.WF_ParseItemLink = _G.WF_ParseItemLink or WF_ParseItemLink


function UI:_EnsureAddRunItemPopup()
  if WF_AddRunItemFrame and WF_AddRunItemCatcher then
    return WF_AddRunItemFrame
  end

  WF_AddRunItemCatcher = CreateFrame("Button", nil, UIParent)
  WF_AddRunItemCatcher:SetAllPoints(UIParent)
  -- IMPORTANT:
  -- Do NOT use a fullscreen click-catcher that closes on any click.
  -- That breaks shift-clicking / dragging items from bags into this popup.
  -- Keep this behind normal UI and non-interactive.
  WF_AddRunItemCatcher:SetFrameStrata("LOW")
  WF_AddRunItemCatcher:SetFrameLevel(1)
  WF_AddRunItemCatcher:EnableMouse(false)
  WF_AddRunItemCatcher:Hide()

  local f = CreateFrame("Frame", "WF_AddItemToRunPopup", UIParent, "BackdropTemplate")
  WF_AddRunItemFrame = f
  f:SetFrameStrata("DIALOG")
  f:SetFrameLevel(100)
  f:SetSize(380, 170)
  f:SetPoint("CENTER")
  f:EnableMouse(true)
  if f.SetClampedToScreen then f:SetClampedToScreen(true) end
  f:Hide()

-- Let ESC reliably close this popup (no modal click-catcher needed).
if type(UISpecialFrames) == "table" then
  local exists = false
  for i = 1, #UISpecialFrames do
    if UISpecialFrames[i] == "WF_AddItemToRunPopup" then
      exists = true
      break
    end
  end
  if not exists then
    table.insert(UISpecialFrames, "WF_AddItemToRunPopup")
  end
end

f:SetScript("OnHide", function()
  if WF_AddRunItemCatcher then WF_AddRunItemCatcher:Hide() end
  if f._itemBox and f._itemBox.ClearFocus then f._itemBox:ClearFocus() end
  if f._qtyBox and f._qtyBox.ClearFocus then f._qtyBox:ClearFocus() end
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(Theme.bg[1], Theme.bg[2], Theme.bg[3], Theme.bg[4])
  f:SetBackdropBorderColor(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4])

  local title = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  title:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], 1)
  title:SetText("Add item to run")

  local hint = f:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  hint:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
  hint:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4] or 1)
  hint:SetText("Shift-click a link, or drag an item from your bags.")

  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(Theme.border[1], Theme.border[2], Theme.border[3], Theme.border[4])
    eb:SetTextInsets(6, 6, 2, 2)

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

  local itemLab = f:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
  itemLab:SetPoint("TOPLEFT", hint, "BOTTOMLEFT", 0, -14)
  itemLab:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], 1)
  itemLab:SetText("Item")

  local itemBox = MakeInput(f, 280, 22)
  itemBox:SetPoint("LEFT", itemLab, "RIGHT", 10, 0)
  itemBox:SetPoint("TOP", itemLab, "TOP", 0, 2)
  itemBox:SetScript("OnMouseDown", function(self)
    self:SetFocus()
  end)

  itemBox:SetScript("OnReceiveDrag", function(self)
    if not GetCursorInfo then return end
    local t, id = GetCursorInfo()
    if t == "item" and id then
      local link = select(2, GetItemInfo(id))
      if not link then
        link = "item:" .. tostring(id)
      end
      self:SetText(link)
      if ClearCursor then ClearCursor() end
      self:HighlightText()
    end
  end)



-- Allow dropping an item anywhere on the popup (not just directly onto the input box).
local function _WF_AddRunItem_TryReceiveCursor()
  if not GetCursorInfo then return false end
  local t, id, link = GetCursorInfo()
  if t == "item" and id then
    local itemLink = link
    if (not itemLink) and GetItemInfo then
      itemLink = select(2, GetItemInfo(id))
    end
    if not itemLink then
      itemLink = "item:" .. tostring(id)
    end
    itemBox:SetText(itemLink)
    if ClearCursor then ClearCursor() end
    itemBox:HighlightText()
    if itemBox.SetFocus then itemBox:SetFocus() end
    return true
  end
  return false
end

if f and f.HookScript then
  f:HookScript("OnReceiveDrag", function()
    _WF_AddRunItem_TryReceiveCursor()
  end)
  f:HookScript("OnMouseUp", function(_, btn)
    if btn == "LeftButton" then
      _WF_AddRunItem_TryReceiveCursor()
    end
  end)
end

  local qtyLab = f:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
  qtyLab:SetPoint("TOPLEFT", itemLab, "BOTTOMLEFT", 0, -14)
  qtyLab:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], 1)
  qtyLab:SetText("Qty")

  local qtyBox = MakeInput(f, 70, 22)
  qtyBox:SetPoint("LEFT", qtyLab, "RIGHT", 10, 0)
  qtyBox:SetPoint("TOP", qtyLab, "TOP", 0, 2)
  qtyBox:SetText("1")

  local bAdd = Widgets:Button(f, "Add", 90, 22)
  bAdd:SetPoint("BOTTOMRIGHT", -12, 12)

  local bCancel = Widgets:Button(f, CANCEL or "Cancel", 90, 22)
  bCancel:SetPoint("RIGHT", bAdd, "LEFT", -10, 0)

  bCancel:SetScript("OnClick", function()
    WF_HideAddRunItem()
  end)

  local function DoAdd()
    local linkText = itemBox:GetText() or ""
    local link = WF_ParseItemLink(linkText)
    if not link then
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: Paste/shift-click an item link first.", 0.35, 0.78, 1.00, 1)
      end
      return
    end

    local qty = tonumber(qtyBox:GetText() or "") or 1
    qty = math.floor(qty + 0.0001)
    if qty < 1 then qty = 1 end
    if qty > 999999 then qty = 999999 end

    if not (Tracker and Tracker.AddManualItem) then
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: AddManualItem() not available.", 0.35, 0.78, 1.00, 1)
      end
      return
    end

    local ok = Tracker:AddManualItem(link, qty)
    if ok then
      WF_HideAddRunItem()
      if UI and UI.RefreshLoot then UI:RefreshLoot(true) end
      if UI and UI.RefreshMetrics then UI:RefreshMetrics() end
    else
      if UIErrorsFrame and UIErrorsFrame.AddMessage then
        UIErrorsFrame:AddMessage("What Farm?: Couldn't add that item to this run.", 0.35, 0.78, 1.00, 1)
      end
    end
  end

  bAdd:SetScript("OnClick", DoAdd)
  itemBox:SetScript("OnEnterPressed", DoAdd)
  qtyBox:SetScript("OnEnterPressed", DoAdd)

  f._itemBox = itemBox
  f._qtyBox = qtyBox

  if not WF_AddRunItemHookedInsertLink and type(ChatEdit_InsertLink) == "function" then
    WF_AddRunItemHookedInsertLink = true
    local orig = ChatEdit_InsertLink
    ChatEdit_InsertLink = function(link)
      -- When this popup is open, route modified-click item links into it.
      -- Don't rely on editbox focus (bag clicks can steal focus).
      if WF_AddRunItemFrame and WF_AddRunItemFrame:IsShown() and WF_AddRunItemFrame._itemBox then
        WF_AddRunItemFrame._itemBox:SetText(link)
        WF_AddRunItemFrame._itemBox:HighlightText()
        if WF_AddRunItemFrame._itemBox.SetFocus then WF_AddRunItemFrame._itemBox:SetFocus() end
        return true
      end
      return orig(link)
    end
  end


-- Catch modified-clicks (e.g. CHATLINK clicks) even when chat isn't active.
-- This makes "Shift-click a link" work reliably for this popup.
if not WF_AddRunItemHookedModifiedClick and type(hooksecurefunc) == "function" and type(HandleModifiedItemClick) == "function" then
  WF_AddRunItemHookedModifiedClick = true
  hooksecurefunc("HandleModifiedItemClick", function(link)
    if not (WF_AddRunItemFrame and WF_AddRunItemFrame:IsShown() and WF_AddRunItemFrame._itemBox) then
      return
    end

    local allow = true
    if type(IsModifiedClick) == "function" then
      allow = IsModifiedClick("CHATLINK")
    end
    if not allow then
      return
    end

    if type(link) == "string" then
      WF_AddRunItemFrame._itemBox:SetText(link)
      WF_AddRunItemFrame._itemBox:HighlightText()
      if WF_AddRunItemFrame._itemBox.SetFocus then WF_AddRunItemFrame._itemBox:SetFocus() end
    end
  end)
end

  return f
end

function UI:PromptAddItemToRun()
  if not self.frame then
    self:Create()
  end

  local f = self:_EnsureAddRunItemPopup()
  if not f then return end

  if WF_AddRunItemCatcher then WF_AddRunItemCatcher:Show() end
  f:Show()

  if f._itemBox then
    f._itemBox:SetText("")
    f._itemBox:SetFocus()
  end

  if f._qtyBox then
    f._qtyBox:SetText("1")
  end
end
function UI:Show()
  self:Create()
  if WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("mainWindowOpen", true)
  end
  self:EnsureActiveProfileApplied(false)
  self.frame:Show()
  self:StartTicker()

  if self._RestorePauseStateFromSession then
    self:_RestorePauseStateFromSession()
  end

  -- WF_AUTOSTART_ON_OPEN
  if WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("autoStartSessionOnOpen") then
    local compLocked = false
    if self._IsCompetitiveMainControlLocked then
      compLocked = self:_IsCompetitiveMainControlLocked() and true or false
    end
    if (not compLocked) and not ((self.IsPaused and self:IsPaused()) or RealTrackerIsActive()) then
      if self.ClearPauseState then
        self:ClearPauseState()
      end
      if Tracker.Start then
        Tracker:Start()
      end
      if self.RefreshTitle then self:RefreshTitle() end
      if self.RefreshMetrics then self:RefreshMetrics() end
      if self.RefreshLoot then self:RefreshLoot() end
    end
  end

  -- Keep paused state obvious without any alpha flashing.
  self:StopPauseFlash()
  self:_ApplyTimerStateVisuals()
end

function UI:Hide()
  if WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("mainWindowOpen", false)
  end
  if self.frame then
    self.frame:Hide()
  end
  self:StopTicker()
  self:StopPauseFlash()
end

function UI:Toggle()
  self:Create()
  if self.frame:IsShown() then
    self:Hide()
  else
    self:Show()
  end
end

-- ---------------------------------------------------------
-- Title, metrics, loot
-- ---------------------------------------------------------
function UI:RefreshTitle()
  if not self.frame then
    return
  end

  local active = false
  if Tracker and Tracker.IsActive then
    active = Tracker:IsActive()
  end

  
  local layout = "Normal"
  if self.GetLayoutMode then
    layout = NormalizeLayout(self:GetLayoutMode())
  end

  local function ApplyCompactMeta(modeName)
    if not (self.header and self.header.sub) then return end

    if layout ~= "Compact" then
      if self._compactMetaKey then
        self._compactMetaKey = nil
        self.header.sub:SetText("")
      end
      return
    end

    if self._compactMetaKey then
      self._compactMetaKey = nil
    end
    self.header.sub:SetText("")
    return
  end

local pf = self._pendingFarm

  -- If we have a queued/selected farm and we're not actively tracking, show it.
  if (not active) and pf then
    local name = pf.name or "Farm"
    local zone = pf.zone or "Unknown Zone"
    local coord = ""
    if pf.x and pf.y then
      coord = string.format(" (%.1f, %.1f)", pf.x * 100, pf.y * 100)
    end
    self.titleText:SetText(name)

    local prof = DisplayProfileName(UI:GetActiveProfileName() or "Default")
    local ps = Util:PrettyToken((WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("priceSource")) or "vendor")
    self.subText:SetText(zone .. coord .. " • Mode: " .. prof .. " • Source: " .. ps)
    ApplyCompactMeta(nil)
    return
  end

  -- Otherwise show current session (active) or last stopped session.
  local sess = Tracker.current or self._lastStoppedSession
  if not sess then
    self.titleText:SetText("No farm selected")
    self.subText:SetText("Start anywhere. What Farm? will capture your session automatically.")
    ApplyCompactMeta(nil)
    return
  end
  ApplyCompactMeta(sess.mode)


  local name = sess.farmName or sess.name or "Unknown"
  local zone = sess.zone or "Unknown Zone"
  local coord = ""
  if sess.x and sess.y then
    coord = string.format(" (%.1f, %.1f)", sess.x * 100, sess.y * 100)
  end

  self.titleText:SetText(name)

    local prof = DisplayProfileName(UI:GetActiveProfileName() or "Default")
    local ps = Util:PrettyToken((WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("priceSource")) or "vendor")
    self.subText:SetText(zone .. coord .. " • Mode: " .. prof .. " • Source: " .. ps)
end


-- ---------------------------------------------------------
-- Metric chip width binding (prevents chips from running off
-- the right edge in Compact / resized layouts)
-- ---------------------------------------------------------
function UI:_LayoutMetricChips()
  if not self.chipsFrame then return end

  if self._UpdateChipsWidth then
    self:_UpdateChipsWidth()
  end

  local mode = (self.GetLayoutMode and self:GetLayoutMode()) or self._layoutMode or "Normal"
  local chipGap = 10
  if mode == "Compact" then chipGap = 8 end

  local prev
  local chips = self.metricChips or {}
  for i = 1, #chips do
    local chip = chips[i]
    if chip then
      chip:ClearAllPoints()
      if chip:IsShown() then
        if prev then
          chip:SetPoint("LEFT", prev, "RIGHT", chipGap, 0)
        else
          chip:SetPoint("LEFT", 0, 0)
        end
        prev = chip
      end
    end
  end

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

function UI:_UpdateChipsWidth()
  if not (self.frame and self.chipsFrame) then return end

  local fw = (self.frame.GetWidth and self.frame:GetWidth()) or 0
  if fw <= 0 then return end

  -- Keep left indent (12) and right padding (-10) consistent with the rest of the UI.
  local w = fw - 22
  if w < 220 then w = 220 end

  self.chipsFrame:SetWidth(w)
  if self.chipsFrame.SetClipsChildren then
    self.chipsFrame:SetClipsChildren(true)
  end

  -- Also clip each chip so long values never bleed outside the pill.
  local chips = self.metricChips or {}
  for i = 1, #chips do
    local c = chips[i]
    if c and c.SetClipsChildren then
      c:SetClipsChildren(true)
    end
  end
end


-- ---------------------------------------------------------
-- Metric chip autosizing (prevents long values from bleeding)
-- ---------------------------------------------------------
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:_FitMetricChips()
  if not self.chipsFrame then
    return
  end

  local mode = (self.GetLayoutMode and self:GetLayoutMode()) or self._layoutMode or "Normal"
  local chipGap = 10
  if mode == "Compact" then chipGap = 8 end

  local chipsW = (self.chipsFrame.GetWidth and self.chipsFrame:GetWidth()) or 0
  if chipsW <= 0 then
    chipsW = 780
  end

  local visible = self._wfVisibleMetricChips or {}
  self._wfVisibleMetricChips = visible
  for i = #visible, 1, -1 do
    visible[i] = nil
  end

  local chips = self.metricChips or {}
  local visibleCount = 0
  for i = 1, #chips do
    local chip = chips[i]
    if chip and chip:IsShown() then
      visibleCount = visibleCount + 1
      visible[visibleCount] = chip
    end
  end
  if visibleCount == 0 then
    return
  end

  local function _TextW(fs)
    if fs and fs.GetStringWidth then
      return fs:GetStringWidth() or 0
    end
    return 0
  end

  local function _NaturalW(chip)
    if not chip then return 0 end
    local lw = _TextW(chip.label)
    local vw = _TextW(chip.value)
    local pad = 22
    local gap = 8
    return lw + gap + vw + pad
  end

  local totalGap = chipGap * math.max(0, (visibleCount - 1))
  local maxTotal = chipsW - totalGap
  if maxTotal < 0 then maxTotal = 0 end

  local naturalTotal = 0
  local minTotal = 0
  for i = 1, visibleCount do
    local chip = visible[i]
    local metric = chip._wfMetric or "Time"
    local bounds = GetMetricPillWidth(metric)
    local natural = _Clamp(_NaturalW(chip), bounds.min, bounds.max)
    chip._wfNaturalWidth = natural
    chip._wfMinWidth = bounds.min
    chip._wfMaxWidth = bounds.max
    naturalTotal = naturalTotal + natural
    minTotal = minTotal + bounds.min
  end

  if naturalTotal <= maxTotal then
    for i = 1, visibleCount do
      local chip = visible[i]
      chip:SetWidth(chip._wfNaturalWidth or chip._wfMinWidth or 120)
    end
  else
    if minTotal > maxTotal then
      local scale = maxTotal / math.max(minTotal, 1)
      for i = 1, visibleCount do
        local chip = visible[i]
        local minW = chip._wfMinWidth or 80
        local scaled = math.floor((minW * scale) + 0.5)
        chip:SetWidth(math.max(64, scaled))
      end
    else
      local rangeTotal = 0
      for i = 1, visibleCount do
        local chip = visible[i]
        rangeTotal = rangeTotal + math.max(0, (chip._wfNaturalWidth or 0) - (chip._wfMinWidth or 0))
      end

      local ratio = 0
      if rangeTotal > 0 then
        ratio = (maxTotal - minTotal) / rangeTotal
        ratio = _Clamp(ratio, 0, 1)
      end

      local used = 0
      for i = 1, visibleCount do
        local chip = visible[i]
        local minW = chip._wfMinWidth or 80
        local natW = chip._wfNaturalWidth or minW
        local width = minW
        if natW > minW then
          width = minW + math.floor(((natW - minW) * ratio) + 0.5)
        end
        width = _Clamp(width, minW, chip._wfMaxWidth or natW)
        chip:SetWidth(width)
        used = used + width
      end

      local leftover = maxTotal - used
      if leftover > 0 and visibleCount > 0 then
        local last = visible[visibleCount]
        local maxW = last._wfMaxWidth or ((last.GetWidth and last:GetWidth()) or 0)
        local curW = (last.GetWidth and last:GetWidth()) or 0
        if curW < maxW then
          last:SetWidth(math.min(maxW, curW + leftover))
        end
      end
    end
  end

  local function _CacheBaseFont(fs)
    if not fs or not fs.GetFont then return nil end
    if not fs._wfBaseFont then
      local f, s, fl = fs:GetFont()
      fs._wfBaseFont = { f = f, s = tonumber(s) or 12, fl = fl }
    end
    return fs._wfBaseFont
  end

  local function _RestoreBaseFont(fs)
    local bf = _CacheBaseFont(fs)
    if bf and bf.f and bf.s then
      fs:SetFont(bf.f, bf.s, bf.fl)
    end
  end

  -- Shrink chip fonts until the combined label + value fits inside the pill.
  local function _ShrinkChipTextToFit(chip)
    if not chip or not chip.value or not chip.label then return end
    if not chip.label.GetFont or not chip.value.GetFont then return end
    if not chip.label.GetStringWidth or not chip.value.GetStringWidth then return end

    local labelFS = chip.label
    local valueFS = chip.value
    local labelBase = _CacheBaseFont(labelFS)
    local valueBase = _CacheBaseFont(valueFS)
    if not labelBase or not valueBase then return end

    _RestoreBaseFont(labelFS)
    _RestoreBaseFont(valueFS)

    local chipW = (chip.GetWidth and chip:GetWidth()) or 0
    if chipW <= 0 then return end

    local innerPad = 20
    local gap = 6
    local maxTextW = chipW - innerPad
    if maxTextW < 24 then return end

    local labelPt = tonumber(labelBase.s) or 12
    local valuePt = tonumber(valueBase.s) or 12
    local labelMin = (mode == "Compact") and 7 or 8
    local valueMin = (mode == "Compact") and 7 or 8

    local function totalTextW()
      return _TextW(labelFS) + gap + _TextW(valueFS)
    end

    local totalW = totalTextW()
    if totalW <= maxTextW then return end

    while totalW > maxTextW and (valuePt > valueMin or labelPt > labelMin) do
      if valuePt > valueMin then
        valuePt = valuePt - 1
        valueFS:SetFont(valueBase.f, valuePt, valueBase.fl)
        totalW = totalTextW()
        if totalW <= maxTextW then break end
      end

      if labelPt > labelMin then
        labelPt = labelPt - 1
        labelFS:SetFont(labelBase.f, labelPt, labelBase.fl)
        totalW = totalTextW()
        if totalW <= maxTextW then break end
      end

      if valuePt <= valueMin and labelPt <= labelMin then
        break
      end
    end
  end

  for i = 1, visibleCount do
    _ShrinkChipTextToFit(visible[i])
  end
end


function UI:GetMetricPillOptions()
  return METRIC_PILL_OPTIONS
end

function UI:ApplyMetricPillConfig(refreshNow)
  if not self.metricChips then return end

  self:StopPauseFlash()
  self:_ResetMetricChipVisuals()

  self._timeMetricChips = {}
  self._raceTimerMetricChips = {}
  self.chipTime = nil
  self.chipGold = nil
  self.chipItems = nil

  local mode = (self.GetLayoutMode and self:GetLayoutMode()) or self._layoutMode or "Normal"
  local maxVisible = 4
  if mode == "Compact" then
    local allowFourthCompact = false
    if WF and WF.DB and WF.DB.GetSetting then
      allowFourthCompact = WF.DB:GetSetting("compactModeShowFourthPill") and true or false
    end
    maxVisible = allowFourthCompact and 4 or 3
  end

  local shownCount = 0
  local chips = self.metricChips
  for slot = 1, #chips do
    local chip = chips[slot]
    local metric = GetMetricPillSetting(slot)
    local label = GetMetricPillLabel(metric)
    local wantsShow = (metric ~= "Hidden")
    local show = wantsShow

    if wantsShow and shownCount >= maxVisible then
      show = false
    end
    if show then
      shownCount = shownCount + 1
    end

    chip._wfMetric = metric
    chip._wfLastText = nil
    chip._wfLastMetric = nil

    if chip.label then
      chip.label:SetText(label)
    end
    if chip.value then
      chip.value:SetText(show and "--" or "")
      chip.value:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
      chip.value:SetAlpha(1)
    end

    chip:SetShown(show)

    if show and metric == "Time" then
      self._timeMetricChips[#self._timeMetricChips + 1] = chip
      if not self.chipTime then self.chipTime = chip end
    elseif show and metric == "Race Timer" then
      self._raceTimerMetricChips[#self._raceTimerMetricChips + 1] = chip
    elseif show and metric == "Looted Gold" then
      if not self.chipGold then self.chipGold = chip end
    elseif show and metric == "Looted Items" then
      if not self.chipItems then self.chipItems = chip end
    end
  end

  if self._LayoutMetricChips then
    self:_LayoutMetricChips()
  elseif self._FitMetricChips then
    self:_FitMetricChips()
  end

  self._wfTimerVisualKey = nil
  self:_ApplyTimerStateVisuals()

  if refreshNow then
    self:RefreshMetrics()
  end
end

local function FormatItemCountText(count)
  count = math.max(0, tonumber(count) or 0)
  return BreakUpLargeNumbers(math.floor(count + 0.0001))
end

local function SessionLootedItemValue(sess, totalValue, goldValue)
  local direct =
      tonumber(sess and sess.itemsValue)
      or tonumber(sess and sess.itemValue)
      or tonumber(sess and sess.lootedItemValue)
      or tonumber(sess and sess.lootItemValue)
      or tonumber(sess and sess.itemsTotalValue)
      or tonumber(sess and sess.itemTotalValue)
      or tonumber(sess and sess.itemsValueTotal)
      or tonumber(sess and sess.itemValueTotal)
  if direct ~= nil then
    if direct < 0 then direct = 0 end
    return direct
  end

  local summed = 0
  local foundEntryValue = false
  if sess and type(sess.items) == "table" then
    for _, entry in pairs(sess.items) do
      local v = tonumber(entry and entry.value)
      if v and v > 0 then
        summed = summed + v
        foundEntryValue = true
      end
    end
  end
  if foundEntryValue then
    return summed
  end

  local total = tonumber(totalValue)
  local gold = tonumber(goldValue)
  if total ~= nil and gold ~= nil then
    local liv = total - gold
    if liv < 0 then liv = 0 end
    return liv
  end

  total = tonumber(total) or 0
  if total < 0 then total = 0 end
  return total
end

local function MetricValueText(metric, state)
  if metric == "Time" then
    return SecondsToClock(state.sec or 0)
  elseif metric == "Looted Gold" then
    return WF_FormatMoneyLetters(state.gold or 0)
  elseif metric == "Looted Items" then
    return FormatItemCountText(state.items or 0)
  elseif metric == "Looted Value" then
    return WF_FormatMoneyLetters(state.itemValue or 0)
  elseif metric == "Estimated GPH" then
    return WF_FormatMoneyLetters(state.gph or 0)
  elseif metric == "Total Value" then
    return WF_FormatMoneyLetters(state.total or 0)
  elseif metric == "Race Timer" then
    if UI and UI._RaceTimerValueText then
      return UI:_RaceTimerValueText()
    end
    return "Set Time"
  end
  return ""
end

function UI:RefreshMetrics()
  if not self.frame then
    return
  end

  local sess = Tracker and Tracker.current or nil
  local active = (Tracker and Tracker.IsActive and Tracker:IsActive()) or false
  local paused = (self.IsPaused and self:IsPaused()) or false

  local elapsed = 0
  if (active or paused) and Tracker and Tracker.GetElapsed then
    elapsed = tonumber(Tracker:GetElapsed()) or 0
  end
  if elapsed < 0 then elapsed = 0 end
  local sec = math.floor(elapsed + 0.0001)

  local gold = 0
  if sess and sess.moneyGained then
    gold = tonumber(sess.moneyGained) or 0
  end
  if gold < 0 then gold = 0 end

  -- Only force repricing while active; otherwise display the last stored totals.
  local total = 0
  if active and Tracker and Tracker.GetTotalValue then
    total = tonumber(Tracker:GetTotalValue()) or 0
  elseif sess and sess.totalValue then
    total = tonumber(sess.totalValue) or 0
  end
  if total < 0 then total = 0 end

  local items = 0
  if sess and type(sess.items) == "table" then
    for _, entry in pairs(sess.items) do
      items = items + math.max(0, tonumber(entry and entry.count) or 0)
    end
  end

  local itemValue = SessionLootedItemValue(sess, total, gold)
  local gph = SessionGPH(sec, total)
  if gph < 0 then gph = 0 end

  local changed = false
  local needsFit = false
  local state = self._wfMetricState or {}
  self._wfMetricState = state
  state.sec = sec
  state.gold = gold
  state.total = total
  state.items = items
  state.itemValue = itemValue
  state.gph = gph
  state.raceDuration = self:GetRaceTimerSeconds()
  state.raceRemaining = self:_RaceTimerRemainingSeconds()

  local chips = self.metricChips or {}
  for i = 1, #chips do
    local chip = chips[i]
    if chip and chip:IsShown() and chip.value then
      local metric = chip._wfMetric or GetMetricPillSetting(i)
      local text = MetricValueText(metric, state)
      if chip._wfLastText ~= text or chip._wfLastMetric ~= metric then
        local lastText = chip._wfLastText
        local lastMetric = chip._wfLastMetric
        chip.value:SetText(text)
        chip._wfLastText = text
        chip._wfLastMetric = metric
        changed = true

        if lastMetric ~= metric then
          needsFit = true
        elseif metric == "Time" then
          if not lastText or #lastText ~= #text then
            needsFit = true
          end
        else
          needsFit = true
        end
      end

      if metric == "Race Timer" and chip.value then
        local duration = self:GetRaceTimerSeconds()
        if duration <= 0 then
          chip.value:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
        elseif (active or paused) and (state.raceRemaining or 0) <= 60 then
          chip.value:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], Theme.accent[4])
        elseif active or paused then
          chip.value:SetTextColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], Theme.accent[4])
        else
          chip.value:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
        end
      end
    end
  end

  if self._CheckRaceTimerAutoStop then
    self:_CheckRaceTimerAutoStop()
  end

  -- Recording state visuals (only update when state changes)
  if self._wfMetric_lastActive ~= active or self._wfMetric_lastPaused ~= paused then
    self._wfMetric_lastActive = active
    self._wfMetric_lastPaused = paused
    self:_ApplyTimerStateVisuals()
    changed = true
  end

  if changed and needsFit then
    if self._FitMetricChips then
      self:_FitMetricChips()
    end
  end

  -- These are lightweight and keep the UX feeling responsive.
  self:RefreshInstanceResetButton()

  if changed and self._RefreshCompactControls then
    self:_RefreshCompactControls()
  end

  if changed then
    if self:IsCompareOpen() then
      self:RefreshComparePanel()
    elseif self:IsCompetitiveOpen() then
      self:RefreshCompetitivePanel(false)
    end
  end
end


-- ---------------------------------------------------------
-- Loot scrolling (prevent blank scrolling; show scrollbar only when needed)
-- ---------------------------------------------------------
function UI:_UpdateLootScroll(shownCount)
  if not (self.scroll and self.content) then
    return
  end

  -- Throttle expensive item tooltip loads while scrolling (prevents junk loads)
  local _sf = self.scroll
  if _sf._wfLastScroll == nil then _sf._wfLastScroll = 0 end
  if not _sf._wfLastScrollHooked then
    _sf._wfLastScrollHooked = true
    local _sb = _sf.ScrollBar
    if (not _sb) and _sf.GetName then
      local _n = _sf:GetName()
      if _n then _sb = _G and _G[_n .. "ScrollBar"] end
    end
    if _sb and _sb.HookScript then
      _sb:HookScript("OnValueChanged", function() _sf._wfLastScroll = GetTime() end)
    end
  end

  shownCount = tonumber(shownCount) or 0
  if shownCount < 0 then shownCount = 0 end

  local rowH = 24
  local h = shownCount * rowH
  if h < 1 then h = 1 end

  self.content:SetHeight(h)

  local viewH = self.scroll:GetHeight() or 0
  local canScroll = (h > (viewH + 1))

  local sb = self.scroll.ScrollBar
  if sb then
    if canScroll then sb:Show() else sb:Hide() end
  end

  self.scroll:EnableMouseWheel(canScroll)

  if canScroll then
    self.scroll:SetScript("OnMouseWheel", function(sf, delta)
      sf._wfLastScroll = GetTime()
      local cur = sf:GetVerticalScroll() or 0
      local step = rowH * 3
      if delta > 0 then
        cur = cur - step
      else
        cur = cur + step
      end
      if cur < 0 then cur = 0 end
      local max = (h - (sf:GetHeight() or 0))
      if max < 0 then max = 0 end
      if cur > max then cur = max end
      sf:SetVerticalScroll(cur)
    end)
  else
    self.scroll:SetVerticalScroll(0)
    self.scroll:SetScript("OnMouseWheel", nil)
  end
end

function UI:_UpdateLootEmptyState(shownCount)
  local ef = self._lootEmptyFrame
  if not ef then
    return
  end

  shownCount = tonumber(shownCount) or 0
  if shownCount < 0 then shownCount = 0 end

  -- Only show the "not active" message when a session isn't running.
  -- (Paused sessions keep showing existing loot rows.)
  local active = RealTrackerIsActive()

  if (not active) and shownCount == 0 then
    ef:Show()
  else
    ef:Hide()
  end
end


function UI:RefreshLoot(force)
  if not self.frame then
    return
  end

  -- Some Tracker builds return a wrapper table { entry=..., itemID=... }
  -- others return the entry directly. Support both so the loot list never
  -- "goes blank" when Tracker internals change.
  if not force then
    local ok = true
    if Tracker and Tracker.ConsumeDirty then
      ok = Tracker:ConsumeDirty()
    end
    if not ok then
      return
    end
  end

  -- Highlight active sort column in the main loot header (matches Overview → Recent Runs behavior)
  do
    local hdr = self._lootHdr
    if hdr then
      local sk = Tracker and (Tracker._lootSortKey or Tracker._lootSortMode) or nil
      sk = tostring(sk or "value")

      local function _Set(fs, active)
        if not fs or not fs.SetTextColor then return end
        if active then
          fs:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
        else
          fs:SetTextColor(Theme.muted[1], Theme.muted[2], Theme.muted[3], Theme.muted[4])
        end
      end

      _Set(hdr._wfLabelItem,  sk == "name")
      _Set(hdr._wfLabelCount, sk == "count")
      _Set(hdr._wfLabelPrice, sk == "price")
      _Set(hdr._wfLabelValue, sk == "value")
    end
  end

  local data = {}
  if Tracker and Tracker.GetSortedItems then
    data = Tracker:GetSortedItems() or {}
  end

  local shown = 0

  for i = 1, #self.rows do
    local row = self.rows[i]
    local d = data[i]

    -- Accept either:
    --  1) { entry = <table>, itemID = 123 }
    --  2) <entry table> (direct)
    local e = nil
    local itemID = nil
    if type(d) == "table" then
      if d.entry then
        e = d.entry
        itemID = d.itemID or d.id or d.itemId or (type(e) == "table" and (e.itemID or e.id))
      else
        e = d
        itemID = d.itemID or d.id or d.itemId
      end
    end

    if e then
      shown = shown + 1
      row._entry = e
      row:Show()

      if e.icon then
        row.icon:SetTexture(e.icon)
        row.icon:Show()
      else
        row.icon:Hide()
      end

      local link = e.link or e.itemLink or (itemID and ("item:" .. tostring(itemID))) or nil
      row.name:SetText(link or e.name or e.itemName or "Item")

      if WF.DB:GetSetting("colorByQuality") then
        -- Prefer the instance link (Remix/upgraded variants can differ from the base itemID quality).
        local r, g, b = Util:GetQualityColor(link or itemID)
        row.name:SetTextColor(r, g, b)
      else
        row.name:SetTextColor(Theme.text[1], Theme.text[2], Theme.text[3], Theme.text[4])
      end

      local count = (e.count ~= nil and e.count) or (type(d) == "table" and d.count) or 0
      row.count:SetText(tostring(count))

      local unit = (e.unitValue ~= nil and e.unitValue) or (type(d) == "table" and d.unitValue) or 0
      row.price:SetText(WF_FormatMoneyLetters(unit))

      local v = (e.value ~= nil and e.value) or (type(d) == "table" and d.value) or 0
      row.value:SetText(WF_FormatMoneyLetters(v))
    else
      row._entry = nil
      row:Hide()
    end
  end

  self:_UpdateLootScroll(shown)
  self:_UpdateLootEmptyState(shown)
end


function UI:_RecoverFromTransientScene(reason)
  if not (self and self.frame and self.frame.IsShown and self.frame:IsShown()) then
    return
  end

  local function DoRefresh()
    if not (UI and UI.frame and UI.frame.IsShown and UI.frame:IsShown()) then
      return
    end

    if UI.StartTicker then
      UI:StartTicker()
    end

    if UI.ApplyLayoutMode then
      UI:ApplyLayoutMode()
    end

    if UI._UpdateChipsWidth then
      UI:_UpdateChipsWidth()
    end
    if UI._FitMetricChips then
      UI:_FitMetricChips()
    end

    if UI.RefreshProfileUI then
      UI:RefreshProfileUI()
    end
    if UI.RefreshTitle then
      UI:RefreshTitle()
    end
    if UI.RefreshMetrics then
      UI:RefreshMetrics()
    end
    if UI.RefreshLoot then
      UI:RefreshLoot(true)
    end

    if UI.IsCompareOpen and UI:IsCompareOpen() and UI.RefreshComparePanel then
      UI:RefreshComparePanel()
    elseif UI.IsCompetitiveOpen and UI:IsCompetitiveOpen() and UI.RefreshCompetitivePanel then
      UI:RefreshCompetitivePanel(true)
    end
  end

  DoRefresh()

  if C_Timer and C_Timer.After then
    C_Timer.After(0, DoRefresh)
    C_Timer.After(0.15, DoRefresh)
    C_Timer.After(0.5, DoRefresh)
  end
end

function UI:_TryHookMovieFrameRecovery()
  if self._wfMovieFrameRecoveryHooked then
    return
  end

  local movieFrame = _G and _G.MovieFrame
  if not (movieFrame and movieFrame.HookScript) then
    return
  end

  movieFrame:HookScript("OnHide", function()
    if UI and UI._RecoverFromTransientScene then
      UI:_RecoverFromTransientScene("MovieFrameHide")
    end
  end)

  self._wfMovieFrameRecoveryHooked = true
end

function UI:_EnsureSceneRecoveryHooks()
  if self._wfSceneRecoveryHooksInstalled then
    self:_TryHookMovieFrameRecovery()
    return
  end
  self._wfSceneRecoveryHooksInstalled = true

  if self._TryHookMovieFrameRecovery then
    self:_TryHookMovieFrameRecovery()
  end

  if not (WF and WF.Events and WF.Events.On) then
    return
  end

  local function Recover(reason)
    if UI and UI._RecoverFromTransientScene then
      UI:_RecoverFromTransientScene(reason)
    end
  end

  WF.Events:On("CINEMATIC_STOP", function()
    Recover("CINEMATIC_STOP")
  end)

  WF.Events:On("PLAYER_ENTERING_WORLD", function()
    Recover("PLAYER_ENTERING_WORLD")
  end)

  WF.Events:On("DISPLAY_SIZE_CHANGED", function()
    Recover("DISPLAY_SIZE_CHANGED")
  end)

  WF.Events:On("UI_SCALE_CHANGED", function()
    Recover("UI_SCALE_CHANGED")
  end)

  WF.Events:On("PLAYER_FLAGS_CHANGED", function(_, unit)
    if unit == "player" then
      Recover("PLAYER_FLAGS_CHANGED")
    end
  end)

  WF.Events:On("PLAYER_LOGIN", function()
    if UI and UI._TryHookMovieFrameRecovery then
      UI:_TryHookMovieFrameRecovery()
    end
    Recover("PLAYER_LOGIN")
  end)

  if type(hooksecurefunc) == "function" and type(CinematicFinished) == "function" then
    hooksecurefunc("CinematicFinished", function()
      Recover("CinematicFinished")
    end)
  end
end

-- ---------------------------------------------------------
-- Ticker
-- ---------------------------------------------------------
function UI:StartTicker()
  if self._ticker then
    return
  end

  self._lastBroadcastAt = self._lastBroadcastAt or 0
  self._wfLastLootForce = self._wfLastLootForce or 0

  self._ticker = C_Timer.NewTicker(0.25, function()
    if not UI.frame or not UI.frame:IsShown() then
      return
    end

    UI:RefreshMetrics()

    -- Force a loot refresh at least once per second so the list can’t get “stuck”
    -- if Tracker forgets to set the dirty flag during a prepatch edge-case.
    local now = GetTime()
    if collectgarbage and (now - (UI._wfLastGcStepAt or 0)) >= 1.5 then
      UI._wfLastGcStepAt = now
      pcall(collectgarbage, "step", 64)
    end
    local forceLoot = false
    if (now - (UI._wfLastLootForce or 0)) >= 1.0 then
      UI._wfLastLootForce = now
      forceLoot = true
    end
    UI:RefreshLoot(forceLoot)

    -- broadcast competitive updates at ~1Hz while active
    if UI:IsCompetitiveOpen() and Tracker:IsActive() and UI._compSessionId then
      local n = SafeNow()
      if (n - (UI._lastBroadcastAt or 0)) >= 1 then
        UI._lastBroadcastAt = n
        UI:_SendCompetitiveUpdate()
      end
    end
  end)
end


function UI:StopTicker()
  if self._ticker then
    self._ticker:Cancel()
    self._ticker = nil
  end
end

-- ---------------------------------------------------------
-- Save Farm (existing behavior)
-- ---------------------------------------------------------
function UI:SaveCurrentFarm()
  local sess = Tracker.current or self._lastStoppedSession
  local pf = self._pendingFarm

  local mapId, x, y = Util:GetPlayerMapXY()
  local zone = Util:GetZoneLabel()
  local name = zone or "Farm"

  if sess then
    name = sess.farmName or sess.name or name
    zone = sess.zone or zone
    mapId = sess.mapId or mapId
    x = sess.x or x
    y = sess.y or y
  elseif pf then
    name = pf.name or name
    zone = pf.zone or zone
    mapId = pf.mapId or mapId
    x = pf.x or x
    y = pf.y or y
  end

  if not name or name == "" then name = "Farm" end
  if not zone or zone == "" then zone = "Unknown Zone" end

  local rec = {
    name = name,
    zone = zone,
    source = "manual",
    expansion = (pf and pf.expansion) or (sess and sess.expansion) or "Unknown",
    farmType  = (pf and pf.farmType)  or (sess and sess.farmType)  or "Any",
    focus     = (pf and pf.focus)     or (sess and sess.focus)     or nil,
    tags      = (pf and pf.tags)      or (sess and sess.tags)      or nil,
    notes     = (pf and pf.notes)     or (sess and sess.notes)     or nil,
  }

  if mapId then rec.mapId = mapId end
  if x and y then
    rec.x = x
    rec.y = y
  end

  local id = nil
  if WF.DB and WF.DB.UpsertFarm then
    id = WF.DB:UpsertFarm(rec)
  else
    -- hard fallback
    local db = WF.DB:Get()
    local newId = WF.DB:NextFarmId()
    rec.id = newId
    rec.createdAt = rec.createdAt or SafeNow()
    db.farms[newId] = rec
    id = newId
  end

  if Tracker.current then
    Tracker.current.farmId = id
  end

  if WF.UI.Help and WF.UI.Help.RefreshFarms then
    WF.UI.Help:RefreshFarms()
  end

  if UIErrorsFrame and UIErrorsFrame.AddMessage then
    UIErrorsFrame:AddMessage("What Farm?: Farm saved to library.", 0.35, 0.78, 1.0, 1)
  end

  -- Important: saving farms inside instances is a common use case.
  -- If your settings currently block it elsewhere, this explicit save still succeeds;
  -- coordinates may be missing depending on the map API.
end
