-- LidPanels_Editor.lua
-- ============================================================

-- NOTE: Load order: Core -> Panels -> Editor -> Init

local LogInfo = _G.LIDPANELS_LogInfo
local LogError = _G.LIDPANELS_LogError
local LogSilent = _G.LIDPANELS_LogSilent
local Compat = _G.LIDPANELS_Compat
local function GetDebugStore()
  return _G.LIDPANELS_Debug
end
if LogInfo then LogInfo("Editor.lua loaded") end

local function ReportEditorRuntimeError(label, err)
  if not LogError then return end
  LogError("[" .. tostring(label or "Editor") .. "] " .. tostring(err))
  if debugstack then
    LogError(debugstack(3, 8, 8))
  end
end

local function RequireApi(name, value)
  if value == nil then
    value = rawget(_G, name)
  end
  if value == nil then
    local api = rawget(_G, "LIDPANELS_API")
    if type(api) == "table" then
      value = api[name]
    end
  end
  if value == nil then
    error("LidPanels missing API: " .. tostring(name))
  end
  return value
end

local INTERNAL_API = (function()
  local api = rawget(_G, "LIDPANELS_API")
  if type(api) ~= "table" or type(api.__internal) ~= "table" then
    error("LidPanels missing internal API registry.")
  end
  return api.__internal
end)()

local function RequireInternalApi(name)
  local value = INTERNAL_API[name]
  if value == nil then
    error("LidPanels missing internal API: " .. tostring(name))
  end
  return value
end

local RootDB = RequireApi("LIDPANELS_RootDB", _G.LIDPANELS_RootDB)
local DB = RequireApi("LIDPANELS_DB", _G.LIDPANELS_DB)
local DeepCopy = RequireApi("LIDPANELS_DeepCopy", _G.LIDPANELS_DeepCopy)
local EnsureDefaults = RequireApi("LIDPANELS_EnsureDefaults", _G.LIDPANELS_EnsureDefaults)
local Print = RequireApi("LIDPANELS_Print", _G.LIDPANELS_Print)
local RebuildAllPanels = RequireApi("LIDPANELS_RebuildAllPanels", _G.LIDPANELS_RebuildAllPanels)
local SetAllUnlocked = RequireApi("LIDPANELS_SetAllUnlocked", _G.LIDPANELS_SetAllUnlocked)
local NudgeSelectedPanel = RequireApi("LIDPANELS_NudgeSelectedPanel", _G.LIDPANELS_NudgeSelectedPanel)
local EnsureGradientTables = RequireApi("LIDPANELS_EnsureGradientTables", _G.LIDPANELS_EnsureGradientTables)
local CreateEditorWindowStateBridge = RequireInternalApi("LIDPANELS_CreateEditorWindowStateBridge")
local SetupEditorFrameShell = RequireInternalApi("LIDPANELS_SetupEditorFrameShell")
local BindEditorTheme = RequireInternalApi("LIDPANELS_BindEditorTheme")
local frames = _G.LIDPANELS_PanelFrames or {}
_G.LIDPANELS_PanelFrames = frames

local EditorWindowState = CreateEditorWindowStateBridge({
  RootDB = RootDB,
})
local ApplyEditorState = EditorWindowState.ApplyEditorState
local GetEditorScale = EditorWindowState.GetEditorScale
local ApplyEditorScale = EditorWindowState.ApplyEditorScale
local SetDropdownSelection = EditorWindowState.SetDropdownSelection
local ApplyDropdownLimits = EditorWindowState.ApplyDropdownLimits
local SaveEditorState = EditorWindowState.SaveEditorState

-- Editor runtime state stays file-local; cross-file access goes through LIDPANELS_* APIs.
local editor
local selectedPanelName
local selectedBackgroundName
local selectedBackgroundCategory
local selectedBackgroundCategoryGroup

local function GetPanelsUnlockedState()
  local unlocked = _G.LIDPANELS_PanelsUnlocked
  if unlocked == nil then
    return true
  end
  return unlocked
end

local function OpenColorPickerRGBOnly(initialRGB, onChangedRGB, initialAlpha)
  local r, g, b = initialRGB[1] or 1, initialRGB[2] or 1, initialRGB[3] or 1
  local hasOpacity = initialAlpha ~= nil
  local a = initialAlpha

  local function ApplyNow()
    local nr, ng, nb = ColorPickerFrame:GetColorRGB()
    local na = nil
    if hasOpacity and ColorPickerFrame.GetColorAlpha then
      na = ColorPickerFrame:GetColorAlpha()
    end
    onChangedRGB(nr, ng, nb, na)
  end

  local function ApplyPrevious(prev)
    local pr, pg, pb, pa = r, g, b, a
    if type(prev) == "table" then
      pr = prev.r or prev[1] or pr
      pg = prev.g or prev[2] or pg
      pb = prev.b or prev[3] or pb
      if hasOpacity then
        pa = prev.opacity
        if pa == nil then pa = prev.a end
        if pa == nil then pa = prev[4] end
        if pa == nil then pa = a end
      end
    elseif hasOpacity and ColorPickerFrame.GetPreviousValues then
      local vr, vg, vb, va = ColorPickerFrame:GetPreviousValues()
      pr = vr or pr
      pg = vg or pg
      pb = vb or pb
      pa = va
      if pa == nil then pa = a end
    end
    onChangedRGB(pr, pg, pb, pa)
  end

  if ColorPickerFrame and ColorPickerFrame.SetupColorPickerAndShow then
    local info = {
      r = r, g = g, b = b,
      swatchFunc = ApplyNow,
      colorFunc  = ApplyNow, -- LIVE while dragging (when supported)
      cancelFunc = function(prev)
        ApplyPrevious(prev)
      end,
    }
    if hasOpacity then
      info.hasOpacity = true
      info.opacity = a
      info.opacityFunc = ApplyNow
    else
      info.hasOpacity = false
    end
    ColorPickerFrame:SetupColorPickerAndShow(info)
    return
  end

  local cp = ColorPickerFrame
  cp.func = function() ApplyNow() end
  cp.swatchFunc = function() ApplyNow() end
  cp.cancelFunc = function() ApplyPrevious() end
  if cp.opacityFunc ~= nil or hasOpacity then
    cp.opacityFunc = function() ApplyNow() end
  end
  if cp.SetHasOpacity then cp:SetHasOpacity(hasOpacity) end
  cp.hasOpacity = hasOpacity
  cp.opacity = a
  if cp.SetColorRGB then cp:SetColorRGB(r, g, b) end
  cp:Show()
end

-- ============================================================
-- ##--------------------------EDITOR UI HELPERS----------------
-- ============================================================

local MAX_PANEL_NAME = 25
local MAX_PROFILE_NAME = 25
local MAX_PROFILE_GROUP_NAME = 25
local MAX_BACKGROUND_NAME = 25
local MAX_BG_CATEGORY_NAME = 25
local MAX_BG_CATEGORY_GROUP_NAME = 25
local LISTS_PER_PAGE = 18

local ADDON_VERSION = "1.0.2"
local WOW_CLIENT_VERSION = "12.0.5 (Build 67451)"

local DEFAULT_SKIN_HEADER = "Interface\\AddOns\\LidPanels\\Media\\Lidpanels\\Logos\\LidPanels_HeaderBackground.tga"

local function ApplySkinPreset()
  local ui = RootDB().ui
  ui.skin = "LP"
  ui.headerBg = nil
  ui.__skinPreset = "LP"
end

local function ApplyFrameSkin(frame)
  frame:SetBackdrop({
    bgFile = "Interface/Tooltips/UI-Tooltip-Background",
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true, tileSize = 16, edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 }
  })
  frame:SetBackdropColor(0.03, 0.03, 0.04, 0.94)
end

local function ApplyEditorLogo()
  if not editor or not editor.logo then return end
  editor.logo:SetTexture("Interface\\AddOns\\LidPanels\\Media\\Lidpanels\\Logos\\LidPanels_HeaderLogo.tga")
end

local function ApplyHeaderBackground()
  if not editor or not editor.headerBg then return end
  local tex = editor.headerBg
  if tex.SetAtlas then tex:SetAtlas(nil) end
  tex:SetTexture(nil)
  if tex.SetDesaturated then tex:SetDesaturated(false) end
  tex:SetVertexColor(1, 1, 1, 1)
  tex:SetAlpha(1)
  tex:SetBlendMode("BLEND")
  tex:SetTexture(DEFAULT_SKIN_HEADER)
  tex:SetTexCoord(0, 1, 0, 1)
  tex:Show()
  if Compat and Compat.After then
    Compat.After(0.05, function()
      if editor and editor.headerBg == tex then
        if tex.SetDesaturated then tex:SetDesaturated(false) end
        tex:SetVertexColor(1, 1, 1, 1)
        tex:SetAlpha(1)
        tex:SetBlendMode("BLEND")
        tex:SetTexCoord(0, 1, 0, 1)
      end
    end)
  end
end

local TALENT_TEXCOORD = {
  -- The source sheets include padded margins, so we crop to the active area.
  -- TOP/BOTTOM then split inside that cropped area.
  FULL = { 0.0, 0.77, 0.0, 0.75 },
  TOP = { 0.0, 0.77, 0.0, 0.38 },
  BOTTOM = { 0.0, 0.77, 0.38, 0.75 },
}

local TALENT_BACKGROUND_DEFS = {
  { text = "Death Knight - Blood", fileID = 4631290, split = "TOP" },
  { text = "Death Knight - Frost", fileID = 4631290, split = "BOTTOM" },
  { text = "Death Knight - Unholy", fileID = 4631293, split = "FULL" },
  { text = "Demon Hunter - Havoc", fileID = 4631296, split = "TOP" },
  { text = "Demon Hunter - Vengeance", fileID = 4631296, split = "BOTTOM" },
  { text = "Demon Hunter - Devourer", fileID = 7323575, split = "FULL" },
  { text = "Druid - Balance", fileID = 4631299, split = "TOP" },
  { text = "Druid - Feral", fileID = 4631299, split = "BOTTOM" },
  { text = "Druid - Guardian", fileID = 4631304, split = "TOP" },
  { text = "Druid - Restoration", fileID = 4631304, split = "BOTTOM" },
  { text = "Evoker - Devastation", fileID = 4631307, split = "TOP" },
  { text = "Evoker - Preservation", fileID = 4631307, split = "BOTTOM" },
  { text = "Evoker - Augmentation", fileID = 5179179, split = "FULL" },
  { text = "Hunter - Beast Mastery", fileID = 4631310, split = "TOP" },
  { text = "Hunter - Marksmanship", fileID = 4631310, split = "BOTTOM" },
  { text = "Hunter - Survival", fileID = 4631313, split = "FULL" },
  { text = "Mage - Arcane", fileID = 4631318, split = "TOP" },
  { text = "Mage - Fire", fileID = 4631318, split = "BOTTOM" },
  { text = "Mage - Frost", fileID = 4631321, split = "FULL" },
  { text = "Monk - Brewmaster", fileID = 4631324, split = "TOP" },
  { text = "Monk - Mistweaver", fileID = 4631324, split = "BOTTOM" },
  { text = "Monk - Windwalker", fileID = 4631329, split = "FULL" },
  { text = "Paladin - Holy", fileID = 4631337, split = "TOP" },
  { text = "Paladin - Protection", fileID = 4631337, split = "BOTTOM" },
  { text = "Paladin - Retribution", fileID = 4631340, split = "FULL" },
  { text = "Priest - Discipline", fileID = 4631348, split = "TOP" },
  { text = "Priest - Holy", fileID = 4631348, split = "BOTTOM" },
  { text = "Priest - Shadow", fileID = 4631371, split = "FULL" },
  { text = "Rogue - Assassination", fileID = 4631374, split = "TOP" },
  { text = "Rogue - Outlaw", fileID = 4631374, split = "BOTTOM" },
  { text = "Rogue - Subtlety", fileID = 4631377, split = "FULL" },
  { text = "Shaman - Elemental", fileID = 4631381, split = "TOP" },
  { text = "Shaman - Enhancement", fileID = 4631381, split = "BOTTOM" },
  { text = "Shaman - Restoration", fileID = 4631383, split = "FULL" },
  { text = "Warlock - Affliction", fileID = 4631386, split = "TOP" },
  { text = "Warlock - Demonology", fileID = 4631386, split = "BOTTOM" },
  { text = "Warlock - Destruction", fileID = 4631389, split = "FULL" },
  { text = "Warrior - Arms", fileID = 4631392, split = "TOP" },
  { text = "Warrior - Fury", fileID = 4631392, split = "BOTTOM" },
  { text = "Warrior - Protection", fileID = 4631395, split = "FULL" },
}

local function BuildBackgroundOptions()
  local opts = {}
  for _, def in ipairs(TALENT_BACKGROUND_DEFS) do
    local split = TALENT_TEXCOORD[def.split] or TALENT_TEXCOORD.FULL
    opts[#opts + 1] = {
      text = def.text,
      value = string.format("TALENT:%d:%s", def.fileID, def.split),
      bg = def.fileID,
      texCoord = { split[1], split[2], split[3], split[4] },
    }
  end
  table.sort(opts, function(a, b) return a.text < b.text end)
  return opts
end

local function BuildBackgroundList(includeNone)
  local list = {}
  if includeNone then
    list[#list + 1] = { text = "Color (no image)", value = "NONE" }
  end
  for _, opt in ipairs(BuildBackgroundOptions()) do
    list[#list + 1] = opt
  end
  return list
end

local function MakeYellowSubHeader(parent, text, x, y)
  local h = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  h:SetPoint("TOPLEFT", x or 0, y or 0)
  h:SetText(text)
  h:SetTextColor(1.0, 0.82, 0.0, 1.0)
  return h
end

-- Logo sizing (kept consistent across header + tutorial).
local LOGO_SRC_W = 2257
local LOGO_SRC_H = 1794
local LOGO_TARGET_W = 150
local LOGO_TARGET_H = math.floor((LOGO_TARGET_W * LOGO_SRC_H / LOGO_SRC_W) + 0.5)

-- Tutorial/Welcome card sizing (kept constant, layout adapts by columns).
local CARD_W = 320
local CARD_H = 240

local function CreateScrollEditBox(parent, width, height)
  local bg = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  bg:SetSize(width, height)
  bg:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  bg:SetBackdropColor(0.05, 0.05, 0.07, 0.85)
  bg:SetBackdropBorderColor(0, 0, 0, 0.9)

  local scroll = CreateFrame("ScrollFrame", nil, bg, "UIPanelScrollFrameTemplate")
  scroll:SetPoint("TOPLEFT", 6, -6)
  scroll:SetPoint("BOTTOMRIGHT", -28, 6)

  local edit = CreateFrame("EditBox", nil, scroll)
  edit:SetMultiLine(true)
  edit:SetAutoFocus(false)
  edit:SetFontObject(ChatFontNormal)
  edit:SetWidth(width - 40)
  edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
  edit:SetScript("OnTextChanged", function()
    scroll:UpdateScrollChildRect()
  end)

  scroll:SetScrollChild(edit)
  bg.editBox = edit
  bg.scrollFrame = scroll
  return bg, edit
end

local sliderId = 0
local function MakeSlider(parent, label, x, y, minVal, maxVal, step, onValue)
  -- Slider with a live numeric edit box (right side).
  -- NOTE: We guard programmatic SetValue calls via s.__silent to avoid triggering callbacks.
  sliderId = sliderId + 1
  local s = CreateFrame("Slider", "LIDPANELS_Slider" .. sliderId, parent, "OptionsSliderTemplate")
  s:SetPoint("TOPLEFT", x, y)
  s:SetWidth(260)
  s:SetMinMaxValues(minVal, maxVal)
  s:SetValueStep(step or 1)
  s:SetObeyStepOnDrag(true)
  s:EnableMouse(true)

  -- Ensure unique-named regions exist; OptionsSliderTemplate creates Low/High/Text with global names.
  local baseName = s:GetName()
  if baseName then
    _G[baseName .. "Low"]:SetText(tostring(minVal))
    _G[baseName .. "High"]:SetText(tostring(maxVal))
    _G[baseName .. "Text"]:SetText(tostring(label or ""))
  end

  -- Thumb: make it visible and easy to grab
  if s.SetThumbTexture then
    s:SetThumbTexture("Interface/Buttons/WHITE8x8")
    local thumb = s.GetThumbTexture and s:GetThumbTexture()
    if thumb then
      thumb:SetSize(10, 18)
      thumb:SetVertexColor(0.9, 0.9, 0.9, 0.9)
    end
  end

  -- Right-side value edit box
  local eb = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
  eb:SetSize(58, 20)
  eb:SetPoint("LEFT", s, "RIGHT", 10, 0)
  eb:SetAutoFocus(false)
  if minVal >= 0 then
    eb:SetNumeric(true)
  end
  eb:SetJustifyH("RIGHT")
  s.valueBox = eb

  local function clamp(v)
    if v < minVal then return minVal end
    if v > maxVal then return maxVal end
    return v
  end

  local function round(v)
    if v >= 0 then return math.floor(v + 0.5) end
    return math.ceil(v - 0.5)
  end

  local function setBox(v)
    eb:SetText(tostring(round(v or 0)))
  end

  s.SetValueSilent = function(self, v)
    self.__silent = true
    self:SetValue(clamp(v))
    self.__silent = false
  end

  eb:SetScript("OnEnterPressed", function(self)
    self:ClearFocus()
    local v = tonumber(self:GetText())
    if not v then return end
    v = clamp(v)
    s:SetValue(v) -- will trigger OnValueChanged
  end)
  eb:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

  s:SetScript("OnValueChanged", function(self, val)
    setBox(val)

    -- Ignore programmatic refresh or silent updates
    if self.__silent then return end
    if editor and editor.__refreshing then return end

    if onValue then onValue(val) end
  end)

  -- initialize box
  setBox(minVal)

  return s
end

local SelectPanel

local function AttachListGlow(button)
  if not button then return nil end
  local glow = button:CreateTexture(nil, "BACKGROUND")
  glow:SetAllPoints(true)
  glow:SetTexture("Interface/Buttons/UI-Listbox-Highlight2")
  glow:SetBlendMode("ADD")
  glow:Hide()
  button.__glow = glow
  return glow
end

local function CreatePurpleOutline(parent)
  if not parent then return nil end
  local border = CreateFrame("Frame", nil, parent, "BackdropTemplate")
  border:SetPoint("TOPLEFT", -2, 2)
  border:SetPoint("BOTTOMRIGHT", 2, -2)
  border:SetBackdrop({ edgeFile = "Interface/Buttons/WHITE8x8", edgeSize = 1 })
  border:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.95)
  border:Hide()
  return border
end

local function MakeBaseListButton(parent, text, y, onClick)
  local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
  b:SetSize(200, 20)
  b:SetPoint("TOPLEFT", 0, y)
  b:SetText(text)
  b:SetScript("OnClick", onClick)
  AttachListGlow(b)
  return b
end

local function MakeListButton(parent, text, y, onClick)
  local b = MakeBaseListButton(parent, text, y, onClick)
  local icon = b:CreateTexture(nil, "ARTWORK")
  icon:SetSize(18, 18)
  icon:SetPoint("RIGHT", -6, 0)
  icon:Hide()
  b.__lockIcon = icon
  b.__selectedBorder = CreatePurpleOutline(b)
  return b
end

local function MakeSimpleListButton(parent, text, y, onClick)
  return MakeBaseListButton(parent, text, y, onClick)
end

local function ConfigureProfileGroupButton(button, label, collapsed)
  if not button then return end

  local fs = button:GetFontString()
  if fs then
    fs:ClearAllPoints()
    fs:SetPoint("LEFT", 22, 0)
    fs:SetPoint("RIGHT", -8, 0)
    fs:SetJustifyH("LEFT")
  end

  if not button.__groupArrowButton then
    local arrowButton = CreateFrame("Button", nil, button)
    arrowButton:SetPoint("TOPLEFT", 2, -1)
    arrowButton:SetPoint("BOTTOMLEFT", 2, 1)
    arrowButton:SetWidth(18)
    arrowButton:RegisterForClicks("LeftButtonUp")
    arrowButton:SetScript("OnClick", function()
      if button.__onToggleProfileGroup then
        button.__onToggleProfileGroup()
      end
    end)
    button.__groupArrowButton = arrowButton

    local arrow = arrowButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    arrow:SetPoint("CENTER", 0, 0)
    arrow:SetJustifyH("CENTER")
    button.__groupArrow = arrow
  end

  button.__groupArrow:SetText(collapsed and ">" or "v")
  button:SetText(label or "")
end

local function ConfigureBackgroundCategoryGroupButton(button, label, collapsed)
  if not button then return end

  local fs = button:GetFontString()
  if fs then
    fs:ClearAllPoints()
    fs:SetPoint("LEFT", 22, 0)
    fs:SetPoint("RIGHT", -8, 0)
    fs:SetJustifyH("LEFT")
  end

  if not button.__bgCategoryGroupArrowButton then
    local arrowButton = CreateFrame("Button", nil, button)
    arrowButton:SetPoint("TOPLEFT", 2, -1)
    arrowButton:SetPoint("BOTTOMLEFT", 2, 1)
    arrowButton:SetWidth(18)
    arrowButton:RegisterForClicks("LeftButtonUp")
    arrowButton:SetScript("OnClick", function()
      if button.__onToggleBackgroundCategoryGroup then
        button.__onToggleBackgroundCategoryGroup()
      end
    end)
    button.__bgCategoryGroupArrowButton = arrowButton

    local arrow = arrowButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    arrow:SetPoint("CENTER", 0, 0)
    arrow:SetJustifyH("CENTER")
    button.__bgCategoryGroupArrow = arrow
  end

  button.__bgCategoryGroupArrow:SetText(collapsed and ">" or "v")
  button:SetText(label or "")
end

local function MakeProfileButton(parent, text, y, onClick)
  local b = MakeBaseListButton(parent, text, y, onClick)
  local icon = b:CreateTexture(nil, "ARTWORK")
  icon:SetSize(14, 14)
  icon:SetPoint("RIGHT", -4, 0)
  icon:SetTexture("Interface/Buttons/UI-CheckBox-Check")
  icon:Hide()
  b.__activeIcon = icon
  b.__activeBorder = CreatePurpleOutline(b)
  return b
end

local function ClampListPage(page, totalPages)
  totalPages = math.max(1, tonumber(totalPages) or 1)
  page = tonumber(page) or 1
  if page < 1 then page = 1 end
  if page > totalPages then page = totalPages end
  return page
end

local function GetListPageForValue(list, value, pageSize, getValue)
  if value == nil then return nil end
  pageSize = math.max(1, tonumber(pageSize) or LISTS_PER_PAGE)
  getValue = getValue or function(item) return item end
  for i, item in ipairs(list or {}) do
    if getValue(item) == value then
      return math.floor((i - 1) / pageSize) + 1
    end
  end
  return nil
end

local function UpdatePagerControls(prevBtn, nextBtn, pageText, page, totalPages)
  local show = (tonumber(totalPages) or 1) > 1
  if prevBtn then
    prevBtn:SetShown(show)
    prevBtn:SetEnabled(show and page > 1)
  end
  if nextBtn then
    nextBtn:SetShown(show)
    nextBtn:SetEnabled(show and page < totalPages)
  end
  if pageText then
    pageText:SetShown(show)
    if show then
      pageText:SetText(string.format("Page %d / %d", page, totalPages))
    else
      pageText:SetText("")
    end
  end
end

local RenameProfile
local CopyProfileFrom
local DeleteProfile
local CreateProfileNamed
local CreateProfileGroupNamed
local RenameProfileGroup
local DeleteProfileGroup
local ToggleProfileGroupCollapsed
local RenameSelectedPanel
local CopySelectedPanel
local DeleteSelectedPanel
local CreateNewPanelNamed
local TrimName
local ClampNameLen
local NormalizePanelName
local CreateBackgroundNamed
local RenameBackground
local DeleteBackground
local CreateBackgroundCategoryNamed
local DeleteBackgroundCategory
local RenameBackgroundCategory
local CreateBackgroundCategoryGroupNamed
local RenameBackgroundCategoryGroup
local DeleteBackgroundCategoryGroup
local CopySelectedPanelToProfile

local function GetPopupEditBox(popup)
  if not popup then return nil end
  if popup.editBox and popup.editBox.GetText then
    return popup.editBox
  end
  if popup.EditBox and popup.EditBox.GetText then
    return popup.EditBox
  end
  if popup.GetName then
    local eb = _G[popup:GetName() .. "EditBox"]
    if eb and eb.GetText then
      return eb
    end
  end
  return nil
end

local function GetPopupText(popup)
  local editBox = GetPopupEditBox(popup)
  if editBox and editBox.GetText then
    return editBox:GetText() or ""
  end
  return ""
end

local function ConfirmPopupPrimary(popup)
  if not popup then return end
  if StaticPopup_OnClick then
    StaticPopup_OnClick(popup, 1)
    return
  end
  local button = popup.button1 or popup.Button1 or (popup.Buttons and popup.Buttons[1])
  if button and button.Click then
    button:Click()
    return
  end
  local info = popup.which and StaticPopupDialogs and StaticPopupDialogs[popup.which]
  if info and info.OnAccept then
    info.OnAccept(popup, popup.data, popup.data2)
  end
end

local function PopupEditBoxConfirm(editBox)
  local popup = editBox and editBox.GetParent and editBox:GetParent()
  if not popup then return end
  ConfirmPopupPrimary(popup)
end

local function HookPopupEnterAccept(popup)
  if not popup then return end
  local editBox = GetPopupEditBox(popup)
  if editBox and editBox.SetScript then
    editBox:SetScript("OnEnterPressed", function()
      PopupEditBoxConfirm(editBox)
    end)
  end
  if not popup.__lidEnterHooked then
    popup.__lidEnterHooked = true
    popup:EnableKeyboard(true)
    popup:HookScript("OnKeyDown", function(frame, key)
      if key == "ENTER" or key == "NUMPADENTER" then
        ConfirmPopupPrimary(frame)
      end
    end)
  end
end

local function HookPopupMaxLetters(popup, maxChars)
  local editBox = GetPopupEditBox(popup)
  if not editBox or not maxChars then return end
  editBox:SetMaxLetters(maxChars)
  editBox:SetScript("OnTextChanged", function(self)
    local txt = self:GetText() or ""
    if #txt > maxChars then
      self:SetText(txt:sub(1, maxChars))
      self:SetCursorPosition(maxChars)
    end
  end)
end

local function GetCopyPanelTargetProfiles()
  local root = RootDB()
  local currentProfile = tostring(root.currentProfile or "")
  local baseNames = ListProfiles()
  if GetCopyPanelTargetProfiles.__root == root
    and GetCopyPanelTargetProfiles.__currentProfile == currentProfile
    and GetCopyPanelTargetProfiles.__baseNames == baseNames
    and GetCopyPanelTargetProfiles.__cache
  then
    return GetCopyPanelTargetProfiles.__cache
  end
  local names = {}

  if currentProfile ~= "" and currentProfile ~= "Default" then
    for _, name in ipairs(baseNames) do
      if name == currentProfile then
        names[#names + 1] = name
        break
      end
    end
  end

  for _, name in ipairs(baseNames) do
    if name ~= currentProfile then
      names[#names + 1] = name
    end
  end

  if #names == 0 and currentProfile ~= "" then
    names[1] = currentProfile
  end

  GetCopyPanelTargetProfiles.__root = root
  GetCopyPanelTargetProfiles.__currentProfile = currentProfile
  GetCopyPanelTargetProfiles.__baseNames = baseNames
  GetCopyPanelTargetProfiles.__cache = names
  return names
end

local COPY_PANEL_PROFILE_PAGE_SIZE = 12

local function ClampCopyPanelProfilePage(page, totalPages)
  totalPages = math.max(1, tonumber(totalPages) or 1)
  page = tonumber(page) or 1
  if page < 1 then page = 1 end
  if page > totalPages then page = totalPages end
  return page
end

local function GetCopyPanelProfileSelectedPage(profiles, selectedProfile)
  for index, profileName in ipairs(profiles or {}) do
    if profileName == selectedProfile then
      return math.floor((index - 1) / COPY_PANEL_PROFILE_PAGE_SIZE) + 1
    end
  end
  return 1
end

local function BuildCopyPanelProfilePagedMenu(popup, dropdown)
  local profiles = popup and popup.__lidCopyPanelProfiles or {}
  local selectedProfile = popup and popup.__lidCopyProfileSelected or nil
  local totalPages = math.max(1, math.ceil(#profiles / COPY_PANEL_PROFILE_PAGE_SIZE))
  local page = ClampCopyPanelProfilePage(popup and popup.__lidCopyProfilePage or 1, totalPages)
  local firstIndex = ((page - 1) * COPY_PANEL_PROFILE_PAGE_SIZE) + 1
  local lastIndex = math.min(#profiles, firstIndex + COPY_PANEL_PROFILE_PAGE_SIZE - 1)
  local entries = {}

  for index = firstIndex, lastIndex do
    local profileName = profiles[index]
    if profileName then
      entries[#entries + 1] = {
        text = profileName,
        value = profileName,
        checked = (profileName == selectedProfile),
        func = function()
          popup.__lidCopyProfileSelected = profileName
          popup.__lidCopyProfilePage = GetCopyPanelProfileSelectedPage(profiles, profileName)
          if dropdown.__lidpanelsCustomDropdown then
            dropdown:SetSelectedValue(profileName)
            dropdown:SetText(profileName)
          else
            UIDropDownMenu_SetSelectedValue(dropdown, profileName)
            UIDropDownMenu_SetText(dropdown, profileName)
          end
        end,
      }
    end
  end

  if totalPages > 1 then
    entries[#entries + 1] = {
      text = string.format("Page %d/%d", page, totalPages),
      isTitle = true,
      notCheckable = true,
    }
    entries[#entries + 1] = {
      text = "< Prev",
      notCheckable = true,
      disabled = (page <= 1),
      keepShownOnClick = true,
      func = function()
        popup.__lidCopyProfilePage = ClampCopyPanelProfilePage(page - 1, totalPages)
      end,
    }
    entries[#entries + 1] = {
      text = "Next >",
      notCheckable = true,
      disabled = (page >= totalPages),
      keepShownOnClick = true,
      func = function()
        popup.__lidCopyProfilePage = ClampCopyPanelProfilePage(page + 1, totalPages)
      end,
    }
  end

  local function OnMouseWheel(delta)
    if totalPages <= 1 then
      return false
    end
    local targetPage = ClampCopyPanelProfilePage(page - delta, totalPages)
    if targetPage == page then
      return false
    end
    popup.__lidCopyProfilePage = targetPage
    return true
  end

  return {
    entries = entries,
    width = 180,
    onMouseWheel = OnMouseWheel,
  }
end

local function EnsureCopyPanelProfileControls(popup)
  if not popup then return nil, nil end

  if not popup.__lidCopyProfileLabel then
    local label = popup:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    label:SetText("Target Profile:")
    popup.__lidCopyProfileLabel = label
  end

  if not popup.__lidCopyProfileDrop then
    local createCustomDropdown = rawget(_G, "LIDPANELS_CreateCustomDropdownControl")
    local dropdown
    if type(createCustomDropdown) == "function" then
      dropdown = createCustomDropdown(editor, popup, {
        width = 180,
        buttonHeight = 24,
        text = "(select profile)",
      })
    else
      dropdown = CreateFrame("Frame", nil, popup, "UIDropDownMenuTemplate")
      UIDropDownMenu_SetWidth(dropdown, 180)
    end
    popup.__lidCopyProfileDrop = dropdown
  end

  return popup.__lidCopyProfileLabel, popup.__lidCopyProfileDrop
end

local function HideCopyPanelProfileControls(popup)
  if not popup then return end
  popup.__lidCopyPanelLayoutUntil = nil
  if popup.__lidCopyProfileLabel then popup.__lidCopyProfileLabel:Hide() end
  if popup.__lidCopyProfileDrop then
    if popup.__lidCopyProfileDrop.__lidpanelsCustomDropdown and popup.__lidCopyProfileDrop.CloseMenu then
      popup.__lidCopyProfileDrop:CloseMenu()
    end
    popup.__lidCopyProfileDrop:Hide()
  end
end

local function LayoutCopyPanelPopup(popup)
  if not popup or not popup:IsShown() then return end

  local editBox = GetPopupEditBox(popup)
  local label = popup.__lidCopyProfileLabel
  local dropdown = popup.__lidCopyProfileDrop
  local popupName = popup.GetName and popup:GetName() or nil
  local button1 = popup.button1 or (popupName and _G[popupName .. "Button1"]) or nil
  local button2 = popup.button2 or (popupName and _G[popupName .. "Button2"]) or nil
  if not label or not dropdown then return end

  label:ClearAllPoints()
  if editBox then
    label:SetPoint("TOP", editBox, "BOTTOM", 0, -12)
  else
    label:SetPoint("TOP", popup.text, "BOTTOM", 0, -44)
  end
  label:Show()

  dropdown:ClearAllPoints()
  if dropdown.__lidpanelsCustomDropdown then
    dropdown:SetPoint("TOP", label, "BOTTOM", 0, -6)
  else
    dropdown:SetPoint("TOP", label, "BOTTOM", 0, -2)
  end
  dropdown:Show()

  local buttonHeight = math.max(
    (button1 and button1.GetHeight and button1:GetHeight()) or 22,
    (button2 and button2.GetHeight and button2:GetHeight()) or 22
  )
  local buttonGapY = 32
  local bottomPadding = 14
  local extraPadding = 4
  local desiredHeight = 188
  local popupTop = popup.GetTop and popup:GetTop() or nil
  local dropdownBottom = dropdown.GetBottom and dropdown:GetBottom() or nil
  if popupTop and dropdownBottom then
    desiredHeight = math.ceil((popupTop - dropdownBottom) + buttonGapY + buttonHeight + bottomPadding + extraPadding)
    if desiredHeight < 188 then
      desiredHeight = 188
    end
  end

  if button1 and button2 then
    button1:ClearAllPoints()
    button1:SetPoint("TOPRIGHT", dropdown, "BOTTOM", -8, -buttonGapY)
    button2:ClearAllPoints()
    button2:SetPoint("TOPLEFT", dropdown, "BOTTOM", 8, -buttonGapY)
  end
  popup:SetHeight(desiredHeight)
end

local function EnsureCopyPanelPopupLayoutHook(popup)
  if not popup or popup.__lidCopyPanelLayoutHooked or type(popup.HookScript) ~= "function" then
    return
  end
  popup.__lidCopyPanelLayoutHooked = true
  popup:HookScript("OnUpdate", function(self)
    local untilTime = self.__lidCopyPanelLayoutUntil
    if not untilTime then
      return
    end
    local now = (GetTime and GetTime()) or 0
    if now > untilTime then
      self.__lidCopyPanelLayoutUntil = nil
      return
    end
    LayoutCopyPanelPopup(self)
  end)
end

local function RequestCopyPanelPopupLayout(popup, duration)
  if not popup then return end
  EnsureCopyPanelPopupLayoutHook(popup)
  local now = (GetTime and GetTime()) or 0
  popup.__lidCopyPanelLayoutUntil = now + (tonumber(duration) or 0.4)
  LayoutCopyPanelPopup(popup)
end

StaticPopupDialogs["LIDPANELS_RENAME_PANEL"] = {
  text = "Rename panel:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_PANEL_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PANEL_NAME)
      editBox:SetText(selectedPanelName or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    if not selectedPanelName then return end
    local txt = GetPopupText(self)
    RenameSelectedPanel(txt)
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RESET_MINIMAP"] = {
  text = "Are you sure you want to reset the Minimap Icon?",
  button1 = "Yes",
  button2 = "Cancel",
  OnAccept = function()
    local root = RootDB()
    root.ui = root.ui or {}
    root.ui.minimap = root.ui.minimap or {}

    local defaultSize = 28
    if root.ui.editorScalePreset == "4K" then defaultSize = 43 end
    root.ui.minimap.size = defaultSize
    root.ui.minimap.angle = 45
    root.ui.minimap.alpha = 1
    root.ui.minimap.onlyOnHover = false

    if editor and editor.minimapSizeSlider then
      editor.minimapSizeSlider:SetValueSilent(defaultSize)
    end
    if editor and editor.minimapAlphaSlider then
      editor.minimapAlphaSlider:SetValueSilent(100)
    end
    if editor and editor.minimapHoverCheck then
      editor.minimapHoverCheck:SetChecked(false)
    end
    local updateFn = _G.LIDPANELS_UpdateMinimapButton
    if updateFn then updateFn() end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_COPY_PANEL"] = {
  text = "Copy panel to new name:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_PANEL_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PANEL_NAME)
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
    local label, dropdown = EnsureCopyPanelProfileControls(self)
    if label and dropdown then
      local profiles = GetCopyPanelTargetProfiles()
      local root = RootDB()
      local defaultProfile = root.currentProfile
      if defaultProfile == nil or defaultProfile == "" then
        defaultProfile = profiles[1]
      end
      if defaultProfile == "Default" and profiles[1] then
        defaultProfile = profiles[1]
      end
      self.__lidCopyPanelProfiles = profiles
      self.__lidCopyProfileSelected = defaultProfile
      self.__lidCopyProfilePage = GetCopyPanelProfileSelectedPage(profiles, defaultProfile)

      if dropdown.__lidpanelsCustomDropdown then
        dropdown:SetMenuBuilder(function()
          return BuildCopyPanelProfilePagedMenu(self, dropdown)
        end)
        dropdown:SetSelectedValue(self.__lidCopyProfileSelected)
        dropdown:SetText(self.__lidCopyProfileSelected or "(select profile)")
      else
        UIDropDownMenu_Initialize(dropdown, function(_, level)
          if level ~= 1 then return end
          local menu = BuildCopyPanelProfilePagedMenu(self, dropdown)
          for _, entry in ipairs(menu.entries or {}) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = entry.text
            info.value = entry.value
            info.checked = entry.checked
            info.isTitle = entry.isTitle
            info.notCheckable = entry.notCheckable
            info.disabled = entry.disabled
            info.keepShownOnClick = entry.keepShownOnClick
            info.func = function()
              if type(entry.func) == "function" then
                entry.func()
                UIDropDownMenu_Refresh(dropdown)
              end
            end
            UIDropDownMenu_AddButton(info, level)
          end
        end)
        UIDropDownMenu_SetSelectedValue(dropdown, self.__lidCopyProfileSelected)
        UIDropDownMenu_SetText(dropdown, self.__lidCopyProfileSelected or "(select profile)")
      end
      RequestCopyPanelPopupLayout(self, 0.5)
    end
  end,
  OnAccept = function(self)
    if not selectedPanelName then return end
    local txt = GetPopupText(self)
    CopySelectedPanelToProfile(txt, self.__lidCopyProfileSelected)
  end,
  OnHide = function(self)
    HideCopyPanelProfileControls(self)
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_PANEL"] = {
  text = "Enter Name for your new Panel:",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_PANEL_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PANEL_NAME)
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    local txt = GetPopupText(self)
    if CreateNewPanelNamed then CreateNewPanelNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_NEED_PROFILE"] = {
  text = "No active profile. :(\nPlease activate a Profile first:\n\n1. go to Profiles\n2. select a Profile\n3. click \"Activate\"\n4. The profile should be highlight purple now",
  button1 = "OK Boss!",
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_BG"] = {
  text = "Enter the name of your file\n(without .tga)",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_BACKGROUND_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
      HookPopupMaxLetters(self, MAX_BACKGROUND_NAME)
    end
  end,
  OnAccept = function(self)
    local txt = GetPopupText(self)
    if CreateBackgroundNamed then CreateBackgroundNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_BG_CATEGORY"] = {
  text = "Enter the name of your new category, for example \"Character Images\"",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_BG_CATEGORY_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
      HookPopupMaxLetters(self, MAX_BG_CATEGORY_NAME)
    end
  end,
  OnAccept = function(self)
    local txt = GetPopupText(self)
    if CreateBackgroundCategoryNamed then CreateBackgroundCategoryNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RENAME_BG_CATEGORY"] = {
  text = "Rename category:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_BG_CATEGORY_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      editBox:SetText(selectedBackgroundCategory or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
      HookPopupMaxLetters(self, MAX_BG_CATEGORY_NAME)
    end
  end,
  OnAccept = function(self)
    if not selectedBackgroundCategory then return end
    local txt = GetPopupText(self)
    if RenameBackgroundCategory then RenameBackgroundCategory(selectedBackgroundCategory, txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_BG_CATEGORY"] = {
  text = "Are you sure you want to delete \"%s\"?",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if not selectedBackgroundCategory then return end
    DeleteBackgroundCategory(selectedBackgroundCategory)
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_BG_CATEGORY_GROUP"] = {
  text = "Create new category group:",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_BG_CATEGORY_GROUP_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
      HookPopupMaxLetters(self, MAX_BG_CATEGORY_GROUP_NAME)
    end
  end,
  OnAccept = function(self)
    local txt = GetPopupText(self)
    if CreateBackgroundCategoryGroupNamed then CreateBackgroundCategoryGroupNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RENAME_BG_CATEGORY_GROUP"] = {
  text = "Rename category group:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_BG_CATEGORY_GROUP_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      editBox:SetText(selectedBackgroundCategoryGroup or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
      HookPopupMaxLetters(self, MAX_BG_CATEGORY_GROUP_NAME)
    end
  end,
  OnAccept = function(self)
    if not selectedBackgroundCategoryGroup then return end
    local txt = GetPopupText(self)
    if RenameBackgroundCategoryGroup then RenameBackgroundCategoryGroup(selectedBackgroundCategoryGroup, txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_BG_CATEGORY_GROUP"] = {
  text = "Delete the category group \"%s\"?\nCategories inside the group will remain and become groupless.",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if not selectedBackgroundCategoryGroup then return end
    DeleteBackgroundCategoryGroup(selectedBackgroundCategoryGroup)
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_NEED_BG_CATEGORY"] = {
  text = "Select or create a category first.",
  button1 = "OK",
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RENAME_BG"] = {
  text = "Rename background (without .tga):",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_BACKGROUND_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_BACKGROUND_NAME)
      editBox:SetText(selectedBackgroundName or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    if not selectedBackgroundName then return end
    local txt = GetPopupText(self)
    RenameBackground(selectedBackgroundName, txt)
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_BG"] = {
  text = "Are you sure you want to delete the background \"%s\"?",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if not selectedBackgroundName then return end
    DeleteBackground(selectedBackgroundName)
    if editor and editor.RefreshBackgroundListDetailsSection then
      editor:RefreshBackgroundListDetailsSection()
    elseif editor and editor.RefreshBackgroundsSection then
      editor:RefreshBackgroundsSection()
    elseif editor and editor.Refresh then
      editor:Refresh()
    end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_PANEL"] = {
  text = "Are you sure you want to delete the panel \"%s\"?",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if DeleteSelectedPanel then DeleteSelectedPanel() end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RENAME_PROFILE"] = {
  text = "Rename profile:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_PROFILE_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PROFILE_NAME)
      editBox:SetText(editor and editor.selectedProfileName or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    if not editor or not editor.selectedProfileName then return end
    local txt = TrimName(GetPopupText(self))
    if txt == "" then
      Print("Please enter a profile name.")
      return
    end
    RenameProfile(editor.selectedProfileName, txt)
    editor.selectedProfileName = txt
    editor.selectedProfileGroupName = nil
    if editor.RefreshProfilesSection then
      editor:RefreshProfilesSection()
    elseif editor.Refresh then
      editor:Refresh()
    end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_PROFILE"] = {
  text = "Create new profile:",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_PROFILE_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PROFILE_NAME)
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    local txt = TrimName(GetPopupText(self))
    if txt == "" then
      Print("Please enter a profile name.")
      return
    end
    if CreateProfileNamed then CreateProfileNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_CREATE_PROFILE_GROUP"] = {
  text = "Create new profile group:",
  button1 = "Create",
  button2 = "Cancel",
  maxLetters = MAX_PROFILE_GROUP_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PROFILE_GROUP_NAME)
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    local txt = TrimName(GetPopupText(self))
    if txt == "" then
      Print("Please enter a group name.")
      return
    end
    if CreateProfileGroupNamed then CreateProfileGroupNamed(txt) end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_RENAME_PROFILE_GROUP"] = {
  text = "Rename profile group:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_PROFILE_GROUP_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PROFILE_GROUP_NAME)
      editBox:SetText(editor and editor.selectedProfileGroupName or "")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    if not editor or not editor.selectedProfileGroupName then return end
    local txt = TrimName(GetPopupText(self))
    if txt == "" then
      Print("Please enter a group name.")
      return
    end
    RenameProfileGroup(editor.selectedProfileGroupName, txt)
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_PROFILE_GROUP"] = {
  text = "Delete the profile group \"%s\"?\nProfiles inside the group will remain and become groupless.",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if not editor or not editor.selectedProfileGroupName then return end
    DeleteProfileGroup(editor.selectedProfileGroupName)
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_COPY_PROFILE"] = {
  text = "Copy profile to new name:",
  button1 = "Confirm",
  button2 = "Cancel",
  maxLetters = MAX_PROFILE_NAME,
  hasEditBox = true,
  OnShow = function(self)
    local editBox = GetPopupEditBox(self)
    if editBox then
      HookPopupMaxLetters(self, MAX_PROFILE_NAME)
      editBox:SetText("")
      editBox:HighlightText()
      editBox:SetFocus()
      HookPopupEnterAccept(self)
    end
  end,
  OnAccept = function(self)
    if not editor or not editor.selectedProfileName then return end
    local txt = TrimName(GetPopupText(self))
    if txt == "" then
      Print("Please enter a profile name.")
      return
    end
    CopyProfileFrom(editor.selectedProfileName, txt)
    editor.selectedProfileName = txt
    editor.selectedProfileGroupName = nil
    if editor.RefreshProfilesSection then
      editor:RefreshProfilesSection()
    elseif editor.Refresh then
      editor:Refresh()
    end
  end,
  EditBoxOnEnterPressed = PopupEditBoxConfirm,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

StaticPopupDialogs["LIDPANELS_DELETE_PROFILE"] = {
  text = "Are you sure you want to delete the Profile \"%s\"?",
  button1 = "Delete",
  button2 = "Cancel",
  OnShow = function(self)
    HookPopupEnterAccept(self)
  end,
  OnAccept = function()
    if not editor or not editor.selectedProfileName then return end
    DeleteProfile(editor.selectedProfileName)
    editor.selectedProfileName = nil
    EnsureDefaults()
    RebuildAllPanels()
    if editor.RefreshProfilesSection then
      editor:RefreshProfilesSection()
    elseif editor.Refresh then
      editor:Refresh()
    end
  end,
  timeout = 0,
  whileDead = true,
  hideOnEscape = true,
}

-- ============================================================
-- ##--------------------------EDITOR WINDOW STATE---------------
-- ============================================================

-- ============================================================
-- ##--------------------------PROFILES HELPERS------------------
-- ============================================================

local profileGroupsNormalizedRoot
local backgroundCategoryGroupsNormalizedRoot
local profileGroupMembershipRoot
local profileGroupMembershipByProfile
local backgroundCategoryGroupMembershipRoot
local backgroundCategoryGroupMembershipByCategory
local sortedProfileNamesRoot
local sortedProfileNamesCache
local sortedPanelNamesByDb = setmetatable({}, { __mode = "k" })
local sortedBackgroundNamesByList = setmetatable({}, { __mode = "k" })

local function InvalidateSortedProfileNames(root)
  if not root or sortedProfileNamesRoot == root then
    sortedProfileNamesRoot = nil
    sortedProfileNamesCache = nil
  end
end

local function InvalidateSortedPanelNames(db)
  if not db then
    sortedPanelNamesByDb = setmetatable({}, { __mode = "k" })
    return
  end
  sortedPanelNamesByDb[db] = nil
end

local function InvalidateSortedBackgroundNames(list)
  if not list then
    sortedBackgroundNamesByList = setmetatable({}, { __mode = "k" })
    return
  end
  sortedBackgroundNamesByList[list] = nil
end

local function GetSortedBackgroundNames(list)
  if type(list) ~= "table" then return {} end
  local cached = sortedBackgroundNamesByList[list]
  if cached then
    return cached
  end
  local names = {}
  for _, name in ipairs(list) do
    names[#names + 1] = name
  end
  table.sort(names, function(a, b)
    return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
  end)
  sortedBackgroundNamesByList[list] = names
  return names
end

local function ListProfiles()
  local root = RootDB()
  if sortedProfileNamesRoot == root and type(sortedProfileNamesCache) == "table" then
    return sortedProfileNamesCache
  end
  local names = {}
  for k, _ in pairs(root.profiles) do
    if k ~= "Default" then
      names[#names+1] = k
    end
  end
  table.sort(names)
  sortedProfileNamesRoot = root
  sortedProfileNamesCache = names
  return names
end

local function SwitchProfile(name)
  if not name or name == "" then return end
  RootDB().currentProfile = name
  DB()
end

local function EnsureProfileGroups()
  local root = RootDB()
  root.profileGroups = root.profileGroups or {}
  if profileGroupsNormalizedRoot == root and profileGroupMembershipRoot == root and type(root.profileGroups) == "table" then
    return root.profileGroups
  end

  local groups = root.profileGroups
  local normalized = {}
  local seenNames = {}
  local membership = {}

  local function sortNames(list)
    table.sort(list, function(a, b)
      return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
    end)
  end

  for _, group in ipairs(groups) do
    if type(group) == "table" then
      local rawName = TrimName and TrimName(group.name) or tostring(group.name or "")
      local name = ClampNameLen and ClampNameLen(rawName, MAX_PROFILE_GROUP_NAME, "Profile group") or rawName
      local key = string.lower(name or "")
      if name ~= "" and not seenNames[key] then
        seenNames[key] = true
        local clean = {
          name = name,
          collapsed = group.collapsed == true,
          profiles = {},
        }
        local seenProfiles = {}
        for _, profileName in ipairs(group.profiles or {}) do
          local pname = tostring(profileName or "")
          if pname ~= "" and pname ~= "Default" and root.profiles[pname] and not seenProfiles[pname] then
            clean.profiles[#clean.profiles + 1] = pname
            seenProfiles[pname] = true
            membership[pname] = name
          end
        end
        sortNames(clean.profiles)
        normalized[#normalized + 1] = clean
      end
    end
  end

  table.sort(normalized, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)

  root.profileGroups = normalized
  profileGroupsNormalizedRoot = root
  profileGroupMembershipRoot = root
  profileGroupMembershipByProfile = membership
  return normalized
end

local function FindProfileGroupIndex(groups, groupName)
  local needle = string.lower(tostring(groupName or ""))
  for i, group in ipairs(groups or {}) do
    if string.lower(tostring(group.name or "")) == needle then
      return i
    end
  end
  return nil
end

local function GetProfileGroupName(profileName)
  if not profileName or profileName == "" then return nil end
  local root = RootDB()
  if profileGroupMembershipRoot ~= root then
    EnsureProfileGroups()
  end
  return profileGroupMembershipByProfile and profileGroupMembershipByProfile[profileName] or nil
end

local function RemoveProfileFromAllGroups(profileName, groups)
  if not profileName or profileName == "" then return nil end
  groups = groups or EnsureProfileGroups()
  local root = RootDB()
  local removedFrom = nil
  for _, group in ipairs(groups) do
    for i = #group.profiles, 1, -1 do
      if group.profiles[i] == profileName then
        removedFrom = group.name
        table.remove(group.profiles, i)
      end
    end
  end
  if profileGroupMembershipRoot == root and profileGroupMembershipByProfile then
    profileGroupMembershipByProfile[profileName] = nil
  end
  return removedFrom
end

local function AssignProfileToGroup(profileName, groupName)
  if not profileName or profileName == "" then return false end
  local groups = EnsureProfileGroups()
  local root = RootDB()
  if not root.profiles[profileName] or profileName == "Default" then
    return false
  end

  if not groupName or groupName == "" then
    RemoveProfileFromAllGroups(profileName, groups)
    profileGroupsNormalizedRoot = root
    return true
  end

  local idx = FindProfileGroupIndex(groups, groupName)
  if not idx then
    return false
  end

  RemoveProfileFromAllGroups(profileName, groups)

  local group = groups[idx]
  group.collapsed = false
  group.profiles = group.profiles or {}
  group.profiles[#group.profiles + 1] = profileName
  table.sort(group.profiles, function(a, b)
    return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
  end)
  profileGroupsNormalizedRoot = root
  if profileGroupMembershipRoot == root and profileGroupMembershipByProfile then
    profileGroupMembershipByProfile[profileName] = group.name
  end
  return true
end

ToggleProfileGroupCollapsed = function(groupName)
  local groups = EnsureProfileGroups()
  local idx = FindProfileGroupIndex(groups, groupName)
  if not idx then return false end
  groups[idx].collapsed = not groups[idx].collapsed
  return true
end

CreateProfileGroupNamed = function(rawName)
  local name = TrimName(rawName)
  name = ClampNameLen(name, MAX_PROFILE_GROUP_NAME, "Profile group")
  if name == "" then
    Print("Please enter a group name.")
    return false
  end

  local groups = EnsureProfileGroups()
  if FindProfileGroupIndex(groups, name) then
    Print("Profile group already exists: " .. name)
    return false
  end

  groups[#groups + 1] = {
    name = name,
    collapsed = false,
    profiles = {},
  }
  table.sort(groups, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)
  profileGroupsNormalizedRoot = RootDB()

  if editor and editor.RefreshProfilesSection then
    editor:RefreshProfilesSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

RenameProfileGroup = function(oldName, rawNewName)
  local name = TrimName(rawNewName)
  name = ClampNameLen(name, MAX_PROFILE_GROUP_NAME, "Profile group")
  if not oldName or oldName == "" then return false end
  if name == "" then
    Print("Please enter a group name.")
    return false
  end

  local groups = EnsureProfileGroups()
  local idx = FindProfileGroupIndex(groups, oldName)
  if not idx then
    return false
  end

  local existingIdx = FindProfileGroupIndex(groups, name)
  if existingIdx and existingIdx ~= idx then
    Print("Profile group already exists: " .. name)
    return false
  end

  local renamedGroup = groups[idx]
  renamedGroup.name = name
  table.sort(groups, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)
  profileGroupsNormalizedRoot = RootDB()
  if profileGroupMembershipRoot == RootDB() and profileGroupMembershipByProfile then
    for _, profileName in ipairs(renamedGroup.profiles or {}) do
      profileGroupMembershipByProfile[profileName] = name
    end
  end

  if editor and editor.selectedProfileGroupName == oldName then
    editor.selectedProfileGroupName = name
  end
  if editor and editor.RefreshProfilesSection then
    editor:RefreshProfilesSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

DeleteProfileGroup = function(groupName)
  if not groupName or groupName == "" then return false end
  local groups = EnsureProfileGroups()
  local idx = FindProfileGroupIndex(groups, groupName)
  if not idx then
    return false
  end

  if profileGroupMembershipRoot == RootDB() and profileGroupMembershipByProfile then
    for _, profileName in ipairs(groups[idx].profiles or {}) do
      profileGroupMembershipByProfile[profileName] = nil
    end
  end
  table.remove(groups, idx)
  profileGroupsNormalizedRoot = RootDB()
  if editor and editor.selectedProfileGroupName == groupName then
    editor.selectedProfileGroupName = nil
  end
  if editor and editor.RefreshProfilesSection then
    editor:RefreshProfilesSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

CopyProfileFrom = function(sourceName, newName)
  if not sourceName or sourceName == "" then return end
  if not newName or newName == "" then return end
  newName = ClampNameLen(TrimName(newName), MAX_PROFILE_NAME, "Profile")
  if newName == "" then return end
  local root = RootDB()
  if root.profiles[newName] then
    Print("Profile already exists: " .. newName)
    return
  end
  root.profiles[newName] = DeepCopy(root.profiles[sourceName] or { panels = {}, positions = {}, spec = { auto = false, layouts = {} }, __seeded = false })
  InvalidateSortedProfileNames(root)
  local sourceGroup = GetProfileGroupName(sourceName)
  if sourceGroup then
    AssignProfileToGroup(newName, sourceGroup)
  end
  SwitchProfile(newName)
  EnsureDefaults()
end

DeleteProfile = function(name)
  if not name or name == "" then return end
  local root = RootDB()
  if name == "Default" then
    Print("Cannot delete Default profile.")
    return
  end
  RemoveProfileFromAllGroups(name)
  root.profiles[name] = nil
  InvalidateSortedProfileNames(root)
  if root.currentProfile == name then root.currentProfile = "Default" end
  DB()
  EnsureDefaults()
end

CreateProfileNamed = function(name)
  local newName = TrimName(name)
  newName = ClampNameLen(newName, MAX_PROFILE_NAME, "Profile")
  if newName == "" then
    Print("Please enter a profile name.")
    return false
  end
  if newName == "Default" then
    Print("Cannot create profile named Default.")
    return false
  end
  local root = RootDB()
  if root.profiles[newName] then
    Print("Profile already exists: " .. newName)
    return false
  end

  root.profiles[newName] = {
    panels = {},
    positions = {},
    spec = { auto = false, layouts = {} },
    __seeded = true,
    profileSpec = { enabled = false, mode = "SPEC", specID = nil, character = nil },
  }
  InvalidateSortedProfileNames(root)

  SwitchProfile(newName)
  EnsureDefaults()
  RebuildAllPanels()
  if editor then
    editor.selectedProfileName = newName
    editor.selectedProfileGroupName = nil
    if editor.RefreshProfilesSection then
      editor:RefreshProfilesSection()
    elseif editor.Refresh then
      editor:Refresh()
    end
  end
  return true
end

RenameProfile = function(oldName, newName)
  if not oldName or oldName == "" then return end
  newName = TrimName(newName)
  newName = ClampNameLen(newName, MAX_PROFILE_NAME, "Profile")
  if not newName or newName == "" then return end
  if oldName == newName then return end

  local root = RootDB()
  if root.profiles[newName] then
    Print("Profile already exists: " .. newName)
    return
  end
  if oldName == "Default" then
    Print("Cannot rename Default profile.")
    return
  end

  root.profiles[newName] = root.profiles[oldName]
  root.profiles[oldName] = nil
  InvalidateSortedProfileNames(root)
  local groups = EnsureProfileGroups()
  for _, group in ipairs(groups) do
    for i, profileName in ipairs(group.profiles or {}) do
      if profileName == oldName then
        group.profiles[i] = newName
      end
    end
    table.sort(group.profiles, function(a, b)
      return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
    end)
  end
  profileGroupsNormalizedRoot = root
  if profileGroupMembershipRoot == root and profileGroupMembershipByProfile then
    profileGroupMembershipByProfile[newName] = profileGroupMembershipByProfile[oldName]
    profileGroupMembershipByProfile[oldName] = nil
  end
  if root.currentProfile == oldName then root.currentProfile = newName end
end

local function BuildVisibleProfileRows(root, selectedProfileName)
  local rows = {}
  local grouped = {}
  local currentProfile = root and root.currentProfile or nil
  local groups = EnsureProfileGroups()

  for _, group in ipairs(groups) do
    local childNames = {}
    for _, profileName in ipairs(group.profiles or {}) do
      if root.profiles[profileName] and profileName ~= "Default" then
        childNames[#childNames + 1] = profileName
        grouped[profileName] = group.name
      end
    end

    local containsCurrent = false
    local containsSelected = false
    for _, profileName in ipairs(childNames) do
      if profileName == currentProfile then
        containsCurrent = true
      end
      if profileName == selectedProfileName then
        containsSelected = true
      end
    end

    rows[#rows + 1] = {
      type = "group",
      key = "group:" .. string.lower(group.name or ""),
      name = group.name,
      collapsed = group.collapsed == true,
      childCount = #childNames,
      containsCurrent = containsCurrent,
      containsSelected = containsSelected,
    }

    if group.collapsed ~= true then
      for _, profileName in ipairs(childNames) do
        rows[#rows + 1] = {
          type = "profile",
          key = "profile:" .. profileName,
          name = profileName,
          groupName = group.name,
        }
      end
    end
  end

  for _, profileName in ipairs(ListProfiles()) do
    if not grouped[profileName] then
      rows[#rows + 1] = {
        type = "profile",
        key = "profile:" .. profileName,
        name = profileName,
        groupName = nil,
      }
    end
  end

  return rows
end

local function GetProfileFocusKey(selectedProfileName, selectedGroupName)
  if selectedGroupName and selectedGroupName ~= "" then
    return "group:" .. string.lower(selectedGroupName)
  end
  if not selectedProfileName or selectedProfileName == "" then
    return nil
  end
  local groupName = GetProfileGroupName(selectedProfileName)
  if groupName then
    local groups = EnsureProfileGroups()
    local idx = FindProfileGroupIndex(groups, groupName)
    if idx and groups[idx] and groups[idx].collapsed then
      return "group:" .. string.lower(groupName)
    end
  end
  return "profile:" .. selectedProfileName
end

-- ============================================================
-- ##--------------------------EDITOR UI-------------------------
-- ============================================================

editor = nil
selectedPanelName = nil
selectedBackgroundName = nil
selectedBackgroundCategory = nil
selectedBackgroundCategoryGroup = nil

local function IsValidPanelName(name)
  if not name or name == "" then return false end
  return not name:find("[%c]")
end

TrimName = function(name)
  if not name then return "" end
  name = tostring(name)
  name = name:gsub("^%s+", ""):gsub("%s+$", "")
  return name
end

NormalizePanelName = function(raw)
  local name = TrimName(raw)
  return name
end

local function NormalizeBackgroundName(raw)
  local name = TrimName(raw or "")
  if name == "" then return "" end
  name = name:gsub("%.tga$", "")
  name = name:gsub("[^%w%s%-%_]", "")
  name = name:gsub("%s+", " ")
  return name
end

local function GetBackgroundPath(name)
  return "Interface\\AddOns\\LidPanels_UserMedia\\Your_Backgrounds\\" .. name .. ".tga"
end

local function EnsureBackgroundCategories()
  local root = RootDB()
  root.backgroundCategories = root.backgroundCategories or {}
  return root.backgroundCategories
end

local function ListBackgroundCategories()
  return EnsureBackgroundCategories()
end

  local function ListBackgrounds(categoryName)
    local cats = EnsureBackgroundCategories()
    if not categoryName or categoryName == "" then
      return {}
    end
    for _, cat in ipairs(cats) do
      if cat.name == categoryName then
        cat.items = cat.items or {}
        return cat.items
      end
    end
    return {}
  end

local function FindBackgroundIndex(list, name)
  local needle = string.lower(name)
  for i, v in ipairs(list) do
    if string.lower(v) == needle then return i end
  end
  return nil
end

local function FindCategoryIndex(list, name)
  local needle = string.lower(name)
  for i, v in ipairs(list) do
    if string.lower(v.name or "") == needle then return i end
  end
  return nil
end

local function EnsureBackgroundCategoryGroups()
  local root = RootDB()
  root.backgroundCategoryGroups = root.backgroundCategoryGroups or {}
  if backgroundCategoryGroupsNormalizedRoot == root and backgroundCategoryGroupMembershipRoot == root and type(root.backgroundCategoryGroups) == "table" then
    return root.backgroundCategoryGroups
  end

  local groups = root.backgroundCategoryGroups
  local normalized = {}
  local seenNames = {}
  local categories = ListBackgroundCategories()
  local membership = {}

  for _, group in ipairs(groups) do
    if type(group) == "table" then
      local rawName = TrimName and TrimName(group.name) or tostring(group.name or "")
      local name = ClampNameLen and ClampNameLen(rawName, MAX_BG_CATEGORY_GROUP_NAME, "Category group") or rawName
      local key = string.lower(name or "")
      if name ~= "" and not seenNames[key] then
        seenNames[key] = true
        local clean = {
          name = name,
          collapsed = group.collapsed == true,
          categories = {},
        }
        local seenCategories = {}
        for _, categoryName in ipairs(group.categories or {}) do
          local cname = tostring(categoryName or "")
          if cname ~= "" and not seenCategories[cname] and FindCategoryIndex(categories, cname) then
            clean.categories[#clean.categories + 1] = cname
            seenCategories[cname] = true
            membership[cname] = name
          end
        end
        table.sort(clean.categories, function(a, b)
          return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
        end)
        normalized[#normalized + 1] = clean
      end
    end
  end

  table.sort(normalized, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)

  root.backgroundCategoryGroups = normalized
  backgroundCategoryGroupsNormalizedRoot = root
  backgroundCategoryGroupMembershipRoot = root
  backgroundCategoryGroupMembershipByCategory = membership
  return normalized
end

local function FindBackgroundCategoryGroupIndex(groups, groupName)
  local needle = string.lower(tostring(groupName or ""))
  for i, group in ipairs(groups or {}) do
    if string.lower(tostring(group.name or "")) == needle then
      return i
    end
  end
  return nil
end

local function GetBackgroundCategoryGroupName(categoryName)
  if not categoryName or categoryName == "" then return nil end
  local root = RootDB()
  if backgroundCategoryGroupMembershipRoot ~= root then
    EnsureBackgroundCategoryGroups()
  end
  return backgroundCategoryGroupMembershipByCategory and backgroundCategoryGroupMembershipByCategory[categoryName] or nil
end

local function RemoveBackgroundCategoryFromAllGroups(categoryName, groups)
  if not categoryName or categoryName == "" then return nil end
  groups = groups or EnsureBackgroundCategoryGroups()
  local root = RootDB()
  local removedFrom = nil
  for _, group in ipairs(groups) do
    for i = #group.categories, 1, -1 do
      if group.categories[i] == categoryName then
        removedFrom = group.name
        table.remove(group.categories, i)
      end
    end
  end
  if backgroundCategoryGroupMembershipRoot == root and backgroundCategoryGroupMembershipByCategory then
    backgroundCategoryGroupMembershipByCategory[categoryName] = nil
  end
  return removedFrom
end

local function AssignBackgroundCategoryToGroup(categoryName, groupName)
  if not categoryName or categoryName == "" then return false end
  local categories = ListBackgroundCategories()
  if not FindCategoryIndex(categories, categoryName) then
    return false
  end

  local groups = EnsureBackgroundCategoryGroups()
  if not groupName or groupName == "" then
    RemoveBackgroundCategoryFromAllGroups(categoryName, groups)
    backgroundCategoryGroupsNormalizedRoot = RootDB()
    return true
  end

  local idx = FindBackgroundCategoryGroupIndex(groups, groupName)
  if not idx then
    return false
  end

  RemoveBackgroundCategoryFromAllGroups(categoryName, groups)
  local group = groups[idx]
  group.collapsed = false
  group.categories = group.categories or {}
  group.categories[#group.categories + 1] = categoryName
  table.sort(group.categories, function(a, b)
    return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
  end)
  backgroundCategoryGroupsNormalizedRoot = RootDB()
  if backgroundCategoryGroupMembershipRoot == RootDB() and backgroundCategoryGroupMembershipByCategory then
    backgroundCategoryGroupMembershipByCategory[categoryName] = group.name
  end
  return true
end

local function ToggleBackgroundCategoryGroupCollapsed(groupName)
  local groups = EnsureBackgroundCategoryGroups()
  local idx = FindBackgroundCategoryGroupIndex(groups, groupName)
  if not idx then return false end
  groups[idx].collapsed = not groups[idx].collapsed
  return true
end

local function BuildVisibleBackgroundCategoryRows(selectedCategoryName)
  local rows = {}
  local grouped = {}
  local groups = EnsureBackgroundCategoryGroups()
  local categories = ListBackgroundCategories()
  local categoryExists = {}

  for _, category in ipairs(categories) do
    if category and category.name then
      categoryExists[category.name] = true
    end
  end

  for _, group in ipairs(groups) do
    local childNames = {}
    for _, categoryName in ipairs(group.categories or {}) do
      if categoryExists[categoryName] then
        childNames[#childNames + 1] = categoryName
        grouped[categoryName] = group.name
      end
    end

    local containsSelected = false
    for _, categoryName in ipairs(childNames) do
      if categoryName == selectedCategoryName then
        containsSelected = true
      end
    end

    rows[#rows + 1] = {
      type = "group",
      key = "bg-group:" .. string.lower(group.name or ""),
      name = group.name,
      collapsed = group.collapsed == true,
      childCount = #childNames,
      containsSelected = containsSelected,
    }

    if group.collapsed ~= true then
      for _, categoryName in ipairs(childNames) do
        rows[#rows + 1] = {
          type = "category",
          key = "bg-category:" .. categoryName,
          name = categoryName,
          groupName = group.name,
        }
      end
    end
  end

  local ungrouped = {}
  for _, category in ipairs(categories) do
    if category and category.name and not grouped[category.name] then
      ungrouped[#ungrouped + 1] = category.name
    end
  end
  table.sort(ungrouped, function(a, b)
    return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
  end)
  for _, categoryName in ipairs(ungrouped) do
    rows[#rows + 1] = {
      type = "category",
      key = "bg-category:" .. categoryName,
      name = categoryName,
      groupName = nil,
    }
  end

  return rows
end

local function GetBackgroundCategoryFocusKey(selectedCategoryName, selectedGroupName)
  if selectedGroupName and selectedGroupName ~= "" then
    return "bg-group:" .. string.lower(selectedGroupName)
  end
  if not selectedCategoryName or selectedCategoryName == "" then
    return nil
  end
  local groupName = GetBackgroundCategoryGroupName(selectedCategoryName)
  if groupName then
    local groups = EnsureBackgroundCategoryGroups()
    local idx = FindBackgroundCategoryGroupIndex(groups, groupName)
    if idx and groups[idx] and groups[idx].collapsed then
      return "bg-group:" .. string.lower(groupName)
    end
  end
  return "bg-category:" .. selectedCategoryName
end

local function SelectBackground(name)
  selectedBackgroundName = name
  selectedBackgroundCategoryGroup = nil
  if editor and editor.RefreshBackgroundListDetailsSection then
    editor:RefreshBackgroundListDetailsSection()
  elseif editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
end

CreateBackgroundNamed = function(raw)
  local name = NormalizeBackgroundName(raw)
  name = ClampNameLen(name, MAX_BACKGROUND_NAME, "Background")
  if name == "" then
    Print("Please enter a background name.")
    return false
  end
  if raw and TrimName(raw) ~= name then
    Print("Background name adjusted to: " .. name)
  end
  local category = selectedBackgroundCategory
  if not category or category == "" then
    StaticPopup_Show("LIDPANELS_NEED_BG_CATEGORY")
    return false
  end
  local list = ListBackgrounds(category)
  if FindBackgroundIndex(list, name) then
    Print("Background already exists: " .. name)
    return false
  end
  table.insert(list, name)
  InvalidateSortedBackgroundNames(list)
  if editor then
    editor.customBgOptions = nil
  end
  SelectBackground(name)
  return true
end

RenameBackground = function(oldName, rawNewName)
  if not oldName or oldName == "" then return end
  local newName = NormalizeBackgroundName(rawNewName)
  newName = ClampNameLen(newName, MAX_BACKGROUND_NAME, "Background")
  if newName == "" then
    Print("Please enter a background name.")
    return
  end
  if rawNewName and TrimName(rawNewName) ~= newName then
    Print("Background name adjusted to: " .. newName)
  end
  if oldName == newName then return end
  local list = ListBackgrounds(selectedBackgroundCategory)
  if FindBackgroundIndex(list, newName) then
    Print("Background already exists: " .. newName)
    return
  end
  local idx = FindBackgroundIndex(list, oldName)
  if not idx then return end
  list[idx] = newName
  InvalidateSortedBackgroundNames(list)
  if editor then
    editor.customBgOptions = nil
  end
  SelectBackground(newName)
end

DeleteBackground = function(name)
  if not name or name == "" then return end
  local list = ListBackgrounds(selectedBackgroundCategory)
  local idx = FindBackgroundIndex(list, name)
  if not idx then return end
  table.remove(list, idx)
  InvalidateSortedBackgroundNames(list)
  if editor then
    editor.customBgOptions = nil
  end
  if selectedBackgroundName == name then selectedBackgroundName = nil end
end

CreateBackgroundCategoryNamed = function(raw)
  local name = TrimName(raw or "")
  name = ClampNameLen(name, MAX_BG_CATEGORY_NAME, "Category")
  if name == "" then
    Print("Please enter a category name.")
    return false
  end
  local cats = ListBackgroundCategories()
  if FindCategoryIndex(cats, name) then
    Print("Category already exists: " .. name)
    return false
  end
  cats[#cats + 1] = { name = name, items = {} }
  selectedBackgroundCategory = name
  selectedBackgroundCategoryGroup = nil
  selectedBackgroundName = nil
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

RenameBackgroundCategory = function(oldName, rawNewName)
  if not oldName or oldName == "" then return false end
  local newName = TrimName(rawNewName or "")
  newName = ClampNameLen(newName, MAX_BG_CATEGORY_NAME, "Category")
  if newName == "" then
    Print("Please enter a category name.")
    return false
  end
  if oldName == newName then
    return true
  end

  local cats = ListBackgroundCategories()
  if FindCategoryIndex(cats, newName) then
    Print("Category already exists: " .. newName)
    return false
  end

  local idx = FindCategoryIndex(cats, oldName)
  if not idx then
    return false
  end

  cats[idx].name = newName
  local groups = EnsureBackgroundCategoryGroups()
  for _, group in ipairs(groups) do
    for i, categoryName in ipairs(group.categories or {}) do
      if categoryName == oldName then
        group.categories[i] = newName
      end
    end
    table.sort(group.categories, function(a, b)
      return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
    end)
  end
  backgroundCategoryGroupsNormalizedRoot = RootDB()
  if backgroundCategoryGroupMembershipRoot == RootDB() and backgroundCategoryGroupMembershipByCategory then
    backgroundCategoryGroupMembershipByCategory[newName] = backgroundCategoryGroupMembershipByCategory[oldName]
    backgroundCategoryGroupMembershipByCategory[oldName] = nil
  end
  if selectedBackgroundCategory == oldName then
    selectedBackgroundCategory = newName
  end
  table.sort(groups, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

DeleteBackgroundCategory = function(name)
  if not name or name == "" then return end
  local cats = ListBackgroundCategories()
  local idx = FindCategoryIndex(cats, name)
  if not idx then return end
  RemoveBackgroundCategoryFromAllGroups(name)
  table.remove(cats, idx)
  if editor then
    editor.customBgOptions = nil
  end
  if selectedBackgroundCategory == name then
    selectedBackgroundCategory = nil
    selectedBackgroundName = nil
  end
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
end

CreateBackgroundCategoryGroupNamed = function(raw)
  local name = TrimName(raw or "")
  name = ClampNameLen(name, MAX_BG_CATEGORY_GROUP_NAME, "Category group")
  if name == "" then
    Print("Please enter a category group name.")
    return false
  end

  local groups = EnsureBackgroundCategoryGroups()
  if FindBackgroundCategoryGroupIndex(groups, name) then
    Print("Category group already exists: " .. name)
    return false
  end

  groups[#groups + 1] = {
    name = name,
    collapsed = false,
    categories = {},
  }
  table.sort(groups, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)
  backgroundCategoryGroupsNormalizedRoot = RootDB()
  selectedBackgroundCategoryGroup = name
  selectedBackgroundCategory = nil
  selectedBackgroundName = nil
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

RenameBackgroundCategoryGroup = function(oldName, rawNewName)
  local newName = TrimName(rawNewName or "")
  newName = ClampNameLen(newName, MAX_BG_CATEGORY_GROUP_NAME, "Category group")
  if not oldName or oldName == "" then return false end
  if newName == "" then
    Print("Please enter a category group name.")
    return false
  end
  if oldName == newName then
    return true
  end

  local groups = EnsureBackgroundCategoryGroups()
  local idx = FindBackgroundCategoryGroupIndex(groups, oldName)
  if not idx then
    return false
  end
  local existingIdx = FindBackgroundCategoryGroupIndex(groups, newName)
  if existingIdx and existingIdx ~= idx then
    Print("Category group already exists: " .. newName)
    return false
  end

  local renamedGroup = groups[idx]
  renamedGroup.name = newName
  table.sort(groups, function(a, b)
    return string.lower(tostring((a and a.name) or "")) < string.lower(tostring((b and b.name) or ""))
  end)
  backgroundCategoryGroupsNormalizedRoot = RootDB()
  if backgroundCategoryGroupMembershipRoot == RootDB() and backgroundCategoryGroupMembershipByCategory then
    for _, categoryName in ipairs(renamedGroup.categories or {}) do
      backgroundCategoryGroupMembershipByCategory[categoryName] = newName
    end
  end
  if selectedBackgroundCategoryGroup == oldName then
    selectedBackgroundCategoryGroup = newName
  end
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

DeleteBackgroundCategoryGroup = function(name)
  if not name or name == "" then return false end
  local groups = EnsureBackgroundCategoryGroups()
  local idx = FindBackgroundCategoryGroupIndex(groups, name)
  if not idx then
    return false
  end
  if backgroundCategoryGroupMembershipRoot == RootDB() and backgroundCategoryGroupMembershipByCategory then
    for _, categoryName in ipairs(groups[idx].categories or {}) do
      backgroundCategoryGroupMembershipByCategory[categoryName] = nil
    end
  end
  table.remove(groups, idx)
  backgroundCategoryGroupsNormalizedRoot = RootDB()
  if selectedBackgroundCategoryGroup == name then
    selectedBackgroundCategoryGroup = nil
  end
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

local ClearBackgroundCategoryDragState
local CancelBackgroundCategoryDrag
local CompleteBackgroundCategoryDrag
local MoveBackgroundToCategory
local EnsureBackgroundDragBadge

MoveBackgroundToCategory = function(name, sourceCategory, targetCategory)
  local bgName = NormalizeBackgroundName(name)
  local sourceName = TrimName(sourceCategory or "")
  local targetName = TrimName(targetCategory or "")
  if bgName == "" or sourceName == "" or targetName == "" then
    return false
  end

  local sourceList = ListBackgrounds(sourceName)
  local sourceIndex = FindBackgroundIndex(sourceList, bgName)
  if not sourceIndex then
    return false
  end

  if sourceName == targetName then
    local needsRefresh = (selectedBackgroundCategory ~= sourceName)
      or (selectedBackgroundCategoryGroup ~= nil)
      or (selectedBackgroundName ~= bgName)
    selectedBackgroundCategory = sourceName
    selectedBackgroundCategoryGroup = nil
    selectedBackgroundName = bgName
    if editor and editor.RestoreBackgroundsCurrentLabel then
      editor:RestoreBackgroundsCurrentLabel()
    end
    if needsRefresh and editor and editor.RefreshBackgroundListDetailsSection then
      editor:RefreshBackgroundListDetailsSection()
    elseif needsRefresh and editor and editor.RefreshBackgroundsSection then
      editor:RefreshBackgroundsSection()
    elseif needsRefresh and editor and editor.Refresh then
      editor:Refresh()
    end
    return true
  end

  local targetList = ListBackgrounds(targetName)
  if FindBackgroundIndex(targetList, bgName) then
    Print("Category \"" .. targetName .. "\" already contains the background \"" .. bgName .. "\".")
    local needsRefresh = (selectedBackgroundCategory ~= sourceName)
      or (selectedBackgroundCategoryGroup ~= nil)
      or (selectedBackgroundName ~= bgName)
    selectedBackgroundCategory = sourceName
    selectedBackgroundCategoryGroup = nil
    selectedBackgroundName = bgName
    if editor and editor.RestoreBackgroundsCurrentLabel then
      editor:RestoreBackgroundsCurrentLabel()
    end
    if needsRefresh and editor and editor.RefreshBackgroundListDetailsSection then
      editor:RefreshBackgroundListDetailsSection()
    elseif needsRefresh and editor and editor.RefreshBackgroundsSection then
      editor:RefreshBackgroundsSection()
    elseif needsRefresh and editor and editor.Refresh then
      editor:Refresh()
    end
    return false
  end

  table.insert(targetList, bgName)
  table.remove(sourceList, sourceIndex)
  InvalidateSortedBackgroundNames(sourceList)
  InvalidateSortedBackgroundNames(targetList)

  selectedBackgroundCategory = sourceName
  selectedBackgroundCategoryGroup = nil
  selectedBackgroundName = nil
  Print("Moved background \"" .. bgName .. "\" to category \"" .. targetName .. "\".")
  if editor and editor.RefreshBackgroundListDetailsSection then
    editor:RefreshBackgroundListDetailsSection()
  elseif editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

EnsureBackgroundDragBadge = function()
  if not editor then return nil end
  if editor.__bgDragBadge then
    return editor.__bgDragBadge
  end

  local badge = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  badge:SetFrameStrata("TOOLTIP")
  badge:SetFrameLevel(250)
  badge:SetClampedToScreen(true)
  badge:SetSize(260, 36)
  badge:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
  })
  badge:SetBackdropColor(0.04, 0.05, 0.08, 0.97)
  badge:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.98)
  badge:Hide()

  local text = badge:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  text:SetPoint("LEFT", 10, 0)
  text:SetPoint("RIGHT", -10, 0)
  text:SetJustifyH("LEFT")
  text:SetTextColor(0.98, 0.94, 1.00, 1.00)
  badge.text = text

  editor.__bgDragBadge = badge
  return badge
end

ClearBackgroundCategoryDragState = function()
  if not editor then return nil end
  local state = editor.__backgroundDrag
  if not state then return nil end

  if state.sourceButton and state.sourceButton.SetAlpha then
    state.sourceButton:SetAlpha(1)
  end
  if state.sourceButton and state.sourceButton.__bgDragSourceBorder then
    state.sourceButton.__bgDragSourceBorder:Hide()
  end
  if state.hoveredButton and state.hoveredButton.__bgDragDropBorder then
    state.hoveredButton.__bgDragDropBorder:Hide()
  end
  if editor.__bgDragBadge then
    editor.__bgDragBadge:Hide()
  end
  if editor.__bgDragMonitor then
    editor.__bgDragMonitor:Hide()
  end

  editor.__backgroundDrag = nil
  return state
end

CancelBackgroundCategoryDrag = function(skipRefresh)
  local state = ClearBackgroundCategoryDragState()
  if not state then return false end
  if editor and editor.RestoreBackgroundsCurrentLabel then
    editor:RestoreBackgroundsCurrentLabel()
  end
  if (not skipRefresh) and editor and editor.ApplyTheme then
    editor:ApplyTheme()
  end
  return true
end

CompleteBackgroundCategoryDrag = function(targetCategory)
  local state = ClearBackgroundCategoryDragState()
  if not state then return false end
  if not targetCategory or targetCategory == "" then
    if editor and editor.RestoreBackgroundsCurrentLabel then
      editor:RestoreBackgroundsCurrentLabel()
    end
    if editor and editor.ApplyTheme then editor:ApplyTheme() end
    return false
  end
  return MoveBackgroundToCategory(state.name, state.sourceCategory, targetCategory)
end

local function EnsureBackgroundDragMonitor()
  if not editor then return nil end
  if editor.__bgDragMonitor then
    return editor.__bgDragMonitor
  end

  local monitor = CreateFrame("Frame", nil, editor)
  local badge = EnsureBackgroundDragBadge()
  monitor:Hide()
  monitor:SetAllPoints(editor)
  monitor:SetScript("OnUpdate", function(self)
    local state = editor and editor.__backgroundDrag
    if not state then
      if badge then
        badge:Hide()
      end
      self:Hide()
      return
    end

    local hoveredButton = nil
    if type(MouseIsOver) == "function" then
      for _, button in ipairs(editor.bgCatDropButtons or editor.bgCatButtons or {}) do
        if button and button.IsShown and button:IsShown() and MouseIsOver(button) then
          hoveredButton = button
          break
        end
      end
    end

    if hoveredButton ~= state.hoveredButton then
      if state.hoveredButton and state.hoveredButton.__bgDragDropBorder then
        state.hoveredButton.__bgDragDropBorder:Hide()
      end
      state.hoveredButton = hoveredButton
      if hoveredButton and hoveredButton.__bgDragDropBorder then
        hoveredButton.__bgDragDropBorder:Show()
      end
    end

    if badge then
      local scale = (UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
      if scale == 0 then scale = 1 end
      local cx, cy = GetCursorPosition()
      if cx and cy then
        cx = cx / scale
        cy = cy / scale
        badge:ClearAllPoints()
        badge:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cx + 18, cy - 16)
      end
      local targetName = hoveredButton and hoveredButton.__backgroundCategoryName or nil
      if targetName and targetName ~= "" then
        badge.text:SetText("Moving: " .. state.name .. "  ->  " .. targetName)
      else
        badge.text:SetText("Moving: " .. state.name)
      end
      badge:Show()
    end

    local leftDown = (IsMouseButtonDown and IsMouseButtonDown("LeftButton")) or false
    if leftDown then
      return
    end

    self:Hide()
    if hoveredButton and hoveredButton.__backgroundCategoryName then
      CompleteBackgroundCategoryDrag(hoveredButton.__backgroundCategoryName)
    else
      CancelBackgroundCategoryDrag()
    end
  end)

  editor.__bgDragMonitor = monitor
  return monitor
end

local function BeginBackgroundCategoryDrag(name, sourceCategory, sourceButton)
  if not editor or not sourceButton then return false end
  local bgName = NormalizeBackgroundName(name)
  local sourceName = TrimName(sourceCategory or "")
  if bgName == "" or sourceName == "" then
    return false
  end

  CancelBackgroundCategoryDrag(true)

  sourceButton.__bgDragSourceBorder = sourceButton.__bgDragSourceBorder or CreatePurpleOutline(sourceButton)
  sourceButton.__bgDragSourceBorder:Show()
  if sourceButton.SetAlpha then
    sourceButton:SetAlpha(0.72)
  end

  editor.__backgroundDrag = {
    name = bgName,
    sourceCategory = sourceName,
    sourceButton = sourceButton,
    hoveredButton = nil,
  }

  if editor.backgroundsCurrentLabel then
    editor.backgroundsCurrentLabel:SetText("Dragging: " .. bgName .. "  |  Drop it on a category to move it.")
  end

  local monitor = EnsureBackgroundDragMonitor()
  if monitor then
    monitor:Show()
  end
  return true
end

local ClearBackgroundCategoryGroupDragState
local CancelBackgroundCategoryGroupDrag
local CompleteBackgroundCategoryGroupDrag
local MoveBackgroundCategoryToGroup
local EnsureBackgroundCategoryGroupDragBadge

MoveBackgroundCategoryToGroup = function(categoryName, sourceGroupName, targetGroupName)
  local name = TrimName(categoryName or "")
  local sourceName = TrimName(sourceGroupName or "")
  local targetName = TrimName(targetGroupName or "")
  if name == "" then
    return false
  end

  if sourceName == targetName then
    local resolvedGroupName = (targetName ~= "" and targetName or nil)
    local needsRefresh = (selectedBackgroundCategory ~= name)
      or (selectedBackgroundCategoryGroup ~= resolvedGroupName)
      or (selectedBackgroundName ~= nil)
    selectedBackgroundCategory = name
    selectedBackgroundCategoryGroup = resolvedGroupName
    selectedBackgroundName = nil
    if editor and editor.RestoreBackgroundsCurrentLabel then
      editor:RestoreBackgroundsCurrentLabel()
    end
    if needsRefresh and editor and editor.RefreshBackgroundsSection then
      editor:RefreshBackgroundsSection()
    elseif needsRefresh and editor and editor.Refresh then
      editor:Refresh()
    end
    return true
  end

  local ok = AssignBackgroundCategoryToGroup(name, targetName ~= "" and targetName or nil)
  if not ok then
    return false
  end

  selectedBackgroundCategory = name
  selectedBackgroundCategoryGroup = nil
  selectedBackgroundName = nil
  if targetName ~= "" then
    Print("Moved category \"" .. name .. "\" to group \"" .. targetName .. "\".")
  else
    Print("Removed category \"" .. name .. "\" from its group.")
  end
  if editor and editor.RefreshBackgroundsSection then
    editor:RefreshBackgroundsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
  return true
end

EnsureBackgroundCategoryGroupDragBadge = function()
  if not editor then return nil end
  if editor.__bgCategoryGroupDragBadge then
    return editor.__bgCategoryGroupDragBadge
  end

  local badge = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  badge:SetFrameStrata("TOOLTIP")
  badge:SetFrameLevel(250)
  badge:SetClampedToScreen(true)
  badge:SetSize(328, 42)
  badge:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
  })
  badge:SetBackdropColor(0.13, 0.08, 0.20, 0.98)
  badge:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 1.00)
  badge:Hide()

  local text = badge:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  text:SetPoint("LEFT", 12, 0)
  text:SetPoint("RIGHT", -12, 0)
  text:SetJustifyH("LEFT")
  text:SetTextColor(0.98, 0.94, 1.00, 1.00)
  badge.text = text

  editor.__bgCategoryGroupDragBadge = badge
  return badge
end

ClearBackgroundCategoryGroupDragState = function()
  if not editor then return nil end
  local state = editor.__backgroundCategoryGroupDrag
  if not state then return nil end

  if state.sourceButton and state.sourceButton.SetAlpha then
    state.sourceButton:SetAlpha(1)
  end
  if state.sourceButton and state.sourceButton.__bgCategoryGroupDragSourceBorder then
    state.sourceButton.__bgCategoryGroupDragSourceBorder:Hide()
  end
  for _, button in ipairs(editor.bgCatGroupButtons or {}) do
    if button and button.__bgCategoryGroupDragDropBorder then
      button.__bgCategoryGroupDragDropBorder:Hide()
      button.__bgCategoryGroupDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.95)
    end
  end
  if editor.__bgCategoryGroupDragBadge then
    editor.__bgCategoryGroupDragBadge:Hide()
  end
  if editor.__bgCategoryGroupDragMonitor then
    editor.__bgCategoryGroupDragMonitor:Hide()
  end

  editor.__backgroundCategoryGroupDrag = nil
  return state
end

CancelBackgroundCategoryGroupDrag = function(skipRefresh)
  local state = ClearBackgroundCategoryGroupDragState()
  if not state then return false end
  if editor and editor.RestoreBackgroundsCurrentLabel then
    editor:RestoreBackgroundsCurrentLabel()
  end
  if (not skipRefresh) and editor and editor.ApplyTheme then
    editor:ApplyTheme()
  end
  return true
end

CompleteBackgroundCategoryGroupDrag = function(targetGroupName)
  local state = ClearBackgroundCategoryGroupDragState()
  if not state then return false end
  return MoveBackgroundCategoryToGroup(state.name, state.sourceGroupName, targetGroupName)
end

local function EnsureBackgroundCategoryGroupDragMonitor()
  if not editor then return nil end
  if editor.__bgCategoryGroupDragMonitor then
    return editor.__bgCategoryGroupDragMonitor
  end

  local monitor = CreateFrame("Frame", nil, editor)
  local badge = EnsureBackgroundCategoryGroupDragBadge()
  monitor:Hide()
  monitor:SetAllPoints(editor)
  monitor:SetScript("OnUpdate", function(self)
    local state = editor and editor.__backgroundCategoryGroupDrag
    if not state then
      if badge then badge:Hide() end
      self:Hide()
      return
    end

    local hoveredButton = nil
    if type(MouseIsOver) == "function" then
      for _, button in ipairs(editor.bgCatGroupButtons or {}) do
        if button and button.IsShown and button:IsShown() and MouseIsOver(button) then
          hoveredButton = button
          break
        end
      end
    end

    if hoveredButton ~= state.hoveredButton then
      if state.hoveredButton and state.hoveredButton.__bgCategoryGroupDragDropBorder then
        state.hoveredButton.__bgCategoryGroupDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.42)
        state.hoveredButton.__bgCategoryGroupDragDropBorder:Show()
      end
      state.hoveredButton = hoveredButton
      if hoveredButton and hoveredButton.__bgCategoryGroupDragDropBorder then
        hoveredButton.__bgCategoryGroupDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 1.00)
        hoveredButton.__bgCategoryGroupDragDropBorder:Show()
      end
    end

    local overList = false
    if type(MouseIsOver) == "function" and editor.bgCatBg and MouseIsOver(editor.bgCatBg) then
      overList = true
    end

    if badge then
      local scale = (UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
      if scale == 0 then scale = 1 end
      local cx, cy = GetCursorPosition()
      if cx and cy then
        cx = cx / scale
        cy = cy / scale
        badge:ClearAllPoints()
        badge:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cx + 18, cy - 16)
      end
      local targetName = hoveredButton and hoveredButton.__backgroundCategoryGroupName or nil
      if targetName and targetName ~= "" then
        badge.text:SetText("Moving: " .. state.name .. "  ->  " .. targetName)
      elseif overList then
        badge.text:SetText("Moving: " .. state.name .. "  ->  No group")
      else
        badge.text:SetText("Moving: " .. state.name .. "  |  Drop it on a group")
      end
      badge:Show()
    end

    local leftDown = (IsMouseButtonDown and IsMouseButtonDown("LeftButton")) or false
    if leftDown then
      return
    end

    self:Hide()
    if hoveredButton and hoveredButton.__backgroundCategoryGroupName then
      CompleteBackgroundCategoryGroupDrag(hoveredButton.__backgroundCategoryGroupName)
    elseif overList then
      CompleteBackgroundCategoryGroupDrag(nil)
    else
      CancelBackgroundCategoryGroupDrag()
    end
  end)

  editor.__bgCategoryGroupDragMonitor = monitor
  return monitor
end

local function BeginBackgroundCategoryGroupDrag(categoryName, sourceGroupName, sourceButton)
  if not editor or not sourceButton then return false end
  local name = TrimName(categoryName or "")
  if name == "" then
    return false
  end

  CancelBackgroundCategoryGroupDrag(true)

  sourceButton.__bgCategoryGroupDragSourceBorder = sourceButton.__bgCategoryGroupDragSourceBorder or CreatePurpleOutline(sourceButton)
  sourceButton.__bgCategoryGroupDragSourceBorder:Show()
  if sourceButton.SetAlpha then
    sourceButton:SetAlpha(0.72)
  end

  editor.__backgroundCategoryGroupDrag = {
    name = name,
    sourceGroupName = TrimName(sourceGroupName or ""),
    sourceButton = sourceButton,
    hoveredButton = nil,
  }

  for _, button in ipairs(editor.bgCatGroupButtons or {}) do
    if button and button.__bgCategoryGroupDragDropBorder then
      button.__bgCategoryGroupDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.42)
      button.__bgCategoryGroupDragDropBorder:Show()
    end
  end

  if editor.backgroundsCurrentLabel then
    editor.backgroundsCurrentLabel:SetText("Dragging: " .. name .. "  |  Drop it on a group or release inside the list to remove the group.")
  end

  local monitor = EnsureBackgroundCategoryGroupDragMonitor()
  if monitor then
    monitor:Show()
  end
  return true
end

local ClearProfileGroupDragState
local CancelProfileGroupDrag
local CompleteProfileGroupDrag
local MoveProfileToGroup
local EnsureProfileGroupDragBadge

MoveProfileToGroup = function(profileName, sourceGroupName, targetGroupName)
  local name = TrimName(profileName or "")
  local sourceName = TrimName(sourceGroupName or "")
  local targetName = TrimName(targetGroupName or "")
  if name == "" then
    return false
  end

  if sourceName == targetName then
    if editor then
      local needsDetailsRefresh = (editor.selectedProfileName ~= name) or (editor.selectedProfileGroupName ~= nil)
      editor.selectedProfileName = name
      editor.selectedProfileGroupName = nil
      if editor.RestoreProfileCurrentLabel then
        editor:RestoreProfileCurrentLabel()
      end
      if needsDetailsRefresh and editor.RefreshProfilesSection then
        editor:RefreshProfilesSection()
      elseif (not needsDetailsRefresh) and editor.RefreshProfilesListSection then
        editor:RefreshProfilesListSection()
      elseif editor.Refresh then
        editor:Refresh()
      end
    end
    return true
  end

  local ok = AssignProfileToGroup(name, targetName ~= "" and targetName or nil)
  if not ok then
    return false
  end

  if editor then
    local needsDetailsRefresh = (editor.selectedProfileName ~= name) or (editor.selectedProfileGroupName ~= nil)
    editor.selectedProfileName = name
    editor.selectedProfileGroupName = nil
    if editor.RestoreProfileCurrentLabel then
      editor:RestoreProfileCurrentLabel()
    end
    if targetName ~= "" then
      Print("Moved profile \"" .. name .. "\" to group \"" .. targetName .. "\".")
    else
      Print("Removed profile \"" .. name .. "\" from its group.")
    end
    if needsDetailsRefresh and editor.RefreshProfilesSection then
      editor:RefreshProfilesSection()
    elseif (not needsDetailsRefresh) and editor.RefreshProfilesListSection then
      editor:RefreshProfilesListSection()
    elseif editor.Refresh then
      editor:Refresh()
    end
    return true
  end
  if targetName ~= "" then
    Print("Moved profile \"" .. name .. "\" to group \"" .. targetName .. "\".")
  else
    Print("Removed profile \"" .. name .. "\" from its group.")
  end
  return true
end

EnsureProfileGroupDragBadge = function()
  if not editor then return nil end
  if editor.__profileGroupDragBadge then
    return editor.__profileGroupDragBadge
  end

  local badge = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  badge:SetFrameStrata("TOOLTIP")
  badge:SetFrameLevel(250)
  badge:SetClampedToScreen(true)
  badge:SetSize(328, 42)
  badge:SetBackdrop({
    bgFile = "Interface\\Buttons\\WHITE8x8",
    edgeFile = "Interface\\Buttons\\WHITE8x8",
    edgeSize = 1,
  })
  badge:SetBackdropColor(0.13, 0.08, 0.20, 0.98)
  badge:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 1.00)
  badge:Hide()

  local text = badge:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  text:SetPoint("LEFT", 12, 0)
  text:SetPoint("RIGHT", -12, 0)
  text:SetJustifyH("LEFT")
  text:SetTextColor(0.98, 0.94, 1.00, 1.00)
  badge.text = text

  editor.__profileGroupDragBadge = badge
  return badge
end

ClearProfileGroupDragState = function()
  if not editor then return nil end
  local state = editor.__profileGroupDrag
  if not state then return nil end

  if state.sourceButton and state.sourceButton.SetAlpha then
    state.sourceButton:SetAlpha(1)
  end
  if state.sourceButton and state.sourceButton.__profileDragSourceBorder then
    state.sourceButton.__profileDragSourceBorder:Hide()
  end
  for _, button in ipairs(editor.profGroupButtons or {}) do
    if button and button.__profileDragDropBorder then
      button.__profileDragDropBorder:Hide()
      button.__profileDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.95)
    end
  end
  if editor.__profileGroupDragBadge then
    editor.__profileGroupDragBadge:Hide()
  end
  if editor.__profileGroupDragMonitor then
    editor.__profileGroupDragMonitor:Hide()
  end

  editor.__profileGroupDrag = nil
  return state
end

CancelProfileGroupDrag = function(skipRefresh)
  local state = ClearProfileGroupDragState()
  if not state then return false end
  if editor and editor.RestoreProfileCurrentLabel then
    editor:RestoreProfileCurrentLabel()
  end
  if (not skipRefresh) and editor and editor.ApplyTheme then
    editor:ApplyTheme()
  end
  return true
end

CompleteProfileGroupDrag = function(targetGroupName)
  local state = ClearProfileGroupDragState()
  if not state then return false end
  return MoveProfileToGroup(state.name, state.sourceGroupName, targetGroupName)
end

local function EnsureProfileGroupDragMonitor()
  if not editor then return nil end
  if editor.__profileGroupDragMonitor then
    return editor.__profileGroupDragMonitor
  end

  local monitor = CreateFrame("Frame", nil, editor)
  local badge = EnsureProfileGroupDragBadge()
  monitor:Hide()
  monitor:SetAllPoints(editor)
  monitor:SetScript("OnUpdate", function(self)
    local state = editor and editor.__profileGroupDrag
    if not state then
      if badge then
        badge:Hide()
      end
      self:Hide()
      return
    end

    local hoveredButton = nil
    if type(MouseIsOver) == "function" then
      for _, button in ipairs(editor.profGroupButtons or {}) do
        if button and button.IsShown and button:IsShown() and MouseIsOver(button) then
          hoveredButton = button
          break
        end
      end
    end

    if hoveredButton ~= state.hoveredButton then
      if state.hoveredButton and state.hoveredButton.__profileDragDropBorder then
        state.hoveredButton.__profileDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.42)
        state.hoveredButton.__profileDragDropBorder:Show()
      end
      state.hoveredButton = hoveredButton
      if hoveredButton and hoveredButton.__profileDragDropBorder then
        hoveredButton.__profileDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 1.00)
        hoveredButton.__profileDragDropBorder:Show()
      end
    end

    local overList = false
    if type(MouseIsOver) == "function" and editor.profListBg and MouseIsOver(editor.profListBg) then
      overList = true
    end

    if badge then
      local scale = (UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
      if scale == 0 then scale = 1 end
      local cx, cy = GetCursorPosition()
      if cx and cy then
        cx = cx / scale
        cy = cy / scale
        badge:ClearAllPoints()
        badge:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cx + 18, cy - 16)
      end
      local targetName = hoveredButton and hoveredButton.__profileGroupName or nil
      if targetName and targetName ~= "" then
        badge.text:SetText("Moving: " .. state.name .. "  ->  " .. targetName)
      elseif overList then
        badge.text:SetText("Moving: " .. state.name .. "  ->  No group")
      else
        badge.text:SetText("Moving: " .. state.name .. "  |  Drop it on a group")
      end
      badge:Show()
    end

    local leftDown = (IsMouseButtonDown and IsMouseButtonDown("LeftButton")) or false
    if leftDown then
      return
    end

    self:Hide()
    if hoveredButton and hoveredButton.__profileGroupName then
      CompleteProfileGroupDrag(hoveredButton.__profileGroupName)
    elseif overList then
      CompleteProfileGroupDrag(nil)
    else
      CancelProfileGroupDrag()
    end
  end)

  editor.__profileGroupDragMonitor = monitor
  return monitor
end

local function BeginProfileGroupDrag(profileName, sourceGroupName, sourceButton)
  if not editor or not sourceButton then return false end
  local name = TrimName(profileName or "")
  if name == "" then
    return false
  end

  CancelProfileGroupDrag(true)

  sourceButton.__profileDragSourceBorder = sourceButton.__profileDragSourceBorder or CreatePurpleOutline(sourceButton)
  sourceButton.__profileDragSourceBorder:Show()
  if sourceButton.SetAlpha then
    sourceButton:SetAlpha(0.72)
  end

  editor.__profileGroupDrag = {
    name = name,
    sourceGroupName = TrimName(sourceGroupName or ""),
    sourceButton = sourceButton,
    hoveredButton = nil,
  }

  for _, button in ipairs(editor.profGroupButtons or {}) do
    if button and button.__profileDragDropBorder then
      button.__profileDragDropBorder:SetBackdropBorderColor(0.6588, 0.4784, 0.9098, 0.42)
      button.__profileDragDropBorder:Show()
    end
  end

  if editor.profileLabel then
    editor.profileLabel:SetText("Dragging: " .. name .. "  |  Drop it on a group or release inside the list to remove the group.")
  end

  local monitor = EnsureProfileGroupDragMonitor()
  if monitor then
    monitor:Show()
  end
  return true
end

ClampNameLen = function(name, maxLen, label)
  if not name then return "" end
  if #name > maxLen then
    local trimmed = string.sub(name, 1, maxLen)
    Print(label .. " name too long. Trimmed to: " .. trimmed)
    if LogSilent then LogSilent(label .. " name trimmed to: " .. trimmed) end
    return trimmed
  end
  return name
end

local function GetSortedPanelNames(db)
  if sortedPanelNamesByDb[db] then
    return sortedPanelNamesByDb[db]
  end
  local names = {}
  for name in pairs(db.panels or {}) do
    names[#names + 1] = name
  end
  table.sort(names, function(a, b)
    return string.lower(tostring(a or "")) < string.lower(tostring(b or ""))
  end)
  sortedPanelNamesByDb[db] = names
  return names
end

local function SelectPanel(name)
  if name and not DB().panels[name] then
    name = nil
  end
  selectedPanelName = name
  if editor and editor.RefreshPanelsSection then
    editor:RefreshPanelsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
end

local function GetSelectedPanelName()
  return selectedPanelName
end

local function GetSelectedBackgroundName()
  return selectedBackgroundName
end

local function GetSelectedBackgroundCategory()
  return selectedBackgroundCategory
end

CreateNewPanelNamed = function(name)
  local root = RootDB()
  if root.currentProfile == "Default" then
    StaticPopup_Show("LIDPANELS_NEED_PROFILE")
    return false
  end
  local db = DB()
  local raw = name
  name = NormalizePanelName(name)
  name = ClampNameLen(name, MAX_PANEL_NAME, "Panel")
  if name == "" then
    Print("Please enter a panel name.")
    return false
  end
  if raw and TrimName(raw) ~= name then
    Print("Panel name adjusted to: " .. name)
    if LogSilent then LogSilent("Panel name adjusted to: " .. name) end
  end
  if not IsValidPanelName(name) then
    Print("Invalid panel name. Please remove line breaks/control characters.")
    return false
  end
  if db.panels[name] then
    Print("A panel with that name already exists: " .. name)
    return false
  end

  db.panels[name] = {
    w = 300, h = 120,
    anchor = { "CENTER", "UIParent", "CENTER", 0, 0 },
    color = { 0.05, 0.05, 0.07, 0.75 },
    strata = "BACKGROUND",
    border = "tooltip",
    locked = false,
    gradient = nil,
    diagnostic = { fpsGraph = false },
    specialContent = {
      mode = "NONE",
      showGold = false,
      showFPS = false,
      showStats = false,
      showCustomText = false,
      gold = {
        color = { 1.00, 0.82, 0.00, 1.00 },
        font = "GameFontHighlightSmall",
        size = 14,
      },
      fps = {
        font = "GameFontHighlightSmall",
        size = 14,
        alpha = 1,
        lowValue = 30,
        highValue = 60,
        lowColor = { 0.95, 0.24, 0.24, 1.00 },
        midColor = { 1.00, 0.62, 0.16, 1.00 },
        highColor = { 0.20, 0.90, 0.20, 1.00 },
      },
      stats = {},
      statsTitleColors = {},
      statsValueColors = {},
      statsSize = 14,
      statsFont = "GameFontHighlightSmall",
      statsAlpha = 1,
      customText = {
        text = "Custom Text",
        color = { 1.00, 1.00, 1.00, 1.00 },
        font = "GameFontHighlightSmall",
        size = 14,
      },
    },
  }
  InvalidateSortedPanelNames(db)

  RebuildAllPanels()
  SelectPanel(name)
  return true
end

DeleteSelectedPanel = function()
  if not selectedPanelName then return end
  local panelName = selectedPanelName
  local db = DB()
  db.panels[panelName] = nil
  db.positions[panelName] = nil
  InvalidateSortedPanelNames(db)
  local destroyRuntime = _G.LIDPANELS_DestroyPanelRuntime
  if type(destroyRuntime) == "function" then
    destroyRuntime(panelName)
  elseif frames[panelName] then
    frames[panelName]:Hide()
    frames[panelName] = nil
  end
  selectedPanelName = nil
  RebuildAllPanels()
  if editor and editor.RefreshPanelsSection then
    editor:RefreshPanelsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
end

RenameSelectedPanel = function(newName)
  if not selectedPanelName then return end
  local raw = newName
  newName = NormalizePanelName(newName)
  newName = ClampNameLen(newName, MAX_PANEL_NAME, "Panel")
  if newName == "" then
    Print("Please enter a panel name.")
    return
  end
  if raw and TrimName(raw) ~= newName then
    Print("Panel name adjusted to: " .. newName)
    if LogSilent then LogSilent("Panel name adjusted to: " .. newName) end
  end
  if not IsValidPanelName(newName) then
    Print("Invalid panel name. Please remove line breaks/control characters.")
    return
  end
  if newName == selectedPanelName then return end

  local db = DB()
  if db.panels[newName] then
    Print("A panel with that name already exists: " .. newName)
    return
  end

  local old = selectedPanelName
  local oldCfg = db.panels[old]
  if not oldCfg then return end

  db.panels[newName] = DeepCopy(oldCfg)
  if db.positions[old] then db.positions[newName] = DeepCopy(db.positions[old]) end

  db.panels[old] = nil
  db.positions[old] = nil
  InvalidateSortedPanelNames(db)

  if frames[old] then
    frames[old]:Hide()
    frames[old] = nil
  end

  selectedPanelName = newName
  RebuildAllPanels()
  if editor and editor.RefreshPanelsSection then
    editor:RefreshPanelsSection()
  elseif editor and editor.Refresh then
    editor:Refresh()
  end
end

CopySelectedPanel = function(newName)
  return CopySelectedPanelToProfile(newName, RootDB().currentProfile)
end

CopySelectedPanelToProfile = function(newName, targetProfileName)
  if not selectedPanelName then return end
  local raw = newName
  newName = NormalizePanelName(newName)
  newName = ClampNameLen(newName, MAX_PANEL_NAME, "Panel")
  if newName == "" then
    Print("Please enter a panel name.")
    return
  end
  if raw and TrimName(raw) ~= newName then
    Print("Panel name adjusted to: " .. newName)
    if LogSilent then LogSilent("Panel name adjusted to: " .. newName) end
  end
  if not IsValidPanelName(newName) then
    Print("Invalid panel name. Please remove line breaks/control characters.")
    return
  end

  local root = RootDB()
  local sourceProfileName = root.currentProfile or "Default"
  targetProfileName = TrimName(targetProfileName or sourceProfileName)
  if targetProfileName == "" then
    Print("Please select a target profile.")
    return
  end

  local sourceDb = root.profiles[sourceProfileName]
  if sourceProfileName == root.currentProfile then
    sourceDb = DB()
  end
  sourceDb = sourceDb or { panels = {}, positions = {} }

  root.profiles[targetProfileName] = root.profiles[targetProfileName] or {
    panels = {},
    positions = {},
    spec = { auto = false, layouts = {} },
    __seeded = false,
  }
  local targetDb = root.profiles[targetProfileName]
  targetDb.panels = targetDb.panels or {}
  targetDb.positions = targetDb.positions or {}

  if targetDb.panels[newName] then
    Print("Profile \"" .. tostring(targetProfileName) .. "\" already has a panel named: " .. newName)
    return
  end

  local src = sourceDb.panels[selectedPanelName]
  if not src then return end

  targetDb.panels[newName] = DeepCopy(src)
  if sourceDb.positions and sourceDb.positions[selectedPanelName] then
    targetDb.positions[newName] = DeepCopy(sourceDb.positions[selectedPanelName])
  else
    targetDb.positions[newName] = nil
  end
  InvalidateSortedPanelNames(targetDb)

  if targetProfileName == sourceProfileName then
    RebuildAllPanels()
    SelectPanel(newName)
  else
    Print("Copied panel \"" .. tostring(selectedPanelName) .. "\" to profile \"" .. tostring(targetProfileName) .. "\" as \"" .. tostring(newName) .. "\".")
  end
end

-- ============================================================
-- ##--------------------------EXPORT / IMPORT WINDOWS-----------
-- ============================================================

  local exportWin, importWin, errorWin, silentWin

local function ShowErrorLogWindow()
  if not errorWin then
    errorWin = CreateFrame("Frame", "LIDPANELS_ErrorLogWindow", UIParent, "BackdropTemplate")
    errorWin:SetFrameStrata("DIALOG")
    errorWin:SetSize(540, 360)
    errorWin:SetClampedToScreen(true)
    ApplyFrameSkin(errorWin)

    local title = errorWin:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 12, -10)
    title:SetText("Lid Error Log")
    errorWin.title = title

    local close = CreateFrame("Button", nil, errorWin, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -2, -2)
    close:SetScript("OnClick", function() errorWin:Hide() end)

    local boxFrame, boxEdit = CreateScrollEditBox(errorWin, 510, 250)
    boxFrame:SetPoint("TOPLEFT", 12, -40)
    errorWin.boxEdit = boxEdit

    local ok = CreateFrame("Button", nil, errorWin, "UIPanelButtonTemplate")
    ok:SetSize(100, 22)
    ok:SetPoint("BOTTOMRIGHT", -12, 12)
    ok:SetText("OK")
    ok:SetScript("OnClick", function() errorWin:Hide() end)
  end

  local lines = {}
  local debugStore = GetDebugStore()
  if debugStore and debugStore.errors and #debugStore.errors > 0 then
    for _, msg in ipairs(debugStore.errors) do
      lines[#lines + 1] = tostring(msg)
    end
  else
    lines[1] = "No errors captured."
  end

  errorWin.boxEdit:SetText(table.concat(lines, "\n\n"))
  errorWin.boxEdit:HighlightText()
  errorWin.boxEdit:SetFocus()

  errorWin:ClearAllPoints()
  errorWin:SetPoint("CENTER", UIParent, "CENTER", 220, 0)
  errorWin:Show()
end

local function ShowSilentLogWindow()
  if not silentWin then
    silentWin = CreateFrame("Frame", "LIDPANELS_SilentLogWindow", UIParent, "BackdropTemplate")
    silentWin:SetFrameStrata("DIALOG")
    silentWin:SetSize(540, 360)
    silentWin:SetClampedToScreen(true)
    ApplyFrameSkin(silentWin)

    local title = silentWin:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 12, -10)
    title:SetText("Lid Silent Error Log")
    silentWin.title = title

    local close = CreateFrame("Button", nil, silentWin, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -2, -2)
    close:SetScript("OnClick", function() silentWin:Hide() end)

    local boxFrame, boxEdit = CreateScrollEditBox(silentWin, 510, 250)
    boxFrame:SetPoint("TOPLEFT", 12, -40)
    silentWin.boxEdit = boxEdit

    local ok = CreateFrame("Button", nil, silentWin, "UIPanelButtonTemplate")
    ok:SetSize(100, 22)
    ok:SetPoint("BOTTOMRIGHT", -12, 12)
    ok:SetText("OK")
    ok:SetScript("OnClick", function() silentWin:Hide() end)
  end

  local lines = {}
  local debugStore = GetDebugStore()
  if debugStore and debugStore.silent and #debugStore.silent > 0 then
    for _, msg in ipairs(debugStore.silent) do
      lines[#lines + 1] = tostring(msg)
    end
  else
    lines[1] = "No silent errors captured."
  end

  silentWin.boxEdit:SetText(table.concat(lines, "\n\n"))
  silentWin.boxEdit:HighlightText()
  silentWin.boxEdit:SetFocus()

  silentWin:ClearAllPoints()
  silentWin:SetPoint("CENTER", UIParent, "CENTER", 220, 0)
  silentWin:Show()
end

local function PrepareFloatingPopupWindow(win)
  if not win or win.__lidFloatingPrepared then return end
  win.__lidFloatingPrepared = true
  win:SetMovable(true)
  win:EnableMouse(true)
  win:RegisterForDrag("LeftButton")
  win:SetToplevel(true)
  win:SetFrameStrata("FULLSCREEN_DIALOG")
  win:SetScript("OnDragStart", function(self) self:StartMoving() end)
  win:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
end

local function RaiseFloatingPopupWindow(win)
  if not win then return end
  PrepareFloatingPopupWindow(win)
  local baseLevel = 200
  if editor and editor.GetFrameLevel then
    baseLevel = math.max(baseLevel, (editor:GetFrameLevel() or 1) + 40)
  end
  win:SetFrameLevel(baseLevel)
  if win.Raise then
    win:Raise()
  end
end

local function ShowExportWindow(profileName, exportString)
  if not exportWin then
    exportWin = CreateFrame("Frame", "LIDPANELS_ExportWindow", UIParent, "BackdropTemplate")
    exportWin:SetSize(540, 320)
    exportWin:SetClampedToScreen(true)
    ApplyFrameSkin(exportWin)
    PrepareFloatingPopupWindow(exportWin)

    local title = exportWin:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 12, -10)
    exportWin.title = title

    local close = CreateFrame("Button", nil, exportWin, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -2, -2)

    local boxFrame, boxEdit = CreateScrollEditBox(exportWin, 510, 220)
    boxFrame:SetPoint("TOPLEFT", 12, -40)
    exportWin.boxEdit = boxEdit

    local closeBtn = CreateFrame("Button", nil, exportWin, "UIPanelButtonTemplate")
    closeBtn:SetSize(100, 22)
    closeBtn:SetPoint("BOTTOMRIGHT", -12, 12)
    closeBtn:SetText("Close")
    closeBtn:SetScript("OnClick", function() exportWin:Hide() end)
    exportWin.closeBtn = closeBtn

    local copyBtn = CreateFrame("Button", nil, exportWin, "UIPanelButtonTemplate")
    copyBtn:SetSize(100, 22)
    copyBtn:SetPoint("RIGHT", closeBtn, "LEFT", -8, 0)
    copyBtn:SetText("Copy String")
    copyBtn:SetScript("OnClick", function()
      if exportWin and exportWin.boxEdit then
        exportWin.boxEdit:SetFocus()
        exportWin.boxEdit:HighlightText()
        Print("String selected. Press CTRL+C now to copy it.")
      end
    end)
    exportWin.copyBtn = copyBtn
  end

  if importWin and importWin:IsShown() then
    importWin:Hide()
  end

  exportWin.title:SetText("Lid Export: " .. tostring(profileName))
  exportWin.boxEdit:SetText(exportString or "")
  exportWin.boxEdit:HighlightText()
  exportWin.boxEdit:SetFocus()

  RaiseFloatingPopupWindow(exportWin)
  if not exportWin.__lidPlaced then
    exportWin.__lidPlaced = true
    exportWin:ClearAllPoints()
    exportWin:SetPoint("CENTER", UIParent, "CENTER", 220, 0)
  end
  exportWin:Show()
end

local function ShowImportWindow(onImport)
  if not importWin then
    importWin = CreateFrame("Frame", "LIDPANELS_ImportWindow", UIParent, "BackdropTemplate")
    importWin:SetSize(540, 360)
    importWin:SetClampedToScreen(true)
    ApplyFrameSkin(importWin)
    PrepareFloatingPopupWindow(importWin)

    local title = importWin:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 12, -10)
    title:SetText("Lid Import")
    importWin.title = title

    local close = CreateFrame("Button", nil, importWin, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -2, -2)
    close:SetScript("OnClick", function() importWin:Hide() end)

    local nameLabel = importWin:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    nameLabel:SetPoint("TOPLEFT", 12, -40)
    nameLabel:SetText("New Profile Name:")

    local nameEdit = CreateFrame("EditBox", nil, importWin, "InputBoxTemplate")
    nameEdit:SetSize(220, 20)
    nameEdit:SetPoint("TOPLEFT", 130, -36)
    nameEdit:SetAutoFocus(false)
    importWin.nameEdit = nameEdit

    local boxFrame, boxEdit = CreateScrollEditBox(importWin, 510, 220)
    boxFrame:SetPoint("TOPLEFT", 12, -70)
    importWin.boxEdit = boxEdit

    local pastePlaceholder = CreateFrame("Button", nil, boxFrame)
    pastePlaceholder:SetPoint("TOPLEFT", 2, -2)
    pastePlaceholder:SetPoint("BOTTOMRIGHT", -2, 2)
    pastePlaceholder:SetNormalTexture("Interface/Buttons/WHITE8x8")
    pastePlaceholder:GetNormalTexture():SetVertexColor(0, 0, 0, 0)
    pastePlaceholder:SetScript("OnClick", function()
      if importWin and importWin.boxEdit then
        importWin.boxEdit:SetFocus()
      end
    end)
    local placeholderText = pastePlaceholder:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    placeholderText:SetPoint("TOPLEFT", 4, -2)
    placeholderText:SetJustifyH("LEFT")
    placeholderText:SetText("Click here and insert your string")
    pastePlaceholder.text = placeholderText
    importWin.pastePlaceholder = pastePlaceholder

    local function UpdateImportPlaceholder()
      if not importWin or not importWin.pastePlaceholder or not importWin.boxEdit then return end
      local txt = importWin.boxEdit:GetText() or ""
      local hasText = txt ~= ""
      local focused = importWin.boxEdit:HasFocus() and true or false
      importWin.pastePlaceholder:SetShown((not hasText) and (not focused))
    end
    importWin.UpdateImportPlaceholder = UpdateImportPlaceholder
    boxEdit:HookScript("OnTextChanged", UpdateImportPlaceholder)
    boxEdit:HookScript("OnEditFocusGained", UpdateImportPlaceholder)
    boxEdit:HookScript("OnEditFocusLost", UpdateImportPlaceholder)

    local importBtn = CreateFrame("Button", nil, importWin, "UIPanelButtonTemplate")
    importBtn:SetSize(110, 22)
    importBtn:SetPoint("BOTTOMRIGHT", -12, 12)
    importBtn:SetText("Import")

    local cancelBtn = CreateFrame("Button", nil, importWin, "UIPanelButtonTemplate")
    cancelBtn:SetSize(110, 22)
    cancelBtn:SetPoint("RIGHT", importBtn, "LEFT", -8, 0)
    cancelBtn:SetText("Cancel")
    cancelBtn:SetScript("OnClick", function() importWin:Hide() end)

    importBtn:SetScript("OnClick", function()
      local newName = (importWin.nameEdit:GetText() or ""):gsub("^%s+", ""):gsub("%s+$", "")
      local pasted = importWin.boxEdit:GetText() or ""
      if newName == "" then
        Print("Import: please enter a profile name.")
        return
      end
      onImport(newName, pasted)
      importWin:Hide()
    end)
  end

  if exportWin and exportWin:IsShown() then
    exportWin:Hide()
  end

  importWin.nameEdit:SetText("")
  importWin.boxEdit:SetText("")
  if importWin.UpdateImportPlaceholder then
    importWin:UpdateImportPlaceholder()
  end

  RaiseFloatingPopupWindow(importWin)
  if not importWin.__lidPlaced then
    importWin.__lidPlaced = true
    importWin:ClearAllPoints()
    importWin:SetPoint("CENTER", UIParent, "CENTER", 220, 0)
  end
  importWin:Show()
end

-- ============================================================
-- ##--------------------------EDITOR CREATE---------------------
-- ============================================================

local EDITOR_CREATE_DEPS = {
  RootDB = RootDB,
  DB = DB,
  GetDebugStore = GetDebugStore,
  ReportEditorRuntimeError = ReportEditorRuntimeError,
  SetupEditorFrameShell = SetupEditorFrameShell,
  BindEditorTheme = BindEditorTheme,
  ApplyFrameSkin = ApplyFrameSkin,
  ApplyEditorState = ApplyEditorState,
  ApplyEditorScale = ApplyEditorScale,
  SaveEditorState = SaveEditorState,
  ApplyEditorLogo = ApplyEditorLogo,
  SetAllUnlocked = SetAllUnlocked,
  GetPanelsUnlockedState = GetPanelsUnlockedState,
  NudgeSelectedPanel = NudgeSelectedPanel,
  CreatePurpleOutline = CreatePurpleOutline,
  ApplySkinPreset = ApplySkinPreset,
  ApplyHeaderBackground = ApplyHeaderBackground,
  EnsureGradientTables = EnsureGradientTables,
  LISTS_PER_PAGE = LISTS_PER_PAGE,
  GetSortedPanelNames = GetSortedPanelNames,
  GetListPageForValue = GetListPageForValue,
  ClampListPage = ClampListPage,
  MakeListButton = MakeListButton,
  MakeSimpleListButton = MakeSimpleListButton,
  MakeProfileButton = MakeProfileButton,
  SelectPanel = SelectPanel,
  UpdatePagerControls = UpdatePagerControls,
  ListBackgroundCategories = ListBackgroundCategories,
  FindCategoryIndex = FindCategoryIndex,
  FindBackgroundCategoryGroupIndex = FindBackgroundCategoryGroupIndex,
  EnsureBackgroundCategoryGroups = EnsureBackgroundCategoryGroups,
  BuildVisibleBackgroundCategoryRows = BuildVisibleBackgroundCategoryRows,
  GetBackgroundCategoryFocusKey = GetBackgroundCategoryFocusKey,
  ToggleBackgroundCategoryGroupCollapsed = ToggleBackgroundCategoryGroupCollapsed,
  ConfigureBackgroundCategoryGroupButton = ConfigureBackgroundCategoryGroupButton,
  ClearBackgroundCategoryDragState = ClearBackgroundCategoryDragState,
  BeginBackgroundCategoryGroupDrag = BeginBackgroundCategoryGroupDrag,
  BeginBackgroundCategoryDrag = BeginBackgroundCategoryDrag,
  SelectBackground = SelectBackground,
  GetSortedBackgroundNames = GetSortedBackgroundNames,
  ListBackgrounds = ListBackgrounds,
  FindProfileGroupIndex = FindProfileGroupIndex,
  EnsureProfileGroups = EnsureProfileGroups,
  BuildVisibleProfileRows = BuildVisibleProfileRows,
  GetProfileFocusKey = GetProfileFocusKey,
  ToggleProfileGroupCollapsed = ToggleProfileGroupCollapsed,
  ConfigureProfileGroupButton = ConfigureProfileGroupButton,
  BeginProfileGroupDrag = BeginProfileGroupDrag,
  ClearProfileGroupDragState = ClearProfileGroupDragState,
  InvalidateSortedProfileNames = InvalidateSortedProfileNames,
  InvalidateSortedPanelNames = InvalidateSortedPanelNames,
  ApplyDropdownLimits = ApplyDropdownLimits,
  ADDON_VERSION = ADDON_VERSION,
  WOW_CLIENT_VERSION = WOW_CLIENT_VERSION,
  LOGO_TARGET_W = LOGO_TARGET_W,
  LOGO_TARGET_H = LOGO_TARGET_H,
  LogSilent = LogSilent,
}

local function CreateEditor()
  local deps = EDITOR_CREATE_DEPS
  local RootDB = deps.RootDB
  local DB = deps.DB
  local GetDebugStore = deps.GetDebugStore
  local ReportEditorRuntimeError = deps.ReportEditorRuntimeError
  local SetupEditorFrameShell = deps.SetupEditorFrameShell
  local BindEditorTheme = deps.BindEditorTheme
  local ApplyFrameSkin = deps.ApplyFrameSkin
  local ApplyEditorState = deps.ApplyEditorState
  local ApplyEditorScale = deps.ApplyEditorScale
  local SaveEditorState = deps.SaveEditorState
  local ApplyEditorLogo = deps.ApplyEditorLogo
  local SetAllUnlocked = deps.SetAllUnlocked
  local GetPanelsUnlockedState = deps.GetPanelsUnlockedState
  local NudgeSelectedPanel = deps.NudgeSelectedPanel
  local CreatePurpleOutline = deps.CreatePurpleOutline
  local ApplySkinPreset = deps.ApplySkinPreset
  local ApplyHeaderBackground = deps.ApplyHeaderBackground
  local EnsureGradientTables = deps.EnsureGradientTables
  local LISTS_PER_PAGE = deps.LISTS_PER_PAGE
  local GetSortedPanelNames = deps.GetSortedPanelNames
  local GetListPageForValue = deps.GetListPageForValue
  local ClampListPage = deps.ClampListPage
  local MakeListButton = deps.MakeListButton
  local MakeSimpleListButton = deps.MakeSimpleListButton
  local MakeProfileButton = deps.MakeProfileButton
  local SelectPanel = deps.SelectPanel
  local UpdatePagerControls = deps.UpdatePagerControls
  local ListBackgroundCategories = deps.ListBackgroundCategories
  local FindCategoryIndex = deps.FindCategoryIndex
  local FindBackgroundCategoryGroupIndex = deps.FindBackgroundCategoryGroupIndex
  local EnsureBackgroundCategoryGroups = deps.EnsureBackgroundCategoryGroups
  local BuildVisibleBackgroundCategoryRows = deps.BuildVisibleBackgroundCategoryRows
  local GetBackgroundCategoryFocusKey = deps.GetBackgroundCategoryFocusKey
  local ToggleBackgroundCategoryGroupCollapsed = deps.ToggleBackgroundCategoryGroupCollapsed
  local ConfigureBackgroundCategoryGroupButton = deps.ConfigureBackgroundCategoryGroupButton
  local ClearBackgroundCategoryDragState = deps.ClearBackgroundCategoryDragState
  local BeginBackgroundCategoryGroupDrag = deps.BeginBackgroundCategoryGroupDrag
  local BeginBackgroundCategoryDrag = deps.BeginBackgroundCategoryDrag
  local SelectBackground = deps.SelectBackground
  local GetSortedBackgroundNames = deps.GetSortedBackgroundNames
  local ListBackgrounds = deps.ListBackgrounds
  local FindProfileGroupIndex = deps.FindProfileGroupIndex
  local EnsureProfileGroups = deps.EnsureProfileGroups
  local BuildVisibleProfileRows = deps.BuildVisibleProfileRows
  local GetProfileFocusKey = deps.GetProfileFocusKey
  local ToggleProfileGroupCollapsed = deps.ToggleProfileGroupCollapsed
  local ConfigureProfileGroupButton = deps.ConfigureProfileGroupButton
  local BeginProfileGroupDrag = deps.BeginProfileGroupDrag
  local ClearProfileGroupDragState = deps.ClearProfileGroupDragState
  local InvalidateSortedProfileNames = deps.InvalidateSortedProfileNames
  local InvalidateSortedPanelNames = deps.InvalidateSortedPanelNames
  local ApplyDropdownLimits = deps.ApplyDropdownLimits
  local ADDON_VERSION = deps.ADDON_VERSION
  local WOW_CLIENT_VERSION = deps.WOW_CLIENT_VERSION
  local LOGO_TARGET_W = deps.LOGO_TARGET_W
  local LOGO_TARGET_H = deps.LOGO_TARGET_H
  local LogSilent = deps.LogSilent

  if _G.LIDPANELS_Editor and _G.LIDPANELS_Editor.__lidPanelsEditor then
    editor = _G.LIDPANELS_Editor
    return
  end
  if editor and editor.GetName and editor:GetName() == "LIDPANELS_Editor" then return end

  editor = CreateFrame("Frame", "LIDPANELS_Editor", UIParent, "BackdropTemplate")
  editor.__lidPanelsEditor = true
  _G.LIDPANELS_Editor = editor
  editor:SetFrameStrata("DIALOG")
  editor:SetClampedToScreen(true)
  editor:SetClipsChildren(true)
  editor:Hide()

  local function DisplayProfileNameLocal(name)
    if not name or name == "" then return "(none)" end
    if name == "Default" then return "No active Profile :(" end
    return name
  end

  local buildTutorialPage = _G.LIDPANELS_BuildTutorialPage
  local shell = SetupEditorFrameShell(editor, {
    RootDB = RootDB,
    ApplyFrameSkin = ApplyFrameSkin,
    ApplyEditorState = ApplyEditorState,
    ApplyEditorScale = ApplyEditorScale,
    SaveEditorState = SaveEditorState,
    ApplyEditorLogo = ApplyEditorLogo,
    SetAllUnlocked = SetAllUnlocked,
    GetPanelsUnlockedState = GetPanelsUnlockedState,
    NudgeSelectedPanel = NudgeSelectedPanel,
    CreatePurpleOutline = CreatePurpleOutline,
    DisplayProfileName = DisplayProfileNameLocal,
    BuildTutorialPage = buildTutorialPage,
    ADDON_VERSION = ADDON_VERSION,
    WOW_CLIENT_VERSION = WOW_CLIENT_VERSION,
    LOGO_TARGET_W = LOGO_TARGET_W,
    LOGO_TARGET_H = LOGO_TARGET_H,
    LogSilent = LogSilent,
  })

  local panelsPage = shell.panelsPage
  local profilesPage = shell.profilesPage
  local backgroundsPage = shell.backgroundsPage
  local uiPage = shell.uiPage
  local tutorialPage = shell.tutorialPage
  local aboutPage = shell.aboutPage

  BindEditorTheme(editor, {
    RootDB = RootDB,
    ApplySkinPreset = ApplySkinPreset,
    ApplyEditorLogo = ApplyEditorLogo,
    ApplyHeaderBackground = ApplyHeaderBackground,
  })
  
  function editor:UpdateBgPreviewLayout()
    if not self.bgPreviewFrame or not self.bgPreviewTex then return end
    local path = self.bgPreviewTex.__path
    if not path or path == "" then
      self.bgPreviewTex:SetAllPoints(true)
      return
    end
    local frameW = self.bgPreviewFrame:GetWidth() or 0
    local frameH = self.bgPreviewFrame:GetHeight() or 0
    if frameW <= 1 or frameH <= 1 then return end

    local texW, texH
    if C_Texture and C_Texture.GetFileIDFromPath and C_Texture.GetTextureInfo then
      local fileID = C_Texture.GetFileIDFromPath(path)
      if fileID then
        local w, h = C_Texture.GetTextureInfo(fileID)
        if type(w) == "table" then
          texW = w.width
          texH = w.height
        else
          texW = w
          texH = h
        end
      end
    end

    if not texW or not texH or texW <= 0 or texH <= 0 then
      self.bgPreviewTex:ClearAllPoints()
      self.bgPreviewTex:SetAllPoints(true)
      return
    end

    local scaleW = frameW / texW
    local scaleH = frameH / texH
    local scale = math.min(scaleW, scaleH)
    local drawW = math.floor(texW * scale)
    local drawH = math.floor(texH * scale)

    self.bgPreviewTex:ClearAllPoints()
    self.bgPreviewTex:SetPoint("CENTER", 0, 0)
    self.bgPreviewTex:SetSize(drawW, drawH)
  end
  local buildPanelsPage = _G.LIDPANELS_BuildPanelsPage
  local buildBackgroundsPage = _G.LIDPANELS_BuildBackgroundsPage
  local buildProfilesPage = _G.LIDPANELS_BuildProfilesPage
  local buildUIPage = _G.LIDPANELS_BuildUIPage
  local buildAboutPage = _G.LIDPANELS_BuildAboutPage
  if buildPanelsPage then buildPanelsPage(editor, panelsPage) end
  if buildBackgroundsPage then buildBackgroundsPage(editor, backgroundsPage) end
  if buildProfilesPage then buildProfilesPage(editor, profilesPage) end
  if buildUIPage then buildUIPage(editor, uiPage) end
  if buildAboutPage then buildAboutPage(editor, aboutPage) end

  function editor:RefreshAbout()
    if not self.aboutMeta then return end
    local root = RootDB()
    local debugStore = GetDebugStore()

    self.aboutMeta:SetText(
      "|cffa87ae8Title:|r LidPanels\n" ..
      "|cffa87ae8Version:|r " .. ADDON_VERSION .. "\n" ..
      "|cffa87ae8Author:|r Lidian\n" ..
      "|cffa87ae8WoW Client:|r " .. WOW_CLIENT_VERSION
    )
    if self.aboutDesc then
      self.aboutDesc:SetText([[|cffa87ae8About the creator:|r
My Name is Lidian and I'am playing World of Warcraft since mid-end Classic. The real hook came with Legion when M+ has been added to the game and brought a challenge next to raiding. Altough my interest in the game is focusing on M+ and Raid, I have an affection for a unique personal UI. 


KGPanels used to be one of my favorite AddOns when it comes down to creating and designing your own personal UI. Altough it never got any update for years, it just kept working throughout multiple patches. With the recent World of Warcraft - Midnight changes, unfortunately one of my favorite AddOns stopped working. Since the AddOn never recieved any updates, I didn't know if the Creator would come back. 


This is how LidPanels came to life. 


I hope you enjoy this AddOn as much as I do.]])
    end

    local hasErrors = debugStore and debugStore.errors and #debugStore.errors > 0
    if not hasErrors then
      self.aboutStatusIcon:SetTexture("Interface/Buttons/UI-CheckBox-Check")
      self.aboutStatusIcon:SetVertexColor(0.2, 0.9, 0.2, 1)
      self.aboutStatusText:SetText("No errors found, yeayyyy :)")
      self.aboutStatusText:SetTextColor(0.2, 0.9, 0.2, 1)
    else
      self.aboutStatusIcon:SetTexture("Interface/RAIDFRAME/ReadyCheck-NotReady")
      self.aboutStatusIcon:SetVertexColor(1, 0.2, 0.2, 1)
      self.aboutStatusText:SetText("Errors detected. See details below.")
      self.aboutStatusText:SetTextColor(1, 0.2, 0.2, 1)
    end

    local infoLines = {}
    local okIcon = "|TInterface/RAIDFRAME/ReadyCheck-Ready:14:14:0:0|t"
    local failIcon = "|TInterface/RAIDFRAME/ReadyCheck-NotReady:14:14:0:0|t"
    local function iconFor(msg)
      local l = string.lower(tostring(msg or ""))
      if l:find("fail", 1, true) or l:find("error", 1, true) or l:find("missing", 1, true) then
        return failIcon
      end
      return okIcon
    end
    if debugStore and debugStore.info then
      for _, msg in ipairs(debugStore.info) do
        infoLines[#infoLines + 1] = iconFor(msg) .. " " .. tostring(msg)
      end
    end
    if #infoLines == 0 then infoLines[1] = okIcon .. " No status messages yet." end
    self.aboutDetails:SetText(table.concat(infoLines, "\n"))

    local errCount = (debugStore and debugStore.errors and #debugStore.errors) or 0
    if errCount == 0 then
      self.aboutErrors:SetText("No errors captured.")
    else
      self.aboutErrors:SetText("Errors captured: " .. tostring(errCount))
    end

    local silentCount = (debugStore and debugStore.silent and #debugStore.silent) or 0
    if self.aboutSilent then
      local devMode = (root and root.ui and root.ui.developerMode) == true
      if devMode then
        if silentCount == 0 then
          self.aboutSilent:SetText("No silent errors captured.\n\nNote: Silent-error tracing is active only while Developer Mode is enabled.")
        else
          self.aboutSilent:SetText("Silent errors captured: " .. tostring(silentCount) .. "\n\nNote: Silent-error tracing is active only while Developer Mode is enabled.")
        end
      else
        self.aboutSilent:SetText("Developer Mode is OFF.\nSilent-error tracing is currently disabled.")
      end
    end
  end
  aboutPage.__contentHeight = 600

  local RefreshPanelsSection
  local RefreshPanelPropertiesSection
  local RefreshBackgroundsSection
  local RefreshBackgroundListDetailsSection
  local RefreshProfilesSection
  local RefreshProfileDetailsSection

  editor.AcquirePooledButton = function(_, pool, index, creator)
    local button = pool[index]
    if not button then
      button = creator()
      pool[index] = button
    end
    return button
  end

  editor.PreparePooledListButton = function(_, button, text, y, onClick, opts)
    if not button then return nil end
    opts = opts or {}
    button:ClearAllPoints()
    button:SetPoint("TOPLEFT", opts.x or 0, y or 0)
    button:SetSize(opts.width or 200, opts.height or 20)
    button:SetText(text or "")
    button:SetScript("OnClick", onClick)
    button:Show()

    local fs = button:GetFontString()
    if fs then
      fs:SetTextColor(1, 1, 1, 1)
    end

    if button.__glow then button.__glow:Hide() end
    if button.__selectedBorder then button.__selectedBorder:Hide() end
    if button.__lockIcon then button.__lockIcon:Hide() end
    if button.__activeIcon then button.__activeIcon:Hide() end
    if button.__activeBorder then button.__activeBorder:Hide() end
    if button.__profileDragDropBorder then button.__profileDragDropBorder:Hide() end
    if button.__profileDragSourceBorder then button.__profileDragSourceBorder:Hide() end
    if button.__bgDragDropBorder then button.__bgDragDropBorder:Hide() end
    if button.__bgDragSourceBorder then button.__bgDragSourceBorder:Hide() end
    if button.__bgCategoryGroupDragDropBorder then button.__bgCategoryGroupDragDropBorder:Hide() end
    if button.__bgCategoryGroupDragSourceBorder then button.__bgCategoryGroupDragSourceBorder:Hide() end

    return button
  end

  editor.RestoreProfileCurrentLabel = function(self)
    if not self or not self.profileLabel then
      return
    end
    local root = RootDB()
    self.profileLabel:SetText("Current Profile: " .. DisplayProfileNameLocal(root.currentProfile or "Default"))
  end

  editor.RestoreBackgroundsCurrentLabel = function(self)
    if not self or not self.backgroundsCurrentLabel then
      return
    end
    local curCat = selectedBackgroundCategory or "(none)"
    self.backgroundsCurrentLabel:SetText("Current Category: " .. curCat)
  end

  local function RefreshPanelsListSection(self, db)
    local order = GetSortedPanelNames(db)
    local panelTotalPages = math.max(1, math.ceil(#order / LISTS_PER_PAGE))
    local selectedPanelPage = GetListPageForValue(order, selectedPanelName, LISTS_PER_PAGE)
    if selectedPanelName ~= self.__lastPagedSelectedPanelName and selectedPanelPage then
      self.panelListPage = selectedPanelPage
    end
    self.panelListPage = ClampListPage(self.panelListPage or 1, panelTotalPages)
    self.__lastPagedSelectedPanelName = selectedPanelName
    local panelStartIndex = ((self.panelListPage - 1) * LISTS_PER_PAGE) + 1
    local panelEndIndex = math.min(#order, panelStartIndex + LISTS_PER_PAGE - 1)

    self.listButtonPool = self.listButtonPool or {}
    self.listButtons = self.listButtons or {}
    for _, b in ipairs(self.listButtonPool) do b:Hide() end
    wipe(self.listButtons)

    local y = 0
    local usedButtons = 0
    for i = panelStartIndex, panelEndIndex do
      local name = order[i]
      usedButtons = usedButtons + 1
      local b = self:AcquirePooledButton(self.listButtonPool, usedButtons, function()
        return MakeListButton(self.listContent, name, y, function() SelectPanel(name) end)
      end)
      self:PreparePooledListButton(b, name, y, function() SelectPanel(name) end)
      local p = db.panels[name]
      if name == selectedPanelName then
        b:GetFontString():SetTextColor(0.6, 0.9, 1.0)
      end
      if b.__glow then b.__glow:SetShown(name == selectedPanelName) end
      if b.__selectedBorder then
        b.__selectedBorder:SetShown(name == selectedPanelName and GetPanelsUnlockedState() and p and not p.locked)
      end
      if b.__lockIcon then
        if p and p.locked then
          b.__lockIcon:SetTexture("Interface/Buttons/LockButton-Locked-Up")
          b.__lockIcon:SetVertexColor(1.0, 0.25, 0.25, 1)
        else
          b.__lockIcon:SetTexture("Interface/Buttons/LockButton-Unlocked-Up")
          b.__lockIcon:SetVertexColor(0.2, 1.0, 0.2, 1)
        end
        b.__lockIcon:Show()
      end
      self.listButtons[#self.listButtons+1] = b
      y = y - 22
    end
    for i = usedButtons + 1, #self.listButtonPool do
      self.listButtonPool[i]:Hide()
    end
    self.listContent:SetHeight(math.max(1, -y))
    if self.listScroll and self.listScroll.UpdateScrollChildRect then
      self.listScroll:UpdateScrollChildRect()
    end
    if self.listScroll then self.listScroll:SetVerticalScroll(0) end
    UpdatePagerControls(self.panelPrevBtn, self.panelNextBtn, self.panelPageText, self.panelListPage, panelTotalPages)
    if self.delBtn then
      self.delBtn:SetEnabled(selectedPanelName ~= nil and selectedPanelName ~= "")
    end
  end

  local function RefreshPanelPropertiesState(self, root, db)
    if selectedPanelName and db.panels[selectedPanelName] then
      if self.propsScroll then self.propsScroll:Show() end
      if self.noPanelScroll then self.noPanelScroll:Hide() end
      if self.noPanelFrame then self.noPanelFrame:Hide() end
      local p = db.panels[selectedPanelName]
      p.color = p.color or { 0.05, 0.05, 0.07, 0.8 }
      self.selLabel:SetText("Selected: " .. selectedPanelName)

      self.wSlider:SetValueSilent(p.w or 300)
      self.hSlider:SetValueSilent(p.h or 120)

      local pos = db.positions[selectedPanelName]
      self.xSlider:SetValueSilent(pos and pos.x or 0)
      self.ySlider:SetValueSilent(pos and pos.y or 0)

      if self.borderDrop and self.borderOptions then
        local bval = p.border or "tooltip"
        local btext = bval
        for _, opt in ipairs(self.borderOptions) do
          if opt.value == bval then btext = opt.text break end
        end
        UIDropDownMenu_SetSelectedValue(self.borderDrop, bval)
        UIDropDownMenu_SetText(self.borderDrop, btext)
      end
      if self.lockCheck then
        self.lockCheck:SetChecked(p.locked and true or false)
      end
      do
        local showEditMode = not (p.locked and true or false)
        local editMode = ((root.ui and root.ui.panelEditMode) == "PREVIEW") and "PREVIEW" or "EDITOR"
        if self.editModeBox then self.editModeBox:SetShown(showEditMode) end
        if self.editModeLabel then self.editModeLabel:SetShown(showEditMode) end
        if self.editModeEditorCheck then
          self.editModeEditorCheck:SetShown(showEditMode)
          self.editModeEditorCheck:SetChecked(editMode == "EDITOR")
        end
        if self.editModePreviewCheck then
          self.editModePreviewCheck:SetShown(showEditMode)
          self.editModePreviewCheck:SetChecked(editMode == "PREVIEW")
        end
      end

      if p.gradient then
        EnsureGradientTables(p)
        self.alphaSlider:SetValue(p.gradient.c1[4] or 0.8)
      else
        self.alphaSlider:SetValue((p.color and p.color[4]) or 0.8)
      end
      if self.UpdateColorDesignUI then self:UpdateColorDesignUI() end
    else
      if self.propsScroll then self.propsScroll:Hide() end
      if self.noPanelScroll then self.noPanelScroll:Show() end
      if self.noPanelFrame then self.noPanelFrame:Show() end
      if self.LayoutNoPanel then self:LayoutNoPanel() end
      self.selLabel:SetText("Selected: (none)")
      if self.lockCheck then
        self.lockCheck:SetChecked(false)
      end
      if self.editModeBox then self.editModeBox:Hide() end
      if self.editModeLabel then self.editModeLabel:Hide() end
      if self.editModeEditorCheck then self.editModeEditorCheck:Hide() end
      if self.editModePreviewCheck then self.editModePreviewCheck:Hide() end
      if self.UpdateColorDesignUI then self:UpdateColorDesignUI() end
    end
  end

  RefreshPanelsSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      local db = DB()
      self.__refreshing = true
      RefreshPanelsListSection(self, db)
      RefreshPanelPropertiesState(self, root, db)
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Panels Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  editor.RefreshPanelsListSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local db = DB()
      self.__refreshing = true
      RefreshPanelsListSection(self, db)
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Panel List Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  RefreshPanelPropertiesSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      local db = DB()
      self.__refreshing = true
      RefreshPanelPropertiesState(self, root, db)
      self.__refreshing = false
    end, function(err)
      ReportEditorRuntimeError("Editor Panel Properties Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  local function RefreshBackgroundsSectionState(self)
    if self.backgroundsCurrentLabel then
      local curCat = selectedBackgroundCategory or "(none)"
      self.backgroundsCurrentLabel:SetText("Current Category: " .. curCat)
    end
    if selectedBackgroundCategory and not FindCategoryIndex(ListBackgroundCategories(), selectedBackgroundCategory) then
      selectedBackgroundCategory = nil
      selectedBackgroundName = nil
    end
    if selectedBackgroundCategoryGroup and not FindBackgroundCategoryGroupIndex(EnsureBackgroundCategoryGroups(), selectedBackgroundCategoryGroup) then
      selectedBackgroundCategoryGroup = nil
    end
    if (not self.__skipBackgroundCategoryListRebuild) and self.bgCatButtons then
      self.bgCatGroupButtonPool = self.bgCatGroupButtonPool or {}
      self.bgCatItemButtonPool = self.bgCatItemButtonPool or {}
      self.bgCatButtons = self.bgCatButtons or {}
      self.bgCatGroupButtons = self.bgCatGroupButtons or {}
      self.bgCatDropButtons = self.bgCatDropButtons or {}
      for _, b in ipairs(self.bgCatGroupButtonPool) do b:Hide() end
      for _, b in ipairs(self.bgCatItemButtonPool) do b:Hide() end
      wipe(self.bgCatButtons)
      wipe(self.bgCatGroupButtons)
      wipe(self.bgCatDropButtons)
      local catRows = BuildVisibleBackgroundCategoryRows(selectedBackgroundCategory)
      if not selectedBackgroundCategory and not selectedBackgroundCategoryGroup and catRows[1] then
        local firstGroupName = nil
        for _, row in ipairs(catRows) do
          if row and row.type == "category" then
            selectedBackgroundCategory = row.name
            break
          elseif row and row.type == "group" and not firstGroupName then
            firstGroupName = row.name
          end
        end
        if not selectedBackgroundCategory and firstGroupName then
          selectedBackgroundCategoryGroup = firstGroupName
        end
      end
      local catFocusKey = GetBackgroundCategoryFocusKey(selectedBackgroundCategory, selectedBackgroundCategoryGroup)
      local bgCatTotalPages = math.max(1, math.ceil(#catRows / LISTS_PER_PAGE))
      local selectedCatPage = GetListPageForValue(catRows, catFocusKey, LISTS_PER_PAGE, function(row)
        return row and row.key
      end)
      if catFocusKey ~= self.__lastPagedBackgroundCategoryKey and selectedCatPage then
        self.bgCatPage = selectedCatPage
      end
      self.bgCatPage = ClampListPage(self.bgCatPage or 1, bgCatTotalPages)
      self.__lastPagedBackgroundCategoryKey = catFocusKey
      local bgCatStartIndex = ((self.bgCatPage - 1) * LISTS_PER_PAGE) + 1
      local bgCatEndIndex = math.min(#catRows, bgCatStartIndex + LISTS_PER_PAGE - 1)
      local yc = 0
      local usedGroupButtons = 0
      local usedItemButtons = 0
      for i = bgCatStartIndex, bgCatEndIndex do
        local row = catRows[i]
        if row and row.type == "group" then
          local label = string.format("%s (%d)", row.name or "Group", row.childCount or 0)
          usedGroupButtons = usedGroupButtons + 1
          local b = self:AcquirePooledButton(self.bgCatGroupButtonPool, usedGroupButtons, function()
            return MakeSimpleListButton(self.bgCatContent, label, yc, function()
              selectedBackgroundCategoryGroup = row.name
              selectedBackgroundCategory = nil
              selectedBackgroundName = nil
              RefreshBackgroundsSection(self)
            end)
          end)
          self:PreparePooledListButton(b, label, yc, function()
            selectedBackgroundCategoryGroup = row.name
            selectedBackgroundCategory = nil
            selectedBackgroundName = nil
            RefreshBackgroundsSection(self)
          end)
          b.__backgroundCategoryGroupName = row.name
          b.__bgCategoryGroupDragDropBorder = b.__bgCategoryGroupDragDropBorder or CreatePurpleOutline(b)
          b.__bgCategoryGroupDragDropBorder:Hide()
          b.__lid_customBg = {0.1804, 0.1216, 0.2863, 0.98}
          b.__lid_customHover = {0.72, 0.86, 1.00, 0.24}
          local isSelectedGroup = (selectedBackgroundCategoryGroup == row.name)
          local groupTextColor
          if isSelectedGroup or row.containsSelected then
            groupTextColor = {0.6, 0.9, 1.0, 1.0}
          else
            groupTextColor = {0.92, 0.82, 1.0, 1.0}
          end
          b.__lid_customText = groupTextColor
          b.__onToggleBackgroundCategoryGroup = function()
            ToggleBackgroundCategoryGroupCollapsed(row.name)
            selectedBackgroundCategoryGroup = row.name
            selectedBackgroundCategory = nil
            selectedBackgroundName = nil
            RefreshBackgroundsSection(self)
          end
          ConfigureBackgroundCategoryGroupButton(b, label, row.collapsed)
          if b.__bgCategoryGroupArrow then
            b.__bgCategoryGroupArrow:SetTextColor(groupTextColor[1], groupTextColor[2], groupTextColor[3], groupTextColor[4] or 1)
          end
          if b.__glow then b.__glow:SetShown(isSelectedGroup or (row.containsSelected and row.collapsed)) end
          self.bgCatButtons[#self.bgCatButtons + 1] = b
          self.bgCatGroupButtons[#self.bgCatGroupButtons + 1] = b
        elseif row and row.type == "category" then
          local name = row.name
          local label = row.groupName and ("  " .. name) or name
          usedItemButtons = usedItemButtons + 1
          local b = self:AcquirePooledButton(self.bgCatItemButtonPool, usedItemButtons, function()
            return MakeSimpleListButton(self.bgCatContent, label, yc, function()
              selectedBackgroundCategory = name
              selectedBackgroundCategoryGroup = nil
              selectedBackgroundName = nil
              RefreshBackgroundsSection(self)
            end)
          end)
          self:PreparePooledListButton(b, label, yc, function()
            selectedBackgroundCategory = name
            selectedBackgroundCategoryGroup = nil
            selectedBackgroundName = nil
            RefreshBackgroundsSection(self)
          end, {
            x = row.groupName and 16 or 0,
            width = row.groupName and 184 or 200,
          })
          b.__backgroundCategoryName = name
          b.__bgDragDropBorder = b.__bgDragDropBorder or CreatePurpleOutline(b)
          b.__bgDragDropBorder:Hide()
          b.__bgCategoryGroupDragSourceBorder = b.__bgCategoryGroupDragSourceBorder or CreatePurpleOutline(b)
          b.__bgCategoryGroupDragSourceBorder:Hide()
          b:RegisterForDrag("LeftButton")
          b:SetScript("OnDragStart", function(button)
            BeginBackgroundCategoryGroupDrag(name, row.groupName, button)
          end)

          if name == selectedBackgroundCategory then
            b:GetFontString():SetTextColor(0.6, 0.9, 1.0)
          end
          if b.__glow then b.__glow:SetShown(name == selectedBackgroundCategory) end
          self.bgCatButtons[#self.bgCatButtons + 1] = b
          self.bgCatDropButtons[#self.bgCatDropButtons + 1] = b
        end
        yc = yc - 22
      end
      for i = usedGroupButtons + 1, #self.bgCatGroupButtonPool do
        self.bgCatGroupButtonPool[i]:Hide()
      end
      for i = usedItemButtons + 1, #self.bgCatItemButtonPool do
        self.bgCatItemButtonPool[i]:Hide()
      end
      if self.bgCatContent then
        self.bgCatContent:SetHeight(math.max(1, -yc))
      end
      if self.bgCatScroll and self.bgCatScroll.UpdateScrollChildRect then
        self.bgCatScroll:UpdateScrollChildRect()
      end
      if self.bgCatScroll then self.bgCatScroll:SetVerticalScroll(0) end
      UpdatePagerControls(self.bgCatPrevBtn, self.bgCatNextBtn, self.bgCatPageText, self.bgCatPage, bgCatTotalPages)
      if self.bgDeleteCatBtn then
        self.bgDeleteCatBtn:SetEnabled(selectedBackgroundCategory ~= nil and selectedBackgroundCategory ~= "")
      end
      self.bgSelectedCategoryGroupName = selectedBackgroundCategoryGroup
    end

    if not self.__skipBackgroundListRebuild and self.bgListButtons then
      for _, b in ipairs(self.bgListButtons) do b:Hide() end
      wipe(self.bgListButtons)
      local list = ListBackgrounds and ListBackgrounds(selectedBackgroundCategory) or {}
      local sortedList = GetSortedBackgroundNames(list)
      local bgListTotalPages = math.max(1, math.ceil(#sortedList / LISTS_PER_PAGE))
      local selectedBgPage = GetListPageForValue(sortedList, selectedBackgroundName, LISTS_PER_PAGE)
      if selectedBackgroundName ~= self.__lastPagedBackgroundName and selectedBgPage then
        self.bgListPage = selectedBgPage
      end
      self.bgListPage = ClampListPage(self.bgListPage or 1, bgListTotalPages)
      self.__lastPagedBackgroundName = selectedBackgroundName
      local bgListStartIndex = ((self.bgListPage - 1) * LISTS_PER_PAGE) + 1
      local bgListEndIndex = math.min(#sortedList, bgListStartIndex + LISTS_PER_PAGE - 1)
      local yb = 0
      for i = bgListStartIndex, bgListEndIndex do
        local name = sortedList[i]
        local b = MakeSimpleListButton(self.bgListContent, name, yb, function() SelectBackground(name) end)
        b.__backgroundItemName = name
        b.__bgDragSourceBorder = b.__bgDragSourceBorder or CreatePurpleOutline(b)
        b.__bgDragSourceBorder:Hide()
        b:RegisterForDrag("LeftButton")
        b:SetScript("OnDragStart", function(button)
          BeginBackgroundCategoryDrag(name, selectedBackgroundCategory, button)
        end)
        if name == selectedBackgroundName then
          b:GetFontString():SetTextColor(0.6, 0.9, 1.0)
        end
        if b.__glow then b.__glow:SetShown(name == selectedBackgroundName) end
        self.bgListButtons[#self.bgListButtons + 1] = b
        yb = yb - 22
      end
      if self.bgListContent then
        self.bgListContent:SetHeight(math.max(1, -yb))
      end
      if self.bgListScroll and self.bgListScroll.UpdateScrollChildRect then
        self.bgListScroll:UpdateScrollChildRect()
      end
      if self.bgListScroll then self.bgListScroll:SetVerticalScroll(0) end
      UpdatePagerControls(self.bgPrevBtn, self.bgNextBtn, self.bgPageText, self.bgListPage, bgListTotalPages)
      if self.bgDeleteListBtn then
        self.bgDeleteListBtn:SetEnabled(selectedBackgroundName ~= nil and selectedBackgroundName ~= "")
      end
    end

    if self.__skipBackgroundDetailsRebuild then
      return
    end

    if self.bgSelLabel then
      if selectedBackgroundName then
        self.bgSelLabel:SetText("Selected: " .. selectedBackgroundName)
        if self.bgSourceText then
          self.bgSourceText:SetText("Source:\nAddOns/LidPanels_UserMedia/Your_Backgrounds/" .. selectedBackgroundName .. ".tga")
          self.bgSourceText:Show()
        end
        if self.bgPreviewTex then
          local path = (GetBackgroundPath and GetBackgroundPath(selectedBackgroundName))
            or ("Interface\\AddOns\\LidPanels_UserMedia\\Your_Backgrounds\\" .. selectedBackgroundName .. ".tga")
          local hasTex = true
          if C_Texture and C_Texture.GetFileIDFromPath then
            hasTex = (C_Texture.GetFileIDFromPath(path) ~= nil)
          end
          if hasTex then
            self.bgPreviewTex:SetTexture(path)
            self.bgPreviewTex.__path = path
          else
            self.bgPreviewTex:SetTexture(nil)
            self.bgPreviewTex.__path = nil
          end
        end
        if self.bgSourceText then self.bgSourceText:Show() end
        if self.bgReloadNote then self.bgReloadNote:Show() end
        if self.bgPreviewFrame then self.bgPreviewFrame:Show() end
        if self.bgEmptyText then self.bgEmptyText:Hide() end
        if self.bgRenameBtn then self.bgRenameBtn:Show(); self.bgRenameBtn:Enable() end
        if self.bgDeleteBtn then self.bgDeleteBtn:Show(); self.bgDeleteBtn:Enable() end
        if self.bgRenameCatBtn then self.bgRenameCatBtn:Hide(); self.bgRenameCatBtn:Disable() end
        if self.bgDeleteCatPropsBtn then self.bgDeleteCatPropsBtn:Hide(); self.bgDeleteCatPropsBtn:Disable() end
        if self.bgRenameCatGroupBtn then self.bgRenameCatGroupBtn:Hide(); self.bgRenameCatGroupBtn:Disable() end
        if self.bgDeleteCatGroupPropsBtn then self.bgDeleteCatGroupPropsBtn:Hide(); self.bgDeleteCatGroupPropsBtn:Disable() end
        if self.UpdateBgPreviewLayout then self:UpdateBgPreviewLayout() end
        if self.UpdateBgPropsLayout then self:UpdateBgPropsLayout() end
      elseif selectedBackgroundCategoryGroup then
        self.bgSelLabel:SetText("Selected Group: " .. selectedBackgroundCategoryGroup)
        if self.bgSourceText then self.bgSourceText:SetText("Source:\nAddOns/LidPanels_UserMedia/Your_Backgrounds/"); self.bgSourceText:Hide() end
        if self.bgReloadNote then self.bgReloadNote:Hide() end
        if self.bgPreviewTex then self.bgPreviewTex:SetTexture(nil) end
        if self.bgPreviewTex then self.bgPreviewTex.__path = nil end
        if self.bgPreviewFrame then self.bgPreviewFrame:Hide() end
        if self.bgEmptyText then
          self.bgEmptyText:SetText("Open the group or drag categories into it.\nDeleting the group keeps all categories and makes them groupless.")
          self.bgEmptyText:Show()
        end
        if self.bgRenameBtn then self.bgRenameBtn:Hide(); self.bgRenameBtn:Disable() end
        if self.bgDeleteBtn then self.bgDeleteBtn:Hide(); self.bgDeleteBtn:Disable() end
        if self.bgRenameCatBtn then self.bgRenameCatBtn:Hide(); self.bgRenameCatBtn:Disable() end
        if self.bgDeleteCatPropsBtn then self.bgDeleteCatPropsBtn:Hide(); self.bgDeleteCatPropsBtn:Disable() end
        if self.bgRenameCatGroupBtn then self.bgRenameCatGroupBtn:Show(); self.bgRenameCatGroupBtn:Enable() end
        if self.bgDeleteCatGroupPropsBtn then self.bgDeleteCatGroupPropsBtn:Show(); self.bgDeleteCatGroupPropsBtn:Enable() end
        if self.UpdateBgPropsLayout then self:UpdateBgPropsLayout() end
      elseif selectedBackgroundCategory then
        self.bgSelLabel:SetText("Selected Category: " .. selectedBackgroundCategory)
        if self.bgSourceText then self.bgSourceText:SetText("Source:\nAddOns/LidPanels_UserMedia/Your_Backgrounds/"); self.bgSourceText:Hide() end
        if self.bgReloadNote then self.bgReloadNote:Hide() end
        if self.bgPreviewTex then self.bgPreviewTex:SetTexture(nil) end
        if self.bgPreviewTex then self.bgPreviewTex.__path = nil end
        if self.bgPreviewFrame then self.bgPreviewFrame:Hide() end
        if self.bgEmptyText then
          self.bgEmptyText:SetText("Add or select a background.")
          self.bgEmptyText:Show()
        end
        if self.bgRenameBtn then self.bgRenameBtn:Hide(); self.bgRenameBtn:Disable() end
        if self.bgDeleteBtn then self.bgDeleteBtn:Hide(); self.bgDeleteBtn:Disable() end
        if self.bgRenameCatBtn then self.bgRenameCatBtn:Show(); self.bgRenameCatBtn:Enable() end
        if self.bgDeleteCatPropsBtn then self.bgDeleteCatPropsBtn:Show(); self.bgDeleteCatPropsBtn:Enable() end
        if self.bgRenameCatGroupBtn then self.bgRenameCatGroupBtn:Hide(); self.bgRenameCatGroupBtn:Disable() end
        if self.bgDeleteCatGroupPropsBtn then self.bgDeleteCatGroupPropsBtn:Hide(); self.bgDeleteCatGroupPropsBtn:Disable() end
        if self.UpdateBgPropsLayout then self:UpdateBgPropsLayout() end
      else
        self.bgSelLabel:SetText("Selected: (none)")
        if self.bgSourceText then self.bgSourceText:SetText("Source:\nAddOns/LidPanels_UserMedia/Your_Backgrounds/"); self.bgSourceText:Hide() end
        if self.bgReloadNote then self.bgReloadNote:Hide() end
        if self.bgPreviewTex then self.bgPreviewTex:SetTexture(nil) end
        if self.bgPreviewTex then self.bgPreviewTex.__path = nil end
        if self.bgPreviewFrame then self.bgPreviewFrame:Hide() end
        if self.bgEmptyText then
          self.bgEmptyText:SetText("Select or create a category first.")
          self.bgEmptyText:Show()
        end
        if self.bgRenameBtn then self.bgRenameBtn:Hide(); self.bgRenameBtn:Disable() end
        if self.bgDeleteBtn then self.bgDeleteBtn:Hide(); self.bgDeleteBtn:Disable() end
        if self.bgRenameCatBtn then self.bgRenameCatBtn:Hide(); self.bgRenameCatBtn:Disable() end
        if self.bgDeleteCatPropsBtn then self.bgDeleteCatPropsBtn:Hide(); self.bgDeleteCatPropsBtn:Disable() end
        if self.bgRenameCatGroupBtn then self.bgRenameCatGroupBtn:Hide(); self.bgRenameCatGroupBtn:Disable() end
        if self.bgDeleteCatGroupPropsBtn then self.bgDeleteCatGroupPropsBtn:Hide(); self.bgDeleteCatGroupPropsBtn:Disable() end
        if self.UpdateBgPropsLayout then self:UpdateBgPropsLayout() end
      end
    end
  end

  RefreshBackgroundsSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      self.__refreshing = true
      RefreshBackgroundsSectionState(self)
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Backgrounds Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  RefreshBackgroundListDetailsSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      self.__refreshing = true
      self.__skipBackgroundCategoryListRebuild = true
      RefreshBackgroundsSectionState(self)
      self.__skipBackgroundCategoryListRebuild = nil
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Background List/Details Refresh", err)
    end)
    if not ok then
      self.__skipBackgroundCategoryListRebuild = nil
      self.__refreshing = false
    end
  end

  editor.RefreshBackgroundCategoryListSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      self.__refreshing = true
      self.__skipBackgroundListRebuild = true
      self.__skipBackgroundDetailsRebuild = true
      RefreshBackgroundsSectionState(self)
      self.__skipBackgroundListRebuild = nil
      self.__skipBackgroundDetailsRebuild = nil
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Background Category List Refresh", err)
    end)
    if not ok then
      self.__skipBackgroundListRebuild = nil
      self.__skipBackgroundDetailsRebuild = nil
      self.__refreshing = false
    end
  end

  editor.RefreshBackgroundListSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      self.__refreshing = true
      self.__skipBackgroundCategoryListRebuild = true
      self.__skipBackgroundDetailsRebuild = true
      RefreshBackgroundsSectionState(self)
      self.__skipBackgroundCategoryListRebuild = nil
      self.__skipBackgroundDetailsRebuild = nil
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Background List Refresh", err)
    end)
    if not ok then
      self.__skipBackgroundCategoryListRebuild = nil
      self.__skipBackgroundDetailsRebuild = nil
      self.__refreshing = false
    end
  end

  local function RefreshProfileLabelsState(self, root)
    local curProfLabel = DisplayProfileNameLocal(root.currentProfile or "Default")
    if self.profileBarText then
      self.profileBarText:SetText("Current Profile: " .. curProfLabel)
    end
    self.profileLabel:SetText("Current Profile: " .. curProfLabel)
    if self.panelsCurrentLabel then
      self.panelsCurrentLabel:SetText("Current Profile: " .. curProfLabel)
    end
    if self.uiCurrentLabel then
      self.uiCurrentLabel:SetText("Current Profile: " .. curProfLabel)
    end
    if self.tutorialCurrentLabel then
      self.tutorialCurrentLabel:SetText("Current Profile: " .. curProfLabel)
    end
    if self.aboutCurrentLabel then
      self.aboutCurrentLabel:SetText("Current Profile: " .. curProfLabel)
    end
  end

  local function RefreshProfilesSectionState(self, root)
    if self.selectedProfileName == "Default" then
      self.selectedProfileName = nil
    end
    if not self.selectedProfileName and not self.selectedProfileGroupName and root.currentProfile and root.currentProfile ~= "Default" then
      self.selectedProfileName = root.currentProfile
    end
    if self.selectedProfileGroupName and not FindProfileGroupIndex(EnsureProfileGroups(), self.selectedProfileGroupName) then
      self.selectedProfileGroupName = nil
    end

    if not self.__skipProfileListRebuild then
      self.profGroupButtonPool = self.profGroupButtonPool or {}
      self.profItemButtonPool = self.profItemButtonPool or {}
      self.profButtons = self.profButtons or {}
      self.profGroupButtons = self.profGroupButtons or {}
      for _, b in ipairs(self.profGroupButtonPool) do b:Hide() end
      for _, b in ipairs(self.profItemButtonPool) do b:Hide() end
      wipe(self.profButtons)
      wipe(self.profGroupButtons)

      local profilePageFocus = GetProfileFocusKey(self.selectedProfileName, self.selectedProfileGroupName)
      if not profilePageFocus and root.currentProfile and root.currentProfile ~= "Default" then
        profilePageFocus = GetProfileFocusKey(root.currentProfile)
      end
      local profileRows = BuildVisibleProfileRows(root, self.selectedProfileName)
      local selectedProfilePage = GetListPageForValue(profileRows, profilePageFocus, LISTS_PER_PAGE, function(row)
        return row and row.key
      end)
      local profTotalPages = math.max(1, math.ceil(#profileRows / LISTS_PER_PAGE))
      if profilePageFocus ~= self.__lastPagedSelectedProfileKey and selectedProfilePage then
        self.profListPage = selectedProfilePage
      end
      self.profListPage = ClampListPage(self.profListPage or 1, profTotalPages)
      self.__lastPagedSelectedProfileKey = profilePageFocus
      local profStartIndex = ((self.profListPage - 1) * LISTS_PER_PAGE) + 1
      local profEndIndex = math.min(#profileRows, profStartIndex + LISTS_PER_PAGE - 1)
      local py = 0
      local usedProfileGroupButtons = 0
      local usedProfileButtons = 0
      for i = profStartIndex, profEndIndex do
        local row = profileRows[i]
        if row and row.type == "group" then
          local label = string.format("%s (%d)", row.name or "Group", row.childCount or 0)
          usedProfileGroupButtons = usedProfileGroupButtons + 1
          local b = self:AcquirePooledButton(self.profGroupButtonPool, usedProfileGroupButtons, function()
            return MakeSimpleListButton(self.profContent, label, py, function()
              self.selectedProfileGroupName = row.name
              self.selectedProfileName = nil
              RefreshProfilesSection(self)
            end)
          end)
          self:PreparePooledListButton(b, label, py, function()
            self.selectedProfileGroupName = row.name
            self.selectedProfileName = nil
            RefreshProfilesSection(self)
          end)
          b.__profileGroupName = row.name
          b.__profileDragDropBorder = b.__profileDragDropBorder or CreatePurpleOutline(b)
          b.__profileDragDropBorder:Hide()
          b.__lid_customBg = {0.1804, 0.1216, 0.2863, 0.98}
          b.__lid_customHover = {0.72, 0.86, 1.00, 0.24}
          local isSelectedGroup = (self.selectedProfileGroupName == row.name)
          local groupTextColor
          if isSelectedGroup then
            groupTextColor = {0.6, 0.9, 1.0, 1.0}
          elseif row.containsCurrent then
            groupTextColor = {0.7, 1.0, 0.7, 1.0}
          elseif row.containsSelected then
            groupTextColor = {0.6, 0.9, 1.0, 1.0}
          else
            groupTextColor = {0.92, 0.82, 1.0, 1.0}
          end
          b.__lid_customText = groupTextColor
          b.__onToggleProfileGroup = function()
            ToggleProfileGroupCollapsed(row.name)
            self.selectedProfileGroupName = row.name
            self.selectedProfileName = nil
            RefreshProfilesSection(self)
          end
          ConfigureProfileGroupButton(b, label, row.collapsed)
          if b.__groupArrow then
            b.__groupArrow:SetTextColor(groupTextColor[1], groupTextColor[2], groupTextColor[3], groupTextColor[4] or 1)
          end
          if b.__glow then b.__glow:SetShown(isSelectedGroup or (row.containsSelected and row.collapsed)) end
          self.profButtons[#self.profButtons + 1] = b
          self.profGroupButtons[#self.profGroupButtons + 1] = b
        elseif row and row.type == "profile" then
          local pname = row.name
          local label = row.groupName and ("  " .. pname) or pname
          usedProfileButtons = usedProfileButtons + 1
          local b = self:AcquirePooledButton(self.profItemButtonPool, usedProfileButtons, function()
            return MakeProfileButton(self.profContent, label, py, function()
              self.selectedProfileName = pname
              self.selectedProfileGroupName = nil
              RefreshProfilesSection(self)
            end)
          end)
          self:PreparePooledListButton(b, label, py, function()
            self.selectedProfileName = pname
            self.selectedProfileGroupName = nil
            RefreshProfilesSection(self)
          end, {
            x = row.groupName and 16 or 0,
            width = row.groupName and 184 or 200,
          })
          b.__profileDragSourceBorder = b.__profileDragSourceBorder or CreatePurpleOutline(b)
          b.__profileDragSourceBorder:Hide()
          b:RegisterForDrag("LeftButton")
          b:SetScript("OnDragStart", function(button)
            BeginProfileGroupDrag(pname, row.groupName, button)
          end)

          if pname == root.currentProfile then
            b:GetFontString():SetTextColor(0.7, 1.0, 0.7)
          elseif pname == self.selectedProfileName then
            b:GetFontString():SetTextColor(0.6, 0.9, 1.0)
          end
          if b.__glow then b.__glow:SetShown(pname == self.selectedProfileName) end
          if b.__activeIcon then b.__activeIcon:SetShown(pname == root.currentProfile) end
          if b.__activeBorder then b.__activeBorder:SetShown(pname == root.currentProfile) end
          self.profButtons[#self.profButtons + 1] = b
        end
        py = py - 22
      end
      for i = usedProfileGroupButtons + 1, #self.profGroupButtonPool do
        self.profGroupButtonPool[i]:Hide()
      end
      for i = usedProfileButtons + 1, #self.profItemButtonPool do
        self.profItemButtonPool[i]:Hide()
      end
      self.profContent:SetHeight(math.max(1, -py))
      if self.profScroll and self.profScroll.UpdateScrollChildRect then
        self.profScroll:UpdateScrollChildRect()
      end
      if self.profScroll then self.profScroll:SetVerticalScroll(0) end
      UpdatePagerControls(self.profPrevBtn, self.profNextBtn, self.profPageText, self.profListPage, profTotalPages)
    end

    if self.__skipProfileDetailsRebuild then
      return
    end

    self.selProfLabel:SetText("Selected: " .. (self.selectedProfileName or "(none)"))
    if self.selProfGroupLabel then
      self.selProfGroupLabel:SetText("Selected Group: " .. (self.selectedProfileGroupName or "(none)"))
    end
    if self.activateBtn then
      self.activateBtn:SetEnabled(self.selectedProfileName ~= nil and self.selectedProfileName ~= "")
    end
    if self.deactivateBtn then
      self.deactivateBtn:SetEnabled(self.selectedProfileName ~= nil and self.selectedProfileName == root.currentProfile and root.currentProfile ~= "Default")
    end
    if self.renameProfGroupBtn then
      self.renameProfGroupBtn:SetEnabled(self.selectedProfileGroupName ~= nil and self.selectedProfileGroupName ~= "")
    end
    if self.delProfGroupBtn then
      self.delProfGroupBtn:SetEnabled(self.selectedProfileGroupName ~= nil and self.selectedProfileGroupName ~= "")
    end
    if self.profGroupInfoLabel then
      local groupInfoText = "Deleting a group will only ungroup its profiles."
      if self.selectedProfileGroupName then
        local groups = EnsureProfileGroups()
        local idx = FindProfileGroupIndex(groups, self.selectedProfileGroupName)
        local childCount = (idx and groups[idx] and #(groups[idx].profiles or {})) or 0
        groupInfoText = string.format("Profiles in group: %d\nDeleting this group will keep all profiles and make them groupless.", childCount)
      end
      self.profGroupInfoLabel:SetText(groupInfoText)
    end

    if self.selectedProfileGroupName then
      if self.profRightScroll then self.profRightScroll:Hide() end
      if self.profGroupScroll then self.profGroupScroll:Show() end
      if self.profWelcomeScroll then self.profWelcomeScroll:Hide() end
      if self.profWelcomeFrame then self.profWelcomeFrame:Hide() end
    elseif self.selectedProfileName and root.profiles[self.selectedProfileName] then
      if self.profRightScroll then self.profRightScroll:Show() end
      if self.profGroupScroll then self.profGroupScroll:Hide() end
      if self.profWelcomeScroll then self.profWelcomeScroll:Hide() end
      if self.profWelcomeFrame then self.profWelcomeFrame:Hide() end
    else
      if self.profRightScroll then self.profRightScroll:Hide() end
      if self.profGroupScroll then self.profGroupScroll:Hide() end
      if self.profWelcomeScroll then self.profWelcomeScroll:Show() end
      if self.profWelcomeFrame then self.profWelcomeFrame:Show() end
      if self.LayoutProfileWelcome then self:LayoutProfileWelcome() end
    end

    if self.specAutoCheck then
      local prof = self.selectedProfileName and root.profiles[self.selectedProfileName]
      local ps = prof and prof.profileSpec
      local autoMode = (ps and ps.mode == "CHARACTER") and "CHARACTER" or "SPEC"
      local autoModeText = (autoMode == "CHARACTER") and "Change by Character" or "Change by Specialization"
      if ps and ps.enabled == nil then
        ps.enabled = false
      end
      if ps and ps.mode ~= "CHARACTER" then
        ps.mode = "SPEC"
      end
      self.specAutoCheck:SetChecked(ps and ps.enabled)
      local specAutoText = _G[self.specAutoCheck:GetName() .. "Text"]
      if specAutoText then
        specAutoText:SetText("Activate automatic profile change")
      end
      if self.profileModeDrop then
        SetDropdownSelection(self.profileModeDrop, autoMode, autoModeText)
      end
      if self.specDesc then
        if autoMode == "CHARACTER" then
          self.specDesc:SetText("If this option is activated, this profile will automatically activate when you log onto the selected character. The specialization setting is ignored in this mode.")
        else
          self.specDesc:SetText("If this option is activated, this profile will automatically activate when you change to the selected specialization. You can optionally limit this behavior to a specific character.")
        end
      end
      if self.charLabel then
        if autoMode == "CHARACTER" then
          self.charLabel:SetText("Choose Character")
        else
          self.charLabel:SetText("Optional Character Filter")
        end
      end

      local specID = ps and ps.specID
      local specText = "Unbound"
      if specID then
        local C = _G.LIDPANELS_Compat
        if C and C.GetSpecializationInfoByID then
          local _, name, _, _, role, class = C.GetSpecializationInfoByID(specID)
          if name then
            specText = class and (class .. "_" .. name) or name
            if role then specText = specText .. " (" .. role .. ")" end
          else
            specText = tostring(specID)
          end
        else
          specText = "SpecID: " .. tostring(specID)
        end
      end
      SetDropdownSelection(self.specDrop, specID or "NONE", specText)
      if self.specLabel then
        self.specLabel:SetShown(autoMode == "SPEC")
      end
      if self.specDrop then
        if autoMode == "SPEC" then
          self.specDrop:Show()
        else
          if self.specDrop.CloseMenu then
            self.specDrop:CloseMenu()
          end
          self.specDrop:Hide()
        end
      end

      if self.charDrop then
        local charVal = ps and ps.character
        local charText = charVal or ((autoMode == "CHARACTER") and "Choose character" or "Any character")
        local charValue = charVal
        if charValue == nil and autoMode ~= "CHARACTER" then
          charValue = "ANY"
        end
        SetDropdownSelection(self.charDrop, charValue, charText)
      end
    end
  end

  RefreshProfilesSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      self.__refreshing = true
      RefreshProfileLabelsState(self, root)
      RefreshProfilesSectionState(self, root)
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Profiles Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  RefreshProfileDetailsSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      self.__refreshing = true
      self.__skipProfileListRebuild = true
      RefreshProfileLabelsState(self, root)
      RefreshProfilesSectionState(self, root)
      self.__skipProfileListRebuild = nil
      self.__refreshing = false
    end, function(err)
      ReportEditorRuntimeError("Editor Profile Details Refresh", err)
    end)
    if not ok then
      self.__skipProfileListRebuild = nil
      self.__refreshing = false
    end
  end

  editor.RefreshProfilesListSection = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      self.__refreshing = true
      self.__skipProfileDetailsRebuild = true
      RefreshProfilesSectionState(self, root)
      self.__skipProfileDetailsRebuild = nil
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
    end, function(err)
      ReportEditorRuntimeError("Editor Profile List Refresh", err)
    end)
    if not ok then
      self.__skipProfileDetailsRebuild = nil
      self.__refreshing = false
    end
  end

  editor.RefreshCurrentProfileSections = function(self)
    if not self or self.__refreshing then return end
    local ok = xpcall(function()
      local root = RootDB()
      local db = DB()
      self.__refreshing = true
      if self.__backgroundDrag then
        ClearBackgroundCategoryDragState()
      end
      if self.__profileGroupDrag then
        ClearProfileGroupDragState()
      end
      RefreshProfileLabelsState(self, root)
      RefreshPanelsListSection(self, db)
      RefreshPanelPropertiesState(self, root, db)
      RefreshBackgroundsSectionState(self)
      RefreshProfilesSectionState(self, root)
      self.__refreshing = false
      if self.ApplyTheme then self:ApplyTheme() end
      if self.RefreshAbout then self:RefreshAbout() end
    end, function(err)
      ReportEditorRuntimeError("Editor Current Profile Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  editor.InvalidateSortedNameCaches = function()
    InvalidateSortedProfileNames()
    InvalidateSortedPanelNames()
  end

  editor.RefreshPanelsSection = RefreshPanelsSection
  editor.RefreshPanelPropertiesSection = RefreshPanelPropertiesSection
  editor.RefreshBackgroundsSection = RefreshBackgroundsSection
  editor.RefreshBackgroundListDetailsSection = RefreshBackgroundListDetailsSection
  editor.RefreshProfilesSection = RefreshProfilesSection
  editor.RefreshProfileDetailsSection = RefreshProfileDetailsSection

  -- ============================================================
  -- ##----------------------EDITOR REFRESH-----------------------
  -- ============================================================

  function editor:Refresh()
    local ok = xpcall(function()
      local root = RootDB()
      local db = DB()

      self.__refreshing = true
      if self.__backgroundDrag then
        ClearBackgroundCategoryDragState()
      end
      if self.__profileGroupDrag then
        ClearProfileGroupDragState()
      end
      -- Title + profile bar
      self.title:SetText("LidPanels Version " .. ADDON_VERSION)
      if self.versionText then
        self.versionText:SetText("v" .. WOW_CLIENT_VERSION)
      end
      if self.profileBarText then
        local profLabel = DisplayProfileNameLocal(root.currentProfile or "Default")
        self.profileBarText:SetText("Current Profile: " .. profLabel)
      end

      if self.minimapSizeSlider then
        local size = (root.ui.minimap and root.ui.minimap.size) or 28
        self.minimapSizeSlider:SetValueSilent(size)
      end
      if self.minimapAlphaSlider then
        local alpha = (root.ui.minimap and root.ui.minimap.alpha) or 1
        self.minimapAlphaSlider:SetValueSilent(alpha * 100)
      end
      if self.minimapHoverCheck then
        local hov = (root.ui.minimap and root.ui.minimap.onlyOnHover) and true or false
        self.minimapHoverCheck:SetChecked(hov)
      end

      ApplyDropdownLimits(self)
      RefreshPanelsListSection(self, db)
      RefreshBackgroundsSectionState(self)
      RefreshPanelPropertiesState(self, root, db)
      RefreshProfileLabelsState(self, root)
      RefreshProfilesSectionState(self, root)

      self.__refreshing = false
      -- Re-apply theme every refresh
      self:ApplyTheme()
      if self.RefreshAbout then self:RefreshAbout() end
      self.__applyingTheme = false
    end, function(err)
      ReportEditorRuntimeError("Editor Refresh", err)
    end)
    if not ok then
      self.__refreshing = false
    end
  end

  if editor.ShowPage then
    editor.ShowPage("Profiles")
  end
end
local API = _G.LIDPANELS_API
if type(API) ~= "table" then
  error("LidPanels missing API table")
end

local function PublishApi(name, value)
  _G[name] = value
  API[name] = value
end

PublishApi("LIDPANELS_CreateEditor", CreateEditor)
PublishApi("LIDPANELS_OpenColorPickerRGBOnly", OpenColorPickerRGBOnly)
PublishApi("LIDPANELS_ApplyFrameSkin", ApplyFrameSkin)
PublishApi("LIDPANELS_BuildBackgroundList", BuildBackgroundList)
PublishApi("LIDPANELS_GetBackgroundPath", GetBackgroundPath)
PublishApi("LIDPANELS_MakeYellowSubHeader", MakeYellowSubHeader)
PublishApi("LIDPANELS_MakeSlider", MakeSlider)
PublishApi("LIDPANELS_CARD_W", CARD_W)
PublishApi("LIDPANELS_CARD_H", CARD_H)
PublishApi("LIDPANELS_ApplyEditorState", ApplyEditorState)
PublishApi("LIDPANELS_ApplyEditorScale", ApplyEditorScale)
PublishApi("LIDPANELS_SwitchProfile", SwitchProfile)
PublishApi("LIDPANELS_SelectPanel", SelectPanel)
PublishApi("LIDPANELS_GetSelectedPanelName", GetSelectedPanelName)
PublishApi("LIDPANELS_GetSelectedBackgroundName", GetSelectedBackgroundName)
PublishApi("LIDPANELS_GetSelectedBackgroundCategory", GetSelectedBackgroundCategory)
PublishApi("LIDPANELS_ShowErrorLogWindow", ShowErrorLogWindow)
PublishApi("LIDPANELS_ShowSilentLogWindow", ShowSilentLogWindow)
PublishApi("LIDPANELS_ShowExportWindow", ShowExportWindow)
PublishApi("LIDPANELS_ShowImportWindow", ShowImportWindow)

-- ============================================================
