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

local Helpers = Orbit.GroupFrameHelpers
local Pixel = Orbit.Engine.Pixel

-- [ CONSTANTS ]--------------------------------------------------------------------------------------
local MAX_GROUP_FRAMES = Helpers.LAYOUT.MaxGroupFrames
local DEFAULT_WIDTH = Helpers.LAYOUT.DefaultWidth
local DEFAULT_HEIGHT = Helpers.LAYOUT.DefaultHeight
local AURA_BASE_ICON_SIZE = Helpers.LAYOUT.AuraBaseIconSize
local PARTY_DEFENSIVE_ICON_SIZE = 24
local PARTY_CC_ICON_SIZE = 24
local PARTY_PRIVATE_AURA_SIZE = 24
local PARTY_HEALER_AURA_SIZE = 16
local RAID_DEFENSIVE_ICON_SIZE = 18
local RAID_CC_ICON_SIZE = 18
local RAID_PRIVATE_AURA_SIZE = 18
local RAID_HEALER_AURA_SIZE = 12
local RAID_STATUS_ICON_SIZE = 18
local RAID_ROLE_ICON_SIZE = 12
local HealerReg = Orbit.HealerAuraRegistry

-- [ TIER DEFAULTS ]----------------------------------------------------------------------------------
local TIER_DEFAULTS = {
    Party = {
        Width = 160, Height = 40, Scale = 100, Spacing = 3, Orientation = 0,
        GrowthDirection = "down", IncludePlayer = true,
        ShowPowerBar = true, PowerBarHeight = 10,
        HealthTextMode = "percent_short",
        OutOfRangeOpacity = 30,
        ComponentPositions = {
            Name = { anchorX = "LEFT", offsetX = 5, anchorY = "CENTER", offsetY = 0, justifyH = "LEFT", selfAnchorY = "CENTER", posX = -75, posY = 0 },
            HealthText = { anchorX = "RIGHT", offsetX = 5, anchorY = "CENTER", offsetY = 0, justifyH = "RIGHT", selfAnchorY = "CENTER", posX = 75, posY = 0 },
            MarkerIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = 2, justifyH = "CENTER", selfAnchorY = "TOP", posX = 0, posY = 18 },
            RoleIcon = { anchorX = "RIGHT", offsetX = 5, anchorY = "TOP", offsetY = 5, justifyH = "RIGHT", selfAnchorY = "TOP", posX = 75, posY = 15 },
            LeaderIcon = { anchorX = "LEFT", offsetX = 10, anchorY = "TOP", offsetY = 0, justifyH = "LEFT", selfAnchorY = "TOP", posX = -70, posY = 20 },
            StatusIcons = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", selfAnchorY = "BOTTOM", posX = 0, posY = -10, overrides = { IconSize = 15 } },
            SummonIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", posX = 0, posY = -10 },
            PhaseIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", posX = 0, posY = -10 },
            ResIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", posX = 0, posY = -10 },
            ReadyCheckIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", posX = 0, posY = -10 },
            DefensiveIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", selfAnchorY = "CENTER", posX = 0, posY = 0, overrides = { IconSize = 34 } },
            CrowdControlIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = 2 },
            PrivateAuraAnchor = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", selfAnchorY = "CENTER", posX = 0, posY = 0 },
            Buffs = { anchorX = "LEFT", anchorY = "CENTER", offsetX = -2, offsetY = 0, justifyH = "RIGHT", selfAnchorY = "CENTER", posX = -110, posY = 0, overrides = { MaxIcons = 4, IconSize = 34, MaxRows = 2 } },
            Debuffs = { anchorX = "RIGHT", anchorY = "CENTER", offsetX = -2, offsetY = 0, justifyH = "LEFT", selfAnchorY = "CENTER", posX = 110, posY = 0, overrides = { MaxIcons = 4, IconSize = 34, MaxRows = 2 } },
            MainTankIcon = { anchorX = "LEFT", offsetX = 25, anchorY = "TOP", offsetY = 1, justifyH = "LEFT", selfAnchorY = "TOP", posX = -55, posY = 19, overrides = { Scale = 0.7 } },
            DispelIcon = { anchorX = "RIGHT", offsetX = 2, anchorY = "BOTTOM", offsetY = 2, justifyH = "RIGHT", selfAnchorY = "BOTTOM", posX = 65, posY = -10, overrides = { IconSize = 18 } },
        },
        DisabledComponents = (function()
            local d = { "CrowdControlIcon" }
            for _, k in ipairs(Orbit.HealerAuraRegistry:AllSlotKeys()) do d[#d + 1] = k end
            d[#d + 1] = "RaidBuff"
            d[#d + 1] = "Status"
            return d
        end)(),
        DisabledComponentsMigrated = true,
        DispelIndicatorEnabled = true, DispelGlowType = Orbit.Constants.Glow.Type.Pixel, DispelThickness = 2, DispelFrequency = 0.0, DispelNumLines = 4, DispelLength = 30, DispelBorder = true,
        DispelColorMagic = { r = 0.2, g = 0.6, b = 1.0, a = 1 },
        DispelColorCurse = { r = 0.6, g = 0.0, b = 1.0, a = 1 },
        DispelColorDisease = { r = 0.6, g = 0.4, b = 0.0, a = 1 },
        DispelColorPoison = { r = 0.0, g = 0.6, b = 0.0, a = 1 },
        ColorByAuraSpellId = 0, ColorByAuraColor = { r = 0.2, g = 0.8, b = 0.2, a = 1 },
        AggroIndicatorEnabled = true, AggroColor = { r = 1.0, g = 0.0, b = 0.0, a = 1 },
        SelectionColor = { r = 0.8, g = 0.9, b = 1.0, a = 1 },
        AggroThickness = 1, AggroFrequency = 0.25, AggroNumLines = 8,
    },
    Mythic = {
        Width = 100, Height = 40, Scale = 100, MemberSpacing = 2, GroupSpacing = 2,
        GroupsPerRow = 6, GrowthDirection = "down", SortMode = "group",
        Orientation = "horizontal", FlatRows = 1,
        ShowPowerBar = true, PowerBarHeight = 16, ShowGroupLabels = true,
        HealthTextFormat = "", HealthTextMode = "percent_short",
        OutOfRangeOpacity = 30,
        ComponentPositions = {
            Name = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = 10, justifyH = "CENTER", selfAnchorY = "TOP", posX = 0, posY = 10 },
            HealthText = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 10, justifyH = "CENTER", selfAnchorY = "BOTTOM", posX = 0, posY = -10, overrides = { FontSize = 10 } },
            MarkerIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = -1, justifyH = "CENTER", selfAnchorY = "TOP", posX = 0, posY = 21 },
            RoleIcon = { anchorX = "RIGHT", offsetX = 5, anchorY = "TOP", offsetY = 5, justifyH = "RIGHT", selfAnchorY = "TOP", posX = 45, posY = 15, overrides = { Scale = 0.7 } },
            LeaderIcon = { anchorX = "LEFT", offsetX = 8, anchorY = "TOP", offsetY = 0, justifyH = "LEFT", selfAnchorY = "TOP", posX = -42, posY = 20, overrides = { Scale = 0.8 } },
            MainTankIcon = { anchorX = "LEFT", offsetX = 20, anchorY = "TOP", offsetY = 0, justifyH = "LEFT", selfAnchorY = "TOP", posX = -30, posY = 20, overrides = { Scale = 0.6 } },
            StatusIcons = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", selfAnchorY = "CENTER", posX = 0, posY = 0 },
            SummonIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
            PhaseIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
            ResIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
            ReadyCheckIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
            DefensiveIcon = { anchorX = "LEFT", offsetX = 1, anchorY = "BOTTOM", offsetY = 1, justifyH = "LEFT", selfAnchorY = "BOTTOM", posX = -49, posY = -19, overrides = { IconSize = 22 } },
            CrowdControlIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = 2 },
            PrivateAuraAnchor = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", selfAnchorY = "CENTER", posX = 0, posY = 0, overrides = { IconSize = 20 } },
            Buffs = { anchorX = "RIGHT", anchorY = "BOTTOM", offsetX = 1, offsetY = 1, justifyH = "RIGHT", selfAnchorY = "BOTTOM", posX = 48, posY = -18, overrides = { MaxIcons = 5, IconSize = 18, MaxRows = 2 } },
            DispelIcon = { anchorX = "RIGHT", offsetX = 1, anchorY = "TOP", offsetY = 1, justifyH = "RIGHT", selfAnchorY = "TOP", posX = 38, posY = 15, overrides = { IconSize = 14 } },
        },
        DisabledComponents = (function()
            local d = { "CrowdControlIcon" }
            for _, k in ipairs(Orbit.HealerAuraRegistry:AllSlotKeys()) do d[#d + 1] = k end
            d[#d + 1] = "RaidBuff"
            d[#d + 1] = "Status"
            d[#d + 1] = "Debuffs"
            return d
        end)(),
        DisabledComponentsMigrated = true,
        AggroIndicatorEnabled = true, AggroColor = { r = 1.0, g = 0.0, b = 0.0, a = 1 },
        SelectionColor = { r = 0.8, g = 0.9, b = 1.0, a = 1 },
        AggroThickness = 1,
        DispelIndicatorEnabled = true, DispelGlowType = Orbit.Constants.Glow.Type.Pixel, DispelThickness = 2, DispelFrequency = 0.0, DispelNumLines = 4, DispelLength = 30, DispelBorder = true,
        DispelColorMagic = { r = 0.2, g = 0.6, b = 1.0, a = 1 },
        DispelColorCurse = { r = 0.6, g = 0.0, b = 1.0, a = 1 },
        DispelColorDisease = { r = 0.6, g = 0.4, b = 0.0, a = 1 },
        DispelColorPoison = { r = 0.0, g = 0.6, b = 0.0, a = 1 },
        ColorByAuraSpellId = 0, ColorByAuraColor = { r = 0.2, g = 0.8, b = 0.2, a = 1 },
    },
}
-- Heroic / World inherit from Mythic with size overrides
TIER_DEFAULTS.Heroic = setmetatable({ Width = 100, Height = 40, ShowPowerBar = false, PowerBarHeight = 8 }, { __index = TIER_DEFAULTS.Mythic })
TIER_DEFAULTS.World = setmetatable({
    Width = 65, Height = 30, GroupsPerRow = 4, ShowGroupLabels = false, ShowPowerBar = false, DispelOnlyByMe = true,
    ComponentPositions = {
        RoleIcon = { anchorX = "RIGHT", offsetX = 5, anchorY = "TOP", offsetY = 5, justifyH = "RIGHT", selfAnchorY = "TOP", posX = 31, posY = 9, overrides = { Scale = 0.7 } },
        LeaderIcon = { anchorX = "LEFT", offsetX = 8, anchorY = "TOP", offsetY = 0, justifyH = "LEFT", selfAnchorY = "TOP", posX = -28, posY = 14, overrides = { Scale = 0.8 } },
        MarkerIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = -1, justifyH = "CENTER", selfAnchorY = "TOP", posX = 0, posY = 15, overrides = { Scale = 0.6 } },
        SummonIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
        StatusIcons = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", selfAnchorY = "CENTER", posX = 0, posY = 0, overrides = { IconSize = 14 } },
        PrivateAuraAnchor = { anchorX = "LEFT", offsetX = 1, anchorY = "BOTTOM", offsetY = 1, justifyH = "LEFT", selfAnchorY = "BOTTOM", posX = -35, posY = -13, overrides = { IconSize = 14 } },
        Name = { anchorX = "CENTER", offsetX = 0, anchorY = "TOP", offsetY = 8, justifyH = "CENTER", selfAnchorY = "TOP", posX = 0, posY = 6 },
        ResIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
        PhaseIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
        HealthText = { anchorX = "CENTER", offsetX = 0, anchorY = "BOTTOM", offsetY = 7, justifyH = "CENTER", selfAnchorY = "BOTTOM", posX = 0, posY = -7, overrides = { FontSize = 8 } },
        ReadyCheckIcon = { anchorX = "CENTER", offsetX = 0, anchorY = "CENTER", offsetY = 0, justifyH = "CENTER", posX = 0, posY = 0 },
        MainTankIcon = { anchorX = "LEFT", offsetX = 20, anchorY = "TOP", offsetY = 0, justifyH = "LEFT", selfAnchorY = "TOP", posX = -16, posY = 14, overrides = { Scale = 0.6 } },
        DispelIcon = { anchorX = "RIGHT", offsetX = 1, anchorY = "TOP", offsetY = 1, justifyH = "RIGHT", selfAnchorY = "TOP", posX = 22, posY = 10, overrides = { IconSize = 10 } },
    },
    DisabledComponents = (function()
        local d = { "CrowdControlIcon" }
        for _, k in ipairs(Orbit.HealerAuraRegistry:AllSlotKeys()) do d[#d + 1] = k end
        d[#d + 1] = "RaidBuff"
        d[#d + 1] = "Status"
        d[#d + 1] = "Debuffs"
        d[#d + 1] = "DefensiveIcon"
        d[#d + 1] = "Buffs"
        return d
    end)(),
}, { __index = TIER_DEFAULTS.Mythic })

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

local Plugin = Orbit:RegisterPlugin("Group Frames", SYSTEM_ID, {
    displayName = L.PLG_NAME_GROUP_FRAMES,
    defaults = {
        Tiers = TIER_DEFAULTS,
        _EditTier = nil,
    },
})

Mixin(Plugin, Orbit.UnitFrameMixin, Orbit.GroupFramePreviewMixin, Orbit.AuraMixin,
    Orbit.DispelIndicatorMixin, Orbit.AggroIndicatorMixin, Orbit.StatusIconMixin,
    Orbit.GroupFrameFactoryMixin, Orbit.GroupFrameLayoutMixin)

Plugin.canvasMode = true
Plugin.supportsHealthText = true

-- [ TIER API ]---------------------------------------------------------------------------------------
function Plugin:GetCurrentTier()
    if self._editTierOverride then return self._editTierOverride end
    -- Cached: CheckTierChange refreshes _currentTier on every tier-affecting event (roster, PLAYER_ENTERING_WORLD, ZONE_CHANGED_NEW_AREA), so hot callers (GetTierSetting on the per-UNIT_AURA path) skip GetRealTier's 3 C calls.
    return self._currentTier or self:GetRealTier()
end

function Plugin:GetRealTier()
    local numMembers = GetNumGroupMembers()
    local isInRaid = IsInRaid()
    local _, _, difficultyID, _, maxPlayers = GetInstanceInfo()
    local instanceMax = (difficultyID and difficultyID > 0) and maxPlayers or nil
    return Helpers:GetTierForGroupSize(numMembers, isInRaid, instanceMax)
end

function Plugin:IsPartyTier(tier)
    return Helpers:IsPartyTier(tier or self:GetCurrentTier())
end

function Plugin:GetTierSetting(key, tier)
    tier = tier or self:GetCurrentTier()
    local tiers = self:GetSetting(1, "Tiers")
    if tiers and tiers[tier] and tiers[tier][key] ~= nil then return tiers[tier][key] end
    local defaults = TIER_DEFAULTS[tier]
    return defaults and defaults[key]
end

function Plugin:SetTierSetting(key, value, tier)
    tier = tier or self:GetCurrentTier()
    local tiers = self:GetSetting(1, "Tiers") or {}
    if not tiers[tier] then tiers[tier] = {} end
    tiers[tier][key] = value
    self:SetSetting(1, "Tiers", tiers)
end

function Plugin:GetDefaultComponentPositions()
    local defaults = TIER_DEFAULTS[self:GetCurrentTier()]
    return defaults and defaults.ComponentPositions
end

function Plugin:GetDefaultDisabledComponents()
    local defaults = TIER_DEFAULTS[self:GetCurrentTier()]
    return defaults and defaults.DisabledComponents
end

function Plugin:CopyTierSettings(sourceTier, destTier)
    local tiers = self:GetSetting(1, "Tiers") or {}
    local source = tiers[sourceTier] or TIER_DEFAULTS[sourceTier] or {}
    
    local existingDest = tiers[destTier] or {}
    local preservedPos = existingDest.Position and CopyTable(existingDest.Position) or nil

    tiers[destTier] = {}
    for k, v in pairs(source) do
        if type(v) == "table" then
            tiers[destTier][k] = CopyTable(v)
        else
            tiers[destTier][k] = v
        end
    end

    tiers[destTier].Position = preservedPos
    self:SetSetting(1, "Tiers", tiers)
end

local PluginMixin_GetSetting = Orbit.PluginMixin.GetSetting
local PluginMixin_SetSetting = Orbit.PluginMixin.SetSetting

-- Position/Anchor are tier-scoped though absent from TIER_DEFAULTS — Persistence's drag-time writes must land in Tiers[tier].
local TIER_KEYS = { ComponentPositions = true, DisabledComponents = true, Position = true, Anchor = true }
for _, defaults in pairs(TIER_DEFAULTS) do
    for k in pairs(defaults) do TIER_KEYS[k] = true end
end

function Plugin:GetSetting(systemIndex, key)
    if TIER_KEYS[key] then
        local txn = self._ActiveTransaction and self:_ActiveTransaction()
        if txn then
            local pending = txn:GetPending(key)
            if pending ~= nil then return pending end
        end
        local val = self:GetTierSetting(key)
        if val ~= nil then return val end
        if key == "ComponentPositions" or key == "DisabledComponents" then return {} end
        return nil
    end
    return PluginMixin_GetSetting(self, systemIndex, key)
end

function Plugin:SetSetting(systemIndex, key, val)
    if TIER_KEYS[key] then
        self:SetTierSetting(key, val)
        return
    end
    PluginMixin_SetSetting(self, systemIndex, key, val)
end

-- [ HELPERS ]----------------------------------------------------------------------------------------
local SafeRegisterUnitWatch = Orbit.GroupFrameMixin.SafeRegisterUnitWatch
local SafeUnregisterUnitWatch = Orbit.GroupFrameMixin.SafeUnregisterUnitWatch
local function GetPowerColor(powerType) return Orbit.Constants.Colors:GetPowerColor(powerType) end

local function GetDefensiveSize(plugin) return plugin:IsPartyTier() and PARTY_DEFENSIVE_ICON_SIZE or RAID_DEFENSIVE_ICON_SIZE end
local function GetCCSize(plugin) return plugin:IsPartyTier() and PARTY_CC_ICON_SIZE or RAID_CC_ICON_SIZE end
local function GetPrivateAuraSize(plugin) return plugin:IsPartyTier() and PARTY_PRIVATE_AURA_SIZE or RAID_PRIVATE_AURA_SIZE end
local function GetHealerAuraSize(plugin) return plugin:IsPartyTier() and PARTY_HEALER_AURA_SIZE or RAID_HEALER_AURA_SIZE end
local function GetComponentIconSize(plugin, key)
    local positions = plugin:GetTierSetting("ComponentPositions")
    local overrides = positions and positions[key] and positions[key].overrides
    return (overrides and overrides.IconSize) or GetHealerAuraSize(plugin)
end

-- [ POWER BAR UPDATE ]-------------------------------------------------------------------------------
local POWER_EVENTS = Orbit.GroupFrameFactoryMixin.POWER_EVENTS

local function UpdatePowerBar(frame, plugin)
    if not frame.Power or not frame.unit or not UnitExists(frame.unit) then return end
    local isParty = plugin:IsPartyTier()
    local showPower = plugin:GetTierSetting("ShowPowerBar")
    local isHealer = UnitGroupRolesAssigned(frame.unit) == "HEALER"
    local shouldShow = isParty and (showPower ~= false or isHealer) or (isHealer and showPower ~= false)
    if shouldShow then
        if not frame._powerEventsRegistered then
            for _, ev in ipairs(POWER_EVENTS) do frame:RegisterUnitEvent(ev, frame.unit) end
            frame._powerEventsRegistered = true
        end
        frame.Power:Show()
        local power, maxPower, powerType = UnitPower(frame.unit), UnitPowerMax(frame.unit), UnitPowerType(frame.unit)
        frame.Power:SetMinMaxValues(0, maxPower)
        frame.Power:SetValue(power)
        local color = GetPowerColor(powerType)
        frame.Power:SetStatusBarColor(color.r, color.g, color.b)
    else
        if frame._powerEventsRegistered then
            for _, ev in ipairs(POWER_EVENTS) do frame:UnregisterEvent(ev) end
            frame._powerEventsRegistered = false
        end
        frame.Power:Hide()
    end
end

local function UpdateFrameLayout(frame, borderSize, plugin, showPowerOverride)
    local showPower
    if showPowerOverride ~= nil then
        showPower = showPowerOverride
    else
        local showSetting = plugin and plugin:GetTierSetting("ShowPowerBar")
        if showSetting == nil then showSetting = true end
        local isHealer = frame.unit and UnitGroupRolesAssigned(frame.unit) == "HEALER"
        if plugin:IsPartyTier() then
            showPower = showSetting or isHealer
        else
            showPower = (showSetting and isHealer) or false
        end
    end
    local pct = plugin and plugin:GetTierSetting("PowerBarHeight")
    local ratio = pct and (pct / 100) or nil
    Helpers:UpdateFrameLayout(frame, borderSize, showPower, ratio)
end

-- [ AURA DISPLAY CONFIG ] ---------------------------------------------------------------------------
local Filters = Orbit.GroupAuraFilters
local GroupDebuffPostFilter = Filters:CreateDebuffFilter({
    raidFilterFn = function() return UnitAffectingCombat("player") and "HARMFUL|RAID_IN_COMBAT" or "HARMFUL" end,
})
local GroupBuffPostFilter = Filters:CreateBuffFilter()
local GROUP_SKIN = Orbit.Constants.Aura.SkinWithTimer

local GROUP_DEBUFF_CFG = {
    componentKey = "Debuffs", fetchFilter = "HARMFUL", fetchMax = 40,
    postFilter = GroupDebuffPostFilter, tooltipFilter = "HARMFUL",
    skinSettings = GROUP_SKIN, defaultAnchorX = "RIGHT", defaultJustifyH = "LEFT",
    helpers = function() return Orbit.GroupFrameHelpers end,
}
local GROUP_BUFF_CFG = {
    componentKey = "Buffs", fetchFilter = "HELPFUL|PLAYER", fetchMax = 40,
    postFilter = GroupBuffPostFilter, tooltipFilter = "HELPFUL",
    skinSettings = GROUP_SKIN, defaultAnchorX = "LEFT", defaultJustifyH = "RIGHT",
    helpers = function() return Orbit.GroupFrameHelpers end,
}

local function UpdateDebuffs(frame, plugin) plugin:UpdateAuraContainer(frame, plugin, "debuffContainer", "debuffPool", GROUP_DEBUFF_CFG) end
local function UpdateBuffs(frame, plugin) plugin:UpdateAuraContainer(frame, plugin, "buffContainer", "buffPool", GROUP_BUFF_CFG) end
local function UpdateDefensiveIcon(frame, plugin) plugin:UpdateDefensiveIcon(frame, plugin, GetDefensiveSize(plugin)) end
local function UpdateCrowdControlIcon(frame, plugin) plugin:UpdateCrowdControlIcon(frame, plugin, GetCCSize(plugin)) end
local function UpdateHealerAuras(frame, plugin)
    for _, slot in ipairs(HealerReg:ActiveSlots()) do
        plugin:UpdateSpellAuraIcon(frame, plugin, slot.key, slot.spellId, GetComponentIconSize(plugin, slot.key), slot.altSpellId)
    end
end
local function UpdateMissingRaidBuffs(frame, plugin)
    plugin:UpdateMissingRaidBuffs(frame, plugin, "RaidBuff", HealerReg:ActiveRaidBuffs(), GetComponentIconSize(plugin, "RaidBuff"))
end
local function UpdatePrivateAuras(frame, plugin) Orbit.PrivateAuraMixin:Update(frame, plugin, GetPrivateAuraSize(plugin)) end

local StatusDispatch = Orbit.GroupFrameMixin.StatusDispatch
local UpdateInRange = Orbit.GroupFrameMixin.UpdateInRange

local function SchedulePrivateAuraReanchor(plugin)
    if plugin._pendingPrivateAuraReanchor then return end
    plugin._pendingPrivateAuraReanchor = true
    C_Timer.After(0, function()
        plugin._pendingPrivateAuraReanchor = false
        if not plugin.frames then return end
        for _, frame in ipairs(plugin.frames) do
            if frame.unit and frame:IsShown() then UpdatePrivateAuras(frame, plugin) end
        end
    end)
end




-- [ DEBOUNCED ROSTER UPDATE ] -----------------------------------------------------------------------
local function ScheduleDebouncedRosterUpdate(plugin, updateVisibility)
    if updateVisibility then plugin._rosterNeedsVisibility = true end
    -- First event of a burst resizes synchronously so tier flips don't show a 1-frame stale layout.
    if not plugin._rosterUpdatePending and not InCombatLockdown() then
        plugin:CheckTierChange()
    end
    if plugin._rosterUpdatePending then return end
    plugin._rosterUpdatePending = true
    C_Timer.After(0, function()
        plugin._rosterUpdatePending = false
        local needsVis = plugin._rosterNeedsVisibility
        plugin._rosterNeedsVisibility = false
        if needsVis and plugin.UpdateVisibilityDriver then plugin.UpdateVisibilityDriver() end
        local oldTier = plugin._currentTier
        plugin:CheckTierChange()
        if plugin._currentTier ~= oldTier then
            Orbit.EventBus:Fire("ORBIT_GROUP_ROSTER_SETTLED")
            return
        end
        if not InCombatLockdown() then 
            plugin:UpdateFrameUnits() 
        else 
            -- In combat we can't add/remove secure frames, but WoW silently shifts unit tokens — force visible frames to pull the new player's Health/Name.
            if plugin.frames then
                for _, frame in ipairs(plugin.frames) do
                    if not frame.preview and frame.unit and frame:IsShown() then
                        -- Flush old GUID cache to force validation post-combat
                        frame._guidCache = nil 
                        if frame.UpdateAll then frame:UpdateAll() end
                        UpdateInRange(frame)
                    end
                end
            end
            SchedulePrivateAuraReanchor(plugin) 
        end
        Orbit.EventBus:Fire("ORBIT_GROUP_ROSTER_SETTLED")
    end)
end

-- [ SHARED EVENT CALLBACKS ]-------------------------------------------------------------------------
local SHARED_EVENT_CALLBACKS = {
    UpdatePowerBar = UpdatePowerBar, UpdateDebuffs = UpdateDebuffs, UpdateBuffs = UpdateBuffs,
    UpdateDefensiveIcon = UpdateDefensiveIcon, UpdateCrowdControlIcon = UpdateCrowdControlIcon,
    UpdatePrivateAuras = UpdatePrivateAuras, UpdateFrameLayout = UpdateFrameLayout,
    UpdateHealerAuras = UpdateHealerAuras, UpdateMissingRaidBuffs = UpdateMissingRaidBuffs,
    UpdateMainTankIcon = true,
}

-- [ GROUP FRAME CREATION ]---------------------------------------------------------------------------
local function CreateGroupFrame(index, plugin)
    local unit = "raid" .. index
    local frameName = "OrbitGroupFrame" .. index

    local frame = OrbitEngine.UnitButton:Create(plugin.container, unit, frameName, true)
    frame.editModeName = L.PLU_GROUP_FRAME_INDEX_F:format(index)
    frame.systemIndex = 1
    frame.groupIndex = index

    local width = plugin:GetTierSetting("Width") or DEFAULT_WIDTH
    local height = plugin:GetTierSetting("Height") or DEFAULT_HEIGHT
    frame:SetSize(width, height)
    frame:SetFrameStrata(Orbit.Constants.Strata.HUD)
    frame:SetFrameLevel(Orbit.StrataEngine:GetFrameLevel("Global_HUD", "Orbit_GroupFrames") + index)

    UpdateFrameLayout(frame, Orbit.db.GlobalSettings.BorderSize, plugin)

    frame.Power = plugin:CreatePowerBar(frame, unit)
    frame.debuffContainer = CreateFrame("Frame", nil, frame)
    frame.debuffContainer:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.Overlay)
    frame.buffContainer = CreateFrame("Frame", nil, frame)
    frame.buffContainer:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.Overlay)

    plugin:CreateStatusIcons(frame, plugin:IsPartyTier())

    local originalOnEvent = frame:GetScript("OnEvent")
    frame:SetScript("OnShow", Orbit.GroupFrameMixin.CreateOnShowHandler(plugin, SHARED_EVENT_CALLBACKS))
    frame:SetScript("OnEvent", Orbit.GroupFrameMixin.CreateEventHandler(plugin, SHARED_EVENT_CALLBACKS, originalOnEvent))

    plugin:ConfigureFrame(frame)
    frame:Hide()
    return frame
end

-- [ HIDE NATIVE FRAMES ]-----------------------------------------------------------------------------
local function HideNativeGroupFrames()
    UIParent:UnregisterEvent("GROUP_ROSTER_UPDATE")
    if CompactRaidFrameManager_SetSetting then
        CompactRaidFrameManager_SetSetting("IsShown", "0")
    end
    for i = 1, 4 do
        local f = _G["PartyMemberFrame" .. i]
        if f then OrbitEngine.NativeFrame:Park(f) end
    end
    OrbitEngine.NativeFrame:Park(PartyFrame)
    OrbitEngine.NativeFrame:Park(CompactPartyFrame)
    OrbitEngine.NativeFrame:Park(CompactRaidFrameContainer)
    if PartyFrame and PartyFrame.PartyMemberFramePool then
        for child in PartyFrame.PartyMemberFramePool:EnumerateActive() do
            OrbitEngine.NativeFrame:Park(child)
        end
    end
end

function Plugin:AddSettings(dialog, systemFrame)
    Orbit.GroupFrameSettings(self, dialog, systemFrame)
end

-- [ ON LOAD ]----------------------------------------------------------------------------------------
function Plugin:OnLoad()
    HideNativeGroupFrames()

    self._currentTier = self:GetCurrentTier()

    self.container = CreateFrame("Frame", "OrbitGroupFrameContainer", UIParent, "SecureHandlerStateTemplate")
    self.container:SetAttribute("_onstate-visibility", [[ if newstate == "hide" then self:Hide() else self:Show() end ]])
    self.container.editModeName = L.PLU_GROUP_FRAMES_NAME
    self.container.systemIndex = 1
    self.container:SetFrameStrata(Orbit.Constants.Strata.HUD)
    self.container:SetFrameLevel(Orbit.StrataEngine:GetFrameLevel("Global_HUD", "Orbit_GroupFrames") - 1)
    self.container:SetClampedToScreen(true)
    Pixel:Enforce(self.container)

    self.frames = {}
    for i = 1, MAX_GROUP_FRAMES do
        self.frames[i] = CreateGroupFrame(i, self)
        self.frames[i]:SetParent(self.container)
        self.frames[i].orbitPlugin = self
        self.frames[i]:Hide()
    end

    -- Centralized global event handler (replaces per-frame registration)
    self._globalEventFrame = Orbit.GroupFrameMixin.CreateGlobalEventHandler(self, SHARED_EVENT_CALLBACKS)

    -- Canvas Mode registration
    local firstFrame = self.frames[1]
    for _, k in ipairs(HealerReg:ActiveKeys()) do
        if k == "RaidBuff" then
            local raidBuffs = HealerReg:ActiveRaidBuffs()
            if #raidBuffs > 0 then self:EnsureRaidBuffContainer(firstFrame, k, raidBuffs, GetComponentIconSize(self, k)) end
        else
            self:EnsureAuraIcon(firstFrame, k, GetComponentIconSize(self, k))
        end
    end
    local iconKeys = { "RoleIcon", "LeaderIcon", "MainTankIcon", "MarkerIcon", "DefensiveIcon", "CrowdControlIcon", "PrivateAuraAnchor", "DispelIcon" }
    for _, k in ipairs(HealerReg:ActiveKeys()) do iconKeys[#iconKeys + 1] = k end
    Orbit.GroupCanvasRegistration:RegisterComponents(self, self.container, firstFrame,
        { "Name", "HealthText" }, iconKeys, AURA_BASE_ICON_SIZE)

    self.frame = self.container
    self.frame.anchorOptions = { horizontal = true, vertical = false }
    self.frame.orbitHeightSync = true
    self.frame.orbitResizeBounds = { minW = 50, maxW = 400, minH = 20, maxH = 100 }
    OrbitEngine.Frame:AttachSettingsListener(self.frame, self, 1)

    self.container.orbitCanvasFrame = self.frames[1]
    self.container.orbitCanvasTitle = L.PLU_GROUP_FRAME_TITLE_F:format(self:GetCurrentTier())

    self:RestoreTierPosition(self._currentTier)

    -- Visibility driver: unified — show when in any group
    local function UpdateVisibilityDriver()
        if InCombatLockdown() or Orbit:IsEditMode() then return end
        local _, instanceType = IsInInstance()
        local driver = "[petbattle] hide; [group] show; hide"
        if instanceType == "arena" or instanceType == "pvp" then
            driver = "[petbattle] hide; show"
        end
        RegisterStateDriver(self.container, "visibility", driver)
    end
    self.UpdateVisibilityDriver = function() UpdateVisibilityDriver() end
    UpdateVisibilityDriver()
    self.mountedConfig = { frame = self.container }
    if Orbit.OOCFadeMixin then Orbit.OOCFadeMixin:ApplyOOCFade(self.container, self, 1) end

    self.container:Show()
    self.container:SetSize(self:GetTierSetting("Width") or DEFAULT_WIDTH, DEFAULT_HEIGHT)

    self:PositionFrames()
    self:ApplySettings()
    self:UpdateFrameUnits()

    -- Global event frame
    local eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    eventFrame:RegisterEvent("PLAYER_ROLES_ASSIGNED")
    eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
    eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")

    eventFrame:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" then
            -- Nuclear option: wipe GUID cache on reload/zone to force full re-validate
            for _, f in ipairs(self.frames) do f._guidCache = nil end

            C_Timer.After(1, function()
                if InCombatLockdown() then return end
                UpdateVisibilityDriver()
                if not self:CheckTierChange() then
                    self:UpdateFrameUnits()
                end
                for _, frame in ipairs(self.frames) do
                    if not frame.preview and frame.unit and frame:IsShown() then
                        if frame.UpdateAll then frame:UpdateAll() end
                        UpdateInRange(frame)
                    end
                end
            end)
            return
        end
        if event == "GROUP_ROSTER_UPDATE" or event == "PLAYER_ROLES_ASSIGNED" then
            ScheduleDebouncedRosterUpdate(self, true)
            return
        end
        if event == "PLAYER_REGEN_ENABLED" then
            UpdateVisibilityDriver()
            if self._pendingTierApply then
                local oldTier = self._pendingTierApply
                self._pendingTierApply = nil
                self:SaveCurrentTierPosition(oldTier)
                self:UpdateFrameUnits()
                self:ApplySettings()
                self:RestoreTierPosition(self._currentTier)
            else
                if not self:CheckTierChange() then
                    self:UpdateFrameUnits()
                end
            end
            return
        end
        if event == "ZONE_CHANGED_NEW_AREA" then
            C_Timer.After(0.5, function()
                UpdateVisibilityDriver()
                if not self:CheckTierChange() and not InCombatLockdown() then
                    self:UpdateFrameUnits()
                    self:ApplySettings()
                end
            end)
        end
    end)

    self.skipEditModeApply = true
    self:RegisterStandardEvents()

    -- Unique string owner so these tier-switching handlers don't overwrite RegisterStandardEvents' (event, self) ApplySettings pair.
    if EventRegistry and not self.editModeCallbacksRegistered then
        self.editModeCallbacksRegistered = true
        local editModeOwner = "OrbitGroupFramesEditModePreview"
        EventRegistry:RegisterCallback("EditMode.Enter", function()
            if not InCombatLockdown() then
                local activeTier = self:GetRealTier() or "Party"
                self:SetSetting(1, "_EditTier", activeTier)
                self._editTierOverride = activeTier
                self._currentTier = activeTier
                self.container.orbitCanvasTitle = L.PLU_GROUP_FRAME_TITLE_F:format(activeTier)
                self:ApplySettings()
                UnregisterStateDriver(self.container, "visibility")
                self.container:Show()
                self:ShowPreview()
            end
        end, editModeOwner)
        EventRegistry:RegisterCallback("EditMode.Exit", function()
            if not InCombatLockdown() then
                self._undoSnapshot = nil
                self:SaveCurrentTierPosition()
                self:HidePreview()
                self._currentTier = self:GetRealTier()
                self:ApplySettings()
                UpdateVisibilityDriver()
                self:UpdateFrameUnits()
            end
        end, editModeOwner)
    end

    -- Canvas Mode dialog hook (sanctioned API; replaces the previous Dialog.Open monkey-patch).
    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
end

-- [ PER-TIER POSITION ]------------------------------------------------------------------------------
function Plugin:SaveCurrentTierPosition(tier)
    tier = tier or self:GetCurrentTier()
    if not self.container or not tier then return end
    local point, relativeTo, relativePoint, x, y = self.container:GetPoint()
    if not point then return end
    local relName = relativeTo and relativeTo.GetName and relativeTo:GetName() or "UIParent"
    local pm = OrbitEngine.PositionManager
    if pm then
        local eph = pm:GetPosition(self.container)
        if eph and eph.point then
            point, x, y = eph.point, eph.x, eph.y
            relName = eph.relativeTo or relName
            relativePoint = eph.relativePoint or relativePoint
        end
    end
    self:SetTierSetting("Position", { point = point, relativeTo = relName, relativePoint = relativePoint or point, x = x, y = y }, tier)
end

local TIER_DEFAULT_POSITIONS = {
    Party  = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 100,  y = -120 },
    Mythic = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 100,  y = -260 },
    Heroic = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 260,  y = -260 },
    World  = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 420,  y = -260 },
}

function Plugin:RestoreTierPosition(tier)
    tier = tier or self:GetCurrentTier()
    if not self.container or InCombatLockdown() then return end
    local pos = self:GetTierSetting("Position", tier)
    if not pos or not pos.point then
        local legacy = PluginMixin_GetSetting(self, 1, "Position")
        if legacy and legacy.point then
            pos = legacy
            self:SetTierSetting("Position", pos, tier)
        end
    end
    if not pos or not pos.point then
        pos = TIER_DEFAULT_POSITIONS[tier] or TIER_DEFAULT_POSITIONS.Party
        self:SetTierSetting("Position", pos, tier)
    end
    local x, y = Pixel:SnapPosition(pos.x, pos.y, pos.point, self.container:GetWidth(), self.container:GetHeight(), self.container:GetEffectiveScale())
    local relativeTo = (pos.relativeTo and _G[pos.relativeTo]) or UIParent
    self.container:ClearAllPoints()
    self.container:SetPoint(pos.point, relativeTo, pos.relativePoint or pos.point, x, y)
    if OrbitEngine.PositionManager then OrbitEngine.PositionManager:ClearFrame(self.container) end
end

-- [ TIER CHANGE DETECTION ]--------------------------------------------------------------------------
function Plugin:CheckTierChange()
    -- Compute the fresh real tier (not the cached GetCurrentTier) so change detection still works.
    local newTier = self._editTierOverride or self:GetRealTier()
    if newTier ~= self._currentTier then
        local oldTier = self._currentTier
        self._currentTier = newTier
        self.container.orbitCanvasTitle = L.PLU_GROUP_FRAME_TITLE_F:format(newTier)
        if not InCombatLockdown() then
            self._pendingTierApply = nil
            self:SaveCurrentTierPosition(oldTier)
            self:UpdateFrameUnits()
            self:ApplySettings()
            self:RestoreTierPosition(newTier)
        else
            self._pendingTierApply = oldTier
        end
        return true
    end
    return false
end

-- [ PREPARE ICONS FOR CANVAS MODE ]------------------------------------------------------------------
function Plugin:PrepareIconsForCanvasMode()
    local frame = self.frames[1]
    if not frame then return end
    local isParty = self:IsPartyTier()
    Orbit.GroupCanvasRegistration:PrepareIcons(self, frame, {
        statusIcons = { "PhaseIcon", "ReadyCheckIcon", "ResIcon", "SummonIcon" },
        statusIconSize = isParty and 24 or RAID_STATUS_ICON_SIZE,
        roleIcons = isParty and { "RoleIcon", "LeaderIcon" } or { "RoleIcon", "LeaderIcon", "MainTankIcon" },
        roleIconSize = isParty and 16 or RAID_ROLE_ICON_SIZE,
        defensiveSize = GetDefensiveSize(self),
        crowdControlSize = GetCCSize(self),
        privateAuraSize = GetPrivateAuraSize(self),
        healerAuraSize = GetHealerAuraSize(self),
        dispelIconSize = isParty and 24 or RAID_STATUS_ICON_SIZE,
    }, HealerReg:ActiveSlots(), HealerReg:ActiveRaidBuffs())
end


-- [ DYNAMIC UNIT ASSIGNMENT ] -----------------------------------------------------------------------
function Plugin:UpdateFrameUnits()
    if InCombatLockdown() then
        Orbit.CombatManager:QueueUpdate(function() self:UpdateFrameUnits() end)
        return
    end
    if self.frames and self.frames[1] and self.frames[1].preview then return end

    local isParty = self:IsPartyTier()
    local changed

    if isParty then
        changed = self:AssignPartyUnits()
    else
        changed = self:AssignRaidUnits()
    end

    if changed then
        self:PositionFrames()
        self:UpdateContainerSize()
    end
end

function Plugin:AssignPartyUnits()
    local includePlayer = self:GetTierSetting("IncludePlayer")
    local sortedUnits = Helpers:GetSortedPartyUnits(includePlayer)
    local tierWidth = self:GetTierSetting("Width") or DEFAULT_WIDTH
    local tierHeight = self:GetTierSetting("Height") or DEFAULT_HEIGHT
    local changed = false

    for i = 1, MAX_GROUP_FRAMES do
        local frame = self.frames[i]
        if frame then
            local unit = sortedUnits[i]
            if unit then
                local currentUnit = frame:GetAttribute("unit")
                -- issecretvalue BEFORE any nil/boolean test — UnitGUID is secret in combat.
                local newGuid = UnitGUID(unit)
                if issecretvalue(newGuid) then newGuid = nil end

                local guidChanged = newGuid and frame._guidCache ~= newGuid

                if currentUnit ~= unit or guidChanged then
                    SafeUnregisterUnitWatch(frame)
                    Orbit.AuraMixin:WipeCaches(frame)
                    if frame._colorByAuraOverride then
                        frame._colorByAuraOverride = nil
                        frame:ApplyHealthColor()
                    end
                    frame:SetAttribute("unit", unit)
                    frame.unit = unit
                    frame._guidCache = newGuid
                    self:UnregisterFrameEvents(frame)
                    self:RegisterUnitEvents(frame, unit)
                    UpdatePrivateAuras(frame, self)
                    frame:SetSize(tierWidth, tierHeight)
                    SafeRegisterUnitWatch(frame)
                    changed = true
                end
                if not frame:IsShown() then frame:Show(); changed = true end
                if frame.UpdateAll then frame:UpdateAll() end
                UpdatePowerBar(frame, self)
                UpdateInRange(frame)
            else
                local wasVisible = frame:IsShown() or frame.unit ~= nil
                SafeUnregisterUnitWatch(frame)
                self:UnregisterFrameEvents(frame)
                frame:SetAttribute("unit", nil)
                frame.unit = nil
                frame:Hide()
                if wasVisible then changed = true end
            end
        end
    end
    return changed
end

function Plugin:AssignRaidUnits()
    local sortMode = self:GetTierSetting("SortMode") or "group"
    local sortedUnits = Helpers:GetSortedRaidUnits(sortMode)
    local tierWidth = self:GetTierSetting("Width") or DEFAULT_WIDTH
    local tierHeight = self:GetTierSetting("Height") or DEFAULT_HEIGHT
    local changed = false

    for i = 1, MAX_GROUP_FRAMES do
        local frame = self.frames[i]
        if frame then
            local unitData = sortedUnits[i]
            if unitData then
                local token = unitData.token
                local currentUnit = frame:GetAttribute("unit")
                -- issecretvalue BEFORE any nil/boolean test — UnitGUID is secret in combat.
                local newGuid = token and UnitGUID(token)
                if issecretvalue(newGuid) then newGuid = nil end

                local guidChanged = newGuid and frame._guidCache ~= newGuid

                if currentUnit ~= token or guidChanged then
                    SafeUnregisterUnitWatch(frame)
                    Orbit.AuraMixin:WipeCaches(frame)
                    if frame._colorByAuraOverride then
                        frame._colorByAuraOverride = nil
                        frame:ApplyHealthColor()
                    end
                    frame:SetAttribute("unit", token)
                    frame.unit = token
                    frame._guidCache = newGuid
                    self:UnregisterFrameEvents(frame)
                    self:RegisterUnitEvents(frame, token)
                    UpdatePrivateAuras(frame, self)
                    frame:SetSize(tierWidth, tierHeight)
                    SafeRegisterUnitWatch(frame)
                    changed = true
                end
                if not frame:IsShown() then frame:Show(); changed = true end
                if frame.UpdateAll then frame:UpdateAll() end
                UpdatePowerBar(frame, self)
                UpdateInRange(frame)
            else
                local wasVisible = frame:IsShown() or frame.unit ~= nil
                SafeUnregisterUnitWatch(frame)
                self:UnregisterFrameEvents(frame)
                frame:SetAttribute("unit", nil)
                frame.unit = nil
                frame:Hide()
                if wasVisible then changed = true end
            end
        end
    end
    return changed
end

-- [ SETTINGS APPLICATION ]---------------------------------------------------------------------------
function Plugin:UpdateLayout(frame)
    if not frame or InCombatLockdown() then return end
    local width = self:GetTierSetting("Width") or DEFAULT_WIDTH
    local height = self:GetTierSetting("Height") or DEFAULT_HEIGHT
    for _, f in ipairs(self.frames) do
        f:SetSize(width, height)
        UpdateFrameLayout(f, self:GetSetting(1, "BorderSize"), self)
        self:UpdateTextSize(f)
    end
    self:PositionFrames()
    for _, f in ipairs(self.frames) do
        if f.ConstrainNameWidth then f:ConstrainNameWidth() end
    end
end

function Plugin:ApplyFrameStyle(frame, showPower)
    local width = self:GetTierSetting("Width") or DEFAULT_WIDTH
    local height = self:GetTierSetting("Height") or DEFAULT_HEIGHT
    local borderSize = self:GetSetting(1, "BorderSize") or Orbit.Engine.Pixel:DefaultBorderSize(UIParent:GetEffectiveScale() or 1)
    local textureName = self:GetSetting(1, "Texture")

    local isParty = self:IsPartyTier()
    local spacing = self:GetTierSetting("Spacing") or 0
    local mSpacing = self:GetTierSetting("MemberSpacing") or 2
    local gSpacing = self:GetTierSetting("GroupSpacing") or 2

    frame:SetSize(width, height)
    UpdateFrameLayout(frame, borderSize, self, showPower)
    if frame.SetBorder then frame:SetBorder(borderSize) end

    if frame.Health 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 then
        local texturePath = LSM:Fetch("statusbar", textureName) or "Interface\\TargetingFrame\\UI-StatusBar"
        frame.Power:SetStatusBarTexture(texturePath)
    end

    if showPower ~= nil and frame.Power then
        if showPower then frame.Power:Show() else frame.Power:Hide() end
    end

    self:ApplyTextStyling(frame)
    if frame.ApplyComponentPositions then frame:ApplyComponentPositions() end

    -- Reset icon base sizes to current tier (icons are created once at load; tier may change)
    local iconSize = isParty and 16 or 12
    local savedPositions = self:GetComponentPositions(1)
    local statusOverrides = savedPositions and savedPositions.StatusIcons and savedPositions.StatusIcons.overrides
    local customStatusSize = statusOverrides and statusOverrides.IconSize

    local centerIconSize = customStatusSize or (isParty and 24 or 18)
    for _, k in ipairs({ "RoleIcon", "LeaderIcon", "MainTankIcon", "MarkerIcon" }) do
        if frame[k] and frame[k].SetSize then frame[k]:SetSize(iconSize, iconSize); frame[k].orbitOriginalWidth, frame[k].orbitOriginalHeight = iconSize, iconSize end
    end
    for _, k in ipairs({ "PhaseIcon", "ReadyCheckIcon", "ResIcon", "SummonIcon", "DispelIcon" }) do
        if frame[k] and frame[k].SetSize then frame[k]:SetSize(centerIconSize, centerIconSize); frame[k].orbitOriginalWidth, frame[k].orbitOriginalHeight = centerIconSize, centerIconSize end
    end

    if savedPositions then
        local allIconKeys = { "RoleIcon", "LeaderIcon", "MainTankIcon", "StatusIcons", "PhaseIcon", "ReadyCheckIcon", "ResIcon", "SummonIcon", "MarkerIcon", "DefensiveIcon", "CrowdControlIcon", "PrivateAuraAnchor", "DispelIcon" }
        local activeKeys = HealerReg:ActiveKeys()
        for _, k in ipairs(activeKeys) do allIconKeys[#allIconKeys + 1] = k end
        for _, k in ipairs(activeKeys) do
            if savedPositions[k] then
                if k == "RaidBuff" then
                    if not frame.RaidBuff then
                        local sz = GetComponentIconSize(self, k)
                        local c = CreateFrame("Frame", nil, frame)
                        c:SetPoint("CENTER", frame, "CENTER", 0, 0)
                        c:SetFrameLevel(frame:GetFrameLevel() + Orbit.Constants.Levels.Overlay)
                        c._raidIcons = {}
                        c:SetSize(sz, sz)
                        frame.RaidBuff = c
                    end
                else
                    self:EnsureAuraIcon(frame, k, GetComponentIconSize(self, k))
                end
            end
        end
        Orbit.GroupCanvasRegistration:ApplyIconPositions({ frame }, savedPositions, allIconKeys)
    end
end

function Plugin:OnCanvasApply()
    self:ApplySettings()
    Orbit.GroupCanvasRegistration:OnCanvasApply(self)
end

function Plugin:ApplySettings()
    if not self.frames then return end
    if InCombatLockdown() then
        Orbit.CombatManager:QueueUpdate(function() self:ApplySettings() end)
        return
    end
    self._auraComponentsActive = nil
    self._dispelSettingsCache = nil
    self._aggroSettingsCache = nil

    -- Color-by-aura is off for virtually all users; cache whether ANY tier configures it so UpdateColorByAura can bail O(1) per UNIT_AURA instead of resolving tier settings.
    self._anyColorByAura = false
    local tiers = self:GetSetting(1, "Tiers")
    if tiers then
        for _, t in pairs(tiers) do
            if t.ColorByAuraSpellId and t.ColorByAuraSpellId ~= 0 then self._anyColorByAura = true; break end
        end
    end

    local tierWidth = self:GetTierSetting("Width") or DEFAULT_WIDTH
    local tierHeight = self:GetTierSetting("Height") or DEFAULT_HEIGHT
    local borderSize = self:GetSetting(1, "BorderSize")
    for _, frame in ipairs(self.frames) do
        if not frame.preview and frame.unit then
            Orbit.AuraMixin:InvalidateContainerLayout(frame)
            frame:SetSize(tierWidth, tierHeight)
            UpdateFrameLayout(frame, borderSize, self)
            Orbit:SafeAction(function() self:ApplyFrameStyle(frame) end)

            local healthTextMode = self:GetTierSetting("HealthTextMode") or "percent_short"
            if frame.SetHealthTextMode then frame:SetHealthTextMode(healthTextMode) end
            local healthTextFormat = self:GetTierSetting("HealthTextFormat")
            if frame.SetHealthTextFormat then frame:SetHealthTextFormat(healthTextFormat) end
            frame.healthTextEnabled = true
            if frame.UpdateHealthText then frame:UpdateHealthText() end
            StatusDispatch(frame, self, "UpdateStatusText")
            if frame.SetClassColour then frame:SetClassColour(true) end
            UpdatePowerBar(frame, self)
            UpdateDebuffs(frame, self)
            UpdateBuffs(frame, self)
            UpdateDefensiveIcon(frame, self)
            UpdateCrowdControlIcon(frame, self)
            UpdateHealerAuras(frame, self)
            UpdateMissingRaidBuffs(frame, self)
            UpdatePrivateAuras(frame, self)
            self:UpdateColorByAura(frame)
            StatusDispatch(frame, self, "UpdateAllPartyStatusIcons")
            Orbit.DispelIndicatorMixin:UpdateDispelIndicator(frame, self)
            if frame.UpdateAll then frame:UpdateAll() end
        end
    end

    self:PositionFrames()

    if self.frames[1] and self.frames[1].preview then
        self:SchedulePreviewUpdate()
    end
end

-- [ COLOR BY AURA ]----------------------------------------------------------------------------------
local function FindSpecSpellAltId(spellId)
    for _, spell in ipairs(HealerReg:GetCurrentSpecSpells()) do
        if spell.spellId == spellId then return spell.altSpellId end
    end
end

local function IsTrackedAuraPresent(snapshot, unit, spellId, altSpellId)
    -- O(1) against the per-event snapshot's spellId map (built once, any caster); same secret-skip semantics as the scan below.
    local m = snapshot and snapshot.helpfulBySpell
    if m then return m[spellId] ~= nil or (altSpellId ~= nil and m[altSpellId] ~= nil) end
    -- Fallback only when no snapshot is in scope (e.g. forced refresh / preview).
    local found = false
    AuraUtil.ForEachAura(unit, "HELPFUL", 40, function(aura)
        local sid = aura.spellId
        if not issecretvalue(sid) and (sid == spellId or (altSpellId and sid == altSpellId)) then
            found = true
            return true
        end
    end, true)
    return found
end

function Plugin:UpdateColorByAura(frame)
    if not frame or not frame.Health or not frame.unit then return end
    -- _anyColorByAura (cached on settings-change) lets the default/most users skip the whole tier+aura resolution on every UNIT_AURA.
    local spellId = self._anyColorByAura and self:GetTierSetting("ColorByAuraSpellId")
    local active = spellId and spellId ~= 0 and UnitExists(frame.unit)
        and IsTrackedAuraPresent(frame._auraSnapshot, frame.unit, spellId, FindSpecSpellAltId(spellId))
    if active then
        frame._colorByAuraOverride = self:GetTierSetting("ColorByAuraColor")
        frame:ApplyHealthColor()
    elseif frame._colorByAuraOverride ~= nil then
        frame._colorByAuraOverride = nil
        frame:ApplyHealthColor()
    end
end

function Plugin:UpdateAllColorByAura()
    if not self.frames then return end
    for _, frame in ipairs(self.frames) do
        if not frame.preview and frame.unit and frame:IsShown() then
            self:UpdateColorByAura(frame)
        end
    end
end
