local ADDON_NAME, RPAL = ...

-- ============================================================
-- Reward / progression toast director
-- ============================================================

local TEX="Interface\\AddOns\\ODYN\\Textures\\"
local SPARK=TEX.."Talent\\sparkle.tga"
local REWARD_SEAL=TEX.."Talent\\node_reward.tga"
local GOLD={0.96,0.76,0.30}
local VIOLET={0.67,0.48,1.00}
local PALE={0.96,0.93,0.86}
local MUTED={0.70,0.68,0.72}

local levelToast
local procToast
local toastQueue={}
local toastBusy=false
local StartQueuedToast
local managedToastFrames=setmetatable({},{__mode="k"})
local toastLayoutEditor

local function Clamp(value,minimum,maximum)
    value=tonumber(value) or minimum
    return math.max(minimum,math.min(maximum,value))
end

local function ToastLayout()
    local settings=RPAL.GetSettings and RPAL.GetSettings() or nil
    if not settings then return {x=0,y=160,scale=1} end
    if type(settings.toastLayout)~="table" then settings.toastLayout={} end
    local layout=settings.toastLayout
    layout.x=Clamp(layout.x,-2400,2400)
    layout.y=Clamp(layout.y,-1400,1400)
    layout.scale=Clamp(layout.scale,.65,1.50)
    return layout
end

function RPAL.GetToastLayoutScale()
    return ToastLayout().scale
end

function RPAL.ApplyToastLayout(frame)
    if not frame then return end
    local layout=ToastLayout()
    managedToastFrames[frame]=true
    frame._rpalToastManaged=true
    frame._rpalToastBaseScale=layout.scale
    frame:ClearAllPoints()
    frame:SetPoint("CENTER",UIParent,"CENTER",layout.x,layout.y)
    frame:SetScale(layout.scale)
    if RPAL.ApplyToastFrameCustomization then RPAL.ApplyToastFrameCustomization(frame,GOLD) end
end

function RPAL.RefreshToastLayout()
    local layout=ToastLayout()
    for frame in pairs(managedToastFrames) do
        if frame then
            frame._rpalToastBaseScale=layout.scale
            frame:ClearAllPoints()
            frame:SetPoint("CENTER",UIParent,"CENTER",layout.x,layout.y)
            frame:SetScale(layout.scale)
        end
    end
end

local function ToastBaseScale(frame)
    return (frame and tonumber(frame._rpalToastBaseScale)) or ToastLayout().scale
end

local function EditorText(parent,text,size,color)
    local fs=parent:CreateFontString(nil,"OVERLAY")
    fs:SetFont("Fonts\\FRIZQT__.TTF",size or 11,"OUTLINE")
    fs:SetText(text or "")
    color=color or PALE
    fs:SetTextColor(color[1],color[2],color[3],color[4] or 1)
    return fs
end

local function EditorButton(parent,label,width)
    local b=CreateFrame("Button",nil,parent,"BackdropTemplate")
    b:SetSize(width or 96,30)
    b:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8",edgeFile="Interface\\Buttons\\WHITE8X8",edgeSize=1})
    b:SetBackdropColor(.055,.045,.075,.98);b:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],.66)
    b.label=EditorText(b,label,9.5,PALE);b.label:SetPoint("CENTER")
    b:SetScript("OnEnter",function(self)self:SetBackdropBorderColor(1,.86,.42,1)end)
    b:SetScript("OnLeave",function(self)self:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],.66)end)
    if RPAL.AttachButtonMotion then RPAL.AttachButtonMotion(b,0.018) end
    if RPAL.AttachFocusMark then RPAL.AttachFocusMark(b,GOLD) end
    return b
end

function RPAL.OpenToastLayoutEditor()
    if RPAL.ResetToastDirector then RPAL.ResetToastDirector() end
    local function RefreshEditor(shade)
        local layout=ToastLayout()
        local preview=shade.preview
        preview:ClearAllPoints();preview:SetPoint("CENTER",UIParent,"CENTER",layout.x,layout.y);preview:SetScale(layout.scale)
        shade.sizeValue:SetText(tostring(math.floor((layout.scale*100)+.5)).."%")
        shade.positionValue:SetText("X "..tostring(math.floor(layout.x+.5)).."  •  Y "..tostring(math.floor(layout.y+.5)))
        RPAL.RefreshToastLayout()
    end

    if toastLayoutEditor then
        toastLayoutEditor:Show()
        RefreshEditor(toastLayoutEditor)
        return toastLayoutEditor
    end

    local shade=CreateFrame("Frame","RPAL_ToastLayoutEditor",UIParent,"BackdropTemplate")
    toastLayoutEditor=shade
    shade:SetAllPoints(UIParent);shade:SetFrameStrata("FULLSCREEN_DIALOG");shade:SetFrameLevel(9900);shade:SetToplevel(true);shade:EnableMouse(true)
    shade:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8"});shade:SetBackdropColor(0,0,0,.72)
    if RPAL.RegisterEscapeFrame then RPAL.RegisterEscapeFrame(shade) else table.insert(UISpecialFrames,"RPAL_ToastLayoutEditor") end

    local heading=EditorText(shade,"TOAST LAYOUT",18,GOLD);heading:SetPoint("TOP",0,-36)
    local help=EditorText(shade,"Drag the preview. Size and position apply to ODYN's transient notifications.",10,MUTED);help:SetPoint("TOP",heading,"BOTTOM",0,-9)

    local preview=CreateFrame("Frame",nil,shade,"BackdropTemplate")
    shade.preview=preview
    preview:SetSize(452,70);preview:SetFrameStrata("TOOLTIP");preview:SetFrameLevel(1200);preview:SetToplevel(true);preview:EnableMouse(true);preview:SetMovable(true);preview:RegisterForDrag("LeftButton");preview:SetClampedToScreen(true)
    preview:SetBackdrop({bgFile="Interface\\Tooltips\\UI-Tooltip-Background",edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",edgeSize=14,insets={left=4,right=4,top=4,bottom=4}})
    preview:SetBackdropColor(.012,.009,.025,.96);preview:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],.92)
    local previewTitle=EditorText(preview,"TOAST PREVIEW",13,GOLD);previewTitle:SetPoint("CENTER",0,10)
    local previewHelp=EditorText(preview,"Drag me to the position you want",9,PALE);previewHelp:SetPoint("CENTER",0,-12)
    local dragGlow=preview:CreateTexture(nil,"BACKGROUND");dragGlow:SetTexture(SPARK);dragGlow:SetBlendMode("ADD");dragGlow:SetAllPoints();dragGlow:SetVertexColor(GOLD[1],GOLD[2],GOLD[3],.10)
    if RPAL.ApplyToastFrameCustomization then RPAL.ApplyToastFrameCustomization(preview,GOLD) end

    preview:SetScript("OnDragStart",function(self)
        local effective=(UIParent.GetEffectiveScale and UIParent:GetEffectiveScale()) or 1
        local cx,cy=GetCursorPosition()
        local layout=ToastLayout()
        self._dragStartX=cx/effective;self._dragStartY=cy/effective
        self._layoutStartX=layout.x;self._layoutStartY=layout.y
        self:SetScript("OnUpdate",function(frame)
            local nowX,nowY=GetCursorPosition();nowX=nowX/effective;nowY=nowY/effective
            local current=ToastLayout()
            local halfWidth=math.max(120,((UIParent.GetWidth and UIParent:GetWidth()) or 1280)/2-70)
            local halfHeight=math.max(80,((UIParent.GetHeight and UIParent:GetHeight()) or 720)/2-55)
            current.x=Clamp((frame._layoutStartX or 0)+(nowX-(frame._dragStartX or nowX)),-halfWidth,halfWidth)
            current.y=Clamp((frame._layoutStartY or 160)+(nowY-(frame._dragStartY or nowY)),-halfHeight,halfHeight)
            RefreshEditor(shade)
        end)
    end)
    preview:SetScript("OnDragStop",function(self)self:SetScript("OnUpdate",nil)end)

    local controls=CreateFrame("Frame",nil,shade,"BackdropTemplate")
    controls:SetSize(520,104);controls:SetPoint("BOTTOM",0,34);controls:SetFrameStrata("TOOLTIP");controls:SetFrameLevel(1190);controls:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8",edgeFile="Interface\\Buttons\\WHITE8X8",edgeSize=1});controls:SetBackdropColor(.018,.014,.030,.98);controls:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],.54)
    local sizeLabel=EditorText(controls,"SIZE",8,MUTED);sizeLabel:SetPoint("TOPLEFT",18,-17)
    local minus=EditorButton(controls,"−",34);minus:SetPoint("TOPLEFT",18,-38)
    shade.sizeValue=EditorText(controls,"100%",11,GOLD);shade.sizeValue:SetPoint("LEFT",minus,"RIGHT",18,0);shade.sizeValue:SetWidth(54);shade.sizeValue:SetJustifyH("CENTER")
    local plus=EditorButton(controls,"+",34);plus:SetPoint("LEFT",shade.sizeValue,"RIGHT",18,0)
    shade.positionValue=EditorText(controls,"",9,MUTED);shade.positionValue:SetPoint("LEFT",plus,"RIGHT",22,0);shade.positionValue:SetWidth(142);shade.positionValue:SetJustifyH("CENTER")
    local reset=EditorButton(controls,"RESET",76);reset:SetPoint("TOPRIGHT",-98,-38)
    local done=EditorButton(controls,"DONE",76);done:SetPoint("TOPRIGHT",-14,-38)
    minus:SetScript("OnClick",function()local layout=ToastLayout();layout.scale=Clamp(layout.scale-.05,.65,1.50);RefreshEditor(shade)end)
    plus:SetScript("OnClick",function()local layout=ToastLayout();layout.scale=Clamp(layout.scale+.05,.65,1.50);RefreshEditor(shade)end)
    reset:SetScript("OnClick",function()local layout=ToastLayout();layout.x=0;layout.y=160;layout.scale=1;RefreshEditor(shade)end)
    done:SetScript("OnClick",function()shade:Hide()end)
    shade:HookScript("OnHide",function()preview:SetScript("OnUpdate",nil)end)

    RefreshEditor(shade)
    shade:Show();shade:Raise()
    return shade
end

local BADGE_ASSETS={
    ["Initiate Seal"]=TEX.."Cosmetics\\Badges2\\initiate_seal.tga",
    ["Verdant Mark"]=TEX.."Cosmetics\\Badges2\\verdant_mark.tga",
    ["Azure Sigil"]=TEX.."Cosmetics\\Badges2\\azure_sigil.tga",
    ["Amethyst Crest"]=TEX.."Cosmetics\\Badges2\\amethyst_crest.tga",
    ["Rose Emblem"]=TEX.."Cosmetics\\Badges2\\rose_emblem.tga",
    ["Sunforged Seal"]=TEX.."Cosmetics\\Badges2\\sunforged_seal.tga",
    ["Crimson Legend"]=TEX.."Cosmetics\\Badges2\\crimson_legend.tga",
    ["Legacy Chronicle"]=TEX.."Cosmetics\\Badges2\\legacy_chronicle.tga",
    ["Captain's Radiance"]=TEX.."Cosmetics\\Badges2\\captains_radiance.tga",
    ["Open Beta Crest"]=TEX.."Cosmetics\\Badges2\\open_beta_crest.tga",
    ["Maplefall Seal"]=TEX.."Cosmetics\\Badges2\\maplefall_seal.tga",
    ["Northstar Seal"]=TEX.."Cosmetics\\Badges2\\northstar_seal.tga",
    ["Archivist Seal"]=TEX.."Cosmetics\\Badges2\\archivist_seal.tga",
    ["Wayglass Seal"]=TEX.."Cosmetics\\Badges2\\wayglass_seal.tga",
    ["Wolf Seal"]=TEX.."Cosmetics\\Badges2\\wolf_seal.tga",
}

local function LevelColor(level)
    level=math.floor(tonumber(level) or 1)
    if level<=4 then return 0.68,0.68,0.68
    elseif level<=14 then return 0.34,0.88,0.38
    elseif level<=29 then return 0.32,0.62,1.00
    elseif level<=39 then return 0.72,0.39,0.96
    elseif level<=49 then return 1.00,0.45,0.72
    elseif level<=59 then return 1.00,0.84,0.28
    end
    return 1.00,0.31,0.44
end

local function BadgeForLevel(level,rewards)
    for _,reward in ipairs(rewards or {}) do
        if reward.kind=="level" and BADGE_ASSETS[reward.name] then
            return BADGE_ASSETS[reward.name]
        end
    end
    level=math.floor(tonumber(level) or 1)
    if level>=60 then return BADGE_ASSETS["Crimson Legend"] end
    if level>=50 then return BADGE_ASSETS["Sunforged Seal"] end
    if level>=40 then return BADGE_ASSETS["Rose Emblem"] end
    if level>=30 then return BADGE_ASSETS["Amethyst Crest"] end
    if level>=15 then return BADGE_ASSETS["Azure Sigil"] end
    if level>=5 then return BADGE_ASSETS["Verdant Mark"] end
    return BADGE_ASSETS["Initiate Seal"]
end

local function Font(parent,text,size,color,font)
    local fs=parent:CreateFontString(nil,"OVERLAY")
    local path=font or "Fonts\\FRIZQT__.TTF"
    local base=size or 12
    fs:SetFont(path,RPAL.GetScaledFontSize and RPAL.GetScaledFontSize(base) or base,"OUTLINE")
    if RPAL.RegisterScalableFontObject then RPAL.RegisterScalableFontObject(fs,path,base,"OUTLINE") end
    color=color or PALE
    fs:SetTextColor(color[1],color[2],color[3],color[4] or 1)
    fs:SetShadowColor(0,0,0,1);fs:SetShadowOffset(2,-2)
    return fs
end

local function RewardSummary(rewards,limit)
    local names={}
    for _,reward in ipairs(rewards or {}) do
        if reward and reward.name then names[#names+1]=tostring(reward.name) end
    end
    if #names==0 then return "",0 end
    limit=math.max(1,math.floor(tonumber(limit) or 2))
    local shown={}
    for i=1,math.min(limit,#names) do shown[#shown+1]=names[i] end
    local suffix=#names>limit and ("  +"..tostring(#names-limit).." MORE") or ""
    return table.concat(shown,"  •  ")..suffix,#names
end

local LEVEL_REWARD_KIND_LABELS={
    portrait="Portrait",header="Header",theme="Theme",effect="Effect",
    ornament="Ornament",aura="Aura",level="Level Badge",background="Background",
    platform="Platform",animation="Animation",toast="Toast Frame",tooltip="Tooltip Treatment",focus="Focus Mark",
}

local function LevelRewardLines(rewards,talentPoints)
    local lines={}
    talentPoints=math.max(0,math.floor(tonumber(talentPoints) or 0))
    if talentPoints>0 then
        lines[#lines+1]="+"..tostring(talentPoints).." Talent Point"..(talentPoints==1 and "" or "s")
    end
    for _,reward in ipairs(rewards or {}) do
        if reward and reward.name then
            local kind=LEVEL_REWARD_KIND_LABELS[tostring(reward.kind or ""):lower()] or "Reward"
            lines[#lines+1]="New "..kind.." Unlocked: \""..tostring(reward.name).."\""
        end
    end
    return lines
end

local function FinishQueuedToast(frame)
    if frame then
        frame:Hide()
        frame:SetAlpha(1)
        frame:SetScale(ToastBaseScale(frame))
        frame:SetScript("OnUpdate",nil)
    end
    toastBusy=false
    local nextEntry=table.remove(toastQueue,1)
    if nextEntry and StartQueuedToast then StartQueuedToast(nextEntry) end
end

local function EnqueueToast(entry,priority)
    if not entry then return end
    if toastBusy then
        if priority then table.insert(toastQueue,1,entry)
        else toastQueue[#toastQueue+1]=entry end
        return
    end
    StartQueuedToast(entry)
end

function RPAL.ResetToastDirector()
    toastQueue={}
    toastBusy=false
    for _,frame in ipairs({levelToast,procToast}) do
        if frame then
            frame:Hide();frame:SetAlpha(1);frame:SetScale(ToastBaseScale(frame));frame:SetScript("OnUpdate",nil)
        end
    end
end

local function EnsureLevelToast()
    local f=levelToast or _G.RPAL_LevelToast
    if not f then f=CreateFrame("Frame","RPAL_LevelToast",UIParent,"BackdropTemplate") end

    -- Height is finalized per level-up after its reward list is known.
    f:SetSize(520,142)
    RPAL.ApplyToastLayout(f)
    if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"TOAST",0) else f:SetFrameStrata("TOOLTIP");f:SetFrameLevel(14000) end
    f:EnableMouse(false)
    f:Hide()
    f:SetBackdrop({
        bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",
        tile=false,tileSize=16,edgeSize=14,
        insets={left=4,right=4,top=4,bottom=4},
    })
    f:SetBackdropColor(0.010,0.008,0.022,0.91)
    f:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],0.72)

    if not f._rpalLevelToastV2 then
        f._rpalLevelToastV2=true
        f._rpalPanelGlow=f:CreateTexture(nil,"BACKGROUND",nil,-1)
        f._rpalPanelGlow:SetTexture(SPARK)
        f._rpalPanelGlow:SetBlendMode("ADD")
        f._rpalPanelGlow:SetPoint("TOPLEFT",8,-8)
        f._rpalPanelGlow:SetPoint("BOTTOMRIGHT",-8,8)
        f._rpalPanelGlow:SetAlpha(0)

        f._rpalMedallionHalo=f:CreateTexture(nil,"ARTWORK",nil,0)
        f._rpalMedallionHalo:SetTexture(SPARK)
        f._rpalMedallionHalo:SetBlendMode("ADD")
        f._rpalMedallionHalo:SetSize(104,104)
        f._rpalMedallionHalo:SetPoint("TOPLEFT",1,3)

        f._rpalMedallionShadow=f:CreateTexture(nil,"ARTWORK",nil,1)
        f._rpalMedallionShadow:SetSize(76,76)
        f._rpalMedallionShadow:SetPoint("TOPLEFT",17,-15)
        f._rpalMedallionShadow:SetTexCoord(0,1,1,0)
        f._rpalMedallionShadow:SetVertexColor(0.002,0.004,0.010,0.90)

        f._rpalMedallion=f:CreateTexture(nil,"OVERLAY",nil,2)
        f._rpalMedallion:SetSize(74,74)
        f._rpalMedallion:SetPoint("TOPLEFT",16,-13)
        f._rpalMedallion:SetTexCoord(0,1,1,0)

        f._rpalDivider=f:CreateTexture(nil,"ARTWORK")
        f._rpalDivider:SetTexture("Interface\\Buttons\\WHITE8X8")
        f._rpalDivider:SetHeight(1)
        f._rpalDivider:SetPoint("TOPLEFT",106,-70)
        f._rpalDivider:SetPoint("TOPRIGHT",-18,-70)
        f._rpalDivider:SetBlendMode("ADD")

        f._rpalEyebrow=Font(f,"RENOWN ASCENSION",8,MUTED)
        f._rpalEyebrow:SetPoint("TOPLEFT",106,-16)

        f._rpalLevelText=Font(f,"",25,GOLD,"Fonts\\FRIZQT__.TTF")
        f._rpalLevelText:SetPoint("TOPLEFT",104,-32)
        f._rpalLevelText:SetWidth(390);f._rpalLevelText:SetWordWrap(false)

        f._rpalRewardLines={}

        f._rpalBurstGlow=f:CreateTexture(nil,"OVERLAY",nil,3)
        f._rpalBurstGlow:SetTexture(SPARK)
        f._rpalBurstGlow:SetBlendMode("ADD")
        f._rpalBurstGlow:SetSize(240,92)
        f._rpalBurstGlow:SetPoint("CENTER",f._rpalLevelText,34,0)
        f._rpalBurstGlow:SetAlpha(0)
    end

    levelToast=f
    return f
end

local function StartLevel(entry)
    toastBusy=true
    local f=EnsureLevelToast()
    RPAL.ApplyToastLayout(f)
    local baseScale=ToastBaseScale(f)
    local level=math.max(1,math.floor(tonumber(entry.level) or 1))
    local rewards=entry.rewards or {}
    local r,g,b=LevelColor(level)
    local badgeAsset=BadgeForLevel(level,rewards)
    local oldLevel=math.max(1,math.floor(tonumber(entry.oldLevel) or (level-1)))
    local oldTalentTotal=RPAL.GetTalentPointsForLevel and RPAL.GetTalentPointsForLevel(oldLevel) or 0
    local newTalentTotal=RPAL.GetTalentPointsForLevel and RPAL.GetTalentPointsForLevel(level) or 0
    local talentPoints=math.max(0,newTalentTotal-oldTalentTotal)
    local legacy=entry.source=="legacy_calibration"
    local lines=LevelRewardLines(rewards,talentPoints)
    local lineCount=#lines
    local height=math.max(132,102+(math.max(1,lineCount)*21))
    f:SetSize(520,height)

    f:SetBackdropBorderColor(r,g,b,0.78)
    if RPAL.ApplyToastFrameCustomization then RPAL.ApplyToastFrameCustomization(f,{r,g,b,1}) end
    f._rpalDivider:SetVertexColor(r,g,b,0.48)
    f._rpalPanelGlow:SetVertexColor(r,g,b,0.76)
    f._rpalMedallionHalo:SetVertexColor(r,g,b,0.88)
    f._rpalMedallion:SetTexture(badgeAsset)
    f._rpalMedallionShadow:SetTexture(badgeAsset)
    f._rpalMedallion:SetVertexColor(r,g,b,0.98)
    f._rpalBurstGlow:SetVertexColor(r,g,b,1)
    f._rpalLevelText:SetText("LEVEL "..tostring(level).." REACHED")
    f._rpalLevelText:SetTextColor(r,g,b,1)
    f._rpalEyebrow:SetText(legacy and "LEGACY AWAKENING" or "RENOWN ASCENSION")

    for index,text in ipairs(lines) do
        local row=f._rpalRewardLines[index]
        if not row then
            row=Font(f,"",9.5,PALE)
            row:SetWidth(390);row:SetWordWrap(false)
            f._rpalRewardLines[index]=row
        end
        row:ClearAllPoints();row:SetPoint("TOPLEFT",108,-84-((index-1)*21))
        row:SetText("•  "..text);row:SetTextColor(PALE[1],PALE[2],PALE[3],1);row:SetAlpha(0);row:Show()
    end
    for index=lineCount+1,#(f._rpalRewardLines or {}) do f._rpalRewardLines[index]:Hide() end

    f._rpalEyebrow:SetAlpha(0);f._rpalLevelText:SetAlpha(0);f._rpalDivider:SetAlpha(0)
    f._rpalMedallion:SetAlpha(0);f._rpalMedallionHalo:SetAlpha(0)
    f._rpalMedallionShadow:SetAlpha(0)
    f._rpalPanelGlow:SetAlpha(0);f._rpalBurstGlow:SetAlpha(0)
    f:SetAlpha(0);f:SetScale(baseScale*0.94);f._age=0;f:Show()

    f:SetScript("OnUpdate",function(self,elapsed)
        self._age=(self._age or 0)+(elapsed or 0)
        local t=self._age
        local enter=math.max(0,math.min(1,t/0.30))
        local ease=1-((1-enter)*(1-enter)*(1-enter))
        local headerA=math.max(0,math.min(1,(t-0.14)/0.34))
        local dividerA=math.max(0,math.min(1,(t-0.42)/0.24))
        local lastLineReady=lineCount>0 and (0.68+((lineCount-1)*0.31)+0.28) or 0.72
        local fadeAt=lastLineReady+1.55
        local finishAt=fadeAt+0.44
        local tail=t>fadeAt and math.max(0,1-((t-fadeAt)/0.44)) or 1

        self:SetAlpha(ease*tail)
        self:SetScale(baseScale*(0.94+(ease*0.06)))
        self._rpalEyebrow:SetAlpha(headerA*tail)
        self._rpalLevelText:SetAlpha(headerA*tail)
        self._rpalDivider:SetAlpha(dividerA*tail)
        self._rpalMedallion:SetAlpha(headerA*tail)
        self._rpalMedallionShadow:SetAlpha((headerA*0.82)*tail)
        self._rpalMedallionHalo:SetAlpha((0.10+(headerA*0.20))*tail)
        self._rpalPanelGlow:SetAlpha((0.05+(headerA*0.10))*tail)
        for index,row in ipairs(self._rpalRewardLines or {}) do
            if index<=lineCount then
                local startAt=0.68+((index-1)*0.31)
                local lineA=math.max(0,math.min(1,(t-startAt)/0.28))
                row:SetAlpha(lineA*tail)
            end
        end

        local burst=math.max(0,1-math.abs(t-0.72)/0.54)
        self._rpalBurstGlow:SetAlpha(burst*0.82*tail)
        self._rpalBurstGlow:SetSize(226+(burst*72),86+(burst*26))

        if t>=finishAt then FinishQueuedToast(self) end
    end)
end

local function EnsureProcToast()
    local f=procToast or _G.RPAL_RewardToast
    if not f then f=CreateFrame("Frame","RPAL_RewardToast",UIParent,"BackdropTemplate") end

    f:SetSize(392,98)
    RPAL.ApplyToastLayout(f)
    if RPAL.ApplyUILayer then RPAL.ApplyUILayer(f,"TOAST",10) else f:SetFrameStrata("TOOLTIP");f:SetFrameLevel(14010) end
    f:EnableMouse(false)
    f:Hide()
    f:SetBackdrop({
        bgFile="Interface\\Tooltips\\UI-Tooltip-Background",
        edgeFile="Interface\\Tooltips\\UI-Tooltip-Border",
        tile=false,tileSize=16,edgeSize=14,
        insets={left=4,right=4,top=4,bottom=4},
    })
    f:SetBackdropColor(0.014,0.010,0.028,0.92)
    f:SetBackdropBorderColor(GOLD[1],GOLD[2],GOLD[3],0.76)

    if not f._rpalPanelGlow then
        f._rpalPanelGlow=f:CreateTexture(nil,"BACKGROUND",nil,-1)
        f._rpalPanelGlow:SetTexture(SPARK)
        f._rpalPanelGlow:SetBlendMode("ADD")
        f._rpalPanelGlow:SetSize(330,108)
        f._rpalPanelGlow:SetPoint("CENTER",16,-2)
        f._rpalPanelGlow:SetVertexColor(GOLD[1],GOLD[2],GOLD[3],0.72)
        f._rpalPanelGlow:SetAlpha(0)

        f._rpalSeal=f:CreateTexture(nil,"ARTWORK")
        f._rpalSeal:SetTexture(REWARD_SEAL)
        f._rpalSeal:SetSize(58,58)
        f._rpalSeal:SetPoint("LEFT",15,0)
        f._rpalSeal:SetBlendMode("ADD")
        f._rpalSeal:SetVertexColor(GOLD[1],GOLD[2],GOLD[3],0.62)

        f._rpalEyebrow=Font(f,"TALENT TRIGGERED",8,MUTED)
        f._rpalEyebrow:SetPoint("TOPLEFT",82,-14)

        f._rpalTitleText=Font(f,"",15,PALE,"Fonts\\FRIZQT__.TTF")
        f._rpalTitleText:SetPoint("TOPLEFT",82,-32)
        f._rpalTitleText:SetWidth(286);f._rpalTitleText:SetWordWrap(false)

        f._rpalValueText=Font(f,"+0 RENOWN",23,GOLD,"Fonts\\FRIZQT__.TTF")
        f._rpalValueText:SetPoint("TOPLEFT",80,-56)

        f._rpalDetailText=Font(f,"",9,PALE)
        f._rpalDetailText:SetPoint("TOPLEFT",82,-62)
        f._rpalDetailText:SetWidth(286);f._rpalDetailText:SetWordWrap(false)

        f._rpalBurstGlow=f:CreateTexture(nil,"OVERLAY")
        f._rpalBurstGlow:SetTexture(SPARK)
        f._rpalBurstGlow:SetBlendMode("ADD")
        f._rpalBurstGlow:SetSize(176,78)
        f._rpalBurstGlow:SetPoint("CENTER",f._rpalValueText,40,0)
        f._rpalBurstGlow:SetAlpha(0)
    end

    procToast=f
    return f
end

local function StartProc(entry)
    toastBusy=true
    if entry and entry.soundKind and RPAL.PlayODYNSound then RPAL.PlayODYNSound(entry.soundKind) end
    local f=EnsureProcToast()
    RPAL.ApplyToastLayout(f)
    local baseScale=ToastBaseScale(f)
    local amount=math.max(0,math.floor(tonumber(entry.amount) or 0))
    local accent=type(entry.accent)=="table" and entry.accent or GOLD
    f:SetBackdropBorderColor(accent[1] or GOLD[1],accent[2] or GOLD[2],accent[3] or GOLD[3],0.78)
    if RPAL.ApplyToastFrameCustomization then RPAL.ApplyToastFrameCustomization(f,accent) end
    f._rpalSeal:SetTexture(entry.sealTexture or REWARD_SEAL)
    f._rpalSeal:SetVertexColor(accent[1] or GOLD[1],accent[2] or GOLD[2],accent[3] or GOLD[3],0.72)
    f._rpalPanelGlow:SetVertexColor(accent[1] or GOLD[1],accent[2] or GOLD[2],accent[3] or GOLD[3],0.78)
    f._rpalBurstGlow:SetVertexColor(accent[1] or GOLD[1],accent[2] or GOLD[2],accent[3] or GOLD[3],0.88)
    f._rpalValueText:SetTextColor(accent[1] or GOLD[1],accent[2] or GOLD[2],accent[3] or GOLD[3],1)
    f._rpalEyebrow:SetTextColor((entry.accent and (accent[1] or 1)) or MUTED[1],(entry.accent and (accent[2] or 1)) or MUTED[2],(entry.accent and (accent[3] or 1)) or MUTED[3],1)
    f._rpalEyebrow:SetText(entry.eyebrow or "TALENT TRIGGERED")
    f._rpalTitleText:SetText(entry.title or "Talent Triggered")
    if entry.largeValueText then
        f._rpalValueText:SetText(entry.valueText or "")
        f._rpalValueText:SetShown(true)
        f._rpalDetailText:SetText("")
        f._rpalDetailText:SetShown(false)
    else
        f._rpalValueText:SetText(entry.valueText or "+0 RENOWN")
        f._rpalValueText:SetShown(not entry.valueText)
        f._rpalDetailText:SetText(entry.valueText or "")
        f._rpalDetailText:SetShown(entry.valueText and true or false)
    end
    f._rpalPanelGlow:SetAlpha(0);f._rpalBurstGlow:SetAlpha(0)
    f:SetAlpha(0);f:SetScale(baseScale*0.96);f._age=0;f:Show()

    f:SetScript("OnUpdate",function(self,elapsed)
        self._age=(self._age or 0)+(elapsed or 0)
        local t=self._age
        local fade=math.min(1,t/0.20)
        local countT=math.max(0,math.min(1,(t-0.14)/0.88))
        local eased=countT*countT*countT
        if not entry.valueText then
            local shown=math.floor((amount*eased)+0.5)
            if shown>amount then shown=amount end
            self._rpalValueText:SetText("+"..tostring(shown).." RENOWN")
        end
        local tail=1
        if t>2.05 then tail=math.max(0,1-((t-2.05)/0.34)) end
        self:SetAlpha(fade*tail)
        self:SetScale(baseScale*(0.96+(fade*0.04)))
        self._rpalPanelGlow:SetAlpha((0.07+(countT*0.07))*tail)
        local pulse=(math.sin((t-0.70)*7)+1)*0.5
        self._rpalBurstGlow:SetAlpha(countT*(0.18+(pulse*0.22))*tail)
        if t>=2.40 then FinishQueuedToast(self) end
    end)
end

StartQueuedToast=function(entry)
    if not entry then return end
    if entry.kind=="level" then StartLevel(entry) else StartProc(entry) end
end


local function DMSuppressesProgressionUI()
    return RPAL.DMKit and RPAL.DMKit.ShouldSuppressProgressionUI and RPAL.DMKit.ShouldSuppressProgressionUI()
end

local function MergeDeferredRewards(dst,src)
    dst=dst or {};local seen={}
    for _,r in ipairs(dst) do seen[tostring(r.kind)..":"..tostring(r.name)]=true end
    for _,r in ipairs(src or {}) do local k=tostring(r.kind)..":"..tostring(r.name);if not seen[k] then dst[#dst+1]=r;seen[k]=true end end
    return dst
end

function RPAL.OnDMSessionBegan()
    -- DM play is presentation-authoritative: progression still accrues, but
    -- automatic progression UI must never interrupt the event.
    RPAL.ResetToastDirector()
end

function RPAL.OnDMSessionEnded()
    RPAL.ResetToastDirector()
    local deferred=RPAL._dmDeferredLevelToast;RPAL._dmDeferredLevelToast=nil
    if deferred then
        local function show()
            if DMSuppressesProgressionUI() then RPAL._dmDeferredLevelToast=deferred;return end
            RPAL.ShowLevelToast(deferred.level,deferred.rewards,deferred.oldLevel,deferred.source)
        end
        if C_Timer and C_Timer.After then C_Timer.After(.65,show) else show() end
    end
end

function RPAL.ShowLevelToast(level,rewards,oldLevel,source)
    if DMSuppressesProgressionUI() then
        local d=RPAL._dmDeferredLevelToast
        level=math.max(1,math.floor(tonumber(level) or 1));oldLevel=math.max(1,math.floor(tonumber(oldLevel) or (level-1)))
        if not d then d={level=level,oldLevel=oldLevel,source=source,rewards={}};RPAL._dmDeferredLevelToast=d end
        d.level=math.max(tonumber(d.level) or 1,level);d.oldLevel=math.min(tonumber(d.oldLevel) or oldLevel,oldLevel);d.source=source or d.source;d.rewards=MergeDeferredRewards(d.rewards,rewards)
        return
    end
    -- A level-up takes precedence over queued minor procs, but never interrupts
    -- an animation already on screen.
    EnqueueToast({kind="level",level=level,oldLevel=oldLevel,source=source,rewards=rewards or {}},true)
end

function RPAL.ShowTalentProcToast(title,amount,eyebrow)
    if DMSuppressesProgressionUI() then return end
    EnqueueToast({kind="proc",title=title,amount=amount,eyebrow=eyebrow})
end

function RPAL.ShowRewardToast(title,amount,eyebrow)
    RPAL.ShowTalentProcToast(title,amount,eyebrow or "REWARD")
end

function RPAL.ShowHearthstonePackRewardToast(amount,eyebrow)
    if DMSuppressesProgressionUI() then return end
    amount=math.max(0,math.floor(tonumber(amount) or 0))
    EnqueueToast({
        kind="proc",
        title="Hearthstone Packs Added",
        valueText="+"..tostring(amount).." PACK"..(amount==1 and "" or "S").." OWNED",
        largeValueText=true,
        eyebrow=eyebrow or "SEASON REWARD",
        accent=GOLD,
    },true)
end

function RPAL.ShowAchievementUnlockedToast(title)
    if DMSuppressesProgressionUI() then return end
    EnqueueToast({
        kind="proc",
        title=tostring(title or "Achievement Unlocked"),
        valueText="ACHIEVEMENT UNLOCKED",
        eyebrow="ODYN MILESTONE",
        soundKind="achievement",
    })
end

function RPAL.ShowDailyRPRewardToast(renown,sxp)
    if DMSuppressesProgressionUI() then return end
    renown=math.max(0,math.floor(tonumber(renown) or 5))
    sxp=math.max(0,math.floor(tonumber(sxp) or 0))
    local value="+"..tostring(renown).." RENOWN"
    if sxp>0 then value=value.."  •  +"..tostring(sxp).." SXP" end
    EnqueueToast({
        kind="proc",
        title="Daily RP Reward!",
        valueText=value,
        largeValueText=true,
        eyebrow="DAILY ROLEPLAY",
        accent=VIOLET,
    },true)
end

function RPAL.ShowSeasonQuestToast(scope,title,xp)
    if DMSuppressesProgressionUI() then return end
    scope=tostring(scope or "season"):lower()
    xp=math.max(0,math.floor(tonumber(xp) or 0))
    local eyebrow=(scope=="weekly") and "WEEKLY QUEST COMPLETE" or "DAILY QUEST COMPLETE"
    EnqueueToast({
        kind="proc",
        title=tostring(title or "Season Quest Complete"),
        valueText="+"..tostring(xp).." SXP",
        largeValueText=true,
        eyebrow=eyebrow,
        accent=VIOLET,
    },scope=="weekly")
end

function RPAL.ShowCosmeticRewardToast(rewards,eyebrow)
    if DMSuppressesProgressionUI() then return end
    local summary,count=RewardSummary(rewards,2)
    if count==0 then return end
    EnqueueToast({
        kind="proc",
        title=count>1 and "Collection Rewards Unlocked" or "Cosmetic Unlocked",
        valueText=summary,
        eyebrow=eyebrow or "COLLECTION REWARD",
    })
end

-- Talent.lua calls this hook for major proc rewards.
RPAL.OnTalentProc=function(title,amount)
    amount=tonumber(amount) or tonumber(tostring(amount or ""):match("(%d+)")) or 0
    RPAL.ShowTalentProcToast(title,amount,"TALENT TRIGGERED")
end

-- Layer the cinematic level-up presentation on the existing sound, progression
-- refresh and Atlas point refresh hooks.
local previousLevelUp=RPAL.OnRenownLevelUp
RPAL.OnRenownLevelUp=function(newLevel,oldLevel,source,rewards)
    if previousLevelUp then previousLevelUp(newLevel,oldLevel,source,rewards) end
    RPAL.ShowLevelToast(newLevel,rewards,oldLevel,source)
end
