---@type Orbit
local Orbit = Orbit
local L = Orbit.L
local OrbitEngine = Orbit.Engine
local Constants = Orbit.Constants
local MasqueBridge = Orbit.Skin and Orbit.Skin.Masque
local ABC = Orbit.ActionBarsContainer
local ABText = Orbit.ActionBarsText
local ABPreview = Orbit.ActionBarsPreview
local GC = Orbit.Engine.GlowController

-- [ CONSTANTS ]--------------------------------------------------------------------------------------
local DEFAULT_ICON_SIZE = 34
local BUTTON_SIZE = 32                                              -- spell-flyout button size (matches sibling ActionBars*.lua convention; S19-C1)
local PET_BAR_INDEX = 9
local STANCE_BAR_INDEX = 10
local POSSESS_BAR_INDEX = 11
local MIN_STANCE_ICONS = 2
local VEHICLE_EXIT_INDEX = 13
local VEHICLE_EXIT_VISIBILITY = "[canexitvehicle] show; hide"
local DEFAULT_OOR_COLOR = { pins = { { position = 0, color = { r = 0.8, g = 0.2, b = 0.2, a = 1 } } } }
local DEFAULT_OOM_COLOR = { pins = { { position = 0, color = { r = 0.2, g = 0.2, b = 0.8, a = 1 } } } }
local DEFAULT_UNUSABLE_COLOR = { pins = { { position = 0, color = { r = 0.4, g = 0.4, b = 0.4, a = 1 } } } }
local DEFAULT_CD_SWIPE = { pins = { { position = 0, color = { r = 0, g = 0, b = 0, a = 0.8 } } } }

local BASE_VISIBILITY_DRIVER = "[petbattle][vehicleui] hide; show"
local PET_BAR_BASE_DRIVER = "[petbattle][vehicleui] hide; [pet,nooverridebar,nopossessbar] show; hide"
local BAR1_BASE_DRIVER = "[petbattle][overridebar] hide; show"

local SPECIAL_BAR_INDICES = { [STANCE_BAR_INDEX] = true, [POSSESS_BAR_INDEX] = true }
local DROPPABLE_CURSOR_TYPES = { spell = true, petaction = true, flyout = true, item = true, macro = true, mount = true }
local cachedOORColor, cachedOOMColor, cachedUnusableColor

-- [ PLUGIN REGISTRATION ]----------------------------------------------------------------------------
local BAR_CONFIG = {
    { blizzName = "MainActionBar", orbitName = "OrbitActionBar1", label = L.PLU_ACTION_BAR_1, index = 1, buttonPrefix = "ActionButton", count = 12 },
    { blizzName = "MultiBarBottomLeft", orbitName = "OrbitActionBar2", label = L.PLU_ACTION_BAR_2, index = 2, buttonPrefix = "MultiBarBottomLeftButton", count = 12 },
    { blizzName = "MultiBarBottomRight", orbitName = "OrbitActionBar3", label = L.PLU_ACTION_BAR_3, index = 3, buttonPrefix = "MultiBarBottomRightButton", count = 12 },
    { blizzName = "MultiBarRight", orbitName = "OrbitActionBar4", label = L.PLU_ACTION_BAR_4, index = 4, buttonPrefix = "MultiBarRightButton", count = 12 },
    { blizzName = "MultiBarLeft", orbitName = "OrbitActionBar5", label = L.PLU_ACTION_BAR_5, index = 5, buttonPrefix = "MultiBarLeftButton", count = 12 },
    { blizzName = "MultiBar5", orbitName = "OrbitActionBar6", label = L.PLU_ACTION_BAR_6, index = 6, buttonPrefix = "MultiBar5Button", count = 12 },
    { blizzName = "MultiBar6", orbitName = "OrbitActionBar7", label = L.PLU_ACTION_BAR_7, index = 7, buttonPrefix = "MultiBar6Button", count = 12 },
    { blizzName = "MultiBar7", orbitName = "OrbitActionBar8", label = L.PLU_ACTION_BAR_8, index = 8, buttonPrefix = "MultiBar7Button", count = 12 },
    { blizzName = "PetActionBar", orbitName = "OrbitPetBar", label = L.PLU_PET_BAR, index = 9, buttonPrefix = "PetActionButton", count = 10 },
    { blizzName = "StanceBar", orbitName = "OrbitStanceBar", label = L.PLU_STANCE_BAR, index = 10, buttonPrefix = "StanceButton", count = 10, isSpecial = true },
    { blizzName = "PossessBarFrame", orbitName = "OrbitPossessBar", label = L.PLU_POSSESS_BAR, index = 11, buttonPrefix = "PossessButton", count = 2, isSpecial = true },
}



local Plugin = Orbit:RegisterPlugin("Action Bars", "Orbit_ActionBars", {
    displayName = L.PLG_NAME_ACTION_BARS,
    defaults = {
        IconSize = 34, IconPadding = 2, Rows = 1, NumIcons = 12,
        Opacity = 100, HideEmptyButtons = false, UseGlobalTextStyle = true,
        DisabledComponents = {},
        ComponentPositions = {}, GlobalComponentPositions = {
            Keybind = { anchorX = "RIGHT", anchorY = "TOP", offsetX = 2, offsetY = 2, justifyH = "RIGHT" },
            MacroText = { anchorX = "CENTER", anchorY = "BOTTOM", offsetX = 0, offsetY = 2, justifyH = "CENTER" },
            Timer = { anchorX = "CENTER", anchorY = "CENTER", offsetX = 0, offsetY = 0, justifyH = "CENTER" },
            Stacks = { anchorX = "LEFT", anchorY = "BOTTOM", offsetX = 2, offsetY = 2, justifyH = "LEFT" },
        },
        GlobalDisabledComponents = {},
        OutOfCombatFade = false, ShowOnMouseover = true,
        KeypressColor = { pins = { { position = 0, color = { r = 1, g = 1, b = 1, a = 0.6 } } } },
        -- IconBackdropColor unset → inherits global "Background"; Colors-tab override falls through per-bar.
        OORColor = DEFAULT_OOR_COLOR, OOMColor = DEFAULT_OOM_COLOR, UnusableColor = DEFAULT_UNUSABLE_COLOR,
        CooldownSwipeColor = DEFAULT_CD_SWIPE,
        ProcGlowType = Constants.Glow.Type.Medium,
        ProcGlowColor = Constants.Glow.DefaultColor,
    },
})

Plugin.canvasMode = true
Plugin.supportsGlobalSync = true
Mixin(Plugin, Orbit.NativeBarMixin)
Plugin.containers = {}
Plugin.buttons = {}
Plugin.blizzBars = {}
Plugin.gridCache = {}

-- [ SPELL STATE ] -----------------------------------------------------------------------------------
local function ExtractColor(curveData)
    return OrbitEngine.ColorCurve:GetFirstColorFromCurve(curveData) or curveData
end

local function IsDesaturated(curveData) return curveData and curveData.desaturated or false end

local function InvalidateColorCache() cachedOORColor = nil; cachedOOMColor = nil; cachedUnusableColor = nil end

local function RefreshIconColor(plugin, button)
    if not button or not button.icon or not button.action then return end
    if not C_ActionBar.HasAction(button.action) then return end
    local _, spellID = GetActionInfo(button.action)
    local isUsable, notEnoughMana = IsUsableAction(button.action)
    local outOfRange = spellID and C_Spell.SpellHasRange(spellID) and C_Spell.IsSpellInRange(spellID) == false
    if not cachedOORColor then cachedOORColor = plugin:GetSetting(1, "OORColor") or DEFAULT_OOR_COLOR end
    if not cachedOOMColor then cachedOOMColor = plugin:GetSetting(1, "OOMColor") or DEFAULT_OOM_COLOR end
    if not cachedUnusableColor then cachedUnusableColor = plugin:GetSetting(1, "UnusableColor") or DEFAULT_UNUSABLE_COLOR end
    local oorData, oomData, unusableData = cachedOORColor, cachedOOMColor, cachedUnusableColor
    if outOfRange then
        local c = ExtractColor(oorData)
        button.icon:SetVertexColor(c.r, c.g, c.b)
        button.icon:SetDesaturated(IsDesaturated(oorData))
    elseif isUsable then
        button.icon:SetVertexColor(1, 1, 1)
        button.icon:SetDesaturated(false)
    elseif notEnoughMana then
        local c = ExtractColor(oomData)
        button.icon:SetVertexColor(c.r, c.g, c.b)
        button.icon:SetDesaturated(IsDesaturated(oomData))
    else
        local c = ExtractColor(unusableData)
        button.icon:SetVertexColor(c.r, c.g, c.b)
        button.icon:SetDesaturated(IsDesaturated(unusableData))
    end
end

local function RefreshAllManagedButtons(plugin)
    for _, buttons in pairs(plugin.buttons) do
        for _, button in ipairs(buttons) do RefreshIconColor(plugin, button) end
    end
end

-- [ HELPERS ] ---------------------------------------------------------------------------------------
local function EnsureHiddenFrame()
    if not Orbit.ButtonHideFrame then
        Orbit.ButtonHideFrame = CreateFrame("Frame")
        Orbit.ButtonHideFrame:Hide()
        Orbit.ButtonHideFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
    end
    return Orbit.ButtonHideFrame
end

-- [ SETTINGS UI ] -----------------------------------------------------------------------------------
function Plugin:AddSettings(dialog, systemFrame)
    local systemIndex = systemFrame.systemIndex or 1
    local SB = OrbitEngine.SchemaBuilder
    local container = self.containers[systemIndex]
    local schema = { hideNativeSettings = true, controls = {} }
    if systemIndex == 1 then
        schema.multiFrameOverride = {}
        for _, config in ipairs(BAR_CONFIG) do
            if self.containers[config.index] then table.insert(schema.multiFrameOverride, self.containers[config.index]) end
        end
    end
    if systemIndex == VEHICLE_EXIT_INDEX then
        table.insert(schema.controls, { type = "slider", key = "IconSize", label = L.PLU_AB_ICON_SIZE, min = 20, max = 64, step = 1, default = DEFAULT_ICON_SIZE, formatter = function(v) return v .. "px" end,
            onChange = function(val) self:SetSetting(systemIndex, "IconSize", val); self:ApplySettings(container) end })
        OrbitEngine.Config:Render(dialog, systemFrame, self, schema)
        return
    end
    SB:SetTabRefreshCallback(dialog, self, systemFrame)
    local tabs = { L.PLU_AB_TAB_LAYOUT, L.PLU_AB_TAB_GLOW, L.PLU_AB_TAB_COLORS }
    local currentTab = SB:AddSettingsTabs(schema, dialog, tabs, L.PLU_AB_TAB_LAYOUT)
    if currentTab == L.PLU_AB_TAB_LAYOUT then
        table.insert(schema.controls, { type = "slider", key = "NumActionBars", label = "|cFFFFD100" .. L.PLU_AB_NUM_BARS .. "|r", min = 1, max = 8, step = 1, default = 4,
            getValue = function() return self:GetSetting(1, "NumActionBars") end,
            onChange = function(val)
                self:SetSetting(1, "NumActionBars", val)
                for index, cont in pairs(self.containers) do
                    if index <= 8 then self:ApplySettings(cont) end
                end
            end })
        local config = BAR_CONFIG[systemIndex]
        local isSpecialBar = (config and config.isSpecial) or SPECIAL_BAR_INDICES[systemIndex]
        if config and config.count > 1 and not isSpecialBar then
            table.insert(schema.controls, { type = "slider", key = "NumIcons", label = L.PLU_AB_NUM_ICONS, min = 1, max = config.count, step = 1, default = config.count,
                onChange = function(val)
                    self:SetSetting(systemIndex, "NumIcons", val)
                    self:ApplySettings(container)
                end })
        end

        local numIcons = self:GetSetting(systemIndex, "NumIcons") or (config and config.count or 12)
        local factors = {}
        for i = 1, numIcons do if numIcons % i == 0 then table.insert(factors, i) end end
        local currentRows = self:GetSetting(systemIndex, "Rows") or 1
        local currentIndex = 1
        for i, v in ipairs(factors) do if v == currentRows then currentIndex = i; break end end
        if #factors > 1 then
            table.insert(schema.controls, { type = "slider", key = "_RowsSlider", label = L.PLU_AB_ROWS, min = 1, max = #factors, step = 1, default = currentIndex,
                formatter = function(v) return tostring(factors[math.floor(v)] or 1) end,
                onChange = function(val)
                    local newRows = factors[math.floor(val)] or 1
                    if newRows ~= self:GetSetting(systemIndex, "Rows") then
                        self:SetSetting(systemIndex, "Rows", newRows)
                        if container then self:ApplySettings(container) end
                    end
                end })
        end
        table.insert(schema.controls, { type = "slider", key = "IconSize", label = L.PLU_AB_ICON_SIZE, min = 20, max = 64, step = 1, default = DEFAULT_ICON_SIZE, formatter = function(v) return v .. "px" end,
            onChange = function(val) self:SetSetting(systemIndex, "IconSize", val); self:ApplySettings(container) end })
        table.insert(schema.controls, { type = "slider", key = "IconPadding", label = L.PLU_AB_ICON_PADDING, min = 0, max = 10, step = 1, default = 2,
            onChange = function(val) self:SetSetting(systemIndex, "IconPadding", val); self:ApplySettings(container) end })
        local isForcedHideEmpty = SPECIAL_BAR_INDICES[systemIndex]
        if not isForcedHideEmpty then table.insert(schema.controls, { type = "checkbox", key = "HideEmptyButtons", label = L.PLU_AB_HIDE_EMPTY, default = false }) end
    elseif currentTab == L.PLU_AB_TAB_GLOW then
        SB:AddGlowSettings(self, schema, 1, dialog, systemFrame, {
            prefix = "ProcGlow",
            label = L.PLU_AB_PROC_GLOW,
            default = Constants.Glow.Type.Medium,
        })
    elseif currentTab == L.PLU_AB_TAB_COLORS then
        -- Picker's starting swatch matches what the bar renders when IconBackdropColor is unset (inherits global "Background").
        local DEFAULT_BACKDROP = (Orbit.db.GlobalSettings and Orbit.db.GlobalSettings.UnitFrameBackdropColourCurve)
            or { pins = { { position = 0, color = { r = 0.08, g = 0.08, b = 0.08, a = 0.5 } } } }
        local DEFAULT_KEYPRESS = { pins = { { position = 0, color = { r = 1, g = 1, b = 1, a = 0.6 } } } }
        table.insert(schema.controls, { type = "colorcurve", key = "IconBackdropColor", label = L.PLU_AB_BACKDROP_COLOR, singleColor = true,
            default = DEFAULT_BACKDROP,
            onChange = function(val) self:SetSetting(systemIndex, "IconBackdropColor", val); self:ApplyAll() end })
        table.insert(schema.controls, { type = "colorcurve", key = "KeypressColor", label = L.PLU_AB_KEYPRESS_FLASH, singleColor = true,
            default = DEFAULT_KEYPRESS,
            onChange = function(val) self:SetSetting(systemIndex, "KeypressColor", val); self:ApplyAll() end })
        local function RefreshAllButtons()
            for _, buttons in pairs(self.buttons) do
                for _, button in ipairs(buttons) do RefreshIconColor(self, button) end
            end
        end
        table.insert(schema.controls, { type = "colorcurve", key = "OORColor", label = L.PLU_AB_OUT_OF_RANGE, singleColor = true, hasDesaturation = true,
            default = DEFAULT_OOR_COLOR,
            onChange = function(val) self:SetSetting(1, "OORColor", val); InvalidateColorCache(); RefreshAllButtons() end })
        table.insert(schema.controls, { type = "colorcurve", key = "OOMColor", label = L.PLU_AB_OUT_OF_MANA, singleColor = true, hasDesaturation = true,
            default = DEFAULT_OOM_COLOR,
            onChange = function(val) self:SetSetting(1, "OOMColor", val); InvalidateColorCache(); RefreshAllButtons() end })
        table.insert(schema.controls, { type = "colorcurve", key = "UnusableColor", label = L.PLU_AB_NOT_USABLE, singleColor = true, hasDesaturation = true,
            default = DEFAULT_UNUSABLE_COLOR,
            onChange = function(val) self:SetSetting(1, "UnusableColor", val); InvalidateColorCache(); RefreshAllButtons() end })
        table.insert(schema.controls, { type = "colorcurve", key = "CooldownSwipeColor", label = L.PLU_AB_COOLDOWN_SWIPE, singleColor = true,
            default = DEFAULT_CD_SWIPE,
            onChange = function(val) self:SetSetting(systemIndex, "CooldownSwipeColor", val); self:ApplyAll() end })
    end
    schema.extraButtons = { { text = L.PLU_AB_QUICK_KEYBIND, callback = function()
        if EditModeManagerFrame and EditModeManagerFrame:IsShown() then securecall("HideUIPanel", EditModeManagerFrame) end
        if QuickKeybindFrame then QuickKeybindFrame:Show() end
    end } }
    OrbitEngine.Config:Render(dialog, systemFrame, self, schema)
end

-- [ LIFECYCLE ] -------------------------------------------------------------------------------------
function Plugin:OnLoad()
    GC:PreLoad("Medium", 20)
    GC:PreLoad("Classic", 20)
    self:InitializeContainers()
    ABC:CreateVehicleExit(self)
    for index, container in pairs(self.containers) do ABPreview:Setup(self, container, index) end
    for index, container in pairs(self.containers) do
        self:ReparentButtons(index)
        OrbitEngine.Frame:RestorePosition(container, self, index)
        self:RefreshSkinSettings(index)
        self:LayoutButtons(index)
        if MasqueBridge then
            local config = BAR_CONFIG[index]
            if config then
                local groupName = config.label
                MasqueBridge:RegisterDisableCallback(groupName, function()
                    local isDisabled = not MasqueBridge:IsGroupEnabled(groupName)
                    for _, btn in ipairs(self.buttons[index] or {}) do
                        if isDisabled then Orbit.Skin.ActionButtonSkin:Apply(btn, { style = 1, aspectRatio = "1:1", zoom = 8, borderStyle = 1, borderSize = Orbit.db.GlobalSettings.BorderSize, iconBorder = true }) end
                    end
                    if not isDisabled then MasqueBridge:ReSkinGroup(groupName) end
                    self:ApplySettings(container)
                end)
            end
        end
    end
    self.UpdateVisibilityDriver = function()
        local numBars = self:GetSetting(1, "NumActionBars") or 4
        for index, container in pairs(self.containers) do
            if index == VEHICLE_EXIT_INDEX then
                if not InCombatLockdown() and Orbit:IsEditMode() then UnregisterStateDriver(container, "visibility"); container:Show() end
            elseif index == PET_BAR_INDEX and Orbit:IsEditMode() then
                if not InCombatLockdown() then UnregisterStateDriver(container, "visibility"); container:Show() end
            elseif not (index <= 8 and index > numBars) then
                if not InCombatLockdown() then
                    if index == PET_BAR_INDEX then RegisterStateDriver(container, "visibility", PET_BAR_BASE_DRIVER)
                    elseif index == 1 then RegisterStateDriver(container, "visibility", BAR1_BASE_DRIVER)
                    else RegisterStateDriver(container, "visibility", BASE_VISIBILITY_DRIVER) end
                end
            end
        end
        if InCombatLockdown() then
            Orbit.CombatManager:QueueUpdate(function() self:UpdateVisibilityDriver() end)
        end
    end
    Orbit.EventBus:On("PLAYER_REGEN_ENABLED", self.OnCombatEnd, self)
    if Orbit.Engine.EditMode then
        Orbit.Engine.EditMode:RegisterCallbacks({
            Enter = function() self:UpdateVisibilityDriver() end,
            Exit = function() self:ApplyAll() end,
        }, self)
    end
    Orbit.EventBus:On("UPDATE_MULTI_CAST_ACTIONBAR", function() C_Timer.After(0.1, function() self:ApplyAll() end) end, self)
    local function RefreshPetBar()
        if InCombatLockdown() then
            Orbit.CombatManager:QueueUpdate(RefreshPetBar)
            return
        end
        if self.petDebounce then self.petDebounce:Cancel() end
        self.petDebounce = C_Timer.NewTimer(0.05, function()
            self.petDebounce = nil
            if self.containers[PET_BAR_INDEX] then self:LayoutButtonPositions(PET_BAR_INDEX) end
        end)
    end

    Orbit.EventBus:On("UNIT_PET", RefreshPetBar, self)
    Orbit.EventBus:On("PET_BAR_UPDATE", RefreshPetBar, self)
    Orbit.EventBus:On("PET_UI_UPDATE", RefreshPetBar, self)
    Orbit.EventBus:On("UPDATE_VEHICLE_ACTIONBAR", RefreshPetBar, self)
    Orbit.EventBus:On("PLAYER_CONTROL_GAINED", RefreshPetBar, self)
    Orbit.EventBus:On("ORBIT_PLAYER_ENTERING_WORLD", RefreshPetBar, self)
    local function HideFlyoutBackground()
        local bg = SpellFlyoutBackgroundEnd
        if not bg then return end
        if bg.End then bg.End:Hide() end
        if bg.Start then bg.Start:Hide() end
        if bg.VerticalMiddle then bg.VerticalMiddle:Hide() end
        if bg.HorizontalMiddle then bg.HorizontalMiddle:Hide() end
    end
    local function SkinFlyoutButtons()
        HideFlyoutBackground()
        local flyout = SpellFlyout
        if not flyout or not flyout:IsShown() then return end
        if InCombatLockdown() then self.flyoutSkinPending = true; return end
        self.flyoutSkinPending = false
        local skinSettings = { style = 1, aspectRatio = "1:1", zoom = 8, borderStyle = 1, borderSize = Orbit.db.GlobalSettings.BorderSize, iconBorder = true, swipeColor = { r = 0, g = 0, b = 0, a = 0.8 }, showTimer = true, hideName = false, backdropColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(self:GetSetting(1, "IconBackdropColor")) or { r = 0.08, g = 0.08, b = 0.08, a = 0.5 }, keypressColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(self:GetSetting(1, "KeypressColor")) or { r = 1, g = 1, b = 1, a = 0.6 } }
        local i = 1
        while true do
            local btn = _G["SpellFlyoutButton" .. i]
            if not btn then break end
            btn:SetSize(BUTTON_SIZE, BUTTON_SIZE)
            Orbit.Skin.ActionButtonSkin:Apply(btn, skinSettings)
            i = i + 1
        end
    end
    if SpellFlyout then
        SpellFlyout:HookScript("OnShow", SkinFlyoutButtons)
        hooksecurefunc(SpellFlyout, "UpdateBackground", HideFlyoutBackground)
    end
    Orbit.EventBus:On("PLAYER_REGEN_ENABLED", function() if self.flyoutSkinPending then SkinFlyoutButtons() end end, self)
    Orbit.EventBus:On("CURSOR_CHANGED", function()
        local cursorType = GetCursorInfo()
        local isDraggingDroppable = DROPPABLE_CURSOR_TYPES[cursorType]
        local wasDragging = self.isDraggingDroppable
        if isDraggingDroppable then self.isDraggingDroppable = true
        elseif wasDragging then self.isDraggingDroppable = false end
        if not isDraggingDroppable and not wasDragging then return end
        if self.cursorTimer then self.cursorTimer:Cancel() end
        self.cursorTimer = C_Timer.NewTimer(0.05, function()
            if InCombatLockdown() then return end
            for index in pairs(self.containers) do self:LayoutButtonPositions(index) end
        end)
    end, self)
    -- [ SPELL STATE EVENTS ]-------------------------------------------------------------------------
    local function RefreshAll() RefreshAllManagedButtons(self) end
    Orbit.EventBus:On("PLAYER_TARGET_CHANGED", RefreshAll, self)
    Orbit.EventBus:On("ACTIONBAR_UPDATE_USABLE", RefreshAll, self)
    -- SPELL_UPDATE_USABLE fires at high frequency in combat; throttle leading-edge drops the burst tail.
    Orbit.EventBus:On("SPELL_UPDATE_USABLE", function()
        Orbit.Async:Throttle("ActionBars_RefreshAll", RefreshAll, 0.1)
    end, self)
    local plugin = self
    -- [ PROC GLOW HOOKS ]----------------------------------------------------------------------------
    if ActionButtonSpellAlertManager then
        local abPlugin = self
        local pendingHides = {}
        local hideScheduled = false
        local function FlushHides()
            hideScheduled = false
            for btn in pairs(pendingHides) do GC:HideProc(btn) end
            wipe(pendingHides)
        end
        local function IsOurButton(button)
            local parent = button:GetParent()
            if not parent or not abPlugin.containers then return false end
            for _, container in pairs(abPlugin.containers) do
                if parent == container then return true end
            end
            return false
        end
        hooksecurefunc(ActionButtonSpellAlertManager, "ShowAlert", function(_, button)
            if not IsOurButton(button) then return end
            pendingHides[button] = nil
            GC:ShowProc(button, function(k) return abPlugin:GetSetting(1, k) end, "ProcGlow", Constants.Glow.DefaultColor)
        end)
        hooksecurefunc(ActionButtonSpellAlertManager, "HideAlert", function(_, button)
            if not IsOurButton(button) then return end
            pendingHides[button] = true
            if not hideScheduled then
                hideScheduled = true
                C_Timer.After(0, FlushHides)
            end
        end)
    end
    Orbit.EventBus:On("ACTION_RANGE_CHECK_UPDATE", function(slot)
        local buttons = ActionBarButtonRangeCheckFrame.actions[slot]
        if not buttons then return end
        for _, button in pairs(buttons) do RefreshIconColor(plugin, button) end
    end)
end

-- [ EDIT MODE NUM ICONS PATCH ] ---------------------------------------------------------------------
StaticPopupDialogs["ORBIT_ACTIONBARS_RELOAD"] = { text = L.MSG_AB_ICON_COUNT_RELOAD, button1 = L.CMN_RELOAD, button2 = L.CMN_LATER, OnAccept = function() ReloadUI() end, timeout = 0, whileDead = true, hideOnEscape = true }

Orbit.EventBus:On("EDIT_MODE_LAYOUTS_UPDATED", function()
    local layoutInfo = C_EditMode.GetLayouts()
    if not layoutInfo then return end
    local dirty = false
    for _, layout in ipairs(layoutInfo.layouts) do
        if not layout.systems then break end
        for _, sys in ipairs(layout.systems) do
            if sys.system == Enum.EditModeSystem.ActionBar then
                for _, s in ipairs(sys.settings) do
                    if s.setting == Enum.EditModeActionBarSetting.NumIcons and s.value < 12 then s.value = 12; dirty = true end
                end
            end
        end
    end
    if not dirty then return end
    C_EditMode.SaveLayouts(layoutInfo)
    StaticPopup_Show("ORBIT_ACTIONBARS_RELOAD")
end, Plugin)

-- [ FACADE DELEGATES ] ------------------------------------------------------------------------------
function Plugin:InitializeContainers()
    for _, config in ipairs(BAR_CONFIG) do
        if not self.containers[config.index] then self.containers[config.index] = ABC:Create(self, config) end
    end
end

function Plugin:ReparentButtons(index) ABC:ReparentButtons(self, index, BAR_CONFIG) end

local _abDisabledHashCache = setmetatable({}, { __mode = "k" })

function Plugin:IsComponentDisabled(componentKey, systemIndex)
    systemIndex = systemIndex or 1
    local Txn = OrbitEngine.CanvasMode and OrbitEngine.CanvasMode.Transaction
    local txnActive = Txn and Txn:IsActive() and Txn:GetPlugin() == self
    local disabled
    if txnActive then disabled = Txn:GetDisabledComponents()
    elseif self:GetSetting(systemIndex, "UseGlobalTextStyle") ~= false then disabled = self:GetSetting(1, "GlobalDisabledComponents") or {}
    else disabled = self:GetSetting(systemIndex, "DisabledComponents") or {} end
    local hash = _abDisabledHashCache[disabled]
    if not hash then
        hash = {}
        for _, key in ipairs(disabled) do hash[key] = true end
        _abDisabledHashCache[disabled] = hash
    end
    return hash[componentKey] or false
end

function Plugin:OnCombatEnd() C_Timer.After(0.5, function() self:ApplyAll() end) end

-- [ BUTTON LAYOUT AND SKINNING ] --------------------------------------------------------------------
-- Cache per-bar skinSettings + 4 ColorCurve resolves; rebuilt only on settings/Masque change so cheap relayouts don't re-resolve.
function Plugin:RefreshSkinSettings(index)
    self._skinSettings = self._skinSettings or {}
    self._skinSettings[index] = {
        style = 1, aspectRatio = "1:1", zoom = 8, borderStyle = 1,
        borderSize = Orbit.db.GlobalSettings.BorderSize,
        iconBorder = true,
        padding = self:GetSetting(index, "IconPadding") or 2,
        cooldownSwipeColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(self:GetSetting(index, "CooldownSwipeColor")) or { r = 0, g = 0, b = 0, a = 0.8 },
        showTimer = true, hideName = false,
        backdropColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(self:GetSetting(index, "IconBackdropColor")),
        keypressColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(self:GetSetting(index, "KeypressColor")) or { r = 1, g = 1, b = 1, a = 0.6 },
    }
end

-- Cheap path: position + visibility only, skips ActionButtonSkin:Apply / ABText:Apply (only needed on real skin changes).
function Plugin:LayoutButtonPositions(index)
    self:LayoutButtons(index, true)
end

function Plugin:LayoutButtons(index, positionOnly)
    if InCombatLockdown() then return end
    local container = self.containers[index]
    local buttons = self.buttons[index]
    if container and container.orbitDisabled then return end
    if not container or not buttons or #buttons == 0 then return end
    local rawPadding = self:GetSetting(index, "IconPadding") or 2
    local rows = self:GetSetting(index, "Rows") or 1
    local orientation = 0
    local hideEmpty = self:GetSetting(index, "HideEmptyButtons")
    if SPECIAL_BAR_INDICES[index] then hideEmpty = true end
    local cursorType = GetCursorInfo()
    local cursorOverridesHide = DROPPABLE_CURSOR_TYPES[cursorType]
    if cursorOverridesHide and not SPECIAL_BAR_INDICES[index] then hideEmpty = false end
    local config = BAR_CONFIG[index]
    local numIcons = self:GetSetting(index, "NumIcons") or (config and config.count or 12)
    local iconSize = self:GetSetting(index, "IconSize") or DEFAULT_ICON_SIZE
    local scale = container:GetEffectiveScale() or 1
    local w = OrbitEngine.Pixel:Snap(iconSize, scale)
    local h = w
    local padding = OrbitEngine.Pixel:Multiple(rawPadding, scale)
    local useMasque = MasqueBridge and MasqueBridge.enabled
    local masqueGroup = useMasque and (config and config.label or "Action Bar " .. index)
    -- Defensive first-build; steady-state RefreshSkinSettings populates from ApplySettings/OnLoad/Masque-callback.
    if not positionOnly and (not self._skinSettings or not self._skinSettings[index]) then
        self:RefreshSkinSettings(index)
    end
    local skinSettings = self._skinSettings and self._skinSettings[index]
    local totalEffective = math.min(#buttons, numIcons)
    local limitPerLine
    if orientation == 0 then limitPerLine = math.max(1, math.ceil(totalEffective / rows))
    else limitPerLine = rows end
    local cacheKey = string.format("%d_%d_%d_%d_%d_%d_%.4f", numIcons, limitPerLine, orientation, iconSize, rawPadding, Orbit.db.GlobalSettings.IconBorderSize or 2, scale)
    local cache = self.gridCache[index]
    if not cache or cache.key ~= cacheKey then
        local positions = {}
        for i = 1, numIcons do
            local x, y = OrbitEngine.Layout:ComputeGridPosition(i, limitPerLine, orientation, w, h, padding)
            positions[i] = { x = x, y = y }
        end
        self.gridCache[index] = { key = cacheKey, positions = positions }
        cache = self.gridCache[index]
    end
    local cachedPositions = cache.positions
    for i, button in ipairs(buttons) do
        if i > numIcons then
            if not InCombatLockdown() then 
                button:SetParent(EnsureHiddenFrame())
                button:ClearAllPoints()
                button:SetPoint("CENTER", EnsureHiddenFrame(), "CENTER")
                button:Hide() 
            end
            button.orbitHidden = true
        else
            local hasAction = false
            if button.HasAction then
                hasAction = button:HasAction()
            elseif index == PET_BAR_INDEX then
                local petName, petTexture, petIsToken = GetPetActionInfo(button:GetID() or i)
                if petName or petTexture or petIsToken then hasAction = true end
            elseif index == STANCE_BAR_INDEX then
                local stanceTexture, stanceName = GetShapeshiftFormInfo(button:GetID() or i)
                if stanceTexture or stanceName then hasAction = true end
            end

            local shouldShow = not (hideEmpty and not hasAction)
            if not shouldShow then
                if not InCombatLockdown() then 
                    button:SetParent(EnsureHiddenFrame())
                    button:ClearAllPoints()
                    button:SetPoint("CENTER", EnsureHiddenFrame(), "CENTER")
                    button:Hide() 
                end
                button.orbitHidden = true
                if button.orbitBackdrop then button.orbitBackdrop:Hide() end
            else
                if button.orbitHidden and not InCombatLockdown() then button:SetParent(container) end
                button.orbitHidden = false; button:Show(); button:SetSize(w, h)
                if useMasque then MasqueBridge:AddActionButton(masqueGroup, button) end
                -- Skip per-button re-skin on cheap paths and Masque-owned bars (Masque owns the visual stack).
                if not positionOnly and skinSettings and (not useMasque or not MasqueBridge:IsGroupEnabled(masqueGroup)) then
                    Orbit.Skin.ActionButtonSkin:Apply(button, skinSettings)
                end
                if not positionOnly then ABText:Apply(self, button, index) end
                button:ClearAllPoints()
                local pos = cachedPositions[i]
                button:SetPoint("TOPLEFT", container, "TOPLEFT", pos.x, pos.y)
                
                if GC:IsActive(button, "orbitProc") then
                    GC:ShowProc(button, function(k) return self:GetSetting(1, k) end, "ProcGlow", Constants.Glow.DefaultColor)
                end
            end
        end
    end
    local sizeCount = totalEffective
    if index == STANCE_BAR_INDEX then
        local visibleCount = 0
        for i = 1, totalEffective do if buttons[i] and not buttons[i].orbitHidden then visibleCount = visibleCount + 1 end end
        sizeCount = math.max(visibleCount, MIN_STANCE_ICONS)
    end
    local sizeLimitPerLine = (orientation == 0) and math.max(1, math.ceil(sizeCount / rows)) or rows
    local finalW, finalH = OrbitEngine.Layout:ComputeGridContainerSize(sizeCount, sizeLimitPerLine, orientation, w, h, padding)
    container:SetSize(finalW, finalH)
    container.orbitRowHeight, container.orbitColumnWidth = h, w
    container._isIconContainer = true
    local iconNineSlice = Orbit.Skin:GetActiveIconBorderStyle()
    if rawPadding == 0 then Orbit.Skin:ApplyIconGroupBorder(container, iconNineSlice)
    else Orbit.Skin:ClearIconGroupBorder(container) end
    -- ApplyAll suppresses per-bar fire and coalesces into one event at the tail; direct callers still fire per-call (they only touch one bar).
    if not self._suppressBorderEvent then
        Orbit.EventBus:Fire("ORBIT_BORDER_LAYOUT_CHANGED")
    end
end

-- [ SETTINGS APPLICATION ] --------------------------------------------------------------------------
function Plugin:ApplyAll()
    self._suppressBorderEvent = true
    for index, container in pairs(self.containers) do self:ApplySettings(container) end
    self._suppressBorderEvent = false
    Orbit.EventBus:Fire("ORBIT_BORDER_LAYOUT_CHANGED")
end

function Plugin:ApplySettings(frame)
    if not frame then self:ApplyAll(); return end
    if InCombatLockdown() then return end
    local actualFrame = frame.systemFrame or frame
    local index = frame.systemIndex or actualFrame.systemIndex
    if not index or not actualFrame then return end
    if index == VEHICLE_EXIT_INDEX then
        if Orbit:IsEditMode() then UnregisterStateDriver(actualFrame, "visibility"); actualFrame:Show()
        else RegisterStateDriver(actualFrame, "visibility", VEHICLE_EXIT_VISIBILITY) end
        local veSize = self:GetSetting(index, "IconSize") or DEFAULT_ICON_SIZE
        local veScale = actualFrame:GetEffectiveScale() or 1
        veSize = OrbitEngine.Pixel:Snap(veSize, veScale)
        actualFrame:SetSize(veSize, veSize)
        if self.vehicleExitButton and self.vehicleExitButton == actualFrame then
            local btn = _G["OrbitVehicleExitButton"]
            if btn then btn:SetAllPoints(actualFrame) end
        end
        OrbitEngine.Frame:RestorePosition(actualFrame, self, index)
        return
    end
    local enabled = true
    if index <= 8 then
        local numBars = self:GetSetting(1, "NumActionBars") or 4
        if index > numBars then enabled = false end
    end
    if not self.blizzBars[index] then local config = BAR_CONFIG[index]; if config then self.blizzBars[index] = _G[config.blizzName] end end
    local blizzBar = self.blizzBars[index]
    if blizzBar then OrbitEngine.NativeFrame:KeepAliveHidden(blizzBar) end
    if enabled == false then
        UnregisterStateDriver(actualFrame, "visibility")
        if blizzBar then OrbitEngine.NativeFrame:SecureHide(blizzBar) end
        local point, _, _, x, y = actualFrame:GetPoint(1)
        if point then self:SetSetting(index, "Position", { point = point, x = x, y = y }) end
        local buttons = self.buttons[index]
        if buttons and #buttons > 0 then
            local hiddenFrame = EnsureHiddenFrame()
            for _, button in ipairs(buttons) do button:SetParent(hiddenFrame); button:Hide(); button.orbitHidden = true end
        end
        actualFrame:Hide()
        OrbitEngine.FrameAnchor:SetFrameDisabled(actualFrame, true)
        return
    end
    OrbitEngine.FrameAnchor:SetFrameDisabled(actualFrame, false)
    if index == 1 then RegisterStateDriver(actualFrame, "visibility", BAR1_BASE_DRIVER)
    elseif index == PET_BAR_INDEX then RegisterStateDriver(actualFrame, "visibility", PET_BAR_BASE_DRIVER)
    else RegisterStateDriver(actualFrame, "visibility", BASE_VISIBILITY_DRIVER) end
    if not self.buttons[index] or #self.buttons[index] == 0 then self:ReparentButtons(index) end
    local enableHover = self:GetSetting(index, "ShowOnMouseover") ~= false
    if Orbit.OOCFadeMixin then Orbit.OOCFadeMixin:ApplyOOCFade(actualFrame, self, index, "OutOfCombatFade", enableHover) end
    self:ApplyMouseOver(actualFrame, index)
    self:RefreshSkinSettings(index)
    self:LayoutButtons(index)
    OrbitEngine.Frame:RestorePosition(actualFrame, self, index)
    if self.buttons[index] then
        local direction = "UP"
        if actualFrame.GetSpellFlyoutDirection then direction = actualFrame:GetSpellFlyoutDirection() end
        for _, button in ipairs(self.buttons[index]) do
            if not InCombatLockdown() then button:SetAttribute("flyoutDirection", direction) end
            if button.SetPopupDirection then button:SetPopupDirection(direction) end
            if button.UpdateFlyout then button:UpdateFlyout() end
        end
    end
end

function Plugin:GetFrameBySystemIndex(systemIndex) return self.containers[systemIndex] end
