
-- ============================================================
-- FILE: Nameplates.lua
-- PURPOSE: Complete replacement of Blizzard's nameplate UI with
--   custom health bars, threat indicators, party threat markers,
--   hover glow effects, dispel icons, and an integrated cast bar.
-- HOW IT WORKS:
--   NameplateManager (a bare Frame) receives all events. On
--   NAME_PLATE_UNIT_ADDED, StyleNameplate() creates the custom
--   frame hierarchy as children of the Blizzard plate root, and
--   SuppressDefault() hides all default Blizzard nameplate elements
--   with hooksecurefunc guards to prevent them re-showing.
--   A 0.2s C_Timer ticker calls UpdateAllPlates() → UpdatePlate()
--   for every tracked plate, updating health, threat, name, and
--   cast bar visuals. Spell cast events are delegated entirely to
--   MidnightUI_CastBars.
--   Non-target nameplates fade in/out via RootOnUpdate based on
--   combat state and proximity (oocAlphaFar/oocAlphaNear).
--   Threat display supports both player perspective and raid-tank
--   perspective with a group relative threat algorithm.
-- DEPENDS ON:
--   MidnightUI_CastBars        — cast bar creation and all spell events
--   MidnightUI_Core.GetClassColor — class colour lookup
--   MidnightUISettings.Nameplates — enabled/hover/size settings
--   MidnightUISettings.CastBars   — cast bar settings
--   C_NamePlate, C_ClassColor, UnitThreatSituation, UnitFactionGroup
-- EXPORTS:
--   _G.MidnightUI_RefreshNameplates()
--   _G.MidnightUI_GetNameplateCastBar(unit)
--   _G.MidnightUI_ApplyNameplateCastStyleToBar(bar, isTargetUnit)
--   _G.MidnightUI_GetNameplateHealthPct(guid) [via NameplateHealthPctCache]
-- OWNED SAVEDVARIABLES: none (reads only from MidnightUISettings)
-- EVENTS REGISTERED (34): NAME_PLATE_UNIT_ADDED/REMOVED, UNIT_HEALTH,
--   UNIT_MAXHEALTH, PLAYER_TARGET_CHANGED, UNIT_TARGET,
--   UNIT_THREAT_SITUATION_UPDATE, UNIT_THREAT_LIST_UPDATE,
--   PLAYER_ROLES_ASSIGNED, GROUP_ROSTER_UPDATE, PLAYER_REGEN_DISABLED,
--   PLAYER_REGEN_ENABLED, CVAR_UPDATE, UPDATE_MOUSEOVER_UNIT,
--   UNIT_SPELLCAST_* (14 events — delegated to CastBars module),
--   SPELLS_CHANGED, PLAYER_TALENT_UPDATE, UNIT_PET,
--   PLAYER_SPECIALIZATION_CHANGED, ADDON_LOADED
-- FRAMES CREATED: custom overlay frames as children of each Blizzard
--   plate root (not the unitFrame) — one root per nameplate unit token
-- GOTCHAS:
--   SuppressDefault hooks unitFrame.SetAlpha and OnShow to re-suppress
--   after each Blizzard show attempt. Uses alpha=0.01 not 0 to avoid
--   fully hiding the frame (which breaks WidgetContainer).
--   WidgetContainer (buff icons) is rescued from unitFrame and
--   reparented to plate with raised frame level.
--   Hover sensor SetPropagateMouseClicks/Motion is blocked in combat;
--   deferred to pendingHoverPropagate and applied on PLAYER_REGEN_ENABLED.
--   Threat cache (2s TTL), group threat cache (0.3s TTL), and health
--   pct cache are all in-memory only — not persisted.
-- ============================================================

local ADDON_NAME = "MidnightUI"
local NameplateManager = CreateFrame("Frame")

local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitThreatSituation = UnitThreatSituation
local UnitExists = UnitExists
local UnitIsUnit = UnitIsUnit
local GetTime = GetTime
local pairs = pairs
local type = type
local string_match = string.match

local config = {
    width = 200,
    height = 20,
    threatHeight = 5,
    spacing = 3,
    namePadding = 6,
    nameFontSize = 10,
    healthPctFontSize = 9,
    nameAlign = "LEFT",
    healthPctDisplay = "RIGHT",
    borderAlpha = 0.7,
    castWidth = 200,
    castHeight = 16,
    castAlpha = 1.0,
    castFontSize = 12,
    castIconPadding = 4,
    updateInterval = 0.2,
}

config.oocAlphaFar = 0.5
config.oocAlphaNear = 1.0
config.oocFadeSpeed = 4.0
config.hoverBorderSize   = 2
config.hoverBorderColor  = { 1, 1, 1, 0.85 }

-- ============================================================
-- SECTION: Module Init + Default Config + Constants
-- NameplateManager is the sole event frame for this module.
-- config table provides default dimensions, font sizes, fade
-- speeds, and hover border settings. These are compile-time
-- defaults; runtime values come from GetNameplateHealthConfig()
-- and GetNameplateCastConfig() which read MidnightUISettings.
-- PARTY_THREAT_MARKER_* constants drive the raid threat icon
-- rendering (size, atlas, rotation, colour boost).
-- CAST_COLORS maps cast state strings to RGB for the cast bar.
-- IsNameplatesEnabled() is the master on/off gate — checked at
-- the top of StyleNameplate and UpdatePlate.
-- READS:  MidnightUISettings.Nameplates.enabled
-- WRITES: (pure declarations + config table)
-- FUNCTIONS:
--   GetHoverSettings()      → enabled, pulse, borderSize
--   IsNameplatesEnabled()   → bool
-- ============================================================
local function GetHoverSettings()
    local s = MidnightUISettings and MidnightUISettings.Nameplates and MidnightUISettings.Nameplates.hover
    local enabled = not s or s.enabled ~= false
    local pulse = not s or s.pulse ~= false
    local borderSize = s and s.borderSize or config.hoverBorderSize
    return enabled, pulse, borderSize
end

local PARTY_THREAT_MARKER_SIZE = 14
local PARTY_THREAT_MARKER_Y_OFFSET = 2
local PARTY_THREAT_MARKER_TEXTURE = "Interface\\Buttons\\Arrow-Up"
local PARTY_THREAT_MARKER_ATLAS = "ui-frame-genericplayerchoice-portrait-qualitywhite-01"
local PARTY_THREAT_MARKER_ROTATION = math.rad(-90)
local PARTY_THREAT_TANK_ATLAS = "icons_64x64_tank"
local PARTY_THREAT_TANK_ROTATION = 0
local PARTY_THREAT_MARKER_SAT_BOOST = 1.0
local PARTY_THREAT_MARKER_BRIGHT_BOOST = 1.0

local function IsNameplatesEnabled()
    local s = MidnightUISettings and MidnightUISettings.Nameplates
    if s and s.enabled == false then return false end
    return true
end

local CAST_COLORS = {
    NORMAL = {1.0, 0.8, 0.0},
    CHANNEL = {0.0, 1.0, 0.0},
    UNINTERRUPTIBLE = {0.55, 0.55, 0.55},
    FAILED = {1.0, 0.0, 0.0},
}

local function _SafeEquals(a, b) return a == b end
local function _TestEqualsTrue(value) return value == true end
local function _TestUnitIsUnit(a, b)
    if UnitIsUnit(a, b) then return true end
    return false
end
local function _SetBorderColorInner(border, r, g, b, a)
    if border.top then border.top:SetColorTexture(r, g, b, a) end
    if border.bottom then border.bottom:SetColorTexture(r, g, b, a) end
    if border.left then border.left:SetColorTexture(r, g, b, a) end
    if border.right then border.right:SetColorTexture(r, g, b, a) end
end
local function _SetBorderThicknessInner(border, t)
    if border.top then border.top:SetHeight(t) end
    if border.bottom then border.bottom:SetHeight(t) end
    if border.left then border.left:SetWidth(t) end
    if border.right then border.right:SetWidth(t) end
end
local function _CalcClampedPercent(threatPct)
    local v = (threatPct or 0) / 100
    if v < 0 then return 0 end
    if v > 1 then return 1 end
    return v
end
local function _CalcHealthPct(curVal, maxVal)
    return (curVal / maxVal) * 100
end
local function _UpdateHealthBar(bar, max, current)
    bar:SetMinMaxValues(0, max)
    bar:SetValue(current)
end
local function _SetFormattedPct(fontString, pct)
    fontString:SetFormattedText("%.0f%%", pct)
end
local function _GetFromCache(cache, key)
    return cache[key]
end

-- ============================================================
-- SECTION: Pure Helpers — Safe Comparisons + Health Calculations
-- All _Safe* helpers use pcall to avoid taint from secret unit
-- health/guid values during restricted combat.
-- IsTargetUnit: GUID-based fallback when UnitIsUnit is tainted.
-- GetNameplateHealthConfig: reads MidnightUISettings.Nameplates for
--   size/alpha/font size, returns separate values for target vs.
--   non-target. Callers use the isTargetUnit bool to pick the right
--   set.
-- GetNameplateScale: reads settings for per-target scale multiplier.
-- READS:  MidnightUISettings.Nameplates
-- WRITES: (pure functions)
-- FUNCTIONS:
--   IsTargetUnit(unit)                       → bool
--   GetNameplateHealthConfig(isTarget)       → w, h, alpha, nameFontSize, hpFontSize, nameAlign, pctDisplay
--   GetNameplateScale(isTarget)              → float (0.5–2.0)
-- ============================================================
local function IsTargetUnit(unit)
    if not unit or not UnitExists("target") then return false end
    local okIsUnit, isUnit = pcall(UnitIsUnit, unit, "target")
    if okIsUnit and isUnit then return true end
    local guid = UnitGUID(unit)
    local targetGuid = UnitGUID("target")
    if guid and targetGuid then
        local okGuid, match = pcall(_SafeEquals, guid, targetGuid)
        if okGuid and match then return true end
    end
    return false
end

local function GetNameplateHealthConfig(isTarget)
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    local health = settings and settings.healthBar
    local target = settings and settings.target and settings.target.healthBar
    local width = (health and health.width) or config.width
    local height = (health and health.height) or config.height
    local alpha = (health and health.alpha) or 1.0
    local nonTargetAlpha = (health and health.nonTargetAlpha) or alpha
    local nameFontSize = (health and health.nameFontSize) or config.nameFontSize
    local healthPctFontSize = (health and health.healthPctFontSize) or config.healthPctFontSize
    local nameAlign = (health and health.nameAlign) or config.nameAlign
    local healthPctDisplay = (health and health.healthPctDisplay) or config.healthPctDisplay
    if isTarget and target then
        width = target.width or width
        height = target.height or height
        nameFontSize = target.nameFontSize or nameFontSize
        healthPctFontSize = target.healthPctFontSize or healthPctFontSize
    else
        alpha = nonTargetAlpha
    end
    return width, height, alpha, nameFontSize, healthPctFontSize, nameAlign, healthPctDisplay
end

local function GetNameplateScale(isTarget)
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    local scale = settings and settings.scale
    local targetScale = settings and settings.target and settings.target.scale
    if isTarget and type(targetScale) == "number" then
        scale = targetScale
    end
    if type(scale) ~= "number" then return 1.0 end
    if scale > 5 then
        return math.max(0.5, math.min(scale / 100, 2.0))
    end
    return math.max(0.5, math.min(scale, 2.0))
end

-- ============================================================
-- SECTION: Nameplate Layout + Appearance Config
-- ApplyNameplateTextLayout: repositions name/health text containers
--   within a nameplate entry to match current settings (alignment,
--   width, padding). Called on entry creation and settings change.
-- GetNameplateThreatConfig: returns threat bar enabled/width/height/
--   alpha from MidnightUISettings.Nameplates.
-- GetNameplateCastConfig: returns cast bar enabled/width/height/alpha.
-- THREAT_HEALTH_COLORS / THREAT_CAST_COLORS: RGB presets for threat
--   states. Threat cache TTL constants.
-- READS:  MidnightUISettings.Nameplates, entry fields
-- WRITES: entry.nameContainer, entry.name positions
-- FUNCTIONS:
--   ApplyNameplateTextLayout(entry, isTargetUnit)
--   GetNameplateThreatConfig(isTarget)  → enabled, w, h, alpha
--   GetNameplateCastConfig(isTarget)    → enabled, w, h, alpha
-- ============================================================
local function ApplyNameplateTextLayout(entry, isTargetUnit)
    if not entry or not entry.healthBar then return end
    local _, _, _, _, _, nameAlign, pctDisplay = GetNameplateHealthConfig(isTargetUnit)
    local nameContainer = entry.nameContainer
    local nameText = entry.name
    local healthPct = entry.healthPct

    local leftPad = config.namePadding
    local rightPad = config.namePadding
    local pctGap = 4

    if healthPct then
        healthPct:ClearAllPoints()
        if pctDisplay == "HIDE" then
            healthPct:Hide()
        elseif pctDisplay == "LEFT" then
            healthPct:Show()
            healthPct:SetPoint("LEFT", entry.healthBar, "LEFT", leftPad, 0)
        else
            healthPct:Show()
            healthPct:SetPoint("RIGHT", entry.healthBar, "RIGHT", -rightPad, 0)
        end
    end

    if nameContainer then
        nameContainer:ClearAllPoints()
        if pctDisplay == "LEFT" and healthPct and healthPct:IsShown() then
            nameContainer:SetPoint("TOPLEFT", entry.healthBar, "TOPLEFT", leftPad, -1)
            nameContainer:SetPoint("BOTTOMLEFT", entry.healthBar, "BOTTOMLEFT", leftPad, 1)
            nameContainer:SetPoint("LEFT", healthPct, "RIGHT", pctGap, 0)
            nameContainer:SetPoint("RIGHT", entry.healthBar, "RIGHT", -rightPad, 0)
        elseif pctDisplay == "RIGHT" and healthPct and healthPct:IsShown() then
            nameContainer:SetPoint("TOPLEFT", entry.healthBar, "TOPLEFT", leftPad, -1)
            nameContainer:SetPoint("BOTTOMLEFT", entry.healthBar, "BOTTOMLEFT", leftPad, 1)
            nameContainer:SetPoint("RIGHT", healthPct, "LEFT", -pctGap, 0)
        else
            nameContainer:SetPoint("TOPLEFT", entry.healthBar, "TOPLEFT", leftPad, -1)
            nameContainer:SetPoint("BOTTOMRIGHT", entry.healthBar, "BOTTOMRIGHT", -rightPad, 1)
        end
    end

    if nameText and nameContainer then
        nameText:ClearAllPoints()
        nameText:SetPoint("LEFT", nameContainer, "LEFT", 0, 0)
        nameText:SetPoint("RIGHT", nameContainer, "RIGHT", 0, 0)
        local align = (nameAlign == "CENTER" or nameAlign == "RIGHT") and nameAlign or "LEFT"
        nameText:SetJustifyH(align)
    end
end

local function GetNameplateThreatConfig(isTarget)
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    local threat = settings and settings.threatBar
    local target = settings and settings.target and settings.target.threatBar
    local enabled = true
    if threat and threat.enabled ~= nil then enabled = threat.enabled end
    local width = (threat and threat.width) or config.width
    local height = (threat and threat.height) or config.threatHeight
    local alpha = (threat and threat.alpha) or 1.0
    if isTarget and target then
        width = target.width or width
        height = target.height or height
    end
    return enabled, width, height, alpha
end

local function GetNameplateCastConfig(isTarget)
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    local cast = settings and settings.castBar
    local target = settings and settings.target and settings.target.castBar
    local width = (cast and cast.width) or config.castWidth or config.width
    local height = (cast and cast.height) or config.castHeight or config.height
    local alpha = (cast and cast.alpha) or config.castAlpha or 1.0
    local fontSize = (cast and cast.fontSize) or config.castFontSize or (config.nameFontSize + 2)
    if isTarget and target then
        width = target.width or width
        height = target.height or height
    end
    return width, height, alpha, fontSize
end

local THREAT_HEALTH_COLORS = {
    red = {0.90, 0.10, 0.10},
    yellow = {1.00, 0.90, 0.20},
    green = {0.10, 0.90, 0.20},
    gray = {0.55, 0.55, 0.55},
}

local THREAT_BAR_COLORS = {
    red = {0.90, 0.10, 0.10},
    yellow = {1.00, 0.90, 0.20},
    green = {0.10, 0.90, 0.20},
}

local THREAT_CACHE_TTL = 2.0
local THREAT_NO_DATA_DELAY = 0.3
local GROUP_THREAT_CACHE_TTL = 0.3


-- ============================================================
-- SECTION: Plate Registry + Hover System
-- plates{}: unit-token → entry table for every active nameplate.
--   Populated by StyleNameplate, cleared by OnNameplateRemoved.
-- pendingHoverPropagate{}: sensor frames whose propagation flags
--   could not be set during combat. Drained on PLAYER_REGEN_ENABLED.
-- unitHoverRoot: the plate root frame currently under the mouse.
-- StopHoverPulse/ClearHoverState: reset the animated hover glow.
-- RootOnUpdate: per-frame alpha interpolation for non-target plates
--   (fade out when OOC + far, fade in when targeted/near/in-combat).
--   Also drives hover pulse animation on unitHoverRoot.
-- HideAllPlates: emergency hide; called on ADDON_LOADED or disable.
-- READS:  config.oocAlpha*, config.oocFadeSpeed, GetHoverSettings()
-- WRITES: plates, pendingHoverPropagate, unitHoverRoot
-- FUNCTIONS:
--   StopHoverPulse(hp)     — stop pulse animation
--   ClearHoverState(entry) — reset hover glow
--   RootOnUpdate(self, elapsed) — OOC alpha + hover pulse driver
--   HideAllPlates()        — hide all plates + clear hover
-- ============================================================
local plates = {}
local pendingHoverPropagate = {}
local unitHoverRoot = nil

local function StopHoverPulse(hp)
    if not hp then return end
    hp._muiPulseActive = false
    hp:SetScript("OnUpdate", nil)
    hp._muiPulseT = 0
    hp:SetAlpha(0)
    if hp._muiAnchor then
        hp:ClearAllPoints()
        hp:SetPoint("TOPLEFT", hp._muiAnchor, "TOPLEFT", 0, 0)
        hp:SetPoint("BOTTOMRIGHT", hp._muiAnchor, "BOTTOMRIGHT", 0, 0)
    end
    hp:Hide()
end

local function ClearHoverState(entry)
    if not entry then return end
    if entry.hoverGlow then
        entry.hoverGlow._muiTargetAlpha = 0
    end
    StopHoverPulse(entry.hoverPulse)
    if entry.root then
        entry.root._muiHoverActive = false
        entry.root._muiUnitHover = false
    end
end

local function RootOnUpdate(self, elapsed)
    local target = self._muiTargetAlpha or 1
    local cur = self:GetAlpha()
    if math.abs(cur - target) < 0.01 then
        if cur ~= target then self:SetAlpha(target) end
    else
        local step = config.oocFadeSpeed * elapsed
        if cur < target then
            self:SetAlpha(math.min(cur + step, target))
        else
            self:SetAlpha(math.max(cur - step, target))
        end
    end
    if self._muiUnitHover then
        local e = self.MidnightUIEntry
        local unit = e and e.unit
        if unit and IsTargetUnit(unit) then
            local mouseWasOnBar = self._muiHoverActive
            ClearHoverState(e)
            if unitHoverRoot == self then unitHoverRoot = nil end
            if mouseWasOnBar then self._muiHoverActive = true end
        elseif not unit or not UnitExists("mouseover") or not UnitIsUnit("mouseover", unit) then
            if not self._muiHoverActive then
                self._muiUnitHover = false
                unitHoverRoot = nil
                if e then
                    if e.hoverGlow then e.hoverGlow._muiTargetAlpha = 0 end
                    StopHoverPulse(e.hoverPulse)
                end
            end
        end
    elseif self._muiHoverActive and not self._muiUnitHover then
        local e = self.MidnightUIEntry
        local unit = e and e.unit
        if unit and not IsTargetUnit(unit) then
            local sensor = e and e.hoverSensor
            if sensor then
                local onEnter = sensor:GetScript("OnEnter")
                if onEnter then onEnter(sensor) end
            end
        end
    end
    local e = self.MidnightUIEntry
    if e and e.hoverGlow then
        local hg = e.hoverGlow
        local hTarget = hg._muiTargetAlpha or 0
        local hCur = hg:GetAlpha()
        local diff = hTarget - hCur
        if math.abs(diff) > 0.005 then
            local step = elapsed * 8
            if diff > 0 then
                hg:SetAlpha(math.min(hCur + step, 1))
            else
                local newA = math.max(hCur - step, 0)
                hg:SetAlpha(newA)
                if newA <= 0 then hg:Hide() end
            end
        elseif hCur ~= hTarget then
            hg:SetAlpha(hTarget)
            if hTarget == 0 then hg:Hide() end
        end
    end
end

local threatCache = {}
local groupThreatCache = {}
local UpdateTargetGlow

local function HideAllPlates()
    for _, entry in pairs(plates) do
        if entry then
            UpdateTargetGlow(entry, false)
            if entry.root then entry.root:Hide() end
            if entry.castBar then entry.castBar:Hide() end
            if entry.threatBar then entry.threatBar:Hide() end
            if entry.threatContainer then entry.threatContainer:Hide() end
            if entry.threatBar2 then entry.threatBar2:Hide() end
            if entry.threatContainer2 then entry.threatContainer2:Hide() end
            if entry.partyThreatMarkerContainer then entry.partyThreatMarkerContainer:Hide() end
        end
    end
end

local function SetFontSafe(fs, fontPath, size, flags)
    local resolved = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath(fontPath)) or fontPath
    local ok = fs:SetFont(resolved, size, flags)
    if not ok then
        local fallback = (_G.MidnightUI_ResolveFontPath and _G.MidnightUI_ResolveFontPath()) or (GameFontNormal and GameFontNormal:GetFont())
        if fallback then
            fs:SetFont(fallback, size or 12, flags)
        end
    end
    return ok
end

local function ApplyNameplateFont(fs, fontPath, size, flags, label)
    SetFontSafe(fs, fontPath, size, flags)
end

-- ============================================================
-- SECTION: Frame Construction Utilities
-- Low-level helpers used by StyleNameplate to build sub-frames.
-- SetFontSafe/ApplyNameplateFont: pcall-wrapped font setters;
--   log failures so a missing font file doesn't crash.
-- GetUnitClassColor: delegates to Core.GetClassColor then falls
--   back to RAID_CLASS_COLORS for the unit's class.
-- CreateBlackBorder: builds a 4-texture 1px border frame with
--   raised frame level.
-- EnsureCastBarGlowHalo: creates a glow border around the cast bar
--   frame (size/outset configurable, lazy init).
-- EnsureCastBarSheen: animated shine overlay on the cast bar.
-- SetBorderColor/SetBorderThickness: pcall-wrapped wrappers around
--   the _SetBorderColorInner/_SetBorderThicknessInner helpers.
-- READS:  MidnightUI_Core.GetClassColor, RAID_CLASS_COLORS
-- WRITES: (frame construction — no SavedVar writes)
-- FUNCTIONS:
--   SetFontSafe(fs, path, size, flags)
--   ApplyNameplateFont(fs, path, size, flags, label)
--   GetUnitClassColor(unit)                 → r, g, b
--   CreateBlackBorder(parent, alpha)        → border frame
--   EnsureCornerCut(frame, size, r, g, b, a)
--   EnsureCastBarGlowHalo(target, outset, parentOverride)
--   EnsureCastBarSheen(castBar)
--   SetBorderColor(border, r, g, b, a)
--   SetBorderThickness(border, t)
-- ============================================================
local function GetUnitClassColor(unit)
    if _G.MidnightUI_Core and _G.MidnightUI_Core.GetClassColor then
        return _G.MidnightUI_Core.GetClassColor(unit)
    end
    local _, class = UnitClass(unit)
    if class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
        local c = RAID_CLASS_COLORS[class]
        return c.r, c.g, c.b
    end
    return 0.9, 0.9, 0.9
end

local function CreateBlackBorder(parent, alpha)
    local a = alpha or 1
    local border = CreateFrame("Frame", nil, parent)
    border:SetAllPoints()
    if border.SetFrameLevel and parent and parent.GetFrameLevel then
        border:SetFrameLevel(parent:GetFrameLevel() + 5)
    end
    border.top = border:CreateTexture(nil, "OVERLAY"); border.top:SetHeight(1); border.top:SetPoint("TOPLEFT"); border.top:SetPoint("TOPRIGHT"); border.top:SetColorTexture(0, 0, 0, a)
    border.bottom = border:CreateTexture(nil, "OVERLAY"); border.bottom:SetHeight(1); border.bottom:SetPoint("BOTTOMLEFT"); border.bottom:SetPoint("BOTTOMRIGHT"); border.bottom:SetColorTexture(0, 0, 0, a)
    border.left = border:CreateTexture(nil, "OVERLAY"); border.left:SetWidth(1); border.left:SetPoint("TOPLEFT"); border.left:SetPoint("BOTTOMLEFT"); border.left:SetColorTexture(0, 0, 0, a)
    border.right = border:CreateTexture(nil, "OVERLAY"); border.right:SetWidth(1); border.right:SetPoint("TOPRIGHT"); border.right:SetPoint("BOTTOMRIGHT"); border.right:SetColorTexture(0, 0, 0, a)
    border.innerHighlight = border:CreateTexture(nil, "OVERLAY", nil, 2)
    border.innerHighlight:SetHeight(3)
    border.innerHighlight:SetPoint("TOPLEFT", border.top, "BOTTOMLEFT", 1, 0)
    border.innerHighlight:SetPoint("TOPRIGHT", border.top, "BOTTOMRIGHT", -1, 0)
    border.innerHighlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    border.innerHighlight:SetGradient("VERTICAL",
        CreateColor(1, 1, 1, 0),
        CreateColor(1, 1, 1, 0.07))
    border.innerShadow = border:CreateTexture(nil, "OVERLAY", nil, 2)
    border.innerShadow:SetHeight(3)
    border.innerShadow:SetPoint("BOTTOMLEFT", border.bottom, "TOPLEFT", 1, 0)
    border.innerShadow:SetPoint("BOTTOMRIGHT", border.bottom, "TOPRIGHT", -1, 0)
    border.innerShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    border.innerShadow:SetGradient("VERTICAL",
        CreateColor(0, 0, 0, 0.25),
        CreateColor(0, 0, 0, 0))
    return border
end

local function EnsureCornerCut(frame, size, r, g, b, a)
    if not frame then return end
    local cut = math.max(1, math.floor(size or 2))
    local corners = frame._muiCornerCut
    if not corners then
        corners = {}
        corners.tl = frame:CreateTexture(nil, "OVERLAY")
        corners.tr = frame:CreateTexture(nil, "OVERLAY")
        corners.bl = frame:CreateTexture(nil, "OVERLAY")
        corners.br = frame:CreateTexture(nil, "OVERLAY")
        corners.tl:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
        corners.tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
        corners.bl:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
        corners.br:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
        frame._muiCornerCut = corners
    end
    corners.tl:SetSize(cut, cut)
    corners.tr:SetSize(cut, cut)
    corners.bl:SetSize(cut, cut)
    corners.br:SetSize(cut, cut)
    corners.tl:SetColorTexture(r, g, b, a)
    corners.tr:SetColorTexture(r, g, b, a)
    corners.bl:SetColorTexture(r, g, b, a)
    corners.br:SetColorTexture(r, g, b, a)
end

local function EnsureCastBarGlowHalo(target, outset, parentOverride)
    if not target then return nil end
    if target._muiGlowHalo then return target._muiGlowHalo end
    local pad = outset or 3
    local parent = parentOverride or target
    local halo = CreateFrame("Frame", nil, parent)
    halo:SetPoint("TOPLEFT", target, "TOPLEFT", -pad, pad)
    halo:SetPoint("BOTTOMRIGHT", target, "BOTTOMRIGHT", pad, -pad)
    if halo.SetFrameStrata and parent.GetFrameStrata then
        halo:SetFrameStrata(parent:GetFrameStrata() or "MEDIUM")
    end
    if halo.SetFrameLevel and parent.GetFrameLevel then
        local lvl = parent:GetFrameLevel() or 1
        halo:SetFrameLevel(math.max(0, lvl - 1))
    end
    halo.layer1 = halo:CreateTexture(nil, "BACKGROUND", nil, -5)
    halo.layer1:SetAllPoints(halo)
    halo.layer1:SetColorTexture(0, 0, 0, 0.15)
    halo.layer2 = halo:CreateTexture(nil, "BACKGROUND", nil, -4)
    halo.layer2:SetPoint("TOPLEFT", halo, "TOPLEFT", 1, -1)
    halo.layer2:SetPoint("BOTTOMRIGHT", halo, "BOTTOMRIGHT", -1, 1)
    halo.layer2:SetColorTexture(0, 0, 0, 0.25)
    target._muiGlowHalo = halo
    return halo
end

local function EnsureCastBarSheen(castBar)
    if not castBar then return nil end
    if castBar._muiSheen then return castBar._muiSheen end
    local sheen = castBar:CreateTexture(nil, "OVERLAY", nil, 7)
    sheen:SetPoint("TOPLEFT", castBar, "TOPLEFT", 1, -1)
    sheen:SetPoint("TOPRIGHT", castBar, "TOPRIGHT", -1, -1)
    sheen:SetHeight(2)
    sheen:SetTexture("Interface\\Buttons\\WHITE8X8")
    sheen:SetGradient("VERTICAL",
        CreateColor(1, 1, 1, 0),
        CreateColor(1, 1, 1, 0.22))
    castBar._muiSheen = sheen
    return sheen
end

local function SetBorderColor(border, r, g, b, a)
    if not border then return end
    local ok = pcall(_SetBorderColorInner, border, r, g, b, a)
    if not ok then return end
end

local function SetBorderThickness(border, t)
    if not border or not t then return end
    local ok = pcall(_SetBorderThicknessInner, border, t)
    if not ok then return end
end

-- ============================================================
-- SECTION: Faction + Reaction + Threat Colors
-- FACTION_BORDER_COLORS: Alliance blue / Horde red border tints.
-- GetFactionToken: UnitFactionGroup with pcall, handles realm variant.
-- GetPlayerFactionBorderColor: returns border RGBA for Alliance/Horde.
-- ApplyHealthBorderStyle: applies border color to the health bar border
--   based on faction, reaction (hostile/neutral/friendly), or class.
-- UpdateTargetGlowPulse: OnUpdate driver for the pulsing target glow
--   ring animation (alpha sine wave over time).
-- Clamp01: utility clamping helper for animation values.
-- ENEMY_PLAYER_NAMEPLATE_CVARS: CVar list required for enemy player
--   nameplates; written by EnsureEnemyPlayerNameplatesEnabled.
-- UpdateNameplateDispelIndicator: shows/hides the dispel pulse icon.
-- ShouldShowFriendlyNameplate/ShouldShowNameplate: visibility logic.
-- Various numeric + color utility helpers (ColorGradient,
--   NormalizeThreatPercent, etc.) used by the threat subsystem.
-- READS:  MidnightUISettings.Nameplates, UnitFactionGroup, UnitReaction
-- WRITES: border frame colours (no SavedVar writes)
-- FUNCTIONS:
--   GetFactionToken(unit), GetPlayerFactionBorderColor(unit)
--   ApplyHealthBorderStyle(entry, unit)
--   UpdateTargetGlowPulse(pulseGlow, elapsed)
--   EnsureEnemyPlayerNameplatesEnabled()
--   ShouldShowFriendlyNameplate(unit), ShouldShowNameplate(unit)
--   ColorGradient(...), NormalizeThreatPercent(...), and ~15 helpers
-- ============================================================
local FACTION_BORDER_COLORS = {
    Alliance = { 0.20, 0.45, 1.00, 1.0 },
    Horde = { 0.95, 0.15, 0.15, 1.0 },
}

local function GetFactionToken(unit)
    if not unit then return nil end
    local okFaction, factionA, factionB = pcall(UnitFactionGroup, unit)
    if okFaction then
        if FACTION_BORDER_COLORS[factionA] then return factionA end
        if FACTION_BORDER_COLORS[factionB] then return factionB end
    end

    local okPlayer, isPlayer = pcall(UnitIsPlayer, unit)
    if not okPlayer or not isPlayer then return nil end

    local okPlayerFaction, playerFactionA, playerFactionB = pcall(UnitFactionGroup, "player")
    if not okPlayerFaction then return nil end
    local playerFaction = FACTION_BORDER_COLORS[playerFactionA] and playerFactionA or playerFactionB
    if not FACTION_BORDER_COLORS[playerFaction] then return nil end

    local okFriend, isFriend = pcall(UnitIsFriend, "player", unit)
    if okFriend and isFriend then
        return playerFaction
    end
    if playerFaction == "Horde" then return "Alliance" end
    if playerFaction == "Alliance" then return "Horde" end
    return nil
end

local function GetPlayerFactionBorderColor(unit)
    if not unit then return nil end
    local okPlayer, isPlayer = pcall(UnitIsPlayer, unit)
    if not okPlayer or not isPlayer then return nil end
    local faction = GetFactionToken(unit)
    if not faction then return nil end
    return FACTION_BORDER_COLORS[faction]
end

local function ApplyHealthBorderStyle(entry, unit)
    if not entry or not entry.healthBorder then return end
    local showFactionBorder = true
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    if settings and settings.showFactionBorder == false then
        showFactionBorder = false
    end
    local factionColor = showFactionBorder and GetPlayerFactionBorderColor(unit) or nil
    if factionColor then
        SetBorderColor(entry.healthBorder, factionColor[1], factionColor[2], factionColor[3], factionColor[4])
    else
        SetBorderColor(entry.healthBorder, 0, 0, 0, config.borderAlpha)
    end
    SetBorderThickness(entry.healthBorder, 1)
end

local function UpdateTargetGlowPulse(pulseGlow, elapsed)
    if not pulseGlow or not pulseGlow._muiAnchor then return end
    local duration = 1.65
    local maxExpand = 8
    local fadeIn = 0.12
    local t = (pulseGlow._muiPulseT or 0) + (elapsed or 0)
    if t > duration then
        t = t - duration
    end
    pulseGlow._muiPulseT = t
    local p = t / duration
    local expand = maxExpand * p
    local alpha
    if p < fadeIn then
        alpha = 0.62 * (p / fadeIn)
    else
        alpha = 0.62 * (1 - (p - fadeIn) / (1 - fadeIn))
    end
    pulseGlow:ClearAllPoints()
    pulseGlow:SetPoint("TOPLEFT", pulseGlow._muiAnchor, "TOPLEFT", -expand, expand)
    pulseGlow:SetPoint("BOTTOMRIGHT", pulseGlow._muiAnchor, "BOTTOMRIGHT", expand, -expand)
    pulseGlow:SetAlpha(alpha)
end

UpdateTargetGlow = function(entry, isTargetUnit)
    if not entry then return end
    local baseGlow = entry.targetGlowBase
    local pulseGlow = entry.targetGlowPulse
    local settings = MidnightUISettings and MidnightUISettings.Nameplates
    local targetBorderEnabled = not (settings and settings.targetBorder == false)
    local targetPulseEnabled = not (settings and settings.targetPulse == false)
    if not baseGlow and not pulseGlow then return end
    if isTargetUnit then
        if baseGlow and targetBorderEnabled then
            baseGlow:SetAlpha(1.0)
            baseGlow:Show()
        elseif baseGlow then
            baseGlow:SetAlpha(0)
            baseGlow:Hide()
        end
        if pulseGlow and targetPulseEnabled then
            pulseGlow:Show()
            if not pulseGlow._muiPulseActive then
                pulseGlow._muiPulseActive = true
                pulseGlow._muiPulseT = 0
                pulseGlow:SetScript("OnUpdate", UpdateTargetGlowPulse)
            end
        elseif pulseGlow then
            pulseGlow._muiPulseActive = false
            pulseGlow:SetScript("OnUpdate", nil)
            pulseGlow._muiPulseT = 0
            pulseGlow:SetAlpha(0)
            if pulseGlow._muiAnchor then
                pulseGlow:ClearAllPoints()
                pulseGlow:SetPoint("TOPLEFT", pulseGlow._muiAnchor, "TOPLEFT", 0, 0)
                pulseGlow:SetPoint("BOTTOMRIGHT", pulseGlow._muiAnchor, "BOTTOMRIGHT", 0, 0)
            end
            pulseGlow:Hide()
        end
    else
        if pulseGlow then
            pulseGlow._muiPulseActive = false
            pulseGlow:SetScript("OnUpdate", nil)
            pulseGlow._muiPulseT = 0
            pulseGlow:SetAlpha(0)
            if pulseGlow._muiAnchor then
                pulseGlow:ClearAllPoints()
                pulseGlow:SetPoint("TOPLEFT", pulseGlow._muiAnchor, "TOPLEFT", 0, 0)
                pulseGlow:SetPoint("BOTTOMRIGHT", pulseGlow._muiAnchor, "BOTTOMRIGHT", 0, 0)
            end
            pulseGlow:Hide()
        end
        if baseGlow then
            baseGlow:SetAlpha(0)
            baseGlow:Hide()
        end
    end
end

local function Clamp01(value)
    if not value then return 0 end
    if value < 0 then return 0 end
    if value > 1 then return 1 end
    return value
end

local ENEMY_PLAYER_NAMEPLATE_CVARS = {
    "nameplateShowEnemies",
    "nameplateShowEnemyPlayers",
}

local function UpdateNameplateDispelIndicator(entry, unit)
    if not entry or not entry.dispelIndicator then return end
    entry.dispelIndicator:Hide()
    if entry.dispelIndicator.glowAnim then entry.dispelIndicator.glowAnim:Stop() end
    entry.dispelIndicator._muiReasonKey = "disabled"
    entry.dispelIndicator._muiLastKey = nil
end


local function GetCVarBoolSafe(name)
    local ok, value = pcall(GetCVarBool, name)
    if ok and value ~= nil then return value end
    local v = GetCVar(name)
    if v == "1" then return true end
    if v == "0" then return false end
    return nil
end

local function EnsureEnemyPlayerNameplatesEnabled()
    if not SetCVar then return end
    for _, cvar in ipairs(ENEMY_PLAYER_NAMEPLATE_CVARS) do
        local current = GetCVarBoolSafe(cvar)
        if current ~= true then
            pcall(SetCVar, cvar, "1")
        end
    end
end

local function ShouldShowFriendlyNameplate(unit)
    if not UnitIsFriend("player", unit) then return true end

    local showFriends = GetCVarBoolSafe("nameplateShowFriends")
    if UnitIsPlayer(unit) then
        if showFriends == nil then return true end
        return showFriends
    end

    local showNPCs = GetCVarBoolSafe("nameplateShowFriendlyNPCs")
    local showPets = GetCVarBoolSafe("nameplateShowFriendlyPets")
    local showGuardians = GetCVarBoolSafe("nameplateShowFriendlyGuardians")
    local showMinions = GetCVarBoolSafe("nameplateShowFriendlyMinions")
    local showTotems = GetCVarBoolSafe("nameplateShowFriendlyTotems")

    local anyDefined = (showNPCs ~= nil) or (showPets ~= nil) or (showGuardians ~= nil)
        or (showMinions ~= nil) or (showTotems ~= nil)
    if anyDefined then
        return (showNPCs or showPets or showGuardians or showMinions or showTotems) and true or false
    end
    if showFriends ~= nil then return showFriends end
    return true
end

local function ShouldShowNameplate(unit)
    local showAll = GetCVarBoolSafe("nameplateShowAll")
    if showAll == false then return false end

    if UnitIsFriend("player", unit) then
        return ShouldShowFriendlyNameplate(unit)
    end

    local showEnemies = GetCVarBoolSafe("nameplateShowEnemies")
    if showEnemies == false then return false end
    return true
end

local function SafeGetSpellName(spellId)
    if spellId == nil then return nil end
    if C_Spell and C_Spell.GetSpellInfo then
        local info = C_Spell.GetSpellInfo(spellId)
        return info and info.name or nil
    end
    if GetSpellInfo then
        local ok, name = pcall(GetSpellInfo, spellId)
        if ok then return name end
    end
    return nil
end

local function IsSecretValue(val)
    if type(issecretvalue) ~= "function" then return false end
    local ok, res = pcall(issecretvalue, val)
    return ok and res == true
end

local function SanitizeCacheKey(value)
    if value == nil or IsSecretValue(value) then return nil end
    return value
end

local function GetSafeUnitGuid(unit)
    return SanitizeCacheKey(UnitGUID(unit))
end

local function NormalizeThreatStatus(value)
    if value == nil or IsSecretValue(value) then return nil end
    local n = tonumber(value)
    if not n then return nil end
    return n
end

local function IsStrictTrue(value)
    if IsSecretValue(value) then
        return false
    end
    local ok, result = pcall(_TestEqualsTrue, value)
    return ok and result == true
end

local function ColorGradient(perc, r1, g1, b1, r2, g2, b2, r3, g3, b3)
    if perc <= 0 then return r1, g1, b1 end
    if perc >= 1 then return r3, g3, b3 end
    if perc < 0.5 then
        local rel = perc * 2
        return r1 + (r2 - r1) * rel, g1 + (g2 - g1) * rel, b1 + (b2 - b1) * rel
    end
    local rel = (perc - 0.5) * 2
    return r2 + (r3 - r2) * rel, g2 + (g3 - g2) * rel, b2 + (b3 - b2) * rel
end

local function SafePercent(threatPct)
    local ok, result = pcall(_CalcClampedPercent, threatPct)
    if not ok or not result then return 0 end
    return result
end

local function NormalizeThreatPercent(scaledPct, rawPct, isTanking, status, fallbackStatus)
    local threatPct = NormalizeThreatStatus(scaledPct)
    local threatStatus = NormalizeThreatStatus(status)
    local tankingState = IsStrictTrue(isTanking)
    if threatStatus == nil then
        threatStatus = NormalizeThreatStatus(fallbackStatus)
    end
    local rawThreatPct = NormalizeThreatStatus(rawPct)
    if threatPct == nil and rawThreatPct ~= nil then
        if rawThreatPct > 100 then
            threatPct = (rawThreatPct / 255) * 100
        else
            threatPct = rawThreatPct
        end
    end
    if threatPct == nil then
        if tankingState then
            threatPct = 100
        elseif threatStatus then
            threatPct = threatStatus * 33
        else
            threatPct = 0
        end
    end
    if threatPct <= 1 then threatPct = threatPct * 100 end
    if threatPct > 100 then threatPct = 100 end
    return threatPct
end

local function IsUnitTargetingPlayer(unit)
    if not unit then return false end
    local unitTarget = unit .. "target"
    
    local okExists, exists = pcall(UnitExists, unitTarget)
    if not okExists or not exists then return false end

    local okTarget, isTargeting = pcall(_TestUnitIsUnit, unitTarget, "player")

    if not okTarget then return false end

    return isTargeting
end

local function IsTargetTargetingPlayer()
    if UnitExists("targettarget") and UnitIsUnit("targettarget", "player") then
        return true
    end
    return false
end

local function GetThreatDataForUnit(unit)
    local isTanking, status, scaledPct, rawPct = UnitDetailedThreatSituation("player", unit)
    local simpleStatus = UnitThreatSituation("player", unit)

    if UnitExists("target") then
        local isTargetUnit = false
        
        local okIsUnit, isUnitResult = pcall(UnitIsUnit, unit, "target")
        if okIsUnit and isUnitResult then
            isTargetUnit = true
        else
            local guid = UnitGUID(unit)
            local targetGuid = UnitGUID("target")
            if guid and targetGuid then
                local okGuid, guidMatch = pcall(_SafeEquals, guid, targetGuid)
                if okGuid and guidMatch then
                    isTargetUnit = true
                end
            end
        end

        if isTargetUnit then
            local tIsTanking, tStatus, tScaledPct, tRawPct = UnitDetailedThreatSituation("player", "target")
            local tSimpleStatus = UnitThreatSituation("player", "target")
            
            if (isTanking == nil and status == nil and scaledPct == nil and rawPct == nil and simpleStatus == nil) then
                isTanking, status, scaledPct, rawPct, simpleStatus = tIsTanking, tStatus, tScaledPct, tRawPct, tSimpleStatus
            elseif (tIsTanking ~= nil or tStatus ~= nil or tScaledPct ~= nil or tRawPct ~= nil or tSimpleStatus ~= nil) then
                isTanking = tIsTanking
                status = tStatus
                scaledPct = tScaledPct
                rawPct = tRawPct
                simpleStatus = tSimpleStatus
            end
        end
    end

    return isTanking, status, scaledPct, rawPct, simpleStatus, unit
end

local function HideElement(element)
    if not element then return end
    if element.SetAlpha then element:SetAlpha(0) end
    if element.Hide then element:Hide() end
    if element.SetShown then element:SetShown(false) end
end

-- ============================================================
-- SECTION: Blizzard Nameplate Suppression
-- SuppressDefault() is the most critical function in this file.
-- It hides every Blizzard nameplate sub-element (health bar,
-- name, borders, threat glow, deselected overlay, etc.) and
-- hooks unitFrame.SetAlpha to re-apply alpha=0.01 any time
-- Blizzard tries to show the frame. _muiSuppressed flag prevents
-- re-running on the same frame.
-- WidgetContainer (Blizzard-managed buff/boss icons) is RESCUED:
-- reparented to the plate root with a raised frame level so it
-- remains visible above our custom overlay.
-- HideElement(): safe Hide+SetShown(false) wrapper used by Suppress.
-- READS:  unitFrame sub-elements, plate root
-- WRITES: unitFrame._muiSuppressed, alpha hooks
-- FUNCTIONS:
--   HideElement(element)             — safe hide wrapper
--   SuppressDefault(unitFrame)       — suppress Blizzard nameplate UI
-- ============================================================
local function SuppressDefault(unitFrame)
    if not unitFrame then return end
    if unitFrame._muiSuppressed then return end
    unitFrame._muiSuppressed = true

    if unitFrame.SetAlpha then unitFrame:SetAlpha(0.01) end

    local elements = {
        unitFrame.border, unitFrame.Border,
        unitFrame.selectionHighlight, unitFrame.SelectionHighlight,
        unitFrame.threatGlow, unitFrame.ThreatGlow,
        unitFrame.deselectedOverlay, unitFrame.DeselectedOverlay,
        unitFrame.deselectedHighlight, unitFrame.DeselectedHighlight,
        unitFrame.HealthBarsContainer,
        unitFrame.healthBarsContainer, unitFrame.HealthBarsContainer,
        unitFrame.HealthBarsContainerFrame, unitFrame.HealthBarContainer,
        unitFrame.name, unitFrame.Name, unitFrame.nameText,
        unitFrame.healthBar, unitFrame.HealthBar, unitFrame.healthbar,
        unitFrame.healthBarBorder, unitFrame.HealthBarBorder,
    }
    for _, element in ipairs(elements) do
        HideElement(element)
    end

    if not unitFrame._muiAlphaHooked and hooksecurefunc then
        unitFrame._muiAlphaHooked = true
        hooksecurefunc(unitFrame, "SetAlpha", function(self, a)
            if self._muiAlphaGuard then return end
            if self._muiSuppressed and a ~= 0.01 then
                self._muiAlphaGuard = true
                self:SetAlpha(0.01)
                self._muiAlphaGuard = false
            end
        end)
    end
    if not unitFrame._muiShowHooked then
        unitFrame._muiShowHooked = true
        unitFrame:HookScript("OnShow", function(self)
            if self._muiSuppressed and self.SetAlpha then
                self:SetAlpha(0.01)
            end
        end)
    end

    local plate = unitFrame:GetParent()
    if plate then
        local classFrame = unitFrame.ClassificationFrame or unitFrame.classificationFrame
        if classFrame then
            classFrame:SetAlpha(0)
            classFrame:Hide()
        end
        local widgetContainer = unitFrame.WidgetContainer or unitFrame.widgetContainer
        if widgetContainer and not widgetContainer._muiRescued then
            widgetContainer._muiRescued = true
            widgetContainer:SetParent(plate)
            widgetContainer:SetAlpha(1)
            widgetContainer:SetFrameLevel((plate:GetFrameLevel() or 0) + 20)
        end
    end
end

do
    local function ReSuppressAfterUpdate(unitFrame)
        if not unitFrame or not unitFrame._muiSuppressed then return end
        if unitFrame.SetAlpha then unitFrame:SetAlpha(0) end
        local elements = {
            unitFrame.border, unitFrame.Border,
            unitFrame.selectionHighlight, unitFrame.SelectionHighlight,
            unitFrame.threatGlow, unitFrame.ThreatGlow,
            unitFrame.deselectedOverlay, unitFrame.DeselectedOverlay,
            unitFrame.deselectedHighlight, unitFrame.DeselectedHighlight,
            unitFrame.HealthBarsContainer,
            unitFrame.healthBarsContainer, unitFrame.HealthBarsContainer,
            unitFrame.HealthBarsContainerFrame, unitFrame.HealthBarContainer,
            unitFrame.name, unitFrame.Name, unitFrame.nameText,
            unitFrame.healthBar, unitFrame.HealthBar, unitFrame.healthbar,
            unitFrame.healthBarBorder, unitFrame.HealthBarBorder,
        }
        for _, element in ipairs(elements) do
            HideElement(element)
        end
    end

    local blizzFuncs = {
        "CompactUnitFrame_UpdateAll",
        "CompactUnitFrame_UpdateName",
        "CompactUnitFrame_UpdateHealthColor",
        "CompactUnitFrame_UpdateHealth",
        "CompactUnitFrame_UpdateHealthBorder",
        "CompactUnitFrame_UpdateSelectionHighlight",
        "CompactUnitFrame_UpdateAggroHighlight",
    }
    for _, funcName in ipairs(blizzFuncs) do
        if type(_G[funcName]) == "function" then
            hooksecurefunc(funcName, function(frame)
                if frame and frame._muiSuppressed then
                    ReSuppressAfterUpdate(frame)
                end
            end)
        end
    end
end

-- ============================================================
-- SECTION: Cast Bar System
-- nameplateCastBarPool: object pool of cast bar frames to avoid
--   creating/destroying frames per nameplate add/remove event.
-- AcquireNameplateCastBar/ReleaseNameplateCastBar: pool management.
-- StyleNameplateCastBar: creates or reuses a cast bar from the pool,
--   parents it to the nameplate entry, and calls into MidnightUI_CastBars
--   for style application.
-- _G.MidnightUI_ApplyNameplateCastStyleToBar: exported for external
--   callers (e.g. targeting frame cast bar).
-- Cast glow animations: EnsureNameplateCastGlow creates frame-level
--   animated overlays. PlayNameplateCastGlow runs them.
--   PlayNameplateCastSuccessPulse fires on UNIT_SPELLCAST_SUCCEEDED.
--   SetNameplateCastInterruptedState applies red border + glows on
--   UNIT_SPELLCAST_INTERRUPTED.
-- UpdateNameplateCastBarState: updates cast bar visibility, label,
--   interruptibility, and colour based on the unit's current spell.
-- MarkNameplateCastFailed: plays failure animation.
-- EnsureNameplateCastTextReady: lazily creates cast name/time text.
-- READS:  MidnightUI_CastBars (external module), nameplateCastBarPool
-- WRITES: nameplateCastBarPool, entry.castBar fields
-- FUNCTIONS:
--   AcquireNameplateCastBar(parent), ReleaseNameplateCastBar(cb)
--   StyleNameplateCastBar(entry, unit)
--   EnsureNameplateCastGlow(entry, key, r, g, b, alpha)
--   ClearNameplateCastGlow(entry, key)
--   PlayNameplateCastGlow(entry, key, r, g, b, alpha, duration, expand)
--   SetNameplateCastInterruptedState(entry, active)
--   PlayNameplateCastSuccessPulse(entry)
--   EnsureNameplateCastTextReady(entry, unit)
--   UpdateNameplateCastBarState(entry, unit)
--   MarkNameplateCastFailed(entry, interrupterName)
-- ============================================================
local nameplateCastBarPool = {}

local function AcquireNameplateCastBar(parent)
    local CB = _G.MidnightUI_CastBars
    if not CB or not CB.CreateCastBar then return nil end
    local cb = table.remove(nameplateCastBarPool)
    if not cb then
        cb = CB.CreateCastBar(nil, parent)
    else
        cb.container:SetParent(parent)
    end
    return cb
end

local function ReleaseNameplateCastBar(cb)
    if not cb then return end
    local CB = _G.MidnightUI_CastBars
    if CB and CB.HideCastBar then CB.HideCastBar(cb) end
    cb.container:SetParent(UIParent)
    cb.container:Hide()
    table.insert(nameplateCastBarPool, cb)
end

local function StyleNameplateCastBar(entry, unit)
    if not entry or not entry.unitFrame or not entry.root then return end

    local cbSettings = MidnightUISettings and MidnightUISettings.CastBars
    local nativeCB = entry.unitFrame.castBar or entry.unitFrame.CastBar

    if cbSettings and cbSettings.nameplate and cbSettings.nameplate.enabled == false then
        if entry.customCastBar then
            ReleaseNameplateCastBar(entry.customCastBar)
            entry.customCastBar = nil
        end
        if nativeCB and not (nativeCB.IsForbidden and nativeCB:IsForbidden()) then
            pcall(function() nativeCB:SetAlpha(1) end)
        end
        return
    end

    if nativeCB and not (nativeCB.IsForbidden and nativeCB:IsForbidden()) then
        if not nativeCB._muiHooked then
            nativeCB._muiHooked = true
            pcall(function()
                nativeCB:HookScript("OnShow", function(self)
                    local s = MidnightUISettings and MidnightUISettings.CastBars and MidnightUISettings.CastBars.nameplate
                    if not (s and s.enabled == false) then self:Hide() end
                end)
            end)
        end
        pcall(function() nativeCB:SetAlpha(0); nativeCB:Hide() end)
    end

    if not entry.customCastBar then
        entry.customCastBar = AcquireNameplateCastBar(entry.root)
        if not entry.customCastBar then return end
    end

    local cb = entry.customCastBar
    cb.unit = unit or entry.unit

    local isTargetUnit = IsTargetUnit(unit or entry.unit)
    local castWidth, castHeight = GetNameplateCastConfig(isTargetUnit)
    local CB = _G.MidnightUI_CastBars
    if CB and CB.LayoutCastBar then
        CB.LayoutCastBar(cb, castWidth, castHeight)
    end

    cb.container:ClearAllPoints()
    cb.container:SetPoint("BOTTOM", entry.root, "TOP", 0, config.spacing + 2)
    if cb.container.SetIgnoreParentScale then
        cb.container:SetIgnoreParentScale(true)
        cb.container:SetScale(1)
    end
    cb.container:SetFrameLevel(entry.root:GetFrameLevel() + 6)
    cb.container:SetFrameStrata(entry.root:GetFrameStrata() or "MEDIUM")

    entry.castBar = cb.container
end

function _G.MidnightUI_ApplyNameplateCastStyleToBar(bar, isTargetUnit)
end

local function BoostMarkerColor(r, g, b)
    if not r then return 1, 1, 1 end
    return r, g, b
end

local function ApplyMarkerAtlas(tex, atlas, fallbackTexture, rotation)
    if not tex then return end
    if tex.SetAtlas then
        tex:SetAtlas(atlas, true)
    end
    if not tex:GetTexture() then
        tex:SetTexture(fallbackTexture)
    end
    if tex.SetRotation then
        tex:SetRotation(rotation or 0)
    end
end

local function GetPartyMarkerClassColor(unit)
    local _, class = UnitClass(unit)
    if _G.MidnightUI_Core and _G.MidnightUI_Core.ClassColorsExact and class and _G.MidnightUI_Core.ClassColorsExact[class] then
        local c = _G.MidnightUI_Core.ClassColorsExact[class]
        return c.r, c.g, c.b, true
    end
    if _G.MidnightUI_Core and _G.MidnightUI_Core.GetClassColor then
        return _G.MidnightUI_Core.GetClassColor(unit), false
    end
    return GetUnitClassColor(unit), false
end

local INTERRUPTIBLE_CAST_BORDER = { 0, 0, 0, 0.7 }
local UNINTERRUPTIBLE_CAST_BORDER = { 0, 0, 0, 0.7 }
local DEFAULT_CAST_BORDER = { 0, 0, 0, 0.7 }
local INTERRUPTED_CAST_BORDER = { 0, 0, 0, 0.7 }
local NAMEPLATE_CAST_SUCCESS_ALPHA = 0.42
local NAMEPLATE_CAST_SUCCESS_DURATION = 0.48
local NAMEPLATE_CAST_SUCCESS_EXPAND = 10
local NAMEPLATE_CAST_INTERRUPT_ALPHA = 0.55
local NAMEPLATE_CAST_INTERRUPT_DURATION = 0.24

local function SetNameplateCastBorderColor(entry, color)
    if not entry or not entry.castBar then return end

    local border = entry.castBar.border
    local borderApplied = false
    if border then
        SetBorderColor(border, color[1], color[2], color[3], color[4])
        borderApplied = true
    end

    local iconBorder = entry.castBar.iconBorderBorder or entry.castBar.iconBorder
    local iconBorderApplied = false
    if iconBorder then
        SetBorderColor(iconBorder, color[1], color[2], color[3], color[4])
        iconBorderApplied = true
    end

end

local function SetNameplateCastBorderState(entry, isInterruptible)
    local color = isInterruptible and INTERRUPTIBLE_CAST_BORDER or UNINTERRUPTIBLE_CAST_BORDER
    SetNameplateCastBorderColor(entry, color)
end

local function EnsureNameplateCastGlow(entry, key, r, g, b, alpha)
    if not entry or not entry.castBar or not entry.root then return nil end
    local overlay = entry[key]
    if overlay then return overlay end

    overlay = CreateFrame("Frame", nil, entry.root)
    overlay.tex = overlay:CreateTexture(nil, "ARTWORK")
    overlay.tex:SetAllPoints()
    overlay.tex:SetTexture("Interface\\Buttons\\WHITE8X8")
    overlay.tex:SetBlendMode("ADD")
    overlay.tex:SetVertexColor(r, g, b, alpha or 1)
    overlay:SetAlpha(0)
    overlay:Hide()
    overlay._muiAnchor = entry.castBar
    if overlay.SetFrameLevel and entry.castBar and entry.castBar.GetFrameLevel then
        overlay:SetFrameLevel(entry.castBar:GetFrameLevel() + 1)
    end
    entry[key] = overlay
    return overlay
end

local function ClearNameplateCastGlow(entry, key)
    local overlay = entry and entry[key]
    if not overlay then return end
    overlay._muiPulseActive = false
    overlay._muiPulseTicker = 0
    overlay:SetScript("OnUpdate", nil)
    overlay:SetAlpha(0)
    overlay:Hide()
    if overlay._muiAnchor then
        overlay:ClearAllPoints()
        overlay:SetPoint("TOPLEFT", overlay._muiAnchor, "TOPLEFT", -2, 2)
        overlay:SetPoint("BOTTOMRIGHT", overlay._muiAnchor, "BOTTOMRIGHT", 2, -2)
    end
end

local function PlayNameplateCastGlow(entry, key, r, g, b, alpha, duration, expand)
    local overlay = EnsureNameplateCastGlow(entry, key, r, g, b, alpha)
    if not overlay then return end

    overlay.tex:SetVertexColor(r, g, b, 1)
    overlay._muiPulseActive = true
    overlay._muiPulseTicker = 0
    overlay._muiPulseLoggedMid = false
    overlay:ClearAllPoints()
    overlay:SetPoint("TOPLEFT", overlay._muiAnchor, "TOPLEFT", -2, 2)
    overlay:SetPoint("BOTTOMRIGHT", overlay._muiAnchor, "BOTTOMRIGHT", 2, -2)
    overlay:SetAlpha(alpha)
    overlay:Show()
    overlay:SetScript("OnUpdate", function(self, elapsed)
        if not self._muiPulseActive or not self._muiAnchor then
            self:SetScript("OnUpdate", nil)
            return
        end

        local t = (self._muiPulseTicker or 0) + (elapsed or 0)
        self._muiPulseTicker = t
        local p = t / duration
        if p >= 1 then
            self._muiPulseActive = false
            self._muiPulseTicker = 0
            self:SetAlpha(0)
            self:Hide()
            self:ClearAllPoints()
            self:SetPoint("TOPLEFT", self._muiAnchor, "TOPLEFT", -2, 2)
            self:SetPoint("BOTTOMRIGHT", self._muiAnchor, "BOTTOMRIGHT", 2, -2)
            self:SetScript("OnUpdate", nil)
            return
        end

        local easedExpand = 1 - ((1 - p) * (1 - p))
        local easedFade = (1 - p) * (1 - p)
        local currentExpand = expand * easedExpand
        self:ClearAllPoints()
        self:SetPoint("TOPLEFT", self._muiAnchor, "TOPLEFT", -2 - currentExpand, 2 + currentExpand)
        self:SetPoint("BOTTOMRIGHT", self._muiAnchor, "BOTTOMRIGHT", 2 + currentExpand, -2 - currentExpand)
        self:SetAlpha(alpha * easedFade)
    end)
end

local function SetNameplateCastInterruptedState(entry, active)
    if not entry or not entry.castBar then return end
    entry.castBar._midnightInterruptedVisualActive = active and true or false

    if active then
        SetNameplateCastBorderColor(entry, INTERRUPTED_CAST_BORDER)
        ClearNameplateCastGlow(entry, "castSuccessGlow")
        PlayNameplateCastGlow(
            entry,
            "castInterruptGlow",
            INTERRUPTED_CAST_BORDER[1],
            INTERRUPTED_CAST_BORDER[2],
            INTERRUPTED_CAST_BORDER[3],
            NAMEPLATE_CAST_INTERRUPT_ALPHA,
            NAMEPLATE_CAST_INTERRUPT_DURATION,
            6
        )
        return
    end

    ClearNameplateCastGlow(entry, "castInterruptGlow")
end

local function PlayNameplateCastSuccessPulse(entry)
    if not entry or not entry.castBar then return end
    entry.castBar._midnightInterruptedVisualActive = false
    ClearNameplateCastGlow(entry, "castInterruptGlow")
    local wasUninterruptible = entry.castBar._muiShieldIsShown == true
        or entry.castBar._midnightLastCastUninterruptible == true
    local holdColor = wasUninterruptible and UNINTERRUPTIBLE_CAST_BORDER or INTERRUPTIBLE_CAST_BORDER
    entry.castBar._midnightSuccessVisualActive = true
    SetNameplateCastBorderColor(entry, holdColor)
    if C_Timer and C_Timer.After then
        local castBar = entry.castBar
        C_Timer.After(NAMEPLATE_CAST_SUCCESS_DURATION or 0.5, function()
            if castBar then castBar._midnightSuccessVisualActive = false end
        end)
    end
    ClearNameplateCastGlow(entry, "castSuccessGlow")
end

local function EnsureNameplateCastTextReady(entry, unit)
    if not entry or not entry.castBar then return nil end
    local text = entry.castBar.Text or entry.castBar.text or entry.castBar.Name or entry.castBar.SpellName or entry.castBar.spellName or entry.castBar._midnightText
    if not text then return nil end

    local isTargetUnit = IsTargetUnit(unit or entry.unit)
    local castWidth, _, _, castFontSize = GetNameplateCastConfig(isTargetUnit)
    entry.castBar._midnightText = text
    text:ClearAllPoints()
    text:SetPoint("CENTER", entry.castBar, "CENTER", 0, 0)
    text:SetWidth(math.max(1, castWidth - 10))
    ApplyNameplateFont(text, "Fonts\\FRIZQT__.TTF", castFontSize, "OUTLINE", "CastText")
    text:SetJustifyH("CENTER")
    if text.SetJustifyV then text:SetJustifyV("MIDDLE") end
    text:SetTextColor(1, 1, 1, 1)
    text:SetShadowOffset(1, -1)
    text:SetShadowColor(0, 0, 0, 1)
    if text.SetWordWrap then text:SetWordWrap(false) end
    if text.SetMaxLines then text:SetMaxLines(1) end
    text:SetAlpha(1)
    if text.Show then text:Show() end
    return text
end

local function UpdateNameplateCastBarState(entry, unit)
    if not entry or not entry.castBar or not unit then return end
    if entry.castBar._midnightForceFailedOnce then
        SetNameplateCastInterruptedState(entry, true)
        entry.castBar:SetMinMaxValues(0, 1)
        entry.castBar:SetValue(1)
        local failedText = EnsureNameplateCastTextReady(entry, unit)
        if entry.castBar._midnightFailText and failedText then
            failedText:SetText(entry.castBar._midnightFailText)
        end
        entry.castBar._midnightForceFailedOnce = false
        return
    end

    local name = entry.castBar._muiCastActive and "cast" or nil
    if not name then
        if entry.castBar._midnightInterruptedVisualActive and entry.castBar.IsShown and entry.castBar:IsShown() then
            SetNameplateCastBorderColor(entry, INTERRUPTED_CAST_BORDER)
            return
        end
        if entry.castBar._midnightSuccessVisualActive and entry.castBar.IsShown and entry.castBar:IsShown() then
            local holdColor = entry.castBar._midnightLastCastUninterruptible and UNINTERRUPTIBLE_CAST_BORDER or INTERRUPTIBLE_CAST_BORDER
            SetNameplateCastBorderColor(entry, holdColor)
            return
        end
        entry.castBar._midnightInterruptedVisualActive = false
        ClearNameplateCastGlow(entry, "castInterruptGlow")
        SetNameplateCastBorderState(entry, false)
        EnsureNameplateCastTextReady(entry, unit)
        return
    end

    entry.castBar._midnightInterruptedVisualActive = false
    ClearNameplateCastGlow(entry, "castInterruptGlow")
    local isUninterruptible = entry.castBar._muiShieldIsShown == true
    entry.castBar._midnightLastCastUninterruptible = isUninterruptible
    SetNameplateCastBorderState(entry, not isUninterruptible)
    EnsureNameplateCastTextReady(entry, unit)
end

local function MarkNameplateCastFailed(entry, interrupterName)
    if not entry or not entry.castBar then return end
    entry.castBar._midnightForceFailedOnce = true
    entry.castBar._midnightFailText = "Interrupted"
    SetNameplateCastInterruptedState(entry, true)
    entry.castBar:SetMinMaxValues(0, 1)
    entry.castBar:SetValue(1)

end

-- ============================================================
-- SECTION: Health Display + Threat Logic
-- TrimTextToWidth: truncates a FontString's text with ellipsis
--   when it exceeds a pixel width (used for long unit names).
-- GetBarPercent: reads StatusBar value as %-of-max.
-- AllowSecretHealthPercent/NameplateHealthPctCache: in-combat secret
--   health values are cached by GUID. If UnitHealth is blocked,
--   the last known % is shown instead of 0.
-- GetDisplayHealthPercent: resolves health % with cache fallback.
-- IsPlayerTank/UseTankThreatColors/GroupHasTank: role detection for
--   switching between player-perspective and tank-perspective threat.
-- GetGroupRelativeThreatState: determines if player is top threat,
--   close to top, or safe relative to the raid's threat list.
-- GetReactionColor/GetHealthBarColor/GetThreatBarColor: RGB resolvers
--   for the three bar types.
-- UpdateThreatMarker: positions the threat indicator line at % position
--   on the threat bar.
-- GetPartyUnits/GetPartyMemberThreatPercent: enumerate group members
--   and fetch their individual threat percentages.
-- UpdatePartyThreatMarkers: renders per-member threat icons below
--   the nameplate when in raid combat.
-- GetTankUnitsInRaid/GetTankThreatPercent: tank threat for the
--   tank-perspective display overlay.
-- READS:  MidnightUISettings.Nameplates, MidnightUISettings.General,
--         NameplateHealthPctCache, UnitThreatSituation, UnitGroupRolesAssigned
-- WRITES: NameplateHealthPctCache (health % by GUID)
-- FUNCTIONS: ~20 helpers (listed in section above)
-- ============================================================
local function TrimTextToWidth(fs, text, maxWidth)
    if not fs then return end
    if not text then fs:SetText(""); return end
    
    fs:SetText(text)

    local function SafeCheckWidth()
        local w = fs:GetStringWidth()
        if type(issecretvalue) == "function" then
            if issecretvalue(w) or issecretvalue(maxWidth) then
                return true
            end
        end
        if w <= maxWidth then return true end
        return false
    end

    local ok, fits = pcall(SafeCheckWidth)

    if not ok then return end

    if fits then return end

    local len = #text
    while len > 0 do
        local candidate = string.sub(text, 1, len) .. "..."
        fs:SetText(candidate)
        
        local loopOk, loopFits = pcall(SafeCheckWidth)
        
        if not loopOk then return end
        if loopFits then return end
        
        len = len - 1
    end
    fs:SetText("")
end

local function GetBarPercent(bar)
    if not bar then return nil end
    local okMinMax, minVal, maxVal = pcall(bar.GetMinMaxValues, bar)
    local okVal, curVal = pcall(bar.GetValue, bar)
    if not okMinMax or not okVal or maxVal == nil then return nil end
    if type(maxVal) ~= "number" or type(curVal) ~= "number" then return nil end
    if maxVal <= 0 then return nil end
    if type(issecretvalue) == "function" then
        local okSecretMax, isSecretMax = pcall(issecretvalue, maxVal)
        local okSecretCur, isSecretCur = pcall(issecretvalue, curVal)
        if (okSecretMax and isSecretMax) or (okSecretCur and isSecretCur) then
            return nil
        end
    end
    local okPct, pct = pcall(_CalcHealthPct, curVal, maxVal)
    if not okPct then return nil end
    return pct
end

local function AllowSecretHealthPercent()
    if _G.MidnightUI_ForceHideHealthPct then return false end
    return MidnightUISettings and MidnightUISettings.General and MidnightUISettings.General.allowSecretHealthPercent == true
end

local lastAllowSecretState = nil
local NameplateHealthPctCache = {}

_G.MidnightUI_GetNameplateHealthPct = function(guid)
    guid = SanitizeCacheKey(guid)
    if not guid then return nil end
    return NameplateHealthPctCache[guid]
end

local function GetDisplayHealthPercent(unit, bar)
    if not unit then return nil end
    local allowSecret = AllowSecretHealthPercent()
    if not allowSecret then
        return nil
    end
    if UnitHealthPercent then
        local scaleTo100 = CurveConstants and CurveConstants.ScaleTo100 or nil
        local ok, pct = pcall(UnitHealthPercent, unit, true, scaleTo100)
        if ok and pct ~= nil then
            if allowSecret or not IsSecretValue(pct) then
                return pct
            end
        end
    end
    return GetBarPercent(bar)
end

local function IsPlayerTank()
    local role = UnitGroupRolesAssigned("player")
    if role == "TANK" then return true end
    if GetSpecializationRole and GetSpecialization then
        local specIndex = GetSpecialization()
        local specRole = specIndex and GetSpecializationRole(specIndex) or nil
        if specRole == "TANK" then return true end
    end
    return false
end


local function GroupHasTank()
    if IsPlayerTank() then return true end
    if IsInRaid() then
        for i = 1, GetNumGroupMembers() do
            local unit = "raid" .. i
            if UnitExists(unit) and UnitGroupRolesAssigned(unit) == "TANK" then
                return true
            end
        end
    elseif IsInGroup() then
        for i = 1, GetNumSubgroupMembers() do
            local unit = "party" .. i
            if UnitExists(unit) and UnitGroupRolesAssigned(unit) == "TANK" then
                return true
            end
        end
    end
    return false
end

local function UseTankThreatColors()
    if IsPlayerTank() then return true end
    if not IsInGroup() and not IsInRaid() then return true end
    if IsInGroup() and not IsInRaid() and GetNumGroupMembers() <= 1 then return true end
    if not GroupHasTank() then return true end
    return false
end

local function ClearGroupThreatCache()
    for k in pairs(groupThreatCache) do
        groupThreatCache[k] = nil
    end
end

local function GetGroupRelativeThreatState(unit, playerThreatPct)
    if (not IsInGroup() and not IsInRaid()) then return nil end
    if IsPlayerTank() then return nil end
    if GroupHasTank() then return nil end
    if not playerThreatPct or playerThreatPct <= 0 then return nil end

    local guid = GetSafeUnitGuid(unit)
    local now = GetTime()
    if guid then
        local cached = groupThreatCache[guid]
        if cached and (now - cached.time) <= GROUP_THREAT_CACHE_TTL then
            return cached.state
        end
    end

    local topPct = playerThreatPct

    if IsInRaid() then
        for i = 1, GetNumGroupMembers() do
            local member = "raid" .. i
            if not UnitIsUnit(member, "player") and UnitExists(member) then
                local isTanking, mStatus, scaledPct, rawPct = UnitDetailedThreatSituation(member, unit)
                if not (isTanking == nil and mStatus == nil and scaledPct == nil and rawPct == nil) then
                    local pct = NormalizeThreatPercent(scaledPct, rawPct, isTanking, mStatus)
                    if pct and pct > topPct then
                        topPct = pct
                    end
                end
            end
        end
    elseif IsInGroup() then
        for i = 1, GetNumSubgroupMembers() do
            local member = "party" .. i
            if UnitExists(member) then
                local isTanking, mStatus, scaledPct, rawPct = UnitDetailedThreatSituation(member, unit)
                if not (isTanking == nil and mStatus == nil and scaledPct == nil and rawPct == nil) then
                    local pct = NormalizeThreatPercent(scaledPct, rawPct, isTanking, mStatus)
                    if pct and pct > topPct then
                        topPct = pct
                    end
                end
            end
        end
    end

    if topPct <= 0 then return nil end

    local ratio = playerThreatPct / topPct
    local state
    if ratio >= 1 then
        state = "top"
    elseif ratio >= 0.8 then
        state = "close"
    else
        state = "low"
    end

    if guid then
        groupThreatCache[guid] = { time = now, state = state }
    end

    return state
end

local function GetReactionColor(unit)
    if UnitIsTapDenied(unit) then
        return THREAT_HEALTH_COLORS.gray
    end
    if UnitIsPlayer(unit) and UnitIsFriend("player", unit) then
        local r, g, b = GetUnitClassColor(unit)
        return { r, g, b }
    end
    local reaction = UnitReaction(unit, "player")
    if reaction == 4 then
        return THREAT_HEALTH_COLORS.yellow
    end
    if reaction and reaction >= 5 then
        return THREAT_HEALTH_COLORS.green
    end
    return THREAT_HEALTH_COLORS.red
end

local function GetHealthBarColor(unit)
    if not unit or IsSecretValue(unit) or not UnitExists(unit) then return nil end

    local isPlayerTank = UseTankThreatColors()
    local inCombat = UnitAffectingCombat(unit)
    local isTanking, status = UnitDetailedThreatSituation("player", unit)
    local simpleStatus = UnitThreatSituation("player", unit)
    local threatStatus = NormalizeThreatStatus(status)
    if threatStatus == nil then
        threatStatus = NormalizeThreatStatus(simpleStatus)
    end

    if inCombat and threatStatus ~= nil then
        if isPlayerTank then
            if threatStatus >= 3 then
                return THREAT_HEALTH_COLORS.green
            elseif threatStatus == 2 then
                return THREAT_HEALTH_COLORS.yellow
            end
            return THREAT_HEALTH_COLORS.red
        else
            if threatStatus >= 3 then
                return THREAT_HEALTH_COLORS.red
            elseif threatStatus == 2 then
                return THREAT_HEALTH_COLORS.yellow
            end
            return THREAT_HEALTH_COLORS.green
        end
    end

    return GetReactionColor(unit)
end

local function GetThreatBarColor(threatPct, isTank)
    local perc = SafePercent(threatPct)
    if isTank then
        return ColorGradient(
            perc,
            THREAT_BAR_COLORS.red[1], THREAT_BAR_COLORS.red[2], THREAT_BAR_COLORS.red[3],
            THREAT_BAR_COLORS.yellow[1], THREAT_BAR_COLORS.yellow[2], THREAT_BAR_COLORS.yellow[3],
            THREAT_BAR_COLORS.green[1], THREAT_BAR_COLORS.green[2], THREAT_BAR_COLORS.green[3]
        )
    end
    return ColorGradient(
        perc,
        THREAT_BAR_COLORS.green[1], THREAT_BAR_COLORS.green[2], THREAT_BAR_COLORS.green[3],
        THREAT_BAR_COLORS.yellow[1], THREAT_BAR_COLORS.yellow[2], THREAT_BAR_COLORS.yellow[3],
        THREAT_BAR_COLORS.red[1], THREAT_BAR_COLORS.red[2], THREAT_BAR_COLORS.red[3]
    )
end

local function UpdateThreatMarker(marker, container, pct, glow)
    if not marker or not container or not pct then
        if container then container:Hide() end
        return
    end
    local width = container:GetWidth()
    if not width or width <= 0 then
        if container then container:Hide() end
        return
    end
    local clamped = math.max(0, math.min(100, pct))
    local offset = (width - 4) * (clamped / 100)
    container:Show()
    marker:ClearAllPoints()
    marker:SetPoint("LEFT", container, "LEFT", offset, 0)
    marker:SetPoint("TOP")
    marker:SetPoint("BOTTOM")
    if glow then
        glow:ClearAllPoints()
        glow:SetPoint("LEFT", container, "LEFT", offset - 3, 0)
        glow:SetPoint("TOP")
        glow:SetPoint("BOTTOM")
    end
end

local function GetPartyUnits()
    local units = {}
    units[#units + 1] = "player"
    for i = 1, 4 do
        local unit = "party" .. i
        if UnitExists(unit) then
            units[#units + 1] = unit
        end
    end
    return units
end

local function GetPartyMemberThreatPercent(memberUnit, targetUnit)
    if not memberUnit or not UnitExists(memberUnit) then return nil end
    if not targetUnit or not UnitExists(targetUnit) then return nil end

    local isTanking, status, scaledPct, rawPct
    local tier = "none"

    if UnitExists("target") then
        local isTarget = false
        pcall(function() isTarget = UnitIsUnit(targetUnit, "target") end)
        if isTarget then
            local ok2
            ok2, isTanking, status, scaledPct, rawPct = pcall(UnitDetailedThreatSituation, memberUnit, "target")
            if ok2 and scaledPct ~= nil and not IsSecretValue(scaledPct) then
                tier = "detailed_target"
            else
                isTanking, status, scaledPct, rawPct = nil, nil, nil, nil
            end
        end
    end

    if tier == "none" then
        local ok, t, s, sp, rp = pcall(UnitDetailedThreatSituation, memberUnit, targetUnit)
        if ok and sp ~= nil and not IsSecretValue(sp) then
            isTanking, status, scaledPct, rawPct = t, s, sp, rp
            tier = "detailed_nameplate"
        end
    end

    local pct = nil
    if tier ~= "none" then
        pct = NormalizeThreatPercent(scaledPct, rawPct, isTanking, status, nil)
        if pct == nil then
            tier = "none"
        end
    end

    if pct == nil then
        local okSimple, simple = pcall(UnitThreatSituation, memberUnit, targetUnit)
        if okSimple and simple ~= nil and not IsSecretValue(simple) then
            local n = tonumber(simple)
            if n then
                tier = "simple"
                local statusMap = { [0] = 8, [1] = 40, [2] = 72, [3] = 95 }
                pct = statusMap[n] or 0
            end
        end
    end

    if pct == nil then
        tier = "role"
    end

    return pct
end

local function UpdatePartyThreatMarkers(entry, unit, threatWidth, threatEnabled, engagedCombat)
    if not entry or not entry.partyThreatMarkerContainer or not entry.partyThreatMarkers then return end
    local inInstance, instanceType = IsInInstance()
    if not inInstance or instanceType ~= "party" then
        entry.partyThreatMarkerContainer:Hide()
        for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
        return
    end
    if not threatEnabled or not IsInGroup() or IsInRaid() then
        entry.partyThreatMarkerContainer:Hide()
        for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
        return
    end
    if not engagedCombat then
        local unitCombat = UnitAffectingCombat(unit)
        local playerCombat = UnitAffectingCombat("player")
        if not unitCombat and not playerCombat then
            entry.partyThreatMarkerContainer:Hide()
            for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
            return
        end
    end
    if not unit or not UnitExists(unit) then
        entry.partyThreatMarkerContainer:Hide()
        for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
        return
    end
    if UnitIsFriend("player", unit) or not UnitCanAttack("player", unit) then
        entry.partyThreatMarkerContainer:Hide()
        for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
        return
    end
    if not threatWidth or threatWidth <= 0 then
        entry.partyThreatMarkerContainer:Hide()
        for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
        return
    end

    entry.partyThreatMarkerContainer:SetSize(threatWidth, PARTY_THREAT_MARKER_SIZE)
    entry.partyThreatMarkerContainer:Show()

    local members = GetPartyUnits()
    local width = threatWidth
    local maxMarkers = #entry.partyThreatMarkers

    for i = 1, maxMarkers do
        local marker = entry.partyThreatMarkers[i]
        local memberUnit = members[i]
        if marker and memberUnit then
            local pct = GetPartyMemberThreatPercent(memberUnit, unit)
            if pct == nil and UnitGroupRolesAssigned(memberUnit) == "TANK"
               and UnitAffectingCombat(unit) then
                pct = 100
            end
            if pct then
                local clamped = math.max(0, math.min(100, pct))
                local offset = (width - PARTY_THREAT_MARKER_SIZE) * (clamped / 100)
                marker:ClearAllPoints()
                marker:SetPoint("LEFT", entry.partyThreatMarkerContainer, "LEFT", offset, 0)
                marker:SetPoint("BOTTOM", entry.partyThreatMarkerContainer, "BOTTOM", 0, 0)
                local useTankIcon = UnitGroupRolesAssigned(memberUnit) == "TANK"
                if useTankIcon then
                    ApplyMarkerAtlas(marker.shadow, PARTY_THREAT_TANK_ATLAS, PARTY_THREAT_MARKER_TEXTURE, PARTY_THREAT_TANK_ROTATION)
                    ApplyMarkerAtlas(marker.tex, PARTY_THREAT_TANK_ATLAS, PARTY_THREAT_MARKER_TEXTURE, PARTY_THREAT_TANK_ROTATION)
                else
                    ApplyMarkerAtlas(marker.shadow, marker.defaultAtlas, PARTY_THREAT_MARKER_TEXTURE, marker.defaultRotation)
                    ApplyMarkerAtlas(marker.tex, marker.defaultAtlas, PARTY_THREAT_MARKER_TEXTURE, marker.defaultRotation)
                end
                local r, g, b, isExact = GetPartyMarkerClassColor(memberUnit)
                if not isExact then
                    r, g, b = BoostMarkerColor(r, g, b)
                end
                if marker.tex then
                    marker.tex:SetVertexColor(r, g, b, 1)
                end
                marker:Show()
            else
                marker:Hide()
            end
        elseif marker then
            marker:Hide()
        end
    end
end

local function GetTankUnitsInRaid()
    if not IsInRaid() then return nil, nil end
    local tanks = {}
    for i = 1, GetNumGroupMembers() do
        local unit = "raid" .. i
        if UnitExists(unit) and UnitGroupRolesAssigned(unit) == "TANK" then
            tanks[#tanks + 1] = unit
        end
    end
    if IsPlayerTank() then
        local other = nil
        for _, unit in ipairs(tanks) do
            if not UnitIsUnit(unit, "player") then
                other = unit
                break
            end
        end
        return "player", other
    end
    return tanks[1], tanks[2]
end

local function GetTankThreatPercent(tankUnit, targetUnit)
    if not tankUnit or not UnitExists(tankUnit) then return nil end
    local threatUnit = targetUnit
    if UnitIsUnit(targetUnit, "target") then
        threatUnit = "target"
    end
    local isTanking, status, scaledPct, rawPct = UnitDetailedThreatSituation(tankUnit, threatUnit)
    return NormalizeThreatPercent(scaledPct, rawPct, isTanking, status)
end

local function AdjustTankThreatColors(tank1, tank2, r1, g1, b1, r2, g2, b2)
    if not tank1 or not tank2 then return r1, g1, b1, r2, g2, b2 end
    local _, class1 = UnitClass(tank1)
    local _, class2 = UnitClass(tank2)
    if class1 and class2 and class1 == class2 then
        local t1IsPlayer = UnitIsUnit(tank1, "player")
        local t2IsPlayer = UnitIsUnit(tank2, "player")
        if t1IsPlayer and not t2IsPlayer then
            r1, g1, b1 = r1 * 1.15, g1 * 1.15, b1 * 1.15
            r2, g2, b2 = r2 * 0.7, g2 * 0.7, b2 * 0.7
        elseif t2IsPlayer and not t1IsPlayer then
            r2, g2, b2 = r2 * 1.15, g2 * 1.15, b2 * 1.15
            r1, g1, b1 = r1 * 0.7, g1 * 0.7, b1 * 0.7
        else
            r2, g2, b2 = r2 * 0.7, g2 * 0.7, b2 * 0.7
        end
    end
    return r1, g1, b1, r2, g2, b2
end

local function GetNameplate(unit)
    if not C_NamePlate or not C_NamePlate.GetNamePlateForUnit then return nil end
    return C_NamePlate.GetNamePlateForUnit(unit)
end

-- ============================================================
-- ============================================================
-- SECTION: Per-Nameplate Debuff Row (Personal Promotion)
-- ============================================================
local NP_DEBUFF_COLORS = {
    Magic   = {r = 0.20, g = 0.60, b = 1.00},
    Curse   = {r = 0.60, g = 0.00, b = 1.00},
    Disease = {r = 0.60, g = 0.40, b = 0.00},
    Poison  = {r = 0.00, g = 0.60, b = 0.00},
    none    = {r = 0.80, g = 0.00, b = 0.00},
}

local function NP_CreateDebuffButton(parent)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(20, 20)
    btn:EnableMouse(false)
    btn.icon = btn:CreateTexture(nil, "BACKGROUND")
    btn.icon:SetAllPoints()
    btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    btn.cd = CreateFrame("Cooldown", nil, btn, "CooldownFrameTemplate")
    btn.cd:SetAllPoints()
    btn.cd:SetHideCountdownNumbers(false)
    btn.border = CreateFrame("Frame", nil, btn, "BackdropTemplate")
    btn.border:SetAllPoints()
    btn.border:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1})
    btn.border:SetBackdropBorderColor(0, 0, 0, 1)
    btn.personalGlow = btn:CreateTexture(nil, "OVERLAY")
    btn.personalGlow:SetPoint("TOPLEFT", btn, "TOPLEFT", -2, 2)
    btn.personalGlow:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 2, -2)
    btn.personalGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
    btn.personalGlow:SetBlendMode("ADD")
    btn.personalGlow:SetVertexColor(1, 0.92, 0.4, 0.25)
    btn.personalGlow:Hide()
    btn.count = btn:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
    btn.count:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 0)
    return btn
end

local function NP_UpdateDebuffs(entry, unit)
    if not entry or not entry.root or not unit then return end
    if not UnitExists(unit) or not UnitCanAttack("player", unit) then
        if entry.debuffRow then entry.debuffRow:Hide() end
        return
    end

    if not entry.debuffRow then
        entry.debuffRow = CreateFrame("Frame", nil, entry.root)
        entry.debuffRow:SetSize(1, 20)
        entry.debuffRow:SetFrameLevel((entry.root:GetFrameLevel() or 0) + 3)
        entry.debuffButtons = {}
    end
    entry.debuffRow:ClearAllPoints()
    local anchorTarget, anchorPoint, yOffset = entry.root, "TOP", 6
    if entry.castBar and entry.castBar.IsShown and entry.castBar:IsShown() then
        anchorTarget = entry.castBar
        anchorPoint  = "TOP"
        yOffset      = 4
    end
    entry.debuffRow:SetPoint("BOTTOM", anchorTarget, anchorPoint, 0, yOffset)

    local promo = (MidnightUISettings and MidnightUISettings.TargetFrame and MidnightUISettings.TargetFrame.personalPromotion) or {}
    local promoEnabled = promo.enabled ~= false

    local mineIIDs = {}
    if promoEnabled and C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
        for i = 1, 20 do
            local a = C_UnitAuras.GetAuraDataByIndex(unit, i, "HARMFUL|PLAYER")
            if not a or not a.name then break end
            if a.auraInstanceID then mineIIDs[a.auraInstanceID] = true end
        end
    end

    local entries = {}
    if C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
        for i = 1, 20 do
            if #entries >= 6 then break end
            local a = C_UnitAuras.GetAuraDataByIndex(unit, i, "HARMFUL")
            if not a or not a.name then break end
            entries[#entries + 1] = {
                icon = a.icon, count = a.applications, debuffType = a.dispelName,
                duration = a.duration, expirationTime = a.expirationTime,
                iid = a.auraInstanceID, spellId = a.spellId,
                mine = (a.auraInstanceID and mineIIDs[a.auraInstanceID]) and true or false,
            }
        end
    end

    local restrictedNow = false
    if C_Secrets and C_Secrets.ShouldAurasBeSecret then
        local ok, v = pcall(C_Secrets.ShouldAurasBeSecret)
        if ok and v then restrictedNow = true end
    end

    if promoEnabled and promo.specFilter and not restrictedNow then
        for _, e in ipairs(entries) do
            if e.mine then
                local sid = e.spellId
                if sid and not (issecretvalue and issecretvalue(sid)) then
                    if IsPlayerSpell and not IsPlayerSpell(sid) then
                        e.mine = false
                    end
                end
            end
        end
    end
    if promoEnabled and promo.sortExpiring and not restrictedNow and not (InCombatLockdown and InCombatLockdown()) then
        pcall(function()
            table.sort(entries, function(a, b)
                if a.mine ~= b.mine then return a.mine end
                return (a.expirationTime or 0) < (b.expirationTime or 0)
            end)
        end)
    end

    if #entries == 0 then
        entry.debuffRow:Hide()
        for _, b in ipairs(entry.debuffButtons or {}) do b:Hide() end
        return
    end

    entry.debuffRow:Show()
    local size, spacing = 20, 4
    local totalW = #entries * (size + spacing) - spacing
    entry.debuffRow:SetWidth(math.max(1, totalW))

    for idx, e in ipairs(entries) do
        if not entry.debuffButtons[idx] then
            entry.debuffButtons[idx] = NP_CreateDebuffButton(entry.debuffRow)
        end
        local btn = entry.debuffButtons[idx]
        btn:Show()
        btn.icon:SetTexture(e.icon)
        if btn.count then
            local cnt = e.count
            local cntSecret = issecretvalue and cnt and issecretvalue(cnt)
            if cntSecret then
                btn.count:SetText("")
            elseif cnt and cnt > 1 then
                btn.count:SetText(cnt)
            else
                btn.count:SetText("")
            end
        end
        local dur, exp = e.duration, e.expirationTime
        local durSecret = issecretvalue and dur and issecretvalue(dur)
        local expSecret = issecretvalue and exp and issecretvalue(exp)
        if not durSecret and not expSecret and dur and exp and dur > 0 and exp > 0 then
            btn.cd:SetCooldown(exp - dur, dur)
        else
            btn.cd:Clear()
        end

        local tkey = e.debuffType
        if issecretvalue and tkey and issecretvalue(tkey) then tkey = nil end
        if not tkey then tkey = "none" end
        local c = NP_DEBUFF_COLORS[tkey] or NP_DEBUFF_COLORS.none
        if e.mine then
            btn.border:SetBackdropBorderColor(0, 0, 0, 0)
            btn.personalGlow:Show()
            btn.personalGlow:SetAlpha(0.25)
            btn:SetAlpha(1.0)
            if btn.icon.SetDesaturated then btn.icon:SetDesaturated(false) end
        else
            btn.border:SetBackdropBorderColor(c.r, c.g, c.b, 0.55)
            btn.personalGlow:Hide()
            if promoEnabled then
                btn:SetAlpha(tonumber(promo.othersAlpha) or 0.50)
                if btn.icon.SetDesaturated then btn.icon:SetDesaturated(promo.desaturateOthers == true) end
            else
                btn:SetAlpha(1.0)
                if btn.icon.SetDesaturated then btn.icon:SetDesaturated(false) end
            end
        end

        btn:ClearAllPoints()
        btn:SetPoint("LEFT", entry.debuffRow, "LEFT", (idx - 1) * (size + spacing), 0)
    end
    for i = #entries + 1, #entry.debuffButtons do
        entry.debuffButtons[i]:Hide()
    end
end

-- SECTION: Nameplate Factory — StyleNameplate
-- StyleNameplate(unit) is the frame constructor for a single plate.
-- Called once on NAME_PLATE_UNIT_ADDED (and re-used if entry already
-- exists for the unit). Builds the full hierarchy:
--   1. plate.root — transparent anchor Frame parented to the Blizzard
--        plate (NOT unitFrame), SetParent required for layout engine
--   2. SuppressDefault(unitFrame) — hides all Blizzard elements
--   3. healthBar — StatusBar with border, background fill
--   4. nameContainer + nameText — left/center-aligned name label
--   5. healthPct — right-aligned health percentage text
--   6. threatBars (x2) — bottom bar for player and tank threat
--   7. threatMarkerLine/threatMarkerGlow — positional threat markers
--   8. partyThreat[] — per-member raid threat icon row
--   9. targetGlow / hoverGlow — animated highlight rings
--  10. hoverSensor — invisible mouse region that drives hover state;
--        propagation flags deferred to pendingHoverPropagate if in combat
--  11. castBar — from AcquireNameplateCastBar pool
--  12. dispelIndicator — animated pulse icon for dispellable buffs
-- Returns the entry table stored in plates[unit].
-- READS:  plates[unit], config, MidnightUISettings.Nameplates
-- WRITES: plates[unit], pendingHoverPropagate (hover sensor deferred)
-- FUNCTIONS:
--   StyleNameplate(unit) → entry table
--   UpdateLockVisual() [upvalue] — lock icon sync (if applicable)
-- ============================================================
local function StyleNameplate(unit)
    if not IsNameplatesEnabled() then return nil end
    if not UnitExists(unit) then return nil end

    local plate = GetNameplate(unit)
    if not plate then return nil end

    local entry = plates[unit]
    if entry and entry.root and entry.root:IsShown() then return entry end

    local unitFrame = plate.UnitFrame or plate.unitFrame
    if unitFrame then SuppressDefault(unitFrame) end

    local root = plate.MidnightUIRoot
      if root and root.MidnightUIEntry then
          root:EnableMouse(false)
          local entry = root.MidnightUIEntry
          entry.plate = plate
          entry.unit = unit
          entry.unitFrame = unitFrame
          plates[unit] = entry
          return entry
      end
    if root and not root.MidnightUIEntry then
        root:Hide()
        root:SetParent(nil)
        plate.MidnightUIRoot = nil
        root = nil
    end
      if not root then
          root = CreateFrame("Frame", nil, plate)
        root:EnableMouse(false)
        root:SetSize(config.width, config.height)
        root:SetScale(GetNameplateScale(IsTargetUnit(unit)))
        root:SetPoint("TOP", plate, "CENTER", 0, 0)
        root:SetFrameLevel((unitFrame and unitFrame:GetFrameLevel() or plate:GetFrameLevel()) + 5)
        plate.MidnightUIRoot = root

          entry = {}
          root.MidnightUIEntry = entry

        local healthBar = CreateFrame("StatusBar", nil, root)
        healthBar:SetAllPoints()
        healthBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
        healthBar:GetStatusBarTexture():SetHorizTile(false)
        healthBar:GetStatusBarTexture():SetVertTile(false)
        healthBar:SetMinMaxValues(0, 1)
        healthBar:SetValue(1)
        local hoverSensor = CreateFrame("Frame", nil, root)
        hoverSensor:SetAllPoints(healthBar)
        hoverSensor:SetFrameLevel(healthBar:GetFrameLevel() + 1)
        hoverSensor:EnableMouse(true)
        if not InCombatLockdown() then
            hoverSensor:SetPropagateMouseClicks(true)
            hoverSensor:SetPropagateMouseMotion(true)
        else
            pendingHoverPropagate[hoverSensor] = true
        end
        hoverSensor:SetScript("OnEnter", function(self)
            local r = self:GetParent()
            if not r then return end
            local e = r.MidnightUIEntry
            if e and e.unit and IsTargetUnit(e.unit) then return end
            local hEnabled, hPulse = GetHoverSettings()
            if not hEnabled then return end
            r._muiHoverActive = true
            r._muiHoverStart = GetTime()
            r._muiUnitHover = true
            unitHoverRoot = r
            if e then
                if e.hoverGlow then
                    e.hoverGlow:Show()
                    e.hoverGlow._muiTargetAlpha = 1
                end
                local hp = e.hoverPulse
                if hPulse and hp and not hp._muiPulseActive then
                    hp._muiPulseActive = true
                    hp._muiPulseT = 0
                    hp:Show()
                    hp:SetScript("OnUpdate", UpdateTargetGlowPulse)
                end
            end
        end)
        hoverSensor:SetScript("OnLeave", function(self)
            local r = self:GetParent()
            if not r then return end
            r._muiHoverActive = false
        end)

        local healthBg = healthBar:CreateTexture(nil, "BACKGROUND")
        healthBg:SetAllPoints()
        healthBg:SetColorTexture(0.05, 0.05, 0.05, 0.6)

        local healthShadow = root:CreateTexture(nil, "BACKGROUND")
        healthShadow:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -1, 1)
        healthShadow:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 1, -1)
        healthShadow:SetColorTexture(0, 0, 0, 0.12)

        local healthBorder = CreateBlackBorder(healthBar, config.borderAlpha)

        local targetGlowBase = CreateBlackBorder(healthBar, 1.0)
        targetGlowBase:SetFrameLevel(healthBar:GetFrameLevel() + 16)
        SetBorderColor(targetGlowBase, 1.00, 0.84, 0.20, 0.85)
        SetBorderThickness(targetGlowBase, 2)
        targetGlowBase:SetAlpha(0)
        targetGlowBase:Hide()

        local targetGlowPulse = CreateFrame("Frame", nil, root)
        targetGlowPulse:SetPoint("TOPLEFT", healthBar, "TOPLEFT", 0, 0)
        targetGlowPulse:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", 0, 0)
        targetGlowPulse:SetFrameLevel(healthBar:GetFrameLevel() + 15)
        local targetGlowPulseBorder = CreateBlackBorder(targetGlowPulse, 1.0)
        SetBorderColor(targetGlowPulseBorder, 1.00, 0.90, 0.35, 0.65)
        SetBorderThickness(targetGlowPulseBorder, 2)
        targetGlowPulse._muiAnchor = healthBar
        targetGlowPulse._muiPulseActive = false
        targetGlowPulse._muiPulseT = 0
        targetGlowPulse:SetAlpha(0)
        targetGlowPulse:Hide()

        local _, _, bs = GetHoverSettings()
        local hbc = config.hoverBorderColor
        local br, bg, bb, ba = hbc[1], hbc[2], hbc[3], hbc[4]

        local hoverGlow = CreateFrame("Frame", nil, root)
        hoverGlow:SetFrameLevel(healthBar:GetFrameLevel() + 14)
        hoverGlow:SetPoint("TOPLEFT", healthBar, "TOPLEFT", -bs, bs)
        hoverGlow:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", bs, -bs)
        hoverGlow:SetAlpha(0)
        hoverGlow:Hide()

        local hbTop = hoverGlow:CreateTexture(nil, "OVERLAY")
        hbTop:SetColorTexture(br, bg, bb, ba)
        hbTop:SetHeight(bs)
        hbTop:SetPoint("TOPLEFT")
        hbTop:SetPoint("TOPRIGHT")

        local hbBottom = hoverGlow:CreateTexture(nil, "OVERLAY")
        hbBottom:SetColorTexture(br, bg, bb, ba)
        hbBottom:SetHeight(bs)
        hbBottom:SetPoint("BOTTOMLEFT")
        hbBottom:SetPoint("BOTTOMRIGHT")

        local hbLeft = hoverGlow:CreateTexture(nil, "OVERLAY")
        hbLeft:SetColorTexture(br, bg, bb, ba)
        hbLeft:SetWidth(bs)
        hbLeft:SetPoint("TOPLEFT", hbTop, "BOTTOMLEFT")
        hbLeft:SetPoint("BOTTOMLEFT", hbBottom, "TOPLEFT")

        local hbRight = hoverGlow:CreateTexture(nil, "OVERLAY")
        hbRight:SetColorTexture(br, bg, bb, ba)
        hbRight:SetWidth(bs)
        hbRight:SetPoint("TOPRIGHT", hbTop, "BOTTOMRIGHT")
        hbRight:SetPoint("BOTTOMRIGHT", hbBottom, "TOPRIGHT")

        local hoverPulse = CreateFrame("Frame", nil, root)
        hoverPulse:SetPoint("TOPLEFT", hoverGlow, "TOPLEFT", 0, 0)
        hoverPulse:SetPoint("BOTTOMRIGHT", hoverGlow, "BOTTOMRIGHT", 0, 0)
        hoverPulse:SetFrameLevel(healthBar:GetFrameLevel() + 13)
        local hpTop = hoverPulse:CreateTexture(nil, "OVERLAY")
        hpTop:SetColorTexture(1, 1, 1, 1)
        hpTop:SetHeight(bs)
        hpTop:SetPoint("TOPLEFT")
        hpTop:SetPoint("TOPRIGHT")
        local hpBottom = hoverPulse:CreateTexture(nil, "OVERLAY")
        hpBottom:SetColorTexture(1, 1, 1, 1)
        hpBottom:SetHeight(bs)
        hpBottom:SetPoint("BOTTOMLEFT")
        hpBottom:SetPoint("BOTTOMRIGHT")
        local hpLeft = hoverPulse:CreateTexture(nil, "OVERLAY")
        hpLeft:SetColorTexture(1, 1, 1, 1)
        hpLeft:SetWidth(bs)
        hpLeft:SetPoint("TOPLEFT", hpTop, "BOTTOMLEFT")
        hpLeft:SetPoint("BOTTOMLEFT", hpBottom, "TOPLEFT")
        local hpRight = hoverPulse:CreateTexture(nil, "OVERLAY")
        hpRight:SetColorTexture(1, 1, 1, 1)
        hpRight:SetWidth(bs)
        hpRight:SetPoint("TOPRIGHT", hpTop, "BOTTOMRIGHT")
        hpRight:SetPoint("BOTTOMRIGHT", hpBottom, "TOPRIGHT")
        hoverPulse._muiAnchor = hoverGlow
        hoverPulse._muiPulseActive = false
        hoverPulse._muiPulseT = 0
        hoverPulse:SetAlpha(0)
        hoverPulse:Hide()

        local nameContainer = CreateFrame("Frame", nil, healthBar)
        nameContainer:SetPoint("TOPLEFT", healthBar, "TOPLEFT", config.namePadding, -1)
        nameContainer:SetPoint("BOTTOMRIGHT", healthBar, "BOTTOMRIGHT", -config.namePadding, 1)
        nameContainer:SetClipsChildren(true)

        local nameText = healthBar:CreateFontString(nil, "OVERLAY")
        ApplyNameplateFont(nameText, "Fonts\\FRIZQT__.TTF", config.nameFontSize, "OUTLINE", "Name")
        nameText:SetParent(nameContainer)
        nameText:SetPoint("LEFT", nameContainer, "LEFT", 0, 0)
        nameText:SetPoint("RIGHT", nameContainer, "RIGHT", 0, 0)
        nameText:SetJustifyH("LEFT")
        nameText:SetWordWrap(false)
        nameText:SetTextColor(1, 1, 1, 1)
        nameText:SetShadowOffset(1, -1)
        nameText:SetShadowColor(0, 0, 0, 1)

        local healthPct = healthBar:CreateFontString(nil, "OVERLAY")
        ApplyNameplateFont(healthPct, "Fonts\\ARIALN.TTF", config.healthPctFontSize, "OUTLINE", "HealthPct")
        healthPct:SetPoint("RIGHT", healthBar, "RIGHT", -6, 0)
        healthPct:SetJustifyH("RIGHT")
        healthPct:SetTextColor(1, 1, 1, 0.9)
        healthPct:SetShadowOffset(1, -1)
        healthPct:SetShadowColor(0, 0, 0, 1)

        nameContainer:ClearAllPoints()
        nameContainer:SetPoint("TOPLEFT", healthBar, "TOPLEFT", config.namePadding, -1)
        nameContainer:SetPoint("BOTTOMLEFT", healthBar, "BOTTOMLEFT", config.namePadding, 1)
        nameContainer:SetPoint("RIGHT", healthPct, "LEFT", -4, 0)

        local dispelIndicator = CreateFrame("Frame", nil, healthBar)
        dispelIndicator:SetSize(18, 18)
        dispelIndicator:SetPoint("BOTTOM", healthBar, "TOP", 0, 6)
        dispelIndicator:SetFrameLevel(healthBar:GetFrameLevel() + 10)
        dispelIndicator:Hide()

        dispelIndicator.icon = dispelIndicator:CreateTexture(nil, "ARTWORK")
        dispelIndicator.icon:SetAllPoints()
        dispelIndicator.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)

        dispelIndicator.border = CreateFrame("Frame", nil, dispelIndicator, "BackdropTemplate")
        dispelIndicator.border:SetAllPoints()
        dispelIndicator.border:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
        dispelIndicator.border:SetBackdropBorderColor(0, 0, 0, 1)

        dispelIndicator.glow = dispelIndicator:CreateTexture(nil, "OVERLAY")
        dispelIndicator.glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
        dispelIndicator.glow:SetBlendMode("ADD")
        dispelIndicator.glow:SetPoint("CENTER")
        dispelIndicator.glow:SetSize(36, 36)
        dispelIndicator.glow:SetAlpha(0)

        dispelIndicator.glowAnim = dispelIndicator.glow:CreateAnimationGroup()
        local glowIn = dispelIndicator.glowAnim:CreateAnimation("Alpha")
        glowIn:SetFromAlpha(0.2); glowIn:SetToAlpha(0.85)
        glowIn:SetDuration(0.6); glowIn:SetSmoothing("IN_OUT")
        local glowOut = dispelIndicator.glowAnim:CreateAnimation("Alpha")
        glowOut:SetFromAlpha(0.85); glowOut:SetToAlpha(0.2)
        glowOut:SetDuration(0.6); glowOut:SetSmoothing("IN_OUT")
        glowOut:SetOrder(2)
        dispelIndicator.glowAnim:SetLooping("REPEAT")

        local threatContainer = CreateFrame("Frame", nil, root)
        threatContainer:SetSize(config.width, config.threatHeight)
        threatContainer:SetPoint("TOP", root, "BOTTOM", 0, -config.spacing)

        local threatBg = threatContainer:CreateTexture(nil, "BACKGROUND")
        threatBg:SetAllPoints()
        threatBg:SetColorTexture(0, 0, 0, 0.5)
        CreateBlackBorder(threatContainer, config.borderAlpha)

        local threatBar = CreateFrame("StatusBar", nil, threatContainer)
        threatBar:SetPoint("TOPLEFT", 1, -1)
        threatBar:SetPoint("BOTTOMRIGHT", -1, 1)
        threatBar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
        threatBar:GetStatusBarTexture():SetHorizTile(false)
        threatBar:GetStatusBarTexture():SetVertTile(false)
        threatBar:SetMinMaxValues(0, 100)
        threatBar:SetValue(0)

        local threatContainer2 = CreateFrame("Frame", nil, root)
        threatContainer2:SetSize(config.width, config.threatHeight)
        threatContainer2:SetPoint("TOP", threatContainer, "BOTTOM", 0, -config.spacing)
        threatContainer2:Hide()

        local threatBg2 = threatContainer2:CreateTexture(nil, "BACKGROUND")
        threatBg2:SetAllPoints()
        threatBg2:SetColorTexture(0, 0, 0, 0.5)
        CreateBlackBorder(threatContainer2, config.borderAlpha)

        local threatBar2 = CreateFrame("StatusBar", nil, threatContainer2)
        threatBar2:SetPoint("TOPLEFT", 1, -1)
        threatBar2:SetPoint("BOTTOMRIGHT", -1, 1)
        threatBar2:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
        threatBar2:GetStatusBarTexture():SetHorizTile(false)
        threatBar2:GetStatusBarTexture():SetVertTile(false)
        threatBar2:SetMinMaxValues(0, 100)
        threatBar2:SetValue(0)

        local threatMarkerStack = CreateFrame("Frame", nil, root)
        threatMarkerStack:SetPoint("TOP", threatContainer, "TOP", 0, 0)
        threatMarkerStack:SetSize(config.width, (config.threatHeight * 2) + config.spacing)
        threatMarkerStack:SetFrameLevel(threatContainer:GetFrameLevel() + 10)
        threatMarkerStack:Hide()

        local threatMarkerLine = threatMarkerStack:CreateTexture(nil, "OVERLAY")
        threatMarkerLine:SetColorTexture(1, 1, 1, 1)
        threatMarkerLine:SetWidth(6)
        threatMarkerLine:SetPoint("TOP")
        threatMarkerLine:SetPoint("BOTTOM")

        local threatMarkerGlow = threatMarkerStack:CreateTexture(nil, "OVERLAY")
        threatMarkerGlow:SetColorTexture(1, 1, 1, 0.5)
        threatMarkerGlow:SetWidth(16)
        threatMarkerGlow:SetPoint("TOP")
        threatMarkerGlow:SetPoint("BOTTOM")

        local partyThreatMarkerContainer = CreateFrame("Frame", nil, root)
        partyThreatMarkerContainer:SetSize(config.width, PARTY_THREAT_MARKER_SIZE)
        partyThreatMarkerContainer:SetPoint("TOP", threatContainer, "BOTTOM", 0, -PARTY_THREAT_MARKER_Y_OFFSET)
        partyThreatMarkerContainer:SetFrameStrata(threatContainer:GetFrameStrata())
        partyThreatMarkerContainer:SetFrameLevel(threatContainer:GetFrameLevel() + 20)
        partyThreatMarkerContainer:Hide()

        local partyThreatMarkers = {}
        for i = 1, 5 do
            local marker = CreateFrame("Frame", nil, partyThreatMarkerContainer)
            marker:SetSize(PARTY_THREAT_MARKER_SIZE, PARTY_THREAT_MARKER_SIZE)
            marker:SetPoint("BOTTOM")
            marker:Hide()

            local shadow = marker:CreateTexture(nil, "OVERLAY")
            ApplyMarkerAtlas(shadow, PARTY_THREAT_MARKER_ATLAS, PARTY_THREAT_MARKER_TEXTURE, PARTY_THREAT_MARKER_ROTATION)
            shadow:SetAllPoints()
            shadow:SetVertexColor(0, 0, 0, 0.6)
            shadow:SetBlendMode("BLEND")

            local tex = marker:CreateTexture(nil, "OVERLAY")
            ApplyMarkerAtlas(tex, PARTY_THREAT_MARKER_ATLAS, PARTY_THREAT_MARKER_TEXTURE, PARTY_THREAT_MARKER_ROTATION)
            tex:SetAllPoints()
            tex:SetBlendMode("BLEND")

            marker.shadow = shadow
            marker.tex = tex
            marker.defaultAtlas = PARTY_THREAT_MARKER_ATLAS
            marker.defaultRotation = PARTY_THREAT_MARKER_ROTATION
            partyThreatMarkers[i] = marker
        end

        entry.plate = plate
        entry.unit = unit
        entry.unitFrame = unitFrame
        entry.root = root
        entry.healthBar = healthBar
        entry.healthBorder = healthBorder
        entry.healthBg = healthBg
        entry.targetGlowBase = targetGlowBase
        entry.targetGlowPulse = targetGlowPulse
        entry.hoverGlow = hoverGlow
        entry.hoverSensor = hoverSensor
        entry.hoverPulse = hoverPulse
        entry.name = nameText
        entry.nameContainer = nameContainer
        entry.healthPct = healthPct
        entry.dispelIndicator = dispelIndicator
        entry.threatBar = threatBar
        entry.threatContainer = threatContainer
        entry.threatBar2 = threatBar2
        entry.threatContainer2 = threatContainer2
        entry.threatMarkerStack = threatMarkerStack
        entry.threatMarkerLine = threatMarkerLine
        entry.threatMarkerGlow = threatMarkerGlow
        entry.partyThreatMarkerContainer = partyThreatMarkerContainer
        entry.partyThreatMarkers = partyThreatMarkers
          
          local function DisableMouseRecursive(frame)
              if frame ~= hoverSensor then
                  frame:EnableMouse(false)
              end
              for _, child in ipairs({ frame:GetChildren() }) do
                  DisableMouseRecursive(child)
              end
          end
          DisableMouseRecursive(root)

          plates[unit] = entry
      end

      if entry then
          ApplyNameplateTextLayout(entry, IsTargetUnit(unit))
          StyleNameplateCastBar(entry, unit)
      end
  
      return entry
  end

-- ============================================================
-- SECTION: Main Update Loop — UpdatePlate
-- UpdatePlate(unit) is called every 0.2s by the ticker for every
-- entry in plates{}, and also directly on threat/health events.
-- Per-call work:
--   1. Guard: IsNameplatesEnabled and UnitExists checks.
--   2. StyleNameplate if entry missing (late attachment).
--   3. Health bar: value, max, color (class/reaction/dead).
--   4. Health % cache update (writes NameplateHealthPctCache[guid]).
--   5. Name text update via TrimTextToWidth.
--   6. Target glow: show/animate on IsTargetUnit.
--   7. Threat data: GetThreatDataForUnit → threat bar fill + color.
--   8. Cast bar: UpdateNameplateCastBarState.
--   9. Dispel indicator: UpdateNameplateDispelIndicator.
--  10. Party threat markers: UpdatePartyThreatMarkers.
--  11. Border style: ApplyHealthBorderStyle.
-- All WoW API calls are pcall-wrapped to avoid taint crashes.
-- READS:  plates[unit], entry fields, MidnightUISettings.Nameplates,
--         NameplateHealthPctCache, threatCache, groupThreatCache
-- WRITES: entry visual frames (SetValue, SetVertexColor, etc.),
--         NameplateHealthPctCache (health % update)
-- FUNCTIONS:
--   UpdatePlate(unit) — main per-plate visual refresh
-- ============================================================
local function UpdatePlate(unit)
    if not UnitExists(unit) then return end

    if not IsNameplatesEnabled() then
        local entry = plates[unit]
        if entry then
            UpdateTargetGlow(entry, false)
            if entry.root then entry.root:Hide() end
        end
        return
    end

    local entry = plates[unit] or StyleNameplate(unit)
    if not entry or not entry.healthBar then return end

    if entry.unitFrame then SuppressDefault(entry.unitFrame) end
    if entry then
        StyleNameplateCastBar(entry, unit)
        UpdateNameplateDispelIndicator(entry, unit)
        pcall(NP_UpdateDebuffs, entry, unit)
    end

    if not ShouldShowNameplate(unit) then
        UpdateTargetGlow(entry, false)
        if entry.root then entry.root:Hide() end
        return
    elseif entry.root and not entry.root:IsShown() then
        entry.root:Show()
    end

    local isTargetUnit = IsTargetUnit(unit)
    local healthWidth, healthHeight, healthAlpha, nameFontSize, healthPctFontSize = GetNameplateHealthConfig(isTargetUnit)
    if entry.root then entry.root:SetSize(healthWidth, healthHeight) end
    if entry.root then entry.root:SetScale(GetNameplateScale(isTargetUnit)) end

    UpdateTargetGlow(entry, isTargetUnit)

    if entry.root then
        local playerInCombat = InCombatLockdown and InCombatLockdown()
        if playerInCombat or isTargetUnit then
            entry.root._muiTargetAlpha = 1
        else
            local isNear = CheckInteractDistance and CheckInteractDistance(unit, 4)
            entry.root._muiTargetAlpha = isNear and config.oocAlphaNear or config.oocAlphaFar
        end
        if not entry.root._muiFadeInstalled then
            entry.root._muiFadeInstalled = true
            entry.root._muiTargetAlpha = entry.root._muiTargetAlpha or 1
            entry.root:HookScript("OnUpdate", RootOnUpdate)
        end
    end

    if entry.healthBar then entry.healthBar:SetAlpha(healthAlpha) end
    if entry.name then ApplyNameplateFont(entry.name, "Fonts\\FRIZQT__.TTF", nameFontSize, "OUTLINE", "Name") end
    if entry.healthPct then ApplyNameplateFont(entry.healthPct, "Fonts\\ARIALN.TTF", healthPctFontSize, "OUTLINE", "HealthPct") end
    ApplyNameplateTextLayout(entry, isTargetUnit)

    local threatEnabled, threatWidth, threatHeight, threatAlpha = GetNameplateThreatConfig(isTargetUnit)
    if entry.threatContainer then
        entry.threatContainer:SetSize(threatWidth, threatHeight)
        entry.threatContainer:SetAlpha(threatAlpha)
    end
    if entry.threatContainer2 then
        entry.threatContainer2:SetSize(threatWidth, threatHeight)
        entry.threatContainer2:SetAlpha(threatAlpha)
    end
    if not threatEnabled then
        if entry.threatBar then entry.threatBar:SetValue(0); entry.threatBar:Hide() end
        if entry.threatContainer then entry.threatContainer:Hide() end
        if entry.threatBar2 then entry.threatBar2:SetValue(0); entry.threatBar2:Hide() end
        if entry.threatContainer2 then entry.threatContainer2:Hide() end
        if entry.partyThreatMarkerContainer then
            entry.partyThreatMarkerContainer:Hide()
            if entry.partyThreatMarkers then
                for _, marker in ipairs(entry.partyThreatMarkers) do marker:Hide() end
            end
        end
    elseif entry.threatContainer then
        entry.threatContainer:Show()
    end

    local function SafeBoolean(func, ...)
        local function runner(...)
            return func(...)
        end

        local ok, result = pcall(runner, ...)

        if ok then
            local checkOk, isTrue = pcall(_TestEqualsTrue, result)

            if checkOk and isTrue then
                return true
            end
        end
        return false
    end

    local name = UnitName(unit) or ""
    if entry.name and entry.nameContainer then
        TrimTextToWidth(entry.name, name, entry.nameContainer:GetWidth())
    end

    local current = UnitHealth(unit)
    local max = UnitHealthMax(unit)
    
    pcall(_UpdateHealthBar, entry.healthBar, max, current)
    
    if entry.healthPct then
        local allowSecret = AllowSecretHealthPercent()
        if lastAllowSecretState == nil or lastAllowSecretState ~= allowSecret then
            lastAllowSecretState = allowSecret
        end
        local pct = GetDisplayHealthPercent(unit, entry.healthBar)
        if UnitGUID then
            local guid = GetSafeUnitGuid(unit)
            local cachePct = nil
            if type(pct) == "number" then
                cachePct = pct
            else
                cachePct = GetBarPercent(entry.healthBar)
            end
            if guid and cachePct and type(cachePct) == "number" and not IsSecretValue(cachePct) then
                NameplateHealthPctCache[guid] = cachePct
            end
        end
        local hasDisplay = false
        local fmt = _G.MidnightUI_FormatHealthText
        if fmt then
            local text = fmt(current, max, pct)
            if text then
                entry.healthPct:SetText(text)
                hasDisplay = true
            end
        end
        if not hasDisplay and pct then
            local ok = pcall(_SetFormattedPct, entry.healthPct, pct)
            if ok then hasDisplay = true end
        end
        if not hasDisplay then
            entry.healthPct:SetText("")
        end
    end

    local color = GetHealthBarColor(unit)
    if color then
        entry.healthBar:SetStatusBarColor(color[1], color[2], color[3], 1.0)
        if entry.healthBg then
            entry.healthBg:SetColorTexture(color[1] * 0.15, color[2] * 0.15, color[3] * 0.15, 0.6)
        end
    end
    ApplyHealthBorderStyle(entry, unit)

    UpdateNameplateDispelIndicator(entry, unit)

    if entry.threatBar and threatEnabled then
        local isTanking, status, scaledPct, rawPct, simpleStatus
        local playerCombat = UnitAffectingCombat("player")
        local unitCombat = UnitAffectingCombat(unit)
        local hasThreatData = false
        local isTargetingPlayer = IsUnitTargetingPlayer(unit)
        local isTargetGuidMatch = false
        
        if UnitExists("target") then
            local guid = UnitGUID(unit)
            local targetGuid = UnitGUID("target")
            if guid and targetGuid then
                local ok, match = pcall(_SafeEquals, guid, targetGuid)
                if ok and match then isTargetGuidMatch = true end
            end
        end

        local isTargetUnit = SafeBoolean(UnitIsUnit, unit, "target") or isTargetGuidMatch
        
        local engagedCombat = unitCombat or isTargetingPlayer or (isTargetUnit and playerCombat)
        local inCombat = (playerCombat or unitCombat)
        
        local unitGuid = GetSafeUnitGuid(unit)
        local guidChanged = true
        if unitGuid and entry.unitGuid then
            local ok, match = pcall(_SafeEquals, entry.unitGuid, unitGuid)
            if ok and match then guidChanged = false end
        elseif unitGuid == nil and entry.unitGuid == nil then
            guidChanged = false
        end

        if guidChanged then
            entry.unitGuid = unitGuid
            entry.noDataSince = nil
        end

        local now = GetTime()
        local cached = nil
        if unitGuid then
            local ok, val = pcall(_GetFromCache, threatCache, unitGuid)
            if ok then cached = val end
        end
        local cacheAge = cached and (now - cached.time) or nil
        local hasThreatDataEffective = false
        local delayActive = false

        if SafeBoolean(UnitIsDeadOrGhost, unit) then
            entry.threatBar:SetValue(0)
            entry.threatBar:Hide()
            if entry.threatBar2 then entry.threatBar2:SetValue(0); entry.threatBar2:Hide() end
            if entry.threatContainer2 then entry.threatContainer2:Hide() end
            entry.noDataSince = nil
        elseif SafeBoolean(UnitIsFriend, "player", unit) or (not SafeBoolean(UnitCanAttack, "player", unit) and not inCombat and not isTargetUnit) then
            entry.threatBar:SetValue(0)
            entry.threatBar:Hide()
            if entry.threatBar2 then entry.threatBar2:SetValue(0); entry.threatBar2:Hide() end
            if entry.threatContainer2 then entry.threatContainer2:Hide() end
            entry.noDataSince = nil
        elseif not engagedCombat then
            local tank1, tank2 = GetTankUnitsInRaid()
            if IsInRaid() and tank1 and isTargetUnit then
                entry.threatBar:Show()
                entry.threatBar:SetValue(1)
                entry.threatBar:SetAlpha(0.35)
                local r1, g1, b1 = GetUnitClassColor(tank1)
                entry.threatBar:SetStatusBarColor(r1, g1, b1, 1.0)
                if entry.threatContainer2 and entry.threatBar2 and tank2 then
                    entry.threatContainer2:Show()
                    entry.threatBar2:Show()
                    entry.threatBar2:SetValue(1)
                    entry.threatBar2:SetAlpha(0.35)
                    local r2, g2, b2 = GetUnitClassColor(tank2)
                    r1, g1, b1, r2, g2, b2 = AdjustTankThreatColors(tank1, tank2, r1, g1, b1, r2, g2, b2)
                    entry.threatBar:SetStatusBarColor(r1, g1, b1, 1.0)
                    entry.threatBar2:SetStatusBarColor(r2, g2, b2, 1.0)
                elseif entry.threatContainer2 and entry.threatBar2 then
                    entry.threatContainer2:Hide()
                    entry.threatBar2:Hide()
                end
                UpdateThreatMarker(entry.threatMarkerLine, entry.threatMarkerStack, nil, entry.threatMarkerGlow, entry.threatMarkerLabel)
            else
                entry.threatBar:SetValue(0)
                entry.threatBar:Hide()
                if entry.threatBar2 then entry.threatBar2:SetValue(0); entry.threatBar2:Hide() end
                if entry.threatContainer2 then entry.threatContainer2:Hide() end
                entry.noDataSince = nil
            end
        else
            isTanking, status, scaledPct, rawPct, simpleStatus = GetThreatDataForUnit(unit)
            hasThreatData = (isTanking ~= nil) or (status ~= nil) or (scaledPct ~= nil) or (rawPct ~= nil) or (simpleStatus ~= nil)
            hasThreatDataEffective = hasThreatData
            if not hasThreatData then
                if not entry.noDataSince then entry.noDataSince = now end
                hasThreatDataEffective = false
            elseif entry.noDataSince then
                if (now - entry.noDataSince) < THREAT_NO_DATA_DELAY then
                    delayActive = true
                    hasThreatDataEffective = false
                else
                    entry.noDataSince = nil
                end
            end
            local tank1, tank2 = GetTankUnitsInRaid()
            if IsInRaid() and tank1 then
                local playerThreatPct = nil
                if UnitIsDeadOrGhost("player") then
                    playerThreatPct = 0
                    entry.lastPlayerThreatPct = 0
                elseif not IsPlayerTank() then
                    playerThreatPct = NormalizeThreatPercent(scaledPct, rawPct, isTanking, status, simpleStatus)
                end
                local source = "live"
                if playerThreatPct ~= nil then
                    entry.lastPlayerThreatPct = playerThreatPct
                else
                    playerThreatPct = entry.lastPlayerThreatPct
                    source = "cache"
                end
                local pct1 = GetTankThreatPercent(tank1, unit)
                entry.threatBar:Show()
                local v1 = (pct1 and pct1 > 0) and pct1 or 1
                local a1 = (pct1 and pct1 > 0) and 1.0 or 0.4
                entry.threatBar:SetValue(v1)
                entry.threatBar:SetAlpha(a1)
                local r1, g1, b1 = GetUnitClassColor(tank1)
                if entry.threatContainer2 and entry.threatBar2 and tank2 then
                    entry.threatContainer2:Show()
                    local pct2 = GetTankThreatPercent(tank2, unit)
                    entry.threatBar2:Show()
                    local v2 = (pct2 and pct2 > 0) and pct2 or 1
                    local a2 = (pct2 and pct2 > 0) and 1.0 or 0.4
                    entry.threatBar2:SetValue(v2)
                    entry.threatBar2:SetAlpha(a2)
                    local r2, g2, b2 = GetUnitClassColor(tank2)
                    r1, g1, b1, r2, g2, b2 = AdjustTankThreatColors(tank1, tank2, r1, g1, b1, r2, g2, b2)
                    entry.threatBar:SetStatusBarColor(r1, g1, b1, 1.0)
                    entry.threatBar2:SetStatusBarColor(r2, g2, b2, 1.0)
                    entry.lastMarkerPct = playerThreatPct
                    UpdateThreatMarker(entry.threatMarkerLine, entry.threatMarkerStack, playerThreatPct, entry.threatMarkerGlow)
                elseif entry.threatContainer2 and entry.threatBar2 then
                    entry.threatContainer2:Hide()
                    entry.threatBar2:Hide()
                    entry.threatBar:SetStatusBarColor(r1, g1, b1, 1.0)
                    entry.lastMarkerPct = playerThreatPct
                    UpdateThreatMarker(entry.threatMarkerLine, entry.threatMarkerStack, playerThreatPct, entry.threatMarkerGlow)
                end

                local highlight = false
                if IsInGroup() and not IsPlayerTank() and (isTargetUnit or isTargetingPlayer) then
                    local threatStatus = NormalizeThreatStatus(status)
                    if threatStatus == nil then
                        threatStatus = NormalizeThreatStatus(simpleStatus)
                    end
                    if threatStatus == nil then
                        local okTs, ts = pcall(UnitThreatSituation, "player", unit)
                        if okTs then threatStatus = NormalizeThreatStatus(ts) end
                    end
                    if IsStrictTrue(isTanking) or (threatStatus and threatStatus >= 3) then
                        highlight = true
                    end
                end
                if entry.healthBorder then
                    local isPlayer = false
                    local okIsPlayer, value = pcall(UnitIsPlayer, unit)
                    if okIsPlayer and value then isPlayer = true end
                    if highlight and not isPlayer then
                        SetBorderColor(entry.healthBorder, 1, 0.85, 0.1, 1)
                        SetBorderThickness(entry.healthBorder, 2)
                    else
                        ApplyHealthBorderStyle(entry, unit)
                    end
                end
        else
            entry.threatBar:Show()
            if entry.threatContainer2 then entry.threatContainer2:Hide() end
            if entry.threatBar2 then entry.threatBar2:Hide() end
            UpdateThreatMarker(entry.threatMarkerLine, entry.threatMarkerStack, nil, entry.threatMarkerGlow)

                ApplyHealthBorderStyle(entry, unit)
                local threatPct = NormalizeThreatPercent(scaledPct, rawPct, isTanking, status, simpleStatus)
                if (not hasThreatData or threatPct == 0) and isTargetingPlayer then
                    threatPct = 100
                end
                if hasThreatData and unitGuid then
                    threatCache[unitGuid] = { pct = threatPct, time = now, status = status, simpleStatus = simpleStatus }
                end
                local cacheUsed = false
                local cacheReason = nil
                local fallbackApplied = false
                if engagedCombat and not SafeBoolean(UnitIsFriend, "player", unit) and not hasThreatDataEffective then
                    local unitTarget = unit .. "target"
                    if UnitExists(unitTarget) then
                        if SafeBoolean(UnitIsUnit, unitTarget, "player") then
                            threatPct = 50
                            cacheReason = delayActive and "DATA_DELAY_TARGET_PLAYER_YELLOW" or "NO_DATA_TARGET_PLAYER_YELLOW"
                        else
                            threatPct = 0
                            cacheReason = delayActive and "DATA_DELAY_TARGET_OTHER_RED" or "NO_DATA_TARGET_OTHER_RED"
                        end
                    else
                        threatPct = 50
                        cacheReason = delayActive and "DATA_DELAY_NO_TARGET_YELLOW" or "NO_DATA_NO_TARGET_YELLOW"
                    end
                    fallbackApplied = true
                end
                entry.threatBar:SetValue(threatPct)
                local r, g, b = GetThreatBarColor(threatPct, UseTankThreatColors())
                entry.threatBar:SetStatusBarColor(r, g, b, 1.0)
                if hasThreatData and not delayActive then
                    local groupState = GetGroupRelativeThreatState(unit, threatPct)
                    if groupState then
                        local hc
                        local tc
                        local useTankColors = UseTankThreatColors()
                        if groupState == "top" then
                            if useTankColors then
                                hc = THREAT_HEALTH_COLORS.green
                                tc = THREAT_BAR_COLORS.green
                            else
                                hc = THREAT_HEALTH_COLORS.red
                                tc = THREAT_BAR_COLORS.red
                            end
                        elseif groupState == "close" then
                            hc = THREAT_HEALTH_COLORS.yellow
                            tc = THREAT_BAR_COLORS.yellow
                        else
                            if useTankColors then
                                hc = THREAT_HEALTH_COLORS.red
                                tc = THREAT_BAR_COLORS.red
                            else
                                hc = THREAT_HEALTH_COLORS.green
                                tc = THREAT_BAR_COLORS.green
                            end
                        end
                        entry.healthBar:SetStatusBarColor(hc[1], hc[2], hc[3], 1.0)
                        entry.threatBar:SetStatusBarColor(tc[1], tc[2], tc[3], 1.0)
                    end
                end
            end
        end
    end

    UpdatePartyThreatMarkers(entry, unit, threatWidth, threatEnabled, engagedCombat)
end

-- ============================================================
-- SECTION: Event Handlers + Public API + Ticker
-- OnNameplateAdded: StyleNameplate → UpdatePlate → ensure ticker.
-- OnNameplateRemoved: cleans entry, clears hover state, resets
--   unitHoverRoot if it was this unit's root.
-- UpdateAllPlates: iterates plates{} and calls UpdatePlate on each.
-- _G.MidnightUI_RefreshNameplates: public API for forced refresh.
-- _G.MidnightUI_GetNameplateCastBar: returns the castBar frame from
--   an entry for external use (e.g. CastBars module).
-- Event registration block: 34 events on NameplateManager.
-- NameplateManager OnEvent: routes each event to the appropriate
--   handler. Spell cast events (UNIT_SPELLCAST_*) are delegated to
--   _G.MidnightUI_CastBars.On*() methods. Threat/role/group events
--   clear caches. PLAYER_REGEN_ENABLED drains pendingHoverPropagate.
-- StartNameplateTicker/StopNameplateTicker: manage the 0.2s C_Timer
--   repeating ticker that drives UpdateAllPlates.
-- READS:  plates, pendingHoverPropagate, MidnightUI_CastBars
-- WRITES: plates (remove on REMOVED), unitHoverRoot
-- FUNCTIONS:
--   OnNameplateAdded(unit), OnNameplateRemoved(unit)
--   UpdateAllPlates()
--   _G.MidnightUI_RefreshNameplates()
--   _G.MidnightUI_GetNameplateCastBar(unit)
--   StartNameplateTicker(), StopNameplateTicker()
--   NameplateManager OnEvent (inline)
-- ============================================================
local function OnNameplateAdded(unit)
    StyleNameplate(unit)
    UpdatePlate(unit)
    if not NameplateManager.updateTicker and NameplateManager._startTicker then
        NameplateManager._startTicker()
    end
end

local function OnNameplateRemoved(unit)
    local entry = plates[unit]
    if entry then
        UpdateTargetGlow(entry, false)
        ClearHoverState(entry)
        if entry.root and unitHoverRoot == entry.root then
            unitHoverRoot = nil
        end
    end
    if entry and entry.threatBar then entry.threatBar:Hide() end
    if entry and entry.customCastBar then
        ReleaseNameplateCastBar(entry.customCastBar)
        entry.customCastBar = nil
    end
    plates[unit] = nil
end

local function UpdateAllPlates()
    if not IsNameplatesEnabled() then
        HideAllPlates()
        return
    end
    for unit, _ in pairs(plates) do
        if UnitExists(unit) then
            UpdatePlate(unit)
        else
            plates[unit] = nil
        end
    end

    if text then
        text:ClearAllPoints()
        text:SetPoint("CENTER", castBar, "CENTER", 0, 0)
        text:SetWidth(math.max(1, castWidth - 10))
        ApplyNameplateFont(text, "Fonts\\FRIZQT__.TTF", castFontSize, "OUTLINE", "CastText")
        text:SetJustifyH("CENTER")
        text:SetJustifyV("MIDDLE")
        text:SetTextColor(1, 1, 1, 1)
        text:SetShadowOffset(1, -1)
        text:SetShadowColor(0, 0, 0, 1)
        if text.SetWordWrap then text:SetWordWrap(false) end
        if text.SetMaxLines then text:SetMaxLines(1) end
        text:SetAlpha(1)
        if text.Show then text:Show() end
    end

    if timer then
        timer:ClearAllPoints()
        timer:SetPoint("RIGHT", castBar, "RIGHT", -5, 0)
        ApplyNameplateFont(timer, "Fonts\\ARIALN.TTF", config.healthPctFontSize + 2, "OUTLINE", "CastTimer")
        timer:SetJustifyH("RIGHT")
        timer:SetTextColor(1, 1, 1, 1)
        timer:SetShadowOffset(1, -1)
        timer:SetShadowColor(0, 0, 0, 1)
        timer:SetAlpha(1)
        if timer.Show then timer:Show() end
    end

    local debugUnit = unit
    if debugUnit == nil and entry then
        debugUnit = entry.unit
    end
    local barName = nil
    if castBar and castBar.GetName then
        local ok, name = pcall(castBar.GetName, castBar)
        if ok then
            barName = name
        end
    end
    local textShown = nil
    if text and text.IsShown then
        local ok, shown = pcall(text.IsShown, text)
        if ok then
            textShown = shown
        end
    end
    local textAlpha = nil
    if text and text.GetAlpha then
        local ok, alpha = pcall(text.GetAlpha, text)
        if ok then
            textAlpha = alpha
        end
    end
    local textParent = nil
    if text and text.GetParent then
        local ok, parent = pcall(text.GetParent, text)
        if ok then
            textParent = parent
        end
    end
    local textParentName = nil
    if textParent and textParent.GetName then
        local ok, name = pcall(textParent.GetName, textParent)
        if ok then
            textParentName = name
        end
    end
end

function MidnightUI_RefreshNameplates()
    if not IsNameplatesEnabled() then
        HideAllPlates()
        return
    end
    UpdateAllPlates()
end

NameplateManager:RegisterEvent("ADDON_LOADED")
NameplateManager:RegisterEvent("NAME_PLATE_UNIT_ADDED")
NameplateManager:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
NameplateManager:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE")
NameplateManager:RegisterEvent("UNIT_THREAT_LIST_UPDATE")
NameplateManager:RegisterEvent("PLAYER_ROLES_ASSIGNED")
NameplateManager:RegisterEvent("GROUP_ROSTER_UPDATE")
NameplateManager:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
NameplateManager:RegisterEvent("UNIT_HEALTH")
NameplateManager:RegisterEvent("UNIT_MAXHEALTH")
NameplateManager:RegisterEvent("PLAYER_TARGET_CHANGED")
NameplateManager:RegisterEvent("UNIT_TARGET")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_START")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_INTERRUPTIBLE")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_NOT_INTERRUPTIBLE")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_STOP")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_FAILED")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_FAILED_QUIET")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_DELAYED")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_EMPOWER_START")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_EMPOWER_STOP")
NameplateManager:RegisterEvent("UNIT_SPELLCAST_EMPOWER_UPDATE")
NameplateManager:RegisterEvent("PLAYER_REGEN_DISABLED")
NameplateManager:RegisterEvent("PLAYER_REGEN_ENABLED")
NameplateManager:RegisterEvent("CVAR_UPDATE")
NameplateManager:RegisterEvent("SPELLS_CHANGED")
NameplateManager:RegisterEvent("PLAYER_TALENT_UPDATE")
NameplateManager:RegisterEvent("UNIT_PET")
NameplateManager:RegisterEvent("UPDATE_MOUSEOVER_UNIT")

NameplateManager:SetScript("OnEvent", function(self, event, ...)
    if not IsNameplatesEnabled() then
        HideAllPlates()
        return
    end
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName ~= ADDON_NAME then return end
        EnsureEnemyPlayerNameplatesEnabled()
        UpdateAllPlates()
        return
    end

    if event == "UPDATE_MOUSEOVER_UNIT" then
        local moExists = UnitExists("mouseover")
        local hEnabled = GetHoverSettings()
        local newRoot = nil
        local newEntry = nil
        if hEnabled and moExists then
            for unit, entry in pairs(plates) do
                if entry.root and UnitIsUnit("mouseover", unit) then
                    if not IsTargetUnit(unit) then
                        newRoot = entry.root
                        newEntry = entry
                    end
                    break
                end
            end
        end
        if newRoot and newRoot == unitHoverRoot then
            newRoot._muiHoverStart = GetTime()
            return
        end
        if not newRoot then
            return
        end
        if unitHoverRoot then
            unitHoverRoot._muiUnitHover = false
            unitHoverRoot._muiHoverActive = false
            local e = unitHoverRoot.MidnightUIEntry
            if e then
                if e.hoverGlow then e.hoverGlow._muiTargetAlpha = 0 end
                StopHoverPulse(e.hoverPulse)
            end
        end
        newRoot._muiHoverStart = GetTime()
        unitHoverRoot = newRoot
        return
    end

    if event == "NAME_PLATE_UNIT_ADDED" then
        OnNameplateAdded(...)
    elseif event == "NAME_PLATE_UNIT_REMOVED" then
        OnNameplateRemoved(...)
    elseif event == "UNIT_THREAT_SITUATION_UPDATE" or event == "UNIT_THREAT_LIST_UPDATE" then
        local unit = ...
        if unit and plates[unit] then UpdatePlate(unit) end
    elseif event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
        local unit = ...
        if unit and plates[unit] then UpdatePlate(unit) end
elseif event == "UNIT_SPELLCAST_START" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastStart(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_CHANNEL_START" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnChannelStart(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_EMPOWER_START" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnEmpowerStart(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
        local unit = ...
        local interrupterGUID = select(4, ...)
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastInterrupted(plates[unit].customCastBar, unit, interrupterGUID)
        end
    elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" or event == "UNIT_SPELLCAST_EMPOWER_STOP" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastStop(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_STOP" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastStop(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastSucceeded(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_FAILED" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastFailed(plates[unit].customCastBar, unit, false)
        end
    elseif event == "UNIT_SPELLCAST_FAILED_QUIET" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastFailed(plates[unit].customCastBar, unit, true)
        end
    elseif event == "UNIT_SPELLCAST_DELAYED" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnCastDelayed(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_EMPOWER_UPDATE" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnChannelUpdate(plates[unit].customCastBar, unit)
        end
    elseif event == "UNIT_SPELLCAST_INTERRUPTIBLE" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnInterruptibleChanged(plates[unit].customCastBar, false)
        end
    elseif event == "UNIT_SPELLCAST_NOT_INTERRUPTIBLE" then
        local unit = ...
        local CB = _G.MidnightUI_CastBars
        if unit and plates[unit] and plates[unit].customCastBar and CB then
            CB.OnInterruptibleChanged(plates[unit].customCastBar, true)
        end
        
    elseif event == "PLAYER_ROLES_ASSIGNED" or event == "GROUP_ROSTER_UPDATE" then
        ClearGroupThreatCache()
        UpdateAllPlates()
    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        C_Timer.After(0.5, function()
            ClearGroupThreatCache()
            UpdateAllPlates()
        end)
        
    elseif event == "PLAYER_TARGET_CHANGED" or event == "UNIT_TARGET" or event == "CVAR_UPDATE" then
        UpdateAllPlates()
        
    elseif event == "SPELLS_CHANGED" or event == "PLAYER_TALENT_UPDATE" or event == "UNIT_PET" then
        UpdateAllPlates()

    elseif event == "PLAYER_REGEN_DISABLED" then
        UpdateAllPlates()
    elseif event == "PLAYER_REGEN_ENABLED" then
        for sensor in pairs(pendingHoverPropagate) do
            if sensor and sensor.SetPropagateMouseClicks then
                sensor:SetPropagateMouseClicks(true)
                sensor:SetPropagateMouseMotion(true)
            end
            pendingHoverPropagate[sensor] = nil
        end
        UpdateAllPlates()
    end
end)

local function StopNameplateTicker()
    if not NameplateManager.updateTicker then return end
    NameplateManager.updateTicker:Cancel()
    NameplateManager.updateTicker = nil
end

local function StartNameplateTicker()
    if NameplateManager.updateTicker then return end
    NameplateManager.updateTicker = C_Timer.NewTicker(config.updateInterval, function()
        if not IsNameplatesEnabled() then return end
        if not next(plates) then
            StopNameplateTicker()
            return
        end
        UpdateAllPlates()
    end)
end

StartNameplateTicker()
NameplateManager._startTicker = StartNameplateTicker

function _G.MidnightUI_GetNameplateCastBar(unit)
    if not unit then return nil end
    local entry = plates[unit]
    if entry and entry.castBar then
        return entry.castBar
    end
    if C_NamePlate and C_NamePlate.GetNamePlateForUnit then
        local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, unit)
        if ok and plate then
            local unitFrame = plate.UnitFrame or plate.unitFrame
            if unitFrame then
                return unitFrame.castBar or unitFrame.CastBar
            end
        end
    end
    return nil
end

