GuildHelper = GuildHelper or {}

local addon = GuildHelper

addon.NewUIGroups = addon.NewUIGroups or {}

local page = addon.NewUIGroups
local PANEL_ART_KEY = "MyToonsPanelBackground"
local PANEL_HEADER_KEY = "HomePanelHeader"
local CARD_ART_KEY = "MyToonsCardBackground"
local BUTTON_ART_KEY = "MyToonsButtonBackground"
local BUTTON_SELECTED_ART_KEY = "MyToonsButtonSelected"
local INPUT_ART_KEY = "MyToonsRowBackground"
local LEGACY_CONTENT_OFFSET_Y = 28
local refreshPage

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

local function cleanupFrameTree(frame)
    if not frame then
        return
    end

    for _, child in ipairs({ frame:GetChildren() }) do
        cleanupFrameTree(child)
    end

    if type(frame._gh3Cleanup) == "function" then
        pcall(frame._gh3Cleanup, frame)
        frame._gh3Cleanup = nil
    end

    if type(frame.UnregisterAllEvents) == "function" then
        frame:UnregisterAllEvents()
    end

    if type(frame.SetScript) == "function" then
        frame:SetScript("OnEvent", nil)
        frame:SetScript("OnUpdate", nil)
    end

    if frame.Hide then
        frame:Hide()
    end

    if frame.SetParent then
        frame:SetParent(nil)
    end
end

local function isGroupsPageActive()
    return addon.NewUIShell and addon.NewUIShell.activePage == "groups"
end

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

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

local function applyArtTexture(texture, textureKey, fallbackColor)
    local texturePath = resolveTexturePath(textureKey)
    local metrics = resolveArtMetrics(textureKey)

    if texturePath then
        texture:SetTexture(texturePath)
        texture:SetVertexColor(1, 1, 1, 0.98)
        if metrics and type(metrics.texCoord) == "table" and #metrics.texCoord == 4 then
            texture:SetTexCoord(metrics.texCoord[1], metrics.texCoord[3], metrics.texCoord[2], metrics.texCoord[4])
        end
        return
    end

    texture:SetColorTexture(unpack(fallbackColor or { 0.10, 0.10, 0.12, 0.94 }))
end

local function createArtContentFrame(parent, textureKey, width, height, fallbackInsets)
    local metrics = resolveArtMetrics(textureKey)
    local insets = metrics and metrics.insets or fallbackInsets or {}
    local sourceWidth = metrics and tonumber(metrics.sourceWidth) or tonumber(width) or 0
    local sourceHeight = metrics and tonumber(metrics.sourceHeight) or tonumber(height) or 0
    local scaleX = sourceWidth > 0 and (tonumber(width) or 0) / sourceWidth or 1
    local scaleY = sourceHeight > 0 and (tonumber(height) or 0) / sourceHeight or 1
    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)
    local content = CreateFrame("Frame", nil, parent)
    content:SetPoint("TOPLEFT", parent, "TOPLEFT", left, -top)
    content:SetSize(math.max(1, (tonumber(width) or 1) - left - right), math.max(1, (tonumber(height) or 1) - top - bottom))
    return content
end

local function createPanel(parent, rect, titleText)
    local panel = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(panel, parent, rect)
    local background = panel:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(panel)
    applyArtTexture(background, PANEL_ART_KEY, { 0.09, 0.10, 0.13, 0.94 })

    local innerContent = createArtContentFrame(panel, PANEL_ART_KEY, rect.w, rect.h, { left = 24, right = 24, top = 42, bottom = 22 })
    local headerBand = panel:CreateTexture(nil, "ARTWORK")
    headerBand:SetPoint("TOPLEFT", panel, "TOPLEFT", 24, -14)
    headerBand:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -24, -14)
    headerBand:SetHeight(24)
    applyArtTexture(headerBand, PANEL_HEADER_KEY, { 0.08, 0.08, 0.10, 0.82 })

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

    panel.Content = innerContent
    return panel
end

local function hideLegacyTitle(frame, targetText)
    if not frame then
        return false
    end

    for _, region in ipairs({ frame:GetRegions() }) do
        if region and region.GetText and string.lower(tostring(region:GetText() or "")) == string.lower(targetText or "") then
            region:Hide()
            return true
        end
    end

    for _, child in ipairs({ frame:GetChildren() }) do
        if hideLegacyTitle(child, targetText) then
            return true
        end
    end

    return false
end

local function clearLegacySurface(frame)
    if not frame then
        return
    end

    for _, region in ipairs({ frame:GetRegions() }) do
        if region and region.GetObjectType and region:GetObjectType() == "Texture"
            and region ~= frame._gh3GroupsArtTexture
            and region ~= frame._gh3GroupsTrack then
            region:SetAlpha(0)
        end
    end

    if frame.SetBackdropColor then
        frame:SetBackdropColor(0, 0, 0, 0)
    end
    if frame.SetBackdropBorderColor then
        frame:SetBackdropBorderColor(0, 0, 0, 0)
    end

    if type(frame._gh3Depth) == "table" then
        for _, region in pairs(frame._gh3Depth) do
            if region and region.Hide then
                region:Hide()
            end
        end
    end
end

local function ensureSurfaceTexture(frame, textureKey)
    if not frame then
        return nil
    end

    local texture = frame._gh3GroupsArtTexture
    if not texture then
        texture = frame:CreateTexture(nil, "ARTWORK", nil, 1)
        texture:SetAllPoints(frame)
        frame._gh3GroupsArtTexture = texture
    end

    applyArtTexture(texture, textureKey, { 0.09, 0.10, 0.13, 0.94 })
    return texture
end

local function resolveSurfaceTextureKey(frame)
    local height = tonumber(frame and frame:GetHeight()) or 0

    if height >= 140 then
        return PANEL_ART_KEY
    end

    if height <= 28 then
        return INPUT_ART_KEY
    end

    return CARD_ART_KEY
end

local function skinLegacyButton(button)
    clearLegacySurface(button)
    ensureSurfaceTexture(button, (button:GetHeight() or 0) >= 36 and INPUT_ART_KEY or BUTTON_ART_KEY)

    for _, texture in ipairs({ button.GetNormalTexture and button:GetNormalTexture() or nil, button.GetPushedTexture and button:GetPushedTexture() or nil, button.GetHighlightTexture and button:GetHighlightTexture() or nil, button.GetDisabledTexture and button:GetDisabledTexture() or nil }) do
        if texture then
            texture:SetAlpha(0)
        end
    end

    if button.GetFontString and button:GetFontString() then
        button:GetFontString():SetTextColor(button:IsEnabled() and 0.96 or 0.62, button:IsEnabled() and 0.93 or 0.62, button:IsEnabled() and 0.84 or 0.58, 1)
    end
end

local function skinLegacyInput(input)
    if not input or not input.GetParent then
        return
    end

    if input.Left then input.Left:SetAlpha(0) end
    if input.Middle then input.Middle:SetAlpha(0) end
    if input.Right then input.Right:SetAlpha(0) end

    local chrome = input._gh3GroupsChrome
    if not chrome then
        chrome = CreateFrame("Frame", nil, input:GetParent(), BackdropTemplateMixin and "BackdropTemplate" or nil)
        chrome:SetFrameLevel(math.max((input:GetFrameLevel() or 1) - 1, 0))
        chrome:SetPoint("TOPLEFT", input, "TOPLEFT", -4, 4)
        chrome:SetPoint("BOTTOMRIGHT", input, "BOTTOMRIGHT", 4, -4)
        input._gh3GroupsChrome = chrome
    end

    clearLegacySurface(chrome)
    ensureSurfaceTexture(chrome, INPUT_ART_KEY)
    input:SetTextColor(0.96, 0.93, 0.84, 1)
end

local function skinLegacyFrame(frame)
    if not frame or frame:GetObjectType() ~= "Frame" or not frame.SetBackdrop then
        return
    end

    clearLegacySurface(frame)
    ensureSurfaceTexture(frame, resolveSurfaceTextureKey(frame))
end

local function skinLegacySlider(slider)
    if not slider or slider:GetObjectType() ~= "Slider" then
        return
    end

    clearLegacySurface(slider)

    local track = slider._gh3GroupsTrack
    if not track then
        track = slider:CreateTexture(nil, "BACKGROUND")
        track:SetPoint("TOPLEFT", slider, "TOPLEFT", 2, -2)
        track:SetPoint("BOTTOMRIGHT", slider, "BOTTOMRIGHT", -2, 2)
        slider._gh3GroupsTrack = track
    end
    track:SetColorTexture(0.10, 0.08, 0.05, 0.72)

    local thumb = slider.GetThumbTexture and slider:GetThumbTexture() or nil
    if thumb then
        thumb:SetTexture("Interface\\Buttons\\WHITE8X8")
        thumb:SetVertexColor(0.82, 0.66, 0.18, 0.9)
        thumb:SetAlpha(1)
    end
end

local function skinLegacyTree(frame)
    if not frame then
        return
    end

    for _, child in ipairs({ frame:GetChildren() }) do
        local objectType = child.GetObjectType and child:GetObjectType() or ""

        if objectType == "Button" then
            skinLegacyButton(child)
        elseif objectType == "EditBox" then
            skinLegacyInput(child)
        elseif objectType == "Slider" then
            skinLegacySlider(child)
        elseif objectType == "Frame" then
            skinLegacyFrame(child)
        end

        skinLegacyTree(child)
    end
end

local function installMainFrameBridge()
    if page._bridgeInstalled or not addon.MainFrame then
        return
    end

    page._bridgeInstalled = true
    page._originalRefreshCurrentView = addon.MainFrame.RefreshCurrentView
    page._originalMarkBackgroundRefreshPending = addon.MainFrame.MarkBackgroundRefreshPending

    addon.MainFrame.RefreshCurrentView = function(self, ...)
        if isGroupsPageActive() and page._parentFrame then
            refreshPage(page._parentFrame, page._snapshot)
            return
        end
        if page._originalRefreshCurrentView then
            return page._originalRefreshCurrentView(self, ...)
        end
    end

    addon.MainFrame.MarkBackgroundRefreshPending = function(self, ...)
        if isGroupsPageActive() and page._parentFrame then
            refreshPage(page._parentFrame, page._snapshot)
            return
        end
        if page._originalMarkBackgroundRefreshPending then
            return page._originalMarkBackgroundRefreshPending(self, ...)
        end
    end
end

local function renderLegacyPane(host)
    local mainFrame = addon.MainFrame
    if not mainFrame or type(mainFrame.RenderGroups) ~= "function" then
        return
    end

    if mainFrame.EnsureState then
        mainFrame:EnsureState()
    end

    local originalViewHost = mainFrame.viewHost
    mainFrame.viewHost = host
    local ok, errorMessage = pcall(mainFrame.RenderGroups, mainFrame)
    mainFrame.viewHost = originalViewHost

    if not ok then
        local errorText = host:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        errorText:SetPoint("TOPLEFT", host, "TOPLEFT", 18, -18)
        errorText:SetText("Groups page failed to render: " .. tostring(errorMessage))
        errorText:SetTextColor(1, 0.3, 0.3, 1)
    end
end

local function openLegacyMainFrameView(viewKey)
    if not addon.Router or type(addon.Router.Navigate) ~= "function" or not addon.MainFrame or type(addon.MainFrame.ShowForCurrentView) ~= "function" then
        return
    end

    addon.Router:Navigate(viewKey)
    addon.MainFrame:ShowForCurrentView()
end

local function getGroupTypeLabel(group)
    local difficulty = string.lower(tostring(group and group.difficulty or ""))
    if tostring(group and group.activityType or "") == "raid" then
        return "Raid"
    end
    if string.find(difficulty, "mythic%+", 1, true) then
        return "Mythic+"
    end
    return "Party"
end

local function getGroupDifficultyLabel(group)
    local difficulty = tostring(group and group.difficulty or "")
    if difficulty == "" or difficulty == "Party" then
        return "Open Run"
    end
    if string.find(string.lower(difficulty), "mythic+", 1, true) then
        return "Key " .. tostring(addon.GroupsService:GetMythicPlusLevel(group))
    end
    return difficulty
end

local function buildCompactNeedsSummary(deficits)
    local parts = {}
    for _, key in ipairs({ "tanks", "healers", "melee", "ranged" }) do
        local value = deficits and deficits[key] or 0
        if value and value > 0 then
            parts[#parts + 1] = string.format("%s%d", string.upper(string.sub(key, 1, 1)), value)
        end
    end
    return #parts > 0 and table.concat(parts, " ") or "Covered"
end

local function buildSuggestedText(planner)
    if not planner or #(planner.suggested or {}) == 0 then
        return "No suggested lineup yet. Add signups that meet the role and item level requirements to populate recommendations."
    end

    local lines = {}
    for index, entry in ipairs(planner.suggested) do
        lines[#lines + 1] = string.format("%d. %s | %s | iLvl %s", index, entry.fullName or "Unknown", entry.role or "DPS", tostring(entry.itemLevel or 0))
    end
    return table.concat(lines, "\n")
end

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

local function parseWeekdayText(value)
    local map = { sun = 1, sunday = 1, mon = 2, monday = 2, tue = 3, tues = 3, tuesday = 3, wed = 4, wednesday = 4, thu = 5, thur = 5, thursday = 5, fri = 6, friday = 6, sat = 7, saturday = 7 }
    local seen = {}
    local result = {}
    for token in string.gmatch(string.lower(trimText(value)), "[^,%s/]+") do
        local day = map[token] or tonumber(token)
        if day and day >= 1 and day <= 7 and not seen[day] then
            seen[day] = true
            result[#result + 1] = day
        end
    end
    table.sort(result)
    return result
end

local function buildWeekdayText(days)
    local labels = { [1] = "Sun", [2] = "Mon", [3] = "Tue", [4] = "Wed", [5] = "Thu", [6] = "Fri", [7] = "Sat" }
    local parts = {}
    for _, day in ipairs(days or {}) do
        parts[#parts + 1] = labels[tonumber(day)] or tostring(day)
    end
    return table.concat(parts, ",")
end

local function buildEditorState(group)
    local startDate, startTime = group and addon.GroupsService:GetStartFields(group) or "", ""
    local recurrenceMode, weekdayText = group and addon.GroupsService:GetRecurrenceFields(group) or "none", ""
    local activityKind = "party"
    if group and tostring(group.activityType or "") == "raid" then
        activityKind = "raid"
    elseif group and string.find(string.lower(tostring(group.difficulty or "")), "mythic+", 1, true) then
        activityKind = "mythicplus"
    end

    return {
        mode = group and "edit" or "create",
        groupId = group and group.id or nil,
        eventName = group and (group.eventName or group.name) or "",
        activityKind = activityKind,
        raidDifficulty = group and tostring(group.difficulty or "Heroic") or "Heroic",
        mythicLevel = group and addon.GroupsService:GetMythicPlusLevel(group) or 2,
        startDate = startDate,
        startTime = startTime ~= "" and startTime or "20:00",
        durationHours = tostring(group and group.durationHours or 2),
        minItemLevel = tostring(group and group.minItemLevel or 0),
        recurrenceMode = recurrenceMode ~= "" and recurrenceMode or "none",
        weekdayText = weekdayText,
    }
end

local function createActionButton(parent, width, point, relativeTo, relativePoint, x, y, text, onClick)
    local button = CreateFrame("Button", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    button:SetSize(width, 30)
    button:SetPoint(point, relativeTo, relativePoint, x, y)
    button:SetScript("OnClick", onClick)

    local art = button._gh3GroupsButtonArt
    if not art then
        art = button:CreateTexture(nil, "ARTWORK", nil, 1)
        art:SetAllPoints(button)
        button._gh3GroupsButtonArt = art
    end
    applyArtTexture(art, BUTTON_ART_KEY, { 0.10, 0.09, 0.07, 0.96 })

    local function setButtonArt(textureKey)
        applyArtTexture(art, textureKey or BUTTON_ART_KEY, { 0.10, 0.09, 0.07, 0.96 })
    end

    button:SetScript("OnEnter", function()
        setButtonArt(BUTTON_SELECTED_ART_KEY)
    end)
    button:SetScript("OnLeave", function()
        setButtonArt(BUTTON_ART_KEY)
    end)
    button:SetScript("OnMouseDown", function()
        setButtonArt(BUTTON_SELECTED_ART_KEY)
    end)
    button:SetScript("OnMouseUp", function(self)
        setButtonArt(self:IsMouseOver() and BUTTON_SELECTED_ART_KEY or BUTTON_ART_KEY)
    end)

    local label = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("CENTER", button, "CENTER", 0, 0)
    label:SetText(text)
    label:SetTextColor(0.96, 0.93, 0.84, 1)
    label:SetScale(0.92)
    button.Label = label
    return button
end

local function createSectionFrame(parent, rect, titleText)
    local frame = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    ApplyRect(frame, parent, rect)
    local useOuterPanelArt = (tonumber(rect.h) or 0) >= 420

    if useOuterPanelArt then
        local background = frame:CreateTexture(nil, "BACKGROUND")
        background:SetAllPoints(frame)
        applyArtTexture(background, PANEL_ART_KEY, { 0.07, 0.07, 0.08, 0.94 })
        frame.Background = background
    else
        frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        frame:SetBackdropColor(0.08, 0.08, 0.09, 0.96)
        frame:SetBackdropBorderColor(0.42, 0.33, 0.12, 1)
    end

    local title = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -10)
    title:SetText(titleText or "")
    title:SetTextColor(0.94, 0.83, 0.45, 1)
    title:SetScale(1.02)

    local divider = frame:CreateTexture(nil, "ARTWORK")
    divider:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -30)
    divider:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -30)
    divider:SetHeight(1)
    divider:SetColorTexture(0.42, 0.33, 0.12, 0.9)

    local content = nil
    if useOuterPanelArt then
        content = createArtContentFrame(frame, PANEL_ART_KEY, rect.w, rect.h, { left = 18, right = 18, top = 42, bottom = 18 })
    else
        content = CreateFrame("Frame", nil, frame)
        content:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -38)
        content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 12)
    end
    frame.Content = content
    return frame
end

local function createSummaryCard(parent, x, y, width, titleText, valueText, subText)
    local card = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    card:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    card:SetSize(width, 88)
    card:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
    card:SetBackdropColor(0.09, 0.10, 0.12, 0.96)
    card:SetBackdropBorderColor(0.62, 0.47, 0.16, 1)

    local title = card:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    title:SetPoint("TOPLEFT", card, "TOPLEFT", 12, -12)
    title:SetText(titleText)
    title:SetTextColor(0.94, 0.83, 0.45, 1)

    local value = card:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    value:SetPoint("TOPLEFT", card, "TOPLEFT", 12, -34)
    value:SetText(valueText)
    value:SetTextColor(0.96, 0.93, 0.84, 1)
    value:SetScale(1.08)

    local sub = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    sub:SetPoint("TOPLEFT", card, "TOPLEFT", 12, -58)
    sub:SetWidth(width - 24)
    sub:SetJustifyH("LEFT")
    sub:SetText(subText or "")
    sub:SetTextColor(0.82, 0.78, 0.68, 1)
    sub:SetSpacing(2)
    return card
end

local function createEditorField(parent, x, y, width, labelText, initialText)
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    label:SetText(labelText)
    label:SetTextColor(0.90, 0.80, 0.42, 1)

    local host = CreateFrame("Frame", nil, parent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    host:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 18)
    host:SetSize(width, 26)

    local background = host:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(host)
    applyArtTexture(background, INPUT_ART_KEY, { 0.08, 0.08, 0.10, 0.96 })

    local input = CreateFrame("EditBox", nil, host, "InputBoxTemplate")
    if input.Left then input.Left:SetAlpha(0) end
    if input.Middle then input.Middle:SetAlpha(0) end
    if input.Right then input.Right:SetAlpha(0) end
    input:SetAutoFocus(false)
    input:SetPoint("TOPLEFT", host, "TOPLEFT", 8, -4)
    input:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -8, 4)
    input:SetFontObject("GameFontHighlight")
    input:SetText(initialText or "")
    input:SetTextColor(0.96, 0.93, 0.84, 1)
    host.Input = input
    host.Label = label
    return host
end

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

function page:Render(parent, snapshot)
    local groupsLayout = addon.NewUILayout and addon.NewUILayout.Groups or {}
    local root = CreateFrame("Frame", nil, parent)

    ApplyRect(root, parent, groupsLayout.Page or { x = 0, y = 0, w = tonumber(parent:GetWidth()) or 1380, h = tonumber(parent:GetHeight()) or 742 })
    page._rootFrame = root
    page._parentFrame = parent
    page._snapshot = snapshot
    installMainFrameBridge()

    local basePanelRect = groupsLayout.Panel or { x = 20, y = -36, w = 1340, h = 638 }
    local panelRect = {
        x = basePanelRect.x,
        y = (tonumber(basePanelRect.y) or -36) + LEGACY_CONTENT_OFFSET_Y,
        w = basePanelRect.w,
        h = (tonumber(basePanelRect.h) or 638) + LEGACY_CONTENT_OFFSET_Y,
    }
    local panel = createPanel(root, panelRect, "Groups")
    local filterType = addon.MainFrame and addon.MainFrame.state and addon.MainFrame.state.groupsFilterType or "all"
    local sortKey = addon.MainFrame and addon.MainFrame.state and addon.MainFrame.state.groupsSortKey or "start"
    local groups = addon.GroupsService and addon.GroupsService.ListGroups and addon.GroupsService:ListGroups({ activityType = filterType, sortBy = sortKey }) or {}
    local selectedGroupId = addon.MainFrame and addon.MainFrame.GetSelectedGroupId and addon.MainFrame:GetSelectedGroupId() or nil
    local selectedGroup = selectedGroupId and addon.GroupsService and addon.GroupsService.GetGroup and addon.GroupsService:GetGroup(selectedGroupId) or groups[1]

    if selectedGroup and addon.MainFrame and addon.MainFrame.SetSelectedGroup and selectedGroup.id ~= selectedGroupId then
        addon.MainFrame:SetSelectedGroup(selectedGroup.id)
    end

    local intro = panel.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    intro:SetPoint("TOPLEFT", panel.Content, "TOPLEFT", 8, 0)
    intro:SetText("Plan activities, manage signups, and build suggested lineups.")
    intro:SetTextColor(0.82, 0.78, 0.68, 1)

    local activeStart = selectedGroup and addon.GroupsService:GetResolvedOccurrenceStart(selectedGroup) or nil
    local occurrenceDisplay = selectedGroup and addon.GroupsService:GetOccurrenceDisplay(selectedGroup, activeStart) or nil
    local planner = selectedGroup and addon.GroupsService:GetPlannerData(selectedGroup.id, activeStart) or nil
    local canManageGroups = addon.GroupsService and addon.GroupsService:CanManageGroups() or false

    local listPanel = createSectionFrame(panel.Content, { x = 0, y = -26, w = 458, h = 604 }, "Activity List")
    local detailPanel = createSectionFrame(panel.Content, { x = 474, y = -26, w = 824, h = 604 }, selectedGroup and (selectedGroup.eventName or selectedGroup.name) or "Groups")

    local function setActionButtonSelected(button, isSelected)
        if button and button._gh3GroupsButtonArt then
            applyArtTexture(button._gh3GroupsButtonArt, isSelected and BUTTON_SELECTED_ART_KEY or BUTTON_ART_KEY, { 0.10, 0.09, 0.07, 0.96 })
        end
        if button and button.Label then
            button.Label:SetTextColor(isSelected and 1 or 0.96, isSelected and 0.95 or 0.93, isSelected and 0.86 or 0.84, 1)
        end
    end

    local editorOverlay = CreateFrame("Frame", nil, root, BackdropTemplateMixin and "BackdropTemplate" or nil)
    editorOverlay:SetAllPoints(root)
    editorOverlay:SetFrameStrata("DIALOG")
    editorOverlay:SetFrameLevel(root:GetFrameLevel() + 40)
    editorOverlay:EnableMouse(true)
    editorOverlay:Hide()

    local editorShade = editorOverlay:CreateTexture(nil, "BACKGROUND")
    editorShade:SetAllPoints(editorOverlay)
    editorShade:SetColorTexture(0, 0, 0, 0.72)

    local editorDialog = createSectionFrame(editorOverlay, { x = 266, y = -74, w = 840, h = 456 }, "Activity Editor")
    editorDialog:SetFrameLevel(editorOverlay:GetFrameLevel() + 2)

    local editorHint = editorDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    editorHint:SetPoint("TOPLEFT", editorDialog.Content, "TOPLEFT", 10, 0)
    editorHint:SetWidth(680)
    editorHint:SetJustifyH("LEFT")
    editorHint:SetTextColor(0.82, 0.78, 0.68, 1)

    local eventField = createEditorField(editorDialog.Content, 10, -28, 320, "Event", "")
    local dateField = createEditorField(editorDialog.Content, 346, -28, 132, "Date", "")
    local timeField = createEditorField(editorDialog.Content, 492, -28, 96, "Time", "")
    local durationField = createEditorField(editorDialog.Content, 602, -28, 92, "Hours", "2")
    local minField = createEditorField(editorDialog.Content, 708, -28, 92, "Min iLvl", "0")
    local weekdaysField = createEditorField(editorDialog.Content, 10, -158, 220, "Weekdays", "")
    local mythicField = createEditorField(editorDialog.Content, 246, -158, 100, "M+ Level", "2")

    local typeLabel = editorDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    typeLabel:SetPoint("TOPLEFT", editorDialog.Content, "TOPLEFT", 10, -88)
    typeLabel:SetText("Type")
    typeLabel:SetTextColor(0.90, 0.80, 0.42, 1)

    local recurrenceLabel = editorDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    recurrenceLabel:SetPoint("TOPLEFT", editorDialog.Content, "TOPLEFT", 330, -88)
    recurrenceLabel:SetText("Repeat")
    recurrenceLabel:SetTextColor(0.90, 0.80, 0.42, 1)

    local editorStatus = editorDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    editorStatus:SetPoint("BOTTOMLEFT", editorDialog.Content, "BOTTOMLEFT", 10, 16)
    editorStatus:SetWidth(520)
    editorStatus:SetJustifyH("LEFT")
    editorStatus:SetTextColor(0.96, 0.76, 0.38, 1)

    local editorState = buildEditorState(nil)
    local refreshEditorButtons
    local activityButtons = {}
    local recurrenceButtons = {}
    local raidButtons = {}

    activityButtons.raid = createActionButton(editorDialog.Content, 92, "TOPLEFT", editorDialog.Content, "TOPLEFT", 10, -106, "Raid", function() editorState.activityKind = "raid" if refreshEditorButtons then refreshEditorButtons() end end)
    activityButtons.mythicplus = createActionButton(editorDialog.Content, 110, "TOPLEFT", editorDialog.Content, "TOPLEFT", 112, -106, "Mythic+", function() editorState.activityKind = "mythicplus" if refreshEditorButtons then refreshEditorButtons() end end)
    activityButtons.party = createActionButton(editorDialog.Content, 92, "TOPLEFT", editorDialog.Content, "TOPLEFT", 232, -106, "Party", function() editorState.activityKind = "party" if refreshEditorButtons then refreshEditorButtons() end end)

    recurrenceButtons.none = createActionButton(editorDialog.Content, 92, "TOPLEFT", editorDialog.Content, "TOPLEFT", 330, -106, "None", function() editorState.recurrenceMode = "none" if refreshEditorButtons then refreshEditorButtons() end end)
    recurrenceButtons.weekly = createActionButton(editorDialog.Content, 92, "TOPLEFT", editorDialog.Content, "TOPLEFT", 432, -106, "Weekly", function() editorState.recurrenceMode = "weekly" if refreshEditorButtons then refreshEditorButtons() end end)
    recurrenceButtons.daily = createActionButton(editorDialog.Content, 92, "TOPLEFT", editorDialog.Content, "TOPLEFT", 534, -106, "Daily", function() editorState.recurrenceMode = "daily" if refreshEditorButtons then refreshEditorButtons() end end)

    local raidLabel = editorDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    raidLabel:SetPoint("TOPLEFT", editorDialog.Content, "TOPLEFT", 10, -218)
    raidLabel:SetText("Raid Difficulty")
    raidLabel:SetTextColor(0.90, 0.80, 0.42, 1)

    raidButtons.Normal = createActionButton(editorDialog.Content, 96, "TOPLEFT", editorDialog.Content, "TOPLEFT", 10, -236, "Normal", function() editorState.raidDifficulty = "Normal" if refreshEditorButtons then refreshEditorButtons() end end)
    raidButtons.Heroic = createActionButton(editorDialog.Content, 96, "TOPLEFT", editorDialog.Content, "TOPLEFT", 116, -236, "Heroic", function() editorState.raidDifficulty = "Heroic" if refreshEditorButtons then refreshEditorButtons() end end)
    raidButtons.Mythic = createActionButton(editorDialog.Content, 96, "TOPLEFT", editorDialog.Content, "TOPLEFT", 222, -236, "Mythic", function() editorState.raidDifficulty = "Mythic" if refreshEditorButtons then refreshEditorButtons() end end)

    refreshEditorButtons = function()
        for kind, button in pairs(activityButtons) do setActionButtonSelected(button, editorState.activityKind == kind) end
        for mode, button in pairs(recurrenceButtons) do setActionButtonSelected(button, editorState.recurrenceMode == mode) end
        for difficultyKey, button in pairs(raidButtons) do setActionButtonSelected(button, editorState.raidDifficulty == difficultyKey) end
        local showRaid = editorState.activityKind == "raid"
        local showMythic = editorState.activityKind == "mythicplus"
        raidLabel:SetShown(showRaid)
        raidButtons.Normal:SetShown(showRaid)
        raidButtons.Heroic:SetShown(showRaid)
        raidButtons.Mythic:SetShown(showRaid)
        mythicField:SetShown(showMythic)
        mythicField.Label:SetShown(showMythic)
        local showWeekdays = editorState.recurrenceMode ~= "none"
        weekdaysField:SetShown(showWeekdays)
        weekdaysField.Label:SetShown(showWeekdays)
    end

    local function closeEditor()
        editorOverlay:Hide()
    end

    local function openEditor(group)
        if not canManageGroups then
            return
        end

        editorState = buildEditorState(group)
        eventField.Input:SetText(editorState.eventName or "")
        dateField.Input:SetText(editorState.startDate or "")
        timeField.Input:SetText(editorState.startTime or "20:00")
        durationField.Input:SetText(editorState.durationHours or "2")
        minField.Input:SetText(editorState.minItemLevel or "0")
        weekdaysField.Input:SetText(editorState.weekdayText or "")
        mythicField.Input:SetText(tostring(editorState.mythicLevel or 2))
        editorHint:SetText(editorState.mode == "edit" and "Edit the selected activity directly from the New UI." or "Create a new activity directly from the New UI.")
        editorStatus:SetText("")
        refreshEditorButtons()
        editorOverlay:Show()
    end

    local function saveEditor()
        local startDateText = trimText(dateField.Input:GetText())
        local startTimeText = trimText(timeField.Input:GetText())
        local mythicLevel = math.max(2, tonumber(mythicField.Input:GetText()) or 2)
        local payload = {
            eventName = trimText(eventField.Input:GetText()),
            activityType = editorState.activityKind == "raid" and "raid" or "party",
            difficulty = editorState.activityKind == "raid" and editorState.raidDifficulty or (editorState.activityKind == "mythicplus" and ("Mythic+" .. tostring(mythicLevel)) or "Party"),
            mythicLevel = mythicLevel,
            durationHours = tonumber(durationField.Input:GetText()) or 2,
            minItemLevel = tonumber(minField.Input:GetText()) or 0,
            recurrenceMode = editorState.recurrenceMode,
            recurrenceWeekdays = parseWeekdayText(weekdaysField.Input:GetText()),
        }

        if startDateText ~= "" or startTimeText ~= "" then
            payload.startDate = startDateText
            payload.startTime = startTimeText
        end

        if editorState.mode == "edit" and editorState.groupId then
            local ok, message = addon.GroupsService:UpdateGroupConfig(editorState.groupId, payload)
            if not ok then
                editorStatus:SetText(tostring(message or "Could not save activity."))
                return
            end
        else
            local created, message = addon.GroupsService:CreateGroup(payload.eventName, payload)
            if not created then
                editorStatus:SetText(tostring(message or "Could not create activity."))
                return
            end
            if addon.MainFrame and addon.MainFrame.SetSelectedGroup then
                addon.MainFrame:SetSelectedGroup(created.id)
            end
        end

        closeEditor()
        refreshPage(parent, snapshot)
    end

    createActionButton(editorDialog.Content, 92, "BOTTOMRIGHT", editorDialog.Content, "BOTTOMRIGHT", -10, 10, "Save", function() saveEditor() end)
    createActionButton(editorDialog.Content, 92, "BOTTOMRIGHT", editorDialog.Content, "BOTTOMRIGHT", -112, 10, "Cancel", function() closeEditor() end)

    local exportOverlay = CreateFrame("Frame", nil, root, BackdropTemplateMixin and "BackdropTemplate" or nil)
    exportOverlay:SetAllPoints(root)
    exportOverlay:SetFrameStrata("DIALOG")
    exportOverlay:SetFrameLevel(root:GetFrameLevel() + 45)
    exportOverlay:EnableMouse(true)
    exportOverlay:Hide()

    local exportShade = exportOverlay:CreateTexture(nil, "BACKGROUND")
    exportShade:SetAllPoints(exportOverlay)
    exportShade:SetColorTexture(0, 0, 0, 0.72)

    local exportDialog = createSectionFrame(exportOverlay, { x = 286, y = -84, w = 800, h = 430 }, "Export")
    exportDialog:SetFrameLevel(exportOverlay:GetFrameLevel() + 2)

    local exportHint = exportDialog.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    exportHint:SetPoint("TOPLEFT", exportDialog.Content, "TOPLEFT", 10, 0)
    exportHint:SetTextColor(0.82, 0.78, 0.68, 1)
    exportHint:SetWidth(730)
    exportHint:SetJustifyH("LEFT")

    local exportScroll = CreateFrame("ScrollFrame", nil, exportDialog.Content, "UIPanelScrollFrameTemplate")
    exportScroll:SetPoint("TOPLEFT", exportDialog.Content, "TOPLEFT", 10, -28)
    exportScroll:SetPoint("BOTTOMRIGHT", exportDialog.Content, "BOTTOMRIGHT", -28, 50)

    local exportEditBox = CreateFrame("EditBox", nil, exportScroll, "InputBoxTemplate")
    if exportEditBox.Left then exportEditBox.Left:SetAlpha(0) end
    if exportEditBox.Middle then exportEditBox.Middle:SetAlpha(0) end
    if exportEditBox.Right then exportEditBox.Right:SetAlpha(0) end
    exportEditBox:SetMultiLine(true)
    exportEditBox:SetAutoFocus(false)
    exportEditBox:SetWidth(720)
    exportEditBox:SetFontObject("GameFontHighlightSmall")
    exportEditBox:SetTextColor(0.96, 0.93, 0.84, 1)
    exportEditBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() exportOverlay:Hide() end)
    exportScroll:SetScrollChild(exportEditBox)

    local function openExport(titleText, exportText)
        exportHint:SetText(titleText or "Export")
        exportEditBox:SetText(tostring(exportText or ""))
        exportEditBox:HighlightText(0, 0)
        exportEditBox:SetCursorPosition(0)
        exportOverlay:Show()
    end

    createActionButton(exportDialog.Content, 92, "BOTTOMRIGHT", exportDialog.Content, "BOTTOMRIGHT", -10, 10, "Close", function() exportOverlay:Hide() end)

    createActionButton(panel.Content, 126, "TOPRIGHT", panel.Content, "TOPRIGHT", -502, -4, "Create Activity", function()
        openEditor(nil)
    end)
    createActionButton(panel.Content, 148, "TOPRIGHT", panel.Content, "TOPRIGHT", -346, -4, "Export Party/Raid", function()
        if addon.MainFrame and addon.MainFrame.ShowCsvExportDialog and addon.MainFrame.BuildCurrentPartyRaidExportCsv then
            openExport("Party/Raid Export", addon.MainFrame:BuildCurrentPartyRaidExportCsv())
        end
    end)
    createActionButton(panel.Content, 126, "TOPRIGHT", panel.Content, "TOPRIGHT", -212, -4, "Export Events", function()
        if addon.MainFrame and addon.MainFrame.ShowCsvExportDialog and addon.MainFrame.BuildGroupsEventsExportCsv then
            openExport("Events Export", addon.MainFrame:BuildGroupsEventsExportCsv(groups))
        end
    end)

    createActionButton(listPanel.Content, 108, "TOPLEFT", listPanel.Content, "TOPLEFT", 8, -6, "Filter: " .. string.upper(filterType), function()
        if not addon.MainFrame or not addon.MainFrame.state then return end
        addon.MainFrame.state.groupsFilterType = filterType == "all" and "raid" or (filterType == "raid" and "party" or "all")
        refreshPage(parent, snapshot)
    end)
    createActionButton(listPanel.Content, 108, "TOPLEFT", listPanel.Content, "TOPLEFT", 126, -6, "Sort: " .. (sortKey == "type" and "Type" or "Start"), function()
        if not addon.MainFrame or not addon.MainFrame.state then return end
        addon.MainFrame.state.groupsSortKey = sortKey == "type" and "start" or "type"
        refreshPage(parent, snapshot)
    end)

    for index = 1, math.min(#groups, 6) do
        local group = groups[index]
        local row = CreateFrame("Button", nil, listPanel.Content, BackdropTemplateMixin and "BackdropTemplate" or nil)
        row:SetPoint("TOPLEFT", listPanel.Content, "TOPLEFT", 8, -44 - ((index - 1) * 70))
        row:SetSize(412, 62)
        row:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
        row:SetBackdropColor(group.id == (selectedGroup and selectedGroup.id or nil) and 0.08 or 0.06, 0.09, group.id == (selectedGroup and selectedGroup.id or nil) and 0.13 or 0.08, 0.96)
        row:SetBackdropBorderColor(group.id == (selectedGroup and selectedGroup.id or nil) and 0.70 or 0.42, group.id == (selectedGroup and selectedGroup.id or nil) and 0.56 or 0.33, group.id == (selectedGroup and selectedGroup.id or nil) and 0.20 or 0.12, 1)
        row:SetScript("OnClick", function()
            if addon.MainFrame and addon.MainFrame.SetSelectedGroup then addon.MainFrame:SetSelectedGroup(group.id) end
            refreshPage(parent, snapshot)
        end)

        local name = row:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        name:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -10)
        name:SetText(group.eventName or group.name or "Activity")
        name:SetTextColor(group.id == (selectedGroup and selectedGroup.id or nil) and 0.96 or 0.90, 0.90, 0.84, 1)

        local meta = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        meta:SetPoint("TOPLEFT", row, "TOPLEFT", 12, -32)
        meta:SetText(string.format("%s  |  %s", getGroupTypeLabel(group), group.recurrenceSummary or "One-time"))
        meta:SetTextColor(0.82, 0.78, 0.68, 1)

        local when = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        when:SetPoint("TOPRIGHT", row, "TOPRIGHT", -12, -14)
        when:SetText(addon.GroupsService:GetOccurrenceDisplay(group, addon.GroupsService:GetResolvedOccurrenceStart(group)).localCompactLabel or "No start")
        when:SetTextColor(0.94, 0.83, 0.45, 1)

        local difficulty = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        difficulty:SetPoint("TOPRIGHT", row, "TOPRIGHT", -12, -36)
        difficulty:SetText(getGroupDifficultyLabel(group))
        difficulty:SetTextColor(0.96, 0.93, 0.84, 1)
    end

    local listFooter = listPanel.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    listFooter:SetPoint("BOTTOMLEFT", listPanel.Content, "BOTTOMLEFT", 12, 8)
    listFooter:SetText(string.format("%d activities", #groups))
    listFooter:SetTextColor(0.96, 0.93, 0.84, 1)

    if selectedGroup then
        local meta = detailPanel.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        meta:SetPoint("TOPLEFT", detailPanel.Content, "TOPLEFT", 10, -8)
        meta:SetWidth(760)
        meta:SetJustifyH("LEFT")
        meta:SetText(string.format("Owner: %s  |  %s  |  %s", selectedGroup.owner or "Unknown", selectedGroup.recurrenceSummary or "One-time", occurrenceDisplay and occurrenceDisplay.localLabel or "No start time"))
        meta:SetTextColor(0.82, 0.78, 0.68, 1)

        createActionButton(detailPanel.Content, 90, "TOPLEFT", detailPanel.Content, "TOPLEFT", 10, -38, addon.GroupsService:IsSignedUp(selectedGroup.id, nil, activeStart) and "Withdraw" or "Sign Up", function()
            local ok = addon.GroupsService:IsSignedUp(selectedGroup.id, nil, activeStart) and addon.GroupsService:WithdrawPlayer(selectedGroup.id, activeStart) or addon.GroupsService:SignupPlayer(selectedGroup.id, activeStart)
            if ok then refreshPage(parent, snapshot) end
        end)
        createActionButton(detailPanel.Content, 84, "TOPLEFT", detailPanel.Content, "TOPLEFT", 110, -38, "Invite Best", function()
            if addon.GroupsService:InviteSuggestedGroup(selectedGroup.id, activeStart) then refreshPage(parent, snapshot) end
        end)
        createActionButton(detailPanel.Content, 80, "TOPLEFT", detailPanel.Content, "TOPLEFT", 204, -38, "Invite All", function()
            if addon.GroupsService:InviteGroup(selectedGroup.id, activeStart) then refreshPage(parent, snapshot) end
        end)
        createActionButton(detailPanel.Content, 60, "TOPLEFT", detailPanel.Content, "TOPLEFT", 294, -38, "Edit", function() openEditor(selectedGroup) end)
        createActionButton(detailPanel.Content, 72, "TOPLEFT", detailPanel.Content, "TOPLEFT", 364, -38, "Delete", function()
            if canManageGroups and addon.GroupsService:DeleteGroup(selectedGroup.id) then if addon.MainFrame and addon.MainFrame.SetSelectedGroup then addon.MainFrame:SetSelectedGroup(nil) end refreshPage(parent, snapshot) end
        end)

        createSummaryCard(detailPanel.Content, 10, -76, 182, "Type", getGroupTypeLabel(selectedGroup), getGroupDifficultyLabel(selectedGroup))
        createSummaryCard(detailPanel.Content, 202, -76, 182, "Next Run", occurrenceDisplay and (occurrenceDisplay.localDateText or "Unset") or "Unset", occurrenceDisplay and (occurrenceDisplay.localTimeText or "Add a start time") or "Add a start time")
        createSummaryCard(detailPanel.Content, 394, -76, 182, "Signups", tostring(planner and planner.totalSignups or 0), string.format("Qualified %d", planner and planner.qualifiedCount or 0))
        createSummaryCard(detailPanel.Content, 586, -76, 182, "Needs", tostring((planner and planner.deficits and (planner.deficits.tanks + planner.deficits.healers + planner.deficits.melee + planner.deficits.ranged)) or 0), buildCompactNeedsSummary(planner and planner.deficits))

        local detailsBlock = createSectionFrame(detailPanel.Content, { x = 10, y = -174, w = 758, h = 186 }, "Activity Details")
        for dividerIndex = 1, 3 do
            local divider = detailsBlock.Content:CreateTexture(nil, "ARTWORK")
            divider:SetPoint("TOPLEFT", detailsBlock.Content, "TOPLEFT", 182 * dividerIndex, -4)
            divider:SetPoint("BOTTOMLEFT", detailsBlock.Content, "BOTTOMLEFT", 182 * dividerIndex, 4)
            divider:SetWidth(1)
            divider:SetColorTexture(0.34, 0.28, 0.16, 0.9)
        end
        for dividerIndex = 1, 2 do
            local divider = detailsBlock.Content:CreateTexture(nil, "ARTWORK")
            divider:SetPoint("TOPLEFT", detailsBlock.Content, "TOPLEFT", 4, -40 - ((dividerIndex - 1) * 44))
            divider:SetPoint("TOPRIGHT", detailsBlock.Content, "TOPRIGHT", -4, -40 - ((dividerIndex - 1) * 44))
            divider:SetHeight(1)
            divider:SetColorTexture(0.34, 0.28, 0.16, 0.9)
        end
        local entries = {
            { "Event", selectedGroup.eventName or selectedGroup.name or "Activity" },
            { "Duration", string.format("%d hours", selectedGroup.durationHours or 0) },
            { "Type", getGroupTypeLabel(selectedGroup) },
            { "Difficulty", getGroupDifficultyLabel(selectedGroup) },
            { "Min iLvl", tostring(selectedGroup.minItemLevel or 0) },
            { "Repeat", selectedGroup.recurrenceSummary or "One-time" },
            { "Local Time", occurrenceDisplay and occurrenceDisplay.localLabel or "Not scheduled" },
            { "Creator Time", occurrenceDisplay and occurrenceDisplay.creatorLabel or "Viewer local" },
            { "Roles", string.format("T%s H%s M%s R%s", selectedGroup.requirements.tanks or 0, selectedGroup.requirements.healers or 0, selectedGroup.requirements.melee or 0, selectedGroup.requirements.ranged or 0) },
            { "Owner", tostring(selectedGroup.owner or "Unknown") },
            { "Updated", selectedGroup.updatedAt and date("%m/%d %H:%M", selectedGroup.updatedAt) or "Unknown" },
            { "Updated By", tostring(selectedGroup.updatedBy or "Unknown") },
        }
        for index, entry in ipairs(entries) do
            local column = (index - 1) % 4
            local row = math.floor((index - 1) / 4)
            local label = detailsBlock.Content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            label:SetPoint("TOPLEFT", detailsBlock.Content, "TOPLEFT", 8 + (column * 184), -8 - (row * 44))
            label:SetWidth(170)
            label:SetJustifyH("LEFT")
            label:SetText(entry[1])
            label:SetTextColor(0.90, 0.80, 0.42, 1)

            local value = detailsBlock.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            value:SetPoint("TOPLEFT", label, "BOTTOMLEFT", 0, -2)
            value:SetWidth(170)
            value:SetJustifyH("LEFT")
            value:SetText(entry[2])
            value:SetTextColor(0.96, 0.93, 0.84, 1)
        end

        local suggestedBlock = createSectionFrame(detailPanel.Content, { x = 10, y = -370, w = 758, h = 94 }, "Suggested Lineup")
        local suggestedText = suggestedBlock.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        suggestedText:SetPoint("TOPLEFT", suggestedBlock.Content, "TOPLEFT", 8, -6)
        suggestedText:SetWidth(730)
        suggestedText:SetJustifyH("LEFT")
        suggestedText:SetText(buildSuggestedText(planner))
        suggestedText:SetTextColor(0.82, 0.78, 0.68, 1)

        local notesBlock = createSectionFrame(detailPanel.Content, { x = 10, y = -472, w = 758, h = 76 }, "Signup Notes")
        local notesText = notesBlock.Content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        notesText:SetPoint("TOPLEFT", notesBlock.Content, "TOPLEFT", 8, -10)
        notesText:SetWidth(730)
        notesText:SetJustifyH("LEFT")
        notesText:SetText("Add notes or instructions for signups in the legacy editor while this page-native editor is still being built.")
        notesText:SetTextColor(0.82, 0.78, 0.68, 1)
    end

    return root
end

return page