local addonName, ns = ...

-- ============================================================
-- Personal Reminders: text-only reminder box
-- ============================================================

local PR = {}
ns.PersonalReminders = PR

-- ============================================================
-- SPEC ID CONSTANTS
-- ============================================================

local SPEC = {
    SHAMAN_ELEMENTAL    = 262, SHAMAN_ENHANCEMENT  = 263, SHAMAN_RESTORATION = 264,
    PALADIN_HOLY        = 65,  PALADIN_PROTECTION  = 66,  PALADIN_RETRIBUTION = 70,
    PRIEST_DISCIPLINE   = 256, PRIEST_HOLY         = 257, PRIEST_SHADOW      = 258,
    MAGE_ARCANE         = 62,  MAGE_FIRE           = 63,  MAGE_FROST         = 64,
    ROGUE_ASSASSINATION = 259, ROGUE_OUTLAW        = 260, ROGUE_SUBTLETY     = 261,
    WARRIOR_ARMS        = 71,  WARRIOR_FURY        = 72,  WARRIOR_PROTECTION = 73,
    HUNTER_BM           = 253, HUNTER_MM           = 254, HUNTER_SURVIVAL    = 255,
    DK_BLOOD            = 250, DK_FROST            = 251, DK_UNHOLY          = 252,
    WARLOCK_AFFLICTION  = 265, WARLOCK_DEMONOLOGY  = 266, WARLOCK_DESTRUCTION = 267,
    DRUID_BALANCE       = 102, DRUID_FERAL         = 103, DRUID_GUARDIAN     = 104, DRUID_RESTORATION = 105,
    EVOKER_DEVASTATION  = 1467, EVOKER_PRESERVATION = 1468, EVOKER_AUGMENTATION = 1473,
    MONK_BREWMASTER     = 268, MONK_WINDWALKER     = 269, MONK_MISTWEAVER    = 270,
    DH_HAVOC            = 577, DH_VENGEANCE        = 581,
}

-- ============================================================
-- BUFF / SPELL ID CONSTANTS
-- Spell/buff IDs verified against live Midnight (12.x) API
-- ============================================================

-- Shaman shields
local LIGHTNING_SHIELD_IDS = { 192106 }
local WATER_SHIELD_IDS     = { 52127 }
local EARTH_SHIELD_IDS     = { 974, 383648 }

-- Shaman weapon imbues
local WINDFURY_IDS         = { 33757 }
local WINDFURY_NAMES       = { "Windfury Weapon" }
local FLAMETONGUE_IDS      = { 318038 }
local FLAMETONGUE_NAMES    = { "Flametongue Weapon" }
local EARTHLIVING_IDS      = { 382021 }
local EARTHLIVING_NAMES    = { "Earthliving Weapon" }
local EARTHLIVING_SPELL_ID = 382021

-- Paladin Rites
local RITE_OF_ADJURATION     = { spellID = 433583, buffIDs = { 433583 }, names = { "Rite of Adjuration" },    enchantID = 7144 }
local RITE_OF_SANCTIFICATION = { spellID = 433568, buffIDs = { 433568 }, names = { "Rite of Sanctification" }, enchantID = 7143 }

-- Paladin Beacons
local BEACON_OF_LIGHT_IDS    = { 53563 }
local BEACON_OF_LIGHT_NAMES  = { "Beacon of Light" }
local BEACON_OF_FAITH_IDS    = { 156910 }
local BEACON_OF_FAITH_NAMES  = { "Beacon of Faith" }
local BEACON_OF_VIRTUE_ID    = 200025

-- Priest Shadowform
local SHADOWFORM_IDS         = { 232698, 15473 }

-- Druid
local SYMBIOTIC_IDS          = { 474750 }
local SYMBIOTIC_NAMES        = { "Symbiotic Relationship" }
local SYMBIOTIC_SPELL_ID     = 474750

-- Evoker
local SOURCE_OF_MAGIC_IDS    = { 369459 }
local SOURCE_OF_MAGIC_NAMES  = { "Source of Magic" }
local SOURCE_OF_MAGIC_SPELL_ID = 369459

-- Mage
local ARCANE_FAMILIAR_IDS    = { 210126 }
local ARCANE_FAMILIAR_SPELL_ID = 205022

-- Warlock
local SOULSTONE_IDS          = { 160930, 20707 }
local BURNING_RUSH_ID        = 111400

-- Rogue poisons
local LETHAL_POISON_IDS      = { 315584, 8679, 2823, 381664 }
local LETHAL_POISON_NAMES    = { "Instant Poison", "Wound Poison", "Deadly Poison", "Amplifying Poison" }
local UTILITY_POISON_IDS     = { 3408, 381637, 5761 }
local UTILITY_POISON_NAMES   = { "Crippling Poison", "Atrophic Poison", "Numbing Poison" }

-- ============================================================
-- AURA DETECTION
-- Aura detection cascade:
--   1. AuraUtil.FindAuraBySpellID  (primary — available in Midnight)
--   2. C_UnitAuras.GetAuraDataBySpellName (fallback)
--   3. false
-- ============================================================

local CheckUnitAura
local CheckUnitAuraByName

if AuraUtil and AuraUtil.FindAuraBySpellID then
    CheckUnitAura = function(spellID, unit, filter)
        return AuraUtil.FindAuraBySpellID(spellID, unit, filter) ~= nil
    end
elseif C_UnitAuras and C_UnitAuras.GetAuraDataBySpellName and C_Spell and C_Spell.GetSpellName then
    local nameCache = {}
    CheckUnitAura = function(spellID, unit, filter)
        local name = nameCache[spellID]
        if name == nil then
            name = C_Spell.GetSpellName(spellID) or false
            nameCache[spellID] = name
        end
        if not name then return false end
        return C_UnitAuras.GetAuraDataBySpellName(unit, name, filter) ~= nil
    end
else
    CheckUnitAura = function() return false end
end

if C_UnitAuras and C_UnitAuras.GetAuraDataBySpellName then
    CheckUnitAuraByName = function(name, unit, filter)
        return C_UnitAuras.GetAuraDataBySpellName(unit, name, filter) ~= nil
    end
elseif AuraUtil and AuraUtil.FindAuraByName then
    CheckUnitAuraByName = function(name, unit, filter)
        return AuraUtil.FindAuraByName(name, unit, filter) ~= nil
    end
else
    CheckUnitAuraByName = function() return false end
end

local function UnitHasAnyBuff(unit, ids, names)
    if ids then
        -- Fast path for player
        if unit == "player" and C_UnitAuras and C_UnitAuras.GetPlayerAuraBySpellID then
            for _, id in ipairs(ids) do
                if C_UnitAuras.GetPlayerAuraBySpellID(id) then return true end
            end
        end
        for _, id in ipairs(ids) do
            if CheckUnitAura(id, unit, "HELPFUL") then return true end
        end
    end
    if names then
        for _, name in ipairs(names) do
            if CheckUnitAuraByName(name, unit, "HELPFUL") then return true end
        end
    end
    return false
end

local function PlayerHasAnyBuff(ids, names)
    return UnitHasAnyBuff("player", ids, names)
end

-- Check if the player has applied a buff to any group/raid member (for Symbiotic, Beacons, etc.)
local function PlayerHasAppliedBuffToGroup(ids, names)
    local function unitHasIt(unit)
        if not UnitExists(unit) or UnitIsDeadOrGhost(unit) then return false end
        return UnitHasAnyBuff(unit, ids, names)
    end

    if IsInRaid() then
        for i = 1, GetNumGroupMembers() do
            if unitHasIt("raid" .. i) then return true end
        end
        return false
    elseif IsInGroup() then
        if unitHasIt("player") then return true end
        for i = 1, GetNumGroupMembers() - 1 do
            if unitHasIt("party" .. i) then return true end
        end
        return false
    else
        return unitHasIt("player")
    end
end

local function AnyGroupMemberHasBuff(ids, names)
    return PlayerHasAppliedBuffToGroup(ids, names)
end

-- Weapon enchant helpers
local function HasMainHandEnchant()
    if not GetWeaponEnchantInfo then return false end
    local hasMain = GetWeaponEnchantInfo()
    return hasMain == true
end

local function HasOffHandEnchant()
    if not GetWeaponEnchantInfo then return false end
    local _, _, _, _, hasOff = GetWeaponEnchantInfo()
    return hasOff == true
end

local function HasWeaponEnchantID(enchantID)
    if not GetWeaponEnchantInfo or not enchantID then return false end
    local _, _, _, mainID, _, _, _, offID = GetWeaponEnchantInfo()
    return mainID == enchantID or offID == enchantID
end

-- ============================================================
-- SPEC HELPERS
-- ============================================================

local function GetPlayerClass()
    local _, c = UnitClass("player")
    return c or ""
end

local function GetCurrentSpecID()
    local idx = GetSpecialization()
    if not idx then return 0 end
    return (select(1, GetSpecializationInfo(idx))) or 0
end

local function IsSpec(specID)
    return GetCurrentSpecID() == specID
end

local function IsInDungeonInstance() local _, t = IsInInstance(); return t == "party" end
local function IsInRaidInstance()    local _, t = IsInInstance(); return t == "raid"  end
local function IsInAnyInstance()     return IsInDungeonInstance() or IsInRaidInstance() end
local function IsInAnyGroup()        return IsInGroup() or IsInRaid() end

local function ShouldShowForConditions(cfg)
    if not cfg then return false end
    -- OR logic: show if current context matches ANY checked box
    -- No boxes checked = never show
    if cfg.onlyInDungeon and IsInDungeonInstance()  then return true end
    if cfg.onlyInRaid    and IsInRaidInstance()     then return true end
    if cfg.onlyInGroup   and IsInAnyGroup()         then return true end
    if cfg.onlyOutside   and not IsInAnyInstance()  then return true end
    return false
end

-- ============================================================
-- SHADOWFORM STATE (event-driven)
-- ============================================================

local shadowformActive = false

local function DetectShadowform()
    return PlayerHasAnyBuff(SHADOWFORM_IDS)
end

-- ============================================================
-- REMINDER DEFINITIONS
-- ============================================================

local DEFS = {}
ns.PersonalReminderDefs = DEFS
local function Def(t) table.insert(DEFS, t) end

-- -------------------- WARLOCK -----------------------

Def({ id="warlock_pet", label="Pet Missing", class="WARLOCK",
    check=function()
        if not IsSpellKnown(688) then return false end
        if IsPlayerSpell(108503) then return false end -- Grimoire of Sacrifice
        return not UnitExists("pet") or UnitIsDead("pet")
    end })

Def({ id="soulstone", label="Soulstone", class="WARLOCK",
    check=function()
        if not IsSpellKnown(20707) then return false end
        if not IsInAnyGroup() then return false end
        return not AnyGroupMemberHasBuff(SOULSTONE_IDS)
    end })

Def({ id="burning_rush", label="Burning Rush Active", class="WARLOCK",
    check=function()
        return PlayerHasAnyBuff({ BURNING_RUSH_ID })
    end })

-- -------------------- ROGUE -----------------------

Def({ id="lethal_poison", label="Lethal Poison (Main Hand)", class="ROGUE",
    check=function()
        if PlayerHasAnyBuff(LETHAL_POISON_IDS, LETHAL_POISON_NAMES) then return false end
        if HasMainHandEnchant() then return false end
        return true
    end })

Def({ id="utility_poison", label="Utility Poison (Off Hand)", class="ROGUE",
    check=function()
        if PlayerHasAnyBuff(UTILITY_POISON_IDS, UTILITY_POISON_NAMES) then return false end
        if HasOffHandEnchant() then return false end
        return true
    end })

-- -------------------- SHAMAN -----------------------

Def({ id="windfury_weapon", label="Windfury Weapon", class="SHAMAN", spec=SPEC.SHAMAN_ENHANCEMENT,
    check=function()
        if PlayerHasAnyBuff(WINDFURY_IDS, WINDFURY_NAMES) then return false end
        if HasMainHandEnchant() then return false end
        return true
    end })

Def({ id="flametongue_weapon", label="Flametongue Weapon", class="SHAMAN", spec=SPEC.SHAMAN_ENHANCEMENT,
    check=function()
        if PlayerHasAnyBuff(FLAMETONGUE_IDS, FLAMETONGUE_NAMES) then return false end
        if HasOffHandEnchant() then return false end
        return true
    end })

Def({ id="earthliving_weapon", label="Earthliving Weapon", class="SHAMAN", spec=SPEC.SHAMAN_RESTORATION,
    check=function()
        if not IsPlayerSpell(EARTHLIVING_SPELL_ID) then return false end
        if PlayerHasAnyBuff(EARTHLIVING_IDS, EARTHLIVING_NAMES) then return false end
        if HasMainHandEnchant() then return false end
        return true
    end })

-- Lightning Shield: spell ID 192106 in Midnight (not 324)
Def({ id="lightning_shield", label="Lightning Shield", class="SHAMAN",
    check=function()
        if IsSpec(SPEC.SHAMAN_RESTORATION) then return false end
        return not PlayerHasAnyBuff(LIGHTNING_SHIELD_IDS)
    end })

-- Water Shield: always available for Resto Shaman
Def({ id="water_shield", label="Water Shield", class="SHAMAN",
    check=function()
        if not IsSpec(SPEC.SHAMAN_RESTORATION) then return false end
        return not PlayerHasAnyBuff(WATER_SHIELD_IDS)
    end })

-- Earth Shield: talent, check with IsPlayerSpell
Def({ id="earth_shield", label="Earth Shield", class="SHAMAN",
    check=function()
        if not IsPlayerSpell(974) then return false end
        return not AnyGroupMemberHasBuff(EARTH_SHIELD_IDS)
    end })

-- -------------------- PALADIN -----------------------

-- Rites: Adjuration (433583) OR Sanctification (433568), detect which is talented
-- Fallback to weapon enchant IDs (7144 / 7143)
Def({ id="paladin_rite", label="Rite Missing", class="PALADIN",
    check=function()
        local rite = nil
        if IsPlayerSpell(RITE_OF_ADJURATION.spellID) then
            rite = RITE_OF_ADJURATION
        elseif IsPlayerSpell(RITE_OF_SANCTIFICATION.spellID) then
            rite = RITE_OF_SANCTIFICATION
        end
        if not rite then return false end
        -- Check buff auras
        if UnitHasAnyBuff("player", rite.buffIDs, rite.names) then return false end
        -- Fallback: weapon enchant ID
        if HasWeaponEnchantID(rite.enchantID) then return false end
        return true
    end })

-- Beacon of Light: Holy only, not when Beacon of Virtue is talented
Def({ id="beacon_of_light", label="Beacon of Light", class="PALADIN",
    check=function()
        if not IsSpec(SPEC.PALADIN_HOLY) then return false end
        if IsPlayerSpell(BEACON_OF_VIRTUE_ID) then return false end
        if not IsPlayerSpell(53563) then return false end
        if not IsInAnyGroup() then return false end
        return not PlayerHasAppliedBuffToGroup(BEACON_OF_LIGHT_IDS, BEACON_OF_LIGHT_NAMES)
    end })

-- Beacon of Faith: Holy only, talent required
Def({ id="beacon_of_faith", label="Beacon of Faith", class="PALADIN",
    check=function()
        if not IsSpec(SPEC.PALADIN_HOLY) then return false end
        if not IsPlayerSpell(156910) then return false end
        if not IsInAnyGroup() then return false end
        return not PlayerHasAppliedBuffToGroup(BEACON_OF_FAITH_IDS, BEACON_OF_FAITH_NAMES)
    end })

-- -------------------- PRIEST -----------------------

-- Shadowform: use event-driven state (more reliable than polling)
Def({ id="shadowform", label="Shadowform", class="PRIEST",
    check=function()
        if not IsSpec(SPEC.PRIEST_SHADOW) then return false end
        return not shadowformActive
    end })

-- -------------------- DRUID -----------------------

-- Symbiotic Relationship: spell ID 474750
Def({ id="symbiotic_relationship", label="Symbiotic Relationship", class="DRUID",
    check=function()
        if not IsSpec(SPEC.DRUID_RESTORATION) then return false end
        if not IsPlayerSpell(SYMBIOTIC_SPELL_ID) then return false end
        if not IsInAnyGroup() then return false end
        return not PlayerHasAppliedBuffToGroup(SYMBIOTIC_IDS, SYMBIOTIC_NAMES)
    end })

-- -------------------- EVOKER -----------------------

Def({ id="source_of_magic", label="Source of Magic", class="EVOKER",
    check=function()
        if not IsSpec(SPEC.EVOKER_PRESERVATION) then return false end
        if not IsPlayerSpell(SOURCE_OF_MAGIC_SPELL_ID) then return false end
        if not IsInAnyGroup() then return false end
        return not PlayerHasAppliedBuffToGroup(SOURCE_OF_MAGIC_IDS, SOURCE_OF_MAGIC_NAMES)
    end })

-- -------------------- MAGE -----------------------

Def({ id="arcane_familiar", label="Arcane Familiar", class="MAGE",
    check=function()
        if not IsSpec(SPEC.MAGE_ARCANE) then return false end
        if not IsPlayerSpell(ARCANE_FAMILIAR_SPELL_ID) then return false end
        return not PlayerHasAnyBuff(ARCANE_FAMILIAR_IDS)
    end })

-- -------------------- HUNTER -----------------------

Def({ id="hunter_pet", label="Pet Missing", class="HUNTER",
    check=function()
        if IsSpec(SPEC.HUNTER_MM) and IsPlayerSpell(155228) and not IsPlayerSpell(378986) then return false end
        return not UnitExists("pet") or UnitIsDead("pet")
    end })

-- -------------------- DEATH KNIGHT -----------------------

Def({ id="dk_ghoul", label="Ghoul Missing", class="DEATHKNIGHT",
    check=function()
        if not IsSpec(SPEC.DK_UNHOLY) then return false end
        if not IsPlayerSpell(46584) then return false end
        return not UnitExists("pet") or UnitIsDead("pet")
    end })

-- ============================================================
-- TEXT DISPLAY
-- ============================================================

local container     = nil
local textLines     = {}
local pulseTime     = 0

local function ResolveFont()
    local db = BrightQoL.personalReminders
    local path = db.fontPath
    if not path or path == "" then return "Fonts\\FRIZQT__.TTF" end
    if path:find("\\") or path:find("/") then return path end
    if ns.GetFontPath then return ns.GetFontPath(path) or "Fonts\\FRIZQT__.TTF" end
    return "Fonts\\FRIZQT__.TTF"
end

local function GetOutlineFlag()
    local o = BrightQoL.personalReminders.fontOutline or "OUTLINE"
    if o == "NONE" then return "" end
    if o == "THICKOUTLINE" then return "THICKOUTLINE" end
    return "OUTLINE"
end

local function GetOrCreateLine(idx)
    if not textLines[idx] then
        local fs = container:CreateFontString(nil, "OVERLAY")
        fs:SetShadowOffset(1, -1)
        fs:SetShadowColor(0, 0, 0, 1)
        textLines[idx] = fs
    end
    return textLines[idx]
end

local function UpdatePulse(elapsed)
    local db = BrightQoL.personalReminders
    pulseTime = pulseTime + elapsed * (db.pulseSpeed or 1.5)
    local t = (math.sin(pulseTime) + 1) * 0.5
    local ar = db.pulseColorAR or 1; local ag = db.pulseColorAG or 0.5; local ab = db.pulseColorAB or 0.1
    local br = db.pulseColorBR or 1; local bg = db.pulseColorBG or 1;   local bb = db.pulseColorBB or 0.2
    local r = ar + (br - ar) * t
    local g = ag + (bg - ag) * t
    local b = ab + (bb - ab) * t
    for _, fs in ipairs(textLines) do
        if fs:IsShown() then fs:SetTextColor(r, g, b) end
    end
end

local function LayoutText(visible)
    local db       = BrightQoL.personalReminders
    local fontSize = db.fontSize  or 14
    local fontPath = ResolveFont()
    local outline  = GetOutlineFlag()
    local cr       = db.fontColorR or 1
    local cg       = db.fontColorG or 0.5
    local cb       = db.fontColorB or 0.1

    for _, fs in ipairs(textLines) do fs:Hide() end

    if #visible == 0 then
        container:SetScript("OnUpdate", nil)
        pulseTime = 0
        if db.unlock then
            container:SetSize(200, fontSize + 6)
            container:SetBackdropColor(0.05, 0.05, 0.05, 0.75)
            container:SetBackdropBorderColor(0, 0.8, 1, 0.6)
            container:Show()
            container._label:Show()
        else
            container:SetBackdropColor(0, 0, 0, 0)
            container:SetBackdropBorderColor(0, 0, 0, 0)
            container:Hide()
        end
        return
    end

    container._label:Hide()

    local lineH = fontSize + 4
    local maxW  = 10

    for i, def in ipairs(visible) do
        local fs = GetOrCreateLine(i)
        fs:SetFont(fontPath, fontSize, outline)
        fs:SetTextColor(cr, cg, cb)
        fs:SetText(def.label)
        fs:Show()
        local w = fs:GetStringWidth()
        if w > maxW then maxW = w end
    end

    local totalH = #visible * lineH + 8
    container:SetSize(maxW + 16, totalH)
    container:Show()

    if db.unlock then
        container:SetBackdropColor(0.05, 0.05, 0.05, 0.75)
        container:SetBackdropBorderColor(0, 0.8, 1, 0.6)
    else
        container:SetBackdropColor(0, 0, 0, 0)
        container:SetBackdropBorderColor(0, 0, 0, 0)
    end

    local yOff = 4
    for i = 1, #visible do
        local fs = textLines[i]
        fs:ClearAllPoints()
        fs:SetPoint("TOP", container, "TOP", 0, -yOff)
        fs:SetJustifyH("CENTER")
        yOff = yOff + lineH
    end

    if db.pulseEnabled then
        container:SetScript("OnUpdate", function(_, elapsed) UpdatePulse(elapsed) end)
    else
        container:SetScript("OnUpdate", nil)
        for _, fs in ipairs(textLines) do
            if fs:IsShown() then fs:SetTextColor(cr, cg, cb) end
        end
    end
end

local function BuildContainer()
    local db = BrightQoL.personalReminders
    container = CreateFrame("Frame", "BrightQoL_PersonalReminders", UIParent, "BackdropTemplate")
    container:SetClampedToScreen(true)
    container:SetMovable(true)
    container:SetToplevel(true)
    container:SetSize(200, 24)
    container:EnableMouse(true)
    container:RegisterForDrag("LeftButton")
    container:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
    container:SetBackdropColor(0, 0, 0, 0)
    container:SetBackdropBorderColor(0, 0, 0, 0)
    container:SetScript("OnDragStart", function(self)
        if BrightQoL.personalReminders.unlock then self:StartMoving() end
    end)
    container:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, _, x, y = self:GetPoint()
        BrightQoL.personalReminders.point = point
        BrightQoL.personalReminders.x     = math.floor(x + 0.5)
        BrightQoL.personalReminders.y     = math.floor(y + 0.5)
    end)
    local pt = db.point or "CENTER"
    container:SetPoint(pt, UIParent, pt, db.x or 0, db.y or 100)
    container._label = container:CreateFontString(nil, "OVERLAY")
    container._label:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
    container._label:SetTextColor(0, 0.81, 1)
    container._label:SetPoint("TOP", container, "TOP", 0, 0)
    container._label:SetJustifyH("CENTER")
    container._label:SetText("[ Personal Reminders ]")
    container._label:Hide()
    container:Hide()
end

-- ============================================================
-- UPDATE LOGIC
-- ============================================================

local ticker    = nil
local lastSound = 0
local prevActive = {}

local function DoUpdate()
    local db = BrightQoL.personalReminders
    if not db or not db.enabled then
        if container then container:Hide() end
        return
    end

    local classFile = GetPlayerClass()
    local visible   = {}
    local needSound = false
    local curSpec   = GetCurrentSpecID()

    for _, def in ipairs(DEFS) do
        local classOk = (def.class == nil or def.class == classFile)
        if classOk then
            local specOk = true
            if def.spec ~= nil then
                if type(def.spec) == "table" then
                    specOk = false
                    for _, s in ipairs(def.spec) do if s == curSpec then specOk = true; break end end
                else
                    specOk = (def.spec == curSpec)
                end
            end
            if specOk then
                local cfg = db.reminders and db.reminders[def.id]
                if not cfg or cfg.enabled ~= false then
                    if ShouldShowForConditions(cfg) then
                        local ok, result = pcall(def.check)
                        if ok and result then
                            table.insert(visible, def)
                            if not prevActive[def.id] then needSound = true end
                        end
                    end
                end
            end
        end
    end

    prevActive = {}
    for _, def in ipairs(visible) do prevActive[def.id] = true end

    if needSound then
        local now = GetTime()
        if now - lastSound >= 3 then
            lastSound = now
            PlaySound(SOUNDKIT.IG_QUEST_NEW or 8456)
        end
    end

    LayoutText(visible)
end

-- ============================================================
-- DEFAULTS
-- ============================================================

local function InitDefaults()
    BrightQoL.personalReminders = BrightQoL.personalReminders or {}
    local db = BrightQoL.personalReminders
    if db.enabled     == nil then db.enabled     = false end
    if db.unlock      == nil then db.unlock      = false end
    if db.fontSize    == nil then db.fontSize    = 14   end
    if db.fontPath    == nil then db.fontPath    = "Fonts\\FRIZQT__.TTF" end
    if db.fontOutline == nil then db.fontOutline = "OUTLINE" end
    if db.fontColorR  == nil then db.fontColorR  = 1   end
    if db.fontColorG  == nil then db.fontColorG  = 0.5 end
    if db.fontColorB  == nil then db.fontColorB  = 0.1 end
    if db.pulseEnabled == nil then db.pulseEnabled = true end
    if db.pulseColorAR == nil then db.pulseColorAR = 1   end
    if db.pulseColorAG == nil then db.pulseColorAG = 0.5 end
    if db.pulseColorAB == nil then db.pulseColorAB = 0.1 end
    if db.pulseColorBR == nil then db.pulseColorBR = 1   end
    if db.pulseColorBG == nil then db.pulseColorBG = 1   end
    if db.pulseColorBB == nil then db.pulseColorBB = 0.2 end
    if db.pulseSpeed   == nil then db.pulseSpeed   = 1.5 end
    if db.point        == nil then db.point        = "CENTER" end
    if db.x            == nil then db.x            = 0   end
    if db.y            == nil then db.y            = 100 end
    db.reminders = db.reminders or {}
    for _, def in ipairs(DEFS) do
        if not db.reminders[def.id] then
            db.reminders[def.id] = { enabled = true, onlyInDungeon = false, onlyInRaid = false, onlyInGroup = false, onlyOutside = false }
        end
    end
    db.unlock = false
end

-- ============================================================
-- EVENTS
-- ============================================================

local eventFrame = CreateFrame("Frame", "BrightQoL_PersonalRemindersEvents")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
eventFrame:RegisterEvent("UNIT_PET")
eventFrame:RegisterEvent("UNIT_AURA")
eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
-- Shadowform needs spell_activation events for accurate state
eventFrame:RegisterEvent("SPELL_UPDATE_CHARGES")
eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")

eventFrame:SetScript("OnEvent", function(self, event, arg1)
    if event == "PLAYER_LOGIN" then
        InitDefaults()
        BuildContainer()
        -- Seed shadowform state on login
        shadowformActive = DetectShadowform()
        ticker = C_Timer.NewTicker(0.5, DoUpdate)
        C_Timer.After(1, DoUpdate)
    elseif event == "UNIT_AURA" then
        if arg1 == "player" then
            -- Update shadowform state immediately on player aura change
            shadowformActive = DetectShadowform()
            C_Timer.After(0.05, DoUpdate)
        elseif arg1 and (arg1:match("^raid") or arg1:match("^party")) then
            C_Timer.After(0.1, DoUpdate)
        end
    elseif event == "PLAYER_EQUIPMENT_CHANGED" then
        C_Timer.After(0.1, DoUpdate)
    elseif event == "PLAYER_SPECIALIZATION_CHANGED" then
        shadowformActive = DetectShadowform()
        C_Timer.After(0.5, DoUpdate)
    else
        C_Timer.After(0.3, DoUpdate)
    end
end)

-- ============================================================
-- PUBLIC API
-- ============================================================

function PR:ForceUpdate()    DoUpdate() end

function PR:ApplySettings()
    if not container then return end
    local db = BrightQoL.personalReminders
    container:ClearAllPoints()
    local pt = db.point or "CENTER"
    container:SetPoint(pt, UIParent, pt, db.x or 0, db.y or 100)
    DoUpdate()
end

function PR:SetUnlocked(val)
    local db = BrightQoL.personalReminders
    if not db then return end
    db.unlock = val
    DoUpdate()
end
