-- LidPanels_Panels.lua
-- ============================================================

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

local LogInfo = _G.LIDPANELS_LogInfo
local LogSilent = _G.LIDPANELS_LogSilent
if LogInfo then LogInfo("Panels.lua loaded") 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 DB = RequireApi("LIDPANELS_DB", _G.LIDPANELS_DB)
local RootDB = RequireApi("LIDPANELS_RootDB", _G.LIDPANELS_RootDB)
local Print = RequireApi("LIDPANELS_Print", _G.LIDPANELS_Print)

local EnsureGradientTables, ApplyFill
local RebuildAllPanels, SetAllUnlocked
local ResetPanelToCenterDefaultSize, NudgeSelectedPanel

local function GetPanelsEditor()
  local e = _G.LIDPANELS_Editor
  if e and e.__lidPanelsEditor then return e end
  return nil
end

local function GetSelectedPanelName()
  local fn = _G.LIDPANELS_GetSelectedPanelName
  if type(fn) == "function" then
    local ok, name = pcall(fn)
    if ok then return name end
  end
  return nil
end

local BORDER_STYLES = {
  none = nil,
  tooltip = {
    bgFile = nil, -- IMPORTANT: no backdrop background (we draw our own fill)
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true, tileSize = 16, edgeSize = 12,
    insets = { left = 3, right = 3, top = 3, bottom = 3 }
  },
  thin = {
    bgFile = nil,
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, tileSize = 0, edgeSize = 1,
    insets = { left = 0, right = 0, top = 0, bottom = 0 }
  },
  medium = {
    bgFile = nil,
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, tileSize = 0, edgeSize = 2,
    insets = { left = 0, right = 0, top = 0, bottom = 0 }
  },
  thick = {
    bgFile = nil,
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, tileSize = 0, edgeSize = 3,
    insets = { left = 1, right = 1, top = 1, bottom = 1 }
  },
  frame = {
    bgFile = nil,
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, tileSize = 0, edgeSize = 4,
    insets = { left = 1, right = 1, top = 1, bottom = 1 }
  },
  wide = {
    bgFile = nil,
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, tileSize = 0, edgeSize = 6,
    insets = { left = 2, right = 2, top = 2, bottom = 2 }
  },
  dialog = {
    bgFile = nil,
    edgeFile = "Interface/DialogFrame/UI-DialogBox-Border",
    tile = true, tileSize = 32, edgeSize = 32,
    insets = { left = 6, right = 6, top = 6, bottom = 6 }
  },
  soft = {
    bgFile = nil,
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true, tileSize = 16, edgeSize = 8,
    insets = { left = 2, right = 2, top = 2, bottom = 2 }
  },
  rounded = {
    bgFile = nil,
    edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
    tile = true, tileSize = 16, edgeSize = 16,
    insets = { left = 4, right = 4, top = 4, bottom = 4 }
  },
}

local TINTABLE_BORDERS = {
  thin = true,
  medium = true,
  thick = true,
  frame = true,
  wide = true,
}

local frames = _G.LIDPANELS_PanelFrames or {}
_G.LIDPANELS_PanelFrames = frames

local unlocked = _G.LIDPANELS_PanelsUnlocked
if unlocked == nil then unlocked = true end
_G.LIDPANELS_PanelsUnlocked = unlocked

local STRATA_ORDER = {
  BACKGROUND = 1,
  LOW = 2,
  MEDIUM = 3,
  HIGH = 4,
  DIALOG = 5,
  FULLSCREEN = 6,
  FULLSCREEN_DIALOG = 7,
  TOOLTIP = 8,
}

local EDIT_UNLOCK_STRATA = "HIGH"

local function ResolveAnchor(anchor)
  local point, parent, relativePoint, x, y = unpack(anchor)
  if type(parent) == "string" then
    if parent == "UIParent" then parent = UIParent else parent = _G[parent] or UIParent end
  end
  return point, parent, relativePoint, x, y
end

local function GetPanelResizeLimits()
  local minW, minH = 10, 10
  local maxW = math.floor((UIParent and UIParent:GetWidth()) or 1920)
  local maxH = math.floor((UIParent and UIParent:GetHeight()) or 1080)
  if maxW < minW then maxW = minW end
  if maxH < minH then maxH = minH end
  return minW, minH, maxW, maxH
end

local function ClampPanelSize(w, h)
  local minW, minH, maxW, maxH = GetPanelResizeLimits()
  if not w then w = minW end
  if not h then h = minH end
  if w < minW then w = minW end
  if h < minH then h = minH end
  if w > maxW then w = maxW end
  if h > maxH then h = maxH end
  return w, h, minW, minH, maxW, maxH
end

local function SavePosition(panelName, frame)
  local db = DB()
  local cx, cy = frame:GetCenter()
  local ux, uy = UIParent:GetCenter()
  if cx and cy and ux and uy then
    local x = (cx - ux)
    local y = (cy - uy)
    x = math.floor((x * 10) + 0.5) / 10
    y = math.floor((y * 10) + 0.5) / 10
    db.positions[panelName] = {
      point = "CENTER",
      relativePoint = "CENTER",
      x = x,
      y = y
    }
  else
    local point, _, relativePoint, xOfs, yOfs = frame:GetPoint(1)
    xOfs = math.floor((xOfs * 10) + 0.5) / 10
    yOfs = math.floor((yOfs * 10) + 0.5) / 10
    db.positions[panelName] = { point = point, relativePoint = relativePoint, x = xOfs, y = yOfs }
  end
end

local function SyncPositionSliders(panelName)
  local e = GetPanelsEditor()
  local selected = GetSelectedPanelName()
  if e and e:IsShown() and selected == panelName and e.xSlider and e.ySlider then
    local pos = DB().positions[panelName]
    if pos then
      e.xSlider:SetValueSilent(pos.x or 0)
      e.ySlider:SetValueSilent(pos.y or 0)
    end
  end
end

local function ApplyPosition(panelName, frame, cfg)
  frame:ClearAllPoints()
  local db = DB()
  local pos = db.positions[panelName]
  if pos and pos.point and pos.relativePoint and pos.x and pos.y then
    frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
  else
    frame:SetPoint(ResolveAnchor(cfg.anchor))
  end
end

local function ApplyBorder(frame, borderKey)
  local cfg = DB().panels[frame.__lid_name]
  local style = BORDER_STYLES[borderKey or "tooltip"]
  if style then
    frame:SetBackdrop(style)
    if frame.SetBackdropColor then frame:SetBackdropColor(0, 0, 0, 0) end
    if TINTABLE_BORDERS[borderKey] then
      local borderColor = cfg and cfg.borderColor
      local r = (type(borderColor) == "table" and tonumber(borderColor[1])) or 0
      local g = (type(borderColor) == "table" and tonumber(borderColor[2])) or 0
      local b = (type(borderColor) == "table" and tonumber(borderColor[3])) or 0
      local a = (type(borderColor) == "table" and tonumber(borderColor[4])) or 0.9
      frame:SetBackdropBorderColor(r, g, b, a)
    elseif borderKey == "dialog" then
      local a = (cfg and tonumber(cfg.borderAlpha)) or 0.9
      frame:SetBackdropBorderColor(0.7, 0.6, 0.5, a)
    else
      local a = (cfg and tonumber(cfg.borderAlpha)) or 0.9
      frame:SetBackdropBorderColor(0.2, 0.2, 0.25, a)
    end
  else
    frame:SetBackdrop(nil)
  end
end

function EnsureGradientTables(p)
  if not p or not p.gradient then return end

  local a = 0.8
  if p.color and p.color[4] ~= nil then a = p.color[4] end

  if type(p.gradient.c1) ~= "table" then
    p.gradient.c1 = { p.color[1] or 0.05, p.color[2] or 0.05, p.color[3] or 0.07, a }
  end
  if type(p.gradient.c2) ~= "table" then
    p.gradient.c2 = { 0.0, 0.0, 0.0, a }
  end

  if p.gradient.c1 == p.gradient.c2 then
    local c1 = p.gradient.c1
    p.gradient.c2 = { c1[1], c1[2], c1[3], c1[4] }
  end

  if p.gradient.c1[4] == nil then p.gradient.c1[4] = a end
  if p.gradient.c2[4] == nil then p.gradient.c2[4] = a end
end

local function GetEffectiveUnlockedStrata(baseStrata)
  local base = tostring(baseStrata or "BACKGROUND")
  local baseRank = STRATA_ORDER[base] or STRATA_ORDER.BACKGROUND
  local unlockedRank = STRATA_ORDER[EDIT_UNLOCK_STRATA]
  if baseRank >= unlockedRank then
    return base
  end
  return EDIT_UNLOCK_STRATA
end

local function GetPanelEditMode()
  local root = RootDB()
  local ui = root and root.ui
  if ui and ui.panelEditMode == "PREVIEW" then
    return "PREVIEW"
  end
  return "EDITOR"
end

local function GetCenterOffsets(frame)
  if not frame then return nil, nil end
  local cx, cy = frame:GetCenter()
  local ux, uy = UIParent:GetCenter()
  if not cx or not cy or not ux or not uy then return nil, nil end
  return cx - ux, cy - uy
end

local function SetCenterOffsets(frame, x, y)
  if not frame or not x or not y then return end
  frame:ClearAllPoints()
  frame:SetPoint("CENTER", UIParent, "CENTER", x, y)
end

local function SyncEditOverlay(frame)
  local overlay = frame and frame.__lid_editOverlay
  if not overlay or overlay.__lid_syncing then return end
  local x, y = GetCenterOffsets(frame)
  if not x or not y then return end
  overlay.__lid_syncing = true
  SetCenterOffsets(overlay, x, y)
  overlay:SetSize(frame:GetSize())
  overlay.__lid_syncing = nil
end

local function SyncFrameFromEditOverlay(frame)
  local overlay = frame and frame.__lid_editOverlay
  if not overlay or frame.__lid_overlaySyncing then return end
  local x, y = GetCenterOffsets(overlay)
  if not x or not y then return end
  frame.__lid_overlaySyncing = true
  frame.__lid_previewSyncing = true
  SetCenterOffsets(frame, x, y)
  local fw, fh = frame:GetSize()
  local ow, oh = overlay:GetSize()
  if math.abs((fw or 0) - (ow or 0)) > 0.01 or math.abs((fh or 0) - (oh or 0)) > 0.01 then
    frame:SetSize(ow, oh)
  end
  frame.__lid_previewSyncing = nil
  frame.__lid_overlaySyncing = nil
end

local function SyncPanelLayering(frame)
  if not frame then return end
  if frame.__lid_model then
    frame.__lid_model:SetFrameStrata(frame:GetFrameStrata())
    frame.__lid_model:SetFrameLevel(frame:GetFrameLevel())
  end
  if frame.__lid_editOverlay then
    local cfg = DB().panels[frame.__lid_name]
    local baseStrata = (cfg and cfg.strata) or frame:GetFrameStrata() or "BACKGROUND"
    frame.__lid_editOverlay:SetFrameStrata(GetEffectiveUnlockedStrata(baseStrata))
    frame.__lid_editOverlay:SetFrameLevel((frame:GetFrameLevel() or 0) + 20)
  end
  if frame.__lid_grip then
    local strataFrame = frame.__lid_grip:GetParent() or frame
    frame.__lid_grip:SetFrameStrata(strataFrame:GetFrameStrata())
    frame.__lid_grip:SetFrameLevel((strataFrame:GetFrameLevel() or 0) + 10)
  end
end

local function RaisePanelForEditing(frame)
  if not frame then return end
  local target = frame.__lid_editOverlay or frame
  if target.Raise then
    target:Raise()
  end
  SyncPanelLayering(frame)
end

local function TrySelectPanelForEditing(panelName)
  if not panelName or not DB().panels[panelName] then return end
  local editor = GetPanelsEditor()
  if not (editor and editor:IsShown()) then return end
  local fn = _G.LIDPANELS_SelectPanel
  if type(fn) == "function" then
    pcall(fn, panelName)
  end
end

local function ClampNumber(value, defaultValue, minValue, maxValue)
  local n = tonumber(value)
  if n == nil then return defaultValue end
  if minValue ~= nil and n < minValue then n = minValue end
  if maxValue ~= nil and n > maxValue then n = maxValue end
  return n
end

local function ClampTexCoordValue(value, defaultValue)
  return ClampNumber(value, defaultValue, 0, 1)
end

local function SafeClearModel(model)
  if not model then return end
  model:Hide()
  if model.ClearModel then
    pcall(model.ClearModel, model)
  end
end

local function SafeApplyModel(model, modelId, settings, alpha)
  local id = tonumber(modelId)
  if not model or not id or id <= 0 then
    if modelId ~= nil and LogSilent then
      LogSilent("Skipped invalid panel model id: " .. tostring(modelId))
    end
    SafeClearModel(model)
    return false
  end

  local ok = pcall(model.SetModel, model, id)
  if not ok then
    if LogSilent then
      LogSilent("Failed to load panel model id: " .. tostring(id))
    end
    SafeClearModel(model)
    return false
  end

  local zoom = ClampNumber(settings and settings.zoom, 1, 0.05, 10)
  local facing = ClampNumber(settings and settings.facing, 0, -360, 360)
  local x = ClampNumber(settings and settings.x, 0, -10000, 10000)
  local y = ClampNumber(settings and settings.y, 0, -10000, 10000)
  local a = ClampNumber(alpha, 1, 0, 1)

  pcall(model.SetAlpha, model, a)
  pcall(model.SetModelScale, model, zoom)
  pcall(model.SetFacing, model, math.rad(facing))
  pcall(model.SetPosition, model, x, y, 0)
  model:Show()
  return true
end

local function SafeApplyBackgroundTexture(tex, asset, texCoord, alpha)
  if not tex or not asset or asset == "NONE" then
    return false
  end

  local ok = pcall(tex.SetTexture, tex, asset)
  if not ok then
    if LogSilent then
      LogSilent("Failed to load panel texture: " .. tostring(asset))
    end
    tex:SetTexture("Interface/Buttons/WHITE8x8")
    tex:SetTexCoord(0, 1, 0, 1)
    tex:SetVertexColor(1, 1, 1, 0)
    return false
  end

  if type(texCoord) == "table" then
    tex:SetTexCoord(
      ClampTexCoordValue(texCoord[1], 0),
      ClampTexCoordValue(texCoord[2], 1),
      ClampTexCoordValue(texCoord[3], 0),
      ClampTexCoordValue(texCoord[4], 1)
    )
  else
    tex:SetTexCoord(0, 1, 0, 1)
  end

  tex:SetVertexColor(1, 1, 1, ClampNumber(alpha, 1, 0, 1))
  return true
end

-- Gradient + flat fill (robust)
function ApplyFill(frame, cfg)
  local tex = frame.__lid_colorTex
  if not tex or not cfg then return end

  if cfg.bgType == "MODEL" and cfg.bgModelId then
    if frame.__lid_model then
      tex:Hide()
      local a = 1
      if cfg.color and cfg.color[4] ~= nil then a = cfg.color[4] end
      local ok = SafeApplyModel(frame.__lid_model, cfg.bgModelId, cfg.bgModelSettings or {}, a)
      if not ok then
        tex:Show()
      else
        frame.__lid_model:Show()
      end
      return
    end
  end

  if frame.__lid_model then
    SafeClearModel(frame.__lid_model)
  end
  tex:Show()

  tex:SetTexture("Interface/Buttons/WHITE8x8")
  tex:SetTexCoord(0, 1, 0, 1)

  if cfg.bg and cfg.bg ~= "NONE" then
    local a = 1
    if cfg.color and cfg.color[4] ~= nil then a = cfg.color[4] end
    if SafeApplyBackgroundTexture(tex, cfg.bg, cfg.bgTexCoord, a) then
      return
    end
  end

  if cfg.gradient then
    EnsureGradientTables(cfg)
    local dir = (cfg.gradient.dir == "HORIZONTAL") and "HORIZONTAL" or "VERTICAL"
    local c1 = cfg.gradient.c1
    local c2 = cfg.gradient.c2

    local r1, g1, b1, a1 = c1[1] or 0, c1[2] or 0, c1[3] or 0, c1[4]
    local r2, g2, b2, a2 = c2[1] or 0, c2[2] or 0, c2[3] or 0, c2[4]
    if a1 == nil then a1 = 0.8 end
    if a2 == nil then a2 = 0.8 end

    if tex.SetGradientAlpha then
      tex:SetGradientAlpha(dir, r1, g1, b1, a1, r2, g2, b2, a2)
      return
    end

    if tex.SetGradient and CreateColor then
      local minC = CreateColor(r1, g1, b1, a1)
      local maxC = CreateColor(r2, g2, b2, a2)
      tex:SetGradient(dir, minC, maxC)
      return
    end

    -- fallback
    tex:SetVertexColor(r1, g1, b1, a1)
    return
  end

  local rgba = cfg.color or { 0, 0, 0, 0.8 }
  local r, g, b, a = rgba[1] or 0, rgba[2] or 0, rgba[3] or 0, rgba[4]
  if a == nil then a = 0.8 end
  tex:SetVertexColor(r, g, b, a)
end

local PANEL_SPECIAL_STATS = {
  { key = "agility", label = "Agility" },
  { key = "stamina", label = "Stamina" },
  { key = "armor", label = "Armor" },
  { key = "speed", label = "Speed" },
  { key = "crit", label = "Critical Strike" },
  { key = "haste", label = "Haste" },
  { key = "mastery", label = "Mastery" },
  { key = "versatility", label = "Versatility" },
  { key = "leech", label = "Leech" },
  { key = "avoidance", label = "Avoidance" },
}

local function GetFPSColor(fps, cfg)
  local lowValue, highValue = 30, 60
  local lowColor = { 0.95, 0.24, 0.24, 1.00 }
  local midColor = { 1.00, 0.62, 0.16, 1.00 }
  local highColor = { 0.20, 0.90, 0.20, 1.00 }

  local special = cfg and cfg.specialContent
  local fpsCfg = special and special.fps
  if fpsCfg then
    lowValue = math.floor((tonumber(fpsCfg.lowValue) or lowValue) + 0.5)
    highValue = math.floor((tonumber(fpsCfg.highValue) or highValue) + 0.5)
    if highValue <= lowValue then highValue = lowValue + 1 end
    if type(fpsCfg.lowColor) == "table" then lowColor = fpsCfg.lowColor end
    if type(fpsCfg.midColor) == "table" then midColor = fpsCfg.midColor end
    if type(fpsCfg.highColor) == "table" then highColor = fpsCfg.highColor end
  end

  if fps > highValue then
    return highColor[1] or 0.20, highColor[2] or 0.90, highColor[3] or 0.20
  elseif fps >= lowValue then
    return midColor[1] or 1.00, midColor[2] or 0.62, midColor[3] or 0.16
  end
  return lowColor[1] or 0.95, lowColor[2] or 0.24, lowColor[3] or 0.24
end

local function GetPanelGraphSettings(cfg)
  local graph = cfg and cfg.diagnostic and cfg.diagnostic.graph or {}
  local updateInterval = tonumber(graph.updateInterval) or 0.1
  if updateInterval < 0.1 then updateInterval = 0.1 end
  if updateInterval > 2.0 then updateInterval = 2.0 end
  updateInterval = math.floor((updateInterval * 10) + 0.5) / 10
  local avgWindowSec = math.floor((tonumber(graph.avgWindowSec) or 10) + 0.5)
  if avgWindowSec < 5 then avgWindowSec = 5 end
  if avgWindowSec > 300 then avgWindowSec = 300 end
  local infoIntervalFPS = tonumber(graph.infoIntervalFPS) or 0.1
  if infoIntervalFPS < 0.1 then infoIntervalFPS = 0.1 end
  if infoIntervalFPS > 300 then infoIntervalFPS = 300 end
  infoIntervalFPS = math.floor((infoIntervalFPS * 10) + 0.5) / 10
  local infoIntervalAVG = tonumber(graph.infoIntervalAVG) or 10
  if infoIntervalAVG < 0.1 then infoIntervalAVG = 0.1 end
  if infoIntervalAVG > 300 then infoIntervalAVG = 300 end
  infoIntervalAVG = math.floor((infoIntervalAVG * 10) + 0.5) / 10
  local infoIntervalLOW = tonumber(graph.infoIntervalLOW) or 0.1
  if infoIntervalLOW < 0.1 then infoIntervalLOW = 0.1 end
  if infoIntervalLOW > 300 then infoIntervalLOW = 300 end
  infoIntervalLOW = math.floor((infoIntervalLOW * 10) + 0.5) / 10
  local style = (graph.style == "WAVE") and "WAVE" or "BARS"
  return {
    showFPS = (graph.showFPS ~= false),
    showAvg = graph.showAvg and true or false,
    showLow = graph.showLow and true or false,
    updateInterval = updateInterval,
    avgWindowSec = avgWindowSec,
    infoIntervalFPS = infoIntervalFPS,
    infoIntervalAVG = infoIntervalAVG,
    infoIntervalLOW = infoIntervalLOW,
    style = style,
  }
end

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

local function EnsurePanelOverlayWidgets(frame)
  if not frame or frame.__lid_overlayReady then return end

  local graph = CreateFrame("Frame", nil, frame, "BackdropTemplate")
  graph:SetPoint("TOPLEFT", 3, -3)
  graph:SetPoint("BOTTOMRIGHT", -3, 3)
  graph:SetBackdrop({
    bgFile = "Interface/Buttons/WHITE8x8",
    edgeFile = "Interface/Buttons/WHITE8x8",
    tile = false, edgeSize = 1,
    insets = { left = 1, right = 1, top = 1, bottom = 1 },
  })
  graph:SetBackdropColor(0.02, 0.02, 0.03, 0.25)
  graph:SetBackdropBorderColor(0, 0, 0, 0.45)
  graph:SetFrameLevel(frame:GetFrameLevel() + 1)
  graph:Hide()
  frame.__lid_diagGraph = graph
  frame.__lid_diagBars = {}
  frame.__lid_diagHistory = {}
  frame.__lid_diagElapsed = 0
  frame.__lid_diagInfoWidth = nil
  frame.__lid_diagInfoCache = nil

  local diagInfo = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  diagInfo:SetPoint("TOPRIGHT", -6, -8)
  diagInfo:SetJustifyH("RIGHT")
  diagInfo:SetWidth(92)
  diagInfo:SetText("")
  diagInfo:Hide()
  frame.__lid_diagInfoText = diagInfo

  local specialText = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  specialText:SetPoint("TOPRIGHT", -6, -24)
  specialText:SetJustifyH("RIGHT")
  specialText:SetWidth(math.max(20, (frame:GetWidth() or 40) - 12))
  specialText:SetText("")
  specialText:Hide()
  frame.__lid_specialText = specialText

  frame.__lid_overlayReady = true
end

local function DrawPanelFPSGraph(frame, fps, cfg, elapsedForInfo)
  local graph = frame.__lid_diagGraph
  local bars = frame.__lid_diagBars
  local history = frame.__lid_diagHistory
  if not graph or not bars or not history then return end

  local settings = GetPanelGraphSettings(cfg)
  local showInfo = settings.showFPS or settings.showAvg or settings.showLow
  local panelW = frame:GetWidth() or 120
  local rightInset = 3
  if showInfo then
    rightInset = math.max(58, math.floor(panelW * 0.34))
    if rightInset > 130 then rightInset = 130 end
  end
  if frame.__lid_diagInfoWidth ~= rightInset then
    graph:ClearAllPoints()
    graph:SetPoint("TOPLEFT", 3, -3)
    graph:SetPoint("BOTTOMRIGHT", -rightInset, 3)
    frame.__lid_diagInfoWidth = rightInset
  end

  history[#history + 1] = fps
  while #history > 240 do
    table.remove(history, 1)
  end

  local graphW = (graph:GetWidth() or 1) - 10
  local graphH = (graph:GetHeight() or 1) - 10
  if graphW < 1 then graphW = 1 end
  if graphH < 1 then graphH = 1 end

  local sampleCount
  if settings.style == "WAVE" then
    sampleCount = math.max(10, math.floor(graphW / 2))
  else
    sampleCount = math.max(12, math.floor(graphW / 4))
  end
  if sampleCount > 220 then sampleCount = 220 end
  if sampleCount < 2 then sampleCount = 2 end

  local srcOffset = #history - sampleCount
  local maxFPS = 60
  local sampleValues = {}
  local visibleLow = nil

  local function SampleAt(index)
    local v = history[index] or 0
    if settings.style == "WAVE" then
      local p1 = history[index - 1] or v
      local p2 = history[index - 2] or p1
      local n1 = history[index + 1] or v
      local n2 = history[index + 2] or n1
      return (p2 + (p1 * 2) + (v * 3) + (n1 * 2) + n2) / 9
    end
    return v
  end

  for i = 1, sampleCount do
    local src = SampleAt(srcOffset + i)
    sampleValues[i] = src
    if src > maxFPS then maxFPS = src end
    if (not visibleLow) or src < visibleLow then visibleLow = src end
  end
  if maxFPS < 1 then maxFPS = 1 end

  if settings.style == "WAVE" then
    local step = graphW / math.max(1, sampleCount - 1)
    for i = 1, sampleCount - 1 do
      if not bars[i] then
        local seg = graph:CreateTexture(nil, "ARTWORK")
        seg:SetTexture("Interface/Buttons/WHITE8x8")
        bars[i] = seg
      end
      local s1 = sampleValues[i] or 0
      local s2 = sampleValues[i + 1] or s1
      local y1 = 5 + math.floor((s1 / maxFPS) * graphH)
      local y2 = 5 + math.floor((s2 / maxFPS) * graphH)
      local x1 = 5 + math.floor((i - 1) * step)
      local x2 = 5 + math.floor(i * step)
      local bottom = math.min(y1, y2) - 1
      if bottom < 5 then bottom = 5 end
      local h = math.max(2, math.abs(y2 - y1) + 2)
      local w = math.max(1, (x2 - x1) + 1)
      local seg = bars[i]
      seg:ClearAllPoints()
      seg:SetPoint("BOTTOMLEFT", graph, "BOTTOMLEFT", x1, bottom)
      seg:SetSize(w, h)
      local r, g, b = GetFPSColor((s1 + s2) * 0.5, cfg)
      seg:SetVertexColor(r, g, b, 0.9)
      seg:Show()
    end
    for i = sampleCount, #bars do
      bars[i]:Hide()
    end
  else
    local barW = math.max(1, math.floor(graphW / sampleCount) - 1)
    for i = 1, sampleCount do
      if not bars[i] then
        local bar = graph:CreateTexture(nil, "ARTWORK")
        bar:SetTexture("Interface/Buttons/WHITE8x8")
        bars[i] = bar
      end
      local src = sampleValues[i] or 0
      local h = math.floor((src / maxFPS) * graphH)
      if h < 1 then h = 1 end
      local x = 5 + ((i - 1) * (barW + 1))
      local bar = bars[i]
      bar:ClearAllPoints()
      bar:SetPoint("BOTTOMLEFT", graph, "BOTTOMLEFT", x, 5)
      bar:SetSize(barW, h)
      local r, g, b = GetFPSColor(src, cfg)
      bar:SetVertexColor(r, g, b, 0.85)
      bar:Show()
    end
    for i = sampleCount + 1, #bars do
      bars[i]:Hide()
    end
  end

  local info = frame.__lid_diagInfoText
  if info then
    if showInfo then
      local fpsCfg = cfg and cfg.specialContent and cfg.specialContent.fps or {}
      if info.SetAlpha then
        info:SetAlpha(math.max(0, math.min(1, tonumber(fpsCfg.alpha) or 1)))
      end
      local fontKey = (fpsCfg.font and _G[fpsCfg.font]) and fpsCfg.font or "GameFontHighlightSmall"
      local fontSize = math.floor((tonumber(fpsCfg.size) or 14) + 0.5)
      if fontSize < 8 then fontSize = 8 end
      if fontSize > 40 then fontSize = 40 end
      -- Auto-cap graph metric font by panel dimensions so tiny panels stay readable.
      local pW = frame:GetWidth() or 120
      local pH = frame:GetHeight() or 80
      local autoCap = math.floor((math.min(pW, pH) * 0.24) + 0.5)
      if autoCap < 8 then autoCap = 8 end
      if autoCap > 24 then autoCap = 24 end
      if fontSize > autoCap then fontSize = autoCap end
      local fontObj = _G[fontKey]
      local fontPath, _, fontFlags
      if fontObj and fontObj.GetFont then
        fontPath, _, fontFlags = fontObj:GetFont()
      end
      if not fontPath and GameFontHighlightSmall and GameFontHighlightSmall.GetFont then
        fontPath, _, fontFlags = GameFontHighlightSmall:GetFont()
      end
      if fontPath and info.SetFont then
        info:SetFont(fontPath, fontSize, fontFlags)
      end

      info:ClearAllPoints()
      info:SetPoint("TOPRIGHT", -6, -8)
      info:SetWidth(math.max(42, rightInset - 10))
      info:SetJustifyH("RIGHT")

      local stepElapsed = tonumber(elapsedForInfo) or settings.updateInterval
      if stepElapsed < 0.01 then stepElapsed = 0.01 end

      local windowSamples = math.max(1, math.floor((settings.avgWindowSec / math.max(0.1, settings.updateInterval)) + 0.5))
      local from = #history - windowSamples + 1
      if from < 1 then from = 1 end
      local avgSum, avgN = 0, 0
      for i = from, #history do
        avgSum = avgSum + (history[i] or 0)
        avgN = avgN + 1
      end
      if avgN < 1 then avgN = 1 end
      local avgFPS = avgSum / avgN
      local lowFPS = visibleLow or 0
      frame.__lid_diagInfoCache = frame.__lid_diagInfoCache or {
        fps = { elapsed = 0, value = fps },
        avg = { elapsed = 0, value = avgFPS },
        low = { elapsed = 0, value = lowFPS },
      }
      local cache = frame.__lid_diagInfoCache
      cache.fps.elapsed = (cache.fps.elapsed or 0) + stepElapsed
      cache.avg.elapsed = (cache.avg.elapsed or 0) + stepElapsed
      cache.low.elapsed = (cache.low.elapsed or 0) + stepElapsed
      if (not cache.fps.value) or cache.fps.elapsed >= settings.infoIntervalFPS then
        cache.fps.value = fps
        cache.fps.elapsed = 0
      end
      if (not cache.avg.value) or cache.avg.elapsed >= settings.infoIntervalAVG then
        cache.avg.value = avgFPS
        cache.avg.elapsed = 0
      end
      if (not cache.low.value) or cache.low.elapsed >= settings.infoIntervalLOW then
        cache.low.value = lowFPS
        cache.low.elapsed = 0
      end
      local lines = {}
      if settings.showFPS then
        local v = cache.fps.value or fps
        local r, g, b = GetFPSColor(v, cfg)
        lines[#lines + 1] = string.format("%sFPS %.1f|r", RGBToCode(r, g, b), v)
      end
      if settings.showAvg then
        local v = cache.avg.value or avgFPS
        local r, g, b = GetFPSColor(v, cfg)
        lines[#lines + 1] = string.format("%sAVG %.1f|r", RGBToCode(r, g, b), v)
      end
      if settings.showLow then
        local v = cache.low.value or lowFPS
        local r, g, b = GetFPSColor(v, cfg)
        lines[#lines + 1] = string.format("%sLOW %.1f|r", RGBToCode(r, g, b), v)
      end
      info:SetText(table.concat(lines, "\n"))
      info:Show()
    else
      info:SetText("")
      info:Hide()
      frame.__lid_diagInfoCache = nil
    end
  end
end

local function FormatMoneyText(copper)
  local gold = math.floor((copper or 0) / 10000)
  local silver = math.floor(((copper or 0) % 10000) / 100)
  local cop = math.floor((copper or 0) % 100)
  return string.format("%dg %ds %dc", gold, silver, cop)
end

local function BuildSpecialCache(fps, needs)
  local cache = {}
  needs = needs or {}

  if needs.fps then
    cache.fps = fps or 0
  end

  if needs.money then
    cache.money = (GetMoney and GetMoney()) or 0
  end

  if needs.stats then
    cache.stats = {}
    local _, agi = UnitStat("player", 2)
    local _, stam = UnitStat("player", 3)
    local _, armor = UnitArmor("player")
    local speedPct = 100
    if GetUnitSpeed then
      local curSpeed = GetUnitSpeed("player") or 0
      if curSpeed > 0 then
        speedPct = (curSpeed / 7) * 100
      else
        speedPct = 0
      end
    end
    local crit = (GetCritChance and GetCritChance()) or 0
    local haste = (GetHaste and GetHaste()) or 0
    local mastery = (GetMasteryEffect and GetMasteryEffect()) or 0
    local vers = 0
    if GetCombatRatingBonus and CR_VERSATILITY_DAMAGE_DONE then
      vers = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) or 0
    end
    local leech = (GetLifesteal and GetLifesteal()) or 0
    local avoidance = (GetAvoidance and GetAvoidance()) or 0

    cache.stats.agility = tostring(math.floor((agi or 0) + 0.5))
    cache.stats.stamina = tostring(math.floor((stam or 0) + 0.5))
    cache.stats.armor = tostring(math.floor((armor or 0) + 0.5))
    cache.stats.speed = string.format("%.0f%%", speedPct)
    cache.stats.crit = string.format("%.1f%%", crit)
    cache.stats.haste = string.format("%.1f%%", haste)
    cache.stats.mastery = string.format("%.1f%%", mastery)
    cache.stats.versatility = string.format("%.1f%%", vers)
    cache.stats.leech = string.format("%.1f%%", leech)
    cache.stats.avoidance = string.format("%.1f%%", avoidance)
  end

  return cache
end

local function ColorToHex(c, fallback)
  local src = c or fallback or { 1, 1, 1, 1 }
  local r = math.floor((math.max(0, math.min(1, tonumber(src[1]) or 1)) * 255) + 0.5)
  local g = math.floor((math.max(0, math.min(1, tonumber(src[2]) or 1)) * 255) + 0.5)
  local b = math.floor((math.max(0, math.min(1, tonumber(src[3]) or 1)) * 255) + 0.5)
  return string.format("|cff%02x%02x%02x", r, g, b)
end

local function ResolveSpecialFont(fontKey, size)
  local key = (fontKey and _G[fontKey]) and fontKey or "GameFontHighlightSmall"
  local obj = _G[key]
  local font, _, flags
  if obj and obj.GetFont then
    font, _, flags = obj:GetFont()
  end
  if not font and GameFontHighlightSmall and GameFontHighlightSmall.GetFont then
    font, _, flags = GameFontHighlightSmall:GetFont()
  end
  return font, math.max(8, math.floor((tonumber(size) or 14) + 0.5)), flags
end

local function ApplySpecialTextStyle(txt, fontKey, size, color)
  local font, fontSize, flags = ResolveSpecialFont(fontKey, size)
  if font and txt.SetFont then
    txt:SetFont(font, fontSize, flags)
  end
  local c = color or { 1, 1, 1, 1 }
  txt:SetTextColor(c[1] or 1, c[2] or 1, c[3] or 1, 1)
  if txt.SetAlpha then
    txt:SetAlpha(c[4] or 1)
  end
end

local function ResolveSpecialMode(cfg)
  local special = cfg and cfg.specialContent
  if not special then
    return "NONE", nil
  end

  local mode = special.mode
  if not mode then
    if cfg and cfg.diagnostic and cfg.diagnostic.fpsGraph then
      mode = "FPS_GRAPH"
    elseif special.showGold then
      mode = "GOLD"
    elseif special.showFPS then
      mode = "FPS"
    elseif special.showStats then
      mode = "STATS"
    elseif special.showCustomText then
      mode = "CUSTOM_TEXT"
    else
      mode = "NONE"
    end
  end
  return mode, special
end

local function ResetPanelOverlay(frame)
  if not frame or not frame.__lid_overlayReady then return end
  if frame.__lid_diagGraph then frame.__lid_diagGraph:Hide() end
  if frame.__lid_diagInfoText then
    frame.__lid_diagInfoText:SetText("")
    frame.__lid_diagInfoText:Hide()
  end
  if frame.__lid_specialText then
    frame.__lid_specialText:SetText("")
    frame.__lid_specialText:Hide()
  end
  frame.__lid_diagInfoCache = nil
  frame.__lid_diagElapsed = 0
end

local function PanelNeedsOverlayUpdates(cfg)
  if not cfg then return false end
  local mode = ResolveSpecialMode(cfg)
  if (cfg.diagnostic and cfg.diagnostic.fpsGraph) or (mode == "FPS_GRAPH") then
    return true
  end
  if mode == "GOLD" or mode == "FPS" or mode == "STATS" or mode == "CUSTOM_TEXT" then
    return true
  end
  return false
end

local function UpdatePanelSpecialContent(frame, cfg, fps, cache)
  local txt = frame.__lid_specialText
  if not txt then return end

  local special = cfg and cfg.specialContent
  if not special then
    txt:SetText("")
    txt:Hide()
    return
  end

  local mode = ResolveSpecialMode(cfg)

  local lines = {}
  if mode == "GOLD" then
    local goldCfg = special.gold or {}
    lines[1] = "Gold: " .. FormatMoneyText((cache and cache.money) or 0)
    ApplySpecialTextStyle(txt, goldCfg.font, goldCfg.size, goldCfg.color or { 1.00, 0.82, 0.00, 1.00 })
  elseif mode == "FPS" then
    local fpsCfg = special.fps or {}
    local fpsValue = (cache and cache.fps) or fps or 0
    local lowV = math.floor((tonumber(fpsCfg.lowValue) or 30) + 0.5)
    local highV = math.floor((tonumber(fpsCfg.highValue) or 60) + 0.5)
    if highV <= lowV then highV = lowV + 1 end
    local color = fpsCfg.midColor or { 1.00, 0.62, 0.16, 1.00 }
    if fpsValue < lowV then
      color = fpsCfg.lowColor or { 0.95, 0.24, 0.24, 1.00 }
    elseif fpsValue > highV then
      color = fpsCfg.highColor or { 0.20, 0.90, 0.20, 1.00 }
    end
    color = { color[1] or 1, color[2] or 1, color[3] or 1, tonumber(fpsCfg.alpha) or 1 }
    lines[1] = string.format("FPS: %.1f", fpsValue)
    ApplySpecialTextStyle(txt, fpsCfg.font, fpsCfg.size, color)
  elseif mode == "STATS" then
    local statsCfg = special.stats or {}
    local titleColors = special.statsTitleColors or {}
    local valueColors = special.statsValueColors or {}
    local selected = 0
    for _, def in ipairs(PANEL_SPECIAL_STATS) do
      if statsCfg[def.key] then
        local value = (cache and cache.stats and cache.stats[def.key]) or "-"
        local titleCode = ColorToHex(titleColors[def.key], { 0.84, 0.91, 1.00, 1.00 })
        local valueCode = ColorToHex(valueColors[def.key], { 1.00, 1.00, 1.00, 1.00 })
        lines[#lines + 1] = string.format("%s%s:|r %s%s|r", titleCode, def.label, valueCode, tostring(value))
        selected = selected + 1
      end
    end
    if selected == 0 then
      lines[1] = "Stats: none selected"
    end
    ApplySpecialTextStyle(txt, special.statsFont, special.statsSize, { 1, 1, 1, tonumber(special.statsAlpha) or 1 })
  elseif mode == "CUSTOM_TEXT" then
    local customCfg = special.customText or {}
    local customText = tostring(customCfg.text or "")
    if customText == "" then
      customText = "Custom Text"
    end
    lines[1] = customText
    ApplySpecialTextStyle(txt, customCfg.font, customCfg.size, customCfg.color or { 1, 1, 1, 1 })
  elseif mode == "FPS_GRAPH" then
    txt:SetText("")
    txt:Hide()
    return
  else
    txt:SetText("")
    txt:Hide()
    return
  end

  if #lines > 0 then
    txt:SetWidth(math.max(20, (frame:GetWidth() or 40) - 12))
    txt:SetText(table.concat(lines, "\n"))
    txt:Show()
  else
    txt:SetText("")
    txt:Hide()
  end
end

local panelOverlayUpdater = CreateFrame("Frame")
panelOverlayUpdater:SetScript("OnUpdate", function(_, elapsed)
  panelOverlayUpdater.__lid_elapsed = (panelOverlayUpdater.__lid_elapsed or 0) + (elapsed or 0)
  if panelOverlayUpdater.__lid_elapsed < 0.1 then return end
  local tick = panelOverlayUpdater.__lid_elapsed
  panelOverlayUpdater.__lid_elapsed = 0

  local db = DB()
  local workItems = {}
  local needs = { fps = false, money = false, stats = false }

  for name, frame in pairs(frames) do
    if frame and frame:IsShown() then
      local cfg = db.panels[name]
      if cfg then
        local mode = ResolveSpecialMode(cfg)
        local showGraph = (cfg.diagnostic and cfg.diagnostic.fpsGraph) or (mode == "FPS_GRAPH")
        local showSpecial = (mode == "GOLD") or (mode == "FPS") or (mode == "STATS") or (mode == "CUSTOM_TEXT")
        if showGraph or showSpecial then
          workItems[#workItems + 1] = { frame = frame, cfg = cfg, mode = mode, showGraph = showGraph }
          if showGraph or mode == "FPS" then needs.fps = true end
          if mode == "GOLD" then needs.money = true end
          if mode == "STATS" then needs.stats = true end
        else
          ResetPanelOverlay(frame)
        end
      end
    end
  end

  if #workItems == 0 then
    return
  end

  local fps = needs.fps and ((GetFramerate and GetFramerate()) or 0) or 0
  local cache = BuildSpecialCache(fps, needs)

  for _, item in ipairs(workItems) do
    local frame = item.frame
    local cfg = item.cfg
    EnsurePanelOverlayWidgets(frame)
    if item.showGraph then
      frame.__lid_diagGraph:Show()
      local graphSettings = GetPanelGraphSettings(cfg)
      frame.__lid_diagElapsed = (frame.__lid_diagElapsed or graphSettings.updateInterval) + tick
      if frame.__lid_diagElapsed >= graphSettings.updateInterval then
        local graphStep = frame.__lid_diagElapsed
        frame.__lid_diagElapsed = 0
        DrawPanelFPSGraph(frame, fps, cfg, graphStep)
      end
    else
      if frame.__lid_diagGraph then frame.__lid_diagGraph:Hide() end
      if frame.__lid_diagInfoText then
        frame.__lid_diagInfoText:SetText("")
        frame.__lid_diagInfoText:Hide()
      end
      frame.__lid_diagInfoCache = nil
      frame.__lid_diagElapsed = 0
    end
    UpdatePanelSpecialContent(frame, cfg, fps, cache)
  end
end)
panelOverlayUpdater:Hide()

local function RefreshOverlayUpdaterState()
  local db = DB()
  local hasDemand = false

  for name, frame in pairs(frames) do
    if frame and frame:IsShown() then
      local cfg = db.panels[name]
      if PanelNeedsOverlayUpdates(cfg) then
        hasDemand = true
        break
      end
    else
      ResetPanelOverlay(frame)
    end
  end

  if hasDemand then
    if not panelOverlayUpdater:IsShown() then
      panelOverlayUpdater.__lid_elapsed = 0
      panelOverlayUpdater:Show()
    end
    return
  end

  panelOverlayUpdater.__lid_elapsed = 0
  panelOverlayUpdater:Hide()
  for _, frame in pairs(frames) do
    ResetPanelOverlay(frame)
  end
end

local function SetUnlocked(frame, isUnlocked)
  local cfg = DB().panels[frame.__lid_name]
  local panelLocked = (cfg and cfg.locked) and true or false
  local allowMove = isUnlocked and (not panelLocked)
  local baseStrata = (cfg and cfg.strata) or "BACKGROUND"
  local editMode = GetPanelEditMode()
  local usePreviewMode = allowMove and (editMode == "PREVIEW")

  frame.__lid_resizeTarget = usePreviewMode and frame.__lid_editOverlay or frame
  frame:SetFrameStrata((allowMove and not usePreviewMode) and GetEffectiveUnlockedStrata(baseStrata) or baseStrata)
  frame:EnableMouse(allowMove and (not usePreviewMode))
  frame:SetMovable(allowMove and (not usePreviewMode))
  if allowMove and not usePreviewMode then
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnMouseDown", function(self, button)
      if button and button ~= "LeftButton" then return end
      TrySelectPanelForEditing(self.__lid_name)
      RaisePanelForEditing(self)
    end)
    frame:SetScript("OnDragStart", function(self)
      if self.__lid_resizePending or self.__lid_resizing or self.__lid_resizeSuppressDrag then
        self.__lid_dragging = false
        self:SetScript("OnUpdate", nil)
        return
      end
      TrySelectPanelForEditing(self.__lid_name)
      RaisePanelForEditing(self)
      self:StartMoving()
      self.__lid_dragging = true
      self.__lid_dragElapsed = 0
      self:SetScript("OnUpdate", function(s, elapsed)
        if not s.__lid_dragging then return end
        s.__lid_dragElapsed = (s.__lid_dragElapsed or 0) + elapsed
        if s.__lid_dragElapsed < 0.05 then return end
        s.__lid_dragElapsed = 0
        SavePosition(s.__lid_name, s)
        SyncPositionSliders(s.__lid_name)
      end)
    end)
    frame:SetScript("OnDragStop", function(self)
      if self.__lid_resizePending or self.__lid_resizing or self.__lid_resizeSuppressDrag or not self.__lid_dragging then
        self.__lid_dragging = false
        self:SetScript("OnUpdate", nil)
        return
      end
      self:StopMovingOrSizing()
      self.__lid_dragging = false
      self:SetScript("OnUpdate", nil)
      SavePosition(self.__lid_name, self)
      SyncPositionSliders(self.__lid_name)
    end)
  else
    frame:RegisterForDrag()
    frame:SetScript("OnMouseDown", nil)
    frame:SetScript("OnDragStart", nil)
    frame:SetScript("OnDragStop", nil)
    frame:SetScript("OnUpdate", nil)
    frame.__lid_dragging = false
  end
  SyncPanelLayering(frame)

  local overlay = frame.__lid_editOverlay
  if overlay then
    overlay:SetShown(usePreviewMode)
    overlay:EnableMouse(usePreviewMode)
    overlay:SetMovable(usePreviewMode)
    overlay:SetResizable(usePreviewMode)
    if usePreviewMode then
      overlay:RegisterForDrag("LeftButton")
    else
      overlay:RegisterForDrag()
    end
    if usePreviewMode then
      SyncEditOverlay(frame)
      RaisePanelForEditing(frame)
    else
      overlay:SetScript("OnUpdate", nil)
      overlay.__lid_dragging = false
      overlay.__lid_resizing = false
      overlay.__lid_resizePending = false
    end
  end
  if frame.__lid_grip then
    local gripParent = usePreviewMode and overlay or frame
    if gripParent and frame.__lid_grip:GetParent() ~= gripParent then
      frame.__lid_grip:SetParent(gripParent)
      frame.__lid_grip:ClearAllPoints()
      frame.__lid_grip:SetPoint("BOTTOMRIGHT", -2, 2)
    end
  end
  SyncPanelLayering(frame)

  local e = GetPanelsEditor()
  local showHandles = e and e:IsShown()
  local showLabel = showHandles or (not panelLocked)
  if frame.__lid_label then
    frame.__lid_label:SetText(frame.__lid_name .. (panelLocked and " (LOCKED)" or " (UNLOCKED)"))
    frame.__lid_label:SetShown(showLabel)
  end
  if frame.__lid_grip then frame.__lid_grip:SetShown(allowMove) end
end

local function CreatePanel(panelName, cfg)
  local frame = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
  frame.__lid_name = panelName
  local initW, initH = ClampPanelSize(cfg.w, cfg.h)
  frame:SetSize(initW, initH)
  frame:SetClampedToScreen(true)
  if frame.SetClampRectInsets then frame:SetClampRectInsets(0, 0, 0, 0) end
  frame:SetFrameStrata(cfg.strata or "BACKGROUND")

  ApplyBorder(frame, cfg.border)

  local fill = frame:CreateTexture(nil, "BACKGROUND")
  fill:SetAllPoints(true)
  fill:SetTexture("Interface/Buttons/WHITE8x8")
  frame.__lid_colorTex = fill

  local model = CreateFrame("PlayerModel", nil, frame)
  model:SetAllPoints(true)
  model:SetFrameStrata(frame:GetFrameStrata())
  model:SetFrameLevel(frame:GetFrameLevel())
  model:EnableMouse(false)
  model:Hide()
  frame.__lid_model = model

  local overlay = CreateFrame("Frame", nil, UIParent)
  overlay.__lid_name = panelName
  overlay:SetSize(initW, initH)
  overlay:SetClampedToScreen(true)
  if overlay.SetClampRectInsets then overlay:SetClampRectInsets(0, 0, 0, 0) end
  overlay:SetFrameStrata(GetEffectiveUnlockedStrata(cfg.strata or "BACKGROUND"))
  overlay:EnableMouse(false)
  overlay:Hide()
  frame.__lid_editOverlay = overlay

  ApplyPosition(panelName, frame, cfg)
  ApplyFill(frame, cfg)

  local label = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  label:SetPoint("TOPLEFT", 6, -6)
  label:SetText(panelName .. " (UNLOCKED)")
  label:Hide()
  frame.__lid_label = label

  frame:HookScript("OnShow", function()
    RefreshOverlayUpdaterState()
  end)
  frame:HookScript("OnHide", function()
    RefreshOverlayUpdaterState()
  end)

  frame:SetResizable(true)
  do
    local minW, minH, maxW, maxH = GetPanelResizeLimits()
    if frame.SetMinResize then frame:SetMinResize(minW, minH) end
    if frame.SetMaxResize then frame:SetMaxResize(maxW, maxH) end
    if overlay.SetMinResize then overlay:SetMinResize(minW, minH) end
    if overlay.SetMaxResize then overlay:SetMaxResize(maxW, maxH) end
  end

  local grip = CreateFrame("Button", nil, overlay)
  grip:SetSize(16, 16)
  grip:SetPoint("BOTTOMRIGHT", -2, 2)
  grip:Hide()

  local gripTex = grip:CreateTexture(nil, "ARTWORK")
  gripTex:SetAllPoints(true)
  gripTex:SetTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Up")

  local function GetResizeTarget()
    return frame.__lid_resizeTarget or overlay
  end

  local function ApplyManualResize(target, newW, newH)
    if not target then return end
    local w, h, minW, minH = ClampPanelSize(newW, newH)
    if target.__lid_resizeLeft and target.__lid_resizeTop then
      local uiLeft = UIParent:GetLeft()
      local uiRight = UIParent:GetRight()
      local uiBottom = UIParent:GetBottom()
      if uiLeft and uiRight then
        local maxWFromLeft = math.floor(uiRight - target.__lid_resizeLeft)
        if maxWFromLeft < minW then maxWFromLeft = minW end
        if w > maxWFromLeft then w = maxWFromLeft end
      end
      if uiBottom then
        local maxHFromTop = math.floor(target.__lid_resizeTop - uiBottom)
        if maxHFromTop < minH then maxHFromTop = minH end
        if h > maxHFromTop then h = maxHFromTop end
      end
    end

    target.__lid_resizeApplying = true
    target:SetSize(w, h)
    if target.__lid_resizeLeft and target.__lid_resizeTop then
      local ux, uy = UIParent:GetCenter()
      if ux and uy then
        local cx = target.__lid_resizeLeft + (w * 0.5)
        local cy = target.__lid_resizeTop - (h * 0.5)
        SetCenterOffsets(target, cx - ux, cy - uy)
      end
    end
    target.__lid_resizeApplying = nil

    if target == overlay then
      SyncFrameFromEditOverlay(frame)
    elseif frame.__lid_editOverlay and frame.__lid_editOverlay:IsShown() then
      SyncEditOverlay(frame)
    end
  end

  local function FinishResize()
    local target = GetResizeTarget()
    local wasResizing = target.__lid_resizing == true
    if wasResizing then
      target:StopMovingOrSizing()
    end
    target.__lid_resizing = false
    target.__lid_resizePending = false
    target.__lid_resizeLeft = nil
    target.__lid_resizeTop = nil
    target.__lid_resizeStartCursorX = nil
    target.__lid_resizeStartCursorY = nil
    target.__lid_resizeStartWidth = nil
    target.__lid_resizeStartHeight = nil
    target.__lid_resizeApplying = nil
    frame.__lid_resizeSuppressDrag = nil
    if overlay then overlay.__lid_resizeSuppressDrag = nil end
    if not wasResizing then
      return
    end

    if target == overlay then
      SyncFrameFromEditOverlay(frame)
    end
    local w, h = target:GetSize()
    w, h = ClampPanelSize(w, h)
    target.__lid_syncing = true
    target:SetSize(w, h)
    target.__lid_syncing = nil
    if target == overlay then
      SyncFrameFromEditOverlay(frame)
    else
      if frame.__lid_editOverlay and frame.__lid_editOverlay:IsShown() then
        SyncEditOverlay(frame)
      end
    end
    local db = DB()
    local panelDb = db.panels[panelName]
    if not panelDb then
      return
    end
    panelDb.w = math.floor(w + 0.5)
    panelDb.h = math.floor(h + 0.5)
    SavePosition(panelName, target)
    SyncPositionSliders(panelName)

    -- LIVE sync sliders if this panel is selected in the editor
    local e = GetPanelsEditor()
    local selected = GetSelectedPanelName()
    if e and e:IsShown() and selected == panelName and e.wSlider and e.hSlider then
      e.wSlider:SetValueSilent(panelDb.w)
      e.hSlider:SetValueSilent(panelDb.h)
    end
  end

  overlay:SetScript("OnMouseDown", function(self, button)
    if button and button ~= "LeftButton" then return end
    TrySelectPanelForEditing(panelName)
    RaisePanelForEditing(frame)
  end)

  overlay:SetScript("OnDragStart", function(self)
    local p = DB().panels[panelName]
    if not unlocked or (p and p.locked) then return end
    if self.__lid_resizePending or self.__lid_resizing or self.__lid_resizeSuppressDrag then
      self.__lid_dragging = false
      self:SetScript("OnUpdate", nil)
      return
    end
    TrySelectPanelForEditing(panelName)
    RaisePanelForEditing(frame)
    self:StartMoving()
    self.__lid_dragging = true
    self:SetScript("OnUpdate", function(s, elapsed)
      if not s.__lid_dragging then return end
      SyncFrameFromEditOverlay(frame)
    end)
  end)

  overlay:SetScript("OnDragStop", function(self)
    if self.__lid_resizePending or self.__lid_resizing or self.__lid_resizeSuppressDrag or not self.__lid_dragging then
      self.__lid_dragging = false
      self:SetScript("OnUpdate", nil)
      return
    end
    self:StopMovingOrSizing()
    self.__lid_dragging = false
    self:SetScript("OnUpdate", nil)
    SyncFrameFromEditOverlay(frame)
    SavePosition(panelName, frame)
    SyncPositionSliders(panelName)
  end)

  overlay:SetScript("OnHide", function(self)
    self:SetScript("OnUpdate", nil)
    self.__lid_dragging = false
    FinishResize()
  end)

  grip:SetScript("OnMouseDown", function(_, button)
    if button and button ~= "LeftButton" then return end
    if not unlocked then return end
    local p = DB().panels[panelName]
    if p and p.locked then return end

    local target = GetResizeTarget()
    frame.__lid_resizeSuppressDrag = true
    if overlay then overlay.__lid_resizeSuppressDrag = true end
    target.__lid_resizing = false
    target.__lid_resizePending = true
    local scale = UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
    if scale == 0 then scale = 1 end
    local startX, startY = GetCursorPosition()
    target.__lid_resizeStartCursorX = (startX or 0) / scale
    target.__lid_resizeStartCursorY = (startY or 0) / scale
    target.__lid_resizeStartWidth, target.__lid_resizeStartHeight = target:GetSize()
    target.__lid_resizeLeft = target:GetLeft()
    target.__lid_resizeTop = target:GetTop()
    if not target.__lid_resizeLeft or not target.__lid_resizeTop then
      frame.__lid_resizeSuppressDrag = nil
      if overlay then overlay.__lid_resizeSuppressDrag = nil end
      target.__lid_resizePending = false
      target.__lid_resizeLeft = nil
      target.__lid_resizeTop = nil
      target.__lid_resizeStartCursorX = nil
      target.__lid_resizeStartCursorY = nil
      return
    end

    grip:SetScript("OnUpdate", function()
      local activeTarget = GetResizeTarget()
      if not (activeTarget.__lid_resizePending or activeTarget.__lid_resizing) then
        grip:SetScript("OnUpdate", nil)
        return
      end

      local leftDown = (IsMouseButtonDown and IsMouseButtonDown("LeftButton")) or true
      if not leftDown then
        grip:SetScript("OnUpdate", nil)
        FinishResize()
        return
      end

      local scale = UIParent and UIParent.GetEffectiveScale and UIParent:GetEffectiveScale() or 1
      if scale == 0 then scale = 1 end
      local cx, cy = GetCursorPosition()
      if not cx or not cy then return end
      cx = cx / scale
      cy = cy / scale
      local sx = activeTarget.__lid_resizeStartCursorX or cx
      local sy = activeTarget.__lid_resizeStartCursorY or cy
      local moved = math.abs(cx - sx) + math.abs(cy - sy)
      if not activeTarget.__lid_resizing then
        if moved < 2 then
          return
        end
        activeTarget.__lid_resizePending = false
        activeTarget.__lid_resizing = true
      end

      local startW = activeTarget.__lid_resizeStartWidth or activeTarget:GetWidth() or 0
      local startH = activeTarget.__lid_resizeStartHeight or activeTarget:GetHeight() or 0
      local dx = cx - sx
      local dy = sy - cy
      ApplyManualResize(activeTarget, startW + dx, startH + dy)
    end)
  end)

  grip:SetScript("OnMouseUp", function(_, button)
    if button and button ~= "LeftButton" then return end
    grip:SetScript("OnUpdate", nil)
    FinishResize()
  end)

  grip:SetScript("OnHide", function()
    grip:SetScript("OnUpdate", nil)
    FinishResize()
  end)

  overlay:SetScript("OnSizeChanged", function(self)
    if not unlocked then return end
    local p = DB().panels[panelName]
    if p and p.locked then return end
    if self.__lid_syncing or self.__lid_resizeApplying then return end

    local minW, minH, maxW, maxH = GetPanelResizeLimits()
    if self.SetMinResize then self:SetMinResize(minW, minH) end
    if self.SetMaxResize then self:SetMaxResize(maxW, maxH) end

    local w, h = self:GetSize()
    local clampedW, clampedH = ClampPanelSize(w, h)
    if (math.abs(clampedW - w) > 0.01 or math.abs(clampedH - h) > 0.01) and not self.__lid_sizeClampGuard then
      self.__lid_sizeClampGuard = true
      self:SetSize(clampedW, clampedH)
      self.__lid_sizeClampGuard = nil
      return
    end

    SyncFrameFromEditOverlay(frame)
  end)

  frame:SetScript("OnSizeChanged", function(self)
    if not unlocked then return end
    local p = DB().panels[self.__lid_name]
    if p and p.locked then return end
    if self.__lid_resizeApplying then return end
    if self.__lid_previewSyncing and self.__lid_editOverlay and self.__lid_editOverlay:IsShown() then
      return
    end

    local minW, minH, maxW, maxH = GetPanelResizeLimits()
    if self.SetMinResize then self:SetMinResize(minW, minH) end
    if self.SetMaxResize then self:SetMaxResize(maxW, maxH) end

    local w, h = self:GetSize()
    local clampedW, clampedH = ClampPanelSize(w, h)
    if (math.abs(clampedW - w) > 0.01 or math.abs(clampedH - h) > 0.01) and not self.__lid_sizeClampGuard then
      self.__lid_sizeClampGuard = true
      self:SetSize(clampedW, clampedH)
      self.__lid_sizeClampGuard = nil
      return
    end
    w, h = clampedW, clampedH

    if self.__lid_resizing then
      if self.__lid_editOverlay and self.__lid_editOverlay:IsShown() then
        SyncEditOverlay(self)
      end
      return
    end

    local db = DB()
    local name = self.__lid_name
    if name and db.panels[name] then
      db.panels[name].w = math.floor(w + 0.5)
      db.panels[name].h = math.floor(h + 0.5)
    end
    SavePosition(name, self)

    local e = GetPanelsEditor()
    local selected = GetSelectedPanelName()
    if e and e:IsShown() and selected == name and e.wSlider and e.hSlider then
      e.wSlider:SetValueSilent(db.panels[name].w)
      e.hSlider:SetValueSilent(db.panels[name].h)
    end

    if self.__lid_editOverlay and self.__lid_editOverlay:IsShown() then
      SyncEditOverlay(self)
    end
  end)

  frame.__lid_grip = grip
  SyncEditOverlay(frame)
  SetUnlocked(frame, unlocked)
  frames[panelName] = frame
  RefreshOverlayUpdaterState()
end

local function DestroyPanelRuntime(panelName)
  local f = frames[panelName]
  if not f then return end

  local overlay = f.__lid_editOverlay
  local grip = f.__lid_grip

  if grip then
    grip:SetScript("OnUpdate", nil)
    grip:SetScript("OnMouseDown", nil)
    grip:SetScript("OnMouseUp", nil)
    grip:SetScript("OnHide", nil)
    grip:Hide()
  end

  if overlay then
    overlay:SetScript("OnUpdate", nil)
    overlay:SetScript("OnMouseDown", nil)
    overlay:SetScript("OnDragStart", nil)
    overlay:SetScript("OnDragStop", nil)
    overlay:SetScript("OnHide", nil)
    overlay:SetScript("OnSizeChanged", nil)
    overlay:EnableMouse(false)
    overlay:RegisterForDrag()
    overlay:Hide()
  end

  f:SetScript("OnMouseDown", nil)
  f:SetScript("OnDragStart", nil)
  f:SetScript("OnDragStop", nil)
  f:SetScript("OnUpdate", nil)
  f:SetScript("OnSizeChanged", nil)
  f:EnableMouse(false)
  f:RegisterForDrag()
  f:Hide()

  frames[panelName] = nil
  RefreshOverlayUpdaterState()
end

function RebuildAllPanels()
  local db = DB()

  for name, cfg in pairs(db.panels) do
    local f = frames[name]
    if not f then
      CreatePanel(name, cfg)
      f = frames[name]
    end
    f.__lid_name = name
    local clampedW, clampedH = ClampPanelSize(cfg.w, cfg.h)
    cfg.w, cfg.h = clampedW, clampedH
    f:SetSize(clampedW, clampedH)
    f:SetFrameStrata(cfg.strata or "BACKGROUND")
    ApplyBorder(f, cfg.border)
    ApplyPosition(name, f, cfg)
    ApplyFill(f, cfg)
    SetUnlocked(f, unlocked)
    f:Show()
  end

  local toDestroy = {}
  for name in pairs(frames) do
    if not db.panels[name] then
      toDestroy[#toDestroy + 1] = name
    end
  end
  for i = 1, #toDestroy do
    DestroyPanelRuntime(toDestroy[i])
  end

  RefreshOverlayUpdaterState()
end

function SetAllUnlocked(isUnlocked)
  unlocked = isUnlocked
  _G.LIDPANELS_PanelsUnlocked = unlocked
  for _, f in pairs(frames) do SetUnlocked(f, unlocked) end
end

function ResetPanelToCenterDefaultSize(panelName)
  if not panelName then return end
  local db = DB()
  local p = db.panels[panelName]
  if not p then return end

  -- Default size only
  p.w, p.h = 300, 120

  -- Center it
  db.positions[panelName] = { point = "CENTER", relativePoint = "CENTER", x = 0, y = 0 }

  RebuildAllPanels()
  Print("Reset panel to center/default size: " .. panelName)
end

-- Keyboard nudge for selected panel (used by editor arrow keys).
function NudgeSelectedPanel(dx, dy)
  local selected = GetSelectedPanelName()
  if not selected then return false end
  local db = DB()
  local cfg = db.panels[selected]
  local f = frames and frames[selected]
  if not cfg or not f then return false end
  if not unlocked or (cfg.locked and cfg.locked == true) then return false end

  local pos = db.positions[selected]
  if not pos then
    SavePosition(selected, f)
    pos = db.positions[selected]
  end
  if not pos then return false end

  pos.point = pos.point or "CENTER"
  pos.relativePoint = pos.relativePoint or "CENTER"
  pos.x = (pos.x or 0) + (dx or 0)
  pos.y = (pos.y or 0) + (dy or 0)
  db.positions[selected] = pos

  f:ClearAllPoints()
  f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.x, pos.y)
  if f.__lid_editOverlay and f.__lid_editOverlay:IsShown() then
    SyncEditOverlay(f)
  end
  SyncPositionSliders(selected)
  return true
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_RebuildAllPanels", RebuildAllPanels)
PublishApi("LIDPANELS_SetAllUnlocked", SetAllUnlocked)
PublishApi("LIDPANELS_ResetPanelToCenterDefaultSize", ResetPanelToCenterDefaultSize)
PublishApi("LIDPANELS_NudgeSelectedPanel", NudgeSelectedPanel)
PublishApi("LIDPANELS_EnsureGradientTables", EnsureGradientTables)
PublishApi("LIDPANELS_DestroyPanelRuntime", DestroyPanelRuntime)
