local addonName, NS = ...

-- ============================================================================
-- VAULTSKINS: FRAME EFFECTS
-- ============================================================================
local Skin = NS.Skin
local Shared = NS.SkinShared
local GetEffectiveThemeStyle = Shared.GetEffectiveThemeStyle
local GetThemeQualityProfile = Shared.GetThemeQualityProfile
local RecordStabilityMetric = Shared.RecordStabilityMetric
local CreatePulseAnimation = Shared.CreatePulseAnimation
local PlayVaultSound = Shared.PlayVaultSound
local SafeCall = Shared.SafeCall
local ScheduleOwnedTimer = Shared.ScheduleOwnedTimer
local ScheduleVaultSound = Shared.ScheduleVaultSound
local SetColor = Shared.SetColor
local SetVertexColor = Shared.SetVertexColor
local CUSTOM_JACKPOT_SOUND_FILES = Shared.CUSTOM_JACKPOT_SOUND_FILES

-- ============================ ANIMATION HELPERS =============================
local function StopGroup(group)
    if group and group.IsPlaying and group:IsPlaying() then group:Stop() end
end

local function ConfigureIdleAlpha(region, minimum, maximum, duration, startDelay)
    if not region then return end
    local group = region.VaultSkinsFrameIdle
    local created = false
    if not group then
        group = region:CreateAnimationGroup()
        created = true
        region.VaultSkinsFrameIdle = group
        group:SetLooping("BOUNCE")
        group.Alpha = group:CreateAnimation("Alpha")
        group.Alpha:SetOrder(1)
        group:SetScript("OnPlay", function()
            region:Show()
            region:SetAlpha(group.Minimum or 0)
        end)
        group:SetScript("OnStop", function()
            region:SetAlpha(group.RestoreAlpha or 0)
            if group.HideOnStop then region:Hide() else region:Show() end
        end)
    end
    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    if group.IsPlaying and group:IsPlaying() then group:Stop() end
    group.Minimum = minimum or 0
    group.Maximum = maximum or 0.3
    group.RestoreAlpha = minimum or 0
    group.HideOnStop = false
    group.Alpha:SetFromAlpha(group.Minimum)
    group.Alpha:SetToAlpha(group.Maximum)
    group.Alpha:SetDuration(duration or 2.4)
    if group.Alpha.SetStartDelay then group.Alpha:SetStartDelay(startDelay or 0) end
    return group
end

local function ConfigureFrameTrace(texture, frame, side, looping, peakAlpha, travelDuration, pauseDuration)
    if not texture or not frame then return end
    local group = texture.VaultSkinsFrameTrace
    local created = false
    if not group then
        group = texture:CreateAnimationGroup()
        created = true
        texture.VaultSkinsFrameTrace = group
        group.FadeIn = group:CreateAnimation("Alpha")
        group.FadeIn:SetOrder(1)
        group.Move = group:CreateAnimation("Translation")
        group.Move:SetOrder(2)
        group.FadeOut = group:CreateAnimation("Alpha")
        group.FadeOut:SetOrder(3)
        group.Pause = group:CreateAnimation("Alpha")
        group.Pause:SetOrder(4)
        group:SetScript("OnPlay", function()
            texture:Show()
            texture:SetAlpha(0)
        end)
        local function Finish()
            texture:SetAlpha(0)
            if not group.IsLoopingTrace then texture:Hide() end
        end
        group:SetScript("OnFinished", Finish)
        group:SetScript("OnStop", function()
            texture:SetAlpha(0)
            texture:Hide()
        end)
    end

    RecordStabilityMetric(created and "animationGroupsCreated" or "animationGroupsReused")
    StopGroup(group)
    texture:ClearAllPoints()
    local width, height = math.max(640, frame:GetWidth() or 0), math.max(420, frame:GetHeight() or 0)
    local offsetX, offsetY = 0, 0
    if side == "top" then
        texture:SetPoint("TOPLEFT", frame, "TOPLEFT", 36, -22)
        offsetX = math.max(220, width - 242)
    elseif side == "bottom" then
        texture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -36, 24)
        offsetX = -math.max(220, width - 242)
    elseif side == "left" then
        texture:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -68)
        offsetY = -math.max(180, height - 214)
    else
        texture:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -2, -68)
        offsetY = -math.max(180, height - 214)
    end

    group.IsLoopingTrace = looping and true or false
    group:SetLooping(looping and "REPEAT" or "NONE")
    group.FadeIn:SetFromAlpha(0)
    group.FadeIn:SetToAlpha(peakAlpha or 0.55)
    group.FadeIn:SetDuration(0.10)
    group.Move:SetOffset(offsetX, offsetY)
    group.Move:SetDuration(travelDuration or 0.95)
    group.Move:SetSmoothing("IN_OUT")
    group.FadeOut:SetFromAlpha(peakAlpha or 0.55)
    group.FadeOut:SetToAlpha(0)
    group.FadeOut:SetDuration(0.16)
    group.Pause:SetFromAlpha(0)
    group.Pause:SetToAlpha(0)
    group.Pause:SetDuration(looping and (pauseDuration or 4.0) or 0.01)
    return group
end


local function ClampFrameValue(value, minimum, maximum)
    value = tonumber(value) or minimum
    if value < minimum then return minimum end
    if value > maximum then return maximum end
    return value
end

local function GetFrameEffectProfile(theme)
    return GetEffectiveThemeStyle(theme, "frameEffectProfile")
end

local function GetFrameEffectColor(theme, profile, key, fallbackKey)
    local value = profile and profile[key]
    if type(value) == "string" and theme and type(theme[value]) == "table" then
        return theme[value]
    end
    if type(value) == "table" and value.r ~= nil then
        return value
    end
    if theme and type(theme[fallbackKey]) == "table" then
        return theme[fallbackKey]
    end
    return { r = 1, g = 1, b = 1, a = 1 }
end

local function FrameControlEnabled(key)
    return not VaultSkinsDB or VaultSkinsDB[key] ~= false
end

local function GetUserFrameIntensity()
    return ClampFrameValue(VaultSkinsDB and VaultSkinsDB.frameIntensity or 1, 0.40, 1.40)
end

local function GetUserFrameSpeedScale()
    local speed = VaultSkinsDB and VaultSkinsDB.frameEffectSpeed or "normal"
    return ({ fast = 0.72, normal = 1.00, cinematic = 1.32 })[speed] or 1.00
end

-- =========================== IDLE EFFECT PROFILES ===========================
local IDLE_PATTERN_PROFILES = {
    breathe = { outlineMin = 0.18, outlineMax = 0.56, outlineDuration = 2.15, topDuration = 4.8, sideDuration = 5.4, crestDuration = 4.6, dividerDuration = 5.2, cornerDuration = 5.0, particleDuration = 1.00, traceSpeed = 1.00, tracePause = 1.00 },
    wave = { outlineMin = 0.13, outlineMax = 0.50, outlineDuration = 2.55, topDuration = 5.2, sideDuration = 5.8, crestDuration = 5.0, dividerDuration = 5.6, cornerDuration = 5.4, particleDuration = 1.05, traceSpeed = 1.05, tracePause = 0.92, leftDelay = 0.18, rightDelay = 0.58, cornerDelayStep = 0.14 },
    heartbeat = { outlineMin = 0.10, outlineMax = 0.62, outlineDuration = 1.18, topDuration = 3.4, sideDuration = 3.8, crestDuration = 2.8, dividerDuration = 3.2, cornerDuration = 3.6, particleDuration = 0.82, traceSpeed = 0.86, tracePause = 0.72, leftDelay = 0.04, rightDelay = 0.12, cornerDelayStep = 0.04 },
    radiant = { outlineMin = 0.16, outlineMax = 0.66, outlineDuration = 3.10, topDuration = 5.8, sideDuration = 6.4, crestDuration = 5.4, dividerDuration = 5.8, cornerDuration = 5.9, particleDuration = 1.12, traceSpeed = 1.10, tracePause = 1.12, leftDelay = 0.20, rightDelay = 0.42, cornerDelayStep = 0.10 },
    shimmer = { outlineMin = 0.14, outlineMax = 0.54, outlineDuration = 1.72, topDuration = 4.1, sideDuration = 4.4, crestDuration = 3.8, dividerDuration = 4.0, cornerDuration = 4.2, particleDuration = 0.72, traceSpeed = 0.78, tracePause = 0.62, leftDelay = 0.10, rightDelay = 0.32, cornerDelayStep = 0.08 },
    slow = { outlineMin = 0.10, outlineMax = 0.40, outlineDuration = 4.20, topDuration = 6.8, sideDuration = 7.4, crestDuration = 6.5, dividerDuration = 6.9, cornerDuration = 7.0, particleDuration = 1.25, traceSpeed = 1.22, tracePause = 1.28, leftDelay = 0.28, rightDelay = 0.72, cornerDelayStep = 0.18 },
    steady = { outlineMin = 0.18, outlineMax = 0.32, outlineDuration = 5.20, topDuration = 7.2, sideDuration = 7.8, crestDuration = 7.0, dividerDuration = 7.4, cornerDuration = 7.6, particleDuration = 1.35, traceSpeed = 1.30, tracePause = 1.35 },
}

local function GetIdlePatternProfile(theme)
    local quality = GetThemeQualityProfile(theme)
    return IDLE_PATTERN_PROFILES[quality.idlePattern or "breathe"] or IDLE_PATTERN_PROFILES.breathe
end

local function UsesFrameParticle(profile, index, count)
    if profile.particles == false then return false end
    if type(profile.particleIndices) == "table" and #profile.particleIndices > 0 then
        for _, allowed in ipairs(profile.particleIndices) do
            if allowed == index then return true end
        end
        return false
    end
    local density = ClampFrameValue(profile.particleDensity or 1, 0, 1)
    return index <= math.max(0, math.ceil((count or 0) * density))
end

-- ============================ IDLE FRAME EFFECTS ============================
function Skin:StopFrameIdleAnimation()
    self.frameIdleToken = (self.frameIdleToken or 0) + 1
    local frame = WeeklyRewardsFrame
    if not frame or not frame.VaultSkins then return end
    local main = frame.VaultSkins
    local regions = { main.OpeningGlow, main.TopGlow, main.SideGlowLeft, main.SideGlowRight, main.CrestGlow, main.HeaderDividerGlow }
    for _, corner in ipairs(main.Corners or {}) do
        regions[#regions + 1] = corner.Horizontal
        regions[#regions + 1] = corner.Vertical
    end
    for _, region in ipairs(regions) do
        if region then StopGroup(region.VaultSkinsFrameIdle) end
    end
    for _, trace in ipairs({ main.FrameTraceTop, main.FrameTraceLeft, main.FrameTraceRight, main.FrameTraceBottom }) do
        if trace then StopGroup(trace.VaultSkinsFrameTrace); trace:SetAlpha(0); trace:Hide() end
    end
    for _, sparkle in ipairs(main.FrameSparkles or {}) do
        StopGroup(sparkle.VaultSkinsFrameIdle)
        StopGroup(sparkle.VaultSkinsPulse)
        sparkle:SetAlpha(0)
        sparkle:Hide()
    end

    local theme = self:GetTheme()
    local style = GetEffectiveThemeStyle(theme, "frameStyle")
    if main.OpeningGlow then main.OpeningGlow:SetAlpha(0); main.OpeningGlow:Hide() end
    if main.TopGlow then main.TopGlow:SetAlpha(style.topGlowAlpha or 0.38) end
    if main.SideGlowLeft then main.SideGlowLeft:SetAlpha(style.sideGlowAlpha or 0.16) end
    if main.SideGlowRight then main.SideGlowRight:SetAlpha(style.sideGlowAlpha or 0.16) end
    if main.CrestGlow then main.CrestGlow:SetAlpha(0); main.CrestGlow:Hide() end
    if main.HeaderDividerGlow then main.HeaderDividerGlow:SetAlpha(0); main.HeaderDividerGlow:Hide() end
    for _, corner in ipairs(main.Corners or {}) do
        corner.Horizontal:SetAlpha(style.cornerAlpha or 0.62)
        corner.Vertical:SetAlpha(style.cornerAlpha or 0.62)
    end
end

function Skin:ApplyStaticFrameAppearance()
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() or not self:IsEnabled() then return end

    self:StopFrameIdleAnimation()

    local main = self:CreateMainSkin(frame)
    local theme = self:GetTheme()
    local style = GetEffectiveThemeStyle(theme, "frameStyle")
    local intensity = GetUserFrameIntensity()
    local outerGlowColor = theme.outerGlow or theme.frameAccent or theme.accent

    main.OpeningGlow:SetBackdropBorderColor(outerGlowColor.r, outerGlowColor.g, outerGlowColor.b, 1)
    main.OpeningGlow:SetAlpha(ClampFrameValue((style.staticOuterGlowAlpha or 0.24) * intensity, 0.10, 0.46))
    main.OpeningGlow:Show()

    if main.TopGlow then
        main.TopGlow:SetAlpha(ClampFrameValue((style.topGlowAlpha or 0.38) * 0.78, 0.08, 0.42))
        main.TopGlow:Show()
    end
    if main.SideGlowLeft then
        main.SideGlowLeft:SetAlpha(ClampFrameValue((style.sideGlowAlpha or 0.16) * 0.78, 0.04, 0.24))
        main.SideGlowLeft:Show()
    end
    if main.SideGlowRight then
        main.SideGlowRight:SetAlpha(ClampFrameValue((style.sideGlowAlpha or 0.16) * 0.78, 0.04, 0.24))
        main.SideGlowRight:Show()
    end
    for _, corner in ipairs(main.Corners or {}) do
        if corner.Horizontal then corner.Horizontal:Show() end
        if corner.Vertical then corner.Vertical:Show() end
    end
end

function Skin:StartFrameIdleAnimation()
    if self.sessionDisabledFeatures and self.sessionDisabledFeatures.frameEffects then return end
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() or not self:IsEnabled() or self.revealActive then return end
    local mode = VaultSkinsDB and VaultSkinsDB.frameAnimationMode or "full"
    if mode == "off" then
        self:ApplyStaticFrameAppearance()
        return
    end
    if not FrameControlEnabled("frameIdleAnimation") then
        self:StopFrameIdleAnimation()
        return
    end

    self:StopFrameIdleAnimation()
    local main = self:CreateMainSkin(frame)
    local theme = self:GetTheme()
    local style = GetEffectiveThemeStyle(theme, "frameStyle")
    local effect = GetFrameEffectProfile(theme)
    local full = mode == "full"
    local intensity = GetUserFrameIntensity()
    local idleSpeed = ClampFrameValue(effect.idleSpeed or 1, 0.55, 1.80) * GetUserFrameSpeedScale()
    local idle = GetIdlePatternProfile(theme)
    local crestEnabled = FrameControlEnabled("frameCrestEffects")
    local borderEnabled = FrameControlEnabled("frameBorderFlow")
    local cornersEnabled = FrameControlEnabled("frameCornerEffects")
    local particlesEnabled = FrameControlEnabled("frameParticles")

    local topBase = style.topGlowAlpha or 0.38
    local sideBase = style.sideGlowAlpha or 0.16
    local cornerBase = style.cornerAlpha or 0.62

    local crestColor = GetFrameEffectColor(theme, effect, "crestColor", "accent")
    local dividerColor = GetFrameEffectColor(theme, effect, "dividerColor", "glow")
    local topColor = GetFrameEffectColor(theme, effect, "topColor", "glow")
    local sideColor = GetFrameEffectColor(theme, effect, "sideColor", "glow")
    local cornerColor = GetFrameEffectColor(theme, effect, "cornerColor", "metal")

    SetVertexColor(main.TopGlow, topColor)
    SetVertexColor(main.SideGlowLeft, sideColor)
    SetVertexColor(main.SideGlowRight, sideColor)
    main.CrestGlow:SetBackdropBorderColor(crestColor.r, crestColor.g, crestColor.b, 1)
    SetVertexColor(main.HeaderDividerGlow, dividerColor)
    local outerGlowColor = theme.outerGlow or theme.frameAccent or theme.accent
    main.OpeningGlow:SetBackdropBorderColor(outerGlowColor.r, outerGlowColor.g, outerGlowColor.b, 1)

    if borderEnabled then
        local subtleScale = full and 1 or 0.62
        local outlineMin = idle.outlineMin * subtleScale * intensity
        local outlineMax = idle.outlineMax * subtleScale * intensity
        local outlineDuration = idle.outlineDuration * (full and 1 or 1.28) * idleSpeed
        local outlineGroup = ConfigureIdleAlpha(main.OpeningGlow, outlineMin, outlineMax, outlineDuration, idle.outlineDelay or 0)
        if outlineGroup then
            outlineGroup.RestoreAlpha = 0
            outlineGroup.HideOnStop = true
            outlineGroup:Play()
        end
        ConfigureIdleAlpha(main.TopGlow, math.max(0.05, topBase * 0.68), math.min(0.90, topBase + ((full and 0.12 or 0.06) * intensity)), idle.topDuration * (full and 1 or 1.24) * idleSpeed, idle.topDelay or 0):Play()
        if borderEnabled and effect.sides ~= false then
            ConfigureIdleAlpha(main.SideGlowLeft, math.max(0.025, sideBase * 0.58), math.min(0.62, sideBase + ((full and 0.09 or 0.04) * intensity)), idle.sideDuration * (full and 1 or 1.22) * idleSpeed, idle.leftDelay or 0):Play()
            ConfigureIdleAlpha(main.SideGlowRight, math.max(0.025, sideBase * 0.58), math.min(0.62, sideBase + ((full and 0.09 or 0.04) * intensity)), idle.sideDuration * (full and 1 or 1.22) * idleSpeed, idle.rightDelay or 0):Play()
        end
        if effect.idleTrace ~= false then
            SetVertexColor(main.FrameTraceTop, topColor)
            local topTrace = ConfigureFrameTrace(main.FrameTraceTop, frame, "top", true, (full and 0.22 or 0.13) * intensity, (full and 1.55 or 1.85) * idle.traceSpeed * idleSpeed, (full and 7.6 or 9.4) * idle.tracePause * idleSpeed)
            if topTrace then topTrace:Play() end
        end
    end

    if crestEnabled and effect.crest ~= false then
        ConfigureIdleAlpha(main.CrestGlow, 0.02, math.min(0.80, (full and 0.24 or 0.14) * (effect.crestMultiplier or 1) * intensity), idle.crestDuration * (full and 1 or 1.28) * idleSpeed, idle.crestDelay or 0):Play()
    end
    if crestEnabled and effect.divider ~= false then
        ConfigureIdleAlpha(main.HeaderDividerGlow, 0.012, math.min(0.65, (full and 0.16 or 0.09) * (effect.dividerMultiplier or 1) * intensity), idle.dividerDuration * (full and 1 or 1.25) * idleSpeed, idle.dividerDelay or 0):Play()
    end

    if cornersEnabled and effect.corners ~= false then
        for index, corner in ipairs(main.Corners or {}) do
            SetColor(corner.Horizontal, cornerColor)
            SetColor(corner.Vertical, cornerColor)
            local duration = (idle.cornerDuration + (index * 0.12)) * (full and 1 or 1.24) * idleSpeed
            local delay = (idle.cornerDelayStep or 0) * (index - 1)
            local peak = math.min(1, cornerBase + ((full and 0.10 or 0.04) * intensity))
            ConfigureIdleAlpha(corner.Horizontal, cornerBase * 0.78, peak, duration, delay):Play()
            ConfigureIdleAlpha(corner.Vertical, cornerBase * 0.78, peak, duration, delay + 0.04):Play()
        end
    end

    if particlesEnabled and effect.particles ~= false and effect.idleParticles ~= false then
        local sparkles = main.FrameSparkles or {}
        local primary = GetFrameEffectColor(theme, effect, "particlePrimary", "accent")
        local secondary = GetFrameEffectColor(theme, effect, "particleSecondary", "silver")
        for index, sparkle in ipairs(sparkles) do
            if UsesFrameParticle(effect, index, #sparkles) then
                SetVertexColor(sparkle, index % 2 == 0 and primary or secondary)
                local group = ConfigureIdleAlpha(sparkle, 0.008, math.min(0.55, (index % 3 == 0 and 0.22 or 0.14) * (effect.particleMultiplier or 1) * intensity), (2.8 + (index * 0.23)) * idle.particleDuration * idleSpeed, (index - 1) * 0.07)
                group.HideOnStop = true
                group:Play()
            end
        end
    end
end

-- ========================= OPENING EFFECT PROFILES ==========================
local function GetFrameMotionProfile(mode, theme)
    local effect = GetFrameEffectProfile(theme)
    local intensity = GetUserFrameIntensity()
    local speedScale = ClampFrameValue(effect.speed or 1, 0.55, 1.65) * GetUserFrameSpeedScale()
    local subtle = mode == "subtle"

    local profile = {
        effect = effect,
        crestPeak = (subtle and 0.54 or 0.86) * (effect.crestMultiplier or 1) * intensity,
        crestRise = (subtle and 0.14 or 0.22) * speedScale,
        crestHold = (subtle and 0.04 or 0.09) * speedScale,
        crestFall = (subtle and 0.30 or 0.52) * speedScale,
        dividerPeak = (subtle and 0.30 or 0.58) * (effect.dividerMultiplier or 1) * intensity,
        topPeak = (subtle and 0.34 or 0.68) * (effect.topMultiplier or 1) * intensity,
        topDuration = (subtle and 0.52 or 0.76) * speedScale,
        sidePeak = (subtle and 0.30 or 0.62) * (effect.sideMultiplier or 1) * intensity,
        sideStart = (subtle and 0.34 or 0.62) * speedScale,
        sideDuration = (subtle and 0.56 or 0.80) * speedScale,
        bottomPeak = (subtle and 0.32 or 0.66) * (effect.bottomMultiplier or 1) * intensity,
        bottomStart = (subtle and 0.82 or 1.34) * speedScale,
        bottomDuration = (subtle and 0.50 or 0.70) * speedScale,
        cornerPeak = (subtle and 0.70 or 0.90) * (effect.cornerMultiplier or 1) * intensity,
        particlePeak = (subtle and 0.18 or 0.32) * (effect.particleMultiplier or 1) * intensity,
        speedScale = speedScale,
    }

    profile.crestPeak = ClampFrameValue(profile.crestPeak, 0, 1)
    profile.dividerPeak = ClampFrameValue(profile.dividerPeak, 0, 1)
    profile.topPeak = ClampFrameValue(profile.topPeak, 0, 1)
    profile.sidePeak = ClampFrameValue(profile.sidePeak, 0, 1)
    profile.bottomPeak = ClampFrameValue(profile.bottomPeak, 0, 1)
    profile.cornerPeak = ClampFrameValue(profile.cornerPeak, 0, 1)
    profile.particlePeak = ClampFrameValue(profile.particlePeak, 0, 1)
    profile.totalDuration = profile.bottomStart + profile.bottomDuration + (0.34 * speedScale)
    if effect.bottom == false then
        profile.totalDuration = profile.sideStart + profile.sideDuration + (0.25 * speedScale)
    end
    if effect.sides == false then
        profile.totalDuration = profile.topDuration + (0.25 * speedScale)
    end
    return profile
end

-- ========================= OPENING FRAME ANIMATION ==========================
-- Only addon-owned decorative regions are animated.
function Skin:PlayFrameOpeningAnimation(theme, token)
    if self.sessionDisabledFeatures and self.sessionDisabledFeatures.frameEffects then return 0 end
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() or not self:IsEnabled() then return 0 end
    local mode = VaultSkinsDB and VaultSkinsDB.frameAnimationMode or "full"
    if mode == "off" or not FrameControlEnabled("frameOpeningAnimation") then return 0 end

    self:StopFrameIdleAnimation()
    token = token or self.animationToken
    local main = self:CreateMainSkin(frame)
    local profile = GetFrameMotionProfile(mode, theme)
    local effect = profile.effect
    local full = mode == "full"
    local crestEnabled = FrameControlEnabled("frameCrestEffects")
    local borderEnabled = FrameControlEnabled("frameBorderFlow")
    local cornersEnabled = FrameControlEnabled("frameCornerEffects")
    local particlesEnabled = FrameControlEnabled("frameParticles")
    local style = GetEffectiveThemeStyle(theme, "frameStyle")
    local cornerBase = style.cornerAlpha or 0.62

    local crestColor = GetFrameEffectColor(theme, effect, "crestColor", "accent")
    local dividerColor = GetFrameEffectColor(theme, effect, "dividerColor", "glow")
    local topColor = GetFrameEffectColor(theme, effect, "topColor", "glow")
    local sideColor = GetFrameEffectColor(theme, effect, "sideColor", "glow")
    local bottomColor = GetFrameEffectColor(theme, effect, "bottomColor", "accent")
    local cornerColor = GetFrameEffectColor(theme, effect, "cornerColor", "metal")

    if VaultSkinsDB and VaultSkinsDB.soundSync then
        PlayVaultSound("frameOpen", theme)
        if crestEnabled and (effect.crest ~= false or effect.divider ~= false) then
            ScheduleVaultSound("crest", theme, 0.04, token)
        end
        if borderEnabled and effect.top ~= false then
            ScheduleVaultSound("border", theme, math.max(0.06, profile.topDuration * 0.16), token)
        end
        if cornersEnabled and effect.corners ~= false then
            local cornerSoundDelay = effect.bottom == false and (profile.sideStart + 0.04) or (profile.bottomStart + 0.04)
            ScheduleVaultSound("corner", theme, cornerSoundDelay, token)
        end
    end

    if crestEnabled and (effect.crest ~= false or effect.divider ~= false) then
        ScheduleOwnedTimer(nil, 0.04, function()
            SafeCall("theme crest frame effect", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                if effect.crest ~= false then
                    main.CrestGlow:SetBackdropBorderColor(crestColor.r, crestColor.g, crestColor.b, 1)
                    CreatePulseAnimation(main.CrestGlow, profile.crestPeak, profile.crestRise, profile.crestHold, profile.crestFall, 0, true):Play()
                end
                if effect.divider ~= false then
                    SetVertexColor(main.HeaderDividerGlow, dividerColor)
                    CreatePulseAnimation(main.HeaderDividerGlow, profile.dividerPeak, profile.crestRise * 0.72, 0.10, profile.crestFall * 0.72, 0, true):Play()
                end
            end)
        end)
    end

    if borderEnabled and effect.top ~= false then
        SetVertexColor(main.FrameTraceTop, topColor)
        local top = ConfigureFrameTrace(main.FrameTraceTop, frame, "top", false, profile.topPeak, profile.topDuration, 0)
        if top then top:Play() end
    end

    if borderEnabled and effect.sides ~= false then
        local stagger = math.max(0, tonumber(effect.sideStagger) or 0) * profile.speedScale
        local function PlaySide(texture, label)
            SafeCall(label, function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                SetVertexColor(texture, sideColor)
                local group = ConfigureFrameTrace(texture, frame, texture == main.FrameTraceLeft and "left" or "right", false, profile.sidePeak, profile.sideDuration, 0)
                if group then group:Play() end
            end)
        end
        ScheduleOwnedTimer(nil, profile.sideStart, function() PlaySide(main.FrameTraceLeft, "theme left frame effect") end)
        ScheduleOwnedTimer(nil, profile.sideStart + stagger, function() PlaySide(main.FrameTraceRight, "theme right frame effect") end)
    end

    if borderEnabled and effect.bottom ~= false then
        ScheduleOwnedTimer(nil, profile.bottomStart, function()
            SafeCall("theme bottom frame effect", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                SetVertexColor(main.FrameTraceBottom, bottomColor)
                local bottom = ConfigureFrameTrace(main.FrameTraceBottom, frame, "bottom", false, profile.bottomPeak, profile.bottomDuration, 0)
                if bottom then bottom:Play() end
            end)
        end)
    end

    if cornersEnabled and effect.corners ~= false then
        for index, corner in ipairs(main.Corners or {}) do
            SetColor(corner.Horizontal, cornerColor)
            SetColor(corner.Vertical, cornerColor)
            local delay = index <= 2 and 0.12 or (effect.bottom == false and profile.sideStart or profile.bottomStart) + 0.04
            ScheduleOwnedTimer(nil, delay, function()
                SafeCall("theme corner frame effect", function()
                    if token ~= Skin.animationToken or not frame:IsShown() then return end
                    CreatePulseAnimation(corner.Horizontal, profile.cornerPeak, 0.16, 0.02, 0.46, cornerBase, false):Play()
                    CreatePulseAnimation(corner.Vertical, profile.cornerPeak, 0.16, 0.02, 0.46, cornerBase, false):Play()
                end)
            end)
        end
    end

    if particlesEnabled and effect.particles ~= false then
        local sparkles = main.FrameSparkles or {}
        local primary = GetFrameEffectColor(theme, effect, "particlePrimary", "accent")
        local secondary = GetFrameEffectColor(theme, effect, "particleSecondary", "silver")
        for index, sparkle in ipairs(sparkles) do
            if UsesFrameParticle(effect, index, #sparkles) then
                ScheduleOwnedTimer(nil, 0.18 + (index * 0.07 * profile.speedScale), function()
                    SafeCall("theme particle frame effect", function()
                        if token ~= Skin.animationToken or not frame:IsShown() then return end
                        SetVertexColor(sparkle, index % 2 == 0 and primary or secondary)
                        CreatePulseAnimation(sparkle, index % 3 == 0 and profile.particlePeak or (profile.particlePeak * 0.68), 0.18, 0.03, 0.62, 0, true):Play()
                    end)
                end)
            end
        end
    end

    if crestEnabled and effect.secondaryDividerPulse and effect.divider ~= false then
        ScheduleOwnedTimer(nil, profile.topDuration + 0.10, function()
            SafeCall("theme divider echo", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                SetVertexColor(main.HeaderDividerGlow, dividerColor)
                CreatePulseAnimation(main.HeaderDividerGlow, profile.dividerPeak * 0.52, 0.12, 0.02, 0.30, 0, true):Play()
            end)
        end)
    end
    if crestEnabled and effect.secondaryCrestPulse and effect.crest ~= false then
        ScheduleOwnedTimer(nil, profile.sideStart + profile.sideDuration * 0.45, function()
            SafeCall("theme crest echo", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                main.CrestGlow:SetBackdropBorderColor(crestColor.r, crestColor.g, crestColor.b, 1)
                CreatePulseAnimation(main.CrestGlow, profile.crestPeak * 0.55, 0.12, 0.02, 0.34, 0, true):Play()
            end)
        end)
    end
    if cornersEnabled and effect.secondaryCornerPulse and effect.corners ~= false then
        ScheduleOwnedTimer(nil, profile.bottomStart + profile.bottomDuration * 0.40, function()
            SafeCall("theme corner echo", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                for _, corner in ipairs(main.Corners or {}) do
                    CreatePulseAnimation(corner.Horizontal, profile.cornerPeak * 0.62, 0.10, 0.01, 0.26, cornerBase, false):Play()
                    CreatePulseAnimation(corner.Vertical, profile.cornerPeak * 0.62, 0.10, 0.01, 0.26, cornerBase, false):Play()
                end
            end)
        end)
    end
    if borderEnabled and effect.bottomEcho and effect.bottom ~= false then
        ScheduleOwnedTimer(nil, profile.bottomStart + profile.bottomDuration + 0.08, function()
            SafeCall("theme bottom echo", function()
                if token ~= Skin.animationToken or not frame:IsShown() then return end
                SetVertexColor(main.FrameTraceBottom, bottomColor)
                local bottom = ConfigureFrameTrace(main.FrameTraceBottom, frame, "bottom", false, profile.bottomPeak * 0.48, profile.bottomDuration * 0.62, 0)
                if bottom then bottom:Play() end
            end)
        end)
    end

    local duration = profile.totalDuration
    if not borderEnabled then
        duration = crestEnabled and (profile.crestRise + profile.crestHold + profile.crestFall + 0.12) or 0
        if cornersEnabled then duration = math.max(duration, 0.80 * profile.speedScale) end
        if particlesEnabled and effect.particles ~= false then duration = math.max(duration, 1.20 * profile.speedScale) end
    end
    return duration
end

-- ========================= PREVIEW AND REFRESH API ==========================
function Skin:PreviewFrameAnimation()
    local frame = WeeklyRewardsFrame
    if not frame or not frame:IsShown() then return false end
    self.animationToken = (self.animationToken or 0) + 1
    local token = self.animationToken
    local duration = 0
    local ok, result = pcall(self.PlayFrameOpeningAnimation, self, self:GetTheme(), token)
    if ok then duration = tonumber(result) or 0 end
    ScheduleOwnedTimer(nil, duration + 0.18, function()
        SafeCall("safe frame preview idle", function()
            if token == Skin.animationToken and frame:IsShown() then Skin:StartFrameIdleAnimation() end
        end)
    end)
    return ok
end

function Skin:ClearTransientFramePulses()
    local frame = WeeklyRewardsFrame
    if not frame or not frame.VaultSkins then return end
    local main = frame.VaultSkins
    local regions = {
        main.OpeningGlow,
        main.TopGlow,
        main.SideGlowLeft,
        main.SideGlowRight,
        main.CrestGlow,
        main.HeaderDividerGlow,
    }
    for _, corner in ipairs(main.Corners or {}) do
        regions[#regions + 1] = corner.Horizontal
        regions[#regions + 1] = corner.Vertical
    end
    for _, region in ipairs(regions) do
        local group = region and region.VaultSkinsPulse
        if group and group.IsPlaying and group:IsPlaying() then
            group:Stop()
        end
    end
end

function Skin:RefreshFrameAnimation()
    self:ClearTransientFramePulses()
    self:StopFrameIdleAnimation()
    if WeeklyRewardsFrame and WeeklyRewardsFrame:IsShown() and self:IsEnabled() then
        if VaultSkinsDB.frameAnimationMode == "off" then
            self:ApplyStaticFrameAppearance()
        elseif VaultSkinsDB.frameIdleAnimation ~= false then
            self:StartFrameIdleAnimation()
        end
    end
end


Shared.GetFrameEffectProfile = GetFrameEffectProfile
Shared.GetFrameEffectColor = GetFrameEffectColor
