local CritMaticGoldYellow = "|cffffd700"
local CritMaticGold = "|cffed9d09"
local CritMaticWhite = "|cffe8e7e3"
local CritMaticGray = "|cffc2bfb6"
local CritMaticRed = "|cffd41313"
local CritMaticBrown = "|cffada27f"

-- Retail (Midnight 12.0+) removed the global GetSpellInfo. Restore the
-- classic multi-value signature on top of C_Spell.GetSpellInfo so the rest
-- of this file (which targets every flavor) keeps working unchanged.
if not _G.GetSpellInfo and _G.C_Spell and _G.C_Spell.GetSpellInfo then
    _G.GetSpellInfo = function(spellID)
        if not spellID then return nil end
        local info = _G.C_Spell.GetSpellInfo(spellID)
        if not info then return nil end
        return info.name, nil, info.iconID, info.castTime, info.minRange, info.maxRange, info.originalIconID or info.spellID
    end
end

Critmatic = LibStub("AceAddon-3.0"):NewAddon(CritMaticGold .. "CritMatic|r", "AceConsole-3.0", "AceTimer-3.0",
        "AceEvent-3.0",
        "AceComm-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("CritMatic")

local MAX_HIT = 1e9

local spellAggregateCache = {}
local aggregateDirty = true

local function InvalidateAggregateCache()
    aggregateDirty = true
end

local function GetSpellAggregate(localizedSpellName)
    if aggregateDirty then
        wipe(spellAggregateCache)
        for sID, data in pairs(CritMaticData) do
            local name = GetSpellInfo(sID)
            if name then
                if not spellAggregateCache[name] then
                    spellAggregateCache[name] = {
                        highestCrit = data.highestCrit or 0,
                        highestNormal = data.highestNormal or 0,
                        highestHealCrit = data.highestHealCrit or 0,
                        highestHeal = data.highestHeal or 0,
                    }
                else
                    local c = spellAggregateCache[name]
                    c.highestCrit = math.max(c.highestCrit, data.highestCrit or 0)
                    c.highestNormal = math.max(c.highestNormal, data.highestNormal or 0)
                    c.highestHealCrit = math.max(c.highestHealCrit, data.highestHealCrit or 0)
                    c.highestHeal = math.max(c.highestHeal, data.highestHeal or 0)
                end
            end
        end
        aggregateDirty = false
    end
    return spellAggregateCache[localizedSpellName]
end

local function _GetSpellCooldown(spellID)
    if C_Spell and C_Spell.GetSpellCooldown then
        local info = C_Spell.GetSpellCooldown(spellID)
        if info then
            return info.startTime, info.duration, info.isEnabled
        end
    end
    if _G.GetSpellCooldown then
        return _G.GetSpellCooldown(spellID)
    end
    return 0, 0, false
end

local function GetGCD()
    local _, gcdDuration = _GetSpellCooldown(78) -- 78 is the spell ID for Warrior's Heroic Strike
    if not gcdDuration or gcdDuration == 0 then
        return 1.5 -- Default GCD duration if not available
    end
    return gcdDuration
end

local function RenderHighestHitsForSpellID(self, spellID)
    local localizedSpellName = GetSpellInfo(spellID)
    if not localizedSpellName then
        return
    end

    local agg = GetSpellAggregate(localizedSpellName)
    if not agg then
        return
    end
    local highestCrit = agg.highestCrit
    local highestNormal = agg.highestNormal
    local highestHealCrit = agg.highestHealCrit
    local highestHeal = agg.highestHeal

    if highestCrit == 0 and highestNormal == 0 and highestHealCrit == 0 and highestHeal == 0 then
        return
    end

    local cooldown = (GetSpellBaseCooldown(spellID) or 0) / 1000
    local _, _, _, castTime = GetSpellInfo(spellID)
    local effectiveCastTime = (castTime and castTime > 0) and (castTime / 1000) or GetGCD()
    local effectiveTime = max(effectiveCastTime, cooldown)

    local critDPS = highestCrit / effectiveTime
    local normalDPS = highestNormal / effectiveTime
    local critHPS = highestHealCrit / effectiveTime
    local normalHPS = highestHeal / effectiveTime

    local CritMaticLeft = L["action_bar_crit"] .. ": "
    local DPS = L["action_bar_dps"] .. ") "
    local CritMaticRight = tostring(highestCrit) .. " (" .. format("%.1f", critDPS) .. DPS
    local normalMaticLeft = L["action_bar_hit"] .. ": "
    local normalMaticRight = tostring(highestNormal) .. " (" .. format("%.1f", normalDPS) .. DPS

    local CritMaticHealLeft = L["action_bar_crit_heal"] .. ": "
    local HPS = L["action_bar_hps"] .. ") "
    local CritMaticHealRight = tostring(highestHealCrit) .. " (" .. format("%.1f", critHPS) .. HPS
    local normalMaticHealLeft = L["action_bar_heal"] .. ": "
    local normalMaticHealRight = tostring(highestHeal) .. " (" .. format("%.1f", normalHPS) .. HPS

    local critMaticHealExists = false
    local normalMaticHealExists = false
    local critMaticExists = false
    local normalMaticExists = false

    for i = 1, self:NumLines() do
        local gtl = _G["GameTooltipTextLeft" .. i]
        local gtr = _G["GameTooltipTextRight" .. i]

        if gtl and gtr then
            if gtl:GetText() == CritMaticHealLeft and gtr:GetText() == CritMaticHealRight then
                critMaticHealExists = true
            elseif gtl:GetText() == normalMaticHealLeft and gtr:GetText() == normalMaticHealRight then
                normalMaticHealExists = true
            end
            if gtl:GetText() == CritMaticLeft and gtr:GetText() == CritMaticRight then
                critMaticExists = true
            elseif gtl:GetText() == normalMaticLeft and gtr:GetText() == normalMaticRight then
                normalMaticExists = true
            end
        end
    end

    local addedLine = false

    if highestCrit > 0 and not critMaticExists then
        self:AddDoubleLine(CritMaticLeft, CritMaticRight, 0.9, 0.9, 0.9, 0.9, 0.82, 0)
        addedLine = true
    end

    if highestNormal > 0 and not normalMaticExists then
        self:AddDoubleLine(normalMaticLeft, normalMaticRight, 0.9, 0.9, 0.9, 0.9, 0.82, 0)
        addedLine = true
    end

    if highestHealCrit > 0 and not critMaticHealExists then
        self:AddDoubleLine(CritMaticHealLeft, CritMaticHealRight, 0.9, 0.9, 0.9, 0.9, 0.82, 0)
        addedLine = true
    end

    if highestHeal > 0 and not normalMaticHealExists then
        self:AddDoubleLine(normalMaticHealLeft, normalMaticHealRight, 0.9, 0.9, 0.9, 0.9, 0.82, 0)
        addedLine = true
    end

    if addedLine then
        self:Show()
    end
end

local function AddHighestHitsToTooltip(self, slot, isSpellBook)
    if (not slot) then
        return
    end
    local actionType, id, spellID

    if isSpellBook then
        if _G.GetSpellBookItemInfo then
            local itemType, itemID = _G.GetSpellBookItemInfo(slot, BOOKTYPE_SPELL)
            if itemType == "SPELL" or itemType == "FUTURESPELL" then
                spellID = itemID
            end
        elseif C_SpellBook and C_SpellBook.GetSpellBookItemInfo then
            local bank = Enum and Enum.SpellBookSpellBank and Enum.SpellBookSpellBank.Player
            local info = C_SpellBook.GetSpellBookItemInfo(slot, bank)
            if info and info.spellID then
                spellID = info.spellID
            end
        else
            spellID = select(3, GetSpellBookItemName(slot, BOOKTYPE_SPELL))
        end
        actionType = "spell"
    else
        actionType, id = GetActionInfo(slot)
        if actionType == "spell" then
            spellID = id
        elseif actionType == "macro" and id then
            if _G.GetMacroSpell then
                spellID = _G.GetMacroSpell(id)
            end
        end
    end

    if not spellID then
        return
    end

    RenderHighestHitsForSpellID(self, spellID)
end

local function AddHighestHitsToPetTooltip(self, slot)
    if not slot then
        return
    end
    if not GetPetActionInfo then
        return
    end
    local spellID = select(7, GetPetActionInfo(slot))
    if not spellID then
        return
    end
    RenderHighestHitsForSpellID(self, spellID)
end
-- Function to create a new frame based on the template
Critmatic.CreateNewMessageFrame = function()
    local f = CreateFrame("Frame", nil, UIParent)
    f:SetSize(1000, 30)
    
    -- Get position settings from database
    local xPos = Critmatic.db.profile.alertNotificationFormat.position.xPos
    local yPos = Critmatic.db.profile.alertNotificationFormat.position.yPos
    
    -- Convert percentages to pixel offsets
    local screenWidth = UIParent:GetWidth()
    local screenHeight = UIParent:GetHeight()
    local xOffset = (xPos - 50) * screenWidth / 100
    local yOffset = (yPos - 50) * screenHeight / 100
    
    f:SetPoint("CENTER", UIParent, "CENTER", xOffset, yOffset)

    f.text = f:CreateFontString(nil, "ARTWORK", "GameFontNormalHuge")
    f.text:SetAllPoints()

    local bounceStyle = Critmatic.db.profile.alertNotificationFormat.global.bounceStyle or "legacy"
    local bouncePeak, bounceUp, bouncePause, bounceDown
    if bounceStyle == "mop" then
        bouncePeak, bounceUp, bouncePause, bounceDown = 1.5, 0.06, 0, 0.10
    else
        bouncePeak, bounceUp, bouncePause, bounceDown = 1.5, 0.15, 0.12, 0.15
    end

    f.bounce = f:CreateAnimationGroup()
    local scaleUp = f.bounce:CreateAnimation("Scale")
    scaleUp:SetScale(bouncePeak, bouncePeak)
    scaleUp:SetDuration(bounceUp)
    scaleUp:SetOrder(1)
    local bounceOrder = 2
    if bouncePause > 0 then
        local pause = f.bounce:CreateAnimation("Pause")
        pause:SetDuration(bouncePause)
        pause:SetOrder(bounceOrder)
        bounceOrder = bounceOrder + 1
    end
    local scaleDown = f.bounce:CreateAnimation("Scale")
    scaleDown:SetScale(1 / bouncePeak, 1 / bouncePeak)
    scaleDown:SetDuration(bounceDown)
    scaleDown:SetOrder(bounceOrder)
    local LSM = LibStub("LibSharedMedia-3.0")
    local fontPath = LSM:Fetch("font", Critmatic.db.profile.fontSettings.font)
    f.text:SetFont(fontPath, Critmatic.db.profile.fontSettings.fontSize, Critmatic.db.profile.fontSettings.fontOutline)
    f.text:SetShadowOffset(unpack(Critmatic.db.profile.fontSettings.fontShadowSize))
    f.text:SetShadowColor(unpack(Critmatic.db.profile.fontSettings.fontShadowColor))

    return f
end

function Critmatic:OnInitialize()
    -- Initialization code here.
    self.db = LibStub("AceDB-3.0"):New("CritMaticDB", defaults, true)

    if _G.CritMaticDB14 and _G.CritMaticDB14.profile and not _G.CritMaticDB._cm_migrated then
        self.db.profiles["Default"] = CopyTable(_G.CritMaticDB14.profile)
        _G.CritMaticDB._cm_migrated = true
    end

    if self.db.profile.crits == nil then
        self.db.profile.crits = {}
    end
    if _G.CritMaticData and next(_G.CritMaticData) then
        for spellID, data in pairs(_G.CritMaticData) do
            if not self.db.profile.crits[spellID] then
                self.db.profile.crits[spellID] = CopyTable(data)
            end
        end
    end
    _G.CritMaticData = self.db.profile.crits

    local opts = Critmatic:CreateOptionsTable()
    local Settings = LibStub("LibMatic-Settings-1.0", true)
    if Settings then
        opts.args.profiles_tab = Settings:BuildProfilesPage({ db = self.db, order = 90 })
    end
    LibStub("AceConfig-3.0"):RegisterOptionsTable("CritMaticOptions", opts)

    self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileChanged")
    self.db.RegisterCallback(self, "OnProfileCopied", "OnProfileChanged")
    self.db.RegisterCallback(self, "OnProfileReset", "OnProfileChanged")

    local Broadcast = LibStub("LibMatic-Broadcast-1.0", true)
    if Broadcast then
        Broadcast:Register({
            name = "CritMatic",
            commPrefix = "CRITMATICBC",
            electionEnabled = false,
        })
    end

    -- Some Classic/TBC clients may not expose GetAddOnMetadata as a global.
    -- Newer clients provide addon metadata via C_AddOns.
    local function _GetAddOnMetadata(addonName, field)
        if _G.GetAddOnMetadata then
            return _G.GetAddOnMetadata(addonName, field)
        end
        if _G.C_AddOns and _G.C_AddOns.GetAddOnMetadata then
            return _G.C_AddOns.GetAddOnMetadata(addonName, field)
        end
        return nil
    end

    local version = _GetAddOnMetadata("CritMatic", "Version") or "0"

    -- Some Classic/TBC clients may not expose IsAddOnLoaded as a global.
    -- Use the C_AddOns API when available, otherwise fall back to a safe default.
    local function _IsAddOnLoaded(addonName)
        if _G.IsAddOnLoaded then
            return _G.IsAddOnLoaded(addonName)
        end
        if _G.C_AddOns and _G.C_AddOns.IsAddOnLoaded then
            return _G.C_AddOns.IsAddOnLoaded(addonName)
        end
        return false
    end

    local function compareVersions(version1, version2)
        local function splitVersion(str)
            local parts = {}
            for part in string.gmatch(str, "[%d]+") do
                table.insert(parts, tonumber(part))
            end
            return parts
        end

        local parts1 = splitVersion(version1)
        local parts2 = splitVersion(version2)

        for i = 1, math.max(#parts1, #parts2) do
            local num1 = parts1[i] or 0
            local num2 = parts2[i] or 0
            if num1 > num2 then
                return true -- version1 is newer
            elseif num1 < num2 then
                return false -- version2 is newer
            end
        end

        return false
    end

    function Critmatic:OnCommReceived(prefix, message, distribution, sender)
        if message and version then
            local isNewerVersion = compareVersions(tostring(message), tostring(version))
            if isNewerVersion and not Critmatic.hasDisplayedUpdateMessage then
                Critmatic:Print(CritMaticRed .. L["new_version_notification"] .. "|r" .. CritMaticGray .. L["new_version_notification_part"] .. "|r")
                Critmatic.hasDisplayedUpdateMessage = true
            end
        end
    end

    --if self.db.profile.isCritLogFrameShown then
    toggleCritMaticCritLog() -- This will show the frame if it was shown before
    --end

    Critmatic.oldVersion = Critmatic.db.profile.oldVersion
    Critmatic.newVersion = tostring(version)
    if Critmatic.newVersion and Critmatic.oldVersion then

        local isNewerVersion = compareVersions(Critmatic.newVersion, Critmatic.oldVersion)
        if isNewerVersion then
            if Critmatic.db.profile.generalSettings.isChangeLogAutoPopUpEnabled then
                Critmatic.showChangeLog()
            end

            Critmatic.db.profile.oldVersion = Critmatic.newVersion

        end

    end
    -- Function to broadcast your version
    function Critmatic:BroadcastVersion()
        if IsInGuild() then
            self:SendCommMessage("Critmatic", version, "GUILD")
        end

        local inInstance, instanceType = IsInInstance()
        if inInstance and (instanceType == "pvp" or instanceType == "arena") then
            return
        end

        if IsInGroup() then
            if IsPartyLFG and IsPartyLFG() then
                self:SendCommMessage("Critmatic", version, "INSTANCE_CHAT")
            elseif IsInRaid() then
                self:SendCommMessage("Critmatic", version, "RAID")
            else
                self:SendCommMessage("Critmatic", version, "PARTY")
            end
        end
    end

    -- GROUP_ROSTER_UPDATE fires frequently; coalesce broadcasts within a short window.
    function Critmatic:GROUP_ROSTER_UPDATE()
        if self._versionBroadcastPending then
            return
        end
        self._versionBroadcastPending = true
        self:ScheduleTimer(function()
            self._versionBroadcastPending = false
            self:BroadcastVersion()
        end, 5)
    end

    -- Register the slash commands
    Critmatic:RegisterChatCommand("critmatic", "OpenOptions")
    Critmatic:RegisterChatCommand("cm", "OpenOptions")
    Critmatic:RegisterChatCommand(L["slash_cmlog"], "OpenChangeLog")
    Critmatic:RegisterChatCommand(L["slash_cmcritlog"], function()
        toggleCritMaticCritLog()
    end)

    local function capitalizeFirstLetterOfEachWord(str)
        return (str:gsub("(%a)([%w_']*)", function(first, rest)
            return first:upper() .. rest:lower()
        end))
    end
    -- Function to handle the slash command input
    local function resetSingleSpellDataSlashCommand(input)
        if not input or input:trim() == "" then
            Critmatic:Print(CritMaticRed .. "Please provide a spell name!" .. "|r")
            return
        end

        local capitalizedInput = capitalizeFirstLetterOfEachWord(input)
        local inputSpellName = capitalizedInput:lower() -- Standardize for comparison

        local spellFound = false
        for spellID, _ in pairs(CritMaticData) do
            local spellName = GetSpellInfo(spellID)
            if spellName and spellName:lower() == inputSpellName then
                -- Reset data for the found spell
                CritMaticData[spellID] = nil -- or reset to initial state if needed
                spellFound = true
                Critmatic:Print(CritMaticGoldYellow .. capitalizedInput .. "|r" .. CritMaticRed .. " data has been reset." .. "|r")
                RedrawCritMaticWidget()
                break
            end
        end

        if not spellFound then
            Critmatic:Print(CritMaticGoldYellow .. capitalizedInput .. "|r" .. CritMaticRed .. " is not a tracked spell currently" .. "|r")
        end

    end

    local function ignoredSpellSlashCommand(input)
        if not input or input:trim() == "" then
            Critmatic:Print(CritMaticRed .. "Please provide a spell name!" .. "|r")
            return
        end

        local capitalizedInput = capitalizeFirstLetterOfEachWord(input)
        local inputSpellName = capitalizedInput:lower() -- Standardize for comparison

        local spellFound = false
        for spellID, _ in pairs(CritMaticData) do
            local spellName = GetSpellInfo(spellID)
            if spellName and spellName:lower() == inputSpellName then
                spellFound = true
                break
            end
        end

        if not spellFound then
            Critmatic:Print(CritMaticRed .. capitalizedInput .. "|r" .. CritMaticWhite .. " is not a tracked spell currently" .. "|r")
            return
        end

        -- Add the spell name to the ignoredSpells table
        Critmatic.ignoredSpells[inputSpellName] = true
        Critmatic:Print(CritMaticGoldYellow .. capitalizedInput .. "|r" .. CritMaticWhite .. " added to " .. CritMaticRed .. "ignored" .. "|r " .. CritMaticWhite .. "spells." .. "|r")
        RedrawCritMaticWidget()
    end

    local function ListIgnoredSpells()
        if not Critmatic.ignoredSpells or next(Critmatic.ignoredSpells) == nil then
            Critmatic:Print(CritMaticRed .. "No spells are currently being ignored." .. "|r")
            return
        end

        Critmatic:Print(CritMaticRed .. "Ignored" .. "|r " .. "Spells:")
        for spellName, _ in pairs(Critmatic.ignoredSpells) do
            Critmatic:Print(CritMaticGoldYellow .. "- " .. capitalizeFirstLetterOfEachWord(spellName) .. "|r")
        end
    end

    local function RemoveIgnoredSpell(input)
        if not input or input:trim() == "" then
            Critmatic:Print(CritMaticRed .. "Please provide a spell name." .. "|r")
            return
        end

        local spellName = input:lower()  -- assuming spell names are stored in lowercase
        if Critmatic.ignoredSpells and Critmatic.ignoredSpells[spellName] then
            Critmatic.ignoredSpells[spellName] = nil
            Critmatic:Print(CritMaticGoldYellow .. capitalizeFirstLetterOfEachWord(spellName) .. "|r" .. CritMaticRed .. " has been removed from ignored spells." .. "|r")
            RedrawCritMaticWidget()
        else
            Critmatic:Print(CritMaticRed .. "Spell not found in ignored spells." .. "|r")
        end
    end
    -- Function to wipe all ignored spells
    local function WipeIgnoredSpells()
        if not Critmatic.ignoredSpells or next(Critmatic.ignoredSpells) == nil then
            Critmatic:Print(CritMaticRed .. "The ignored spells list is already empty!" .. "|r")
            return
        end

        wipe(Critmatic.ignoredSpells)  -- Clear the table
        Critmatic:Print(CritMaticRed .. "All ignored spells have been removed." .. "|r")
        RedrawCritMaticWidget()
    end
    local function ignoredTargetSlashCommand(input)
        local inputTargetID = tonumber(input)
        if not input or not inputTargetID  then
            Critmatic:Print(CritMaticRed .. "Please provide a target id!" .. "|r")
            return
        end

        -- Add the target id to the ignoredTargets table
        Critmatic.ignoredTargets[inputTargetID] = true
        Critmatic:Print(CritMaticGoldYellow .. input .. "|r" .. CritMaticWhite .. " added to " .. CritMaticRed .. "ignored" .. "|r " .. CritMaticWhite .. "targets." .. "|r")
        RedrawCritMaticWidget()
    end

    local function ListIgnoredTargets()
        if not Critmatic.ignoredTargets or next(Critmatic.ignoredTargets) == nil then
            Critmatic:Print(CritMaticRed .. "No targets are currently being ignored." .. "|r")
            return
        end

        Critmatic:Print(CritMaticRed .. "Ignored" .. "|r " .. "Targets:")
        for targetID, _ in pairs(Critmatic.ignoredTargets) do
            Critmatic:Print(CritMaticGoldYellow .. "- " .. tostring(targetID) .. "|r")
        end
    end
    local function RemoveIgnoredTarget(input)
        local targetID = tonumber(input)
        if not input or not targetID then
            Critmatic:Print(CritMaticRed .. "Please provide a target id." .. "|r")
            return
        end

        if Critmatic.ignoredTargets and Critmatic.ignoredTargets[targetID] then
            Critmatic.ignoredTargets[targetID] = nil
            Critmatic:Print(CritMaticGoldYellow .. tostring(targetID) .. "|r" .. CritMaticRed .. " has been removed from ignored targets." .. "|r")
            RedrawCritMaticWidget()
        else
            Critmatic:Print(CritMaticRed .. "Target not found in ignored targets." .. "|r")
        end
    end
    -- Function to wipe all ignored targets
    local function WipeIgnoredTargets()
        if not Critmatic.ignoredTargets or next(Critmatic.ignoredTargets) == nil then
            Critmatic:Print(CritMaticRed .. "The ignored targets list is already empty!" .. "|r")
            return
        end

        wipe(Critmatic.ignoredTargets)  -- Clear the table
        Critmatic:Print(CritMaticRed .. "All ignored targets have been removed." .. "|r")
        RedrawCritMaticWidget()
    end

    Critmatic:RegisterChatCommand("cmhelp", function()
        self:Print(CritMaticBrown .. "Commands:|r")
        self:Print(CritMaticBrown .. "There can be is a short calibration period, if you you dont have any crit data, level up or have multiple new gear upgrades" .. "|r")
        self:Print(CritMaticGoldYellow .. "/cm" .. "|r " .. CritMaticGray .. "- Open the CritMatic options menu." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmlog" .. "|r " .. CritMaticGray .. "- Open the CritMatic changelog." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmcritlog" .. "|r " .. CritMaticGray .. "- Open the CritMatic crit log." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmcritlogdefaultpos" .. "|r " .. CritMaticGray .. "- Resets the Crit Log position. Causes a Reload." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmdeletespelldata spell name" .. "|r " .. CritMaticGray .. "- Reset a single spell's data." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmreset" .. "|r " .. CritMaticGray .. "- Reset all CritMatic data." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmignore  spell name" .. "|r " .. CritMaticGray .. "- Ignore a spell." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmignoredspells" .. "|r " .. CritMaticGray .. "- List all ignored spells." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmremoveignoredspell spell name" .. "|r  " .. CritMaticGray .. "- Remove a spell from the ignored spells list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmwipeignoredspells" .. "|r  " .. CritMaticGray .. "- Remove all spells from the ignored spells list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmignoretarget  target id" .. "|r " .. CritMaticGray .. "- Ignore a target." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmignoredtargets" .. "|r " .. CritMaticGray .. "- List all ignored targets." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmremoveignoredtarget target id" .. "|r  " .. CritMaticGray .. "- Remove a target from the ignored target list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/cmwipeignoredtargets" .. "|r  " .. CritMaticGray .. "- Remove all targets from the ignored targets list." .. "|r")
    end)
    Critmatic:RegisterChatCommand("cmdeletespelldata", resetSingleSpellDataSlashCommand)
    Critmatic:RegisterChatCommand("cmwipeignoredspells", WipeIgnoredSpells)
    Critmatic:RegisterChatCommand("cmremoveignoredspell", RemoveIgnoredSpell)
    Critmatic:RegisterChatCommand("cmignoredspells", ListIgnoredSpells)
    Critmatic:RegisterChatCommand("cmwipeignoredtargets", WipeIgnoredTargets)
    Critmatic:RegisterChatCommand("cmignoredtargets", ListIgnoredTargets)
    Critmatic:RegisterChatCommand(L["slash_cmignore"], ignoredSpellSlashCommand)
    Critmatic:RegisterChatCommand(L["slash_cmreset"], "CritMaticReset")
    Critmatic:RegisterChatCommand(L["slash_cmignoretarget"], ignoredTargetSlashCommand)

    self:RegisterComm("Critmatic")
    Critmatic:RegisterEvent("GROUP_ROSTER_UPDATE")
    -- Function to handle incoming messages

    if GameTooltip and GameTooltip.SetAction then
        hooksecurefunc(GameTooltip, "SetAction", AddHighestHitsToTooltip)
    end
    local SpellbookTooltip = _IsAddOnLoaded("ElvUI") and _G.ElvUISpellBookTooltip or _G.GameTooltip
    if SpellbookTooltip and SpellbookTooltip.SetSpellBookItem then
        hooksecurefunc(SpellbookTooltip, "SetSpellBookItem", AddHighestHitsToTooltip)
    end
    if GameTooltip and GameTooltip.SetPetAction then
        hooksecurefunc(GameTooltip, "SetPetAction", AddHighestHitsToPetTooltip)
    end

    function Critmatic:CritMaticLoaded()
        self:Print(CritMaticGray .. " " .. L["version_string"] .. "|r" .. CritMaticWhite .. " " .. version .. "|r " .. CritMaticGray .. L["critmatic_loaded"] .. CritMaticGoldYellow .. "  /cm" .. "|r" .. CritMaticGray .. " " .. L["critmatic_loaded_for_options"] .. "|r " .. CritMaticGoldYellow .. L["critmatic_loaded_cmhelp"] .. "|r " .. CritMaticGray .. L["critmatic_loaded_for_all_slash_commands"] .. "|r ")
    end

    local f = Critmatic.CreateNewMessageFrame()
    -- Ensure Ace3 and AceGUI are loaded

    function Critmatic:TimerCritMaticLoaded()
        Critmatic:CritMaticLoaded()
    end

    if _IsAddOnLoaded("ElvUI") then
        Critmatic:ScheduleTimer("TimerCritMaticLoaded", 8)
    else
        Critmatic:ScheduleTimer("TimerCritMaticLoaded", 4)
    end


    -- Flag to indicate whether the message has been displayed
    Critmatic.hasDisplayedUpdateMessage = false

end

function Critmatic:OnEnable()
    -- Called when the addon is fully loaded and all saved variables are available.
    CritMaticData = _G["CritMaticData"]
    CritMatic_ignoredSpells = CritMatic_ignoredSpells or {}
    Critmatic.ignoredSpells = CritMatic_ignoredSpells
    CritMatic_ignoredTargets = CritMatic_ignoredTargets or {}
    Critmatic.ignoredTargets = CritMatic_ignoredTargets
    -- Combat-event registration. Retail Midnight 12.0 blocks
    -- COMBAT_LOG_EVENT_UNFILTERED for unprivileged addons (Ace3
    -- maintainer nevcairiel confirmed in WowAce issue #697:
    -- "Blizzard blocked the ability to use the
    -- COMBAT_LOG_EVENT_UNFILTERED event"). Attempting to register it
    -- fires ADDON_ACTION_FORBIDDEN and taints the addon so every
    -- subsequent RegisterEvent also gets FORBIDDEN. So on retail
    -- Midnight we skip combat-event registration entirely and tell the
    -- user once. Classic flavors work as before.
    local _, _, _, ifaceVer = GetBuildInfo()
    local isRetailMidnight = (_G.WOW_PROJECT_ID == _G.WOW_PROJECT_MAINLINE)
            and ifaceVer and ifaceVer >= 120000
    if isRetailMidnight then
        Critmatic:Print(CritMaticGray .. "Combat tracking disabled on retail Midnight 12.0+ (Blizzard's combat-addon restriction). Classic flavors are unaffected.|r")
    elseif Critmatic._combatHandler then
        local cf = Critmatic._combatLogFrame
        local function dispatch(event, ...)
            Critmatic._combatHandler(cf, event, ...)
        end
        Critmatic:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", dispatch)
        Critmatic:RegisterEvent("PLAYER_REGEN_ENABLED", dispatch)
        Critmatic:RegisterEvent("NAME_PLATE_UNIT_ADDED", function(_, unit) Critmatic.TrackUnit(unit) end)
        Critmatic:RegisterEvent("NAME_PLATE_UNIT_REMOVED", function(_, unit)
            if unit then
                local g = UnitGUID(unit)
                if g then Critmatic._guidToUnit[g] = nil end
            end
        end)
        Critmatic:RegisterEvent("PLAYER_TARGET_CHANGED", function() Critmatic.TrackUnit("target") end)
        Critmatic:RegisterEvent("UPDATE_MOUSEOVER_UNIT", function() Critmatic.TrackUnit("mouseover") end)
    end
    -- Now that we know our data is available, we can safely draw the widget.
    if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    if Critmatic.SetupMinimapIcon then Critmatic.SetupMinimapIcon() end
    if Critmatic.UpdateBroker then Critmatic.UpdateBroker() end
end

function Critmatic:OnDisable()
    -- Code to run when the addon is disabled.
end
function Critmatic:OpenOptions()
    LibStub("AceConfigDialog-3.0"):Open("CritMaticOptions")
end
function Critmatic:OpenCritLog()
    toggleCritMaticCritLog()
end
function Critmatic:OpenChangeLog()
    self.showChangeLog()
end

function Critmatic:CritMaticReset()
    wipe(CritMaticData)
    InvalidateAggregateCache()
    Critmatic:Print(CritMaticRed .. L["critmatic_reset"] .. "|r")
    RedrawCritMaticWidget()

end

function Critmatic:OnProfileChanged()
    if Critmatic.db.profile.crits == nil then
        Critmatic.db.profile.crits = {}
    end
    _G.CritMaticData = Critmatic.db.profile.crits
    InvalidateAggregateCache()
    if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    LibStub("AceConfigRegistry-3.0"):NotifyChange("CritMaticOptions")
    Critmatic:Print(CritMaticGoldYellow .. L["profile_changed_reload"] .. "|r")
end

-- Register an event that fires when the player hits an enemy.
-- Frame is created here at load time, but RegisterEvent is deferred to
-- Critmatic:OnEnable() to dodge a retail Midnight 12.0 main-chunk
-- ADDON_ACTION_FORBIDDEN fired against pre-ADDON_LOADED registrations.
local f = CreateFrame("Frame")
Critmatic._combatLogFrame = f

Critmatic._guidToUnit = {}
local GUILD_BROADCAST_COOLDOWN = 60

function Critmatic.TrackUnit(unit)
    if unit and UnitExists(unit) then
        local guid = UnitGUID(unit)
        if guid then
            Critmatic._guidToUnit[guid] = unit
        end
    end
end

local function CritMaticGreyLevel(level)
    if level <= 5 then
        return 0
    elseif level <= 39 then
        return level - math.floor(level / 10) - 5
    elseif level <= 59 then
        return level - math.floor(level / 5) - 1
    else
        return level - 9
    end
end

local function CritMaticIsTrivialGUID(destGUID)
    if not destGUID then
        return false
    end
    local unit = Critmatic._guidToUnit[destGUID]
    if not (unit and UnitExists(unit) and UnitGUID(unit) == destGUID) then
        Critmatic._guidToUnit[destGUID] = nil
        return false
    end
    local ulvl = UnitLevel(unit)
    if not ulvl or ulvl <= 0 then
        return false
    end
    return ulvl <= CritMaticGreyLevel(UnitLevel("player") or 0)
end

-- Variables to hold the highest values during combat
local highestCritDuringCombat = 0
local highestCritHealDuringCombat = 0
local highestCritSpellName = ""
local highestCritHealSpellName = ""
local function combatHandler(self, event, ...)


    if event == "COMBAT_LOG_EVENT_UNFILTERED" then
        local eventInfo = { CombatLogGetCurrentEventInfo() }

        local _, eventType, _, sourceGUID, _, _, _, destGUID = unpack(eventInfo)
        local _, spellID, spellName, spellSchool, amount, overhealing, absorbed, critical
        if eventType == "SWING_DAMAGE" then
            spellName = "Auto Attack"
            spellID = 6603 -- or specify the path to a melee icon, if you have one
            amount, _, _, _, _, _, critical = unpack(eventInfo, 12, 18)
        elseif eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" then
            spellID, spellName, spellSchool = unpack(eventInfo, 12, 14)
            amount, overhealing, absorbed, critical = unpack(eventInfo, 15, 18)
        elseif eventType == "SPELL_DAMAGE" or eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "RANGE_DAMAGE" then
            spellID, spellName, spellSchool = unpack(eventInfo, 12, 14)
            amount, overhealing, _, _, _, absorbed, critical = unpack(eventInfo, 15, 21)
        end

        local localizedSpellName = GetSpellInfo(spellID)
        local baseSpellName = localizedSpellName
        if spellID == 6603 and not baseSpellName then
            baseSpellName = "Auto Attack"
        end

        local LSM = LibStub("LibSharedMedia-3.0")
        local soundCrit = LSM:Fetch("sound", Critmatic.db.profile.soundSettings.damageCrit)
        local soundNormal = LSM:Fetch("sound", Critmatic.db.profile.soundSettings.damageNormal)
        local soundHealCrit = LSM:Fetch("sound", Critmatic.db.profile.soundSettings.healCrit)
        local soundHealNormal = LSM:Fetch("sound", Critmatic.db.profile.soundSettings.healNormal)

        local isHealEvent = eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL"
        if (sourceGUID == UnitGUID("player") or sourceGUID == UnitGUID("pet"))
                and (isHealEvent or destGUID ~= UnitGUID("player"))
                and (eventType == "SPELL_DAMAGE" or eventType == "SWING_DAMAGE" or eventType == "RANGE_DAMAGE" or eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" or eventType == "SPELL_PERIODIC_DAMAGE")
                and amount and amount > 0 then

            if spellID then
                CritMaticData[spellID] = CritMaticData[spellID] or {
                    highestCrit = 0,
                    highestCritOld = 0,
                    highestNormal = 0,
                    highestNormalOld = 0,
                    highestHealCrit = 0,
                    highestHealCritOld = 0,
                    highestHeal = 0,
                    highestHealOld = 0,
                    spellIcon = select(3, GetSpellInfo(spellID)),
                }
                if not CritMaticData[spellID].spellIcon then
                    CritMaticData[spellID].spellIcon = select(3, GetSpellInfo(spellID))
                end

                if baseSpellName and Critmatic.ignoredSpells and Critmatic.ignoredSpells[baseSpellName:lower()] then
                    return
                end
                if destGUID and Critmatic.ignoredTargets then
                    local _, _, _, _, _, destID = strsplit("-", destGUID)
                    destID = tonumber(destID)
                    if destID and Critmatic.ignoredTargets[destID] then
                        return
                    end
                end
                if Critmatic.db.profile.generalSettings.ignoreTrivialTargets and CritMaticIsTrivialGUID(destGUID) then
                    return
                end
                --print(CombatLogGetCurrentEventInfo())



                if eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" then
                    if critical then

                        -- When the event is a heal and it's a critical heal.
                        if amount > CritMaticData[spellID].highestHealCrit and amount <= MAX_HIT then
                            CritMaticData[spellID].highestHealCritOld = CritMaticData[spellID].highestHealCrit
                            CritMaticData[spellID].highestHealCrit = amount

                            highestCritHealDuringCombat = amount
                            highestCritHealSpellName = baseSpellName
                            if not Critmatic.db.profile.soundSettings.muteAllSounds then
                                PlaySoundFile(soundHealCrit)
                            end

                            --PlaySoundFile("Interface\\AddOns\\CritMatic\\Media\\Sounds\\LevelUp.ogg", "SFX")

                            if Critmatic.db.profile.generalSettings.alertNotificationsEnabled then
                                Critmatic.ShowNewHealCritMessage(baseSpellName, amount, CritMaticData[spellID].spellIcon, CritMaticData[spellID].highestHealCritOld)
                            end

                            if Critmatic.db.profile.generalSettings.chatNotificationsEnabled then
                                Critmatic:Print(CritMaticGoldYellow .. L["chat_crit_heal"] .. baseSpellName .. ": |r" ..
                                        CritMaticData[spellID].highestHealCrit .. Critmatic.FormatPreviousRecord(CritMaticData[spellID].highestHealCritOld, CritMaticData[spellID].highestHealCrit))
                            end
                            InvalidateAggregateCache()
                            RecordEvent(spellID)
                            RedrawCritMaticWidget()
                        end
                    elseif not critical and not Critmatic.db.profile.generalSettings.critsOnlyEnabled then
                        if amount > CritMaticData[spellID].highestHeal and amount <= MAX_HIT then
                            CritMaticData[spellID].highestHealOld = CritMaticData[spellID].highestHeal
                            CritMaticData[spellID].highestHeal = amount

                            if not Critmatic.db.profile.soundSettings.muteAllSounds then
                                PlaySoundFile(soundHealNormal)
                            end

                            --PlaySoundFile("Interface\\AddOns\\CritMatic\\Media\\Sounds\\Heaven.ogg", "SFX")

                            if Critmatic.db.profile.generalSettings.alertNotificationsEnabled then
                                Critmatic.ShowNewHealMessage(baseSpellName, amount, CritMaticData[spellID].spellIcon, CritMaticData[spellID].highestHealOld)
                            end

                            if Critmatic.db.profile.generalSettings.chatNotificationsEnabled then
                                Critmatic:Print(" " .. CritMaticWhite .. L["chat_heal"] .. baseSpellName .. ": " .. CritMaticData[spellID].highestHeal .. Critmatic.FormatPreviousRecord(CritMaticData[spellID].highestHealOld, CritMaticData[spellID].highestHeal) .. "|r")
                            end
                            InvalidateAggregateCache()
                            RecordEvent(spellID)
                            RedrawCritMaticWidget()
                        end
                    end
                elseif eventType == "SPELL_DAMAGE" or eventType == "SWING_DAMAGE" or eventType == "SPELL_PERIODIC_DAMAGE" or eventType == "RANGE_DAMAGE" then
                    if critical then
                        -- When the event is damage and it's a critical hit.
                        if amount > CritMaticData[spellID].highestCrit and amount <= MAX_HIT then
                            CritMaticData[spellID].highestCritOld = CritMaticData[spellID].highestCrit
                            CritMaticData[spellID].highestCrit = amount
                            highestCritDuringCombat = amount
                            highestCritSpellName = baseSpellName
                            --PlaySound(888, "SFX")
                            if not Critmatic.db.profile.soundSettings.muteAllSounds then
                                PlaySoundFile(soundCrit)
                            end

                            --PlaySoundFile("Interface\\AddOns\\CritMatic\\Media\\Sounds\\LevelUp.ogg", "SFX")
                            if Critmatic.db.profile.generalSettings.alertNotificationsEnabled then
                                Critmatic.ShowNewCritMessage(baseSpellName, amount, CritMaticData[spellID].spellIcon, CritMaticData[spellID].highestCritOld)
                            end

                            if Critmatic.db.profile.generalSettings.chatNotificationsEnabled then
                                Critmatic:Print(CritMaticGoldYellow .. L["chat_crit"] .. baseSpellName .. ": |r" ..
                                        CritMaticData[spellID].highestCrit .. Critmatic.FormatPreviousRecord(CritMaticData[spellID].highestCritOld, CritMaticData[spellID].highestCrit))
                            end
                            InvalidateAggregateCache()
                            RecordEvent(spellID)
                            RedrawCritMaticWidget()
                        end
                    elseif not critical and not Critmatic.db.profile.generalSettings.critsOnlyEnabled then
                        -- When the event is damage but it's not a critical hit.
                        if amount > CritMaticData[spellID].highestNormal and amount <= MAX_HIT then
                            CritMaticData[spellID].highestNormalOld = CritMaticData[spellID].highestNormal
                            CritMaticData[spellID].highestNormal = amount
                            if not Critmatic.db.profile.soundSettings.muteAllSounds then
                                PlaySoundFile(soundNormal)
                            end

                            --PlaySoundFile("Interface\\AddOns\\CritMatic\\Media\\Sounds\\Heroism_Cast.ogg", "SFX")
                            if Critmatic.db.profile.generalSettings.alertNotificationsEnabled then
                                Critmatic.ShowNewNormalMessage(baseSpellName, amount, CritMaticData[spellID].spellIcon, CritMaticData[spellID].highestNormalOld)
                            end

                            if Critmatic.db.profile.generalSettings.chatNotificationsEnabled then
                                Critmatic:Print(CritMaticWhite .. L["chat_hit"] .. baseSpellName .. ": " ..
                                        CritMaticData[spellID].highestNormal .. Critmatic.FormatPreviousRecord(CritMaticData[spellID].highestNormalOld, CritMaticData[spellID].highestNormal) .. "|r")
                            end
                            InvalidateAggregateCache()
                            RecordEvent(spellID)
                            RedrawCritMaticWidget()
                        end
                    end
                end

                -- end


            end
        end
    elseif event == "PLAYER_REGEN_ENABLED" then
        -- Questie-style channel selection: pick exactly ONE group-context
        -- channel based on where the player is, and optionally also send to
        -- guild as a separate concern. /say is intentionally not an option:
        -- /say is muted in instances and mixes badly with combat-end timing,
        -- so removing it eliminates a class of "did it send?" bugs.
        local inInstance, instanceType = IsInInstance()

        local function sendChat(chatType)
            local Broadcast = LibStub("LibMatic-Broadcast-1.0", true)
            if highestCritDuringCombat > 0 then
                local text = "CritMatic: " .. L["social_crit"] .. highestCritSpellName .. ": " .. highestCritDuringCombat
                if Broadcast then
                    Broadcast:Broadcast("CritMatic", { text = text, marker = 8, channel = chatType })
                else
                    SendChatMessage("{rt1}" .. text, chatType)
                end
            end
            if highestCritHealDuringCombat > 0 then
                local text = "CritMatic: " .. L["social_crit_heal"] .. highestCritHealSpellName .. ": " .. highestCritHealDuringCombat
                if Broadcast then
                    Broadcast:Broadcast("CritMatic", { text = text, marker = 8, channel = chatType })
                else
                    SendChatMessage("{rt1}" .. text, chatType)
                end
            end
        end

        if IsInGroup() then
            local social = Critmatic.db.profile.social
            local channel, allowed
            if inInstance and instanceType == "pvp" then
                channel = "INSTANCE_CHAT"
                allowed = social.canSendCritsToBattleGrounds
            elseif IsPartyLFG and IsPartyLFG() then
                channel = "INSTANCE_CHAT"
                if IsInRaid() then
                    allowed = social.canSendCritsToRaid
                else
                    allowed = social.canSendCritsToParty
                end
            elseif IsInRaid() then
                channel = "RAID"
                allowed = social.canSendCritsToRaid
            else
                channel = "PARTY"
                allowed = social.canSendCritsToParty
            end
            if allowed then
                sendChat(channel)
            end
        end

        if IsInGuild() and Critmatic.db.profile.social.canSendCritsToGuild then
            local hasContent = highestCritDuringCombat > 0 or highestCritHealDuringCombat > 0
            local now = GetTime()
            if hasContent and (now - (Critmatic._lastGuildBroadcast or 0)) >= GUILD_BROADCAST_COOLDOWN then
                Critmatic._lastGuildBroadcast = now
                sendChat("GUILD")
            end
        end

        highestCritDuringCombat = 0
        highestCritHealDuringCombat = 0
        highestCritSpellName = ""
        highestCritHealSpellName = ""
    end
end

f:SetScript("OnEvent", combatHandler)
Critmatic._combatHandler = combatHandler
