local addonName, L = ...

local AceGUI = LibStub("AceGUI-3.0")

L.settings = L.settings or {
    previewVisibilityTicker = nil,
}
L.settings.pi = L.settings.pi or {}

L.tabSettings = L.tabSettings or {
    canvas = nil,
    category = nil,
    currentRoot = nil,
    pendingFocusSpellIndex = nil,
    pendingScrollValue = nil,
    root = nil,
    tabGroup = nil,
    selectedTab = nil,
    widgets = {},
    selectingTab = false,
}

local tabSettings = L.tabSettings
L.tabSettingsUI = L.tabSettingsUI or {}
local ui = L.tabSettingsUI
local DEFAULT_TAB_ID = "general"
local SETTINGS_UI_LISTENER_KEY = "tabbed_settings_ui"
local TRACK_PREVIEW_CONTEXT = { piGiverName = "Priest" }
local REQUEST_PREVIEW_CONTEXT = { requesterName = "Requester" }
local RESET_ALL_POPUP_KEY = "LUSTOMIZER_CONFIRM_RESET_ALL"
local OVERVIEW_LOGO_SOURCE_WIDTH = 1045
local OVERVIEW_LOGO_SOURCE_HEIGHT = 716
local OVERVIEW_LOGO_DISPLAY_HEIGHT = 120
local OVERVIEW_LOGO_DISPLAY_WIDTH = math.floor((OVERVIEW_LOGO_SOURCE_WIDTH / OVERVIEW_LOGO_SOURCE_HEIGHT) * OVERVIEW_LOGO_DISPLAY_HEIGHT + 0.5)
local RefreshCurrentTab
local RefreshTabsAndCurrentView
local SelectTab
local EnsureSettingsCloseHooks

tabSettings.selectedTab = tabSettings.selectedTab or DEFAULT_TAB_ID

if L.RegisterSettingsChangeListener then
    L:RegisterSettingsChangeListener(SETTINGS_UI_LISTENER_KEY, function(_reason, _profileKey)
        if L.RefreshTabbedSettingsUIState then
            L:RefreshTabbedSettingsUIState()
        end
    end)
end

if StaticPopupDialogs and not StaticPopupDialogs[RESET_ALL_POPUP_KEY] then
    StaticPopupDialogs[RESET_ALL_POPUP_KEY] = {
        text = "Reset all Lustomizer settings to their defaults?\n\nThis will overwrite your current configuration.",
        button1 = YES,
        button2 = CANCEL,
        OnAccept = function()
            if L.ResetAllSettingsToDefaults then
                L:ResetAllSettingsToDefaults()
            end
            if L.RefreshTabbedSettingsUIState then
                L:RefreshTabbedSettingsUIState()
            end
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = STATICPOPUP_NUMDIALOGS,
    }
end

function L:BuildSoundListForAce()
    local list = {}
    local order = {}

    for _, entry in ipairs(self.availableSounds) do
        local uid = self:MakeUniqueID(entry.source, entry.key)
        list[uid] = entry.name
        order[#order + 1] = uid
    end

    return list, order
end

function L:BuildVoiceListForAce()
    local list = {}

    for _, voice in ipairs(self.availableVoices) do
        list[tostring(voice.id)] = voice.name
    end

    return list
end

function L:BuildImageListForAce()
    local list = {}

    for _, entry in ipairs(self.availableImages) do
        local label = entry.name
        if self.IsAnimatedImageEntry and self:IsAnimatedImageEntry(entry) then
            label = label .. " (Animated)"
        end
        list[entry.key] = label
    end

    return list
end

function L:BuildFontListForAce()
    local list = {}

    for _, entry in ipairs(self.availableFonts) do
        list[entry.key] = entry.name
    end

    return list
end

function L:BuildDirectionListForAce()
    return {
        bottom = "From Bottom",
        top = "From Top",
        left = "From Left",
        right = "From Right",
    }
end

function L:BuildEasingListForAce()
    return {
        linear = "Linear",
        ["in"] = "Ease In",
        out = "Ease Out",
        inout = "Ease In/Out",
    }
end

function L:BuildSoundChannelListForAce()
    return {
        Master = "Master",
        SFX = "SFX",
        Dialog = "Dialog",
        Music = "Music",
        Ambience = "Ambience",
    }
end

function L:BuildSpinDirectionListForAce()
    return {
        cw = "Clockwise",
        ccw = "Counter-clockwise",
    }
end

function L:IsSettingsCanvasActive(canvas)
    if canvas and canvas.IsVisible and canvas:IsVisible() then
        if SettingsPanel and SettingsPanel.IsVisible then
            return SettingsPanel:IsVisible()
        end

        return true
    end

    if self.IsTabbedSettingsActive and self:IsTabbedSettingsActive() then
        return true
    end

    return false
end

function L:IsAnySettingsCanvasVisible()
    return self:IsSettingsCanvasActive(nil)
end

function L:StopImagePreviewIfSettingsHidden()
    if not self.IsOverlayPreviewActive or not self:IsOverlayPreviewActive() then
        return
    end

    if self:IsAnySettingsCanvasVisible() then
        return
    end

    self:StopOverlayPreview()
end

function L:StopTextPreviewIfSettingsHidden()
    if not self.IsTextOverlayPreviewActive or not self:IsTextOverlayPreviewActive() then
        return
    end

    if self:IsAnySettingsCanvasVisible() then
        return
    end

    self:StopTextOverlayPreview()
end

function L:StopTextEditModeIfSettingsHidden(profileKey)
    if not self.IsTextEditModeEnabled or not self:IsTextEditModeEnabled(profileKey) then
        return
    end

    C_Timer.After(0, function()
        if self:IsAnySettingsCanvasVisible() then
            return
        end

        self:SetTextEditMode(false, profileKey)
    end)
end

function L:StopImageEditModeIfSettingsHidden(profileKey)
    if not self.IsEditModeEnabled or not self:IsEditModeEnabled(profileKey) then
        return
    end

    C_Timer.After(0, function()
        if self:IsAnySettingsCanvasVisible() then
            return
        end

        self:SetEditMode(false, profileKey)
    end)
end

function L:StopAllSettingsTransientState()
    if self.StopSlashAlertTests then
        self:StopSlashAlertTests()
    end
    if self.StopOverlayPreview then
        self:StopOverlayPreview()
    end
    if self.StopTextOverlayPreview then
        self:StopTextOverlayPreview()
    end
    if self.StopPreviewAudioIfPossible then
        self:StopPreviewAudioIfPossible()
    end
    if self.SetEditMode then
        self:SetEditMode(false, "bloodlust")
        self:SetEditMode(false, "piTrack")
    end
    if self.SetTextEditMode then
        self:SetTextEditMode(false, "bloodlust")
        self:SetTextEditMode(false, "piTrack")
        self:SetTextEditMode(false, "piRequest")
    end
end

function L:StopPreviewVisibilityMonitor()
    if self.settings.previewVisibilityTicker then
        self.settings.previewVisibilityTicker:Cancel()
        self.settings.previewVisibilityTicker = nil
    end
end

EnsureSettingsCloseHooks = function()
    if tabSettings.closeHooksInstalled then
        return
    end

    if not SettingsPanel then
        return
    end

    SettingsPanel:HookScript("OnHide", function()
        L:StopAllSettingsTransientState()
    end)

    local function HookCloseButton(button)
        if not button or not button.HookScript then
            return false
        end

        button:HookScript("OnClick", function()
            L:StopAllSettingsTransientState()
        end)
        return true
    end

    HookCloseButton(SettingsPanel.ClosePanelButton)
    HookCloseButton(SettingsPanel.CloseButton)

    if SettingsPanel.Container and SettingsPanel.Container.HookScript then
        SettingsPanel.Container:HookScript("OnHide", function()
            L:StopAllSettingsTransientState()
        end)
    end

    if SettingsPanel.Container and SettingsPanel.Container.CloseButton then
        HookCloseButton(SettingsPanel.Container.CloseButton)
    end

    tabSettings.closeHooksInstalled = true
end

function L:EnsurePreviewVisibilityMonitor()
    if self.settings.previewVisibilityTicker then
        return
    end

    -- Settings canvases can be closed via Blizzard UI flows that do not notify
    -- every child widget. Poll lightly so previews and edit modes never get stranded.
    self.settings.previewVisibilityTicker = C_Timer.NewTicker(0.1, function()
        local imagePreviewActive = self.IsOverlayPreviewActive and self:IsOverlayPreviewActive()
        local textPreviewActive = self.IsTextOverlayPreviewActive and self:IsTextOverlayPreviewActive()
        local imageEditActive = self.IsEditModeEnabled and self:IsEditModeEnabled()
        local textEditActive = self.IsTextEditModeEnabled and self:IsTextEditModeEnabled()

        if not imagePreviewActive and not textPreviewActive and not imageEditActive and not textEditActive then
            self:StopPreviewVisibilityMonitor()
            return
        end

        if not self:IsAnySettingsCanvasVisible() then
            if imagePreviewActive then
                self:StopOverlayPreview()
            end
            if textPreviewActive then
                self:StopTextOverlayPreview()
            end
            if imageEditActive then
                self:SetEditMode(false)
            end
            if textEditActive then
                self:SetTextEditMode(false)
            end
        end

        if self.settings.previewVisibilityTicker then
            local imageStillActive = self.IsOverlayPreviewActive and self:IsOverlayPreviewActive()
            local textStillActive = self.IsTextOverlayPreviewActive and self:IsTextOverlayPreviewActive()
            local imageEditStillActive = self.IsEditModeEnabled and self:IsEditModeEnabled()
            local textEditStillActive = self.IsTextEditModeEnabled and self:IsTextEditModeEnabled()
            if not imageStillActive and not textStillActive and not imageEditStillActive and not textEditStillActive then
                self:StopPreviewVisibilityMonitor()
            end
        end
    end)
end

local function CreateScrollRoot(parent)
    local root = AceGUI:Create("ScrollFrame")
    root:SetLayout("List")
    return root
end

local function SetWidgetFullWidth(widget)
    if widget and widget.SetFullWidth then
        widget:SetFullWidth(true)
    end
end

local function DisableMouseDown(widget)
    if widget and widget.frame then
        widget.frame:SetScript("OnMouseDown", nil)
        widget.frame:SetScript("OnMouseUp", nil)
    end
end

local function AddHeading(root, title, intro)
    local heading = AceGUI:Create("Heading")
    heading:SetText(title)
    heading:SetFullWidth(true)
    root:AddChild(heading)

    if intro then
        local label = AceGUI:Create("Label")
        label:SetText(intro)
        label:SetFullWidth(true)
        root:AddChild(label)
    end
end

local function AddOverviewLogo(root)
    local logo = AceGUI:Create("Icon")
    logo:SetFullWidth(true)
    logo:SetImage((L.GetOverviewLogoTexture and L:GetOverviewLogoTexture()) or "Interface\\AddOns\\Lustomizer\\assets\\overview-logo")
    logo:SetImageSize(OVERVIEW_LOGO_DISPLAY_WIDTH, OVERVIEW_LOGO_DISPLAY_HEIGHT)
    if logo.frame and logo.frame.EnableMouse then
        logo.frame:EnableMouse(false)
    end
    root:AddChild(logo)
end

local function GetField(profileKey, field)
    return L:GetProfileDBValue(profileKey, field)
end

local function SetField(profileKey, field, value)
    L:SetProfileDBValue(profileKey, field, value)
end

local function IsBLActive()
    return L:GetDB().enableBloodlustAlerts ~= false
end

local function IsPIActive()
    return L:GetDB().enableExperimentalPI == true
end

local function IsProfileModuleEnabled(profileKey)
    if profileKey == "bloodlust" then
        return IsBLActive()
    end

    if profileKey == "piTrack" or profileKey == "piRequest" then
        return IsPIActive()
    end

    return true
end

local function EnsureSelectedSound(profileKey)
    return L:EnsureValidSelectedSound(profileKey)
end

local function EnsureSelectedVoice(profileKey)
    return L:EnsureValidSelectedVoice(profileKey)
end

local function EnsureSelectedImage(profileKey)
    return L:EnsureValidSelectedImage(profileKey)
end

local function EnsureSelectedFont(profileKey)
    return L:EnsureValidSelectedFont(profileKey)
end

ui.trackPreviewContext = TRACK_PREVIEW_CONTEXT
ui.requestPreviewContext = REQUEST_PREVIEW_CONTEXT
ui.AddHeading = AddHeading
ui.GetField = GetField
ui.SetField = SetField
ui.SetWidgetFullWidth = SetWidgetFullWidth
ui.DisableMouseDown = DisableMouseDown
ui.EnsureSelectedSound = EnsureSelectedSound
ui.EnsureSelectedVoice = EnsureSelectedVoice
ui.EnsureSelectedImage = EnsureSelectedImage
ui.EnsureSelectedFont = EnsureSelectedFont
ui.IsProfileModuleEnabled = IsProfileModuleEnabled

local function RefreshGeneralWidgets(widgets)
    widgets.blEnabled:SetValue(IsBLActive())
    widgets.piEnabled:SetValue(IsPIActive())
    widgets.minimapIconEnabled:SetValue(not (L.IsMinimapIconHidden and L:IsMinimapIconHidden()))
    if widgets.bloodlustTestButton then
        widgets.bloodlustTestButton:SetText((L.IsSlashAlertTestActive and L:IsSlashAlertTestActive("bloodlust")) and "Stop Bloodlust Test" or "Start Bloodlust Test")
        widgets.bloodlustTestButton:SetDisabled(not IsBLActive())
    end
    if widgets.piTrackTestButton then
        widgets.piTrackTestButton:SetText((L.IsSlashAlertTestActive and L:IsSlashAlertTestActive("piTrack")) and "Stop PI Receiver Test" or "Start PI Receiver Test")
        widgets.piTrackTestButton:SetDisabled(not IsPIActive())
    end
    if widgets.piRequestTestButton then
        widgets.piRequestTestButton:SetText((L.IsSlashAlertTestActive and L:IsSlashAlertTestActive("piRequest")) and "Stop PI Request Test" or "Start PI Request Test")
        widgets.piRequestTestButton:SetDisabled(not IsPIActive())
    end
    if widgets.testStatusLabel then
        local activeProfileKey = L.activeSlashAlertTestProfileKey
        local statusText = "No test running"
        if activeProfileKey == "bloodlust" then
            statusText = "Running: Bloodlust test"
        elseif activeProfileKey == "piTrack" then
            statusText = "Running: PI receiver test"
        elseif activeProfileKey == "piRequest" then
            statusText = "Running: PI request test"
        end
        widgets.testStatusLabel:SetText("Test status: " .. statusText)
    end
end

local function DeactivateProfileUIState(profileKey)
    if L.IsOverlayPreviewActive and L:IsOverlayPreviewActive(profileKey) and L.StopOverlayPreview then
        L:StopOverlayPreview(profileKey)
    end
    if L.IsTextOverlayPreviewActive and L:IsTextOverlayPreviewActive(profileKey) and L.StopTextOverlayPreview then
        L:StopTextOverlayPreview(profileKey)
    end
    if L.IsOverlayShown and L:IsOverlayShown(profileKey) and L.HideOverlay then
        L:HideOverlay(profileKey)
    end
    if L.IsTextOverlayShown and L:IsTextOverlayShown(profileKey) and L.HideTextOverlay then
        L:HideTextOverlay(profileKey)
    end
    if L.IsEditModeEnabled and L:IsEditModeEnabled(profileKey) then
        L:SetEditMode(false, profileKey)
    end
    if L.IsTextEditModeEnabled and L:IsTextEditModeEnabled(profileKey) then
        L:SetTextEditMode(false, profileKey)
    end
end

local function ScheduleTabbedSettingsRefresh()
    C_Timer.After(0, function()
        if L.RefreshTabbedSettingsUIState then
            L:RefreshTabbedSettingsUIState()
        end
    end)
end

local function ToggleOverviewTest(profileKey, context)
    if L.IsSlashAlertTestActive and L:IsSlashAlertTestActive(profileKey) then
        if L.StopSlashAlertTests then
            L:StopSlashAlertTests()
        end
    else
        if L.TriggerSlashAlertTest then
            L:TriggerSlashAlertTest(profileKey, context)
        end
    end

    ScheduleTabbedSettingsRefresh()
end

local function BuildGeneralTab(root)
    local widgets = {}
    AddOverviewLogo(root)
    AddHeading(root, "Lustomizer", "Choose which alerts you want to use and which settings pages should be available.")

    local group = AceGUI:Create("InlineGroup")
    group:SetTitle("Alert Modules")
    group:SetFullWidth(true)
    group:SetLayout("List")
    root:AddChild(group)

    local blEnabled = AceGUI:Create("CheckBox")
    blEnabled:SetLabel("Enable Bloodlust / Heroism alerts")
    SetWidgetFullWidth(blEnabled)
    blEnabled:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().enableBloodlustAlerts = value ~= false
        if value == false then
            DeactivateProfileUIState("bloodlust")
        end
        ScheduleTabbedSettingsRefresh()
    end)
    group:AddChild(blEnabled)

    local piEnabled = AceGUI:Create("CheckBox")
    piEnabled:SetLabel("Enable Power Infusion alerts (experimental)")
    SetWidgetFullWidth(piEnabled)
    piEnabled:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().enableExperimentalPI = value == true
        if value ~= true then
            L:GetDB().debugExperimentalPI = false
            DeactivateProfileUIState("piTrack")
            DeactivateProfileUIState("piRequest")
        end
        ScheduleTabbedSettingsRefresh()
    end)
    group:AddChild(piEnabled)

    local minimapIconEnabled = AceGUI:Create("CheckBox")
    minimapIconEnabled:SetLabel("Show minimap button")
    SetWidgetFullWidth(minimapIconEnabled)
    minimapIconEnabled:SetCallback("OnValueChanged", function(_, _, value)
        if L.SetMinimapIconHidden then
            L:SetMinimapIconHidden(value ~= true)
        end
        ScheduleTabbedSettingsRefresh()
    end)
    group:AddChild(minimapIconEnabled)

    local testGroup = AceGUI:Create("InlineGroup")
    testGroup:SetTitle("Quick Tests")
    testGroup:SetFullWidth(true)
    testGroup:SetLayout("List")
    root:AddChild(testGroup)

    local testIntro = AceGUI:Create("Label")
    testIntro:SetText("Run alert tests directly from the overview. Clicking the active test button again stops that test.")
    testIntro:SetFullWidth(true)
    testGroup:AddChild(testIntro)

    local testButtonRow = AceGUI:Create("SimpleGroup")
    testButtonRow:SetFullWidth(true)
    testButtonRow:SetLayout("Flow")
    testGroup:AddChild(testButtonRow)

    local bloodlustTestButton = AceGUI:Create("Button")
    bloodlustTestButton:SetWidth(170)
    bloodlustTestButton:SetCallback("OnClick", function()
        ToggleOverviewTest("bloodlust", nil)
    end)
    testButtonRow:AddChild(bloodlustTestButton)
    DisableMouseDown(bloodlustTestButton)

    local piTrackTestButton = AceGUI:Create("Button")
    piTrackTestButton:SetWidth(170)
    piTrackTestButton:SetCallback("OnClick", function()
        ToggleOverviewTest("piTrack", TRACK_PREVIEW_CONTEXT)
    end)
    testButtonRow:AddChild(piTrackTestButton)
    DisableMouseDown(piTrackTestButton)

    local piRequestTestButton = AceGUI:Create("Button")
    piRequestTestButton:SetWidth(170)
    piRequestTestButton:SetCallback("OnClick", function()
        ToggleOverviewTest("piRequest", REQUEST_PREVIEW_CONTEXT)
    end)
    testButtonRow:AddChild(piRequestTestButton)
    DisableMouseDown(piRequestTestButton)

    local testStatusLabel = AceGUI:Create("Label")
    testStatusLabel:SetFullWidth(true)
    testGroup:AddChild(testStatusLabel)

    local dangerGroup = AceGUI:Create("InlineGroup")
    dangerGroup:SetTitle("Danger Zone")
    dangerGroup:SetFullWidth(true)
    dangerGroup:SetLayout("List")
    root:AddChild(dangerGroup)

    local dangerLabel = AceGUI:Create("Label")
    dangerLabel:SetText("Reset all saved settings and restore every Lustomizer option to its default value.")
    dangerLabel:SetFullWidth(true)
    dangerGroup:AddChild(dangerLabel)

    local resetButton = AceGUI:Create("Button")
    resetButton:SetText("|cffff4040Reset All Settings|r")
    resetButton:SetWidth(220)
    resetButton:SetCallback("OnClick", function()
        if StaticPopup_Show then
            StaticPopup_Show(RESET_ALL_POPUP_KEY)
        elseif L.ResetAllSettingsToDefaults then
            L:ResetAllSettingsToDefaults()
            if L.RefreshTabbedSettingsUIState then
                L:RefreshTabbedSettingsUIState()
            end
        end
    end)
    dangerGroup:AddChild(resetButton)
    DisableMouseDown(resetButton)

    widgets.blEnabled = blEnabled
    widgets.piEnabled = piEnabled
    widgets.minimapIconEnabled = minimapIconEnabled
    widgets.bloodlustTestButton = bloodlustTestButton
    widgets.piTrackTestButton = piTrackTestButton
    widgets.piRequestTestButton = piRequestTestButton
    widgets.testStatusLabel = testStatusLabel
    widgets.resetButton = resetButton
    return widgets
end

local function RebuildSelectedTab()
    if not tabSettings.selectedTab then
        return
    end

    local currentRoot = tabSettings.currentRoot
    if currentRoot then
        tabSettings.pendingScrollValue = (currentRoot.status and currentRoot.status.scrollvalue)
            or (currentRoot.localstatus and currentRoot.localstatus.scrollvalue)
            or 0
    else
        tabSettings.pendingScrollValue = 0
    end

    SelectTab(tabSettings.selectedTab)
end

local function RebuildSelectedTabForNewSpell(spellIndex)
    tabSettings.pendingScrollValue = 1000
    tabSettings.pendingFocusSpellIndex = spellIndex
    SelectTab(tabSettings.selectedTab)
end

local function GetResolvedSpellName(spellID)
    local id = math.floor(tonumber(spellID) or 0)
    if id <= 0 then
        return "Enter a spell ID"
    end

    local info = C_Spell and C_Spell.GetSpellInfo and C_Spell.GetSpellInfo(id)
    local name = info and info.name
    if name and name ~= "" then
        return name
    end

    return "Spell unknown"
end

local function BuildPISettingsTab(root)
    local widgets = {
        spellRows = {},
    }

    AddHeading(root, "Power Infusion Settings", "Configure who can request PI from a priest, which trigger methods are accepted, and which spells can auto-request PI.")

    local experimentalNotice = AceGUI:Create("Label")
    experimentalNotice:SetText("Power Infusion requests are still experimental. Some behaviors may still be buggy or not work correctly in every situation.")
    experimentalNotice:SetFullWidth(true)
    root:AddChild(experimentalNotice)

    local requestersGroup = AceGUI:Create("InlineGroup")
    requestersGroup:SetTitle("Who Can Request")
    requestersGroup:SetFullWidth(true)
    requestersGroup:SetLayout("List")
    root:AddChild(requestersGroup)

    local allowFocus = AceGUI:Create("CheckBox")
    allowFocus:SetLabel("Allow @focus to request PI")
    SetWidgetFullWidth(allowFocus)
    allowFocus:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestAllowFocus = value == true
        L:RefreshTabbedSettingsUIState()
    end)
    requestersGroup:AddChild(allowFocus)

    local requesterName = AceGUI:Create("EditBox")
    requesterName:SetLabel("Allow this player name")
    requesterName:SetFullWidth(true)
    requesterName:DisableButton(true)
    requesterName:SetCallback("OnEnterPressed", function(_, _, value)
        L:GetDB().piRequestAllowedPlayerName = L:Trim(value)
        L:RefreshTabbedSettingsUIState()
    end)
    requestersGroup:AddChild(requesterName)

    local requesterHint = AceGUI:Create("Label")
    requesterHint:SetText("Use `Player-Server` if you want to pin requests to one specific character. Leave empty to only use @focus.")
    requesterHint:SetFullWidth(true)
    requestersGroup:AddChild(requesterHint)

    local triggerGroup = AceGUI:Create("InlineGroup")
    triggerGroup:SetTitle("React To")
    triggerGroup:SetFullWidth(true)
    triggerGroup:SetLayout("List")
    root:AddChild(triggerGroup)

    local acceptSpellCasts = AceGUI:Create("CheckBox")
    acceptSpellCasts:SetLabel("Spell casts")
    SetWidgetFullWidth(acceptSpellCasts)
    acceptSpellCasts:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestAcceptSpellCasts = value ~= false
        L:RefreshTabbedSettingsUIState()
    end)
    triggerGroup:AddChild(acceptSpellCasts)

    local acceptWhispers = AceGUI:Create("CheckBox")
    acceptWhispers:SetLabel("Whispers")
    SetWidgetFullWidth(acceptWhispers)
    acceptWhispers:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestAcceptWhispers = value == true
        L:RefreshTabbedSettingsUIState()
    end)
    triggerGroup:AddChild(acceptWhispers)

    local whisperPhrase = AceGUI:Create("EditBox")
    whisperPhrase:SetLabel("Whisper phrase(s)")
    whisperPhrase:SetFullWidth(true)
    whisperPhrase:DisableButton(true)
    whisperPhrase:SetCallback("OnEnterPressed", function(_, _, value)
        local trimmed = L:Trim(value)
        if trimmed == "" then
            trimmed = L.DEFAULT_PI_WHISPER_PHRASE
        end
        L:GetDB().piRequestWhisperPhrase = trimmed
        L:RefreshTabbedSettingsUIState()
    end)
    triggerGroup:AddChild(whisperPhrase)

    local whisperHint = AceGUI:Create("Label")
    whisperHint:SetText("Separate multiple phrases with `;`. Matching is case-insensitive, and whispers still only count from configured requesters in your current group.")
    whisperHint:SetFullWidth(true)
    triggerGroup:AddChild(whisperHint)

    local behaviorGroup = AceGUI:Create("InlineGroup")
    behaviorGroup:SetTitle("Request Behavior")
    behaviorGroup:SetFullWidth(true)
    behaviorGroup:SetLayout("List")
    root:AddChild(behaviorGroup)

    local piDebug = AceGUI:Create("CheckBox")
    piDebug:SetLabel("Show Power Infusion debug messages")
    SetWidgetFullWidth(piDebug)
    piDebug:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().debugExperimentalPI = value == true
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(piDebug)

    local piRespectCooldown = AceGUI:Create("CheckBox")
    piRespectCooldown:SetLabel("Hide incoming PI requests while Power Infusion is on cooldown")
    SetWidgetFullWidth(piRespectCooldown)
    piRespectCooldown:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestRespectCooldown = value ~= false
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(piRespectCooldown)

    local piGraceEnabled = AceGUI:Create("CheckBox")
    piGraceEnabled:SetLabel("Allow PI requests shortly before cooldown is ready")
    SetWidgetFullWidth(piGraceEnabled)
    piGraceEnabled:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestGracePeriodEnabled = value == true
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(piGraceEnabled)

    local piGraceSeconds = AceGUI:Create("EditBox")
    piGraceSeconds:SetLabel("Grace period (seconds)")
    piGraceSeconds:SetFullWidth(true)
    piGraceSeconds:DisableButton(true)
    piGraceSeconds:SetCallback("OnEnterPressed", function(_, _, value)
        L:GetDB().piRequestGracePeriodSeconds = L:ParsePositiveNumber(value, tonumber(L:GetDB().piRequestGracePeriodSeconds) or 5)
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(piGraceSeconds)

    local piRequestTimeout = AceGUI:Create("EditBox")
    piRequestTimeout:SetLabel("Expire unanswered requests after (seconds)")
    piRequestTimeout:SetFullWidth(true)
    piRequestTimeout:DisableButton(true)
    piRequestTimeout:SetCallback("OnEnterPressed", function(_, _, value)
        L:GetDB().piRequestTimeoutSeconds = L:ParsePositiveNumber(value, tonumber(L:GetDB().piRequestTimeoutSeconds) or 8)
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(piRequestTimeout)

    local cooldownReplyEnabled = AceGUI:Create("CheckBox")
    cooldownReplyEnabled:SetLabel("Whisper requester when PI is still on cooldown")
    SetWidgetFullWidth(cooldownReplyEnabled)
    cooldownReplyEnabled:SetCallback("OnValueChanged", function(_, _, value)
        L:GetDB().piRequestCooldownReplyEnabled = value == true
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(cooldownReplyEnabled)

    local cooldownReplyText = AceGUI:Create("EditBox")
    cooldownReplyText:SetLabel("Cooldown reply text")
    cooldownReplyText:SetFullWidth(true)
    cooldownReplyText:DisableButton(true)
    cooldownReplyText:SetCallback("OnEnterPressed", function(_, _, value)
        local trimmed = L:Trim(value)
        if trimmed == "" then
            trimmed = "Power Infusion ready in {time_left}s."
        end
        L:GetDB().piRequestCooldownReplyText = trimmed
        L:RefreshTabbedSettingsUIState()
    end)
    behaviorGroup:AddChild(cooldownReplyText)

    local cooldownReplyHint = AceGUI:Create("Label")
    cooldownReplyHint:SetText("Use `{time_left}` as a wildcard for the remaining cooldown in seconds.")
    cooldownReplyHint:SetFullWidth(true)
    behaviorGroup:AddChild(cooldownReplyHint)

    local spellsGroup = AceGUI:Create("InlineGroup")
    spellsGroup:SetTitle("Spell Cast Auto-Request List")
    spellsGroup:SetFullWidth(true)
    spellsGroup:SetLayout("List")
    root:AddChild(spellsGroup)

    local spellsIntro = AceGUI:Create("Label")
    spellsIntro:SetText("Enabled spell IDs will auto-send a PI request when your character successfully casts them. You can disable individual entries, add your own IDs, delete rows, or reset the whole list to the current defaults.")
    spellsIntro:SetFullWidth(true)
    spellsGroup:AddChild(spellsIntro)

    local spellButtons = AceGUI:Create("SimpleGroup")
    spellButtons:SetFullWidth(true)
    spellButtons:SetLayout("Flow")
    spellsGroup:AddChild(spellButtons)

    local addSpellButton = AceGUI:Create("Button")
    addSpellButton:SetText("Add spell")
    addSpellButton:SetWidth(140)
    addSpellButton:SetCallback("OnClick", function()
        local entries = L:GetPIRequestSpellEntries()
        entries[#entries + 1] = {
            spellID = 0,
            enabled = true,
        }
        L:SetPIRequestSpellEntries(entries)
        RebuildSelectedTabForNewSpell(#entries)
    end)
    spellButtons:AddChild(addSpellButton)

    local resetSpellButton = AceGUI:Create("Button")
    resetSpellButton:SetText("Reset to defaults")
    resetSpellButton:SetWidth(160)
    resetSpellButton:SetCallback("OnClick", function()
        L:ResetPIRequestSpellEntriesToDefaults()
        RebuildSelectedTab()
    end)
    spellButtons:AddChild(resetSpellButton)

    local spellEntries = L:GetPIRequestSpellEntries()
    if #spellEntries == 0 then
        local emptyLabel = AceGUI:Create("Label")
        emptyLabel:SetText("No spell IDs configured right now.")
        emptyLabel:SetFullWidth(true)
        spellsGroup:AddChild(emptyLabel)
        widgets.emptySpellLabel = emptyLabel
    end

    for index, entry in ipairs(spellEntries) do
        local row = AceGUI:Create("SimpleGroup")
        row:SetFullWidth(true)
        row:SetLayout("Flow")
        spellsGroup:AddChild(row)

        local enabled = AceGUI:Create("CheckBox")
        enabled:SetLabel("")
        enabled:SetWidth(30)
        enabled:SetCallback("OnValueChanged", function(_, _, value)
            local entries = L:GetPIRequestSpellEntries()
            if entries[index] then
                entries[index].enabled = value == true
                L:SetPIRequestSpellEntries(entries)
                L:RefreshTabbedSettingsUIState()
            end
        end)
        row:AddChild(enabled)

        local spellIDBox = AceGUI:Create("EditBox")
        spellIDBox:SetLabel("")
        spellIDBox:SetWidth(110)
        spellIDBox:DisableButton(true)
        spellIDBox:SetCallback("OnEnterPressed", function(_, _, value)
            local entries = L:GetPIRequestSpellEntries()
            if not entries[index] then
                return
            end

            local numericValue = math.max(0, math.floor(tonumber(value) or 0))
            entries[index].spellID = numericValue
            L:SetPIRequestSpellEntries(entries)
            RebuildSelectedTab()
        end)
        row:AddChild(spellIDBox)

        local spellName = AceGUI:Create("Label")
        spellName:SetWidth(260)
        row:AddChild(spellName)

        local deleteButton = AceGUI:Create("Button")
        deleteButton:SetText("Delete")
        deleteButton:SetWidth(90)
        deleteButton:SetCallback("OnClick", function()
            local entries = L:GetPIRequestSpellEntries()
            table.remove(entries, index)
            L:SetPIRequestSpellEntries(entries)
            RebuildSelectedTab()
        end)
        row:AddChild(deleteButton)

        widgets.spellRows[index] = {
            enabled = enabled,
            spellIDBox = spellIDBox,
            spellName = spellName,
            deleteButton = deleteButton,
        }
    end

    widgets.allowFocus = allowFocus
    widgets.requesterName = requesterName
    widgets.acceptSpellCasts = acceptSpellCasts
    widgets.acceptWhispers = acceptWhispers
    widgets.whisperPhrase = whisperPhrase
    widgets.piDebug = piDebug
    widgets.piRespectCooldown = piRespectCooldown
    widgets.piGraceEnabled = piGraceEnabled
    widgets.piGraceSeconds = piGraceSeconds
    widgets.piRequestTimeout = piRequestTimeout
    widgets.cooldownReplyEnabled = cooldownReplyEnabled
    widgets.cooldownReplyText = cooldownReplyText
    widgets.addSpellButton = addSpellButton
    widgets.resetSpellButton = resetSpellButton
    return widgets
end

local function RefreshPISettingsWidgets(widgets)
    local enabled = IsPIActive()
    local db = L:GetDB()

    widgets.allowFocus:SetValue(db.piRequestAllowFocus ~= false)
    widgets.allowFocus:SetDisabled(not enabled)
    widgets.requesterName:SetText(tostring(db.piRequestAllowedPlayerName or ""))
    widgets.requesterName:SetDisabled(not enabled)
    widgets.acceptSpellCasts:SetValue(db.piRequestAcceptSpellCasts ~= false)
    widgets.acceptSpellCasts:SetDisabled(not enabled)
    widgets.acceptWhispers:SetValue(db.piRequestAcceptWhispers == true)
    widgets.acceptWhispers:SetDisabled(not enabled)
    widgets.whisperPhrase:SetText(tostring(db.piRequestWhisperPhrase or L.DEFAULT_PI_WHISPER_PHRASE))
    widgets.whisperPhrase:SetDisabled((not enabled) or (db.piRequestAcceptWhispers ~= true))
    widgets.piDebug:SetValue(db.debugExperimentalPI == true)
    widgets.piDebug:SetDisabled(not enabled)
    widgets.piRespectCooldown:SetValue(db.piRequestRespectCooldown ~= false)
    widgets.piRespectCooldown:SetDisabled(not enabled)
    widgets.piGraceEnabled:SetValue(db.piRequestGracePeriodEnabled == true)
    widgets.piGraceEnabled:SetDisabled((not enabled) or (db.piRequestRespectCooldown == false))
    widgets.piGraceSeconds:SetText(string.format("%.0f", tonumber(db.piRequestGracePeriodSeconds) or 5))
    widgets.piGraceSeconds:SetDisabled((not enabled) or (db.piRequestRespectCooldown == false) or (db.piRequestGracePeriodEnabled ~= true))
    widgets.piRequestTimeout:SetText(string.format("%.0f", tonumber(db.piRequestTimeoutSeconds) or 8))
    widgets.piRequestTimeout:SetDisabled(not enabled)
    widgets.cooldownReplyEnabled:SetValue(db.piRequestCooldownReplyEnabled == true)
    widgets.cooldownReplyEnabled:SetDisabled((not enabled) or (db.piRequestRespectCooldown == false))
    widgets.cooldownReplyText:SetText(tostring(db.piRequestCooldownReplyText or "Power Infusion ready in {time_left}s."))
    widgets.cooldownReplyText:SetDisabled((not enabled) or (db.piRequestRespectCooldown == false) or (db.piRequestCooldownReplyEnabled ~= true))
    widgets.addSpellButton:SetDisabled(not enabled)
    widgets.resetSpellButton:SetDisabled(not enabled)

    local spellEntries = L:GetPIRequestSpellEntries()
    for index, row in pairs(widgets.spellRows or {}) do
        local entry = spellEntries[index] or { spellID = 0, enabled = true }
        row.enabled:SetValue(entry.enabled ~= false)
        row.enabled:SetDisabled(not enabled)
        row.spellIDBox:SetText(entry.spellID > 0 and tostring(entry.spellID) or "")
        row.spellIDBox:SetDisabled(not enabled)
        row.spellName:SetText(GetResolvedSpellName(entry.spellID))
        row.deleteButton:SetDisabled(not enabled)
    end
end

local function RefreshPISoundWidgets(widgets)
    ui.RefreshSimpleSoundWidgets(widgets.receiver, "piTrack")
    ui.RefreshSimpleSoundWidgets(widgets.request, "piRequest")
    widgets.statusLabel:SetText("Power Infusion alerts: " .. tostring(IsPIActive()) .. " | TTS status: " .. tostring(L.ttsPlaybackState))
end

local function BuildPISoundTab(root)
    local widgets = {}
    widgets.receiver = ui.BuildOutputGroup(root, "piTrack", "Power Infusion Receiver Alert", "Settings for the player receiving Power Infusion.", false, TRACK_PREVIEW_CONTEXT)
    widgets.request = ui.BuildOutputGroup(root, "piRequest", "Power Infusion Request Alert", "Settings for the optional request alert when someone asks for PI.", false, REQUEST_PREVIEW_CONTEXT)

    local statusGroup = AceGUI:Create("InlineGroup")
    statusGroup:SetTitle("Current Status")
    statusGroup:SetFullWidth(true)
    statusGroup:SetLayout("List")
    root:AddChild(statusGroup)

    local statusLabel = AceGUI:Create("Label")
    statusLabel:SetFullWidth(true)
    statusGroup:AddChild(statusLabel)

    widgets.statusLabel = statusLabel
    return widgets
end

local function BuildPITextTab(root)
    return {
        track = ui.BuildTextGroup(root, "piTrack", "Power Infusion Receiver Text", "Customize the animated text shown to the Power Infusion receiver.", L.PI_TRACK_VISUAL_TOTAL, TRACK_PREVIEW_CONTEXT, "Show remaining time (15 second countdown)"),
        request = ui.BuildTextGroup(root, "piRequest", "Power Infusion Request Text", "Customize the optional text shown when someone requests Power Infusion.", L.PI_REQUEST_VISUAL_TOTAL, REQUEST_PREVIEW_CONTEXT),
    }
end

local function RefreshPITextWidgets(widgets)
    ui.RefreshTextWidgets(widgets.track, "piTrack", L.PI_TRACK_VISUAL_TOTAL, TRACK_PREVIEW_CONTEXT)
    ui.RefreshTextWidgets(widgets.request, "piRequest", L.PI_REQUEST_VISUAL_TOTAL, REQUEST_PREVIEW_CONTEXT)
end

local TAB_DEFS = {
    general = {
        text = "Overview",
        categoryKey = "main",
        isEnabled = function()
            return true
        end,
        build = BuildGeneralTab,
        refresh = RefreshGeneralWidgets,
    },
    bl_sound = {
        text = "Bloodlust Sound/TTS",
        categoryKey = "sound",
        isEnabled = IsBLActive,
        build = function(root)
            return ui.BuildOutputGroup(root, "bloodlust", "Bloodlust Sound/TTS", "Choose whether Bloodlust alerts should use a sound file or spoken text.", true)
        end,
        refresh = function(widgets)
            ui.RefreshSimpleSoundWidgets(widgets, "bloodlust")
        end,
    },
    pi_settings = {
        text = "PI Settings",
        categoryKey = "piSettings",
        isEnabled = IsPIActive,
        build = BuildPISettingsTab,
        refresh = RefreshPISettingsWidgets,
    },
    pi_sound = {
        text = "PI Sound/TTS",
        categoryKey = "piTrack",
        isEnabled = IsPIActive,
        build = BuildPISoundTab,
        refresh = RefreshPISoundWidgets,
    },
    bl_image = {
        text = "Bloodlust Image",
        categoryKey = "image",
        isEnabled = IsBLActive,
        build = function(root)
            return ui.BuildImageGroup(root, "bloodlust", "Bloodlust Image Overlay", "Customize the image shown during a Bloodlust alert.")
        end,
        refresh = function(widgets)
            ui.RefreshImageWidgets(widgets, "bloodlust")
        end,
    },
    pi_image = {
        text = "PI Image",
        isEnabled = IsPIActive,
        build = function(root)
            return ui.BuildImageGroup(root, "piTrack", "Power Infusion Image Overlay", "Customize the image shown for the Power Infusion receiver alert.")
        end,
        refresh = function(widgets)
            ui.RefreshImageWidgets(widgets, "piTrack")
        end,
    },
    bl_text = {
        text = "Bloodlust Text",
        categoryKey = "text",
        isEnabled = IsBLActive,
        build = function(root)
            return ui.BuildTextGroup(root, "bloodlust", "Bloodlust Text Overlay", "Customize the animated text shown during a Bloodlust alert.", L.BLOODLUST_VISUAL_TOTAL, nil, "Show remaining time (40 second countdown)")
        end,
        refresh = function(widgets)
            ui.RefreshTextWidgets(widgets, "bloodlust", L.BLOODLUST_VISUAL_TOTAL)
        end,
    },
    pi_text = {
        text = "PI Text",
        categoryKey = "piRequest",
        isEnabled = IsPIActive,
        build = BuildPITextTab,
        refresh = RefreshPITextWidgets,
    },
}

local TAB_ORDER = {
    "general",
    "bl_sound",
    "bl_image",
    "bl_text",
    "pi_settings",
    "pi_sound",
    "pi_image",
    "pi_text",
}

local CATEGORY_TAB_IDS = {}

for _, tabId in ipairs(TAB_ORDER) do
    local tabDef = TAB_DEFS[tabId]
    if tabDef and tabDef.categoryKey and not CATEGORY_TAB_IDS[tabDef.categoryKey] then
        CATEGORY_TAB_IDS[tabDef.categoryKey] = tabId
    end
end

local function IsTabEnabled(tabId)
    local tabDef = TAB_DEFS[tabId]
    if not tabDef then
        return false
    end
    if type(tabDef.isEnabled) ~= "function" then
        return true
    end

    return tabDef.isEnabled() ~= false
end

local function BuildTabs()
    local tabs = {}

    -- Keep the full tab list stable and disable unavailable sections instead of
    -- rebuilding the whole UI tree away. This preserves scroll/context better.
    for _, tabId in ipairs(TAB_ORDER) do
        local tabDef = TAB_DEFS[tabId]
        local enabled = IsTabEnabled(tabId)
        tabs[#tabs + 1] = {
            value = tabId,
            text = tabDef.text or tabId,
            disabled = not enabled,
        }
    end

    return tabs
end

RefreshCurrentTab = function()
    if not tabSettings.widgets.current then
        return
    end

    local tabDef = TAB_DEFS[tabSettings.selectedTab]
    if not tabDef or type(tabDef.refresh) ~= "function" then
        return
    end

    tabDef.refresh(tabSettings.widgets.current)
end

RefreshTabsAndCurrentView = function()
    if tabSettings.tabGroup then
        tabSettings.tabGroup:SetTabs(BuildTabs())
    end

    if tabSettings.selectedTab and not IsTabEnabled(tabSettings.selectedTab) then
        if tabSettings.tabGroup then
            tabSettings.tabGroup:SelectTab(DEFAULT_TAB_ID)
        else
            tabSettings.selectedTab = DEFAULT_TAB_ID
        end
        return
    end

    RefreshCurrentTab()
end

SelectTab = function(tabId)
    if not tabSettings.tabGroup then
        return
    end

    if tabSettings.selectingTab then
        return
    end

    if not IsTabEnabled(tabId) then
        return
    end

    if tabSettings.selectedTab and tabSettings.selectedTab ~= tabId then
        -- Leaving a tab should clean up live previews/edit states so hidden frames
        -- do not keep animating or intercepting input after navigation.
        if L.IsSlashAlertTestActive and L:IsSlashAlertTestActive() and L.StopSlashAlertTests then
            L:StopSlashAlertTests()
        end
        if L.IsOverlayPreviewActive and L:IsOverlayPreviewActive() and L.StopOverlayPreview then
            L:StopOverlayPreview()
        end
        if L.IsTextOverlayPreviewActive and L:IsTextOverlayPreviewActive() and L.StopTextOverlayPreview then
            L:StopTextOverlayPreview()
        end
        if L.StopPreviewAudioIfPossible then
            L:StopPreviewAudioIfPossible()
        end
        if L.IsEditModeEnabled and L:IsEditModeEnabled() then
            L:SetEditMode(false)
        end
        if L.IsTextEditModeEnabled and L:IsTextEditModeEnabled() then
            L:SetTextEditMode(false)
        end
    end

    tabSettings.selectingTab = true
    tabSettings.selectedTab = tabId
    tabSettings.tabGroup:ReleaseChildren()

    local container = AceGUI:Create("SimpleGroup")
    container:SetFullWidth(true)
    container:SetFullHeight(true)
    container:SetLayout("Fill")
    tabSettings.tabGroup:AddChild(container)

    local root = CreateScrollRoot()
    root:SetFullWidth(true)
    root:SetFullHeight(true)
    container:AddChild(root)
    tabSettings.currentRoot = root

    local tabDef = TAB_DEFS[tabId]
    tabSettings.widgets.current = tabDef and tabDef.build and tabDef.build(root) or nil

    RefreshCurrentTab()
    local restoreScrollValue = tabSettings.pendingScrollValue
    local focusSpellIndex = tabSettings.pendingFocusSpellIndex
    C_Timer.After(0, function()
        if root and root.DoLayout then
            root:DoLayout()
        end
        if root and root.FixScroll then
            root:FixScroll()
        end
        if restoreScrollValue and root and root.SetScroll then
            root:SetScroll(restoreScrollValue)
            if root.FixScroll then
                root:FixScroll()
            end
        end
        if focusSpellIndex and tabSettings.widgets.current and tabSettings.widgets.current.spellRows then
            local row = tabSettings.widgets.current.spellRows[focusSpellIndex]
            if row and row.spellIDBox and row.spellIDBox.editbox and row.spellIDBox.editbox.SetFocus then
                row.spellIDBox.editbox:SetFocus()
                row.spellIDBox.editbox:HighlightText()
            end
        end
        tabSettings.pendingScrollValue = nil
        tabSettings.pendingFocusSpellIndex = nil
    end)
    tabSettings.selectingTab = false
end

function L:RefreshTabbedSettingsUIState()
    RefreshTabsAndCurrentView()
end

function L:RequestSettingsRefresh(_reason, _profileKey)
    self:NotifySettingsChanged(_reason, _profileKey)
end

function L:IsTabbedSettingsActive()
    return tabSettings.canvas and tabSettings.canvas:IsShown()
end

function L:GetSettingsCategoryTabId(categoryKey)
    if type(categoryKey) ~= "string" or categoryKey == "" then
        return nil
    end

    return CATEGORY_TAB_IDS[categoryKey]
end

function L:OpenSettingsCategory(categoryKey)
    self:OpenTabbedSettingsCategory(self:GetSettingsCategoryTabId(categoryKey) or categoryKey)
end

function L:OpenMainSettingsCategory()
    self:OpenSettingsCategory("main")
end

function L:OpenImageSettingsCategory()
    self:OpenSettingsCategory("image")
end

function L:OpenTextSettingsCategory()
    self:OpenSettingsCategory("text")
end

function L:OpenPITrackSettingsCategory()
    self:OpenSettingsCategory("piTrack")
end

function L:OpenPIRequestSettingsCategory()
    self:OpenSettingsCategory("piRequest")
end

function L:OpenPISettingsCategory()
    self:OpenSettingsCategory("piSettings")
end

local function ResolveOpenTabID(requestedTabId)
    local tabId = requestedTabId or tabSettings.selectedTab or DEFAULT_TAB_ID
    if not TAB_DEFS[tabId] then
        tabId = DEFAULT_TAB_ID
    end

    if IsTabEnabled(tabId) then
        return tabId
    end

    if tabSettings.selectedTab and TAB_DEFS[tabSettings.selectedTab] and IsTabEnabled(tabSettings.selectedTab) then
        return tabSettings.selectedTab
    end

    return DEFAULT_TAB_ID
end

function L:QueueSettingsCategoryOpen(tabId)
    tabSettings.pendingOpenTabId = ResolveOpenTabID(tabId)
end

function L:OpenQueuedSettingsCategoryIfPossible()
    local pendingTabId = tabSettings.pendingOpenTabId
    if not pendingTabId then
        return false
    end

    if InCombatLockdown and InCombatLockdown() then
        return false
    end

    tabSettings.pendingOpenTabId = nil
    self:OpenTabbedSettingsCategory(pendingTabId)
    return true
end

function L:OpenTabbedSettingsCategory(tabId)
    tabId = ResolveOpenTabID(tabId)

    if InCombatLockdown and InCombatLockdown() then
        self:QueueSettingsCategoryOpen(tabId)
        self:Print("Die Einstellungen koennen im Kampf nicht geoeffnet werden. Sie werden nach dem Kampf geoeffnet.")
        return false
    end

    if not tabSettings.category then
        self:CreateTabbedSettingsPanels()
    end

    if tabSettings.category and Settings and Settings.OpenToCategory then
        Settings.OpenToCategory(tabSettings.category:GetID())
        C_Timer.After(0, function()
            EnsureSettingsCloseHooks()
            -- Delay selection until Blizzard has actually opened the category;
            -- selecting immediately can race the category creation/open flow.
            if tabSettings.tabGroup then
                tabSettings.tabGroup:SelectTab(tabId)
            else
                tabSettings.selectedTab = tabId
            end
        end)
        return true
    end

    return false
end

function L:CreateTabbedSettingsPanels()
    if tabSettings.canvas then
        return
    end

    local canvas = CreateFrame("Frame")
    canvas.name = "Lustomizer"
    canvas:SetScript("OnShow", function()
        if tabSettings.root then
            return
        end

        -- Build the AceGUI tree lazily. Blizzard may recreate/show/hide this canvas
        -- multiple times and we only want to allocate widgets while visible.
        local root = AceGUI:Create("SimpleGroup")
        root.frame:SetParent(canvas)
        root.frame:ClearAllPoints()
        root.frame:SetPoint("TOPLEFT", canvas, "TOPLEFT", 10, -10)
        root.frame:SetPoint("BOTTOMRIGHT", canvas, "BOTTOMRIGHT", -10, 10)
        root:SetFullWidth(true)
        root:SetFullHeight(true)
        root:SetLayout("Fill")
        root.frame:Show()
        tabSettings.root = root

        local tabGroup = AceGUI:Create("TabGroup")
        tabGroup:SetFullWidth(true)
        tabGroup:SetFullHeight(true)
        tabGroup:SetLayout("Fill")
        tabGroup:SetTabs(BuildTabs())
        tabGroup:SetCallback("OnGroupSelected", function(_, _, group)
            SelectTab(group)
        end)
        root:AddChild(tabGroup)
        tabSettings.tabGroup = tabGroup
        tabGroup:SelectTab(tabSettings.selectedTab or DEFAULT_TAB_ID)
    end)
    canvas:SetScript("OnHide", function()
        -- Release the temporary widget tree on hide so reopening always reflects the
        -- latest runtime state and does not keep stale references alive.
        if L.StopImagePreviewIfSettingsHidden then
            L:StopImagePreviewIfSettingsHidden()
        end
        if L.StopTextPreviewIfSettingsHidden then
            L:StopTextPreviewIfSettingsHidden()
        end
        if L.StopPreviewAudioIfPossible then
            L:StopPreviewAudioIfPossible()
        end
        if L.StopImageEditModeIfSettingsHidden then
            L:StopImageEditModeIfSettingsHidden("bloodlust")
            L:StopImageEditModeIfSettingsHidden("piTrack")
        end
        if L.StopTextEditModeIfSettingsHidden then
            L:StopTextEditModeIfSettingsHidden("bloodlust")
            L:StopTextEditModeIfSettingsHidden("piTrack")
            L:StopTextEditModeIfSettingsHidden("piRequest")
        end
        if tabSettings.root then
            AceGUI:Release(tabSettings.root)
            tabSettings.currentRoot = nil
            tabSettings.root = nil
            tabSettings.tabGroup = nil
            tabSettings.widgets.current = nil
        end
    end)

    tabSettings.canvas = canvas
    tabSettings.category = Settings.RegisterCanvasLayoutCategory(canvas, "Lustomizer")
    Settings.RegisterAddOnCategory(tabSettings.category)
    EnsureSettingsCloseHooks()
end
