-- UI/HelpPages/History.lua
-- What Farm? :: HelpWindow - History page (split out)

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

WF.UI = WF.UI or {}
WF.UI.HelpPages = WF.UI.HelpPages or {}

WF.UI.HelpPages.History = function(ctx)
  local UI = ctx.UI
  local self = UI

  local WF = ctx.WF
  local Widgets = ctx.Widgets
  local Theme = ctx.Theme
  local Util = ctx.Util

  -- format helpers (provided by HelpWindow ctx)
  local Money = ctx.Money
  local FormatTime = ctx.FormatTime
  local SessionGPH = ctx.SessionGPH
  local SessionLIV = ctx.SessionLIV

  local RebuildHistoryAggregates = ctx.RebuildHistoryAggregates

  local C4 = ctx.C4
  local ACCENT = ctx.ACCENT
  local TEXT = ctx.TEXT
  local MUTED = ctx.MUTED
  local BG = ctx.BG
  local PANEL = ctx.PANEL
  local BORDER = ctx.BORDER

  local Money = ctx.Money
  local Clamp = ctx.Clamp
  local EnsureSettingsDefaults = ctx.EnsureSettingsDefaults
  local SafeNow = ctx.SafeNow
  local FormatTime = ctx.FormatTime

  -- -----------------------------------------------------
  -- Top loot: column layout state (keep in function scope)
  -- -----------------------------------------------------
  local lootLayout = { valueW = 112, countW = 34, tight = false }

  local H = {}

  local SetWaypoint = ctx.SetWaypoint


  -- -----------------------------------------------------
  -- Waypoint resolver (History)
  -- Allows waypoint/promote for instanced runs where sess.x/sess.y are often nil.
  -- -----------------------------------------------------
  function H._WF_Clamp01(v)
    v = tonumber(v)
    if not v then return nil end
    if v > 1 then
      v = v / 100
    end
    if v < 0 then return 0 end
    if v > 1 then return 1 end
    return v
  end

  function H._WF_TryLoadEncounterJournal()
    if (EJ_GetInstanceForMap and EJ_GetInstanceInfo) then return true end
    if not LoadAddOn then return false end
    pcall(LoadAddOn, "Blizzard_EncounterJournal")
    return (EJ_GetInstanceForMap and EJ_GetInstanceInfo) and true or false
  end

  function H._WF_EJ_DungeonAreaMapID(instanceID)
    if not instanceID then return nil end
    if not (EJ_GetInstanceInfo or H._WF_TryLoadEncounterJournal()) then return nil end
    if not EJ_GetInstanceInfo then return nil end

    local r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11 = EJ_GetInstanceInfo(instanceID)
    local candidates = { r6, r7, r8, r9, r10, r11 }
    for i = 1, #candidates do
      local v = candidates[i]
      if type(v) == "number" and v > 0 then
        if C_Map and C_Map.GetMapInfo then
          local info = C_Map.GetMapInfo(v)
          if info then return v end
        else
          return v
        end
      end
    end
    return nil
  end

  function H._WF_ResolveSessionWaypoint(sess)
    if type(sess) ~= "table" then return false end

    -- 1) Use saved world coords if present (best)
    if sess.worldMapId and sess.worldX and sess.worldY then
      local x = H._WF_Clamp01(sess.worldX)
      local y = H._WF_Clamp01(sess.worldY)
      if x and y then
        return true, sess.worldMapId, x, y
      end
    end

    -- 2) Use saved local coords if present
    if sess.mapId and sess.x and sess.y then
      local x = H._WF_Clamp01(sess.x)
      local y = H._WF_Clamp01(sess.y)
      if x and y then
        return true, sess.mapId, x, y
      end
    end

    -- 3) If we have a mapId but no coords (common for instanced runs),
    --    try to resolve a more useful "entrance" map via Encounter Journal.
    local mapId = sess.mapId or sess.worldMapId
    if mapId then
      local ejMap = nil
      if H._WF_TryLoadEncounterJournal() and EJ_GetInstanceForMap and EJ_GetInstanceInfo then
        local instanceID = sess.instanceID
        if not instanceID then
          instanceID = EJ_GetInstanceForMap(mapId)
        end
        ejMap = H._WF_EJ_DungeonAreaMapID(instanceID)
      end

      if ejMap then
        mapId = ejMap
      else
        -- If the map is a dungeon/instance map, prefer its parent map so the waypoint is visible in the world map.
        if C_Map and C_Map.GetMapInfo and Enum and Enum.UIMapType then
          local cur = mapId
          local loops = 0
          while loops < 8 do
            loops = loops + 1
            local info = C_Map.GetMapInfo(cur)
            if not info then break end
            local t = info.mapType
            if t == Enum.UIMapType.Dungeon or t == Enum.UIMapType.MicroDungeon or t == Enum.UIMapType.Instance then
              if info.parentMapID and info.parentMapID > 0 then
                cur = info.parentMapID
              else
                break
              end
            else
              break
            end
          end
          mapId = cur
        end
      end

      -- Safe fallback pin: center of the resolved map.
      return true, mapId, 0.5, 0.5
    end

    return false
  end
  function H._Trim(s)
    s = tostring(s or "")
    s = s:gsub("^%s+", ""):gsub("%s+$", "")
    return s
  end


  local MakePage = ctx.MakePage
  local MakePanel = ctx.MakePanel
  local MakeCard = ctx.MakeCard
  local MakeSectionTitle = ctx.MakeSectionTitle
  local MakeMutedText = ctx.MakeMutedText
  local MakeBodyText = ctx.MakeBodyText
  local MakeDivider = ctx.MakeDivider
  local MakeHeaderText = ctx.MakeHeaderText
  local MakeCombo = ctx.MakeCombo
  local MakeCheck = ctx.MakeCheck

  local EXPANSIONS = ctx.EXPANSIONS
  local FARM_TYPES = ctx.FARM_TYPES
  local FARM_AIMS = ctx.FARM_AIMS
  local RANDOM_MODES = ctx.RANDOM_MODES
  local PRICE_SOURCES = ctx.PRICE_SOURCES
  local TSM_LABELS = ctx.TSM_LABELS
  local TSMStringToLabel = ctx.TSMStringToLabel
  local TSM_BY_LABEL = ctx.TSM_BY_LABEL
  local TSM_BY_STR = ctx.TSM_BY_STR

-- -----------------------------------------------------
  -- HISTORY
  -- -----------------------------------------------------
  local pHistory = MakePage("History", "History")
  do
    local selected = nil

    local function ClearSelectedSession()
      UI._pendingSelectSession = nil
      selected = nil
    end

    local left = MakePanel(pHistory, PANEL)

    -- Solid header strip so the page title ("History") can't show through behind this panel header.
    do
      local rC, gC, bC, aC = C4(PANEL, PANEL)
      local cover = left:CreateTexture(nil, "BACKGROUND")
      cover:SetTexture("Interface/Buttons/WHITE8x8")
      cover:SetVertexColor(0, 0, 0, 0.96)
      cover:SetPoint("TOPLEFT", 1, -1)
      cover:SetPoint("TOPRIGHT", -1, -1)
      cover:SetHeight(34)
    end

    local t = MakeSectionTitle(left, "Sessions (newest)")
    t:SetPoint("TOPLEFT", 12, -10)

    -- -----------------------------------------------------
    -- Sort control (click the header)
    -- -----------------------------------------------------
    function H._GetHistorySortMode()
      if WF.DB and WF.DB.GetSetting then
        local m = WF.DB:GetSetting("historySortMode")
        if type(m) == "string" and m ~= "" then return m end
      end
      local db = (WF.DB and WF.DB.Get) and (WF.DB:Get() or {}) or {}
      db.settings = db.settings or {}
      local m = db.settings.historySortMode
      if type(m) == "string" and m ~= "" then return m end
      return "recent_desc"
    end

    function H._GetDisplayMetric()
      local m = "gph"
      if WF.DB and WF.DB.GetSetting then
        m = tostring(WF.DB:GetSetting("overviewLast5Metric") or "gph"):lower()
      else
        local db = (WF.DB and WF.DB.Get) and (WF.DB:Get() or {}) or {}
        db.settings = db.settings or {}
        m = tostring(db.settings.overviewLast5Metric or "gph"):lower()
      end
      if m ~= "liv" then m = "gph" end
      return m
    end

    function H._SessTotals(sess)
      local total = tonumber(sess and (sess.totalValue or sess.total or sess.value)) or 0
      local gold =
          tonumber(sess and sess.moneyGained)
          or tonumber(sess and sess.lootedGold)
          or tonumber(sess and sess.goldLooted)
          or tonumber(sess and sess.rawGold)
          or tonumber(sess and sess.money)
          or tonumber(sess and sess.gold)
          or 0
      gold = tonumber(gold) or 0
      if gold < 0 then gold = 0 end
      local livTotal = total - gold
      if livTotal < 0 then livTotal = 0 end
      return total, gold, livTotal
    end

    function H._SetHistorySortMode(mode)
      if type(mode) ~= "string" or mode == "" then return end
      if WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting("historySortMode", mode)
      else
        local db = WF.DB:Get() or {}
        db.settings = db.settings or {}
        db.settings.historySortMode = mode
      end
    end

    function H._ToggleHistorySort(primary, secondary)
      if type(primary) ~= "string" or primary == "" then return end
      local cur = H._GetHistorySortMode()
      if secondary and cur == primary then
        H._SetHistorySortMode(secondary)
      else
        H._SetHistorySortMode(primary)
      end
      if UI and UI.RefreshHistory then
        UI:RefreshHistory()
      end
    end

    function H._ToggleHistorySortValue()
      local metric = H._GetDisplayMetric()
      if metric == "liv" then
        H._ToggleHistorySort("liv_desc", "liv_asc")
      else
        H._ToggleHistorySort("gph_desc", "gph_asc")
      end
    end

    function H._SortLabel(mode)
      if mode == "recent_desc" then return "Newest" end
      if mode == "recent_asc"  then return "Oldest" end
      if mode == "total_desc"  then return "Value • High -> Low" end
      if mode == "total_asc"   then return "Value • Low -> High" end
      if mode == "gph_desc"    then return "Gold/hr • High -> Low" end
      if mode == "gph_asc"     then return "Gold/hr • Low -> High" end
      if mode == "liv_desc"    then return "LIV • High -> Low" end
      if mode == "liv_asc"     then return "LIV • Low -> High" end
      if mode == "items_desc"  then return "Most items" end
      if mode == "items_asc"   then return "Fewest items" end
      if mode == "dur_desc"    then return "Longest" end
      if mode == "dur_asc"     then return "Shortest" end
      if mode == "mode_az"     then return "Mode • A -> Z" end
      if mode == "mode_za"     then return "Mode • Z -> A" end
      if mode == "name_az"     then return "Name A -> Z" end
      if mode == "name_za"     then return "Name Z -> A" end
      return "Newest"
    end

    function H._UpdateHistoryHeader()
      if not t then return end
      local parts = { "Sessions", H._SortLabel(H._GetHistorySortMode()) }
      local mf = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
      if type(mf) == "string" and mf ~= "" then
        parts[#parts + 1] = H._WF_History_ModeName(mf)
      end
      t:SetText(table.concat(parts, " • "))
    end

    local _cycle = { "recent_desc", "total_desc", "gph_desc", "items_desc", "dur_desc", "name_az", "recent_asc" }
    function H._CycleHistorySort(dir)
      dir = tonumber(dir) or 1
      local cur = H._GetHistorySortMode()
      local idx = 1
      for i = 1, #_cycle do
        if _cycle[i] == cur then idx = i break end
      end
      idx = idx + dir
      if idx < 1 then idx = #_cycle end
      if idx > #_cycle then idx = 1 end
      H._SetHistorySortMode(_cycle[idx])
      UI:RefreshHistory()
    end

    function H._OpenHistorySortMenu()
      local cur = H._GetHistorySortMode()
      function H._item(text, mode)
        local isSel = (cur == mode)
        return {
          text = text,
          checked = isSel,
          disabled = isSel,
          fn = function()
            H._SetHistorySortMode(mode)
            UI:RefreshHistory()
          end
        }
      end

      UI:OpenContextMenu({
        H._item("Newest", "recent_desc"),
        H._item("Oldest", "recent_asc"),
        { sep = true },
        H._item("Value: High → Low", "total_desc"),
        H._item("Value: Low → High", "total_asc"),
        { sep = true },
        H._item("Gold/hr: High → Low", "gph_desc"),
        { sep = true },
        H._item("Items: Most", "items_desc"),
        H._item("Items: Least", "items_asc"),
        { sep = true },
        H._item("Duration: Longest", "dur_desc"),
        H._item("Duration: Shortest", "dur_asc"),
        { sep = true },
        H._item("Name: A → Z", "name_az"),
        H._item("Name: Z → A", "name_za"),
      }, 240)
    end

    do
      local headerBtn = CreateFrame("Button", nil, left)
      headerBtn:SetPoint("TOPLEFT", t, "TOPLEFT", -8, 8)
      headerBtn:SetPoint("BOTTOMRIGHT", t, "BOTTOMRIGHT", 8, -8)
      headerBtn:SetFrameLevel((left:GetFrameLevel() or 0) + 20)
      headerBtn:EnableMouse(false)
    end


-- ---------------------------------------------------------
-- History cleanup modal (Right-click: Clear History)
-- ---------------------------------------------------------
local WF_HistoryCleanupFrame

function H._WF_History_ParseSeconds(s)
  if type(s) ~= "string" then return 0 end
  s = s:lower()
  s = s:gsub(",", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")

  -- Support H:MM:SS or MM:SS
  if s:find(":") then
    local a, b, c = s:match("^(%d+):(%d+):(%d+)$")
    if a and b and c then
      return (tonumber(a) or 0) * 3600 + (tonumber(b) or 0) * 60 + (tonumber(c) or 0)
    end
    local m, ss = s:match("^(%d+):(%d+)$")
    if m and ss then
      return (tonumber(m) or 0) * 60 + (tonumber(ss) or 0)
    end
  end

  local h = tonumber(s:match("(%d+)%s*h")) or 0
  local m = tonumber(s:match("(%d+)%s*m")) or 0
  local ss = tonumber(s:match("(%d+)%s*s")) or 0

  -- If they typed just a number, treat it as seconds.
  if h == 0 and m == 0 and ss == 0 then
    local n = tonumber(s:match("^(%d+)$")) or 0
    return n
  end

  return h * 3600 + m * 60 + ss
end

function H._WF_History_ParseCopper(s)
  if type(s) ~= "string" then return 0 end
  s = s:lower()
  s = s:gsub(",", " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")

  local g = tonumber(s:match("(%d+)%s*g")) or 0
  local si = tonumber(s:match("(%d+)%s*s")) or 0
  local c = tonumber(s:match("(%d+)%s*c")) or 0

  if g > 0 or si > 0 or c > 0 then
    return g * 10000 + si * 100 + c
  end

  -- If they typed just a number, treat it as gold.
  local n = tonumber(s) or 0
  if n <= 0 then return 0 end
  return math.floor(n * 10000 + 0.5)
end

function H._WF_History_SessionElapsed(sess)
  if not sess then return 0 end
  local e = tonumber(sess.elapsed or sess.timeSpent or sess.duration or 0) or 0
  if e and e > 0 then return e end

  local a = tonumber(sess.startedAt or 0) or 0
  local b = tonumber(sess.endedAt or sess.stoppedAt or 0) or 0
  if a > 0 and b > a then
    return (b - a)
  end
  return 0
end

function H._WF_History_SessionTotalValue(sess)
  if not sess then return 0 end
  return tonumber(sess.totalValue or sess.total or sess.value) or 0
end

function H._WF_History_GetSessions()
  local db = WF.DB:Get() or {}
  db.history = db.history or {}
  db.history.sessions = db.history.sessions or {}
  return db, db.history.sessions
end

function H._WF_History_CountMatches(sessions, pred)
  local n = 0
  for i = 1, #sessions do
    if pred(sessions[i]) then n = n + 1 end
  end
  return n
end

function H._WF_History_Prune(db, pred)
  if not db then return end
  db.history = db.history or {}
  db.history.sessions = db.history.sessions or {}
  local sessions = db.history.sessions

  for i = #sessions, 1, -1 do
    if pred(sessions[i]) then
      table.remove(sessions, i)
    end
  end

  -- Rebuild aggregates cleanly
  db.history.totalsByFarm = {}
  db.history.lastRunByFarm = {}
  if RebuildHistoryAggregates then
    RebuildHistoryAggregates(db)
  end

  UI._pendingSelectSession = nil
  selected = nil
  UI:RefreshOverview()
  UI:RefreshHistory()
end

function H._WF_History_ModeName(v)
  local m = ""
  if type(v) == "table" then
    m = H._Trim(v.mode or "")
  else
    m = H._Trim(v or "")
  end
  if m == "" then m = "Default" end
  return m
end

function H._WF_History_ModeKey(v)
  return string.lower(tostring(H._WF_History_ModeName(v) or ""))
end

function H._WF_History_GetCleanupModeNames()
  local out, seen = {}, {}

  function H.Add(name)
    local display = H._WF_History_ModeName(name)
    local key = string.lower(display)
    if display ~= "" and not seen[key] then
      seen[key] = true
      out[#out + 1] = display
    end
  end

  local active = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
  if active then H.Add(active) end
  H.Add("Default")

  if WF and WF.DB and WF.DB.GetProfileNames then
    local names = WF.DB:GetProfileNames()
    if type(names) == "table" then
      for i = 1, #names do
        H.Add(names[i])
      end
    end
  end

  return out
end

function H._WF_History_EnsureCleanupPopup()
  if WF_HistoryCleanupFrame then return WF_HistoryCleanupFrame end

  local f = CreateFrame("Frame", "WF_HistoryCleanupPopup", UIParent, "BackdropTemplate")
  WF_HistoryCleanupFrame = f
  f:SetFrameStrata("DIALOG")
  f:SetFrameLevel(170)
  f:SetSize(480, 332)
  f:SetPoint("CENTER")
  f:EnableMouse(true)
  if f.SetClampedToScreen then f:SetClampedToScreen(true) end
  f:Hide()

  f:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 }
  })
  f:SetBackdropColor(0, 0, 0, 1)
  f:SetBackdropBorderColor(1, 1, 1, 0.14)

  if type(UISpecialFrames) == "table" then
    local exists = false
    for i = 1, #UISpecialFrames do
      if UISpecialFrames[i] == "WF_HistoryCleanupPopup" then exists = true break end
    end
    if not exists then table.insert(UISpecialFrames, "WF_HistoryCleanupPopup") end
  end

  local title = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  title:SetTextColor(ACCENT[1], ACCENT[2], ACCENT[3], 1)
  title:SetText("Cleanup History")

  local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
  close:SetPoint("TOPRIGHT", -4, -4)
  if Widgets and Widgets.SkinCloseButton then
    Widgets:SkinCloseButton(close)
  end

  function H.MakeInput(parent, w)
    local box = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    box:SetSize(w or 120, 22)
    box:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
      insets = { left = 1, right = 1, top = 1, bottom = 1 }
    })
    box:SetBackdropColor(0, 0, 0, 0.22)
    box:SetBackdropBorderColor(1, 1, 1, 0.10)

    local e = CreateFrame("EditBox", nil, box)
    e:SetAllPoints(box)
    e:SetFontObject("WF_GameFontHighlightSmall")
    e:SetAutoFocus(false)
    e:SetJustifyH("CENTER")
    e:SetTextInsets(6, 6, 0, 0)
    e:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
    e:SetScript("OnEscapePressed", e.ClearFocus)
    e:SetScript("OnEnterPressed", e.ClearFocus)

    e:SetScript("OnEditFocusGained", function()
      if box.SetBackdropBorderColor then box:SetBackdropBorderColor(1, 1, 1, 0.18) end
      if box.SetBackdropColor then box:SetBackdropColor(0, 0, 0, 0.28) end
    end)
    e:SetScript("OnEditFocusLost", function()
      if box.SetBackdropBorderColor then box:SetBackdropBorderColor(1, 1, 1, 0.10) end
      if box.SetBackdropColor then box:SetBackdropColor(0, 0, 0, 0.22) end
    end)

    box.edit = e
    return box
  end

  local divider = f:CreateTexture(nil, "ARTWORK")
  divider:SetColorTexture(1, 1, 1, 0.08)
  divider:SetHeight(1)
  divider:SetPoint("LEFT", 14, 0)
  divider:SetPoint("RIGHT", -14, 0)

  local previewTitle = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  previewTitle:SetTextColor(ACCENT[1], ACCENT[2], ACCENT[3], 1)
  previewTitle:SetText("Matches")

  local preview = f:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  preview:SetPoint("RIGHT", f, "RIGHT", -14, 0)
  preview:SetJustifyH("LEFT")
  preview:SetTextColor(MUTED[1], MUTED[2], MUTED[3], 1)
  preview:SetText("")

  function H.UpdateModeButton()
    local modeName = H._WF_History_ModeName(f._cleanupMode or "Default")
    if f._modeButton and f._modeButton.label then
      f._modeButton.label:SetText(modeName)
    end
    if f._modeLabel then
      f._modeLabel:SetText("Delete all runs for mode:")
    end
  end

  function H.UpdatePreview()
    local db, sessions = H._WF_History_GetSessions()

    local t = H._WF_History_ParseSeconds(f._timeBox and f._timeBox.edit and f._timeBox.edit:GetText() or "")
    local v = H._WF_History_ParseCopper(f._valueBox and f._valueBox.edit and f._valueBox.edit:GetText() or "")
    local g = H._WF_History_ParseCopper(f._gphBox and f._gphBox.edit and f._gphBox.edit:GetText() or "")
    local modeName = H._WF_History_ModeName(f._cleanupMode or "Default")
    local modeKey = H._WF_History_ModeKey(modeName)

    local nTime = H._WF_History_CountMatches(sessions, function(sess)
      return H._WF_History_SessionElapsed(sess) <= t
    end)

    local nVal = H._WF_History_CountMatches(sessions, function(sess)
      return H._WF_History_SessionTotalValue(sess) <= v
    end)

    local nGPH = H._WF_History_CountMatches(sessions, function(sess)
      return (tonumber(SessionGPH(sess)) or 0) <= g
    end)

    local nMode = H._WF_History_CountMatches(sessions, function(sess)
      return H._WF_History_ModeKey(sess) == modeKey
    end)

    preview:SetText(string.format(
      "Time <= %s: %d\nValue <= %s: %d\nGPH <= %s: %d\nMode = %s: %d",
      tostring(H._FormatDuration and H._FormatDuration(t) or (tostring(t or 0) .. "s")),
      nTime,
      tostring(Money(v) or "0g 0s 0c"),
      nVal,
      tostring(Money(g) or "0g 0s 0c"),
      nGPH,
      tostring(modeName),
      nMode
    ))
  end

  function H.OpenModeMenu()
    local items = {}
    local names = H._WF_History_GetCleanupModeNames()
    local cur = H._WF_History_ModeName(f._cleanupMode or "Default")

    for i = 1, #names do
      local modeName = names[i]
      items[#items + 1] = {
        text = modeName,
        checked = (H._WF_History_ModeKey(modeName) == H._WF_History_ModeKey(cur)),
        fn = function()
          f._cleanupMode = modeName
          H.UpdateModeButton()
          H.UpdatePreview()
        end
      }
    end

    if UI and UI.OpenContextMenu then
      UI:OpenContextMenu(items, 220)
    end
  end

  function H.ShowCleanupConfirm(bodyText, onYes, yesText)
    if not (UI and UI.Confirm) then
      if onYes then onYes() end
      return
    end

    UI:Confirm("Cleanup history", bodyText, onYes, yesText)

    if UI._confirm then
      UI._confirm:SetWidth(f:GetWidth())
      UI._confirm:ClearAllPoints()
      UI._confirm:SetPoint("CENTER", f, "CENTER", 0, 0)
    end
  end

  -- Row 1: time
  local row1 = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  row1:SetPoint("TOPLEFT", 14, -48)
  row1:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
  row1:SetText("Delete runs under time:")

  local timeBox = H.MakeInput(f, 120)
  timeBox:SetPoint("LEFT", row1, "RIGHT", 10, 0)
  timeBox.edit:SetText("0m 0s")
  f._timeBox = timeBox

  local bDelTime = Widgets:Button(f, "Delete", 90, 22)
  bDelTime:SetPoint("LEFT", timeBox, "RIGHT", 10, 0)
  bDelTime:SetScript("OnClick", function()
    local t = H._WF_History_ParseSeconds(timeBox.edit:GetText() or "")
    local db, sessions = H._WF_History_GetSessions()
    local n = H._WF_History_CountMatches(sessions, function(sess) return H._WF_History_SessionElapsed(sess) <= t end)
    local timeText = tostring(H._FormatDuration and H._FormatDuration(t) or (tostring(t or 0) .. "s"))
    H.ShowCleanupConfirm(string.format("Delete %d runs with time equal to or below %s? This cannot be undone.", n, timeText), function()
      H._WF_History_Prune(db, function(sess) return H._WF_History_SessionElapsed(sess) <= t end)
      f:Hide()
    end)
  end)

  -- Row 2: value
  local row2 = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  row2:SetPoint("TOPLEFT", row1, "BOTTOMLEFT", 0, -22)
  row2:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
  row2:SetText("Delete runs under value:")

  local valueBox = H.MakeInput(f, 120)
  valueBox:SetPoint("LEFT", row2, "RIGHT", 10, 0)
  valueBox.edit:SetText("0g 0s 0c")
  f._valueBox = valueBox

  local bDelVal = Widgets:Button(f, "Delete", 90, 22)
  bDelVal:SetPoint("LEFT", valueBox, "RIGHT", 10, 0)
  bDelVal:SetScript("OnClick", function()
    local v = H._WF_History_ParseCopper(valueBox.edit:GetText() or "")
    local db, sessions = H._WF_History_GetSessions()
    local n = H._WF_History_CountMatches(sessions, function(sess) return H._WF_History_SessionTotalValue(sess) <= v end)
    H.ShowCleanupConfirm(string.format("Delete %d runs with value equal to or below %s? This cannot be undone.", n, tostring(Money(v) or "")), function()
      H._WF_History_Prune(db, function(sess) return H._WF_History_SessionTotalValue(sess) <= v end)
      f:Hide()
    end)
  end)

  -- Row 3: gph
  local row3 = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  row3:SetPoint("TOPLEFT", row2, "BOTTOMLEFT", 0, -22)
  row3:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
  row3:SetText("Delete runs under GPH:")

  local gphBox = H.MakeInput(f, 120)
  gphBox:SetPoint("LEFT", row3, "RIGHT", 10, 0)
  gphBox.edit:SetText("0g 0s 0c")
  f._gphBox = gphBox

  local bDelGPH = Widgets:Button(f, "Delete", 90, 22)
  bDelGPH:SetPoint("LEFT", gphBox, "RIGHT", 10, 0)
  bDelGPH:SetScript("OnClick", function()
    local g = H._WF_History_ParseCopper(gphBox.edit:GetText() or "")
    local db, sessions = H._WF_History_GetSessions()
    local n = H._WF_History_CountMatches(sessions, function(sess)
      return (tonumber(SessionGPH(sess)) or 0) <= g
    end)

    H.ShowCleanupConfirm(string.format("Delete %d runs with GPH equal to or below %s? This cannot be undone.", n, tostring(Money(g) or "")), function()
      H._WF_History_Prune(db, function(sess)
        return (tonumber(SessionGPH(sess)) or 0) <= g
      end)
      f:Hide()
    end)
  end)

  -- Row 4: mode selector
  local row4 = f:CreateFontString(nil, "OVERLAY", "WF_GameFontNormal")
  row4:SetPoint("TOPLEFT", row3, "BOTTOMLEFT", 0, -26)
  row4:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
  row4:SetText("Delete all runs for mode:")
  f._modeLabel = row4

  local modeButton = Widgets:Button(f, "Default", 140, 22)
  modeButton:SetPoint("LEFT", row4, "RIGHT", 10, 0)
  modeButton:SetScript("OnClick", H.OpenModeMenu)
  f._modeButton = modeButton

  local bDelMode = Widgets:Button(f, "Delete Mode", 120, 22)
  bDelMode:SetPoint("LEFT", modeButton, "RIGHT", 10, 0)
  bDelMode:SetScript("OnClick", function()
    local modeName = H._WF_History_ModeName(f._cleanupMode or "Default")
    local modeKey = H._WF_History_ModeKey(modeName)
    local db, sessions = H._WF_History_GetSessions()
    local n = H._WF_History_CountMatches(sessions, function(sess)
      return H._WF_History_ModeKey(sess) == modeKey
    end)

    H.ShowCleanupConfirm(string.format("Delete %d %s runs? This cannot be undone.", n, tostring(modeName)), function()
      H._WF_History_Prune(db, function(sess)
        return H._WF_History_ModeKey(sess) == modeKey
      end)
      f:Hide()
    end)
  end)

  -- Row 5: delete all (safety confirm)
  local bDelAll = Widgets:Button(f, "Delete ALL History", 150, 22)
  bDelAll:SetPoint("TOPLEFT", row4, "BOTTOMLEFT", 0, -24)

  divider:SetPoint("TOP", bDelAll, "BOTTOM", 0, -18)
  previewTitle:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", 0, -10)
  preview:SetPoint("TOPLEFT", previewTitle, "BOTTOMLEFT", 0, -6)
  bDelAll:SetScript("OnClick", function()
    UI:Confirm("Clear history", "This will delete all saved sessions and reset your Recent runs. This cannot be undone.", function()
      local db = WF.DB:Get() or {}
      db.history = db.history or {}
      db.history.sessions = {}
      db.history.totalsByFarm = {}
      db.history.lastRunByFarm = {}
      UI._pendingSelectSession = nil
      selected = nil
      UI:RefreshOverview()
      UI:RefreshHistory()
      f:Hide()
    end)
  end)

  -- Live preview updates while typing
  timeBox.edit:SetScript("OnTextChanged", function() H.UpdatePreview() end)
  valueBox.edit:SetScript("OnTextChanged", function() H.UpdatePreview() end)
  gphBox.edit:SetScript("OnTextChanged", function() H.UpdatePreview() end)

  f._UpdatePreview = H.UpdatePreview
  f._UpdateModeButton = H.UpdateModeButton

  local activeMode = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
  f._cleanupMode = H._WF_History_ModeName(activeMode or "Default")
  H.UpdateModeButton()
  H.UpdatePreview()

  return f
end

function H._WF_History_PromptCleanup()
  local f = H._WF_History_EnsureCleanupPopup()
  local activeMode = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil
  if f then
    f._cleanupMode = H._WF_History_ModeName(activeMode or f._cleanupMode or "Default")
    if f._UpdateModeButton then f._UpdateModeButton() end
    if f._UpdatePreview then f._UpdatePreview() end
  end
  f:Show()
  if f._timeBox and f._timeBox.edit and f._timeBox.edit.SetFocus then f._timeBox.edit:SetFocus() end
end

    local bClearHistory = Widgets:Button(left, "Clear History", 120, 22)
    bClearHistory:SetPoint("TOPRIGHT", -12, -10)
    if bClearHistory.RegisterForClicks then bClearHistory:RegisterForClicks("AnyUp") end
    bClearHistory:SetScript("OnClick", function(_, button)
      if button == "RightButton" then
        H._WF_History_PromptCleanup()
        return
      end
      UI:Confirm("Clear history", "This will delete all saved sessions and reset your Recent runs. This cannot be undone.", function()
        local db = WF.DB:Get() or {}
        db.history = db.history or {}
        db.history.sessions = {}
        db.history.totalsByFarm = {}
        db.history.lastRunByFarm = {}
        UI._pendingSelectSession = nil
        selected = nil
        UI:RefreshOverview()
        UI:RefreshHistory()
      end)
    end)

    -- Search sessions (fixed width; won't stretch when the window is resized)
    UI._historySearchText = UI._historySearchText or ""

    local searchBox = CreateFrame("Frame", nil, left, "BackdropTemplate")
    searchBox:SetSize(190, 22) -- fixed width (no stretch)
    searchBox:SetPoint("TOPRIGHT", bClearHistory, "TOPLEFT", -8, 0)
    searchBox:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
      insets = { left = 1, right = 1, top = 1, bottom = 1 }
    })
    searchBox:SetBackdropColor(0, 0, 0, 0.22)
    searchBox:SetBackdropBorderColor(1, 1, 1, 0.10)

    local searchIcon = searchBox:CreateTexture(nil, "ARTWORK")
    searchIcon:SetTexture("Interface/Common/UI-Searchbox-Icon")
    searchIcon:SetSize(14, 14)
    searchIcon:SetPoint("LEFT", 6, 0)
    searchIcon:SetVertexColor(MUTED[1], MUTED[2], MUTED[3], 0.9)

    local searchEdit = CreateFrame("EditBox", nil, searchBox)
    searchEdit:SetAllPoints(searchBox)
    searchEdit:SetFontObject("WF_GameFontHighlightSmall")
    searchEdit:SetAutoFocus(false)
    searchEdit:SetJustifyH("LEFT")
    searchEdit:SetTextInsets(22, 6, 0, 0)
    searchEdit:SetTextColor(TEXT[1], TEXT[2], TEXT[3], 1)
    searchEdit:SetScript("OnEscapePressed", searchEdit.ClearFocus)
    searchEdit:SetScript("OnEnterPressed", searchEdit.ClearFocus)

    local ph = searchBox:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    ph:SetPoint("LEFT", searchBox, "LEFT", 22, 0)
    ph:SetTextColor(MUTED[1], MUTED[2], MUTED[3], 0.9)
    ph:SetText("Search History")

    function H._UpdateSearchPlaceholder()
      if not searchEdit or not ph then return end
      local txt = searchEdit:GetText() or ""
      if txt == "" and not searchEdit:HasFocus() then ph:Show() else ph:Hide() end
    end

    local _searchPending = false
    function H._ApplyHistorySearch()
      _searchPending = false
      UI._historySearchText = H._Trim(tostring(searchEdit:GetText() or ""))
      if UI and UI.RefreshHistory then UI:RefreshHistory() end
      H._UpdateSearchPlaceholder()
    end

    searchEdit:SetScript("OnTextChanged", function(_, userInput)
      if not userInput then
        H._UpdateSearchPlaceholder()
        return
      end
      H._UpdateSearchPlaceholder()
      if _searchPending then return end
      _searchPending = true
      if C_Timer and C_Timer.After then
        C_Timer.After(0.08, H._ApplyHistorySearch)
      else
        H._ApplyHistorySearch()
      end
    end)

    searchEdit:SetScript("OnEditFocusGained", function()
      if searchBox.SetBackdropBorderColor then searchBox:SetBackdropBorderColor(1, 1, 1, 0.18) end
      if searchBox.SetBackdropColor then searchBox:SetBackdropColor(0, 0, 0, 0.28) end
      H._UpdateSearchPlaceholder()
    end)

    searchEdit:SetScript("OnEditFocusLost", function()
      if searchBox.SetBackdropBorderColor then searchBox:SetBackdropBorderColor(1, 1, 1, 0.10) end
      if searchBox.SetBackdropColor then searchBox:SetBackdropColor(0, 0, 0, 0.22) end
      H._ApplyHistorySearch()
    end)

    if UI._historySearchText ~= "" then
      searchEdit:SetText(UI._historySearchText)
    end
    H._UpdateSearchPlaceholder()

    UI._historySearchEdit = searchEdit



    local div = MakeDivider(left)
    div:SetPoint("TOPLEFT", 12, -34)
    div:SetPoint("TOPRIGHT", -12, -34)

    -- Sessions list header (table-style, matches Overview "Recent runs")
local header = CreateFrame("Frame", nil, left)
header:SetPoint("TOPLEFT", 10, -40)
header:SetPoint("TOPRIGHT", -32, -40)
header:SetHeight(22)

header.bg = header:CreateTexture(nil, "BACKGROUND")
header.bg:SetAllPoints(header)
header.bg:SetTexture("Interface/Buttons/WHITE8x8")
header.bg:SetVertexColor(1, 1, 1, 0.06)
local hFarm = header:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
local hVal  = header:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
local hMode = header:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
local hDur  = header:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
local hDate = header:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")

hFarm:SetText("Farm")
hMode:SetText("Mode")
hDur:SetText("Time")
hDate:SetText("Date")

-- Blue headers (match Overview)
do
  local rH, gH, bH, aH = C4(ACCENT, ACCENT)
  hFarm:SetTextColor(rH, gH, bH, 0.95)
  hVal:SetTextColor(rH, gH, bH, 0.95)
  hMode:SetTextColor(rH, gH, bH, 0.95)
  hDur:SetTextColor(rH, gH, bH, 0.95)
  hDate:SetTextColor(rH, gH, bH, 0.95)
end


-- Header font (match Overview)
do
  function H._HdrFont(fs)
    if not (fs and fs.GetFont and fs.SetFont) then return end
    local f, _, fl = fs:GetFont()
    if f then
      fs:SetFont(f, (WF and WF.UI and WF.UI.Fonts and WF.UI.Fonts.sizes and WF.UI.Fonts.sizes.headerSmall) or 11, fl or "OUTLINE")
    end
  end
  H._HdrFont(hFarm)
  H._HdrFont(hVal)
  H._HdrFont(hMode)
  H._HdrFont(hDur)
  H._HdrFont(hDate)
end
hFarm:SetJustifyH("LEFT")
hVal:SetJustifyH("RIGHT")
hMode:SetJustifyH("CENTER")
hDur:SetJustifyH("CENTER")
hDate:SetJustifyH("RIGHT")


-- Header click targets (sort by column)
local hbFarm = CreateFrame("Button", nil, header)
local hbVal  = CreateFrame("Button", nil, header)
local hbMode = CreateFrame("Button", nil, header)
local hbDur  = CreateFrame("Button", nil, header)
local hbDate = CreateFrame("Button", nil, header)


-- Hover highlight (match Overview)
function H._HeaderHover(btn, on)
  if btn and btn._hl then
    btn._hl:SetAlpha(on and 0.08 or 0)
  end
end

function H._MakeHeaderHL(btn)
  if not btn or btn._hl then return end
  local hl = btn:CreateTexture(nil, "BACKGROUND")
  hl:SetAllPoints(btn)
  hl:SetTexture("Interface/Buttons/WHITE8x8")
  hl:SetVertexColor(1, 1, 1, 1)
  hl:SetAlpha(0)
  btn._hl = hl
end

H._MakeHeaderHL(hbFarm)
H._MakeHeaderHL(hbVal)
H._MakeHeaderHL(hbMode)
H._MakeHeaderHL(hbDur)
H._MakeHeaderHL(hbDate)

hbFarm:SetScript("OnEnter", function(self) H._HeaderHover(self, true) end)
hbFarm:SetScript("OnLeave", function(self) H._HeaderHover(self, false) end)
hbVal:SetScript("OnEnter", function(self) H._HeaderHover(self, true) end)
hbVal:SetScript("OnLeave", function(self) H._HeaderHover(self, false) end)
hbDur:SetScript("OnEnter", function(self) H._HeaderHover(self, true) end)
hbDur:SetScript("OnLeave", function(self) H._HeaderHover(self, false) end)
hbDate:SetScript("OnEnter", function(self) H._HeaderHover(self, true) end)
hbDate:SetScript("OnLeave", function(self) H._HeaderHover(self, false) end)

function H._HeaderClickOnlyLeft(fn)
  return function(_, btn)
    if btn ~= "LeftButton" then return end
    if fn then fn() end
  end
end

hbFarm:SetScript("OnClick", H._HeaderClickOnlyLeft(function()
  H._ToggleHistorySort("name_az", "name_za")
end))

hbVal:SetScript("OnClick", H._HeaderClickOnlyLeft(function()
  H._ToggleHistorySortValue()
end))

hbMode:RegisterForClicks("LeftButtonUp", "RightButtonUp")
hbMode:SetScript("OnClick", function(_, button)
  if button == "RightButton" then
    if UI and UI.OpenModeFilterMenu then UI:OpenModeFilterMenu() end
    return
  end
  H._ToggleHistorySort("mode_az", "mode_za")
end)

hbMode:SetScript("OnEnter", function(self)
  H._HeaderHover(self, true) -- <-- add this
  GameTooltip:SetOwner(self, "ANCHOR_TOPRIGHT")
  GameTooltip:AddLine("Mode", ACCENT[1], ACCENT[2], ACCENT[3], true)
  GameTooltip:AddLine("Left-click: sort by Mode", 0.70, 0.70, 0.70)
  GameTooltip:AddLine("Right-click: open Mode filter", 0.70, 0.70, 0.70)
  GameTooltip:Show()
end)

hbMode:SetScript("OnLeave", function(self)
  H._HeaderHover(self, false)
  GameTooltip:Hide()
end)

hbDur:SetScript("OnClick", H._HeaderClickOnlyLeft(function()
  H._ToggleHistorySort("dur_desc", "dur_asc")
end))

hbDate:SetScript("OnClick", H._HeaderClickOnlyLeft(function()
  H._ToggleHistorySort("recent_desc", "recent_asc")
end))


-- Sessions scroll (starts below header)
local scroll = CreateFrame("ScrollFrame", nil, left, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
scroll:SetPoint("TOPLEFT", 6, -62)
scroll:SetPoint("BOTTOMRIGHT", -28, 10)

local sc = CreateFrame("Frame", nil, scroll)
    sc:SetSize(1, 1)
    scroll:SetScrollChild(sc)
    scroll:EnableMouse(true)
    scroll:SetScript("OnMouseUp", function(_, button)
      if button ~= "LeftButton" then return end
      if not selected then return end
      ClearSelectedSession()
      UI:RefreshHistory()
    end)

    -- Empty state (Sessions list)
    local emptySessions = scroll:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    emptySessions:SetPoint("CENTER", scroll, "CENTER", 0, 0)
    emptySessions:SetJustifyH("CENTER")
    emptySessions:SetTextColor(C4(MUTED, MUTED))
    emptySessions:SetText("No sessions yet.\nStart tracking a farm to populate History.")
    emptySessions:Hide()

    function H._UpdateEmptySessionsWidth()
      local w = scroll:GetWidth() or 0
      if w > 60 then emptySessions:SetWidth(w - 40) end
    end
    H._UpdateEmptySessionsWidth()
    scroll:HookScript("OnSizeChanged", H._UpdateEmptySessionsWidth)
    UI._historyEmptySessions = emptySessions

    local right = MakePanel(pHistory, PANEL)

    local rTitle = MakeSectionTitle(right, "Session details")
    rTitle:SetPoint("TOPLEFT", 12, -10)

    local shareBtn = Widgets:Button(right, "Share", 92, 22)
    shareBtn:SetPoint("TOPRIGHT", -12, -8)
    shareBtn:SetScript("OnClick", function()
      if WF and WF.Share and WF.Share.ShowShareDialog then
        WF.Share:ShowShareDialog(selected)
      end
    end)

    function H._SetShareButtonEnabled(enabled)
      if not shareBtn then return end
      enabled = enabled and true or false

      if enabled then
        if shareBtn.Enable then shareBtn:Enable() end
        if shareBtn.EnableMouse then shareBtn:EnableMouse(true) end
        shareBtn:SetAlpha(1)
        if shareBtn.label then
          shareBtn.label:SetTextColor(C4(TEXT, TEXT))
        end
      else
        if shareBtn.Disable then shareBtn:Disable() end
        if shareBtn.EnableMouse then shareBtn:EnableMouse(false) end
        shareBtn:SetAlpha(0.45)
        if shareBtn.label then
          shareBtn.label:SetTextColor(C4(MUTED, MUTED))
        end
      end
    end
    H._SetShareButtonEnabled(false)

    local name = MakeBodyText(right, "Select a session")
    name:SetPoint("TOPLEFT", 12, -32)
    -- Empty-state default (we swap to ACCENT when a session is selected)
    name:SetTextColor(C4(MUTED, MUTED))

    local metaScroll = CreateFrame("ScrollFrame", nil, right, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and metaScroll.ScrollBar then Widgets:SkinScrollBar(metaScroll.ScrollBar) end
  metaScroll:SetPoint("TOPLEFT", 12, -52)
  metaScroll:SetPoint("TOPRIGHT", -28, -52)
  metaScroll:SetHeight(112)

  local metaContent = CreateFrame("Frame", nil, metaScroll)
  metaContent:SetSize(1, 1)
  metaScroll:SetScrollChild(metaContent)

  local meta = metaContent:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  meta:SetPoint("TOPLEFT", 0, 0)
  meta:SetPoint("TOPRIGHT", 0, 0)
  meta:SetJustifyH("LEFT")
  meta:SetJustifyV("TOP")
  meta:SetTextColor(C4(TEXT, TEXT))
  meta:Hide()

  local detailRows = {}

  function H._AcquireDetailRow(index)
    local row = detailRows[index]
    if row then return row end

    row = CreateFrame("Frame", nil, metaContent)
    row:SetHeight(18)

    row.single = row:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    row.single:SetPoint("TOPLEFT", 0, 0)
    row.single:SetJustifyH("LEFT")
    row.single:SetJustifyV("TOP")
    row.single:SetWordWrap(true)
    row.single:SetTextColor(C4(TEXT, TEXT))

    row.left = row:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    row.left:SetPoint("TOPLEFT", 0, 0)
    row.left:SetJustifyH("LEFT")
    row.left:SetJustifyV("TOP")
    row.left:SetWordWrap(true)
    row.left:SetTextColor(C4(TEXT, TEXT))

    row.right = row:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    row.right:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    row.right:SetJustifyH("LEFT")
    row.right:SetJustifyV("TOP")
    row.right:SetWordWrap(true)
    row.right:SetTextColor(C4(TEXT, TEXT))

    detailRows[index] = row
    return row
  end

  function H._HideAllDetailRows()
    for i = 1, #detailRows do
      local row = detailRows[i]
      if row then
        row:Hide()
        if row.single then row.single:Hide() end
        if row.left then row.left:Hide() end
        if row.right then row.right:Hide() end
      end
    end
  end

  function H._RenderDetailRows(specs, specCount)
    if not metaContent or not metaScroll then return end

    local width = metaScroll:GetWidth() or 1
    if width < 1 then width = 1 end
    local colGap = 18
    local colWidth = math.floor((width - colGap) / 2)
    if colWidth < 40 then colWidth = width end

    metaContent:SetWidth(width)

    local prev = nil
    local totalHeight = 0
    for i = 1, specCount do
      local spec = specs[i]
      local row = H._AcquireDetailRow(i)

      row:ClearAllPoints()
      if prev then
        row:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -1)
        row:SetPoint("TOPRIGHT", prev, "BOTTOMRIGHT", 0, -1)
        totalHeight = totalHeight + 1
      else
        row:SetPoint("TOPLEFT", metaContent, "TOPLEFT", 0, 0)
        row:SetPoint("TOPRIGHT", metaContent, "TOPRIGHT", 0, 0)
      end

      if spec and spec.kind == "spacer" then
        row.single:SetText("")
        row.single:Hide()
        row.left:SetText("")
        row.right:SetText("")
        row.left:Hide()
        row.right:Hide()
        row:SetHeight(tonumber(spec.height) or 10)
      elseif spec and spec.kind == "double" then
        row.single:SetText("")
        row.single:Hide()

        row.left:ClearAllPoints()
        row.left:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        row.left:SetWidth(colWidth)
        row.left:SetText(spec.leftText or "")
        row.left:Show()

        row.right:ClearAllPoints()
        row.right:SetPoint("TOPLEFT", row, "TOPLEFT", colWidth + colGap, 0)
        row.right:SetWidth(colWidth)
        row.right:SetText(spec.rightText or "")
        row.right:Show()

        local leftH = row.left:GetStringHeight() or 0
        local rightH = row.right:GetStringHeight() or 0
        row:SetHeight(math.max(leftH, rightH, 14) + 1)
      else
        row.left:SetText("")
        row.right:SetText("")
        row.left:Hide()
        row.right:Hide()

        row.single:ClearAllPoints()
        row.single:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        row.single:SetWidth(width)
        row.single:SetText((spec and spec.text) or "")
        row.single:Show()

        local singleH = row.single:GetStringHeight() or 0
        row:SetHeight(math.max(singleH, 14) + 1)
      end

      row:Show()
      totalHeight = totalHeight + (row:GetHeight() or 0)
      prev = row
    end

    for i = specCount + 1, #detailRows do
      local row = detailRows[i]
      if row then
        row:Hide()
        if row.single then row.single:Hide() end
        if row.left then row.left:Hide() end
        if row.right then row.right:Hide() end
      end
    end

    if totalHeight < 1 then totalHeight = 1 end
    metaContent:SetHeight(totalHeight + 2)

    if metaScroll.SetVerticalScroll then metaScroll:SetVerticalScroll(0) end
  end

  -- Empty-state text for Session details (when no session is selected)
  local emptyMeta = metaScroll:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  emptyMeta:SetPoint("CENTER", metaScroll, "CENTER", 0, 0)
  emptyMeta:SetJustifyH("CENTER")
  emptyMeta:SetTextColor(C4(MUTED, MUTED))
  emptyMeta:SetText("Select a session to view details.")
  emptyMeta:Hide()
  UI._historyEmptyMeta = emptyMeta

    local lootPanel = MakePanel(right, BG)
    -- Give the loot section a little more breathing room than the meta block above it.
    lootPanel:SetPoint("TOPLEFT", metaScroll, "BOTTOMLEFT", -6, -8)
    lootPanel:SetPoint("BOTTOMRIGHT", -6, 10)

    local lootTitle = MakeSectionTitle(lootPanel, "Top loot (session)")
    lootTitle:SetPoint("TOPLEFT", 10, -8)

-- Loot view mode for the Top loot panel (click the header)
-- "top"     = normal top loot list (excluding deleted/ignored)
-- "deleted" = items you removed from the Top loot list
-- "ignored" = items excluded from session value totals
local lootView = "top"

function H._LootViewLabel(mode)
  if mode == "deleted" then return "Deleted loot (session)" end
  if mode == "ignored" then return "Ignored loot (session)" end
  return "Top loot (session)"
end

function H._SetLootView(mode)
  if mode ~= "top" and mode ~= "deleted" and mode ~= "ignored" then mode = "top" end
  lootView = mode
  if lootTitle then
    lootTitle:SetText(H._LootViewLabel(lootView))
  end
end

function H._GetHistoryLootSortMode()
  if WF.DB and WF.DB.GetSetting then
    local m = WF.DB:GetSetting("historyLootSortMode")
    if type(m) == "string" and m ~= "" then return m end
  end

  local db = (WF.DB and WF.DB.Get) and (WF.DB:Get() or {}) or {}
  db.settings = db.settings or {}
  local m = db.settings.historyLootSortMode
  if type(m) == "string" and m ~= "" then return m end
  return "total_desc"
end

function H._SetHistoryLootSortMode(mode)
  if type(mode) ~= "string" or mode == "" then return end
  if WF.DB and WF.DB.SetSetting then
    WF.DB:SetSetting("historyLootSortMode", mode)
  else
    local db = (WF.DB and WF.DB.Get) and (WF.DB:Get() or {}) or {}
    db.settings = db.settings or {}
    db.settings.historyLootSortMode = mode
  end
end

function H._ToggleHistoryLootSort(primary, secondary)
  if type(primary) ~= "string" or primary == "" then return end

  local cur = H._GetHistoryLootSortMode()
  if secondary and cur == primary then
    H._SetHistoryLootSortMode(secondary)
  else
    H._SetHistoryLootSortMode(primary)
  end

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

function H._ToggleLootView()
  if lootView == "top" then
    H._SetLootView("deleted")
  else
    H._SetLootView("top")
  end
  UI:RefreshHistoryLoot()
end

function H._OpenLootViewMenu()
  UI:OpenContextMenu({
    { text = "Top loot (session)", disabled = (lootView == "top"), fn = function() H._SetLootView("top"); UI:RefreshHistoryLoot() end },
    { text = "Deleted loot (session)", disabled = (lootView == "deleted"), fn = function() H._SetLootView("deleted"); UI:RefreshHistoryLoot() end },
    { text = "Ignored loot (session)", disabled = (lootView == "ignored"), fn = function() H._SetLootView("ignored"); UI:RefreshHistoryLoot() end },
  }, 230)
end

do
  local r0, g0, b0, a0 = lootTitle:GetTextColor()

  local headerBtn = CreateFrame("Button", nil, lootPanel)
  headerBtn:SetPoint("TOPLEFT", lootTitle, "TOPLEFT", -8, 8)
  headerBtn:SetPoint("BOTTOMRIGHT", lootTitle, "BOTTOMRIGHT", 8, -8)
  headerBtn:SetFrameLevel((lootPanel:GetFrameLevel() or 0) + 20)
  headerBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")

  headerBtn:SetScript("OnEnter", function()
    lootTitle:SetTextColor(C4(ACCENT, ACCENT))
    GameTooltip:SetOwner(headerBtn, "ANCHOR_RIGHT")
    GameTooltip:AddLine("Top loot view", ACCENT[1], ACCENT[2], ACCENT[3], true)
    GameTooltip:AddLine("Click: Top ↔ Deleted", 0.70, 0.70, 0.70)
    GameTooltip:AddLine("Shift + Click: Ignored", 0.70, 0.70, 0.70)
    GameTooltip:AddLine("Right-click: choose view", 0.70, 0.70, 0.70)
    GameTooltip:Show()
  end)

  headerBtn:SetScript("OnLeave", function()
    lootTitle:SetTextColor(r0, g0, b0, a0)
    GameTooltip:Hide()
  end)

  headerBtn:SetScript("OnClick", function(_, button)
    if button == "RightButton" then
      H._OpenLootViewMenu()
      return
    end

    if button == "LeftButton" then
      if IsShiftKeyDown and IsShiftKeyDown() then
        H._SetLootView("ignored")
        UI:RefreshHistoryLoot()
      else
        H._ToggleLootView()
      end
      return
    end
  end)

-- Ensure the label matches the initial mode.
  H._SetLootView("top")
end


    local lootCount = MakeMutedText(lootPanel, "")
    lootCount:SetPoint("TOPRIGHT", -10, -8)
    lootCount:SetJustifyH("RIGHT")
    lootCount:SetWidth(150)
    lootCount:SetWordWrap(false)
    if lootCount.SetMaxLines then lootCount:SetMaxLines(1) end


    local lootDiv = MakeDivider(lootPanel)
    lootDiv:SetPoint("TOPLEFT", 10, -30)
    lootDiv:SetPoint("TOPRIGHT", -10, -30)

    -- Column header (Item / Qty / Each / Total) for Top loot
    local lootHeader = CreateFrame("Frame", nil, lootPanel)
    lootHeader:SetPoint("TOPLEFT", 6, -34)
    lootHeader:SetPoint("TOPRIGHT", -18, -34)
    lootHeader:SetHeight(18)

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

    lootHeader.item  = lootHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    lootHeader.count = lootHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    lootHeader.each  = lootHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
    lootHeader.price = lootHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")

    lootHeader.item:SetText("Item")
    lootHeader.count:SetText("Count")
    lootHeader.each:SetText("Each")
    lootHeader.price:SetText("Total")

    do
      local rH, gH, bH, aH = C4(ACCENT, ACCENT)
      lootHeader.item:SetTextColor(rH, gH, bH, 0.95)
      lootHeader.count:SetTextColor(rH, gH, bH, 0.95)
      lootHeader.each:SetTextColor(rH, gH, bH, 0.95)
      lootHeader.price:SetTextColor(rH, gH, bH, 0.95)
    end

    lootHeader.item:SetJustifyH("LEFT")
    lootHeader.count:SetJustifyH("CENTER")
    lootHeader.each:SetJustifyH("RIGHT")
    lootHeader.price:SetJustifyH("RIGHT")

    function H._TightenLootNumberFont(fs)
      if not (fs and fs.GetFont and fs.SetFont) then return end
      local f, size, fl = fs:GetFont()
      if not f then return end
      local newSize = tonumber(size) or 11
      newSize = newSize - 1
      if newSize < 9 then newSize = 9 end
      fs:SetFont(f, newSize, fl or "OUTLINE")
    end

    function H._TightenResetHeaderFont(fs)
      if not (fs and fs.GetFont and fs.SetFont) then return end
      if fs._wfResetHeaderFontTightened then return end
      local f, size, fl = fs:GetFont()
      if not f then return end
      local newSize = tonumber(size) or 11
      newSize = newSize - 1
      if newSize < 9 then newSize = 9 end
      fs:SetFont(f, newSize, fl or "OUTLINE")
      fs._wfResetHeaderFontTightened = true
    end
    H._TightenLootNumberFont(lootHeader.count)
    H._TightenLootNumberFont(lootHeader.each)
    H._TightenLootNumberFont(lootHeader.price)
    lootHeader.item:SetWordWrap(false)
    lootHeader.count:SetWordWrap(false)
    lootHeader.each:SetWordWrap(false)
    lootHeader.price:SetWordWrap(false)
    if lootHeader.item.SetMaxLines then lootHeader.item:SetMaxLines(1) end
    if lootHeader.count.SetMaxLines then lootHeader.count:SetMaxLines(1) end
    if lootHeader.each.SetMaxLines then lootHeader.each:SetMaxLines(1) end
    if lootHeader.price.SetMaxLines then lootHeader.price:SetMaxLines(1) end

    -- Align to row layout (icon at 10px, 18px wide, 8px gap)
    lootHeader.price:SetPoint("RIGHT", lootHeader, "RIGHT", -4, 0)
    lootHeader.each:SetPoint("RIGHT", lootHeader.price, "LEFT", -2, 0)
    lootHeader.count:SetPoint("RIGHT", lootHeader.each, "LEFT", -1, 0)
    lootHeader.item:SetPoint("LEFT", lootHeader, "LEFT", 26, 0)
    lootHeader.item:SetPoint("RIGHT", lootHeader.count, "LEFT", -4, 0)

    -- Initialize widths (updated responsively in H.LayoutLootColumns)
    lootHeader.price:SetWidth(lootLayout.valueW or 64)
    lootHeader.each:SetWidth(lootLayout.eachW or 58)
    lootHeader.count:SetWidth(lootLayout.countW or 36)

    lootHeader.itemBtn = CreateFrame("Button", nil, lootHeader)
    lootHeader.countBtn = CreateFrame("Button", nil, lootHeader)
    lootHeader.eachBtn = CreateFrame("Button", nil, lootHeader)
    lootHeader.priceBtn = CreateFrame("Button", nil, lootHeader)

    H._MakeHeaderHL(lootHeader.itemBtn)
    H._MakeHeaderHL(lootHeader.countBtn)
    H._MakeHeaderHL(lootHeader.eachBtn)
    H._MakeHeaderHL(lootHeader.priceBtn)

    function H._BindLootHeaderButton(btn, title, hint, fn)
      if not btn then return end
      btn:RegisterForClicks("LeftButtonUp")
      btn:SetScript("OnEnter", function(self)
        H._HeaderHover(self, true)
      end)
      btn:SetScript("OnLeave", function(self)
        H._HeaderHover(self, false)
      end)
      btn:SetScript("OnClick", H._HeaderClickOnlyLeft(fn))
    end

    H._BindLootHeaderButton(lootHeader.itemBtn, "Item", "Left-click: sort by Item", function()
      H._ToggleHistoryLootSort("name_az", "name_za")
    end)

    H._BindLootHeaderButton(lootHeader.countBtn, "Count", "Left-click: sort by Count", function()
      H._ToggleHistoryLootSort("count_desc", "count_asc")
    end)

    H._BindLootHeaderButton(lootHeader.eachBtn, "Each", "Left-click: sort by Each value", function()
      H._ToggleHistoryLootSort("each_desc", "each_asc")
    end)

    H._BindLootHeaderButton(lootHeader.priceBtn, "Total", "Left-click: sort by Total value", function()
      H._ToggleHistoryLootSort("total_desc", "total_asc")
    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", 2, -54)
    lootScroll:SetPoint("BOTTOMRIGHT", -18, 8)

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

    -- Empty state (Top loot list)
    local emptyLoot = lootScroll:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    emptyLoot:SetPoint("CENTER", lootScroll, "CENTER", 0, 0)
    emptyLoot:SetJustifyH("CENTER")
    emptyLoot:SetTextColor(C4(MUTED, MUTED))
    emptyLoot:SetText("Select a session to view loot.")
    emptyLoot:Hide()

    function H._UpdateEmptyLootWidth()
      local w = lootScroll:GetWidth() or 0
      if w > 60 then emptyLoot:SetWidth(w - 40) end
    end
    H._UpdateEmptyLootWidth()
    lootScroll:HookScript("OnSizeChanged", H._UpdateEmptyLootWidth)
    UI._historyEmptyLoot = emptyLoot


    -- Throttle expensive tooltip item loads while scrolling (prevents AddOn memory spikes)
    lootScroll._wfLastScroll = 0
    if lootScroll.HookScript then
      lootScroll:HookScript("OnMouseWheel", function() lootScroll._wfLastScroll = GetTime() end)
    end
    local _sb = lootScroll.ScrollBar
    if (not _sb) and lootScroll.GetName then
      local _n = lootScroll:GetName()
      if _n then _sb = _G and _G[_n .. "ScrollBar"] end
    end
    if _sb and _sb.HookScript then
      _sb:HookScript("OnValueChanged", function() lootScroll._wfLastScroll = GetTime() end)
    end
    lootScroll:SetScript("OnVerticalScroll", function(selfScroll, offset)
      if UIPanelScrollFrame_OnVerticalScroll then
        UIPanelScrollFrame_OnVerticalScroll(selfScroll, offset, lootRowH, function()
          if UI and UI._RenderHistoryLootVisible then UI:_RenderHistoryLootVisible() end
        end)
      else
        selfScroll:SetVerticalScroll(offset)
        if UI and UI._RenderHistoryLootVisible then UI:_RenderHistoryLootVisible() end
      end
    end)
    if lootScroll.HookScript then
      lootScroll:HookScript("OnSizeChanged", function()
        if UI and UI._RenderHistoryLootVisible then UI:_RenderHistoryLootVisible() end
      end)
    end

    local rows, lootRows = {}, {}
    local rowH = 36
    local lootRowH = 24
    local _historyLootScratch = {}
    local _historySessionScratch = {}
    local _historySessionList = {}
    local _historyMetaLines = {}
    local _historyMergedZones = {}

    function H._HistoryHex(color, fallback)
      local r, g, b = C4(color, fallback)

      function H._Byte(v)
        v = tonumber(v) or 1
        if v < 0 then v = 0 end
        if v > 1 then v = 1 end
        return math.floor((v * 255) + 0.5)
      end

      return string.format("%02x%02x%02x", H._Byte(r), H._Byte(g), H._Byte(b))
    end

    local HISTORY_META_HEX_LABEL  = H._HistoryHex(ACCENT, TEXT)
    local HISTORY_META_HEX_VALUE  = H._HistoryHex(TEXT, TEXT)
    local HISTORY_META_HEX_ACCENT = H._HistoryHex(ACCENT, TEXT)

    function H._HistoryMetaColor(hex, value)
      return string.format("|cff%s%s|r", tostring(hex or HISTORY_META_HEX_VALUE), tostring(value or ""))
    end

    function H._HistoryMetaLine(label, value, valueHex)
      return H._HistoryMetaColor(HISTORY_META_HEX_LABEL, tostring(label or "") .. ":") .. " " ..
             H._HistoryMetaColor(valueHex or HISTORY_META_HEX_VALUE, value or "—")
    end

    -- -----------------------------------------------------
    -- Session row helpers (premium formatting)
    -- -----------------------------------------------------
    function H._MinLootValue()
      if WF.DB and WF.DB.GetSetting then
        local v =
          tonumber(WF.DB:GetSetting("lootMinValue")) or
          tonumber(WF.DB:GetSetting("lootMinGold")) or
          tonumber(WF.DB:GetSetting("minItemValue")) or
          tonumber(WF.DB:GetSetting("lootMinItemValue"))
        if v and v > 0 then return v end
      end
      return 1
    end



function H._SessIgnore(sess, bucket)
  if type(sess) ~= "table" then return nil end
  sess._wfIgnore = sess._wfIgnore or {}
  sess._wfIgnore[bucket] = sess._wfIgnore[bucket] or {}
  return sess._wfIgnore[bucket]
end

function H._IsHidden(sess, itemID)
  if type(sess) ~= "table" or itemID == nil then return false end
  local ig = sess._wfIgnore
  local t = ig and ig.hideTop
  return type(t) == "table" and t[itemID] or false
end

function H._IsExcluded(sess, itemID)
  if type(sess) ~= "table" or itemID == nil then return false end
  local ig = sess._wfIgnore
  local t = ig and ig.excludeValue
  return type(t) == "table" and t[itemID] or false
end

function H._IsIgnored(sess, itemID)
  return H._IsHidden(sess, itemID) or H._IsExcluded(sess, itemID)
end

function H._IsEmpty(t)
  if type(t) ~= "table" then return true end
  for _ in pairs(t) do return false end
  return true
end

function H._RecalcSessionTotalValue(sess)
  local items = sess and sess.items
  if type(items) ~= "table" then return 0 end

  local ig = type(sess) == "table" and sess._wfIgnore
  local ex = ig and ig.excludeValue

  local total = 0
  for itemID, e in pairs(items) do
    if not (type(ex) == "table" and ex[itemID]) then
      total = total + (tonumber(e and e.value) or 0)
    end
  end
  return total
end

function H._ApplySessionTotalOverride(sess)
  if type(sess) ~= "table" then return end
  if sess._wfOrigTotalValue == nil then
    sess._wfOrigTotalValue = tonumber(sess.totalValue or sess.total or sess.value) or 0
  end
  local newTotal = H._RecalcSessionTotalValue(sess)
  sess.totalValue = newTotal
  sess.total = newTotal
  sess.value = newTotal
end

function H._MaybeClearTotalOverride(sess)
  if type(sess) ~= "table" then return end
  local ig = sess._wfIgnore
  local ex = ig and ig.excludeValue
  if type(ex) == "table" and (not H._IsEmpty(ex)) then
    H._ApplySessionTotalOverride(sess)
    return
  end

  if sess._wfOrigTotalValue ~= nil then
    local v = tonumber(sess._wfOrigTotalValue) or 0
    sess.totalValue = v
    sess.total = v
    sess.value = v
    sess._wfOrigTotalValue = nil
  end
  if type(ig) == "table" then ig.excludeValue = nil end
end

    
function H._CountValuableItems(sess)
  local items = sess and sess.items
  if type(items) ~= "table" then return 0, 0 end

  local minV = H._MinLootValue()
  local unique, total = 0, 0

  for itemID, e in pairs(items) do
    if not H._IsIgnored(sess, itemID) then
      local v = tonumber(e and e.value) or 0
      if v >= minV then
        unique = unique + 1
        local c = tonumber(e and (e.count or e.qty or e.stack)) or 1
        if c < 1 then c = 1 end
        total = total + c
      end
    end
  end

  return unique, total
end

function H._CountAllItems(sess)
  local items = sess and sess.items
  if type(items) ~= "table" then return 0, 0 end

  local unique, total = 0, 0
  for _, e in pairs(items) do
    unique = unique + 1
    if type(e) == "number" then
      local n = math.floor((e or 0) + 0.00001)
      if n < 1 then n = 1 end
      total = total + n
    else
      local n = tonumber(e and (e.count or e.qty or e.stack or e.num)) or 1
      n = math.floor(n + 0.00001)
      if n < 1 then n = 1 end
      total = total + n
    end
  end

  return unique, total
end


function H._CountBucketItems(sess, bucket)
  if type(sess) ~= "table" then return 0 end
  local items = sess.items
  if type(items) ~= "table" then return 0 end

  local ig = sess._wfIgnore
  local t = ig and ig[bucket]
  if type(t) ~= "table" then return 0 end

  local total = 0
  for itemID in pairs(t) do
    local e = items[itemID]
    local c = tonumber(e and (e.count or e.qty or e.stack)) or 1
    if c < 1 then c = 1 end
    total = total + c
  end
  return total
end

function H._CopyResetEntryForMerge(reset)
  if type(reset) ~= "table" then return nil end

  local out = {
    resetNumber = tonumber(reset.resetNumber) or 0,
    reason = reset.reason,
    startedAt = reset.startedAt,
    endedAt = reset.endedAt,
    elapsed = reset.elapsed,
    kills = reset.kills,
    moneyGained = reset.moneyGained,
    itemValue = reset.itemValue,
    totalValue = reset.totalValue,
    itemTypes = reset.itemTypes,
    itemCount = reset.itemCount,
    items = {},
  }

  if type(reset.items) == "table" then
    for itemID, e in pairs(reset.items) do
      if type(e) == "table" then
        out.items[itemID] = H._CopyEntryShallow(e)
      else
        out.items[itemID] = e
      end
    end
  end

  return out
end

function H._MergeResetBreakdowns(targetSess, sourceSess)
  if type(targetSess) ~= "table" or type(sourceSess) ~= "table" then return end

  local combined = {}
  local function AddFrom(sess)
    local resets = sess and sess.resets
    if type(resets) ~= "table" then return end
    for i = 1, #resets do
      local copy = H._CopyResetEntryForMerge(resets[i])
      if copy then combined[#combined + 1] = copy end
    end
  end

  AddFrom(targetSess)
  AddFrom(sourceSess)

  if #combined == 0 then
    targetSess.resets = nil
    return
  end

  table.sort(combined, function(a, b)
    local ta = tonumber(a.startedAt or a.endedAt) or 0
    local tb = tonumber(b.startedAt or b.endedAt) or 0
    if ta ~= tb then return ta < tb end
    return (tonumber(a.resetNumber) or 0) < (tonumber(b.resetNumber) or 0)
  end)

  for i = 1, #combined do
    combined[i].resetNumber = i
  end

  targetSess.resets = combined
end


    function H._FormatDuration(sec)
      sec = tonumber(sec) or 0
      if sec < 0 then sec = 0 end
      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 %dm %ds", h, m, s) end
      if m > 0 then return string.format("%dm %ds", m, s) end
      return string.format("%ds", s)
    end

    function H._ResetBreakdownCount(sess)
      local resets = sess and sess.resets
      if type(resets) ~= "table" then return 0 end
      local n = 0
      for i = 1, #resets do
        if type(resets[i]) == "table" then n = n + 1 end
      end
      return n
    end

    function H._GroupTopLootByResetEnabled()
      return (WF and WF.DB and WF.DB.GetSetting and WF.DB:GetSetting("groupTopLootByReset")) and true or false
    end

    function H._ResetGPH(reset)
      local elapsed = tonumber(reset and reset.elapsed) or 0
      local total = tonumber(reset and reset.totalValue) or 0
      if elapsed <= 0 then return 0 end
      return (total / elapsed) * 3600
    end

    function H._ResetLIV(reset)
      -- In What Farm's History UI, LIV means Looted Item Value, not an hourly
      -- rate. The reset header already has GPH for the per-hour number, so keep
      -- LIV as the actual item value earned inside this reset.
      local itemValue =
          tonumber(reset and reset.itemValue)
          or tonumber(reset and reset.itemsValue)
          or tonumber(reset and reset.lootedItemValue)

      if itemValue == nil then
        local total = tonumber(reset and reset.totalValue) or 0
        local raw = tonumber(reset and reset.moneyGained) or 0
        itemValue = total - raw
      end

      if itemValue < 0 then itemValue = 0 end
      return itemValue
    end

    function H._CommaNumber(n)
      n = math.floor(tonumber(n) or 0)
      local neg = n < 0
      if neg then n = -n end
      local s = tostring(n)
      while true do
        local ns, k = s:gsub("^(%d+)(%d%d%d)", "%1,%2")
        s = ns
        if k == 0 then break end
      end
      if neg then s = "-" .. s end
      return s
    end

    function H._FormatMoneyCompact(copper)
      copper = math.floor((tonumber(copper) or 0) + 0.5)
      local neg = copper < 0
      if neg then copper = -copper end
      local g = math.floor(copper / 10000)
      local s = math.floor((copper % 10000) / 100)
      local c = copper % 100
      local out
      if g > 0 then
        out = H._CommaNumber(g) .. "g"
      elseif s > 0 then
        out = tostring(s) .. "s"
      else
        out = tostring(c) .. "c"
      end
      if neg then out = "-" .. out end
      return out
    end

    function H._FormatMoneyShort(copper)
      copper = math.floor((tonumber(copper) or 0) + 0.5)
      local neg = copper < 0
      if neg then copper = -copper end

      local gold = copper / 10000
      local out
      if gold >= 1000000 then
        out = string.format("%.2fm", gold / 1000000)
      elseif gold >= 100000 then
        out = string.format("%.0fk", gold / 1000)
      elseif gold >= 10000 then
        out = string.format("%.1fk", gold / 1000)
      elseif gold >= 1000 then
        out = string.format("%.2fk", gold / 1000)
      else
        out = H._FormatMoneyCompact(copper)
      end

      out = tostring(out):gsub("%.0([km])$", "%1"):gsub("(%.[1-9])0([km])$", "%1%2")
      if neg then out = "-" .. out end
      return out
    end

    function H._ResetTopLootMetric(label, value)
      return H._HistoryMetaColor(HISTORY_META_HEX_LABEL, tostring(label or "") .. ":") .. " " ..
             H._HistoryMetaColor(HISTORY_META_HEX_VALUE, tostring(value or "—"))
    end

    function H._FormatResetTopLootHeaderMeta(reset, full)
      local elapsed = H._FormatDuration(tonumber(reset and reset.elapsed) or 0)
      local gphValue = H._ResetGPH(reset)
      local livValue = H._ResetLIV(reset)
      local gph = full and H._FormatMoneyCompact(gphValue) or H._FormatMoneyShort(gphValue)
      local liv = full and H._FormatMoneyCompact(livValue) or H._FormatMoneyShort(livValue)
      return H._ResetTopLootMetric("Time", elapsed) .. "  " ..
             H._ResetTopLootMetric("GPH", gph) .. "  " ..
             H._ResetTopLootMetric("LIV", liv)
    end

    function H._ShowResetHeaderTooltip(row)
      if not row or row._wfKind ~= "resetHeader" then return end
      local reset = row._resetData
      if type(reset) ~= "table" then return end

      GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
      GameTooltip:SetText("Reset " .. tostring(row._resetNumber or "?"), 1, 1, 1, 1, true)
      GameTooltip:AddLine("Time: " .. H._FormatDuration(tonumber(reset.elapsed) or 0), 0.75, 0.75, 0.75, 1, true)
      GameTooltip:AddLine("GPH: " .. H._FormatMoneyCompact(H._ResetGPH(reset)), 0.75, 0.75, 0.75, 1, true)
      GameTooltip:AddLine("LIV: " .. H._FormatMoneyCompact(H._ResetLIV(reset)), 0.75, 0.75, 0.75, 1, true)

      local total = tonumber(reset.totalValue) or 0
      local raw = tonumber(reset.moneyGained) or 0
      if total > 0 or raw > 0 then
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Total value: " .. H._FormatMoneyCompact(total), 0.70, 0.70, 0.70, 1, true)
        if raw > 0 then
          GameTooltip:AddLine("Raw gold: " .. H._FormatMoneyCompact(raw), 0.70, 0.70, 0.70, 1, true)
        end
      end

      GameTooltip:Show()
    end


    function H._GetResetCollapseMap(sess)
      if type(sess) ~= "table" then return nil end
      if type(sess._wfResetCollapsed) ~= "table" then
        sess._wfResetCollapsed = {}
      end
      return sess._wfResetCollapsed
    end

    function H._IsResetCollapsed(sess, resetNo)
      local t = H._GetResetCollapseMap(sess)
      if not t then return false end
      resetNo = tonumber(resetNo) or resetNo
      return t[resetNo] and true or false
    end

    function H._SetResetCollapsed(sess, resetNo, collapsed)
      local t = H._GetResetCollapseMap(sess)
      if not t then return end
      resetNo = tonumber(resetNo) or resetNo
      if collapsed then
        t[resetNo] = true
      else
        t[resetNo] = nil
      end
    end

    function H._ToggleResetCollapsed(sess, resetNo)
      H._SetResetCollapsed(sess, resetNo, not H._IsResetCollapsed(sess, resetNo))
    end

    function H._FormatResetLine(reset, index)
      local n = tonumber(reset and reset.resetNumber) or index or 1
      local elapsed = H._FormatDuration(tonumber(reset and reset.elapsed) or 0)
      local total = tonumber(reset and reset.totalValue) or 0
      local raw = tonumber(reset and reset.moneyGained) or 0
      local kills = tonumber(reset and reset.kills) or 0
      local itemTypes = tonumber(reset and reset.itemTypes) or 0
      local itemCount = tonumber(reset and reset.itemCount) or 0
      local gph = H._ResetGPH(reset)
      local gphStr = tostring(Money(Util:Round(gph)) or "0")
      gphStr = gphStr:gsub("%s*/hr", ""):gsub("/hr", "")

      local itemText
      if itemTypes > 0 then
        if itemCount > itemTypes then
          itemText = tostring(itemTypes) .. " types / " .. tostring(itemCount) .. " items"
        else
          itemText = tostring(itemTypes) .. " items"
        end
      else
        itemText = "0 items"
      end

      local parts = {
        "Reset " .. tostring(n),
        elapsed,
        Money(total),
        gphStr .. " GPH",
        itemText,
      }
      if raw > 0 then parts[#parts + 1] = Money(raw) .. " raw" end
      if kills > 0 then parts[#parts + 1] = tostring(kills) .. " kills" end
      return table.concat(parts, " • ")
    end



    function H._SortHistoryLootListByMode(list, mode)
      table.sort(list, function(a, b)
        local aName = tostring(a and a.name or "")
        local bName = tostring(b and b.name or "")
        local aCount = tonumber(a and a.count) or 0
        local bCount = tonumber(b and b.count) or 0
        local aEach = tonumber(a and a.each) or 0
        local bEach = tonumber(b and b.each) or 0
        local aTotal = tonumber(a and a.total) or tonumber(a and a.value) or 0
        local bTotal = tonumber(b and b.total) or tonumber(b and b.value) or 0

        if mode == "name_az" then
          if aName ~= bName then return aName < bName end
        elseif mode == "name_za" then
          if aName ~= bName then return aName > bName end
        elseif mode == "count_desc" then
          if aCount ~= bCount then return aCount > bCount end
        elseif mode == "count_asc" then
          if aCount ~= bCount then return aCount < bCount end
        elseif mode == "each_desc" then
          if aEach ~= bEach then return aEach > bEach end
        elseif mode == "each_asc" then
          if aEach ~= bEach then return aEach < bEach end
        elseif mode == "total_asc" then
          if aTotal ~= bTotal then return aTotal < bTotal end
        else
          if aTotal ~= bTotal then return aTotal > bTotal end
        end

        if aTotal ~= bTotal then return aTotal > bTotal end
        if aCount ~= bCount then return aCount > bCount end
        if aName ~= bName then return aName < bName end

        return tostring(a and a.itemID or "") < tostring(b and b.itemID or "")
      end)
    end

    function H._BuildResetTopLoot(reset, limit)
      local out = {}
      local items = reset and reset.items
      if type(items) ~= "table" then return out end

      for itemID, e in pairs(items) do
        if type(e) == "table" then
          local count = tonumber(e.count) or 0
          local total = tonumber(e.value) or 0
          if count > 0 then
            local each = tonumber(e.unitValue) or 0
            if each <= 0 and count > 0 then each = total / count end

            local display = e.link or e.name
            if not display or display == "" then
              display = H._LinkToName(e.link)
            end
            if not display or display == "" then display = "Item " .. tostring(itemID or "") end

            out[#out + 1] = {
              itemID = itemID,
              entry = e,
              display = display,
              name = string.lower(tostring(e.name or H._LinkToName(e.link) or display or "")),
              count = count,
              total = total,
              each = each,
            }
          end
        end
      end

      H._SortHistoryLootListByMode(out, H._GetHistoryLootSortMode and H._GetHistoryLootSortMode() or "total_desc")

      limit = tonumber(limit) or 5
      if limit > 0 then
        for i = #out, limit + 1, -1 do
          out[i] = nil
        end
      end

      return out
    end

    function H._FormatResetLootLine(rec)
      if type(rec) ~= "table" then return nil end
      local count = tonumber(rec.count) or 0
      local total = tonumber(rec.total) or 0
      local display = tostring(rec.display or "Item")
      local countText = "x" .. tostring(Util:Round(count) or count)
      local totalText = Money(total)
      return "  " .. display .. "  |cff9ca3af" .. countText .. " • " .. tostring(totalText or "0") .. "|r"
    end




    -- -----------------------------------------------------
    -- Merge sessions (History)
    -- -----------------------------------------------------
    function H._Say(msg)
      if UI and UI.Toast then
        UI:Toast(tostring(msg))
      else
        print("WhatFarm: " .. tostring(msg))
      end
    end

    function H._ModeKey(sess)
      local m = H._Trim(sess and sess.mode or "")
      if m == "" then m = "custom" end
      return string.lower(m)
    end

    function H._MergeMenuText(label, value, valueHex)
      label = tostring(label or "")
      value = tostring(value or "")
      valueHex = tostring(valueHex or "ffffff")
      return "|cff7f7f7f" .. label .. ":|r |cff" .. valueHex .. value .. "|r"
    end

    function H._SessionLabel(sess, showMode)
      if not sess then return "Session" end
      local dn = (UI and UI.GetSessionDisplayName) and UI:GetSessionDisplayName(sess) or nil
      local name = dn or sess._wfCustomFarmName or sess.farmName or sess.zone or "Session"
      local ts = FormatTime(sess.startedAt or sess.endedAt or sess.stoppedAt)
      local dur = H._FormatDuration(tonumber(sess.elapsed or 0))

      local metric = H._GetDisplayMetric()
      local valueText = "0"
      if metric == "liv" then
        local _, _, livTotal = H._SessTotals(sess)
        valueText = tostring(Money(livTotal) or "0") .. " LIV"
      else
        local gph = SessionGPH and SessionGPH(sess) or 0
        valueText = tostring(Money(Util:Round(gph)) or "0")
        valueText = valueText:gsub("%s*/hr", ""):gsub("/hr", "") .. " GPH"
      end

      local modeTxt = ""
      if showMode ~= false then
        local mode = H._WF_History_ModeName(sess.mode or "Default")
        if mode ~= "" then
          modeTxt = " |cff7f7f7f•|r |cff33aaff" .. tostring(mode) .. "|r"
        end
      end

      return "|cffffffff" .. tostring(name) .. "|r" ..
        " |cff7f7f7f• " .. tostring(ts) ..
        " • " .. tostring(dur) ..
        " •|r |cffffffff" .. tostring(valueText) .. "|r" ..
        modeTxt
    end

    function H._CopyEntryShallow(e)
      if type(e) ~= "table" then return {} end
      local c = {}
      for k, v in pairs(e) do
        if type(v) ~= "table" then
          c[k] = v
        end
      end
      if type(e._wfUnverifiedMeta) == "table" then
        local m = {}
        for k, v in pairs(e._wfUnverifiedMeta) do m[k] = v end
        c._wfUnverifiedMeta = m
      end
      return c
    end

    function H._MergeItemEntry(tgt, src)
      if type(tgt) ~= "table" or type(src) ~= "table" then return end

      local cT = tonumber(tgt.count) or 0
      local vT = tonumber(tgt.value) or 0
      local cS = tonumber(src.count) or 0
      local vS = tonumber(src.value) or 0

      local newCount = cT + cS
      local newValue = vT + vS

      tgt.count = newCount
      tgt.value = newValue
      if newCount > 0 then
        tgt.unitValue = newValue / newCount
      end

      if not tgt.link and src.link then tgt.link = src.link end
      if not tgt.name and src.name then tgt.name = src.name end
      if not tgt.icon and src.icon then tgt.icon = src.icon end
      if not tgt.lastSource and src.lastSource then tgt.lastSource = src.lastSource end
      if tgt.isReagent == nil and src.isReagent ~= nil then tgt.isReagent = src.isReagent end

      -- Unverified meta: if either side is verified, clear the unverified flag.
      local unvT = tgt._wfUnverified and true or false
      local unvS = src._wfUnverified and true or false
      if (not unvT) or (not unvS) then
        tgt._wfUnverified = false
        tgt._wfUnverifiedMeta = nil
      else
        tgt._wfUnverified = true
        local mT = tgt._wfUnverifiedMeta
        local mS = src._wfUnverifiedMeta
        if type(mT) ~= "table" then mT = {} tgt._wfUnverifiedMeta = mT end
        if type(mS) == "table" then
          local sr = tonumber(mT.saleRate) or 0
          local sr2 = tonumber(mS.saleRate) or 0
          if sr2 > sr then mT.saleRate = sr2 end
          local mb = tonumber(mT.minBuyout) or 0
          local mb2 = tonumber(mS.minBuyout) or 0
          if mb2 > mb then mT.minBuyout = mb2 end
        end
      end
    end

    function H._UnionIgnoreTables(tgtSess, srcSess)
      if type(tgtSess) ~= "table" or type(srcSess) ~= "table" then return end
      if type(srcSess._wfIgnore) ~= "table" then return end
      tgtSess._wfIgnore = tgtSess._wfIgnore or {}

      for bucket, t in pairs(srcSess._wfIgnore) do
        if type(t) == "table" then
          local out = tgtSess._wfIgnore[bucket]
          if type(out) ~= "table" then out = {} tgtSess._wfIgnore[bucket] = out end
          for k, v in pairs(t) do
            if v then out[k] = v end
          end
        end
      end
    end

    function H._RecalcTotalValue(sess)
      if type(sess) ~= "table" then return 0 end
      local total = 0
      local items = sess.items
      if type(items) == "table" then
        for itemID, e in pairs(items) do
          if not H._IsExcluded(sess, itemID) then
            total = total + (tonumber(e and e.value) or 0)
          end
        end
      end
      total = total + (tonumber(sess.moneyGained) or 0)
      sess.totalValue = total
      sess.total = total
      sess.value = total
      return total
    end

    function H._MarkMerged(tgt, src)
      if type(tgt) ~= "table" or type(src) ~= "table" then return end
      tgt._wfMergedCount = tonumber(tgt._wfMergedCount) or 1
      local srcCount = tonumber(src._wfMergedCount) or 1
      tgt._wfMergedCount = tgt._wfMergedCount + srcCount

      tgt._wfMergedZones = tgt._wfMergedZones or {}
      function H._AddZone(z)
        z = H._Trim(z or "")
        if z ~= "" then tgt._wfMergedZones[z] = true end
      end

      H._AddZone(tgt.zone)
      H._AddZone(src.zone)
      if type(src._wfMergedZones) == "table" then
        for z in pairs(src._wfMergedZones) do H._AddZone(z) end
      end

      tgt._wfMergedAt = time()
    end

    function H._RemoveSessionFromHistory(db, sess)
      if not db or not sess then return end
      db.history = db.history or {}
      db.history.sessions = db.history.sessions or {}
      local sessions = db.history.sessions
      for i = #sessions, 1, -1 do
        if sessions[i] == sess then
          table.remove(sessions, i)
          return true
        end
      end
      return false
    end

    function H._MergeHistorySessions(targetSess, sourceSess)
      if not targetSess or not sourceSess or targetSess == sourceSess then return end

      -- merge core stats
      targetSess.moneyGained = (tonumber(targetSess.moneyGained) or 0) + (tonumber(sourceSess.moneyGained) or 0)
      targetSess.kills = (tonumber(targetSess.kills) or 0) + (tonumber(sourceSess.kills) or 0)
      targetSess.elapsed = (tonumber(targetSess.elapsed) or 0) + (tonumber(sourceSess.elapsed) or 0)

      local sA = tonumber(targetSess.startedAt) or 0
      local sB = tonumber(sourceSess.startedAt) or 0
      if sA == 0 then sA = sB end
      if sB ~= 0 and (sA == 0 or sB < sA) then targetSess.startedAt = sB end

      local eA = tonumber(targetSess.endedAt or targetSess.stoppedAt) or 0
      local eB = tonumber(sourceSess.endedAt or sourceSess.stoppedAt) or 0
      if eA == 0 then eA = eB end
      if eB ~= 0 and (eA == 0 or eB > eA) then
        if targetSess.endedAt ~= nil then targetSess.endedAt = eB else targetSess.stoppedAt = eB end
      end

      -- merge items
      targetSess.items = targetSess.items or {}
      local items = sourceSess.items
      if type(items) == "table" then
        for itemID, e in pairs(items) do
          if targetSess.items[itemID] == nil then
            targetSess.items[itemID] = H._CopyEntryShallow(e)
          else
            H._MergeItemEntry(targetSess.items[itemID], e)
          end
        end
      end

      -- union ignore flags (deleted/ignored)
      H._UnionIgnoreTables(targetSess, sourceSess)

      -- preserve per-reset child rows when merging sessions
      H._MergeResetBreakdowns(targetSess, sourceSess)

      H._MarkMerged(targetSess, sourceSess)

      local db = WF.DB:Get() or {}
      H._RemoveSessionFromHistory(db, sourceSess)

      -- recompute totals + aggregates
      H._RecalcTotalValue(targetSess)
      RebuildHistoryAggregates(db)

      UI._pendingSelectSession = targetSess
      selected = targetSess

      UI:RefreshOverview()
      UI:RefreshHistory()

      H._Say("Merged sessions.")
    end

    function H._CandidatesForMerge(targetSess, requireZone)
      local db = WF.DB:Get()
      local sessions = (db.history and db.history.sessions) or {}
      local key = H._ModeKey(targetSess)
      local zKey = requireZone and H._Trim(targetSess.zone or "") or nil
      local out = {}

      for i = 1, #sessions do
        local s = sessions[i]
        if s and s ~= targetSess then
          if H._ModeKey(s) == key then
            if (not requireZone) or (H._Trim(s.zone or "") == zKey) then
              out[#out + 1] = s
            end
          end
        end
      end

      table.sort(out, function(a, b)
        local ta = tonumber(a.endedAt or a.stoppedAt or a.startedAt) or 0
        local tb = tonumber(b.endedAt or b.stoppedAt or b.startedAt) or 0
        return ta > tb
      end)

      return out
    end

    function H._OpenMergeMenu(targetSess, requireZone, preserveAnchor)
      if not targetSess then return end

      requireZone = requireZone and true or false

      local cands = H._CandidatesForMerge(targetSess, requireZone)
      local mode = H._WF_History_ModeName(targetSess.mode or "Default")
      local zoneName = H._Trim(targetSess.zone or "")

      local menu = {}
      menu[#menu + 1] = { text = "|cffffffffMerge another session into this|r", disabled = true }

      if mode and mode ~= "" then
        menu[#menu + 1] = { text = H._MergeMenuText("Mode", tostring(mode), "33aaff"), disabled = true }
      end

      if requireZone then
        if zoneName ~= "" then
          menu[#menu + 1] = { text = H._MergeMenuText("Showing", "same zone only", "33aaff"), disabled = true }
          menu[#menu + 1] = { text = H._MergeMenuText("Zone", tostring(zoneName), "ffffff"), disabled = true }
        else
          menu[#menu + 1] = { text = H._MergeMenuText("Showing", "same zone only", "33aaff"), disabled = true }
          menu[#menu + 1] = { text = H._MergeMenuText("Zone", "no zone", "ffffff"), disabled = true }
        end
      else
        menu[#menu + 1] = { text = H._MergeMenuText("Showing", "all zones", "33aaff"), disabled = true }
        menu[#menu + 1] = { text = "|cff7f7f7fHold Shift for same zone only|r", disabled = true }
      end

      menu[#menu + 1] = { sep = true }

      if #cands == 0 then
        if requireZone then
          menu[#menu + 1] = { text = "No other sessions found in this zone for this mode.", disabled = true }
        else
          menu[#menu + 1] = { text = "No other sessions found for this mode.", disabled = true }
        end
      else
        for i = 1, #cands do
          local s = cands[i]
          menu[#menu + 1] = {
            text = H._SessionLabel(s, false),
            fn = function()
              local sourceLabel = tostring(H._SessionLabel(s, false))
              local targetLabel = tostring(H._SessionLabel(targetSess, false))
              UI:Confirm(
                "Merge sessions?",
                "Source:\n  " .. sourceLabel .. "\n\nTarget:\n  " .. targetLabel .. "\n\nThis removes the source session from History.",
                function()
                  H._MergeHistorySessions(targetSess, s)
                end,
                "Merge"
              )
            end
          }
        end
      end

      local anchorX, anchorY
      if preserveAnchor and UI and UI._ctx and UI._ctx._wfAnchorX and UI._ctx._wfAnchorY then
        anchorX, anchorY = UI._ctx._wfAnchorX, UI._ctx._wfAnchorY
      end

      UI:OpenContextMenu(menu, 460, anchorX, anchorY)

      if UI and UI.SetContextMenuOnUpdate and UI._ctx then
        local ctx = UI._ctx
        ctx._wfMergeMenuState = {
          targetSess = targetSess,
          requireZone = requireZone,
        }
        ctx._wfMergeMenuTicker = 0

        UI:SetContextMenuOnUpdate(function(self, elapsed)
          local state = self._wfMergeMenuState
          if not state or not self:IsShown() then return end

          self._wfMergeMenuTicker = (self._wfMergeMenuTicker or 0) + (elapsed or 0)
          if self._wfMergeMenuTicker < 0.05 then return end
          self._wfMergeMenuTicker = 0

          local nowRequireZone = IsShiftKeyDown() and true or false
          if nowRequireZone ~= state.requireZone then
            state.requireZone = nowRequireZone
            H._OpenMergeMenu(state.targetSess, nowRequireZone, true)
          end
        end)
      end
    end
    function H.EnsureRow(i)
      local r = rows[i]
      if r then return r end

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


      r:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
      do
        local hl = r:GetHighlightTexture()
        if hl then
          hl:ClearAllPoints()
          hl:SetPoint("TOPLEFT", r, "TOPLEFT", 6, 0)
          hl:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", -6, 0)
          if hl.SetTexCoord then hl:SetTexCoord(0.02, 0.98, 0, 1) end
          if hl.SetDesaturation then
            hl:SetDesaturation(1)
          elseif hl.SetDesaturated then
            hl:SetDesaturated(true)
          end
          local ar, ag, ab
          if C4 then ar, ag, ab = C4(ACCENT, ACCENT) end
          if not ar then ar, ag, ab = 0.25, 0.75, 1.00 end
          hl:SetVertexColor(ar, ag, ab, 0.22)
        end
      end

      r.bg = r:CreateTexture(nil, "BACKGROUND")
      r.bg:SetAllPoints(r)
      r.bg:SetTexture("Interface/Buttons/WHITE8x8")
      do
        local ar, ag, ab
        if C4 then
          ar, ag, ab = C4(ACCENT, ACCENT)
        end
        if not ar then ar, ag, ab = 0.25, 0.75, 1.00 end
        r.bg:SetVertexColor(ar, ag, ab, 1)
      end
      r.bg:SetAlpha(0)

      r.accent = r:CreateTexture(nil, "ARTWORK")
      r.accent:SetTexture("Interface/Buttons/WHITE8x8")
      do
        local ar, ag, ab, aa = C4(ACCENT, ACCENT)
        r.accent:SetVertexColor(ar, ag, ab, 0.85)
      end
      r.accent:SetPoint("LEFT", 2, 0)
      r.accent:SetPoint("TOP", 0, -3)
      r.accent:SetPoint("BOTTOM", 0, 3)
      r.accent:SetWidth(3)
      r.accent:Hide()

      -- Columns
      r.farm  = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      r.farm:SetTextColor(C4(TEXT, TEXT))
      r.farm:SetJustifyH("LEFT")
      r.farm:SetWordWrap(false)
      if r.farm.SetMaxLines then r.farm:SetMaxLines(1) end

      r.val   = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      r.val:SetTextColor(C4(TEXT, TEXT))
      r.val:SetJustifyH("RIGHT")
      r.val:SetWordWrap(false)
      if r.val.SetMaxLines then r.val:SetMaxLines(1) end

      r.mode  = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.mode:SetTextColor(C4(MUTED, MUTED))
      r.mode:SetJustifyH("CENTER")
      r.mode:SetWordWrap(false)
      if r.mode.SetMaxLines then r.mode:SetMaxLines(1) end

      r.dur   = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.dur:SetTextColor(C4(MUTED, MUTED))
      r.dur:SetJustifyH("CENTER")
      r.dur:SetWordWrap(false)
      if r.dur.SetMaxLines then r.dur:SetMaxLines(1) end
      r.date  = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.date:SetTextColor(C4(MUTED, MUTED))
      r.date:SetJustifyH("RIGHT")
      r.date:SetWordWrap(false)
      if r.date.SetMaxLines then r.date:SetMaxLines(1) end

      r.line = r:CreateTexture(nil, "BORDER")
      r.line:SetTexture("Interface/Buttons/WHITE8x8")
      r.line:SetVertexColor(1, 1, 1, 0.06)

      local px = 1
      local leftX, rightX = 10, -10
      local y = 1
      if Widgets and Widgets.PixelSize then
        px = Widgets:PixelSize(r)
      end
      if Widgets and Widgets.Snap then
        leftX = Widgets:Snap(r, leftX)
        rightX = Widgets:Snap(r, rightX)
        y = Widgets:Snap(r, y)
      end

      r.line:SetHeight(px)
      if Widgets and Widgets.MakePixelPerfect then
        Widgets:MakePixelPerfect(r.line, r)
      end
      r.line:SetPoint("BOTTOMLEFT", leftX, y)
      r.line:SetPoint("BOTTOMRIGHT", rightX, y)

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

      r:SetScript("OnEnter", function(self)
        -- no boxy hover; QuestTitleHighlight handles hover
        if selected ~= self._sess then self.bg:SetAlpha(0) end
      end)
      r:SetScript("OnLeave", function(self)
        if selected == self._sess then self.bg:SetAlpha(0.08) else self.bg:SetAlpha(0) end
      end)
      r:SetScript("OnMouseUp", function(self, button)
        if button == "LeftButton" and self._sess and selected == self._sess and not (IsShiftKeyDown and IsShiftKeyDown()) then
          ClearSelectedSession()
          UI:RefreshHistory()
          return
        end

        UI._pendingSelectSession = nil
        selected = self._sess
        UI:RefreshHistory()

        if button == "LeftButton" and self._sess and IsShiftKeyDown and IsShiftKeyDown() then
          if WF and WF.Share and WF.Share.InsertRunLink then
            WF.Share:InsertRunLink(self._sess)
          end
          return
        end

        if button == "RightButton" and self._sess then
          local sess = self._sess
          local canMerge = (#H._CandidatesForMerge(sess, IsShiftKeyDown()) > 0)

          local canMergeSameZone = (#H._CandidatesForMerge(sess, true) > 0)
          local hasPos, wpMapId, wpX, wpY = H._WF_ResolveSessionWaypoint(sess)
          function H.HasCustomName(s)
            local cur = H._Trim(s._wfCustomFarmName or s.farmName or "")
            local zone = H._Trim(s.zone or "")
            if H._Trim(s._wfOrigFarmName or "") ~= "" then return true end
            if H._Trim(s._wfCustomFarmName or "") ~= "" then return true end
            if cur ~= "" and zone ~= "" and H._Trim(cur) ~= H._Trim(zone) then return true end
            return false
          end

          function H.SessionHasLiveFarm(s)
            if type(s) ~= "table" then return false end
            local farmId = s.farmId
            if farmId == nil then return false end
            if not (WF and WF.DB and WF.DB.GetFarm) then return false end
            return type(WF.DB:GetFarm(farmId)) == "table"
          end

          function H.SessionHasVisibleAverageData(s)
            if type(s) ~= "table" then return false end
            if not H.SessionHasLiveFarm(s) then return false end
            if not (WF and WF.DB and WF.DB.IsSessionCounted) then return false end
            return WF.DB:IsSessionCounted(s) and true or false
          end

          function H.FarmHasVisibleAverageData(farmId)
            if farmId == nil then return false end
            if not (WF and WF.DB and WF.DB.Get and WF.DB.IsSessionCounted) then return false end
            if type(WF.DB:GetFarm(farmId)) ~= "table" then return false end

            local db = WF.DB:Get() or {}
            local sessions = db.history and db.history.sessions
            if type(sessions) ~= "table" then return false end

            for i = 1, #sessions do
              local s = sessions[i]
              if type(s) == "table" and tonumber(s.farmId) == tonumber(farmId) then
                if WF.DB:IsSessionCounted(s) then
                  return true
                end
              end
            end
            return false
          end

          function H.RemoveThisSession()
            local db = WF.DB:Get() or {}
            if not (db.history and db.history.sessions) then return end
            if H._RemoveSessionFromHistory(db, sess) then
              if selected == sess then selected = nil end
              RebuildHistoryAggregates(db)
              UI:RefreshOverview()
              UI:RefreshHistory()
            end
          end

          function H.RemoveThisSessionAndData()
            if WF.DB and WF.DB.RemoveSessionFromFarmStats then
              WF.DB:RemoveSessionFromFarmStats(sess)
            end
            H.RemoveThisSession()
          end

          function H.RemoveThisSessionDataOnly()
            local removed = false
            if WF.DB and WF.DB.RemoveSessionFromFarmStats then
              removed = WF.DB:RemoveSessionFromFarmStats(sess) and true or false
            end

            if removed then
              local db = WF.DB:Get() or {}
              RebuildHistoryAggregates(db)
              UI:RefreshOverview()
              UI:RefreshHistory()
            else
              if UI and UI.Toast then
                UI:Toast("This session is not currently counted in Farm averages.", "info")
              end
            end
          end

          function H.ConfirmRemoveThisSession()
            local counted = H.SessionHasVisibleAverageData(sess)

            if counted and UI.ConfirmThreeActions then
              UI:ConfirmThreeActions(
                "Delete session",
"Choose what to remove for this run.\n\nDelete session only removes it from History and Recent runs.\n\nRemove from averages only keeps the session in History, but removes its contribution from Farm averages.\n\nDelete session + averages removes both.",
                "Delete session + averages",
                H.RemoveThisSessionAndData,
                "Remove from averages only",
                H.RemoveThisSessionDataOnly,
                "Delete session only",
                H.RemoveThisSession,
                nil,
                nil,
                nil,
                "Delete session + averages",
                "Remove this run from History and Recent runs, and also remove its contribution from Farm averages.",
                "Remove from averages only",
                "Keep this run in History, but remove it from Farm averages.",
                "Delete session only",
                "Remove this run from History and Recent runs, but leave Farm averages unchanged.",
                "Cancel",
                "Close this dialog without removing anything."
              )
              return
            end

            if counted and UI.ConfirmTwoActions then
              UI:ConfirmTwoActions(
                "Delete session",
"Delete this session from History?\n\nYou can optionally also remove it from Farm averages.\n\nAuto-pruned history keeps averages by design.",
                "Delete session only",
                H.RemoveThisSession,
                "Delete + remove averages",
                H.RemoveThisSessionAndData,
                nil,
                nil,
                "Delete session only",
                "Remove this run from History and Recent runs, but leave Farm averages unchanged.",
                "Delete + remove averages",
                "Remove this run from History and Recent runs, and also remove its contribution from Farm averages.",
                "Cancel",
                "Close this dialog without removing anything."
              )
              return
            end

            if WF.DB:GetSetting("confirmDestructive") then
              UI:Confirm("Delete session", "Delete this session from History? This will also remove it from Recent runs.", H.RemoveThisSession, "Delete session")
            else
              H.RemoveThisSession()
            end
          end

          function H.RemoveAllForFarm()
            local farmId = sess.farmId
            if not farmId then
              H.ConfirmRemoveThisSession()
              return
            end

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

            for i = #db.history.sessions, 1, -1 do
              local s = db.history.sessions[i]
              if s and s.farmId == farmId then
                table.remove(db.history.sessions, i)
              end
            end

            if selected and selected.farmId == farmId then selected = nil end
            RebuildHistoryAggregates(db)
            UI:RefreshOverview()
            UI:RefreshHistory()
          end



          function H.RemoveAllForFarmAndData()
            local farmId = sess.farmId
            if not farmId then
              H.ConfirmRemoveThisSession()
              return
            end

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

            for i = #db.history.sessions, 1, -1 do
              local s = db.history.sessions[i]
              if s and s.farmId == farmId then
                if WF.DB and WF.DB.RemoveSessionFromFarmStats then
                  WF.DB:RemoveSessionFromFarmStats(s)
                end
                table.remove(db.history.sessions, i)
              end
            end

            if selected and selected.farmId == farmId then selected = nil end
            RebuildHistoryAggregates(db)
            UI:RefreshOverview()
            UI:RefreshHistory()
          end
          function H.RemoveAllForFarmDataOnly()
            local farmId = sess.farmId
            if not farmId then
              H.RemoveThisSessionDataOnly()
              return
            end

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

            local removedAny = false
            for i = 1, #db.history.sessions do
              local s = db.history.sessions[i]
              if s and s.farmId == farmId then
                if WF.DB and WF.DB.RemoveSessionFromFarmStats then
                  if WF.DB:RemoveSessionFromFarmStats(s) then
                    removedAny = true
                  end
                end
              end
            end

            if removedAny then
              RebuildHistoryAggregates(db)
              UI:RefreshOverview()
              UI:RefreshHistory()
            else
              if UI and UI.Toast then
                UI:Toast("These sessions are not currently counted in Farm averages.", "info")
              end
            end
          end

          function H.ConfirmRemoveAllForFarm()
            local counted = H.FarmHasVisibleAverageData(sess and sess.farmId or nil)

            if counted and UI.ConfirmThreeActions then
              UI:ConfirmThreeActions(
                "Remove farm history",
"Choose what to remove for this farm.\n\nHistory only removes the saved sessions, but keeps Farm averages intact.\n\nAverage only keeps the saved sessions, but removes their contribution from Farm averages.\n\nHistory + average removes both.\n\nAuto-pruned sessions stay in averages by design.",
                "History + average",
                H.RemoveAllForFarmAndData,
                "Average only",
                H.RemoveAllForFarmDataOnly,
                "History only",
                H.RemoveAllForFarm,
                nil,
                nil,
                nil,
                "History + average",
                "Remove this farm's saved History sessions and also remove their contribution from Farm averages.",
                "Average only",
                "Keep the saved History sessions, but remove their contribution from Farm averages.",
                "History only",
                "Remove the saved History sessions, but leave Farm averages unchanged.",
                "Cancel",
                "Close this dialog without removing anything."
              )
              return
            end

            if counted and UI.ConfirmTwoActions then
              UI:ConfirmTwoActions(
                "Remove farm history",
"Remove ALL saved history for this farm?\n\nOptionally also remove these visible runs from Farm averages. (Auto-pruned runs stay in averages.)",
                "Delete history only",
                H.RemoveAllForFarm,
                "Delete + remove data",
                H.RemoveAllForFarmAndData,
                nil,
                nil,
                "Delete history only",
                "Remove this farm's saved History sessions, but leave Farm averages unchanged.",
                "Delete + remove data",
                "Remove this farm's saved History sessions and also remove their contribution from Farm averages.",
                "Cancel",
                "Close this dialog without removing anything."
              )
              return
            end

            if WF.DB:GetSetting("confirmDestructive") then
              UI:Confirm("Remove farm history", "Remove ALL saved history for this farm? This cannot be undone.", H.RemoveAllForFarm)
            else
              H.RemoveAllForFarm()
            end
          end

          local hasCustom = H.HasCustomName(sess)

          local noteLabel = "Add note"
          if H._Trim(sess.note or "") ~= "" then noteLabel = "Edit note" end

          local menu = {
            { text = "Waypoint", disabled = not hasPos, fn = function()
                if not hasPos then
                  UI:Toast("No waypoint data for this session.", "info")
                  return
                end
                SetWaypoint(wpMapId, wpX, wpY)
              end
            },
            { text = "Promote to Farm", disabled = not hasPos, fn = function()
                if not hasPos then
                  UI:Toast("No waypoint data for this session.", "info")
                  return
                end

                -- Promote creates/updates a Farm record AND links this history session to it.
                -- This is required so farm averages (Avg GPH/LIV) are sourced from History.
                local db = (WF.DB and WF.DB.Get) and (WF.DB:Get() or {}) or {}
                db.history = db.history or {}
                db.history.sessions = db.history.sessions or {}

                local prevFarmId = sess.farmId

                local _, farm = WF.DB:UpsertFarm({
                  name = sess.farmName or sess._wfCustomFarmName or sess.zone or "Farm",
                  zone = sess.zone,
                  mapId = wpMapId,
                  x = wpX,
                  y = wpY,
                })

                -- Link the session so history aggregates include it.
                if farm and farm.id then
                  -- If this session wasn't previously linked to a farm, it also wasn't
                  -- counted into farm averages. Accumulate it now so Avg GPH/LIV updates
                  -- immediately after promoting.
                  if not prevFarmId and WF.DB and WF.DB.AccumulateFarmTotals then
                    WF.DB:AccumulateFarmTotals(
                      farm.id,
                      tonumber(sess.elapsed) or 0,
                      tonumber(sess.totalValue) or 0,
                      tonumber(sess.moneyGained) or 0,
                      tonumber(sess.kills) or 0,
                      tostring(sess.mode or "Default")
                    )
                  end

                  sess.farmId = farm.id
                  -- Keep the session display name aligned with the farm name where possible.
                  if farm.name and farm.name ~= "" then
                    sess.farmName = farm.name
                  end
                end

                -- Rebuild totals so Farms/Overview immediately reflect the promoted run.
                if RebuildHistoryAggregates then
                  RebuildHistoryAggregates(db)
                end

                if farm then farm.updatedAt = time() end
                if WF.UI and WF.UI.Help and WF.UI.Help.Farms and WF.UI.Help.Farms.Refresh then
                  WF.UI.Help.Farms:Refresh()
                end
                if UI and UI.ShowFarmById and farm and farm.id then
                  UI:ShowFarmById(farm.id)
                end

                -- Immediately refresh counters/pool views after promoting.
                if UI and UI.RefreshOverview then UI:RefreshOverview() end
                if UI and UI.RefreshHistory then UI:RefreshHistory() end
                if UI and UI.MarkFarmAveragesDirty then UI:MarkFarmAveragesDirty() end
                if UI and UI.RefreshFarms then UI:RefreshFarms() end
              end
            },

            { sep = true },
            { text = "Rename farm", fn = function()
                if UI and UI.PromptRenameFarm then
                  UI:PromptRenameFarm(sess)
                elseif UI and UI.PromptRenameHistorySession then
                  UI:PromptRenameHistorySession(sess)
                end
              end
            },
            { text = noteLabel, fn = function()
                if UI and UI.PromptHistorySessionNote then
                  UI:PromptHistorySessionNote(sess)
                end
              end
            },
            { text = "Share", fn = function()
                if WF and WF.Share and WF.Share.ShowShareDialog then
                  WF.Share:ShowShareDialog(sess)
                end
              end
            },

            { sep = true },
            { text = "Merge another session into this", disabled = not canMerge, fn = function()
                H._OpenMergeMenu(sess, IsShiftKeyDown())
              end
            },

            { sep = true },
            { text = "Remove session from Recent", fn = H.ConfirmRemoveThisSession },
          }

          if hasCustom then
            table.insert(menu, 5, { text = "Reset farm name", fn = function()
              if UI and UI.ResetFarmNameFromSession then
                UI:ResetFarmNameFromSession(sess)
              elseif UI and UI.ResetHistorySessionName then
                UI:ResetHistorySessionName(sess)
              end
            end })
          end

          UI:OpenContextMenu(menu, 320)
        end
      end)

      rows[i] = r
      return r
    end
-- Layout the session list columns (header + all visible rows)
function H.LayoutSessionColumns()
  local w = header and header:GetWidth() or (scroll and scroll:GetWidth()) or 0
  if w < 1 then return end

  -- Keep the scroll-child width synced so rows/columns actually stretch on resize.
  if scroll and sc and scroll.GetWidth and sc.SetWidth then
    sc:SetWidth(scroll:GetWidth() or w)
  end

  local pad = 12
  local gap = 10

  -- Fixed columns (Farm is flexible)
  local dateW = 94
  local valW  = 130
  local durW  = 66
  local modeW = 66

  -- Minimums (so we never "lose" Mode/Time again)
  local dateMin, valMin, durMin, modeMin = 70, 100, 48, 48
  local minFarm = 140

  local ref = header or scroll or left or UIParent
  if Widgets and Widgets.Snap then
    pad = Widgets:Snap(ref, pad)
    gap = Widgets:Snap(ref, gap)

    dateW = Widgets:Snap(ref, dateW)
    valW  = Widgets:Snap(ref, valW)
    durW  = Widgets:Snap(ref, durW)
    modeW = Widgets:Snap(ref, modeW)

    dateMin = Widgets:Snap(ref, dateMin)
    valMin  = Widgets:Snap(ref, valMin)
    durMin  = Widgets:Snap(ref, durMin)
    modeMin = Widgets:Snap(ref, modeMin)
    minFarm = Widgets:Snap(ref, minFarm)
  end

  local showDate = true

  -- Shrink gracefully as the panel narrows (never hide Mode/Time)
  do
    local need = (pad * 2) + (gap * 3) + dateW + valW + durW + modeW + minFarm
    local overflow = need - w
    if overflow > 0 then
      local take = math.min(overflow, dateW - dateMin)
      dateW = dateW - take
      overflow = overflow - take

      take = math.min(overflow, valW - valMin)
      valW = valW - take
      overflow = overflow - take

      take = math.min(overflow, durW - durMin)
      durW = durW - take
      overflow = overflow - take

      take = math.min(overflow, modeW - modeMin)
      modeW = modeW - take
      overflow = overflow - take

      -- If we're still too narrow, hide Date first (Mode/Time stay)
      if overflow > 0 and w < 420 then
        showDate = false
        dateW = 0
      end
    end
  end

  -- Header anchors (right-anchored chain)
  hFarm:ClearAllPoints()
  hVal:ClearAllPoints()
  hMode:ClearAllPoints()
  hDur:ClearAllPoints()
  hDate:ClearAllPoints()

  if showDate then
    hDate:Show()
    hDate:SetPoint("RIGHT", header, "RIGHT", -pad, 0)
    hDate:SetWidth(dateW)

    hDur:Show()
    hDur:SetPoint("RIGHT", hDate, "LEFT", -gap, 0)
  else
    hDate:Hide()

    hDur:Show()
    hDur:SetPoint("RIGHT", header, "RIGHT", -pad, 0)
  end
  hDur:SetWidth(durW)

  hMode:Show()
  hMode:SetPoint("RIGHT", hDur, "LEFT", -gap, 0)
  hMode:SetWidth(modeW)

  hVal:SetPoint("RIGHT", hMode, "LEFT", -gap, 0)
  hVal:SetWidth(valW)

  hFarm:SetPoint("LEFT", header, "LEFT", 10, 0)
  hFarm:SetPoint("RIGHT", hVal, "LEFT", -gap, 0)


  -- Header click targets match the column bounds
  -- Stretch click targets like Overview (fill padding + gutters so the highlight reaches the edges)
  if hbDate then
    hbDate:Hide()
  end

  -- Rightmost first (so we can chain LEFT)
  if showDate and hbDate and hDate then
    hbDate:ClearAllPoints()
    hbDate:SetPoint("TOP", header, "TOP", 0, 0)
    hbDate:SetPoint("BOTTOM", header, "BOTTOM", 0, 0)
    hbDate:SetPoint("RIGHT", header, "RIGHT", 0, 0) -- include right padding area
    hbDate:SetPoint("LEFT", hDate, "LEFT", -6, 0)
    hbDate:Show()
  end

  if hbDur and hDur then
    hbDur:ClearAllPoints()
    hbDur:SetPoint("TOP", header, "TOP", 0, 0)
    hbDur:SetPoint("BOTTOM", header, "BOTTOM", 0, 0)
    if showDate and hbDate and hbDate:IsShown() then
      hbDur:SetPoint("RIGHT", hbDate, "LEFT", 0, 0)
    else
      hbDur:SetPoint("RIGHT", header, "RIGHT", 0, 0) -- include right padding area
    end
    hbDur:SetPoint("LEFT", hDur, "LEFT", -6, 0)
    hbDur:Show()
  end

  if hbMode and hMode and hbDur then
    hbMode:ClearAllPoints()
    hbMode:SetPoint("TOP", header, "TOP", 0, 0)
    hbMode:SetPoint("BOTTOM", header, "BOTTOM", 0, 0)
    hbMode:SetPoint("RIGHT", hbDur, "LEFT", 0, 0)
    hbMode:SetPoint("LEFT", hMode, "LEFT", -6, 0)
    hbMode:Show()
  end

  if hbVal and hVal and hbMode then
    hbVal:ClearAllPoints()
    hbVal:SetPoint("TOP", header, "TOP", 0, 0)
    hbVal:SetPoint("BOTTOM", header, "BOTTOM", 0, 0)
    hbVal:SetPoint("RIGHT", hbMode, "LEFT", 0, 0)
    hbVal:SetPoint("LEFT", hVal, "LEFT", -6, 0)
    hbVal:Show()
  end

  if hbFarm and hbVal then
    hbFarm:ClearAllPoints()
    hbFarm:SetPoint("TOP", header, "TOP", 0, 0)
    hbFarm:SetPoint("BOTTOM", header, "BOTTOM", 0, 0)
    hbFarm:SetPoint("LEFT", header, "LEFT", 0, 0) -- include left padding area
    hbFarm:SetPoint("RIGHT", hbVal, "LEFT", 0, 0)
    hbFarm:Show()
  end


  -- Rows
  for i = 1, #rows do
    local r = rows[i]
    if r and r.farm then
      r.date:ClearAllPoints()
      r.val:ClearAllPoints()
      r.dur:ClearAllPoints()
      r.mode:ClearAllPoints()
      r.farm:ClearAllPoints()

      if showDate then
        r.date:Show()
        r.date:SetPoint("RIGHT", r, "RIGHT", -pad, 0)
        r.date:SetWidth(dateW)

        r.dur:Show()
        r.dur:SetPoint("RIGHT", r.date, "LEFT", -gap, 0)
      else
        r.date:Hide()

        r.dur:Show()
        r.dur:SetPoint("RIGHT", r, "RIGHT", -pad, 0)
      end
      r.dur:SetWidth(durW)

      r.mode:Show()
      r.mode:SetPoint("RIGHT", r.dur, "LEFT", -gap, 0)
      r.mode:SetWidth(modeW)

      r.val:SetPoint("RIGHT", r.mode, "LEFT", -gap, 0)
      r.val:SetWidth(valW)

      r.farm:SetPoint("LEFT", r, "LEFT", 12, 0)
      r.farm:SetPoint("RIGHT", r.val, "LEFT", -gap, 0)

    end
  end
end

H.LayoutSessionColumns()
if header and header.HookScript then
  header:HookScript("OnSizeChanged", H.LayoutSessionColumns)
end
if left and left.HookScript then
  left:HookScript("OnSizeChanged", H.LayoutSessionColumns)
end

-- -----------------------------------------------------
-- Top loot: responsive columns + cleaner text
-- -----------------------------------------------------

function H._FmtGold(n, grouped)
  n = tonumber(n) or 0
  n = math.floor(n + 0.00001)
  if grouped ~= false and BreakUpLargeNumbers then return BreakUpLargeNumbers(n) end
  local s = tostring(n)
  local left, num, right = s:match("^([^%d]*)(%d+)(.-)$")
  num = num or s
  if grouped == false then
    return (left or "") .. num .. (right or "")
  end
  local rev = num:reverse():gsub("(%d%d%d)", "%1,"):reverse()
  rev = rev:gsub("^,", "")
  return (left or "") .. rev .. (right or "")
end

function H._MoneyCompact(copper, tight, squeeze, grouped)
  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 sep = " "

  -- Keep the row format readable: gold + silver with a space between units.
  -- Tight still hides copper when we're in the loot rows.
  if g > 0 then
    if s > 0 then return H._FmtGold(g, grouped) .. "g" .. sep .. tostring(s) .. "s" end
    return H._FmtGold(g, grouped) .. "g"
  end

  if s > 0 then
    if tight then return tostring(s) .. "s" end
    if c > 0 then return tostring(s) .. "s" .. sep .. tostring(c) .. "c" end
    return tostring(s) .. "s"
  end

  return tostring(c) .. "c"
end

function H._MoneyCompactEach(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)

  -- Keep the "Each" column clean and readable:
  -- when gold is present, show gold only so mog/item names keep their room.
  -- under 1g, preserve silver/copper so cheap items still make sense at a glance.
  if g > 0 then
    return H._FmtGold(g, true) .. "g"
  end

  if s > 0 then
    if c > 0 then return tostring(s) .. "s " .. tostring(c) .. "c" end
    return tostring(s) .. "s"
  end

  return tostring(c) .. "c"
end

function H._LinkToName(s)
  s = tostring(s or "")
  if s == "" then return "" end

  if s:find("|H") then
    s = s:gsub("|c%x%x%x%x%x%x%x%x", "")
    s = s:gsub("|H.-|h", "")
    s = s:gsub("|h", "")
    s = s:gsub("|r", "")
  end

  s = s:gsub("^%[", ""):gsub("%]$", "")
  return s
end

function H.LayoutLootColumns()
  if not lootScroll then return end
  local w = lootScroll:GetWidth() or 0
  if w < 1 then return end

  -- Keep the scroll-child width synced so value column stays flush right when the panel grows.
  if lootContent and lootContent.SetWidth then
    lootContent:SetWidth(w)
  end

  local rightPad = 4
  local leftPad = 4
  local iconW = 18
  local gapIcon = 3
  local gapNameCount = 2
  local gapCountEach = 1
  local gapEachValue = 2

  local valueW = 70
  local eachW = 58
  local countW = 36

  local minValueW = 62
  local minEachW = 50
  local minCountW = 32
  local minNameW = 138

  if Widgets and Widgets.Snap then
    local ref = lootScroll
    rightPad = Widgets:Snap(ref, rightPad)
    leftPad  = Widgets:Snap(ref, leftPad)
    iconW    = Widgets:Snap(ref, iconW)
    gapIcon  = Widgets:Snap(ref, gapIcon)
    gapNameCount = Widgets:Snap(ref, gapNameCount)
    gapCountEach = Widgets:Snap(ref, gapCountEach)
    gapEachValue = Widgets:Snap(ref, gapEachValue)

    valueW = Widgets:Snap(ref, valueW)
    eachW  = Widgets:Snap(ref, eachW)
    countW = Widgets:Snap(ref, countW)

    minValueW = Widgets:Snap(ref, minValueW)
    minEachW  = Widgets:Snap(ref, minEachW)
    minCountW = Widgets:Snap(ref, minCountW)
    minNameW  = Widgets:Snap(ref, minNameW)
  end

  local nameRoom = w - (leftPad + iconW + gapIcon) - gapNameCount - countW - gapCountEach - eachW - gapEachValue - valueW - rightPad
  local tight = false

  if nameRoom < minNameW then
    local need = minNameW - nameRoom

    local take = math.min(need, eachW - minEachW)
    eachW = eachW - take
    need = need - take
    nameRoom = nameRoom + take

    if need > 0 then
      take = math.min(need, countW - minCountW)
      countW = countW - take
      need = need - take
      nameRoom = nameRoom + take
    end

    if need > 0 then
      take = math.min(need, valueW - minValueW)
      valueW = valueW - take
      need = need - take
      nameRoom = nameRoom + take
    end

    if need > 0 then
      tight = true
      valueW = minValueW
      eachW = minEachW
      countW = minCountW
    end
  end

  lootLayout.valueW = valueW
  lootLayout.eachW = eachW
  lootLayout.countW = countW
  lootLayout.tight = tight

  if lootHeader and lootHeader.price and lootHeader.each and lootHeader.count then
    lootHeader.price:SetWidth(valueW)
    lootHeader.each:SetWidth(eachW)
    lootHeader.count:SetWidth(countW)

    if lootHeader.priceBtn and lootHeader.eachBtn and lootHeader.countBtn and lootHeader.itemBtn then
      lootHeader.priceBtn:ClearAllPoints()
      lootHeader.priceBtn:SetPoint("TOPRIGHT", lootHeader, "TOPRIGHT", -4, 0)
      lootHeader.priceBtn:SetPoint("BOTTOMRIGHT", lootHeader, "BOTTOMRIGHT", -4, 0)
      lootHeader.priceBtn:SetWidth(valueW)

      lootHeader.eachBtn:ClearAllPoints()
      lootHeader.eachBtn:SetPoint("TOPRIGHT", lootHeader.priceBtn, "TOPLEFT", -2, 0)
      lootHeader.eachBtn:SetPoint("BOTTOMRIGHT", lootHeader.priceBtn, "BOTTOMLEFT", -2, 0)
      lootHeader.eachBtn:SetWidth(eachW)

      lootHeader.countBtn:ClearAllPoints()
      lootHeader.countBtn:SetPoint("TOPRIGHT", lootHeader.eachBtn, "TOPLEFT", -1, 0)
      lootHeader.countBtn:SetPoint("BOTTOMRIGHT", lootHeader.eachBtn, "BOTTOMLEFT", -1, 0)
      lootHeader.countBtn:SetWidth(countW)

      lootHeader.itemBtn:ClearAllPoints()
      lootHeader.itemBtn:SetPoint("TOPLEFT", lootHeader, "TOPLEFT", 26, 0)
      lootHeader.itemBtn:SetPoint("BOTTOMLEFT", lootHeader, "BOTTOMLEFT", 26, 0)
      lootHeader.itemBtn:SetPoint("RIGHT", lootHeader.countBtn, "LEFT", -4, 0)
    end
  end

  for i = 1, #lootRows do
    local r = lootRows[i]
    if r and r.value and r.each and r.count then
      r.value:SetWidth(valueW)
      r.each:SetWidth(eachW)
      r.count:SetWidth(countW)
    end
  end
end

H.LayoutLootColumns()
if lootScroll and lootScroll.HookScript then
  lootScroll:HookScript("OnSizeChanged", H.LayoutLootColumns)
end
if lootPanel and lootPanel.HookScript then
  lootPanel:HookScript("OnSizeChanged", H.LayoutLootColumns)
end

-- -----------------------------------------------------
-- History page: make panels scale with the page width
-- (so there isn't a giant dead zone when you stretch wider)
-- -----------------------------------------------------
function H.LayoutHistoryPanels()
  if not pHistory then return end

  -- Anchor panels to the History page frame (avoids overlapping the left nav),
  -- but read width from the parent if needed.
  local host = pHistory
  local w = (host.GetWidth and host:GetWidth() or 0)
  if w < 1 then
    local parent = host.GetParent and host:GetParent() or nil
    w = (parent and parent.GetWidth and parent:GetWidth()) or 0
  end
  if w < 1 then return end

  local pad = 10
  local gap = 10

  -- Right panel width scales, but stays within sane bounds.
  local minRight, maxRight = 352, 580
  local rightW = w * 0.392

  if Widgets and Widgets.Snap then
    pad = Widgets:Snap(host, pad)
    gap = Widgets:Snap(host, gap)
    minRight = Widgets:Snap(host, minRight)
    maxRight = Widgets:Snap(host, maxRight)
    rightW = Widgets:Snap(host, rightW)
  else
    rightW = math.floor(rightW + 0.5)
  end

  if rightW < minRight then rightW = minRight end
  if rightW > maxRight then rightW = maxRight end

  if right and right.ClearAllPoints and right.SetPoint and right.SetWidth then
    right:ClearAllPoints()
    right:SetPoint("TOPRIGHT", host, "TOPRIGHT", -pad, -pad)
    right:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -pad, pad)
    right:SetWidth(rightW)
  end

  if left and left.ClearAllPoints and left.SetPoint then
    left:ClearAllPoints()
    left:SetPoint("TOPLEFT", host, "TOPLEFT", pad, -pad)
    left:SetPoint("BOTTOMLEFT", host, "BOTTOMLEFT", pad, pad)
    if right then
      left:SetPoint("RIGHT", right, "LEFT", -gap, 0)
    else
      left:SetPoint("RIGHT", host, "RIGHT", -pad, 0)
    end
  end

  -- Re-flow columns now that widths have changed.
  H.LayoutSessionColumns()
  H.LayoutLootColumns()
end

H.LayoutHistoryPanels()
if pHistory and pHistory.HookScript then
  pHistory:HookScript("OnSizeChanged", H.LayoutHistoryPanels)
end
local host = pHistory and pHistory.GetParent and pHistory:GetParent() or nil
if host and host.HookScript then
  host:HookScript("OnSizeChanged", H.LayoutHistoryPanels)
end

function H.EnsureLootRow(i)
      local r = lootRows[i]
      if r then return r end

      r = CreateFrame("Button", nil, lootContent)
      r:SetHeight(22)
      r:SetPoint("LEFT", 0, 0)
      r:SetPoint("RIGHT", 0, 0)

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

      r.toggle = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      r.toggle:SetPoint("CENTER", r.icon, "CENTER", 0, 0)
      r.toggle:SetJustifyH("CENTER")
      r.toggle:SetTextColor(C4(TEXT, TEXT))
      r.toggle:SetText("")

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

      -- Right side columns (fixed widths so item names stay readable)
      r.value = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.value:SetPoint("RIGHT", -4, 0)
      r.value:SetTextColor(C4(MUTED, MUTED))
      r.value:SetJustifyH("RIGHT")
      r.value:SetWordWrap(false)
      if r.value.SetMaxLines then r.value:SetMaxLines(1) end
      r.value:SetWidth(lootLayout.valueW or 64)

      r.each = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.each:SetTextColor(C4(MUTED, MUTED))
      r.each:SetJustifyH("RIGHT")
      r.each:SetWordWrap(false)
      if r.each.SetMaxLines then r.each:SetMaxLines(1) end
      r.each:SetPoint("RIGHT", r.value, "LEFT", -2, 0)
      r.each:SetWidth(lootLayout.eachW or 58)

      r.count = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.count:SetTextColor(C4(MUTED, MUTED))
      r.count:SetJustifyH("CENTER")
      r.count:SetWordWrap(false)
      if r.count.SetMaxLines then r.count:SetMaxLines(1) end
      r.count:SetPoint("RIGHT", r.each, "LEFT", -1, 0)
      r.count:SetWidth(lootLayout.countW or 22)

      H._TightenLootNumberFont(r.count)
      H._TightenLootNumberFont(r.each)
      H._TightenLootNumberFont(r.value)

      -- Name fills the remaining space between icon and quantity
      r.name:SetPoint("LEFT", r.icon, "RIGHT", 3, 0)
      r.name:SetPoint("RIGHT", r.count, "LEFT", -4, 0)
      r._hoverToken = 0

      function H._LiteTooltip(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local nm = (self.name and self.name.GetText and self.name:GetText()) or "Item"
        local c  = (self.count and self.count.GetText and self.count:GetText()) or ""
        local e  = self._fullEachText or ((self.each and self.each.GetText and self.each:GetText()) or "")
        local p  = self._fullTotalText or ((self.value and self.value.GetText and self.value:GetText()) or "")

        GameTooltip:SetText(nm, 1, 1, 1, 1, true)
        if c ~= "" then GameTooltip:AddLine("Qty: " .. c, 0.75, 0.75, 0.75, 1, true) end
        if e ~= "" then GameTooltip:AddLine("Each: " .. e, 0.75, 0.75, 0.75, 1, true) end
        if p ~= "" then GameTooltip:AddLine("Total: " .. p, 0.75, 0.75, 0.75, 1, true) end

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

        GameTooltip:Show()
      end

      function H._FullTooltip(self)
        if not self._link then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        local okLink = false
        if Util and Util.TrySetHyperlinkCached then
          okLink = Util:TrySetHyperlinkCached(GameTooltip, self._link)
        elseif GameTooltip and GameTooltip.SetHyperlink then
          okLink = pcall(GameTooltip.SetHyperlink, GameTooltip, self._link)
        end
        if not okLink then
          GameTooltip:ClearLines()
          GameTooltip:SetText((self.name and self.name.GetText and self.name:GetText()) or "Item", 1, 1, 1, 1, true)
          GameTooltip:AddLine("Full tooltip unavailable", 0.70, 0.70, 0.70, 1, true)
        end

        if self._unv then
          GameTooltip:AddLine(" ")
          GameTooltip:AddLine("Unverified price (TSM): no auctions / no sale rate", 0.70, 0.70, 0.70, 1, true)
          local m = self._unvMeta
          if type(m) == "table" then
            local sr = tonumber(m.saleRate) or 0
            local mb = tonumber(m.minBuyout) or 0
            GameTooltip:AddLine(string.format("Sale rate: %.4f  Min buyout: %s", sr, Money(mb)), 0.65, 0.65, 0.65, true)
          end
        end

        local c  = (self.count and self.count.GetText and self.count:GetText()) or ""
        local e  = self._fullEachText or ((self.each and self.each.GetText and self.each:GetText()) or "")
        local p  = self._fullTotalText or ((self.value and self.value.GetText and self.value:GetText()) or "")
        if c ~= "" or e ~= "" or p ~= "" then
          GameTooltip:AddLine(" ")
          if c ~= "" then GameTooltip:AddLine("Qty: " .. c, 0.75, 0.75, 0.75, 1, true) end
          if e ~= "" then GameTooltip:AddLine("Each: " .. e, 0.75, 0.75, 0.75, 1, true) end
          if p ~= "" then GameTooltip:AddLine("Total: " .. p, 0.75, 0.75, 0.75, 1, true) end
        end

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

        GameTooltip:Show()
      end

      r:SetScript("OnEnter", function(self)
        self._hoverToken = (self._hoverToken or 0) + 1
        if self._wfKind == "resetHeader" then
          H._ShowResetHeaderTooltip(self)
          return
        end
        local tok = self._hoverToken

        -- Lightweight tooltip immediately (doesn't force item info loads).
        H._LiteTooltip(self)

        -- 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
            if lootScroll and lootScroll._wfLastScroll and (GetTime() - lootScroll._wfLastScroll) < 0.20 then return end
            H._FullTooltip(self)
          end)
        end
      end)

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


r:RegisterForClicks("LeftButtonUp", "RightButtonUp")
r:SetScript("OnMouseUp", function(self, button)
  if self._wfKind == "resetHeader" then
    if self._sess and self._resetNumber then
      H._ToggleResetCollapsed(self._sess, self._resetNumber)
      UI:RefreshHistoryLoot()
    end
    return
  end
  if IsShiftKeyDown() and ChatEdit_InsertLink then
    local link = self._link
    local resolved = nil
    if type(link) == "string" and link:find("|H", 1, true) then
      if GetItemInfo then
        local _, rl = GetItemInfo(link)
        if rl and rl ~= "" then resolved = rl end
      end
      if not resolved and link:find("^|c") then resolved = link end
    end
    if not resolved and self._itemID and self._itemID > 0 and GetItemInfo then
      local _, rl = GetItemInfo(self._itemID)
      if rl and rl ~= "" then resolved = rl end
    end
    if resolved then
      ChatEdit_InsertLink(resolved)
      return
    end
    return
  end

  local sess = self._sess
  local itemID = self._itemID
  if not sess or not itemID then return end

  function H._SetHidden(v)
    local t = H._SessIgnore(sess, "hideTop")
    if not t then return end
    if v then t[itemID] = true else t[itemID] = nil end
    if H._IsEmpty(t) and sess._wfIgnore then sess._wfIgnore.hideTop = nil end
  end

  function H._SetExcluded(v)
    local t = H._SessIgnore(sess, "excludeValue")
    if not t then return end
    if v then t[itemID] = true else t[itemID] = nil end
    if H._IsEmpty(t) and sess._wfIgnore then sess._wfIgnore.excludeValue = nil end
    if v then H._ApplySessionTotalOverride(sess) else H._MaybeClearTotalOverride(sess) end
  end

  function H._ApplyToFarm()
    local farmId = sess and sess.farmId
    if not farmId then return end

    local hideNow = H._IsHidden(sess, itemID)
    local exNow = H._IsExcluded(sess, itemID)

    local db = WF.DB:Get() or {}
    local sessions = (db.history and db.history.sessions) or {}
    for i = 1, #sessions do
      local s = sessions[i]
      if s and s.farmId == farmId then
        local tH = H._SessIgnore(s, "hideTop")
        if hideNow then tH[itemID] = true else tH[itemID] = nil end
        if H._IsEmpty(tH) and s._wfIgnore then s._wfIgnore.hideTop = nil end

        local tE = H._SessIgnore(s, "excludeValue")
        if exNow then tE[itemID] = true else tE[itemID] = nil end
        if H._IsEmpty(tE) and s._wfIgnore then s._wfIgnore.excludeValue = nil end

        if exNow then
          H._ApplySessionTotalOverride(s)
        else
          H._MaybeClearTotalOverride(s)
        end
      end
    end

    RebuildHistoryAggregates(db)
    UI:RefreshOverview()
    UI:RefreshHistory()
  end

  local hide = H._IsHidden(sess, itemID)
  local ex = H._IsExcluded(sess, itemID)
  local menu = {}

  if hide then
    menu[#menu + 1] = {
      text = "Restore to Top loot",
      fn = function()
        H._SetHidden(false)
        UI:RefreshHistory()
      end
    }
  else
    menu[#menu + 1] = {
      text = "Delete from Top loot",
      fn = function()
        H._SetHidden(true)
        UI:RefreshHistory()
      end
    }
  end

  if ex then
    menu[#menu + 1] = {
      text = "Include in session value",
      fn = function()
        local db = WF.DB:Get() or {}
        H._SetExcluded(false)
        RebuildHistoryAggregates(db)
        UI:RefreshOverview()
        UI:RefreshHistory()
      end
    }
  else
    menu[#menu + 1] = {
      text = "Ignore in session value",
      fn = function()
        UI:Confirm(
          "Ignore in session value?",
          "This will subtract this item from the session's total value and refresh aggregates.",
          function()
            local db = WF.DB:Get() or {}
            H._SetExcluded(true)
            RebuildHistoryAggregates(db)
            UI:RefreshOverview()
            UI:RefreshHistory()
          end
        )
      end
    }
  end

  if (not hide) and (not ex) then
    menu[#menu + 1] = { sep = true }
    menu[#menu + 1] = {
      text = "Delete + Ignore",
      fn = function()
        UI:Confirm(
          "Delete + Ignore?",
          "This will remove the item from Top loot and also remove it from the session's value totals.",
          function()
            local db = WF.DB:Get() or {}
            H._SetHidden(true)
            H._SetExcluded(true)
            RebuildHistoryAggregates(db)
            UI:RefreshOverview()
            UI:RefreshHistory()
          end
        )
      end
    }
  end

  if hide or ex then
    menu[#menu + 1] = { sep = true }
    menu[#menu + 1] = {
      text = "Reset for this item",
      fn = function()
        local hadEx = ex
        H._SetHidden(false)
        H._SetExcluded(false)

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

  UI:OpenContextMenu(menu, 260)

end)

      lootRows[i] = r
      return r
    end

    -- -----------------------------------------------------
    -- Top loot: async quality color fix (item info can be uncached)
    -- -----------------------------------------------------
    -- Quality cache + deduped pending loads for History "Top loot (session)" rows.
    local _wfHistLootQCache = {}
    local _wfHistLootQCacheOrder = {}
    local _wfHistLootQPending = {}
    local _wfHistLootQWait = {}

    function H._RememberLootQualityCache(key, q)
      if key == nil or q == nil then return end
      if _wfHistLootQCache[key] == nil then
        _wfHistLootQCacheOrder[#_wfHistLootQCacheOrder + 1] = key
      end
      _wfHistLootQCache[key] = q
      local limit = 256
      local extra = #_wfHistLootQCacheOrder - limit
      if extra > 0 then
        for i = 1, extra do
          local oldKey = table.remove(_wfHistLootQCacheOrder, 1)
          if oldKey ~= nil then
            _wfHistLootQCache[oldKey] = nil
            _wfHistLootQPending[oldKey] = nil
            _wfHistLootQWait[oldKey] = nil
          end
        end
      end
    end

    function H._DetachLootRowWaitRefs(row)
      if not row then return end
      for key, wait in pairs(_wfHistLootQWait) do
        wait[row] = nil
        if not next(wait) then
          _wfHistLootQWait[key] = nil
        end
      end
    end

    function H._ResetHistoryLootAsync(clearCache)
      for key in pairs(_wfHistLootQWait) do
        _wfHistLootQWait[key] = nil
      end
      for key in pairs(_wfHistLootQPending) do
        _wfHistLootQPending[key] = nil
      end
      if clearCache then
        for key in pairs(_wfHistLootQCache) do
          _wfHistLootQCache[key] = nil
        end
        for i = #_wfHistLootQCacheOrder, 1, -1 do
          _wfHistLootQCacheOrder[i] = nil
        end
      end
    end

    function H._SetLootRowQuality(row, tok, q)
      if not q or type(q) ~= "number" then return end
      if not row or not row.name or not row.IsShown or not row:IsShown() then return end
      if row._wfQTok ~= tok then return end
      local r2, g2, b2 = GetItemQualityColor(q)
      if r2 and g2 and b2 then
        row.name:SetTextColor(r2, g2, b2)
      end
    end

    function H._QueueLootQualityLoad(key, id, link)
      if not key then return end
      if _wfHistLootQPending[key] then return end
      _wfHistLootQPending[key] = true

      function H._OnLoaded()
        _wfHistLootQPending[key] = nil

        local q = nil
        if id and C_Item and C_Item.GetItemQualityByID then
          q = C_Item.GetItemQualityByID(id)
        end
        if not q then
          local _, _, q2 = GetItemInfo(link or id or key)
          q = q2
        end

        if q then H._RememberLootQualityCache(key, q) end

        local wait = _wfHistLootQWait[key]
        _wfHistLootQWait[key] = nil
        if not wait then return end

        if q then
          for row, tok in pairs(wait) do
            H._SetLootRowQuality(row, tok, q)
          end
        end
      end

      -- Prefer the modern async item API when available (retail). One load per key.
      if Item then
        if id and Item.CreateFromItemID then
          local it = Item:CreateFromItemID(id)
          if it and it.ContinueOnItemLoad then
            it:ContinueOnItemLoad(H._OnLoaded)
            return
          end
        end
        if Item.CreateFromItemLink and type(link) == "string" and link ~= "" and link:find("|H") then
          local it = Item:CreateFromItemLink(link)
          if it and it.ContinueOnItemLoad then
            it:ContinueOnItemLoad(H._OnLoaded)
            return
          end
        end
      end

      -- Fallback (classic / edge cases): one delayed retry via GetItemInfo
      if C_Timer and C_Timer.After then
        C_Timer.After(0.15, H._OnLoaded)
      end
    end

    function H._ApplyLootQualityColor(row, link, itemID)
      if not row or not row.name then return end

      -- Cancel any pending color updates for this row (row frames get reused).
      row._wfQTok = (row._wfQTok or 0) + 1
      local tok = row._wfQTok

      local want = false
      if WF.DB and WF.DB.GetSetting then
        want = WF.DB:GetSetting("colorByQuality") and true or false
      end

      if not want then
        row.name:SetTextColor(C4(TEXT, TEXT))
        row._wfQKey = nil
        return
      end

      local id = tonumber(itemID)

      -- Prefer itemID parsed from the hyperlink (more reliable than any stored id).
      if type(link) == "string" and link ~= "" then
        local id2 = nil
        if GetItemInfoInstant then
          id2 = select(1, GetItemInfoInstant(link))
        end
        if not id2 then
          id2 = tonumber(link:match("item:(%d+)"))
        end
        if id2 then id = tonumber(id2) end
      end

      local key = id or link
      row._wfQKey = key

      -- Cache hit (but allow a loaded hyperlink to correct a bad cached value).
      if key and _wfHistLootQCache[key] ~= nil then
        local cq = _wfHistLootQCache[key]
        if type(link) == "string" and link ~= "" then
          local _, _, q2 = GetItemInfo(link)
          if type(q2) == "number" and q2 ~= cq then
            cq = q2
            H._RememberLootQualityCache(key, q2)
          end
        end
        H._SetLootRowQuality(row, tok, cq)
        return
      end

      -- Immediate best-effort (cached item info path).
      local q = nil

      -- Prefer quality from the hyperlink when available (matches tooltip quality exactly).
      if type(link) == "string" and link ~= "" then
        local _, _, q2 = GetItemInfo(link)
        if type(q2) == "number" then q = q2 end
      end

      if not q and id and C_Item and C_Item.GetItemQualityByID then
        q = C_Item.GetItemQualityByID(id)
      end

      if not q and id then
        local _, _, q2 = GetItemInfo(id)
        q = q2
      end

      if q then
        if key then H._RememberLootQualityCache(key, q) end
        H._SetLootRowQuality(row, tok, q)
        return
      end

      -- Default until load completes.
      row.name:SetTextColor(C4(TEXT, TEXT))

      if not key then return end

      _wfHistLootQWait[key] = _wfHistLootQWait[key] or {}
      _wfHistLootQWait[key][row] = tok

      H._QueueLootQualityLoad(key, id, link)
    end


    function H._GetHistoryLootVisibleRows()
      local h = lootScroll and lootScroll.GetHeight and (lootScroll:GetHeight() or 0) or 0
      local n = math.ceil(((h > 0 and h or lootRowH) / lootRowH)) + 2
      if n < 1 then n = 1 end
      return n
    end

    function H._AssignHistoryLootRow(row, data, absoluteIndex)
      local e = data and data.entry or nil
      row:Show()
      row:ClearAllPoints()
      row:SetPoint("TOPLEFT", 0, -((absoluteIndex - 1) * lootRowH))
      row:SetPoint("TOPRIGHT", 0, -((absoluteIndex - 1) * lootRowH))

      row._wfKind = data and data.kind or nil
      if row._wfKind == "resetHeader" then
        H._DetachLootRowWaitRefs(row)
        row._wfQTok = (row._wfQTok or 0) + 1
        row._link = nil
        row._src = nil
        row._itemID = nil
        row._sess = selected
        row._resetNumber = data and data.resetNumber or nil
        row._resetData = data and data.resetData or nil
        row._unv = nil
        row._unvMeta = nil
        row._fullEachText = nil
        row._fullTotalText = nil
        row.icon:SetTexture(nil)
        if row.toggle then
          row.toggle:SetText((data and data.collapsed) and "+" or "-")
        end

        -- Reset headers get the full row width so the Time/GPH/LIV line does
        -- not get squeezed into the Count/Each/Total columns or show ellipses.
        row.name:ClearAllPoints()
        row.name:SetPoint("LEFT", row.icon, "RIGHT", 3, 0)
        row.name:SetPoint("RIGHT", row, "RIGHT", -4, 0)

        local resetLabel = tostring(data.text or "Reset")
        local resetMeta = tostring(data.valueText or "")
        if resetMeta ~= "" then
          resetLabel = resetLabel .. "  " .. resetMeta
        end
        row.name:SetText(H._HistoryMetaColor(HISTORY_META_HEX_ACCENT, resetLabel))
        row.name:SetTextColor(C4(TEXT, TEXT))
        H._TightenResetHeaderFont(row.name)
        row._fullResetHeaderText = H._FormatResetTopLootHeaderMeta(row._resetData, true)
        row.count:SetText("")
        row.each:SetText("")
        row.value:SetText("")
        row.value:SetTextColor(C4(MUTED, MUTED))
        return
      end

      row.name:ClearAllPoints()
      row.name:SetPoint("LEFT", row.icon, "RIGHT", 3, 0)
      row.name:SetPoint("RIGHT", row.count, "LEFT", -4, 0)

      row._link = e and e.link or nil
      row._src = e and e.lastSource or nil
      row._itemID = data and data.itemID or nil
      row._sess = selected
      row._resetNumber = nil
      row._resetData = nil
      row._fullResetHeaderText = nil
      row._unv = (e and e._wfUnverified) or false
      row._unvMeta = (e and e._wfUnverifiedMeta) or nil
      row.icon:SetTexture(e and e.icon or nil)
      if row.toggle then row.toggle:SetText("") end

      local cnt = tonumber(e and e.count) or 1
      if cnt < 0 then cnt = 0 end
      local totalValue = tonumber(data and data.value) or 0
      local eachValue = tonumber(e and e.unitValue) or 0
      if eachValue <= 0 and cnt > 0 then
        eachValue = totalValue / cnt
      end

      row.count:SetText(H._FmtGold(cnt, false))
      row._fullEachText = H._MoneyCompact(eachValue, false, true, true)
      row._fullTotalText = H._MoneyCompact(totalValue, false, true, true)
      row.each:SetText(H._MoneyCompactEach(eachValue))

      local nm = e and e.name
      if not nm or nm == "" then
        nm = H._LinkToName(e and e.link)
      end
      if nm == "" then nm = "Item" end
      row.name:SetText(nm)
      row.value:SetText(H._MoneyCompact(totalValue, true, true, true))
      H._ApplyLootQualityColor(row, e and e.link, data and data.itemID)
    end

    function UI:_RenderHistoryLootVisible()
      if not lootScroll or not lootContent then return end

      local list = _historyLootScratch
      local total = #list
      local visible = H._GetHistoryLootVisibleRows()
      local startIndex = math.floor((lootScroll:GetVerticalScroll() or 0) / lootRowH) + 1
      if startIndex < 1 then startIndex = 1 end

      for i = 1, visible do
        local absoluteIndex = startIndex + i - 1
        local data = list[absoluteIndex]
        local row = H.EnsureLootRow(i)
        if data then
          H._AssignHistoryLootRow(row, data, absoluteIndex)
        else
          H._DetachLootRowWaitRefs(row)
          row._wfQTok = (row._wfQTok or 0) + 1
          row._sess = nil
          row._itemID = nil
          row._link = nil
          row._src = nil
          row._unv = nil
          row._unvMeta = nil
          row:Hide()
        end
      end

      for i = visible + 1, #lootRows do
        local row = lootRows[i]
        if row then
          H._DetachLootRowWaitRefs(row)
          row._wfQTok = (row._wfQTok or 0) + 1
          row._sess = nil
          row._itemID = nil
          row._link = nil
          row._src = nil
          row._unv = nil
          row._unvMeta = nil
          row:Hide()
        end
      end

      lootContent:SetHeight((total > 0 and total or 1) * lootRowH)
    end


    function UI:RefreshHistoryLoot()
      for i = 1, #lootRows do
        lootRows[i]:Hide()
      end
      lootContent:SetHeight(1)

      if UI._historyEmptyLoot then
        UI._historyEmptyLoot:Hide()
      end

      if lootCount then
        lootCount:SetText("")
      end

      if not pHistory or not pHistory:IsShown() then return end

      if UI._wfHistoryLootSelected ~= selected then
        UI._wfHistoryLootSelected = selected
        H._ResetHistoryLootAsync(false)
        if lootScroll and lootScroll.SetVerticalScroll then
          lootScroll:SetVerticalScroll(0)
        end
      end

      if not selected then
        if UI._historyEmptyLoot then
          UI._historyEmptyLoot:SetText("Select a session to view loot.")
          UI._historyEmptyLoot:Show()
        end
        if lootScroll and lootScroll.SetVerticalScroll then lootScroll:SetVerticalScroll(0) end
        for i = 1, #_historyLootScratch do _historyLootScratch[i] = nil end
        return
      end

      if lootCount then
        if lootView == "deleted" then
          local n = H._CountBucketItems(selected, "hideTop")
          if n and n > 0 then
            lootCount:SetText("Deleted items: " .. tostring(n))
          else
            lootCount:SetText("")
          end
        elseif lootView == "ignored" then
          local n = H._CountBucketItems(selected, "excludeValue")
          if n and n > 0 then
            lootCount:SetText("Ignored items: " .. tostring(n))
          else
            lootCount:SetText("")
          end
        else
          local rc = H._ResetBreakdownCount(selected)
          if rc and rc > 0 and H._GroupTopLootByResetEnabled() then
            lootCount:SetText("Grouped by reset")
          elseif rc and rc > 0 then
            lootCount:SetText("Reset stats available")
          else
            lootCount:SetText("")
          end
        end
      end

      local items = selected.items or {}
      local list = _historyLootScratch
      local nList = 0
      local usingResetBreakdown = false

      if lootView == "top" and H._ResetBreakdownCount(selected) > 0 and H._GroupTopLootByResetEnabled() then
        usingResetBreakdown = true
        local resets = selected.resets or {}
        for rIndex = 1, #resets do
          local reset = resets[rIndex]
          if type(reset) == "table" then
            local resetNo = tonumber(reset.resetNumber) or rIndex
            nList = nList + 1
            local header = list[nList]
            if not header then
              header = {}
              list[nList] = header
            end
            local collapsed = H._IsResetCollapsed(selected, resetNo)
            header.kind = "resetHeader"
            header.resetNumber = resetNo
            header.resetData = reset
            header.collapsed = collapsed
            header.text = "Reset " .. tostring(resetNo)
            header.valueText = H._FormatResetTopLootHeaderMeta(reset, false)
            header.itemID = nil
            header.entry = nil
            header.value = 0
            header.total = 0
            header.count = 0
            header.each = 0
            header.name = ""

            if not collapsed then
              local topLoot = H._BuildResetTopLoot(reset, 0)
              for j = 1, #topLoot do
                local itemID = topLoot[j].itemID
                if not H._IsIgnored(selected, itemID) then
                  local e = topLoot[j].entry or {}
                  local fallback = type(items) == "table" and items[itemID] or nil
                  if type(fallback) == "table" then
                    -- Fill display-only fields from the session item when the reset
                    -- delta only has count/value. This keeps item icons and quality
                    -- coloring identical to normal Top loot rows.
                    if not e.link or e.link == "" then e.link = fallback.link end
                    if not e.name or e.name == "" then e.name = fallback.name end
                    if not e.icon then e.icon = fallback.icon end
                    if e.quality == nil then e.quality = fallback.quality end
                    if not e.lastSource or e.lastSource == "" then e.lastSource = fallback.lastSource end
                  end

                  local cnt = tonumber(e.count) or tonumber(topLoot[j].count) or 1
                  if cnt < 0 then cnt = 0 end
                  local totalValue = tonumber(e.value) or tonumber(topLoot[j].total) or 0
                  local eachValue = tonumber(e.unitValue) or tonumber(topLoot[j].each) or 0
                  if eachValue <= 0 and cnt > 0 then eachValue = totalValue / cnt end

                  local nm = e.name
                  if not nm or nm == "" then nm = H._LinkToName(e.link) end
                  if not nm or nm == "" then nm = "Item" end

                  nList = nList + 1
                  local rec = list[nList]
                  if not rec then
                    rec = {}
                    list[nList] = rec
                  end
                  rec.kind = "resetItem"
                  rec.resetNumber = resetNo
                  rec.itemID = itemID
                  rec.entry = e
                  rec.value = totalValue
                  rec.total = totalValue
                  rec.count = cnt
                  rec.each = eachValue
                  rec.name = string.lower(tostring(nm or ""))
                end
              end
            end
          end
        end
      else
        for itemID, e in pairs(items) do
          local include = false
          if lootView == "deleted" then
            include = H._IsHidden(selected, itemID)
          elseif lootView == "ignored" then
            include = H._IsExcluded(selected, itemID)
          else
            include = not H._IsIgnored(selected, itemID)
          end

          if include then
            nList = nList + 1
            local rec = list[nList]
            if not rec then
              rec = {}
              list[nList] = rec
            end
            local cnt = tonumber(e and e.count) or 1
            if cnt < 0 then cnt = 0 end

            local totalValue = tonumber(e and e.value) or 0
            local eachValue = tonumber(e and e.unitValue) or 0
            if eachValue <= 0 and cnt > 0 then
              eachValue = totalValue / cnt
            end

            local nm = e and e.name
            if not nm or nm == "" then
              nm = H._LinkToName(e and e.link)
            end
            if nm == "" then nm = "Item" end

            rec.kind = "sessionItem"
            rec.itemID = itemID
            rec.entry = e
            rec.value = totalValue
            rec.total = totalValue
            rec.count = cnt
            rec.each = eachValue
            rec.name = string.lower(tostring(nm or ""))
          end
        end
      end

      for i = nList + 1, #list do
        list[i] = nil
      end

      if nList == 0 then
        if UI._historyEmptyLoot then
          local msg = "No loot to display."
          if lootView == "deleted" then msg = "No deleted loot." end
          if lootView == "ignored" then msg = "No ignored loot." end
          UI._historyEmptyLoot:SetText(msg)
          UI._historyEmptyLoot:Show()
        end
        if lootScroll and lootScroll.SetVerticalScroll then lootScroll:SetVerticalScroll(0) end
      end

      local lootSortMode = H._GetHistoryLootSortMode()
      if not usingResetBreakdown then
        H._SortHistoryLootListByMode(list, lootSortMode)
      end

      local w = lootScroll:GetWidth()
      if w and w > 1 then lootContent:SetWidth(w) end
      H.LayoutLootColumns()
      self:_RenderHistoryLootVisible()

      -- Keep history rendering lightweight and reuse addon-owned tables.
    end

    function UI:RefreshHistory()
      if not pHistory or not pHistory:IsShown() then return end

      H._UpdateHistoryHeader()

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

      local sortMode = H._GetHistorySortMode()
      local tmp = _historySessionScratch

      local modeFilter = (UI and UI.GetModeFilter) and UI:GetModeFilter() or nil

      local q = H._Trim(tostring(UI._historySearchText or ""))
      if q ~= "" then q = string.lower(q) end

      local tmpN = 0
      for i = 1, #sessions do
        local sess = sessions[i]
        if sess then
          local include = true
          if modeFilter then include = (tostring(sess.mode or "") == modeFilter) end
          if include then
          local ts = tonumber(sess.endedAt or sess.stoppedAt or sess.startedAt or sess.ts or sess.time) or 0
          local total = tonumber(sess.totalValue or sess.total or sess.value) or 0
          local dur = tonumber(sess.elapsed or sess.duration or sess.timeSpent) or 0
          local gph = tonumber(SessionGPH(sess)) or 0

          local itemsCount = 0
          local _, vTotal = H._CountValuableItems(sess)
          if vTotal and vTotal > 0 then
            itemsCount = vTotal
          else
            local items = sess.items
            if type(items) == "table" then
              for _ in pairs(items) do itemsCount = itemsCount + 1 end
            end
          end

          local dn = nil
          if UI and UI.GetSessionDisplayName then
            dn = UI:GetSessionDisplayName(sess)
          end
          local baseName = dn or sess._wfCustomFarmName or sess.farmName or sess.zone or ""
          local nameKey = string.lower(tostring(baseName))

          local modeKey = string.lower(H._Trim(sess.mode or ""))
          local _, _, livTotal = H._SessTotals(sess)


          if include and q ~= "" then
            local zoneKey = string.lower(tostring(sess.zone or ""))
            local noteKey = string.lower(tostring(sess.note or ""))
            if not (nameKey:find(q, 1, true) or zoneKey:find(q, 1, true) or noteKey:find(q, 1, true) or modeKey:find(q, 1, true)) then
              include = false
            end
          end

          if include then
            tmpN = tmpN + 1
            local rec = tmp[tmpN]
            if not rec then
              rec = {}
              tmp[tmpN] = rec
            end
            rec.sess = sess
            rec.idx = i
            rec.ts = ts
            rec.total = total
            rec.gph = gph
            rec.dur = dur
            rec.items = itemsCount
            rec.liv = livTotal
            rec.mode = modeKey
            rec.name = nameKey
          end
          end
        end
      end
      for i = tmpN + 1, #tmp do
        tmp[i] = nil
      end

      table.sort(tmp, function(a, b)
        if sortMode == "recent_desc" then
          if a.ts ~= b.ts then return a.ts > b.ts end
        elseif sortMode == "recent_asc" then
          if a.ts ~= b.ts then return a.ts < b.ts end

        elseif sortMode == "total_desc" then
          if a.total ~= b.total then return a.total > b.total end
        elseif sortMode == "total_asc" then
          if a.total ~= b.total then return a.total < b.total end

        elseif sortMode == "liv_desc" then
          if a.liv ~= b.liv then return a.liv > b.liv end
        elseif sortMode == "liv_asc" then
          if a.liv ~= b.liv then return a.liv < b.liv end

        elseif sortMode == "gph_desc" then
          if a.gph ~= b.gph then return a.gph > b.gph end
        elseif sortMode == "gph_asc" then
          if a.gph ~= b.gph then return a.gph < b.gph end

        elseif sortMode == "items_desc" then
          if a.items ~= b.items then return a.items > b.items end
        elseif sortMode == "items_asc" then
          if a.items ~= b.items then return a.items < b.items end

        elseif sortMode == "dur_desc" then
          if a.dur ~= b.dur then return a.dur > b.dur end
        elseif sortMode == "dur_asc" then
          if a.dur ~= b.dur then return a.dur < b.dur end

        elseif sortMode == "mode_az" then
          if a.mode ~= b.mode then return a.mode < b.mode end
        elseif sortMode == "mode_za" then
          if a.mode ~= b.mode then return a.mode > b.mode end

        elseif sortMode == "name_az" then
          if a.name ~= b.name then return a.name < b.name end
        elseif sortMode == "name_za" then
          if a.name ~= b.name then return a.name > b.name end
        end

        -- tie-breakers: newest first, then newest insert
        if a.ts ~= b.ts then return a.ts > b.ts end
        return a.idx > b.idx
      end)

      local list = _historySessionList
      for i = 1, #tmp do list[i] = tmp[i].sess end
      for i = #tmp + 1, #list do list[i] = nil end

      local w = scroll:GetWidth()
      if w and w > 1 then sc:SetWidth(w) end

local metricNow = H._GetDisplayMetric()
if hVal then
  if metricNow == "liv" then hVal:SetText("LIV") else hVal:SetText("GPH") end
end
H.LayoutSessionColumns()


      local showN = math.min(#list, 500)
      for i = 1, math.max(showN, #rows) do
        local row = rows[i]
        local sess = list[i]

        if sess and i <= showN then
          row = H.EnsureRow(i)
          row:Show()
          row:SetPoint("TOPLEFT", 0, -((i - 1) * rowH))
          row:SetPoint("TOPRIGHT", 0, -((i - 1) * rowH))
          row._sess = sess

          local total = tonumber(sess.totalValue or sess.total or sess.value) or 0
          local gph = SessionGPH(sess)

          local dn = nil
          if UI and UI.GetSessionDisplayName then
            dn = UI:GetSessionDisplayName(sess)
          end
          local baseName = dn or sess._wfCustomFarmName or sess.farmName or sess.zone or "Session"
          local mode = H._WF_History_ModeName(sess.mode or "Default")

          local suffix = ""
          local mCount = tonumber(sess._wfMergedCount) or 1
          if mCount and mCount > 1 then
            suffix = " |cff7f7f7f• merged x" .. tostring(mCount) .. "|r"
          end
          local resetCount = H._ResetBreakdownCount(sess)
          if resetCount > 0 then
            suffix = suffix .. " |cff7f7f7f• " .. tostring(resetCount) .. " resets|r"
          end

          row.farm:SetText(tostring(baseName) .. suffix)
          row.mode:SetText(mode or "Default")

local _, totalItems = H._CountAllItems(sess)
local dateStr = FormatTime(sess.startedAt or sess.endedAt or sess.stoppedAt)

local t0 = tonumber(sess.startedAt) or 0
local t1 = tonumber(sess.endedAt or sess.stoppedAt) or t0
local dur = tonumber(sess.duration) or tonumber(sess.elapsed) or (t1 - t0)
if dur < 0 then dur = 0 end
local durStr = H._FormatDuration(dur)

local metric = H._GetDisplayMetric()
local _, _, livTotal = H._SessTotals(sess)

local valStr = ""
if metric == "liv" then
  valStr = Money(livTotal)
else
  valStr = tostring(Money(Util:Round(gph)) or "")
  valStr = valStr:gsub("%s*/hr","")
end

row.val:SetText(valStr)
row.dur:SetText(durStr)
row.date:SetText(dateStr)

          local isSel = (selected == sess)
          row.bg:SetAlpha(isSel and 0.08 or 0)
          if row.accent then
            if isSel then row.accent:Show() else row.accent:Hide() end
          end
        elseif row then
          row._sess = nil
          row:Hide()
        end
      end
      sc:SetHeight(showN * rowH)

      -- Empty state (Sessions list)
      if UI._historyEmptySessions then
        if #list > 0 then
          UI._historyEmptySessions:Hide()
        else
          if #sessions > 0 then
            UI._historyEmptySessions:SetText("No sessions match your search/filters.")
          else
            UI._historyEmptySessions:SetText("No sessions yet.\nStart tracking a farm to populate History.")
          end
          UI._historyEmptySessions:Show()
        end
      end

      local show = UI._pendingSelectSession or selected
      if show then
        selected = show
        UI._pendingSelectSession = nil

        -- Selected state: hide the empty-details helper + use accent for the title line.
        if emptyMeta then emptyMeta:Hide() end
        if name then name:SetTextColor(C4(ACCENT, ACCENT)) end
        H._SetShareButtonEnabled(true)

        -- Ensure the selected row is visible in the Sessions list,
        -- but do not snap it to the top if it is already on screen.
        do
          local sb = scroll and scroll.ScrollBar
          if sb and list and type(list) == "table" then
            local idx = nil
            for i = 1, #list do
              if list[i] == selected then idx = i break end
            end
            if idx then
              local current = sb.GetValue and sb:GetValue() or 0
              local desired = current
              local maxScroll = 0
              local ch = sc and sc.GetHeight and sc:GetHeight() or 0
              local vh = scroll and scroll.GetHeight and scroll:GetHeight() or 0
              if ch > 0 and vh > 0 then
                maxScroll = ch - vh
                if maxScroll < 0 then maxScroll = 0 end

                local rowTop = (idx - 1) * rowH
                local rowBottom = rowTop + rowH
                local viewTop = current
                local viewBottom = current + vh

                if rowTop < viewTop then
                  desired = rowTop
                elseif rowBottom > viewBottom then
                  desired = rowBottom - vh
                end
              end

              if desired > maxScroll then desired = maxScroll end
              if desired < 0 then desired = 0 end
              if math.abs((desired or 0) - (current or 0)) > 0.5 then
                sb:SetValue(desired)
              end
            end
          end
        end

        local gph = SessionGPH(show)
        local total = tonumber(show.totalValue or show.total or show.value) or 0
        local _, _, livTotal = H._SessTotals(show)
        local rawGold = tonumber(show.moneyGained or show.lootedGoldValue or show.lootedGold or show.goldLooted or show.rawGold or show.money or show.gold) or 0
        local vU, vT = H._CountValuableItems(show)

local z = H._Trim(show.zone or "")
if z == "" and WF and WF.DB and WF.DB.GetFarm and show.farmId then
  local f = WF.DB:GetFarm(show.farmId)
  if f then z = H._Trim(f.zone or "") end
end

local farmName = H._Trim(show._wfCustomFarmName or show.farmName or "")
if farmName == "" and WF and WF.DB and WF.DB.GetFarm and show.farmId then
  local f = WF.DB:GetFarm(show.farmId)
  if f then farmName = H._Trim(f.name or "") end
end
if farmName == "" then farmName = z end

local displayFarm = farmName
if displayFarm == "" then displayFarm = "Unknown" end
name:SetText(displayFarm)

local detailSpecs = _historyMetaLines
for i = 1, #detailSpecs do detailSpecs[i] = nil end
local detailCount = 0
function H._PushLine(label, value, valueHex)
  detailCount = detailCount + 1
  detailSpecs[detailCount] = {
    kind = "single",
    text = H._HistoryMetaLine(label, value, valueHex),
  }
end
function H._PushRaw(text)
  detailCount = detailCount + 1
  detailSpecs[detailCount] = {
    kind = "single",
    text = tostring(text or ""),
  }
end
function H._PushSpacer(height)
  detailCount = detailCount + 1
  detailSpecs[detailCount] = {
    kind = "spacer",
    height = tonumber(height) or 10,
  }
end
function H._PushPair(leftLabel, leftValue, rightLabel, rightValue, leftHex, rightHex)
  detailCount = detailCount + 1
  detailSpecs[detailCount] = {
    kind = "double",
    leftText = H._HistoryMetaLine(leftLabel, leftValue, leftHex),
    rightText = H._HistoryMetaLine(rightLabel, rightValue, rightHex),
  }
end
if H._Trim(show._wfSharedFrom or "") ~= "" then
  H._PushLine("Shared by", tostring(show._wfSharedFrom))
end
if z ~= "" and z ~= displayFarm then
  H._PushLine("Zone", z)
end

  local mCount = tonumber(show._wfMergedCount) or 1
  if mCount and mCount > 1 then
    H._PushLine("Merged runs", tostring(mCount))
    local zt = show._wfMergedZones
    if type(zt) == "table" then
      local zs = _historyMergedZones
      local zN = 0
      for zn in pairs(zt) do
        if H._Trim(zn) ~= "" then zN = zN + 1 zs[zN] = zn end
      end
      for i = zN + 1, #zs do zs[i] = nil end
      table.sort(zs)
      if zN > 0 then
        local preview = table.concat(zs, ", ", 1, zN)
        if string.len(preview) > 80 then
          preview = string.sub(preview, 1, 77) .. "…"
        end
        H._PushLine("Merged zones", preview)
      end
    end
  end

local startedAt = tonumber(show.startedAt or show.endedAt or show.stoppedAt)
local sessionDate = "—"
local sessionTime = "—"
if startedAt then
  sessionDate = date("%m/%d", startedAt)
  sessionTime = date("%H:%M", startedAt)
end
H._PushPair("Date", sessionDate, "Time", sessionTime)
H._PushLine("Duration", H._FormatDuration(show.elapsed))

local note = H._Trim(show.note or "")
if note ~= "" then
  H._PushLine("Note", note)
end

      local iU, iT = H._CountAllItems(show)
      if iT and iT > 0 then
        H._PushLine("Items looted", tostring(iT))
        H._PushLine("Item types", tostring(iU))
      end

if rawGold > 0 then
  H._PushLine("Gold Looted", Money(rawGold))
end

if total and total > 0 then
  H._PushLine("Value", Money(livTotal))
else
  H._PushLine("Value", "0")
end

local rateStr = tostring(Money(Util:Round(gph)) or "")
rateStr = rateStr:gsub("%s*/hr", "")
rateStr = rateStr:gsub("/hr", "")
H._PushLine("Gold per Hour", rateStr)

local resetCount = H._ResetBreakdownCount(show)
if resetCount > 0 then
  H._PushSpacer(14)
  H._PushRaw(H._HistoryMetaColor(HISTORY_META_HEX_ACCENT, "Reset breakdown"))

  local resets = show.resets
  local totalResetElapsed = 0
  local totalResetValue = 0
  if type(resets) == "table" then
    for i = 1, #resets do
      local r = resets[i]
      if type(r) == "table" then
        totalResetElapsed = totalResetElapsed + math.max(0, tonumber(r.elapsed) or 0)
        totalResetValue = totalResetValue + math.max(0, tonumber(r.totalValue) or 0)
      end
    end
  end

  local avgResetTime = 0
  local avgResetGPH = 0
  if resetCount > 0 then
    avgResetTime = totalResetElapsed / resetCount
  end
  if totalResetElapsed > 0 then
    avgResetGPH = (totalResetValue / totalResetElapsed) * 3600
  end

  H._PushLine("Total resets", tostring(resetCount))
  H._PushPair("Average length", H._FormatDuration(avgResetTime), "Average GPH", H._FormatMoneyShort(avgResetGPH))

  if type(resets) == "table" then
    for i = 1, #resets do
      local r = resets[i]
      if type(r) == "table" then
        local resetNo = tonumber(r.resetNumber) or i
        local elapsed = H._FormatDuration(tonumber(r.elapsed) or 0)
        local gph = H._FormatMoneyShort(H._ResetGPH(r))
        local liv = H._FormatMoneyShort(H._ResetLIV(r))
        local items = tonumber(r.itemCount) or 0
        H._PushLine(
          "Reset " .. tostring(resetNo),
          H._HistoryMetaColor(HISTORY_META_HEX_VALUE, elapsed) .. "  " ..
          H._HistoryMetaColor(HISTORY_META_HEX_LABEL, "GPH:") .. " " ..
          H._HistoryMetaColor(HISTORY_META_HEX_VALUE, gph) .. "  " ..
          H._HistoryMetaColor(HISTORY_META_HEX_LABEL, "LIV:") .. " " ..
          H._HistoryMetaColor(HISTORY_META_HEX_VALUE, liv) .. "  " ..
          H._HistoryMetaColor(HISTORY_META_HEX_LABEL, "Items:") .. " " ..
          H._HistoryMetaColor(HISTORY_META_HEX_VALUE, tostring(items))
        )
      end
    end
  end
end

H._RenderDetailRows(detailSpecs, detailCount)
      else
	        if name then
	          name:SetText("Select a session")
	          name:SetTextColor(C4(MUTED, MUTED))
	        end
	        H._SetShareButtonEnabled(false)
	        if meta then meta:SetText("") end
	        H._HideAllDetailRows()
	        if metaContent then metaContent:SetHeight(1) end
	        if emptyMeta then emptyMeta:Show() end
	        if metaScroll and metaScroll.SetVerticalScroll then metaScroll:SetVerticalScroll(0) end
      end

      UI:RefreshHistoryLoot()
    end
    pHistory:SetScript("OnShow", function()
      UI:_Layout()
      UI:RefreshHistory()
    end)

    self._layout.history = {
      page = pHistory,
      left = left,
      right = right,
      meta = meta,
    }
  end
end
