-- LidCouncil_Panels.lua
-- ============================================================

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

local LogInfo = _G.LIDCOUNCIL_LogInfo
local LogSilent = _G.LIDCOUNCIL_LogSilent
if LogInfo then LogInfo("Panels.lua loaded") end

local API = rawget(_G, "LIDCOUNCIL_API")
local INTERNAL_API = type(API) == "table" and type(API.__internal) == "table" and API.__internal or nil
if type(INTERNAL_API) ~= "table" then
  error("LidCouncil missing internal API registry.")
end

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

local function ResolveOptionalApiFunction(name)
  local api = rawget(_G, "LIDCOUNCIL_API")
  if type(api) == "table" and type(api[name]) == "function" then
    return api[name]
  end
  local fallback = rawget(_G, name)
  if type(fallback) == "function" then
    return fallback
  end
  return nil
end

local DB = RequireApi("LIDCOUNCIL_DB", _G.LIDCOUNCIL_DB)
local RootDB = RequireApi("LIDCOUNCIL_RootDB", _G.LIDCOUNCIL_RootDB)
local Print = RequireApi("LIDCOUNCIL_Print", _G.LIDCOUNCIL_Print)
local CreatePanelsOverlayHelpers = INTERNAL_API.LIDCOUNCIL_CreatePanelsOverlayHelpers_Impl
if type(CreatePanelsOverlayHelpers) ~= "function" then
  error("LidCouncil missing panels overlay helpers.")
end

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

local function GetPanelsEditor()
  local e = _G.LIDCOUNCIL_Editor
  if e and e.__lidCouncilEditor then return e end
  return nil
end

local function GetSelectedPanelName()
  local fn = ResolveOptionalApiFunction("LIDCOUNCIL_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.LIDCOUNCIL_PanelFrames or {}
_G.LIDCOUNCIL_PanelFrames = frames

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

local panelsOverlayHelpers = CreatePanelsOverlayHelpers({
  DB = DB,
  frames = frames,
})
RefreshOverlayUpdaterState = panelsOverlayHelpers.RefreshOverlayUpdaterState
if type(RefreshOverlayUpdaterState) ~= "function" then
  error("LidCouncil missing panels overlay refresh helper.")
end

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 = ResolveOptionalApiFunction("LIDCOUNCIL_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 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.LIDCOUNCIL_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

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

if type(API) ~= "table" then
  error("LidCouncil missing API table")
end

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

PublishApi("LIDCOUNCIL_RebuildAllPanels", RebuildAllPanels)
PublishApi("LIDCOUNCIL_SetAllUnlocked", SetAllUnlocked)
PublishApi("LIDCOUNCIL_ResetPanelToCenterDefaultSize", ResetPanelToCenterDefaultSize)
PublishApi("LIDCOUNCIL_NudgeSelectedPanel", NudgeSelectedPanel)
PublishApi("LIDCOUNCIL_EnsureGradientTables", EnsureGradientTables)
PublishApi("LIDCOUNCIL_DestroyPanelRuntime", DestroyPanelRuntime)
