local addonName, addonTable = ...
local prefijoAddon = "PremadeCDs"

-- ==========================================
-- 1. MOTOR DE EVENTOS (LIMPIO Y SEGURO)
-- ==========================================
local fCore = CreateFrame("Frame")
fCore:RegisterEvent("ADDON_LOADED")
fCore:RegisterEvent("PLAYER_ENTERING_WORLD")
fCore:RegisterEvent("GROUP_ROSTER_UPDATE")
fCore:RegisterEvent("CHAT_MSG_ADDON")
fCore:RegisterEvent("ZONE_CHANGED_NEW_AREA")
fCore:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED")
fCore:RegisterEvent("SPELL_UPDATE_COOLDOWN")
fCore:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")

C_ChatInfo.RegisterAddonMessagePrefix(prefijoAddon)

addonTable.db = {}
local db = addonTable.db
local spellDB = addonTable.spellDB
local L = addonTable.L
local cortesBasePorClase = addonTable.cortesBasePorClase
local mapaClases = addonTable.mapaClases or {}
local specsDetectadas = {} 
local ultimoSyncEnviado = {} 
addonTable.throttleDB = {}
local modoTestActivo = false

local barrasActivas, ordenBarras, iconosActivos = {}, {}, {}
local anclasGrupo = {}
local unidadesGrupo = {"player", "party1", "party2", "party3", "party4"}
local ultimoCorteAnunciado = nil 

local function GuardarPosicionAncla(self)
    self:StopMovingOrSizing()
    local point, _, relativePoint, xOfs, yOfs = self:GetPoint()
    if db then
        db.anchors = db.anchors or {}
        db.anchors[self:GetName()] = {point, relativePoint, xOfs, yOfs}
    end
end

addonTable.GuardarTodasLasAnclas = function()
    if not db then return end
    db.anchors = db.anchors or {}
    local function saveA(f)
        if f then
            local point, _, relativePoint, xOfs, yOfs = f:GetPoint()
            db.anchors[f:GetName()] = {point, relativePoint, xOfs, yOfs}
        end
    end
    saveA(PremadeCDsAnchorCortes)
    saveA(PremadeCDsAnchorCCs)
    for _, u in ipairs(unidadesGrupo) do saveA(_G["PremadeCDsAnchor_"..u]) end
end

local anchorCortes = CreateFrame("Frame", "PremadeCDsAnchorCortes", UIParent)
anchorCortes:SetSize(150, 20); anchorCortes:SetMovable(true); anchorCortes:EnableMouse(true); anchorCortes:RegisterForDrag("LeftButton")
anchorCortes:SetScript("OnDragStart", anchorCortes.StartMoving); anchorCortes:SetScript("OnDragStop", GuardarPosicionAncla)
anchorCortes.bg = anchorCortes:CreateTexture(nil, "BACKGROUND"); anchorCortes.bg:SetAllPoints(); anchorCortes.bg:SetColorTexture(1, 0, 0, 0.5)
anchorCortes.text = anchorCortes:CreateFontString(nil, "ARTWORK", "GameFontNormal"); anchorCortes.text:SetPoint("CENTER"); anchorCortes.text:SetText("Anchor: CORTES"); anchorCortes:Hide()

local anchorCCs = CreateFrame("Frame", "PremadeCDsAnchorCCs", UIParent)
anchorCCs:SetSize(150, 20); anchorCCs:SetMovable(true); anchorCCs:EnableMouse(true); anchorCCs:RegisterForDrag("LeftButton")
anchorCCs:SetScript("OnDragStart", anchorCCs.StartMoving); anchorCCs:SetScript("OnDragStop", GuardarPosicionAncla)
anchorCCs.bg = anchorCCs:CreateTexture(nil, "BACKGROUND"); anchorCCs.bg:SetAllPoints(); anchorCCs.bg:SetColorTexture(0, 1, 0, 0.5)
anchorCCs.text = anchorCCs:CreateFontString(nil, "ARTWORK", "GameFontNormal"); anchorCCs.text:SetPoint("CENTER"); anchorCCs.text:SetText("Anchor: CCs GRUPO"); anchorCCs:Hide()

for i, unit in ipairs(unidadesGrupo) do
    local f = CreateFrame("Frame", "PremadeCDsAnchor_"..unit, UIParent)
    f:SetSize(80, 30); f:SetMovable(true); f:EnableMouse(true); f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving); f:SetScript("OnDragStop", GuardarPosicionAncla)
    f.bg = f:CreateTexture(nil, "BACKGROUND"); f.bg:SetAllPoints(); f.bg:SetColorTexture(0, 0.5, 1, 0.5)
    f.text = f:CreateFontString(nil, "ARTWORK", "GameFontNormal"); f.text:SetPoint("CENTER"); f.text:SetText("Anchor: " .. unit)
    f:Hide()
    anclasGrupo[unit] = f
    iconosActivos[unit] = {}
end

local function ObtenerUnidadDeMarco(frame)
    if not frame then return nil end
    if type(frame.unit) == "string" then return frame.unit end
    if frame.GetAttribute and frame:GetAttribute("unit") then return frame:GetAttribute("unit") end
    return nil
end

local function FrameCoincideUnidad(frame, unitBuscada)
    if not frame then return false end
    local frameUnit = ObtenerUnidadDeMarco(frame)
    if frameUnit and UnitIsUnit(frameUnit, unitBuscada) then return true end
    return false
end

local function GetPartyUnitFrame(unit)
    local function IsValid(f) return f and (modoTestActivo or f:IsVisible()) end

    if _G["DandersPartyHeader"] or _G["DUI_PlayerFrame"] or _G["DandersPlayerFrame"] then
        for i = 1, 5 do
            local f = _G["DUI_PartyFrame"..i] or _G["DUI_UnitFrameParty"..i] or _G["DandersPartyHeaderUnitButton"..i] or _G["DUI_PartyGroup1UnitButton"..i]
            if IsValid(f) and FrameCoincideUnidad(f, unit) then return f end 
        end
        if unit == "player" then
            local p = _G["DUI_PlayerFrame"] or _G["DandersPlayerFrame"] or _G["DandersPartyHeaderUnitButton0"]
            if IsValid(p) and (not ObtenerUnidadDeMarco(p) or FrameCoincideUnidad(p, "player")) then return p end 
        end
        if modoTestActivo then
            if unit == "player" then
                local f = _G["DandersPartyHeaderUnitButton1"] or _G["DUI_PartyFrame1"] or _G["DUI_UnitFrameParty1"]
                if IsValid(f) then return f end
            end
            local idx = string.match(unit, "party(%d)")
            if idx then
                local f = _G["DUI_PartyFrame"..idx] or _G["DUI_UnitFrameParty"..idx] or _G["DandersPartyHeaderUnitButton"..idx] or _G["DUI_PartyGroup1UnitButton"..idx]
                if IsValid(f) then return f end
            end
        end
    end

    if _G["ElvUI"] then
        for i = 1, 5 do
            local f = _G["ElvUF_PartyGroup1UnitButton"..i]
            if IsValid(f) and FrameCoincideUnidad(f, unit) then return f end 
        end
        if unit == "player" then 
            local p = _G["ElvUF_Player"]
            if IsValid(p) and (not ObtenerUnidadDeMarco(p) or FrameCoincideUnidad(p, "player")) then return p end 
        end
    end

    if _G["Grid2LayoutFrame"] then
        for i = 1, 5 do
            local f = _G["Grid2LayoutHeader1UnitButton"..i]
            if IsValid(f) and FrameCoincideUnidad(f, unit) then return f end 
        end
    end

    for i = 1, 5 do
        local f = _G["CompactPartyFrameMember"..i]
        if IsValid(f) and FrameCoincideUnidad(f, unit) then return f end 
    end
    if unit == "player" then 
        local p = _G["PlayerFrame"]
        if IsValid(p) and (not ObtenerUnidadDeMarco(p) or FrameCoincideUnidad(p, "player")) then return p end 
    end

    return nil
end

addonTable.RestaurarAnclas = function()
    if not db then return end
    db.anchors = db.anchors or {}
    
    local anclasEstaticas = {PremadeCDsAnchorCortes = anchorCortes, PremadeCDsAnchorCCs = anchorCCs}
    for nombreAncla, frame in pairs(anclasEstaticas) do
        frame:ClearAllPoints()
        if db.anchors[nombreAncla] then
            local data = db.anchors[nombreAncla]
            frame:SetPoint(data[1], UIParent, data[2], data[3], data[4])
        else
            if nombreAncla == "PremadeCDsAnchorCortes" then frame:SetPoint("CENTER", -200, 100)
            elseif nombreAncla == "PremadeCDsAnchorCCs" then frame:SetPoint("CENTER", 200, 0) end
        end
    end

    for _, u in ipairs(unidadesGrupo) do
        local ancla = anclasGrupo[u]
        ancla:ClearAllPoints()
        if db.anchorMode == "Manual" and db.anchors[ancla:GetName()] then
            local data = db.anchors[ancla:GetName()]
            ancla:SetPoint(data[1], UIParent, data[2], data[3], data[4])
        else
            local idx = tonumber(u:match("party(%d)") or 0)
            if u:match("player") then idx = 0 end
            ancla:SetPoint("CENTER", -100, 50 - (idx * 50))
        end
    end
end

local function ActualizarAnclajesDinamicos()
    if not db then return end
    
    if db.anchorMode == "Manual" then
        for unit, ancla in pairs(anclasGrupo) do
            ancla.hasValidAnchor = true
        end
        return
    end
    
    local oX = db.offsetX or 0
    local oY = db.offsetY or 0

    for unit, ancla in pairs(anclasGrupo) do
        local targetFrame = GetPartyUnitFrame(unit)
        ancla:ClearAllPoints()
        
        if targetFrame and (modoTestActivo or targetFrame:IsVisible()) then
            ancla.hasValidAnchor = true
            ancla:SetSize(1, 1)
            
            if db.anchorSide == "RIGHT" then
                ancla:SetPoint("TOPLEFT", targetFrame, "TOPRIGHT", 5 + oX, oY)
            elseif db.anchorSide == "LEFT" then
                ancla:SetPoint("TOPRIGHT", targetFrame, "TOPLEFT", -5 + oX, oY)
            elseif db.anchorSide == "TOP" then
                ancla:SetPoint("BOTTOMLEFT", targetFrame, "TOPLEFT", oX, 5 + oY)
            elseif db.anchorSide == "BOTTOM" then
                ancla:SetPoint("TOPLEFT", targetFrame, "BOTTOMLEFT", oX, -5 + oY)
            end
        else
            ancla.hasValidAnchor = false
            ancla:SetSize(80, 30)
            local idx = tonumber(unit:match("party(%d)") or 0)
            if unit:match("player") then idx = 0 end
            ancla:SetPoint("CENTER", -100, 50 - (idx * 50))
        end
    end
end

local NOMBRES_TEST = {}

local function LimpiarNombreSeguro(nombreCompuesto)
    if type(nombreCompuesto) ~= "string" then return "" end
    local namePart = strsplit("-", nombreCompuesto)
    return namePart or nombreCompuesto
end

local function ObtenerCooldownSeguro(spellID)
    local cdInfo = C_Spell.GetSpellCooldown(spellID)
    if not cdInfo then return 0, 0, false end
    
    local ok1, dur = pcall(function() return cdInfo.duration + 0 end)
    local ok2, start = pcall(function() return cdInfo.startTime + 0 end)
    
    if ok1 and ok2 and type(dur) == "number" and type(start) == "number" then
        return start, dur, true
    end
    return 0, 0, false
end

local function DeberiaMostrarHechizo(unit, spellID)
    local info = spellDB[spellID]
    if not info or db.disabledSpells[spellID] then return false end
    
    local classFile
    if modoTestActivo then
        for _, mock in ipairs(NOMBRES_TEST) do
            if mock.unit == unit then classFile = mock.class; break end
        end
    else
        _, classFile = UnitClass(unit)
    end

    if classFile and info.clase ~= classFile then return false end

    if modoTestActivo then return true end
    if info.spec == 0 then return true end
    
    if unit == "player" then
        local specIndex = GetSpecialization()
        local mySpec = specIndex and GetSpecializationInfo(specIndex) or 0
        return mySpec == info.spec
    else
        local name = UnitName(unit)
        if name and specsDetectadas[name] then return specsDetectadas[name] == info.spec end
    end
    
    return false
end

local function DeberiaEstarActivoEnZona()
    if anchorCortes:IsShown() or anchorCCs:IsShown() then return true end
    local _, instanceType = GetInstanceInfo()
    if instanceType == "party" then return db.activeDungeon end
    if instanceType == "raid" then return db.activeRaid end
    if instanceType == "arena" or instanceType == "pvp" then return db.activePvp end
    if instanceType == "none" then return db.activeWorld end
    return true
end

local function EsNombreActivoEnGrupo(nombreBuscar)
    if anchorCortes:IsShown() then return true end
    if not nombreBuscar then return false end
    
    local miNombre = UnitName("player")
    if miNombre and (miNombre == nombreBuscar or LimpiarNombreSeguro(miNombre) == nombreBuscar) then return true end
    
    for i=1, 4 do
        local n = UnitName("party"..i)
        if n and (n == nombreBuscar or LimpiarNombreSeguro(n) == nombreBuscar) then return true end
    end
    return false
end

local function ObtenerUnidadPorNombre(nombreBuscado)
    if type(nombreBuscado) ~= "string" then return nil end
    if modoTestActivo then
        for _, mock in ipairs(NOMBRES_TEST) do
            if string.match(mock.name, nombreBuscado) or string.match(nombreBuscado, mock.name) then return mock.unit end
        end
    end
    local nombreLimpio = LimpiarNombreSeguro(nombreBuscado)
    if UnitName("player") == nombreLimpio or UnitName("player") == nombreBuscado then return "player" end
    for i=1, 4 do if UnitName("party"..i) == nombreLimpio or UnitName("party"..i) == nombreBuscado then return "party"..i end end
    return nil
end

local function EnviarMiSpec()
    local specIndex = GetSpecialization()
    if specIndex then
        local specID = GetSpecializationInfo(specIndex)
        local miNombre = UnitName("player")
        if miNombre and specID then specsDetectadas[miNombre] = specID end
        if IsInGroup() then
            local channel = IsInRaid() and "RAID" or "PARTY"
            C_ChatInfo.SendAddonMessage(prefijoAddon, "SPEC#"..tostring(specID), channel)
        end
    end
end

-- ==========================================
-- MOTOR DE AUDIO (REPRODUCCIÓN MP3)
-- ==========================================
local function AnunciarProximoCorte(classFile, isPlayer)
    if db and db.ttsEnabled == false then return end
    
    local rutaBase = "Interface\\AddOns\\"..addonName.."\\Sounds\\"
    
    if isPlayer then
        PlaySoundFile(rutaBase .. "YO.mp3", "Master")
    elseif classFile then
        PlaySoundFile(rutaBase .. classFile .. ".mp3", "Master")
    end
end

SLASH_PREMADECDS3 = "/premadetts"
SlashCmdList["PREMADECDS3"] = function()
    local _, myClass = UnitClass("player")
    AnunciarProximoCorte(myClass, true)
    C_Timer.After(2, function()
        AnunciarProximoCorte(myClass, false)
    end)
end

local function EvaluarProximoCorte()
    if not IsInGroup() and not modoTestActivo then return end

    local listos = {}
    local enCD = {}
    local ahora = GetTime()

    for barKey, row in pairs(barrasActivas) do
        local senderName = string.match(barKey, "^(.-)_%d+$")
        
        if not db.disabledSpells[row.spellID] and (EsNombreActivoEnGrupo(senderName) or modoTestActivo) then
            local unit = ObtenerUnidadPorNombre(senderName)
            local role = "DAMAGER"
            local classFile = "UNKNOWN"
            local spec = specsDetectadas[senderName] or 0

            if modoTestActivo then
                for _, mock in ipairs(NOMBRES_TEST) do
                    if mock.name == senderName then
                        classFile = mock.class
                        if mock.unit == "player" then unit = "player" end
                        break
                    end
                end
            elseif unit then
                role = UnitGroupRolesAssigned(unit)
                _, classFile = UnitClass(unit)
            end

            local score = 1000 
            local isMelee = false
            if role == "TANK" or classFile == "ROGUE" or classFile == "WARRIOR" or classFile == "DEMONHUNTER" or classFile == "DEATHKNIGHT" or classFile == "MONK" or classFile == "PALADIN" then
                isMelee = true
            end
            if spec == 255 or spec == 263 or spec == 103 then isMelee = true end

            if isMelee then score = 2000 end

            local info = { 
                name = senderName, 
                cdExpiration = row.cdExpiration, 
                priority = score,
                classFile = classFile,
                isPlayer = (unit == "player")
            }
            if row.cdExpiration == 0 or row.cdExpiration <= ahora then
                table.insert(listos, info)
            else
                table.insert(enCD, info)
            end
        end
    end

    local ganador = nil
    if #listos > 0 then
        table.sort(listos, function(a, b)
            if a.priority == b.priority then return a.name < b.name end
            return a.priority > b.priority
        end)
        ganador = listos[1]
    elseif #enCD > 0 then
        table.sort(enCD, function(a, b)
            if math.abs(a.cdExpiration - b.cdExpiration) < 0.1 then 
                if a.priority == b.priority then return a.name < b.name end
                return a.priority > b.priority 
            end
            return a.cdExpiration < b.cdExpiration
        end)
        ganador = enCD[1]
    end

    if ganador then
        if ganador.name ~= ultimoCorteAnunciado then
            ultimoCorteAnunciado = ganador.name
            AnunciarProximoCorte(ganador.classFile, ganador.isPlayer)
        end
    else
        ultimoCorteAnunciado = nil
    end
end

-- ==========================================
-- 3. MOTOR DE RENDERIZADO EN VIVO Y ORDEN
-- ==========================================
local function ActualizarTodoEnVivo()
    if not db then return end
    if addonTable.Media and addonTable.Media.LoadFromDB then addonTable.Media:LoadFromDB(db) end
    local estaActivo = DeberiaEstarActivoEnZona()
    
    ActualizarAnclajesDinamicos()
    
    -- CORTES
    local barrasOrdenadas = {}
    for barKey, row in pairs(barrasActivas) do
        row.barKey = barKey
        local senderName = string.match(barKey, "^(.-)_%d+$")
        if db.disabledSpells[row.spellID] or not estaActivo or (not EsNombreActivoEnGrupo(senderName) and not modoTestActivo) or not db.showCortes then
            row:Hide()
        else
            table.insert(barrasOrdenadas, row)
        end
    end

    table.sort(barrasOrdenadas, function(a, b)
        local remA = (a.cdExpiration and a.cdExpiration > GetTime()) and (a.cdExpiration - GetTime()) or 0
        local remB = (b.cdExpiration and b.cdExpiration > GetTime()) and (b.cdExpiration - GetTime()) or 0
        if db.sortMode == "CD_ASC" then return remA < remB
        elseif db.sortMode == "CD_DESC" then return remA > remB
        else return a.barKey < b.barKey end
    end)

    local prevRow = nil
    for i, row in ipairs(barrasOrdenadas) do
        row:Show(); row:SetSize(db.width, db.height); row.icon:SetSize(db.height - 2, db.height - 2)
        
        if db.pixelPerfect then
            row.icon:SetPoint("TOPLEFT", row, "TOPLEFT", 1, -1)
            row.icon:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 1, 1)
            row.bg:Show()
        else
            row.icon:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
            row.icon:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
            row.bg:Hide()
        end

        if addonTable.Media and addonTable.Media.ApplyFont then 
            addonTable.Media:ApplyFont(row.text, db.nameFontSize); addonTable.Media:ApplyFont(row.timerText, db.fontSizeName)
        else
            row.text:SetFont("Fonts\\FRIZQT__.TTF", db.nameFontSize, "OUTLINE"); row.timerText:SetFont("Fonts\\FRIZQT__.TTF", db.fontSizeName, "OUTLINE")
        end
        if addonTable.Media and addonTable.Media.ApplyTexture then addonTable.Media:ApplyTexture(row.bar) end
        
        if row.cdExpiration == 0 or row.cdExpiration <= GetTime() then
            row.timerText:SetText(db.showListo and L.READY or "")
            -- RESEATO VISUAL FORZOSO DE LA BARRA
            row.bar:SetScript("OnUpdate", nil)
            row.bar:SetMinMaxValues(0, row.cdTotal)
            row.bar:SetValue(row.cdTotal)
            row.timerText:SetTextColor(1, 1, 1)
            row.cdExpiration = 0
        end
        
        row:ClearAllPoints()
        if not prevRow then row:SetPoint("TOP", anchorCortes, "BOTTOM", 0, 0) else row:SetPoint("TOP", prevRow, "BOTTOM", 0, -2) end
        prevRow = row
    end

    -- CCs
    local visiblesCC = {}
    for unit, lista in pairs(iconosActivos) do
        local isUnitValid = false
        local pName = ""
        local pClass = "UNKNOWN"
        
        if modoTestActivo then
            isUnitValid = (unit == "player") or unit:match("party")
            for _, mock in ipairs(NOMBRES_TEST) do 
                if mock.unit == unit then 
                    pName = mock.name
                    pClass = mock.class
                    break 
                end 
            end
        else
            isUnitValid = UnitExists(unit) or (unit == "player")
            pName = UnitName(unit) or ""
            _, pClass = UnitClass(unit)
        end
        
        for _, ic in ipairs(lista) do
            local info = spellDB[ic.spellID]
            if info and info.cat == "CCs" then
                if isUnitValid and estaActivo and db.showCCs and DeberiaMostrarHechizo(unit, ic.spellID) then
                    ic:Show(); ic:SetSize(db.ccSize, db.ccSize)
                    
                    if db.pixelPerfect then
                        ic.icon:SetPoint("TOPLEFT", 1, -1); ic.icon:SetPoint("BOTTOMRIGHT", -1, 1)
                        ic.bg:Show()
                    else
                        ic.icon:SetPoint("TOPLEFT", 0, 0); ic.icon:SetPoint("BOTTOMRIGHT", 0, 0)
                        ic.bg:Hide()
                    end

                    if ic.playerName then 
                        ic.playerName:SetText(strsub(pName, 1, 4))
                        if addonTable.Media and addonTable.Media.ApplyFont then addonTable.Media:ApplyFont(ic.playerName, 10) end 
                    end
                    ic.currentClass = pClass or "UNKNOWN"
                    ic.currentUnit = unit
                    table.insert(visiblesCC, ic)
                else
                    ic:Hide()
                end
            end
        end
    end
    
    table.sort(visiblesCC, function(a, b)
        if a.currentClass == b.currentClass then
            if a.currentUnit == b.currentUnit then return a.spellID < b.spellID end
            return a.currentUnit < b.currentUnit
        end
        return a.currentClass < b.currentClass
    end)
    
    local ccSpacing = db.ccSpacing or 2
    local cDir = db.ccGrowDirection or "RIGHT_DOWN"
    local growLeftCC = (cDir == "LEFT_DOWN" or cDir == "LEFT_UP")
    local growUpCC = (cDir == "RIGHT_UP" or cDir == "LEFT_UP")

    local pointYCC = growUpCC and "BOTTOM" or "TOP"
    local pointXCC = growLeftCC and "RIGHT" or "LEFT"
    local pointCC = pointYCC .. pointXCC
    local relYCC = growUpCC and "TOP" or "BOTTOM"
    local relPointCC = relYCC .. pointXCC

    local xMCC = growLeftCC and -1 or 1
    local yMCC = growUpCC and 1 or -1

    for i, ic in ipairs(visiblesCC) do
        ic:ClearAllPoints()
        local cPR = math.max(1, db.ccPerRow or 5)
        local col = (i - 1) % cPR; local row = math.floor((i - 1) / cPR)
        ic:SetPoint(pointCC, anchorCCs, relPointCC, xMCC * (col * (db.ccSize + ccSpacing)), yMCC * (row * (db.ccSize + ccSpacing)))
    end

    -- DEFENSIVOS Y OFENSIVOS
    local spacing = db.iconSpacing or 2
    local gDir = db.growDirection or "AUTO"
    
    for unit, listaIconos in pairs(iconosActivos) do
        local ancla = anclasGrupo[unit]
        local visiblesDef, visiblesOfen = {}, {}
        local isUnitValid = modoTestActivo or (unit == "player") or UnitExists(unit)

        if isUnitValid and db.anchorMode == "Auto" then
            local targetFrame = GetPartyUnitFrame(unit)
            if not targetFrame or not targetFrame:IsVisible() then isUnitValid = false end
        end

        if isUnitValid and ancla and ancla.hasValidAnchor then
            for _, iconFrame in ipairs(listaIconos) do
                local info = spellDB[iconFrame.spellID]
                if info and info.cat ~= "CCs" then
                    if estaActivo and DeberiaMostrarHechizo(unit, iconFrame.spellID) then
                        if info.cat == "Defensivos" and db.showDefensivos then
                            iconFrame:Show(); iconFrame:SetSize(db.iconSize, db.iconSize); table.insert(visiblesDef, iconFrame)
                        elseif info.cat == "Ofensivos" and db.showOfensivos then
                            iconFrame:Show(); iconFrame:SetSize(db.iconSize, db.iconSize); table.insert(visiblesOfen, iconFrame)
                        else iconFrame:Hide() end
                        
                        if db.pixelPerfect then
                            iconFrame.icon:SetPoint("TOPLEFT", 1, -1); iconFrame.icon:SetPoint("BOTTOMRIGHT", -1, 1)
                            iconFrame.bg:Show()
                        else
                            iconFrame.icon:SetPoint("TOPLEFT", 0, 0); iconFrame.icon:SetPoint("BOTTOMRIGHT", 0, 0)
                            iconFrame.bg:Hide()
                        end
                    else iconFrame:Hide() end
                end
            end
            
            local growLeft, growUp = false, false
            if gDir == "AUTO" then
                if db.anchorMode ~= "Manual" and db.anchorSide == "LEFT" then growLeft = true end
            elseif gDir == "LEFT_DOWN" then growLeft = true; growUp = false
            elseif gDir == "RIGHT_UP" then growLeft = false; growUp = true
            elseif gDir == "LEFT_UP" then growLeft = true; growUp = true
            end

            local pointY = growUp and "BOTTOM" or "TOP"
            local pointX = growLeft and "RIGHT" or "LEFT"
            local pointIcon = pointY .. pointX
            local xMult = growLeft and -1 or 1
            local yMult = growUp and 1 or -1
            local iPR = math.max(1, db.iconsPerRow or 4)

            local arr1 = db.defensivosFirst and visiblesDef or visiblesOfen
            local arr2 = db.defensivosFirst and visiblesOfen or visiblesDef

            for i, iconFrame in ipairs(arr1) do
                iconFrame:ClearAllPoints()
                local col = (i - 1) % iPR; local row = math.floor((i - 1) / iPR)
                iconFrame:SetPoint(pointIcon, ancla, pointIcon, xMult * (col * (db.iconSize + spacing)), yMult * (row * (db.iconSize + spacing)))
            end
            
            local filasArr1 = math.ceil(#arr1 / iPR)

            for i, iconFrame in ipairs(arr2) do
                iconFrame:ClearAllPoints()
                local col = (i - 1) % iPR; local row = math.floor((i - 1) / iPR) + filasArr1
                iconFrame:SetPoint(pointIcon, ancla, pointIcon, xMult * (col * (db.iconSize + spacing)), yMult * (row * (db.iconSize + spacing)))
            end
        else
            for _, iconFrame in ipairs(listaIconos) do 
                local info = spellDB[iconFrame.spellID]
                if info and info.cat ~= "CCs" then iconFrame:Hide() end
            end
        end
    end
end
addonTable.ActualizarTodoEnVivo = ActualizarTodoEnVivo

local function ObtenerOCrearBarra(senderName, spellID, cdTotalBase, claseFondo)
    if not senderName or not spellID then return nil end
    -- FILTRO MAGO CROSS-REALM: Limpia el servidor del nombre siempre
    senderName = LimpiarNombreSeguro(senderName)
    local barKey = senderName .. "_" .. spellID
    if barrasActivas[barKey] then return barrasActivas[barKey] end

    local row = CreateFrame("Frame", nil, UIParent)
    row.spellID = spellID; row.cdTotal = math.max(0.1, cdTotalBase or 1)
    row.cdExpiration = 0 
    
    row.bg = row:CreateTexture(nil, "BACKGROUND"); row.bg:SetAllPoints(); row.bg:SetColorTexture(0, 0, 0, 1)
    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetPoint("TOPLEFT", row, "TOPLEFT", 1, -1)
    row.icon:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 1, 1)
    row.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    if spellInfo then row.icon:SetTexture(spellInfo.iconID) end

    row.bar = CreateFrame("StatusBar", nil, row)
    row.bar:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 1, 0); row.bar:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -1, 0)
    row.bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8x8") 

    local c = (claseFondo and RAID_CLASS_COLORS[claseFondo]) or {r=0.5, g=0.5, b=0.5}
    row.bar:SetStatusBarColor(c.r, c.g, c.b)
    row.bar.bg = row.bar:CreateTexture(nil, "BACKGROUND"); row.bar.bg:SetAllPoints(); row.bar.bg:SetColorTexture(c.r * 0.3, c.g * 0.3, c.b * 0.3, 0.8)

    row.text = row.bar:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); row.text:SetPoint("LEFT", row.bar, "LEFT", 5, 0); row.text:SetText(senderName)
    row.timerText = row.bar:CreateFontString(nil, "OVERLAY", "GameFontHighlight"); row.timerText:SetPoint("RIGHT", row.bar, "RIGHT", -5, 0)
    row.timerText:SetText(db.showListo and L.READY or "")

    barrasActivas[barKey] = row
    ActualizarTodoEnVivo()
    return row
end

local function IniciarBarraCorte(row, remaining)
    if not row or type(remaining) ~= "number" then return end
    local maxCd = row.cdTotal
    local startTime = GetTime() - (maxCd - remaining)
    row.cdExpiration = GetTime() + remaining
    
    if row.lastInterruptTime and (GetTime() - row.lastInterruptTime) < 1.0 then
        row.kickSuccess = true
        row.timerText:SetTextColor(0.1, 1, 0.1)
    else
        row.kickSuccess = false
        row.kickCastTime = GetTime()
        row.timerText:SetTextColor(1, 1, 1) 
        
        C_Timer.After(0.5, function()
            if not row.kickSuccess and (GetTime() - row.kickCastTime) < 0.6 then
                row.timerText:SetTextColor(1, 0.2, 0.2) 
            end
        end)
    end
    
    row.bar:SetMinMaxValues(0, maxCd)
    row.bar:SetScript("OnUpdate", function(self)
        local elapsed = GetTime() - startTime
        local rem = maxCd - elapsed
        if rem <= 0 then
            self:SetScript("OnUpdate", nil); self:SetValue(maxCd); 
            row.timerText:SetText(db.showListo and L.READY or "")
            row.timerText:SetTextColor(1, 1, 1) 
            row.cdExpiration = 0
            ActualizarTodoEnVivo() 
            EvaluarProximoCorte() 
        else
            self:SetValue(rem); row.timerText:SetText(string.format("%.1f", rem))
        end
    end)
    
    ActualizarTodoEnVivo() 
    C_Timer.After(0.1, EvaluarProximoCorte) 
end

local function ObtenerOCrearIcono(unit, spellID, cd)
    if not unit or not iconosActivos[unit] then return nil end
    for _, iconFrame in ipairs(iconosActivos[unit]) do
        if iconFrame.spellID == spellID then return iconFrame end
    end

    local frame = CreateFrame("Frame", nil, UIParent)
    frame.spellID = spellID; frame.cdTotal = math.max(0.1, cd or 1)
    frame.bg = frame:CreateTexture(nil, "BACKGROUND"); frame.bg:SetAllPoints(); frame.bg:SetColorTexture(0, 0, 0, 1)
    frame.icon = frame:CreateTexture(nil, "ARTWORK"); 
    frame.icon:SetPoint("TOPLEFT", 1, -1); 
    frame.icon:SetPoint("BOTTOMRIGHT", -1, 1)
    frame.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    if spellInfo then frame.icon:SetTexture(spellInfo.iconID) end

    frame.cooldown = CreateFrame("Cooldown", nil, frame, "CooldownFrameTemplate")
    frame.cooldown:SetAllPoints(); frame.cooldown:SetDrawEdge(false); frame.cooldown:SetHideCountdownNumbers(false)

    frame.chargesText = frame.cooldown:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    frame.chargesText:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
    frame.chargesText:SetText("")

    local info = spellDB[spellID]
    if info and info.cat == "CCs" then
        frame.playerName = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        frame.playerName:SetPoint("BOTTOM", frame, "BOTTOM", 0, 2)
    end

    table.insert(iconosActivos[unit], frame)
    ActualizarTodoEnVivo()
    return frame
end

local function LimpiarIconos(unit)
    if iconosActivos[unit] then
        for _, frame in ipairs(iconosActivos[unit]) do frame:Hide() end
    end
end

local function AutoPoblarUnidad(unit)
    local name = UnitName(unit); local _, classFile = UnitClass(unit)
    if not classFile or not name then LimpiarIconos(unit); return end
    
    LimpiarIconos(unit)
    local corteBase = cortesBasePorClase[classFile]
    if corteBase and spellDB[corteBase] then
        ObtenerOCrearBarra(name, corteBase, spellDB[corteBase].cd, classFile)
    end
    
    for sID, info in pairs(spellDB) do
        if info.cat ~= "Cortes" and info.clase == classFile then ObtenerOCrearIcono(unit, sID, info.cd) end
    end
    ActualizarTodoEnVivo()
end

local function EscanearGrupo()
    if not db then return end
    for i = 1, 4 do 
        if UnitExists("party" .. i) then AutoPoblarUnidad("party" .. i) else LimpiarIconos("party" .. i) end
    end
    AutoPoblarUnidad("player")
    EvaluarProximoCorte() 
end

local testTicker = nil
addonTable.IniciarModoTest = function()
    modoTestActivo = not modoTestActivo
    if modoTestActivo then
        ultimoCorteAnunciado = nil 
        anchorCortes:Show(); anchorCCs:Show()
        
        if db.anchorMode == "Manual" then
            for _, unit in ipairs(unidadesGrupo) do anclasGrupo[unit]:Show() end
        end

        local myName = UnitName("player") or "Player"
        local _, myClass = UnitClass("player")
        myClass = myClass or "WARRIOR"
        local myCorte = cortesBasePorClase[myClass] or 6552
        
        NOMBRES_TEST = {
            {name = myName,     class = myClass,       spellID = myCorte, unit = "player"},
            {name = "Tankerino",class = "PALADIN",     spellID = 96231,   unit = "party1"},
            {name = "Healerino",class = "DRUID",       spellID = 106839,  unit = "party2"},
            {name = "MeleeGod", class = "DEATHKNIGHT", spellID = 47528,   unit = "party3"},
            {name = "RangoMax", class = "MAGE",        spellID = 2139,    unit = "party4"}
        }
        
        for i, mock in ipairs(NOMBRES_TEST) do
            local cdTotal = spellDB[mock.spellID] and spellDB[mock.spellID].cd or 15
            local row = ObtenerOCrearBarra(mock.name, mock.spellID, cdTotal, mock.class)
            if row then
                IniciarBarraCorte(row, i * 4) 
            end
            
            for sID, info in pairs(spellDB) do
                if info.cat ~= "Cortes" and info.clase == mock.class then 
                    local ic = ObtenerOCrearIcono(mock.unit, sID, info.cd) 
                    if ic and ic.cooldown then
                        ic.cooldown:Show()
                        ic.cooldown:SetCooldown(GetTime() - math.random(1, 10), info.cd)
                    end
                end
            end
        end
        ActualizarTodoEnVivo()
        
        testTicker = C_Timer.NewTicker(4, function()
            if not modoTestActivo then return end
            for _, mock in ipairs(NOMBRES_TEST) do
                local barKey = mock.name .. "_" .. mock.spellID
                local r = barrasActivas[barKey]
                if r and (r.cdExpiration == 0 or r.cdExpiration <= GetTime()) then
                    local cdTotal = spellDB[mock.spellID] and spellDB[mock.spellID].cd or 15
                    IniciarBarraCorte(r, cdTotal)
                end
                
                if iconosActivos[mock.unit] then
                    for _, ic in ipairs(iconosActivos[mock.unit]) do
                        local sInfo = spellDB[ic.spellID]
                        if sInfo and sInfo.cat ~= "CCs" then
                            if ic:IsVisible() and not ic.cooldown:IsShown() then
                                ic.cooldown:Show()
                                ic.cooldown:SetCooldown(GetTime() - math.random(1, 10), sInfo.cd)
                            end
                        end
                    end
                end
            end
        end)
        
        if addonTable.btnTestGlobal then addonTable.btnTestGlobal:SetText(L.CLOSE_TEST) end
        EvaluarProximoCorte() 
    else
        anchorCortes:Hide(); anchorCCs:Hide()
        for _, unit in ipairs(unidadesGrupo) do anclasGrupo[unit]:Hide() end
        if testTicker then testTicker:Cancel(); testTicker = nil end
        
        for _, mock in ipairs(NOMBRES_TEST) do
            local barKey = mock.name .. "_" .. mock.spellID
            local r = barrasActivas[barKey]
            if r then r:Hide(); r.cdExpiration = 0; r.bar:SetScript("OnUpdate", nil) end
            LimpiarIconos(mock.unit)
        end
        
        if addonTable.btnTestGlobal then addonTable.btnTestGlobal:SetText(L.TEST) end
        EscanearGrupo()
        ActualizarTodoEnVivo()
    end
end

fCore:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" and (...) == addonName then
        
        local charName = UnitName("player")
        if not charName then return end
        
        PremadeCDsDB = PremadeCDsDB or {}
        PremadeCDsDB.profiles = PremadeCDsDB.profiles or {}
        PremadeCDsDB.charToProfile = PremadeCDsDB.charToProfile or {}
        
        if PremadeCDsDB.width and not PremadeCDsDB.profiles["Default"] then
            PremadeCDsDB.profiles["Default"] = {}
            for k, v in pairs(PremadeCDsDB) do
                if k ~= "profiles" and k ~= "charToProfile" then PremadeCDsDB.profiles["Default"][k] = v end
            end
        end
        if not PremadeCDsDB.profiles["Default"] then PremadeCDsDB.profiles["Default"] = { profileName = "Default" } end
        if not PremadeCDsDB.charToProfile[charName] then PremadeCDsDB.charToProfile[charName] = "Default" end
        
        local currentProfile = PremadeCDsDB.charToProfile[charName]
        if not PremadeCDsDB.profiles[currentProfile] then PremadeCDsDB.profiles[currentProfile] = { profileName = currentProfile } end
        
        if addonTable.ConstruirUI then addonTable.ConstruirUI() end
        if addonTable.AplicarPerfil then addonTable.AplicarPerfil(currentProfile) end

    elseif event == "PLAYER_ENTERING_WORLD" then
        EscanearGrupo()
        EnviarMiSpec()
        if IsInGroup() then
            local channel = IsInRaid() and "RAID" or "PARTY"
            C_ChatInfo.SendAddonMessage(prefijoAddon, "REQ_SPEC", channel)
        end
        ActualizarTodoEnVivo()

    elseif event == "GROUP_ROSTER_UPDATE" or event == "ZONE_CHANGED_NEW_AREA" or event == "PLAYER_SPECIALIZATION_CHANGED" then
        EscanearGrupo()
        EnviarMiSpec()
        if IsInGroup() then
            local channel = IsInRaid() and "RAID" or "PARTY"
            C_ChatInfo.SendAddonMessage(prefijoAddon, "REQ_SPEC", channel)
        end
        ActualizarTodoEnVivo()

    elseif event == "CHAT_MSG_ADDON" then
        local pre, msg, _, sender = ...
        if pre == prefijoAddon then
            local sName = LimpiarNombreSeguro(sender)
            if msg == "REQ_SPEC" then
                EnviarMiSpec()
            elseif string.find(msg, "SPEC#") then
                local _, sIDStr = strsplit("#", msg)
                local sID = tonumber(sIDStr)
                if sID then specsDetectadas[sName] = sID; ActualizarTodoEnVivo() end
            else
                local sIDStr, remStr, cat, class, specStr = strsplit("#", msg)
                local id = tonumber(sIDStr); local rem = tonumber(remStr)
                local specID = tonumber(specStr)
                
                if specID and specID > 0 and specsDetectadas[sName] ~= specID then
                    specsDetectadas[sName] = specID
                    ActualizarTodoEnVivo()
                end

                if id and rem and spellDB[id] then
                    local info = spellDB[id]
                    if cat == "Cortes" then
                        local r = ObtenerOCrearBarra(sName, id, info.cd, class)
                        if r then IniciarBarraCorte(r, rem) end
                    else
                        local u = ObtenerUnidadPorNombre(sName)
                        if u then
                            local ic = ObtenerOCrearIcono(u, id, info.cd)
                            if ic then
                                local fakeStart = GetTime() - (info.cd - rem)
                                ic.cooldown:Show()
                                ic.cooldown:SetCooldown(fakeStart, math.max(0.1, info.cd))
                            end
                        end
                    end
                end
            end
        end

    elseif event == "SPELL_UPDATE_COOLDOWN" then
        if iconosActivos["player"] then
            for _, ic in ipairs(iconosActivos["player"]) do
                local startReal, durReal, isValid = ObtenerCooldownSeguro(ic.spellID)
                if isValid and durReal > 2 then
                    if ic.lastStart ~= startReal or ic.lastDur ~= durReal then
                        ic.cooldown:SetCooldown(startReal, durReal)
                        ic.lastStart = startReal; ic.lastDur = durReal
                    end
                end
            end
        end

    elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
        local unit, _, sID = ...
        if unit == "player" and sID and spellDB[sID] then
            local info = spellDB[sID]; local _, class = UnitClass("player")
            class = class or "UNKNOWN"
            
            local startReal, durReal, isValid = ObtenerCooldownSeguro(sID)
            
            if not isValid or durReal <= 2 then
                durReal = info.cd
                startReal = GetTime()
            end
            
            if IsInGroup() then
                local specIndex = GetSpecialization()
                local mySpec = specIndex and GetSpecializationInfo(specIndex) or 0
                local channel = IsInRaid() and "RAID" or "PARTY"
                C_ChatInfo.SendAddonMessage(prefijoAddon, tostring(sID).."#"..tostring(durReal).."#"..tostring(info.cat).."#"..tostring(class).."#"..tostring(mySpec), channel)
            end
            
            if info.cat == "Cortes" then
                local r = ObtenerOCrearBarra(UnitName("player"), sID, info.cd, class)
                if r then IniciarBarraCorte(r, durReal) end
            else
                local ic = ObtenerOCrearIcono("player", sID, info.cd)
                if ic then
                    ic.cooldown:Show()
                    ic.cooldown:SetCooldown(startReal, math.max(0.1, durReal))
                end
            end
        end
    end
end)

local fSync = CreateFrame("Frame")
local pulseTimer = 0
fSync:SetScript("OnUpdate", function(self, elapsed)
    pulseTimer = pulseTimer + elapsed
    if pulseTimer > 1.5 then 
        pulseTimer = 0
        local enGrupo = IsInGroup()
        local _, miClase = UnitClass("player") -- FIX: Filtro de Mago
        
        for sID, info in pairs(spellDB) do
            if info.clase == miClase then -- FIX: Ignoramos las demas clases
                local startReal, durReal, isValid = ObtenerCooldownSeguro(sID)
                if isValid and durReal > 2 then
                    local rem = durReal - (GetTime() - startReal)
                    if rem > 0 then
                        local data = ultimoSyncEnviado[sID]
                        local expectedRem = data and (data.rem - (GetTime() - data.t)) or 0
                        if not data or math.abs(expectedRem - rem) > 1.0 then
                            ultimoSyncEnviado[sID] = { rem = rem, t = GetTime() }
                            
                            if enGrupo then
                                local _, class = UnitClass("player")
                                class = class or "UNKNOWN"
                                local specIndex = GetSpecialization()
                                local mySpec = specIndex and GetSpecializationInfo(specIndex) or 0
                                local channel = IsInRaid() and "RAID" or "PARTY"
                                C_ChatInfo.SendAddonMessage(prefijoAddon, tostring(sID).."#"..tostring(rem).."#"..tostring(info.cat).."#"..tostring(class).."#"..tostring(mySpec), channel)
                            end
                        end
                    end
                end
            end
        end
    end
end)

SLASH_PREMADECDS1 = "/premade"
SLASH_PREMADECDS2 = "/interrupts"
SlashCmdList["PREMADECDS"] = function()
    if addonTable.ToggleMenu then addonTable.ToggleMenu() end
end