-- =============================================================
-- Specs/PriestDiscipline.lua
-- Nem: Healer Alerts — Discipline Priest spec module
--
-- FULLY SELF-CONTAINED. This file contains ALL Disc Priest
-- spell IDs, aura scan logic, state management, evaluate logic,
-- event handling, glow evaluation, and options panel additions.
--
-- It has ZERO dependencies on any other spec module. Adding or
-- removing other spec files will never affect this module.
--
-- Registers itself with the core via NHA:RegisterSpec().
-- =============================================================

local NHA = _G.NemHealerAlerts
if not NHA then return end  -- core must load first

local pairs, ipairs       = pairs, ipairs
local math_floor          = math.floor
local string_format       = string.format
local GetTime             = GetTime
local UnitExists          = UnitExists
local UnitIsEnemy         = UnitIsEnemy
local C_UnitAuras         = C_UnitAuras
local issecretvalue       = NHA.issecretvalue

-- =============================================================
-- Spell / Aura IDs
-- =============================================================
local ATONEMENT_ID   = 194384
local MTD_PROC_ID    = 1253591   -- Master the Darkness (player proc buff)
local MTD_ICON_ID    = 5976941   -- Master the Darkness icon texture
local VOID_SHIELD_ID = 1253593   -- Void Shield (absorb on target)
local PWS_ID         = 17
local ATONEMENT_PANIC_THRESHOLD = 4.5

-- =============================================================
-- Taint-Safe Helpers
-- =============================================================
-- In WoW Midnight, aura spell IDs can be "secret number values"
-- that throw errors on ANY operation (==, tostring, etc.).
-- These helpers wrap comparisons in pcall to avoid taint errors.

-- Returns true if aura.spellId safely equals the given ID
local function AuraSpellIs(aura, id)
    local ok, result = pcall(function()
        return aura.spellId and aura.spellId == id
    end)
    return ok and result
end

-- Returns the spellId as a number, or nil if tainted/missing
local function SafeGetSpellId(aura)
    local ok, result = pcall(function()
        return aura.spellId
    end)
    if ok and result and not issecretvalue(result) then
        return result
    end
    return nil
end

-- =============================================================
-- Module State (owned entirely by this module)
-- =============================================================
local state = {
    -- Atonement expiring on priority target
    atonActive      = false,
    atonWasActive   = false,
    atonExpire      = 0,
    atonTimeMod     = 1,
    atonUnit        = nil,
    atonInstID      = nil,
    -- Void Shield proc (Master the Darkness)
    vsActive        = false,
    vsWasActive     = false,
    -- Debounce
    playerAuraPending   = false,
    atonAuraPending     = false,
    atonScanPending     = false,
    rosterUpdatePending = false,
}

-- =============================================================
-- Glow Frame Tables (owned entirely by this module)
-- =============================================================
local atonGlowFrames      = {}   -- buffFrame → safe aura copy
local voidShieldGlowFrames = {}  -- buffFrame → true

-- =============================================================
-- Alert Definitions
-- =============================================================
local VS_Y         = -30
local ATON_Y        = -54
local ATON_TIMER_Y  = -74

local ALERTS = {
    {
        key          = "vs",
        label        = "Cast Void Shield",
        text         = "* CAST VOID SHIELD *",
        defaultColor = { r = 0.600, g = 0.200, b = 0.800 },  -- Purple/void
        hasTimer     = false,
        yOffset      = VS_Y,
        defaultSound = "Ping",
    },
    {
        key          = "aton",
        label        = "Atonement Expiring",
        text         = "* ATONEMENT EXPIRING *",
        defaultColor = { r = 1.000, g = 1.000, b = 1.000 },  -- Priest class color
        hasTimer     = true,
        timerCycleSec = 4.0,
        timerThresholds = {
            { 2.0, 0.667, 1.0, 0.0 },   -- > 2s: #aaff00
            { 1.0, 1.0, 0.816, 0.0 },    -- > 1s: #ffd000
            { 0,   1.0, 0.4,   0.0 },    -- <= 1s: #ff6600
        },
        yOffset      = ATON_Y,
        timerYOffset = ATON_TIMER_Y,
        defaultSound = "Bell",
    },
}

-- =============================================================
-- Saved-Variable Defaults (merged into core db)
-- =============================================================
local DEFAULTS = {
    vsEnabled            = true,
    vsSoundEnabled       = true,
    vsSoundName          = "Ping",
    atonEnabled          = true,
    atonSoundEnabled     = true,
    atonSoundName        = "Bell",
    atonTimerShown       = true,
    alertColors          = {
        vs   = { r = 0.600, g = 0.200, b = 0.800 },
        aton = { r = 1.000, g = 1.000, b = 1.000 },
    },
}

-- =============================================================
-- Atonement Scan
-- =============================================================
local function FindAtonementOnUnit(unit)
    if not UnitExists(unit) or UnitIsEnemy("player", unit) then return nil end
    if not C_UnitAuras then return nil end

    -- Direct API lookup (if available)
    if C_UnitAuras.GetAuraDataBySpellID then
        local ok, aura = pcall(C_UnitAuras.GetAuraDataBySpellID,
            unit, ATONEMENT_ID, "HELPFUL")
        if ok and aura and aura.auraInstanceID ~= 0
           and aura.sourceUnit == "player" then
            return aura
        end
        -- Fall through to index scan if direct lookup failed
    end

    -- Index scan fallback (taint-safe)
    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, ATONEMENT_ID) and aura.sourceUnit == "player" then
            return aura
        end
        i = i + 1
    end
    return nil
end

local function FindSoonestAtonement()
    local bestUnit, bestAura, bestExpire = nil, nil, math.huge
    local now = GetTime()

    for _, unit in ipairs(NHA:GetGroupUnits()) do
        local aura = FindAtonementOnUnit(unit)
        if aura then
            local expire = aura.expirationTime or 0
            local remaining = expire - now
            if remaining > 0 and remaining < bestExpire then
                bestUnit   = unit
                bestAura   = aura
                bestExpire = remaining
            end
        end
    end

    return bestUnit, bestAura
end

-- =============================================================
-- Atonement Tracking Helpers
-- =============================================================
local function StoreAtonAura(unit, aura)
    state.atonUnit    = unit
    state.atonInstID  = aura.auraInstanceID
    state.atonExpire  = aura.expirationTime or 0
    state.atonTimeMod = (aura.timeMod and aura.timeMod > 0)
                        and aura.timeMod or 1
end

local function ClearAtonTracking()
    state.atonUnit    = nil
    state.atonInstID  = nil
    state.atonExpire  = 0
    state.atonTimeMod = 1
end

-- =============================================================
-- Void Shield Proc Detection
-- =============================================================
-- PW:S normal icon texture IDs (when MTD is NOT active)
local PWS_NORMAL_ICONS = {
    [135940] = true,   -- Standard PW:S icon
}
-- Void Shield icon texture ID (when MTD IS active, PW:S shows this)
local VOID_SHIELD_SPELL_ICON = 7514191  -- Purple void shield icon on spell

local function CheckVoidShieldProc()
    -- ── Strategy: Check if PW:S spell texture has changed ────
    -- When Master the Darkness procs, PW:S's icon changes from
    -- the normal shield to the void shield icon. This is taint-safe
    -- because it queries the spellbook, not aura data.
    --
    -- Try C_Spell.GetSpellTexture first (Midnight API), fall back
    -- to GetSpellTexture (legacy).
    local texture = nil
    if C_Spell and C_Spell.GetSpellTexture then
        local ok, tex = pcall(C_Spell.GetSpellTexture, PWS_ID)
        if ok then texture = tex end
    end
    if not texture and GetSpellTexture then
        local ok, tex = pcall(GetSpellTexture, PWS_ID)
        if ok then texture = tex end
    end

    if texture then
        -- If the icon is NOT the normal PW:S icon, MTD is active
        if not PWS_NORMAL_ICONS[texture] then
            return true
        end
        return false
    end

    -- ── Fallback: aura scan (out-of-combat only) ─────────────
    -- In combat, aura fields are tainted and unusable.
    -- This only works reliably outside combat.
    if not C_UnitAuras then return false end

    -- Direct API lookup
    if C_UnitAuras.GetAuraDataBySpellID then
        local ok, aura = pcall(C_UnitAuras.GetAuraDataBySpellID,
            "player", MTD_PROC_ID, "HELPFUL")
        if ok and aura and aura.auraInstanceID and aura.auraInstanceID ~= 0 then
            return true
        end
    end

    -- Index scan with pcall (taint-safe but may not work in combat)
    if C_UnitAuras.GetAuraDataByIndex then
        local i = 1
        while true do
            local ok, aura = pcall(C_UnitAuras.GetAuraDataByIndex,
                "player", i, "HELPFUL")
            if not ok or not aura then break end
            -- Try icon match (may be <no value> if tainted)
            local iconOk, iconMatch = pcall(function()
                return aura.icon and aura.icon == MTD_ICON_ID
            end)
            if iconOk and iconMatch then return true end
            -- Try spell ID match (will fail if tainted)
            local idOk, idMatch = pcall(function()
                return aura.spellId and aura.spellId == MTD_PROC_ID
            end)
            if idOk and idMatch then return true end
            i = i + 1
        end
    end

    return false
end

-- =============================================================
-- MODULE TABLE
-- =============================================================
local PriestDisc = {
    class     = "PRIEST",
    specIndex = 1,  -- Discipline is spec index 1
    specName  = "Discipline",
    alerts    = ALERTS,
    defaults  = DEFAULTS,
    state     = state,

    extraEvents = {
        "UNIT_AURA",
        "UNIT_SPELLCAST_SUCCEEDED",
    },
}

-- =============================================================
-- ClearAllGlows
-- =============================================================
function PriestDisc:ClearAllGlows()
    for buffFrame in pairs(atonGlowFrames) do
        NHA:HideProcGlow(buffFrame)
    end
    wipe(atonGlowFrames)
    for buffFrame in pairs(voidShieldGlowFrames) do
        NHA:HideProcGlow(buffFrame)
    end
    wipe(voidShieldGlowFrames)
end

-- =============================================================
-- ClearAllState
-- =============================================================
function PriestDisc:ClearAllState()
    ClearAtonTracking()
    state.atonActive        = false
    state.atonWasActive     = false
    state.vsActive          = false
    state.vsWasActive       = false
    state.playerAuraPending = false
    state.atonAuraPending   = false
    state.atonScanPending   = false
    state.rosterUpdatePending = false
    self:ClearAllGlows()
end

-- =============================================================
-- NeedsTicker — keep ticker alive when Atonement is tracked (for glows)
-- =============================================================
function PriestDisc:NeedsTicker(db)
    return state.atonUnit ~= nil or next(voidShieldGlowFrames) ~= nil
end

-- =============================================================
-- RefreshGlows (called every 0.25s by ticker)
-- =============================================================
function PriestDisc:RefreshGlows(db)
    if not db or not db.glowEnabled then return end
    local now = GetTime()

    -- ── Atonement pandemic glows (matches Druid LB pattern) ──
    local c = db.alertColors and db.alertColors.aton
              or { r=1, g=1, b=1 }
    for buffFrame, aura in pairs(atonGlowFrames) do
        if buffFrame.auraInstanceID ~= aura.auraInstanceID then
            NHA:HideProcGlow(buffFrame)
            atonGlowFrames[buffFrame] = nil
        else
            local remaining = (aura.expirationTime - now) / aura.timeMod
            if remaining > 0 and remaining <= ATONEMENT_PANIC_THRESHOLD then
                NHA:ShowProcGlow(buffFrame, c.r, c.g, c.b)
            elseif remaining <= 0 then
                NHA:HideProcGlow(buffFrame)
                atonGlowFrames[buffFrame] = nil
            else
                NHA:HideProcGlow(buffFrame)
            end
        end
    end

    -- ── Void Shield glow cleanup ─────────────────────────────
    local GlobalAuras = _G.C_UnitAuras
    if not GlobalAuras or not GlobalAuras.GetAuraDataByAuraInstanceID then return end
    for buffFrame in pairs(voidShieldGlowFrames) do
        local valid = false
        if buffFrame.auraInstanceID then
            local parentUnit = nil
            local parent = buffFrame:GetParent()
            if parent and parent.unit then
                parentUnit = parent.unit
            end
            if parentUnit then
                local ok, aura = pcall(GlobalAuras.GetAuraDataByAuraInstanceID,
                    parentUnit, buffFrame.auraInstanceID)
                if ok and aura and AuraSpellIs(aura, VOID_SHIELD_ID) then
                    valid = true
                end
            end
        end
        if not valid then
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
            voidShieldGlowFrames[buffFrame] = nil
        end
    end
end

-- =============================================================
-- EvaluateFrameGlow (called from hook pipeline)
-- =============================================================
function PriestDisc:EvaluateFrameGlow(db, buffFrame, aura)
    if not db or not db.glowEnabled or not db.enabled then
        if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
        atonGlowFrames[buffFrame]      = nil
        voidShieldGlowFrames[buffFrame] = nil
        return
    end

    local tracked = atonGlowFrames[buffFrame] or voidShieldGlowFrames[buffFrame]

    if not aura then
        if tracked then
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
            atonGlowFrames[buffFrame]      = nil
            voidShieldGlowFrames[buffFrame] = nil
        end
        return
    end

    -- Use taint-safe spell ID extraction
    local spellId = SafeGetSpellId(aura)

    if not spellId then
        -- Spell ID is tainted/secret — can't identify this aura
        if tracked then
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
            atonGlowFrames[buffFrame]      = nil
            voidShieldGlowFrames[buffFrame] = nil
        end
        return
    end

    -- Source check: must be player's aura (except Void Shield)
    if aura.sourceUnit ~= "player" then
        if spellId == VOID_SHIELD_ID then
            -- Void Shield: accept any source (sourceUnit may be tainted)
        else
            if tracked then
                if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
                atonGlowFrames[buffFrame]      = nil
                voidShieldGlowFrames[buffFrame] = nil
            end
            return
        end
    end

    -- ── Atonement pandemic glow ──────────────────────────────
    if spellId == ATONEMENT_ID then
        -- Clear Void Shield tracking on this frame if present
        if voidShieldGlowFrames[buffFrame] then
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
            voidShieldGlowFrames[buffFrame] = nil
        end
        -- Store safe aura copy (matches Druid _safeNHAAura pattern)
        if not buffFrame._safeNHAAura then buffFrame._safeNHAAura = {} end
        local sa = buffFrame._safeNHAAura
        sa.auraInstanceID = aura.auraInstanceID
        sa.expirationTime = aura.expirationTime or 0
        sa.duration       = aura.duration or 0
        sa.timeMod        = (aura.timeMod and aura.timeMod > 0) and aura.timeMod or 1
        atonGlowFrames[buffFrame] = sa

        local now       = GetTime()
        local remaining = (sa.expirationTime - now) / sa.timeMod
        if remaining > 0 and remaining <= ATONEMENT_PANIC_THRESHOLD then
            local c = db.alertColors.aton
            NHA:ShowProcGlow(buffFrame, c.r, c.g, c.b)
        else
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
        end
        return
    end

    -- ── Void Shield absorb glow ──────────────────────────────
    if spellId == VOID_SHIELD_ID then
        -- Clear Atonement tracking on this frame if present
        if atonGlowFrames[buffFrame] then
            if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
            atonGlowFrames[buffFrame] = nil
        end
        voidShieldGlowFrames[buffFrame] = true
        local c = db.alertColors and db.alertColors.vs
                  or { r=0.6, g=0.2, b=0.8 }
        NHA:ShowProcGlow(buffFrame, c.r, c.g, c.b)
        return
    end

    -- ── Not a spell we care about ────────────────────────────
    if tracked then
        if buffFrame._nhaGlowing then NHA:HideProcGlow(buffFrame) end
        atonGlowFrames[buffFrame]      = nil
        voidShieldGlowFrames[buffFrame] = nil
    end
end

-- =============================================================
-- Evaluate (called by core ticker and events)
-- =============================================================
function PriestDisc:Evaluate(db)
    -- ── Atonement pandemic check ─────────────────────────────
    -- Always find the soonest-expiring Atonement across all group
    -- members. If we have a tracked unit, verify it's still valid
    -- first, then check if another unit is more urgent.
    local now = GetTime()

    if state.atonUnit and state.atonInstID then
        -- Verify tracked aura still exists
        local ok, aura = pcall(C_UnitAuras.GetAuraDataByAuraInstanceID,
            state.atonUnit, state.atonInstID)
        if not ok or not aura or not AuraSpellIs(aura, ATONEMENT_ID)
            or aura.sourceUnit ~= "player" then
            -- Tracked aura gone — clear and re-scan
            ClearAtonTracking()
        else
            StoreAtonAura(state.atonUnit, aura)
        end
    end

    -- Check if tracked unit is still the most urgent, or if we
    -- need to find a new one (including when tracking was just cleared)
    local trackedRemaining = math.huge
    if state.atonUnit then
        trackedRemaining = (state.atonExpire - now) / state.atonTimeMod
    end

    -- Re-scan if: no tracked unit, tracked expired, or tracked
    -- is above panic threshold (a more urgent one might exist)
    if not state.atonUnit
        or trackedRemaining <= 0
        or trackedRemaining > ATONEMENT_PANIC_THRESHOLD then
        local u, a = FindSoonestAtonement()
        if u and a then
            StoreAtonAura(u, a)
        elseif not state.atonUnit then
            ClearAtonTracking()
        end
    end

    local atonPanic = false
    if state.atonUnit then
        local remaining = (state.atonExpire - now) / state.atonTimeMod
        if remaining > 0 then
            if db.atonEnabled then
                atonPanic = remaining <= ATONEMENT_PANIC_THRESHOLD
            end
        else
            ClearAtonTracking()
        end
    end
    state.atonActive = atonPanic

    -- ── Void Shield proc check ───────────────────────────────
    local vsProc = false
    if db.vsEnabled then
        vsProc = CheckVoidShieldProc()
    end
    state.vsActive = vsProc

    -- ── Rising-edge sounds ───────────────────────────────────
    if atonPanic and not state.atonWasActive then NHA:PlayAlertSound("aton") end
    if vsProc    and not state.vsWasActive   then NHA:PlayAlertSound("vs")   end
    state.atonWasActive = atonPanic
    state.vsWasActive   = vsProc
end

-- =============================================================
-- OnActivate
-- =============================================================
function PriestDisc:OnActivate(db)
    NHA.ScheduleEvaluate()
end

-- =============================================================
-- OnDeactivate
-- =============================================================
function PriestDisc:OnDeactivate(db)
    -- State is cleared by core calling ClearAllState + ClearAllGlows
end

-- =============================================================
-- OnEvent
-- =============================================================
function PriestDisc:OnEvent(db, event, ...)
    if event == "UNIT_AURA" then
        local unit, updateInfo = ...

        -- Player auras (Master the Darkness proc)
        if unit == "player" then
            if not updateInfo or updateInfo.isFullUpdate
               or updateInfo.addedAuras or updateInfo.removedAuraInstanceIDs then
                if not state.playerAuraPending then
                    state.playerAuraPending = true
                    C_Timer.After(0.1, function()
                        state.playerAuraPending = false
                        NHA.Evaluate()
                    end)
                end
            end
            return true
        end

        -- Atonement on tracked unit (fast-path removal)
        if unit == state.atonUnit then
            if not updateInfo or updateInfo.isFullUpdate then
                NHA.ScheduleEvaluate(); return true
            end
            if updateInfo.removedAuraInstanceIDs and state.atonInstID then
                for _, instID in ipairs(updateInfo.removedAuraInstanceIDs) do
                    if instID == state.atonInstID then
                        ClearAtonTracking()
                        NHA.ScheduleEvaluate()
                        return true
                    end
                end
            end
            if updateInfo.updatedAuraInstanceIDs and state.atonInstID then
                for _, instID in ipairs(updateInfo.updatedAuraInstanceIDs) do
                    if instID == state.atonInstID then
                        NHA.ScheduleEvaluate(); return true
                    end
                end
            end
            if updateInfo.addedAuras then
                for _, aura in ipairs(updateInfo.addedAuras) do
                    if AuraSpellIs(aura, ATONEMENT_ID)
                        and aura.sourceUnit == "player" then
                        NHA.ScheduleEvaluate(); return true
                    end
                end
            end
            return true  -- handled, no default ScheduleEvaluate
        end

        -- Other units: Atonement not yet tracked — scan for new
        if not state.atonUnit and not state.atonScanPending then
            if not updateInfo or updateInfo.isFullUpdate or updateInfo.addedAuras then
                state.atonScanPending = true
                C_Timer.After(0.1, function()
                    state.atonScanPending = false
                    NHA.Evaluate()
                end)
            end
        elseif state.atonUnit then
            -- Already tracking a unit, but another unit got an aura change.
            -- Debounced re-evaluate to pick up a more urgent Atonement.
            if not state.atonAuraPending then
                state.atonAuraPending = true
                C_Timer.After(0.15, function()
                    state.atonAuraPending = false
                    NHA.Evaluate()
                end)
            end
        end
        return true
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit, _, spellID = ...
        if unit == "player" then
            local ok, match = pcall(function() return spellID == PWS_ID end)
            if ok and match then
                -- PW:S consumes the Master the Darkness proc
                NHA.ScheduleEvaluate()
            end
        end
        return true
    end

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

    if event == "PLAYER_ENTERING_WORLD" then
        ClearAtonTracking()
        return false  -- let core handle
    end

    if event == "PLAYER_REGEN_ENABLED" then
        -- Combat ended; Atonements drop quickly, clear stale tracking
        ClearAtonTracking()
        NHA.ScheduleEvaluate()
        return false  -- let core handle
    end

    return false
end

-- =============================================================
-- BuildOptions (spec-specific options in the Display section)
-- =============================================================
function PriestDisc:BuildOptions(parent, db, helpers)
    helpers.MakeCheckbox("Atonement Timer", 160, -90,
        function() return db.atonTimerShown end,
        function(v)
            db.atonTimerShown = v and true or false
            if not db.atonTimerShown then
                local f = NHA.display
                if f then
                    for _, row in ipairs(f.alertRows) do
                        if row.key == "aton" and row.timerFS then
                            row.timerFS:Hide()
                        end
                    end
                end
            end
        end)
end

-- =============================================================
-- Slash Commands (spec-specific)
-- =============================================================
PriestDisc.slashCommands = {
    ["debug"] = function(self, db)
        NHA.Msg("|cffffd700Disc Priest Debug:|r")
        NHA.Msg("  enabled: " .. tostring(db.enabled))
        NHA.Msg("  atonEnabled: " .. tostring(db.atonEnabled))
        NHA.Msg("  vsEnabled: " .. tostring(db.vsEnabled))
        NHA.Msg("  glowEnabled: " .. tostring(db.glowEnabled))
        NHA.Msg("  atonActive: " .. tostring(state.atonActive))
        NHA.Msg("  vsActive: " .. tostring(state.vsActive))
        NHA.Msg("  atonUnit: " .. tostring(state.atonUnit))
        NHA.Msg("  atonInstID: " .. tostring(state.atonInstID))
        NHA.Msg("  atonExpire: " .. string_format("%.1f", state.atonExpire))

        -- Glow frame counts
        local atonGlowCount = 0
        for _ in pairs(atonGlowFrames) do atonGlowCount = atonGlowCount + 1 end
        local vsGlowCount = 0
        for _ in pairs(voidShieldGlowFrames) do vsGlowCount = vsGlowCount + 1 end
        NHA.Msg("  atonGlowFrames: " .. atonGlowCount)
        NHA.Msg("  voidShieldGlowFrames: " .. vsGlowCount)

        -- Test MTD proc detection via direct API
        local mtdResult = "nil"
        if C_UnitAuras and C_UnitAuras.GetAuraDataBySpellID then
            local ok, aura = pcall(C_UnitAuras.GetAuraDataBySpellID,
                "player", MTD_PROC_ID, "HELPFUL")
            if ok and aura then
                mtdResult = "found (instID=" .. tostring(aura.auraInstanceID)
                    .. " spellId=" .. tostring(aura.spellId) .. ")"
            elseif ok then
                mtdResult = "not found"
            else
                mtdResult = "error: " .. tostring(aura)
            end
        else
            mtdResult = "API missing"
        end
        NHA.Msg("  MTD direct API: " .. mtdResult)

        -- Test PW:S spell texture (primary detection method)
        local texResult = "unavailable"
        if C_Spell and C_Spell.GetSpellTexture then
            local ok, tex = pcall(C_Spell.GetSpellTexture, PWS_ID)
            if ok then
                local isNormal = PWS_NORMAL_ICONS[tex] and "YES" or "NO"
                texResult = tostring(tex) .. " normal=" .. isNormal
            else
                texResult = "error: " .. tostring(tex)
            end
        elseif GetSpellTexture then
            local ok, tex = pcall(GetSpellTexture, PWS_ID)
            if ok then
                local isNormal = PWS_NORMAL_ICONS[tex] and "YES" or "NO"
                texResult = tostring(tex) .. " normal=" .. isNormal
                    .. " (legacy API)"
            else
                texResult = "error: " .. tostring(tex)
            end
        end
        NHA.Msg("  PW:S texture: " .. texResult)

        -- Test CheckVoidShieldProc directly
        local vsOk, vsResult = pcall(CheckVoidShieldProc)
        NHA.Msg("  CheckVoidShieldProc: ok=" .. tostring(vsOk)
            .. " result=" .. tostring(vsResult))

        -- Dump player aura index scan for MTD diagnosis
        NHA.Msg("|cffffd700  Player Aura Scan:|r")
        if C_UnitAuras and C_UnitAuras.GetAuraDataByIndex then
            local idx = 1
            local found = 0
            while true do
                local ok2, aura2 = pcall(C_UnitAuras.GetAuraDataByIndex,
                    "player", idx, "HELPFUL")
                if not ok2 or not aura2 then break end
                local iconId = nil
                -- All aura fields may be tainted; wrap everything in pcall
                local sidStr = "(tainted)"
                local isSecret = false
                local isMtdSpell = false
                local isIconMatch = false
                local auraName = "(tainted)"
                pcall(function() iconId = aura2.icon end)
                pcall(function() auraName = tostring(aura2.name) end)
                pcall(function()
                    sidStr = tostring(aura2.spellId)
                    isSecret = issecretvalue(aura2.spellId)
                end)
                pcall(function()
                    isMtdSpell = (aura2.spellId == MTD_PROC_ID)
                end)
                pcall(function()
                    isIconMatch = (iconId and iconId == MTD_ICON_ID)
                end)
                -- Build tag string
                local tag = ""
                if isSecret then tag = " |cffff4444[SECRET]|r" end
                if isMtdSpell then tag = tag .. " |cff00ff00[SPELL MATCH]|r" end
                if isIconMatch then tag = tag .. " |cff00ffff[ICON MATCH]|r" end
                NHA.Msg("    #" .. idx .. ": spell="
                    .. sidStr .. " icon=" .. tostring(iconId)
                    .. " name=" .. auraName .. tag)
                found = found + 1
                if found >= 30 then
                    NHA.Msg("    ... (capped at 30)")
                    break
                end
                idx = idx + 1
            end
            if found == 0 then
                NHA.Msg("    (no buffs found)")
            end
        else
            NHA.Msg("    GetAuraDataByIndex not available")
        end
    end,
}

-- =============================================================
-- Register with core
-- =============================================================
NHA:RegisterSpec("PRIEST_DISC", PriestDisc)
