---@type Orbit
local Orbit = Orbit
local L = Orbit.L
local OrbitEngine = Orbit.Engine
local Pixel = Orbit.Engine.Pixel
local LSM = LibStub("LibSharedMedia-3.0")

local CB = Orbit.BossFrameCastBar

-- [ CONSTANTS ]--------------------------------------------------------------------------------------
local MAX_BOSS_FRAMES = 5
local POWER_BAR_HEIGHT_RATIO = 0.2
local DEFAULT_DEBUFF_ICON_SIZE = 25
local DEFAULT_BUFF_ICON_SIZE = 20
local MARKER_ICON_SIZE = 16
local DEFAULT_BOSS_OFFSET_X = -100
local DEFAULT_BOSS_OFFSET_Y = 100
local AURA_CONTAINER_PLACEHOLDER_W = 100
local AURA_CONTAINER_PLACEHOLDER_H = 20

-- [ PLUGIN REGISTRATION ] ---------------------------------------------------------------------------
local SYSTEM_ID = "Orbit_BossFrames"

local Plugin = Orbit:RegisterPlugin("Boss Frames", SYSTEM_ID, {
    displayName = L.PLG_NAME_BOSS_FRAMES,
    defaults = {
        Width = 116, Height = 39, Scale = 100, Spacing = 24,
        Orientation = 0,
        CastBarHeight = 18, CastBarWidth = 98, CastBarIcon = false, CastBarPosition = "Below",
        DebuffSize = 32, MaxDebuffs = 4, DebuffPosition = "Left",
        ReactionColour = false,
        PandemicGlowType = Orbit.Constants.Glow.Type.Thin,
        PandemicGlowColor = Orbit.Constants.Glow.DefaultColor,
        PandemicGlowColorCurve = { pins = { { position = 0.589, color = { r = 1, g = 0.188, b = 0, a = 1 } } } },
        DisabledComponents = { "Status" },
        ComponentPositions = {
            CastBar    = { anchorX = "CENTER", anchorY = "BOTTOM", offsetX = 0, offsetY = -10, justifyH = "CENTER", selfAnchorY = "BOTTOM", posX = 0,   posY = -28, overrides = { CastBarWidth = 98, CastBarHeight = 18, CastBarIcon = false }, subComponents = { Timer = { anchorX = "RIGHT", anchorY = "CENTER", offsetX = 4, offsetY = 0, justifyH = "RIGHT" }, Text = { anchorX = "LEFT", anchorY = "CENTER", offsetX = 4, offsetY = 0, justifyH = "LEFT" } } },
            Debuffs    = { anchorX = "LEFT",   anchorY = "CENTER", offsetX = -1, offsetY = 0, justifyH = "RIGHT",  selfAnchorY = "CENTER", posX = -58, posY = 0,   overrides = { MaxIcons = 5, MaxRows = 1, FilterDensity = 2, IconSize = 50, PandemicGlowColorCurve = { pins = { { position = 0.589, color = { r = 1, g = 0.188, b = 0, a = 1 } } } }, PandemicGlowType = Orbit.Constants.Glow.Type.Thin } },
            Name       = { anchorX = "LEFT",   anchorY = "CENTER", offsetX = 5,  offsetY = 0, justifyH = "LEFT",   selfAnchorY = "CENTER", posX = -53, posY = 0 },
            HealthText = { anchorX = "RIGHT",  anchorY = "CENTER", offsetX = 5,  offsetY = 0, justifyH = "RIGHT",  selfAnchorY = "CENTER", posX = 53,  posY = 0 },
            Buffs      = { anchorX = "RIGHT",  anchorY = "CENTER", offsetX = -1, offsetY = 0, justifyH = "LEFT",   selfAnchorY = "CENTER", posX = 58,  posY = 0,   overrides = { MaxIcons = 6, MaxRows = 2, FilterDensity = 2, IconSize = 34 } },
            MarkerIcon = { anchorX = "CENTER", anchorY = "TOP",    offsetX = 0,  offsetY = 0, justifyH = "CENTER", selfAnchorY = "TOP",    posX = 0,   posY = 19 },
        },
        CastBarColor = { r = 1, g = 0.7, b = 0 },
        CastBarColorCurve = { pins = { { position = 0, color = { r = 1, g = 0.7, b = 0, a = 1 } } } },
        NonInterruptibleColor = { r = 0.7, g = 0.7, b = 0.7 },
        NonInterruptibleColorCurve = { pins = { { position = 0, color = { r = 0.7, g = 0.7, b = 0.7, a = 1 } } } },
        CastBarText = true, CastBarTimer = true,
    },
})

Mixin(Plugin, Orbit.UnitFrameMixin, Orbit.BossFramePreviewMixin, Orbit.AuraMixin, Orbit.StatusIconMixin)
Plugin.canvasMode = true
Plugin.supportsPandemicGlow = true

-- [ POWER BAR ] -------------------------------------------------------------------------------------
local function UpdateFrameLayout(frame, borderSize)
    Plugin:UpdateFrameLayout(frame, borderSize, { powerBarRatio = POWER_BAR_HEIGHT_RATIO })
end

local function CreatePowerBar(parent, unit)
    local power = CreateFrame("StatusBar", nil, parent)
    power:SetPoint("BOTTOMLEFT", 0, 0)
    power:SetPoint("BOTTOMRIGHT", 0, 0)
    power:SetHeight(Pixel:Snap(parent:GetHeight() * POWER_BAR_HEIGHT_RATIO, parent:GetEffectiveScale()))
    power:SetStatusBarTexture("Interface\\TargetingFrame\\UI-TargetingFrame-BarFill")
    power:SetMinMaxValues(0, 1)
    power:SetValue(0)
    power.unit = unit
    power.bg = power:CreateTexture(nil, "BACKGROUND")
    power.bg:SetAllPoints()
    local globalSettings = Orbit.db.GlobalSettings or {}
    Orbit.Skin:ApplyGradientBackground(power, globalSettings.UnitFrameBackdropColourCurve, Orbit.Constants.Colors.Background)
    -- Register power surfaces so the rounded border mask clips them (matches GroupFrames).
    Orbit.Skin:RegisterMaskedSurface(parent, power.bg)
    Orbit.Skin:RegisterMaskedSurface(parent, power:GetStatusBarTexture())
    return power
end

local function UpdatePowerBar(frame)
    if not frame.Power or not UnitExists(frame.unit) then return end
    local power, maxPower, powerType = UnitPower(frame.unit), UnitPowerMax(frame.unit), UnitPowerType(frame.unit)
    frame.Power:SetMinMaxValues(0, maxPower)
    frame.Power:SetValue(power)
    local color = Orbit.Constants.Colors:GetPowerColor(powerType)
    frame.Power:SetStatusBarColor(color.r, color.g, color.b)
end

-- [ AURA DISPLAY CONFIG ]----------------------------------------------------------------------------
local function BossDebuffSkin(plugin)
    local skin = {
        zoom = 0, borderStyle = 1, borderSize = 1, showTimer = true, enablePandemic = true,
        pandemicGlowType = plugin:GetSetting(1, "PandemicGlowType") or Orbit.Constants.Glow.Type.Pixel,
        pandemicColor = OrbitEngine.ColorCurve:GetFirstColorFromCurve(plugin:GetSetting(1, "PandemicGlowColorCurve"))
            or plugin:GetSetting(1, "PandemicGlowColor") or Orbit.Constants.Glow.DefaultColor,
    }
    skin.overrides = setmetatable({
        PandemicGlowType = skin.pandemicGlowType,
        PandemicGlowColor = skin.pandemicColor,
    }, { __index = function(_, k) return plugin:GetSetting(1, k) end })
    return skin
end

local BOSS_BUFF_SKIN = Orbit.Constants.Aura.SkinWithTimer
local BOSS_DEBUFF_CFG = { componentKey = "Debuffs", fetchFilter = "HARMFUL|PLAYER", tooltipFilter = "HARMFUL|PLAYER", defaultMaxIcons = 4, skinSettings = BossDebuffSkin, defaultAnchorX = "LEFT", defaultJustifyH = "LEFT", helpers = function() return Orbit.BossFrameHelpers end }
local BOSS_BUFF_CFG = { componentKey = "Buffs", fetchFilter = "HELPFUL", tooltipFilter = "HELPFUL", defaultMaxIcons = 3, skinSettings = BOSS_BUFF_SKIN, defaultAnchorX = "RIGHT", defaultJustifyH = "RIGHT", helpers = function() return Orbit.BossFrameHelpers end }

local function UpdateDebuffs(frame, plugin) plugin:UpdateAuraContainer(frame, plugin, "debuffContainer", "debuffPool", BOSS_DEBUFF_CFG) end
local function UpdateBuffs(frame, plugin) plugin:UpdateAuraContainer(frame, plugin, "buffContainer", "buffPool", BOSS_BUFF_CFG) end

-- [ BOSS FRAME CREATION ] ---------------------------------------------------------------------------
local function CreateBossFrame(bossIndex, plugin)
    local unit = "boss" .. bossIndex
    local frame = OrbitEngine.UnitButton:Create(UIParent, unit, "OrbitBossFrame" .. bossIndex, true)
    frame:RegisterUnitEvent("UNIT_HEALTH", unit)
    frame:RegisterUnitEvent("UNIT_MAXHEALTH", unit)
    frame:RegisterUnitEvent("UNIT_ABSORB_AMOUNT_CHANGED", unit)
    frame:RegisterUnitEvent("UNIT_HEAL_ABSORB_AMOUNT_CHANGED", unit)
    frame:RegisterUnitEvent("UNIT_HEAL_PREDICTION", unit)
    frame:RegisterEvent("PLAYER_ENTERING_WORLD")
    if frame.HealthDamageBar then
        frame.HealthDamageBar:Hide()
        frame.HealthDamageBar = nil
    end
    if frame.HealthDamageTexture then
        frame.HealthDamageTexture:Hide()
        frame.HealthDamageTexture:ClearAllPoints()
        frame.HealthDamageTexture = nil
    end
    frame.editModeName = L.PLU_BOSS_FRAME_INDEX_F:format(bossIndex)
    frame.systemIndex, frame.bossIndex = 1, bossIndex
    frame:SetSize(plugin:GetSetting(1, "Width") or 150, plugin:GetSetting(1, "Height") or 40)
    frame:SetFrameStrata(Orbit.Constants.Strata.HUD)
    frame:SetFrameLevel(Orbit.StrataEngine:GetFrameLevel("Global_HUD", "Orbit_BossFrames") + bossIndex)
    UpdateFrameLayout(frame, Orbit.db.GlobalSettings.BorderSize)
    frame.Power = CreatePowerBar(frame, unit)
    frame:RegisterUnitEvent("UNIT_POWER_UPDATE", unit)
    frame:RegisterUnitEvent("UNIT_MAXPOWER", unit)
    frame:RegisterUnitEvent("UNIT_DISPLAYPOWER", unit)
    frame:RegisterUnitEvent("UNIT_POWER_FREQUENT", unit)
    frame:SetScript("OnShow", function(self)
        self:UpdateAll(); UpdatePowerBar(self); UpdateFrameLayout(self, Orbit.db.GlobalSettings.BorderSize)
        UpdateDebuffs(self, plugin); UpdateBuffs(self, plugin); plugin:UpdateMarkerIcon(self, plugin)
    end)
    frame.debuffContainer = CreateFrame("Frame", nil, frame); frame.debuffContainer:SetSize(AURA_CONTAINER_PLACEHOLDER_W, AURA_CONTAINER_PLACEHOLDER_H)
    frame.buffContainer = CreateFrame("Frame", nil, frame); frame.buffContainer:SetSize(AURA_CONTAINER_PLACEHOLDER_W, AURA_CONTAINER_PLACEHOLDER_H)
    frame.StatusOverlay = CreateFrame("Frame", nil, frame)
    frame.StatusOverlay:SetAllPoints()
    frame.StatusOverlay:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.Overlay)
    frame.MarkerIcon = frame.StatusOverlay:CreateTexture(nil, "OVERLAY")
    frame.MarkerIcon:SetSize(MARKER_ICON_SIZE, MARKER_ICON_SIZE)
    frame.MarkerIcon.orbitOriginalWidth, frame.MarkerIcon.orbitOriginalHeight = MARKER_ICON_SIZE, MARKER_ICON_SIZE
    frame.MarkerIcon:SetPoint("TOP", frame, "TOP", 0, -2)
    frame.MarkerIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
    frame.MarkerIcon:Hide()
    frame:RegisterUnitEvent("UNIT_AURA", unit)
    frame:RegisterEvent("RAID_TARGET_UPDATE")
    frame.CastBar = CB:Create(frame, bossIndex, plugin)
    CB:Position(frame.CastBar, frame, plugin)
    local originalOnEvent = frame:GetScript("OnEvent")
    frame:SetScript("OnEvent", function(f, event, eventUnit, updateInfo, ...)
        local p = Orbit.Profiler
        local s = p and p:Begin()
        if event == "UNIT_POWER_UPDATE" or event == "UNIT_MAXPOWER" then
            if eventUnit == unit then UpdatePowerBar(f) end
        elseif event == "UNIT_AURA" then
            if eventUnit == unit then
                local isFullUpdate = not updateInfo or updateInfo.isFullUpdate
                local snap
                if not isFullUpdate and f._harmfulAuraCache then
                    if Orbit.AuraSnapshotCache:Patch(f, unit, updateInfo) then
                        snap = Orbit.AuraSnapshotCache:Build(f)
                    end
                else
                    snap = plugin:BuildAuraSnapshot(unit)
                    Orbit.AuraSnapshotCache:Populate(f, snap)
                end
                if snap then
                    f._auraSnapshot = snap
                    UpdateDebuffs(f, plugin)
                    UpdateBuffs(f, plugin)
                    f._auraSnapshot = nil
                end
            end
        elseif event == "RAID_TARGET_UPDATE" then
            plugin:UpdateMarkerIcon(f, plugin)
        elseif originalOnEvent then
            originalOnEvent(f, event, eventUnit, updateInfo, ...)
        end
        if p then p:End(plugin, event, s) end
    end)
    frame.healthTextEnabled = true
    if frame.SetAbsorbsEnabled then frame:SetAbsorbsEnabled(true) end
    if frame.SetHealAbsorbsEnabled then frame:SetHealAbsorbsEnabled(true) end
    return frame
end

-- [ NATIVE FRAME HIDING ] ---------------------------------------------------------------------------
local function HideNativeBossFrames()
    for i = 1, MAX_BOSS_FRAMES do
        local bossFrame = _G["Boss" .. i .. "TargetFrame"]
        if bossFrame then OrbitEngine.NativeFrame:Park(bossFrame) end
    end
end

-- [ SETTINGS UI ] -----------------------------------------------------------------------------------
function Plugin:AddSettings(dialog, systemFrame)
    local SB = OrbitEngine.SchemaBuilder
    local schema = { hideNativeSettings = true, controls = {} }
    SB:SetTabRefreshCallback(dialog, self, systemFrame)
    local currentTab = SB:AddSettingsTabs(schema, dialog, { L.PLU_BOSS_TAB_LAYOUT }, L.PLU_BOSS_TAB_LAYOUT, self)
    if currentTab == L.PLU_BOSS_TAB_LAYOUT then
        table.insert(schema.controls, {
            type = "dropdown", key = "Orientation", label = L.PLU_GRP_ORIENTATION, default = 0,
            options = { { text = L.PLU_GRP_ORIENT_VERTICAL, value = 0 }, { text = L.PLU_GRP_ORIENT_HORIZONTAL, value = 1 } },
        })
        table.insert(schema.controls, { type = "slider", key = "Width", label = L.PLU_BOSS_WIDTH, min = 50, max = 400, step = 1, default = 150 })
        table.insert(schema.controls, { type = "slider", key = "Height", label = L.PLU_BOSS_HEIGHT, min = 20, max = 100, step = 1, default = 40 })
        table.insert(schema.controls, { type = "slider", key = "Spacing", label = L.PLU_BOSS_SPACING, min = 20, max = 100, step = 1, default = 40, formatter = function(v) return v .. "px" end })
    end
    OrbitEngine.Config:Render(dialog, systemFrame, self, schema)
end

-- [ LIFECYCLE ] -------------------------------------------------------------------------------------
function Plugin:OnLoad()
    HideNativeBossFrames()
    self.container = CreateFrame("Frame", "OrbitBossContainer", UIParent, "SecureHandlerStateTemplate")
    self.container:SetAttribute("_onstate-visibility", [[ if newstate == "hide" then self:Hide() else self:Show() end ]])
    self.container.editModeName, self.container.systemIndex = L.PLU_BOSS_FRAMES_NAME, 1
    self.container:SetFrameStrata(Orbit.Constants.Strata.HUD)
    self.container:SetFrameLevel(Orbit.StrataEngine:GetFrameLevel("Global_HUD", "Orbit_BossFrames") - 1)
    self.container:SetClampedToScreen(true)
    Pixel:Enforce(self.container)
    self.frames = {}
    for i = 1, MAX_BOSS_FRAMES do
        self.frames[i] = CreateBossFrame(i, self)
        self.frames[i]:SetParent(self.container)
        self.frames[i].orbitPlugin = self
        RegisterUnitWatch(self.frames[i])
        local bossIndex = i
        Orbit:SafeAction(function()
            if self.frames[bossIndex] and self.frames[bossIndex].CastBar then CB:SetupHooks(self.frames[bossIndex].CastBar, "boss" .. bossIndex) end
        end)
    end
    local pluginRef = self
    local firstFrame = self.frames[1]
    if OrbitEngine.ComponentDrag and firstFrame then
        for _, key in ipairs({ "Name", "HealthText" }) do
            local element = firstFrame[key]
            if element then OrbitEngine.ComponentDrag:Attach(element, self.container, { key = key, onPositionChange = OrbitEngine.ComponentDrag:MakePositionCallback(pluginRef, 1, key) }) end
        end
        if not firstFrame.debuffContainer then firstFrame.debuffContainer = CreateFrame("Frame", nil, firstFrame); firstFrame.debuffContainer:SetSize(DEFAULT_DEBUFF_ICON_SIZE, DEFAULT_DEBUFF_ICON_SIZE) end
        OrbitEngine.ComponentDrag:Attach(firstFrame.debuffContainer, self.container, { key = "Debuffs", isAuraContainer = true, onPositionChange = OrbitEngine.ComponentDrag:MakeAuraPositionCallback(pluginRef, 1, "Debuffs") })
    end
    self.frame = self.container
    self.frame.anchorOptions = { horizontal = false, vertical = false, noAnchor = true }
    self.frame.orbitResizeBounds = { minW = 50, maxW = 400, minH = 20, maxH = 100 }
    self.container.orbitCanvasFrame = self.frames[1]
    self.container.orbitCanvasTitle = L.PLU_BOSS_FRAME_NAME
    if OrbitEngine.ComponentDrag and firstFrame then
        if not firstFrame.buffContainer then firstFrame.buffContainer = CreateFrame("Frame", nil, firstFrame); firstFrame.buffContainer:SetSize(DEFAULT_BUFF_ICON_SIZE, DEFAULT_BUFF_ICON_SIZE) end
        OrbitEngine.ComponentDrag:Attach(firstFrame.buffContainer, self.container, { key = "Buffs", isAuraContainer = true, onPositionChange = OrbitEngine.ComponentDrag:MakeAuraPositionCallback(pluginRef, 1, "Buffs") })
        if firstFrame.MarkerIcon then OrbitEngine.ComponentDrag:Attach(firstFrame.MarkerIcon, self.container, { key = "MarkerIcon", onPositionChange = OrbitEngine.ComponentDrag:MakePositionCallback(pluginRef, 1, "MarkerIcon") }) end
        if firstFrame.CastBar then
            OrbitEngine.ComponentDrag:Attach(firstFrame.CastBar, self.container, {
                key = "CastBar",
                onPositionChange = function(comp, anchorX, anchorY, offsetX, offsetY, justifyH)
                    local compParent = comp:GetParent()
                    local posX, posY
                    if compParent then
                        local cx, cy = comp:GetCenter()
                        local px, py = compParent:GetCenter()
                        if cx and px then posX = cx - px end
                        if cy and py then posY = cy - py end
                    end
                    local posData = { anchorX = anchorX, anchorY = anchorY, offsetX = offsetX, offsetY = offsetY, justifyH = justifyH, posX = posX, posY = posY }
                    local Txn = OrbitEngine.CanvasMode and OrbitEngine.CanvasMode.Transaction
                    if Txn and Txn:IsActive() and Txn:GetPlugin() == pluginRef then
                        local positions = Txn:GetPositions()
                        posData.subComponents = positions.CastBar and positions.CastBar.subComponents
                        Txn:SetPosition("CastBar", posData)
                    else
                        local positions = pluginRef:GetComponentPositions(1)
                        posData.subComponents = positions.CastBar and positions.CastBar.subComponents
                        positions.CastBar = posData
                        pluginRef:SetSetting(1, "ComponentPositions", positions)
                    end
                end,
            })
        end
    end
    -- Replaces the previous Dialog.Open monkey-patch with the sanctioned hook API.
    local dialog = OrbitEngine.CanvasModeDialog or Orbit.CanvasModeDialog
    if dialog and dialog.RegisterOnBeforeOpen and not self.canvasModeHooked then
        self.canvasModeHooked = true
        dialog:RegisterOnBeforeOpen(function(frame)
            if frame == self.container or frame == self.frames[1] then self:PrepareIconsForCanvasMode() end
        end)
        dialog:RegisterOnAfterOpen(function(frame)
            if frame == self.container or frame == self.frames[1] then self:SchedulePreviewUpdate() end
        end)
    end
    OrbitEngine.Frame:AttachSettingsListener(self.frame, self, 1)
    if not self.container:GetPoint() then self.container:SetPoint("RIGHT", UIParent, "RIGHT", DEFAULT_BOSS_OFFSET_X, DEFAULT_BOSS_OFFSET_Y) end
    local BOSS_BASE_DRIVER = "[petbattle] hide; [@boss1,exists] show; [@boss2,exists] show; [@boss3,exists] show; [@boss4,exists] show; [@boss5,exists] show; hide"
    local function UpdateVisibilityDriver()
        if InCombatLockdown() or Orbit:IsEditMode() then return end
        RegisterStateDriver(self.container, "visibility", BOSS_BASE_DRIVER)
    end
    UpdateVisibilityDriver()
    self.container:Show()
    if Orbit.OOCFadeMixin then Orbit.OOCFadeMixin:ApplyOOCFade(self.container, self, 1) end
    self.container:SetSize(self:GetSetting(1, "Width") or 150, 100)
    self:PositionFrames()
    self:ApplySettings()
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT")
    eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
    eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    -- Per-boss-unit so the OnEvent path rebuilds just the changed frame, not all 5 on every zone-wide targetable change.
    eventFrame:RegisterUnitEvent("UNIT_TARGETABLE_CHANGED", "boss1", "boss2", "boss3", "boss4", "boss5")
    local function RebuildFrame(frame)
        if not frame.UpdateAll then return end
        frame:UpdateAll(); UpdatePowerBar(frame)
        frame._auraSnapshot = self:BuildAuraSnapshot(frame.unit)
        UpdateDebuffs(frame, self); UpdateBuffs(frame, self)
        frame._auraSnapshot = nil
    end
    eventFrame:SetScript("OnEvent", function(_, event, unit)
        if event == "UNIT_TARGETABLE_CHANGED" then
            local idx = unit and tonumber(unit:match("^boss(%d+)$"))
            local frame = idx and self.frames[idx]
            if frame then RebuildFrame(frame) end
        elseif event == "INSTANCE_ENCOUNTER_ENGAGE_UNIT" or event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" then
            for _, frame in ipairs(self.frames) do RebuildFrame(frame) end
        end
        if not InCombatLockdown() then self:UpdateContainerSize() end
    end)
    self.skipEditModeApply = true
    self:RegisterStandardEvents()
    if EventRegistry and not self.editModeCallbacksRegistered then
        self.editModeCallbacksRegistered = true
        EventRegistry:RegisterCallback("EditMode.Enter", function()
            if not InCombatLockdown() then UnregisterStateDriver(self.container, "visibility"); self.container:Show() end
            self:ShowPreview()
        end, self)
        EventRegistry:RegisterCallback("EditMode.Exit", function()
            self:HidePreview()
            if not InCombatLockdown() then UpdateVisibilityDriver(); self:UpdateContainerSize() end
        end, self)
    end
    if not InCombatLockdown() then self:UpdateContainerSize() end
end

-- [ CANVAS MODE PREP ] ------------------------------------------------------------------------------
function Plugin:PrepareIconsForCanvasMode()
    local frame = self.frames[1]
    if not frame then return end
    if frame.MarkerIcon then
        Orbit.StatusIconMixin:ApplyMarkerSprite(frame.MarkerIcon, 8)
        frame.MarkerIcon:Show()
    end
    if frame.CastBar then
        local castBarHeight = self:GetSetting(1, "CastBarHeight") or 18
        local castBarWidth = self:GetSetting(1, "CastBarWidth") or 120
        frame.CastBar:SetSize(castBarWidth + castBarHeight, castBarHeight)
        frame.CastBar.unit = "preview"
        if frame.CastBar.Icon then
            frame.CastBar.Icon:SetTexture(136116)
            frame.CastBar.Icon:Show()
        end
        if frame.CastBar.UpdateBarInsets then frame.CastBar:UpdateBarInsets() end
        local bar = frame.CastBar.Bar
        if bar then
            local textureName = self:GetSetting(1, "Texture") or self:GetPlayerSetting("Texture")
            local texturePath = textureName and LSM:Fetch("statusbar", textureName)
            if texturePath then bar:SetStatusBarTexture(texturePath) end
            bar:SetMinMaxValues(0, 2.0)
            bar:SetValue(1.2)
        end
        if frame.CastBar.Text then
            frame.CastBar.Text:ClearAllPoints()
            frame.CastBar.Text:SetPoint("LEFT", bar, "LEFT", 4, 0)
            frame.CastBar.Text:SetText(L.PLU_BOSS_ABILITY)
        end
        if frame.CastBar.Timer then frame.CastBar.Timer:SetText("1.5") end
        frame.CastBar:Show()
    end
end

-- [ POSITIONING ] -----------------------------------------------------------------------------------
function Plugin:PositionFrames()
    if not self.frames or not self.container then return end
    local spacing = Pixel:Snap(self:GetSetting(1, "Spacing") or 40, self.container:GetEffectiveScale())
    local horizontal = (self:GetSetting(1, "Orientation") or 0) == 1
    for i, frame in ipairs(self.frames) do
        frame:ClearAllPoints()
        if i == 1 then
            frame:SetPoint(horizontal and "LEFT" or "TOP", self.container, horizontal and "LEFT" or "TOP", 0, 0)
        elseif horizontal then
            frame:SetPoint("LEFT", self.frames[i - 1], "RIGHT", spacing, 0)
        else
            frame:SetPoint("TOP", self.frames[i - 1], "BOTTOM", 0, -spacing)
        end
    end
    self:UpdateContainerSize()
end

function Plugin:UpdateContainerSize()
    if not self.container or not self.frames then return end
    local width = self:GetSetting(1, "Width") or 150
    local scale = (self:GetSetting(1, "Scale") or 100) / 100
    local spacing = self:GetSetting(1, "Spacing") or 40
    local horizontal = (self:GetSetting(1, "Orientation") or 0) == 1
    local visibleCount = 0
    if self.isPreviewActive or Orbit:IsEditMode() then visibleCount = MAX_BOSS_FRAMES
    else for _, frame in ipairs(self.frames) do if frame:IsShown() then visibleCount = visibleCount + 1 end end end
    if visibleCount == 0 then visibleCount = MAX_BOSS_FRAMES end
    local frameHeight = self:GetSetting(1, "Height") or 40

    local extraWidth = 0
    local extraHeight = 0
    local castBarDisabled = self.IsComponentDisabled and self:IsComponentDisabled("CastBar")
    if not castBarDisabled then
        local cbWidth = self:GetSetting(1, "CastBarWidth") or 120
        local cbHeight = self:GetSetting(1, "CastBarHeight") or 18
        local totalCbWidth = cbWidth + cbHeight
        if totalCbWidth > width then extraWidth = totalCbWidth - width end

        local compPos = self:GetSetting(1, "ComponentPositions")
        local cbY = (compPos and compPos.CastBar and compPos.CastBar.posY) or -15
        if cbY < 0 then extraHeight = math.abs(cbY) + cbHeight / 2 end
    end

    if horizontal then
        self.container:SetSize(visibleCount * width + (visibleCount - 1) * spacing + extraWidth, frameHeight + extraHeight)
    else
        self.container:SetSize(width + extraWidth, visibleCount * frameHeight + (visibleCount - 1) * spacing + extraHeight)
    end
    self.container:SetScale(scale)
end

function Plugin:UpdateLayout()
    if not self.frames or InCombatLockdown() then return end
    local width = self:GetSetting(1, "Width") or 150
    local height = self:GetSetting(1, "Height") or 40
    local borderSize = self:GetSetting(1, "BorderSize") or self:GetPlayerSetting("BorderSize") or Orbit.Engine.Pixel:DefaultBorderSize(UIParent:GetEffectiveScale() or 1)
    for _, frame in ipairs(self.frames) do
        frame:SetSize(width, height)
        UpdateFrameLayout(frame, borderSize)
        local textSize = 12
        Orbit.Skin:ApplyUnitFrameText(frame.Name, "LEFT", nil, textSize)
        Orbit.Skin:ApplyUnitFrameText(frame.HealthText, "RIGHT", nil, textSize)
        if frame.ConstrainNameWidth then frame:ConstrainNameWidth() end
    end
    self:PositionFrames()
end

-- [ SETTINGS APPLICATION ] --------------------------------------------------------------------------
function Plugin:ApplySettings()
    if not self.frames or InCombatLockdown() then return end
    local scale = self:GetSetting(1, "Scale") or 100
    local width = self:GetSetting(1, "Width") or 150
    local height = self:GetSetting(1, "Height") or 40
    local castBarHeight = self:GetSetting(1, "CastBarHeight") or 14
    local castBarWidth = self:GetSetting(1, "CastBarWidth") or width
    local borderSize = self:GetSetting(1, "BorderSize") or self:GetPlayerSetting("BorderSize") or Orbit.Engine.Pixel:DefaultBorderSize(UIParent:GetEffectiveScale() or 1)
    local textureName = self:GetSetting(1, "Texture") or self:GetPlayerSetting("Texture")
    local fontName = self:GetSetting(1, "Font") or self:GetPlayerSetting("Font")
    local reactionColour = self:GetSetting(1, "ReactionColour")
    local textSize = 12
    -- Hoist plugin-level cast-bar config out of the per-frame loop — only `frame` varies per iteration.
    local castBarDisabled = self.IsComponentDisabled and self:IsComponentDisabled("CastBar")
    local componentPositions = self:GetComponentPositions(1)
    local castData = componentPositions.CastBar or {}
    local subComps = castData.subComponents or {}
    local cbTextSize = 10
    local fontPath = LSM:Fetch("font", fontName)
    local fontOutline = Orbit.Skin:GetFontOutline()
    local cbTextDisabled = self.IsComponentDisabled and self:IsComponentDisabled("CastBar.Text")
    local cbTimerDisabled = self.IsComponentDisabled and self:IsComponentDisabled("CastBar.Timer")
    local textSubPos = subComps.Text or { anchorX = "LEFT", anchorY = "CENTER", offsetX = 4, offsetY = 0, justifyH = "LEFT" }
    local timerSubPos = subComps.Timer or { anchorX = "RIGHT", anchorY = "CENTER", offsetX = 4, offsetY = 0, justifyH = "RIGHT" }
    local function ApplySubPos(element, anchorTo, subPos, defaultJustify)
        if not element or not subPos then return end
        element:ClearAllPoints()
        local aX = subPos.anchorX or defaultJustify
        local aY = subPos.anchorY or "CENTER"
        local oX = subPos.offsetX or 4
        local oY = subPos.offsetY or 0
        local jH = subPos.justifyH or defaultJustify
        local anchor = OrbitEngine.PositionUtils.BuildAnchorPoint(aX, aY)
        local selfAnchor = OrbitEngine.PositionUtils.BuildComponentSelfAnchor(true, false, aY, jH)
        local fX, fY = oX, oY
        if aX == "RIGHT" then fX = -fX end
        if aY == "TOP" then fY = -fY end
        local elemScale = element:GetEffectiveScale()
        fX, fY = Pixel:SnapPosition(fX, fY, selfAnchor, element:GetWidth() or 0, element:GetHeight() or 0, elemScale)
        element:SetPoint(selfAnchor, anchorTo, anchor, fX, fY)
        element:SetJustifyH(jH)
    end
    for _, frame in ipairs(self.frames) do
        Orbit.AuraMixin:InvalidateContainerLayout(frame)
        frame.borderSize = borderSize
        frame:SetSize(width, height)
        frame:SetScale(scale / 100)
        if frame.SetReactionColour then frame:SetReactionColour(reactionColour) end
        if frame.SetClassColour then frame:SetClassColour(not reactionColour) end
        frame:SetBorder(borderSize)
        UpdateFrameLayout(frame, borderSize)
        if frame.Health and textureName then Orbit.Skin:SkinStatusBar(frame.Health, textureName, nil, true) end
        if frame.TotalAbsorbBar then
            local absorbTextureName = Orbit.db.GlobalSettings and Orbit.db.GlobalSettings.AbsorbTexture
            Orbit.Skin:ApplyAbsorbTexture(frame.TotalAbsorbBar, absorbTextureName)
        end
        if frame.Power and textureName then Orbit.Skin:SkinStatusBar(frame.Power, textureName, nil, true); if frame.Power.bg then frame.Power.bg:SetColorTexture(0, 0, 0, 0.5) end end
        Orbit.Skin:ApplyUnitFrameText(frame.Name, "LEFT", nil, textSize)
        Orbit.Skin:ApplyUnitFrameText(frame.HealthText, "RIGHT", nil, textSize)
        if frame.ApplyComponentPositions then frame:ApplyComponentPositions() end
        if frame.CastBar then
            if castBarDisabled then frame.CastBar:Hide()
            else
                frame.CastBar:SetSize(castBarWidth + castBarHeight, castBarHeight)
                CB:Position(frame.CastBar, frame, self)
                if frame.CastBar.SetBorder then frame.CastBar:SetBorder(borderSize) end
                if frame.CastBar.Bar and textureName then Orbit.Skin:SkinStatusBar(frame.CastBar.Bar, textureName, nil, true) end
                if frame.CastBar.Text then frame.CastBar.Text:SetFont(fontPath, cbTextSize, fontOutline) end
                if frame.CastBar.Timer then frame.CastBar.Timer:SetFont(fontPath, cbTextSize, fontOutline) end
                local castBarAnchor = frame.CastBar.Bar or frame.CastBar
                ApplySubPos(frame.CastBar.Text, castBarAnchor, textSubPos, "LEFT")
                ApplySubPos(frame.CastBar.Timer, castBarAnchor, timerSubPos, "RIGHT")
                if frame.CastBar.Text then frame.CastBar.Text:SetShown(not cbTextDisabled) end
                if frame.CastBar.Timer then frame.CastBar.Timer:SetShown(not cbTimerDisabled) end
            end
        end
        frame:UpdateAll(); UpdatePowerBar(frame); UpdateDebuffs(frame, self); UpdateBuffs(frame, self); self:UpdateMarkerIcon(frame, self)
    end
    self:PositionFrames()
    OrbitEngine.Frame:RestorePosition(self.container, self, 1)
    if self.isPreviewActive then self:SchedulePreviewUpdate() end
end

