-- =============================================================
-- Specs/EvokerPreservation.lua
-- Nem: Healer Alerts — Preservation Evoker spec module.
-- =============================================================

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

local pairs, ipairs       = pairs, ipairs
local GetTime             = GetTime
local UnitExists          = UnitExists
local UnitAffectingCombat = UnitAffectingCombat
local C_Timer             = C_Timer
local C_UnitAuras         = C_UnitAuras
local wipe                = wipe
local issecretvalue       = NHA.issecretvalue

-- =============================================================
-- Spell IDs
-- =============================================================
local ECHO_ID              = 364343   -- SpellID, whitelisted
local REVERSION_ID         = 366155   -- SpellID, whitelisted
local ECHO_REVERSION_ID    = 367364   -- SpellID, whitelisted
local DREAM_BREATH_ID      = 355941   -- SpellID, whitelisted
local ECHO_DREAM_BREATH_ID = 376788   -- SpellID, whitelisted
local LIFEBIND_ID          = 373267   -- SpellID, whitelisted

local REVERSION_IDS = {
    [366155] = true,  -- SpellID
    [367364] = true,  -- SpellID
}

-- =============================================================
-- 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

-- =============================================================
-- State
-- =============================================================
local state = {
    revActive       = false,
    revWasActive    = false,
    dbState              = { wasReady = false },
    playerAuraPending   = false,
    rosterUpdatePending = false,
}

-- =============================================================
-- CUF Whole-Frame Glow State
-- =============================================================
local cufRevCache  = {}
local cufEchoCache = {}
local cufStateMap  = {}

-- =============================================================
-- Alert Definitions
-- =============================================================
local ALERTS = {
    {
        key          = "rev",
        label        = "Reversion Low",
        text         = "* REVERSION LOW *",
        defaultColor = { r = 0.200, g = 0.576, b = 0.498 },  -- #33937F
        hasTimer     = false,
        yOffset      = 25,
        defaultSound = "Evoker: Spread Reversion",
    },
    {
        key          = "revglow",
        label        = "Reversion (CUF Glow)",
        text         = nil,   -- glow-only, no text alert
        defaultColor = { r = 0.000, g = 0.831, b = 0.671 },  -- #00D4AB
        hasTimer     = false,
        yOffset      = 0,
        defaultSound = nil,
        priority     = NHA.VOICE_PRIO_LOW,
    },
    {
        key          = "db",
        label        = "Dream Breath Ready",
        text         = "* DREAM BREATH *",
        defaultColor = { r = 0.486, g = 1.000, b = 0.831 },  -- #7CFFD4
        hasTimer     = false,
        yOffset      = 50,
        defaultSound = "Evoker: Dream Breath",
        priority     = NHA.VOICE_PRIO_NORMAL,
    },
    {
        key          = "echoglow",
        label        = "Echo Holder (CUF Glow)",
        text         = nil,
        defaultColor = { r = 1.000, g = 0.831, b = 0.486 },  -- #FFD47C
        hasTimer     = false,
        yOffset      = 0,
        defaultSound = nil,
        priority     = NHA.VOICE_PRIO_LOW,
    },
}

-- =============================================================
-- Defaults
-- =============================================================
local DEFAULTS = {
    revEnabled            = true,
    revSoundEnabled       = true,
    revSoundName          = "Evoker: Spread Reversion",
    revThresholdDungeon   = 2,
    revThresholdRaid      = 4,
    revglowEnabled        = true,
    revglowCufGlowEnabled = true,
    dbEnabled            = true,
    dbSoundEnabled       = true,
    dbSoundName          = "Evoker: Dream Breath",
    echoglowEnabled        = true,
    echoglowCufGlowEnabled = true,
    alertColors           = {
        rev      = { r = 0.200, g = 0.576, b = 0.498 },
        revglow  = { r = 0.000, g = 0.831, b = 0.671 },  -- #00D4AB
        db       = { r = 0.486, g = 1.000, b = 0.831 },  -- #7CFFD4
        echoglow = { r = 1.000, g = 0.831, b = 0.486 },  -- #FFD47C
    },
}

-- =============================================================
-- Reversion Count Scan
-- =============================================================
local function CountActiveReversions()
    local count = 0
    for _, unit in ipairs(NHA:GetGroupUnits()) do
        if UnitExists(unit) and C_UnitAuras and C_UnitAuras.GetAuraDataBySpellID then
            for spellId in pairs(REVERSION_IDS) do
                local aura = C_UnitAuras.GetAuraDataBySpellID(unit, spellId, "HELPFUL")
                if aura and aura.auraInstanceID ~= 0
                   and aura.sourceUnit == "player" then
                    count = count + 1
                end
            end
        end
    end
    return count
end

local function GetReversionThreshold()
    local db = NHA.db
    if IsInRaid and IsInRaid() then
        return db.revThresholdRaid or 4
    end
    return db.revThresholdDungeon or 2
end

-- =============================================================
-- Module Table
-- =============================================================
local EvokerPres = {
    class     = "EVOKER",
    specIndex = 2,
    specID    = 1468,
    specName  = "Preservation",
    alerts    = ALERTS,
    defaults  = DEFAULTS,
    state     = state,

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

    cufRelevantSpells = {
        [REVERSION_ID] = true,
        [ECHO_ID]      = true,
    },
}

-- =============================================================
-- ClearAllGlows
-- =============================================================
function EvokerPres:ClearAllGlows()
    for frame in pairs(cufStateMap) do
        if frame._nhaFrameGlowing then NHA:HideFrameGlow(frame) end
    end
    wipe(cufStateMap)
    wipe(cufRevCache)
    wipe(cufEchoCache)
end

-- =============================================================
-- DoEvaluateCUFGlow — Reversion (priority) and Echo present-on-unit glow
-- =============================================================
local function DoEvaluateCUFGlow(self, db, frame, unit)
    local revglowOn  = db and db.revglowEnabled  and db.revglowCufGlowEnabled
    local echoglowOn = db and db.echoglowEnabled and db.echoglowCufGlowEnabled
    if not db or not db.enabled or not db.cufGlowEnabled
       or (not revglowOn and not echoglowOn) then
        if cufStateMap[frame] then
            NHA:HideFrameGlow(frame)
            cufStateMap[frame] = nil
        end
        cufRevCache[frame]  = nil
        cufEchoCache[frame] = nil
        return
    end

    if revglowOn then
        local revAura = FindAuraOnUnit(unit, REVERSION_ID, true)
        if revAura then
            local sa = cufRevCache[frame]
            if not sa then sa = {}; cufRevCache[frame] = sa end
            sa.auraInstanceID = revAura.auraInstanceID
            sa.expirationTime = SafeNum(revAura.expirationTime, 0)
            sa.duration       = SafeNum(revAura.duration, 0)
            sa.timeMod        = SafeNum(revAura.timeMod, 1)
            if sa.timeMod <= 0 then sa.timeMod = 1 end

            local now       = GetTime()
            local remaining = (sa.expirationTime - now) / sa.timeMod
            if remaining > 0 then
                local c = db.alertColors and db.alertColors.revglow
                          or { r = 0.000, g = 0.831, b = 0.671 }  -- #00D4AB
                NHA:ShowFrameGlow(frame, c.r, c.g, c.b)
                cufStateMap[frame]  = "rev"
                cufEchoCache[frame] = nil
                return
            end
            cufRevCache[frame] = nil
        else
            cufRevCache[frame] = nil
        end
    else
        cufRevCache[frame] = nil
    end

    if echoglowOn then
        local echoAura = FindAuraOnUnit(unit, ECHO_ID, false)
        if echoAura then
            local sa = cufEchoCache[frame]
            if not sa then sa = {}; cufEchoCache[frame] = sa end
            sa.auraInstanceID = echoAura.auraInstanceID
            local c = db.alertColors and db.alertColors.echoglow
                      or { r = 1.000, g = 0.831, b = 0.486 }  -- #FFD47C
            NHA:ShowFrameGlow(frame, c.r, c.g, c.b)
            cufStateMap[frame] = "echo"
            return
        end
        cufEchoCache[frame] = nil
    else
        cufEchoCache[frame] = nil
    end

    if cufStateMap[frame] then
        NHA:HideFrameGlow(frame)
        cufStateMap[frame] = nil
    end
end

-- =============================================================
-- EvaluateCUFGlow — public entry point called by core CUF hook
-- =============================================================
function EvokerPres:EvaluateCUFGlow(db, frame, unit)
    DoEvaluateCUFGlow(self, db, frame, unit)
end

-- =============================================================
-- ClearAllState
-- =============================================================
function EvokerPres:ClearAllState()
    state.revActive     = false
    state.revWasActive  = false
    -- Reset the Dream Breath ready latch so the alert can't fire/stay-stale
    -- on the first event of the next combat (helper-backed via EvalSpellReady).
    if state.dbState then state.dbState.wasReady = false end
    self:ClearAllGlows()
end

-- =============================================================
-- Evaluate
-- =============================================================
function EvokerPres:Evaluate(db)
    local revLow = false
    if db.revEnabled then
        local count     = CountActiveReversions()
        local threshold = GetReversionThreshold()
        revLow = count < threshold
    end
    state.revActive = revLow

    -- Dream Breath ready (Pattern B — empower variant; GetSpellCooldown handles empower CDs)
    state.dbActive = NHA.AlertHelpers.EvalSpellReady(state.dbState, DREAM_BREATH_ID, "db")

    if revLow and not state.revWasActive then NHA:PlayAlertSound("rev") end
    -- Suite-wide rule (see [[nha-combat-gate-rule]]): the latch tracks rising
    -- edges across combat boundaries. Core now gates Evaluate to in-combat only,
    -- so the latch updates unconditionally here; ClearAllState resets it on
    -- combat-exit/leave-group.
    state.revWasActive = revLow
end

-- =============================================================
-- OnActivate
-- =============================================================
function EvokerPres:OnActivate(db)
end

-- =============================================================
-- OnEvent
-- =============================================================
function EvokerPres:OnEvent(db, event, ...)
    if event == "UNIT_AURA" then
        local unit = ...
        if unit == "player" then
            if not state.playerAuraPending then
                state.playerAuraPending = true
                C_Timer.After(0.1, function()
                    state.playerAuraPending = false
                    NHA.Evaluate()
                end)
            end
            return true
        end
        return false
    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

    return false
end

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