GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.NewUIMyToonsEditProfile = addon.NewUIMyToonsEditProfile or {}

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

local MYTOONS_PANEL_ART = { backgroundTextureKey = "MyToonsPanelBackground", borderTextureKey = "MyToonsPanelBorder" }
local MYTOONS_CARD_ART = { backgroundTextureKey = "MyToonsCardBackground", borderTextureKey = "MyToonsCardBorder" }
local MYTOONS_BUTTON_ART = { backgroundTextureKey = "MyToonsButtonBackground", borderTextureKey = "MyToonsButtonBorder" }
local MYTOONS_CLASS_ICON_TEXTURE = CLASS_ICON_TEXTURE or "Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES"
local INTEREST_OPTIONS = { "raiding", "mythic+", "delves", "pvp", "alts", "crafting", "farming", "events", "social" }
local PROFESSION_OPTIONS = { "alchemy", "blacksmithing", "enchanting", "engineering", "herbalism", "inscription", "jewelcrafting", "leatherworking", "mining", "skinning", "tailoring" }
local trimString
local pickValue
local formatUpdatedAt

local theme = addon.NewUITheme and addon.NewUITheme.Default or {
    Card = {
        Background = { 0.12, 0.12, 0.14, 0.98 },
        Border = { 0.34, 0.34, 0.4, 1 },
        Title = { 0.96, 0.93, 0.84, 1 },
        Value = { 0.72, 0.68, 0.58, 1 },
    },
    Panel = {
        Background = { 0.12, 0.12, 0.14, 0.98 },
        Border = { 0.34, 0.34, 0.4, 1 },
        Title = { 0.96, 0.93, 0.84, 1 },
    },
}

local TITLE_FONT_PATH = "Fonts\\MORPHEUS.ttf"
local DEFAULT_PANE_BODY_FONT_KEY = "default"
local DEFAULT_PANE_TITLE_FONT_KEY = "morpheus"
local PANE_TITLE_FONT_OPTIONS = {
    { key = "morpheus", label = "Morpheus", path = "Fonts\\MORPHEUS.ttf" },
    { key = "frizqt", label = "Friz Quadrata", path = "Fonts\\FRIZQT__.TTF" },
    { key = "arialn", label = "Arial Narrow", path = "Fonts\\ARIALN.ttf" },
    { key = "skurri", label = "Skurri", path = "Fonts\\skurri.ttf" },
}
local PANE_BODY_FONT_OPTIONS = {
    { key = "default", label = "Default UI", path = false },
    { key = "frizqt", label = "Friz Quadrata", path = "Fonts\\FRIZQT__.TTF" },
    { key = "arialn", label = "Arial Narrow", path = "Fonts\\ARIALN.ttf" },
    { key = "morpheus", label = "Morpheus", path = "Fonts\\MORPHEUS.ttf" },
    { key = "skurri", label = "Skurri", path = "Fonts\\skurri.ttf" },
}
local LABEL_TEXT_COLOR = { 0.96, 0.82, 0.46, 1 }
local VALUE_TEXT_COLOR = { 0.96, 0.93, 0.84, 1 }
local MUTED_TEXT_COLOR = { 0.72, 0.68, 0.58, 1 }

local function getPaneFontOption(options, optionKey, defaultKey)
    local resolvedKey = tostring(optionKey or defaultKey or "")

    for _, option in ipairs(options or {}) do
        if option.key == resolvedKey then
            return option
        end
    end

    for _, option in ipairs(options or {}) do
        if option.key == defaultKey then
            return option
        end
    end

    return options and options[1] or nil
end

local function getPaneFontProfile()
    local profile = addon.Store and addon.Store.GetProfile and addon.Store:GetProfile() or nil

    if type(profile) ~= "table" then
        return nil
    end

    profile.ui = profile.ui or {}
    profile.ui.fonts = profile.ui.fonts or {}
    return profile.ui.fonts
end

local function getConfiguredPaneFontOption(fontKind)
    local fontProfile = getPaneFontProfile() or {}

    if fontKind == "paneTitle" then
        return getPaneFontOption(PANE_TITLE_FONT_OPTIONS, fontProfile.paneTitle, DEFAULT_PANE_TITLE_FONT_KEY)
    end

    return getPaneFontOption(PANE_BODY_FONT_OPTIONS, fontProfile.paneBody, DEFAULT_PANE_BODY_FONT_KEY)
end

local function getConfiguredPaneFontPath(fontKind)
    local option = getConfiguredPaneFontOption(fontKind)
    return option and option.path or nil
end

local function applyConfiguredFont(fontObject, requestedPath, requestedSize, requestedFlags, fallbackPath, fallbackSize, fallbackFlags)
    if not fontObject or type(fontObject.SetFont) ~= "function" then
        return
    end

    local currentPath
    local currentSize
    local currentFlags

    if type(fontObject.GetFont) == "function" then
        currentPath, currentSize, currentFlags = fontObject:GetFont()
    end

    local fontPath = requestedPath or currentPath or fallbackPath
    local fontSize = requestedSize or currentSize or fallbackSize
    local fontFlags = requestedFlags

    if fontFlags == nil then
        if currentFlags ~= nil and currentFlags ~= "" then
            fontFlags = currentFlags
        else
            fontFlags = fallbackFlags or ""
        end
    end

    if fontPath and fontSize then
        fontObject:SetFont(fontPath, fontSize, fontFlags)
    end
end

local function stylePaneTitleText(fontString, size, color, flags)
    if not fontString then
        return
    end

    applyConfiguredFont(fontString, getConfiguredPaneFontPath("paneTitle") or TITLE_FONT_PATH, size, flags, TITLE_FONT_PATH, size or 16, flags or "OUTLINE")
    fontString:SetTextColor(unpack(color or VALUE_TEXT_COLOR))
    fontString:SetShadowOffset(1, -1)
end

local function stylePaneBodyText(fontString, size, color, flags)
    if not fontString then
        return
    end

    applyConfiguredFont(fontString, getConfiguredPaneFontPath("paneBody"), size, flags, nil, size or 12, flags or "")
    fontString:SetTextColor(unpack(color or VALUE_TEXT_COLOR))
    fontString:SetShadowOffset(1, -1)
end

local function stylePaneInputText(input, size)
    if not input then
        return
    end

    applyConfiguredFont(input, getConfiguredPaneFontPath("paneBody"), size, nil, nil, size or 12, "")
    input:SetTextColor(unpack(VALUE_TEXT_COLOR))
    input:SetShadowOffset(1, -1)
end

local function styleLabelText(fontString, size)
    stylePaneBodyText(fontString, size, LABEL_TEXT_COLOR)
end

local function styleMutedText(fontString, size)
    stylePaneBodyText(fontString, size, MUTED_TEXT_COLOR)
end

local function styleValueText(fontString, size, color)
    stylePaneBodyText(fontString, size, color or VALUE_TEXT_COLOR)
end

local function styleLargeValueText(fontString, size, color)
    stylePaneTitleText(fontString, size or 15, color or VALUE_TEXT_COLOR, "")
end

local function resolveTexturePath(textureKeyOrPath)
    if type(textureKeyOrPath) ~= "string" or textureKeyOrPath == "" then
        return nil
    end

    local textures = addon.NewUIAssets and addon.NewUIAssets.Textures or nil
    if textures and textures[textureKeyOrPath] ~= nil then
        return textures[textureKeyOrPath] ~= "" and textures[textureKeyOrPath] or nil
    end

    return textureKeyOrPath
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 resolveAsset(textureKey, fallbackInsets, fallbackWidth, fallbackHeight)
    local metrics = resolveArtMetrics(textureKey) or {}
    return {
        texture = resolveTexturePath(textureKey),
        sourceWidth = tonumber(metrics.sourceWidth) or tonumber(fallbackWidth) or 1,
        sourceHeight = tonumber(metrics.sourceHeight) or tonumber(fallbackHeight) or 1,
        insets = metrics.insets or fallbackInsets or {},
        texCoord = metrics.texCoord,
    }
end

local function ApplyRect(frame, parent, rect)
    if not frame or not parent or not rect then
        return
    end

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

local function CreateArtFrame(parent, rect, asset)
    local frame = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(frame, parent, rect)

    local texture = frame:CreateTexture(nil, "BACKGROUND")
    texture:SetAllPoints(frame)
    if asset and asset.texture then
        texture:SetTexture(asset.texture)
        texture:SetVertexColor(1, 1, 1, 0.98)
        if type(asset.texCoord) == "table" and #asset.texCoord == 4 then
            texture:SetTexCoord(asset.texCoord[1], asset.texCoord[3], asset.texCoord[2], asset.texCoord[4])
        end
    else
        texture:SetColorTexture(0.12, 0.12, 0.14, 0.98)
    end

    local content = CreateFrame("Frame", nil, frame)
    local sourceWidth = tonumber(asset and asset.sourceWidth) or math.max(1, rect.w or 1)
    local sourceHeight = tonumber(asset and asset.sourceHeight) or math.max(1, rect.h or 1)
    local insets = asset and asset.insets or {}
    local scaleX = (rect.w or 1) / math.max(1, sourceWidth)
    local scaleY = (rect.h or 1) / math.max(1, sourceHeight)
    local left = math.floor(((tonumber(insets.left) or 0) * scaleX) + 0.5)
    local right = math.floor(((tonumber(insets.right) or 0) * scaleX) + 0.5)
    local top = math.floor(((tonumber(insets.top) or 0) * scaleY) + 0.5)
    local bottom = math.floor(((tonumber(insets.bottom) or 0) * scaleY) + 0.5)
    content:SetPoint("TOPLEFT", frame, "TOPLEFT", left, -top)
    content:SetSize(math.max(1, (rect.w or 1) - left - right), math.max(1, (rect.h or 1) - top - bottom))

    frame.Texture = texture
    frame.Content = content
    return frame
end

local function CreateSection(parent, rect, titleText, iconPath)
    local section = CreateArtFrame(parent, rect, resolveAsset("MyToonsPanelBackground", { left = 16, right = 16, top = 12, bottom = 12 }, rect.w, rect.h))
    local titleX = 0

    if trimString(iconPath) ~= "" then
        local icon = section.Content:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", section.Content, "TOPLEFT", 0, -1)
        icon:SetSize(18, 18)
        icon:SetTexture(iconPath)
        titleX = 28
        section.TitleIcon = icon
    end

    if trimString(titleText) ~= "" then
        local title = section.Content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOPLEFT", section.Content, "TOPLEFT", titleX, 0)
        title:SetText(titleText)
        stylePaneTitleText(title, 16, (theme.Panel and theme.Panel.Title) or VALUE_TEXT_COLOR, "OUTLINE")
        section.TitleText = title

        local body = CreateFrame("Frame", nil, section)
        body:SetPoint("TOPLEFT", section.Content, "TOPLEFT", 0, -30)
        body:SetPoint("BOTTOMRIGHT", section.Content, "BOTTOMRIGHT", 0, 0)
        section.Content = body
    end

    return section
end

local function resolveClassToken(classFile, className)
    for _, value in ipairs({ classFile, className }) do
        local normalized = string.upper((trimString(value):gsub("%s+", "")))
        if normalized ~= "" and type(CLASS_ICON_TCOORDS) == "table" and CLASS_ICON_TCOORDS[normalized] then
            return normalized
        end
    end
    return ""
end

local function resolveSummaryRoleIcon(roleText)
    local normalized = string.lower(trimString(roleText))
    if string.find(normalized, "tank", 1, true) then return "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Role_Tank-l.png" end
    if string.find(normalized, "heal", 1, true) then return "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Role_Healer-l.png" end
    return "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Role_DPS-l.png"
end

local function formatPreciseItemLevel(primaryValue, fallbackValue)
    local numericValue = tonumber(primaryValue)
    if numericValue == nil then numericValue = tonumber(fallbackValue) end
    if numericValue and numericValue > 0 then return string.format("%.1f", numericValue) end
    return pickValue(primaryValue, pickValue(fallbackValue, "-"))
end

local function formatDisplayLabel(textValue)
    local trimmed = trimString(textValue)
    if trimmed == "" then return "-" end
    if string.upper(trimmed) == "DPS" then return "DPS" end
    return trimmed:gsub("(%a)([%w']*)", function(first, rest) return string.upper(first) .. string.lower(rest) end)
end

local function resolveFactionVisual(factionText)
    local normalized = string.lower(trimString(factionText))

    if string.find(normalized, "horde", 1, true) then
        return "HomeMythicCardBackground", { 0.96, 0.28, 0.12, 1 }
    end

    if string.find(normalized, "alliance", 1, true) then
        return "HomeNewsCardBackground", { 0.52, 0.76, 1.00, 1 }
    end

    return "MyToonsCardBackground", VALUE_TEXT_COLOR
end

local function ResolveStatusVisual(statusText)
    local normalized = string.lower(trimString(statusText))
    if string.find(normalized, "ready", 1, true) or string.find(normalized, "complete", 1, true) then return { 0.11, 0.32, 0.14, 0.96 }, { 0.42, 0.92, 0.36, 1 } end
    if string.find(normalized, "missing", 1, true) or string.find(normalized, "issues", 1, true) then return { 0.36, 0.09, 0.07, 0.96 }, { 0.96, 0.40, 0.28, 1 } end
    if string.find(normalized, "low", 1, true) or string.find(normalized, "attention", 1, true) then return { 0.34, 0.18, 0.05, 0.96 }, { 0.96, 0.74, 0.24, 1 } end
    return { 0.11, 0.15, 0.24, 0.96 }, { 0.62, 0.82, 1, 1 }
end

local function resolveSummaryCardAsset(textValue)
    local normalized = string.lower(trimString(textValue))
    if string.find(normalized, "ready", 1, true) then return "HomeMembersCardBackground", { 1, 1, 1, 0.98 }, { 0.90, 0.98, 0.82, 1 } end
    if string.find(normalized, "gem", 1, true) then return "HomeRaidCardBackground", { 1, 1, 1, 0.98 }, { 1.00, 0.92, 0.58, 1 } end
    if string.find(normalized, "enchant", 1, true) then return "HomeMythicCardBackground", { 1, 1, 1, 0.98 }, { 0.92, 0.82, 1.00, 1 } end
    return "HomeNewsCardBackground", { 1, 1, 1, 0.98 }, { 0.84, 0.92, 1.00, 1 }
end

local function CreateCardActionButton(parent, rect, textValue, assetKey, tint, textColor, onClick)
    local button = CreateArtFrame(parent, rect, resolveAsset(assetKey, { left = 28, right = 28, top = 10, bottom = 10 }, rect.w, rect.h))
    button:EnableMouse(true)
    if button.Texture and type(tint) == "table" then
        button.Texture:SetVertexColor(tint[1] or 1, tint[2] or 1, tint[3] or 1, tint[4] or 0.98)
    end

    local text = button.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("CENTER", button.Content, "CENTER", 0, 0)
    text:SetText(textValue)
    stylePaneTitleText(text, 14, textColor or VALUE_TEXT_COLOR, "OUTLINE")

    button:SetScript("OnMouseUp", function()
        if type(onClick) == "function" then onClick(button) end
    end)

    button.Text = text
    return button
end

local function CreateSummaryAlertChip(parent, x, y, width, height, textValue, tint, iconPath)
    local assetKey, textureTint, textColor = resolveSummaryCardAsset(textValue)
    local chip = CreateArtFrame(parent, { x = x, y = y, w = width, h = height }, resolveAsset(assetKey, { left = 28, right = 28, top = 10, bottom = 10 }, width, height))
    if chip.Texture and type(textureTint) == "table" then
        chip.Texture:SetVertexColor(textureTint[1] or 1, textureTint[2] or 1, textureTint[3] or 1, textureTint[4] or 0.98)
    end

    local icon = chip:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("LEFT", chip, "LEFT", 10, 0)
    icon:SetSize(16, 16)
    icon:SetTexture(iconPath)

    local text = chip:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("LEFT", icon, "RIGHT", 10, 0)
    text:SetWidth(width - 40)
    text:SetWordWrap(false)
    text:SetJustifyH("LEFT")
    text:SetText(textValue)
    styleLargeValueText(text, 13, textColor)

    chip.Text = text
    return chip
end

local function CreateSummaryBlock(parent, x, y, width, labelText, primaryText, secondaryText, iconPath, primaryColor, useLargeFont)
    local block = CreateFrame("Frame", nil, parent)
    block:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    block:SetSize(width, 56)

    local label = block:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("TOPLEFT", block, "TOPLEFT", 0, 0)
    label:SetText(labelText)
    styleLabelText(label, 11)

    local leftInset = 0
    local icon = nil
    if trimString(iconPath) ~= "" then
        icon = block:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", block, "TOPLEFT", 0, -18)
        icon:SetSize(28, 28)
        icon:SetTexture(iconPath)
        leftInset = 36
    end

    local primary = block:CreateFontString(nil, "OVERLAY", useLargeFont and "GameFontNormalLarge" or "GameFontHighlight")
    primary:SetPoint("TOPLEFT", block, "TOPLEFT", leftInset, -18)
    primary:SetWidth(math.max(1, width - leftInset))
    primary:SetWordWrap(false)
    primary:SetJustifyH("LEFT")
    primary:SetText(primaryText)
    if useLargeFont then
        styleLargeValueText(primary, 18, primaryColor or VALUE_TEXT_COLOR)
    else
        styleValueText(primary, 13, primaryColor or VALUE_TEXT_COLOR)
    end

    if trimString(secondaryText) ~= "" then
        local secondary = block:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        secondary:SetPoint("TOPLEFT", primary, "BOTTOMLEFT", 0, -2)
        secondary:SetWidth(math.max(1, width - leftInset))
        secondary:SetWordWrap(false)
        secondary:SetJustifyH("LEFT")
        secondary:SetText(secondaryText)
        styleValueText(secondary, 12)
        block.SecondaryText = secondary
    end

    block.Icon = icon
    block.PrimaryText = primary
    return block
end

local function CreateFieldBar(parent, x, y, width, labelText, valueText, options)
    local resolved = type(options) == "table" and options or {}
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    label:SetText(labelText)
    styleLabelText(label, 12)

    local bar = CreateArtFrame(parent, { x = x, y = y - 18, w = width, h = resolved.height or 30 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, width, resolved.height or 30))
    local value = bar.Content:CreateFontString(nil, "OVERLAY", resolved.multiline and "GameFontHighlight" or "GameFontHighlightLarge")
    value:SetPoint("TOPLEFT", bar.Content, "TOPLEFT", 8, -1)
    value:SetPoint("BOTTOMRIGHT", bar.Content, "BOTTOMRIGHT", resolved.rightInset or -8, 1)
    value:SetJustifyH("LEFT")
    value:SetJustifyV(resolved.multiline and "TOP" or "MIDDLE")
    value:SetWordWrap(resolved.multiline == true)
    value:SetText(valueText)
    if resolved.multiline then
        styleValueText(value, 12, resolved.valueColor or VALUE_TEXT_COLOR)
    else
        styleLargeValueText(value, resolved.largeFont and 16 or 13, resolved.valueColor or VALUE_TEXT_COLOR)
    end

    if trimString(resolved.iconPath) ~= "" then
        local icon = bar.Content:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("RIGHT", bar.Content, "RIGHT", -6, 0)
        icon:SetSize(16, 16)
        icon:SetTexture(resolved.iconPath)
        bar.RightIcon = icon
    end

    bar.LabelText = label
    bar.ValueText = value
    return bar
end

local function joinDisplayValues(values)
    local labels = {}
    for index, value in ipairs(type(values) == "table" and values or {}) do
        labels[index] = formatDisplayLabel(value)
    end
    return #labels > 0 and table.concat(labels, ", ") or "-"
end

local function hidePopupMenu(frame)
    if frame then
        if frame.childMenu then
            hidePopupMenu(frame.childMenu)
        end
        frame:Hide()
    end
end

local function ensurePopupMenuFrame()
    local anchorFrame = page._popupAnchorFrame

    if anchorFrame and anchorFrame._gh3MenuFrame then
        return anchorFrame._gh3MenuFrame
    end

    local frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    frame:SetFrameStrata("TOOLTIP")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:Hide()
    frame.itemButtons = {}
    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.05, 0.06, 0.08, 0.96)
        frame:SetBackdropBorderColor(0.44, 0.34, 0.16, 0.95)
    end
    frame:SetScript("OnHide", function(self)
        if self.childMenu then
            hidePopupMenu(self.childMenu)
        end
    end)

    if anchorFrame then
        anchorFrame._gh3MenuFrame = frame
    else
        page._popupMenuFrame = frame
    end

    return frame
end

local function ensurePopupMenuItem(frame, index)
    local item = frame.itemButtons[index]
    if item then return item end

    item = CreateFrame("Button", nil, frame, BackdropTemplateMixin and "BackdropTemplate" or nil)
    if item.SetBackdrop then
        item:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        item:SetBackdropColor(0.10, 0.11, 0.14, 0.92)
        item:SetBackdropBorderColor(0.30, 0.24, 0.12, 0.75)
    end
    local text = item:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("LEFT", item, "LEFT", 8, 0)
    text:SetPoint("RIGHT", item, "RIGHT", -8, 0)
    text:SetJustifyH("LEFT")
    item.Text = text
    styleValueText(text, 12)
    frame.itemButtons[index] = item
    return item
end

local function measurePopupMenuWidth(anchorFrame, menuItems)
    local width = math.max(160, tonumber(anchorFrame and anchorFrame:GetWidth()) or 160)

    for _, option in ipairs(type(menuItems) == "table" and menuItems or {}) do
        local text = tostring(option.text or option.label or option.value or "Select")
        local isChecked = type(option.checked) == "function" and option.checked() or option.checked
        local prefix = option.notCheckable and "" or (isChecked and "[x] " or "[ ] ")
        local suffix = option.hasArrow and "  >" or ""
        local estimatedWidth = (#tostring(prefix .. text .. suffix) * 7) + 28

        if estimatedWidth > width then
            width = estimatedWidth
        end
    end

    return math.min(width, 320)
end

local function populatePopupMenu(frame, anchorFrame, menuItems, level, menuList)
    local items = type(menuItems) == "function" and (menuItems(level, menuList) or {}) or (type(menuItems) == "table" and menuItems or {})
    local width = measurePopupMenuWidth(anchorFrame, items)

    for index, option in ipairs(items) do
        local item = ensurePopupMenuItem(frame, index)
        local isChecked = type(option.checked) == "function" and option.checked() or option.checked
        local prefix = option.notCheckable and "" or (isChecked and "[x] " or "[ ] ")
        item:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -8 - ((index - 1) * 24))
        item:SetSize(width, 22)
        item.Text:SetText(prefix .. tostring(option.text or "Select") .. (option.hasArrow and "  >" or ""))
        item._gh3Option = option
        item._gh3MenuList = menuList
        item._gh3MenuLevel = level or 1

        if option.disabled then
            item:Disable()
        else
            item:Enable()
        end

        item:SetScript("OnEnter", function(self)
            if self._gh3Option and self._gh3Option.hasArrow then
                frame.childMenu = frame.childMenu or CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
                frame.childMenu:SetFrameStrata("TOOLTIP")
                frame.childMenu:SetToplevel(true)
                frame.childMenu:SetClampedToScreen(true)
                frame.childMenu.itemButtons = frame.childMenu.itemButtons or {}
                if frame.childMenu.SetBackdrop then
                    frame.childMenu:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
                    frame.childMenu:SetBackdropColor(0.05, 0.06, 0.08, 0.96)
                    frame.childMenu:SetBackdropBorderColor(0.44, 0.34, 0.16, 0.95)
                end
                frame.childMenu:ClearAllPoints()
                frame.childMenu:SetPoint("TOPLEFT", self, "TOPRIGHT", 4, 0)
                populatePopupMenu(frame.childMenu, anchorFrame, menuItems, (self._gh3MenuLevel or 1) + 1, self._gh3Option.menuList)
                frame.childMenu:Show()
            elseif frame.childMenu then
                hidePopupMenu(frame.childMenu)
            end
        end)

        item:SetScript("OnClick", function()
            if option.disabled then
                return
            end

            if option.hasArrow then
                frame.childMenu = frame.childMenu or CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
                frame.childMenu:SetFrameStrata("TOOLTIP")
                frame.childMenu:SetToplevel(true)
                frame.childMenu:SetClampedToScreen(true)
                frame.childMenu.itemButtons = frame.childMenu.itemButtons or {}
                if frame.childMenu.SetBackdrop then
                    frame.childMenu:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
                    frame.childMenu:SetBackdropColor(0.05, 0.06, 0.08, 0.96)
                    frame.childMenu:SetBackdropBorderColor(0.44, 0.34, 0.16, 0.95)
                end
                frame.childMenu:ClearAllPoints()
                frame.childMenu:SetPoint("TOPLEFT", item, "TOPRIGHT", 4, 0)
                populatePopupMenu(frame.childMenu, anchorFrame, menuItems, (level or 1) + 1, option.menuList)
                frame.childMenu:Show()
                return
            end

            if type(option.onClick) == "function" then
                option.onClick(option.value, option, menuList, level)
            elseif type(option.func) == "function" then
                option.func()
            end

            if option.keepShownOnClick then
                populatePopupMenu(frame, anchorFrame, menuItems, level, menuList)
                frame:Show()
                page._activePopupAnchor = anchorFrame
            else
                hidePopupMenu(frame)
                page._activePopupAnchor = nil
            end
        end)
        item:Show()
    end

    for index = #items + 1, #(frame.itemButtons or {}) do
        frame.itemButtons[index]:Hide()
    end

    frame:SetSize(width + 16, (#items * 24) + 16)
end

local function ShowDropdownMenu(anchorFrame, menuItems)
    if not anchorFrame then return end

    page._popupAnchorFrame = anchorFrame
    local frame = ensurePopupMenuFrame()

    if page._activePopupAnchor == anchorFrame and frame:IsShown() then
        hidePopupMenu(frame)
        page._activePopupAnchor = nil
        return
    end

    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", anchorFrame, "BOTTOMLEFT", 0, -4)
    populatePopupMenu(frame, anchorFrame, menuItems, 1, nil)
    frame:Show()
    page._activePopupAnchor = anchorFrame
end

local function CreateInputControl(parent, x, y, width, valueText, onChanged, options)
    local resolved = type(options) == "table" and options or {}
    local height = resolved.height or 24
    local textInset = resolved.textInset or 6
    local bar = CreateArtFrame(parent, { x = x, y = y, w = width, h = height }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, width, height))
    local editBox = CreateFrame(resolved.multiline and "EditBox" or "EditBox", nil, bar, resolved.multiline and "InputBoxTemplate" or "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:SetAutoFocus(false)
    editBox:SetFontObject(resolved.multiline and "GameFontHighlight" or "GameFontHighlightLarge")
    editBox:SetPoint("TOPLEFT", bar.Content, "TOPLEFT", textInset, -1)
    editBox:SetPoint("BOTTOMRIGHT", bar.Content, "BOTTOMRIGHT", -(resolved.rightInset or 6), 1)
    editBox:SetTextInsets(0, 0, 0, 0)
    stylePaneInputText(editBox, resolved.multiline and 12 or 13)
    editBox:SetText(valueText or "")
    editBox:SetMultiLine(resolved.multiline == true)
    if resolved.numeric then editBox:SetNumeric(true) end
    if tonumber(resolved.maxLetters) then editBox:SetMaxLetters(math.max(1, math.floor(resolved.maxLetters))) end

    local function commit()
        if type(onChanged) == "function" then
            onChanged(trimString(editBox:GetText()))
        end
    end

    editBox:SetScript("OnEditFocusLost", commit)
    editBox:SetScript("OnEnterPressed", function(self)
        if not resolved.multiline then
            commit()
            self:ClearFocus()
        end
    end)
    editBox:SetScript("OnTextChanged", function(self)
        if type(onChanged) == "function" and resolved.liveUpdate then
            onChanged(trimString(self:GetText()))
        end
    end)

    bar.EditBox = editBox
    return bar
end

local function CreateSelectControl(parent, x, y, width, textValue, onOpen, options)
    local resolved = type(options) == "table" and options or {}
    local button = CreateArtFrame(parent, { x = x, y = y, w = width, h = resolved.height or 24 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, width, resolved.height or 24))
    button:EnableMouse(true)
    local text = button.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("LEFT", button.Content, "LEFT", resolved.textInset or 6, 0)
    text:SetWidth(math.max(1, width - ((resolved.textInset or 6) + 22)))
    text:SetJustifyH("LEFT")
    text:SetWordWrap(false)
    text:SetText(textValue)
    styleValueText(text, 12)

    local arrow = nil
    if resolved.showArrow ~= false then
        arrow = button.Content:CreateTexture(nil, "OVERLAY")
        arrow:SetPoint("RIGHT", button.Content, "RIGHT", -4, 0)
        arrow:SetSize(14, 14)
        arrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
        arrow:SetVertexColor(0.96, 0.82, 0.46, 1)
    end

    button:SetScript("OnMouseUp", function()
        if type(onOpen) == "function" then
            onOpen(button)
        end
    end)

    button.ValueText = text
    button.Arrow = arrow
    return button
end

local function formatTimePlayedDuration(totalSeconds)
    local seconds = math.max(0, math.floor(tonumber(totalSeconds) or 0))
    local days = math.floor(seconds / 86400)
    local hours = math.floor((seconds % 86400) / 3600)
    local minutes = math.floor((seconds % 3600) / 60)
    if days > 0 then return string.format("%dd %dh", days, hours) end
    if hours > 0 then return string.format("%dh %dm", hours, minutes) end
    return string.format("%dm", minutes)
end

local function ensureCurrentToonTimePlayedRequest(selectedRow)
    local playerId = addon.Store and addon.Store.GetPlayerName and trimString(addon.Store:GetPlayerName()) or ""
    if playerId == "" or not selectedRow or trimString(selectedRow.id) ~= playerId or type(RequestTimePlayed) ~= "function" then
        return
    end

    page.State = page.State or {}
    if type(page.State.timePlayed) == "table" and page.State.timePlayed.toonId == playerId then
        return
    end

    if not page._timePlayedFrame then
        page._timePlayedFrame = CreateFrame("Frame")
        page._timePlayedFrame:SetScript("OnEvent", function(self, event, totalTime)
            if event ~= "TIME_PLAYED_MSG" then return end
            self:UnregisterEvent("TIME_PLAYED_MSG")
            local activeId = page._activeToonId or ""
            page.State.timePlayed = {
                toonId = addon.Store and addon.Store.GetPlayerName and trimString(addon.Store:GetPlayerName()) or "",
                total = tonumber(totalTime) or 0,
            }
            if page.State.formValuesByToonId and activeId ~= "" and page.State.formValuesByToonId[activeId] then
                page.State.formValuesByToonId[activeId].timePlayedSeconds = tonumber(totalTime) or 0
            end
            if page._activeTimePlayedText and page.State.timePlayed.toonId == activeId then
                page._activeTimePlayedText:SetText(formatTimePlayedDuration(page.State.timePlayed.total))
            end
        end)
    end

    page._timePlayedFrame:RegisterEvent("TIME_PLAYED_MSG")
    RequestTimePlayed()
end

local function resolveInterestTagPalette(labelText)
    local normalized = string.lower(trimString(labelText))
    if string.find(normalized, "raid", 1, true) then return { 0.34, 0.12, 0.10, 0.94 }, { 0.84, 0.36, 0.30, 1 } end
    if string.find(normalized, "mythic", 1, true) then return { 0.18, 0.14, 0.36, 0.94 }, { 0.66, 0.54, 0.96, 1 } end
    if string.find(normalized, "pvp", 1, true) then return { 0.34, 0.18, 0.05, 0.94 }, { 0.96, 0.74, 0.24, 1 } end
    if string.find(normalized, "profession", 1, true) or string.find(normalized, "achievement", 1, true) then return { 0.10, 0.26, 0.12, 0.94 }, { 0.46, 0.88, 0.36, 1 } end
    return { 0.12, 0.14, 0.18, 0.94 }, { 0.72, 0.68, 0.58, 1 }
end

local function CreateInterestTag(parent, x, y, width, textValue)
    local backgroundColor, borderColor = resolveInterestTagPalette(textValue)
    local chip = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    chip:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    chip:SetSize(width, 28)
    if chip.SetBackdrop then
        chip:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        chip:SetBackdropColor(backgroundColor[1], backgroundColor[2], backgroundColor[3], backgroundColor[4])
        chip:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4])
    end

    local text = chip:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("CENTER", chip, "CENTER", 0, 0)
    text:SetText(formatDisplayLabel(textValue))
    text:SetTextColor(borderColor[1], borderColor[2], borderColor[3], 1)
    chip.Text = text
    return chip
end

local function CreatePreferenceRow(parent, y, iconPath, labelText, enabled, options)
    local resolved = type(options) == "table" and options or {}
    local icon = parent:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, y)
    icon:SetSize(18, 18)
    icon:SetTexture(iconPath)

    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("LEFT", icon, "RIGHT", 12, 0)
    label:SetText(labelText)
    styleValueText(label, 12)

    local check = CreateArtFrame(parent, { x = 196, y = y + 2, w = 32, h = 22 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, 32, 22))
    local checkText = check.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    checkText:SetPoint("CENTER", check.Content, "CENTER", 0, 0)
    styleValueText(checkText, 12)
    local function applyToggleState(nextValue)
        checkText:SetText(nextValue and "Yes" or "No")
        checkText:SetTextColor(nextValue and 0.42 or 0.96, nextValue and 0.92 or 0.40, nextValue and 0.36 or 0.28, 1)
    end
    applyToggleState(enabled)

    check:EnableMouse(type(resolved.onToggle) == "function")
    check:SetScript("OnMouseUp", function()
        if type(resolved.onToggle) ~= "function" then
            return
        end

        enabled = not enabled
        applyToggleState(enabled)
        resolved.onToggle(enabled)
    end)

    if trimString(resolved.secondaryText) ~= "" then
        local extra = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        extra:SetPoint("LEFT", check, "RIGHT", 14, 0)
        extra:SetText(resolved.secondaryText)
        styleValueText(extra, 12)
        check.SecondaryText = extra
    end

    return check
end

local function CreateQuickAlertRow(parent, y, iconPath, titleText, subtitleText, tint)
    local row = CreateArtFrame(parent, { x = 0, y = y, w = math.max(1, tonumber(parent:GetWidth()) or 320), h = 42 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, tonumber(parent:GetWidth()) or 320, 42))
    local icon = row.Content:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("LEFT", row.Content, "LEFT", 6, 0)
    icon:SetSize(18, 18)
    icon:SetTexture(iconPath)

    local title = row.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -1)
    title:SetWordWrap(false)
    title:SetText(titleText)
    styleLargeValueText(title, 13, tint)

    local subtitle = row.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
    subtitle:SetWidth(math.max(1, (tonumber(parent:GetWidth()) or 320) - 44))
    subtitle:SetJustifyH("LEFT")
    subtitle:SetText(subtitleText)
    styleMutedText(subtitle, 11)
end

local function CreateLegacyCalendarButton(parent, width, height, point, relativeTo, relativePoint, x, y, textValue, onClick)
    local button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    button:SetSize(width, height)
    button:SetPoint(point, relativeTo, relativePoint, x, y)
    button:SetText(textValue or "")
    if button.GetFontString and button:GetFontString() then
        stylePaneBodyText(button:GetFontString(), 12, VALUE_TEXT_COLOR)
    end
    if type(onClick) == "function" then
        button:SetScript("OnClick", onClick)
    end
    return button
end

local function CreateLegacyCalendarInput(parent, width, height, point, relativeTo, relativePoint, x, y)
    local input = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    input:SetAutoFocus(false)
    input:SetSize(width, height)
    input:SetPoint(point, relativeTo, relativePoint, x, y)
    stylePaneInputText(input, 12)
    return input
end

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

pickValue = function(value, fallback)
    if value == nil then
        return fallback or "-"
    end

    if type(value) == "string" then
        if value == "" then
            return fallback or "-"
        end
        return value
    end

    return tostring(value)
end

formatUpdatedAt = function(updatedAt)
    if type(updatedAt) == "number" then
        return date("%Y-%m-%d %H:%M:%S", updatedAt)
    end

    return pickValue(updatedAt, "-")
end

local function splitIsoDateParts(value)
    local yearText, monthText, dayText = trimString(value):match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
    return tonumber(yearText), tonumber(monthText), tonumber(dayText)
end

local function isLeapYear(year)
    if tonumber(year) == nil then
        return false
    end

    year = math.floor(tonumber(year) or 0)
    return (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
end

local function getDaysInMonth(year, month)
    local monthDays = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
    if tonumber(month) == 2 and isLeapYear(year) then
        return 29
    end
    return monthDays[tonumber(month) or 0] or 31
end

local function buildIsoDate(year, month, day)
    if tonumber(year) == nil or tonumber(month) == nil or tonumber(day) == nil then
        return ""
    end

    year = math.floor(tonumber(year) or 0)
    month = math.floor(tonumber(month) or 0)
    day = math.floor(tonumber(day) or 0)

    if year < 1900 or year > 2100 or month < 1 or month > 12 or day < 1 or day > getDaysInMonth(year, month) then
        return ""
    end

    return string.format("%04d-%02d-%02d", year, month, day)
end

local function createFieldRows(panel, rows, options)
    if not panel or not panel.Content or not addon.NewUIComponents or not addon.NewUIComponents.CreateFieldRow then
        return
    end

    for _, rowData in ipairs(rows) do
        addon.NewUIComponents.CreateFieldRow(panel.Content, rowData.y, rowData.label, rowData.value, {
            x = rowData.x or 0,
            labelW = (options and options.labelW) or 110,
            valueW = (options and options.valueW) or 220,
            rowH = (options and options.rowH) or 18,
            gap = (options and options.gap) or 8,
        })
    end
end

local function cloneList(values)
    local copy = {}

    for _, value in ipairs(type(values) == "table" and values or {}) do
        copy[#copy + 1] = value
    end

    return copy
end

local function deepCopyValue(value)
    if type(value) ~= "table" then
        return value
    end

    local copy = {}
    for key, nestedValue in pairs(value) do
        copy[key] = deepCopyValue(nestedValue)
    end
    return copy
end

local AVAILABILITY_DAY_OPTIONS = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }

local function normalizeAvailabilityDays(values)
    local lookup = {}

    for _, value in ipairs(type(values) == "table" and values or {}) do
        local trimmed = trimString(value)
        if trimmed ~= "" then
            lookup[string.lower(trimmed)] = true
        end
    end

    local resolved = {}
    for _, day in ipairs(AVAILABILITY_DAY_OPTIONS) do
        if lookup[string.lower(day)] then
            resolved[#resolved + 1] = day
        end
    end

    return resolved
end

local function formatAvailabilitySummary(days, startText, endText, fallbackText)
    local dayList = normalizeAvailabilityDays(days)
    local startValue = trimString(startText)
    local endValue = trimString(endText)
    local parts = {}

    if #dayList > 0 then
        parts[#parts + 1] = table.concat(dayList, ", ")
    end

    if startValue ~= "" and endValue ~= "" then
        parts[#parts + 1] = startValue .. " - " .. endValue
    elseif startValue ~= "" then
        parts[#parts + 1] = "From " .. startValue
    elseif endValue ~= "" then
        parts[#parts + 1] = "Until " .. endValue
    end

    if #parts > 0 then
        return table.concat(parts, " | ")
    end

    return trimString(fallbackText)
end

local function getDefaultReadinessPreferences()
    local defaults = addon.ReadinessService and type(addon.ReadinessService.GetDefaultReadiness) == "function" and addon.ReadinessService:GetDefaultReadiness() or nil
    local source = type(defaults) == "table" and type(defaults.preferences) == "table" and defaults.preferences or {}

    return {
        trackGems = source.trackGems ~= false,
        trackEnchants = source.trackEnchants ~= false,
        trackConsumables = source.trackConsumables ~= false,
        trackDurability = source.trackDurability ~= false,
        weeklyVault = source.weeklyVault ~= false,
        durabilityAlertBelow = math.max(0, math.min(100, math.floor(tonumber(source.durabilityAlertBelow) or 95))),
    }
end

local function normalizeReadinessPreferences(preferences)
    local normalized = getDefaultReadinessPreferences()
    local source = type(preferences) == "table" and preferences or {}

    for key, value in pairs(source) do
        if key ~= "durabilityAlertBelow" and normalized[key] ~= nil then
            normalized[key] = value and true or false
        end
    end

    if tonumber(source.durabilityAlertBelow) ~= nil then
        normalized.durabilityAlertBelow = math.max(0, math.min(100, math.floor(tonumber(source.durabilityAlertBelow) or normalized.durabilityAlertBelow)))
    end

    return normalized
end

local function withClick(options, onClick)
    local resolved = {}

    for key, value in pairs(type(options) == "table" and options or {}) do
        resolved[key] = value
    end

    resolved.onClick = onClick
    return resolved
end

local function buildSavePayload(selectedRow, formState)
    local member = type(selectedRow) == "table" and type(selectedRow.member) == "table" and selectedRow.member or {}
    local myToonRecord = type(selectedRow) == "table" and type(selectedRow.myToonRecord) == "table" and selectedRow.myToonRecord or {}
    local state = type(formState) == "table" and formState or {}
    local availabilityDays = normalizeAvailabilityDays(state.availabilityDays)
    local availabilityStart = trimString(state.availabilityStart)
    local availabilityEnd = trimString(state.availabilityEnd)
    local availabilitySummary = formatAvailabilitySummary(
        availabilityDays,
        availabilityStart,
        availabilityEnd,
        state._availabilityTouched and "" or state.availabilityLegacy
    )
    local payload = {
        spec = state.spec or selectedRow and selectedRow.spec or member.spec or "",
        itemLevel = tonumber(state.itemLevel or selectedRow and selectedRow.itemLevel or member.itemLevel) or 0,
        primaryRole = state.primaryRole or selectedRow and selectedRow.role or member.primaryRole or "",
        meleeOrRanged = state.meleeOrRanged or member.meleeOrRanged or myToonRecord.meleeOrRanged or "",
        secondaryRoles = cloneList(state.secondaryRoles or member.secondaryRoles or myToonRecord.secondaryRoles),
        mainCharacter = state.mainCharacter or selectedRow and selectedRow.mainCharacter or member.mainCharacter or "",
        guildJoinDate = state.guildJoinDate or member.guildJoinDate or myToonRecord.guildJoinDate or "",
        birthdate = state.birthdate or myToonRecord.birthdate or member.birthdate or "",
        battleTag = state.battleTag or myToonRecord.battleTag or member.battleTag or "",
        note = state.note or myToonRecord.note or member.note or "",
        professions = cloneList(state.professions or myToonRecord.professions or member.professions),
        interests = cloneList(state.interests or myToonRecord.interests or member.interests),
        availability = availabilitySummary,
        availabilityDays = cloneList(availabilityDays),
        availabilityStart = availabilityStart,
        availabilityEnd = availabilityEnd,
    }
    local readinessPayload = type(myToonRecord.readiness) == "table" and addon.ReadinessService and addon.ReadinessService.NormalizeReadiness and addon.ReadinessService:NormalizeReadiness(myToonRecord.readiness) or nil

    if tonumber(state.timePlayedSeconds) and tonumber(state.timePlayedSeconds) > 0 then
        payload.timePlayedSeconds = tonumber(state.timePlayedSeconds)
    end

    if selectedRow and selectedRow.id == addon.Store:GetPlayerName()
        and type(addon.ReadinessService) == "table"
        and type(addon.ReadinessService.BuildCurrentReadinessSnapshot) == "function" then
        readinessPayload = addon.ReadinessService:BuildCurrentReadinessSnapshot()
    end

    if readinessPayload and type(state.readinessPreferences) == "table" then
        readinessPayload.preferences = readinessPayload.preferences or {}
        for key, value in pairs(normalizeReadinessPreferences(state.readinessPreferences)) do
            readinessPayload.preferences[key] = value
        end
    end

    if readinessPayload ~= nil then
        payload.readiness = readinessPayload
    end

    return payload
end

local function buildSelectedRow(memberId)
    if trimString(memberId or "") == ""
        or type(addon.RosterService) ~= "table"
        or type(addon.RosterService.GetMyToonMember) ~= "function"
        or type(addon.RosterService.GetMyToonRecord) ~= "function" then
        return nil
    end

    local member = addon.RosterService:GetMyToonMember(memberId)
    local myToonRecord = addon.RosterService:GetMyToonRecord(memberId)

    if type(member) ~= "table" then
        return nil
    end

    local numericItemLevel = tonumber(member.itemLevel) or 0
    return {
        id = member.id or memberId,
        name = member.fullName or member.name or member.id or memberId,
        realm = member.realm or "",
        className = member.className or member.classFile or "",
        spec = member.spec or "",
        role = member.primaryRole or member.spec or "",
        itemLevel = numericItemLevel > 0 and tostring(math.floor(numericItemLevel)) or "-",
        mainCharacter = member.mainCharacter or "",
        updatedAt = tonumber(member.updatedAt) or 0,
        updatedBy = member.updatedBy or "",
        member = member,
        myToonRecord = myToonRecord,
        readiness = type(member.readiness) == "table" and member.readiness or nil,
        readinessSnapshot = type(myToonRecord) == "table" and type(myToonRecord.readiness) == "table" and myToonRecord.readiness or nil,
        timePlayedSeconds = tonumber((myToonRecord and myToonRecord.timePlayedSeconds) or member.timePlayedSeconds) or 0,
    }
end

function page:Render(parent, selectedRow, navigation)
    if not parent or not addon.NewUIComponents or not addon.NewUIComponents.CreatePageRoot or not addon.NewUIComponents.CreatePanel then
        return nil
    end

    hidePopupMenu(page._popupMenuFrame)

    if page._rootFrame then
        page._rootFrame:Hide()
        page._rootFrame = nil
    end

    local root = addon.NewUIComponents.CreatePageRoot(parent)
    page._rootFrame = root
    local member = type(selectedRow) == "table" and type(selectedRow.member) == "table" and selectedRow.member or {}
    local record = type(selectedRow) == "table" and type(selectedRow.myToonRecord) == "table" and selectedRow.myToonRecord or {}
    local readinessSnapshot = type(record.readiness) == "table" and record.readiness or type(selectedRow) == "table" and type(selectedRow.readinessSnapshot) == "table" and selectedRow.readinessSnapshot or nil
    local toonId = trimString(selectedRow and selectedRow.id)
    page._activeToonId = toonId
    page.State.formValuesByToonId = page.State.formValuesByToonId or {}

    local function normalizeList(values)
        local resolved = {}
        local seen = {}
        for _, value in ipairs(type(values) == "table" and values or {}) do
            local trimmed = trimString(value)
            if trimmed ~= "" and not seen[string.lower(trimmed)] then
                seen[string.lower(trimmed)] = true
                resolved[#resolved + 1] = trimmed
            end
        end
        return resolved
    end

    local roleOptions = addon.RosterService and type(addon.RosterService.GetRoleOptions) == "function" and addon.RosterService:GetRoleOptions(member) or { "dps" }
    local profileOptions = addon.RosterService and type(addon.RosterService.GetProfileOptions) == "function" and addon.RosterService:GetProfileOptions(member) or { "unknown" }
    local myToonOptions = {}
    if addon.RosterService and type(addon.RosterService.ListMyToons) == "function" then
        for _, toon in ipairs(addon.RosterService:ListMyToons() or {}) do
            local optionValue = trimString(toon and (toon.id or toon.fullName or toon.name))
            if optionValue ~= "" then
                myToonOptions[#myToonOptions + 1] = optionValue
            end
        end
    end
    if #myToonOptions == 0 and toonId ~= "" then
        myToonOptions[1] = toonId
    end

    local formState = page.State.formValuesByToonId[toonId]
    if not formState then
        formState = {
            spec = trimString(selectedRow and selectedRow.spec or member.spec),
            itemLevel = tonumber(member.itemLevel or selectedRow and selectedRow.itemLevel) or 0,
            primaryRole = trimString(selectedRow and selectedRow.role or member.primaryRole),
            meleeOrRanged = trimString(member.meleeOrRanged or record.meleeOrRanged),
            secondaryRoles = normalizeList(record.secondaryRoles or member.secondaryRoles),
            mainCharacter = trimString(record.mainCharacter or selectedRow and selectedRow.mainCharacter or member.mainCharacter or toonId),
            guildJoinDate = trimString(record.guildJoinDate or member.guildJoinDate),
            birthdate = trimString(record.birthdate or member.birthdate),
            battleTag = trimString(record.battleTag or member.battleTag),
            note = trimString(record.note or member.note),
            professions = normalizeList(record.professions or member.professions),
            interests = normalizeList(record.interests or member.interests),
            availabilityDays = normalizeAvailabilityDays(record.availabilityDays or member.availabilityDays),
            availabilityStart = trimString(record.availabilityStart or member.availabilityStart),
            availabilityEnd = trimString(record.availabilityEnd or member.availabilityEnd),
            availabilityLegacy = trimString(record.availability or member.availability),
            _availabilityTouched = false,
            readinessPreferences = normalizeReadinessPreferences(readinessSnapshot and readinessSnapshot.preferences),
            timePlayedSeconds = tonumber(record.timePlayedSeconds or member.timePlayedSeconds or selectedRow and selectedRow.timePlayedSeconds) or 0,
        }
        page.State.formValuesByToonId[toonId] = formState
    elseif type(formState.readinessPreferences) ~= "table" then
        formState.readinessPreferences = normalizeReadinessPreferences(readinessSnapshot and readinessSnapshot.preferences)
    end

    formState.availabilityDays = normalizeAvailabilityDays(formState.availabilityDays)
    formState.availabilityStart = trimString(formState.availabilityStart)
    formState.availabilityEnd = trimString(formState.availabilityEnd)
    formState.availabilityLegacy = trimString(formState.availabilityLegacy or record.availability or member.availability)

    local function createRowLabel(parentFrame, x, y, textValue)
        local label = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", x, y)
        label:SetText(textValue)
        styleLabelText(label, 12)
        return label
    end

    local function createStaticRow(parentFrame, x, y, labelText, width, valueText, options)
        local labelWidth = (options and options.labelWidth) or 94
        local bar = CreateArtFrame(parentFrame, { x = x + labelWidth + 10, y = y, w = width, h = (options and options.height) or 24 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, width, (options and options.height) or 24))
        local value = bar.Content:CreateFontString(nil, "OVERLAY", (options and options.largeFont) and "GameFontHighlightLarge" or "GameFontHighlight")
        value:SetPoint("LEFT", bar.Content, "LEFT", (options and options.textInset) or 6, 0)
        value:SetWidth(math.max(1, width - (((options and options.textInset) or 6) + 6)))
        value:SetJustifyH("LEFT")
        value:SetWordWrap(false)
        value:SetText(valueText)
        if options and options.largeFont then
            styleLargeValueText(value, 16, options.valueColor or VALUE_TEXT_COLOR)
        else
            styleValueText(value, 12, options and options.valueColor or VALUE_TEXT_COLOR)
        end
        local label = bar:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("LEFT", bar, "LEFT", -labelWidth - 10, 0)
        label:SetWidth(labelWidth)
        label:SetJustifyH("LEFT")
        label:SetWordWrap(false)
        label:SetText(labelText)
        styleLabelText(label, 12)
        bar.LabelText = label
        return bar, value
    end

    local function createInputRow(parentFrame, x, y, labelText, width, valueText, onChanged, options)
        local labelWidth = (options and options.labelWidth) or 94
        local control = CreateInputControl(parentFrame, x + labelWidth + 10, y, width, valueText, onChanged, options)
        local label = control:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("LEFT", control, "LEFT", -labelWidth - 10, 0)
        label:SetWidth(labelWidth)
        label:SetJustifyH("LEFT")
        label:SetWordWrap(false)
        label:SetText(labelText)
        styleLabelText(label, 12)
        control.LabelText = label
        return control
    end

    local function createSelectRow(parentFrame, x, y, labelText, width, displayText, buildMenu, options)
        local labelWidth = (options and options.labelWidth) or 94
        local control = CreateSelectControl(parentFrame, x + labelWidth + 10, y, width, displayText, function(button)
            ShowDropdownMenu(button, buildMenu(button))
        end, options)
        local label = control:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("LEFT", control, "LEFT", -labelWidth - 10, 0)
        label:SetWidth(labelWidth)
        label:SetJustifyH("LEFT")
        label:SetWordWrap(false)
        label:SetText(labelText)
        styleLabelText(label, 12)
        control.LabelText = label
        return control
    end

    local function refreshAvailabilityState()
        formState.availabilityDays = normalizeAvailabilityDays(formState.availabilityDays)
        formState.availabilityStart = trimString(formState.availabilityStart)
        formState.availabilityEnd = trimString(formState.availabilityEnd)
        formState.availability = formatAvailabilitySummary(
            formState.availabilityDays,
            formState.availabilityStart,
            formState.availabilityEnd,
            formState._availabilityTouched and "" or formState.availabilityLegacy
        )

        if page._availabilitySummaryText then
            page._availabilitySummaryText:SetText(formState.availability ~= "" and formState.availability or "Select days and add times")
        end
    end

    local function createAvailabilityDayButton(parentFrame, x, y, dayLabel, width, height)
        local buttonWidth = math.max(20, math.floor(tonumber(width) or 28))
        local buttonHeight = math.max(18, math.floor(tonumber(height) or 20))
        local asset = resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, buttonWidth, buttonHeight)
        local button = CreateFrame("Button", nil, parentFrame, BackdropTemplateMixin and "BackdropTemplate" or nil)
        ApplyRect(button, parentFrame, { x = x, y = y, w = buttonWidth, h = buttonHeight })

        local texture = button:CreateTexture(nil, "BACKGROUND")
        texture:SetAllPoints(button)
        if asset and asset.texture then
            texture:SetTexture(asset.texture)
            texture:SetVertexColor(1, 1, 1, 0.82)
            if type(asset.texCoord) == "table" and #asset.texCoord == 4 then
                texture:SetTexCoord(asset.texCoord[1], asset.texCoord[3], asset.texCoord[2], asset.texCoord[4])
            end
        else
            texture:SetColorTexture(0.12, 0.12, 0.14, 0.98)
        end

        local content = CreateFrame("Frame", nil, button)
        local sourceWidth = tonumber(asset and asset.sourceWidth) or buttonWidth
        local sourceHeight = tonumber(asset and asset.sourceHeight) or buttonHeight
        local insets = asset and asset.insets or {}
        local scaleX = buttonWidth / math.max(1, sourceWidth)
        local scaleY = buttonHeight / math.max(1, sourceHeight)
        local left = math.floor(((tonumber(insets.left) or 0) * scaleX) + 0.5)
        local right = math.floor(((tonumber(insets.right) or 0) * scaleX) + 0.5)
        local top = math.floor(((tonumber(insets.top) or 0) * scaleY) + 0.5)
        local bottom = math.floor(((tonumber(insets.bottom) or 0) * scaleY) + 0.5)
        content:SetPoint("TOPLEFT", button, "TOPLEFT", left, -top)
        content:SetSize(math.max(1, buttonWidth - left - right), math.max(1, buttonHeight - top - bottom))

        button.Texture = texture
        button.Content = content

        local text = button.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        text:SetPoint("CENTER")
        text:SetText(dayLabel)
        styleValueText(text, buttonWidth <= 24 and 8 or 9)
        button.Text = text

        local function syncVisual()
            local isSelected = false
            for _, selectedDay in ipairs(type(formState.availabilityDays) == "table" and formState.availabilityDays or {}) do
                if selectedDay == dayLabel then
                    isSelected = true
                    break
                end
            end
            if button.Texture then
                if isSelected then
                    button.Texture:SetVertexColor(1, 1, 1, 1)
                else
                    button.Texture:SetVertexColor(1, 1, 1, 0.82)
                end
            end
            text:SetTextColor(isSelected and 0.96 or 0.82, isSelected and 0.82 or 0.77, isSelected and 0.46 or 0.65, 1)
        end

        button:SetScript("OnClick", function()
            local nextDays = {}
            local found = false

            for _, day in ipairs(normalizeAvailabilityDays(formState.availabilityDays)) do
                if day == dayLabel then
                    found = true
                else
                    nextDays[#nextDays + 1] = day
                end
            end

            if not found then
                nextDays[#nextDays + 1] = dayLabel
            end

            formState.availabilityDays = normalizeAvailabilityDays(nextDays)
            formState._availabilityTouched = true
            refreshAvailabilityState()
            syncVisual()
        end)

        syncVisual()
        return button
    end

    refreshAvailabilityState()

    local function initializeSingleSelectControl(control, options, selectedValue, formatValue, onChanged)
        local formatter = formatValue or tostring
        control.selectedValue = selectedValue
        if control.ValueText then
            control.ValueText:SetText(control.selectedValue and formatter(control.selectedValue) or "Select")
        end

        control:SetScript("OnMouseUp", function(buttonSelf)
            ShowDropdownMenu(buttonSelf, function()
                local menuItems = {}
                for _, optionValue in ipairs(options or {}) do
                    menuItems[#menuItems + 1] = {
                        text = formatter(optionValue),
                        value = optionValue,
                        checked = control.selectedValue == optionValue,
                        onClick = function(value)
                            control.selectedValue = value
                            if control.ValueText then control.ValueText:SetText(formatter(value)) end
                            if type(onChanged) == "function" then onChanged(value) end
                        end,
                    }
                end
                return menuItems
            end)
        end)
    end

    local function initializeMultiSelectControl(control, options, selectedValues, formatValue, onChanged)
        local formatter = formatValue or tostring
        control.selectedValues = normalizeList(selectedValues)

        local function containsSelectedValue(expected)
            local normalizedExpected = string.lower(trimString(expected))

            for _, selectedValue in ipairs(control.selectedValues or {}) do
                if string.lower(trimString(selectedValue)) == normalizedExpected then
                    return true
                end
            end

            return false
        end

        local function toggleSelectedValue(expected)
            local normalizedExpected = string.lower(trimString(expected))

            for index, selectedValue in ipairs(control.selectedValues or {}) do
                if string.lower(trimString(selectedValue)) == normalizedExpected then
                    table.remove(control.selectedValues, index)
                    return
                end
            end

            control.selectedValues[#control.selectedValues + 1] = expected
        end

        local function refreshLabel()
            local labels = {}
            for index, value in ipairs(control.selectedValues or {}) do
                labels[index] = formatter(value)
            end
            if control.ValueText then control.ValueText:SetText(#labels > 0 and table.concat(labels, ", ") or "Select") end
        end

        refreshLabel()

        control:SetScript("OnMouseUp", function(buttonSelf)
            ShowDropdownMenu(buttonSelf, function()
                local menuItems = {}
                for _, optionValue in ipairs(options or {}) do
                    local currentValue = optionValue

                    menuItems[#menuItems + 1] = {
                        text = formatter(currentValue),
                        value = currentValue,
                        checked = containsSelectedValue(currentValue),
                        isNotRadio = true,
                        keepShownOnClick = true,
                        onClick = function(value)
                            toggleSelectedValue(value)
                            refreshLabel()
                            if type(onChanged) == "function" then onChanged(control.selectedValues, value) end
                        end,
                    }
                end
                return menuItems
            end)
        end)
    end

    local calendarPanel
    local calendarTitle
    local calendarYearInput
    local calendarDayButtons = {}
    local activeCalendarField
    local activeCalendarButton
    local birthdateButton
    local joinDateButton
    local memberCalendarMonth = tonumber(date("%m")) or 1
    local memberCalendarYear = tonumber(date("%Y")) or 2000
    local refreshMemberCalendar

    local function getDateValue(fieldKey)
        return fieldKey == "birthdate" and formState.birthdate or formState.guildJoinDate
    end

    local function setDateValue(fieldKey, value)
        if fieldKey == "birthdate" then formState.birthdate = value or "" else formState.guildJoinDate = value or "" end
    end

    local function normalizeCalendarMonth()
        while memberCalendarMonth < 1 do memberCalendarMonth = memberCalendarMonth + 12; memberCalendarYear = memberCalendarYear - 1 end
        while memberCalendarMonth > 12 do memberCalendarMonth = memberCalendarMonth - 12; memberCalendarYear = memberCalendarYear + 1 end
    end

    local function refreshCalendarButtonText(button, value, emptyLabel)
        if button and button.ValueText then button.ValueText:SetText(value ~= "" and value or emptyLabel) end
    end

    local function applyMemberCalendarYearJump()
        local requestedYear = math.floor(tonumber(calendarYearInput and calendarYearInput.GetText and calendarYearInput:GetText()) or 0)
        if requestedYear < 1900 or requestedYear > 2100 then
            if calendarYearInput and calendarYearInput.SetText then calendarYearInput:SetText(tostring(memberCalendarYear)) end
            return
        end
        memberCalendarYear = requestedYear
        refreshMemberCalendar()
    end

    local function refreshDateButtons()
        refreshCalendarButtonText(birthdateButton, formState.birthdate, "Pick Birthdate")
        refreshCalendarButtonText(joinDateButton, formState.guildJoinDate, "Pick Join Date")
    end

    function refreshMemberCalendar()
        normalizeCalendarMonth()
        local monthNames = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }
        local firstDay = time({ year = memberCalendarYear, month = memberCalendarMonth, day = 1, hour = 12, min = 0, sec = 0 })
        local firstWeekday = date("*t", firstDay).wday
        local daysInMonth = getDaysInMonth(memberCalendarYear, memberCalendarMonth)
        if calendarTitle then calendarTitle:SetText(string.format("%s %d", monthNames[memberCalendarMonth] or "Month", memberCalendarYear)) end
        if calendarYearInput and calendarYearInput.SetText then calendarYearInput:SetText(tostring(memberCalendarYear)) end
        for buttonIndex, button in ipairs(calendarDayButtons) do
            local dayNumber = buttonIndex - firstWeekday + 1
            if dayNumber >= 1 and dayNumber <= daysInMonth then
                local selectedDate = string.format("%04d-%02d-%02d", memberCalendarYear, memberCalendarMonth, dayNumber)
                button._selectedDate = selectedDate
                button:SetText(getDateValue(activeCalendarField) == selectedDate and ("[" .. tostring(dayNumber) .. "]") or tostring(dayNumber))
                button:Enable()
            else
                button._selectedDate = nil
                button:SetText("")
                button:Disable()
            end
        end
    end

    local function openMemberCalendar(fieldKey, anchorButton)
        activeCalendarField = fieldKey
        activeCalendarButton = anchorButton
        local value = getDateValue(fieldKey)
        memberCalendarYear = tonumber(value:match("^(%d%d%d%d)%-%d%d%-%d%d$")) or tonumber(date("%Y")) or memberCalendarYear
        memberCalendarMonth = tonumber(value:match("^%d%d%d%d%-(%d%d)%-%d%d$")) or tonumber(date("%m")) or memberCalendarMonth
        calendarPanel:ClearAllPoints()
        calendarPanel:SetPoint("TOPLEFT", anchorButton, "BOTTOMLEFT", 0, -6)
        calendarPanel:Show()
        refreshMemberCalendar()
    end

    local function createDateButtonRow(parentFrame, x, y, labelText, fieldKey, options)
        local resolved = type(options) == "table" and options or {}
        local labelWidth = resolved.labelWidth or 94
        local startX = x + labelWidth + 10
        local dateButton = CreateSelectControl(parentFrame, startX, y, resolved.buttonWidth or 132, "", nil, { textInset = 13, showArrow = false })
        local label = dateButton:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        label:SetPoint("LEFT", dateButton, "LEFT", -labelWidth - 10, 0)
        label:SetWidth(labelWidth)
        label:SetJustifyH("LEFT")
        label:SetWordWrap(false)
        label:SetText(labelText)
        styleLabelText(label, 12)
        dateButton:SetScript("OnMouseUp", function(self)
            if calendarPanel:IsShown() and activeCalendarField == fieldKey then calendarPanel:Hide(); return end
            openMemberCalendar(fieldKey, self)
        end)
        if fieldKey == "birthdate" then birthdateButton = dateButton else joinDateButton = dateButton end
        return dateButton
    end

    if toonId ~= "" and tonumber(formState.timePlayedSeconds) <= 0 then
        ensureCurrentToonTimePlayedRequest(selectedRow)
    end

    if addon.NewUIComponents.CreateButton then
        local saveButton = CreateCardActionButton(root, { x = 854, y = -12, w = 160, h = 34 }, "Save Toon", "HomeRaidCardBackground", { 1, 1, 1, 0.98 }, { 1.00, 0.93, 0.70, 1 }, function()
            if not selectedRow or trimString(selectedRow.id or "") == "" or type(addon.RosterService) ~= "table" or type(addon.RosterService.UpdateMember) ~= "function" then
                return
            end

            local okUpdate = pcall(addon.RosterService.UpdateMember, addon.RosterService, selectedRow.id, buildSavePayload(selectedRow, formState))

            if not okUpdate then
                return
            end

            local refreshedRow = buildSelectedRow(selectedRow.id)
            if navigation and type(navigation.refresh) == "function" then
                navigation.refresh(selectedRow.id)
            else
                page:Render(parent, refreshedRow or selectedRow, navigation)
            end
        end)
        local cancelButton = CreateCardActionButton(root, { x = 1026, y = -12, w = 120, h = 34 }, "Cancel", "HomeNewsCardBackground", { 1, 1, 1, 0.98 }, { 0.96, 0.93, 0.84, 1 }, function()
            if navigation and type(navigation.showStats) == "function" then
                navigation.showStats(selectedRow and selectedRow.id)
            else
                page:Render(parent, selectedRow, navigation)
            end
        end)
        local deleteButton = CreateCardActionButton(root, { x = 1158, y = -12, w = 160, h = 34 }, "Delete Toon", "HomeNewsCardBackground", { 0.88, 0.40, 0.38, 0.98 }, { 1.00, 0.82, 0.82, 1 }, function()
            if not selectedRow or trimString(selectedRow.id or "") == "" or type(addon.RosterService) ~= "table" or type(addon.RosterService.DeleteMyToon) ~= "function" then
                return
            end

            local deletedRecord, errorMessage = addon.RosterService:DeleteMyToon(selectedRow.id)

            if not deletedRecord then
                if type(addon.Print) == "function" then
                    addon:Print(errorMessage or "Unable to delete toon.")
                end
                return
            end

            if page.State and type(page.State.formValuesByToonId) == "table" then
                page.State.formValuesByToonId[selectedRow.id] = nil
            end

            if addon.NewUIMyToons then
                addon.NewUIMyToons.selectedToonId = nil
            end

            if type(addon.Print) == "function" then
                addon:Print("Deleted My Toon:", selectedRow.name or selectedRow.id)
            end

            if navigation and type(navigation.showStats) == "function" then
                navigation.showStats(nil)
            elseif addon.NewUIMyToons and type(addon.NewUIMyToons.SelectSubTab) == "function" then
                addon.NewUIMyToons:SelectSubTab("stats", nil)
            end
        end)

        local playerId = addon.Store and addon.Store.GetPlayerName and trimString(addon.Store:GetPlayerName()) or ""
        if trimString(selectedRow and selectedRow.id) == playerId and deleteButton and type(deleteButton.EnableMouse) == "function" then
            deleteButton:EnableMouse(false)
            if deleteButton.Text then
                deleteButton.Text:SetAlpha(0.45)
            end
        end
    end

    calendarPanel = CreateFrame("Frame", nil, root, BackdropTemplateMixin and "BackdropTemplate" or nil)
    calendarPanel:SetSize(252, 242)
    if calendarPanel.SetBackdrop then
        calendarPanel:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        calendarPanel:SetBackdropColor(0.07, 0.09, 0.13, 0.98)
        calendarPanel:SetBackdropBorderColor(0.66, 0.50, 0.17, 1)
    end
    calendarPanel:SetFrameStrata("FULLSCREEN_DIALOG")
    calendarPanel:SetFrameLevel(root:GetFrameLevel() + 30)
    calendarPanel:SetToplevel(true)
    calendarPanel:Hide()

    calendarTitle = calendarPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    calendarTitle:SetPoint("TOP", calendarPanel, "TOP", 0, -12)
    stylePaneTitleText(calendarTitle, 15, VALUE_TEXT_COLOR, "OUTLINE")

    local calendarYearLabel = calendarPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    calendarYearLabel:SetPoint("TOPLEFT", calendarPanel, "TOPLEFT", 16, -40)
    calendarYearLabel:SetWidth(34)
    calendarYearLabel:SetJustifyH("LEFT")
    calendarYearLabel:SetText("Year")
    styleLabelText(calendarYearLabel, 11)

    calendarYearInput = CreateLegacyCalendarInput(calendarPanel, 58, 20, "LEFT", calendarYearLabel, "RIGHT", 6, 0)
    calendarYearInput:SetNumeric(true)
    calendarYearInput:SetMaxLetters(4)
    calendarYearInput:SetScript("OnEnterPressed", function(self)
        self:ClearFocus()
        applyMemberCalendarYearJump()
    end)
    CreateLegacyCalendarButton(calendarPanel, 48, 20, "LEFT", calendarYearInput, "RIGHT", 6, 0, "Jump", function()
        applyMemberCalendarYearJump()
    end)
    CreateLegacyCalendarButton(calendarPanel, 24, 22, "TOPLEFT", calendarPanel, "TOPLEFT", 12, -10, "<", function()
        memberCalendarMonth = memberCalendarMonth - 1
        refreshMemberCalendar()
    end)
    CreateLegacyCalendarButton(calendarPanel, 24, 22, "TOPRIGHT", calendarPanel, "TOPRIGHT", -12, -10, ">", function()
        memberCalendarMonth = memberCalendarMonth + 1
        refreshMemberCalendar()
    end)

    for columnIndex, labelText in ipairs({ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }) do
        local label = calendarPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOPLEFT", calendarPanel, "TOPLEFT", 14 + ((columnIndex - 1) * 32), -70)
        label:SetWidth(28)
        label:SetJustifyH("CENTER")
        label:SetText(labelText)
        styleMutedText(label, 10)
    end

    for rowIndex = 1, 6 do
        for columnIndex = 1, 7 do
            local buttonIndex = ((rowIndex - 1) * 7) + columnIndex
            local button = CreateLegacyCalendarButton(calendarPanel, 28, 22, "TOPLEFT", calendarPanel, "TOPLEFT", 12 + ((columnIndex - 1) * 32), -86 - ((rowIndex - 1) * 24), "", function(self)
                if not self._selectedDate or not activeCalendarField then return end
                setDateValue(activeCalendarField, self._selectedDate)
                refreshDateButtons()
                calendarPanel:Hide()
                activeCalendarButton = nil
                refreshMemberCalendar()
            end)
            calendarDayButtons[buttonIndex] = button
        end
    end

    local summaryPanel = CreateArtFrame(root, { x = 24, y = -58, w = 1260, h = 168 }, resolveAsset("MyToonsPanelBackground", { left = 16, right = 16, top = 12, bottom = 12 }, 1260, 168))
    if selectedRow then
        local summaryContent = summaryPanel.Content
        local contentWidth = tonumber(summaryContent:GetWidth()) or 1210
        local classToken = resolveClassToken(selectedRow.classFile or member.classFile, selectedRow.className or member.className)
        local playerId = addon.Store and addon.Store.GetPlayerName and addon.Store:GetPlayerName() or ""
        local factionCardAsset, factionTextColor = resolveFactionVisual(member.faction or record.faction)
        local portraitFrame = CreateArtFrame(summaryContent, { x = 0, y = 0, w = 128, h = 124 }, resolveAsset(factionCardAsset, { left = 18, right = 18, top = 12, bottom = 12 }, 128, 124))
        local portraitTexture = portraitFrame:CreateTexture(nil, "OVERLAY")
        portraitTexture:SetPoint("TOPLEFT", portraitFrame.Content, "TOPLEFT", 0, 0)
        portraitTexture:SetPoint("BOTTOMRIGHT", portraitFrame.Content, "BOTTOMRIGHT", 0, 0)
        if trimString(selectedRow.id) == trimString(playerId) and type(SetPortraitTexture) == "function" then
            SetPortraitTexture(portraitTexture, "player")
            portraitTexture:SetTexCoord(0.07, 0.93, 0.07, 0.93)
        elseif classToken ~= "" and type(CLASS_ICON_TCOORDS) == "table" and CLASS_ICON_TCOORDS[classToken] then
            local coords = CLASS_ICON_TCOORDS[classToken]
            portraitTexture:SetTexture(MYTOONS_CLASS_ICON_TEXTURE)
            portraitTexture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
        else
            portraitTexture:SetColorTexture(0.72, 0.68, 0.58, 0.22)
        end

        local roleBadge = CreateArtFrame(portraitFrame, { x = 88, y = -88, w = 34, h = 34 }, resolveAsset("MyToonsCardBackground", { left = 18, right = 18, top = 12, bottom = 12 }, 34, 34))
        local roleBadgeTexture = roleBadge:CreateTexture(nil, "OVERLAY")
        roleBadgeTexture:SetPoint("TOPLEFT", roleBadge.Content, "TOPLEFT", 0, 0)
        roleBadgeTexture:SetPoint("BOTTOMRIGHT", roleBadge.Content, "BOTTOMRIGHT", 0, 0)
        roleBadgeTexture:SetTexture(resolveSummaryRoleIcon(selectedRow.role or selectedRow.spec))

        local name = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        name:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", 152, -2)
        name:SetText(pickValue(selectedRow.name or selectedRow.id, "-"))
        stylePaneTitleText(name, 22, LABEL_TEXT_COLOR, "OUTLINE")

        local realmLabel = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        realmLabel:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", 152, -32)
        realmLabel:SetText("Realm")
        styleLabelText(realmLabel, 12)

        local realmValue = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
        realmValue:SetPoint("TOPLEFT", realmLabel, "BOTTOMLEFT", 0, -1)
        realmValue:SetText(pickValue(selectedRow.realm or member.realm, "-"))
        styleLargeValueText(realmValue, 16)

        local factionLabel = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        factionLabel:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", 152, -82)
        factionLabel:SetText("Faction")
        styleLabelText(factionLabel, 12)

        local factionValue = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
        factionValue:SetPoint("TOPLEFT", factionLabel, "BOTTOMLEFT", 0, -1)
        factionValue:SetText(pickValue(member.faction or record.faction, "-"))
        styleLargeValueText(factionValue, 16, factionTextColor)

        local classBlock = CreateSummaryBlock(summaryContent, 430, -8, 118, "Class", pickValue(selectedRow.className or member.className, "-"), "", MYTOONS_CLASS_ICON_TEXTURE, { 0.96, 0.93, 0.84, 1 }, false)
        if classBlock.Icon and classToken ~= "" and type(CLASS_ICON_TCOORDS) == "table" and CLASS_ICON_TCOORDS[classToken] then
            local coords = CLASS_ICON_TCOORDS[classToken]
            classBlock.Icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
        end
        CreateSummaryBlock(summaryContent, 568, -8, 132, "Spec", pickValue(selectedRow.spec, "-"), "", resolveSummaryRoleIcon(selectedRow.spec), { 0.96, 0.93, 0.84, 1 }, false)
        CreateSummaryBlock(summaryContent, 720, -8, 118, "Role", formatDisplayLabel(selectedRow.role), "", resolveSummaryRoleIcon(selectedRow.role), { 0.96, 0.93, 0.84, 1 }, false)
        CreateSummaryBlock(summaryContent, 858, -8, 128, "Item Level", formatPreciseItemLevel(member.itemLevel, selectedRow.itemLevel), "", "", { 0.96, 0.82, 0.46, 1 }, true)

        for _, dividerX in ipairs({ 410, 550, 702, 842, 996 }) do
            local divider = summaryContent:CreateTexture(nil, "ARTWORK")
            divider:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", dividerX, 6)
            divider:SetSize(1, 92)
            divider:SetColorTexture(0.44, 0.34, 0.16, 0.45)
        end

        local footerLine = summaryContent:CreateTexture(nil, "ARTWORK")
        footerLine:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", 0, -122)
        footerLine:SetSize(math.max(1, contentWidth - 192), 1)
        footerLine:SetColorTexture(0.44, 0.34, 0.16, 0.45)

        local updatedText = summaryContent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        updatedText:SetPoint("TOPLEFT", summaryContent, "TOPLEFT", 156, -126)
        updatedText:SetText("Last Updated: " .. formatUpdatedAt(selectedRow.updatedAt))
        styleValueText(updatedText, 12)

        local alerts = {
            { text = "Ready", color = { 0.42, 0.92, 0.36 }, icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Ready-l.png" },
        }
        if readinessSnapshot and readinessSnapshot.gems and readinessSnapshot.gems.status == "issues" then alerts[#alerts + 1] = { text = "Missing Gem", color = { 0.96, 0.74, 0.24 }, icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_NeedGems-l.png" } end
        if readinessSnapshot and readinessSnapshot.enchants and readinessSnapshot.enchants.status == "issues" then alerts[#alerts + 1] = { text = "Missing Enchant", color = { 0.96, 0.40, 0.28 }, icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png" } end
        if readinessSnapshot and readinessSnapshot.consumables and readinessSnapshot.consumables.status == "issues" then alerts[#alerts + 1] = { text = "Missing Consumables", color = { 0.96, 0.40, 0.28 }, icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png" } end
        for index = 1, math.min(3, #alerts) do
            local alert = alerts[index]
            CreateSummaryAlertChip(summaryContent, contentWidth - 186, -2 - ((index - 1) * 44), 176, 36, alert.text, alert.color, alert.icon)
        end
    else
        local emptyText = summaryPanel.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
        emptyText:SetPoint("TOPLEFT", summaryPanel.Content, "TOPLEFT", 0, 0)
        emptyText:SetText("No toon selected")
        styleValueText(emptyText, 12)
    end

    local basicProfilePanel = CreateSection(root, { x = 24, y = -238, w = 610, h = 192 }, "Basic Profile", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Roster-l.png")
    createStaticRow(basicProfilePanel.Content, 0, 0, "Name", 193, pickValue(selectedRow and selectedRow.name, "-"), { textInset = 13 })
    createStaticRow(basicProfilePanel.Content, 290, 0, "Guild Name", 213, pickValue(member.guildName or record.guildName, "-"), { textInset = 13 })
    createStaticRow(basicProfilePanel.Content, 0, -28, "Realm", 193, pickValue(selectedRow and selectedRow.realm or member.realm, "-"), { textInset = 13 })
    createStaticRow(basicProfilePanel.Content, 290, -28, "Guild Rank", 213, pickValue(member.rank or member.rankName, "-"), { textInset = 13 })
    createStaticRow(basicProfilePanel.Content, 0, -56, "Class", 193, pickValue(selectedRow and selectedRow.className or member.className, "-"), { textInset = 13 })
    createInputRow(basicProfilePanel.Content, 290, -56, "BattleTag", 213, formState.battleTag, function(value)
        formState.battleTag = value
    end, { textInset = 13 })
    createStaticRow(basicProfilePanel.Content, 0, -84, "Spec", 193, pickValue(selectedRow and selectedRow.spec or member.spec, "-"), { textInset = 13 })
    local mainCharacterControl = createSelectRow(basicProfilePanel.Content, 290, -84, "Main Character", 213, pickValue(formState.mainCharacter, "-"), function() return {} end, { textInset = 13 })
    initializeSingleSelectControl(mainCharacterControl, myToonOptions, formState.mainCharacter, function(value) return pickValue(value, "-") end, function(value)
        formState.mainCharacter = value
    end)
    createStaticRow(basicProfilePanel.Content, 0, -112, "Item Level", 193, formatPreciseItemLevel(member.itemLevel, selectedRow and selectedRow.itemLevel), { valueColor = { 0.96, 0.82, 0.46, 1 }, largeFont = true, textInset = 13 })

    local datesInfoPanel = CreateSection(root, { x = 654, y = -238, w = 630, h = 210 }, "Dates & Info", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Calendar-l.png")
    createDateButtonRow(datesInfoPanel.Content, 0, 0, "Birthday", "birthdate")
    createDateButtonRow(datesInfoPanel.Content, 0, -28, "Guild Join Date", "guildJoinDate")
    refreshDateButtons()
    local timePlayedBar, timePlayedText = createStaticRow(datesInfoPanel.Content, 0, -56, "Time Played", 176, tonumber(formState.timePlayedSeconds) > 0 and formatTimePlayedDuration(formState.timePlayedSeconds) or (toonId ~= "" and toonId == trimString(addon.Store and addon.Store.GetPlayerName and addon.Store:GetPlayerName()) and "Loading..." or "-"), { valueColor = { 0.62, 0.82, 1, 1 }, textInset = 13 })
    page._activeTimePlayedText = timePlayedText
    local availabilityLabel = createRowLabel(datesInfoPanel.Content, 0, -90, "Availability")
    styleLabelText(availabilityLabel, 12)
    for index, dayLabel in ipairs(AVAILABILITY_DAY_OPTIONS) do
        createAvailabilityDayButton(datesInfoPanel.Content, (index - 1) * 31, -110, dayLabel, 28, 20)
    end
    createInputRow(datesInfoPanel.Content, 0, -138, "From", 64, formState.availabilityStart, function(value)
        formState.availabilityStart = value
        formState._availabilityTouched = true
        refreshAvailabilityState()
    end, { labelWidth = 34, textInset = 13, liveUpdate = true, maxLetters = 16 })
    createInputRow(datesInfoPanel.Content, 118, -138, "To", 64, formState.availabilityEnd, function(value)
        formState.availabilityEnd = value
        formState._availabilityTouched = true
        refreshAvailabilityState()
    end, { labelWidth = 22, textInset = 13, liveUpdate = true, maxLetters = 16 })
    page._availabilitySummaryText = nil
    refreshAvailabilityState()
    createInputRow(datesInfoPanel.Content, 284, 0, "Notes", 220, formState.note, function(value)
        formState.note = value
        if page._noteCountText then
            page._noteCountText:SetText(string.format("%d / 250", #value))
        end
    end, { height = 110, multiline = true, liveUpdate = true, labelWidth = 58, textInset = 13 })
    local noteCount = datesInfoPanel.Content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    noteCount:SetPoint("TOPLEFT", datesInfoPanel.Content, "TOPLEFT", 520, -130)
    noteCount:SetText(string.format("%d / 250", #formState.note))
    styleMutedText(noteCount, 11)
    page._noteCountText = noteCount

    local rolesPanel = CreateSection(root, { x = 24, y = -460, w = 460, h = 230 }, "Roles & Interests", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Role_Tank-l.png")
    local primaryRoleControl = createSelectRow(rolesPanel.Content, 0, -10, "Primary Role", 136, formatDisplayLabel(formState.primaryRole), function() return {} end, { textInset = 13 })
    initializeSingleSelectControl(primaryRoleControl, roleOptions, formState.primaryRole, formatDisplayLabel, function(value)
        formState.primaryRole = value
    end)
    local secondaryRolesControl = createSelectRow(rolesPanel.Content, 0, -44, "Secondary Roles", 136, joinDisplayValues(formState.secondaryRoles), function() return {} end, { textInset = 13 })
    initializeMultiSelectControl(secondaryRolesControl, roleOptions, formState.secondaryRoles, formatDisplayLabel, function(values)
        formState.secondaryRoles = normalizeList(values)
    end)
    local combatProfileControl = createSelectRow(rolesPanel.Content, 0, -78, "Combat Profile", 136, formatDisplayLabel(formState.meleeOrRanged), function() return {} end, { textInset = 13 })
    initializeSingleSelectControl(combatProfileControl, profileOptions, formState.meleeOrRanged, formatDisplayLabel, function(value)
        formState.meleeOrRanged = value
    end)
    local interestsControl = createSelectRow(rolesPanel.Content, 0, -122, "Content Interests", 300, joinDisplayValues(formState.interests), function() return {} end, { textInset = 13, labelWidth = 118 })
    initializeMultiSelectControl(interestsControl, INTEREST_OPTIONS, formState.interests, formatDisplayLabel, function(values)
        formState.interests = normalizeList(values)
    end)

    local readinessPanel = CreateSection(root, { x = 496, y = -460, w = 354, h = 230 }, "Readiness Preferences", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Ready-l.png")
    local readinessPreferences = formState.readinessPreferences or normalizeReadinessPreferences(readinessSnapshot and readinessSnapshot.preferences)
    formState.readinessPreferences = readinessPreferences
    CreatePreferenceRow(readinessPanel.Content, 0, "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_NeedGems-l.png", "Track Gems", readinessPreferences.trackGems, { onToggle = function(value) readinessPreferences.trackGems = value end })
    CreatePreferenceRow(readinessPanel.Content, -24, "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", "Track Enchants", readinessPreferences.trackEnchants, { onToggle = function(value) readinessPreferences.trackEnchants = value end })
    CreatePreferenceRow(readinessPanel.Content, -48, "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", "Track Consumables", readinessPreferences.trackConsumables, { onToggle = function(value) readinessPreferences.trackConsumables = value end })
    CreatePreferenceRow(readinessPanel.Content, -72, "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", "Track Durability", readinessPreferences.trackDurability, { onToggle = function(value) readinessPreferences.trackDurability = value end })
    local durabilityAlertLabel = createRowLabel(readinessPanel.Content, 30, -103, "Alert Below")
    local durabilityAlertInput
    durabilityAlertInput = CreateInputControl(readinessPanel.Content, 146, -96, 48, tostring(readinessPreferences.durabilityAlertBelow or 95), function(value)
        readinessPreferences.durabilityAlertBelow = math.max(0, math.min(100, math.floor(tonumber(value) or (readinessPreferences.durabilityAlertBelow or 95))))
        if durabilityAlertInput and durabilityAlertInput.EditBox then durabilityAlertInput.EditBox:SetText(tostring(readinessPreferences.durabilityAlertBelow)) end
    end, { numeric = true, maxLetters = 3, textInset = 13, liveUpdate = true })
    styleLabelText(durabilityAlertLabel, 12)
    CreatePreferenceRow(readinessPanel.Content, -128, "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Roster-l.png", "Weekly Vault", readinessPreferences.weeklyVault, { onToggle = function(value) readinessPreferences.weeklyVault = value end })

    local quickAlertsPanel = CreateSection(root, { x = 862, y = -460, w = 422, h = 230 }, "Quick Alerts Preview", "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png")
    local quickAlerts = {}
    if readinessSnapshot and readinessSnapshot.gems and readinessSnapshot.gems.status == "issues" then quickAlerts[#quickAlerts + 1] = { title = "Missing Gem", subtitle = string.format("%d socket%s empty", math.max(1, tonumber(readinessSnapshot.gems.missing) or 1), (tonumber(readinessSnapshot.gems.missing) or 1) == 1 and "" or "s"), icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_NeedGems-l.png", color = { 0.96, 0.74, 0.24 } } end
    if readinessSnapshot and readinessSnapshot.enchants and readinessSnapshot.enchants.status == "issues" then quickAlerts[#quickAlerts + 1] = { title = "Missing Enchant", subtitle = "Weapon or gear enchant missing", icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", color = { 0.96, 0.40, 0.28 } } end
    if readinessSnapshot and readinessSnapshot.durability and readinessSnapshot.durability.status == "issues" then quickAlerts[#quickAlerts + 1] = { title = "Durability Alert", subtitle = string.format("Current minimum durability %d%%", math.floor((tonumber(readinessSnapshot.durability.minPercent) or 0) + 0.5)), icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", color = { 0.96, 0.74, 0.24 } } end
    if readinessSnapshot and readinessSnapshot.consumables and readinessSnapshot.consumables.status == "issues" then quickAlerts[#quickAlerts + 1] = { title = "No Flask Active", subtitle = "Flask or food requirement not met", icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Warning-l.png", color = { 0.96, 0.40, 0.28 } } end
    if #quickAlerts == 0 then quickAlerts[1] = { title = "Ready", subtitle = "No current quick alerts detected", icon = "Interface\\AddOns\\GH3\\Media\\Icons\\Icon_Ready-l.png", color = { 0.42, 0.92, 0.36 } } end
    for index = 1, math.min(#quickAlerts, 4) do
        local alert = quickAlerts[index]
        CreateQuickAlertRow(quickAlertsPanel.Content, -((index - 1) * 34), alert.icon, alert.title, alert.subtitle, alert.color)
    end

    return root
end

return page