-- =============================================================
-- Specs/DruidRestoration.lua
-- Nem: Healer Alerts — Restoration Druid spec module
--
-- FULLY SELF-CONTAINED. This file contains ALL Resto Druid
-- 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 UnitAffectingCombat = UnitAffectingCombat
local C_UnitAuras         = C_UnitAuras
local issecretvalue       = NHA.issecretvalue

-- =============================================================
-- Spell / Aura IDs
-- =============================================================
local LIFEBLOOM_IDS = {
    [33763]  = true,   -- Lifebloom
}

local REJUV_IDS = {
    [774]    = true,   -- Rejuvenation
    [155777] = true,   -- Germination
}

local SOTF_EMPOWERED_ICONS = {
    [7195165] = true,  -- Rejuvenation (empowered)
    [1033486] = true,  -- Germination  (empowered)
}

local SPELL_SOTF          = 114108
local LB_PANIC_THRESHOLD  = 4.5
local WILD_GROWTH_ID      = 48438
local REFORESTATION_BUFF_ID   = 392360
local REFORESTATION_NODE_ID   = 82069
local REGROWTH_ID         = 8936
local SWIFTMEND_ID        = 18562
local SOTF_CONSUMER_IDS   = { [774]=true, [155777]=true, [8936]=true }
local SOTF_BUFF_DURATION  = 15.0

-- =============================================================
-- Module State (owned entirely by this module)
-- =============================================================
local state = {
    -- Lifebloom
    lbUnit         = nil,
    lbInstID       = nil,
    lbExpire       = 0,
    lbDuration     = 0,
    lbTimeMod      = 1,
    lbActive       = false,   -- "Active" = in pandemic window
    lbWasActive    = false,
    lbScanPending       = false,
    playerAuraPending   = false,
    rosterUpdatePending = false,
    -- SOTF
    sotfActive      = false,
    sotfWasActive   = false,
    sotfExpire      = 0,
    sotfTimeMod     = 1,
    sotfProcPending = false,
    -- Wild Growth
    wgActive      = false,   -- "Active" = ready to cast
    wgWasReady    = false,
    wgCDTimerSet  = false,
    wgJustCast    = false,
    wgOnRealCD    = false,
    wgInitialized = false,
    wgCastTime    = 0,
    -- Reforestation
    refoActive    = false,
    refoWasActive = false,
    hasRefoTalent  = false,
    swiftmendCount = 0,
    -- Empowered Regrowth glow
    empRegrowthPending  = false,
    empRegrowthDeadline = 0,
    empRegrowthInstIDs  = {},
    -- Debug
    _debugTrace = false,
}

-- =============================================================
-- Glow Frame Tables (owned entirely by this module)
-- =============================================================
local lbGlowFrames       = {}
local rejuvGlowFrames    = {}
local regrowthGlowFrames = {}

-- =============================================================
-- Alert Definitions
-- =============================================================
-- Display Y offsets (bottom-to-top: LB, SOTF, WG, Refo)
local REFO_Y       =  22
local WG_Y         =  -2
local SOTF_Y       = -30
local SOTF_TIMER_Y = -50
local LB_TEXT_Y    = -76
local LB_TIMER_Y   = -96

local ALERTS = {
    {
        key          = "lb",
        label        = "Lifebloom Expiring",
        text         = "* LIFEBLOOM EXPIRING *",
        defaultColor = { r = 0.141, g = 0.631, b = 0.282 },  -- #24A148
        hasTimer     = true,
        timerCycleSec = 4.5,
        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      = LB_TEXT_Y,
        timerYOffset = LB_TIMER_Y,
        defaultSound = "Bell",
    },
    {
        key          = "sotf",
        label        = "Soul of the Forest",
        text         = "* SOUL OF THE FOREST *",
        defaultColor = { r = 0.0,   g = 0.898, b = 0.753 },  -- #00E5C0
        hasTimer     = true,
        timerCycleSec = 15.0,
        timerThresholds = {
            { 10.0, 0.667, 1.0, 0.0 },   -- > 10s: #aaff00
            { 5.0,  1.0, 0.816, 0.0 },    -- > 5s:  #ffd000
            { 0,    1.0, 0.4,   0.0 },    -- ≤ 5s:  #ff6600
        },
        yOffset      = SOTF_Y,
        timerYOffset = SOTF_TIMER_Y,
        defaultSound = "Ping",
    },
    {
        key          = "wg",
        label        = "Wild Growth Ready",
        text         = "* WILD GROWTH READY *",
        defaultColor = { r = 0.651, g = 0.886, b = 0.180 },  -- #A6E22E
        hasTimer     = false,
        yOffset      = WG_Y,
        defaultSound = "Lamp",
    },
    {
        key          = "refo",
        label        = "Tree of Life Ready",
        text         = "* TREE OF LIFE NEXT SWIFTMEND *",
        defaultColor = { r = 0.204, g = 0.596, b = 0.859 },  -- #3498DB
        hasTimer     = false,
        yOffset      = REFO_Y,
        defaultSound = "Redfox",
    },
}

-- =============================================================
-- Saved-Variable Defaults (merged into core db)
-- =============================================================
local DEFAULTS = {
    lbEnabled        = true,
    sotfEnabled      = true,
    lbTimerShown     = true,
    sotfTimerShown   = true,
    lbSoundEnabled      = true,
    lbSoundName         = "Bell",
    sotfSoundEnabled    = true,
    sotfSoundName       = "Ping",
    wgEnabled           = true,
    wgSoundEnabled      = true,
    wgSoundName         = "Lamp",
    refoEnabled         = true,
    refoSoundEnabled    = true,
    refoSoundName       = "Redfox",
    empRegrowthEnabled  = true,
    alertColors         = {
        lb   = { r = 0.141, g = 0.631, b = 0.282 },
        sotf = { r = 0.0,   g = 0.898, b = 0.753 },
        wg   = { r = 0.651, g = 0.886, b = 0.180 },
        refo = { r = 0.204, g = 0.596, b = 0.859 },
    },
}

-- =============================================================
-- Utility: Reforestation talent check
-- =============================================================
local function CheckRefoTalent()
    local configID = C_ClassTalents and C_ClassTalents.GetActiveConfigID
                     and C_ClassTalents.GetActiveConfigID()
    if not configID then return false end
    local nodeInfo = C_Traits and C_Traits.GetNodeInfo
                     and C_Traits.GetNodeInfo(configID, REFORESTATION_NODE_ID)
    if not nodeInfo then return false end
    if nodeInfo.ranksPurchased and nodeInfo.ranksPurchased > 0 then return true end
    if nodeInfo.activeRank     and nodeInfo.activeRank > 0     then return true end
    return false
end

local function SyncSwiftmendCount()
    if not C_UnitAuras or not C_UnitAuras.GetAuraDataBySpellID then return end
    local aura = C_UnitAuras.GetAuraDataBySpellID("player", REFORESTATION_BUFF_ID, "HELPFUL")
    if aura and not issecretvalue(aura.applications) then
        state.swiftmendCount = aura.applications or 0
    elseif not aura then
        state.swiftmendCount = 0
    end
end

-- =============================================================
-- Lifebloom Scan
-- =============================================================
local function ScanUnitForLB(unit)
    if not UnitExists(unit) then return nil end
    if C_UnitAuras and C_UnitAuras.GetAuraDataBySpellID then
        for spellId in pairs(LIFEBLOOM_IDS) do
            local aura = C_UnitAuras.GetAuraDataBySpellID(unit, spellId, "HELPFUL")
            if aura and aura.auraInstanceID ~= 0
                and aura.sourceUnit == "player" then
                return aura
            end
        end
        return nil
    end
    local i = 1
    while true do
        local aura = C_UnitAuras.GetAuraDataByIndex(unit, i, "HELPFUL")
        if not aura then break end
        if issecretvalue(aura.spellId) then
            -- skip
        elseif LIFEBLOOM_IDS[aura.spellId]
            and aura.sourceUnit == "player" then
            return aura
        end
        i = i + 1
    end
    return nil
end

local function FullScanForLB()
    for _, unit in ipairs(NHA:GetGroupUnits()) do
        local aura = ScanUnitForLB(unit)
        if aura then return unit, aura end
    end
    return nil, nil
end

local function StoreLBAura(unit, aura)
    state.lbUnit     = unit
    state.lbInstID   = aura.auraInstanceID
    state.lbExpire   = aura.expirationTime or 0
    state.lbDuration = aura.duration or 0
    state.lbTimeMod  = (aura.timeMod and aura.timeMod > 0) and aura.timeMod or 1
end

local function ClearLBTracking()
    state.lbUnit     = nil
    state.lbInstID   = nil
    state.lbExpire   = 0
    state.lbDuration = 0
    state.lbTimeMod  = 1
end

-- =============================================================
-- SOTF Detection
-- =============================================================
local function CheckSOTF()
    if not C_UnitAuras then return nil end
    if C_UnitAuras.GetAuraDataBySpellID then
        local aura = C_UnitAuras.GetAuraDataBySpellID("player", SPELL_SOTF, "HELPFUL")
        if aura and aura.auraInstanceID and aura.auraInstanceID ~= 0 then
            return aura
        end
        return nil
    end
    local i = 1
    while true do
        local aura = C_UnitAuras.GetAuraDataByIndex("player", i, "HELPFUL")
        if not aura then break end
        if not issecretvalue(aura.spellId) and aura.spellId == SPELL_SOTF then
            return aura
        end
        i = i + 1
    end
    return nil
end

-- =============================================================
-- Empowered Regrowth Active Scan
-- =============================================================
local function ScanForEmpoweredRegrowth(db)
    if not db or not db.empRegrowthEnabled or not db.glowEnabled then return end
    if not state.empRegrowthPending then return end
    if GetTime() > state.empRegrowthDeadline then return end

    local GlobalAuras = _G.C_UnitAuras
    if not GlobalAuras or not GlobalAuras.GetAuraDataByAuraInstanceID then return end

    local function ScanFramePool(frame)
        if not frame or not frame.auraPools or not frame.unit then return end
        for buffFrame in frame.auraPools:EnumerateActive() do
            if buffFrame.auraInstanceID then
                local ok, aura = pcall(GlobalAuras.GetAuraDataByAuraInstanceID,
                    frame.unit, buffFrame.auraInstanceID)
                if ok and aura and aura.spellId
                   and not issecretvalue(aura.spellId)
                   and aura.spellId == REGROWTH_ID
                   and aura.sourceUnit == "player" then
                    local instID = aura.auraInstanceID
                    if instID and instID ~= 0 then
                        state.empRegrowthInstIDs[instID] = true
                        regrowthGlowFrames[buffFrame] = instID
                        local sc = db.alertColors.sotf
                        NHA:ShowProcGlow(buffFrame, sc.r, sc.g, sc.b)
                    end
                end
            end
        end
    end

    ScanFramePool(TargetFrame)
    ScanFramePool(FocusFrame)

    if CompactRaidFrameContainer then
        local ok, children = pcall(function()
            return { CompactRaidFrameContainer:GetChildren() }
        end)
        if ok and children then
            for _, unitFrame in ipairs(children) do
                if unitFrame.buffFrames then
                    for _, buffFrame in ipairs(unitFrame.buffFrames) do
                        if buffFrame:IsShown() and buffFrame.auraInstanceID then
                            local unit = unitFrame.unit
                            if unit then
                                local ok2, aura = pcall(
                                    GlobalAuras.GetAuraDataByAuraInstanceID,
                                    unit, buffFrame.auraInstanceID)
                                if ok2 and aura and aura.spellId
                                   and not issecretvalue(aura.spellId)
                                   and aura.spellId == REGROWTH_ID
                                   and aura.sourceUnit == "player" then
                                    local instID = aura.auraInstanceID
                                    if instID and instID ~= 0 then
                                        state.empRegrowthInstIDs[instID] = true
                                        regrowthGlowFrames[buffFrame] = instID
                                        local sc = db.alertColors.sotf
                                        NHA:ShowProcGlow(buffFrame,
                                            sc.r, sc.g, sc.b)
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end

    if state._debugTrace then
        local instCount = 0
        for _ in pairs(state.empRegrowthInstIDs) do instCount = instCount + 1 end
        local frameCount = 0
        for _ in pairs(regrowthGlowFrames) do frameCount = frameCount + 1 end
        NHA.Msg("|cff00ff00[TRACE]|r Scan complete. Tagged " .. instCount
            .. " instance(s), " .. frameCount .. " frame(s) glowing.")
        state._debugTrace = false
    end
end

-- =============================================================
-- MODULE TABLE
-- =============================================================
local DruidResto = {
    class     = "DRUID",
    specIndex = 4,
    specName  = "Restoration",
    alerts    = ALERTS,
    defaults  = DEFAULTS,
    state     = state,

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

-- =============================================================
-- ClearAllGlows
-- =============================================================
function DruidResto:ClearAllGlows()
    for buffFrame in pairs(lbGlowFrames) do
        NHA:HideProcGlow(buffFrame)
    end
    wipe(lbGlowFrames)
    for buffFrame in pairs(rejuvGlowFrames) do
        NHA:HideProcGlow(buffFrame)
    end
    wipe(rejuvGlowFrames)
    for buffFrame in pairs(regrowthGlowFrames) do
        NHA:HideProcGlow(buffFrame)
    end
    wipe(regrowthGlowFrames)
    state.empRegrowthPending  = false
    state.empRegrowthDeadline = 0
    wipe(state.empRegrowthInstIDs)
end

-- =============================================================
-- ClearAllState
-- =============================================================
function DruidResto:ClearAllState()
    ClearLBTracking()
    state.lbActive           = false
    state.lbWasActive        = false
    state.sotfActive         = false
    state.sotfWasActive      = false
    state.sotfProcPending    = false
    state.sotfExpire         = 0
    state.sotfTimeMod        = 1
    state.wgActive           = false
    state.wgWasReady         = false
    state.wgOnRealCD         = false
    state.wgJustCast         = false
    state.wgCDTimerSet       = false
    state.wgCastTime         = 0
    state.refoActive         = false
    state.refoWasActive      = false
    state.swiftmendCount     = 0
    state.empRegrowthPending = false
    state.empRegrowthDeadline = 0
    wipe(state.empRegrowthInstIDs)
    self:ClearAllGlows()
end

-- =============================================================
-- NeedsTicker — keep ticker alive when LB is tracked (for glows)
-- =============================================================
function DruidResto:NeedsTicker(db)
    return state.lbUnit ~= nil
end

-- =============================================================
-- RefreshGlows (called every 0.25s by ticker)
-- =============================================================
function DruidResto:RefreshGlows(db)
    if not db or not db.glowEnabled then return end
    local now = GetTime()
    local c   = db.alertColors and db.alertColors.lb
                or { r=0.141, g=0.631, b=0.282 }
    for buffFrame, aura in pairs(lbGlowFrames) do
        if buffFrame.auraInstanceID ~= aura.auraInstanceID then
            NHA:HideProcGlow(buffFrame)
            lbGlowFrames[buffFrame] = nil
        else
            local remaining = (aura.expirationTime - now) / aura.timeMod
            if remaining > 0 and remaining <= LB_PANIC_THRESHOLD then
                NHA:ShowProcGlow(buffFrame, c.r, c.g, c.b)
            elseif remaining <= 0 then
                NHA:HideProcGlow(buffFrame)
                lbGlowFrames[buffFrame] = nil
            else
                NHA:HideProcGlow(buffFrame)
            end
        end
    end
end

-- =============================================================
-- EvaluateFrameGlow (called from hook pipeline)
-- =============================================================
function DruidResto:EvaluateFrameGlow(db, buffFrame, aura)
    if not db or not db.glowEnabled or not db.enabled then
        NHA:HideProcGlow(buffFrame)
        lbGlowFrames[buffFrame]       = nil
        rejuvGlowFrames[buffFrame]    = nil
        regrowthGlowFrames[buffFrame] = nil
        return
    end

    local tracked = lbGlowFrames[buffFrame] or rejuvGlowFrames[buffFrame]
                    or regrowthGlowFrames[buffFrame]
    if not aura then
        if tracked then
            NHA:HideProcGlow(buffFrame)
            lbGlowFrames[buffFrame]       = nil
            rejuvGlowFrames[buffFrame]    = nil
            regrowthGlowFrames[buffFrame] = nil
        end
        return
    end

    if issecretvalue(aura.spellId) then
        if tracked then
            NHA:HideProcGlow(buffFrame)
            lbGlowFrames[buffFrame]       = nil
            rejuvGlowFrames[buffFrame]    = nil
            regrowthGlowFrames[buffFrame] = nil
        end
        return
    end

    if aura.sourceUnit ~= "player" then
        if tracked then
            NHA:HideProcGlow(buffFrame)
            lbGlowFrames[buffFrame]       = nil
            rejuvGlowFrames[buffFrame]    = nil
            regrowthGlowFrames[buffFrame] = nil
        end
        return
    end

    -- Lifebloom
    if LIFEBLOOM_IDS[aura.spellId] then
        if rejuvGlowFrames[buffFrame] then
            NHA:HideProcGlow(buffFrame)
            rejuvGlowFrames[buffFrame] = nil
        end
        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
        lbGlowFrames[buffFrame] = sa

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

    -- Empowered Rejuv / Germination
    if REJUV_IDS[aura.spellId]
        and SOTF_EMPOWERED_ICONS[aura.icon] then
        if lbGlowFrames[buffFrame] then
            NHA:HideProcGlow(buffFrame)
            lbGlowFrames[buffFrame] = nil
        end
        if regrowthGlowFrames[buffFrame] then
            NHA:HideProcGlow(buffFrame)
            regrowthGlowFrames[buffFrame] = nil
        end
        rejuvGlowFrames[buffFrame] = true
        local c = db.alertColors.sotf
        NHA:ShowProcGlow(buffFrame, c.r, c.g, c.b)
        return
    end

    -- Empowered Regrowth
    if aura.spellId == REGROWTH_ID and db.empRegrowthEnabled then
        local instID = aura.auraInstanceID
        if instID and instID ~= 0
           and state.empRegrowthPending
           and GetTime() <= state.empRegrowthDeadline
           and not state.empRegrowthInstIDs[instID] then
            state.empRegrowthInstIDs[instID] = true
        end
        local isEmpowered = instID and state.empRegrowthInstIDs[instID]
        if isEmpowered or (instID and regrowthGlowFrames[buffFrame] == instID) then
            if lbGlowFrames[buffFrame] then
                NHA:HideProcGlow(buffFrame)
                lbGlowFrames[buffFrame] = nil
            end
            if rejuvGlowFrames[buffFrame] then
                NHA:HideProcGlow(buffFrame)
                rejuvGlowFrames[buffFrame] = nil
            end
            regrowthGlowFrames[buffFrame] = instID
            local sc = db.alertColors.sotf
            NHA:ShowProcGlow(buffFrame, sc.r, sc.g, sc.b)
            return
        end
        if regrowthGlowFrames[buffFrame] and regrowthGlowFrames[buffFrame] ~= instID then
            NHA:HideProcGlow(buffFrame)
            regrowthGlowFrames[buffFrame] = nil
        end
    end

    -- Not a spell we care about
    NHA:HideProcGlow(buffFrame)
    lbGlowFrames[buffFrame]       = nil
    rejuvGlowFrames[buffFrame]    = nil
    regrowthGlowFrames[buffFrame] = nil
end

-- =============================================================
-- Evaluate (called by core ticker and events)
-- =============================================================
function DruidResto:Evaluate(db)
    -- ── Lifebloom pandemic check ─────────────────────────────
    if state.lbUnit and state.lbInstID then
        local aura = C_UnitAuras.GetAuraDataByAuraInstanceID(
            state.lbUnit, state.lbInstID)
        if aura and not issecretvalue(aura.spellId)
            and LIFEBLOOM_IDS[aura.spellId] and aura.sourceUnit == "player" then
            StoreLBAura(state.lbUnit, aura)
        else
            local u, a = FullScanForLB()
            if u and a then StoreLBAura(u, a) else ClearLBTracking() end
        end
    else
        local u, a = FullScanForLB()
        if u and a then StoreLBAura(u, a) else ClearLBTracking() end
    end

    local lbPanic = false
    if state.lbUnit then
        local remaining = (state.lbExpire - GetTime()) / state.lbTimeMod
        if remaining > 0 then
            if db.lbEnabled then
                lbPanic = remaining <= LB_PANIC_THRESHOLD
            end
        else
            ClearLBTracking()
        end
    end
    state.lbActive = lbPanic
    -- Expose for OnUpdate timer
    state.lbExpire_  = state.lbExpire
    state.lbTimeMod_ = state.lbTimeMod

    -- ── SOTF check ───────────────────────────────────────────
    local sotfActive = false
    if db.sotfEnabled then
        local sotfAura = CheckSOTF()
        if sotfAura then
            sotfActive = true
            state.sotfProcPending = false
            state.sotfExpire  = sotfAura.expirationTime or 0
            state.sotfTimeMod = (sotfAura.timeMod and sotfAura.timeMod > 0)
                                and sotfAura.timeMod or 1
        elseif state.sotfProcPending then
            if state.sotfExpire > 0 and GetTime() > state.sotfExpire then
                state.sotfProcPending = false
            else
                sotfActive = true
            end
        else
            state.sotfExpire  = 0
            state.sotfTimeMod = 1
        end
    end
    state.sotfActive = sotfActive
    if not sotfActive then
        state.sotfExpire  = 0
        state.sotfTimeMod = 1
    end

    -- ── Wild Growth cooldown check ────────────────────────────
    local wgReady = false
    if db.wgEnabled and UnitAffectingCombat("player") then
        local cdInfo = C_Spell.GetSpellCooldown(WILD_GROWTH_ID)
        local start = cdInfo and cdInfo.startTime or 0
        local dur   = cdInfo and cdInfo.duration  or 0
        if issecretvalue(start) or issecretvalue(dur) then
            if state.wgJustCast then
                wgReady = false; state.wgJustCast = false; state.wgOnRealCD = true
            elseif state.wgOnRealCD then
                if state.wgCastTime > 0
                   and (GetTime() - state.wgCastTime) >= 10.0 then
                    wgReady = true
                else
                    wgReady = false
                end
            else
                wgReady = true
            end
        elseif state.wgJustCast then
            wgReady = false; state.wgJustCast = false; state.wgOnRealCD = true
        elseif dur == 0 or (start > 0 and dur > 0 and dur <= 1.5) then
            wgReady = true
        elseif start > 0 and dur > 1.5 then
            state.wgOnRealCD = true
            local remaining = (start + dur) - GetTime()
            if remaining > 0 and not state.wgCDTimerSet then
                state.wgCDTimerSet = true
                C_Timer.After(remaining + 0.05, function()
                    state.wgCDTimerSet = false
                    NHA.ScheduleEvaluate()
                end)
            end
        end
    end
    state.wgActive = wgReady

    -- ── Reforestation check ────────────────────────────────────
    state.refoActive = db.refoEnabled and state.hasRefoTalent
                       and (state.swiftmendCount == 3)

    -- ── Empowered Regrowth deadline expiry ───────────────────
    if state.empRegrowthPending
       and GetTime() > state.empRegrowthDeadline then
        state.empRegrowthPending = false
    end

    -- ── Rising-edge sounds ───────────────────────────────────
    if lbPanic    and not state.lbWasActive   then NHA:PlayAlertSound("lb")   end
    if sotfActive and not state.sotfWasActive then NHA:PlayAlertSound("sotf") end
    if wgReady and not state.wgWasReady
       and state.wgOnRealCD and state.wgInitialized then
        NHA:PlayAlertSound("wg"); state.wgOnRealCD = false
    end
    if not wgReady then state.wgInitialized = true end
    if state.refoActive and not state.refoWasActive then NHA:PlayAlertSound("refo") end
    state.lbWasActive   = lbPanic
    state.sotfWasActive = sotfActive
    state.wgWasReady    = wgReady
    state.refoWasActive = state.refoActive
end

-- =============================================================
-- OnActivate
-- =============================================================
function DruidResto:OnActivate(db)
    state.hasRefoTalent = CheckRefoTalent()
    SyncSwiftmendCount()
end

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

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

        -- Empowered Regrowth cleanup
        if updateInfo and updateInfo.removedAuraInstanceIDs
           and next(state.empRegrowthInstIDs) then
            for _, instID in ipairs(updateInfo.removedAuraInstanceIDs) do
                if state.empRegrowthInstIDs[instID] then
                    state.empRegrowthInstIDs[instID] = nil
                    for buffFrame, trackedID in pairs(regrowthGlowFrames) do
                        if trackedID == instID then
                            NHA:HideProcGlow(buffFrame)
                            regrowthGlowFrames[buffFrame] = nil
                        end
                    end
                end
            end
        end

        -- LB target
        if unit == state.lbUnit then
            if not updateInfo or updateInfo.isFullUpdate then
                NHA.ScheduleEvaluate(); return true
            end
            if updateInfo.removedAuraInstanceIDs and state.lbInstID then
                for _, instID in ipairs(updateInfo.removedAuraInstanceIDs) do
                    if instID == state.lbInstID then
                        ClearLBTracking()
                        NHA.ScheduleEvaluate()
                        return true
                    end
                end
            end
            if updateInfo.updatedAuraInstanceIDs and state.lbInstID then
                for _, instID in ipairs(updateInfo.updatedAuraInstanceIDs) do
                    if instID == state.lbInstID then
                        NHA.ScheduleEvaluate(); return true
                    end
                end
            end
            if updateInfo.addedAuras then
                for _, aura in ipairs(updateInfo.addedAuras) do
                    local sid = aura.spellId
                    if sid and not issecretvalue(sid)
                        and LIFEBLOOM_IDS[sid]
                        and aura.sourceUnit == "player" then
                        NHA.ScheduleEvaluate(); return true
                    end
                end
            end
            return true  -- handled, no default ScheduleEvaluate
        end

        -- Player (SOTF buff)
        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

        -- Other units: LB not yet tracked
        if not state.lbUnit and not state.lbScanPending then
            if not updateInfo or updateInfo.isFullUpdate or updateInfo.addedAuras then
                state.lbScanPending = true
                C_Timer.After(0.1, function()
                    state.lbScanPending = false
                    NHA.Evaluate()
                end)
            end
        end
        return true
    end

    if event == "SPELL_UPDATE_COOLDOWN" then
        if db.wgEnabled then NHA.ScheduleEvaluate() end
        return true
    end

    if event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit, _, spellID = ...
        if unit == "player" then
            if spellID == WILD_GROWTH_ID then
                state.wgJustCast = true
                state.wgActive   = false
                state.wgCastTime = GetTime()
                if not state.wgCDTimerSet then
                    state.wgCDTimerSet = true
                    C_Timer.After(10.1, function()
                        state.wgCDTimerSet = false
                        NHA.ScheduleEvaluate()
                    end)
                end
                NHA.ScheduleEvaluate()
            end
            if spellID == SWIFTMEND_ID then
                state.swiftmendCount = state.swiftmendCount + 1
                if state.swiftmendCount >= 4 then
                    state.swiftmendCount = 0
                    state.refoActive = false
                end
                state.sotfProcPending = true
                state.sotfExpire  = GetTime() + SOTF_BUFF_DURATION
                state.sotfTimeMod = 1
                C_Timer.After(0.1, NHA.Evaluate)
                C_Timer.After(0.3, NHA.Evaluate)
            elseif SOTF_CONSUMER_IDS[spellID] then
                if spellID == REGROWTH_ID and db.empRegrowthEnabled
                   and (state.sotfActive or state.sotfProcPending) then
                    state.empRegrowthPending  = true
                    state.empRegrowthDeadline = GetTime() + 0.75
                    if state._debugTrace then
                        NHA.Msg("|cff00ff00[TRACE]|r empRegrowthPending SET. sotfActive="
                            .. tostring(state.sotfActive)
                            .. " sotfProcPending=" .. tostring(state.sotfProcPending))
                    end
                    C_Timer.After(0.1, function() ScanForEmpoweredRegrowth(db) end)
                    C_Timer.After(0.3, function() ScanForEmpoweredRegrowth(db) end)
                    C_Timer.After(0.5, function() ScanForEmpoweredRegrowth(db) end)
                else
                    if spellID == REGROWTH_ID then
                        wipe(state.empRegrowthInstIDs)
                        for buffFrame in pairs(regrowthGlowFrames) do
                            NHA:HideProcGlow(buffFrame)
                        end
                        wipe(regrowthGlowFrames)
                    end
                    if state._debugTrace and spellID == REGROWTH_ID then
                        NHA.Msg("|cffff4444[TRACE]|r Regrowth cast but NOT empowered.")
                        state._debugTrace = false
                    end
                end
                state.sotfProcPending = false
                state.sotfActive      = false
                state.sotfExpire      = 0
                state.sotfWasActive   = false
                NHA.ScheduleEvaluate()
            end
        end
        return true
    end

    -- Talent change events
    if event == "PLAYER_TALENT_UPDATE"
    or event == "PLAYER_SPECIALIZATION_CHANGED"
    or event == "TRAIT_CONFIG_UPDATED" then
        state.hasRefoTalent  = false
        state.swiftmendCount = 0
        state.refoActive     = false
        C_Timer.After(0.5, function()
            state.hasRefoTalent = CheckRefoTalent()
            SyncSwiftmendCount()
            NHA.ScheduleEvaluate()
        end)
        return false  -- let core handle spec switch detection too
    end

    if event == "PLAYER_ENTERING_WORLD" then
        state.sotfProcPending = false
        state.hasRefoTalent = CheckRefoTalent()
        SyncSwiftmendCount()
        return false  -- let core handle
    end

    if event == "PLAYER_REGEN_DISABLED" then
        if state.sotfActive and state.sotfExpire > GetTime() then
            state.sotfProcPending = true
        end
        return false
    end

    if event == "PLAYER_REGEN_ENABLED" then
        SyncSwiftmendCount()
        return false
    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  -- suppress core's default ScheduleEvaluate (we debounce)
    end

    return false
end

-- =============================================================
-- BuildOptions (spec-specific options in the Display section)
-- =============================================================
function DruidResto:BuildOptions(parent, db, helpers)
    -- Right-column checkboxes in the Display section
    helpers.MakeCheckbox("LB Timer", 230, -90,
        function() return db.lbTimerShown end,
        function(v)
            db.lbTimerShown = v and true or false
            if not db.lbTimerShown then
                local f = NHA.display
                if f then
                    for _, row in ipairs(f.alertRows) do
                        if row.key == "lb" and row.timerFS then
                            row.timerFS:Hide()
                        end
                    end
                end
            end
        end)

    helpers.MakeCheckbox("SOTF Timer", 230, -114,
        function() return db.sotfTimerShown end,
        function(v)
            db.sotfTimerShown = v and true or false
            if not db.sotfTimerShown then
                local f = NHA.display
                if f then
                    for _, row in ipairs(f.alertRows) do
                        if row.key == "sotf" and row.timerFS then
                            row.timerFS:Hide()
                        end
                    end
                end
            end
        end)
end

-- =============================================================
-- Slash Commands (spec-specific)
-- =============================================================
DruidResto.slashCommands = {
    ["debug"] = function(self, db)
        local GA = _G.C_UnitAuras
        NHA.Msg("|cffffd700Empowered Regrowth Debug:|r")
        NHA.Msg("  empRegrowthEnabled: " .. tostring(db.empRegrowthEnabled))
        NHA.Msg("  glowEnabled: " .. tostring(db.glowEnabled))
        NHA.Msg("  empRegrowthPending: " .. tostring(state.empRegrowthPending))
        NHA.Msg("  empRegrowthDeadline: " .. string.format("%.1f", state.empRegrowthDeadline))
        NHA.Msg("  sotfActive: " .. tostring(state.sotfActive))
        NHA.Msg("  sotfProcPending: " .. tostring(state.sotfProcPending))
        local count = 0
        for _ in pairs(state.empRegrowthInstIDs) do count = count + 1 end
        NHA.Msg("  empRegrowthInstIDs count: " .. count)
        local rgCount = 0
        for _ in pairs(regrowthGlowFrames) do rgCount = rgCount + 1 end
        NHA.Msg("  regrowthGlowFrames count: " .. rgCount)
        NHA.Msg("  local C_UnitAuras.GetAuraDataByAuraInstanceID: "
            .. tostring(C_UnitAuras and C_UnitAuras.GetAuraDataByAuraInstanceID ~= nil))
        NHA.Msg("  global C_UnitAuras.GetAuraDataByAuraInstanceID: "
            .. tostring(GA and GA.GetAuraDataByAuraInstanceID ~= nil))
        if TargetFrame and TargetFrame.auraPools and GA then
            local found = false
            for buffFrame in TargetFrame.auraPools:EnumerateActive() do
                if buffFrame.auraInstanceID then
                    local ok, aura = pcall(GA.GetAuraDataByAuraInstanceID,
                        TargetFrame.unit, buffFrame.auraInstanceID)
                    if ok and aura and aura.spellId == REGROWTH_ID then
                        found = true
                        NHA.Msg("  Target Regrowth: instID=" .. tostring(aura.auraInstanceID)
                            .. " source=" .. tostring(aura.sourceUnit)
                            .. " secret=" .. tostring(issecretvalue(aura.spellId)))
                    end
                end
            end
            if not found then NHA.Msg("  Target: no Regrowth aura found") end
        else
            NHA.Msg("  Target: no auraPools or no global API")
        end
    end,

    ["trace"] = function(self, db)
        state._debugTrace = true
        NHA.Msg("Trace |cff00ff00enabled|r. Cast Regrowth with SOTF active — watch chat for trace output.")
    end,
}

-- =============================================================
-- Register with core
-- =============================================================
NHA:RegisterSpec("DRUID_RESTO", DruidResto)
