-- ============================================================
-- FILE: Core.lua
-- MODULE: MidnightUI_Core  (_G.MidnightUI_Core, local M)
--
-- PURPOSE:
--   The central utility and lifecycle hub of MidnightUI. Loaded
--   second (after Bootstrap.lua), it provides every shared system
--   the rest of the addon depends on: debug logging, taint-safe
--   helpers, the module registration/init pipeline, overlay frame
--   management, frame snapping, pixel nudging, global scale, and
--   the overlay settings UI. Almost every other file calls into
--   something defined here.
--
-- HOW IT WORKS (lifecycle):
--   1. File-scope: localizes globals, builds M table, registers
--      taint/debug infrastructure.
--   2. ADDON_LOADED "MidnightUI" → EnsureSettings(), FlushDebugQueue(),
--      InitModules("ADDON_LOADED")
--   3. PLAYER_LOGIN → InitModules("PLAYER_LOGIN"), shows Welcome screen
--   4. PLAYER_ENTERING_WORLD → InitModules("PLAYER_ENTERING_WORLD")
--      (deferred if in combat)
--   5. PLAYER_REGEN_ENABLED → FlushDeferred() runs any combat-blocked work
--
-- DEPENDS ON:
--   Bootstrap.lua  — MidnightUI_DebugQueue must exist before M.Debug runs
--   Settings.lua   — MidnightUISettings SavedVar (read via EnsureSettings)
--
-- EXPORTS — functions other files call:
--   M.Debug(...)                    — log a debug message
--   M.SafeCall(label, fn, ...)      — pcall wrapper with debug logging
--   M.RunOrDefer(label, fn)         — run fn now or after combat ends
--   M.FlushDeferred()               — drain the deferred queue (called on regen)
--   M.EnsureSettings()              — guarantee all SavedVar subtables exist
--   M.RegisterModule(name, def)     — register a module for lifecycle init
--   M.InitModule(name, phase)       — run one module's init/apply
--   M.InitModules(phase)            — run all registered modules
--   M.CleanNumber(val, fallback)    — taint-safe number coercion
--   M.GetClassColor(unitOrClass)    — returns r,g,b for a unit or class
--   M.ResolveFontPath(path)         — safe font path, avoids legacy paths
--   M.ApplyGlobalScale(newScale)    — proportionally rescale all frames
--   M.BeginFrameSnap(frame)         — start snap-to-grid/screen drag mode
--   M.EndFrameSnap(frame)           — apply best snap when drag ends
--   M.EnterNudgeMode(frame,ovl,key) — enter arrow-key pixel nudge mode
--   M.ExitNudgeMode()               — exit nudge mode
--   M.NudgeSelected(dx, dy)         — move nudged frame by dx/dy pixels
--   M.ShowOverlaySettings(key)      — open the overlay settings panel
--   M.StyleButton(button)           — apply MidnightUI button skin
--   M.TaintTrace(tag)               — log a taint trace entry
--
-- KEY GLOBALS EXPORTED (for cross-module access without require):
--   MidnightUI_FormatHealthText     — health text formatter
--   MidnightUI_CleanNumber          — taint-safe number coercion
--   MidnightUI_ApplyGlobalScale     — global scale entry point
--   MidnightUI_StyleButton          — button skin entry point
--   MidnightUI_RegisterOverlaySettings / ShowOverlaySettings /
--   AttachOverlaySettings / CreateOverlayBuilder / GetOverlayHandle /
--   StyleOverlay / SaveOverlayPosition / ApplyOverlayPosition /
--   BeginFrameSnap / EndFrameSnap
--
-- OWNED SAVEDVARIABLES:
--   MidnightUISettings.General.debug
--   MidnightUISettings.General.debugToChat
--   MidnightUISettings.General.lowMemory
--   MidnightUISettings.General.snapToFrames
--   MidnightUISettings.General.globalScale
--   MidnightUISettings.General.taintTrace
--
-- EVENTS REGISTERED:
--   ADDON_LOADED, PLAYER_LOGIN, PLAYER_ENTERING_WORLD,
--   PLAYER_REGEN_ENABLED, ADDON_ACTION_BLOCKED, ADDON_ACTION_FORBIDDEN
--
-- SLASH COMMANDS:
--   /muidump / /muidumppositions  — dumps all SavedVar settings to diagnostics
--
-- GOTCHAS:
--   • M.CleanNumber uses tonumber(tostring(val)) — the tostring() breaks
--     the taint chain on secret WoW values. Never use plain tonumber().
--   • M.RunOrDefer is critical for any frame SetPoint/SetSize call that
--     might happen during combat — WoW blocks those in lockdown.
--   • Frame snap (BeginFrameSnap/EndFrameSnap) reads MidnightUISettings
--     .General.snapToFrames — if false, snap is silently skipped.
--   • The overlay right-click menu only opens when Messenger is unlocked
--     (MidnightUISettings.Messenger.locked == false).
--   • M.DEFAULTS is set externally by Settings.lua after it loads — do
--     not reference M.DEFAULTS before ADDON_LOADED fires.
-- ============================================================

-- ============================================================
-- SECTION: Module Initialization + Global Localization
-- Localizes frequently-called WoW globals for performance.
-- Creates or resumes the MidnightUI_Core module table.
-- Everything below this point uses M instead of _G.MidnightUI_Core.
--
-- TABLES CREATED:
--   M.Modules       — registered module definitions keyed by name
--   M._deferQueue   — combat-deferred function queue
--   M._moduleState  — tracks which modules have been initialized
-- ============================================================
local _G = _G
local CreateFrame = CreateFrame
local InCombatLockdown = InCombatLockdown
local pcall = pcall
local tostring = tostring
local type = type
local wipe = wipe

local M = _G.MidnightUI_Core or {}
_G.MidnightUI_Core = M

M.Version = "1.0"
M.Modules = M.Modules or {}
M._deferQueue = M._deferQueue or {}
M._moduleState = M._moduleState or {}


-- ============================================================
-- SECTION: Taint and Secret Value Utilities
-- WoW restricts access to certain values during instanced combat.
-- These helpers let code safely check whether a value is tainted
-- before operating on it, preventing Lua errors and forbidden-action
-- events that would break the addon mid-fight.
--
-- FUNCTIONS:
--   IsSecretValue(value)     — returns true if value is a WoW
--                              "secret" (tainted) value; wraps
--                              issecretvalue() in pcall for safety
--   CanUseUnitToken(unit)    — returns true if unit is a plain,
--                              untainted string safe to pass to
--                              WoW unit APIs like UnitHealth()
-- ============================================================
local function IsSecretValue(value)
    if type(issecretvalue) ~= "function" then return false end
    local ok, result = pcall(issecretvalue, value)
    return ok and result == true
end



local function CanUseUnitToken(unit)
    if IsSecretValue(unit) then
        return false
    end
    return type(unit) == "string"
end

-- ============================================================
-- SECTION: Font System
-- MidnightUI originally shipped with hardcoded font paths that
-- no longer exist in Midnight. ResolveFontPath() falls back to
-- safe system fonts, ensuring text never goes blank on upgrade.
-- Used by every module that creates a FontString.
--
-- CONSTANTS:
--   LEGACY_MIDNIGHT_FONT_PATHS  — set of old font paths to reject
--
-- FUNCTIONS:
--   GetFontPathFromObject(fontObject)  — extracts the font path
--                                        string from a WoW font
--                                        object via pcall
--   ResolveFontPath(fontPath)          — returns fontPath if valid,
--                                        otherwise falls back to
--                                        ChatFontNormal / GameFontNormal
--                                        / ARIALN.TTF in that order
--
-- EXPORTS:
--   M.ResolveFontPath                  — module-scoped access
--   _G.MidnightUI_ResolveFontPath      — global access for other files
-- ============================================================
local LEGACY_MIDNIGHT_FONT_PATHS = {
    ["Fonts\\FRIZQT__.TTF"] = true,
    ["Fonts\\ARIALN.TTF"] = true,
}

local function GetFontPathFromObject(fontObject)
    if not fontObject or not fontObject.GetFont then return nil end
    local ok, fontPath = pcall(function()
        local path = fontObject:GetFont()
        return path
    end)
    if ok and type(fontPath) == "string" and fontPath ~= "" then
        return fontPath
    end
    return nil
end

local function ResolveFontPath(fontPath)
    if type(fontPath) == "string" and fontPath ~= "" and not LEGACY_MIDNIGHT_FONT_PATHS[fontPath] then
        return fontPath
    end

    local preferredFont = GetFontPathFromObject(_G.ChatFontNormal)
        or GetFontPathFromObject(_G.GameFontNormal)
        or GetFontPathFromObject(_G.SystemFont_Shadow_Med1)
    if preferredFont then
        return preferredFont
    end

    if type(fontPath) == "string" and fontPath ~= "" then
        if LEGACY_MIDNIGHT_FONT_PATHS[fontPath] then
            return "Fonts\\ARIALN.TTF"
        end
        return fontPath
    end

    return "Fonts\\ARIALN.TTF"
end

M.ResolveFontPath = ResolveFontPath
_G.MidnightUI_ResolveFontPath = ResolveFontPath


-- ============================================================
-- SECTION: Health Text Formatting
-- Formats the numeric health display shown on unit frame bars.
-- Reads MidnightUISettings.General.healthTextMode to choose
-- between three display styles. All string.format calls are
-- wrapped in pcall because current/max can be tainted secret
-- values inside instances.
--
-- MODES (set in Settings → General):
--   "percent"     — "72%"
--   "value"       — "145k / 200k"
--   "both"        — "145k / 200k · 72%"
--
-- FUNCTIONS:
--   FormatHealthText(current, max, pct)  — returns display string
--                                          or nil if values are
--                                          unreadable (tainted)
--
-- EXPORTS:
--   _G.MidnightUI_FormatHealthText       — called by PlayerFrame,
--                                          TargetFrame, FocusFrame
-- ============================================================
local function FormatHealthText(current, max, pct)
    local gen = MidnightUISettings and MidnightUISettings.General
    local mode = gen and gen.healthTextMode or "percent"

    if mode == "percent" then
        if pct == nil then return nil end
        local ok, text = pcall(string.format, "%.0f%%", pct)
        return ok and text or nil
    end

    if type(AbbreviateNumbers) ~= "function" then
        if pct == nil then return nil end
        local ok, text = pcall(string.format, "%.0f%%", pct)
        return ok and text or nil
    end

    local okCur, curStr = pcall(AbbreviateNumbers, current)
    local okMax, maxStr = pcall(AbbreviateNumbers, max)
    if not okCur or not okMax then
        if pct == nil then return nil end
        local ok, text = pcall(string.format, "%.0f%%", pct)
        return ok and text or nil
    end

    if mode == "value" then
        local ok, text = pcall(string.format, "%s / %s", curStr, maxStr)
        return ok and text or nil
    end

    if pct ~= nil then
        local ok, text = pcall(string.format, "%s / %s \194\183 %.0f%%", curStr, maxStr, pct)
        return ok and text or nil
    end
    local ok, text = pcall(string.format, "%s / %s", curStr, maxStr)
    return ok and text or nil
end

_G.MidnightUI_FormatHealthText = FormatHealthText


-- ============================================================
-- SECTION: Class Color System
-- Provides exact class colors consistent with Midnight's palette.
-- Falls back through three sources: hardcoded table → RAID_CLASS_COLORS
-- → C_ClassColor API, ensuring colors are always returned even if
-- one source is missing.
--
-- CONSTANTS:
--   M.ClassColorsExact   — hardcoded {r,g,b} per class (all 13 classes)
--
-- FUNCTIONS:
--   M.GetClassColor(unitOrClass)       — accepts either a unit token
--                                        ("player", "target") or a
--                                        class name string ("WARRIOR");
--                                        returns r, g, b (0–1 range)
--   M.GetClassColorTable(unitOrClass)  — same but returns {r,g,b} table
-- ============================================================
M.ClassColorsExact = {
    DEATHKNIGHT = { r = 0.77, g = 0.12, b = 0.23 },
    DEMONHUNTER = { r = 0.64, g = 0.19, b = 0.79 },
    DRUID = { r = 1.00, g = 0.49, b = 0.04 },
    EVOKER = { r = 0.20, g = 0.58, b = 0.50 },
    HUNTER = { r = 0.67, g = 0.83, b = 0.45 },
    MAGE = { r = 0.25, g = 0.78, b = 0.92 },
    MONK = { r = 0.00, g = 1.00, b = 0.60 },
    PALADIN = { r = 0.96, g = 0.55, b = 0.73 },
    PRIEST = { r = 1.00, g = 1.00, b = 1.00 },
    ROGUE = { r = 1.00, g = 0.96, b = 0.41 },
    SHAMAN = { r = 0.00, g = 0.44, b = 0.87 },
    WARLOCK = { r = 0.53, g = 0.53, b = 0.93 },
    WARRIOR = { r = 0.78, g = 0.61, b = 0.43 },
}

function M.GetClassColor(unitOrClass)
    if not unitOrClass then return 1, 1, 1 end
    local class = unitOrClass
    if UnitExists and CanUseUnitToken(unitOrClass) and UnitExists(unitOrClass) then
        local _, c = UnitClass(unitOrClass)
        class = c
    end
    -- Blizzard's 11.x secret-value system can route a tainted string
    -- through here (chat-message author paths during a Mythic+ key).
    -- Using a secret value as a table index throws, so every lookup
    -- is wrapped in pcall and we fall back to the default colour on
    -- failure.
    if class then
        local ok1, hit1 = pcall(function() return M.ClassColorsExact[class] end)
        if ok1 and hit1 then
            return hit1.r, hit1.g, hit1.b
        end
        if RAID_CLASS_COLORS then
            local ok2, hit2 = pcall(function() return RAID_CLASS_COLORS[class] end)
            if ok2 and hit2 then
                return hit2.r or hit2[1], hit2.g or hit2[2], hit2.b or hit2[3]
            end
        end
        if C_ClassColor and C_ClassColor.GetClassColor then
            local ok3, hit3 = pcall(C_ClassColor.GetClassColor, class)
            if ok3 and hit3 then return hit3.r, hit3.g, hit3.b end
        end
    end
    return 1, 1, 1
end

function M.GetClassColorTable(unitOrClass)
    local r, g, b = M.GetClassColor(unitOrClass)
    return { r = r, g = g, b = b }
end



-- ============================================================
-- SECTION: Debug and Logging System
-- Provides M.Debug(), taint tracing, and the ADDON_ACTION
-- event watcher. M.Debug() stores messages in MidnightUI_DebugLog
-- (capped at DEBUG_MAX entries) and optionally echoes to chat.
-- TaintTrace logs stack traces when ADDON_ACTION_BLOCKED or
-- ADDON_ACTION_FORBIDDEN fires, helping diagnose what triggered
-- a restricted-action violation.
--
-- CONSTANTS:
--   DEBUG_MAX   — maximum entries in MidnightUI_DebugLog (500)
--
-- FUNCTIONS:
--   SafeToString(value)          — local taint-safe tostring;
--                                  returns "[Restricted]" on failure
--   M.TaintTraceEnabled()        — returns true if taintTrace is on
--   M.TaintTrace(tag)            — writes a tagged trace + stack to log
--   M.SetDebug(enabled, toChat)  — enables/disables debug output;
--                                  toChat=true also echoes to chat frame
--   M.Debug(...)                 — main debug logger; no-op if disabled;
--                                  appends to MidnightUI_DebugLog
--   M.FlushDebugQueue()          — drains Bootstrap's pre-init queue
--                                  into M.Debug; wipes queue afterward
--
-- FRAMES CREATED:
--   TaintTraceFrame   — listens for ADDON_ACTION_BLOCKED /
--                       ADDON_ACTION_FORBIDDEN events
-- ============================================================
local W8 = "Interface\\Buttons\\WHITE8x8"


function M.ApplyTooltipAnchorSettings() end
_G.MidnightUI_ApplyTooltipAnchorSettings = function() end


local DEBUG_MAX = 500

local function SafeToString(value)
    if value == nil then return "nil" end
    local ok, s = pcall(tostring, value)
    if not ok then return "[Restricted]" end
    local ok2 = pcall(function() return s .. "" end)
    if not ok2 then return "[Restricted]" end
    return s
end

function M.TaintTraceEnabled()
    return MidnightUISettings and MidnightUISettings.General and MidnightUISettings.General.taintTrace == true
end

function M.TaintTrace(tag)
    if not M.TaintTraceEnabled() then return end
    local stack = ""
    if debugstack then
        local ok, ds = pcall(debugstack, 2, 8, 8)
        if ok and ds then stack = ds end
    end
    if _G.MidnightUI_Debug then
        _G.MidnightUI_Debug("[TaintTrace] " .. tostring(tag) .. " stack=" .. tostring(stack))
    else
        _G.MidnightUI_DebugQueue = _G.MidnightUI_DebugQueue or {}
        table.insert(_G.MidnightUI_DebugQueue, "[TaintTrace] " .. tostring(tag) .. " stack=" .. tostring(stack))
    end
end

_G.MidnightUI_Settings = _G.MidnightUI_Settings or {}
_G.MidnightUI_Settings.TaintTrace = M.TaintTrace
_G.MidnightUI_Settings.TaintTraceEnabled = M.TaintTraceEnabled


local function EmitTaintMessage(tag, detail)
    if not M.TaintTraceEnabled() then return end
    local msg = "[TaintTrace] " .. tostring(tag)
    if detail then
        msg = msg .. " " .. tostring(detail)
    end
    if _G.MidnightUI_Diagnostics and _G.MidnightUI_Diagnostics.LogDebugSource
        and _G.MidnightUI_Diagnostics.IsEnabled
        and _G.MidnightUI_Diagnostics.IsEnabled() then
        _G.MidnightUI_Diagnostics.LogDebugSource("TaintTrace", msg)
        return
    end
    if _G.MidnightUI_Debug then
        _G.MidnightUI_Debug(msg)
        return
    end
    _G.MidnightUI_DebugQueue = _G.MidnightUI_DebugQueue or {}
    table.insert(_G.MidnightUI_DebugQueue, msg)
end

local TaintTraceFrame = CreateFrame("Frame")
TaintTraceFrame:RegisterEvent("ADDON_ACTION_BLOCKED")
TaintTraceFrame:RegisterEvent("ADDON_ACTION_FORBIDDEN")
TaintTraceFrame:SetScript("OnEvent", function(_, event, ...)
    if not M.TaintTraceEnabled() then return end
    if event == "ADDON_ACTION_BLOCKED" or event == "ADDON_ACTION_FORBIDDEN" then
        local addonName, action = ...
        EmitTaintMessage(event, "addon=" .. tostring(addonName) .. " action=" .. tostring(action))
        M.TaintTrace(event .. " " .. tostring(addonName) .. " " .. tostring(action))
    end
end)



local function EnsureTable(root, key)
    if not root[key] then root[key] = {} end
    return root[key]
end

function M.SafeCall(label, fn, ...)
    if type(fn) ~= "function" then return end
    local ok, res = pcall(fn, ...)
    if not ok then
        M.Debug("[Core] Error in " .. SafeToString(label) .. ": " .. SafeToString(res))
        return false, res
    end
    return true, res
end

function M.SetDebug(enabled, toChat)
    M.DebugEnabled = enabled == true
    M.DebugToChat = toChat == true
end

local function IsTooltipDebugMessage(msg)
    if type(msg) ~= "string" or msg == "" then return false end
    return msg:find("TooltipDbg", 1, true) ~= nil
        or msg:find("[TT:", 1, true) ~= nil
end

function M.Debug(...)
    if not M.DebugEnabled then return end
    local parts = {}
    for i = 1, select("#", ...) do
        parts[#parts + 1] = SafeToString(select(i, ...))
    end
    local msg = table.concat(parts, " ")
    if IsTooltipDebugMessage(msg) then
        return
    end
    local log = _G.MidnightUI_DebugLog
    if not log then
        log = {}
        _G.MidnightUI_DebugLog = log
    end
    log[#log + 1] = msg
    if #log > DEBUG_MAX then
        table.remove(log, 1)
    end
    if M.DebugToChat and _G.DEFAULT_CHAT_FRAME then
        _G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffMidnightUI:|r " .. msg)
    end
end

function M.FlushDebugQueue()
    local q = _G.MidnightUI_DebugQueue
    if not q then return end
    if not M.DebugEnabled then
        wipe(q)
        _G.MidnightUI_DebugQueue = nil
        return
    end
    for i = 1, #q do
        M.Debug(q[i])
    end
    wipe(q)
    _G.MidnightUI_DebugQueue = nil
end

-- ============================================================
-- SECTION: Settings Bootstrap + Core Utilities
-- EnsureSettings() is called on ADDON_LOADED to guarantee every
-- SavedVariable subtable exists before any module reads from it.
-- The utility functions here (SafeCall, RunOrDefer, FlushDeferred,
-- CleanNumber) are called throughout the entire addon.
--
-- FUNCTIONS:
--   M.EnsureSettings()           — creates any missing subtables in
--                                  MidnightUISettings; also applies
--                                  debug flags from saved settings
--   M.SafeCall(label, fn, ...)   — runs fn(...) in pcall; logs errors
--                                  via M.Debug; returns ok, result
--   M.RunOrDefer(label, fn)      — if InCombatLockdown(), queues fn
--                                  for after combat; otherwise runs
--                                  it immediately via SafeCall
--   M.FlushDeferred()            — runs all queued deferred functions;
--                                  called on PLAYER_REGEN_ENABLED
--   M.CleanNumber(val, fallback) — converts val to number safely via
--                                  tonumber(tostring(val)); the tostring
--                                  call breaks WoW taint chains
--   EnsureTable(root, key)       — ensures root[key] = {} if nil
-- ============================================================
function M.EnsureSettings()
    _G.MidnightUISettings = _G.MidnightUISettings or {}
    local s = _G.MidnightUISettings
    EnsureTable(s, "Messenger")
    EnsureTable(s, "General")
    EnsureTable(s, "Minimap")
    EnsureTable(s, "ActionBars")
    EnsureTable(s, "PlayerFrame")
    EnsureTable(s, "TargetFrame")
    EnsureTable(s, "FocusFrame")
    EnsureTable(s, "CastBars")
    EnsureTable(s, "Nameplates")
    EnsureTable(s, "PartyFrames")
    EnsureTable(s, "RaidFrames")
    EnsureTable(s, "MainTankFrames")
    EnsureTable(s, "StanceBar")
    EnsureTable(s, "PetBar")
    EnsureTable(s, "Market")

    if s.debug == nil then s.debug = false end
    if s.debugToChat == nil then s.debugToChat = false end
    if s.lowMemory == nil then s.lowMemory = false end
    if s.General.useBlizzardQuestingInterface == nil then
        s.General.useBlizzardQuestingInterface = false
    end
    M.SetDebug(s.debug == true, s.debugToChat == true)
end

-- ============================================================
-- SECTION: Module Registration System
-- Provides a lightweight init pipeline so each subsystem can
-- declare its own init/apply callbacks without Core.lua needing
-- to know about every file. Modules are registered here in Core
-- or self-register in their own file via M.RegisterModule().
-- InitModules(phase) is called three times: ADDON_LOADED,
-- PLAYER_LOGIN, PLAYER_ENTERING_WORLD.
--
-- MODULE DEFINITION FIELDS:
--   enabled()   — function returning true/false (optional; defaults true)
--   init(phase) — runs once per session (guarded by def.once)
--   apply(phase)— runs every phase (re-applies settings)
--   once        — if true (default), init() only runs once per session
--
-- FUNCTIONS:
--   M.RegisterModule(name, def)   — registers a module table;
--                                   name is used as the lookup key
--   M.InitModule(name, phase)     — runs one module's init + apply
--                                   for the given lifecycle phase
--   M.InitModules(phase)          — iterates all registered modules
--                                   and calls InitModule on each
--
-- MODULES REGISTERED HERE (others self-register in their own files):
--   "Messenger"   — applies Messenger + default chat visibility
--   "Nameplates"  — applies nameplate settings
--   "ActionBars"  — applies action bar settings
--   "Frames"      — applies player/target/focus/castbar settings
--   "PartyRaid"   — applies party + raid + main tank settings
--   "Minimap"     — applies minimap settings
--   "Market"      — placeholder (skipped in lowMemory mode)
-- ============================================================
function M.RegisterModule(name, def)
    if not name or type(def) ~= "table" then return end
    def.name = name
    if not def.enabled then
        def.enabled = function() return true end
    end
    if def.once == nil then def.once = true end
    M.Modules[name] = def
end

function M.InitModule(name, phase)
    local def = M.Modules[name]
    if not def then return end
    if def.enabled and def.enabled() == false then return end

    local state = M._moduleState[name] or {}
    M._moduleState[name] = state

    if def.init and (not state.inited or def.once == false) then
        M.SafeCall(name .. ".init", def.init, phase)
        state.inited = true
    end
    if def.apply then
        M.SafeCall(name .. ".apply", def.apply, phase)
    end
end

function M.InitModules(phase)
    for name, _ in pairs(M.Modules) do
        M.InitModule(name, phase)
    end
end

function M.CleanNumber(val, fallback)
    if val == nil then return fallback end
    local n = tonumber(tostring(val))
    if n == nil then return fallback end
    return n
end
_G.MidnightUI_CleanNumber = M.CleanNumber

function M.RunOrDefer(label, fn)
    if InCombatLockdown and InCombatLockdown() then
        M._deferQueue[#M._deferQueue + 1] = { label = label, fn = fn }
        return
    end
    M.SafeCall(label, fn)
end

function M.FlushDeferred()
    if InCombatLockdown and InCombatLockdown() then return end
    if #M._deferQueue == 0 then return end
    local q = M._deferQueue
    M._deferQueue = {}
    for i = 1, #q do
        local item = q[i]
        M.SafeCall(item.label, item.fn)
    end
    wipe(q)
end


M.RegisterModule("Messenger", {
    enabled = function()
        return true
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyMessengerSettings then S.ApplyMessengerSettings() end
        if _G.MidnightUI_ApplyDefaultChatInterfaceVisibility then
            _G.MidnightUI_ApplyDefaultChatInterfaceVisibility()
        end
    end,
})

M.RegisterModule("Nameplates", {
    enabled = function()
        local s = _G.MidnightUISettings
        return not (s and s.Nameplates and s.Nameplates.enabled == false)
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyNameplateSettings then S.ApplyNameplateSettings() end
    end,
})

M.RegisterModule("ActionBars", {
    enabled = function()
        local s = _G.MidnightUISettings
        if not s or not s.ActionBars then return true end
        if s.ActionBars.globalStyle and s.ActionBars.globalStyle ~= "Disabled" then return true end
        for i = 1, 8 do
            local bar = s.ActionBars["bar" .. i]
            if bar and bar.enabled ~= false then return true end
        end
        return false
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyActionBarSettings then S.ApplyActionBarSettings() end
        if _G.MyActionBars_ReloadSettingsImmediate then _G.MyActionBars_ReloadSettingsImmediate()
        elseif _G.MyActionBars_ReloadSettings then _G.MyActionBars_ReloadSettings() end
    end,
})

M.RegisterModule("Frames", {
    enabled = function()
        local s = _G.MidnightUISettings
        if not s then return true end
        local pf = s.PlayerFrame and s.PlayerFrame.enabled ~= false
        local tf = s.TargetFrame and s.TargetFrame.enabled ~= false
        local ff = s.FocusFrame and s.FocusFrame.enabled ~= false
        local cb = s.CastBars
        local cbOn = true
        if cb and cb.player and cb.target then
            cbOn = (cb.player.enabled ~= false) or (cb.target.enabled ~= false)
        end
        return pf or tf or ff or cbOn
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyPlayerSettings then S.ApplyPlayerSettings() end
        if S and S.ApplyTargetSettings then S.ApplyTargetSettings() end
        if S and S.ApplyFocusSettings then S.ApplyFocusSettings() end
        if S and S.ApplyCastBarSettings then S.ApplyCastBarSettings() end
    end,
})

M.RegisterModule("PartyRaid", {
    enabled = function()
        local s = _G.MidnightUISettings
        return not (s and s.lowMemory == true)
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyPartyFramesSettings then S.ApplyPartyFramesSettings() end
        if S and S.ApplyRaidFramesSettings then S.ApplyRaidFramesSettings() end
        if _G.MidnightUI_ApplyMainTankSettings then _G.MidnightUI_ApplyMainTankSettings() end
    end,
})

M.RegisterModule("Minimap", {
    enabled = function()
        return true
    end,
    apply = function()
        local S = _G.MidnightUI_Settings
        if S and S.ApplyMinimapSettings then S.ApplyMinimapSettings() end
    end,
})

M.RegisterModule("Market", {
    enabled = function()
        local s = _G.MidnightUISettings
        return not (s and s.lowMemory == true)
    end,
    apply = function()
    end,
})


-- ============================================================
-- SECTION: Overlay Position + Registration System
-- Every draggable MidnightUI frame has an "overlay" — a transparent
-- drag handle on top of it. This section manages registering those
-- overlays, saving their positions to SavedVars, and attaching the
-- right-click settings menu that appears when Messenger is unlocked.
--
-- TABLES:
--   M.OverlaySettings   — overlay key → settings definition (title, build fn)
--   M.OverlayHandles    — overlay key → overlay frame reference
--   OVERLAY_CATEGORY_COLORS — color per category (unit/cast/auras/bars/world)
--
-- FUNCTIONS:
--   M.RegisterOverlaySettings(key, def)         — registers right-click
--                                                 settings for an overlay
--   ResolveOverlayPositionTarget(key)            — maps an overlay key
--                                                 (e.g. "PlayerFrame") to
--                                                 its SavedVar position table
--   AttachOverlayRightClick(overlay, key)        — hooks right-click to
--                                                 open settings panel; hooks
--                                                 drag start/stop for snap
--   SaveOverlayPosition(frame, target)           — writes frame's current
--                                                 point to target.position
--   ApplyOverlayPosition(frame, position)        — restores position from
--                                                 SavedVar; returns true/false
--   SaveRelativeOverlayPosition(frame,owner,tgt) — saves offset relative
--                                                 to another frame
--   ApplyRelativeOverlayPosition(frame,owner,pos)— restores relative pos
--   ScaleFromPercent(value)                      — converts 0–100 percent
--                                                 to 0.0–1.0 multiplier
-- ============================================================
M.OverlaySettings = M.OverlaySettings or {}
M.OverlayHandles = M.OverlayHandles or {}

local OVERLAY_CATEGORY_COLORS = {
    unit   = { 0.30, 0.70, 0.95 },
    cast   = { 0.90, 0.65, 0.30 },
    auras  = { 0.65, 0.40, 0.90 },
    bars   = { 0.45, 0.85, 0.45 },
    world  = { 0.85, 0.75, 0.40 },
}
M.OVERLAY_CATEGORY_COLORS = OVERLAY_CATEGORY_COLORS

local OVERLAY_CATEGORY_LABELS = {
    unit = "Unit Frame", cast = "Cast Bar", auras = "Auras",
    bars = "Action Bar", world = "Interface",
}

function M.RegisterOverlaySettings(key, def)
    if not key or type(def) ~= "table" then return end
    M.OverlaySettings[key] = def
end

local function ResolveOverlayPositionTarget(key)
    local s = _G.MidnightUISettings
    if not s or type(key) ~= "string" then return nil end

    local barNum = tonumber(key:match("^ActionBar(%d+)$") or "")
    if barNum and s.ActionBars then
        return s.ActionBars["bar" .. barNum]
    end

    local castKind = key:match("^CastBar_(%a+)$")
    if castKind and s.CastBars and s.CastBars[castKind] then
        return s.CastBars[castKind]
    end

    local DIRECT_KEYS = {
        "Messenger", "PlayerFrame", "TargetFrame", "FocusFrame",
        "RaidFrames", "PartyFrames", "ConsumableBars", "SoulShards",
        "PetBar", "StanceBar", "PetFrame", "MainTankFrames",
        "Minimap", "InterfaceMenu", "Inventory",
        "XPBar", "RepBar", "MinimapInfoPanel",
    }
    for _, dk in ipairs(DIRECT_KEYS) do
        if key == dk then
            if not s[dk] then s[dk] = {} end
            return s[dk]
        end
    end
    if key == "PlayerAuras" then return s.PlayerFrame and s.PlayerFrame.auras end
    if key == "PlayerDebuffs" then return s.PlayerFrame and s.PlayerFrame.debuffs end
    if key == "TargetAuras" then return s.TargetFrame and s.TargetFrame.auras end
    if key == "TargetDebuffs" then return s.TargetFrame and s.TargetFrame.debuffs end
    if key == "TargetOfTarget" then return s.TargetFrame and s.TargetFrame.targetOfTarget end
    if key == "DispelTracking" or key == "PartyDispelTracking" then
        if not s.Combat then s.Combat = {} end
        return s.Combat
    end
    return nil
end

local function AttachOverlayRightClick(overlay, key)
    if not overlay or not key then return end
    if overlay._muiOverlayKey == key then return end
    overlay._muiOverlayKey = key
    M.OverlayHandles[key] = overlay
    local prev = overlay:GetScript("OnMouseUp")
    overlay:SetScript("OnMouseUp", function(self, button, ...)
        if button == "RightButton" and _G.MidnightUI_ShowOverlaySettings
            and MidnightUISettings and MidnightUISettings.Messenger and MidnightUISettings.Messenger.locked == false then
            if M.ExitNudgeMode then M.ExitNudgeMode() end
            local f = _G.MidnightUI_OverlaySettingsFrame
            if f and f:IsShown() and f._muiKey == key then
                f:Hide()
            else
                M._lastClickedOverlay = self
                _G.MidnightUI_ShowOverlaySettings(key)
            end
            return
        end
        if prev then
            prev(self, button, ...)
        end
        if button == "LeftButton" and _G.MidnightUI_SaveOverlayPosition then
            local host = self.GetParent and self:GetParent() or nil
            if host then
                local target = ResolveOverlayPositionTarget(key)
                if target then
                    _G.MidnightUI_SaveOverlayPosition(host, target)
                end
            end
        end
        if button == "LeftButton" then
            local wasDrag = self._muiDidDrag
            self._muiDidDrag = false
            if not wasDrag and M.EnterNudgeMode then
                local host = self.GetParent and self:GetParent() or nil
                if host then M.EnterNudgeMode(host, self, key) end
            end
        end
    end)

    if overlay.HookScript then
        pcall(overlay.HookScript, overlay, "OnDragStart", function(self)
            self._muiDidDrag = true
            if M._snapActive then return end
            local host = self.GetParent and self:GetParent() or nil
            if host and M.BeginFrameSnap then M.BeginFrameSnap(host) end
        end)
        pcall(overlay.HookScript, overlay, "OnDragStop", function(self)
            self._muiDidDrag = true
            if not M._snapActive then return end
            local host = self.GetParent and self:GetParent() or nil
            if host and M.EndFrameSnap then M.EndFrameSnap(host) end
            local MR = _G.MidnightUI_MoverRegistry
            if MR and MR.SavePosition then pcall(MR.SavePosition, key) end
        end)
    end
end


local function SaveOverlayPosition(frame, target)
    if not frame or not target then return end
    local point, _, relativePoint, xOfs, yOfs = frame:GetPoint()
    if not point or not relativePoint or not xOfs or not yOfs then return end
    local scale = frame.GetScale and frame:GetScale()
    if not scale or scale == 0 then scale = 1 end
    target.position = { point, relativePoint, xOfs / scale, yOfs / scale }
end

local function ApplyOverlayPosition(frame, position)
    if not frame or not position or #position < 4 then return false end
    local scale = frame.GetScale and frame:GetScale()
    if not scale or scale == 0 then scale = 1 end
    frame:ClearAllPoints()
    if #position >= 5 and type(position[4]) == "number" and type(position[5]) == "number" then
        frame:SetPoint(position[1], UIParent, position[3], position[4] * scale, position[5] * scale)
    else
        frame:SetPoint(position[1], UIParent, position[2], position[3] * scale, position[4] * scale)
    end
    return true
end

local function SaveRelativeOverlayPosition(frame, ownerFrame, target)
    if not frame or not ownerFrame or not target then return end
    if not frame.GetLeft or not ownerFrame.GetLeft then return end
    if not frame.GetTop or not ownerFrame.GetBottom then return end
    local frameLeft = frame:GetLeft()
    local ownerLeft = ownerFrame:GetLeft()
    local frameTop = frame:GetTop()
    local ownerBottom = ownerFrame:GetBottom()
    if not frameLeft or not ownerLeft or not frameTop or not ownerBottom then return end
    target.positionRelative = { frameLeft - ownerLeft, frameTop - ownerBottom }
end

local function ApplyRelativeOverlayPosition(frame, ownerFrame, position)
    if not frame or not ownerFrame or not position or #position < 2 then return false end
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", ownerFrame, "BOTTOMLEFT", position[1], position[2])
    return true
end

local function ScaleFromPercent(value)
    if type(value) ~= "number" then return 1 end
    return value / 100
end


-- ============================================================
-- SECTION: Frame Snap System
-- When the player drags an overlay frame, this system shows
-- visual snap guides (ghost outline, alignment poles, gap labels)
-- and snaps the frame to screen edges, screen center, quarter
-- points, neighbor frame edges, or a 20px grid. Snap activates
-- when a frame gets within SNAP_SCREEN (32px), SNAP_FRAME (20px),
-- or SNAP_GRID (16px) of a target.
-- Respects MidnightUISettings.General.snapToFrames — if false, skipped.
--
-- SNAP PRIORITY: screen edges → neighbor frames → grid
--
-- CONSTANTS:
--   SNAP_SCREEN, SNAP_FRAME, SNAP_GRID, GRID_STEP — snap thresholds
--   KIND_COLOR — color per snap type (screen=blue, grid=gold, frame=green)
--
-- FUNCTIONS:
--   EnsureSnapVisuals()          — lazy-creates all visual frames
--                                  (ghost, poles, labels) on first drag
--   ShowGhost(l,r,t,b,kind)      — shows the ghost outline preview
--   HideGhost()                  — hides the ghost outline
--   ShowPole(axis,pos,...)        — shows a horizontal or vertical
--                                  alignment pole
--   HidePole(axis)               — hides a pole
--   ShowRegionLabel(text,cx,y)   — shows the snap target name label
--   ShowGapLabel(x,y,dist)       — shows pixel gap between frames
--   HideAllSnapVisuals()         — hides all snap visual elements
--   FrameRectUI(f)               — returns frame bounds in UIParent
--                                  coordinate space (accounts for scale)
--   GetNeighborFrames(dragFrame) — returns list of all other visible
--                                  overlay-registered frames
--   PickBestSnap(f)              — evaluates all snap candidates and
--                                  returns the best X and Y snaps
--   PickSmartAnchor(cx,cy,sw,sh) — picks the best anchor point
--                                  (TOPLEFT, CENTER, etc.) based on
--                                  screen position of the frame
--   ComputeSmartOffset(anchor,...) — computes the x/y offset to pass
--                                  to SetPoint for the chosen anchor
--   M.BeginFrameSnap(frame)      — called on drag start; starts a
--                                  C_Timer ticker that updates snaps
--   M.EndFrameSnap(frame)        — called on drag stop; applies the
--                                  final best snap via SetPoint
--
-- FRAMES CREATED (lazy, on first drag):
--   snapVisuals.ghost         — ghost outline frame
--   snapVisuals.poleH/poleV   — horizontal/vertical pole frames
--   snapVisuals.regionLabel   — snap target name tooltip frame
--   snapVisuals.gapLabel      — pixel distance label frame
-- ============================================================
local SNAP_SCREEN = 32
local SNAP_FRAME  = 20
local SNAP_GRID   = 16
local GRID_STEP   = 20

local KIND_COLOR = {
    screen = { 0.34, 0.78, 1.00, 0.95 },
    grid   = { 1.00, 0.74, 0.30, 0.90 },
    frame  = { 0.40, 0.95, 0.55, 0.90 },
}

local GHOST_EDGE_W   = 2
local GHOST_CORNER_L = 14
local GHOST_CORNER_W = 3
local POLE_GLOW_W    = 8
local POLE_CORE_W    = 2

local snapVisuals = nil

local function EnsureSnapVisuals()
    if snapVisuals then return end
    snapVisuals = {}

    local ghost = CreateFrame("Frame", nil, UIParent)
    ghost:SetFrameStrata("TOOLTIP")
    ghost:SetFrameLevel(600)
    ghost:Hide()
    local fill = ghost:CreateTexture(nil, "BACKGROUND")
    fill:SetTexture(W8)
    local edges = {}
    for _, side in ipairs({ "top", "bottom", "left", "right" }) do
        local t = ghost:CreateTexture(nil, "BORDER")
        t:SetTexture(W8)
        edges[side] = t
    end
    local corners = {}
    for _, name in ipairs({ "tlH", "tlV", "trH", "trV", "blH", "blV", "brH", "brV" }) do
        local t = ghost:CreateTexture(nil, "OVERLAY")
        t:SetTexture(W8)
        t:SetBlendMode("ADD")
        corners[name] = t
    end
    ghost._fill    = fill
    ghost._edges   = edges
    ghost._corners = corners
    snapVisuals.ghost = ghost

    for _, axis in ipairs({ "H", "V" }) do
        local p = CreateFrame("Frame", nil, UIParent)
        p:SetFrameStrata("TOOLTIP")
        p:SetFrameLevel(601)
        p:Hide()
        local glow = p:CreateTexture(nil, "BORDER")
        glow:SetTexture(W8)
        glow:SetAllPoints()
        glow:SetBlendMode("ADD")
        local core = p:CreateTexture(nil, "ARTWORK")
        core:SetTexture(W8)
        p._glow = glow
        p._core = core
        snapVisuals["pole" .. axis] = p
    end

    local lbl = CreateFrame("Frame", nil, UIParent)
    lbl:SetFrameStrata("TOOLTIP")
    lbl:SetFrameLevel(602)
    local bg = lbl:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture(W8)
    bg:SetVertexColor(0.03, 0.05, 0.08, 0.94)
    bg:SetAllPoints()
    local bT = lbl:CreateTexture(nil, "BORDER"); bT:SetTexture(W8)
    local bB = lbl:CreateTexture(nil, "BORDER"); bB:SetTexture(W8)
    local bL = lbl:CreateTexture(nil, "BORDER"); bL:SetTexture(W8)
    local bR = lbl:CreateTexture(nil, "BORDER"); bR:SetTexture(W8)
    local accent = lbl:CreateTexture(nil, "ARTWORK")
    accent:SetTexture(W8)
    accent:SetBlendMode("ADD")
    local text = lbl:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    text:SetPoint("LEFT", lbl, "LEFT", 12, 0)
    text:SetTextColor(0.96, 0.98, 1.00)
    lbl._bg, lbl._bT, lbl._bB, lbl._bL, lbl._bR = bg, bT, bB, bL, bR
    lbl._accent = accent
    lbl._text   = text
    lbl:SetSize(80, 22)
    lbl:Hide()
    snapVisuals.regionLabel = lbl

    local gap = CreateFrame("Frame", nil, UIParent)
    gap:SetFrameStrata("TOOLTIP")
    gap:SetFrameLevel(602)
    local gbg = gap:CreateTexture(nil, "BACKGROUND")
    gbg:SetTexture(W8)
    gbg:SetVertexColor(0.03, 0.05, 0.08, 0.92)
    gbg:SetAllPoints()
    local gBT = gap:CreateTexture(nil, "BORDER"); gBT:SetTexture(W8)
    local gBB = gap:CreateTexture(nil, "BORDER"); gBB:SetTexture(W8)
    local gBL = gap:CreateTexture(nil, "BORDER"); gBL:SetTexture(W8)
    local gBR = gap:CreateTexture(nil, "BORDER"); gBR:SetTexture(W8)
    local gtext = gap:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    gtext:SetPoint("CENTER", 0, 0)
    gtext:SetTextColor(0.55, 1.00, 0.65)
    gap._bg = gbg
    gap._bT, gap._bB, gap._bL, gap._bR = gBT, gBB, gBL, gBR
    gap._text = gtext
    gap:SetSize(48, 18)
    gap:Hide()
    snapVisuals.gapLabel = gap
end

local function ShowGhost(gl, gr, gt, gb, kind)
    EnsureSnapVisuals()
    local g = snapVisuals.ghost
    local w = math.max(8, gr - gl)
    local h = math.max(8, gt - gb)
    g:ClearAllPoints()
    g:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", gl, gb)
    g:SetSize(w, h)
    local c = KIND_COLOR[kind] or KIND_COLOR.screen
    local r, gg, bb = c[1], c[2], c[3]
    local edgeA = c[4] or 0.9

    g._fill:ClearAllPoints()
    g._fill:SetPoint("TOPLEFT",     g, "TOPLEFT",      GHOST_EDGE_W, -GHOST_EDGE_W)
    g._fill:SetPoint("BOTTOMRIGHT", g, "BOTTOMRIGHT", -GHOST_EDGE_W,  GHOST_EDGE_W)
    g._fill:SetVertexColor(r, gg, bb, 0.10)

    local e = g._edges
    e.top:ClearAllPoints()
    e.top:SetPoint("TOPLEFT",  g, "TOPLEFT",  0, 0)
    e.top:SetPoint("TOPRIGHT", g, "TOPRIGHT", 0, 0)
    e.top:SetHeight(GHOST_EDGE_W)
    e.top:SetVertexColor(r, gg, bb, edgeA)
    e.bottom:ClearAllPoints()
    e.bottom:SetPoint("BOTTOMLEFT",  g, "BOTTOMLEFT",  0, 0)
    e.bottom:SetPoint("BOTTOMRIGHT", g, "BOTTOMRIGHT", 0, 0)
    e.bottom:SetHeight(GHOST_EDGE_W)
    e.bottom:SetVertexColor(r, gg, bb, edgeA)
    e.left:ClearAllPoints()
    e.left:SetPoint("TOPLEFT",    g, "TOPLEFT",    0, 0)
    e.left:SetPoint("BOTTOMLEFT", g, "BOTTOMLEFT", 0, 0)
    e.left:SetWidth(GHOST_EDGE_W)
    e.left:SetVertexColor(r, gg, bb, edgeA)
    e.right:ClearAllPoints()
    e.right:SetPoint("TOPRIGHT",    g, "TOPRIGHT",    0, 0)
    e.right:SetPoint("BOTTOMRIGHT", g, "BOTTOMRIGHT", 0, 0)
    e.right:SetWidth(GHOST_EDGE_W)
    e.right:SetVertexColor(r, gg, bb, edgeA)

    local cn  = g._corners
    local len = GHOST_CORNER_L
    local th  = GHOST_CORNER_W
    local function setCorner(hTex, vTex, anchor)
        hTex:ClearAllPoints()
        hTex:SetPoint(anchor, g, anchor, 0, 0)
        hTex:SetSize(len, th)
        hTex:SetVertexColor(r, gg, bb, 1.0)
        vTex:ClearAllPoints()
        vTex:SetPoint(anchor, g, anchor, 0, 0)
        vTex:SetSize(th, len)
        vTex:SetVertexColor(r, gg, bb, 1.0)
    end
    setCorner(cn.tlH, cn.tlV, "TOPLEFT")
    setCorner(cn.trH, cn.trV, "TOPRIGHT")
    setCorner(cn.blH, cn.blV, "BOTTOMLEFT")
    setCorner(cn.brH, cn.brV, "BOTTOMRIGHT")

    g:Show()
end

local function HideGhost()
    if snapVisuals and snapVisuals.ghost then snapVisuals.ghost:Hide() end
end

local function ShowPole(axis, pos, startPt, stopPt, kind)
    EnsureSnapVisuals()
    local p = snapVisuals["pole" .. axis]
    local c = KIND_COLOR[kind] or KIND_COLOR.screen
    local r, gg, bb = c[1], c[2], c[3]
    if startPt > stopPt then startPt, stopPt = stopPt, startPt end
    local halfGlow = POLE_GLOW_W / 2
    p:ClearAllPoints()
    if axis == "H" then
        p:SetPoint("BOTTOMLEFT",  UIParent, "BOTTOMLEFT", startPt, pos - halfGlow)
        p:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", stopPt,  pos - halfGlow)
        p:SetHeight(POLE_GLOW_W)
        p._core:ClearAllPoints()
        p._core:SetPoint("LEFT",  p, "LEFT",  0, 0)
        p._core:SetPoint("RIGHT", p, "RIGHT", 0, 0)
        p._core:SetHeight(POLE_CORE_W)
    else
        p:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos - halfGlow, startPt)
        p:SetPoint("TOPLEFT",    UIParent, "BOTTOMLEFT", pos - halfGlow, stopPt)
        p:SetWidth(POLE_GLOW_W)
        p._core:ClearAllPoints()
        p._core:SetPoint("TOP",    p, "TOP",    0, 0)
        p._core:SetPoint("BOTTOM", p, "BOTTOM", 0, 0)
        p._core:SetWidth(POLE_CORE_W)
    end
    p._glow:SetVertexColor(r, gg, bb, 0.28)
    p._core:SetVertexColor(r, gg, bb, 1.00)
    p:Show()
end

local function HidePole(axis)
    if snapVisuals and snapVisuals["pole" .. axis] then
        snapVisuals["pole" .. axis]:Hide()
    end
end

local function StyleLabelBorder(lbl, r, g, b, a)
    lbl._bT:ClearAllPoints()
    lbl._bT:SetPoint("TOPLEFT",  lbl, "TOPLEFT",  0, 0)
    lbl._bT:SetPoint("TOPRIGHT", lbl, "TOPRIGHT", 0, 0)
    lbl._bT:SetHeight(1)
    lbl._bT:SetVertexColor(r, g, b, a)
    lbl._bB:ClearAllPoints()
    lbl._bB:SetPoint("BOTTOMLEFT",  lbl, "BOTTOMLEFT",  0, 0)
    lbl._bB:SetPoint("BOTTOMRIGHT", lbl, "BOTTOMRIGHT", 0, 0)
    lbl._bB:SetHeight(1)
    lbl._bB:SetVertexColor(r, g, b, a)
    lbl._bL:ClearAllPoints()
    lbl._bL:SetPoint("TOPLEFT",    lbl, "TOPLEFT",    0, 0)
    lbl._bL:SetPoint("BOTTOMLEFT", lbl, "BOTTOMLEFT", 0, 0)
    lbl._bL:SetWidth(1)
    lbl._bL:SetVertexColor(r, g, b, a)
    lbl._bR:ClearAllPoints()
    lbl._bR:SetPoint("TOPRIGHT",    lbl, "TOPRIGHT",    0, 0)
    lbl._bR:SetPoint("BOTTOMRIGHT", lbl, "BOTTOMRIGHT", 0, 0)
    lbl._bR:SetWidth(1)
    lbl._bR:SetVertexColor(r, g, b, a)
end

local function ShowRegionLabel(text, cx, topY, kind)
    EnsureSnapVisuals()
    local lbl = snapVisuals.regionLabel
    lbl._text:SetText(text)
    local tw = (lbl._text:GetStringWidth() or 40)
    local padL, padR = 16, 12
    lbl:SetSize(math.floor(tw + padL + padR + 0.5), 22)
    lbl:ClearAllPoints()
    lbl:SetPoint("BOTTOM", UIParent, "BOTTOMLEFT", cx, topY + 10)

    local c = KIND_COLOR[kind or "screen"]
    local r, g, b = c[1], c[2], c[3]

    StyleLabelBorder(lbl, r * 0.40, g * 0.40, b * 0.40, 0.95)

    lbl._accent:ClearAllPoints()
    lbl._accent:SetPoint("TOPLEFT",    lbl, "TOPLEFT",    2, -2)
    lbl._accent:SetPoint("BOTTOMLEFT", lbl, "BOTTOMLEFT", 2,  2)
    lbl._accent:SetWidth(3)
    lbl._accent:SetVertexColor(r, g, b, 1.0)

    lbl._text:SetTextColor(r * 0.45 + 0.55, g * 0.45 + 0.55, b * 0.45 + 0.55, 1.0)
    lbl:Show()
end

local function HideRegionLabel()
    if snapVisuals and snapVisuals.regionLabel then
        snapVisuals.regionLabel:Hide()
    end
end

local function ShowGapLabel(x, y, dist)
    EnsureSnapVisuals()
    local lbl = snapVisuals.gapLabel
    lbl._text:SetText(math.floor(dist + 0.5) .. "px")
    local tw = (lbl._text:GetStringWidth() or 30)
    lbl:SetSize(math.floor(tw + 16 + 0.5), 18)
    lbl:ClearAllPoints()
    lbl:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
    local c = KIND_COLOR.frame
    StyleLabelBorder(lbl, c[1] * 0.40, c[2] * 0.40, c[3] * 0.40, 0.95)
    lbl:Show()
end

local function HideGapLabel()
    if snapVisuals and snapVisuals.gapLabel then
        snapVisuals.gapLabel:Hide()
    end
end

local function HideAllSnapVisuals()
    HideGhost()
    HidePole("H")
    HidePole("V")
    HideRegionLabel()
    HideGapLabel()
end

local function FrameRectUI(f)
    if not (f and f.GetLeft) then return nil end
    local fl, fr = f:GetLeft(), f:GetRight()
    local ft, fb = f:GetTop(),  f:GetBottom()
    if not (fl and fr and ft and fb) then return nil end
    local fe = (f.GetEffectiveScale and f:GetEffectiveScale()) or 1
    local ue = UIParent:GetEffectiveScale() or 1
    if fe == 0 then fe = 1 end
    if ue == 0 then ue = 1 end
    local r = fe / ue
    return fl * r, fr * r, ft * r, fb * r
end

local function GetNeighborFrames(dragFrame)
    local list = {}
    for key, overlay in pairs(M.OverlayHandles or {}) do
        local host = overlay and overlay.GetParent and overlay:GetParent()
        if host and host ~= dragFrame and host ~= UIParent
            and host.IsShown and host:IsShown() then
            local l, r, t, b = FrameRectUI(host)
            if l then
                list[#list + 1] = {
                    l = l, r = r, t = t, b = b,
                    cx = (l + r) / 2, cy = (t + b) / 2,
                    name = key,
                }
            end
        end
    end
    return list
end

local function PickBestSnap(f)
    local fl, fr, ft, fb = FrameRectUI(f)
    if not fl then return nil end
    local fcx, fcy = (fl + fr) / 2, (ft + fb) / 2
    local w, h     = fr - fl, ft - fb

    local sw = UIParent:GetWidth()  or 0
    local sh = UIParent:GetHeight() or 0
    local scx, scy = sw / 2, sh / 2

    local result = {
        fl = fl, fr = fr, ft = ft, fb = fb, w = w, h = h,
        fcx = fcx, fcy = fcy, sw = sw, sh = sh,
    }

    local xCand = {
        { tgt = 0,         frameEdge = fl,  label = "Left edge" },
        { tgt = sw,        frameEdge = fr,  label = "Right edge" },
        { tgt = scx,       frameEdge = fcx, label = "Screen center" },
        { tgt = sw * 0.25, frameEdge = fcx, label = "Left quarter" },
        { tgt = sw * 0.75, frameEdge = fcx, label = "Right quarter" },
    }
    local bestX
    for _, c in ipairs(xCand) do
        local d = math.abs(c.tgt - c.frameEdge)
        if d <= SNAP_SCREEN and (not bestX or d < bestX.dist) then
            bestX = { tgt = c.tgt, frameEdge = c.frameEdge, dist = d,
                      label = c.label, kind = "screen" }
        end
    end

    local neighbors = GetNeighborFrames(f)

    for _, n in ipairs(neighbors) do
        local pairs_ = {
            { n.l,  fl,  "Aligned left with "    .. n.name },
            { n.l,  fcx, "Centered on left of "  .. n.name },
            { n.l,  fr,  "Touching "             .. n.name },
            { n.r,  fl,  "Touching "             .. n.name },
            { n.r,  fcx, "Centered on right of " .. n.name },
            { n.r,  fr,  "Aligned right with "   .. n.name },
            { n.cx, fcx, "Centered on "          .. n.name },
        }
        for _, pair in ipairs(pairs_) do
            local d = math.abs(pair[1] - pair[2])
            if d <= SNAP_FRAME and (not bestX or d < bestX.dist) then
                bestX = { tgt = pair[1], frameEdge = pair[2], dist = d,
                          label = pair[3], kind = "frame", neighbor = n }
            end
        end
    end

    if not bestX then
        local nearestLeft   = math.floor(fl  / GRID_STEP + 0.5) * GRID_STEP
        local nearestCenter = math.floor(fcx / GRID_STEP + 0.5) * GRID_STEP
        local dL = math.abs(nearestLeft   - fl)
        local dC = math.abs(nearestCenter - fcx)
        if dL <= SNAP_GRID and (dL <= dC or dC > SNAP_GRID) then
            bestX = { tgt = nearestLeft, frameEdge = fl, dist = dL,
                      label = "Grid", kind = "grid" }
        elseif dC <= SNAP_GRID then
            bestX = { tgt = nearestCenter, frameEdge = fcx, dist = dC,
                      label = "Grid", kind = "grid" }
        end
    end

    local yCand = {
        { tgt = 0,         frameEdge = fb,  label = "Bottom edge" },
        { tgt = sh,        frameEdge = ft,  label = "Top edge" },
        { tgt = scy,       frameEdge = fcy, label = "Screen center" },
        { tgt = sh * 0.25, frameEdge = fcy, label = "Bottom quarter" },
        { tgt = sh * 0.75, frameEdge = fcy, label = "Top quarter" },
    }
    local bestY
    for _, c in ipairs(yCand) do
        local d = math.abs(c.tgt - c.frameEdge)
        if d <= SNAP_SCREEN and (not bestY or d < bestY.dist) then
            bestY = { tgt = c.tgt, frameEdge = c.frameEdge, dist = d,
                      label = c.label, kind = "screen" }
        end
    end

    for _, n in ipairs(neighbors) do
        local pairs_ = {
            { n.t,  ft,  "Aligned top with "     .. n.name },
            { n.t,  fcy, "Centered on top of "   .. n.name },
            { n.t,  fb,  "Touching "             .. n.name },
            { n.b,  ft,  "Touching "             .. n.name },
            { n.b,  fcy, "Centered on bottom of ".. n.name },
            { n.b,  fb,  "Aligned bottom with "  .. n.name },
            { n.cy, fcy, "Centered on "          .. n.name },
        }
        for _, pair in ipairs(pairs_) do
            local d = math.abs(pair[1] - pair[2])
            if d <= SNAP_FRAME and (not bestY or d < bestY.dist) then
                bestY = { tgt = pair[1], frameEdge = pair[2], dist = d,
                          label = pair[3], kind = "frame", neighbor = n }
            end
        end
    end

    if not bestY then
        local nearestBottom = math.floor(fb  / GRID_STEP + 0.5) * GRID_STEP
        local nearestCenter = math.floor(fcy / GRID_STEP + 0.5) * GRID_STEP
        local dB = math.abs(nearestBottom - fb)
        local dC = math.abs(nearestCenter - fcy)
        if dB <= SNAP_GRID and (dB <= dC or dC > SNAP_GRID) then
            bestY = { tgt = nearestBottom, frameEdge = fb, dist = dB,
                      label = "Grid", kind = "grid" }
        elseif dC <= SNAP_GRID then
            bestY = { tgt = nearestCenter, frameEdge = fcy, dist = dC,
                      label = "Grid", kind = "grid" }
        end
    end

    result.x = bestX
    result.y = bestY
    return result
end

local function PickSmartAnchor(fcx, fcy, sw, sh)
    local thirdW = sw / 3
    local thirdH = sh / 3
    local col, row
    if     fcx < thirdW      then col = "LEFT"
    elseif fcx > sw - thirdW then col = "RIGHT"
    else                          col = "CENTER" end
    if     fcy > sh - thirdH then row = "TOP"
    elseif fcy < thirdH      then row = "BOTTOM"
    else                          row = "MIDDLE" end

    if row == "TOP" then
        if col == "LEFT"  then return "TOPLEFT"  end
        if col == "RIGHT" then return "TOPRIGHT" end
        return "TOP"
    elseif row == "BOTTOM" then
        if col == "LEFT"  then return "BOTTOMLEFT"  end
        if col == "RIGHT" then return "BOTTOMRIGHT" end
        return "BOTTOM"
    else
        if col == "LEFT"  then return "LEFT"  end
        if col == "RIGHT" then return "RIGHT" end
        return "CENTER"
    end
end

local function ComputeSmartOffset(anchor, fl, fr, ft, fb, sw, sh)
    local fcx, fcy = (fl + fr) / 2, (ft + fb) / 2
    local xOfs, yOfs = 0, 0

    if anchor == "TOPLEFT" or anchor == "LEFT" or anchor == "BOTTOMLEFT" then
        xOfs = fl
    elseif anchor == "TOPRIGHT" or anchor == "RIGHT" or anchor == "BOTTOMRIGHT" then
        xOfs = fr - sw
    else
        xOfs = fcx - sw / 2
    end

    if anchor == "TOPLEFT" or anchor == "TOP" or anchor == "TOPRIGHT" then
        yOfs = ft - sh
    elseif anchor == "BOTTOMLEFT" or anchor == "BOTTOM" or anchor == "BOTTOMRIGHT" then
        yOfs = fb
    else
        yOfs = fcy - sh / 2
    end

    return xOfs, yOfs
end

local snapState = {
    active = false,
    frame  = nil,
    ticker = nil,
    last   = nil,
}

function M.BeginFrameSnap(frame)
    if not frame or frame == UIParent then return end
    if M.ExitNudgeMode then M.ExitNudgeMode() end
    if MidnightUISettings and MidnightUISettings.General
        and MidnightUISettings.General.snapToFrames == false then return end

    EnsureSnapVisuals()
    HideAllSnapVisuals()
    snapState.active = true
    snapState.frame  = frame
    snapState.last   = nil
    M._snapActive    = true

    if snapState.ticker then snapState.ticker:Cancel() end
    snapState.ticker = C_Timer.NewTicker(0.016, function()
        if not snapState.active or not snapState.frame
            or not snapState.frame:IsShown() then
            M.EndFrameSnap()
            return
        end

        local res = PickBestSnap(snapState.frame)
        snapState.last = res
        if not res then HideAllSnapVisuals() return end

        local fl, fr, ft, fb = res.fl, res.fr, res.ft, res.fb
        local dx, dy = 0, 0
        if res.x then dx = res.x.tgt - res.x.frameEdge end
        if res.y then dy = res.y.tgt - res.y.frameEdge end
        local gl, gr = fl + dx, fr + dx
        local gb, gt = fb + dy, ft + dy

        local function rank(k)
            if k == "screen" then return 3 end
            if k == "frame"  then return 2 end
            if k == "grid"   then return 1 end
            return 0
        end
        local xKind = res.x and res.x.kind
        local yKind = res.y and res.y.kind
        local kind  = "screen"
        if rank(xKind) >= rank(yKind) then kind = xKind or "screen"
        else                                kind = yKind or "screen" end

        if res.x or res.y then
            ShowGhost(gl, gr, gt, gb, kind)
        else
            HideGhost()
        end

        if res.x then
            ShowPole("V", res.x.tgt, gb - 24, gt + 24, res.x.kind)
        else
            HidePole("V")
        end
        if res.y then
            ShowPole("H", res.y.tgt, gl - 24, gr + 24, res.y.kind)
        else
            HidePole("H")
        end

        local labelAxis = res.x
        if res.y and (not labelAxis or (labelAxis.kind == "grid" and res.y.kind ~= "grid")) then
            labelAxis = res.y
        end
        if labelAxis then
            local gcx = (gl + gr) / 2
            ShowRegionLabel(labelAxis.label, gcx, gt, labelAxis.kind)
        else
            HideRegionLabel()
        end

        local gapShown = false
        if res.x and res.x.kind == "frame" and res.x.neighbor then
            local n = res.x.neighbor
            local gap = 0
            if gr < n.l then gap = n.l - gr
            elseif gl > n.r then gap = gl - n.r end
            if gap > 4 then
                ShowGapLabel(((gl + gr) / 2 + n.cx) / 2, (gb + gt) / 2 + 12, gap)
                gapShown = true
            end
        end
        if not gapShown and res.y and res.y.kind == "frame" and res.y.neighbor then
            local n = res.y.neighbor
            local gap = 0
            if gb > n.t then gap = gb - n.t
            elseif gt < n.b then gap = n.b - gt end
            if gap > 4 then
                ShowGapLabel((gl + gr) / 2 + 16, ((gb + gt) / 2 + n.cy) / 2, gap)
                gapShown = true
            end
        end
        if not gapShown then HideGapLabel() end
    end)
end

function M.EndFrameSnap(frame)
    local target = frame or snapState.frame
    local function cleanup()
        HideAllSnapVisuals()
        snapState.active = false
        snapState.frame  = nil
        snapState.last   = nil
        M._snapActive    = false
        if snapState.ticker then snapState.ticker:Cancel(); snapState.ticker = nil end
    end

    if not target or target == UIParent then cleanup() return end
    if MidnightUISettings and MidnightUISettings.General
        and MidnightUISettings.General.snapToFrames == false then
        cleanup()
        return
    end

    local res = PickBestSnap(target)
    if not res then cleanup() return end

    local fl, fr, ft, fb = res.fl, res.fr, res.ft, res.fb
    local dx, dy = 0, 0
    if res.x then dx = res.x.tgt - res.x.frameEdge end
    if res.y then dy = res.y.tgt - res.y.frameEdge end

    local newL = fl + dx
    local newR = fr + dx
    local newT = ft + dy
    local newB = fb + dy
    local sw, sh = res.sw, res.sh
    local newCX = (newL + newR) / 2
    local newCY = (newT + newB) / 2

    local anchor = PickSmartAnchor(newCX, newCY, sw, sh)
    local xOfs_ui, yOfs_ui = ComputeSmartOffset(anchor, newL, newR, newT, newB, sw, sh)

    local te = (target.GetEffectiveScale and target:GetEffectiveScale()) or 1
    local ue = UIParent:GetEffectiveScale() or 1
    if te == 0 then te = 1 end
    if ue == 0 then ue = 1 end
    local xOfs = xOfs_ui * ue / te
    local yOfs = yOfs_ui * ue / te

    target:ClearAllPoints()
    target:SetPoint(anchor, UIParent, anchor, xOfs, yOfs)

    cleanup()
end


-- ============================================================
-- SECTION: Nudge System (Pixel-Precise Frame Positioning)
-- A single left-click on a non-dragged overlay enters Nudge Mode.
-- In Nudge Mode, arrow keys move the frame 1px at a time and a
-- small HUD appears near the frame showing its current X/Y position.
-- Esc exits nudge mode. Combat (PLAYER_REGEN_DISABLED) also exits it.
-- Nudge auto-saves position to MoverRegistry on every keystroke.
--
-- CONSTANTS:
--   NUDGE_STEP   — pixels per arrow key press (1)
--
-- STATE:
--   nudgeState   — { frame, overlay, key } — what is currently nudged
--
-- FUNCTIONS:
--   EnsureNudgeListener()        — lazy-creates the keyboard listener
--                                  frame (MidnightUI_NudgeListener)
--   EnsureNudgeHUD()             — lazy-creates the on-screen position
--                                  display HUD with arrow buttons
--   MakeNudgeButton(parent,...)  — creates one arrow button for the HUD
--   UpdateNudgePositionText()    — updates "X 0   Y 0" display in HUD
--   UpdateNudgeHUDPosition()     — keeps the HUD near the nudged frame
--   M.NudgeSelected(dx, dy)      — moves nudged frame by dx/dy pixels,
--                                  updates HUD, saves position
--   M.EnterNudgeMode(frame,ovl,key) — activates nudge mode for a frame;
--                                  blocked during combat
--   M.ExitNudgeMode()            — deactivates nudge mode and hides HUD
--
-- FRAMES CREATED (lazy):
--   MidnightUI_NudgeListener     — keyboard capture frame
--   nudgeHUD                     — position display + arrow buttons
-- ============================================================
local NUDGE_STEP = 1

local nudgeState = { frame = nil, overlay = nil, key = nil }
local nudgeListener = nil

local function EnsureNudgeListener()
    if nudgeListener then return end
    local f = CreateFrame("Frame", "MidnightUI_NudgeListener", UIParent)
    f:SetFrameStrata("TOOLTIP")
    f:EnableKeyboard(false)
    f:SetPropagateKeyboardInput(true)
    f:Hide()
    f:RegisterEvent("PLAYER_REGEN_DISABLED")
    f:SetScript("OnEvent", function(self, event)
        if event == "PLAYER_REGEN_DISABLED" then
            M.ExitNudgeMode()
        end
    end)
    f:SetScript("OnKeyDown", function(self, key)
        if InCombatLockdown() then return end
        if not nudgeState.frame then
            self:SetPropagateKeyboardInput(true)
            return
        end
        if nudgeState.overlay and not nudgeState.overlay:IsShown() then
            self:SetPropagateKeyboardInput(true)
            M.ExitNudgeMode()
            return
        end
        local dx, dy = 0, 0
        if     key == "UP"    then dy =  NUDGE_STEP
        elseif key == "DOWN"  then dy = -NUDGE_STEP
        elseif key == "LEFT"  then dx = -NUDGE_STEP
        elseif key == "RIGHT" then dx =  NUDGE_STEP
        elseif key == "ESCAPE" then
            self:SetPropagateKeyboardInput(false)
            M.ExitNudgeMode()
            return
        else
            self:SetPropagateKeyboardInput(true)
            return
        end
        self:SetPropagateKeyboardInput(false)
        M.NudgeSelected(dx, dy)
    end)
    nudgeListener = f
end

local nudgeHUD = nil

local function MakeNudgeButton(parent, rotation, dx, dy)
    local b = CreateFrame("Button", nil, parent)
    b:SetSize(22, 22)
    local bg = b:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture(W8)
    bg:SetVertexColor(0.08, 0.11, 0.16, 0.95)
    bg:SetAllPoints()
    local bT = b:CreateTexture(nil, "BORDER"); bT:SetTexture(W8); bT:SetVertexColor(0.45, 0.36, 0.14, 0.95); bT:SetHeight(1); bT:SetPoint("TOPLEFT"); bT:SetPoint("TOPRIGHT")
    local bB = b:CreateTexture(nil, "BORDER"); bB:SetTexture(W8); bB:SetVertexColor(0.45, 0.36, 0.14, 0.95); bB:SetHeight(1); bB:SetPoint("BOTTOMLEFT"); bB:SetPoint("BOTTOMRIGHT")
    local bL = b:CreateTexture(nil, "BORDER"); bL:SetTexture(W8); bL:SetVertexColor(0.45, 0.36, 0.14, 0.95); bL:SetWidth(1);  bL:SetPoint("TOPLEFT"); bL:SetPoint("BOTTOMLEFT")
    local bR = b:CreateTexture(nil, "BORDER"); bR:SetTexture(W8); bR:SetVertexColor(0.45, 0.36, 0.14, 0.95); bR:SetWidth(1);  bR:SetPoint("TOPRIGHT"); bR:SetPoint("BOTTOMRIGHT")
    local icon = b:CreateTexture(nil, "OVERLAY")
    icon:SetAtlas("common-dropdown-icon-play")
    icon:SetSize(14, 14)
    icon:SetPoint("CENTER", 0, 0)
    icon:SetVertexColor(1.00, 0.88, 0.30, 1.0)
    icon:SetRotation(rotation)
    b._bg   = bg
    b._icon = icon
    b:RegisterForClicks("LeftButtonUp")
    b:SetScript("OnClick", function() M.NudgeSelected(dx, dy) end)
    b:SetScript("OnEnter", function() bg:SetVertexColor(0.16, 0.22, 0.30, 1.0) end)
    b:SetScript("OnLeave", function() bg:SetVertexColor(0.08, 0.11, 0.16, 0.95) end)
    return b
end

local function EnsureNudgeHUD()
    if nudgeHUD then return end
    local BTN = 22
    local GAP = 6
    local PAD = 12
    local btnRowW = BTN * 4 + GAP * 3
    local hudW    = btnRowW + PAD * 2
    local hudH    = 16 + BTN + 16 + PAD

    local hud = CreateFrame("Frame", nil, UIParent)
    hud:SetFrameStrata("TOOLTIP")
    hud:SetFrameLevel(608)
    hud:SetSize(hudW, hudH)
    hud:Hide()

    local bg = hud:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture(W8)
    bg:SetVertexColor(0.03, 0.05, 0.08, 0.94)
    bg:SetAllPoints()

    local bT = hud:CreateTexture(nil, "BORDER"); bT:SetTexture(W8); bT:SetVertexColor(0.60, 0.48, 0.18, 0.95); bT:SetHeight(1); bT:SetPoint("TOPLEFT"); bT:SetPoint("TOPRIGHT")
    local bB = hud:CreateTexture(nil, "BORDER"); bB:SetTexture(W8); bB:SetVertexColor(0.60, 0.48, 0.18, 0.95); bB:SetHeight(1); bB:SetPoint("BOTTOMLEFT"); bB:SetPoint("BOTTOMRIGHT")
    local bL = hud:CreateTexture(nil, "BORDER"); bL:SetTexture(W8); bL:SetVertexColor(0.60, 0.48, 0.18, 0.95); bL:SetWidth(1);  bL:SetPoint("TOPLEFT"); bL:SetPoint("BOTTOMLEFT")
    local bR = hud:CreateTexture(nil, "BORDER"); bR:SetTexture(W8); bR:SetVertexColor(0.60, 0.48, 0.18, 0.95); bR:SetWidth(1);  bR:SetPoint("TOPRIGHT"); bR:SetPoint("BOTTOMRIGHT")

    local posText = hud:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    posText:SetPoint("TOP", hud, "TOP", 0, -6)
    posText:SetTextColor(1.00, 0.88, 0.30, 1.0)
    posText:SetText("X 0   Y 0")
    hud._posText = posText

    local left  = MakeNudgeButton(hud,  math.pi,     -NUDGE_STEP, 0)
    local down  = MakeNudgeButton(hud, -math.pi / 2,  0, -NUDGE_STEP)
    local up    = MakeNudgeButton(hud,  math.pi / 2,  0,  NUDGE_STEP)
    local right = MakeNudgeButton(hud,  0,            NUDGE_STEP, 0)
    left:SetPoint("TOPLEFT", hud, "TOPLEFT", PAD, -(16 + PAD / 2))
    down:SetPoint("LEFT",    left,  "RIGHT",  GAP, 0)
    up:SetPoint("LEFT",      down,  "RIGHT",  GAP, 0)
    right:SetPoint("LEFT",   up,    "RIGHT",  GAP, 0)

    local hint = hud:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    hint:SetPoint("BOTTOM", hud, "BOTTOM", 0, 6)
    hint:SetText("Arrow keys  ·  Esc to exit")
    hint:SetTextColor(0.70, 0.76, 0.84, 0.95)

    nudgeHUD = hud
end

local function UpdateNudgePositionText()
    if not nudgeHUD or not nudgeHUD._posText then return end
    if not nudgeState.frame then return end
    local l, _, t, _ = FrameRectUI(nudgeState.frame)
    if not l then return end
    local uh = UIParent:GetHeight() or 0
    local x = math.floor(l + 0.5)
    local y = math.floor((uh - t) + 0.5)
    nudgeHUD._posText:SetText(string.format("X %d   Y %d", x, y))
end

local function UpdateNudgeHUDPosition()
    if not nudgeHUD or not nudgeState.frame then return end
    local l, r, t, b = FrameRectUI(nudgeState.frame)
    if not l then return end
    local cx = (l + r) / 2
    local hudW, hudH = nudgeHUD:GetWidth(), nudgeHUD:GetHeight()
    local uw = UIParent:GetWidth()  or 2133
    local uh = UIParent:GetHeight() or 1200
    local clampedCX = math.max(hudW / 2 + 4, math.min(uw - hudW / 2 - 4, cx))
    nudgeHUD:ClearAllPoints()
    if t + hudH + 14 < uh then
        nudgeHUD:SetPoint("BOTTOM", UIParent, "BOTTOMLEFT", clampedCX, t + 10)
    else
        nudgeHUD:SetPoint("TOP", UIParent, "BOTTOMLEFT", clampedCX, b - 10)
    end
end

function M.NudgeSelected(dxUI, dyUI)
    local frame = nudgeState.frame
    if not frame then return end
    local point, relFrame, relPoint, xOfs, yOfs = frame:GetPoint()
    if not point then return end
    local te = (frame.GetEffectiveScale and frame:GetEffectiveScale()) or 1
    local ue = UIParent:GetEffectiveScale() or 1
    if te == 0 then te = 1 end
    if ue == 0 then ue = 1 end
    local ratio = ue / te
    frame:ClearAllPoints()
    frame:SetPoint(point, relFrame or UIParent, relPoint,
        xOfs + dxUI * ratio, yOfs + dyUI * ratio)
    UpdateNudgeHUDPosition()
    UpdateNudgePositionText()
    local MR = _G.MidnightUI_MoverRegistry
    if MR and MR.SavePosition and nudgeState.key then
        pcall(MR.SavePosition, nudgeState.key)
    end
end

function M.EnterNudgeMode(frame, overlay, key)
    if not frame or not key then return end
    if InCombatLockdown() then return end
    nudgeState.frame   = frame
    nudgeState.overlay = overlay
    nudgeState.key     = key
    EnsureNudgeListener()
    EnsureNudgeHUD()
    UpdateNudgeHUDPosition()
    UpdateNudgePositionText()
    nudgeListener:Show()
    nudgeListener:EnableKeyboard(true)
    nudgeHUD:Show()
end

function M.ExitNudgeMode()
    nudgeState.frame   = nil
    nudgeState.overlay = nil
    nudgeState.key     = nil
    if nudgeHUD then nudgeHUD:Hide() end
    if nudgeListener then
        if not InCombatLockdown() then
            nudgeListener:SetPropagateKeyboardInput(true)
        end
        nudgeListener:EnableKeyboard(false)
        nudgeListener:Hide()
    end
end


-- ============================================================
-- SECTION: Global Scale System
-- Lets the player scale all MidnightUI frames simultaneously from
-- Settings → General → Global Scale. When the slider moves, every
-- tracked frame's scale value is adjusted proportionally relative to
-- the snapshot taken when the drag started. Avoids compound rounding
-- drift by re-computing from the base snapshot each adjustment.
--
-- CONSTANTS:
--   SCALE_KEYS   — list of {path, field} pairs in MidnightUISettings
--                  that represent per-frame scale values
--
-- FUNCTIONS:
--   CollectScaleTargets(s)      — builds a list of all scale target
--                                 references (pointers into SavedVars)
--   M.ApplyGlobalScale(newScale)— proportionally rescales all tracked
--                                 frames; updates SavedVars; calls
--                                 SetScale on all live overlay handles
--
-- EXPORTS:
--   _G.MidnightUI_ApplyGlobalScale  — entry point called by Settings_UI
-- ============================================================
local SCALE_KEYS = {
    { path = "PlayerFrame",    field = "scale" },
    { path = "TargetFrame",    field = "scale" },
    { path = "FocusFrame",     field = "scale" },
    { path = "PetFrame",       field = "scale" },
    { path = "RaidFrames",     field = "scale" },
    { path = "PartyFrames",    field = "scale" },
    { path = "StanceBar",      field = "scale" },
    { path = "SoulShards",     field = "scale" },
    { path = "ComboPoints",    field = "scale" },
    { path = "DKRunes",        field = "scale" },
    { path = "MonkChi",        field = "scale" },
    { path = "MonkStagger",    field = "scale" },
    { path = "HolyPower",      field = "scale" },
    { path = "ArcaneCharges",  field = "scale" },
    { path = "EvokerEssence",  field = "scale" },
    { path = "InterfaceMenu",  field = "scale" },
    { path = "Minimap",        field = "scale" },
    { path = "Minimap",        field = "infoPanelScale" },
    { path = "ConsumableBars", field = "scale" },
    { path = "Inventory",      field = "dockScale" },
    { path = "LootWindow",     field = "scale" },
    { path = "GroupLoot",      field = "scale" },
    { path = "ObjectiveTracker", field = "scale" },
    { path = "XPBar",          field = "scale" },
    { path = "RepBar",         field = "scale" },
}

local _scaleSnapshot = nil
local _scaleSnapshotTime = 0

local function CollectScaleTargets(s)
    local targets = {}
    local function add(tbl, field, minV, maxV, decimal)
        if tbl and type(tbl[field]) == "number" then
            targets[#targets + 1] = {
                tbl = tbl, field = field, min = minV, max = maxV, decimal = decimal,
            }
        end
    end
    for _, entry in ipairs(SCALE_KEYS) do
        add(s[entry.path], entry.field, 25, 300, false)
    end
    if s.CastBars then
        for _, kind in ipairs({"player", "target", "focus"}) do
            add(s.CastBars[kind], "scale", 25, 300, false)
        end
    end
    if s.ActionBars then
        for i = 1, 8 do
            add(s.ActionBars["bar" .. i], "scale", 25, 300, false)
        end
    end
    for _, parent in ipairs({"PlayerFrame", "TargetFrame"}) do
        if s[parent] then
            add(s[parent].auras, "scale", 25, 300, false)
            add(s[parent].debuffs, "scale", 25, 300, false)
        end
    end
    if s.TargetFrame and s.TargetFrame.targetOfTarget then
        add(s.TargetFrame.targetOfTarget, "scale", 25, 300, false)
    end
    if s.Combat then
        add(s.Combat, "dispelTrackingIconScale", 25, 300, false)
        add(s.Combat, "partyDispelTrackingIconScale", 25, 300, false)
    end
    if s.CooldownManager then
        for _, key in ipairs({"essential", "utility", "buffIcon", "buffBar"}) do
            add(s.CooldownManager[key], "scale", 25, 300, false)
        end
    end
    add(s.MainTankFrames, "scale", 0.25, 3.0, true)
    add(s.Messenger, "scale", 0.5, 2.0, true)
    return targets
end

function M.ApplyGlobalScale(newScale)
    if type(newScale) ~= "number" or newScale <= 0 then return end
    local s = MidnightUISettings
    if not s or not s.General then return end

    local now = GetTime and GetTime() or 0
    if not _scaleSnapshot or (now - _scaleSnapshotTime) > 0.5 then
        local targets = CollectScaleTargets(s)
        local bases = {}
        for i = 1, #targets do
            bases[i] = targets[i].tbl[targets[i].field]
        end
        _scaleSnapshot = {
            globalScale = s.General.globalScale or 100,
            targets = targets,
            bases = bases,
        }
    end
    _scaleSnapshotTime = now

    local baseGlobal = _scaleSnapshot.globalScale
    if baseGlobal <= 0 then baseGlobal = 100 end
    local ratio = newScale / baseGlobal

    for i, t in ipairs(_scaleSnapshot.targets) do
        local base = _scaleSnapshot.bases[i]
        local newVal = base * ratio
        if not t.decimal then
            newVal = math.floor(newVal + 0.5)
        end
        if newVal < t.min then newVal = t.min end
        if newVal > t.max then newVal = t.max end
        t.tbl[t.field] = newVal
    end

    s.General.globalScale = newScale

    local scaled = {}
    for key, overlay in pairs(M.OverlayHandles or {}) do
        local host = overlay and overlay.GetParent and overlay:GetParent()
        if host == UIParent then host = overlay end
        if host and host ~= UIParent then
            local absScale = nil
            local cdPanel = key:match("^CooldownManager_(%a+)$")
            local barNum = tonumber(key:match("^ActionBar(%d+)$") or "")
            local castKind = key:match("^CastBar_(%a+)$")
            if barNum and s.ActionBars and s.ActionBars["bar" .. barNum] then
                absScale = s.ActionBars["bar" .. barNum].scale
            elseif castKind and s.CastBars and s.CastBars[castKind] then
                absScale = s.CastBars[castKind].scale
            elseif key == "PlayerAuras" and s.PlayerFrame and s.PlayerFrame.auras then
                absScale = s.PlayerFrame.auras.scale
            elseif key == "PlayerDebuffs" and s.PlayerFrame and s.PlayerFrame.debuffs then
                absScale = s.PlayerFrame.debuffs.scale
            elseif key == "TargetAuras" and s.TargetFrame and s.TargetFrame.auras then
                absScale = s.TargetFrame.auras.scale
            elseif key == "TargetDebuffs" and s.TargetFrame and s.TargetFrame.debuffs then
                absScale = s.TargetFrame.debuffs.scale
            elseif key == "TargetOfTarget" and s.TargetFrame and s.TargetFrame.targetOfTarget then
                absScale = s.TargetFrame.targetOfTarget.scale
            elseif key == "DispelTracking" and s.Combat then
                absScale = s.Combat.dispelTrackingIconScale
            elseif key == "PartyDispelTracking" and s.Combat then
                absScale = s.Combat.partyDispelTrackingIconScale
            elseif cdPanel and s.CooldownManager then
                local pkey = (cdPanel == "Essential" and "essential")
                    or (cdPanel == "Utility"   and "utility")
                    or (cdPanel == "BuffIcon"  and "buffIcon")
                    or (cdPanel == "BuffBar"   and "buffBar")
                if pkey and s.CooldownManager[pkey] then
                    absScale = s.CooldownManager[pkey].scale
                end
            elseif key == "MainTankFrames" then
                if s.MainTankFrames and type(s.MainTankFrames.scale) == "number" then
                    pcall(host.SetScale, host, s.MainTankFrames.scale)
                end
                scaled[host] = true
            elseif key == "Messenger" then
                if s.Messenger and type(s.Messenger.scale) == "number" then
                    pcall(host.SetScale, host, s.Messenger.scale)
                end
                scaled[host] = true
            elseif s[key] and type(s[key].scale) == "number" then
                absScale = s[key].scale
            end
            if absScale and type(absScale) == "number" then
                pcall(host.SetScale, host, absScale / 100)
                scaled[host] = true
            elseif not scaled[host] then
                pcall(host.SetScale, host, newScale / 100)
                scaled[host] = true
            end
        end
    end
    for _, name in ipairs({
        "MidnightUI_RaidAnchor",
        "MidnightUI_MainTankAnchor",
        "MidnightDockMain",
        "MidnightUI_CharacterPanel",
        "MidnightUI_GuildPanel",
        "MidnightUI_QuestInterface",
        "MidnightUI_AchievementsPanel",
        "MidnightBagWindow",
        "MidnightUI_SoulShardsContainer",
        "MidnightUI_ComboPointsContainer",
        "MidnightUI_DKRunesContainer",
        "MidnightUI_MonkChiContainer",
        "MidnightUI_MonkStaggerContainer",
        "MidnightUI_HolyPowerContainer",
        "MidnightUI_ArcaneChargesContainer",
        "MidnightUI_EvokerEssenceContainer",
    }) do
        local f = _G[name]
        if f and not scaled[f] and f ~= UIParent and f.GetScale and f.SetScale then
            pcall(f.SetScale, f, newScale / 100)
        end
    end

    -- After SavedVars are updated, ask each module to re-apply its
    -- own scale to whatever frames it owns. This covers cases the
    -- overlay-handles loop above can't reach (cast bars, the loot
    -- window, the objective tracker, the XP/rep bars, the cooldown
    -- manager panels, target of target, main tank frames, class
    -- power containers, etc.).
    local function CallApply(name)
        local fn = _G[name]
        if type(fn) == "function" then pcall(fn) end
    end
    CallApply("MidnightUI_ApplyCastBarSettings")
    CallApply("MidnightUI_ApplyLootWindowSettings")
    CallApply("MidnightUI_ApplyGroupLootScale")
    CallApply("MidnightUI_ApplyObjectiveTrackerScale")
    CallApply("MidnightUI_ApplyStatusBarScales")
    CallApply("MidnightUI_ApplyCooldownManagerSettings")
    CallApply("MidnightUI_ApplyTargetOfTargetSettings")
    CallApply("MidnightUI_ApplyMainTankSettings")
    CallApply("MidnightUI_ApplySoulShardsSettings")
    CallApply("MidnightUI_ApplyComboPointsSettings")
    CallApply("MidnightUI_ApplyDKRunesSettings")
    CallApply("MidnightUI_ApplyMonkChiSettings")
    CallApply("MidnightUI_ApplyMonkStaggerSettings")
    CallApply("MidnightUI_ApplyHolyPowerSettings")
    CallApply("MidnightUI_ApplyArcaneChargesSettings")
    CallApply("MidnightUI_ApplyEvokerEssenceSettings")
end

_G.MidnightUI_ApplyGlobalScale = function(newScale) M.ApplyGlobalScale(newScale) end


-- ============================================================
-- SECTION: Overlay Visual Styling
-- Defines the color palette and applies the MidnightUI visual style
-- to overlay drag handles and their hover states. Also styles
-- buttons (StyleMidnightButton) used throughout the addon's UI.
--
-- CONSTANTS:
--   OVERLAY_STYLE       — colors for overlay background/border/accent
--                         in normal and hover states
--   OVERLAY_MENU_STYLE  — colors for the overlay settings popup panel
--
-- FUNCTIONS:
--   ApplyOverlayVisual(overlay, highlighted) — applies normal or hover
--                                              color set to an overlay
--   BalanceOverlayTextRegions(overlay, min)  — ensures all FontStrings
--                                              inside an overlay use the
--                                              correct color and min size
--   StyleOverlay(overlay, label, font, cat)  — full style pass on an
--                                              overlay: backdrop, accent
--                                              strip, inner stroke, hover
--                                              hooks, optional label text;
--                                              category tints border color
--   StyleMidnightButton(button)              — applies MidnightUI dark
--                                              button skin to any Button
--                                              frame; idempotent
--
-- EXPORTS:
--   M.StyleButton                    — module-scoped button skin
--   _G.MidnightUI_StyleButton        — global entry point
--   _G.MidnightUI_StyleOverlay       — global entry point
-- ============================================================
local OVERLAY_STYLE = {
    bg = { 0.03, 0.05, 0.08, 0.40 },
    border = { 0.25, 0.40, 0.55, 0.85 },
    accent = { 0.20, 0.50, 0.70, 0.22 },
    inset = { 0.25, 0.35, 0.48, 0.40 },
    label = { 0.95, 0.97, 1.00, 1.00 },
    labelBg = { 0.02, 0.04, 0.07, 0.70 },
    hoverBg = { 0.06, 0.10, 0.15, 0.55 },
    hoverBorder = { 0.40, 0.65, 0.85, 0.95 },
    hoverAccent = { 0.30, 0.70, 0.92, 0.35 },
    hoverInset = { 0.40, 0.55, 0.70, 0.60 },
}

local OVERLAY_MENU_STYLE = {
    panelBg     = { 0.03, 0.05, 0.09, 0.88 },
    panelBorder = { 0.20, 0.35, 0.50, 0.40 },
    headerBg    = { 0.05, 0.08, 0.14, 0.95 },
    footerLine  = { 0.18, 0.25, 0.35, 0.50 },
    section     = { 0.88, 0.92, 0.98, 1.00 },
    sectionRule = { 0.16, 0.22, 0.30, 0.55 },
    label       = { 0.92, 0.95, 0.99, 1.00 },
    value       = { 0.40, 0.85, 1.00, 1.00 },
    note        = { 0.60, 0.67, 0.76, 0.85 },
    hoverBar    = { 0.20, 0.40, 0.60, 0.12 },
    cardBg      = { 0.04, 0.06, 0.10, 0.92 },
    cardBorder  = { 0.12, 0.18, 0.26, 0.70 },
    cardTop     = { 0.15, 0.40, 0.60, 0.12 },
}

local function ApplyOverlayVisual(overlay, highlighted)
    if not overlay then return end

    local bg = highlighted and OVERLAY_STYLE.hoverBg or OVERLAY_STYLE.bg
    local border = highlighted and OVERLAY_STYLE.hoverBorder or OVERLAY_STYLE.border
    local accent = highlighted and OVERLAY_STYLE.hoverAccent or OVERLAY_STYLE.accent
    local inset = highlighted and OVERLAY_STYLE.hoverInset or OVERLAY_STYLE.inset

    overlay:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
    overlay:SetBackdropBorderColor(border[1], border[2], border[3], border[4])

    if overlay._muiTopAccent then
        overlay._muiTopAccent:SetVertexColor(accent[1], accent[2], accent[3], accent[4])
    end
    if overlay._muiInnerStroke and overlay._muiInnerStroke.SetBackdropBorderColor then
        overlay._muiInnerStroke:SetBackdropBorderColor(inset[1], inset[2], inset[3], inset[4])
    end
    if overlay._muiFillGradient then
        local tr, tg, tb, ta, br, bgc, bb, ba
        if highlighted then
            tr, tg, tb, ta = 0.12, 0.18, 0.25, 0.26
            br, bgc, bb, ba = 0.03, 0.05, 0.08, 0.08
        else
            tr, tg, tb, ta = 0.10, 0.15, 0.21, 0.16
            br, bgc, bb, ba = 0.02, 0.04, 0.06, 0.04
        end

        if overlay._muiFillGradient.SetColorTexture then
            overlay._muiFillGradient:SetColorTexture((tr + br) * 0.5, (tg + bgc) * 0.5, (tb + bb) * 0.5, math.max(ta, ba))
        end

        if overlay._muiFillGradient.SetGradientAlpha then
            pcall(overlay._muiFillGradient.SetGradientAlpha, overlay._muiFillGradient, "VERTICAL",
                tr, tg, tb, ta,
                br, bgc, bb, ba
            )
        end
    end
end

local function BalanceOverlayTextRegions(overlay, minSize)
    if not overlay then return end
    local regions = { overlay:GetRegions() }
    for _, region in ipairs(regions) do
        if region and region.GetObjectType and region:GetObjectType() == "FontString" then
            if region.SetTextColor then
                region:SetTextColor(
                    OVERLAY_STYLE.label[1],
                    OVERLAY_STYLE.label[2],
                    OVERLAY_STYLE.label[3],
                    OVERLAY_STYLE.label[4]
                )
            end
            if region.SetShadowColor then
                region:SetShadowColor(0, 0, 0, 0.85)
                region:SetShadowOffset(1, -1)
            end
            if region.GetFont and region.SetFont then
                local font, size, flags = region:GetFont()
                if font and size and size < (minSize or 12) then
                    pcall(region.SetFont, region, font, minSize or 12, flags)
                end
            end
        end
    end
end

local function StyleOverlay(overlay, labelText, labelFont, category)
    if not overlay then return end
    overlay._muiCategory = category
    overlay:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        tile = true,
        tileSize = 16,
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })

    if not overlay._muiTopAccent then
        local accent = overlay:CreateTexture(nil, "ARTWORK")
        accent:SetTexture("Interface\\Buttons\\WHITE8X8")
        accent:SetPoint("TOPLEFT", 1, -1)
        accent:SetPoint("TOPRIGHT", -1, -1)
        overlay._muiTopAccent = accent
    end
    overlay._muiTopAccent:Hide()

    if not overlay._muiFillGradient then
        local fill = overlay:CreateTexture(nil, "BACKGROUND")
        fill:SetTexture("Interface\\Buttons\\WHITE8X8")
        fill:SetPoint("TOPLEFT", 1, -1)
        fill:SetPoint("BOTTOMRIGHT", -1, 1)
        fill:SetColorTexture(0.06, 0.10, 0.14, 0.14)
        overlay._muiFillGradient = fill
    end

    local rawHeight = (overlay.GetHeight and overlay:GetHeight()) or 24
    local accentHeight = math.max(6, math.min(12, math.floor(rawHeight * 0.28)))
    overlay._muiTopAccent:SetHeight(accentHeight)

    if category and OVERLAY_CATEGORY_COLORS[category] then
        local cc = OVERLAY_CATEGORY_COLORS[category]
        overlay._muiTopAccent:SetVertexColor(cc[1], cc[2], cc[3], 0.55)
        overlay:SetBackdropBorderColor(
            cc[1] * 0.5 + OVERLAY_STYLE.border[1] * 0.5,
            cc[2] * 0.5 + OVERLAY_STYLE.border[2] * 0.5,
            cc[3] * 0.5 + OVERLAY_STYLE.border[3] * 0.5,
            OVERLAY_STYLE.border[4]
        )
        if not overlay._muiCategoryBadge then
            local badge = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            badge:SetPoint("TOPLEFT", 6, -4)
            overlay._muiCategoryBadge = badge
        end
        overlay._muiCategoryBadge:SetText("")
        overlay._muiCategoryBadge:Hide()
    end

    if not overlay._muiInnerStroke then
        local inset = CreateFrame("Frame", nil, overlay, "BackdropTemplate")
        inset:SetPoint("TOPLEFT", 2, -2)
        inset:SetPoint("BOTTOMRIGHT", -2, 2)
        inset:SetBackdrop({
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
        })
        inset:SetBackdropBorderColor(0, 0, 0, 0)
        inset:EnableMouse(false)
        overlay._muiInnerStroke = inset
    end
    overlay._muiInnerStroke:Hide()
    ApplyOverlayVisual(overlay, overlay._muiHoverActive == true)
    BalanceOverlayTextRegions(overlay, 12)

    if labelText then
        local label = overlay._muiLabel
        if not label then
            label = overlay:CreateFontString(nil, "OVERLAY", labelFont or "GameFontNormal")
            overlay._muiLabel = label
        end
        if label.SetFontObject and labelFont then
            label:SetFontObject(labelFont)
        elseif label.SetFontObject then
            label:SetFontObject("GameFontNormalLarge")
        end
        label:ClearAllPoints()
        label:SetPoint("CENTER")
        label:SetText(labelText)
        if label.GetFont and label.SetFont then
            local fontPath, _, flags = label:GetFont()
            local labelSize = math.max(12, math.min(16, math.floor(rawHeight * 0.30)))
            if fontPath then
                pcall(label.SetFont, label, fontPath, labelSize, flags)
            end
        end
        label:SetTextColor(
            OVERLAY_STYLE.label[1],
            OVERLAY_STYLE.label[2],
            OVERLAY_STYLE.label[3],
            OVERLAY_STYLE.label[4]
        )
        if label.SetShadowColor then
            label:SetShadowColor(0, 0, 0, 0.85)
            label:SetShadowOffset(1, -1)
        end

        if not overlay._muiLabelBg then
            local labelBg = overlay:CreateTexture(nil, "ARTWORK")
            labelBg:SetTexture("Interface\\Buttons\\WHITE8X8")
            overlay._muiLabelBg = labelBg
        end
        overlay._muiLabelBg:ClearAllPoints()
        overlay._muiLabelBg:SetPoint("TOPLEFT", label, "TOPLEFT", -6, 4)
        overlay._muiLabelBg:SetPoint("BOTTOMRIGHT", label, "BOTTOMRIGHT", 6, -4)
        overlay._muiLabelBg:SetVertexColor(
            OVERLAY_STYLE.labelBg[1],
            OVERLAY_STYLE.labelBg[2],
            OVERLAY_STYLE.labelBg[3],
            OVERLAY_STYLE.labelBg[4]
        )
        overlay._muiLabelBg:Show()
    elseif overlay._muiLabelBg then
        overlay._muiLabelBg:Hide()
    end

    if not overlay._muiHoverHooksInstalled and overlay.HookScript then
        overlay:HookScript("OnEnter", function(self)
            self._muiHoverActive = true
            ApplyOverlayVisual(self, true)
            BalanceOverlayTextRegions(self, 12)
        end)
        overlay:HookScript("OnLeave", function(self)
            self._muiHoverActive = false
            ApplyOverlayVisual(self, false)
            BalanceOverlayTextRegions(self, 12)
        end)
        overlay:HookScript("OnShow", function(self)
            ApplyOverlayVisual(self, self._muiHoverActive == true)
            BalanceOverlayTextRegions(self, 12)
        end)
        overlay:HookScript("OnSizeChanged", function(self)
            local h = (self.GetHeight and self:GetHeight()) or 24
            if self._muiTopAccent then
                local hAccent = math.max(6, math.min(12, math.floor(h * 0.28)))
                self._muiTopAccent:SetHeight(hAccent)
            end
            if self._muiLabel and self._muiLabel.GetFont and self._muiLabel.SetFont then
                local fontPath, _, flags = self._muiLabel:GetFont()
                local labelSize = math.max(12, math.min(16, math.floor(h * 0.30)))
                if fontPath then
                    pcall(self._muiLabel.SetFont, self._muiLabel, fontPath, labelSize, flags)
                end
            end
        end)
        overlay._muiHoverHooksInstalled = true
    end
end


local function GetTooltipText(label)
    local t = _G.MidnightUI_Tooltips
    if t and label then
        return t[label]
    end
end

local function AttachTooltip(control, label)
    local tip = GetTooltipText(label)
    if not control or not tip then return end
    control:SetScript("OnEnter", function(self)
        if not GameTooltip then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(label, 1, 1, 1)
        GameTooltip:AddLine(tip, 0.9, 0.9, 0.9, true)
        if GameTooltip.SetWrapTextInTooltip then
            GameTooltip:SetWrapTextInTooltip(true)
        end
        GameTooltip:Show()
    end)
    control:SetScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
    end)
end

local function StyleMidnightButton(button)
    if not button or button.MidnightUI_Styled then return end
    button.MidnightUI_Styled = true

    if button.SetNormalFontObject then button:SetNormalFontObject("GameFontNormal") end
    if button.SetHighlightFontObject then button:SetHighlightFontObject("GameFontHighlight") end
    if button.SetDisabledFontObject then button:SetDisabledFontObject("GameFontDisable") end

    if button.Left then button.Left:Hide() end
    if button.Middle then button.Middle:Hide() end
    if button.Right then button.Right:Hide() end

    local border = button:CreateTexture(nil, "BACKGROUND")
    border:SetTexture("Interface\\Buttons\\WHITE8X8")
    border:SetVertexColor(0.18, 0.22, 0.30, 1.0)
    border:SetPoint("TOPLEFT", 0, 0)
    border:SetPoint("BOTTOMRIGHT", 0, 0)

    local bg = button:CreateTexture(nil, "BACKGROUND")
    bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    bg:SetVertexColor(0.07, 0.08, 0.10, 0.95)
    bg:SetPoint("TOPLEFT", 2, -2)
    bg:SetPoint("BOTTOMRIGHT", -2, 2)

    local sheen = button:CreateTexture(nil, "ARTWORK")
    sheen:SetTexture("Interface\\Buttons\\WHITE8X8")
    sheen:SetVertexColor(0.18, 0.22, 0.28, 0.35)
    sheen:SetPoint("TOPLEFT", 3, -3)
    sheen:SetPoint("BOTTOMRIGHT", -3, 12)

    local hover = button:CreateTexture(nil, "HIGHLIGHT")
    hover:SetTexture("Interface\\Buttons\\WHITE8X8")
    hover:SetVertexColor(0.35, 0.40, 0.46, 0.25)
    hover:SetPoint("TOPLEFT", 2, -2)
    hover:SetPoint("BOTTOMRIGHT", -2, 2)

    if button.SetNormalTexture then button:SetNormalTexture("") end
    if button.SetHighlightTexture then button:SetHighlightTexture(hover) end
    if button.SetPushedTexture then button:SetPushedTexture("") end
    if button.SetDisabledTexture then button:SetDisabledTexture("") end
end

M.StyleButton = StyleMidnightButton
_G.MidnightUI_StyleButton = function(button) StyleMidnightButton(button) end


-- ============================================================
-- SECTION: Overlay Settings UI Builder
-- A fluent builder that constructs the scrollable settings panel
-- shown when a player right-clicks an overlay. Called by each
-- module's overlay settings definition (def.build function).
-- Returns a builder object (b) with methods to add rows.
--
-- BUILDER METHODS (called on the returned b object):
--   b:Header(text, defaultCollapsed) — collapsible section header
--                                      with arrow toggle
--   b:Checkbox(label, value, fn)     — a toggle checkbox row;
--                                      fn(checked) called on change
--   b:Slider(label, min, max, step,  — a labeled slider row with
--            value, fn)               live value display; fn(v) on change
--   b:Dropdown(label, opts, cur, fn) — a UIDropDownMenu row;
--                                      fn(option) on selection
--   b:Note(text)                     — a small info text row
--   b:Height()                       — returns the total content height
--                                      so the panel can resize itself
--
-- EXPORTS:
--   _G.MidnightUI_CreateOverlayBuilder  — called by module overlay defs
-- ============================================================
local function CreateOverlayBuilder(content, opts)
    local b = {}
    b.y = (opts and opts.startY) or -4
    b.width = (opts and opts.width) or 0
    b._sections = {}
    b._currentSection = nil
    b._sectionControls = {}

    local W8 = "Interface\\Buttons\\WHITE8X8"

    local function CreateRow(height)
        local row = CreateFrame("Frame", nil, content)
        row:SetPoint("TOPLEFT", 4, b.y)
        row:SetPoint("RIGHT", -4, 0)
        row:SetHeight(height)
        b.y = b.y - height - 3
        if b._currentSection and b._sectionControls[b._currentSection] then
            b._sectionControls[b._currentSection][#b._sectionControls[b._currentSection] + 1] = row
        end
        return row
    end

    function b:Header(text, defaultCollapsed)
        self._currentSection = text
        self._sectionControls[text] = {}

        self.y = self.y - 8

        local sectionFrame = CreateFrame("Frame", nil, content)
        sectionFrame:SetPoint("TOPLEFT", 0, self.y)
        sectionFrame:SetPoint("RIGHT", 0, 0)
        sectionFrame:SetHeight(26)

        local arrow = sectionFrame:CreateTexture(nil, "OVERLAY")
        arrow:SetSize(10, 10)
        arrow:SetPoint("LEFT", 6, 0)
        arrow:SetAtlas("common-dropdown-icon-back")
        arrow:SetRotation(defaultCollapsed and math.pi or -math.pi / 2)

        local header = sectionFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        header:SetPoint("LEFT", arrow, "RIGHT", 6, 0)
        header:SetText(text)
        header:SetTextColor(OVERLAY_MENU_STYLE.section[1], OVERLAY_MENU_STYLE.section[2], OVERLAY_MENU_STYLE.section[3], OVERLAY_MENU_STYLE.section[4])

        local line = sectionFrame:CreateTexture(nil, "ARTWORK")
        line:SetTexture(W8)
        line:SetPoint("LEFT", header, "RIGHT", 10, 0)
        line:SetPoint("RIGHT", -4, 0)
        line:SetHeight(1)
        line:SetVertexColor(OVERLAY_MENU_STYLE.sectionRule[1], OVERLAY_MENU_STYLE.sectionRule[2], OVERLAY_MENU_STYLE.sectionRule[3], OVERLAY_MENU_STYLE.sectionRule[4])

        sectionFrame._muiHeaderText = header
        sectionFrame._muiArrow = arrow

        self.y = self.y - 28

        local isCollapsed = defaultCollapsed == true
        local sectionInfo = { frame = sectionFrame, collapsed = isCollapsed, controls = self._sectionControls[text], arrow = arrow }
        self._sections[#self._sections + 1] = sectionInfo

        sectionFrame:EnableMouse(true)
        sectionFrame:SetScript("OnMouseUp", function()
            sectionInfo.collapsed = not sectionInfo.collapsed
            arrow:SetRotation(sectionInfo.collapsed and math.pi or -math.pi / 2)
            for _, ctrl in ipairs(sectionInfo.controls) do
                if sectionInfo.collapsed then ctrl:Hide() else ctrl:Show() end
            end
        end)
    end

    function b:Checkbox(label, value, onChange)
        local row = CreateRow(26)
        local cb = CreateFrame("CheckButton", nil, row, "InterfaceOptionsCheckButtonTemplate")
        cb:SetPoint("LEFT", 4, 0)
        cb.Text:SetText(label)
        cb.Text:SetFontObject("GameFontNormal")
        cb.Text:SetTextColor(OVERLAY_MENU_STYLE.label[1], OVERLAY_MENU_STYLE.label[2], OVERLAY_MENU_STYLE.label[3])
        cb:SetChecked(value == true)
        cb:SetScript("OnClick", function(self) if onChange then onChange(self:GetChecked() == true) end end)
        AttachTooltip(cb, label)
        cb._muiRow = row
        return cb
    end

    function b:Slider(label, min, max, step, value, onChange)
        local holder = CreateRow(50)

        local labelText = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        labelText:SetPoint("TOPLEFT", 6, -6)
        labelText:SetText(label)
        labelText:SetTextColor(OVERLAY_MENU_STYLE.label[1], OVERLAY_MENU_STYLE.label[2], OVERLAY_MENU_STYLE.label[3])

        local valText = holder:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        valText:SetPoint("TOPRIGHT", -6, -6)
        valText:SetTextColor(OVERLAY_MENU_STYLE.value[1], OVERLAY_MENU_STYLE.value[2], OVERLAY_MENU_STYLE.value[3])

        local slider = CreateFrame("Slider", nil, holder, "OptionsSliderTemplate")
        slider:SetPoint("TOPLEFT", 4, -24)
        slider:SetPoint("TOPRIGHT", -4, -24)
        slider:SetHeight(18)
        slider:SetMinMaxValues(min, max)
        slider:SetValueStep(step)
        slider:SetObeyStepOnDrag(true)
        slider:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal")
        if slider.GetThumbTexture then
            local thumb = slider:GetThumbTexture()
            if thumb then thumb:SetSize(36, 24) end
        end

        if slider.Low then slider.Low:Hide() end
        if slider.High then slider.High:Hide() end
        if slider.Text then slider.Text:Hide() end

        local function FormatValue(v)
            if step < 1 then return string.format("%.2f", v) end
            return tostring(math.floor(v + 0.5))
        end

        slider:SetValue(value or min)
        valText:SetText(FormatValue(value or min))
        slider:SetScript("OnValueChanged", function(self, v)
            if step >= 1 then v = math.floor(v + 0.5) end
            valText:SetText(FormatValue(v))
            if onChange then onChange(v) end
        end)
        AttachTooltip(holder, label)
        AttachTooltip(slider, label)
        holder.slider = slider
        holder.labelText = labelText
        holder.valText = valText
        holder.SetMinMaxValues = function(self, a, bv) slider:SetMinMaxValues(a, bv) end
        holder.SetValue = function(self, v) slider:SetValue(v) end
        holder.GetValue = function(self) return slider:GetValue() end
        holder.SetValueStep = function(self, v) slider:SetValueStep(v) end
        holder.SetEnabled = function(self, v) slider:SetEnabled(v) end
        holder._muiSlider = slider
        return holder
    end

    function b:Dropdown(label, options, current, onChange)
        local holder = CreateRow(62)

        local labelText = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        labelText:SetPoint("TOPLEFT", 6, -6)
        labelText:SetPoint("TOPRIGHT", -6, -6)
        labelText:SetJustifyH("LEFT")
        labelText:SetText(label)
        labelText:SetTextColor(OVERLAY_MENU_STYLE.label[1], OVERLAY_MENU_STYLE.label[2], OVERLAY_MENU_STYLE.label[3])

        local dropdown = CreateFrame("Frame", nil, holder, "UIDropDownMenuTemplate")
        dropdown:SetPoint("TOPLEFT", -6, -24)
        UIDropDownMenu_SetWidth(dropdown, 176)
        UIDropDownMenu_SetText(dropdown, current)

        UIDropDownMenu_Initialize(dropdown, function()
            for _, option in ipairs(options or {}) do
                local info = UIDropDownMenu_CreateInfo()
                info.text = option
                info.func = function()
                    UIDropDownMenu_SetText(dropdown, option)
                    if onChange then onChange(option) end
                end
                UIDropDownMenu_AddButton(info)
            end
        end)

        AttachTooltip(holder, label)
        AttachTooltip(dropdown, label)
        holder.dropdown = dropdown
        holder.labelText = labelText
        holder.SetValue = function(self, v) UIDropDownMenu_SetText(dropdown, v) end
        holder.SetDisabled = function(self, isDisabled)
            if isDisabled then UIDropDownMenu_DisableDropDown(dropdown) else UIDropDownMenu_EnableDropDown(dropdown) end
        end
        return holder
    end

    function b:Note(text)
        local row = CreateRow(24)
        local msg = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        msg:SetPoint("TOPLEFT", 8, -3)
        msg:SetPoint("BOTTOMRIGHT", -8, 3)
        msg:SetJustifyH("LEFT")
        msg:SetTextColor(OVERLAY_MENU_STYLE.note[1], OVERLAY_MENU_STYLE.note[2], OVERLAY_MENU_STYLE.note[3], OVERLAY_MENU_STYLE.note[4])
        msg:SetText(text)
        return msg
    end

    function b:Height()
        return -self.y + 4
    end

    return b
end


-- ============================================================
-- SECTION: Overlay Settings Panel (Frame Creation + Show Logic)
-- The floating popup that appears when a player right-clicks any
-- overlay drag handle. Created lazily on first right-click.
-- Contains a scrollable content area built by CreateOverlayBuilder,
-- a "Reset Position" button, and a "Reset All" button that restores
-- SavedVar defaults for that specific overlay's module.
--
-- FUNCTIONS:
--   CreateOverlaySettingsFrame()     — builds the panel frame once;
--                                      stored at MidnightUI_OverlaySettings
--   M.ShowOverlaySettings(key)       — tears down old content, rebuilds
--                                      via def.build(), resizes panel,
--                                      shows it; key identifies which
--                                      overlay's settings to display
--
-- FRAMES CREATED (lazy):
--   MidnightUI_OverlaySettings       — the main floating settings panel
--
-- EXPORTS:
--   _G.MidnightUI_ShowOverlaySettings      — global entry point
--   _G.MidnightUI_RegisterOverlaySettings  — global entry point
--   _G.MidnightUI_AttachOverlaySettings    — global entry point
--   _G.MidnightUI_AttachOverlayTooltip     — attaches GameTooltip to a control
--   _G.MidnightUI_GetOverlayHandle         — returns overlay frame by key
--   _G.MidnightUI_ResolveOverlayPositionTarget — maps key to SavedVar table
--   _G.MidnightUI_SaveOverlayPosition      — saves frame position
--   _G.MidnightUI_ApplyOverlayPosition     — restores saved position
--   _G.MidnightUI_SaveRelativeOverlayPosition
--   _G.MidnightUI_ApplyRelativeOverlayPosition
--   _G.MidnightUI_ScaleFromPercent
--   _G.MidnightUI_BeginFrameSnap / EndFrameSnap
-- ============================================================
local function CreateOverlaySettingsFrame()
    local HEADER_H = 52
    local FOOTER_H = 46
    local W8 = "Interface\\Buttons\\WHITE8X8"

    local f = CreateFrame("Frame", "MidnightUI_OverlaySettings", UIParent, "BackdropTemplate")
    f:SetSize(380, 400)
    f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    f:SetFrameStrata("DIALOG")
    f:SetFrameLevel(50)
    table.insert(UISpecialFrames, "MidnightUI_OverlaySettings")

    f:SetBackdrop({
        bgFile = W8,
        edgeFile = W8,
        edgeSize = 2,
        insets = { left = 2, right = 2, top = 2, bottom = 2 }
    })
    f:SetBackdropColor(OVERLAY_MENU_STYLE.panelBg[1], OVERLAY_MENU_STYLE.panelBg[2], OVERLAY_MENU_STYLE.panelBg[3], OVERLAY_MENU_STYLE.panelBg[4])
    f:SetBackdropBorderColor(OVERLAY_MENU_STYLE.panelBorder[1], OVERLAY_MENU_STYLE.panelBorder[2], OVERLAY_MENU_STYLE.panelBorder[3], OVERLAY_MENU_STYLE.panelBorder[4])
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)

    local headerBg = f:CreateTexture(nil, "ARTWORK")
    headerBg:SetTexture(W8)
    headerBg:SetPoint("TOPLEFT", 2, -2)
    headerBg:SetPoint("TOPRIGHT", -2, -2)
    headerBg:SetHeight(HEADER_H)
    headerBg:SetVertexColor(OVERLAY_MENU_STYLE.headerBg[1], OVERLAY_MENU_STYLE.headerBg[2], OVERLAY_MENU_STYLE.headerBg[3], OVERLAY_MENU_STYLE.headerBg[4])

    local headerAccent = f:CreateTexture(nil, "OVERLAY")
    headerAccent:SetTexture(W8)
    headerAccent:SetPoint("TOPLEFT", 2, -2)
    headerAccent:SetPoint("TOPRIGHT", -2, -2)
    headerAccent:SetHeight(3)
    headerAccent:SetVertexColor(0.30, 0.60, 0.85, 0.70)
    f.headerAccent = headerAccent

    local headerLine = f:CreateTexture(nil, "ARTWORK")
    headerLine:SetTexture(W8)
    headerLine:SetPoint("TOPLEFT", 2, -(HEADER_H + 2))
    headerLine:SetPoint("TOPRIGHT", -2, -(HEADER_H + 2))
    headerLine:SetHeight(1)
    headerLine:SetVertexColor(OVERLAY_MENU_STYLE.footerLine[1], OVERLAY_MENU_STYLE.footerLine[2], OVERLAY_MENU_STYLE.footerLine[3], OVERLAY_MENU_STYLE.footerLine[4])

    local close = CreateFrame("Button", nil, f, "BackdropTemplate")
    close:SetSize(22, 22)
    close:SetPoint("TOPRIGHT", -8, -8)
    close:SetText("X")
    close:SetNormalFontObject("GameFontNormalSmall")
    if M.StyleButton then M.StyleButton(close) end
    close:SetScript("OnClick", function() f:Hide() end)

    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 14, -10)
    title:SetPoint("RIGHT", close, "LEFT", -8, 0)
    title:SetJustifyH("LEFT")
    title:SetText("Overlay Settings")
    title:SetTextColor(0.96, 0.98, 1.00)
    f.title = title

    local badgeBg = f:CreateTexture(nil, "ARTWORK")
    badgeBg:SetTexture(W8)
    badgeBg:SetHeight(16)
    badgeBg:SetPoint("TOPLEFT", 12, -32)
    badgeBg:SetVertexColor(0.20, 0.45, 0.65, 0.25)
    f._badgeBg = badgeBg

    local categoryBadge = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    categoryBadge:SetPoint("TOPLEFT", 16, -33)
    categoryBadge:SetJustifyH("LEFT")
    categoryBadge:SetText("")
    categoryBadge:SetTextColor(0.75, 0.82, 0.90)
    f.categoryBadge = categoryBadge

    local subtitle = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    subtitle:SetPoint("LEFT", categoryBadge, "RIGHT", 10, 0)
    subtitle:SetPoint("RIGHT", close, "LEFT", -8, 0)
    subtitle:SetJustifyH("LEFT")
    subtitle:SetText("")
    subtitle:SetTextColor(0.50, 0.55, 0.62)
    f.subtitle = subtitle

    local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 12, -(HEADER_H + 8))
    scroll:SetPoint("BOTTOMRIGHT", -28, FOOTER_H + 4)
    scroll:HookScript("OnSizeChanged", function(self, w)
        if f.content and f.content.SetWidth and w and w > 0 then
            f.content:SetWidth(math.max(1, w - 4))
        end
    end)
    f.scroll = scroll

    local footerLine = f:CreateTexture(nil, "ARTWORK")
    footerLine:SetTexture(W8)
    footerLine:SetPoint("BOTTOMLEFT", 2, FOOTER_H)
    footerLine:SetPoint("BOTTOMRIGHT", -2, FOOTER_H)
    footerLine:SetHeight(1)
    footerLine:SetVertexColor(OVERLAY_MENU_STYLE.footerLine[1], OVERLAY_MENU_STYLE.footerLine[2], OVERLAY_MENU_STYLE.footerLine[3], OVERLAY_MENU_STYLE.footerLine[4])

    local btnResetPos = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    btnResetPos:SetSize(160, 26)
    btnResetPos:SetPoint("BOTTOMLEFT", 10, 10)
    btnResetPos:SetText("Reset Position")
    if M.StyleButton then M.StyleButton(btnResetPos) end
    btnResetPos:SetScript("OnClick", function()
        local key = f._muiKey
        if not key then return end
        local MReg = _G.MidnightUI_MoverRegistry
        if MReg and MReg.ResetPosition then
            MReg.ResetPosition(key)
        end
    end)
    f.btnResetPos = btnResetPos

    local btnResetSettings = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    btnResetSettings:SetSize(160, 26)
    btnResetSettings:SetPoint("BOTTOMRIGHT", -10, 10)
    btnResetSettings:SetText("Reset All")
    if M.StyleButton then M.StyleButton(btnResetSettings) end
    btnResetSettings:SetScript("OnClick", function()
        local key = f._muiKey
        if not key then return end
        local D = M.DEFAULTS
        if not D then return end

        local RESET_MAP = {
            PlayerFrame    = { defaults = "PlayerFrame",    apply = "ApplyPlayerSettings" },
            TargetFrame    = { defaults = "TargetFrame",    apply = "ApplyTargetSettings" },
            FocusFrame     = { defaults = "FocusFrame",     apply = "ApplyFocusSettings" },
            PetFrame       = { defaults = "PetFrame",       apply = "ApplyPetSettings" },
            PlayerAuras    = { defaults = {"PlayerFrame", "auras"}, apply = "ApplyPlayerSettings" },
            PlayerDebuffs  = { defaults = {"PlayerFrame", "debuffs"}, apply = "ApplyPlayerSettings" },
            TargetAuras    = { defaults = {"TargetFrame", "auras"}, apply = "ApplyTargetSettings" },
            TargetDebuffs  = { defaults = {"TargetFrame", "debuffs"}, apply = "ApplyTargetSettings" },
            RaidFrames     = { defaults = "RaidFrames",     apply = "ApplyRaidFramesSettings" },
            PartyFrames    = { defaults = "PartyFrames",    apply = "ApplyPartyFramesSettings" },
            MainTankFrames = { defaults = "MainTankFrames", apply = "ApplyMainTankSettings" },
            ConsumableBars = { defaults = "ConsumableBars" },
            Minimap        = { defaults = "Minimap",        apply = "ApplyMinimapSettings" },
            InterfaceMenu  = { defaults = "InterfaceMenu" },
            PetBar         = { defaults = "PetBar" },
            StanceBar      = { defaults = "StanceBar" },
            SoulShards     = { defaults = "SoulShards" },
        }
        for i = 1, 8 do
            RESET_MAP["ActionBar" .. i] = { defaults = {"ActionBars", "bar" .. i}, apply = "ApplyActionBarSettingsImmediate" }
        end
        RESET_MAP["CastBar_player"] = { defaults = {"CastBars", "player"}, apply = "ApplyCastBarSettings" }
        RESET_MAP["CastBar_target"] = { defaults = {"CastBars", "target"}, apply = "ApplyCastBarSettings" }
        RESET_MAP["CastBar_focus"]  = { defaults = {"CastBars", "focus"},  apply = "ApplyCastBarSettings" }

        local mapping = RESET_MAP[key]
        if not mapping then return end

        local defaultSrc, settingsTarget
        if type(mapping.defaults) == "table" then
            defaultSrc = D
            settingsTarget = MidnightUISettings
            for _, seg in ipairs(mapping.defaults) do
                defaultSrc = defaultSrc and defaultSrc[seg]
                settingsTarget = settingsTarget and settingsTarget[seg]
            end
        else
            defaultSrc = D[mapping.defaults]
            settingsTarget = MidnightUISettings[mapping.defaults]
        end

        if defaultSrc and settingsTarget then
            for k, v in pairs(defaultSrc) do
                if k ~= "position" then
                    if type(v) == "table" then
                        settingsTarget[k] = {}
                        for k2, v2 in pairs(v) do settingsTarget[k][k2] = v2 end
                    else
                        settingsTarget[k] = v
                    end
                end
            end
        end

        local MReg = _G.MidnightUI_MoverRegistry
        if MReg and MReg.ResetPosition then
            MReg.ResetPosition(key)
        end

        if mapping.apply and M[mapping.apply] then
            M[mapping.apply]()
        end

        if M.ShowOverlaySettings then
            M.ShowOverlaySettings(key)
        end
    end)
    f.btnResetSettings = btnResetSettings

    return f
end

function M.ShowOverlaySettings(key)
    local def = M.OverlaySettings[key]
    local f = _G.MidnightUI_OverlaySettingsFrame
    if not f then
        f = CreateOverlaySettingsFrame()
        _G.MidnightUI_OverlaySettingsFrame = f
    end
    f._muiKey = key

    if f.content then
        f.content:Hide()
        f.content:SetParent(nil)
        f.content = nil
    end

    local content = CreateFrame("Frame", nil, f.scroll)
    content:SetPoint("TOPLEFT", 0, 0)
    content:SetWidth(math.max(1, (f.scroll:GetWidth() or 400) - 8))
    content:SetHeight(200)
    f.scroll:SetScrollChild(content)
    f.content = content

    if def and def.title then
        f.title:SetText(def.title)
    else
        f.title:SetText("Overlay Settings")
    end

    local category = def and def.category
    f._muiSettingsPage = def and def.settingsPage
    if f.categoryBadge then
        if category and OVERLAY_CATEGORY_COLORS[category] then
            local cc = OVERLAY_CATEGORY_COLORS[category]
            local label = OVERLAY_CATEGORY_LABELS[category] or category
            f.categoryBadge:SetText(label)
            f.categoryBadge:SetTextColor(cc[1], cc[2], cc[3], 0.90)
            if f.headerAccent then
                f.headerAccent:SetVertexColor(cc[1], cc[2], cc[3], 0.65)
            end
            f:SetBackdropBorderColor(cc[1] * 0.6, cc[2] * 0.6, cc[3] * 0.6, 0.45)
            if f._badgeBg then
                f._badgeBg:SetVertexColor(cc[1], cc[2], cc[3], 0.18)
                local badgeW = f.categoryBadge:GetStringWidth() or 60
                f._badgeBg:SetWidth(badgeW + 12)
            end
        else
            f.categoryBadge:SetText("")
            f:SetBackdropBorderColor(OVERLAY_MENU_STYLE.panelBorder[1], OVERLAY_MENU_STYLE.panelBorder[2], OVERLAY_MENU_STYLE.panelBorder[3], OVERLAY_MENU_STYLE.panelBorder[4])
            if f._badgeBg then f._badgeBg:SetVertexColor(0, 0, 0, 0) end
        end
    end
    if f.subtitle then
        f.subtitle:SetText("")
    end

    if def and type(def.build) == "function" then
        local ok, height = M.SafeCall("OverlaySettings:" .. SafeToString(key), def.build, content, key)
        if ok and type(height) == "number" and height > 0 then
            content:SetHeight(height)
            local desired = math.max(286, math.min(760, height + 122))
            f:SetHeight(desired)
        end
    else
        local msg = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        msg:SetPoint("TOPLEFT", 0, 0)
        msg:SetText("No Settings Available")
        msg:SetTextColor(0.9, 0.9, 0.9)
        f:SetHeight(286)
    end

    if f.scroll and f.scroll.SetVerticalScroll then
        f.scroll:SetVerticalScroll(0)
    end

    f:Show()
end

_G.MidnightUI_RegisterOverlaySettings = function(key, def) M.RegisterOverlaySettings(key, def) end
_G.MidnightUI_ShowOverlaySettings = function(key) M.ShowOverlaySettings(key) end
_G.MidnightUI_AttachOverlaySettings = function(overlay, key) AttachOverlayRightClick(overlay, key) end
_G.MidnightUI_AttachOverlayTooltip = function(control, label) AttachTooltip(control, label) end
_G.MidnightUI_CreateOverlayBuilder = function(content, opts) return CreateOverlayBuilder(content, opts) end
_G.MidnightUI_GetOverlayHandle = function(key) return M.OverlayHandles[key] end
_G.MidnightUI_ResolveOverlayPositionTarget = function(key) return ResolveOverlayPositionTarget(key) end
_G.MidnightUI_StyleOverlay = function(overlay, labelText, labelFont, category) StyleOverlay(overlay, labelText, labelFont, category) end
_G.MidnightUI_SaveOverlayPosition = function(frame, target) SaveOverlayPosition(frame, target) end
_G.MidnightUI_ApplyOverlayPosition = function(frame, position) return ApplyOverlayPosition(frame, position) end
_G.MidnightUI_SaveRelativeOverlayPosition = function(frame, ownerFrame, target) SaveRelativeOverlayPosition(frame, ownerFrame, target) end
_G.MidnightUI_ApplyRelativeOverlayPosition = function(frame, ownerFrame, position) return ApplyRelativeOverlayPosition(frame, ownerFrame, position) end
_G.MidnightUI_ScaleFromPercent = function(value) return ScaleFromPercent(value) end
_G.MidnightUI_BeginFrameSnap = function(frame) M.BeginFrameSnap(frame) end
_G.MidnightUI_EndFrameSnap = function(frame) M.EndFrameSnap(frame) end


-- ============================================================
-- SECTION: Lifecycle Event Handler
-- Drives the three-phase module init pipeline and handles
-- post-login housekeeping (Welcome screen, deferred queue).
--
-- EVENTS HANDLED:
--   ADDON_LOADED "MidnightUI"     — EnsureSettings, FlushDebugQueue,
--                                   InitModules("ADDON_LOADED")
--   PLAYER_LOGIN                  — InitModules("PLAYER_LOGIN"),
--                                   TryShowWelcome, TryShowWhatsNew,
--                                   PendingResetForceWelcome check
--   ADDON_LOADED "Blizzard_AchievementUI" — registers AchievementFrame
--                                   in UISpecialFrames so Esc closes it
--   PLAYER_ENTERING_WORLD         — InitModules via RunOrDefer
--   PLAYER_REGEN_ENABLED          — FlushDeferred()
--
-- FUNCTIONS:
--   OnEvent(self, event, arg1)    — main event handler; dispatches to
--                                   the appropriate lifecycle phase
-- ============================================================
local function OnEvent(self, event, arg1)
    if event == "ADDON_LOADED" and arg1 == "MidnightUI" then
        M.EnsureSettings()
        if not _G.MidnightUI_Debug then
            _G.MidnightUI_Debug = function(...) M.Debug(...) end
        end
        if not _G.MidnightUI_LogDebug then
            _G.MidnightUI_LogDebug = function(...)
                if _G.MidnightUI_Debug then
                    return _G.MidnightUI_Debug(...)
                end
                if _G.DEFAULT_CHAT_FRAME then
                    local parts = {}
                    for i = 1, select("#", ...) do
                        parts[#parts + 1] = tostring(select(i, ...))
                    end
                    _G.DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffMidnightUI:|r " .. table.concat(parts, " "))
                end
            end
        end
        M.FlushDebugQueue()
        M.InitModules("ADDON_LOADED")
        return
    end
    if event == "PLAYER_LOGIN" then
        M.InitModules("PLAYER_LOGIN")
        if _G.MidnightUI_TryShowWelcome then
            _G.MidnightUI_TryShowWelcome()
        end
        if C_Timer and C_Timer.After then
            C_Timer.After(2.0, function()
                if _G.MidnightUI_TryShowWhatsNew then
                    pcall(_G.MidnightUI_TryShowWhatsNew)
                end
            end)
        end
        if _G.MidnightUISettings and _G.MidnightUISettings.PendingResetForceWelcome then
            if _G.MidnightUI_ShowWelcome then
                local ok, err = pcall(_G.MidnightUI_ShowWelcome, true)
                if ok then
                    _G.MidnightUISettings.PendingResetForceWelcome = nil
                else
                    if _G.MidnightUI_Diagnostics and _G.MidnightUI_Diagnostics.LogDebugSource and _G.MidnightUI_Diagnostics.IsEnabled and _G.MidnightUI_Diagnostics.IsEnabled() then
                        _G.MidnightUI_Diagnostics.LogDebugSource("Core", "PLAYER_LOGIN: force Welcome failed: " .. tostring(err))
                    elseif _G.MidnightUI_Debug then
                        _G.MidnightUI_Debug("[Core] PLAYER_LOGIN: force Welcome failed: " .. tostring(err))
                    end
                    if C_Timer and C_Timer.After then
                        C_Timer.After(1.0, function()
                            if _G.MidnightUISettings and _G.MidnightUISettings.PendingResetForceWelcome and _G.MidnightUI_ShowWelcome then
                                local ok2 = pcall(_G.MidnightUI_ShowWelcome, true)
                                if ok2 then
                                    _G.MidnightUISettings.PendingResetForceWelcome = nil
                                end
                            end
                        end)
                    end
                end
            else
                if C_Timer and C_Timer.After then
                    C_Timer.After(1.0, function()
                        if _G.MidnightUISettings and _G.MidnightUISettings.PendingResetForceWelcome and _G.MidnightUI_ShowWelcome then
                            local ok2 = pcall(_G.MidnightUI_ShowWelcome, true)
                            if ok2 then
                                _G.MidnightUISettings.PendingResetForceWelcome = nil
                            end
                        end
                    end)
                end
            end
        end
        return
    end
    if event == "ADDON_LOADED" and arg1 == "Blizzard_AchievementUI" then
        local af = _G.AchievementFrame
        if af and af.GetName then
            local name = af:GetName()
            if name then
                local found = false
                for _, n in ipairs(UISpecialFrames) do
                    if n == name then found = true; break end
                end
                if not found then
                    table.insert(UISpecialFrames, name)
                end
            end
        end
    end
    if event == "PLAYER_ENTERING_WORLD" then
        M.RunOrDefer("InitModules(PLAYER_ENTERING_WORLD)", function()
            M.InitModules("PLAYER_ENTERING_WORLD")
        end)
        return
    end
    if event == "PLAYER_REGEN_ENABLED" then
        M.FlushDeferred()
    end
end

local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:SetScript("OnEvent", OnEvent)

-- ============================================================
-- SECTION: UI Utilities (Frame Clamping + Help Tip Suppression)
-- Two unrelated quality-of-life hooks that run passively.
-- Frame clamping keeps WoW's tutorial pointer frames on screen
-- after resolution changes. Help tip suppression hides Blizzard's
-- in-game tutorial popups which can overlap MidnightUI elements.
--
-- FUNCTIONS:
--   ClampFrameOnScreen(frame)    — moves a frame back on screen
--                                  if it has drifted off-edge
--   HookFrameClamp(frame)        — hooks SetPoint and OnShow on a
--                                  frame so it auto-clamps after
--                                  any position change
--   HideAllActiveHelpTips()      — hides all currently shown HelpTip
--                                  frames from Blizzard's pool
--   HookHelpTipSystem()          — hooks HelpTip.Show to suppress
--                                  future tips as soon as they appear
-- ============================================================
do
    local PADDING = 20

    local function ClampFrameOnScreen(frame)
        if not frame or not frame.GetCenter then return end
        local cx, cy = frame:GetCenter()
        if not cx or not cy then return end
        local w, h = frame:GetSize()
        if not w or not h or w == 0 or h == 0 then return end
        local sw = GetScreenWidth()
        local sh = GetScreenHeight()
        if not sw or not sh or sw == 0 or sh == 0 then return end
        local s = frame:GetEffectiveScale()
        if not s or s == 0 then s = 1 end
        local halfW = (w / 2)
        local halfH = (h / 2)
        local needsMove = false
        local newX, newY = cx, cy
        if (cx - halfW) < PADDING then newX = halfW + PADDING; needsMove = true end
        if (cx + halfW) > (sw / s - PADDING) then newX = sw / s - halfW - PADDING; needsMove = true end
        if (cy - halfH) < PADDING then newY = halfH + PADDING; needsMove = true end
        if (cy + halfH) > (sh / s - PADDING) then newY = sh / s - halfH - PADDING; needsMove = true end
        if needsMove then
            frame:ClearAllPoints()
            frame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", newX, newY)
        end
    end

    local function HookFrameClamp(frame)
        if not frame or frame._muiClampHooked then return end
        frame._muiClampHooked = true
        hooksecurefunc(frame, "SetPoint", function(self)
            if self._muiClamping then return end
            self._muiClamping = true
            C_Timer.After(0, function()
                ClampFrameOnScreen(self)
                self._muiClamping = false
            end)
        end)
        frame:HookScript("OnShow", function(self)
            if self._muiClamping then return end
            self._muiClamping = true
            C_Timer.After(0, function()
                ClampFrameOnScreen(self)
                self._muiClamping = false
            end)
        end)
    end

    local helpTipHooked = false

    local function HideAllActiveHelpTips()
        if not HelpTip or not HelpTip.framePool then return end
        for frame in HelpTip.framePool:EnumerateActive() do
            if frame and frame:IsShown() then
                frame:Hide()
            end
        end
    end

    local function HookHelpTipSystem()
        if helpTipHooked then return end
        if not HelpTip or type(HelpTip.Show) ~= "function" then return end
        helpTipHooked = true
        hooksecurefunc(HelpTip, "Show", function(self)
            if not self.framePool then return end
            for frame in self.framePool:EnumerateActive() do
                if frame and frame:IsShown() then
                    frame:Hide()
                end
            end
        end)
        HideAllActiveHelpTips()
    end

    HookHelpTipSystem()

    local clampWatcher = CreateFrame("Frame")
    clampWatcher:RegisterEvent("PLAYER_ENTERING_WORLD")
    clampWatcher:RegisterEvent("ADDON_LOADED")
    clampWatcher:SetScript("OnEvent", function(self, event)
        for i = 1, 10 do
            local name = "TutorialPointerFrame_" .. i
            local ptr = _G[name]
            if ptr then HookFrameClamp(ptr) end
        end
        HookHelpTipSystem()
        HideAllActiveHelpTips()
    end)
end

-- ============================================================
-- SECTION: Settings Dump (/muidump)
-- Developer diagnostic tool. Serializes the entire MidnightUISettings
-- SavedVariable to a human-readable Lua-table string and sends it
-- to the Diagnostics Console. Useful for sharing settings with
-- support or verifying what values are actually saved.
--
-- SLASH COMMANDS:
--   /muidump  /muidumppositions  — triggers the dump
--
-- FUNCTIONS:
--   SerializeValue(v, indent)   — recursive Lua value serializer;
--                                  handles strings, numbers, booleans,
--                                  tables (arrays and dicts)
--   DumpAllSettings()           — collects all named sections from
--                                  MidnightUISettings, serializes each,
--                                  sends to Diagnostics + chat notice
-- ============================================================
do
    local function SerializeValue(v, indent)
        local t = type(v)
        if t == "string" then
            return string.format("%q", v)
        elseif t == "number" then
            if v == math.floor(v) then
                return tostring(v)
            else
                return string.format("%.8g", v)
            end
        elseif t == "boolean" then
            return v and "true" or "false"
        elseif t == "nil" then
            return "nil"
        elseif t == "table" then
            local pad = string.rep("  ", indent)
            local padInner = string.rep("  ", indent + 1)
            local n = #v
            local isArray = n > 0
            if isArray then
                for i = 1, n do
                    if v[i] == nil then isArray = false; break end
                end
                local count = 0
                for _ in pairs(v) do count = count + 1 end
                if count ~= n then isArray = false end
            end
            if isArray then
                local parts = {}
                for i = 1, n do
                    parts[i] = SerializeValue(v[i], indent + 1)
                end
                local oneLine = "{ " .. table.concat(parts, ", ") .. " }"
                if #oneLine < 100 then return oneLine end
                local lines = { "{" }
                for i = 1, n do
                    lines[#lines + 1] = padInner .. parts[i] .. (i < n and "," or "")
                end
                lines[#lines + 1] = pad .. "}"
                return table.concat(lines, "\n")
            else
                local keys = {}
                for k in pairs(v) do keys[#keys + 1] = k end
                table.sort(keys, function(a, b)
                    if type(a) == type(b) then return tostring(a) < tostring(b) end
                    return type(a) < type(b)
                end)
                if #keys == 0 then return "{}" end
                local lines = { "{" }
                for idx, k in ipairs(keys) do
                    local kStr
                    if type(k) == "string" and k:match("^[%a_][%w_]*$") then
                        kStr = k
                    else
                        kStr = "[" .. SerializeValue(k, 0) .. "]"
                    end
                    local comma = idx < #keys and "," or ""
                    local valStr = SerializeValue(v[k], indent + 1)
                    lines[#lines + 1] = padInner .. kStr .. " = " .. valStr .. comma
                end
                lines[#lines + 1] = pad .. "}"
                return table.concat(lines, "\n")
            end
        end
        return "nil --[[ unsupported type: " .. t .. " ]]"
    end

    local function DumpAllSettings()
        if not MidnightUISettings then
            if DEFAULT_CHAT_FRAME then
                DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r No settings to dump — MidnightUISettings is nil.", 1, 0.4, 0.4)
            end
            return
        end

        local sections = {
            "PlayerFrame", "TargetFrame", "FocusFrame", "PetFrame",
            "PartyFrames", "RaidFrames", "MainTankFrames",
            "ActionBars", "PetBar", "StanceBar",
            "CastBars", "Messenger", "ConsumableBars",
            "Minimap", "Nameplates", "Combat",
            "General", "Inventory", "XPBar", "RepBar",
            "InterfaceMenu", "GlobalStyle",
            "movers",
        }

        local lines = {}
        local function L(s) lines[#lines + 1] = s end

        L("@@MIDNIGHTUI_SETTINGS_DUMP@@")
        L("")

        for _, section in ipairs(sections) do
            local val = MidnightUISettings[section]
            if val ~= nil then
                L("== " .. section .. " ==")
                L(section .. " = " .. SerializeValue(val, 0))
                L("")
            end
        end

        L("@@END_SETTINGS_DUMP@@")

        local fullText = table.concat(lines, "\n")

        if _G.MidnightUI_Diagnostics and _G.MidnightUI_Diagnostics.LogDebugSource then
            _G.MidnightUI_Diagnostics.LogDebugSource("SettingsDump", fullText)
        end

        if DEFAULT_CHAT_FRAME then
            DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[MidnightUI]|r Full settings dump sent to Diagnostics Console (" .. #lines .. " lines). Open with /muiconsole.", 0.5, 1.0, 0.8)
        end
    end

    SlashCmdList["MIDNIGHTUI_DUMPPOS"] = DumpAllSettings
    SLASH_MIDNIGHTUI_DUMPPOS1 = "/muidumppositions"
    SLASH_MIDNIGHTUI_DUMPPOS2 = "/muidump"
end
