-- =============================================================
-- Specs/PaladinHoly.lua
-- Nem: Healer Alerts — Holy Paladin spec module.
-- =============================================================

local NHA = _G.NemHealerAlerts
if not NHA then return end

local pairs, ipairs       = pairs, ipairs
local string_format       = string.format
local debugprofilestop    = debugprofilestop
local GetTime             = GetTime
local UnitExists          = UnitExists
local UnitAffectingCombat = UnitAffectingCombat
local UnitGUID            = UnitGUID
local C_UnitAuras         = C_UnitAuras
local C_Spell             = C_Spell
local IsPlayerSpell       = IsPlayerSpell
local issecretvalue       = NHA.issecretvalue

-- =============================================================
-- Spell / Aura IDs
-- =============================================================
local BEACON_OF_LIGHT_ID   = 53563
local BEACON_OF_FAITH_ID   = 156910
local BEACON_OF_VIRTUE_ID  = 200025
local BEACON_OF_SAVIOR_ID  = 1244893
local ETERNAL_FLAME_ID     = 156322
local HOLY_SHOCK_ID        = 20473

local HOLY_POWER_TYPE = (Enum and Enum.PowerType and Enum.PowerType.HolyPower) or 9

local state

-- =============================================================
-- Taint-Safe Helpers
-- =============================================================
local function _toNumber(val)
    return val + 0
end
local function _checkPlayerSource(aura)
    return aura.sourceUnit == "player"
end
local function _checkInstanceID(aura)
    return aura.auraInstanceID ~= nil and aura.auraInstanceID ~= 0
end
local function _checkSpellIs(aura, id)
    return aura.spellId and aura.spellId == id
end

local function SafeNum(val, fallback)
    if val == nil then return fallback end
    local ok, result = pcall(_toNumber, val)
    if ok then return result end
    return fallback
end

local function IsPlayerOrUnknownSource(aura)
    if not aura then return false end
    local ok, result = pcall(_checkPlayerSource, aura)
    if not ok then return true end
    return result
end

local function HasValidInstanceID(aura)
    if not aura then return false end
    local ok, result = pcall(_checkInstanceID, aura)
    if not ok then return true end
    return result
end

local function AuraSpellIs(aura, id)
    local ok, result = pcall(_checkSpellIs, aura, id)
    return ok and result
end

local function FindAuraOnUnit(unit, spellId, checkSource)
    if not UnitExists(unit) then return nil end
    if not C_UnitAuras then return nil end

    if C_UnitAuras.GetAuraDataBySpellID then
        local ok, aura = pcall(C_UnitAuras.GetAuraDataBySpellID,
            unit, spellId, "HELPFUL")
        if ok then
            if aura and HasValidInstanceID(aura) then
                if not checkSource or IsPlayerOrUnknownSource(aura) then
                    return aura
                end
            end
            return nil
        end
    end

    if not C_UnitAuras.GetAuraDataByIndex then return nil end
    local i = 1
    while true do
        local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex,
            unit, i, "HELPFUL")
        if not ok or not aura then break end
        if AuraSpellIs(aura, spellId) then
            if not checkSource or IsPlayerOrUnknownSource(aura) then
                return aura
            end
        end
        i = i + 1
    end
    return nil
end

local function AnyMemberHasAura(spellId)
    local seen = {}
    for _, unit in ipairs(NHA:GetGroupUnits()) do
        local guid     = UnitGUID(unit)
        local canDedup = guid and not issecretvalue(guid)
        if not canDedup or not seen[guid] then
            if FindAuraOnUnit(unit, spellId, true) then
                return true
            end
            if canDedup then seen[guid] = true end
        end
    end
    return false
end

-- =============================================================
-- Talent Detection
-- =============================================================
local function RefreshTalentFlags()
    local faith  = false
    local virtue = false
    if IsPlayerSpell then
        local ok1, r1 = pcall(IsPlayerSpell, BEACON_OF_FAITH_ID)
        if ok1 then faith = r1 and true or false end
        local ok2, r2 = pcall(IsPlayerSpell, BEACON_OF_VIRTUE_ID)
        if ok2 then virtue = r2 and true or false end
    end
    state.faithTalented  = faith
    state.virtueTalented = virtue
end

-- =============================================================
-- Beacon of Virtue Cooldown (taint-safe, cached fallback)
-- =============================================================
local function ReadDurationNumber(raw)
    if raw == nil then return nil end
    if type(raw) == "number" then return raw end
    if type(raw) == "table" and raw.GetTotalDuration then
        local ok, v = pcall(raw.GetTotalDuration, raw)
        if ok and type(v) == "number" then return v end
    end
    return nil
end

local function PollVirtueCooldown()
    if not C_Spell or not C_Spell.GetSpellCooldown then return end
    local ok, info = pcall(C_Spell.GetSpellCooldown, BEACON_OF_VIRTUE_ID)
    if not ok or type(info) ~= "table" then return end
    local startTime = SafeNum(info.startTime, nil)
    local duration  = ReadDurationNumber(info.duration)
    if startTime ~= nil and duration ~= nil then
        state.virtueCDStart    = startTime
        state.virtueCDDuration = duration
    end
end

local function IsVirtueReady()
    local startTime = state.virtueCDStart
    local duration  = state.virtueCDDuration
    if not startTime or not duration then return true end
    if duration <= 0 then return true end
    return GetTime() >= (startTime + duration)
end

-- =============================================================
-- Module State
-- =============================================================
state = {
    bolActive           = false,
    bolWasActive        = false,
    faithActive         = false,
    faithWasActive      = false,
    virtueActive        = false,
    virtueWasActive     = false,
    faithTalented       = false,
    virtueTalented      = false,
    virtueCDStart       = nil,
    virtueCDDuration    = nil,
    playerAuraPending   = false,
    rosterUpdatePending = false,
    hsReadyState        = { wasReady = false },
    hpPowerState        = { wasCapped = false },
}

-- =============================================================
-- CUF Whole-Frame Glow State
-- =============================================================
local cufStateMap    = {}
local cufExpiryMap   = {}
local dirtyCUFFrames = {}
local drainScheduled = false

-- =============================================================
-- Alert Definitions
-- =============================================================
local BOL_Y    =  50
local FAITH_Y  =  25
local VIRTUE_Y = FAITH_Y

local ALERTS = {
    {
        key          = "bol",
        label        = "Beacon of Light",
        text         = "* BEACON OF LIGHT *",
        defaultColor = { r = 0.957, g = 0.549, b = 0.729 },  -- #F48CBA
        hasTimer     = false,
        yOffset      = BOL_Y,
        defaultSound = "Paladin: Beacon of Light",
    },
    {
        key          = "faith",
        label        = "Beacon of Faith",
        text         = "* BEACON OF FAITH *",
        defaultColor = { r = 0.616, g = 0.851, b = 0.957 },  -- #9DD9F4
        hasTimer     = false,
        yOffset      = FAITH_Y,
        defaultSound = "Paladin: Beacon of Faith",
    },
    {
        key          = "virtue",
        label        = "Beacon of Virtue",
        text         = "* BEACON OF VIRTUE *",
        defaultColor = { r = 1.000, g = 0.918, b = 0.000 },  -- #FFEA00
        hasTimer     = false,
        yOffset      = VIRTUE_Y,
        defaultSound = "Paladin: Beacon of Virtue",
        noTest       = true,
    },
    {
        key          = "hs",
        label        = "Holy Shock Charges Capped",
        text         = "* HOLY SHOCK *",
        defaultColor = { r = 1.000, g = 0.831, b = 0.318 },  -- #FFD451
        hasTimer     = false,
        yOffset      = 0,
        defaultSound = "Paladin: Holy Shock",
        priority     = NHA.VOICE_PRIO_NORMAL,
    },
    {
        key          = "hp",
        label        = "Holy Power Capped",
        text         = "* SPEND HOLY POWER *",
        defaultColor = { r = 1.000, g = 0.671, b = 0.000 },  -- #FFAB00
        hasTimer     = false,
        yOffset      = -25,
        defaultSound = "Paladin: Spend Holy Power",
        priority     = NHA.VOICE_PRIO_HIGH,
    },
}

-- =============================================================
-- Saved-Variable Defaults
-- =============================================================
local DEFAULTS = {
    bolEnabled         = true,
    bolSoundEnabled    = true,
    bolSoundName       = "Paladin: Beacon of Light",
    faithEnabled       = true,
    faithSoundEnabled  = true,
    faithSoundName     = "Paladin: Beacon of Faith",
    virtueEnabled      = true,
    virtueSoundEnabled = true,
    virtueSoundName    = "Paladin: Beacon of Virtue",
    hsEnabled          = true,
    hsSoundEnabled     = true,
    hsSoundName        = "Paladin: Holy Shock",
    hpEnabled          = true,
    hpSoundEnabled     = true,
    hpSoundName        = "Paladin: Spend Holy Power",
    alertColors = {
        bol    = { r = 0.957, g = 0.549, b = 0.729 },  -- #F48CBA
        faith  = { r = 0.616, g = 0.851, b = 0.957 },  -- #9DD9F4
        virtue = { r = 1.000, g = 0.918, b = 0.000 },  -- #FFEA00
        hs     = { r = 1.000, g = 0.831, b = 0.318 },  -- #FFD451
        hp     = { r = 1.000, g = 0.671, b = 0.000 },  -- #FFAB00
    },
}

-- =============================================================
-- Module Table
-- =============================================================
local PaladinHoly = {
    class     = "PALADIN",
    specIndex = 1,
    specID    = 65,
    specName  = "Holy",
    alerts    = ALERTS,
    defaults  = DEFAULTS,
    state     = state,

    extraEvents = {
        "UNIT_AURA",
        "SPELL_UPDATE_COOLDOWN",
        "SPELL_UPDATE_CHARGES",
        "UNIT_POWER_FREQUENT",
    },

    cufRelevantSpells = {
        [BEACON_OF_LIGHT_ID]  = true,
        [BEACON_OF_FAITH_ID]  = true,
        [BEACON_OF_SAVIOR_ID] = true,
        [ETERNAL_FLAME_ID]    = true,
    },
}

-- =============================================================
-- ClearAllGlows
-- =============================================================
function PaladinHoly:ClearAllGlows()
    for frame in pairs(cufStateMap) do
        if frame._nhaFrameGlowing then NHA:HideFrameGlow(frame) end
    end
    wipe(cufStateMap)
    wipe(cufExpiryMap)
    wipe(dirtyCUFFrames)
    drainScheduled = false
end

-- =============================================================
-- ClearAllState
-- =============================================================
function PaladinHoly:ClearAllState()
    state.bolActive           = false
    state.bolWasActive        = false
    state.faithActive         = false
    state.faithWasActive      = false
    state.virtueActive        = false
    state.virtueWasActive     = false
    state.hsReadyState.wasReady = false
    state.hpPowerState.wasCapped  = false
    state.playerAuraPending   = false
    state.rosterUpdatePending = false
    self:ClearAllGlows()
end

-- =============================================================
-- Perf Instrumentation
-- =============================================================
local _perfBlockTimer    = 0
local _perfCUFLastPath   = nil
local _perfFrameStamp    = 0
local _perfFrameTotal    = 0
local _perfFrameEval     = 0
local _perfFrameCUF      = 0
local _perfFrameRefr     = 0
local _perfFrameCUFCount = 0

local function PerfFrameRoll()
    local p = state._perf
    if not (p and p.enabled) then return end
    local now = GetTime()
    if now ~= _perfFrameStamp then
        if _perfFrameTotal > (p.spike and p.spike.totalMs or 0) then
            p.spike = p.spike or {}
            p.spike.totalMs   = _perfFrameTotal
            p.spike.evalMs    = _perfFrameEval
            p.spike.cufMs     = _perfFrameCUF
            p.spike.refreshMs = _perfFrameRefr
            p.spike.cufCount  = _perfFrameCUFCount
            p.spike.atTime    = now
        end
        _perfFrameStamp     = now
        _perfFrameTotal     = 0
        _perfFrameEval      = 0
        _perfFrameCUF       = 0
        _perfFrameRefr      = 0
        _perfFrameCUFCount  = 0
    end
end

local function PerfBeginBlocks()
    local p = state._perf
    if not (p and p.blocksEnabled) then return end
    _perfBlockTimer = debugprofilestop()
end

local function PerfMarkBlock(name)
    local p = state._perf
    if not (p and p.blocksEnabled) then return end
    local now = debugprofilestop()
    local dt  = now - _perfBlockTimer
    local b   = p.blocks[name]
    if not b then
        b = { calls = 0, totalMs = 0, maxMs = 0 }
        p.blocks[name] = b
    end
    b.calls   = b.calls + 1
    b.totalMs = b.totalMs + dt
    if dt > b.maxMs then b.maxMs = dt end
    _perfBlockTimer = now
end

local _DrainGlowDeferred

-- =============================================================
-- DoEvaluateCUFGlow
-- Priority: Savior > BoL/Virtue (talent-colored) > Faith > EF > none
-- =============================================================
local function DoEvaluateCUFGlow(self, db, frame, unit)
    if not db or not db.enabled or not db.cufGlowEnabled then
        if cufStateMap[frame] then
            NHA:HideFrameGlow(frame)
            cufStateMap[frame] = nil
        end
        cufExpiryMap[frame] = nil
        _perfCUFLastPath = "disabled"
        return
    end

    local kind, color, aura

    aura = FindAuraOnUnit(unit, BEACON_OF_SAVIOR_ID, true)
    if aura then
        kind  = "savior"
        color = { r = 0.800, g = 0.467, b = 0.133 }  -- #CC7722
    else
        aura = FindAuraOnUnit(unit, BEACON_OF_LIGHT_ID, true)
        if aura then
            if state.virtueTalented then
                kind  = "virtue"
                color = (db.alertColors and db.alertColors.virtue)
                        or { r = 1.000, g = 0.918, b = 0.000 }  -- #FFEA00
            else
                kind  = "bol"
                color = (db.alertColors and db.alertColors.bol)
                        or { r = 0.957, g = 0.549, b = 0.729 }  -- #F48CBA
            end
        else
            aura = FindAuraOnUnit(unit, BEACON_OF_FAITH_ID, true)
            if aura then
                kind  = "faith"
                color = (db.alertColors and db.alertColors.faith)
                        or { r = 0.616, g = 0.851, b = 0.957 }  -- #9DD9F4
            else
                aura = FindAuraOnUnit(unit, ETERNAL_FLAME_ID, true)
                if aura then
                    kind  = "ef"
                    color = { r = 1.000, g = 0.816, b = 0.000 }  -- #FFD000
                end
            end
        end
    end

    if kind then
        NHA:ShowFrameGlow(frame, color.r, color.g, color.b)
        cufStateMap[frame] = kind
        _perfCUFLastPath = kind

        local exp = SafeNum(aura.expirationTime, 0)
        if exp > 0 then
            cufExpiryMap[frame] = exp
        else
            cufExpiryMap[frame] = nil
        end
    else
        if cufStateMap[frame] then
            NHA:HideFrameGlow(frame)
            cufStateMap[frame] = nil
        end
        cufExpiryMap[frame] = nil
        _perfCUFLastPath = "none"
    end
end

-- =============================================================
-- InstrumentedDoEvaluateCUFGlow
-- =============================================================
local function InstrumentedDoEvaluateCUFGlow(self, db, frame, unit)
    local perf = state._perf
    if not (perf and perf.enabled) then
        DoEvaluateCUFGlow(self, db, frame, unit)
        return
    end
    PerfFrameRoll()
    _perfCUFLastPath = nil
    local t0 = debugprofilestop()
    DoEvaluateCUFGlow(self, db, frame, unit)
    local dt  = debugprofilestop() - t0
    local cuf = perf.cuf
    cuf.calls   = cuf.calls + 1
    cuf.totalMs = cuf.totalMs + dt
    if dt > cuf.maxMs then cuf.maxMs = dt end
    local path = _perfCUFLastPath or "none"
    cuf.paths[path] = (cuf.paths[path] or 0) + 1
    _perfFrameTotal     = _perfFrameTotal     + dt
    _perfFrameCUF       = _perfFrameCUF       + dt
    _perfFrameCUFCount  = _perfFrameCUFCount  + 1
end

-- =============================================================
-- _DrainGlowDeferred
-- =============================================================
_DrainGlowDeferred = function()
    drainScheduled = false
    local db = NHA.db
    if not db or not db.cufGlowEnabled then
        wipe(dirtyCUFFrames)
        return
    end
    local processed = 0
    for frame, unit in pairs(dirtyCUFFrames) do
        local liveUnit = frame._nhaCUFUnit or unit
        if liveUnit then
            InstrumentedDoEvaluateCUFGlow(nil, db, frame, liveUnit)
        end
        dirtyCUFFrames[frame] = nil
        processed = processed + 1
        if processed >= 4 then break end
    end
    if next(dirtyCUFFrames) then
        drainScheduled = true
        C_Timer.After(0, _DrainGlowDeferred)
    end
end

-- =============================================================
-- EvaluateCUFGlow
-- =============================================================
function PaladinHoly:EvaluateCUFGlow(db, frame, unit)
    if not db or not db.enabled or not db.cufGlowEnabled then
        if cufStateMap[frame] then
            NHA:HideFrameGlow(frame)
            cufStateMap[frame] = nil
        end
        dirtyCUFFrames[frame] = nil
        return
    end
    dirtyCUFFrames[frame] = unit
    if not drainScheduled then
        drainScheduled = true
        C_Timer.After(0, _DrainGlowDeferred)
    end
end

-- =============================================================
-- RefreshGlows
-- =============================================================
function PaladinHoly:RefreshGlows(db)
end

-- =============================================================
-- Evaluate
-- =============================================================
function PaladinHoly:Evaluate(db)
    PerfBeginBlocks()

    local bolMissing = false
    if db.bolEnabled and not state.virtueTalented then
        bolMissing = not AnyMemberHasAura(BEACON_OF_LIGHT_ID)
    end
    state.bolActive = bolMissing
    PerfMarkBlock("BOL")

    local faithMissing = false
    if db.faithEnabled and state.faithTalented then
        faithMissing = not AnyMemberHasAura(BEACON_OF_FAITH_ID)
    end
    state.faithActive = faithMissing
    PerfMarkBlock("FAITH")

    local virtueMissing = false
    if db.virtueEnabled and state.virtueTalented then
        if IsVirtueReady() then
            virtueMissing = not AnyMemberHasAura(BEACON_OF_LIGHT_ID)
        end
    end
    state.virtueActive = virtueMissing
    PerfMarkBlock("VIRTUE")

    if bolMissing    and not state.bolWasActive    then NHA:PlayAlertSound("bol")    end
    if faithMissing  and not state.faithWasActive  then NHA:PlayAlertSound("faith")  end
    if virtueMissing and not state.virtueWasActive then NHA:PlayAlertSound("virtue") end

    -- Holy Shock ready (Pattern A)
    state.hsActive = NHA.AlertHelpers.EvalSpellReady(state.hsReadyState, HOLY_SHOCK_ID, "hs")

    -- Holy Power capped (Pattern A power variant)
    state.hpActive = NHA.AlertHelpers.EvalPowerCap(state.hpPowerState, HOLY_POWER_TYPE, "hp")

    -- Suite-wide rule (see [[nha-combat-gate-rule]]): preserve all latches
    -- across combat boundaries so the next combat's first eval can fire on
    -- actual rising edges instead of resetting to false and re-tripping.
    state.bolWasActive    = bolMissing
    state.faithWasActive  = faithMissing
    state.virtueWasActive = virtueMissing
    PerfMarkBlock("END")
end

-- =============================================================
-- OnActivate
-- =============================================================
function PaladinHoly:OnActivate(db)
    RefreshTalentFlags()
    PollVirtueCooldown()
    if NHA.RefreshAllCUFs then
        C_Timer.After(0, function()
            NHA:RefreshAllCUFs()
        end)
    end
end

-- =============================================================
-- OnDeactivate
-- =============================================================
function PaladinHoly:OnDeactivate(db)
end

-- =============================================================
-- OnEvent
-- =============================================================
function PaladinHoly:OnEvent(db, event, ...)
    if event == "UNIT_AURA" then
        local unit = ...
        if unit ~= "player"
            and not (UnitInParty and UnitInParty(unit))
            and not (UnitInRaid  and UnitInRaid(unit)) then
            return true
        end
        if not state.playerAuraPending then
            state.playerAuraPending = true
            C_Timer.After(0.1, function()
                state.playerAuraPending = false
                NHA.Evaluate()
            end)
        end
        return true
    end

    if event == "GROUP_ROSTER_UPDATE" then
        if not state.rosterUpdatePending then
            state.rosterUpdatePending = true
            C_Timer.After(0.2, function()
                state.rosterUpdatePending = false
                NHA.Evaluate()
            end)
        end
        return true
    end

    if event == "PLAYER_TALENT_UPDATE"
    or event == "TRAIT_CONFIG_UPDATED"
    or event == "PLAYER_SPECIALIZATION_CHANGED" then
        RefreshTalentFlags()
        return false
    end

    if event == "SPELL_UPDATE_COOLDOWN" then
        PollVirtueCooldown()
        return false
    end

    if event == "PLAYER_REGEN_DISABLED" then
        if NHA.RefreshAllCUFs then
            NHA:RefreshAllCUFs()
        end
        return false
    end

    if event == "PLAYER_REGEN_ENABLED" then
        state.bolActive       = false
        state.bolWasActive    = false
        state.faithActive     = false
        state.faithWasActive  = false
        state.virtueActive    = false
        state.virtueWasActive = false
        return false
    end

    if event == "PLAYER_ENTERING_WORLD" then
        RefreshTalentFlags()
        PollVirtueCooldown()
        return false
    end

    return false
end

-- =============================================================
-- Slash Commands
-- =============================================================
PaladinHoly.slashCommands = {
    ["debug"] = function(self, db)
        NHA.Msg("|cffffd700Holy Paladin Debug:|r")
        NHA.Msg("  enabled: "         .. tostring(db.enabled))
        NHA.Msg("  bolEnabled: "      .. tostring(db.bolEnabled))
        NHA.Msg("  faithEnabled: "    .. tostring(db.faithEnabled))
        NHA.Msg("  virtueEnabled: "   .. tostring(db.virtueEnabled))
        NHA.Msg("  cufGlowEnabled: "  .. tostring(db.cufGlowEnabled))
        NHA.Msg("  inCombat: "        .. tostring(UnitAffectingCombat("player")))
        NHA.Msg("  bolActive: "       .. tostring(state.bolActive))
        NHA.Msg("  faithActive: "     .. tostring(state.faithActive))
        NHA.Msg("  virtueActive: "    .. tostring(state.virtueActive))
        NHA.Msg("  faithTalented: "   .. tostring(state.faithTalented))
        NHA.Msg("  virtueTalented: "  .. tostring(state.virtueTalented))
        NHA.Msg("  virtueCDStart: "    .. tostring(state.virtueCDStart))
        NHA.Msg("  virtueCDDuration: " .. tostring(state.virtueCDDuration))
        NHA.Msg("  IsVirtueReady: "    .. tostring(IsVirtueReady()))

        local function CountAuraOnGroup(spellId)
            local count = 0
            local seen  = {}
            for _, unit in ipairs(NHA:GetGroupUnits()) do
                local guid     = UnitGUID(unit)
                local canDedup = guid and not issecretvalue(guid)
                if not canDedup or not seen[guid] then
                    if FindAuraOnUnit(unit, spellId, true) then
                        count = count + 1
                    end
                    if canDedup then seen[guid] = true end
                end
            end
            return count
        end
        NHA.Msg("  group with Beacon of Light: "  .. CountAuraOnGroup(BEACON_OF_LIGHT_ID))
        NHA.Msg("  group with Beacon of Faith: "  .. CountAuraOnGroup(BEACON_OF_FAITH_ID))
        NHA.Msg("  group with Beacon of Savior: " .. CountAuraOnGroup(BEACON_OF_SAVIOR_ID))
        NHA.Msg("  group with Eternal Flame: "    .. CountAuraOnGroup(ETERNAL_FLAME_ID))
    end,

    ["cufglow"] = function(self, db)
        NHA.Msg("|cffffd700CUF Frame Glow Debug (Holy Paladin):|r")
        NHA.Msg("  cufGlowEnabled: " .. tostring(db.cufGlowEnabled))
        NHA.Msg("  cufHooks: "       .. tostring(NHA._cufHooksInstalled and "installed" or "not installed"))
        local trackedCount = 0
        for _ in pairs(NHA._cufPool or {}) do trackedCount = trackedCount + 1 end
        local stateCount = 0
        local kindCounts = { savior = 0, bol = 0, virtue = 0, faith = 0, ef = 0 }
        for _, kind in pairs(cufStateMap) do
            stateCount = stateCount + 1
            kindCounts[kind] = (kindCounts[kind] or 0) + 1
        end
        local expiryCount = 0
        for _ in pairs(cufExpiryMap) do expiryCount = expiryCount + 1 end
        local dirtyCount = 0
        for _ in pairs(dirtyCUFFrames) do dirtyCount = dirtyCount + 1 end
        NHA.Msg("  tracked CUFs: "      .. trackedCount)
        NHA.Msg("  in combat: "         .. tostring(UnitAffectingCombat("player")))
        NHA.Msg("  glowing frames: "    .. stateCount)
        NHA.Msg(string_format(
            "  by kind: savior=%d  bol=%d  virtue=%d  faith=%d  ef=%d",
            kindCounts.savior, kindCounts.bol, kindCounts.virtue,
            kindCounts.faith,  kindCounts.ef))
        NHA.Msg("  cufExpiryMap size: " .. expiryCount)
        NHA.Msg("  dirtyCUFFrames: "    .. dirtyCount .. " (pending re-eval)")

        NHA.Msg("|cffffd700  Per-unit aura detection:|r")
        local probedUnits = 0
        for unit in pairs(NHA._unitCUFs or {}) do
            if probedUnits >= 8 then break end
            local existsOk, exists = pcall(UnitExists, unit)
            if existsOk and exists then
                local nameOk, name = pcall(UnitName, unit)
                local hasSavior = FindAuraOnUnit(unit, BEACON_OF_SAVIOR_ID, true) ~= nil
                local hasBoL    = FindAuraOnUnit(unit, BEACON_OF_LIGHT_ID,  true) ~= nil
                local hasFaith  = FindAuraOnUnit(unit, BEACON_OF_FAITH_ID,  true) ~= nil
                local hasEF     = FindAuraOnUnit(unit, ETERNAL_FLAME_ID,    true) ~= nil
                local glowKind  = "none"
                if NHA._unitCUFs and NHA._unitCUFs[unit] then
                    for f in pairs(NHA._unitCUFs[unit]) do
                        if cufStateMap[f] then
                            glowKind = cufStateMap[f]
                            break
                        end
                    end
                end
                NHA.Msg(string_format(
                    "  %s (%s): SAV=%s BoL=%s FAI=%s EF=%s glow=%s",
                    unit,
                    (nameOk and tostring(name)) or "?",
                    tostring(hasSavior),
                    tostring(hasBoL),
                    tostring(hasFaith),
                    tostring(hasEF),
                    glowKind))
                probedUnits = probedUnits + 1
            end
        end
    end,

    ["perf on"] = function(self, db)
        local perf = state._perf
        if not perf then
            perf = { enabled = false }
            state._perf = perf
        end
        if perf.enabled then NHA.Msg("|cffffd700NHA Perf:|r already on"); return end
        perf.enabled       = true
        perf.calls         = 0
        perf.totalMs       = 0
        perf.maxMs         = 0
        perf.startMs       = debugprofilestop()
        perf.blocksEnabled = true
        perf.blocks        = {}
        perf.cuf = {
            calls = 0, totalMs = 0, maxMs = 0,
            paths = { savior = 0, bol = 0, virtue = 0, faith = 0, ef = 0,
                      none = 0, disabled = 0 },
        }
        perf.refresh = { calls = 0, totalMs = 0, maxMs = 0 }
        perf.spike = {
            totalMs = 0, evalMs = 0, cufMs = 0, refreshMs = 0,
            cufCount = 0, atTime = 0,
        }

        perf.origEvaluate = PaladinHoly.Evaluate
        PaladinHoly.Evaluate = function(slf, sdb)
            PerfFrameRoll()
            local t0 = debugprofilestop()
            perf.origEvaluate(slf, sdb)
            local dt = debugprofilestop() - t0
            perf.calls   = perf.calls + 1
            perf.totalMs = perf.totalMs + dt
            if dt > perf.maxMs then perf.maxMs = dt end
            _perfFrameTotal = _perfFrameTotal + dt
            _perfFrameEval  = _perfFrameEval  + dt
        end

        perf.origRefresh = PaladinHoly.RefreshGlows
        PaladinHoly.RefreshGlows = function(slf, sdb)
            PerfFrameRoll()
            local t0 = debugprofilestop()
            perf.origRefresh(slf, sdb)
            local dt = debugprofilestop() - t0
            local r = perf.refresh
            r.calls   = r.calls + 1
            r.totalMs = r.totalMs + dt
            if dt > r.maxMs then r.maxMs = dt end
            _perfFrameTotal = _perfFrameTotal + dt
            _perfFrameRefr  = _perfFrameRefr  + dt
        end

        NHA.Msg("|cff00ff00NHA Perf: ENABLED|r — wraps on Evaluate + EvaluateCUFGlow + RefreshGlows. Run a pull, then /nha perf off")
    end,

    ["perf off"] = function(self, db)
        local perf = state._perf
        if not perf or not perf.enabled then
            NHA.Msg("|cffffd700NHA Perf:|r already off"); return
        end
        PaladinHoly.Evaluate     = perf.origEvaluate
        PaladinHoly.RefreshGlows = perf.origRefresh
        perf.origEvaluate        = nil
        perf.origRefresh         = nil
        PerfFrameRoll()
        perf.enabled       = false
        perf.blocksEnabled = false

        if perf.calls == 0 then
            NHA.Msg("|cffffd700NHA Perf FINAL:|r no samples")
            return
        end

        local elapsed = (debugprofilestop() - perf.startMs) / 1000
        local rate    = elapsed > 0 and (perf.calls / elapsed) or 0
        local avg     = perf.totalMs / perf.calls
        NHA.Msg(string_format(
            "|cffffd700NHA Perf FINAL:|r %.1fs, %d calls (%.1f/s), avg=%.3fms max=%.3fms total=%.1fms",
            elapsed, perf.calls, rate, avg, perf.maxMs, perf.totalMs))

        local order = { "BOL", "FAITH", "VIRTUE", "END" }
        for _, name in ipairs(order) do
            local b = perf.blocks and perf.blocks[name]
            if b and b.calls > 0 then
                NHA.Msg(string_format(
                    "  %-7s avg=%.3fms max=%.3fms total=%.1fms (%d calls)",
                    name, b.totalMs / b.calls, b.maxMs, b.totalMs, b.calls))
            end
        end

        local cuf = perf.cuf
        if cuf and cuf.calls > 0 then
            local cufRate = elapsed > 0 and (cuf.calls / elapsed) or 0
            local cufAvg  = cuf.totalMs / cuf.calls
            NHA.Msg(string_format(
                "|cffffd700CUF GLOW:|r %d calls (%.1f/s) avg=%.3fms max=%.3fms total=%.1fms",
                cuf.calls, cufRate, cufAvg, cuf.maxMs, cuf.totalMs))
            NHA.Msg(string_format(
                "  paths: SAV=%d  BoL=%d  VIRT=%d  FAI=%d  EF=%d  NONE=%d  DIS=%d",
                cuf.paths.savior or 0, cuf.paths.bol    or 0,
                cuf.paths.virtue or 0, cuf.paths.faith  or 0,
                cuf.paths.ef     or 0, cuf.paths.none   or 0,
                cuf.paths.disabled or 0))
        else
            NHA.Msg("|cffffd700CUF GLOW:|r no calls in window")
        end

        local r = perf.refresh
        if r and r.calls > 0 then
            local rRate = elapsed > 0 and (r.calls / elapsed) or 0
            local rAvg  = r.totalMs / r.calls
            NHA.Msg(string_format(
                "|cffffd700REFRESH:|r %d calls (%.1f/s) avg=%.3fms max=%.3fms total=%.1fms",
                r.calls, rRate, rAvg, r.maxMs, r.totalMs))
        end

        local s = perf.spike
        if s and s.totalMs > 0 then
            NHA.Msg(string_format(
                "|cffffd700WORST FRAME:|r %.3fms total (Eval=%.3fms  CUF=%.3fms x%d  Refresh=%.3fms)",
                s.totalMs, s.evalMs, s.cufMs, s.cufCount, s.refreshMs))
        end
    end,

    ["perf"] = function(self, db)
        local perf = state._perf
        if not perf or perf.calls == 0 then
            NHA.Msg("|cffffd700NHA Perf:|r " ..
                ((perf and perf.enabled) and "RUNNING — no samples yet"
                                          or "off (use /nha perf on)"))
            return
        end
        local elapsed = (debugprofilestop() - perf.startMs) / 1000
        local rate    = elapsed > 0 and (perf.calls / elapsed) or 0
        local avg     = perf.totalMs / perf.calls
        NHA.Msg(string_format(
            "|cffffd700NHA Perf %s:|r %.1fs, %d Eval (%.1f/s), avg=%.3fms max=%.3fms",
            perf.enabled and "RUNNING" or "STOPPED",
            elapsed, perf.calls, rate, avg, perf.maxMs))
        local cuf = perf.cuf
        if cuf and cuf.calls > 0 then
            local cufRate = elapsed > 0 and (cuf.calls / elapsed) or 0
            NHA.Msg(string_format(
                "  CUF: %d calls (%.1f/s) avg=%.3fms max=%.3fms",
                cuf.calls, cufRate, cuf.totalMs / cuf.calls, cuf.maxMs))
        end
        local s = perf.spike
        if s and s.totalMs > 0 then
            NHA.Msg(string_format(
                "  worst frame: %.3fms (E=%.3f C=%.3f x%d R=%.3f)",
                s.totalMs, s.evalMs, s.cufMs, s.cufCount, s.refreshMs))
        end
    end,
}

-- =============================================================
-- Register
-- =============================================================
-- v12.0.25 — spec not shipped this release; re-enable in v12.0.26 by uncommenting.
-- NHA:RegisterSpec("PALADIN_HOLY", PaladinHoly)
