-- UI/HelpPages/Farms.lua
-- What Farm? :: HelpWindow - Farms 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.Farms = function(ctx)
  local UI = ctx.UI

  local WF = ctx.WF
  local Widgets = ctx.Widgets

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

  local RebuildHistoryAggregates = ctx.RebuildHistoryAggregates

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

  local SafeNow = ctx.SafeNow
  local SetWaypoint = ctx.SetWaypoint

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

  local EXPANSIONS = ctx.EXPANSIONS
  local FARM_EXP_OPTS = EXPANSIONS
  do
    if type(EXPANSIONS) == "table" then
      local tmp = {}
      local hasUnknown = false
      for i = 1, #EXPANSIONS do
        local v = EXPANSIONS[i]
        if v ~= "Any" then
          tmp[#tmp + 1] = v
          if v == "Unknown" then hasUnknown = true end
        end
      end
      if #tmp > 0 then
        if not hasUnknown then table.insert(tmp, 1, "Unknown") end
        FARM_EXP_OPTS = tmp
      end
    end
  end
  local FARM_TYPES = ctx.FARM_TYPES
  local FARM_AIMS = ctx.FARM_AIMS

-- -----------------------------------------------------
  -- FARMS
  -- -----------------------------------------------------
  local pFarms = MakePage("Farms", "Farms")
  do
    -- Safety: prevent page children from drawing outside the page when the window is resized very small.
    if pFarms and pFarms.SetClipsChildren then pFarms:SetClipsChildren(true) end

    -- -------------------------------------------------
    -- Search + Filters (library controls)
    -- -------------------------------------------------
    local F = {}
    F.search = MakeInput(pFarms, 360, 24)
    F.search:SetText("")

  -- Search placeholder (cleaner than external hint text)
  F.searchPH = MakeMutedText(F.search, "Search Farms")
  F.searchPH:SetPoint("LEFT", 10, 0)
  F.searchPH:SetPoint("RIGHT", -10, 0)
  F.searchPH:SetJustifyH("LEFT")
  F.searchPH:SetAlpha(0.70)

  local function UpdateSearchPH()
    local hasText = (F.search:GetText() or "") ~= ""
    local hasFocus = F.search:HasFocus() and true or false
    F.searchPH:SetShown((not hasText) and (not hasFocus))
  end

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

    local target = frame._edit or frame._btn or frame._check or frame._slider or frame
    if not target then return end

    target._wfTipTitle = title
    target._wfTipBody = body
    target._wfTipAnchor = anchorPoint or "ANCHOR_RIGHT"

    if target._wfTipHooked or not target.HookScript then
      return
    end
    target._wfTipHooked = true

    target:HookScript("OnEnter", function(self)
      if not GameTooltip then return end
      local tipTitle = self._wfTipTitle
      local tipBody = self._wfTipBody
      if (not tipTitle or tipTitle == "") and (not tipBody or tipBody == "") then return end

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

      if tipTitle and tipTitle ~= "" then
        GameTooltip:AddLine(tipTitle, ACCENT[1], ACCENT[2], ACCENT[3], true)
      end
      if type(tipBody) == "table" then
        for i = 1, #tipBody do
          local line = tostring(tipBody[i] or "")
          if line ~= "" then
            GameTooltip:AddLine(line, 0.78, 0.78, 0.78, true)
          end
        end
      elseif tipBody and tipBody ~= "" then
        GameTooltip:AddLine(tostring(tipBody), 0.78, 0.78, 0.78, true)
      end
      GameTooltip:Show()
    end)

    target:HookScript("OnLeave", function(self)
      if GameTooltip and GameTooltip:IsOwned(self) then
        GameTooltip:Hide()
      end
    end)
  end

  local GROUP_OPTS = { "None", "Expansion", "Type", "Aim" }

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

  local _EXPANSION_DISPLAY = {
    ["BfA"] = "Battle for Azeroth",
    ["TWW"] = "The War Within",
    ["MoP"] = "Mists of Pandaria",
    ["WoD"] = "Warlords of Draenor",
    ["WotLK"] = "Wrath of the Lich King",
    ["Wrath"] = "Wrath of the Lich King",
    ["Cata"] = "Cataclysm",
    ["DF"] = "Dragonflight",
    ["SL"] = "Shadowlands",
    ["TBC"] = "The Burning Crusade",
    ["BC"] = "The Burning Crusade",
  }

  local _TYPE_DISPLAY = {
    ["OpenWorld"] = "Open World",
    ["Openworld"] = "Open World",
  }

  local function _HumanizeFarmLabel(v)
    v = _TrimDisplayValue(v)
    if v == "" then return v end
    v = v:gsub("_", " ")
    v = v:gsub("%-", " ")
    v = v:gsub("(%l)(%u)", "%1 %2")
    v = v:gsub("(%a)(%d)", "%1 %2")
    v = v:gsub("(%d)(%a)", "%1 %2")
    v = v:gsub("%s+", " ")
    return v
  end

  local function _DisplayExpansion(v)
    v = _TrimDisplayValue(v)
    if v == "" then return v end
    return _EXPANSION_DISPLAY[v] or _HumanizeFarmLabel(v)
  end

  local function _DisplayFarmType(v)
    v = _TrimDisplayValue(v)
    if v == "" then return v end
    return _TYPE_DISPLAY[v] or _HumanizeFarmLabel(v)
  end

  local function _DisplayFarmAim(v)
    v = _TrimDisplayValue(v)
    if v == "" then return v end
    return _HumanizeFarmLabel(v)
  end

  local function BuildExpansionDropdownItems()
    local items = {}
    local src = EXPANSIONS or {}
    for i = 1, #src do
      local v = tostring(src[i] or "")
      if v ~= "" then
        local display = _DisplayExpansion(v)
        local body
        if v == "Any" then
          body = "Show farms from every expansion."
        elseif v == "Unknown" then
          body = "Only show farms that still need an expansion assigned."
        else
          body = "Show only farms from " .. display .. "."
        end
        items[#items + 1] = { text = display, value = v, tooltipTitle = "Expansion", tooltipBody = body }
      end
    end
    return items
  end

  local function BuildTypeDropdownItems()
    local items = {}
    local src = FARM_TYPES or {}
    for i = 1, #src do
      local v = tostring(src[i] or "")
      if v ~= "" then
        local display = _DisplayFarmType(v)
        local body
        if v == "Any" then
          body = "Show every farm type."
        elseif v == "Unknown" then
          body = "Only show farms that still need a type assigned."
        else
          body = "Show only farms tagged as " .. display .. "."
        end
        items[#items + 1] = { text = display, value = v, tooltipTitle = "Type", tooltipBody = body }
      end
    end
    return items
  end

  local function BuildGroupDropdownItems()
    local items = {}
    for i = 1, #GROUP_OPTS do
      local v = GROUP_OPTS[i]
      local body = "Leave the farm list ungrouped."
      if v == "Expansion" then
        body = "Group farms into expansion sections."
      elseif v == "Type" then
        body = "Group farms by farm type."
      elseif v == "Aim" then
        body = "Group farms by aim or purpose."
      end
      items[#items + 1] = { text = v, value = v, tooltipTitle = "Group", tooltipBody = body }
    end
    return items
  end

    F.topArea = CreateFrame("Frame", nil, pFarms)

    F.filterBar = CreateFrame("Frame", nil, pFarms)
    -- IMPORTANT: height must include 2 full combo rows, otherwise it overlaps the list below.
    F.filterBar:SetHeight(124)

    F.fExp = MakeCombo(F.filterBar, "Expansion",
      function() return WF.DB:GetSetting("farmFilterExpansion") or "Any" end,
      function(v) WF.DB:SetSetting("farmFilterExpansion", v); UI:RefreshFarms() end,
      BuildExpansionDropdownItems
    )

    F.fType = MakeCombo(F.filterBar, "Type",
      function() return WF.DB:GetSetting("farmFilterType") or "Any" end,
      function(v) WF.DB:SetSetting("farmFilterType", v); UI:RefreshFarms() end,
      BuildTypeDropdownItems
    )

    F.ckZone = MakeCheck(F.filterBar, "Current zone only",
      function() return WF.DB:GetSetting("farmFilterCurrentZoneOnly") end,
      function(v) WF.DB:SetSetting("farmFilterCurrentZoneOnly", v); UI:RefreshFarms() end
    )

    F.ckArchived = MakeCheck(F.filterBar, "Show archived",
      function() return WF.DB:GetSetting("farmShowArchived") end,
      function(v) WF.DB:SetSetting("farmShowArchived", v); UI:RefreshFarms() end
    )
    F.fGroup = MakeCombo(F.filterBar, "Group",
      function() return WF.DB:GetSetting("farmGroup") or "None" end,
      function(v) WF.DB:SetSetting("farmGroup", v); UI:RefreshFarms() end,
      BuildGroupDropdownItems
    )

    -- Sorting is handled via clickable column headers (Farm/Aim/Type/Zone/Avg).
    -- Keep a hidden stub so older layout code that expects fSort won't error.
    F.fSort = CreateFrame("Frame", nil, F.filterBar)
    F.fSort:SetSize(1, 1)
    F.fSort:Hide()

    F.bReset = Widgets:Button(pFarms, "Reset", 86, 24)
    F.bResync = Widgets:Button(pFarms, "Resync", 96, 24)
    F.tResync = MakeMutedText(pFarms, "Avg data uses cached History values until you resync.")
    F.tResync:SetJustifyH("LEFT")
    F.tResync:SetAlpha(0.92)
    F.bReset:SetScript("OnClick", function()
      if WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting("farmFilterExpansion", "Any")
        WF.DB:SetSetting("farmFilterType", "Any")
        WF.DB:SetSetting("farmFilterCurrentZoneOnly", false)
        WF.DB:SetSetting("farmGroup", "None")
        WF.DB:SetSetting("farmSortKey", "name")
        WF.DB:SetSetting("farmSortDir", "asc")
        WF.DB:SetSetting("farmSort", "Name (A-Z)") -- legacy
        WF.DB:SetSetting("farmShowArchived", false)
      end
      if F.ckZone and F.ckZone.SetChecked then F.ckZone:SetChecked(false) end
      if F.ckArchived and F.ckArchived.SetChecked then F.ckArchived:SetChecked(false) end
      if F.search and F.search.SetText then F.search:SetText("") end
      if UpdateSearchPH then UpdateSearchPH() end
      UI:RefreshFarms()
    end)

    -- Fallback layout (UI:_Layout refines widths/anchors)
    F.fExp:SetWidth(200); if F.fExp._btn then F.fExp._btn:SetWidth(200) end
    F.fType:SetWidth(200); if F.fType._btn then F.fType._btn:SetWidth(200) end
    F.fGroup:SetWidth(200); if F.fGroup._btn then F.fGroup._btn:SetWidth(200) end
    F.fSort:SetWidth(1)

    F.fExp:SetPoint("TOPLEFT", 0, 0)
    F.fType:SetPoint("LEFT", F.fExp, "RIGHT", 12, 0)

    F.fGroup:SetPoint("TOPLEFT", F.fExp, "BOTTOMLEFT", 0, -12)
    F.fSort:SetPoint("LEFT", F.fGroup, "RIGHT", 12, 0)

    F.topArea:SetPoint("TOPLEFT", F.search, "TOPLEFT", 0, 0)
    F.topArea:SetPoint("BOTTOMRIGHT", F.filterBar, "BOTTOMRIGHT", 0, 0)

    -- Move checkboxes beside the Group dropdown (not beside Type)
    -- Keep them side-by-side (not stacked). IMPORTANT: anchor the 2nd checkbox
    -- to the first checkbox's label (Text) so we account for label width.
    local groupAnchor = (F.fGroup and F.fGroup._btn) or F.fGroup
    F.ckZone:SetPoint("TOPLEFT", groupAnchor, "TOPRIGHT", 16, -4)
    if F.ckArchived and F.ckZone and F.ckZone.Text then
      F.ckArchived:SetPoint("LEFT", F.ckZone.Text, "RIGHT", 18, 0)
      F.ckArchived:SetPoint("TOP", F.ckZone, "TOP", 0, 0)
    else
      F.ckArchived:SetPoint("LEFT", F.ckZone, "RIGHT", 18, 0)
    end

    -- Reset / Resync sit next to the search box
    F.bReset:SetPoint("LEFT", F.search, "RIGHT", 10, 0)
    F.bReset:SetPoint("TOP", F.search, "TOP", 0, 0)
    F.bResync:SetPoint("LEFT", F.bReset, "RIGHT", 8, 0)
    F.bResync:SetPoint("TOP", F.search, "TOP", 0, 0)
    F.tResync:SetPoint("TOPLEFT", F.search, "BOTTOMLEFT", 2, -6)

    AttachTip(F.search, "Search Farms", {
      "Search by farm name, zone, notes, or tags.",
      "This only filters the library view — it does not edit the farm itself.",
    }, "ANCHOR_BOTTOMRIGHT")
    AttachTip(F.fExp, "Expansion Filter", "Filter the farm library by expansion. Dropdown entries now explain each option.")
    AttachTip(F.fType, "Type Filter", "Filter the farm library by farm type. Dropdown entries include quick descriptions.")
    AttachTip(F.fGroup, "Group", "Group the farm library by nothing, expansion, type, or aim.")
    AttachTip(F.ckZone, "Current Zone Only", "Only show farms that match your current zone or map.")
    AttachTip(F.ckArchived, "Show Archived", "Include archived farms in the library list.")
    AttachTip(F.bReset, "Reset Filters", "Clear the current Farms filters and restore the default sort.")
    AttachTip(F.bResync, "Resync Averages", {
      "Rebuild farm averages from saved History values.",
      "This does not reprice runs to current market values.",
      "Use this after major history edits or imports.",
    })

    -- Keep the top search/actions row stable, but let Search Farms shrink before
    -- Reset/Resync ever drift into the right details rail.
    local function _LockFarmsSearchAndReset()
      if not F.search or not F.search.GetPoint then return end

      local p1, rel1, rp1, x1, y1 = F.search:GetPoint(1)
      if p1 then
        F.search:ClearAllPoints()
        F.search:SetPoint(p1, rel1, rp1, x1, y1)
      end

      local searchW = 360
      local searchMin = 140
      local rightPad = 14
      local gap1 = 10
      local gap2 = 8
      local resetW = (F.bReset and F.bReset.GetWidth and F.bReset:GetWidth()) or 86
      local resyncW = (F.bResync and F.bResync.GetWidth and F.bResync:GetWidth()) or 96
      local searchLeft = F.search.GetLeft and F.search:GetLeft() or nil
      local cutoff = nil

      local lay = UI and UI._layout and UI._layout.farms or nil
      local rightPanel = lay and lay.right or nil
      if rightPanel and rightPanel.GetLeft then
        cutoff = rightPanel:GetLeft()
      end
      if (not cutoff) and lay and lay.listPanel and lay.listPanel.GetRight then
        cutoff = lay.listPanel:GetRight()
      end

      if searchLeft and cutoff and cutoff > searchLeft then
        local maxSearch = math.floor(cutoff - searchLeft - rightPad - gap1 - resetW - gap2 - resyncW)
        if maxSearch and maxSearch > 0 then
          searchW = math.min(searchW, maxSearch)
          searchW = math.max(searchMin, searchW)
        end
      end

      F.search:SetWidth(searchW)
      F.search:SetHeight(24)

      if F.bReset then
        F.bReset:ClearAllPoints()
        F.bReset:SetPoint("LEFT", F.search, "RIGHT", gap1, 0)
        F.bReset:SetPoint("TOP", F.search, "TOP", 0, 0)
      end
      if F.bResync then
        F.bResync:ClearAllPoints()
        F.bResync:SetPoint("LEFT", F.bReset, "RIGHT", gap2, 0)
        F.bResync:SetPoint("TOP", F.search, "TOP", 0, 0)
      end
      if F.tResync then
        F.tResync:ClearAllPoints()
        F.tResync:SetPoint("TOPLEFT", F.search, "BOTTOMLEFT", 2, -6)
        F.tResync:SetPoint("TOPRIGHT", F.bResync, "BOTTOMRIGHT", 0, -6)
      end
    end

    _LockFarmsSearchAndReset()

    -- Re-apply after the centralized layout (runs on resize/tab switches).
    if pFarms and pFarms.HookScript then
      pFarms:HookScript("OnShow", function()
        _LockFarmsSearchAndReset()
      end)
    end

    if UI and UI._Layout and hooksecurefunc and not UI._FarmsSearchResetHooked then
      UI._FarmsSearchResetHooked = true
      hooksecurefunc(UI, "_Layout", function()
        if pFarms and pFarms.IsShown and pFarms:IsShown() then
          _LockFarmsSearchAndReset()
        end
      end)
    end

    -- Layout is handled centrally by UI:_Layout() (HelpWindow.lua).
    -- Do not install local OnSizeChanged handlers here; they fight the main layout and cause jitter while resizing.

-- -------------------------------------------------
    -- Library list
    -- -------------------------------------------------
    local L = {}
    L.listPanel = MakePanel(pFarms, PANEL)
    if L.listPanel and L.listPanel.SetClipsChildren then L.listPanel:SetClipsChildren(true) end
    L.listTitle = MakeSectionTitle(L.listPanel, "Farm library")
    L.listTitle:SetPoint("TOPLEFT", 12, -10)

    L.listCount = MakeMutedText(L.listPanel, "")
    L.listCount:SetPoint("TOPRIGHT", -12, -12)
    L.listCount:SetJustifyH("RIGHT")

    L.listDiv = MakeDivider(L.listPanel)
    L.listDiv:SetPoint("TOPLEFT", 12, -34)
    L.listDiv:SetPoint("TOPRIGHT", -12, -34)

    -- Column headers (match Overview)
    L.colHeader = CreateFrame("Frame", nil, L.listPanel)
    L.colHeader:SetHeight(22)
    L.colHeader:SetPoint("TOPLEFT", 6, -40)
    L.colHeader:SetPoint("TOPRIGHT", -28, -40)

    -- Header row background (match Overview)
    L.colHeader.bg = L.colHeader:CreateTexture(nil, "BACKGROUND")
    L.colHeader.bg:SetAllPoints(L.colHeader)
    L.colHeader.bg:SetTexture("Interface/Buttons/WHITE8x8")
    L.colHeader.bg:SetVertexColor(1, 1, 1, 0.06)

    local function MakeFarmHeaderText(text)
      local fs = L.colHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalSmall")
      fs:SetText(text or "")
      fs:SetTextColor(C4(ACCENT, ACCENT))
      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
      return fs
    end

    L.hName = MakeFarmHeaderText("Farm")
    L.hAim  = MakeFarmHeaderText("Details")
    L.hType = MakeFarmHeaderText("Type")
    L.hExp  = MakeFarmHeaderText("Zone")
    L.hGph  = MakeFarmHeaderText("Gold per Hour")

    L.hName:SetJustifyH("LEFT")
    L.hAim:SetJustifyH("LEFT")
    L.hType:SetJustifyH("LEFT")
    L.hExp:SetJustifyH("LEFT")
    L.hGph:SetJustifyH("RIGHT")


    -- Click-to-sort column headers (replaces the Sort dropdown)
    local _FARM_HDR = {
      name = "Farm",
      aim  = "Details",
      type = "Type",
      zone = "Zone",
      gph  = "Gold per Hour",
    }

    local function _DefaultSortDir(key)
      if key == "gph" then return "desc" end
      return "asc"
    end

    local function _GetFarmSort()
      local key = WF.DB:GetSetting("farmSortKey")
      local dir = WF.DB:GetSetting("farmSortDir")

      if not key or key == "" then
        local legacy = WF.DB:GetSetting("farmSort") or "Name (A-Z)"
        if legacy == "GPH (high)" then
          key, dir = "gph", "desc"
        elseif legacy == "Type" then
          key, dir = "aim", "asc"
        elseif legacy == "Expansion" then
          -- legacy "Expansion" now maps to Zone sorting for the library list
          key, dir = "zone", "asc"
        else
          key, dir = "name", "asc"
        end
      end

      if key == "type" then
        key = "aim"
      end

      if dir ~= "asc" and dir ~= "desc" then
        dir = _DefaultSortDir(key)
      end

      -- One-time migrate if missing
      if WF.DB and WF.DB.SetSetting then
        if (WF.DB:GetSetting("farmSortKey") or "") == "" then WF.DB:SetSetting("farmSortKey", key) end
        if (WF.DB:GetSetting("farmSortDir") or "") == "" then WF.DB:SetSetting("farmSortDir", dir) end
      end

      return key, dir
    end

    local function _FarmsMetric()
      if not WF or not WF.DB or not WF.DB.GetSetting then return "gph" end
      return tostring(WF.DB:GetSetting("overviewLast5Metric") or "gph"):lower()
    end

    local function _UpdateSortHeaders(hoverKey)
      local key, dir = _GetFarmSort()

      -- Keep the header copy stable; only the column widths should tighten.
      _FARM_HDR.gph = (_FarmsMetric() == "liv") and "Avg LIV" or "Gold per Hour"

      local function set(label, base, k)
        -- Do NOT append unicode arrow glyphs here.
        -- Some fonts render them as a "missing glyph" box (the exact "box" you keep seeing).
        -- Active sort is indicated by accent color; direction still toggles on click.
        label:SetText(base)
        label:SetTextColor(C4(ACCENT, ACCENT))
      end

      set(L.hName, _FARM_HDR.name, "name")
      set(L.hAim,  _FARM_HDR.aim,  "aim")
      set(L.hType, _FARM_HDR.type, "type")
      set(L.hExp,  _FARM_HDR.zone, "zone")
      set(L.hGph,  _FARM_HDR.gph,  "gph")
    end

    local function _SetFarmSort(key)
      local curKey, curDir = _GetFarmSort()
      local dir

      if key == curKey then
        dir = (curDir == "asc") and "desc" or "asc"
      else
        dir = _DefaultSortDir(key)
      end

      if WF.DB and WF.DB.SetSetting then
        WF.DB:SetSetting("farmSortKey", key)
        WF.DB:SetSetting("farmSortDir", dir)

        -- Legacy mirror (harmless, keeps older saved logic stable)
        if key == "name" and dir == "asc" then
          WF.DB:SetSetting("farmSort", "Name (A-Z)")
        elseif key == "gph" and dir == "desc" then
          WF.DB:SetSetting("farmSort", "GPH (high)")
        elseif key == "aim" and dir == "asc" then
          WF.DB:SetSetting("farmSort", "Type")
        elseif key == "zone" and dir == "asc" then
          WF.DB:SetSetting("farmSort", "Expansion")
        else
          WF.DB:SetSetting("farmSort", "Name (A-Z)")
        end
      end

      _UpdateSortHeaders()
      UI:RefreshFarms()
    end

    local headerBtns = {}


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

    -- -------------------------------------------------
    -- Farm Library "View Mode"
    -- Right-click Gold per Hour column header to switch which farm mode the
    -- averages are calculated from (e.g., Mats vs Raw Gold).
    -- -------------------------------------------------

    local function _ActiveFarmMode()
      if WF and WF.DB and WF.DB.GetActiveProfileName then
        return tostring(WF.DB:GetActiveProfileName() or "Default")
      end
      return "Default"
    end


    local function _FarmsViewMode()
      local active = tostring(_ActiveFarmMode() or "Default")
      local pref = (WF and WF.DB and WF.DB.GetSetting) and (WF.DB:GetSetting("farmLibraryViewMode") or "Active") or "Active"
      pref = tostring(pref or "Active")
      if pref == "" or pref == "Active" then
        return active, true
      end
      return pref, false
    end

    local function _ListKnownFarmModes(db)
      local out, seen = {}, {}

      local active = tostring(_ActiveFarmMode() or "Default")
      if active ~= "" then
        seen[active] = true
        out[#out + 1] = active
      end

      if db and db.history and type(db.history.sessions) == "table" then
        for i = 1, #db.history.sessions do
          local s = db.history.sessions[i]
          if type(s) == "table" then
            local m = tostring(s.mode or "")
            if m ~= "" and not seen[m] then
              seen[m] = true
              out[#out + 1] = m
            end
          end
        end
      end

      if db and type(db.farmStats) == "table" then
        for _, fs in pairs(db.farmStats) do
          if type(fs) == "table" and type(fs.modes) == "table" then
            for k in pairs(fs.modes) do
              local m = tostring(k or "")
              if m ~= "" and not seen[m] then
                seen[m] = true
                out[#out + 1] = m
              end
            end
          end
        end
      end

      table.sort(out, function(a, b) return tostring(a):lower() < tostring(b):lower() end)
      return out
    end

    local function _RebuildHistoryAggregatesForFarm(db, farmId)
      if not db or not farmId then return end
      db.history = db.history or {}
      db.settings = db.settings or {}
      local h = db.history

      h.totalsByFarm = h.totalsByFarm or {}
      h.totalsByFarmMode = h.totalsByFarmMode or {}
      h.lastRunByFarm = h.lastRunByFarm or {}

      -- clear this farm's buckets before rebuilding
      h.totalsByFarm[farmId] = nil
      h.totalsByFarmMode[farmId] = nil
      h.lastRunByFarm[farmId] = nil

      local sessions = h.sessions
      if type(sessions) ~= "table" then return end

      local byMode = {}
      local total = { time = 0, value = 0, gold = 0, kills = 0, sessions = 0 }
      local last = nil
      local minAvgSeconds = tonumber(db.settings.historyAvgMinSeconds) or 0
      if minAvgSeconds < 0 then minAvgSeconds = 0 end

      for i = 1, #sessions do
        local s = sessions[i]
        if type(s) == "table" then
          local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
          if tonumber(sid) == tonumber(farmId) then
            local elapsed = tonumber(s.elapsedSec or s.elapsed or s.durationSec or s.duration) or 0
            local value = tonumber(s.totalValue or s.valueCopper or s.value) or 0
            local gold = tonumber(s.money or s.moneyGained or s.lootedGoldValue or s.goldValue or s.rawGoldValue or s.goldLooted or s.lootedGold) or 0
            local kills = tonumber(s.kills) or 0
            local mode = tostring(s.mode or "Default")

            if elapsed >= minAvgSeconds then
              total.time = total.time + elapsed
              total.value = total.value + value
              total.gold = total.gold + gold
              total.kills = total.kills + kills
              total.sessions = total.sessions + 1

              byMode[mode] = byMode[mode] or { time = 0, value = 0, gold = 0, kills = 0, sessions = 0 }
              local t = byMode[mode]
              t.time = t.time + elapsed
              t.value = t.value + value
              t.gold = t.gold + gold
              t.kills = t.kills + kills
              t.sessions = t.sessions + 1
            end

            local when = tonumber(s.endedAt) or tonumber(s.startedAt) or tonumber(s.ts) or nil
            if when and (not last or when > last) then
              last = when
            end
          end
        end
      end

      h.totalsByFarm[farmId] = total
      h.totalsByFarmMode[farmId] = byMode
      h.lastRunByFarm[farmId] = last
    end

    local function _ClearFarmPriceDataForFarm(db, farmId)
      if not db or not farmId then return end

      if type(db.farmStats) == "table" then
        db.farmStats[farmId] = nil
      end

      if type(db.farmStatsBySid) == "table" then
        for sid, fid in pairs(db.farmStatsBySid) do
          if tonumber(fid) == tonumber(farmId) then
            db.farmStatsBySid[sid] = nil
          elseif type(fid) == "table" then
            local id = fid._id or fid.id
            if tonumber(id) == tonumber(farmId) then
              db.farmStatsBySid[sid] = nil
            end
          end
        end
      end

      if db.history then
        if type(db.history.totalsByFarm) == "table" then db.history.totalsByFarm[farmId] = nil end
        if type(db.history.totalsByFarmMode) == "table" then db.history.totalsByFarmMode[farmId] = nil end
        if type(db.history.lastRunByFarm) == "table" then db.history.lastRunByFarm[farmId] = nil end
      end
    end

    local function _ResetFarmPriceDataFromHistoryForFarm(farmId)
      if not WF or not WF.DB or not WF.DB.Get or not farmId then return end
      local db = WF.DB:Get()
      if not db then return end

      db.farmStats = db.farmStats or {}
      db.farmStatsBySid = db.farmStatsBySid or {}

      _ClearFarmPriceDataForFarm(db, farmId)

      local farm = db.farms and (db.farms[tonumber(farmId) or farmId] or db.farms[farmId]) or nil
      local farmName = farm and tostring(farm.name or "") or ""
      local farmNameL = farmName:lower()
      local farmMap = farm and farm.mapId or nil

      local sessions = db.history and db.history.sessions
      if type(sessions) ~= "table" or #sessions == 0 then
        _RebuildHistoryAggregatesForFarm(db, farmId)
        return
      end

      for i = 1, #sessions do
        local s = sessions[i]
        if type(s) == "table" then
          local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil

          if not sid and farmName ~= "" then
            local name = tostring(s.farmName or s._wfCustomFarmName or "")
            if name ~= "" and name == farmName then
              if (not farmMap) or (not s.mapId) or (s.mapId == farmMap) then
                sid = farmId
                s.farmId = farmId
              end
            end
          end

          if tonumber(sid) == tonumber(farmId) then
            if WF.DB.IngestSessionToFarmStats then
              pcall(WF.DB.IngestSessionToFarmStats, WF.DB, s, farmId)
            elseif WF.DB.AccumulateFarmTotals then
              local elapsed = tonumber(s.elapsed) or tonumber(s.duration) or 0
              local value = tonumber(s.totalValue) or tonumber(s.value) or 0
              local gold = tonumber(s.money) or 0
              local kills = tonumber(s.kills) or 0
              pcall(WF.DB.AccumulateFarmTotals, WF.DB, farmId, elapsed, value, gold, kills, s.mode)
            end
          end
        end
      end

      _RebuildHistoryAggregatesForFarm(db, farmId)
    end

    local function _MenuBlueDotLabel(text)
      text = tostring(text or "")
      return "|cff57c7ff•|r |cff57c7ff" .. text .. "|r"
    end

    local function _OpenAvgHeaderMenu()
      local db = (WF and WF.DB and WF.DB.Get) and WF.DB:Get() or nil
      local active = tostring(_ActiveFarmMode() or "Default")
      local pref = (WF and WF.DB and WF.DB.GetSetting) and (WF.DB:GetSetting("farmLibraryViewMode") or "Active") or "Active"
      pref = tostring(pref or "Active")
      if pref == "" then pref = "Active" end

      if not UI or not UI.OpenContextMenu then
        return
      end

      local items = {}

      local followLabel = "Follow active mode"
      if pref == "Active" then
        followLabel = followLabel .. "  " .. _MenuBlueDotLabel("Current")
      end
      items[#items + 1] = {
        text = followLabel,
        fn = function()
          if WF and WF.DB and WF.DB.SetSetting then
            WF.DB:SetSetting("farmLibraryViewMode", "Active")
          end
          UI:RefreshFarms()
        end,
      }

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

      local modes = _ListKnownFarmModes(db)
      for i = 1, #modes do
        local m = modes[i]
        local label = DisplayProfileName(m)
        if pref == m then
          label = label .. "  " .. _MenuBlueDotLabel("Current")
        end
        items[#items + 1] = {
          text = label,
          fn = function()
            if WF and WF.DB and WF.DB.SetSetting then
              WF.DB:SetSetting("farmLibraryViewMode", m)
            end
            UI:RefreshFarms()
          end,
        }
      end

      UI:OpenContextMenu(items, 320)
    end

    local function MakeHeaderBtn(key)
      local b = CreateFrame("Button", nil, L.colHeader)
      b:SetFrameLevel(L.colHeader:GetFrameLevel() + 5)
      if key == "gph" then
        b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
      else
        b:RegisterForClicks("LeftButtonUp")
      end
      b:EnableMouse(true)

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

      b:SetScript("OnClick", function(_, mouseBtn)
        if mouseBtn == "RightButton" and key == "gph" then
          _OpenAvgHeaderMenu()
          return
        end
        _SetFarmSort(key)
      end)
      b:SetScript("OnEnter", function(self)
        if self and self._hl then self._hl:SetAlpha(0.08) end
        if not GameTooltip then return end
        if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
        GameTooltip:SetOwner(self, "ANCHOR_TOPRIGHT")
        GameTooltip:ClearLines()

        local title = _FARM_HDR[key] or "Sort"
        GameTooltip:AddLine(title, ACCENT[1], ACCENT[2], ACCENT[3], true)
        if key == "gph" then
          local metricLabel = (_FarmsMetric() == "liv") and "Avg LIV" or "Gold per Hour"
          GameTooltip:AddLine("Left-click: sort by " .. metricLabel, 0.75, 0.75, 0.75, true)
          GameTooltip:AddLine("Right-click: choose which farm mode feeds the average", 0.75, 0.75, 0.75, true)
        else
          GameTooltip:AddLine("Click to sort the farm list by " .. tostring((_FARM_HDR[key] or "this column"):lower()) .. ".", 0.75, 0.75, 0.75, true)
        end
        GameTooltip:Show()
      end)
      b:SetScript("OnLeave", function(self)
        if self and self._hl then self._hl:SetAlpha(0) end
        if GameTooltip then GameTooltip:Hide() end
      end)

      headerBtns[key] = b
      return b
    end

    MakeHeaderBtn("name")
    MakeHeaderBtn("aim")
    MakeHeaderBtn("type")
    MakeHeaderBtn("zone")
    MakeHeaderBtn("gph")
    _UpdateSortHeaders()


    L.colDiv = MakeDivider(L.listPanel)
    L.colDiv:SetPoint("TOPLEFT", 12, -62)
    L.colDiv:SetPoint("TOPRIGHT", -12, -62)

    L.scroll = CreateFrame("ScrollFrame", nil, L.listPanel, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and L.scroll.ScrollBar then Widgets:SkinScrollBar(L.scroll.ScrollBar) end
    L.scroll:SetPoint("TOPLEFT", 6, -68)
    L.scroll:SetPoint("BOTTOMRIGHT", -28, 10)

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

    L.empty = MakeMutedText(L.listPanel, "No farms yet. Save farms from the tracker window, import seeds, or promote a session from History.")
    L.empty:SetPoint("TOPLEFT", 12, -60)
    L.empty:SetPoint("TOPRIGHT", -12, -60)
    if L.empty.SetWordWrap then L.empty:SetWordWrap(true) end
    L.empty:SetJustifyH("LEFT")
    L.empty:Hide()

        -- -------------------------------------------------
    -- Right: details / metadata (scrollable, stable while resizing)
    -- -------------------------------------------------
    local R = {}
    local right = MakePanel(pFarms, PANEL)
    if right and right.SetClipsChildren then right:SetClipsChildren(true) end

    -- Scroll the whole right rail so sections can't clip/overlap when the window is resized smaller.
    R.rScroll = CreateFrame("ScrollFrame", nil, right, "UIPanelScrollFrameTemplate")
    if Widgets and Widgets.SkinScrollBar and R.rScroll.ScrollBar then Widgets:SkinScrollBar(R.rScroll.ScrollBar) end
    R.rScroll:SetPoint("TOPLEFT", 6, -8)
    R.rScroll:SetPoint("BOTTOMRIGHT", -28, 8)
    if R.rScroll and R.rScroll.SetClipsChildren then R.rScroll:SetClipsChildren(true) end

    R.rSC = CreateFrame("Frame", nil, R.rScroll)
    R.rSC:SetPoint("TOPLEFT", 0, 0)
    R.rSC:SetSize(1, 1)
    R.rScroll:SetScrollChild(R.rSC)

    local function _ScrollWheel(scrollFrame, delta)
      local sb = scrollFrame and scrollFrame.ScrollBar
      if not sb or not sb.GetValue then return end

      local cur = sb:GetValue() or 0
      local minV, maxV = sb:GetMinMaxValues()
      local step = 28

      local nxt = cur - (delta * step)
      if nxt < minV then nxt = minV end
      if nxt > maxV then nxt = maxV end
      sb:SetValue(nxt)
    end

    right:EnableMouseWheel(true)
    right:SetScript("OnMouseWheel", function(_, delta) _ScrollWheel(R.rScroll, delta) end)

    R.rScroll:EnableMouseWheel(true)
    R.rScroll:SetScript("OnMouseWheel", function(_, delta) _ScrollWheel(R.rScroll, delta) end)

    R.rTitle = MakeSectionTitle(R.rSC, "Farm details")
    R.rTitle:SetPoint("TOPLEFT", 12, -10)

    R.rName = MakeBodyText(R.rSC, "Select a farm")
    R.rName:SetPoint("TOPLEFT", 12, -36)
    R.rName:SetTextColor(C4(ACCENT, ACCENT))
    R.rName:SetJustifyH("LEFT")
    R.rName:SetPoint("RIGHT", R.rSC, "RIGHT", -12, 0)
    if R.rName.SetMaxLines then R.rName:SetMaxLines(2) end
    if R.rName.SetWordWrap then R.rName:SetWordWrap(true) end

    R.infoBox = CreateFrame("Frame", nil, R.rSC)
    R.infoBox:SetPoint("TOPLEFT", R.rName, "BOTTOMLEFT", 0, -12)
    R.infoBox:SetPoint("TOPRIGHT", R.rSC, "TOPRIGHT", -12, -48)
    R.infoBox:SetHeight(152)

    -- Two-column detail cards (more readable than tightly packed inline rows).
    R.mid = CreateFrame("Frame", nil, R.infoBox)
    R.mid:SetPoint("TOP", R.infoBox, "TOP", 0, 0)
    R.mid:SetPoint("BOTTOM", R.infoBox, "BOTTOM", 0, 0)
    R.mid:SetWidth(1)

    local INFO_ROW_H = 36
    local INFO_COL_GAP = 12

    local function MakeKVCard(rowIndex, isLeft, labelText)
      local y = -((rowIndex - 1) * INFO_ROW_H)

      local holder = CreateFrame("Frame", nil, R.infoBox)
      if isLeft then
        holder:SetPoint("TOPLEFT", R.infoBox, "TOPLEFT", 0, y)
        holder:SetPoint("TOPRIGHT", R.mid, "TOPLEFT", -(INFO_COL_GAP / 2), y)
      else
        holder:SetPoint("TOPLEFT", R.mid, "TOPLEFT", INFO_COL_GAP / 2, y)
        holder:SetPoint("TOPRIGHT", R.infoBox, "TOPRIGHT", 0, y)
      end
      holder:SetHeight(INFO_ROW_H)

      local k = MakeMutedText(holder, labelText)
      k:SetPoint("TOPLEFT", 0, 0)
      k:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, 0)
      k:SetJustifyH("LEFT")
      if k.SetMaxLines then k:SetMaxLines(1) end
      if k.SetWordWrap then k:SetWordWrap(false) end

      local v = MakeBodyText(holder, "-")
      v:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, -14)
      v:SetPoint("TOPRIGHT", holder, "TOPRIGHT", 0, -14)
      v:SetJustifyH("LEFT")
      if v.SetMaxLines then v:SetMaxLines(1) end
      if v.SetWordWrap then v:SetWordWrap(false) end

      return v, k
    end

    R.kvZone = MakeKVCard(1, true,  "Zone")
    R.kvCoords = MakeKVCard(1, false, "Coords")
    R.kvAim = MakeKVCard(2, true,  "Aim")
    R.kvExp = MakeKVCard(2, false, "Expansion")
    R.kvType = MakeKVCard(3, true,  "Type")
    R.kvAvgGph, R.kvAvgLabel = MakeKVCard(3, false, "Gold per Hour")
    R.kvExpected = MakeKVCard(4, true,  "Expected")
    R.kvUpdated = MakeKVCard(4, false, "Updated")

    R.div = MakeDivider(R.rSC)
    R.div:SetPoint("TOPLEFT", R.infoBox, "BOTTOMLEFT", 0, -12)
    R.div:SetPoint("TOPRIGHT", R.infoBox, "BOTTOMRIGHT", 0, -12)

    R.selectedFarm, R.selectedRow = nil, nil

    -- -------------------------------------------------
    local function SetBtnEnabled(btn, on)
      if not btn then return end
      local target = btn._btn or btn
      if on then
        if target.Enable then target:Enable() end
        if target.SetAlpha then target:SetAlpha(1.0) end
      else
        if target.Disable then target:Disable() end
        if target.SetAlpha then target:SetAlpha(0.65) end
      end
    end

    -- Right panel: header actions + preview + read-only fields
    -- -------------------------------------------------

    -- Header actions: keep Edit tucked into the header row so the farm name gets more room.
    R.bEditFarm = Widgets:Button(R.rSC, "Edit", 72, 22)
    R.bEditFarm:SetPoint("TOPRIGHT", R.rSC, "TOPRIGHT", -12, -8)
    SetBtnEnabled(R.bEditFarm, false)
    AttachTip(R.bEditFarm, "Edit Farm", {
      "Edit the selected farm's saved details.",
      "This modal now stays open until you save or cancel.",
    })

    R.bShareFarm = Widgets:Button(R.rSC, "Share", 78, 22)
    R.bShareFarm:SetPoint("RIGHT", R.bEditFarm, "LEFT", -8, 0)
    SetBtnEnabled(R.bShareFarm, false)
    AttachTip(R.bShareFarm, "Share Farm", {
      "Export the selected farm into a share string.",
      "Imports include a preview before anything is saved.",
    })

    -- Re-anchor name to leave space for header buttons and allow two-line wrapping.
    R.rName:ClearAllPoints()
    R.rName:SetPoint("TOPLEFT", 12, -36)
    R.rName:SetPoint("TOPRIGHT", R.bShareFarm, "TOPLEFT", -10, 0)

    R.selectedFarm, R.selectedRow = nil, nil

    -- Preview panel (fills the blank space): image + top items
    R.pvTitle = MakeSectionTitle(R.rSC, "Preview")
    R.pvTitle:SetPoint("TOPLEFT", R.div, "BOTTOMLEFT", 0, -18)

    R.preview = CreateFrame("Frame", nil, R.rSC, "BackdropTemplate")
    R.preview:SetPoint("TOPLEFT", R.pvTitle, "BOTTOMLEFT", 0, -8)
    R.preview:SetPoint("TOPRIGHT", R.rSC, "TOPRIGHT", -12, 0)
    R.preview:SetHeight(120)
    R.preview:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
      edgeSize = 12,
      insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    R.preview:SetBackdropColor(0.06, 0.06, 0.06, 0.55)
    R.preview:SetBackdropBorderColor(C4(BORDER, BORDER))

    -- Safety: keep preview internals clipped when the window is resized very small.
    if R.preview and R.preview.SetClipsChildren then R.preview:SetClipsChildren(true) end

    R.imgBtn = CreateFrame("Button", nil, R.preview)
    R.imgBtn:EnableMouse(true)

    -- Center the preview image and scale it up without leaving dead space on the right.
    -- Target size is clamped to the panel width to avoid clipping on smaller layouts.
    local PREVIEW_IMG_TARGET_W = 260
    local PREVIEW_IMG_ASPECT_W, PREVIEW_IMG_ASPECT_H = 16, 9

    local function _SizePreviewImage()
      if not R.imgBtn:IsShown() then return end

      local pw = R.preview:GetWidth() or 0
      local availW = pw - 16
      if availW <= 0 then availW = PREVIEW_IMG_TARGET_W end

      local w = math.min(PREVIEW_IMG_TARGET_W, availW)
      local h = math.floor((w * PREVIEW_IMG_ASPECT_H / PREVIEW_IMG_ASPECT_W) + 0.5)

      R.imgBtn:ClearAllPoints()
      R.imgBtn:SetPoint("TOP", R.preview, "TOP", 0, -8)
      R.imgBtn:SetSize(w, h)
    end

    R.imgTex = R.imgBtn:CreateTexture(nil, "ARTWORK")
    R.imgTex:SetAllPoints(R.imgBtn)
    R.imgTex:SetColorTexture(0, 0, 0, 0)
    R.imgTex:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    R.imgBtn._tex = R.imgTex

    R.imgFrame = R.imgBtn:CreateTexture(nil, "BORDER")
    R.imgFrame:SetAllPoints(R.imgBtn)
    R.imgFrame:SetTexture("Interface/Buttons/WHITE8x8")
    R.imgFrame:SetAlpha(0.10)

    R.imgHint = R.imgBtn:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    R.imgHint:SetPoint("BOTTOMLEFT", 8, 8)
    R.imgHint:SetJustifyH("LEFT")
    R.imgHint:SetTextColor(0.88, 0.88, 0.88, 0.76)
    R.imgHint:SetShadowOffset(1, -1)
    R.imgHint:SetShadowColor(0, 0, 0, 0.95)
    R.imgHint:SetText("click to expand")

    local function _RefreshImageHintBadge(active)
      if not R.imgHint then return end
      if active then
        R.imgHint:SetTextColor(C4(ACCENT, 0.96))
      else
        R.imgHint:SetTextColor(0.88, 0.88, 0.88, 0.76)
      end
    end

    R.topTitle = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    R.topTitle:SetJustifyH("LEFT")
    R.topTitle:SetTextColor(C4(MUTED, MUTED))
    R.topTitle:SetText("Top loot")
    if R.topTitle.SetMaxLines then R.topTitle:SetMaxLines(1) end
    if R.topTitle.SetWordWrap then R.topTitle:SetWordWrap(false) end

    R.topCountSub = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    R.topCountSub:SetJustifyH("RIGHT")
    R.topCountSub:SetTextColor(C4(MUTED, MUTED))
    R.topCountSub:SetText("Count")
    if R.topCountSub.SetMaxLines then R.topCountSub:SetMaxLines(1) end
    if R.topCountSub.SetWordWrap then R.topCountSub:SetWordWrap(false) end

    R.topSub = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    R.topSub:SetJustifyH("RIGHT")
    R.topSub:SetTextColor(C4(MUTED, MUTED))
    R.topSub:SetText("Value")
    if R.topSub.SetMaxLines then R.topSub:SetMaxLines(1) end
    if R.topSub.SetWordWrap then R.topSub:SetWordWrap(false) end

    R.topBGs = {}
    R.topLines = {}
    R.topCounts = {}
    R.topVals = {}

    local function _SetTopLootFontSize(fs, size)
      if not fs or not fs.GetFont or not fs.SetFont then return end
      local fontFile, _, flags = fs:GetFont()
      if fontFile then
        fs:SetFont(fontFile, size, flags)
      end
    end

    for i = 1, 5 do
      local bg = R.preview:CreateTexture(nil, "BACKGROUND")
      bg:SetColorTexture(1, 1, 1, (i % 2 == 1) and 0.035 or 0.06)
      R.topBGs[i] = bg

      local t = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      t:SetJustifyH("LEFT")
      t:SetTextColor(C4(TEXT, TEXT))
      t:SetText("")
      _SetTopLootFontSize(t, 12)
      if t.SetMaxLines then t:SetMaxLines(1) end
      if t.SetWordWrap then t:SetWordWrap(false) end
      R.topLines[i] = t

      t = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      t:SetJustifyH("RIGHT")
      t:SetTextColor(C4(MUTED, MUTED))
      t:SetText("")
      _SetTopLootFontSize(t, 12)
      if t.SetMaxLines then t:SetMaxLines(1) end
      if t.SetWordWrap then t:SetWordWrap(false) end
      R.topCounts[i] = t

      t = R.preview:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
      t:SetJustifyH("RIGHT")
      t:SetTextColor(C4(ACCENT, ACCENT))
      t:SetText("")
      _SetTopLootFontSize(t, 12)
      if t.SetMaxLines then t:SetMaxLines(1) end
      if t.SetWordWrap then t:SetWordWrap(false) end
      R.topVals[i] = t
    end

    _SetTopLootFontSize(R.topTitle, 12)
    _SetTopLootFontSize(R.topCountSub, 12)
    _SetTopLootFontSize(R.topSub, 12)

    local function _AnchorTopItems()
      local previewW = R.preview:GetWidth() or 0
      local valueInset = 14
      local countToValueGap = 16
      local nameToCountGap = 10
      local valueW = 104
      local countW = 36

      if previewW > 0 and previewW < 340 then
        valueW = 94
        countW = 32
        countToValueGap = 12
        nameToCountGap = 8
      end

      R.topTitle:ClearAllPoints()
      R.topCountSub:ClearAllPoints()
      R.topSub:ClearAllPoints()

      if R.imgBtn:IsShown() then
        R.topTitle:SetPoint("TOPLEFT", R.imgBtn, "BOTTOMLEFT", 2, -12)
        R.topTitle:SetPoint("RIGHT", R.preview, "RIGHT", -10, 0)
      else
        R.topTitle:SetPoint("TOPLEFT", R.preview, "TOPLEFT", 10, -10)
        R.topTitle:SetPoint("RIGHT", R.preview, "RIGHT", -10, 0)
      end

      R.topSub:SetWidth(valueW)
      R.topSub:SetPoint("RIGHT", R.preview, "RIGHT", -valueInset, 0)
      R.topSub:SetPoint("CENTER", R.topTitle, "CENTER", 0, 0)

      R.topCountSub:SetWidth(countW)
      R.topCountSub:SetPoint("RIGHT", R.topSub, "LEFT", -countToValueGap, 0)
      R.topCountSub:SetPoint("CENTER", R.topTitle, "CENTER", 0, 0)

      for i = 1, #R.topLines do
        local t = R.topLines[i]
        local c = R.topCounts and R.topCounts[i]
        local v = R.topVals and R.topVals[i]
        local bg = R.topBGs and R.topBGs[i]
        local y = -8 - ((i - 1) * 18)

        if bg then
          bg:ClearAllPoints()
          bg:SetPoint("TOPLEFT", R.topTitle, "BOTTOMLEFT", 0, y)
          bg:SetPoint("TOPRIGHT", R.topTitle, "BOTTOMRIGHT", 0, y)
          bg:SetHeight(16)
        end

        if v and bg then
          v:ClearAllPoints()
          v:SetPoint("RIGHT", bg, "RIGHT", -valueInset, 0)
          v:SetPoint("CENTER", bg, "CENTER", 0, 0)
          v:SetWidth(valueW)
        end

        if c and bg then
          c:ClearAllPoints()
          c:SetPoint("RIGHT", v, "LEFT", -countToValueGap, 0)
          c:SetPoint("CENTER", bg, "CENTER", 0, 0)
          c:SetWidth(countW)
        end

        if bg then
          t:ClearAllPoints()
          t:SetPoint("LEFT", bg, "LEFT", 8, 0)
          t:SetPoint("CENTER", bg, "CENTER", 0, 0)
          if c then
            t:SetPoint("RIGHT", c, "LEFT", -nameToCountGap, 0)
          elseif v then
            t:SetPoint("RIGHT", v, "LEFT", -8, 0)
          else
            t:SetPoint("RIGHT", bg, "RIGHT", -8, 0)
          end
        end
      end
    end

    R.preview:HookScript("OnSizeChanged", function()
      if R.imgBtn:IsShown() then
        _SizePreviewImage()
      end
      _AnchorTopItems()

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

    -- Layout: hide image preview entirely if no image is set.
    local function _LayoutPreview(hasImage)
      if hasImage then
        R.imgBtn:Show()
        _SizePreviewImage()
        R.preview:SetHeight((R.imgBtn:GetHeight() or 0) + 150)
      else
        R.imgBtn:Hide()
        R.preview:SetHeight(134)
      end
      _AnchorTopItems()
    end

    _LayoutPreview(false)

    -- Tags & description (read-only; edited via Edit modal)
    R.hlTitle = MakeSectionTitle(R.rSC, "Tags & description")
    R.hlTitle:SetPoint("TOPLEFT", R.preview, "BOTTOMLEFT", 0, -18)

    R.tagLabel = MakeMutedText(R.rSC, "Tags")
    R.tagLabel:SetJustifyH("LEFT")
    if R.tagLabel.SetWordWrap then R.tagLabel:SetWordWrap(false) end

    R.tagText = MakeBodyText(R.rSC, "")
    R.tagText:SetJustifyH("LEFT")
    if R.tagText.SetWordWrap then R.tagText:SetWordWrap(true) end

    R.noteLabel = MakeMutedText(R.rSC, "Description")
    R.noteLabel:SetJustifyH("LEFT")
    if R.noteLabel.SetWordWrap then R.noteLabel:SetWordWrap(false) end

    R.noteBox = CreateFrame("Frame", nil, R.rSC)
    R.noteBox:SetSize(1, 56)

    R.noteText = MakeBodyText(R.noteBox, "")
    R.noteText:SetJustifyH("LEFT")
    R.noteText:SetJustifyV("TOP")
    if R.noteText.SetWordWrap then R.noteText:SetWordWrap(true) end
    if R.noteText.SetNonSpaceWrap then R.noteText:SetNonSpaceWrap(true) end
    R.noteText:SetPoint("TOPLEFT", R.noteBox, "TOPLEFT", 0, 0)
    R.noteText:SetPoint("TOPRIGHT", R.noteBox, "TOPRIGHT", 0, 0)
    R.noteText:SetPoint("BOTTOMLEFT", R.noteBox, "BOTTOMLEFT", 0, 0)
    R.noteText:SetPoint("BOTTOMRIGHT", R.noteBox, "BOTTOMRIGHT", 0, 0)

    R.emptyMeta = MakeMutedText(R.rSC, "No tags or description yet.")
    R.emptyMeta:SetJustifyH("LEFT")
    if R.emptyMeta.SetWordWrap then R.emptyMeta:SetWordWrap(true) end

    R.editHint = MakeMutedText(R.rSC, "Select a farm to view details. Use Edit to change details.")
    R.editHint:SetJustifyH("CENTER")
    if R.editHint.SetWordWrap then R.editHint:SetWordWrap(true) end

    R.actionDiv = MakeDivider(R.rSC)

    R.actionBar = CreateFrame("Frame", nil, R.rSC)
    R.actionBar:SetSize(288, 30)

    R.bWaypoint = Widgets:Button(R.actionBar, "Waypoint", 132, 28)
    R.bWaypoint:SetPoint("LEFT", 0, 0)

    R.bRandom = Widgets:Button(R.actionBar, "Random Farm", 144, 28)
    R.bRandom:SetPoint("LEFT", R.bWaypoint, "RIGHT", 12, 0)

    local function _LayoutActionButtons()
      local w = R.actionBar:GetWidth() or 0
      if w <= 0 then return end

      local gap = 12
      local minW = 118
      local leftW = math.floor((w - gap) / 2)
      local rightW = w - gap - leftW

      if leftW < minW or rightW < minW then
        leftW = math.max(minW, leftW)
        rightW = math.max(minW, rightW)
      end

      R.bWaypoint:ClearAllPoints()
      R.bWaypoint:SetPoint("LEFT", R.actionBar, "LEFT", 0, 0)
      R.bWaypoint:SetWidth(leftW)
      R.bWaypoint:SetHeight(28)

      R.bRandom:ClearAllPoints()
      R.bRandom:SetPoint("LEFT", R.bWaypoint, "RIGHT", gap, 0)
      R.bRandom:SetPoint("RIGHT", R.actionBar, "RIGHT", 0, 0)
      R.bRandom:SetHeight(28)
    end

    R.actionBar:HookScript("OnSizeChanged", function()
      _LayoutActionButtons()
    end)

    local function _RefreshMetaSection(tagsValue, noteValue)
      tagsValue = tostring(tagsValue or "")
      noteValue = tostring(noteValue or "")

      R.tagLabel:Hide(); R.tagText:Hide(); R.noteLabel:Hide(); R.noteBox:Hide(); R.noteText:Hide(); R.emptyMeta:Hide()

      local bottomAnchor = R.hlTitle
      local hadContent = false

      if noteValue ~= "" then
        R.noteLabel:ClearAllPoints()
        R.noteLabel:SetPoint("TOPLEFT", R.hlTitle, "BOTTOMLEFT", 0, -8)
        R.noteLabel:SetPoint("RIGHT", R.rSC, "RIGHT", -12, 0)
        R.noteLabel:Show()

        R.noteBox:ClearAllPoints()
        R.noteBox:SetPoint("TOPLEFT", R.noteLabel, "BOTTOMLEFT", 0, -2)
        R.noteBox:SetPoint("TOPRIGHT", R.rSC, "RIGHT", -12, 0)
        R.noteBox:SetHeight(56)
        R.noteBox:Show()

        R.noteText:SetText(noteValue)
        R.noteText:Show()

        bottomAnchor = R.noteBox
        hadContent = true
      else
        R.noteBox:Hide()
      end

      if tagsValue ~= "" then
        R.tagLabel:ClearAllPoints()
        if hadContent then
          R.tagLabel:SetPoint("TOPLEFT", bottomAnchor, "BOTTOMLEFT", 0, -10)
        else
          R.tagLabel:SetPoint("TOPLEFT", R.hlTitle, "BOTTOMLEFT", 0, -8)
        end
        R.tagLabel:SetPoint("RIGHT", R.rSC, "RIGHT", -12, 0)
        R.tagLabel:Show()

        R.tagText:ClearAllPoints()
        R.tagText:SetPoint("TOPLEFT", R.tagLabel, "BOTTOMLEFT", 0, -2)
        R.tagText:SetPoint("RIGHT", R.rSC, "RIGHT", -12, 0)
        R.tagText:SetText(tagsValue)
        R.tagText:Show()

        bottomAnchor = R.tagText
        hadContent = true
      end

      if not hadContent then
        R.emptyMeta:ClearAllPoints()
        R.emptyMeta:SetPoint("TOPLEFT", R.hlTitle, "BOTTOMLEFT", 0, -8)
        R.emptyMeta:SetPoint("RIGHT", R.rSC, "RIGHT", -12, 0)
        R.emptyMeta:Show()
        bottomAnchor = R.emptyMeta
      end

      R.actionDiv:ClearAllPoints()
      R.actionDiv:SetPoint("TOPLEFT", bottomAnchor, "BOTTOMLEFT", 0, -16)
      R.actionDiv:SetPoint("TOPRIGHT", R.rSC, "RIGHT", -12, 0)

      R.actionBar:ClearAllPoints()
      R.actionBar:SetPoint("TOPLEFT", R.actionDiv, "BOTTOMLEFT", 0, -14)
      R.actionBar:SetPoint("TOPRIGHT", R.actionDiv, "BOTTOMRIGHT", 0, -14)
      R.actionBar:SetHeight(30)
      _LayoutActionButtons()

      R.editHint:ClearAllPoints()
      R.editHint:SetPoint("TOP", R.actionBar, "BOTTOM", 0, -10)
      R.editHint:SetPoint("LEFT", R.rSC, "LEFT", 18, 0)
      R.editHint:SetPoint("RIGHT", R.rSC, "RIGHT", -18, 0)

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

    _RefreshMetaSection("", "")

    R.rightBottom = CreateFrame("Frame", nil, R.rSC)
    R.rightBottom:SetPoint("TOP", R.editHint, "BOTTOM", 0, -10)
    R.rightBottom:SetSize(1, 1)

    function UI:_UpdateFarmsRightScroll()
      if not (R.rScroll and R.rScroll.IsShown and R.rScroll:IsShown()) then return end

      local w = R.rScroll:GetWidth() or 0
      if w > 1 then
        R.rSC:SetWidth(w)
      end

      local top = (R.rTitle and R.rTitle.GetTop and R.rTitle:GetTop()) or (R.rSC.GetTop and R.rSC:GetTop())
      local bottom = (R.rightBottom and R.rightBottom.GetBottom and R.rightBottom:GetBottom()) or (R.actionBar and R.actionBar.GetBottom and R.actionBar:GetBottom())

      if top and bottom then
        local h = (top - bottom) + 20
        if h < 1 then h = 1 end
        R.rSC:SetHeight(h)
        if R.rScroll.UpdateScrollChildRect then R.rScroll:UpdateScrollChildRect() end
      end
    end

    R.rScroll:HookScript("OnSizeChanged", function()
      if UI and UI._UpdateFarmsRightScroll then
        UI:_UpdateFarmsRightScroll()
      end
    end)

    if UI and UI._Layout and hooksecurefunc and not UI._FarmsRightScrollHooked then
      UI._FarmsRightScrollHooked = true
      hooksecurefunc(UI, "_Layout", function()
        if pFarms and pFarms.IsShown and pFarms:IsShown() and UI._UpdateFarmsRightScroll then
          UI:_UpdateFarmsRightScroll()
        end
      end)
    end

    -- -------------------------------------------------
    -- Preview helpers (top items + image)
    -- -------------------------------------------------
    local function _FarmId(f)
      return (f and (f._id or f.id)) or nil
    end

    local function _CollectTopItems(db, farm, limit)
      limit = limit or 5
      local id = _FarmId(farm)
      if not id then return {} end

      local h = db and db.history or nil
      local sessions = h and h.sessions or nil
      if type(sessions) ~= "table" then return {} end

      local totals = {}

      local function add(name, count, value)
        name = tostring(name or "")
        count = tonumber(count) or 0
        value = tonumber(value) or 0
        if name == "" then return end
        if count <= 0 and value <= 0 then return end

        local cur = totals[name]
        if cur then
          cur.count = (cur.count or 0) + count
          cur.value = (cur.value or 0) + value
        else
          totals[name] = {
            name = name,
            count = count,
            value = value,
          }
        end
      end

      local function entryValue(it, count)
        local value = tonumber(it and (it.value or it.totalValue or it.total or it.priceTotal)) or 0
        if value > 0 then return value end

        local unit = tonumber(it and (it.unitValue or it.unitPrice or it.priceEach or it.eachValue or it.price)) or 0
        count = tonumber(count) or 0
        if unit > 0 and count > 0 then
          return unit * count
        end

        return 0
      end

      local function parseItems(items)
        if type(items) ~= "table" then return end

        -- array form
        if items[1] ~= nil then
          for i = 1, #items do
            local it = items[i]
            if type(it) == "table" then
              local nm = it.name or it.itemName or it.label or it.text
              local c = it.count or it.qty or it.amount or it.n or 0
              if nm then add(nm, c, entryValue(it, c)) end
            end
          end
          return
        end

        -- map form
        for k, v in pairs(items) do
          if type(v) == "number" then
            add(k, v, 0)
          elseif type(v) == "table" then
            local nm = v.name or v.itemName or v.label or k
            local c = v.count or v.qty or v.amount or v.n or v[1] or 0
            if nm then add(nm, c, entryValue(v, c)) end
          end
        end
      end

      for i = 1, #sessions do
        local s = sessions[i]
        if type(s) == "table" then
          local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
          if sid == id then
            parseItems(s.items)
            parseItems(s.loot)
            parseItems(s.drops)
            parseItems(s.topItems)
            parseItems(s.itemsByName)
            parseItems(s.itemsById)
            parseItems(s.lootedItems)
          end
        end
      end

      local out = {}
      for _, v in pairs(totals) do out[#out + 1] = v end
      table.sort(out, function(a, b)
        local av = tonumber(a.value) or 0
        local bv = tonumber(b.value) or 0
        if av ~= bv then return av > bv end

        local ac = tonumber(a.count) or 0
        local bc = tonumber(b.count) or 0
        if ac ~= bc then return ac > bc end

        return tostring(a.name or "") < tostring(b.name or "")
      end)

      if #out == 0 and type(farm.previewItems) == "table" then
        for i = 1, math.min(limit, #farm.previewItems) do
          local it = farm.previewItems[i]
          if type(it) == "table" then
            out[#out + 1] = {
              name = tostring(it.name or it.itemName or "Item"),
              count = tonumber(it.count or it.qty or it.amount or 0) or 0,
              value = tonumber(it.value or it.totalValue or it.total or 0) or 0,
            }
          end
        end
      end

      local final = {}
      for i = 1, math.min(limit, #out) do final[i] = out[i] end
      return final
    end

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

    local function _SeedDefaultImage(seedKey)
      seedKey = tostring(seedKey or "")
      if seedKey == "" then return nil end
      local seeds = (WF.Seeds and WF.Seeds.farms) or WF.FARM_SEEDS
      if type(seeds) ~= "table" then return nil end
      for i = 1, #seeds do
        local s = seeds[i]
        if s and s.seedKey == seedKey and s.image and s.image ~= "" then
          return s.image
        end
      end
      return nil
    end

    local function _NormalizeFarmImagePath(path)
      path = Trim(path or "")
      if path == "" then return nil end

      path = path:gsub("/", "\\")
      local lower = string.lower(path)

      if lower:find("^interface\\") or lower:find("^world\\") then
        return path
      end

      if lower:find("^whatfarm\\") then
        return "Interface\\AddOns\\" .. path
      end

      if lower:find("^media\\") then
        return "Interface\\AddOns\\WhatFarm\\" .. path
      end

      if not path:find("\\") then
        if not lower:match("%.blp$") and not lower:match("%.tga$") and not lower:match("%.png$") then
          path = path .. ".tga"
        end
        return "Interface\\AddOns\\WhatFarm\\Media\\FarmImages\\" .. path
      end

      return "Interface\\AddOns\\WhatFarm\\" .. path
    end

    local function _ResolveFarmImage(farm)
      if not farm then return nil end
      local tex = Trim(farm.image or "")
      if tex == "" and farm.seedKey then
        tex = Trim(_SeedDefaultImage(farm.seedKey) or "")
      end
      if tex == "" then return nil end
      return _NormalizeFarmImagePath(tex)
    end

    local function _PreviewMoney(copper)
      local s
      if Money then
        s = Money(copper)
      else
        s = tostring(math.floor((tonumber(copper) or 0) + 0.5))
      end
      s = tostring(s or "")
      s = s:gsub("%s0c$", "")
      s = s:gsub("%s0s$", "")
      s = s:gsub("%s+$", "")
      return s
    end

local function _SyncPreview(farm)
      if not farm then
        _LayoutPreview(false)
        for i = 1, #R.topLines do
          R.topLines[i]:SetText("")
          if R.topCounts and R.topCounts[i] then R.topCounts[i]:SetText("") end
          if R.topVals and R.topVals[i] then R.topVals[i]:SetText("") end
          if R.topBGs and R.topBGs[i] then R.topBGs[i]:Hide() end
        end
        return
      end

      local tex = _ResolveFarmImage(farm)
      local hasImage = (tex and tex ~= "")
      _LayoutPreview(hasImage)

      if hasImage then
        R.imgTex:SetTexture(tex)
        R.imgHint:SetText("click to expand")
        _RefreshImageHintBadge(false)
      else
        R.imgTex:SetTexture(nil)
        R.imgHint:SetText("Click Edit to set an image")
        _RefreshImageHintBadge(false)
      end

      local db = WF.DB:Get() or {}
      local tops = _CollectTopItems(db, farm, #R.topLines)

      if #tops == 0 then
        R.topLines[1]:SetText("No history yet")
        if R.topCounts and R.topCounts[1] then R.topCounts[1]:SetText("") end
        if R.topVals and R.topVals[1] then R.topVals[1]:SetText("") end
        if R.topBGs and R.topBGs[1] then R.topBGs[1]:Show() end
        for i = 2, #R.topLines do
          R.topLines[i]:SetText("")
          if R.topCounts and R.topCounts[i] then R.topCounts[i]:SetText("") end
          if R.topVals and R.topVals[i] then R.topVals[i]:SetText("") end
          if R.topBGs and R.topBGs[i] then R.topBGs[i]:Hide() end
        end
      else
        for i = 1, #R.topLines do
          local it = tops[i]
          local c = R.topCounts and R.topCounts[i]
          local v = R.topVals and R.topVals[i]
          if it then
            local itemName = tostring(it.name or "Item")
            local itemValue = tonumber(it.value) or 0
            local itemCount = tonumber(it.count) or 0

            R.topLines[i]:SetText(itemName)

            if R.topBGs and R.topBGs[i] then R.topBGs[i]:Show() end

            if c then
              if itemCount > 0 then
                c:SetText(string.format("x%d", itemCount))
              else
                c:SetText("")
              end
            end

            if v then
              if itemValue > 0 then
                v:SetText(_PreviewMoney(itemValue))
                v:SetTextColor(C4(ACCENT, ACCENT))
              else
                v:SetText("")
              end
            end
          else
            R.topLines[i]:SetText("")
            if c then c:SetText("") end
            if v then v:SetText("") end
            if R.topBGs and R.topBGs[i] then R.topBGs[i]:Hide() end
          end
        end
      end
    end


    -- -------------------------------------------------
    -- Helpers: tags / money parsing
    -- -------------------------------------------------
    local function ParseTags(s)
      local out = {}
      s = tostring(s or "")
      for part in s:gmatch("[^,]+") do
        local v = Trim(part)
        if v ~= "" then out[#out + 1] = v end
      end
      return out
    end

    local function ParseMoneyInput(s)
      s = Trim(s):lower()
      s = s:gsub(",", "")
      -- Reject scientific notation (it can appear when huge values get tostring()'d).
      if s:match("%d+e[+-]?%d+") then return nil end
      if s == "" then return nil end

      local compactNum, compactSuffix = s:match("^(%-?[%d%.]+)%s*([kmb])$")
      if compactNum and compactSuffix then
        local base = tonumber(compactNum)
        if not base or base < 0 then return nil end
        local mult = 1
        if compactSuffix == "k" then
          mult = 1000
        elseif compactSuffix == "m" then
          mult = 1000000
        elseif compactSuffix == "b" then
          mult = 1000000000
        end
        return math.floor((base * mult * 10000) + 0.5)
      end

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

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

      -- plain number => treat as gold
      local num = tonumber(s)
      if not num then return nil end
      if num > 1000000000000 then return nil end
      if num >= 1000000 then
        return math.floor(num + 0.5) -- assume already copper for very large inputs
      end
      return math.floor(num * 10000 + 0.5)
    end

    local function _HasHistorySessions(db)
      return db and db.history and type(db.history.sessions) == "table" and #db.history.sessions > 0
    end

    local function _HasFarmStats(db)
      return db and type(db.farmStats) == "table" and next(db.farmStats) ~= nil
    end

    function UI:UpdateFarmResyncStatus()
      if not F.tResync then return end
      local db = (WF and WF.DB and WF.DB.Get) and WF.DB:Get() or nil
      if not _HasHistorySessions(db) then
        F.tResync:SetText("No History runs yet.")
        return
      end
      if self._farmAvgDirty then
        F.tResync:SetText("History changed — click Resync to rebuild Avg data from saved History values.")
        return
      end
      if _HasFarmStats(db) then
        F.tResync:SetText("Avg data is using cached History values.")
        return
      end
      F.tResync:SetText("Avg data has not been rebuilt from History yet.")
    end

    function UI:MarkFarmAveragesDirty()
      self._farmAvgDirty = true
      if self.UpdateFarmResyncStatus then self:UpdateFarmResyncStatus() end
    end

    function UI:MarkFarmAveragesClean()
      self._farmAvgDirty = false
      if self.UpdateFarmResyncStatus then self:UpdateFarmResyncStatus() end
    end

    function UI:ResyncFarmAveragesFromHistory()
      local db = (WF and WF.DB and WF.DB.Get) and WF.DB:Get() or nil
      if not db then return end

      db.history = db.history or {}
      db.farmStats = {}
      db.farmStatsBySid = {}
      db.history.totalsByFarm = {}
      db.history.totalsByFarmMode = {}
      db.history.lastRunByFarm = {}

      local sessions = db.history.sessions
      if type(sessions) == "table" then
        for i = 1, #sessions do
          local s = sessions[i]
          if type(s) == "table" then
            local farmId = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
            if not farmId and WF and WF.DB and WF.DB.FindFarmByName then
              local name = tostring(s._wfCustomFarmName or s.farmName or "")
              name = name:gsub("^%s+", ""):gsub("%s+$", "")
              if name ~= "" then
                local fid, farm = WF.DB:FindFarmByName(name, s.mapId)
                if fid and farm then
                  farmId = tonumber(fid) or fid
                  s.farmId = farmId
                  if farm.name and farm.name ~= "" then
                    s.farmName = farm.name
                  end
                end
              end
            end
            if farmId then
              farmId = tonumber(farmId) or farmId
              if WF and WF.DB and WF.DB.IngestSessionToFarmStats then
                pcall(WF.DB.IngestSessionToFarmStats, WF.DB, s, farmId)
              elseif WF and WF.DB and WF.DB.AccumulateFarmTotals then
                local elapsed = tonumber(s.elapsed) or tonumber(s.duration) or 0
                local value = tonumber(s.totalValue) or tonumber(s.value) or 0
                local gold = tonumber(s.money) or 0
                local kills = tonumber(s.kills) or 0
                pcall(WF.DB.AccumulateFarmTotals, WF.DB, farmId, elapsed, value, gold, kills, s.mode)
                s.farmId = farmId
              end
            end
          end
        end
      end

      if RebuildHistoryAggregates then
        RebuildHistoryAggregates(db)
      end

      self:MarkFarmAveragesClean()
      self:RefreshFarms()
    end

    local function _GetFarmStats(db, id)
      if not db or not id then return nil end
      local fs = db.farmStats
      if type(fs) ~= "table" then return nil end
      local key = tonumber(id) or id
      return fs[key] or fs[id]
    end


    local function _GetFarmStatsBucket(db, id)
      local fs = _GetFarmStats(db, id)
      local mode, followsActive = _FarmsViewMode()

      if db and db.history and tonumber((db.settings and db.settings.historyAvgMinSeconds) or 0) > 0 then
        local h = db.history
        local byMode = h.totalsByFarmMode
        local hb = byMode and byMode[id] and byMode[id][mode]
        if hb and tonumber(hb.time or 0) > 0 then
          return hb, mode, true, fs, followsActive
        end
        return nil, mode, true, fs, followsActive
      end

      -- We ONLY return the selected "view mode" bucket here.
      -- This prevents "Mats" runs from polluting "Raw Gold" (etc) views.
      if not fs then
        return nil, mode, true, nil, followsActive
      end

      local mb = nil
      if mode ~= "" and fs.modes and fs.modes[mode] then
        mb = fs.modes[mode]
        if mb and tonumber(mb.time or 0) > 0 then
          return mb, mode, true, fs, followsActive
        end
      end

      -- No data for this mode.
      return nil, mode, true, fs, followsActive
    end



    local function FarmAvgGPH(db, farm)
      if not db or not farm then return 0 end
      local id = farm.id or farm._id
      if not id then return 0 end

      -- Prefer persistent farmStats (does NOT shrink when history is deleted).
      local b = select(1, _GetFarmStatsBucket(db, id))
      if b and tonumber(b.time) and tonumber(b.time) > 0 then
        return (tonumber(b.value) or 0) / (tonumber(b.time) or 1) * 3600
      end

      -- Fallback: read already-cached history aggregates only.
      -- Do NOT rebuild on page open; manual Resync owns that work.
      db.history = db.history or {}
      local h = db.history
      h.totalsByFarmMode = h.totalsByFarmMode or {}

      local mode = tostring((_FarmsViewMode()) or "Default")
      local t = h.totalsByFarmMode and h.totalsByFarmMode[id] and h.totalsByFarmMode[id][mode]
      if t and t.time and t.time > 0 then
        return (tonumber(t.value) or 0) / (tonumber(t.time) or 1) * 3600
      end

      local preview = tonumber(farm.previewAvgGPH or farm.sharedAvgGPH) or 0
      if preview > 0 then
        return preview
      end
      return 0
    end

    local function FarmAvgLIV(db, farm)
      if not db or not farm then return 0 end
      local id = farm.id or farm._id
      if not id then return 0 end

      -- Prefer persistent farmStats.
      local b = select(1, _GetFarmStatsBucket(db, id))
      if b and tonumber(b.time) and tonumber(b.time) > 0 then
        local total = tonumber(b.value) or 0
        local gold  = tonumber(b.gold) or 0
        local v = total - gold
        if v < 0 then v = 0 end
        return v / (tonumber(b.time) or 1) * 3600
      end

      -- Fallback: read already-cached history aggregates only.
      -- Do NOT rebuild on page open; manual Resync owns that work.
      db.history = db.history or {}
      local h = db.history
      h.totalsByFarmMode = h.totalsByFarmMode or {}

      local mode = tostring((_FarmsViewMode()) or "Default")
      local t = h.totalsByFarmMode and h.totalsByFarmMode[id] and h.totalsByFarmMode[id][mode]
      if not t or not t.time or t.time <= 0 then return 0 end

      local total = tonumber(t.value) or 0
      local gold  = tonumber(t.gold) or 0
      local v = total - gold
      if v < 0 then v = 0 end
      return v / (tonumber(t.time) or 1) * 3600
    end

    local function FarmAvgMetric(db, farm)
      local m = _FarmsMetric()
      if m == "liv" then
        return FarmAvgLIV(db, farm)
      end
      return FarmAvgGPH(db, farm)
    end

    -- -------------------------------------------------
    -- Modal: edit tags/description (prevents off-screen)
    -- -------------------------------------------------
    local function EnsureFarmEditModal()
      if UI._farmEdit then return end

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

      local box = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
      box:SetFrameStrata("FULLSCREEN_DIALOG")
      box:SetFrameLevel(110)
      box:SetSize(520, 300)
      box:EnableMouse(true)
      box:Hide()
      box:SetBackdrop({
        bgFile = "Interface/Tooltips/UI-Tooltip-Background",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 14,
        insets = { left = 6, right = 6, top = 6, bottom = 6 },
      })
      box:SetBackdropColor(0.04, 0.04, 0.04, 1.00)
      box:SetBackdropBorderColor(C4(BORDER, BORDER))
      box:SetScript("OnMouseDown", function() end)

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

      local sub = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
      sub:SetPoint("TOPLEFT", 14, -38)
      sub:SetPoint("TOPRIGHT", -14, -38)
      sub:SetJustifyH("LEFT")
      sub:SetTextColor(C4(MUTED, MUTED))
      sub:SetText("-")

      local input = MakeInput(box, 480, 24)
      input:SetPoint("TOPLEFT", 14, -62)

      local scroll2 = CreateFrame("ScrollFrame", nil, box, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll2.ScrollBar then Widgets:SkinScrollBar(scroll2.ScrollBar) end
      scroll2:SetPoint("TOPLEFT", 14, -62)
      scroll2:SetPoint("BOTTOMRIGHT", -30, 52)
      scroll2:Hide()

      local inputML = CreateFrame("EditBox", nil, scroll2)
      inputML:SetMultiLine(true)
      inputML:SetAutoFocus(false)
      inputML:SetFontObject("WF_ChatFontNormal")
      inputML:SetJustifyH("LEFT")
      inputML:SetWidth(480)
      inputML:SetText("")
      scroll2:SetScrollChild(inputML)

      local bSave2 = Widgets:Button(box, "Save", 110, 26)
      bSave2:SetPoint("BOTTOMRIGHT", -14, 14)

      local bCancel2 = Widgets:Button(box, "Cancel", 110, 26)
      bCancel2:SetPoint("RIGHT", bSave2, "LEFT", -10, 0)

      bCancel2:SetScript("OnClick", function() UI:CloseFarmEditModal() end)

      box._title = title
      box._sub = sub
      box._input = input
      box._scroll = scroll2
      box._inputML = inputML
      box._onSave = nil
      box._save = bSave2
      box._cancel = bCancel2

      bSave2:SetScript("OnClick", function()
        local fn = box._onSave
        if fn then fn() end
      end)

      UI._farmEdit = box
      UI._farmEditBlocker = blocker
    end

    function UI:CloseFarmEditModal()
      if self._farmEdit then self._farmEdit:Hide() end
      if self._farmEditBlocker then self._farmEditBlocker:Hide() end
    end


    -- -------------------------------------------------
    -- Modal: edit all farm metadata (name + details + tags + notes + image)
    -- -------------------------------------------------
    local function EnsureFarmMetaModal()
      if UI._farmMeta then return end

      local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
      blocker:SetAllPoints(UIParent)
      blocker:EnableMouse(true)
      blocker:SetFrameStrata("FULLSCREEN_DIALOG")
      blocker:SetFrameLevel(100)
      blocker:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
      blocker:SetBackdropColor(0, 0, 0, 0.60)
      blocker:Hide()

      local box = CreateFrame("Frame", "WhatFarmFarmMetaModal", UIParent, "BackdropTemplate")
      box:SetSize(560, 560)
      box:SetPoint("CENTER", UIParent, "CENTER", 0, 30)
      box:SetFrameStrata("FULLSCREEN_DIALOG")
      box:SetFrameLevel(110)
      box:EnableMouse(true)
      box:SetMovable(true)
      box:SetClampedToScreen(true)
      box:SetUserPlaced(false)
      box:Hide()

      box:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8x8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
      })
      box:SetBackdropColor(0.05, 0.05, 0.05, 1.00)
      box:SetBackdropBorderColor(C4(BORDER, BORDER))
      box:SetScript("OnMouseDown", function() end)

      local dragBar = CreateFrame("Frame", nil, box)
      dragBar:SetPoint("TOPLEFT", 8, -8)
      dragBar:SetPoint("TOPRIGHT", -92, -8)
      dragBar:SetHeight(28)
      dragBar:EnableMouse(true)
      dragBar:RegisterForDrag("LeftButton")
      dragBar:SetScript("OnDragStart", function()
        box:StartMoving()
      end)
      dragBar:SetScript("OnDragStop", function()
        box:StopMovingOrSizing()
      end)
      if UISpecialFrames then
        local n = box.GetName and box:GetName()
        if n and n ~= "" then
          local exists = false
          for i = 1, #UISpecialFrames do
            if UISpecialFrames[i] == n then
              exists = true
              break
            end
          end
          if not exists then
            tinsert(UISpecialFrames, n)
          end
        end
      end

      local title = box:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      title:SetPoint("TOPLEFT", 16, -14)
      title:SetTextColor(C4(TEXT, TEXT))
      title:SetText("Edit farm")
      box._title = title

      local close = Widgets:Button(box, "Close", 70, 22)
      close:SetPoint("TOPRIGHT", -14, -12)

      local function L(parent, text)
        local fs = parent:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
        fs:SetTextColor(C4(MUTED, MUTED))
        fs:SetText(text)
        fs:SetJustifyH("LEFT")
        return fs
      end

      -- layout constants
      local PAD = 16
      local COL1_W = 360
      local COL2_W = 160
      local GAP = 16
      local FULL_W = 560 - (PAD * 2)

      -- Row 1: Name (left) / Expansion (right)
      local nameLabel = L(box, "Name")
      nameLabel:SetPoint("TOPLEFT", PAD, -48)
      local nameBox = MakeInput(box, COL1_W, 22)
      nameBox:SetPoint("TOPLEFT", nameLabel, "BOTTOMLEFT", 0, -6)

      local tmpExp = "Unknown"
      local comboExp
      comboExp = MakeCombo(box, "Expansion", function() return tmpExp end, function(v)
        tmpExp = v
        if C_Timer and C_Timer.After then
          C_Timer.After(0, function()
            if comboExp and comboExp._txt then
              comboExp._txt:SetText(_DisplayExpansion(tmpExp or "Unknown"))
            end
          end)
        elseif comboExp and comboExp._txt then
          comboExp._txt:SetText(_DisplayExpansion(tmpExp or "Unknown"))
        end
      end, function()
        local items = {}
        for i = 1, #(FARM_EXP_OPTS or {}) do
          local v = tostring(FARM_EXP_OPTS[i] or "")
          if v ~= "" then
            local display = _DisplayExpansion(v)
            local body
            if v == "Unknown" then
              body = "The farm has not been assigned to an expansion yet."
            else
              body = "Set this farm to " .. display .. "."
            end
            items[#items + 1] = { text = display, value = v, tooltipTitle = "Expansion", tooltipBody = body }
          end
        end
        return items
      end)
      comboExp:ClearAllPoints()
      comboExp:SetPoint("TOPLEFT", nameLabel, "TOPLEFT", COL1_W + GAP, 0)
      comboExp:SetWidth(COL2_W); if comboExp._btn then comboExp._btn:SetWidth(COL2_W) end
      comboExp._refresh = function()
        if comboExp._txt then
          comboExp._txt:SetText(_DisplayExpansion(tmpExp or "Unknown"))
        end
      end
      comboExp:HookScript("OnShow", function()
        if comboExp and comboExp._refresh then
          comboExp._refresh()
        end
      end)

      -- Row 2: Zone (left) / Aim (right)
      local zoneLabel = L(box, "Zone")
      zoneLabel:SetPoint("TOPLEFT", nameBox, "BOTTOMLEFT", 0, -14)
      local zoneBox = MakeInput(box, COL1_W, 22)
      zoneBox:SetPoint("TOPLEFT", zoneLabel, "BOTTOMLEFT", 0, -6)

      local tmpAim = "Unknown"
      local comboAim
      local function OpenCustomAimPrompt(initialValue)
        EnsureFarmEditModal()
        local promptBlocker = UI._farmEditBlocker
        local prompt = UI._farmEdit
        if not prompt or not promptBlocker then return end

        local priorAim = tmpAim

        local function RestoreFarmMetaAfterPrompt()
          UI:CloseFarmEditModal()
          if blocker then blocker:Show() end
          if box then box:Show() end
          if comboAim and comboAim._refresh then comboAim._refresh() end
        end

        local function CommitCustomAim()
          local val = Trim(prompt._input:GetText() or "")
          if val ~= "" then
            tmpAim = val
          else
            tmpAim = priorAim
          end
          RestoreFarmMetaAfterPrompt()
        end

        prompt:SetSize(520, 210)
        prompt._title:SetText("Edit aim")
        prompt._sub:SetText("Enter a custom aim label for this farm.")
        prompt._input:SetShown(true)
        prompt._scroll:Hide()
        if prompt._inputML then prompt._inputML:Hide() end
        prompt._input:SetText((initialValue and initialValue ~= "Unknown" and initialValue ~= "Other") and tostring(initialValue) or "")
        prompt._input:ClearFocus()

        AttachTip(prompt._input, "Custom Aim", "Enter your own aim label for this farm. Examples: Cloth Shuffle, Treasure Route, or Rare Hunt.")
        AttachTip(prompt._save, "Save Aim", "Use this custom aim for the farm you are editing.")
        AttachTip(prompt._cancel, "Cancel", "Close this prompt without changing the farm aim.")

        prompt._onSave = CommitCustomAim
        prompt._save:SetScript("OnClick", function()
          local fn = prompt._onSave
          if fn then fn() end
        end)

        prompt._cancel:SetScript("OnClick", function()
          tmpAim = priorAim
          RestoreFarmMetaAfterPrompt()
        end)

        if prompt._input then
          prompt._input:SetScript("OnEnterPressed", function()
            CommitCustomAim()
          end)
          prompt._input:SetScript("OnEscapePressed", function()
            tmpAim = priorAim
            RestoreFarmMetaAfterPrompt()
          end)
        end

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

        if box then
          box._suspendHideReset = true
          box:Hide()
        end
        promptBlocker:Show()
        prompt:Show()
        if prompt._input.SetFocus then prompt._input:SetFocus() end
        if prompt._input.HighlightText then prompt._input:HighlightText() end
      end

      local function BuildFarmAimItems(currentValue)
        local items = {}
        local seen = {}

        local function addAim(value, body)
          value = _TrimDisplayValue(value)
          if value == "" or value == "Other" then return end
          local key = value:lower()
          if seen[key] then return end
          seen[key] = true
          local display = _DisplayFarmAim(value)
          items[#items + 1] = { text = display, value = value, tooltipTitle = "Aim", tooltipBody = body or ("Set this farm aim to " .. display .. ".") }
        end

        addAim("Unknown", "The farm does not have a defined aim yet.")

        for i = 1, #(FARM_AIMS or {}) do
          local v = tostring(FARM_AIMS[i] or "")
          if v ~= "" and v ~= "Other" then
            local display = _DisplayFarmAim(v)
            local body
            if v == "Unknown" then
              body = "The farm does not have a defined aim yet."
            else
              body = "Set this farm aim to " .. display .. "."
            end
            addAim(v, body)
          end
        end

        local db = (WF and WF.DB and WF.DB.Get and WF.DB:Get()) or nil
        local farms = db and db.farms or nil
        if type(farms) == "table" then
          local dynamic = {}
          for _, farm in pairs(farms) do
            if type(farm) == "table" then
              local aim = _TrimDisplayValue(farm.aim)
              if aim ~= "" and aim ~= "Other" then
                dynamic[#dynamic + 1] = aim
              end
            end
          end
          table.sort(dynamic, function(a, b)
            return _DisplayFarmAim(a):lower() < _DisplayFarmAim(b):lower()
          end)
          for i = 1, #dynamic do
            local v = dynamic[i]
            addAim(v, "Set this farm aim to " .. _DisplayFarmAim(v) .. ".")
          end
        end

        local currentAim = _TrimDisplayValue(currentValue)
        if currentAim ~= "" and currentAim ~= "Other" then
          addAim(currentAim, "Keep this farm aim set to " .. _DisplayFarmAim(currentAim) .. ".")
        end

        items[#items + 1] = { text = "Custom", value = "__CUSTOM__", tooltipTitle = "Aim", tooltipBody = "Enter your own aim label for this farm." }
        return items
      end

      comboAim = MakeCombo(box, "Aim", function() return tmpAim end, function(v)
        if v == "__CUSTOM__" then
          OpenCustomAimPrompt(tmpAim)
          return
        end
        tmpAim = v
        if C_Timer and C_Timer.After then
          C_Timer.After(0, function()
            if comboAim and comboAim._txt then
              comboAim._txt:SetText(_DisplayFarmAim(tmpAim or "Unknown"))
            end
          end)
        elseif comboAim and comboAim._txt then
          comboAim._txt:SetText(_DisplayFarmAim(tmpAim or "Unknown"))
        end
      end, function()
        return BuildFarmAimItems(tmpAim)
      end)
      comboAim:ClearAllPoints()
      comboAim:SetPoint("TOPLEFT", zoneLabel, "TOPLEFT", COL1_W + GAP, 0)
      comboAim:SetWidth(COL2_W); if comboAim._btn then comboAim._btn:SetWidth(COL2_W) end
      comboAim._refresh = function()
        if comboAim._txt then
          comboAim._txt:SetText(_DisplayFarmAim(tmpAim or "Unknown"))
        end
      end

      -- Row 3: Coords (left) / Type (right)
      local coordsLabel = L(box, "Coords (percent)")
      coordsLabel:SetPoint("TOPLEFT", zoneBox, "BOTTOMLEFT", 0, -14)

      local xBox = MakeInput(box, 80, 22)
      xBox:SetPoint("TOPLEFT", coordsLabel, "BOTTOMLEFT", 0, -6)
      local xHint = L(box, "X")
      xHint:SetPoint("LEFT", xBox, "RIGHT", 8, 0)

      local yBox = MakeInput(box, 80, 22)
      yBox:SetPoint("LEFT", xHint, "RIGHT", 18, 0)
      local yHint = L(box, "Y")
      yHint:SetPoint("LEFT", yBox, "RIGHT", 8, 0)

      local tmpType = "Any"
      local comboType
      comboType = MakeCombo(box, "Type", function() return tmpType end, function(v)
        tmpType = v
        if C_Timer and C_Timer.After then
          C_Timer.After(0, function()
            if comboType and comboType._txt then
              comboType._txt:SetText(_DisplayFarmType(tmpType or "Any"))
            end
          end)
        elseif comboType and comboType._txt then
          comboType._txt:SetText(_DisplayFarmType(tmpType or "Any"))
        end
      end, function()
        local items = {}
        for i = 1, #(FARM_TYPES or {}) do
          local v = tostring(FARM_TYPES[i] or "")
          if v ~= "" then
            local display = _DisplayFarmType(v)
            local body
            if v == "Any" then
              body = "No specific farm type is assigned."
            else
              body = "Set this farm type to " .. display .. "."
            end
            items[#items + 1] = { text = display, value = v, tooltipTitle = "Type", tooltipBody = body }
          end
        end
        return items
      end)
      comboType:ClearAllPoints()
      comboType:SetPoint("TOPLEFT", coordsLabel, "TOPLEFT", COL1_W + GAP, 0)
      comboType:SetWidth(COL2_W); if comboType._btn then comboType._btn:SetWidth(COL2_W) end
      comboType._refresh = function()
        if comboType._txt then
          comboType._txt:SetText(_DisplayFarmType(tmpType or "Any"))
        end
      end

      -- Row 4: Expected GPH (left)
      local expectedLabel = L(box, "Expected GPH")
      expectedLabel:SetPoint("TOPLEFT", xBox, "BOTTOMLEFT", 0, -14)
      local expectedBox = MakeInput(box, 160, 22)
      expectedBox:SetPoint("TOPLEFT", expectedLabel, "BOTTOMLEFT", 0, -6)

      -- Row 5: Image (preset dropdown + optional custom path)
      local IMAGE_PRESETS = {
        { label = "None",   path = "" },
        { label = "Custom", path = "__CUSTOM__" },
      }


      local function SeedDefaultImage(seedKey)
        seedKey = Trim(seedKey or "")
        if seedKey == "" then return nil end
        local seeds = (WF.Seeds and WF.Seeds.farms) or WF.FARM_SEEDS
        if type(seeds) ~= "table" then return nil end
        for i = 1, #seeds do
          local s = seeds[i]
          if s and s.seedKey == seedKey and s.image and s.image ~= "" then
            return s.image
          end
        end
        return nil
      end

      local function ImagePresetOptions()
        local out = {}
        for i = 1, #IMAGE_PRESETS do out[i] = IMAGE_PRESETS[i].label end
        return out
      end

      local function ImagePresetPath(label)
        for i = 1, #IMAGE_PRESETS do
          local p = IMAGE_PRESETS[i]
          if p.label == label then return p.path end
        end
        return ""
      end

      local function ImagePresetForPath(path)
        path = Trim(path or "")
        if path == "" then return "None" end
        for i = 1, #IMAGE_PRESETS do
          local p = IMAGE_PRESETS[i]
          if p.path and p.path ~= "" and p.path ~= "__CUSTOM__" and p.path == path then
            return p.label
          end
        end
        return "Custom"
      end

      local tmpImagePreset = "None"
      local ApplyImagePreset -- forward decl

      local comboImage = MakeCombo(box, "Image", function() return tmpImagePreset end, function(v) ApplyImagePreset(v) end, function() return ImagePresetOptions() end)
      comboImage:SetPoint("TOPLEFT", expectedBox, "BOTTOMLEFT", 0, -14)
      local RESET_W = 72
      comboImage:SetWidth(FULL_W - (RESET_W + 6)); if comboImage._btn then comboImage._btn:SetWidth(FULL_W - (RESET_W + 6)) end

      local bResetImage = Widgets:Button(box, "Reset", RESET_W, 22)
      bResetImage:SetPoint("LEFT", (comboImage._btn or comboImage), "RIGHT", 6, 0)
      bResetImage:Hide()

      local imageLabel = L(box, "Custom image (texture path)")
      imageLabel:SetPoint("TOPLEFT", (comboImage._btn or comboImage), "BOTTOMLEFT", 0, -12)

      local imageBox = MakeInput(box, FULL_W, 22)
      imageBox:SetPoint("TOPLEFT", imageLabel, "BOTTOMLEFT", 0, -6)

      -- Prevent accidental edits unless "Custom" is selected.
      local imageBlocker = CreateFrame("Frame", nil, box)
      imageBlocker:SetAllPoints(imageBox)
      imageBlocker:EnableMouse(true)
      imageBlocker:SetFrameLevel((imageBox:GetFrameLevel() or 0) + 5)

      local imageHint = imageBlocker:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      imageHint:SetPoint("RIGHT", imageBlocker, "RIGHT", -6, 0)
      imageHint:SetJustifyH("RIGHT")
      imageHint:SetTextColor(C4(MUTED, MUTED))
      imageHint:SetText('Pick "Custom" to edit')

      ApplyImagePreset = function(label, keepCustomText)
        tmpImagePreset = label or "None"
        local p = ImagePresetPath(tmpImagePreset) or ""

        if p == "__CUSTOM__" then
          imageBlocker:Hide()
          imageBox:SetTextColor(C4(TEXT, TEXT))
          if not keepCustomText then
            -- leave whatever is currently typed in imageBox
          end
          return
        end

        imageBox:ClearFocus()

        local eff = p or ""
        if eff == "" and tmpImagePreset == "None" then
          local f = box._farmRef
          local def = f and SeedDefaultImage(f.seedKey)
          if def and def ~= "" then eff = def end
        end

        imageBox:SetText(eff)
        imageBox:SetTextColor(C4(MUTED, MUTED))
        imageBlocker:Show()

        if tmpImagePreset == "None" and eff ~= "" then
          imageHint:SetText('Default - pick "Custom" to edit')
        else
          imageHint:SetText('Pick "Custom" to edit')
        end
      end


      bResetImage:SetScript("OnClick", function()
        tmpImagePreset = "None"
        if comboImage and comboImage._refresh then comboImage._refresh() end
        if ApplyImagePreset then ApplyImagePreset("None", false) end
      end)

      imageBlocker:SetScript("OnMouseDown", function()
        if comboImage and comboImage._btn and comboImage._btn.Click then
          comboImage._btn:Click()
        end
      end)

      -- initialize state
      ApplyImagePreset(tmpImagePreset, true)

      -- Row 6: Tags (full width)
      local tagsLabel = L(box, "Tags (comma-separated)")
      tagsLabel:SetPoint("TOPLEFT", imageBox, "BOTTOMLEFT", 0, -14)
      local tagsBox = MakeInput(box, FULL_W, 22)
      tagsBox:SetPoint("TOPLEFT", tagsLabel, "BOTTOMLEFT", 0, -6)

      -- Notes (fills remaining space above buttons)
      local notesLabel = L(box, "Notes")
      notesLabel:SetPoint("TOPLEFT", tagsBox, "BOTTOMLEFT", 0, -14)

      local notesWrap = CreateFrame("Frame", nil, box, "BackdropTemplate")
      notesWrap:SetPoint("TOPLEFT", notesLabel, "BOTTOMLEFT", 0, -6)
      notesWrap:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -PAD, 52)
      notesWrap:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8x8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
      })
      notesWrap:SetBackdropColor(0.03, 0.03, 0.03, 1.00)
      notesWrap:SetBackdropBorderColor(C4(BORDER, BORDER))

      local notesScroll = CreateFrame("ScrollFrame", nil, notesWrap, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and notesScroll.ScrollBar then Widgets:SkinScrollBar(notesScroll.ScrollBar) end
      notesScroll:SetPoint("TOPLEFT", 6, -6)
      notesScroll:SetPoint("BOTTOMRIGHT", -26, 6)

      local notesEdit = CreateFrame("EditBox", nil, notesScroll)
      notesEdit:SetMultiLine(true)
      notesEdit:SetAutoFocus(false)
      notesEdit:SetFontObject("WF_ChatFontNormal")
      notesEdit:SetTextColor(C4(TEXT, TEXT))
      notesEdit:SetWidth(FULL_W - 30)
      local function CloseMetaOnEscape() UI:CloseFarmMetaModal() end

      notesEdit:SetScript("OnEscapePressed", CloseMetaOnEscape)
      notesEdit:SetScript("OnTextChanged", function(self)
        notesScroll:UpdateScrollChildRect()
      end)
      notesScroll:SetScrollChild(notesEdit)

      -- keep edit box width in sync if the modal is resized
      notesWrap:HookScript("OnSizeChanged", function()
        local w = notesScroll:GetWidth()
        if w and w > 10 then
          notesEdit:SetWidth(w)
        end
      end)


      local btnRow = CreateFrame("Frame", nil, box)
      btnRow:SetPoint("BOTTOMLEFT", 16, 16)
      btnRow:SetPoint("BOTTOMRIGHT", -16, 16)
      btnRow:SetHeight(28)

      local bCancel = Widgets:Button(btnRow, "Cancel", 120, 26)
      bCancel:SetPoint("LEFT", 0, 0)

      local bSaveMeta = Widgets:Button(btnRow, "Save", 120, 26)
      bSaveMeta:SetPoint("RIGHT", 0, 0)

      local farmRef = nil
      box._farmRef = nil

      local function ParseCoord(s)
        s = Trim(s)
        if s == "" then return nil end
        local n = tonumber(s)
        if not n then return nil end
        if n > 1 then n = n / 100 end
        if n < 0 then n = 0 end
        if n > 1 then n = 1 end
        return n
      end

      local function FormatExpectedForEdit(copper)
        local n = tonumber(copper)
        if not n or n <= 0 then return "" end
        -- normalize to integer (prevents float/mod precision issues and scientific notation)
        n = math.floor(n + 0.5)
        if tostring(n):find("e") then return "" end

        local gold = n / 10000
        if gold >= 1000 then
          local suffix = "k"
          local divisor = 1000
          if gold >= 1000000000 then
            suffix = "b"
            divisor = 1000000000
          elseif gold >= 1000000 then
            suffix = "m"
            divisor = 1000000
          end
          local compact = gold / divisor
          local rounded = math.floor((compact * 100) + 0.5) / 100
          local out = tostring(rounded)
          out = out:gsub("(%..-)0+$", "%1")
          out = out:gsub("%.$", "")
          return out .. suffix
        end

        if gold == math.floor(gold) then
          return tostring(math.floor(gold))
        end

        if Money then return Money(n) end
        return tostring(n)
      end

      local function LoadFarm(farm)
        box._farmRef = farm
        title:SetText((farm and farm.name and farm.name ~= "" and ("Edit: " .. farm.name)) or "Edit farm")

        nameBox:SetText(farm and (farm.name or "") or "")
        zoneBox:SetText(farm and (farm.zone or "") or "")

        local x = farm and farm.x or nil
        local y = farm and farm.y or nil
        xBox:SetText(x and string.format("%.1f", x * 100) or "")
        yBox:SetText(y and string.format("%.1f", y * 100) or "")

        tmpExp = (farm and (farm.expansion or "Unknown")) or "Unknown"
        tmpAim = (farm and (farm.aim or "Unknown")) or "Unknown"
        tmpType = (farm and (farm.farmType or "Any")) or "Any"

        if comboExp._refresh then comboExp._refresh() end
        if comboAim._refresh then comboAim._refresh() end
        if comboType._refresh then comboType._refresh() end

        expectedBox:SetText(FormatExpectedForEdit(farm and farm.expectedGPH))

        local imgPath = (farm and farm.image) or ""
        imageBox:SetText(imgPath)
        if ImagePresetForPath then
          tmpImagePreset = ImagePresetForPath(imgPath)
          if comboImage and comboImage._refresh then comboImage._refresh() end
          if ApplyImagePreset then ApplyImagePreset(tmpImagePreset, true) end
        end

        if bResetImage then
          local canReset = farm and farm.seedKey and (Trim(farm.image or "") ~= "")
          if canReset then bResetImage:Show() else bResetImage:Hide() end
        end

        if farm and type(farm.tags) == "table" and #farm.tags > 0 then
          tagsBox:SetText(table.concat(farm.tags, ", "))
        else
          tagsBox:SetText("")
        end

        notesEdit:SetText((farm and farm.notes) or "")
      end

      local function DoClose()
        box:Hide()
      end

      local function DoSave()
        local f = box._farmRef
        if not f then DoClose() return end

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

        local id = f._id or f.id
        local store = (id and db.farms[id]) or f

        local name = Trim(nameBox:GetText() or "")
        if name ~= "" then store.name = name end

        store.zone = Trim(zoneBox:GetText() or "")
        if store.zone == "" then store.zone = nil end

        local xx = ParseCoord(xBox:GetText() or "")
        local yy = ParseCoord(yBox:GetText() or "")
        store.x = xx
        store.y = yy

        store.expansion = tmpExp or store.expansion

        local aimValue = Trim(tmpAim or "")
        if aimValue == "" or aimValue == "__CUSTOM__" then
          aimValue = "Unknown"
        end
        store.aim = aimValue

        store.farmType = tmpType or store.farmType

        local eg = ParseMoneyInput(expectedBox:GetText() or "")
        store.expectedGPH = eg or nil

        local img = ""
        if tmpImagePreset == "Custom" then
          img = Trim(imageBox:GetText() or "")
        else
          img = Trim(ImagePresetPath and ImagePresetPath(tmpImagePreset) or "")
        end
        store.image = (img ~= "" and img) or nil

        store.tags = ParseTags(tagsBox:GetText() or "")
        local notes = Trim(notesEdit:GetText() or "")
        store.notes = (notes ~= "" and notes) or nil

        store.updatedAt = SafeNow()

        UI:RefreshFarms()
        UI:RefreshOverview()
        UI:RefreshHistory()

        -- RefreshFarms() will re-select the current row via id match.
        DoClose()
      end

      local function ApplyEscapeClose(editBox)
        if not editBox then return end
        editBox:SetScript("OnEscapePressed", CloseMetaOnEscape)
      end

      ApplyEscapeClose(nameBox)
      ApplyEscapeClose(zoneBox)
      ApplyEscapeClose(xBox)
      ApplyEscapeClose(yBox)
      ApplyEscapeClose(expectedBox)
      ApplyEscapeClose(imageBox)
      ApplyEscapeClose(tagsBox)

      AttachTip(nameBox, "Name", "Set the display name used for this farm throughout the addon.")
      AttachTip(comboExp, "Expansion", "Assign the farm to the correct expansion. The dropdown now uses full expansion names.")
      AttachTip(zoneBox, "Zone", "Set the zone name that should be shown for this farm.")
      AttachTip(comboAim, "Aim", "Choose the farm's main purpose. Use Custom to enter your own aim label.")
      AttachTip(xBox, "X Coordinate", "Enter the X coordinate as a percent, like 45.2.")
      AttachTip(yBox, "Y Coordinate", "Enter the Y coordinate as a percent, like 61.8.")
      AttachTip(comboType, "Type", "Choose the farm type used for filtering and organization.")
      AttachTip(expectedBox, "Expected GPH", "Accepts plain gold values or shorthand like 25k, 1.2m, or 25000g.")
      AttachTip(comboImage, "Image", "Choose a preset image or switch to Custom to type a texture path.")
      AttachTip(bResetImage, "Reset Image", "Clear any custom image and fall back to the seeded default for this farm, when available.")
      AttachTip(imageBox, "Custom Image", "Type a custom WoW texture path when the Image dropdown is set to Custom.")
      AttachTip(tagsBox, "Tags", "Comma-separated search tags for the farm library.")
      AttachTip(notesEdit, "Notes", "Store route notes, pull notes, or any extra farm details here.")
      AttachTip(bCancel, "Cancel", "Close the edit modal without saving your changes.")
      AttachTip(bSaveMeta, "Save", "Save the current farm metadata changes.")
      AttachTip(close, "Close", "Close the edit modal without saving your changes.")
      AttachTip(dragBar, "Move", "Drag the top bar to move this edit window.", "ANCHOR_TOP")

      blocker:SetScript("OnMouseDown", function() end)
      close:SetScript("OnClick", function() DoClose() end)
      bCancel:SetScript("OnClick", function() DoClose() end)
      bSaveMeta:SetScript("OnClick", function() DoSave() end)
      box:SetScript("OnHide", function()
        blocker:Hide()
        nameBox:ClearFocus()
        zoneBox:ClearFocus()
        xBox:ClearFocus()
        yBox:ClearFocus()
        expectedBox:ClearFocus()
        imageBox:ClearFocus()
        tagsBox:ClearFocus()
        notesEdit:ClearFocus()
        if box._suspendHideReset then
          box._suspendHideReset = nil
          return
        end
        box._farmRef = nil
      end)

      box._loadFarm = LoadFarm

      UI._farmMeta = box
      UI._farmMetaBlocker = blocker
    end

    function UI:CloseFarmMetaModal()
      if self._farmMeta then self._farmMeta:Hide() end
    end

    local function OpenFarmMetaModalImpl(farm)
      if not farm then return end
      EnsureFarmMetaModal()
      local box = UI._farmMeta
      local blocker = UI._farmMetaBlocker
      box._loadFarm(farm)
      blocker:Show()
      box:Show()
    end

    OpenFarmMetaModal = OpenFarmMetaModalImpl

    -- -------------------------------------------------
    -- Modal: expanded farm image
    -- -------------------------------------------------
    local function EnsureFarmImageModal()
      if UI._farmImage then return end

      local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
      blocker:SetAllPoints(UIParent)
      blocker:EnableMouse(true)
      blocker:SetFrameStrata("FULLSCREEN_DIALOG")
      blocker:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
      blocker:SetBackdropColor(0, 0, 0, 0.60)
      blocker:Hide()

      local box = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
      box:SetSize(740, 480)
      box:SetPoint("CENTER", UIParent, "CENTER", 0, 30)
      box:SetFrameStrata("FULLSCREEN_DIALOG")
      box:Hide()

      box:SetBackdrop({
        bgFile = "Interface/Buttons/WHITE8x8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
      })
      box:SetBackdropColor(0.03, 0.03, 0.03, 0.96)
      box:SetBackdropBorderColor(C4(BORDER, BORDER))

      local title = box:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      title:SetPoint("TOPLEFT", 16, -14)
      title:SetTextColor(C4(TEXT, TEXT))
      title:SetText("Farm image")
      box._title = title

      local close = Widgets:Button(box, "Close", 70, 22)
      close:SetPoint("TOPRIGHT", -14, -12)

      local img = box:CreateTexture(nil, "ARTWORK")
      img:SetPoint("TOPLEFT", 16, -46)
      img:SetPoint("BOTTOMRIGHT", -16, 16)
      img:SetTexture("Interface/Icons/INV_Misc_QuestionMark")
      img:SetTexCoord(0, 1, 0, 1)
      box._img = img

      local function DoClose()
        box:Hide()
        blocker:Hide()
      end

      blocker:SetScript("OnMouseDown", function() DoClose() end)
      close:SetScript("OnClick", function() DoClose() end)

      UI._farmImage = box
      UI._farmImageBlocker = blocker
    end

    function UI:CloseFarmImageModal()
      if self._farmImage then self._farmImage:Hide() end
      if self._farmImageBlocker then self._farmImageBlocker:Hide() end
    end

    local function OpenFarmImageModalImpl(farm)
      if not farm then return end
      EnsureFarmImageModal()
      local box = UI._farmImage
      local blocker = UI._farmImageBlocker

      box._title:SetText((farm.name and farm.name ~= "" and farm.name) or "Farm image")
      box._img:SetTexture(_ResolveFarmImage(farm) or "Interface/Icons/INV_Misc_QuestionMark")

      blocker:Show()
      box:Show()
    end

    OpenFarmImageModal = OpenFarmImageModalImpl


    -- -------------------------------------------------
    -- Confirmation helpers are shared from HelpWindow.lua.
    -- Keep a local fallback here only when the shared helper
    -- has not been installed yet.
    -- -------------------------------------------------
    if not (UI and UI._EnsureConfirm and UI.ConfirmTwoActions) then
      local function EnsureConfirmModal()
        if UI._confirm and UI._confirm._yes and UI._confirm._cancel then return end

        local blocker = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
        blocker:SetAllPoints(UIParent)
        blocker:EnableMouse(true)
        blocker:SetFrameStrata("FULLSCREEN_DIALOG")
        blocker:SetFrameLevel(99)
        blocker:SetBackdrop({ bgFile = "Interface/Buttons/WHITE8x8" })
        blocker:SetBackdropColor(0, 0, 0, 0.60)
        blocker:Hide()

        local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
        box:SetSize(520, 210)
        box:SetPoint("CENTER", UIParent, "CENTER", 0, 40)
        box:SetBackdrop({
          bgFile = "Interface/Tooltips/UI-Tooltip-Background",
          edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
          edgeSize = 12,
          insets = { left = 6, right = 6, top = 6, bottom = 6 },
        })
        box:SetBackdropColor(0.06, 0.06, 0.06, 0.96)
        box:SetBackdropBorderColor(C4(BORDER, BORDER))
        box:Hide()

        local title = MakeHeaderText(box, "Confirm")
        title:SetPoint("TOPLEFT", 14, -12)
        title:SetPoint("RIGHT", -14, 0)

        local body = MakeBodyText(box, "")
        body:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
        body:SetPoint("TOPRIGHT", -14, 0)
        body:SetJustifyH("LEFT")
        body:SetJustifyV("TOP")
        if body.SetWordWrap then body:SetWordWrap(true) end

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

        local bCancel = Widgets:Button(box, "Cancel", 120, 26)
        bCancel:SetPoint("RIGHT", bYes, "LEFT", -10, 0)

        blocker:SetScript("OnMouseDown", function()
          if UI.CloseConfirm then UI:CloseConfirm() end
        end)
        box:SetScript("OnMouseDown", function() end)

        box._title = title
        box._body = body
        box._yes = bYes
        box._cancel = bCancel

        UI._confirm = box
        UI._confirmBlocker = blocker
      end

      function UI:Confirm(titleText, bodyText, onYes, yesText)
        EnsureConfirmModal()
        local box = self._confirm
        local blocker = self._confirmBlocker
        local yesButton = box._yes or box._bYes
        local cancelButton = box._cancel or box._bNo

        box._title:SetText(titleText or "Confirm")
        box._body:SetText(bodyText or "")
        if yesButton and yesButton.SetText then
          yesButton:SetText(yesText or "Confirm")
        end
        box._onYes = onYes

        if yesButton then
          yesButton:SetScript("OnClick", function()
            local fn = box._onYes
            self:CloseConfirm()
            if fn then fn() end
          end)
        end
        if cancelButton then
          cancelButton:SetScript("OnClick", function() self:CloseConfirm() end)
        end

        blocker:Show()
        box:Show()
      end

      function UI:CloseConfirm()
        if self._confirm then self._confirm:Hide() end
        if self._confirmBlocker then self._confirmBlocker:Hide() end
      end
    end
    local function OpenFarmEditModal(farm, mode)
      EnsureFarmEditModal()
      local blocker = UI._farmEditBlocker
      local box = UI._farmEdit

      mode = mode or "desc"
      local isTags   = (mode == "tags")
      local isRename = (mode == "rename")
      local isDesc   = (mode == "desc") or (not isTags and not isRename)

      box:SetSize(520, (isTags or isRename) and 210 or 340)

      if isRename then
        box._title:SetText("Rename farm")
        box._sub:SetText("Set a display name for this farm.")
      elseif isTags then
        box._title:SetText("Edit tags")
        box._sub:SetText("Comma-separated tags (ex: raw gold, transmog, cloth).")
      else
        box._title:SetText("Edit description")
        box._sub:SetText("A short note about the route, pull, reset method, etc.")
      end

      box._input:SetShown(isTags or isRename)
      box._inputML:SetShown(isDesc)

      if isTags then
        if type(farm.tags) == "table" and #farm.tags > 0 then
          box._input:SetText(table.concat(farm.tags, ", "))
        else
          box._input:SetText("")
        end
      elseif isRename then
        box._input:SetText(farm.name or "")
      else
        box._inputML:SetText(farm.notes or "")
      end

      box._save:SetScript("OnClick", function()
        if not farm then UI:CloseFarmEditModal(); return end

        if isTags then
          local t = box._input:GetText() or ""
          local tags = {}
          for part in string.gmatch(t, "([^,]+)") do
            local s = (part:gsub("^%s+", ""):gsub("%s+$", ""))
            if s ~= "" then tags[#tags + 1] = s end
          end
          farm.tags = tags
        elseif isRename then
          local nm = (box._input:GetText() or "")
          nm = nm:gsub("^%s+", ""):gsub("%s+$", "")
          if nm ~= "" then
            farm.name = nm
          end
        else
          farm.notes = box._inputML:GetText() or ""
        end

        farm.updatedAt = SafeNow()
        UI:CloseFarmEditModal()
        UI:RefreshFarms()
        UI:RefreshOverview()
      end)

      box._cancel:SetScript("OnClick", function() UI:CloseFarmEditModal() end)

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

      if isTags or isRename then
        if box._input.SetFocus then box._input:SetFocus() end
        if box._input.HighlightText then box._input:HighlightText() end
      else
        if box._inputML.SetFocus then box._inputML:SetFocus() end
      end
    end


    local rows = {}
    local rowH = 30
    -- Column layout (responsive)
    local COL = {
      padL = 12,
      padR = 12,
      gap  = 10,
      nameW = 0,
      aimW  = 0,
      typeW = 0,
      expW  = 0,
      gphW  = 0,
      showAim = true,
      showType = false,
      showExp = true,
      showGph = true,
      innerW = 0,
    }

    local function UpdateColumns(onlyRow)
      local w = L.scroll and L.scroll:GetWidth() or 0
      if not w or w <= 10 then return end

      local inner = w - COL.padL - COL.padR
      if inner < 260 then inner = 260 end

      -- Priority order:
      -- Farm + Details + GPH stay alive as long as possible.
      -- Zone is the first column to disappear when space gets tight.
      local gap = COL.gap
      local aimPref, aimMin, expPref, expMin, gphW, nameMin, typeW

      COL.showType = false
      COL.showAim = true
      COL.showGph = inner >= 300

      if inner >= 560 then
        gap = 10
        aimPref = 120
        aimMin = 104
        expPref = 140
        expMin = 120
        gphW = COL.showGph and 100 or 0
        nameMin = 138
      elseif inner >= 500 then
        gap = 10
        aimPref = 110
        aimMin = 96
        expPref = 126
        expMin = 112
        gphW = COL.showGph and 98 or 0
        nameMin = 122
      elseif inner >= 430 then
        gap = 8
        aimPref = 100
        aimMin = 88
        expPref = 116
        expMin = 104
        gphW = COL.showGph and 96 or 0
        nameMin = 106
      else
        gap = 6
        aimPref = 90
        aimMin = 80
        expPref = 108
        expMin = 98
        gphW = COL.showGph and 92 or 0
        nameMin = 88
      end

      typeW = 0
      COL.gap = gap

      local fixedCore = 0
      fixedCore = fixedCore + aimMin + gap
      if COL.showGph then
        fixedCore = fixedCore + gphW + gap
      end

      COL.showExp = inner >= (nameMin + fixedCore + expMin + gap)

      local fixed = 0
      local aimW = aimPref
      local expW = 0

      if COL.showExp then
        expW = expPref
        fixed = fixed + expW + gap
      end

      fixed = fixed + aimW + gap
      if COL.showGph then
        fixed = fixed + gphW + gap
      end

      local nameW = inner - fixed

      if nameW < nameMin then
        local need = nameMin - nameW
        local spareAim = aimW - aimMin
        if spareAim > 0 then
          local take = need
          if take > spareAim then take = spareAim end
          aimW = aimW - take
          need = need - take
        end

        if need > 0 and COL.showExp then
          local spareExp = expW - expMin
          if spareExp > 0 then
            local take = need
            if take > spareExp then take = spareExp end
            expW = expW - take
            need = need - take
          end
        end

        fixed = 0
        if COL.showExp then fixed = fixed + expW + gap end
        fixed = fixed + aimW + gap
        if COL.showGph then fixed = fixed + gphW + gap end
        nameW = inner - fixed

        if nameW < nameMin and COL.showExp then
          COL.showExp = false
          expW = 0
          fixed = aimW + gap
          if COL.showGph then fixed = fixed + gphW + gap end
          nameW = inner - fixed
        end

        if nameW < nameMin then
          nameW = nameMin
        end
      end

      COL.innerW = inner
      COL.nameW, COL.aimW, COL.typeW, COL.expW, COL.gphW = nameW, aimW, typeW, expW, gphW

      -- Header positioning
      L.hName:ClearAllPoints()
      L.hName:SetPoint("LEFT", L.colHeader, "LEFT", COL.padL, 0)
      L.hName:SetWidth(COL.nameW)

      local x = COL.padL + COL.nameW + gap

      L.hExp:ClearAllPoints()
      if COL.showExp then
        L.hExp:Show()
        L.hExp:SetPoint("LEFT", L.colHeader, "LEFT", x, 0)
        L.hExp:SetWidth(COL.expW)
        x = x + COL.expW + gap
      else
        L.hExp:Hide()
      end

      L.hType:ClearAllPoints()
      L.hType:Hide()

      L.hAim:ClearAllPoints()
      if COL.showAim then
        L.hAim:Show()
        L.hAim:SetPoint("LEFT", L.colHeader, "LEFT", x, 0)
        L.hAim:SetWidth(COL.aimW)
        x = x + COL.aimW + gap
      else
        L.hAim:Hide()
      end

      L.hGph:ClearAllPoints()
      if COL.showGph then
        L.hGph:Show()
        L.hGph:SetPoint("RIGHT", L.colHeader, "RIGHT", -COL.padR, 0)
        L.hGph:SetWidth(COL.gphW)
      else
        L.hGph:Hide()
      end

      -- Header click targets (so the list sorts when you click the headers)
      if headerBtns then
        local hw = (L.colHeader and L.colHeader.GetWidth and L.colHeader:GetWidth()) or w or 0
        if not hw or hw <= 0 then hw = w or 0 end

        local gap = COL.gap
        local starts = {}

        -- Column start positions (relative to colHeader LEFT)
        starts.name = COL.padL
        local x = COL.padL + COL.nameW + gap

        if COL.showExp then
          starts.zone = x
          x = x + COL.expW + gap
        end
        if COL.showAim then
          starts.aim = x
          x = x + COL.aimW + gap
        end

        if COL.showGph then
          local gphStart = hw - (COL.padR + COL.gphW)
          if gphStart < 0 then gphStart = 0 end
          starts.gph = gphStart
        end

        local order = { "name" }
        if COL.showExp then order[#order + 1] = "zone" end
        if COL.showAim then order[#order + 1] = "aim" end
        if COL.showGph then order[#order + 1] = "gph" end

        -- Stretch click targets so the hover highlight fills padding + gutters
        for i = 1, #order do
          local key = order[i]
          local b = headerBtns[key]
          if b then
            b:ClearAllPoints()
            b:SetPoint("TOP", L.colHeader, "TOP", 0, 0)
            b:SetPoint("BOTTOM", L.colHeader, "BOTTOM", 0, 0)

            local left = (key == "name") and 0 or (starts[key] or 0)
            local right

            if key == "gph" then
              right = hw
            else
              local nextKey = order[i + 1]
              if nextKey then
                right = starts[nextKey] or hw
              else
                right = hw
              end
            end

            b:SetPoint("LEFT", L.colHeader, "LEFT", left, 0)
            b:SetPoint("RIGHT", L.colHeader, "LEFT", right, 0)
            b:SetShown(true)
          end
        end

        -- Hide collapsed columns (in case they were visible previously)
        if headerBtns.aim  then headerBtns.aim:SetShown(COL.showAim) end
        if headerBtns.type then headerBtns.type:SetShown(false) end
        if headerBtns.zone then headerBtns.zone:SetShown(COL.showExp) end
        if headerBtns.gph  then headerBtns.gph:SetShown(COL.showGph) end
      end
      if _UpdateSortHeaders then _UpdateSortHeaders() end

      -- Apply to existing rows (so resizing keeps alignment)
      local function ApplyRow(r)
        if not (r and r.name and r.aim and r.typ and r.exp and r.gph) then return end

        r.name:ClearAllPoints()
        r.name:SetPoint("LEFT", COL.padL, 0)
        r.name:SetWidth(r._isHeader and COL.innerW or COL.nameW)

        local rx = COL.padL + (r._isHeader and COL.innerW or COL.nameW) + gap

        r.exp:ClearAllPoints()
        if r.zoneHot then r.zoneHot:ClearAllPoints() end
        if (not r._isHeader) and COL.showExp then
          r.exp:Show()
          r.exp:SetPoint("LEFT", rx, 0)
          r.exp:SetWidth(COL.expW)

          if r.zoneHot then
            r.zoneHot:Show()
            r.zoneHot:SetPoint("TOPLEFT", rx, -2)
            r.zoneHot:SetPoint("BOTTOMLEFT", rx, 2)
            r.zoneHot:SetWidth(COL.expW)
          end

          rx = rx + COL.expW + gap
        else
          r.exp:Hide()
          if r.zoneHot then r.zoneHot:Hide() end
        end

        r.typ:ClearAllPoints()
        r.typ:Hide()

        r.aim:ClearAllPoints()
        if r.detailHot then r.detailHot:ClearAllPoints() end
        if (not r._isHeader) and COL.showAim then
          r.aim:Show()
          r.aim:SetPoint("LEFT", rx, 0)
          r.aim:SetWidth(COL.aimW)

          if r.detailHot then
            r.detailHot:Show()
            r.detailHot:SetPoint("TOPLEFT", rx, -2)
            r.detailHot:SetPoint("BOTTOMLEFT", rx, 2)
            r.detailHot:SetWidth(COL.aimW)
          end

          rx = rx + COL.aimW + gap
        else
          r.aim:Hide()
          if r.detailHot then r.detailHot:Hide() end
        end

        r.gph:ClearAllPoints()
        if r.gphHot then r.gphHot:ClearAllPoints() end
        if (not r._isHeader) and COL.showGph then
          r.gph:Show()
          r.gph:SetPoint("RIGHT", -COL.padR, 0)
          r.gph:SetWidth(COL.gphW)

          if r.gphHot then
            r.gphHot:Show()
            r.gphHot:SetPoint("TOPRIGHT", r, "TOPRIGHT", -COL.padR, -2)
            r.gphHot:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", -COL.padR, 2)
            r.gphHot:SetWidth(COL.gphW)
          end
        else
          r.gph:Hide()
          if r.gphHot then r.gphHot:Hide() end
        end
      end

      if onlyRow then
        ApplyRow(onlyRow)
      else
        for i = 1, #rows do
          ApplyRow(rows[i])
        end
      end
    end

    pFarms._render = {}
    local ClearSelection
    local function _BindFarmListDeselection()
      if not (L and L.scroll and L.scroll.SetScript) then return end
      L.scroll:EnableMouse(true)
      L.scroll:SetScript("OnMouseUp", function(_, button)
        if button ~= "LeftButton" then return end
        if not R.selectedFarm then return end
        ClearSelection()
      end)
    end

    ClearSelection = function()
      R.selectedFarm, R.selectedRow = nil, nil
      for i = 1, #rows do
        if rows[i] then
          if rows[i].sel then rows[i].sel:SetAlpha(0) end
          if rows[i].bg then rows[i].bg:SetAlpha(0) end
        end
      end
      R.rName:SetText("Select a farm")
      R.kvZone:SetText("-"); R.kvCoords:SetText("-"); R.kvAim:SetText("-"); R.kvExp:SetText("-"); R.kvType:SetText("-")
      R.kvAvgGph:SetText("-"); R.kvExpected:SetText("-"); R.kvUpdated:SetText("-")
      if R.kvAvgLabel and R.kvAvgLabel.SetText then
        R.kvAvgLabel:SetText((_FarmsMetric() == "liv") and "Avg LIV:" or "Gold per Hour:")
      end
      _RefreshMetaSection("", "")
      if R.rScroll and R.rScroll.SetVerticalScroll then R.rScroll:SetVerticalScroll(0) end
      _SyncPreview(nil)
      SetBtnEnabled(R.bEditFarm, false)
      SetBtnEnabled(R.bShareFarm, false)
      if UI and UI._UpdateFarmsRightScroll then UI:_UpdateFarmsRightScroll() end
    end

    local function SelectFarm(farm, row)
      R.selectedFarm = farm
      R.selectedRow = row or R.selectedRow

      for i = 1, #rows do
        if rows[i] and rows[i] ~= R.selectedRow then
          if rows[i].sel then rows[i].sel:SetAlpha(0) end
          if rows[i].bg then rows[i].bg:SetAlpha(0) end
        end
      end
      if R.selectedRow then
        if R.selectedRow.sel then R.selectedRow.sel:SetAlpha(0) end
        if R.selectedRow.bg then R.selectedRow.bg:SetAlpha(0.08) end
      end

      if not farm then
        ClearSelection()
        return
      end

      local db = WF.DB:Get() or {}
      local metric = _FarmsMetric()
      local avg = FarmAvgMetric(db, farm)

      if R.kvAvgLabel and R.kvAvgLabel.SetText then
        R.kvAvgLabel:SetText((metric == "liv") and "Avg LIV:" or "Gold per Hour:")
      end

      R.rName:SetText(farm.name or "Farm")

      R.kvZone:SetText(farm.zone or "Unknown")
      if farm.x and farm.y then
        R.kvCoords:SetText(string.format("%.1f, %.1f", farm.x * 100, farm.y * 100))
      else
        R.kvCoords:SetText("-")
      end
      R.kvAim:SetText(_DisplayFarmAim(farm.aim or "Unknown"))
      R.kvExp:SetText(_DisplayExpansion(farm.expansion or "Unknown"))
      R.kvType:SetText(_DisplayFarmType(farm.farmType or "Any"))

      R.kvAvgGph:SetText((avg and avg > 0) and (Money(avg)) or "-")
      R.kvExpected:SetText((farm.expectedGPH and tonumber(farm.expectedGPH) > 0) and (Money(farm.expectedGPH)) or "-")
      R.kvUpdated:SetText(farm.updatedAt and FormatTime(farm.updatedAt) or "-")

      local tagsValue = ""
      if type(farm.tags) == "table" then
        local cleanTags = {}
        for i = 1, #farm.tags do
          local v = tostring(farm.tags[i] or "")
          v = v:gsub("^%s+", ""):gsub("%s+$", "")
          if v ~= "" and v ~= "-" then
            cleanTags[#cleanTags + 1] = v
          end
        end
        if #cleanTags > 0 then
          tagsValue = table.concat(cleanTags, ", ")
        end
      elseif type(farm.tags) == "string" then
        tagsValue = tostring(farm.tags or "")
        tagsValue = tagsValue:gsub("^%s+", ""):gsub("%s+$", "")
        if tagsValue == "-" then
          tagsValue = ""
        end
      end

      local noteValue = tostring(farm.notes or "")
      noteValue = noteValue:gsub("^%s+", ""):gsub("%s+$", "")
      if noteValue == "-" then
        noteValue = ""
      end

      _RefreshMetaSection(tagsValue, noteValue)

      if R.rScroll and R.rScroll.SetVerticalScroll then R.rScroll:SetVerticalScroll(0) end

      _SyncPreview(farm)
      SetBtnEnabled(R.bEditFarm, true)
      SetBtnEnabled(R.bShareFarm, true)
      if UI and UI._UpdateFarmsRightScroll then UI:_UpdateFarmsRightScroll() end
    end

    _BindFarmListDeselection()


    R.bEditFarm:SetScript("OnClick", function()
      if R.selectedFarm then
        OpenFarmMetaModal(R.selectedFarm)
      end
    end)

    R.bShareFarm:SetScript("OnClick", function()
      if R.selectedFarm and WF and WF.Share and WF.Share.ShowFarmShareDialog then
        WF.Share:ShowFarmShareDialog(R.selectedFarm)
      end
    end)

    R.imgBtn:SetScript("OnEnter", function()
      if _ResolveFarmImage(R.selectedFarm) then
        _RefreshImageHintBadge(true)
      end
    end)

    R.imgBtn:SetScript("OnLeave", function()
      _RefreshImageHintBadge(false)
    end)

    R.imgBtn:SetScript("OnClick", function()
      if not R.selectedFarm then return end
      if _ResolveFarmImage(R.selectedFarm) then
        OpenFarmImageModal(R.selectedFarm)
      else
        OpenFarmMetaModal(R.selectedFarm)
      end
    end)

    -- Zone column helpers (Farm Library: show Zone, tooltip includes Expansion + Coords)
    local function _FarmZoneLabel(farm)
      if not farm then return "Unknown" end
      local z = farm.zone
      if z and z ~= "" then return z end
      if farm.mapId and C_Map and C_Map.GetMapInfo then
        local info = C_Map.GetMapInfo(farm.mapId)
        if info and info.name then return info.name end
      end
      return "Unknown"
    end

    local function _ZoneCoordString(farm)
      if farm and farm.x and farm.y then
        return string.format("%.1f, %.1f", farm.x * 100, farm.y * 100)
      end
      return "-"
    end

    local function _FarmPrimaryDetail(farm)
      local aim = _DisplayFarmAim((farm and farm.aim) or "")
      local typ = _DisplayFarmType((farm and farm.farmType) or "")

      if aim ~= "" and aim ~= "Unknown" and aim ~= "Any" and aim ~= "-" then
        return aim
      end
      if typ ~= "" and typ ~= "Unknown" and typ ~= "Any" and typ ~= "-" then
        return typ
      end
      if aim ~= "" and aim ~= "-" then
        return aim
      end
      if typ ~= "" and typ ~= "-" then
        return typ
      end
      return "-"
    end

    local function _FormatDurationShort(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", h, m)
      end
      if m > 0 then
        return string.format("%dm", m)
      end
      return string.format("%ds", s)
    end

    local function _ShowZoneTooltip(owner, farm)
      if not (GameTooltip and owner and farm) then return end
      if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
      GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
      GameTooltip:ClearLines()
      local pal = (Widgets and Widgets.GetTooltipPalette and Widgets:GetTooltipPalette()) or {
        title = { 0.35, 0.78, 1.00, 1.00 },
        body = { 0.92, 0.92, 0.92, 1.00 },
        muted = { 0.65, 0.65, 0.65, 1.00 },
      }
      GameTooltip:AddLine(_FarmZoneLabel(farm), pal.title[1], pal.title[2], pal.title[3], true)
      GameTooltip:AddLine("Expansion: " .. _DisplayExpansion(farm.expansion or "Unknown"), pal.muted[1], pal.muted[2], pal.muted[3], true)
      GameTooltip:AddLine("Coords: " .. _ZoneCoordString(farm), pal.muted[1], pal.muted[2], pal.muted[3], true)
      GameTooltip:Show()
    end

    -- Build a per-run breakdown for the tooltip from History sessions.
    -- Note: farmStats stores totals, so this is best-effort (History may be pruned).
    local function _CollectHistoryRunsForFarmMode(db, farm, farmId, mode, metric)
      local sessions = db and db.history and db.history.sessions
      if type(sessions) ~= "table" then return nil end

      local out = {}
      mode = tostring(mode or "Default")
      local modeL = mode:lower()
      local wantId = tostring(farmId or "")
      local farmName = farm and tostring(farm.name or "") or ""
      local farmNameL = farmName:lower()
      local farmMap = farm and (farm.mapId or farm.mapID or farm.map) or nil

      for i = 1, #sessions do
        local s = sessions[i]
        if type(s) == "table" then
          local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
          if (not sid) and farmName ~= "" then
            local sname = tostring(s.farmName or s._wfCustomFarmName or (s.farm and s.farm.name) or "")
            if sname ~= "" and sname:lower() == farmNameL then
              local smap = s.mapId or (s.farm and (s.farm.mapId or s.farm.mapID or s.farm.map)) or nil
              if (not farmMap) or (not smap) or (smap == farmMap) then
                sid = wantId
              end
            end
          end

          if sid and (wantId == "" or tostring(sid) == wantId) then
            local smode = tostring(s.mode or s.profile or s.profileName or s.farmMode or "Default")
            if smode:lower() == modeL then
              local elapsed = tonumber(s.elapsedSec or s.elapsed or s.durationSec or s.duration) or 0
              local minAvgSeconds = tonumber((db and db.settings and db.settings.historyAvgMinSeconds) or 0) or 0
              if elapsed > 0 and elapsed >= minAvgSeconds then
                local total = tonumber(s.totalValue or s.valueCopper or s.value) or 0
                local gold = tonumber(s.money or s.moneyGained or s.lootedGoldValue or s.goldValue or s.rawGoldValue or s.goldLooted or s.lootedGold) or 0
                local v = total

                if metric == "liv" then
                  v = total - gold
                  if v < 0 then v = 0 end
                end

                local perHour = (v / elapsed) * 3600
                local when = tonumber(s.endedAt) or tonumber(s.startedAt) or tonumber(s.ts) or i
                out[#out + 1] = {
                  when = when,
                  perHour = perHour,
                  elapsed = elapsed,
                  value = v,
                }
              end
            end
          end
        end
      end

      table.sort(out, function(a, b)
        return (tonumber(a.when) or 0) > (tonumber(b.when) or 0)
      end)

      return out
    end

    -- Hover tooltip for Gold per Hour column (shows mode bucket sample size + tracked time)
    local function _ShowAvgTooltip(owner, farm)
      if not (GameTooltip and owner and farm) then return end

      local db = WF.DB:Get() or {}
      local id = farm.id or farm._id

      local bucket, mode, isMode, fs, followsActive = _GetFarmStatsBucket(db, id)

      if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
      GameTooltip:SetOwner(owner, "ANCHOR_RIGHT")
      GameTooltip:ClearLines()
      local pal = (Widgets and Widgets.GetTooltipPalette and Widgets:GetTooltipPalette()) or {
        title = { 0.35, 0.78, 1.00, 1.00 },
        body = { 0.92, 0.92, 0.92, 1.00 },
        muted = { 0.65, 0.65, 0.65, 1.00 },
      }
      GameTooltip:AddLine(tostring(farm.name or "Farm"), pal.title[1], pal.title[2], pal.title[3], true)

      local metric = _FarmsMetric()
      local label = (metric == "liv") and "Avg LIV" or "Gold per Hour"

      GameTooltip:AddLine(string.format("View: %s%s", DisplayProfileName(mode or "Default"), (followsActive and " (Active)" or "")), pal.muted[1], pal.muted[2], pal.muted[3], true)

      if not bucket or (tonumber(bucket.time) or 0) <= 0 then
        GameTooltip:AddLine("No tracked runs for this mode yet", pal.muted[1], pal.muted[2], pal.muted[3], true)
        local minAvgSeconds = tonumber((db.settings and db.settings.historyAvgMinSeconds) or 0) or 0
        if minAvgSeconds > 0 then
          GameTooltip:AddLine(string.format("Min duration for averages: %s", _FormatDurationShort(minAvgSeconds)), pal.muted[1], pal.muted[2], pal.muted[3], true)
        end

        GameTooltip:Show()
        return
      end

      local time = tonumber(bucket.time) or 0
      local runs = tonumber(bucket.sessions) or 0
      local total = tonumber(bucket.value) or 0
      local gold  = tonumber(bucket.gold) or 0
      local liv = total - gold
      if liv < 0 then liv = 0 end

      local perHour = 0
      if metric == "liv" then
        perHour = (time > 0) and (liv / time * 3600) or 0
      else
        perHour = (time > 0) and (total / time * 3600) or 0
      end

      GameTooltip:AddLine(string.format("%s: %s", label, Money(perHour)), pal.body[1], pal.body[2], pal.body[3], true)
      GameTooltip:AddLine(string.format("Runs: %d", runs), pal.muted[1], pal.muted[2], pal.muted[3], true)
      GameTooltip:AddLine(string.format("Tracked: %s", _FormatDurationShort(time)), pal.muted[1], pal.muted[2], pal.muted[3], true)

      local minAvgSeconds = tonumber((db.settings and db.settings.historyAvgMinSeconds) or 0) or 0
      if minAvgSeconds > 0 then
        GameTooltip:AddLine(string.format("Min duration for averages: %s", _FormatDurationShort(minAvgSeconds)), pal.muted[1], pal.muted[2], pal.muted[3], true)
      end

      -- Per-run breakdown (from History, best-effort)
      do
        local hist = _CollectHistoryRunsForFarmMode(db, farm, id, mode, metric)
        if type(hist) == "table" and #hist > 0 then
          local showN = 10
          if #hist < showN then showN = #hist end

          GameTooltip:AddLine(" ", 1, 1, 1)
          GameTooltip:AddLine("Runs (this mode):", pal.title[1], pal.title[2], pal.title[3], true)

          for i = 1, showN do
            local r = hist[i]
            local rate = (Money and Money(r.perHour) or tostring(math.floor((tonumber(r.perHour) or 0) / 10000)))
            local dur = _FormatDurationShort(tonumber(r.elapsed) or 0)
            GameTooltip:AddLine(string.format("Run %d: %s  •  %s", i, rate, dur), pal.body[1], pal.body[2], pal.body[3], true)
          end

          if #hist > showN then
            GameTooltip:AddLine(string.format("…and %d more", (#hist - showN)), pal.muted[1], pal.muted[2], pal.muted[3], true)
          end

          if runs > #hist then
            GameTooltip:AddLine(" ", 1, 1, 1)
            GameTooltip:AddLine(string.format("Note: %d tracked runs are not in History (older/pruned)", (runs - #hist)), pal.muted[1], pal.muted[2], pal.muted[3], true)
          end
        end
      end

      GameTooltip:Show()
    end

-- Shared: blue QuestTitleHighlight hover (keeps the feathered WoW look but matches WF accent)
local function _WF_ApplyBlueQuestHighlight(btn, alpha)
  if not btn or not btn.SetHighlightTexture then return end

  btn:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight", "ADD")
  local hl = (btn.GetHighlightTexture and btn:GetHighlightTexture()) or nil
  if not hl then return end

  hl:ClearAllPoints()
  hl:SetPoint("TOPLEFT", btn, "TOPLEFT", 6, 0)
  hl:SetPoint("BOTTOMRIGHT", btn, "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 = (ACCENT and ACCENT[1]) or 0.25
    ag = (ACCENT and ACCENT[2]) or 0.75
    ab = (ACCENT and ACCENT[3]) or 1.00
  end

  hl:SetVertexColor(ar, ag, ab, alpha or 0.22)
end



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

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

      _WF_ApplyBlueQuestHighlight(r, 0.22)

      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 = (ACCENT and ACCENT[1]) or 0.25
          ag = (ACCENT and ACCENT[2]) or 0.75
          ab = (ACCENT and ACCENT[3]) or 1.00
        end
        r.bg:SetVertexColor(ar, ag, ab, 1)
      end
      r.bg:SetAlpha(0)

      r.sel = r:CreateTexture(nil, "BACKGROUND")
      r.sel:ClearAllPoints()
      r.sel:SetPoint("TOPLEFT", r, "TOPLEFT", 6, 0)
      r.sel:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", -6, 0)
      r.sel:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
      if r.sel.SetBlendMode then r.sel:SetBlendMode("ADD") end
      if r.sel.SetTexCoord then r.sel:SetTexCoord(0.02, 0.98, 0, 1) end
      if r.sel.SetDesaturation then
        r.sel:SetDesaturation(1)
      elseif r.sel.SetDesaturated then
        r.sel:SetDesaturated(true)
      end
      local ar, ag, ab
      if C4 then ar, ag, ab = C4(ACCENT, ACCENT) end
      if not ar then
        ar = (ACCENT and ACCENT[1]) or 0.25
        ag = (ACCENT and ACCENT[2]) or 0.75
        ab = (ACCENT and ACCENT[3]) or 1.00
      end
      r.sel:SetVertexColor(ar, ag, ab, 0.16)
      r.sel:SetAlpha(0)

      r.name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
      r.name:SetPoint("LEFT", 12, 0)
      r.name:SetTextColor(C4(TEXT, TEXT))
      r.name:SetJustifyH("LEFT")
      if r.name.SetMaxLines then r.name:SetMaxLines(1) end
      if r.name.SetWordWrap then r.name:SetWordWrap(false) end

      -- Structured columns (consistent with Overview/History)
      r.aim = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.aim:SetTextColor(C4(MUTED, MUTED))
      r.aim:SetJustifyH("LEFT")

      r.typ = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.typ:SetTextColor(C4(MUTED, MUTED))
      r.typ:SetJustifyH("LEFT")

      r.exp = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.exp:SetTextColor(C4(MUTED, MUTED))
      r.exp:SetJustifyH("LEFT")

-- Hover tooltip for Zone column (shows expansion + coords)
r.zoneHot = CreateFrame("Frame", nil, r)
r.zoneHot:SetFrameLevel(r:GetFrameLevel() + 3)
r.zoneHot:EnableMouse(true)
r.zoneHot:Hide()

r.zoneHot:SetScript("OnEnter", function(self)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnEnter")
    if fn then fn(parent) end
  end
  local farm = parent and parent._farm
  _ShowZoneTooltip(self, farm)
end)

r.zoneHot:SetScript("OnLeave", function(self)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnLeave")
    if fn then fn(parent) end
  end
  if GameTooltip then GameTooltip:Hide() end
end)

r.zoneHot:SetScript("OnMouseUp", function(self, button)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnMouseUp")
    if fn then fn(parent, button) end
  end
end)

-- Hover tooltip for Details column (shows aim + type)
r.detailHot = CreateFrame("Frame", nil, r)
r.detailHot:SetFrameLevel(r:GetFrameLevel() + 3)
r.detailHot:EnableMouse(true)
r.detailHot:Hide()

r.detailHot:SetScript("OnEnter", function(self)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnEnter")
    if fn then fn(parent) end
  end

  local farm = parent and parent._farm
  if not (GameTooltip and farm) then return end
  if Widgets and Widgets.EnsureTooltipSkin then Widgets:EnsureTooltipSkin() end
  GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
  GameTooltip:ClearLines()

  local pal = (Widgets and Widgets.GetTooltipPalette and Widgets:GetTooltipPalette()) or {
    title = { 0.35, 0.78, 1.00, 1.00 },
    body = { 0.92, 0.92, 0.92, 1.00 },
    muted = { 0.65, 0.65, 0.65, 1.00 },
  }

  GameTooltip:AddLine(_FarmPrimaryDetail(farm), pal.title[1], pal.title[2], pal.title[3], true)
  GameTooltip:AddLine("Aim: " .. _DisplayFarmAim(farm.aim or "Unknown"), pal.muted[1], pal.muted[2], pal.muted[3], true)
  GameTooltip:AddLine("Type: " .. _DisplayFarmType(farm.farmType or "Any"), pal.muted[1], pal.muted[2], pal.muted[3], true)
  GameTooltip:Show()
end)

r.detailHot:SetScript("OnLeave", function(self)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnLeave")
    if fn then fn(parent) end
  end
  if GameTooltip then GameTooltip:Hide() end
end)

r.detailHot:SetScript("OnMouseUp", function(self, button)
  local parent = self:GetParent()
  if parent and parent.GetScript then
    local fn = parent:GetScript("OnMouseUp")
    if fn then fn(parent, button) end
  end
end)

      r.gph = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
      r.gph:SetTextColor(C4(MUTED, MUTED))
      r.gph:SetJustifyH("RIGHT")

      -- Hover tooltip for Avg column (runs + tracked time, per active mode)
      r.gphHot = CreateFrame("Frame", nil, r)
      r.gphHot:SetFrameLevel(r:GetFrameLevel() + 3)
      r.gphHot:EnableMouse(true)
      r.gphHot:Hide()

      r.gphHot:SetScript("OnEnter", function(self)
        local parent = self:GetParent()
        if parent and parent.GetScript then
          local fn = parent:GetScript("OnEnter")
          if fn then fn(parent) end
        end
        local farm = parent and parent._farm
        _ShowAvgTooltip(self, farm)
      end)

      r.gphHot:SetScript("OnLeave", function(self)
        local parent = self:GetParent()
        if parent and parent.GetScript then
          local fn = parent:GetScript("OnLeave")
          if fn then fn(parent) end
        end
        if GameTooltip then GameTooltip:Hide() end
      end)

      r.gphHot:SetScript("OnMouseUp", function(self, button)
        local parent = self:GetParent()
        if parent and parent.GetScript then
          local fn = parent:GetScript("OnMouseUp")
          if fn then fn(parent, button) end
        end
      end)

      r.aim:Hide(); r.typ:Hide(); r.exp:Hide(); r.gph:Hide()
      if r.detailHot then r.detailHot:Hide() end

      r.sep = r:CreateTexture(nil, "BORDER")
      r.sep:SetTexture("Interface/Buttons/WHITE8x8")
      local px = 1
      local leftX, rightX = 12, -12
      local y = 2
      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.sep:SetHeight(px)
      if Widgets and Widgets.MakePixelPerfect then
        Widgets:MakePixelPerfect(r.sep, r)
      end
      r.sep:SetPoint("BOTTOMLEFT", leftX, y)
      r.sep:SetPoint("BOTTOMRIGHT", rightX, y)
      r.sep:SetAlpha(0.18)
      r.sep:Hide()

      -- Row divider (shown for normal farm rows; matches Overview/History)
      r.line = r:CreateTexture(nil, "BORDER")
      r.line:SetTexture("Interface/Buttons/WHITE8x8")
      r.line:SetVertexColor(1, 1, 1, 0.06)

      do
        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)
      end
      r.line:Show()


      r:SetScript("OnEnter", function(self)
        if self._isHeader then return end
        if R.selectedRow ~= self then
          self.bg:SetAlpha(0)
        end
      end)
      r:SetScript("OnLeave", function(self)
        if self._isHeader then return end
        if R.selectedRow == self then
          self.bg:SetAlpha(0.08)
        else
          self.bg:SetAlpha(0)
        end
      end)

      r:SetScript("OnMouseUp", function(self, button)
        if self._isHeader then return end
        local f = self._farm
        if not f then return end

        local isSameSelection = false
        if R.selectedFarm and R.selectedRow == self then
          if R.selectedFarm == f then
            isSameSelection = true
          else
            local selectedId = R.selectedFarm.id or R.selectedFarm._id
            local farmId = f.id or f._id
            if selectedId and farmId and selectedId == farmId then
              isSameSelection = true
            end
          end
        end

        if button == "LeftButton" and isSameSelection then
          ClearSelection()
          return
        end

        SelectFarm(f, self)

        if button == "RightButton" then
          local hasWaypoint = (f.mapId and f.x and f.y) and true or false
          UI:OpenContextMenu({
            { text = (f.name or "Farm"), passive = true, textColor = { ACCENT[1] or 0.32, ACCENT[2] or 0.72, ACCENT[3] or 1.00, 1.00 } },
            { sep = true },
            { text = "Edit", fn = function() OpenFarmMetaModal(f) end },
            { text = "Share", fn = function()
                if WF and WF.Share and WF.Share.ShowFarmShareDialog then
                  WF.Share:ShowFarmShareDialog(f)
                end
              end
            },
            { sep = true },
            { text = "Set Waypoint", disabled = not hasWaypoint, fn = function()
                SetWaypoint(f.mapId, f.x, f.y)
                if ToggleWorldMap then ToggleWorldMap() end
              end
            },
            { text = "Send Coordinates to Chat", fn = function()
                local msg = (f.name or "Farm") .. " - " .. (f.zone or "Unknown Zone")
                if f.x and f.y then msg = msg .. string.format(" (%.1f, %.1f)", f.x * 100, f.y * 100) end
                if ChatEdit_InsertLink and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
                  ChatEdit_InsertLink(msg)
                elseif DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                  DEFAULT_CHAT_FRAME:AddMessage(msg)
                end
              end
            },
            { text = "Send Waypoint Link", disabled = not hasWaypoint, fn = function()
                SetWaypoint(f.mapId, f.x, f.y)
                local link = (C_Map and C_Map.GetUserWaypointHyperlink) and C_Map.GetUserWaypointHyperlink()
                local out = link or ((f.name or "Farm") .. " - " .. (f.zone or "Unknown Zone"))
                if ChatEdit_InsertLink and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
                  ChatEdit_InsertLink(out)
                elseif DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
                  DEFAULT_CHAT_FRAME:AddMessage(out)
                end
              end
            },
            { sep = true },
            { text = "Rebuild Averages", fn = function()
                UI:CloseContextMenu()
                local id = f._id or f.id
                if not id then return end

                local db = (WF and WF.DB and WF.DB.Get) and WF.DB:Get() or nil
                local hasHistory = (db and db.history and type(db.history.sessions) == "table" and #db.history.sessions > 0) and true or false
                if not hasHistory then
                  UI:Confirm("No History runs", "You have no History sessions to rebuild from yet.", function() end, "OK")
                  return
                end

                UI:Confirm(
                  "Rebuild Avg data (this farm)",
                  "This rebuilds this farm's Avg values from its saved History runs only. It does not reprice them to current market values. Continue?",
                  function()
                    _ResetFarmPriceDataFromHistoryForFarm(id)
                    UI:RefreshFarms()
                  end,
                  "Rebuild"
                )
              end
            },
            { text = "Clear Stored Prices", fn = function()
                UI:CloseContextMenu()
                local id = f._id or f.id
                if not id then return end

                UI:Confirm(
                  "Remove price data (this farm)",
                  "This clears stored Avg values for this farm. History runs are not deleted.",
                  function()
                    local db = (WF and WF.DB and WF.DB.Get) and WF.DB:Get() or nil
                    if db then
                      _ClearFarmPriceDataForFarm(db, id)
                      _RebuildHistoryAggregatesForFarm(db, id)
                    end
                    UI:RefreshFarms()
                  end,
                  "Remove"
                )
              end
            },
            { sep = true },
            { text = (f.archived and "Restore" or "Archive"), fn = function()
                UI:CloseContextMenu()
                local db = WF.DB:Get() or {}
                db.farms = db.farms or {}
                local id = f._id or f.id
                local store = (id and db.farms[id]) or f
                if not store then return end

                if store.archived then
                  store.archived = nil
                  store.archivedAt = nil
                else
                  store.archived = true
                  store.archivedAt = SafeNow()
                end

                store.updatedAt = SafeNow()

                if R.selectedFarm == f then
                  SelectFarm(store, R.selectedRow)
                end

                UI:RefreshFarms()
                UI:RefreshOverview()
                UI:RefreshHistory()
              end
            },
            { text = "Delete", fn = function()
                local db = WF.DB:Get() or {}
                db.farms = db.farms or {}
                db.history = db.history or {}
                db.history.sessions = db.history.sessions or {}

                local id = f._id or f.id
                local linkedCount = 0

                if id then
                  for i = 1, #db.history.sessions do
                    local s = db.history.sessions[i]
                    if s then
                      local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
                      if sid == id then
                        linkedCount = linkedCount + 1
                      end
                    end
                  end
                end

                local function finalizeDelete()
                  db.history.lastRunByFarm = db.history.lastRunByFarm or {}
                  db.history.totalsByFarm = db.history.totalsByFarm or {}
                  db.history.totalsByFarmMode = db.history.totalsByFarmMode or {}
                  if id then
                    db.history.lastRunByFarm[id] = nil
                    db.history.totalsByFarm[id] = nil
                    if db.history.totalsByFarmMode[id] then db.history.totalsByFarmMode[id] = nil end
                  end

                  RebuildHistoryAggregates(db)

                  if R.selectedFarm == f then
                    ClearSelection()
                  end

                  UI:RefreshFarms()
                  UI:RefreshOverview()
                  UI:RefreshHistory()
                end

                local function doDeleteFarmOnly()
                  UI:CloseContextMenu()

                  if f and f.source == "seed" and f.seedKey and WF.DB and WF.DB.RememberDeletedSeedFarmByKey then
                    WF.DB:RememberDeletedSeedFarmByKey(f.seedKey)
                  end

                  if id then
                    db.farms[id] = nil
                  end

                  if id then
                    for i = 1, #db.history.sessions do
                      local s = db.history.sessions[i]
                      if s then
                        local sid = s.farmId or (s.farm and (s.farm._id or s.farm.id)) or nil
                        if sid == id then
                          if WF.DB and WF.DB.RemoveSessionFromFarmStats then
                            WF.DB:RemoveSessionFromFarmStats(s)
                          end
                          s.farmId = nil
                          s.farm = nil
                        end
                      end
                    end
                  end

                  finalizeDelete()
                end

                local function doDeleteFarmAndHistory()
                  UI:CloseContextMenu()

                  if f and f.source == "seed" and f.seedKey and WF.DB and WF.DB.RememberDeletedSeedFarmByKey then
                    WF.DB:RememberDeletedSeedFarmByKey(f.seedKey)
                  end

                  if id then
                    db.farms[id] = nil
                  end

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

                  finalizeDelete()
                end

                if linkedCount > 0 and UI.ConfirmTwoActions then
                  UI:ConfirmTwoActions(
                    "Delete farm",
                    "Choose what to remove for this farm.\n\nDelete farm only removes it from your Farm library and unlinks its saved History runs from Farm averages.\n\nDelete farm + history removes the farm and all linked History sessions. This cannot be undone.",
                    "Delete farm + history",
                    doDeleteFarmAndHistory,
                    "Delete farm only",
                    doDeleteFarmOnly,
                    nil,
                    nil,
                    "Delete farm + history",
                    "Remove this farm and all of its linked History sessions. This cannot be undone.",
                    "Delete farm only",
                    "Remove this farm from your Farm library, but keep its linked History sessions.",
                    "Cancel",
                    "Close this dialog without removing anything."
                  )
                  return
                end

                if linkedCount > 0 then
                  UI:Confirm(
                    "Delete farm + history?",
                    "This permanently deletes the farm AND all its History sessions. This cannot be undone.",
                    doDeleteFarmAndHistory,
                    "Delete farm + history"
                  )
                  return
                end

                UI:Confirm(
                  "Delete farm",
                  "Delete this farm from your Farm library?",
                  doDeleteFarmOnly,
                  "Delete farm"
                )
              end
            },


          }, 300)
        else
          UI:CloseContextMenu()
        end
      end)

      rows[i] = r
      UpdateColumns(r)
      return r
    end

    -- -------------------------------------------------
    -- Render list with sort/group
    -- -------------------------------------------------
    function UI:RefreshFarms()
      if not pFarms or not pFarms:IsShown() then return end

      local q = (F.search:GetText() or ""):lower()
      local db = WF.DB:Get() or {}
      local farms = (db.farms or {})

      local expFilter = WF.DB:GetSetting("farmFilterExpansion") or "Any"
      local typeFilter = WF.DB:GetSetting("farmFilterType") or "Any"
      local zoneOnly = WF.DB:GetSetting("farmFilterCurrentZoneOnly") and true or false
      local showArchived = WF.DB:GetSetting("farmShowArchived") and true or false
      local curZone = (GetZoneText and GetZoneText()) or ""
      curZone = tostring(curZone or ""):lower()

      local groupBy = WF.DB:GetSetting("farmGroup") or "None"
      local sortKey, sortDir = _GetFarmSort()

      local function TextMatch(farm)
        if q == "" then return true end
        local name = tostring(farm.name or ""):lower()
        local zone = tostring(farm.zone or ""):lower()
        local notes = tostring(farm.notes or ""):lower()
        if name:find(q, 1, true) or zone:find(q, 1, true) or notes:find(q, 1, true) then
          return true
        end
        if type(farm.tags) == "table" then
          for i = 1, #farm.tags do
            local t = tostring(farm.tags[i] or ""):lower()
            if t ~= "" and t:find(q, 1, true) then return true end
          end
        end
        return false
      end

      local function PassFilters(farm)
        local exp = farm.expansion or "Unknown"
        local typ = farm.farmType or "Any"
        if (not showArchived) and farm.archived then return false end
        if expFilter ~= "Any" and exp ~= expFilter then return false end
        if typeFilter ~= "Any" and typ ~= typeFilter then return false end
        if zoneOnly then
          local z = tostring(farm.zone or ""):lower()
          if z == "" or curZone == "" or z ~= curZone then return false end
        end
        return true
      end

      local list = {}
      local total = 0
      for farmId, farm in pairs(farms) do
        if type(farm) == "table" and farm.name and farm.name ~= "" then
          total = total + 1
          if PassFilters(farm) and TextMatch(farm) then
            farm._id = farmId
            farm._avgGph = FarmAvgGPH(db, farm)
            farm._avgLiv = FarmAvgLIV(db, farm)
            farm._avgMetric = FarmAvgMetric(db, farm)
            list[#list + 1] = farm
          end
        end
      end

      local function SafeLower(s) return tostring(s or ""):lower() end
      table.sort(list, function(a, b)
        local desc = (sortDir == "desc")

        if sortKey == "gph" then
          local ag = tonumber(a._avgMetric) or 0
          local bg = tonumber(b._avgMetric) or 0
          if ag ~= bg then
            if desc then return ag > bg end
            return ag < bg
          end
        else
          local av, bv
          if sortKey == "aim" or sortKey == "type" then
            av, bv = SafeLower(_FarmPrimaryDetail(a)), SafeLower(_FarmPrimaryDetail(b))
          elseif sortKey == "zone" then
            av, bv = SafeLower(_FarmZoneLabel(a)), SafeLower(_FarmZoneLabel(b))
          else -- name
            av, bv = SafeLower(a.name), SafeLower(b.name)
          end

          if av ~= bv then
            if desc then return av > bv end
            return av < bv
          end
        end

        -- tie-breakers (stable)
        local an, bn = SafeLower(a.name), SafeLower(b.name)
        if an ~= bn then return an < bn end
        return tostring(a._id or "") < tostring(b._id or "")
      end)

      local render

      local render = {}

      local function GroupKey(farm)
        if groupBy == "Expansion" then return farm.expansion or "Unknown" end
        if groupBy == "Type" then return farm.farmType or "Any" end
        if groupBy == "Aim" then return farm.aim or "Unknown" end
        return nil
      end

      local function GroupLabel(value)
        if groupBy == "Expansion" then return _DisplayExpansion(value or "Unknown") end
        if groupBy == "Type" then return _DisplayFarmType(value or "Any") end
        if groupBy == "Aim" then return _DisplayFarmAim(value or "Unknown") end
        return _HumanizeFarmLabel(value or "-")
      end

      if groupBy and groupBy ~= "None" then
        local buckets = {}
        local order = {}

        for i = 1, #list do
          local f = list[i]
          local key = GroupKey(f) or "Unknown"
          local b = buckets[key]
          if not b then
            b = {}
            buckets[key] = b
            order[#order + 1] = key
          end
          b[#b + 1] = f
        end

        table.sort(order, function(a, b) return SafeLower(a) < SafeLower(b) end)

        for i = 1, #order do
          local key = order[i]
          render[#render + 1] = { kind = "header", label = GroupLabel(key) }
          local b = buckets[key]
          for j = 1, #b do
            render[#render + 1] = { kind = "farm", farm = b[j] }
          end
        end
      else
        for i = 1, #list do
          render[#render + 1] = { kind = "farm", farm = list[i] }
        end
      end

      pFarms._render = render
      L.empty:SetShown(#render == 0)

      if L.listCount then
        local shown = #list
        if total and total > 0 then
          L.listCount:SetText(string.format("%d shown - %d total", shown, total))
        else
          L.listCount:SetText(string.format("%d shown", shown))
        end
      end

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

      local foundSelected = false
      local newSelectedRow = nil

      for i = 1, math.max(#render, #rows) do
        local row = rows[i]
        local item = render[i]

        if item then
          row = EnsureRow(i)
          row:Show()
          row:SetPoint("TOPLEFT", 0, -((i - 1) * rowH))
          row:SetPoint("TOPRIGHT", 0, -((i - 1) * rowH))

          if item.kind == "header" then
            row._isHeader = true
            row._farm = nil
            UpdateColumns(row)
            row.name:SetFontObject("WF_GameFontDisableSmall")
            row.name:SetTextColor(C4(MUTED, MUTED))
            row.name:SetText(item.label or "-")
            if row.aim then row.aim:SetText("") end
            if row.typ then row.typ:SetText("") end
            if row.exp then row.exp:SetText("") end
            if row.gph then row.gph:SetText("") end
            row.bg:SetAlpha(0)
            if row.sel then row.sel:SetAlpha(0) end
            row:EnableMouse(false)
            if row.sep then row.sep:Show() end
            if row.line then row.line:Hide() end
          else
            local farm = item.farm
            row._isHeader = false
            row._farm = farm
            if row.bg then row.bg:SetAlpha(0) end
            if row.sel then row.sel:SetAlpha(0) end
            UpdateColumns(row)
            row:EnableMouse(true)
            if row.sep then row.sep:Hide() end
            if row.line then row.line:Show() end
            row.name:SetFontObject("WF_GameFontHighlight")
            local displayName = farm.name or "Farm"
            if farm.archived then
              displayName = displayName .. " (Archived)"
              row.name:SetTextColor(C4(MUTED, MUTED))
            else
              row.name:SetTextColor(C4(TEXT, TEXT))
            end
            row.name:SetText(displayName)

            row.aim:SetText(_FarmPrimaryDetail(farm))
            row.typ:SetText("")
            row.exp:SetText(_FarmZoneLabel(farm))

            local v = tonumber(farm._avgMetric) or 0
            if v > 0 then
              row.gph:SetText(Money(v))
            else
              row.gph:SetText("-")
            end

            if R.selectedFarm and (R.selectedFarm == farm or (R.selectedFarm.id and farm.id and R.selectedFarm.id == farm.id)) then
              foundSelected = true
              newSelectedRow = row
            end
          end
        elseif row then
          row._farm = nil
          row._isHeader = false
          if row.zoneHot then row.zoneHot:Hide() end
          if row.detailHot then row.detailHot:Hide() end
          row:Hide()
        end
      end

      L.sc:SetHeight(#render * rowH)

      if R.selectedFarm and foundSelected then
        SelectFarm(R.selectedFarm, newSelectedRow)
      else
        ClearSelection()
      end
    end

local throttle

F.search:SetScript("OnTextChanged", function()
  UpdateSearchPH()

  if throttle and throttle.Cancel then
    throttle:Cancel()
  end

  throttle = C_Timer.NewTimer(0.05, function()
    UI:RefreshFarms()
  end)
end)

F.search:SetScript("OnEditFocusGained", function()
  UpdateSearchPH()
end)

F.search:SetScript("OnEditFocusLost", function()
  UpdateSearchPH()
end)

F.bResync:SetScript("OnClick", function()
  UI:ResyncFarmAveragesFromHistory()
end)

UpdateSearchPH()
if UI and UI.UpdateFarmResyncStatus then UI:UpdateFarmResyncStatus() end

-- -------------------------------------------------
-- Layout safety
--
-- The Farms page is mostly positioned by the main help window layout
-- pass (UI:_Layout). If that pass errors for any reason, the page
-- widgets can remain unanchored and the tab looks blank. Keep a small
-- fallback layout so the page always shows something usable.
-- -------------------------------------------------

    R.bWaypoint:SetScript("OnClick", function()
      if not R.selectedFarm then return end
      SetWaypoint(R.selectedFarm.mapId, R.selectedFarm.x, R.selectedFarm.y)
    end)

    R.bRandom:SetScript("OnClick", function()
      local farm = UI:PickRandomFarm()
      if not farm then return end
      SelectFarm(farm, nil)
      SetWaypoint(farm.mapId, farm.x, farm.y)
      UI:RefreshFarms()
    end)

	    pFarms:SetScript("OnShow", function()
      UI:_Layout()
      if UI and UI.UpdateFarmResyncStatus then UI:UpdateFarmResyncStatus() end
      UI:RefreshFarms()
    end)

    UI._layout.farms = {
      page = pFarms,
      search = F.search,
      topArea = F.topArea,
      filterBar = F.filterBar,
      fExp = F.fExp,
      fType = F.fType,
      ckZone = F.ckZone,
      ckArchived = F.ckArchived,
      fGroup = F.fGroup,
      fSort = F.fSort,
      bReset = F.bReset,
      bResync = F.bResync,
      tResync = F.tResync,
      listPanel = L.listPanel,
      listCount = L.listCount,
      right = right,
      rightScroll = R.rScroll,
      tagText = R.tagText,
      noteText = R.noteText,
    }
  end
end
