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

local function CritMaticColorEscape(colorTable)
    if type(colorTable) ~= "table" then
        return "|cffffffff"
    end
    local r = math.floor((colorTable[1] or 1) * 255 + 0.5)
    local g = math.floor((colorTable[2] or 1) * 255 + 0.5)
    local b = math.floor((colorTable[3] or 1) * 255 + 0.5)
    return string.format("|cff%02x%02x%02x", r, g, b)
end

-- 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 ShouldAnnounceRecord(oldAmount, newAmount)
    if not Critmatic.db then return true end
    local pct = Critmatic.db.profile.generalSettings.recordAnnounceThreshold or 0
    if pct <= 0 then return true end
    if not oldAmount or oldAmount <= 0 or not newAmount then return true end
    return newAmount >= oldAmount * (1 + pct / 100)
end

local function BindRecordsTable()
    local rdb = Critmatic.recordsDB
    if not rdb then return end
    local p = rdb.profile
    if type(rawget(p, "crits")) ~= "table" then
        rawset(p, "crits", {})
    end
    _G.CritMaticData = p.crits
end

local RECORD_MERGE_FIELDS = { "highestCrit", "highestNormal", "highestHealCrit", "highestHeal" }
local function MergeRecords(dest, src)
    if type(dest) ~= "table" or type(src) ~= "table" then return end
    for spellID, data in pairs(src) do
        if type(data) == "table" then
            local d = dest[spellID]
            if d == nil then
                dest[spellID] = CopyTable(data)
            else
                for i = 1, #RECORD_MERGE_FIELDS do
                    local f = RECORD_MERGE_FIELDS[i]
                    if (data[f] or 0) > (d[f] or 0) then
                        d[f] = data[f]
                    end
                end
                if not d.spellIcon and data.spellIcon then
                    d.spellIcon = data.spellIcon
                end
            end
        end
    end
end

local function GetRecordName(key, record)
    if record and record.displayName then
        return record.displayName
    end
    if type(key) == "number" then
        return GetSpellInfo(key)
    end
    return nil
end

local function GetRecordIcon(key, record)
    if record and record.spellIcon then
        return record.spellIcon
    end
    if type(key) == "number" then
        return select(3, GetSpellInfo(key))
    end
    return nil
end

Critmatic.GetRecordName = GetRecordName

function Critmatic.GetGroupedSpellRecords(opts)
    local grouping = _G.CritMaticRecordGrouping
    if not grouping then return {} end
    return grouping.GroupAndSort(CritMaticData or {}, GetRecordName, opts)
end

Critmatic.GetRecordIcon = GetRecordIcon

local function PetRecordKeyFromGUID(guid)
    if not guid then return nil end
    local id = select(6, strsplit("-", guid))
    id = tonumber(id)
    if not id then return nil end
    return "petauto:" .. id
end

local function GetPetAttackIcon()
    if not (GetPetActionInfo and IsPetAttackAction) then return nil end
    local numSlots = _G.NUM_PET_ACTION_SLOTS or 10
    for i = 1, numSlots do
        if IsPetAttackAction(i) then
            local _, texture, isToken = GetPetActionInfo(i)
            if isToken and type(texture) == "string" then
                texture = _G[texture] or texture
            end
            return texture
        end
    end
    return nil
end

local function GetSpellAggregate(localizedSpellName)
    if aggregateDirty then
        wipe(spellAggregateCache)
        for sID, data in pairs(CritMaticData) do
            local name = GetRecordName(sID, data)
            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 GetGCD()
    local haste = (UnitSpellHaste and UnitSpellHaste("player")) or 0
    local gcd = 1.5 / (1 + haste / 100)
    if gcd < 0.75 then
        gcd = 0.75
    end
    return gcd
end

local function RenderHighestHitsForSpellID(self, recordKey)
    local localizedSpellName = GetRecordName(recordKey, CritMaticData and CritMaticData[recordKey])
    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, castTime = 0, nil
    if type(recordKey) == "number" then
        cooldown = ((_G.GetSpellBaseCooldown and GetSpellBaseCooldown(recordKey)) or 0) / 1000
        castTime = select(4, GetSpellInfo(recordKey))
    end
    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

    local frameName = self.GetName and self:GetName()
    for i = 1, (frameName and self:NumLines() or 0) do
        local gtl = _G[frameName .. "TextLeft" .. i]
        local gtr = _G[frameName .. "TextRight" .. 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 isSpellBook == (_G.BOOKTYPE_PET or "pet") then
            return
        end
        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 spellID then
        RenderHighestHitsForSpellID(self, spellID)
        return
    end
    if IsPetAttackAction and IsPetAttackAction(slot) then
        local key = PetRecordKeyFromGUID(UnitGUID("pet"))
        if key then
            local petType = (UnitCreatureFamily and UnitCreatureFamily("pet")) or (UnitName and UnitName("pet"))
            local tname = self.GetName and self:GetName()
            local line1 = tname and _G[tname .. "TextLeft1"]
            if petType and line1 then
                local cur = line1:GetText()
                if cur and cur ~= "" and not cur:find(petType, 1, true) then
                    line1:SetText(petType .. " " .. cur)
                end
            end
            RenderHighestHitsForSpellID(self, key)
        end
    end
end

local function AddHighestHitsToItemTooltip(self)
    if not self or not self.GetItem then
        return
    end
    local _, itemLink = self:GetItem()
    if not itemLink then
        return
    end
    local getItemSpell = (C_Item and C_Item.GetItemSpell) or _G.GetItemSpell
    if not getItemSpell then
        return
    end
    local resolver = _G.CritMaticItemRecords
    local spellID = resolver and resolver.ResolveItemSpellID(getItemSpell, itemLink)
    if spellID then
        RenderHighestHitsForSpellID(self, spellID)
    end
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)
    local LMAShadow = LibStub("LibMatic-AlertShadow-1.0", true)
    local fset = Critmatic.db.profile.fontSettings
    if LMAShadow then
        LMAShadow:ApplyTextShadow(f.text, fset.shadowStrength, { color = fset.fontShadowColor, offset = fset.fontShadowSize })
    else
        f.text:SetShadowOffset(unpack(fset.fontShadowSize))
        f.text:SetShadowColor(unpack(fset.fontShadowColor))
    end

    return f
end

function Critmatic:RegisterSharedAlertStack()
    local stack = LibStub("LibMatic-AlertStack-1.0", true)
    if not stack then return end
    stack:RegisterAddon("CritMatic", {
        isEnabled = function()
            return Critmatic.db.profile.alertNotificationFormat.position.useSharedPosition ~= false
        end,
        getSaved = function()
            return Critmatic.db.profile.alertNotificationFormat.position.shared
        end,
        setSaved = function(x, y, rev, maxAlerts)
            local p = Critmatic.db.profile.alertNotificationFormat.position
            p.shared = p.shared or {}
            p.shared.x, p.shared.y, p.shared.rev = x, y, rev
            p.shared.maxAlerts = maxAlerts
        end,
        demo = function()
            if Critmatic.PreviewCritAlert then Critmatic.PreviewCritAlert() end
        end,
    })
end

function Critmatic.EnsureCritLogReady()
    if Critmatic._uiReady then
        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
        return
    end
    Critmatic._critLogReadyTries = (Critmatic._critLogReadyTries or 0) + 1
    local renderable = true
    local LSM = LibStub("LibSharedMedia-3.0", true)
    local profile = Critmatic.db and Critmatic.db.profile
    if LSM and profile and profile.critLogFontSettings then
        local path = profile.critLogFontSettings.font and LSM:Fetch("font", profile.critLogFontSettings.font)
        if path then
            local probe = Critmatic._fontProbe
            if not probe then
                probe = UIParent:CreateFontString(nil, "BACKGROUND")
                probe:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
                probe:SetAlpha(0)
                Critmatic._fontProbe = probe
            end
            probe:SetFont(path, 12)
            probe:SetText("Mg")
            renderable = (probe:GetStringWidth() or 0) > 0
        end
    end
    if renderable or Critmatic._critLogReadyTries >= 20 then
        Critmatic._uiReady = true
        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    elseif C_Timer and C_Timer.After then
        C_Timer.After(0.5, Critmatic.EnsureCritLogReady)
    else
        Critmatic._uiReady = true
        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    end
end

function Critmatic:OnInitialize()
    -- Initialization code here.
    self.db = LibStub("AceDB-3.0"):New("CritMaticDB", defaults, true)
    self.recordsDB = LibStub("AceDB-3.0"):New("CritMaticRecordsDB", { profile = { crits = {} } })

    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

    local legacyPerCharRecords = _G.CritMaticData
    BindRecordsTable()

    if not self.db.char._recordsSplit then
        local target = _G.CritMaticData
        -- profile.crits is migrated into recordsDB by the global import below.
        -- Only the per-character legacy CritMaticData needs merging here; re-merging
        -- the shared profile.crits on every fresh alt would resurrect records the
        -- player deleted from recordsDB.
        if type(legacyPerCharRecords) == "table" and legacyPerCharRecords ~= target then
            MergeRecords(target, legacyPerCharRecords)
        end
        self.db.char._recordsSplit = true
    end

    if not self.recordsDB.global._importedSettingsProfiles then
        for profName, profData in pairs(self.db.profiles) do
            if type(profData) == "table" and type(profData.crits) == "table" and next(profData.crits) then
                local destProfile = self.recordsDB.profiles[profName]
                if type(destProfile) ~= "table" then
                    destProfile = {}
                    self.recordsDB.profiles[profName] = destProfile
                end
                if type(destProfile.crits) ~= "table" then
                    destProfile.crits = {}
                end
                MergeRecords(destProfile.crits, profData.crits)
            end
        end
        self.recordsDB.global._importedSettingsProfiles = true
    end

    local opts = Critmatic:CreateOptionsTable()
    local Settings = LibStub("LibMatic-Settings-1.0", true)
    if Settings then
        opts.args.profiles_tab = Settings:BuildProfilesPage({ db = self.db, name = L["settings_profiles_tab"], order = 90 })
        opts.args.record_profiles_tab = Settings:BuildProfilesPage({ db = self.recordsDB, name = L["record_profiles_tab"], order = 91 })
    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")

    self.recordsDB.RegisterCallback(self, "OnProfileChanged", "OnRecordsProfileChanged")
    self.recordsDB.RegisterCallback(self, "OnProfileCopied", "OnRecordsProfileChanged")
    self.recordsDB.RegisterCallback(self, "OnProfileReset", "OnRecordsProfileChanged")

    self:RegisterSharedAlertStack()

    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 and tostring(message):match("^%d[%d%.]*$") 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.global.lastSeenVersion or 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.global.lastSeenVersion = Critmatic.newVersion

        end

    end
    -- Function to broadcast your version
    function Critmatic:BroadcastVersion()
        local inInstance, instanceType = IsInInstance()
        if inInstance and (instanceType == "pvp" or instanceType == "arena") then
            return
        end

        if IsInGuild() then
            self:SendCommMessage("Critmatic", version, "GUILD")
        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(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, data in pairs(CritMaticData) do
            local spellName = GetRecordName(spellID, data)
            if spellName and spellName:lower() == inputSpellName then
                -- Classic spells have one record per rank (multiple spellIDs share
                -- a name), so clear every matching rank, not just the first.
                CritMaticData[spellID] = nil
                spellFound = true
            end
        end

        if spellFound then
            InvalidateAggregateCache()
            Critmatic:Print(CritMaticGoldYellow .. capitalizedInput .. "|r" .. CritMaticRed .. " data has been reset." .. "|r")
            if RedrawCritMaticWidget then RedrawCritMaticWidget() 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, data in pairs(CritMaticData) do
            local spellName = GetRecordName(spellID, data)
            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

    local function printHelp()
        self:Print(CritMaticBrown .. "Commands:|r")
        self:Print(CritMaticBrown .. "There can be a short calibration period if you don't have any crit data yet, level up, or get multiple new gear upgrades." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic" .. "|r " .. CritMaticGray .. "- Open the CritMatic options menu." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic help" .. "|r " .. CritMaticGray .. "- Show this command list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic changelog" .. "|r " .. CritMaticGray .. "- Open the CritMatic changelog." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic critlog" .. "|r " .. CritMaticGray .. "- Open the CritMatic crit log." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic critlogdefaultpos" .. "|r " .. CritMaticGray .. "- Resets the Crit Log position. Causes a Reload." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic deletespelldata spell name" .. "|r " .. CritMaticGray .. "- Reset a single spell's data." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic reset" .. "|r " .. CritMaticGray .. "- Reset all CritMatic data." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic ignore spell name" .. "|r " .. CritMaticGray .. "- Ignore a spell." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic ignoredspells" .. "|r " .. CritMaticGray .. "- List all ignored spells." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic removeignoredspell spell name" .. "|r  " .. CritMaticGray .. "- Remove a spell from the ignored spells list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic wipeignoredspells" .. "|r  " .. CritMaticGray .. "- Remove all spells from the ignored spells list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic ignoretarget target id" .. "|r " .. CritMaticGray .. "- Ignore a target." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic ignoredtargets" .. "|r " .. CritMaticGray .. "- List all ignored targets." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic removeignoredtarget target id" .. "|r  " .. CritMaticGray .. "- Remove a target from the ignored target list." .. "|r")
        self:Print(CritMaticGoldYellow .. "/critmatic wipeignoredtargets" .. "|r  " .. CritMaticGray .. "- Remove all targets from the ignored targets list." .. "|r")
    end

    local function SlashDispatch(input)
        local sub, rest = (input or ""):match("^(%S*)%s*(.-)%s*$")
        sub = (sub or ""):lower()
        if sub == "" then
            Critmatic:OpenOptions()
        elseif sub == "help" then
            printHelp()
        elseif sub == "changelog" or sub == "log" then
            Critmatic:OpenChangeLog()
        elseif sub == "critlog" then
            toggleCritMaticCritLog()
        elseif sub == "critlogdefaultpos" then
            if Critmatic.ResetCritLogPosition then Critmatic.ResetCritLogPosition() end
        elseif sub == "reset" then
            Critmatic:CritMaticReset()
        elseif sub == "deletespelldata" then
            resetSingleSpellDataSlashCommand(rest)
        elseif sub == "ignore" then
            ignoredSpellSlashCommand(rest)
        elseif sub == "ignoredspells" then
            ListIgnoredSpells()
        elseif sub == "removeignoredspell" then
            RemoveIgnoredSpell(rest)
        elseif sub == "wipeignoredspells" then
            WipeIgnoredSpells()
        elseif sub == "ignoretarget" then
            ignoredTargetSlashCommand(rest)
        elseif sub == "ignoredtargets" then
            ListIgnoredTargets()
        elseif sub == "removeignoredtarget" then
            RemoveIgnoredTarget(rest)
        elseif sub == "wipeignoredtargets" then
            WipeIgnoredTargets()
        elseif sub == "test" then
            local style = (rest or ""):lower()
            if style == "levelup" or style == "renown" or style == "cinematic" then
                local guard = LibStub("LibMatic-OverlayGuard-1.0", true)
                if guard and guard.SimulateOverlay then guard:SimulateOverlay(style, 4) end
            end
            if not Critmatic.db.profile.soundSettings.muteAllSounds then
                local LSM = LibStub("LibSharedMedia-3.0", true)
                if LSM then Critmatic:DeferAlertSound(LSM:Fetch("sound", Critmatic.db.profile.soundSettings.damageCrit)) end
            end
            if Critmatic.MessageFrame and Critmatic.MessageFrame.CreateMessage then
                Critmatic.MessageFrame:CreateMessage("Test Crit 12345!", 1, 0.82, 0, "Interface\\Icons\\Spell_Fire_Fireball", 12345)
            end
        else
            self:Print(CritMaticRed .. "Unknown command. Type " .. "|r" .. CritMaticGoldYellow .. "/critmatic help" .. "|r")
        end
    end

    Critmatic:RegisterChatCommand("critmatic", SlashDispatch)
    Critmatic:RegisterChatCommand("cm", SlashDispatch)
    Critmatic:RegisterChatCommand("cmhelp", printHelp)
    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
    if GameTooltip and GameTooltip.HasScript and GameTooltip:HasScript("OnTooltipSetItem") then
        GameTooltip:HookScript("OnTooltipSetItem", AddHighestHitsToItemTooltip)
    end
    if _G.ItemRefTooltip and _G.ItemRefTooltip.HasScript and _G.ItemRefTooltip:HasScript("OnTooltipSetItem") then
        _G.ItemRefTooltip:HookScript("OnTooltipSetItem", AddHighestHitsToItemTooltip)
    end

    local startup = LibStub("LibMatic-Startup-1.0", true)
    if startup then
        startup:Register({
            name            = "CritMatic",
            version         = version,
            optionsCommand  = "/critmatic",
            helpCommand     = "/critmatic help",
            useFactionColors = false,
            brandColor      = "ffd700",
        })
    end

    function Critmatic:CritMaticLoaded()
        if startup then
            startup:PrintBanner("CritMatic")
        end
    end

    -- 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.
    BindRecordsTable()
    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._inCombat = (UnitAffectingCombat and UnitAffectingCombat("player")) and true or false
        Critmatic._playerGUID = UnitGUID("player")
        Critmatic._petGUID = UnitGUID("pet")
        Critmatic:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", dispatch)
        Critmatic:RegisterEvent("PLAYER_REGEN_ENABLED", dispatch)
        Critmatic:RegisterEvent("PLAYER_REGEN_DISABLED", dispatch)
        Critmatic:RegisterEvent("UNIT_PET", function(_, unit)
            if unit == "player" then Critmatic._petGUID = UnitGUID("pet") end
        end)
        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
    Critmatic:RegisterEvent("PLAYER_ENTERING_WORLD", Critmatic.EnsureCritLogReady)
    Critmatic.EnsureCritLogReady()
end

function Critmatic:OnDisable()
    -- Code to run when the addon is disabled.
end
function Critmatic:OpenOptions()
    local s = LibStub("LibMatic-Settings-1.0", true)
    if s then s:OpenPanel("CritMaticOptions") else LibStub("AceConfigDialog-3.0"):Open("CritMaticOptions") end
end

function Critmatic:DeferAlertSound(snd)
    if not snd then return end
    local guard = LibStub("LibMatic-OverlayGuard-1.0", true)
    if guard and guard.RunWhenClear then
        guard:RunWhenClear(function() PlaySoundFile(snd) end)
    else
        PlaySoundFile(snd)
    end
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()
    self:RegisterSharedAlertStack()
    local stack = LibStub("LibMatic-AlertStack-1.0", true)
    if stack then
        stack:SyncFromSaved("CritMatic")
    end
    if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    if Critmatic.SetupMinimapIcon then Critmatic.SetupMinimapIcon() end
    if Critmatic.UpdateBroker then Critmatic.UpdateBroker() end
    LibStub("AceConfigRegistry-3.0"):NotifyChange("CritMaticOptions")
    Critmatic:Print(CritMaticGoldYellow .. L["profile_changed_reload"] .. "|r")
end

function Critmatic:OnRecordsProfileChanged()
    BindRecordsTable()
    InvalidateAggregateCache()
    if RedrawCritMaticWidget then RedrawCritMaticWidget() end
    if Critmatic.UpdateBroker then Critmatic.UpdateBroker() end
    LibStub("AceConfigRegistry-3.0"):NotifyChange("CritMaticOptions")
    Critmatic:Print(CritMaticGoldYellow .. L["records_profile_changed"] .. "|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)
    -- _guidToUnit only feeds the trivial-target filter, so skip it entirely when
    -- that feature is off; otherwise the map grows for the whole session.
    if not (Critmatic.db and Critmatic.db.profile.generalSettings.ignoreTrivialTargets) then
        return
    end
    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 combatRecap = {}
local deferredAlerts = {}
local RECAP_KIND_ORDER = { "crit", "hit", "healcrit", "heal" }
local ALERT_KINDS = {
    crit = { show = "ShowNewCritMessage", soundKey = "damageCrit", chatKey = "chat_crit", colorKey = "fontColorCrit", prefix = "" },
    hit = { show = "ShowNewNormalMessage", soundKey = "damageNormal", chatKey = "chat_hit", colorKey = "fontColor", prefix = "" },
    healcrit = { show = "ShowNewHealCritMessage", soundKey = "healCrit", chatKey = "chat_crit_heal", colorKey = "fontColorCritHeal", prefix = "" },
    heal = { show = "ShowNewHealMessage", soundKey = "healNormal", chatKey = "chat_heal", colorKey = "fontColorHeal", prefix = " " },
}

local function EmitRecordAlert(kind, name, amount, icon, oldAmount)
    local def = ALERT_KINDS[kind]
    if not def then return end
    local profile = Critmatic.db.profile
    if not profile.soundSettings.muteAllSounds then
        local LSM = LibStub("LibSharedMedia-3.0")
        Critmatic:DeferAlertSound(LSM:Fetch("sound", profile.soundSettings[def.soundKey]))
    end
    if profile.generalSettings.alertNotificationsEnabled then
        local show = Critmatic[def.show]
        if show then show(name, amount, icon, oldAmount) end
    end
    if profile.generalSettings.chatNotificationsEnabled then
        local colorTable = profile.fontSettings[def.colorKey]
        Critmatic:Print(def.prefix .. CritMaticColorEscape(colorTable) .. Critmatic.BuildRecordChatLine(L[def.chatKey], name, amount, Critmatic.FormatPreviousRecord(oldAmount, amount)) .. "|r")
    end
end

local function CritMaticIsMutedGroup()
    local g = Critmatic.db.profile.generalSettings
    if not (g.muteAlertsInRaid or g.muteAlertsInDungeon) then
        return false
    end
    local inRaid = IsInRaid()
    if g.muteAlertsInRaid and inRaid then
        return true
    end
    if g.muteAlertsInDungeon and not inRaid and IsInGroup() then
        local inInstance, instanceType = IsInInstance()
        if inInstance and instanceType == "party" then
            return true
        end
    end
    return false
end

local function RouteRecordAlert(kind, name, amount, icon, oldAmount)
    local g = Critmatic.db.profile.generalSettings
    local decision = CritMaticAlertRouting.Decide({
        muted = CritMaticIsMutedGroup(),
        inCombat = Critmatic._inCombat == true,
        defer = g.deferAlertsUntilCombatEnd == true,
        recap = g.showCombatRecap == true,
    })
    if decision.accumulate then
        local entry = combatRecap[name]
        if not entry then
            entry = { name = name, icon = icon }
            combatRecap[name] = entry
        end
        if not entry.icon then entry.icon = icon end
        local prev = entry[kind]
        if not prev or amount > prev.amount then
            entry[kind] = { amount = amount, oldAmount = oldAmount }
        end
    end
    if decision.action == "emit" then
        EmitRecordAlert(kind, name, amount, icon, oldAmount)
    elseif decision.action == "queue" then
        deferredAlerts[#deferredAlerts + 1] = { kind = kind, name = name, amount = amount, icon = icon, oldAmount = oldAmount }
    end
end

local function FlushCombatRecap()
    -- Flush whatever was accumulated/queued during the fight regardless of the
    -- current setting values. Routing.Decide only ever populates one of these
    -- per fight under stable settings, but flushing both means a mid-combat
    -- toggle can never strand alerts in the queue it stopped reading.
    for _, entry in pairs(combatRecap) do
        for i = 1, #RECAP_KIND_ORDER do
            local kind = RECAP_KIND_ORDER[i]
            local rec = entry[kind]
            if rec then
                EmitRecordAlert(kind, entry.name, rec.amount, entry.icon, rec.oldAmount)
            end
        end
    end
    for i = 1, #deferredAlerts do
        local d = deferredAlerts[i]
        EmitRecordAlert(d.kind, d.name, d.amount, d.icon, d.oldAmount)
    end
    wipe(combatRecap)
    wipe(deferredAlerts)
end

local function combatHandler(self, event, ...)


    if event == "COMBAT_LOG_EVENT_UNFILTERED" then
        local _, eventType, _, sourceGUID, _, _, _, destGUID,
              _, _, _, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21 = CombatLogGetCurrentEventInfo()

        -- Cheap relevance gate first: of the hundreds of CLEU events per second
        -- in a raid, only the player's and pet's events matter. Bail in two
        -- compares before doing any parsing, allocation, or C-calls.
        local playerGUID = Critmatic._playerGUID
        if not playerGUID then
            playerGUID = UnitGUID("player")
            Critmatic._playerGUID = playerGUID
        end
        -- _petGUID is seeded in OnEnable and refreshed on UNIT_PET, so a nil here
        -- legitimately means "no pet" and costs no per-event C-call.
        local fromPlayer = sourceGUID == playerGUID
        local fromPet = (not fromPlayer) and sourceGUID ~= nil and sourceGUID == Critmatic._petGUID
        if not (fromPlayer or fromPet) then
            return
        end

        local isHealEvent = eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL"
        local isDamageEvent = eventType == "SPELL_DAMAGE" or eventType == "SWING_DAMAGE"
                or eventType == "RANGE_DAMAGE" or eventType == "SPELL_PERIODIC_DAMAGE"
        if not (isHealEvent or isDamageEvent) then
            return
        end
        -- Ignore self-inflicted damage (environmental, etc.) but allow self-heals.
        if not (isHealEvent or destGUID ~= playerGUID) then
            return
        end

        local spellID, spellName, amount, critical
        local petAutoNameResolved = false
        if eventType == "SWING_DAMAGE" then
            if fromPet then
                spellID = PetRecordKeyFromGUID(sourceGUID) or 6603
                local petType = (UnitCreatureFamily and UnitCreatureFamily("pet")) or (UnitName and UnitName("pet"))
                if petType then
                    spellName = petType .. " " .. L["attack"]
                    petAutoNameResolved = true
                else
                    spellName = L["pet_attack"]
                end
            else
                spellID = 6603
                spellName = L["auto_attack"]
            end
            amount, critical = a12, a18
        elseif isHealEvent then
            spellID, spellName = a12, a13
            amount, critical = a15, a18
        else
            -- SPELL_DAMAGE / SPELL_PERIODIC_DAMAGE / RANGE_DAMAGE
            spellID, spellName = a12, a13
            amount, critical = a15, a21
        end

        if not (amount and amount > 0) or not spellID then
            return
        end

        local localizedSpellName = GetSpellInfo(spellID)
        local baseSpellName = localizedSpellName or spellName

        -- Filter BEFORE minting a record so ignored spells/targets never create
        -- a SavedVariables entry. The next() guards keep the string/strsplit
        -- garbage out of the hot path when the ignore lists are empty.
        if baseSpellName and Critmatic.ignoredSpells and next(Critmatic.ignoredSpells)
                and Critmatic.ignoredSpells[baseSpellName:lower()] then
            return
        end
        if destGUID and Critmatic.ignoredTargets and next(Critmatic.ignoredTargets) then
            local _, _, _, _, _, destID = strsplit("-", destGUID)
            destID = tonumber(destID)
            if destID and Critmatic.ignoredTargets[destID] then
                return
            end
        end
        -- Trivial-target filtering applies to damage only; heal output does not
        -- depend on the target's level.
        if not isHealEvent and Critmatic.db.profile.generalSettings.ignoreTrivialTargets
                and CritMaticIsTrivialGUID(destGUID) then
            return
        end

        local rec = CritMaticData[spellID]
        local recordIcon
        if not rec or not rec.spellIcon or type(spellID) ~= "number" then
            if type(spellID) == "number" then
                recordIcon = select(3, GetSpellInfo(spellID))
            else
                recordIcon = GetPetAttackIcon() or select(3, GetSpellInfo(6603)) or "Interface\\Icons\\INV_Misc_QuestionMark"
            end
        end
        if not rec then
            rec = {
                highestCrit = 0,
                highestCritOld = 0,
                highestNormal = 0,
                highestNormalOld = 0,
                highestHealCrit = 0,
                highestHealCritOld = 0,
                highestHeal = 0,
                highestHealOld = 0,
                spellIcon = recordIcon,
            }
            CritMaticData[spellID] = rec
        end
        if type(spellID) ~= "number" then
            local changed = false
            if recordIcon and rec.spellIcon ~= recordIcon then
                rec.spellIcon = recordIcon
                changed = true
            end
            if (petAutoNameResolved or not rec.displayName) and rec.displayName ~= baseSpellName then
                rec.displayName = baseSpellName
                InvalidateAggregateCache()
                changed = true
            end
            if changed and RedrawCritMaticWidget then RedrawCritMaticWidget() end
        elseif not rec.spellIcon then
            rec.spellIcon = recordIcon
        end

        if fromPet then rec.isPet = true end

        if isHealEvent then
            if critical then

                -- When the event is a heal and it's a critical heal.
                if amount > rec.highestHealCrit and amount <= MAX_HIT then
                    local agg = GetSpellAggregate(baseSpellName)
                    local champ = (agg and agg.highestHealCrit) or rec.highestHealCrit
                    if amount > champ and ShouldAnnounceRecord(champ, amount) then
                        rec.highestHealCritOld = champ
                        rec.highestHealCrit = amount
                        highestCritHealDuringCombat = amount
                        highestCritHealSpellName = baseSpellName
                        RouteRecordAlert("healcrit", baseSpellName, amount, rec.spellIcon, champ)
                        InvalidateAggregateCache()
                        if RecordEvent then RecordEvent(spellID) end
                        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
                    end
                end
            elseif not critical and not Critmatic.db.profile.generalSettings.critsOnlyEnabled then
                if amount > rec.highestHeal and amount <= MAX_HIT then
                    local agg = GetSpellAggregate(baseSpellName)
                    local champ = (agg and agg.highestHeal) or rec.highestHeal
                    if amount > champ and ShouldAnnounceRecord(champ, amount) then
                        rec.highestHealOld = champ
                        rec.highestHeal = amount
                        RouteRecordAlert("heal", baseSpellName, amount, rec.spellIcon, champ)
                        InvalidateAggregateCache()
                        if RecordEvent then RecordEvent(spellID) end
                        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
                    end
                end
            end
        else
            if critical then
                -- When the event is damage and it's a critical hit.
                if amount > rec.highestCrit and amount <= MAX_HIT then
                    local agg = GetSpellAggregate(baseSpellName)
                    local champ = (agg and agg.highestCrit) or rec.highestCrit
                    if amount > champ and ShouldAnnounceRecord(champ, amount) then
                        rec.highestCritOld = champ
                        rec.highestCrit = amount
                        highestCritDuringCombat = amount
                        highestCritSpellName = baseSpellName
                        RouteRecordAlert("crit", baseSpellName, amount, rec.spellIcon, champ)
                        InvalidateAggregateCache()
                        if RecordEvent then RecordEvent(spellID) end
                        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
                    end
                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 > rec.highestNormal and amount <= MAX_HIT then
                    local agg = GetSpellAggregate(baseSpellName)
                    local champ = (agg and agg.highestNormal) or rec.highestNormal
                    if amount > champ and ShouldAnnounceRecord(champ, amount) then
                        rec.highestNormalOld = champ
                        rec.highestNormal = amount
                        RouteRecordAlert("hit", baseSpellName, amount, rec.spellIcon, champ)
                        InvalidateAggregateCache()
                        if RecordEvent then RecordEvent(spellID) end
                        if RedrawCritMaticWidget then RedrawCritMaticWidget() end
                    end
                end
            end
        end
    elseif event == "PLAYER_REGEN_DISABLED" then
        Critmatic._inCombat = true
    elseif event == "PLAYER_REGEN_ENABLED" then
        FlushCombatRecap()
        -- 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 = ""
        Critmatic._inCombat = false
    end
end

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