GuildHelper = GuildHelper or {}

local addon = GuildHelper
addon.MainFrame = addon.MainFrame or {}

local compat = addon.MainFrame

local function lowerText(value)
    return string.lower(tostring(value or ""))
end

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

    local count = 0

    for key in pairs(value) do
        if type(key) ~= "number" then
            return false
        end

        count = count + 1
    end

    for index = 1, count do
        if value[index] == nil then
            return false
        end
    end

    return true
end

local function stringifyExportValue(value)
    if value == nil then
        return ""
    end

    local valueType = type(value)

    if valueType == "boolean" then
        return value and "true" or "false"
    end

    if valueType == "number" or valueType == "string" then
        return tostring(value)
    end

    if valueType == "table" then
        if isPlainArray(value) then
            local parts = {}

            for _, entry in ipairs(value) do
                parts[#parts + 1] = stringifyExportValue(entry)
            end

            return table.concat(parts, " | ")
        end

        if addon.Json and addon.Json.Encode then
            return addon.Json:Encode(value) or ""
        end
    end

    return tostring(value)
end

local function escapeCsvValue(value)
    local text = tostring(stringifyExportValue(value) or "")

    if text:find('[",\r\n]') then
        text = '"' .. text:gsub('"', '""') .. '"'
    end

    return text
end

local function convertExportTimestamps(record)
    local converted = {}

    for key, value in pairs(record or {}) do
        if type(key) == "string"
            and key:match("At$")
            and type(value) == "number"
            and value > 0
            and math.floor(value) == value then
            converted[key] = date("%Y-%m-%d %H:%M:%S", value)
        else
            converted[key] = value
        end
    end

    return converted
end

local function buildCsvFromRecords(records, preferredOrder)
    local headers = {}
    local seen = {}

    for _, key in ipairs(preferredOrder or {}) do
        if key and key ~= "" and not seen[key] then
            seen[key] = true
            headers[#headers + 1] = key
        end
    end

    local extraHeaders = {}

    for _, record in ipairs(records or {}) do
        for key in pairs(record or {}) do
            if not seen[key] then
                seen[key] = true
                extraHeaders[#extraHeaders + 1] = key
            end
        end
    end

    table.sort(extraHeaders)

    for _, key in ipairs(extraHeaders) do
        headers[#headers + 1] = key
    end

    local lines = {}
    lines[#lines + 1] = table.concat(headers, ",")

    for _, record in ipairs(records or {}) do
        local row = {}

        for _, key in ipairs(headers) do
            row[#row + 1] = escapeCsvValue(record and record[key] or "")
        end

        lines[#lines + 1] = table.concat(row, ",")
    end

    return table.concat(lines, "\n")
end

local function resolveShellView(viewKey)
    local settingsPane = nil
    local shellPage = "home"

    if viewKey == "board" then
        shellPage = "news"
    elseif viewKey == "groups" then
        shellPage = "groups"
    elseif viewKey == "mytoons" then
        shellPage = "mytoons"
    elseif viewKey == "roster" then
        shellPage = "roster"
    elseif viewKey == "bis" then
        shellPage = "bis"
    elseif viewKey == "mythics" then
        shellPage = "mythicplus"
    elseif viewKey == "raidbingo" then
        shellPage = "raidbingo"
    elseif viewKey == "celebrations" then
        shellPage = "celebrate"
    elseif viewKey == "settings" then
        shellPage = "settings"
        settingsPane = "settings"
    elseif viewKey == "sync" then
        shellPage = "settings"
        settingsPane = "datasync"
    elseif viewKey == "setup" then
        shellPage = "settings"
        settingsPane = "setup"
    elseif viewKey == "help" then
        shellPage = "settings"
        settingsPane = "help"
    elseif viewKey == "maintenance" then
        shellPage = "settings"
        settingsPane = "maintenance"
    end

    return shellPage, settingsPane
end

local function ensureShell()
    local shell = addon.NewUIShell
    if not shell then
        return nil
    end

    if not shell.frame and shell.Create then
        shell:Create(UIParent)
    end

    compat.frame = shell.frame
    compat.contentHost = shell.contentHost
    return shell
end

local function renderPlaceholderPane(host, title, bodyLines)
    if not host then
        return
    end

    local panel = CreateFrame("Frame", nil, host, "BackdropTemplate")
    panel:SetAllPoints(host)

    if panel.SetBackdrop then
        panel:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        panel:SetBackdropColor(0.04, 0.05, 0.07, 0.82)
        panel:SetBackdropBorderColor(0.32, 0.26, 0.12, 1)
    end

    local heading = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    heading:SetPoint("TOPLEFT", panel, "TOPLEFT", 18, -18)
    heading:SetText(title or "GH3")

    local body = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    body:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -12)
    body:SetPoint("RIGHT", panel, "RIGHT", -18, 0)
    body:SetJustifyH("LEFT")
    body:SetJustifyV("TOP")
    body:SetText(table.concat(bodyLines or {}, "\n"))
    body:SetTextColor(0.84, 0.84, 0.84, 1)
end

local function buildMinimapQuickLinkOptions()
    return {
        {
            checked = false,
            text = "Preview UI",
            onClick = function()
                compat:ShowForCurrentView()
            end,
        },
        {
            checked = compat.state and compat.state.maintenanceTab == "invites" and addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() == "maintenance",
            text = "Invites",
            onClick = function()
                compat:OpenQuickLink("maintenance", "invites")
            end,
        },
        {
            checked = addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() == "mythics",
            text = "Mythic+",
            onClick = function()
                compat:OpenQuickLink("mythics")
            end,
            visible = addon.IsFeatureEnabled and addon:IsFeatureEnabled("mythics"),
        },
        {
            checked = addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() == "groups",
            text = "Groups",
            onClick = function()
                compat:OpenQuickLink("groups")
            end,
            visible = addon.IsFeatureEnabled and addon:IsFeatureEnabled("groups"),
        },
        {
            checked = addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() == "mytoons",
            text = "My Toons",
            onClick = function()
                compat:OpenQuickLink("mytoons")
            end,
        },
        {
            checked = addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() == "roster",
            text = "Roster",
            onClick = function()
                compat:OpenQuickLink("roster")
            end,
        },
    }
end

local function hideMinimapQuickLinkMenu()
    if compat._minimapQuickLinkMenuFrame then
        compat._minimapQuickLinkMenuFrame:Hide()
    end
end

local function ensureMinimapQuickLinkMenuFrame()
    if compat._minimapQuickLinkMenuFrame then
        return compat._minimapQuickLinkMenuFrame
    end

    local frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil)
    frame:SetFrameStrata("TOOLTIP")
    frame:SetToplevel(true)
    frame:SetClampedToScreen(true)
    frame:Hide()
    frame.buttons = {}

    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.04, 0.05, 0.07, 0.98)
        frame:SetBackdropBorderColor(0.62, 0.48, 0.16, 1)
    end

    frame:SetScript("OnHide", function(self)
        for _, button in ipairs(self.buttons or {}) do
            button:Hide()
        end
    end)

    compat._minimapQuickLinkMenuFrame = frame
    return frame
end

local function styleHelperLauncherButton(button)
    if not button then
        return
    end

    if button.SetBackdrop then
        button:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        button:SetBackdropColor(0.06, 0.07, 0.10, 0.98)
        button:SetBackdropBorderColor(0.74, 0.58, 0.22, 0.95)
    end

    if not button.Text then
        button.Text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        button.Text:SetPoint("CENTER", button, "CENTER", 0, 0)
    end

    button.Text:SetText("GH3 Helper")
    button.Text:SetTextColor(0.96, 0.93, 0.84, 1)
end

local function createHelperLauncher(parent, point, relativeTo, relativePoint, x, y, width, height, onClick)
    local button = CreateFrame("Button", nil, parent, "BackdropTemplate")
    button:SetSize(width or 98, height or 22)
    button:SetPoint(point, relativeTo, relativePoint, x, y)
    styleHelperLauncherButton(button)
    button:SetScript("OnClick", function()
        if type(onClick) == "function" then
            onClick()
        end
    end)
    return button
end

function compat:EnsureState()
    self.state = self.state or {
        boardDraft = nil,
        boardDraftDirty = false,
        boardFilterGuild = "all",
        boardFilterPinnedOnly = false,
        boardFilterText = "",
        boardFilterType = "all",
        boardIsEditing = false,
        groupsDetailScrollOffset = 0,
        groupsFilterType = "all",
        groupsSignupScrollOffset = 0,
        groupsSortKey = "start",
        helpArticleKey = "overview",
        maintenanceTab = "cleanup",
        pendingBackgroundRefresh = false,
        selectedContentId = nil,
        selectedGroupId = nil,
        selectedSyncPeer = nil,
    }

    self.state.boardFilterGuild = tostring(self.state.boardFilterGuild or "all")
    self.state.boardFilterText = tostring(self.state.boardFilterText or "")
    self.state.boardFilterType = self.state.boardFilterType == "announcement"
        and "announcement"
        or (self.state.boardFilterType == "news" and "news" or (self.state.boardFilterType == "event" and "event" or "all"))
    self.state.groupsFilterType = self.state.groupsFilterType == "raid"
        and "raid"
        or (self.state.groupsFilterType == "party" and "party" or "all")
    self.state.groupsSortKey = self.state.groupsSortKey == "type" and "type" or "start"
    self.state.helpArticleKey = tostring(self.state.helpArticleKey or "overview")
    self.state.maintenanceTab = self.state.maintenanceTab == "audit"
        and "audit"
        or (self.state.maintenanceTab == "invites" and "invites" or "cleanup")
end

function compat:SetSelectedContent(contentId)
    self:EnsureState()

    if self.state.selectedContentId ~= contentId then
        self.state.boardDraft = nil
        self.state.boardDraftDirty = false
        self.state.boardIsEditing = false
    end

    self.state.selectedContentId = contentId
end

function compat:GetSelectedContentId()
    self:EnsureState()

    if self.state.selectedContentId and addon.ContentService and addon.ContentService.GetContent and addon.ContentService:GetContent(self.state.selectedContentId) then
        return self.state.selectedContentId
    end

    self.state.selectedContentId = addon.ContentService and addon.ContentService.GetFirstVisibleContentId and addon.ContentService:GetFirstVisibleContentId() or nil
    return self.state.selectedContentId
end

function compat:GetBoardDraft(record)
    self:EnsureState()

    if not record then
        self.state.boardDraft = nil
        return nil
    end

    local draft = self.state.boardDraft

    if type(draft) ~= "table" or draft.contentId ~= record.id then
        draft = {
            body = record.body or "",
            contentId = record.id,
            guildName = record.guildName or "",
            title = record.title or "",
            type = record.type or "announcement",
        }
        self.state.boardDraft = draft
        self.state.boardDraftDirty = false
    end

    return draft
end

function compat:SetBoardEditing(isEditing)
    self:EnsureState()
    self.state.boardIsEditing = isEditing and true or false

    if not self.state.boardIsEditing then
        self.state.boardDraft = nil
        self.state.boardDraftDirty = false
    end
end

function compat:SetSelectedGroup(groupId)
    self:EnsureState()

    if self.state.selectedGroupId ~= groupId then
        self.state.groupsDetailScrollOffset = 0
        self.state.groupsSignupScrollOffset = 0
    end

    self.state.selectedGroupId = groupId
end

function compat:GetSelectedGroupId()
    self:EnsureState()

    if self.state.selectedGroupId and addon.GroupsService and addon.GroupsService.GetGroup and addon.GroupsService:GetGroup(self.state.selectedGroupId) then
        return self.state.selectedGroupId
    end

    local groups = addon.GroupsService and addon.GroupsService.ListGroups and addon.GroupsService:ListGroups() or {}
    self.state.selectedGroupId = groups[1] and groups[1].id or nil
    return self.state.selectedGroupId
end

function compat:SetSelectedSyncPeer(peer)
    self:EnsureState()
    self.state.selectedSyncPeer = peer and peer ~= "" and peer or nil
end

function compat:GetSelectedSyncPeer(peers)
    self:EnsureState()

    if self.state.selectedSyncPeer then
        for _, peer in ipairs(peers or {}) do
            if peer.sender == self.state.selectedSyncPeer or peer.player == self.state.selectedSyncPeer then
                return self.state.selectedSyncPeer
            end
        end
    end

    self.state.selectedSyncPeer = peers and peers[1] and (peers[1].sender or peers[1].player) or nil
    return self.state.selectedSyncPeer
end

function compat:BuildRosterExportCsv(members)
    local records = {}

    for _, member in ipairs(members or {}) do
        records[#records + 1] = convertExportTimestamps(member)
    end

    table.sort(records, function(left, right)
        return lowerText(left.fullName) < lowerText(right.fullName)
    end)

    return buildCsvFromRecords(records, {
        "id", "fullName", "name", "mainCharacter", "guildName", "rank", "realm", "faction", "className", "classFile",
        "spec", "primaryRole", "secondaryRoles", "roles", "meleeOrRanged", "itemLevel", "battleTag", "birthdate",
        "guildJoinDate", "interests", "professions", "online", "note", "updatedAt", "updatedBy", "revision"
    })
end

function compat:BuildGuildJoinDateExportCsv(members)
    local records = {}

    for _, member in ipairs(members or {}) do
        records[#records + 1] = convertExportTimestamps(member)
    end

    table.sort(records, function(left, right)
        return lowerText(left.fullName) < lowerText(right.fullName)
    end)

    return buildCsvFromRecords(records, {
        "id", "fullName", "name", "guildName", "rank", "mainCharacter", "guildJoinDate",
        "className", "classFile", "level", "online", "updatedAt", "updatedBy", "revision"
    })
end

function compat:BuildCurrentPartyRaidExportCsv()
    local exportedAt = time()
    local records = {}
    local seen = {}
    local units = {}

    local function normalizeGroupRole(roleValue, member)
        local role = tostring(roleValue or "")

        if role == "TANK" then
            return "tank"
        end

        if role == "HEALER" then
            return "healer"
        end

        if role == "DAMAGER" then
            return "dps"
        end

        if member and member.primaryRole and member.primaryRole ~= "" then
            return tostring(member.primaryRole)
        end

        return ""
    end

    if IsInRaid and IsInRaid() then
        for index = 1, (GetNumGroupMembers and GetNumGroupMembers() or 0) do
            units[#units + 1] = "raid" .. index
        end
    elseif IsInGroup and IsInGroup() then
        units[#units + 1] = "player"

        for index = 1, (GetNumSubgroupMembers and GetNumSubgroupMembers() or 0) do
            units[#units + 1] = "party" .. index
        end
    else
        units[#units + 1] = "player"
    end

    for _, unitId in ipairs(units) do
        if UnitExists and UnitExists(unitId) then
            local unitName, unitRealm = UnitFullName(unitId)
            local fullName = addon.Store and addon.Store.NormalizeFullName and addon.Store:NormalizeFullName(unitName, unitRealm or (GetRealmName and GetRealmName()) or "Unknown") or ""

            if fullName ~= "" and not seen[fullName] then
                seen[fullName] = true

                local _, localizedClass, classFile = UnitClass(unitId)
                local member = (addon.RosterService and addon.RosterService.GetMyToonMember and addon.RosterService:GetMyToonMember(fullName))
                    or (addon.RosterService and addon.RosterService.GetMember and addon.RosterService:GetMember(fullName))
                    or {}
                local itemLevel = tonumber(member.itemLevel) or 0
                local roleValue = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unitId) or nil

                records[#records + 1] = {
                    exportedAt = date("%Y-%m-%d %H:%M:%S", exportedAt),
                    source = (IsInRaid and IsInRaid()) and "raid" or ((IsInGroup and IsInGroup()) and "party" or "solo"),
                    toonName = fullName,
                    className = localizedClass or member.className or "",
                    classFile = classFile or member.classFile or "",
                    role = normalizeGroupRole(roleValue, member),
                    itemLevel = itemLevel,
                    guildName = member.guildName or "",
                    mainCharacter = member.mainCharacter or "",
                    battleTag = member.battleTag or "",
                    online = UnitIsConnected and UnitIsConnected(unitId) or (member.online and true or false),
                    timestamp = date("%Y-%m-%d %H:%M:%S", exportedAt),
                }
            end
        end
    end

    table.sort(records, function(left, right)
        return lowerText(left.toonName) < lowerText(right.toonName)
    end)

    return buildCsvFromRecords(records, {
        "exportedAt", "source", "toonName", "className", "classFile", "role", "itemLevel", "guildName",
        "mainCharacter", "battleTag", "online", "timestamp"
    })
end

function compat:BuildGroupsEventsExportCsv(groups)
    local records = {}

    for _, group in ipairs(groups or {}) do
        local planner = addon.GroupsService and addon.GroupsService.GetPlannerData and addon.GroupsService:GetPlannerData(group.id) or nil

        records[#records + 1] = {
            id = group.id or "",
            eventName = group.eventName or group.name or "",
            owner = group.owner or "",
            activityType = group.activityType or "",
            difficulty = group.difficulty or "",
            startAt = group.startAt and date("%Y-%m-%d %H:%M:%S", group.startAt) or "",
            nextOccurrenceAt = planner and planner.activeOccurrenceStart and planner.activeOccurrenceStart > 0 and date("%Y-%m-%d %H:%M:%S", planner.activeOccurrenceStart) or "",
            recurrenceSummary = group.recurrenceSummary or "",
            minItemLevel = group.minItemLevel or 0,
            durationHours = group.durationHours or 0,
            tanksRequired = group.requirements and group.requirements.tanks or 0,
            healersRequired = group.requirements and group.requirements.healers or 0,
            meleeRequired = group.requirements and group.requirements.melee or 0,
            rangedRequired = group.requirements and group.requirements.ranged or 0,
            totalSignups = planner and planner.totalSignups or 0,
            qualifiedCount = planner and planner.qualifiedCount or 0,
            suggestedCount = planner and #(planner.suggested or {}) or 0,
            updatedAt = group.updatedAt and date("%Y-%m-%d %H:%M:%S", group.updatedAt) or "",
            updatedBy = group.updatedBy or "",
        }
    end

    table.sort(records, function(left, right)
        if left.startAt == right.startAt then
            return lowerText(left.eventName) < lowerText(right.eventName)
        end

        return tostring(left.startAt) < tostring(right.startAt)
    end)

    return buildCsvFromRecords(records, {
        "id", "eventName", "owner", "activityType", "difficulty", "startAt", "nextOccurrenceAt", "recurrenceSummary",
        "minItemLevel", "durationHours", "tanksRequired", "healersRequired", "meleeRequired", "rangedRequired",
        "totalSignups", "qualifiedCount", "suggestedCount", "updatedAt", "updatedBy"
    })
end

function compat:ShowCsvExportDialog()
    return true
end

function compat:RenderSettings()
    renderPlaceholderPane(self.viewHost, "Settings", {
        "The legacy Settings renderer has been detached from the New UI runtime owner.",
        "Use the Tester tab for migrated popup previews while the remaining Settings controls are rebuilt natively.",
    })
end

function compat:RenderSync()
    renderPlaceholderPane(self.viewHost, "DataSync", {
        "The DataSync pane no longer calls back into the legacy MainFrame renderer.",
        "This surface needs a native New UI implementation before full parity is restored.",
    })
end

function compat:RenderSetup()
    renderPlaceholderPane(self.viewHost, "Linked Guild Setup", {
        "The linked guild setup pane has been detached from the legacy frame owner.",
        "Build the remaining controls here as a native Settings sub-pane instead of routing back to UI/MainFrame.lua.",
    })
end

function compat:RenderHelp()
    self:EnsureState()
    renderPlaceholderPane(self.viewHost, "Help", {
        "Selected article: " .. tostring(self.state.helpArticleKey or "overview"),
        "The legacy Help renderer has been removed from the runtime path.",
    })
end

function compat:RenderMaintenance()
    self:EnsureState()
    renderPlaceholderPane(self.viewHost, "Maintenance", {
        "Selected maintenance tab: " .. tostring(self.state.maintenanceTab or "cleanup"),
        "The Maintenance pane is now isolated from UI/MainFrame.lua and needs a page-native rebuild.",
    })
end

function compat:OpenHelpArticle(articleKey)
    self:EnsureState()
    self.state.helpArticleKey = tostring(articleKey or "overview")

    if addon.Router and addon.Router.Navigate then
        addon.Router:Navigate("help")
    else
        self:RenderView("help")
    end
end

function compat:OpenQuickLink(viewKey, maintenanceTab)
    if type(viewKey) ~= "string" or viewKey == "" then
        return
    end

    self:EnsureState()

    if viewKey == "maintenance" and maintenanceTab then
        self.state.maintenanceTab = maintenanceTab
    end

    if addon.Router and addon.Router.Navigate then
        addon.Router:Navigate(viewKey)
    else
        self:RenderView(viewKey)
    end

    self:ShowForCurrentView()
end

function compat:MarkBackgroundRefreshPending()
    self:EnsureState()
    self.state.pendingBackgroundRefresh = true
end

function compat:IsRaidBingoViewActive()
    return self.frame
        and self.frame.IsShown
        and self.frame:IsShown()
        and addon.Router
        and addon.Router.GetCurrentView
        and addon.Router:GetCurrentView() == "raidbingo"
end

function compat:RenderView(viewKey)
    local shell = ensureShell()
    if not shell then
        return
    end

    self:EnsureState()

    if viewKey == "bis" and addon.BISService and addon.BISService.SetSelectedToonId and addon.Store and addon.Store.GetPlayerName then
        addon.BISService:SetSelectedToonId(addon.Store:GetPlayerName())
    end

    local shellPage, settingsPane = resolveShellView(viewKey or (addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView()) or "dashboard")

    if shellPage == "settings" and addon.NewUISettings then
        addon.NewUISettings.State = addon.NewUISettings.State or {}
        addon.NewUISettings.State.activePane = settingsPane or addon.NewUISettings.State.activePane or "settings"
    end

    if shell.ShowPage then
        shell:ShowPage(shellPage)
    end

    self.frame = shell.frame
    self.contentHost = shell.contentHost
end

function compat:RefreshCurrentView(force)
    local shell = ensureShell()
    if not shell then
        return
    end

    if InCombatLockdown and InCombatLockdown() then
        if force then
            self.pendingRefreshAfterCombat = true
        end
        return
    end

    if not force and not (self.frame and self.frame.IsShown and self.frame:IsShown()) then
        return
    end

    if not force then
        if shell.isDragging then
            return
        end

        local currentView = addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() or "dashboard"
        local shellPage = resolveShellView(currentView)

        if shell.activePage ~= shellPage then
            return
        end

        self.state = self.state or {}
        self.state.pendingBackgroundRefresh = false
        return
    end

    self.state = self.state or {}
    self.state.pendingBackgroundRefresh = false
    self:RenderView(addon.Router and addon.Router.GetCurrentView and addon.Router:GetCurrentView() or "dashboard")
end

function compat:RegisterViews()
    if self._viewsRegistered or not addon.Router or not addon.Router.Register then
        return
    end

    self._viewsRegistered = true

    for _, viewKey in ipairs({
        "dashboard", "board", "roster", "mytoons", "mythics", "bis", "raidbingo", "gratsu", "celebrations", "groups", "settings", "sync", "setup", "help", "maintenance"
    }) do
        addon.Router:Register(viewKey, function()
            compat:RenderView(viewKey)
        end)
    end
end

function compat:Initialize()
    if InCombatLockdown and InCombatLockdown() then
        self.pendingInitializeAfterCombat = true
        self.pendingRefreshAfterCombat = true
        return
    end

    ensureShell()
    self:EnsureState()
    self:RegisterViews()
    self:InitializeGuildCleanupLaunchers()
end

function compat:ShowForCurrentView()
    if not self.frame then
        self:Initialize()
    end

    if not self.frame then
        self.pendingShowAfterCombat = true
        self.pendingRefreshAfterCombat = true
        return
    end

    if InCombatLockdown and InCombatLockdown() then
        self.pendingShowAfterCombat = true
        self.pendingRefreshAfterCombat = true
        return
    end

    self:RefreshCurrentView(true)
    self.frame:Show()
    self.frame:Raise()
end

function compat:HideAfterCombatSafe()
    if not self.frame then
        return false
    end

    if InCombatLockdown and InCombatLockdown() then
        self.pendingHideAfterCombat = true
        self.pendingShowAfterCombat = false
        self.pendingRefreshAfterCombat = false
        addon:Print("GH3 cannot close during combat. It will close after combat ends.")
        return false
    end

    self.pendingHideAfterCombat = false
    self.frame:Hide()
    return true
end

function compat:HandleDeferredProtectedUiAfterCombat()
    if InCombatLockdown and InCombatLockdown() then
        return
    end

    if self.pendingInitializeAfterCombat then
        self.pendingInitializeAfterCombat = false
        self:Initialize()
    end

    if self.pendingShowAfterCombat then
        self.pendingShowAfterCombat = false
        self:ShowForCurrentView()
        return
    end

    if self.pendingRefreshAfterCombat then
        self.pendingRefreshAfterCombat = false
        self:RefreshCurrentView(true)
    end

    if addon.NewUIBIS and addon.NewUIBIS._pendingDropReviewAfterCombat and addon.NewUIBIS.ShowDropReviewDialog then
        addon.NewUIBIS._pendingDropReviewAfterCombat = false
        addon.NewUIBIS:ShowDropReviewDialog()
    end

    if addon.NewUISettings and addon.NewUISettings._guildCleanupDialog and addon.NewUISettings._guildCleanupDialog.ProcessDeferredCombatActions then
        addon.NewUISettings._guildCleanupDialog:ProcessDeferredCombatActions()
    end

    if self.pendingHideAfterCombat then
        self.pendingHideAfterCombat = false

        if self.frame and self.frame.IsShown and self.frame:IsShown() then
            self.frame:Hide()
        end
    end

    if self.TryAttachGuildCleanupLaunchers then
        self:TryAttachGuildCleanupLaunchers()
    end
end

function compat:Toggle()
    if not self.frame then
        self:Initialize()
    end

    if not self.frame then
        self.pendingShowAfterCombat = true
        self.pendingRefreshAfterCombat = true
        return
    end

    if self.frame:IsShown() then
        self:HideAfterCombatSafe()
    else
        self:ShowForCurrentView()
    end
end

function compat:ShowBISLootHelperDialog()
    if addon.NewUIBIS then
        addon.NewUIBIS.State = addon.NewUIBIS.State or {}
        addon.NewUIBIS.State.showLootHelper = true
    end

    if addon.Router and addon.Router.Navigate then
        addon.Router:Navigate("bis")
    else
        self:RenderView("bis")
    end

    self:ShowForCurrentView()
end

function compat:ShowBISGearSelectorDialog(titleText, toonId)
    if addon.NewUIBIS and addon.NewUIBIS.OpenGearSelector then
        addon.NewUIBIS:OpenGearSelector(titleText, toonId)
    end

    if addon.Router and addon.Router.Navigate then
        addon.Router:Navigate("bis")
    else
        self:RenderView("bis")
    end

    self:ShowForCurrentView()
end

function compat:ShowGuildCleanupDialog(initialMode)
    if addon.NewUISettings and addon.NewUISettings.ShowGuildCleanupDialog then
        addon.NewUISettings:ShowGuildCleanupDialog(initialMode)
        return
    end

    self:EnsureState()
    self.state.maintenanceTab = initialMode == "audit" and "audit" or (initialMode == "invites" and "invites" or "cleanup")

    if addon.Router and addon.Router.Navigate then
        addon.Router:Navigate("maintenance")
    else
        self:RenderView("maintenance")
    end

    self:ShowForCurrentView()
end

function compat:TryAttachGuildCleanupLaunchers()
    local canUseCleanupLauncher = addon.MaintenanceService
        and ((addon.MaintenanceService.CanGuildRemoveMembers and addon.MaintenanceService:CanGuildRemoveMembers())
            or (addon.MaintenanceService.CanGuildPromoteMembers and addon.MaintenanceService:CanGuildPromoteMembers()))

    local function applyLauncherTooltip(button)
        if not button or button.GH3TooltipConfigured then
            return
        end

        button.GH3TooltipConfigured = true
        button:SetScript("OnEnter", function(control)
            GameTooltip:SetOwner(control, "ANCHOR_RIGHT")
            GameTooltip:SetText("GH3 Helper", 0.93, 0.84, 0.63, 1, true)

            if canUseCleanupLauncher then
                GameTooltip:AddLine("Open GH3 Helper for cleanup review, batch-kick prep, and batch-promotion prep.", 0.82, 0.82, 0.82, true)
            else
                GameTooltip:AddLine("Visible for discoverability, but disabled until this character can remove or promote guild members.", 0.82, 0.82, 0.82, true)
            end

            GameTooltip:Show()
        end)
        button:HookScript("OnLeave", function()
            GameTooltip:Hide()
        end)
    end

    local function updateLauncherState(button)
        if not button then
            return
        end

        if InCombatLockdown and InCombatLockdown() then
            button:Disable()
        elseif not canUseCleanupLauncher then
            button:Disable()
        else
            button:Enable()
        end

        button:Show()
    end

    local function launchHelper()
        if addon.MaintenanceService and addon.MaintenanceService.ScanRoster then
            addon.MaintenanceService:ScanRoster()
        end
        compat:ShowGuildCleanupDialog("promote")
    end

    local function configureCommunitiesTopTab()
        local communitiesFrame = _G.CommunitiesFrame
        if not communitiesFrame then
            return false
        end

        local anchor = communitiesFrame.CloseButton or communitiesFrame.closeButton or communitiesFrame
        local button = communitiesFrame.GH3HelperTopTab

        if not button then
            button = createHelperLauncher(communitiesFrame, "BOTTOMRIGHT", anchor, "TOPRIGHT", -58, 2, 110, 22, launchHelper)
            communitiesFrame.GH3HelperTopTab = button
        else
            button:ClearAllPoints()
            button:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", -58, 2)
            styleHelperLauncherButton(button)
        end

        button:SetFrameStrata(communitiesFrame:GetFrameStrata())
        button:SetFrameLevel(communitiesFrame:GetFrameLevel() + 40)
        applyLauncherTooltip(button)
        updateLauncherState(button)
        return true
    end

    local function configureCommunitiesTitleLauncher()
        local communitiesFrame = _G.CommunitiesFrame
        if not communitiesFrame then
            return false
        end

        local rosterFrame = communitiesFrame.TitleContainer or communitiesFrame.PortraitOverlay
        local anchorButton = communitiesFrame.MaximizeMinimizeFrame and communitiesFrame.MaximizeMinimizeFrame.MinimizeButton or nil

        if not rosterFrame or not anchorButton then
            return false
        end

        local button = rosterFrame.GH3CleanupButton

        if not button then
            button = createHelperLauncher(rosterFrame, "RIGHT", anchorButton, "LEFT", -14, 0, 98, 22, launchHelper)
            rosterFrame.GH3CleanupButton = button
        else
            button:ClearAllPoints()
            button:SetPoint("RIGHT", anchorButton, "LEFT", -14, 0)
            styleHelperLauncherButton(button)
        end

        button:SetFrameStrata(rosterFrame:GetFrameStrata())
        button:SetFrameLevel(anchorButton:GetFrameLevel() + 20)
        applyLauncherTooltip(button)
        updateLauncherState(button)
        return true
    end

    local function configureFooterLauncher(anchorButton, key)
        if not anchorButton or not anchorButton.GetParent then
            return false
        end

        local parent = anchorButton:GetParent()
        if not parent then
            return false
        end

        local button = parent[key]

        if not button then
            button = createHelperLauncher(parent, "BOTTOMRIGHT", anchorButton, "TOPRIGHT", 0, 6, 98, 22, launchHelper)
            parent[key] = button
        else
            button:ClearAllPoints()
            button:SetPoint("BOTTOMRIGHT", anchorButton, "TOPRIGHT", 0, 6)
            styleHelperLauncherButton(button)
        end

        button:SetFrameStrata(anchorButton:GetFrameStrata())
        button:SetFrameLevel(anchorButton:GetFrameLevel() + 20)
        applyLauncherTooltip(button)
        updateLauncherState(button)
        return true
    end

    local function configureLauncher(parent, key)
        if not parent then
            return
        end

        local button = parent[key]
        local anchor = parent.CloseButton or parent.closeButton or parent
        local anchorPoint = (anchor == parent) and "TOPRIGHT" or "TOPLEFT"
        local relativePoint = (anchor == parent) and "TOPRIGHT" or "TOPRIGHT"
        local offsetX = (anchor == parent) and -18 or -116
        local offsetY = (anchor == parent) and -18 or 0

        if parent == _G.GuildFrame then
            local inviteButton = _G.GuildInviteButton
            local recruitmentButton = _G.GuildRecruitmentButton

            if inviteButton then
                anchor = inviteButton
                anchorPoint = "BOTTOMRIGHT"
                relativePoint = "TOPRIGHT"
                offsetX = 0
                offsetY = 6
            elseif recruitmentButton then
                anchor = recruitmentButton
                anchorPoint = "BOTTOMLEFT"
                relativePoint = "TOPLEFT"
                offsetX = 0
                offsetY = 6
            end
        end

        if not button then
            button = createHelperLauncher(parent, anchorPoint, anchor, relativePoint, offsetX, offsetY, 98, 22, launchHelper)
            parent[key] = button
        else
            button:ClearAllPoints()
            button:SetPoint(anchorPoint, anchor, relativePoint, offsetX, offsetY)
            styleHelperLauncherButton(button)
        end

        button:SetFrameStrata(parent:GetFrameStrata())
        button:SetFrameLevel(parent:GetFrameLevel() + 20)
        applyLauncherTooltip(button)
        updateLauncherState(button)
    end

    if configureCommunitiesTopTab() then
        return
    end

    if configureCommunitiesTitleLauncher() then
        return
    end

    if configureFooterLauncher(_G.GuildInviteButton, "GH3CleanupFooterButton") then
        return
    end

    if configureFooterLauncher(_G.GuildRecruitmentButton, "GH3CleanupFooterButton") then
        return
    end

    configureLauncher(_G.CommunitiesFrame, "GH3CleanupButton")
    configureLauncher(_G.GuildFrame, "GH3CleanupButton")
end

function compat:InitializeGuildCleanupLaunchers()
    if self.guildCleanupLauncherController then
        self:TryAttachGuildCleanupLaunchers()
        return
    end

    local controller = CreateFrame("Frame")
    controller:RegisterEvent("PLAYER_LOGIN")
    controller:RegisterEvent("PLAYER_REGEN_ENABLED")
    controller:RegisterEvent("ADDON_LOADED")
    controller:SetScript("OnEvent", function(_, event, addonName)
        if event == "PLAYER_REGEN_ENABLED" then
            compat:HandleDeferredProtectedUiAfterCombat()
            compat:TryAttachGuildCleanupLaunchers()
            return
        end

        if event == "ADDON_LOADED" and addonName ~= "Blizzard_Communities" and addonName ~= "Blizzard_GuildUI" then
            return
        end

        compat:TryAttachGuildCleanupLaunchers()
    end)

    self.guildCleanupLauncherController = controller
    self:TryAttachGuildCleanupLaunchers()
end

function compat:ShowMinimapQuickLinkMenu(anchorButton)
    if not anchorButton then
        return
    end

    local menu = ensureMinimapQuickLinkMenuFrame()
    local menuItems = {}

    for _, option in ipairs(buildMinimapQuickLinkOptions()) do
        if option.visible == nil or option.visible == true then
            menuItems[#menuItems + 1] = option
        end
    end

    local width = 180

    for index, option in ipairs(menuItems) do
        local button = menu.buttons[index]
        if not button then
            button = CreateFrame("Button", nil, menu, "BackdropTemplate")
            button:SetHeight(22)
            if button.SetBackdrop then
                button:SetBackdrop({
                    bgFile = "Interface\\Buttons\\WHITE8X8",
                    edgeFile = "Interface\\Buttons\\WHITE8X8",
                    edgeSize = 1,
                    insets = { left = 1, right = 1, top = 1, bottom = 1 },
                })
            end
            button.Text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            button.Text:SetPoint("LEFT", button, "LEFT", 8, 0)
            button.Text:SetJustifyH("LEFT")
            menu.buttons[index] = button
        end

        button:ClearAllPoints()
        button:SetPoint("TOPLEFT", menu, "TOPLEFT", 8, -8 - ((index - 1) * 24))
        button:SetSize(width, 22)
        button:SetBackdropColor(option.checked and 0.18 or 0.08, option.checked and 0.14 or 0.08, option.checked and 0.06 or 0.10, 0.98)
        button:SetBackdropBorderColor(option.checked and 0.74 or 0.28, option.checked and 0.58 or 0.28, option.checked and 0.22 or 0.16, 1)
        button.Text:SetText(option.text)
        button.Text:SetTextColor(option.checked and 1 or 0.9, option.checked and 0.86 or 0.82, option.checked and 0.62 or 0.68, 1)
        button:SetScript("OnClick", function()
            hideMinimapQuickLinkMenu()
            if type(option.onClick) == "function" then
                option.onClick()
            end
        end)
        button:Show()
    end

    for index = #menuItems + 1, #(menu.buttons or {}) do
        menu.buttons[index]:Hide()
    end

    menu:SetSize(width + 16, (#menuItems * 24) + 16)
    menu:ClearAllPoints()
    menu:SetPoint("TOPLEFT", anchorButton, "BOTTOMLEFT", 0, -4)
    menu:Show()
end