-- modules/BigDebuffs.lua
-- Big Debuffs (Crowd Control) for ArenaCore -- because knowing your healer is sheeped is kinda important
-- Adapted from shade's proven compact-frame CC system, since its our addon and we can do that

local _, addon = ...
local AC = _G.ArenaCore
if not AC then return end

AC.BigDebuffs = AC.BigDebuffs or {}
local BD = AC.BigDebuffs

-- Taint-safe external state tables (never touch Blizzard frames directly)
local __bdFrames        = setmetatable({}, { __mode = "k" })
local __bdEventFrame    = nil
local __bdTestFrame     = nil
local __bdTestTicker    = nil
local __bdSoloHooked    = false
local __bdSoloUpdating  = false
local __bdMaxSlots      = 4

local TEST_CC_ICON_ID   = 136183
local TEST_CC_DURATION  = 8

-- Compact frame iteration (party + raid)
local PARTY_UNITS = { "party1", "party2", "party3", "party4" }
local RAID_UNITS  = { "raid1", "raid2", "raid3", "raid4", "raid5" }

local function IsSimpleGroupUnit(unit)
    if not unit then return false end
    return unit == "player"
        or unit:match("^party%d$") ~= nil
        or unit:match("^raid%d+$") ~= nil
end

local function ForEachCompactFrame(fn)
    if CompactPartyFrame and CompactPartyFrame.memberUnitFrames then
        for _, f in ipairs(CompactPartyFrame.memberUnitFrames) do
            if f then fn(f) end
        end
    end

    -- raids use CompactRaidFrameContainer for all members
    if CompactRaidFrameContainer and CompactRaidFrameContainer.flowFrames then
        for _, f in ipairs(CompactRaidFrameContainer.flowFrames) do
            if f and f.unit then
                local u = f.displayedUnit or f.unit
                if IsSimpleGroupUnit(u) then fn(f) end
            end
        end
    else
        -- fallback for dungeons / old raid frames
        for i = 1, 5 do
            local rf = _G["CompactRaidFrame" .. i]
            local u = rf and (rf.displayedUnit or rf.unit)
            if rf and IsSimpleGroupUnit(u) then fn(rf) end
        end
    end
end

-- Compact frame helpers
local function HookCompactPartyVisibility()
    if not CompactPartyFrame then return nil end
    return CompactPartyFrame
end

local function GetPlayerCompactFrame()
    if CompactRaidFrame1 and CompactRaidFrame1.unit == "player" and CompactRaidFrame1:IsShown() then
        return CompactRaidFrame1
    end
    if CompactPartyFrame and CompactPartyFrame.memberUnitFrames then
        for _, f in ipairs(CompactPartyFrame.memberUnitFrames) do
            if f and f.unit == "player" and f:IsShown() then return f end
        end
    end
    return nil
end

-- Database access
local function GetDB()
    local db = AC.DB and AC.DB.profile and AC.DB.profile.bigDebuffs
    if not db then
        db = {
            enabled      = false,
            theme        = "Blackout",
            iconScale    = 140,
            iconSpacing  = 15,
            offsetX      = 0,
            offsetY      = 0,
            customGlow   = true,
            soloRaidFrame = false,
            borderThickness = 2,
            iconZoom     = 0.08,
        }
    end
    return db
end

local function AnySoloRaidFrameEnabled()
    local profile = AC.DB and AC.DB.profile
    if not profile then return false end
    return (profile.bigDebuffs and profile.bigDebuffs.soloRaidFrame == true)
        or (profile.partyTrinkets and profile.partyTrinkets.soloRaidFrame == true)
end

local function PrimeBlizzardGlowFlipbook(glowFrame)
    if not glowFrame then return end
    local cols, rows = 5, 6
    local frameWidth = 1 / cols
    local frameHeight = 1 / rows
    local left, right = 0, frameWidth
    local top, bottom = 0, frameHeight
    if glowFrame.ProcStartFlipbook then
        glowFrame.ProcStartFlipbook:SetTexCoord(left, right, top, bottom)
    end
    if glowFrame.ProcLoopFlipbook then
        glowFrame.ProcLoopFlipbook:SetTexCoord(left, right, top, bottom)
    end
end

local function PlayBlizzardGlowOnce(glowFrame)
    if not glowFrame then return end
    if glowFrame._blizzardGlowPlayed then return end
    glowFrame._blizzardGlowPlayed = true
    -- Skip the opening flash and jump straight to the steady glow - the burst stutters and looks messy
    if glowFrame.ProcLoop then
        glowFrame.ProcLoop:Stop()
        glowFrame.ProcLoop:Play()
    end
end

-- ════════════════════════════════════════════════════════════════
-- FRAME CREATION
-- ════════════════════════════════════════════════════════════════

local function CreateBigDebuffSlot(parent, slot)
    local cc = CreateFrame("Button", nil, parent)
    cc:EnableMouse(false)
    cc:SetFrameLevel((parent:GetFrameLevel() or 0) + 30)
    cc._slot = slot

    cc.icon = cc:CreateTexture(nil, "ARTWORK")
    cc.icon:SetAllPoints(cc)

    cc.cooldown = CreateFrame("Cooldown", nil, cc, "CooldownFrameTemplate")
    cc.cooldown:SetAllPoints(cc.icon)
    cc.cooldown:SetReverse(true)
    cc.cooldown:SetDrawEdge(true)
    if cc.cooldown.SetHideCountdownNumbers then
        cc.cooldown:SetHideCountdownNumbers(false)
    end

    cc.borderTop    = cc:CreateTexture(nil, "OVERLAY")
    cc.borderTop:SetColorTexture(0, 0, 0, 1)
    cc.borderTop:Hide()

    cc.borderBottom = cc:CreateTexture(nil, "OVERLAY")
    cc.borderBottom:SetColorTexture(0, 0, 0, 1)
    cc.borderBottom:Hide()

    cc.borderLeft   = cc:CreateTexture(nil, "OVERLAY")
    cc.borderLeft:SetColorTexture(0, 0, 0, 1)
    cc.borderLeft:Hide()

    cc.borderRight  = cc:CreateTexture(nil, "OVERLAY")
    cc.borderRight:SetColorTexture(0, 0, 0, 1)
    cc.borderRight:Hide()

    cc.glow = cc:CreateTexture(nil, "OVERLAY")
    cc.glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
    cc.glow:SetBlendMode("ADD")
    cc.glow:SetPoint("CENTER", cc, "CENTER", 0, 0)

    -- Pulsing glow animation (like BuffReminder)
    cc.pulsingGlow = cc:CreateTexture(nil, "OVERLAY")
    cc.pulsingGlow:SetTexture("Interface\\SpellActivationOverlay\\IconAlert")
    cc.pulsingGlow:SetTexCoord(0.00781250, 0.50781250, 0.27734375, 0.52734375)
    cc.pulsingGlow:SetPoint("TOPLEFT", cc.icon, "TOPLEFT", -14, 14)
    cc.pulsingGlow:SetPoint("BOTTOMRIGHT", cc.icon, "BOTTOMRIGHT", 14, -14)
    cc.pulsingGlow:SetBlendMode("ADD")
    cc.pulsingGlow:Hide()

    cc.pulsingGlowAnim = cc.pulsingGlow:CreateAnimationGroup()
    cc.pulsingGlowAnim:SetLooping("BOUNCE")
    local fadeOut = cc.pulsingGlowAnim:CreateAnimation("Alpha")
    fadeOut:SetFromAlpha(0.85)
    fadeOut:SetToAlpha(0.35)
    fadeOut:SetDuration(0.8)
    fadeOut:SetSmoothing("IN_OUT")

    -- Blizzard Glow: authentic action bar proc glow via Blizzard's own template
    cc.blizzardGlowFrame = CreateFrame("Frame", nil, cc, "ActionButtonSpellAlertTemplate")
    cc.blizzardGlowFrame:SetFrameLevel(cc:GetFrameLevel() + 10)
    cc.blizzardGlowFrame:Hide()
    -- Manually initialize the mixin to ensure animation transition is set up
    if cc.blizzardGlowFrame.OnLoad then
        cc.blizzardGlowFrame:OnLoad()
    end
    PrimeBlizzardGlowFlipbook(cc.blizzardGlowFrame)
    cc.blizzardGlowFrame:SetScript("OnShow", function(self)
        self._blizzardGlowPlayed = nil
        PlayBlizzardGlowOnce(self)
    end)
    cc.blizzardGlowFrame:SetScript("OnHide", function(self)
        if self.ProcLoop and self.ProcLoop:IsPlaying() then
            self.ProcLoop:Stop()
        end
        self._blizzardGlowPlayed = nil
    end)

    cc.blackoutBorder = cc:CreateTexture(nil, "OVERLAY", nil, 7)
    cc.blackoutBorder:SetTexture([[Interface\Buttons\UI-Quickslot2]])
    cc.blackoutBorder:Hide()

    cc:SetScript("OnEnter", function(self)
        if not GameTooltip or not self._unit or not self._auraInstanceID then return end
        GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT")
        if GameTooltip.SetUnitDebuffByAuraInstanceID then
            GameTooltip:SetUnitDebuffByAuraInstanceID(self._unit, self._auraInstanceID)
            GameTooltip:Show()
        end
    end)
    cc:SetScript("OnLeave", function()
        if GameTooltip then GameTooltip:Hide() end
    end)

    cc:Hide()
    return cc
end

local function EnsureBigDebuffSlots(parent)
    if not parent then return nil end
    local pool = __bdFrames[parent]
    if not pool then
        pool = {}
        for i = 1, __bdMaxSlots do
            pool[i] = CreateBigDebuffSlot(parent, i)
        end
        __bdFrames[parent] = pool
    end
    return pool
end

-- ════════════════════════════════════════════════════════════════
-- AURA DETECTION
-- ════════════════════════════════════════════════════════════════

local function GetAllCrowdControlAuras(unit)
    if not unit or not UnitExists(unit) or not C_LossOfControl then return nil end
    if not C_LossOfControl.GetActiveLossOfControlDataCountByUnit or not C_LossOfControl.GetActiveLossOfControlDataByUnit then return nil end

    local results = {}
    local seenIDs = {} -- because blizzard loves giving us the same aura twice
    local count = C_LossOfControl.GetActiveLossOfControlDataCountByUnit(unit) or 0
    for i = 1, count do
        local data = C_LossOfControl.GetActiveLossOfControlDataByUnit(unit, i)
        if data and data.auraInstanceID and not seenIDs[data.auraInstanceID] and C_UnitAuras and C_UnitAuras.GetAuraDataByAuraInstanceID then
            local auraData = C_UnitAuras.GetAuraDataByAuraInstanceID(unit, data.auraInstanceID)
            if auraData and auraData.icon then
                seenIDs[data.auraInstanceID] = true
                table.insert(results, {
                    aura = auraData,
                    locIndex = i,
                    priority = data.priority or 0,
                })
            end
        end
    end

    table.sort(results, function(a, b) return a.priority > b.priority end)
    return results
end

-- ════════════════════════════════════════════════════════════════
-- LAYOUT + UPDATE
-- ════════════════════════════════════════════════════════════════

local function LayoutBigDebuffFrame(cc, parent, db, slot)
    slot = slot or 1
    local baseSize = 22
    if parent and parent.GetHeight then
        local h = parent:GetHeight()
        if type(h) == "number" and h > 0 then baseSize = h end
    end

    local scale = db.iconScale or 140
    if scale < 50 then scale = 50 end
    if scale > 300 then scale = 300 end

    local size = math.floor((baseSize * scale / 100) + 0.5)
    if size < 12 then size = 12 end

    cc:SetSize(size, size)
    cc:ClearAllPoints()

    -- Row layout: centered on parent, growing horizontally to the right
    local spacing = db.iconSpacing
    if spacing == nil then spacing = math.max(2, math.floor(size * 0.15)) end
    if spacing < 0 then spacing = 0 end
    if spacing > 100 then spacing = 100 end
    local totalRowWidth = (size * slot) + (spacing * (slot - 1))
    local startOffsetX = -((totalRowWidth - size) / 2)
    local posX = startOffsetX + ((slot - 1) * (size + spacing)) + (db.offsetX or 0)
    local posY = db.offsetY or 0

    cc:SetPoint("CENTER", parent, "CENTER", posX, posY)

    cc.icon:SetAllPoints(cc)
    cc.cooldown:SetAllPoints(cc.icon)

    -- Apply cooldown text size - need to find the font string child
    local textSize = db.cooldownTextSize or 12
    if textSize < 8 then textSize = 8 end
    if textSize > 32 then textSize = 32 end

    -- CooldownFrameTemplate has internal font strings we need to update
    for _, region in ipairs({cc.cooldown:GetRegions()}) do
        if region:IsObjectType("FontString") then
            region:SetFont(AC.FONT_PATH or "Interface\\AddOns\\ArenaCore\\Media\\Fonts\\arenacore.ttf", textSize, "OUTLINE")
        end
    end

    local zoom = db.iconZoom or 0.08
    if zoom < 0 then zoom = 0 end
    if zoom > 0.15 then zoom = 0.15 end
    cc.icon:SetTexCoord(zoom, 1 - zoom, zoom, 1 - zoom)

    local thickness = db.borderThickness or 2
    if thickness < 1 then thickness = 1 end
    if thickness > 5 then thickness = 5 end

    local effectiveScale = 1
    if cc.icon.GetEffectiveScale then
        effectiveScale = cc.icon:GetEffectiveScale() or 1
    end
    local uiScale = 1
    if UIParent and UIParent.GetEffectiveScale then
        uiScale = UIParent:GetEffectiveScale() or 1
    end
    local scaledThickness = thickness
    if effectiveScale > 0 then
        scaledThickness = thickness * (uiScale / effectiveScale)
    end

    cc.borderTop:ClearAllPoints()
    cc.borderTop:SetPoint("TOPLEFT", cc.icon, "TOPLEFT", -scaledThickness, scaledThickness)
    cc.borderTop:SetPoint("TOPRIGHT", cc.icon, "TOPRIGHT", scaledThickness, scaledThickness)
    cc.borderTop:SetHeight(scaledThickness)

    cc.borderBottom:ClearAllPoints()
    cc.borderBottom:SetPoint("BOTTOMLEFT", cc.icon, "BOTTOMLEFT", -scaledThickness, -scaledThickness)
    cc.borderBottom:SetPoint("BOTTOMRIGHT", cc.icon, "BOTTOMRIGHT", scaledThickness, -scaledThickness)
    cc.borderBottom:SetHeight(scaledThickness)

    cc.borderLeft:ClearAllPoints()
    cc.borderLeft:SetPoint("TOPLEFT", cc.icon, "TOPLEFT", -scaledThickness, scaledThickness)
    cc.borderLeft:SetPoint("BOTTOMLEFT", cc.icon, "BOTTOMLEFT", -scaledThickness, -scaledThickness)
    cc.borderLeft:SetWidth(scaledThickness)

    cc.borderRight:ClearAllPoints()
    cc.borderRight:SetPoint("TOPRIGHT", cc.icon, "TOPRIGHT", scaledThickness, scaledThickness)
    cc.borderRight:SetPoint("BOTTOMRIGHT", cc.icon, "BOTTOMRIGHT", scaledThickness, -scaledThickness)
    cc.borderRight:SetWidth(scaledThickness)

    cc.glow:ClearAllPoints()
    cc.glow:SetPoint("TOPLEFT", cc, "TOPLEFT", -size * 0.35, size * 0.35)
    cc.glow:SetPoint("BOTTOMRIGHT", cc, "BOTTOMRIGHT", size * 0.35, -size * 0.35)
    cc.glow:SetShown(db.customGlow == true)

    -- Size and position Blizzard glow frame (1.6x to fully cover sharp icon corners)
    cc.blizzardGlowFrame:SetSize(size * 1.6, size * 1.6)
    cc.blizzardGlowFrame:ClearAllPoints()
    cc.blizzardGlowFrame:SetPoint("CENTER", cc.icon, "CENTER", 0, 0)

    local gc = db.glowColor or { r = 0.8, g = 0.4, b = 0.9, a = 0.85 }
    cc.glow:SetVertexColor(gc.r or 0.8, gc.g or 0.4, gc.b or 0.9, gc.a or 0.85)

    local theme = db.theme or "Blackout"
    if theme == "Blackout" then
        local basePad = math.max(8, math.floor((size * 0.33) + 2.5))
        cc.blackoutBorder:ClearAllPoints()
        cc.blackoutBorder:SetPoint("TOPLEFT", cc.icon, "TOPLEFT", -(basePad + 1), basePad + 1)
        cc.blackoutBorder:SetPoint("BOTTOMRIGHT", cc.icon, "BOTTOMRIGHT", (basePad + 1), -(basePad + 1))
        cc.blackoutBorder:SetVertexColor(0, 0, 0, 1)
        cc.blackoutBorder:Show()
        cc.borderTop:Hide(); cc.borderBottom:Hide(); cc.borderLeft:Hide(); cc.borderRight:Hide()
        cc.pulsingGlow:Hide()
        cc.blizzardGlowFrame:Hide()
        if cc.blizzardGlowFrame.ProcStartAnim then cc.blizzardGlowFrame.ProcStartAnim:Stop() end
        if cc.blizzardGlowFrame.ProcLoop then cc.blizzardGlowFrame.ProcLoop:Stop() end
        if cc.pulsingGlowAnim and cc.pulsingGlowAnim:IsPlaying() then
            cc.pulsingGlowAnim:Stop()
        end
    elseif theme == "pixel_black" then
        cc.blackoutBorder:Hide()
        cc.borderTop:Show(); cc.borderBottom:Show(); cc.borderLeft:Show(); cc.borderRight:Show()
        cc.borderTop:SetVertexColor(0, 0, 0, 1)
        cc.borderBottom:SetVertexColor(0, 0, 0, 1)
        cc.borderLeft:SetVertexColor(0, 0, 0, 1)
        cc.borderRight:SetVertexColor(0, 0, 0, 1)
        cc.pulsingGlow:Hide()
        cc.blizzardGlowFrame:Hide()
        if cc.blizzardGlowFrame.ProcStartAnim then cc.blizzardGlowFrame.ProcStartAnim:Stop() end
        if cc.blizzardGlowFrame.ProcLoop then cc.blizzardGlowFrame.ProcLoop:Stop() end
        if cc.pulsingGlowAnim and cc.pulsingGlowAnim:IsPlaying() then
            cc.pulsingGlowAnim:Stop()
        end
    elseif theme == "pulsing_glow" then
        cc.blackoutBorder:Hide()
        cc.borderTop:Hide(); cc.borderBottom:Hide(); cc.borderLeft:Hide(); cc.borderRight:Hide()
        cc.glow:Hide()
        cc.blizzardGlowFrame:Hide()
        if cc.blizzardGlowFrame.ProcStartAnim then cc.blizzardGlowFrame.ProcStartAnim:Stop() end
        if cc.blizzardGlowFrame.ProcLoop then cc.blizzardGlowFrame.ProcLoop:Stop() end
        cc.pulsingGlow:Show()
        local gc = db.glowColor or { r = 0.8, g = 0.4, b = 0.9, a = 0.85 }
        cc.pulsingGlow:SetVertexColor(gc.r or 0.8, gc.g or 0.4, gc.b or 0.9, gc.a or 0.85)
        if cc.pulsingGlowAnim and not cc.pulsingGlowAnim:IsPlaying() then
            cc.pulsingGlowAnim:Play()
        end
    elseif theme == "blizzard_glow" then
        cc.blackoutBorder:Hide()
        cc.borderTop:Hide(); cc.borderBottom:Hide(); cc.borderLeft:Hide(); cc.borderRight:Hide()
        cc.glow:Hide()
        cc.pulsingGlow:Hide()
        PrimeBlizzardGlowFlipbook(cc.blizzardGlowFrame)
        cc.blizzardGlowFrame:Show()
        local gc = db.glowColor or { r = 1, g = 0.84, b = 0, a = 1 }
        if cc.blizzardGlowFrame.ProcStartFlipbook then
            cc.blizzardGlowFrame.ProcStartFlipbook:SetVertexColor(gc.r or 1, gc.g or 0.84, gc.b or 0, gc.a or 1)
        end
        if cc.blizzardGlowFrame.ProcLoopFlipbook then
            cc.blizzardGlowFrame.ProcLoopFlipbook:SetVertexColor(gc.r or 1, gc.g or 0.84, gc.b or 0, gc.a or 1)
        end
        -- No opening flash, just keep the steady loop alive for cleaner frames
        if cc.blizzardGlowFrame.ProcStartAnim and cc.blizzardGlowFrame.ProcStartAnim:IsPlaying() then
            cc.blizzardGlowFrame.ProcStartAnim:Stop()
        end
        if cc.blizzardGlowFrame.ProcLoop and not cc.blizzardGlowFrame.ProcLoop:IsPlaying() then
            cc.blizzardGlowFrame.ProcLoop:Play()
        end
        if cc.pulsingGlowAnim and cc.pulsingGlowAnim:IsPlaying() then
            cc.pulsingGlowAnim:Stop()
        end
    else
        cc.blackoutBorder:Hide()
        cc.borderTop:Hide(); cc.borderBottom:Hide(); cc.borderLeft:Hide(); cc.borderRight:Hide()
        cc.pulsingGlow:Hide()
        cc.blizzardGlowFrame:Hide()
        if cc.blizzardGlowFrame.ProcStartAnim then cc.blizzardGlowFrame.ProcStartAnim:Stop() end
        if cc.blizzardGlowFrame.ProcLoop then cc.blizzardGlowFrame.ProcLoop:Stop() end
        if cc.pulsingGlowAnim and cc.pulsingGlowAnim:IsPlaying() then
            cc.pulsingGlowAnim:Stop()
        end
    end
end

local function UpdateBigDebuffFrame(parent)
    local db = GetDB()
    local slots = EnsureBigDebuffSlots(parent)
    if not slots then return end

    local unit = parent and (parent.displayedUnit or parent.unit)
    if not db.enabled or not unit or not UnitExists(unit) then
        for i = 1, __bdMaxSlots do
            if slots[i] then slots[i]:Hide() end
        end
        return
    end

    local allAuras = GetAllCrowdControlAuras(unit)
    local auraCount = allAuras and #allAuras or 0

    -- Show active CC slots
    for i = 1, math.min(auraCount, __bdMaxSlots) do
        local entry = allAuras[i]
        local aura = entry and entry.aura
        local cc = slots[i]
        if not aura or not aura.icon or not cc then break end

        LayoutBigDebuffFrame(cc, parent, db, i)
        cc._unit = unit
        cc._auraInstanceID = aura.auraInstanceID
        cc.icon:SetTexture(aura.icon)

        local durationSet = false
        if entry.locIndex and C_LossOfControl.GetActiveLossOfControlDuration and cc.cooldown.SetCooldownFromDurationObject then
            local durationObj = C_LossOfControl.GetActiveLossOfControlDuration(unit, entry.locIndex)
            if durationObj then
                local ok = pcall(cc.cooldown.SetCooldownFromDurationObject, cc.cooldown, durationObj, true)
                durationSet = ok == true
                if durationSet and cc.cooldown.SetHideCountdownNumbers then
                    cc.cooldown:SetHideCountdownNumbers(false)
                end
            end
        end

        if not durationSet then
            cc.cooldown:Clear()
        end
        cc:Show()
    end

    -- Hide unused slots
    for i = auraCount + 1, __bdMaxSlots do
        if slots[i] then slots[i]:Hide() end
    end
end

local function RefreshBigDebuffFrames()
    ForEachCompactFrame(UpdateBigDebuffFrame)
end

-- ════════════════════════════════════════════════════════════════
-- EVENTS
-- ════════════════════════════════════════════════════════════════

local function EnsureBigDebuffEvents()
    if __bdEventFrame then return end

    __bdEventFrame = CreateFrame("Frame")
    __bdEventFrame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
    __bdEventFrame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
    __bdEventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    __bdEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    __bdEventFrame:SetScript("OnEvent", function(_, event)
        if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ENTERING_WORLD" then
            if BD and BD.UpdateSoloRaidFrame then BD:UpdateSoloRaidFrame() end
        end
        if not GetDB().enabled then return end
        RefreshBigDebuffFrames()
    end)
end

-- ════════════════════════════════════════════════════════════════
-- SOLO RAID FRAME (so users can test / keep consistent layout)
-- ════════════════════════════════════════════════════════════════

function BD:UpdateSoloRaidFrame()
    if InCombatLockdown and InCombatLockdown() then return end
    if EditModeManagerFrame and EditModeManagerFrame.IsEditModeActive and EditModeManagerFrame:IsEditModeActive() then
        return
    end

    local frame = HookCompactPartyVisibility()
    if not frame then return end

    local db = GetDB()
    local solo = not (IsInGroup() or IsInRaid())

    if not db.soloRaidFrame then
        if solo and AnySoloRaidFrameEnabled() then
            if PartyFrame then
                PartyFrame:Show()
                if PartyFrame.HidePartyFrames then
                    PartyFrame:HidePartyFrames()
                end
            end
            frame:Show()
            if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
                PartyFrame:UpdatePaddingAndLayout()
            end
            return
        end

        if solo then
            frame:Hide()
            if PartyFrame and not (EditModeManagerFrame and EditModeManagerFrame.ArePartyFramesForcedShown and EditModeManagerFrame:ArePartyFramesForcedShown()) then
                PartyFrame:Hide()
            end
            if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
                PartyFrame:UpdatePaddingAndLayout()
            end
        elseif frame.UpdateVisibility then
            if PartyFrame and PartyFrame.Show then
                PartyFrame:Show()
                if PartyFrame.UpdatePaddingAndLayout then
                    PartyFrame:UpdatePaddingAndLayout()
                end
            end
            __bdSoloUpdating = true
            frame:UpdateVisibility()
            __bdSoloUpdating = false
        end
        return
    end

    if not solo then
        if frame.UpdateVisibility then
            __bdSoloUpdating = true
            frame:UpdateVisibility()
            __bdSoloUpdating = false
        else
            frame:Show()
        end
        return
    end

    if PartyFrame then
        PartyFrame:Show()
        if PartyFrame.HidePartyFrames then
            PartyFrame:HidePartyFrames()
        end
    end
    frame:Show()
    if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
        PartyFrame:UpdatePaddingAndLayout()
    end
end

function BD:ToggleSoloRaidFrame()
    self:UpdateSoloRaidFrame()
end

-- ════════════════════════════════════════════════════════════════
-- TEST MODE
-- ════════════════════════════════════════════════════════════════

local function BuildTestBigDebuffIcon(playerFrame, keepCooldown)
    if not playerFrame then return nil end
    local db = GetDB()
    local slots = EnsureBigDebuffSlots(playerFrame)
    if not slots then return nil end

    -- Show at least 2 test CC icons so spacing is visible
    local testIcons = {
        { icon = TEST_CC_ICON_ID },
        { icon = 135848 }, -- Deep Freeze (blue) for contrast
    }

    for i = 1, math.min(#testIcons, __bdMaxSlots) do
        local cc = slots[i]
        if not cc then break end

        LayoutBigDebuffFrame(cc, playerFrame, db, i)
        cc._unit = nil
        cc._auraInstanceID = nil
        cc.icon:SetTexture(testIcons[i].icon)
        if not keepCooldown then
            cc.cooldown:SetCooldown(GetTime(), TEST_CC_DURATION)
        end
        cc:Show()

        -- Force cooldown text size application for test mode
        local textSize = db.cooldownTextSize or 12
        if textSize < 8 then textSize = 8 end
        if textSize > 32 then textSize = 32 end
        for _, region in ipairs({cc.cooldown:GetRegions()}) do
            if region:IsObjectType("FontString") then
                region:SetFont(AC.FONT_PATH or "Interface\\AddOns\\ArenaCore\\Media\\Fonts\\arenacore.ttf", textSize, "OUTLINE")
            end
        end
    end

    -- Hide extra slots
    for i = #testIcons + 1, __bdMaxSlots do
        if slots[i] then slots[i]:Hide() end
    end

    return slots[1]
end

local function StopTestBigDebuffLoop()
    if __bdTestTicker then
        __bdTestTicker:Cancel()
        __bdTestTicker = nil
    end
end

local function StartTestBigDebuffLoop()
    StopTestBigDebuffLoop()
    __bdTestTicker = C_Timer.NewTicker(TEST_CC_DURATION, function()
        if not __bdTestFrame or not __bdTestFrame:IsShown() then
            StopTestBigDebuffLoop()
            return
        end
        -- Refresh all shown slots on the test parent
        local parent = __bdTestFrame:GetParent()
        local slots = parent and __bdFrames[parent]
        if slots then
            for _, cc in ipairs(slots) do
                if cc and cc:IsShown() and cc.cooldown then
                    cc.cooldown:SetCooldown(GetTime(), TEST_CC_DURATION)
                end
            end
        end
    end)
end

local function HideAllTestSlots()
    local parent = __bdTestFrame and __bdTestFrame:GetParent()
    local slots = parent and __bdFrames[parent]
    if slots then
        for _, cc in ipairs(slots) do
            if cc then cc:Hide() end
        end
    end
end

function BD:IsTestActive()
    return __bdTestFrame and __bdTestFrame:IsShown()
end

function BD:ToggleTest()
    if __bdTestFrame then
        StopTestBigDebuffLoop()
        HideAllTestSlots()
        __bdTestFrame = nil
        return
    end

    local playerFrame = GetPlayerCompactFrame()
    if not playerFrame then return end

    __bdTestFrame = BuildTestBigDebuffIcon(playerFrame)
    if __bdTestFrame then
        StartTestBigDebuffLoop()
    end
end

function BD:RefreshTest()
    if not __bdTestFrame then return end
    local playerFrame = GetPlayerCompactFrame()
    if not playerFrame then
        StopTestBigDebuffLoop()
        HideAllTestSlots()
        __bdTestFrame = nil
        return
    end
    __bdTestFrame = BuildTestBigDebuffIcon(playerFrame, true)
end

function BD:HideTest()
    if __bdTestFrame then
        StopTestBigDebuffLoop()
        HideAllTestSlots()
        __bdTestFrame = nil
    end
end

-- ════════════════════════════════════════════════════════════════
-- PUBLIC API
-- ════════════════════════════════════════════════════════════════

function BD:Apply()
    EnsureBigDebuffEvents()
    local testActive = self:IsTestActive()
    RefreshBigDebuffFrames()
    self:UpdateSoloRaidFrame()
    if testActive then
        self:RefreshTest()
    end
    -- Refresh portrait spell overlay so toggles take effect immediately
    if AC.ClassIconAuras and AC.ClassIconAuras.RefreshImportantAuras then
        AC.ClassIconAuras:RefreshImportantAuras()
    end
    -- Refresh portrait glow line when settings change
    if AC.ClassIconAuras and AC.ClassIconAuras.UpdatePortraitGlowLine then
        AC.ClassIconAuras:UpdatePortraitGlowLine()
    end
end

function BD:Toggle()
    self:Apply()
end

function BD:Initialize()
    if self.__initialized then return end
    self.__initialized = true

    local ef = CreateFrame("Frame")
    self.eventFrame = ef
    ef:RegisterEvent("PLAYER_ENTERING_WORLD")
    ef:RegisterEvent("GROUP_ROSTER_UPDATE")
    ef:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" or event == "GROUP_ROSTER_UPDATE" then
            self:UpdateSoloRaidFrame()
        end
    end)

    HookCompactPartyVisibility()
    C_Timer.After(0.5, function()
        if BD and BD.Apply then BD:Apply() end
    end)
    C_Timer.After(1.0, function()
        if BD and BD.UpdateSoloRaidFrame then BD:UpdateSoloRaidFrame() end
    end)
end
