-- Nested Settings UI for Dynamic Profiles
-- Hierarchy:
--   Dynamic Profiles   (overview / latest changes)
--     Profiles         (dynamic list: Activate / Edit / Delete)
--     AddOn Sets       (checkbox set manager; BAL bridge when present)
--     Broker Config    (LibDataBroker text format)
-- Editor is a modal overlay (not a Settings subcategory).

local addonName, DP = ...
DP = DP or _G.DynamicProfiles or {}

local ACCENT = { r = 0.35, g = 0.55, b = 0.85 }
local ACTIVE_BG = { r = 0.10, g = 0.18, b = 0.28, a = 0.95 }
local ACTIVE_BORDER = { r = 0.35, g = 0.65, b = 0.95, a = 1 }
local CARD_BG = { r = 0.08, g = 0.09, b = 0.12, a = 0.85 }
local CARD_BORDER = { r = 0.25, g = 0.30, b = 0.40, a = 1 }
local ROW_BG = { r = 0.12, g = 0.14, b = 0.18, a = 0.9 }

local rootCategory
local profilesCategory
local addonSetsCategory
local brokerCategory
local editorPanel
local profilesIndexPanel
local addonSetsPanel
local brokerConfigPanel

local layoutPopup
local layoutPopupButtons = {}
local setPickerPopup
local setPickerButtons = {}

local OpenEditorForCreate
local OpenEditorForProfile
local ConfirmDeleteProfile
local RefreshProfilesList
local HideEditorOverlay
local OpenAddonSetsCategory

-------------------------------------------------------------------------------
-- Helpers
-------------------------------------------------------------------------------

local function ASP(msg)
    if DP.Print then DP.Print(msg) else print("Dynamic Profiles: " .. tostring(msg)) end
end

local function GetAddonVersion()
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata(addonName, "Version") or "?"
    end
    return GetAddOnMetadata and (GetAddOnMetadata(addonName, "Version") or "?") or "?"
end

local function GetAddonAuthor()
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata(addonName, "Author") or "TeoDaTank"
    end
    return "TeoDaTank"
end

local function MakeFont(parent, template, text, r, g, b)
    local fs = parent:CreateFontString(nil, "ARTWORK", template or "GameFontHighlight")
    if text then fs:SetText(text) end
    if r then fs:SetTextColor(r, g, b) end
    return fs
end

local function MakeCard(parent, width, height)
    local card = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    card:SetSize(width, height)
    card:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    card:SetBackdropColor(CARD_BG.r, CARD_BG.g, CARD_BG.b, CARD_BG.a)
    card:SetBackdropBorderColor(CARD_BORDER.r, CARD_BORDER.g, CARD_BORDER.b, CARD_BORDER.a)
    return card
end

local function MakeButton(parent, width, height, label)
    local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btn:SetSize(width, height)
    btn:SetText(label)
    return btn
end

local function GetLayoutNames()
    if not C_EditMode or not C_EditMode.GetLayouts then
        if C_AddOns and C_AddOns.LoadAddOn then
            pcall(C_AddOns.LoadAddOn, "Blizzard_EditMode")
        elseif LoadAddOn then
            pcall(LoadAddOn, "Blizzard_EditMode")
        end
    end
    local names = {}
    local layouts = C_EditMode and C_EditMode.GetLayouts and C_EditMode.GetLayouts()
    if layouts and layouts.layouts then
        for _, layoutInfo in pairs(layouts.layouts) do
            if layoutInfo.layoutName then
                table.insert(names, layoutInfo.layoutName)
            end
        end
    end
    table.sort(names)
    return names
end

local function HideLayoutPopup()
    if layoutPopup then layoutPopup:Hide() end
end

local function ShowLayoutPopup(anchorButton, targetEditBox)
    if not targetEditBox then return end
    if layoutPopup and layoutPopup:IsShown() and layoutPopup.anchorButton == anchorButton then
        HideLayoutPopup()
        return
    end
    if not layoutPopup then
        layoutPopup = CreateFrame("Frame", "DynamicProfilesLayoutPopup", UIParent, "BackdropTemplate")
        layoutPopup:SetSize(220, 220)
        layoutPopup:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
            edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
            tile = true, tileSize = 16, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 },
        })
        layoutPopup:SetFrameStrata("TOOLTIP")
        local close = CreateFrame("Button", nil, layoutPopup, "UIPanelCloseButton")
        close:SetPoint("TOPRIGHT", -3, -3)
        close:SetScript("OnClick", HideLayoutPopup)
        local scroll = CreateFrame("ScrollFrame", nil, layoutPopup, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 10, -10)
        scroll:SetPoint("BOTTOMRIGHT", -30, 10)
        local content = CreateFrame("Frame", nil, scroll)
        content:SetSize(180, 400)
        scroll:SetScrollChild(content)
        layoutPopup.content = content
    end
    layoutPopup:ClearAllPoints()
    layoutPopup:SetPoint("TOPLEFT", anchorButton, "BOTTOMLEFT", 0, -2)
    layoutPopup.anchorButton = anchorButton
    for _, btn in ipairs(layoutPopupButtons) do btn:Hide() end
    local layouts = GetLayoutNames()
    if #layouts == 0 then
        ASP("No Edit Mode layouts found. Open Edit Mode once and save a layout.")
    end
    local y = 0
    for i, name in ipairs(layouts) do
        local btn = layoutPopupButtons[i]
        if not btn then
            btn = CreateFrame("Button", nil, layoutPopup.content, "GameMenuButtonTemplate")
            btn:SetSize(180, 20)
            layoutPopupButtons[i] = btn
        end
        btn:SetPoint("TOPLEFT", 0, -y)
        btn:SetText(name)
        btn:SetScript("OnClick", function()
            targetEditBox:SetText(name)
            HideLayoutPopup()
        end)
        btn:Show()
        y = y + 22
    end
    layoutPopup.content:SetHeight(math.max(y, 20))
    layoutPopup:Show()
end

local function OpenCategory(category)
    if not category or not Settings or not Settings.OpenToCategory then return end
    local id = category.GetID and category:GetID() or category.ID
    if id ~= nil then
        Settings.OpenToCategory(id)
    else
        Settings.OpenToCategory(category)
    end
end

local function ProfileSubText(profile)
    return string.format("%s  •  Layout: %s  •  Scale: %s",
        profile.resolution or "?",
        profile.editLayoutBase or "?",
        profile.scale and tostring(profile.scale) or "default")
end

local function IsAutoProfile(profile)
    if not profile or not profile.resolution or not DP.GetAutoProfileID then return false end
    return DP.GetAutoProfileID(profile.resolution) == profile.id
end

local function RefreshAfterProfileChange()
    if RefreshProfilesList then RefreshProfilesList() end
    if DP.UpdatePickerButton then DP.UpdatePickerButton() end
end

HideEditorOverlay = function()
    HideLayoutPopup()
    if editorPanel then editorPanel:Hide() end
end

-------------------------------------------------------------------------------
-- Delete confirmation
-------------------------------------------------------------------------------

StaticPopupDialogs["DYNAMICPROFILES_DELETE_PROFILE"] = {
    text = "Delete profile \"%s\"?\nThis cannot be undone.",
    button1 = YES,
    button2 = NO,
    OnAccept = function(_, data)
        local id = data and data.id
        if not id or not DP.DeleteProfile then return end
        local pname = data.name or id
        DP.DeleteProfile(id)
        ASP("Deleted profile '" .. tostring(pname) .. "'.")
        if editorPanel and (editorPanel.editingProfileID == id or editorPanel.profileID == id) then
            if editorPanel.ClearForm then editorPanel.ClearForm() end
            HideEditorOverlay()
        end
        RefreshAfterProfileChange()
    end,
    timeout = 0,
    whileDead = 1,
    hideOnEscape = 1,
    preferredIndex = 3,
}

ConfirmDeleteProfile = function(profileID)
    if not profileID or not DP.GetProfile then return end
    local profile = DP.GetProfile(profileID)
    if not profile then return end
    StaticPopup_Show(
        "DYNAMICPROFILES_DELETE_PROFILE",
        profile.name or profile.id,
        nil,
        { id = profile.id, name = profile.name or profile.id }
    )
end

-------------------------------------------------------------------------------
-- Overview canvas
-------------------------------------------------------------------------------

local function BuildOverviewCanvas()
    local panel = CreateFrame("Frame", "DynamicProfilesOverviewPanel")
    panel:Hide()
    panel.name = "Dynamic Profiles"

    local title = MakeFont(panel, "GameFontNormalHuge", "Dynamic Profiles", 1, 1, 1)
    title:SetPoint("TOPLEFT", 20, -20)

    local meta = MakeFont(panel, "GameFontHighlight", "", 0.75, 0.8, 0.9)
    meta:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
    panel.meta = meta

    local statusCard = MakeCard(panel, 640, 70)
    statusCard:SetPoint("TOPLEFT", 20, -70)
    local statusTitle = MakeFont(statusCard, "GameFontNormal", "Current Status", ACCENT.r, ACCENT.g, ACCENT.b)
    statusTitle:SetPoint("TOPLEFT", 14, -12)
    local statusBody = MakeFont(statusCard, "GameFontHighlight", "", 0.9, 0.9, 0.9)
    statusBody:SetPoint("TOPLEFT", 14, -32)
    statusBody:SetJustifyH("LEFT")
    panel.statusBody = statusBody

    local changesCard = MakeCard(panel, 640, 220)
    changesCard:SetPoint("TOPLEFT", statusCard, "BOTTOMLEFT", 0, -14)
    local changesTitle = MakeFont(changesCard, "GameFontNormal", "Latest Changes", ACCENT.r, ACCENT.g, ACCENT.b)
    changesTitle:SetPoint("TOPLEFT", 14, -12)
    local changesBody = MakeFont(changesCard, "GameFontHighlightSmall",
        "|cffffffffv2.1.5  (latest)|r\n" ..
        "• LittleWigs is its own pack now — no more hiding inside BigWigs on the reload list\n\n" ..
        "|cffffffffRecently|r\n" ..
        "• Disable All leaves DP standing — a lock now guards it in the AddOn List (v2.1.4)\n" ..
        "• Big reload lists collapse packs — BigWigs (+12 dependencies) instead of spam (v2.1.3)\n" ..
        "• A red * flags AddOn changes waiting for /reload — hover for details (v2.1.2)\n" ..
        "• Data broker certified with Titan Panel & Bazooka (v2.1.0–2.1.1)\n" ..
        "• AddOn Sets — checkbox manager, BAL support, native Sets button (v2.0.0)",
        0.85, 0.88, 0.92)
    changesBody:SetPoint("TOPLEFT", 14, -34)
    changesBody:SetJustifyH("LEFT")
    changesBody:SetSpacing(2)

    local guideCard = MakeCard(panel, 640, 150)
    guideCard:SetPoint("TOPLEFT", changesCard, "BOTTOMLEFT", 0, -14)
    local guideTitle = MakeFont(guideCard, "GameFontNormal", "Quick Start", ACCENT.r, ACCENT.g, ACCENT.b)
    guideTitle:SetPoint("TOPLEFT", 14, -12)
    local guideBody = MakeFont(guideCard, "GameFontHighlightSmall",
        "1. Open |cffffffffProfiles|r to create PC / TV / Deck profiles\n" ..
        "2. Open |cffffffffAddOn Sets|r to toggle addons into named sets\n" ..
        "3. Assign a set on each profile (or None)\n" ..
        "4. Mark one profile as |cffffffffAutomatic for this resolution|r\n" ..
        "5. Use |cffffffffActivate|r or the on-screen picker to switch",
        0.85, 0.88, 0.92)
    guideBody:SetPoint("TOPLEFT", 14, -34)
    guideBody:SetJustifyH("LEFT")
    guideBody:SetSpacing(3)

    local pickerCheck = CreateFrame("CheckButton", nil, panel, "ChatConfigCheckButtonTemplate")
    pickerCheck:SetPoint("TOPLEFT", guideCard, "BOTTOMLEFT", -4, -16)
    if pickerCheck.Text then
        pickerCheck.Text:SetText("Show on-screen profile picker")
    end
    panel.pickerCheck = pickerCheck
    pickerCheck:SetScript("OnClick", function(self)
        if DP.SetPickerEnabled then DP.SetPickerEnabled(self:GetChecked()) end
    end)

    local muteCheck = CreateFrame("CheckButton", nil, panel, "ChatConfigCheckButtonTemplate")
    muteCheck:SetPoint("TOPLEFT", pickerCheck, "BOTTOMLEFT", 0, -4)
    if muteCheck.Text then
        muteCheck.Text:SetText("Mute chat log output (Dynamic Profiles only)")
    end
    panel.muteCheck = muteCheck
    muteCheck:SetScript("OnClick", function(self)
        if DP.SetMuteChat then DP.SetMuteChat(self:GetChecked()) end
    end)

    local openProfiles = MakeButton(panel, 160, 28, "Open Profiles")
    openProfiles:SetPoint("LEFT", pickerCheck, "RIGHT", 220, 0)
    openProfiles:SetScript("OnClick", function()
        OpenCategory(profilesCategory)
    end)

    panel:SetScript("OnShow", function()
        local version = GetAddonVersion()
        local author = GetAddonAuthor()
        meta:SetText(string.format("v%s  •  by %s  •  /dp", version, author))

        local profile, mode, res = nil, "none", "?"
        if DP.ResolveActiveProfile then
            profile, mode, res = DP.ResolveActiveProfile()
        end
        local modeLabel = (mode == "manual" and "Manual") or (mode == "auto" and "Auto") or "None"
        local profileLabel = profile and (profile.name or profile.id) or "—"
        statusBody:SetText(string.format(
            "Mode: |cffffffff%s|r    Profile: |cffffffff%s|r    Resolution: |cffffffff%s|r",
            modeLabel, profileLabel, tostring(res)))

        local db = DP.GetDB and DP.GetDB()
        pickerCheck:SetChecked(db and db.picker and db.picker.enabled ~= false)
        muteCheck:SetChecked(db and db.muteChat == true)
    end)

    return panel
end

-------------------------------------------------------------------------------
-- Profiles list canvas (dynamic)
-------------------------------------------------------------------------------

local function CreateProfileRow(parent)
    local row = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    row:SetSize(600, 56)
    row:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    row:SetBackdropColor(ROW_BG.r, ROW_BG.g, ROW_BG.b, ROW_BG.a)
    row:SetBackdropBorderColor(CARD_BORDER.r, CARD_BORDER.g, CARD_BORDER.b, 1)

    row.nameFS = MakeFont(row, "GameFontNormal", "", 1, 1, 1)
    row.nameFS:SetPoint("TOPLEFT", 12, -8)
    row.subFS = MakeFont(row, "GameFontHighlightSmall", "", 0.75, 0.8, 0.85)
    row.subFS:SetPoint("BOTTOMLEFT", 12, 10)

    row.activateBtn = MakeButton(row, 80, 22, "Activate")
    row.editBtn = MakeButton(row, 60, 22, "Edit")
    row.deleteBtn = MakeButton(row, 70, 22, "Delete")
    row.deleteBtn:SetPoint("RIGHT", -10, 0)
    row.editBtn:SetPoint("RIGHT", row.deleteBtn, "LEFT", -6, 0)
    row.activateBtn:SetPoint("RIGHT", row.editBtn, "LEFT", -6, 0)

    return row
end

local function BindProfileRow(row, profile, isActiveRow)
    row.profileID = profile.id
    local badges = {}
    -- Active state is shown by the highlighted card + disabled button; no ACTIVE text badge.
    if IsAutoProfile(profile) then table.insert(badges, "|cff66ccffAUTO|r") end
    local badgeText = (#badges > 0) and ("  " .. table.concat(badges, " ")) or ""
    row.nameFS:SetText((profile.name or "Profile") .. badgeText)
    row.subFS:SetText(ProfileSubText(profile))

    if isActiveRow then
        -- Transparent so the outer activeCard is the only visible box.
        row:SetBackdropColor(0, 0, 0, 0)
        row:SetBackdropBorderColor(0, 0, 0, 0)
        row.activateBtn:Disable()
        row.activateBtn:SetText("Active")
    else
        row:SetBackdropColor(ROW_BG.r, ROW_BG.g, ROW_BG.b, ROW_BG.a)
        row:SetBackdropBorderColor(CARD_BORDER.r, CARD_BORDER.g, CARD_BORDER.b, 1)
        row.activateBtn:Enable()
        row.activateBtn:SetText("Activate")
    end

    row.activateBtn:SetScript("OnClick", function()
        if DP.SelectProfile then
            DP.SelectProfile(profile.id, true)
        end
        RefreshAfterProfileChange()
    end)
    row.editBtn:SetScript("OnClick", function()
        OpenEditorForProfile(profile.id)
    end)
    row.deleteBtn:SetScript("OnClick", function()
        ConfirmDeleteProfile(profile.id)
    end)
end

local function BuildProfilesIndexCanvas()
    local panel = CreateFrame("Frame", "DynamicProfilesProfilesIndexPanel")
    panel:Hide()
    panel.name = "Profiles"

    local title = MakeFont(panel, "GameFontNormalLarge", "Profiles", 1, 1, 1)
    title:SetPoint("TOPLEFT", 20, -20)

    local subtitle = MakeFont(panel, "GameFontHighlight",
        "Activate, edit, or delete profiles. Changes appear here immediately.", 0.75, 0.8, 0.9)
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)

    local newBtn = MakeButton(panel, 140, 30, "New Profile")
    newBtn:SetPoint("TOPLEFT", 20, -60)
    newBtn:SetScript("OnClick", function()
        OpenEditorForCreate()
    end)

    local hint = MakeFont(panel, "GameFontHighlightSmall",
        "Edit opens a popup editor; the list stays underneath.",
        0.7, 0.75, 0.85)
    hint:SetPoint("LEFT", newBtn, "RIGHT", 16, 0)

    -- Active section (single highlighted card)
    local activeLabel = MakeFont(panel, "GameFontNormal", "Active Profile", ACCENT.r, ACCENT.g, ACCENT.b)
    activeLabel:SetPoint("TOPLEFT", 20, -104)

    local activeCard = CreateFrame("Frame", nil, panel, "BackdropTemplate")
    activeCard:SetSize(640, 72)
    activeCard:SetPoint("TOPLEFT", 20, -124)
    activeCard:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    activeCard:SetBackdropColor(ACTIVE_BG.r, ACTIVE_BG.g, ACTIVE_BG.b, ACTIVE_BG.a)
    activeCard:SetBackdropBorderColor(ACTIVE_BORDER.r, ACTIVE_BORDER.g, ACTIVE_BORDER.b, ACTIVE_BORDER.a)
    panel.activeCard = activeCard

    local activeEmpty = MakeFont(activeCard, "GameFontHighlight",
        "No active profile. Create one or Activate a saved profile.", 0.65, 0.7, 0.75)
    activeEmpty:SetPoint("LEFT", 16, 0)
    panel.activeEmpty = activeEmpty

    local activeRow = CreateProfileRow(activeCard)
    activeRow:SetPoint("TOPLEFT", 8, -8)
    activeRow:SetPoint("TOPRIGHT", -8, -8)
    activeRow:SetHeight(56)
    activeRow:Hide()
    panel.activeRow = activeRow

    -- Other profiles
    local listLabel = MakeFont(panel, "GameFontNormal", "All Profiles", ACCENT.r, ACCENT.g, ACCENT.b)
    listLabel:SetPoint("TOPLEFT", 20, -212)

    local listCard = MakeCard(panel, 640, 300)
    listCard:SetPoint("TOPLEFT", 20, -232)

    local scroll = CreateFrame("ScrollFrame", nil, listCard, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 10, -12)
    scroll:SetPoint("BOTTOMRIGHT", -30, 12)
    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(580, 10)
    scroll:SetScrollChild(content)
    panel.listContent = content
    panel.rows = {}

    local function Refresh()
        local profiles = DP.GetSortedProfiles and DP.GetSortedProfiles() or {}
        local activeProfile, mode = nil, "none"
        if DP.ResolveActiveProfile then
            activeProfile, mode = DP.ResolveActiveProfile()
        end
        local activeID = activeProfile and activeProfile.id

        if activeProfile then
            activeEmpty:Hide()
            BindProfileRow(activeRow, activeProfile, true)
            local modeBadge = (mode == "manual" and "Manual") or (mode == "auto" and "Auto") or ""
            local name = activeProfile.name or "Profile"
            local autoPart = IsAutoProfile(activeProfile) and "  |cff66ccffAUTO|r" or ""
            local modePart = (modeBadge ~= "") and ("  |cffaaaaaa(" .. modeBadge .. ")|r") or ""
            activeRow.nameFS:SetText(name .. autoPart .. modePart)
            activeRow:Show()
        else
            activeRow:Hide()
            activeEmpty:Show()
        end

        for _, row in ipairs(panel.rows) do row:Hide() end

        local y = 0
        local shown = 0
        if #profiles == 0 then
            if not panel.emptyLabel then
                panel.emptyLabel = MakeFont(content, "GameFontHighlight",
                    "No profiles yet. Click New Profile to create one.", 0.6, 0.65, 0.7)
                panel.emptyLabel:SetPoint("TOPLEFT", 8, -8)
            end
            panel.emptyLabel:Show()
            content:SetHeight(40)
            return
        end
        if panel.emptyLabel then panel.emptyLabel:Hide() end

        for _, data in ipairs(profiles) do
            if data.id ~= activeID then
                shown = shown + 1
                local row = panel.rows[shown]
                if not row then
                    row = CreateProfileRow(content)
                    panel.rows[shown] = row
                end
                BindProfileRow(row, data, false)
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", 4, -y)
                row:SetWidth(580)
                row:Show()
                y = y + 64
            end
        end

        if shown == 0 and activeID then
            if not panel.onlyActiveLabel then
                panel.onlyActiveLabel = MakeFont(content, "GameFontHighlight",
                    "No other profiles. Create another with New Profile.", 0.6, 0.65, 0.7)
                panel.onlyActiveLabel:SetPoint("TOPLEFT", 8, -8)
            end
            panel.onlyActiveLabel:Show()
            content:SetHeight(40)
        else
            if panel.onlyActiveLabel then panel.onlyActiveLabel:Hide() end
            content:SetHeight(math.max(y, 40))
        end
    end

    RefreshProfilesList = Refresh
    panel:SetScript("OnShow", Refresh)
    panel.Refresh = Refresh
    return panel
end

-------------------------------------------------------------------------------
-- Shared New / Edit editor (modal overlay)
-------------------------------------------------------------------------------

local function CreateLabeledEditBox(parent, label, width, height)
    local box = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    box:SetSize(width, height or 20)
    box:SetAutoFocus(false)
    local lbl = MakeFont(parent, "GameFontNormal", label, 0.85, 0.88, 0.92)
    lbl:SetPoint("BOTTOMLEFT", box, "TOPLEFT", 0, 2)
    return box, lbl
end

local function BuildEditorOverlay()
    -- Full-screen click blocker (sibling of the dialog, not its parent)
    local blocker = CreateFrame("Frame", "DynamicProfilesEditorBlocker", UIParent)
    blocker:SetAllPoints(UIParent)
    blocker:SetFrameStrata("FULLSCREEN_DIALOG")
    blocker:SetFrameLevel(90)
    blocker:EnableMouse(true)
    blocker:Hide()

    local dim = blocker:CreateTexture(nil, "BACKGROUND")
    dim:SetAllPoints()
    dim:SetColorTexture(0, 0, 0, 0.55)

    local panel = CreateFrame("Frame", "DynamicProfilesEditorFrame", UIParent, "BackdropTemplate")
    panel:SetSize(660, 470)
    panel:SetPoint("CENTER")
    panel:SetFrameStrata("FULLSCREEN_DIALOG")
    panel:SetFrameLevel(100)
    panel:EnableMouse(true)
    panel:SetBackdrop({
        bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
        edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
        tile = true, tileSize = 32, edgeSize = 32,
        insets = { left = 8, right = 8, top = 8, bottom = 8 },
    })
    panel:Hide()

    -- Esc closes via UISpecialFrames
    tinsert(UISpecialFrames, "DynamicProfilesEditorFrame")

    panel.blocker = blocker
    panel.editingProfileID = nil
    panel.profileID = nil

    local title = MakeFont(panel, "GameFontNormalLarge", "New Profile", 1, 1, 1)
    title:SetPoint("TOPLEFT", 24, -20)
    panel.titleFS = title

    local subtitle = MakeFont(panel, "GameFontHighlightSmall",
        "Fill in the fields below, then Save.", 0.7, 0.75, 0.85)
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
    panel.subtitleFS = subtitle

    local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -4, -4)
    closeBtn:SetScript("OnClick", HideEditorOverlay)

    local formTop = -52

    local nameInput = CreateLabeledEditBox(panel, "Profile Name", 220, 20)
    panel.nameInput = nameInput

    local resInput = CreateLabeledEditBox(panel, "Resolution hint (e.g. 1920x1080)", 140, 20)
    panel.resInput = resInput

    local useCurrent = MakeButton(panel, 110, 22, "Use Current")
    useCurrent:SetScript("OnClick", function()
        if DP.GetCurrentResolution then
            resInput:SetText(DP.GetCurrentResolution())
            resInput:ClearFocus()
        end
    end)

    local autoCheck = CreateFrame("CheckButton", nil, panel, "ChatConfigCheckButtonTemplate")
    if autoCheck.Text then autoCheck.Text:SetText("Automatic for this resolution") end
    panel.autoResCheck = autoCheck

    local layoutInput = CreateLabeledEditBox(panel, "Edit Mode Layout Name", 260, 20)
    panel.baseLayoutInput = layoutInput
    local pickLayout = MakeButton(panel, 110, 22, "Pick Layout")
    pickLayout:SetScript("OnClick", function()
        ShowLayoutPopup(pickLayout, layoutInput)
    end)

    local scaleSlider = CreateFrame("Slider", nil, panel, "OptionsSliderTemplate")
    scaleSlider:SetSize(200, 17)
    scaleSlider:SetMinMaxValues(0.65, 1.15)
    scaleSlider:SetValueStep(0.01)
    scaleSlider:SetObeyStepOnDrag(true)
    local scaleText = MakeFont(panel, "GameFontNormal", "UI Scale: 1.00", 0.85, 0.88, 0.92)
    scaleSlider:SetScript("OnValueChanged", function(_, value)
        scaleText:SetText(string.format("UI Scale: %.2f", value))
    end)
    panel.scaleSlider = scaleSlider

    local suppressCheck = CreateFrame("CheckButton", nil, panel, "ChatConfigCheckButtonTemplate")
    if suppressCheck.Text then suppressCheck.Text:SetText("Suppress 'layout applied' chat messages") end
    panel.suppressCheck = suppressCheck

    local setLabel = MakeFont(panel, "GameFontNormal", "AddOn Set", 0.85, 0.88, 0.92)
    local setValueFS = MakeFont(panel, "GameFontHighlight", "None", 1, 1, 1)
    panel.selectedSetName = nil
    panel.setValueFS = setValueFS

    local pickSetBtn = MakeButton(panel, 110, 22, "Pick Set")
    local clearSetBtn = MakeButton(panel, 70, 22, "None")
    local manageSetsBtn = MakeButton(panel, 110, 22, "Manage Sets")

    local function SetSelectedSetName(name)
        panel.selectedSetName = name
        if name and name ~= "" then
            local source = DP.AddonSets and DP.AddonSets.GetSource and DP.AddonSets.GetSource(name)
            local suffix = (source == "local" and DP.AddonSets.UsingBAL and DP.AddonSets.UsingBAL())
                and " (Local)" or ""
            setValueFS:SetText(name .. suffix)
        else
            setValueFS:SetText("None")
        end
    end
    panel.SetSelectedSetName = SetSelectedSetName

    local function HideSetPicker()
        if setPickerPopup then setPickerPopup:Hide() end
    end

    local function ShowSetPicker()
        if setPickerPopup and setPickerPopup:IsShown() then
            HideSetPicker()
            return
        end
        if not setPickerPopup then
            setPickerPopup = CreateFrame("Frame", "DynamicProfilesSetPickerPopup", UIParent, "BackdropTemplate")
            setPickerPopup:SetSize(240, 220)
            setPickerPopup:SetBackdrop({
                bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
                edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
                tile = true, tileSize = 16, edgeSize = 16,
                insets = { left = 4, right = 4, top = 4, bottom = 4 },
            })
            setPickerPopup:SetFrameStrata("TOOLTIP")
            local scroll = CreateFrame("ScrollFrame", nil, setPickerPopup, "UIPanelScrollFrameTemplate")
            scroll:SetPoint("TOPLEFT", 10, -10)
            scroll:SetPoint("BOTTOMRIGHT", -30, 10)
            local content = CreateFrame("Frame", nil, scroll)
            content:SetSize(200, 400)
            scroll:SetScrollChild(content)
            setPickerPopup.content = content
        end
        setPickerPopup:ClearAllPoints()
        setPickerPopup:SetPoint("TOPLEFT", pickSetBtn, "BOTTOMLEFT", 0, -2)
        for _, btn in ipairs(setPickerButtons) do btn:Hide() end

        local entries = { { name = nil, label = "None" } }
        if DP.AddonSets and DP.AddonSets.GetPickerEntries then
            for _, e in ipairs(DP.AddonSets.GetPickerEntries()) do
                table.insert(entries, e)
            end
        end
        local y = 0
        for i, entry in ipairs(entries) do
            local btn = setPickerButtons[i]
            if not btn then
                btn = CreateFrame("Button", nil, setPickerPopup.content, "GameMenuButtonTemplate")
                btn:SetSize(190, 20)
                setPickerButtons[i] = btn
            end
            btn:SetPoint("TOPLEFT", 0, -y)
            btn:SetText(entry.label or entry.name or "None")
            btn:SetScript("OnClick", function()
                SetSelectedSetName(entry.name)
                HideSetPicker()
            end)
            btn:Show()
            y = y + 22
        end
        setPickerPopup.content:SetHeight(math.max(y, 20))
        setPickerPopup:Show()
    end

    pickSetBtn:SetScript("OnClick", ShowSetPicker)
    clearSetBtn:SetScript("OnClick", function()
        SetSelectedSetName(nil)
        HideSetPicker()
    end)
    manageSetsBtn:SetScript("OnClick", function()
        HideEditorOverlay()
        HideSetPicker()
        OpenAddonSetsCategory()
    end)

    local saveBtn = MakeButton(panel, 130, 28, "Save / Update")
    local applyBtn = MakeButton(panel, 110, 28, "Apply Now")
    local deleteBtn = MakeButton(panel, 100, 28, "Delete")
    local clearBtn = MakeButton(panel, 110, 28, "Clear Form")
    local cancelBtn = MakeButton(panel, 90, 28, "Cancel")
    panel.deleteButton = deleteBtn

    cancelBtn:SetScript("OnClick", HideEditorOverlay)

    local function LayoutWidgets()
        local top = formTop
        nameInput:ClearAllPoints(); nameInput:SetPoint("TOPLEFT", 28, top - 40)
        resInput:ClearAllPoints(); resInput:SetPoint("TOPLEFT", 28, top - 90)
        useCurrent:ClearAllPoints(); useCurrent:SetPoint("LEFT", resInput, "RIGHT", 10, 0)
        autoCheck:ClearAllPoints(); autoCheck:SetPoint("LEFT", useCurrent, "RIGHT", 12, 0)
        layoutInput:ClearAllPoints(); layoutInput:SetPoint("TOPLEFT", 28, top - 140)
        pickLayout:ClearAllPoints(); pickLayout:SetPoint("LEFT", layoutInput, "RIGHT", 10, 0)
        scaleSlider:ClearAllPoints(); scaleSlider:SetPoint("TOPLEFT", 420, top - 140)
        scaleText:ClearAllPoints(); scaleText:SetPoint("TOP", scaleSlider, "BOTTOM", 0, -4)
        suppressCheck:ClearAllPoints(); suppressCheck:SetPoint("TOPLEFT", 24, top - 190)
        setLabel:ClearAllPoints(); setLabel:SetPoint("TOPLEFT", 28, top - 240)
        setValueFS:ClearAllPoints(); setValueFS:SetPoint("TOPLEFT", 28, top - 262)
        pickSetBtn:ClearAllPoints(); pickSetBtn:SetPoint("LEFT", setValueFS, "RIGHT", 16, 0)
        clearSetBtn:ClearAllPoints(); clearSetBtn:SetPoint("LEFT", pickSetBtn, "RIGHT", 6, 0)
        manageSetsBtn:ClearAllPoints(); manageSetsBtn:SetPoint("LEFT", clearSetBtn, "RIGHT", 6, 0)
        saveBtn:ClearAllPoints(); saveBtn:SetPoint("BOTTOMLEFT", 24, 20)
        applyBtn:ClearAllPoints(); applyBtn:SetPoint("LEFT", saveBtn, "RIGHT", 8, 0)
        clearBtn:ClearAllPoints(); clearBtn:SetPoint("LEFT", applyBtn, "RIGHT", 8, 0)
        deleteBtn:ClearAllPoints(); deleteBtn:SetPoint("LEFT", clearBtn, "RIGHT", 8, 0)
        cancelBtn:ClearAllPoints(); cancelBtn:SetPoint("BOTTOMRIGHT", -24, 20)
        deleteBtn:SetShown(panel.editingProfileID ~= nil)
    end

    local function ClearForm()
        panel.editingProfileID = nil
        panel.profileID = nil
        nameInput:SetText("")
        resInput:SetText(DP.GetCurrentResolution and DP.GetCurrentResolution() or "")
        layoutInput:SetText("")
        scaleSlider:SetValue(tonumber(GetCVar("uiScale")) or 1.0)
        suppressCheck:SetChecked(false)
        autoCheck:SetChecked(false)
        SetSelectedSetName(nil)
        title:SetText("New Profile")
        subtitle:SetText("Fill in the fields below, then Save.")
        LayoutWidgets()
    end

    local function LoadProfile(profile)
        if not profile then return end
        panel.editingProfileID = profile.id
        panel.profileID = profile.id
        nameInput:SetText(profile.name or "")
        resInput:SetText(profile.resolution or "")
        layoutInput:SetText(profile.editLayoutBase or "")
        scaleSlider:SetValue(profile.scale or (tonumber(GetCVar("uiScale")) or 1.0))
        suppressCheck:SetChecked(profile.suppressChat or false)
        SetSelectedSetName(profile.addonSetName)
        local autoID = DP.GetAutoProfileID and DP.GetAutoProfileID(profile.resolution)
        autoCheck:SetChecked(autoID == profile.id)
        title:SetText("Edit Profile")
        subtitle:SetText("Editing  •  " .. (profile.name or profile.id))
        LayoutWidgets()
    end

    panel.ClearForm = ClearForm
    panel.LoadProfile = LoadProfile

    saveBtn:SetScript("OnClick", function()
        local name = nameInput:GetText() or ""
        local res = resInput:GetText() or ""
        local baseLayout = layoutInput:GetText() or ""
        local scale = tonumber(string.format("%.2f", scaleSlider:GetValue()))
        local suppress = suppressCheck:GetChecked() and true or false
        local makeAuto = autoCheck:GetChecked() and true or false

        if name == "" then
            ASP("Please enter a profile name (e.g. PC or TV).")
            return
        end
        if baseLayout == "" then
            ASP("Please enter an Edit Mode layout name.")
            return
        end
        if res == "" and DP.GetCurrentResolution then
            res = DP.GetCurrentResolution()
        end

        local profile
        if panel.editingProfileID and DP.GetProfile(panel.editingProfileID) then
            profile = DP.GetProfile(panel.editingProfileID)
        else
            profile = DP.CreateEmptyProfile()
            panel.editingProfileID = profile.id
            panel.profileID = profile.id
        end

        profile.name = name
        profile.resolution = res
        profile.editLayoutBase = baseLayout
        profile.scale = scale
        profile.suppressChat = suppress
        profile.addonSetName = panel.selectedSetName
        profile.addonSet = nil

        if makeAuto then
            DP.SetAutoForResolution(res, profile.id)
        else
            local currentAuto = DP.GetAutoProfileID(res)
            if currentAuto == profile.id then
                DP.SetAutoForResolution(res, nil)
            end
        end

        ASP("Saved profile '" .. name .. "'.")
        RefreshAfterProfileChange()
        HideEditorOverlay()
    end)

    applyBtn:SetScript("OnClick", function()
        if panel.editingProfileID then
            DP.SelectProfile(panel.editingProfileID, true)
            RefreshAfterProfileChange()
        elseif DP.EvaluateProfileState then
            DP.EvaluateProfileState(true)
            RefreshAfterProfileChange()
        end
    end)

    clearBtn:SetScript("OnClick", function()
        ClearForm()
    end)

    deleteBtn:SetScript("OnClick", function()
        local id = panel.editingProfileID or panel.profileID
        if id then ConfirmDeleteProfile(id) end
    end)

    panel:SetScript("OnShow", function(self)
        blocker:Show()
        blocker:SetFrameLevel(90)
        self:SetFrameLevel(100)
        self:Raise()
    end)

    panel:SetScript("OnHide", function()
        HideLayoutPopup()
        if setPickerPopup then setPickerPopup:Hide() end
        blocker:Hide()
    end)

    LayoutWidgets()
    return panel
end

OpenEditorForCreate = function()
    if not editorPanel then return end
    editorPanel.ClearForm()
    editorPanel:Show()
end

OpenEditorForProfile = function(profileID)
    if not editorPanel or not profileID then return end
    local profile = DP.GetProfile and DP.GetProfile(profileID)
    if profile then
        editorPanel.LoadProfile(profile)
    else
        editorPanel.ClearForm()
    end
    editorPanel:Show()
end

DP.OpenEditorForCreate = function() OpenEditorForCreate() end
DP.OpenEditorForProfile = function(id) OpenEditorForProfile(id) end

-------------------------------------------------------------------------------
-- AddOn Sets Settings canvas
-------------------------------------------------------------------------------

local function BuildAddonSetsCanvas()
    local panel = CreateFrame("Frame", "DynamicProfilesAddonSetsPanel")
    panel:Hide()
    panel.name = "AddOn Sets"

    local title = MakeFont(panel, "GameFontNormalLarge", "AddOn Sets", 1, 1, 1)
    title:SetPoint("TOPLEFT", 20, -20)

    local subtitle = MakeFont(panel, "GameFontHighlight", "", 0.75, 0.8, 0.9)
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
    panel.subtitle = subtitle

    local balBanner = MakeCard(panel, 640, 56)
    balBanner:SetPoint("TOPLEFT", 20, -60)
    balBanner:SetBackdropColor(0.12, 0.16, 0.22, 0.95)
    balBanner:SetBackdropBorderColor(ACCENT.r, ACCENT.g, ACCENT.b, 1)
    local balText = MakeFont(balBanner, "GameFontHighlight",
        "Sets are managed by BetterAddonList. Use Addon List to edit members.",
        0.9, 0.92, 0.95)
    balText:SetPoint("LEFT", 14, 0)
    local openAddonList = MakeButton(balBanner, 130, 24, "Open Addon List")
    openAddonList:SetPoint("RIGHT", -12, 0)
    openAddonList:SetScript("OnClick", function()
        if ShowUIPanel and AddonList then
            ShowUIPanel(AddonList)
        elseif AddonList then
            AddonList:Show()
        end
    end)
    balBanner:Hide()
    panel.balBanner = balBanner

    -- Left: set list
    local listCard = MakeCard(panel, 200, 360)
    listCard:SetPoint("TOPLEFT", 20, -130)
    local listTitle = MakeFont(listCard, "GameFontNormal", "Sets", ACCENT.r, ACCENT.g, ACCENT.b)
    listTitle:SetPoint("TOPLEFT", 10, -10)

    local newSetBtn = MakeButton(listCard, 80, 22, "New")
    newSetBtn:SetPoint("TOPRIGHT", -10, -8)
    local deleteSetBtn = MakeButton(listCard, 80, 22, "Delete")
    deleteSetBtn:SetPoint("TOP", newSetBtn, "BOTTOM", 0, -4)
    local renameSetBtn = MakeButton(listCard, 80, 22, "Rename")
    renameSetBtn:SetPoint("TOP", deleteSetBtn, "BOTTOM", 0, -4)

    local setScroll = CreateFrame("ScrollFrame", nil, listCard, "UIPanelScrollFrameTemplate")
    setScroll:SetPoint("TOPLEFT", 8, -90)
    setScroll:SetPoint("BOTTOMRIGHT", -28, 10)
    local setContent = CreateFrame("Frame", nil, setScroll)
    setContent:SetSize(160, 10)
    setScroll:SetScrollChild(setContent)
    panel.setRows = {}
    panel.selectedSet = nil

    -- Right: addon checkboxes
    local editCard = MakeCard(panel, 420, 360)
    editCard:SetPoint("TOPLEFT", listCard, "TOPRIGHT", 16, 0)
    local editTitle = MakeFont(editCard, "GameFontNormal", "AddOns in set", ACCENT.r, ACCENT.g, ACCENT.b)
    editTitle:SetPoint("TOPLEFT", 12, -10)
    editTitle:SetWidth(396)
    editTitle:SetJustifyH("LEFT")
    panel.editTitle = editTitle

    local searchBox = CreateFrame("EditBox", nil, editCard, "SearchBoxTemplate")
    searchBox:SetSize(396, 20)
    searchBox:SetPoint("TOPLEFT", 12, -36)
    searchBox:SetAutoFocus(false)
    panel.searchBox = searchBox

    local selectAllBtn = MakeButton(editCard, 80, 20, "All")
    selectAllBtn:SetPoint("TOPLEFT", 12, -62)
    local selectNoneBtn = MakeButton(editCard, 80, 20, "None")
    selectNoneBtn:SetPoint("LEFT", selectAllBtn, "RIGHT", 6, 0)
    local saveSetBtn = MakeButton(editCard, 100, 22, "Save Set")
    saveSetBtn:SetPoint("TOPRIGHT", -12, -60)

    local addonScroll = CreateFrame("ScrollFrame", nil, editCard, "UIPanelScrollFrameTemplate")
    addonScroll:SetPoint("TOPLEFT", 8, -92)
    addonScroll:SetPoint("BOTTOMRIGHT", -28, 10)
    local addonContent = CreateFrame("Frame", nil, addonScroll)
    addonContent:SetSize(370, 10)
    addonScroll:SetScrollChild(addonContent)
    panel.addonChecks = {}
    panel.groupHeaders = {}
    panel.checkByName = {}
    panel.working = {}
    panel.workingFor = false

    local function PromptSetName(titleText, defaultText, onAccept)
        StaticPopupDialogs["DYNAMICPROFILES_SET_NAME_SETTINGS"] = StaticPopupDialogs["DYNAMICPROFILES_SET_NAME_SETTINGS"] or {
            text = "%s",
            button1 = ACCEPT,
            button2 = CANCEL,
            hasEditBox = 1,
            maxLetters = 64,
            OnAccept = function(self, data)
                local text = self.EditBox and self.EditBox:GetText() or ""
                text = text:gsub("^%s*(.-)%s*$", "%1")
                if data and data.callback then data.callback(text) end
            end,
            OnShow = function(self, data)
                if self.EditBox then
                    self.EditBox:SetText((data and data.default) or "")
                    self.EditBox:HighlightText()
                    self.EditBox:SetFocus()
                end
            end,
            EditBoxOnEnterPressed = function(self)
                local parent = self:GetParent()
                local data = parent.data
                local text = self:GetText() or ""
                text = text:gsub("^%s*(.-)%s*$", "%1")
                if data and data.callback then data.callback(text) end
                parent:Hide()
            end,
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            preferredIndex = 3,
        }
        StaticPopup_Show("DYNAMICPROFILES_SET_NAME_SETTINGS", titleText, nil, {
            default = defaultText or "",
            callback = onAccept,
        })
    end

    -- Membership is tracked in panel.working (name -> true) rather than on the
    -- checkboxes themselves, so that collapsed groups don't lose their state.
    local function CollectCheckedMembers()
        local members = {}
        for name in pairs(panel.working or {}) do
            table.insert(members, name)
        end
        table.sort(members, function(a, b) return a:lower() < b:lower() end)
        return members
    end

    local function GetCollapsedStore()
        local db = DP.GetDB and DP.GetDB()
        if not db then return {} end
        db.addonSetsUI = db.addonSetsUI or {}
        db.addonSetsUI.collapsed = db.addonSetsUI.collapsed or {}
        return db.addonSetsUI.collapsed
    end

    -- Forward declaration so factory closures can trigger a rebuild.
    local RefreshAddonChecks

    local function MakeAddonCheck(idx)
        local check = CreateFrame("CheckButton", nil, addonContent, "ChatConfigCheckButtonTemplate")
        check:SetSize(24, 24)
        check.label = MakeFont(check, "GameFontHighlightSmall", "", 0.9, 0.9, 0.9)
        check.label:SetPoint("LEFT", check, "RIGHT", 4, 0)
        check.label:SetWidth(300)
        check.label:SetJustifyH("LEFT")
        check:SetScript("OnClick", function(self)
            if not self.addonName then return end
            panel.working = panel.working or {}
            panel.working[self.addonName] = self:GetChecked() and true or nil
            if self.header and self.header.UpdateCount then self.header.UpdateCount() end
        end)
        panel.addonChecks[idx] = check
        return check
    end

    local function MakeGroupHeader(idx)
        local header = CreateFrame("Button", nil, addonContent, "BackdropTemplate")
        header:SetSize(360, 22)
        header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
        header:SetBackdropColor(0.16, 0.19, 0.24, 0.95)
        header.arrow = MakeFont(header, "GameFontNormalLarge", "-", ACCENT.r, ACCENT.g, ACCENT.b)
        header.arrow:SetPoint("LEFT", 6, 0)
        header.arrow:SetWidth(14)
        header.arrow:SetJustifyH("CENTER")
        header.check = CreateFrame("CheckButton", nil, header, "ChatConfigCheckButtonTemplate")
        header.check:SetSize(22, 22)
        header.check:SetPoint("LEFT", 22, 0)
        header.label = MakeFont(header, "GameFontNormal", "", 1, 1, 1)
        header.label:SetPoint("LEFT", header.check, "RIGHT", 4, 0)
        header.label:SetWidth(280)
        header.label:SetJustifyH("LEFT")
        header.UpdateCount = function()
            local checked, total = 0, 0
            for _, info in ipairs(header.groupAddons or {}) do
                total = total + 1
                if panel.working and panel.working[info.name] then checked = checked + 1 end
            end
            header.label:SetText((header.groupTitle or "")
                .. "  |cff888888(" .. checked .. "/" .. total .. ")|r")
            header.check:SetChecked(total > 0 and checked == total)
        end
        header:SetScript("OnClick", function()
            local store = GetCollapsedStore()
            store[header.groupKey] = (not store[header.groupKey]) or nil
            RefreshAddonChecks()
        end)
        header.check:SetScript("OnClick", function(self)
            local val = self:GetChecked() and true or nil
            panel.working = panel.working or {}
            for _, info in ipairs(header.groupAddons or {}) do
                panel.working[info.name] = val
            end
            RefreshAddonChecks()
        end)
        panel.groupHeaders[idx] = header
        return header
    end

    RefreshAddonChecks = function()
        local AS = DP.AddonSets
        if not AS then return end
        local filter = (searchBox:GetText() or ""):lower()
        local searching = filter ~= ""
        local readOnly = AS.UsingBAL() and AS.GetSource(panel.selectedSet) == "bal"

        -- Sync working state whenever the selected set changes.
        if panel.selectedSet ~= panel.workingFor then
            panel.working = {}
            if panel.selectedSet then
                for _, n in ipairs(AS.GetMembers(panel.selectedSet)) do
                    panel.working[n] = true
                end
            end
            panel.workingFor = panel.selectedSet
        end
        local working = panel.working or {}
        local collapsed = GetCollapsedStore()

        for _, c in ipairs(panel.addonChecks) do c:Hide() end
        for _, h in ipairs(panel.groupHeaders) do h:Hide() end
        wipe(panel.checkByName)

        local function matches(info)
            if not searching then return true end
            local hay = ((info.title or "") .. " " .. (info.name or "")):lower()
            return hay:find(filter, 1, true) ~= nil
        end

        local y = 0
        local checkIdx, headerIdx = 0, 0
        for _, g in ipairs(AS.GetGroupedAddOns()) do
            local visible = {}
            for _, info in ipairs(g.addons) do
                if matches(info) then table.insert(visible, info) end
            end
            if #visible > 0 then
                local isCollapsed = (not searching) and collapsed[g.key] and true or false

                headerIdx = headerIdx + 1
                local header = panel.groupHeaders[headerIdx] or MakeGroupHeader(headerIdx)
                header.groupKey = g.key
                header.groupTitle = g.title
                header.groupAddons = g.addons
                header.arrow:SetText(isCollapsed and "+" or "-")
                header.UpdateCount()
                if readOnly then header.check:Disable() else header.check:Enable() end
                header:ClearAllPoints()
                header:SetPoint("TOPLEFT", 2, -y)
                header:Show()
                y = y + 24

                if not isCollapsed then
                    for _, info in ipairs(visible) do
                        checkIdx = checkIdx + 1
                        local check = panel.addonChecks[checkIdx] or MakeAddonCheck(checkIdx)
                        check.addonName = info.name
                        check.header = header
                        check.label:SetText((info.title or info.name) .. "  |cff888888" .. info.name .. "|r")
                        check:SetChecked(working[info.name] and true or false)
                        if readOnly then check:Disable() else check:Enable() end
                        check:ClearAllPoints()
                        check:SetPoint("TOPLEFT", 24, -y)
                        check:Show()
                        panel.checkByName[info.name] = check
                        y = y + 22
                    end
                end
            end
        end

        addonContent:SetHeight(math.max(y, 40))
        saveSetBtn:SetEnabled(not readOnly and panel.selectedSet ~= nil)
        editTitle:SetText(readOnly and "AddOns in set (view only — edit in Addon List)" or "AddOns in set")
    end

    local function RefreshSetList()
        local AS = DP.AddonSets
        local usingBAL = AS and AS.UsingBAL()
        if usingBAL then
            balBanner:Show()
            listCard:ClearAllPoints()
            listCard:SetPoint("TOPLEFT", 20, -130)
            editCard:ClearAllPoints()
            editCard:SetPoint("TOPLEFT", listCard, "TOPRIGHT", 16, 0)
            subtitle:SetText("BetterAddonList detected — using BAL sets. Toggle members in the Addon List.")
        else
            balBanner:Hide()
            listCard:ClearAllPoints()
            listCard:SetPoint("TOPLEFT", 20, -70)
            editCard:ClearAllPoints()
            editCard:SetPoint("TOPLEFT", listCard, "TOPRIGHT", 16, 0)
            listCard:SetHeight(400)
            editCard:SetHeight(400)
            subtitle:SetText("Create named sets of enabled AddOns, then assign them on Profiles.")
        end
        newSetBtn:Enable()
        renameSetBtn:Enable()
        deleteSetBtn:Enable()

        for _, row in ipairs(panel.setRows) do row:Hide() end
        local entries = {}
        if usingBAL then
            for _, name in ipairs(AS.GetNames()) do
                table.insert(entries, { name = name, source = "bal", label = name })
            end
            for _, name in ipairs(AS.GetLocalNames()) do
                table.insert(entries, { name = name, source = "local", label = name .. " (Local)" })
            end
        else
            for _, name in ipairs(AS and AS.GetLocalNames() or {}) do
                table.insert(entries, { name = name, source = "local", label = name })
            end
        end

        if #entries == 0 then
            if not panel.emptySets then
                panel.emptySets = MakeFont(setContent, "GameFontHighlightSmall", "No sets yet.", 0.6, 0.65, 0.7)
                panel.emptySets:SetPoint("TOPLEFT", 4, -4)
            end
            panel.emptySets:Show()
            panel.selectedSet = nil
            RefreshAddonChecks()
            return
        end
        if panel.emptySets then panel.emptySets:Hide() end

        local y = 0
        local stillSelected = false
        for i, entry in ipairs(entries) do
            local row = panel.setRows[i]
            if not row then
                row = CreateFrame("Button", nil, setContent, "BackdropTemplate")
                row:SetSize(150, 22)
                row:SetBackdrop({
                    bgFile = "Interface\\Buttons\\WHITE8X8",
                    edgeFile = "Interface\\Buttons\\WHITE8X8",
                    edgeSize = 1,
                })
                row.label = MakeFont(row, "GameFontHighlightSmall", "", 1, 1, 1)
                row.label:SetPoint("LEFT", 6, 0)
                row.label:SetWidth(140)
                row.label:SetJustifyH("LEFT")
                panel.setRows[i] = row
            end
            row.setName = entry.name
            row.source = entry.source
            row.label:SetText(entry.label)
            local selected = panel.selectedSet == entry.name
            if selected then stillSelected = true end
            if selected then
                row:SetBackdropColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.45)
                row:SetBackdropBorderColor(ACCENT.r, ACCENT.g, ACCENT.b, 1)
            else
                row:SetBackdropColor(0.1, 0.12, 0.15, 0.9)
                row:SetBackdropBorderColor(0.3, 0.35, 0.4, 1)
            end
            row:SetScript("OnClick", function()
                panel.selectedSet = entry.name
                RefreshSetList()
                RefreshAddonChecks()
            end)
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", 2, -y)
            row:Show()
            y = y + 24
        end
        if not stillSelected then
            panel.selectedSet = entries[1].name
            -- recolor first
            if panel.setRows[1] then
                panel.setRows[1]:SetBackdropColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.45)
                panel.setRows[1]:SetBackdropBorderColor(ACCENT.r, ACCENT.g, ACCENT.b, 1)
            end
        end
        setContent:SetHeight(math.max(y, 40))
        RefreshAddonChecks()
    end

    newSetBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS then return end
        PromptSetName("New AddOn set name:", "", function(name)
            if name == "" then return end
            if AS.Exists(name) then
                ASP("A set named '" .. name .. "' already exists.")
                return
            end
            if AS.Create(name) then
                if not AS.UsingBAL() then
                    -- Seed from what's currently enabled so the set is a full snapshot;
                    -- the user then unchecks what this profile should turn off.
                    AS.SaveFromCurrentEnabled(name)
                end
                panel.selectedSet = name
                RefreshSetList()
                if AS.UsingBAL() then
                    ASP("Created BAL set '" .. name .. "'. Enable addons in Addon List, then Save there.")
                else
                    ASP("Created set '" .. name .. "' from your enabled addons. Uncheck what this set should disable, then Save Set.")
                end
            else
                ASP("Could not create set.")
            end
        end)
    end)

    renameSetBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS or not panel.selectedSet then return end
        local old = panel.selectedSet
        PromptSetName("Rename set:", old, function(name)
            if name == "" or name == old then return end
            if AS.Rename(old, name) then
                panel.selectedSet = name
                for _, profile in ipairs(DP.GetSortedProfiles() or {}) do
                    if profile.addonSetName == old then
                        profile.addonSetName = name
                    end
                end
                RefreshSetList()
                ASP("Renamed set to '" .. name .. "'.")
            else
                ASP("Could not rename (name in use?).")
            end
        end)
    end)

    deleteSetBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS or not panel.selectedSet then return end
        local name = panel.selectedSet
        StaticPopupDialogs["DYNAMICPROFILES_DELETE_SET_SETTINGS"] = {
            text = "Delete AddOn set \"%s\"?",
            button1 = YES,
            button2 = NO,
            OnAccept = function(_, data)
                if data and data.name and DP.AddonSets then
                    DP.AddonSets.Delete(data.name)
                    for _, profile in ipairs(DP.GetSortedProfiles() or {}) do
                        if profile.addonSetName == data.name then
                            profile.addonSetName = nil
                        end
                    end
                    panel.selectedSet = nil
                    RefreshSetList()
                    ASP("Deleted set '" .. data.name .. "'.")
                end
            end,
            timeout = 0,
            whileDead = 1,
            hideOnEscape = 1,
            preferredIndex = 3,
        }
        StaticPopup_Show("DYNAMICPROFILES_DELETE_SET_SETTINGS", name, nil, { name = name })
    end)

    saveSetBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS or not panel.selectedSet then return end
        if AS.UsingBAL() and AS.GetSource(panel.selectedSet) == "bal" then
            ASP("Edit BAL sets from the Addon List, then Save there.")
            return
        end
        AS.Save(panel.selectedSet, CollectCheckedMembers())
        ASP("Saved set '" .. panel.selectedSet .. "'.")
        RefreshAddonChecks()
    end)

    selectAllBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS or not panel.selectedSet then return end
        if AS.UsingBAL() and AS.GetSource(panel.selectedSet) == "bal" then return end
        panel.working = panel.working or {}
        for _, info in ipairs(AS.GetInstalledAddOns()) do
            panel.working[info.name] = true
        end
        RefreshAddonChecks()
    end)
    selectNoneBtn:SetScript("OnClick", function()
        local AS = DP.AddonSets
        if not AS or not panel.selectedSet then return end
        if AS.UsingBAL() and AS.GetSource(panel.selectedSet) == "bal" then return end
        panel.working = {}
        RefreshAddonChecks()
    end)

    searchBox:SetScript("OnTextChanged", function(self)
        if SearchBoxTemplate_OnTextChanged then
            pcall(SearchBoxTemplate_OnTextChanged, self)
        end
        RefreshAddonChecks()
    end)

    panel:SetScript("OnShow", RefreshSetList)
    panel.Refresh = RefreshSetList
    return panel
end

OpenAddonSetsCategory = function()
    if addonSetsCategory then
        OpenCategory(addonSetsCategory)
    elseif profilesCategory then
        OpenCategory(profilesCategory)
    end
end

-------------------------------------------------------------------------------
-- Broker Config Settings canvas
-------------------------------------------------------------------------------

local function BuildBrokerConfigCanvas()
    local panel = CreateFrame("Frame", "DynamicProfilesBrokerConfigPanel")
    panel:Hide()
    panel.name = "Broker Config"

    local title = MakeFont(panel, "GameFontNormalLarge", "Broker Config", 1, 1, 1)
    title:SetPoint("TOPLEFT", 20, -20)

    local subtitle = MakeFont(panel, "GameFontHighlight",
        "Customize the LibDataBroker text shown on Titan / ChocolateBar / Bazooka.",
        0.75, 0.8, 0.9)
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
    subtitle:SetWidth(620)
    subtitle:SetJustifyH("LEFT")

    local formatCard = MakeCard(panel, 640, 260)
    formatCard:SetPoint("TOPLEFT", 20, -70)

    local formatLabel = MakeFont(formatCard, "GameFontNormal", "Broker Text Format", ACCENT.r, ACCENT.g, ACCENT.b)
    formatLabel:SetPoint("TOPLEFT", 14, -12)

    local formatBox = CreateFrame("EditBox", nil, formatCard, "InputBoxTemplate")
    formatBox:SetSize(600, 22)
    formatBox:SetPoint("TOPLEFT", 18, -42)
    formatBox:SetAutoFocus(false)
    formatBox:SetMaxLetters(128)
    panel.formatBox = formatBox

    local help = MakeFont(formatCard, "GameFontHighlightSmall",
        "Placeholders:\n" ..
        "  |cffffffff%profile%|r    — active profile label (e.g. 4kPC or Auto: 4kPC)\n" ..
        "  |cffffffff%icon%|r       — inline broker icon (16px)\n" ..
        "  |cffffffff%shortname%|r  — DP\n" ..
        "  |cffffffff%fullname%|r   — Dynamic Profiles\n\n" ..
        "Examples:  |cffffffff%profile%|r   ·   |cffffffff%icon% %profile%|r   ·   |cffffffff%shortname%: %profile%|r\n" ..
        "Bar addons can still show/hide their own icon separately from %icon%.",
        0.85, 0.88, 0.92)
    help:SetPoint("TOPLEFT", 14, -78)
    help:SetWidth(610)
    help:SetJustifyH("LEFT")
    help:SetSpacing(2)

    local previewCard = MakeCard(panel, 640, 70)
    previewCard:SetPoint("TOPLEFT", formatCard, "BOTTOMLEFT", 0, -14)
    local previewTitle = MakeFont(previewCard, "GameFontNormal", "Preview", ACCENT.r, ACCENT.g, ACCENT.b)
    previewTitle:SetPoint("TOPLEFT", 14, -12)
    local previewBody = MakeFont(previewCard, "GameFontHighlight", "", 0.95, 0.95, 0.95)
    previewBody:SetPoint("TOPLEFT", 14, -34)
    previewBody:SetWidth(610)
    previewBody:SetJustifyH("LEFT")
    panel.previewBody = previewBody

    local function RefreshPreview()
        local draft = formatBox:GetText() or ""
        if DP.FormatBrokerText then
            local rendered = DP.FormatBrokerText(draft)
            if rendered == "" then
                previewBody:SetText("|cff888888(empty)|r")
            else
                previewBody:SetText(rendered)
            end
        else
            previewBody:SetText(draft)
        end
    end

    local function LoadSaved()
        local saved = DP.GetBrokerTextFormat and DP.GetBrokerTextFormat() or "%profile%"
        formatBox:SetText(saved)
        RefreshPreview()
    end

    local function ApplyFormat()
        local text = formatBox:GetText() or ""
        text = text:gsub("^%s*(.-)%s*$", "%1")
        if DP.SetBrokerTextFormat then
            DP.SetBrokerTextFormat(text)
        end
        LoadSaved()
    end

    local applyBtn = MakeButton(panel, 100, 28, "Apply")
    applyBtn:SetPoint("TOPLEFT", previewCard, "BOTTOMLEFT", 0, -14)
    applyBtn:SetScript("OnClick", ApplyFormat)

    local resetBtn = MakeButton(panel, 140, 28, "Reset to Default")
    resetBtn:SetPoint("LEFT", applyBtn, "RIGHT", 10, 0)
    resetBtn:SetScript("OnClick", function()
        if DP.ResetBrokerTextFormat then
            DP.ResetBrokerTextFormat()
        elseif DP.SetBrokerTextFormat then
            DP.SetBrokerTextFormat(DP.GetDefaultBrokerTextFormat and DP.GetDefaultBrokerTextFormat() or "%profile%")
        end
        LoadSaved()
    end)

    formatBox:SetScript("OnTextChanged", function(self, userInput)
        if userInput then RefreshPreview() end
    end)
    formatBox:SetScript("OnEnterPressed", function(self)
        ApplyFormat()
        self:ClearFocus()
    end)
    formatBox:SetScript("OnEscapePressed", function(self)
        LoadSaved()
        self:ClearFocus()
    end)

    panel:SetScript("OnShow", LoadSaved)
    panel.Refresh = LoadSaved
    return panel
end

-------------------------------------------------------------------------------
-- Settings registration
-------------------------------------------------------------------------------

local function RegisterSettingsTree()
    if not Settings or not Settings.RegisterCanvasLayoutCategory then
        return
    end
    if rootCategory then return end

    local overview = BuildOverviewCanvas()
    profilesIndexPanel = BuildProfilesIndexCanvas()
    DP.profilesIndexPanel = profilesIndexPanel
    addonSetsPanel = BuildAddonSetsCanvas()
    DP.addonSetsPanel = addonSetsPanel
    brokerConfigPanel = BuildBrokerConfigCanvas()
    DP.brokerConfigPanel = brokerConfigPanel

    rootCategory = Settings.RegisterCanvasLayoutCategory(overview, "Dynamic Profiles")
    Settings.RegisterAddOnCategory(rootCategory)
    DP.optionsCategory = rootCategory

    profilesCategory = Settings.RegisterCanvasLayoutSubcategory(rootCategory, profilesIndexPanel, "Profiles")
    DP.profilesCategory = profilesCategory

    addonSetsCategory = Settings.RegisterCanvasLayoutSubcategory(rootCategory, addonSetsPanel, "AddOn Sets")
    DP.addonSetsCategory = addonSetsCategory

    brokerCategory = Settings.RegisterCanvasLayoutSubcategory(rootCategory, brokerConfigPanel, "Broker Config")
    DP.brokerCategory = brokerCategory

    -- Modal editor overlay (not a Settings subcategory)
    editorPanel = BuildEditorOverlay()
    DP.newEditorPanel = editorPanel
end

local bootstrapFrame = CreateFrame("Frame")
bootstrapFrame:RegisterEvent("ADDON_LOADED")
bootstrapFrame:SetScript("OnEvent", function(self, event, name)
    if event == "ADDON_LOADED" and name == addonName then
        self:UnregisterEvent("ADDON_LOADED")
        if DP.GetDB then DP.GetDB() end
        RegisterSettingsTree()
    end
end)

function DynamicProfiles_Options_OnLoad()
    -- Registration happens on ADDON_LOADED so SavedVariables are available.
end

function DynamicProfiles_OpenOptionsPanel()
    if not rootCategory then
        if DP.GetDB then DP.GetDB() end
        RegisterSettingsTree()
    end
    OpenCategory(rootCategory)
end

function DynamicProfiles_OpenProfilesCategory()
    if not profilesCategory then
        if DP.GetDB then DP.GetDB() end
        RegisterSettingsTree()
    end
    OpenCategory(profilesCategory)
end

DP.OpenOptionsPanel = DynamicProfiles_OpenOptionsPanel
DP.OpenProfilesCategory = DynamicProfiles_OpenProfilesCategory
