GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.NewUICelebrate = addon.NewUICelebrate or {}

local page = addon.NewUICelebrate
page.State = page.State or {}

local refreshPage
local PANEL_ART_KEY = "MyToonsPanelBackground"
local PANEL_HEADER_KEY = "HomePanelHeader"
local CARD_ART_KEY = "MyToonsCardBackground"
local BUTTON_ART_KEY = "MyToonsButtonBackground"
local BUTTON_ACTIVE_ART_KEY = "MyToonsButtonSelected"

local function trimString(value)
    return tostring(value or ""):match("^%s*(.-)%s*$") or ""
end

local function shortPlayerName(value)
    return trimString(value):match("^[^-]+") or trimString(value)
end

local function formatTimestampText(timestamp)
    local resolved = tonumber(timestamp) or 0
    if resolved <= 0 then
        return "Unknown"
    end
    return date("%b %d %I:%M %p", resolved)
end

local function ApplyRect(frame, parent, rect)
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", parent, "TOPLEFT", tonumber(rect.x) or 0, tonumber(rect.y) or 0)
    frame:SetSize(tonumber(rect.w) or 1, tonumber(rect.h) or 1)
end

local function clearFrame(frame)
    if not frame then
        return
    end
    if frame.Hide then
        frame:Hide()
    end
    if frame.SetParent then
        frame:SetParent(nil)
    end
end

local function resolveTexturePath(textureKey)
    local textures = addon.NewUIAssets and addon.NewUIAssets.Textures or nil
    return textures and textures[textureKey] or nil
end

local function resolveArtMetrics(textureKey)
    local metrics = addon.NewUIAssets and addon.NewUIAssets.Metrics or nil
    return metrics and metrics[textureKey] or nil
end

local function applyArtTexture(texture, textureKey, fallbackColor)
    local texturePath = resolveTexturePath(textureKey)
    local metrics = resolveArtMetrics(textureKey)
    if texturePath then
        texture:SetTexture(texturePath)
        texture:SetVertexColor(1, 1, 1, 0.98)
        if metrics and type(metrics.texCoord) == "table" and #metrics.texCoord == 4 then
            texture:SetTexCoord(metrics.texCoord[1], metrics.texCoord[3], metrics.texCoord[2], metrics.texCoord[4])
        end
        return
    end
    texture:SetColorTexture(unpack(fallbackColor or { 0.10, 0.10, 0.12, 0.95 }))
end

local function createSurface(parent, rect)
    local frame = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(frame, parent, rect)
    if frame.SetBackdrop then
        frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        frame:SetBackdropColor(0.09, 0.10, 0.13, 0.94)
        frame:SetBackdropBorderColor(0.42, 0.33, 0.16, 0.92)
    end
    return frame
end

local function createPanel(parent, rect, titleText, metaText)
    local panel = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(panel, parent, rect)
    if panel.SetBackdrop then
        panel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        panel:SetBackdropColor(0, 0, 0, 0)
        panel:SetBackdropBorderColor(0, 0, 0, 0)
    end

    local background = panel:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(panel)
    applyArtTexture(background, PANEL_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local headerBand = panel:CreateTexture(nil, "ARTWORK")
    headerBand:SetPoint("TOPLEFT", panel, "TOPLEFT", 24, -14)
    headerBand:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -24, -14)
    headerBand:SetHeight(24)
    applyArtTexture(headerBand, PANEL_HEADER_KEY, { 0.08, 0.08, 0.10, 0.82 })

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("CENTER", headerBand, "CENTER", 0, -1)
    title:SetText(titleText or "")
    title:SetTextColor(0.98, 0.92, 0.76, 1)

    if trimString(metaText) ~= "" then
        local meta = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        meta:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -46)
        meta:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 28))
        meta:SetJustifyH("LEFT")
        meta:SetText(metaText)
        meta:SetTextColor(0.76, 0.72, 0.64, 1)
        panel.Meta = meta
    end

    panel.Title = title
    return panel
end

local function createButton(parent, rect, labelText, onClick, isActive)
    local button = CreateFrame("Button", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(button, parent, rect)
    local background = button:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(button)
    applyArtTexture(background, isActive and BUTTON_ACTIVE_ART_KEY or BUTTON_ART_KEY, isActive and { 0.20, 0.23, 0.31, 0.98 } or { 0.10, 0.11, 0.14, 0.95 })
    button.Background = background

    local text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("CENTER", button, "CENTER", 0, 0)
    text:SetText(labelText or "")
    text:SetTextColor(isActive and 0.98 or 0.96, isActive and 0.88 or 0.93, isActive and 0.58 or 0.84, 1)
    button.Text = text
    button:SetScript("OnClick", function()
        if onClick then
            onClick(button)
        end
    end)
    return button
end

local function createCard(parent, rect, titleText, valueText, subtitleText, backgroundTextureKey)
    local card = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(card, parent, rect)

    local background = card:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(card)
    applyArtTexture(background, backgroundTextureKey or CARD_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    title:SetPoint("TOP", card, "TOP", 0, -16)
    title:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
    title:SetJustifyH("CENTER")
    title:SetText(titleText or "")
    title:SetTextColor(0.78, 0.74, 0.66, 1)

    local value = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    value:SetPoint("TOP", title, "BOTTOM", 0, -8)
    value:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
    value:SetJustifyH("CENTER")
    value:SetText(valueText or "-")
    value:SetTextColor(0.96, 0.82, 0.34, 1)

    if trimString(subtitleText) ~= "" then
        local subtitle = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        subtitle:SetPoint("BOTTOM", card, "BOTTOM", 0, 12)
        subtitle:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
        subtitle:SetJustifyH("CENTER")
        subtitle:SetText(subtitleText)
        subtitle:SetTextColor(0.72, 0.68, 0.58, 1)
        card.Subtitle = subtitle
    end

    card.Title = title
    card.Value = value
    return card
end

local function createInput(parent, rect, initialText)
    local host = createSurface(parent, rect)
    local input = CreateFrame("EditBox", nil, host, "InputBoxTemplate")
    if input.Left then input.Left:SetAlpha(0) end
    if input.Middle then input.Middle:SetAlpha(0) end
    if input.Right then input.Right:SetAlpha(0) end
    input:SetAutoFocus(false)
    input:SetFontObject("GameFontHighlight")
    input:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -4)
    input:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -8, 4)
    input:SetText(initialText or "")
    host.Input = input
    return host
end

local function createMultilineInput(parent, rect, initialText)
    local host = createSurface(parent, rect)
    local scrollFrame = CreateFrame("ScrollFrame", nil, host, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -8)
    scrollFrame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -28, 8)

    local input = CreateFrame("EditBox", nil, scrollFrame)
    input:SetMultiLine(true)
    input:SetAutoFocus(false)
    input:SetFontObject("ChatFontNormal")
    input:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
    input:SetWidth(math.max((tonumber(rect.w) or 1) - 40, 20))
    input:SetHeight(math.max((tonumber(rect.h) or 1) - 24, 40))
    if input.SetJustifyH then
        input:SetJustifyH("LEFT")
    end
    if input.SetJustifyV then
        input:SetJustifyV("TOP")
    end
    if input.SetMaxLetters then
        input:SetMaxLetters(0)
    end
    input:SetText(initialText or "")
    input:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    scrollFrame:SetScrollChild(input)
    host.Input = input
    return host
end

local function createReadOnlyMultiline(parent, rect, textValue)
    local host = createSurface(parent, rect)
    local scrollFrame = CreateFrame("ScrollFrame", nil, host, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -8)
    scrollFrame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -28, 8)

    local editBox = CreateFrame("EditBox", nil, scrollFrame, "InputBoxTemplate")
    if editBox.Left then editBox.Left:SetAlpha(0) end
    if editBox.Middle then editBox.Middle:SetAlpha(0) end
    if editBox.Right then editBox.Right:SetAlpha(0) end
    editBox:SetMultiLine(true)
    editBox:SetAutoFocus(false)
    editBox:SetFontObject("GameFontHighlightSmall")
    editBox:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 40))
    editBox:SetText(textValue or "")
    if editBox.SetCursorPosition then
        editBox:SetCursorPosition(0)
    end
    editBox:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)
    local textHeight = 0
    if editBox.GetStringHeight then
        textHeight = tonumber(editBox:GetStringHeight()) or 0
    end
    editBox:SetHeight(math.max((tonumber(rect.h) or 1) - 16, math.floor(textHeight + 16)))
    scrollFrame:SetScrollChild(editBox)
    host.EditBox = editBox
    return host
end

local function createCheckbox(parent, x, y, labelText, checked, onChanged)
    local checkbox = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    checkbox:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    checkbox:SetChecked(checked and true or false)
    checkbox:SetScript("OnClick", function(self)
        if onChanged then
            onChanged(self:GetChecked() and true or false)
        end
    end)

    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("LEFT", checkbox, "RIGHT", 2, 1)
    label:SetText(labelText or "")
    label:SetTextColor(0.82, 0.80, 0.76, 1)
    checkbox.Label = label
    return checkbox
end

local function createMessageRow(parent, width, y, textValue, selected, onClick)
    local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
    row:SetSize(width, 26)
    row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    if selected then
        row:SetBackdropColor(0.16, 0.20, 0.29, 0.98)
        row:SetBackdropBorderColor(0.82, 0.66, 0.24, 1)
    else
        row:SetBackdropColor(0.06, 0.09, 0.13, 0.96)
        row:SetBackdropBorderColor(0.30, 0.38, 0.48, 1)
    end
    row:SetScript("OnClick", function()
        if onClick then
            onClick()
        end
    end)

    local rowText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    rowText:SetPoint("LEFT", row, "LEFT", 8, 0)
    rowText:SetWidth(width - 16)
    rowText:SetJustifyH("LEFT")
    rowText:SetText(textValue or "")
    rowText:SetTextColor(0.82, 0.80, 0.76, 1)
    return row
end

local function renderCardRow(parent, rect, definitions)
    local gap = tonumber(rect.gap) or 8
    local width = tonumber(rect.w) or 1120
    local height = tonumber(rect.h) or 94
    local cardWidth = math.floor((width - (gap * math.max(#definitions - 1, 0))) / math.max(#definitions, 1))

    for index, definition in ipairs(definitions) do
        createCard(parent, {
            x = (tonumber(rect.x) or 20) + ((cardWidth + gap) * (index - 1)),
            y = tonumber(rect.y) or -92,
            w = cardWidth,
            h = height,
        }, definition.title, definition.value, definition.subtitle, definition.background)
    end
end

local function renderGratsUPane(root, celebrateLayout, parent, snapshot)
    local service = addon.GratsUService
    local state = page.State
    if not service then
        return createPanel(root, { x = 20, y = -92, w = 720, h = 180 }, "GratsU Disabled", "The GratsU service is not loaded right now, so this view is temporarily unavailable.")
    end

    if not service:CanAccess() then
        local restricted = root:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        restricted:SetPoint("TOPLEFT", root, "TOPLEFT", 20, -100)
        restricted:SetText("GratsU is only available to officers and the guild master.")
        restricted:SetTextColor(0.82, 0.80, 0.76, 1)
        return restricted
    end

    local settings = service:GetSettings()
    local summary = service:GetSummary()
    local activeTab = state.gratsUTab == "welcome" and "welcome" or "achievement"
    local kindLabel = activeTab == "welcome" and "Welcome" or "Achievement"
    local messages = service:GetMessages(activeTab)
    local selectedIndex = tonumber(state.gratsUSelectedIndex)
    local activePeerNames = summary.activePeerNames or {}
    local peerSummaryText = #activePeerNames > 0 and table.concat(activePeerNames, "\n") or "No other active officer or guild master peers detected."
    local lastSentText = summary.lastSentAt and summary.lastSentAt > 0 and formatTimestampText(summary.lastSentAt) or "No automated messages sent yet"

    if not selectedIndex or selectedIndex < 1 or selectedIndex > #messages then
        selectedIndex = #messages > 0 and 1 or nil
        state.gratsUSelectedIndex = selectedIndex
    end

    if state.gratsUDraftTab ~= activeTab or state.gratsUDraftIndex ~= selectedIndex then
        state.gratsUDraft = selectedIndex and (messages[selectedIndex] or "") or ""
        state.gratsUDraftTab = activeTab
        state.gratsUDraftIndex = selectedIndex
    end

    renderCardRow(root, celebrateLayout.Cards or { x = 20, y = -92, w = 1120, h = 94, gap = 10 }, {
        { title = "Access", value = summary.access, subtitle = "Visible only to officers and the guild master", background = "HomeCardBackground" },
        { title = "Peers", value = tostring(summary.activePeerCount or 0), subtitle = "Coordinated senders active in the guild", background = "HomeMembersCardBackground" },
        { title = "Achievements", value = settings.enableAchievement == false and "Off" or "On", subtitle = string.format("%d templates | %ds cooldown", summary.achievementCount or 0, settings.achievementCooldownSeconds or 120), background = "HomeMythicCardBackground" },
        { title = "Welcomes", value = settings.enableWelcome == false and "Off" or "On", subtitle = string.format("%d templates | %ds offline gate", summary.welcomeCount or 0, settings.minOfflineSeconds or 120), background = "HomeNewsCardBackground" },
    })

    local settingsPanel = createPanel(root, celebrateLayout.Settings or { x = 20, y = -204, w = 320, h = 512 }, "Automation Settings", "GratsU coordinates across active officer and GM clients so only one sender answers each event.")
    createCheckbox(settingsPanel, 12, -72, "Enable welcome messages", settings.enableWelcome ~= false, function(checked)
        service:UpdateSettings({ enableWelcome = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -98, "Enable achievement congrats", settings.enableAchievement ~= false, function(checked)
        service:UpdateSettings({ enableAchievement = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -124, "Ignore my own events", settings.ignoreSelf ~= false, function(checked)
        service:UpdateSettings({ ignoreSelf = checked })
        refreshPage(parent, snapshot)
    end)

    local function createSettingRow(parentPanel, labelText, value, y)
        local label = parentPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOPLEFT", parentPanel, "TOPLEFT", 14, y)
        label:SetWidth(150)
        label:SetJustifyH("LEFT")
        label:SetText(labelText)
        label:SetTextColor(0.82, 0.80, 0.76, 1)

        return createInput(parentPanel, { x = 170, y = y + 10, w = 110, h = 22 }, tostring(value or 0)).Input
    end

    local antiSpamInput = createSettingRow(settingsPanel, "Anti-Spam (sec)", settings.antiSpamSeconds, -166)
    local minOfflineInput = createSettingRow(settingsPanel, "Offline Gate (sec)", settings.minOfflineSeconds, -196)
    local achievementCooldownInput = createSettingRow(settingsPanel, "Achieve CD (sec)", settings.achievementCooldownSeconds, -226)
    local welcomeDelayInput = createSettingRow(settingsPanel, "Welcome Delay (sec)", settings.welcomeDelaySeconds, -256)

    createButton(settingsPanel, { x = 14, y = -292, w = 134, h = 24 }, "Save Settings", function()
        service:UpdateSettings({
            antiSpamSeconds = antiSpamInput:GetText(),
            minOfflineSeconds = minOfflineInput:GetText(),
            achievementCooldownSeconds = achievementCooldownInput:GetText(),
            welcomeDelaySeconds = welcomeDelayInput:GetText(),
        })
        refreshPage(parent, snapshot)
    end, false)
    createButton(settingsPanel, { x = 156, y = -292, w = 134, h = 24 }, "Restore All", function()
        service:ResetAllDefaults()
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)

    local statsLabel = settingsPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    statsLabel:SetPoint("TOPLEFT", settingsPanel, "TOPLEFT", 14, -340)
    statsLabel:SetText("Activity")
    local statsText = settingsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    statsText:SetPoint("TOPLEFT", settingsPanel, "TOPLEFT", 14, -362)
    statsText:SetWidth(278)
    statsText:SetJustifyH("LEFT")
    statsText:SetText(string.format("Welcomes sent: %d\nAchievement congrats sent: %d\nLast automated send: %s", summary.stats and summary.stats.welcomeSent or 0, summary.stats and summary.stats.achievementSent or 0, lastSentText))
    statsText:SetTextColor(0.76, 0.72, 0.64, 1)

    local peersLabel = settingsPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    peersLabel:SetPoint("TOPLEFT", settingsPanel, "TOPLEFT", 14, -418)
    peersLabel:SetText("Active Peers")
    createReadOnlyMultiline(settingsPanel, { x = 14, y = -442, w = 278, h = 54 }, peerSummaryText)

    local listPanel = createPanel(root, celebrateLayout.Library or { x = 356, y = -204, w = 320, h = 512 }, "Message Library", "")
    createButton(listPanel, { x = 14, y = -36, w = 136, h = 22 }, activeTab == "achievement" and "Achievements *" or "Achievements", function()
        state.gratsUTab = "achievement"
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, activeTab == "achievement")
    createButton(listPanel, { x = 158, y = -36, w = 136, h = 22 }, activeTab == "welcome" and "Welcomes *" or "Welcomes", function()
        state.gratsUTab = "welcome"
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, activeTab == "welcome")

    local countLabel = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countLabel:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -64)
    countLabel:SetWidth(280)
    countLabel:SetJustifyH("LEFT")
    countLabel:SetText(string.format("%s templates: %d / 50", kindLabel, #messages))
    countLabel:SetTextColor(0.76, 0.72, 0.64, 1)

    local listScrollFrame = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listScrollFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -88)
    listScrollFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -30, 14)
    local listContent = CreateFrame("Frame", nil, listScrollFrame)
    listContent:SetSize(258, math.max(#messages * 30, 1))
    listScrollFrame:SetScrollChild(listContent)

    if #messages == 0 then
        local emptyText = listContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        emptyText:SetPoint("TOPLEFT", listContent, "TOPLEFT", 0, 0)
        emptyText:SetWidth(236)
        emptyText:SetJustifyH("LEFT")
        emptyText:SetText("No messages are configured for this tab.")
        emptyText:SetTextColor(0.76, 0.72, 0.64, 1)
    end

    for index, messageText in ipairs(messages) do
        createMessageRow(listContent, 258, -((index - 1) * 30), messageText, index == selectedIndex, function()
            state.gratsUSelectedIndex = index
            state.gratsUDraft = messageText
            state.gratsUDraftTab = activeTab
            state.gratsUDraftIndex = index
            refreshPage(parent, snapshot)
        end)
    end

    local editorPanel = createPanel(root, celebrateLayout.Editor or { x = 692, y = -204, w = 340, h = 512 }, kindLabel .. " Editor", selectedIndex and string.format("Editing %s message %d of %d", string.lower(kindLabel), selectedIndex, #messages) or string.format("Create a new %s message", string.lower(kindLabel)))
    local editorHost = createMultilineInput(editorPanel, { x = 14, y = -58, w = 300, h = 196 }, state.gratsUDraft or "")
    local editorInput = editorHost.Input
    editorInput:SetScript("OnTextChanged", function(input)
        state.gratsUDraft = input:GetText()
        state.gratsUDraftTab = activeTab
        state.gratsUDraftIndex = selectedIndex
    end)

    createButton(editorPanel, { x = 14, y = -264, w = 142, h = 24 }, "Save Message", function()
        local ok, errorMessage
        if selectedIndex then
            ok, errorMessage = service:UpdateMessage(activeTab, selectedIndex, state.gratsUDraft)
        else
            ok, errorMessage = service:AddMessage(activeTab, state.gratsUDraft)
            if ok then
                state.gratsUSelectedIndex = #service:GetMessages(activeTab)
            end
        end
        if not ok then
            addon:Print(errorMessage)
            return
        end
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 172, y = -264, w = 142, h = 24 }, "New Message", function()
        state.gratsUSelectedIndex = nil
        state.gratsUDraft = ""
        state.gratsUDraftTab = activeTab
        state.gratsUDraftIndex = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 14, y = -296, w = 142, h = 24 }, "Delete Message", function()
        local ok, errorMessage = service:DeleteMessage(activeTab, selectedIndex)
        if not ok then
            addon:Print(errorMessage)
            return
        end
        local remaining = service:GetMessages(activeTab)
        state.gratsUSelectedIndex = math.min(selectedIndex or 1, #remaining)
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 172, y = -296, w = 142, h = 24 }, "Restore Tab", function()
        service:ResetMessages(activeTab)
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)

    local previewLabel = editorPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    previewLabel:SetPoint("TOPLEFT", editorPanel, "TOPLEFT", 14, -336)
    previewLabel:SetText("Guild Chat Preview")
    createReadOnlyMultiline(editorPanel, { x = 14, y = -360, w = 300, h = 116 }, service:GetPreview(activeTab, state.gratsUDraft))
end

local function renderCelebrationsPane(root, celebrateLayout, parent, snapshot)
    local service = addon.ToonCelebrationService
    local state = page.State
    if not service then
        return createPanel(root, { x = 20, y = -92, w = 720, h = 180 }, "Toon Celebrations Disabled", "The Toon Celebrations service is not loaded right now, so this view is temporarily unavailable.")
    end

    if not service:CanAccess() then
        local restricted = root:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        restricted:SetPoint("TOPLEFT", root, "TOPLEFT", 20, -100)
        restricted:SetText("Toon Celebrations is only available to officers and the guild master.")
        restricted:SetTextColor(0.82, 0.80, 0.76, 1)
        return restricted
    end

    local settings = service:GetSettings()
    local summary = service:GetSummary(settings.lookbackDays, settings.lookaheadDays)
    local entries = service:GetCelebrationWindow(settings.lookbackDays, settings.lookaheadDays)
    local activeTab = state.celebrationsTab or "birthday"
    local activeTiming = state.celebrationsTimingTab or "today"
    local messages = service:GetMessages(activeTab, activeTiming)
    local selectedIndex = tonumber(state.celebrationsSelectedIndex)
    local tabLabels = {
        birthday = "Birthdays",
        ["anniversary-month"] = "1 Month",
        ["anniversary-six-month"] = "6 Months",
        ["anniversary-year"] = "Yearly",
    }
    local timingLabels = {
        upcoming = "Upcoming",
        today = "Today",
        recent = "Just Missed",
    }
    local editorLabels = {
        birthday = "Birthday",
        ["anniversary-month"] = "1 Month Anniversary",
        ["anniversary-six-month"] = "6 Month Anniversary",
        ["anniversary-year"] = "Yearly Anniversary",
    }
    local activeLabel = editorLabels[activeTab] or "Birthday"
    local previewYears = activeTab == "anniversary-year" and 3 or 0
    local previewOffset = activeTiming == "upcoming" and 1 or (activeTiming == "recent" and -1 or 0)

    if not selectedIndex or selectedIndex < 1 or selectedIndex > #messages then
        selectedIndex = #messages > 0 and 1 or nil
        state.celebrationsSelectedIndex = selectedIndex
    end

    if state.celebrationDraftKind ~= string.format("%s|%s", activeTab, activeTiming) or state.celebrationDraftIndex ~= selectedIndex then
        state.celebrationDraft = selectedIndex and (messages[selectedIndex] or "") or ""
        state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
        state.celebrationDraftIndex = selectedIndex
    end

    renderCardRow(root, celebrateLayout.Cards or { x = 20, y = -92, w = 1120, h = 94, gap = 10 }, {
        { title = "Guild", value = summary.guildKey or "Unknown", subtitle = "Current live guild for celebration review and sending", background = "HomeCardBackground" },
        { title = "Window", value = string.format("-%d / +%d", settings.lookbackDays or 7, settings.lookaheadDays or 14), subtitle = string.format("%d recent | %d today | %d upcoming", summary.recent or 0, summary.today or 0, summary.upcoming or 0), background = "HomeMembersCardBackground" },
        { title = "Reminder", value = settings.enableBirthday == false and settings.enableAnniversary == false and "Off" or "On", subtitle = string.format("Login note only | Sent %d", summary.sent or 0), background = "HomeMythicCardBackground" },
        { title = "Templates", value = tostring((summary.birthdayCount or 0) + (summary.anniversaryMonthCount or 0) + (summary.anniversarySixMonthCount or 0) + (summary.anniversaryYearCount or 0)), subtitle = "Birthday and anniversary message library entries", background = "HomeNewsCardBackground" },
    })

    local settingsPanel = createPanel(root, celebrateLayout.Settings or { x = 20, y = -204, w = 320, h = 352 }, "Reminder Settings", "Only officers and the guild master can manage celebration reminders for the current guild. GH3 now reminds you after login instead of auto-sending.")
    createCheckbox(settingsPanel, 12, -72, "Enable birthday messages", settings.enableBirthday ~= false, function(checked)
        service:UpdateSettings({ enableBirthday = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -98, "Enable anniversary messages", settings.enableAnniversary ~= false, function(checked)
        service:UpdateSettings({ enableAnniversary = checked })
        refreshPage(parent, snapshot)
    end)

    local function createSettingRow(parentPanel, labelText, value, y)
        local label = parentPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOPLEFT", parentPanel, "TOPLEFT", 14, y)
        label:SetWidth(150)
        label:SetJustifyH("LEFT")
        label:SetText(labelText)
        label:SetTextColor(0.82, 0.80, 0.76, 1)

        return createInput(parentPanel, { x = 170, y = y + 10, w = 110, h = 22 }, tostring(value or 0)).Input
    end

    local lookbackInput = createSettingRow(settingsPanel, "Recent Window", settings.lookbackDays, -140)
    local lookaheadInput = createSettingRow(settingsPanel, "Upcoming Window", settings.lookaheadDays, -170)
    createButton(settingsPanel, { x = 14, y = -236, w = 134, h = 24 }, "Save Settings", function()
        service:UpdateSettings({ lookbackDays = lookbackInput:GetText(), lookaheadDays = lookaheadInput:GetText() })
        refreshPage(parent, snapshot)
    end, false)
    createButton(settingsPanel, { x = 156, y = -236, w = 134, h = 24 }, "Restore All", function()
        service:ResetAllDefaults()
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)

    local statsLabel = settingsPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    statsLabel:SetPoint("TOPLEFT", settingsPanel, "TOPLEFT", 14, -274)
    statsLabel:SetText("Activity")
    local statsText = settingsPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    statsText:SetPoint("TOPLEFT", settingsPanel, "TOPLEFT", 14, -296)
    statsText:SetWidth(278)
    statsText:SetJustifyH("LEFT")
    statsText:SetText(string.format("Birthdays sent: %d\n1 month sent: %d\n6 month sent: %d\nYearly sent: %d", summary.stats and summary.stats.birthdaySent or 0, summary.stats and summary.stats.anniversaryMonthSent or 0, summary.stats and summary.stats.anniversarySixMonthSent or 0, summary.stats and summary.stats.anniversaryYearSent or 0))
    statsText:SetTextColor(0.76, 0.72, 0.64, 1)

    local listPanel = createPanel(root, celebrateLayout.Library or { x = 356, y = -204, w = 320, h = 352 }, "Message Library", "")
    createButton(listPanel, { x = 14, y = -36, w = 136, h = 22 }, activeTab == "birthday" and "Birthdays *" or "Birthdays", function() state.celebrationsTab = "birthday"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTab == "birthday")
    createButton(listPanel, { x = 158, y = -36, w = 136, h = 22 }, activeTab == "anniversary-month" and "1 Month *" or "1 Month", function() state.celebrationsTab = "anniversary-month"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTab == "anniversary-month")
    createButton(listPanel, { x = 14, y = -64, w = 136, h = 22 }, activeTab == "anniversary-six-month" and "6 Months *" or "6 Months", function() state.celebrationsTab = "anniversary-six-month"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTab == "anniversary-six-month")
    createButton(listPanel, { x = 158, y = -64, w = 136, h = 22 }, activeTab == "anniversary-year" and "Yearly *" or "Yearly", function() state.celebrationsTab = "anniversary-year"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTab == "anniversary-year")
    createButton(listPanel, { x = 14, y = -94, w = 84, h = 22 }, activeTiming == "upcoming" and "Upcoming *" or "Upcoming", function() state.celebrationsTimingTab = "upcoming"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTiming == "upcoming")
    createButton(listPanel, { x = 106, y = -94, w = 76, h = 22 }, activeTiming == "today" and "Today *" or "Today", function() state.celebrationsTimingTab = "today"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTiming == "today")
    createButton(listPanel, { x = 190, y = -94, w = 104, h = 22 }, activeTiming == "recent" and "Just Missed *" or "Just Missed", function() state.celebrationsTimingTab = "recent"; state.celebrationsSelectedIndex = 1; state.celebrationDraftKind = nil; refreshPage(parent, snapshot) end, activeTiming == "recent")

    local countLabel = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countLabel:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -124)
    countLabel:SetWidth(280)
    countLabel:SetJustifyH("LEFT")
    countLabel:SetText(string.format("%s %s templates: %d / 50", timingLabels[activeTiming] or "Today", tabLabels[activeTab] or "Messages", #messages))
    countLabel:SetTextColor(0.76, 0.72, 0.64, 1)

    local listScrollFrame = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listScrollFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -148)
    listScrollFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -30, 14)
    local listContent = CreateFrame("Frame", nil, listScrollFrame)
    listContent:SetSize(258, math.max(#messages * 30, 1))
    listScrollFrame:SetScrollChild(listContent)

    if #messages == 0 then
        local emptyText = listContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        emptyText:SetPoint("TOPLEFT", listContent, "TOPLEFT", 0, 0)
        emptyText:SetWidth(236)
        emptyText:SetJustifyH("LEFT")
        emptyText:SetText("No messages are configured for this celebration type.")
        emptyText:SetTextColor(0.76, 0.72, 0.64, 1)
    end

    for index, messageText in ipairs(messages) do
        createMessageRow(listContent, 258, -((index - 1) * 30), messageText, index == selectedIndex, function()
            state.celebrationsSelectedIndex = index
            state.celebrationDraft = messageText
            state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
            state.celebrationDraftIndex = index
            refreshPage(parent, snapshot)
        end)
    end

    local editorPanel = createPanel(root, celebrateLayout.Editor or { x = 692, y = -204, w = 340, h = 352 }, activeLabel .. " Editor", selectedIndex and string.format("Editing %s %s message %d of %d", string.lower(timingLabels[activeTiming] or "today"), string.lower(activeLabel), selectedIndex, #messages) or string.format("Create a new %s %s message", string.lower(timingLabels[activeTiming] or "today"), string.lower(activeLabel)))
    local editorHost = createMultilineInput(editorPanel, { x = 14, y = -58, w = 300, h = 138 }, state.celebrationDraft or "")
    local editorInput = editorHost.Input
    editorInput:SetScript("OnTextChanged", function(input)
        state.celebrationDraft = input:GetText()
        state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
        state.celebrationDraftIndex = selectedIndex
    end)

    createButton(editorPanel, { x = 14, y = -206, w = 142, h = 24 }, "Save Message", function()
        local ok, errorMessage
        if selectedIndex then
            ok, errorMessage = service:UpdateMessage(activeTab, activeTiming, selectedIndex, state.celebrationDraft)
        else
            ok, errorMessage = service:AddMessage(activeTab, activeTiming, state.celebrationDraft)
            if ok then
                state.celebrationsSelectedIndex = #service:GetMessages(activeTab, activeTiming)
            end
        end
        if not ok then
            addon:Print(errorMessage)
            return
        end
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 172, y = -206, w = 142, h = 24 }, "New Message", function()
        state.celebrationsSelectedIndex = nil
        state.celebrationDraft = ""
        state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
        state.celebrationDraftIndex = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 14, y = -238, w = 142, h = 24 }, "Delete Message", function()
        local ok, errorMessage = service:DeleteMessage(activeTab, activeTiming, selectedIndex)
        if not ok then
            addon:Print(errorMessage)
            return
        end
        local remaining = service:GetMessages(activeTab, activeTiming)
        state.celebrationsSelectedIndex = math.min(selectedIndex or 1, #remaining)
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 172, y = -238, w = 142, h = 24 }, "Restore Tab", function()
        service:ResetMessages(activeTab, activeTiming)
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)

    local previewLabel = editorPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    previewLabel:SetPoint("TOPLEFT", editorPanel, "TOPLEFT", 14, -276)
    previewLabel:SetText("Guild Chat Preview")
    createReadOnlyMultiline(editorPanel, { x = 14, y = -300, w = 300, h = 46 }, service:GetPreview(activeTab, activeTiming, state.celebrationDraft, previewYears, previewOffset))

    local queueRect = celebrateLayout.Queue or { x = 20, y = -572, w = 1012, h = 150 }
    local queuePanel = createPanel(root, queueRect, "Celebration Queue", string.format("Showing %d entries for %s across the configured recent and upcoming window. Use Send to post to guild chat or Ignore to clear that date from the queue.", #entries, summary.guildKey or "Unknown guild"))
    queuePanel:SetFrameLevel(math.max((queuePanel:GetFrameLevel() or 0), (root:GetFrameLevel() or 0) + 10))
    local queueScrollFrame = CreateFrame("ScrollFrame", nil, queuePanel, "UIPanelScrollFrameTemplate")
    queueScrollFrame:SetPoint("TOPLEFT", queuePanel, "TOPLEFT", 14, -86)
    queueScrollFrame:SetPoint("BOTTOMRIGHT", queuePanel, "BOTTOMRIGHT", -30, 10)
    local queueContent = CreateFrame("Frame", nil, queueScrollFrame)
    queueContent:SetWidth((tonumber(queueRect.w) or 1012) - 44)
    queueContent:SetHeight(1)
    queueScrollFrame:SetScrollChild(queueContent)

    if #entries == 0 then
        local empty = queueContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        empty:SetPoint("TOPLEFT", queueContent, "TOPLEFT", 14, -8)
        empty:SetWidth((tonumber(queueRect.w) or 1012) - 80)
        empty:SetJustifyH("LEFT")
        empty:SetText("No recent or upcoming birthdays or guild anniversaries were found for your toons in the current guild.")
        empty:SetTextColor(0.82, 0.80, 0.76, 1)
        queueContent:SetHeight(60)
        return
    end

    local rowHeight = 42
    local rowPitch = 48
    for index, entry in ipairs(entries) do
        local row = CreateFrame("Frame", nil, queueContent, "BackdropTemplate")
        row:SetPoint("TOPLEFT", queueContent, "TOPLEFT", 0, -((index - 1) * rowPitch))
        row:SetSize((tonumber(queueRect.w) or 1012) - 44, rowHeight)
        row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        row:SetBackdropColor(0.06, 0.09, 0.13, 0.96)
        row:SetBackdropBorderColor(0.30, 0.38, 0.48, 1)

        local toonLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        toonLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 8, -7)
        toonLabel:SetWidth(170)
        toonLabel:SetJustifyH("LEFT")
        toonLabel:SetText(entry.memberName or entry.memberId or "Unknown")
        toonLabel:SetTextColor(0.82, 0.80, 0.76, 1)

        local kindLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        kindLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 186, -7)
        kindLabel:SetWidth(148)
        kindLabel:SetJustifyH("LEFT")
        kindLabel:SetText(entry.kindLabel or entry.kind or "Celebration")
        kindLabel:SetTextColor(0.82, 0.80, 0.76, 1)

        local dateLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        dateLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 342, -8)
        dateLabel:SetWidth(96)
        dateLabel:SetJustifyH("LEFT")
        dateLabel:SetText(entry.dateLabel or entry.dayKey or "Unknown")
        dateLabel:SetTextColor(0.76, 0.72, 0.64, 1)

        local timingLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        timingLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 446, -8)
        timingLabel:SetWidth(96)
        timingLabel:SetJustifyH("LEFT")
        timingLabel:SetText(entry.timingLabel or "Today")
        timingLabel:SetTextColor(0.76, 0.72, 0.64, 1)

        local statusLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        statusLabel:SetPoint("TOPLEFT", row, "TOPLEFT", 550, -8)
        statusLabel:SetWidth(74)
        statusLabel:SetJustifyH("LEFT")
        statusLabel:SetText(entry.sent and "Sent" or "Ready")
        statusLabel:SetTextColor(0.76, 0.72, 0.64, 1)

        local sendButton = createButton(row, { x = 632, y = -10, w = 64, h = 22 }, entry.sent and "Sent" or "Send", function()
            local ok, message = service:SendManualCelebration(entry)
            if ok then
                addon:Print("Celebration message sent.")
                refreshPage(parent, snapshot)
            elseif message then
                addon:Print(message)
            end
        end, false)
        local ignoreButton = createButton(row, { x = 706, y = -10, w = 64, h = 22 }, "Ignore", function()
            local ok, message = service:IgnoreCelebration(entry)
            if ok then
                addon:Print("Celebration ignored for that date.")
                refreshPage(parent, snapshot)
            elseif message then
                addon:Print(message)
            end
        end, false)
        if entry.sent then
            sendButton:Disable()
            ignoreButton:Disable()
        end
    end

    queueContent:SetHeight(math.max(1, (#entries * rowPitch) - (rowPitch - rowHeight)))
end

refreshPage = function(parent, snapshot)
    if page._rootFrame then
        clearFrame(page._rootFrame)
        page._rootFrame = nil
    end
    page:Render(parent, snapshot)
end

function page:Render(parent, snapshot)
    local celebrateLayout = addon.NewUILayout and addon.NewUILayout.Celebrate or nil
    local root = CreateFrame("Frame", nil, parent)
    ApplyRect(root, parent, celebrateLayout and celebrateLayout.Page or { x = 0, y = 0, w = tonumber(parent:GetWidth()) or 1380, h = tonumber(parent:GetHeight()) or 742 })
    page._rootFrame = root

    local tabs = celebrateLayout and celebrateLayout.Tabs or {}
    local activePane = page.State.activePane == "tooncelebrations" and "tooncelebrations" or "gratsu"
    createButton(root, tabs.GratsU or { x = 20, y = -52, w = 112, h = 28 }, "GratsU", function()
        page.State.activePane = "gratsu"
        refreshPage(parent, snapshot)
    end, activePane == "gratsu")
    createButton(root, tabs.ToonCelebrations or { x = 140, y = -52, w = 188, h = 28 }, "Toon Celebrations", function()
        page.State.activePane = "tooncelebrations"
        refreshPage(parent, snapshot)
    end, activePane == "tooncelebrations")

    if activePane == "tooncelebrations" then
        renderCelebrationsPane(root, celebrateLayout and celebrateLayout.ToonCelebrations or {}, parent, snapshot)
    else
        renderGratsUPane(root, celebrateLayout and celebrateLayout.GratsU or {}, parent, snapshot)
    end

    return root
end

return page
--[[
GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.NewUICelebrate = addon.NewUICelebrate or {}

local page = addon.NewUICelebrate
page.State = page.State or {}

local refreshPage

local PANEL_ART_KEY = "MyToonsPanelBackground"
local PANEL_HEADER_KEY = "HomePanelHeader"
local CARD_ART_KEY = "MyToonsCardBackground"
local BUTTON_ART_KEY = "MyToonsButtonBackground"
local BUTTON_ACTIVE_ART_KEY = "MyToonsButtonSelected"

local function trimString(value)
    return tostring(value or ""):match("^%s*(.-)%s*$") or ""
end

local function shortPlayerName(value)
    return trimString(value):match("^[^-]+") or trimString(value)
end

local function formatTimestampText(timestamp)
    local resolved = tonumber(timestamp) or 0
    if resolved <= 0 then
        return "Unknown"
    end
    return date("%b %d %I:%M %p", resolved)
end

local function ApplyRect(frame, parent, rect)
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", parent, "TOPLEFT", tonumber(rect.x) or 0, tonumber(rect.y) or 0)
    frame:SetSize(tonumber(rect.w) or 1, tonumber(rect.h) or 1)
end

local function clearFrame(frame)
    if not frame then
        return
    end
    if frame.Hide then
        frame:Hide()
    end
    if frame.SetParent then
        frame:SetParent(nil)
    end
end

local function resolveTexturePath(textureKey)
    local textures = addon.NewUIAssets and addon.NewUIAssets.Textures or nil
    return textures and textures[textureKey] or nil
end

local function resolveArtMetrics(textureKey)
    local metrics = addon.NewUIAssets and addon.NewUIAssets.Metrics or nil
    return metrics and metrics[textureKey] or nil
end

local function applyArtTexture(texture, textureKey, fallbackColor)
    local texturePath = resolveTexturePath(textureKey)
    local metrics = resolveArtMetrics(textureKey)
    if texturePath then
        texture:SetTexture(texturePath)
        texture:SetVertexColor(1, 1, 1, 0.98)
        if metrics and type(metrics.texCoord) == "table" and #metrics.texCoord == 4 then
            texture:SetTexCoord(metrics.texCoord[1], metrics.texCoord[3], metrics.texCoord[2], metrics.texCoord[4])
        end
        return
    end
    texture:SetColorTexture(unpack(fallbackColor or { 0.10, 0.10, 0.12, 0.95 }))
end

local function createSurface(parent, rect)
    local frame = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(frame, parent, rect)
    if frame.SetBackdrop then
        frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        frame:SetBackdropColor(0.09, 0.10, 0.13, 0.94)
        frame:SetBackdropBorderColor(0.42, 0.33, 0.16, 0.92)
    end
    return frame
end

local function createPanel(parent, rect, titleText, metaText)
    local panel = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(panel, parent, rect)
    if panel.SetBackdrop then
        panel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        panel:SetBackdropColor(0, 0, 0, 0)
        panel:SetBackdropBorderColor(0, 0, 0, 0)
    end

    local background = panel:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(panel)
    applyArtTexture(background, PANEL_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local headerBand = panel:CreateTexture(nil, "ARTWORK")
    headerBand:SetPoint("TOPLEFT", panel, "TOPLEFT", 24, -14)
    headerBand:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -24, -14)
    headerBand:SetHeight(24)
    applyArtTexture(headerBand, PANEL_HEADER_KEY, { 0.08, 0.08, 0.10, 0.82 })

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("CENTER", headerBand, "CENTER", 0, -1)
    title:SetText(titleText or "")
    title:SetTextColor(0.98, 0.92, 0.76, 1)

    if trimString(metaText) ~= "" then
        local meta = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        meta:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, -46)
        meta:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 28))
        meta:SetJustifyH("LEFT")
        meta:SetText(metaText)
        meta:SetTextColor(0.76, 0.72, 0.64, 1)
        panel.Meta = meta
    end

    panel.Title = title
    return panel
end

local function createButton(parent, rect, labelText, onClick, isActive)
    local button = CreateFrame("Button", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(button, parent, rect)

    local background = button:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(button)
    applyArtTexture(background, isActive and BUTTON_ACTIVE_ART_KEY or BUTTON_ART_KEY, isActive and { 0.20, 0.23, 0.31, 0.98 } or { 0.10, 0.11, 0.14, 0.95 })
    button.Background = background

    local text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("CENTER", button, "CENTER", 0, 0)
    text:SetText(labelText or "")
    text:SetTextColor(isActive and 0.98 or 0.96, isActive and 0.88 or 0.93, isActive and 0.58 or 0.84, 1)
    button.Text = text

    button:SetScript("OnClick", function()
        if onClick then
            onClick(button)
        end
    end)

    return button
end

local function createCard(parent, rect, titleText, valueText, subtitleText, backgroundTextureKey)
    local card = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(card, parent, rect)

    local background = card:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(card)
    applyArtTexture(background, backgroundTextureKey or CARD_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    title:SetPoint("TOP", card, "TOP", 0, -16)
    title:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
    title:SetJustifyH("CENTER")
    title:SetText(titleText or "")
    title:SetTextColor(0.78, 0.74, 0.66, 1)

    local value = card:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    value:SetPoint("TOP", title, "BOTTOM", 0, -8)
    value:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
    value:SetJustifyH("CENTER")
    value:SetText(valueText or "-")
    value:SetTextColor(0.96, 0.82, 0.34, 1)

    if trimString(subtitleText) ~= "" then
        local subtitle = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        subtitle:SetPoint("BOTTOM", card, "BOTTOM", 0, 12)
        subtitle:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 24))
        subtitle:SetJustifyH("CENTER")
        subtitle:SetText(subtitleText)
        subtitle:SetTextColor(0.72, 0.68, 0.58, 1)
        card.Subtitle = subtitle
    end

    card.Title = title
    card.Value = value
    return card
end

local function createInput(parent, rect, initialText)
    local host = createSurface(parent, rect)
    local input = CreateFrame("EditBox", nil, host, "InputBoxTemplate")
    if input.Left then input.Left:SetAlpha(0) end
    if input.Middle then input.Middle:SetAlpha(0) end
    if input.Right then input.Right:SetAlpha(0) end
    input:SetAutoFocus(false)
    input:SetFontObject("GameFontHighlight")
    input:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -4)
    input:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -8, 4)
    input:SetText(initialText or "")
    host.Input = input
    return host
end

local function createScrollEditBox(parent, rect, initialText, fontObject, isReadOnly)
    local host = createSurface(parent, rect)
    local scrollFrame = CreateFrame("ScrollFrame", nil, host, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -8)
    scrollFrame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -28, 8)

    local input = CreateFrame("EditBox", nil, scrollFrame)
    input:SetMultiLine(true)
    input:SetAutoFocus(false)
    input:SetFontObject(fontObject or "ChatFontNormal")
    input:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
    input:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 40))
    input:SetHeight(math.max(1, (tonumber(rect.h) or 1) - 18))
    input:SetText(initialText or "")
    if input.SetTextInsets then
        input:SetTextInsets(0, 0, 2, 2)
    end
    if input.SetJustifyH then
        input:SetJustifyH("LEFT")
    end
    if input.SetJustifyV then
        input:SetJustifyV("TOP")
    end
    if isReadOnly then
        input:EnableMouse(true)
    end
    input:SetScript("OnEscapePressed", function(self)
        self:ClearFocus()
    end)

    local measure = host:CreateFontString(nil, "ARTWORK", fontObject or "ChatFontNormal")
    measure:SetPoint("TOPLEFT", host, "TOPLEFT", -10000, -10000)
    measure:SetWidth(math.max(1, (tonumber(rect.w) or 1) - 40))
    if measure.SetJustifyH then
        measure:SetJustifyH("LEFT")
    end
    if measure.SetJustifyV then
        measure:SetJustifyV("TOP")
    end
    measure:SetTextColor(0, 0, 0, 0)

    local function updateHeight(text)
        measure:SetText(text or "")
        local textHeight = measure.GetStringHeight and tonumber(measure:GetStringHeight()) or 0
        input:SetHeight(math.max((tonumber(rect.h) or 1) - 18, math.ceil(textHeight + 16)))
    end

    input._gh3UpdateHeight = updateHeight
    input:SetScript("OnTextChanged", function(self)
        updateHeight(self:GetText())
    end)
    updateHeight(initialText or "")

    scrollFrame:SetScrollChild(input)
    host.ScrollFrame = scrollFrame
    host.EditBox = input
    return host
end

local function createMultilineInput(parent, rect, initialText)
    return createScrollEditBox(parent, rect, initialText, "ChatFontNormal", false)
end

local function createReadOnlyMultiline(parent, rect, textValue)
    local host = createScrollEditBox(parent, rect, textValue, "GameFontHighlightSmall", true)
    if host and host.EditBox then
        host.EditBox:SetScript("OnTextChanged", nil)
        if host.EditBox._gh3UpdateHeight then
            host.EditBox:_gh3UpdateHeight(textValue or "")
        end
    end
    return host
end

local function createCheckbox(parent, x, y, labelText, checked, onChanged)
    local box = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    box:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    box:SetChecked(checked and true or false)
    box:SetScript("OnClick", function(self)
        if onChanged then
            onChanged(self:GetChecked() and true or false)
        end
    end)

    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("LEFT", box, "RIGHT", 4, 0)
    label:SetText(labelText or "")
    label:SetTextColor(0.82, 0.80, 0.76, 1)
    box.Label = label
    return box
end

local function createSelectableRow(parent, y, width, textValue, isSelected, onClick)
    local row = CreateFrame("Button", nil, parent, "BackdropTemplate")
    row:SetSize(width, 26)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
    if row.SetBackdrop then
        row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        if isSelected then
            row:SetBackdropColor(0.16, 0.20, 0.29, 0.98)
            row:SetBackdropBorderColor(0.82, 0.66, 0.24, 1)
        else
            row:SetBackdropColor(0.06, 0.09, 0.13, 0.96)
            row:SetBackdropBorderColor(0.30, 0.38, 0.48, 1)
        end
    end
    row:SetScript("OnClick", function()
        if onClick then
            onClick()
        end
    end)
    row:SetScript("OnEnter", function(self)
        if not isSelected and self.SetBackdropColor then
            self:SetBackdropColor(0.12, 0.16, 0.22, 0.98)
            self:SetBackdropBorderColor(0.72, 0.58, 0.22, 1)
        end
    end)
    row:SetScript("OnLeave", function(self)
        if self.SetBackdropColor then
            if isSelected then
                self:SetBackdropColor(0.16, 0.20, 0.29, 0.98)
                self:SetBackdropBorderColor(0.82, 0.66, 0.24, 1)
            else
                self:SetBackdropColor(0.06, 0.09, 0.13, 0.96)
                self:SetBackdropBorderColor(0.30, 0.38, 0.48, 1)
            end
        end
    end)

    local rowText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    rowText:SetPoint("LEFT", row, "LEFT", 8, 0)
    rowText:SetWidth(width - 16)
    rowText:SetJustifyH("LEFT")
    rowText:SetText(textValue or "")
    rowText:SetTextColor(0.82, 0.80, 0.76, 1)
    return row
end

local function createSectionLabel(parent, x, y, textValue)
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    label:SetText(textValue or "")
    label:SetTextColor(0.96, 0.84, 0.34, 1)
    return label
end

local function renderGratsUPane(root, layout, parent, snapshot)
    local service = addon.GratsUService
    local state = page.State
    local paneLayout = layout.GratsU or {}

    if not service then
        createPanel(root, { x = 20, y = -92, w = 420, h = 160 }, "GratsU Disabled", "The GratsU service is not loaded right now, so this preview pane is unavailable.")
        return
    end

    if not service:CanAccess() then
        local restricted = root:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        restricted:SetPoint("TOPLEFT", root, "TOPLEFT", 20, -100)
        restricted:SetText("GratsU is only available to officers and the guild master.")
        restricted:SetTextColor(0.82, 0.80, 0.76, 1)
        return
    end

    local settings = service:GetSettings()
    local summary = service:GetSummary()
    local activeTab = state.gratsUTab == "welcome" and "welcome" or "achievement"
    local kindLabel = activeTab == "welcome" and "Welcome" or "Achievement"
    local messages = service:GetMessages(activeTab)
    local selectedIndex = tonumber(state.gratsUSelectedIndex)
    local activePeerNames = summary.activePeerNames or {}
    local peerSummaryText = #activePeerNames > 0 and table.concat(activePeerNames, "\n") or "No other active officer or guild master peers detected."
    local lastSentText = summary.lastSentAt and summary.lastSentAt > 0 and formatTimestampText(summary.lastSentAt) or "No automated messages sent yet"

    if not selectedIndex or selectedIndex < 1 or selectedIndex > #messages then
        selectedIndex = #messages > 0 and 1 or nil
        state.gratsUSelectedIndex = selectedIndex
    end

    if state.gratsUDraftTab ~= activeTab or state.gratsUDraftIndex ~= selectedIndex then
        state.gratsUDraft = selectedIndex and (messages[selectedIndex] or "") or ""
        state.gratsUDraftTab = activeTab
        state.gratsUDraftIndex = selectedIndex
    end

    local cardsRect = paneLayout.Cards or { x = 20, y = -92, w = 1120, h = 94, gap = 10 }
    local gap = tonumber(cardsRect.gap) or 10
    local cardWidth = math.floor(((tonumber(cardsRect.w) or 1120) - (gap * 3)) / 4)
    local cardX = tonumber(cardsRect.x) or 20
    local cardY = tonumber(cardsRect.y) or -92
    local cardH = tonumber(cardsRect.h) or 94

    createCard(root, { x = cardX, y = cardY, w = cardWidth, h = cardH }, "Access", summary.access, "Visible only to officers and the guild master", "HomeCardBackground")
    createCard(root, { x = cardX + (cardWidth + gap), y = cardY, w = cardWidth, h = cardH }, "Peers", tostring(summary.activePeerCount or 0), "Coordinated senders active in the guild", "HomeMembersCardBackground")
    createCard(root, { x = cardX + ((cardWidth + gap) * 2), y = cardY, w = cardWidth, h = cardH }, "Achievements", settings.enableAchievement == false and "Off" or "On", string.format("%d templates | %ds cooldown", summary.achievementCount or 0, settings.achievementCooldownSeconds or 120), "HomeMythicCardBackground")
    createCard(root, { x = cardX + ((cardWidth + gap) * 3), y = cardY, w = cardWidth, h = cardH }, "Welcomes", settings.enableWelcome == false and "Off" or "On", string.format("%d templates | %ds offline gate", summary.welcomeCount or 0, settings.minOfflineSeconds or 120), "HomeRaidCardBackground")

    local settingsRect = paneLayout.Settings or { x = 20, y = -204, w = 320, h = 512 }
    local settingsPanel = createPanel(root, settingsRect, "Automation Settings", "GratsU coordinates across active officer and GM clients so only one sender answers each event.")
    createCheckbox(settingsPanel, 12, -72, "Enable welcome messages", settings.enableWelcome ~= false, function(checked)
        service:UpdateSettings({ enableWelcome = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -98, "Enable achievement congrats", settings.enableAchievement ~= false, function(checked)
        service:UpdateSettings({ enableAchievement = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -124, "Ignore my own events", settings.ignoreSelf ~= false, function(checked)
        service:UpdateSettings({ ignoreSelf = checked })
        refreshPage(parent, snapshot)
    end)

    local function createSettingRow(panel, labelText, valueText, y)
        local label = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, y)
        label:SetWidth(134)
        label:SetJustifyH("LEFT")
        label:SetText(labelText)
        label:SetTextColor(0.82, 0.80, 0.76, 1)

        local inputHost = createInput(panel, { x = 158, y = y + 10, w = 96, h = 22 }, tostring(valueText or 0))
        local input = inputHost.Input
        if input and input.SetNumeric then
            input:SetNumeric(false)
        end
        return input
    end

    local antiSpamInput = createSettingRow(settingsPanel, "Anti-Spam (sec)", settings.antiSpamSeconds, -166)
    local minOfflineInput = createSettingRow(settingsPanel, "Offline Gate (sec)", settings.minOfflineSeconds, -196)
    local achievementCooldownInput = createSettingRow(settingsPanel, "Achieve CD (sec)", settings.achievementCooldownSeconds, -226)
    local welcomeDelayInput = createSettingRow(settingsPanel, "Welcome Delay (sec)", settings.welcomeDelaySeconds, -256)

    createButton(settingsPanel, { x = 14, y = -290, w = 116, h = 24 }, "Save Settings", function()
        service:UpdateSettings({
            antiSpamSeconds = antiSpamInput and antiSpamInput:GetText() or settings.antiSpamSeconds,
            minOfflineSeconds = minOfflineInput and minOfflineInput:GetText() or settings.minOfflineSeconds,
            achievementCooldownSeconds = achievementCooldownInput and achievementCooldownInput:GetText() or settings.achievementCooldownSeconds,
            welcomeDelaySeconds = welcomeDelayInput and welcomeDelayInput:GetText() or settings.welcomeDelaySeconds,
        })
        refreshPage(parent, snapshot)
    end, false)
    createButton(settingsPanel, { x = 142, y = -290, w = 116, h = 24 }, "Restore All", function()
        service:ResetAllDefaults()
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)

    createSectionLabel(settingsPanel, 14, -338, "Activity")
    createReadOnlyMultiline(settingsPanel, { x = 14, y = -360, w = 246, h = 58 }, string.format("Welcomes sent: %d\nAchievement congrats sent: %d\nLast automated send: %s", summary.stats and summary.stats.welcomeSent or 0, summary.stats and summary.stats.achievementSent or 0, lastSentText))
    createSectionLabel(settingsPanel, 14, -432, "Active Peers")
    createReadOnlyMultiline(settingsPanel, { x = 14, y = -454, w = 246, h = 44 }, peerSummaryText)

    local listRect = paneLayout.Library or { x = 356, y = -204, w = 320, h = 512 }
    local listPanel = createPanel(root, listRect, "Message Library", "")
    createButton(listPanel, { x = 14, y = -36, w = 122, h = 22 }, "Achievements", function()
        state.gratsUTab = "achievement"
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, activeTab == "achievement")
    createButton(listPanel, { x = 146, y = -36, w = 122, h = 22 }, "Welcomes", function()
        state.gratsUTab = "welcome"
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, activeTab == "welcome")

    local countLabel = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countLabel:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -64)
    countLabel:SetWidth(260)
    countLabel:SetJustifyH("LEFT")
    countLabel:SetText(string.format("%s templates: %d / 50", kindLabel, #messages))
    countLabel:SetTextColor(0.82, 0.80, 0.76, 1)

    local listScrollFrame = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listScrollFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -88)
    listScrollFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -30, 14)
    local listContent = CreateFrame("Frame", nil, listScrollFrame)
    listContent:SetSize(236, math.max(#messages * 30, 1))
    listScrollFrame:SetScrollChild(listContent)

    if #messages == 0 then
        local emptyText = listContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        emptyText:SetPoint("TOPLEFT", listContent, "TOPLEFT", 0, 0)
        emptyText:SetWidth(236)
        emptyText:SetJustifyH("LEFT")
        emptyText:SetText("No messages are configured for this tab.")
        emptyText:SetTextColor(0.72, 0.70, 0.66, 1)
    end

    for index, messageText in ipairs(messages) do
        createSelectableRow(listContent, -((index - 1) * 30), 236, messageText, index == selectedIndex, function()
            state.gratsUSelectedIndex = index
            state.gratsUDraft = messageText
            state.gratsUDraftTab = activeTab
            state.gratsUDraftIndex = index
            refreshPage(parent, snapshot)
        end)
    end

    local editorRect = paneLayout.Editor or { x = 692, y = -204, w = 340, h = 512 }
    local editorPanel = createPanel(root, editorRect, kindLabel .. " Editor", selectedIndex and string.format("Editing %s message %d of %d", string.lower(kindLabel), selectedIndex, #messages) or string.format("Create a new %s message", string.lower(kindLabel)))
    local editorHost = createMultilineInput(editorPanel, { x = 14, y = -58, w = 284, h = 196 }, state.gratsUDraft or "")
    local editorInput = editorHost.EditBox
    local previousOnTextChanged = editorInput and editorInput:GetScript("OnTextChanged") or nil
    if editorInput then
        editorInput:SetScript("OnTextChanged", function(input)
            if previousOnTextChanged then
                previousOnTextChanged(input)
            end
            state.gratsUDraft = input:GetText()
            state.gratsUDraftTab = activeTab
            state.gratsUDraftIndex = selectedIndex
        end)
    end

    createButton(editorPanel, { x = 14, y = -264, w = 128, h = 24 }, "Save Message", function()
        local ok, errorMessage
        if selectedIndex then
            ok, errorMessage = service:UpdateMessage(activeTab, selectedIndex, state.gratsUDraft)
        else
            ok, errorMessage = service:AddMessage(activeTab, state.gratsUDraft)
            if ok then
                state.gratsUSelectedIndex = #service:GetMessages(activeTab)
            end
        end
        if not ok then
            addon:Print(errorMessage)
            return
        end
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 154, y = -264, w = 128, h = 24 }, "New Message", function()
        state.gratsUSelectedIndex = nil
        state.gratsUDraft = ""
        state.gratsUDraftTab = activeTab
        state.gratsUDraftIndex = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 14, y = -296, w = 128, h = 24 }, "Delete Message", function()
        local ok, errorMessage = service:DeleteMessage(activeTab, selectedIndex)
        if not ok then
            addon:Print(errorMessage)
            return
        end
        local remaining = service:GetMessages(activeTab)
        state.gratsUSelectedIndex = math.min(selectedIndex or 1, #remaining)
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 154, y = -296, w = 128, h = 24 }, "Restore Tab", function()
        service:ResetMessages(activeTab)
        state.gratsUSelectedIndex = 1
        state.gratsUDraftTab = nil
        refreshPage(parent, snapshot)
    end, false)

    createSectionLabel(editorPanel, 14, -336, "Guild Chat Preview")
    createReadOnlyMultiline(editorPanel, { x = 14, y = -360, w = 284, h = 116 }, service:GetPreview(activeTab, state.gratsUDraft))
end

local function renderToonCelebrationsPane(root, layout, parent, snapshot)
    local service = addon.ToonCelebrationService
    local state = page.State
    local paneLayout = layout.ToonCelebrations or {}

    if not service then
        createPanel(root, { x = 20, y = -92, w = 420, h = 160 }, "Toon Celebrations Disabled", "The Toon Celebrations service is not loaded right now, so this preview pane is unavailable.")
        return
    end

    if not service:CanAccess() then
        local restricted = root:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        restricted:SetPoint("TOPLEFT", root, "TOPLEFT", 20, -100)
        restricted:SetText("Toon Celebrations is only available to officers and the guild master.")
        restricted:SetTextColor(0.82, 0.80, 0.76, 1)
        return
    end

    local settings = service:GetSettings()
    local summary = service:GetSummary(settings.lookbackDays, settings.lookaheadDays)
    local entries = service:GetCelebrationWindow(settings.lookbackDays, settings.lookaheadDays)
    local activeTab = state.celebrationsTab or "birthday"
    local activeTiming = state.celebrationsTimingTab or "today"
    local messages = service:GetMessages(activeTab, activeTiming)
    local selectedIndex = tonumber(state.celebrationsSelectedIndex)
    local tabLabels = {
        birthday = "Birthdays",
        ["anniversary-month"] = "1 Month",
        ["anniversary-six-month"] = "6 Months",
        ["anniversary-year"] = "Yearly",
    }
    local timingLabels = {
        upcoming = "Upcoming",
        today = "Today",
        recent = "Just Missed",
    }
    local editorLabels = {
        birthday = "Birthday",
        ["anniversary-month"] = "1 Month Anniversary",
        ["anniversary-six-month"] = "6 Month Anniversary",
        ["anniversary-year"] = "Yearly Anniversary",
    }
    local activeLabel = editorLabels[activeTab] or "Birthday"
    local previewYears = activeTab == "anniversary-year" and 3 or 0
    local previewOffset = activeTiming == "upcoming" and 1 or (activeTiming == "recent" and -1 or 0)

    if not selectedIndex or selectedIndex < 1 or selectedIndex > #messages then
        selectedIndex = #messages > 0 and 1 or nil
        state.celebrationsSelectedIndex = selectedIndex
    end

    if state.celebrationDraftKind ~= string.format("%s|%s", activeTab, activeTiming) or state.celebrationDraftIndex ~= selectedIndex then
        state.celebrationDraft = selectedIndex and (messages[selectedIndex] or "") or ""
        state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
        state.celebrationDraftIndex = selectedIndex
    end

    local cardsRect = paneLayout.Cards or { x = 20, y = -92, w = 1120, h = 94, gap = 10 }
    local gap = tonumber(cardsRect.gap) or 10
    local cardWidth = math.floor(((tonumber(cardsRect.w) or 1120) - (gap * 3)) / 4)
    local cardX = tonumber(cardsRect.x) or 20
    local cardY = tonumber(cardsRect.y) or -92
    local cardH = tonumber(cardsRect.h) or 94

    createCard(root, { x = cardX, y = cardY, w = cardWidth, h = cardH }, "Guild", summary.guildKey or "Unknown", "Current live guild for celebration review and sending", "HomeCardBackground")
    createCard(root, { x = cardX + (cardWidth + gap), y = cardY, w = cardWidth, h = cardH }, "Window", string.format("-%d / +%d", settings.lookbackDays or 7, settings.lookaheadDays or 14), string.format("%d recent | %d today | %d upcoming", summary.recent or 0, summary.today or 0, summary.upcoming or 0), "HomeMembersCardBackground")
    createCard(root, { x = cardX + ((cardWidth + gap) * 2), y = cardY, w = cardWidth, h = cardH }, "Reminder", settings.enableBirthday == false and settings.enableAnniversary == false and "Off" or "On", string.format("Login note only | Sent %d", summary.sent or 0), "HomeMythicCardBackground")
    createCard(root, { x = cardX + ((cardWidth + gap) * 3), y = cardY, w = cardWidth, h = cardH }, "Templates", tostring((summary.birthdayCount or 0) + (summary.anniversaryMonthCount or 0) + (summary.anniversarySixMonthCount or 0) + (summary.anniversaryYearCount or 0)), "Birthday and anniversary library entries", "HomeRaidCardBackground")

    local settingsRect = paneLayout.Settings or { x = 20, y = -204, w = 320, h = 352 }
    local settingsPanel = createPanel(root, settingsRect, "Reminder Settings", "Only officers and the guild master can manage celebration reminders for the current guild. GH3 now reminds you after login instead of auto-sending.")
    createCheckbox(settingsPanel, 12, -72, "Enable birthday messages", settings.enableBirthday ~= false, function(checked)
        service:UpdateSettings({ enableBirthday = checked })
        refreshPage(parent, snapshot)
    end)
    createCheckbox(settingsPanel, 12, -98, "Enable anniversary messages", settings.enableAnniversary ~= false, function(checked)
        service:UpdateSettings({ enableAnniversary = checked })
        refreshPage(parent, snapshot)
    end)

    local function createSettingRow(panel, labelText, valueText, y)
        local label = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, y)
        label:SetWidth(134)
        label:SetJustifyH("LEFT")
        label:SetText(labelText)
        label:SetTextColor(0.82, 0.80, 0.76, 1)

        local inputHost = createInput(panel, { x = 158, y = y + 10, w = 96, h = 22 }, tostring(valueText or 0))
        local input = inputHost.Input
        if input and input.SetNumeric then
            input:SetNumeric(false)
        end
        return input
    end

    local lookbackInput = createSettingRow(settingsPanel, "Recent Window", settings.lookbackDays, -140)
    local lookaheadInput = createSettingRow(settingsPanel, "Upcoming Window", settings.lookaheadDays, -170)
    createButton(settingsPanel, { x = 14, y = -236, w = 116, h = 24 }, "Save Settings", function()
        service:UpdateSettings({
            lookbackDays = lookbackInput and lookbackInput:GetText() or settings.lookbackDays,
            lookaheadDays = lookaheadInput and lookaheadInput:GetText() or settings.lookaheadDays,
        })
        refreshPage(parent, snapshot)
    end, false)
    createButton(settingsPanel, { x = 142, y = -236, w = 116, h = 24 }, "Restore All", function()
        service:ResetAllDefaults()
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)

    createSectionLabel(settingsPanel, 14, -280, "Activity")
    createReadOnlyMultiline(settingsPanel, { x = 14, y = -302, w = 246, h = 42 }, string.format("Birthdays sent: %d\n1 month sent: %d\n6 month sent: %d\nYearly sent: %d", summary.stats and summary.stats.birthdaySent or 0, summary.stats and summary.stats.anniversaryMonthSent or 0, summary.stats and summary.stats.anniversarySixMonthSent or 0, summary.stats and summary.stats.anniversaryYearSent or 0))

    local listRect = paneLayout.Library or { x = 356, y = -204, w = 320, h = 352 }
    local listPanel = createPanel(root, listRect, "Message Library", "")
    createButton(listPanel, { x = 14, y = -36, w = 122, h = 22 }, "Birthdays", function()
        state.celebrationsTab = "birthday"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTab == "birthday")
    createButton(listPanel, { x = 146, y = -36, w = 122, h = 22 }, "1 Month", function()
        state.celebrationsTab = "anniversary-month"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTab == "anniversary-month")
    createButton(listPanel, { x = 14, y = -64, w = 122, h = 22 }, "6 Months", function()
        state.celebrationsTab = "anniversary-six-month"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTab == "anniversary-six-month")
    createButton(listPanel, { x = 146, y = -64, w = 122, h = 22 }, "Yearly", function()
        state.celebrationsTab = "anniversary-year"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTab == "anniversary-year")
    createButton(listPanel, { x = 14, y = -94, w = 78, h = 22 }, "Upcoming", function()
        state.celebrationsTimingTab = "upcoming"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTiming == "upcoming")
    createButton(listPanel, { x = 100, y = -94, w = 70, h = 22 }, "Today", function()
        state.celebrationsTimingTab = "today"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTiming == "today")
    createButton(listPanel, { x = 178, y = -94, w = 100, h = 22 }, "Just Missed", function()
        state.celebrationsTimingTab = "recent"
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, activeTiming == "recent")

    local countLabel = listPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    countLabel:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -124)
    countLabel:SetWidth(260)
    countLabel:SetJustifyH("LEFT")
    countLabel:SetText(string.format("%s %s templates: %d / 50", timingLabels[activeTiming] or "Today", tabLabels[activeTab] or "Messages", #messages))
    countLabel:SetTextColor(0.82, 0.80, 0.76, 1)

    local listScrollFrame = CreateFrame("ScrollFrame", nil, listPanel, "UIPanelScrollFrameTemplate")
    listScrollFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 14, -148)
    listScrollFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -30, 14)
    local listContent = CreateFrame("Frame", nil, listScrollFrame)
    listContent:SetSize(236, math.max(#messages * 30, 1))
    listScrollFrame:SetScrollChild(listContent)

    if #messages == 0 then
        local emptyText = listContent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        emptyText:SetPoint("TOPLEFT", listContent, "TOPLEFT", 0, 0)
        emptyText:SetWidth(236)
        emptyText:SetJustifyH("LEFT")
        emptyText:SetText("No messages are configured for this celebration type.")
        emptyText:SetTextColor(0.72, 0.70, 0.66, 1)
    end

    for index, messageText in ipairs(messages) do
        createSelectableRow(listContent, -((index - 1) * 30), 236, messageText, index == selectedIndex, function()
            state.celebrationsSelectedIndex = index
            state.celebrationDraft = messageText
            state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
            state.celebrationDraftIndex = index
            refreshPage(parent, snapshot)
        end)
    end

    local editorRect = paneLayout.Editor or { x = 692, y = -204, w = 340, h = 352 }
    local editorMeta = selectedIndex and string.format("Editing %s %s message %d of %d", string.lower(timingLabels[activeTiming] or "today"), string.lower(activeLabel), selectedIndex, #messages) or string.format("Create a new %s %s message", string.lower(timingLabels[activeTiming] or "today"), string.lower(activeLabel))
    local editorPanel = createPanel(root, editorRect, activeLabel .. " Editor", editorMeta)
    local editorHost = createMultilineInput(editorPanel, { x = 14, y = -58, w = 284, h = 138 }, state.celebrationDraft or "")
    local editorInput = editorHost.EditBox
    local previousOnTextChanged = editorInput and editorInput:GetScript("OnTextChanged") or nil
    if editorInput then
        editorInput:SetScript("OnTextChanged", function(input)
            if previousOnTextChanged then
                previousOnTextChanged(input)
            end
            state.celebrationDraft = input:GetText()
            state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
            state.celebrationDraftIndex = selectedIndex
        end)
    end

    createButton(editorPanel, { x = 14, y = -206, w = 128, h = 24 }, "Save Message", function()
        local ok, errorMessage
        if selectedIndex then
            ok, errorMessage = service:UpdateMessage(activeTab, activeTiming, selectedIndex, state.celebrationDraft)
        else
            ok, errorMessage = service:AddMessage(activeTab, activeTiming, state.celebrationDraft)
            if ok then
                state.celebrationsSelectedIndex = #service:GetMessages(activeTab, activeTiming)
            end
        end
        if not ok then
            addon:Print(errorMessage)
            return
        end
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 154, y = -206, w = 128, h = 24 }, "New Message", function()
        state.celebrationsSelectedIndex = nil
        state.celebrationDraft = ""
        state.celebrationDraftKind = string.format("%s|%s", activeTab, activeTiming)
        state.celebrationDraftIndex = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 14, y = -238, w = 128, h = 24 }, "Delete Message", function()
        local ok, errorMessage = service:DeleteMessage(activeTab, activeTiming, selectedIndex)
        if not ok then
            addon:Print(errorMessage)
            return
        end
        local remaining = service:GetMessages(activeTab, activeTiming)
        state.celebrationsSelectedIndex = math.min(selectedIndex or 1, #remaining)
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)
    createButton(editorPanel, { x = 154, y = -238, w = 128, h = 24 }, "Restore Tab", function()
        service:ResetMessages(activeTab, activeTiming)
        state.celebrationsSelectedIndex = 1
        state.celebrationDraftKind = nil
        refreshPage(parent, snapshot)
    end, false)

    createSectionLabel(editorPanel, 14, -276, "Guild Chat Preview")
    createReadOnlyMultiline(editorPanel, { x = 14, y = -300, w = 284, h = 46 }, service:GetPreview(activeTab, activeTiming, state.celebrationDraft, previewYears, previewOffset))

    local queueRect = paneLayout.Queue or { x = 20, y = -572, w = 1012, h = 150 }
    local queuePanel = createPanel(root, queueRect, "Celebration Queue", string.format("Showing %d entries for %s across the configured recent and upcoming window. Use Send to post to guild chat or Ignore to clear that date from the queue.", #entries, summary.guildKey or "Unknown guild"))

    local headerRow = CreateFrame("Frame", nil, queuePanel, "BackdropTemplate")
    headerRow:SetSize((tonumber(queueRect.w) or 1012) - 56, 24)
    headerRow:SetPoint("TOPLEFT", queuePanel, "TOPLEFT", 14, -58)
    if headerRow.SetBackdrop then
        headerRow:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        headerRow:SetBackdropColor(0.16, 0.20, 0.29, 0.98)
        headerRow:SetBackdropBorderColor(0.82, 0.66, 0.24, 1)
    end

    local function createHeader(textValue, width, x)
        local label = headerRow:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        label:SetPoint("LEFT", headerRow, "LEFT", x, 0)
        label:SetWidth(width)
        label:SetJustifyH("LEFT")
        label:SetText(textValue)
        label:SetTextColor(0.96, 0.84, 0.34, 1)
    end

    createHeader("Toon", 170, 8)
    createHeader("Celebration", 148, 186)
    createHeader("Date", 96, 342)
    createHeader("When", 96, 446)
    createHeader("Status", 74, 550)
    createHeader("Actions", 150, 632)

    local scrollFrame = CreateFrame("ScrollFrame", nil, queuePanel, "UIPanelScrollFrameTemplate")
    scrollFrame:SetPoint("TOPLEFT", queuePanel, "TOPLEFT", 14, -86)
    scrollFrame:SetPoint("BOTTOMRIGHT", queuePanel, "BOTTOMRIGHT", -30, 10)
    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetWidth((tonumber(queueRect.w) or 1012) - 56)
    scrollChild:SetHeight(1)
    scrollFrame:SetScrollChild(scrollChild)

    if #entries == 0 then
        local empty = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        empty:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 14, -8)
        empty:SetWidth((tonumber(queueRect.w) or 1012) - 100)
        empty:SetJustifyH("LEFT")
        empty:SetText("No recent or upcoming birthdays or guild anniversaries were found for your toons in the current guild.")
        empty:SetTextColor(0.82, 0.80, 0.76, 1)
        scrollChild:SetHeight(60)
        return
    end

    local rowHeight = 42
    local rowPitch = 48
    for index, entry in ipairs(entries) do
        local row = CreateFrame("Frame", nil, scrollChild, "BackdropTemplate")
        row:SetSize((tonumber(queueRect.w) or 1012) - 56, rowHeight)
        row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -((index - 1) * rowPitch))
        if row.SetBackdrop then
            row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
            row:SetBackdropColor(0.06, 0.09, 0.13, 0.96)
            row:SetBackdropBorderColor(0.30, 0.38, 0.48, 1)
        end

        local function createValue(textValue, width, x, fontName, muted)
            local label = row:CreateFontString(nil, "OVERLAY", fontName or "GameFontNormalSmall")
            label:SetPoint("TOPLEFT", row, "TOPLEFT", x, -8)
            label:SetWidth(width)
            label:SetJustifyH("LEFT")
            label:SetText(textValue)
            if muted then
                label:SetTextColor(0.72, 0.70, 0.66, 1)
            else
                label:SetTextColor(0.82, 0.80, 0.76, 1)
            end
        end

        createValue(entry.memberName or entry.memberId or "Unknown", 170, 8, "GameFontNormal", false)
        createValue(entry.kindLabel or entry.kind or "Celebration", 148, 186, "GameFontNormal", false)
        createValue(entry.dateLabel or entry.dayKey or "Unknown", 96, 342, "GameFontNormalSmall", true)
        createValue(entry.timingLabel or "Today", 96, 446, "GameFontNormalSmall", true)
        createValue(entry.sent and "Sent" or "Ready", 74, 550, "GameFontNormalSmall", true)

        local sendButton = createButton(row, { x = 632, y = -10, w = 64, h = 22 }, entry.sent and "Sent" or "Send", function()
            local ok, message = service:SendManualCelebration(entry)
            if ok then
                addon:Print("Celebration message sent.")
                refreshPage(parent, snapshot)
            elseif message then
                addon:Print(message)
            end
        end, false)
        local ignoreButton = createButton(row, { x = 702, y = -10, w = 64, h = 22 }, "Ignore", function()
            local ok, message = service:IgnoreCelebration(entry)
            if ok then
                addon:Print("Celebration ignored for that date.")
                refreshPage(parent, snapshot)
            elseif message then
                addon:Print(message)
            end
        end, false)

        if entry.sent then
            sendButton:Disable()
            ignoreButton:Disable()
        end
    end

    scrollChild:SetHeight(math.max(1, (#entries * rowPitch) - (rowPitch - rowHeight)))
end

refreshPage = function(parent, snapshot)
    if page._rootFrame then
        clearFrame(page._rootFrame)
        page._rootFrame = nil
    end
    page:Render(parent, snapshot)
end

function page:Render(parent, snapshot)
    local layout = addon.NewUILayout and addon.NewUILayout.Celebrate or nil
    local root = CreateFrame("Frame", nil, parent)
    ApplyRect(root, parent, layout and layout.Page or { x = 0, y = 0, w = tonumber(parent:GetWidth()) or 1380, h = tonumber(parent:GetHeight()) or 742 })
    page._rootFrame = root

    local tabs = layout and layout.Tabs or {}
    local activePane = page.State.activePane == "celebrations" and "celebrations" or "gratsu"
    createButton(root, { x = tonumber((tabs.GratsU or {}).x) or 20, y = tonumber((tabs.GratsU or {}).y) or -52, w = tonumber((tabs.GratsU or {}).w) or 112, h = tonumber((tabs.GratsU or {}).h) or 28 }, "GratsU", function()
        page.State.activePane = "gratsu"
        refreshPage(parent, snapshot)
    end, activePane == "gratsu")
    createButton(root, { x = tonumber((tabs.ToonCelebrations or {}).x) or 140, y = tonumber((tabs.ToonCelebrations or {}).y) or -52, w = tonumber((tabs.ToonCelebrations or {}).w) or 188, h = tonumber((tabs.ToonCelebrations or {}).h) or 28 }, "Toon Celebrations", function()
        page.State.activePane = "celebrations"
        refreshPage(parent, snapshot)
    end, activePane == "celebrations")

    if activePane == "celebrations" then
        renderToonCelebrationsPane(root, layout or {}, parent, snapshot)
    else
        renderGratsUPane(root, layout or {}, parent, snapshot)
    end

    return root
end

return page
]]