-- UI/HelpPages/SettingsProfiles.lua
-- Settings > Profiles (premium manager UI)
-- $10k pass: fewer buttons, clean layout, collapsed "More" actions, fixed list click width + FIXED initial render (profiles now show).

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

local Util = WF.Util


local Theme = WF.UI.Theme


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

  local function tint(tex, a)
    if not tex or not tex.SetVertexColor then return end
    if tex.SetDesaturated then tex:SetDesaturated(true) end
    tex:SetVertexColor(Theme.accent[1], Theme.accent[2], Theme.accent[3], a or 1)
  end

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

  apply()
  if btn.HookScript then
    btn:HookScript("OnShow", apply)
  end
end

local function WF_SetFavoriteStarVisual(tex, alpha)
  if not tex then return end
  if tex.SetDesaturated then tex:SetDesaturated(true) end

  local accent = Theme and Theme.accent
  local r, g, b = 1, 1, 1
  if type(accent) == "table" then
    r = accent[1] or 1
    g = accent[2] or 1
    b = accent[3] or 1
  end

  tex:SetVertexColor(r, g, b, alpha or 1)
  tex:SetAlpha(alpha or 1)
end

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

-- ---------------------------------------------------------
-- Small utils (fast + safe)
-- ---------------------------------------------------------
local tostring, tonumber, type = tostring, tonumber, type
local floor, format = math.floor, string.format

-- quality labels (for profile preview display)
local QUALITY_LABEL = {
  [0] = "Poor+",
  [1] = "Common+",
  [2] = "Uncommon+",
  [3] = "Rare+",
  [4] = "Epic+",
  [5] = "Legendary+",
}


local CATEGORY_META = {
  { key = "lootRecordMaterials", label = "Materials" },
  { key = "lootRecordGear", label = "Gear" },
  { key = "lootRecordRecipes", label = "Recipes" },
  { key = "lootRecordPets", label = "Pets" },
  { key = "lootRecordToys", label = "Toys" },
  { key = "lootRecordMounts", label = "Mounts" },
  { key = "lootRecordConsumables", label = "Consumables" },
  { key = "lootRecordMisc", label = "Misc / Other" },
}

local CATEGORY_TOOLTIPS = {
  lootRecordMaterials = { "Materials", "Crafting reagents and farm mats like cloth, ore, herbs, leather, dust, essences, and similar drops." },
  lootRecordGear = { "Gear", "Armor, weapons, BOEs, transmogs, and other equippable item drops." },
  lootRecordRecipes = { "Recipes", "Recipes, patterns, plans, schematics, formulas, and other teachable drops." },
  lootRecordPets = { "Pets", "Battle pets and cageable pet drops." },
  lootRecordToys = { "Toys", "Toy items and collectible utility unlocks." },
  lootRecordMounts = { "Mounts", "Mount items and collectible riding or flying unlocks." },
  lootRecordConsumables = { "Consumables", "Potions, flasks, food, scrolls, enchants, and other single-use items." },
  lootRecordMisc = { "Misc / Other", "Anything that does not cleanly fit the other tracked categories." },
  lootIgnoreSoulbound = { "Soulbound or BoP items", "Skip items that bind on pick up or are soulbound." },
}

local function ResolveTooltipText(v, self)
  if type(v) == "function" then
    return v(self)
  end
  return v
end

local function _WF_GetTooltipAccentColor()
  local t = Theme and Theme.text
  if type(t) == "table" then
    return t[1] or 0.95, t[2] or 0.95, t[3] or 0.95
  end
  return 0.95, 0.95, 0.95
end

local function _WF_AddTooltipBody(body)
  if not GameTooltip or body == nil then return end

  local lines = {}
  if type(body) == "table" then
    for i = 1, #body do
      local line = body[i]
      if line ~= nil and tostring(line) ~= "" then
        lines[#lines + 1] = tostring(line)
      end
    end
  else
    local s = tostring(body)
    for line in s:gmatch("[^\n]+") do
      if line ~= "" then
        lines[#lines + 1] = line
      end
    end
  end

  for i = 1, #lines do
    local line = lines[i]
    if i == 1 then
      GameTooltip:AddLine(line, 0.90, 0.90, 0.90, true)
    else
      GameTooltip:AddLine(line, 0.75, 0.75, 0.75, true)
    end
  end
end

local function _WF_ShowStyledTooltip(owner, title, body, anchor)
  if not owner or not GameTooltip then return end
  if (not title or title == "") and (not body or body == "") then return end

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

  if title and title ~= "" then
    local ar, ag, ab = _WF_GetTooltipAccentColor()
    GameTooltip:AddLine(title, ar, ag, ab)
  end

  _WF_AddTooltipBody(body)
  GameTooltip:Show()
end

local function BindTooltip(widget, title, body, anchor)
  if not widget then return end
  widget._wfTooltipTitle = title
  widget._wfTooltipBody = body
  widget._wfTooltipAnchor = anchor or "ANCHOR_RIGHT"

  if widget._wfTooltipBound then
    return
  end
  widget._wfTooltipBound = true

  local function Show(self)
    local tipTitle = ResolveTooltipText(self._wfTooltipTitle, self)
    local tipBody = ResolveTooltipText(self._wfTooltipBody, self)
    if (not tipTitle or tipTitle == "") and (not tipBody or tipBody == "") then
      return
    end

    _WF_ShowStyledTooltip(self, tipTitle, tipBody, self._wfTooltipAnchor or "ANCHOR_RIGHT")
  end

  local function Hide(self)
    if GameTooltip and GameTooltip:IsOwned(self) then
      GameTooltip:Hide()
    end
  end

  if widget.HookScript then
    widget:HookScript("OnEnter", Show)
    widget:HookScript("OnLeave", Hide)
  else
    widget:SetScript("OnEnter", Show)
    widget:SetScript("OnLeave", Hide)
  end
end

local function ApplyCategoryDefaults(p)
  if type(p) ~= "table" then return end
  for i = 1, #CATEGORY_META do
    local key = CATEGORY_META[i].key
    p[key] = p[key] and true or false
  end
end

local function HasCategorySelection(p)
  if type(p) ~= "table" then return false end
  for i = 1, #CATEGORY_META do
    if p[CATEGORY_META[i].key] then
      return true
    end
  end
  return false
end

local function HydrateCategorySelections(p)
  if type(p) ~= "table" then return end
  ApplyCategoryDefaults(p)
  if HasCategorySelection(p) then return end

  local includeMaterials = p.lootIncludeMaterials
  if includeMaterials == nil then includeMaterials = true end
  includeMaterials = includeMaterials and true or false

  local onlyMaterials = p.lootOnlyMaterials and true or false
  local onlyGear = p.lootOnlyGear and true or false

  if onlyMaterials then
    p.lootRecordMaterials = true
    return
  end

  if onlyGear then
    p.lootRecordGear = true
    return
  end

  if includeMaterials == false then
    p.lootRecordGear = true
    p.lootRecordRecipes = true
    p.lootRecordPets = true
    p.lootRecordToys = true
    p.lootRecordMounts = true
    p.lootRecordConsumables = true
    p.lootRecordMisc = true
  end
end

local function GetSelectedCategoryLabels(p)
  local out = {}
  if type(p) ~= "table" then return out end
  for i = 1, #CATEGORY_META do
    local meta = CATEGORY_META[i]
    if p[meta.key] then
      out[#out + 1] = meta.label
    end
  end
  return out
end

local function CategorySummary(p, compact)
  HydrateCategorySelections(p)
  local labels = GetSelectedCategoryLabels(p)
  if #labels == 0 then
    return "Everything"
  end

  local total = #CATEGORY_META
  if #labels == (total - 1) and not p.lootRecordMaterials then
    return "Everything except Materials"
  end

  if compact and #labels > 3 then
    return tostring(#labels) .. " selected"
  end

  return table.concat(labels, ", ")
end

local function SyncLegacyCategoryFlags(p)
  if type(p) ~= "table" then return end
  HydrateCategorySelections(p)

  local labels = GetSelectedCategoryLabels(p)
  local hasMaterials = p.lootRecordMaterials and true or false
  local hasGear = p.lootRecordGear and true or false

  if #labels == 1 and hasMaterials then
    p.lootIncludeMaterials = true
    p.lootOnlyMaterials = true
    p.lootOnlyGear = false
    return
  end

  if #labels == 1 and hasGear then
    p.lootIncludeMaterials = false
    p.lootOnlyMaterials = false
    p.lootOnlyGear = true
    return
  end

  p.lootOnlyMaterials = false
  p.lootOnlyGear = false
  p.lootIncludeMaterials = hasMaterials or (#labels == 0)
end

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

local function Lower(s)
  return tostring(s or ""):lower()
end

local function SafeCall(fn, ...)
  if type(fn) ~= "function" then return nil end
  return fn(...)
end

local function YesNo(v)
  if v then return "Yes" end
  return "No"
end

local function SetShown(f, show)
  if not f then return end
  if show then f:Show() else f:Hide() end
end

local function ClampInt(v, lo, hi)
  v = tonumber(v)
  if not v then return lo end
  v = floor(v + 0.00001)
  if v < lo then return lo end
  if hi and v > hi then return hi end
  return v
end


local QUALITY_NAME_TO_VALUE = {
  poor = 0,
  common = 1,
  uncommon = 2,
  rare = 3,
  epic = 4,
  legendary = 5,
  gray = 0,
  grey = 0,
  white = 1,
  green = 2,
  blue = 3,
  purple = 4,
  orange = 5,
}

local function QualityValueToDisplay(v)
  local n = tonumber(v) or 0
  if n < 0 then n = 0 end
  if n > 5 then n = 5 end
  local label = QUALITY_LABEL[n] or QUALITY_LABEL[0] or "Poor+"
  return tostring(label):gsub("%+", "")
end

local function ParseQualityValue(v)
  if type(v) == "number" then
    return ClampInt(v, 0, 5)
  end

  local raw = Trim(v or "")
  if raw == "" then
    return 0
  end

  local n = tonumber(raw)
  if n then
    return ClampInt(n, 0, 5)
  end

  raw = Lower(raw):gsub("%+", "")
  return QUALITY_NAME_TO_VALUE[raw] or 0
end

local function MetricDisplayToken(raw)
  raw = Lower(Trim(raw or ""))
  if raw == "" then return "" end

  local MAP = {
    gph = "GPH",
    liv = "LIV",
  }

  if MAP[raw] then
    return MAP[raw]
  end

  if raw:match("^[%a%d_]+$") then
    return raw:upper()
  end

  if Util and Util.PrettyToken then
    return Util:PrettyToken(raw)
  end

  return raw
end

local function NormalizeMetricToken(raw)
  raw = Lower(Trim(raw or ""))
  if raw == "" then
    return ""
  end
  return raw
end

local function PriceSourceEditorDisplay(raw)
  raw = Trim(raw or "")
  local prov = raw:match("^([%a_]+)%:")
  if prov and prov ~= "" then
    prov = Lower(prov)
  else
    prov = Lower(raw)
  end
  if prov == "" then
    return ""
  end
  if prov == "tsm" then
    return "TSM"
  end
  if Util and Util.PrettyToken then
    return Util:PrettyToken(prov)
  end
  return prov
end

local function Toast(ctx, msg)
  if ctx and ctx.UI and ctx.UI.Toast then
    ctx.UI:Toast(msg)
    return
  end
  if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
    DEFAULT_CHAT_FRAME:AddMessage("|cff8fd3ffWhat Farm:|r " .. tostring(msg))
  end
end

local function RefreshMainAndSettings(ctx)
  if ctx and ctx.WF and ctx.WF.UI and ctx.WF.UI.Main then
    local M = ctx.WF.UI.Main
    SafeCall(M.RefreshProfileUI, M)
    SafeCall(M.RefreshHeaderPriceSource, M)
    SafeCall(M.RefreshTitle, M)
    SafeCall(M.RefreshMetrics, M)
    SafeCall(M.RefreshLoot, M, true)
  end
  if ctx and ctx.UI and ctx.UI.RefreshLootSettings then
    ctx.UI:RefreshLootSettings()
  end
end

-- ---------------------------------------------------------
-- Persistence helpers (WFDB settings when available)
-- ---------------------------------------------------------
local function GetPersist(ctx, WFDB, key, fallback)
  if WFDB and WFDB.GetSetting then
    local v = WFDB:GetSetting(key)
    if v ~= nil then return v end
  end

  local UI = ctx and ctx.UI
  if UI then
    UI._wfLocalSettings = UI._wfLocalSettings or {}
    local v = UI._wfLocalSettings[key]
    if v ~= nil then return v end
  end

  return fallback
end

local function SetPersist(ctx, WFDB, key, value)
  if WFDB and WFDB.SetSetting then
    WFDB:SetSetting(key, value)
    return
  end

  local UI = ctx and ctx.UI
  if UI then
    UI._wfLocalSettings = UI._wfLocalSettings or {}
    UI._wfLocalSettings[key] = value
  end
end

local function GetFavorites(ctx, WFDB)
  local t = GetPersist(ctx, WFDB, "profileFavorites", nil)
  if type(t) ~= "table" then t = {} end
  return t
end

local function IsFavorite(ctx, WFDB, name)
  local fav = GetFavorites(ctx, WFDB)
  return fav[name] and true or false
end

local function ToggleFavorite(ctx, WFDB, name)
  name = Trim(name)
  if name == "" then return end

  local fav = GetFavorites(ctx, WFDB)
  if fav[name] then
    fav[name] = nil
  else
    fav[name] = true
  end
  SetPersist(ctx, WFDB, "profileFavorites", fav)
end

local function GetProfileDisplayName(ctx, WFDB, name)
  name = tostring(name or "")
  if name == "" then
    return "Default"
  end
  return name
end

-- ---------------------------------------------------------
-- Percent encoding (share-safe strings)
-- ---------------------------------------------------------
local function UrlEncode(s)
  s = tostring(s or "")
  s = s:gsub("%%", "%%25")
  s = s:gsub("([^%w_%.%-])", function(c)
    return format("%%%02X", string.byte(c))
  end)
  return s
end

local function UrlDecode(s)
  s = tostring(s or "")
  s = s:gsub("%%(%x%x)", function(h)
    return string.char(tonumber(h, 16) or 32)
  end)
  return s
end

local function ParseMoneyToCopper(s)
  s = Trim(s)
  if s == "" then return 0 end

  local g, si, c = 0, 0, 0
  local hasUnit = s:find("[gGsScC]") and true or false

  if hasUnit then
    local gg = s:match("(%d+)%s*[gG]")
    local ss = s:match("(%d+)%s*[sS]")
    local cc = s:match("(%d+)%s*[cC]")
    if gg then g = tonumber(gg) or 0 end
    if ss then si = tonumber(ss) or 0 end
    if cc then c = tonumber(cc) or 0 end
    return (g * 10000) + (si * 100) + c
  end

  local n = tonumber(s)
  if not n then return 0 end
  if n < 0 then n = 0 end
  return floor(n * 10000 + 0.00001)
end


-- ---------------------------------------------------------
-- Price source display helpers (Profiles UI)
-- ---------------------------------------------------------
local function SplitProvider(p)
  p = Trim(p or "")
  if p == "" then return "", nil end
  local a, b = p:match("^([%a_]+)%:(.+)$")
  if a and b then
    a = a:lower()
    b = Trim(b)
    if b == "" then b = nil end
    return a, b
  end
  return p:lower(), nil
end

local function PrettyTSMPriceString(raw)
  raw = Trim(raw or ""):lower()
  if raw == "" then raw = "dbregionmarketavg" end

  local MAP = {
    dbmarket = "DBMarket",
    dbhistorical = "DBHistorical",
    dbrecent = "DBRecent",
    dbminbuyout = "DBMinBuyout",
    dbregionmarketavg = "DBRegionMarketAvg",
    dbregionhistorical = "DBRegionHistorical",
    dbregionsaleavg = "DBRegionSaleAvg",
    dbregionsalerate = "DBRegionSaleRate",
    dbregionavgdailysold = "DBRegionAvgDailySold",
  }

  local priceMeta = MAP[raw]
  if not priceMeta then
    if raw:match("^[%w_]+$") then
      if raw:sub(1, 2) == "db" and raw:len() > 2 then
        priceMeta = "DB" .. raw:sub(3, 3):upper() .. raw:sub(4)
      else
        if Util and Util.PrettyToken then
          priceMeta = Util:PrettyToken(raw)
        else
          priceMeta = raw
        end
      end
    else
      priceMeta = "TSM"
    end
  end

  return priceMeta
end

local function PriceSourceDisplayForProfile(pN, fallbackTSM)
  if type(pN) ~= "table" then return "—" end

  local prov, extra = SplitProvider(pN.priceSource or "")
  if prov == "" then return "—" end

  if prov == "tsm" then
    local raw = extra
    if not raw or raw == "" then
      raw = Trim(pN.tsmPriceString or "")
    end
    if (not raw or raw == "") and fallbackTSM then
      raw = tostring(fallbackTSM)
    end
    local prettyTSM = PrettyTSMPriceString(raw or "")
    return "TSM (" .. prettyTSM .. ")"
  end

  if Util and Util.PrettyToken then
    return Util:PrettyToken(prov)
  end
  return prov
end

local B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
local B64_REVERSE = {}
do
  for i = 1, #B64_ALPHABET do
    B64_REVERSE[B64_ALPHABET:sub(i, i)] = i - 1
  end
end

local SHARE_FLAG_ORDER = {
  "lootIncludeMaterials",
  "lootOnlyMaterials",
  "lootOnlyGear",
  "lootRecordMaterials",
  "lootRecordGear",
  "lootRecordRecipes",
  "lootRecordPets",
  "lootRecordToys",
  "lootRecordMounts",
  "lootRecordConsumables",
  "lootRecordMisc",
  "lootIgnoreSoulbound",
}

local function PackShareFlags(prof)
  local value = 0
  local bitValue = 1

  for i = 1, #SHARE_FLAG_ORDER do
    local key = SHARE_FLAG_ORDER[i]
    if prof and prof[key] then
      value = value + bitValue
    end
    bitValue = bitValue * 2
  end

  return value
end

local function UnpackShareFlags(value, prof)
  value = tonumber(value) or 0
  prof = prof or {}

  local bitValue = 1
  for i = 1, #SHARE_FLAG_ORDER do
    local key = SHARE_FLAG_ORDER[i]
    prof[key] = (floor(value / bitValue) % 2) == 1
    bitValue = bitValue * 2
  end
end

local function Base64Encode(data)
  data = tostring(data or "")
  if data == "" then return "" end

  local out = {}
  local len = #data
  local i = 1

  while i <= len do
    local a = string.byte(data, i) or 0
    local b = string.byte(data, i + 1)
    local c = string.byte(data, i + 2)

    local n = (a * 65536) + ((b or 0) * 256) + (c or 0)
    local c1 = floor(n / 262144) % 64
    local c2 = floor(n / 4096) % 64
    local c3 = floor(n / 64) % 64
    local c4 = n % 64

    out[#out + 1] = B64_ALPHABET:sub(c1 + 1, c1 + 1)
    out[#out + 1] = B64_ALPHABET:sub(c2 + 1, c2 + 1)
    out[#out + 1] = b and B64_ALPHABET:sub(c3 + 1, c3 + 1) or "="
    out[#out + 1] = c and B64_ALPHABET:sub(c4 + 1, c4 + 1) or "="

    i = i + 3
  end

  return table.concat(out)
end

local function Base64Decode(data)
  data = Trim(data or "")
  if data == "" then return "" end
  if (#data % 4) ~= 0 then return nil end

  local out = {}
  local i = 1
  local len = #data

  while i <= len do
    local c1 = data:sub(i, i)
    local c2 = data:sub(i + 1, i + 1)
    local c3 = data:sub(i + 2, i + 2)
    local c4 = data:sub(i + 3, i + 3)

    local v1 = B64_REVERSE[c1]
    local v2 = B64_REVERSE[c2]
    local pad3 = c3 == "="
    local pad4 = c4 == "="
    local v3 = pad3 and 0 or B64_REVERSE[c3]
    local v4 = pad4 and 0 or B64_REVERSE[c4]

    if v1 == nil or v2 == nil or ((not pad3) and v3 == nil) or ((not pad4) and v4 == nil) then
      return nil
    end
    if pad3 and not pad4 then
      return nil
    end

    local n = (v1 * 262144) + (v2 * 4096) + (v3 * 64) + v4
    local a = floor(n / 65536) % 256
    local b = floor(n / 256) % 256
    local c = n % 256

    out[#out + 1] = string.char(a)
    if not pad3 then
      out[#out + 1] = string.char(b)
    end
    if not pad4 then
      out[#out + 1] = string.char(c)
    end

    i = i + 4
  end

  return table.concat(out)
end

local function ShareChecksum(data)
  data = tostring(data or "")

  local a, b = 1, 0
  for i = 1, #data do
    a = (a + (string.byte(data, i) or 0)) % 65521
    b = (b + a) % 65521
  end

  return format("%04X%04X", b, a)
end

local function SplitSharePayload(raw)
  raw = tostring(raw or "")

  local out = {}
  local from = 1
  while true do
    local at = string.find(raw, "~", from, true)
    if not at then
      out[#out + 1] = string.sub(raw, from)
      break
    end

    out[#out + 1] = string.sub(raw, from, at - 1)
    from = at + 1
  end

  return out
end

local function SerializeProfile(name, prof)
  if type(prof) ~= "table" then return "" end

  local payload = {
    UrlEncode(name or ""),
    tostring(tonumber(prof.lootMinGold) or 0),
    tostring(tonumber(prof.lootMinQty) or 1),
    tostring(tonumber(prof.lootMinQuality) or 0),
    format("%03X", PackShareFlags(prof)),
    UrlEncode(prof.priceSource or ""),
    UrlEncode(prof.tsmPriceString or ""),
    UrlEncode(prof.overviewLast5Metric or ""),
  }

  local encoded = Base64Encode(table.concat(payload, "~"))
  return "WFPROF2:" .. encoded .. ":" .. ShareChecksum(encoded)
end

local function DeserializeProfileV1(s)
  local out = {}
  for part in string.gmatch(s, "([^|]+)") do
    if part ~= "WFPROF1" then
      local k, v = part:match("^([^=]+)=(.*)$")
      if k and v then
        out[k] = v
      end
    end
  end

  local function ToBool(v)
    if v == "1" or v == "true" or v == "TRUE" then return true end
    return false
  end

  local name = UrlDecode(out.name or "")
  if name == "" then name = "Imported" end

  local prof = {
    lootMinGold = tonumber(out.lootMinGold) or 0,
    lootMinQty = tonumber(out.lootMinQty) or 1,
    lootMinQuality = tonumber(out.lootMinQuality) or 0,
    lootIncludeMaterials = ToBool(out.lootIncludeMaterials),
    lootOnlyMaterials = ToBool(out.lootOnlyMaterials),
    lootOnlyGear = ToBool(out.lootOnlyGear),
    lootRecordMaterials = ToBool(out.lootRecordMaterials),
    lootRecordGear = ToBool(out.lootRecordGear),
    lootRecordRecipes = ToBool(out.lootRecordRecipes),
    lootRecordPets = ToBool(out.lootRecordPets),
    lootRecordToys = ToBool(out.lootRecordToys),
    lootRecordMounts = ToBool(out.lootRecordMounts),
    lootRecordConsumables = ToBool(out.lootRecordConsumables),
    lootRecordMisc = ToBool(out.lootRecordMisc),
    lootIgnoreSoulbound = ToBool(out.lootIgnoreSoulbound),
    priceSource = UrlDecode(out.priceSource or ""),
    tsmPriceString = UrlDecode(out.tsmPriceString or ""),
    overviewLast5Metric = UrlDecode(out.overviewLast5Metric or ""),
  }

  HydrateCategorySelections(prof)
  SyncLegacyCategoryFlags(prof)

  return { name = name, profile = prof }, nil
end

local function ExtractShareV2Parts(s)
  s = tostring(s or "")
  local body = s:match("^WFPROF2(.*)$")
  if body == nil then
    return nil, nil
  end

  body = body:gsub("%s+", "")
  if body == "" then
    return nil, nil
  end

  local encoded, checksum = body:match("^[:|;,.]([A-Za-z0-9%-%_=]+)[:|;,.]([0-9A-Fa-f]+)$")
  if encoded and checksum then
    return encoded, checksum
  end

  encoded, checksum = body:match("^([A-Za-z0-9%-%_=]+)[:|;,.]([0-9A-Fa-f]+)$")
  if encoded and checksum then
    return encoded, checksum
  end

  local compact = body:gsub("^[:|;,.]+", "", 1)
  compact = compact:gsub("[:|;,.]+", "")
  if #compact > 8 then
    return compact:sub(1, -9), compact:sub(-8)
  end

  return nil, nil
end

local function DeserializeProfileV2(s)
  local encoded, checksum = ExtractShareV2Parts(s)
  if not encoded or not checksum or #checksum ~= 8 or not checksum:match("^[0-9A-Fa-f]+$") then
    return nil, "Malformed WhatFarm profile string"
  end

  checksum = string.upper(checksum)
  if ShareChecksum(encoded) ~= checksum then
    return nil, "Profile string checksum mismatch"
  end

  local raw = Base64Decode(encoded)
  if not raw then
    return nil, "Profile string decode failed"
  end

  local fields = SplitSharePayload(raw)
  if #fields < 8 then
    return nil, "Profile payload incomplete"
  end

  local name = UrlDecode(fields[1] or "")
  if name == "" then name = "Imported" end

  local prof = {
    lootMinGold = tonumber(fields[2]) or 0,
    lootMinQty = tonumber(fields[3]) or 1,
    lootMinQuality = tonumber(fields[4]) or 0,
    priceSource = UrlDecode(fields[6] or ""),
    tsmPriceString = UrlDecode(fields[7] or ""),
    overviewLast5Metric = UrlDecode(fields[8] or ""),
  }

  UnpackShareFlags(tonumber(fields[5] or "0", 16) or 0, prof)
  HydrateCategorySelections(prof)
  SyncLegacyCategoryFlags(prof)

  return { name = name, profile = prof }, nil
end

local function NormalizeImportedProfileString(s)
  s = tostring(s or "")
  if s == "" then return "" end

  s = s:gsub("\r", "")
  s = s:gsub("%s+", "")
  s = Trim(s)
  if s == "" then return "" end

  s = s:gsub("^WFPROF1ame=", "WFPROF1|name=")

  return s
end

local function DeserializeProfile(s)
  s = NormalizeImportedProfileString(s)
  if s == "" then return nil, "Empty" end

  if s:find("^WFPROF2") then
    return DeserializeProfileV2(s)
  end

  if s:find("^WFPROF1") then
    return DeserializeProfileV1(s)
  end

  return nil, "Not a WhatFarm profile string"
end

-- ---------------------------------------------------------
-- Text prompt modal
-- ---------------------------------------------------------
local function EnsureTextPrompt(ctx)
  local UI = ctx and ctx.UI
  if not UI then return end
  if UI._wfSettingsProfilesPrompt then return end

  local C4 = ctx.C4
  local TEXT, MUTED, PANEL, BORDER = ctx.TEXT, ctx.MUTED, ctx.PANEL, ctx.BORDER
  local Widgets = ctx.Widgets
  local MakeInput = ctx.MakeInput

  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, blocker, "BackdropTemplate")
  box:SetSize(440, 188)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 40)
  box:SetFrameStrata("FULLSCREEN_DIALOG")
  box:SetFrameLevel(blocker:GetFrameLevel() + 60)
  box:EnableMouse(true)
  box:SetScript("OnMouseDown", function() end)
  box:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  box:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  box:SetBackdropBorderColor(C4(BORDER, BORDER))

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

  local body = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  body:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
  body:SetPoint("TOPRIGHT", box, "TOPRIGHT", -14, -40)
  body:SetTextColor(C4(MUTED, MUTED))
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")
  body:SetText("Enter a name:")

  local input = MakeInput(box, 412, 24)
  input:SetPoint("TOPLEFT", 14, -80)

  local bCancel = Widgets:Button(box, "Cancel", 92, 24)
  bCancel:SetPoint("BOTTOMRIGHT", -14, 12)

  local bOK = Widgets:Button(box, "OK", 92, 24)
  bOK:SetPoint("RIGHT", bCancel, "LEFT", -10, 0)

  local function Close()
    blocker:Hide()
    box:Hide()
    input:ClearFocus()
    input:SetText("")
    box._onOK = nil
  end

  blocker:SetScript("OnMouseDown", Close)

  local function Commit()
    local cb = box._onOK
    if type(cb) ~= "function" then Close() return end
    local txt = Trim(input:GetText())
    Close()
    cb(txt)
  end

  bCancel:SetScript("OnClick", Close)
  bOK:SetScript("OnClick", Commit)
  input:SetScript("OnEnterPressed", Commit)
  input:SetScript("OnEscapePressed", Close)

  UI._wfSettingsProfilesPrompt = {
    blocker = blocker,
    box = box,
    title = title,
    body = body,
    input = input,
    ok = bOK,
    cancel = bCancel,
    close = Close,
  }
end

local function PromptText(ctx, title, body, defaultText, okText, onOK)
  local UI = ctx and ctx.UI
  local Widgets = ctx and ctx.Widgets
  if UI and Widgets and Widgets.OpenTextPrompt then
    Widgets:OpenTextPrompt(UI, "_wfSettingsProfilesPrompt", {
      title = title or "Profile Name",
      body = body or "Enter a name:",
      defaultText = defaultText or "",
      okText = okText or "OK",
      maxLetters = 64,
      onOK = onOK,
    })
    return
  end

  EnsureTextPrompt(ctx)
  if not UI or not UI._wfSettingsProfilesPrompt then return end

  local P = UI._wfSettingsProfilesPrompt
  P.title:SetText(title or "Profile Name")
  P.body:SetText(body or "Enter a name:")
  P.ok:SetText(okText or "OK")
  P.box._onOK = onOK

  P.input:SetText(defaultText or "")
  P.blocker:Show()
  P.box:Show()
  P.input:SetFocus()
  P.input:HighlightText()
end

-- ---------------------------------------------------------
-- Share modal (combined export + import)
-- ---------------------------------------------------------
local function EnsureShareModal(ctx)
  local UI = ctx and ctx.UI
  if not UI then return end
  if UI._wfSettingsProfilesShare then return end

  local C4 = ctx.C4
  local TEXT, MUTED, PANEL, BORDER = ctx.TEXT, ctx.MUTED, ctx.PANEL, ctx.BORDER
  local Widgets = ctx.Widgets

  local function CreateTextArea(parent, editable)
    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
    })
    panel:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
    panel:SetBackdropBorderColor(C4(BORDER, BORDER))

    local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
    if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
    scroll:SetPoint("TOPLEFT", 8, -8)
    scroll:SetPoint("BOTTOMRIGHT", -28, 8)
    scroll:EnableMouse(true)

    local eb = CreateFrame("EditBox", nil, scroll)
    eb:SetMultiLine(true)
    eb:SetAutoFocus(false)
    eb:SetFontObject(WF_ChatFontNormal)
    eb:SetTextInsets(6, 6, 6, 6)
    eb:SetWidth(1)
    eb:SetHeight(1)
    eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    eb:SetScript("OnTabPressed", function(self) self:ClearFocus() end)
    scroll:SetScrollChild(eb)

    panel.edit = eb
    panel.scroll = scroll
    panel._editable = editable and true or false
    panel._syncing = false

    local function UpdateScrollBarVisibility(resetWhenHidden)
      local range = scroll and scroll.GetVerticalScrollRange and tonumber(scroll:GetVerticalScrollRange()) or 0
      local holderH = panel and panel.GetHeight and tonumber(panel:GetHeight()) or 0
      local editH = eb and eb.GetHeight and tonumber(eb:GetHeight()) or 0
      local showBar = (range > 0.5) and (editH > (holderH + 2))
      if scroll and scroll.ScrollBar then
        if showBar then
          scroll.ScrollBar:Show()
          if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Show then
            scroll.ScrollBar._wfBG:Show()
          end
        else
          if resetWhenHidden and scroll.SetVerticalScroll then
            pcall(scroll.SetVerticalScroll, scroll, 0)
          end
          if resetWhenHidden and scroll.ScrollBar.SetValue then
            pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
          end
          scroll.ScrollBar:Hide()
          if scroll.ScrollBar._wfBG and scroll.ScrollBar._wfBG.Hide then
            scroll.ScrollBar._wfBG:Hide()
          end
        end
      end
    end

    local function UpdateHeight()
      local width = (scroll:GetWidth() or 0) - 8
      if width < 1 then width = 1 end
      eb:SetWidth(width)

      local textValue = eb:GetText() or ""
      local lineCount = 1
      for _ in textValue:gmatch("\n") do
        lineCount = lineCount + 1
      end

      local lineHeight = select(2, eb:GetFont()) or 14
      if type(lineHeight) ~= "number" or lineHeight < 12 then
        lineHeight = 14
      end

      local minHeight = (scroll:GetHeight() or 0) - 2
      if minHeight < lineHeight then minHeight = lineHeight end
      local wanted = (lineCount * lineHeight) + 18
      if wanted < minHeight then wanted = minHeight end
      eb:SetHeight(wanted)
      if scroll and scroll.UpdateScrollChildRect then
        scroll:UpdateScrollChildRect()
      end
      UpdateScrollBarVisibility(false)
    end

    local function ResetScroll(selectAll)
      scroll:SetVerticalScroll(0)
      if scroll.ScrollBar and scroll.ScrollBar.SetValue then
        pcall(scroll.ScrollBar.SetValue, scroll.ScrollBar, 0)
      end
      UpdateScrollBarVisibility(true)
      if selectAll then
        eb:SetFocus()
        eb:HighlightText()
      end
    end

    panel.UpdateHeight = UpdateHeight
    panel.ResetScroll = ResetScroll

    scroll:SetScript("OnSizeChanged", function()
      UpdateHeight()
      UpdateScrollBarVisibility(false)
      if C_Timer and C_Timer.After then
        C_Timer.After(0, function()
          UpdateScrollBarVisibility(false)
        end)
      end
    end)
    eb:SetScript("OnTextChanged", function(self, userInput)
      if panel._syncing then
        UpdateHeight()
        return
      end
      if userInput and not panel._editable then
        panel._syncing = true
        self:SetText(panel._value or "")
        self:SetCursorPosition(0)
        panel._syncing = false
      end
      UpdateHeight()
    end)

    eb:SetScript("OnMouseUp", function(self)
      self:SetFocus()
      if not panel._editable then
        panel.ResetScroll(true)
      end
    end)

    function panel:SetValue(value)
      self._value = tostring(value or "")
      self._syncing = true
      eb:SetText(self._value)
      eb:SetCursorPosition(0)
      self._syncing = false
      UpdateHeight()
      scroll:SetVerticalScroll(0)
    end

    function panel:GetValue()
      return eb:GetText() or ""
    end

    if scroll.HookScript then
      scroll:HookScript("OnShow", function()
        UpdateHeight()
        UpdateScrollBarVisibility(false)
        if C_Timer and C_Timer.After then
          C_Timer.After(0, function()
            UpdateScrollBarVisibility(false)
          end)
        end
      end)
    end
    if scroll.ScrollBar and scroll.ScrollBar.HookScript then
      scroll.ScrollBar:HookScript("OnMinMaxChanged", function()
        UpdateScrollBarVisibility(false)
      end)
    end

    return panel
  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:SetScript("OnMouseDown", function() end)
  blocker:Hide()

  local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
  box:SetSize(760, 520)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 24)
  box:SetFrameLevel(blocker:GetFrameLevel() + 20)
  box:EnableMouse(true)
  box:SetScript("OnMouseDown", function() end)
  box:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  box:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  box:SetBackdropBorderColor(C4(BORDER, BORDER))

  local title = box:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  title:SetPoint("TOPLEFT", 14, -12)
  title:SetTextColor(C4(TEXT, TEXT))
  title:SetText("Share / Import Profile")

  local body = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  body:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
  body:SetPoint("TOPRIGHT", box, "TOPRIGHT", -14, -40)
  body:SetTextColor(C4(MUTED, MUTED))
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")
  body:SetText("Quick export on top, import on bottom. This mirrors the History share flow so both windows feel like the same system.")

  local exportPanel = CreateFrame("Frame", nil, box, "BackdropTemplate")
  exportPanel:SetPoint("TOPLEFT", 14, -64)
  exportPanel:SetPoint("TOPRIGHT", -14, -64)
  exportPanel:SetHeight(180)
  exportPanel:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
  })
  exportPanel:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  exportPanel:SetBackdropBorderColor(C4(BORDER, BORDER))

  local exportTitle = exportPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
  exportTitle:SetPoint("TOPLEFT", 12, -10)
  exportTitle:SetTextColor(C4(TEXT, TEXT))
  exportTitle:SetText("EXPORT")

  local exportBody = exportPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  exportBody:SetPoint("TOPLEFT", exportTitle, "BOTTOMLEFT", 0, -4)
  exportBody:SetPoint("TOPRIGHT", exportPanel, "TOPRIGHT", -12, -30)
  exportBody:SetTextColor(C4(MUTED, MUTED))
  exportBody:SetJustifyH("LEFT")
  exportBody:SetText("Select all and copy this string to share your currently selected profile.")

  local exportArea = CreateTextArea(exportPanel, false)
  exportArea:SetPoint("TOPLEFT", 12, -52)
  exportArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local bExportSelect = Widgets:Button(exportPanel, "Select All", 92, 22)
  bExportSelect:SetPoint("BOTTOMRIGHT", -12, 10)

  local importPanel = CreateFrame("Frame", nil, box, "BackdropTemplate")
  importPanel:SetPoint("TOPLEFT", exportPanel, "BOTTOMLEFT", 0, -10)
  importPanel:SetPoint("TOPRIGHT", exportPanel, "BOTTOMRIGHT", 0, -10)
  importPanel:SetPoint("BOTTOMLEFT", box, "BOTTOMLEFT", 14, 52)
  importPanel:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -14, 52)
  importPanel:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
  })
  importPanel:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  importPanel:SetBackdropBorderColor(C4(BORDER, BORDER))

  local importTitle = importPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
  importTitle:SetPoint("TOPLEFT", 12, -10)
  importTitle:SetTextColor(C4(TEXT, TEXT))
  importTitle:SetText("IMPORT")

  local importBody = importPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  importBody:SetPoint("TOPLEFT", importTitle, "BOTTOMLEFT", 0, -4)
  importBody:SetPoint("TOPRIGHT", importPanel, "TOPRIGHT", -12, -30)
  importBody:SetTextColor(C4(MUTED, MUTED))
  importBody:SetJustifyH("LEFT")
  importBody:SetText("Paste a shared profile string here. You'll get a preview before anything is imported.")

  local importArea = CreateTextArea(importPanel, true)
  importArea:SetPoint("TOPLEFT", 12, -52)
  importArea:SetPoint("BOTTOMRIGHT", -12, 40)

  local bImportPreview = Widgets:Button(importPanel, "Preview", 88, 22)
  bImportPreview:SetPoint("BOTTOMRIGHT", -12, 10)

  local bImportClear = Widgets:Button(importPanel, "Clear", 76, 22)
  bImportClear:SetPoint("RIGHT", bImportPreview, "LEFT", -8, 0)

  local bClose = Widgets:Button(box, "Close", 92, 24)
  bClose:SetPoint("BOTTOMRIGHT", -14, 14)

  local function Close()
    blocker:Hide()
    box:Hide()
    exportArea.edit:ClearFocus()
    importArea.edit:ClearFocus()
    box._onImport = nil
  end

  bClose:SetScript("OnClick", Close)

  bExportSelect:SetScript("OnClick", function()
    exportArea.edit:SetFocus()
    exportArea.ResetScroll(true)
    if C_Timer and C_Timer.After then
      C_Timer.After(0, function()
        exportArea.edit:SetFocus()
        exportArea.edit:HighlightText()
      end)
    end
  end)

  bImportClear:SetScript("OnClick", function()
    importArea:SetValue("")
    importArea.edit:SetFocus()
  end)

  bImportPreview:SetScript("OnClick", function()
    local cb = box._onImport
    if type(cb) ~= "function" then return end
    cb(importArea:GetValue() or "")
  end)

  UI._wfSettingsProfilesShare = {
    blocker = blocker,
    box = box,
    title = title,
    body = body,
    exportBody = exportBody,
    importBody = importBody,
    exportArea = exportArea,
    importArea = importArea,
    close = Close,
    open = function(profileName, exportText, onImport, initialImport)
      local display = tostring(profileName or "Profile")
      title:SetText("Share / Import Profile")
      body:SetText("Export \"" .. display .. "\" or paste a shared profile string below.")
      exportBody:SetText("Select all and copy this string to share \"" .. display .. "\".")
      exportArea:SetValue(exportText or "")
      importArea:SetValue(initialImport or "")
      box._onImport = onImport
      blocker:Show()
      box:Show()
      exportArea.ResetScroll(true)
      importArea.ResetScroll(false)
      importArea.edit:ClearFocus()
    end,
  }
end

local function ShowShare(ctx, profileName, exportText, onImport, initialImport)
  EnsureShareModal(ctx)
  local UI = ctx and ctx.UI
  if not UI or not UI._wfSettingsProfilesShare then return end
  UI._wfSettingsProfilesShare.open(profileName, exportText, onImport, initialImport)
end
-- ---------------------------------------------------------
-- Import Preview modal
-- ---------------------------------------------------------
local function EnsureImportPreview(ctx)
  local UI = ctx and ctx.UI
  if not UI then return end
  if UI._wfSettingsProfilesImportPreview then return end

  local C4 = ctx.C4
  local TEXT, MUTED, PANEL, BORDER = ctx.TEXT, ctx.MUTED, ctx.PANEL, ctx.BORDER
  local Widgets = ctx.Widgets
  local MakeInput = ctx.MakeInput

  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, blocker, "BackdropTemplate")
  box:SetSize(620, 420)
  box:SetPoint("CENTER", UIParent, "CENTER", 0, 20)
  box:SetFrameStrata("FULLSCREEN_DIALOG")
  box:SetFrameLevel(blocker:GetFrameLevel() + 60)
  box:EnableMouse(true)
  box:SetScript("OnMouseDown", function() end)
  box:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  box:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  box:SetBackdropBorderColor(C4(BORDER, BORDER))

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

  local body = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  body:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
  body:SetPoint("TOPRIGHT", box, "TOPRIGHT", -14, -40)
  body:SetTextColor(C4(MUTED, MUTED))
  body:SetJustifyH("LEFT")
  body:SetJustifyV("TOP")
  body:SetText("Review changes before importing.")

  local nameLab = box:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
  nameLab:SetPoint("TOPLEFT", 14, -64)
  nameLab:SetTextColor(C4(MUTED, MUTED))
  nameLab:SetText("Save as")

  local nameInput = MakeInput(box, 300, 24)
  nameInput:SetPoint("LEFT", nameLab, "RIGHT", 12, 0)

  local cb = CreateFrame("CheckButton", nil, box, "UICheckButtonTemplate")
  cb:SetSize(20, 20)

  WF_ApplyBlueCheck(cb)
cb:SetPoint("LEFT", nameInput, "RIGHT", 14, 0)
  if cb.Text then cb.Text:Hide() end

  local cbText = box:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
  cbText:SetPoint("LEFT", cb, "RIGHT", 6, 0)
  cbText:SetTextColor(C4(TEXT, TEXT))
  cbText:SetText("Activate after import")

  local panel = CreateFrame("Frame", nil, box, "BackdropTemplate")
  panel:SetPoint("TOPLEFT", 14, -100)
  panel:SetPoint("BOTTOMRIGHT", -14, 46)
  panel:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    edgeSize = 1,
  })
  panel:SetBackdropColor(0.02, 0.02, 0.02, 1.0)
  panel:SetBackdropBorderColor(C4(BORDER, BORDER))
  panel:EnableMouse(true)
  panel:SetScript("OnMouseDown", function() end)

  local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and scroll.ScrollBar then Widgets:SkinScrollBar(scroll.ScrollBar) end
  scroll:SetPoint("TOPLEFT", 8, -8)
  scroll:SetPoint("BOTTOMRIGHT", -28, 8)
  scroll:EnableMouse(true)

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

  local lines = {}

  local function GetPreviewContentWidth()
    local width = (scroll:GetWidth() or 0) - 12
    if width < 1 then width = 1 end
    return width
  end

  local function SetLines(textLines)
    box._previewLines = textLines or {}

    local width = GetPreviewContentWidth()
    sc:SetWidth(width)

    local y = 0
    for i = 1, #box._previewLines do
      local fs = lines[i]
      if not fs then
        fs = sc:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightSmall")
        fs:SetJustifyH("LEFT")
        fs:SetJustifyV("TOP")
        fs:SetWordWrap(true)
        fs:SetNonSpaceWrap(true)
        fs:SetTextColor(C4(TEXT, TEXT))
        lines[i] = fs
      end
      fs:ClearAllPoints()
      fs:SetPoint("TOPLEFT", 6, -y)
      fs:SetWidth(width - 12)
      fs:SetText(box._previewLines[i] or "")
      fs:Show()

      local h = fs:GetStringHeight() or 14
      if h < 14 then h = 14 end
      y = y + h + 4
    end

    for i = #box._previewLines + 1, #lines do
      if lines[i] then lines[i]:Hide() end
    end

    sc:SetHeight(y + 8)
  end

  scroll:SetScript("OnSizeChanged", function()
    if box._previewLines then
      SetLines(box._previewLines)
    end
  end)

  local bCancel = Widgets:Button(box, "Cancel", 92, 24)
  bCancel:SetPoint("BOTTOMRIGHT", -14, 14)

  local bImport = Widgets:Button(box, "Import", 110, 24)
  bImport:SetPoint("RIGHT", bCancel, "LEFT", -10, 0)

  local function Close(reopenShare)
    blocker:Hide()
    box:Hide()

    local share = UI and UI._wfSettingsProfilesShare
    if share then
      if reopenShare and share._hiddenForImportPreview and share.box then
        share.box:Show()
      end
      share._hiddenForImportPreview = nil
    end

    nameInput:ClearFocus()
    box._onImport = nil
  end

  blocker:SetScript("OnMouseDown", function() Close(true) end)
  bCancel:SetScript("OnClick", function() Close(true) end)

  bImport:SetScript("OnClick", function()
    local cbFn = box._onImport
    local share = UI and UI._wfSettingsProfilesShare

    if type(cbFn) ~= "function" then
      Close(false)
      if share and share.close then
        share.close()
      end
      return
    end

    local nm = Trim(nameInput:GetText() or "")
    local activate = cb:GetChecked() and true or false

    Close(false)

    if share and share.close then
      share.close()
    elseif share then
      if share.blocker then share.blocker:Hide() end
      if share.box then share.box:Hide() end
      share._hiddenForImportPreview = nil
    end

    cbFn(nm, activate)
  end)

  nameInput:SetScript("OnEnterPressed", function() bImport:Click() end)
  nameInput:SetScript("OnEscapePressed", function() Close(true) end)

  UI._wfSettingsProfilesImportPreview = {
    blocker = blocker,
    box = box,
    title = title,
    body = body,
    nameInput = nameInput,
    cb = cb,
    setLines = SetLines,
    open = function(suggestedName, activateDefault, previewLines, onImport)
      local share = UI and UI._wfSettingsProfilesShare
      if share and share.box and share.box:IsShown() then
        share._hiddenForImportPreview = true
        share.box:Hide()
      else
        if share then
          share._hiddenForImportPreview = nil
        end
      end

      nameInput:SetText(suggestedName or "")
      nameInput:HighlightText()
      cb:SetChecked(activateDefault and true or false)
      box._onImport = onImport
      blocker:Show()
      box:Show()
      box:Raise()
      SetLines(previewLines or {})
      nameInput:SetFocus()
    end,
  }
end

local function ShowImportPreview(ctx, suggestedName, activateDefault, previewLines, onImport)
  EnsureImportPreview(ctx)
  local UI = ctx and ctx.UI
  if not UI or not UI._wfSettingsProfilesImportPreview then return end
  UI._wfSettingsProfilesImportPreview.open(suggestedName, activateDefault, previewLines, onImport)
end

-- ---------------------------------------------------------
-- Page
-- ---------------------------------------------------------
WF.UI.HelpPages.SettingsProfiles = function(ctx, tab)
  local DEFAULT_HELP_MIN_W = 1040
  local DEFAULT_HELP_MIN_H = 640
  local PROFILES_HELP_MIN_W = 1120
  local PROFILES_HELP_MIN_H = DEFAULT_HELP_MIN_H
  local HELP_MAX_W = 2200
  local HELP_MAX_H = 1400

  local function GetRootHelpFrame()
    if ctx and ctx.UI and ctx.UI.frame then
      return ctx.UI.frame
    end

    local p = tab
    while p do
      if p.GetName and p:GetName() == "WhatFarmHelpFrame" then
        return p
      end
      p = p.GetParent and p:GetParent() or nil
    end

    return nil
  end

  local function SetHelpResizeFloor(minW, minH)
    local frame = GetRootHelpFrame()
    if not frame then return end

    if frame.SetResizeBounds then
      frame:SetResizeBounds(minW, minH, HELP_MAX_W, HELP_MAX_H)
    else
      if frame.SetMinResize then frame:SetMinResize(minW, minH) end
      if frame.SetMaxResize then frame:SetMaxResize(HELP_MAX_W, HELP_MAX_H) end
    end
  end

  local function EnsureProfilesResizeFloor()
    local frame = GetRootHelpFrame()
    if not frame then return end

    SetHelpResizeFloor(PROFILES_HELP_MIN_W, PROFILES_HELP_MIN_H)

    local curW = floor((frame:GetWidth() or 0) + 0.5)
    local curH = floor((frame:GetHeight() or 0) + 0.5)
    local newW = curW < PROFILES_HELP_MIN_W and PROFILES_HELP_MIN_W or curW
    local newH = curH < PROFILES_HELP_MIN_H and PROFILES_HELP_MIN_H or curH

    if newW ~= curW or newH ~= curH then
      frame:SetSize(newW, newH)
    end
  end

  local function RestoreDefaultResizeFloor()
    SetHelpResizeFloor(DEFAULT_HELP_MIN_W, DEFAULT_HELP_MIN_H)
  end
  if not (ctx and tab) then return end
  if not (ctx.WF and ctx.WF.DB and ctx.WF.DB.GetProfileNames) then
    local t = tab:CreateFontString(nil, "OVERLAY", "WF_GameFontDisable")
    t:SetPoint("TOPLEFT", 18, -54)
    t:SetText("Profiles are unavailable (missing DB profile APIs).")
    return
  end

  local UI = ctx.UI
  local WFDB = ctx.WF.DB
  local Widgets = ctx.Widgets
  local C4 = ctx.C4

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

  local MakePanel = ctx.MakePanel
  local MakeSectionTitle = ctx.MakeSectionTitle
  local MakeDivider = ctx.MakeDivider
  local MakeMutedText = ctx.MakeMutedText
  local MakeBodyText = ctx.MakeBodyText
  local MakeHeaderText = ctx.MakeHeaderText
  local MakeInput = ctx.MakeInput
  local Money = ctx.Money

  local function FormatGold(v)
    v = tonumber(v) or 0
    if Money then
      return Money(v)
    end
    return tostring(v)
  end

  local function IsDefault(name)
    if WFDB.IsDefaultProfile then
      return WFDB:IsDefaultProfile(name)
    end
    return false
  end

  local function NormalizeProfile(p)
    if type(p) ~= "table" then return nil end
    local out = {
      lootMinGold = tonumber(p.lootMinGold) or 0,
      lootMinQty = tonumber(p.lootMinQty) or 1,
      lootMinQuality = tonumber(p.lootMinQuality) or 0,
      lootIncludeMaterials = p.lootIncludeMaterials and true or false,
      lootOnlyMaterials = p.lootOnlyMaterials and true or false,
      lootOnlyGear = p.lootOnlyGear and true or false,
      lootRecordMaterials = p.lootRecordMaterials and true or false,
      lootRecordGear = p.lootRecordGear and true or false,
      lootRecordRecipes = p.lootRecordRecipes and true or false,
      lootRecordPets = p.lootRecordPets and true or false,
      lootRecordToys = p.lootRecordToys and true or false,
      lootRecordMounts = p.lootRecordMounts and true or false,
      lootRecordConsumables = p.lootRecordConsumables and true or false,
      lootRecordMisc = p.lootRecordMisc and true or false,
      lootIgnoreSoulbound = p.lootIgnoreSoulbound and true or false,
      priceSource = tostring(p.priceSource or ""),
      tsmPriceString = tostring(p.tsmPriceString or ""),
      overviewLast5Metric = tostring(p.overviewLast5Metric or ""),
    }
    HydrateCategorySelections(out)
    SyncLegacyCategoryFlags(out)
    return out
  end

  local function CopyProfileValues(src, dst)
    if type(src) ~= "table" or type(dst) ~= "table" then return end
    dst.lootMinGold = tonumber(src.lootMinGold) or 0
    dst.lootMinQty = tonumber(src.lootMinQty) or 1
    dst.lootMinQuality = tonumber(src.lootMinQuality) or 0
    dst.lootIncludeMaterials = src.lootIncludeMaterials and true or false
    dst.lootOnlyMaterials = src.lootOnlyMaterials and true or false
    dst.lootOnlyGear = src.lootOnlyGear and true or false
    dst.lootRecordMaterials = src.lootRecordMaterials and true or false
    dst.lootRecordGear = src.lootRecordGear and true or false
    dst.lootRecordRecipes = src.lootRecordRecipes and true or false
    dst.lootRecordPets = src.lootRecordPets and true or false
    dst.lootRecordToys = src.lootRecordToys and true or false
    dst.lootRecordMounts = src.lootRecordMounts and true or false
    dst.lootRecordConsumables = src.lootRecordConsumables and true or false
    dst.lootRecordMisc = src.lootRecordMisc and true or false
    dst.lootIgnoreSoulbound = src.lootIgnoreSoulbound and true or false
    dst.priceSource = tostring(src.priceSource or "")
    dst.tsmPriceString = tostring(src.tsmPriceString or "")
    dst.overviewLast5Metric = tostring(src.overviewLast5Metric or "")
    HydrateCategorySelections(dst)
    SyncLegacyCategoryFlags(dst)
  end

  local function BuildDiff(activeP, selP)
    local out = {}

    if not activeP or not selP then
      out[#out + 1] = "Select a profile to see differences."
      return out
    end

    local DIFF_LABEL = "8fd3ff"
    local DIFF_HINT  = "7f8a94"
    local DIFF_OLD   = "8fd3ff"
    local DIFF_NEW   = "f2f7ff"
    local DIFF_OK_A  = "8fd3ff"
    local DIFF_OK_B  = "f2f7ff"

    local function DiffColor(hex, value)
      return "|cff" .. tostring(hex or "ffffff") .. tostring(value or "") .. "|r"
    end

    local function CleanDiffValue(v)
      local s = tostring(v or "")
      s = s:gsub("\r", " "):gsub("\n", " ")
      s = s:gsub("%s+", " ")
      s = Trim(s)
      if s == "" then s = "Not set" end
      return s
    end

    local function Add(label, a, b)
      a = CleanDiffValue(a)
      b = CleanDiffValue(b)
      if a == b then return end
      out[#out + 1] = DiffColor(DIFF_LABEL, label) .. ": "
        .. DiffColor(DIFF_HINT, "was previously ")
        .. DiffColor(DIFF_OLD, a)
        .. DiffColor(DIFF_HINT, ", is now ")
        .. DiffColor(DIFF_NEW, b)
    end

    local function EnabledDisabled(v)
      return v and "Enabled" or "Disabled"
    end

    Add("Min value", FormatGold(activeP.lootMinGold), FormatGold(selP.lootMinGold))
    Add("Min quantity", tostring(activeP.lootMinQty), tostring(selP.lootMinQty))
    Add("Min quality", QualityValueToDisplay(activeP.lootMinQuality), QualityValueToDisplay(selP.lootMinQuality))
    Add("Categories", CategorySummary(activeP), CategorySummary(selP))
    Add("Soulbound or BoP items", EnabledDisabled(activeP.lootIgnoreSoulbound), EnabledDisabled(selP.lootIgnoreSoulbound))
    local fallbackTSM = (WFDB and WFDB.GetSetting and WFDB:GetSetting("tsmPriceString")) or "dbregionmarketavg"

    local psA = PriceSourceDisplayForProfile(activeP, fallbackTSM)
    local psB = PriceSourceDisplayForProfile(selP, fallbackTSM)

    local mA  = activeP.overviewLast5Metric ~= "" and MetricDisplayToken(activeP.overviewLast5Metric) or "—"
    local mB  = selP.overviewLast5Metric ~= "" and MetricDisplayToken(selP.overviewLast5Metric) or "—"

    Add("Price Source", psA, psB)
    Add("Overview Metric", mA, mB)

    if #out == 0 then
      out[#out + 1] = DiffColor(DIFF_OK_A, "No differences.") .. " " .. DiffColor(DIFF_OK_B, "This profile matches your active settings.")
    end

    return out
  end

  local function BuildImportPreviewLines(parsedName, importedP, activeName, activeP)
    local out = {}
    local imported = NormalizeProfile(importedP)
    local active = NormalizeProfile(activeP)
    local fallbackTSM = (WFDB and WFDB.GetSetting and WFDB:GetSetting("tsmPriceString")) or "dbregionmarketavg"

    out[#out + 1] = "Imported profile: " .. tostring(parsedName or "Imported")
    out[#out + 1] = "Compared against active: " .. tostring(GetProfileDisplayName(ctx, WFDB, activeName))
    out[#out + 1] = " "
    out[#out + 1] = "Contains"

    if not imported then
      out[#out + 1] = "- Unable to read profile settings."
      return out
    end

    out[#out + 1] = "- Min value: " .. tostring(FormatGold(imported.lootMinGold))
    out[#out + 1] = "- Min quantity: " .. tostring(imported.lootMinQty)
    out[#out + 1] = "- Min quality: " .. tostring(QualityValueToDisplay(imported.lootMinQuality))
    out[#out + 1] = "- Categories: " .. tostring(CategorySummary(imported))
    out[#out + 1] = "- Soulbound or BoP items: " .. tostring(YesNo(imported.lootIgnoreSoulbound))
    out[#out + 1] = "- Price Source: " .. tostring(PriceSourceDisplayForProfile(imported, fallbackTSM))

    local metric = imported.overviewLast5Metric ~= "" and MetricDisplayToken(imported.overviewLast5Metric) or "—"
    out[#out + 1] = "- Overview Metric: " .. tostring(metric)
    out[#out + 1] = " "
    out[#out + 1] = "Changes vs active"

    local diff = BuildDiff(active, imported)
    for i = 1, #diff do
      out[#out + 1] = diff[i]
    end

    return out
  end

  local function FindUniqueName(base)
    base = Trim(base)
    if base == "" then base = "Imported" end

    local exists = {}
    local names = WFDB:GetProfileNames() or {}
    for i = 1, #names do
      exists[names[i]] = true
    end

    if not exists[base] then return base end

    local n = 2
    while n < 999 do
      local candidate = base .. " " .. tostring(n)
      if not exists[candidate] then
        return candidate
      end
      n = n + 1
    end

    return base .. " " .. tostring(time() or 0)
  end

  -- Header
  local header = nil
  if type(MakeHeaderText) == "function" then
    header = MakeHeaderText(tab, "Profiles")
  elseif type(MakeSectionTitle) == "function" then
    header = MakeSectionTitle(tab, "Profiles")
  end
  if header then
    header:SetPoint("TOPLEFT", 12, -12)
  end

  -- Root panels
  local left = MakePanel(tab, PANEL)
  local right = MakePanel(tab, PANEL)

  if header then
    left:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -12)
  else
    left:SetPoint("TOPLEFT", 12, -12)
  end
  left:SetPoint("BOTTOMLEFT", 12, 12)
  left:SetWidth(284)

  right:SetPoint("TOPLEFT", left, "TOPRIGHT", 12, 0)
  right:SetPoint("BOTTOMRIGHT", -12, 12)

  -- ---------------------------------------------------------
  -- Left: search + favorites-only + list
  -- ---------------------------------------------------------
  local filterPanel = MakePanel(left, BG)
  filterPanel:SetPoint("TOPLEFT", 16, -16)
  filterPanel:SetPoint("TOPRIGHT", -16, -16)
  filterPanel:SetHeight(66)
  filterPanel:SetBackdropColor(C4(BG))

  local search = MakeInput(filterPanel, 236, 24)
  search:SetPoint("TOPLEFT", 10, -10)
  search:SetPoint("TOPRIGHT", -10, -10)
  search:SetText("")

  local searchPH = MakeMutedText(search, "Search profiles…")
  searchPH:SetPoint("LEFT", 10, 0)
  searchPH:SetPoint("RIGHT", -10, 0)
  searchPH:SetJustifyH("LEFT")
  searchPH:SetAlpha(0.72)

  local clear = search.ClearButton
  if clear then
    clear:Hide()
    clear:SetAlpha(0)
    if clear.Disable then
      clear:Disable()
    end
    if clear.SetScript then
      clear:SetScript("OnShow", clear.Hide)
    elseif clear.HookScript then
      clear:HookScript("OnShow", clear.Hide)
    end
  end

  local favOnly = CreateFrame("CheckButton", nil, filterPanel, "UICheckButtonTemplate")
  favOnly:SetSize(20, 20)
  WF_ApplyBlueCheck(favOnly)
  favOnly:SetPoint("TOPLEFT", search, "BOTTOMLEFT", -4, -8)
  if favOnly.Text then favOnly.Text:Hide() end

  local favOnlyText = filterPanel:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  favOnlyText:SetPoint("LEFT", favOnly, "RIGHT", 4, 0)
  favOnlyText:SetTextColor(C4(MUTED, MUTED))
  favOnlyText:SetText("Favorites only")

  local function UpdateSearchChrome()
    local txt = search:GetText() or ""
    local hasText = txt ~= ""
    local hasFocus = search:HasFocus() and true or false
    SetShown(searchPH, (not hasText) and (not hasFocus))
    if clear and clear.Hide then
      clear:Hide()
    end
  end

  search:SetScript("OnEditFocusGained", UpdateSearchChrome)
  search:SetScript("OnEditFocusLost", UpdateSearchChrome)
  UpdateSearchChrome()

  local listPanel = CreateFrame("Frame", nil, left)
  listPanel:SetPoint("TOPLEFT", filterPanel, "BOTTOMLEFT", 0, -10)
  listPanel:SetPoint("TOPRIGHT", filterPanel, "TOPRIGHT", 0, -76)
  listPanel:SetPoint("BOTTOMLEFT", 16, 16)

  local listHeader = CreateFrame("Frame", nil, listPanel)
  listHeader:SetPoint("TOPLEFT", 0, 0)
  listHeader:SetPoint("TOPRIGHT", -18, 0)
  listHeader:SetHeight(18)

  local listHeaderFav = listHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  listHeaderFav:SetPoint("LEFT", 10, 0)
  listHeaderFav:SetTextColor(C4(MUTED, MUTED))
  listHeaderFav:SetText("")

  local listHeaderName = listHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  listHeaderName:SetPoint("LEFT", 34, 0)
  listHeaderName:SetTextColor(C4(MUTED, MUTED))
  listHeaderName:SetText("Profile")

  local listHeaderType = listHeader:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  listHeaderType:SetPoint("RIGHT", -12, 0)
  listHeaderType:SetTextColor(C4(MUTED, MUTED))
  listHeaderType:SetText("Type")


  local listHeaderLine = listPanel:CreateTexture(nil, "ARTWORK")
  listHeaderLine:SetTexture("Interface/Buttons/WHITE8x8")
  listHeaderLine:SetPoint("TOPLEFT", 10, -22)
  listHeaderLine:SetPoint("TOPRIGHT", -18, -22)
  listHeaderLine:SetHeight(1)
  listHeaderLine:SetVertexColor(C4(BORDER, BORDER))
  listHeaderLine:SetAlpha(0.30)

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

  -- IMPORTANT: scroll child must have a real width or nothing shows/clicks.
  local sc = CreateFrame("Frame", nil, scroll)
  sc:SetPoint("TOPLEFT", 0, 0)
  sc:SetPoint("TOPRIGHT", 0, 0)
  sc:SetHeight(1)
  sc:SetWidth(1)
  scroll:SetScrollChild(sc)

  local emptyText = MakeMutedText(listPanel, "No profiles found.")
  emptyText:SetPoint("CENTER", 0, -4)
  emptyText:SetAlpha(0.75)
  emptyText:Hide()

  local function SyncListScrollWidth()
    local w = scroll:GetWidth() or 1
    if w < 1 then w = 1 end
    sc:SetWidth(w)
  end
  scroll:SetScript("OnSizeChanged", SyncListScrollWidth)

  local rowH = 26
  local rows = {}
  local selectedName = nil
  local filterText = ""
  local showFavOnly = false
  local lastLeftClickName = nil
  local lastLeftClickAt = 0
  local doubleClickWindow = 0.35

  local function FilterAndSort(names, needle)
    needle = Trim(needle)
    local nNeedle = Lower(needle)

    local fav = GetFavorites(ctx, WFDB)
    local out = {}

    for i = 1, #names do
      local n = names[i]
      local displayName = GetProfileDisplayName(ctx, WFDB, n)
      local matches = (needle == "")
        or Lower(displayName):find(nNeedle, 1, true)
        or Lower(n):find(nNeedle, 1, true)
      if matches then
        if (not showFavOnly) or fav[n] then
          out[#out + 1] = n
        end
      end
    end

    table.sort(out, function(a, b)
      local af = fav[a] and 1 or 0
      local bf = fav[b] and 1 or 0
      if af ~= bf then return af > bf end

      local ad = IsDefault(a) and 1 or 0
      local bd = IsDefault(b) and 1 or 0
      if ad ~= bd then return ad > bd end

      return tostring(a) < tostring(b)
    end)

    return out
  end

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

    local r = CreateFrame("Button", nil, sc)
    r:SetHeight(rowH)
    r:SetPoint("LEFT", 0, 0)
    r:SetPoint("RIGHT", 0, 0)
    r:EnableMouse(true)
    r:RegisterForClicks("LeftButtonUp", "RightButtonUp")

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

    r.star = CreateFrame("Button", nil, r)
    r.star:SetSize(16, 16)
    r.star:SetPoint("LEFT", 10, 0)
    r.star:EnableMouse(true)
    r.star:RegisterForClicks("LeftButtonUp")

    r.star.tex = r.star:CreateTexture(nil, "OVERLAY")
    r.star.tex:SetAllPoints(r.star)
    r.star.tex:SetTexture("Interface\\COMMON\\FavoritesIcon")
    WF_SetFavoriteStarVisual(r.star.tex, 0.35)

    r.name = r:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    r.name:SetPoint("LEFT", r.star, "RIGHT", 8, 0)
    r.name:SetTextColor(C4(TEXT, TEXT))
    r.name:SetJustifyH("LEFT")

    r.badge = r:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    r.badge:SetPoint("RIGHT", -12, 0)
    r.badge:SetTextColor(C4(MUTED, MUTED))
    r.badge:SetJustifyH("RIGHT")

    r.name:SetPoint("RIGHT", r.badge, "LEFT", -16, 0)

    r.sep = r:CreateTexture(nil, "BORDER")
    r.sep:SetTexture("Interface/Buttons/WHITE8x8")
    r.sep:SetHeight(1)
    r.sep:SetPoint("BOTTOMLEFT", 10, 0)
    r.sep:SetPoint("BOTTOMRIGHT", -1, 0)
    r.sep:SetAlpha(0.06)

    r:SetScript("OnEnter", function(self)
      if self._selected then return end
      self.bg:SetAlpha(0.08)
    end)

    r:SetScript("OnLeave", function(self)
      if self._selected then
        self.bg:SetAlpha(0.16)
      else
        self.bg:SetAlpha(0)
      end
    end)

    BindTooltip(r, function(self)
      return GetProfileDisplayName(ctx, WFDB, self._name or "Profile")
    end, function(self)
      local state = self._badgeText or "Profile"
      return {
        "Left-click to select this profile.",
        "Right-click for actions.",
        "Type: " .. tostring(state),
      }
    end)

    r.star:SetScript("OnEnter", function(self)
      local name = self._name
      if not name or name == "" then return end
      WF_SetFavoriteStarVisual(self.tex, 0.95)

      local title
      if IsFavorite(ctx, WFDB, name) then
        title = "Unfavorite"
      else
        title = "Favorite"
      end
      _WF_ShowStyledTooltip(self, title, { "Pinned profiles sort to the top." }, "ANCHOR_RIGHT")
    end)

    r.star:SetScript("OnLeave", function(self)
      local name = self._name
      if name and IsFavorite(ctx, WFDB, name) then
        WF_SetFavoriteStarVisual(self.tex, 0.85)
      else
        WF_SetFavoriteStarVisual(self.tex, 0.35)
      end
      if GameTooltip:IsOwned(self) then GameTooltip:Hide() end
    end)

    rows[i] = r
    return r
  end

  -- ---------------------------------------------------------
  -- Right: title + chips + primary actions + "More" menu + preview + editor + diff
  -- ---------------------------------------------------------
  local rTitle = right:CreateFontString(nil, "OVERLAY", "WF_GameFontNormalLarge")
  rTitle:SetPoint("TOPLEFT", 16, -16)
  rTitle:SetTextColor(C4(TEXT, TEXT))
  rTitle:SetText("Profile")

  local rSub = right:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
  rSub:SetPoint("TOPLEFT", rTitle, "BOTTOMLEFT", 0, -4)
  rSub:SetTextColor(C4(MUTED, MUTED))
  rSub:SetText(" ")

  local chips = CreateFrame("Frame", nil, right)
  chips:SetPoint("TOPLEFT", 16, -54)
  chips:SetPoint("TOPRIGHT", -16, -54)
  chips:SetHeight(20)

  local function MakeChip(parent, text, style)
    local f = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    f:SetHeight(18)
    f:SetBackdrop({
      bgFile = "Interface/Buttons/WHITE8x8",
      edgeFile = "Interface/Buttons/WHITE8x8",
      edgeSize = 1,
    })

    local bgA = 0.10
    local br = BORDER
    if style == "active" then
      br = ACCENT
      bgA = 0.16
    elseif style == "preset" then
      bgA = 0.12
    elseif style == "fav" then
      bgA = 0.12
    end

    f:SetBackdropColor(0, 0, 0, bgA)
    f:SetBackdropBorderColor(C4(br, br))

    local t = f:CreateFontString(nil, "OVERLAY", "WF_GameFontDisableSmall")
    t:SetPoint("LEFT", 8, 0)
    t:SetPoint("RIGHT", -8, 0)
    t:SetTextColor(C4(MUTED, MUTED))
    t:SetText(text or "")
    f.text = t

    local w = t:GetStringWidth() + 18
    if w < 54 then w = 54 end
    f:SetWidth(w)

    return f
  end

  local chipActive = MakeChip(chips, "Active", "active")
  local chipPreset = MakeChip(chips, "Preset", "preset")
  local chipBase = MakeChip(chips, "Default", "base")
  local chipFav = MakeChip(chips, "Favorite", "fav")

  local function LayoutChips()
    local x = 0
    local gap = 8
    local order = { chipActive, chipPreset, chipBase, chipFav }
    for i = 1, #order do
      local c = order[i]
      if c:IsShown() then
        c:ClearAllPoints()
        c:SetPoint("LEFT", x, 0)
        x = x + c:GetWidth() + gap
      end
    end
  end

  local divTop = MakeDivider(right)
  divTop:SetPoint("TOPLEFT", 16, -80)
  divTop:SetPoint("TOPRIGHT", -16, -80)

  local function MakeAction(text, w)
    local b = Widgets:Button(right, text, w or 110, 24)
    b._wfPreferredWidth = w or 110
    return b
  end

  local bSetActive = MakeAction("Set Active", 110)
  local bSave = MakeAction("Save Current", 120)
  local bDuplicate = MakeAction("Duplicate", 110)
  local bEdit = MakeAction("Edit Rules", 110)
  local bMore = MakeAction("More", 84)

  BindTooltip(bSave, "Save Current", "Overwrite this profile with your current live settings.")
  BindTooltip(bDuplicate, "Duplicate", "Create a copy of this profile so you can tweak it without changing the original.")
  BindTooltip(bEdit, "Edit Rules", "Open or close the advanced rule editor for this profile.")
  BindTooltip(bMore, "More", "Open extra profile actions like apply to active, create a new profile, rename, share, or delete.")

  local cards = CreateFrame("Frame", nil, right)

  local function MakeCard(parent, w)
    local p = MakePanel(parent, BG)
    p:SetHeight(52)
    p:SetWidth(w)
    p:SetBackdropColor(C4(BG))

    local lab = MakeMutedText(p, "")
    lab:SetPoint("TOPLEFT", 12, -8)

    local val = p:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlightLarge")
    val:SetPoint("TOPLEFT", lab, "BOTTOMLEFT", 0, -2)
    val:SetTextColor(C4(TEXT, TEXT))
    val:SetText("—")

    p.lab = lab
    p.val = val
    return p
  end

  local c1 = MakeCard(cards, 176)
  local c2 = MakeCard(cards, 176)
  local c3 = MakeCard(cards, 176)

  c1:SetPoint("LEFT", 0, 0)
  c2:SetPoint("LEFT", c1, "RIGHT", 10, 0)
  c3:SetPoint("LEFT", c2, "RIGHT", 10, 0)

  c1.lab:SetText("Min Value")
  c2.lab:SetText("Price Source")
  c3.lab:SetText("Overview Metric")

  local detailScroll = CreateFrame("ScrollFrame", nil, right, "UIPanelScrollFrameTemplate")
  if Widgets and Widgets.SkinScrollBar and detailScroll.ScrollBar then Widgets:SkinScrollBar(detailScroll.ScrollBar) end
  detailScroll:EnableMouseWheel(true)

  local function LayoutResponsiveRightTop()
    local innerW = floor(((right:GetWidth() or 0) - 32) + 0.5)
    if innerW < 220 then innerW = 220 end

    local buttons = { bSetActive, bSave, bDuplicate, bEdit, bMore }
    local gapX = 8
    local gapY = 8
    local rowH = 24
    local x = 0
    local y = 0

    for i = 1, #buttons do
      local btn = buttons[i]
      local bw = btn._wfPreferredWidth or btn:GetWidth() or 110
      btn:SetWidth(bw)
      if x > 0 and (x + bw) > innerW then
        x = 0
        y = y + rowH + gapY
      end
      btn:ClearAllPoints()
      btn:SetPoint("TOPLEFT", right, "TOPLEFT", 16 + x, -92 - y)
      x = x + bw + gapX
    end

    local actionsHeight = y + rowH
    local cardsTop = 92 + actionsHeight + 10

    cards:ClearAllPoints()
    cards:SetPoint("TOPLEFT", right, "TOPLEFT", 16, -cardsTop)
    cards:SetPoint("TOPRIGHT", right, "TOPRIGHT", -16, -cardsTop)

    local cardGap = 10
    local minCardW = 156
    local cols = 1
    if innerW >= (minCardW * 3) + (cardGap * 2) then
      cols = 3
    elseif innerW >= (minCardW * 2) + cardGap then
      cols = 2
    end

    local cardW = floor((innerW - (cardGap * (cols - 1))) / cols)
    if cardW < 120 then cardW = 120 end

    local cardList = { c1, c2, c3 }
    local cardH = 52
    local cardRows = math.ceil(#cardList / cols)
    local cardsHeight = (cardRows * cardH) + ((cardRows - 1) * cardGap)
    cards:SetHeight(cardsHeight)

    for i = 1, #cardList do
      local card = cardList[i]
      local row = math.floor((i - 1) / cols)
      local col = (i - 1) % cols
      card:ClearAllPoints()
      card:SetWidth(cardW)
      card:SetPoint("TOPLEFT", cards, "TOPLEFT", col * (cardW + cardGap), -(row * (cardH + cardGap)))
    end

    detailScroll:ClearAllPoints()
    detailScroll:SetPoint("TOPLEFT", right, "TOPLEFT", 16, -(cardsTop + cardsHeight + 14))
    detailScroll:SetPoint("BOTTOMRIGHT", right, "BOTTOMRIGHT", -28, 16)
  end

  local detailSC = CreateFrame("Frame", nil, detailScroll)
  detailSC:SetPoint("TOPLEFT", 0, 0)
  detailSC:SetPoint("TOPRIGHT", 0, 0)
  detailSC:SetHeight(1)
  detailSC:SetWidth(1)
  detailScroll:SetScrollChild(detailSC)

  local function SyncDetailScrollWidth()
    local w = detailScroll:GetWidth() or 1
    if w < 1 then w = 1 end
    detailSC:SetWidth(w)
  end

  local function ScrollDetailByDelta(delta)
    local step = 44
    local cur = detailScroll:GetVerticalScroll() or 0
    local max = detailScroll:GetVerticalScrollRange() or 0
    if max <= 0 then return false end
    local nextPos = cur - ((delta or 0) * step)
    if nextPos < 0 then nextPos = 0 end
    if nextPos > max then nextPos = max end
    detailScroll:SetVerticalScroll(nextPos)
    return true
  end

  local function UpdateDetailScrollState()
    local max = detailScroll:GetVerticalScrollRange() or 0
    local canScroll = max > 0
    if detailScroll.ScrollBar then
      if canScroll then
        detailScroll.ScrollBar:Show()
      else
        detailScroll.ScrollBar:Hide()
      end
    end
    detailScroll:EnableMouseWheel(canScroll)
    if detailScroll.SetPropagateMouseWheel then
      detailScroll:SetPropagateMouseWheel(not canScroll)
    end
  end

  detailScroll:SetScript("OnMouseWheel", function(_, delta)
    ScrollDetailByDelta(delta)
  end)

  local divMid = MakeDivider(detailSC)
  divMid:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, 0)
  divMid:SetPoint("TOPRIGHT", detailSC, "TOPRIGHT", 0, 0)

  local pvTitle = MakeSectionTitle(detailSC, "Rules Preview")
  pvTitle:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, -14)

  local pvBox = CreateFrame("Frame", nil, detailSC)
  pvBox:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, -38)
  pvBox:SetHeight(118)

  local function MakeKV(parent, x, y, wLabel)
    local lab = MakeMutedText(parent, "")
    lab:SetPoint("TOPLEFT", x, -y)
    lab:SetWidth(wLabel)
    lab:SetJustifyH("LEFT")
    lab:SetAlpha(0.85)

    local val = MakeBodyText(parent, "")
    val:SetPoint("TOPLEFT", x + wLabel + 8, -y)
    val:SetJustifyH("LEFT")

    return lab, val
  end

  local kv = { L = {}, R = {} }
  do
    local step = 18
    local y = 0
    local lx = 0
    local rx = 330

    kv.L.minValueL, kv.L.minValueV = MakeKV(pvBox, lx, y, 120); y = y + step
    kv.L.minQtyL, kv.L.minQtyV     = MakeKV(pvBox, lx, y, 120); y = y + step
    kv.L.minQualL, kv.L.minQualV   = MakeKV(pvBox, lx, y, 120); y = y + step
    kv.L.categoriesL, kv.L.categoriesV = MakeKV(pvBox, lx, y, 120); y = y + step
    kv.L.ignoreSbL, kv.L.ignoreSbV = MakeKV(pvBox, lx, y, 120)

    local y2 = 0
    kv.R.priceL, kv.R.priceV       = MakeKV(pvBox, rx, y2, 120); y2 = y2 + step
    kv.R.metricL, kv.R.metricV     = MakeKV(pvBox, rx, y2, 120); y2 = y2 + step
    kv.R.typeL, kv.R.typeV         = MakeKV(pvBox, rx, y2, 120); y2 = y2 + step
    kv.R.statusL, kv.R.statusV     = MakeKV(pvBox, rx, y2, 120)
  end

  local function GetDetailContentWidth()
    local w = detailSC and detailSC:GetWidth() or detailScroll:GetWidth() or right:GetWidth() or 1
    w = floor((w or 1) + 0.5)
    if w < 1 then w = 1 end
    return w
  end

  local function LayoutPreviewKV()
    local boxW = GetDetailContentWidth()
    pvBox:SetWidth(boxW)

    local labelW = 120
    local valueGap = 8
    local rowStep = 18

    local function PlacePair(lab, val, x, y, rightEdge, allowWrap)
      lab:ClearAllPoints()
      lab:SetPoint("TOPLEFT", pvBox, "TOPLEFT", x, -y)
      lab:SetWidth(labelW)
      lab:SetJustifyH("LEFT")

      val:ClearAllPoints()
      val:SetPoint("TOPLEFT", pvBox, "TOPLEFT", x + labelW + valueGap, -y)
      val:SetPoint("TOPRIGHT", pvBox, "TOPLEFT", rightEdge, -y)
      val:SetJustifyH("LEFT")
      if val.SetWordWrap then val:SetWordWrap(allowWrap and true or false) end
      if val.SetNonSpaceWrap then val:SetNonSpaceWrap(allowWrap and true or false) end

      local h = rowStep
      if allowWrap and val.GetStringHeight then
        local vh = val:GetStringHeight() or rowStep
        if vh > h then h = vh end
      end
      if h < rowStep then h = rowStep end
      return h
    end

    if boxW >= 620 then
      local colGap = 24
      local leftColW = floor((boxW - colGap) * 0.5)
      local rightX = leftColW + colGap
      local leftRight = leftColW - 6
      local rightRight = boxW - 6
      local y = 0
      y = y + PlacePair(kv.L.minValueL, kv.L.minValueV, 0, y, leftRight, false)
      y = y + PlacePair(kv.L.minQtyL, kv.L.minQtyV, 0, y, leftRight, false)
      y = y + PlacePair(kv.L.minQualL, kv.L.minQualV, 0, y, leftRight, false)
      y = y + PlacePair(kv.L.categoriesL, kv.L.categoriesV, 0, y, leftRight, false)
      y = y + PlacePair(kv.L.ignoreSbL, kv.L.ignoreSbV, 0, y, leftRight, false)

      local y2 = 0
      y2 = y2 + PlacePair(kv.R.priceL, kv.R.priceV, rightX, y2, rightRight, false)
      y2 = y2 + PlacePair(kv.R.metricL, kv.R.metricV, rightX, y2, rightRight, false)
      y2 = y2 + PlacePair(kv.R.typeL, kv.R.typeV, rightX, y2, rightRight, false)
      y2 = y2 + PlacePair(kv.R.statusL, kv.R.statusV, rightX, y2, rightRight, false)
      pvBox:SetHeight(math.max(y, y2) + 10)
    else
      local ordered = {
        { kv.L.minValueL, kv.L.minValueV },
        { kv.L.minQtyL, kv.L.minQtyV },
        { kv.L.minQualL, kv.L.minQualV },
        { kv.L.categoriesL, kv.L.categoriesV },
        { kv.L.ignoreSbL, kv.L.ignoreSbV },
        { kv.R.priceL, kv.R.priceV },
        { kv.R.metricL, kv.R.metricV },
        { kv.R.typeL, kv.R.typeV },
        { kv.R.statusL, kv.R.statusV },
      }
      local y = 0
      local rightEdge = boxW - 6
      for i = 1, #ordered do
        y = y + PlacePair(ordered[i][1], ordered[i][2], 0, y, rightEdge, true) + 4
      end
      pvBox:SetHeight(y)
    end
  end

  local editorOpen = false

  local editTitle = MakeSectionTitle(detailSC, "Rule Editor")
  editTitle:Hide()

  local editPanel = MakePanel(detailSC, BG)
  editPanel:SetHeight(244)
  editPanel:SetBackdropColor(C4(BG))
  editPanel:Hide()

  local editHelp = MakeMutedText(editPanel, "Applies to the selected profile.")
  editHelp:SetPoint("TOPLEFT", 12, -10)
  editHelp:SetAlpha(0.75)

  local function MakeLabel(parent, txt)
    local t = MakeMutedText(parent, txt or "")
    t:SetAlpha(0.85)
    return t
  end

  local editorInputX = 118
  local editorInputW = 118

  local eMinValueL = MakeLabel(editPanel, "Min value")
  eMinValueL:SetPoint("TOPLEFT", 12, -34)

  local eMinValue = MakeInput(editPanel, editorInputW, 22)
  eMinValue:SetPoint("TOPLEFT", editPanel, "TOPLEFT", editorInputX, -34)
  BindTooltip(eMinValue, "Min Value", "Click to set the minimum item value a drop needs before this profile records it.")

  local eMinQtyL = MakeLabel(editPanel, "Min Quantity")
  eMinQtyL:SetPoint("TOPLEFT", 12, -58)

  local eMinQty = MakeInput(editPanel, editorInputW, 22)
  eMinQty:SetPoint("TOPLEFT", editPanel, "TOPLEFT", editorInputX, -58)
  BindTooltip(eMinQty, "Min Quantity", "Click to set the minimum stack size required before this profile records a drop.")

  local eMinQualL = MakeLabel(editPanel, "Min quality")
  eMinQualL:SetPoint("TOPLEFT", 12, -82)

  local eMinQual = MakeInput(editPanel, editorInputW, 22)
  eMinQual:SetPoint("TOPLEFT", editPanel, "TOPLEFT", editorInputX, -82)
  BindTooltip(eMinQual, "Min Quality", "Click to set the minimum item quality. You can use names like Common, Uncommon, Rare, or Epic.")

  local function MakeCheck(parent, label)
    local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    cb:SetSize(20, 20)
    if cb.SetHitRectInsets then
      cb:SetHitRectInsets(0, -126, 0, 0)
    end

  WF_ApplyBlueCheck(cb)
if cb.Text then cb.Text:Hide() end
    local text = cb:CreateFontString(nil, "OVERLAY", "WF_GameFontHighlight")
    text:SetPoint("LEFT", cb, "RIGHT", 4, 0)
    text:SetTextColor(C4(TEXT, TEXT))
    text:SetText(label or "")
    cb._label = text
    return cb
  end


  local categoriesBox = MakePanel(editPanel, PANEL)
  categoriesBox:SetPoint("TOPLEFT", 248, -30)
  categoriesBox:SetSize(310, 126)
  categoriesBox:SetBackdropColor(0, 0, 0, 0.12)

  local eCategoriesL = MakeLabel(categoriesBox, "Record Categories")
  eCategoriesL:SetPoint("TOPLEFT", 12, -10)

  local eCatMaterials = MakeCheck(categoriesBox, "Materials")
  eCatMaterials:SetPoint("TOPLEFT", 12, -28)

  local eCatGear = MakeCheck(categoriesBox, "Gear")
  eCatGear:SetPoint("TOPLEFT", 12, -52)

  local eCatRecipes = MakeCheck(categoriesBox, "Recipes")
  eCatRecipes:SetPoint("TOPLEFT", 12, -76)

  local eCatPets = MakeCheck(categoriesBox, "Pets")
  eCatPets:SetPoint("TOPLEFT", 12, -100)

  local eCatToys = MakeCheck(categoriesBox, "Toys")
  eCatToys:SetPoint("TOPLEFT", 172, -28)

  local eCatMounts = MakeCheck(categoriesBox, "Mounts")
  eCatMounts:SetPoint("TOPLEFT", 172, -52)

  local eCatConsumables = MakeCheck(categoriesBox, "Consumables")
  eCatConsumables:SetPoint("TOPLEFT", 172, -76)

  local eCatMisc = MakeCheck(categoriesBox, "Misc / Other")
  eCatMisc:SetPoint("TOPLEFT", 172, -100)

  BindTooltip(eCatMaterials, unpack(CATEGORY_TOOLTIPS.lootRecordMaterials))
  BindTooltip(eCatGear, unpack(CATEGORY_TOOLTIPS.lootRecordGear))
  BindTooltip(eCatRecipes, unpack(CATEGORY_TOOLTIPS.lootRecordRecipes))
  BindTooltip(eCatPets, unpack(CATEGORY_TOOLTIPS.lootRecordPets))
  BindTooltip(eCatToys, unpack(CATEGORY_TOOLTIPS.lootRecordToys))
  BindTooltip(eCatMounts, unpack(CATEGORY_TOOLTIPS.lootRecordMounts))
  BindTooltip(eCatConsumables, unpack(CATEGORY_TOOLTIPS.lootRecordConsumables))
  BindTooltip(eCatMisc, unpack(CATEGORY_TOOLTIPS.lootRecordMisc))

  local eRestrictionsL = MakeLabel(editPanel, "Restrictions")
  eRestrictionsL:SetPoint("TOPLEFT", 260, -164)

  local eIgnoreSB = MakeCheck(editPanel, "Do not record Soulbound or BoP items")
  eIgnoreSB:SetPoint("TOPLEFT", 260, -180)
  BindTooltip(eIgnoreSB, unpack(CATEGORY_TOOLTIPS.lootIgnoreSoulbound))

  local ePriceL = MakeLabel(editPanel, "Price Source")
  ePriceL:SetPoint("TOPLEFT", 12, -106)

  local ePrice = MakeInput(editPanel, editorInputW, 22)
  ePrice:SetPoint("TOPLEFT", editPanel, "TOPLEFT", editorInputX, -106)
  BindTooltip(ePrice, "Price Source", "Click to choose the value source this profile uses, like TSM, Auctionator, or vendor.")

  local eMetricL = MakeLabel(editPanel, "Overview Metric")
  eMetricL:SetPoint("TOPLEFT", 12, -130)

  local eMetric = MakeInput(editPanel, editorInputW, 22)
  eMetric:SetPoint("TOPLEFT", editPanel, "TOPLEFT", editorInputX, -130)
  BindTooltip(eMetric, "Overview Metric", "Click to choose which metric this profile emphasizes in the overview, like GPH or LIV.")

  local bRevert = Widgets:Button(editPanel, "Revert", 90, 22)
  bRevert:SetPoint("BOTTOMRIGHT", -12, 10)

  local bApply = Widgets:Button(editPanel, "Apply", 90, 22)
  bApply:SetPoint("RIGHT", bRevert, "LEFT", -10, 0)

  local divDiff = MakeDivider(detailSC)
  local diffTitle = MakeSectionTitle(detailSC, "Changes vs Active")
  local diffPanel = MakePanel(detailSC, BG)
  diffPanel:SetBackdropColor(C4(BG))

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

  local function ScrollDiffByDelta(delta)
    local step = 32
    local cur = diffScroll:GetVerticalScroll() or 0
    local max = diffScroll:GetVerticalScrollRange() or 0
    if max <= 0 then return false end
    local nextPos = cur - ((delta or 0) * step)
    if nextPos < 0 then nextPos = 0 end
    if nextPos > max then nextPos = max end
    diffScroll:SetVerticalScroll(nextPos)
    return true
  end

  local function UpdateDiffScrollState()
    local max = diffScroll:GetVerticalScrollRange() or 0
    local canScroll = max > 0
    if diffScroll.ScrollBar then
      if canScroll then
        diffScroll.ScrollBar:Show()
      else
        diffScroll.ScrollBar:Hide()
      end
    end
    diffScroll:EnableMouseWheel(canScroll)
    if diffScroll.SetPropagateMouseWheel then
      diffScroll:SetPropagateMouseWheel(not canScroll)
    end
  end

  diffScroll:SetScript("OnMouseWheel", function(_, delta)
    if not ScrollDiffByDelta(delta) then
      ScrollDetailByDelta(delta)
    end
  end)

  diffPanel:EnableMouse(true)
  diffPanel:EnableMouseWheel(true)
  diffPanel:SetScript("OnMouseWheel", function(_, delta)
    if not ScrollDiffByDelta(delta) then
      ScrollDetailByDelta(delta)
    end
  end)

  local diffSC = CreateFrame("Frame", nil, diffScroll)
  diffSC:SetPoint("TOPLEFT", 0, 0)
  diffSC:SetPoint("TOPRIGHT", 0, 0)
  diffSC:SetHeight(1)
  diffSC:SetWidth(1)
  diffSC:EnableMouse(true)
  diffSC:EnableMouseWheel(true)
  diffSC:SetScript("OnMouseWheel", function(_, delta)
    if not ScrollDiffByDelta(delta) then
      ScrollDetailByDelta(delta)
    end
  end)
  diffScroll:SetScrollChild(diffSC)

  local function SyncDiffScrollWidth()
    local w = diffScroll:GetWidth() or 1
    if w < 1 then w = 1 end
    diffSC:SetWidth(w)
  end
  diffScroll:SetScript("OnSizeChanged", SyncDiffScrollWidth)

  local diffLines = {}
  local function SetDiffLines(lines)
    for i = 1, #lines do
      local fs = diffLines[i]
      if not fs then
        fs = MakeBodyText(diffSC, "")
        fs:SetJustifyH("LEFT")
        diffLines[i] = fs
      end
      fs:ClearAllPoints()
      fs:SetPoint("TOPLEFT", 6, -((i - 1) * 16))
      fs:SetPoint("TOPRIGHT", -6, -((i - 1) * 16))
      fs:SetText(lines[i] or "")
      fs:Show()
    end

    for i = #lines + 1, #diffLines do
      if diffLines[i] then diffLines[i]:Hide() end
    end

    diffSC:SetHeight((#lines * 16) + 8)
    if C_Timer and C_Timer.After then
      C_Timer.After(0, UpdateDiffScrollState)
    else
      UpdateDiffScrollState()
    end
  end

  local DIFF_PANEL_HEIGHT = 140

  local function UpdateDetailContentHeight()
    local target = diffPanel and diffPanel:IsShown() and diffPanel or (editPanel and editPanel:IsShown() and editPanel or pvBox)
    if not target or not detailSC then return end

    local pTop = detailSC:GetTop()
    local fBottom = target:GetBottom()
    if not pTop or not fBottom then return end

    local h = math.ceil((pTop - fBottom) + 16)
    if h < 1 then h = 1 end
    detailSC:SetHeight(h)
  end

  local function UpdateDetailContentHeightDeferred()
    local function apply()
      if tab and not tab:IsShown() then return end
      UpdateDetailContentHeight()
      SyncDetailScrollWidth()
      SyncDiffScrollWidth()
      UpdateDetailScrollState()
      UpdateDiffScrollState()
    end

    if C_Timer and C_Timer.After then
      C_Timer.After(0, apply)
    else
      apply()
    end
  end

  local function ReanchorSections()
    local contentW = GetDetailContentWidth()
    LayoutPreviewKV()

    divMid:ClearAllPoints()
    divMid:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, 0)
    divMid:SetPoint("TOPRIGHT", detailSC, "TOPRIGHT", 0, 0)

    pvTitle:ClearAllPoints()
    pvTitle:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, -14)

    pvBox:ClearAllPoints()
    pvBox:SetPoint("TOPLEFT", detailSC, "TOPLEFT", 0, -38)
    pvBox:SetWidth(contentW)

    local anchor = pvBox

    if editorOpen then
      editTitle:Show()
      editPanel:Show()

      editTitle:ClearAllPoints()
      editTitle:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -14)

      editPanel:ClearAllPoints()
      editPanel:SetPoint("TOPLEFT", editTitle, "BOTTOMLEFT", 0, -10)
      editPanel:SetWidth(contentW)

      divDiff:ClearAllPoints()
      divDiff:SetPoint("TOPLEFT", editPanel, "BOTTOMLEFT", 0, -12)
      divDiff:SetWidth(contentW)
    else
      editTitle:Hide()
      editPanel:Hide()

      divDiff:ClearAllPoints()
      divDiff:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -14)
      divDiff:SetWidth(contentW)
    end

    diffTitle:ClearAllPoints()
    diffTitle:SetPoint("TOPLEFT", divDiff, "BOTTOMLEFT", 0, -10)

    diffPanel:ClearAllPoints()
    diffPanel:SetPoint("TOPLEFT", diffTitle, "BOTTOMLEFT", 0, -10)
    diffPanel:SetWidth(contentW)
    diffPanel:SetHeight(DIFF_PANEL_HEIGHT)

    UpdateDetailContentHeightDeferred()
  end

  detailScroll:SetScript("OnSizeChanged", function()
    SyncDetailScrollWidth()
    LayoutPreviewKV()
    ReanchorSections()
    SyncDiffScrollWidth()
  end)

  right:SetScript("OnSizeChanged", function()
    LayoutResponsiveRightTop()
    SyncDetailScrollWidth()
    LayoutPreviewKV()
    ReanchorSections()
    SyncDiffScrollWidth()
  end)

  local function PopulateEditor(pN)
    if not pN then return end
    HydrateCategorySelections(pN)
    eMinValue:SetText(FormatGold(pN.lootMinGold))
    eMinQty:SetText(tostring(pN.lootMinQty))
    eMinQual:SetText(QualityValueToDisplay(pN.lootMinQuality))
    eCatMaterials:SetChecked(pN.lootRecordMaterials)
    eCatGear:SetChecked(pN.lootRecordGear)
    eCatRecipes:SetChecked(pN.lootRecordRecipes)
    eCatPets:SetChecked(pN.lootRecordPets)
    eCatToys:SetChecked(pN.lootRecordToys)
    eCatMounts:SetChecked(pN.lootRecordMounts)
    eCatConsumables:SetChecked(pN.lootRecordConsumables)
    eCatMisc:SetChecked(pN.lootRecordMisc)
    eIgnoreSB:SetChecked(pN.lootIgnoreSoulbound)
    ePrice:SetText(PriceSourceEditorDisplay(pN.priceSource or ""))
    eMetric:SetText(MetricDisplayToken(pN.overviewLast5Metric or ""))
  end

  local function ApplyEditorToProfile(profileTable)
    if type(profileTable) ~= "table" then return end

    profileTable.lootMinGold = ClampInt(ParseMoneyToCopper(eMinValue:GetText()), 0)
    profileTable.lootMinQty = ClampInt(eMinQty:GetText(), 1, 9999)
    profileTable.lootMinQuality = ParseQualityValue(eMinQual:GetText())

    profileTable.lootRecordMaterials = eCatMaterials:GetChecked() and true or false
    profileTable.lootRecordGear = eCatGear:GetChecked() and true or false
    profileTable.lootRecordRecipes = eCatRecipes:GetChecked() and true or false
    profileTable.lootRecordPets = eCatPets:GetChecked() and true or false
    profileTable.lootRecordToys = eCatToys:GetChecked() and true or false
    profileTable.lootRecordMounts = eCatMounts:GetChecked() and true or false
    profileTable.lootRecordConsumables = eCatConsumables:GetChecked() and true or false
    profileTable.lootRecordMisc = eCatMisc:GetChecked() and true or false
    profileTable.lootIgnoreSoulbound = eIgnoreSB:GetChecked() and true or false
    SyncLegacyCategoryFlags(profileTable)

    local rawPS = Trim(ePrice:GetText() or "")
    local prov, extra = SplitProvider(rawPS)

    if prov == "tsm" then
      profileTable.priceSource = "tsm"
      if extra and extra ~= "" then
        profileTable.tsmPriceString = Trim(extra):lower()
      else
        local existing = Trim(profileTable.tsmPriceString or "")
        if existing == "" then
          local cur = (WFDB and WFDB.GetSetting and WFDB:GetSetting("tsmPriceString")) or "dbregionmarketavg"
          profileTable.tsmPriceString = Trim(cur):lower()
        end
      end
    else
      profileTable.priceSource = rawPS
      -- leave tsmPriceString untouched for non-TSM sources
    end

    profileTable.overviewLast5Metric = NormalizeMetricToken(eMetric:GetText() or "")
  end

  -- ---------------------------------------------------------
  -- Action menus (More + row context kept separate)
  -- ---------------------------------------------------------
  local function CreateActionMenu(stateKey)
    if not UI then return nil end
    if UI[stateKey] then return UI[stateKey] end

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

    local box = CreateFrame("Frame", nil, blocker, "BackdropTemplate")
    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.02, 0.02, 0.02, 1.0)
    box:SetBackdropBorderColor(C4(BORDER, BORDER))
    box:SetSize(210, 10)
    box:SetFrameLevel(blocker:GetFrameLevel() + 20)
    box:EnableMouse(true)
    box:SetScript("OnMouseDown", function() end)

    local items = {}

    local function Close()
      blocker:Hide()
      box:Hide()
      box._onPick = nil
    end

    blocker:SetScript("OnMouseDown", function(_, btn)
      if btn then
        Close()
      end
    end)

    local function SetItems(defs)
      local y = -10
      local pad = 8
      local w = 184

      for i = 1, #defs do
        local d = defs[i]
        local b = items[i]
        if not b then
          b = Widgets:Button(box, d.text or "Item", w, 22)
          items[i] = b
        end

        local label = d.text or "Item"
        local enabled = d.enabled and true or false
        local shown = d.shown and true or false
        local tooltipTitle = d.tooltipTitle or label or "Action"
        local tooltipBody = d.tooltipBody
        local onClick = d.onClick

        b:ClearAllPoints()
        b:SetPoint("TOPLEFT", 10, y)
        b:SetText(label)
        b:SetEnabled(enabled)
        SetShown(b, shown)
        BindTooltip(b, tooltipTitle, tooltipBody)

        b:SetScript("OnClick", function()
          Close()
          if type(onClick) == "function" then
            onClick()
          end
        end)

        if shown then
          y = y - (22 + 6)
        end
      end

      for i = #defs + 1, #items do
        if items[i] then
          items[i]:Hide()
        end
      end

      local h = (-y) + pad
      if h < 40 then
        h = 40
      end
      box:SetHeight(h)
    end

    UI[stateKey] = {
      blocker = blocker,
      box = box,
      open = function(anchorBtn, defs)
        if not anchorBtn then return end
        SetItems(defs or {})
        box:ClearAllPoints()
        box:SetPoint("TOPRIGHT", anchorBtn, "BOTTOMRIGHT", 0, -6)
        blocker:Show()
        box:Show()
      end,
      close = Close,
      isOpen = function()
        return blocker:IsShown() and true or false
      end,
    }

    return UI[stateKey]
  end

  local function EnsureMoreMenu()
    return CreateActionMenu("_wfProfilesMoreMenu")
  end

  local function EnsureRowMenu()
    return CreateActionMenu("_wfProfilesRowMenu")
  end

  local function EnsureShareMenu()
    return CreateActionMenu("_wfProfilesShareMenu")
  end

  local HandleRowRightClick

  local function OpenMoreMenu(defs, anchor)
    local menu = EnsureMoreMenu()
    if UI and UI._wfProfilesRowMenu then
      UI._wfProfilesRowMenu.close()
    end
    if UI and UI._wfProfilesShareMenu then
      UI._wfProfilesShareMenu.close()
    end
    if menu then
      menu.open(anchor or bMore, defs)
    end
  end

  local function OpenRowMenu(defs, anchor)
    local menu = EnsureRowMenu()
    if UI and UI._wfProfilesMoreMenu then
      UI._wfProfilesMoreMenu.close()
    end
    if UI and UI._wfProfilesShareMenu then
      UI._wfProfilesShareMenu.close()
    end
    if menu then
      menu.open(anchor or bMore, defs)
    end
  end

  local function OpenShareMenu(defs, anchor)
    local menu = EnsureShareMenu()
    if UI and UI._wfProfilesMoreMenu then
      UI._wfProfilesMoreMenu.close()
    end
    if UI and UI._wfProfilesRowMenu then
      UI._wfProfilesRowMenu.close()
    end
    if menu then
      menu.open(anchor or bMore, defs)
    end
  end

  local function CloseMoreMenu()
    if UI and UI._wfProfilesMoreMenu then
      UI._wfProfilesMoreMenu.close()
    end
    if UI and UI._wfProfilesRowMenu then
      UI._wfProfilesRowMenu.close()
    end
    if UI and UI._wfProfilesShareMenu then
      UI._wfProfilesShareMenu.close()
    end
  end

  -- ---------------------------------------------------------
  -- State + refresh
  -- ---------------------------------------------------------
  local function RefreshRight()
    local active = WFDB:GetActiveProfileName() or "Default"
    if not selectedName or selectedName == "" then selectedName = active end

    local name = selectedName
    local prof = WFDB.GetProfile and WFDB:GetProfile(name) or nil
    local activeProf = WFDB.GetProfile and WFDB:GetProfile(active) or nil

    local displayName = GetProfileDisplayName(ctx, WFDB, name)
    local activeDisplayName = GetProfileDisplayName(ctx, WFDB, active)

    rTitle:SetText("Profile: " .. tostring(displayName))
    do
      local lowerDisplay = tostring(displayName or ""):lower()
      if lowerDisplay == "mats" then
        rTitle:SetTextColor(C4(ACCENT, ACCENT))
      else
        rTitle:SetTextColor(C4(TEXT, TEXT))
      end
    end

    local isActive = (name == active)
    local isPreset = IsDefault(name)
    local isFav = IsFavorite(ctx, WFDB, name)

    rSub:SetText("Active: " .. tostring(activeDisplayName) .. "   •   " .. (isPreset and "Preset" or "User"))

    SetShown(chipActive, isActive)
    SetShown(chipPreset, isPreset)
    SetShown(chipBase, false)
    SetShown(chipFav, isFav)
    LayoutChips()

    bSetActive:SetEnabled(not isActive)
    bSave:SetEnabled(true)
    bDuplicate:SetEnabled(true)
    bEdit:SetEnabled(true)
    bMore:SetEnabled(true)

    local pN = NormalizeProfile(prof)
    if not pN then
      c1.val:SetText("—")
      c2.val:SetText("—")
      c3.val:SetText("—")
      SetDiffLines({ "Select a profile to see differences." })
      return
    end

    c1.val:SetText(FormatGold(pN.lootMinGold))

    local fallbackTSM = (WFDB and WFDB.GetSetting and WFDB:GetSetting("tsmPriceString")) or "dbregionmarketavg"
    c2.val:SetText(PriceSourceDisplayForProfile(pN, fallbackTSM))

    local mRaw  = pN.overviewLast5Metric ~= "" and MetricDisplayToken(pN.overviewLast5Metric) or "—"
    c3.val:SetText(mRaw)

    kv.L.minValueL:SetText("Min value")
    kv.L.minValueV:SetText(FormatGold(pN.lootMinGold))

    kv.L.minQtyL:SetText("Min Quantity")
    kv.L.minQtyV:SetText(tostring(pN.lootMinQty))

    kv.L.minQualL:SetText("Min quality")
    kv.L.minQualV:SetText(QualityValueToDisplay(pN.lootMinQuality))

    kv.L.categoriesL:SetText("Categories")
    kv.L.categoriesV:SetText(CategorySummary(pN, true))

    kv.L.ignoreSbL:SetText("Soulbound or BoP items")
    kv.L.ignoreSbV:SetText(YesNo(pN.lootIgnoreSoulbound))

    kv.R.priceL:SetText("Price Source")
    kv.R.priceV:SetText(c2.val:GetText() or "—")

    kv.R.metricL:SetText("Overview Metric")
    kv.R.metricV:SetText(c3.val:GetText() or "—")

    kv.R.typeL:SetText("Type")
    if isPreset then
      kv.R.typeV:SetText("Preset")
    else
      kv.R.typeV:SetText("User")
    end

    kv.R.statusL:SetText("Status")
    if isActive then
      kv.R.statusV:SetText("Active")
    else
      kv.R.statusV:SetText("Idle")
    end

    if editorOpen then
      PopulateEditor(pN)
    end

    local aN = NormalizeProfile(activeProf)
    SetDiffLines(BuildDiff(aN, pN))
    UpdateDetailContentHeightDeferred()
  end

  local function RefreshList()
    SyncListScrollWidth()

    local names = WFDB:GetProfileNames() or {}
    local active = WFDB:GetActiveProfileName() or "Default"

    if not selectedName or selectedName == "" then selectedName = active end

    local filtered = FilterAndSort(names, filterText)
    SetShown(emptyText, #filtered == 0)

    local fav = GetFavorites(ctx, WFDB)

    local y = 0
    for i = 1, #filtered do
      local n = filtered[i]
      local r = EnsureRow(i)

      r:ClearAllPoints()
      r:SetPoint("TOPLEFT", 0, -y)
      r:SetPoint("TOPRIGHT", 0, -y)
      r:Show()

      r._name = n
      r._selected = (n == selectedName)

      r.name:SetText(GetProfileDisplayName(ctx, WFDB, n))

      local badge = "User"
      if n == active then
        badge = "Active"
      elseif IsDefault(n) then
        badge = "Preset"
      end
      r.badge:SetText(badge)
      r._badgeText = badge

      r.star._name = n
      r.star:Show()
      r.star:EnableMouse(true)
      if fav[n] then
        WF_SetFavoriteStarVisual(r.star.tex, 0.85)
      else
        WF_SetFavoriteStarVisual(r.star.tex, 0.35)
      end

      if r._selected then
        r.bg:SetAlpha(0.16)
      else
        r.bg:SetAlpha(0)
      end

      if not r._wfBound then
        r._wfBound = true

        r:SetScript("OnClick", function(self, button)
          if not self._name then return end
          selectedName = self._name
          CloseMoreMenu()
          RefreshList()
          RefreshRight()

          if button == "RightButton" then
            lastLeftClickName = nil
            lastLeftClickAt = 0
            if HandleRowRightClick then
              HandleRowRightClick(self)
            end
            return
          end

          if button == "LeftButton" then
            local now = (GetTime and GetTime()) or 0
            local isDoubleClick = (lastLeftClickName == self._name) and ((now - (lastLeftClickAt or 0)) <= doubleClickWindow)
            lastLeftClickName = self._name
            lastLeftClickAt = now

            if isDoubleClick and WFDB.ApplyProfile and WFDB:ApplyProfile(self._name) then
              RefreshMainAndSettings(ctx)
              RefreshList()
              RefreshRight()
            end
          end
        end)

        r.star:SetScript("OnClick", function(self)
          if not self._name then return end
          ToggleFavorite(ctx, WFDB, self._name)
          CloseMoreMenu()
          RefreshList()
          RefreshRight()
        end)
      end

      y = y + rowH
    end

    for i = #filtered + 1, #rows do
      if rows[i] then rows[i]:Hide() end
    end

    sc:SetHeight(y + 6)
  end

  local function FullRefresh()
    EnsureProfilesResizeFloor()

    filterText = Trim(search:GetText() or "")
    if not selectedName or selectedName == "" then
      selectedName = WFDB:GetActiveProfileName() or "Default"
    end

    showFavOnly = GetPersist(ctx, WFDB, "profilesFavoritesOnly", false) and true or false
    favOnly:SetChecked(showFavOnly)

    UpdateSearchChrome()
    SyncListScrollWidth()
    SyncDetailScrollWidth()
    SyncDiffScrollWidth()
    ReanchorSections()
    RefreshList()
    RefreshRight()
    UpdateDetailContentHeightDeferred()
  end

  -- ---------------------------------------------------------
  -- Actions
  -- ---------------------------------------------------------
  bSetActive:SetScript("OnClick", function()
    local name = selectedName or (WFDB:GetActiveProfileName() or "Default")
    if WFDB.ApplyProfile and WFDB:ApplyProfile(name) then
      RefreshMainAndSettings(ctx)
      RefreshList()
      RefreshRight()
    end
  end)

  bSave:SetScript("OnClick", function()
    local name = selectedName or (WFDB:GetActiveProfileName() or "Default")
    if WFDB.SaveCurrentToProfile then
      WFDB:SaveCurrentToProfile(name)
      RefreshList()
      RefreshRight()
    end
  end)

  bDuplicate:SetScript("OnClick", function()
    local src = selectedName or (WFDB:GetActiveProfileName() or "Default")
    local srcDisplay = GetProfileDisplayName(ctx, WFDB, src)
    local proposed = tostring(srcDisplay) .. " Copy"

    PromptText(
      ctx,
      "Duplicate Profile",
      "Create a new profile based on \"" .. tostring(srcDisplay) .. "\".",
      proposed,
      "Create",
      function(newName)
        newName = Trim(newName)
        if newName == "" then return end
        if not (WFDB.GetProfile and WFDB.SaveCurrentToProfile and WFDB.ApplyProfile) then return end

        local restore = WFDB:GetActiveProfileName() or "Default"
        WFDB:ApplyProfile(src)
        WFDB:SaveCurrentToProfile(newName)
        WFDB:ApplyProfile(restore)

        selectedName = newName
        RefreshList()
        RefreshRight()
      end
    )
  end)

  bEdit:SetScript("OnClick", function()
    editorOpen = not editorOpen
    if editorOpen then
      bEdit:SetText("Close Editor")
    else
      bEdit:SetText("Edit Rules")
    end
    CloseMoreMenu()
    ReanchorSections()
    RefreshRight()
  end)

  bRevert:SetScript("OnClick", function()
    local name = selectedName or (WFDB:GetActiveProfileName() or "Default")
    local prof = WFDB.GetProfile and WFDB:GetProfile(name) or nil
    local pN = NormalizeProfile(prof)
    if pN then PopulateEditor(pN) end
  end)

  bApply:SetScript("OnClick", function()
    local name = selectedName or (WFDB:GetActiveProfileName() or "Default")
    local prof = WFDB.GetProfile and WFDB:GetProfile(name) or nil
    if type(prof) ~= "table" then return end

    ApplyEditorToProfile(prof)

    local active = WFDB:GetActiveProfileName() or "Default"
    if name == active and WFDB.ApplyProfile then
      WFDB:ApplyProfile(name)
      RefreshMainAndSettings(ctx)
    end

    RefreshList()
    RefreshRight()
  end)

  local function GetSelectedProfileName()
    return selectedName or (WFDB:GetActiveProfileName() or "Default")
  end

  local function doApplyToActive()
    local selected = GetSelectedProfileName()
    local active = WFDB:GetActiveProfileName() or "Default"
    local selectedDisplay = GetProfileDisplayName(ctx, WFDB, selected)
    local activeDisplay = GetProfileDisplayName(ctx, WFDB, active)

    if not (WFDB.GetProfile and WFDB.ApplyProfile) then return end

    local src = WFDB:GetProfile(selected)
    local dst = WFDB:GetProfile(active)
    if type(src) ~= "table" or type(dst) ~= "table" then return end

    local function doApply()
      CopyProfileValues(src, dst)
      WFDB:ApplyProfile(active)
      RefreshMainAndSettings(ctx)
      RefreshList()
      RefreshRight()
      Toast(ctx, "Applied \"" .. tostring(selectedDisplay) .. "\" to active profile (" .. tostring(activeDisplay) .. ").")
    end

    if UI and UI.Confirm then
      UI:Confirm(
        "Apply to Active",
        "This will overwrite your ACTIVE profile settings with \"" .. tostring(selectedDisplay) .. "\".\n\nYour active profile name will stay \"" .. tostring(activeDisplay) .. "\".\n\nContinue?",
        doApply
      )
    else
      doApply()
    end
  end

  local function doNew()
    if not (WFDB.SaveCurrentToProfile and WFDB.ApplyProfile) then return end
    PromptText(
      ctx,
      "New Profile",
      "Create a new profile from your current settings.",
      "",
      "Create",
      function(newName)
        newName = Trim(newName)
        if newName == "" then return end
        WFDB:SaveCurrentToProfile(newName)
        WFDB:ApplyProfile(newName)
        selectedName = newName
        RefreshMainAndSettings(ctx)
        RefreshList()
        RefreshRight()
      end
    )
  end

  local function doRename()
    local old = GetSelectedProfileName()
    if not WFDB.RenameProfile then return end

    PromptText(
      ctx,
      "Rename Profile",
      "Rename \"" .. tostring(GetProfileDisplayName(ctx, WFDB, old)) .. "\" to:",
      old,
      "Rename",
      function(newName)
        newName = Trim(newName)
        if newName == "" then return end
        local ok = WFDB:RenameProfile(old, newName)
        if ok then
          selectedName = newName
          RefreshList()
          RefreshRight()
          RefreshMainAndSettings(ctx)
        end
      end
    )
  end

  local function doDelete()
    local name = GetSelectedProfileName()
    if not WFDB.DeleteProfile then return end

    local function really()
      WFDB:DeleteProfile(name)
      selectedName = WFDB:GetActiveProfileName() or "Default"
      RefreshMainAndSettings(ctx)
      RefreshList()
      RefreshRight()
    end

    if WFDB.GetSetting and WFDB:GetSetting("confirmDestructive") and UI and UI.Confirm then
      UI:Confirm("Delete profile?", "This will permanently delete \"" .. tostring(GetProfileDisplayName(ctx, WFDB, name)) .. "\".", really)
    else
      really()
    end
  end

  local function doResetPreset()
    local name = GetSelectedProfileName()
    if not (WFDB.ResetProfileToDefault and WFDB.IsDefaultProfile and WFDB:IsDefaultProfile(name)) then
      return
    end

    local function really()
      WFDB:ResetProfileToDefault(name)
      WFDB:ApplyProfile(name)
      RefreshMainAndSettings(ctx)
      RefreshList()
      RefreshRight()
    end

    if UI and UI.Confirm then
      UI:Confirm(
        "Reset Preset",
        "Reset \"" .. tostring(name) .. "\" back to the shipped preset?\n\nAny custom edits to this preset will be lost.",
        really
      )
    else
      really()
    end
  end

  local function HandleImportedProfileRaw(raw)
    local parsed, err = DeserializeProfile(raw or "")
    if not parsed then
      Toast(ctx, "Import failed: " .. tostring(err or "Invalid string"))
      return
    end

    local activeName = WFDB:GetActiveProfileName() or "Default"
    local activeProf = WFDB.GetProfile and WFDB:GetProfile(activeName) or nil

    local suggested = FindUniqueName(parsed.name or "Imported")
    local activateDefault = GetPersist(ctx, WFDB, "importActivateDefault", true) and true or false

    local preview = BuildImportPreviewLines(parsed.name or "Imported", parsed.profile, activeName, activeProf)

    ShowImportPreview(ctx, suggested, activateDefault, preview, function(finalName, doActivate)
      finalName = FindUniqueName(finalName)
      SetPersist(ctx, WFDB, "importActivateDefault", doActivate and true or false)

      if not (WFDB.SaveCurrentToProfile and WFDB.GetProfile) then return end

      WFDB:SaveCurrentToProfile(finalName)
      local dest = WFDB:GetProfile(finalName)
      if type(dest) ~= "table" then return end

      CopyProfileValues(parsed.profile, dest)
      selectedName = finalName

      if doActivate and WFDB.ApplyProfile then
        WFDB:ApplyProfile(finalName)
        RefreshMainAndSettings(ctx)
      end

      RefreshList()
      RefreshRight()
      Toast(ctx, "Imported profile \"" .. tostring(finalName) .. "\".")
    end)
  end

  local function doShare()
    local name = GetSelectedProfileName()
    local prof = WFDB.GetProfile and WFDB:GetProfile(name) or nil
    if type(prof) ~= "table" then return end

    local txt = SerializeProfile(GetProfileDisplayName(ctx, WFDB, name), prof)
    ShowShare(ctx, GetProfileDisplayName(ctx, WFDB, name), txt, HandleImportedProfileRaw, "")
  end

  local function doOpenEditor()
    editorOpen = true
    bEdit:SetText("Close Editor")
    CloseMoreMenu()
    ReanchorSections()
    RefreshRight()
  end

  local BuildMoreButtonDefs

  local function BuildRowContextDefs(anchor)
    local selected = GetSelectedProfileName()
    local isActive = (selected == (WFDB:GetActiveProfileName() or "Default"))

    return {
      { text = "Set Active", enabled = not isActive, shown = true, onClick = function() bSetActive:Click() end, tooltipBody = isActive and "This profile is already active." or "Make this the active profile." },
      { text = "Rename", enabled = true, shown = true, onClick = doRename, tooltipBody = "Rename this profile." },
      { text = "Share", enabled = true, shown = true, onClick = doShare, tooltipBody = "Open the combined share and import window." },
      { text = "Duplicate", enabled = true, shown = true, onClick = function() bDuplicate:Click() end, tooltipBody = "Create a copy of this profile." },
      { text = "Delete", enabled = true, shown = true, onClick = doDelete, tooltipBody = "Delete this profile permanently." },
    }
  end


  BuildMoreButtonDefs = function(anchor)
    local selected = GetSelectedProfileName()
    local isPreset = IsDefault(selected)

    return {
      { text = "Apply to Active", enabled = true, shown = true, onClick = doApplyToActive, tooltipBody = "Overwrite your active profile's rules with this profile's settings." },
      { text = "New Profile", enabled = true, shown = true, onClick = doNew, tooltipBody = "Create a new profile from your current live settings." },
      { text = "Rename", enabled = true, shown = true, onClick = doRename, tooltipBody = "Rename this profile." },
      { text = "Share", enabled = true, shown = true, onClick = doShare, tooltipBody = "Open the combined share and import window." },
      { text = "Delete", enabled = true, shown = true, onClick = doDelete, tooltipBody = "Delete this profile permanently." },
      { text = "Reset Preset", enabled = isPreset and true or false, shown = isPreset and true or false, onClick = doResetPreset, tooltipBody = "Restore this preset back to its original shipped rules." },
    }
  end

  HandleRowRightClick = function(row)
    if not row then return end
    OpenRowMenu(BuildRowContextDefs(row), row)
  end

  bMore:SetScript("OnClick", function()
    OpenMoreMenu(BuildMoreButtonDefs(bMore), bMore)
  end)

  -- Search + favorites-only
  search:SetScript("OnTextChanged", function(self)
    filterText = Trim(self:GetText() or "")
    UpdateSearchChrome()
    RefreshList()
  end)

  favOnly:SetScript("OnClick", function(self)
    showFavOnly = self:GetChecked() and true or false
    SetPersist(ctx, WFDB, "profilesFavoritesOnly", showFavOnly)
    RefreshList()
  end)

  -- If your tab system doesn't actually fire OnShow for this page, we still force initial paint.
  tab._wfTutorialLayout = {
    page = tab,
    panel = tab,
    header = header,
    left = left,
    right = right,
    filterPanel = filterPanel,
    search = search,
    favorites = favOnly,
    favoritesText = favOnlyText,
    listPanel = listPanel,
    listHeader = listHeader,
    scroll = scroll,
    content = sc,
  }

  tab:SetScript("OnShow", function()
    EnsureProfilesResizeFloor()
    FullRefresh()
  end)

  tab:SetScript("OnHide", function()
    CloseMoreMenu()
    RestoreDefaultResizeFloor()
  end)

  tab._wf_profiles_refresh = function()
    FullRefresh()
  end

  -- FIX: force an immediate initial refresh now (this is why you were seeing an empty list)
  LayoutResponsiveRightTop()
  FullRefresh()

  -- also re-sync widths next frame so scroll widths are correct even if frames size after layout
  if C_Timer and C_Timer.After then
    C_Timer.After(0, function()
      if tab and tab:IsShown() then
        SyncListScrollWidth()
        SyncDetailScrollWidth()
        SyncDiffScrollWidth()
        ReanchorSections()
        RefreshList()
      end
    end)
  end
end